着色器简介
终于到了探索 着色器 世界的时刻了。着色器是创建各种视觉效果的关键。在本章中,我们将学习什么是着色器,它们能实现什么,以及如何在 React Three Fiber 中使用它们。
前奏
在开始之前,我想提到的是,着色器可能需要一些时间来适应。它的工作方式与我们迄今为止编写的其他代码有所不同。这是一种全新的思维方式和视觉效果的创造方式。但别担心,我会引导你完成这个过程,我们将从基础开始,逐步过渡到更高级的话题。
如果一开始你不理解所有内容,不要气馁。这是正常的。这是一个新概念,需要练习才能更熟练掌握。我建议你花些时间,进行实验和练习,但我向你保证,这一切都是值得的!着色器拥有强大的能力,可以让你创造你能想象到的任何视觉效果。
此外,每个人的学习方式都不同。有些人通过阅读学得更好,有些人则通过观看视频,还有些人通过实践学习。对于这个话题,交叉引用不同的资源可能非常有帮助。我将在本章末尾分享资源,以巩固你的知识,并超越我们在这里所涵盖的内容。
希望我没有吓到你,并且你对学习着色器感到兴奋。让我们开始吧!
什么是着色器?
着色器是运行在 GPU(图形处理单元)上的小程序。它们使用一种称为 GLSL(OpenGL Shading Language)的语言编写,类似于 C 语言。
它们用于定位网格的顶点(Vertex Shader)以及为面部的每个像素着色(Fragment Shader)。
事实上,我们一直在使用着色器。当我们创建一个 material 时,我们就在使用着色器。例如,当我们创建一个 MeshBasicMaterial
时,我们使用的是一种单色着色的 shader。当我们创建一个 MeshStandardMaterial
时,我们使用的是一种模拟光照、阴影和反射的 shader。
Vertex Shader
Vertex Shader 是为几何体的每个顶点执行的程序。它的主要职责是将顶点从 3D 空间(我们的 3D 世界)转换到 2D 空间(我们的屏幕或视口)。它通过利用多个矩阵来实现这种转换:
- View Matrix:该矩阵表示场景中相机的位置和方向。它将顶点从世界空间转换到相机空间。
- Projection Matrix:该矩阵,要么是透视的要么是正交的,将顶点从相机空间转换为归一化设备坐标(NDC),为最终投影到 2D 屏幕做好准备。
- Model Matrix:此矩阵封装了场景中每个对象的位置、旋转和缩放。它将顶点从对象空间转换到世界空间。
此外,vertex shader 还包含顶点的原始位置和与之关联的任何其他 attributes。
对于几何体的每个顶点,vertex shader 将被执行。
最后,顶点在 2D 空间中的变换位置通过预定义变量 gl_Position
返回。在所有顶点被变换后,GPU 在它们之间插值以生成几何体的面,接着进行光栅化和渲染到屏幕上。
片段着色器
片段着色器,也称为像素着色器,是在光栅化过程生成的每个片段(或像素)上执行的程序。它的主要任务是确定屏幕上每个像素的最终颜色。
在光栅化过程中生成的每个片段上,都会执行片段着色器。
片段着色器接收来自顶点着色器的插值值,如颜色、纹理坐标、法线以及与几何体顶点相关的任何其他属性。这些插值值称为 varyings,提供了有关每个片段位置的表面属性的信息。
除了插值值之外,片段着色器还可能采样纹理并访问在所有片段中都是恒定的 uniform 变量。这些 uniform 变量可以表示诸如光源位置、材质属性或着色计算所需的任何其他数据。
我们将在本课后面的部分进一步讨论 attributes 和 uniforms。
通过使用输入数据,片段着色器执行各种计算以确定片段的最终颜色。这可能涉及复杂的光照计算、纹理映射、着色效果或场景中所需的任何其他视觉效果。
一旦颜色计算完成,片段着色器使用预定义变量 gl_FragColor
输出片段的最终颜色。
我尽量简单地解释着色器,并故意省略了一些技术细节,但我理解这可能仍然有些抽象。让我们创建一个简单的着色器来看看它在实践中的运作方式。
您的第一个着色器
让我们运行启动包。你应该会看到这个框,中间有一个黑色平面:
打开名为 ShaderPlane.jsx
的文件,它包含一个简单的具有平面几何体和基本材质的 mesh。我们将用自定义着色器材质替换该材质。
shaderMaterial
要创建一个着色器材质,我们使用 Drei library 的 shaderMaterial
函数。
它需要三个参数:
uniforms
: 一个包含着色器中使用的 uniform 变量的对象。现在保持为空。vertexShader
: 包含顶点着色器 GLSL 代码的字符串。fragmentShader
: 包含片段着色器 GLSL 代码的字符串。
在我们文件的顶部,声明一个名为 MyShaderMaterial
的新着色器材质:
import { shaderMaterial } from "@react-three/drei"; const MyShaderMaterial = shaderMaterial( {}, ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, ` void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } ` );
我们稍后将深入探讨着色器代码。
为了能够与 React Three Fiber 声明性地使用它,我们使用 extend
方法:
import { extend } from "@react-three/fiber"; // ... extend({ MyShaderMaterial });
现在我们可以用新的着色器材质替换 <meshBasicMaterial>
:
import { shaderMaterial } from "@react-three/drei"; import { extend } from "@react-three/fiber"; const MyShaderMaterial = shaderMaterial( {}, ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, ` void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } ` ); extend({ MyShaderMaterial }); export const ShaderPlane = ({ ...props }) => { return ( <mesh {...props}> <planeGeometry args={[1, 1]} /> <myShaderMaterial /> </mesh> ); };
你应该会看到和之前一样的黑色平面。我们还没有更改任何内容,但我们现在正在使用自定义着色器材质。
为了验证其是否在工作,让我们更改片段着色器中返回的颜色。用以下内容替换 gl_FragColor
行:
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
gl_FragColor
是一个代表片段颜色的预定义变量。它是一个 vec4
(具有4个分量的向量),代表颜色的 红、绿、蓝和 alpha 通道。每个分量是一个介于0到1之间的 float 。
通过将第一个分量设置为 1.0
,我们将红色通道设置为其最大值,这将导致显示为红色。
你应该会在屏幕中间看到一个红色平面:
恭喜!你刚刚创建了你的第一个着色器材质。虽然它很简单,但这是一个开始。
Shader Code
在继续之前,让我们设置开发环境以更舒适地编写 shaders。
你有两种选择来编写 shader 代码:
- 内联:你可以直接在 JavaScript 文件中编写 shader 代码。
- 外部:你可以在一个扩展名为
.glsl
的单独文件中编写 shader 代码,并在你的 JavaScript 文件中导入它。
我通常更喜欢在适当的 material 文件中使用 内联 方法,这样 shader 代码就接近 material 声明。
但为了更容易地编写和阅读,我建议为 GLSL 使用 语法高亮器。你可以使用 Visual Studio Code 的 Comment tagget templates 扩展。它将高亮显示模板字符串中的 GLSL 代码。
安装后,要启用语法高亮器,你需要在 shader 代码的开头添加以下注释:
const MyShaderMaterial = shaderMaterial( {}, /* glsl */ ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, /* glsl */ ` void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); } ` );
你应该能看到模板字符串中的 GLSL 代码被高亮显示:
上面的顶点 shader 现在具有正确的语法高亮,更容易阅读。
这就是内联 shader 代码所需要的全部内容。如果你更喜欢将 shader 代码分开,你仍然可以选择使用外部文件。让我们来看看如何做到这一点。
导入 GLSL 文件
首先,在 src
文件夹中创建一个名为 shaders
的新文件夹。在此文件夹中,创建两个文件:myshader.vertex.glsl
和 myshader.fragment.glsl
,并将相应的着色器代码复制到每个文件中。
myshader.vertex.glsl
:
void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
myshader.fragment.glsl
:
void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }
您可以随意使用您偏好的命名约定,并在有很多着色器时将它们分组到子文件夹中。
接下来,为了能够在我们的 JavaScript 文件中导入这些文件,我们需要安装 vite-plugin-glsl 插件作为开发依赖:
yarn add vite-plugin-glsl --dev
然后,在您的 vite.config.js
文件中,导入该插件并将其添加到 plugins
数组中:
import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; import glsl from "vite-plugin-glsl"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), glsl()], });
现在,您可以在 JavaScript 文件中导入 GLSL 文件,并将它们用作着色器代码:
import myShaderFragment from "./shaders/myshader.fragment.glsl"; import myShaderVertex from "./shaders/myshader.vertex.glsl"; const MyShaderMaterial = shaderMaterial({}, myShaderVertex, myShaderFragment);
现在我们有了一种编写和导入着色器代码的方便方式,我们可以开始探索着色器代码的不同部分。
GLSL
着色器代码使用 GLSL (OpenGL Shading Language) 编写。它是一种类似C的语言,下面我们来看看其基础知识。
类型
GLSL 有多种类型,但最常用的有:
- bool: 布尔值(
true
或false
)。 - int: 整数。
- float: 浮点数。
- vectors: 数字集合。
vec2
是包含 2 个浮点数 (x
和y
) 的集合,vec3
是包含 3 个浮点数 (x
、y
和z
) 的集合,vec4
是包含 4 个浮点数 (x
、y
、z
和w
) 的集合。除了使用x
、y
、z
和w
,你也可以使用r
、g
、b
和a
表示颜色,它们可以互换。 - matrices: 向量集合。例如,
mat2
是包含 2 个向量的集合,mat3
是包含 3 个向量的集合,mat4
是包含 4 个向量的集合。
Swizzling 和操作
您可以使用 swizzling 访问向量的组件。例如,您可以使用另一个向量的组件创建一个新向量:
vec3 a = vec3(1.0, 2.0, 3.0); vec2 b = a.xy;
在此示例中,b
将是一个带有 a
的 x
和 y
组件的向量。
您还可以使用 swizzling 更改组件的顺序:
vec3 a = vec3(1.0, 2.0, 3.0); vec3 b = a.zyx;
在此示例中,b
将等于 vec3(3.0, 2.0, 1.0)
。
要创建一个具有相同组件的新向量,可以使用构造函数:
vec3 a = vec3(1.0);
在此示例中,a
将等于 vec3(1.0, 1.0, 1.0)
。
运算符
GLSL 具有常见的算术运算符:+
,-
,*
,/
,+=
,/=
,*=
和常见的比较运算符:==
,!=
,>
,<
,>=
,<=
。
这些运算符需要与正确的类型一起使用。例如,你不能将一个整数加到一个浮点数上,你需要先将整数转换为浮点数:
int a = 1; float b = 2.0; float c = float(a) + b;
你也可以对向量和矩阵进行操作:
vec3 a = vec3(1.0, 2.0, 3.0); vec3 b = vec3(4.0, 5.0, 6.0); vec3 c = a + b;
这与以下方式相同:
vec3 c = vec3(a.x + b.x, a.y + b.y, a.z + b.z);
函数
顶点和片段着色器的入口点是 main
函数。当着色器被调用时,将执行此函数。
void main() { // 在这里编写你的代码 }
void 是函数的返回类型。它意味着函数不返回任何值。
你也可以定义自己的函数:
float add(float a, float b) { return a + b; }
然后你可以在 main
函数中调用这个函数:
void main() { float result = add(1.0, 2.0); // ... }
GLSL 提供了许多用于常见操作的内置函数,如 sin
,cos
,max
,min
,abs
,round
,floor
,ceil
,以及其他许多有用的函数,如 mix
,step
,length
,distance
,等等。
我们将在下一节课中探索和练习这些基本的函数。
循环和条件
GLSL 支持 for
循环和 if
语句。它们的工作方式类似于 JavaScript:
for (int i = 0; i < 10; i++) { // 在此处编写代码 } if (condition) { // 在此处编写代码 } else { // 在此处编写代码 }
日志记录 / 调试
由于着色器程序会为每个顶点和片段并行运行,因此无法使用 console.log
来调试代码,也无法添加断点。这就是调试着色器困难的原因。
调试着色器的一种常见方法是使用 gl_FragColor
来可视化变量的值。
编译错误
如果你的着色器代码中出现错误,你将在控制台中看到一个编译错误。它会告诉你错误的行和类型。虽然不总是容易理解,但这是一个知道问题所在的好方法。
让我们从 gl_FragColor
中移除 alpha 通道,看会发生什么:
void main() { gl_FragColor = vec4(1.0, 0.0, 0.0); }
你应该会在控制台中看到一个编译错误:
告诉我们 gl_FragColor
期望有 4 个组件,但我们只提供了 3 个。
别忘了将 alpha 通道恢复为 1.0
以消除错误。
Uniforms
为了从 JavaScript 代码传递数据给 shader,我们使用 uniforms。它们在所有顶点和片段中是常量。
projectionMatrix
、modelViewMatrix
和 position
是自动传递给 shader 的内置 uniform 的示例。
让我们创建一个自定义的 uniform 来将颜色传递给 shader。我们将使用它为平面上色。我们将其命名为 uColor
。在我们的代码中,前缀为 u
的名字是一种好的实践,可以清楚表明它是一个 uniform。
首先,在 shaderMaterial
的 uniform 对象中声明它:
import { Color } from "three"; // ... const MyShaderMaterial = shaderMaterial( { uColor: new Color("pink"), } // ... ); // ...
接着,我们可以在 fragment shader 中使用它:
uniform vec3 uColor; void main() { gl_FragColor = vec4(uColor, 1.0); }
你应该会看到平面被涂成粉色:
这里,粉色是 uniform 的默认值。我们可以直接在材质上更改它:
<MyShaderMaterial uColor={"lightblue"} />
平面现在被染成了淡蓝色。
顶点 shader 和片段 shader 都可以访问 uniforms。我们在顶点 shader 中添加时间作为第二个 uniform,使平面上下移动:
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.