ArcGIS JS 基础教程(30):体元系列 - VoxelLayer 体元图层
零、写在前面
📌 本系列教程完整目录 :ArcGIS JS 系列基础教程(100个项目常用热门功能)
💡 在线示例 :完整可运行的 HTML 示例,无需任何环境配置,可直接在浏览器中打开体验
🗂️ 专栏导航 :收藏 + 关注,专栏文章第一时间送达
❤️ 一键三连:点赞 + 评论 + 收藏
一、功能介绍
VoxelLayer(体元图层)用于可视化三维体元数据(Volume / Voxel)------在连续三维空间内规则采样的标量场或矢量场。它能把海洋温盐、大气雾霾、地下水质污染等「看不见的三维场」以体积、切片、等值面等形式直观呈现。
⚠️ 三维专项关键限制(务必注意)
- VoxelLayer 只能在
SceneView的local模式下显示 ,必须在构造视图时设置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/VoxelVolume、VoxelSlice、VoxelDynamicSection、VoxelIsosurface。
⚠️ 踩坑提醒 :enableSlices/enableIsosurfaces和renderMode = "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

操作说明:
- 场景以
local模式加载 VoxelPM10 连续体元数据。- 点击「切换变量」在多个标量场之间切换(
currentVariableId)。- 点击「切换渲染模式」在
volume(完整体积)与surfaces(仅显示切片 / 等值面等表面)间切换。- 点击「切片开关」会向
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
七、系列导航
💡 小贴士 :VoxelLayer 是「三维场数据」的专属图层。记住两个前提------必须在 local 模式 、opacity 不可改 。理解
volumes/variables两个集合,是后续掌握切片、动态剖面、等值面的基础。