【转载+修正】Shader学习 (18)PBR光照模型效果分析手动复现

548 阅读22分钟

原文链接

Shader学习 (18)PBR光照模型效果分析手动复现 | 九猫

原文写得很好,但是主题格式我不太喜欢,所以我对文章的格式进行 修整,并对部分错误的地方进行了 修正,方便自己日后阅读

正文

关于 PBR 光照模型的知识点,已经有很多大佬们做了详细的讲解,抽丝剥茧,讲解公式道理深,深入浅出,各种效果实践明。各位大佬珠玉在前,我这里讲解原理什么的只是班门弄斧,就只能以一种不会画画的作家绝对不是一个好程序员的心态来水一篇文章,我就以 PBR 材质的美术效果为切入点,来分析一下 PBR 材质中每个要素的视觉效果。

我这里想做的,就是一个字:“” 。我要把一个各种游戏中常见的 PBR 的基础效果拆分开来,来看一下 一个完整的 PBR 效果是由哪些更为基础的效果所构成的

这,是你的 PBR 材质。

image.png

它的 PBR 效果可以拆为两大部分,直接光照间接光照

在 直接光照 和 间接光照 的效果中,都能再分为 漫反射镜面反射 两种计算。

直接光照 就是上面右侧的黑白分明的效果,它赋予了材质亮暗分明的效果,它构成了材质的基础效果,表现了绘画中基础的亮灰暗面和高光。

在自然界中,光线从光源中发射出来,要在空间中经过非常多次的反射,在多个物体的表面被吸收和反弹,最终能量才会消散殆尽。那些自然界中的黑色物体就是因为物体表面对光线的能量的吸收能力太强所以才没有多少光线反射出来到人的眼睛里。这一切吸收和反射都是在瞬间完成的,因为光速太快了。

现在,想象一个光速非常慢的世界,大概每秒一米这样子。有纯黑的空间,没有任何一丝光线透进来,这时忽然开了一个洞口,光线涌了进来,离洞口最近的部分物体朝向光线的面最先被照亮,这时其他的部分都是黑的,所有的物体的背面都是纯黑。然后距离洞口从近到远的物体的朝向洞口的面都逐步亮了起来,这时整个空间中的视觉效果就像是上图中右边的直接光照的效果一样,黑白分明,当然需要注意的是,物体的亮面是有颜色的,不是像上图中我放的灰色物体一样。随着光线从物体表面反射出去,接触到了其他物体的背面,其他物体的背面也亮了起来,这时物体的背面还会带上光线第一次反射时上一个物体的颜色。随着时间越来越长,洞中的光线越来越充足,而物体的各种反射效果也越来越清晰,最后就像是我们现实世界中的效果一样了。

上面说的,其实就是一些传统渲染器的思路,在空间中释放很多射线,然后一步一步计算光照和反射。

  • 直接光照 计算的是 灯光在第一次接触到物体的时候产生的计算结果
  • 间接光照 计算的就是 光线从物体上被反射出去之后的效果

有过使用 Arnold 渲染器经验的同学应该了解 Arnold 渲染器中有反射次数的设置(采样数),这其实就是设置光线在空间中的反射次数,如果反射的次数设置很低的话,在场景中放置两个镜子材质的球体,渲染后的结果就是球体上反射到的另一个球体是黑色的。

当然传统渲染器中可以一步一步计算光线在空间中的反射,而在实时渲染中没有这么多时间来计算反射,所以一般都是把 间接光照 的信息储存在 CubeMap 中,等到渲染时直接读取,这是一种用空间换时间的方式。

以前的游戏中,很多都只是使用 Lambert 或者 Blinn 光照模型,然后 Shader 中只使用颜色贴图,并没有间接光照的计算。只看 PBR 的直接光照的效果的话,其实也不会好很多。

接下来可以再分析一下直接光的效果由哪些更基础的效果组成。

直接光照

如果使用一张图表来说明的话,大概是这样的。

1674669231190.jpg

先说一下 漫反射,漫反射部分单独拿出来就是很常见的 Lambert 模型,效果大概是这样的:

  • 一个简单的从亮到暗的过渡 image.png

再说 直接光照镜面反射 ,先看一下镜面反射的效果。

7b5765a2-9d36-11eb-8d0e-8ed36bc4c96d 00_00_00-00_00_30.gif

直接光照镜面反射 效果又可以拆分为几个更基础的效果:

  1. 镜面高光 (D)(更严格应该叫 法线分布函数
  2. 几何遮蔽 (G)
  3. 菲涅尔 (F)

镜面高光

82af2498-9d36-11eb-8e32-f690e416edc3 00_00_00-00_00_30.gif

镜面高光在光滑的物体表面出现,当物体越粗糙时,镜面高光的颜色就更加平均,越光滑时,就越倾向于变成一个高光点。这符合我们平时对物体的观察结果,物体的表面越光滑,就越是会在高光部分出现一个很亮的亮点。

D 其实为法线分布函数, 它描述了有多少光会反射到观察方向上,因为是镜面反射,所以我们只考虑那些能够直接反射到我们眼睛的光,使用的是基于 GGX 的公式

几何遮蔽

7b5765a2-9d36-11eb-8d0e-8ed36bc4c96d 00_00_00-00_00_30.gif

image.png

几何遮蔽在运算中会和第一个镜面高光相乘,它的作用是 限制高光光点出现的位置,毕竟高光点不应该出现在背光的面。

上面图中显示,物体越是粗糙,物体边缘处越黑,这样的话,高光就不会在边缘出现。平时如果观察一下平时物体也可以看到,光滑物体可以出现高光的范围更大。

G 为阴影遮掩函数, 它描述了有多少光不会被遮挡,有些光即使满足了上面的所有条件,但还有可能会被遮挡,所以我们也必须将这些会被遮挡的光也去除掉,一般会和 镜面反射 的分母 (n·l)(n·v) 进行结合,称为可见性项,这次使用的公式是 Smith-Joint

菲涅尔

image.png

这里的菲涅尔其实并不是我们平时理解的那种视角向量和法线向量点积产生的菲涅尔,其实我也不太清楚为什么这里会叫做菲涅尔。

F 为菲涅尔反射, 它描述了会有多少光来参与镜面反射,就像我们前面所说的只有一部分光会被镜面反射,菲涅尔项就是来计算这个比率的

这个效果是由 金属度 参数来控制的,上图中金属度为 1 时就完全是我给的材质颜色。

它的作用是和 直接光照镜面反射 相乘,当金属度越强时,反射越强烈,而且反射的光线会带有金属的本身的颜色。当金属度越低时,反射越弱,反射的颜色就越倾向于环境本身的颜色。

区分金属和非金属特性的一个 重要的视觉差异 就是看反光是否带有物体本身的颜色,金属的反光会带有金属本身的颜色,而非金属物体的反光并不会带有物体颜色。很多游戏做得塑料感十足就是在这一点上没有做到位。

到这里,作为 直接光照镜面反射被除数部分 已经分开来看过效果了。再看一下它们相乘之后是什么效果:

image.png

看起来就像是一个颜色很暗淡的 Blinn 光照模型,效果平平无奇。

接下来再看作为除数的 : 4(viewDirnormalDir)(lightDirnormalDir)4(viewDir \cdot normalDir)(lightDir \cdot normalDir)

这是把这个效果单独显示出来的样子。

7b5765a2-9d36-11eb-8d0e-8ed36bc4c96d 00_00_00-00_00_30.gif

这个效果其实和上面的几何遮蔽有些相似,但是为什么要把它作为除数我却没有找到很科学的解释。它的作用就是 材质的光照结果进行柔和化处理,让非常亮的部分变暗,让非常暗的部分变亮。

这是被除数部分单独的显示结果:

这是除过 4(viewDirnormalDir)(lightDirnormalDir)4(viewDir \cdot normalDir)(lightDir \cdot normalDir) 的显示结果:

至此,镜面反射的效果就展示出来了。

总结一下

在直接光照中,粗糙度的作用是 控制高光点的扩散范围,金属度的作用是 控制高光是否带有材质本身的颜色,非金属的物体反射的高光是白色的,而金属物体会带有物体本身的颜色。

再看一下 镜面反射 和 漫反射 结合的效果:

7b5765a2-9d36-11eb-8d0e-8ed36bc4c96d 00_00_00-00_00_30.gif

间接光照

PBR 模型中,如果只是看直接光照的效果,效果实在是普通,只是单独把它拿出来并没有比以前的 Blinn 材质效果好多少,PBR 材质的直接光部分 最主要的一点提升 就是确保了物体反射出来的光线不会比光源的光线更强,以保证材质的光照效果符合物理。直接光照并没有让 PBR 的视觉效果脱颖而出,那么是什么让它如此出色?是间接光照!出色的间接光照 就是让 PBR 材质的效果与众不同以至于成为业界标杆的原因。

间接光照可以分为 漫反射镜面反射

  • 其中漫反射的间接光使用 球谐光照 来实现,【应用在 光照探针 上】
  • 而镜面反射的间接光照由 CubeMap 来实现。

上图中,左边是球谐光照右边是 CubeMap。

间接光照的漫反射

球谐光照的的作用是 为非金属的材质添加间接光照,它只受到金属度这一个参数的影响,当物体是金属时,不受到漫反射间接光照(球谐光照)的影响。

这是仅把球谐光照显示出来的样子,当金属度为 1 时它变黑说明金属物体不受到球谐光照影响

它的效果就像是石膏体受到周边台灯的照射一样,只能体现出光源的方向、颜色和强度,但是不会产生任何的高光。你可以把它的效果想象为场景中所有光源对物体进行了一次兰伯特运算并且累加之后的结果。

球谐光照对场景中所有光源进行球面积分而计算出来的【对光照的一种简化,简单还原一个球面光照的过程。其原理类似傅里叶变换,不同的是这次是作用在 球坐标系 上,通过彼此 正交的球谐基 和对应的系数相乘来达到还原 球面方程 的目的。】,这种计算方式称为球谐函数。说实话这部分的计算太过于硬核,搞懂是不可能搞懂的,如果想要研究这个部分,那就需要再另开一篇文章了。

间接光照的镜面反射

间接光照的镜面反射是采样了一张立方体贴图(CubeMap),这张立方体贴图把周边的环境记录下来,然后再作为反射信息显示到物体的表面。

CubeMap 就是一张全景 HDR 图,这种图片在谷歌地图的全景导航里和 YouTube 上面的全景视频都有很成熟的应用,可以直接去了解一下。关于 PBR 的视觉效果为什么这么出色的原因,一大半都归结于由 CubeMap 记录的镜面反射信息,在材质上面显示如此逼真的镜面反射效果直接让 PBR 材质的细节丰富程度提升了几个档次,那些传统的实时渲染当中的只能显示明暗和高光的材质无论如何也无法达到如此精致的视觉效果。

CubeMap 在制作或者说拍摄的时候,有 球面映射 的和 立方体映射 的,在导入 Unity 或者 UE4 的时候,引擎都可以识别到图片并且正确映射,现在在各种网站上找到的全景 HDR 图中都是球面映射的比较多。

镜面反射的显示受到两个参数的影响:MetallicRoughness

Metallic 数值越高,镜面反射会带有物体本身的颜色。金属物体反射的光线会带有金属本身的颜色,而非金属物体不论本身是什么颜色,它们反射的光线都是光线原本的颜色。Metallic 也会影响到镜面反射的强度,并不仅仅影响颜色,间接光 的镜面反射和 直接光照 部分的 F 菲尼尔项是有关联的,金属物体的镜面反射的强度更高。

贴图的 MipMap 技术在各个游戏和影视中的应用非常广泛,简单来说就是把一张贴图缩小为多个等级的更小分辨率的图片保存下来,游戏运行时根据物体和摄像机的距离读取不同分辨率的图片。

Roughness 控制 CubeMap 的 Mip 等级,Mip 等级越高,贴图就越模糊。当仅显示出来调整贴图 Mip 等级对镜面反射的影响时,效果如下:

与此同时 Roughness 还会影响到镜面反射的强度,越粗糙的物体反射的光线越少,而越光滑的物体镜面反射就越强。当仅显示粗糙度对镜面反射的强度的影响,并且排除粗糙度对 Mip 等级的影响时,效果如下:

最后来总结一下间接光照

image.png

效果总结

  1. 纯金属材质是没有漫反射的,只受到 镜面反射的影响。
  2. 非金属材质反射的光线是光线原本的颜色,而金属材质反射的光线带有材质自身的颜色。
  3. 在相同粗糙度的时候,金属材质颜色更暗一些,金属吸收的能量更多,自然界中放在太阳下的金属物体相对更容易被晒热就是这个原理;
  4. 金属度 的作用:
    • 控制 漫反射 和 镜面反射 的比例;
    • 控制反射颜色和材质本身颜色相乘;
  5. 粗糙度 的作用:
    • 控制 直接光照 高光点的扩散;
    • 控制 CubeMap 的模糊程度;
    • 控制镜面反射颜色的强度;

在 Unity 中实现:

想要把 PBR 中的每个要素拆分出来逐个分析,就必须首先把它本身实现出来,要实现一个 PBR 效果,有一件事情不得不面对,那就是 BRDF 公式。说实话公式这种东西并不是这个文章的重点,重点是效果,我们需要知道,一个最终的 PBR 效果是由哪些基础的效果组成的,这些基础的效果又对应公式里的哪些符号。所以这里我们不需要去理解这个公式为什么是这个公式,只需要知道这个公式里面有哪些要素,每个要素的视觉效果是怎么样的,各个要素之间是什么关系

BRDF 描述了 光在该点是如何分布的, 也就是说当一个光照向一个物体时,光是如何反射到我们眼睛的。

光在照向物体时会有一部分被镜面反射,有一部分会被漫反射 image.png 如上图所示,褐色为光的 入射方向,橘黄色为 镜面反射,蓝色为 漫反射。在左图中,我们可以看到当像素大小小于漫反射的散射距离时,这些散射光会从别的像素点射出,这被称为 次表面散射。在右图中,当像素大小大于漫反射的散射距离时,这些散射我们可以看做都是从这个像素点射出去的, 而 BRDF 就是用来描述它的。

关于那些公式是什么来由,怎么推导的,有什么科学道理,这里就不去细说了。我只是把 Shader 按照大佬们已经总结好的公式做出来就好了。

以上公式中

  • Lo(p,ωo) 代表的是最终的输出颜色。具体地说 p 代表物体表面的一个点 p;而 ω 涉及到了一些 立体角 相关的概念,这里可以简单理解为它是 一个方向L 代表的是 辐照度,可以简单理解为 某个点上接收到的所有光线的强度总和。因此这个输出结果 Lo 就是在物体表面某一点 p 的位置,从 ωo 方向观察到的所有光线的总和,自然而然,这个 ωo 方向代表的就是 我们眼睛(摄像机)的观察方向
  • 上面的 ∫Ω......dωi 这个符号,代表的是 半球积分,用来做 多光源下 的光照结果的叠加。
  • 这里的 kdks 代表的是 漫反射 比例和 镜面反射 的比例,它们代表在最终的光照输出结果中,漫反射效果以及镜面反射效果对最终效果的影响。
  • 这里的 c 代表 物体表面的纹理颜色,而这个颜色在计算的时候需要先除以 π 使颜色变暗。
  • 公式中的 DFG4(won)(win)\frac{DFG}{4(w_{o} \cdot n)(w_{i} \cdot n)} 分开来解读,分子上的 DGF 三个字母代表了三个公式,分别是 D 法线分布函数(镜面高光),G 几何函数(几何遮蔽),F 菲涅尔系数(菲涅尔效应)。分母的 ωo 代表的是视角方向(viewDir),还有就是 ωi 代表的是光线的入射方向(LightDir),n 代表的是物体表面的法线方向。
  • 接下来的 Li(p,ωi) 可以根据上文中的 L 的解释来理解,代表的是物体表面某一点 p 的位置上从 ωi 方向上入射光线的强度。也可以理解为 光源的颜色
  • 然后 n⋅ωi 代表的是表面法线和入射光的点积结果,也就是 Lambert 效果。

解释过这个公式里各个符号的意义之后,再回顾开始时的图表。

1674669231190.jpg

首先把一些需要用到的数据准备好 ,搭一个 基础的框架

 Shader "Custom/SelfPBR"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Roughness ("Roughness", Range(0,1)) = 0.5
        //Gamma矫正金属度变化
        [Gamma]_Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200
        Pass
	{
	    Tags {
		 "LightMode" = "ForwardBase"
	    }
            CGPROGRAM

            #pragma target 3.0

            #pragma vertex vert
            #pragma fragment frag

            #include "UnityStandardBRDF.cginc" 

            float4 _Color;
            float _Metallic;
            float _Roughness;
            sampler2D _MainTex;
            float4 _MainTex_ST;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
            };

            v2f vert(a2v 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.normal = normalize(o.normal);
                return o;
            }


            fixed4 frag(v2f i) : SV_Target
            {
                //**********准备数据************
                i.normal = normalize(i.normal);
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                float3 lightColor = _LightColor0.rgb;
                float3 halfVector = normalize(lightDir + viewDir);      //半角向量

                float roughness = _Roughness * _Roughness;
                float squareRoughness = roughness * roughness;

                float3 Albedo = _Color.rgb * tex2D(_MainTex, i.uv);     //颜色

                //对每个数据做限制,防止除0
                float nl = max(saturate(dot(i.normal, lightDir)), 0.000001);
                float nv = max(saturate(dot(i.normal, viewDir)), 0.000001);
                float vh = max(saturate(dot(viewDir, halfVector)), 0.000001);
                float lh = max(saturate(dot(lightDir, halfVector)), 0.000001);
                float nh = max(saturate(dot(i.normal, halfVector)), 0.000001);
                //**********分割************

                return fixed4(1,1,1,1);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

直接光照 + 镜面反射

接下来就先实现一下 直接光照镜面反射 部分,直接光的镜面反射主要是由三个部分组成:

  • D 项法线分布函数、
  • G 项几何函数、
  • F 项菲涅尔函数。

先说 法线分布函数

image.png

这个式子中 α 代表表面的粗糙度。

 //法线分布函数D
float Distribution(float roughness , float nh)
{
    float lerpSquareRoughness = pow(lerp(0.002, 1, roughness), 2);
    float D = lerpSquareRoughness / (pow((pow(nh, 2) * (lerpSquareRoughness - 1) + 1), 2) * UNITY_PI);
    return D;
}

几何函数:

image.png

 //几何遮蔽G
float Geometry(float roughness , float nl , float nv)
{
    float kInDirectLight = pow(roughness + 1, 2) / 8;
    float kInIBL = pow(roughness, 2) / 8;
    float GLeft = nl / lerp(nl, 1, kInDirectLight);
    float GRight = nv / lerp(nv, 1, kInDirectLight);
    float G = GLeft * GRight;
    return G;
}

菲涅尔函数:

image.png

这里 F0 的计算可以看下一段。

 //菲尼尔Fresnel
float3 FresnelEquation(float3 F0 , float vh)
{
    float3 F = F0 + (1 - F0) * exp2((-5.55473 * vh - 6.98316) * vh);
    return F;
}

准备好了上面的三项之后,就可以 直接光镜面反射 的结果计算出来了。

这里计算出的高光点数值是大于 1 的,如果没有打开后处理的 泛光 效果的话,也看不出来,但是如果打开之后,就会发现效果不对。我这里强行把它限制到了 0 到 1 的区间了。

 //********直接光照-镜面反射部分*********
float D = Distribution(roughness , nh);
float G = Geometry(roughness , nl , nv);
//unity_ColorSpaceDielectricSpec是一个Unity常量,大概为float3(0.04) ,但是直接输出后显示的效果是float3(0.22) ,这个应该是gamma空间导致的
float3 F0 = lerp(unity_ColorSpaceDielectricSpec.rgb, Albedo, _Metallic);
float3 F = FresnelEquation(F0 , vh);

float3 SpecularResult = (D * G * F) / (nv * nl * 4);
float3 specColor = SpecularResult * lightColor * nl * UNITY_PI;
specColor = saturate(specColor);
//********直接光照-镜面反射部分完成*********    

直接光照 + 漫反射

再接下来是 直接光照漫反射部分

漫反射部分其实就是一个 Lambert 效果,这里还会对金属颜色产生关联。

//********直接光照-漫反射部分*********
float3 kd = (1 - F)*(1 - _Metallic);
float3 diffColor = kd * Albedo * lightColor * nl;
//********直接光照-漫反射部分完成*********

间接光照 + 镜面反射

间接光的镜面反射采样的是一张 立方体贴图,所以首先计算采样贴图相关的数据。

这是 Mip 等级的计算方法:

//立方体贴图的Mip等级计算
float CubeMapMip(float _Roughness)
{
    //基于粗糙度计算CubeMap的Mip等级
    float mip_roughness = _Roughness * (1.7 - 0.7 * _Roughness);
    half mip = mip_roughness * UNITY_SPECCUBE_LOD_STEPS; 
    return mip;
}

Unity_ColorSpaceDielectricSpec.a 的数值大概是 0.75 ,我其实并不太理解这里为什么是这个数字。

//***********间接光照-镜面反射部分********* 
half mip = CubeMapMip(_Roughness);                              //计算Mip等级,用于采样CubeMap
float3 reflectVec = reflect(-viewDir, i.normal);                //计算反射向量,用于采样CubeMap

half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflectVec, mip);
float3 iblSpecular = DecodeHDR(rgbm, unity_SpecCube0_HDR);      //采样CubeMap之后,储存在四维向量rgbm中,然后在使用函数DecodeHDR解码到rgb

half surfaceReduction=1.0/(roughness*roughness+1.0);            //压暗非金属的反射

float oneMinusReflectivity = unity_ColorSpaceDielectricSpec.a-unity_ColorSpaceDielectricSpec.a*_Metallic;
half grazingTerm=saturate((1 - _Roughness)+(1-oneMinusReflectivity));
half t = Pow5(1-nv);
float3 FresnelLerp =  lerp(F0,grazingTerm,t);                   //控制反射的菲涅尔和金属色

float3 iblSpecularResult = surfaceReduction*iblSpecular*FresnelLerp;
//***********间接光照-镜面反射部分完成********* 

间接光照 + 漫反射

这里其实只要获取 球谐光照 的结果,然后再使用 菲涅尔 对边缘进行处理就好了。

//***********间接光照-漫反射部分********* 
half3 iblDiffuse = ShadeSH9(float4(normal,1));                  //获取球谐光照

float3 Flast = fresnelSchlickRoughness(max(nv, 0.0), F0, roughness);
float kdLast = (1 - Flast) * (1 - _Metallic);                   //压暗边缘,边缘处应当有更多的镜面反射

float3 iblDiffuseResult = iblDiffuse * kdLast * Albedo;
//***********间接光照-漫反射部分完成********* 
 
//间接光的菲涅尔系数
float3 fresnelSchlickRoughness(float cosTheta, float3 F0, float roughness)
{
    return F0 + (max(float3(1 ,1, 1) * (1 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
}

接下来就是完整代码

image.png

 Shader "Custom/SelfPBR"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Roughness ("Roughness", Range(0,1)) = 0.5
        //Gamma矫正金属度变化 ,这个矫正是否有必要?做截图对比
        [Gamma]_Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200
        Pass
		{
			Tags {
				"LightMode" = "ForwardBase"
			}
            CGPROGRAM

            #pragma target 3.0

            #pragma vertex vert
            #pragma fragment frag

            #include "UnityStandardBRDF.cginc" 

            float4 _Color;
            float _Metallic;
            float _Roughness;
            sampler2D _MainTex;
            float4 _MainTex_ST;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
            };

            v2f vert(a2v 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.normal = normalize(o.normal);
                return o;
            }

            //法线分布函数D
            float Distribution(float roughness , float nh)
            {
                float lerpSquareRoughness = pow(lerp(0.002, 1, roughness), 2);
                float D = lerpSquareRoughness / (pow((pow(nh, 2) * (lerpSquareRoughness - 1) + 1), 2) * UNITY_PI);
                return D;
            }

            //几何遮蔽G
            float Geometry(float roughness , float nl , float nv)
            {
                float kInDirectLight = pow(roughness + 1, 2) / 8;
                float kInIBL = pow(roughness, 2) / 8;
                float GLeft = nl / lerp(nl, 1, kInDirectLight);
                float GRight = nv / lerp(nv, 1, kInDirectLight);
                float G = GLeft * GRight;
                return G;
            }

            //菲尼尔Fresnel
            float3 FresnelEquation(float3 F0 , float vh)
            {
                float3 F = F0 + (1 - F0) * exp2((-5.55473 * vh - 6.98316) * vh);
                return F;
            }

            //立方体贴图的Mip等级计算
            float CubeMapMip(float _Roughness)
            {
                //基于粗糙度计算CubeMap的Mip等级
                float mip_roughness = _Roughness * (1.7 - 0.7 * _Roughness);
                half mip = mip_roughness * UNITY_SPECCUBE_LOD_STEPS; 
                return mip;
            }

            //间接光的菲涅尔系数
            float3 fresnelSchlickRoughness(float cosTheta, float3 F0, float roughness)
            {
                return F0 + (max(float3(1 ,1, 1) * (1 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
            }

            fixed4 frag(v2f i) : SV_Target
            {
                //**********准备数据************
                float3 normal = normalize(i.normal);
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                float3 lightColor = _LightColor0.rgb;
                float3 halfVector = normalize(lightDir + viewDir);      //半角向量

                float roughness = _Roughness * _Roughness;
                float squareRoughness = roughness * roughness;

                float3 Albedo = _Color.rgb * tex2D(_MainTex, i.uv);     //颜色

                //对每个数据做限制,防止除0
                float nl = max(saturate(dot(i.normal, lightDir)), 0.000001);
                float nv = max(saturate(dot(i.normal, viewDir)), 0.000001);
                float vh = max(saturate(dot(viewDir, halfVector)), 0.000001);
                float lh = max(saturate(dot(lightDir, halfVector)), 0.000001);
                float nh = max(saturate(dot(i.normal, halfVector)), 0.000001);
                //**********分割************

                //********直接光照-镜面反射部分*********
                float D = Distribution(roughness , nh);
                float G = Geometry(roughness , nl , nv);
                //unity_ColorSpaceDielectricSpec是一个Unity常量,大概为float3(0.04) ,但是直接输出后显示的效果是float3(0.22) ,这个应该是gamma空间导致的
                float3 F0 = lerp(unity_ColorSpaceDielectricSpec.rgb, Albedo, _Metallic);
                float3 F = FresnelEquation(F0 , vh);

                float3 SpecularResult = (D * G * F) / (nv * nl * 4);
                float3 specColor = SpecularResult * lightColor * nl * UNITY_PI;
                specColor = saturate(specColor);
                //********直接光照-镜面反射部分完成*********    

                //********直接光照-漫反射部分*********
                float3 kd = (1 - F)*(1 - _Metallic);
                float3 diffColor = kd * Albedo * lightColor * nl;
                //********直接光照-漫反射部分完成*********

                float3 directLightResult = diffColor + specColor;   //直接光照部分结果
                //********直接光照部分完成*********

                //***********间接光照-镜面反射部分********* 
                half mip = CubeMapMip(_Roughness);                              //计算Mip等级,用于采样CubeMap
                float3 reflectVec = reflect(-viewDir, i.normal);                //计算反射向量,用于采样CubeMap

                half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflectVec, mip);
                float3 iblSpecular = DecodeHDR(rgbm, unity_SpecCube0_HDR);      //采样CubeMap之后,储存在四维向量rgbm中,然后在使用函数DecodeHDR解码到rgb

                half surfaceReduction=1.0/(roughness*roughness+1.0);            //压暗非金属的反射

                float oneMinusReflectivity = unity_ColorSpaceDielectricSpec.a-unity_ColorSpaceDielectricSpec.a*_Metallic;
                half grazingTerm=saturate((1 - _Roughness)+(1-oneMinusReflectivity));
                half t = Pow5(1-nv);
                float3 FresnelLerp =  lerp(F0,grazingTerm,t);                   //控制反射的菲涅尔和金属色

                float3 iblSpecularResult = surfaceReduction*iblSpecular*FresnelLerp;
                //***********间接光照-镜面反射部分完成********* 

                //***********间接光照-漫反射部分********* 
                half3 iblDiffuse = ShadeSH9(float4(normal,1));                  //获取球谐光照

                float3 Flast = fresnelSchlickRoughness(max(nv, 0.0), F0, roughness);
                float kdLast = (1 - Flast) * (1 - _Metallic);                   //压暗边缘,边缘处应当有更多的镜面反射

                float3 iblDiffuseResult = iblDiffuse * kdLast * Albedo;
                //***********间接光照-漫反射部分完成********* 
                float3 indirectResult = iblSpecularResult + iblDiffuseResult;
                //***********间接光照完成********* 

                float3 finalResult = directLightResult + indirectResult;

                return fixed4(finalResult,1);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"

在 UE4 中手动实现:

【请移步原文

参考文章