@threlte/extras
<CubeCamera>
A wrapper around three’s
CubeCamera that
exposes a renderTarget
prop. Before rendering to the render target, children
are set to invisible to exclude them from the render.
<script lang="ts">
import { Canvas } from '@threlte/core'
import Scene, { hdrs } from './Scene.svelte'
import { Checkbox, Folder, List, Pane, Slider } from 'svelte-tweakpane-ui'
import type { ListOptions } from 'svelte-tweakpane-ui'
const resolutionOptions: ListOptions<number> = {
32: 32,
64: 64,
128: 128,
256: 256,
512: 512
} as const
const environmentOptions: { [Key in keyof typeof hdrs]: Key } & { auto: 'auto' } = {
auto: 'auto',
industrial: 'industrial',
puresky: 'puresky',
workshop: 'workshop'
} as const
let hdr: keyof typeof environmentOptions = $state('auto')
let metalness = $state(1)
let resolution = $state(256)
let roughness = $state(0)
let capFrames = $state(false)
let frames = $derived(capFrames ? 3 : Infinity)
let near = $state(0.1)
</script>
<Pane
position="fixed"
title="CubeCamera"
>
<Folder title="render target">
<List
bind:value={resolution}
label="resolution"
options={resolutionOptions}
/>
<List
bind:value={hdr}
label="environment"
options={environmentOptions}
/>
<Checkbox
bind:value={capFrames}
label="cap frames"
/>
</Folder>
<Folder title="cube camera props">
<Slider
bind:value={near}
label="near"
max={15}
min={0.1}
/>
</Folder>
<Folder title="material props">
<Slider
bind:value={metalness}
max={1}
min={0}
step={0.1}
label="metalness"
/>
<Slider
bind:value={roughness}
max={1}
min={0}
step={0.1}
label="roughness"
/>
</Folder>
</Pane>
<div>
<Canvas>
<Scene
{frames}
{hdr}
{metalness}
{near}
{resolution}
{roughness}
/>
</Canvas>
</div>
<style>
div {
height: 100%;
}
</style>
<script
lang="ts"
module
>
export const hdrs = {
industrial: 'industrial_sunset_puresky_1k.hdr',
workshop: 'aerodynamics_workshop_1k.hdr',
puresky: 'mpumalanga_veld_puresky_1k.hdr'
} as const
const isHdrKey = (u: PropertyKey): u is keyof typeof hdrs => {
return u in hdrs
}
</script>
<script lang="ts">
import type { Group } from 'three'
import { CubeCamera, Environment, Grid, OrbitControls } from '@threlte/extras'
import { EquirectangularReflectionMapping } from 'three'
import { RGBELoader } from 'three/examples/jsm/Addons.js'
import { T, useLoader, useTask } from '@threlte/core'
type SceneProps = {
frames?: number
hdr?: 'auto' | keyof typeof hdrs
metalness?: number
near?: number
resolution?: number
roughness?: number
}
let {
frames = Infinity,
hdr = 'auto',
metalness = 1,
near = 0.1,
resolution = 256,
roughness = 0
}: SceneProps = $props()
const colors = ['#ff00ff', '#ffff00', '#00ffff'] as const
const increment = (2 * Math.PI) / colors.length
const radius = 3
let time = 0
const groups: Group[] = []
useTask((delta) => {
time += delta
let i = 0
for (const group of groups) {
group.position.setY(2 * Math.sin(time + i))
i += 1
}
})
const hdrPath = '/textures/equirectangular/hdr/'
const loader = useLoader(RGBELoader, {
extend(loader) {
loader.setPath(hdrPath)
}
})
const backgrounds = loader.load(hdrs, {
transform(texture) {
texture.mapping = EquirectangularReflectionMapping
return texture
}
})
</script>
<T.PerspectiveCamera
makeDefault
position={[10, 5, 10]}
fov={30}
>
<OrbitControls
enableDamping
enablePan={false}
enableZoom={false}
target.y={0.5}
autoRotate
autoRotateSpeed={0.1}
/>
</T.PerspectiveCamera>
<Environment url={`${hdrPath}shanghai_riverside_1k.hdr`} />
<Grid
position.y={-3}
sectionColor="#fff"
cellColor="#fff"
/>
{#await backgrounds then backgroundMap}
{@const background = isHdrKey(hdr) ? backgroundMap[hdr] : hdr}
{#each colors as color, i}
{@const r = increment * i}
<T.Mesh
position.x={radius * Math.cos(r)}
position.y={i}
position.z={radius * Math.sin(r)}
>
<T.MeshStandardMaterial {color} />
<T.SphereGeometry />
</T.Mesh>
{/each}
{#each Array(colors.length) as _, i}
{@const r = Math.PI + increment * i}
<T.Group
position.x={radius * Math.cos(r)}
position.z={radius * Math.sin(r)}
oncreate={(ref) => {
groups.push(ref)
}}
>
<CubeCamera
{background}
{frames}
{near}
{resolution}
>
{#snippet children({ renderTarget })}
<T.Mesh>
<T.SphereGeometry />
<T.MeshStandardMaterial
{roughness}
{metalness}
envMap={renderTarget.texture}
/>
</T.Mesh>
{/snippet}
</CubeCamera>
</T.Group>
{/each}
{/await}
Usage
The entire render target that is used by the underlying cube camera is available
through the renderTarget
snippet prop. Usually you’ll only want to use the
renderTarget.texture
.
<CubeCamera>
{#snippet children({ renderTarget })}
<T.Mesh>
<T.SphereGeometry />
<T.MeshStandardMaterial envMap={renderTarget.texture} />
</T.Mesh>
{/snippet}
</CubeCamera>
Controlling Updates
By default, frames
is set to Infinity
which means the scene is rendered to
the render target every frame. This is sometimes unnecessary especially if you
have a static scene. To improve performance, you can use the frames
prop to
control how many times the scene should be rendered.
For moving objects, let frames
default to Infinity
. If you have a static
scene, set frames
equal to the number of <CubeCamera>
s in the scene. This
will allow each one to render and then be picked up in each other’s reflection.
Manual Updates
If you want full control over updates, 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 cubeCameraComponent = $state()
$effect(() => {
// …
cubeCameraComponent?.update()
})
</script>
<CubeCamera
frames={0}
bind:this={cubeCameraComponent}
>
<!-- … -->
</CubeCamera>
Through Children Snippet
<CubeCamera frames={0}>
{#snippet children({ update })}
<T.Mesh oncreate{update}>
<T.BoxGeometry />
</T.Mesh>
{/snippet}
</CubeCamera>
Restarting the Task
If you need to restart the update task, you can do so through the restart
component export
<script>
let cubeCameraComponent = $state()
$effect(() => {
// dependencies here
cubeCameraComponent?.restart()
})
</script>
<CubeCamera
frames={1}
bind:this={cubeCameraComponent}
>
<!-- ... -->
</CubeCamera>
restart
is also available through the children
snippet.
<CubeCamerae frames={1}>
{#snippet children({ restart })}
<T.Mesh oncreate={restart}>
<!-- ... -->
</T.Mesh>
{/snippet}
</CubeCamera>
Scene Props
<CubeCamera>
accepts a background
prop that can be used to set the
background of the scene when rendering to the render target. By default the
current scene.background
is used. background
can be any valid
Scene.background.
<script>
import { Color } from 'three'
const background = new Color(0xff_00_ff)
</script>
<CubeCamera {background}>
<!-- ... -->
</CubeCamera>
The fog
prop is used the same way and accepts any valid
scene.fog. By
default scene.fog
is used.
These “scene” props are only used when rendering the scene to the underlying render target.
Callback Props
The onupdatestart
callback prop is called anytime the underlying update task
has been started.
<CubeCamera
onupdatestart={() => {
console.log('update started')
}}
>
<!-- ... -->
</CubeCamera>
The onupdatestop
callback fires anytime the update task has stopped. It is
called on restarts and when the internal counter goes over the frames
limit.
This means that if frames
is set to Infinity
, as it is by default,
onupdatestop
is never called.