Catlike Coding CustomSRP部分的练习笔记,记录了工程思路、知识点和一些注意事项。跟随的中文翻译版本。英文原教程页面这里
1.自定义渲染管线
- CustomRenderPipeAsset.cs:配置管线的Asset,并返回自定义的管线实例对象,用于创建管线资产。
- CustomRenderPipeline.cs:自定义渲染管线类,负责渲染。渲染方式是调用摄像机的渲染方法。Unity每一帧都会调用Render方法,此方法是SRP的入口方法。
- CameraRenderer:用于管理RenderPipeline中的Camera渲染的辅助类,方便为每个Camera调用渲染方法。分为两个partial。
- CameraRenderer.cs: 管理发行版与开发版中通用的渲染的部分。由Render()方法作为入口开始渲染
- void Cull(): 剔除.相机获取剔除参数,context执行剔除返回剔除结果。
- void SetUp(): 1.为摄像机添加必要的属性,比如VP变换矩阵 2.清除渲染对象:获取相机的clear flags,以便根据相机的clear flag确定清除不清除frame buffer。3.开启性能分析器,采样程序执行过程。
- void Submit(): 提交渲染命令:ScriptableRenderContext发送的命令都是进入缓冲的,需要显式提交命令,发送了缓冲区渲染命令才会进行渲染。有的任务比如天空盒可以直接通过ScriptableRenderContext发送渲染命令,而其他指令需要单独通过CommandBuffer->ScriptableRenderContext的方式间接发出。Commandbuffer是个容器保存了渲染命令。调用
Graphics.ExecuteCommandBuffer()
方法可以立即执行CommandBuffers中的指令而不通过ScriptableRenderContext。1.结束性能分析器采样 2.ScriptableRenderContext.submit()
提交并执行ScriptableRenderContext中的指令,在调用submit之前不会执行任何指令。 - void ExecuteBuffer(): Commandbuffer和ScriptableRenderContext的桥梁,从Commandbuffer向ScriptableRenderContext复制指令,并清除Commandbuffer中的指令缓冲。
- void DrawVisibleGeometry(): 绘制可见物体:需要注意绘制顺序。因为透明物体渲染会关闭深度测试。所以先渲染all过滤对象,然后渲染天空盒,会导致渲染天空盒的时候打开了深度测试,于是覆盖了透明物体。所以注意绘制顺序,不透明物体-天空盒-透明物体。就不会让天空盒覆盖透明物体了。
- CameraRenderer.Editor.cs: 管理编辑器下的开发版的渲染的部分
- void PrepareBuffer(): 为Commandbuffer采样准备SampleName。如果buffername相同的话,两个相机渲染的条目会被合并在默认buffername下,为了区分两个相机,使用相机名字定义缓冲区。为了避免在发布环境下运行时,每帧都访问camera.name,造成开销大,所以在编辑器环境下可以访问camera name,而在发布环境下使用固定字符串。于是给buffername套个皮。只在编辑器下才会为其分配内存。使性能分析器在层级视图和时间轴视图中显示样本。在非开发版中部署时开销为零。
- void DrawGizmos(): 在编辑器状态下绘制Gizmos
- void PrepareForSceneWindow(): 负责在Scene视图中显示UI。默认情况下在Scene视图中看不到UI,此方法调用了ScriptableRenderContext.EmitWorldGeometryForSceneView(camera),在Scene视图下,往Scene场景中发送UI几何体进行渲染,所以需要在剔除前绘制。我们将Canvas的Rendermode设置为了主相机,默认情况下这样做会让相机在渲染透明物体时渲染UI,这就出现了渲染顺序问题。一般情况都是用一个额外的相机独立渲染UI的。
- void DrawUnsupportedShaders(): 默认SRP里大部分Shader都不能用,但是保留了Unlit。所以其他不支持的Shader需要以“错误提示”的方式绘制。
- CameraRenderer.cs: 管理发行版与开发版中通用的渲染的部分。由Render()方法作为入口开始渲染
2.Draw Calls
ShaderLibrary目录
- UnityInput.hlsl: 定义了必要的变换矩阵,这些矩阵会在运行时由Unity传入。其中在CBUFFER的UnityPerDraw缓冲区中,定义了每个实例不同的属性。
- Common.hlsl: 引用了UnityInput.hlsl和其他必要的hlsl库文件,为UnityInput.hlsl的矩阵命名和库文件中的命名提供了宏替换的桥梁。
Shaders目录
- UnlitShader.shader: UnityShader,为材质提供供引用的着色器,内部定义必要的属性、设置和参数,引用UnlitPass.hlsl
- UnlitPass.hlsl: 着色器主要的代码所在
- struct Attributes:顶点着色器输入结构体,内部定义有顶点属性。特别地,为了使用GPU Instancing(后面解释),定义有
UNITY_VERTEX_INPUT_INSTANCE_ID
来标记实例的索引。 - struct Varyings:顶点着色器的输出结构体和片元着色器的输入结构体。其中也定义有
UNITY_VERTEX_INPUT_INSTANCE_ID
- UNITY_INSTANCING_BUFFER:为了使用GPU Instancing而使用的Unity实例缓冲空间,在UnityPerMaterial缓冲区中保存了每个实例所不同的信息。
- Varyings UnlitPassVertex(Atrributes input):顶点着色器主体函数。1.内部对顶点进行MVP变换;2.使用
UNITY_SETUP_INSTANCE_ID(input)
提取输入顶点数据中的实例对象索引数据,并存储到全局静态变量中,并使用UNITY_TRANSFER_INSTANCE_ID(input, output)
将索引数据复制到输出结构体中;3.提取纹理缩放和平移,并处理纹理缩放平移,传递到片元函数中。 - float4 UnlitPassFragment(Varyings input):片元着色器主体函数。1.提取实例索引数据;2.访问UNITY_INSTANCEING_BUFFER中 该实例的材质其他属性;3.透明度测试
- 其他的工作:定义2D纹理,并使用SAMPLER(sampler+纹理名)为其指定采样器。
- struct Attributes:顶点着色器输入结构体,内部定义有顶点属性。特别地,为了使用GPU Instancing(后面解释),定义有
Examples目录
- PerObjectMaterialProperties.cs:允许每个物体可以单独设置材质属性(设置本脚本后,此时SRP Batcher失效了,因为无法处理每个对象的材质属性,缓冲位置都不同了)
- MeshBall.cs:用来生成多个Mesh和多个小球对象,展示很多个实例用GPU Instancing合批的效果。绘制1023个小球产生了3个Draw Call,每个Draw Call的最大缓冲区不一样,需要几个是不同平台决定的。本机每个Draw call最大511实例。
其他修改
为CustomRenderPipeAsset.cs,CustomRenderPipeline.cs,CameraRenderer.cs类增加了设置批处理的开关。
- CameraRenderer.cs:为
void DrawVisibleGeometry()
添加了控制动态批处理和GPU Instancing的开关(修改drawingSettings传入相应参数即可) - CustomRenderPipeAsset.cs:增加了在管线资产的查看面板的选项控件,以控制开启三种批处理。
笔记
批处理
CPU与GPU即独立又并行工作的桥梁是Command Buffer,CPU需要渲染时就向CommandBuffer添加指令,GPU完成上一次渲染任务后就从Command buffer中取一个命令执行,添加和读取是独立的。Command buffer又许多种类,其中包括Draw Call和Set Pass Call。Set Pass Call代表了改变渲染状态,当切换材质或同一材质中Shader的不同Pass进行渲染时会触发Set Pass Call。比如渲染1000相同物体和1000不同物体,DrawCall都是1000,但是前者Set Pass Call是1,后者是1000。切换渲染状态往往比Draw Call更耗时
CPU发送Draw Call需要发送很多内容,Draw Call前CPU的准备工作相比于GPU的执行更耗时,所以如果Draw Call数量过多,则会影响CPU执行速度,拖慢渲染进程。所以要通过批处理(Batching)降低DrawCall数量。相关技术有动态批处理和GPU Instancing。降低Set Pass Call的技术有SRP Batcher。
- SRP Batcher:
一个Drawcall被一个新的材质使用的时候,需要准备进行渲染设置工作,这部分耗时是一个drawcall的主要耗时点,所以如果场景有越多的materials,就会有越多的CPU必须使用去设置GPU 数据。传统的优化做法是减少drawcall数量去提升CPU渲染性能,而实际上真正的CPU消耗来自那些设置工作,而不是GPU drawcall本身。
SRP Batcher不会减少Draw Call数量,但可以减少Set Pass Call数量并减少绘制调用命令的开销。CPU不需要每帧都给GPU发送数据,如果这些数据不变,则会保存在GPU内存中,每次绘制调用只需要一个指向正确内存位置的偏移量。SRP Batcher是否被打断依据是是不是相同的Shader变种,如果材质不同,但Shader变种相同也不会被打断(以往的批处理方法是要求材质相同的)。SRP Batcher会将主存中的坐标、材质、主光源阴影参数、非主光源阴影参数等分别保存在不同的CBUFFER(常量缓冲区)中只有CBUFFER发生变化才会重新向GPU提交。
默认不兼容SRP Batcher,设置方法:1.设置必要的CBUFFER 2.在管线构造函数中启用SRPBatcher
结果:不减少Draw Call,优化了序列,减少了Batch,节省了CPU的渲染准备时间
- GPU Instancing:将数据一次性发到GPU,然后使用一个绘制函数让流水线利用数据绘制多个相同的物体。GPU Instancing能在一次调用中渲染多个相同网格的物体,CPU收集每个物体的材质属性和变换,放入数组发到GPU,GPU遍历数组按顺序渲染。GPU Instancing会把每个实例不同的信息存储在缓冲区(可能是顶点缓冲区,可能是着色器Uniform变量的常量缓冲区中),然后直接操作缓冲区的数据来设置。假设渲染100个相同模型,每个模型有256个面。那么需要两个缓冲区,一个描述顶点信息(储存256个三角形),一个描述各个实例不同的变换信息。
使用方法:1.在Shader的Pass中添加应用GPU Instancing的指令#pragma multi_compile_instancing
。(材质球上能看到开关,此时Unity会为Shader生成两种变体) 2.引入UnityInstancing.hlsl,它重新定义了宏去访问实例的数据数组,需要知道当前渲染对象的索引,该索引是通过顶点数据提供。3.SRP Batcher优先级比较高,不能实现每个物体分别的实例数据,所以把CBUFFER取消,使用UNITY_INSTANCING_BUFFER
保存每个物体的不同材质数据。4.顶点着色器从输入中解析实例索引数据,传入片元着色器。在片元着色器中解析实例索引信息,访问UNITY_INSTANCING_BUFFER
获取保存在其中的该实例的材质数据。
结果:同一材质的多个不同实例减少到一个Draw Call
注意:GPU Instancing 和SRP Batcher二者只能存在其一,SRPBatcher优先级最高,但是建议直接支持GPU Instancing而不支持SRP Batcher
- 动态批处理:每一帧把可以合批的网格模型进行合并,再把合并好的数据传给CPU,然后使用同一个材质渲染,好处是合批的物体仍可以移动,这是由于Unity每帧都会合并。
限制:使用逐对象的材质属性会失效(因为毕竟网格都合批了,没法逐对象),顶点规模也有限制,最大不超过900,适用于共享材质的小型网格。
使用方法:为DrawingSettings
设置属性即可。
透明
在处理透明时,渲染顺序是很重要的。渲染不透明物体时,由于深度缓冲,在不考虑渲染顺序时也能得到正确的渲染结果,此时既开启深度测试Z-test,也开启了深度写入Z-write。而在渲染透明物体时,因为使用透明度混合,我们会关闭深度写入(不更新深度)。
- 透明度测试:片元没有通过测试就会被丢弃,不会对颜色缓冲产生影响。通过测试就按照处理不透明物体的方式处理,正常进行深度测试和深度写入。会使Early-z失效。
使用方法:1.定义阈值属性,将阈值属性放进GPU Instancing缓冲 2.在片元着色器中使用clip函数进行测试透明度 - 透明度混合:使用当前片元的透明度作为混合因子,与颜色缓冲中的颜色进行混合,得到新的颜色。透明度混合需要关闭深度写入(不关闭深度测试,相当于深度缓冲是只读的),所以需要特别注意渲染顺序。
使用方法:1.设置Blend SrcFactor DstFactor指令:properties中设置混合模式,在Pass中定义混合模式。2.增加控制是否写入深度 3.将不透明物体的渲染队列设置为Transparent.
其他
- 使用纹理:1.定义纹理属性,2.在shader中定义纹理并为纹理分配采样器,3.GPU Instancing定义处理纹理的缩放和平移的缓冲 4.顶点着色器中计算缩放和平移,片元着色器中采样纹理颜色。
- Shader Feature:可以让Unity根据不同定义条件或关键字编译多次,生成多个着色器变体,然后通过外部代码或面板设置开关某个关键字加载对应的着色器变种来执行对应的功能,在开发中比较常用。
使用方法:1.增加是否开启的属性 2.使用#pragma为该属性添加shader_feature,如#pragma shader_feature _CLIPPING
3.在shader中通过#if defined()
判断是否被定义,以决定执行什么分支内容
第2章演示代码下载
3.平行光下的材质
使shader受光照影响:1.修改shader的tag,并在DrawSettings中加入对此tag的支持。2.着色器输入加入法线属性。3.新增surface struct,使片元着色器的处理以表面为单位。4.新增Lighting文件,用于根据物体表面信息和光线信息计算最终光照,首先是固定数值光照。
灯光的处理:上面一节是固定数值的表面颜色。现在加入灯光处理使表面受到场景灯光的影响。在本章处理方向光。1.新增Light文件保存灯光和获取灯光属性。2.Lighting中新增光照函数,计算一根光线在表面的颜色分量。
cos(normal,lightdir)*lightcolor
3.新增光线表面着色函数,计算表面本身颜色在一根光线影响下呈现的颜色。lightcolor*surfacecolor
。4.新增表面着色函数,计算所有的光线影响下表面的最终颜色。sum_all(lightcolor*surfacecolor)
。5.向GPU发送灯光数据:GPU开辟缓冲区保存灯光,CPU向GPU发送方向光灯光数量、灯光颜色、灯光方向,GPU从缓冲区中获取灯光数据并使用。(此时只可以发送一个灯光)6.处理可见光,多个光源:从裁剪结果获取可见光,分离出不同种类的灯光,GPU缓冲区以数组方式保存灯光属性,CPU向GPU发送特定种类的灯光数据,GPU遍历计算灯光,得出最终颜色。(其他:使管线支持线性光强,设置着色器编译目标级别以不支持旧的图形API)BRDF:Unity内置管线中支持两种基于物理的工作流:金属工作流和高光反射工作流。其中金属工作流是默认的工作流,对应的Shader是Standard Shader。如果想使用高光反射工作流,需要在Shader的下拉框选择Standard(Specular setup)。需要注意的是,不同的工作流可以实现相同的效果,只是参数不同而已。实际工程中,可以选择偏好的工作流也可以混合着用。这里使用金属工作流。 本章BRDF只是通过材质的金属度和光滑度来控制.Metallic定义了表面更像金属或非金属,1金属0非金属。Smoothness是Metallic的附属值,定义了光滑程度,1完全光滑镜面反射明显,0完全粗糙。
想知道物体表面一点是如何与光交互的,使用BRDF定量分析,大多数情况下,BRDF可以使用来表示,是入射光线方向,是观察方向。BRDF有两种理解方式。第一种是当给出入射角后,BRDF可以给出所有出射方向上的反射和散射光线的相对分布情况;第二种是,当给定观察方向后,即出射方向,BRDF可以给出所有入射方向到该出射方向的光线分布。更直观的理解是,当一束光线沿入射方向l到达表面某点时,表示有多少能量被反射到观察方向上。
做法:我们使用表面的属性计算BRDF,这是漫反射和镜面反射的组合,我们需要把表面颜色分为漫反射和镜面反射两个部分,还需知道表面粗糙度。
- 添加BRDF文件,规定BRDF,根据表面属性获取表面BRDF的函数。
- Lighting中计算光线着色的方法中传入BRDF数据。
- 计算漫反射,BRDF的漫反射就是表面color,替换一下漫反射的计算即可。
- 表面反射计算。当使用金属工作流时,物体表面对光线的反射率会受到金属度的影响,金属度越大,自身反照率(albedo)颜色越不明显,对周边环境的反射越清晰。所以修改获取漫反射BRDF函数, 得到不反射率,表面自身的漫反射需要乘不反射率加权(合理)。考虑到一些绝缘体也会有光从表面反射出来(高低也得有一点反射率),所以没有完美的绝对不反射的物体,所以不反射率是有上限的。镜面反射:考虑到能量守恒,表面颜色 = 漫反射+镜面反射。同时考虑到,金属度影响镜面反射颜色,非金属镜面反射是白色(单通道),所以要在最小反射率和表面颜色之间用金属度(镜面反射率)插值。
- 粗糙度和光滑度相反,1-smoothness,通过源码的方法用光滑度得到粗糙度,然后用源码的方法将粗糙度平方,得到真实粗糙度,与迪士尼的模型匹配。
- 获取相机位置,与世界坐标的表面计算,得到视角方向。使视角方向成为表面的属性。7.镜面反射强度:取决于视角方向和完美反射方向的对齐程度。计算公式:,粗糙度, 表面法线, 光照方向, 视角方向, 中间向量,归一化项.
注:这里BRDF使用金属工作流,只有金属度和粗糙度两个参数,计算采用了cook-Torrance BRDF的简略版。主要得到反射的颜色,即光照项,包括漫反射和镜面反射;主要得到反射率,即BRDF项。
具体细节请见:基于物理的渲染:微平面理论(Cook-Torrance BRDF推导)
透明度:当前随alpha变小,镜面反射也会消失,整体都会变得透明,这是不对的,实际情况下镜面反射不会消失。现在还不能做到这一点。我们想要的结果是调整alpha只改变漫反射的颜色,镜面反射不变。通过预乘alpha:把RGB通道也乘上alpha比例,变成,好处是可以让两个像素之间线性插值后颜色更加合理,使带透明通道图片的纹理可以正常的线性插值。
ShaderGUI:我们的材质支持多种模式,切换材质模式需要同时修改一些参数,使用ShaderGUI对材质面板拓展,一键配置参数完成调节。
- 使用CustomEditor拓展材质面板
- 创建子文件夹Editor,创建脚本CustomShaderGUI.cs,用于便利地调整属性
Reference:
[1] 基于物理的渲染:微平面理论(Cook-Torrance BRDF推导) - 知乎 (zhihu.com)