Bootstrap

Three.js渲染较大的模型之解决方案

Three.js渲染较大的模型 解决方案

一、模型优化方面

  1. 简化模型结构

    • 对于复杂的3D模型,可以使用专业的3D建模软件(如Blender、Maya等)来减少模型的面数和顶点数。例如,在不影响模型整体外观的前提下,将高细节的装饰部分进行简化,将复杂的曲线用更简单的几何形状近似。
    • 利用建模软件中的减面工具,这些工具可以根据设定的参数,自动减少模型的多边形数量。比如将一个具有数百万个面的高精度角色模型,通过合理的减面操作,降低到数十万面,同时保留主要的外形特征。
  2. 使用LOD(Level of Detail)技术

    • LOD是一种根据物体与摄像机的距离来切换模型细节程度的技术。在Three.js中,可以创建多个具有不同细节层次的模型版本。
    • 例如,对于一个大型的建筑模型,当摄像机距离建筑较远时,使用一个低细节版本的模型,它可能只有简单的几何形状和较少的纹理;当摄像机靠近建筑时,切换到高细节版本的模型,显示更多的细节,如门窗的细节、建筑表面的装饰纹理等。通过这种方式,可以有效地减少远处模型的渲染负担。
    • 可以使用THREE.LOD对象来实现。首先创建不同细节层次的模型,然后将它们添加到THREE.LOD对象中,并设置相应的距离范围。例如:
    const lod = new THREE.LOD();
    const lowDetailModel = new THREE.Mesh(lowDetailGeometry, lowDetailMaterial);
    const mediumDetailModel = new THREE.Mesh(mediumDetailGeometry, 
    mediumDetailMaterial);
    const highDetailModel = new THREE.Mesh(highDetailGeometry, highDetailMaterial);
    lod.addLevel(lowDetailModel, 200); // 当距离大于200时显示低细节模型
    lod.addLevel(mediumDetailModel, 100); // 当距离小于200大于100时显示中细节模型
    lod.addLevel(highDetailModel, 0);   // 当距离小于100时显示高细节模型
    scene.add(lod);
    
  3. 压缩纹理

    • 对于模型的纹理,如果纹理文件较大,可以使用图像编辑软件(如Photoshop)或专门的纹理压缩工具来减小纹理文件的大小。
    • 例如,将高分辨率的纹理(如4096x4096像素)转换为更合适的分辨率(如2048x2048像素),同时采用合适的纹理压缩格式,如DXT(DirectX Texture Compression)或ETC(Ericsson Texture Compression)格式,这些格式可以在保持一定纹理质量的同时显著减小文件大小。在Three.js中,加载纹理时可以指定压缩后的纹理文件路径。

二、渲染优化方面

  1. 视锥体剔除(Frustum Culling)

    • Three.js会自动进行视锥体剔除,它的原理是只渲染位于摄像机视锥体内的物体。但是对于复杂的场景和大型模型,确保正确设置模型的包围盒(Bounding Box或Bounding Sphere)可以提高视锥体剔除的效率。
    • 例如,对于一个由多个部分组成的大型机械模型,为每个可分离的部分设置准确的包围盒,这样当某个部分完全在视锥体外时,Three.js可以快速地跳过对该部分的渲染。在模型加载或初始化阶段,可以通过计算模型的最小包围盒或包围球来实现更精确的视锥体剔除。
  2. 遮挡剔除(Occlusion Culling)

    • 遮挡剔除是指不渲染被其他物体完全遮挡的物体。在Three.js中,可以使用一些插件或自定义算法来实现遮挡剔除。
    • 一种简单的方法是基于深度缓冲来实现近似的遮挡剔除。首先渲染场景的深度信息,然后在后续渲染过程中,对于那些深度值大于当前像素深度的物体部分,可以认为是被遮挡的,从而不进行渲染。不过这种方法有一定的局限性,对于复杂的透明物体等情况可能需要更复杂的算法。
  3. 使用实例化(Instancing)技术

    • 如果模型中有大量重复的元素,例如森林中的树木、城市中的路灯等,使用实例化可以显著提高渲染效率。
    • 在Three.js中,可以使用THREE.InstancedMesh来实现实例化渲染。它允许使用一个单一的几何体和材质来渲染多个相同的物体实例。例如,要渲染一片森林,可以先创建一个树的几何体和材质,然后使用THREE.InstancedMesh来创建大量的树实例,通过设置每个实例的位置、旋转和缩放等变换矩阵,就可以高效地渲染整个森林。代码示例如下:
    const treeGeometry = new THREE.BoxGeometry(1, 2, 1);
    const treeMaterial = new THREE.MeshLambertMaterial({ color: 0x00ff00 });
    const treeInstances = new THREE.InstancedMesh(treeGeometry, treeMaterial,
     1000); // 1000个树实例
    for (let i = 0; i < 1000; i++) {
        const matrix = new THREE.Matrix4();
        matrix.setPosition(new THREE.Vector3(Math.random() * 100 - 50, 0, 
        Math.random() * 100 - 50));
        matrix.setRotationFromAxisAngle(new THREE.Vector3(0, 1, 0),
         Math.random() * 2 * Math.PI);
        matrix.scale(new THREE.Vector3(Math.random() * 0.5 
        + 0.5, Math.random() * 0.5 + 0.5, Math.random() * 0.5 + 0.5));
        treeInstances.setMatrixAt(i, matrix);
    }
    scene.add(treeInstances);
    
  4. 优化渲染循环(Render Loop)

    • 在渲染循环中,避免不必要的计算和渲染操作。例如,只有当模型的属性(如位置、旋转、材质等)发生变化时,才重新计算和渲染相关部分。
    • 可以使用节流(Throttle)或防抖(Debounce)技术来控制渲染频率。如果模型的动画更新频率不需要非常高,可以使用节流函数来限制每秒的渲染次数。例如,使用lodash库中的throttle函数来控制渲染循环的执行频率:
    import throttle from 'lodash/throttle';
    function render() {
        renderer.render(scene, camera);
    }
    const throttledRender = throttle(render, 1000 / 30); // 限制每秒渲染30次
    function animate() {
        // 模型更新等操作
        throttledRender();
        requestAnimationFrame(animate);
    }
    animate();
    

视锥体剔除实例 和 遮挡剔除 实例

视锥体剔除(Frustum Culling)实例

原理概述

视锥体剔除是指只渲染位于摄像机视锥体内的物体,Three.js本身内置了对视锥体剔除的基本支持,但我们可以通过合理设置物体的包围盒(Bounding Box或Bounding Sphere)来优化这个过程,提高剔除效率。

代码示例
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Frustum Culling Example</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
        }
    </style>
    <script src="
    https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js
    "></script>
</head>

<body>
    <script>
        // 创建场景
        const scene = new THREE.Scene();

        // 创建摄像机
        const camera = new THREE.PerspectiveCamera
        (75, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.z = 5;

        // 创建渲染器
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        // 创建多个立方体(模拟多个物体)
        const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
        const cubeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });

        const numCubes = 10;
        for (let i = 0; i < numCubes; i++) {
            const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
            cube.position.x = (Math.random() - 0.5) * 10;
            cube.position.y = (Math.random() - 0.5) * 10;
            cube.position.z = (Math.random() - 0.5) * 10;
            // 为每个立方体设置包围盒
            //(这里使用包围盒辅助对象可视化展示,实际应用中不需要可视化部分)
            const box = new THREE.Box3().setFromObject(cube);
            scene.add(cube);
        }

        // 渲染函数
        const render = () => {
            renderer.render(scene, camera);
        };

        // 动画循环
        const animate = () => {
            requestAnimationFrame(animate);
            // 模拟摄像机移动
            camera.position.x += 0.01;
            camera.position.y += 0.01;
            camera.lookAt(scene.position);
            render();
        };

        animate();
    </script>
</body>

</html>
解释
  1. 首先创建了一个基本的Three.js场景,包含了透视摄像机、渲染器等基础元素。
  2. 通过循环创建了多个立方体(模拟多个物体),并为每个立方体设置了随机的位置。
  3. 对于每个立方体,使用THREE.Box3().setFromObject(cube)创建了包围盒,这里额外添加了THREE.Box3Helper来可视化包围盒(在实际的应用中,如果不需要可视化展示,这一步只是为了内部计算包围盒范围,不需要添加这个辅助对象)。Three.js会基于这个包围盒自动进行视锥体剔除,在渲染时,只有处于摄像机视锥体内的立方体会被渲染(以及它们对应的可视化包围盒辅助对象,如果有的话)。
  4. 在动画循环中,模拟了摄像机的移动,随着摄像机位置变化,视锥体范围改变,视锥体剔除机制持续生效,决定哪些立方体被渲染出来。

遮挡剔除(Occlusion Culling)实例

原理概述

遮挡剔除是指不渲染被其他物体完全遮挡的物体,下面的示例是一种基于深度缓冲来实现近似遮挡剔除的简单方法。基本思路是先渲染场景的深度信息,然后在后续渲染过程中,对于那些深度值大于当前像素深度的物体部分,认为是被遮挡的,从而不进行渲染。不过这种方法对于复杂的透明物体等情况有一定局限性。

代码示例
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Occlusion Culling Example</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
        }
    </style>
    <script 
    src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"
    ></script>
</head>

<body>
    <script>
        // 创建场景
        const scene = new THREE.Scene();

        // 创建透视摄像机
        const camera = new THREE.PerspectiveCamera(75,
         window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.z = 5;

        // 创建渲染器
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        // 创建地板平面(作为遮挡物示例)
        const floorGeometry = new THREE.PlaneGeometry(10, 10);
        const floorMaterial = new THREE.MeshBasicMaterial({ color: 0xcccccc });
        const floor = new THREE.Mesh(floorGeometry, floorMaterial);
        floor.rotation.x = -Math.PI / 2;
        scene.add(floor);

        // 创建多个球体(模拟被遮挡物体)
        const sphereGeometry = new THREE.SphereGeometry(0.5, 32, 32);
        const sphereMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });

        const numSpheres = 5;
        for (let i = 0; i < numSpheres; i++) {
            const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
            sphere.position.x = (Math.random() - 0.5) * 5;
            sphere.position.y = (Math.random() * 0.5) + 0.5;
            sphere.position.z = (Math.random() - 0.5) * 5;
            scene.add(sphere);
        }

        // 渲染深度缓冲的渲染目标
        const depthRenderTarget = new THREE.WebGLRenderTarget(window.innerWidth,
         window.innerHeight);

        // 渲染深度信息的函数
        const renderDepth = () => {
            renderer.setRenderTarget(depthRenderTarget);
            renderer.clear();
            renderer.render(scene, camera);
            renderer.setRenderTarget(null);
        };

        // 正常渲染函数
        const renderScene = () => {
            renderer.render(scene, camera);
        };

        // 动画循环
        const animate = () => {
            requestAnimationFrame(animate);
            // 先渲染深度信息
            renderDepth();
            // 再渲染场景,此时可以基于深度信息进行简单的遮挡判断
            //(Three.js内部基于深度缓冲有一定的机制来辅助实现部分遮挡剔除效果)
            renderScene();
        };

        animate();
    </script>
</body>

</html>
解释
  1. 同样先构建了基础的Three.js场景,包含摄像机、渲染器等,并且创建了一个地板平面(作为遮挡物)以及多个球体(作为可能被遮挡的物体),设置了它们的位置和几何形状、材质等属性。
  2. 创建了一个WebGLRenderTarget对象作为渲染深度缓冲的目标,它的大小与窗口大小一致。
  3. 在动画循环中,首先调用renderDepth函数,这个函数将渲染器的目标设置为之前创建的深度渲染目标,先清空它,然后渲染整个场景到这个深度渲染目标中,这样就获取了场景的深度信息(实际上是深度缓冲的内容),之后再将渲染器的目标设置回默认(null,即渲染到屏幕上)。
  4. 接着调用renderScene函数正常渲染整个场景,此时Three.js会基于之前渲染得到的深度缓冲信息,在一定程度上自动进行一些简单的遮挡判断,不渲染那些深度值大于当前像素深度的物体部分(不过要注意这种方法对于复杂情况有局限性,比如透明物体的遮挡关系处理就比较复杂,还需要更复杂的算法来完善)。

three.js 模型压缩

  1. Draco在Three.js中的应用

    • Draco压缩原理
      • Draco是一种高效的3D数据压缩格式。它主要通过预测和量化技术来减少3D模型数据的大小。例如,对于模型的顶点位置和法向量等几何信息,Draco会分析其分布规律,利用相邻顶点之间的相关性进行预测编码。在量化过程中,它会将高精度的数值转换为较低精度的表示形式,从而大大减少数据量。
    • Three.js中的Draco加载流程
      • 设置加载器
        • 首先需要引入DRACOLoaderGLTFLoaderDRACOLoader用于处理Draco压缩的数据,GLTFLoader用于加载GLTF模型格式(因为Draco通常用于压缩GLTF模型)。
        import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
        import { DRACOLoader} from 'three/examples/jsm/loaders/DRACOLoader.js';
        
      • 配置解码器路径
        • 初始化DRACOLoader后,需要设置解码器路径。解码器可以从Google的官方服务器获取,也可以下载到本地使用。
        const dracoLoader = new DRACOLoader();
        dracoLoader.setDecoderPath
        ('https://www.gstatic.com/draco/v1/decoders/');
        
      • 关联加载器
        • DRACOLoader关联到GLTFLoader上,这样GLTFLoader就能识别和处理Draco压缩的GLTF模型。
        const gltfLoader = new GLTFLoader();
        gltfLoader.setDRACOLoader(dracoLoader);
        
      • 加载模型
        • 最后使用gltfLoader.load方法加载模型。这个方法接受模型文件路径、加载成功回调函数、加载进度回调函数和加载失败回调函数。
        gltfLoader.load('draco_compressed_model.gltf', function (gltf) {
            const model = gltf.scene;
            scene.add(model);
        }, function (xhr) {
            console.log((xhr.loaded / xhr.total * 100) + '% loaded');
        }, function (error) {
            console.log('An error occurred', error);
        });
        
  2. gltf - pipeline

    • gltf - pipeline概述
      • gltf - pipeline是一个用于处理GLTF模型的工具集。它允许开发者在模型加载前或加载后对GLTF模型进行各种优化操作,如压缩、转换、添加或删除特定的模型属性等。
    • 主要功能和应用场景
      • 模型压缩
        • 可以使用gltf - pipeline进一步压缩已经是GLTF格式的模型。例如,通过优化纹理、量化顶点数据等方式,在Draco压缩的基础上进一步减小模型文件大小。它可以将模型的几何数据、纹理数据等进行更精细的处理,以达到更好的压缩效果。
      • 模型转换和优化
        • 有时候,原始的GLTF模型可能包含一些不必要的信息或者不符合特定的渲染要求。gltf - pipeline可以用于转换模型,比如将模型的坐标系进行调整,或者将模型的材质属性进行统一优化。例如,将多个不同类型的材质转换为更适合WebGL渲染的材质类型,提高渲染效率。
    • 使用示例(命令行工具)
      • 安装
        • 首先需要在项目目录下通过npm安装gltf - pipeline。
        npm install -g gltf-pipeline
        
      • 基本用法 - 模型压缩
        • 假设要压缩一个GLTF模型,可以使用以下命令。这个命令会对输入的模型进行一系列优化处理,包括几何数据和纹理数据的优化,然后输出一个新的压缩后的模型。
        gltf-pipeline -i input_model.gltf -o output_model.gltf -d
        
        • 其中-i指定输入模型路径,-o指定输出模型路径,-d表示进行Draco压缩(如果已经是Draco压缩的模型,这个选项可能会进一步优化)。
    • 在JavaScript代码中集成(高级用法)
      • 安装依赖
        • 除了安装gltf - pipeline,还需要安装相关的JavaScript库,如@gltf - pipeline/functions等。
        npm install @gltf-pipeline/functions
        
      • 代码示例 - 模型优化
        • 以下代码片段展示了如何在JavaScript代码中使用gltf - pipeline的函数来优化模型。
        const { optimize } = require('@gltf-pipeline/functions');
        const fs = require('fs');
        const gltfPipeline = require('gltf-pipeline');
        const { GLTF } = gltfPipeline;
        // 读取GLTF模型文件
        const inputGltf = JSON.parse(fs.readFileSync('input_model.gltf'));
        const options = {
            dracoOptions: {
                compressionLevel: 7
            }
        };
        // 对模型进行优化
        optimize(inputGltf, options).then((optimizedGltf) => {
            // 将优化后的模型保存为新的文件
            fs.writeFileSync('optimized_model.gltf', 
            JSON.stringify(optimizedGltf));
        });
        
        • 这段代码首先读取一个GLTF模型文件,然后设置优化选项(这里包括Draco压缩的级别),接着使用optimize函数对模型进行优化,最后将优化后的模型保存为一个新的文件。
;