第8章:可见性与遮挡
8.1 深度缓冲
8.1.1 可见性问题
在渲染 3D 场景时,一个关键问题是确定哪些物体对观察者可见。当多个物体在屏幕上的投影重叠时,需要正确处理遮挡关系。
可见性问题
从相机视角看,物体 A 遮挡了物体 B:
相机
•
╱│╲
╱ │ ╲
╱ │ ╲
╱ │ ╲
╱ │ ╲
┌─────┼─────┐ ← A(近)
│ │ │
└─────┼─────┘
│
┌─────┼─────┐ ← B(远)
│ │ │
└─────┼─────┘
问题:如何确定哪个像素显示 A,哪个显示 B?8.1.2 Z-Buffer 算法
Z-Buffer(深度缓冲)
最广泛使用的可见性算法。
原理:
1. 为每个像素维护一个深度值
2. 渲染时,比较新片段与缓冲区中的深度
3. 只有更近的片段才会更新颜色缓冲
初始状态:
┌───┬───┬───┬───┐
│ ∞ │ ∞ │ ∞ │ ∞ │ ← 深度缓冲(初始化为最大值)
├───┼───┼───┼───┤
│ ∞ │ ∞ │ ∞ │ ∞ │
├───┼───┼───┼───┤
│ ∞ │ ∞ │ ∞ │ ∞ │
└───┴───┴───┴───┘
渲染物体 A(深度 5):
┌───┬───┬───┬───┐
│ 5 │ 5 │ ∞ │ ∞ │
├───┼───┼───┼───┤
│ 5 │ 5 │ ∞ │ ∞ │
├───┼───┼───┼───┤
│ ∞ │ ∞ │ ∞ │ ∞ │
└───┴───┴───┴───┘
渲染物体 B(深度 10):
┌───┬───┬───┬───┐
│ 5 │ 5 │ 10│ 10│ ← 左上角保持 5(比 10 近)
├───┼───┼───┼───┤
│ 5 │ 5 │ 10│ 10│
├───┼───┼───┼───┤
│ 10│ 10│ 10│ 10│ ← 下方没有 A,显示 B
└───┴───┴───┴───┘8.1.3 Z-Buffer 实现
javascript
/**
* 软件 Z-Buffer 实现
*/
class ZBuffer {
constructor(width, height) {
this.width = width;
this.height = height;
// 深度缓冲
this.depthBuffer = new Float32Array(width * height);
// 颜色缓冲
this.colorBuffer = new Uint8ClampedArray(width * height * 4);
this.clear();
}
/**
* 清除缓冲区
*/
clear(clearColor = [0, 0, 0, 255], clearDepth = Infinity) {
for (let i = 0; i < this.depthBuffer.length; i++) {
this.depthBuffer[i] = clearDepth;
}
for (let i = 0; i < this.colorBuffer.length; i += 4) {
this.colorBuffer[i] = clearColor[0];
this.colorBuffer[i + 1] = clearColor[1];
this.colorBuffer[i + 2] = clearColor[2];
this.colorBuffer[i + 3] = clearColor[3];
}
}
/**
* 测试并写入像素
* @returns true 如果通过深度测试
*/
testAndSet(x, y, depth, color) {
x = Math.floor(x);
y = Math.floor(y);
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
return false;
}
const index = y * this.width + x;
// 深度测试:小于等于则通过
if (depth <= this.depthBuffer[index]) {
// 更新深度缓冲
this.depthBuffer[index] = depth;
// 更新颜色缓冲
const colorIndex = index * 4;
this.colorBuffer[colorIndex] = color[0];
this.colorBuffer[colorIndex + 1] = color[1];
this.colorBuffer[colorIndex + 2] = color[2];
this.colorBuffer[colorIndex + 3] = color[3] ?? 255;
return true;
}
return false;
}
/**
* 渲染三角形
*/
renderTriangle(v0, v1, v2, color) {
// 计算包围盒
const minX = Math.floor(Math.min(v0.x, v1.x, v2.x));
const maxX = Math.ceil(Math.max(v0.x, v1.x, v2.x));
const minY = Math.floor(Math.min(v0.y, v1.y, v2.y));
const maxY = Math.ceil(Math.max(v0.y, v1.y, v2.y));
// 边函数系数(用于判断点是否在三角形内)
const edge = (ax, ay, bx, by, px, py) => {
return (px - ax) * (by - ay) - (py - ay) * (bx - ax);
};
const area = edge(v0.x, v0.y, v1.x, v1.y, v2.x, v2.y);
if (area === 0) return; // 退化三角形
// 遍历包围盒内的所有像素
for (let y = minY; y <= maxY; y++) {
for (let x = minX; x <= maxX; x++) {
// 计算重心坐标
const px = x + 0.5;
const py = y + 0.5;
const w0 = edge(v1.x, v1.y, v2.x, v2.y, px, py) / area;
const w1 = edge(v2.x, v2.y, v0.x, v0.y, px, py) / area;
const w2 = edge(v0.x, v0.y, v1.x, v1.y, px, py) / area;
// 判断是否在三角形内
if (w0 >= 0 && w1 >= 0 && w2 >= 0) {
// 插值深度
const depth = w0 * v0.z + w1 * v1.z + w2 * v2.z;
// 深度测试并写入
this.testAndSet(x, y, depth, color);
}
}
}
}
/**
* 获取 ImageData
*/
getImageData() {
return new ImageData(this.colorBuffer, this.width, this.height);
}
}8.1.4 深度精度问题
深度缓冲的精度问题
非线性深度分布:
透视投影后,深度值是非线性的:
z_ndc = (f + n) / (f - n) + (2fn) / ((f - n) * z_eye)
这导致近处深度精度高,远处精度低。
精度高 精度低
┌──────────┬────────────────────────────────┐
近平面 10% 90% 的范围 远平面
Z-Fighting(深度冲突):
当两个面距离非常近时,可能出现闪烁:
┌─────────────┐ ← 面 A(z = 10.0001)
├─────────────┤ ← 面 B(z = 10.0000)
└─────────────┘
由于精度限制,两个面可能交替显示。
解决方案:
1. 减小 far/near 比例
2. 使用反向深度(Reversed-Z)
3. 使用对数深度(Logarithmic Depth)
4. 避免共面几何8.2 画家算法
8.2.1 画家算法原理
画家算法(Painter's Algorithm)
原理:像画家作画一样,先画远处的物体,再画近处的物体。
步骤:
1. 按深度对所有多边形排序(从远到近)
2. 依次渲染每个多边形
示例:
远 近
┌─────┐ ┌───┐
│ │ │ │
│ C │ │ A │
│ │ ┌────┐ │ │
└─────┘ │ B │ └───┘
└────┘
渲染顺序:C → B → A
优点:
- 简单
- 支持透明物体
缺点:
- 需要排序 O(n log n)
- 无法处理循环遮挡8.2.2 循环遮挡问题
循环遮挡
三个物体互相遮挡,无法确定绘制顺序:
┌─────┐
│ A │
┌───┴─┐ │
│ B │───┘
│ ┌─┴───┐
└───┤ C │
└─────┘
A 遮挡 B
B 遮挡 C
C 遮挡 A
这种情况画家算法无法处理!
解决方案:
1. 切割多边形
2. 使用 BSP 树
3. 使用 Z-Buffer8.2.3 画家算法实现
javascript
/**
* 画家算法实现
*/
class PaintersAlgorithm {
constructor(ctx) {
this.ctx = ctx;
this.polygons = [];
}
/**
* 添加多边形
*/
addPolygon(vertices, color, depth = null) {
// 如果没有提供深度,计算中心点深度
if (depth === null) {
depth = vertices.reduce((sum, v) => sum + v.z, 0) / vertices.length;
}
this.polygons.push({ vertices, color, depth });
}
/**
* 渲染
*/
render() {
// 按深度排序(从远到近)
this.polygons.sort((a, b) => b.depth - a.depth);
// 依次绘制
for (const polygon of this.polygons) {
this.ctx.fillStyle = polygon.color;
this.ctx.beginPath();
const v = polygon.vertices;
this.ctx.moveTo(v[0].x, v[0].y);
for (let i = 1; i < v.length; i++) {
this.ctx.lineTo(v[i].x, v[i].y);
}
this.ctx.closePath();
this.ctx.fill();
}
}
/**
* 清除
*/
clear() {
this.polygons = [];
}
}8.3 BSP 树
8.3.1 BSP 树原理
BSP 树(Binary Space Partitioning)
通过递归地用平面分割空间,构建二叉树。
空间
│
┌───────────┼───────────┐
│ │ │
│ 平面 P │ │
│ │ │
├───────────┴───────────┤
│ │
前方 后方
构建示例:
原始多边形:
┌───┐
│ A │
└───┘ ┌───┐
│ B │
┌───┐ └───┘
│ C │
└───┘
选择 A 作为分割平面:
A
╱ ╲
╱ ╲
前 后
│ │
C B
遍历顺序(从相机看):
1. 如果相机在节点平面前方:先后、再节点、最后前
2. 如果相机在节点平面后方:先前、再节点、最后后8.3.2 BSP 树实现
javascript
/**
* BSP 树节点
*/
class BSPNode {
constructor(polygon) {
this.polygon = polygon; // 分割平面上的多边形
this.front = null; // 前方子树
this.back = null; // 后方子树
}
}
/**
* BSP 树
*/
class BSPTree {
constructor() {
this.root = null;
}
/**
* 计算多边形的平面方程
*/
computePlane(polygon) {
const v = polygon.vertices;
// 使用前三个顶点计算法向量
const e1 = {
x: v[1].x - v[0].x,
y: v[1].y - v[0].y,
z: v[1].z - v[0].z
};
const e2 = {
x: v[2].x - v[0].x,
y: v[2].y - v[0].y,
z: v[2].z - v[0].z
};
// 法向量 = e1 × e2
const normal = {
x: e1.y * e2.z - e1.z * e2.y,
y: e1.z * e2.x - e1.x * e2.z,
z: e1.x * e2.y - e1.y * e2.x
};
// d = -n · v0
const d = -(normal.x * v[0].x + normal.y * v[0].y + normal.z * v[0].z);
return { normal, d };
}
/**
* 判断点在平面的哪一侧
*/
classifyPoint(point, plane) {
const dist =
plane.normal.x * point.x +
plane.normal.y * point.y +
plane.normal.z * point.z +
plane.d;
const epsilon = 0.0001;
if (dist > epsilon) return 'front';
if (dist < -epsilon) return 'back';
return 'on';
}
/**
* 分类多边形
*/
classifyPolygon(polygon, plane) {
let frontCount = 0;
let backCount = 0;
for (const v of polygon.vertices) {
const side = this.classifyPoint(v, plane);
if (side === 'front') frontCount++;
if (side === 'back') backCount++;
}
if (frontCount > 0 && backCount === 0) return 'front';
if (backCount > 0 && frontCount === 0) return 'back';
if (frontCount === 0 && backCount === 0) return 'coplanar';
return 'spanning'; // 跨越平面,需要分割
}
/**
* 分割多边形
*/
splitPolygon(polygon, plane) {
const frontVertices = [];
const backVertices = [];
const vertices = polygon.vertices;
for (let i = 0; i < vertices.length; i++) {
const current = vertices[i];
const next = vertices[(i + 1) % vertices.length];
const currentSide = this.classifyPoint(current, plane);
const nextSide = this.classifyPoint(next, plane);
if (currentSide !== 'back') {
frontVertices.push(current);
}
if (currentSide !== 'front') {
backVertices.push(current);
}
// 如果边跨越平面,计算交点
if ((currentSide === 'front' && nextSide === 'back') ||
(currentSide === 'back' && nextSide === 'front')) {
const intersection = this.computeIntersection(current, next, plane);
frontVertices.push(intersection);
backVertices.push({ ...intersection });
}
}
return {
front: frontVertices.length >= 3
? { vertices: frontVertices, color: polygon.color }
: null,
back: backVertices.length >= 3
? { vertices: backVertices, color: polygon.color }
: null
};
}
/**
* 计算线段与平面的交点
*/
computeIntersection(p1, p2, plane) {
const d1 = plane.normal.x * p1.x + plane.normal.y * p1.y +
plane.normal.z * p1.z + plane.d;
const d2 = plane.normal.x * p2.x + plane.normal.y * p2.y +
plane.normal.z * p2.z + plane.d;
const t = d1 / (d1 - d2);
return {
x: p1.x + t * (p2.x - p1.x),
y: p1.y + t * (p2.y - p1.y),
z: p1.z + t * (p2.z - p1.z)
};
}
/**
* 构建 BSP 树
*/
build(polygons) {
if (polygons.length === 0) return null;
// 选择第一个多边形作为分割平面
const splitter = polygons[0];
const plane = this.computePlane(splitter);
const node = new BSPNode(splitter);
const frontPolygons = [];
const backPolygons = [];
// 分类其余多边形
for (let i = 1; i < polygons.length; i++) {
const polygon = polygons[i];
const classification = this.classifyPolygon(polygon, plane);
switch (classification) {
case 'front':
frontPolygons.push(polygon);
break;
case 'back':
backPolygons.push(polygon);
break;
case 'coplanar':
// 与分割面共面,可以放在任一侧
frontPolygons.push(polygon);
break;
case 'spanning':
// 需要分割
const split = this.splitPolygon(polygon, plane);
if (split.front) frontPolygons.push(split.front);
if (split.back) backPolygons.push(split.back);
break;
}
}
// 递归构建子树
node.front = this.build(frontPolygons);
node.back = this.build(backPolygons);
return node;
}
/**
* 按正确顺序遍历(画家算法)
*/
traverse(cameraPosition, callback) {
this.traverseNode(this.root, cameraPosition, callback);
}
traverseNode(node, cameraPosition, callback) {
if (!node) return;
const plane = this.computePlane(node.polygon);
const side = this.classifyPoint(cameraPosition, plane);
if (side === 'front') {
// 相机在前:先后、再节点、最后前
this.traverseNode(node.back, cameraPosition, callback);
callback(node.polygon);
this.traverseNode(node.front, cameraPosition, callback);
} else {
// 相机在后:先前、再节点、最后后
this.traverseNode(node.front, cameraPosition, callback);
callback(node.polygon);
this.traverseNode(node.back, cameraPosition, callback);
}
}
}8.4 背面剔除
8.4.1 背面剔除原理
背面剔除(Back-face Culling)
不渲染背对相机的面。
对于封闭物体,背面总是被前面遮挡,可以安全剔除。
判断方法:
计算面法向量与视线向量的点积:
法向量 N
↑
│
────┼──── 面
│
│
● 相机
如果 N · V > 0(法向量朝向相机):正面,渲染
如果 N · V < 0(法向量背离相机):背面,剔除
顶点顺序(Winding Order):
正面通常定义为顶点逆时针排列(从外部看):
正面(CCW): 背面(CW):
V2 V2
╱ ╲ ╱ ╲
╱ ╲ ╱ ╲
╱──────╲ ╱──────╲
V0 V1 V1 V08.4.2 背面剔除实现
javascript
/**
* 背面剔除
*/
class BackfaceCulling {
/**
* 判断三角形是否为背面
* @param v0, v1, v2 屏幕空间顶点
* @returns true 如果是背面
*/
static isBackface(v0, v1, v2) {
// 计算屏幕空间的有符号面积(叉积的 z 分量)
// 正值 = CCW(正面),负值 = CW(背面)
const area = (v1.x - v0.x) * (v2.y - v0.y) -
(v2.x - v0.x) * (v1.y - v0.y);
return area < 0;
}
/**
* 使用视线向量判断(3D 空间)
*/
static isBackface3D(v0, v1, v2, cameraPosition) {
// 计算面法向量
const e1 = {
x: v1.x - v0.x,
y: v1.y - v0.y,
z: v1.z - v0.z
};
const e2 = {
x: v2.x - v0.x,
y: v2.y - v0.y,
z: v2.z - v0.z
};
const normal = {
x: e1.y * e2.z - e1.z * e2.y,
y: e1.z * e2.x - e1.x * e2.z,
z: e1.x * e2.y - e1.y * e2.x
};
// 计算视线向量(从面中心到相机)
const center = {
x: (v0.x + v1.x + v2.x) / 3,
y: (v0.y + v1.y + v2.y) / 3,
z: (v0.z + v1.z + v2.z) / 3
};
const viewDir = {
x: cameraPosition.x - center.x,
y: cameraPosition.y - center.y,
z: cameraPosition.z - center.z
};
// 点积
const dot = normal.x * viewDir.x +
normal.y * viewDir.y +
normal.z * viewDir.z;
return dot < 0;
}
}
// WebGL 中启用背面剔除
// gl.enable(gl.CULL_FACE);
// gl.cullFace(gl.BACK); // 剔除背面
// gl.frontFace(gl.CCW); // 逆时针为正面8.5 视锥剔除
8.5.1 视锥剔除原理
视锥剔除(Frustum Culling)
不渲染视锥体外的物体。
视锥体
╱─────────╲
╱ ╲
╱ ● ╲ ← 可见
╱ 渲染 ╲
╱─────────────────╲
│
● ←────── │ ──────→ ●
不渲染 不渲染
判断步骤:
1. 为每个物体计算包围体(球或 AABB)
2. 测试包围体与视锥体 6 个平面的关系
3. 完全在外:剔除
4. 完全在内或相交:渲染8.5.2 视锥剔除实现
javascript
/**
* 视锥剔除
*/
class FrustumCuller {
constructor() {
// 6 个平面:near, far, left, right, top, bottom
this.planes = [];
}
/**
* 从视图投影矩阵提取平面
*/
extractPlanes(viewProjectionMatrix) {
const m = viewProjectionMatrix.elements;
// 左平面: row4 + row1
this.planes[0] = this.normalizePlane(
m[3] + m[0], m[7] + m[4], m[11] + m[8], m[15] + m[12]
);
// 右平面: row4 - row1
this.planes[1] = this.normalizePlane(
m[3] - m[0], m[7] - m[4], m[11] - m[8], m[15] - m[12]
);
// 底平面: row4 + row2
this.planes[2] = this.normalizePlane(
m[3] + m[1], m[7] + m[5], m[11] + m[9], m[15] + m[13]
);
// 顶平面: row4 - row2
this.planes[3] = this.normalizePlane(
m[3] - m[1], m[7] - m[5], m[11] - m[9], m[15] - m[13]
);
// 近平面: row4 + row3
this.planes[4] = this.normalizePlane(
m[3] + m[2], m[7] + m[6], m[11] + m[10], m[15] + m[14]
);
// 远平面: row4 - row3
this.planes[5] = this.normalizePlane(
m[3] - m[2], m[7] - m[6], m[11] - m[10], m[15] - m[14]
);
}
normalizePlane(a, b, c, d) {
const len = Math.sqrt(a * a + b * b + c * c);
return { a: a / len, b: b / len, c: c / len, d: d / len };
}
/**
* 测试球体
*/
testSphere(center, radius) {
for (const plane of this.planes) {
const dist = plane.a * center.x +
plane.b * center.y +
plane.c * center.z +
plane.d;
if (dist < -radius) {
return 'outside'; // 完全在平面外
}
}
return 'inside'; // 至少部分在内
}
/**
* 测试 AABB
*/
testAABB(min, max) {
for (const plane of this.planes) {
// 找到最正方向的顶点
const px = plane.a > 0 ? max.x : min.x;
const py = plane.b > 0 ? max.y : min.y;
const pz = plane.c > 0 ? max.z : min.z;
const dist = plane.a * px + plane.b * py + plane.c * pz + plane.d;
if (dist < 0) {
return 'outside';
}
}
return 'inside';
}
/**
* 剔除物体列表
*/
cull(objects) {
return objects.filter(obj => {
if (obj.boundingSphere) {
return this.testSphere(
obj.boundingSphere.center,
obj.boundingSphere.radius
) !== 'outside';
}
if (obj.boundingBox) {
return this.testAABB(
obj.boundingBox.min,
obj.boundingBox.max
) !== 'outside';
}
return true; // 没有包围体,不剔除
});
}
}8.6 遮挡剔除
8.6.1 遮挡剔除原理
遮挡剔除(Occlusion Culling)
不渲染被其他物体遮挡的物体。
场景示例:
相机
•
╱│╲
╱ │ ╲
╱ │ ╲
╱ ┌─┼─┐ ╲
╱ │ A │ ╲ ← 遮挡物 A
╱ └───┘ ╲
╱ │ ╲
╱ ┌──┼──┐ ╲
╱ │ B │ ╲ ← B 被 A 完全遮挡,可以剔除
╱ └────┘ ╲
通过视锥剔除后,B 仍会被渲染。
但遮挡剔除可以识别并跳过 B。
常用技术:
1. 硬件遮挡查询(Occlusion Query)
2. 层次化 Z 缓冲(Hierarchical Z-Buffer)
3. 软件光栅化预判(Software Rasterization)
4. Portal/Cell 划分8.6.2 遮挡查询实现
javascript
/**
* WebGL 遮挡查询
*/
class OcclusionQuery {
constructor(gl) {
this.gl = gl;
this.queries = new Map();
}
/**
* 开始查询
*/
begin(objectId) {
const gl = this.gl;
const ext = gl.getExtension('EXT_disjoint_timer_query_webgl2') ||
gl.getExtension('EXT_occlusion_query_boolean');
if (!ext) {
console.warn('Occlusion query not supported');
return null;
}
const query = gl.createQuery();
gl.beginQuery(gl.ANY_SAMPLES_PASSED, query);
this.queries.set(objectId, query);
return query;
}
/**
* 结束查询
*/
end() {
const gl = this.gl;
gl.endQuery(gl.ANY_SAMPLES_PASSED);
}
/**
* 获取查询结果
*/
getResult(objectId) {
const gl = this.gl;
const query = this.queries.get(objectId);
if (!query) return true; // 默认可见
// 检查结果是否可用
const available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE);
if (!available) {
return null; // 结果还没准备好
}
// 获取结果
const visible = gl.getQueryParameter(query, gl.QUERY_RESULT);
// 清理
gl.deleteQuery(query);
this.queries.delete(objectId);
return visible > 0;
}
}
/**
* 层次化遮挡剔除
*/
class HierarchicalOcclusionCulling {
constructor() {
this.hiZ = null; // 层次化 Z 缓冲
}
/**
* 构建层次化 Z 缓冲
*/
buildHiZ(depthBuffer, width, height) {
// 创建多级 mipmap
const levels = [];
levels.push(depthBuffer);
let w = width;
let h = height;
let current = depthBuffer;
while (w > 1 || h > 1) {
const newW = Math.max(1, Math.floor(w / 2));
const newH = Math.max(1, Math.floor(h / 2));
const next = new Float32Array(newW * newH);
// 下采样:取 4 个像素的最大深度
for (let y = 0; y < newH; y++) {
for (let x = 0; x < newW; x++) {
const x0 = x * 2;
const y0 = y * 2;
const x1 = Math.min(x0 + 1, w - 1);
const y1 = Math.min(y0 + 1, h - 1);
const d00 = current[y0 * w + x0];
const d01 = current[y0 * w + x1];
const d10 = current[y1 * w + x0];
const d11 = current[y1 * w + x1];
next[y * newW + x] = Math.max(d00, d01, d10, d11);
}
}
levels.push(next);
w = newW;
h = newH;
current = next;
}
this.hiZ = levels;
}
/**
* 测试包围盒是否被遮挡
*/
isOccluded(screenBounds, minDepth) {
if (!this.hiZ) return false;
// 选择合适的 mipmap 级别
const boxWidth = screenBounds.maxX - screenBounds.minX;
const boxHeight = screenBounds.maxY - screenBounds.minY;
const level = Math.floor(Math.log2(Math.max(boxWidth, boxHeight)));
const safeLevel = Math.min(level, this.hiZ.length - 1);
const hiZLevel = this.hiZ[safeLevel];
// 采样 Hi-Z
const scale = 1 / Math.pow(2, safeLevel);
const x = Math.floor(screenBounds.minX * scale);
const y = Math.floor(screenBounds.minY * scale);
const hiZDepth = hiZLevel[y * Math.floor(this.width * scale) + x];
// 如果包围盒的最近深度比 Hi-Z 更远,则被遮挡
return minDepth > hiZDepth;
}
}8.7 本章小结
剔除技术对比
| 技术 | 剔除内容 | 复杂度 | 效果 |
|---|---|---|---|
| 背面剔除 | 背面三角形 | O(1) | 约 50% |
| 视锥剔除 | 视锥外物体 | O(n) | 场景相关 |
| 遮挡剔除 | 被遮挡物体 | O(n log n) | 场景相关 |
| Z-Buffer | 被遮挡像素 | O(pixels) | 像素级 |
剔除流水线
场景物体
│
▼
┌─────────┐
│视锥剔除 │ ← 粗粒度,物体级
└────┬────┘
│
▼
┌─────────┐
│遮挡剔除 │ ← 中粒度,物体级
└────┬────┘
│
▼
┌─────────┐
│背面剔除 │ ← 细粒度,三角形级
└────┬────┘
│
▼
┌─────────┐
│深度测试 │ ← 最细粒度,像素级
└────┬────┘
│
▼
最终图像关键要点
- Z-Buffer 是最通用的可见性解决方案
- 背面剔除简单高效,应始终启用
- 视锥剔除是基本的场景管理技术
- 遮挡剔除在复杂场景中效果显著
- 多种技术结合使用效果最佳
下一章预告:在第9章中,我们将学习光照与着色模型,包括 Phong 模型、BRDF 和全局光照。
文档版本:v1.0
字数统计:约 11,000 字
