threlte logo
@threlte/extras

<VirtualEnvironment>

<script>
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
  import { Checkbox, Pane } from 'svelte-tweakpane-ui'

  let debug = $state(true)
</script>

<Pane
  title="Virtual Environment"
  position="fixed"
>
  <Checkbox
    bind:value={debug}
    label="debug"
  />
</Pane>

<div>
  <Canvas>
    <Scene {debug} />
  </Canvas>
</div>

<style>
  div {
    background-color: black;
    width: 100%;
    height: 100%;
  }
</style>
<script lang="ts">
  import { injectPlugin, isInstanceOf, T, useTask } from '@threlte/core'
  import {
    Grid,
    interactivity,
    OrbitControls,
    TransformControls,
    VirtualEnvironment
  } from '@threlte/extras'
  import { DoubleSide } from 'three'

  let { debug }: { debug: boolean } = $props()

  interactivity()

  // lookAt plugin from the plugin examples
  injectPlugin<{
    lookAt?: [number, number, number]
  }>('lookAt', (args) => {
    if (!isInstanceOf(args.ref, 'Object3D') || !args.props.lookAt) return
    useTask(
      () => {
        if (!args.props.lookAt) return
        args.ref.lookAt(args.props.lookAt[0], args.props.lookAt[1], args.props.lookAt[2])
      },
      {
        autoInvalidate: false
      }
    )
    return { pluginProps: ['lookAt'] }
  })
</script>

<T.PerspectiveCamera
  makeDefault
  position={[10, 10, 10]}
>
  <OrbitControls
    autoRotate={!debug}
    autoRotateSpeed={0.15}
    enableDamping
  />
</T.PerspectiveCamera>

<Grid
  cellColor="white"
  sectionColor="white"
/>

<T.Mesh position.y={1}>
  <T.SphereGeometry />
  <T.MeshStandardMaterial
    color="white"
    roughness={0.15}
  />
</T.Mesh>

{#snippet lightformer(
  color: string,
  shape: 'circle' | 'plane',
  size: number,
  position: [number, number, number],
  visible: boolean
)}
  <T.Group {position}>
    {#snippet children({ ref })}
      {#if visible}
        <TransformControls object={ref} />
      {/if}

      <T.Mesh lookAt={[0, 0, 0]}>
        {#if shape === 'circle'}
          <T.CircleGeometry args={[size / 2]} />
        {:else}
          <T.PlaneGeometry args={[size, size]} />
        {/if}
        <T.MeshBasicMaterial
          {color}
          side={DoubleSide}
        />
      </T.Mesh>
    {/snippet}
  </T.Group>
{/snippet}

<VirtualEnvironment visible={debug}>
  {@render lightformer('#FF4F4F', 'plane', 20, [0, 0, -20], debug)}
  {@render lightformer('#FFD0CB', 'circle', 5, [0, 5, 0], debug)}
  {@render lightformer('#2223FF', 'plane', 8, [-3, 0, 4], debug)}
</VirtualEnvironment>

<VirtualEnvironment> allows you to create dynamic environment maps which can be used to light your scene and adjust reflections on your scene’s objects.

It uses a cube camera to create a cubemap of its contents and applies that cubemap texture to the scene’s environment. <VirtualEnvironment> internally creates a new scene to render the virtual environment into. The contents of <VirtualEnvironment> may be mounted and made visible by setting the visible prop to true.

Controlling Updates

By default, the cube camera updates and renders to its render target every frame. The frames prop is used to control the amount of updates that occur. If your virtual scene is static, you may only need the cube camera to update once. You can achieve this by settings frames to 1:

<VirtualEnvironment frames={1} />

This will cause the cube camera to update once and then stop its update task. If you ever need to restart the task, a restart function is available as a component export and through the children snippet.

As-A-Component-Export
<script>
  let virtualEnvironment = $state()

  $effect(() => {
    // yourDependencyHere
    virtualEnvironment?.restart()
  })
</script>

<VirtualEnvironment
  frames={1}
  bind:this={virtualEnvironment}
/>
Through-Children-Snippet
<script>
  let meshInScene = $state(true)
</script>

<VirtualEnvironment
  frames={1}
  bind:this={virtualEnvironment}
>
  {#snippet children({ restart })}
    {#if meshInScene}
      <T.Mesh>
        <T.PlaneGeometry />
      </T.Mesh>
    {/if}
  {/snippet}
</VirtualEnvironment>

Manual Updates

For cases where you want full control over when the render target is updated, use the update function available as a component export and through the children snippet.

If you use the update function, be sure to set frames to 0 to prevent the internal update task from starting automatically.

As a Component Export

Scene.svelte
<script>
  let virtualEnvironment = $state()
  $effect(() => {
    // …
    virtualEnvironment?.update()
  })
</script>

<VirtualEnvironment
  frames={0}
  bind:this={virtualEnvironment}
>
  <!-- Your scene contents here -->
</VirtualEnvironment>

Through Children Snippet

Scene.svelte
<VirtualEnvironment frames={0}>
  {#snippet children({ update })}
    <T.Mesh
      oncreate={() => {
        update()
      }}
    >
      <T.PlaneGeometry />
    </T.Mesh>
  {/snippet}
</VirtualEnvironment>

The example below is the same as the one above but it only updates the cube camera’s render target when the light formers are updated instead of every frame.

<script>
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
</script>

<div>
  <Canvas>
    <Scene />
  </Canvas>
</div>

<style>
  div {
    background-color: black;
    width: 100%;
    height: 100%;
  }
</style>
<script lang="ts">
  import { useStage, useTask, useThrelte } from '@threlte/core'
  import { WaveformMonitor } from 'svelte-tweakpane-ui'

  const { shouldRender, renderStage } = useThrelte()

  const afterRenderStage = useStage('after-render', {
    after: renderStage
  })

  let log = Array(200).fill(0)

  useTask(
    () => {
      log = update(log)
    },
    {
      autoInvalidate: false,
      stage: afterRenderStage
    }
  )

  function update(log: number[]) {
    log.shift()
    log.push(shouldRender() ? 1 : 0)
    return log
  }
</script>

<WaveformMonitor
  label="Render Activity"
  value={log}
  min={-1}
  max={2}
/>
<script lang="ts">
  import { T } from '@threlte/core'
  import {
    Grid,
    interactivity,
    OrbitControls,
    TransformControls,
    VirtualEnvironment
  } from '@threlte/extras'
  import { Checkbox, Pane } from 'svelte-tweakpane-ui'
  import { DoubleSide } from 'three'
  import RenderIndicator from './RenderIndicator.svelte'

  let debug = $state(true)

  interactivity()
</script>

<Pane
  position="fixed"
  title="Render Indicator"
>
  <Checkbox
    bind:value={debug}
    label="debug"
  />
  <RenderIndicator />
</Pane>

<T.PerspectiveCamera
  makeDefault
  position={[10, 10, 10]}
>
  <OrbitControls
    autoRotate={!debug}
    autoRotateSpeed={0.15}
    enableDamping
  />
</T.PerspectiveCamera>

<Grid
  cellColor="white"
  sectionColor="white"
/>

<T.Mesh position.y={1}>
  <T.SphereGeometry />
  <T.MeshStandardMaterial
    color="white"
    roughness={0.15}
  />
</T.Mesh>

{#snippet lightformer(
  update: () => void,
  color: string,
  shape: 'circle' | 'plane',
  size: number,
  position: [number, number, number],
  visible: boolean
)}
  <T.Group {position}>
    {#snippet children({ ref })}
      {@const lookAtCenter = () => ref.lookAt(0, 0, 0)}
      {#if visible}
        <TransformControls
          object={ref}
          oncreate={lookAtCenter}
          onobjectChange={() => {
            lookAtCenter()
            update()
          }}
        />
      {/if}

      <T.Mesh>
        {#if shape === 'circle'}
          <T.CircleGeometry args={[size / 2]} />
        {:else}
          <T.PlaneGeometry args={[size, size]} />
        {/if}
        <T.MeshBasicMaterial
          {color}
          side={DoubleSide}
        />
      </T.Mesh>
    {/snippet}
  </T.Group>
{/snippet}

<VirtualEnvironment
  frames={0}
  visible={debug}
>
  {#snippet children({ update })}
    <T.Group
      oncreate={() => {
        update()
      }}
    >
      {@render lightformer(update, '#FF4F4F', 'plane', 20, [0, 0, -20], debug)}
      {@render lightformer(update, '#FFD0CB', 'circle', 5, [0, 5, 0], debug)}
      {@render lightformer(update, '#2223FF', 'plane', 8, [-3, 0, 4], debug)}
    </T.Group>
  {/snippet}
</VirtualEnvironment>

Mixing Virtual and Real Environments

You can also mix <VirtualEnvironment> with <Environment> or <CubeEnvironment> to create a mix of “real” and virtual environments.

Scene.svelte
<VirtualEnvironment>
  <Environment
    url=""
    isBackground
  />

  <T.Mesh>
    <T.PlaneGeometry />
  </T.Mesh>
</VirtualEnvironment>
<script>
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
  import { Checkbox, Pane } from 'svelte-tweakpane-ui'
  import { Suspense } from '@threlte/extras'

  let debug = $state(true)
  let mixEnvironment = $state(true)
</script>

<Pane
  title="Virtual Environment"
  position="fixed"
>
  <Checkbox
    bind:value={debug}
    label="debug"
  />
  <Checkbox
    bind:value={mixEnvironment}
    label="Mix Environment Map"
  />
</Pane>

<div>
  <Canvas>
    <Scene
      {debug}
      {mixEnvironment}
    />
  </Canvas>
</div>

<style>
  div {
    background-color: black;
    width: 100%;
    height: 100%;
  }
</style>
<script lang="ts">
  import { injectPlugin, isInstanceOf, T, useTask } from '@threlte/core'
  import {
    Environment,
    Grid,
    interactivity,
    OrbitControls,
    TransformControls,
    VirtualEnvironment
  } from '@threlte/extras'
  import { DoubleSide } from 'three'

  let { debug, mixEnvironment }: { debug: boolean; mixEnvironment: boolean } = $props()

  interactivity()

  // lookAt plugin from the plugin examples
  injectPlugin<{
    lookAt?: [number, number, number]
  }>('lookAt', (args) => {
    if (!isInstanceOf(args.ref, 'Object3D') || !args.props.lookAt) return
    useTask(
      () => {
        if (!args.props.lookAt) return
        args.ref.lookAt(args.props.lookAt[0], args.props.lookAt[1], args.props.lookAt[2])
      },
      {
        autoInvalidate: false
      }
    )
    return { pluginProps: ['lookAt'] }
  })
</script>

<T.PerspectiveCamera
  makeDefault
  position={[10, 10, 10]}
>
  <OrbitControls
    autoRotate={!debug}
    autoRotateSpeed={0.15}
    enableDamping
  />
</T.PerspectiveCamera>

<Grid
  cellColor="white"
  sectionColor="white"
/>

<T.Mesh position.y={2}>
  <T.TorusGeometry />
  <T.MeshStandardMaterial
    color="white"
    roughness={0.4}
    metalness={1}
  />
</T.Mesh>

{#snippet lightformer(
  color: string,
  size: number,
  position: [number, number, number],
  visible: boolean
)}
  <T.Group {position}>
    {#snippet children({ ref })}
      {#if visible}
        <TransformControls object={ref} />
      {/if}

      <T.Mesh lookAt={[0, 0, 0]}>
        <T.CircleGeometry args={[size / 2]} />
        <T.MeshBasicMaterial
          {color}
          side={DoubleSide}
        />
      </T.Mesh>
    {/snippet}
  </T.Group>
{/snippet}

<VirtualEnvironment visible={debug}>
  {#if mixEnvironment}
    <Environment
      url="/textures/equirectangular/hdr/mpumalanga_veld_puresky_1k.hdr"
      isBackground
    />
  {/if}

  {@render lightformer('#FF4F4F', 20, [0, 0, -20], debug)}
  {@render lightformer('#2223FF', 8, [-3, 0, 4], debug)}
</VirtualEnvironment>

Component Signature

Props

name
type
required
default
description

far
number
no
1000
The far plane of the cube camera

frames
number
no
Infinity
Determines how many frames the internal task will run for

isBackground
boolean
no
false
Whether to set `scene.background` to the environment texture

near
number
no
0.1
The near plane of the cube camera

onupdatestart
() => void
no
Callback that is ran when the internal update task is started

onupdatestop
() => void
no
Callback that is ran when the internal update task is stopped

resolution
number
no
256
The width and height of the cube cameras render target

scene
THREE.Scene
no
useThrelte().scene
The scene that will have its environment and/or background set

visible
boolean
no
false
Whether to render the virtual environment into the scene

Exports

name
type
description

restart
() => void
Restarts the update task

update
() => void
Causes the cube camera to render to its render target