中unity渲染管线大中华区技术总监張黎明介绍可编程渲染管线SRP和轻量级渲染管线LWRP、LWRP与内置渲染管线区别、LWRP源码解析Core RP和Lightweight RP。本文将分享如何定制轻量级渲染管线LWRP
过去我们制作射击类游戏,经常要做的一件事情是这把***用不同的FOV单独渲染如果***不进行特殊处理,使用和场景相同的FOV去画的话画出来会很窄,占屏幕面积很小很不美观。所以过去制作FPS的游戏经常要单独画***它的FOV可能会小一点,这样它占的屏幕空间会更大
过去没有可编程渲染管线SRP,过去在内置渲染管线Pipeline里我们要用二个相机,一个相机画***一个相机画场景,这样可以设置不同的FOV即视角但是这样的问题是过詓unity渲染管线的Camera其实很费性能,即使加入一个什么Camera都不做仍有很大的消耗。
因为它会计算相机的剔除Flatten裁剪之类的计算,所以相机越多性能会越差我们建议大家尽量不使用多个Camera,能用一个Camera解决就用一个Camera现在有了可编程渲染管线SRP,我们就可以用一个Camera来解决
默认的场景渲染是放置在最上面,是不可调整的除了First Person Object的Layer没画,其它的场景对象都是在这里上面渲染的
下面有三个Feature是用于渲染***的。第一个Render Feature画***上不透明的部分后面二个Render Feature画半透明的部分。因为***上有一些液体液体是半透明的,其它地方是不透明的
相机上设置了比较大的FOV,然后在苐一个Render Feature这里是设置相机的FOV是40然后主场景相机的FOV是80。常见的FPS游戏使用的FOV是80所以在Render Feature里某些对象可以有自己的FOV。
我们还可以给它设置不同的材质参数模板缓冲区,Stencil Buffer等这里我们用到了Stencil Buffer,还有Position偏移等参数这些是让我们管理场景中不同物体渲染的参数,在过去的话有些参数呮能在相机上设置。
简单介绍一下模板缓冲区在做这种效果的时候,我们目标是先画***因为***总是在场景的前面。过去画半透明物体時我们先画离相机最近的,再画远的这样深度裁剪时,前面物体会挡住后面的物体不会重复填充像素,这样性能会更好一些
等画唍***再去画场景的时候,它会检查Stencil Buffer是0才会去填充因为***填充过的像素Stencil已经是1了,在画场景的时候就不会重复填充***的像素省掉一些像素填充。这就是对Stencil Buffer的利用方法
可编程渲染管线里面有很多Render Pass,我们可以添加自己的Render Pass也可以删除一些Pass,可编程渲染管线就是按顺序执行不哃的Render Pass
下面有个类叫EnqueuePass,如果要自己写Render Feature的话要把自己的Render Pass加入到这个队列里面,加入上面的List数组里面
如上图所示,我们看到它执行时会执荇一个个BlockBlock里有一个while循环,去执行每个Pass
下面是执行Pass的函数。最下面这个SortStable函数不太醒目但它很有用的,它用来排序所有Render Pass的执行顺序
SortStable函數控制加入Pass后,Pass是在什么时间点画的Pass的执行顺序是什么样的。它会For循环所有的Pass然后做排序,排序时会对每个Pass做比较
每个Pass重载Operator的类,偅载Operator类会根据Event的值决定在不透明之前画还是半透明之前画,还是在天空盒之前画它会根据Event在SortStable函数中进行排序。
其实刚刚ScriptableRenderer是空的里面沒有Pass,它最终渲染的Pass是使用这里的Forward Renderer它派生了刚才的类后负责往里面加入Pass,加了正向渲染的Pass先画不透明再画半透明,再画后处理之类
Feature鈳以在渲染管线的某个时间点增加一个Pass或者多个Pass。
下面是Render Pass焦散的Render Pass其实比较简单,就是画了一个全屏的Quad去根据深度算出世界的坐标,计算阳光投影过去相当于把焦散的图投到水底的地方,使用全屏的Quad来实现的
如上图所示,红框标出了关键的几个地方AddRenderPasses会往Renderer里加入Pass,下媔的Pass是某个效果的实现
在很多RenderPass的Pass代码里,我们都无法直接看到Event它其实是在基类ScriptableRenderPass里设置了默认Opaque,所以如果你没有看到它设置在什么时间點执行的话它默认在不透明物体之后画。
最下面的红框部分Operator用来比较二个RenderPass应该先画谁根据Event的数值,转换成Int来比较
如何基于unity渲染管线輕量级渲染管线LWRP进行一些简单定制?应该怎么做
经过调查,我发现目前设计的确没有考虑到这种情况如果不改代码的话,目前无法实現这个需求于是我尝试通过修改一点代码,实现在运行时可以增加一个Render Feature
我调查的过程是:首先要找一个场景进行测试,于是我找到了BoatAttack項目用于做测试刚好它的Renderer里有二个Render Feature,所以我首先把它默认Asset里的Render Feature删掉试试是否可以在运行时增加Feature。
但是我发现它加到了Forward Renderer的基类里的一个數组里这个数组不是一个Public的变量,从外面不能直接访问m_RenderFeatures这个变量不能公开访问,而且也没有修改这个数组的API导致不能增加,删除Render Feature
那么我如何修改该数组呢?为了节省时间我暴力的把它改成了Public,然后就可以在脚本里访问这个变量给它增加Render Feature了。这是为了给大家说明峩们可以定制LWRP但这种做法其实不太好。
将它暴露成Public之后呢我就另写了一个脚本,加到相机上面在相机上面进行判断,如果相机上设置了Renderer我就获取相机上的Renderer,如果相机上没有设置Renderer我就从Asset上获取Renderer。
我会在这点这个按钮可能效果不太明显。在我点按钮之前水下没有焦散效果,点了按钮后就可以看到Caustic焦散效果。
还有一个非常小的定制案例过去unity渲染管线一般用来做游戏,做VR、AR几乎没有人用unity渲染管線做手机上普通的App。但unity渲染管线中国的团队开发了一套工具叫UIWidgets可以用它来做App。
过去最主要的问题是UI不方便App开发者用我们的UGUI时,由于它昰用来做游戏的他们会觉得要重新学习,而且用起来也没有做App的那些工具方便因此我们就制作了UIWidgets。UIWidgets的问题是:如果里面有3D场景的话烸帧都要渲染。
作为App来说功耗上和消耗电量会比较大一些,因为普通App和游戏不同不会不停地刷新画面。所以我们有这个需求:能不能鈈要每帧都渲染
刚好我们有脚本的ScriptableRenderPipeline,我们可以在不需要渲染的时候关闭渲染因为App不是每帧都需要操作,可能我们在看一段话时至少會几分钟都没有操作,也没必要重新渲染
代码实践很简单,首先在LightweightRenderPipeline的主要Render函数里进行判断判断它过了多少毫秒,如果超过特定毫秒會继续执行,如果没到特定毫秒就先Return。
当然此时引擎的主线即逻辑帧还会执行。你可以设置它的Frame rate为30帧或60帧但渲染会直接跳过,不做渲染操作从而节省一些GPU的功耗。
这是一个很简单的定制案例大家考虑如果用unity渲染管线开发手机App的时候,因为游戏引擎会每秒几十帧渲染整个场景导致手机功耗很高。那有没有可能没有输入操作的时候停止渲染呢
因为现在有了可编程渲染管线SRP,会非常简单实现的方式就是在Lightweight RenderPipeline的主函数里判断要不要渲染,如果没有输入操作就跳过渲染的执行。
如果希望偠定制轻量级渲染管线,你要修改它的源码应该怎么做呢?
我们推荐的方法是先到Github上把SRP可编程渲染管线源码的工程Clone下来
在这里可以指萣路径,指向我们Clone下来的地址然后现在开始,游戏里就使用我们Clone下来的目录下的轻量级渲染管线代码直接在代码里修改就好了,改完後可以提交到自己托管源代码的服务器上
还有一个关于Shader的小坑。大家都知道写Shader时会在最上面定义Shader属性,过去在Shader Property上我们会随便写一个變量,half+变量这样写上去就行了。
到了轻量级渲染管线LWRP中你必须把它加入到Constant Buffer里面,否则变量无效定义后,即使在外面材质部分调参数吔会没有作用所以一定要记住这点。
Shader有一个需要特别注意的地方在添加Shader Properties的时候,一定要把变量添加到Constant Buffer里面否则添加的属性无效。
另外如果在工程里需要引用轻量级渲染管线里的某个Shader,它的引用路径要像上图最下面这部分大家写的时候要按这样的格式去写URL。
需要注意:由于unity渲染管线 2019.1推出之后有很大的更新导致大部分老的Sample工程在2019.1上打开会报错,只有前面二个工程不会报错
不过大家还是可以用它们來了解LWRP开发的整个历程,了解LWRP是如何一步步发展过来的