threlte logo
Advanced

WebGPU and TSL

The WebGPU specification is still in active development. WebGPU support in Three.js is in an early stage and is subject to frequent breaking changes. As of now, we do not recommend using WebGPU in production.

We highly recommend targeting version r171 onwards because of potential duplication and configuration issues.

WebGPU

To use the WebGPU renderer, import it and then initialize it within your <Canvas>’s createRenderer prop. This will replace the default WebGL renderer.

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

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

WebGPU is still young and has limited availability across major browsers. For this reason, Three.js’s WebGPU renderer fallbacks to WebGL when WebGPU is not available.

This same approach can be used to swap out the default renderer for any other custom renderer.

The WebGPU renderer doesn’t immediately render. If the renderer you provide needs to delay rendering, you can defer rendering by initially setting the renderMode to manual.

App.svelte
<script>
  import { Canvas, T } from '@threlte/core'
  import { WebGPURenderer } from 'three/webgpu'
  let renderMode = $state('manual')
</script>

<Canvas
  {renderMode}
  createRenderer={(canvas) => {
    const renderer = new WebGPURenderer({
      canvas,
      antialias: true,
      forceWebGL: false
    })

    renderer.init().then(() => {
      renderMode = 'on-demand'
    })

    return renderer
  }}
>
  <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.

<script lang="ts">
  import { Canvas, extend } from '@threlte/core'
  import Scene from './Scene.svelte'
  import * as THREE from 'three/webgpu'

  extend(THREE)
</script>

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

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { T, useTask, useThrelte } from '@threlte/core'
  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.Group()
  group.static = true

  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.

TSL

A question that comes up often in Three.js development is “How do I extend Three.js materials?“. External libraries such as three-custom-shader-material use a find and replace solution to get this job done. Three.js has identified that it’s not an ideal solution and recommends using the Three.js Shading Language or TSL for short.

The example below is an adaptation of this Three.js example. There are many more TSL examples within Three.js that you can use or adapt for your project.

Using the <T> catalogue

The <T> component defaults to all the exports from three and will error on webgpu things like <T.MeshPhysicalNodeMaterial />. This is because the MeshPhysicalNodeMaterial class is an export of three/webgpu and not three. Here are a few options to resolve this.

Option 1

Extend <T> with all the definitions from three/webgpu by using the extend function. Adding all of the definitions will increase the bundle size of your application because both three and three/webgpu will be imported in a non-tree-shakeable way.

App.svelte
<script>
  import Scene from './Scene.svelte'
  import { Canvas, extend } from '@threlte/core'
  import * as THREE from 'three/webgpu'

  extend(THREE)
</script>

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

Option 2

Use explicit imports for the objects, functions, and other classes that you use from three/webgpu. You can then use <T>’s is prop with those imports from three/webgpu.

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

	const material = new MeshPhysicalNodeMaterial()
</script>

<T.Mesh>
	<T.BoxGeometry>
	<T is={material}>
</T.Mesh>

Option 3

Same as option #2 but using extend with the imports so that you can have <T.MeshPhysicalNodeMaterial /> etc…

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

  extend({ MeshPhysicalNodeMaterial })
</script>

<Canvas
  createRenderer={(canvas) => {
    return new WebGPURenderer({
      canvas,
      antialias: true,
      forceWebGL: false
    })
  }}
>
  <Scene />
</Canvas>
Scene.svelte
<script>
  import { T } from '@threlte/core'
</script>

<T.Mesh>
  <T.BoxGeometry />
  <T.MeshPhysicalNodeMaterial />
</T.Mesh>

Options 2 and 3 will keep the bundle size of your application small but you’ll have to keep it updated as you go.

Careful! three and three/webgpu don’t mix well

You will need to overwrite some of the default catalogue if you use three/webgpu. For example, if you’re using a MeshPhysicalNodeMaterial, you need to update any lighing classes you use like so:

App.svelte
<script>
  import { DirectionalLight, MeshPhysicalNodeMaterial } from 'three/webgpu'

  // tell <T.DirectionalLight> to use the definition from `three/webgpu`
  extend({ MeshPhysicalNodeMaterial, DirectionalLight })
</script>

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

<T.DirectionalLight />

<T.Mesh>
  <T.BoxGeometry />
  <T.MeshPhysicalNodeMaterial />
</T.Mesh>

This is because the exports from three/webgpu are different than those in three and make use of the additional features that node materials have.

An easy option for projects is to start with option #1 and then transition to the other options when bundle size becomes an issue or you need to ship to production.

Nodes

The material’s nodes can be directly assigned like any other prop on the <T> component.

<T.MeshPhysicalNodeMaterial
  {outputNode}
  castShadowNode={Fn(() => {
    /* ... */
  })()}
/>

Or can create the material in the script tag and use <T>’s is prop to attach the material.

<script>
	const material = new MeshPhysicalNodeMaterial();
	material.outputNode = outputNode;
	material.castShadowNode = Fn(() => { /* ... */ }();
</script>

<T is={material} />

Node materials give you the ability to modify three’s builtin materials. In the sliced gear example, two nodes are modified; the outputNode and the castShadowNode. The outputNode is set up in such a way that it discards any fragments that are outside the permitted startAngle and arcAngle. If a fragment is not discarded and it is not front-facing, it is assigned the color in the uColor uniform. The material needs its side set to THREE.DoubleSide otherwise three.js will cull them out if they are facing away from the camera. Any fragment that is discarded in the shadowNode will not cast shadows.