WebGPU / TSL

Starter pack

WebGPU 是一项新的网络标准,提供了一个低级别的 API,用于在 GPU 上渲染图形和进行计算。它被设计为 WebGL 的继任者,提供更好的性能和更高级的功能。

好消息是,现在可以用极少的代码更改在 Three.js 中使用它。

在本课中,我们将探索如何在 Three.js 和 React Three Fiber 中使用 WebGPU,以及如何使用新的 Three Shading Language (TSL) 编写 shader。

如果你是 shader 新手,建议你先完成 Shaders 章节,然后再继续学习这一章。

WebGPU Renderer

要使用 WebGPU API 而不是WebGL,我们需要使用 WebGPURenderer(Three.js 文档中尚无专用部分)替代 WebGLRenderer

React Three Fiber 中,当创建 <Canvas> 组件时,renderer 的设置是自动完成的。然而,我们可以通过传递一个函数到 <Canvas> 组件的 gl 属性来覆盖默认 renderer。

App.jsx 中,我们有一个使用默认 WebGLRenderer<Canvas> 组件。让我们将其修改为使用 WebGPURenderer

首先,我们需要在 WebGPURenderer 准备好之前停止 frameloop。我们可以通过将 frameloop 属性设置为 never 来做到这一点。

// ...
import { useState } from "react";

function App() {
  const [frameloop, setFrameloop] = useState("never");

  return (
    <>
      {/* ... */}
      <Canvas
        // ...
        frameloop={frameloop}
      >
        {/* ... */}
      </Canvas>
    </>
  );
}

export default App;

接下来,我们需要导入 Three.js 的 WebGPU 版本:

import * as THREE from "three/webgpu";

使用 WebGPU 时,我们需要使用 three/webgpu 模块而不是默认的 three 模块。这是因为 WebGPURenderer 没有包含在 Three.js 的默认构建中。

然后,我们可以使用 gl 属性来创建一个新的 WebGPURenderer 实例:

// ...

function App() {
  const [frameloop, setFrameloop] = useState("never");

  return (
    <>
      {/* ... */}
      <Canvas
        // ...
        gl={(canvas) => {
          const renderer = new THREE.WebGPURenderer({
            canvas,
            powerPreference: "high-performance",
            antialias: true,
            alpha: false,
            stencil: false,
            shadowMap: true,
          });
          renderer.init().then(() => {
            setFrameloop("always");
          });
          return renderer;
        }}
      >
        {/* ... */}
      </Canvas>
    </>
  );
}
// ...

我们创建一个新的 WebGPURenderer 实例,并将 canvas 元素传递给它。我们还为 renderer 设置了一些选项,比如 powerPreferenceantialiasalphastencilshadowMap这些选项与 WebGLRenderer 中使用的选项相似。

最后,我们调用 renderer 的 init() 方法来初始化它。一旦初始化完成,我们将 frameloop 状态设置为 "always" 以开始渲染。

让我们在浏览器中查看结果:

我们的立方体现在使用 WebGPURenderer 而不是 WebGLRenderer 渲染。

就这么简单!我们已成功在 React Three Fiber 应用中设置了一个 WebGPURenderer。您现在可以使用相同的 Three.js API 创建和操作 3D 对象,就像在 WebGLRenderer 中一样。

最大的变化是在编写 shader 的时候。WebGPU API 使用与 WebGL 不同的 shading language,这意味着我们需要以不同的方式编写 shader。在 WGSL 中,而不是在 GLSL 中。

这就是 Three Shading Language (TSL) 的用武之地。

Three Shading Language

TSL 是一种新型的着色语言,旨在与 Three.js 配合使用,以更用户友好的方式通过节点的方式编写着色器

TSL 的一个很大优势是它与渲染器无关,这意味着你可以在不同的渲染器上使用相同的着色器,例如 WebGLWebGPU

这使得编写和维护着色器变得更加容易,因为你无需担心两种着色语言之间的差异。

这也是面向未来的,因为如果有新的渲染器发布,只要 TSL 支持,我们可以在不进行任何更改的情况下使用相同的着色器。

Three Shading Language 仍在开发中,但已经可以在最新版本的 Three.js 中使用。学习它并跟踪更新的最佳方式是查看 Three Shading Language 维基页面。我大量使用它来学习如何使用它。

基于节点的材料

要了解如何使用 TSL 创建着色器,我们需要了解基于节点的含义。

在基于节点的方法中,我们通过将不同的节点连接在一起来创建一个图表来创建着色器。每个节点代表一个特定的操作或函数,节点之间的连接表示数据流。

这种方法有很多优势,例如:

  • 可视化表示:更容易理解和可视化着色器中的数据和操作流程。
  • 重用性:可以创建可重用的节点,在不同的着色器中使用。
  • 灵活性:可以通过添加或移除节点轻松修改和更改着色器的行为。
  • 扩展性:现在添加或自定义现有材料的功能变得轻而易举。
  • 无关性TSL 会为目标渲染器(无论是 WebGL (GLSL) 还是 WebGPU (WGSL))生成适当的代码。

在我们开始编写第一个基于节点的材料之前,我们可以使用在线 Three.js playground 来以可视化的方式试验节点系统

打开 Three.js playground,在顶部点击 Examples 按钮,选择 basic > fresnel 示例。

Three.js playground

你应该会看到一个基于节点的材料编辑器,其中有两个 color 节点和一个连接到 fresnel 节点的 float 节点。Color AColor B,和 Fresnel Factor

fresnel 节点连接到 Basic Material 的颜色上,结果是 Teapot 显示出 fresnel 效果。

Three.js playground

使用 Splitscreen 按钮在右侧预览结果。

假设我们想让 Basic Material 的透明度随时间变化。我们可以添加一个 Timer 节点并连接到一个 Fract 节点,以便时间达到 1 时重置为 0。然后将其连接到 Basic Materialopacity 输入。

我们的茶壶现在淡入淡出,不断地消失和再次显现。

花点时间试试不同的节点,看看它们如何影响材料。

现在我们对基于节点的材料有了基本的了解,让我们来看看如何在 React Three Fiber 中使用新的 Three.js基于节点的材料

React Three Fiber 实现

到目前为止,使用 WebGL 时,我们一直在使用 MeshBasicMaterialMeshStandardMaterial 甚至自定义的 ShaderMaterial 来创建我们的材质。

使用 WebGPU 时,我们需要使用与 TSL 兼容的新材质。它们的名称与我们之前使用的 Node 材质相比略有不同:

  • MeshBasicMaterial -> MeshBasicNodeMaterial
  • MeshStandardMaterial -> MeshStandardNodeMaterial
  • MeshPhysicalMaterial -> MeshPhysicalNodeMaterial
  • ...

为了在 React Three Fiber 中以声明式的方式使用它们,我们需要 extend 这些材质。在 App.jsx 中:

// ...
import { extend } from "@react-three/fiber";

extend({
  MeshBasicNodeMaterial: THREE.MeshBasicNodeMaterial,
  MeshStandardNodeMaterial: THREE.MeshStandardNodeMaterial,
});
// ...

在未来版本的 React Three Fiber 中,这可能会自动完成。

现在我们可以在我们的组件中使用新的 MeshBasicNodeMaterialMeshStandardNodeMaterial

让我们将 Experience 组件中立方体的 MeshStandardMaterial 替换为 MeshStandardNodeMaterial

<mesh>
  <boxGeometry args={[1, 1, 1]} />
  <meshStandardNodeMaterial color="pink" />
</mesh>

WebGPU Pink Cube

我们可以像使用 MeshStandardMaterial 一样使用 MeshStandardNodeMaterial

我们的立方体现在依赖于 MeshStandardNodeMaterial 而不是 MeshStandardMaterial。现在我们可以使用节点来自定义材质。

颜色节点

让我们学习如何使用TSL创建自定义节点来个性化我们的材质。

首先,让我们在src/components文件夹中创建一个名为PracticeNodeMaterial.jsx的新组件。

export const PracticeNodeMaterial = ({
  colorA = "white",
  colorB = "orange",
}) => {
  return <meshStandardNodeMaterial color={colorA} />;
};

然后在Experience.jsx中,使用PracticeNodeMaterial将我们的立方体替换为一个平面:

// ...
import { PracticeNodeMaterial } from "./PracticeNodeMaterial";

export const Experience = () => {
  return (
    <>
      {/* ... */}

      <mesh rotation-x={-Math.PI / 2}>
        <planeGeometry args={[2, 2, 200, 200]} />
        <PracticeNodeMaterial />
      </mesh>
    </>
  );
};

WebGPU Plane

我们有一个使用PracticeNodeMaterial的平面。

为了自定义我们的材质,现在可以使用不同的节点来改变我们可用的不同节点。可用节点的列表可以在wiki页面中找到。

让我们简单地从colorNode节点开始,以改变我们材质的颜色。在PracticeNodeMaterial.jsx中:

import { color } from "three/tsl";

export const PracticeNodeMaterial = ({
  colorA = "white",
  colorB = "orange",
}) => {
  return <meshStandardNodeMaterial colorNode={color(colorA)} />;
};

我们使用来自three/tsl模块的color节点设置colorNode属性。color节点接受一个颜色作为参数,并返回一个可以在材质中使用的颜色节点。

这给了我们与之前相同的结果,但现在我们可以添加更多节点来自定义我们的材质。

让我们从three/tsl模块中导入mixuv节点,并使用它们根据平面的UV坐标混合两种颜色。

import { color, mix, uv } from "three/tsl";

export const PracticeNodeMaterial = ({
  colorA = "white",
  colorB = "orange",
}) => {
  return (
    <meshStandardNodeMaterial
      colorNode={mix(color(colorA), color(colorB), uv())}
    />
  );
};

它将执行不同的节点以获得材质的最终颜色。mix节点接受两个颜色和一个因子(在这个例子中是UV坐标),返回一个基于因子混合的颜色。

这与在GLSL中使用mix函数完全相同,但现在我们可以用节点的方式来实现。(更具可读性!)

WebGPU Plane with Mix

我们现在可以看到两种颜色根据平面UV坐标混合。

令人难以置信的是,我们并不是从零开始。我们使用现有的MeshStandardNodeMaterial,并只是在其中添加自定义节点。这意味着MeshStandardNodeMaterial的阴影、光照和所有其他功能仍然可用。

对于非常简单的节点逻辑,内联声明节点是可以的,但对于更复杂的逻辑,我建议在useMemo钩子中声明节点(以及后来的uniforms等):

// ...
import { useMemo } from "react";

export const PracticeNodeMaterial = ({
  colorA = "white",
  colorB = "orange",
}) => {
  const { nodes } = useMemo(() => {
    return {
      nodes: {
        colorNode: mix(color(colorA), color(colorB), uv()),
      },
    };
  }, []);

  return <meshStandardNodeMaterial {...nodes} />;
};

这与之前做的完全相同,但现在我们可以在nodes对象中添加更多节点,并以更加组织和通用的方式传递给meshStandardNodeMaterial

通过更改colorAcolorB属性,由于useMemo钩子,不会导致着色器重新编译。

让我们添加controls来更改材质的颜色。在Experience.jsx中:

// ...
import { useControls } from "leva";

export const Experience = () => {
  const { colorA, colorB } = useControls({
    colorA: { value: "skyblue" },
    colorB: { value: "blueviolet" },
  });
  return (
    <>
      {/* ... */}

      <mesh rotation-x={-Math.PI / 2}>
        <planeGeometry args={[2, 2, 200, 200]} />
        <PracticeNodeMaterial colorA={colorA} colorB={colorB} />
      </mesh>
    </>
  );
};

默认颜色正常显示,但更新颜色无效。

这是因为我们需要将颜色作为uniforms传递给meshStandardNodeMaterial

Uniforms

TSL 中声明 uniforms,我们可以使用来自 three/tsl 模块的 uniform 节点。uniform 节点接受一个值作为参数(可以是不同类型,如 floatvec3vec4 等),并返回一个 uniform 节点,该节点可以用于不同的节点,同时从组件代码中更新。

让我们在 PracticeNodeMaterial.jsx 中将硬编码的颜色切换为 uniforms:

// ...
import { uniform } from "three/tsl";

export const PracticeNodeMaterial = ({
  colorA = "white",
  colorB = "orange",
}) => {
  const { nodes, uniforms } = useMemo(() => {
    const uniforms = {
      colorA: uniform(color(colorA)),
      colorB: uniform(color(colorB)),
    };

    return {
      nodes: {
        colorNode: mix(uniforms.colorA, uniforms.colorB, uv()),
      },
      uniforms,
    };
  }, []);

  return <meshStandardNodeMaterial {...nodes} />;
};

我们声明了一个 uniforms 对象以更好地组织代码,并使用 uniform 值来代替在创建节点时获得的默认值。

通过在 useMemo 中返回它们,我们现在可以在组件中访问这些 uniforms。

useFrame 中我们可以更新 uniforms:

// ...
import { useFrame } from "@react-three/fiber";

export const PracticeNodeMaterial = ({
  colorA = "white",
  colorB = "orange",
}) => {
  // ...

  useFrame(() => {
    uniforms.colorA.value.set(colorA);
    uniforms.colorB.value.set(colorB);
  });

  return <meshStandardNodeMaterial {...nodes} />;
};

更新对象 uniform 时使用 value.set 方法。例如,colorvec3 类型的 uniforms。对于 float 类型的 uniforms,你需要直接设置值:uniforms.opacity.value = opacity;

颜色现在可以实时正确更新。

在对颜色进行更多操作之前,让我们看看如何使用 positionNode 影响平面顶点的位置。

节点位置

positionNode 节点允许我们影响几何体顶点的位置。

Three.js logoReact logo

React Three Fiber: The Ultimate Guide to 3D Web Development

✨ You have reached the end of the preview ✨

Go to the next level with Three.js and React Three Fiber!

Get full access to this lesson and the complete course when you enroll:

  • 🔓 Full lesson videos with no limits
  • 💻 Access to the final source code
  • 🎓 Course progress tracking & completion
  • 💬 Invite to our private Discord community
Unlock the Full Course – Just $85

One-time payment. Lifetime updates included.