threlte logo
@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

Scene.svelte
<script>
  let cubeCameraComponent = $state()

  $effect(() => {
    // …
    cubeCameraComponent?.update()
  })
</script>

<CubeCamera
  frames={0}
  bind:this={cubeCameraComponent}
>
  <!-- … -->
</CubeCamera>

Through Children Snippet

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

Component Signature

<CubeCamera> extends <T . Group> and supports all its props, slot props, bindings and events.

Props

name
type
required
default
description

autoStart
boolean
no
true
whether to automatically start the render task.

background
Three.Scene['background'] | 'auto'
no
'auto'
custom background to use when rendering to the render target

far
number
no
1000
passed along to the underlying cube camera

fog
Three.Scene['fog'] | 'auto'
no
'auto'
custom fog to use when rendering to the render target

frames
number
no
Infinity
controls how many frames the render task will run for

near
number
no
0.1
passed along to the underlying cube camera

onupdatestart
() => void
no
a callback prop that is called anytime the render task is started

onupdatestop
() => void
no
a callback prop that is called anytime the render task is stopped

resolution
number
no
256
size of the render target. more resolution means more detail

Exports

name
type
description

restart
() => void
restarts the internal update task

update
() => void
causes the cube camera to update which renders to the render target