对于我正在写的游戏,我正在添加100,000棵树,每棵树都是合并的几何体.当我使用克隆模型添加这些时tree.clone()
,我通过这样做可以节省大量内存,但由于100k几何形状,游戏以3 FPS运行.
为了使游戏达到60 FPS,我需要将这些树合并为几个几何总体.但是,当我这样做时,由于使用了太多内存,chrome崩溃了.
合并这些树时极端内存使用的原因是什么?是因为我正在撤消使用该.clone()
功能的积极因素吗?
您需要查看实例化,您的用例正是为此而制作的.
或者,使用BufferGeometry
而不是常规几何,应该减少内存密集.
编辑
实际占用你的记忆的是合并操作的开销THREE.Geometry
.这样做的原因是,你必须分配一吨JS对象,如Vector3
,Vector2
,Face3
,等自原始几何体不存在了,然后被丢弃.这会强调一切,即使你没有遇到崩溃,你也会遇到从垃圾收集中慢下来的问题.缓冲区几何体更好用的原因是因为它使用的是类型化数组.对于初学者来说,所有浮动都是不是双打的浮点数,只复制基元,没有对象分配等.
你在gpu上占用更多的内存,因为你不是存储一个几何实例并在多个绘制调用中引用它,你现在在同一个缓冲区中保存N个实例(相同的数据只是重复,并进行预转换).这是实例化将有所帮助的地方.总之,你有:
网格(节点,对象)
描述"场景图"中的3d对象.它是否是某个其他节点的子节点,它是否有子节点,它的位置(平移),如何旋转,以及它是否缩放.这是THREE.Object3D类.从那里扩展的是THREE.Mesh,它引用了几何和材料,以及一些其他属性.
几何
保存几何数据(在建模程序中实际上称为"网格"),文件格式等因此模糊.在您的示例中,这将是一个"树":
描述叶子距离根有多远或者如何分割树干或树枝(顶点)
叶子或树干在哪里查找纹理(UVS),
它如何对光做出反应(显式法线,可选,但实际上有渲染叶子的方法有覆盖/修改/非常规法线)
重要的是要理解这个东西存在于"对象(模型)空间"中.假设建模者对此建模,这意味着他将对象指定为"垂直"(树干向上说Z轴,地面被认为是XY)从而给它初始旋转,他将根很好地置于0, 0,0因此给它初始翻译,默认我们可以假设比例部分是1,1,1.
这个树现在可以分散在3d场景中,无论是在建模程序中还是三个.js.让我们说我们将它导入像Blender这样的东西.它将位于世界的中心,0,0,0,旋转0,0,0,自身的比例为1,1,1.我们意识到建模者以英寸为单位工作,而我们的世界以米为单位,因此我们将它在所有三个方向上按一定的常数进行缩放.现在我们意识到树在地下,所以我们将它向上移动X个单位,直到它位于地形上.但是现在它经过一个房子,所以我们将它移到侧面,或者可能在所有三个方向,因为房子在山上,现在它就在我们想要的地方.我们现在观察到轮廓在美学上并不令人愉悦,因此我们围绕"垂直"轴旋转N度.
我们现在观察发生了什么.我们没有对树进行单一克隆,我们缩放它,移动它并旋转它.我们没有以任何方式修改几何体(添加树叶,树枝,删除内容,更改uv),它仍然是同一棵树.如果我们说几何是它自己的实体,我们有一个场景图节点,其TRS集.在three.js的上下文中,这是THREE.Mesh
(继承自Object3D
),有.rotation
(.scale
,position
翻译)和最后.geometry
(在你的情况下是"树").
现在让我们说我们需要一个新的森林树.该树实际上将是前一棵树的精确副本,但位于不同的位置(T),沿Z轴(R)旋转,并且非均匀地缩放(S).我们只需要一个具有不同TRS的新节点,让我们调用它tree_02
,它使用相同的几何体,让我们调用它treeGeometryOption_1
.由于树几何体具有一组定义的UVS,因此它也具有相应的纹理.纹理进入材质,材质描述属性,叶子有多光泽,树干有多沉闷,是否使用法线贴图,是否有颜色叠加等.
这意味着您可以使用某种TreeMasterMaterial
设置这些属性,然后treeOptionX_material
对应于地理测量.IE浏览器.如果叶片在某个范围内查找紫外线,那么纹理应该是绿色,并且更加有光泽,然后是树干查找的范围.
现在让我们重申整个过程.我们导入了初始树模型,并给它一些比例,旋转和位置.这是一个节点,附有几何图形.然后,我们制作了该节点的多个副本,这些副本都指向相同的几何体TreeOption1
.由于几何体是相同的,所有这些克隆可以具有相同的材料treeOption1_material
,该材料具有自己的纹理集.
这对于为什么克隆代码如下所示是一个超长的解释:
return new this.constructor( //a new instance of Mesh this.geometry , //with the sources geometry (reference) this.material //with the sources material (reference) ).copy(this) //utility to copy other properties per class (Points, Mesh...)
另一个答案是误导,使它听起来像:
return new this.constructor( //a new instance of Mesh this.geometry.clone() , //this would be devastating for memory this.material.clone() //this is actually sort of common to be done, but it would be done after the clone operation ).copy(this)
假设我们想要将树木染成不同的颜色,例如3.
var materialOptions = []; colorOptions.forEach( color =>{ var mOption = masterTreeMaterial.clone() //var mOption = myImportedTreeMesh.material.clone(); //lets say youve loaded the mesh with a material mOption.color.copy( color ); //generate three copies of the material with different color tints materialOptions.push( mOption ); }); scatterTrees( myImportedTreeMesh , materialOptions ); //where you would have something like var newTree = myImportedTreeMesh.clone(); //newTree has the same geometry, same material - the master one newTree.material = someMaterialOption; //assign it a different material //set the node TRS newTree.position.copy( somePosition ); newTree.rotation.copy( someRotation ); newTree.scale.copy( someScale );
现在发生的是,这会产生许多绘制调用.对于要绘制的每个树,需要发送一组低级指令来设置制服(TRS的矩阵),纹理(当绘制具有不同材料的树时)并且这产生开销.如果这些被组合并且绘制调用数减少,则开销减少,并且webgl可以处理变换许多顶点,因此可以以60fps数千次绘制低多边形对象,但是没有数千个绘制调用.
这是你的3 fps结果的原因.
除了花哨的优化之外,蛮力方式是将多个树合并为单个对象.如果我们将多个THREE.Mesh
节点合并为一个,我们只有一个TRS的空间.我们如何处理散落在地形上的数千棵树木,它们的TRS发生了什么?它们被烘焙成几何形状.
首先,每个节点现在都需要克隆几何体,因为几何体将被修改.第一步是将每个顶点乘以该节点的TRS矩阵.现在这是一棵树,它不是0,0,0,不再是英寸,而是相对于地形位于XYZ的某个地方,以米为单位.
在完成这一千次之后,需要将这千个单独的树几何合并为一个.它很简单,它只是制作一个新的几何体,并用这千个新几何体填充它的顶点,面和uv.您可以想象当这些数字很高时涉及的开销,JS有其局限性,GC可能很慢,而且这是很多数据,因为它是3d.
这应该回答标题中的问题.如果我们反过来,我们可以说你有一个消耗大量内存的游戏(通过一个模型 - 几何,一个"森林"),但运行速度为60fps.您使用克隆方法来节省内存,方法是将林分成单独的树,在每个根中提取TRS,并为每个节点使用单个树引用.
现在gpu只拥有一棵树的低多边形模型,而不是一个巨大的森林模型.记忆得救了.画出呼叫丰度.
FPS RIP.
两者如何减少绘制调用的数量,同时渲染树的模型,而不是森林?
通过使用称为实例化的功能.Webgl允许发出特殊的绘图调用.它使用属性缓冲区同时设置多个TRS信息.10000个节点将具有10000 TRS矩阵的缓冲区,这是16个浮点数,描述单个三角形没有uv或法线需要9,所以你可以看到它的去向.您在gpu上保存了一个树几何实例,而不是设置数千个绘制调用,而是设置一个包含所有这些数据的数据.如果对象是静态的,则开销很小,因为您一次设置了两个缓冲区.
Three.js用THREE.InstancedBufferGeometry(或类似的东西)很好地抽象了这个.
一棵树,很多节点:T内存N绘制调用
一个森林,单个节点:T*N内存1绘制调用
一棵树,多次实例化:T + N内存(有点,但它主要只是T)1个绘制调用
/编辑
100k是相当多的,三个可能比以前更好地处理它,但它曾用于在你有超过65536个顶点时使用你的fps.我不确定是不是最重要的,但它现在通过在内部将其分解为多个drawcalls或者webgl可以处理超过2 ^ 16个顶点的事实来解决.
通过这样做我节省了大量内存,但由于100k几何形状,游戏以3 FPS运行.
您仍然有一个几何体,您有100k"节点",它们都指向相同的几何实例.这是100k 绘制调用的开销正在减慢这种速度.
有很多方法可以给这只猫上皮.