从第一篇记录到现在,大约过了三周。期间项目的核心框架已经跑通,基础场景搭建、相机控制、光源配置这些底子都打好了。这篇记录一下第二阶段的实战内容:三维建筑体生成、标注系统、飞线动画、点击交互,以及一个大屏可视化项目从数据到落地的完整链路。如果你正在用three.js做3D地图相关项目,这篇内容应该能帮你少踩一些我踩过的坑。
1. 项目现状与这一阶段要解决的核心问题
1.1 第一期完成的内容回顾
第一期主要完成了三件事:一是选型确认,对比了Three.js、Cesium、Mapbox GL之后,最终确定以three.js作为基础渲染引擎;二是搭建了基础场景,包括场景、透视相机、OrbitControls轨道控制器、半球光加平行光的光源组合;三是加载了区域底图数据,将在线瓦片地图作为地面纹理,初步实现了地图的平移、旋转、缩放。
整体架构不复杂,核心就是Renderer(渲染器)、Scene(场景)、Camera(相机)三者各司其职。渲染器负责把场景画到屏幕上,场景里装所有物体,相机决定从哪个角度去看。这个架构在3D地图场景里尤其重要——它决定了后续每加一个功能模块时,是往里填充内容还是推翻重来。
第一期结束时的效果是:一块贴了卫星图的平面,带有基本的光影效果,可以用鼠标拖拽旋转视角。但离一个真正可用的3D地图可视化大屏还差得很远。
1.2 第二阶段的目标拆解
第二期接到的需求很明确:做一个区域3D地图可视化大屏,要求展示区域内重点建筑的三维形态,叠加业务数据(如楼宇入驻率、能耗、人流量),支持点击建筑查看详情,同时要有数据流动的效果来体现业务流转。
拆解下来,核心问题有四个:
- 如何把平面地图变成“立体地图”,也就是在正确的位置生成长方体建筑块
- 如何把业务数据绑定到三维物体上,让数据跟随模型,而不是悬浮在场景里算个叠加层
- 如何实现飞线、脉冲等动效,让大屏看起来“活”起来
- 如何解决场景卡顿问题——建筑数量一多,draw call指数级上升,帧率直线下降
这一篇不写原理教科书,直接记录实操过程和踩坑心得,每个环节都附上我的思考逻辑和最终落地的代码方案。
2. 技术方案选型:为什么继续用three.js而不是换Cesium
2.1 three.js、Cesium、Mapbox GL的取舍逻辑
在项目开始前,团队里有人提过要不要换Cesium,理由是“Cesium是专业GIS引擎,自带地形、影像、矢量切片,处理地理数据更成熟”。这个建议有一定道理,但我最终还是选择坚持用three.js。
关键在于项目的真实诉求。这个项目本质上是一个“数字孪生风格的可视化大屏”,核心是展示建筑形态和数据关系,而不是做精确的地理分析。Cesium强在真实地理坐标、全球尺度、海量地形影像,但在“酷炫大屏”这个方向上,它需要叠加大量自定义Shader和特效代码,灵活性反而不如three.js。three.js的优势在于它是一个通用3D渲染引擎,所有物体都是Mesh,想怎么改材质、加动画、做交互都相对自由。地图底图在three.js里只是一个大平面,建筑就是BoxGeometry拉伸出来的立方体,数据通过自定义属性挂载到物体上,这些在Cesium里反而要绕很多弯子。
另外,团队已经有three.js的基础,直接上手成本最低。3D地图可视化项目的技术选型,关键不在于哪个引擎“最强”,而在于哪个最适合当前团队和当前需求。
2.2 基于WebGL的架构决策
从底层来看,three.js封装了WebGL,我们不用直接写GLSL Shader就能实现大部分视觉效果。但理解底层原理依然重要——比如draw call的限制、纹理采样的规则、深度缓冲的工作方式,这些知识在后期做性能优化时是刚需。
大屏项目有一个特殊点:它运行在一个固定分辨率的屏幕上(通常是1080p或4K的拼接大屏),用户不会缩放浏览器窗口。这意味着我们可以针对固定的视口尺寸做优化,比如锁定相机视锥体参数、控制像素比(pixelRatio)上限。这是一个很多教程不会提的高性价比优化手段。
所以架构上,我采用了“three.js为渲染核心 + 少量自定义Shader + 数据驱动配置”的方式。所有建筑、标注、飞线的样式数据都通过JSON文件配置,前端只负责解析和渲染。这样做的直接好处是:甲方改数据时不用动代码,改JSON就行。
3. 核心功能实现:从数据到3D场景的关键链路
3.1 坐标系与配准问题,最难的一步
做3D地图,第一个绕不开的问题是坐标系。GIS领域常用的坐标系包括WGS84(经纬度)、Web Mercator(投影坐标),而three.js使用的是笛卡尔直角坐标系(x、y、z)。如何把经纬度坐标映射到three.js场景里,直接决定了建筑能不能落到正确的位置上。
最粗暴的做法是:取区域的中心点作为原点,然后计算每个点相对中心点的偏移量,将偏移量换算成three.js的x和z坐标。换算公式如下:
const center = [116.39, 39.92]; // 区域中心点经纬度 const origin = new THREE.Vector3(0, 0, 0); function lonLatToVector3(lon, lat, height = 0) { // 每度纬度约等于 111320 米,每度经度随纬度变化 const x = (lon - center[0]) * 111320 * Math.cos(center[1] * Math.PI / 180); const z = (lat - center[1]) * 111320; return new THREE.Vector3(x, height, z); }这个公式是第一版用的近似方法,在区域尺度不大(方圆几公里)时误差可以接受。原因在于:将经纬度直接做线性映射,本质上是在局部用平面近似球面,区域越小误差越小。
但如果项目范围扩大到几十公里甚至更广,就必须用Web Mercator投影做严格换算,否则边缘区域会出现明显的形变偏移。我的经验是:城市级别的可视化项目,用上面的近似公式完全够用;省级甚至全国级的项目,必须有专业的投影转换环节。
3.2 建筑体块生成:数据驱动与样式控制
有了坐标换算能力,接下来就是生成建筑模型。项目拿到的原始数据是建筑物的轮廓坐标(一个多边形)加高度属性,这是GIS数据最常见的格式之一。
方案一:将轮廓拉伸成3D体块。具体做法是在three.js中使用THREE.Shape解析轮廓点,然后通过ExtrudeGeometry拉伸成带高度的立体几何体。这是最标准、最灵活的方案。
方案二:针对规则矩形建筑,直接用BoxGeometry生成。BoxGeometry只需中心点坐标、长宽高三个参数即可生成,代码简单,性能也好。但真实城市里没有多少建筑是完美矩形,效果会失真。
方案三:人工建模(如使用Blender建模后导出glTF)。效果最好,但工作量大,不适合几十上百栋建筑的大规模场景。
最终我采用的是方案一的变体——用ExtrudeGeometry生成真实轮廓体块,但对最小高度做了过滤处理。地下室、配电站这类小额建筑不生成体块,避免场景中出现大量又矮又小的碎片化模型。
建筑生成的核心代码如下:
function createBuilding(geoPoints, height, level) { const shape = new THREE.Shape(); geoPoints.forEach((point, index) => { const v = lonLatToVector3(point[0], point[1]); if (index === 0) { shape.moveTo(v.x, v.z); } else { shape.lineTo(v.x, v.z); } }); const extrudeSettings = { depth: height, bevelEnabled: false }; const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); // 旋转几何体使拉伸方向朝上(Y轴) geometry.rotateX(-Math.PI / 2); const material = createBuildingMaterial(level); const mesh = new THREE.Mesh(geometry, material); mesh.position.y = height / 2; mesh.userData = { height, level, type: 'building' }; return mesh; }这里有个细节很多人容易忽略:ExtrudeGeometry默认沿Z轴拉伸,而three.js的3D地图场景中,Y轴通常是向上的方向,所以必须旋转几何体使其方向正确。
3.3 建筑材质与楼层差异化显示
为了让大屏效果更有层次感,我根据建筑高度做了分级配色。20米以下的建筑用深灰色,体现城市基底;20米到60米用银灰色,作为过渡区域;60米以上的高层建筑用带有科技感的半透明蓝色,突出核心建筑。
材质使用的是MeshPhongMaterial配合少量透明度和金属感处理。这里有一个性能小技巧:如果场景中大量建筑共用一种材质,可以让它们使用同一个材质实例,而不是每个Mesh创建独立的Material。
function createBuildingMaterial(level) { const colorMap = { low: 0x3a4a5a, mid: 0x5a6a7a, high: 0x1e90ff }; return new THREE.MeshPhongMaterial({ color: colorMap[level], transparent: true, opacity: level === 'high' ? 0.85 : 1, shininess: 30 }); }另外,我还实现了按楼层生成横向线条的“窗格效果”。原理很简单,不单独建模,而是用Shader在片元着色器里根据世界坐标的Y值绘制横向条纹,叠加在建筑材质上。这样可以避免为每栋楼创建大量子网格,性能开销极小,视觉效果却很细腻。这个思路在数字孪生项目中非常实用——用Shader做细节,不要用模型堆细节。
4. 业务数据绑定与标注系统
4.1 数据驱动的建筑拾取与信息展示
3D地图大屏离不开交互:点击建筑,弹出详情面板。这个功能的本质是“屏幕坐标反算三维物体”。three.js提供了Raycaster(射线投射器)来完成这个工作:将鼠标的屏幕坐标转换为3D空间中的射线,再检测射线与场景中物体的交点。
const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); function onMouseClick(event) { mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(buildingMeshes); if (intersects.length > 0) { const building = intersects[0].object; showBuildingInfo(building.userData); highlightBuilding(building); } }要让建筑数据跟随模型,关键在于userData这个属性。每个Mesh都有一个userData对象,可以存放任意自定义数据,比如建筑名称、层数、面积、入驻率、能耗值等。业务上只需要在创建建筑时将数据写入userData,后续任何交互都能直接读取,不需要维护独立的映射表。
点击后的高亮效果,我采用的是“描边+提亮”方案。描边用THREE.BoxHelper可以快速实现,但更好的做法是给建筑添加一个略微放大的半透明外壳Mesh,既能产生科技感光晕,又不会遮挡本体。
4.2 标注系统:CSS2DRenderer与Sprite的选择
大屏需要显示建筑名称和关键指标,标注是刚需。three.js里有两种主流的文字标注方案。
方案一:CSS2DRenderer。这是three.js官方扩展库,核心思路是将DOM元素与3D坐标绑定,让HTML标签跟随3D物体移动。优点是不用处理文字纹理,中文渲染效果好,样式可以用CSS随意控制;缺点是在WebGL画布上叠加DOM元素,会遮挡WebGL内容,无法参与深度测试。
方案二:Sprite(精灵)。将Canvas绘制的文字作为纹理贴在一个始终面向相机的平面上,完全在WebGL中渲染,能参与深度遮罩,视觉效果好,但动态更新文字需要重新绘制Canvas,且大量Sprite会消耗纹理内存。
我的最终方案是:默认使用CSS2DRenderer,因为它开发效率最高,样式调整最方便;在需要深度遮挡或特殊效果的关键建筑上,用Sprite做补充。这样兼顾了开发效率和视觉效果。
import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'; function createLabel(text, position, className) { const div = document.createElement('div'); div.className = className || 'building-label'; div.textContent = text; const label = new CSS2DObject(div); label.position.copy(position); return label; }CSS2DObject的核心就是让DOM元素拥有一个3D坐标,renderer在每帧渲染时会自动将3D坐标投影到屏幕位置。需要注意的坑是:CSS2DRenderer要单独创建一个渲染器,并且它的DOM元素(一个div容器)需要通过CSS设置为绝对定位且pointer-events: none,否则会挡住鼠标操作。
4.3 信息面板与大屏布局的结合
建筑详情面板我采用原生DOM实现,放在页面右侧,点击建筑时通过CSS动画滑出。面板内容通过构造字符串动态填充。
大屏的布局遵循经典的“中间主视图 + 两侧辅助面板”结构。左侧放区域概况数据、右侧放建筑详情,底部放时间轴和飞线图例。需要注意的是:辅助面板不要铺满整个视口,给主视图留出足够空间,否则3D场景的可视范围被挤占,大屏会显得很局促。
5. 飞线动画与粒子特效的实现
5.1 基于CatmullRomCurve3的飞线生成
飞线是大屏可视化里最常见的动效元素,用来表示数据从A点到B点的流动。原理并不复杂:用一条贝塞尔曲线连接两个点,然后在曲线上匀速移动若干个小球或箭头,就形成了“数据流动”的视觉。
three.js中可以用CatmullRomCurve3生成平滑曲线。我生成的飞线,起点和终点是两栋建筑的顶部坐标,中间控制点取两点中点的上方位置,形成一个弧线拱起的效果。
function createFlightLine(start, end, heightOffset = 200) { const mid = new THREE.Vector3( (start.x + end.x) / 2, Math.max(start.y, end.y) + heightOffset, (start.z + end.z) / 2 ); const curve = new THREE.CatmullRomCurve3([start, mid, end]); // 飞线本体:一条沿曲线的管线 const tubeGeometry = new THREE.TubeGeometry(curve, 64, 0.6, 8, false); const material = new THREE.MeshBasicMaterial({ color: 0x00e5ff, transparent: true, opacity: 0.6 }); const tube = new THREE.Mesh(tubeGeometry, material); // 流动的小球:沿着曲线取点 const flowPoint = new THREE.Sprite( new THREE.SpriteMaterial({ color: 0x00ffff, transparent: true, opacity: 0.9 }) ); flowPoint.scale.set(12, 12, 1); return { tube, flowPoint, curve }; }运动逻辑在动画循环里实现:每次更新sprite的位置,取曲线上某个特定比例的点。
function updateFlightLine(flight, progress) { const point = flight.curve.getPoint(progress); // progress 从 0 到 1 flight.flowPoint.position.copy(point); }用曲线参数t(0到1之间)来表示运动进度,循环播放时让t从0递增到1再归零,就形成了连续的流动效果。为了看起来更自然,我给不同的飞线设置了不同的运动周期和起始相位,避免所有小球同步移动,视觉上更接近真实数据流。
5.2 脉冲波与粒子光点
除了飞线,我还加了两种粒子动效。
第一种是建筑的脉冲波效果。选中建筑时,从建筑顶部向外扩散一个半透明的圆环,模拟雷达波。实现思路:用RingGeometry创建一个圆环Mesh,通过scale随时间变化放大,同时降低透明度,放大到一定程度后重置。
第二种是场景中的漂浮粒子。在城市上方随机生成几百个粒子,让它们缓慢上下浮动,模拟能量粒子的感觉。这个效果用THREE.Points实现,配合PointsMaterial,性能开销非常小。
粒子系统是3D地图项目中性价比最高的特效手段。几百个粒子就能让画面“活”起来,而GPU处理这些粒子的成本几乎可以忽略。但要注意,粒子的数量不宜过多,又不是点云项目,500个以内足够了,再多就喧宾夺主,还会影响性能。
5.3 三种动效的性能对比与选型
| 动效类型 | 实现方式 | 性能开销 | 适用场景 |
|---|---|---|---|
| 飞线 | TubeGeometry + Sprite | 低 | 数据流转、关系网络 |
| 脉冲波 | RingGeometry + scale动画 | 极低 | 重点点位标识、选中反馈 |
| 漂浮粒子 | THREE.Points + 顶点动画 | 极低 | 场景氛围营造 |
实际项目中,动效不是越多越好。真实大屏交付后我发现,飞线数量控制在20条以内、粒子在300个左右时效果最好。数量再多,画面会杂乱,反而掩盖了核心信息。这个度需要根据实际项目反复调整。
6. 性能优化:让大场景维持流畅帧率的硬手段
6.1 优化Draw Call:合并与实例化
当建筑数量达到上百栋,每栋楼一个Mesh,就会有几百个draw call。再加上飞线、粒子、地面、标注,帧率很容易掉到30fps以下并持续卡顿。
优化的第一步是合并。对于不参与独立交互的建筑,使用BufferGeometryUtils.mergeBufferGeometries将它们的几何体合并成一个。合并前需要在userData里记录这些建筑不可单独点击,交互需求的建筑单独保留。
import { mergeBufferGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'; const geometries = []; staticBuildings.forEach(building => { geometries.push(building.geometry); }); const mergedGeometry = mergeBufferGeometries(geometries); const mergedMesh = new THREE.Mesh(mergedGeometry, staticMaterial);这个操作可以把几百个draw call瞬间降到个位数。代价是合并后的Mesh无法单独操作或拾取,所以需要按照“可交互”和“纯展示”两个维度对建筑做分级处理。
对于重复度高的物体(比如路灯、树木、路障),可以用InstancedMesh实例化渲染。InstancedMesh只提交一次几何体和材质,但通过矩阵数组绘制成千上万个实例。我在场景里添加路灯的时候用这个技术,300盏路灯只增加了一个draw call。
6.2 LOD(细节层次)策略
对于大场景地图,LOD是必要的。远处的建筑不需要完整的细节,一个简化的长方体就足够;靠近相机的建筑才需要展示完整轮廓。
我实现了分三档的LOD方案:近景(200米内)用完整ExtrudeGeometry体块,中景(200-600米)用简化体块(轮廓简化为矩形),远景(600米以上)直接切换为半透明色块,甚至可以通过设置camera.far控制整体可见范围,减少不必要渲染。
LOD的切换阈值需要根据实际场景尺寸调整。不同的场景缩放范围差异很大,直接套教程里的数字会出问题。正确做法是:先运行动画,在相机操作过程中观察切换是否明显,再用数值微调阈值。
6.3 像素比控制与抗锯齿策略
大屏通常接在4K的电视或拼接屏上,如果没有限制像素比,渲染器会按设备的物理像素渲染,4K屏上像素比是2甚至更高,性能开销呈指数级上升。
强制限制像素比是最简单有效的优化手段:
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));此外,大屏展示以静态视角为主,可以关闭或降低抗锯齿。抗锯齿(MSAA)在WebGL里是默认开启的,对性能有影响。对于大屏近距离观看的场景,抗锯齿确实有必要保留,但在小会议室大屏上,2K分辨率下关掉抗锯齿肉眼几乎察觉不到差别,而帧率能提升接近20%。
我的实际做法是:在开发调试阶段开启抗锯齿,方便检查细节;部署到大屏时,根据实际情况决定是否关闭。这个配置可以用环境变量或URL参数控制,不用频繁改代码。
6.4 纹理压缩与加载策略
地图底图和建筑纹理的加载也是性能瓶颈。在线瓦片地图如果一次性加载而没有任何缓存策略,会出现明显的加载白屏和内存暴涨。我的方案是:
- 瓦片地图使用瓦片服务自带的多级缩放,加载时控制只加载当前视角范围内的瓦片
- 建筑纹理压缩到512x512或256x256以下
- 地面纹理用3张Mipmap级别递减的贴图,距离越远使用越低精度的层级
这里用到了three.js内置的纹理Mipmap机制。设置纹理的minFilter为THREE.LinearMipmapLinearFilter,并让贴图自带Mipmap链,GPU在采样远距离表面时会自动选择低精度的mip层级,开销小且效果好。
7. 调试与避坑心得(第二期踩过的坑)
7.1 建筑浮空或陷入地下的问题
第一期就遇到过建筑不在正确高度的问题,第二期又出现了。根部原因是建筑几何体在y方向上的原点位置不同。ExtrudeGeometry创建出来的几何体,原点在底面;BoxGeometry的原点在中心。所以设置位置时,BoxGeometry需要额外加上height / 2的y偏移,否则会出现一半埋在地下、一半露在外面的情况。
排查技巧很简单:把地面变成半透明的,观察建筑底部是否贴合地面。同时把地面稍微下沉0.1到0.5个单位,可以减少深度冲突导致的闪烁问题。
7.2 Raycaster点击不准确
大型项目中,Raycaster拾取不准确的原因往往是射线穿过了多个建筑,而你取到的交点不是用户“想点”的那一个。多次点击后我发现,在建筑密集区域,鼠标稍微偏一点就会命中旁边的建筑。
解决方案:首先,提高Raycaster的精度,给它设置一个阈值范围,只检测用户点击位置附近的目标;其次,在拾取后做一个容错判断——如果命中的建筑是“背景级别”的小建筑,而射线也穿过了近处的“高层建筑”,则优先选择更靠近相机的那一个。这个逻辑简化为:在intersects数组中取距离相机最近的,而不是数组中的第一个。
7.3 透明材质排序混乱
使用半透明建筑和大面积透明地面时,会出现“透过玻璃看建筑一片黑”的问题。根本原因是three.js的透明度排序:WebGL默认不保证透明物体的渲染顺序,后面的半透明物体可能会被前面的半透明物体遮挡。
解决方案有三个:
- 给透明材质设置depthWrite: false,不让透明物体写入深度缓冲,避免遮挡其他半透明物体
- 按距离排序透明物体的渲染顺序,这个可以用renderer.sortObjects属性控制
- 将透明物体的渲染顺序整体挪到不透明物体之后
实际项目中我采用深度缓冲关闭加手动排序的方案,大屏下半透明建筑和飞线的显示效果正常了,不再有“穿帮”画面。
7.4 常见问题速查
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 建筑全部沉入地下或浮空 | 几何体原点不同 | 统一校正y偏移 |
| 点击建筑无响应 | Raycaster未排除地面 | 检测前过滤非建筑Mesh |
| 大屏画面闪烁 | 深度冲突 | 地面下沉0.1-0.5个单位 |
| 帧率骤降 | Draw Call过多 | 合并几何体、使用InstancedMesh |
| 4K大屏卡顿 | 像素比未限制 | 设置pixelRatio上限为2 |
| 透明材质发黑 | depthWrite冲突 | 关闭depthWrite |
| 标注文字跟随抖动 | CSS2DObject位置计算开销大 | 降低标注刷新频率,或改用Sprite |
8. 真实项目部署后的优化复盘
8.1 从开发机到大屏的环境差异
开发机通常是高清显示器加独立显卡,而实际部署的大屏环境五花八门——有的用普通办公电脑接4K电视,有的用迷你主机接拼接屏,显卡性能和开发机差了一大截。第二期交付时,我在开发机上跑60fps丝滑流畅,部署到目标环境后直接掉到25fps。排查后发现,目标机器的Chrome版本过旧,不支持WebGL2的部分特性,导致three.js自动回退到WebGL1,性能惨不忍睹。
解决方案是统一升级浏览器版本,同时代码里做了WebGL能力检测,不兼容时给出降级提示,而不是让用户体验卡成动画片的页面。这个坑提醒我:开发过程中要尽早用目标环境测试,不要等到上线前才换设备。
8.2 数据更新与动态刷新
大屏项目几乎都会遇到一个需求:视觉做成了一版,但业务数据每天都会更新。这时候如果数据每变一次就要重新加载整个页面,体验很差。我通过构建一个DataManager模块来解决:它负责统一管理外部数据(JSON、API接口、WebSocket),当数据变更时,只更新对应building的userData和标注文字,几何体不变,这样避免了大量不必要的重建开销。
8.3 防止内存泄漏的几个细节
大屏项目通常长期运行,内存泄漏会随时间积累导致卡顿甚至崩溃。代码review时我重点检查了几个点:
- 动画循环里每次创建的临时对象要尽量复用,new Vector3、new Color这类操作尽量抽到循环外部
- 移除场景对象时,要同步dispose几何体和材质,否则GPU显存不会释放
- 监听window resize时,每次resize要更新camera的aspect和renderer的size,否则画面会拉伸变形
- 销毁场景时,调用renderer.dispose()和renderer.forceContextLoss(),强制释放WebGL上下文
这些看起来是小问题,在长期运行的大屏项目里都是大隐患。我见过一个大屏因为更新数据时反复创建材质导致显存占满,跑了两天后浏览器直接崩溃。排查时发现代码里new Material的地方比预想多了好几倍。
9. 下一步计划与个人经验总结
9.1 第三期的规划
第二期算是把3D地图可视化的核心功能打通了,接下来第三期准备做几件事:一是接入实时数据流,用WebSocket推送业务数据,实现建筑颜色的实时动态变化和大屏数字的跳动;二是增加时间轴播放功能,实现历史数据的回放;三是将整个项目从配置驱动进一步升级为“编辑器配置 + 运行时渲染”的架构,让非开发人员也能通过可视化操作调整大屏样式。
9.2 这期项目做完的感受
回头看第二期的开发过程,最大的收获不是某个API用熟了,而是形成了一个相对稳定的开发路径:拿到需求后先拆解功能模块、确定数据结构,再动手写代码。而不是直接打开编辑器一通写,写完再改。
具体到three.js这个技术栈,我个人的体会是:它的API相对底层,灵活度极高,适合做定制化效果,但也正因如此,对开发者的空间想象力和图形学基础有要求。如果不理解坐标系转换、不理解深度缓冲、不理解draw call,很多问题解决起来就只能靠试。而反过来,如果理解了这些基础原理,three.js就是做3D地图可视化最趁手的工具之一,比任何封装好的高级引擎都更自由。
最后再分享一个小技巧:调试3D场景时,把相机初始位置保存下来,然后写一个快捷键恢复视角。开发过程中视角经常被拖到奇怪的位置,一键复位能省下很多调整的时间。这个习惯我从第一期用到第二期,估计第三期也会继续用下去。