Skip to content

<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}

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>

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.

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.

Scene.svelte
<script>
let cubeCameraComponent = $state()
$effect(() => {
// …
cubeCameraComponent?.update()
})
</script>
<CubeCamera
frames={0}
bind:this={cubeCameraComponent}
>
<!-- … -->
</CubeCamera>
Scene.svelte
<CubeCamera frames={0}>
{#snippet children({ update })}
<T.Mesh oncreate{update}>
<T.BoxGeometry />
</T.Mesh>
{/snippet}
</CubeCamera>

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>

<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.

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.