Skip to content

实体贴地(Ground Clamp)技术方案

状态:设计中,暂未实现(2026-07-01;API 于 2026-07-03 改版:§4 由 HeightReference 7 值枚举改为统一 clamp 字段) 范围:点 / 线 / 面实体的真·贴地渲染,对标 Cesium GroundPrimitive / GroundPolylinePrimitive。 渲染器:WebGL 优先全量实现;WebGPU 支持暂不处理,后续单独立项。


0. 背景与决策

0.1 现状

实体位置由 Viewer.cartographicToVector3 经椭球 getCartographicToPosition 落点,输入里的 height 米数即最终绝对椭球高,无任何地形跟随。材质统一 depthWrite: falsePolygonGraphicPolylineGraphicEntityRenderManager)。

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.depthTexturetelluxDepthDiscard(L336-357)
多 pass 编排effects 链 +EntityRenderManager 已是多 pass 范式
tile 材质打 stencil bit已有 tile 材质插件机制(TilesetModelPluginsWebGPUTerrainOverlayPlugin)可 patch 材质

即:贴地分类 pass 与 OIT pass 同构——读 readBuffer.depthTexture、用自定义材质渲一组几何、把分类结果合成回主色。

0.4 关键约束

  • WebGPU 下 onBeforeCompile GLSL 注入失效(已记录坑点)。本方案的材质注入强依赖该机制,故 WebGL 优先;WebGPU 需改用 TSL/WGSL 原生着色器,本期不做
  • 点实体走 CPU clamp 采样(见 §3.7)。这是 Cesium 对点 / 布告板 / 标签 / 模型的正解Model.updateClampingBillboard._updateClamping 均 CPU 采样改 modelMatrix),不是妥协。线 / 面才用 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_INDICES L1075)。min/maxHeight 仅来自超粗全局表 ApproximateTerrainHeights(6 级 ≈1°),只用于撑大包围球、保证墙体包住地形范围(L1434),不参与真实贴合。
  • 顶点属性startHi/startLoEncodedCartesian3 高 / 低双精度拆分)、forwardOffsetstartPlaneNormalendPlaneNormalrightNormal、texcoord 归一化(L1130-1558)。
  • 渲染Cesium.js GroundPolylinePrimitive / PolylineShadowVolumeFS L57231,pass TERRAIN_CLASSIFICATION=3):
    glsl
    float 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:trueClassificationPrimitive,L54413)每几何两条命令

passcolorMask模板作用
stencil-depth(L53611)全 falsezFail: 前 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.sourceterrain | tileset | all

1.4 heightReference:CPU 采样(点 / 布告板 / 模型)

CLAMP_TO_GROUND = 清零高度偏移;RELATIVE_TO_GROUND = 采样地表高 + 偏移。CPU 改 modelMatrix / 位置属性,与 GroundPrimitive 着色器无关

1.5 两个值得落实的工程细节

  1. EncodedCartesian3 高 / 低双精度:顶点位置拆 high + low(Float32 + Float32),VS 里 vec3 p = high.xyz + low.xyz 还原,对抗地球尺度 Float32 抖动。当前 PolylineGraphicLineGeometry.setPositions(Float32),长贴地线必抖。
  2. 法向微偏移(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
GroundClampPasseffects pass:绑定深度、渲分类几何、合成TERRAIN_CLASSIFICATION pass 编排
GroundPolylineGraphic线墙体积几何 + 分类材质GroundPolylineGeometry + PolylineShadowVolumeFS
GroundPolygonGraphic面阴影体几何 + 模板两遍材质PolygonGeometry.createShadowVolume + ShadowVolumeAppearanceFS
EncodedCartesian3(util)高 / 低双精度拆分EncodedCartesian3
clamp 字段 + 解析API(统一 boolean | GroundClamp取代 HeightReference 7 值枚举
点:复用HeightSamplerCPU 采样改点位置Billboard._updateClamping

3. 组件设计

3.1 深度源与坐标还原

  • 深度源:贴地分类位于大气之前,复用主场景的 readBuffer.depthTexture。clamped 实体 depthWrite:false,不污染地表深度;该纹理所含即 terrain + 3D Tiles 深度(Phase 1 = union)。

  • 还原(ShaderMaterial 内置 projectionMatrix / viewMatrix):

    glsl
    float 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.depthTexturetype(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/startLoforwardOffsetstart/end/right 平面法向、texcoord 归一化(含 2D 属性可裁剪,tellux 仅 3D 模式)。
  • 细节 1 落实startHi/startLoEncodedCartesian3.fromCartesian 拆分。
  • 细节 2 落实:前 / 后两面沿 rightNormal±EPSILON5 偏移 + nudgeXZ

3.3 贴地线分类材质(port PolylineShadowVolumeFS/VS

  • VS:用 startHi + startLo 双精度还原段起点;由属性 + modelViewMatrix 算眼空间 rightPlaneEC / startPlaneEC / endPlaneEChalfWidth 作 varying。
  • FS:§3.1 还原地表 eyeabs(planeDistance(rightPlane, eye)) <= halfWidth && planeDistance(startPlane) >= 0 && planeDistance(endPlane) >= 0 否则 discard;命中输出预乘 alpha 颜色。
  • 渲染状态depthTest:false(手动比深度)、depthWrite:falseblending: 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(...) / #rrggbbaa alpha 生效(线同步改为 BackSide)。
  • 渲染状态与线一致:depthTest:falsedepthWrite:false、常规混合,由 GroundClampPass 统一渲染,无 stencil 依赖。extrudeHeight / outline 留后续阶段。

3.6 GroundClampPass 编排

  • 实现 ThreeEffectPass(同 EntityRenderManager 形态):render(renderer, writeBuffer, readBuffer)
  • 维护 groundClampRoot: THREE.Group,挂所有 GroundPolylineGraphic / GroundPolygonGraphicobject3D
  • 绑定 readBuffer.depthTexture 到分类材质的 telluxGroundDepth uniform(同 OIT 的 telluxSceneDepth 注入范式)。
  • 渲分类几何到 writeBuffer(主色 ⊕ 分类色);needsSwap = true
  • 插入位置:effects 链中、EntityRenderManager 邻近,但必须在云 / 大气合成之前。真实顺序为 GroundClamp → Atmosphere → Entity OIT。透明实体必须在大气之后,否则大气天空分支会抹掉其地平线像素。
  • 大气之后的深度规则:大气 pass 可能 swap 到无深度的 targetB。因此 EntityRenderManager 及其他需要场景深度的后处理 pass 必须从 readBuffer.depthTexture ?? writeBuffer.depthTexture 两侧探测,不能只读 readBuffer

3.7 点:clamp CPU 采样(正解,非妥协)

  • clamp 存在(true 或对象)时,Entity add 发起 HeightSampler.sampleHeightMostDetailed([pos], { source }),resolve 后 PointGraphic.setPosition
  • add 立即用椭球高先摆出来,采样 resolve 后 snap(渐进式,同瓦片流式体验)。
  • LOD 重采样:挂 TilesetSamplingAdapter readiness 信号或 update() 周期去抖重采样(点仅 1 顶点,几乎免费)。
  • source 直接取自 clamp.source(缺省 'all');最终点位 = 采样高程 + clamp.offset(缺省 0)。

3.8 实体集成

  • Entity 构造:polyline.clamp 存在 → 建 GroundPolylineGraphic,加入 groundClampRoot(非普通 entity root);面同理。
  • EntityManager 需持有 groundClampRoot 引用(由 GroundClampPass 提供 / 注入)。
  • 普通拾取(EntityPicker)对线 / 面走射线或屏幕投影——贴地几何为墙体积,射线命中语义需重新定义(建议按 footprint 投影拾取,复用现有 forEachSegment 屏幕投影逻辑)。

4. API 设计

贴地本质是坐标的垂直定位属性,不是图形样式。它只有两个正交轴:贴到哪个面(深度源)、离那个面多高(偏移)。故不采用 Cesium 的 7 值枚举(笛卡尔积爆炸、height 语义被枚举远程改写、点 / 线 / 面三套 API 各行其是),改用一个统一 clamp 字段boolean 走常见场景,对象走精细控制。

ts
export interface GroundClamp {
  /** 贴到什么面;直接透传给 HeightSampler.source。默认 'all'(terrain 与 3D Tiles 取上)。 */
  source?: 'all' | 'terrain' | 'tileset'
  /** 地表之上的偏移(米)。0 或缺省 = 真·贴地;> 0 = 抬离地表。 */
  offset?: number
}
  • 落到 PointOptions / PolylineOptions / PolygonOptions,三者共用同一字段
    ts
    clamp?: boolean | GroundClamp
  • 取值语义:
    • 缺省 / false → 绝对椭球高(当前行为,替代旧 NONE)。
    • true{ source: 'all', offset: 0 },贴地,覆盖最常见诉求。
    • 对象 → 精细控制,如 { source: 'terrain', offset: 5 } = 地形上方 5 米。
  • clamp 即 offset=0 的 relative:Cesium 的 CLAMP_TO_*RELATIVE_TO_* 在此塌缩为 offset 一个数字,不需两套枚举族;source 轴与 HeightSamplerSampleHeightOptions.source 一一对应,不重复编码。
  • 内部分发对用户透明:source 决定深度源(点采样)/ 分类深度快照(线 / 面);点走 CPU 采样、线 / 面走分类渲染,用户侧始终是同一字段。

4.1 实现断崖:offset=0 与 offset>0(线 / 面)

概念连续,实现不连续,必须显式处理:

offset线 / 面
0(真贴地)CPU 采样改点位GPU 影体分类(逐像素,不重建几何)
> 0(抬离)CPU 采样 + 偏移CPU 重采样抬升几何(采样间浮沉,后续阶段)
  • 点的 offset 天然是「采样高程 + offset」,无断崖。
  • 线 / 面 Phase 1 只实现 offset=0(GPU 分类);对「线 / 面 + offset>0」未实现组合 console.warn 降级,不抛异常。

4.2 polygon 的 height / extrudeHeight 交互

避免「字段被远程改写」:

  • clamp 存在 → 底面 = 贴地面,height(绝对底高)被忽略;
  • extrudeHeight 重定义为相对底面的厚度(米),而非绝对顶高。

即「贴地 + 向上拉伸体块」是自然组合,比 Cesium GroundPrimitive 只能压平更灵活。

4.3 分期

  • Phase 1 仅实现 clamp: true / { source: 'all', offset: 0 }(线 / 面真贴地 + 点采样)。
  • source: 'terrain' | 'tileset' 依赖 §3.6 的分离深度快照(Phase 3)。
  • offset > 0(RELATIVE 语义)为 Phase 4。
  • 类型可一次性声明完整(API 稳定),运行时对未实现取值 warn 降级到最近的已实现档。

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 点 clampHeightSampler 接入 + LOD 重采样、clamp: true(点)点 snap 到地表,LOD 载入后位置收敛
P3 terrain/tileset 分离主渲染中插入 terrain-only 深度快照 → clamp.source: 'terrain' | 'tileset'同一线可仅贴地形或仅贴建筑顶
P4 offset>0 + 打磨clamp.offset > 0(relative 语义)、精度 / 性能(scissor、bounding)、与 OIT 交互厘清偏移贴地稳定,大场景帧率达标
P5 WebGPU本期不做,后续单独立项(TSL/WGSL 原生着色器)

细节 1(双精度)与细节 2(微偏移)随 P0 落实,贯穿后续。


6. 风险与开放问题

  • 深度源完整性readBuffer.depthTexture 是否含 3D Tiles 深度?需核实 ViewerRenderLoop3d-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.136D:/dev_work/gis-template/node_modules/cesium/Build/CesiumUnminified/):

  • Workers/createGroundPolylineGeometry.js — 线墙体积几何
  • Cesium.js L52658 ShadowVolumeAppearanceFS、L57231 PolylineShadowVolumeFS、L53611/53645 模板 / color render state、L36379 windowToEyeCoordinates、L54413 GroundPrimitive、L57964 GroundPolylinePrimitive.createCommands

tellux