Skip to main content

第 2 节:render() 的完整旅程——一帧内部发生了什么

🎯 学习目标

  • 完整理解 WebGLRenderer.render() 的调用链
  • 掌握 projectObject() 的递归遍历与对象收集机制
  • 理解不透明/透明队列的排序策略及其性能考量
  • 追踪从 renderObjects()gl.drawArrays() 的最终绘制路径
  • 实现一个能遍历场景图并按顺序绘制的极简渲染循环

❓ 带着问题读源码

  1. renderer.render(scene, camera) 进去以后,第一件事做的是什么?最后一件事又是什么?
  2. projectObject() 是如何区分 Mesh、Light、Sprite 等不同类型对象的?
  3. 不透明队列为什么要按 material.id 排序?透明队列为什么要按 z 从远到近排序?
  4. renderBufferDirect() 最终发出了哪条 GL 命令?它是如何决定用 drawArrays 还是 drawElements 的?

📖 render() 入口全貌

源码位置:src/renderers/WebGLRenderer.jsthis.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 选择"收集-排序-渲染"的三阶段管线,而非边遍历边渲染。原因有三:

  1. 排序需要全局信息:不透明排序需要比较所有对象的 material.id 和深度值
  2. 阴影需要先于主渲染:shadowMap.render() 必须在所有光源收集完毕后执行
  3. 光照需要统一 setup:所有 Light 对象收集完毕后才能打包成 Uniform 数组

🔍 projectObject() 深入分析

源码位置:src/renderers/WebGLRenderer.jsfunction 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() 的核心职责:

  1. 计算 ModelView 矩阵modelViewMatrix = viewMatrix × worldMatrix
  2. 计算 Normal 矩阵normalMatrix = transpose(inverse(modelViewMatrix)) 的左上 3×3
  3. 调用钩子onBeforeRender / onAfterRender 允许用户注入自定义逻辑
  4. 转发到 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() 的核心三阶段:

  1. 遍历_projectObject() 递归收集可渲染对象并计算深度
  2. 排序MiniRenderList.sort() 分别对不透明和透明队列排序
  3. 渲染_renderObject() 依次绑定状态并发出绘制命令

🔍 OGL 对比

OGL 的 Renderer.js 同样实现了遍历-排序-渲染管线,但更精简:

维度Three.jsOGL
renderList 抽象独立的 WebGLRenderList 类,带对象池无独立类,直接用临时数组
排序键groupOrder → renderOrder → material.id → zprogram.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 则内置了完善的排序策略和剔除机制,开箱即用。


📝 本节总结

  1. render() 八阶段:矩阵更新 → 场景遍历 → 排序 → 阴影 → 光照 → 清屏 → 渲染 → 收尾
  2. projectObject():递归遍历场景图,用 visible/layers/frustumCulling 三层过滤,将对象分流到 opaque/transparent 队列
  3. 不透明排序:优先按 material.id 聚合(减少状态切换),再按 z 从近到远(利用 Early-Z)
  4. 透明排序:严格按 z 从远到近(保证 Alpha 混合正确)
  5. renderBufferDirect():根据 index 存在与否选择 drawArrays/drawElements,支持 Instanced 路径
  6. info 统计:每帧记录 Draw Call 数、三角面数、内存使用等信息

➡️ 下一节预告

第 3 节:Geometry 系统——BufferGeometry 如何驯服 VBO

我们将深入 Three.js 的几何数据层,理解:

  • BufferAttribute 如何把 TypedArray 包装成带元数据的属性描述
  • BufferGeometry 如何组织 attributes/index/groups 的数据结构
  • WebGLAttributes 如何决定 gl.bufferData vs gl.bufferSubData
  • Interleaved 模式如何通过 stride 共享减少 VBO 绑定
  • 实现一个带 setAttribute / setIndex / update 的极简 Geometry 类