Skip to content

渲染管线

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/camera
    • stage: post-process(可选)自定义阶段,插入在 main 之后

核心概念

  • Stage — 命名的渲染阶段,按注册顺序依次执行。默认只有 'main',其他阶段通过 insertStage 显式插入。
  • Pass — stage 内的具体渲染单元,实现 render(frame),可选 resize(size)dispose()
  • Order — pass 的排序键,数字越小越先跑。相同 order 时按注册顺序稳定排序。
  • Enabled — 布尔值或 getter,动态决定本帧是否执行该 pass。

每帧 pipeline 会:

  1. 按 stage 的插入顺序遍历;
  2. 对每个 stage,过滤出当前启用的 pass(按 order + 注册顺序排序);
  3. 依次调用 pass.render(frame)

PipelinePassDefinition

名称 类型 默认值 说明
name必填stringpass 的标识,用于调试/日志
stage必填string目标 stage 名;若该 stage 不存在,自动插入到 main 之后
ordernumber0阶段内排序键,数字越小越先执行
enabledboolean | () => booleantrue静态布尔或每帧求值的 getter,决定本帧是否参与渲染
setup必填(ctx: PipelinePassContext) => PipelinePass首次创建时调用,返回 pass 实例(需实现 render)

PipelinePassContext 暴露了 enginepipelineview 三项,供 setup 里构建资源。

PipelinePass

名称 类型 默认值 说明
render必填(frame: EngineFrameContext) => void每帧调用;frame 包含 renderer / scene / camera / delta / elapsed
resize(size: EngineViewSize) => void视口变更时调用,用来重建 render target
dispose() => voidpass 被移除或 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

新阶段默认追加到末尾;传入 beforeafter 可定位到已存在 stage 的前后。

ts
// 在 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' });

beforeafter 不能同时传;目标 stage 不存在会抛错。

addPass

addPass 返回一个取消函数。调用它会同步从 pipeline 移除该 pass 并触发 dispose

ts
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 帧循环或挂载逻辑自动触发,但也可以在测试、离屏渲染、截图等场景手动驱动:

ts
// 手动渲染一帧
engine.pipeline.render(frameContext);

// 视口变化时广播 resize
engine.pipeline.resize({ width: 1280, height: 720 });

// 清空所有 pass 与自定义 stage
engine.pipeline.clear();

完整示例:插入后处理阶段

下面在 'main' 之后插入 'post-process' 阶段,注册一个全屏 quad pass——概念上可以类比 EffectComposer,但 pass 的资源与生命周期由自己掌控。

ts
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 里调用。

基于 MIT 协议发布(内部使用)