如何用Cocos引擎打造次世代3D画质‘游戏大观

如何用Cocos引擎打造次世代3D画质‘游戏大观,第1张

从Cocos 2d-x 3.0起我们已经可以在游戏中使用3D元素。Cocos引擎推出3D功能的时间不算太迟,我们已经可以看到越来越多的手机上能流畅地渲染3D游戏,而且这些机型正在成为主流。在最近两年我们可以看到,高端手机游戏从2D转到3D的倾向很明显。许多游戏开发商试图在竞争激烈的红海里占有一席之地,那么选择开发3D游戏或许会是一个强有力的竞争手段。

上面的视频是我的下一款游戏作品《Food of the Gods》。这游戏使用了Cocos 2d-x 3.3,视频是从我iPhone上录制的实际运行效果。在这篇文章里我将要介绍我是如何制作它、如何把它跑在cocos引擎上的。对于熟悉cocos官方提供的3D示例游戏 《Fantasy Warrior》的开发者,将会看到以下一些主要不同点:

1. 光照贴图(Light Mapping):你将看到每件物体都有被照亮并且投射阴影。光影效果的质量是由你的3D工具软件决定的,用3D软件能烘焙出复杂的光效,包括直接光照,反射光照,以及阴影。

2. 顶点合并(Vertex Blending):请注意看路、草地和悬崖交接的地方,看不到任何可见的接缝。

3. 透明遮罩(Alpha Masks):灌木如果没有透明遮罩就跟纸片一样。

4. 滤色叠加的公告板(Billboards):增加一些光束和其他环境的效果。

所有的模型都是用一个叫Modo的3D 软件建模制作的,贴图则是使用Photoshop。关于3D模型的制作和贴图的绘制在此就不再赘述,网上已经有很多教程,在此主要介绍下跟Cocos 2d-x有关的部分。

模型网格和贴图(Meshes and Textures)

如下图所示,每个模型的贴图都是由几个256 x 256或者更小的贴图组成的。同时你也会注意到我把所有的小图片都合在了一张贴图上,这是减少GPU绘制次数(draw call)最简单的方法之一。贴图是从http://www.cgtextures.com 或者网上找的。

为了把这些图片拼接起来,我使用的是Photoshop的补偿滤镜(offset filter)然后在接缝的地方用修复画笔来做一些自然的过渡。为了获得一种油画的视觉效果我会先使用cutout滤镜(注意:cutout滤镜也会使得png格式图片的压缩效果更好),然后在需要的地方绘制一些高光和阴影的效果。我发现如果直接拿照片当贴图的话,当你把它尺寸缩小的时候会出现图像噪点。

另一种方案是为每一个模型网格制作一整张独立的贴图。当网格比较小或者摄像机不是很靠近网格的时候这种方法是可行的。如果你的photoshop技术过硬的话,出来的效果会更好。附带的好处是,因为只使用一张贴图因此只有一次GPU绘制调用。但我不建议采用这种方法来制作第一人称射击游戏(FPS)中的建筑,因为当你走得很靠近建筑物的时候,贴图分辨率过低的问题就会显露出来。我不喜欢用这种整张贴图方法,因为这实在太费时耗力了。这个场景的制作花了我足足四天时间。

光照贴图(Light Maps)

当你做好模型和贴图之后,现在就可以来烘焙光照贴图了。Cocos 2d-x目前还不像Unreal或Unity一样在官方编辑器里提供烘焙光照贴图的功能,但是别失望,大部分的制作3D模型的软件都可以烘焙光照贴图,并且效果比市面上任何游戏引擎的效果还好。首先,在你的3D工具软件里,先给场景打好灯光,照亮场景,然后为每份网格制作第二张UV map。每份网格的表面都必须被映射在0到1范围内的UV 平面上。这听起来好像很复杂且耗时,但在Modo里这是非常简单的。我先后使用 “Atlas map”的UV 工具和“Pack UV”工具,这两个工具会自动将网格展开成一个相当不错的排布图。

这些都完成之后,设置3D工具软件的渲染器为“只渲染烘焙的光照”,然后开始渲染。当然了,如果你想做一些环境光遮罩的效果也是可以的。

你也可以使用一些分辨率较低的光照贴图。有时候这样的效果反而会看起来更好,因为相互混叠的模糊像素会让阴影看起来更柔和。上面的这些建筑都映射到一张512 x 512的光照贴图上。整个场景总共使用了4 张512 x 512的光照贴图。请确保每个小图块之间有一定的空隙,且让你的渲染范围比这些图块的边界多出几个像素。这样可以防止当较低的mip-maps(一种纹理采样)起作用时黑边出现在网格周围的角落里。

最后一点听起来像是3D技术的行话。如果是对Texture Packer熟悉的话,那么其中的“Extrude”值起到的作用就是刚刚我所描述的。对贴图的边缘接缝做一些涂抹处理,这样在精灵之间就不会有那些烦人的缝隙了,那些缝隙在这里会变成多边形边缘的黑边。

如果你想牺牲内存和包大小来提高性能的话,你可以把颜色和光照信息都烘焙到一张贴图上并避免共同使用一张光照贴图。但是这样做的话,同样的像素密度,贴图的大小至少得翻一倍。这完全取决于你个人、以及你游戏的要求。

接下来,添加顶点颜色。我在地形上提供了顶点颜色,这可以让着色器在合成悬崖顶上的草地贴图时,不会有任何可见的接缝。下图中涂成白色的顶点部分可以合成你指定的贴图。在这个例子里实际上我只使用红色通道,当然了根据实际需要你可以使用4个通道(RGBA)去合成不同的贴图。

最后,我把整个场景分成了很多独立的网格(mesh):每个建筑都有自己独立的网格,地形独立一个网格,水也是独立一个。带透明遮罩的贴图也会有一个网格——比如视频中看到的植物叶子和小旗子。我这样做有两个原因,首先,让地形、建筑、水和带透明遮罩的贴图各自使用不同的着色器。其次,我们打算通过不渲染摄像机范围外的对象来减少性能开支。很重要的一点是摄像机会根据网格的包围盒来决定对象是否可见,因此尽量把网格弄成小块,这样包围盒会比较小。

导出

完成了模型和贴图之后,我们需要把每个mesh导出为一个.fbx文件。幸运的是,大多数的3D建模软件都支持这个功能。Autodesk为此格式提供了一个免费SDK。但不幸的是,Modo 701在导出fbx格式时会出现相当多的错误。因此我必须自己写一些脚本来保证第二组贴图坐标和顶点颜色的正确导出。你可以从我个人网站上的“Modo Scripts”部分下载这个导出脚本。搞定fbx之后,你将需要用到Cocos 2d-x自带的fbx-conv.exe命令行工具,它位于Cocos 2d-x根目录的/tools下。

fbx-conv.exe -a your_mesh_name_here.fbx

使用“-a”参数后,工具会同时导出mesh的二进制文件(.c3b)和文本格式文件(.c3t)。文本格式的文件非常的有用,你可以利用它来查看所有的东西是否被正确导出,但千万不要把它放到resource目录下。如果所有的都被正确地导出的话,你将在c3t文件的开头看到以下的内容:

“attributes”: [{

“size”: 3,

“type”: “GL_FLOAT”,

“attribute”: “VERTEX_ATTRIB_POSITION”

}, {

“size”: 3,

“type”: “GL_FLOAT”,

“attribute”: “VERTEX_ATTRIB_NORMAL”

}, {

“size”: 2,

“type”: “GL_FLOAT”,

“attribute”: “VERTEX_ATTRIB_TEX_COORD”

}, {

“size”: 2,

“type”: “GL_FLOAT”,

“attribute”: “VERTEX_ATTRIB_TEX_COORD1″

}]

注意VERTEX_ATTRIB_TEX_COORD1这个属性。如果没有它光照贴图将无法显示。如果你导出了一张带顶点颜色的mesh,你也应该要看到一个类似的属性才行。还有一点很重要,贴图的坐标也必须按正确的顺序才行。我通常采用的是第一个tex_coord是瓦片贴图,最后一个tex_coord是光照贴图。使用Modo的话,uv maps会按照字母顺序排列。

着色器(Shaders)

我花了很长的一段时间来搞懂GLSL和着色器,但正如编程中经常遇到的,有时候一个点通了,其他的就都好理解了。一旦理解了其中的原理,你便会发现着色器真的很简单。如果你不只是想用Cocos 2d-x来把贴图套到模型网格上的话,你需要学会如何写着色器。目前Cocos 2d-x没有Unreal那样好用的着色器可视化编辑器(visual shader editor),所以我们只能自己动手焊代码。

本节我将讲解我为视频中的游戏场景所写的着色器,并说明我做了什么、为什么这样做。如果你对着色器已经非常熟悉了,那么可以快速跳过本节。

首先,先来看一下如何将着色器应用到模型网格上。

这段代码摘自Cocos 2d-x的测试集cpp-tests工程。如果你用不同的着色器来加载大量的meshes,那么最好根据功能来进行,这样可以避免冗余。那么现在我们只关心如下的代码段,来看下这个着色器。

GLProgram* shader =GLProgram::createWithFilenames(“shaders/lightmap1.vert”,”shaders/lightmap2.frag”)

GLProgramState* state = GLProgramState::create(shader)

mesh->setGLProgramState(state)

Texture2D* lightmap =Director::getInstance()->getTextureCache()->addImage(“lightmap.png”)

state->setUniformTexture(“lightmap”,lightmap)

“lightmap1.vert”是顶点着色器(vertex shader)。如果将其应用到网格上,那么每个顶点的每一帧都将执行这个 *** 作。而“lightmap2.frag”是片段着色器(fragment shader),网格上贴图的每个像素的每一帧都将执行这个 *** 作。我不太确定为什么将其命名为“片段着色器”,我一直认为应叫做“像素”着色器(pixel shader)。从这段描述,我们可以很容易理解为什么大量着色器指令会降低帧率,尤其是你用片段着色器的话。

接下来我们详细地分解顶点着色器:

attribute vec4 a_position

attribute vec2 a_texCoord

attribute vec2 a_texCoord1

这些属性是由渲染器提供的。“a_position”是顶点的位置。“a_texCoord” 和 “a_texCoord1”对应你那两个UV坐标。还记得在.cbt文本格式文件中开头部分的“VERTEX_ATTRIB_TEX_COORD”么?这些值与属性对应起来了。你可以在渲染器中获取更多其他的属性,包括顶点法线(vertexnormal)和顶点颜色(vertex color)。请在cocos引擎的CCGLProgram.cpp中查看完整属性列表。

varying vec2 v_texture_coord

varying vec2 v_texture_coord1

“varying”值将被传到片段着色器中(fragment shader)。片段着色器所需要的任何变量前都需要添加“varying”限定符。这个例子中,我们仅需要知道这两个贴图的坐标。

void main(void)

{

gl_Position = CC_MVPMatrix * a_position

v_texture_coord.x = a_texCoord.x

v_texture_coord.y = (1.0 – a_texCoord.y)

v_texture_coord1.x = a_texCoord1.x

v_texture_coord1.y = (1.0 – a_texCoord1.y)

}

设置顶点位置,拷贝贴图的坐标给varying values,这样片段着色器就可以使用这些值。现在我们一起来分解片段着色器。

#ifdef GL_ES

varying mediump vec2 v_texture_coord

varying mediump vec2 v_texture_coord1

#else

varying vec2 v_texture_coord

varying vec2 v_texture_coord1

#endif

声明从顶点着色器传递过来的“varying” 值

uniform sampler2D lightmap

还记得在将着色器应用到网格时所使用的 state->setUniformTexture(“lightmap“,light map)语句么?这个值就是对应语句中的那个贴图。

void main(void)

{

gl_FragColor = texture2D(CC_Texture0, v_texture_coord) *(texture2D(lightmap, v_texture_coord1) * 2.0)

}

这个语句设置像素颜色。首先你会注意到从未声明过的 CC_Texture0变量。Cocos 2d-x中有大量可在着色器中使用的默认统一变量。再次强调,可在CCGLProgram.cpp中查看完整属性列表。这个例子中,CC_Texture0对应在3D模型中所应用到网格中的贴图。texture2D命令会在给定的贴图坐标中去查找贴图的像素颜色和透明度。它会返回一个包含了那个像素的RGBA值的vec4值 。所以这里我会在UV1中查找到瓦片贴图的颜色值,然后在UV2中查到光照贴图的颜色值,最后把两个值相乘。

你应该注意到了我先是把光照贴图的颜色值两两相乘了。因为贴图颜色值范围为0.0-1.0,所以很显然,如果用白色值vec4(1.0, 1.0, 1.0, 1.0)去乘中间灰值vec4( 0.5, 0.5, 0.5,1.0 ),那么你仍是得到一个中间灰值vec4( 0.5, 0.5, 0.5,1.0 )。将两个值相乘可以使贴图更亮,同时也可以使贴图更暗,这将使你获得一个很好的可变的亮度范围。

参考

由浅到浅入门批量渲染(一)

由浅到浅入门批量渲染(二)

由浅到浅入门批量渲染(三)

由浅到浅入门批量渲染(四)

由浅到浅入门批量渲染(完)

【Unity游戏开发】静态、动态合批与GPU Instancing

本文只摘抄部分原文,达到概念性了解。

批量渲染其实是个老生常谈的话题,它的另一个名字叫做“合批”。在日常开发中,通常说到优化、提高帧率时,总是会提到它。

可以简单的理解为: 批量渲染是通过减少CPU向GPU发送渲染命令(DrawCall)的次数,以及减少GPU切换渲染状态的次数,尽量让GPU一次多做一些事情,来提升逻辑线和渲染线的整体效率。 但这是建立在GPU相对空闲,而CPU把更多的时间都耗费在渲染命令的提交上时,才有意义。

如果瓶颈在GPU,比如GPU性能偏差,或片段着色器过于复杂等,那么没准适当减少批处理,反而能达到优化的效果。所以要做性能优化,还是应该先定位瓶颈到底在哪儿,然后再考虑优化方案,而不是一股脑的就啪啪啪合批。当然,通常情况下,确实是以CPU出现瓶颈更为常见,所以适当的了解些批量渲染的技法,是有那么一丢丢必要的。

静态合批是一种听起来很常用,但在大多数手游项目里又没那么常用的合批技术。这里,我简单的将静态合批分为预处理阶段的合并,和运行阶段的批处理。

合并时,引擎将符合合批条件的渲染器身上的网格取出,对网格上的顶点进行空间变换,变换到合并根节点的坐标系下后,再合并成一个新的网格;这里需要注意的是,新网格是以若干个子网格的形式组合而成的,因为需要记录每一个合并前网格的索引数量和起始索引(相对于合并后的新网格)。

空间变换的目的,是为了“固化”顶点缓冲区和索引缓冲区内的数据,使其顶点位置等信息都在相同的坐标系下。这样运行时如果需要对合并后的对象进行空间变换(手动静态合批对象的根节点可被空间变换),则无需修改缓冲区内的顶点属性,只提供根节点的变换矩阵即可。

在Unity中,可以通过勾选静态批处理标记,让引擎在打包时自动合并;当然,也可以在运行时调用合并函数,手动合并。打包时的自动合并会膨胀场景文件,会在一定程度上影响场景的加载时间。此外,不同平台对于合并是有顶点和索引数量限制的,超过此限制则会合并成多个新网格。

运行时是否可以合批(Batch)成功,还取决于渲染器材质的设置。

当然,如果手动替换过场景中所有Material,也会打断批次。

静态合批与直接使用大网格(是指直接制作而成,非静态合并产生的网格)的不同,主要体现在两方面。

其一,静态合批可以主动隐藏部分对象。静态合批在运行时,由于每个参与合并的对象可以通过起始索引等彼此区分,因此可以通过上述多次DrawCall的策略,实现隐藏指定的对象;而直接使用大网格,则无法做到这一点。

其二,静态合批可以有效参与CPU的视锥剔除。当有剔除发生时,被送进渲染管线的顶点数量就会减少(通过参数控制),也就意味着被顶点着色器处理的顶点会减少,提升了GPU的效率;而使用大网格渲染时,由于整个网格都会被送进渲染管线,因此每一个顶点都需要被顶点着色器处理,如果摄像机只能照到一点点,那么绝大多数参与计算的顶点最后都会被裁减掉,有一些浪费。

当然,这并不意味着静态合批一定就比使用大网格要更好。如果子网格数量非常多,视锥剔除时CPU的压力也会增加,所以具体情况具体分析吧~

静态合批采用了以空间换时间的策略来提升渲染效率。

其优势在于:网格通常在预处理阶段(打包)时合并,运行时顶点、索引信息也不会发生变化,所以无需CPU消耗算力维护;若采用相同的材质,则以一次渲染命令,便可以同时渲染出多个本来相对独立的物体,减少了DrawCall的次数。

在渲染前,可以先进行视锥体剔除,减少了顶点着色器对不可见顶点的处理次数,提高了GPU的效率。

其弊端在于:合批后的网格会常驻内存,在有些场景下可能并不适用。比如森林中的每一棵树的网格都相同,如果对它采用静态合批策略,合批后的网格基本等同于:单颗树网格 x 树的数量,这对内存的消耗可能就十分巨大了。

总而言之,静态合批在解决场景中材质基本相同、网格不同、且自始至终都保持静止的物体上时,很适用。

试想一个场景:一场激烈的战斗中,双方射出的箭矢飞行在空中,数量很多,材质也相同;但因为都在运动状态,所以无法进行静态合批;倘若一个一个的绘制这些箭矢,则会产生非常多次绘制命令的调用。

对于这些模型简单、材质相同、但处在运动状态下的物体,有没有适合的批处理策略呢?有吧,动态合批就是为了解决这样的问题。

动态合批没有像静态合批打包时的预处理阶段,它只会在程序运行时发生。 动态合批会在每次绘制前,先将可以合批的对象整理在一起(Unity中由引擎自动完成),然后将这些单位的网格信息进行“合并”,接着仅向GPU发送一次绘制命令,就可以完成它们整体的绘制。

动态合批比较简单,但有两点仍然需要注意:

动态合批不会在绘制前创建新的网格,它只是将可以参与合批单位的顶点属性,连续填充到一块顶点和索引缓冲区中,让GPU认为它们是一个整体。

在Unity中,引擎已自动为每种可以动态合批的渲染器分配了其类型公用的顶点和索引缓冲区,所以动态合批不会频繁的创建顶点和索引缓冲区。

在向顶点和索引缓冲区内填充数据前,引擎会处理被合批网格的每个顶点信息,将其空间变换到世界坐标系下。

这是因为这些对象可能都不属于相同的父节点,因此无法对其进行统一的空间转换(本地到世界),需要在送进渲染管线前将每个顶点的坐标转换为世界坐标系下的坐标(所以Unity中,合并后对象的顶点着色器内被传入的M矩阵,都是单位矩阵)。

相对于上述看起来有点厉害但是本质上无用的知识而言,了解动态合批规则其实更为重要。比如:

当我们想要呈现这样的场景:一片茂密的森林、广阔的草原或崎岖的山路时,会发现在这些场景中存在大量重复性元素:树木、草和岩石。它们都使用了相同的模型,或者模型的种类很少,比如:树可能只有几种;但为了做出差异化,它们的颜色略有不同,高低参差不齐,当然位置也各不相同。

使用静态合批来处理它们(假设它们都没有动画),是不合适的。因为数量太多(林子大了,多少树都有), 所以合并后的网格体积可能非常大,这会引起内存的增加 ;而且,这个合并后的网格还是由大量重复网格组成的,不划算。

使用动态合批来处理他们,虽然不会“合并”网格,但是仍然需要在渲染前遍历所有顶点,进行空间变换的 *** 作;虽然单颗树、石头的顶点数量可能不多,但由于 数量很多,所以也会在一定程度上增加CPU性能的开销,没必要

那么,对于场景中这些模型重复、数量多的渲染需求,有没有适合的批处理策略呢?有吧,实例化渲染就是为了解决这样的问题。

实例化渲染,是通过调用“特殊”的渲染接口,由GPU完成的“批处理”。

它与传统的渲染方式相比,最大的差别在于:调用渲染命令时需要告知GPU这次渲染的次数(绘制N个)。当GPU接到这个命令时,就会连续绘制N个物体到我们的屏幕上,其效率远高于连续调用N次传统渲染命令的和(一次绘制一个)。

举个例子,假设希望在屏幕上绘制出两个颜色、位置均不同的箱子。如果使用传统的渲染,则需要调用两次渲染命令(DrawCall = 2),分别为:画一个红箱子 和 画一个绿箱子。

如果使用实例化渲染,则只需要调用一次渲染命令(DrawCall = 1),并且附带一个参数2(表示绘制两个)即可。

当然,如果只是这样,那GPU就会把两个箱子画在相同的位置上。所以我们还需要告诉GPU两个箱子各自的位置(其实是转换矩阵)以及颜色。

这个位置和颜色我们会按照数组的方式传递给GPU,大概这个样子吧:

那接下来GPU在进行渲染时,就会在渲染每一个箱子的时候,根据当前箱子的索引(第几个),拿到正确的属性(位置、颜色)来进行绘制了。

静、动态合批实质上是将可以合批的对象真正的合并成一个大物体后,再通知GPU进行渲染,也就是其顶点索引缓冲区中必须包含全部参与合批对象的顶点信息; 因此,可以认为是CPU完成的批处理。

实例化渲染是对网格信息的重复利用,无论最终要渲染出几个单位,其顶点和索引缓冲区内都只有一份数据, 可以认为是GPU完成的批处理

其实这么总结也有点问题,本质上讲: 动、静态合批解决的是合批问题,也就是先有大量存在的单位,再通过一些手段合并成为批次;而实例化渲染其实是个复制的事儿,是从少量复制为大量,只是利用了它“可以通过传入属性实现差异化”的特点,在某些条件下达到了与合批相同的效果。

无论是静态合批、动态合批或实例化渲染,本质上并无孰优孰劣,它们都只是提高渲染效率的解决方案,也都有自己适合的场景或擅长解决的问题。个人以为:

Unity下可以通过以下两种方式快速优化骨骼蒙皮动画:

在相同的测试环境下,再次进行测试后可以发现,这种方法确实可以产生一定效果。其原因我认为主要有以下两点:

开启GPU蒙皮,Unity会通过ComputeShader的方式,使用GPU进行蒙皮。GPU上有大量的ALU(算数逻辑单元),可以并行大量的数值计算,效率较高,应该很适合这种针对顶点属性的数值计算。

但是实际情况是:在移动设备上使用GPU蒙皮反而会使主线程的耗时增加。通过Profiler可以发现,CPU把更多的时间放在了执行ComputeShader上,由于骨骼动画的实例很多(500个),所以这个调用时间本身成为了性能热点。所以,以目前的情况来看,这种在移动设备上使用GPU蒙皮的方式,似乎不适合处理大量骨骼蒙皮动画实例(也许是我使用的方式存在问题)。

简单来说,它们基本的思路,都是将骨骼蒙皮动画的“结果”预先保存在一张纹理中;然后在运行时通过GPU从这张纹理中采样,并使用采样结果来更新顶点属性; 再结合实例化技术(GPU instancing) ,达到高效、大批量渲染的目的。

可以简单的将它的工作流程分为两个阶段:

这个阶段其实是为后面的播放阶段准备动画资源,你也可以把这个资源简单的理解为一种特殊类型的动画文件。

首先,在编辑状态下,让携带骨骼蒙皮动画的角色,按照一定帧率播放动画。我们知道:动画播放时,角色网格会被蒙皮网格渲染器(SkinnedMeshRenderer)更新而产生变形(顶点变化);如果在动画播放的同时,记录下每一个顶点在这一时刻相对于角色坐标系(通常是角色脚下)的位置。那么,当动画播放完毕时,我们会得到一张“每一个顶点在每一个关键帧时的位置表”,我们就叫它“帧顶点位置表”吧。可是“帧顶点位置表”名字太长,你可能不太好记,我打字也比较麻烦,所以我们后面就叫它“表表”吧。

除了表表,我们还能得到与它对应的动画信息,比如这个动画的名称、时长、帧率、总帧数、是否需要循环播放等信息。

到这里,第一个阶段我们需要的内容就准备就绪了,可以进入下一个阶段:播放动画阶段。

在动画播放时,通过一个变量(播放时长)来更新播放进度;结合上一阶段我们记录下来的动画总长度、动画总帧数信息,就可以计算出当前动画播放到了第几帧(当前帧数 = 已播放时长 / 动画总时长 x 动画总帧数)。一旦得到当前动画帧,就表示可以通过表表锁定一行顶点数据。

接下来,只要遍历这行顶点数据;然后根据索引找到网格中对应的顶点并更新它的位置,就等于完成了蒙皮工作。

既然通过一个播放进度、表表和一些简单的动画信息,就能直接完成蒙皮(对顶点属性的更新),那么我们就不再需要以下内容了:

使用CPU来读取表表,并遍历更新所有顶点,显然不如GPU来的高效;所以接下来,我们将这一步放到渲染管线中的顶点变换阶段,借着GPU处理顶点的契机,完成使用表表中的数据对顶点属性进行更新。

实例化渲染的特点是使用相同网格,相同材质,通过不同的实例属性完成大批量的带有一定差异性的渲染;而烘焙顶点恰好符合了实例化渲染的使用需求。

所以,我们只需将控制动画播放的关键属性:比如过渡动画播放的V坐标、当前和下一个动画的插值比例等,放入实例化数据数组中进行传递;再在顶点着色器中,对关键属性获取并使用即可。

与蒙皮网格渲染器的比较

烘焙顶点的主要问题

除了烘焙顶点,另一种常用的优化方案是烘焙骨骼矩阵动画。

听名字就知道,烘焙骨骼矩阵与烘焙顶点位置,原理十分相似;最大的差异在于它们在烘焙时所记录的内容不一样:烘焙顶点记录下来的是每个顶点的位置,而烘焙骨骼矩阵记录下来的是每一根骨骼的矩阵,仅此而已。

烘焙骨骼矩阵最大的意义在于它补上了烘焙顶点的短板:受顶点数量限制、烘焙的动画纹理过大 及 纹理数量较多。

烘焙顶点对于纹理(面积)的使用是受顶点数量决定的,可以简单理解为:

所以,当模型的顶点数量过多时(数以千计),这种烘焙方式或者无法烘焙下整个模型(顶点数量>2048),或者需要一张或多张(法线、切线)大尺寸纹理(<2048 &&>1024)。

但是,烘焙骨骼矩阵主要取决于骨骼的数量,可以简单理解为:

在移动平台上,通常20根左右的骨骼就可以取得不错的表现效果,所以相对于烘焙顶点,烘焙骨骼可以记录下更长的动画,同时它也不再受顶点数量的限制,也无需对法线或切线进行特殊处理(因为可以在采样后通过矩阵计算得出)。

针对这两种方案,我个人认为并没有绝对的孰优孰劣;正如你所看到的,烘焙骨骼在处理复杂模型或多动作时更具优势;但如果渲染数量较多、模型顶点数较少、表现需求较弱(无需法向量参与计算)时,烘焙顶点也是值得尝试的,因为它在性能上会更好一些。

目前静态合批方案为运行时静态合批,通过调用 BatchingUtility.batchStaticModel 可进行静态合批。

该函数接收一个节点,然后将该节点下的所有 MeshRenderer 里的 Mesh 合并成一个,并将其挂到另一个节点下。

在合批后,将无法改变原有的 MeshRenderer 的 transform,但可以改变合批后的根节点的 transform。只有满足以下条件的节点才能进行静态合批:

引擎目前提供两套动态合批系统,instancing 合批和合并 VB 方式的合批,两种方式不能共存,instancing 优先级大于合并 VB。

要开启合批,只需在模型所使用的材质中对应勾选 USE_INSTANCING 或 USE_BATCHING 开关即可。

通过 Instancing 的合批适用于绘制大量顶点数据完全相同的动态模型,启用后绘制时会根据材质和顶点数据分组,每组内组织 instanced attributes 信息,然后一次性完成绘制。

合并 VB 合批适用于绘制大量低面数且顶点数据各不相同的非蒙皮动态模型,启用后绘制时会根据材质分组,然后每组内每帧合并顶点和世界变换信息,然后分批完成绘制

通常来说合批系统的使用优先级为:静态合批 >instancing 合批 >合并 VB 合批。

骨骼动画是一种常见但类型特殊的动画,我们提供了 预烘焙骨骼动画 和 实时计算骨骼动画 两套系统,针对不同方向的需求,分别优化。

这两套系统的唯一开关就是 SkeletalAnimation 上的 useBakedAnimation 开关,启用时会使用预烘焙骨骼动画系统,禁用后会使用实时计算骨骼动画系统,运行时也可以无缝切换。

目前所有模型资源在导入后,prefab 中全部默认使用预烘焙系统,以达到最佳性能。我们建议只在明显感到预烘焙系统的表现力无法达标的情况下,再使用实时计算系统。虽然两套系统可以在运行时无缝切换,但尽量不要高频执行,因为每次切换都涉及底层渲染数据的重建。

基于预烘焙系统的框架设计,蒙皮模型的 instancing 也成为了触手可及的功能,但要保证正确性还需要收集一些比较底层的信息。

这里的根本问题是,同一个 drawcall 内的各个模型使用的骨骼贴图必须是同一张,如果不是同一张,显示效果会完全错乱。所以如何将动画数据分配到每张骨骼贴图上,就成为了一个需要用户自定义的信息,对应在编辑器项目设置的 骨骼贴图布局面板 进行配置。

注意

目前底层上传 GPU 的骨骼纹理已做到全局自动合批复用,上层数据目前可以通过使用 批量蒙皮模型组件(BatchedSkinnedMeshRenderer)将同一个骨骼动画组件控制的所有子蒙皮模型合并:


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/bake/11689018.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-18
下一篇 2023-05-18

发表评论

登录后才能评论

评论列表(0条)

保存