本篇继续讲URP中的数据与接口。
本篇从相机栈渲染讲起,也就是RenderCameraStack方法,遇到什么再说什么。

编辑相机栈

选择一个Base相机,在Stack栏中用+号按钮添加Overlay相机,组成相机栈;
没有Overlay相机,就新建一个Camera,设置RenderType为Overlay。

渲染管线中出现的几种常见类型

PipelineAsset:就是我们创建的管线配置文件,全局独一份;
Pipeline:被Asset的CreatePipeline方法实例化,也是全局独一份;
RendererData:Asset的m_RendererDataList参数,指定一个渲染方案列表,长度至少为1;
注:Data列表中有一个被标记为默认值;Camera从列表中选择一个Data执行渲染任务;
Renderer:与Data一一对应,包含渲染逻辑,比如Renderer2D、ForwardRenderer。
Feature:Data的m_RendererFeatures参数,是一个Feature列表,这些Feature可以插入Renderer逻辑中的某一个环节,比如透明队列后-后处理前,是用户自定义管线的重要手段。

RenderCameraStack(上下文、baseCamera)

只有Base相机能作为相机栈渲染任务的开始,没有被分配的Overlay相机不生效。
一个栈的任务包括Base任务和多个Overlay任务。
每个任务都是一样的形式:
BeginCameraRendering(context, camera);
VFX.VFXManager.PrepareCamera(camera);
UpdateVolumeFramework(camera, data);
InitializeCameraData(camera, data, !isOverlay, out var cameraData);
//对于Overlay相机:InitializeAdditionalCameraData
RenderSingleCamera(context, cameraData, anyPostProcessingEnabled);
EndCameraRendering(context, camera);

Begin和End方法,Unity约定的单个相机的渲染开始前和结束后执行,;
VFX,也就是VEG的底层,预处理GPU特效,可能需要引用相机(黑盒子);
前文提到了关于Volume的混合机制,相机要先提交自己的身份;
cameraData表示相机渲染一帧需要的全部数据,struct类型;
我们可以从InitializeCameraData方法中了解具体收集了那些数据;
根据这些数据的收集和使用,我们可以知道,编辑器中的调整如何影响渲染结果;
RenderSingleCamera表示一个独立的相机渲染任务,剔除、规划Pass队列、执行等;

对于Scene视图来说,使用独立的Scene视图预览用相机,区别于相机栈的工作模式。
注:Scene相机默认没有UniversalAdditionalCameraData组件;
视图在显示内容上和Game视图也有差别:
可以显示参考线、轨迹、碰撞盒子、UPBR的某个层、UV(替换shader);
Scene视图的开发对策划、美术也很重要,能显著提高开发效率;
开发者在对实时渲染流程熟悉之后,可以尝试开发编辑器工具,辅助团队。

UpdateVolumeFramework

VolumeManager.instance.Update(trigger, layerMask);
默认trigger是相机的transform,默认layer是1。
Scene视图相机虽然没有data组件,默认使用main相机的data;

VolumeManager类型

介绍:管理场景中的全部Volume,处理插值工作,单例。
我注意到这里的Instance实例,有点特殊:
new Lazy<VolumeManager>(() => new VolumeManager());
这样创建的实例是延迟初始化的,首次访问迟缓变量的Value属性前不会实例化;
URP演示的这个例子并不好,首次访问变量就直接触发了初始化;
延迟初始化的功能,我们可以利用在大量读取表格上,避免一张表上几百条配置读很久,但是又不一定用得上;
比如记录下每条配置在字节流中的地址,触发初始化时再实例化配置。

VolumeManager自构

创建容器,用于存储Volume信息,包括Volume的优先级、碰撞器、Profile。
参考Core库/Runtime/Volume/目录下的其他几个类型:
Volume.cs:挂载在场景中的组件,触发器,携带sharedProfile;
VolumeComponent.cs:
VolumeParameter.cs:
VolumeProfile.cs:后处理效果配置;
VolumeStack.cs:

Volume类型需要实时通知(Start/Update方法)VolumeManager优先级、layer变化,创建/释放;
类似于Renderer的mesh/sharedMesh属性,Volume根据访问方式判断是否缓存Profile;
一个VolumeProfile可以定义多种效果,这些效果都是VolumeComponent,效果不能重复;
每个效果又有很多属性,这些属性都是VolumeParameter,我们可以重写部分属性;

VolumeManager.instance

初始化VolumeManager时,收集所有效果类型实例;
存储类型到baseComponentTypes,存储实例到m_ComponentsDefaultState(备份、模板);
我们可以根据Unity的继承规则(继承VolumeComponent),创建新的后处理效果。

VolumeStack:定义了一个效果列表,以type为key;
VolumeManager.stack:包含全部继承了VolumeComponent的type ⇒ 全局效果列表

VolumeManager.Update方法:单个相机的注册事件,重新计算混合效果
ReplaceData(stack, tigger, layerMask):重置全局效果列表
OverrideData(stack, volume.profileRef.components, Mathf.Clamp01(volume.weight));
使用有效的Volume重写全局效果列表;
对于Local Volume来说,插值为权重 * (1-触发器离碰撞器的距离的平方/混合距离的平方);
这里我们可以发现,Volume的生效机制是针对全局特效列表的,对应着Camera;
对于单个相机来说,Volume的混合不使碰撞器内的物体被区别对待;
比如,有一个碰撞器内在刮风下雨 + 移动,进入/离开相机触发范围内时,实时计算混合插值。

后处理动画

到这里,VolumeManager就说完了;我还需要考虑下运行时修改后处理效果参数;
比如我的人物死了,我希望全屏慢慢变灰,或者被攻击的时候屏幕周围冒血光;
玩家在复活点复活,或者离开战斗状态时,就得取消这些状态,得符合GamePlay逻辑;
可以播放插值动画,比如用Timeline来做,可以录制,便于调试(手动找变量很麻烦)。

InitializeCameraData

相机栈的Base相机相比Overlay相机多一步程序:
cameraData = new CameraData(); //使用字段默认值初始化 字段值为0或null
InitializeStackedCameraData(camera, data, ref cameraData)
InitializeAdditionalCameraData(...)
而且后面的Overlay相机全都拷贝一次cameraData,作为初始数据。

CameraData结构

私有成员 get访问

m_ViewMatrix:V矩阵
关联方法:GetViewMatrix

m_ProjectionMatrix:P矩阵
关联方法:GetProjectionMatrix

GetGPUProjectionMatrix方法:获取P矩阵的GPU版本

根据不同设备(OpenGL/DX),反转y(-y)和z(0.5-z/2);

DX平台,UV起点在左上角,绘制到RT时,需要反转;
OpenGL平台,UV起点在左下角,绘制到RT时,不需要反转;
Y轴的反转使图像上下颠倒,这关系到采样:
Unity自动帮我们处理了DX平台的UV反转,使我们采样贴图时不会出错;
这里renderIntoTexture参数判断渲染目标是否为自定义RT正是用于上下翻转图像;
当renderIntoTexture为false时,不会反转Y。
如果RT是用于Compute Shader中按像素序列访问,不需要反转,从左下角开始。

DX的Z范围是(1,0) GL是(-1,1)
是否反转Z与平台有关,与是否绘制到RT无关;
当前平台为GL时,z=mul(P, posWS).z,即裁剪空间中的Z值;
而在shader中,深度测试使用GL规则,默认LessEqual(深度更小才能通过测试);
当前平台为DX时,z = 0.5 - z/2;
比如在Win10,Frame Debugger中观察深度图,越靠近相机的地方越亮(接近1);

IsCameraProjectionMatrixFlipped方法:判断绘制目标是否是RT
SystemInfo.graphicsUVStartsAtTop:判断渲染平台
renderer.cameraColorTarget != BuiltinRenderTextureType.CameraTarget:
判断渲染目标;注意,等式两边类型不同,底层实现了特殊的对比逻辑(黑盒子):

SortingCriteria defaultOpaqueSortFlags:默认渲染顺序

我们在场景中拼命的添加GameObject,但是不一定关心他们的渲染顺序,不管也行;
这个渲染顺序可以综合多种因素,可供用户调整渲染先后顺序:
None:不排序,将任务队列直接丢给GPU;
SortingLayer:一个相机可能兼顾多个Layer;
RenderQueue:材质参数,2000是不透明物体、3000是透明物体这样;
BackToFront:GameObject位置,从后往前排序;
QuantizedFrontToBack:GameObject位置,粗粒度,从前往后排序;
OptimizeStateChanges:尽可能合批,减少渲染状态变化次数;
CommonTransparent:
CanvasOrder:
CommonOpaque:
RendererPriority:

相机总览设置

targetTexture:渲染目标,用户可以将渲染结果保存到指定RT;
cameraType:相机的工作场景:Game/Scene/Preview/VR/Reflection
isSceneViewCamera:
defaultOpaqueSortFlags:排序方式,如果GPU支持排序则可以优化一下;

后处理设置:主要是在相机面板设置

volumeLayerMask
volumeTrigger
isStopNaNEnabled
isDitheringEnabled
antialiasing
antialiasingQuality

输出设置

isHdrEnabled
pixelRect:Camera.rect(百分比版)的像素版矩形,(x起点, y起点, x宽度, y宽度);
pixelWidth:x宽度,左上角的x值为0,右下角的x值为1;
pixelHeight:y宽度,左上角的x值为0,右下角的y值为1;
aspectRatio:x宽度/y宽度
isDefaultViewport:rect要包含整个屏则为true;
renderScale:只在Game视图缩放

VR模式相关参数

isStereoEnabled:双眼渲染
numberOfXRPasses:对于常规渲染,相当于单眼,Pass数量为1;
isXRMultipass:

编辑器模式相关参数

isSceneViewCamera:Scene视图相机;
captureActions:返一个委托,作为录制行为的逻辑入口;

cameraTargetDescriptor:创建渲染目标的RT描述

使用CreateRenderTextureDescriptor方法创建RT描述;
Unity中,RT的底层是黑盒子,所以我们其实不方便直接获得RT的指针、修改RT;
黑盒子的问题,有利也有弊,给用户全部操作权限,反倒是增加了不稳定性;
所以,只能通过有限的接口创建RT、对比、设置,RT描述(值类型)就是其中一种;
通过RT描述(RenderTextureDescriptor),我们可以准确的初始化一个RT;

目标相机设置了渲染目标为自定义的RT,则描述由用户在RT资产面板设置;

如果相机没有设置渲染目标,则渲染到颜色RT和深度RT;
widthheight:取相机的屏幕范围,受到renderScale的影响;
graphicsFormat:选择RT的图形格式,我们自然希望使用HDR格式,精度更高;
如果不需要A颜色通道:B10G11R11_UFloatPack32,32位都分配给RGB三个通道;
如果需要A通道:R16G16B16A16_SFloat,64位分配给RGBA四个通道;
不支持指定格式时:DefaultFormat.HDR,根据平台返回HDR或者LDR格式;
LDR模式:DefaultFormat.LDR,根据平台返回LDR格式;
depthBufferBits:深度缓冲的位数;
sRGB:比如,我们在PS中创建的图片,需要标记为sRGB;
enableRandomWrite:随机读写,默认关闭,用于SM4.5以上的GPU计算;
bindMS:不自动处理MSAA缓冲,默认关闭;
useDynamicScale:动态分辨率,一种性能优化,当GPU负载很高时智能降低分辨率;

注:我们平常采样贴图都是SampleTexture2D,但是MASS缓冲不是Texture2D格式;
所以,开启MSAA后,要对MSAA缓冲进行转换,或者采样multisampled texture;
我没有见过直接采样multisampled texture的代码,一般都是先转换再采样;

注:图形格式(GraphicsFormat)区别于RT格式(RenderTextureFormat),区别待定;
图形格式:只能指定浮点精度和通道
RT格式:可以指定通道和大致精度(half/float)、还包括用途(Depth/hadowmap)

注:现代GPU支持分离颜色缓冲和深度缓冲目标,两者不一定在一个RT;
那么也就不确定,指定了深度缓冲位数之后,是否同时生成颜色缓冲和深度缓冲;
有可能,一个RT包括颜色+深度两个buffer;
也可能是指定RT的RT格式为Depth后,生成指定位数的深度buffer,不包含颜色buffer;
在实际读写的过程中,渲染目标的读和写目标必须相同:
比如,使用A(深度buffer)进行深度测试,测试通过的写入必须保存到A;颜色读写同理;

InitializeAdditionalCameraData:栈内相机的重写数据操作

包括Base相机,也会执行这部分逻辑;

camera:保存相机引用
maxShadowDistance:结合管线阴影开启状态、管线阴影距离设置、相机远截面距离判断;
相机面板中,关闭renderShadows选项,可以使阴影距离为0;
renderType:相机在栈中的类型,Base/Overlay;
clearDepth:只有Overlay相机能可选设置是否清理深度buffer;
postProcessEnabled:一般都开启后处理;Scene视图下不开启后处理;
requiresDepthTexture:一般都会创建深度RT;Overlay清理后继续使用;
requiresOpaqueTexture:可选复制颜色RT;Overlay不使用;
renderer:使用相机指定的Renderer,或者管线默认Renderer;

resolveFinalTarget:相机栈中最后一个相机提交渲染结果;非栈模式时直接提交;

Overlay相机如果是正交的,直接使用自己的P矩阵;
Overlay相机如果是透视的,则与Base相机共用FOV,避免出现显示BUG;

相机数据收集任务到这里就结束了,多数属性都是我们能在编辑面板上直接看到的;
剩下的则是与相机栈的工作模式密切关联的,为混合多个角度的视觉效果做准备;

总结Base和Overlay相机的差别

1.Overlay相机没有渲染到RT的选项,他的渲染目标被Base捆绑了;
相关的输出设置上,Overlay相机都没有自主权(毕竟是用的人家的地);
2.没有限制Overlay相机的投影(占显示器的)范围,用户自己手动调;
3.在渲染逻辑任务上,Overlay与Base相机有相同的可自定义权限;

RenderSingleCamera

这个方法处理Scene/Base/Overlay等相机的渲染任务;
ProfilingSampler sampler:指定性能分析采样器,建议根据相机名采样,便于调试;
整体任务流程是:
⇒ camera.TryGetCullingParameters:获得默认剔除属性cullingParameters(黑盒子);
⇒ ScriptableRenderer.current = renderer;:切换当前渲染方案,供逻辑引用;
⇒ 开始性能采样:cmd.BeginSample(m_Name),添加开始采样命令到CommandBuffer;
⇒ renderer.Clear:根据渲染方案(Forward/2D)重置数据为准备渲染状态;
⇒ renderer.SetupCullingParameters:根据渲染方案(Forward/2D)修改剔除属性;
⇒ context.ExecuteCommandBuffer:第一次提交已有命令(开始采样)给上下文;
⇒ cmd.Clear:清空CommandBuffer;
⇒ EmitWorldGeometryForSceneView:编辑模式下,Scene视图物体额外描边;
⇒ context.Cull:底层执行粗粒度剔除(黑盒子);
⇒ InitializeRenderingData:获取全部渲染一帧所需的数据;
⇒ renderer.Setup:根据渲染方案(Forward/2D)配置Pass队列;
⇒ renderer.Execute:执行Pass队列;
⇒ context.ExecuteCommandBuffer:第二次提交已有命令(结束采样)给上下文;
⇒ CommandBufferPool.Release:回收CommandBuffer;
⇒ context.Submit():提交渲染任务给GPU;
⇒ ScriptableRenderer.current = null;:取消当前渲染方案引用;

Renderer的重写与使用

Unity给我们提供了接口,重写ScriptableRenderer类型,来自定义渲染方案;
URP中已经定义好了ForwardRenderer和Renderer2D,分别对应前向渲染和2D渲染;
这两种方案已经可以面对大多数的情况,比如:
场景中有主相机作为Base,使用前向渲染,渲染主要场景;
UI相机作为Overlay,使用2D渲染,渲染UI(包括小地图等交互原件);
所以,通常来说,2个相机、2种Renderer可以满足简单的移动游戏需求。
渲染过程的多样性可以通过自定义Pass来完成,切换使用多个Renderer实例。

ScriptableRenderer的初始化

在Asset的CreatePipeline方法中,创建管线实例前执行了CreateRenderers方法;
如果用户配置了渲染方案列表,那么m_RendererDataList不为空;
使用ScriptableRendererData的InternalCreateRenderer方法实例化渲染方案;
ScriptableRendererData的子类中保存了渲染方案所需的数据,用于方案的实例化:
public ForwardRenderer(ForwardRendererData data) : base(data)
public Renderer2D(Renderer2DData data) : base(data)
从自构方法中可以看出,有部分数据是属于父级-ScriptableRenderer类型的:
public ScriptableRenderer(ScriptableRendererData data)

数据中包含自定义Pass(ScriptableRendererFeature)列表:rendererFeatures
自定义Pass,是URP提供的主要的自定义管线方式,可以定义一个渲染阶段逻辑;
在初始化Renderer时,初始化Feature(对应着一个Pass),并添加到自身的容器。

相关类型:
ScriptableRendererData
ScriptableRendererFeature
ScriptableRenderPass

ScriptableRendererFeature.Create:自定义Pass初始化接口

这个接口用于定义Feature实例的初始化行为(类似于Awake/Start);
如果在这里声明了变量,基本上是全局缓存的;
比如创建Pass实例,准备Pass需要的必要变量;

ScriptableRendererFeature.AddRenderPasses:自定义Pass执行前接口

这个借口用于定义Feature实例,在执行前的调整(类似于Upadte);
接口中提供了Renderer和RenderingDta,用于判断在本帧中如何预设值Pass;
所以我们需要充分了解这2个类型中的数据结构,便于灵活使用;
执行renderer.EnqueuePass方法,添加到待执行队列;

ScriptableRenderPass的自构

对于一个Pass,需要指定在队列中插入的时机,等待前面的Pass执行完;
同时,Pass对颜色和深度等自定义RT的写入也会影响到后面的Pass;
URP使用RenderPassEvent类型排序,比如:
不透明队列声明在RenderPassEvent.BeforeRenderingOpaques;
我们要定义一个事件,紧跟着不透明队列,可以声明Pass在:
RenderPassEvent.AfterRenderingOpaques

Pass并不是可序列化的类型,所以数据由关联的Featrue实例提供;
在自构时,引用必要的数据,主要是渲染命令需要的RT引用和材质;

ScriptableRenderPass的属性和方法

⇒ 自构方法
renderPassEvent:Pass队列的优先级,自定义Pass插队到指定位置;
colorAttachments:颜色写目标,用于多RenderTarget(MRT)渲染;
colorAttachment:颜色写目标,用于单RenderTarget渲染;
depthAttachment:深度读写目标;
clearFlag:设置RT时,对RT的清理行为;
clearColor:设置RT时,对RT的颜色缓冲清理时指定的颜色值;
eyeIndex:眼睛序列,用于在VR中区分左右眼,默认为0;
overrideCameraTarget:标记Pass是否有特殊的写目标;
ConfigureTarget:提供给Pass外部,用于修改写目标的接口;
ConfigureClear:提供给Pass外部,用于修改设置目标的清理行为;
⇒ Configure:每帧在执行Pass前会调用的接口,可初始化写目标RT;
⇒ FrameCleanup:全部Pass执行完成后,统一调用的清理接口;
OnFinishCameraStackRendering:相机栈渲染执行完成后的清理接口;
⇒ Execute:Pass的主要逻辑,提交CommandBuffer命令;
Blit:一个工具方法,用于复制贴图,同时改变了写入目标;
CreateDrawingSettings:创建批量渲染任务设置;

RenderTargetIdentifier:RT标识符

标识一个RT,可与int(shader变量ID)进行对比;
identifier == 0; //判断RT是否为null
identifier == someInt; //判断两边是否代指同一个RT

RenderTargetIdentifier由Unity底层提供,支持多种自构方式:
1.RenderTexture实例
2.BuiltinRenderTextureType
3.shader id(GetTemporayRT)

RenderTargetHandle:URP中定义的 也是一种RT标识符

RenderTargetHandle.CameraTarget:-1,代表FrameBuffer
非-1值对应shader变量ID,用前先声明:
handle.Init("TemporaryColorTexture")
进行读写前,要先请求临时RT:
cmd.GetTemporaryRT(handle.id, rtDesc, FilterMode.Point);
用完了,记得释放临时RT:
cmd.ReleaseTemporaryRT(handle.id);

BuiltinRenderTextureType:Unity中标记的特殊RT

比如:BuiltinRenderTextureType.CameraTarget
相机目标为null时,代表FrameBuffer;
相机目标为指定RT时,代表指定RT。

关于自定义Feature

URP提供了一个模板,我们可以在资源目录里面右键创建;
在CustomRenderPassFeature.cs中观察Feature的各种接口和说明;
怎么开始写自定义Feature是一个比较头疼的问题,但是不要心急;
在URP中,所有的渲染业务,都可以用Feature表示,先看看主干代码。


关注成长,注重因果。