Custom SRP笔记

Posted by FlowingCrescent on 2021-06-19
Estimated Reading Time 12 Minutes
Words 3k In Total
Viewed Times

本文为个人学习Catlike Coding的教程系列Custom SRP时的学习笔记
​

Custom Render Pipeline - Taking Control of Rendering

搭建了渲染不透明、天空盒、半透明物体以及错误渲染物体的基本渲染管线
此时会发现调用到的API与RenderFeature中的相差无几,或许RenderFeature其实就是作为管线的补充内容。
​

使用C#的partial关键字对类进行分割

catlike使用partial关键字将CameraRenderer类进行了拆分,将Editor模式时才运行的功能拆分了出去,
然后使用#if UNITY_EDITOR以及#endif包含了Editor模式时才运行的代码。
具体可参考官方文档
​

一次DrawRenderers可包含多种ShaderTagId的物体

写法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    static ShaderTagId[] legacyShaderTagIds = {
new ShaderTagId("Always"),
new ShaderTagId("ForwardBase"),
new ShaderTagId("PrepassBase"),
new ShaderTagId("Vertex"),
new ShaderTagId("VertexLMRGBM"),
new ShaderTagId("VertexLM")
};


//.....

var drawingSettings = new DrawingSettings(
legacyShaderTagIds[0], new SortingSettings(camera)
)
{
overrideMaterial = errorMaterial
};

for (int i = 1; i < legacyShaderTagIds.Length; i++)
{
drawingSettings.SetShaderPassName(i, legacyShaderTagIds[i]);
}

var filteringSettings = FilteringSettings.defaultValue;
context.DrawRenderers(
cullingResults, ref drawingSettings, ref filteringSettings
);


就是给DrawingSetting调用SetShaderPassName以增加更多的Pass种类
image.png

这节做完时的效果

​

2021.06.18


Draw Calls - Shaders and Batches

这一节主要是写Shader以及实现批处理,是个人比较熟悉的内容,没什么特别好讲的,但其中有一处细节是第一次知道。
关于Unity各种批处理的详细内容也可以看此文:相关文档

Clear Buffer也是一次DrawCall

第一节中有提到如果Clear的位置在SetUpCameraProperties之前,则会比较低效地调用Shader进行Clear,因此本质上算是一次绘制,这也是比较好理解的
image.png

但之后的快速Clear也是作为一次DrawCall,这在第二节中被提及:
image.png
姑且作为一个小知识记下。

image.png

这节做完时的效果

2021.06.19


Directional Lights - Direct Illumination

这一节主要是实现Directional Light以及BRDF,也就是PBR的直接光照部分,大部分也是我比较熟悉的内容

关于法线插值与归一化

在fragment shader时需要对法线进行归一化也是老生常谈的基础内容,不过个人还是第一次可视化地直观看见artifact,还是挺有趣的:
image.png

NativeArray

Catlike使用NativeArray来储存灯光数据

It’s a struct that acts like an array, but provides a connection to a native memory buffer.** It makes it possible to efficiently share data between managed C# code and the native Unity engine code.**

简单来说就是相比普通的数组,它在C#与Unity底层代码之间传递效率更高。
NativeArray官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void SetupLights()
{
NativeArray<VisibleLight> visibleLights = cullingResults.visibleLights;
int dirLightCount = 0;
for (int i = 0; i < visibleLights.Length; i++)
{
VisibleLight visibleLight = visibleLights[i];
if (visibleLight.lightType == LightType.Directional)
{
SetupDirectionalLight(dirLightCount++,ref visibleLight);
if (dirLightCount >= maxDirLightCount)
{
break;
}
}
}

buffer.SetGlobalInt(dirLightCountId, visibleLights.Length);
buffer.SetGlobalVectorArray(dirLightColorsId, dirLightColors);
buffer.SetGlobalVectorArray(dirLightDirectionsId, dirLightDirections);
}

就用法上而言可以说跟数组相差无几

Shader GUI

其实这是个人第一次做Shader GUI相关的内容,做过一次之后发现其实跟Editor拓展编辑器极为神似,Unity在这方面还是挺方便的。
总之,在Shader结尾写上CustomEditor "CustomShaderGUI"
然后将CustomShaderGUI.cs放入Editor文件夹下表示是个Editor程序即可
最关键的就是调用OnGUI()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
{
//绘制原有的材质GUI
base.OnGUI(materialEditor, properties);

//对自定义GUI所必要的变量进行赋值
editor = materialEditor;
materials = materialEditor.targets;
this.properties = properties;

EditorGUILayout.Space();
//嵌套选项
showPresets = EditorGUILayout.Foldout(showPresets, "Presets", true);
if (showPresets)
{
OpaquePreset();
ClipPreset();
FadePreset();
TransparentPreset();
}
}

​

其对Property的变量进行赋值的做法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool SetProperty(string name, float value)
{
MaterialProperty property = FindProperty(name, properties, false);
if (property != null)
{
property.floatValue = value;
return true;
}
return false;
}
void SetProperty(string name, string keyword, bool value)
{
if (SetProperty(name, value ? 1f : 0f))
{
SetKeyword(keyword, value);
}
}

是Keyword的就增加个Keyword值设置,同时还要将Property的float进行修改。
​
image.png

这节做完的效果

2021.06.21


Directional Shadows - Cascaded Shadow Maps

这一节内容相当多,阴影的原理虽然早就了解,但是实现起来还是比较繁琐复杂。

万能的CullingResults

在前几节的时候还觉得CullingResults类就是单纯的储存被裁剪后的物体的类,但后来发现这个类能做的事情相当多,且与Shadow Map的实现强相关。
​

1
2
3
4
5
cullingResults.ComputeDirectionalShadowMatricesAndCullingPrimitives(
light.visibleLightIndex, i, cascadeCount, ratios, tileSize,
light.nearPlaneOffset, out Matrix4x4 viewMatrix,
out Matrix4x4 projectionMatrix, out ShadowSplitData splitData
);

可通过它自带的ComputeDirectionalShadowMatricesAndCullingPrimitives()获取VP矩阵以及SplitData,可以说有这几项内容之后普通的ShadowMap基本就实现完了……
​

而这个cullingResults的获取也十分简单(取自CameraRenderer.cs):

1
2
3
4
5
6
7
8
bool Cull (float maxShadowDistance) {
if (camera.TryGetCullingParameters(out ScriptableCullingParameters p)) {
p.shadowDistance = Mathf.Min(maxShadowDistance, camera.farClipPlane);
cullingResults = context.Cull(ref p);
return true;
}
return false;
}

最后调用context.DrawShadows进行shadowmap的绘制:

1
2
3
4
5
6
7
8
9
var shadowSettings = new ShadowDrawingSettings(cullingResults, light.visibleLightIndex);

//......

shadowSettings.splitData = splitData;

//......

context.DrawShadows(ref shadowSettings);

可发现这里也还是有cullingResults的存在

Shadow Texture的一些特殊设置

Shadow Texture从它刚申请来的时候就是那么的与众不同:

1
2
3
4
buffer.GetTemporaryRT(
dirShadowAtlasId, atlasSize, atlasSize,
32, FilterMode.Bilinear, RenderTextureFormat.Shadowmap
);

奢华的32bit,还有Unity根据平台自适应的TextureFormat。
​

1
2
3
4
buffer.SetRenderTarget(
dirShadowAtlasId,
RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store
);

关于RenderBufferLoadAction:

This enum describes what should be done on the render target when it is activated (loaded).
When the GPU starts rendering into a render target, this setting specifies the action that should be performed on the existing contents of the surface. Tile-based GPUs may get performance advantage if the load action is Clear or DontCare. The user should avoid using RenderBufferLoadAction.Load whenever possible.

Please note that not all platforms have load/store actions, so this setting might be ignored at runtime. Generally mobile-oriented graphics APIs (OpenGL ES, Metal) take advantage of these settings.

设置为DontCare就是说读取这个RenderBuffer时不对它的内容进行任何处理
​

Shadow Pancaking

由于ShadowCaster渲染时,相机的near plane会尽可能地靠近物体以提高精度,但这也可能导致有些物体的个别顶点比near plane更加靠近相机而被剔除
image.png

然而当物体本身就交错于near plane两边时,还是会存在artifact,这是就要将near plane进行后(向相机)移
image.png

Culling Bias

由于Cascade Shadow Map会在Shadow Texture上多次绘制同一物体,为了减少Draw Call,就尽可能剔除已经在较低层级的Cascade中绘制过了的物体,因此有了Culling Bias的概念。
image.png

image.png

这节做完的效果

2021.06.23

Baked Light - Light Maps and Probes

Light Probe Proxy Volumes

由于光照探针是逐物体使用(一个物体对应一个探针的信息),因此若物体过大,其间接光照信息便容易出错,此时便可以使用Light Probe Proxy Volumes,简称LPPVs。
image.png
image.png
image.png
但Catlike提到,此时探针只会使用L1SH的信息,因此会显得精度较低。这也是能够理解的,毕竟是动态更新,应当尽量减少计算量。

Meta Pass

因为间接漫射光应该受到这些物体表面的反射率的影响,因此需要新定义一个Meta Pass,用来补完烘焙间接光的颜色。
​

其中最重要的就是Fragment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
float4 MetaPassFragment(Varyings input): SV_TARGET
{
float4 base = GetBase(input.baseUV);
Surface surface;
ZERO_INITIALIZE(Surface, surface);
surface.color = base.rgb;
surface.metallic = GetMetallic(input.baseUV);
surface.smoothness = GetSmoothness(input.baseUV);
BRDF brdf = GetBRDF(surface);
float4 meta = 0.0;
if (unity_MetaFragmentControl.x)
{
meta = float4(brdf.diffuse, 1.0);
meta.rgb += brdf.specular * brdf.roughness * 0.5;
meta.rgb = min(
PositivePow(meta.rgb, unity_OneOverOutputBoost), unity_MaxOutputValue
);
}
else if (unity_MetaFragmentControl.y)
{
meta = float4(GetEmission(input.baseUV), 1.0);
}
return meta;
}

其实与普通Pass的Frag没什么特别的区别

image.png

这节做完时的效果

2021.06.27

Shadow Masks - Baking Direct Occlusion

这一节内容较少,主要还是实现Shadow Mask以及Distance Shadow Mask

逻辑或运算符 |

CameraRenderer中有这么一段写法,我一直很在意其中"|"的用法:

1
2
3
4
5
6
7
8
9
10
11
12
var drawingSettings = new DrawingSettings(
unlitShaderTagId, sortingSettings
)
{
enableDynamicBatching = useDynamicBatching,
enableInstancing = useGPUInstancing,
perObjectData =
PerObjectData.Lightmaps | PerObjectData.ShadowMask |
PerObjectData.LightProbe | PerObjectData.OcclusionProbe |
PerObjectData.LightProbeProxyVolume |
PerObjectData.OcclusionProbeProxyVolume
};

​
而PerObjectData是一个枚举类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public enum PerObjectData
{
//
// 摘要:
// Do not setup any particular per-object data besides the transformation matrix.
None = 0,
//
// 摘要:
// Setup per-object light probe SH data.
LightProbe = 1,
//
// 摘要:
// Setup per-object reflection probe data.
ReflectionProbes = 2,
//
// 摘要:
// Setup per-object light probe proxy volume data.
LightProbeProxyVolume = 4,
//
// 摘要:
// Setup per-object lightmaps.
Lightmaps = 8,
//
// 摘要:
// Setup per-object light data.
LightData = 16,
//
// 摘要:
// Setup per-object motion vectors.
MotionVectors = 32,
//
// 摘要:
// Setup per-object light indices.
LightIndices = 64,
//
// 摘要:
// Setup per-object reflection probe index offset and count.
ReflectionProbeData = 128,
//
// 摘要:
// Setup per-object occlusion probe data.
OcclusionProbe = 256,
//
// 摘要:
// Setup per-object occlusion probe proxy volume data (occlusion in alpha channels).
OcclusionProbeProxyVolume = 512,
//
// 摘要:
// Setup per-object shadowmask.
ShadowMask = 1024
}

每个值都是2的n次方
​

官方文档中如此解释"|":
image.png
也就是说,对于c中每一个二进制位,如果ab两者中对应的二进制位有一个是1,则这个二进制位值即为1
如果再形象一些表达,就是一排电闸,每个电闸对应一盏灯,上面perObjectData的赋值其实就是打开对应灯的开关。
还是挺有意思的写法。
​

关于矢量值的通道选择

在进行多光源Shadowmask采样时需要对通道进行选择,Catlike此处使用float4 value[channel]的写法来获取对应的通道值,他也解释了为什么不是手动传入一个float4值来点乘获取(如获取r通道值则传入float4(1, 0, 0, 0)来dot)。
实际上value[channel]的写法,GPU会帮我们编译成之前所说的传入对应通道值为1的float4值进行点乘,如果我们还写成点乘,则还得额外设置一个float4数组,反而变得麻烦了。
image.png

image.png

这节做完时的效果

2021.06.29

LOD and Reflections - Adding Details

这节主要就是实现LOD和反射探针,内容也比较少。

LOD Bias

已知LOD是根据距离判断物体所占屏幕比例大小,来进行模型和贴图的切换以减少渲染开销。而LOD Bias则是对这一比例进行调整:

image.png

LOD Bias = 2.0(默认值)

image.png

LOD Bias = 1.0

可以看出其实就是单纯在比例上增加一个乘法。
如果更感性来说,就是LOD Bias越大,对设备性能要求越高,LOD Bias越低则设备性能要求越低。

LOD Cross Fade

由于单纯进行LOD模型的切换会导致跳变,导致观感较差,因此在切换时也要追求自然,便有了Cross Fade
Cross Fade其实就是对进行切换的两级LOD模型都进行渲染,此时在Shader里通过Clip来进行切换,写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void ClipLOD (float2 positionCS, float fade) {
#if defined(LOD_FADE_CROSSFADE)
float dither = InterleavedGradientNoise(positionCS.xy, 0);
clip(fade + (fade < 0.0 ? dither : -dither));
#endif
}

float4 LitPassFragment(Varyings input): SV_TARGET
{
UNITY_SETUP_INSTANCE_ID(input);

ClipLOD(input.positionCS.xy, unity_LODFade.x);
//......
}

通过算法可以看出,正在消隐的LOD模型,fade值会大于0且不断减小,而正在出现的LOD模型fade值则小于零而不断增加。
​

倒是其中调用的InterleavedGradientNoise函数非常值得挖掘,它被定义在srp自带的Random.hlsl中:

1
2
3
4
5
6
7
8
9
//From  Next Generation Post Processing in Call of Duty: Advanced Warfare [Jimenez 2014]
// http://advances.realtimerendering.com/s2014/index.html
float InterleavedGradientNoise(float2 pixCoord, int frameCount)
{
const float3 magic = float3(0.06711056f, 0.00583715f, 52.9829189f);
float2 frameMagicScale = float2(2.083f, 4.867f);
pixCoord += frameCount * frameMagicScale;
return frac(magic.z * frac(dot(pixCoord, magic.xy)));
}

Animated Cross-Fading

如果只用了Cross Fade而没有开启Animated Cross-Fading,那么当摄像机与物体距离正好在两个LOD层级切换的阈值之间时,便会看见物体上的切换Noise:
image.png

显然我们并不希望玩家看清楚这些玩意,而Animated Cross-Fading便是为此而开发。
如果摄像机达到了LOD切换的距离阈值,那么LOD Group就会用半秒(即LODGroup.crossFadeAnimationDuration的默认值)的时间在两个LOD层级之间切换,不会出现玩家停留在某一距离,而LOD切换也停留在一半的情况。
​
image.png

这节完成时的效果

2021.06.30

​

Complex Maps - Masks, Details, and Normals

这节就是用上各种Texture,没什么特别值得讲的内容
其中提及的相对陌生一些的内容应当也就是关于纹理压缩格式了

DXT5

image.png
根据压缩格式的不同,采用使用的函数也不一样,DXT5(即BC3)会将原本R通道的值移到A通道(8bits),G通道不变(6bits),其余两通道则为5bits。
image.png

2021.08.09

to be continue


感谢您阅读完本文,若您认为本文对您有所帮助,可以将其分享给其他人;若您发现文章对您的利益产生侵害,请联系作者进行删除。