渲染管线
EnginePipeline 负责把每一帧的渲染切成若干 stage(阶段),每个 stage 内部再注册若干 pass(通道)。它让渲染流程可插拔——后处理、辉光、描边、独立 gizmo 图层都只是"在某个阶段插入一个 pass",不必重写主循环。
概述
默认情况下,pipeline 只有一个 stage:'main'。当 'main' 阶段没有任何 pass 时,pipeline 会自动执行一次 renderer.render(scene, camera),等价于传统的单遍渲染。一旦你向 'main' 注册了 pass,pipeline 就把控制权交给这些 pass,由你自己负责调用 renderer.render(...)。
pipeline.render(frame)按 stage 顺序遍历stage: pre-process(可选)自定义阶段,插入在 main 之前stage: main默认阶段;无 pass 时自动渲染 scene/camerastage: post-process(可选)自定义阶段,插入在 main 之后
核心概念
- Stage — 命名的渲染阶段,按注册顺序依次执行。默认只有
'main',其他阶段通过insertStage显式插入。 - Pass — stage 内的具体渲染单元,实现
render(frame),可选resize(size)与dispose()。 - Order — pass 的排序键,数字越小越先跑。相同
order时按注册顺序稳定排序。 - Enabled — 布尔值或 getter,动态决定本帧是否执行该 pass。
每帧 pipeline 会:
- 按 stage 的插入顺序遍历;
- 对每个 stage,过滤出当前启用的 pass(按
order+ 注册顺序排序); - 依次调用
pass.render(frame)。
PipelinePassDefinition
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name必填 | string | — | pass 的标识,用于调试/日志 |
stage必填 | string | — | 目标 stage 名;若该 stage 不存在,自动插入到 main 之后 |
order | number | 0 | 阶段内排序键,数字越小越先执行 |
enabled | boolean | () => boolean | true | 静态布尔或每帧求值的 getter,决定本帧是否参与渲染 |
setup必填 | (ctx: PipelinePassContext) => PipelinePass | — | 首次创建时调用,返回 pass 实例(需实现 render) |
PipelinePassContext 暴露了 engine、pipeline、view 三项,供 setup 里构建资源。
PipelinePass
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
render必填 | (frame: EngineFrameContext) => void | — | 每帧调用;frame 包含 renderer / scene / camera / delta / elapsed |
resize | (size: EngineViewSize) => void | — | 视口变更时调用,用来重建 render target |
dispose | () => void | — | pass 被移除或 engine 销毁时调用,负责释放 GPU 资源 |
EnginePipeline
| 字段 / 方法 | 签名 | 说明 |
|---|---|---|
addPass | <T>(def: PipelinePassDefinition<T>) => () => void | 注册一个 pass,返回取消函数;取消时自动调用 dispose |
insertStage | (name: string, options?: { before?: string; after?: string }) => void | 插入一个新 stage;before/after 互斥 |
hasStage | (name: string) => boolean | 判断 stage 是否存在 |
getStages | () => string[] | 返回当前 stage 顺序快照 |
render | (frame: EngineFrameContext) => void | 手动驱动一帧;通常由 engine 的帧循环调用 |
resize | (size: EngineViewSize) => void | 广播 resize 给所有 pass |
clear | () => void | 清空全部 pass(含 dispose)并重置 stage 顺序为 [main] |
insertStage
新阶段默认追加到末尾;传入 before 或 after 可定位到已存在 stage 的前后。
// 在 main 之前插入 'pre-process'
engine.pipeline.insertStage('pre-process', { before: 'main' });
// 在 main 之后插入 'post-process'
engine.pipeline.insertStage('post-process', { after: 'main' });
// 在 'post-process' 之后再追加一个 'overlay'
engine.pipeline.insertStage('overlay', { after: 'post-process' });before 和 after 不能同时传;目标 stage 不存在会抛错。
addPass
addPass 返回一个取消函数。调用它会同步从 pipeline 移除该 pass 并触发 dispose。
const cancel = engine.pipeline.addPass({
name: 'debug-axes',
stage: 'main',
order: 100,
setup({ engine }) {
return {
render(frame) {
// 依赖默认 main 渲染过后再画一层 debug
frame.renderer.render(frame.scene, frame.camera);
},
};
},
});
// 卸载时
cancel();注意:一旦 'main' 阶段注册了任何 pass,pipeline 就不会再自动执行默认渲染——每个 pass 必须自行决定是否调用 renderer.render(...)。
手动驱动
下面三个方法通常由 engine 帧循环或挂载逻辑自动触发,但也可以在测试、离屏渲染、截图等场景手动驱动:
// 手动渲染一帧
engine.pipeline.render(frameContext);
// 视口变化时广播 resize
engine.pipeline.resize({ width: 1280, height: 720 });
// 清空所有 pass 与自定义 stage
engine.pipeline.clear();完整示例:插入后处理阶段
下面在 'main' 之后插入 'post-process' 阶段,注册一个全屏 quad pass——概念上可以类比 EffectComposer,但 pass 的资源与生命周期由自己掌控。
import { WebGLRenderTarget } from 'three';
engine.pipeline.insertStage('post-process', { after: 'main' });
const cancel = engine.pipeline.addPass({
name: 'tone-mapping',
stage: 'post-process',
order: 0,
setup({ view }) {
let renderTarget = new WebGLRenderTarget(view.size.width, view.size.height);
const quad = createFullscreenQuad(/* shader material */);
return {
render(frame) {
// 1. 主画面已经被 main stage 渲染到 renderTarget
frame.renderer.setRenderTarget(renderTarget);
frame.renderer.render(frame.scene, frame.camera);
// 2. 再把 renderTarget 当纹理,绘制到屏幕
frame.renderer.setRenderTarget(null);
quad.material.uniforms.tDiffuse.value = renderTarget.texture;
frame.renderer.render(quad.scene, quad.camera);
},
resize(size) {
renderTarget.setSize(size.width, size.height);
},
dispose() {
renderTarget.dispose();
quad.dispose();
},
};
},
});由于 'post-process' 接管了最终的屏幕写入,建议把 'main' 也改写为"渲染到 renderTarget"而非默认行为——避免默认渲染直接吞掉屏幕。
最佳实践
- order 留间隔。建议用
-100 / 0 / 100这种稀疏数字,后续需要在既有 pass 之间再插入时就不必全局调整。 - render 里不要重分配资源。renderTarget、几何体、Material 一律在
setup里建好,resize里更新尺寸,dispose里清理。 - resize 只做尺寸更新。它被视口变化触发,不应在这里创建新 pass 或改变 stage 拓扑。
- 一个 stage 一件事。描边、辉光、tone mapping 分别独立成 stage 或独立 order 的 pass,便于单独开关。
- 取消即释放。
addPass返回的取消函数是销毁 pass 的唯一入口——别忘了在 System 或 Script 的onDestroy里调用。