ArcGIS JS 基础教程(30):体元系列 - VoxelLayer 体元图层

ArcGIS JS 基础教程(30):体元系列 - VoxelLayer 体元图层

零、写在前面

📌 本系列教程完整目录ArcGIS JS 系列基础教程(100个项目常用热门功能)

💡 在线示例 :完整可运行的 HTML 示例,无需任何环境配置,可直接在浏览器中打开体验

🗂️ 专栏导航 :收藏 + 关注,专栏文章第一时间送达

❤️ 一键三连:点赞 + 评论 + 收藏


一、功能介绍

VoxelLayer(体元图层)用于可视化三维体元数据(Volume / Voxel)------在连续三维空间内规则采样的标量场或矢量场。它能把海洋温盐、大气雾霾、地下水质污染等「看不见的三维场」以体积、切片、等值面等形式直观呈现。

参考:官方 VoxelLayer API

⚠️ 三维专项关键限制(务必注意)

  • VoxelLayer 只能在 SceneViewlocal 模式下显示 ,必须在构造视图时设置 viewingMode: "local"
  • 在 3D 中 opacity 不可修改(官方明确限制)。
  • 需要 WebGL 浮点纹理等扩展支持,移动端兼容性差,不建议移动端使用。

二、功能实现

核心 API: VoxelLayer@arcgis/core/layers/VoxelLayer.js)。

2.1 通过服务 URL 加载

javascript 复制代码
const VoxelLayer = await $arcgis.import("@arcgis/core/layers/VoxelLayer.js");

const voxelLayer = new VoxelLayer({
    url: "https://gs3d.geosceneonline.cn/server/rest/services/Hosted/VoxelPM10/SceneServer"
});
map.add(voxelLayer);

2.2 必须在 local 模式下创建视图

javascript 复制代码
const view = new SceneView({
    container: "mapContainer",
    map: map,
    viewingMode: "local",          // 体元图层要求 local 模式
    camera: { position: { longitude: -70, latitude: 18, z: 800000 }, tilt: 60 }
});

2.3 核心属性与切换

javascript 复制代码
voxelLayer.when(() => {
    // 当前显示的变量(标量场),默认 0
    voxelLayer.currentVariableId = 0;

    // 渲染模式:完整体积 volume / 仅表面(切片+等值面) surfaces
    voxelLayer.renderMode = "volume";

    // 各可视化开关
    voxelLayer.enableSlices = true;
    voxelLayer.enableDynamicSections = true;
    voxelLayer.enableIsosurfaces = true;

    // 集合信息(只读)
    console.log("体积数:", voxelLayer.volumes.length);
    console.log("变量数:", voxelLayer.variables.length);
});

第 31~34 课将分别深入 VoxelVariable / VoxelVolumeVoxelSliceVoxelDynamicSectionVoxelIsosurface
⚠️ 踩坑提醒enableSlices / enableIsosurfacesrenderMode = "surfaces" 都只是开关 ------它们本身不产生任何可见几何体。要让切片 / 等值面真正显示,必须向 volumeStyle.slices 添加 VoxelSlice、向当前变量的 variableStyle.isosurfaces 添加 VoxelIsosurface;且等值面、动态剖面只在 surfaces 模式下可见。本课示例已内置「一键添加默认切片 / 等值面」演示这一点。


三、功能应用

应用场景 说明
地下水质污染扩散 三维体元展示污染物浓度场
大气雾霾浓度分布 体素体积 + 等值面呈现污染羽流
海洋温盐三维结构 多变量体元数据切换查看
医学/科研三维场 CT、气象、流体仿真结果可视化

四、核心代码

📦 完整代码 已保存至 sample/lesson30_voxel_layer.html,可直接在浏览器打开。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>第30课:VoxelLayer 体元图层</title>
    <link rel="stylesheet" href="https://js.arcgis.com/5.0/esri/themes/light/main.css">
    <script type="module" src="https://js.arcgis.com/5.0/"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: "Microsoft YaHei", sans-serif; }
        #mapContainer { width: 100vw; height: 100vh; }
        .page-title {
            position: absolute; top: 20px; left: 50%; transform: translateX(-50%);
            background: rgba(255,255,255,0.95); padding: 10px 24px; border-radius: 6px;
            font-size: 18px; font-weight: bold; z-index: 100;
            box-shadow: 0 2px 8px rgba(0,0,0,0.15);
        }
        .control-panel {
            position: absolute; top: 80px; right: 20px;
            background: rgba(255,255,255,0.95); padding: 16px; border-radius: 8px;
            box-shadow: 0 2px 12px rgba(0,0,0,0.15);
            z-index: 100; min-width: 300px;
        }
        .control-panel h3 { margin: 0 0 8px 0; font-size: 14px; color: #333; }
        .section { margin-bottom: 12px; padding-bottom: 10px; border-bottom: 1px solid #eee; }
        .section:last-child { border-bottom: none; margin-bottom: 0; }
        .btn-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
        .btn-row button {
            flex: 1; min-width: 60px; padding: 6px 0;
            border: 1px solid #d9d9d9; border-radius: 4px;
            background: white; cursor: pointer; font-size: 12px;
        }
        .btn-row button:hover { border-color: #1890ff; color: #1890ff; }
        .btn-row button.on { background: #1890ff; color: white; border-color: #1890ff; }
        .info-card {
            margin-top: 10px; padding: 10px 12px;
            background: #f0f5ff; border-radius: 6px;
            border-left: 3px solid #1890ff; font-size: 12px; line-height: 1.6;
        }
        .info-card .val { font-weight: bold; color: #1890ff; }
        .status-text {
            position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
            background: rgba(0,0,0,0.7); color: white; padding: 8px 20px;
            border-radius: 20px; font-size: 13px; z-index: 100; pointer-events: none;
            white-space: nowrap;
        }
    </style>
</head>
<body>
<h1 class="page-title">第30课:VoxelLayer 体元图层</h1>

<div class="control-panel">
    <div class="section">
        <h3>🧊 VoxelLayer 整体控制</h3>
        <div class="btn-row">
            <button id="btnVisible" class="on">👁️ 显示 / 隐藏</button>
        </div>
        <div class="btn-row">
            <button id="btnVar">🔁 切换变量</button>
            <button id="btnMode">🎛️ 切换渲染模式</button>
        </div>
        <div class="btn-row">
            <button id="btnSlices">🪓 切片开关</button>
            <button id="btnIso">🫧 等值面开关</button>
        </div>
    </div>
    <div class="info-card">
        <div>变量数:<span class="val" id="varCount">-</span> | 体积数:<span class="val" id="volCount">-</span></div>
        <div>当前变量:<span class="val" id="curVar">-</span></div>
    </div>
</div>

<div class="status-text" id="statusText">VoxelLayer | 体元图层 · local 模式 · 体积/变量/渲染模式</div>

<div id="mapContainer"></div>

<script type="module">
    const Map = await $arcgis.import("@arcgis/core/Map.js");
    const SceneView = await $arcgis.import("@arcgis/core/views/SceneView.js");
    const VoxelLayer = await $arcgis.import("@arcgis/core/layers/VoxelLayer.js");
    const VoxelSlice = await $arcgis.import("@arcgis/core/layers/voxel/VoxelSlice.js");
    const VoxelIsosurface = await $arcgis.import("@arcgis/core/layers/voxel/VoxelIsosurface.js");
    const getTianditu = await $arcgis.import("https://openlayers.vip/examples/resources/tianditu.js");

    const vecLayers = getTianditu.default({ type: "vec_w" });
    const map = new Map({ basemap: { baseLayers: [vecLayers.base, vecLayers.anno] } });

    // 体元图层要求 local 模式
    const view = new SceneView({
        container: "mapContainer", map: map,
        viewingMode: "local",
        camera: { position: { longitude: 116.4, latitude: 39.9, z: 2000000 }, heading: 0, tilt: 60 }
    });
    window.view = view;

    const voxelLayer = new VoxelLayer({
        url: "https://gs3d.geosceneonline.cn/server/rest/services/Hosted/VoxelPM10/SceneServer"
    });
    map.add(voxelLayer);

    let volumeStyle = null;
    let variableStyle = null;
    let demoSlice = null;
    let demoIso = null;

    // 默认竖直切片:取体积体素空间的中心点(orientation:"x" 为垂直于 x 轴的竖直剖面)
    function buildSlice() {
        const size = voxelLayer.volumes.getItemAt(0).sizeInVoxels;
        return new VoxelSlice({
            enabled: true,
            orientation: "x",
            point: [size[0] / 2, size[1] / 2, size[2] / 2],
            renderMode: "stretch"
        });
    }

    // 默认等值面:取 transferFunction 映射区间的中点
    function buildIso() {
        const tf = variableStyle.transferFunction;
        const r = tf.stretchRange || [0, 1];
        const mid = (r[0] + r[1]) / 2;
        return new VoxelIsosurface({
            enabled: true,
            value: mid,
            color: [255, 80, 0, 255],
            label: "等值面 " + mid.toFixed(1)
        });
    }

    view.when(() => {
        voxelLayer.when(() => {
            document.getElementById("varCount").textContent = voxelLayer.variables.length;
            document.getElementById("volCount").textContent = voxelLayer.volumes.length;
            document.getElementById("curVar").textContent = voxelLayer.currentVariableId;
            volumeStyle = voxelLayer.getVolumeStyle(0);
            variableStyle = voxelLayer.getVariableStyle(voxelLayer.currentVariableId);
            view.goTo(voxelLayer.fullExtent, { duration: 2000 });
        }).catch(err => {
            document.getElementById("statusText").textContent = "体元图层加载失败";
            console.error(err);
        });
    });

    function setStatus(msg) { document.getElementById("statusText").textContent = msg; }

    document.getElementById("btnVisible").addEventListener("click", function () {
        voxelLayer.visible = !voxelLayer.visible;
        this.classList.toggle("on", voxelLayer.visible);
        setStatus("体元图层:" + (voxelLayer.visible ? "显示" : "隐藏"));
    });

    document.getElementById("btnVar").addEventListener("click", () => {
        const next = (voxelLayer.currentVariableId + 1) % voxelLayer.variables.length;
        voxelLayer.currentVariableId = next;
        variableStyle = voxelLayer.getVariableStyle(next);
        document.getElementById("curVar").textContent = next;
        setStatus("当前变量 id = " + next);
    });

    let isVolume = true;
    document.getElementById("btnMode").addEventListener("click", function () {
        isVolume = !isVolume;
        voxelLayer.renderMode = isVolume ? "volume" : "surfaces";
        this.classList.toggle("on", !isVolume);
        setStatus("渲染模式 = " + voxelLayer.renderMode);
    });

    // 切片开关:必须向 volumeStyle.slices 添加 VoxelSlice 才有可见效果
    document.getElementById("btnSlices").addEventListener("click", function () {
        voxelLayer.enableSlices = true;
        if (!demoSlice) {
            demoSlice = buildSlice();
            volumeStyle.slices.add(demoSlice);
        }
        demoSlice.enabled = !demoSlice.enabled;
        voxelLayer.enableSlices = demoSlice.enabled;
        this.classList.toggle("on", demoSlice.enabled);
        setStatus("切片:" + (demoSlice.enabled ? "开" : "关") + "(surfaces 模式可见)");
    });

    // 等值面开关:必须向 variableStyle.isosurfaces 添加 VoxelIsosurface,且仅 surfaces 模式可见
    document.getElementById("btnIso").addEventListener("click", function () {
        voxelLayer.enableIsosurfaces = true;
        if (!demoIso) {
            demoIso = buildIso();
            variableStyle.isosurfaces.add(demoIso);
        }
        demoIso.enabled = !demoIso.enabled;
        voxelLayer.enableIsosurfaces = demoIso.enabled;
        if (demoIso.enabled && voxelLayer.renderMode !== "surfaces") {
            isVolume = false;
            voxelLayer.renderMode = "surfaces";
            document.getElementById("btnMode").classList.add("on");
        }
        this.classList.toggle("on", demoIso.enabled);
        setStatus("等值面:" + (demoIso.enabled ? "开" : "关") + "(已切到 surfaces)");
    });
</script>
</body>
</html>

五、在线示例

🔗 在线体验https://southjor.github.io/arcgis-examples/lessons/lesson30.html

操作说明

  1. 场景以 local 模式加载 VoxelPM10 连续体元数据。
  2. 点击「切换变量」在多个标量场之间切换(currentVariableId)。
  3. 点击「切换渲染模式」在 volume(完整体积)与 surfaces(仅显示切片 / 等值面等表面)间切换。
  4. 点击「切片开关」会向 volumeStyle.slices 添加一个默认竖直 切片(再点关闭);点击「等值面开关」会向当前变量的 variableStyle.isosurfaces 添加一个默认等值面,并自动切到 surfaces 模式------只有真正添加了对象,开关才有可见效果。

六、关键 API 说明

API 说明
`new VoxelLayer({ url portalItem })`
viewingMode: "local" 必须,体元仅支持 local 场景
voxelLayer.volumes 体积集合(Collection,通常 1 个)
voxelLayer.variables 变量集合(Collection,每个标量场一个)
voxelLayer.currentVariableId 当前显示的变量(默认 0)
voxelLayer.renderMode "volume" / "surfaces"
enableSlices / enableDynamicSections / enableIsosurfaces 各可视化开关(boolean)

参考链接: VoxelLayer API


七、系列导航

⬅️ 上一篇ArcGIS JS 基础教程(29):CSVLayer 表格点位图层

➡️ 下一篇ArcGIS JS 基础教程(31):VoxelVariable 与 VoxelVolume


💡 小贴士 :VoxelLayer 是「三维场数据」的专属图层。记住两个前提------必须在 local 模式opacity 不可改 。理解 volumes/variables 两个集合,是后续掌握切片、动态剖面、等值面的基础。

相关推荐
非科班Java出身GISer3 天前
ArcGIS JS 基础教程(29):CSVLayer 表格点位图层
arcgis·arcgis js·arcgis js csv图层·arcgis js scv·arcgis csv 图层
CAE3203 天前
ArcGIS DEM水文分析与山洪风险评估
arcgis·dem·流域提取
你是一个铁憨憨7 天前
从 GIS 到 Spatial Agent:MCP 如何重新定义 GIS 的 AI 入口
arcgis·ai·agent·mcp·spatial
码农小旋风10 天前
Claude Code 最佳实践指南
arcgis
兔年鸿运Q小Q21 天前
cesium1.140以上版本加载地形
arcgis·cesium
杨超越luckly21 天前
Agent应用指南:巨幕之下 · 中国 IMAX 影院205城的空间布局
人工智能·arcgis·html·agent·数据可视化
GISerQ.23 天前
DWG格式文件导入到ArcGIS中
arcgis·shp·cad文件·dwg文件
En^_^Joy1 个月前
Vue项目创建与入口配置全攻略
前端·vue.js·arcgis
图件制作GIS,储量计算1 个月前
补充耕地项目类、非项目类资料清单
arcgis