threlte logo
@threlte/core

WebGPU

@threlte/core ships a webgpu export that mirrors the regular library but swaps the default renderer for WebGPURenderer, awaits renderer.init() internally, and points <T> at three/webgpu so node materials (e.g. <T.MeshPhysicalNodeMaterial />) resolve out of the box.

useThrelte from the webgpu export is also updated to have WebGPURenderer types.

App.svelte
<script>
  import Scene from './Scene.svelte'
  import { Canvas } from '@threlte/core/webgpu'
</script>

<Canvas>
  <Scene />
</Canvas>
Scene.svelte
<script>
  import { T } from '@threlte/core/webgpu'
</script>

<T.Mesh>
  <T.BoxGeometry />
  <T.MeshPhysicalNodeMaterial />
</T.Mesh>
<script lang="ts">
  import { Canvas } from '@threlte/core/webgpu'
  import Scene from './Scene.svelte'
</script>

<div>
  <Canvas>
    <Scene />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { T, useTask, useThrelte } from '@threlte/core/webgpu'
  import { OrbitControls } from '@threlte/extras'
  import Stats from 'three/addons/libs/stats.module.js'
  import * as THREE from 'three/webgpu'

  const { scene, dom, invalidate } = useThrelte()

  scene.background = new THREE.Color(0xc1c1c1)

  let geometries: THREE.BufferGeometry[] = [
    new THREE.ConeGeometry(1.0, 2.0, 3, 1),
    new THREE.BoxGeometry(2.0, 2.0, 2.0),
    new THREE.PlaneGeometry(2.0, 2, 1, 1),
    new THREE.CapsuleGeometry(),
    new THREE.CircleGeometry(1.0, 3),
    new THREE.CylinderGeometry(1.0, 1.0, 2.0, 3, 1),
    new THREE.DodecahedronGeometry(1.0, 0),
    new THREE.IcosahedronGeometry(1.0, 0),
    new THREE.OctahedronGeometry(1.0, 0),
    new THREE.PolyhedronGeometry([0, 0, 0], [0, 0, 0], 1, 0),
    new THREE.RingGeometry(1.0, 1.5, 3),
    new THREE.SphereGeometry(1.0, 3, 2),
    new THREE.TetrahedronGeometry(1.0, 0),
    new THREE.TorusGeometry(1.0, 0.5, 3, 3),
    new THREE.TorusKnotGeometry(1.0, 0.5, 20, 3, 1, 1)
  ]

  const group = new THREE.BundleGroup()

  const position = new THREE.Vector3()
  const rotation = new THREE.Euler()
  const quaternion = new THREE.Quaternion()
  const scale = new THREE.Vector3()
  const count = 3000

  function randomizeMatrix(matrix: THREE.Matrix4) {
    position.x = Math.random() * 80 - 40
    position.y = Math.random() * 80 - 40
    position.z = Math.random() * 80 - 40

    rotation.x = Math.random() * 2 * Math.PI
    rotation.y = Math.random() * 2 * Math.PI
    rotation.z = Math.random() * 2 * Math.PI

    quaternion.setFromEuler(rotation)

    const factorScale = 1
    scale.x = scale.y = scale.z = 0.35 * factorScale + Math.random() * 0.5 * factorScale

    return matrix.compose(position, quaternion, scale)
  }

  const randomizeRotationSpeed = (rotation: THREE.Euler) => {
    rotation.x = Math.random() * 0.05
    rotation.y = Math.random() * 0.05
    rotation.z = Math.random() * 0.05
    return rotation
  }

  for (let i = 0; i < count; i++) {
    const material = new THREE.MeshToonNodeMaterial({
      color: new THREE.Color(Math.random() * 0xffffff),
      side: THREE.DoubleSide
    })

    const child = new THREE.Mesh(geometries[i % geometries.length], material)
    randomizeMatrix(child.matrix)
    child.matrix.decompose(child.position, child.quaternion, child.scale)
    child.userData.rotationSpeed = randomizeRotationSpeed(new THREE.Euler())
    child.frustumCulled = false
    group.add(child)
  }

  const stats = new Stats()
  dom.appendChild(stats.dom)

  stats.begin()

  useTask(() => {
    stats.end()

    for (const child of group.children) {
      if (!child) return

      const { rotationSpeed } = child.userData

      child.rotation.set(
        child.rotation.x + rotationSpeed.x,
        child.rotation.y + rotationSpeed.y,
        child.rotation.z + rotationSpeed.z
      )
    }

    stats.begin()
  })
</script>

<T is={group} />

<T.PerspectiveCamera
  position.z={50}
  makeDefault
>
  <OrbitControls
    autoRotate
    enableZoom={false}
    autoRotateSpeed={1}
    onchange={invalidate}
  />
</T.PerspectiveCamera>

<T.DirectionalLight intensity={3.4} />

Adapted from this Three.js example, demonstrating WebGPU render bundles.

WebGPU is still young and has limited availability across major browsers. Three.js’s WebGPURenderer will fall back to WebGL when WebGPU is not available.

Customizing the renderer

To configure the renderer (e.g. force a specific backend), pass a createRenderer factory. The <Canvas> still awaits init() for you.

App.svelte
<script>
  import Scene from './Scene.svelte'
  import { Canvas } from '@threlte/core/webgpu'
  import { WebGPURenderer } from 'three/webgpu'
</script>

<Canvas
  createRenderer={(canvas) => {
    return new WebGPURenderer({
      canvas,
      antialias: true,
      forceWebGL: false
    })
  }}
>
  <Scene />
</Canvas>

Vite

WebGPU uses top-level async to determine WebGPU compatibility. Vite will often throw an error when it detects this.

To circumvent this issue, the following can be added to your Vite config.

vite.config.js
optimizeDeps: {
  esbuildOptions: {
    target: 'esnext'
  }
},
build: {
  target: 'esnext'
}

Alternatively, vite-plugin-top-level-await can be used, although less success has been reported with this method.

Extras

@threlte/extras ships a matching webgpu export. Import from it instead of the package root whenever your <Canvas> comes from @threlte/core/webgpu.

Scene.svelte
<script>
  import { T } from '@threlte/core/webgpu'
  import { Grid, OrbitControls } from '@threlte/extras/webgpu'
</script>

Most of the package is renderer-agnostic and works as-is, a few components have been rewritten in TSL, and the rest throw until they are migrated. See @threlte/extras WebGPU for the full breakdown.

TSL

Materials on WebGPU are extended with nodes rather than string shaders. See TSL for what that looks like in Threlte.