工具向TANotes

🔍TOOL丨批处理2.0:Lighting工具[2] - GI与LightLayer更改

by ERIN.Z, 2022-07-05


Global Illumination

GI全局光照是对直接和间接光照进行建模以提供逼真光照效果的一组技术。Unity 有两个全局光照系统:烘焙(Baked)和预计算实时(Precomputed Realtime)全局光照。 项目在场景阶段主要需要批量配置Baked GI。 Receive GI 在Scene窗口光照绘制模式中选择Global Illumination即可查看场景物体的GI状态,共分为三种:

  • 🟨Contribute GI : Off / Receive GI : Light Probes
  • 🟦Contribute GI : On / Receive GI : Lightmaps
  • 🟧Contribute GI : On / Receive GI : Lightmaps GI View

    Receive GI

    先说简单的。 Receive GI是MeshRenderer的属性,直接用枚举值设置即可:

      mr.receiveGI = ReceiveGI.Lightmaps;
      mr.receiveGI = ReceiveGI.LightProbes;

    Contribute GI

    Contribute GI是属于GameObject的一个Static Editor Flags属性:

    Static Editor Flags 属性列出了许多 Unity 系统,这些系统的预计算中可以包含一个静态游戏对象。使用下拉选单可定义哪些系统应在它们的预计算中包含此游戏对象。

Contribute GIMeshRenderer暴露了额外的开关接口,当ContributeGI开启时,Unity进行光照预计算时就会包含所在的GameObjectMeshRenderer组件。 SetGI

可以使用GameObjectUtility.SetStaticEditorFlags方法对其进行设置。

using UnityEngine;
using UnityEditor;
public class StaticEditorFlagsExample
{
    [MenuItem("Examples/Create GameObject and set Static Editor Flags")]
    static void CreateGameObjectAndSetStaticEditorFlags()
    {
        // Create a GameObject
        var go = new GameObject("Example");
        // Set the GameObject's StaticEditorFlags
        var flags = StaticEditorFlags.OccluderStatic | StaticEditorFlags.OccludeeStatic;
        GameObjectUtility.SetStaticEditorFlags(go, flags);
    }
}

这里要拓展一点点!

位掩码枚举(Flags)

Flags关键字允许我们在使用枚举变量时,使用多个组合值,如Flags、Light Layers都属于位掩码枚举。只需要在Enum上使用[Flags]标签即可标记为可组合的枚举。 声明位掩码枚举时需要注意每一个枚举值都需要规定为2的倍数,即进行位运算时,每一位都独立对应一个枚举。 例如: flag 在Flags类型的GUI选框中,有两个特别的类型:Nothing(全是0)和Everything(全是1,也是-1).不得不去补了一下补码的知识...欠的东西总有一条要还.... negative flags 在表示多个状态时,就可以使用位运算符来进行组合:

  • 按位与 &
  • 按位或 |
  • 按位取反 ~
  • 按位异或 ^

Static Editor Flags为例,在原Flag基础上增加ContributeGI的代码为:

StaticEditorFlags flags = GameObjectUtility.GetStaticEditorFlags(go);
GameObjectUtility.SetStaticEditorFlags(go, flags | StaticEditorFlags.ContributeGI);

原Flag基础上移除ContributeGI的代码为:

StaticEditorFlags flags = GameObjectUtility.GetStaticEditorFlags(go);
GameObjectUtility.SetStaticEditorFlags(go, flags & (~(StaticEditorFlags.ContributeGI)));

这次的工具中遇到了超级多位掩码的使用实例,在TreeView里记录展开State用的也是位掩码😵‍💫 去恶补了二进制运算...... 拓展阅读: Unity中的枚举和标志 【Unity】在Inspector上显示自定义的位掩码枚举(Flags)

Light Layers

LightLayer是HDRP的特性,允许灯光只作用于特定层的物体。 对于新建的hdrp工程而言,需要先在HDRP Asset中启用Light Layer。灯光作用层可以在Light组件中的General中编辑,网格物体的所在层在Mesh Renderer组件中的Additional Settings-Rendering Layer Mask。 LightSettings

Rendering Layer Mask

之前一直以为Rendering Layer Mask和Light Layer是一回事,今天才发现非也。Rendering Layer Mask包含Light Layers和Decal Layers(和很多Unused的空间)。 在HDAdditionalLightData.cs中定义了这样两个方法,可以进行light layer和rendering layer mask的相互转换:

        internal static int RenderingLayerMaskToLightLayer(int renderingLayerMask)
            => (byte)renderingLayerMask;

        internal static int LightLayerToRenderingLayerMask(int lightLayer, int renderingLayerMask)
        {
            var renderingLayerMask_u32 = (uint)renderingLayerMask;
            var lightLayer_u8 = (byte)lightLayer;
            return (int)((renderingLayerMask_u32 & 0xFFFFFF00) | lightLayer_u8);
        }

因为hdrp这部分没写官方文档,这里浅猜一下: Light Layer是8位的无符号整数byte,Rendering Layer mask是32位的无符号整数uint,Rendering Layer mask的后8位代表Light Layer,这也就是为什么第二个方法中要& 0xFFFFFF00(11111111 11111111 11111111 00000000)。

具体位掩码的运算就与上文相同啦,使用|加选,&~减选。

网格体的Rendering Layer Mask可以这么设置:

foreach (Renderer renderer in selectgo.GetComponentsInChildren<Renderer>(true))
   {
       Undo.RecordObject(renderer, "Light Layer Batched Modification");
       renderer.renderingLayerMask =
       LightLayerToRenderingLayerMask( LL_Flags, renderer.renderingLayerMask);
       PrefabUtility.RecordPrefabInstancePropertyModifications(renderer);
   }

(LightLayerToRenderingLayerMask()看后面!)

HDAdditionalLightData

这里又踩了个坑。 对于网格而言,直接将位运算后的位掩码转换为uint设置给meshrenderer.renderingLayerMask即可;设置灯光的层时,我起先是同理设置到了light.renderingLayerMask,然后发现虽然确实改变了灯光的作用层,但editor的ui中不会同步修改。 请教了mentor,发现问题来自hdrp中灯光自带的脚本:HDAdditionalLightData。 HDRP使用基于物理的照明,比Built-in中灯光额外多出的属性记录于HDAdditionalLightData这个类中。 对于LightLayer的修改有两个属性非常重要:lightlayersMask(LightLayerEnum)和linkShadowLayers(bool). linkShadowLayers会影响lightlayerMask的get/set方法:

        // Now the renderingLayerMask is used for shadow layers and not light layers
        [SerializeField, FormerlySerializedAs("lightlayersMask")]
        LightLayerEnum m_LightlayersMask = LightLayerEnum.LightLayerDefault;
        // Controls which layer will be affected by this light
        public LightLayerEnum lightlayersMask
        {
            get => linkShadowLayers ? (LightLayerEnum)RenderingLayerMaskToLightLayer(legacyLight.renderingLayerMask) : m_LightlayersMask;
            set
            {
                m_LightlayersMask = value;

                if (linkShadowLayers)
                    legacyLight.renderingLayerMask = LightLayerToRenderingLayerMask((int)m_LightlayersMask, legacyLight.renderingLayerMask);
            }
        }

在hdrp中,灯光的Layer运算分为两个部分:照亮和阴影。阴影层直接使用原本的renderingLayerMask属性,光照层使用m_LightlayersMask记录;若link为True,则其二者会关联设置。 shadowlayer 之前用meshrenderer一样的代码出问题的原因就是这个了!我实际更改的属性是灯光的阴影层;因为link为真,所以引擎调取参数时可以读到我设置到renderingLayerMask上的信息;但显示在面板上的是m_LightlayersMask,其实一直没有被更改;且link为假时之前的设置就会失效。 最终设置LightLayer的代码如下:

                foreach (Light light in selectgo.GetComponentsInChildren<Light>(true))
                {
                    Undo.RecordObject(light, "Light Layer Batched Modification");
                    lightData.lightlayersMask = LL_Flags;
                    PrefabUtility.RecordPrefabInstancePropertyModifications(light);
                }

HDAdditionalLightData里还有一些值得注意的小细节,例如如何处理uint下的-1:

        // Returns a mask of shadow light layers as uint and handle the case of Everything as being 0xFF and not -1
        public uint GetShadowLayers()
        {
            int value = RenderingLayerMaskToLightLayer(legacyLight.renderingLayerMask);
            return value < 0 ? (uint)LightLayerEnum.Everything : (uint)value;
        }

Inspector Fields

最后还有一个小需求:在面板上选择layers。之前用过的Popup是下拉单选框,但Layer还是存在复选的需求。 最开始使用的是MaskField

string[] LLs =System.Enum.GetNames(typeof(LightLayerEnum));
int targetLayer = EditorGUI.MaskField(new Rect(0, 50, position.width * 3 / 4 , 20), "Light Layer", targetLayer, LLs);

然后遇到了跟这一篇一模一样的问题!(但当时没搜到这一篇因为都还不知道啥是位掩码😠 因为有一位的错位,所以当时的解决方法是直接错回去(..

if (targetLayer > 0) light.renderingLayerMask =  targetLayer >> 2;
else light.renderingLayerMask =  targetLayer;

后来mentor给我改成了EnumFlagsField就方便了许多,也不需要先去获取枚举名称了:

LightLayerEnum LL_Flags = (LightLayerEnum)EditorGUI.EnumFlagsField(new Rect(,,,), "Light Layer", LL_Flags);

by ERIN.Z

2024 © typecho & elise