Unity 2017 Game Optimization 读书笔记 Dynamic Graphics (5) Shader优化

Shader optimization

Fill Rate和 Memory Bandwidth开销最大的地方就是Fragment Shader。开销多大取决于Fragment Shader的复杂程度:多少纹理需要采样,多少数学计算函数需要使用等等。GPU的并行特性意味着在线程中如果任何地方存在瓶颈,都会导致有大量fragments的渲染出现问题。

Shader编程和优化是游戏开发中比较困难的一部分,因为它很抽象,想要写出高质量的Shader代码要比写普通的CPU的游戏逻辑代码差别很大。有时候还要需要些走后门的歪路子小技巧,比如提前计算数据,把数据存入纹理贴图中。

很多开发者喜欢用可视化的工具编写Shader,比如Shader Forge或者Amplify Shader Editor,但是它们生成的代码有可能不是最高效的。无论我们用不用可视化工具编写Shader,都应该来使用一些能够优化Shader的技巧。

1.Consider using Shaders intended for mobile platforms

Unity的内置Shader中有Mobile的版本,这些版本的Shader效率更高,PC主机游戏平台也可以使用这些版本的Shader来减少开销,但是要评估使用这些Shader导致的渲染效果下降是否可以接受。

2.Use small data types

数据类型越小,GPU计算更快。特别是移动平台上。所以我们第一个方法就是将float(32位)替换成更小的类型,比如half(16位),甚至fixed(12位),越小的位数也就需要更少的计算量。

Color 数值是优化对象之一,通常减少一些精度效果上并没有非常明显的感知,但是也要权衡是否真的可以接受渲染上效果的下降。

要注意的是这个优化方法会根据GPU的架构甚至品牌都会有影响,优化的效果也会不尽相同,我们可能得需要进行不少测试来验证优化效果。

3.Avoid changing precision while swizzling

Swizzling 是shader中从一个现有的vector通过我们想要的数据创建一个新的vector。一些例子比如:

Unity 2017 Game Optimization 读书笔记 Dynamic Graphics (5) Shader优化_第1张图片

用xyzw和rgba都可以,无论是个vector还是color,这种写法都使得Shader代码更容易理解。

在shader中进行精度的转换是开销很大的操作,在swizzling时进行数据精度的转换开销就更大了,所以我们要杜绝在在swizzling时进行数据精度的转换,如果有这种需求,还不如一开始就使用告精度的数据。

4.Use GPU-optimized helper functions

Shader 编译器通常会有效的优化数学运算,但是CG和Unity提供的库函数相比自己写的代码大多数情况下还是效率高的多。当我们写自定义的shader时候,可以找找库里有没有已经有提供的,比如CG库提供了abs(),step()等,Unity库提供了 WorldSpaceViewDir()来计算摄像机朝向,Luminance()将Color转成grayScale等。

CG库函数:http://http.developer.nvidia.com/CgTutorial/cg_tutorial_appendix_e.html

Unity库函数: http://docs.unity3d.com/Manual/SL-BuiltinIncludes.html.

5.Disable unnecessary features

去掉不必要的属性可以节省性能,比如我们使用的Shader有没有必要需要transparency, Z-writing, alpha-testing或者alpha blending?去掉这些设置或者属性带来的渲染效果的下降是否可以接受,如果可以的话这是一个降低Fill Rate开销的好办法。

6.Remove unnecessary input data

删除掉Shader中没用到的输入数据,因为它们会需要从内存中读取,消耗性能。

7.Expose only necessary variables

把Shader中的变量在material中暴露出来是有开销的,因为GPU没办法将这些变量认为是constant的,编译器没法按常量编译它们,这些数据必须每次都从CPU传入到GPU中。当然把变量暴露出来,调整效果调试的时候会比较麻烦,所以这条优化可以是在项目的后期再处理。

8.Reduce mathematical complexity

复杂的数学运算会导致渲染的瓶颈,因此我们要尽量避免。一种方法是提前计算好,把结果存入一张textrue中,之后在运行时可以将这个texture传入Shader中然后采样获得结果从而代替复杂的运算。

对于sin()和cos()这些函数我们duck不必这么做,因为它们已经被GPU高度优化过,但是其他类似 pow(), exp(), log()和我们自己写的函数就是可以优化简化的对象。

这种技术会有额外的内存开销去存储texture,也会增加Memory Bandwidth的开销,但是Shader没采用这种技术前本来也需要textrue的话,如果textrue的alpha通道并没有使用的话,我们就可以把结果存入alpha通道中,这种做法就非常nice,并不会有额外的开销,唯一需要的就是程序和美术要配合起来,程序提供所需存的结果给美术生成texture。

9.Reduce texture sampling

Memory Bandwidth开销最大的就是texture采样。Texture使用的越少,texture越小,性能越好。Texture越多,cache命中率就会越低,Texture越大,memory Bandwidth就会消耗的越多。

10.Avoid conditional statements

现代的CPU中,对于条件判断语句,CPU会尝试进行预测,判断最有可能运行的分支,提前用空闲的核心计算。等程序真正运行到的时候,如果发现决定错误了,就舍弃之前的运算而选择另外的分支。这种技术提高了CPU的运行速度,因为提前运算或者舍弃结果都要比等待决定哪个才是正确的分支要快,而且绝大多数的时候,CPU的预测是正确的。

但是在GPU上,因为处理是并行的,对于条件判断语句,必须得决定多少核心运行这个分支,多少核心运行其他分支,直到所有分支都运行完。比如一个if-else判断,GPU需要告诉一部分核心处理true的这条分支路径,然后再去要求其他核心处理false的路径,除非所有核心处理同样的path,否则必须每次都要处理两个path。因此我们应该尽量避免在shader中使用条件判断和多分支,当然也取决于必要程度。对于像素级别的计算,相比于多分支的开销,可能我们倒不如接受一些不必要的计算。

11.Reduce data dependencies

编译器会尽可能的优化我们的Shader代码,让GPU处理起来更舒爽。下边这个例子是对优化非常不友好的一段代码:

上边的代码数据依赖非常严重,每一行的计算都依赖于上一行代码计算的结果,这种写法导致编译器没办法优化这段代码,因为他们没法在指令级别并行处理。下边的代码则解决了这个问题:

Unity 2017 Game Optimization 读书笔记 Dynamic Graphics (5) Shader优化_第2张图片

 这一次,编译器识别出这段代码可以在指令级线程并行处理,这可以大大提高执行速度。

12.Surface Shaders

Unity的Surface Shaders是一种简化的Fragment Shaders,unity引擎会自动将Surface Shaders代码转换,但是也就让我们少了很多优化的机会。Surface Shaders能做的事Fragment Shaders一定可以,但是Fragment Shader能做的Surface Shaders不一定可以。个人认为是尽量不要用Surface Shaders。

13.Use Shader-based LOD

我们可以强制Unity在渲染远距离的物体时使用更简单的Shaders,这样可以有效降低Fill Rate的开销,特别是对于我们的目标是要支持多平台或者非常广的硬件范围。LOD keyword可以在Shader中使用来设置当前支持的Shader,如果当前的shader和LOD level并不匹配,则会降低至其他备用Shader一直到有匹配的Shader,我们在运行时可以通过maximumLOD属性来动态更改shader的LOD值。

这个特性和之前介绍过的mesh的LOD技术很相似, 可以查看Shader-based LOD 的文档获取更多信息。https://docs.unity3d.com/Manual/SL-ShaderLOD.html

 

 

你可能感兴趣的:(Unity,优化,Unity,Unity,优化,shader,优化)