简介这份源码实例面向希望入门或进阶 WebGL 与 Three.js 的开发者聚焦智慧城市这一典型 3D 可视化场景帮助读者理解如何把浏览器端图形技术落地到真实城市应用中。压缩包共 60 个文件约 3.06MB以 23 个 js 脚本、10 个 css 样式、1 个 html 入口为主另含 16 张 png 与 8 张 jpg 贴图、1 个 wasm 解码模块及说明文档覆盖场景搭建、样式布局与资源加载等环节。项目涉及地图与地理数据处理、建筑与道路等 3D 模型构建、光照材质、动画交互、实时数据可视化以及 LOD、批处理等性能优化思路还包含响应式布局与 UI 交互设计目录中另有中文文档辅助理解代码结构。目前已有 3927 人学习下载适合作为智慧城市可视化项目的参考范例帮助读者对照源码梳理实现路径、积累可复用的开发经验。1. 拿到一份 WebGLThree.js 智慧城市源码先别急着 npm run前端圈里做可视化大屏的多多少少都碰过智慧城市这类需求领导要一个能转、能点、能看数据的 3D 城市最好还带点科技感光效。真从零写光是场景搭建、模型加载、坐标对齐就能耗掉一两周。所以当我拿到这份webgl-threejs智慧城市应用项目源码实例.zip时第一反应不是它多惊艳而是——它能不能直接跑起来、里面的坑我能不能提前避开。这份资源是一套已经构建过的 Three.js 智慧城市前端工程从目录结构看它把css、js、images、texture分得很清楚还带了draco_decoder_gltf.wasm这类模型解压组件说明场景里大概率有压缩过的 glTF 建筑模型。它适合两类人一类是想快速搭出可视化大屏原型的前端另一类是想研究 Three.js 在真实城市级场景里怎么组织代码、怎么优化性能的开发者。关键词里提到的源码软件、智慧城市、人工智能落到这份资源上最实在的价值是「可参考的工程结构 可复用的渲染套路」而不是一个开箱即用的产品。2. 拆开目录看门道构建产物里藏着哪些技术选型2.1 从文件名反推技术栈先看根目录和zhcs子目录两边结构几乎对称都有css、js、images、index.html。这种「双份」布局常见于两种场景一是主站和子模块各自独立打包二是作者留了一份未压缩的源码目录。从chunk-vendors、app.xxxx.js这种带哈希的命名来看这是典型的 Vue CLI 或 Vite 构建产物chunk-vendors里装的是 Three.js、Tween、Stats 这些第三方库。js目录里几个文件值得单独拎出来说文件名作用说明stats.min.js性能监视器实时显示 FPS、内存调优必备tween.min.js补间动画库相机飞行、物体位移常用它做缓动html2Canvas.min.js截图导出大屏常有「导出当前视角」需求draco_decoder_gltf.wasmDraco 解码器加载压缩 glTF 模型必须配套geoutil.js地理工具大概率封装了经纬度转场景坐标texture目录里的waternormals.jpg是水面法线贴图building.png、road.png是建筑和道路的纹理sky_ny.jpg到sky_pz.jpg六张图是标准立方体贴图用来做天空盒。images里的sy.png、wifi.png、hj.png、sh.png、jy.png、yl.png从命名看是图标类资源可能对应「水源」「WiFi」「环境」「生活」「教育」「医疗」这些城市数据图层。2.2 为什么它用 Draco 而不是普通 glTF城市级场景里建筑模型动辄几百个如果每个模型都是未压缩的 glTF首屏加载能拖到十几秒。Draco 是 Google 出的几何压缩方案能把顶点数据压到原来的十分之一左右。代价是浏览器端要多加载一个 wasm 解码器也就是目录里那个draco_decoder_gltf.wasm。常见做法是建模阶段用 Blender 导出 glTF 时勾选 Draco 压缩前端用DRACOLoader挂到GLTFLoader上。这份源码把 wasm 文件直接放在js目录说明它没有走 CDN而是本地加载。这样做的好处是离线可用坏处是部署时路径必须配对否则控制台会报THREE.DRACOLoader: Unable to load Draco decoder。2.3 天空盒与场景氛围的配合六张sky_*.jpg是经典立方体贴图命名法px正 X、nx负 X、py正 Y、ny负 Y、pz正 Z、nz负 Z。Three.js 里用CubeTextureLoader一次性加载六张图赋给scene.background就能得到全景天空。很多新手会直接把天空盒贴图当环境贴图用结果金属材质反射出的是天空而不是周围建筑看起来特别假。正确做法是天空盒归天空盒环境贴图另外用PMREMGenerator从场景或 HDR 生成。3. 本地跑起来静态服务、路径与模型加载3.1 起一个静态服务器别双击 index.html构建产物里的 JS 用的是绝对路径或相对根路径直接双击index.html打开浏览器会因file://协议限制无法加载 wasm 和部分贴图。最省事的办法是在项目根目录起一个静态服务# 进入解压后的项目根目录 cd webgl-threejs-smart-city # 用 Python 自带的 http.server端口随意 python -m http.server 8080 # 或者用 Node 的 serve需先 npm i -g serve serve -l 8080启动后访问http://localhost:8080如果页面白屏先按 F12 看 Console 和 Network。常见报错是Failed to load resource: 404多半是js或texture路径不对。这份源码的index.html里引用资源的方式决定了你必须从根目录起服务不能进到zhcs子目录再起。3.2 模型加载与 Draco 解码器的挂载虽然构建产物里代码已压缩但如果你拿到的是可读版本或者想自己改模型加载逻辑核心代码大概长这样import * as THREE from three; import { GLTFLoader } from three/examples/jsm/loaders/GLTFLoader; import { DRACOLoader } from three/examples/jsm/loaders/DRACOLoader; // 1. 创建 Draco 解码器路径指向 wasm 所在目录 const dracoLoader new DRACOLoader(); dracoLoader.setDecoderPath(./js/); // 注意结尾斜杠 dracoLoader.setDecoderConfig({ type: wasm }); // 2. 挂到 GLTFLoader 上 const gltfLoader new GLTFLoader(); gltfLoader.setDRACOLoader(dracoLoader); // 3. 加载建筑模型 gltfLoader.load(./models/city.glb, (gltf) { const model gltf.scene; model.scale.set(1, 1, 1); model.position.set(0, 0, 0); scene.add(model); }, (progress) { console.log(加载进度${(progress.loaded / progress.total * 100).toFixed(1)}%); }, (error) { console.error(模型加载失败, error); });setDecoderPath必须指向draco_decoder_gltf.wasm和draco_wasm_wrapper_gltf.js所在的目录结尾斜杠不能少。setDecoderConfig({ type: wasm })是告诉解码器优先用 wasm 而不是 JS 回退方案性能差好几倍。加载回调里的progress参数可以用来做进度条城市模型大的时候没有进度条用户会以为卡死。3.3 地理坐标转场景坐标的常见做法geoutil.js这个文件名暗示它处理了经纬度到 Three.js 场景坐标的映射。智慧城市项目里建筑位置通常来自 GIS 数据经纬度是 WGS84 或 GCJ02直接扔进场景会得到一堆小数点后好几位的坐标相机根本找不到。常见做法是选一个中心点作为原点把经纬度差值按米换算// 简化版以中心点经纬度为原点粗略换算 const centerLng 116.397; const centerLat 39.908; const meterPerDegLng 111320 * Math.cos(centerLat * Math.PI / 180); const meterPerDegLat 110540; function lngLatToScene(lng, lat) { const x (lng - centerLng) * meterPerDegLng; const z -(lat - centerLat) * meterPerDegLat; // 注意 Z 轴方向 return new THREE.Vector3(x, 0, z); }这段代码里meterPerDegLat取 110540 米是近似值精度要求高的话得用 Vincenty 公式。Z 轴取负是因为 Three.js 默认相机朝 -Z 看而地图上纬度向北增大不取负会导致南北颠倒。这个坑我见过不止一个项目踩表现是「建筑位置整体镜像了」。4. 性能与交互城市级场景不卡顿的几个关键设置4.1 渲染器参数与像素比控制Three.js 的WebGLRenderer默认按设备像素比渲染在 4K 屏或 Retina 屏上像素比可能是 2 甚至 3渲染压力翻四倍。城市场景面数本来就高不限制像素比很容易掉到 20 FPS 以下const renderer new THREE.WebGLRenderer({ antialias: true, alpha: false, powerPreference: high-performance }); // 关键限制像素比上限通常 1.5 足够 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); renderer.setSize(window.innerWidth, window.innerHeight); // 开启阴影会显著增加开销按需开启 renderer.shadowMap.enabled true; renderer.shadowMap.type THREE.PCFSoftShadowMap; // 输出编码避免颜色发灰 renderer.outputEncoding THREE.sRGBEncoding; renderer.toneMapping THREE.ACESFilmicToneMapping; renderer.toneMappingExposure 1.2;powerPreference: high-performance会提示浏览器优先用独显。outputEncoding和toneMapping这两个设置很多教程不提但不设的话场景颜色会偏暗偏灰尤其是用了 PBR 材质的建筑看起来像蒙了一层灰。4.2 光照方案环境光 平行光 点光源的配比城市白模场景常用「环境光打底 平行光做主光 点光源点缀」的组合。环境光太强会失去立体感太弱则暗部死黑。我一般这样配// 环境光提供基础亮度强度 0.4~0.6 const ambient new THREE.AmbientLight(0xffffff, 0.5); scene.add(ambient); // 平行光模拟太阳产生明暗对比 const directional new THREE.DirectionalLight(0xffffff, 1.2); directional.position.set(100, 200, 100); directional.castShadow true; directional.shadow.mapSize.set(2048, 2048); directional.shadow.camera.near 1; directional.shadow.camera.far 500; directional.shadow.camera.left -200; directional.shadow.camera.right 200; directional.shadow.camera.top 200; directional.shadow.camera.bottom -200; scene.add(directional); // 点光源给重点建筑或数据点加高光 const point new THREE.PointLight(0x00aaff, 1, 100); point.position.set(0, 50, 0); scene.add(point);阴影相机的范围要刚好包住场景范围太大会导致阴影模糊太小则边缘建筑没影子。mapSize设 2048 是性能和质量的平衡点4096 在移动端会明显卡顿。4.3 交互拾取与射线检测的优化智慧城市大屏少不了点击建筑弹信息框。Three.js 用Raycaster做拾取但场景里物体多了之后每次点击遍历所有物体很慢。常见优化是给可交互物体单独放一个数组只对这个数组做射线检测const raycaster new THREE.Raycaster(); const mouse new THREE.Vector2(); const interactiveObjects []; // 只放需要点击的建筑 window.addEventListener(click, (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(interactiveObjects, true); if (intersects.length 0) { const hit intersects[0].object; // 向上找到带业务数据的父级 let target hit; while (target !target.userData.buildingInfo) { target target.parent; } if (target) { showInfoPanel(target.userData.buildingInfo); } } });intersectObjects第二个参数true表示递归检测子物体因为建筑模型通常是 Group 套 Mesh。userData是 Three.js 给开发者留的自定义数据挂载点把业务信息挂在建筑根节点上点击后向上查找即可。5. 避坑排查这份源码跑不起来时先看这几条5.1 白屏但控制台无报错现象页面打开一片白Console 干净Network 里资源都是 200。 原因多半是相机位置或朝向不对或者场景背景色和物体颜色一样导致「看起来是白的」。也有可能是camera.near设得太大近处物体被裁掉了。 解决先把scene.background设成红色确认渲染循环在跑再把相机位置调到(0, 100, 200)看向原点逐步排查。5.2 Draco 解码失败导致模型不显示现象控制台报THREE.DRACOLoader: Unable to load Draco decoder或Uncaught RuntimeError: abort。 原因setDecoderPath路径不对或者 wasm 文件没有以正确的 MIME 类型返回。有些静态服务器不认.wasm后缀返回application/octet-stream浏览器拒绝实例化。 解决确认路径指向js目录且结尾有斜杠在服务器配置里给.wasm加上application/wasm。用python -m http.server一般没问题用 Nginx 要手动加。5.3 贴图加载了但模型是纯黑现象Network 里贴图 200但建筑表面全黑。 原因贴图色彩空间没设对。Three.js r152 之后色彩管理默认开启贴图需要设texture.colorSpace THREE.SRGBColorSpace否则会被当成线性空间处理颜色发暗甚至发黑。 解决遍历加载的贴图统一设置色彩空间或者检查渲染器的outputEncoding是否和贴图匹配。5.4 帧率随运行时间越来越低现象刚打开 60 FPS跑几分钟掉到 20 FPS。 原因动画循环里不断创建新对象如new THREE.Vector3()垃圾回收跟不上或者事件监听没解绑重复添加。 解决把循环内用到的向量、矩阵提到循环外复用requestAnimationFrame里避免console.log组件销毁时removeEventListener。5.5 移动端触摸事件与点击冲突现象桌面端点击正常移动端要么没反应要么拖动视角时误触发点击。 原因移动端click事件有 300ms 延迟且触摸滑动也会触发 click。 解决用pointerdownpointerup组合判断记录按下和抬起的位置差超过阈值视为拖动而非点击。6. 二次开发进阶把静态城市变成会呼吸的数据看板跑通之后这份源码真正的价值在于它的可扩展性。我一般会从三个方向动手一是把images里那些图标做成可点击的图层开关控制不同数据图层的显隐二是用tween.min.js做相机巡航让视角自动在城市上空缓慢移动大屏演示时特别出效果三是接 WebSocket 或轮询接口把实时数据映射到建筑颜色或高度上。图层开关的实现思路很简单给每类建筑或图标打上userData.layer wifi这样的标记UI 上勾选时遍历场景设置visiblefunction toggleLayer(layerName, visible) { scene.traverse((obj) { if (obj.userData.layer layerName) { obj.visible visible; } }); }相机巡航用 Tween 控制相机位置和lookAt目标点注意lookAt不能直接补间要补间一个中间向量再在每帧调用const cameraTarget new THREE.Vector3(0, 0, 0); const tween new TWEEN.Tween(camera.position) .to({ x: 200, y: 150, z: 200 }, 5000) .easing(TWEEN.Easing.Quadratic.InOut) .onUpdate(() { camera.lookAt(cameraTarget); }) .start(); // 动画循环里别忘了 function animate() { requestAnimationFrame(animate); TWEEN.update(); renderer.render(scene, camera); }数据映射到建筑高度时注意提前记录每个建筑的原始高度否则每次更新都在上一次基础上叠加建筑会越长越高。我习惯在加载模型时把原始scale.y存到userData.baseScaleY更新时用baseScaleY * (1 value / maxValue)计算。最后说个血泪经验这份源码的构建产物里app.xxxx.js和chunk-vendors是压缩过的想改业务逻辑最好找到对应的源码工程直接在压缩代码上改下次构建就覆盖了。如果只有构建产物那就把要改的逻辑单独写一个 JS 文件在index.html里引在最后通过全局变量或事件和原逻辑通信。从那以后我每次拿到构建产物都先确认有没有源码目录没有的话就做好「外挂补丁」的心理准备。希望这份拆解能帮你少走点弯路。本文还有配套的精品资源点击获取