[{"data":1,"prerenderedAt":841},["ShallowReactive",2],{"\u002Fblog\u002Fcesium-water-reflection-implementation":3,"\u002Fblog\u002Fcesium-water-reflection-implementation-surround":834},{"id":4,"title":5,"author":6,"body":10,"date":825,"description":826,"extension":827,"image":828,"meta":829,"minRead":142,"navigation":246,"path":830,"seo":831,"stem":832,"__hash__":833},"blog\u002Fblog\u002Fcesium-water-reflection-implementation.md","Cesium 实时水面镜面反射的实现",{"name":7,"avatar":8},"Kevin",{"src":9,"alt":7},"\u002Favatar.jpg",{"type":11,"value":12,"toc":798},"minimark",[13,17,21,24,27,30,37,43,54,57,60,63,69,72,75,170,173,177,395,406,409,485,500,504,507,515,545,557,583,587,610,631,639,656,664,683,691,707,715,734,737,740,743,781,784,794],[14,15,16],"h2",{"id":16},"这篇文章解决什么问题",[18,19,20],"p",{},"在三维场景中，水面倒影是提升视觉真实感的关键效果之一。和简单的半透明水面不同，镜面反射需要把场景中的建筑、模型在水面上呈现出正确的倒影——倒影的位置、角度要随相机移动实时变化。",[18,22,23],{},"这个效果的核心技术问题是：如何在 Cesium 中实现一个\"虚拟镜面\"，让水面能够实时反映出它上方的场景内容？",[18,25,26],{},"最终实现的方案是：每帧计算一个关于水面对称的反射相机，用这个相机把场景渲染到离屏 Framebuffer，再把渲染结果作为纹理贴到水面 Primitive 上。配合水面法线扰动动画，就能得到带有波纹效果的实时倒影。",[14,28,29],{"id":29},"核心难点",[18,31,32,36],{},[33,34,35],"strong",{},"反射相机的计算","。反射相机不是简单地把主相机翻转，而是要根据水面的法线方向，把相机的位置、视线方向和上方向分别做镜像变换。在球面坐标系下，水面法线不是固定的 (0,1,0)，而是水面中心点的归一化向量，这让反射计算比平面场景复杂得多。",[18,38,39,42],{},[33,40,41],{},"斜裁剪面（Oblique Clipping Plane）","。反射相机渲染时，如果不做裁剪，水面以下的内容也会被渲染进反射纹理，导致穿帮。解决方案是修改反射相机的投影矩阵，把近裁剪面替换为水面平面，这样只有水面以上的内容才会出现在反射纹理中。",[18,44,45,48,49,53],{},[33,46,47],{},"Cesium 渲染管线的介入","。Cesium 不像 Three.js 那样提供公开的多 pass 渲染接口。要实现离屏渲染，必须在 ",[50,51,52],"code",{},"preRender"," 事件中临时替换场景的默认相机，手动驱动 Cesium 的帧状态更新、命令执行和 Framebuffer 解析流程，渲染完成后再恢复原始状态。",[14,55,56],{"id":56},"实现思路",[18,58,59],{},"第一步是建立反射相机。每帧根据主相机的世界坐标，计算其关于水面平面的镜像位置。同时把相机的视线方向和上方向也做镜像变换，确保反射相机\"看到\"的内容和真实倒影一致。",[18,61,62],{},"第二步是构建斜裁剪面投影矩阵。把水面平面变换到反射相机的视图空间，然后用 Eric Lengyel 的斜裁剪面算法修改投影矩阵的第三行，使近裁剪面与水面对齐。",[18,64,65,66,68],{},"第三步是离屏渲染。在 ",[50,67,52],{}," 回调中，临时把场景的默认相机替换为反射相机，隐藏水面 Primitive 本身（避免自反射），然后驱动 Cesium 的完整渲染流程把结果写入自建的 Framebuffer。",[18,70,71],{},"第四步是纹理映射。把离屏渲染得到的颜色纹理传给水面材质的 shader，在顶点着色器中通过反射相机的 ViewProjection 矩阵把世界坐标投影到纹理坐标，片元着色器直接采样得到倒影颜色，再叠加水面法线扰动和高光。",[14,73,74],{"id":74},"技术流程图",[76,77,82],"pre",{"className":78,"code":79,"language":80,"meta":81,"style":81},"language-mermaid shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#1a1b26', 'primaryTextColor': '#a9b1d6', 'primaryBorderColor': '#414868', 'lineColor': '#414868', 'secondaryColor': '#24283b', 'tertiaryColor': '#1a1b26', 'background': '#1a1b26', 'mainBkg': '#24283b', 'nodeBorder': '#414868', 'clusterBkg': '#1a1b26', 'titleColor': '#c0caf5', 'edgeLabelBackground': '#1a1b26' }}}%%\nflowchart TD\n  A([开始：preRender 回调]) --> B[计算反射相机位置和姿态]\n  B --> C{相机在水面上方?}\n  C -->|否| D([结束：跳过本帧])\n  C -->|是| E[构建斜裁剪面投影矩阵]\n  E --> F[替换 defaultView.camera 为反射相机]\n  F --> G[隐藏水面 Primitive \u002F 关闭地球]\n  G --> H[驱动 Cesium 渲染到离屏 Framebuffer]\n  H --> I[context.endFrame 解绑 Framebuffer]\n  I --> J[Texture.fromFramebuffer 创建反射纹理]\n  J --> K[更新 appearance.uniforms 和 material.uniforms]\n  K --> L[恢复场景状态 \u002F 显示水面]\n  L --> M([结束：水面显示倒影])\n","mermaid","",[50,83,84,92,98,104,110,116,122,128,134,140,146,152,158,164],{"__ignoreMap":81},[85,86,89],"span",{"class":87,"line":88},"line",1,[85,90,91],{},"%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#1a1b26', 'primaryTextColor': '#a9b1d6', 'primaryBorderColor': '#414868', 'lineColor': '#414868', 'secondaryColor': '#24283b', 'tertiaryColor': '#1a1b26', 'background': '#1a1b26', 'mainBkg': '#24283b', 'nodeBorder': '#414868', 'clusterBkg': '#1a1b26', 'titleColor': '#c0caf5', 'edgeLabelBackground': '#1a1b26' }}}%%\n",[85,93,95],{"class":87,"line":94},2,[85,96,97],{},"flowchart TD\n",[85,99,101],{"class":87,"line":100},3,[85,102,103],{},"  A([开始：preRender 回调]) --> B[计算反射相机位置和姿态]\n",[85,105,107],{"class":87,"line":106},4,[85,108,109],{},"  B --> C{相机在水面上方?}\n",[85,111,113],{"class":87,"line":112},5,[85,114,115],{},"  C -->|否| D([结束：跳过本帧])\n",[85,117,119],{"class":87,"line":118},6,[85,120,121],{},"  C -->|是| E[构建斜裁剪面投影矩阵]\n",[85,123,125],{"class":87,"line":124},7,[85,126,127],{},"  E --> F[替换 defaultView.camera 为反射相机]\n",[85,129,131],{"class":87,"line":130},8,[85,132,133],{},"  F --> G[隐藏水面 Primitive \u002F 关闭地球]\n",[85,135,137],{"class":87,"line":136},9,[85,138,139],{},"  G --> H[驱动 Cesium 渲染到离屏 Framebuffer]\n",[85,141,143],{"class":87,"line":142},10,[85,144,145],{},"  H --> I[context.endFrame 解绑 Framebuffer]\n",[85,147,149],{"class":87,"line":148},11,[85,150,151],{},"  I --> J[Texture.fromFramebuffer 创建反射纹理]\n",[85,153,155],{"class":87,"line":154},12,[85,156,157],{},"  J --> K[更新 appearance.uniforms 和 material.uniforms]\n",[85,159,161],{"class":87,"line":160},13,[85,162,163],{},"  K --> L[恢复场景状态 \u002F 显示水面]\n",[85,165,167],{"class":87,"line":166},14,[85,168,169],{},"  L --> M([结束：水面显示倒影])\n",[14,171,172],{"id":172},"关键代码解析",[174,175,176],"h3",{"id":176},"反射相机的镜像计算",[76,178,182],{"className":179,"code":180,"language":181,"meta":81,"style":81},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u002F\u002F 获取相机指向水面中心的向量\nlet view = Cesium.Cartesian3.subtract(center, cameraWorldPosition, new Cesium.Cartesian3())\n\n\u002F\u002F 相机在水面背面时跳过\nif (Cesium.Cartesian3.dot(view, normal) > 0) return false\n\n\u002F\u002F 计算反射位置：view 关于法线的反射，再取反方向\nview = reflectVector(Cesium, view, normal)\nCesium.Cartesian3.negate(view, view)\nCesium.Cartesian3.add(view, center, view)\nreflectCamera.position = view.clone()\n","js",[50,183,184,190,242,248,253,296,300,305,328,349,373],{"__ignoreMap":81},[85,185,186],{"class":87,"line":88},[85,187,189],{"class":188},"sHwdD","\u002F\u002F 获取相机指向水面中心的向量\n",[85,191,192,196,200,204,207,210,213,215,219,222,225,228,230,233,235,237,239],{"class":87,"line":94},[85,193,195],{"class":194},"spNyl","let",[85,197,199],{"class":198},"sTEyZ"," view ",[85,201,203],{"class":202},"sMK4o","=",[85,205,206],{"class":198}," Cesium",[85,208,209],{"class":202},".",[85,211,212],{"class":198},"Cartesian3",[85,214,209],{"class":202},[85,216,218],{"class":217},"s2Zo4","subtract",[85,220,221],{"class":198},"(center",[85,223,224],{"class":202},",",[85,226,227],{"class":198}," cameraWorldPosition",[85,229,224],{"class":202},[85,231,232],{"class":202}," new",[85,234,206],{"class":198},[85,236,209],{"class":202},[85,238,212],{"class":217},[85,240,241],{"class":198},"())\n",[85,243,244],{"class":87,"line":100},[85,245,247],{"emptyLinePlaceholder":246},true,"\n",[85,249,250],{"class":87,"line":106},[85,251,252],{"class":188},"\u002F\u002F 相机在水面背面时跳过\n",[85,254,255,259,262,264,266,268,271,274,276,279,282,286,289,292],{"class":87,"line":112},[85,256,258],{"class":257},"s7zQu","if",[85,260,261],{"class":198}," (Cesium",[85,263,209],{"class":202},[85,265,212],{"class":198},[85,267,209],{"class":202},[85,269,270],{"class":217},"dot",[85,272,273],{"class":198},"(view",[85,275,224],{"class":202},[85,277,278],{"class":198}," normal) ",[85,280,281],{"class":202},">",[85,283,285],{"class":284},"sbssI"," 0",[85,287,288],{"class":198},") ",[85,290,291],{"class":257},"return",[85,293,295],{"class":294},"sfNiH"," false\n",[85,297,298],{"class":87,"line":118},[85,299,247],{"emptyLinePlaceholder":246},[85,301,302],{"class":87,"line":124},[85,303,304],{"class":188},"\u002F\u002F 计算反射位置：view 关于法线的反射，再取反方向\n",[85,306,307,310,312,315,318,320,323,325],{"class":87,"line":130},[85,308,309],{"class":198},"view ",[85,311,203],{"class":202},[85,313,314],{"class":217}," reflectVector",[85,316,317],{"class":198},"(Cesium",[85,319,224],{"class":202},[85,321,322],{"class":198}," view",[85,324,224],{"class":202},[85,326,327],{"class":198}," normal)\n",[85,329,330,333,335,337,339,342,344,346],{"class":87,"line":136},[85,331,332],{"class":198},"Cesium",[85,334,209],{"class":202},[85,336,212],{"class":198},[85,338,209],{"class":202},[85,340,341],{"class":217},"negate",[85,343,273],{"class":198},[85,345,224],{"class":202},[85,347,348],{"class":198}," view)\n",[85,350,351,353,355,357,359,362,364,366,369,371],{"class":87,"line":142},[85,352,332],{"class":198},[85,354,209],{"class":202},[85,356,212],{"class":198},[85,358,209],{"class":202},[85,360,361],{"class":217},"add",[85,363,273],{"class":198},[85,365,224],{"class":202},[85,367,368],{"class":198}," center",[85,370,224],{"class":202},[85,372,348],{"class":198},[85,374,375,378,380,383,385,387,389,392],{"class":87,"line":148},[85,376,377],{"class":198},"reflectCamera",[85,379,209],{"class":202},[85,381,382],{"class":198},"position ",[85,384,203],{"class":202},[85,386,322],{"class":198},[85,388,209],{"class":202},[85,390,391],{"class":217},"clone",[85,393,394],{"class":198},"()\n",[18,396,397,398,401,402,405],{},"核心是向量反射公式 ",[50,399,400],{},"R = V - 2(V·N)N","。先计算主相机到水面中心的向量，对其做关于水面法线的反射，得到反射相机的位置。",[50,403,404],{},"dot > 0"," 的判断确保相机在水面下方时跳过渲染。",[174,407,408],{"id":408},"顶点着色器计算反射纹理坐标",[76,410,414],{"className":411,"code":412,"language":413,"meta":81,"style":81},"language-glsl shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","uniform mat4 reflectorProjectionMatrix;\nuniform mat4 reflectorViewMatrix;\nuniform mat4 reflectMatrix;\nout vec4 v_uv;\n\nvoid main() {\n  vec4 p = czm_computePosition();\n  mat4 modelView = reflectorViewMatrix * reflectMatrix * czm_model;\n  modelView[3][0] = 0.0;\n  modelView[3][1] = 0.0;\n  modelView[3][2] = 0.0;\n  v_uv = reflectorProjectionMatrix * modelView * p;\n  gl_Position = czm_modelViewProjectionRelativeToEye * p;\n}\n","glsl",[50,415,416,421,426,431,436,440,445,450,455,460,465,470,475,480],{"__ignoreMap":81},[85,417,418],{"class":87,"line":88},[85,419,420],{},"uniform mat4 reflectorProjectionMatrix;\n",[85,422,423],{"class":87,"line":94},[85,424,425],{},"uniform mat4 reflectorViewMatrix;\n",[85,427,428],{"class":87,"line":100},[85,429,430],{},"uniform mat4 reflectMatrix;\n",[85,432,433],{"class":87,"line":106},[85,434,435],{},"out vec4 v_uv;\n",[85,437,438],{"class":87,"line":112},[85,439,247],{"emptyLinePlaceholder":246},[85,441,442],{"class":87,"line":118},[85,443,444],{},"void main() {\n",[85,446,447],{"class":87,"line":124},[85,448,449],{},"  vec4 p = czm_computePosition();\n",[85,451,452],{"class":87,"line":130},[85,453,454],{},"  mat4 modelView = reflectorViewMatrix * reflectMatrix * czm_model;\n",[85,456,457],{"class":87,"line":136},[85,458,459],{},"  modelView[3][0] = 0.0;\n",[85,461,462],{"class":87,"line":142},[85,463,464],{},"  modelView[3][1] = 0.0;\n",[85,466,467],{"class":87,"line":148},[85,468,469],{},"  modelView[3][2] = 0.0;\n",[85,471,472],{"class":87,"line":154},[85,473,474],{},"  v_uv = reflectorProjectionMatrix * modelView * p;\n",[85,476,477],{"class":87,"line":160},[85,478,479],{},"  gl_Position = czm_modelViewProjectionRelativeToEye * p;\n",[85,481,482],{"class":87,"line":166},[85,483,484],{},"}\n",[18,486,487,488,491,492,495,496,499],{},"反射纹理坐标在顶点着色器里算好，通过 ",[50,489,490],{},"v_uv"," 传给片元着色器。将 ",[50,493,494],{},"modelView[3]"," 的平移分量清零是为了消除 Cesium 相对于眼坐标的偏移。片元着色器只需 ",[50,497,498],{},"v_uv.xy \u002F v_uv.w * 0.5 + 0.5"," 即可得到采样坐标。",[14,501,503],{"id":502},"从-cesium-199-移植到-1141-踩过的坑","从 Cesium 1.99 移植到 1.141 踩过的坑",[18,505,506],{},"这个 Demo 最初参考了 Cesium 1.99 版本的实现，移植到 1.141 时遇到了一系列问题，每一个都指向 Cesium 内部机制的变化。",[174,508,510,511,514],{"id":509},"坑一cesiumdefaultvalue-不再是顶层导出","坑一：",[50,512,513],{},"Cesium.defaultValue"," 不再是顶层导出",[18,516,517,518,521,522,524,525,528,529,532,533,536,537,540,541,544],{},"1.99 的代码里大量使用 ",[50,519,520],{},"Cesium.defaultValue(a, b)","，在 1.141 的 ES module 构建中这个函数不再挂在 ",[50,523,332],{}," 命名空间上。直接用 ",[50,526,527],{},"a || b"," 替换即可。同理，",[50,530,531],{},"Cesium.Math.sign"," 和 ",[50,534,535],{},"Cesium.Math.log2"," 也可以换成原生 ",[50,538,539],{},"Math.sign"," \u002F ",[50,542,543],{},"Math.log2","。",[174,546,548,549,552,553,556],{"id":547},"坑二glsl-版本升级varyingattribute-变成保留字","坑二：GLSL 版本升级，",[50,550,551],{},"varying","\u002F",[50,554,555],{},"attribute"," 变成保留字",[18,558,559,560,562,563,566,567,562,569,572,573,562,575,566,577,562,580,544],{},"Cesium 1.141 的 shader 全面升级到 GLSL 300 es（WebGL2），顶点着色器里的 ",[50,561,555],{}," 要改成 ",[50,564,565],{},"in","，",[50,568,551],{},[50,570,571],{},"out","；片元着色器里的 ",[50,574,551],{},[50,576,565],{},[50,578,579],{},"texture2D",[50,581,582],{},"texture",[174,584,586],{"id":585},"坑三material-source-里不能声明-varying","坑三：material source 里不能声明 varying",[18,588,589,590,593,594,597,598,601,602,605,606,609],{},"Cesium 的 ",[50,591,592],{},"Material"," 系统在组装最终片元着色器时，会把 ",[50,595,596],{},"material.shaderSource","（即 ",[50,599,600],{},"czm_getMaterial"," 函数体）拼接在 Appearance 的 FS 模板",[33,603,604],{},"之前","。这意味着 material source 里的代码比 FS 里的 ",[50,607,608],{},"in vec3 v_positionMC"," 等声明更早出现，在 GLSL 300 es 里引用一个还没声明的变量会直接报编译错误。",[18,611,612,613,616,617,620,621,624,625,627,628,630],{},"最初的思路是在 material source 里声明 ",[50,614,615],{},"in vec4 v_worldPosition"," 来传递世界坐标，但这在 1.141 里行不通。最终的解法是换用 ",[50,618,619],{},"MaterialAppearance","，把反射纹理坐标的计算移到",[33,622,623],{},"顶点着色器","里完成，输出 ",[50,626,490],{},"，material source 只需直接读取 ",[50,629,490],{}," 采样，完全绕开了 varying 声明顺序的问题。",[174,632,634,635,638],{"id":633},"坑四ellipsoidsurfaceappearance-的-material-uniform-在顶点着色器里不可见","坑四：",[50,636,637],{},"EllipsoidSurfaceAppearance"," 的 material uniform 在顶点着色器里不可见",[18,640,641,642,644,645,648,649,651,652,655],{},"反射矩阵需要在顶点着色器里使用，但 ",[50,643,637],{}," 的 ",[50,646,647],{},"material.uniforms"," 只注入到片元着色器。换成 ",[50,650,619],{}," 后，可以通过 ",[50,653,654],{},"appearance.uniforms","（应用层级 uniform）把矩阵传给顶点着色器，这才是正确的做法。",[174,657,659,660,663],{"id":658},"坑五picking_pickoffscreenview-是-11-像素的离屏-view","坑五：",[50,661,662],{},"Picking._pickOffscreenView"," 是 1×1 像素的离屏 view",[18,665,666,667,670,671,674,675,678,679,682],{},"为了避免污染主 view，曾尝试用 ",[50,668,669],{},"new Cesium.Picking(scene)._pickOffscreenView"," 作为离屏渲染的 view。但这个 view 的 viewport 固定是 ",[50,672,673],{},"1×1"," 像素，用它渲染出来的反射纹理只有 1 个像素，当然看不到任何倒影。正确做法是直接使用 ",[50,676,677],{},"scene._defaultView","，临时替换它的 ",[50,680,681],{},"camera"," 属性，渲染完后恢复。",[174,684,686,687,690],{"id":685},"坑六contextendframe-必须在离屏渲染后调用","坑六：",[50,688,689],{},"context.endFrame()"," 必须在离屏渲染后调用",[18,692,693,695,696,699,700,703,704,706],{},[50,694,52],{}," 事件在 Cesium 主 ",[50,697,698],{},"render()"," 函数之前触发，此时 ",[50,701,702],{},"context.beginFrame()"," 还没被调用。离屏渲染结束后必须调用 ",[50,705,689],{},"，它会解绑 framebuffer 并清理纹理绑定，让 Cesium 的主渲染流程从干净的 WebGL 状态开始。如果不调用，主渲染会继续往离屏 buffer 写入，导致屏幕全被水面颜色覆盖，或者帧率骤降到 3 FPS。",[174,708,710,711,714],{"id":709},"坑七反射纹理必须用-texturefromframebuffer-创建","坑七：反射纹理必须用 ",[50,712,713],{},"Texture.fromFramebuffer"," 创建",[18,716,717,718,721,722,725,726,729,730,733],{},"直接把 ",[50,719,720],{},"this._colorTexture","（自建的 ",[50,723,724],{},"Cesium.Texture"," 实例）赋给 ",[50,727,728],{},"material.uniforms.reflectMap"," 不起作用——水面只显示纯色，没有倒影。Cesium 的 Material 系统对 sampler2D uniform 的识别有特定要求，必须每帧通过 ",[50,731,732],{},"Cesium.Texture.fromFramebuffer({ context, framebuffer })"," 创建一个新的纹理对象，才能被正确绑定到 shader 里。",[14,735,736],{"id":736},"效果与边界",[18,738,739],{},"最终效果是水面能够实时反映场景中 3D Tiles 建筑的倒影，倒影随相机移动和旋转正确变化，水面带有法线扰动产生的波纹动画和高光效果，透明度和反射强度均可通过参数调节。",[18,741,742],{},"当前实现的边界：",[744,745,746,750,757,764,767],"ul",{},[747,748,749],"li",{},"每帧需要额外渲染一次完整场景，GPU 开销约翻倍。适合局部水面区域，不适合全球尺度的海洋反射。",[747,751,752,753,756],{},"必须关闭对数深度缓冲（",[50,754,755],{},"logarithmicDepthBuffer = false","），否则投影矩阵修改会导致深度计算错误。",[747,758,759,760,763],{},"反射只包含 3D Tiles 和 Primitive，默认不包含地球表面（可通过 ",[50,761,762],{},"showGlobe"," 参数开启）。",[747,765,766],{},"水面是平面近似，不支持曲面水体或有高度变化的水面。",[747,768,769,770,532,773,776,777,780],{},"猴子补丁修改了 Cesium 原型方法（",[50,771,772],{},"UniformState.prototype.updateFrustum",[50,774,775],{},"PerspectiveFrustum.prototype.clone","），同一页面只能有一个反射工具实例，",[50,778,779],{},"destroy()"," 时会自动恢复。",[14,782,783],{"id":783},"小结",[18,785,786,787,790,791,793],{},"水面镜面反射的核心链路是：反射相机镜像计算 → 斜裁剪面投影矩阵 → 离屏 Framebuffer 渲染 → 反射纹理投影采样。从 Cesium 1.99 移植到 1.141 的过程中，GLSL 版本升级、material source 拼接顺序、appearance uniform 可见性、",[50,788,789],{},"endFrame"," 调用时机、",[50,792,713],{}," 的必要性，这些都是容易踩的坑，每一个都需要对 Cesium 内部渲染机制有一定了解才能定位。",[795,796,797],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":81,"searchDepth":94,"depth":94,"links":799},[800,801,802,803,804,808,823,824],{"id":16,"depth":94,"text":16},{"id":29,"depth":94,"text":29},{"id":56,"depth":94,"text":56},{"id":74,"depth":94,"text":74},{"id":172,"depth":94,"text":172,"children":805},[806,807],{"id":176,"depth":100,"text":176},{"id":408,"depth":100,"text":408},{"id":502,"depth":94,"text":503,"children":809},[810,812,814,815,817,819,821],{"id":509,"depth":100,"text":811},"坑一：Cesium.defaultValue 不再是顶层导出",{"id":547,"depth":100,"text":813},"坑二：GLSL 版本升级，varying\u002Fattribute 变成保留字",{"id":585,"depth":100,"text":586},{"id":633,"depth":100,"text":816},"坑四：EllipsoidSurfaceAppearance 的 material uniform 在顶点着色器里不可见",{"id":658,"depth":100,"text":818},"坑五：Picking._pickOffscreenView 是 1×1 像素的离屏 view",{"id":685,"depth":100,"text":820},"坑六：context.endFrame() 必须在离屏渲染后调用",{"id":709,"depth":100,"text":822},"坑七：反射纹理必须用 Texture.fromFramebuffer 创建",{"id":736,"depth":94,"text":736},{"id":783,"depth":94,"text":783},"2026-05-14","通过离屏 Framebuffer 渲染反射相机视角，结合斜裁剪面和水面法线扰动，在 Cesium 中实现实时水面倒影。","md","\u002Fimages\u002Fdemos\u002Fcesium-water-reflection-cover.svg",{},"\u002Fblog\u002Fcesium-water-reflection-implementation",{"title":5,"description":826},"blog\u002Fcesium-water-reflection-implementation","Fn_AF3TQIlHgrsLWPM2tUe88MZ9Iegi-U33VLJOb1a0",[835,840],{"title":836,"path":837,"stem":838,"description":839,"children":-1},"Cesium 最高地形瓦片高度图实现","\u002Fblog\u002Fcesium-terrain-heightmap-implementation","blog\u002Fcesium-terrain-heightmap-implementation","解析如何从 Cesium Terrain 采样最高可用层级、生成灰度高度图，并用同一份高度数据构建带真实起伏的三角网格。",null,1778723986331]