Skip to content

第3章:着色器与 GLSL

3.1 章节概述

着色器是 WebGL 的灵魂。在前两章中,我们编写了简单的着色器代码,但并没有深入理解它们。本章将带你全面掌握 GLSL(OpenGL Shading Language)——WebGL 使用的着色器编程语言。

掌握 GLSL 是成为 WebGL 专家的必经之路。通过着色器,你可以实现:

  • 复杂的 3D 变换和动画
  • 逼真的光照和材质效果
  • 图像处理和后处理特效
  • 程序化纹理和图形生成
  • GPU 加速的并行计算

本章将深入讲解:

  • 着色器程序的完整生命周期:从源码到可执行程序
  • GLSL 语法详解:数据类型、操作符、流程控制
  • 变量限定符:attribute、uniform、varying 的深入理解
  • 内置变量和函数:gl_Position、gl_FragColor 等
  • 着色器调试和优化技巧

3.2 着色器基础概念

3.2.1 什么是着色器?

着色器(Shader) 是运行在 GPU 上的小程序。与 JavaScript 运行在 CPU 上不同,着色器程序被编译成 GPU 指令,由 GPU 的众多处理核心并行执行。

着色器的工作方式

CPU (JavaScript)                    GPU (GLSL 着色器)
┌──────────────────┐               ┌─────────────────────────────┐
│                  │               │                             │
│  准备数据        │  ─────────→  │  ┌───┐ ┌───┐ ┌───┐ ┌───┐   │
│  设置 uniform    │               │  │ VS│ │ VS│ │ VS│ │ VS│   │
│  调用 draw       │               │  └───┘ └───┘ └───┘ └───┘   │
│                  │               │    ↓     ↓     ↓     ↓     │
│  等待结果...     │  ←─────────  │  ┌───┐ ┌───┐ ┌───┐ ┌───┐   │
│                  │               │  │ FS│ │ FS│ │ FS│ │ FS│   │
└──────────────────┘               │  └───┘ └───┘ └───┘ └───┘   │
                                   │         ...                │
                                   │  (成千上万个并行执行)      │
                                   └─────────────────────────────┘

VS = 顶点着色器(每顶点执行一次)
FS = 片段着色器(每像素执行一次)

3.2.2 两种着色器的角色

WebGL 需要两种着色器协同工作:

顶点着色器(Vertex Shader)

顶点着色器的职责

输入:顶点属性(位置、颜色、纹理坐标等)
输出:裁剪空间位置 (gl_Position) + 传递给片段着色器的数据

┌─────────────────────────────────────────────────┐
│                  顶点着色器                      │
├─────────────────────────────────────────────────┤
│                                                 │
│   主要任务:                                    │
│   1. 坐标变换(模型空间 → 裁剪空间)            │
│   2. 计算需要传递的数据(法线、纹理坐标等)     │
│   3. 逐顶点光照计算(可选)                     │
│                                                 │
│   执行频率:                                    │
│   每个顶点执行一次                              │
│   1000 个顶点 = 执行 1000 次                    │
│                                                 │
└─────────────────────────────────────────────────┘

片段着色器(Fragment Shader)

片段着色器的职责

输入:插值后的数据(从顶点着色器)、纹理、uniform
输出:片段颜色 (gl_FragColor)

┌─────────────────────────────────────────────────┐
│                  片段着色器                      │
├─────────────────────────────────────────────────┤
│                                                 │
│   主要任务:                                    │
│   1. 计算最终颜色                               │
│   2. 纹理采样                                   │
│   3. 光照计算(逐像素)                         │
│   4. 特效处理                                   │
│                                                 │
│   执行频率:                                    │
│   每个片段(像素)执行一次                      │
│   1920×1080 分辨率 = 可能执行 200 万次/帧       │
│                                                 │
└─────────────────────────────────────────────────┘

3.2.3 着色器程序(Program)

着色器程序是顶点着色器和片段着色器链接后的可执行对象:

着色器程序结构

┌─────────────────────────────────────────────────┐
│               着色器程序 (Program)               │
├─────────────────────────────────────────────────┤
│                                                 │
│   ┌──────────────┐     ┌──────────────┐         │
│   │  顶点着色器  │ ←─→ │  片段着色器  │         │
│   │              │     │              │         │
│   │ - attribute  │     │ - varying    │         │
│   │ - uniform    │ ──→ │ - uniform    │         │
│   │ - varying    │     │ - sampler    │         │
│   │              │     │              │         │
│   │ 输出:        │     │ 输出:        │         │
│   │ gl_Position  │     │ gl_FragColor │         │
│   └──────────────┘     └──────────────┘         │
│                                                 │
│   共享的 uniform 变量                           │
│   ┌────────────────────────────────────────┐    │
│   │ u_modelMatrix, u_viewMatrix, ...       │    │
│   └────────────────────────────────────────┘    │
│                                                 │
└─────────────────────────────────────────────────┘

3.3 着色器程序的生命周期

3.3.1 完整的创建流程

创建一个可用的着色器程序需要以下步骤:

着色器程序创建详细流程

步骤 1: 创建着色器对象
─────────────────────────────────────────────
gl.createShader(gl.VERTEX_SHADER)   → vertexShader
gl.createShader(gl.FRAGMENT_SHADER) → fragmentShader

步骤 2: 设置源代码
─────────────────────────────────────────────
gl.shaderSource(vertexShader, vertexSource)
gl.shaderSource(fragmentShader, fragmentSource)

步骤 3: 编译着色器
─────────────────────────────────────────────
gl.compileShader(vertexShader)
gl.compileShader(fragmentShader)

检查编译状态:
gl.getShaderParameter(shader, gl.COMPILE_STATUS)

获取错误信息:
gl.getShaderInfoLog(shader)

步骤 4: 创建程序对象
─────────────────────────────────────────────
gl.createProgram() → program

步骤 5: 附加着色器
─────────────────────────────────────────────
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)

步骤 6: 链接程序
─────────────────────────────────────────────
gl.linkProgram(program)

检查链接状态:
gl.getProgramParameter(program, gl.LINK_STATUS)

获取错误信息:
gl.getProgramInfoLog(program)

步骤 7: 清理着色器对象(可选但推荐)
─────────────────────────────────────────────
gl.deleteShader(vertexShader)
gl.deleteShader(fragmentShader)

步骤 8: 使用程序
─────────────────────────────────────────────
gl.useProgram(program)

3.3.2 完整的工具类实现

javascript
/**
 * 着色器程序管理器
 * 提供完整的着色器创建、编译、链接、调试功能
 */
class ShaderProgram {
    /**
     * @param {WebGLRenderingContext} gl - WebGL 上下文
     */
    constructor(gl) {
        this.gl = gl;
        this.program = null;
        this.uniformLocations = new Map();
        this.attributeLocations = new Map();
        this.uniformSetters = new Map();
    }
    
    /**
     * 从源代码创建程序
     * @param {string} vertexSource - 顶点着色器源码
     * @param {string} fragmentSource - 片段着色器源码
     * @returns {boolean} 是否成功
     */
    create(vertexSource, fragmentSource) {
        const gl = this.gl;
        
        // 编译两个着色器
        const vertexShader = this._compileShader(gl.VERTEX_SHADER, vertexSource);
        if (!vertexShader) return false;
        
        const fragmentShader = this._compileShader(gl.FRAGMENT_SHADER, fragmentSource);
        if (!fragmentShader) {
            gl.deleteShader(vertexShader);
            return false;
        }
        
        // 创建程序
        this.program = gl.createProgram();
        gl.attachShader(this.program, vertexShader);
        gl.attachShader(this.program, fragmentShader);
        
        // 链接程序
        gl.linkProgram(this.program);
        
        // 检查链接状态
        if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
            console.error('程序链接失败:');
            console.error(gl.getProgramInfoLog(this.program));
            this._cleanup(vertexShader, fragmentShader);
            return false;
        }
        
        // 验证程序(可选,用于开发调试)
        gl.validateProgram(this.program);
        if (!gl.getProgramParameter(this.program, gl.VALIDATE_STATUS)) {
            console.warn('程序验证警告:', gl.getProgramInfoLog(this.program));
        }
        
        // 删除着色器对象(已链接到程序)
        gl.deleteShader(vertexShader);
        gl.deleteShader(fragmentShader);
        
        // 自动获取所有 uniform 和 attribute
        this._introspect();
        
        return true;
    }
    
    /**
     * 编译单个着色器
     * @private
     */
    _compileShader(type, source) {
        const gl = this.gl;
        const shader = gl.createShader(type);
        
        gl.shaderSource(shader, source);
        gl.compileShader(shader);
        
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            const typeName = type === gl.VERTEX_SHADER ? '顶点' : '片段';
            console.error(`${typeName}着色器编译失败:`);
            
            // 解析错误信息,提供更友好的输出
            const errorLog = gl.getShaderInfoLog(shader);
            this._formatShaderError(source, errorLog);
            
            gl.deleteShader(shader);
            return null;
        }
        
        return shader;
    }
    
    /**
     * 格式化着色器编译错误
     * @private
     */
    _formatShaderError(source, errorLog) {
        console.error(errorLog);
        
        // 解析错误行号
        const errorMatch = errorLog.match(/ERROR:\s*\d+:(\d+)/);
        if (errorMatch) {
            const errorLine = parseInt(errorMatch[1]);
            const lines = source.split('\n');
            
            console.error('\n源代码上下文:');
            console.error('─'.repeat(50));
            
            // 显示错误行前后的代码
            const start = Math.max(0, errorLine - 3);
            const end = Math.min(lines.length, errorLine + 2);
            
            for (let i = start; i < end; i++) {
                const lineNum = (i + 1).toString().padStart(4, ' ');
                const marker = (i + 1) === errorLine ? ' >>>' : '    ';
                console.error(`${marker}${lineNum} | ${lines[i]}`);
            }
            
            console.error('─'.repeat(50));
        }
    }
    
    /**
     * 自动发现所有 uniform 和 attribute
     * @private
     */
    _introspect() {
        const gl = this.gl;
        
        // 获取所有 uniform
        const numUniforms = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS);
        for (let i = 0; i < numUniforms; i++) {
            const info = gl.getActiveUniform(this.program, i);
            const location = gl.getUniformLocation(this.program, info.name);
            
            // 处理数组 uniform(名字会带 [0])
            const name = info.name.replace('[0]', '');
            this.uniformLocations.set(name, location);
            
            // 创建 setter
            this.uniformSetters.set(name, this._createUniformSetter(info, location));
        }
        
        // 获取所有 attribute
        const numAttributes = gl.getProgramParameter(this.program, gl.ACTIVE_ATTRIBUTES);
        for (let i = 0; i < numAttributes; i++) {
            const info = gl.getActiveAttrib(this.program, i);
            const location = gl.getAttribLocation(this.program, info.name);
            this.attributeLocations.set(info.name, location);
        }
        
        console.log('Uniforms:', Array.from(this.uniformLocations.keys()));
        console.log('Attributes:', Array.from(this.attributeLocations.keys()));
    }
    
    /**
     * 创建 uniform setter 函数
     * @private
     */
    _createUniformSetter(info, location) {
        const gl = this.gl;
        const type = info.type;
        const size = info.size;
        
        // 根据类型返回对应的 setter
        const setters = {
            [gl.FLOAT]: (v) => gl.uniform1f(location, v),
            [gl.FLOAT_VEC2]: (v) => gl.uniform2fv(location, v),
            [gl.FLOAT_VEC3]: (v) => gl.uniform3fv(location, v),
            [gl.FLOAT_VEC4]: (v) => gl.uniform4fv(location, v),
            [gl.INT]: (v) => gl.uniform1i(location, v),
            [gl.INT_VEC2]: (v) => gl.uniform2iv(location, v),
            [gl.INT_VEC3]: (v) => gl.uniform3iv(location, v),
            [gl.INT_VEC4]: (v) => gl.uniform4iv(location, v),
            [gl.BOOL]: (v) => gl.uniform1i(location, v ? 1 : 0),
            [gl.FLOAT_MAT2]: (v) => gl.uniformMatrix2fv(location, false, v),
            [gl.FLOAT_MAT3]: (v) => gl.uniformMatrix3fv(location, false, v),
            [gl.FLOAT_MAT4]: (v) => gl.uniformMatrix4fv(location, false, v),
            [gl.SAMPLER_2D]: (v) => gl.uniform1i(location, v),
            [gl.SAMPLER_CUBE]: (v) => gl.uniform1i(location, v),
        };
        
        return setters[type] || ((v) => {
            console.warn(`未知的 uniform 类型: ${type}`);
        });
    }
    
    /**
     * 清理资源
     * @private
     */
    _cleanup(vertexShader, fragmentShader) {
        const gl = this.gl;
        if (this.program) {
            gl.deleteProgram(this.program);
            this.program = null;
        }
        if (vertexShader) gl.deleteShader(vertexShader);
        if (fragmentShader) gl.deleteShader(fragmentShader);
    }
    
    /**
     * 使用这个程序
     */
    use() {
        this.gl.useProgram(this.program);
    }
    
    /**
     * 设置 uniform 值
     * @param {string} name - uniform 名称
     * @param {*} value - 值
     */
    setUniform(name, value) {
        const setter = this.uniformSetters.get(name);
        if (setter) {
            setter(value);
        } else {
            console.warn(`未找到 uniform: ${name}`);
        }
    }
    
    /**
     * 批量设置 uniform
     * @param {Object} uniforms - uniform 名称和值的映射
     */
    setUniforms(uniforms) {
        for (const [name, value] of Object.entries(uniforms)) {
            this.setUniform(name, value);
        }
    }
    
    /**
     * 获取 attribute 位置
     * @param {string} name - attribute 名称
     * @returns {number} 位置
     */
    getAttributeLocation(name) {
        return this.attributeLocations.get(name) ?? -1;
    }
    
    /**
     * 获取 uniform 位置
     * @param {string} name - uniform 名称
     * @returns {WebGLUniformLocation} 位置
     */
    getUniformLocation(name) {
        return this.uniformLocations.get(name) ?? null;
    }
    
    /**
     * 释放资源
     */
    dispose() {
        if (this.program) {
            this.gl.deleteProgram(this.program);
            this.program = null;
        }
        this.uniformLocations.clear();
        this.attributeLocations.clear();
        this.uniformSetters.clear();
    }
}

3.3.3 使用示例

javascript
// 创建着色器程序
const shader = new ShaderProgram(gl);

const success = shader.create(`
    attribute vec3 a_position;
    attribute vec2 a_texCoord;
    
    uniform mat4 u_mvpMatrix;
    uniform float u_time;
    
    varying vec2 v_texCoord;
    
    void main() {
        vec3 pos = a_position;
        pos.y += sin(u_time + pos.x * 5.0) * 0.1;  // 波动效果
        
        gl_Position = u_mvpMatrix * vec4(pos, 1.0);
        v_texCoord = a_texCoord;
    }
`, `
    precision mediump float;
    
    varying vec2 v_texCoord;
    uniform sampler2D u_texture;
    uniform vec3 u_tintColor;
    
    void main() {
        vec4 texColor = texture2D(u_texture, v_texCoord);
        gl_FragColor = vec4(texColor.rgb * u_tintColor, texColor.a);
    }
`);

if (!success) {
    console.error('着色器创建失败');
}

// 渲染时
shader.use();
shader.setUniforms({
    u_mvpMatrix: mvpMatrix,
    u_time: performance.now() / 1000,
    u_texture: 0,  // 纹理单元索引
    u_tintColor: [1.0, 0.8, 0.6]
});

gl.drawArrays(gl.TRIANGLES, 0, vertexCount);

3.4 GLSL 语法详解

3.4.1 版本和预处理器

glsl
// WebGL 1.0 使用 GLSL ES 1.0,不需要版本声明
// 以下代码用于 WebGL 1.0

// WebGL 2.0 使用 GLSL ES 3.0,必须声明版本
#version 300 es
precision highp float;

// 预处理器指令
#define PI 3.14159265359
#define TWO_PI (PI * 2.0)

// 条件编译
#ifdef USE_NORMAL_MAP
    varying vec3 v_tangent;
    varying vec3 v_bitangent;
#endif

// 检查扩展
#extension GL_OES_standard_derivatives : enable

3.4.2 基本数据类型

GLSL 提供了丰富的数据类型:

glsl
// 标量类型
bool flag = true;           // 布尔值
int count = 42;             // 整数
float value = 3.14;         // 浮点数

// 向量类型 - 最常用的类型
vec2 position2D = vec2(1.0, 2.0);           // 2D 向量
vec3 position3D = vec3(1.0, 2.0, 3.0);      // 3D 向量
vec4 color = vec4(1.0, 0.0, 0.0, 1.0);      // 4D 向量 (常用于颜色 RGBA)

ivec2 pixelCoord = ivec2(100, 200);         // 整数向量
bvec3 results = bvec3(true, false, true);   // 布尔向量

// 矩阵类型
mat2 rotation2D = mat2(1.0);                // 2×2 矩阵
mat3 normalMatrix = mat3(1.0);              // 3×3 矩阵
mat4 modelMatrix = mat4(1.0);               // 4×4 矩阵 (最常用)

// 采样器类型 (只能作为 uniform)
uniform sampler2D u_texture;                // 2D 纹理采样器
uniform samplerCube u_cubemap;              // 立方体纹理采样器

3.4.3 向量构造和访问

向量是 GLSL 中最重要的数据类型,它有多种构造和访问方式:

glsl
// ============ 向量构造 ============

// 从标量构造
vec3 v1 = vec3(1.0);              // (1.0, 1.0, 1.0) - 所有分量相同
vec4 v2 = vec4(1.0, 2.0, 3.0, 4.0);

// 从其他向量构造
vec2 xy = vec2(1.0, 2.0);
vec3 v3 = vec3(xy, 3.0);          // (1.0, 2.0, 3.0)
vec4 v4 = vec4(v3, 4.0);          // (1.0, 2.0, 3.0, 4.0)
vec4 v5 = vec4(xy, xy);           // (1.0, 2.0, 1.0, 2.0)

// ============ 分量访问 ============

vec4 color = vec4(1.0, 0.5, 0.2, 1.0);

// 方式 1: 位置访问 (x, y, z, w)
float r = color.x;  // 1.0
float g = color.y;  // 0.5
float b = color.z;  // 0.2
float a = color.w;  // 1.0

// 方式 2: 颜色访问 (r, g, b, a) - 等价于 x, y, z, w
float red = color.r;    // 1.0
float green = color.g;  // 0.5
float blue = color.b;   // 0.2
float alpha = color.a;  // 1.0

// 方式 3: 纹理访问 (s, t, p, q) - 等价于 x, y, z, w
float s = color.s;  // 1.0
float t = color.t;  // 0.5

// ============ Swizzling (分量重排) ============
// 这是 GLSL 最强大的特性之一!

vec4 c = vec4(1.0, 2.0, 3.0, 4.0);

vec3 rgb = c.rgb;           // (1.0, 2.0, 3.0) - 提取前三个分量
vec3 bgr = c.bgr;           // (3.0, 2.0, 1.0) - 反转顺序
vec2 rg = c.rg;             // (1.0, 2.0) - 只取前两个
vec4 rrrr = c.rrrr;         // (1.0, 1.0, 1.0, 1.0) - 重复同一分量
vec3 xxx = c.xxx;           // (1.0, 1.0, 1.0) - 同上

// 可以用于赋值
vec4 v = vec4(0.0);
v.rgb = vec3(1.0, 0.5, 0.2);  // 只修改 rgb,保持 a 不变
v.xz = vec2(1.0, 2.0);        // 只修改 x 和 z

// ============ 数组访问 ============
float components[4];
components[0] = c.x;
components[1] = c.y;
// 或者
float first = c[0];  // 等价于 c.x

3.4.4 矩阵操作

glsl
// ============ 矩阵构造 ============

// 单位矩阵(对角线为 1)
mat4 identity = mat4(1.0);

// 从数值构造(列主序!)
mat2 m2 = mat2(
    1.0, 2.0,    // 第一列
    3.0, 4.0     // 第二列
);

// 在内存中的布局:
// m2[0][0] = 1.0, m2[0][1] = 2.0  (第一列)
// m2[1][0] = 3.0, m2[1][1] = 4.0  (第二列)

// 从向量构造
vec4 col0 = vec4(1.0, 0.0, 0.0, 0.0);
vec4 col1 = vec4(0.0, 1.0, 0.0, 0.0);
vec4 col2 = vec4(0.0, 0.0, 1.0, 0.0);
vec4 col3 = vec4(0.0, 0.0, 0.0, 1.0);
mat4 m4 = mat4(col0, col1, col2, col3);

// ============ 矩阵访问 ============

mat4 m = mat4(1.0);

vec4 column0 = m[0];       // 获取第一列
float element = m[1][2];   // 获取第二列第三行的元素
m[0] = vec4(2.0);          // 设置第一列

// ============ 矩阵运算 ============

mat4 a = mat4(1.0);
mat4 b = mat4(2.0);

mat4 product = a * b;      // 矩阵乘法
vec4 v = vec4(1.0, 2.0, 3.0, 1.0);
vec4 transformed = a * v;  // 矩阵-向量乘法

// 注意:GLSL 中矩阵乘法是 后乘(post-multiply)
// transformed = a * v 表示 v 被 a 变换
// 而不是 v * a

3.4.5 运算符

glsl
// ============ 算术运算符 ============

float a = 10.0, b = 3.0;
float sum = a + b;        // 13.0
float diff = a - b;       // 7.0
float prod = a * b;       // 30.0
float quot = a / b;       // 3.333...

// 向量运算(逐分量)
vec3 v1 = vec3(1.0, 2.0, 3.0);
vec3 v2 = vec3(2.0, 3.0, 4.0);
vec3 vsum = v1 + v2;      // (3.0, 5.0, 7.0)
vec3 vprod = v1 * v2;     // (2.0, 6.0, 12.0) - 注意是逐分量乘法!
vec3 scaled = v1 * 2.0;   // (2.0, 4.0, 6.0) - 标量乘法

// ============ 比较运算符 ============

bool eq = a == b;         // false
bool ne = a != b;         // true
bool lt = a < b;          // false
bool gt = a > b;          // true
bool le = a <= b;         // false
bool ge = a >= b;         // true

// 向量比较返回布尔向量
bvec3 comparison = lessThan(v1, v2);  // (true, true, true)
bool anyTrue = any(comparison);        // true
bool allTrue = all(comparison);        // true

// ============ 逻辑运算符 ============

bool x = true, y = false;
bool andResult = x && y;   // false
bool orResult = x || y;    // true
bool notResult = !x;       // false

// ============ 位运算符 (WebGL 2.0 / GLSL ES 3.0) ============

int i = 5, j = 3;
int bitAnd = i & j;        // 1
int bitOr = i | j;         // 7
int bitXor = i ^ j;        // 6
int bitNot = ~i;           // -6
int leftShift = i << 1;    // 10
int rightShift = i >> 1;   // 2

3.4.6 流程控制

glsl
// ============ 条件语句 ============

if (condition) {
    // ...
} else if (otherCondition) {
    // ...
} else {
    // ...
}

// 三元运算符
float result = condition ? valueA : valueB;

// ============ 循环 ============

// for 循环
for (int i = 0; i < 10; i++) {
    // 循环体
}

// while 循环
while (condition) {
    // 循环体
}

// do-while 循环
do {
    // 循环体
} while (condition);

// break 和 continue
for (int i = 0; i < 100; i++) {
    if (i == 50) break;      // 退出循环
    if (i % 2 == 0) continue; // 跳过本次迭代
    // ...
}

// ============ GLSL ES 1.0 的循环限制 ============
// WebGL 1.0 中,循环有一些限制:

// ❌ 错误:循环次数不能由 uniform 决定
uniform int u_count;
for (int i = 0; i < u_count; i++) {  // 编译可能失败!
    // ...
}

// ✅ 正确:使用固定上限 + 条件退出
const int MAX_ITERATIONS = 100;
for (int i = 0; i < MAX_ITERATIONS; i++) {
    if (i >= u_count) break;
    // ...
}

3.4.7 函数定义

glsl
// ============ 函数声明 ============

// 基本函数
float add(float a, float b) {
    return a + b;
}

// 返回向量
vec3 blend(vec3 color1, vec3 color2, float factor) {
    return mix(color1, color2, factor);
}

// 无返回值
void outputColor(vec4 color) {
    gl_FragColor = color;
}

// ============ 参数限定符 ============

// in: 输入参数(默认,只读)
// out: 输出参数(只写)
// inout: 输入输出参数(可读可写)

void swap(inout float a, inout float b) {
    float temp = a;
    a = b;
    b = temp;
}

void decompose(in vec4 color, out vec3 rgb, out float alpha) {
    rgb = color.rgb;
    alpha = color.a;
}

// 使用示例
vec4 myColor = vec4(1.0, 0.5, 0.2, 0.8);
vec3 rgb;
float alpha;
decompose(myColor, rgb, alpha);
// rgb = (1.0, 0.5, 0.2), alpha = 0.8

// ============ 函数重载 ============

float brightness(vec3 color) {
    return dot(color, vec3(0.299, 0.587, 0.114));
}

float brightness(vec4 color) {
    return brightness(color.rgb);
}

// ============ 递归限制 ============
// GLSL 不支持递归!
// 以下代码会编译失败:
// float factorial(int n) {
//     if (n <= 1) return 1.0;
//     return float(n) * factorial(n - 1);  // ❌ 错误!
// }

3.5 变量限定符详解

3.5.1 attribute(顶点属性)

attribute 是顶点着色器的输入,每个顶点有不同的值:

glsl
// 顶点着色器
attribute vec3 a_position;    // 位置
attribute vec3 a_normal;      // 法线
attribute vec2 a_texCoord;    // 纹理坐标
attribute vec4 a_color;       // 颜色
attribute vec4 a_tangent;     // 切线(用于法线贴图)

void main() {
    // 使用这些属性...
    gl_Position = vec4(a_position, 1.0);
}
javascript
// JavaScript 端设置 attribute

// 1. 获取属性位置
const positionLoc = gl.getAttribLocation(program, 'a_position');
const normalLoc = gl.getAttribLocation(program, 'a_normal');

// 2. 启用属性数组
gl.enableVertexAttribArray(positionLoc);
gl.enableVertexAttribArray(normalLoc);

// 3. 绑定缓冲区并设置指针
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.vertexAttribPointer(
    positionLoc,    // 属性位置
    3,              // 分量数量
    gl.FLOAT,       // 数据类型
    false,          // 是否归一化
    stride,         // 步长(字节)
    offset          // 偏移(字节)
);

// 4. 绘制完成后可以禁用(可选)
gl.disableVertexAttribArray(positionLoc);

attribute 数据流

JavaScript 数据                     顶点着色器

Float32Array [                     顶点 0:
  x0, y0, z0,  ←──────────────→   a_position = (x0, y0, z0)
  nx0, ny0, nz0, ←────────────→   a_normal = (nx0, ny0, nz0)
  u0, v0,      ←──────────────→   a_texCoord = (u0, v0)
  
  x1, y1, z1,  ←──────────────→   顶点 1:
  nx1, ny1, nz1, ←────────────→   a_position = (x1, y1, z1)
  u1, v1,      ←──────────────→   ...
  ...
]

3.5.2 uniform(统一变量)

uniform 是全局常量,所有顶点/片段共享同一个值:

glsl
// 顶点着色器和片段着色器都可以使用 uniform

// 变换矩阵
uniform mat4 u_modelMatrix;
uniform mat4 u_viewMatrix;
uniform mat4 u_projectionMatrix;
uniform mat4 u_normalMatrix;

// 时间和动画
uniform float u_time;
uniform float u_deltaTime;

// 光照参数
uniform vec3 u_lightPosition;
uniform vec3 u_lightColor;
uniform float u_lightIntensity;

// 材质参数
uniform vec3 u_diffuseColor;
uniform vec3 u_specularColor;
uniform float u_shininess;

// 纹理采样器
uniform sampler2D u_diffuseMap;
uniform sampler2D u_normalMap;
uniform samplerCube u_environmentMap;

// 其他参数
uniform vec2 u_resolution;    // 画布尺寸
uniform vec2 u_mouse;         // 鼠标位置
javascript
// JavaScript 端设置 uniform

// 获取位置(只需获取一次,可以缓存)
const timeLoc = gl.getUniformLocation(program, 'u_time');
const modelMatrixLoc = gl.getUniformLocation(program, 'u_modelMatrix');
const lightPosLoc = gl.getUniformLocation(program, 'u_lightPosition');
const textureLoc = gl.getUniformLocation(program, 'u_diffuseMap');

// 设置标量
gl.uniform1f(timeLoc, performance.now() / 1000);
gl.uniform1i(textureLoc, 0);  // 纹理单元索引

// 设置向量
gl.uniform2f(resolutionLoc, canvas.width, canvas.height);
gl.uniform3f(lightPosLoc, 10.0, 10.0, 10.0);
gl.uniform3fv(lightPosLoc, [10.0, 10.0, 10.0]);  // 使用数组
gl.uniform4fv(colorLoc, new Float32Array([1, 0, 0, 1]));

// 设置矩阵
gl.uniformMatrix4fv(modelMatrixLoc, false, modelMatrix);
// 第二个参数 false 表示矩阵不需要转置
// (GLSL 和 JavaScript 使用相同的列主序)

所有 uniform 设置函数

javascript
// 标量
gl.uniform1f(location, v0);                      // float
gl.uniform1i(location, v0);                      // int, bool, sampler

// 向量
gl.uniform2f(location, v0, v1);                  // vec2
gl.uniform3f(location, v0, v1, v2);              // vec3
gl.uniform4f(location, v0, v1, v2, v3);          // vec4
gl.uniform2fv(location, array);                  // vec2 from array
gl.uniform3fv(location, array);                  // vec3 from array
gl.uniform4fv(location, array);                  // vec4 from array

// 整数向量
gl.uniform2i(location, v0, v1);                  // ivec2
gl.uniform3i(location, v0, v1, v2);              // ivec3
gl.uniform4i(location, v0, v1, v2, v3);          // ivec4

// 矩阵
gl.uniformMatrix2fv(location, transpose, array); // mat2
gl.uniformMatrix3fv(location, transpose, array); // mat3
gl.uniformMatrix4fv(location, transpose, array); // mat4

3.5.3 varying(插值变量)

varying 用于从顶点着色器向片段着色器传递数据,数据会在光栅化阶段被插值

glsl
// ============ 顶点着色器 ============
attribute vec3 a_position;
attribute vec3 a_normal;
attribute vec2 a_texCoord;
attribute vec4 a_color;

uniform mat4 u_mvpMatrix;
uniform mat4 u_modelMatrix;
uniform mat3 u_normalMatrix;

// 声明 varying 变量(输出)
varying vec3 v_worldPosition;
varying vec3 v_normal;
varying vec2 v_texCoord;
varying vec4 v_color;

void main() {
    // 计算世界坐标
    vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0);
    v_worldPosition = worldPos.xyz;
    
    // 变换法线
    v_normal = normalize(u_normalMatrix * a_normal);
    
    // 直接传递纹理坐标和颜色
    v_texCoord = a_texCoord;
    v_color = a_color;
    
    // 输出裁剪空间位置
    gl_Position = u_mvpMatrix * vec4(a_position, 1.0);
}


// ============ 片段着色器 ============
precision mediump float;

// 声明 varying 变量(输入,必须与顶点着色器完全匹配)
varying vec3 v_worldPosition;
varying vec3 v_normal;
varying vec2 v_texCoord;
varying vec4 v_color;

uniform sampler2D u_texture;
uniform vec3 u_lightPosition;

void main() {
    // 现在 v_normal, v_texCoord 等都是插值后的值
    vec3 normal = normalize(v_normal);  // 重新归一化!
    
    // 光照计算
    vec3 lightDir = normalize(u_lightPosition - v_worldPosition);
    float diff = max(dot(normal, lightDir), 0.0);
    
    // 纹理采样
    vec4 texColor = texture2D(u_texture, v_texCoord);
    
    // 最终颜色
    vec3 finalColor = texColor.rgb * v_color.rgb * diff;
    gl_FragColor = vec4(finalColor, texColor.a * v_color.a);
}

varying 的插值过程

varying 插值示意

三角形三个顶点的颜色:
V0: v_color = 红 (1, 0, 0)
V1: v_color = 绿 (0, 1, 0)
V2: v_color = 蓝 (0, 0, 1)

              V1 (绿)
              ╱╲
             ╱  ╲
            ╱    ╲
           ╱  P   ╲   ← 点 P 的 v_color 是什么?
          ╱        ╲
         ╱          ╲
        V0───────────V2
       (红)         (蓝)

光栅化时,GPU 使用重心坐标插值:

假设 P 的重心坐标是 (0.2, 0.5, 0.3)

P 的 v_color = 0.2 × V0.color + 0.5 × V1.color + 0.3 × V2.color
             = 0.2 × (1,0,0) + 0.5 × (0,1,0) + 0.3 × (0,0,1)
             = (0.2, 0.5, 0.3)

所有 varying 变量都会这样插值!

3.5.4 WebGL 2.0 中的变化

WebGL 2.0(GLSL ES 3.0)使用新的语法:

glsl
// ============ WebGL 2.0 顶点着色器 ============
#version 300 es

// attribute 改为 in
in vec3 a_position;
in vec3 a_normal;
in vec2 a_texCoord;

uniform mat4 u_mvpMatrix;

// varying 改为 out
out vec3 v_normal;
out vec2 v_texCoord;

void main() {
    gl_Position = u_mvpMatrix * vec4(a_position, 1.0);
    v_normal = a_normal;
    v_texCoord = a_texCoord;
}


// ============ WebGL 2.0 片段着色器 ============
#version 300 es
precision highp float;

// varying 改为 in
in vec3 v_normal;
in vec2 v_texCoord;

uniform sampler2D u_texture;

// gl_FragColor 改为自定义输出
out vec4 fragColor;

void main() {
    vec4 texColor = texture(u_texture, v_texCoord);  // texture2D 改为 texture
    fragColor = texColor;
}


// ============ 插值限定符(WebGL 2.0 新增)============

// flat: 不插值,使用第一个顶点的值
flat out vec3 v_flatColor;

// smooth: 默认,透视校正插值
smooth out vec3 v_smoothPosition;

// noperspective: 线性插值,不做透视校正
noperspective out vec2 v_screenCoord;

3.6 内置变量

3.6.1 顶点着色器内置变量

glsl
// ============ 输出变量(必须/可选设置)============

// gl_Position: 裁剪空间位置(必须设置!)
// 类型: vec4
// 这是顶点着色器最重要的输出
gl_Position = u_mvpMatrix * vec4(a_position, 1.0);

// gl_PointSize: 点的大小(像素)
// 类型: float
// 只在绘制 gl.POINTS 时有效
gl_PointSize = 10.0;  // 10 像素的点

// 动态点大小
gl_PointSize = u_baseSize / gl_Position.w;  // 根据距离调整大小


// ============ 输入变量(WebGL 2.0)============

// gl_VertexID: 当前顶点的索引
// 类型: int
// 对于 drawArrays: 从 first 开始的索引
// 对于 drawElements: 索引缓冲中的值
int index = gl_VertexID;

// gl_InstanceID: 实例化渲染时的实例索引
// 类型: int
// 对于 drawArraysInstanced 和 drawElementsInstanced
int instance = gl_InstanceID;

3.6.2 片段着色器内置变量

glsl
// ============ 输出变量 ============

// gl_FragColor: 片段颜色(WebGL 1.0)
// 类型: vec4
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);  // 红色

// WebGL 2.0 使用自定义输出
out vec4 fragColor;
fragColor = vec4(1.0, 0.0, 0.0, 1.0);

// gl_FragData[n]: 多渲染目标输出(需要扩展)
// 类型: vec4
gl_FragData[0] = albedo;
gl_FragData[1] = normal;


// ============ 输入变量 ============

// gl_FragCoord: 片段在窗口中的坐标
// 类型: vec4
// gl_FragCoord.xy: 窗口坐标(像素),原点在左下角
// gl_FragCoord.z: 深度值 [0, 1]
// gl_FragCoord.w: 1/w (透视除法的倒数)

vec2 screenPos = gl_FragCoord.xy;
float depth = gl_FragCoord.z;

// 归一化到 [0, 1]
vec2 uv = gl_FragCoord.xy / u_resolution;

// 应用示例:棋盘格
vec2 cell = floor(gl_FragCoord.xy / 50.0);
float checker = mod(cell.x + cell.y, 2.0);


// gl_FrontFacing: 是否是正面
// 类型: bool
if (gl_FrontFacing) {
    gl_FragColor = frontColor;
} else {
    gl_FragColor = backColor;
}

// 双面材质示例
vec3 normal = gl_FrontFacing ? v_normal : -v_normal;


// gl_PointCoord: 点精灵内的坐标
// 类型: vec2
// 范围: [0, 1],从左上角到右下角
// 只在渲染 gl.POINTS 时有效

// 圆形点
float dist = distance(gl_PointCoord, vec2(0.5));
if (dist > 0.5) discard;

// 纹理贴图的点
gl_FragColor = texture2D(u_sprite, gl_PointCoord);

3.6.3 实用示例

glsl
// 示例 1: 使用 gl_FragCoord 创建屏幕空间效果

precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;

void main() {
    // 归一化坐标
    vec2 uv = gl_FragCoord.xy / u_resolution;
    
    // 将坐标映射到 [-1, 1]
    vec2 st = uv * 2.0 - 1.0;
    st.x *= u_resolution.x / u_resolution.y;  // 修正宽高比
    
    // 距离中心的距离
    float d = length(st);
    
    // 动画圆环
    float ring = smoothstep(0.3, 0.31, d) - smoothstep(0.4, 0.41, d);
    ring *= 0.5 + 0.5 * sin(u_time * 3.0 + d * 10.0);
    
    gl_FragColor = vec4(vec3(ring), 1.0);
}


// 示例 2: 使用 gl_PointCoord 创建粒子效果

precision mediump float;
uniform sampler2D u_particleTexture;
varying vec4 v_color;

void main() {
    // 圆形裁切
    vec2 center = gl_PointCoord - vec2(0.5);
    float dist = length(center);
    if (dist > 0.5) discard;
    
    // 软边缘
    float alpha = 1.0 - smoothstep(0.3, 0.5, dist);
    
    // 或者使用纹理
    vec4 texColor = texture2D(u_particleTexture, gl_PointCoord);
    
    gl_FragColor = v_color * vec4(1.0, 1.0, 1.0, alpha);
}


// 示例 3: 双面渲染

precision mediump float;
varying vec3 v_normal;
varying vec2 v_texCoord;

uniform sampler2D u_frontTexture;
uniform sampler2D u_backTexture;

void main() {
    // 根据正反面选择纹理
    vec4 color;
    if (gl_FrontFacing) {
        color = texture2D(u_frontTexture, v_texCoord);
    } else {
        // 背面可能需要翻转纹理坐标
        vec2 flippedUV = vec2(1.0 - v_texCoord.x, v_texCoord.y);
        color = texture2D(u_backTexture, flippedUV);
    }
    
    gl_FragColor = color;
}

3.7 内置函数

GLSL 提供了丰富的内置函数,让我们按类别详细介绍:

3.7.1 数学函数

glsl
// ============ 角度和三角函数 ============

float rad = radians(180.0);    // 度 → 弧度: π
float deg = degrees(3.14159);  // 弧度 → 度: ≈180

float s = sin(angle);          // 正弦
float c = cos(angle);          // 余弦
float t = tan(angle);          // 正切
float as = asin(x);            // 反正弦 [-π/2, π/2]
float ac = acos(x);            // 反余弦 [0, π]
float at = atan(y, x);         // 反正切 [-π, π](推荐用这个)
float at2 = atan(x);           // 反正切 [-π/2, π/2]


// ============ 指数函数 ============

float p = pow(x, y);           // x^y
float e = exp(x);              // e^x
float l = log(x);              // ln(x)
float e2 = exp2(x);            // 2^x
float l2 = log2(x);            // log₂(x)
float sq = sqrt(x);            // √x
float isq = inversesqrt(x);    // 1/√x (比 1.0/sqrt(x) 更快)


// ============ 通用函数 ============

float a = abs(x);              // |x|
float s = sign(x);             // x>0 返回1, x<0 返回-1, x=0 返回0
float f = floor(x);            // 向下取整
float c = ceil(x);             // 向上取整
float t = trunc(x);            // 截断小数(WebGL 2.0)
float r = round(x);            // 四舍五入(WebGL 2.0)
float fr = fract(x);           // 小数部分: x - floor(x)
float m = mod(x, y);           // x mod y: x - y * floor(x/y)
float mn = min(x, y);          // 最小值
float mx = max(x, y);          // 最大值
float cl = clamp(x, min, max); // 限制范围


// ============ 插值函数 ============

// mix: 线性插值
float m = mix(a, b, t);        // a*(1-t) + b*t
vec3 color = mix(red, blue, 0.5);  // 混合颜色

// step: 阶跃函数
float s = step(edge, x);       // x < edge ? 0 : 1

// smoothstep: 平滑阶跃
float ss = smoothstep(e0, e1, x);
// 当 x < e0 时返回 0
// 当 x > e1 时返回 1
// 中间是平滑的 S 曲线


// ============ 实用示例 ============

// 创建锯齿波
float sawtooth = fract(u_time);

// 创建三角波
float triangle = abs(fract(u_time) * 2.0 - 1.0);

// 软化边缘
float softEdge = smoothstep(0.4, 0.5, distance(uv, center));

// 量化颜色(减少色阶)
vec3 quantized = floor(color * levels + 0.5) / levels;

3.7.2 几何函数

glsl
// ============ 向量运算 ============

float len = length(v);           // 向量长度: √(x²+y²+z²)
float dist = distance(a, b);     // 两点距离: length(a-b)
float d = dot(a, b);             // 点积: a.x*b.x + a.y*b.y + a.z*b.z
vec3 c = cross(a, b);            // 叉积(只对 vec3 有效)
vec3 n = normalize(v);           // 归一化: v / length(v)


// ============ 法线和反射 ============

// faceforward: 确保法线朝向观察者
vec3 ff = faceforward(N, I, Nref);
// 如果 dot(Nref, I) < 0 返回 N,否则返回 -N

// reflect: 计算反射向量
vec3 R = reflect(I, N);
// I: 入射向量(指向表面)
// N: 法线(必须归一化)
// R: 反射向量

// refract: 计算折射向量
vec3 T = refract(I, N, eta);
// I: 入射向量(必须归一化)
// N: 法线(必须归一化)
// eta: 折射率比值(n1/n2)
// 全反射时返回 vec3(0.0)


// ============ 实用示例 ============

// 计算漫反射光照
vec3 lightDir = normalize(lightPos - fragPos);
float diff = max(dot(normal, lightDir), 0.0);

// 计算镜面反射
vec3 viewDir = normalize(cameraPos - fragPos);
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), shininess);

// Fresnel 效果(边缘发光)
float fresnel = pow(1.0 - max(dot(normal, viewDir), 0.0), 5.0);

// 计算切线空间法线
vec3 T = normalize(v_tangent);
vec3 B = normalize(v_bitangent);
vec3 N = normalize(v_normal);
mat3 TBN = mat3(T, B, N);
vec3 worldNormal = TBN * textureNormal;

3.7.3 纹理函数

glsl
// ============ WebGL 1.0 纹理采样 ============

// 2D 纹理采样
vec4 color = texture2D(sampler, texCoord);

// 立方体纹理采样
vec4 color = textureCube(sampler, direction);

// 带 LOD 的采样(需要扩展或在顶点着色器中)
vec4 color = texture2DLod(sampler, texCoord, lod);

// 投影纹理采样
vec4 color = texture2DProj(sampler, texCoord);
// texCoord 是 vec3 或 vec4,会自动除以 .z 或 .w


// ============ WebGL 2.0 纹理采样 ============

// 统一的采样函数
vec4 color = texture(sampler, texCoord);

// 带 LOD 的采样
vec4 color = textureLod(sampler, texCoord, lod);

// 带偏移的采样(偏移是整数像素)
vec4 color = textureOffset(sampler, texCoord, ivec2(1, 0));

// 获取纹理尺寸
ivec2 size = textureSize(sampler, lod);

// 获取纹理像素(整数坐标,不过滤)
vec4 color = texelFetch(sampler, ivec2(x, y), lod);


// ============ 实用示例 ============

// 简单纹理采样
vec4 albedo = texture2D(u_albedoMap, v_texCoord);

// 法线贴图采样并解码
vec3 normalMap = texture2D(u_normalMap, v_texCoord).rgb;
vec3 normal = normalMap * 2.0 - 1.0;  // [0,1] → [-1,1]

// 多纹理混合
vec4 grass = texture2D(u_grassTexture, v_texCoord);
vec4 rock = texture2D(u_rockTexture, v_texCoord);
float blend = texture2D(u_blendMap, v_texCoord).r;
vec4 terrain = mix(grass, rock, blend);

// 环境反射
vec3 reflectDir = reflect(-viewDir, normal);
vec4 envColor = textureCube(u_environmentMap, reflectDir);

// 屏幕空间纹理
vec2 screenUV = gl_FragCoord.xy / u_resolution;
vec4 screenColor = texture2D(u_screenTexture, screenUV);

3.7.4 常用工具函数集

以下是一些实用的自定义函数:

glsl
// ============ 颜色处理 ============

// RGB 转灰度
float grayscale(vec3 color) {
    return dot(color, vec3(0.299, 0.587, 0.114));
}

// 调整饱和度
vec3 adjustSaturation(vec3 color, float saturation) {
    float gray = grayscale(color);
    return mix(vec3(gray), color, saturation);
}

// 调整对比度
vec3 adjustContrast(vec3 color, float contrast) {
    return (color - 0.5) * contrast + 0.5;
}

// 伽马校正
vec3 gammaCorrect(vec3 color, float gamma) {
    return pow(color, vec3(1.0 / gamma));
}

// 色调映射(简单版)
vec3 toneMap(vec3 hdr) {
    return hdr / (hdr + vec3(1.0));
}


// ============ 数学工具 ============

// 将值重映射到新范围
float remap(float value, float low1, float high1, float low2, float high2) {
    return low2 + (value - low1) * (high2 - low2) / (high1 - low1);
}

// 平滑最小值(用于 SDF 混合)
float smoothMin(float a, float b, float k) {
    float h = max(k - abs(a - b), 0.0) / k;
    return min(a, b) - h * h * k * 0.25;
}

// 旋转 2D 向量
vec2 rotate2D(vec2 v, float angle) {
    float s = sin(angle);
    float c = cos(angle);
    return vec2(v.x * c - v.y * s, v.x * s + v.y * c);
}


// ============ 噪声函数 ============

// 简单的伪随机
float random(vec2 st) {
    return fract(sin(dot(st, vec2(12.9898, 78.233))) * 43758.5453123);
}

// 2D 值噪声
float noise(vec2 st) {
    vec2 i = floor(st);
    vec2 f = fract(st);
    
    // 四角随机值
    float a = random(i);
    float b = random(i + vec2(1.0, 0.0));
    float c = random(i + vec2(0.0, 1.0));
    float d = random(i + vec2(1.0, 1.0));
    
    // 平滑插值
    vec2 u = f * f * (3.0 - 2.0 * f);
    
    return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}

// FBM (分形布朗运动)
float fbm(vec2 st) {
    float value = 0.0;
    float amplitude = 0.5;
    
    for (int i = 0; i < 5; i++) {
        value += amplitude * noise(st);
        st *= 2.0;
        amplitude *= 0.5;
    }
    
    return value;
}

3.8 精度限定符

3.8.1 精度级别

GLSL 中的浮点数有三种精度级别:

glsl
// 声明默认精度(片段着色器必须声明)
precision highp float;    // 高精度
precision mediump float;  // 中等精度
precision lowp float;     // 低精度

// 为特定变量指定精度
highp vec4 position;      // 高精度位置
mediump vec2 texCoord;    // 中等精度纹理坐标
lowp vec4 color;          // 低精度颜色

各精度的规格

精度范围精度(小数位)建议使用场景
highp±2^62~16 位位置、矩阵、物理计算
mediump±2^14~10 位纹理坐标、法线、时间
lowp±2~8 位颜色、简单计算

3.8.2 移动端精度问题

glsl
// 某些移动 GPU 不支持片段着色器中的 highp
// 检测方法:

#ifdef GL_FRAGMENT_PRECISION_HIGH
    precision highp float;
#else
    precision mediump float;
    // 可能需要调整算法以避免精度问题
#endif

精度相关的常见问题和解决方案

glsl
// 问题 1: 大坐标精度丢失
// 当世界坐标很大时,精度可能不足

// ❌ 可能有问题
varying highp vec3 v_worldPos;  // 如果坐标很大

// ✅ 使用相对坐标
varying mediump vec3 v_localOffset;  // 相对于某个参考点的偏移


// 问题 2: 法线归一化后的精度
// 插值后的法线可能不再归一化

// ❌ 直接使用
vec3 N = v_normal;

// ✅ 重新归一化
vec3 N = normalize(v_normal);


// 问题 3: 深度精度
// 在计算阴影等需要深度比较的地方

// 添加偏移避免 z-fighting
float bias = 0.005;
float shadow = depth < shadowDepth - bias ? 0.0 : 1.0;

3.9 着色器调试技巧

3.9.1 可视化调试

由于无法在着色器中使用 console.log,我们需要通过颜色输出来调试:

glsl
// 调试技巧:将数值可视化为颜色

// 调试标量值
float debugValue = someCalculation();
gl_FragColor = vec4(vec3(debugValue), 1.0);  // 灰度显示

// 调试向量(如法线)
vec3 debugNormal = v_normal;
gl_FragColor = vec4(debugNormal * 0.5 + 0.5, 1.0);  // [-1,1] → [0,1]

// 调试 UV 坐标
gl_FragColor = vec4(v_texCoord, 0.0, 1.0);  // R=U, G=V

// 调试深度
float depth = gl_FragCoord.z;
gl_FragColor = vec4(vec3(depth), 1.0);

// 调试 NaN 和 Inf
if (isnan(value) || isinf(value)) {
    gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);  // 粉色警告
    return;
}

// 热力图可视化
vec3 heatmap(float t) {
    t = clamp(t, 0.0, 1.0);
    return vec3(
        smoothstep(0.5, 0.8, t),                    // R
        smoothstep(0.0, 0.5, t) - smoothstep(0.5, 1.0, t),  // G
        1.0 - smoothstep(0.2, 0.5, t)              // B
    );
}
gl_FragColor = vec4(heatmap(someValue), 1.0);

3.9.2 常见错误和解决方案

glsl
// ============ 错误 1: 忘记精度声明 ============
// ❌ 片段着色器没有默认精度
float x = 1.0;  // 编译错误!

// ✅ 添加精度声明
precision mediump float;
float x = 1.0;


// ============ 错误 2: varying 名称不匹配 ============
// 顶点着色器
varying vec3 v_Normal;  // 大写 N

// 片段着色器
varying vec3 v_normal;  // 小写 n
// 链接错误!名称必须完全匹配


// ============ 错误 3: 类型不匹配 ============
// ❌ 整数和浮点数混用
int count = 10;
float result = count / 3;  // 可能警告

// ✅ 显式转换
float result = float(count) / 3.0;


// ============ 错误 4: 使用未初始化的变量 ============
// ❌
vec3 color;
color.r = 1.0;  // g 和 b 未初始化

// ✅
vec3 color = vec3(0.0);
color.r = 1.0;


// ============ 错误 5: 除以零 ============
// ❌ 可能除以零
float result = value / divisor;

// ✅ 安全除法
float result = value / max(divisor, 0.0001);


// ============ 错误 6: normalize 零向量 ============
// ❌ 零向量无法归一化
vec3 dir = vec3(0.0);
dir = normalize(dir);  // NaN!

// ✅ 先检查长度
float len = length(dir);
dir = len > 0.0 ? dir / len : vec3(0.0, 0.0, 1.0);

3.10 本章小结

核心概念

概念说明
着色器运行在 GPU 上的程序
GLSLWebGL 使用的着色器语言
顶点着色器处理每个顶点,输出位置
片段着色器处理每个像素,输出颜色
attribute顶点着色器输入(每顶点不同)
uniform全局常量(所有着色器共享)
varying顶点→片段的插值数据

关键数据类型

类型说明常用场景
float浮点数各种计算
vec2/3/4浮点向量位置、颜色、纹理坐标
mat2/3/4矩阵变换
sampler2D2D 纹理采样器纹理

常用内置函数

类别函数
数学sin, cos, pow, sqrt, abs, floor, fract
插值mix, step, smoothstep, clamp
几何length, distance, dot, cross, normalize, reflect
纹理texture2D, textureCube

3.11 练习题

基础练习

  1. 创建一个颜色渐变的着色器(从左到右颜色变化)

  2. 实现一个随时间变化的动画效果

  3. 使用 gl_PointCoord 创建圆形的点

进阶练习

  1. 实现简单的图像处理效果(灰度、反色、模糊)

  2. 创建一个棋盘格图案着色器

  3. 实现基本的 Phong 光照模型

挑战练习

  1. 创建一个程序化的噪声纹理

  2. 实现一个着色器编辑器,支持实时预览和错误提示


下一章预告:在第4章中,我们将学习缓冲区和顶点数据的管理,这是向 GPU 传递数据的核心技术。


文档版本:v1.0
字数统计:约 18,000 字
代码示例:60+ 个

如有转载或 CV 的请标注本站原文地址