技术分享连载(八十二)

技术分享连载(八十二)

本期我们聚集了这些话题:实现动态合批的条件、渲染模块需关注的参数指标、移动端上的阴影实现方式...


我们将从日常技术交流中精选若干个开发相关的问题,建议阅读时间15分钟,认真读完必有收获。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。

UWA QQ群:793972859
UWA 问答社区:answer.uwa4d.com


性能

Q1:请输入图片描述
如上图所示,一个场景里包含2个Cube和一个Quad,除了位置不一样外,其他完全一致:相同的缩放、相同的材质,且均未使用Lightmap,但是只有2个Cube动态合批了,Quad则未合批成功(测试过如果场景中有Plane,Quad和Plane会被合批)。Frame Debugger也没有合批失败的相关提示,我的疑点是:
1)是否跟透明有关?
2)2个模型顶点属性个数不一致。
但是我不知道如何获取这些信息,请问这是什么原因导致的呢?

动态合批的条件需要满足:
1)相同材质,即指向同一份材质对象,参数一致
2)顶点属性不能超过900,目前看来Cube和Quad的顶点都很少不太会超过900的限制。所以可能是材质的不一致,建议题主检查一下两者的材质是否指向同一个,材质参数是否一样。

我用2017.1.1做了下测试,确定是因为Cube的Mesh有UV2,而Quad没有,导致无法合批。

实验结果如下:
场景中放了一个Quad,Batch数是2:
请输入链接描述

测试代码:

    void Start () {
            GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
            cube.GetComponent<MeshRenderer>().sharedMaterial = m_Mat;
            //Mesh mesh = cube.GetComponent<MeshFilter>().mesh;
            //mesh.uv2 = null;
        }

当注释掉UV2设置为null的代码时,Batch数为3:
请输入链接描述
当打开这段代码时,Batch数为2:
请输入链接描述
同时,从Frame Debugger的结果也可以看到,此时Cube和Quad已经合并了。因此,我们认为Standard Shader对有UV2和没有UV2的Mesh进行了分别渲染,从而导致无法进行合批。

该问题来自UWA问答社区,如您对该问题仍有疑问,可以转至社区进行进一步交流。
https://answer.uwa4d.com/question/59e085fa11dbc503627e22c7


动画

Q2:在Unity3d的官方文档中,animationclip.framerate的解释如下:

“Frame rate at which keyframes are sampled. (Read Only) This is the frame rate that was used in the animation program you used to create the animation or model.”

我对此有些疑问:
1)这个是否为动画系统每一秒钟更新动画的次数?如果是,当这个值高于Application.targetFrameRate时,一秒钟更新动画的次数依据哪个为准
2)这个是否只是调节动画播放速度的一个参数,而每秒钟动画状态更新的次数不受影响?

动画采样的时候是根据当前播放的时间去找对应的前后两个关键帧做插值。所以动画本身得告诉Unity两个关键帧(没做keyframe reduction时)的间隔时间是多少,或者一秒有几帧,这个值就是framerate。所以这个是只读的,导出时就应该是确定的。因此这个值也不会影响运行时动画的更新次数,默认情况下动画就是每帧更新一次的。

该问题来自UWA问答社区,如您对该问题仍有疑问,可以转至社区进行进一步交流。
https://answer.uwa4d.com/question/59e0596470c7f4a630fd390d


性能

Q3:Unity中的Frame Debugger中的渲染顺序以及数量和Profiler中看到的Draw Call数量,以及在高通的调试工具里看到的DrawCall数,它们之间有什么关系呢?哪个数值是影响渲染的重要指标?

Frame Debugger里的截图:
请输入链接描述

Profiler里的截图:
请输入链接描述

高通里的截图:
请输入链接描述

说明:Frame Debug和Profiler的截图是同一个工程,同一个视角在Editor下的截图,高通工具的截图是Android机上同一个工程,同一个视角的截图。

简单来说,Unity引擎中Total Batches是我们建议最需要关注的指标。

1个Setpass Call或者Batch,相当于是一次Render State的切换,而1个Draw Call则是CPU让GPU去进行渲染某一个Object的1次操作。在当前的移动设备中,1次Render State的切换要1个Draw Call本身要耗时。所以,Total Batches是我们较为建议的关注指标,也是UWA性能报告中所提供的Draw Call查看指标。而Frame Debugger中,其数量是与Total Batches相一致的,即查看的是每一个Batch的渲染物体。 更为详细一些的说明,可以查看https://answer.uwa4d.com/question/58d29b8b5a5050b366a6b6ae

而Unity Profiler中的Draw Call,其理论上对应的则是glDrawElements的调用次数,其与高通或其他第三方工具所返回的Gl Trace信息操作数量不太一致,但应该与其中的glDrawElements API的调用次数基本一致,题主可以自行检测看看。

该问题来自UWA问答社区,如您对该问题仍有疑问,可以转至社区进行进一步交流。
https://answer.uwa4d.com/question/59ddc3fa43cf099e2d2295be


性能

Q4:在尝试优化DrawCall时,发现即使关掉了需要深度绘制的后期处理,深度绘制的pass也依然存在。按照传统对Shadowmap实现的理解,应该是只需要额外的Shadowmap的Pass即可。 查阅资料了解到Unity 5.0之后,Unity用了一种利用Depth Texture的方式来计算阴影:

“In Forward rendering, directional light shadows are computed from camera’s depth texture instead of a separate “shadow collector” rendering pass. Saves a bunch of draw calls, especially when depth texture is needed for image effects anyway.”

现在问题是:是不是在Unity 5.0之后,只要开启阴影,整个场景的一次深度Pass绘制就无法避免?如果没有其他效果需要用到深度Pass,单纯为了阴影进行一次深度Pass绘制,会额外增加深度Drawcall,可不可以理解为这种情况下,性能比之前的阴影实现方式(指的是5.0之前,只使用ShadowMap进行阴影计算的实现方式)要差?

1)如果是使用Unity引擎本身的实时阴影,那么无论是5.x还是4.x,其开启后,都会生成一张Depth Texture。但这个渲染物体并不是整个场景,而是需要投射阴影的物体需要多渲染一遍,而这个投射阴影的物体是可以进行手动设置的;

2)实时阴影Draw Call的增加主要取决于投射阴影物体的数量,一般来说,数量越多,则Draw Call增加越大,但并不是简单的线性关系。题主说的这个是PC平台的,在移动平台上并非如此,而是像我所回答的那样。在移动端,只会有一个Shadowmap,而没有updatedepthtexture的绘制。

该问题来自UWA问答社区,如您对该问题仍有疑问,可以转至社区进行进一步交流。
https://answer.uwa4d.com/question/59dc2ff0776b39742eece21d


性能

Q5:今天看了唐建伟的《合并Shader系列 | 如何合并渲染状态》,获益良多,但是也产生了一些的问题:
1)这样通过多参数来设置渲染状态不会造成Shader Variant吗?
2、什么情况下在才会造成Shader Variant呢?

1)不会。在具体回答之前,我先理清一下“Shader Variant问题”是什么问题,我的理解,题主是指Shader会编译出更多的Shader变体(Shader Program)。 在这篇文章中,我们只合并了渲染状态,渲染状态的合并不会导致Unity编译出更多的Shader Variant。

我们就先拿一个示例Shader来做测试,我选用了文章中的“ShaderCombine/01.ShaderCombineSimpleZTest”来做测试,Unity版本为 5.5.4p3,使用Unity的Shader Variant Collection来算取Variant数量,不管是否加入合并的代码,Variant的数量都是259,如下图所示:
请输入链接描述
即便是使用“ShaderCombine/02.ShaderCombineCommonState”来测试,Variant的数量也是259,如下图所示:
请输入链接描述
从上面的图片可以看出,不管是否有渲染状态的参数在里面,Variant的数量都不会改变。至于原因我写在下面这个问题里。

2)宏定义(Keyword),“#pragma multi_compile XXX YYY ZZZ”,“#pragma multi_compile_xxx”,SubShader,Pass,Fallback及一些特殊不常用命令等的增减会造成variant的数量变化。

在说之前,先说说什么是Variant吧。其实一个Variant可以理解为一个具体的在GPU上执行的小程序,而一个Shader通常会编译出非常多的variant来应对不同的情况,比如单说雾效就有如下:没有雾效、有线性雾、有指数雾1、有指数雾2这样的4个Variant(PS:这里只考虑雾效,其他条件一致)。至于原因嘛,有很多,粗略归纳一下是因为GPU需要更多的并行处理、逻辑单元少,因此Shader里面要尽可能规避各种判断、循环语句等等,最后本来可以通过逻辑判断来处理的雾效就需要编译成不同的执行程序,来对应不同的情况(PS:这是一种高级优化,背后的原理很多,建议自行查询相关资料,查明前因后果)。

至于一个Shader到底会生成多少Variant呢?精确的计算方法,Unity并没有给出,但是我的归纳总结一下就是几组不同的编译宏的组合了,比如雾效、光照图、光源、阴影等等。另外还可以通过Unity的工具Shader Variant Collection来查看一个Shader到底有多少个Variant,也可以在里面来自己组合和预编译自己想要的Variant。

最后回答一下第一问的原因。刚刚我们说了造成Variant数量增加主要是需要生成不同的Variant来应对不同的情况,那么不同的渲染状态是否情况不同呢?其实不是。生成不同的Variant主要是为了消灭Shader内部的逻辑判断(PS:Shader的真正逻辑是CGPROGRAM…ENDCG中间的东西)。继续用雾效举例,先只考虑有雾效和没雾效,按一般的游戏逻辑写法,我们通常会在逻辑里用一个if判断来搞定,但是由于GPU的特殊性,这样的做法非常低效、不可取,那么就会使用Keyword这样的编译宏在编译的时候就分别编译为有雾效和没雾效的2个执行程序,也就是2个Variant。现在说会渲染状态,看任何的Shader,我们都不会在CGPROGRAM…ENDCG里面有关于渲染状态的处理代码,当然不需要为不同的渲染状态编译不同的variant,也就不会造成variant的增加。(这部分可以参看Unity的渲染流水线,渲染一个物体需要非常多的步骤,我们写的Shader编译成的variant只在流水线中的两个可编程部分执行,而渲染状态是设置其他步骤的,与variant是完全隔离开、互不干扰。也可以说CGPROGRAM…ENDCG内的逻辑决定了variant的数量,CGPROGRAM…ENDCG外的是给Unity配置状态用的,不会引起Shader的逻辑变化,因此没变化)。

另附上一张简化版渲染流程图:
请输入链接描述

该问题来自UWA问答社区,感谢唐建伟提供了回答,如您对该问题仍有疑问,可以转至社区进行进一步交流。
https://answer.uwa4d.com/question/59dd6f350461bc6f45206ad5


今天的分享就到这里。当然,生有涯而知无涯。在漫漫的开发周期中,您看到的这些问题也许都只是冰山一角,我们早已在UWA问答网站(answer.uwa4d.com)上准备了更多的技术话题等你一起来探索和分享。欢迎热爱进步的你加入,也许你的方法恰能解别人的燃眉之急;而他山之“石”,也能攻你之“玉”。
官网:www.uwa4d.com
官方技术博客:blog.uwa4d.com
官方问答社区:answer.uwa4d.com
官方技术QQ群:793972859(仅限技术交流)