着色器简介

Starter pack

终于到了探索 着色器 世界的时刻了。着色器是创建各种视觉效果的关键。在本章中,我们将学习什么是着色器,它们能实现什么,以及如何在 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 的结构图

对于几何体的每个顶点,vertex shader 将被执行。

最后,顶点在 2D 空间中的变换位置通过预定义变量 gl_Position 返回。在所有顶点被变换后,GPU 在它们之间插值以生成几何体的面,接着进行光栅化和渲染到屏幕上。

片段着色器

片段着色器,也称为像素着色器,是在光栅化过程生成的每个片段(或像素)上执行的程序。它的主要任务是确定屏幕上每个像素的最终颜色。

片段着色器示意图

在光栅化过程中生成的每个片段上,都会执行片段着色器。

片段着色器接收来自顶点着色器的插值值,如颜色、纹理坐标、法线以及与几何体顶点相关的任何其他属性。这些插值值称为 varyings,提供了有关每个片段位置的表面属性的信息。

除了插值值之外,片段着色器还可能采样纹理并访问在所有片段中都是恒定的 uniform 变量。这些 uniform 变量可以表示诸如光源位置、材质属性或着色计算所需的任何其他数据。

我们将在本课后面的部分进一步讨论 attributesuniforms

通过使用输入数据,片段着色器执行各种计算以确定片段的最终颜色。这可能涉及复杂的光照计算、纹理映射、着色效果或场景中所需的任何其他视觉效果。

一旦颜色计算完成,片段着色器使用预定义变量 gl_FragColor 输出片段的最终颜色。

我尽量简单地解释着色器,并故意省略了一些技术细节,但我理解这可能仍然有些抽象。让我们创建一个简单的着色器来看看它在实践中的运作方式。

您的第一个着色器

让我们运行启动包。你应该会看到这个框,中间有一个黑色平面:

一个带有黑色平面的框架

打开名为 ShaderPlane.jsx 的文件,它包含一个简单的具有平面几何体和基本材质的 mesh。我们将用自定义着色器材质替换该材质。

shaderMaterial

要创建一个着色器材质,我们使用 Drei libraryshaderMaterial 函数。

它需要三个参数:

  • 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 代码。

GLSL syntax highlighter

安装后,要启用语法高亮器,你需要在 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 代码被高亮显示:

GLSL syntax highlighter in action

上面的顶点 shader 现在具有正确的语法高亮,更容易阅读。

这就是内联 shader 代码所需要的全部内容。如果你更喜欢将 shader 代码分开,你仍然可以选择使用外部文件。让我们来看看如何做到这一点。

导入 GLSL 文件

首先,在 src 文件夹中创建一个名为 shaders 的新文件夹。在此文件夹中,创建两个文件:myshader.vertex.glslmyshader.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: 布尔值(truefalse)。
  • int: 整数。
  • float: 浮点数。
  • vectors: 数字集合。vec2 是包含 2 个浮点数 (xy) 的集合,vec3 是包含 3 个浮点数 (xyz) 的集合,vec4 是包含 4 个浮点数 (xyzw) 的集合。除了使用 xyzw,你也可以使用 rgba 表示颜色,它们可以互换。
  • matrices: 向量集合。例如,mat2 是包含 2 个向量的集合,mat3 是包含 3 个向量的集合,mat4 是包含 4 个向量的集合。

Swizzling 和操作

您可以使用 swizzling 访问向量的组件。例如,您可以使用另一个向量的组件创建一个新向量:

vec3 a = vec3(1.0, 2.0, 3.0);
vec2 b = a.xy;

在此示例中,b 将是一个带有 axy 组件的向量。

您还可以使用 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 提供了许多用于常见操作的内置函数,如 sincosmaxminabsroundfloorceil,以及其他许多有用的函数,如 mixsteplengthdistance,等等。

我们将在下一节课中探索和练习这些基本的函数。

循环和条件

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。它们在所有顶点和片段中是常量。

projectionMatrixmodelViewMatrixposition 是自动传递给 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);
}

你应该会看到平面被涂成粉色:

A frame with a pink plane

这里,粉色是 uniform 的默认值。我们可以直接在材质上更改它:

<MyShaderMaterial uColor={"lightblue"} />

A frame with a light blue plane

平面现在被染成了淡蓝色。

顶点 shader 和片段 shader 都可以访问 uniforms。我们在顶点 shader 中添加时间作为第二个 uniform,使平面上下移动:

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.