From 66f90555ff78b979e0c4ddfad540684f8b091381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=88=E5=85=86=E5=A2=9E?= <你的邮箱@example.com> Date: Wed, 5 Aug 2026 17:42:01 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B93d=E9=94=9A=E7=82=B9=E8=A2=AB?= =?UTF-8?q?=E5=9C=B0=E5=BD=A2=E9=81=AE=E6=8C=A1=E6=B6=88=E5=A4=B1=E4=B9=8B?= =?UTF-8?q?=E5=90=8E=20popup=E4=B9=9F=E8=A6=81=E6=B6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/gis/map.cesium.ts | 102 +++++++++++++++++++--- 1 file changed, 92 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/gis/map.cesium.ts b/frontend/src/components/gis/map.cesium.ts index ab81524c..90f2f555 100644 --- a/frontend/src/components/gis/map.cesium.ts +++ b/frontend/src/components/gis/map.cesium.ts @@ -1003,10 +1003,18 @@ export class MapCesium implements MapInterface { this.hidePopup(); } - /** 判断 3D 位置是否被地形遮挡(基于射线与 terrain 求交) */ + /** + * 判断 3D 位置是否被地形/模型遮挡(只看锚点图标的实际渲染范围)。 + * 只采样图标中心一个像素时,山脊从下往上遮住图标中心、但图标还有一半露在外面 + * 时 popup 就先消失了。因此改为“图标范围内多点采样”: + * 中心可见 → 未遮挡(快速路径);中心被遮 → 继续采样图标上/下/左右范围, + * 任一采样点仍可见即认为图标还有出露部分,popup 保留。 + * 注意:只按图标判定,文字标签是否被遮挡不影响 popup 显隐。 + */ private isPositionBehindTerrain(position: Cesium.Cartesian3): boolean { if (!this.viewer) return true; const scene = this.viewer.scene; + const cameraPos = scene.camera.positionWC; const windowCoord = Cesium.SceneTransforms.worldToWindowCoordinates( scene, @@ -1014,17 +1022,74 @@ export class MapCesium implements MapInterface { ); if (!windowCoord) return true; // 在地球背面 - const ray = scene.camera.getPickRay(windowCoord); - if (!ray) return false; + if (!scene.pickPositionSupported) { + // 降级:不支持深度拾取时回退到射线与地形求交 + const ray = scene.camera.getPickRay(windowCoord); + if (!ray) return false; - const terrainPos = scene.globe.pick(ray, scene); - if (!terrainPos) return false; // 该方向无地形 + const terrainPos = scene.globe.pick(ray, scene); + if (!terrainPos) return false; // 该方向无地形 - const cameraPos = scene.camera.positionWC; - const terrainDist = Cesium.Cartesian3.distance(cameraPos, terrainPos); - const entityDist = Cesium.Cartesian3.distance(cameraPos, position); - // 地形交点明显更近 → 实体被遮挡 - return terrainDist + 10 < entityDist; + const terrainDist = Cesium.Cartesian3.distance(cameraPos, terrainPos); + const entityDist = Cesium.Cartesian3.distance(cameraPos, position); + // 地形交点明显更近 → 实体被遮挡 + return terrainDist + 10 < entityDist; + } + + // 深度缓冲拾取:读取像素处离相机最近的渲染表面(图标自身/地形/模型)的深度。 + // 相比 globe.pick 射线-地形求交,深度缓冲就是实际渲染结果,与锚点可见性一致。 + const posDist = Cesium.Cartesian3.distance(cameraPos, position); + const samplePoint = new Cesium.Cartesian2(0, 0); + // 该像素最近渲染表面不比锚点更近(或对天空无表面)→ 该处锚点可见 + const isPixelVisible = (x: number, y: number): boolean => { + samplePoint.x = x; + samplePoint.y = y; + const picked = scene.pickPosition(samplePoint); + if (!Cesium.defined(picked)) return true; + return Cesium.Cartesian3.distance(cameraPos, picked) + 10 >= posDist; + }; + + // 快速路径:图标中心仍可见 → 直接判定未遮挡(最常见情况,仅 1 次拾取) + if (isPixelVisible(windowCoord.x, windowCoord.y)) return false; + + // 图标中心被遮挡:继续采样整个图标范围(与碰撞检测图标碰撞盒尺寸公式一致), + // 任一采样点仍可见 → 图标还有出露部分,popup 保留 + const half = this.getAnchorIconHalfExtent(); + const step = Math.max(4, Math.ceil(half / 2)); + // 纵向:图标顶 → 底 + for (let d = -half; d <= half; d += step) { + if (isPixelVisible(windowCoord.x, windowCoord.y + d)) return false; + } + // 横向:图标中心行左右边缘 + if ( + isPixelVisible(windowCoord.x - half, windowCoord.y) || + isPixelVisible(windowCoord.x + half, windowCoord.y) + ) { + return false; + } + // 整个图标范围均被遮挡 → 图标不可见 + return true; + } + + /** 锚点图标在屏幕上的近似半宽/半高(px),与碰撞检测中的图标碰撞盒公式一致 */ + private getAnchorIconHalfExtent(): number { + const zoom = this.getCurrentCesiumZoom() || 4.5; + const iconScaleFactor = Math.max( + this.ICON_MIN_SCALE, + Math.min( + this.ICON_MAX_SCALE, + this.ICON_BASE_SCALE + (zoom - 4.5) * this.ICON_SCALE_RATE + ) + ); + const camHeight = this.viewer!.camera.positionCartographic.height; + const sbdT = Math.max( + 0, + Math.min(1, (camHeight - 1000) / (20000000 - 1000)) + ); + const scaleByDist = 1.5 + sbdT * (0.8 - 1.5); + // 图标图片像素宽度 24,视觉尺寸 = 24 * scale * scaleByDistance,半宽约其一半 + const visualIconSize = 24 * iconScaleFactor * scaleByDist; + return Math.max(7, visualIconSize / 2); } /** 根据实体世界坐标把 popup 定位到屏幕坐标 */ @@ -1110,6 +1175,10 @@ export class MapCesium implements MapInterface { this._emptyCandidateCount = 0; this.clearBatchPopups(); this.hidePopup(); // 清除可能残留的 hover popup + // 重新应用深度测试配置:进入批量模式时可能已有锚点图层加载完成, + // 但俯仰角未再变化导致 updateDepthTestForPitch 不会重刷,图标停留为“始终置顶”。 + // 平视下必须开启深度测试(图标被地形遮挡),遮挡检测才能与实际渲染一致。 + this.applyPointDepthTest(this.getDepthTestValue()); // 先注册 postRender(内含 zoom 兜底检测),再构建 popup,确保缩放/拖拽时始终有同步线程 this.setupBatchPopupPostRenderSync(); // 碰撞检测改为 rAF 异步执行,不再同步阻塞主线程,避免进入批量模式时卡顿 1-2s。 @@ -1377,6 +1446,8 @@ export class MapCesium implements MapInterface { const viewW = canvas.clientWidth; const viewH = canvas.clientHeight; const padding = 48; + // 俯视模式下锚点图标始终置顶渲染(深度测试关闭),不会被地形遮挡,跳过遮挡检测省开销 + const skipOcclusionCheck = this._currentPitchMode === 'down'; // 第一遍:更新位置、收集可见项的屏幕矩形(用缓存宽高,不依赖 DOM 测量) const visibleRects: { @@ -1442,6 +1513,17 @@ export class MapCesium implements MapInterface { continue; } + // 拖拽/平视时锚点被地形遮挡 → 隐藏 popup(与 rebuild 候选阶段的遮挡过滤一致); + // 漫游时跳过:模型上方锚点易被新加载的高精度地形误判为遮挡 + if ( + !skipOcclusionCheck && + !this._isRoaming && + this.isPositionBehindTerrain(item.worldPosition) + ) { + element.style.display = 'none'; + continue; + } + const x = cp.x; const y = cp.y; // 通过所有检查后才设为可见(与开头的惰性策略配合,避免全量闪一下)