Cesium CustomShader实战:为3DTiles模型动态更换纹理与风格

张开发
2026/6/14 13:38:55 15 分钟阅读
Cesium CustomShader实战:为3DTiles模型动态更换纹理与风格
1. 为什么需要动态更换3DTiles模型纹理第一次接触Cesium的3DTiles模型渲染时很多开发者都会遇到一个共同的问题加载后的建筑模型外观是固定的想要更换墙面材质或者调整屋顶颜色只能重新生成整个3DTiles数据集。这在实际项目中简直是个噩梦——每次修改都要经历漫长的数据转换流程效率低得让人抓狂。我去年参与一个智慧园区项目时就踩过这个坑。客户要求在系统里实现建筑外观的实时切换功能从现代玻璃幕墙到传统砖墙风格要有十几种预设。最初尝试用传统方法每次切换都要重新生成3DTiles不仅耗时长达数小时还经常出现模型接缝问题。直到发现CustomShader这个神器才真正解决了动态换装的难题。传统3DTiles渲染流程的局限主要体现在三个方面首先是材质固定模型在生成时就已经烘焙了所有纹理信息其次是缺乏交互性无法根据运行时条件动态调整外观最后是性能消耗大任何视觉调整都需要全量数据更新。而CustomShader恰好能突破这些限制它允许我们在不修改原始数据的前提下通过着色器编程实时操控模型外观。2. CustomShader的工作原理与优势理解CustomShader之前得先明白Cesium的渲染管线如何处理3DTiles。与普通Primitive不同3DTiles采用了一种特殊的渲染机制——每个瓦片都是独立的绘制单元但共享统一的渲染状态。这就导致我们无法像处理普通模型那样直接附加材质对象。CustomShader的巧妙之处在于它在渲染管线的最后阶段介入。具体来说当Cesium完成3DTiles的常规渲染后我们的自定义着色器会接管后续处理。这个过程类似于给渲染流水线加了个后处理滤镜但比真正的后处理更精细因为它能访问到每个像素的原始几何信息。从技术架构看CustomShader主要包含三个核心组件Uniform变量外部传入的常量参数比如我们要切换的纹理图片Varying变量在顶点着色器和片元着色器之间传递的数据着色器代码包含vertexShader和fragmentShader两段GLSL程序实测下来这种方案的性能损耗几乎可以忽略不计。我在一台中配开发机上测试同时对500栋建筑应用动态纹理切换帧率仍能保持在60fps以上。这是因为所有计算都在GPU上并行完成CPU只需要负责最基础的调度工作。3. 实现基础纹理替换让我们从一个最简单的例子开始给建筑模型整体更换墙面纹理。首先需要准备着色器程序的基本结构const customShader new Cesium.CustomShader({ uniforms: { u_texture: { value: new Cesium.TextureUniform({ url: textures/brick_wall.jpg }), type: Cesium.UniformType.SAMPLER_2D } }, vertexShaderText: void vertexMain(VertexInput vsInput, inout czm_modelVertexOutput vsOutput) { // 暂时不需要特殊处理 } , fragmentShaderText: void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) { vec2 uv fsInput.attributes.texCoord_0; // 获取原始UV坐标 vec3 rgb texture2D(u_texture, uv).rgb; material.diffuse rgb; } }); tileset.customShader customShader;这段代码虽然简单但有几个关键点需要注意纹理坐标处理大多数3DTiles模型会自带UV坐标通常存储在texCoord_0属性中直接使用这些坐标能确保纹理正确映射材质覆盖我们修改的是material.diffuse属性这会完全替换模型原有的基础颜色资源加载TextureUniform会自动处理图片的异步加载无需手动管理资源生命周期当需要动态更换纹理时只需要更新uniform的值customShader.uniforms.u_texture.value new Cesium.TextureUniform({ url: textures/concrete_wall.jpg });4. 基于几何特征的差异化贴图实际项目中我们往往需要对建筑的不同部位应用不同纹理。比如屋顶用瓦片材质墙面用砖纹这就需要根据几何特征进行条件判断。下面这段代码展示了如何通过法向量识别屋顶fragmentShaderText: void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) { vec3 normal normalize(fsInput.attributes.normalMC); vec2 uv fsInput.attributes.texCoord_0; // 法向量朝上视为屋顶Y轴向上坐标系 if (dot(vec3(0.0, 1.0, 0.0), normal) 0.9) { material.diffuse texture2D(u_roofTexture, uv).rgb; } else { // 墙面使用平铺纹理 vec2 tiledUV vec2( mod(fsInput.attributes.positionMC.x, 20.0) / 20.0, mod(fsInput.attributes.positionMC.y, 10.0) / 10.0 ); material.diffuse texture2D(u_wallTexture, tiledUV).rgb; } } 这里有几个实用技巧法向量判断通过dot乘积计算表面朝向值越接近1表示越平行平铺纹理使用模型世界坐标的模运算实现无缝平铺效果坐标系注意Cesium使用Y轴向上的右手坐标系判断时要对应调整我在一个商业综合体项目中应用这个方案时发现某些斜屋顶会被误判为墙面。后来通过调整阈值和增加边缘平滑处理解决了这个问题float roofFactor smoothstep(0.85, 0.95, dot(vec3(0.0, 1.0, 0.0), normal)); material.diffuse mix( texture2D(u_wallTexture, uv).rgb, texture2D(u_roofTexture, uv).rgb, roofFactor );5. 实现动态风格切换系统有了前面的基础我们可以构建一个完整的建筑风格切换系统。首先定义包含所有纹理配置的风格预设const styles { modern: { wall: textures/glass_curtain.jpg, roof: textures/metal_roof.jpg, window: textures/window_grid.png }, classical: { wall: textures/stone_wall.jpg, roof: textures/tile_roof.jpg, window: textures/classic_window.jpg } };然后改造CustomShader以支持多纹理混合const customShader new Cesium.CustomShader({ uniforms: { u_style: { type: Cesium.UniformType.FLOAT, value: 0.0 }, // 0modern, 1classical u_wallTextures: { value: [ new Cesium.TextureUniform({ url: styles.modern.wall }), new Cesium.TextureUniform({ url: styles.classical.wall }) ], type: Cesium.UniformType.SAMPLER_2D_ARRAY }, // 类似定义屋顶和窗户纹理数组 }, fragmentShaderText: uniform sampler2DArray u_wallTextures; uniform float u_style; void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) { float styleMix clamp(u_style, 0.0, 1.0); vec3 normal normalize(fsInput.attributes.normalMC); if (isRoof(normal)) { vec3 tex1 texture2D(u_roofTextures[0], uv).rgb; vec3 tex2 texture2D(u_roofTextures[1], uv).rgb; material.diffuse mix(tex1, tex2, styleMix); } else { // 类似处理墙面和窗户 } } });切换风格时只需调整u_style的值可以实现平滑过渡效果function setStyle(styleName) { const targetValue styleName modern ? 0.0 : 1.0; Cesium.Tween.stopAll(); new Cesium.Tween({ value: customShader.uniforms.u_style.value }) .to({ value: targetValue }, 1000) .onUpdate((result) { customShader.uniforms.u_style.value result.value; }) .start(); }6. 高级技巧与性能优化当处理大规模城市模型时性能优化变得尤为重要。以下是几个实战中总结的经验纹理压缩使用KTX2等压缩纹理格式可以显著减少内存占用。Cesium原生支持KTX2可以通过以下方式加载new Cesium.TextureUniform({ url: textures/brick_wall.ktx2, format: Cesium.TextureFormat.RGBA, sampler: new Cesium.Sampler({ minificationFilter: Cesium.TextureMinificationFilter.LINEAR_MIPMAP_LINEAR }) })着色器复杂度控制避免在片元着色器中使用复杂循环或分支。对于需要多重条件判断的情况可以使用**纹理查找表LUT**技术// 预计算不同条件下的混合系数存储到纹理中 vec3 coefficients texture2D(u_conditionLUT, vec2(dotValue, heightRatio)).rgb; material.diffuse coefficients.r * texture2D(u_tex1, uv) coefficients.g * texture2D(u_tex2, uv) coefficients.b * texture2D(u_tex3, uv);实例化渲染对于大量重复的建筑元素如标准化楼栋可以通过自定义实例属性实现批量处理// 在3DTiles生产阶段添加自定义属性 properties: { buildingType: { minimum: 0, maximum: 5, values: [0,1,0,2,...] } } // 在着色器中访问 float type fsInput.attributes.buildingType; if (type 1.0) { // 应用类型1的材质 }7. 常见问题排查在实际开发中CustomShader的使用可能会遇到各种奇怪的问题。这里分享几个典型的踩坑案例纹理不显示最常见的原因是UV坐标获取错误。建议添加调试代码输出坐标值// 临时将UV坐标可视化为颜色 material.diffuse vec3(uv, 0.0);如果看到纯色或规律条纹说明坐标计算有问题。可能的原因是模型本身没有UV坐标检查texCoord_0是否存在坐标系转换错误尝试使用positionMC或positionWC替代性能骤降当发现帧率突然降低时通常是因为纹理尺寸过大超过4096x4096着色器中有高成本操作如sin/cos运算uniform更新过于频繁解决方案包括使用console.time定位性能瓶颈添加#pragma optimize(on)指令启用着色器优化对静态uniform使用cacheKey避免重复编译光照异常当使用PBR材质时自定义着色器可能会破坏原有的光照计算。这时需要确保保留原始材质属性material.diffuse ...; material.roughness 0.5; // 必须显式设置或者直接使用非光照模型lightingModel: Cesium.LightingModel.UNLIT8. 创意应用案例除了常规的建筑换装CustomShader还能实现许多酷炫的效果。最近在一个文旅项目中我们用它实现了以下特色功能季节变化系统通过融合不同季节的纹理配合时间参数实现自动过渡float seasonProgress mod(u_dayOfYear / 365.0, 1.0); vec4 winterTex texture2D(u_winterTexture, uv); vec4 summerTex texture2D(u_summerTexture, uv); if (seasonProgress 0.25 || seasonProgress 0.75) { // 冬季到春季的过渡 float blendFactor abs(seasonProgress - 0.5) * 2.0; material.diffuse mix(summerTex, winterTex, blendFactor); }动态污渍效果根据建筑朝向和高度模拟雨水侵蚀痕迹float dirtFactor 1.0 - dot(vec3(0.0, 1.0, 0.0), normal); dirtFactor * smoothstep(10.0, 30.0, positionWC.y); // 高处更明显 vec3 dirtColor texture2D(u_dirtMask, uv * 5.0).rgb; material.diffuse mix(originalColor, dirtColor, dirtFactor * 0.3);交互高亮结合Cesium的Pick功能实现建筑鼠标悬停效果viewer.screenSpaceEventHandler.setInputAction((movement) { const picked viewer.scene.pick(movement.endPosition); if (picked picked.tileset) { customShader.uniforms.u_highlightId.value picked.batchId; } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);在片元着色器中if (fsInput.batchId u_highlightId) { material.emissive vec3(0.3); // 轻微自发光 material.alpha 0.8; // 半透明效果 }

更多文章