1. 从一句"我想把母校装进浏览器"说起
每年开学季,社交平台上都会冒出一批"给母校做点东西"的投稿。有人剪视频,有人画插画,有人写长文。我今年选了一条相对硬核的路——用 vibe coding 的方式,把母校做成了一个能在浏览器里自由旋转、缩放、漫游的 3D 场景。整个项目从起念到跑通,前后大概花了两个周末,中间踩的坑比我预想的多得多,也正是这些坑让我觉得有必要把过程完整写下来。
先把话说清楚:这篇不是"Three.js 从入门到精通"的教程,也不是"三天学会 WebGL"的速成帖。它更像是一份项目复盘——我为什么选 vibe coding 这条路、3D 网页渲染到底难在哪、Three.js 的贴图为什么一开始死活不显示、模型面数怎么压、性能怎么救。如果你也动了"用代码给母校/家乡/某个有纪念意义的场景做个 3D 版本"的念头,或者你只是单纯好奇 vibe coding 这种"边聊边写"的开发方式在真实项目里到底靠不靠谱,那这篇应该能给你省下不少时间。
所谓 vibe coding,说白了就是不再一行行手敲代码,而是把意图用自然语言描述清楚,让 AI 帮你生成、补全、重构代码,你负责判断、调试和拍板。它特别适合我这种"有明确目标、但不想在样板代码上耗时间"的场景。3D 母校这个项目里,真正需要我动脑的是场景取舍、坐标对齐、性能优化这些"判断类"工作,而像相机控制、光照初始化、加载器封装这类"套路类"代码,交给 vibe coding 效率高得离谱。关键词里的 Three.js、WebGL、3D 网页渲染、skill,基本就是这条链路上的几个核心节点,下面我会一个个拆开讲。
2. 为什么是 Three.js,而不是别的 3D 方案
2.1 浏览器里做 3D,绕不开 WebGL 这层地基
想在网页里渲染 3D,底层能力来自 WebGL——它本质上是浏览器暴露给 JavaScript 的一套图形接口,让你能直接调用 GPU 画三角形。但 WebGL 原生 API 极其啰嗦,画一个立方体可能要写上百行,还要自己管着色器、缓冲区、矩阵运算。Three.js 的价值就在于它把这层复杂度包了起来:场景(Scene)、相机(Camera)、渲染器(Renderer)三件套一搭,一个 3D 世界就立起来了。
我选 Three.js 而不是 Babylon.js 或原生 WebGL,理由很实际。第一,生态成熟,遇到问题搜一下基本都有答案,这对独立开发者太重要了。第二,它的 API 设计对"描述式开发"友好——vibe coding 时我只要说"给我一个绕场景旋转的轨道控制器",AI 生成的 OrbitControls 代码几乎不用改就能用。第三,社区里的模型加载、后期处理、性能监控插件齐全,母校场景里要用的 GLTF 加载、雾效、性能面板,全都有现成方案。
2.2 母校场景的特殊性决定了技术选型
母校这种场景和游戏地图不一样。它不需要物理引擎,不需要角色动画,不需要联网对战。它要的是:建筑外形准确、材质有辨识度、能自由漫游、加载别太慢。这几点决定了我的技术栈是"轻量优先"——Three.js 核心 + GLTFLoader + OrbitControls,后期处理只加了最基础的抗锯齿,连阴影都做了取舍。
这里有个很多人会忽略的判断:不是所有 3D 场景都需要阴影和实时光照。母校建筑大多是静态的,我最后用的是烘焙好的贴图 + 环境光 + 一盏方向光,阴影直接用贴图里的暗部模拟。这样 GPU 压力小了一大截,在普通笔记本上也能跑满 60 帧。如果一上来就开实时阴影、开全局光照,模型稍微复杂点就卡成幻灯片,这是新手最容易踩的性能坑。
2.3 vibe coding 在这类项目里的真实定位
我得泼盆冷水:vibe coding 不是"你说一句,AI 给你一个成品"。它更像是"你当架构师,AI 当打字员"。项目里我真正让 AI 干的活,是这些:
- 生成 Three.js 的初始化样板(场景、相机、渲染器、响应式 resize)
- 封装 GLTF 模型加载函数,带加载进度回调
- 写轨道控制器和键盘漫游的输入处理
- 帮我查"贴图不显示"这类报错的常见原因
- 把重复的材质配置抽成函数
而像"哪栋楼该放哪个位置""模型面数压到多少合适""相机初始角度怎么调才好看"这些,AI 给不了准确答案,必须我自己对着参考图一点点调。vibe coding 提效的是"写",不是"想"。想清楚这一点,你就不会对它抱有不切实际的期待,也不会在它给不出满意结果时觉得被坑了。
3. 母校 3D 场景的搭建链路:从空场景到能漫游
3.1 场景骨架:三件套 + 坐标系的坑
Three.js 的起点永远是那三件套。我用 vibe coding 让 AI 生成的第一版骨架大概长这样:
import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; const scene = new THREE.Scene(); scene.background = new THREE.Color(0x87ceeb); scene.fog = new THREE.Fog(0x87ceeb, 50, 200); const camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 1000 ); camera.position.set(30, 20, 30); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); document.body.appendChild(renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement); controls.target.set(0, 5, 0); controls.update();这段代码本身没难度,但里面藏着两个新手必踩的坑。第一个是setPixelRatio——如果不限制上限,在 4K 屏或高 DPI 手机上,渲染分辨率会翻好几倍,GPU 直接跪。我一开始没加Math.min(..., 2),在同事的 MacBook 上跑得挺欢,一到自己 4K 显示器上就掉帧,排查了半天才发现是像素比的问题。
第二个坑是坐标系。Three.js 默认 Y 轴朝上,而很多建模软件(尤其是从 CAD 导出的)是 Z 轴朝上。我第一次导入母校主楼模型时,楼是"躺"在地上的,像被推倒了一样。解决办法要么在建模软件里导出前转好轴向,要么在代码里给模型加一个rotation.x = -Math.PI / 2。我建议在导出环节解决,代码里转容易和其他变换打架,后面调位置时会很痛苦。
3.2 模型从哪来:三条现实路径的取舍
母校的 3D 模型不会凭空出现,我实际考虑过三条路:
| 路径 | 优点 | 缺点 | 适用情况 |
|---|---|---|---|
| 手工建模(Blender/SketchUp) | 外形可控、面数可优化 | 耗时、需要建模基础 | 主体建筑、标志性雕塑 |
| 照片建模/三维重建 | 还原度高 | 需要多角度照片、后处理麻烦 | 复杂立面、雕塑细节 |
| 程序化生成 | 快、可批量 | 只能做规则几何体 | 道路、草坪、围墙、树 |
我最后是混合用的:主楼和图书馆用 Blender 手工建,草坪、道路、路灯用代码程序化生成,几棵树用了现成的低模资源。这里给个实在建议——别想着把母校每一栋楼都建出来。观众看的是整体氛围和几个标志性建筑,你把主楼、图书馆、校门做精致,剩下的用简化体块加贴图带过,效果反而更干净,加载也快。
程序化生成草坪和道路其实很有意思,用 PlaneGeometry 铺地面,再用 Canvas 画一张带纹理的贴图贴上去,比一张张摆模型高效得多。道路可以用 CatmullRomCurve3 画曲线,再沿曲线挤出带状几何体,弯道自然又省事。
3.3 贴图不显示:那个让我卡了两小时的经典问题
关键词里有个"three.js 贴图开始不显示",我猜不少人正被这个问题折磨,因为我就是其中之一。模型加载出来了,灰蒙蒙一片,贴图死活不出现。我按下面这个顺序排查,最后定位到问题:
- 贴图路径对不对。GLTF 文件里的贴图是相对路径引用的,如果你把 .gltf 和 .bin、贴图分开存放,路径一错就加载不到。用浏览器开发者工具的 Network 面板看有没有 404,这是最快的一步。
- 颜色空间设置。Three.js 新版本对颜色管理更严格,贴图如果是颜色贴图(map),需要设置
texture.colorSpace = THREE.SRGBColorSpace,否则会偏暗甚至看起来"没贴上"。这个问题特别隐蔽,因为贴图其实加载了,只是显示不对。 - 材质是否被覆盖。如果你在代码里手动给模型设置了
material = new THREE.MeshBasicMaterial(),那原来 GLTF 里的贴图就被替换掉了。我一开始为了"统一风格"手动改材质,结果把贴图全冲掉了,这个坑很典型。 - UV 坐标丢失。如果模型是从某些软件导出的,UV 可能没正确导出,贴图自然贴不上。这种情况要在建模软件里检查 UV 展开。
我最后的问题是第 2 条和第 3 条叠加——颜色空间没设,同时手动覆盖了材质。修完之后贴图瞬间就对了。排查这类问题的核心思路是:先确认资源加载了没(Network 面板),再确认材质用对了没(控制台打印 material),最后才怀疑渲染设置。顺序反了会浪费大量时间。
4. 性能优化:让母校在普通电脑上也能流畅跑
4.1 面数是 3D 网页渲染的头号敌人
3D 网页渲染和本地 3D 软件最大的区别是:浏览器能调用的 GPU 资源有限,而且用户设备参差不齐。我第一版模型没做减面,主楼一个模型就 20 多万个三角面,加载出来转一下卡一下。后来用减面工具压到 3 万面以内,视觉上几乎看不出差别,帧率直接从 20 多涨到 60。
减面的原则是:远景建筑狠压,近景主体适度压,细节靠贴图补。观众不会凑到墙面上数砖块,但会注意到整体轮廓和材质。所以法线贴图、AO 贴图这些"用贴图骗眼睛"的手段,在网页 3D 里性价比极高。关键词里提到的"3d减面工具"就是这个环节的关键,Blender 自带的 Decimate 修改器就够用,导出前记得检查有没有破面。
4.2 实例化:让重复物体不再拖后腿
母校场景里有一堆重复的东西——路灯、长椅、树、栏杆。如果每个都单独建 Mesh,draw call 会爆炸。Three.js 提供了 InstancedMesh,可以把同一个几何体的多个实例合并成一次绘制调用。我场景里几十盏路灯,用 InstancedMesh 之后 draw call 从几十降到 1,性能提升非常明显。
const lampGeo = new THREE.CylinderGeometry(0.1, 0.1, 4, 8); const lampMat = new THREE.MeshStandardMaterial({ color: 0x333333 }); const lampCount = 40; const lamps = new THREE.InstancedMesh(lampGeo, lampMat, lampCount); const dummy = new THREE.Object3D(); for (let i = 0; i < lampCount; i++) { dummy.position.set( Math.cos(i * 0.5) * 30, 2, Math.sin(i * 0.5) * 30 ); dummy.updateMatrix(); lamps.setMatrixAt(i, dummy.matrix); } scene.add(lamps);这段代码是 vibe coding 帮我生成的,我只需要告诉它"沿圆形路径均匀摆 40 盏路灯"。用 InstancedMesh 的注意点是:每个实例的变换要通过矩阵设置,不能直接改 position,而且实例之间共享材质,想单独改颜色得用 instanceColor。
4.3 加载策略:别让用户对着白屏发呆
3D 场景资源体积大,加载慢是常态。我的做法是分阶段加载:先加载一个极简的占位场景(地面 + 天空盒),让用户马上有东西看,同时显示加载进度条;主体建筑和细节模型在后台异步加载,加载完再逐个淡入。这样用户不会觉得"卡死了"。
GLTFLoader 自带进度回调,可以拿到已加载字节数和总数:
const loader = new GLTFLoader(); loader.load( 'campus.glb', (gltf) => { scene.add(gltf.scene); }, (xhr) => { const percent = (xhr.loaded / xhr.total) * 100; updateProgressBar(percent); }, (error) => { console.error('加载失败', error); } );这里有个经验:GLB 比 GLTF + 外部贴图更适合网页。GLB 是二进制单文件格式,把模型、贴图、材质全打包在一起,一次请求搞定,不会出现贴图路径丢失的问题。我第一版用 GLTF 分离格式,贴图老是加载不全,换成 GLB 之后世界都清净了。
5. vibe coding 实战:哪些活交给 AI,哪些必须自己扛
5.1 让 AI 写"套路代码",效率翻倍
项目里我让 AI 干得最爽的几件事:写响应式 resize 处理、封装加载器、生成相机漫游的键盘控制、写性能监控面板的接入代码。这些代码有固定套路,AI 生成的质量很高,我基本只做微调。比如键盘漫游,我描述"WASD 控制前后左右,鼠标拖动转视角,Shift 加速",AI 给的代码直接能用。
但要注意,AI 生成的代码默认是"能跑",不是"跑得好"。比如它给的 resize 处理经常忘了更新相机宽高比,导致窗口拉伸后画面变形。所以AI 写完的代码,你必须过一遍逻辑,尤其是涉及状态同步的地方。
5.2 让 AI 当"排错助手",比搜索引擎快
贴图不显示那次,我把报错和现象描述给 AI,它一口气列了七八个可能原因,我按它给的顺序排查,两小时搞定。这比我自己一条条搜快多了。AI 排错的价值在于它能快速穷举可能性,你负责验证。但它的答案不一定对,比如它一度让我去改渲染器的 outputColorSpace,其实问题不在那。把 AI 当"可能性生成器",而不是"标准答案",心态就对了。
5.3 这些活千万别交给 AI
有三类工作我坚持自己做:一是场景布局和坐标对齐,AI 不知道你母校长什么样;二是性能取舍,减面减到多少、阴影开不开,需要你对着实际帧率判断;三是视觉审美,相机初始角度、光照氛围、配色,这些是"感觉"层面的东西,AI 给不出你要的味道。vibe coding 的边界,就是"判断"和"审美"这两块它替不了你。
6. 那些没人告诉你、但一定会遇到的坑
6.1 模型单位和场景尺度对不上
我从 Blender 导出的模型是按米建的,但导入 Three.js 后发现楼比预期大十倍。原因是导出时单位设置和 Three.js 的默认单位没对齐。解决办法是在导出时统一用米,或者在代码里给模型整体scale.set(0.1, 0.1, 0.1)。这个坑的隐蔽之处在于,单看模型没问题,一放进场景和其他物体一对比就露馅了。建议项目一开始就定好"1 单位 = 1 米"的约定,所有资源都遵守。
6.2 移动端直接崩
我在电脑上调得好好的,用手机一打开,要么白屏要么卡死。原因有几个:一是模型面数对手机 GPU 还是太高;二是像素比没限制,手机高 DPI 屏渲染压力大;三是内存不够,贴图太大。后来我做了移动端降级:检测到移动设备就加载低模版本、限制像素比、压缩贴图分辨率。3D 网页项目一定要在真机上测,模拟器测不出性能问题。
6.3 光照调不好,整个场景就"假"
3D 场景真实感的一大半来自光照。我一开始只用了一盏平行光,场景平得像纸片。后来加了环境光(提供基础亮度)、半球光(模拟天空和地面反射)、一盏方向光(做主光源和阴影方向),层次感立刻出来了。但光加多了也会过曝,需要反复调强度。这里没有标准参数,只能对着效果调。一个实用技巧:先调环境光定基调,再加主光定方向,最后用补光填暗部。
6.4 浏览器兼容性和"有 Three.js 就卡"的现象
关键词里有个"谷歌网页有 three.js 就卡卡的",这现象真实存在。原因通常是页面里同时跑了多个 WebGL 上下文,或者渲染循环没做节流。我的处理是:确保整个页面只有一个 renderer,用 requestAnimationFrame 驱动渲染循环,页面不可见时(比如切到后台标签)暂停渲染。另外,如果页面里还嵌了其他 3D 内容,尽量合并到一个渲染器里,别开多个。
7. 开学季投稿之外,这个项目还能怎么延展
做完母校 3D 场景后,我发现这套技术栈的延展性比想象中强。同样的思路可以拿来做校园导览、虚拟展厅、毕业纪念册的 3D 版,甚至给社团做个线上招新场景。如果再加点交互——点击建筑弹出介绍、走到特定位置触发回忆文字、加个背景音乐——它就从"能看的模型"变成了"能逛的空间"。
技术上还能继续挖的方向也不少:用 3D 高斯三维重建把真实校园扫描进来,还原度会高一个量级;接入点云数据做更精细的地形;用后期处理加景深和泛光,画面质感再上一层。不过这些都属于"锦上添花",先把基础场景跑流畅、跑稳定,比堆特效重要得多。
我个人最大的体会是:vibe coding 降低了 3D 网页开发的门槛,但没有降低它的难度。门槛低在"你不用背 API 了",难度还在"你得懂原理、会判断、能调优"。工具帮你省下的是打字时间,省不下的是思考时间。如果你也想在开学季给母校做点什么,别被"3D""WebGL"这些词吓住,从一个空场景加一个立方体开始,一步步往里填,你会发现它没有想象中那么遥不可及。真正难的从来不是技术,是你愿不愿意花那两个周末,把脑子里那个场景,一点点变成能转动的现实。