第 2 节:render() 的完整旅程——一帧内部发生了什么
🎯 学习目标
- 完整理解
WebGLRenderer.render()的调用链 - 掌握
projectObject()的递归遍历与对象收集机制 - 理解不透明/透明队列的排序策略及其性能考量
- 追踪从
renderObjects()到gl.drawArrays()的最终绘制路径 - 实现一个能遍历场景图并按顺序绘制的极简渲染循环
❓ 带着问题读源码
renderer.render(scene, camera)进去以后,第一件事做的是什么?最后一件事又是什么?projectObject()是如何区分 Mesh、Light、Sprite 等不同类型对象的?- 不透明队列为什么要按 material.id 排序?透明队列为什么要按 z 从远到近排序?
renderBufferDirect()最终发出了哪条 GL 命令?它是如何决定用drawArrays还是drawElements的?
📖 render() 入口全貌
源码位置:src/renderers/WebGLRenderer.js → this.render = function(scene, camera) { ... }
render() 是整个 Three.js 渲染管线的总调度函数,约 200 行(不含子函数)。下面是简化后的主干结构:
this.render = function (scene, camera) {
// ========== 阶段 1:前置准备 ==========
if (camera.parent === null) camera.updateMatrixWorld();
if (scene.matrixWorldAutoUpdate) scene.updateMatrixWorld();
currentRenderState = renderStates.get(scene, camera);
currentRenderState.init();
_projScreenMatrix.multiplyMatrices(
camera.projectionMatrix,
camera.matrixWorldInverse
);
_frustum.setFromProjectionMatrix(_projScreenMatrix);
currentRenderList = renderLists.get(scene, camera);
currentRenderList.init();
// ========== 阶段 2:场景遍历 ==========
projectObject(scene, camera, 0, _this.sortObjects);
// ========== 阶段 3:排序 ==========
currentRenderList.finish();
if (_this.sortObjects) {
currentRenderList.sort(opaqueSort, transparentSort);
}
// ========== 阶段 4:阴影 ==========
const shadowsArray = currentRenderState.state.shadowsArray;
shadowMap.render(shadowsArray, scene, camera);
// ========== 阶段 5:光照 ==========
currentRenderState.setupLights();
// ========== 阶段 6:清屏 ==========
if (this.autoClear) {
this.clear(this.autoClearColor, this.autoClearDepth, this.autoClearStencil);
}
// ========== 阶段 7:渲染主队列 ==========
const opaqueObjects = currentRenderList.opaque;
const transparentObjects = currentRenderList.transparent;
if (opaqueObjects.length > 0)
renderObjects(opaqueObjects, scene, camera);
if (transparentObjects.length > 0)
renderObjects(transparentObjects, scene, camera);
// ========== 阶段 8:收尾 ==========
state.buffers.depth.setTest(true);
state.buffers.depth.setMask(true);
state.buffers.color.setMask(true);
state.setPolygonOffset(false);
currentRenderList = null;
currentRenderState = null;
};
关键设计决策
为什么先遍历再排序,而不是边遍历边排序?
Three.js 选择"收集-排序-渲染"的三阶段管线,而非边遍历边渲染。原因有三:
- 排序需要全局信息:不透明排序需要比较所有对象的 material.id 和深度值
- 阴影需要先于主渲染:shadowMap.render() 必须在所有光源收集完毕后执行
- 光照需要统一 setup:所有 Light 对象收集完毕后才能打包成 Uniform 数组
🔍 projectObject() 深入分析
源码位置:src/renderers/WebGLRenderer.js → function projectObject(...)
projectObject() 是场景遍历的核心——它递归遍历整棵场景图,把可渲染对象收集到 currentRenderList,把光源收集到 currentRenderState。
function projectObject(object, camera, groupOrder, sortObjects) {
// 不可见对象直接跳过(含所有后代)
if (object.visible === false) return;
// Layers 系统——位掩码过滤
const visible = object.layers.test(camera.layers);
if (visible) {
// ---- 光源 ----
if (object.isLight) {
currentRenderState.pushLight(object);
if (object.castShadow) {
currentRenderState.pushShadow(object);
}
}
// ---- 可渲染对象:Mesh / Line / Points ----
else if (object.isMesh || object.isLine || object.isPoints) {
if (!object.frustumCulled || _frustum.intersectsObject(object)) {
// 计算 NDC 空间的 z 值,用于排序
if (sortObjects) {
_vector4.setFromMatrixPosition(object.matrixWorld);
_vector4.applyMatrix4(_projScreenMatrix);
}
const geometry = object.geometry;
const material = object.material;
// 多材质对象:按 geometry.groups 拆分
if (Array.isArray(material)) {
const groups = geometry.groups;
for (let i = 0, l = groups.length; i < l; i++) {
const group = groups[i];
const groupMaterial = material[group.materialIndex];
if (groupMaterial && groupMaterial.visible) {
currentRenderList.push(
object, geometry, groupMaterial,
groupOrder, _vector4.z, group
);
}
}
}
// 单材质对象
else if (material.visible) {
currentRenderList.push(
object, geometry, material,
groupOrder, _vector4.z, null
);
}
}
}
// ---- Sprite ----
else if (object.isSprite) {
// 类似 Mesh 处理,但始终面向相机
// ...
}
}
// 无条件递归遍历所有子节点
const children = object.children;
for (let i = 0, l = children.length; i < l; i++) {
projectObject(children[i], camera, groupOrder, sortObjects);
}
}
设计要点逐行解析
1. object.visible === false 的短路优化
if (object.visible === false) return;
当一个节点不可见时,它的所有后代也被跳过。这是场景图剔除的第一道关卡。
2. Layers 位掩码过滤
const visible = object.layers.test(camera.layers);
Layers 是一个 32 位掩码系统,允许把对象分组。只有 object.layers & camera.layers !== 0 时才通过过滤。典型用途:多相机渲染(UI 相机、主场景相机各看不同 Layer)。
3. Frustum Culling
if (!object.frustumCulled || _frustum.intersectsObject(object))
先检查 frustumCulled 标志(默认 true),再用视锥体 6 个平面对 BoundingSphere 做相交检测。不在视锥体内的对象直接跳过,不进入渲染队列。
4. NDC 深度值计算
_vector4.setFromMatrixPosition(object.matrixWorld);
_vector4.applyMatrix4(_projScreenMatrix);
取对象的世界坐标原点,乘以投影-视图矩阵,得到 NDC 空间的 z 值。这个 z 值将作为排序的依据。
5. 多材质拆分
一个 Mesh 可以有多个材质(material 为数组),此时按 geometry.groups 把面拆分为多个渲染项,每个 group 对应一个材质。这就是为什么 currentRenderList 中的项包含 group 字段。
📊 WebGLRenderList 与排序策略
源码位置:src/renderers/webgl/WebGLRenderList.js
WebGLRenderList 是 Three.js 内部的渲染队列管理器,维护两个数组:
class WebGLRenderList {
constructor() {
this.opaque = []; // 不透明队列
this.transparent = []; // 透明队列
this.renderItems = []; // 对象池
this.renderItemsIndex = 0;
}
push(object, geometry, material, groupOrder, z, group) {
const renderItem = this.getNextRenderItem(object, geometry, material, groupOrder, z, group);
// 根据材质透明度分流
if (material.transparent) {
this.transparent.push(renderItem);
} else {
this.opaque.push(renderItem);
}
}
sort(customOpaqueSort, customTransparentSort) {
if (this.opaque.length > 1)
this.opaque.sort(customOpaqueSort || painterSortStable);
if (this.transparent.length > 1)
this.transparent.sort(customTransparentSort || reversePainterSortStable);
}
}
不透明排序:painterSortStable
function painterSortStable(a, b) {
if (a.groupOrder !== b.groupOrder) return a.groupOrder - b.groupOrder;
if (a.renderOrder !== b.renderOrder) return a.renderOrder - b.renderOrder;
if (a.material.id !== b.material.id) return a.material.id - b.material.id;
if (a.z !== b.z) return a.z - b.z; // 从近到远
return a.id - b.id;
}
排序优先级:
groupOrder → renderOrder → material.id → z (近→远) → id
为什么 material.id 比深度排序优先?
不透明对象不需要考虑前后遮挡的渲染顺序(有 depth test 保证正确性)。但切换 Material/Program 的开销很大(需要重新编译 Shader、传递 Uniform)。因此优先按 material.id 聚合,减少 GPU 状态切换次数。之后再按 z 从近到远排序——Early-Z 硬件优化可以跳过被遮挡的片元,减少 overdraw。
透明排序:reversePainterSortStable
function reversePainterSortStable(a, b) {
if (a.groupOrder !== b.groupOrder) return a.groupOrder - b.groupOrder;
if (a.renderOrder !== b.renderOrder) return a.renderOrder - b.renderOrder;
if (a.z !== b.z) return b.z - a.z; // 从远到近(反向)
return a.id - b.id;
}
排序优先级:
groupOrder → renderOrder → z (远→近) → id
为什么透明不按 material.id 排序?
透明对象必须严格按从远到近的顺序渲染,否则 Alpha 混合结果会出错。正确的混合顺序比减少状态切换更重要。
排序策略可视化
不透明队列 (Opaque)
┌──────────────────────────────────────────────┐
│ Material A │ Material A │ Material B │
│ z = 0.2 │ z = 0.5 │ z = 0.1 │
│ (近→远排序) │ │ │
└──────────────────────────────────────────────┘
↑ 先按 material.id 聚合,再按 z 排序
效果:减少状态切换 + Early-Z 优化
透明队列 (Transparent)
┌──────────────────────────────────────────────┐
│ Material B │ Material A │ Material A │
│ z = 0.9 │ z = 0.5 │ z = 0.2 │
│ (远→近排序) │ │ │
└──────────────────────────────────────────────┘
↑ 只按 z 排序,保证 Alpha 混合正确
🔄 renderObjects() → renderObject() → renderBufferDirect()
renderObjects()
function renderObjects(renderList, scene, camera) {
for (let i = 0, l = renderList.length; i < l; i++) {
const renderItem = renderList[i];
const object = renderItem.object;
const geometry = renderItem.geometry;
const material = renderItem.material;
const group = renderItem.group;
renderObject(object, scene, camera, geometry, material, group);
}
}
renderObjects 本身很简单——遍历排好序的队列,依次调用 renderObject()。
renderObject()
function renderObject(object, scene, camera, geometry, material, group) {
object.onBeforeRender(_this, scene, camera, geometry, material, group);
// 计算 modelViewMatrix 和 normalMatrix
object.modelViewMatrix.multiplyMatrices(camera.matrixWorldInverse, object.matrixWorld);
object.normalMatrix.getNormalMatrix(object.modelViewMatrix);
// 如果是 ImmediateRenderObject,走特殊路径
// 否则走标准路径
_this.renderBufferDirect(camera, scene, geometry, material, object, group);
object.onAfterRender(_this, scene, camera, geometry, material, group);
}
renderObject() 的核心职责:
- 计算 ModelView 矩阵:
modelViewMatrix = viewMatrix × worldMatrix - 计算 Normal 矩阵:
normalMatrix = transpose(inverse(modelViewMatrix))的左上 3×3 - 调用钩子:
onBeforeRender/onAfterRender允许用户注入自定义逻辑 - 转发到 renderBufferDirect()
renderBufferDirect()——最终绘制
this.renderBufferDirect = function (camera, scene, geometry, material, object, group) {
// 1. 获取/编译 Program
const program = setProgram(camera, scene, geometry, material, object);
// 2. 绑定几何属性(VAO / VBO)
const index = geometry.index;
const position = geometry.attributes.position;
let rangeFactor = 1;
if (material.wireframe) {
index = geometries.getWireframeAttribute(geometry);
rangeFactor = 2;
}
// 3. 设置 vertex attributes
const renderer = bufferRenderer; // drawArrays 路径
let instanceCount;
if (index !== null) {
renderer = indexedBufferRenderer; // drawElements 路径
renderer.setIndex(index);
}
// 4. 确定绘制范围
const dataCount = (index !== null) ? index.count : position.count;
const rangeStart = geometry.drawRange.start * rangeFactor;
const rangeCount = geometry.drawRange.count * rangeFactor;
const groupStart = group !== null ? group.start * rangeFactor : 0;
const groupCount = group !== null ? group.count * rangeFactor : Infinity;
const drawStart = Math.max(rangeStart, groupStart);
const drawEnd = Math.min(
dataCount,
rangeStart + rangeCount,
groupStart + groupCount
) - 1;
const drawCount = Math.max(0, drawEnd - drawStart + 1);
if (drawCount === 0) return;
// 5. 发出绘制命令
if (object.isInstancedMesh) {
renderer.renderInstances(drawStart, drawCount, object.count);
} else if (geometry.isInstancedBufferGeometry) {
const maxInstanceCount = geometry._maxInstanceCount;
renderer.renderInstances(drawStart, drawCount, maxInstanceCount);
} else {
renderer.render(drawStart, drawCount);
}
};
最终的 GL 调用:
有 index?
├── 是 → gl.drawElements(mode, count, type, start * bytesPerElement)
│ 或 gl.drawElementsInstanced(mode, count, type, start, instanceCount)
└── 否 → gl.drawArrays(mode, start, count)
或 gl.drawArraysInstanced(mode, start, count, instanceCount)
setProgram()——渲染前的状态设置
function setProgram(camera, scene, geometry, material, object) {
// 1. 获取/编译 WebGLProgram
const materialProperties = properties.get(material);
const program = materialProperties.currentProgram;
// 2. 绑定 Program
state.useProgram(program.program);
// 3. 设置全局 Uniforms(相机、雾、光照等)
p_uniforms.setValue(_gl, 'projectionMatrix', camera.projectionMatrix);
p_uniforms.setValue(_gl, 'viewMatrix', camera.matrixWorldInverse);
p_uniforms.setValue(_gl, 'modelViewMatrix', object.modelViewMatrix);
p_uniforms.setValue(_gl, 'normalMatrix', object.normalMatrix);
p_uniforms.setValue(_gl, 'modelMatrix', object.matrixWorld);
// 4. 设置材质 Uniforms
// ... (由 material 类型决定)
return program;
}
📈 收尾阶段
渲染完成后,Three.js 做了以下清理工作:
// 恢复默认状态
state.buffers.depth.setTest(true);
state.buffers.depth.setMask(true);
state.buffers.color.setMask(true);
state.setPolygonOffset(false);
// 清空临时引用
currentRenderList = null;
currentRenderState = null;
info 统计
WebGLInfo 模块在每帧渲染中收集统计数据:
// 每次 drawArrays / drawElements 调用时更新
info.render.calls++;
info.render.triangles += count / 3; // 假设 mode = TRIANGLES
info.render.points += count;
info.render.lines += count / 2;
用户可以通过 renderer.info 访问这些统计信息,非常有用:
console.log(renderer.info.render.calls); // 本帧 Draw Call 数
console.log(renderer.info.render.triangles); // 本帧三角面数
console.log(renderer.info.memory.geometries); // GPU 上的几何体数
console.log(renderer.info.memory.textures); // GPU 上的纹理数
🔁 完整调用链总结
renderer.render(scene, camera)
│
├── updateMatrixWorld() ← 更新场景图矩阵
├── 计算 _projScreenMatrix ← projection × view
├── 计算 _frustum ← 从投影矩阵提取 6 个裁剪面
│
├── projectObject(scene) ← 递归遍历场景图
│ ├── layers.test() ← 图层过滤
│ ├── frustum.intersects() ← 视锥体剔除
│ ├── pushLight() ← 收集光源
│ └── renderList.push() ← 收集可渲染对象 + 深度值
│
├── renderList.sort() ← 不透明 / 透明 分别排序
│
├── shadowMap.render() ← 渲染阴影贴图
├── setupLights() ← 打包光照 Uniform
├── clear() ← 清屏
│
├── renderObjects(opaque) ← 渲染不透明队列
│ └── renderObject()
│ ├── 计算 modelViewMatrix
│ └── renderBufferDirect()
│ ├── setProgram() ← 编译/缓存 Program + 设置 Uniforms
│ ├── 绑定 VAO/VBO
│ └── gl.drawElements() 或 gl.drawArrays()
│
├── renderObjects(transparent) ← 渲染透明队列
│
└── 状态复位 + info 统计
🛠️ Mini 实现
扩展第 1 节的 MiniRenderer,实现可工作的遍历-排序-渲染循环:
class MiniRenderItem {
constructor(object, z) {
this.object = object;
this.z = z;
this.transparent = object.material?.transparent ?? false;
}
}
class MiniRenderList {
constructor() {
this.opaque = [];
this.transparent = [];
}
init() {
this.opaque.length = 0;
this.transparent.length = 0;
}
push(object, z) {
const item = new MiniRenderItem(object, z);
if (item.transparent) {
this.transparent.push(item);
} else {
this.opaque.push(item);
}
}
sort() {
this.opaque.sort((a, b) => a.z - b.z); // 近→远
this.transparent.sort((a, b) => b.z - a.z); // 远→近
}
}
class MiniRenderer {
constructor(canvas) {
this.gl = canvas.getContext('webgl2');
this.renderList = new MiniRenderList();
}
render(scene, camera) {
const gl = this.gl;
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
this.renderList.init();
this._projectObject(scene, camera);
this.renderList.sort();
// 先渲染不透明
for (const item of this.renderList.opaque) {
this._renderObject(item.object, camera);
}
// 再渲染透明
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
for (const item of this.renderList.transparent) {
this._renderObject(item.object, camera);
}
gl.disable(gl.BLEND);
}
_projectObject(object, camera) {
if (!object.visible) return;
if (object.geometry && object.material) {
const worldPos = object.getWorldPosition();
const z = this._computeDepth(worldPos, camera);
this.renderList.push(object, z);
}
for (const child of object.children) {
this._projectObject(child, camera);
}
}
_computeDepth(worldPos, camera) {
const dx = worldPos[0] - camera.position[0];
const dy = worldPos[1] - camera.position[1];
const dz = worldPos[2] - camera.position[2];
return dx * dx + dy * dy + dz * dz;
}
_renderObject(object, camera) {
const gl = this.gl;
const { geometry, material } = object;
gl.useProgram(material.program);
// 绑定属性 & uniforms(简化版)
for (const [name, attr] of Object.entries(geometry.attributes)) {
const loc = gl.getAttribLocation(material.program, name);
if (loc < 0) continue;
gl.bindBuffer(gl.ARRAY_BUFFER, attr.buffer);
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, attr.size, gl.FLOAT, false, 0, 0);
}
if (geometry.index) {
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.index.buffer);
gl.drawElements(gl.TRIANGLES, geometry.index.count, gl.UNSIGNED_SHORT, 0);
} else {
gl.drawArrays(gl.TRIANGLES, 0, geometry.attributes.position.count);
}
}
}
这个极简实现体现了 Three.js render() 的核心三阶段:
- 遍历:
_projectObject()递归收集可渲染对象并计算深度 - 排序:
MiniRenderList.sort()分别对不透明和透明队列排序 - 渲染:
_renderObject()依次绑定状态并发出绘制命令
🔍 OGL 对比
OGL 的 Renderer.js 同样实现了遍历-排序-渲染管线,但更精简:
| 维度 | Three.js | OGL |
|---|---|---|
| renderList 抽象 | 独立的 WebGLRenderList 类,带对象池 | 无独立类,直接用临时数组 |
| 排序键 | groupOrder → renderOrder → material.id → z | program.id → zDepth |
| Frustum Culling | 在 projectObject() 中执行 | 在排序阶段的 callback 中执行 |
| 多材质支持 | 支持 material 数组 + geometry.groups | 不内置,需手动拆分 |
| info 统计 | 内置 WebGLInfo | 无内置统计 |
OGL 的 render 简化版
// OGL Renderer.js 的核心渲染逻辑
render({ scene, camera }) {
// 更新矩阵
if (scene) scene.updateMatrixWorld();
if (camera) camera.updateMatrixWorld();
// 获取渲染列表(扁平化场景图)
const renderList = this.getRenderList({ scene, camera });
// 排序:先按 program.id 再按深度
renderList.sort(this.sortOpaque);
// 逐个渲染
renderList.forEach(node => {
node.draw({ camera });
});
}
OGL 把排序和 Frustum Culling 留给用户配置回调函数,做到极致精简。Three.js 则内置了完善的排序策略和剔除机制,开箱即用。
📝 本节总结
- render() 八阶段:矩阵更新 → 场景遍历 → 排序 → 阴影 → 光照 → 清屏 → 渲染 → 收尾
- projectObject():递归遍历场景图,用 visible/layers/frustumCulling 三层过滤,将对象分流到 opaque/transparent 队列
- 不透明排序:优先按 material.id 聚合(减少状态切换),再按 z 从近到远(利用 Early-Z)
- 透明排序:严格按 z 从远到近(保证 Alpha 混合正确)
- renderBufferDirect():根据 index 存在与否选择 drawArrays/drawElements,支持 Instanced 路径
- info 统计:每帧记录 Draw Call 数、三角面数、内存使用等信息
➡️ 下一节预告
第 3 节:Geometry 系统——BufferGeometry 如何驯服 VBO
我们将深入 Three.js 的几何数据层,理解:
BufferAttribute如何把 TypedArray 包装成带元数据的属性描述BufferGeometry如何组织 attributes/index/groups 的数据结构WebGLAttributes如何决定gl.bufferDatavsgl.bufferSubData- Interleaved 模式如何通过 stride 共享减少 VBO 绑定
- 实现一个带
setAttribute/setIndex/update的极简 Geometry 类