球谐函数实验

实验参考这篇全面讲解的球谐光照的pdf
spherical-harmonic-lighting.pdf
以前几次看到球谐的地方就停下来了,因为最讨厌的就是这些文章直接告诉你有无数个正交的球谐基,但怎么来的不知道。也有一些文章说这些基是来自于解拉普拉斯方程,但为什么解一个偏微分方程就能得到正交的球谐基呢,也没有哪里给出明确的解释。或许这需要对这个方程解空间的结构进行学习才能了解。对于我来说超纲了,先接受这个事实,在接受这个事实的情况下,进行实验。
实验要达到的结果就是
环境辐照度图–>分解球谐系数–>通过系统重建辐照度图
说白了就是一个傅立叶变换和反变换的过程。

一. 通过ComputeShader实现SH系数计算

这里的SH系数计算我打算采用CS的方式来计算,将CubeMap传入CS,然后将预计算的采样点也传入CS。然后开8个线程,将计算结果写入一个Vector3的数组中。

public class Reconstruction : MonoBehaviour
{
    const int n_bands = 3;

    [StructLayout(LayoutKind.Explicit)]
    public unsafe struct SHSample
    {
        //public Vector3 sph;

        [FieldOffset(0)]
        public Vector3 vec;
        [FieldOffset(12)]
        public fixed float coeff[n_bands * n_bands];
    }    
    SHSample[] samples;
	....

这里先定义预计算采样点的数据结构SHSample, 因为要传给cs,所以结构体必须是指定内存布局的。SHSample的vec指明了采样的方向向量,coeff存放了在这个方向上,前9个球谐基函数的返回值。n_bands就是球谐基的层数,3层球谐基一共有9个。

接下来照搬前文提到的pdf文献中的所有函数。其中接口函数就是SH_setup_spherical_samples,它就是传入采样点个数和球谐层数,计算出来的结果放入数组,代码中的采样是cosinWeight的采样,具体可见[此处文献]


    unsafe void SH_setup_spherical_samples(SHSample[] samples, int sqrt_n_samples, int n_bands)
    {
        // fill an N*N*2 array with uniformly distributed
        // samples across the sphere using jittered stratification
        int i = 0; // array index
        float oneoverN = 1.0f / sqrt_n_samples;
        for (int a = 0; a < sqrt_n_samples; a++)
        {
            for (int b = 0; b < sqrt_n_samples; b++)
            {
                // generate unbiased distribution of spherical coords
                float x = (a + Random.Range(0f, 1.0f)) * oneoverN; // do not reuse results
                float y = (b + Random.Range(0f, 1.0f)) * oneoverN; // each sample must be random
                float theta = 2.0f * Mathf.Acos(Mathf.Sqrt(1.0f - x));
                float phi = 2.0f * Mathf.PI * y;
                //sph = new Vector3(theta, phi, 1.0f);
                // convert spherical coords to unit vector
                samples[i].vec = new Vector3(Mathf.Sin(theta) * Mathf.Cos(phi), Mathf.Sin(theta) * Mathf.Sin(phi), Mathf.Cos(theta));


                // precompute all SH coefficients for this sample
                for (int l = 0; l < n_bands; ++l)
                {
                    for (int m = -l; m <= l; ++m)
                    {
                        int index = l * (l + 1) + m;

                        fixed (float* pCoeef = samples[i].coeff)
                        {
                            pCoeef[index] = SH(l, m, theta, phi);
                        }
                    }
                }

                ++i;
            }
        }
    }


    float P(int l, int m, float x)
    {
        // evaluate an Associated Legendre Polynomial P(l,m,x) at x
        float pmm = 1.0f;
        if (m > 0)
        {
            float somx2 = Mathf.Sqrt((1.0f - x) * (1.0f + x));

            float fact = 1.0f;
            for (int i = 1; i <= m; i++)
            {
                pmm *= (-fact) * somx2;
                fact += 2.0f;
            }
        }
        if (l == m) return pmm;
        float pmmp1 = x * (2.0f * m + 1.0f) * pmm;
        if (l == m + 1) return pmmp1;
        float pll = 0.0f;
        for (int ll = m + 2; ll <= l; ++ll)
        {
            pll = ((2.0f * ll - 1.0f) * x * pmmp1 - (ll + m - 1.0f) * pmm) / (ll - m);
            pmm = pmmp1;
            pmmp1 = pll;
        }
        return pll;
    }

    int factorial(int n)
    {
        int result = 1;
        for (int i = 2; i <= n; ++i)
        {
            result *= i;
        }
        return result;

    }

    float K(int l, int m)
    {
        // renormalisation constant for SH function
        float temp = ((2.0f * l + 1.0f) * factorial(l - m)) / (4.0f * Mathf.PI * factorial(l + m));
        return Mathf.Sqrt(temp);
    }
    float SH(int l, int m, float theta, float phi)
    {
        // return a point sample of a Spherical Harmonic basis function
        // l is the band, range [0..N]
        // m in the range [-l..l]
        // theta in the range [0..Pi]
        // phi in the range [0..2*Pi]
        float sqrt2 = Mathf.Sqrt(2.0f);
        if (m == 0) return K(l, 0) * P(l, m, Mathf.Cos(theta));
        else if (m > 0) return sqrt2 * K(l, m) * Mathf.Cos(m * phi) * P(l, m, Mathf.Cos(theta));
        else return sqrt2 * K(l, -m) * Mathf.Sin(-m * phi) * P(l, -m, Mathf.Cos(theta));
    }

接下来写Start函数。在启动的时候,先预计算出32*32=1024个球面采样点,每个点的9个球谐基系数。然后它传入compute shader进行计算,computer shader开一个线程组,每个组含9个线程,每个线程计算一个投影系数,最终将反馈的结果存入coeff数组中,代码如下:

    public ComputeShader myshader;
    public Cubemap cube;
    Vector4[] coeff = new Vector4[n_bands * n_bands];
    unsafe void RunShader()
    {
        int k = myshader.FindKernel("CSMain");
        myshader.SetTexture(k, "_CubeTex", cube);

        ComputeBuffer sampleDataBuffer = new ComputeBuffer(samples.Length, Marshal.SizeOf(typeof(SHSample)));

        sampleDataBuffer.SetData(samples);
        myshader.SetBuffer(k, "sampleData", sampleDataBuffer);
        myshader.SetInt("n_samples", samples.Length);

        ComputeBuffer resultBuffer = new ComputeBuffer(n_bands * n_bands, sizeof(Vector4));
        myshader.SetBuffer(k, "Result", resultBuffer);

        myshader.Dispatch(k, 1, 1, 1);

        resultBuffer.GetData(coeff);

        for (int i = 0; i < coeff.Length; ++i)
        {
            Debug.Log(string.Format("output{0}: {1}, {2}, {3}", i, coeff[i].x, coeff[i].y, coeff[i].z));
        }
    }

    void Start()
    {
        int sqrt_n_samples = 32;
        samples = new SHSample[sqrt_n_samples * sqrt_n_samples];
        SH_setup_spherical_samples(samples, sqrt_n_samples, n_bands);

        RunShader();
    }

接下来是Compute Shader的写法, 其中开的线程是一维的,一共有9个。然后对CubeMap的采样必须用_CubeTex.SampleLevel, 用texCUBE或Sample都是编译不过的。最后采用求积分的方式将结果存在Result数组中

#pragma kernel CSMain


struct SampleStruct
{
    float3 vec;
    float coeff[9];
};

RWStructuredBuffer<SampleStruct> sampleData;
RWStructuredBuffer<float4> Result;

TextureCube<float4> _CubeTex; 
SamplerState my_Linear_clamp_sampler;
int n_samples;



[numthreads(9, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{ 
    const double weight = 4.0f * 3.14159265f;

    float4 product = 0;
    for (int i = 0; i<n_samples; ++i)
    {
        float4 color = _CubeTex.SampleLevel(my_Linear_clamp_sampler, sampleData[i].vec, 0);
        product += (color * sampleData[i].coeff[id.x]);
    }

    // divide the result by weight and number of samples
    float factor =  weight / n_samples;
    Result[id.x] = product *factor;
}

然后运行场景,在控制台得到如下日志
在这里插入图片描述
日志输出了,九组投影系数的RGB分量,那么这个结果对不对,需要验证一下,我们就利用这组值,来还原整个cubemap

二. 通过SH系数重建CubeMap

还原的和上一篇博客搭的框架基本一致
场景辐照度练习
主要方式就是在场景中放一个单位正方体,然后用摄像机拍6个表面,然后将系数传入Shader,在Shader中进行重建

        camera.transform.position = Vector3.zero;
        camera.transform.up = Vector3.up;
        for (int i = 0; i < 6; ++i)
        {
            rt[i] = new RenderTexture(512, 512, 0, RenderTextureFormat.ARGBFloat);
            camera.targetTexture = rt[i];
            camera.transform.forward = forwards[i];
            Shader.SetGlobalVectorArray("_coeff", coeff); //系数传入shader
            camera.RenderWithShader(SHConstructionShader, "");
            camera.targetTexture = null;
        }

而Shader的处理也很简单,在VS中计算好世界坐标,作为每个像素的采样输入,然后将这个坐标代入九个球谐基函数(参考前面给出的PDF),最后和系数相乘。最后因为要保存到文件,所以需要gamma校正

Shader "Unlit/SHConstruction"
{
    Properties
    {
    }
    
    SubShader
    {
        Tags{ "RenderType" = "Opaque" }
        LOD 100

        Pass
        {
            Cull Off
            CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float3 worldPos :TEXCOORD1;
                float4 vertex : SV_POSITION;
            };


            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);

                return o;
            }


            float4 _coeff[9];
            fixed4 frag(v2f i) : SV_Target
            {

                float3 N = normalize(i.worldPos);

                float kPI = 3.1415926f;
                float kSqrtPI = sqrt(kPI);
                float kSqrt3 = sqrt(3.0f);
                float kSqrt15 = sqrt(15.0f);

                float x = N.x;
                float y = N.y;
                float z = N.z;

                float outsh[9];
                outsh[0] = 1.0f / (2.0f * kSqrtPI);
                outsh[1] = -(kSqrt3 * y) / (2.0f * kSqrtPI);
                outsh[2] = (kSqrt3 * z) / (2.0f * kSqrtPI);
                outsh[3] = -(kSqrt3 * x) / (2.0f * kSqrtPI);
                outsh[4] = (kSqrt15 * x * y) / (2.0f * kSqrtPI);
                outsh[5] = -(kSqrt15 * y * z) / (2.0f * kSqrtPI);
                outsh[6] = (sqrt(5.0f) * (3.0f * z * z - 1.0f)) / (4.0f * kSqrtPI);
                outsh[7] = -(kSqrt15 * x * z) / (2.0f * kSqrtPI);
                outsh[8] = (kSqrt15 * (x * x - y * y)) / (4.0f * kSqrtPI);

                float4 FragColor = float4(0, 0, 0, 1);
                for (int i = 0; i <9 ; ++i)
                {
                    FragColor += (outsh[i] * _coeff[i]);
                }

                FragColor = float4(LinearToGammaSpace(FragColor.rgb), 1);

                return FragColor;
            }
            ENDCG
        }
    }
}

下面是重建结果和原图的对照,看起来像那么回事
辐照度原图
重建图

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值