Skip to main content

01. GLSL 简介与环境搭建

🖼️ 宏观图景:WebGL 渲染管线

在开始写代码之前,我们需要先理解 WebGL 是如何把 3D 数据变成屏幕上的 2D 图像的。这个过程被称为渲染管线 (Rendering Pipeline)

简单来说,这是一个流水线工厂:

  1. CPU 阶段 (准备数据)

    • 我们在 JavaScript 中定义几何体(顶点)、材质颜色、相机位置等。
    • 将这些数据发送给 GPU。
  2. 顶点处理阶段 (Vertex Processing)

    • 🏭 车间主任:顶点着色器 (Vertex Shader)
    • GPU 拿到每一个顶点,计算它在屏幕上的最终位置。
    • 比如:把一个 3D 旋转立方体的角点,换算成屏幕上的 2D 坐标。
  3. 图元装配 (Primitive Assembly)

    • 🔨 组装工
    • 将顶点连接成具体的形状(通常是三角形)。
    • 这一步会进行裁剪 (Clipping),切掉屏幕视野之外的部分。
  4. 光栅化阶段 (Rasterization)

    • 🧱 像素化
    • 找出这些三角形覆盖了屏幕上的哪些像素。
    • 生成待处理的“片段 (Fragment)”。
  5. 片段处理阶段 (Fragment Processing)

    • 🎨 车间主任:片段着色器 (Fragment Shader)
    • 对每一个片段,计算它的颜色。
    • 这一步决定了物体的最终外观(颜色、光照、纹理)。
  6. 输出合并

    • 将计算好的颜色显示在屏幕上。

🎯 什么是 GLSL?

GLSL (OpenGL Shading Language) 是一种类 C 的编程语言,专门用于编写上述管线中的着色器

与运行在 CPU 上的 JavaScript/TypeScript 不同,GLSL 代码直接运行在 GPU 上,并且是并行执行的。这意味着如果你要渲染屏幕上的 100 万个像素,GPU 会同时启动成千上万个微小的处理器来分别计算每个像素的颜色,而不是像 CPU 那样一个接一个地循环处理。

核心任务

在 WebGL 中,我们主要通过 GLSL 编写两个函数来控制管线:

  1. 顶点着色器 (Vertex Shader)

    • 任务:告诉 GPU 顶点在哪里。
    • 输出gl_Position (屏幕坐标)。
    • 比喻:就像是把充气玩偶(3D 模型)拍扁在照片(2D 屏幕)上的过程。
  2. 片段着色器 (Fragment Shader)

    • 任务:告诉 GPU 像素是什么颜色。
    • 输出gl_FragColor (RGBA 颜色)。
    • 比喻:给照片上的每个点涂上颜色。

🛠️ 环境搭建 (原生 Three.js)

为了彻底理解渲染管线,我们将抛弃 React Three Fiber 的封装,使用最纯粹的 原生 Three.js 配合 RawShaderMaterial

这意味着你必须手动管理场景、相机、渲染器循环,并且手动声明 Shader 中的所有变量。这是掌握 WebGL 的必经之路。

基础模板 (Vanilla JS)

你可以创建一个 HTML 文件,或者在你的 JS 入口文件中写入以下代码:

import * as THREE from "three";

// 1. 初始化场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 2. 定义 Shader 代码
// 顶点着色器
const vertexShader = `
// 设置浮点数精度为中等(WebGL 必须)
precision mediump float;
precision mediump int;

// 接收矩阵 (Three.js 传入)
// projectionMatrix: 投影矩阵 (3D -> 2D)
// modelViewMatrix: 模型视图矩阵 (世界坐标 -> 相机坐标)
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;

// 接收属性 (BufferGeometry 里的数据)
// position: 顶点原始位置
attribute vec3 position;

void main() {
// 1.0 是 w 分量 (齐次坐标),w=1.0 表示这是一个“点”,受平移影响
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;

// 片段着色器
const fragmentShader = `
precision mediump float;

uniform float uTime;
uniform vec3 uColor;

void main() {
// uColor 是 RGB,1.0 是 Alpha (透明度)
gl_FragColor = vec4(uColor, 1.0);
}
`;

// 3. 创建物体
// PlaneGeometry 内部会自动创建 BufferGeometry
// 默认包含以下属性 (Attributes):
// - 'position': 顶点位置 (vec3)
// - 'uv': 纹理坐标 (vec2)
// - 'normal': 法线 (vec3)
const geometry = new THREE.PlaneGeometry(2, 2);

// 如果你要手动创建自定义 attribute:
// const vertices = new Float32Array([...]);
// geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
// geometry.setAttribute('myCustomAttr', new THREE.BufferAttribute(data, 1));

// 创建 Uniforms 数据
const uniforms = {
uTime: { value: 0 },
uColor: { value: new THREE.Color(0.0, 0.5, 1.0) },
};

const material = new THREE.RawShaderMaterial({
vertexShader,
fragmentShader,
uniforms,
});

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

camera.position.z = 5;

// 4. 渲染循环
const clock = new THREE.Clock();

function animate() {
requestAnimationFrame(animate);

// 更新时间 Uniform
uniforms.uTime.value = clock.getElapsedTime();

renderer.render(scene, camera);
}

animate();

📖 核心语法速查 (预告)

你在上面的 Shader 代码中可能注意到了几个关键声明,这里先简单混个脸熟(第 2、4 章会详细讲解):

  1. precision mediump float;

    • 含义:告诉 GPU 用“中等精度”来存储浮点数。
    • 原因:WebGL 为了优化性能,默认不指定精度,因此必须手动声明,否则会报错。
  2. attribute (属性)

    • 含义每个顶点独有的数据。
    • 例子position(每个点位置都不同)。
    • 来源:从 JS 的 BufferGeometry 传给 Vertex Shader。
  3. uniform (统一变量)

    • 含义所有顶点/像素共用的数据(全局常量)。
    • 例子uTime(所有点在同一时刻的时间是一样的)、modelViewMatrix(物体整体的变换)。
    • 来源:从 JS 的 uniforms 对象传给 Shader。

为什么选择原生写法 + RawShaderMaterial

  1. 去魅:没有 React 的声明式封装,你清楚地看到 scene.add(mesh)renderer.render() 是何时发生的。
  2. 拒绝黑盒RawShaderMaterial 强迫你显式写出 attribute vec3 position,让你深刻理解“数据是从 BufferGeometry 来的”。
  3. 通用性:这段代码的核心逻辑可以轻松移植到任何 WebGL 引擎(甚至原生 WebGL)。

🚀 总结

  • 渲染管线是将数据从 CPU 传输到 GPU,经过顶点处理、光栅化,最终由片段处理上色的过程。
  • GLSL 是我们在管线中编写自定义逻辑的语言。
  • 顶点着色器决定位置,片段着色器决定颜色。
  • 我们使用 原生 Three.js + RawShaderMaterial 进行练习,手动管理渲染循环和变量声明。

📝 编程作业

难度:⭐ 简单 | 预计时间:30-45 分钟

任务要求

  1. 环境搭建:按照教程搭建原生 Three.js 环境
  2. 第一个 Shader:创建一个平面(PlaneGeometry),使用 RawShaderMaterial
  3. 基础效果:实现一个简单的单色平面(比如蓝色)
  4. 动态效果:添加一个 uTime uniform,让颜色随时间变化

具体要求

  • ✅ 必须使用 RawShaderMaterial(不能用 ShaderMaterial
  • ✅ 必须手动声明所有变量(precisionattributeuniform
  • ✅ 实现基本的渲染循环
  • ✅ 颜色变化要平滑(可以使用 sin(uTime) 控制亮度)

代码框架

// 顶点着色器
const vertexShader = `
precision mediump float;
precision mediump int;

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
attribute vec3 position;

void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;

// 片段着色器(需要你完成)
const fragmentShader = `
precision mediump float;

uniform float uTime; // 时间 uniform

void main() {
// TODO: 使用 uTime 创建动态颜色效果
// 提示:可以使用 sin(uTime) 或 cos(uTime)
gl_FragColor = vec4(0.0, 0.5, 1.0, 1.0); // 先写一个固定颜色
}
`;

扩展挑战

  • 🌟 让颜色在 RGB 三个通道上分别使用不同的时间函数
  • 🌟 添加鼠标交互,让颜色随鼠标位置变化
  • 🌟 尝试修改顶点位置(在顶点着色器中)

参考资源