WebGPU / TSL
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 设置了一些选项,比如 powerPreference
、antialias
、alpha
、stencil
和 shadowMap
。这些选项与 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 的一个很大优势是它与渲染器无关,这意味着你可以在不同的渲染器上使用相同的着色器,例如 WebGL 和 WebGPU。
这使得编写和维护着色器变得更加容易,因为你无需担心两种着色语言之间的差异。
这也是面向未来的,因为如果有新的渲染器发布,只要 TSL 支持,我们可以在不进行任何更改的情况下使用相同的着色器。
Three Shading Language 仍在开发中,但已经可以在最新版本的 Three.js 中使用。学习它并跟踪更新的最佳方式是查看 Three Shading Language 维基页面。我大量使用它来学习如何使用它。
基于节点的材料
要了解如何使用 TSL 创建着色器,我们需要了解基于节点的含义。
在基于节点的方法中,我们通过将不同的节点连接在一起来创建一个图表来创建着色器。每个节点代表一个特定的操作或函数,节点之间的连接表示数据流。
这种方法有很多优势,例如:
- 可视化表示:更容易理解和可视化着色器中的数据和操作流程。
- 重用性:可以创建可重用的节点,在不同的着色器中使用。
- 灵活性:可以通过添加或移除节点轻松修改和更改着色器的行为。
- 扩展性:现在添加或自定义现有材料的功能变得轻而易举。
- 无关性:TSL 会为目标渲染器(无论是 WebGL (GLSL) 还是 WebGPU (WGSL))生成适当的代码。
在我们开始编写第一个基于节点的材料之前,我们可以使用在线 Three.js playground 来以可视化的方式试验节点系统。
打开 Three.js playground,在顶部点击 Examples 按钮,选择 basic > fresnel
示例。
你应该会看到一个基于节点的材料编辑器,其中有两个 color
节点和一个连接到 fresnel
节点的 float
节点。(Color A
,Color B
,和 Fresnel Factor
)
fresnel
节点连接到 Basic Material
的颜色上,结果是 Teapot 显示出 fresnel 效果。
使用 Splitscreen
按钮在右侧预览结果。
假设我们想让 Basic Material
的透明度随时间变化。我们可以添加一个 Timer
节点并连接到一个 Fract
节点,以便时间达到 1 时重置为 0。然后将其连接到 Basic Material
的 opacity
输入。
我们的茶壶现在淡入淡出,不断地消失和再次显现。
花点时间试试不同的节点,看看它们如何影响材料。
现在我们对基于节点的材料有了基本的了解,让我们来看看如何在 React Three Fiber 中使用新的 Three.js 的基于节点的材料。
React Three Fiber 实现
到目前为止,使用 WebGL 时,我们一直在使用 MeshBasicMaterial、 MeshStandardMaterial 甚至自定义的 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 中,这可能会自动完成。
现在我们可以在我们的组件中使用新的 MeshBasicNodeMaterial
和 MeshStandardNodeMaterial
。
让我们将 Experience
组件中立方体的 MeshStandardMaterial
替换为 MeshStandardNodeMaterial
:
<mesh> <boxGeometry args={[1, 1, 1]} /> <meshStandardNodeMaterial color="pink" /> </mesh>
我们可以像使用 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> </> ); };
我们有一个使用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
模块中导入mix
和uv
节点,并使用它们根据平面的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
函数完全相同,但现在我们可以用节点的方式来实现。(更具可读性!)
我们现在可以看到两种颜色根据平面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
。
通过更改colorA
和colorB
属性,由于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
节点接受一个值作为参数(可以是不同类型,如 float
,vec3
,vec4
等),并返回一个 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
方法。例如,color
或vec3
类型的 uniforms。对于float
类型的 uniforms,你需要直接设置值:uniforms.opacity.value = opacity;
颜色现在可以实时正确更新。
在对颜色进行更多操作之前,让我们看看如何使用 positionNode
影响平面顶点的位置。
节点位置
positionNode
节点允许我们影响几何体顶点的位置。
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
One-time payment. Lifetime updates included.