找回密码
 立即注册
查看: 514|回复: 6

[笔记] unity build-in管线中的PBR材质Shader分析研究

[复制链接]
发表于 2021-11-26 09:04 | 显示全部楼层 |阅读模式
前言

近来,用到了几次Surface Shader,对于其封装好的PBR计算部分,如果不是复杂的效果其实是挺方便实用的了,但是如果我们想要实现更复杂的效果,还是不能依赖Surface Shader。我终于研究了PBR,但是发现很多国内论坛的资料语焉不详,或是单纯的公式推导分析,或是单纯的源码分析。但是有一些资料还是不错的,作者亲自写了pbr Shader,但是大部分也是只写了直接光漫反射和镜面反射两个部分,意思一下就结束了,少有写全间接光照的,而且我实践下来却发现与unity自带的pbr shader有相当大的出入,这些都驱使我更加深入地研究unity PBR的具体实现,在最后,我完成了一个PBR Shader,函数都与Unity基本相同,我的宗旨是: 最终画面效果一定要与unity默认的shader分毫不差,存在一点误差也就意味着失去了意义。
我理解的PBR

或许是因为我是美术生,我喜欢用美术的思维去理解事物,例如PBR,我将它分为四个部分:直接光漫反射、直接光镜面反射、间接光漫反射和间接光镜面反射。我认为直接光漫反射就像画素描时,先铺的大的明暗关系;直接光镜面反射其实可以理解为高光;间接光漫反射类似于我们画完阴影的暗部,会给它用橡皮泥或是纸巾提亮,给它一个环境光的反射,让阴影区域不至于死黑;间接光镜面反射类似于我们画一些光洁的物体如瓷瓶,我们会最后在上面勾画一些反射上去的窗户什么的。这样理解以后,事情就变得容易一些了。 首先我阅读了很多PBR的资料,了解了BRDF,然后看了一些实现,例如一篇知乎文章,作者的思路给了我很大启发,但是遗憾的是作者将unity与unreal的实现方法杂糅在了一起,最终效果也与standard Shader有一些出入,刚好也是两位TA同事入职了,他们在写PBR,于是我决定仔细阅读源码,力求彻底搞清Untiy build-in管线下的PBR实现。
PBR组成部分

注意:Unity的PBR分三档,分别对应性能不同的机器。我这里分析的都是按照BRDF1_Unity_PBS顶配方式分析的。
直接光漫反射

这里Unity使用的是Disney漫反射模型,为什么没有用lambert模型呢?因为Disney做了实验,认为lambert模型边缘地区太暗,与真实测量值不符,所以拟合了新的漫反射模型。公式如下:  


实现代码:
// Note: Disney diffuse must be multiply by diffuseAlbedo / PI. This is done outside of this function.
half DisneyDiffuse(half NdotV, half NdotL, half LdotH, half perceptualRoughness)
{
    half fd90 = 0.5 + 2 * LdotH * LdotH * perceptualRoughness;
    // Two schlick fresnel term
    half lightScatter   = (1 + (fd90 - 1) * Pow5(1 - NdotL));
    half viewScatter    = (1 + (fd90 - 1) * Pow5(1 - NdotV));
    return lightScatter * viewScatter;
}效果(可以看到Disney漫反射模型的确要比lambert更亮一些):  



直接光镜面反射(高光)

直接关镜面反射由三个部分组成:D项法线分布函数、G项几何函数和F项菲涅尔项。 D项法线分布函数 这部分主要是希望得到一个漂亮的高光效果。传统的Blinn-phong高光缺乏真实度,研究发现高光是带有拖尾的,例如铬金属的高光带有显著的拖尾,GGX模型就是为了把这个拖尾模拟出来,虽然还不能完全模拟,但是比之前的模型已经好了很多。  


黑色曲线表示MERL 铬金属(chrome)真实的高光曲线,红色曲线表示 GGX分布(α= 0.006),绿色曲线表示Beckmann分布(m = 0.013),蓝色曲线表示 Blinn Phong(n = 12000),其中,绿色曲线和蓝色曲线基本重合。可以发现,GGX相对于传统的模型,更接近真实了。公式如下:  


实现代码:
//D项 NDF
inline float GGXTerm (float NdotH, float roughness)
{
    float a2 = roughness * roughness;
    float d = (NdotH * a2 - NdotH) * NdotH + 1.0f; // 2 mad
    return UNITY_INV_PI * a2 / (d * d + 1e-7f);
}效果:  



G项几何函数 这是由微表面模型引出的。微表面理论认为物体表面的微小凹凸也会形成微阴影,进而导致物体的受光面没有那么亮,会更暗一些。而且越粗糙的物体微阴影越多,也就越暗。 Unity没有使用常见的G项公式:


而是采用了这篇论文的成果:  



Unity在论文公式的基础上又简化了实现,所以最终代码如下:
inline half SmithJointGGXVisibilityTerm1 (half NdotL, half NdotV, half roughness)
{
    half a = roughness;
    half lambdaV = NdotL * (NdotV * (1 - a) + a);
    half lambdaL = NdotV * (NdotL * (1 - a) + a);
    return 0.5f / (lambdaV + lambdaL + 1e-5f);
}结果:  



F项菲涅尔项 菲涅尔项在Unity中比较特殊,分为了两个部分,一个是FresnelTerm函数控制反射占的比重,一个是FresnelLerp控制飞掠角的镜面反射强度。FresnelTerm函数非常复杂,因为每个金属对不同波段的光响应曲线也不一样,现在实时渲染使用的都是简化过的,如下:  


实现代码如下:
/F项
half3 FresnelTerm(half3 F0,half cosA){
    half t=Pow5(1-cosA);// ala Schlick interpoliation
    return F0+(1-F0)*t;
}这里,F0的计算如下:
float3 F0 = lerp(unity_ColorSpaceDielectricSpec.rgb, Albedo, _Metallic);因此,对于金属来讲,它的Albedo其实就是F0的颜色,对于塑料这种非金属来讲,它的F0就是unity_ColorSpaceDielectricSpec,这是一个Unity内部设置的默认值,非常暗,颜色值为float3(0.04, 0.04, 0.04),算是一个非金属的默认F0了。以下是F0更多参考:  


结果:  



FresnelLerp函数代码如下:
half3 FresnelLerp(half3 F0,half3 F90,half cosA){
half t=Pow5(1-cosA);
return lerp(F0,F90,t);
}


它的结果是边缘很强,视线直视的地方很暗,符合我们对菲涅尔的认知。这个值最终是与间接光镜面反射相乘,作为其系数。
间接光漫反射




我们导入一张cubemap贴图进unity,unity会自动帮我们把cubemap预积分处理成这样的一张模糊的图片。这个图片就是用来做间接光漫反射用的。看到这个图我们会有两个问题:1.是怎么做的?2.为什么糊到这种程度而不是更模糊或是更清晰呢?我的回答是: 1.Unity用的基函数叫三阶的伴随勒让德多项式,将cubemap采样后滤波做出的。具体做法可以在《 Real-time Rendering》得到。 2.因为Unity使用的是三阶伴随勒让德多项式,类似傅里叶变换后只取了几个低频部分,得到的结果自然是非常糊的一张图,丢失了太多高频信息。不过对于漫反射来讲,其实也已经够了。 我们可以非常方便的取出漫反射信息,只需要在片元着色器中加一句:
half3 ambient_contrib = ShadeSH9(float4(i.normal,1));//注意输入为WorldSpace的法线结果:  



间接光镜面反射

不同粗糙度的物体,反射的图像有的粗糙,有的清晰,难道我们需要很多张不同模糊程度的图输入进去吗?当然不需要,Unity帮我们处理好了cubemap的LOD,当物体越粗糙,就调用更高的LOD层级,这样内存占用只增加了百分之三十,但是效果非常好。  


需要注意的是,粗糙度与LOD层级并非线性关系,而是一条曲线,如下:  


代码如下:
float mip_roughness = perceptualRoughness * (1.7 - 0.7*perceptualRoughness );我们可以把视线方向取个负,然后根据法线把它镜像一下,去取cubemap.可以使用UNITY_SAMPLE_TEXCUBE_LOD函数。其中,unity_SpecCube0是unity内部维护的cubemap,我们可以直接取它,reflectVec就是视线方向的法线镜像,mip就是我们根据光滑度算出的mip层级。
half mip = mip_roughness * UNITY_SPECCUBE_LOD_STEPS;//得出mip层级。默认UNITY_SPECCUBE_LOD_STEPS=6(定义在UnityStandardConfig.cginc)
half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflectVec, mip);//视线方向的反射向量,去取样,同时考虑mip层级
half3 iblSpecular = DecodeHDR(rgbm, unity_SpecCube0_HDR);//使用DecodeHDR将颜色从HDR编码下解码。可以看到采样出的rgbm是一个4通道的值,
half surfaceReduction=1.0/(roughness*roughness+1.0);需要注意的是直接取出的cubemap由于是HDR格式,颜色会超过1,导致材质非常亮,需要DecodeHDR一下,看起来才是正常的。
最终加和

最终,我们需要把上面四部分的值加和在一起。 其中直接光部分:
//漫反射系数kd
float3 kd = OneMinusReflectivityFromMetallic(_Metallic);
kd*=Albedo;
float3 specular = D * G * F ;
float3 specColor = specular * lightColor*nl*UNITY_PI;//直接光镜面反射部分。镜面反射的系数就是F。漫反射之前少除π了,所以为了保证漫反射和镜面反射的比例,这里还得乘一个π
float3 diffColor = kd * rawDiffColor;//直接光漫反射部分。
float3 directLightResult = diffColor + specColor;间接光部分:
float3 iblDiffuseResult = iblDiffuse*kd;//乘间接光漫反射系数
half surfaceReduction=1.0/(roughness*roughness+1.0);
float oneMinusReflectivity = unity_ColorSpaceDielectricSpec.a-unity_ColorSpaceDielectricSpec.a*_Metallic;        //grazingTerm压暗非金属的边缘异常高亮
half grazingTerm=saturate(_Smoothness+(1-oneMinusReflectivity));
float3 iblSpecularResult = surfaceReduction*iblSpecular*FresnelLerp(F0,grazingTerm,nv);
float3 indirectResult = (iblDiffuseResult + iblSpecularResult)*occlusion;值得一提的是surfaceReduction项,这是一个拟合项,如果没有它间接光镜面反射在粗糙物体上可能会过亮,导致不真实,这个我倒是没有特别找到合适的解释,按照实现代码看,应该如此。顺便一提,最终的遮蔽也要都乘上去。
最终加和:
float3 finalResult = directLightResult + indirectResult;多光照

如果我们想让我们的Shader同时被多盏灯照亮,那么我们就需要加一个Add Pass,这样的话每加一盏灯,就执行一次Add Pass,结果采用加法(Blend One One)与Base Pass加在一起,最终就可以实现多盏灯同时照亮的效果。需要注意的是:
1.Add Pass的光照计算与Base Pass相同,不过只要计算直接光部分就可以了,不需要再计算间接光部分了,因为Base Pass已经算过了。
2.有时候我们摆了很多灯,却发现Add Pass只计算了其中几盏灯的影响,这时候我们可以选中灯,将其标记为“Important”,这样Unity就会将所有标记为Important的灯进行逐像素渲染,我们的材质也就可以接受到该灯的影响了。


结果

Unity的pbr实现是非常繁琐的,往往需要套娃一样地去层层检索,如果没有VS Code这个得力工具,将更难以阅读。最终我还是实现了基础的PBR shader,不过具体的实现我不再表述,因为我已经将函数的调用直接写出来了,所以大家不必像我一样层层套娃,只需要一次就可以看到公式的实现。可以看到,效果已经和Standard Shader没有任何区别了,即便是快速替换材质,也难以发现区别,说明基本还原了Standard Shader的实现。通过层层检索unity对pbr几张贴图的处理,我添加了贴图的输入,添加了多光源支持,确保它是一个实用的shader。此外,我还添加了lightProbe和reflection Probe以及烘焙lightmap的支持。 非金属材质表现:  


金属材质表现:  


多光照表现:


产生阴影与接收阴影。


注意:需要在build-in管线中将gama空间改为linear空间!
踩坑记录:
本次还原还是踩了不少坑,记录如下:
1.viewDir不可以放在顶点着色器计算,因为插值会出错。如下图,左侧为片元着色器计算的viewDir,右侧为顶点着色器计算后片元着色器自动插值的viewDir。可以看出,左侧视线直视的部分(绿色最强的区域)y值最大,所以变为绿色,而右侧顶点着色器只算对了顶点部分,中间直视的部分直接就插值了,所以出错了。


2.对于worldLightDir需要分情况计算,直接光直接取_WorldSpaceLightPos0.xyz,其他类型的光需要取normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz)
#ifdef USING_DIRECTIONAL_LIGHT
                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                #else
                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif代码部分:



Shader "Custom/myPBR"
{
Properties
    {
        _Tint("Tint",Color)=(1,1,1,1)
        _MainTex ("Texture", 2D) = "white" {}
//金属度要经过gama,否则即便是linear空间下渲染,unity也不会对一个滑条做操作的
        [Gamma]_Metallic("Metallic",Range(0,1))=0
        _MetallicGlossMap("Metallic", 2D) = "white" {}
        _Smoothness("Smoothness(Metallic.a)",Range(0,1))=0.5
        _BumpMap("Normal Map", 2D) = "bump" {}
        _Parallax ("Height Scale", Range (0.00, 0.08)) = 0.0
        _ParallaxMap ("Height Map", 2D) = "black" {}
        _OcclusionMap("Occlusion", 2D) = "white" {}

    }
SubShader
    {
Pass
        {
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
//Shader Model 3.0
            #pragma target 3.0
            #pragma multi_compile_fwdbase   
            #pragma vertex vert
            #pragma fragment frag
//添加lightmap支持
            #pragma multi_compile LIGHTMAP_OFF LIGHTMAP_ON
            #include "UnityStandardBRDF.cginc"
            #include "AutoLight.cginc"

struct a2v
            {
float4 vertex : POSITION;
float3 normal:NORMAL;
float2 uv : TEXCOORD0;
float2 uv1:TEXCOORD1;
fixed4 tangent : TANGENT;
            };

struct v2f
            {
float2 uv : TEXCOORD0;
                #ifndef LIGHTMAP_OFF
half2 uv1:TEXCOORD1;
                #endif
float4 pos : SV_POSITION;
float3 normal:TEXCOORD2;
float3 worldPos:TEXCOORD3;
float4 tangent:TEXCOORD4;
float3x3 tangentToWorld : TEXCOORD5;
float3 objectspaceViewdir:COLOR2;
float3x3 tangentMatrix: TEXCOORD8;
SHADOW_COORDS(11)
            };

sampler2D _MainTex;
float4 _Tint;
float _Metallic;
float _Smoothness;
float4 _MainTex_ST;
sampler2D _MetallicGlossMap;
sampler2D _BumpMap;
sampler2D _OcclusionMap;
float _Parallax;
sampler2D _ParallaxMap;

inline half OneMinusReflectivityFromMetallic(half metallic)
            {
// We'll need oneMinusReflectivity, so
//   1-reflectivity = 1-lerp(dielectricSpec, 1, metallic) = lerp(1-dielectricSpec, 0, metallic)
// store (1-dielectricSpec) in unity_ColorSpaceDielectricSpec.a, then
//   1-reflectivity = lerp(alpha, 0, metallic) = alpha + metallic*(0 - alpha) =
//                  = alpha - metallic * alpha
half oneMinusDielectricSpec = unity_ColorSpaceDielectricSpec.a;
return oneMinusDielectricSpec - metallic * oneMinusDielectricSpec;
            }

inline half3 DiffuseAndSpecularFromMetallic (half3 albedo, half metallic, out half3 specColor, out half oneMinusReflectivity)
            {
                specColor = lerp (unity_ColorSpaceDielectricSpec.rgb, albedo, metallic);
                oneMinusReflectivity = OneMinusReflectivityFromMetallic(metallic);
return albedo * oneMinusReflectivity;
            }
            v2f vert (a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.normal=UnityObjectToWorldNormal(v.normal);
                o.tangent=v.tangent;
float3 normalWorld = o.normal;
float4 tangentWorld = float4(UnityObjectToWorldDir(v.tangent.xyz), v.tangent.w);
// 对于奇怪的负缩放,我们需要sign取反(flip the sign)
half sign = tangentWorld.w * unity_WorldTransformParams.w;
half3 binormal = cross(normalWorld, tangentWorld) * sign;
float3x3 tangentToWorld = half3x3(tangentWorld.xyz, binormal, normalWorld);
                o.tangentToWorld=tangentToWorld;

//Parallax viewDir need to changed from ObjectSpace to Tangent
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(o.worldPos));
fixed3 objectspaceViewdir= mul(unity_WorldToObject, worldViewDir);
                o.objectspaceViewdir =normalize(objectspaceViewdir);
float3 objectSpaceBinormal = normalize(cross(v.normal,v.tangent.xyz) * v.tangent.w);
float3x3 tangentMatrix = float3x3(v.tangent.xyz, objectSpaceBinormal, v.normal);
                o.tangentMatrix = tangentMatrix;
                #ifndef LIGHTMAP_OFF
                o.uv1 = v.uv1.xy*unity_LightmapST.xy + unity_LightmapST.zw;
                #endif
// Pass shadow coordinates to pixel shader
TRANSFER_SHADOW(o);
return o;
            }
fixed4 frag (v2f i) : SV_Target
            {

half height = tex2D(_ParallaxMap, i.uv).g;
float3 tangentspaceViewDir =normalize( mul(i.tangentMatrix, i.objectspaceViewdir));
                i.uv += ParallaxOffset(height,_Parallax,tangentspaceViewDir);

                _Metallic=tex2D(_MetallicGlossMap,i.uv).r*_Metallic;
                _Smoothness=tex2D(_MetallicGlossMap,i.uv).a*_Smoothness;
float occlusion=tex2D(_OcclusionMap,i.uv).r;
float3 normal = normalize(i.normal);//没有加normalize操作,导致其还是取的顶点法线,没有进行插值
//  #ifdef _NORMALMAP
half3 tangent1 = i.tangentToWorld[0].xyz;
half3 binormal1 = i.tangentToWorld[1].xyz;
half3 normal1 = i.tangentToWorld[2].xyz;
float3 normalTangent =UnpackNormal(tex2D(_BumpMap,i.uv));
                normal=normalize((float3)(tangent1 * normalTangent.x + binormal1 * normalTangent.y + normal1 * normalTangent.z));
// #endif
                #ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                #else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
                #endif
float3 viewDir=normalize(UnityWorldSpaceViewDir(i.worldPos));
float3 lightColor = _LightColor0.rgb;
float3 halfVector = normalize(worldLightDir + viewDir);
//roughness相关
float perceptualRoughness = 1 - _Smoothness;
float roughness = perceptualRoughness * perceptualRoughness;
                roughness=max(roughness,0.002);//即便smoothness为1,也要有点高光在
float squareRoughness = roughness * roughness;
float nl = max(saturate(dot(normal , worldLightDir ) ) , 0.0000001);//防止除0
float nv = max(saturate(dot(normal, viewDir)), 0.0000001);
float vh = max(saturate(dot(viewDir, halfVector)), 0.0000001);
float lh = max(saturate(dot(worldLightDir, halfVector)), 0.0000001);
float nh = max(saturate(dot(normal, halfVector)), 0.0000001);

//1.1直接光漫反射部分.兰伯特光照。没有除以π,是因为会显得太暗。
float3 Albedo = _Tint*tex2D(_MainTex,i.uv);
float3 rawDiffColor = DisneyDiffuse(nv,nl,lh,perceptualRoughness)*nl*lightColor;

//1.2.直接光镜面反射部分
// 1.2.1 D项(GGX)
float D=GGXTerm(nh,roughness);
// 1.2.2 G项 几何函数,遮蔽变暗一些
//      直接光照和间接光照时的k都在逼近二分之一,只不过直接光照时这个值最小为八分之一而不是0。这是为了保证在表面绝对光滑时
//      也会吸收一部分光线,毕竟完全不吸收光线的物体在现实中不存在
float G=SmithJointGGXVisibilityTerm(nl,nv,roughness);
//1.2.3 F项 菲涅尔反射 金属反射强边缘反射强
float3 F0 = lerp(unity_ColorSpaceDielectricSpec.rgb, Albedo, _Metallic);
float3 F=FresnelTerm(F0,lh);
//漫反射系数kd
float3 kd = OneMinusReflectivityFromMetallic(_Metallic);
                kd*=Albedo;

float3 specular = D * G * F ;
float3 specColor = specular * lightColor*nl*UNITY_PI;//直接光镜面反射部分。镜面反射的系数就是F。漫反射之前少除π了,所以为了保证漫反射和镜面反射的比例,这里还得乘一个π
float3 diffColor = kd * rawDiffColor;//直接光漫反射部分。
float3 directLightResult = diffColor + specColor;
//至此,直接光部分结束

//2.开始间接光部分
//  2.1间接光漫反射
half3 iblDiffuse = ShadeSH9(float4(normal,1));
float3 iblDiffuseResult = iblDiffuse*kd;//乘间接光漫反射系数
//  2.2间接光镜面反射
float mip_roughness = perceptualRoughness * (1.7 - 0.7*perceptualRoughness );
float3 reflectVec = reflect(-viewDir, normal);
half mip = mip_roughness * UNITY_SPECCUBE_LOD_STEPS;//得出mip层级。默认UNITY_SPECCUBE_LOD_STEPS=6(定义在UnityStandardConfig.cginc)
half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflectVec, mip);//视线方向的反射向量,去取样,同时考虑mip层级
half3 iblSpecular = DecodeHDR(rgbm, unity_SpecCube0_HDR);//使用DecodeHDR将颜色从HDR编码下解码。可以看到采样出的rgbm是一个4通道的值,
half surfaceReduction=1.0/(roughness*roughness+1.0);
float oneMinusReflectivity = unity_ColorSpaceDielectricSpec.a-unity_ColorSpaceDielectricSpec.a*_Metallic;   //grazingTerm压暗非金属的边缘异常高亮
half grazingTerm=saturate(_Smoothness+(1-oneMinusReflectivity));
float3 iblSpecularResult = surfaceReduction*iblSpecular*FresnelLerp(F0,grazingTerm,nv);
float3 indirectResult = (iblDiffuseResult + iblSpecularResult)*occlusion;
//至此,结束间接光部分

//lightmap可以烘焙直接光漫反射、间接光漫反射,但是间接光镜面反射还是要实时的。
                #ifndef LIGHTMAP_OFF
fixed3 lm = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap,i.uv1));
float3 albedo = _Tint*tex2D(_MainTex,i.uv);
//漫反射系数kd
float3 kdLightmap = OneMinusReflectivityFromMetallic(_Metallic);
float3 finalRes = albedo * lm*kdLightmap+iblSpecularResult;
return float4( finalRes,1);
                #endif

//UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
fixed shadow= SHADOW_ATTENUATION(i);
//最终加和
float3 finalResult = directLightResult*shadow + indirectResult;


return float4(finalResult,1);
            }
ENDCG
        }
////////////////////////////////////////////////////////////////////////////////
//Additive forward pass (one light per pass)
//正向附加渲染通道 以每一个光照一个Pass的方式应用附加的逐像素光照
Pass
        {

//正向渲染附加通道
Tags { "LightMode" = "ForwardAdd" }
//混合方式 传一个参数控制
Blend One One
//附加通道中没有雾效
Fog { Color (0,0,0,0) } // in additive pass fog should be black
//关闭深度写入   
ZWrite Off
ZTest LEqual

//===========开启CG着色器语言编写模块===========
CGPROGRAM
            #pragma target 3.0
            #pragma multi_compile_fwdadd
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityStandardBRDF.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"
struct appdata
            {
float4 vertex : POSITION;
float3 normal:NORMAL;
float2 uv : TEXCOORD0;
float2 uv1:TEXCOORD1;
fixed4 tangent : TANGENT;
            };

struct v2f
            {
float2 uv : TEXCOORD0;
                #ifndef LIGHTMAP_OFF
half2 uv1:TEXCOORD1;
                #endif
float4 vertex : SV_POSITION;
float3 normal:TEXCOORD2;
float3 worldPos:TEXCOORD3;
float4 tangent:TEXCOORD4;
float3x3 tangentToWorld : TEXCOORD5;
float3 viewDir:COLOR1;
float3x3 tangentMatrix: TEXCOORD8;
float3 objectspaceViewdir:COLOR2;
            };

sampler2D _MainTex;
float4 _Tint;
float _Metallic;
float _Smoothness;
float4 _MainTex_ST;
sampler2D _MetallicGlossMap;
sampler2D _BumpMap;
sampler2D _OcclusionMap;
float _Parallax;
sampler2D _ParallaxMap;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                o.tangent=v.tangent;
float3 normalWorld = UnityObjectToWorldNormal(v.normal);
float4 tangentWorld = float4(UnityObjectToWorldDir(v.tangent.xyz), v.tangent.w);
// 对于奇怪的负缩放,我们需要sign取反(flip the sign)
half sign = tangentWorld.w * unity_WorldTransformParams.w;
half3 binormal = cross(normalWorld, tangentWorld) * sign;
float3x3 tangentToWorld = half3x3(tangentWorld.xyz, binormal, normalWorld);
                o.tangentToWorld=tangentToWorld;

//Parallax viewDir need to changed from ObjectSpace to Tangent
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(o.worldPos));
fixed3 objectspaceViewdir= mul(unity_WorldToObject, worldViewDir);
                o.objectspaceViewdir =normalize(objectspaceViewdir);
float3 objectSpaceBinormal = normalize(cross(v.normal,v.tangent.xyz) * v.tangent.w);
float3x3 tangentMatrix = float3x3(v.tangent.xyz, objectSpaceBinormal, v.normal);
                o.tangentMatrix = tangentMatrix;
                #ifndef LIGHTMAP_OFF
                o.uv1 = v.uv1.xy*unity_LightmapST.xy + unity_LightmapST.zw;
                #endif
return o;
            }
fixed4 frag (v2f i) : SV_Target
            {

half height = tex2D(_ParallaxMap, i.uv).g;
float3 tangentspaceViewDir =normalize( mul(i.tangentMatrix, i.objectspaceViewdir));
                i.uv += ParallaxOffset(height,_Parallax,tangentspaceViewDir);

                _Metallic=tex2D(_MetallicGlossMap,i.uv).r*_Metallic;
                _Smoothness=tex2D(_MetallicGlossMap,i.uv).a*_Smoothness;
float occlusion=tex2D(_OcclusionMap,i.uv).r;
float3 normal = normalize(i.normal);//没有加normalize操作,导致其还是取的顶点法线,没有进行插值
//  #ifdef _NORMALMAP
half3 tangent1 = i.tangentToWorld[0].xyz;
half3 binormal1 = i.tangentToWorld[1].xyz;
half3 normal1 = i.tangentToWorld[2].xyz;
float3 normalTangent =UnpackNormal(tex2D(_BumpMap,i.uv));
                normal=normalize((float3)(tangent1 * normalTangent.x + binormal1 * normalTangent.y + normal1 * normalTangent.z));
// #endif
    #ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                #else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
    #endif
float3 viewDir=normalize(UnityWorldSpaceViewDir(i.worldPos));
float3 lightColor = _LightColor0.rgb;
float3 halfVector = normalize(worldLightDir + viewDir);
//roughness相关
float perceptualRoughness = 1 - _Smoothness;
float roughness = perceptualRoughness * perceptualRoughness;
                roughness=max(roughness,0.002);//即便smoothness为1,也要有点高光在
float squareRoughness = roughness * roughness;
float nl = max(saturate(dot(normal , worldLightDir ) ) , 0.0000001);//防止除0
float nv = max(saturate(dot(normal, viewDir)), 0.0000001);
float vh = max(saturate(dot(viewDir, halfVector)), 0.0000001);
float lh = max(saturate(dot(worldLightDir, halfVector)), 0.0000001);
float nh = max(saturate(dot(normal, halfVector)), 0.0000001);

//light atten
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
    #if defined (POINT)
// 把点坐标转换到点光源的坐标空间中,_LightMatrix0由引擎代码计算后传递到shader中,这里包含了对点光源范围的计算,具体可参考Unity引擎源码。经过_LightMatrix0变换后,在点光源中心处lightCoord为(0, 0, 0),在点光源的范围边缘处lightCoord为1
float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
// 使用点到光源中心距离的平方dot(lightCoord, lightCoord)构成二维采样坐标,对衰减纹理_LightTexture0采样。_LightTexture0纹理具体长什么样可以看后面的内容
// UNITY_ATTEN_CHANNEL是衰减值所在的纹理通道,可以在内置的HLSLSupport.cginc文件中查看。一般PC和主机平台的话UNITY_ATTEN_CHANNEL是r通道,移动平台的话是a通道
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#elif defined (SPOT)
// 把点坐标转换到聚光灯的坐标空间中,_LightMatrix0由引擎代码计算后传递到shader中,这里面包含了对聚光灯的范围、角度的计算,具体可参考Unity引擎源码。经过_LightMatrix0变换后,在聚光灯光源中心处或聚光灯范围外的lightCoord为(0, 0, 0),在点光源的范围边缘处lightCoord模为1
float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1));
// 与点光源不同,由于聚光灯有更多的角度等要求,因此为了得到衰减值,除了需要对衰减纹理采样外,还需要对聚光灯的范围、张角和方向进行判断
// 此时衰减纹理存储到了_LightTextureB0中,这张纹理和点光源中的_LightTexture0是等价的
// 聚光灯的_LightTexture0存储的不再是基于距离的衰减纹理,而是一张基于张角范围的衰减纹理
fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).r * SHADOW_ATTENUATION(a);
    #else
fixed atten = 1.0;
    #endif
#endif
//light atten           
                lightColor*=atten;  
//1.1直接光漫反射部分.兰伯特光照。没有除以π,是因为会显得太暗。
float3 Albedo = _Tint*tex2D(_MainTex,i.uv);
float3 rawDiffColor = DisneyDiffuse(nv,nl,lh,perceptualRoughness)*nl*lightColor;

//1.2.直接光镜面反射部分
// 1.2.1 D项(GGX)
float D=GGXTerm(nh,roughness);
// 1.2.2 G项 几何函数,遮蔽变暗一些
//      直接光照和间接光照时的k都在逼近二分之一,只不过直接光照时这个值最小为八分之一而不是0。这是为了保证在表面绝对光滑时
//      也会吸收一部分光线,毕竟完全不吸收光线的物体在现实中不存在
float G=SmithJointGGXVisibilityTerm(nl,nv,roughness);
//1.2.3 F项 菲涅尔反射 金属反射强边缘反射强
float3 F0 = lerp(unity_ColorSpaceDielectricSpec.rgb, Albedo, _Metallic);
float3 F=FresnelTerm(F0,lh);
//漫反射系数kd
float3 kd = OneMinusReflectivityFromMetallic(_Metallic);
                kd*=Albedo;

float3 specular = D * G * F ;
float3 specColor = specular * lightColor*nl*UNITY_PI;//直接光镜面反射部分。镜面反射的系数就是F。漫反射之前少除π了,所以为了保证漫反射和镜面反射的比例,这里还得乘一个π
float3 diffColor = kd * rawDiffColor;//直接光漫反射部分。
float3 directLightResult = diffColor + specColor;
//至此,直接光部分结束

//最终加和
float3 finalResult = directLightResult ;
return float4(finalResult,1);
            }
ENDCG
        }
    }
FallBack"Diffuse"
}


参考资料:
1.https://zhuanlan.zhihu.com/p/68025039
2.https://zhuanlan.zhihu.com/p/53086060

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2021-11-26 09:08 | 显示全部楼层
试了下有两个问题:
1.直接光漫反射的暗部,相对Standard Shader更亮,不知道是不是没有除以PI的原因。
2.Mixed烘焙方式直接光照信息会丢失,应该也把直接光照加在烘焙后的finalRes中
  float3 finalRes = albedo * lm*kdLightmap+iblSpecularResult + directLightResult;
发表于 2021-11-26 09:10 | 显示全部楼层
1.我又检查了一下,没有发现更暗。另外standard shader也没有除以π。你可以详细截图给我吗?
2。Mixed烘焙方式只烘焙间接光照也是可以的,直接光还可以实时变。你可以发一下你这边的截图吗?
[干杯]
发表于 2021-11-26 09:18 | 显示全部楼层
你微信多少哈。我加下你[微笑]
发表于 2021-11-26 09:27 | 显示全部楼层
gq342491581
发表于 2021-11-26 09:34 | 显示全部楼层
float3 specular = D * G * F ;float3 specColor = specular * lightColor*nl*UNITY_PI;我看正常的计算直接高光的公式,是需要除以 (4 ×cos ×cos)
发表于 2021-11-26 09:41 | 显示全部楼层
我试了一下第二个问题,的确是直接光照的结果丢失了,确实需要像你这样加一下
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-10 06:33 , Processed in 0.107733 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表