https://catlikecoding.com/unity/tutorials/custom-srp/multiple-cameras/
多摄像机混合与渲染层级
-
用不同的 post FX settings 渲染多个摄像机
-
Layer cameras with custom blending
-
Support rendering layer masks
-
Mask lights per camera

1 Combining Cameras
每个摄像机都要处理裁剪,光照,阴影渲染,因此每一帧渲染的摄像机越少越好,最好是只有一个。但有时确实需要同时在不同的视角下进行渲染。例如分屏的多人游戏,后视镜,俯视图,游戏内的摄像机,3D角色展示等。
1.1 Split Screen
让我们从分屏应用开始,由两个并排的摄像机组成。左边的摄像机的 viewport 的 width 是 0.5,右边的摄像机 viewport width 也是 0.5,同时 x postion 也是 0.5。当没有后效时,一切看起来就是我们想要的

当启用后效,就会出问题,两个摄像机都用正确的尺寸进行了渲染,但是最后覆盖到整个屏幕(frame buffer),因此只能看到第二个摄像机的渲染。

这是因为调用 SetRenderTarget 时,会重置 viewport,因此,我们只需要在 post FX 最后渲染到 frame buffer 时,调用 SetViewport 设置正确的 viewport 即能解决该问题,为此我们定义新的 DrawFinal 来完成最后的上屏渲染:
cs
void DoColorGradingAndToneMapping(int sourceId)
{
...
//Draw(sourceId, BuiltinRenderTextureType.CameraTarget, Pass.Final);
DrawFinal(sourceId);
buffer.ReleaseTemporaryRT(colorGradingLUTId);
}
void DrawFinal(RenderTargetIdentifier from)
{
buffer.SetGlobalTexture(fxSourceID, from);
buffer.SetRenderTarget(BuiltinRenderTextureType.CameraTarget,
RenderBufferLoadAction.DontCare,
RenderBufferStoreAction.Store);
buffer.SetViewport(camera.pixelRect);
buffer.DrawProcedural(Matrix4x4.identity,
settings.Material, (int)Pass.Final, MeshTopology.Triangles, 3);
}

如果用的是 tile-based GPU,某些平台下,在 viewport 周围会有一些错误的像素,超出边界。这是因为标记出的 tile regions 中有垃圾数据。如果 viewport 不是完整的,我们设置 RT 是要求加载该 RT 来解决。如果没遇到该问题则可以忽略
cs
static Rect fullViewPort = new Rect(0f, 0f, 1f, 1f);
void DrawFinal(RenderTargetIdentifier from)
{
buffer.SetGlobalTexture(fxSourceID, from);
buffer.SetRenderTarget(BuiltinRenderTextureType.CameraTarget,
camera.pixelRect == fullViewPort ? RenderBufferLoadAction.DontCare : RenderBufferLoadAction.Load,
RenderBufferStoreAction.Store);
buffer.SetViewport(camera.pixelRect);
buffer.DrawProcedural(Matrix4x4.identity, settings.Material, (int)Pass.Final, MeshTopology.Triangles, 3);
}
1.2 Layering Cameras
除了渲染到不同的区域,还可以让摄像机叠加渲染。最简单的例子是让第一个摄像机正常渲染。第二个摄像机使用较小的 viewport ,比如设置其尺寸为 0.5,并且设置其 xy 值 为 0.25,使其渲染在中间:

如果没有开启后效,通过设置可以让上面的摄像机仅 clear depth,这样下面的部分就能显示出来了,某些叠加渲染可能会有这样的需求。

但是如果开启了后效,那么就又不对了,因为后效渲染时时,强制设置了 CameraClearFlags.Color。

解决该问题,首先,设置 final pass 为 alpha 混合模式,并且在 set render target 是,load action 使用 load
cs
Pass
{
Name "Final Pass"
Blend SrcAlpha OneMinusSrcAlpha
HLSLPROGRAM
#pragma target 3.5
#pragma vertex DefaultPassVertex
#pragma fragment FinalPassFragment
ENDHLSL
}
void DrawFinal(RenderTargetIdentifier from)
{
buffer.SetGlobalTexture(fxSourceID, from);
//RenderBufferLoadAction loadAction = camera.pixelRect == fullViewPort ? RenderBufferLoadAction.DontCare : RenderBufferLoadAction.Load;
RenderBufferLoadAction loadAction = RenderBufferLoadAction.Load; // 总是 load
buffer.SetRenderTarget(BuiltinRenderTextureType.CameraTarget,
loadAction,
RenderBufferStoreAction.Store);
buffer.SetViewport(camera.pixelRect);
buffer.DrawProcedural(Matrix4x4.identity, settings.Material, (int)Pass.Final, MeshTopology.Triangles, 3);
}
现在设置上层摄像机的 clear color 的 alpha 为 0(Cmera.ClearFlags 设置为 solid color 后的 background 颜色),关闭 bloom(设置迭代次数为0),则可以混合下层摄像机。但是如果开启 bloom 依然还是不对,如下图

原因是目前的 bloom 没有保留透明度,通过在 bloom 的 final pass 中,保留高分辨率图的透明度来解决。需要修改两种模式: BloomAddPassFragment 和 BloomScatterFinalPassFragment。
cs
float4 BloomAddPassFragment(Varyings input) : SV_TARGET
{
float3 lowRes;
if (_BloomBicubicUpsampling)
lowRes = GetSourceBicubic(input.screenUV).rgb;
else
lowRes = GetSource(input.screenUV).rgb;
float4 highRes = GetSource2(input.screenUV);
return float4(lowRes * _BloomIntensity + highRes.rgb, highRes.a);
}
float4 BloomScatterFinalPassFragment(Varyings input) : SV_TARGET
{
float3 lowRes;
if (_BloomBicubicUpsampling)
lowRes = GetSourceBicubic(input.screenUV).rgb;
else
lowRes = GetSource(input.screenUV).rgb;
float4 highRes = GetSource2(input.screenUV);
lowRes += highRes.rgb - ApplyBloomThreshold(highRes.rgb);
return float4(lerp(highRes.rgb, lowRes, _BloomIntensity), highRes.a);
}

现在开启 bloom 时半透明也能正常工作了,但是 bloom 在透明区域消失了。可以通过将 final pass 的透明模式改为 premultiplied alpha blending 来解决,这同时需要设置摄像机的 solid color 为黑色,因为该颜色会和下面的摄像机的渲染叠加。
cs
Pass
{
Name "Final Pass"
Blend One OneMinusSrcAlpha
HLSLPROGRAM
#pragma target 3.5
#pragma vertex DefaultPassVertex
#pragma fragment FinalPassFragment
ENDHLSL
}

1.3 Layered Alpha
当前分层渲染的方法,只有在我们的 shader 输出一个合理的 alpha 给摄像机用来混合时,才是正确的。因为没有用到,因此我们不关心之前写入的 alpha 值。如果有两个alpha 都是 0.5 的立方体渲染到同一个像素,那么该像素的 alpha 值将会是 0.25。同时如果在同一个像素上,有任何一次渲染的 alpha 是 1,那么最终也应该是 1;如果后面渲染的 alpha 是 0,那么应该保留之前的 alpha。通过设置 alpha 混合模式为 One OneMinusSrcAlpha 就可以解决该问题。修改 lit.shader 和 unlit.shader 文件:
cs
// 逗号前是 color blend mode,逗号后是 alpha blend mode
Blend [_SrcBlend] [_DstBlend], One OneMinusSrcAlpha
当 alpha 值正常时,这种方法是可以工作的,也就是说只要像素写深度了,那么 alpha 一定是 1。对于不透明材质,这看起来没问题。但是如果材质的 BaseMap 贴图中,含有不同的 alpha 值,那么就会出错。对于 clip 材质,由于依赖 alpha 作为阈值进行裁剪,因此也会出错。如果像素被裁剪掉了就没问题,否则它的 alpha 应该是 1。
最简单的解决办法是将是否写 z 的标记,作为一个常量给 shader,当写 z 时,alpha 强制为 1。
首先为 LitInput 和 UnlitInput 定义 UnityPerMaterial 属性,并定义 GetAlpha 方法,在 lit/unlit pass fragment 返回时,调用 GetAlpha 确定 alpha 值:
cs
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
...
UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff)
UNITY_DEFINE_INSTANCED_PROP(float, _ZWrite)
...
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
...
float GetAlpha(float alpha)
{
return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _ZWrite) ? 1.0 : alpha;
}
float4 LitPassFragment(Varyings input) : SV_TARGET
{
...
return float4(color.rgb, GetAlpha(surface.alpha));
}
float4 UnlitPassFragment(Varyings input) : SV_TARGET
{
...
return float4(base.rgb, GetAlpha(base.a));
}
1.4 Custom Blending
只有 overlay 摄像机混合到前面的摄像机才有合理性。底部的相机(应该是第一个渲染的相机?)将与相机Target的初始内容相融合,这些内容要么是随机的,要么是之前帧的累积,除非编辑器提供了一个已清空的目标(通常都会清空吧)。因此第一个相机应该使用 One Zero 混合模式。为支持替换,覆盖,以及更多特别的覆盖模式,当FX启用时,我们需要为摄像机增加最终混合模式的配置。创建可序列化的 CameraSettings 配置类,为了方便,在 FinalBlendMode 结构体中包装 源 和 目标 混合模式,然后设置默认值为 One Zero。
cs
[Serializable]
public class CameraSettings
{
[Serializable]
public struct FinalBlendMode
{
public BlendMode source, destination;
}
public FinalBlendMode finalBlendMode = new FinalBlendMode
{
source = BlendMode.One,
destination = BlendMode.Zero
};
}
因为无法直接给 Camera 组件增加设置,因此需要定义新的组件来实现配置功能,该组件只能添加到有 Camera 组件的对象上,同时只能有一个该类型组件。定义 CameraSettings 属性,以及 getter。由于 CameraSettings 是类类型,因此在 getter 中根据需要进行创建。
cs
[DisallowMultipleComponent, RequireComponent(typeof(Camera))]
public class CustomRenderPipelineCamera : MonoBehaviour
{
[SerializeField]
CameraSettings settings = default;
public CameraSettings Settings => settings ?? (settings = new CameraSettings());
}
我们可以在 CameraRenderer.Render 实现开始的地方,获取 CustomRenderPipelineCamera 组件,如果没有添加该组件,则用默认配置对象,然后将 FinalBlendMode 传递给 PostFXStack:
cs
// 默认的相机混合设置
static CameraSettings defaultCameraSettings = new CameraSettings();
public void Render(ScriptableRenderContext context,
Camera camera,
bool useDynamicBatching,
bool useGPUInstancing,
bool usePerObjectLights,
ShadowSettings shadows,
PostFXSettings postFXSettings,
bool allowHDR,
int colorLUTResolution
)
{
...
// 设置后处理相关参数
var crpCamera = camera.GetComponent<CustomRenderPipelineCamera>();
CameraSettings cameraSettings = crpCamera ? crpCamera.Settings : defaultCameraSettings;
postFXStack.Setup(context, camera, postFXSettings, useHDR, cameraSettings.finalBlendMode);
...
}
PostFXStack 接收 FinalBlendMode 并存储下来
cs
CameraSettings.FinalBlendMode finalBlendMode;
...
public void Setup(ScriptableRenderContext context,
Camera camera, PostFXSettings settings, bool useHDR,
CameraSettings.FinalBlendMode finalBlendMode)
{
this.context = context;
this.camera = camera;
this.useHDR = useHDR;
this.finalBlendMode = finalBlendMode;
// 只在Scene视图和Game视图中启用后效
this.settings = camera.cameraType <= CameraType.SceneView ? settings : null;
ApplySceneViewState();
}
然后,在 DrawFinal 的最后,设置 _FinalSrcBlend 和 _FinalDstBlend shader 属性。同时,如果目标混合模式不是 0,则需要加载 target
cs
int finalSrcBlendId = Shader.PropertyToID("_FinalSrcBlend");
int finalDstBlendId = Shader.PropertyToID("_FinalDstBlend");
...
void DrawFinal(RenderTargetIdentifier from)
{
buffer.SetGlobalFloat(finalSrcBlendId, (float)finalBlendMode.source);
buffer.SetGlobalFloat(finalDstBlendId, (float)finalBlendMode.destination);
buffer.SetGlobalTexture(fxSourceID, from);
RenderBufferLoadAction loadAction = RenderBufferLoadAction.Load;
if (finalBlendMode.destination == BlendMode.Zero &&
camera.pixelRect == fullViewPort)
{
loadAction = RenderBufferLoadAction.DontCare;
}
buffer.SetRenderTarget(BuiltinRenderTextureType.CameraTarget,
loadAction,
RenderBufferStoreAction.Store);
buffer.SetViewport(camera.pixelRect);
buffer.DrawProcedural(Matrix4x4.identity, settings.Material, (int)Pass.Final, MeshTopology.Triangles, 3);
}
最后在pass定义中用属性来替换硬编码的混合模式
cs
Pass
{
Name "Final Pass"
//Blend One OneMinusSrcAlpha
Blend [_FinalSrcBlend] [_FinalDstBlend]
HLSLPROGRAM
#pragma target 3.5
#pragma vertex DefaultPassVertex
#pragma fragment FinalPassFragment
ENDHLSL
}
现在,如果摄像机没有我们的配置组件,由于默认是 One Zero 混合模式,那么就会覆盖之前的渲染。上层的摄像机需要通过配置组件,设置需要的混合模式,典型的如 One OneMinusSrcAlpha

1.5 Render Texture
除了分屏和叠加摄像机,还有一种常用摄像机是游戏内显示,或用于UI显示的摄像机,这类情况需要将图像渲染到 render texture 上,render texture 可以是资产,也可以运行时动态创建。例如通过 Assets/Create/Render Texture 菜单功能,创建一个 200x100 的 render texture 。因为要用带后效的摄像机渲染,这会创建带有深度的中间 render texture,因此这个新建的 render textur 没有指定 depth buffer。

然后创建一个摄像机,渲染到上面的 render texture 上。

正常情况下,最下面的摄像机要设置为 One Zero 最终混合模式。编辑器初始时提供的是清理为黑色的 texture,但是之后,其内容就是最后一次渲染的结果了。多个相机可以渲染到同一个 render texture 上,同时可以设置不同的 viewport。唯一的不同是 Unity 会自动优先渲染那些渲染到 render texture 的相机,之后才是渲染到屏幕的相机,即,首先,具有目标纹理的摄像机会按照深度递增的顺序进行渲染,然后是那些没有纹理的摄像机。
1.6 Unity UI
Render Texture 可以像普通贴图一样使用,通过 GameObject/UI/Raw Image 创建一个图片UI控件,就可以指定 RT 进行显示。

raw image 使用默认的 UI 材质,执行的是 SrcAllpha OneMinusSrcAlpha 混合,因此半透明显示是没问题的,但是 bloom 不是叠加的,而且除非贴图像素和屏幕像素完美匹配(一样大小),否则 bilinear filtering 会使黑色的渲染背景被显示出来,导致半透明边缘出现黑边(如上图)。
因此我们需要自定义 UI shader 来支持其它混合模式。通过拷贝 Default-UI shader,并通过 _SrcBlen 和 _DstBlend shader 属性,添加可配置的混合模式。
cs
Shader "Custom RP/UI Custom Blending"
{
Properties
{
...
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
[Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("Src Blend", Float) = 1
[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Dst Blend", Float) = 0
}
SubShader
{
...
Blend [_SrcBlend] [_DstBlend]
ColorMask [_ColorMask]
...
通过链接 Unity's download archive,先选择大版本(2022,2023等),在所有子版本列表中,找到自己版本号的 Unity 版本,点击 Downloads 列的 See all,打开该版本所有相关下载。点击 Other installs,下载 Shaders。下载完成解压,shader 在 DefaultResourcesExtra/UI 目录下

我下载的是 2022.3.62f3 版本的 shader,结果发现其本身就是 One OneMinusSrcAlpha 的,而在我的 unity 内,确实也没发现黑边。不过我还是替换了我们自己的可配置 blend mode 的 shader,最后发现没什么变化
1.7 Post FX Setting Per Camera
下面要支持的特性是,当有多个摄像机时,每个摄像机的后效应该可以分别配置,因此为 CameraSettings 增加一个是否覆盖管线后效配置的开关,以及后效配置数据对象。
cs
[Serializable]
public class CameraSettings
{
...
public bool overridePostFX = false;
public PostFXSettings postFXSettings = default;
public FinalBlendMode finalBlendMode = new FinalBlendMode{...};
}

在 CameraRenderer.Render 接口中,检查是否覆盖后效配置,如果覆盖,且指定的后效对象有效,则使用指定的后效配置对象进行渲染。
cs
public void Render(ScriptableRenderContext context,
Camera camera,
bool useDynamicBatching,
bool useGPUInstancing,
bool usePerObjectLights,
ShadowSettings shadows,
PostFXSettings postFXSettings,
bool allowHDR,
int colorLUTResolution
)
{
...
// 设置后处理相关参数
var crpCamera = camera.GetComponent<CustomRenderPipelineCamera>();
CameraSettings cameraSettings = crpCamera ? crpCamera.Settings : defaultCameraSettings;
if(cameraSettings.overridePostFX && postFXSettings != null)
postFXSettings = cameraSettings.postFXSettings;
postFXStack.Setup(context, camera, postFXSettings, useHDR, cameraSettings.finalBlendMode);
...
}

2 Rendering Layers
当同时渲染多个摄像机视口时,我们可能希望每个摄像机渲染不同的场景。比如,我们可能会渲染主视口和角色肖像。Unity 同时只支持一个全局的场景(不是有 sub scene 了么?当然跟该主题无关),因此需要限制每个摄像机能看到场景中不同对象。
2.1 Culling Masks
下面只是分析,culling masks 不好用,为引入 rendering layers 做铺垫。
每个游戏对象只能指定属于一个 layer。通过编辑器右上角的 Layers 下拉框,可以指定显示/隐藏哪些 layer。

同样,每个摄像机有 Culling Mask 属性,用来限制该摄像机会渲染哪些 layer,该 mask 是在渲染的 culling 阶段应用的。
每个对象属于一个 layer,culling masks 可以包含多个 layers。例如,有两个摄像机,它们都渲染 Default layer,同时一个摄像机还渲染 Ignore Raycasts layer,另一个还渲染 Water layer。所以一些对象被两个摄像机渲染,还有一些对象仅被其中一个相机渲染。

灯光也有 Culling Masks,当一个对象被某个灯光裁掉,则该对象不会被这个灯光照亮,也不会投射阴影。但是,如果我们为方向光配置 culling masks,会发现仅仅是阴影收到 culling masks 的影响,此外还是会被该光源照亮。

对于其它类型的光源,如果管线的 Use Lights Per Object 选项被关闭,则也会有类似的问题:

但是如果开启了 Use Lights Per Object 选项,则点光和聚光灯的 culling masks 会正常工作,但是方向光依然不行。

导致这些问题的原因是因为,Unity 是在向 GPU 上传每个对象的光源索引时才应用 culling mask,而我们没有使用这些数据,因此 culling 不起作用。而对于方向光,则永远不会起作用,因为我们总是将方向光应用到所有对象。由于阴影渲染时,是在光源位置构建摄像机,用摄像机的裁剪逻辑,因此阴影渲染的裁剪是正确的。
上面的问题在我们的RP中没办法解决,HDRP 也有同样的问题:光源不支持 culling masks。Unity 为 SRP 提供了 Rendering Layers 来解决该问题。使用 rendering layers 有两个好处:首先, renderer 可以不必限制只能在一个 layer,这带来更多灵活性。其次,rendering layers 不可以用在其它类型的对象,比如 default layers 可以用在物理组件上。
在继续 rendering layers 之前,如果光源设置了 culling masks 为 Everything 之外的值,则在光源的 inspector 面板上显示警告信息。光源的 cullingMask 属性,当值为 -1 时表示所有 layers。在 CustomLightEditor 中如果选中的 light 的该属性不是 -1 则显示警告。对于 point 和 spot 光源,当 Use Lights Per Object 选项开启时,效果是正确的,不需要显示警告
cs
public class CustomLightEditor : LightEditor
{
public override void OnInspectorGUI()
{
...
// 方向光的 culling masks 只能是 Everything
// 点光源和聚光灯,只有在管线的 UsePerObjectLights 开启时,才允许修改 culling masks
var light = target as Light;
if(light.cullingMask != -1)
{
string warning = "";
if(light.type == LightType.Directional)
{
warning = "Directional lights must use 'Everything' Culling Mask.";
EditorGUILayout.HelpBox(warning, MessageType.Warning);
}
else if(light.type == LightType.Point || light.type == LightType.Spot)
{
var rpAsset = GraphicsSettings.currentRenderPipeline as CustomRenderPipelineAsset;
if(rpAsset == null || !rpAsset.usePerObjectLights)
{
warning = "Culling Mask for Point and Spot lights requires 'Use Per Object Lights' option enabled in the Render Pipeline Asset.";
EditorGUILayout.HelpBox(warning, MessageType.Warning);
}
}
}
}
}
2.2 Adjusting the Rendering Layer Mask
当使用 SRP 时,Lights 和 MeshRenderer 组件会在 inspector 上显示 Rendering Layer Mask 属性

下拉列表中默认有 32 个 layers,名字分别是 Layer1, Layer2,...。每个 RP 可以自己配置这些名字,通过 RenderPipelineAsset.renderingLayerMaskNames getter 属性返回。因为只有编辑器才会调用该接口,因此将 CustomRenderPipelineAsset 改为 partial 类。然后创建编辑器脚本,返回这个 string[] 属性。通过创建静态构造函数来初始化静态成员,以 "Layer 1" 的形式作为名字。
实践过程中,发现重载 renderingLayerMaskNames 没有效果,重载 prefixedRenderingLayerMaskNames 才有效果,如下代码:
cs
public partial class CustomRenderPipelineAsset : RenderPipelineAsset
{
#if UNITY_EDITOR
//static string[] renderingLayerNames;
static string[] prefixedRenderingLayerNames;
static CustomRenderPipelineAsset()
{
//renderingLayerNames = new string[32];
prefixedRenderingLayerNames = new string[32];
for (int i = 0; i < 32; i++)
{
//renderingLayerNames[i] = $"Layer - {i + 1}";
prefixedRenderingLayerNames[i] = $"Layer {i + 1} ({i})";
}
}
// 没有效果
//public override string[] renderingLayerMaskNames => renderingLayerNames;
// 实际上需要重载改属性
public override string[] prefixedRenderingLayerMaskNames => prefixedRenderingLayerNames;
#endif
}
我们只是改了个名字,对于 MeshRenderer 组件是生效的,但是对于 Light 当我们在下拉框编辑 layers 时,发现无法进行编辑,这个问题现在没有修复的办法,但是我们可以定义一个我们自己版本的属性,这样就可以编辑了。
-
首先创建 label content
-
然后实现 DrawRenderingLayerMask 方法,绘制,并实现属性的编辑功能
-
最后在 OnInspectorGUI 中调用我们的编辑方法
cs
public class CustomLightEditor : LightEditor
{
// 创建 Rendering layer mask label
static GUIContent renderingLayerMaskLabel =
new GUIContent("Rendering Layer Mask", "Functional version of above property.");
public override void OnInspectorGUI()
{
// 依然用默认方法绘制 Light 编辑面板
base.OnInspectorGUI();
DrawRenderingLayerMask();
// 判断选中的光源,全都是 spot 类型
// 选中的 Light 的属性会被序列化缓存,settings 提供了访问缓存属性的接口
if (!settings.lightType.hasMultipleDifferentValues
&& (LightType)settings.lightType.enumValueIndex == LightType.Spot)
{
// 绘制 inner / outer 角编辑控件
settings.DrawInnerAndOuterSpotAngle();
}
// 应用修改后的数据
settings.ApplyModifiedProperties();
...
}
void DrawRenderingLayerMask()
{
SerializedProperty property = settings.renderingLayerMask;
EditorGUI.showMixedValue = property.hasMultipleDifferentValues;
EditorGUI.BeginChangeCheck();
int mask = property.intValue;
if (mask == int.MaxValue)
mask = -1;
mask = EditorGUILayout.MaskField(renderingLayerMaskLabel, mask,
GraphicsSettings.currentRenderPipeline.prefixedRenderingLayerMaskNames);
if (EditorGUI.EndChangeCheck())
{
property.intValue = mask == -1 ? int.MaxValue : mask;
}
EditorGUI.showMixedValue = false;
}
}
尽管我们正确的编辑了光源的 Rendering Layer Mask,但是因为我们还没有应用该 mask ,所以看不到效果。可以通过在 Shadows 中启用 ShadowDrawingSettings. useRenderingLayerMaskTest 来应用。所有的光源都需要处理,包括 RenderDirectionalShaodw, RenderSpotShadows, RenderPointShadows。这样就可以通过 Rendering layer masks 来处理光源和对象的阴影了。
cs
void RenderDirectionalShadows(int index, int split, int tileSize)
{
ShadowedDirectionalLight light = directionalLights[index];
// 设置阴影渲染参数
var shadowSettings = new ShadowDrawingSettings(...);
shadowSettings.useRenderingLayerMaskTest = true;
...
2.3 Sending a Mask to the GPU
为了能我们的 Lit.shader 也能处理对象和光源的 Rendering layer mask,就需要将 mask 上传到 GPU。在 UnityInput 中的 UnityPerDraw 结构体中,增加属性来接收 mask
cs
CBUFFER_START(UnityPerDraw)
...
real4 unity_WorldTransformParams;
// rendering layer masks
float4 unity_RenderingLayer;
...
在 Surface 结构体中,增加 mask 成员,并在 LitPassFragment 中为其赋值
cs
surface.dither = InterleavedGradientNoise(input.positionCS.xy, 0);
// asuint 直接以原始数据作为无符号整数,避免先解释成浮点数再转无符号整数
surface.renderingLayerMask = asuint(unity_RenderingLayer.x);
同时为光源增加成员:
cs
struct Light
{
...
uint renderingLayerMask;
};
光源的 rendering layer mask 需要由我们自己上传到 GPU,我们将其存储在光源方向的第4个分量上,并在获取光源数据时赋值
cs
CBUFFER_START(_Lights)
// 方向光数量
int _DirLightCount;
float4 _DirLightColors[MAX_DIR_LIGHT_COUNT];
float4 _DirLightDirectionsAndMasks[MAX_DIR_LIGHT_COUNT];
...
float4 _OtherLightDirectionsAndMasks[MAX_OTHER_LIGHT_COUNT];
...
CBUFFER_END
...
Light GetDirectionalLight(int index, Surface surfaceWS, ShadowData shadowData)
{
...
light.renderingLayerMask = asuint(_DirLightDirectionsAndMasks[index].w);
return light;
}
...
Light GetOtherLight(int index, Surface surfaceWS, ShadowData shadowData)
{
...
light.renderingLayerMask = asuint(_OtherLightDirectionsAndMasks[index].w);
float rangeAttenuation = Square(saturate(1.0 - Square(distSqr*_OtherLightPositions[index].w)));
...
}
在 cpu 端进行赋值。但是要注意,我们不能直接将 masks 赋值给 float,需要通过辅助函数来完成(类似C++的union,C#没有该特性,但是我们可以模拟):
cs
public static class ReinterpretExtensions
{
[StructLayout(LayoutKind.Explicit)]
struct IntFloat
{
[FieldOffset(0)]
public int intValue;
[FieldOffset(0)]
public float floatValue;
}
public static float ReinterpretAsFloat(this int value)
{
IntFloat v = default;
v.intValue = value;
return v.floatValue;
}
}
private void SetupDirectionalLight(int index, int visibleIndex, ref VisibleLight visibleLght, Light light)
{
dirLightColors[index] = visibleLght.finalColor;
dirLightDirections[index] = -visibleLght.localToWorldMatrix.GetColumn(2);
dirLightShadowData[index].w = light.renderingLayerMask.ReinterpretAsFloat();
dirLightShadowData[index] = shadows.ReserveDirectionalShadows(light, visibleIndex);
}
private void SetupPointLight(int index, int visibleIndex, ref VisibleLight visibleLght, Light light)
{
...
otherLightShadowData[index] = shadows.ReserveOtherShadows(light, visibleIndex);
otherLightDirections[index] = Vector4.zero;
otherLightDirections[index].w = light.renderingLayerMask.ReinterpretAsFloat();
}
private void SetupSpotLight(int index, int visibleIndex, ref VisibleLight visibleLght, Light light)
{
otherLightPositions[index].w = 1.0f / Mathf.Max(visibleLght.range * visibleLght.range, 0.000001f);
otherLightDirections[index] = -visibleLght.localToWorldMatrix.GetColumn(2);
otherLightDirections[index].w = light.renderingLayerMask.ReinterpretAsFloat();
...
}
在 Lighting.hlsl 中,定义表面和光源 rendering layer mask 是否相交的方法,在计算光照时,先做判断:
cs
bool RenderingLayerOverlap(Surface surface, Light light)
{
return light.renderingLayerMask & surface.renderingLayerMask != 0;
}
...
float3 GetLighting(Surface surfaceWS, BRDF brdf, GI gi)
{
...
for(int i = 0; i < GetDirectionalLightCount(); ++i)
{
Light light = GetDirectionalLight(i, surfaceWS, shadowData);
if (RenderingLayerOverlap(surfaceWS, light))
color += GetLighting(surfaceWS, brdf, light);
}
#if defined(_LIGHTS_PER_OBJECT)
// 每个对象定义了影响的光源
// y 可能大于8,而我们最多支持8个,因此用 min 确保
for(int i = 0; i < min(8,unity_LightData.y); ++i)
{
int index = unity_LightIndices[(uint)i/4][(uint)i%4];
Light light = GetOtherLight(index, surfaceWS, shadowData);
if (RenderingLayerOverlap(surfaceWS, light))
color += GetLighting(surfaceWS, brdf, light);
}
#else
// 没有每个对象光源的数据,因此处理所有
for(int i = 0; i < GetOtherLightCount(); ++i)
{
Light light = GetOtherLight(i, surfaceWS, shadowData);
if (RenderingLayerOverlap(surfaceWS, light))
color += GetLighting(surfaceWS, brdf, light);
}
#endif
return color;
}
最后,因为我们改了 shader 中属性变量名,所以不要忘记在 CPU 同步:
cs
int dirLightDirectionID = Shader.PropertyToID("_DirLightDirectionsAndMasks");
...
int otherLightDirectionsID = Shader.PropertyToID("_OtherLightDirectionsAndMasks");
2.4 Camera Rendering Layer Mask
Camera 通过 culling mask 来限制渲染哪些 layers,此外我们还可以利用 Rendering Layer Masks 来限制渲染哪些对象。Camera 没有 Rendering Layer Masks,因此需要向我们定义的 CameraSettings 中增加该属性,用 int 来定义,因为 Light 也是用了 int。默认值为 -1,表示所有 layer。
然后我们需要让 rendering layer mask 以下拉框的形式进行编辑,可以通过实现 custom editor 类来实现,但是我们用更简单的方法:仅为 Rendering Layer Mask 实现下拉框。
首先定义一个属性语义,并用语义修饰属性:
cs
public class RenderingLayerMaskFieldAttribute : PropertyAttribute { }
[Serializable]
public class CameraSettings
{
[RenderingLayerMaskField]
public int renderingLayerMasks = -1;
...
}
然后通过派生 PropertyDrawer 创建 Rendering Layer Mask 的编辑器绘制类
cs
[CustomPropertyDrawer(typeof(RenderingLayerMaskFieldAttribute))]
public class RenderingLayerMaskDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
Draw(position, property, label);
}
public static void Draw(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.showMixedValue = property.hasMultipleDifferentValues;
EditorGUI.BeginChangeCheck();
int mask = property.intValue;
bool isUint = property.type == "uint";
if (isUint && mask == int.MaxValue)
mask = -1;
mask = EditorGUI.MaskField(position, label, mask,
GraphicsSettings.currentRenderPipeline.prefixedRenderingLayerMaskNames);
if (EditorGUI.EndChangeCheck())
{
property.intValue = isUint && mask == -1 ? int.MaxValue : mask;
}
EditorGUI.showMixedValue = false;
}
// 没有 rect 的版本,直接从 layout engine 中获取
public static void Draw(SerializedProperty property, GUIContent label)
{
Draw(EditorGUILayout.GetControlRect(), property, label);
}
}

之前在 CustomLightEditor.cs 中也有绘制 Rendering Layer Masks 的逻辑,改为调用上面的接口:
cs
void DrawRenderingLayerMask()
{
SerializedProperty property = settings.renderingLayerMask;
RenderingLayerMaskDrawer.Draw(property, renderingLayerMaskLabel);
//EditorGUI.showMixedValue = property.hasMultipleDifferentValues;
//EditorGUI.BeginChangeCheck();
//int mask = property.intValue;
//mask = EditorGUILayout.MaskField(renderingLayerMaskLabel, mask,
// GraphicsSettings.currentRenderPipeline.prefixedRenderingLayerMaskNames);
//if (EditorGUI.EndChangeCheck())
//{
// property.uintValue = (uint)mask;
//}
//EditorGUI.showMixedValue = false;
}
在 CameraRenderer.DrawVisibleGeometry 接口中添加参数传递 mask,并进行应用:
cs
void DrawVisibleGeometry(bool useDynamicBatching, bool useGPUInstancing, bool useLightsPerObject,
int renderingLayerMask)
{
...
var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
filteringSettings.renderingLayerMask = (uint)renderingLayerMask;
context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
...
}
现在可以更灵活的用 Rendering Layer Mask 来控制摄像机的渲染。但是需要注意的是裁剪时只会用 culling mask,因此 rendering layer mask 排除掉越多的对象,culling 的执行效率就越高。
2.5 Masking Lights Per Camera
尽管 unity 的管线没有实现,但是为每个摄像机 mask 光源是可能的。我们还是用 CameraSettings.renderingLayerMask 属性,同时加上一个标记,区分是否启用该裁剪:

然后在 Lighting.SetupLights 中接收并应用该参数:
cs
...
if ((light.renderingLayerMask & renderingLayerMask) != 0)
{
//Light
switch (visibleLght.lightType)
{
...
}
}
...
传递合适的参数值:
cs
var crpCamera = camera.GetComponent<CustomRenderPipelineCamera>();
CameraSettings cameraSettings = crpCamera ? crpCamera.Settings : defaultCameraSettings;
// 设置光照相关参数
lighting.Setup(context, cullingResults, shadows, usePerObjectLights,
cameraSettings.maskLights?cameraSettings.renderingLayerMasks:-1);