Global Illumination_Linearly Transformed Cosines (LTC)

在这里插入图片描述

一、面光源

为什么多边形光源着色很复杂?

因为使用多边形灯光进行着色时需要在灯光覆盖的多边形域上集成 BRDF分布,如下图所示:

在这里插入图片描述

尽管多边形光源理论上是简单的光照模型,但它们在实时渲染中却不容易实现,主要有两个原因:

  • 1:在球面多边形上积分参数球面分布通常很困难,即使是最简单的分布也是如此。
  • 2:基于物理的材料模型不是简单的分布;它们具有复杂的形状,如具有各向异性的材质及其拉伸和偏斜等特性,BRDF都需要需要对这些形状进行表示,以使材料更加真实。

我们也可以来看一下面光源的计算方程:

在这里插入图片描述
其中,S 为面光源区域,s 为面光源上的某一个点,p 为着色点, np 为着色点法线, wisp的向量, θpnpwi 的夹角,L 面光源上纹理的颜色。从这个积分式子就可以看出为什么光泽反射的面光源为什么难以计算了。

二、线性变换余弦算法

2.1 定义

线性变换球面分布(Linearly Transformed Spherical Distributions (LTSDs))的思想就是:对于任意一个球面分布函数,一定可以通过一个线性变换矩阵将其变化到另外一个球面分布函数

即下式表示:
在这里插入图片描述
M 就为线性变换矩阵,也就是说对于任意一个 f(p,wi,wo) 一定找的到一个M变换矩阵把他变换到 cos(θs) 。不过注意这里的*不是普通的矩阵乘法,这里的线性变换是指把我们的入射向量乘以矩阵M。
在这里插入图片描述
其中
[公式]
, [公式]
在这里插入图片描述

J 这一项是雅可比行列式,其实就是线性变换之后面积发生变化进行一个归一化操作,这一项也是和M矩阵一样提前预计算好并且存在纹理中方便采样。

上边的式子也许已经让你头大了,我们接下来以直观的方式来看一下线性变换余弦的内在操作,你可以这样理解:线性变换余弦 (LTC),这是一种新的球面分布,它涵盖了各种各样的球面形状,并且可以在任意多边形上进行分析积分。

首先我们来看一下一个简单的余弦分布,如下图:
在这里插入图片描述

之后我们对其方向向量应用线性变换,允许控制分布形状的属性,例如粗糙度、各向异性和偏斜度等参数(分别如下三个动图所示):

在这里插入图片描述在这里插入图片描述在这里插入图片描述

接下来主要是处理的是:

  • 通过使用M矩阵对原始的光照分布进行线性变换

2.1.1 线性变换矩阵M

在这里插入图片描述

如上图所示,从左到右,对于一个原始的光照分布Do,可以通过将其与矩阵M相乘得到全新的光照分布D。同时要注意选择不同的Do会得到不同的结果,如下图所示:
在这里插入图片描述
由于是线性变换,分布D上的方向向量ω可以通过M的逆矩阵变换回Do上的方向向量ωo。
在这里插入图片描述

注意不管ω还是ωo都是标准向量(模为1的向量)。

综上所述,线性变换后的分布D的计算方程可以表示为:

在这里插入图片描述

其中偏微分 ∂ω0/∂ω 其实是Jacobian(雅各比)矩阵,乘上雅可比矩阵(这里是行列式)的原因:由于分布D0到分布D进行了线性变换,对于一个微面元来说,它的面积很有可能发生改变(如下面即将叙述部分的图片所示),而雅可比行列式的绝对值等于变换前后微面元的面积之比,因此需要乘上雅可比式,从而消除因线性变换导致的积分误差。

2.1.2 线性变换矩阵M

雅各比矩阵的基本概念如下:

在这里插入图片描述

对于在球面坐标系的分布D0来说,它进行了线性变换得到了分布D,所以完全可以通过雅各比矩阵将D0中的所有向量ω0带入得到D中的所有向量ω,反过来同理。为此需要得到分布D0到分布D之间的线性变换关系,由此计算出雅各比矩阵(这里应该是行列式)的值,从而直接进行计算。

在这里插入图片描述

如上图所示,(ω0,ω1,ω2)是一组正交基,∂ω0表示的是一个无穷小立体角在球面上的截面面积,经过M矩阵的线性变换后,原始的正交基变为(Mω0,Mω1,Mω2),设M∂ω0投影到球面后为∂ω(图中绿色部分)则∂ω的计算公式为:

在这里插入图片描述

其中,∂ω0A表示的是经过矩阵M变换后的面积(图中右侧的红色部分),r是红色部分到球体中心点的距离,θ是Mω0与红色平面的法线的夹角。这三者的具体计算公式如下图所示:

在这里插入图片描述

其中Mω1×Mω2可以得到下述结果:

在这里插入图片描述

所以有:

在这里插入图片描述

注意,如果M仅仅进行缩放、旋转变换,则∂ω0/∂ω的值为1。

2.1.3 矢量中值

一般情况下,会定义一个称为矢量中值(Median Vector)的向量,对于原始分布Do来说,它的矢量中值即为(0,0,1)即Z轴,因为任何一个过Z轴的平面都能够将整个球体平分成两部分。矢量中值有一个特性:Do上的矢量中值经过变换后肯定也是D上的矢量中值。

2.2 近似BRDF

通过不同变换它们可以涵盖的各种球形,所以线性变换余弦可以类接近BRDF分布。可以看一下下面的一个示例: GGX BRDF(左)与 LTC(右)近似以用于不同的入射方向。
在这里插入图片描述
当然,这种近似并不完美,但它有效地对照了 BRDF 针对不同粗糙度和入射方向等主要特征。

2.2.1 用线性变换余弦逼近基于物理的BRDF

首先对于原始的分布D0,选择由z方向给出的半球中的标准化余弦分布,公式如下:
在这里插入图片描述

对于分布D来说我们想要其无限靠近BRDF计算得到的结果,用数学表达即为:
在这里插入图片描述

对于各向同性材质来说,BRDF由观察方向ωv=(sinθ,0,cosθ)以及粗糙度α决定,对于任意一组(θ,α)的组合,都需要找到一个M矩阵进行匹配,使其结果无限靠近BRDF的计算结果。M矩阵的通式如下:
在这里插入图片描述

所以其实只需要4个变量值即可完成一个M矩阵的匹配。其匹配的思路是通过Nelder-Mead单纯形算法不同的计算误差,直到误差小于一定值后即为匹配成功。
M矩阵的匹配计算一般在预处理时进行计算,通常会使用两张贴图来保存:由(θ,α)计算出的M矩阵(四个浮点数表示)、由(θ,α)计算出的BRDF系数(一个浮点数)、菲涅尔方程系数(一个浮点数)。

当完成M矩阵的计算后,光照方程即为下式:
在这里插入图片描述

对于多边形常量颜色的面光源来说:

在这里插入图片描述

在这里插入图片描述

对于基于图像的面光源来说:

在这里插入图片描述

公式可以分成两部分处理,第一部分是对多边形进行积分。第二部分是对纹理颜色进行积分。

2.3 多边形积分

由于它们的线性不变特性,在多边形上积分 LTC 等价于在逆线性变换变换的多边形上积分原始余弦分布(它只是变换多边形的辐照度(形状因子),其闭合形式表达式相同)

具体转换过程如下所示:

  • 1.LTC-多边形积分:

在这里插入图片描述

  • 2.变换过程
    在这里插入图片描述
  • 3.余弦多边形积分
    在这里插入图片描述

2.3.1 在多边形上进行积分

变换后的分布D在变换后的多边形P上的积分等于变换前的分布Do在变换前的多边形Po上的积分。如下图所示:

在这里插入图片描述

用数学公式表现为:

在这里插入图片描述

2.3.2 重要性采样

通过概率密度函数在分布D0上得到一组随机采样向量ω0,由于是线性变换,这些随机采样向量变换后即为分布D上的随机采样向量ω,如下图所示:

在这里插入图片描述

三、代码实现

参照:https://www.freesion.com/article/2509217151/

3.1 M矩阵预计算部分

(1)算法总体部分

void fitTab(mat3* tab, vec2* tabMagFresnel, const int N, const Brdf& brdf)
{
    LTC ltc;
 
    // 第一层循环枚举粗糙度,第二层循环枚举观察向量与平面的夹角从而得到与法线的夹角theta
    for (int a = N - 1; a >=     0; --a)
    for (int t =     0; t <= N - 1; ++t)
    {
        // 通过比率枚举观察向量与平面的夹角大小
        float x = t/float(N - 1);
        // 为了获得观察向量与法线的夹角所要1-x^2
        float ct = 1.0f - x*x; 
        // 通过反函数得到角度theta
        float theta = std::min<float>(1.57f, acosf(ct));
        // 计算观察向量
        const vec3 V = vec3(sinf(theta), 0, cosf(theta));
 
        // 由比率枚举粗糙度并计算得到粗糙度的平方值α
        float roughness = a/float(N - 1);
        float alpha = std::max<float>(roughness*roughness, MIN_ALPHA);
 
        vec3 averageDir;
        // 计算BRDF(不包含菲涅尔系数)项与菲涅尔系数项与重要性采样大致方向(用于获得一个初始单纯形)
        computeAvgTerms(brdf, V, alpha, ltc.magnitude, ltc.fresnel, averageDir);
 
        bool isotropic;
 
        // 1. first guess for the fit
        // 首先要猜测一个M矩阵即
        // init the hemisphere in which the distribution is fitted
        // 初始化一个较为合适的半球体分布函数D0
        // if theta == 0 the lobe is rotationally symmetric and aligned with Z = (0 0 1)
        // 如果theta==0,则lobe是关于Z轴各向对称的
        if (t == 0)
        {
            ltc.X = vec3(1, 0, 0);
            ltc.Y = vec3(0, 1, 0);
            ltc.Z = vec3(0, 0, 1);
 
            // 根据粗糙度把M矩阵对角线上的变量进行赋值
            if (a == N - 1) 
            {
                ltc.m11 = 1.0f;
                ltc.m22 = 1.0f;
            }
            else
            {
                ltc.m11 = tab[a + 1 + t*N][0][0];
                ltc.m22 = tab[a + 1 + t*N][1][1];
            }
 
            ltc.m13 = 0;
            // 更新LTC的M矩阵及其逆矩阵
            ltc.update();
 
            isotropic = true;
        }
        // 使用先前计算的重要性采样的大致方向作为第一个猜测
        else
        {
            vec3 L = averageDir;
            vec3 T1(L.z, 0, -L.x);
            vec3 T2(0, 1, 0);
            ltc.X = T1;
            ltc.Y = T2;
            ltc.Z = L;
 
            ltc.update();
 
            isotropic = false;
        }
 
        // 通过单纯形法不断修正M矩阵使积分结果无限近似BRDF
        float epsilon = 0.05f;
        fit(ltc, brdf, V, alpha, epsilon, isotropic);
 
        // 存储M矩阵、BRDF参数、菲涅尔系数
        tab[a + t*N] = ltc.M;
        tabMagFresnel[a + t*N][0] = ltc.magnitude;
        tabMagFresnel[a + t*N][1] = ltc.fresnel;
 
        tab[a+t*N][0][1] = 0;
        tab[a+t*N][1][0] = 0;
        tab[a+t*N][2][1] = 0;
        tab[a+t*N][1][2] = 0;
    }
}

(2)重要性采样以及BRDF系数的计算

void computeAvgTerms(const Brdf& brdf, const vec3& V, const float alpha, float& norm, float& fresnel, vec3& averageDir)
{
    norm = 0.0f;
    fresnel = 0.0f;
    averageDir = vec3(0, 0, 0);
 
    for (int j = 0; j < Nsample; ++j)
    for (int i = 0; i < Nsample; ++i)
    {
        const float U1 = (i + 0.5f)/Nsample;
        const float U2 = (j + 0.5f)/Nsample;
 
        // sample
        // 通过U1、U2枚举各方向的法线并通过观察向量V计算出入射光方向
        const vec3 L = brdf.sample(V, alpha, U1, U2);
        /* brdf.sample(V, alpha, U1, U2)的具体实现
        virtual vec3 sample(const vec3& V, const float alpha, const float U1, const float U2) const
        {
            // 计算方位角
            const float phi = 2.0f * 3.14159f * U1;
            // 修正粗糙度
            const float r = alpha * sqrtf(U2 / (1.0f - U2));
            // 通过枚举法线方向而不是观察方向得到所有法线与观察向量的组合情况
            const vec3 N = normalize(vec3(r * cosf(phi), r * sinf(phi), 1.0f));
            // 通过公式计算反射向量即入射光方向
            const vec3 L = -V + 2.0f * N * dot(N, V);
            return L;
        }
        */
 
        // eval
        // 计算BRDF(不含菲尼尔系数项)方程值,并得到其概率密度函数pdf
        float pdf;
        float eval = brdf.eval(V, L, alpha, pdf);
        /* brdf.eval(V, L, alpha, pdf)的具体实现如下
        virtual float eval(const vec3& V, const vec3& L, const float alpha, float& pdf) const
        {
            // 表示观察方向垂直表面(cos(theta)==0)
            if (V.z <= 0)
            {
                pdf = 0;
                return 0;
            }
            // masking
            const float LambdaV = lambda(alpha, V.z);
            //lambda函数的具体实现(这部分公式跟我所知的BRDF的公式不太一样。。。。)
            //float lambda(const float alpha, const float cosTheta) const
            //{
            //   const float a = 1.0f / alpha / tanf(acosf(cosTheta));
            //   return (cosTheta < 1.0f) ? 0.5f * (-1.0f + sqrtf(1.0f + 1.0f/a/a)) : 0.0f;    
            //}
            
            // shadowing
            // 计算几何遮蔽值
            float G2;
            if (L.z <= 0.0f)
                G2 = 0;
            else
            {
                const float LambdaL = lambda(alpha, L.z);
                G2 = 1.0f/(1.0f + LambdaV + LambdaL);
            }
            // D
            // 法线分布函数部分
            const vec3 H = normalize(V + L);
            const float slopex = H.x/H.z;
            const float slopey = H.y/H.z;
            float D = 1.0f / (1.0f + (slopex*slopex + slopey*slopey)/alpha/alpha);
            D = D*D;
            D = D/(3.14159f * alpha*alpha * H.z*H.z*H.z*H.z);
            // 计算出BRDF(不含菲涅尔系数)值以及概率密度函数,注意一般情况下时通过pdf计算出cdf,
            // 然后带入一个随机变量可计算出重要性采样所需的向量,这里由于是枚举所有情况,
            // 所以可以反向求出pdf,按照加权平均数的方式,计算出每种情况的贡献值
            pdf = fabsf(D * H.z / 4.0f / dot(V, H));
            float res = D * G2 / 4.0f / V.z;
            return res;
        }
        */
 
        if (pdf > 0)
        {
            // 计算权重(重要性采样)
            float weight = eval / pdf;
 
            vec3 H = normalize(V+L);
 
            // accumulate
            norm       += weight;
            // 计算Schlick-菲涅尔项
            fresnel    += weight * pow(1.0f - glm::max(dot(V, H), 0.0f), 5.0f);
            // 计算重要性采样的大致方向(反射镜叶)
            averageDir += weight * L;
        }
    }
 
    norm    /= (float)(Nsample*Nsample);
    fresnel /= (float)(Nsample*Nsample);
 
    // 清除y分量,因为各向同性BRDF
    averageDir.y = 0.0f;
    // 重要性采样的平均方向(镜叶方向)
    averageDir = normalize(averageDir);
}

(3)通过单纯形算法修正M矩阵

https://blog.csdn.net/zhoudi2010/article/details/54584495
  • a、算法总体部分
void fit(LTC& ltc, const Brdf& brdf, const vec3& V, const float alpha, const float epsilon = 0.05f, const bool isotropic = false)
{
    float startFit[3] = { ltc.m11, ltc.m22, ltc.m13 };
    float resultFit[3];
 
    FitLTC fitter(ltc, brdf, isotropic, V, alpha);
 
    // 通过NelderMead单纯形算法,计算误差得到最终的M矩阵
    float error = NelderMead<3>(resultFit, startFit, epsilon, 1e-5f, 100, fitter);
 
    // 更新M矩阵
    fitter.update(resultFit);
}

  • b、NelderMead单纯形算法
template<int DIM, typename FUNC>
float NelderMead(float* pmin, const float* start, float delta, float tolerance, int maxIters, FUNC objectiveFn)
{
    // 反射点系数
    const float reflect  = 1.0f;
    // 膨胀点系数
    const float expand   = 2.0f;
    // 压缩系数
    const float contract = 0.5f;
    // 收缩系数
    const float shrink   = 0.5f;
 
    typedef float point[DIM];
    const int NB_POINTS = DIM + 1;
 
    point s[NB_POINTS];
    float f[NB_POINTS];
 
    // 初始化单纯形:将原始M矩阵的每一行当作单纯形的一个顶点
    mov(s[0], start, DIM);
    for (int i = 1; i < NB_POINTS; i++)
    {
        mov(s[i], start, DIM);
        s[i][i - 1] += delta;
    }
 
    // 对于单纯形的每个顶点计算函数结果
    for (int i = 0; i < NB_POINTS; i++)
        f[i] = objectiveFn(s[i]);
 
    int lo = 0, hi, nh;
 
    for (int j = 0; j < maxIters; j++)
    {
        // 找到最小值、最大值和次大值
        lo = hi = nh = 0;
        for (int i = 1; i < NB_POINTS; i++)
        {
            if (f[i] < f[lo])
                lo = i;
            if (f[i] > f[hi])
            {
                nh = hi;
                hi = i;
            }
            else if (f[i] > f[nh])
                nh = i;
        }
 
        // 如果误差达到要求则停止算法
        float a = fabsf(f[lo]);
        float b = fabsf(f[hi]);
        if (2.0f*fabsf(a - b) < (a + b)*tolerance)
            break;
 
        // 计算单纯形的重心点
        point o;
        set(o, 0.0f, DIM);
        for (int i = 0; i < NB_POINTS; i++)
        {
            if (i == hi) continue;
            add(o, s[i], DIM);
        }
        for (int i = 0; i < DIM; i++)
            o[i] /= DIM;
 
        // 计算反射点以及函数值
        point r;
        for (int i = 0; i < DIM; i++)
            r[i] = o[i] + reflect*(o[i] - s[hi][i]);
 
        float fr = objectiveFn(r);
        if (fr < f[nh])
        {
            if (fr < f[lo])
            {
                // 计算膨胀点以及函数值
                point e;
                for (int i = 0; i < DIM; i++)
                    e[i] = o[i] + expand*(o[i] - s[hi][i]);
 
                float fe = objectiveFn(e);
                if (fe < fr)
                {
                    mov(s[hi], e, DIM);
                    f[hi] = fe;
                    continue;
                }
            }
 
            mov(s[hi], r, DIM);
            f[hi] = fr;
            continue;
        }
 
        // 计算收缩点以及函数值
        point c;
        for (int i = 0; i < DIM; i++)
            c[i] = o[i] - contract*(o[i] - s[hi][i]);
 
        float fc = objectiveFn(c);
        if (fc < f[hi])
        {
            mov(s[hi], c, DIM);
            f[hi] = fc;
            continue;
        }
 
        // 向目前的最小值点进行收缩
        for (int k = 0; k < NB_POINTS; k++)
        {
            if (k == lo) continue;
            for (int i = 0; i < DIM; i++)
                s[k][i] = s[lo][i] + shrink*(s[k][i] - s[lo][i]);
            f[k] = objectiveFn(s[k]);
        }
    }
 
    mov(pmin, s[lo], DIM);
    return f[lo];
}
  • b、NelderMead单纯形算法
float computeError(const LTC& ltc, const Brdf& brdf, const vec3& V, const float alpha)
{
    double error = 0.0;
 
    for (int j = 0; j < Nsample; ++j)
    for (int i = 0; i < Nsample; ++i)
    {
        const float U1 = (i + 0.5f)/Nsample;
        const float U2 = (j + 0.5f)/Nsample;
 
        // 通过此时M矩阵的LTC算法的重要性采样分别计算BRDF积分与LTC积分并计算误差
        {
            // sample
            const vec3 L = ltc.sample(U1, U2);
            /*LTC的采样具体实现,即为在一个半球面上进行采样,获得方位角和俯仰角
            vec3 sample(const float U1, const float U2) const
	        {
		        const float theta = acosf(sqrtf(U1));
		        const float phi = 2.0f*3.14159f * U2;
		        // 球面坐标转笛卡尔坐标后再乘上M矩阵
		        const vec3 L = normalize(M * vec3(sinf(theta)*cosf(phi), sinf(theta)*sinf(phi), cosf(theta)));
		        return L;
	        }
            */
 
            float pdf_brdf;
            float eval_brdf = brdf.eval(V, L, alpha, pdf_brdf);
            float eval_ltc = ltc.eval(L);
            /*LTC的积分具体实现:即变换回原始分布D0,根据公式(含有雅各比矩阵的公式)计算积分
            float eval(const vec3& L) const
	        {
		        vec3 Loriginal = normalize(invM * L);
		        vec3 L_ = M * Loriginal;
		        float l = length(L_);
		        float Jacobian = detM / (l*l*l);
		        float D = 1.0f / 3.14159f * glm::max<float>(0.0f, Loriginal.z); 
                // magnitude是在computeAvgTerms函数中计算的权值总和
		        float res = magnitude * D / Jacobian;
		        return res;
	        }
            */
            float pdf_ltc = eval_ltc/ltc.magnitude;
 
            // error with MIS weight
            double error_ = fabsf(eval_brdf - eval_ltc);
            error_ = error_*error_*error_;
            error += error_/(pdf_ltc + pdf_brdf);
        }
 
        // 通过BRDF的重要性采样分别计算BRDF积分与LTC积分并计算误差
        {
            // sample
            const vec3 L = brdf.sample(V, alpha, U1, U2);
 
            float pdf_brdf;
            float eval_brdf = brdf.eval(V, L, alpha, pdf_brdf);
            float eval_ltc = ltc.eval(L);
            float pdf_ltc = eval_ltc/ltc.magnitude;
 
            // error with MIS weight
            double error_ = fabsf(eval_brdf - eval_ltc);
            error_ = error_*error_*error_;
            error += error_/(pdf_ltc + pdf_brdf);
        }
    }
 
    return (float)error / (float)(Nsample*Nsample);
}

3.2 面积光实时渲染代码

(1)LTC算法的着色器代码

// 由观察向量与法线的夹角以及粗糙度计算纹理坐标
vec2 LTC_Coords(float cosTheta, float roughness)
{
    float theta = acos(cosTheta);
    vec2 coords = vec2(roughness, theta/(0.5*3.14159));
 
    const float LUT_SIZE = 32.0;
    //进行缩放和偏移,以正确查找纹理
    coords = coords*(LUT_SIZE - 1.0)/LUT_SIZE + 0.5/LUT_SIZE;
 
    return coords;
}
 
//获得LTC算法的M矩阵
mat3 LTC_Matrix(sampler2D texLSDMat, vec2 coord)
{
    // 加载M逆矩阵
    vec4 t = texture2D(texLSDMat, coord);
    mat3 Minv = mat3_from_columns(
        vec3(1,     0, t.y),
        vec3(  0, t.z,   0),
        vec3(t.w,   0, t.x)
    );
 
    return Minv;
}
 
// 积分公式:acos(a,b)*cross(a,b)/(|cross(a,b)|*n)
float IntegrateEdge(vec3 v1, vec3 v2)
{
    float cosTheta = dot(v1, v2);
    cosTheta = clamp(cosTheta, -0.9999, 0.9999);
 
    float theta = acos(cosTheta);    
    // 除以sin(theta)是因为v1,v2已经标准化
    float res = cross(v1, v2).z * theta / sin(theta);
 
    return res;
}
 
// 裁剪运算,这里只针对了四边形进行裁剪,四边形裁剪有三种情况:三角形、四边形、五边形
void ClipQuadToHorizon(inout vec3 L[5], out int n)
{
    // 由Z分量分别确定四个点的可见性
    int config = 0;
    if (L[0].z > 0.0) config += 1;
    if (L[1].z > 0.0) config += 2;
    if (L[2].z > 0.0) config += 4;
    if (L[3].z > 0.0) config += 8;
 
    // clip
    n = 0;
 
    if (config == 0)
    {
        // clip all
    }
    else if (config == 1) // V1 clip V2 V3 V4
    {
        n = 3;
        L[1] = -L[1].z * L[0] + L[0].z * L[1];
        L[2] = -L[3].z * L[0] + L[0].z * L[3];
    }
    else if (config == 2) // V2 clip V1 V3 V4
    {
        n = 3;
        L[0] = -L[0].z * L[1] + L[1].z * L[0];
        L[2] = -L[2].z * L[1] + L[1].z * L[2];
    }
    else if (config == 3) // V1 V2 clip V3 V4
    {
        n = 4;
        L[2] = -L[2].z * L[1] + L[1].z * L[2];
        L[3] = -L[3].z * L[0] + L[0].z * L[3];
    }
    else if (config == 4) // V3 clip V1 V2 V4
    {
        n = 3;
        L[0] = -L[3].z * L[2] + L[2].z * L[3];
        L[1] = -L[1].z * L[2] + L[2].z * L[1];
    }
    else if (config == 5) // V1 V3 clip V2 V4) impossible
    {
        n = 0;
    }
    else if (config == 6) // V2 V3 clip V1 V4
    {
        n = 4;
        L[0] = -L[0].z * L[1] + L[1].z * L[0];
        L[3] = -L[3].z * L[2] + L[2].z * L[3];
    }
    else if (config == 7) // V1 V2 V3 clip V4
    {
        n = 5;
        L[4] = -L[3].z * L[0] + L[0].z * L[3];
        L[3] = -L[3].z * L[2] + L[2].z * L[3];
    }
    else if (config == 8) // V4 clip V1 V2 V3
    {
        n = 3;
        L[0] = -L[0].z * L[3] + L[3].z * L[0];
        L[1] = -L[2].z * L[3] + L[3].z * L[2];
        L[2] =  L[3];
    }
    else if (config == 9) // V1 V4 clip V2 V3
    {
        n = 4;
        L[1] = -L[1].z * L[0] + L[0].z * L[1];
        L[2] = -L[2].z * L[3] + L[3].z * L[2];
    }
    else if (config == 10) // V2 V4 clip V1 V3) impossible
    {
        n = 0;
    }
    else if (config == 11) // V1 V2 V4 clip V3
    {
        n = 5;
        L[4] = L[3];
        L[3] = -L[2].z * L[3] + L[3].z * L[2];
        L[2] = -L[2].z * L[1] + L[1].z * L[2];
    }
    else if (config == 12) // V3 V4 clip V1 V2
    {
        n = 4;
        L[1] = -L[1].z * L[2] + L[2].z * L[1];
        L[0] = -L[0].z * L[3] + L[3].z * L[0];
    }
    else if (config == 13) // V1 V3 V4 clip V2
    {
        n = 5;
        L[4] = L[3];
        L[3] = L[2];
        L[2] = -L[1].z * L[2] + L[2].z * L[1];
        L[1] = -L[1].z * L[0] + L[0].z * L[1];
    }
    else if (config == 14) // V2 V3 V4 clip V1
    {
        n = 5;
        L[4] = -L[0].z * L[3] + L[3].z * L[0];
        L[0] = -L[0].z * L[1] + L[1].z * L[0];
    }
    else if (config == 15) // V1 V2 V3 V4
    {
        n = 4;
    }
    
    if (n == 3)
        L[3] = L[0];
    if (n == 4)
        L[4] = L[0];
}
 
vec3 LTC_Evaluate(vec3 N, vec3 V, vec3 P, mat3 Minv, vec4 points[4], bool twoSided, sampler2D texFilteredMap)
{
    // 围绕法线N构造标准正交基
    vec3 T1, T2;
    T1 = normalize(V - N*dot(V, N));
    T2 = cross(N, T1);
 
    // 构建世界坐标系转变成法线N正交基构成的坐标系的转换矩阵
    Minv = mul(Minv, mat3_from_rows(T1, T2, N));
 
    // 面积光的多边形(分配5个顶点用于剪切)P表示片段的世界坐标,points表示面积光(矩形)的顶点
    vec3 L[5];
    L[0] = mul(Minv, points[0].xyz - P);
    L[1] = mul(Minv, points[1].xyz - P);
    L[2] = mul(Minv, points[2].xyz - P);
    L[3] = mul(Minv, points[3].xyz - P);
    L[4] = L[3]; // avoid warning
 
    vec3 textureLight = vec3(1, 1, 1);
#if LTC_TEXTURED
    // 对于使用贴图的面积光,需要从过滤贴图中采样
    textureLight = FetchDiffuseFilteredTexture(texFilteredMap, L[0], L[1], L[2], L[3]);
#endif
 
    int n;
    // 对平面进行裁剪,例如四边形经过裁剪可能变成三角形或者五边形
    ClipQuadToHorizon(L, n);
    
    if (n == 0)
        return vec3(0, 0, 0);
 
    // project onto sphere
    L[0] = normalize(L[0]);
    L[1] = normalize(L[1]);
    L[2] = normalize(L[2]);
    L[3] = normalize(L[3]);
    L[4] = normalize(L[4]);
 
    // 根据积分公式对每条边进行积分(其结果等于多边形的面积积分)
    float sum = 0.0;
 
    sum += IntegrateEdge(L[0], L[1]);
    sum += IntegrateEdge(L[1], L[2]);
    sum += IntegrateEdge(L[2], L[3]);
    if (n >= 4)
        sum += IntegrateEdge(L[3], L[4]);
    if (n == 5)
        sum += IntegrateEdge(L[4], L[0]);
 
    sum = twoSided ? abs(sum) : max(0.0, -sum);
 
    vec3 Lo_i = vec3(sum, sum, sum);
 
    // 叠加纹理颜色
    Lo_i *= textureLight;
 
    return Lo_i;
}

(2)LTC漫反射片段着色器代码

void main()
{
    ShadingContext s = InitShadingContext(v_normal, v_tangent, v_wpos, u_viewPosition.xyz, v_texcoord0);
 
    // 由法线与观察向量的夹角与粗糙度计算纹理坐标
    vec2 coords = LTC_Coords(dot(s.n, s.o), s.roughness);
    // 3×3单位矩阵
    mat3 Minv   = identity33();
    // 使用LTC积分获得面积光的辐射照度
    vec3 Lo_i   = LTC_Evaluate(s.n, s.o, s.p, Minv, u_quadPoints, u_twoSided, s_texFilteredMap);
 
    // scale by light intensity
    Lo_i *= u_lightIntensity;
 
    // 由反照率调整光照结果
    Lo_i *= s.diffColor * u_albedo.rgb;
 
    // 标准化
    Lo_i /= 2.0f * M_PI;
 
    // set output
    gl_FragColor = vec4(Lo_i, 1.0);
}

(3)、LTC镜面反射片段着色器

$input v_normal, v_tangent, v_wpos, v_shadowcoord, v_texcoord0
 
#include "common.sh"
 
#define LTC_NUM_POINTS 4
#define LTC_TEXTURED   1
#include "ltc.sh"
 
// -------------------------------------------------------------------------------------------------
void main()
{
    // 获得一些基础信息
    ShadingContext s = InitShadingContext(v_normal, v_tangent, v_wpos, u_viewPosition.xyz, v_texcoord0);
    /*InitShadingContext具体实现
    ShadingContext InitShadingContext(vec3 ng, vec3 tg, vec3 p, vec3 e, vec2 uv)
    {
        ShadingContext s;
        s.p = p;
        s.o = normalize(e - p);
        vec3 bg = cross(ng, tg);
        mat3 t2w = mat3_from_columns(tg, bg, ng);
        vec3 n = FetchNormal(s_nmlMap, uv, t2w);
        s.n = CorrectNormal(n, s.o);
        vec3 baseColor = texture2D(s_albedoMap, uv).rgb;
        float metallic = texture2D(s_metallicMap, uv).r;
        s.diffColor = baseColor*(1.0 - metallic);
        s.specColor = mix(vec3(u_F0, u_F0, u_F0), baseColor, metallic);
        float roughness    = texture2D(s_roughnessMap, uv).r;
        float nmlRoughness = texture2D(s_nmlMap, uv).b;
        roughness = pow(pow(roughness, 4.0) + pow(nmlRoughness, 4.0), 0.25);
        s.roughness = max(roughness*u_roughness, MIN_ROUGHNESS);
        return s;
    }
    */
 
    // 由法线与观察向量的夹角与粗糙度计算纹理坐标
    vec2 coords = LTC_Coords(dot(s.n, s.o), s.roughness);
    // 由纹理坐标采样矩阵贴图得到LTC的线性变换矩阵M
    mat3 Minv   = LTC_Matrix(s_texLTCMat, coords);
    // LTC算法积分
    vec3 Lo_i   = LTC_Evaluate(s.n, s.o, s.p, Minv, u_quadPoints, u_twoSided, s_texFilteredMap);
 
    Lo_i *= u_lightIntensity;
 
    // 从纹理中采样BRDF系数(对立体角进行积分获得的BRDF预计算系数)以及菲涅尔系数
    vec2 schlick = texture2D(s_texLTCAmp, coords).xy;
    Lo_i *= s.specColor * schlick.x + (1.0 - s.specColor) * schlick.y;
 
    Lo_i /= 2.0f * M_PI;
 
    gl_FragColor = vec4(Lo_i, 1.0);
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值