Skip to main content

Zhuangdong_Course_Note_LightingMap

3802 words·18 mins
Table of Contents

22. Unity Built-in LightingMap Baking System
#

Pre-baking Setup
#

Setting up Skybox
#

  • Path: Windows → Rendering → Lighting Settings → Skybox Material

L22_LightingSetting

L22_Skybox

Setting Scene Objects as Static
#

  • Enable Contribute GI and Reflection Probe Static
  • Static option descriptions:
    • Contribute GI: Responds to global illumination
    • Occluder/Occludee Static: Responds to occlusion culling (OccCulling)
    • Batching Static: Batching optimization
    • Navigation Static/Off Mesh Link Generation: Navigation related
    • Reflection Probe Static: Determines whether objects appear in reflection probe records

L22_Static

Main Directional Light, Emission Material, Reflection Probe Settings
#

  • Set emission material Global Illumination to Baked, Color to HDR

L22_GIBake

LightingMode Settings
#

  • Path: Windows → Rendering → Lighting Settings → MixedLighting → LightingMode

  • LightingMode options:

    • Baked Indirect
    • Subtractive
    • ShadowMask

L22_LightingMode

  • Directional Light Mode:
    • RealTime
    • Mixed
    • Baked

L22_Mode

Note: ShadowMask Mode needs to be enabled in ProjectSetting → Quality → Shadow → Shadowmask Mode. ShadowMask Mode additionally generates a Shadowmask texture.

Combination List
#

  • Bake Indirect
    • Runtime LM = GI = EmitLighting + SkyLighting Shadows: Real-time shadows
    • Mixed LM = GI = EmitLighting + SkyLighting + LightsGI Shadows: Real-time shadows
    • Baked LM = GI + DL = (EmitLighting + SkyLighting + LightsGI) + LightsLighting Shadows: Static objects: shadows written to LM, Dynamic objects: none
  • Subtractive
    • Runtime LM = GI = EmitLighting + SkyLighting Shadows: Real-time shadows, RealTime Shadow Color setting disabled
    • Mixed LM = GI + DL = (EmitLighting + SkyLighting + LightsGI) + LightsLighting Shadows: Static objects: shadows written to LM, Dynamic objects: real-time; RealTime Shadow Color setting effective
    • Baked LM = GI + DL = (EmitLighting + SkyLighting + LightsGI) + LightsLighting Shadows: Static objects: shadows written to LM, Dynamic objects: none; RealTime Shadow Color setting disabled
  • ShadowMask
    • Runtime LM-light = GI = EmitLighting + SkyLighting LM-shadowmask = null Shadows: Real-time shadows
    • Mixed LM-light = GI = EmitLighting + SkyLighting + LightsGI LM-shadowmask = LightsShadow Shadows: Static objects: shadows written to LM, Dynamic objects: real-time shadows
    • Baked LM = GI + DL = (EmitLighting + SkyLighting + LightsGI) + LightsLighting LM-shadowmask = null Shadows: Static objects: shadows written to LM, Dynamic objects: none

Lighting Explanation
#

  • EmitLighting represents lighting caused by emission materials
  • SkyLighting represents lighting caused by skybox
  • LightsGI represents bounce light caused by main light
  • LightsLighting represents lighting caused by main light
  • LM = Lightmap, DL = DirectLighting

Note: Subtractive and ShadowMask handle real-time shadows for dynamic objects differently, ShadowMask provides more natural shadow mixing for static and dynamic objects.

Lightmapping Settings
#

  • Lightmapper: Progressive CPU
  • Lightmap Resolution: 40
  • Directional Mode: Non-Directional (generally not recommended, effect not obvious, energy consumption doubles)

L22_OtherSetting

Common Solutions
#

  • Full real-time lighting: No Lightmap needed
  • Full real-time direct lighting + Baked Indirect
  • Static objects baked, dynamic objects real-time (Subtractive/ShadowMask)
  • Auxiliary lights only for baking (Baked/Mixed) Mixed determines whether to affect dynamic objects
  • Effects RealTime

Batching Strategy and Baking Process Analysis
#

Definition
#

Combining several batches of rendering with the same rendering process to save resources and increase speed. Generally, shared Material with same parameters can be batched.

Common Strategies
#

  • Unity static settings
  • Unity SRPBatching (same Shader can be batched)
  • GPU Instancing
  • Manual batching (merge models in DDC software)

FrameDebugger
#

Windows → Analysis → FrameDebugger

L22_FrameDebugger

FrameDebugger can view batching and rendering process

L22_FrameDebug


23. External LightingMap Baking and Custom Shader
#

External Baking Process
#

  • Use 3dsMax/MAYA to bake LightingMap, need to create UV2 for models
  • Textures:
    • AO Map replaces SkyLighting
    • EmitLighting
    • LightsShadow
    • LightGI + LightsLighting
    • Alpha
  • Use SD’s Blur HQ node to remove noise, merge textures to save resources

L23_Lightingmap

Lighting Composition
#

flowchart LR
    Lighting --> SimpleLightSource
    SimpleLightSource --> MainDirectionalLight
    MainDirectionalLight --> DiffuseReflection
    MainDirectionalLight --> SpecularReflection
    DiffuseReflection --> Occlusion
    SpecularReflection --> Occlusion
    Occlusion --> Result
    Lighting --> ComplexEnvironment
    ComplexEnvironment --> SkyLight
    ComplexEnvironment --> Emission
    ComplexEnvironment --> OtherAmbientLight
    SkyLight --> DiffuseReflectionCubemap
    Emission --> DiffuseReflection1Col
    OtherAmbientLight --> DiffuseReflection
    DiffuseReflectionCubemap --> Occlusion
    DiffuseReflection1Col --> Occlusion
    DiffuseReflection --> Ignore
    SkyLight --> SpecularReflectionCubemap
    Emission --> SpecularReflectionCubemap
    OtherAmbientLight --> SpecularReflectionCubemap
    SpecularReflectionCubemap --> SurfaceOcclusion
    SpecularReflectionCubemap --> FresnelAttenuation
    SurfaceOcclusion --> Result
    FresnelAttenuation --> Result
    Occlusion --> Result

Shader Core Structure
#

Shader "Zhuangdong/AP1/L15/L15_LightingMap" {
    Properties{
        // ... Texture and parameter declarations ...
    }
    SubShader{
        Tags { "RenderType" = "Opaque" }
        Pass {
            Name "FORWARD"
            Tags { "LightMode" = "ForwardBase" }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            // ... Using orge case as template, vertex shader removes TRANSFER_VERTEX_TO_FRAGMENT(o) ...
             VertexOutput vert(VertexInput v) { 
                    ...
                    //Case needs 2 UVs, here merge 2 UVs into one float4, saving TEXCOORD
                    o.uvs = float4(v.uv0, v.uv1);
                    ...
            }
            float3 DecodeNormal (float2 maskXY) {
                //Texture only has RG two channels, we need to supplement B channel
                float2 nDirTSxy = maskXY * 2.0 - 1.0;
                //Texture value range is (0,1), normal is vector, vector value range is (-1,1)
                float nDirTSz = sqrt(1.0 - (nDirTSxy.x * nDirTSxy.x + nDirTSxy.y * nDirTSxy.y));
                //sqrt takes square root, x squared + y squared + z squared = 1
                return float3(nDirTSxy, nDirTSz);
            }
            float4 frag(VertexOutput i) : COLOR {
                // ... Sampling, vectors, dot products ...
                //Extract surface information
                float occlusion = var_MaskTex.r;
                float matMask = var_MaskTex.g;//Metal texture
                float3 diffCol = var_MainTex.rgb * lerp(1.0, _MetalDarken, pow(matMask, 5.0));
                //Here add _MetalDarken to metal, reducing metal part diff
                float specPow = max(1.0, lerp(_SpecParams.x, _SpecParams.z, matMask));
                float specInt = max(0.0, lerp(_SpecParams.y, _SpecParams.w, matMask));
                float reflectMip = clamp(lerp(_EnvReflectParams.x, _EnvReflectParams.z, matMask), 0.0, 7.0);
                //clamp is clamp(value,min,max) which limits value to (min,max)
                float reflectInt = max(0.0, lerp(_EnvReflectParams.y, _EnvReflectParams.w, matMask));
                float fresnel = lerp(pow(1.0 - max(0.0, ndotv), _FresnelPow), 1.0, matMask);
                //Distinguish Fresnel intensity between metal and non-metal
                //Extract lighting information
                float skyLightOcc = var_LightMap.r;
                float emitLightingInt = var_LightMap.g;
                float mainLightGIInt = pow(var_LightMap.b, _GIpow);
                float mainLightShadow = var_LightMap.a;
                //Sample texture Cube
                float3 var_SkyCube = texCUBElod(_SkyCube, float4(vrDirWS, 7.0)).rgb;
                float3 var_EnvCube = texCUBElod(_EnvCube, float4(vrDirWS, reflectMip)).rgb;
                //MainLight
                //Diff
                float3 halfShadowCol = lerp(_HalfShadowCol.rgb, _MainLightCol, mainLightShadow);
                //First assign colors to shadow and main light
                float3 mainLightCol = lerp(_MainLightCol, halfShadowCol, _HalfShadowCol.a) * mainLightShadow;
                //Then use shadow mask to remove shadow color, leaving main light color and penumbra color, here _HalfShadowCol.a controls penumbra intensity
                float3 mainLightDiff = diffCol * mainLightCol * max(0.0, nDotl);
                //DiffCol*Lambert*MainLight
                //Spec
                float3 mainLightSpec = mainLightCol * pow(max(0.0, vDotr), specPow) * specInt;//Phong
                //GI
                float3 mainLightGI = _GICol * occlusion * mainLightGIInt * _GIInt;
                //GI is the bounce light of main light, here add AO for variation
                //Mixed
                float3 mainLight = (mainLightDiff + mainLightSpec + mainLightGI * _MainLightGIOn) * _MainLightOn;
                //Here GI already has ambient AO information, so no need to *LM

                //OtherLight
                float3 skyLightDiff = diffCol * var_SkyCube * _SkyCol * _SkyLightInt * skyLightOcc * occlusion;
                //Here is object diffuse * sky diffuse * LM(LightingMap)
                float3 emitLightDiff = diffCol * _EmissionCol * emitLightingInt * occlusion;
                //Same as above, this part is not affected by ambient AO, so no need to *_SkyLightOcc
                //OtherEnvSpec
                float3 envLightSpec = var_EnvCube * reflectInt * fresnel * occlusion;
                float3 OtherLight = skyLightDiff * _SkyLightOn + emitLightDiff * _EmitLightOn + envLightSpec * _EnvReflectOn;
                float3 finalRGB = mainLight + OtherLight;
                return float4(finalRGB, 1.0);
            }
            ENDCG
        }
    }
        FallBack "Diffuse"
}

Key Parameter Explanation
#

  • _SpecParams

    • x: Non-metal specular power
    • y: Non-metal specular intensity
    • z: Metal specular power
    • w: Metal specular intensity
  • _EnvReflectParams

    • x: Non-metal Cube sampling Mip value
    • y: Non-metal reflection intensity
    • z: Metal Cube sampling Mip value
    • w: Metal reflection intensity

Key Points Summary
#

  • Normal map only has RG, need to manually supplement B channel
  • Penumbra coloring
  • Metal/non-metal parameters separately control specular and reflection
  • Synthesize lighting according to lighting composition

24-26 LightingController Custom Baker and Multi-Script Collaboration
#

Due to the extensive code content in this case, the explanation focuses on summaries. For source code, please download from the tutorial location.

This case implements a custom baker for completing custom baking, result preview, and material editing functions. The baking process generates three textures corresponding to main light, sky light, and emission light sources, and finally merges these three textures to output the final LightingMap. After baking is complete, you can preview the independent effects of each texture separately and flexibly adjust the final image performance through global parameters.

Main Light

L24_MainLight

Sky Light

L24_SkyLight

Emission Light

L24_EmitLight

Functions and File Structure
#

  • LightingController: Main script responsible for the entire baking process.
  • LightingControllerGUI: Custom baking panel (UI), making operations more intuitive.
  • EmissionShaderGUI: Allows material spheres to set emission participation in GI in the Inspector interface.
  • Building/EmitLight/Sky: Respectively correspond to building, emission, and skybox Shaders.

Global Parameter Brief Explanation
#

  • MetalDarken: Darkness of metal parts (affects metal surface reflection intensity).
  • MainLightCol: Main light color.
  • SpecParams: Specular parameters (x/z are specular power, y/w are specular intensity, respectively corresponding to metal/non-metal).
  • SkyLightInt: Sky light intensity.
  • ReflectParams: Reflection parameters (x/z are CubeMap Mip, y/w are reflection intensity).
  • FresnelPow: Fresnel phenomenon intensity.
  • EmissionCol: Emission color.

Baking Process
#

  1. Collect scene baking information: including texture count, paths, texture objects
  2. Create cache: Prepare cache for main light, sky light, emission GI and final synthesized Lightmap respectively.
  3. Bake: Bake main light, sky light, emission GI in sequence, store each step result in cache.
  4. Synthesize Lightmap: Combine the results of three baking steps into one final Lightmap.
  5. Replace old Lightmap: Overwrite the scene’s original lighting textures with the newly synthesized Lightmap.
  6. Reset scene lighting environment: Modify scene settings.
  7. Update global parameters: Synchronize parameters to all related materials.
  8. Bake reflection probes: Make reflection effects display correctly.

Core Code Example
#

public void MultiBake()
{
    var buffer = new LightmapsBuffer();
    Bake(BakeMode.BakeMainLight);
    var info = new LightmapsInfo(LightmapSettings.lightmaps);
    buffer.WriteBuffer(info, LightmapsBuffer.BufferType.MainLight);
    Bake(BakeMode.BakeSkyLight);
    buffer.WriteBuffer(info, LightmapsBuffer.BufferType.SkyLight);
    Bake(BakeMode.BakeEmissionGI);
    buffer.WriteBuffer(info, LightmapsBuffer.BufferType.EmissionGI);
    buffer.CreateLightmaps();
    buffer.OverrideLightmaps(info);
    buffer.Clear();
    ArrangeBakeScene(BakeMode.Default);
    UpdateGlobalProperties();
    BakeReflectProbe();
}

LightingController
#

Class and Inheritance, Methods and Lifecycle
#

public class LightingController : MonoBehaviour
  • class: Can be understood as a “blueprint”, like Shader is a blueprint, Material is an “item” made according to the blueprint.
  • Inheritance: Like “vehicle” is the parent class, “car” is the child class, child class automatically has the basic functions of parent class.
  • MonoBehaviour: All Unity scripts that can be attached to objects must inherit from it to participate in the game lifecycle.
  • [ExecuteInEditMode]: Generally, scripts only run in PlayMode, this directive makes the script also run in preview window
private void OnEnable() {UpdateGlobalProperties();};
  • void is different from methods in previous cases, void functions don’t return values, they are mainly used to perform specific operations.
  • OnEnable means this function starts running at the OnEnable stage of the game lifecycle

UpdateGlobalProperties() method is written after OnEnable, which is allowed in C#

LightmapsInfo
#

To understand the following concepts to understand the role of LightmapsInfo

LightmapData, struct, constructor, Dictionary and foreach loop

LightmapData
#

  • LightmapData[] is Unity’s data type, it’s the collection of all lighting textures in the current scene
  • lightmapsData is a custom name, representing LightmapData[], used for function internal operations
  • LightmapsData[] contains multiple instances, each instance represents each Lightingmap, each instance contains:
    • lightmapColor
    • lightmapDir
    • shadowMask
    • occlusionMask
Struct and Constructor
#

Struct
#

Can be understood as a “container” that can hold different types of data.

Syntax
#

public/private name (same as class/struct name) (type, parameter name)

Constructor
#

A special function used to initialize struct (or class), with the same name as the struct.

Example
#
public struct LightmapsInfo {
    public readonly Dictionary<string, Texture2D> lightmapsInfo;
    public LightmapsInfo(LightmapData[] lightmapsData) {
        // Here all content will be automatically initialized
    }
}
public class Person {
    public string Name;
    public int Age;

    // Constructor
    public Person(string name, int age) {
        Name = name;
        Age = age;
    }
}
  • Without constructor, you need to manually assign values one by one, very tedious.
  • With constructor, just new LightmapsInfo(parameters), all content automatically arranged.

Multiple Uses of Constructor
#
  • Constructor can have multiple, with different parameters or methods.
  • Can also do parameter checking in constructor to prevent errors.
Example
#
public LightmapsInfo(LightmapData[] lightmapsData){
    if(lightmapsData == null) {
        // Do some error handling
    }
    // Other initialization content
}

Dictionary
#

  • Dictionary is a “key-value pair” container that can quickly find corresponding content through “names”.
  • For example: Dictionary<string, Texture2D>, can use string names to find corresponding texture paths and objects.
Case
#
Dictionary<string, Texture2D> myDict = new Dictionary<string, Texture2D>();
myDict["MainLight"] = mainLightTex;
Texture2D tex = myDict["MainLight"];

foreach Loop
#

foreach is used to traverse collections (such as arrays, List, Dictionary, etc.)

Example
#
//data represents instance
foreach (var data in lightmapsData) {
    var texture = data.lightmapColor;
    path = AssetDatabase.GetAssetPath(texture);
    lightmapsInfo.Add(path, texture);
}
int[] numbers = { 1, 2, 3, 4, 5 };
foreach (int num in numbers){
    Debug.Log(num); // Output: 1, 2, 3, 4, 5
}

LightmapsBuffer Cache
#

In Unity’s lighting baking process, we need to cache, synthesize and save different types of lighting textures.

LightmapsBuffer: Role and Implementation of Cache
#

Cache (Buffer)
#

Can be understood as a “storage box” used to temporarily save different types of lighting textures, convenient for subsequent processing and transfer.

Enum
#

Used to define a set of constants, making code clearer and easier to understand.

public enum BufferType {
    MainLight,  // Main light lighting: BufferA
    SkyLight,   // Sky light lighting: BufferB
    EmissionGI, // Emission GI: BufferC
    Lightmap    // Synthesized Lightmap
}

// Create cache for different types of lighting respectively
private Texture2D[] _bufferA;   // Main light
private Texture2D[] _bufferB;   // Sky light
private Texture2D[] _bufferC;   // Emission
private Texture2D[] _lightmap;  // Final synthesis

Clear Cache (ClearBuffer & Clear)
#

  • switch-case: Execute different clearing logic according to different types (BufferType).
  • void(type parameter name):
    • If it’s a single data, parameter name is the temporary name of this data in the function
    • If it’s a collection, parameter name is the temporary name of this collection in the function
    • If it’s an enum, parameter name is the temporary name of elements in the enum.

Write Cache (WriteBuffer)
#

  • First judge the type, if it’s the final Lightmap then return directly.
  • Clear the corresponding type cache.
  • Create new cache and copy textures from info.
if (type == BufferType.Lightmap) return;
// Clear cache
ClearBuffer(type);
// Create cache and copy textures from info
var lightmapsCount = info.lightmapsCount;
var buffer = new Texture2D[lightmapsCount];
for (var i = 0; i < lightmapsCount; i++) {
    var lightmap = info.lightmapsInfo.Values.ElementAt(i);
    buffer[i] = new Texture2D(lightmap.width, lightmap.height, lightmap.format, false);
    Graphics.CopyTexture(lightmap, 0, 0, buffer[i], 0, 0);
}
  • info.lightmapsInfo.Values.ElementAt(i): Take out the i-th texture from the dictionary in order.

  • Texture2D(texture width, height, format (default RGB24), whether to generate mipmap)

  • Graphics.CopyTexture: Unity API, used for efficient texture data copying.


Synthesize Lightmap (CreateLightmaps)
#

  • Read pixels from main light, sky light, emission three types of cache in sequence, synthesize to a new Lightmap.
for (var x = 0; x < width; x++) {
    for (var y = 0; y < height; y++) {
        var colA = _bufferA[i].GetPixel(x, y);
        var colB = _bufferB[i].GetPixel(x, y);
        var colC = _bufferC[i].GetPixel(x, y);
        var newCol = new Color(colA.r, colB.g, colC.b, 1.0f);
        lightmap.SetPixel(x, y, newCol.linear);
    }
}
  • Each pixel point takes corresponding colors from three cache textures, synthesizes to the final Lightmap.
  • Note: Cache saves Texture2D objects, need to read and write pixel by pixel.

Save Lightmap (OverrideLightmaps)
#

  • Encode the synthesized Lightmap to EXR format (supports HDR) and write to disk.
  • Refresh Unity resource database to make new files recognized.
for (var i = 0; i < lightmapsCount; i++) {
    var bytes = _lightmap[i].EncodeToEXR(Texture2D.EXRFlags.CompressZIP);
    File.WriteAllBytes(lightmapsInfo.Keys.ElementAt(i), bytes);
    AssetDatabase.Refresh();
}
  • EncodeToEXR: Unity API, encodes texture to EXR format, EXR is Unity lighting system’s dedicated format
  • Texture2D.EXRFlags.CompressZip compress to zip
  • File.WriteAllBytes: .NET API, writes byte array to file.
  • lightmapsInfo.Keys.ElementAt(i) get the i-th key from dictionary lightmapsInfo, i.e., path
  • AssetDatabase.Refresh(): Refresh resource database (only available in editor mode).

Baking Mode (BakeMode) and Scene Settings (ArrangeBakeScene)
#

  • BakeMode: Use enum to define different baking modes (all, main light, sky light, emission).
  • ArrangeBakeScene: Set scene parameters according to different modes, such as ambient light type and intensity.
RenderSettings.ambientMode = AmbientMode.Skybox;
RenderSettings.ambientIntensity = 1.0f;
  • ambientMode determines ambient light type (skybox, gradient, single color).
  • ambientIntensity controls ambient light intensity (effective after baking).

Corresponds to lightingSetting → lighting → Scene → Environment Lighting → Source & Intensity Multiplier

Set main light as static:

var staticFlags = StaticEditorFlags.ContributeGI | StaticEditorFlags.ReflectionProbeStatic;
GameObjectUtility.SetStaticEditorFlags(mainlight.gameObject, staticFlags);

Execute Baking (Bake) and Reflection Probe Baking (BakeReflectProbe)
#

Bake directly calls Unity API for lighting baking.

public void Bake(BakeMode mode) {
    Lightmapping.Clear();
    Lightmapping.Bake();
}

BakeReflectProbe traverses all reflection probes in the scene, bakes and saves them one by one.

private void BakeReflectProbe() {
    var allProbe = FindObjectsOfType<ReflectionProbe>();
    foreach (var probe in allProbe) {
        var path = AssetDatabase.GetAssetPath(probe.texture);
        Lightmapping.BakeReflectionProbe(probe, path);
    }
    AssetDatabase.Refresh();
}
  • FindObjectsOfType<>(): Find all reflection probes in the scene.
  • AssetDatabase.GetAssetPath() get these reflection probes’ textures and paths
  • Lightmapping.BakeReflectionProbe(): Unity API, bake reflection probe.

If you don’t customize storage path, reflection probe baked Cubemap defaults to LightingData


LightingControllerGUI: Custom Baking Panel
#

After understanding the baking process and methods, we also need a custom UI panel to make baking operations and parameter adjustments more intuitive. Below introduces the implementation ideas and key code of LightingControllerGUI.

L24_LightingController_UI


Basic Structure of Editor Class
#

  • UnityEditor.Editor: Custom editor class must inherit from Editor and implement OnInspectorGUI() method.
  • OnInspectorGUI(): Used to draw the content of Inspector panel.
public class LightingControllerGUI : Editor
{
    public override void OnInspectorGUI()
    {
        var controller = target as LightingController;
        if (controller == null) return;
        DrawFunctionButtons(controller);
        DrawGlobalProperties(controller);
    }
}
  • override when methods in class are set as virtual or abstract, can only use override to reference and override execution content
  • target as LightingController: Get the object bound to current Inspector, must be MonoBehaviour derived class.
  • DrawFunctionButtons: Draw baking-related operation buttons.
  • DrawGlobalProperties: Draw global parameter adjustment controls.

Drawing Function Buttons
#

  • Use GUILayout.Button("button name") to create buttons, execute corresponding methods when clicked.
  • EditorGUILayout.BeginHorizontal() and EndHorizontal() are used to make multiple buttons display in the same row.
if (GUILayout.Button("Forbidden Art·Multiple Baking"))
    controller.MultiBake();

EditorGUILayout.BeginHorizontal();
// Can add more buttons here
EditorGUILayout.EndHorizontal();

Global Parameter Adjustment and Monitoring
#

  • Use EditorGUILayout.BeginFoldoutHeaderGroup to create collapsible parameter groups.
  • Use EditorGUILayout.Slider to create draggable sliders for easy parameter adjustment.
  • Use EditorGUI.BeginChangeCheck() and EndChangeCheck() to monitor parameter changes, automatically update when changed.
EditorGUI.BeginChangeCheck();
_groupAToggle = EditorGUILayout.BeginFoldoutHeaderGroup(_groupAToggle, "Material Properties");
if (_groupAToggle)
//Equivalent to if (groupAToggle == true)
{
    controller.metalDarken = EditorGUILayout.Slider(
        "Metal Darkening",
        controller.metalDarken,
        0.0f, 5.0f);
}
EditorGUILayout.EndFoldoutHeaderGroup();
if (EditorGUI.EndChangeCheck())
{
    controller.UpdateGlobalProperties();
    EditorUtility.SetDirty(controller);
}
  • _groupAToggle: Used to control the expand/collapse of foldout group, defaults to false when not assigned.
  • EditorUtility.SetDirty(controller): Mark object as “modified”, ensure parameter changes can be recorded by Unity.

EmissionShaderGUI: Custom Emission Parameter Panel
#

Through custom ShaderGUI, we can make the material sphere’s Inspector panel appear “Enable Emission GI” option, convenient for controlling whether emission participates in global illumination (GI).

L24_EmitUI


Basic Structure
#

Inherit ShaderGUI
#

When customizing material Inspector panel, need to inherit ShaderGUI class.

public class EmissionShaderGUI : ShaderGUI
{
    // Specific implementation see below
}

After inheritance, Unity will automatically get the Shader and Material information using this script.


Get Current Material
#

Get the current material instance being edited through materialEditor.target as Material.

var material = materialEditor.target as Material;

Use Default Inspector Interface
#

If you don’t need to customize complex UI, you can directly call the base class’s OnGUI, this will display the default property panel.

base.OnGUI(materialEditor, properties);

Add Emission GI Switch
#

Use EditorGUILayout.Toggle to create a checkbox, control whether emission participates in global illumination.

Toggle has 4 parameters

  • label text content
  • value condition
  • style button style
  • option button size
var ifEmissionGIOn = EditorGUILayout.Toggle(
    "Enable Emission GI",
    material.globalIlluminationFlags == MaterialGlobalIlluminationFlags.AnyEmissive);

material.globalIlluminationFlags = ifEmissionGIOn
    ? MaterialGlobalIlluminationFlags.AnyEmissive
    : MaterialGlobalIlluminationFlags.EmissiveIsBlack;
  • EditorGUILayout.Toggle("interface text", condition): Display checkbox state according to condition.
  • Ternary operator condition ? resultA : resultB, equivalent to if-else statement:
if (ifEmissionGIOn)
    material.globalIlluminationFlags = MaterialGlobalIlluminationFlags.AnyEmissive;
else
    material.globalIlluminationFlags = MaterialGlobalIlluminationFlags.EmissiveIsBlack;

Shader Explanation and Case Analysis
#

In this case, Shader has several key differences compared to other cases:

  • Lightmap UV calculation method
  • MetaPass implementation
  • Branch declaration (shader_feature)
  • Custom ShadowCaster

Standard Lightmap UV Calculation
#

In Unity, Lightmap UV coordinates are usually calculated like this:

float2 lmUV = v.uv1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
  • uv1 is the model’s second set of UV (specifically for lighting textures).
  • unity_LightmapST is the scale/offset parameter automatically passed by Unity.

MetaPass: Shading Channel Dedicated to Baking
#

MetaPass is Unity lighting baking system’s dedicated Shader channel. Only Shaders with MetaPass can participate in lighting baking.

Case Code
#

//Building.Shader's MetaPass
Pass {
    Name "META"
    Tags { "LightMode" = "Meta" }
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #include "UnityCG.cginc"
    #include "UnityMetaPass.cginc"
    #pragma shader_feature __ _BAKE_MAINLIGHT _BAKE_SKYLIGHT _BAKE_EMISSIONGI

    struct VertexInput {
        float4 vertex   : POSITION;
        float2 uv0      : TEXCOORD0;
        float2 uv1      : TEXCOORD1;
        float2 uv2      : TEXCOORD2;
    };
    struct VertexOutput {
        float4 pos : SV_POSITION;
        float2 uv : TEXCOORD0;
    };
    VertexOutput vert (VertexInput v) {
        VertexOutput o = (VertexOutput)0;
        o.pos = UnityMetaVertexPosition(v.vertex, v.uv1, v.uv2, unity_LightmapST, unity_DynamicLightmapST);
        o.uv = v.uv0;
        return o;
    }
    float4 frag(VertexOutput i) : COLOR {
        UnityMetaInput metaIN;
        UNITY_INITIALIZE_OUTPUT(UnityMetaInput, metaIN);
        metaIN.Albedo = Luminance(tex2D(_MainTex, i.uv).rgb);
        metaIN.SpecularColor = 0.0f;
        metaIN.Emission = 0.0f;
        return UnityMetaFragment(metaIN);
    }
    ENDCG
}

Key Points Explanation
#

  • UnityStandardMeta.cginc PBR process META Pass dedicated
  • UnityMetaPass.cginc belongs to part of UnityStandardMeta.cginc, since the case is not PBR process, for optimization consideration, only reference this cginc. Compared to UnityStandardMeta.cginc need to customize vertex shader part
  • UnityMetaVertexPosition: Convert vertex and UV to baking space.
  • UnityMetaInput: Contains array of baking required data like Albedo, SpecularColor, Emission.
  • UNITY_INITIALIZE_OUTPUT initialize UnityMetaInput
  • Luminance: Convert color to grayscale, ensure baking only considers brightness, not affected by specular and emission.

Branch Control (shader_feature)
#

//LightingController.cs
...
case BakeMode.Default:
// Turn off main light
mainlight.enabled = true;
// Set environment
RenderSettings.ambientMode = AmbientMode.Skybox;
RenderSettings.ambientIntensity = 1.0f;
// Set Shader global branches
Shader.DisableKeyword("_BAKE_MAINLIGHT");
Shader.DisableKeyword("_BAKE_SKYLIGHT");
Shader.DisableKeyword("_BAKE_EMISSIONGI");
break;
...
#pragma shader_feature __ _BAKE_MAINLIGHT _BAKE_SKYLIGHT _BAKE_EMISSIONGI
  • Need to set global branches in both script and Shader
  • Through branch declaration, can make Shader output different content for different baking modes.
  • Use #if defined(...) for branch judgment in code:
#if defined (_BAKE_EMISSIONGI)
    metaIN.Emission = opacity;
#elif defined (_BAKE_MAINLIGHT) || defined (_BAKE_SKYLIGHT)
    metaIN.Emission = 0.0f;
#endif

Skybox Special Note
#

  • Skybox’s Shader doesn’t need MetaPass, Unity internally handles skybox lighting baking automatically.

ShadowCaster: Custom Shadow Pass
#

Since this case scene is non-enclosed box-shaped and normals face inward, Unity’s default ShadowCaster cannot project correctly. Need to customize ShadowCaster Pass.

Case Code
#

Pass {
    Name "ShadowCaster"
    Tags { "LightMode" = "ShadowCaster" }
    ZWrite On ZTest LEqual Cull off
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma multi_compile_shadowcaster
    #include "UnityCG.cginc"

    struct v2f {
        V2F_SHADOW_CASTER;
    };

    v2f vert(appdata_base v) {
        v2f o;
        TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
        return o;
    }

    float4 frag(v2f i) : SV_Target {
        SHADOW_CASTER_FRAGMENT(i)
    }
    ENDCG
}

Key Points Explanation
#

  • appdata_base: Unity predefined vertex input structure, contains POSITION, NORMAL, TEXCOORD0.
  • V2F_SHADOW_CASTER: Output structure, contains shadow projection required clip space position and auxiliary vectors, equivalent to:
    • float4 pos : SV_POSITION;
    • float3 vec : TEXCOORD0;
  • TRANSFER_SHADOW_CASTER_NORMALOFFSET(o): Calculate shadow offset, ensure shadow projection is correct.
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
    float4 clipPos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); \
    o.pos = UnityApplyLinearShadowBias(clipPos); \
    o.vec = ComputeOffsetData(clipPos);
  • SHADOW_CASTER_FRAGMENT(i) Unity built-in macro, calculate shadow casting in fragment shader
  • Cull off: Turn off culling, ensure inner surfaces can also project.

uv0: TEXCOORD0 in VertexInput will automatically be recognized as the model’s first set of UV, in VertexOutput, TEXCOORD0 is a general interpolation register, it’s a developer-defined data channel, need to manually calculate and assign from input data in vert


The translation is now complete, including all Chinese comments and technical terms translated to English while maintaining the technical accuracy and context of the original content.

Related

EssenceOfLinearAlgebra - 04
1099 words·6 mins
EssenceOfLinearAlgebra - 02
1458 words·7 mins
Zhuangdong_Course_Note_VFXShader
4105 words·20 mins