前言

之前更新过两篇光照模型基础之LambertPhong与Blinn-Phong,实际上在1973年裴祥风(Bui Tuong Phong)提出的标准光照模型中,把进入摄像机的光线分为四部分,分别是自发光(emissive)、高光反射(specular)、漫反射(difuse)、环境光(ambient)。

其中Lambert就是用来模拟漫反射,而Phong或者Blinn-Phong则是用来模拟高光反射,只是标准光照模型的一部分,而自发光可以通过简单的叠加发光颜色以及发光亮度实现,因此本文就介绍一下最后一个环境光照的原理及算法。

 

环境光照介绍

漫反射和高光反射都是用来模拟光线直接照射物体表面时的反射效果,在现实世界中光线通常会在多个物体之间反射,最后进入摄像机,也就是所谓的间接光照(indirect light)。

在室内的红地毯上放一个灰色的沙发,沙发底部也会泛红色,原因就是红色地毯反射的光线照亮了沙发底部,这就是环境光照中的漫反射,这种现象一般称为色彩溢出(Color Bleeding)。

还有一种则是环境光照中的镜面反射,例如光滑金属球表面反射出周围的环境。

在影视制作的离线渲染领域中,环境光照的计算通常是采用光线追踪方式,通过发射一定量的光线,追踪并计算光线多次反射的路径以及反射面的颜色,综合计算后得到最终的表面色彩。

这种算法虽然效果很真实,但是会耗费大量性能,不过在离线渲染领域,渲染质量永远是比速度更为重要的,只要预算足够,一帧渲染几天也是可以接受的。

而在游戏领域,速度则是最为重要的,追求的是在大多数硬件上能够达到预定的效率(比如60fps),在这个前提下再追求更高的渲染质量。

因此长期以来,游戏开发中都是通过各种低消耗算法模拟环境光照或者通过预计算的全局光照实现环境光照,当然随着技术的发展和硬件性能的提升,现在最新的硬件以及游戏引擎已经支持光线追踪,最新的次世代游戏已经可以开启光线追踪模式享受更好的画面质量。

但新技术与硬件的普及还需要一定时间,当前更多的游戏物体依然大量使用模拟环境光照以及预计算全局光照的方式制作。

下面就简单介绍一下模拟环境光照的几种方式。

 

多色环境光

这种方式用来模拟环境光照的漫反射颜色,即将物体分为不同部分,叠加上不同的颜色,例如下面这样通过法线方向将物体分为上部、侧面以及底部三部分,分别叠加不同的环境反射颜色。

UE4中同理

原理很简单,shader代码就不放了。

另外环境反射颜色可以有任意个,渐变方向也可以用其他方式定义或旋转偏移,可以制作出不错的效果。

 

菲涅尔反射

观察一下下图的水面,可以发现近处能看到水底而远处却反射了天空。

这意味着物体的反射程度和观察的角度有关,视线越平行于物体表面,反射越强,视线越垂直于物体表面,反射越弱。因此,当观察一个圆球时,圆球中心反射最弱而边缘反射最强。

这就是菲涅尔反射(fresnel)。

这种现象在现实世界中的所有物体上都存在,只是在光滑的金属表面这类物体上不明显,而影响菲涅尔反射的则是物体本身的折射率,这个就涉及到PBR的原理了,以后会详细介绍。

在引擎中模拟菲涅尔反射也是用了同样的原理,取物体的世界空间法线和观察方向做点积,可以理解为从视角方向发射光线的Lambert模型,只不过明暗关系是相反的。

在Unity中实现如下:

在UE4中同样:

其中Power节点用于控制菲涅尔反射的范围。

其实图上可以看到,在这两个引擎中已经准备好了fresnel的节点可以直接调用,这里只是为了介绍原理因此使用向量点积的方式进行了效果实现。

在Unity中的shader代码如下:

// shader的路径和名称
Shader "Shader Forge/fresnel" {
    // 暴露属性到面板
    Properties {
        _FresnelPow("Fresnel Pow", Range(0, 10)) = 1
    }
    SubShader {
        Tags {
            "RenderType"="Opaque"
        }
        Pass {
            Name "FORWARD"
            Tags {
                "LightMode"="ForwardBase"
            }
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma multi_compile_fwdbase_fullshadows
            #pragma target 3.0
            
            // 声明变量
            uniform float _FresnelPow;
            
            // 输入结构
            struct VertexInput {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };
            
            // 输出结构
            struct VertexOutput {
                float4 pos : SV_POSITION;
                float3 posWS : TEXCOORD0;
                float3 nDirWS : TEXCOORD1;
            };
            
            // 顶点shader
            VertexOutput vert (VertexInput v) {
                VertexOutput o = (VertexOutput)0;
                o.pos = UnityObjectToClipPos( v.vertex );
                o.posWS = mul(unity_ObjectToWorld, v.vertex);
                o.nDirWS = UnityObjectToWorldNormal(v.normal);
                return o;
            }
            
            // 像素shader
            float4 frag(VertexOutput i) : COLOR {
                // 计算Fresnel
                float3 nDirWS = i.nDirWS;
                float3 vDirWS = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz);  
                float vDotn = dot(vDirWS, nDirWS);
                
                // 光照模型
                float fresnel = pow(max(0.0, 1.0 - vDotn), _FresnelPow);
   
                return float4(fresnel, fresnel, fresnel, 1.0);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

 

环境反射之Matcap

模拟镜面反射的最廉价手段就是Matcap,原理是将球形的环境贴图从摄像机的观察视角投射到物体上,这也是Zbrush雕刻软件中常用的计算方法。

例如下面图片中的球形贴图即为Matcap贴图。

这种方式的优点是效率高,缺点是反射是固定的,侧面边缘的反射经常会变形,移动观察视角时很容易穿帮,但在固定视角的游戏中可以适当应用。

具体计算方法是通过物体的观察空间法线取出模型面对摄像机的区域,作为UV对Matcap贴图进行采样,最后叠加上菲涅尔即可得到金属或非金属物体的环境反射。

在Unity中实现如下:

其中使用了法线贴图代替物体的点法线,但法线贴图中的法线是物体空间nDirOS,因此需要使用Transform节点将物体空间转为观察空间的nDirVS。而fresnel则需要世界空间法线nDirWS,因此一共进行了两次Transform空间转换。

UE4中同理:

在Unity中shader代码如下

// shader的路径和名称
Shader "Shader Forge/matcap" {
    // 暴露属性到面板
    Properties {
        _NormalMap ("Normal Map", 2D) = "bump" {}
        _Matcap    ("Matcap", 2D) = "gray" {}
        _FresnelPow("Fresnel Pow", Range(0, 10)) = 1
        _EnvSpecInt("EnvSpecInt", Range(0, 5)) = 1
    }
    SubShader {
        Tags {
            "RenderType"="Opaque"
        }
        Pass {
            Name "FORWARD"
            Tags {
                "LightMode"="ForwardBase"
            }
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma multi_compile_fwdbase_fullshadows
            #pragma target 3.0
            
            // 声明变量
            uniform sampler2D _NormalMap;
            uniform sampler2D _Matcap;
            uniform float _FresnelPow;
            uniform float _EnvSpecInt;
            
            // 输入结构
            struct VertexInput {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv0 : TEXCOORD0;
                float4 tangent : TANGENT;
            };
            
            // 输出结构
            struct VertexOutput {
                float4 pos : SV_POSITION;
                float2 uv0 : TEXCOORD0;
                float3 posWS : TEXCOORD1;
                float3 nDirWS : TEXCOORD2;
                float3 tDirWS : TEXCOORD3;
                float3 bDirWS : TEXCOORD4;
            };
            
            // 顶点shader
            VertexOutput vert (VertexInput v) {
                VertexOutput o = (VertexOutput)0;
                o.pos = UnityObjectToClipPos( v.vertex );
                o.uv0 = v.uv0;
                o.posWS = mul(unity_ObjectToWorld, v.vertex);
                o.nDirWS = UnityObjectToWorldNormal(v.normal);
                o.tDirWS = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz);
                o.bDirWS = normalize(cross(o.nDirWS, o.tDirWS) * v.tangent.w);
                return o;
            }
            
            // 像素shader
            float4 frag(VertexOutput i) : COLOR {
                // 计算Fresnel
                float3 nDirTS = UnpackNormal(tex2D(_NormalMap, i.uv0)).rgb;
                float3x3 TBN = float3x3(i.tDirWS, i.bDirWS, i.nDirWS);
                float3 nDirWS = normalize(mul(nDirTS, TBN));
                float3 vDirWS = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz);  
                float vDotn = dot(vDirWS, nDirWS);

                // 计算MatcapUV
                float3 nDirVS = mul(UNITY_MATRIX_V, nDirWS);   
                float2 matcapUV = nDirVS.rg * 0.5 + 0.5;
                
                // 光照模型
                float3 matcap = tex2D(_Matcap, matcapUV); 
                float fresnel = pow(max(0.0, 1.0 - vDotn), _FresnelPow);
                float3 finalRGB = matcap * fresnel * _EnvSpecInt;
   
                return float4(finalRGB, 1.0);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

由于使用了法线贴图,shader代码中也需要对法线的空间进行转换,fresnel的计算部分会更复杂一些。

 

环境反射之Cubemap

当观察视角是可移动的时,Matcap方式就很难满足环境反射的要求了,此时可以通过Cubemap方式模拟环境反射。

原理和Matcap类似,只是环境贴图不是仅仅映射到物体正面,而是以全景图方式包裹物体全部,例如下图就是一种常用的Latitude-Longitude格式的环境贴图,也称为Spherical Map。

实际使用中,Cubemap是将环境贴图切分为6个不同角度的贴图,再对物体进行投射,例如下图就是Spherical Map类型的环境贴图的切分和投射方式。

还有一种Cubemap类型则是立方体展开形式,通称为Cubic Map,例如下图。

显然两种贴图形式是可以互相转化的,最终的使用方式相同,Spherical Map类型的环境贴图上下方会有拉伸变形,而Cubic Map类型的环境贴图则会有较大的空白区域,浪费贴图性能,因此实际使用中Spherical Map类型更为常用。

在Unity中使用Cube Map环境贴图需要通过以下设置修改为Cubemap贴图。

注意Mapping参数用于设置Cubemap的类型,当前为Spherical Map类型,Cubic Map需要设置为6 Frames Layout(Cubic Environment)。

在Unity中使用方式如下:

其中Cubemap节点输入的DIR参数为观察方向的反射方向,MIP参数用于控制Cubemap贴图的Mipmap等级,参数越高,得到的环境反射越模糊。

在UE4中连接如下:

在Unity中的shader代码如下。

// shader的路径和名称
Shader "Shader Forge/cubemap" {
    // 暴露属性到面板
    Properties {
        _Cubemap   ("Cubemap", Cube) = "_skybox" {}
        _NormalMap ("Normal Map", 2D) = "bump" {}
        _CubemapMip("Cubemap Mip",Range(0, 7)) = 0
        _FresnelPow("Fresnel Pow", Range(0, 10)) = 1
        _EnvSpecInt("EnvSpecInt", Range(0, 5)) = 1
    }
    SubShader {
        Tags {
            "RenderType"="Opaque"
        }
        Pass {
            Name "FORWARD"
            Tags {
                "LightMode"="ForwardBase"
            }
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma multi_compile_fwdbase_fullshadows
            #pragma target 3.0
            
            // 声明变量
            uniform samplerCUBE _Cubemap;
            uniform sampler2D _NormalMap;
            uniform float _CubemapMip;
            uniform float _FresnelPow;
            uniform float _EnvSpecInt;
            
            // 输入结构
            struct VertexInput {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv0 : TEXCOORD0;
                float4 tangent : TANGENT;
            };
            
            // 输出结构
            struct VertexOutput {
                float4 pos : SV_POSITION;
                float2 uv0 : TEXCOORD0;
                float3 posWS : TEXCOORD1;
                float3 nDirWS : TEXCOORD2;
                float3 tDirWS : TEXCOORD3;
                float3 bDirWS : TEXCOORD4;
            };
            
            // 顶点shader
            VertexOutput vert (VertexInput v) {
                VertexOutput o = (VertexOutput)0;
                o.pos = UnityObjectToClipPos( v.vertex );
                o.uv0 = v.uv0;
                o.posWS = mul(unity_ObjectToWorld, v.vertex);
                o.nDirWS = UnityObjectToWorldNormal(v.normal);
                o.tDirWS = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz);
                o.bDirWS = normalize(cross(o.nDirWS, o.tDirWS) * v.tangent.w);
                return o;
            }
            
            // 像素shader
            float4 frag(VertexOutput i) : COLOR {
                // 计算Fresnel
                float3 nDirTS = UnpackNormal(tex2D(_NormalMap, i.uv0)).rgb;
                float3x3 TBN = float3x3(i.tDirWS, i.bDirWS, i.nDirWS);
                float3 nDirWS = normalize(mul(nDirTS, TBN));
                float3 vDirWS = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz);  
                float vDotn = dot(vDirWS, nDirWS);
                
                // 光照模型
                float3 vrDirWS = reflect(-vDirWS, nDirWS);
                float3 var_Cubemap = texCUBElod(_Cubemap, float4(vrDirWS, _CubemapMip)).rgb; 
                float fresnel = pow(max(0.0, 1.0 - vDotn), _FresnelPow);
                float3 finalRGB = var_Cubemap * fresnel * _EnvSpecInt;
   
                return float4(finalRGB, 1.0);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

这种方式虽然在自由移动观察视角时也能得到不错的反射效果,但当场景中的光源或物体发生修改或移动后,就需要重新生成环境贴图;并且只能反射环境,不能反射物体本身,例如两个光滑金属球之间互相多次反射。

这时如果需要获得更真实的反射效果,就得耗费更多性能使用全局光照系统实现环境反射。

 

本文同样参考自B站庄懂的技术美术入门课,感谢。


一个相信童话的孩子,
即使到了不再相信童话的年龄,
仍是更容易相信善良和拒绝冷酷的。

《宝贝,宝贝》
——周国平