threlte logo
@threlte/extras

<View>

This is a port of drei’s <View/> component. It is used to display multiple scenes using only one canvas and one renderer. Our implemenation re-uses parts of the main context but creates new camera, scene, parent, DOM, cache and user contexts. This ensures you can use Threlte’s other components as per normal.

The following example is equivalent to three.js’s multiple elements example.

<script lang="ts">
  import { Canvas } from '@threlte/core'
  import { View } from '@threlte/extras'
  import Scene from './Scene.svelte'
  import * as THREE from 'three'
  import type { itemType } from './types'

  const items: itemType[] = []
  const geometries = [
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.SphereGeometry(0.5, 12, 8),
    new THREE.DodecahedronGeometry(0.5),
    new THREE.CylinderGeometry(0.5, 0.5, 1, 12)
  ]
  for (let i = 0; i < 40; i++) {
    // add one random mesh to each scene
    const geometry = geometries[(geometries.length * Math.random()) | 0]!

    const material = new THREE.MeshStandardMaterial({
      color: new THREE.Color().setHSL(Math.random(), 1, 0.75, THREE.SRGBColorSpace),
      roughness: 0.5,
      metalness: 0,
      flatShading: true
    })
    items.push({ dom: undefined, geometry, material })
  }
</script>

<div
  id="container"
  class="bg-white"
>
  <div
    id="content"
    class="relative z-1 h-full overflow-y-scroll"
  >
    {#each items as item, i}
      <div
        id="item"
        class="m-4 inline-block p-4 shadow-md"
      >
        <div
          bind:this={item.dom}
          class="h-[200px] w-[200px]"
        ></div>
        <div class="mt-2 text-[#888]">Scene {i + 1}</div>
      </div>
    {/each}
  </div>
  <div
    id="canvas"
    class="absolute top-0 h-full"
  >
    <Canvas>
      {#each items as item}
        <View dom={item.dom}>
          <Scene {...item} />
        </View>
      {/each}
    </Canvas>
  </div>
</div>

<style>
  div#container {
    height: 100%;
    background: white;
  }
  div#content {
    position: relative;
    z-index: 1;
    height: 100%;
    overflow-y: scroll;
  }
  div#item {
    margin: 1rem;
    display: inline-block;
    padding: 1rem;
    box-shadow:
      rgba(0, 0, 0, 0) 0px 0px 0px 0px,
      rgba(0, 0, 0, 0) 0px 0px 0px 0px,
      rgba(0, 0, 0, 0.1) 0px 4px 6px -1px,
      rgba(0, 0, 0, 0.1) 0px 2px 4px -2px;
  }
  div#item > div:first-child {
    height: 200px;
    width: 200px;
  }
  div#item > div:last-child {
    margin-top: 0.5rem;
    color: #888;
  }
</style>
<script lang="ts">
  import { T, useTask, useThrelte } from '@threlte/core'
  import { OrbitControls } from '@threlte/extras'
  import { Color } from 'three'

  let { geometry, material } = $props()

  const { scene } = useThrelte()
  let rotation = $state(0)

  scene.background = new Color(0xe0e0e0)

  useTask((delta) => {
    rotation += delta
  })
</script>

<T.PerspectiveCamera
  makeDefault
  position={[0, 0, 2]}
  fov={50}
  near={1}
  far={10}
>
  <OrbitControls
    minDistance={2}
    maxDistance={5}
    enablePan={false}
  />
</T.PerspectiveCamera>

<T.HemisphereLight args={[0xaaaaaa, 0x444444, 3]} />
<T.DirectionalLight
  args={[0xffffff, 1.5]}
  position={[1, 1, 1]}
/>

<T.Mesh rotation.y={rotation}>
  <T is={geometry} />
  <T is={material} />
</T.Mesh>
import type * as THREE from 'three'

export type geoTypes =
  | THREE.BoxGeometry
  | THREE.SphereGeometry
  | THREE.DodecahedronGeometry
  | THREE.CylinderGeometry

export type itemType = {
  dom: HTMLElement | undefined
  geometry: geoTypes
  material: THREE.MeshStandardMaterial
}

Under the hood, this component uses the renderer’s scissor-cut method. Three.js has documentation for when using scissor-cuts as the styling of your canvas has different effects on the renderering. You access the canvas to switch between the options via the useThrelte hook.

Sharing a scene across views

Pass an existing THREE.Scene via the scene prop to render the same content in multiple views instead of creating a new scene per <View>. Each <View> still owns its own camera, DOM, cache and user contexts, so you can show one scene from several angles — for example a main perspective plus a top-down minimap.

<script lang="ts">
  import { Canvas, T } from '@threlte/core'
  import { OrbitControls } from '@threlte/extras'
  import Scene from './Scene.svelte'

  let minimap = $state.raw<HTMLDivElement>()
</script>

<div id="container">
  <Canvas>
    <T.PerspectiveCamera
      makeDefault
      position={[1.6, 1.6, 3.6]}
      fov={50}
      oncreate={(ref) => ref.lookAt(0, 0, 0)}
    />

    <Scene {minimap} />

    <OrbitControls />
  </Canvas>

  <div
    bind:this={minimap}
    id="minimap"
  ></div>
</div>

<style>
  div#container {
    position: relative;
    height: 100%;
  }

  div#minimap {
    position: absolute;
    bottom: 1rem;
    right: 1rem;
    width: 25%;
    aspect-ratio: 1;
    z-index: 1;
    border: 1px solid #333;
  }
</style>
<script lang="ts">
  import { T, useThrelte } from '@threlte/core'
  import { OrbitControls, View } from '@threlte/extras'
  import { Color } from 'three'

  let { minimap } = $props()

  const { scene } = useThrelte()
  scene.background = new Color('white')
</script>

<T.HemisphereLight args={[0xaaaaaa, 0x444444, 3]} />

<T.DirectionalLight
  args={[0xffffff, 1.5]}
  position={[2, 4, 2]}
/>

{#each [0xff7eb6, 0x82cfff, 0xa7f0ba] as color, index (color)}
  <T.Mesh position.x={(index - 1) * 1.2}>
    <T.BoxGeometry args={[0.6, 0.6, 0.6]} />
    <T.MeshStandardMaterial
      {color}
      flatShading
    />
  </T.Mesh>
{/each}

<View
  dom={minimap}
  {scene}
>
  <OrbitControls autoRotate />
</View>

Component Signature

Props

name
type
required
default
description

dom
HTMLElement
no
The target DOM to scissor-cut and attach events. Does nothing if none given.

scene
THREE.Scene
no
new THREE.Scene()
An existing scene to render. If omitted, `<View>` creates a new scene and any children are added to it.