Three.js从入门到实战我踩过的那些坑和优化心得

♫丽敏 交互 阅读 1,616
赞 16 收藏
二维码
手机扫码查看
反馈

这个项目为什么要用Three.js

最近做了个产品展示页面,客户要求做那种3D的产品展示,能旋转、缩放、点击热点查看详情。本来想用CSS 3D Transform凑合一下,但想想还是太假了,交互也不够流畅。客户那边催得紧,时间有限,最后还是选择了Three.js。说实话,虽然之前也用过几次,但真正做商业项目还是第一次,心里还真有点发怵。

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确实功能强大,但要在实际项目中用好并不容易,需要平衡功能、性能和开发时间。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论