一、整装待发
1.1:万事俱备
UnityShader相关:
- UnityShader1:渲染流水线 ~ UnityShader24:最简单的屏幕后处理例子
- 最好还是了解一个图形接口:OpenGL 或者 DirectX
- 永远别忘记 U3D 的官方手册
PBR相关:
1.2:只欠东风
准备好你的参考资料,以及……新建一个 Shader,就可以开始愉快的造轮子了:
Shader "Jaihk662/PBR"
{
Properties
{
_Color("MainColor", Color) = (1, 1, 1, 1)
_SpecularColor("SpecularColor", Color) = (1, 1, 1, 1)
_Glossiness("Smoothness", Range(0, 1)) = 1 //光滑度
_Metallic("Metalness", Range(0, 1)) = 0 //金属度
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 200
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "AutoLight.cginc"
#include "Lighting.cginc"
float4 _Color;
float4 _SpecularColor;
float _Glossiness;
float _Metallic;
//……
ENDCG
}
}
FallBack "Specular"
}
金属度(Metalness)和光滑度(Smoothness)这两个万分重要的属性当然是必不可少的
二、重要属性与方法
对于基础的数据结构和方法,可以提前准备好,包括但不限于:
- 光照方向
:lightDir
- 法线方向
:normal
- 视角方向
:viewDir
- 半角方向
:halfDir
- 切线与副切线:
:tangentDir & bitangentDir
- 粗糙度 roughness
- 光照衰减 atten
struct appdata
{
float4 vertex: POSITION;
float3 normal: NORMAL; //法线
float4 tangent: TANGENT; //切线
float2 uv: TEXCOORD0; //uv坐标
float2 lightMapUV: TEXCOORD1; //光照贴图uv坐标
float2 dynLightMapUV: TEXCOORD2; //动态光照贴图uv坐标
};
struct v2f
{
float4 pos: SV_POSITION;
float2 uv: TEXCOORD0; //uv坐标
float2 lightMapUV: TEXCOORD1; //光照贴图uv坐标
float2 dynLightMapUV: TEXCOORD2; //动态光照贴图uv坐标
float3 normalDir: TEXCOORD3; //法线
float3 posWorld: TEXCOORD4; //当前像素所在世界空间坐标
float3 tangentDir: TEXCOORD5; //主切线
float3 bitangentDir: TEXCOORD6; //副切线
LIGHTING_COORDS(7, 8) //Unity自带阴影
UNITY_FOG_COORDS(9) //Unity内置雾效
};
v2f vert(appdata v)
{
v2f o;
o.uv = v.uv;
o.lightMapUV = v.lightMapUV;
o.dynLightMapUV = v.dynLightMapUV;
o.normalDir = UnityObjectToWorldNormal(v.normal);
o.tangentDir = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz);
o.bitangentDir = normalize(cross(o.normalDir, o.tangentDir) * v.tangent.w);
o.pos = UnityObjectToClipPos(v.vertex);
o.posWorld = mul(unity_ObjectToWorld, v.vertex);
UNITY_TRANSFER_FOG(o, o.pos);
TRANSFER_VERTEX_TO_FRAGMENT(o)
return o;
}
fixed4 frag(v2f i): SV_Target
{
//法线方向
float3 normal = normalize(i.normalDir);
//光源方向,_WorldSpaceLightPos0.xyz为当前Pass中主光源方向,_WorldSpaceLightPos0.w为1表示当前光源为点光源或聚光灯,为0表示当前光源为平行光
float3 lightDir = normalize(lerp(_WorldSpaceLightPos0.xyz, _WorldSpaceLightPos0.xyz - i.posWorld.xyz, _WorldSpaceLightPos0.w));
//光源反射向量
float3 lightReflectDir = reflect(-lightDir, normal);
//视角方向
float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.posWorld.xyz);
//视角反射方向
float3 viewReflectDir = normalize(reflect(-viewDir, normal));
//视线与光照半角方向
float3 halfDir = normalize(viewDir + lightDir);
float NdotL = max(0.0, dot(normal, lightDir));
float NdotH = max(0.0, dot(normal, halfDir));
float NdotV = max(0.0, dot(normal, viewDir));
float VdotH = max(0.0, dot(viewDir, halfDir));
float LdotH = max(0.0, dot(lightDir, halfDir));
float LdotV = max(0.0, dot(lightDir, viewDir));
float RdotV = max(0.0, dot(lightReflectDir, viewDir));
//得到光照衰减
float attenuation = LIGHT_ATTENUATION(i);
//光照衰减颜色
float3 attenColor = attenuation * _LightColor0.rgb;
//粗糙度
float roughness = 1 - (_Glossiness * _Glossiness);
roughness = roughness * roughness;
float3 diffColor = _Color.rgb * (1 - _Metallic) ;
float3 specColor = lerp(_SpecularColor.rgb, _Color.rgb, _Metallic * 0.5);
fixed4 col = fixed4(diffColor + specColor, 1.0);
return col;
}
使用了输入光滑度(Smoothness)的平方来重新计算粗糙度(roughness)可以得到一个不错的效果,而对于金属度(Metalness),它会同时影响漫反射和镜面反射的颜色
- 对于非金属,它拥有绝对的漫反射以及镜面反射,它们全部来源于纹理输入
- 对于金属,它几乎没有漫反射,因此漫反射项颜色往往用于驱动镜面反射
三、完整的反射方程
完整的 Cook-Torrance BRDF 模型的 PBR 方程如下:
在计算菲涅尔系数时,通常要考虑金属和非金属(电介质),对于金属或导体(Conductor)表面,基础反射率 的值需要进行混合,对应的伪代码就是:
vec3 F0 = vec3(0.04);
F0 = mix(F0, surfaceColor.rgb, metalness);
代入 求出对应的菲涅尔系数理应就是
项,但由于金属几乎是不产生漫反射的,所以在计算漫反射项时减去镜面反射的比例后,还要再乘以电介质的比例,这也可以算到
项中,得到
对于镜面反射项系数 ,由于
中菲涅尔项
本身就已经体现了镜面反射的比例,所以这里可以直接去掉,好了,这样去掉
和
项的 PBR 反射方程就是:
2018 UnityStandard 在计算 BRDF 时将整体乘上了一个
,这也是为什么很多时候自己实现的 BRDF 会显得过暗的原因
本章完整代码如下:
Shader "Jaihk662/PBR"
{
Properties
{
_Color("MainColor", Color) = (1, 1, 1, 1)
_SpecularColor("SpecularColor", Color) = (1, 1, 1, 1)
_Glossiness("Smoothness", Range(0, 1)) = 1 //光滑度
_Metallic("Metalness", Range(0, 1)) = 0 //金属度
_Ior("Ior", Range(1, 4)) = 1.5 //折射指数,用于计算基础反射率
_LUT("LUT", 2D) = "white" {}
}
CGINCLUDE
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "AutoLight.cginc"
#include "Lighting.cginc"
#include "UnityStandardBRDF.cginc"
float4 _Color;
float4 _SpecularColor;
float _Glossiness;
float _Metallic;
float _Ior;
sampler2D _LUT;
#define PI 3.1415926535
#define CalculationKAverage(p) pow(p, 2) * 0.797884560802865
#define CalculationKDirect(p) pow(p + 1, 2) / 8
#define CalculationKIBL(p) pow(p, 2) / 8
#define MixFunction(i, j, x) j * x + i * (1.0 - x)
//--------------------------------------------------Distribution Begin--------------------------------------------------
//Beckman NDF(D项)
float BeckmannNormalDistribution(float roughness, float NdotH)
{
float roughnessSqr = roughness * roughness;
float NdotHSqr = NdotH * NdotH;
return max(0.000001, (1.0 / (PI * roughnessSqr * NdotHSqr * NdotHSqr)) * exp((NdotHSqr - 1) / (roughnessSqr * NdotHSqr)));
}
//GGX NDF(D项)
float GGXNormalDistribution(float roughness, float NdotH)
{
float roughnessSqr = roughness * roughness;
float NdotHSqr = NdotH * NdotH;
float TanNdotHSqr = (1 - NdotHSqr) / NdotHSqr;
return (1.0 / PI) * sqrt(roughness / (NdotHSqr * (roughnessSqr + TanNdotHSqr)));
}
//--------------------------------------------------Distribution End--------------------------------------------------
//--------------------------------------------------Geometric Begin--------------------------------------------------
//Schlick-Beckman Geometric(G项)
float SchlickBeckmanGeometricShadowingFunction(float NdotL, float NdotV, float roughness)
{
float k = CalculationKDirect(roughness);
float SmithL = NdotL / (NdotL * (1 - k) + k);
float SmithV = NdotV / (NdotV * (1 - k) + k);
float Gs = SmithL * SmithV;
return Gs;
}
//--------------------------------------------------Geometric End--------------------------------------------------
//--------------------------------------------------Fresnel Begin--------------------------------------------------
float SchlickFresnel(float i)
{
float x = clamp(1.0 - i, 0.0, 1.0);
float x2 = x * x;
return x2 * x2 * x;
}
//Schlick-Fresnel Fresnel(F项)
float3 SchlickFresnelFunction(float3 F0, float LdotH)
{
return F0 + (1 - F0) * SchlickFresnel(LdotH);
}
float SchlickFresnelFunctionSingle(float F0, float LdotH)
{
return F0 + (1 - F0) * SchlickFresnel(LdotH);
}
//Spherical-Gaussian-Fresnel Fresnel拟合(F项)
float SphericalGaussianFresnelFunction(float3 F0, float LdotH)
{
float power = ((-5.55473 * LdotH) - 6.98316) * LdotH;
return F0 + (1 - F0) * pow(2, power);
}
//利用折射指数(Indices of Refraction, IOR)直接计算金属材质的F0
float SchlickIORFresnelFunction(float ior, float LdotH)
{
float F0 = pow(ior - 1, 2)/pow(ior + 1, 2);
return SchlickFresnelFunctionSingle(F0, LdotH);
}
//利用插值计算金属材质的F0,其中unity_ColorSpaceDielectricSpec.rgb为所有非金属基础反射率的平均值,约为0.04
float SchlickLerpFresnelFunction(float LdotH)
{
float F0 = lerp(unity_ColorSpaceDielectricSpec.rgb, _Color, _Metallic);
return SchlickFresnelFunctionSingle(F0, LdotH);
}
//DisneyDiffuseFresnel 漫反射F项
float DisneyDiffuseFresnel(float NdotL, float NdotV, float LdotH, float roughness)
{
float FresnelLight = SchlickFresnel(NdotL);
float FresnelView = SchlickFresnel(NdotV);
float FresnelDiffuse90 = 0.5 + 2.0 * LdotH * LdotH * roughness;
return MixFunction(1, FresnelDiffuse90, FresnelLight) * MixFunction(1, FresnelDiffuse90, FresnelView);
}
//--------------------------------------------------Fresnel End--------------------------------------------------
ENDCG
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 200
Pass
{
CGPROGRAM
struct appdata
{
float4 vertex: POSITION;
float3 normal: NORMAL; //法线
float4 tangent: TANGENT; //切线
float2 uv: TEXCOORD0; //uv坐标
float2 lightMapUV: TEXCOORD1; //光照贴图uv坐标
float2 dynLightMapUV: TEXCOORD2; //动态光照贴图uv坐标
};
struct v2f
{
float4 pos: SV_POSITION;
float2 uv: TEXCOORD0; //uv坐标
float2 lightMapUV: TEXCOORD1; //光照贴图uv坐标
float2 dynLightMapUV: TEXCOORD2; //动态光照贴图uv坐标
float3 normalDir: TEXCOORD3; //法线
float3 posWorld: TEXCOORD4; //当前像素所在世界空间坐标
float3 tangentDir: TEXCOORD5; //主切线
float3 bitangentDir: TEXCOORD6; //副切线
LIGHTING_COORDS(7, 8) //Unity自带阴影
UNITY_FOG_COORDS(9) //Unity内置雾效
};
v2f vert(appdata v)
{
v2f o;
o.uv = v.uv;
o.lightMapUV = v.lightMapUV;
o.dynLightMapUV = v.dynLightMapUV;
o.normalDir = UnityObjectToWorldNormal(v.normal);
o.tangentDir = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz);
o.bitangentDir = normalize(cross(o.normalDir, o.tangentDir) * v.tangent.w);
o.pos = UnityObjectToClipPos(v.vertex);
o.posWorld = mul(unity_ObjectToWorld, v.vertex);
UNITY_TRANSFER_FOG(o, o.pos);
TRANSFER_VERTEX_TO_FRAGMENT(o)
return o;
}
fixed4 frag(v2f i): SV_Target
{
//法线方向
float3 normal = normalize(i.normalDir);
//光源方向,_WorldSpaceLightPos0.xyz为当前Pass中主光源方向,_WorldSpaceLightPos0.w为1表示当前光源为点光源或聚光灯,为0表示当前光源为平行光
float3 lightDir = normalize(lerp(_WorldSpaceLightPos0.xyz, _WorldSpaceLightPos0.xyz - i.posWorld.xyz, _WorldSpaceLightPos0.w));
//光源反射向量
float3 lightReflectDir = reflect(-lightDir, normal);
//视角方向
float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.posWorld.xyz);
//视角反射方向
float3 viewReflectDir = normalize(reflect(-viewDir, normal));
//视线与光照半角方向
float3 halfDir = normalize(viewDir + lightDir);
float NdotL = max(0.00001, dot(normal, lightDir));
float NdotH = max(0.00001, dot(normal, halfDir));
float NdotV = max(0.00001, dot(normal, viewDir));
float VdotH = max(0.00001, dot(viewDir, halfDir));
float LdotH = max(0.00001, dot(lightDir, halfDir));
float LdotV = max(0.00001, dot(lightDir, viewDir));
float RdotV = max(0.00001, dot(lightReflectDir, viewDir));
//得到光照衰减
float attenuation = LIGHT_ATTENUATION(i);
//光照衰减颜色
float3 attenColor = attenuation * _LightColor0.rgb;
//粗糙度
float roughness = 1 - (_Glossiness * _Glossiness);
roughness = roughness * roughness;
float Fd = DisneyDiffuseFresnel(NdotL, NdotV, LdotH, roughness);
float3 diffColor = _Color.rgb * (1 - _Metallic) * Fd;
float3 specColor = lerp(_SpecularColor.rgb, _Color.rgb, _Metallic * 0.5);
float3 D = specColor;
float G = 1;
float3 F = specColor;
D *= GGXNormalDistribution(roughness, NdotH);
G *= SchlickBeckmanGeometricShadowingFunction(NdotL, NdotV, roughness);
F *= SchlickFresnelFunction(specColor, LdotH);
float3 specularity = (D * G * F) / (4 * (NdotL * NdotV)) * PI;
float3 directLightCol = (diffColor + specularity) * NdotL * attenColor;
float4 finalCol = float4(directLightCol, 1);
UNITY_APPLY_FOG(i.fogCoord, finalCol);
fixed4 col = finalCol;
return col;
}
ENDCG
}
}
FallBack "Specular"
}
你会发现金属度高的时候材质显得过黑,如果是这样那就对了,因为目前并没有考虑间接光照
四、法线分布函数(Normal Distribution Function, NDF)
依赖完整的 Cook-Torrance BRDF 模型,反射方程 中的
项就是法线分布函数(Normal Distribution Function)
其中最经典的莫过于之前的 Blinn-Phong 经验模型:,这个已经实现过不少次了,所以这次考虑使用一些更高端的模型
4.1 Beckman NDF
定义 为粗糙度 roughness,有
Beckman NDF 额外的考虑了粗糙度值
CGINCLUDE
#define PI 3.1415926535
//Beckman NDF(D项)
float BeckmannNormalDistribution(float roughness, float NdotH)
{
float roughnessSqr = roughness * roughness;
float NdotHSqr = NdotH * NdotH;
return max(0.000001, (1.0 / (PI * roughnessSqr * NdotHSqr * NdotHSqr)) * exp((NdotHSqr - 1) / (roughnessSqr * NdotHSqr)));
}
ENDCG
4.2 GGX NDF
定义 为粗糙度 roughness,
,有
最常用的经验公式,这也是 2018 UnityStandard 中使用的标准各向同性分布函数
//GGX NDF(D项)
float GGXNormalDistribution(float roughness, float NdotH)
{
float roughnessSqr = roughness * roughness;
float NdotHSqr = NdotH * NdotH;
float TanNdotHSqr = (1 - NdotHSqr) / NdotHSqr;
return (1.0 / PI) * sqrt(roughness / (NdotHSqr * (roughnessSqr + TanNdotHSqr)));
}
4.3 未完待续……
更多的经验方程可参考:https://www.jordanstevenstechart.com/physically-based-rendering
五、阴影遮蔽函数(Geometric Shadowing Function, GSF)
依赖完整的 Cook-Torrance BRDF 模型,反射方程 中的 G 项就是阴影遮蔽函数(Geometric Shadowing Function)
如果不想要计算这一项,那么可以默认值为1,所有的阴影遮蔽函数值域都为
5.1 Schlick-Beckman GSF
定义 为粗糙度 roughness,
,有
当然这里的 可以说是一个折中,对于直接光和间接光,还可以针对性的使用下面两个公式
- 针对光照时:
- 针对 IBL 的重映射(Remapping):
其实本质上,G 项为入射光遮蔽和出射光遮蔽两者的乘积,也就是
#define CalculationKAverage(p) pow(p, 2) * 0.797884560802865;
#define CalculationKDirect(p) pow(p + 1, 2) / 8;
#define CalculationKIBL(p) pow(p, 2) / 8;
//Schlick-Beckman Geometric(G项)
float SchlickBeckmanGeometricShadowingFunction(float NdotL, float NdotV, float roughness)
{
float k = CalculationKDirect(roughness);
float SmithL = (NdotL) / (NdotL * (1 - k) + k);
float SmithV = (NdotV) / (NdotV * (1 - k) + k);
float Gs = (SmithL * SmithV);
return Gs;
}
5.2 未完待续……
更多的经验方程可参考:https://www.jordanstevenstechart.com/physically-based-rendering
六、菲涅尔项(Fresnel Function)
依赖完整的 Cook-Torrance BRDF 模型,反射方程 中的 F 项就是菲涅尔项(Fresnel Function)
不必多说,大名鼎鼎的 Fresnel-Schlick 近似,它几乎是最常用的经验方程:
6.1 Fresnel-Schlick 近似
Fresnel-Schlick 还有另一种拟合版本 Spherical-Gaussian Fresnel:
,它的效率要高一些,这在 UE4 中被使用
float SchlickFresnel(float i)
{
float x = clamp(1.0 - i, 0.0, 1.0);
float x2 = x * x;
return x2 * x2 * x;
}
//Schlick-Fresnel Fresnel(F项)
float3 SchlickFresnelFunction(float3 F0, float LdotH)
{
return F0 + (1 - F0) * SchlickFresnel(LdotH);
}
//Spherical-Gaussian-Fresnel Fresnel拟合(F项)
float SphericalGaussianFresnelFunction(float3 F0, float LdotH)
{
float power = ((-5.55473 * LdotH) - 6.98316) * LdotH;
return F0 + (1 - F0) * pow(2, power);
}
对于金属材质的 Fresnel 计算,需要额外基础反射率,有三个方案:
- 通过表面颜色(一般是反照率 Albedo)和非金属基础反射率的平均值 0.04 插值得到
- 利用折射指数(Indices of Refraction, IOR)直接计算,其中 IOR 由输入指定
- 直接将计算得到的高光颜色(specColor)作为基础反射率
对于①②的代码如下:
//利用折射指数(Indices of Refraction, IOR)直接计算金属材质的F0
float SchlickIORFresnelFunction(float ior, float LdotH)
{
float F0 = pow(ior - 1, 2)/pow(ior + 1, 2);
return SchlickFresnelFunctionSingle(F0, LdotH);
}
//利用插值计算金属材质的F0,其中unity_ColorSpaceDielectricSpec.rgb为所有非金属基础反射率的平均值,约为0.04
float SchlickLerpFresnelFunction(float LdotH)
{
float F0 = lerp(unity_ColorSpaceDielectricSpec.rgb, _Color, _Metallic);
return SchlickFresnelFunctionSingle(F0, LdotH);
}
float SchlickFresnelFunctionSingle(float F0, float LdotH)
{
return F0 + (1 - F0) * SchlickFresnel(LdotH);
}
6.2 Disney BRDF Fresnel
在计算漫反射项 时,
项的计算也可参考 Disney BRDF:
,其中
此时 ,这也是 2018 UnityStandard 的写法
#define MixFunction(i, j, x) j * x + i * (1.0 - x)
float DisneyDiffuseFresnel(float NdotL, float NdotV, float LdotH, float roughness)
{
float FresnelLight = SchlickFresnel(NdotL);
float FresnelView = SchlickFresnel(NdotV);
float FresnelDiffuse90 = 0.5 + 2.0 * LdotH * LdotH * roughness;
return MixFunction(1, FresnelDiffuse90, FresnelLight) * MixFunction(1, FresnelDiffuse90, FresnelView);
}
参考文章:
- https://www.yuque.com/dachengzi-shgxu/manual/uw9c34
- https://www.jordanstevenstechart.com/physically-based-rendering
- https://zhuanlan.zhihu.com/p/96975561
- https://zhuanlan.zhihu.com/p/158025828
- https://blog.csdn.net/Jaihk662/article/details/110644023
- https://zhuanlan.zhihu.com/p/68025039