如何在 Cesium Scene.js 中插入自定义 Pass
为了实现漂亮的天气,需要在 Cesium 的渲染管线中插入自己的 Pass。本文使用的 Cesium 版本是1.144,修改源码后需要重新npx gulp build才能生效。
目标
在packages/engine/Source/Scene/Scene.js的resolveFramebuffers流程中,插入一个自定义 Pass。典型执行时机是后处理之前,这样既能拿到当前帧的场景颜色,又能避免被后处理重复影响。
自定义 Pass 的接口
一个自定义 Pass 至少需要三个生命周期方法:
constmyPass={initialize:asyncfunction(){/* 创建 shader/framebuffer/texture */},destroy:function(){/* 释放资源 */},execute:function(context,passState,frameState,sceneTexture){/* 每帧执行 */},};initialize负责异步准备资源,execute直接拿到当前场景颜色纹理sceneTexture进行绘制。
步骤 1:创建 WeatherPass.js
在packages/engine/Source/Scene/下新建WeatherPass.js:
importBoundingRectanglefrom"../Core/BoundingRectangle.js";importColorfrom"../Core/Color.js";importdefinedfrom"../Core/defined.js";importdestroyObjectfrom"../Core/destroyObject.js";importDrawCommandfrom"../Renderer/DrawCommand.js";importPassfrom"../Renderer/Pass.js";importRenderStatefrom"../Renderer/RenderState.js";importShaderProgramfrom"../Renderer/ShaderProgram.js";importShaderSourcefrom"../Renderer/ShaderSource.js";importVertexArrayfrom"../Renderer/VertexArray.js";importBlendingStatefrom"../Scene/BlendingState.js";constvertexShaderSource=`in vec2 position; in vec2 st; out vec2 v_textureCoordinates; void main() { gl_Position = vec4(position, 0.0, 1.0); v_textureCoordinates = st; }`;constfragmentShaderSource=`in vec2 v_textureCoordinates; uniform sampler2D sceneTexture; out vec4 out_FragColor; void main() { vec3 color = texture(sceneTexture, v_textureCoordinates).rgb; // 这里可以叠加天气效果:雨丝、雾气、体积云合成等 // 示例:做一个简单的蓝色雾效叠加 float fog = 1.0 - v_textureCoordinates.y; color = mix(color, vec3(0.7, 0.8, 0.95), fog * 0.1); out_FragColor = vec4(color, 1.0); }`;functioncreateFullscreenQuad(context){constpositions=newFloat32Array([-1.0,-1.0,0.0,0.0,1.0,-1.0,1.0,0.0,1.0,1.0,1.0,1.0,-1.0,1.0,0.0,1.0,]);constindices=newUint16Array([0,1,2,0,2,3]);returnVertexArray.fromGeometry({context:context,geometry:{attributes:{position:{componentDatatype:ComponentDatatype.FLOAT,componentsPerAttribute:2,values:positions,},st:{componentDatatype:ComponentDatatype.FLOAT,componentsPerAttribute:2,values:positions,},},indices:indices,primitiveType:PrimitiveType.TRIANGLES,boundingSphere:newBoundingSphere(Cartesian3.ZERO,1.0),},attributeLocations:{position:0,st:1,},});}functionWeatherPass(context){this._context=context;this._va=undefined;this._shaderProgram=undefined;this._drawCommand=undefined;}WeatherPass.prototype.initialize=asyncfunction(){constcontext=this._context;this._va=createFullscreenQuad(context);this._shaderProgram=ShaderProgram.fromCache({context:context,vertexShaderSource:newShaderSource({sources:[vertexShaderSource]}),fragmentShaderSource:newShaderSource({sources:[fragmentShaderSource]}),attributeLocations:{position:0,st:1,},});constthat=this;this._drawCommand=newDrawCommand({primitiveType:PrimitiveType.TRIANGLES,vertexArray:this._va,shaderProgram:this._shaderProgram,uniformMap:{sceneTexture:function(){returnthat._sceneTexture;},},renderState:RenderState.fromCache({blending:BlendingState.Alpha_BLEND,depthTest:{enabled:false,},}),pass:Pass.OPAQUE,owner:this,boundingVolume:newBoundingSphere(Cartesian3.ZERO,1.0),});};WeatherPass.prototype.execute=function(context,passState,frameState,sceneTexture,){this._sceneTexture=sceneTexture;// 直接绘制到调用方设置的 passState.framebuffer 上this._drawCommand.execute(context,passState);};WeatherPass.prototype.isDestroyed=function(){returnfalse;};WeatherPass.prototype.destroy=function(){this._va=this._va&&this._va.destroy();this._shaderProgram=this._shaderProgram&&this._shaderProgram.destroy();returndestroyObject(this);};exportdefaultWeatherPass;注意:上面的代码片段为了简洁省略了部分 import(如
ComponentDatatype、PrimitiveType、BoundingSphere、Cartesian3),实际文件里需要从对应模块引入。
步骤 2:在 View.js 中初始化和销毁
打开packages/engine/Source/Scene/View.js,在构造函数里创建 Pass 实例:
importWeatherPassfrom"./WeatherPass.js";functionView(scene,camera,context){// ... 已有代码 ...letweatherPass;if(scene._useWeather!==false){// 你可以根据 scene 的开关决定是否启用weatherPass=newWeatherPass(context);weatherPass.initialize();// async,这里不 await;如果资源依赖异步加载可改为 await}// ...this.weatherPass=weatherPass;}在View.prototype.destroy里释放:
View.prototype.destroy=function(){// ... 已有销毁代码 ...this.weatherPass=this.weatherPass&&this.weatherPass.destroy();returndestroyObject(this);};也可以直接放在 Scene.js 里吗?
可以,但不推荐。技术上完全可以在Scene构造函数里直接this._weatherPass = new WeatherPass(this._context),在destroy里释放,在resolveFramebuffers里直接this._weatherPass.execute(...)。
但 Cesium 的架构是Scene负责调度,View负责持有渲染资源(sceneFramebuffer、globeDepth、postProcessStages等都在View.js里)。把自定义 Pass 也放View.js,生命周期和资源管理更一致,后续升级或维护也更清晰。
除非这个 Pass 是全局单例、不依赖具体 View,否则建议跟随View创建和销毁。
步骤 3:在 Scene.js 的 resolveFramebuffers 中插入执行点
打开packages/engine/Source/Scene/Scene.js,找到Scene.prototype.resolveFramebuffers:
Scene.prototype.resolveFramebuffers=function(passState){constcontext=this._context;constenvironmentState=this._environmentState;constview=this._view;const{globeDepth,translucentTileClassification}=view;if(defined(globeDepth)){globeDepth.prepareColorTextures(context);}...// 自定义 Weather Pass:插入点在这里if(defined(view.weatherPass)){// 输出目标:如果用后处理,写到 sceneFramebuffer;否则写回 originalFramebufferpassState.framebuffer=usePostProcess?sceneFramebuffer.framebuffer:originalFramebuffer;constsceneTexture=sceneFramebuffer.getColorTexture(0);view.weatherPass.execute(context,passState,this._frameState,sceneTexture,);}// 后处理if(usePostProcess){view.sceneFramebuffer.prepareColorTextures(context);letinputFramebuffer=sceneFramebuffer;if(useGlobeDepthFramebuffer){inputFramebuffer=globeFramebuffer;}constpostProcess=this.postProcessStages;constcolorTexture=inputFramebuffer.getColorTexture(0);constidTexture=idFramebuffer.getColorTexture(0);constdepthTexture=(globeFramebuffer??sceneFramebuffer).getDepthStencilTexture();postProcess.execute(context,colorTexture,depthTexture,idTexture);postProcess.copy(context,originalFramebuffer);}// 4. 无后处理、但有 globe depth 时,复制颜色回 originalFramebufferif(!usePostProcess&&useGlobeDepthFramebuffer){passState.framebuffer=originalFramebuffer;globeDepth.executeCopyColor(context,passState);}};这样天气 Pass 就在后处理之前执行,并且会把结果直接写进当前目标 framebuffer。
关键注意点
1. WebGL State 保存与恢复
自定义 Pass 如果直接用原生 WebGL,必须保存/恢复 Cesium 的 WebGL state:
constpreviousFramebuffer=passState.framebuffer;constpreviousViewport=passState.viewport;// ...passState.framebuffer=previousFramebuffer;passState.viewport=previousViewport;如果是通过DrawCommand执行(如上例),Cesium 会帮你管理大部分 state。
2. HDR 空间
如果scene.highDynamicRange === true,sceneTexture是浮点/半浮点纹理,值可能超过 1.0。天气效果应该在线性/HDR 空间计算,不要内部做 gamma 校正或 clamp 到[0, 1],让 Cesium 的 tonemapping 后处理去处理。
3. Depth Texture
如果需要场景深度(例如体积云和地形正确相交),可以从globeFramebuffer或sceneFramebuffer拿 depth texture:
constdepthTexture=(globeFramebuffer??sceneFramebuffer).getDepthStencilTexture();然后作为 uniform 传进 shader。
4. 开关控制
建议在Scene构造函数里加一个开关:
functionScene(options){// ...this._useWeather=options.useWeather??true;// ...}这样用户可以按需启用/禁用自定义 Pass。
总结
在 Cesium 1.144 中插入自定义 Pass 的标准流程是:
- 实现 Pass 类:提供
initialize/execute/destroy三个方法。 - 在
View.js中创建实例:跟随场景生命周期初始化和销毁(也可以直接放Scene.js,但推荐View.js)。 - 在
Scene.resolveFramebuffers中插入执行点:选择合适时机,通常是在后处理之前。 - 注意 state、HDR 和 depth:避免破坏 Cesium 的渲染管线。
- 注意修改viewport可能需要你自己调用glViewport来更新。
修改后记得重新构建:
npx gulp build然后启动开发服务器验证效果:
nodeserver.js