Skip to content

第9章:混合与深度测试

9.1 章节概述

要正确渲染 3D 场景中的透明物体和重叠元素,需要理解深度测试混合机制。深度测试决定哪些物体可见,混合决定透明物体如何组合颜色。

本章将深入讲解:

  • 深度测试:原理、配置、常见问题
  • 混合模式:各种混合公式及应用场景
  • 透明渲染:正确渲染透明物体的策略
  • 模板测试:高级遮罩技术

9.2 深度测试

9.2.1 深度测试原理

深度测试的作用

没有深度测试:
──────────────────────────────────
后绘制的物体总是覆盖先绘制的

绘制顺序:A → B → C
       相机 →  ┌─┐
              │C│ ← 后绘制,遮挡一切
              │ │
              │B│
              │ │
              │A│


使用深度测试:
──────────────────────────────────
根据实际距离决定可见性

深度缓冲记录每个像素的最近距离

       相机 →  ┌─┐
              │A│ ← 最近,显示
              │ │
              │B│ ← 被 A 遮挡的部分不显示
              │ │
              │C│ ← 被 A、B 遮挡的部分不显示


深度测试流程:
1. 渲染片段时计算深度值 z
2. 与深度缓冲中的值比较
3. 如果通过测试,更新颜色缓冲和深度缓冲
4. 否则丢弃该片段

9.2.2 启用和配置深度测试

javascript
// 启用深度测试
gl.enable(gl.DEPTH_TEST);

// 设置深度比较函数
gl.depthFunc(gl.LESS);  // 默认:z 更小(更近)时通过

// 可用的比较函数
gl.NEVER      // 永不通过
gl.LESS       // <  更近时通过(默认)
gl.EQUAL      // == 相等时通过
gl.LEQUAL     // <= 更近或相等时通过
gl.GREATER    // >  更远时通过
gl.NOTEQUAL   // != 不相等时通过
gl.GEQUAL     // >= 更远或相等时通过
gl.ALWAYS     // 总是通过


// 控制深度写入
gl.depthMask(true);   // 允许写入深度缓冲(默认)
gl.depthMask(false);  // 禁止写入深度缓冲

// 设置深度范围(NDC 到深度缓冲的映射)
gl.depthRange(0.0, 1.0);  // 默认

// 清除深度缓冲
gl.clearDepth(1.0);  // 清除值(默认为 1.0,最远)
gl.clear(gl.DEPTH_BUFFER_BIT);

9.2.3 深度值的计算

深度值的非线性特性

透视投影下,深度值是非线性的!

              near                    far
相机 ────────│────────────────────────│
             │                        │
             │                        │
            0.0                      1.0
         (近平面)              (远平面)


深度精度在近平面附近更高:

实际距离:  |──────|──────|──────|──────|
           near  25%    50%    75%   far

深度缓冲:  |─────────────────|───|──|─|
           0.0               0.5    1.0

大部分精度集中在近处!


Z-Fighting 问题

当两个表面非常接近时,深度值可能相同或交替
导致闪烁、条纹状伪影

解决方法:
1. 增大 near 平面距离(提高精度)
2. 使用 24 或 32 位深度缓冲
3. 使用 gl.polygonOffset 偏移
javascript
// 解决共面问题:多边形偏移
gl.enable(gl.POLYGON_OFFSET_FILL);
gl.polygonOffset(factor, units);

// offset = factor × DZ + units × r
// DZ: 深度斜率
// r: 深度缓冲的最小分辨率

// 常用值
gl.polygonOffset(1.0, 1.0);  // 将深度稍微推远

9.3 混合(Blending)

9.3.1 混合原理

混合的概念

将源颜色(新绘制)与目标颜色(已在缓冲中)组合

源颜色(Source)   目标颜色(Destination)
       S                    D
       │                    │
       └──────┬─────────────┘

         混合公式


         最终颜色


混合公式:
FinalColor = SrcFactor × Src + DstFactor × Dst

SrcFactor: 源因子
DstFactor: 目标因子
Src: 源颜色(当前片段)
Dst: 目标颜色(帧缓冲中的颜色)

9.3.2 启用和配置混合

javascript
// 启用混合
gl.enable(gl.BLEND);

// 设置混合函数
gl.blendFunc(srcFactor, dstFactor);

// 常用混合因子
gl.ZERO                    // 0
gl.ONE                     // 1
gl.SRC_COLOR               // 源颜色
gl.ONE_MINUS_SRC_COLOR     // 1 - 源颜色
gl.DST_COLOR               // 目标颜色
gl.ONE_MINUS_DST_COLOR     // 1 - 目标颜色
gl.SRC_ALPHA               // 源 alpha
gl.ONE_MINUS_SRC_ALPHA     // 1 - 源 alpha
gl.DST_ALPHA               // 目标 alpha
gl.ONE_MINUS_DST_ALPHA     // 1 - 目标 alpha
gl.CONSTANT_COLOR          // 常量颜色
gl.ONE_MINUS_CONSTANT_COLOR
gl.CONSTANT_ALPHA
gl.ONE_MINUS_CONSTANT_ALPHA
gl.SRC_ALPHA_SATURATE      // min(srcAlpha, 1-dstAlpha)

9.3.3 常见混合模式

javascript
// ========== 标准透明混合 ==========
// Final = Src × SrcAlpha + Dst × (1 - SrcAlpha)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

// 示例:50% 透明的红色 (1,0,0,0.5) 叠加在蓝色 (0,0,1,1) 上
// Final = (1,0,0) × 0.5 + (0,0,1) × 0.5 = (0.5, 0, 0.5)


// ========== 加法混合(发光效果)==========
// Final = Src × SrcAlpha + Dst × 1
gl.blendFunc(gl.SRC_ALPHA, gl.ONE);

// 颜色相加,适合火焰、激光、光晕


// ========== 乘法混合(变暗)==========
// Final = Src × Dst + Dst × 0
// 简化: Final = Src × Dst
gl.blendFunc(gl.DST_COLOR, gl.ZERO);


// ========== 预乘 Alpha ==========
// 当颜色已预乘 alpha 时使用
// Final = Src × 1 + Dst × (1 - SrcAlpha)
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);


// ========== 分离 RGB 和 Alpha ==========
gl.blendFuncSeparate(
    gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA,  // RGB
    gl.ONE, gl.ONE_MINUS_SRC_ALPHA          // Alpha
);

9.3.4 混合方程

javascript
// 默认混合方程是相加
// Final = SrcFactor × Src + DstFactor × Dst

gl.blendEquation(gl.FUNC_ADD);           // 相加(默认)
gl.blendEquation(gl.FUNC_SUBTRACT);      // Src - Dst
gl.blendEquation(gl.FUNC_REVERSE_SUBTRACT); // Dst - Src

// WebGL 2.0 / 扩展
gl.blendEquation(gl.MIN);  // min(Src, Dst)
gl.blendEquation(gl.MAX);  // max(Src, Dst)

// 分离 RGB 和 Alpha 的方程
gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);

9.4 透明物体渲染

9.4.1 渲染顺序问题

透明渲染的问题

深度测试 + 透明混合 会产生问题

错误的渲染顺序:

1. 先渲染远处的透明物体 A
   
        相机 →   ├───┤     A (透明)
                     深度写入

2. 再渲染近处的透明物体 B
   
        相机 →   ├─┤       B (透明,在 A 前面)
                     B 渲染,与 A 混合 ✓

3. 但如果顺序相反...

   先渲染 B,写入深度
   再渲染 A,深度测试失败,A 被丢弃!
   
   结果:看不到 A,即使它应该透过 B 可见


正确的做法:
1. 先渲染所有不透明物体(启用深度写入)
2. 禁用深度写入
3. 从远到近渲染透明物体(按距离排序)

9.4.2 透明物体渲染策略

javascript
/**
 * 透明物体渲染管理器
 */
class TransparentRenderer {
    constructor(gl) {
        this.gl = gl;
        this.opaqueObjects = [];
        this.transparentObjects = [];
    }
    
    add(object) {
        if (object.isTransparent) {
            this.transparentObjects.push(object);
        } else {
            this.opaqueObjects.push(object);
        }
    }
    
    render(camera) {
        const gl = this.gl;
        
        // ========== Pass 1: 渲染不透明物体 ==========
        gl.enable(gl.DEPTH_TEST);
        gl.depthMask(true);        // 允许深度写入
        gl.disable(gl.BLEND);
        
        this.opaqueObjects.forEach(obj => obj.render());
        
        // ========== Pass 2: 渲染透明物体 ==========
        gl.depthMask(false);       // 禁止深度写入
        gl.enable(gl.BLEND);
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
        
        // 按距离排序(从远到近)
        const cameraPos = camera.position;
        this.transparentObjects.sort((a, b) => {
            const distA = this.distance(a.position, cameraPos);
            const distB = this.distance(b.position, cameraPos);
            return distB - distA;  // 远的先渲染
        });
        
        this.transparentObjects.forEach(obj => obj.render());
        
        // 恢复状态
        gl.depthMask(true);
        gl.disable(gl.BLEND);
    }
    
    distance(a, b) {
        const dx = a[0] - b[0];
        const dy = a[1] - b[1];
        const dz = a[2] - b[2];
        return dx * dx + dy * dy + dz * dz;
    }
}

9.4.3 深度剥离(Depth Peeling)

处理复杂透明物体重叠的高级技术:

深度剥离原理

多次渲染,每次"剥离"一层最近的透明表面

Pass 1: 渲染最近的透明层
         │ 保存深度

Pass 2: 渲染第二近的透明层
         │ 只渲染深度大于 Pass 1 的片段

Pass 3: 渲染第三近...
         │ 只渲染深度大于 Pass 2 的片段

...

最后: 从远到近混合所有层


     相机 →  [A] [B] [C]
              │   │   │
              ▼   ▼   ▼
           Layer 1  (A)
           Layer 2  (B)
           Layer 3  (C)


           混合结果

9.5 模板测试(Stencil Test)

9.5.1 模板测试原理

模板测试

模板缓冲是一个额外的整数缓冲(通常 8 位)
可以用来创建遮罩效果

        ┌─────────────────────────┐
        │   模板缓冲              │
        │   ┌───┬───┬───┐        │
        │   │ 0 │ 1 │ 0 │        │
        │   ├───┼───┼───┤        │
        │   │ 1 │ 1 │ 1 │        │
        │   ├───┼───┼───┤        │
        │   │ 0 │ 1 │ 0 │        │
        │   └───┴───┴───┘        │
        │                         │
        │   0 = 不通过            │
        │   1 = 通过              │
        └─────────────────────────┘


模板测试流程:
1. 将模板值与参考值比较
2. 根据比较结果决定是否渲染
3. 可选更新模板值

9.5.2 配置模板测试

javascript
// 启用模板测试
gl.enable(gl.STENCIL_TEST);

// 设置模板函数
gl.stencilFunc(func, ref, mask);

// func: 比较函数
// ref: 参考值
// mask: 与模板值和参考值进行 AND 运算的掩码

// 比较: (stencilValue & mask) func (ref & mask)

// func 可用值
gl.NEVER
gl.LESS
gl.LEQUAL
gl.GREATER
gl.GEQUAL
gl.EQUAL
gl.NOTEQUAL
gl.ALWAYS


// 设置模板操作
gl.stencilOp(sfail, dpfail, dppass);

// sfail:  模板测试失败时的操作
// dpfail: 模板通过但深度测试失败时的操作
// dppass: 模板和深度都通过时的操作

// 操作值
gl.KEEP       // 保持当前值
gl.ZERO       // 设为 0
gl.REPLACE    // 设为参考值
gl.INCR       // 增加(饱和)
gl.INCR_WRAP  // 增加(循环)
gl.DECR       // 减少(饱和)
gl.DECR_WRAP  // 减少(循环)
gl.INVERT     // 按位取反


// 设置模板掩码(控制哪些位可写)
gl.stencilMask(0xFF);  // 所有位可写
gl.stencilMask(0x00);  // 禁止写入


// 清除模板缓冲
gl.clearStencil(0);
gl.clear(gl.STENCIL_BUFFER_BIT);

9.5.3 模板测试应用:轮廓效果

javascript
/**
 * 使用模板测试绘制物体轮廓
 */
function drawOutline(gl, object, outlineWidth = 1.05) {
    // Pass 1: 正常绘制物体,写入模板
    gl.enable(gl.STENCIL_TEST);
    gl.stencilFunc(gl.ALWAYS, 1, 0xFF);  // 总是通过,写入 1
    gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);  // 深度通过时写入
    gl.stencilMask(0xFF);  // 允许写入
    
    gl.useProgram(normalProgram);
    object.draw();
    
    // Pass 2: 绘制放大的物体作为轮廓
    gl.stencilFunc(gl.NOTEQUAL, 1, 0xFF);  // 模板值不等于 1 时通过
    gl.stencilMask(0x00);  // 禁止写入
    gl.disable(gl.DEPTH_TEST);  // 轮廓始终可见
    
    gl.useProgram(outlineProgram);
    
    // 放大物体
    const scaleMatrix = mat4.create();
    mat4.scale(scaleMatrix, object.modelMatrix, [outlineWidth, outlineWidth, outlineWidth]);
    gl.uniformMatrix4fv(modelLocation, false, scaleMatrix);
    
    object.draw();
    
    // 恢复状态
    gl.stencilMask(0xFF);
    gl.disable(gl.STENCIL_TEST);
    gl.enable(gl.DEPTH_TEST);
}

9.5.4 模板测试应用:镜面反射

javascript
/**
 * 使用模板测试实现镜面反射
 */
function renderMirrorScene(gl, scene, mirror) {
    // Pass 1: 渲染镜面到模板
    gl.enable(gl.STENCIL_TEST);
    gl.stencilFunc(gl.ALWAYS, 1, 0xFF);
    gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
    
    gl.colorMask(false, false, false, false);  // 不写入颜色
    gl.depthMask(false);  // 不写入深度
    
    drawMirrorSurface();  // 绘制镜面形状
    
    // Pass 2: 渲染反射的场景
    gl.colorMask(true, true, true, true);
    gl.depthMask(true);
    
    gl.stencilFunc(gl.EQUAL, 1, 0xFF);  // 只在镜面区域渲染
    gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
    
    // 镜像变换矩阵
    const reflectionMatrix = createReflectionMatrix(mirror.normal, mirror.point);
    
    gl.frontFace(gl.CW);  // 反转面剔除
    drawScene(reflectionMatrix);
    gl.frontFace(gl.CCW);
    
    // Pass 3: 渲染镜面本身(半透明)
    gl.disable(gl.STENCIL_TEST);
    gl.enable(gl.BLEND);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
    
    drawMirrorSurface(0.3);  // 30% 透明度
    
    // Pass 4: 渲染正常场景
    gl.disable(gl.BLEND);
    drawScene();
}

9.6 面剔除(Face Culling)

9.6.1 什么是面剔除

面剔除

3D 模型的背面通常不可见,可以不渲染

         正面(逆时针)

             ╱│╲
            ╱ │ ╲
           ╱  │  ╲
          ╱   ●   ╲  ← 相机
         ╱    │    ╲
        ───────────── 
              背面


逆时针(CCW)= 正面(默认)
顺时针(CW)= 背面


剔除背面的好处:
- 减少约 50% 的渲染片段
- 提高性能
- 避免内部面的渲染问题

9.6.2 配置面剔除

javascript
// 启用面剔除
gl.enable(gl.CULL_FACE);

// 设置剔除哪个面
gl.cullFace(gl.BACK);   // 剔除背面(默认)
gl.cullFace(gl.FRONT);  // 剔除正面
gl.cullFace(gl.FRONT_AND_BACK);  // 剔除所有面

// 设置正面的顶点顺序
gl.frontFace(gl.CCW);  // 逆时针为正面(默认)
gl.frontFace(gl.CW);   // 顺时针为正面

// 常见用例
// 渲染实体物体(剔除背面)
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.BACK);
gl.frontFace(gl.CCW);

// 渲染天空盒(从内部看,剔除正面)
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.FRONT);

// 渲染双面材质(如树叶)
gl.disable(gl.CULL_FACE);

9.7 渲染状态管理

javascript
/**
 * WebGL 渲染状态管理器
 */
class RenderState {
    constructor(gl) {
        this.gl = gl;
        this.states = new Map();
        this.saveInitialState();
    }
    
    saveInitialState() {
        const gl = this.gl;
        
        this.defaultState = {
            depthTest: gl.isEnabled(gl.DEPTH_TEST),
            depthWrite: gl.getParameter(gl.DEPTH_WRITEMASK),
            depthFunc: gl.getParameter(gl.DEPTH_FUNC),
            
            blend: gl.isEnabled(gl.BLEND),
            blendSrcRGB: gl.getParameter(gl.BLEND_SRC_RGB),
            blendDstRGB: gl.getParameter(gl.BLEND_DST_RGB),
            blendSrcAlpha: gl.getParameter(gl.BLEND_SRC_ALPHA),
            blendDstAlpha: gl.getParameter(gl.BLEND_DST_ALPHA),
            
            cullFace: gl.isEnabled(gl.CULL_FACE),
            cullFaceMode: gl.getParameter(gl.CULL_FACE_MODE),
            frontFace: gl.getParameter(gl.FRONT_FACE),
            
            stencilTest: gl.isEnabled(gl.STENCIL_TEST)
        };
    }
    
    // 预设状态
    setOpaque() {
        const gl = this.gl;
        gl.enable(gl.DEPTH_TEST);
        gl.depthMask(true);
        gl.depthFunc(gl.LESS);
        gl.disable(gl.BLEND);
        gl.enable(gl.CULL_FACE);
        gl.cullFace(gl.BACK);
    }
    
    setTransparent() {
        const gl = this.gl;
        gl.enable(gl.DEPTH_TEST);
        gl.depthMask(false);  // 不写入深度
        gl.depthFunc(gl.LESS);
        gl.enable(gl.BLEND);
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
        gl.enable(gl.CULL_FACE);
        gl.cullFace(gl.BACK);
    }
    
    setAdditive() {
        const gl = this.gl;
        gl.enable(gl.DEPTH_TEST);
        gl.depthMask(false);
        gl.depthFunc(gl.LESS);
        gl.enable(gl.BLEND);
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
        gl.disable(gl.CULL_FACE);
    }
    
    setUI() {
        const gl = this.gl;
        gl.disable(gl.DEPTH_TEST);
        gl.depthMask(false);
        gl.enable(gl.BLEND);
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
        gl.disable(gl.CULL_FACE);
    }
    
    reset() {
        const gl = this.gl;
        const s = this.defaultState;
        
        s.depthTest ? gl.enable(gl.DEPTH_TEST) : gl.disable(gl.DEPTH_TEST);
        gl.depthMask(s.depthWrite);
        gl.depthFunc(s.depthFunc);
        
        s.blend ? gl.enable(gl.BLEND) : gl.disable(gl.BLEND);
        gl.blendFuncSeparate(s.blendSrcRGB, s.blendDstRGB, s.blendSrcAlpha, s.blendDstAlpha);
        
        s.cullFace ? gl.enable(gl.CULL_FACE) : gl.disable(gl.CULL_FACE);
        gl.cullFace(s.cullFaceMode);
        gl.frontFace(s.frontFace);
        
        s.stencilTest ? gl.enable(gl.STENCIL_TEST) : gl.disable(gl.STENCIL_TEST);
    }
}

// 使用
const renderState = new RenderState(gl);

// 渲染不透明物体
renderState.setOpaque();
drawOpaqueObjects();

// 渲染透明物体
renderState.setTransparent();
drawTransparentObjects();

// 渲染粒子(加法混合)
renderState.setAdditive();
drawParticles();

// 渲染 UI
renderState.setUI();
drawUI();

renderState.reset();

9.8 本章小结

核心概念

概念说明
深度测试根据深度值决定片段可见性
深度缓冲存储每像素的深度值
混合组合源颜色和目标颜色
模板测试使用整数缓冲进行遮罩
面剔除不渲染背面,提高性能

关键公式

混合公式:
Final = SrcFactor × Src + DstFactor × Dst

常用透明混合:
Final = SrcAlpha × Src + (1-SrcAlpha) × Dst

渲染顺序

1. 设置状态(启用深度/混合等)
2. 渲染不透明物体(启用深度写入)
3. 渲染透明物体(禁用深度写入,从远到近)
4. 渲染 UI(禁用深度测试)

9.9 练习题

基础练习

  1. 启用深度测试,渲染多个不同深度的立方体

  2. 实现标准透明混合,渲染半透明物体

  3. 实现加法混合的粒子效果

进阶练习

  1. 实现透明物体的正确排序渲染

  2. 使用模板测试实现物体轮廓效果

挑战练习

  1. 实现简单的镜面反射效果

下一章预告:在第10章中,我们将学习 WebGL 2.0 的新特性,包括实例化渲染、变换反馈等。


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

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