Skip to main content

第 4 周:渲染管线深入

📅 学习计划

本周目标: 掌握 GLSL 语法,理解 Uniform 和 Attribute 的区别,尝试修改 Shader。

时间安排: 每天 1-2 小时,共 5 天


第 1 天:GLSL 语言基础

📖 理论:GLSL 语法

GLSL (OpenGL Shading Language) 是一种类 C 语言。

基本类型:

  • float: 浮点数(GLSL 中非常常用,整数很少用)。
  • int: 整数。
  • bool: 布尔值。
  • vec2, vec3, vec4: 2/3/4 维向量(核心类型)。
  • mat3, mat4: 3x3, 4x4 矩阵。
  • sampler2D: 2D 纹理。

向量操作(超级方便):

vec3 v = vec3(1.0, 2.0, 3.0);
v.xy; // 取前两个分量 (1.0, 2.0) -> vec2
v.z; // 取第三个分量
v.rgb; // 等同于 xyz
v * 2.0; // 所有分量乘 2 -> (2.0, 4.0, 6.0)
v1 + v2; // 向量加法

内置函数:

  • sin, cos, pow, sqrt
  • dot, cross, normalize
  • mix (线性插值), step, smoothstep (平滑阶梯)

💻 实践:项目中的 GLSL

查看代码: line_shader/index.ts:25

// mix 函数的使用
// mix(x, y, a) = x * (1-a) + y * a
// 在两种颜色间混合
vec3 ccc = mix(diffuse, cColor, mask);

查看代码: line_shader/index.ts:32

// step 函数的使用
// step(edge, x) -> 如果 x < edge 返回 0.0,否则返回 1.0
float mask = step(0.5, select);

🎯 练习 4.1:玩转 GLSL

访问 The Book of Shaders 或者在 Three.js Editor 中尝试。

试着写一个简单的颜色混合:

vec3 colorA = vec3(1.0, 0.0, 0.0);
vec3 colorB = vec3(0.0, 0.0, 1.0);
float t = abs(sin(time)); // 让 t 在 0 到 1 之间变化
vec3 finalColor = mix(colorA, colorB, t);
gl_FragColor = vec4(finalColor, 1.0);

📝 今日任务

  • 熟悉 GLSL 的向量写法 (vec3, swizzling)
  • 理解 mixstep 函数

第 2 天:Uniform, Attribute, Varying

📖 理论:三大变量类型

这三个词是 Shader 编程的核心。

  1. Attribute (属性)

    • 来源:从 JavaScript (BufferGeometry) 传给 Vertex Shader
    • 频率:每个顶点不同。
    • 例子:顶点位置(position)、法线(normal)、UV坐标(uv)。
    • 项目例子lineNormal, lineMiter
    • 补充理解:这些 Attribute 本质上都来自 GPU 上的 顶点缓冲(VBO)(也就是 BufferGeometry 里那些 setAttribute(...) 上传后的数据)。
  2. Uniform (统一变量)

    • 来源:从 JavaScript (Material) 传给 Vertex & Fragment Shader
    • 频率:整个 Draw Call(一次绘制)保持不变。
    • 例子:全局颜色、时间、变换矩阵、纹理。
    • 项目例子thickness, opacity, diffuse
  3. Varying (易变变量)

    • 来源Vertex Shader 输出,传给 Fragment Shader
    • 特性:会自动插值
    • 项目例子:如果想让线有渐变色,需要在 VS 中算出颜色,传给 varying,FS 接收。

💻 实践:项目中的数据传递

JavaScript 端 (LineRender.ts)

const material = newCustomizedShader({
thickness: width, // 这是一个 uniform
// ...
});

// Attribute 是通过 BufferGeometry 设置的
const geometry = newCustomizedVertexLine(...);
// 里面会调用 geometry.setAttribute(...)

Shader 端 (line_shader/index.ts)

// 定义
uniform float thickness;
attribute vec2 lineNormal;
varying vec4 vColor; // 用于传递颜色

void main() {
// 使用
vec3 offset = vec3(lineNormal * thickness, 0.0);
}

🎯 练习 4.2:添加一个 Uniform

  1. CustomShaderMaterialuniforms 中添加 time
  2. 在 JS 的 animate 循环中更新 material.uniforms.time.value
  3. 在 Shader 中声明 uniform float time;
  4. time 让线动起来(比如改变宽度或颜色)。

📝 今日任务

  • 能区分 Attribute (顶点独有) 和 Uniform (全局)
  • 理解 Varying 的插值作用

第 3-5 天:综合项目实战 - Shader 分析

🎯 项目实战:实线与虚线 Shader 对比

任务: 对比 SolidShaderDashedShader

SolidShader (实线)

  • 逻辑很简单,直接输出颜色。
  • 只是处理了选中状态 (select)。

DashedShader (虚线)

  • 位于 line_shader/index.ts:44
  • 多了一个 Attribute: lineDistance (当前点离起点的距离)。
  • 多了一个 Varying: lineU (把距离传给 FS)。

虚线原理 (Fragment Shader)

// mod 取模,让距离循环
// dashSteps + dashDistance 是一个周期长度
float lineUMod = mod(lineU, dashSteps + dashDistance);

// step 判断:如果在实线段内返回 0,空隙段返回 1(或者反过来)
float dash = 1.0 - step(dashDistance, lineUMod);

// 如果 dash 是 0,乘以颜色就变成透明/黑色了
vec3 ccc = diffuse * vec3(dash);
gl_FragColor = vec4(ccc.xyz, opacity * dash);

🎯 思考题

  1. 为什么 lineDistance 必须是 Attribute?
    • 因为每个点离起点的距离不一样,必须预先算好存起来。
  2. 如果想让虚线动起来(流动效果)怎么办?
    • 加一个 uniform float time
    • mod(lineU + time, ...)

📝 本周总结

  • 我能看懂项目里的 Shader 代码了。
  • 我明白了虚线是通过 Shader 里的数学计算模拟出来的,而不是真的断开了几何体。
  • 我掌握了 GLSL 的基本语法。

(选修)第 6 天:Three.js TSL 与 WebGPU 方向

这一节是给“想跟上 Three.js 新版和 WebGPU 趋势”的你准备的,不是必须掌握,但非常推荐扫一遍,建立心智模型。

📖 理论:TSL 是什么?

  • TSL(Three.js Shader Language) 是 Three.js 在新版里引入的一套 “JS/TS 友好的着色语言层”
    • 不是完全抛弃 GLSL,而是在 GLSL 之上加了一层更高级的表达方式。
    • 你用 JS/TS 写出“节点/表达式”的组合,TSL 再把它编译成真正跑在 GPU 上的 Shader 代码(WebGL 用 GLSL,WebGPU 用 WGSL)。
  • 可以粗暴地类比为:
    • TSL = TypeScript 世界里的 Shader DSL
    • 有类型检查、自动拼接、可组合的“节点”,最后统一输出底层 Shader。

✅ 和直接写 GLSL 相比,有什么优势?

站在你刚学完 GLSL 的视角,可以这样对比:

  • 1)类型更安全,和 TS 工具链更友好

    • 传统 GLSL:字符串拼接,很容易写错名字、类型,错误在运行时才在控制台爆红。
    • TSL:用函数/对象/链式写法,很多错误在编译期就能被 TS/IDE 提示出来。
  • 2)更易组合和复用

    • GLSL 要复用一段逻辑,通常靠 #include 或复制粘贴,管理起来比较散。
    • TSL 鼓励你写成节点函数(比如一个 BRDF、一个噪声函数),像搭积木一样组合,Three.js 帮你把依赖关系、注入顺序都处理好。
  • 3)跨后端更容易(为 WebGPU 做准备)

    • 直接写 GLSL:强绑定 WebGL,未来要上 WebGPU 时,要翻译成 WGSL 或重写。
    • 写 TSL:上层逻辑不变,由 Three.js 在编译阶段决定输出 GLSL 还是 WGSL。
    • 这就是官方一直强调的:TSL 是“跨 WebGL / WebGPU 的一层公共着色抽象”
  • 4)更接近“材质节点编辑器”的思路

    • 如果你用过 Blender / UE 的材质节点,就会很熟悉这种“节点拼接出材质”的思路。
    • TSL 把这种节点式表达搬到了代码世界,更适合复杂材质/效果的工程化维护。

简单感受一下对比(伪代码,只看味道):

// 传统 GLSL 片段着色器(字符串)
void main() {
vec3 baseColor = texture(uMap, vUv).rgb;
float nDotL = max(dot(normalize(vNormal), normalize(uLightDir)), 0.0);
gl_FragColor = vec4(baseColor * nDotL, 1.0);
}
// TSL 思路(伪代码):用节点和函数组合同样的逻辑
const baseColor = texture( mapTSL, uvTSL ).rgb;
const n = normalize( normalTSL );
const l = normalize( lightDirTSL );
const nDotL = max( dot( n, l ), 0.0 );

material.colorNode = baseColor.mul( nDotL );

你写的是一棵“表达式树”,Three.js 帮你把它编译成真正跑在 GPU 上的 Shader。

再看几个直观一点的“小对比”:

示例 1:纯色 + 时间闪烁

// GLSL
uniform float uTime;

void main() {
float t = abs(sin(uTime));
gl_FragColor = vec4(t, 0.0, 1.0 - t, 1.0);
}
// TSL(伪代码)
const t = abs( sin( timeTSL ) );
material.colorNode = vec3( t, 0.0, one.sub( t ) );

示例 2:使用一张遮罩纹理控制透明度

// GLSL
uniform sampler2D uMask;
varying vec2 vUv;

void main() {
float m = texture2D(uMask, vUv).r;
gl_FragColor = vec4(1.0, 1.0, 1.0, m);
}
// TSL(伪代码)
const m = texture( maskTSL, uvTSL ).r;
material.colorNode = vec4( one3, m );

示例 3:顶点位移(简单波浪)

// GLSL 顶点着色器
uniform float uTime;
attribute vec3 position;
attribute vec2 uv;

void main() {
vec3 pos = position;
pos.z += sin(pos.x * 4.0 + uTime) * 0.1;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
// TSL(伪代码)
const pos = positionTSL.xyz;
const wave = sin( pos.x.mul( 4.0 ).add( timeTSL ) ).mul( 0.1 );
const displaced = vec3( pos.x, pos.y, pos.z.add( wave ) );

material.positionNode = displaced;

可以看到:

  • 左边 GLSL:你在字符串里写完整的 Shader 源码。
  • 右边 TSL:你在 TS 代码里用“节点 + 表达式”描述同样的计算,Three.js 把这些节点拼成最终 Shader。

🔗 TSL 和 WebGPU 的关系

  • WebGPU 使用 WGSL,而不是 GLSL

    • WebGPU 的官方着色语言是 WGSL(WebGPU Shading Language),语法和 GLSL 有明显差异。
    • 这意味着:如果你项目未来要跑在 WebGPU 上,只会 GLSL 不够用
  • TSL 的目标之一:让同一套材质逻辑可以跑在 WebGL + WebGPU 上

    • 上层你写的还是 TSL 节点;
    • 底层:
      • 在 WebGL Renderer 下输出 GLSL;
      • 在 WebGPU Renderer 下输出 WGSL。
    • 对你来说,迁移成本更像是“换一个 Renderer + 开启部分特性”,而不是“重写所有 Shader”

所以你可以把 TSL 看成 Three.js 在为未来的 WebGPU 渲染管线“铺的路”:
今天用 TSL 写材质,未来换成 WebGPU 时,更大概率是“换后端”,而不是“推翻重写”。

📝 今日任务(选修)

  • 了解 Three.js 官方文档中关于 TSL / NodeMaterial 的介绍。
  • 用 TSL 写一个最简单的“颜色 = 纹理 * 漫反射光照”的材质,和你之前的 GLSL 版本对比。
  • 思考一下:如果未来要把项目迁到 WebGPU,上层逻辑/材质如何尽量写在 TSL 层,让底层切换成本最小。

下一步: 第 5-6 周:实战项目应用