基于 SuperMap iClient3D for WebGL 实现区域掩膜(裁剪遮罩)效果

基于 SuperMap iClient3D for WebGL 实现区域掩膜(裁剪遮罩)效果

一、前言

在 WebGIS 可视化开发中,经常遇到这样一个需求:高亮显示某个区域,同时淡化或遮挡区域外的内容 ,形成一种"聚光灯"式的视觉引导效果。这种效果通常称为掩膜(Mask / Clip Layer)

在二维 GIS 中实现掩膜相对简单------使用一个覆盖全图的大矩形,中间挖掉目标区域即可。但在三维场景(SuperMap iClient3D for WebGL)中,由于涉及地形起伏、相机视角、地球曲率等因素,实现起来有不少需要注意的细节。

本文基于实际项目经验,详细介绍如何基于 SuperMap iClient3D for WebGL实现一个高性能、效果正确的 3D 掩膜功能。

二、效果预览

实现后的效果:

  • 掩膜区域内:地图正常显示,所有要素完整可见
  • 掩膜区域外:被深色半透明遮罩覆盖,形成视觉遮挡
  • 区域边界:带有青色立体墙,增强视觉效果和空间感

三、实现原理

3.1 核心思路

掩膜的核心思路可以概括为:画一个大面,中间掏个洞

具体来说:

  1. 以目标区域为中心,构建一个覆盖周边的大矩形
  2. 在矩形中间"挖掉"目标区域(使用 PolygonHierarchy 的 holes 机制)
  3. 给这个大矩形填充深色半透明材质
  4. 沿着目标区域的边界添加一道立体墙,作为视觉边界

3.2 PolygonHierarchy 的层次结构

SuperMap iClient3D for WebGL 使用 PolygonHierarchy 来表示带孔洞的多边形。其结构如下:

复制代码
PolygonHierarchy {
  positions: Cartesian3[],    // 外环:大矩形的四个角(必须闭合)
  holes: [                    // 孔洞数组
    { positions: Cartesian3[] },  // 内环:目标区域边界(必须闭合)
    { positions: Cartesian3[] }   // 如果有多个面,每个面作为一个孔洞
  ]
}

关键要点:

  • 外环必须是顺时针或逆时针的闭合环(首尾坐标相同)
  • 每个孔洞也必须是闭合环
  • 孔洞的方向通常与外环相反,不过会自动处理
  • 支持多层嵌套:孔洞里还可以继续嵌套子孔洞

四、完整代码实现

以下是完整的 loadClipLayer 函数实现,分为 6 个步骤:

javascript 复制代码
/**
 * 加载 clipLayer 数据并生成 3D 裁剪遮罩效果
 * @param {string} datasetUrl - 数据集 URL
 * @param {string} alias - 裁剪区域名称(可选)
 */
async function loadClipLayer(datasetUrl, alias) {
    const viewer = window.MapManager.getViewer();
    if (!viewer) return;

    try {
        // ===== 步骤 1:获取并解析 GeoJSON 数据 =====
        const geoJsonData = await fetchGeoJsonSmart(datasetUrl);
        if (!geoJsonData || !geoJsonData.features || geoJsonData.features.length === 0) {
            console.warn('裁剪区域数据为空');
            return;
        }

        // 提取所有面的外环坐标
        const allRings = [];
        for (const feature of geoJsonData.features) {
            const geometry = feature.geometry;
            if (!geometry || !geometry.coordinates) continue;
            if (geometry.type === 'Polygon') {
                allRings.push(geometry.coordinates[0]);
            } else if (geometry.type === 'MultiPolygon') {
                for (const polygon of geometry.coordinates) {
                    allRings.push(polygon[0]);
                }
            }
        }
        if (allRings.length === 0) return;

        const bounds = calculateBounds(geoJsonData);
        const name = alias || '裁剪区域';

        // ===== 步骤 2:配置地球/场景参数 =====
        // 为了让遮罩在高于地面的视角下也能正确显示,
        // 需要关闭地形的深度检测,并开启地表半透明度
        const scene = viewer.scene;
        const globe = scene.globe;

        globe.depthTestAgainstTerrain = false;        // 关闭地形深度检测
        scene.skyAtmosphere.show = false;              // 关闭大气阴影
        globe.translucency.enabled = true;             // 开启地表半透
        globe.translucency.frontFaceAlphaByDistance =
            new SuperMap3D.NearFarScalar(400.0, 0.0, 800.0, 1.0);
        globe.translucency.frontFaceAlphaByDistance.nearValue =
            SuperMap3D.Math.clamp(0.3, 0.0, 0.1);      // 近处地表透明
        globe.translucency.frontFaceAlphaByDistance.farValue = 1.0;

        // 限制相机缩放范围,避免拉太远或太近
        scene.screenSpaceCameraController.minimumZoomDistance = 10000;
        scene.screenSpaceCameraController.maximumZoomDistance = 50000;

        // ===== 步骤 3:计算外覆矩形范围 =====
        // 以目标区域为中心,向外扩展约 30%(最少 0.3 度)
        const [minLon, minLat, maxLon, maxLat] = bounds;
        const lonRange = maxLon - minLon;
        const latRange = maxLat - minLat;
        const extend = Math.max(lonRange * 0.3, latRange * 0.3, 0.3);

        const outerWest = minLon - extend;
        const outerSouth = minLat - extend;
        const outerEast = maxLon + extend;
        const outerNorth = maxLat + extend;

        // ===== 步骤 4:构建带孔洞的遮罩多边形 =====
        // 外环:大矩形
        const outerPositions = SuperMap3D.Cartesian3.fromDegreesArray([
            outerWest, outerSouth,
            outerEast, outerSouth,
            outerEast, outerNorth,
            outerWest, outerNorth,
            outerWest, outerSouth   // 闭合环
        ]);

        // 孔洞:所有裁剪面
        const holes = [];
        for (const ring of allRings) {
            const flatRing = [];
            for (const coord of ring) {
                flatRing.push(coord[0], coord[1]);
            }
            if (flatRing.length >= 6) {
                holes.push({
                    positions: SuperMap3D.Cartesian3.fromDegreesArray(flatRing)
                });
            }
        }

        // 创建遮罩实体
        const maskEntity = new SuperMap3D.Entity({
            name: name + ' - 遮罩',
            polygon: {
                hierarchy: {
                    positions: outerPositions,
                    holes: holes
                },
                material: SuperMap3D.Color.fromCssColorString('#0C1F34').withAlpha(0.85),
                outline: false,
                arcType: SuperMap3D.ArcType.RHUMB  // 等角航线,性能更优
            }
        });
        viewer.entities.add(maskEntity);

        // ===== 步骤 5:构建边界立体墙 =====
        // 沿着裁剪边界添加一道竖向墙,增强视觉效果
        if (holes.length > 0) {
            const wallPositions = holes[0].positions;
            const wallHeights = wallPositions.map(() => 600);    // 墙顶高度
            const wallMinHeights = wallPositions.map(() => -600); // 墙底高度

            const wallEntity = new SuperMap3D.Entity({
                name: name + ' - 边界墙',
                wall: {
                    positions: wallPositions,
                    maximumHeights: wallHeights,
                    minimumHeights: wallMinHeights,
                    material: SuperMap3D.Color.fromCssColorString('#6dcdeb').withAlpha(0.8)
                }
            });
            viewer.entities.add(wallEntity);
        }

        // ===== 步骤 6:飞行定位到裁剪区域 =====
        const [west, south, east, north] = bounds;
        viewer.camera.flyTo({
            destination: SuperMap3D.Rectangle.fromDegrees(west, south, east, north),
            orientation: {
                heading: SuperMap3D.Math.toRadians(0),
                pitch: SuperMap3D.Math.toRadians(-90),
                roll: 0.0
            },
            duration: 2.0
        });

        console.log('裁剪遮罩已加载');

    } catch (error) {
        console.error('加载裁剪遮罩失败:', error);
    }
}

五、关键参数详解

5.1 地表透明度设置

javascript 复制代码
globe.translucency.frontFaceAlphaByDistance =
    new SuperMap3D.NearFarScalar(400.0, 0.0, 800.0, 1.0);

NearFarScalar 定义了基于相机距离的渐变参数:

参数 含义
nearDistance: 400.0 近端距离(米)
nearValue: 0.0 近端透明度(0 完全透明)
farDistance: 800.0 远端距离(米)
farValue: 1.0 远端透明度(1 完全不透明)

效果:在距离地面 400 米内,地表逐渐透明;800 米外完全恢复不透明。这样在进入地下视角观察时能看到遮罩。

5.2 外覆矩形的大小控制

javascript 复制代码
const extend = Math.max(lonRange * 0.3, latRange * 0.3, 0.3);

扩展量没有统一标准,可以参考以下原则:

场景 建议扩展量 说明
小范围(<1°) 0.3° ~ 0.5° 覆盖周边即可,太大影响性能
中等范围(1°~5°) 范围的 20%~30% 平衡覆盖面积和性能
大范围(>5°) 范围的 10%~20% 大范围本身视野大,无需太多扩展

5.3 arcType 的选择

javascript 复制代码
arcType: SuperMap3D.ArcType.RHUMB
  • Geodesic(大圆航线):两点之间的最短路径,在地球曲率下呈曲线。精度高,但计算量大。
  • Rhumb(等角航线):以恒定方位角连接两点,在地图上呈直线。计算量小,适合小范围。

对于小范围(几度以内),两者视觉效果几乎无差别,建议使用 RHUMB 以获得更好的性能

六、常见问题与解决方案

6.1 遮罩没有显示或显示不正确

原因 :最常见的错误是 PolygonHierarchy 的 holes 参数格式不对。

正确写法

javascript 复制代码
// ✅ 正确:holes 使用 { positions } 对象数组
hierarchy: {
    positions: outerPositions,
    holes: [
        { positions: holePositions1 },
        { positions: holePositions2 }
    ]
}

错误写法

javascript 复制代码
// ❌ 错误:直接传 Cartesian3 数组
holes: [holePositions1, holePositions2]

// ❌ 错误:传 PolygonHierarchy 对象数组
hierarchy: new SuperMap3D.PolygonHierarchy(outerPositions, [
    new SuperMap3D.PolygonHierarchy(holePositions1)
])

6.2 性能差、卡顿

原因:外覆矩形范围过大,导致需要对数万平方公里的区域进行多边形三角剖分和地形裁剪计算。

解决方案

  1. 缩小外覆范围:按目标范围大小的 20%~30% 扩展,而不是固定扩展 5° 或 10°
  2. 使用 RHUMB arcType:减少曲线插值计算量
  3. 避免 height: 0:不设置 height 可以让框架自动优化渲染策略
  4. 减少孔洞顶点数量:如果原始数据顶点过密,可以适当抽稀

6.3 遮罩与地形之间有缝隙

原因globe.depthTestAgainstTerrain 开启时,遮罩与地形会产生深度冲突。

解决方案:关闭地形深度检测即可:

javascript 复制代码
globe.depthTestAgainstTerrain = false;

6.4 加载多个裁剪区域

如果同时需要多个裁剪区域,只需在 holes 数组中添加多个孔洞即可:

javascript 复制代码
holes: [
    { positions: area1 },  // 第一个裁剪面
    { positions: area2 },  // 第二个裁剪面
    { positions: area3 }   // 第三个裁剪面
]

七、总结

本文介绍了基于 SuperMap iClient3D for WebGL 实现 3D 掩膜效果的方法。核心思路是利用 PolygonHierarchy 的 holes 机制,用一个带孔洞的大多边形来覆盖非目标区域。

几个关键要点:

  1. 格式要对 :holes 必须使用 { positions } 对象格式
  2. 范围要小:外覆矩形只需小幅扩展,避免性能问题
  3. 地表要透 :配合 depthTestAgainstTerraintranslucency 设置才能看到效果
  4. 边界要显:配合 Wall 实体可以增强视觉引导效果

这套方案已经在我们基于 SuperMap iClient3D 的 WebGIS 产品中实际应用,在千万级要素的场景下仍能保持流畅的交互体验。

相关推荐
supermapsupport1 年前
SuperMap GIS基础产品FAQ集锦(20250402)
gis·supermap·webgis·idesktopx·iclient3d