[{"data":1,"prerenderedAt":989},["ShallowReactive",2],{"\u002Fblog\u002Fcesium-offscreen-render-framebuffer":3,"\u002Fblog\u002Fcesium-offscreen-render-framebuffer-surround":978},{"id":4,"title":5,"author":6,"body":10,"date":969,"description":970,"extension":971,"image":972,"meta":973,"minRead":141,"navigation":354,"path":974,"seo":975,"stem":976,"__hash__":977},"blog\u002Fblog\u002Fcesium-offscreen-render-framebuffer.md","Cesium 离屏渲染 Framebuffer 实现",{"name":7,"avatar":8},"Kevin",{"src":9,"alt":7},"\u002Favatar.jpg",{"type":11,"value":12,"toc":960},"minimark",[13,17,21,33,36,39,45,48,58,61,64,71,78,85,95,98,247,250,253,519,522,635,638,799,808,920,923,926,936,939,942,945,953,956],[14,15,16],"h2",{"id":16},"这篇文章解决什么问题",[18,19,20],"p",{},"这个 Demo 想解决的是一个很典型的三维引擎问题：如果不把画面直接画到浏览器正在显示的 canvas 上，能不能在后台再渲染一次 Cesium 场景，并把结果作为普通图像数据拿出来？",[18,22,23,24,28,29,32],{},"答案是可以。WebGL 里这类能力通常通过 ",[25,26,27],"code",{},"Framebuffer"," 完成。它像一个离屏画布，颜色结果写入纹理，深度测试写入深度附件，最后再通过 ",[25,30,31],{},"readPixels()"," 把 GPU 里的像素读回 CPU。",[18,34,35],{},"在 Cesium 里难点不只是创建 WebGL 资源。Cesium 自己维护了相机、视图、帧状态、命令列表、地球瓦片和 3D Tiles 渲染通道。要让离屏渲染得到和正常场景一致的结果，必须把这些状态按 Cesium 的渲染链路重新串起来。",[14,37,38],{"id":38},"核心难点",[18,40,41,42,44],{},"第一个难点是渲染目标要完整。只有颜色纹理还不够，场景里的地球、模型和瓦片仍然需要深度测试，所以离屏 ",[25,43,27],{}," 需要同时挂载颜色纹理和深度模板纹理。",[18,46,47],{},"第二个难点是相机不能简单依赖页面当前帧。Demo 使用 Cesium 的离屏 view，捕获时把相机克隆到这个 view 上，再把 view 的 viewport 改成捕获尺寸。这样页面主视图可以继续显示，离屏结果则按指定尺寸输出。",[18,49,50,51,54,55,57],{},"第三个难点是读回方向。WebGL 像素原点在左下角，canvas 2D 的 ",[25,52,53],{},"ImageData"," 通常按左上角理解。如果直接展示 ",[25,56,31],{}," 的结果，图像会上下颠倒，所以读回后需要做一次行翻转。",[14,59,60],{"id":60},"实现思路",[18,62,63],{},"整体链路可以拆成四步。",[18,65,66,67,70],{},"第一步，按捕获尺寸创建或复用 ",[25,68,69],{},"Texture + Framebuffer","。尺寸不变时直接复用，尺寸变化时销毁旧资源后重建，避免每次点击都创建新的 WebGL 对象。",[18,72,73,74,77],{},"第二步，准备离屏 view 和相机。默认克隆当前相机，也可以传入独立的 ",[25,75,76],{},"cameraView","。view 的 viewport 会被改成离屏图像尺寸，passState 的 framebuffer 指向工具类管理的离屏缓冲区。",[18,79,80,81,84],{},"第三步，按 Cesium 正常渲染流程更新帧状态、环境和命令列表，再调用 ",[25,82,83],{},"updateAndExecuteCommands()"," 把场景命令真正画到离屏 framebuffer。",[18,86,87,88,91,92,94],{},"第四步，通过 ",[25,89,90],{},"context.readPixels()"," 读取 RGBA 像素，翻转行顺序后包装成 ",[25,93,53],{},"，交给页面预览 canvas 展示。",[14,96,97],{"id":97},"技术流程图",[99,100,105],"pre",{"className":101,"code":102,"language":103,"meta":104,"style":104},"language-mermaid shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","%%{init: {'theme':'base', 'themeVariables':{'primaryColor':'#f8fafc', 'primaryTextColor':'#1f2335', 'primaryBorderColor':'#a8b5ff', 'lineColor':'#64748b', 'edgeLabelBackground':'#ffffff'}}}%%\nflowchart TD\n    A([手动触发捕获]) --> B[读取捕获尺寸和主视图相机]\n    B --> C{Framebuffer 尺寸是否可复用}\n    C -->|是| D[复用颜色纹理和深度模板纹理]\n    C -->|否| E[销毁旧资源并重建 Texture 与 Framebuffer]\n    D --> F[复制主相机到离屏 view]\n    E --> F\n    F --> G[设置离屏 viewport 和 passState.framebuffer]\n    G --> H[更新 frameState 和 uniformState]\n    H --> I[执行 Cesium 场景渲染命令]\n    I --> J[resolve framebuffer]\n    J --> K[readPixels 读取 RGBA 像素]\n    K --> L[翻转像素行方向]\n    L --> M[包装成 ImageData]\n    M --> N[更新预览 canvas 和捕获指标]\n    N --> O([捕获完成])\n    \n    style A fill:#7aa2f7,stroke:#7aa2f7,color:#fff\n    style O fill:#9ece6a,stroke:#9ece6a,color:#fff\n    style C fill:#e0af68,stroke:#e0af68,color:#1f2335\n    style E fill:#bb9af7,stroke:#bb9af7,color:#fff\n    style K fill:#bb9af7,stroke:#bb9af7,color:#fff\n","mermaid","",[25,106,107,115,121,127,133,139,145,151,157,163,169,175,181,187,193,199,205,211,217,223,229,235,241],{"__ignoreMap":104},[108,109,112],"span",{"class":110,"line":111},"line",1,[108,113,114],{},"%%{init: {'theme':'base', 'themeVariables':{'primaryColor':'#f8fafc', 'primaryTextColor':'#1f2335', 'primaryBorderColor':'#a8b5ff', 'lineColor':'#64748b', 'edgeLabelBackground':'#ffffff'}}}%%\n",[108,116,118],{"class":110,"line":117},2,[108,119,120],{},"flowchart TD\n",[108,122,124],{"class":110,"line":123},3,[108,125,126],{},"    A([手动触发捕获]) --> B[读取捕获尺寸和主视图相机]\n",[108,128,130],{"class":110,"line":129},4,[108,131,132],{},"    B --> C{Framebuffer 尺寸是否可复用}\n",[108,134,136],{"class":110,"line":135},5,[108,137,138],{},"    C -->|是| D[复用颜色纹理和深度模板纹理]\n",[108,140,142],{"class":110,"line":141},6,[108,143,144],{},"    C -->|否| E[销毁旧资源并重建 Texture 与 Framebuffer]\n",[108,146,148],{"class":110,"line":147},7,[108,149,150],{},"    D --> F[复制主相机到离屏 view]\n",[108,152,154],{"class":110,"line":153},8,[108,155,156],{},"    E --> F\n",[108,158,160],{"class":110,"line":159},9,[108,161,162],{},"    F --> G[设置离屏 viewport 和 passState.framebuffer]\n",[108,164,166],{"class":110,"line":165},10,[108,167,168],{},"    G --> H[更新 frameState 和 uniformState]\n",[108,170,172],{"class":110,"line":171},11,[108,173,174],{},"    H --> I[执行 Cesium 场景渲染命令]\n",[108,176,178],{"class":110,"line":177},12,[108,179,180],{},"    I --> J[resolve framebuffer]\n",[108,182,184],{"class":110,"line":183},13,[108,185,186],{},"    J --> K[readPixels 读取 RGBA 像素]\n",[108,188,190],{"class":110,"line":189},14,[108,191,192],{},"    K --> L[翻转像素行方向]\n",[108,194,196],{"class":110,"line":195},15,[108,197,198],{},"    L --> M[包装成 ImageData]\n",[108,200,202],{"class":110,"line":201},16,[108,203,204],{},"    M --> N[更新预览 canvas 和捕获指标]\n",[108,206,208],{"class":110,"line":207},17,[108,209,210],{},"    N --> O([捕获完成])\n",[108,212,214],{"class":110,"line":213},18,[108,215,216],{},"    \n",[108,218,220],{"class":110,"line":219},19,[108,221,222],{},"    style A fill:#7aa2f7,stroke:#7aa2f7,color:#fff\n",[108,224,226],{"class":110,"line":225},20,[108,227,228],{},"    style O fill:#9ece6a,stroke:#9ece6a,color:#fff\n",[108,230,232],{"class":110,"line":231},21,[108,233,234],{},"    style C fill:#e0af68,stroke:#e0af68,color:#1f2335\n",[108,236,238],{"class":110,"line":237},22,[108,239,240],{},"    style E fill:#bb9af7,stroke:#bb9af7,color:#fff\n",[108,242,244],{"class":110,"line":243},23,[108,245,246],{},"    style K fill:#bb9af7,stroke:#bb9af7,color:#fff\n",[14,248,249],{"id":249},"关键代码解析",[18,251,252],{},"离屏缓冲区需要颜色和深度两个附件。颜色纹理负责最终截图，深度模板纹理负责让地球、模型和 3D Tiles 的遮挡关系仍然成立。",[99,254,258],{"className":255,"code":256,"language":257,"meta":104,"style":104},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","this.colorTexture = new Cesium.Texture({\n  context,\n  width,\n  height,\n  pixelFormat: Cesium.PixelFormat.RGBA,\n  pixelDatatype\n})\n\nthis.depthStencilTexture = new Cesium.Texture({\n  context,\n  width,\n  height,\n  pixelFormat: Cesium.PixelFormat.DEPTH_STENCIL,\n  pixelDatatype: Cesium.PixelDatatype.UNSIGNED_INT_24_8\n})\n\nthis.framebuffer = new Cesium.Framebuffer({\n  context,\n  colorTextures: [this.colorTexture],\n  depthStencilTexture: this.depthStencilTexture,\n  destroyAttachments: false\n})\n","js",[25,259,260,292,300,307,314,337,342,350,356,377,383,389,395,414,433,439,443,464,470,487,502,513],{"__ignoreMap":104},[108,261,262,266,270,273,276,279,282,286,289],{"class":110,"line":111},[108,263,265],{"class":264},"sMK4o","this.",[108,267,269],{"class":268},"sTEyZ","colorTexture ",[108,271,272],{"class":264},"=",[108,274,275],{"class":264}," new",[108,277,278],{"class":268}," Cesium",[108,280,281],{"class":264},".",[108,283,285],{"class":284},"s2Zo4","Texture",[108,287,288],{"class":268},"(",[108,290,291],{"class":264},"{\n",[108,293,294,297],{"class":110,"line":117},[108,295,296],{"class":268},"  context",[108,298,299],{"class":264},",\n",[108,301,302,305],{"class":110,"line":123},[108,303,304],{"class":268},"  width",[108,306,299],{"class":264},[108,308,309,312],{"class":110,"line":129},[108,310,311],{"class":268},"  height",[108,313,299],{"class":264},[108,315,316,320,323,325,327,330,332,335],{"class":110,"line":135},[108,317,319],{"class":318},"swJcz","  pixelFormat",[108,321,322],{"class":264},":",[108,324,278],{"class":268},[108,326,281],{"class":264},[108,328,329],{"class":268},"PixelFormat",[108,331,281],{"class":264},[108,333,334],{"class":268},"RGBA",[108,336,299],{"class":264},[108,338,339],{"class":110,"line":141},[108,340,341],{"class":268},"  pixelDatatype\n",[108,343,344,347],{"class":110,"line":147},[108,345,346],{"class":264},"}",[108,348,349],{"class":268},")\n",[108,351,352],{"class":110,"line":153},[108,353,355],{"emptyLinePlaceholder":354},true,"\n",[108,357,358,360,363,365,367,369,371,373,375],{"class":110,"line":159},[108,359,265],{"class":264},[108,361,362],{"class":268},"depthStencilTexture ",[108,364,272],{"class":264},[108,366,275],{"class":264},[108,368,278],{"class":268},[108,370,281],{"class":264},[108,372,285],{"class":284},[108,374,288],{"class":268},[108,376,291],{"class":264},[108,378,379,381],{"class":110,"line":165},[108,380,296],{"class":268},[108,382,299],{"class":264},[108,384,385,387],{"class":110,"line":171},[108,386,304],{"class":268},[108,388,299],{"class":264},[108,390,391,393],{"class":110,"line":177},[108,392,311],{"class":268},[108,394,299],{"class":264},[108,396,397,399,401,403,405,407,409,412],{"class":110,"line":183},[108,398,319],{"class":318},[108,400,322],{"class":264},[108,402,278],{"class":268},[108,404,281],{"class":264},[108,406,329],{"class":268},[108,408,281],{"class":264},[108,410,411],{"class":268},"DEPTH_STENCIL",[108,413,299],{"class":264},[108,415,416,419,421,423,425,428,430],{"class":110,"line":189},[108,417,418],{"class":318},"  pixelDatatype",[108,420,322],{"class":264},[108,422,278],{"class":268},[108,424,281],{"class":264},[108,426,427],{"class":268},"PixelDatatype",[108,429,281],{"class":264},[108,431,432],{"class":268},"UNSIGNED_INT_24_8\n",[108,434,435,437],{"class":110,"line":195},[108,436,346],{"class":264},[108,438,349],{"class":268},[108,440,441],{"class":110,"line":201},[108,442,355],{"emptyLinePlaceholder":354},[108,444,445,447,450,452,454,456,458,460,462],{"class":110,"line":207},[108,446,265],{"class":264},[108,448,449],{"class":268},"framebuffer ",[108,451,272],{"class":264},[108,453,275],{"class":264},[108,455,278],{"class":268},[108,457,281],{"class":264},[108,459,27],{"class":284},[108,461,288],{"class":268},[108,463,291],{"class":264},[108,465,466,468],{"class":110,"line":213},[108,467,296],{"class":268},[108,469,299],{"class":264},[108,471,472,475,477,480,482,485],{"class":110,"line":219},[108,473,474],{"class":318},"  colorTextures",[108,476,322],{"class":264},[108,478,479],{"class":268}," [",[108,481,265],{"class":264},[108,483,484],{"class":268},"colorTexture]",[108,486,299],{"class":264},[108,488,489,492,494,497,500],{"class":110,"line":225},[108,490,491],{"class":318},"  depthStencilTexture",[108,493,322],{"class":264},[108,495,496],{"class":264}," this.",[108,498,499],{"class":268},"depthStencilTexture",[108,501,299],{"class":264},[108,503,504,507,509],{"class":110,"line":231},[108,505,506],{"class":318},"  destroyAttachments",[108,508,322],{"class":264},[108,510,512],{"class":511},"sfNiH"," false\n",[108,514,515,517],{"class":110,"line":237},[108,516,346],{"class":264},[108,518,349],{"class":268},[18,520,521],{},"真正渲染前，要把 passState 的 framebuffer 指向离屏缓冲区，并把 viewport 设置成捕获尺寸。这样 Cesium 后续执行的 draw command 会进入这块离屏目标，而不是进入屏幕 canvas。",[99,523,525],{"className":255,"code":524,"language":257,"meta":104,"style":104},"viewport.x = 0\nviewport.y = 0\nviewport.width = width\nviewport.height = height\npassState.viewport = Cesium.BoundingRectangle.clone(viewport, passState.viewport)\npassState.framebuffer = framebuffer\n",[25,526,527,543,556,570,584,622],{"__ignoreMap":104},[108,528,529,532,534,537,539],{"class":110,"line":111},[108,530,531],{"class":268},"viewport",[108,533,281],{"class":264},[108,535,536],{"class":268},"x ",[108,538,272],{"class":264},[108,540,542],{"class":541},"sbssI"," 0\n",[108,544,545,547,549,552,554],{"class":110,"line":117},[108,546,531],{"class":268},[108,548,281],{"class":264},[108,550,551],{"class":268},"y ",[108,553,272],{"class":264},[108,555,542],{"class":541},[108,557,558,560,562,565,567],{"class":110,"line":123},[108,559,531],{"class":268},[108,561,281],{"class":264},[108,563,564],{"class":268},"width ",[108,566,272],{"class":264},[108,568,569],{"class":268}," width\n",[108,571,572,574,576,579,581],{"class":110,"line":129},[108,573,531],{"class":268},[108,575,281],{"class":264},[108,577,578],{"class":268},"height ",[108,580,272],{"class":264},[108,582,583],{"class":268}," height\n",[108,585,586,589,591,594,596,598,600,603,605,608,611,614,617,619],{"class":110,"line":135},[108,587,588],{"class":268},"passState",[108,590,281],{"class":264},[108,592,593],{"class":268},"viewport ",[108,595,272],{"class":264},[108,597,278],{"class":268},[108,599,281],{"class":264},[108,601,602],{"class":268},"BoundingRectangle",[108,604,281],{"class":264},[108,606,607],{"class":284},"clone",[108,609,610],{"class":268},"(viewport",[108,612,613],{"class":264},",",[108,615,616],{"class":268}," passState",[108,618,281],{"class":264},[108,620,621],{"class":268},"viewport)\n",[108,623,624,626,628,630,632],{"class":110,"line":141},[108,625,588],{"class":268},[108,627,281],{"class":264},[108,629,449],{"class":268},[108,631,272],{"class":264},[108,633,634],{"class":268}," framebuffer\n",[18,636,637],{},"核心渲染步骤仍然沿用 Cesium 的场景管线：更新 frameState，设置 render pass，更新 uniformState，然后执行命令列表。这里没有手写模型或瓦片绘制逻辑，而是让 Cesium 自己生成并执行当前场景的渲染命令。",[99,639,641],{"className":255,"code":640,"language":257,"meta":104,"style":104},"scene.updateFrameState()\nframeState.passes.render = true\nframeState.passes.offscreen = true\nframeState.tilesetPassState = new Cesium.Cesium3DTilePassState({\n  pass: Cesium.Cesium3DTilePass.RENDER\n})\n\nuniformState.update(frameState)\nscene.updateEnvironment()\nscene.updateAndExecuteCommands(passState, clearColor)\nscene.resolveFramebuffers(passState)\n",[25,642,643,656,676,693,717,736,742,746,759,770,787],{"__ignoreMap":104},[108,644,645,648,650,653],{"class":110,"line":111},[108,646,647],{"class":268},"scene",[108,649,281],{"class":264},[108,651,652],{"class":284},"updateFrameState",[108,654,655],{"class":268},"()\n",[108,657,658,661,663,666,668,671,673],{"class":110,"line":117},[108,659,660],{"class":268},"frameState",[108,662,281],{"class":264},[108,664,665],{"class":268},"passes",[108,667,281],{"class":264},[108,669,670],{"class":268},"render ",[108,672,272],{"class":264},[108,674,675],{"class":511}," true\n",[108,677,678,680,682,684,686,689,691],{"class":110,"line":123},[108,679,660],{"class":268},[108,681,281],{"class":264},[108,683,665],{"class":268},[108,685,281],{"class":264},[108,687,688],{"class":268},"offscreen ",[108,690,272],{"class":264},[108,692,675],{"class":511},[108,694,695,697,699,702,704,706,708,710,713,715],{"class":110,"line":129},[108,696,660],{"class":268},[108,698,281],{"class":264},[108,700,701],{"class":268},"tilesetPassState ",[108,703,272],{"class":264},[108,705,275],{"class":264},[108,707,278],{"class":268},[108,709,281],{"class":264},[108,711,712],{"class":284},"Cesium3DTilePassState",[108,714,288],{"class":268},[108,716,291],{"class":264},[108,718,719,722,724,726,728,731,733],{"class":110,"line":135},[108,720,721],{"class":318},"  pass",[108,723,322],{"class":264},[108,725,278],{"class":268},[108,727,281],{"class":264},[108,729,730],{"class":268},"Cesium3DTilePass",[108,732,281],{"class":264},[108,734,735],{"class":268},"RENDER\n",[108,737,738,740],{"class":110,"line":141},[108,739,346],{"class":264},[108,741,349],{"class":268},[108,743,744],{"class":110,"line":147},[108,745,355],{"emptyLinePlaceholder":354},[108,747,748,751,753,756],{"class":110,"line":153},[108,749,750],{"class":268},"uniformState",[108,752,281],{"class":264},[108,754,755],{"class":284},"update",[108,757,758],{"class":268},"(frameState)\n",[108,760,761,763,765,768],{"class":110,"line":159},[108,762,647],{"class":268},[108,764,281],{"class":264},[108,766,767],{"class":284},"updateEnvironment",[108,769,655],{"class":268},[108,771,772,774,776,779,782,784],{"class":110,"line":165},[108,773,647],{"class":268},[108,775,281],{"class":264},[108,777,778],{"class":284},"updateAndExecuteCommands",[108,780,781],{"class":268},"(passState",[108,783,613],{"class":264},[108,785,786],{"class":268}," clearColor)\n",[108,788,789,791,793,796],{"class":110,"line":171},[108,790,647],{"class":268},[108,792,281],{"class":264},[108,794,795],{"class":284},"resolveFramebuffers",[108,797,798],{"class":268},"(passState)\n",[18,800,801,802,804,805,807],{},"最后一步是读回像素。",[25,803,31],{}," 返回的是 WebGL 坐标方向的数据，所以 Demo 在生成 ",[25,806,53],{}," 前先把每一行反过来，避免预览图上下颠倒。",[99,809,811],{"className":255,"code":810,"language":257,"meta":104,"style":104},"const rawPixels = scene.context.readPixels({\n  framebuffer,\n  width,\n  height\n})\n\nconst imageData = new ImageData(\n  flipPixelsVertically(rawPixels, width, height),\n  width,\n  height\n)\n",[25,812,813,841,848,854,859,865,869,886,906,912,916],{"__ignoreMap":104},[108,814,815,819,822,824,827,829,832,834,837,839],{"class":110,"line":111},[108,816,818],{"class":817},"spNyl","const",[108,820,821],{"class":268}," rawPixels ",[108,823,272],{"class":264},[108,825,826],{"class":268}," scene",[108,828,281],{"class":264},[108,830,831],{"class":268},"context",[108,833,281],{"class":264},[108,835,836],{"class":284},"readPixels",[108,838,288],{"class":268},[108,840,291],{"class":264},[108,842,843,846],{"class":110,"line":117},[108,844,845],{"class":268},"  framebuffer",[108,847,299],{"class":264},[108,849,850,852],{"class":110,"line":123},[108,851,304],{"class":268},[108,853,299],{"class":264},[108,855,856],{"class":110,"line":129},[108,857,858],{"class":268},"  height\n",[108,860,861,863],{"class":110,"line":135},[108,862,346],{"class":264},[108,864,349],{"class":268},[108,866,867],{"class":110,"line":141},[108,868,355],{"emptyLinePlaceholder":354},[108,870,871,873,876,878,880,883],{"class":110,"line":147},[108,872,818],{"class":817},[108,874,875],{"class":268}," imageData ",[108,877,272],{"class":264},[108,879,275],{"class":264},[108,881,882],{"class":284}," ImageData",[108,884,885],{"class":268},"(\n",[108,887,888,891,894,896,899,901,904],{"class":110,"line":153},[108,889,890],{"class":284},"  flipPixelsVertically",[108,892,893],{"class":268},"(rawPixels",[108,895,613],{"class":264},[108,897,898],{"class":268}," width",[108,900,613],{"class":264},[108,902,903],{"class":268}," height)",[108,905,299],{"class":264},[108,907,908,910],{"class":110,"line":159},[108,909,304],{"class":268},[108,911,299],{"class":264},[108,913,914],{"class":110,"line":165},[108,915,858],{"class":268},[108,917,918],{"class":110,"line":171},[108,919,349],{"class":268},[14,921,922],{"id":922},"效果与边界",[18,924,925],{},"这个 Demo 输出的是一次手动捕获的离屏图像，适合做截图预览、后台采样、反射探针的基础实验，或者作为后续批量拾取、深度图反解的底层入口。",[18,927,928,929,931,932,935],{},"它不是实时视频流方案。",[25,930,31],{}," 本身会造成 GPU 到 CPU 的同步读回，尺寸越大，耗时越明显。因此第一版选择手动触发，并把默认尺寸限制在 ",[25,933,934],{},"512 x 512","。",[18,937,938],{},"当前实现也使用了 Cesium 的私有渲染入口，例如离屏 view、frameState 和命令执行方法。这类代码适合封装在工具类内部，并在 Cesium 升级后集中验证，不应该散落在页面逻辑里。",[14,940,941],{"id":941},"小结",[18,943,944],{},"Cesium 离屏渲染的核心不是截取当前 canvas，而是重新指定渲染目标。工具类创建自己的 Framebuffer，把 Cesium 场景命令画进去，再把颜色纹理读回成普通图像数据。",[18,946,947,948,950,951,935],{},"这条链路可以概括为：准备离屏纹理和深度附件，配置离屏 view 和 passState，执行 Cesium 场景渲染命令，最后 ",[25,949,31],{}," 得到 ",[25,952,53],{},[18,954,955],{},"当这套能力稳定后，后续的拾取图、深度图、反射探针和后台渲染缓存，都可以在同一个基础上继续扩展。",[957,958,959],"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 .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}",{"title":104,"searchDepth":117,"depth":117,"links":961},[962,963,964,965,966,967,968],{"id":16,"depth":117,"text":16},{"id":38,"depth":117,"text":38},{"id":60,"depth":117,"text":60},{"id":97,"depth":117,"text":97},{"id":249,"depth":117,"text":249},{"id":922,"depth":117,"text":922},{"id":941,"depth":117,"text":941},"2026-05-13","解析如何在 Cesium 中自建 Framebuffer，把指定视角渲染成离屏纹理并读回为 ImageData。","md","\u002Fimages\u002Fdemos\u002Fcesium-offscreen-render-ai-cover.png",{},"\u002Fblog\u002Fcesium-offscreen-render-framebuffer",{"title":5,"description":970},"blog\u002Fcesium-offscreen-render-framebuffer","DjvFjLGAn3KGSwrvIU1wrtNorTUjvxC-M_yI4-lkQzk",[979,984],{"title":980,"path":981,"stem":982,"description":983,"children":-1},"Cesium 资源请求的 IndexedDB 离线缓存实现","\u002Fblog\u002Fcesium-indexeddb-resource-cache","blog\u002Fcesium-indexeddb-resource-cache","解析如何拦截 Cesium.Resource 请求，把影像瓦片、地形瓦片和 3D Tiles 写入 IndexedDB，并控制缓存范围与二次加载性能。",{"title":985,"path":986,"stem":987,"description":988,"children":-1},"Cesium 行政区反向遮罩的屏幕空间实现","\u002Fblog\u002Fcesium-region-mask-overlay-implementation","blog\u002Fcesium-region-mask-overlay-implementation","解析如何把行政区 GeoJSON 投影到屏幕 canvas，用 evenodd 填充规则实现地图压暗与行政区透明洞口。",1778657542552]