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 Three.js’s WebGPU renderer, import it and then initialize it within your <Canvas>’s createRenderer prop.

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 an experimental browser api and at the time of writing 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.

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

Adapted from this Three.js example.

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 like so:

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.

TSL

A question that comes up often in Three.js development is “How do I extend Three.js’s 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.

<script lang="ts">
  import Scene from './Scene.svelte'
  import { Canvas, extend } from '@threlte/core'
  import { Checkbox, Color, Folder, Pane, Slider } from 'svelte-tweakpane-ui'
  import { MathUtils } from 'three'
  import {
    DirectionalLight,
    MeshPhysicalNodeMaterial,
    MeshStandardMaterial,
    WebGPURenderer
  } from 'three/webgpu'

  extend({ DirectionalLight, MeshPhysicalNodeMaterial, MeshStandardMaterial })

  let arcAngleDegrees = $state(90)
  let startAngleDegrees = $state(60)
  let sliceColor = $state('#ff4500')
  let rotate = $state(true)

  const arcAngle = $derived(MathUtils.DEG2RAD * arcAngleDegrees)
  const startAngle = $derived(MathUtils.DEG2RAD * startAngleDegrees)
</script>

<div>
  <Pane
    position="fixed"
    title="slice shader"
  >
    <Checkbox
      bind:value={rotate}
      label="rotate"
    />
    <Folder title="uniforms">
      <Color
        bind:value={sliceColor}
        label="color"
      />
      <Slider
        bind:value={startAngleDegrees}
        min={0}
        max={360}
        step={1}
        label="start angle (degrees)"
      />
      <Slider
        bind:value={arcAngleDegrees}
        min={0}
        max={360}
        step={1}
        label="arc angle (degrees)"
      />
    </Folder>
  </Pane>
  <Canvas
    createRenderer={(canvas) => {
      return new WebGPURenderer({
        antialias: true,
        canvas,
        forceWebGL: false
      })
    }}
  >
    <Scene
      {rotate}
      {arcAngle}
      {sliceColor}
      {startAngle}
    />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>

Using the <T> catalogue

The <T> component uses all the exports from three. It will error on things like <T.MeshPhysicalNodeMaterial /> because the MeshPhysicalNodeMaterial class is an export of three/webgpu not three. You have a few options to work this out.

  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>
  1. 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>
  1. 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 <T> 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 nodes can be directly assigned like any other prop on the <T> component.

<T.MeshPhysicalNodeMaterial
  outputNode={Fn(([arg1, arg2]) => {
    /* ... */
  })(arg1, arg2)}
  shadowNode={Fn(([arg1, arg2]) => {
    /* ... */
  })(arg2, arg2)}
/>

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

Updating Uniforms

If your node uses uniforms, they can be declared in the script tag of the component and updated via $effect or a callback.

For example, if your material uses elapsed time in a uniform, you can update the uniform inside a useTask callback.

The material in the example below demonstrates two ways to update uniforms. The uTime uniform is updated in useTask whereas uIntensity is updated in an $effect.

<script lang="ts">
  import Scene from './Scene.svelte'
  import { Canvas, extend } from '@threlte/core'
  import { Pane, Slider } from 'svelte-tweakpane-ui'
  import { MeshStandardNodeMaterial, WebGPURenderer } from 'three/webgpu'

  extend({ MeshStandardNodeMaterial })

  let emissiveIntensity = $state(0.5)
</script>

<div>
  <Pane
    title="updating uniforms"
    position="fixed"
  >
    <Slider
      bind:value={emissiveIntensity}
      label="emissive intensity"
      max={1}
      min={0}
      step={0.1}
    />
  </Pane>
  <Canvas
    createRenderer={(canvas) => {
      return new WebGPURenderer({
        antialias: true,
        canvas,
        forceWebGL: false
      })
    }}
  >
    <Scene {emissiveIntensity} />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>

Note that TSL has an oscSine function that oscillates on time that could also be used in the example above.