@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> = {
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 = $state('auto')
let metalness = $state(1)
let resolution = $state(256)
let roughness = $state(0)
let capFrames = $state(false)
let frames = $derived(capFrames ? 3 : Infinity)
</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="texture">
<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}
{resolution}
{roughness}
/>
</Canvas>
</div>
<style>
div {
height: 100%;
}
</style>
<script
lang="ts"
context="module"
>
export const hdrs = {
industrial: '/hdr/industrial_sunset_puresky_1k.hdr',
workshop: '/hdr/aerodynamics_workshop_1k.hdr',
puresky: '/hdr/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 { Vector3Tuple } from 'three'
import { CubeCamera, Environment, OrbitControls } from '@threlte/extras'
import { EquirectangularReflectionMapping } from 'three'
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js'
import { T, useLoader, useTask } from '@threlte/core'
type SceneProps = {
frames?: number
far?: number
hdr?: 'auto' | keyof typeof hdrs
metalness?: number
near?: number
resolution?: number
roughness?: number
}
let {
frames = Infinity,
far = 1000,
hdr = 'auto',
metalness = 1,
near = 0.1,
resolution = 256,
roughness = 0
}: SceneProps = $props()
const loader = useLoader(RGBELoader)
const textures = loader.load(hdrs, {
transform(texture) {
texture.mapping = EquirectangularReflectionMapping
return texture
}
})
const colors = [0xff_00_ff, 0xff_ff_00, 0x00_ff_ff] as const
const increment = (2 * Math.PI) / colors.length
const radius = 3
let time = $state(0)
let y = $derived(2 * Math.sin(time))
useTask((delta) => {
time += delta
})
const cameraPosition: Vector3Tuple = [7, 7, 7]
</script>
<T.PerspectiveCamera
makeDefault
position={cameraPosition}
>
<OrbitControls />
</T.PerspectiveCamera>
<T.AmbientLight />
{#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.BoxGeometry />
</T.Mesh>
{/each}
<Environment
path="/hdr/"
files="shanghai_riverside_1k.hdr"
isBackground
format="hdr"
/>
{#await textures then textureRecord}
{@const background = isHdrKey(hdr) ? textureRecord[hdr] : hdr}
{#each Array(colors.length) as _, i}
{@const r = Math.PI + increment * i}
<CubeCamera
{near}
{far}
{resolution}
{background}
{frames}
position.y={y + i}
position.x={radius * Math.cos(r)}
position.z={radius * Math.sin(r)}
>
{#snippet children({ renderTarget })}
<T.Mesh>
<T.SphereGeometry />
<T.MeshStandardMaterial
{roughness}
{metalness}
envMap={renderTarget.texture}
/>
</T.Mesh>
{/snippet}
</CubeCamera>
{/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>
Other Snippet Props
The children
snippet also has access to the cube camera and a restart
function. Note that updates to the camera, render target or ref
inside the snippet block will not be reflected on the render target unless frames
is set to Infinity
or you use the restart
function. Calling restart
will force the renderer to render to the render target.
{#snippet children({ camera, ref, renderTarget, restart })}
<!-- may need to call `restart` here if updating any of the props above -->
{/snippet}
Controlling Render Count
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.
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. After rendering to the target, the original scene props are restored.
Callback Props
The onrenderstart
callback prop is called anytime the underlying render task has been started. This means that it also fires anytime the renderer has been restarted manually through the restart
component export.
<script>
const onrenderstart = () => {
console.log('render started')
}
let ref
// `onrenderstart` will be fired if `ref.restart` is called
</script>
<CubeCamera
bind:ref
onrenderstart
>
<!-- ... -->
</CubeCamera>
onrenderstart
also fires if you use the restart
children snippet prop
<CubeCamera onrenderstart>
{#snippet children({ restart })}
<T.Mesh oncreate={restart}>
<!-- ... -->
</T.Mesh>
{/snippet}
</CubeCamera>
The onrenderstop
callback fires anytime the render task stops running. It is called on restarts and when the internal counter goes over the frames
limit. This means that if frames
is set to Infinity
, onrenderstop
is never called.