当前位置: 首页 > news >正文

Custom SRP - 14 Multiple Cameras

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 来完成最后的上屏渲染:

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 来解决。如果没遇到该问题则可以忽略

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

Pass
{Name "Final Pass"Blend SrcAlpha OneMinusSrcAlphaHLSLPROGRAM#pragma target 3.5#pragma vertex DefaultPassVertex#pragma fragment FinalPassFragmentENDHLSL
}void DrawFinal(RenderTargetIdentifier from)
{buffer.SetGlobalTexture(fxSourceID, from);//RenderBufferLoadAction loadAction = camera.pixelRect == fullViewPort ? RenderBufferLoadAction.DontCare : RenderBufferLoadAction.Load;RenderBufferLoadAction loadAction = RenderBufferLoadAction.Load;   // 总是 loadbuffer.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。

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;elselowRes = 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 为黑色,因为该颜色会和下面的摄像机的渲染叠加。

Pass
{Name "Final Pass"Blend One OneMinusSrcAlphaHLSLPROGRAM#pragma target 3.5#pragma vertex DefaultPassVertex#pragma fragment FinalPassFragmentENDHLSL
}

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 文件:

// 逗号前是 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 值:

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。

[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 中根据需要进行创建。

[DisallowMultipleComponent, RequireComponent(typeof(Camera))]
public class CustomRenderPipelineCamera : MonoBehaviour
{[SerializeField]CameraSettings settings = default;public CameraSettings Settings => settings ?? (settings = new CameraSettings());
}

我们可以在 CameraRenderer.Render 实现开始的地方,获取 CustomRenderPipelineCamera 组件,如果没有添加该组件,则用默认配置对象,然后将 FinalBlendMode 传递给 PostFXStack:

// 默认的相机混合设置
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 并存储下来

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

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定义中用属性来替换硬编码的混合模式

Pass
{Name "Final Pass"//Blend One OneMinusSrcAlphaBlend [_FinalSrcBlend] [_FinalDstBlend]HLSLPROGRAM#pragma target 3.5#pragma vertex DefaultPassVertex#pragma fragment FinalPassFragmentENDHLSL
}

现在,如果摄像机没有我们的配置组件,由于默认是 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 属性,添加可配置的混合模式。

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 增加一个是否覆盖管线后效配置的开关,以及后效配置数据对象。

[Serializable]
public class CameraSettings
{...public bool overridePostFX = false;public PostFXSettings postFXSettings = default;public FinalBlendMode finalBlendMode = new FinalBlendMode{...};
}

在 CameraRenderer.Render 接口中,检查是否覆盖后效配置,如果覆盖,且指定的后效对象有效,则使用指定的后效配置对象进行渲染。

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 选项开启时,效果是正确的,不需要显示警告

public class CustomLightEditor : LightEditor
{public override void OnInspectorGUI(){...// 方向光的 culling masks 只能是 Everything// 点光源和聚光灯,只有在管线的 UsePerObjectLights 开启时,才允许修改 culling masksvar 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 才有效果,如下代码:

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 中调用我们的编辑方法

public class CustomLightEditor : LightEditor
{// 创建 Rendering layer mask labelstatic 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 来处理光源和对象的阴影了。

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

CBUFFER_START(UnityPerDraw)
...
real4 unity_WorldTransformParams;
// rendering layer masks
float4 unity_RenderingLayer;
...

在 Surface 结构体中,增加 mask 成员,并在 LitPassFragment 中为其赋值

    surface.dither = InterleavedGradientNoise(input.positionCS.xy, 0);// asuint 直接以原始数据作为无符号整数,避免先解释成浮点数再转无符号整数surface.renderingLayerMask = asuint(unity_RenderingLayer.x);

同时为光源增加成员:

struct Light
{...uint renderingLayerMask;
};

光源的 rendering layer mask 需要由我们自己上传到 GPU,我们将其存储在光源方向的第4个分量上,并在获取光源数据时赋值

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#没有该特性,但是我们可以模拟):

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 是否相交的方法,在计算光照时,先做判断:

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);}
#endifreturn color;
}

最后,因为我们改了 shader 中属性变量名,所以不要忘记在 CPU 同步:

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 实现下拉框。

首先定义一个属性语义,并用语义修饰属性:

public class RenderingLayerMaskFieldAttribute : PropertyAttribute { }[Serializable]
public class CameraSettings
{[RenderingLayerMaskField]public int renderingLayerMasks = -1;...
}

然后通过派生 PropertyDrawer 创建 Rendering Layer Mask 的编辑器绘制类

[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 的逻辑,改为调用上面的接口:

    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,并进行应用:

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 中接收并应用该参数:

...
if ((light.renderingLayerMask & renderingLayerMask) != 0)
{//Light switch (visibleLght.lightType){...}
}
...

传递合适的参数值:

var crpCamera = camera.GetComponent<CustomRenderPipelineCamera>();
CameraSettings cameraSettings = crpCamera ? crpCamera.Settings : defaultCameraSettings;// 设置光照相关参数
lighting.Setup(context, cullingResults, shadows, usePerObjectLights,cameraSettings.maskLights?cameraSettings.renderingLayerMasks:-1);

http://www.dtcms.com/a/600933.html

相关文章:

  • QT开发汇总(更新2025.11.12)
  • HTML5 MathML:现代网页中的数学表达利器
  • wordpress admin head简述搜索引擎优化
  • DeepSeek-OCR实战(05):DeepSeek-OCR-WebUI部署(Docker)
  • CI/CD自动化部署革命:“三分钟流水线“背后的工程实践
  • 【工具】PixPin 电脑实用截图工具!带免费OCR截图/贴图/录屏/文字识别
  • 京东关键字搜索接口逆向:从动态签名破解到分布式请求调度
  • 第三章 大语言模型基础学习笔记
  • 莱芜网站设计公司制作图片文字的软件
  • 自己做本地视频网站商城网站开发的任务书
  • 通过 API 与 Gradio 构建 AI 应用
  • 【C++进阶】二叉树进阶
  • 【C++】多态(2):纯虚函数多态底层原理
  • C++/Linux小项目:自主shell命令解释器
  • MEMS振荡器MST8012抗冲击设计应对严苛振动环境
  • 【数据结构】常见的排序算法 -- 交换排序
  • Rust与主流编程语言的深度对比分析
  • NebulaChat 框架学习笔记:深入理解 Reactor 与多线程同步机制
  • 网站开发接口网站建设需要什么
  • 聚焦新“新双高计划”,高职学校如何进行数字化转型?
  • 全志V853视频输入驱动框架详解:从VIN模块到虚通道实战
  • 网站建设需要英语吗wordpress笑话主题模板
  • Azure OpenAI GPT-5 PTU 容量规划与弹性配置实践
  • [linux仓库]多线程同步:基于POSIX信号量实现生产者-消费者模型[线程·柒]
  • Linux 内核驱动加载机制
  • C语言编译软件 | 高效选择适合的C语言编译环境
  • 天津 网站策划微信、网站提成方案点做
  • 工业级部署指南:在西门子IOT2050(Debian 12)上搭建.NET 9.0环境与应用部署(进阶篇)
  • 食品网站建设网站定制开发做网站只买一个程序
  • 中小型项目前后端工时对比