@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.
<script>
let virtualEnvironment = $state()
$effect(() => {
// yourDependencyHere
virtualEnvironment?.restart()
})
</script>
<VirtualEnvironment
frames={1}
bind:this={virtualEnvironment}
/>
<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
<script>
let virtualEnvironment = $state()
$effect(() => {
// …
virtualEnvironment?.update()
})
</script>
<VirtualEnvironment
frames={0}
bind:this={virtualEnvironment}
>
<!-- Your scene contents here -->
</VirtualEnvironment>
Through Children Snippet
<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.
<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>