SRP 实现 Cook-Torrance BRDF
写的很乱!
BRDF(Bidirectional Reflectance Distribution Function)全称双向反射分布函数。辐射量单位非常多,这里为方便直观理解,会用非常不严谨的光照强度来解释说明。
BRDF光照模型,上反射率公式:
计算的是在法线所在半球上的所有入射光线,在出射方向的光强总和,公式有俩个部分。
即BRDF,从特定的入射方向到出射方向,在某个表面上,光强的衰减比率。
有了比率,还需要知道在物体表面接收到的某个方向上的光强,所以前半部分是光线沿入射方向到p点发射的光线强度,后半部分用表面与入射方向的夹角计算实际接收到的光线强度(因为这里虽然是一个点,但同时也需要当做一个极小的平面区域处理)。
关于几何函数
为什么GXX有俩个?只考虑一次反射能成功的,其实就是简单的认为入射方向能不被遮挡的光线比例 × 出射方向能不被遮挡的光线比例 = 整条光路上不会被遮挡的光线比例。
Cook-Torrance BRDF
🔻如果在渲染结果中发现了类似以下的问题或变,极大概率是法线没有在片元着色器中标准化。
SRP中实现的Cook-Torrance BRDF效果(无法线贴图,未进行gamma调整):
增加支持多光源
https://zhuanlan.zhihu.com/p/163360207
关于点光源获取位置和平行光获取方向。
获取位置用VisibleLight.localToWorldMatrix.GetColumn(3);
获取方向用VisibleLight.localToWorldMatrix.GetColumn(2);
因为localToWorldMatrix是光源的模型变换矩阵M,获取点光源的位置即mul(M, float4 (0, 0, 0, 1)),即localToWorldMatrix的最后一列。同理,平行光方向朝向局部坐标z轴,mul(M, float4 (0, 0, 1, 0))故为第三列。
有很多免费的材质素材: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中显示法线,切线,副切线(几何着色器实现代码)
这里再补充一个用几何着色器实现的法线,切线和副切线显示。完全重叠的线只会显示一条,所以有可能会看到缺少了某条线,其实是被覆盖掉了。
// 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,而且法线用平均的但切线又重新算这是什么迷惑操作啊】
// 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字) - 天份& - 博客园