为什么要用球谐函数来计算全局光照

在物体的渲染中,除了计算直接光照的BRDF之外,也要计算间接光照对物体的影响。在引擎中获取间接光照信息的方法通常是在场景中布置一个反射探针,离线渲染一个360度的场景贴图。这样在计算间接光照的高光部分的时候,可以使用视线在物体上的反射方向reflect(-viewDirection, normal),对渲染好的贴图进行采样,再进行brdf的计算,因此这张贴图也会被称作是specular map;然而在计算间接光照的漫反射部分时,因为目标点会受到来自各个方向上的光线带来的漫反射,不能再简单的使用视线的反射来采样这张帖图。这时有两种解决办法,一种是采样这张贴图的mipmap,在一定程度上模糊的mipmap可以认为是综合了各个方向的光照的信息,另一种则是Ravi Ramamoorthi和Pat Hanrahan2001年在An Efficient Representation for Irradiance Environment Maps中提出的,通过球谐函数重新构建低频光照信息的方式,将其作为简介光漫反射部分的贴图。

如何使用球谐函数重新构建光照信息

在Ravi Ramamoorthi的论文中他给出了球谐参数的计算公式和重构光照信息的公式: $$ \tag*{球谐参数} L_{lm} = \int_{\theta = 0}^\pi\int_{\theta = 0}^{2\pi}L(\theta)Y_{lm}(\theta, \phi)sin\theta d\theta d\phi $$ $$ \begin{align*}其中(x, y, z) &= (sin\theta cos\phi, sin\theta sin\phi, cos\theta) \cr Y_{00}(\theta, \phi) &= 0.282095 \cr (Y_{11};Y_{10};Y_{1-1})(\theta, \phi) &= 0.488603(x;z;y) \cr (Y_{21};Y_{2-1};Y_{2-2})(\theta, \phi) &= 1.092548(xz;yz;xy) \cr Y_{20}(\theta, \phi) &= 0.315392(3z^2 - 1) \cr Y_{22}(\theta, \phi) &= 0.546274(x^2 - y^2) \end{align*} $$ $$ \tag*{重构光照} \begin{equation}\begin{split} E(n) =&\ c_1L_{22}(x^2 - y^2) + c_3L_{20}z^2 + c_4L_{00} - c_5L_{20} \cr +&\ 2c_1(L_{2-2}xy + L_{21}xz + L_{2-1}yz) \cr +&\ 2c_2(L_{11}x + L_{1-1}y + L_{10}z)\end{split}\end{equation} $$ $$ 其中c1 = 0.429043, c2 = 0.511664, c3 = 0.743125, c4 = 0.886227, c5 = 0.247708 $$

根据第一条式子,有两种采样的方法:一种是使用蒙特卡洛方法,以\(Y_{lm}(\theta, \phi)sin\theta\)作为权重,随机取球面上均匀分布的方向采样高光贴图;另一种是以\(Y_{lm}(\theta, \phi)sin\theta \)乘当前像素对应的立体角作为权重,采样高光贴图的所有像素。在这篇文章中我参考了Accardi Piero的Unity_SphericalHarmonics_Tools中的用Computes Shader采样所有像素的方法来计算球谐参数的方法,出于一些不知名的原因,计算权重的时候去掉\(sin\theta\)这一项反而能得到正确的结果,这里还希望有人能够指正一下。

使用球谐函数计算全局光照的漫反射部分

首先在Unity中创建CalculateSphericalHarmonics.cs, SphericalHarmonicsComputeShader.compute, 和SphericalHarmonics.shader,分别用来执行Compute Shader,计算球谐参数和使用球谐参数重构光照。 我们这里使用grace作为我们的环境贴图,大概长这个样子。

grace probe

SphericalHarmonicsComputeShader.compute

在Compute Shader中我们需要做以下的操作:获取像素的位置;根据像素的位置,获取该像素的立体角和在球面上的方向,并采样球面获得像素的颜色;对每个像素计算出其方向对应的球谐基\(Y_{lm}\);累积所有像素的立体角、颜色和球谐基的乘积,得到\(4\pi\)倍的球谐参数。

首先是获取像素的位置,立体角,方向和采样(Compute Shader中加不了中文注释,就只能将就了):

#pragma kernel CalculateSHMain

TextureCube _CubeMapTexture;
SamplerState sampler_LinearClamp;
//_TextureSize and _DispatchCount are float3 but used as uint3
float3 _TextureSize;
float3 _DispatchCount;

//Check if we are sampling inside texture
bool CheckInRange(uint3 texturesize, uint3 dispatchThreadID)
{
	return !any(dispatchThreadID >= texturesize);
}

//Calculate direction from dispatchThreadID
float3 GetDirectionFromIndex(uint3 textureSize, uint3 dispatchThreadID)
{
    float2 uv = (dispatchThreadID.xy + 0.5) * rcp(textureSize.xy);
    float u = uv.x;
    float v = uv.y;
    float3 dir = float3(0, 0, 0);
    switch (dispatchThreadID.z)
    {
        case 0: //+X
            dir.x = 1;
            dir.y = v * -2.0f + 1.0f;
            dir.z = u * -2.0f + 1.0f;
            break;

        case 1: //-X
            dir.x = -1;
            dir.y = v * -2.0f + 1.0f;
            dir.z = u * 2.0f - 1.0f;
            break;

        case 2: //+Y
            dir.x = u * 2.0f - 1.0f;
            dir.y = 1.0f;
            dir.z = v * 2.0f - 1.0f;
            break;

        case 3: //-Y
            dir.x = u * 2.0f - 1.0f;
            dir.y = -1.0f;
            dir.z = v * -2.0f + 1.0f;
            break;

        case 4: //+Z
            dir.x = u * 2.0f - 1.0f;
            dir.y = v * -2.0f + 1.0f;
            dir.z = 1;
            break;

        case 5: //-Z
            dir.x = u * -2.0f + 1.0f;
            dir.y = v * -2.0f + 1.0f;
            dir.z = -1;
            break;
        }

    return normalize(dir);
}

float AreaElement(float x, float y)
{
    return atan2(x * y, sqrt(x * x + y * y + 1));
}

//Calculate solid angle
float GetWeightFromIndex(uint3 textureSize, uint3 dispatchThreadID)
{
    float2 invTextureSize = rcp(textureSize.xy);
    float2 uv = (dispatchThreadID.xy + 0.5) * invTextureSize;
    uv = uv * 2 - 1;
    float x0 = uv.x - invTextureSize.x;
    float y0 = uv.y - invTextureSize.y;
    float x1 = uv.x + invTextureSize.x;
    float y1 = uv.y + invTextureSize.y;

    return AreaElement(x0, y0) - AreaElement(x0, y1) - AreaElement(x1, y0) + AreaElement(x1, y1);
}

//Texture format is RGBA SFloat, so we do not need to decode sampleHDR
float3 SampleCubeMap(TextureCube cubTex, float3 sampleDir)
{
    float4 sampleHDR = cubTex.SampleLevel(sampler_LinearClamp, sampleDir, 0);
    return sampleHDR.rgb;
}

[numthreads(8, 8, 6)]
void CalculateSHMain(uint3 groupID : SV_GroupID, uint groupIndex : SV_GroupIndex, uint3 dispatchThreadID : SV_DispatchThreadID)
{
	uint indexGroup = groupID.z * _DispatchCount.x * _DispatchCount.y + groupID.y * _DispatchCount.x + groupID.x;
	bool inRange = CheckInRange(_TextureSize, dispatchThreadID);
	if (inRange)
	{
		float3 direction = GetDirectionFromIndex(_TextureSize, dispatchThreadID);
		float weight = GetWeightFromIndex(_TextureSize, dispatchThreadID);
        //I leave out sin(theta) term here
		float3 sampleColor = SampleCubeMap(_CubeMapTexture, direction).rgb * weight;// * sqrt(1 - direction.z * direction.z);
	}
}

按照之前的公式计算球谐基:

struct SHBasis
{
    float y00;
    float y1p1;
    float y10;
    float y1n1;
    float y2p1;
    float y2n1;
    float y2n2;
    float y20;
    float y2p2;
};

SHBasis Evaluate(float3 normal)
{
    SHBasis shBasis = (SHBasis)0;
    shBasis.y00 = 0.2820947917f;
    shBasis.y1p1 = 0.4886025119f * normal.y;
    shBasis.y10 = 0.4886025119f * normal.z;
    shBasis.y1n1 = 0.4886025119f * normal.x;
    shBasis.y2p1 = 1.0925484306f * normal.x * normal.y;
    shBasis.y2n1 = 1.0925484306f * normal.y * normal.z;
    shBasis.y2n2= 0.3153915652f * (3 * normal.z * normal.z - 1.0f);
    shBasis.y20 = 1.0925484306f * normal.x * normal.z;
    shBasis.y2p2 = 0.5462742153f * (normal.x * normal.x - normal.y * normal.y);
    return shBasis;
}

由于Compute Shader的groupshared memory有大小限制,这里我们传入一个_SHPackIndex,分三次计算球谐参数,每次计算三个float3:

//Pack 3 coeffs in one struct
struct CoeffsPack3
{
    float3 coeff0;
    float3 coeff1;
    float3 coeff2;
};

float _SHPackIndex;
groupshared CoeffsPack3 coeffs[8 * 8 * 6];

[numthreads(8, 8, 6)]
void CalculateSHMain(uint3 groupID : SV_GroupID, uint groupIndex : SV_GroupIndex, uint3 dispatchThreadID : SV_DispatchThreadID)
{
	uint indexGroup = groupID.z * _DispatchCount.x * _DispatchCount.y + groupID.y * _DispatchCount.x + groupID.x;
	bool inRange = CheckInRange(_TextureSize, dispatchThreadID);
	if (inRange)
	{
		float3 direction = GetDirectionFromIndex(_TextureSize, dispatchThreadID);
		float weight = GetWeightFromIndex(_TextureSize, dispatchThreadID);
        //I leave out sin(theta) term here
		float3 sampleColor = SampleCubeMap(_CubeMapTexture, direction).rgb * weight;// * sqrt(1 - direction.z * direction.z);
        
        SHBasis shBasis = Evaluate(direction);
        CoeffsPack3 tempCoeffs = (CoeffsPack3)0;

        switch ((uint)_SHPackIndex)
        {
            case 0:
                tempCoeffs.coeff0 = shBasis.y00 * sampleColor;
                tempCoeffs.coeff1 = shBasis.y1p1 * sampleColor;
                tempCoeffs.coeff2 = shBasis.y10 * sampleColor;
                break;
            case 1:
                tempCoeffs.coeff0 = shBasis.y1n1 * sampleColor;
                tempCoeffs.coeff1 = shBasis.y2p1 * sampleColor;
                tempCoeffs.coeff2 = shBasis.y2n1 * sampleColor;
                break;
            case 2:
                tempCoeffs.coeff0 = shBasis.y2n2 * sampleColor;
                tempCoeffs.coeff1 = shBasis.y20 * sampleColor;
                tempCoeffs.coeff2 = shBasis.y2p2 * sampleColor;
                break;
            default:
                break;
        }

        coeffs[groupIndex] = tempCoeffs;
    }
}

最后是对groupshared CoeffsPack3 coeffs[8 * 8 * 6]求和,在GPU Gems 3中介绍了一种多线程的求和算法Parallel Prefix Sum (Scan) with CUDA,这样就能使用GPU来对每一个Thread Group求和并输出到RWStructuredBuffer中了,最后再在CPU中求所有Thread Group的和:

RWStructuredBuffer<CoeffsPack3> _GroupCoefficients;

CoeffsPack3 CoeffsAdd(CoeffsPack3 coeffsA, CoeffsPack3 coeffsB)
{
    CoeffsPack3 tempCoeffs = (CoeffsPack3)0;
    tempCoeffs.coeff0 = coeffsA.coeff0 + coeffsB.coeff0;
    tempCoeffs.coeff1 = coeffsA.coeff1 + coeffsB.coeff1;
    tempCoeffs.coeff2 = coeffsA.coeff2 + coeffsB.coeff2;
    return tempCoeffs;
}

void SumUp(uint groupIndex, uint indexGroup)
{
    GroupMemoryBarrierWithGroupSync();

    uint faceIndex = groupIndex % 64;

    if (faceIndex < 32)
        coeffs[groupIndex] = CoeffsAdd(coeffs[groupIndex], coeffs[groupIndex + 32]);

    GroupMemoryBarrierWithGroupSync();

    if (faceIndex < 16)
        coeffs[groupIndex] = CoeffsAdd(coeffs[groupIndex], coeffs[groupIndex + 16]);

    GroupMemoryBarrierWithGroupSync();

    if (faceIndex < 8)
        coeffs[groupIndex] = CoeffsAdd(coeffs[groupIndex], coeffs[groupIndex + 8]);

    GroupMemoryBarrierWithGroupSync();

    if (faceIndex < 4)
        coeffs[groupIndex] = CoeffsAdd(coeffs[groupIndex], coeffs[groupIndex + 4]);

    GroupMemoryBarrierWithGroupSync();

    if (faceIndex < 2)
        coeffs[groupIndex] = CoeffsAdd(coeffs[groupIndex], coeffs[groupIndex + 2]);

    GroupMemoryBarrierWithGroupSync();

    if (faceIndex < 1)
        coeffs[groupIndex] = CoeffsAdd(coeffs[groupIndex], coeffs[groupIndex + 1]);

    GroupMemoryBarrierWithGroupSync();

    if (groupIndex == 0)
    {
        CoeffsPack3 output = coeffs[0];
        output = CoeffsAdd(output, coeffs[64]);
        output = CoeffsAdd(output, coeffs[128]);
        output = CoeffsAdd(output, coeffs[192]);
        output = CoeffsAdd(output, coeffs[256]);
        output = CoeffsAdd(output, coeffs[320]);

        _GroupCoefficients[indexGroup] = output;
    }
}

[numthreads(8, 8, 6)]
void CalculateSHMain(uint3 groupID : SV_GroupID, uint groupIndex : SV_GroupIndex, uint3 dispatchThreadID : SV_DispatchThreadID)
{
	uint indexGroup = groupID.z * _DispatchCount.x * _DispatchCount.y + groupID.y * _DispatchCount.x + groupID.x;
    bool inRange = CheckInRange(_TextureSize, dispatchThreadID);
	if (inRange)
	{
		float3 direction = GetDirectionFromIndex(_TextureSize, dispatchThreadID);
		float weight = GetWeightFromIndex(_TextureSize, dispatchThreadID);
        //I leave out sin(theta) term here
		float3 sampleColor = SampleCubeMap(_CubeMapTexture, direction).rgb * weight;// * sqrt(1 - direction.z * direction.z);
        
        SHBasis shBasis = Evaluate(direction);
        CoeffsPack3 tempCoeffs = (CoeffsPack3)0;

        switch ((uint)_SHPackIndex)
        {
            case 0:
                tempCoeffs.coeff0 = shBasis.y00 * sampleColor;
                tempCoeffs.coeff1 = shBasis.y1p1 * sampleColor;
                tempCoeffs.coeff2 = shBasis.y10 * sampleColor;
                break;
            case 1:
                tempCoeffs.coeff0 = shBasis.y1n1 * sampleColor;
                tempCoeffs.coeff1 = shBasis.y2p1 * sampleColor;
                tempCoeffs.coeff2 = shBasis.y2n1 * sampleColor;
                break;
            case 2:
                tempCoeffs.coeff0 = shBasis.y2n2 * sampleColor;
                tempCoeffs.coeff1 = shBasis.y20 * sampleColor;
                tempCoeffs.coeff2 = shBasis.y2p2 * sampleColor;
                break;
            default:
                break;
        }

        coeffs[groupIndex] = tempCoeffs;
    }

    SumUp(groupIndex, indexGroup);
}

这样我们就写好了Compute Shader中所有的内容了,现在只需要把所有需要的参数传入Compute Shader就可以了。

CalculateHarmonics.cs

C#脚本中就没什么特别的操作了,创建Compute Buffer,传递到Compute Shader中,执行Compute Shader就能得到包含了每一个Thread Group的球谐参数的和的一个长度为(8 * 8 * 6),每个参数包含3个float3的数组,对这个数组求和之后再除以\(4\pi\)就能得到最终的球谐参数的三项了。执行三个循环,就能获得全部的球谐参数,最后再传递给相应的shader:

using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
using UnityEditor;
using Unity.Mathematics;

public class CalculateSphericalHarmonics : EditorWindow, IDisposable
{
    struct CoeffsPack3
    {
        public float3 coeff0;
        public float3 coeff1;
        public float3 coeff2;

        public CoeffsPack3(float x)
        {
            coeff0 = new float3(x);
            coeff1 = new float3(x);
            coeff2 = new float3(x);
        }
    }

    private CoeffsPack3 SumCoeffs(CoeffsPack3[] coeffsArray)
    {
        CoeffsPack3 tempCoeffs = new CoeffsPack3(0);
        for (int i = 0; i < coeffsArray.Length; i++)
        {
            tempCoeffs.coeff0 += coeffsArray[i].coeff0;
            tempCoeffs.coeff1 += coeffsArray[i].coeff1;
            tempCoeffs.coeff2 += coeffsArray[i].coeff2;
        }
        return tempCoeffs;
    }

    private Cubemap cubeMap;
    private ComputeShader sphericalHarmonicsComputeShader;

    private ComputeBuffer pack3Buffer;
    private ComputeBuffer coefficiencesBuffer;

    private CoeffsPack3[] pack3Array;

    public class Coefficience : ScriptableObject
    {
        public float3[] coefficiencesArray;
    }
    private Coefficience coefficience;

    private MeshRenderer targetMesh;

    private void OnEnable()
    {
        coefficience = CreateInstance<Coefficience>();
    }

    [MenuItem("zznewclear13/Calculate Spherical Harmonics")]
    public static CalculateSphericalHarmonics GetWindow()
    {
        CalculateSphericalHarmonics window = GetWindow<CalculateSphericalHarmonics>("Calculate Spherical Harmonics");
        return window;
    }

    Rect baseRect
    {
        get { return new Rect(20f, 20f, position.width - 40f, position.height - 40f); }
    }

    private void OnGUI()
    {
        using (new GUILayout.AreaScope(baseRect))
        {
            if (!EditorGUIUtility.wideMode)
            {
                EditorGUIUtility.wideMode = true;
                EditorGUIUtility.labelWidth = EditorGUIUtility.currentViewWidth - 212;
            }

            cubeMap = (Cubemap)EditorGUILayout.ObjectField("Cube Map", cubeMap, typeof(Cubemap), false);
            sphericalHarmonicsComputeShader = (ComputeShader)EditorGUILayout.ObjectField("Compute Shader", sphericalHarmonicsComputeShader, typeof(ComputeShader), false);
            targetMesh = (MeshRenderer)EditorGUILayout.ObjectField("Target Mesh", targetMesh, typeof(MeshRenderer), true);

            EditorGUILayout.Space();

            using (new EditorGUI.DisabledGroupScope(!sphericalHarmonicsComputeShader))
            {
                if (GUILayout.Button("Calculate!", GUILayout.Height(30f)))
                {
                    if (cubeMap == null)
                    {
                        cubeMap = new Cubemap(256, TextureFormat.RGBAFloat, false);
                    }
                    Calculate();
                    SetBuffer();
                }
            }

            EditorGUILayout.Space();

            using (new EditorGUI.DisabledGroupScope(true))
            {
                if(coefficience != null)
                {
                    SerializedObject serializedObject = new SerializedObject(coefficience);
                    SerializedProperty arrayProperty = serializedObject.FindProperty("coefficiencesArray");
                    EditorGUILayout.PropertyField(arrayProperty, true);
                    serializedObject.ApplyModifiedProperties();
                }
            }
        }
    }


    private void Calculate()
    {
        coefficience.coefficiencesArray = new float3[9];

        int kernel = sphericalHarmonicsComputeShader.FindKernel("CalculateSHMain");
        sphericalHarmonicsComputeShader.GetKernelThreadGroupSizes(kernel, out uint x, out uint y, out uint z);
        Vector3Int dispatchCounts = new Vector3Int(Mathf.CeilToInt((float)cubeMap.width / (float)x),
                                                    Mathf.CeilToInt((float)cubeMap.height / (float)y),
                                                    Mathf.CeilToInt((float)6 / (float)z));

        if (pack3Buffer != null)
        {
            pack3Buffer.Release();
        }
        pack3Buffer = new ComputeBuffer(dispatchCounts.x * dispatchCounts.y * dispatchCounts.z, 3 * 3 * 4, ComputeBufferType.Structured);
        pack3Array = new CoeffsPack3[dispatchCounts.x * dispatchCounts.y * dispatchCounts.z];
        for (int i = 0; i < dispatchCounts.x * dispatchCounts.y * dispatchCounts.z; i++)
        {
            pack3Array[i] = new CoeffsPack3(0);
        }
        pack3Buffer.SetData(pack3Array);

        //用于确认传入的参数
        Debug.Log("_TextureSize: " + cubeMap.width + ", " + cubeMap.height + ", " + 6);
        Debug.Log("_ThreadGroups: " + x + ", " + y + ", " + z);
        Debug.Log("_DispatchCounts: " + dispatchCounts.x + ", " + dispatchCounts.y + ", " + dispatchCounts.z);
        Debug.Log("_GroupCounts: " + dispatchCounts.x * dispatchCounts.y * dispatchCounts.z);
        Debug.Log("_GroupThreadCounts: " + x * y * z);
        Debug.Log("_CubeMapTexture: " + cubeMap.name);
        Debug.Log("---------------------------------");

        sphericalHarmonicsComputeShader.SetTexture(kernel, "_CubeMapTexture", cubeMap);
        sphericalHarmonicsComputeShader.SetBuffer(kernel, "_GroupCoefficients", pack3Buffer);
        sphericalHarmonicsComputeShader.SetVector("_DispatchCount", (Vector3)dispatchCounts);
        sphericalHarmonicsComputeShader.SetVector("_TextureSize", new Vector3(cubeMap.width, cubeMap.height, 6));

        float inv4PI = 0.25f / Mathf.PI;
        //分三次,每次计算三个球谐参数
        for (int shPackIndex = 0; shPackIndex < 3; shPackIndex++)
        {
            sphericalHarmonicsComputeShader.SetFloat("_SHPackIndex", shPackIndex);
            sphericalHarmonicsComputeShader.Dispatch(kernel, dispatchCounts.x, dispatchCounts.y, dispatchCounts.z);
            Debug.Log("Dispatch with (" + dispatchCounts.x + ", " + dispatchCounts.y + ", " + dispatchCounts.z + ").");
            pack3Buffer.GetData(pack3Array);
            CoeffsPack3 tempCoeffs = SumCoeffs(pack3Array);

            coefficience.coefficiencesArray[shPackIndex * 3 + 0] = tempCoeffs.coeff0 * inv4PI;
            coefficience.coefficiencesArray[shPackIndex * 3 + 1] = tempCoeffs.coeff1 * inv4PI;
            coefficience.coefficiencesArray[shPackIndex * 3 + 2] = tempCoeffs.coeff2 * inv4PI;
        }
        pack3Buffer.Release();

        string debugLog = "";
        for (int i = 0; i < 9; i++)
        {
            debugLog += coefficience.coefficiencesArray[i] + "...";
        }
    }

    //给对应的mesh renderer的材质传入buffer
    private void SetBuffer()
    {
        if(coefficiencesBuffer != null)
        {
            coefficiencesBuffer.Release();
        }
        coefficiencesBuffer = new ComputeBuffer(9, 3 * 4, ComputeBufferType.Structured);
        coefficiencesBuffer.SetData(coefficience.coefficiencesArray);
        targetMesh.sharedMaterial.SetBuffer("_CoefficienceBuffer", coefficiencesBuffer);
    }

    public void Dispose()
    {
        pack3Buffer.Release();
        coefficiencesBuffer.Release();
    }
}

SphericalHarmonics.shader

从传入的StructuredBuffer中获取球谐参数,计算辐照度,同时我还增加了一个选项,用于对比我计算出的球谐参数和实际的球谐参数:

Shader "Unlit/SphericalHarmonicsShader"
{
    Properties
    {
        [Toggle(USE_GRACE)]_Use_Grace ("Use Grace", float) = 1
    }

    HLSLINCLUDE
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

    #pragma multi_compile _ USE_GRACE

#if USE_GRACE
#define Coeffs grace
#else
#define Coeffs _CoefficienceBuffer
#endif

    StructuredBuffer<float3> _CoefficienceBuffer;

    static float3 grace[] =
    {
        float3(0.7953949, 0.4405923, 0.5459412),
        float3(0.3981450, 0.3526911, 0.6097158),
        float3(-0.3424573, -0.1838151, -0.2715583),
        float3(-0.2944621, -0.0560606, 0.0095193),
        float3(-0.1123051, -0.0513088, -0.1232869),
        float3(-0.2645007, -0.2257996, -0.4785847),
        float3(-0.1569444, -0.0954703, -0.1485053),
        float3(0.5646247, 0.2161586, 0.1402643),
        float3(0.2137442, -0.0547578, -0.3061700)
    };

    struct Attributes
    {
        float4 positionOS   : POSITION;
        float2 uv           : TEXCOORD0;
        float3 normalOS     : NORMAL;
    };

    struct Varyings
    {
        float4 positionCS   : SV_POSITION;
        float2 texcoord     : TEXCOORD0;
        float3 normalWS     : TEXCOORD1;
    };

    float3 CalcIrradiance(float3 nor) { 
        float c1 = 0.429043;
        float c2 = 0.511664;
        float c3 = 0.743125;
        float c4 = 0.886227;
        float c5 = 0.247708;
        return (
            c1 * Coeffs[8] * (nor.x * nor.x - nor.y * nor.y) +
            c3 * Coeffs[6] * nor.z * nor.z +
            c4 * Coeffs[0] -
            c5 * Coeffs[6] +
            2.0 * c1 * Coeffs[4] * nor.x * nor.y +
            2.0 * c1 * Coeffs[7] * nor.x * nor.z +
            2.0 * c1 * Coeffs[5] * nor.y * nor.z +
            2.0 * c2 * Coeffs[3] * nor.x +
            2.0 * c2 * Coeffs[1] * nor.y +
            2.0 * c2 * Coeffs[2] * nor.z
            );
    }

    Varyings Vert(Attributes input)
    {
        Varyings output = (Varyings)0;
        VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
        VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS);
        output.positionCS = vertexInput.positionCS;
        output.texcoord = input.uv;
        output.normalWS = normalInput.normalWS;
        return output;
    }

    float4 Frag(Varyings input) : SV_TARGET
    {
        float3 shColor = CalcIrradiance(normalize(input.normalWS));
        return float4(shColor, 1);
    }

    ENDHLSL

    SubShader
    {
        Pass
        {
            Name "Spherical Harmonics Pass"
            Tags{"LightMode" = "UniversalForward"}
            Cull Back
            ZTest LEqual
            ZWrite On
            HLSLPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag
            ENDHLSL
        }
    }
}

最后点击“Calculate!”就能看到我们重新构建的全局光照的漫反射部分的光照信息了,大概就是这个样子。

Spherical Harmonics