SRP 实现 Cook-Torrance BRDF

写的很乱!

BRDF (Bidirectional Reflectance Distribution Function)全称双向反射分布函数。辐射量单位非常多,这里为方便直观理解,会用非常不严谨的光照强度来解释说明。

BRDF光照模型,上反射率公式

计算的是在法线所在半球上的所有入射光线 ,在出射方向光强总和,公式有俩个部分。

即BRDF,从特定的入射方向到出射方向,在某个表面上,光强的衰减比率。

有了比率,还需要知道在物体表面接收到的某个方向上的光强 ,所以前半部分是光线沿入射方向到p点发射的光线强度 ,后半部分用表面与入射方向的夹角计算实际接收到的光线强度(因为这里虽然是一个点,但同时也需要当做一个极小的平面区域处理)。

关于几何函数

为什么GXX有俩个?只考虑一次反射能成功的,其实就是简单的认为入射方向能不被遮挡的光线比例 × 出射方向能不被遮挡的光线比例 = 整条光路上不会被遮挡的光线比例。

Cook-Torrance BRDF

🔻如果在渲染结果中发现了类似以下的问题或变,极大概率是法线没有在片元着色器中标准化。

SRP中实现的Cook-Torrance BRDF效果(无法线贴图,未进行gamma调整):

增加支持多光源

有很多免费的材质素材:Hundreds of 3D Texture Downloads - Free PBR Materials

这部分是没什么难度的,将相关变量设置为数组即可。

🔻但问题就出现在这,在Unity 2022.3.45f1c1中,如果先实现了.SetGlobalVector然后修改成.SetGlobalVectorArray,那么恭喜你完犊子了。修改后传入GPU的数据还是按.SetGlobalVector处理的,Dubug半天你会发现GPU就是拿不到完整的数组。

解决方法:Unity!重启!😎

🔻如果遇到CullingResults.lightIndexCount在某些视角下会返回非常奇怪的(错误)值的情况,例如明明只有2个可见光源,但返回4个。用CullingResults.visibleLights.Length来获取可见光源数组的个数,而不是CullingResults.lightIndexCount即可。

🔻Shader error in 'Custom/BRDF': Unexpected directive '';这种错多半是用了中文分号或者其他的全半角问题。

支持法线贴图,如果想要在切线空间引用法线贴图后再转到世界坐标系下,需要用TBN矩阵的逆矩阵实现。具体见:TBN矩阵的使用

🔻应用贴图实现的BRDF效果很抽象,我以为是TNB出问题了(甚至怀疑unity默认切线有问题,还从blender中导了个正确的球模型过来),整了很久发现是采样法线贴图出错了,虽然各种设置了Unity项目是线性空间,但纹理这一块疑似过于独立了,需要在贴图中应用为Normal Map才能确保纹理采样在线性空间下。

那么有朋友就要问了,unity!unity!我用Default但不勾选sRGB不就可以了吗?unity答:不行。

为什么呢?因为根据TNB矩阵的组织方式,切线空间中垂直表面的轴实际上是x轴而不是z轴,如果想直接使用原始法线贴图,需要调整颜色通道顺序。

如果只用线性空间采样,输出采样结果经过映射后其实是大红色的,即(1.0,0.0,0.0),怎么回事呢?

映射指颜色范围0~1到法线范围-1~1,而映射前是粉红色的。据说解包出的法线纹理也是粉红色的,和这个有关系吗?

法线贴图And图像压缩

贴图设置为Normal Map就可以了吗?换个法线变换明显材质试试看看😕。

输出法线,很明显少了一部分。

怎么回事呢,采访一下纹理压缩格式。

DXT5|BC3:"我不到啊。"

Unity:"他说(DXT5|BC3)他的alpha和green通道最好用,我就按他说的来了。"

言归正传,BC3是DXT5在DirectX 10中的别称,以下都以将以BC3称呼,BC是怎么压缩图像的这里就不过多解释了,直接看BC3压缩后的格式。

BC3 格式使用 5:6:5 颜色(5 位红色、6 位绿色、5 位蓝色)存储颜色数据,使用一个字节存储 alpha 数据。 更多的位数意味着更大的颜色深度,即更好的颜色表现。对法线来说,其归一化后长度为1,知道x,y即可计算z,所以当选择Texture为NormalMap时,Unity会自动处理成alpha和green通道以帮助我们得到最佳的图像效果。

在压缩大多数 RGBA 纹理时最好使用此格式。对于 RGB(无 Alpha)纹理,DXT1 更适合。当针对 DX11 类硬件(新版 PC、PS4、XboxOne)时,使用 BC7 可能很有用,因为压缩品质通常更好。

拓展一下,Unity中RGBA默认用BC3或BC7,BC7有八种模式,但RGBA支持模式下位数最多(之一)的还是alpha通道,颜色三通道则相同,所以用alpha加green通道也是最优解。

虽然法线贴图是RGB图,但还是用了RGBA的处理方式。

此外,Unity法线贴图也支持用BC5进行压缩,不过对应的采样代码就要修改了,反正怎么压缩怎么采样即可。

所以如果想得到正确的法线贴图采样结果,就需要手动提取alpha和green通道作为yz,再求解出x,这里的对应关系很重要。

通过UV检查图确定UV坐标原点为面左下角,然后只输出法线贴图上采样green通道的颜色。

可以确定green通道对应副切线,输出alpha通道。

可知alpha通道对应切线,那么法线方向的值就是要算的了,计算出法线方向输出值输出结果如下。

说明前面的推测是正确的,又由于TNB矩阵的组织方式是world normal,world tangent,world bitangent,所以alpha和green通道作为yz,再求解出最后一个值作为x。

float3x3 TNB = float3x3(normalize(v2p.normal), normalize(v2p.tangent), normalize(v2p.bitangent));

处理一下就能够将原法线贴图绘制出来,右图颜色更深是由于右图在线性颜色空间下+显示屏Gamma2.2调整,左图在线性空间下+Unity自动Gamma0.45调整+显示屏Gamma2.2调整。

修正后效果:

🔻Gamma问题,光照对了但球的颜色很淡很淡。原因是Color Space选择Linear时,Unity会自动对渲染窗口做一次Gamma0.45。而我参考LearnOpen GL在Shader中也实现了一次Gamma 0.45,所以结果是两次Gamma0.45,加上显示器的一次Gamma2.2,最终输出颜色还是Gamma 0.45,所以会感觉很淡(如果Color Space选Gamma,就需要手动校正)。

参考:Gamma 和 Linear Space的设置 Unity

然后终终终终于实现成功了"网图"的效果!

Unity中显示法线,切线,副切线(附代码)

先前为了查看切线是否有问题,写了个非常简单的脚本显示各顶点的切线坐标轴(使用前要reset对象的transform),结果显示模型非常健康。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ShowNormalsAndTangents : MonoBehaviour
{
    [SerializeField]
    float normalLength = 0.03f;

    // Update is called once per frame
    void OnDrawGizmosSelected()
    {
        Mesh mesh = GetComponent<MeshFilter>().sharedMesh;
        Vector3[] v = mesh.vertices;
        Vector3[] n = mesh.normals;
        Vector4[] t = mesh.tangents;
        for (int i = 0; i < v.Length; i++) {
            Gizmos.color = Color.red;
            Gizmos.DrawRay(v[i], n[i] * normalLength);

            Gizmos.color = Color.green;
            Gizmos.DrawRay(v[i], new Vector3(t[i].x, t[i].y, t[i].z) * normalLength);

            Vector3 bitangent = Vector3.Normalize(Vector3.Cross(n[i], new Vector3(t[i].x, t[i].y, t[i].z)) * t[i].w);
            Gizmos.color = Color.blue;
            Gizmos.DrawRay(v[i], bitangent * normalLength);
        }
    }
}

Unity中显示法线,切线,副切线(几何着色器实现代码)

这里再补充一个用几何着色器实现的法线,切线和副切线显示。完全重叠的线只会显示一条,所以有可能会看到缺少了某条线,其实是被覆盖掉了。

cpp 复制代码
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
 
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
 
Shader "Custom/ShowNormals"
{
    Properties
    {
        _LineLength("LineLength", float) = 0.03
        _NormalLineColor("NormalLineColor", Color) = (1.0, 0.0, 0.0, 1.0)
        _TangentLineColor("TangentLineColor", Color) = (0.0, 1.0, 0.0, 1.0)
        _BitangentLineColor("BitangentLineColor", Color) = (0.0, 0.0, 1.0, 1.0)
    }
    SubShader
    {
        Pass
        {
 
            HLSLPROGRAM
            #pragma vertex VS_Main
            #pragma fragment FS_Main
            #pragma geometry GS_Main
 
            float4x4 unity_MatrixVP;
            float4x4 unity_ObjectToWorld;
            float4x4 unity_WorldToObject;

            float _LineLength;
            float4 _NormalLineColor;
            float4 _TangentLineColor;
            float4 _BitangentLineColor;

                 
            struct VS_INPUT{
                float3 m_pos:POSITION;
                float3 normal:NORMAL;
                float4 tangent:TANGENT;
            };
 
            struct GS_INPUT
            {
                float4 pos:POSITION;
                float3 w_normal:NORMAL;
                float3 w_tangent:TANGENT;
                float3 w_bitangent:TEXCOORD0;
            };
            struct FS_INPUT
            {
                float4 pos:POSITION;
                float4 lineColor:TEXCOORD0;
            };
            //step1
            GS_INPUT VS_Main(VS_INPUT input)
            {
                GS_INPUT output;
                output.pos = mul(unity_ObjectToWorld, float4(input.m_pos, 1.0));
                output.w_normal = mul(transpose((float3x3)unity_WorldToObject), input.normal);
                output.w_tangent = mul((float3x3)unity_ObjectToWorld, input.tangent.xyz);
                output.w_bitangent = cross(output.w_normal, output.w_tangent) * input.tangent.w;

                return output;
            }

            [maxvertexcount(18)] //GS单次调用可以输出的最大顶点数量,这里每个顶点会变成俩顶点所以是3*2;
            void GS_Main(triangle GS_INPUT gsIn[3], inout LineStream<FS_INPUT> triStream)
            {
                for(uint i = 0; i < 3; ++i){
                    FS_INPUT pIn;
                    pIn.pos = mul(unity_MatrixVP, gsIn[i].pos);

                    FS_INPUT pIn1;
                    float4 pos= gsIn[i].pos + float4(normalize(gsIn[i].w_normal), 0.0) * _LineLength;
                    pIn1.pos = mul(unity_MatrixVP, pos);
                    pIn.lineColor = _NormalLineColor;
                    pIn1.lineColor = _NormalLineColor;
                    triStream.Append(pIn);
                    triStream.Append(pIn1);
                    triStream.RestartStrip();

                    FS_INPUT pIn2;
                    pos = gsIn[i].pos + float4(normalize(gsIn[i].w_tangent), 0.0) *_LineLength;
                    pIn2.pos = mul(unity_MatrixVP, pos);
                    pIn.lineColor = _TangentLineColor;
                    pIn2.lineColor = _TangentLineColor;
                    triStream.Append(pIn);
                    triStream.Append(pIn2);
                    triStream.RestartStrip();

                    FS_INPUT pIn3;
                    pos = gsIn[i].pos + float4(normalize(gsIn[i].w_bitangent), 0.0) *_LineLength;
                    pIn3.pos = mul(unity_MatrixVP, pos);
                    pIn.lineColor = _BitangentLineColor;
                    pIn3.lineColor = _BitangentLineColor;
                    triStream.Append(pIn);
                    triStream.Append(pIn3);
                    triStream.RestartStrip();
                }
            }
 
            //step3
            float4 FS_Main(FS_INPUT input) : SV_TARGET0
            {
                return input.lineColor;
            }
        ENDHLSL
        }
    }
}

手动计算TNB

当然也可以手动计算切线空间的坐标系,但这样计算就会使得每个三角形面片都有自己独立的切线空间。例如,左图是模型默认切线空间,法线和切线由该顶点所参与的6个片元的法线和切线平均得到(平滑着色),右图是只根据法线在各片元内重新计算的切线和副切线(如果法线也各片元重新计算,就是平直着色)。【学习TNB矩阵过程中写的意义不明的Shader,而且法线用平均的但切线又重新算这是什么迷惑操作啊】

cpp 复制代码
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
 
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
 
Shader "Custom/ShowNormals"
{
    Properties
    {
        _LineLength("LineLength", float) = 0.03
        _NormalLineColor("NormalLineColor", Color) = (1.0, 0.0, 0.0, 1.0)
        _TangentLineColor("TangentLineColor", Color) = (0.0, 1.0, 0.0, 1.0)
        _BitangentLineColor("BitangentLineColor", Color) = (0.0, 0.0, 1.0, 1.0)
    }
    SubShader
    {
        Pass
        {
 
            HLSLPROGRAM
                #pragma vertex VS_Main
                #pragma fragment FS_Main
                #pragma geometry GS_Main
 
                float4x4 unity_MatrixVP;
                float4x4 unity_ObjectToWorld;
                float4x4 unity_WorldToObject;

                float _LineLength;
                float4 _NormalLineColor;
                float4 _TangentLineColor;
                float4 _BitangentLineColor;

                 
                struct VS_INPUT{
                    float3 m_pos:POSITION;
                    float2 uv:TEXCOORD;
                    float3 normal:NORMAL;
                };
 
                struct GS_INPUT
                {
                    float4 pos:POSITION;
                    float2 uv:TEXCOORD0;
                    float3 normal:NORMAL;
                };
                struct FS_INPUT
                {
                    float4 pos:POSITION;
                    float4 lineColor:TEXCOORD0;
                };
                
                GS_INPUT VS_Main(VS_INPUT input)
                {
                    GS_INPUT output;
                    output.pos = mul(unity_ObjectToWorld, float4(input.m_pos, 1.0));
                    output.normal = normalize(mul(transpose((float3x3)unity_WorldToObject), input.normal));
                    output.uv = input.uv;

                    return output;
                }

                [maxvertexcount(18)] //GS单次调用可以输出的最大顶点数量,这里每个顶点会变成俩顶点所以是3*2;
                void GS_Main(triangle GS_INPUT gsIn[3], inout LineStream<FS_INPUT> triStream)
                {
                    float3 edge1 = gsIn[1].pos - gsIn[0].pos; 
                    float3 edge2 = gsIn[2].pos - gsIn[0].pos;
                    float2 deltaUV1 = gsIn[1].uv - gsIn[0].uv; 
                    float2 deltaUV2 = gsIn[2].uv - gsIn[0].uv; 

                    float f = 1.0 / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);

                    float3 tangent;
                    tangent.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
                    tangent.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
                    tangent.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);

                    float3 bitangent;
                    bitangent.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
                    bitangent.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
                    bitangent.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);

                    for(uint i = 0; i < 3; ++i){
                        float3 m_tangent = normalize(tangent - gsIn[i].normal * dot(gsIn[i].normal, tangent));
                        float3 m_bitangent = normalize(cross(tangent, gsIn[i].normal)); 

                        FS_INPUT pIn;
                        pIn.pos = mul(unity_MatrixVP, gsIn[i].pos);

                        FS_INPUT pIn1;
                        float4 pos= gsIn[i].pos + float4(gsIn[i].normal, 0) *_LineLength;
                        pIn1.pos = mul(unity_MatrixVP, pos);
                        pIn.lineColor = _NormalLineColor;
                        pIn1.lineColor = _NormalLineColor;
                        triStream.Append(pIn);
                        triStream.Append(pIn1);
                        triStream.RestartStrip();

                        FS_INPUT pIn2;
                        pos = gsIn[i].pos + float4(m_tangent, 0) *_LineLength;
                        pIn2.pos = mul(unity_MatrixVP, pos);
                        pIn.lineColor = _TangentLineColor;
                        pIn2.lineColor = _TangentLineColor;
                        triStream.Append(pIn);
                        triStream.Append(pIn2);
                        triStream.RestartStrip();

                        FS_INPUT pIn3;
                        pos = gsIn[i].pos + float4(m_bitangent, 0) *_LineLength;
                        pIn3.pos = mul(unity_MatrixVP, pos);
                        pIn.lineColor = _BitangentLineColor;
                        pIn3.lineColor = _BitangentLineColor;
                        triStream.Append(pIn);
                        triStream.Append(pIn3);
                        triStream.RestartStrip();
                    }
                }
 
                
                float4 FS_Main(FS_INPUT input) : SV_TARGET0
                {
                    return input.lineColor;
                }
            ENDHLSL
        }
    }
}

TNB矩阵和UV坐标关系

(不确定!)使用默认切线,在网格形变时可能会有问题?

把平面网格左下角的顶点移动到平面中心,世界空间下的法线出现了大问题(如下),左图的斜边应该是同样的颜色才对(即法线方向相同)。

梳理梳理TNB和法线贴图采样的过程导致问题的原因其实很明确,个人感觉对于会产生形变的网格,需要手动计算TBN矩阵时,考虑到TNB正交化对UV采样法线贴图的影响,需要对采样后的法线再增加一个和TNB正交化有关系的变换,可能后面会补充一下这部分的实现。

几何着色器:

https://zhuanlan.zhihu.com/p/585436751

DirectX11 With Windows SDK--15 几何着色器初探 - X_Jun - 博客园

渲染管线(主要是重新了解几何着色器):

一文读懂什么是渲染管线(7k字) - 天份& - 博客园

相关推荐
YY-nb14 分钟前
Unity Apple Vision Pro 开发教程:物体识别跟踪
unity·游戏引擎·apple vision pro
Cool-浩18 分钟前
Unity 开发Apple Vision Pro物体识别追踪ObjectTracking
unity·ar·apple vision pro·mr·物体识别·vision pro教程·objecttracking
Am心若依旧40920 分钟前
[c++进阶(三)]单例模式及特殊类的设计
java·c++·单例模式
小王爱吃月亮糖21 分钟前
补充--C++的项目结构和管理
数据结构·c++·笔记·学习
因特麦克斯22 分钟前
如何实现对象的克隆?如何实现单例模式?
c++·单例模式
Crazy learner2 小时前
C 和 C++ 动态库的跨语言调用原理
c语言·c++
金士顿4 小时前
MFC 文档模板 每个文档模板需要实例化吧
c++·mfc
人才程序员7 小时前
QML z轴(z-order)前后层级
c语言·前端·c++·qt·软件工程·用户界面·界面
w(゚Д゚)w吓洗宝宝了7 小时前
C vs C++: 一场编程语言的演变与对比
c语言·开发语言·c++
柳鲲鹏9 小时前
jiangdg/AndroidCamera关闭摄像头流程
笔记