ArcGIS API与Three.js融合:三维场景动态立体墙实现指南

发布时间:2026/10/15 16:28:47
ArcGIS API与Three.js融合:三维场景动态立体墙实现指南 1. 三维场景中动态立体墙效果的整体设计思路1.1 为什么要在三维场景里做动态立体墙做地理信息可视化的人都有一个共同的痛点二维地图看久了数据再多也只是一张平面图决策者看不出空间关系也感受不到数据的“呼吸感”。动态立体墙这个效果本质上是在三维场景里用一堵有高度、有颜色渐变、有流动动画的墙体把某个区域的边界或者某条路径的走向“立”起来让数据从平面变成有体积感的三维实体。我第一次接触这个需求是在一个模拟项目中需要展示某个区域的边界范围。传统的做法是用线框或者半透明面片但视觉冲击力太弱领导看了一眼就说“不够震撼”。后来尝试用立体墙把边界线拉伸成有高度的墙体再加上动态流动的光效整个场景的层次感立刻就不一样了。从技术角度看这个效果解决的核心问题是如何在三维地理场景中用最低的性能开销实现既有空间体积感又有动态视觉引导的边界表达。适合谁来参考这篇内容如果你已经会用ArcGIS API for JavaScript搭建基础的三维场景对Three.js也有初步了解但不知道怎么把两者结合起来做自定义效果那这篇就是写给你的。如果你是完全的新手建议先把ArcGIS API的三维场景搭建和Three.js的基础几何体创建过一遍再来看这篇会顺畅很多。1.2 技术选型的核心考量为什么是ArcGIS API加Three.jsArcGIS API for JavaScript在三维地理场景这块的能力是毋庸置疑的它内置了SceneView、Camera、各种图层体系处理地理坐标系转换、地形起伏、光照阴影这些都很成熟。但它有一个短板自定义几何体的灵活度不够。虽然可以用GeoJSONLayer或者GraphicsLayer画一些简单图形但要做复杂的动态效果比如顶点着色器控制颜色渐变、时间轴驱动动画原生API就显得力不从心了。Three.js恰恰补上了这块短板。它是一个纯WebGL的封装库对几何体、材质、着色器的控制非常自由。你可以用BufferGeometry自己定义顶点用ShaderMaterial写自定义着色器用Clock做时间驱动。把两者结合起来思路就是ArcGIS API负责地理场景的底座Three.js负责在场景里“加戏”。具体怎么结合ArcGIS API的三维场景本质上也是一个WebGL渲染上下文它提供了externalRenderers这个接口允许你在SceneView的渲染循环中插入自定义的WebGL渲染逻辑。你可以把Three.js的Scene和Camera与ArcGIS的SceneView做同步让Three.js渲染的内容和地理场景完美叠加。这个方案的优势在于既保留了ArcGIS API在地理数据处理上的成熟能力又获得了Three.js在视觉效果上的无限可能。注意externalRenderers接口在不同版本的ArcGIS API中略有差异建议使用4.x版本并且在初始化SceneView时确保WebGL上下文支持自定义渲染。1.3 动态立体墙的核心构成要素拆解一堵动态立体墙拆开来看其实就四个核心要素墙体几何体、材质与着色器、动画驱动、地理坐标同步。墙体几何体决定了墙的形状和位置。最简单的做法是给定一条路径一系列经纬度点把每个点拉伸成垂直的线段然后把这些线段连成三角面片。这里的关键是顶点顺序和索引的正确性否则会出现面片法线方向错误导致光照异常。材质与着色器决定了墙的视觉效果。我试过几种方案用MeshBasicMaterial最简单但效果平淡用MeshPhongMaterial有光照但不够炫最后选择ShaderMaterial自己写顶点着色器和片元着色器实现从底部到顶部的颜色渐变、边缘发光、以及随时间流动的光带效果。着色器里可以用uniform float uTime来接收时间参数在片元着色器里根据UV坐标和时间的组合计算出流动效果。动画驱动靠的是Three.js的Clock和requestAnimationFrame在每一帧更新uTime的值然后触发重绘。这里要注意和ArcGIS的渲染循环协调不能各跑各的否则会出现画面撕裂或者性能浪费。地理坐标同步是最容易踩坑的地方。ArcGIS用的是地理坐标系经纬度Three.js用的是笛卡尔坐标系米两者之间需要做转换。ArcGIS API提供了webMercatorUtils和externalRenderers.toRenderCoordinates等方法来做坐标转换把经纬度转成场景内的局部坐标。2. 核心细节解析与实操要点2.1 墙体几何体的构建从路径到三角面片构建墙体几何体的第一步是获取路径数据。假设你有一个多边形或者一条折线你需要提取出它的顶点序列。在ArcGIS API中可以通过geometry.rings或者geometry.paths拿到这些点每个点包含经度和纬度。拿到经纬度序列后不能直接拿去建Three.js的几何体因为经纬度的单位是度而Three.js的场景单位通常是米。你需要做坐标转换。ArcGIS API的externalRenderers.toRenderCoordinates方法可以把地理坐标转成渲染坐标这个转换考虑了当前视图的中心点和缩放级别。转换后的坐标是一个三维向量x和y是平面位置z是高度。接下来是构建墙体的顶点。假设路径上有N个点每个点对应墙体的底部和顶部两个顶点那么总共需要2N个顶点。底部的顶点z坐标为0或者你指定的底部高度顶部的顶点z坐标为墙高。顶点数组按顺序排列先放所有底部顶点再放所有顶部顶点。索引的构建是关键。对于每两个相邻的路径点你需要构建两个三角形来组成一个矩形面片。假设底部顶点索引是i和i1顶部顶点索引是Ni和Ni1那么两个三角形的索引分别是(i, i1, Ni)和(i1, Ni1, Ni)。注意顶点顺序要保证法线朝外否则背面会被剔除。// 假设pathPoints是转换后的三维坐标数组 const wallHeight 50; // 墙体高度单位米 const vertices []; const indices []; // 构建底部和顶部顶点 for (let i 0; i pathPoints.length; i) { const p pathPoints[i]; vertices.push(p.x, p.y, 0); // 底部顶点 vertices.push(p.x, p.y, wallHeight); // 顶部顶点 } // 构建索引 for (let i 0; i pathPoints.length - 1; i) { const bottomA i * 2; const topA i * 2 1; const bottomB (i 1) * 2; const topB (i 1) * 2 1; indices.push(bottomA, bottomB, topA); indices.push(bottomB, topB, topA); }提示如果路径是闭合的记得把最后一个点和第一个点之间的面片也补上否则墙体会有缺口。2.2 着色器编写让墙体“活”起来着色器是动态立体墙的灵魂。我用的方案是自定义ShaderMaterial顶点着色器负责传递UV坐标和位置信息片元着色器负责计算最终颜色。顶点着色器里除了常规的gl_Position计算还需要把顶点的UV坐标传出去。UV的U坐标可以沿路径方向从0到1递增V坐标根据顶点高度从0到1。这样在片元着色器里就能根据UV来定位每个像素在墙体上的位置。片元着色器里做三件事基础颜色渐变、边缘发光、流动光带。基础颜色渐变用mix函数根据V坐标混合底部颜色和顶部颜色。边缘发光可以用fract函数配合U坐标做周期性变化让墙体边缘有呼吸感。流动光带用sin函数结合U坐标和uTime让一条亮带沿着墙体流动。// 片元着色器核心逻辑 uniform float uTime; uniform vec3 uBottomColor; uniform vec3 uTopColor; varying vec2 vUv; void main() { // 基础渐变 vec3 baseColor mix(uBottomColor, uTopColor, vUv.y); // 流动光带 float flow sin(vUv.x * 10.0 - uTime * 2.0) * 0.5 0.5; flow pow(flow, 8.0); // 让光带更集中 // 边缘发光 float edge 1.0 - abs(vUv.y - 0.5) * 2.0; edge pow(edge, 3.0); vec3 finalColor baseColor vec3(flow * 0.5 edge * 0.3); gl_FragColor vec4(finalColor, 0.8); }这个着色器的效果是墙体从底部到顶部有颜色渐变中间有一条流动的亮带边缘有柔和的发光。透明度设为0.8让墙体有半透明的质感不会完全遮挡后面的场景。注意着色器里的数值需要根据实际场景调整。比如流动速度、光带宽度、发光强度这些参数在不同分辨率和不同场景尺度下效果差异很大建议做成uniform变量方便调试。2.3 与ArcGIS场景的同步externalRenderers的正确用法把Three.js的内容渲染到ArcGIS的三维场景里核心是externalRenderers.add方法。你需要在SceneView的view.environment或者渲染循环中注册一个自定义渲染函数。具体步骤是这样的首先创建一个Three.js的Scene和CameraCamera的投影矩阵和视图矩阵需要和ArcGIS的Camera保持同步。ArcGIS API提供了externalRenderers.renderCameraToRenderCamera方法可以把ArcGIS的Camera参数转换到Three.js的Camera上。然后在自定义渲染函数里更新Three.js的Camera矩阵更新uTime调用Three.js的renderer.render方法。最后用externalRenderers.resetWebGLState重置WebGL状态避免影响ArcGIS自己的渲染。// 注册自定义渲染 externalRenderers.add(view, custom-wall, { render: function(context) { // 同步相机 const cam context.camera; threeCamera.projectionMatrix.fromArray(cam.projectionMatrix); threeCamera.matrixWorld.fromArray(cam.viewMatrix); threeCamera.matrixWorldInverse.copy(threeCamera.matrixWorld).invert(); // 更新时间 wallMaterial.uniforms.uTime.value performance.now() / 1000; // 渲染 threeRenderer.resetState(); threeRenderer.render(threeScene, threeCamera); context.resetWebGLState(); } });这里有个坑Three.js的renderer需要和ArcGIS共享同一个WebGL上下文否则会出现两个canvas叠加的问题。正确的做法是用new THREE.WebGLRenderer({ context: view.context })来创建renderer这样它们就在同一个上下文里渲染了。3. 实操过程与核心环节实现3.1 环境搭建与依赖引入先说一下我用的环境ArcGIS API for JavaScript 4.25版本Three.js用r150左右的版本。这两个版本搭配比较稳定再新的版本API变动较大需要额外适配。引入方式很简单ArcGIS API用CDN引入Three.js用npm安装或者CDN都行。如果你用模块化开发可以在AMD的require回调里加载Three.js。!-- ArcGIS API -- link relstylesheet hrefhttps://js.arcgis.com/4.25/esri/themes/light/main.css script srchttps://js.arcgis.com/4.25//script !-- Three.js -- script srchttps://cdnjs.cloudflare.com/ajax/libs/three.js/r150/three.min.js/script初始化SceneView的时候记得把view.environment.atmosphereEnabled设为false不然大气效果会干扰自定义渲染的透明度。还有view.ui.components可以精简一下去掉不需要的组件。require([ esri/Map, esri/views/SceneView, esri/geometry/Polyline, esri/geometry/support/webMercatorUtils ], function(Map, SceneView, Polyline, webMercatorUtils) { const map new Map({ basemap: satellite }); const view new SceneView({ container: viewDiv, map: map, camera: { position: [116.4, 39.9, 5000], tilt: 45, heading: 0 }, environment: { atmosphereEnabled: false } }); view.when(function() { initThreeJS(view); }); });3.2 路径数据的获取与坐标转换假设你有一个多边形的几何数据先把它转成Polyline或者直接取rings。然后对每个点做坐标转换。function convertPathToRenderCoordinates(view, path) { const renderPoints []; const center view.center.clone(); for (let i 0; i path.length; i) { const pt path[i]; const renderPt externalRenderers.toRenderCoordinates( view, [pt[0], pt[1], 0], // 经纬度高度0 0, view.spatialReference, [0, 0, 0], // 输出数组 0, 1 ); renderPoints.push({ x: renderPt[0], y: renderPt[1], z: renderPt[2] }); } return renderPoints; }这里有个细节toRenderCoordinates的第三个参数是点的索引第四个参数是空间参考第五个参数是输出数组。如果你要批量转换多个点可以一次性传入所有点效率会高很多。转换后的坐标是相对于视图中心的局部坐标单位是米。这个坐标系的原点在视图中心x轴向东y轴向北z轴向上。Three.js的场景就用这个坐标系不需要额外转换。3.3 墙体几何体的完整构建流程拿到转换后的坐标数组后就可以构建几何体了。我习惯把几何体构建封装成一个函数传入路径点和墙高返回BufferGeometry。function createWallGeometry(points, height) { const vertices []; const indices []; const uvs []; // 计算路径总长度用于UV的U坐标 let totalLength 0; const lengths [0]; for (let i 1; i points.length; i) { const dx points[i].x - points[i-1].x; const dy points[i].y - points[i-1].y; totalLength Math.sqrt(dx*dx dy*dy); lengths.push(totalLength); } // 构建顶点 for (let i 0; i points.length; i) { const p points[i]; const u lengths[i] / totalLength; // 底部顶点 vertices.push(p.x, p.y, 0); uvs.push(u, 0); // 顶部顶点 vertices.push(p.x, p.y, height); uvs.push(u, 1); } // 构建索引 for (let i 0; i points.length - 1; i) { const bA i * 2; const tA i * 2 1; const bB (i 1) * 2; const tB (i 1) * 2 1; indices.push(bA, bB, tA); indices.push(bB, tB, tA); } const geometry new THREE.BufferGeometry(); geometry.setAttribute(position, new THREE.Float32BufferAttribute(vertices, 3)); geometry.setAttribute(uv, new THREE.Float32BufferAttribute(uvs, 2)); geometry.setIndex(indices); geometry.computeVertexNormals(); return geometry; }UV的U坐标按路径长度比例分配这样流动光带在整条路径上的速度是均匀的。如果按顶点索引平均分配路径长短不一的时候光带速度会不均匀。3.4 材质与动画的完整实现材质用ShaderMaterial把前面写的着色器代码放进去。需要传入的uniform包括uTime、uBottomColor、uTopColor。const wallMaterial new THREE.ShaderMaterial({ uniforms: { uTime: { value: 0 }, uBottomColor: { value: new THREE.Color(0x0066ff) }, uTopColor: { value: new THREE.Color(0x00ffff) } }, vertexShader: varying vec2 vUv; void main() { vUv uv; gl_Position projectionMatrix * modelViewMatrix * vec4(position, 1.0); } , fragmentShader: uniform float uTime; uniform vec3 uBottomColor; uniform vec3 uTopColor; varying vec2 vUv; void main() { vec3 baseColor mix(uBottomColor, uTopColor, vUv.y); float flow sin(vUv.x * 10.0 - uTime * 2.0) * 0.5 0.5; flow pow(flow, 8.0); float edge 1.0 - abs(vUv.y - 0.5) * 2.0; edge pow(edge, 3.0); vec3 finalColor baseColor vec3(flow * 0.5 edge * 0.3); gl_FragColor vec4(finalColor, 0.8); } , transparent: true, side: THREE.DoubleSide, depthWrite: false });side设为DoubleSide是因为墙体从两侧看都需要可见。depthWrite设为false是为了避免半透明物体之间的深度冲突但要注意渲染顺序可能需要手动设置renderOrder。动画驱动就是在自定义渲染函数里更新uTime的值。我用的是performance.now() / 1000这样时间单位是秒流动速度比较自然。3.5 性能优化与渲染顺序控制动态立体墙的性能开销主要来自三个方面顶点数量、着色器复杂度、重绘频率。顶点数量取决于路径的精细程度如果路径点太多可以先用道格拉斯-普克算法做简化减少顶点数。着色器复杂度方面片元着色器里的sin和pow运算比较耗时如果墙体面积很大可以考虑降低片元着色器的计算频率或者用更简单的函数替代。重绘频率方面不需要每帧都更新uTime可以每两帧更新一次视觉上几乎看不出差别但性能能提升不少。渲染顺序也很关键。半透明物体需要在不透明物体之后渲染而且半透明物体之间要按照从远到近的顺序渲染。Three.js的renderer会自动排序但和ArcGIS的场景混合渲染时可能需要手动控制。我的做法是把墙体的renderOrder设为一个较大的值确保它在ArcGIS的地形和建筑之后渲染。实操心得如果发现墙体在某些角度下闪烁或者被地形遮挡异常检查一下depthTest和depthWrite的设置。通常depthTest保持truedepthWrite设为false配合正确的renderOrder就能解决大部分问题。4. 常见问题与排查技巧实录4.1 墙体不显示或者显示位置偏移这是最常见的问题原因通常出在坐标转换环节。ArcGIS的toRenderCoordinates方法返回的坐标是相对于视图中心的如果视图中心变了坐标也要重新计算。如果你在视图初始化之前就调用了转换拿到的坐标可能是错的。排查步骤先在控制台打印转换后的坐标看看数值范围是否合理。如果x和y都是几百到几千的量级说明转换正常。如果数值特别大或者特别小可能是空间参考传错了。另一个常见原因是Three.js的Camera没有和ArcGIS的Camera同步。检查一下自定义渲染函数里是否正确更新了Camera的投影矩阵和视图矩阵。如果矩阵没更新Three.js渲染的内容会出现在错误的位置。还有一种情况是墙体被地形遮挡了。ArcGIS的地形图层会写入深度缓冲如果墙体的深度测试没通过就会被地形挡住。解决办法是把墙体的depthTest设为false或者调整墙体的高度让它在地形之上。4.2 着色器编译报错或者效果异常着色器报错通常是因为GLSL语法问题。ArcGIS API和Three.js对GLSL版本的要求可能不同ArcGIS用的是WebGL 1.0的GLSLThree.js r150默认也是WebGL 1.0但如果你用了WebGL 2.0的特性就会报错。排查方法在浏览器的开发者工具里看Console面板着色器编译错误会打印出具体的行号和错误信息。常见的错误包括变量未声明、类型不匹配、函数参数数量不对。效果异常方面如果颜色不对检查uniform是否正确传入。如果流动效果不动检查uTime是否在更新。如果边缘发光太强或太弱调整pow函数的指数。避坑技巧着色器调试很痛苦建议先用最简单的着色器比如直接输出红色确认渲染管线通了再逐步加效果。每加一个效果就测试一次不要一次性写完再调。4.3 性能卡顿与帧率下降如果场景里墙体很多或者路径很复杂帧率可能会明显下降。先用浏览器的Performance面板录一段看看时间花在哪里。如果是着色器计算太慢简化片元着色器如果是顶点太多做路径简化如果是重绘太频繁降低更新频率。还有一个容易被忽略的点Three.js的renderer每次渲染都会清空画布如果和ArcGIS的渲染循环冲突会导致重复渲染。正确的做法是在自定义渲染函数里不要调用renderer.clear()让ArcGIS自己管理清空操作。如果用了多个墙体可以考虑合并成一个几何体减少draw call。Three.js的BufferGeometryUtils.mergeBufferGeometries可以把多个几何体合并性能提升很明显。4.4 常见问题速查表问题现象可能原因排查方法解决方案墙体完全不显示坐标转换错误打印转换后坐标检查toRenderCoordinates参数墙体位置偏移Camera未同步检查矩阵更新同步projectionMatrix和viewMatrix墙体被地形遮挡深度测试冲突检查depthTest设置设depthTest为false或调整高度着色器编译报错GLSL语法问题看Console错误信息检查变量声明和类型流动效果不动uTime未更新打印uTime值在渲染循环中更新uTime帧率明显下降顶点过多或着色器复杂Performance面板分析简化路径或着色器墙体闪烁渲染顺序问题检查renderOrder调整renderOrder和depthWrite颜色与预期不符uniform传值错误打印uniform值检查Color对象和传值方式4.5 独家避坑经验分享第一个坑ArcGIS API的版本兼容性。4.20之前的版本externalRenderers接口和之后的有差异如果你看的教程是旧版本的代码可能跑不起来。建议统一用4.24以上的版本。第二个坑Three.js的Color对象在着色器里是线性空间的而ArcGIS的场景可能是sRGB空间的颜色会有偏差。解决办法是在着色器里做gamma校正或者直接用vec3传颜色值不用Color对象。第三个坑如果路径是闭合的最后一个点和第一个点之间的面片容易漏掉。我的做法是在构建几何体之前先判断路径是否闭合如果是闭合的在数组末尾追加第一个点。第四个坑半透明墙体的渲染顺序和地形、建筑混合时容易出问题。我的经验是把墙体的renderOrder设为一个较大的值比如999确保它在所有不透明物体之后渲染。同时把depthWrite设为false避免墙体之间互相遮挡。第五个坑如果场景缩放级别变化很大墙体的高度可能需要动态调整。比如在低缩放级别下50米的墙几乎看不见需要按比例放大。我的做法是根据视图的scale动态计算墙高让视觉效果在不同缩放级别下保持一致。5. 效果扩展与进阶玩法5.1 多墙体与路径动画单个墙体做出来之后很容易扩展到多个墙体。比如你有多个区域需要标注可以循环创建多个几何体和材质每个墙体用不同的颜色和流动速度。注意材质要每个墙体单独创建因为uniform是每个材质独立的共享材质会导致所有墙体用同一个uTime。路径动画是另一个有意思的扩展。让墙体沿着路径“生长”出来或者让墙体在路径上移动。生长效果可以通过控制几何体的drawRange来实现逐渐增加索引数量。移动效果可以通过更新几何体的position属性来实现。5.2 与数据联动的动态效果动态立体墙最有价值的扩展方向是和实时数据联动。比如墙体的高度代表某个指标的大小颜色代表指标的变化趋势流动速度代表数据更新频率。这样一堵墙就不只是装饰而是有信息量的可视化元素。实现思路是把数据值映射到uniform变量上在数据更新时更新uniform触发重绘。比如用uHeightScale控制墙高用uColorMix控制颜色混合比例。数据更新频率不高的话不需要每帧更新用事件驱动的方式更新即可。5.3 移动端适配与性能取舍移动端的GPU性能有限动态立体墙的效果需要做取舍。我的经验是降低着色器复杂度去掉边缘发光效果只保留基础渐变和流动光带减少顶点数量路径简化阈值调大降低重绘频率每三帧更新一次uTime。另外移动端的WebGL上下文数量有限如果同时有多个SceneView可能会创建失败。建议在移动端只保留一个SceneView把其他可视化内容合并到同一个场景里。我在实际项目里踩过的坑是移动端浏览器对浮点纹理的支持不一致如果着色器里用了浮点纹理在某些设备上会黑屏。解决办法是尽量用普通纹理或者做特性检测后降级处理。这个效果后续还可以这样扩展把墙体和粒子系统结合在墙体顶部加一层粒子飘散效果或者把墙体和光照结合让墙体对周围场景产生动态光照影响。不过这些扩展的性能开销会成倍增加需要根据实际场景权衡。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询