自言自语
又是很久没有写笔记了。因为一直在啃PBR。总算在昨晚算是啃通了一遍。不过这只是个简单的builltin下的相对简单的PBR框架的shader 不过对于我来说也算是一个阶段性总结吧。 shader是metallic work flow 金属度工作流的。 模型和贴图都是我几年前做的一个东西,那会儿记得刚开始接触SP,瞎捣鼓一通,昨晚翻箱倒柜弄出来,换上自己刚写顺的shader,因为自己做美术这么些年所谓次时代的作品实在太少了。不管怎样,自己做的模型,贴图,配上自己写的shader,算是小功告成哈哈。(尽管公式都是抄,毛病也不少,总归让我有继续坚持下去的小动力了)
一、截图效果
因为优快云没办法上传视频,所以。。只能截图啦
单一平行光照
平行光加点光源
二、shader
Shader "TNShaderPractise/ShaderPractise_PBR"
{
Properties
{
//所用贴图放一起
[Header(Texutrue)]
_Albedo("颜色贴图",2D) = "white"{}
_Metallic("金属度贴图",2D) = "white"{}
[Normal]_Normal("法线贴图",2D) = "bump"{}
_Occlusion("环境遮蔽贴图",2D ) ="white"{}
//调节参数放一起
[Space(20)][Header(Parameter)]
[HDR]_Color ("主体颜色",Color) = (1,1,1,1)
_MetallicValue("金属度",Range(0,1.0))= 1
_Roughness ("粗糙度",Range(0,1))=1
//自己瞎编的一个参数
_GGX("迪士尼拖尾控制",Range(0.001,8))=2
_NormalScale("法线贴图强度",Range(-5,5))=1
_OcclusionStrenth("AO强度",Range(0,1))=0
//自发光算是另类 单独放
[Space(20)][Header(Emission)]
_EmissiveTex ("自发光贴图",2D) ="black"{}
[HDR]_Emissive ("自发光颜色",Color)=(0,0,0,1)
}
SubShader
{
Tags { "RenderType" = "Opaque" "Queue" = "Geometry" }
//单一平行光照pass
pass
{
Tags {"LightMode" = "ForwardBase" }
ZWrite On
Cull Back
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "Lighting.cginc"
#include "UnityCg.cginc"
#include "AutoLight.cginc"
#include "UnityPBSLighting.cginc"
#include "HLSLSupport.cginc"
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 pos : SV_POSITION;
float4 TtoW1 : TEXCOORD1;
float4 TtoW2 : TEXCOORD2;
float4 TtoW3 : TEXCOORD3;
float2 uv : TEXCOORD4;
//阴影宏必须
LIGHTING_COORDS(5,6)
UNITY_FOG_COORDS(7)
};
sampler2D _Albedo;
sampler2D _Metallic;
sampler2D _Normal;
sampler2D _Occlusion;
sampler2D _EmissiveTex;
float4 _Color;
float4 _Emissive;
float _MetallicValue;
float _Roughness;
float _NormalScale;
float _OcclusionStrenth;
float _GGX;
v2f vert (a2v v)
{
v2f o;
UNITY_INITIALIZE_OUTPUT(v2f,o);
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
float3 posWS = mul(unity_ObjectToWorld,v.vertex).xyz;
float3 Normal = normalize(UnityObjectToWorldNormal(v.normal));
float3 tangent = normalize(UnityObjectToWorldDir(v.tangent.xyz));
float3 binormal = cross (Normal,tangent)*v.tangent.w;
//构建TBN矩阵
//其实也可以直接构建一个矩阵在片元着色器.新学的.URP下撸PBR的时候试试
o.TtoW1 = float4 (tangent.x,binormal.x,Normal.x,posWS.x);
o.TtoW2 = float4 (tangent.y,binormal.y,Normal.y,posWS.y);
o.TtoW3 = float4 (tangent.z,binormal.z,Normal.z,posWS.z);
//添加Unity内置雾影响
UNITY_TRANSFER_FOG(o,o.pos);
//阴影宏
TRANSFER_VERTEX_TO_FRAGMENT(o);
return o;
}
-------------------------------------------------------------------------BRDF计算-------------------------------------------------------------
//Diffuse Term f diffuse(l,v) = baseColor / (UNITY_PI*lightF*viewF)
//用inline来声明函数为 内联调用,减少函数因为调用频繁造成的开销
inline float4 DiffuseTerm (float Roughness, float LH,float NL, float NV,float4 Albedo)
{
float Fd90 = 0.5+2*Roughness*LH*LH;
float lightScatter = 1+(Fd90-1)*pow(1-NL,5);
float viewScatter = 1+(Fd90-1)*pow(1-NV,5);
return Albedo *UNITY_INV_PI* lightScatter * viewScatter; //UNITY_INV_PI = 1/3.1415926532
}
//Schlick Fresnel项
inline float3 FresnelTerm (float3 Albedo,float3 SPC ,float LH)
{
float t = pow(1-LH,5);
float3 F0 = SPC;
return F0+(1-F0)*t;
}
//可见性项 把分母部分的4(nl)(nv)一与阴影遮掩函数 Shadowing-Masking Function 进行合并 组成可见性项 V SmithJointGGXVisibilityTerm
inline float CustomSmithJointGGXVisibilityTerm ( float NL,float NV, float R)
{
//错误修正 去掉多余平方
//float a = R*R;
float a2 = R*R;
float lambedV = NV*(NL*(1-a2)+a2);
float lambedL = NL*(NV*(1-a2)+a2);
//1e-5f 科学计数 1e-5的意思就是1乘以10的负5次幂。就是0.000001
return 0.5/(lambedV+lambedL+1e-5f);
}
//正向法线分布函数 Normal Distribution Function
inline float GGXDistribution (float R,float NH)
{
//错误修正 去掉多余平方
//float a = R*R;
float a2 = R*R;
float d = (a2-1)*NH*NH+1;
float t = UNITY_PI*pow(d,_GGX);
return a2/t;
}
//用于做IBL矫正的颜色插值函数
inline half4 CustomFresnelLerp (float4 C1, float4 C2, float NV)
{
//C1 为高光颜色非高光项
half t = pow(1-NV,5);
return lerp (C1,C2,t);
}
-------------------------------------------------------------------------BRDF计算-------------------------------------------------------------
float4 frag (v2f i):SV_Target
{
//世界空间下的顶点
float3 posWS = float3 (i.TtoW1.w,i.TtoW2.w,i.TtoW3.w);
//准备采样的贴图
float4 albedo = tex2D(_Albedo,i.uv);
float4 occlusionTex = tex2D(_Occlusion,i.uv);
//金属度贴图采样,采样得到的r通道为金属度 通用SP流程是把金属度放在R通道的 当然可以自己改
//一般来说金属度纹理的R通道中 白色表示金属 黑色表示非金属 一开始我也不知道自己怎么记得给记反了所以效果总是不对...
float metallic = tex2D (_Metallic,i.uv).r*_MetallicValue;
做掠射角系数 以此来得到更好的菲涅耳反射效果
//非金属部分高光相对较弱但也不是完全没有,所以是个恒定的值0.04 相关材质数值查找表可以去unity或者substance上查找 知乎上也有,就是那张有名的图
float4 specularCol = lerp(0.04,albedo,metallic);
//这一步操作是为了利用金属度贴图 来区分金属部分和非金属部分 金属部分漫反射为0光线都被金属材质吸收了很少反射或者不反射 非金属部分则正常漫反射表现
float oneMinusReflectivityFromMetallic = 0.96*(1-metallic);//根据URP计算更新为乘以0.96f
//这里我把AO单独做为一张图塞进来 为了方便控制AO 其实可以做的更细一点 调节AO范围及颜色. 但都是小trick 这里就不写了 这个shader只是为了总体了解PBR的流程和shader框架
albedo = lerp(albedo,albedo*occlusionTex,_OcclusionStrenth)*oneMinusReflectivityFromMetallic*_Color;
//粗糙度贴图放在金属度纹理的A通道上.这个根据贴图导出设置,可以在计算正向法线分布函数的时候 注意其的指数是否需要再平方
//做个取反操作方便视觉上调整 以达到 _Roughness越大 越光滑 越小越粗糙
float roughness =tex2D (_Metallic,i.uv).a*max(0.02,(1-_Roughness));
//准备向量
float3 bump = UnpackNormal (tex2D(_Normal,i.uv));
bump.xy *= _NormalScale;
//法线贴图的B通道意义不大 因为Z轴还是要通过xy来计算得到. 如果纹理资源紧张,可以利用法线贴图的b通道和a通道来存放一些纹理信息供我们实现更多的需求
bump.z = sqrt(1-saturate(dot(bump.xy,bump.xy)));
//TBN计算世界空间法线贴图信息,URP shader中我会直接用3x3矩阵构建TBN
float3 normal = normalize( half3 (dot(i.TtoW1.xyz,bump),dot(i.TtoW2.xyz,bump),dot(i.TtoW3.xyz,bump)));
float3 lightDir = normalize(UnityWorldSpaceLightDir(float4(posWS,1)));
float3 viewDir = normalize(UnityWorldSpaceViewDir(float4(posWS,1)));
float3 halfDir = normalize(lightDir+viewDir);
float3 reflDir = normalize(reflect(-viewDir,normal));
//准备点乘变量
float nl = saturate(dot(normal,lightDir))*0.5+0.5;
float nv = saturate(dot(normal,viewDir));
float nh = saturate(dot(normal,halfDir));
float lh = saturate (dot(halfDir,lightDir));
float lv = saturate(dot(lightDir,viewDir));
//准备灯光衰减系数
UNITY_LIGHT_ATTENUATION (atten,i,posWS);
//漫反射项 Diffuse term (Bidirectional Reflectance Distribution Function)
float4 diffCol = DiffuseTerm (roughness,lh,nl,nv,albedo);
//高光反射项 Specular term
float3 F = FresnelTerm (albedo.rgb,specularCol.rgb,lh);//新的间歇使用NV
float V = CustomSmithJointGGXVisibilityTerm(nl,nv,roughness);
float D = GGXDistribution(roughness,nh);
//f(l,v)= FGD/(4*nl*nv) ===> V = G/nl*vn ===> f(l,v)= FVD/4 ===> f(l,v)= FVD*0.25 因为可见性项V中已经进行了 除以4的操作 所以后边直接输出FVD相乘
float4 SpecularCol =float4(F * V * D ,1);
//自发光项 Emissive Term
float4 Emissive = tex2D(_EmissiveTex,i.uv)*_Emissive;
//IBL Term (Image Based Lighting) 环境光部分的计算 氛围高光反射项 和漫反射项两部分
首先是高光反射项
half perceptualRoughness = roughness*(1.7-0.7*roughness);
half mip = perceptualRoughness*6;
天空球和反射探针材质声明专用方式 采用Unity内置的 UNITY_DECLARE_TEXCUBE 宏来实现. HLSLSupport.cginc文件中定义
//unity_SpecCube0 可以在 UnityShaderVariables.cginc中找到其声明
//求得的环境贴图若开启了HDR 则要进行解码。测试发现不解码 反射探针不能参数调节不能实时传递
half3 envMap = DecodeHDR(UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0,reflDir,mip),unity_SpecCube0_HDR);
half grazingTerm = saturate((1-roughness)+1-oneMinusReflectivityFromMetallic);
half surfaceReduction = 1/(roughness*roughness+1);
half4 indirectSpecular = surfaceReduction*float4(envMap,1)*CustomFresnelLerp(specularCol,grazingTerm,nv);
IBL的漫反射想部分. 因为球谐光照计算比较复杂,懒得敲了.也就直接伸手党,直接用Unity的ShaderSH9函数进行套用
//这里的球谐函数计算是3阶球谐函数,而SP里的是2阶球谐函数,所以两遍效果是有差异的,要想尽可能减少表现差异,需要进行look dev 矫正
//冯乐乐在UWA中有一篇look dev 矫正文章,付费查看,我买了,看了. 之前没看懂,现在还没找机会去实践.
float4 indirectDiffuse = float4(ShadeSH9(float4(normal,1)),1)*albedo;
//最后输出的颜色 直接光照部分的漫反射项和高光反射项相加 乘以直接光照和阴影衰减系数再乘以π和NL 这就是图形大佬写出的光照方程.最后再把额外的环境光照和自发光也加上就成了
float4 col = Emissive+UNITY_PI*( diffCol+SpecularCol)*_LightColor0*nl*atten+indirectDiffuse +indirectSpecular;
//混合雾效
UNITY_APPLY_FOG(i.fogCoord,col.rgb);
//return albedo;
return col;
}
ENDCG
}
//这部分是其他光照pass 包括额为的点光 平行光 等等
pass
{
//额外光照部分的LightMode 需要 设置好标签
Tags {"LightMode" = "ForwardAdd" }
//并且要注意设置混合模式. 不然会有灯光表现错误
Blend One One
Cull Back
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//这里的pragma也要注意 compile_multi_fwdadd
#pragma multi_compile_fwdadd
#include "Lighting.cginc"
#include "UnityCg.cginc"
#include "AutoLight.cginc"
#include "UnityPBSLighting.cginc"
#include "HLSLSupport.cginc"
//其他部分和直接光照部分一样,复制粘贴。 然后去掉已经在直接光照部分计算过的 自发光, 环境光 即可。 阴影衰减由于每个光源不atten的值不同 所以仍然要计算
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float4 TtoW1 : TEXCOORD1;
float4 TtoW2 : TEXCOORD2;
float4 TtoW3 : TEXCOORD3;
float2 uv : TEXCOORD4;
LIGHTING_COORDS(5,6)
};
sampler2D _Albedo;
sampler2D _Metallic;
sampler2D _Normal;
sampler2D _Occlusion;
sampler2D _EmissiveTex;
float4 _Color;
float4 _Emissive;
float _MetallicValue;
float _Roughness;
float _NormalScale;
float _OcclusionStrenth;
float _GGX;
v2f vert (a2v v)
{
v2f o;
UNITY_INITIALIZE_OUTPUT(v2f,o);
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
float3 posWS = mul(unity_ObjectToWorld,v.vertex).xyz;
float3 Normal = normalize(UnityObjectToWorldNormal(v.normal));
float3 tangent = normalize(UnityObjectToWorldDir(v.tangent.xyz));
float3 binormal = cross (Normal,tangent)*v.tangent.w;
o.TtoW1 = float4 (tangent.x,binormal.x,Normal.x,posWS.x);
o.TtoW2 = float4 (tangent.y,binormal.y,Normal.y,posWS.y);
o.TtoW3 = float4 (tangent.z,binormal.z,Normal.z,posWS.z);
return o;
}
-------------------------------------------------------------------------BRDF计算-------------------------------------------------------------
//Diffuse Term f diffuse(l,v) = baseColor / (UNITY_PI*lightF*viewF)
//用inline来声明函数为 内联调用,减少函数因为调用频繁造成的开销
inline float4 DiffuseTerm (float Roughness, float LH,float NL, float NV,float4 Albedo)
{
float Fd90 = 0.5+2*Roughness*LH*LH;
//float4 diff =albedo*UNITY_INV_PI* (1+(Fd90-1)*pow(1-nl,5))*(1+(Fd90-1)*(pow(1-nv,5)));
float lightScatter = 1+(Fd90-1)*pow(1-NL,5);
float viewScatter = 1+(Fd90-1)*pow(1-NV,5);
//return Albedo /3.1415926532 * lightScatter * viewScatter; //UNITY_INV_PI = 1/3.1415926532
return Albedo *UNITY_INV_PI* lightScatter * viewScatter; //UNITY_INV_PI = 1/3.1415926532
}
//Schlick Fresnel函数 金属度工作流
inline float3 FresnelTerm (float3 Albedo,float3 SPC ,float LH)
{
float t = pow(1-LH,5);
//return SpecColor+(SpecColor-1)*t;
float3 F0 = SPC;
return F0+(1-F0)*t;
}
//可见性项 把分母部分的4(nl)(nv)一与阴影遮掩函数 Shadowing-Masking Function 进行合并 组成可见性项 V SmithJointGGXVisibilityTerm
inline float CustomSmithJointGGXVisibilityTerm ( float NL,float NV, float R)
{
float a = R*R;
float a2 = a*a;
float lambedV = NV*(NL*(1-a2)+a2);
float lambedL = NL*(NV*(1-a2)+a2);
return 0.5/(lambedV+lambedL+1e-5f);
}
//正向法线分布函数 Normal Distribution Function
inline float GGXDistribution (float R,float NH)
{
float a = R*R;
float a2 = a*a;
float d = (a2-1)*NH*NH+1;
float t = UNITY_PI*pow(d,_GGX);
return a2/t;
}
inline half4 CustomFresnelLerp (float4 C1, float4 C2, float NV)
{
half t = pow(1-NV,5);
return lerp (C1,C2,t);
}
-------------------------------------------------------------------------BRDF计算-------------------------------------------------------------
float4 frag (v2f i):SV_Target
{
//世界空间下的顶点
float3 posWS = float3 (i.TtoW1.w,i.TtoW2.w,i.TtoW3.w);
//准备采样的贴图
float4 albedo = tex2D(_Albedo,i.uv);
float4 occlusionTex = tex2D(_Occlusion,i.uv);
float metallic = tex2D (_Metallic,i.uv).r*_MetallicValue;
做掠射角系数 以此来得到更好的菲涅耳反射效果
float4 specularCol = lerp(0.04,albedo,metallic);
float oneMinusReflectivityFromMetallic = 1-metallic;
albedo = lerp(albedo,albedo*occlusionTex,_OcclusionStrenth)*oneMinusReflectivityFromMetallic*_Color;
float roughness =tex2D (_Metallic,i.uv).a*max(0.02,(1-_Roughness));
//roughness = lerp(roughness,1-_Roughness,_Roughness);
//准备向量
float3 bump = UnpackNormal (tex2D(_Normal,i.uv));
bump.xy *= _NormalScale;
bump.z = sqrt(1-saturate(dot(bump.xy,bump.xy)));
float3 normal = normalize( half3 (dot(i.TtoW1.xyz,bump),dot(i.TtoW2.xyz,bump),dot(i.TtoW3.xyz,bump)));
#ifdef USING_DIRECTIONAL_LIGHT
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz -posWS);
#endif
float3 viewDir = normalize(UnityWorldSpaceViewDir(float4(posWS,1)));
float3 halfDir = normalize(lightDir+viewDir);
float3 reflDir = normalize(reflect(-viewDir,normal));
//准备点乘变量
float nl = saturate(dot(normal,lightDir));
float nv = saturate(dot(normal,viewDir));
float nh = saturate(dot(normal,halfDir));
float lh = saturate (dot(halfDir,lightDir));
float lv = saturate(dot(lightDir,viewDir));
//准备灯光衰减系数
UNITY_LIGHT_ATTENUATION(atten,i,posWS);
//漫反射项 Diffuse term (Bidirectional Reflectance Distribution Function)
float4 diffCol = DiffuseTerm (roughness,lh,nl,nv,albedo);
//高光反射项 Specular term
float3 F = FresnelTerm (albedo.rgb,specularCol.rgb,lh);
float V = CustomSmithJointGGXVisibilityTerm(nl,nv,roughness);
float D = GGXDistribution(roughness,nh);
//f(l,v)= FGD/(4*nl*nv) ===> V = G/nl*vn ===> f(l,v)= FVD/4 ===> f(l,v)= FVD*0.25
float4 SpecularCol =max(0, float4(F * V * D ,1));
//最后输出的颜色
float4 col = UNITY_PI*( diffCol+SpecularCol)*_LightColor0*nl*atten;
return col;
}
ENDCG
}
}
//最后注意由于加入了阴影和投影的计算,所以仍然需要一个 ShadowCaster pass 来提供阴影渲染. 这里 我们只要FallBack一个带有此Pass的shader 即可实现调用 不用手打
//不过如果需要进行 AlphaTest 和 Alphablend 这样的渲染需求 阴影pass 则需要手动进行修改 保证其投影和接受阴影的表现上的正确性
FallBack "Diffuse"
}
总结
过几天看看能不能把URP下的 PBRshader给撸出来 加油吧 !