实体贴地(Ground Clamp)技术方案
状态:P0 贴地线与 P1 贴地面已实现;点贴地、深度源分离与正向偏移仍在路线图中(2026-07-30 收紧稳定 API,见 §4)。 范围:点 / 线 / 面实体的真·贴地渲染,对标 Cesium
GroundPrimitive/GroundPolylinePrimitive。 渲染器:WebGL 优先全量实现;WebGPU 支持暂不处理,后续单独立项。
0. 背景与决策
0.1 现状
实体位置由 Viewer.cartographicToVector3 经椭球 getCartographicToPosition 落点,输入里的 height 米数即最终绝对椭球高,无任何地形跟随。材质统一 depthWrite: false(PolygonGraphic、PolylineGraphic、EntityRenderManager)。
0.2 被否决的方案:CPU 采样重建
"沿几何采样地形高程 → 重建几何"的妥协路线被否决,原因:
- terrain/3D Tiles 是流式 LOD,相机移动、新瓦片载入后采样即过时,需要反复重采样重建,面几何每次重建代价高;
- 仍会出现穿山 / 浮空,满足不了真贴地需求。
0.3 采用的方案:GPU 阴影体 + 深度纹理逐片元分类
完全对标 Cesium:CPU 建静态"分类体(shadow volume)"几何,渲染时在片元着色器里读主场景深度纹理、还原地表点、逐像素判定该点是否落在图元 footprint 内。几何不随地形 LOD 重建。
该方案在 tellux 可行且不从零搭,因为:
| Cesium 依赖 | tellux 现状 |
|---|---|
czm_globeDepthTexture(地形+瓦片深度) | postprocessing EffectComposer 的 readBuffer.depthTexture,主场景(terrain + 3D Tiles + 不透明实体)已渲入其中 |
| 片元读深度 + 改写材质注入分类逻辑 | EntityRenderManager 的 OIT pass 已用 onBeforeCompile + telluxSceneDepth uniform 读 readBuffer.depthTexture 做 telluxDepthDiscard(L336-357) |
| 多 pass 编排 | effects 链 +EntityRenderManager 已是多 pass 范式 |
| tile 材质打 stencil bit | 已有 tile 材质插件机制(TilesetModelPlugins、WebGPUTerrainOverlayPlugin)可 patch 材质 |
即:贴地分类 pass 与 OIT pass 同构——读 readBuffer.depthTexture、用自定义材质渲一组几何、把分类结果合成回主色。
0.4 关键约束
- WebGPU 下
onBeforeCompileGLSL 注入失效(已记录坑点)。本方案的材质注入强依赖该机制,故 WebGL 优先;WebGPU 需改用 TSL/WGSL 原生着色器,本期不做。 - 点实体未来如支持贴地,应走 CPU 高度采样(见 §3.7)。这是 Cesium 对点 / 布告板 / 标签 / 模型的常用路径;完成生命周期与 LOD 收敛设计前,该能力不进入稳定公开类型。线 / 面使用 GPU 分类。
1. Cesium 实现回顾(决策依据)
源码版本 1.136,路径
D:/dev_work/gis-template/node_modules/cesium/Build/CesiumUnminified/。
1.1 贴地线:静态墙体积 + 逐片元分类
- 几何(
Workers/createGroundPolylineGeometry.js):沿大地线 / 恒向线按granularity(默认 9999m)细分,不采样地形;每段生成一个minHeight..maxHeight(0..1000m)的墙立方体(8 顶点 36 索引,REFERENCE_INDICESL1075)。min/maxHeight仅来自超粗全局表ApproximateTerrainHeights(6 级 ≈1°),只用于撑大包围球、保证墙体包住地形范围(L1434),不参与真实贴合。 - 顶点属性:
startHi/startLo(EncodedCartesian3高 / 低双精度拆分)、forwardOffset、startPlaneNormal、endPlaneNormal、rightNormal、texcoord 归一化(L1130-1558)。 - 渲染(
Cesium.jsGroundPolylinePrimitive/PolylineShadowVolumeFSL57231,passTERRAIN_CLASSIFICATION=3):glslfloat d = czm_unpackDepth(texture(czm_globeDepthTexture, gl_FragCoord.xy / czm_viewport.zw)); if (d == 0.0) discard; // 天空丢弃 vec4 eye = czm_windowToEyeCoordinates(gl_FragCoord.xy, d); // 用当前线段的 右平面 / 起点平面 / 终点平面 + 半宽 判定地表点是否在线条 footprint 内 if (abs(planeDistance(v_rightPlaneEC, eye.xyz)) > halfWidth) discard; if (planeDistance(v_startPlane..., eye.xyz) < 0.0) discard; if (planeDistance(v_endPlane..., eye.xyz) < 0.0) discard; // 命中 → 输出颜色,czm_depthClamp() 让墙体在地下也参与 czm_depthClamp保证墙体被地形遮挡时片元仍参与计算;墙体几何只提供"哪些像素运行 FS"+"平面属性",可见结果完全由重建的地表点决定。
1.2 贴地面:模板阴影体两遍
GroundPrimitive(= _extruded:true 的 ClassificationPrimitive,L54413)每几何两条命令:
| pass | colorMask | 模板 | 作用 |
|---|---|---|---|
| stencil-depth(L53611) | 全 false | zFail: 前 DECREMENT_WRAP / 后 INCREMENT_WRAP | 阴影体打掩码:地形挡住墙体处掩码 ≠0 = "在体内且可见" |
| color(L53645) | 写色 | NOT_EQUAL 0,命中后 ops 全 ZERO 清掩码 | 只给掩码 ≠0 的像素着色 |
color pass 的 ShadowVolumeAppearanceFS(L52658)同样读深度还原世界坐标,并用 vectorFromOffset 采样相邻像素深度重建地表法线用于打光。
1.3 3D Tiles 分类:同几何 + 换 pass / 模板 / 深度源
3D Tiles 画时 REPLACE 写模板位 128(CESIUM_3D_TILE_MASK),分类命令在 CESIUM_3D_TILE_CLASSIFICATION(pass=6)用 EQUAL 模板门控、深度源换为瓦片打包深度。同一份分类体,仅换 pass + 渲染状态 + 深度来源——对应 tellux HeightSampler.source 的 terrain | tileset | all。
1.4 heightReference:CPU 采样(点 / 布告板 / 模型)
CLAMP_TO_GROUND = 清零高度偏移;RELATIVE_TO_GROUND = 采样地表高 + 偏移。CPU 改 modelMatrix / 位置属性,与 GroundPrimitive 着色器无关。
1.5 两个值得落实的工程细节
EncodedCartesian3高 / 低双精度:顶点位置拆high + low(Float32 + Float32),VS 里vec3 p = high.xyz + low.xyz还原,对抗地球尺度 Float32 抖动。当前 PolylineGraphic 用LineGeometry.setPositions(Float32),长贴地线必抖。- 法向微偏移(normal nudge)治 z-fighting:墙体前 / 后两面沿
rightNormal各偏移±EPSILON5(L1458-1489)+nudgeXZ(L1014)处理日期变更线,而非简单抬高度(抬高度会破坏贴地语义)。
2. 总体架构
postprocessing EffectComposer 链
├─ [主场景渲染] terrain + 3D Tiles + 不透明实体 → readBuffer(含 depthTexture)
├─ EntityRenderManager(OIT,已有)
├─ GroundClampPass(新增) ─────────────────────────────┐
│ in: readBuffer.texture(主色) + readBuffer.depthTexture(地表深度)
│ 渲: groundClampRoot(贴地线墙体积 + 贴地面阴影体,分类材质)
│ 出: writeBuffer(主色 ⊕ 分类着色)
├─ 云 / 大气 EffectPass(可能 swap;之后的 readBuffer 可能无深度)
├─ EntityRenderManager(透明实体 OIT,位于大气之后,就地叠加)
├─ SymbolOcclusionPass
└─ LensFlare → SMAA → Dithering数据流(分类片元):
gl_FragCoord → 采样 depthTexture → 窗口深度 z
→ NDC → clip → inverseProjection → 眼坐标 eye
→ (inverseView → 世界坐标 world,面/打光用)
→ 平面测试(线:右/起/终平面 + 半宽;面:阴影体模板已界定内外)
→ 命中:输出颜色(预乘 alpha);未命中:discard新增组件:
| 组件 | 职责 | 对标 Cesium |
|---|---|---|
GroundClampPass | effects pass:绑定深度、渲分类几何、合成 | TERRAIN_CLASSIFICATION pass 编排 |
GroundPolylineGraphic | 线墙体积几何 + 分类材质 | GroundPolylineGeometry + PolylineShadowVolumeFS |
GroundPolygonGraphic | 面阴影体几何 + 模板两遍材质 | PolygonGeometry.createShadowVolume + ShadowVolumeAppearanceFS |
EncodedCartesian3(util) | 高 / 低双精度拆分 | EncodedCartesian3 |
clamp 字段 | 稳定 API(线 / 面当前仅 boolean) | 取代 HeightReference 7 值枚举 |
点:复用HeightSampler | CPU 采样改点位置 | Billboard._updateClamping |
3. 组件设计
3.1 深度源与坐标还原
深度源:贴地分类位于大气之前,复用主场景的
readBuffer.depthTexture。clamped 实体depthWrite:false,不污染地表深度;该纹理所含即 terrain + 3D Tiles 深度(Phase 1 = union)。还原(ShaderMaterial 内置
projectionMatrix/viewMatrix):glslfloat z = texture2D(telluxGroundDepth, gl_FragCoord.xy / uResolution).r; // [0,1] 窗口深度 if (z >= 1.0) discard; // 天空(需按实际 clear 深度核实) vec4 clip = vec4(vec3(gl_FragCoord.xy / uResolution, z) * 2.0 - 1.0, 1.0); vec4 eye = inverseProjection * clip; eye /= eye.w; vec4 world = inverseView * eye; // 面片元打光用深度纹理类型 / 编码需与投影一致,验证
readBuffer.depthTexture的type(fixed vs HalfFloat)与是否启用 logarithmic depth。
3.2 贴地线几何(port createGroundPolylineGeometry)
- 沿椭球细分:大地线(
EllipsoidGeodesic)/ 恒向线(EllipsoidRhumbLine),granularity默认 9999m。tellux 可用现有椭球(tilesets.tileset.ellipsoid)。 - 每段墙体积:8 顶点 / 36 索引(直引
REFERENCE_INDICES),min/maxHeight撑包络。 min/maxHeight来源:Phase 1 用固定带 +HeightSampler粗采样(对标ApproximateTerrainHeights),仅作包围球与墙体高度,不参与贴合判定。- 顶点属性 port:
startHi/startLo、forwardOffset、start/end/right平面法向、texcoord 归一化(含 2D 属性可裁剪,tellux 仅 3D 模式)。 - 细节 1 落实:
startHi/startLo经EncodedCartesian3.fromCartesian拆分。 - 细节 2 落实:前 / 后两面沿
rightNormal各±EPSILON5偏移 +nudgeXZ。
3.3 贴地线分类材质(port PolylineShadowVolumeFS/VS)
- VS:用
startHi + startLo双精度还原段起点;由属性 +modelViewMatrix算眼空间rightPlaneEC / startPlaneEC / endPlaneEC与halfWidth作 varying。 - FS:§3.1 还原地表
eye;abs(planeDistance(rightPlane, eye)) <= halfWidth && planeDistance(startPlane) >= 0 && planeDistance(endPlane) >= 0否则 discard;命中输出预乘 alpha 颜色。 - 渲染状态:
depthTest:false(手动比深度)、depthWrite:false、blending: Normal或预乘 alpha;不需 stencil(单 pass 深度纹理分类,比 Cesium 更简,因始终有深度纹理)。
3.4 贴地面几何(每三角形棱柱)
已实现(P1,2026-07-03),与原设想的整体阴影体不同:外环投影到质心切平面,THREE.ShapeUtils.triangulateShape(earcut)三角化——凹多边形由三角化天然解决;每个三角形沿逐顶点椭球法向挤出一个棱柱分类体(6 顶点 / 8 三角形,绕序统一朝外),高度带与线一致(固定 [-1000, 9000] m,不采样地形)。相邻三角形共享顶点与法向,侧面严格贴合。见 GroundPolygonGraphic。
3.5 贴地面分类材质(单 pass 半平面测试,非模板两遍)
原方案(Cesium 模板两遍阴影体)被否决:P0 核实内置合成器的 render target stencilBuffer:false(renderer 未开 stencil),走模板需自管 RT + 深度共享,复杂度不成比例。采用原"备选简化"路线并解决其凹多边形难点:
- 凹多边形 point-in-polygon 不在片元里做——CPU earcut 后每片元只需判定所在三角形棱柱:FS 重建眼空间地表点,做三条边的外向半平面测试(
cross(边, up)),全部内侧即命中。 - 全多边形共用一个 up 向量(质心方向,uniform):共享边在相邻三角形里方向恰好相反,叉积精确反号 → 两侧测试严格互补,无缝隙无重叠。
side: BackSide(凸体每像素恰好一次 FS,相机在体内也正确)→ 支持半透明填充不重复混合;填充色的rgba(...)/#rrggbbaaalpha 生效(线同步改为 BackSide)。- 渲染状态与线一致:
depthTest:false、depthWrite:false、常规混合,由GroundClampPass统一渲染,无 stencil 依赖。extrudeHeight/outline留后续阶段。
3.6 GroundClampPass 编排
- 实现
ThreeEffectPass(同EntityRenderManager形态):render(renderer, writeBuffer, readBuffer)。 - 维护
groundClampRoot: THREE.Group,挂所有GroundPolylineGraphic/GroundPolygonGraphic的object3D。 - 绑定
readBuffer.depthTexture到分类材质的telluxGroundDepthuniform(同 OIT 的telluxSceneDepth注入范式)。 - 渲分类几何到
writeBuffer(主色 ⊕ 分类色);needsSwap = true。 - 插入位置:effects 链中、
EntityRenderManager邻近,但必须在云 / 大气合成之前。真实顺序为GroundClamp → Atmosphere → Entity OIT。透明实体必须在大气之后,否则大气天空分支会抹掉其地平线像素。 - 大气之后的深度规则:大气 pass 可能 swap 到无深度的 targetB。因此
EntityRenderManager及其他需要场景深度的后处理 pass 必须从readBuffer.depthTexture ?? writeBuffer.depthTexture两侧探测,不能只读readBuffer。
3.7 点:CPU 采样路线图
- 未来点贴地启用时,
Entityadd 发起HeightSampler.sampleHeightMostDetailed([pos], { source }),resolve 后PointGraphic.setPosition。 - add 立即用椭球高先摆出来,采样 resolve 后 snap(渐进式,同瓦片流式体验)。
- LOD 重采样:挂
TilesetSamplingAdapterreadiness 信号或update()周期去抖重采样(点仅 1 顶点,几乎免费)。 - 深度源与偏移的公开形状需在实现阶段重新评审;当前
PointOptions不暴露clamp。
3.8 实体集成
Entity构造:polyline.clamp === true→ 建GroundPolylineGraphic,加入groundClampRoot(非普通 entity root);面同理。EntityManager需持有groundClampRoot引用(由GroundClampPass提供 / 注入)。- 普通拾取(EntityPicker)对线 / 面走射线或屏幕投影——贴地几何为墙体积,射线命中语义需重新定义(建议按 footprint 投影拾取,复用现有
forEachSegment屏幕投影逻辑)。
4. API 设计
贴地本质是坐标的垂直定位属性,不是图形样式。稳定 API 当前只承诺已经实现的线 / 面布尔贴地:
export interface PolylineOptions {
clamp?: boolean
}
export interface PolygonOptions {
clamp?: boolean
}- 取值语义:
- 缺省 /
false→ 绝对椭球高(当前行为,替代旧NONE)。 true→ 使用主场景深度中 terrain 与 3D Tiles 的可见并集进行贴地。
- 缺省 /
未来的深度源分离、正向偏移和点贴地仍可采用分组对象或独立领域配置,但应在实现、测试、WebGL/WebGPU 降级语义和生命周期边界同时明确后再进入公开类型。不要提前暴露 { source, offset } 再由运行时 warning 降级。
4.1 路线图断崖:零偏移与正向偏移(线 / 面)
概念连续,实现不连续,必须显式处理:
| offset | 点 | 线 / 面 |
|---|---|---|
0(真贴地) | CPU 采样改点位 | GPU 影体分类(逐像素,不重建几何) |
> 0(抬离) | CPU 采样 + 偏移 | CPU 重采样抬升几何(采样间浮沉,后续阶段) |
- 点的偏移天然是「采样高程 + offset」,无断崖。
- 线 / 面当前只实现零偏移 GPU 分类;正向偏移尚未进入公开类型,因此不存在运行时 warning 降级分支。
4.2 polygon 的 height / extrudeHeight 交互
避免「字段被远程改写」:
clamp: true→ 底面 = 贴地面,height(绝对底高)被忽略;- 当前
extrudeHeight/outline会告警并忽略。未来若支持贴地拉伸,应单独评审“相对厚度”语义,不能沿用绝对顶高造成远程改写。
即「贴地 + 向上拉伸体块」是自然组合,比 Cesium GroundPrimitive 只能压平更灵活。
4.3 分期
- Phase 1 已实现
clamp: true(线 / 面真贴地)。 source: 'terrain' | 'tileset'依赖 §3.6 的分离深度快照(Phase 3)。offset > 0(RELATIVE 语义)为 Phase 4。- 未实现阶段不预占稳定类型;路线图字段完成后再扩展 API。
5. 分阶段计划(仅 WebGL)
| 阶段 | 交付 | 验收 |
|---|---|---|
| P0 深度分类管线 + 贴地线 | GroundClampPass 骨架、深度绑定、EncodedCartesian3、port GroundPolylineGeometry 墙体积、线分类材质、normal nudge、clamp: true(线,offset=0) | 线在山地上随地形起伏贴合,相机 / LOD 变化无穿山 / 浮空、无抖动 |
| P1 贴地面(已实现) | GroundPolygonGraphic:earcut 三角化 + 每三角形棱柱 + 单 pass 半平面分类(§3.4/3.5,非模板两遍)、半透明填充、clamp: true(面,offset=0) | 面贴合地形,凹多边形正确,相邻面无 z-fighting |
| P2 点 clamp | HeightSampler 接入 + LOD 重采样;公开 API 在实现阶段评审 | 点 snap 到地表,LOD 载入后位置收敛 |
| P3 terrain/tileset 分离 | 主渲染中插入 terrain-only 深度快照;深度源配置形状待评审 | 同一线可仅贴地形或仅贴建筑顶 |
| P4 正向偏移 + 打磨 | relative 语义、精度 / 性能(scissor、bounding)、与 OIT 交互厘清;配置形状待评审 | 偏移贴地稳定,大场景帧率达标 |
| 本期不做,后续单独立项(TSL/WGSL 原生着色器) | — |
细节 1(双精度)与细节 2(微偏移)随 P0 落实,贯穿后续。
6. 风险与开放问题
- 深度源完整性:
readBuffer.depthTexture是否含 3D Tiles 深度?需核实 ViewerRenderLoop 与3d-tiles-renderer渲染时序;若 3D Tiles 在独立 pass,Phase 1 的 union 深度需调整。 - 深度纹理格式 / log-depth:还原公式依赖深度编码;若管线启用 logarithmic depth,需走对应反演路径(对标
czm_reverseLogDepth)。 - MSAA:分类 pass 读深度需与主场景 MSAA 解析一致;postprocessing 链通常非 MSAA,需确认。
- 模板可用性:composer 目标需
stencilBuffer:true,否则面模板两遍需独立 stencil RT。 - 性能:全屏深度采样按分类片元计;用紧致包围球 +
renderer.setScissor裁剪到图元屏区缓解。 - 与 OIT 交互:贴地实体通常不透明;若半透明,分类色与 OIT 累加的先后 / 混合需厘清。
- 拾取语义:墙体积被射线命中 ≠ 用户意图;需按 footprint 重定义(§3.8)。
- granularity / 墙高默认值:tellux 场景尺度下需标定(Cesium 9999m / 0..1000m)。
7. 参考
Cesium 1.136(D:/dev_work/gis-template/node_modules/cesium/Build/CesiumUnminified/):
Workers/createGroundPolylineGeometry.js— 线墙体积几何Cesium.jsL52658ShadowVolumeAppearanceFS、L57231PolylineShadowVolumeFS、L53611/53645 模板 / color render state、L36379windowToEyeCoordinates、L54413GroundPrimitive、L57964GroundPolylinePrimitive.createCommands
tellux:
- EntityRenderManager.ts — OIT depth-reading pass 范式(
telluxDepthDiscardL351、材质注入 L336) - effects.ts —
ThreeEffectPass/ depthTexture 注入 - HeightSampler.ts — 点贴地路线图的高度采样能力
- EntityPicker.ts — 拾取
- PolylineGraphic.ts / PolygonGraphic.ts / PointGraphic.ts — 现有 graphic
- entities.ts —
EntityOptions/*Options类型