admin管理员组

文章数量:1580455

点线面统计与内存管理

  • 新人菜鸟对模型方面的常见误区(持续更新)
  • 数据量统计
    • 点面法线UV统计
  • 提问:我该如何创建10万个不同颜色不同位置的盒子(Box)
    • 元素数量对内存的影响
    • 优化geometry和material数量
    • 合并几何体
    • instancedMesh
    • LOD
  • TA之路

新人菜鸟对模型方面的常见误区(持续更新)

Q : 我文件只有几M,已经压缩过了,非常小了,为什么加载还是卡?
A1 : 文件大小与占用内存一丁点关系都没有,只影响你加载出来模型的时间,尤其是在web端,你从后端要传到前端,自然是文件越小越好,glb格式的draco压缩,仅能改变文件大小,不能改变你模型的点线面数,更不可能降低你在运行时占用的内存
A2 : 卡之前先看看自己的电脑,如果是台式,保证台式机有独立显卡,且至少是GTX 960,RX580以上的级别,500块钱都舍不得花的人,不建议做WebGL,如果是笔记本,没有独立显卡的笔记本直接pass,换有显卡的电脑吧,部分笔记本会设置浏览器使用cpu渲染,cpu玩命渲染在叫苦连天,gpu一旁闲着看着火急火燎但是浏览器不用它,如果遇到笔记本有独立显卡,且至少在GTX 1050以上的水平,依然卡,建议检查一下是否浏览器使用GPU渲染


Q : 我模型也不大啊,也就场景里加载了几百个,怎么开始卡了
A : 同一个模型不要重复加载,尽量用clone去加载,后续我会在下面讲解clone的优缺点

	loader.load('./model.gltf',obj=>{
		let model = obj.scene;
		for(let i = 0;i< 100;i++){
			//clone函数,复制一份当前的模型
			let clone = model.clone();
			//todo 这里写对克隆模型的操作代码
			scene.add(clone);
		}
	})

Q : 什么模型格式最好 / 哪种格式最好用 / 哪种格式比较小
A : 没有最好,最好用和最小,模型的质量 + 兼容性最好的格式 = 优质模型,通常情况下,glb是兼容性最好的格式,尽量使用Blender来导出,但是最新版的3dmax,maya,c4d已经具备了gltf格式的导出,可以尝试去使用这些软件导出的gltf和glb格式
虽然说没有最好,但是我想说,珍爱生命,远离FBX,重点要远离这样的FBX格式文件:1. 2017版本之前的任何建模软件导出的FBX格式,2. 所有,网上,免费下载的FBX格式,3. 超过50M的FBX格式文件

数据量统计

点面法线UV统计

	    function addMesh() {
        let geometry = new THREE.BoxGeometry(1,1,1);
        // let geometry = new THREE.BoxGeometry(1,1,1,10,10,10);
        // let geometry = new THREE.BoxGeometry(1,1,1,100,100,100);
        // let geometry = new THREE.BoxGeometry(1,1,1,1000,1000,1000);
        let material = new THREE.MeshBasicMaterial({color:0xffffff * Math.random()});
        let mesh = new THREE.Mesh(geometry,material);
        scene.add(mesh);

        console.log(geometry.attributes);
    }

我们写如上的这一段代码,然后,我们去控制台看看结果


我们可以发现,给盒子添加分段数的函数,可以增加三角面数,每次增加10倍,面数都会指数级增加,当我们分段数达到1000的时候,面数达到了恐怖的1803万,并且浏览器已经报出警告,说本次渲染时间达到了1957ms,已经很明白的告诉你要优化程序了

我们可以来算一下数据,就粗略的计算一下上述四种结果,仅点,法线,uv三个数组占用了多少内存

这里js使用了32位float来存储,占用4个字节,所以我们就这样算

分段数为1的盒子,面数为72/3 = 24,占用 (72 + 72 + 48) * 4 = 768字节
分段数位10的盒子,面数为2178 / 3 = 726,占用( 2178 + 2178 + 1452 ) * 4 / 1024 = 22.68k字节
分段数位100的盒子,面数为 183618/3 = 61206,占用 (183618 + 183618 + 122412 ) * 4 / (1024 * 1024) = 1.86M 字节
分段数位1000的盒子,面数为 18036018 /3 = 6012006,占用 (18036018 + 18036018 + 12024012) * 4 / (1024 * 1024) = 183.47M字节

180万面的模型,仅纸面数据就占了183.47M的内存,我们来看看浏览器的任务管理器

在浏览器中按下shift + esc来对比一下四次结果的不同内存占用

截图的时间,都是在按下刷新键的第五秒的时间,基本可以看出,threejs的程序就占了约70M的内存,而分段数1,10并没有多大差别,到了100时,差距达到了好几倍,到1000时,内存占用直接蹦到了1.25G

但是并不是说就一直稳定1.25G的内存占用,在程序稳定后,占用会降到如上图水平

虽然我们叫它模型,但是,模型也是一组数据,如果我们的数据量过于庞大,那么内存占用会随着模型增大而增加,笔者的机子是i7-10750,显卡是RTX 2070S,加载这个180万面都已经出现了帧率警告,更别提用没有显卡或者老机子来加载模型了

所以在建模之前,一定要考虑清楚用户群使用的设备的水平,不能盲目的增加模型的点线面,如果你拿到的模型,就是超过百万面的模型,那么要么要求使用你开发的threejs的程序的电脑,能达到加载100万面的模型不卡的水平,要么就对模型进行优化,减减面,删删细节即可

个人建议,任何的threejs项目,单个场景的所有模型总面数不要超过100万,最低可承受的设备水平为:GTX 1050

提问:我该如何创建10万个不同颜色不同位置的盒子(Box)

元素数量对内存的影响

我们进行下一个案例,本次我们来对比,同时加载10个和10万个box的内存占用

我们在场景中,创建10个和10万个不同颜色不同位置的盒子


        let count = 10;
        // let count = 100000;

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


同时我们计算一下10万个盒子的点线面数
这次不仅渲染压力过大了,而且,出现了严重的掉帧情况,场景卡的异常,只有把相机对准盒子不密集的地方,才不怎么卡

一个盒子,面数为72/3 = 24,占用 (72 + 72 + 48) * 4 = 768字节
10万个盒子,768 * 100000 / (1024 * 1024) = 73.24M 字节

10万个盒子,比点线面数要低于上面的180万面的一个盒子,但是卡顿感已经超越了上面180万面的盒子

所以,有时候,控制元素数量,可以有效的降低场景的卡顿
单个场景建议元素数量 小于1000

部分建模软件,导出模型时,会无止境的累加组,一个物体可能要套个好几层的组,或者一堵墙上的每一块砖都创建一个元素,这样会导致,你的模型本来面数不多,但是元素几万几十万,一样卡的要死,如果你拿到的模型,是这些软件导出来的,请交给建模师处理后再使用

部分建模新人,会使用最基础的几何体,“堆模型”,你打开了之后,发现他的模型,细致看下去,全是一堆的圆柱,圆锥,盒子,球体堆出来的,远看是那么回事,近看一丁点技术含量都没有,遇到这种模型的,建议把建模的揍一顿(开个玩笑),这种模型一般元素数量会逆天的多,面数也少不了,这种模型明摆着就不是给程序用的,祝大家不要遇到这种模型为好

点名3D66网站的免费模型,笔者遇到的上述元素数量和面数均远超常规的模型,很大概率会在模型的某个地方,看到3d66字样,比如说模型的命名,贴图文件的命名,模型内部的元素等等,这里的免费模型是免费模型的重灾区,低质量模型遍地走,与其处理这些垃圾模型,还不如让建模师照着样子重做一个

优化geometry和material数量

刚刚我们说了,我们在场景中,创建10个和10万个不同颜色不同位置的盒子
我们可以用共享geometry和material的方式来对上述程序进行优化

	        // let count = 10;
        let count = 100000;

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


上述代码中,我们一共只创建了一个boxGeometry,所以我们减少了99999个geometry对象占用的内存,内存直接恢复到了35万,比起上面的64万,几乎优化了一倍

不仅geometry可以共享,材质也可以共享

        // let count = 10;
        let count = 100000;

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

        let materials = [
            new THREE.MeshBasicMaterial({color:0xffffff * Math.random()}),
            new THREE.MeshBasicMaterial({color:0xffffff * Math.random()}),
            new THREE.MeshBasicMaterial({color:0xffffff * Math.random()}),
            //你想创建几种随机的颜色,就多new几行
        ]


        for(let i = 0;i < count; i ++ ){
            let material = materials[Math.round(Math.random() * 10)];
            let mesh = new THREE.Mesh(geometry,material);
            mesh.position.x = Math.random() * 100 - 50;
            mesh.position.y = Math.random() * 100 - 50;
            mesh.position.z = Math.random() * 100 - 50;
            scene.add(mesh);
        }


可以看出,我们的内存占用再一次降低了,因为我们少创建了99997个材质

但是我们的帧率依然没变,这还是因为10万个元素实在是太多了

这里还要提一嘴clone

	   let geometry = new THREE.BoxGeometry(1,1,1);
       let material = new THREE.MeshBasicMaterial({color:0xffffff * Math.random()});
       let mesh = new THREE.Mesh(geometry,material);
	
		for(let i = 0;i< 100000;i ++ ){
			let clone = mesh.clone();
			// 给克隆的元素赋予随机颜色
			// clone.material = clone.material.clone();
			// clone.material.color = new THREE.Color(0xffffff * Math.random());
            clone.position.x = Math.random() * 100 - 50;
            clone.position.y = Math.random() * 100 - 50;
            clone.position.z = Math.random() * 100 - 50;
			scene.add(clone);
		}
	

clone的本质,是重新创建了一个Mesh,作用与上述两段代码基本一致

合并几何体

既然我们始终无法解决元素数量过多的问题,那么我们考虑一种新的办法,合并geometry

我们的要求依然是,场景中存在10万个不同颜色的box

	//外部引入
    import {mergeGeometries} from "../three/examples/jsm/utils/BufferGeometryUtils.js";

	    for(let i = 0;i< 1000;i++){
            let geometries = [];
            for(let i = 0;i< 100;i++){
                let boxGeometry = new THREE.BoxGeometry(1,1,1);
                boxGeometry.translate(
                    Math.random() * 100 - 50,
                    Math.random() * 100 - 50,
                    Math.random() * 100 - 50,
                );
                geometries.push(boxGeometry);
            }
            let geometry =  mergeGeometries(geometries);
            let material = new THREE.MeshBasicMaterial({color:0xffffff * Math.random()});
            let mesh = new THREE.Mesh(geometry,material);
            scene.add(mesh);
        }


我们这次的内存达到了17万,更近了一步,我们来聊聊这个函数可以做什么

mergeGeometries( [ geometry ] )
这个函数会把数组中所有的geometry的数据合并,也就是上面提到的 uv,position,normal这些数据合并

所以我们合并了100个之后,我们只需要创建1000个元素,就可以保证场景中有10万个盒子了,但是,本次操作中,我们增加了999个geometry,增加了997个材质,但是减少了990000个Mesh,这样我们的内存就顺理成章的降低了

同时,本次优化后,也再不卡顿了

合并元素,笔者仅建议在建模中完成,如果你的模型存在巨量元素,超过10万个,那么只建议在建模软件中完成

为什么不推荐程序来完成,首先我们要走一轮扩容数组的过程,有多少元素要执行多少次 * 4次,所以我们不可能每次运行程序前,先让程序花大量时间去合并几何体,所以合并几何体的工作,仅建议在建模软件中来完成,能交给建模师就尽量交给建模师

instancedMesh

有些人居然还觉得不够,还是觉得卡,毕竟优化完还有240万面,有好多人的机子依然带不起来,怎么办?

记得,这个是底牌,如果底牌都亮了,还是无法优化卡顿,那么赶紧,安排,让建模师,把模型优化下来!!!!!!!!

	    let count = 100000;

        let geometry = new THREE.BoxGeometry(1,1,1);
        let material = new THREE.MeshBasicMaterial({
            //注意,使用instancedMesh,如果要用随机色,这里默认色要设置为白色
            //如果你比较好奇为什么设置白色,你可以设置成任意颜色来查看结果即可
            color:0xffffff
        });
        let instancedMesh = new THREE.InstancedMesh(geometry,material,count);

		//instancedMesh想改变位置需要用特殊的方式,需要借助矩阵来改变
        let matrix = new THREE.Matrix4();

        for(let i = 0;i<count;i++){
            matrix.setPosition(
                Math.random() * 100 - 50,
                Math.random() * 100 - 50,
                Math.random() * 100 - 50,
            )
            //官方提供了设置物体矩阵的方式
            instancedMesh.setMatrixAt(i,matrix);
            //官方提供的设置物体颜色的方式
            instancedMesh.setColorAt(i,new THREE.Color(0xffffff * Math.random()))
        }
        scene.add(instancedMesh);


这次的内存管理还有人不满意吗?
现在,整个场景中,只有一个geometry,一个material,一个元素,但是,却绘制出了10万个盒子,而且,整个场景中的面数,只有24,对,你没看错!只有24,一个盒子的面数

instancedMesh,是一种特殊的mesh,用来解决大量同geometry和material物体的一种解决方案,比如说,仓库模型,要放置几万个包裹,或者,你的其中一个模型面数很多,但是要放很多个出来,也可以使用这个技术

这个东西的原理笔者暂时讲不清,只需了解这个东西可以用来优化场景即可

LOD

如果建模师已经把你逼到用instancedMesh了,且还不能解决卡顿问题。。。

如果我是开发者,我已经不知道揍了这个建模师多少次了。。。让你优化个面数跟要你死一样,啊?你忙的要死,没空啊?不好意思不好意思,我错了

	    let lodGeometries = [
            [ new THREE.BoxGeometry( 1,1,1,30,30,30 ), 2 ],//距离越近,就使用最精致的这个模型
            [ new THREE.BoxGeometry( 1,1,1,15,15,15), 4 ],
            [ new THREE.BoxGeometry( 1,1,1,10,10,10 ), 6 ],
            [ new THREE.BoxGeometry( 1,1,1,3,3,3 ), 8 ],
            [ new THREE.BoxGeometry( 1,1,1 ), 10 ] //距离越远,就使用越粗糙的模型
        ]

        let material = new THREE.MeshBasicMaterial({
            color:0xffffff * Math.random(),
            wireframe:true,//开启线框模式来观察物体变化
        });
        let lod = new THREE.LOD();
        for(let i = 0;i< lodGeometries.length;i++){
           let mesh = new THREE.Mesh(lodGeometries[i][0],material);
            lod.addLevel(mesh,lodGeometries[i][1]);
        }
        scene.add(lod);

如gif图所示,我们的box分段数实际是30 * 30 * 30,但是因为设置了lod,所以在距离box超过10个单位时,box就变成了分段数1 * 1 * 1的盒子

LOD的主要功能,是创立好几个级别的geometry,根据相机的距离,做精度替换,尤其是针对需要近距离时高精度,远距离需要低精度的情况下,适合使用LOD来优化场景

TA之路

如果你的建模师,连lod模型都不想给你做。。。那没办法,自己上了

首先,你得会至少一个建模软件,作为开发者,我个人推荐Blender > maya = 3dmax = c4d,根据自己的情况酌情学习

Blender是免费开源,优质的建模软件,而且作为最早支持gltf格式,也颇受threejs开发者青睐

maya是最近在建模师群体中耳熟能详最好用的建模软件,具体笔者无细致了解,但是两位优秀的建模师共同推荐了maya

3dmax,拥有极广泛用户群体,哪怕是放现在,3dmax依然是很主流的建模软件,只不过就是非常卡而已,而且3dmax与vray的融合,也可以让3dmax做出非常优秀的烘培效果

c4d,精准的德国工艺,此软件由笔者身边两位优秀的设计推荐,据说2022版本已经支持gltf/glb格式,笔者并无过多了解,请自行了解

但是,无论你走哪一条建模路线,你都至少得会一个,你也至少得会一个处理图片的软件,最常用的Photoshop,当你这两个都入门了之后,你距离TA的路线就不远了

TA,技术美术,既懂技术又懂美术的职位的简称,一般技术美术都是高级职业,出现在游戏领域更多,但是,在现在WebGL项目越来越普及的时代,WebGL已经逐渐标准化,技术也逐渐都成型,本质上,除了渲染引擎用Unity和UE以外,在美术方面,高级的WebGL开发者,要会的软件,和标准TA没有多大区别了,而且,也确实有很多搞Threejs搞着搞着去搞Unity了,这也是很常见的事情,因为除了换了一下环境外,材质纹理几何体,几乎所有的概念都通用

所以,当你做久了之后,你会发现,你距离TA的道路,越来越近了

本文标签: 进阶内存管理教程ThreeJs