URP源码学习(三)UniversalRenderer渲染管线

7 篇文章 2 订阅

整体理解

这部分算是URP的核心了,可编程管线,说的就是这个,有能力的项目,可以根据需要,做出更适合项目的管线。

unity提供了两个默认实现,一个是Universal(内部封装了forward和deferred),一个2D,2D有时间再细看(原谅我的懒),Universal这部分还是有些东西可看的,因为默认管线,这部分是完全看不到的,只能通过framedebugger看个流程,现在有机会看看内部实现,对理解管线也是很有好处的。

12版用UniversalRenderer代替了forward,用一个管线逻辑处理了forward和deferred,因为forward和deferred实际并没有那么大的区别,很多部分是通用的,所以unity就只提供一个选项,内部实现一个分支。在实现上,真正的区别,就是调用了不同的pass,以及对光源数据的处理。


渲染的最底层:pass

UniversalRenderer可以简单理解成驱动各个pass执行的一个管理者,pass则实现了具体的渲染逻辑。

pass的功能,分为两部分,配置rt和执行最终渲染。

pass-事件

基类文件中定义了一系列的渲染事件,RenderPassEvent。每个pass,在初始化的时候,都定义了一个event,这个event用于pass的排序。id之间间隔50,可加offset以添加额外的event。

pass-rt管理

每个pass,在渲染前需要先声明自己用到的color和depth,再由管线决定是否需要切换,以及实际分配rt,renderer基类调用pass的Configure抽象函数。

renderPass.Configure(cmd, cameraData.cameraTargetDescriptor); 

每个pass实现具体逻辑,pass子类调用基类的ConfigureTarget方法,配置渲染目标和clear方法,子类没实现则渲染到相机的目标。12增加了一个OnCameraSetup的方法,也是用于配置rt,看注释只有触发时间不一样,作用是一样的,也许以后会改成只有一个。

渲染目标分两个,color和depth,depth只有一个,color是个数组,默认第一个是相机目标,最大值在SystemInfo.supportedRenderTargetCount定义。

注意这个步骤只是设置了pass内部的数据,并没有真的通知到管线。

RenderTargetIdentifier[] m_ColorAttachments = new RenderTargetIdentifier[]{BuiltinRenderTextureType.CameraTarget}; 
RenderTargetIdentifier m_DepthAttachment = BuiltinRenderTextureType.CameraTarget; 

真正设置渲染目标,是通过CommandBuffer的SetRenderTarget方法,URP在CoreUtils类封装了一个静态函数SetRenderTarget。ScriptableRenderer类在ExecuteRenderPass方法中,先调用pass的Config函数,然后取pass的color和depth数据,设置为真正的渲染目标。

梳理一下这个流程


forward逻辑

forward-初始化

做了以下几件事

  • 创建几个特殊材质,用的是配置里的shader,这几个材质会传给对应的pass。
Material blitMaterial = CoreUtils.CreateEngineMaterial(data.shaders.blitPS); 
Material copyDepthMaterial = CoreUtils.CreateEngineMaterial(data.shaders.copyDepthPS); 
Material samplingMaterial = CoreUtils.CreateEngineMaterial(data.shaders.samplingPS);
Material screenspaceShadowsMaterial = CoreUtils.CreateEngineMaterial(data.shaders.screenSpaceShadowPS); 
  • 设置模板测试StencilState结构体。
  • 创建用到的每个pass,指定渲染RenderPassEvent。
  • 设置各个rt
  • 创建ForwardLights实例,用于光源的相关计算。
  • 创建RenderingFeatures,这个类里只有cameraStacking一个bool值,像是个没开发完的功能。

forward-Setup

简单说这个函数的作用就是把一个个pass加到个list里,供后边执行每个pass。

对于只渲染深度的相机,只需要添加3个pass,opaque、skybox、transparent。

一些重要的判断

  • 后处理是否开启,URP把后处理分成了两步,一个是实现常规特效的后处理,一个是抗锯齿这种,具体逻辑在PostProcessPass内部区分。
  • 是否需要深度图。首先判断相机配置,然后区分scene和game相机,scene相机之外,检测一下是否可以从opaque pass拷贝过来,以提升性能。具体判断在CanCopyDepth函数。

对一些特殊pass的说明,按代码顺序,没有特殊操作的略过

  • DepthOnlyPass
    • 开启条件:scene相机一定开启。game相机首先读取管线配置,同时CanCopyDepth为false,也就是说要注意本来不想开depth,但是开了抗锯齿等后处理效果,depth也会开启。
    • 用法:shader要有DepthOnly pass,渲染所有的DepthOnly Pass到指定texture,shader中通过_CameraDepthTexture获取。
  • CopyColorPass
    • 复制指定颜色buffer到目标颜色buffer,可以复制不透明物的渲染结果,用于扭曲特效。
    • 降采样可作为优化。
    • shader中通过_CameraOpaqueTexture获取。
  • CopyDepth和DepthOnly是互斥的,只需要用到一个,shader都是从_CameraDepthTexture获取。

其他pass还有很多 ,以后看shader的时候再一起细看。

ScriptableRenderer-Execute

unity把这部分放到了渲染管线的基类实现,也就是这部分被定义为通用的框架层,不建议项目扩展。

执行流程

  • 获取相机数据,关闭shader关键字,执行一次clear操作
SetCameraRenderState(cmd, ref cameraData); context.ExecuteCommandBuffer(cmd); cmd.Clear(); 
  • 对pass排序,按之前定义的RenderPassEvent。
  • 设置shader定义的时间变量,SetShaderTimeValues函数实现。
  • 将pass按event顺序,分成四块
    • BeforeRendering:用于处理阴影等,不是实际的渲染,只作为功能使用
    • MainRenderingOpaque:渲染不透明物体
    • MainRenderingTransparent:透明物体
    • AfterRendering:在后处理之后,在unity的demo中没看到具体渲染了啥,可能是扩展用的吧。
  • 执行SetupLights,设置光照需要的一系列参数,细节在光照部分细说。
  • 然后先执行BeforeRendering
  • 接着是一个循环,用于处理VR,由于手游中只需要渲染一次,VR部分先不管,只按一个去看。
    • 首先设置好相机属性,执行一次commandbuffer的clear指令。
    • 然后是不透明物体渲染,depth也在这执行。
    • 渲染透明物体,要注意的是透明和不透明物体的渲染,都是DrawObjectsPass实现的,参数不同,设置了RenderQueueRange和layer。
    • 渲染AfterRendering

forward大致流程大概就这些,感觉上没有太多特殊东西,只是细节很多,流程复杂些,用到了查查就好。


延迟渲染相关内容

URP延迟渲染的简单介绍

  • 首先延迟渲染的核心思想,是利用gbuffer,保存一部分数据,最后做一次对应屏幕分辨率的计算。URP自然也要基于gbuffer去实现。
  • URP对于传统的延迟渲染,又做了一些扩展。gbuffer保存了比法线更多的数据,以及可以支持多种光照模型,代价是drawcall增加。
  • 目前的版本不支持OpenGL,但是支持Vulkan,手机上可以看情况开启,动态切换也比较容易。

多种光照模型的支持方式

基于模板值实现,目前GBufferPass配置了Lit、SimpleLit、Unlit这3个shader对应的模板值,通过DrawRenderers方法直接传给GPU。但是光照计算的时候,只对Lit和SimpleLit做了处理,Unlit实际会在forward的时候渲染。

GBufferPass渲染的结果,是每个像素的各种信息。模板缓冲值,真正意义是记录每个像素对应的光照算法。

GBufferPass做了两件事,一是使用指定的shader pass,渲染相机可见的所有物体。渲染的结果,不包含实时光,只有烘焙光,和各种物体本身数据(法线等)。可以隐藏光源验证,FrameDebug里没用到光源数据。二是对支持的材质,分别写入不同的模板值。

gbuffer数据计算完成后,对每个光源做两次计算,分别用Lit和SimpleLit算法。这样就实现了延迟渲染的多种光照模型。

光源的处理

看着代码,支持stencil和tiled两种方式,但是在延迟渲染下,tiled是固定关闭的,所以先不看tiled的方式,可能没完全实现。

对于stencil光源,会对每个光源类型渲染,最终都是用的DrawMesh方法,用指定的shader pass渲染。对于平行光,mesh是个全屏的三角形。点光源mesh是球体,聚光灯是个半球。

mesh的作用,是通过变换矩阵,将mesh放到光源位置,然后用指定pass渲染一遍,渲染目标是相机颜色buffer。加上深度和模板测试,结果就是对光源范围内的像素做了一次光照计算。

平行光,深度测试设置为NotEqual,效果是对全屏像素生效。

点光源和聚光灯,shader设置Cull Front,只渲染背面,同时ztest为GEqual。效果就是当前像素深度大于等于光源对应模型的深度,就计算光照颜色。

为什么要剔除正面

  • 首先当光源超过屏幕范围,正面会不可见,只有渲染背面,才有光照效果。
  • 如果是渲染正面,那么会把光源前面的物体照亮,而这一面是看不到的,就浪费了计算。
  • 渲染背面的时候,如果mesh的位置,深度大于光源,表示在光源对应mesh背面的前面有像素,就计算像素的颜色。如果没有,表示超出了光源的显示范围。可以理解为从光源位置,向屏幕里面发一定长度的射线,如果打到了像素,表示被光源照到,计算颜色。
  • 这地方有点不好理解,也只是自己的一些猜测。

性能上的消耗

因为对每个光源的每个光照模型都用渲染一次,增加了drawcall。最终drawcall数量=渲染所有物体的drawcall+光源数量*光照算法数量(也就是用到的shader数量)+(光源数量-1(Stencil Volume pass,用于设置模板值))。

尽管增加了drawcall,但overdraw并没有增加,依然是对屏幕像素,做主光源和附加光的光照。因为每个像素,用到的光照算法是固定的,所以每个drawcall,只计算自己对应的那些像素。

最大的消耗在于内存,可以根据配置设置gbuffer数据。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值