ArcGIS JS 基础教程(30):体元系列 - VoxelSlice 体元切片
-
- 零、写在前面
- 一、功能介绍
- 二、功能实现
-
- 2.1 创建并添加切片
- 2.2 切片的三个核心属性
- 2.3 管理集合(增删改)
- 2.1 创建并添加切片
- 三、功能应用
- 四、核心代码
- 五、在线示例
- 六、关键 API 说明
- 七、系列导航
零、写在前面
📌 本系列教程完整目录:ArcGIS JS 系列基础教程(100个项目常用热门功能)
💡 在线示例:完整可运行的 HTML 示例,无需任何环境配置,可直接在浏览器中打开体验
🗂️ 专栏导航:收藏 + 关注,专栏文章第一时间送达
❤️ 一键三连:点赞 + 评论 + 收藏
一、功能介绍
VoxelSlice(体元切片)沿一个无限平面裁剪体数据体积,得到一个可渲染的凸壳(convex shell)。它常用于:
- 把庞大的三维体数据"切开",只展示某一截面(如某一高度层、某一经度面)的标量场,方便观察内部结构;
- 定义感兴趣区域(Area of Interest),屏蔽与当前分析无关的体素。
参考:VoxelSlice API | VoxelVolumeStyle API | 官方示例 Create area of interest for VoxelLayer
⚠️ 关键概念:
VoxelSlice的point是体素空间坐标(基于VoxelVolume.sizeInVoxels的索引[x, y, z]),不是经纬度等地理坐标。这正是它区别于普通Slice分析工具的地方。
二、功能实现
2.1 创建并添加切片
切片由 VoxelSlice 构造,最终装入 VoxelVolumeStyle.slices 这个 Collection 集合中:
1const VoxelSlice = await $arcgis.import("@arcgis/core/layers/voxel/VoxelSlice.js"); 2 3voxelLayer.when(() => { 4 const vol = voxelLayer.getVolume(null); // VoxelVolume(只读元信息) 5 const volSize = vol.sizeInVoxels; // 体素空间尺寸 [x, y, z] 6 7 const volumeStyle = voxelLayer.getVolumeStyle(null); // VoxelVolumeStyle 8 voxelLayer.enableSlices = true; // 启用切片可视化 9 10 // 水平切片:位于中间高度 11 const hSlice = new VoxelSlice({ 12 orientation: 0, 13 tilt: 0, 14 point: [0, 0, Math.floor(volSize[2] / 2)] 15 }); 16 volumeStyle.slices.add(hSlice); // 添加进集合即可渲染 17}); 18
2.2 切片的三个核心属性
| 属性 | 类型 | 说明 |
|---|---|---|
| orientation | number | 切片平面的方向角(单位:度) |
| tilt | number | 切片平面的倾斜角(单位:度) |
| point | [number,number,number] | 切片平面经过的一个点,以 sizeInVoxels 体素空间坐标 [x, y, z] 指定 |
此外还有 enabled(是否启用,默认 true)与 label(标签文本)。
2.3 管理集合(增删改)
slices 是一个 Collection<VoxelSlice>,支持标准集合操作:
1// 新增 2volumeStyle.slices.add(new VoxelSlice({ orientation: 270, tilt: 90, point: [midX, 0, 0] })); 3 4// 移除最末 5volumeStyle.slices.removeAt(volumeStyle.slices.length - 1); 6 7// 实时修改已有切片(无需重新加载) 8const s = volumeStyle.slices.getItemAt(0); 9s.orientation = 45; // 立即生效 10s.tilt = 30; 11s.point = [128, 64, 89]; 12
也可整体替换:
volumeStyle.slices = [sliceA, sliceB];
三、功能应用
| 应用场景 | 实现要点 |
|---|---|
| 单一高度层展示 | orientation:0, tilt:0, point:[0,0,z],z 取中间体素索引 |
| 东西向剖面 | orientation:270, tilt:90, point:[midX,0,0] |
| 感兴趣区域裁剪 | 将 point 设在关注体素附近,组合多个切片 |
| 交互式探查 | 滑块绑定 getItemAt(i).orientation/tilt/point 实时更新 |
四、核心代码
📦 完整代码 已保存至
sample/lesson32_voxel_slice.html,可直接在浏览器打开。
1<!DOCTYPE html> 2<html lang="zh-CN"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>第32课:VoxelSlice 体元切片</title> 7 <link rel="stylesheet" href="https://js.arcgis.com/5.0/esri/themes/light/main.css"> 8 <script type="module" src="https://js.arcgis.com/5.0/"></script> 9 <style> 10 * { margin: 0; padding: 0; box-sizing: border-box; } 11 body { font-family: "Microsoft YaHei", sans-serif; } 12 #mapContainer { width: 100vw; height: 100vh; } 13 .page-title { 14 position: absolute; top: 20px; left: 50%; transform: translateX(-50%); 15 background: rgba(255,255,255,0.95); padding: 10px 24px; border-radius: 6px; 16 font-size: 18px; font-weight: bold; z-index: 100; 17 box-shadow: 0 2px 8px rgba(0,0,0,0.15); 18 } 19 .control-panel { 20 position: absolute; top: 80px; right: 20px; 21 background: rgba(255,255,255,0.95); padding: 16px; border-radius: 8px; 22 box-shadow: 0 2px 12px rgba(0,0,0,0.15); 23 z-index: 100; min-width: 320px; 24 } 25 .control-panel h3 { margin: 0 0 8px 0; font-size: 14px; color: #333; } 26 .section { margin-bottom: 12px; padding-bottom: 10px; border-bottom: 1px solid #eee; } 27 .section:last-child { border-bottom: none; margin-bottom: 0; } 28 .btn-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 6px; } 29 .btn-row button { 30 flex: 1; min-width: 60px; padding: 6px 0; 31 border: 1px solid #d9d9d9; border-radius: 4px; 32 background: white; cursor: pointer; font-size: 12px; 33 } 34 .btn-row button:hover { border-color: #1890ff; color: #1890ff; } 35 .btn-row button.on { background: #1890ff; color: white; border-color: #1890ff; } 36 .slider-row { margin-top: 8px; font-size: 12px; color: #333; } 37 .slider-row label { display: flex; justify-content: space-between; margin-bottom: 2px; } 38 .slider-row .v { color: #1890ff; font-weight: bold; } 39 .slider-row input[type=range] { width: 100%; } 40 .info-card { 41 margin-top: 10px; padding: 10px 12px; 42 background: #f0f5ff; border-radius: 6px; 43 border-left: 3px solid #1890ff; font-size: 12px; line-height: 1.6; 44 } 45 .info-card .val { font-weight: bold; color: #1890ff; } 46 .status-text { 47 position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); 48 background: rgba(0,0,0,0.7); color: white; padding: 8px 20px; 49 border-radius: 20px; font-size: 13px; z-index: 100; pointer-events: none; 50 white-space: nowrap; 51 } 52 </style> 53</head> 54<body> 55<h1 class="page-title">第32课:VoxelSlice 体元切片</h1> 56 57<div class="control-panel"> 58 <div class="section"> 59 <h3>➕ 添加切片(VoxelSlice)</h3> 60 <div class="btn-row"> 61 <button id="btnAddH">水平切片</button> 62 <button id="btnAddV">垂直切片</button> 63 <button id="btnRemove">移除最末</button> 64 </div> 65 </div> 66 <div class="section"> 67 <h3>🎯 选中切片</h3> 68 <div class="btn-row"> 69 <button id="btnPrev">上一个</button> 70 <button id="btnNext">下一个</button> 71 <button id="btnEnable" class="on">切片已启用</button> 72 </div> 73 </div> 74 <div class="section"> 75 <h3>⚙️ 切片参数(选中项)</h3> 76 <div class="slider-row"> 77 <label>方向 orientation<span class="v" id="valOri">0°</span></label> 78 <input type="range" id="rngOri" min="0" max="360" value="0"> 79 </div> 80 <div class="slider-row"> 81 <label>倾斜 tilt<span class="v" id="valTilt">0°</span></label> 82 <input type="range" id="rngTilt" min="0" max="180" value="0"> 83 </div> 84 <div class="slider-row"> 85 <label>位置 point.x<span class="v" id="valPos">0</span></label> 86 <input type="range" id="rngPos" min="0" max="100" value="0"> 87 </div> 88 </div> 89 <div class="info-card"> 90 <div>体素尺寸:<span class="val" id="volSize">加载中...</span></div> 91 <div>切片数量:<span class="val" id="sliceCount">0</span></div> 92 <div>选中索引:<span class="val" id="activeIdx">-</span></div> 93 </div> 94</div> 95 96<div class="status-text" id="statusText">VoxelSlice | 沿无限平面裁剪体数据</div> 97 98<div id="mapContainer"></div> 99 100<script type="module"> 101 const Map = await $arcgis.import("@arcgis/core/Map.js"); 102 const SceneView = await $arcgis.import("@arcgis/core/views/SceneView.js"); 103 const VoxelLayer = await $arcgis.import("@arcgis/core/layers/VoxelLayer.js"); 104 const VoxelSlice = await $arcgis.import("@arcgis/core/layers/voxel/VoxelSlice.js"); 105 const getTianditu = await $arcgis.import("https://openlayers.vip/examples/resources/tianditu.js"); 106 107 const vecLayers = getTianditu.default({ type: "vec_w" }); 108 const map = new Map({ basemap: { baseLayers: [vecLayers.base, vecLayers.anno] } }); 109 110 const view = new SceneView({ 111 container: "mapContainer", map: map, 112 viewingMode: "local", 113 camera: { position: { longitude: -70, latitude: 18, z: 800000 }, heading: 0, tilt: 60 } 114 }); 115 window.view = view; 116 117 const voxelLayer = new VoxelLayer({ 118 url: "https://gs3d.geosceneonline.cn/server/rest/services/Hosted/VoxelPM10/SceneServer" 119 }); 120 map.add(voxelLayer); 121 122 let volumeStyle = null; // VoxelVolumeStyle 123 let volSize = null; // sizeInVoxels: [x, y, z] 124 let activeIndex = 0; // 当前选中的切片索引 125 126 view.when(() => { 127 voxelLayer.when(() => { 128 const vol = voxelLayer.getVolume(null); // VoxelVolume(只读元信息) 129 volSize = vol.sizeInVoxels; // 体素空间尺寸 130 document.getElementById("volSize").textContent = volSize.join(" × "); 131 132 // 样式集合:切片通过 VoxelVolumeStyle.slices 管理 133 volumeStyle = voxelLayer.getVolumeStyle(null); 134 voxelLayer.enableSlices = true; // 启用切片可视化 135 136 // 默认添加一条水平切片(位于中间高度) 137 addSlice({ orientation: 0, tilt: 0, point: [0, 0, Math.floor(volSize[2] / 2)] }); 138 // 再添加一条垂直切片(东西向,位于中间经度) 139 addSlice({ orientation: 270, tilt: 90, point: [Math.floor(volSize[0] / 2), 0, 0] }); 140 141 view.goTo(voxelLayer.fullExtent, { duration: 2000 }); 142 }).catch(err => { 143 document.getElementById("statusText").textContent = "加载失败"; 144 console.error(err); 145 }); 146 }); 147 148 // 在体素空间内添加切片;point 坐标为 [x, y, z](体素索引,非地理坐标) 149 function addSlice(props) { 150 const slice = new VoxelSlice(props); 151 volumeStyle.slices.add(slice); 152 activeIndex = volumeStyle.slices.length - 1; 153 syncControlsToActive(); 154 updateInfo(); 155 } 156 157 function updateInfo() { 158 document.getElementById("sliceCount").textContent = volumeStyle.slices.length; 159 document.getElementById("activeIdx").textContent = 160 volumeStyle.slices.length ? activeIndex : "-"; 161 } 162 163 // 将选中切片的属性同步到滑块显示 164 function syncControlsToActive() { 165 if (!volumeStyle.slices.length) return; 166 const s = volumeStyle.slices.getItemAt(activeIndex); 167 document.getElementById("rngOri").value = s.orientation; 168 document.getElementById("rngTilt").value = s.tilt; 169 document.getElementById("rngPos").value = s.point[0]; 170 document.getElementById("valOri").textContent = s.orientation + "°"; 171 document.getElementById("valTilt").textContent = s.tilt + "°"; 172 document.getElementById("valPos").textContent = s.point[0]; 173 updateInfo(); 174 } 175 176 document.getElementById("btnAddH").addEventListener("click", function () { 177 if (!volSize) return; 178 addSlice({ orientation: 0, tilt: 0, point: [0, 0, Math.floor(volSize[2] / 2)] }); 179 document.getElementById("statusText").textContent = "已添加水平切片"; 180 }); 181 182 document.getElementById("btnAddV").addEventListener("click", function () { 183 if (!volSize) return; 184 addSlice({ orientation: 270, tilt: 90, point: [Math.floor(volSize[0] / 2), 0, 0] }); 185 document.getElementById("statusText").textContent = "已添加垂直切片"; 186 }); 187 188 document.getElementById("btnRemove").addEventListener("click", function () { 189 const n = volumeStyle.slices.length; 190 if (n === 0) return; 191 volumeStyle.slices.removeAt(n - 1); // 移除最末切片 192 if (activeIndex >= volumeStyle.slices.length) { 193 activeIndex = Math.max(0, volumeStyle.slices.length - 1); 194 } 195 syncControlsToActive(); 196 document.getElementById("statusText").textContent = "已移除最末切片"; 197 }); 198 199 document.getElementById("btnPrev").addEventListener("click", function () { 200 if (volumeStyle.slices.length === 0) return; 201 activeIndex = (activeIndex - 1 + volumeStyle.slices.length) % volumeStyle.slices.length; 202 syncControlsToActive(); 203 }); 204 205 document.getElementById("btnNext").addEventListener("click", function () { 206 if (volumeStyle.slices.length === 0) return; 207 activeIndex = (activeIndex + 1) % volumeStyle.slices.length; 208 syncControlsToActive(); 209 }); 210 211 document.getElementById("btnEnable").addEventListener("click", function () { 212 voxelLayer.enableSlices = !voxelLayer.enableSlices; 213 this.classList.toggle("on", voxelLayer.enableSlices); 214 this.textContent = voxelLayer.enableSlices ? "切片已启用" : "切片已禁用"; 215 }); 216 217 // 实时修改选中切片的属性:方向 218 document.getElementById("rngOri").addEventListener("input", function () { 219 if (!volumeStyle.slices.length) return; 220 const s = volumeStyle.slices.getItemAt(activeIndex); 221 s.orientation = Number(this.value); 222 document.getElementById("valOri").textContent = this.value + "°"; 223 }); 224 225 document.getElementById("rngTilt").addEventListener("input", function () { 226 if (!volumeStyle.slices.length) return; 227 const s = volumeStyle.slices.getItemAt(activeIndex); 228 s.tilt = Number(this.value); 229 document.getElementById("valTilt").textContent = this.value + "°"; 230 }); 231 232 // 移动选中切片在 x 轴上的位置(体素空间) 233 document.getElementById("rngPos").addEventListener("input", function () { 234 if (!volumeStyle.slices.length) return; 235 const s = volumeStyle.slices.getItemAt(activeIndex); 236 const p = s.point.slice(); 237 p[0] = Number(this.value) * Math.floor(volSize[0] / 100); 238 s.point = p; 239 document.getElementById("valPos").textContent = p[0]; 240 }); 241</script> 242</body> 243</html> 244
五、在线示例
🔗 在线体验:https://southjor.github.io/arcgis-examples/lessons/lesson32.html

操作说明:
- 场景加载后默认添加一条水平切片与一条垂直切片。
- 点击
水平切片/垂直切片可追加切片,移除最末删除最后一条。- 通过
上一个/下一个选择切片,拖动滑块实时改变其orientation/tilt/point.x,观察截面变化。切片已启用切换voxelLayer.enableSlices,可整体开关切片渲染。
六、关键 API 说明
| API | 说明 |
|---|---|
| new VoxelSlice(properties) | 构造单个切片,point 为体素空间坐标 [x,y,z] |
| voxelLayer.getVolume(null) | 返回 VoxelVolume(只读元信息,含 sizeInVoxels) |
| voxelLayer.getVolumeStyle(null) | 返回 VoxelVolumeStyle,其 slices 为 Collection<VoxelSlice> |
| volumeStyle.slices.add(slice) | 向集合添加切片并立即渲染 |
| volumeStyle.slices.removeAt(i) / getItemAt(i) | 集合删除 / 读取,修改属性实时生效 |
| voxelLayer.enableSlices | 是否启用切片可视化(默认 true) |
参考链接: VoxelSlice | VoxelVolumeStyle | 官方示例
七、系列导航
💡 小贴士:
VoxelSlice的point是体素索引而非地理坐标——先用getVolume(null).sizeInVoxels拿到体素尺寸,再以[x,y,z]取其中点,就能稳定切在体积中心。另外,切片在renderMode: "volume"下也有效,不局限于surfaces模式。
