本章实测环境Babylon.js9.30.0、Vite 8.3.4、TypeScript 5.9.3、Windows 11 ChromeWebGL2 后端。本章代码src/chapters/ch39.ts运行后打开http://localhost:18000/?ch39。图中所有源码出处都指node_modules/babylonjs/core里的真实文件与行号你可以自己打开核对。39.1 本章要做成什么样跑起来之后你应该看到一个概念演示场景“基础演示网格”场景中央有一个紫色球体和地面用于展示标准渲染效果作为对比基准。HUD 说明面板左上角显示 FrameGraph 的核心概念和架构图解什么是 Render Graph有向无环图 DAG节点式渲染管线的组成要素与 DefaultRenderingPipeline 的对比表格典型应用场景列举性能代价分析每个 pass 的成本架构示意图右侧显示简化的 Render Graph 结构图文字描述展示节点之间的依赖关系。实验性 API 警告明确标注 RenderGraph API 在 v9 仍为实验性生产环境建议使用 DefaultRenderingPipeline。如果你的画面和上面描述有任何一条不符先别往下读直接跳到 39.8 的自查清单。39.2 原理一什么是 Render Graph39.2.1 从线性管线到图结构在第 38 章我们学习了DefaultRenderingPipeline它是一个固定顺序的后期处理链几何渲染 → Bloom → ImageProcessing → Grain → Vignette → ...这种线性结构简单易用但缺乏灵活性无法跳过中间某个步骤无法插入自定义效果无法根据条件动态启用/禁用某些 passRender Graph渲染图将渲染流程表示为有向无环图DAG, Directed Acyclic Graph节点Node一个渲染 pass如阴影计算、光照、后期特效边Edge资源依赖关系如这个 pass 需要上一个 pass 输出的纹理无环保证执行顺序不会出现循环依赖类比想象一个厨房做菜流程。线性管线像固定菜谱洗菜 → 切菜 → 炒菜 → 装盘不能跳步。Render Graph 像自由组合的烹饪站冷盘站洗菜 → 切菜 → 装盘不需要炒热菜站洗菜 → 切菜 → 炒 → 装盘汤品站洗菜 → 煮 → 装盘不需要切和炒厨师可以根据订单动态选择路径。这个比喻到这就断了真实厨房可以多人并行做菜而 Render Graph 的执行仍然是串行的GPU 按拓扑排序依次执行节点只是逻辑上允许分支和合并。39.2.2 为什么需要 Render Graph场景 1非真实感渲染NPR你想实现卡通风格需要正常渲染几何体提取轮廓线Sobel 边缘检测量化颜色减少色阶叠加轮廓线和量化后的颜色这种自定义流程无法用 DefaultRenderingPipeline 实现因为它的节点是固定的。场景 2VR 多视图优化VR 需要同时渲染左眼和右眼两个视角。传统方式是渲染两次但通过 Render Graph 可以共享几何阶段顶点变换只算一次分叉为左右两个视口渲染合并输出到立体纹理节省 30-40% 的 GPU 时间。场景 3条件渲染根据玩家设置动态调整低画质跳过 SSR 和 SSAO2中画质开启 SSAO2高画质开启 SSR SSAO2 DOFRender Graph 可以在运行时增删节点而 DefaultRenderingPipeline 只能启用/禁用已有节点但仍会遍历。39.3 原理二Babylon.js 的 FrameGraph API旧称 RenderGraph39.3.1 API 状态说明重要截至 Babylon.js 9.30.0FrameGraph API 仍处于实验性阶段文档不完整API 可能在未来版本变更部分功能仅在 WebGPU 后端可用生产环境建议使用DefaultRenderingPipeline第 38 章或PostProcess链手动串联。本章主要讲解概念和架构帮助你理解未来正式版的用法。39.3.2 核心概念v9.30.0 里这一层叫FrameGraph早期资料叫 RenderGraph包里已经没有RenderGraph/目录。它不是节点 连线而是任务Task 资源引用任务自己声明输出纹理下游任务引用同一个纹理对象就形成依赖没有addEdge()这种东西。import{FrameGraph,FrameGraphShadowGeneratorTask}frombabylonjs/core// 1. 创建第一个参数是 scene不是名字constgraphnewFrameGraph(scene)// 2. 加任务对象依赖靠资源引用表达// const shadow new FrameGraphShadowGeneratorTask(shadow, { shadowGenerator })// graph.addTask(shadow)// 3. 执行graph.execute()常见任务node_modules/babylonjs/core/FrameGraph/frameGraph.d.ts:70-155——构造函数、addTask()、execute()都在这个文件任务类作用对应传统管线阶段FrameGraphGeometryRendererTask渲染几何体顶点变换、光栅化前向渲染主体FrameGraphShadowGeneratorTask生成阴影贴图ShadowGeneratorFrameGraphLightingVolumeTask光照体积Clustered Light 前向材质着色FrameGraphBloomTask等Tasks/PostProcesses/全屏后期特效DefaultRenderingPipelineFrameGraphComputeShaderTaskGPU 计算粒子/物理ComputeShaderFrameGraphGlowLayerTask图层类效果GlowLayer类名以包里的export declare class为准逐个能在FrameGraph/Tasks/下面核对。这一层跨版本改名很频繁别照抄名字——先import一下让编译器告诉你有没有。39.3.3 资源管理FrameGraph 的核心优势是显式资源管理纹理不归你 new也不由引擎偷偷建 RTT而是你在record()里登记一份「创建说明」由FrameGraphTextureManager按依赖生命周期分配、复用、回收。import{Constants,FrameGraph,FrameGraphTask}frombabylonjs/coreimporttype{DrawWrapper,FrameGraphTextureCreationOptions,FrameGraphTextureHandle}frombabylonjs/coreconstgraphnewFrameGraph(scene)graph.optimizeTextureAllocationtrue// 默认 true生命周期不重叠的纹理复用同一块显存classMyTaskextendsFrameGraphTask{sourceTexture?:FrameGraphTextureHandle// 上游输出引用即依赖readonlyoutputTexture:FrameGraphTextureHandle myDrawWrapper!:DrawWrapper// 你自己造的全屏绘制包装见 39.4.2constructor(name:string,frameGraph:FrameGraph){super(name,frameGraph)// 先占一个「悬空句柄」纹理本身要等到 record 才落地this.outputTextureframeGraph.textureManager.createDanglingHandle()}record():void{// 建 pass 只能在 record 期间调用frameGraph.d.ts:116-121 写明了这条限制。// 这里必须用 addRenderPass 而不是 addPassaddPass 返回 FrameGraphPassFrameGraphContext// 它的 context 没有 applyFullScreenEffect只有 addRenderPass 返回的 FrameGraphRenderPass// 才把 context 参数化成 FrameGraphRenderContextPasses/renderPass.d.ts:17constpassthis._frameGraph.addRenderPass(my-pass)this._frameGraph.textureManager.resolveDanglingHandle(this.outputTexture,undefined,my-rt,{size:{width:100,height:100},// sizeIsPercentagetrue 时是屏幕百分比sizeIsPercentage:true,options:{formats:[Constants.TEXTUREFORMAT_RGBA],// RED / RG / RGB / RGBA...types:[Constants.TEXTURETYPE_UNSIGNED_BYTE],// UNSIGNED_BYTE / FLOAT / HALF_FLOAT...createMipMaps:false,samples:1,},}satisfies FrameGraphTextureCreationOptions)pass.setExecuteFunc((context){// context 是 FrameGraphRenderContext绑纹理用 bindTextureHandle(effect, 名, handle)// 画全屏用一个 DrawWrapper里面装你自己的 EffectWrapper见 39.4.2context.applyFullScreenEffect(this.myDrawWrapper)})}}深度纹理没有TextureFormat.DEPTH24这个东西格式常量挂在Constants上能用的是Constants.TEXTUREFORMAT_DEPTH24_STENCIL8Engines/constants.d.ts:215值 13和TEXTUREFORMAT_DEPTH32_FLOAT:217值 14。要「声明节点输入输出」也没有setInput/setOutput——输入就是把上游的 handle 赋给你的sourceTexture字段或addDependencies(handle)输出就是resolveDanglingHandle落地的那个 handle渲染目标本身用context.createRenderTarget(name, 颜色句柄, 深度句柄)包一层FrameGraph/frameGraphRenderContext.d.ts:44。对比传统方式传统Babylon 自动管理 RTTRender To Texture开发者不知道中间纹理的存在Render Graph显式声明所有资源便于优化和调试39.4 原理三自定义 Pass 插入39.4.1 什么是 Custom PassCustom Pass 允许你插入完全自定义的着色器代码实现特殊效果故障艺术Glitch随机偏移像素扫描线模拟老式 CRT 显示器水彩风格边缘模糊 颜色扩散热力图根据深度值映射颜色39.4.2 创建 Custom Pass// v9.30.0 中自定义渲染通过 PostProcess 或 ComputeShader 实现import{PostProcess}frombabylonjs/coreconstglitchPostProcessnewPostProcess(glitch-effect,./shaders/glitch,// 着色器文件路径[time,glitchIntensity],// uniform 参数null,// samplers1.0,// options: scalecamera,Texture.BILINEAR_SAMPLINGMODE)// 在渲染循环中更新 uniformPostProcess 自己没有 setFloat/setVector// uniform 是在「每次应用之前」通过 onApplyObservable 拿到 Effect 再设的glitchPostProcess.onApplyObservable.add((effect){effect.setFloat(time,performance.now()/1000)effect.setFloat(glitchIntensity,0.02)})关键点parameters从 CPU 传递的普通 uniform 名时间、强度等构造第三个参数samplerssampler2D的 uniform 名通常是上一个节点的输出纹理构造第四个参数参数顺序别记反第三个是parameters、第四个才是samplersPostProcesses/postProcess.pure.d.ts:355要走 FrameGraph 那条路对应的任务是FrameGraphCustomPostProcessTaskFrameGraph/Tasks/PostProcesses/customPostProcessTask.d.ts:8-24构造第三个参数收EffectWrapperCreationOptions它的postProcess是ThinCustomPostProcess而且同样给了一个onApplyObservable: ObservableEffect——注释原文是 “Use this to set custom uniforms”。39.4.3 动态参数更新setUniform(time, ...)这种一步到位的 setter 在包里不存在PostProcess上没有任务上也没有。uniform 只能在这次效果被应用之前、拿到Effect之后设置所以是观察者而不是每帧循环// PostProcess 路线glitchPostProcess.onApplyObservable.add((effect){effect.setFloat(time,performance.now()/1000)effect.setFloat(glitchIntensity,0.02)})// FrameGraph 路线同一个口子挂在任务上constglitchTasknewFrameGraphCustomPostProcessTask(glitch,graph,{fragmentShader:shaderSource,// 字段名是 fragmentShader不是 fragmentuniforms:[time,glitchIntensity],// uniformNames 的别名samplers 同理effectRenderer.pure.d.ts:140-152samplers:[textureSampler],})glitchTask.onApplyObservable.add((effect){effect.setFloat(time,performance.now()/1000)})// 依赖接上去这个任务读上游的输出写自己的输出glitchTask.sourceTexturesceneRenderer.outputTexture graph.addTask(glitchTask)39.5 原理四与 DefaultRenderingPipeline 对比39.5.1 功能对比表特性DefaultRenderingPipelineRender Graph易用性⭐⭐⭐⭐⭐ 开箱即用⭐⭐ 需手动配置灵活性⭐⭐ 固定节点⭐⭐⭐⭐⭐ 完全自定义性能优化⭐⭐⭐ 自动批处理⭐⭐⭐⭐⭐ 显式资源管理学习曲线低10 分钟上手高需理解渲染管线适用场景快速原型、通用项目定制引擎、特殊效果API 稳定性稳定v5 未大改实验性v9文档完善度完整稀疏39.5.2 何时选择 Render Graph推荐使用 Render Graph 的情况你需要非标准渲染流程如水彩/素描风格你在开发游戏引擎框架需要暴露渲染接口给上层你需要极致的性能优化手动控制资源生命周期你的目标平台支持 WebGPURender Graph 在 WebGPU 下性能优势更明显推荐使用 DefaultRenderingPipeline 的情况你做产品展示、数据可视化、常规 3D 应用你只需要标准的后期效果bloom、SSAO、DOF你的团队没有图形程序员你需要快速交付 MVP39.5.3 混合使用策略想「DRP 打底 自己插一个效果」正确的接缝不是纹理而是相机上的后期数组——camera.attachPostProcess(postProcess, insertAt?)Cameras/camera.pure.d.ts:429返回插入位置对称的还有detachPostProcess:435// 1. 创建默认管线DRP 内部自己接管相机的后期链constpipelinenewDefaultRenderingPipeline(pipeline,true,scene,[camera])pipeline.bloomEnabledtrue// 2. 自己的效果做成 PostProcess插在链上的指定位置constglitchnewPostProcess(glitch,./shaders/glitch,[time],null,0.5,camera)glitch.onApplyObservable.add((effect)effect.setFloat(time,performance.now()/1000))// insertAt 传 null/省略 追加到链尾想插到 bloom 前面就显式给下标constatcamera.attachPostProcess(glitch,null)console.log(glitch 挂在相机后期链的第${at}位)至于 FrameGraph 和 DRP 混用——没有公开接缝可走。DRP 上既没有getOutputTexture()也不暴露它内部那张FrameGraphPostProcesses/RenderPipeline/下的.d.ts里搜不到frameGraph这个成员而 FrameGraph 那边的graph.addCustomPass/graph.setFinalOutput同样不存在FrameGraph只有addTask/addPass/addRenderPass/addObjectListPass/buildAsync/execute/clear/dispose见FrameGraph/frameGraph.d.ts:104-178。真要同屏共存只能让它们作用于不同的相机各画各的。39.6 最小可运行十行由于 Render Graph API 在 v9 仍是实验性且文档不完整这里提供一个概念性最小示例基于当前可用 API 的近似实现import{Engine,ArcRotateCamera,HemisphericLight,MeshBuilder,PBRMaterial,Color3,Scene,Vector3,PostProcess}frombabylonjs/coreconstcanvasdocument.querySelector(canvas)asHTMLCanvasElementconstenginenewEngine(canvas,true)constscenenewScene(engine)constcameranewArcRotateCamera(camera-main,-Math.PI/2,Math.PI/2.5,5,Vector3.Zero(),scene)camera.attachControl()// 第 5 章四件套之一没有光源时 PBR 的漫反射会算成纯黑newHemisphericLight(light-hemi,newVector3(0,1,0),scene)constsphereMeshBuilder.CreateSphere(mesh-sphere,{diameter:1},scene)constmatnewPBRMaterial(mat-sphere,scene)mat.albedoColornewColor3(0.6,0.3,0.8)mat.metallic0// v9 默认是 null不显式赋值的 PBR 会渲成纯黑mat.roughness0.5sphere.materialmat MeshBuilder.CreateGround(mesh-ground,{width:8,height:8},scene)// 手动串联 PostProcess模拟 Render Graph 的节点连接// 第三个参数是普通 uniform 名第四个才是 sampler 名反色效果只需要读上游那张图constinvertPPnewPostProcess(invert,./invert,null,[textureSampler],1.0,camera)// 注意实际使用时需要编写 invert.fragment.fx 着色器文件engine.runRenderLoop(()scene.render())说明FrameGraph 在 v9.30.0 是能用的就在babylonjs/core/FrameGraph/任务类齐全但它是实验性的文档薄、跨版本改名频繁写生产不如先用PostProcess链或DefaultRenderingPipeline。39.7 逐步加厚从概念到实践这一节请照着敲、并且真的把每一步跑一遍。步 0只建基础场景constsphereMeshBuilder.CreateSphere(mesh-sphere,{diameter:1},scene)constmatnewPBRMaterial(mat-sphere,scene)mat.albedoColornewColor3(0.6,0.3,0.8)mat.metallic0// v9 默认是 null不显式赋值的 PBR 会渲成纯黑mat.roughness0.5sphere.materialmat MeshBuilder.CreateGround(mesh-ground,{width:8,height:8},scene)打开页面看到普通紫色球体。步 1理解节点概念阅读 HUD 中的架构图解理解以下概念每个渲染阶段是一个节点节点之间通过纹理资源连接执行顺序由依赖关系决定拓扑排序步 2对比两种管线在 Inspector 中观察启用 DefaultRenderingPipeline 时Post Processes 面板显示固定节点列表尝试手动添加 PostProcess发现它会自动插入到管线末尾这说明 DefaultRenderingPipeline 内部也是用类似图的結構管理节点只是对外暴露的是线性 API。步 3研究性能代价打开 Chrome DevTools → Performance 标签录制一帧操作GPU 时间增加启用 Bloom1.2ms启用 SSAO23.5ms启用 SSR5.8ms添加自定义 PostProcess0.8-2.0ms取决于复杂度结论每个额外 pass 至少增加 0.5ms GPU 时间。移动端建议总后期 pass ≤ 3 个。步 4本章终态到这里你就写出了src/chapters/ch39.ts的全部内容。它主要是一个概念演示包含基础场景作为视觉参考详细的 HUD 说明 Render Graph 架构与 DefaultRenderingPipeline 的对比表格性能代价数据和优化建议39.8 预期输出与自查成功标志现象说明看到紫色球体和地面基础场景渲染正常HUD 显示 Render Graph 概念说明文本内容加载正确能理解节点/边/DAG 的概念理论学习到位知道何时该用 DefaultRenderingPipeline实践判断力形成了解每个 pass 的性能代价性能意识建立失败排查现象可能原因修复HUD 文字不显示忘记调用 describe()检查章节代码不理解 DAG 概念缺少图论基础复习离散数学或有向图知识想立即用 Render Graph 做项目API 未稳定改用 DefaultRenderingPipeline混淆 PostProcess 和 Render Graph概念不清PostProcess 是单个节点Render Graph 是整个图39.9 坑与出处39-01RenderGraph API 在 v9 不可用版本9.30.0报错原文Module babylonjs/core has no exported member RenderGraph复现步骤尝试import { RenderGraph } from babylonjs/coreTypeScript 编译现象编译错误找不到该类。原因RenderGraph 仍在开发中未公开发布。修复方案 1使用DefaultRenderingPipeline第 38 章方案 2手动串联PostProcess对象方案 3关注 Babylon.js GitHub issue #13xxx 追踪进展源码出处GitHub BabylonJS/Babylon.js 仓库 —— 搜索 “RenderGraph” 相关的 PR 和 Issue。39-02自定义 PostProcess 需要单独着色器文件版本9.30.0报错原文Error: Shader not found: invert复现步骤new PostProcess(invert, ./invert, ...)项目中没有invert.fragment.fx文件现象运行时崩溃。原因PostProcess 需要从文件加载着色器代码。修复方案 1创建shaders/invert.fragment.fx文件方案 2使用Effect.ShadersStore[invertFragment]内联注册见第 40 章源码出处node_modules/babylonjs/core/Engines/shaderStore.js:11-27——按shaderLanguage决定去ShadersStore还是ShadersStoreWGSL取源码取不到才回落到ShadersRepository拼 URL 下载于是!doctype html会被当着色器编译。39-03多个 PostProcess 串联时顺序很重要版本9.30.0报错原文无报错但效果异常复现步骤先添加 Bloom PostProcess再添加色调映射 PostProcess现象色调映射失效。原因PostProcess 按添加顺序执行某些效果必须在其他效果之前/之后。修复// 正确顺序先色调映射再 BloomconsttoneMapPPnewPostProcess(tonemap,...)constbloomPPnewPostProcess(bloom,...)通用规则颜色校正曝光/对比度→ 最早空间效果Bloom/SSAO→ 中间全屏扭曲色差/故障→ 最后源码出处node_modules/babylonjs/core/Cameras/camera.pure.d.ts:289_postProcesses: NullablePostProcess[]——postProcesses 数组顺序说明。39-04WebGL1 不支持浮点纹理版本9.30.0报错原文WebGL warning: texImage2D: Internal format not supported复现步骤强制 WebGL1 后端创建需要 HDR 纹理的 PostProcess现象黑屏或报错。原因浮点纹理RGBA16F/RGBA32F需要 WebGL2 或扩展。修复确保使用 WebGL2new Engine(canvas, true)第二个参数为 true 启用 WebGL2。源码出处node_modules/babylonjs/core/Engines/engine.d.ts:100-110——WebGL 版本检测。39.10 章节挑战任务手动实现一个双通道后期处理链。要求创建一个包含多个物体的场景球体、立方体、地面手动串联三个 PostProcess第一个灰度化将彩色转为黑白第二个反相颜色取反第三个亮度调整乘以系数每个 PostProcess 使用内联着色器代码通过Effect.ShadersStore注册添加交互按 G 键开关灰度化按 I 键开关反相鼠标滚轮调整亮度系数0.5-2.0用 HUD 显示当前启用的效果和亮度值提示灰度化公式gray 0.299 * R 0.587 * G 0.114 * B反相公式color 1.0 - color参考第 40 章的着色器写法参考答案见附录 F。39.11 本章小结本章你学会了Render Graph 概念有向无环图、节点、边、拓扑排序Babylon.js RenderGraph API 状态实验性、未来方向、当前限制自定义 Pass 插入Custom Pass 的着色器结构和参数传递与 DefaultRenderingPipeline 对比易用性 vs 灵活性的权衡性能代价分析每个 pass 的 GPU 时间成本移动端优化策略虽然 Render Graph API 尚未完全成熟但理解其概念有助于你更好地使用 DefaultRenderingPipeline知道内部如何工作为未来 API 稳定做准备在其他引擎Unity/Unreal中迁移知识下一章将带你进入着色器编程的世界学习如何手写 GLSL/WGSL 代码实现自定义材质效果。术语卡中文English一句话解释渲染图Render Graph用图结构表示渲染流程的数据结构有向无环图DAG (Directed Acyclic Graph)边有方向且无循环的图节点Node渲染图中的一个处理单元pass边Edge节点之间的资源依赖关系拓扑排序Topological Sort确定 DAG 执行顺序的算法渲染通道Render PassGPU 执行的一次完整绘制操作资源管理Resource Management纹理/缓冲区等 GPU 资源的分配和释放实验性 APIExperimental API尚未稳定、可能变更的接口自定义着色器Custom Shader手写的 GLSL/WGSL 代码后处理链Post Process Chain多个全屏特效的顺序执行