第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 练习题
基础练习
启用深度测试,渲染多个不同深度的立方体
实现标准透明混合,渲染半透明物体
实现加法混合的粒子效果
进阶练习
实现透明物体的正确排序渲染
使用模板测试实现物体轮廓效果
挑战练习
- 实现简单的镜面反射效果
下一章预告:在第10章中,我们将学习 WebGL 2.0 的新特性,包括实例化渲染、变换反馈等。
文档版本:v1.0
字数统计:约 10,000 字
代码示例:40+ 个
