01. GLSL 简介与环境搭建
🖼️ 宏观图景:WebGL 渲染管线
在开始写代码之前,我们需要先理解 WebGL 是如何把 3D 数据变成屏幕上的 2D 图像的。这个过程被称为渲染管线 (Rendering Pipeline)。
简单来说,这是一个流水线工厂:
CPU 阶段 (准备数据)
- 我们在 JavaScript 中定义几何体(顶点)、材质颜色、相机位置等。
- 将这些数据发送给 GPU。
顶点处理阶段 (Vertex Processing)
- 🏭 车间主任:顶点着色器 (Vertex Shader)
- GPU 拿到每一个顶点,计算它在屏幕上的最终位置。
- 比如:把一个 3D 旋转立方体的角点,换算成屏幕上的 2D 坐标。
图元装配 (Primitive Assembly)
- 🔨 组装工
- 将顶点连接成具体的形状(通常是三角形)。
- 这一步会进行裁剪 (Clipping),切掉屏幕视野之外的部分。
光栅化阶段 (Rasterization)
- 🧱 像素化
- 找出这些三角形覆盖了屏幕上的哪些像素。
- 生成待处理的“片段 (Fragment)”。
片段处理阶段 (Fragment Processing)
- 🎨 车间主任:片段着色器 (Fragment Shader)
- 对每一个片段,计算它的颜色。
- 这一步决定了物体的最终外观(颜色、光照、纹理)。
输出合并
- 将计算好的颜色显示在屏幕上。
🎯 什么是 GLSL?
GLSL (OpenGL Shading Language) 是一种类 C 的编程语言,专门用于编写上述管线中的着色器。
与运行在 CPU 上的 JavaScript/TypeScript 不同,GLSL 代码直接运行在 GPU 上,并且是并行执行的。这意味着如果你要渲染屏幕上的 100 万个像素,GPU 会同时启动成千上万个微小的处理器来分别计算每个像素的颜色,而不是像 CPU 那样一个接一个地循环处理。
核心任务
在 WebGL 中,我们主要通过 GLSL 编写两个函数来控制管线:
顶点着色器 (Vertex Shader):
- 任务:告诉 GPU 顶点在哪里。
- 输出:
gl_Position(屏幕坐标)。 - 比喻:就像是把充气玩偶(3D 模型)拍扁在照片(2D 屏幕)上的过程。
片段着色器 (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 章会详细讲解):
precision mediump float;- 含义:告诉 GPU 用“中等精度”来存储浮点数。
- 原因:WebGL 为了优化性能,默认不指定精度,因此必须手动声明,否则会报错。
attribute(属性)- 含义:每个顶点独有的数据。
- 例子:
position(每个点位置都不同)。 - 来源:从 JS 的
BufferGeometry传给 Vertex Shader。
uniform(统一变量)- 含义:所有顶点/像素共用的数据(全局常量)。
- 例子:
uTime(所有点在同一时刻的时间是一样的)、modelViewMatrix(物体整体的变换)。 - 来源:从 JS 的
uniforms对象传给 Shader。
为什么选择原生写法 + RawShaderMaterial?
- 去魅:没有 React 的声明式封装,你清楚地看到
scene.add(mesh)和renderer.render()是何时发生的。 - 拒绝黑盒:
RawShaderMaterial强迫你显式写出attribute vec3 position,让你深刻理解“数据是从 BufferGeometry 来的”。 - 通用性:这段代码的核心逻辑可以轻松移植到任何 WebGL 引擎(甚至原生 WebGL)。
🚀 总结
- 渲染管线是将数据从 CPU 传输到 GPU,经过顶点处理、光栅化,最终由片段处理上色的过程。
- GLSL 是我们在管线中编写自定义逻辑的语言。
- 顶点着色器决定位置,片段着色器决定颜色。
- 我们使用 原生 Three.js +
RawShaderMaterial进行练习,手动管理渲染循环和变量声明。
📝 编程作业
难度:⭐ 简单 | 预计时间:30-45 分钟
任务要求
- 环境搭建:按照教程搭建原生 Three.js 环境
- 第一个 Shader:创建一个平面(PlaneGeometry),使用
RawShaderMaterial - 基础效果:实现一个简单的单色平面(比如蓝色)
- 动态效果:添加一个
uTimeuniform,让颜色随时间变化
具体要求
- ✅ 必须使用
RawShaderMaterial(不能用ShaderMaterial) - ✅ 必须手动声明所有变量(
precision、attribute、uniform) - ✅ 实现基本的渲染循环
- ✅ 颜色变化要平滑(可以使用
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 三个通道上分别使用不同的时间函数
- 🌟 添加鼠标交互,让颜色随鼠标位置变化
- 🌟 尝试修改顶点位置(在顶点着色器中)