1. 从一句“vibe coding”说起我为什么用三天撸了个3D母校开学季那阵子朋友圈里全是新生拖着箱子进校门的照片我翻着翻着突然冒出一个念头能不能用代码把母校最标志性的那栋主楼和校门做成一个能转、能走、能看的3D网页不是那种贴几张照片的H5而是真正用几何体堆出来的、能在浏览器里实时渲染的3D场景。刚好那段时间“vibe coding”这个词特别火意思大概就是跟着感觉走、边写边调、不追求一开始就架构完美先把东西跑起来再说。我决定就用这种状态来干这件事——不画原型图、不写详细设计文档打开编辑器就开干。这个项目的核心其实就三件事用Three.js在网页里搭出3D场景用WebGL做底层渲染用Node.js起一个本地服务方便调试和后续部署。听起来简单但真做起来从建模思路到贴图加载从光照调到性能优化坑一个接一个。我前后大概花了三个晚上加一个周末最终做出来一个能在浏览器里自由旋转、缩放、带昼夜切换的3D母校场景主楼、校门、操场、旗杆都在虽然精度比不上专业建模软件出的效果但胜在纯代码生成、体积极小、加载飞快。这篇文章适合谁看如果你有一点前端基础听说过Three.js但没真正上手做过完整项目或者你想用代码做点有纪念意义的东西那这篇应该能帮到你。如果你完全没接触过3D编程也没关系我会把每个关键步骤拆开讲包括我踩过的坑和最后怎么绕过去的。整个项目不依赖任何付费工具全部用开源库和免费资源完成你跟着做一遍大概率能做出一个属于自己的版本。2. 整体思路与技术选型为什么是Three.js而不是Blender导出2.1 核心需求拆解我要的到底是什么先想清楚需求再选工具这是我踩过很多次坑之后养成的习惯。这个项目的核心需求可以拆成四条第一场景要能在浏览器里直接打开不需要装任何插件第二模型要能自由旋转和缩放最好还能用键盘或鼠标控制视角移动第三要有基本的光照和阴影不然看起来就是一堆灰色方块第四整个项目的体积要小加载要快不能打开等半天。基于这四条我排除了几个方案。用Blender建模然后导出glTF文件当然可以效果也好但问题是我不太会用Blender而且导出文件动辄几MB甚至几十MB加载速度不理想。用Unity做WebGL导出也行但打包体积更大而且对我来说太重了。最后选Three.js原因很简单它就是一个JavaScript库引入一个几百KB的文件就能用几何体可以用代码直接生成不需要外部模型文件贴图也可以用Canvas动态画出来整个项目可以做到几乎零外部资源依赖。2.2 为什么用代码生成几何体而不是导入模型这里要展开说一下。很多人做3D网页第一反应是找个模型网站下载一个glTF或OBJ文件然后加载进去。这个思路没问题但有几个现实问题第一免费模型的质量参差不齐找到符合母校建筑风格的很难第二模型文件通常包含大量细节面数很高在浏览器里渲染压力大第三加载外部模型需要处理路径、跨域、格式兼容等一系列问题对新手不友好。用代码生成几何体的好处是完全可控想要什么形状就写什么代码面数可以精确控制不需要加载任何外部文件。坏处是复杂形状很难做比如雕塑、树木、人物这些。但母校的主楼和校门本质上就是长方体、圆柱体、平面这些基本几何体的组合用BoxGeometry、CylinderGeometry、PlaneGeometry完全够用。我实际做下来主楼用了大约十几个BoxGeometry拼出主体和窗户校门用了两个圆柱加一个横梁操场就是一个大平面加几条线整体面数控制在几千个三角形以内渲染毫无压力。2.3 Node.js在项目里的角色不只是起个服务很多人觉得Node.js在这个项目里就是起一个本地服务器用npx serve或者http-server就行了。但我实际用下来Node.js还能做更多事。第一我用它写了一个简单的构建脚本把分散的JS模块合并成一个文件减少HTTP请求第二我用它做贴图的预处理把一些需要动态生成的纹理提前用Node.js的Canvas库画好存成图片第三部署的时候用Node.js写了一个极简的静态文件服务方便在本地网络里用手机也能打开看效果。安装Node.js这块我踩过一个坑。当时看到网上有人说用最新版我就去官网下了最新的24.x版本结果跑某个依赖的时候报错说版本不兼容。后来换成LTS版本当时是20.x就一切正常。所以如果你也要做类似的项目建议直接上LTS版本别追最新。Ubuntu下安装可以用NodeSource的脚本也可以直接用nvm管理多版本我个人推荐nvm切换版本方便不会污染系统环境。3. 核心细节解析从零搭建一个能跑的3D场景3.1 Three.js的引入方式与版本选择Three.js的引入方式有好几种可以直接下载three.min.js用script标签引入也可以用npm安装然后打包还可以用ES模块的方式从CDN引入。我一开始用的是CDN引入简单直接但后来发现CDN在某些网络环境下不稳定加载慢甚至加载失败。后来改成把three.module.js下载到本地用importmap的方式引入这样既不需要打包工具又不受网络影响。版本选择上Three.js的更新频率很高新版本会废弃一些旧API。我建议选一个近半年内的稳定版本不要用太老的教程里提到的r70、r80这种那些版本的API和现在差别很大。我用的版本是r160左右核心API比如Scene、PerspectiveCamera、WebGLRenderer、BoxGeometry、MeshStandardMaterial这些都很稳定网上大部分教程也适用。3.2 场景、相机、渲染器3D世界的三件套任何Three.js项目都从这三样东西开始。Scene是场景容器所有物体都要加进去PerspectiveCamera是透视相机模拟人眼看到的近大远小效果WebGLRenderer是渲染器负责把场景画到Canvas上。这三者的初始化代码很固定但有几个参数需要根据实际情况调整。相机的fov视场角我设的是60度这个值太小会显得视野狭窄太大会有畸变。near和far分别是0.1和1000决定了相机能看到的最近和最远距离。相机的位置我一开始放在(0, 5, 20)后来发现主楼比较高需要把相机抬高到(0, 10, 30)才能看到全貌。渲染器的setSize要设置为窗口大小并且监听resize事件动态调整不然窗口变化时画面会拉伸变形。这里有个细节renderer.setPixelRatio(window.devicePixelRatio)这行代码很重要。如果不设置在高分屏上画面会模糊如果设置成2以上性能又会下降。我实测下来设置为Math.min(window.devicePixelRatio, 2)是比较平衡的选择。3.3 用基本几何体拼出主楼和校门主楼的结构其实不复杂一个大的长方体作为主体前面加几个小长方体作为突出部分顶部加一个扁长方体作为屋檐窗户用深色的小长方体贴在墙面上。关键是比例要协调我一开始把主楼做得太宽太矮看起来像个仓库后来调整了长宽高比例参考了母校主楼的实际照片才看起来像那么回事。校门更简单两根圆柱作为门柱一根横梁连接横梁上可以用Canvas画上校名文字然后作为贴图贴上去。这里有个技巧用Canvas画文字然后转成纹理比直接加载字体文件再渲染要简单得多而且效果完全够用。操场就是一个大平面用PlaneGeometry加一个绿色的MeshStandardMaterial再画几条白线作为跑道。注意用代码生成几何体时坐标系是Y轴向上X轴向右Z轴朝向屏幕外。放置物体时一定要想清楚每个物体的位置不然很容易出现物体重叠或者悬空的情况。3.4 光照与阴影让场景从“纸片”变“立体”没有光照的3D场景就是一堆纯色色块完全没有立体感。Three.js支持多种光源AmbientLight环境光提供基础亮度DirectionalLight平行光模拟太阳光PointLight点光源模拟灯泡。我用了环境光加平行光的组合环境光强度设0.4平行光强度设1.2平行光的位置放在(50, 100, 50)模拟太阳从斜上方照射。阴影是提升真实感的关键。开启阴影需要三步渲染器的shadowMap.enabled设为true平行光的castShadow设为true物体的castShadow和receiveShadow分别设为true。但阴影很吃性能尤其是阴影贴图的分辨率。我一开始设了4096结果帧率掉到20以下后来降到2048就流畅了。另外平行光的阴影相机范围要设置好shadow.camera.left/right/top/bottom要覆盖整个场景不然阴影会被裁切。4. 实操过程从零到能跑起来的完整步骤4.1 环境准备Node.js安装与项目初始化第一步是装Node.js。Windows和macOS直接去官网下载LTS版本的安装包一路下一步就行。Ubuntu下我推荐用nvm安装命令是curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash然后nvm install 20再nvm use 20。装完之后用node -v和npm -v验证一下能输出版本号就说明成功了。项目初始化很简单新建一个文件夹在里面执行npm init -y生成package.json然后npm install three安装Three.js。如果你不想用npm也可以直接下载three.module.js文件放到项目里。我两种方式都试过npm的好处是版本管理方便坏处是node_modules文件夹很大直接下载文件的好处是干净坏处是更新麻烦。对于这种小项目我建议直接下载文件整个项目文件夹不超过2MB。4.2 搭建HTML骨架与引入Three.jsHTML文件的结构很简洁一个canvas元素作为渲染目标一个script标签引入Three.js一个script标签写自己的代码。如果用importmap的方式需要在script标签里声明模块路径。我实际用的结构是这样的先引入three.module.js然后在自己的代码里用import * as THREE from three。这样写的好处是代码组织清晰坏处是需要浏览器支持importmap不过现在主流浏览器都支持了。CSS部分只需要把body的margin设为0canvas设为block防止出现滚动条。另外我加了一个loading提示因为场景初始化需要一点时间虽然很短但用户体验上有个过渡会更好。4.3 编写场景初始化代码场景初始化的代码我分成了几个函数initScene()负责创建场景、相机、渲染器createBuilding()负责生成主楼createGate()负责生成校门createGround()负责生成地面createLights()负责光照animate()负责渲染循环。这样拆分的好处是每个函数职责单一调试的时候容易定位问题。渲染循环用requestAnimationFrame实现每一帧调用renderer.render(scene, camera)。如果场景是静态的其实不需要每帧都渲染可以用renderer.render只在需要的时候调用节省性能。但我加了自动旋转功能所以还是用了循环。自动旋转的实现很简单在animate函数里让相机的角度每帧增加一点点然后用camera.position.x Math.sin(angle) * radius和camera.position.z Math.cos(angle) * radius更新位置最后camera.lookAt(0, 5, 0)让相机始终看向场景中心。4.4 贴图与材质让建筑看起来不那么“塑料”纯色材质看起来很像塑料玩具加上贴图会真实很多。但贴图加载有个常见问题贴图开始不显示过一会儿才出现或者干脆不显示。这个问题的原因通常是贴图是异步加载的而渲染循环在贴图加载完成之前就已经开始了。解决办法有两种一种是用TextureLoader的onLoad回调在贴图加载完成后再开始渲染另一种是先用纯色材质等贴图加载完成后再替换材质。我用的第二种体验更平滑。贴图的来源我用了两种一种是程序生成的比如用Canvas画砖墙纹理、画窗户另一种是从免费纹理网站下载的比如草地、天空。程序生成的好处是体积小、可控坏处是细节不够丰富。我实际做下来主楼的墙面用程序生成的砖墙纹理就够了草地用下载的纹理效果更好。提示Three.js的纹理默认是翻转的如果贴图显示上下颠倒设置texture.flipY false即可。另外纹理的wrapS和wrapT要设为THREE.RepeatWrapping这样贴图才能平铺。4.5 交互控制让用户能自由探索Three.js自带的OrbitControls是实现旋转、缩放、平移的标准方案。引入方式是从three/addons/controls/OrbitControls.js导入然后new OrbitControls(camera, renderer.domElement)。几个关键参数enableDamping设为true可以让旋转有惯性手感更好dampingFactor设0.05左右比较合适minDistance和maxDistance限制缩放范围防止用户把相机拉到地底下或者拉到无穷远maxPolarAngle限制垂直旋转角度防止看到场景底部的空白。除了OrbitControls我还加了一个键盘控制按WASD可以平移相机按Q和E可以升降。实现方式是监听keydown和keyup事件记录按键状态在animate函数里根据按键状态更新相机位置。这个功能不是必须的但加上之后探索感更强。5. 常见问题与排查技巧实录5.1 贴图不显示或显示异常这是最常见的问题我至少遇到了三次。第一次是贴图路径写错了浏览器控制台会报404这个好解决检查路径就行。第二次是贴图加载了但显示为纯黑色原因是光照不足材质接收不到光解决方法是增加环境光强度或者把材质的emissive设一个基础色。第三次是贴图显示但颜色不对原因是色彩空间设置问题Three.js的渲染器默认输出sRGB但纹理默认是线性空间需要设置texture.colorSpace THREE.SRGBColorSpace。还有一个隐蔽的问题如果贴图的尺寸不是2的幂次方比如300x300在某些设备上会无法平铺。解决办法是把贴图尺寸调整为256x256或512x512这种2的幂次方尺寸。我一开始用Canvas画了一个300x300的砖墙纹理结果在部分手机上显示异常后来改成512x512就正常了。5.2 性能问题帧率低、卡顿性能问题通常来自三个方面面数太多、阴影太吃性能、渲染次数太多。面数方面我一开始用了很多细分段数很高的几何体比如SphereGeometry(1, 64, 64)一个球就几千个三角形。后来把不需要的细分降到最低比如SphereGeometry(1, 16, 16)视觉上几乎没差别但面数少了十几倍。阴影方面前面提到过阴影贴图分辨率从4096降到2048帧率立刻从20多升到50多。另外不是所有物体都需要投射阴影比如地面只需要接收阴影不需要投射阴影把castShadow设为false可以省不少性能。渲染次数方面如果场景是静态的可以只在相机移动时渲染而不是每帧都渲染。实现方式是监听OrbitControls的change事件在事件回调里调用renderer.render然后把animate循环里的渲染去掉。这样在用户不操作的时候GPU几乎不工作省电又流畅。5.3 移动端适配问题移动端的坑也不少。第一触摸事件和鼠标事件不一样OrbitControls默认支持触摸但需要确保canvas的touch-action设为none否则页面会跟着手指滚动。第二移动端的性能比桌面端差很多阴影和抗锯齿要适当降低。第三移动端的屏幕比例和桌面端不同相机的aspect要动态计算不能写死。我实测下来在手机上把阴影关掉、抗锯齿关掉、像素比降到1.5基本能跑到30帧以上体验可以接受。如果追求更好的效果可以考虑用LODLevel of Detail技术根据距离远近切换不同精度的模型但这个实现起来比较复杂我这个项目没用。5.4 常见问题速查表问题现象可能原因解决方法页面全黑相机位置不对或光照不足检查相机位置增加环境光贴图不显示路径错误或加载未完成检查控制台404用onLoad回调贴图显示为黑色光照不足或色彩空间错误增加光照设置colorSpace帧率低面数太多或阴影分辨率太高降低细分阴影降到2048移动端卡顿性能不足或像素比太高关阴影像素比降到1.5窗口缩放变形未监听resize事件添加resize监听更新aspect物体重叠或悬空坐标计算错误用辅助线调试确认Y轴向上6. 一些让项目更“母校”的细节处理6.1 用Canvas动态生成校名和标语校名和标语如果直接用字体文件渲染需要加载字体而且中文字体文件很大。用Canvas画文字然后转成纹理是最简单的方案。具体做法是创建一个canvas元素用ctx.fillText画上文字然后new THREE.CanvasTexture(canvas)生成纹理贴到横梁或墙面上。字体可以用系统自带的黑体或宋体不需要额外加载。文字的颜色、大小、位置都可以在Canvas里精确控制比在3D里调整方便得多。我实际做的时候校名用了红色宋体标语用了白色黑体背景透明贴到横梁上效果很好。需要注意的是Canvas的尺寸要设得足够大比如1024x256这样文字才清晰。另外纹理的minFilter要设为THREE.LinearFilter避免远处文字模糊。6.2 添加昼夜切换和天气效果昼夜切换的实现思路是改变光照的颜色和强度以及场景的背景色。白天用白色的平行光和浅蓝色的背景傍晚用橙色的平行光和橙红色的背景夜晚用深蓝色的环境光和深蓝色的背景。切换的时候用TWEEN或者简单的线性插值做过渡避免突变。我用了最简单的lerp函数在几秒钟内平滑过渡效果还不错。天气效果我只做了简单的雨和雪用Points粒子系统实现。雨是细长的线段雪是白色的小点从天空往下落落到地面以下就重置到顶部。这个效果对性能有一点影响但不大几千个粒子在现代设备上完全没问题。6.3 用Node.js做简单的构建和部署项目做完之后我想把它部署到一个可以公开访问的地方。最简方案是用Node.js写一个静态文件服务几行代码就行。用http模块创建服务器读取文件返回设置正确的MIME类型。然后把这个服务跑在一台常开的机器上或者用一些免费的静态托管服务。我实际用的是Node.js的serve包一行命令就能起服务方便快捷。构建方面我把所有的JS代码合并成一个文件用esbuild做压缩体积从几百KB降到一百多KB。CSS也做了压缩。图片资源用imagemin压缩。整个项目最终打包出来不到500KB加载速度很快。7. 我踩过的那些坑和最后怎么绕过去的第一个大坑是版本兼容性。我一开始用最新版的Three.js结果OrbitControls的引入路径变了从three/examples/js/controls/OrbitControls.js变成了three/addons/controls/OrbitControls.js我照着旧教程写的一直报错找了半天才发现是路径问题。所以看教程一定要看近期的Three.js的API变化挺频繁的。第二个坑是阴影的锯齿。我开了阴影之后发现边缘全是锯齿很难看。后来查资料才知道要设置shadow.mapSize和shadow.radius还要调整shadow.bias防止阴影痤疮。调了半天才找到一个看起来还行的参数组合。这个没有固定值得根据场景大小和光照角度慢慢试。第三个坑是移动端的触摸事件冲突。在手机上手指滑动旋转场景的时候页面也会跟着滚动体验很差。后来在CSS里给canvas加了touch-action: none才解决。这个属性在桌面端没影响但在移动端是必须的。第四个坑是贴图的跨域问题。我从某个免费纹理网站下载的图片直接引用URL的时候浏览器报跨域错误。后来把图片下载到本地用相对路径引用就没事了。所以如果你也要用外部纹理建议下载到本地别直接引用外链。8. 这个项目还能怎么扩展如果你已经跟着做到了这里恭喜你一个基本的3D母校已经跑起来了。接下来可以玩的东西还很多。比如加一个第一人称漫游模式用PointerLockControls实现鼠标锁定视角WASD移动就像在玩3D游戏一样逛校园。比如加一个热点标注功能点击建筑弹出介绍文字用Raycaster检测鼠标点击的物体。比如加一个时间轴拖动滑块可以看到母校不同年代的样子这个需要准备多套模型或者用变形动画实现。还有一个方向是用真实数据生成地形。如果你能拿到母校的平面图或者高程数据可以用代码生成更精确的地形和建筑布局。这个难度比较大但做出来效果会很震撼。我目前只做了主楼和校门下一步打算把图书馆和操场也加上慢慢完善。最后分享一个小技巧做3D项目的时候善用AxesHelper和GridHelper。前者显示坐标轴后者显示网格地面调试物体位置的时候非常有用。等场景调好了再把它们隐藏掉。我一开始没加这两个辅助工具调位置全靠猜效率很低。后来加上之后位置调整快了很多。