[{"data":1,"prerenderedAt":1638},["ShallowReactive",2],{"\u002Fblog\u002Fcesium-terrain-heightmap-implementation":3,"\u002Fblog\u002Fcesium-terrain-heightmap-implementation-surround":1631},{"id":4,"title":5,"author":6,"body":10,"date":1622,"description":1623,"extension":1624,"image":1625,"meta":1626,"minRead":1345,"navigation":278,"path":1627,"seo":1628,"stem":1629,"__hash__":1630},"blog\u002Fblog\u002Fcesium-terrain-heightmap-implementation.md","Cesium 最高地形瓦片高度图实现说明",{"name":7,"avatar":8},"Kevin",{"src":9,"alt":7},"\u002Favatar.jpg",{"type":11,"value":12,"toc":1611},"minimark",[13,17,21,56,59,62,65,202,205,228,231,234,241,353,360,529,532,535,538,646,653,759,769,773,782,878,885,888,899,1027,1030,1066,1077,1101,1104,1107,1221,1375,1378,1383,1411,1414,1440,1447,1450,1459,1508,1515,1518,1521,1596,1607],[14,15,16],"h2",{"id":16},"这篇笔记解决什么问题",[18,19,20],"p",{},"这个 Demo 的目标不是把一次性逻辑堆到页面里，而是把“从地形生成高度图”拆成可以复用的 SDK 模块：",[22,23,24,32,38,44,50],"ul",{},[25,26,27,31],"li",{},[28,29,30],"code",{},"rectangleDraw.js"," 只负责地形拾取和矩形绘制",[25,33,34,37],{},[28,35,36],{},"terrainSampler.js"," 只负责最高层级解析和规则网格采样",[25,39,40,43],{},[28,41,42],{},"heightmapCanvas.js"," 只负责把高度数组转成灰度 canvas",[25,45,46,49],{},[28,47,48],{},"heightmapSurface.js"," 只负责创建带真实高度顶点的 Cesium Primitive",[25,51,52,55],{},[28,53,54],{},"CesiumTerrainHeightmapDemo.js"," 负责把这些能力组装成 Demo runtime",[18,57,58],{},"这样后续如果要把高度图接入水流模拟、侵蚀模拟或碰撞计算，可以复用采样和 canvas 输出模块，而不需要复制整页交互。",[14,60,61],{"id":61},"整体调用链",[18,63,64],{},"页面启动后先通过统一 runtime 加载 Cesium，再创建 Viewer 和 World Terrain。GUI 暴露三个入口：绘制提取范围、切换地形深度测试、加载或取消地形。",[66,67,72],"pre",{"className":68,"code":69,"language":70,"meta":71,"style":71},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","gui.add(controls, 'drawRange').name('绘制提取范围')\ngui.add(controls, 'terrainDepthTest').name('地形深度测试')\ngui.add(controls, 'toggleTerrain').name('加载 \u002F 取消地形')\n","js","",[28,73,74,128,165],{"__ignoreMap":71},[75,76,79,83,87,91,94,97,100,104,107,110,112,115,118,120,123,125],"span",{"class":77,"line":78},"line",1,[75,80,82],{"class":81},"sTEyZ","gui",[75,84,86],{"class":85},"sMK4o",".",[75,88,90],{"class":89},"s2Zo4","add",[75,92,93],{"class":81},"(controls",[75,95,96],{"class":85},",",[75,98,99],{"class":85}," '",[75,101,103],{"class":102},"sfazB","drawRange",[75,105,106],{"class":85},"'",[75,108,109],{"class":81},")",[75,111,86],{"class":85},[75,113,114],{"class":89},"name",[75,116,117],{"class":81},"(",[75,119,106],{"class":85},[75,121,122],{"class":102},"绘制提取范围",[75,124,106],{"class":85},[75,126,127],{"class":81},")\n",[75,129,131,133,135,137,139,141,143,146,148,150,152,154,156,158,161,163],{"class":77,"line":130},2,[75,132,82],{"class":81},[75,134,86],{"class":85},[75,136,90],{"class":89},[75,138,93],{"class":81},[75,140,96],{"class":85},[75,142,99],{"class":85},[75,144,145],{"class":102},"terrainDepthTest",[75,147,106],{"class":85},[75,149,109],{"class":81},[75,151,86],{"class":85},[75,153,114],{"class":89},[75,155,117],{"class":81},[75,157,106],{"class":85},[75,159,160],{"class":102},"地形深度测试",[75,162,106],{"class":85},[75,164,127],{"class":81},[75,166,168,170,172,174,176,178,180,183,185,187,189,191,193,195,198,200],{"class":77,"line":167},3,[75,169,82],{"class":81},[75,171,86],{"class":85},[75,173,90],{"class":89},[75,175,93],{"class":81},[75,177,96],{"class":85},[75,179,99],{"class":85},[75,181,182],{"class":102},"toggleTerrain",[75,184,106],{"class":85},[75,186,109],{"class":81},[75,188,86],{"class":85},[75,190,114],{"class":89},[75,192,117],{"class":81},[75,194,106],{"class":85},[75,196,197],{"class":102},"加载 \u002F 取消地形",[75,199,106],{"class":85},[75,201,127],{"class":81},[18,203,204],{},"用户完成矩形绘制后，runtime 会顺序执行：",[206,207,208,213,218,223],"ol",{},[25,209,210],{},[28,211,212],{},"sampleTerrainHeightmap(viewer, { rectangle })",[25,214,215],{},[28,216,217],{},"renderTerrainHeightmapCanvas(sampleResult)",[25,219,220],{},[28,221,222],{},"createTerrainHeightmapSurface(viewer, sampleResult, heightmap.canvas)",[25,224,225],{},[28,226,227],{},"heightmapPanel.update(sampleResult, heightmap)",[18,229,230],{},"这条链路保证左下角预览图和场景中的起伏面使用同一份高度结果。",[14,232,233],{"id":233},"矩形绘制怎么做",[18,235,236,237,240],{},"绘制模块使用 Cesium 的屏幕事件系统，第一次左键记录起点，鼠标移动时更新预览矩形，第二次左键完成范围。拾取点优先走 ",[28,238,239],{},"globe.pick","，这样鼠标落在真实地形上时能得到地形表面位置。",[66,242,244],{"className":68,"code":243,"language":70,"meta":71,"style":71},"const ray = viewer.camera.getPickRay(windowPosition)\n\nreturn viewer.scene.globe.pick(ray, viewer.scene) ||\n  viewer.camera.pickEllipsoid(windowPosition, viewer.scene.globe.ellipsoid)\n",[28,245,246,274,280,318],{"__ignoreMap":71},[75,247,248,252,255,258,261,263,266,268,271],{"class":77,"line":78},[75,249,251],{"class":250},"spNyl","const",[75,253,254],{"class":81}," ray ",[75,256,257],{"class":85},"=",[75,259,260],{"class":81}," viewer",[75,262,86],{"class":85},[75,264,265],{"class":81},"camera",[75,267,86],{"class":85},[75,269,270],{"class":89},"getPickRay",[75,272,273],{"class":81},"(windowPosition)\n",[75,275,276],{"class":77,"line":130},[75,277,279],{"emptyLinePlaceholder":278},true,"\n",[75,281,282,286,288,290,293,295,298,300,303,306,308,310,312,315],{"class":77,"line":167},[75,283,285],{"class":284},"s7zQu","return",[75,287,260],{"class":81},[75,289,86],{"class":85},[75,291,292],{"class":81},"scene",[75,294,86],{"class":85},[75,296,297],{"class":81},"globe",[75,299,86],{"class":85},[75,301,302],{"class":89},"pick",[75,304,305],{"class":81},"(ray",[75,307,96],{"class":85},[75,309,260],{"class":81},[75,311,86],{"class":85},[75,313,314],{"class":81},"scene) ",[75,316,317],{"class":85},"||\n",[75,319,321,324,326,328,330,333,336,338,340,342,344,346,348,350],{"class":77,"line":320},4,[75,322,323],{"class":81},"  viewer",[75,325,86],{"class":85},[75,327,265],{"class":81},[75,329,86],{"class":85},[75,331,332],{"class":89},"pickEllipsoid",[75,334,335],{"class":81},"(windowPosition",[75,337,96],{"class":85},[75,339,260],{"class":81},[75,341,86],{"class":85},[75,343,292],{"class":81},[75,345,86],{"class":85},[75,347,297],{"class":81},[75,349,86],{"class":85},[75,351,352],{"class":81},"ellipsoid)\n",[18,354,355,356,359],{},"完成时会把两个角点归一化为 ",[28,357,358],{},"Cesium.Rectangle","：",[66,361,363],{"className":68,"code":362,"language":70,"meta":71,"style":71},"const west = Math.min(startCartographic.longitude, endCartographic.longitude)\nconst east = Math.max(startCartographic.longitude, endCartographic.longitude)\nconst south = Math.min(startCartographic.latitude, endCartographic.latitude)\nconst north = Math.max(startCartographic.latitude, endCartographic.latitude)\n\nreturn new Cesium.Rectangle(west, south, east, north)\n",[28,364,365,400,430,461,490,495],{"__ignoreMap":71},[75,366,367,369,372,374,377,379,382,385,387,390,392,395,397],{"class":77,"line":78},[75,368,251],{"class":250},[75,370,371],{"class":81}," west ",[75,373,257],{"class":85},[75,375,376],{"class":81}," Math",[75,378,86],{"class":85},[75,380,381],{"class":89},"min",[75,383,384],{"class":81},"(startCartographic",[75,386,86],{"class":85},[75,388,389],{"class":81},"longitude",[75,391,96],{"class":85},[75,393,394],{"class":81}," endCartographic",[75,396,86],{"class":85},[75,398,399],{"class":81},"longitude)\n",[75,401,402,404,407,409,411,413,416,418,420,422,424,426,428],{"class":77,"line":130},[75,403,251],{"class":250},[75,405,406],{"class":81}," east ",[75,408,257],{"class":85},[75,410,376],{"class":81},[75,412,86],{"class":85},[75,414,415],{"class":89},"max",[75,417,384],{"class":81},[75,419,86],{"class":85},[75,421,389],{"class":81},[75,423,96],{"class":85},[75,425,394],{"class":81},[75,427,86],{"class":85},[75,429,399],{"class":81},[75,431,432,434,437,439,441,443,445,447,449,452,454,456,458],{"class":77,"line":167},[75,433,251],{"class":250},[75,435,436],{"class":81}," south ",[75,438,257],{"class":85},[75,440,376],{"class":81},[75,442,86],{"class":85},[75,444,381],{"class":89},[75,446,384],{"class":81},[75,448,86],{"class":85},[75,450,451],{"class":81},"latitude",[75,453,96],{"class":85},[75,455,394],{"class":81},[75,457,86],{"class":85},[75,459,460],{"class":81},"latitude)\n",[75,462,463,465,468,470,472,474,476,478,480,482,484,486,488],{"class":77,"line":320},[75,464,251],{"class":250},[75,466,467],{"class":81}," north ",[75,469,257],{"class":85},[75,471,376],{"class":81},[75,473,86],{"class":85},[75,475,415],{"class":89},[75,477,384],{"class":81},[75,479,86],{"class":85},[75,481,451],{"class":81},[75,483,96],{"class":85},[75,485,394],{"class":81},[75,487,86],{"class":85},[75,489,460],{"class":81},[75,491,493],{"class":77,"line":492},5,[75,494,279],{"emptyLinePlaceholder":278},[75,496,498,500,503,506,508,511,514,516,519,521,524,526],{"class":77,"line":497},6,[75,499,285],{"class":284},[75,501,502],{"class":85}," new",[75,504,505],{"class":81}," Cesium",[75,507,86],{"class":85},[75,509,510],{"class":89},"Rectangle",[75,512,513],{"class":81},"(west",[75,515,96],{"class":85},[75,517,518],{"class":81}," south",[75,520,96],{"class":85},[75,522,523],{"class":81}," east",[75,525,96],{"class":85},[75,527,528],{"class":81}," north)\n",[18,530,531],{},"当前版本不处理跨国际日期变更线的特殊矩形，适合常规局部地形提取。",[14,533,534],{"id":534},"最高层级采样怎么做",[18,536,537],{},"参考文章里的核心思想是“先找矩形范围内最高可用地形瓦片，再把高度写进高度图”。这里为了保持 Demo 稳定，使用 Cesium 公开 API 获取范围内整体可用最高层级：",[66,539,541],{"className":68,"code":540,"language":70,"meta":71,"style":71},"const availability = terrainProvider?.availability\nconst level = availability?.computeBestAvailableLevelOverRectangle?.(rectangle)\n\nif (Number.isFinite(level)) {\n  return Math.max(0, Math.floor(level))\n}\n",[28,542,543,561,583,587,606,641],{"__ignoreMap":71},[75,544,545,547,550,552,555,558],{"class":77,"line":78},[75,546,251],{"class":250},[75,548,549],{"class":81}," availability ",[75,551,257],{"class":85},[75,553,554],{"class":81}," terrainProvider",[75,556,557],{"class":85},"?.",[75,559,560],{"class":81},"availability\n",[75,562,563,565,568,570,573,575,578,580],{"class":77,"line":130},[75,564,251],{"class":250},[75,566,567],{"class":81}," level ",[75,569,257],{"class":85},[75,571,572],{"class":81}," availability",[75,574,557],{"class":85},[75,576,577],{"class":89},"computeBestAvailableLevelOverRectangle",[75,579,557],{"class":85},[75,581,582],{"class":81},"(rectangle)\n",[75,584,585],{"class":77,"line":167},[75,586,279],{"emptyLinePlaceholder":278},[75,588,589,592,595,597,600,603],{"class":77,"line":320},[75,590,591],{"class":284},"if",[75,593,594],{"class":81}," (Number",[75,596,86],{"class":85},[75,598,599],{"class":89},"isFinite",[75,601,602],{"class":81},"(level)) ",[75,604,605],{"class":85},"{\n",[75,607,608,611,613,615,617,620,624,626,628,630,633,635,638],{"class":77,"line":492},[75,609,610],{"class":284},"  return",[75,612,376],{"class":81},[75,614,86],{"class":85},[75,616,415],{"class":89},[75,618,117],{"class":619},"swJcz",[75,621,623],{"class":622},"sbssI","0",[75,625,96],{"class":85},[75,627,376],{"class":81},[75,629,86],{"class":85},[75,631,632],{"class":89},"floor",[75,634,117],{"class":619},[75,636,637],{"class":81},"level",[75,639,640],{"class":619},"))\n",[75,642,643],{"class":77,"line":497},[75,644,645],{"class":85},"}\n",[18,647,648,649,652],{},"拿到层级后，模块按矩形宽高比生成规则采样网格，最大边默认限制为 ",[28,650,651],{},"96"," 个采样点，防止一次请求过大范围时拖垮页面。",[66,654,656],{"className":68,"code":655,"language":70,"meta":71,"style":71},"const { columns, rows } = resolveGridSize(Cesium, rectangle, maxGridSide)\nconst positions = createGridPositions(Cesium, rectangle, columns, rows)\n\nawait Cesium.sampleTerrain(terrainProvider, level, positions, false)\n",[28,657,658,695,722,726],{"__ignoreMap":71},[75,659,660,662,665,668,670,673,676,679,682,685,687,690,692],{"class":77,"line":78},[75,661,251],{"class":250},[75,663,664],{"class":85}," {",[75,666,667],{"class":81}," columns",[75,669,96],{"class":85},[75,671,672],{"class":81}," rows ",[75,674,675],{"class":85},"}",[75,677,678],{"class":85}," =",[75,680,681],{"class":89}," resolveGridSize",[75,683,684],{"class":81},"(Cesium",[75,686,96],{"class":85},[75,688,689],{"class":81}," rectangle",[75,691,96],{"class":85},[75,693,694],{"class":81}," maxGridSide)\n",[75,696,697,699,702,704,707,709,711,713,715,717,719],{"class":77,"line":130},[75,698,251],{"class":250},[75,700,701],{"class":81}," positions ",[75,703,257],{"class":85},[75,705,706],{"class":89}," createGridPositions",[75,708,684],{"class":81},[75,710,96],{"class":85},[75,712,689],{"class":81},[75,714,96],{"class":85},[75,716,667],{"class":81},[75,718,96],{"class":85},[75,720,721],{"class":81}," rows)\n",[75,723,724],{"class":77,"line":167},[75,725,279],{"emptyLinePlaceholder":278},[75,727,728,731,733,735,738,741,743,746,748,751,753,757],{"class":77,"line":320},[75,729,730],{"class":284},"await",[75,732,505],{"class":81},[75,734,86],{"class":85},[75,736,737],{"class":89},"sampleTerrain",[75,739,740],{"class":81},"(terrainProvider",[75,742,96],{"class":85},[75,744,745],{"class":81}," level",[75,747,96],{"class":85},[75,749,750],{"class":81}," positions",[75,752,96],{"class":85},[75,754,756],{"class":755},"sfNiH"," false",[75,758,127],{"class":81},[18,760,761,764,765,768],{},[28,762,763],{},"sampleTerrain()"," 会按瓦片组织请求，并调用对应 ",[28,766,767],{},"TerrainData.interpolateHeight()"," 得到每个经纬度点的地形高度。局部缺失的高度会用邻域高度补齐，保证后续 canvas 和网格不会出现空洞。",[14,770,772],{"id":771},"高度图-canvas-怎么生成","高度图 canvas 怎么生成",[18,774,775,777,778,781],{},[28,776,42],{}," 会根据矩形投影宽高比创建 canvas，最大边默认 ",[28,779,780],{},"512px","。每个像素通过双线性插值从采样网格中读取高度，再按当前范围内的最小值和最大值归一化成灰度。",[66,783,785],{"className":68,"code":784,"language":70,"meta":71,"style":71},"const height = sampleHeightBilinear(sampleResult, u, v)\nconst intensity = normalizeHeight(height, sampleResult.minHeight, sampleResult.heightRange)\nconst value = clampInteger(intensity * 255, 0, 255)\n",[28,786,787,812,846],{"__ignoreMap":71},[75,788,789,791,794,796,799,802,804,807,809],{"class":77,"line":78},[75,790,251],{"class":250},[75,792,793],{"class":81}," height ",[75,795,257],{"class":85},[75,797,798],{"class":89}," sampleHeightBilinear",[75,800,801],{"class":81},"(sampleResult",[75,803,96],{"class":85},[75,805,806],{"class":81}," u",[75,808,96],{"class":85},[75,810,811],{"class":81}," v)\n",[75,813,814,816,819,821,824,827,829,832,834,837,839,841,843],{"class":77,"line":130},[75,815,251],{"class":250},[75,817,818],{"class":81}," intensity ",[75,820,257],{"class":85},[75,822,823],{"class":89}," normalizeHeight",[75,825,826],{"class":81},"(height",[75,828,96],{"class":85},[75,830,831],{"class":81}," sampleResult",[75,833,86],{"class":85},[75,835,836],{"class":81},"minHeight",[75,838,96],{"class":85},[75,840,831],{"class":81},[75,842,86],{"class":85},[75,844,845],{"class":81},"heightRange)\n",[75,847,848,850,853,855,858,861,864,867,869,872,874,876],{"class":77,"line":167},[75,849,251],{"class":250},[75,851,852],{"class":81}," value ",[75,854,257],{"class":85},[75,856,857],{"class":89}," clampInteger",[75,859,860],{"class":81},"(intensity ",[75,862,863],{"class":85},"*",[75,865,866],{"class":622}," 255",[75,868,96],{"class":85},[75,870,871],{"class":622}," 0",[75,873,96],{"class":85},[75,875,866],{"class":622},[75,877,127],{"class":81},[18,879,880,881,884],{},"这里输出的是当前范围内的相对灰度高度图：黑色代表当前范围内较低区域，白色代表较高区域。左下角面板直接挂载这张 canvas，场景中的起伏面也把同一张 canvas 当作 ",[28,882,883],{},"Image"," 材质使用。",[14,886,887],{"id":887},"起伏面怎么创建",[18,889,890,891,894,895,898],{},"场景面不是贴地影像，也不是 ",[28,892,893],{},"GroundPrimitive","。它是一个真实三角网格：每个采样点都会转换成带高度的 ",[28,896,897],{},"Cartesian3","，并按规则网格生成三角索引。",[66,900,902],{"className":68,"code":901,"language":70,"meta":71,"style":71},"const height = sampleResult.heights[index] + heightOffset\nconst cartesian = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, height)\n\npositions[positionOffset] = cartesian.x\npositions[positionOffset + 1] = cartesian.y\npositions[positionOffset + 2] = cartesian.z\n",[28,903,904,925,966,970,985,1007],{"__ignoreMap":71},[75,905,906,908,910,912,914,916,919,922],{"class":77,"line":78},[75,907,251],{"class":250},[75,909,793],{"class":81},[75,911,257],{"class":85},[75,913,831],{"class":81},[75,915,86],{"class":85},[75,917,918],{"class":81},"heights[index] ",[75,920,921],{"class":85},"+",[75,923,924],{"class":81}," heightOffset\n",[75,926,927,929,932,934,936,938,940,942,945,948,950,952,954,957,959,961,963],{"class":77,"line":130},[75,928,251],{"class":250},[75,930,931],{"class":81}," cartesian ",[75,933,257],{"class":85},[75,935,505],{"class":81},[75,937,86],{"class":85},[75,939,897],{"class":81},[75,941,86],{"class":85},[75,943,944],{"class":89},"fromRadians",[75,946,947],{"class":81},"(cartographic",[75,949,86],{"class":85},[75,951,389],{"class":81},[75,953,96],{"class":85},[75,955,956],{"class":81}," cartographic",[75,958,86],{"class":85},[75,960,451],{"class":81},[75,962,96],{"class":85},[75,964,965],{"class":81}," height)\n",[75,967,968],{"class":77,"line":167},[75,969,279],{"emptyLinePlaceholder":278},[75,971,972,975,977,980,982],{"class":77,"line":320},[75,973,974],{"class":81},"positions[positionOffset] ",[75,976,257],{"class":85},[75,978,979],{"class":81}," cartesian",[75,981,86],{"class":85},[75,983,984],{"class":81},"x\n",[75,986,987,990,992,995,998,1000,1002,1004],{"class":77,"line":492},[75,988,989],{"class":81},"positions[positionOffset ",[75,991,921],{"class":85},[75,993,994],{"class":622}," 1",[75,996,997],{"class":81},"] ",[75,999,257],{"class":85},[75,1001,979],{"class":81},[75,1003,86],{"class":85},[75,1005,1006],{"class":81},"y\n",[75,1008,1009,1011,1013,1016,1018,1020,1022,1024],{"class":77,"line":497},[75,1010,989],{"class":81},[75,1012,921],{"class":85},[75,1014,1015],{"class":622}," 2",[75,1017,997],{"class":81},[75,1019,257],{"class":85},[75,1021,979],{"class":81},[75,1023,86],{"class":85},[75,1025,1026],{"class":81},"z\n",[18,1028,1029],{},"纹理坐标按西到东、北到南映射，保证 canvas 高度图和矩形范围方向一致。",[66,1031,1033],{"className":68,"code":1032,"language":70,"meta":71,"style":71},"textureCoordinates[stOffset] = columnRatio\ntextureCoordinates[stOffset + 1] = 1 - rowRatio\n",[28,1034,1035,1045],{"__ignoreMap":71},[75,1036,1037,1040,1042],{"class":77,"line":78},[75,1038,1039],{"class":81},"textureCoordinates[stOffset] ",[75,1041,257],{"class":85},[75,1043,1044],{"class":81}," columnRatio\n",[75,1046,1047,1050,1052,1054,1056,1058,1060,1063],{"class":77,"line":130},[75,1048,1049],{"class":81},"textureCoordinates[stOffset ",[75,1051,921],{"class":85},[75,1053,994],{"class":622},[75,1055,997],{"class":81},[75,1057,257],{"class":85},[75,1059,994],{"class":622},[75,1061,1062],{"class":85}," -",[75,1064,1065],{"class":81}," rowRatio\n",[18,1067,1068,1069,1072,1073,1076],{},"最后用 ",[28,1070,1071],{},"GeometryPipeline.computeNormal()"," 计算法线，再交给 ",[28,1074,1075],{},"MaterialAppearance + Image material"," 渲染。",[66,1078,1080],{"className":68,"code":1079,"language":70,"meta":71,"style":71},"return Cesium.GeometryPipeline.computeNormal(geometry)\n",[28,1081,1082],{"__ignoreMap":71},[75,1083,1084,1086,1088,1090,1093,1095,1098],{"class":77,"line":78},[75,1085,285],{"class":284},[75,1087,505],{"class":81},[75,1089,86],{"class":85},[75,1091,1092],{"class":81},"GeometryPipeline",[75,1094,86],{"class":85},[75,1096,1097],{"class":89},"computeNormal",[75,1099,1100],{"class":81},"(geometry)\n",[18,1102,1103],{},"这个面已经拥有自己的地形高度顶点，所以即使后续隐藏或替换 Cesium 地形，它仍然保留原本采样到的凹凸起伏。",[18,1105,1106],{},"为了避免生成面像半透明覆盖层一样透视底下地形，canvas 输出使用满 alpha，材质也走不透明渲染并开启深度写入。",[66,1108,1110],{"className":68,"code":1109,"language":70,"meta":71,"style":71},"return Cesium.Material.fromType('Image', {\n  image: canvas,\n  repeat: new Cesium.Cartesian2(1, 1),\n  color: Cesium.Color.WHITE,\n  transparent: false\n})\n",[28,1111,1112,1141,1155,1184,1205,1215],{"__ignoreMap":71},[75,1113,1114,1116,1118,1120,1123,1125,1128,1130,1132,1134,1136,1138],{"class":77,"line":78},[75,1115,285],{"class":284},[75,1117,505],{"class":81},[75,1119,86],{"class":85},[75,1121,1122],{"class":81},"Material",[75,1124,86],{"class":85},[75,1126,1127],{"class":89},"fromType",[75,1129,117],{"class":81},[75,1131,106],{"class":85},[75,1133,883],{"class":102},[75,1135,106],{"class":85},[75,1137,96],{"class":85},[75,1139,1140],{"class":85}," {\n",[75,1142,1143,1146,1149,1152],{"class":77,"line":130},[75,1144,1145],{"class":619},"  image",[75,1147,1148],{"class":85},":",[75,1150,1151],{"class":81}," canvas",[75,1153,1154],{"class":85},",\n",[75,1156,1157,1160,1162,1164,1166,1168,1171,1173,1176,1178,1180,1182],{"class":77,"line":167},[75,1158,1159],{"class":619},"  repeat",[75,1161,1148],{"class":85},[75,1163,502],{"class":85},[75,1165,505],{"class":81},[75,1167,86],{"class":85},[75,1169,1170],{"class":89},"Cartesian2",[75,1172,117],{"class":81},[75,1174,1175],{"class":622},"1",[75,1177,96],{"class":85},[75,1179,994],{"class":622},[75,1181,109],{"class":81},[75,1183,1154],{"class":85},[75,1185,1186,1189,1191,1193,1195,1198,1200,1203],{"class":77,"line":320},[75,1187,1188],{"class":619},"  color",[75,1190,1148],{"class":85},[75,1192,505],{"class":81},[75,1194,86],{"class":85},[75,1196,1197],{"class":81},"Color",[75,1199,86],{"class":85},[75,1201,1202],{"class":81},"WHITE",[75,1204,1154],{"class":85},[75,1206,1207,1210,1212],{"class":77,"line":492},[75,1208,1209],{"class":619},"  transparent",[75,1211,1148],{"class":85},[75,1213,1214],{"class":755}," false\n",[75,1216,1217,1219],{"class":77,"line":497},[75,1218,675],{"class":85},[75,1220,127],{"class":81},[66,1222,1224],{"className":68,"code":1223,"language":70,"meta":71,"style":71},"appearance: new Cesium.MaterialAppearance({\n  material,\n  materialSupport: Cesium.MaterialAppearance.MaterialSupport.TEXTURED,\n  translucent: false,\n  faceForward: true,\n  renderState: {\n    depthTest: { enabled: true },\n    depthMask: true,\n    cull: { enabled: false }\n  }\n})\n",[28,1225,1226,1247,1254,1279,1290,1302,1311,1331,1343,1362,1368],{"__ignoreMap":71},[75,1227,1228,1232,1234,1236,1238,1240,1243,1245],{"class":77,"line":78},[75,1229,1231],{"class":1230},"sBMFI","appearance",[75,1233,1148],{"class":85},[75,1235,502],{"class":85},[75,1237,505],{"class":81},[75,1239,86],{"class":85},[75,1241,1242],{"class":89},"MaterialAppearance",[75,1244,117],{"class":81},[75,1246,605],{"class":85},[75,1248,1249,1252],{"class":77,"line":130},[75,1250,1251],{"class":81},"  material",[75,1253,1154],{"class":85},[75,1255,1256,1259,1261,1263,1265,1267,1269,1272,1274,1277],{"class":77,"line":167},[75,1257,1258],{"class":619},"  materialSupport",[75,1260,1148],{"class":85},[75,1262,505],{"class":81},[75,1264,86],{"class":85},[75,1266,1242],{"class":81},[75,1268,86],{"class":85},[75,1270,1271],{"class":81},"MaterialSupport",[75,1273,86],{"class":85},[75,1275,1276],{"class":81},"TEXTURED",[75,1278,1154],{"class":85},[75,1280,1281,1284,1286,1288],{"class":77,"line":320},[75,1282,1283],{"class":619},"  translucent",[75,1285,1148],{"class":85},[75,1287,756],{"class":755},[75,1289,1154],{"class":85},[75,1291,1292,1295,1297,1300],{"class":77,"line":492},[75,1293,1294],{"class":619},"  faceForward",[75,1296,1148],{"class":85},[75,1298,1299],{"class":755}," true",[75,1301,1154],{"class":85},[75,1303,1304,1307,1309],{"class":77,"line":497},[75,1305,1306],{"class":619},"  renderState",[75,1308,1148],{"class":85},[75,1310,1140],{"class":85},[75,1312,1314,1317,1319,1321,1324,1326,1328],{"class":77,"line":1313},7,[75,1315,1316],{"class":619},"    depthTest",[75,1318,1148],{"class":85},[75,1320,664],{"class":85},[75,1322,1323],{"class":619}," enabled",[75,1325,1148],{"class":85},[75,1327,1299],{"class":755},[75,1329,1330],{"class":85}," },\n",[75,1332,1334,1337,1339,1341],{"class":77,"line":1333},8,[75,1335,1336],{"class":619},"    depthMask",[75,1338,1148],{"class":85},[75,1340,1299],{"class":755},[75,1342,1154],{"class":85},[75,1344,1346,1349,1351,1353,1355,1357,1359],{"class":77,"line":1345},9,[75,1347,1348],{"class":619},"    cull",[75,1350,1148],{"class":85},[75,1352,664],{"class":85},[75,1354,1323],{"class":619},[75,1356,1148],{"class":85},[75,1358,756],{"class":755},[75,1360,1361],{"class":85}," }\n",[75,1363,1365],{"class":77,"line":1364},10,[75,1366,1367],{"class":85},"  }\n",[75,1369,1371,1373],{"class":77,"line":1370},11,[75,1372,675],{"class":85},[75,1374,127],{"class":81},[14,1376,1377],{"id":1377},"地形深度测试的作用",[18,1379,1380,1382],{},[28,1381,160],{}," 按钮只切换一个 Cesium 场景状态：",[66,1384,1386],{"className":68,"code":1385,"language":70,"meta":71,"style":71},"viewer.scene.globe.depthTestAgainstTerrain = nextValue\n",[28,1387,1388],{"__ignoreMap":71},[75,1389,1390,1393,1395,1397,1399,1401,1403,1406,1408],{"class":77,"line":78},[75,1391,1392],{"class":81},"viewer",[75,1394,86],{"class":85},[75,1396,292],{"class":81},[75,1398,86],{"class":85},[75,1400,297],{"class":81},[75,1402,86],{"class":85},[75,1404,1405],{"class":81},"depthTestAgainstTerrain ",[75,1407,257],{"class":85},[75,1409,1410],{"class":81}," nextValue\n",[18,1412,1413],{},"这个 Demo 默认开启地形深度测试，让生成面一开始就和真实地形保持正确遮挡关系。",[66,1415,1417],{"className":68,"code":1416,"language":70,"meta":71,"style":71},"viewer.scene.globe.depthTestAgainstTerrain = true\n",[28,1418,1419],{"__ignoreMap":71},[75,1420,1421,1423,1425,1427,1429,1431,1433,1435,1437],{"class":77,"line":78},[75,1422,1392],{"class":81},[75,1424,86],{"class":85},[75,1426,292],{"class":81},[75,1428,86],{"class":85},[75,1430,297],{"class":81},[75,1432,86],{"class":85},[75,1434,1405],{"class":81},[75,1436,257],{"class":85},[75,1438,1439],{"class":755}," true\n",[18,1441,1442,1443,1446],{},"关闭时，高度图面更容易完整观察；开启时，真实地形会参与深度遮挡，可以用来判断生成面是否确实贴近当前地形。为了减少完全共面导致的闪烁，生成面只增加 ",[28,1444,1445],{},"0.35m"," 的显示偏移，高度变化仍来自真实采样结果。",[14,1448,1449],{"id":1449},"地形加载开关",[18,1451,1452,1454,1455,1458],{},[28,1453,197],{}," 按钮用于在 Cesium World Terrain 和椭球地形之间切换。取消加载时只替换 ",[28,1456,1457],{},"viewer.terrainProvider","，不会销毁已经生成的高度图 Primitive，所以可以验证这个面是否真的拥有自己的高度顶点。",[66,1460,1462],{"className":68,"code":1461,"language":70,"meta":71,"style":71},"viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider()\nviewer.scene.globe.enableLighting = false\n",[28,1463,1464,1487],{"__ignoreMap":71},[75,1465,1466,1468,1470,1473,1475,1477,1479,1481,1484],{"class":77,"line":78},[75,1467,1392],{"class":81},[75,1469,86],{"class":85},[75,1471,1472],{"class":81},"terrainProvider ",[75,1474,257],{"class":85},[75,1476,502],{"class":85},[75,1478,505],{"class":81},[75,1480,86],{"class":85},[75,1482,1483],{"class":89},"EllipsoidTerrainProvider",[75,1485,1486],{"class":81},"()\n",[75,1488,1489,1491,1493,1495,1497,1499,1501,1504,1506],{"class":77,"line":130},[75,1490,1392],{"class":81},[75,1492,86],{"class":85},[75,1494,292],{"class":81},[75,1496,86],{"class":85},[75,1498,297],{"class":81},[75,1500,86],{"class":85},[75,1502,1503],{"class":81},"enableLighting ",[75,1505,257],{"class":85},[75,1507,1214],{"class":755},[18,1509,1510,1511,1514],{},"重新加载时会复用已经创建过的 World Terrain provider；如果还没有缓存，则再次调用 ",[28,1512,1513],{},"createWorldTerrainAsync()","。",[14,1516,1517],{"id":1517},"生命周期清理",[18,1519,1520],{},"Demo 销毁时会统一清理绘制事件、结果 Primitive、范围矩形、左下角面板和 Cesium Viewer。",[66,1522,1524],{"className":68,"code":1523,"language":70,"meta":71,"style":71},"drawTool.destroy()\nclearPreviousResult()\nheightmapPanel.destroy()\n\nif (!viewer.isDestroyed()) {\n  viewer.destroy()\n}\n",[28,1525,1526,1538,1545,1556,1560,1582,1592],{"__ignoreMap":71},[75,1527,1528,1531,1533,1536],{"class":77,"line":78},[75,1529,1530],{"class":81},"drawTool",[75,1532,86],{"class":85},[75,1534,1535],{"class":89},"destroy",[75,1537,1486],{"class":81},[75,1539,1540,1543],{"class":77,"line":130},[75,1541,1542],{"class":89},"clearPreviousResult",[75,1544,1486],{"class":81},[75,1546,1547,1550,1552,1554],{"class":77,"line":167},[75,1548,1549],{"class":81},"heightmapPanel",[75,1551,86],{"class":85},[75,1553,1535],{"class":89},[75,1555,1486],{"class":81},[75,1557,1558],{"class":77,"line":320},[75,1559,279],{"emptyLinePlaceholder":278},[75,1561,1562,1564,1567,1570,1572,1574,1577,1580],{"class":77,"line":492},[75,1563,591],{"class":284},[75,1565,1566],{"class":81}," (",[75,1568,1569],{"class":85},"!",[75,1571,1392],{"class":81},[75,1573,86],{"class":85},[75,1575,1576],{"class":89},"isDestroyed",[75,1578,1579],{"class":81},"()) ",[75,1581,605],{"class":85},[75,1583,1584,1586,1588,1590],{"class":77,"line":497},[75,1585,323],{"class":81},[75,1587,86],{"class":85},[75,1589,1535],{"class":89},[75,1591,1486],{"class":619},[75,1593,1594],{"class":77,"line":1313},[75,1595,645],{"class":85},[18,1597,1598,1599,1602,1603,1606],{},"这一点对内容站很重要：Demo 详情页通过路由切换反复挂载和卸载，如果不清理 ",[28,1600,1601],{},"ScreenSpaceEventHandler"," 或 ",[28,1604,1605],{},"Primitive","，很容易出现旧事件残留和显存占用增长。",[1608,1609,1610],"style",{},"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 .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}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 .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}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 .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}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}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"title":71,"searchDepth":130,"depth":130,"links":1612},[1613,1614,1615,1616,1617,1618,1619,1620,1621],{"id":16,"depth":130,"text":16},{"id":61,"depth":130,"text":61},{"id":233,"depth":130,"text":233},{"id":534,"depth":130,"text":534},{"id":771,"depth":130,"text":772},{"id":887,"depth":130,"text":887},{"id":1377,"depth":130,"text":1377},{"id":1449,"depth":130,"text":1449},{"id":1517,"depth":130,"text":1517},"2026-04-24","记录 Cesium 高度图 Demo 的 SDK 化实现，包括矩形绘制、最高可用地形层级解析、规则网格采样、灰度高度图生成、起伏 Primitive 构建和地形深度测试。","md","\u002Fimages\u002Fdemos\u002Fcesium-terrain-heightmap.svg",{},"\u002Fblog\u002Fcesium-terrain-heightmap-implementation",{"title":5,"description":1623},"blog\u002Fcesium-terrain-heightmap-implementation","KyVABdfDlZEGPAL_8GNhcxau8iEl3qNa87P4VHHDKX8",[1632,1637],{"title":1633,"path":1634,"stem":1635,"description":1636,"children":-1},"Cesium GeoJSON 热力图 SDK 实现说明","\u002Fblog\u002Fcesium-geojson-heatmap-sdk-implementation","blog\u002Fcesium-geojson-heatmap-sdk-implementation","记录通用 GeoJSON 热力图 SDK 的实现逻辑，包括 Cesium runtime 单例、GeoJSON 点数据解析、canvas 热力纹理生成、GroundPrimitive 分类渲染、世界地形接入和 OSM Buildings 测试场景管理。",null,1777372185303]