Three.js实战:从零搭建3D场景的完整开发流程
为什么选 Three.js?因为老板说要“炫一点”
上个月接了个活,给一个产品页加个3D展示模块。客户没给具体需求,就一句话:“要有点科技感,能转的那种”。我一听就知道逃不掉了——得上 Three.js。虽然之前只做过几个 demo,但好歹跑通过基础流程,心想无非是加载模型、加个轨道控制器,应该问题不大。
结果……真打脸打得啪啪响。
一开始的“理想方案”:直接上 GLTFLoader
项目初期我想得很简单:用 GLTFLoader 加载设计师给的 .glb 文件,配上 OrbitControls,搞定收工。代码也确实几行就能跑起来:
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const loader = new GLTFLoader();
loader.load('model.glb', (gltf) => {
scene.add(gltf.scene);
});
const controls = new OrbitControls(camera, renderer.domElement);
camera.position.z = 5;
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
本地跑起来没问题,模型能转、能缩放,还挺流畅。但一放到测试环境,手机端直接卡成幻灯片。我这才意识到:这玩意儿在低端机上根本扛不住。
最大的坑:性能崩了,尤其是移动端
测试同事拿了个三年前的安卓机来测,页面加载完直接白屏两秒,然后转一下模型就掉到10帧。我赶紧开 DevTools 看 performance,发现光 render 就占了 80% 的主线程时间。
开始没想到问题出在哪。后来查资料才知道,Three.js 默认启用了抗锯齿(antialias),这在桌面端还好,但在移动端会严重拖慢 GPU。关掉它之后帧率立马回升:
// 把 antialias: true 改成 false
const renderer = new THREE.WebGLRenderer({ antialias: false });
但这还不够。模型本身有 12MB,面数太高。设计师给的是高模,根本没做优化。我跟他们沟通后,让他们用减面工具压到 50k 面以内,文件缩小到 3MB。加载速度明显快了,但低端机还是偶尔卡顿。
这时候我想到了 DRACO 压缩。Three.js 官方支持 DRACO 解码,但配置有点绕。折腾了半天,终于搞定了:
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/'); // 注意路径
loader.setDRACOLoader(dracoLoader);
这里注意我踩过好几次坑:setDecoderPath 的路径必须指向有效的 DRACO 解码器文件,而且不能跨域问题。最后用了 Google 的 CDN,省事。
加上 DRACO 后,模型体积又小了 40%,加载快了不少。但低端机旋转时还是偶尔掉帧。我试了降低渲染分辨率:
renderer.setPixelRatio(window.devicePixelRatio > 1 ? 1 : window.devicePixelRatio);
这一招亲测有效!在 Retina 屏上强制用 1x 分辨率渲染,GPU 负担小了很多,帧率稳住了。
交互细节:用户以为能点,其实不能
另一个问题是用户反馈:“为什么点模型没反应?”原来他们以为这是可点击的 3D 商品,能查看细节。但我们项目里只是静态展示,没加任何射线检测(Raycasting)。
临时加了个简单的点击反馈:
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
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(scene.children, true);
if (intersects.length > 0) {
console.log('点中了模型');
// 这里可以加 tooltip 或高亮
}
});
虽然没做复杂交互,但至少让用户知道“系统收到了点击”,心理感受好多了。
最终的妥协:放弃自动旋转,改用手动
最初我还想加个自动缓慢旋转的效果,显得更“高级”。代码很简单:
// 在 animate 里加
gltf.scene.rotation.y += 0.005;
但测试发现,自动旋转会让用户失去方向感,尤其在手机上晃来晃去反而体验差。而且一旦用户手动拖动,自动旋转还会冲突。最后干脆砍掉,只保留纯手动控制。
另外,OrbitControls 默认允许上下翻转,导致模型“倒立”,用户很懵。我限制了极角:
controls.minPolarAngle = Math.PI / 6; // 不能看底部
controls.maxPolarAngle = Math.PI / 2; // 不能翻到头顶
回顾与反思:哪些做得好,哪些还能优化
整体来说,这个模块上线后没收到性能投诉,算是过关了。做得好的地方:
- 及时关掉 antialias 和限制 pixel ratio,保住了移动端帧率
- 推动设计师优化模型,从源头减负
- 用 DRACO 压缩,加载时间缩短近一半
但还有几个没完全解决的问题:
- 首次加载时还是有白屏(约1.5秒),虽然加了 loading 动画,但体验一般。本来想试试懒加载或骨架屏,但工期不够就没做
- 不同机型的 WebGL 支持度差异大,有台华为老机型直接报错“WebGL not supported”。最后加了个兜底判断,不支持就显示静态图,但没做优雅降级
这个方案肯定不是最优的,比如可以用 MeshoptDecoder 替代 DRACO 获得更好压缩率,或者用 instanced rendering 批量渲染。但对我们这种一次性展示场景,简单稳定更重要。
写在最后
以上就是我这次用 Three.js 做产品展示的实战总结。过程里踩了不少坑,但也积累了些实用技巧。如果你也在做类似的轻量级 3D 展示,建议优先考虑性能,别一上来就堆特效。
代码里提到的 API 示例地址如 fetch('https://jztheme.com/api/model') 仅为演示用途,实际项目请替换为自己的接口。
这个技巧的拓展用法还有很多,比如结合 GSAP 做动画、用 postprocessing 加景深,后续会继续分享这类博客。以上是我踩坑后的总结,希望对你有帮助,有更优的实现方式欢迎评论区交流。

暂无评论