《Unity Shader 入门精要》学习总结02


光照概念

为了生成一张由光照来照亮的图像,我们需要考虑这三种要素:

  • 光源:光源中会发射出光线
  • 交互:光源会与物体表面进行交互从而改变路径
  • 成像:光线进入相机会成像,产生看到的画面

光源

首先,我们需要一个标准来量化光照强度。通常,在光学中,我们使用辐照度(Irradiance)来量化光线。在图形学中,我们近似的认为,平行光的辐照度为垂直于 l 的单位面积上单位时间内穿过的光线的能量。对于倾斜表面,根据 Lambert 余弦定律,辐照度需乘以光源方向 l 与法线 n 的夹角余弦值 max(0, n·l)

辐照度示意图

光线的吸收和散射

光与物体表面进行交互时,会发生两种行为,分别为散射吸收

  • 散射:改变光线的方向,不改变光的颜色和密度
  • 吸收:改变光的密度和颜色,不改变方向

而在散射后光又会去往两个方向,分别是折射(透射)反射

  • 折射(透射):光散射到物体内部,有些会从内部再次出来,有些被吸收
  • 反射:将光反射到外部

我们在光照模型中使用两个不同的部分计算这两种散射方向,分别为镜反射(Specular)和漫反射(Diffuse)。

  • 镜反射:完全被反射的光线
  • 漫反射:包含有被折射,吸收后散射出表面的光线。

根据入射光线的数量/方向,我们可以计算得到出射光线的数量/方向,一般我们使用出射度(exitance)描述它。

辐照度与出射度之间满足线性关系,其比值就是材质的漫反射和高光反射属性。

着色

着色指的是根据材质属性,光源信息,使用一个等式计算某观察方向上的出射度的过程。这个等式便是我们所说的光照模型


光照模型

计算某观察方向上的光线出射度的等式。

什么是BRDF

BRDF(双向反射分布函数)描述的是物体表面将光能从任何一个入射方向反射到任何一个视点方向的反射特性,即入射光线经过某个表面反射后如何在各个出射方向上分布。BRDF 模型是绝大多数图形学算法中用于描述光反射现象的基本模型。

BRDF 具有以下两个特性:

  • 交换律:将入射方向和出射方向互换后 BRDF 值不变
  • 能量守恒:反射能量的总和不超过入射光总能量

BRDF 的两个部分:

  • 高光反射项:描述表面反射(镜反射)的部分
  • 漫反射项:描述次表面散射的部分

BRDF 光照模型的大致分类:

  • 经典光照模型:Lambert / Phong / Blinn-Phong 等等
  • 数据光照模型(游戏领域不常见)
  • 基于物理的光照模型

经典光照模型

存在有许多的经典光照模型。这里我们来了解著名学者裴祥风提出的标准光照模型。这个模型非常经典,在许多方面都有所运用。

标准光照模型将入射摄像机的光线分为 4 部分,每一部分使用不同方法计算贡献度:

  • 自发光(emissive):描述给定一个方向时,表面自身会向该方向发射多少辐射量。
  • 高光反射(specular):描述镜面反射光线的光照贡献量。
  • 漫反射(diffuse):描述次表面散射部分的光照贡献量。
  • 环境光(ambient):描述其他来自环境的间接光照。
自发光

计算方式非常简单,直接使用材质的自发光颜色即可。

cemissive=gemissive
漫反射

因为漫反射的光线出射方向是四散的,因此我们在标准光照模型中近似的认为任何方向上反射分布都是一样的。漫反射光照符合兰伯特定律(Lambert’s law)。

cdiffuse=(clightmdiffuse)max(0,nl)
高光反射

可以近似计算沿着完全镜面反射方向被反射的光线。

核心思想:摄像机方向和反射方向越接近,你看到的高光就越亮。当 vr 完全重合时,点积为 1,最亮;角度偏离越大,值越小,因为余弦值下降。

Phong模型:

Phong 模型计算的是:从越接近镜面反射方向的角度观察,高光越亮

Phong模型

Phong 模型

cspecular=(clightmspecular)max(0,v^r)mgloss
  • cspecular :最终输出的镜面高光颜色
  • clight :光源颜色
  • mspecular :材质的高光反射颜色(比如金色高光还是白色高光)
  • v :从物体表面指向摄像机/眼睛的方向向量(单位向量)
  • r :光线打到表面后反射出去的方向(单位向量,后面公式算的就是它)
  • mgloss 光泽度(shininess),控制高光有多"集中"。值越大,亮点越小越锐利。(一般位于 8 到 256 之间)

其中 r 的展开为。

r=2(n^I)n^I

其中:

  • n^ :表面法线方向(单位向量)
  • I 入射光方向(从物体表面指向光源,单位向量)。注意,这里可能是从表面指向光源,而不是从光源指向表面。一般图形学中 I 的方向是指向光源的。

几何意义
反射向量 r 是入射光关于法线的"镜像"。

公式分两步看:

  1. 2 * (n·I) * n:把入射光投影到法线方向,然后翻倍,得到一个在法线方向上的分量。
  2. 再减去原来的入射光方向 I,就得到反射方向。

Blinn模型:

Blinn模型半角向量示意

除了 Phong 模型,Blinn 还提出了另一个简单的修改方法来得到类似效果。Blinn 的模型中可以避免计算反射方向 r,但是我们需要引入一个新的向量 h^ 半角向量)。

h^=v^+Iv^+I cspecular=(clightmspecular)max(0,n^h^)mgloss
环境光

在现实中,物体也可以被间接光照照亮。间接光是指从别的物体上反射从而照射到计算表面的光线。

在标准模型中,我们使用一种被称为环境光的部分近似模拟间接光照。

cambient=gambient

逐像素?逐顶点?

在片元着色器中计算,便是逐像素光照,在顶点着色器中计算,便是逐顶点光照


Unity Shader中的标准模型实现

Blinn-Phong 完整实现:

Shader "Unity Shaders Book/Chapter 6/Blinn-Phong" {
    Properties {
        _Diffuse ("漫反射颜色", Color) = (1, 1, 1, 1)
        _Specular ("高光颜色", Color) = (1, 1, 1, 1)
        _Gloss ("光泽度", Range(8.0, 256)) = 20
    }
    SubShader {
        Pass { 
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

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

            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };

            v2f vert(a2v v) {
                v2f o;
                // 将顶点从物体空间变换到裁剪空间
                o.pos = UnityObjectToClipPos(v.vertex);

                // 将法线从物体空间变换到世界空间
                o.worldNormal = UnityObjectToWorldNormal(v.normal);

                // 将顶点从物体空间变换到世界空间
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                return o;
            }

            fixed4 frag(v2f i) : SV_Target {
                // 获取环境光项
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

                // 计算漫反射项
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

                // 获取世界空间下的视角方向
                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                // 获取世界空间下的半角向量
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                // 计算高光项
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

                return fixed4(ambient + diffuse + specular, 1.0);
            }

            ENDCG
        }
    } 
    FallBack "Specular"
}