上面的视频是我的下一款游戏作品《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 )。将两个值相乘可以使贴图更亮,同时也可以使贴图更暗,这将使你获得一个很好的可变的亮度范围。
本文参照官方文档,涉及camera可以参考 Cocos 3.0 camera
区别于 3D 模型对象,我们将不涉及模型的图片渲染体统称为 2D 渲染对象。 2D 渲染对象的处理在底层的数据提交上与 3D 模型存在差异,其遵循自己的规则做出了一些针对性的调整以实现更好的效率表现和使用体验。
2D 渲染对象的收集采用树状结构,RenderRoot 节点(带有 RenderRoot2D 组件的节点)为 2D 对象数据收集的入口节点, 所有的 2D渲染对象需在 RenderRoot 节点下才可以被渲染 。
由于 Canvas 组件本身继承 RenderRoot2D 组件 ,所以 Canvas 组件也可以作为数据收集的入口。 2D 渲染节点必须带有 UITransform 组件作为渲染顶点数据、点击或者对齐策略等功能生效的必要条件。
2D 渲染也可以支持对模型进行渲染,唯一的条件是带有模型组件(例如 MeshRenderer/SkinnedMeshRenderer)的节点必须添加 UI/UIMeshRenderer 组件才可以和 UI 在相同的管线上进行渲染。
引擎中所有不拥有的 model 的渲染对象都为 2D 渲染对象。与 3D 对象不同,2D 对象本身不拥有 model 信息,其顶点信息是由 UITransform 组件的 Rect 信息持有并由引擎创建的,且本身没有厚度。由于引擎的设计要求,2D渲染对象需要为 RenderRoot 节点(带有 RenderRoot2D 组件的节点)的子节点才能完成数据的收集 *** 作。
所以 2D 渲染对象的渲染要求有两点:
由于 2D 渲染对象在 Camera 的可见性判断上和 3D 渲染节点并无区别,所以用户需要自己控制节点的 layer 属性并设置 Camera 的 Visibility 来配合进行分组渲染,如果场景中出现多个相机的情况,错误的 layer 设置导致节点重复渲染或不渲染。
这里请 3D 1.2 版本升级的用户注意,我们纠正了之前的 Canvas 只会渲染其子节点的行为,目前需要用户自己管理节点的 layer 和相机的 Visibility,之前使用了多 Canvas 渲染的用户可能会需要对项目做出调整以达到更合理的场景结构。
https://docs.cocos.com/creator/3.0/manual/zh/ui-system/components/engine/priority.html
2D 渲染节点可分为在 Canvas 下的节点和不在 Canvas 下的节点两种 ,在 Canvas 下的节点可参考下文的 UI 节点排序 。 对于不在 Canvas 下的节点,用户可选择通过 自定义材质来开启深度检测 实现和 3D 物体的遮挡显示,开启后则会按照物体的 Z 轴坐标进行遮挡渲染(可参考范例 2d-rendering-in-3d ( GitHub | Gitee )。没有开启深度检测的话,则数据提交依旧会按照节点树顺序提交,也就意味着节点树靠下的节点会后渲染。
透明物体的深度写入问题
【关于cocos creator的渲染的提议】
UI 节点特指在 Canvas 节点下的 UI 节点,这些节点并未开启深度测试,所以节点的混合是严格按照节点树进行排序的。UI 的渲染排序采用的是一个深度优先的排序方式,每一个 UITransform 组件身上都有一个 priority 属性,根据 priority 的值来调整节点顺序。排序从根节点下的子节点开始,根据子节点的优先级来确定整体的渲染结构,也就是根节点下的子节点的排序已经决定了最终的渲染顺序。每一个节点下的所有子节点的 priority 则用来确定在当前节点下的渲染顺序。直接修改了 priority 也会直接改变节点树顺序。
举个例子:
因此,上图中整体的渲染顺序则是:B ->b1 ->C ->A ->a1 ->a2,在屏幕上的呈现状态为:a2 ->a1 ->A ->C ->b1 ->B。
注意事项
排序是一个很简单的功能,但是最终的呈现却是根据不同平台提供的渲染能力来的。因此,在这里说明一下,如果遇到了 UI 渲染出错,花屏,闪屏等现象,首先要检查的就是场景里所有相机(Camera 和 Canvas)的 ClearFlag,确保 场景里必须有一个相机要执行 Solid_Color 清屏 *** 作。
具体如何设置 ClearFlag,可参考以下几种情况:
原贴 Cocos Creator3.0 在3D游戏场景下添加2D精灵(利用RenderRoot2D),2D精灵的会默认渲染在3D元素上面,类似于UI的渲染层级。
注:使用双相机时,注意调节3D物体的SCALE,太小了可能看不到
DepthTest可以参考
cocosCreator3.2.0 Skeleton的深度写入问题
参考
https://docs.cocos.com/creator/3.3/manual/zh/ui-system/components/editor/ui-model.html
Creator3D :太厉害了!3D模型原来可以这样显示在2DUI上
Creator 3.0怎么在2D场景里面跑3D人物?
UIMeshRenderer 是一个将 3D 模型从 3D 渲染管线转换到 2D 渲染管线的带有转换功能的渲染组件。 该组件支持 3D 模型和粒子在 UI 上的显示,没有这个组件,即使模型和粒子节点在 UI 里也不会被渲染。
该组件的添加方式是在 层级管理器 中选中带有或继承自 MeshRenderer 组件的节点,然后点击 属性检查器 下方的 添加组件 按钮,选择 UI->UIMeshRenderer 即可。而粒子则是添加到粒子节点上。通常结构如下所示:
注意,如果结点是多层结构,则每一层带有或继承自 MeshRenderer 组件的节点,都要添加UIMeshRenderer。如果运行后看不到,需要检查canvas中Camera的visibility设置。
https://gitee.com/yeshao2069/CocosCreatorDemos/tree/v3.0.0/2DDemo/UIMeshRendererDemo
绑定了MeshRenderer组件的3d节点必须放在Canvas下,也就是2DUI层
3d节点的大小,3d节点添加了MeshRenderer组件,并且放置到Canvas下,那么他的大小将不会按照3d节点在摄像机下的大小显示,而是按照3d节点相对Canvas的大小来显示,通常模型的大小需要缩放到在UI层下的实际预览大小
材质,当大小和层级调整好以后你会发现模型是显示出来了,但是是黑的,,大家的默认effect 应该是builtin-standard吧,只需要将其改为builtin-unlit就可以了
但是,如果是两个material就会有问题,第2个material会失效
如果后面有两张背景图片时,发现有一张看不到了,要检查一下是不是把Depth Write也勾上了。
https://gitee.com/yeshao2069/CocosCreatorDemos/tree/v3.0.0/2DDemo/RenderTextureDemo
https://gitee.com/yeshao2069/CocosCreatorDemos/tree/v3.0.0/2DDemo/RenderTextureDemoForSkeletalAnimation
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)