Three.js从入门到实战我踩过的那些坑和优化心得
这个项目为什么要用Three.js
最近做了个产品展示页面,客户要求做那种3D的产品展示,能旋转、缩放、点击热点查看详情。本来想用CSS 3D Transform凑合一下,但想想还是太假了,交互也不够流畅。客户那边催得紧,时间有限,最后还是选择了Three.js。说实话,虽然之前也用过几次,但真正做商业项目还是第一次,心里还真有点发怵。
基础框架搭建就踩了个坑
刚开始按照官方文档搭了个基础框架,结果发现性能特别差。项目中遇到了帧率只有10-15fps的问题,卡得不行。调试了半天才发现,原来是我的渲染循环写错了:
// 错误的写法
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
// 正确的写法
function animate() {
requestAnimationFrame(animate);
// 这里需要判断是否需要重新渲染
if (needsUpdate) {
renderer.render(scene, camera);
needsUpdate = false;
}
}
后来调整了方案,在用户没有交互的时候暂停渲染,只有在鼠标移动或者触摸的时候才重新渲染。这样做确实提升了不少性能,但代码复杂度也上去了。
模型加载和纹理处理是个头疼事
产品模型是从设计师那里拿来的glb文件,本来以为直接load就行,结果发现模型太大了,gzip后还有3MB多。用户加载要等好久,体验很差。后来用了DRACO压缩插件,把模型压缩到了800KB左右,但加载时间还是不够理想。
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://jztheme.com/libs/draco/');
loader.setDRACOLoader(dracoLoader);
loader.load(
'model.glb',
(gltf) => {
scene.add(gltf.scene);
model = gltf.scene;
// 加载完成后做一些初始化工作
initControls();
showLoading(false);
},
(progress) => {
const percent = Math.round((progress.loaded / progress.total) * 100);
updateProgress(percent);
},
(error) => {
console.error('模型加载失败:', error);
showLoadError();
}
);
纹理贴图这块也有坑,某些设备上会出现纹理模糊或者加载失败的情况。最后发现是纹理格式的问题,把PNG转成WEBP格式后效果好了不少。
交互控制的实现比想象复杂
旋转、缩放这些基本操作看起来简单,实际上要做得顺滑真不容易。开始我想自己写控制逻辑,结果发现各种边界情况处理起来很麻烦。后来还是用了OrbitControls,但默认的参数也不太符合需求,需要大量调整。
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
function initControls() {
controls = new OrbitControls(camera, renderer.domElement);
// 配置参数
controls.enableDamping = true; // 启用阻尼,增加惯性效果
controls.dampingFactor = 0.05; // 阻尼系数
controls.rotateSpeed = 0.8; // 旋转速度
controls.zoomSpeed = 1.2; // 缩放速度
controls.panSpeed = 0.5; // 平移速度
// 限制缩放范围
controls.minDistance = 2;
controls.maxDistance = 10;
// 限制旋转角度
controls.minPolarAngle = Math.PI / 6;
controls.maxPolarAngle = Math.PI / 2;
controls.addEventListener('change', () => {
needsUpdate = true;
});
}
这里注意我踩过好几次坑,移动端的触摸事件处理和PC端不一样,需要额外处理。特别是双指缩放和单指旋转的冲突问题,搞了一整天才调好。
热点交互的设计
产品上有一些热点区域,用户点击后显示相关信息。这个功能本身不难,但要做到准确拾取和视觉反馈就比较复杂了。我用了射线检测来实现点击检测:
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseClick(event) {
// 计算鼠标位置的标准化设备坐标 (-1 到 +1)
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// 检测与模型的交点
const intersects = raycaster.intersectObjects(model.children, true);
if (intersects.length > 0) {
const point = intersects[0].point;
const face = intersects[0].face;
// 检查是否点击了热点区域
checkHotspot(point, face);
}
}
function checkHotspot(point, face) {
// 这里根据实际的热点数据判断点击位置
for (let hotspot of hotspots) {
if (isPointInHotspot(point, hotspot)) {
showHotspotInfo(hotspot);
break;
}
}
}
热点的可视化也很重要,我用了一些半透明的球体作为热点指示器,这样用户能直观看到可点击的位置。不过在复杂的模型上,这些指示器可能会被遮挡,后期还得想办法优化。
性能优化的各种尝试
性能问题始终是最大的挑战。除了之前提到的按需渲染,我还试了其他几种优化手段。LOD(Level of Detail)技术挺有用的,远距离时使用简化模型:
// LOD实现
const lod = new THREE.LOD();
const highDetailModel = loadModel('high-detail.glb');
const mediumDetailModel = loadModel('medium-detail.glb');
const lowDetailModel = loadModel('low-detail.glb');
lod.addLevel(highDetailModel, 0);
lod.addLevel(mediumDetailModel, 10);
lod.addLevel(lowDetailModel, 20);
scene.add(lod);
另外还用了对象池来管理热点标记,避免频繁创建销毁DOM元素。GPU实例化也在考虑范围内,但这个改动较大,项目时间不够就没深入研究了。
内存泄漏也是个需要注意的问题,尤其是场景切换的时候。记得清理所有的事件监听器和定时器,否则容易造成内存堆积。
兼容性和部署问题
不同浏览器的支持程度还是有差异的,特别是Safari在WebGL 2.0支持上有些问题。为了兼容老版本浏览器,我还加了一些特性检测和降级方案。
function checkWebGLSupport() {
try {
const canvas = document.createElement('canvas');
return !!(window.WebGLRenderingContext &&
(canvas.getContext('webgl') || canvas.getContext('experimental-webgl')));
} catch (e) {
return false;
}
}
if (!checkWebGLSupport()) {
showFallbackUI(); // 显示备用的2D界面
}
回过头看还有不少遗憾
总的来说项目勉强交付了,客户也算满意。但仔细想想还是有不少可以优化的地方。比如阴影效果没做,光照计算也比较简单,整体质感还是差点意思。动画过渡也可以更平滑一些。
移动端的适配还有些小问题,某些安卓机型上偶尔会出现渲染异常。这个问题查了很久没找到根本原因,最后只能说是WebGL驱动的兼容性问题,暂时先放着了。
代码结构方面,交互逻辑和渲染逻辑耦合得比较严重,后期维护起来可能会比较困难。如果下次再做类似项目,肯定要重新设计一下架构。
以上是我踩坑后的总结,希望对你有帮助。Three.js确实功能强大,但要在实际项目中用好并不容易,需要平衡功能、性能和开发时间。

暂无评论