

官方LITshader和我的轮子近乎一样
作为一个Bridge重度用户,我在使用hdrp管线完全离不开它了,自带的上万个扫描级材质贴图,大大降低了SP/SD里绘制制作周期,当然它也可以当作SD/SP的素材进行二次编辑。随着我想在URP管线里充分使用Bridge的资源,URP自带的LIT无法充分利用Mask图(它会额外多一次AO的采样的开销),这对于性能来说不太划算,我更希望的是像hdrp那样直接读取;还有个原因是我希望在后续能自由拓展LITshader的功能,目前URP不支持表面着色器虽然Colin神已经开源了一个URP的表面着色器(https://github.com/ColinLeung-NiloCat/UnityURP-SurfaceShaderSolution)有兴趣的可以去看看,但是表面着色器又和第一个原因冲突了。至此,打算自己造一个能直接使用Bridge的PBR的轮子,甚至后续支持bridge的基于PBR的NPRshader开发。

URP有多一次AO的采样
本次轮子的效果如下:
1.本PBR轮子效果近乎和LITshader的效果一直,自认为高达90%的相似度,下图读者可以区分出哪个是手写的,哪个是官方的么?

2.本PBR轮子尽量不增加新的功能,只保证基础效果一致,其他的pass比如depthonly的pass,shadowcaster的pass或者对lightmap的采样就交给读者去自己实现,方便读者自我拓展功能,或者把它作为URP的shader源码的学习资料。
3.只有3张贴图,一张纹理,一张mask,一张法线。mask图遵守HDRP的规则(即Bridge的导出规则),R通道为金属,G通道为AO,A为光滑度。


注意!本篇文章只注重实践,不会涉及PBR的各个公式的原理推导,想要了解的网上一大堆文章和论文或者书籍;PBR的理论部分也不会在本文里讲到,入门精要的理论部分就讲得很好。
核心方程如下,它分为2大部分,漫反射部分和高光部分。

在unity里对直接光照(直射光,点光源,聚光灯)和间接光照(反射探针灯光探针)的处理也不太一样:直接光照的计算和上式基本一致;间接光照则利用了各种trick,各种预计算和近似拟合来达到我们想要的效果。

直射光照部分:
1.高光
1.1法线分布函数GGX(Normal Distribution Function),即核心方程的D项,它是描述微观法线N和半角向量H的趋同性比重。

我们在unity里定义该函数,非常简单,照着公式写即可。

它的效果如下,随着粗糙度的变化而变化。

我们把粗糙度换成贴图后,它的效果如下。

1.2 几何函数G,(Geometry function),即核心方程的G项,它是描述入射射线(即光照方向)和出射射线(视线方向)被自己的微观几何形状遮挡的比重。

G项是由2个几乎一样的子项相乘得来,故我们可以先定义子项的函数。

再去定义真正的G项,把NL和NV代入子项,相乘即可。

注意这里的K系数,在直射光和间接光的计算方式略有不同,代码里是直接光的,而间接光的K=pow(roughness,2)/2,这里要注意。

随着粗糙度的变化
而我们把粗糙度换成贴图后,效果如下。

1.3 菲涅尔函数F(Fresnel equation),即核心方程的F项,如下图所示。

但是由于我们需要的法线方向并不是模型本身的宏观法线n,而是经过D项筛选通过的微观法线H,故需把N改成H。

后来unity对它进行了优化(https://link.zhihu.com/?target=http%3A//filmicworlds.com/blog/optimizing-ggx-shaders-with-dotlh/),视线方向V换成了L,如下所示,这是我们所使用的函数。

其中的5次方计算量比较大,把它变成自然对数函数进行计算可以节省计算量,后续文章里所有的5次方计算都可以换算成对数计算。

故最终,我们的菲涅尔函数如下。

我们把它输出看看,效果和平时简单的菲涅尔不太一样,但是它是正确的。

1.4直接光的高光部分:我们把D,G,F代入公式,先计算出高光部分。注意这里经过半球积分后会乘PI,不要丢了;注意这里并未再次乘KS,因为KS=F,已经计算过了一次不需要重复计算。

直接光_高光部分

2.漫反射
漫反射就超级简单了,本质就是NdotL,计算出KD。这里要注意由于分母带了PI,而半球积分后会乘PI,两者就约掉了这就没有写。

我们把漫反射和高光反射加起来,也就是整个直接光的结果输出,效果如下,至此,直接光完毕。


间接光照部分:
1.漫反射和球谐函数
间接光照的漫反射本质是对光照探针进行采样,这里有篇文章讲得很好(网页链接)
这部分我是抄的urp的Lighting.hlsl的源码,代码如下。

我们把它输出,它是取的最近的4个光照探针的值得到的结果。

得到颜色后,再通过计算间接光照的菲涅尔函数得到KD,其计算如下图。


2.高光反射和IBL
间接光照的高光反射本质是对于反射探针(360全景相机)拍的一张图进行采样,把采样到的颜色当成光照去进行计算,这种光照称为基于图像的光照IBL(Image-Based Lighting),前面的漫反射也是IBL,关于IBL有非常复杂的理论,本人才疏学浅就不展开了。

这张全景相机拍的图是cube类型,我们先计算反射射线的方向对cube进行采样。我们还需要根据不同的粗糙度进行采样不同的模糊度,粗糙度需要修改一下曲线去拟合,它并非线性;模糊度从0-6总共7个模糊等级,算法我是参考URP的源码imageBasedLighting.hlsl和Lighting.hlsl改改写写的。

我们把得到的反射探针的图像输出,效果如下。

我们还需要计算一个间接高光的影响因子,一般来说是按照一张现成的LUT图像进行采样,采样UV的U方向为NdotV,V方向为粗糙度来得到我们需要的影响因子。

LUT,它的采样过程耗费性能不在考虑
但是采样贴图的过程非常耗,unity使用的曲线拟合去得到结果,其计算函数如下。计算过程也是白嫖的Lighting.hlsl的写法。

我们把间接光照和factor相乘输出,环境高光的强度就很弱了。

最后,把全部间接光输出,效果如下。

全部间接光

最终把全部直接光和间接光加起来,输出,就是我们需要的最终结果。从Bridge里拉一个模型出来,丢上贴图,丢上一个官方的lit shader和自己写的pbr shader,对比图如下,近乎一模一样的效果(当然自己写的多了个AO有一些小的差异)。

喜欢卡渲的大佬欢迎入驻群聊950138189。
Git工程地址:https://github.com/NiceTimeJob/PBR_URP
参考文章如下:
https://zhuanlan.zhihu.com/p/68025039
https://zhuanlan.zhihu.com/p/33464301
函数库源码
#ifndef PBR_Function_INCLUDE
#define PBR_Function_INCLUDE
//D项 法线微表面分布函数
float D_Function(float NdotH,float roughness)
{
float a2=roughness*roughness;
float NdotH2=NdotH*NdotH;
//直接根据公式来
float nom=a2;//分子
float denom=NdotH2*(a2-1)+1;//分母
denom=denom*denom*PI;
return nom/denom;
}
//G项子项
float G_section(float dot,float k)
{
float nom=dot;
float denom=lerp(dot,1,k);
return nom/denom;
}
//G项
float G_Function(float NdotL,float NdotV,float roughness)
{
float k=pow(1+roughness,2)/8;
float Gnl=G_section(NdotL,k);
float Gnv=G_section(NdotV,k);
return Gnl*Gnv;
}
//F项 直接光
real3 F_Function(float HdotL,float3 F0)
{
float Fre=exp2((-5.55473*HdotL-6.98316)*HdotL);
return lerp(Fre,1,F0);
}
//F项 间接光
real3 IndirF_Function(float NdotV,float3 F0,float roughness)
{
float Fre=exp2((-5.55473*NdotV-6.98316)*NdotV);
return F0+Fre*saturate(1-roughness-F0);
}
//间接光漫反射 球谐函数 光照探针
real3 SH_IndirectionDiff(float3 normalWS)
{
real4 SHCoefficients[7];
SHCoefficients[0]=unity_SHAr;
SHCoefficients[1]=unity_SHAg;
SHCoefficients[2]=unity_SHAb;
SHCoefficients[3]=unity_SHBr;
SHCoefficients[4]=unity_SHBg;
SHCoefficients[5]=unity_SHBb;
SHCoefficients[6]=unity_SHC;
float3 Color=SampleSH9(SHCoefficients,normalWS);
return max(0,Color);
}
//间接光高光 反射探针
real3 IndirSpeCube(float3 normalWS,float3 viewWS,float roughness,float AO)
{
float3 reflectDirWS=reflect(-viewWS,normalWS);
roughness=roughness*(1.7-0.7*roughness);//Unity内部不是线性 调整下拟合曲线求近似
float MidLevel=roughness*6;//把粗糙度remap到0-6 7个阶级 然后进行lod采样
float4 speColor=SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0,reflectDirWS,MidLevel);//根据不同的等级进行采样
#if !defined(UNITY_USE_NATIVE_HDR)
return DecodeHDREnvironment(speColor,unity_SpecCube0_HDR)*AO;//用DecodeHDREnvironment将颜色从HDR编码下解码。可以看到采样出的rgbm是一个4通道的值,最后一个m存的是一个参数,解码时将前三个通道表示的颜色乘上xM^y,x和y都是由环境贴图定义的系数,存储在unity_SpecCube0_HDR这个结构中。
#else
return speColor.xyz*AO;
#endif
}
//间接高光 曲线拟合 放弃LUT采样而使用曲线拟合
real3 IndirSpeFactor(float roughness,float smoothness,float3 BRDFspe,float3 F0,float NdotV)
{
#ifdef UNITY_COLORSPACE_GAMMA
float SurReduction=1-0.28*roughness,roughness;
#else
float SurReduction=1/(roughness*roughness+1);
#endif
#if defined(SHADER_API_GLES)//Lighting.hlsl 261行
float Reflectivity=BRDFspe.x;
#else
float Reflectivity=max(max(BRDFspe.x,BRDFspe.y),BRDFspe.z);
#endif
half GrazingTSection=saturate(Reflectivity+smoothness);
float Fre=Pow4(1-NdotV);//lighting.hlsl第501行
//float Fre=exp2((-5.55473*NdotV-6.98316)*NdotV);//lighting.hlsl第501行 它是4次方 我是5次方
return lerp(F0,GrazingTSection,Fre)*SurReduction;
}
#endif

Shader源码
Shader "WX/URP/PBR"
{
Properties
{
_BaseColor("BaseColor",Color)=(1,1,1,1)
_BaseMap("MainTex",2D)="white"{}
[NoScaleOffset]_MaskMap("MaskMap",2D)="white"{}
[NoScaleOffset][Normal]_NormalMap("NormalMap",2D)="Bump"{}
_NormalScale("NormalScale",Range(0,1))=1
}
SubShader
{
Tags{
"RenderPipeline"="UniversalRenderPipeline"
}
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "PbrFunction.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
real4 _BaseColor;
float _NormalScale;
CBUFFER_END
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
TEXTURE2D(_MaskMap); SAMPLER(sampler_MaskMap);
TEXTURE2D(_NormalMap); SAMPLER(sampler_NormalMap);
struct a2v
{
float4 positionOS:POSITION;
float4 normalOS:NORMAL;
float2 texcoord:TEXCOORD;
float4 tangentOS:TANGENT;
float2 lightmapUV:TEXCOORD2;//一般来说是2uv是lightmap 这里取3uv比较保险
};
struct v2f
{
float4 positionCS:SV_POSITION;
float4 texcoord:TEXCOORD;
float4 normalWS:NORMAL;
float4 tangentWS:TANGENT;
float4 BtangentWS:TEXCOORD1;
};
ENDHLSL
pass
{
Tags{
"LightMode"="UniversalForward"
"RenderType"="Opaque"
}
HLSLPROGRAM
#pragma vertex VERT
#pragma fragment FRAG
v2f VERT(a2v i)
{
v2f o;
o.positionCS=TransformObjectToHClip(i.positionOS.xyz);
o.texcoord.xy=TRANSFORM_TEX(i.texcoord,_BaseMap);
o.texcoord.zw=i.lightmapUV;
o.normalWS.xyz=normalize(TransformObjectToWorldNormal(i.normalOS.xyz));
o.tangentWS.xyz=normalize(TransformObjectToWorldDir(i.tangentOS.xyz));
o.BtangentWS.xyz=cross(o.normalWS.xyz,o.tangentWS.xyz)*i.tangentOS.w*unity_WorldTransformParams.w;
float3 posWS=TransformObjectToWorld(i.positionOS.xyz);
o.normalWS.w=posWS.x;
o.tangentWS.w=posWS.y;
o.BtangentWS.w=posWS.z;
return o;
}
real4 FRAG(v2f i):SV_TARGET
{
//法线部分得到世界空间法线
float4 nortex=SAMPLE_TEXTURE2D(_NormalMap,sampler_NormalMap,i.texcoord.xy);
float3 norTS=UnpackNormalScale(nortex,_NormalScale);
norTS.z=sqrt(1-saturate(dot(norTS.xy,norTS.xy)));
float3x3 T2W={i.tangentWS.xyz,i.BtangentWS.xyz,i.normalWS.xyz};
T2W=transpose(T2W);
float3 N=NormalizeNormalPerPixel(mul(T2W,norTS));
//return float4(N,1);
//计算一些可能会用到的杂七杂八的东西
float3 positionWS=float3(i.normalWS.w,i.tangentWS.w,i.BtangentWS.w);
real3 Albedo=SAMPLE_TEXTURE2D(_BaseMap,sampler_BaseMap,i.texcoord.xy).xyz*_BaseColor.xyz;
float4 Mask=SAMPLE_TEXTURE2D(_MaskMap,sampler_MaskMap,i.texcoord.xy);
float Metallic=Mask.r;
float AO=Mask.g;
float smoothness=Mask.a;
float TEMProughness=1-smoothness;//中间粗糙度
float roughness=pow(TEMProughness,2);// 粗糙度
float3 F0=lerp(0.04,Albedo,Metallic);
Light mainLight=GetMainLight();
float3 L=normalize(mainLight.direction);
float3 V=SafeNormalize(_WorldSpaceCameraPos-positionWS);
float3 H=normalize(V+L);
float NdotV=max(saturate(dot(N,V)),0.000001);//不取0 避免除以0的计算错误
float NdotL=max(saturate(dot(N,L)),0.000001);
float HdotV=max(saturate(dot(H,V)),0.000001);
float NdotH=max(saturate(dot(H,N)),0.000001);
float LdotH=max(saturate(dot(H,L)),0.000001);
//直接光部分
float D=D_Function(NdotH,roughness);
//return D;
float G=G_Function(NdotL,NdotV,roughness);
//return G;
float3 F=F_Function(LdotH,F0);
//return float4(F,1);
float3 BRDFSpeSection=D*G*F/(4*NdotL*NdotV);
float3 DirectSpeColor=BRDFSpeSection*mainLight.color*NdotL*PI;
//return float4(DirectSpeColor,1);
//高光部分完成 后面是漫反射
float3 KS=F;
float3 KD=(1-KS)*(1-Metallic);
float3 DirectDiffColor=KD*Albedo*mainLight.color*NdotL;//分母要除PI 但是积分后乘PI 就没写
//return float4(DirectDiffColor,1);
float3 DirectColor=DirectSpeColor+DirectDiffColor;
//return float4(DirectColor,1);
//间接光部分
float3 SHcolor=SH_IndirectionDiff(N)*AO;
float3 IndirKS=IndirF_Function(NdotV,F0,roughness);
float3 IndirKD=(1-IndirKS)*(1-Metallic);
float3 IndirDiffColor=SHcolor*IndirKD*Albedo;
//return float4(IndirDiffColor,1);
//漫反射部分完成 后面是高光
float3 IndirSpeCubeColor=IndirSpeCube(N,V,roughness,AO);
//return float4(IndirSpeCubeColor,1);
float3 IndirSpeCubeFactor=IndirSpeFactor(roughness,smoothness,BRDFSpeSection,F0,NdotV);
float3 IndirSpeColor=IndirSpeCubeColor*IndirSpeCubeFactor;
//return float4(IndirSpeColor,1);
float3 IndirColor=IndirSpeColor+IndirDiffColor;
//return float4(IndirColor,1);
//间接光部分计算完成
float3 Color=IndirColor+DirectColor;
return float4(Color,1);
}
ENDHLSL
}
}
}