【Threejs进阶教程-优化篇】3. 卡顿与内存管理(二)

学习ThreeJS的捷径

本段内容会写在0篇以外所有的,本人所编写的Threejs教程中

对,学习ThreeJS有捷径
当你有哪个函数不懂的时候,第一时间去翻一翻文档
当你有哪个效果不会做的时候,第一时间去翻一翻所有的案例,也许就能找到你想要的效果
最重要的一点,就是,绝对不要怕问问题,越怕找找别人问题,你的问题就会被拖的越久

如果你确定要走WebGL/ThreeJS的开发者路线的话,以下行为可以让你更快的学习ThreeJS

  1. 没事就把所有的文档翻一遍,哪怕看不懂,也要留个印象,至少要知道Threejs有什么
  2. 没事多看看案例效果,当你记忆的案例效果足够多时,下次再遇到相似问题时,你就有可能第一时间来找对应的案例,能更快解决你自己的问题
  3. 上述案例不只是官网的案例,郭隆邦技术博客,跃焱邵隼,暮志未晚等站点均有不少优质案例,记得一并收藏
    http://www.yanhuangxueyuan.com/ 郭隆邦技术博客
    https://www.wellyyss.cn/ 跃焱邵隼
    http://www.wjceo.com/ 暮志未晚(暮老的站点暂时挂了,请查阅他之前的threejs相关文档)
    暮老的csdn首页
    这三个站点是我最常逛的站点,推荐各位有事没事逛一下,看看他们的案例和写法思路,绝对没坏处

烘培模型简介

我们以threejs官方开发包的文件举例,在开发包three/examples/models/obj/cerberus中,有这样一个OBJ格式的文件,这个模型也是之前在材质篇中使用到的模型

在这里插入图片描述
我们来查看一下文件大小在这里插入图片描述

在前面的灯光阴影材质篇,我们都有讲过,越好看越逼真的效果,都是建模师烘培出来的,用贴图贴出来的,上述模型,就是这样的模型,各位可以看一下这个模型实际贴出来的效果

在这里插入图片描述

Threeejs官方使用了这个模型的demo地址

这里的模型,笔者称为烘培模型

烘培模型的特点,一般是,模型效果非常逼真,模型与贴图的文件大小占比有明显的差距,且模型占比较小

如何优化烘培模型

有很多人刚好是遇到了烘培模型,但是用上一篇文章并不能解决卡顿问题,这里我们来介绍如何优化烘培模型

首先我们把模型加载进来,并贴好图
在这里插入图片描述

    function addMesh(){
        let loader = new OBJLoader();
        let textureLoader = new THREE.TextureLoader();//创建纹理加载器
        loader.load('./model/Cerberus.obj',obj=>{
            scene.add(obj);

            let map = textureLoader.load('./model/Cerberus_A.jpg'); //颜色贴图
            let normalMap = textureLoader.load('./model/Cerberus_N.jpg'); //法线贴图
            let roughnessMap = textureLoader.load('./model/Cerberus_R.jpg'); //粗糙度贴图
            let metalnessMap = textureLoader.load('./model/Cerberus_RM.jpg');//金属度贴图

            //处理贴图的常规操作
            map.wrapT = map.wrapS = THREE.RepeatWrapping;
            normalMap.wrapT = normalMap.wrapS = THREE.RepeatWrapping;
            roughnessMap.wrapT = roughnessMap.wrapS = THREE.RepeatWrapping;
            metalnessMap.wrapT = metalnessMap.wrapS = THREE.RepeatWrapping;

            //根据上面的所有贴图创建材质
            let material = new THREE.MeshStandardMaterial({
                transparent:true,
                side:THREE.DoubleSide,
                map:map,
                roughnessMap:roughnessMap,
                metalnessMap:metalnessMap,
                normalMap:normalMap
            });

            obj.traverse(object=>{
                if(object.isMesh){
                    let oldMaterial = object.material;
                    object.material = material;
                    oldMaterial.dispose();
                }
            })
        })
    }

在这里插入图片描述
因为本人是使用Chrome来编写文章的,所以避免干扰,本次我这里使用了Edge来统计内存,Edge本质上是套皮的Chrome,所以不会对结果有大的影响

在这里插入图片描述
稳定下来后,内存降低到了45976K

这个是我们初始的内存状态

贴图处理

我们先大概看一下5张贴图的分辨率
在这里插入图片描述
这里,我们对文件做一下区分,在文件夹下新建两个文件夹,1024和2048,拖动所有的图片到2048的文件夹下,并改写代码中的路径

在这里插入图片描述

            let map = textureLoader.load('./model/2048/Cerberus_A.jpg'); //颜色贴图
            let normalMap = textureLoader.load('./model/2048/Cerberus_N.jpg'); //法线贴图
            let roughnessMap = textureLoader.load('./model/2048/Cerberus_R.jpg'); //粗糙度贴图
            let metalnessMap = textureLoader.load('./model/2048/Cerberus_RM.jpg');//金属度贴图

这里的操作,需要你的电脑上有PhotoShop软件

  1. 使用photoShop打开贴图
  2. 文件-> 导出 -> 导出为
  3. 选择降低分辨率,png格式,同时选择八位色

在这里插入图片描述

在左侧我们可以看到图片处理后的大小,779.3kb,原文件位置810kb,大小也发生了改变,然后我们把导出的图片,全部保存到1024文件夹中

在这里插入图片描述
全部处理完后,我们看一下导出结果,文件总大小优化了1.3 M左右,后续加载速度也会有一定的提升

这里我们把代码修改为加载1024的图片,注意,格式已经从jpg变化为了png


            // let map = textureLoader.load('./model/2048/Cerberus_A.jpg'); //颜色贴图
            // let normalMap = textureLoader.load('./model/2048/Cerberus_N.jpg'); //法线贴图
            // let roughnessMap = textureLoader.load('./model/2048/Cerberus_R.jpg'); //粗糙度贴图
            // let metalnessMap = textureLoader.load('./model/2048/Cerberus_RM.jpg');//金属度贴图

            let map = textureLoader.load('./model/1024/Cerberus_A.png'); //颜色贴图
            let normalMap = textureLoader.load('./model/1024/Cerberus_N.png'); //法线贴图
            let roughnessMap = textureLoader.load('./model/1024/Cerberus_R.png'); //粗糙度贴图
            let metalnessMap = textureLoader.load('./model/2048/Cerberus_RM.png');//金属度贴图

此时我们再来看一下结果

在这里插入图片描述

这里我们可以看到,GPU出现了明显下降,而内存并没有发生大的变化

降低贴图分辨率,可以降低文件大小,增加一定的加载速度,同时可以降低一定的显存占用

对贴图的优化,对分辨率越高的贴图效果越好,比如说你的模型贴图分辨率原先是4096 * 4096,那么优化到2048,进一步优化到1024就会比较明显

但是,注意!优化贴图会让部分细节变得模糊!尽量保持贴图的最低分辨率为1024

在这里插入图片描述
上图左侧是使用1024贴图的,右侧是使用2048贴图的,我们在拉近的时候,可以看到明显的细节上的区别,所以优化贴图的技巧慎用!

贴图质量切换

如果你不能确定用户的设备水平,你可以追加这样一个设置,让用户自行选择分辨率

            let map,normalMap,roughnessMap,metalnessMap;

            let effect = "high"

            if(effect === "high"){
                map = textureLoader.load('./model/2048/Cerberus_A.jpg'); //颜色贴图
                normalMap = textureLoader.load('./model/2048/Cerberus_N.jpg'); //法线贴图
                roughnessMap = textureLoader.load('./model/2048/Cerberus_R.jpg'); //粗糙度贴图
                metalnessMap = textureLoader.load('./model/2048/Cerberus_RM.jpg');//金属度贴图
            }else{
                map = textureLoader.load('./model/1024/Cerberus_A.png'); //颜色贴图
                normalMap = textureLoader.load('./model/1024/Cerberus_N.png'); //法线贴图
                roughnessMap = textureLoader.load('./model/1024/Cerberus_R.png'); //粗糙度贴图
                metalnessMap = textureLoader.load('./model/2048/Cerberus_RM.png');//金属度贴图
            }

这样配置了之后,只需要我们修改 effect 为 “low”,就可以使用低分辨率的贴图,来保证用户能更好的跑起来效果,如果用户的设备配置足够,可以直接选择high来运行

小技巧:如果你的项目中涉及到分辨率的切换,可以使用gltf格式来加载模型,手动加载指定位置的贴图,前面讲过,gltf格式是由 gltf + bin + 贴图文件构成,这里我们就可以让建模师导出并处理多套贴图,放到对应的文件路径

为什么光源要限制数量

我们先写这样一个案例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        *{
            margin: 0;
            padding: 0;
            border: 0;
        }
        body{
            width:100vw;
            height: 100vh;
            overflow: hidden;
        }
    </style>
</head>
<body>

<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>

<script type="importmap">
			{
				"imports": {
					"three": "../three/build/three.module.js",
					"three/addons/": "../three/examples/jsm/"
				}
			}
		</script>

<script type="module">

    import * as THREE from "../three/build/three.module.js";
    import {OrbitControls} from "../three/examples/jsm/controls/OrbitControls.js";
    import Stats from "../three/examples/jsm/libs/stats.module.js";

    window.addEventListener('load',e=>{
        init();
        addRoom();
        addMesh();
        addLights();
        render();
    })

    let scene,renderer,camera;
    let orbit;
    let stats = new Stats();

    function init(){

        document.body.appendChild(stats.dom);

        scene = new THREE.Scene();
        renderer = new THREE.WebGLRenderer({
            antialias:true
        });
        renderer.setSize(window.innerWidth,window.innerHeight);
        document.body.appendChild(renderer.domElement);
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.PCFSoftShadowMap;

        camera = new THREE.PerspectiveCamera(50,window.innerWidth/window.innerHeight,0.1,2000);
        camera.position.set(100,100,100);

        orbit = new OrbitControls(camera,renderer.domElement);
        orbit.enableDamping = true;

        scene.add(new THREE.GridHelper(10,10));
    }

    function addRoom() {
        let geometry = new THREE.BoxGeometry(100,100,100);
        let material = new THREE.MeshStandardMaterial({
            color:"#ffffff",
            side:THREE.BackSide
        });
        let mesh = new THREE.Mesh(geometry,material);
        mesh.receiveShadow = true;
        scene.add(mesh);
    }

    function addMesh() {
        let count = 100;

        let geometry = new THREE.BoxGeometry(5,5,5);

        for(let i = 0;i< count;i++){
            let material = new THREE.MeshStandardMaterial({
                color:0xffffff * Math.random()
            });
            let mesh = new THREE.Mesh(geometry,material);
            scene.add(mesh);
            mesh.position.x = Math.random() * 100 - 50;
            mesh.position.y = Math.random() * 100 - 50;
            mesh.position.z = Math.random() * 100 - 50;
            mesh.receiveShadow = true;
            mesh.castShadow = true;
        }
    }

    function addLights() {
        let count = 1;
        for(let i = 0;i< count;i++){
            let pointLight = new THREE.PointLight(0xffffff * Math.random(), 1,1000,0.01);
            pointLight.position.x = Math.random() * 100 - 50;
            pointLight.position.y = Math.random() * 100 - 50;
            pointLight.position.z = Math.random() * 100 - 50;
            pointLight.castShadow = true;
            scene.add(pointLight);
        }
    }

    function render() {
        stats.update();
        renderer.render(scene,camera);
        orbit.update();
        requestAnimationFrame(render);
    }

</script>
</body>
</html>

在这里插入图片描述
运行效果是完全随机的,所以如果和我这里的效果不同不用太在意

我们先看一下,1个点光源时的内存情况和帧率情况
在这里插入图片描述
在这里插入图片描述
本人的屏幕是144hz的屏幕,所以这里显示的是144,帧率会随着屏幕刷新率不同而变化,我们只需要重点关注帧率变化即可

紧接着,我们增加到5个点光源,修改addLights() 中的count = 1 到 count = 5即可

在这里插入图片描述

在这里插入图片描述
可以看到,我们就只是增加了4个点光源,GPU就多了近10万K的消耗,因为本人的设备比较好,帧率暂时还没有出现波动,紧接着我们增加到10个点光源

本人的设备是I7-10750H,显卡RTX 2070Super,测试的结果会比较高,设备差的如果10个点光源卡崩了,就不要再盲目增加点光源了

在这里插入图片描述
10个点光源时,显存占用再次提升了10万左右,内存也跟着提升了近一倍,帧率依然保持坚挺,但是我们在打开网页时已经出现了明显卡顿

我们接着增加到15个点光源
在这里插入图片描述

我们增加到20个。。。但是场景变的一片漆黑,这个问题我们先挖个坑,后面再解决,只需要知道场景中点光源是有上限的即可

从几次的测试结果来看,GPU进程的内存,随着光源数量的增多而增加,GPU占用率随着光源数量增多而增多,也就是说,场景中的光源越多,对GPU的内存消耗就越大,GPU使用率越高

阴影质量的影响

我们在10个点光源的基础上,修改一下光源的阴影质量

    function addLights() {
        let count = 10;
        for(let i = 0;i< count;i++){
            let pointLight = new THREE.PointLight(0xffffff * Math.random(), 1,1000,0.01);
            pointLight.position.x = Math.random() * 100 - 50;
            pointLight.position.y = Math.random() * 100 - 50;
            pointLight.position.z = Math.random() * 100 - 50;
            pointLight.castShadow = true;
            pointLight.shadow.mapSize.set(1024,1024);
            scene.add(pointLight);
        }
    }

在这里插入图片描述

可以看到,显存发生了显著增长,我们此时把阴影质量提高到2048

在这里插入图片描述
显存已经提高了三倍多,帧率此时已经下降到了78帧,已经不能再跑到144帧了

我们此时减少5个点光源再看看结果
在这里插入图片描述

可以看到,阴影质量对显存的影响要远超光源数量对显存的影响

阴影本身也可以理解为是一种贴图

阴影最终会被渲染到材质上,阴影的分辨率越高,我们最终看到的阴影就会越柔和,阴影的实际效果,与你的场景比例有直接关系,

比如说,你是一个小房间,那么,此时,使用高精度阴影的效果就会比较好,

如果你的场景本身比例非常大,比如说园区模型,那么此时无论你怎么调整阴影的精度,最终打出来的效果都会差强人意

最终结论:适当使用光源和阴影,可以降低GPU内存消耗以及GPU使用率

最近更新

  1. TCP协议是安全的吗?

    2024-06-09 01:26:03       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-09 01:26:03       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-09 01:26:03       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-09 01:26:03       18 阅读

热门阅读

  1. Python代码——压缩整个文件夹

    2024-06-09 01:26:03       10 阅读
  2. rust结构体

    2024-06-09 01:26:03       7 阅读
  3. 最小二乘法-拟合平面方程

    2024-06-09 01:26:03       10 阅读
  4. 内网中redis无法连接访问问题

    2024-06-09 01:26:03       10 阅读