我第一次在浏览器里跑出那个旋转立方体的时候,盯着屏幕看了将近一分钟。不是因为那个立方体有多好看——说实话,它就是个简陋的彩色方块——而是因为整个过程完全没有安装任何本地软件,只需要一个浏览器、一个代码编辑器,写完保存、刷新页面,画面就出来了。这类“什么都不装,把代码写进 HTML 就能出 3D 画面”的体验,是你后面所有浏览器 3D 开发的地基。如果你也想在网页里做出 3D 效果,但被“WebGL”“着色器”“矩阵变换”这些名词劝退过,这篇文章就是写给你的。我会基于 Three.js 这个目前最主流的 WebGL 封装库,带你从零开始搭建场景、添加光源、实现交互,最后再补充几个我实际踩过的坑,以及从“一个方块”往“真实项目”迈进的路线。
1. 为什么选择 Three.js:WebGL 的痛点与 Three.js 的解法
1.1 原生 WebGL 为什么让人劝退
想直接在浏览器里做 3D,底层绕不开 WebGL。WebGL 是一套浏览器提供的图形接口,它本身能力很强,但极不友好。用原生 WebGL 画一个三角形,需要你先写顶点着色器、片段着色器,再手动创建缓冲区、绑定数据、编译着色器程序、建立矩阵、调用绘制命令。我当年照着教程敲了一个三角形,耗时大半天,而且一旦写错,控制台里报错信息往往只有一行类似于“ERROR: 0:1: '' : version '300 es' is not supported”的提示,全靠猜。
更麻烦的是 3D 数学。旋转、平移、缩放、透视投影,每一项背后都是一堆矩阵运算。你当然可以硬啃,但问题在于:大部分业务场景里,你真正想写的是“这里有栋楼,那里有棵树,相机围着楼转”——不是“请把这个 4x4 矩阵和我刚才算出的另一个矩阵相乘”。
1.2 Three.js 替你干了什么
Three.js 就是在这层痛点之上做了一套抽象。它把 WebGL 的繁琐细节封装成了几个很直观的核心对象:场景(Scene)、相机(Camera)、渲染器(Renderer)、几何体(Geometry)、材质(Material)、光源(Light)。我用一个生活化的比喻来解释:场景就是舞台,相机是观众的眼睛,渲染器是那个负责把舞台实况转播到屏幕上的摄像机团队,几何体是演员的长相,材质是演员的衣服和化妆,光源则是打光师。
这套抽象的厉害之处在于,你不需要理解 GPU 是怎么工作的,也能写出渲染效果还不错的 3D 页面。底层的东西 Three.js 都给你处理好了,包括矩阵计算、着色器编译、绘制状态管理、动画循环调度这些。
1.3 一个反直觉的事实:引入 Three.js 并不一定“重”
很多人一听“库”就觉得会让页面变重。Three.js 最基础的核心库压缩后大约 600KB 左右,这确实不算轻,但要注意三点:第一,它支持按需引入——如果你只需要核心渲染,可以用 ES Module 的形式只引入需要的模块,配合 tree-shaking 能把打包体积压下来不少;第二,浏览器会缓存,用户第一次加载之后,后面再次访问几乎无感;第三,相对于你手写 WebGL 耗费的开发时间成本,这点体积通常是完全划算的。
如果你做的只是一个数据可视化的小场景,几百 KB 的代价换来的是一套完整的可维护代码,这笔账值得算清楚。我在生产项目里见过有人为了避免引库手写 WebGL,结果一个旋转展示功能写了一周多,而且各种边缘情况处理不完,最终还是换回了 Three.js。
2. 开发环境准备:一个浏览器就够了,但引入方式要选对
2.1 最基础的配置清单
做 Three.js 开发,硬件和软件门槛都很低。你只需要三样东西:
- 一个现代浏览器。推荐 Chrome 或者 Edge,它们对 WebGL 的支持最完善,调试工具也好用。Firefox 和 Safari 也可以,但某些高级特性会有差异。
- 一个代码编辑器。VS Code 是当下最主流的选项,装一个叫 “ES7 React/Redux/GraphQL/React-Native/JSX snippets” 的插件对写 JavaScript 有帮助,但其实你不用插件也能写。
- 一个本地静态服务器。如果你只是写一个单独的 HTML 文件,直接双击打开就能跑;但如果后面要加载外部模型、纹理图片,就涉及跨域问题,最好起一个本地服务器,VS Code 里装 “Live Server” 插件或者用 Python 的
python -m http.server都行。
2.2 引入 Three.js 的几种方式
引入 Three.js 的方式有好几种,选择不同,后续踩坑的概率完全不同。
第一种:直接下载three.min.js文件,用<script>标签引入。这种方式最古老,适合纯静态页面演示,但现在已经不推荐了,因为 Three.js 从 r150 版本左右开始全面转向 ES Module,旧版全局变量的方式已经逐步淘汰。
第二种:CDN 方式,配合 ES Module。这是我最推荐新手使用的方式。不需要下载任何文件,直接在<script type="module">里用 import 引入。不过要注意,使用 CDN 时必须配合 importmap 来指定模块地址。
第三种:npm 安装。如果你用的是 Vue 或 React 这类工程化项目,或者准备长期维护一个项目,直接用包管理器安装最合适。npm install three之后按需 import。
2.3 新手最推荐的方式:CDN + importmap
我建议第一步先用 CDN + importmap 的方式跑起来,因为这条路最直观,也不容易被构建工具的各种配置淹没学习重点。
<script type="importmap"> { "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js", "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/" } } </script>这个 importmap 的作用很简单:给浏览器一个映射表,告诉它当你看到import * as THREE from 'three'这种语句的时候,实际去哪个网址加载代码。后面的three/addons/映射,是用来加载 OrbitControls、GLTFLoader 这些扩展模块的。
注意:这里的版本号我用的是 0.160.0,你在实际使用时可以换成当前更新的稳定版本,但务必保证主库和扩展组件的版本一致,否则会出现 API 不匹配导致的报错。
完成这一步之后,你的开发环境就准备好了。如果你发现连页面里的import都报错,先检查是不是用了file://协议直接打开 HTML 文件——模块加载在file://下会被浏览器拦截,换成 Live Server 或本地服务器就正常了。这个坑我见过无数次。
3. 三个核心部件:场景、相机、渲染器的工作原理
3.1 场景(Scene):你的虚拟世界舞台
Three.js 里创建场景只花一行代码:const scene = new THREE.Scene()。但这一行背后的含义值得理解:场景是一个容器,你往里面放的一切东西——几何体、光源、辅助线、相机本体——都挂在场景这个对象下,或者挂在场景下其他对象的子节点下。Three.js 使用场景图(Scene Graph)的结构来管理物体之间的层级关系。
举个例子:你想做一个太阳系,太阳是一个球体,地球又是另一个球体,还带着月球。这时候你可以把地球作为太阳的子节点,月球作为地球的子节点。当太阳旋转时,地球会跟着绕太阳转?不完全是——更准确地说,是地球在太阳的局部坐标系里运动,而太阳的运动也会叠加到地球上。这种树状结构非常符合现实世界的物体关系,也是 Three.js 组织大型场景的基本方式。
场景还有两个很实用的属性:background和fog。scene.background可以用来设置背景颜色或者天空盒纹理,scene.fog用来添加雾效,让远处的物体逐渐融入背景色,增加场景的空间感。这些属性是在渲染时统一生效的,不需要你逐个物体去设置。
3.2 相机(Camera):决定观众看哪里
Three.js 里有多种相机类型,最常用的是透视相机(PerspectiveCamera)和正交相机(OrthographicCamera)。新手阶段主要用透视相机,因为它的成像原理和我们人眼看世界的方式一致——近大远小。
透视相机的构造函数接受四个参数:
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)四个参数分别是:视野角度(FOV)、宽高比(Aspect)、近裁剪面(Near)、远裁剪面(Far)。
- 视野角度:用度数表示,值越大,看到的世界范围越广,但物体越容易出现透视畸变。75 度是常用值,大致相当于一些第一人称射击游戏的视角感受。
- 宽高比:通常设置成窗口宽度除以窗口高度,这样画面才不会横向拉伸或纵向挤压。这个参数很容易被忽略,但一旦窗口大小变化没同步更新,画面就会变形得很难看。
- 近裁剪面和远裁剪面:表示相机能看到的最近距离和最远距离。比近裁剪面近、比远裁剪面远的物体都会被裁掉。如果你发现模型突然“穿帮”了一部分,或者远景的东西不见了,第一反应应该检查这两个值。
相机本身也需要确定位置和朝向。camera.position.set(x, y, z)设置位置,然后用camera.lookAt(0, 0, 0)让它看向原点。一个常见的误区是只设置了位置却忘了让它看向目标,导致画面里什么都没有。
3.3 渲染器(Renderer):把世界画到屏幕上的画师
场景和相机都准备好了,但如果你不创建渲染器,浏览器里依旧什么都看不到。渲染器的作用就是读取场景里的所有数据,结合相机视角,通过 GPU 把画面绘制到页面的<canvas>元素上。
创建渲染器的方式:
const renderer = new THREE.WebGLRenderer({ antialias: true }) renderer.setSize(window.innerWidth, window.innerHeight) document.body.appendChild(renderer.domElement)antialias: true是开启抗锯齿,默认是关闭的,如果不开,旋转物体会看到明显的锯齿边缘。setSize设置渲染输出的尺寸,同时也会自动设置 canvas 的宽高。最后的renderer.domElement就是实际的 canvas 元素,把它挂到页面上才能显示。
另一个容易忽略的点是renderer.setPixelRatio。在高 DPI 屏幕上(比如 Retina 屏),如果不设置像素比,画面会很模糊。通常这样设置:
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))这里的Math.min(..., 2)是为了限制过高的像素比——因为手机上有的设备像素比能达到 3 甚至更高,如果完全按照像素比渲染,GPU 压力会成倍增长,性能反而不行。限制到 2 可以在画质和性能之间取一个平衡,实测这个方案在所有主流设备上表现都稳定。
4. 手写第一个 3D 场景:从空 HTML 到旋转立方体
4.1 一个完整的骨架
把前面这些基础组件串起来,就有了第一个完整的 HTML 文件。下面这段代码是一个极简但完整的 Three.js 场景,你复制到编辑器里,用 Live Server 打开就能看到效果。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>第一个Three.js场景</title> <style> body { margin: 0; overflow: hidden; } </style> </head> <body> <script type="importmap"> { "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js", "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/" } } </script> <script type="module"> import * as THREE from 'three' // 1. 创建场景 const scene = new THREE.Scene() // 2. 创建透视相机 const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) camera.position.set(2, 2, 5) camera.lookAt(0, 0, 0) // 3. 创建渲染器 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) // 4. 创建立方体 const geometry = new THREE.BoxGeometry(1, 1, 1) const material = new THREE.MeshStandardMaterial({ color: 0x00aaff, roughness: 0.4, metalness: 0.2 }) const cube = new THREE.Mesh(geometry, material) scene.add(cube) // 5. 添加光源 const ambientLight = new THREE.AmbientLight(0xffffff, 0.5) scene.add(ambientLight) const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0) directionalLight.position.set(5, 10, 7) scene.add(directionalLight) // 6. 渲染循环 function animate() { requestAnimationFrame(animate) cube.rotation.x += 0.01 cube.rotation.y += 0.01 renderer.render(scene, camera) } animate() </script> </body> </html>如果你用的 Three.js 版本跟上面一致,这段代码应该能直接跑出一个淡蓝色、带光影的旋转立方体。
4.2 一步步拆解:这段代码里到底发生了什么
第 4 步的立方体看起来只有三行,但背后是 Three.js 的核心抽象:BoxGeometry负责生成一个 1x1x1 的立方体网格数据(顶点坐标、法线、UV 坐标等),Material负责定义表面外观,Mesh则把几何体和材质组合成为一个可以被放入场景的可见对象。在渲染管线里,GPU 会根据几何体的顶点数据画出立方体的形状,再根据材质的参数决定每个片元的颜色,结合光源方向算出明暗。
第 5 步的光源设置非常重要。如果你用MeshBasicMaterial,它不受光照影响,什么光源都不需要就能看见颜色。但MeshStandardMaterial是基于物理渲染(PBR)的材质,没有光源时物体是纯黑色的,只有加上光源,它才会根据光照方向和表面法线计算出阴影和颜色。AmbientLight 提供均匀的基础照明,避免阴影面变成死黑;DirectionalLight 模拟太阳这种平行光源,能产生清晰的明暗对比。
第 6 步的动画循环也值得多说两句。requestAnimationFrame(callback)告诉浏览器:下一帧渲染之前,请先执行这个回调。浏览器会尽量以屏幕刷新率(通常是 60Hz)来调用它。通过这个循环,每一次渲染之前先把方块的旋转角度增大一点,再进行渲染,画面就动起来了。
4.3 为什么有些教程里的立方体是光的?浅谈 MeshBasicMaterial 与 MeshStandardMaterial
如果你在网上看过其他 Three.js 入门教程,可能会碰到用MeshBasicMaterial也能显示彩色立方体的例子。那是因为这个材质不参与光照计算,直接把颜色输出到屏幕,就像一张贴纸,永远亮堂堂的,但没有任何立体感。
新手刚接触时容易踩的坑是:用 BasicMaterial 做出来的东西看起来“很平”,于是误以为自己哪里代码写错了。其实不是,是你选错了材质。我的建议是第一次学就直奔MeshStandardMaterial,它是 Three.js 里最接近真实世界材质表现的模型,配合光源能直观感受到 3D 的“立体感”。后面如果需要做风格化效果,再回头研究其他材质。
4.4 调试视觉的好帮手:添加坐标轴和网格辅助线
初学阶段,你很难通过坐标判断自己看到的东西到底在哪个方向。Three.js 提供了两个非常实用的可视化辅助工具:AxesHelper和GridHelper。
const axes = new THREE.AxesHelper(5) scene.add(axes) const grid = new THREE.GridHelper(10, 10) scene.add(grid)AxesHelper用红、绿、蓝三条线分别代表 X、Y、Z 轴的正方向。GridHelper则在 XZ 平面上画了一个网格,帮你建立地平面概念。这两样东西在调试阶段几乎是必备的——比如你导入了一个 3D 模型,发现模型半截陷进地里或者飘在空中,看一眼网格线马上就能定位问题。
经验之谈:这两个辅助线在开发时很有用,但上线时记得删掉。我见过不止一次,生产环境的页面里网格线堂而皇之地挂在场景里,非常出戏。
5. 动起来:动画循环、轨道控制器与窗口自适应
5.1 requestAnimationFrame 与“帧”的概念
上一节代码里的animate函数是最基础的动画循环。但这里有一个容易忽略的细节:cube.rotation.x += 0.01的方式,每帧的增量是固定的,它跟帧率无关。在 60Hz 屏幕上,这个立方体每秒旋转 0.6 度左右?不对,0.01 弧度每秒 60 帧,大约是每秒旋转 0.6 弧度,约等于 34 度每秒。但如果运行设备的刷新率是 120Hz,同样的增量会让旋转速度翻倍。
更规范的做法是使用时钟对象THREE.Clock,它提供getDelta()和getElapsedTime()来获取真实的时间差。这样,无论帧率如何变化,你的动画速度都是稳定的。
const clock = new THREE.Clock() function animate() { const delta = clock.getDelta() const elapsedTime = clock.getElapsedTime() cube.rotation.y += delta renderer.render(scene, camera) requestAnimationFrame(animate) }实际上,这种需要精确控制速度的场景,我一般直接基于elapsedTime计算角度,比如cube.rotation.y = elapsedTime * 0.5,效果更干净。
5.2 给用户一只手:接入 OrbitControls
看到这里,你已经有一个会旋转的立方体了。但更常见的交互方式是让用户自己拖拽旋转视角。Three.js 官方提供了一个非常实用的扩展组件——OrbitControls(轨道控制器),它封装了鼠标拖拽旋转、滚轮缩放、右键平移等一整套逻辑。
import { OrbitControls } from 'three/addons/controls/OrbitControls.js' const controls = new OrbitControls(camera, renderer.domElement) controls.enableDamping = true // 开启阻尼(惯性)效果 controls.dampingFactor = 0.05 // 阻尼系数接入之后,用户就能用鼠标左键拖拽旋转视角,滚轮缩放,右键平移,非常符合现在 3D 查看器的操作习惯。enableDamping开启后,松手时视角不会立刻停住,而是有一个平滑的惯性滑动,体验感提升非常明显。
需要注意一个坑:开启了阻尼后,你必须在动画循环里调用controls.update(),否则阻尼不会生效。如果你发现画面一直在跳动或者拖拽手感怪怪的,先检查一下是不是忘了这一步。
5.3 窗口大小变化时的自适应处理
浏览器窗口是可以随时调整大小的,但如果你不做任何处理,画面会出现两种情况:一是 canvas 尺寸不变,导致显示被拉伸或留白;二是相机宽高比没变,画面中的物体会被压扁或拉长。
正确的处理方案是监听resize事件:
window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight camera.updateProjectionMatrix() renderer.setSize(window.innerWidth, window.innerHeight) })这段代码里,updateProjectionMatrix()也很容易漏掉。相机的投影矩阵是缓存好的,修改了宽高比之后必须调用这个方法重新计算,否则画面会依然保持拉伸状态。你可以在控制台打印camera.projectionMatrix观察它在调用前后的差异。
5.4 把场景背景从黑色换成喜欢的颜色或渐变
默认场景背景是黑色的。如果你觉得黑色沉闷,可以设置scene.background:
scene.background = new THREE.Color(0x1a1a2e)也可以使用渐变色背景,这个稍微复杂一些,需要创建一个覆盖屏幕的平面或者使用 CSS 背景,然后让 canvas 透明。我个人的习惯是,简单的单色背景直接改scene.background,需要渐变或纹理背景时,用 CSS 背景更省事。
6. 从立方体到真实世界:模型加载、性能优化与深坑排雷
6.1 加载外部模型:把 Blender 里的资产搬到浏览器里
立方体只是起点。真实项目里,你通常需要加载设计师用 Blender、Maya 或 3ds Max 制作的角色、建筑、机械臂等模型。Three.js 支持的模型格式很多,如果团队是自己做资产,我强烈推荐 GLTF/GLB 格式,它是当前 Web 3D 领域的“通用语言”,不仅支持几何体、材质,还支持骨骼动画、场景层级、相机等丰富信息。
加载 GLTF 模型需要使用加载器:
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js' const loader = new GLTFLoader() loader.load( 'path/to/model.glb', function (gltf) { const model = gltf.scene scene.add(model) }, function (xhr) { console.log((xhr.loaded / xhr.total) * 100 + '% loaded') }, function (error) { console.error(error) } )加载器本身不复杂,最常见的坑集中在两个地方:路径问题和尺寸/坐标问题。
路径问题:如果你是用file://协议直接打开 HTML 去加载外部模型,大概率会报跨域错误。解决方案很简单——用本地服务器启动,就像我在开发环境那节提到的。
尺寸/坐标问题:建模软件里制作时可能用的是厘米或者毫米单位,而 Three.js 以米为单位。如果模型导出时没做单位换算,导入后可能大得离谱或小得看不见。遇到这种情况,先用model.scale.set(0.01, 0.01, 0.01)这类数值去试,直到缩放比例合适。还有一种常见情况是模型面朝错误方向,这时可以直接旋转模型节点来修正朝向。
6.2 性能优化:渲染调优的几个检查点
当你的场景从小清新变成了复杂场景,性能问题就会浮出水面。我自己在优化一个包含几千个模型节点的厂房场景时,踩过了不少坑,总结下来优化点集中在五个方面:
| 优化点 | 做法 | 效果 |
|---|---|---|
| 几何体合并 | 把多个静止且使用相同材质的几何体用mergeGeometries合并 | 减少 draw call,效果最显著 |
| 纹理压缩 | 使用压缩格式(如 KTX2)或选择合适的 mipmap 精度 | 降低显存占用和 GPU 采样开销 |
| LOD 多细节层次 | 根据相机距离切换不同精度的模型 | 远距离时大幅降低 GPU 负载 |
| 实例化渲染 | 大量相同模型(如树木、建筑)用InstancedMesh | 一个 draw call 渲染成千上万个副本 |
| 限制阴影范围/分辨率 | 只为近处关键物体开启阴影,必要时降低 shadow map 分辨率 | 让画面明显更流畅 |
我见过最夸张的性能提升来自几何体合并。在一个工厂车间场景里,原来有 3000 多个小螺钉,每个只有几十个面,但每个都是独立 mesh,导致 draw call 爆炸、帧率只有十几帧。合并之后,draw call 从 3000 掉到几十个,帧率直接回到 60。
GPU 侧还有一个隐藏杀手是半透明物体。半透明物体的渲染需要从远到近排序,排序计算在物体数量大的时候非常贵。能用不透明材质实现的,尽量避免半透明;必须用的时候,尽量少用。
6.3 常见深坑:透明贴图、裁剪面与色彩管理
透明贴图是新手最容易踩的一个坑。当你给模型贴带透明区域的 PNG 贴图时,如果你用的是MeshStandardMaterial,还必须在材质上设置transparent: true,否则透明区域会显示成黑色。更隐蔽的问题是透明排序——如果模型内部还有多层透明面,Three.js 的默认排序算法不一定正确,导致透明区域出现遮挡问题。
相机裁剪面我也要再强调一次。默认的近裁剪面是 0.1,远裁剪面是 1000。如果你导入一个超大尺寸的山脉模型,物体的一部分比 1000 还远,你会发现山被“切”了一半,特别诡异。这时候就需要增大远裁剪面,或者在模型导入后统一缩放尺寸把它放进安全的可视范围内。
色彩管理这个问题比较新。从 r152 版本开始,Three.js 默认启用了色彩管理(Color Management),这意味着你设置color: 0xffffff时,内部会进行一次色域转换。如果你从旧版本项目迁移代码,会发现渲染出来的颜色和以前不一样,这是因为旧的代码没有做色彩空间转换,实际是“偏色”的状态。现代 Three.js 里,正确的做法是让美术资产的贴图使用SRGBColorSpace,而逻辑颜色使用LinearSRGBColorSpace,不要两种混在一起不管,否则颜色会越调越离谱。
6.4 数字孪生、产品展示与可视化仪表盘:Three.js 到底适合做什么
学完前面的内容,你可能已经感觉到 Three.js 的上限很高,但它具体能做什么,我根据自己的实践经验给你画一个范围。最适合的场景是产品展示,比如电商的 3D 看鞋、实验室的 3D 展示机械臂模型;其次是数字孪生,把工厂产线、城市建筑在浏览器里做可视化,这个方向这两年需求极多;还有数据可视化,把 3D 散点图、地形图、点云数据在网页里呈现出来,交互性强,视觉冲击力也强。
不太适合的场景是:移动端上非常复杂的全屏 3D 游戏。手机 GPU 的填充率和显存带宽有限,过多的真实感材质和复杂光照会让你在优化上投入巨大精力。如果你想做浏览器游戏,要么限制场景规模,要么考虑换向专注游戏性能的引擎。
7. 浏览器 3D 的边界:点云、高斯泼溅与更多可能
7.1 除了 Three.js,浏览器 3D 还有哪些方向
Three.js 是入门和多数场景的最佳选择,但它并不是唯一答案。顺着浏览器 3D 这条线继续往前走,有两个方向跟你搜索到的关键词高度相关。
第一个是3D 点云(Point Cloud)。激光雷达、结构光相机扫描出来的数据本质上是海量的三维坐标点。Three.js 里有专门的Points类来渲染点云,配合 GPU 级别的粒子系统,可以毫无压力地显示百万级别的点。我的一个做测绘的朋友用 Three.js 做了扫描点云预览方案,整个链路非常清晰,从点云数据解析到可视化只花了两三天就搭出了原型。
第二个是3D 高斯泼溅(3D Gaussian Splatting)。这是最近一两年火起来的技术,用一组带颜色和透明度的高斯函数来拟合物体表面,相比传统网格重建,它的渲染效果极其逼真,而且实时性能相当好。Three.js 社区已经有了相关的渲染器扩展,如果你玩过那些可以直接在网页里 360 度浏览真实物体的 demo,多半就是它。这个方向正好对应你搜索到的“3d gaussian splatting”,它在数字人、实景扫描电商展示这些方向潜力很大。
7.2 与 3D 建模、3D 打印的结合
很多做 3D 打印的朋友搜索“3d建模”和“3d打印”相关内容,其实 Three.js 在这条链路里也能找到位置。比如,你可以在浏览器里展示 STL 或 OBJ 模型的预览,让用户在打印前检查模型结构、旋转查看细节、测量尺寸,甚至对模型做简单的切片预览。Three.js 有针对 STL 格式的加载器,加载后本质就是一个网格,缩放、旋转、控制材质都很方便。
如果你在做机械臂毕业设计这类项目,可以把机械臂的装配体导出成 GLTF,然后在网页里做关节联动动画,展示每个自由度的运动范围。这个实现并不难:给机械臂的各个旋转关节建立好父子层级关系,然后在动画循环里按顺序设置旋转角度即可。
7.3 浏览器 3D 的性能边界与选型建议
浏览器 3D 的边界在哪?从硬件层面看,它受限于设备的 GPU 能力和内存;从软件层面看,它受限于网页安全机制——比如无法直接访问 GPU 底层资源、内存分配有上限。但坦白说,在绝大多数业务场景里,这个边界都足够你用。一个几百 MB 的工业模型确实啃不动,但经过轻量化处理、压缩、合并之后,在主流笔记本上流畅跑起来是完全可以做到的。
选型上,我的建议是:如果你做的是产品可视化、数字孪生、交互展示、数据可视化,直接选 Three.js 是最稳的;如果你要做超大体量的 BIM 建筑信息模型,可以了解下专门的 BIM 可视化引擎;如果你要做的是重度浏览器游戏,可以考虑 Babylon.js 或者专门的游戏引擎。不同工具的擅长领域不太一样,不要盲目跟风,想清楚业务需求再定。
根据我个人折腾这么多项目的体会,浏览器 3D 的门槛远没有想象中高。关键是把场景、相机、渲染器这三个基本组件玩透,再逐步往材质、光照、加载器和性能优化方向扩展。这篇文章里你看到的每一个坑,都是我一次次用自己的项目踩出来的,如果你在照着做的过程中遇到了报错,先把报错信息完整贴到搜索引擎里搜一遍,绝大多数问题都有人遇到过。剩下的那些没人遇到过的问题,才是你真正开始理解 Three.js 的开始。希望你能从那个旋转立方体出发,一点点点亮属于你自己的 3D 世界。