整体理解
这部分算是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数据。