之前的Phong光照模型,计算都是直接光照。除此之外还有间接光照:

  1. 间接光漫反射:光照被二次反弹之后的漫反射
    1. 烘焙Lightmap
    2. 使用Light Probe,它用到了下面介绍的SH球谐光照
  2. 间接光镜面反射:光照被二次反弹之后的镜面反射
    1. 使用Reflection Probe,它用到了下面介绍的环境贴图和IBL

本节和接下来两节介绍三种技术:

  1. 环境贴图:
    1. 实现间接光镜面反射,和Refection Probe差不多
  2. IBL基于图像的照明:基于环境贴图技术,在贴图的基础上通过一系列计算将环境贴图的光照信息提取出来。并将这些信息重新储存回环境贴图中。
    1. 实现间接光镜面反射
    2. 实现间接光漫反射
  3. SH球谐光照:通过球谐函数提取一张贴图的漫反射信息,它不会生成一张新的贴图,而是生成一些球谐数据,计算更快。
    1. 球谐光照
    2. 用来实现实现间接光漫反射

间接光照知识大纲

间接光照大纲

环境贴图

环境贴图有以下类型:

环境贴图类型

Cube贴图中:

+Z代表了Front,-Z代表了Back,-X代表了Left,+X代表了Right,-Y代表了Bottom,+Y代表了Top

其中包括了LatLong经纬度全景贴图/Panorama,SphereMap球面环境贴图,和Cubemap立方体贴图。

在Unity中,将任意格式的环境贴图的Texture Type从2D改为Cube,Unity会根据环境贴图的长宽比自动识别环境贴图类型并还原环境贴图。

通常情况下,使用Cubemap贴图更好一些,因为LatLong贴图和SphereMap贴图都有像素拉伸的区域,浪费空间而且会对渲染精度有影响。

立方体贴图格式转换和创建

使用HDRShop

打开HDRShop软件,将一张hdr后缀的LatLong经纬度全景图拖拽进软件中,然后点击图像——全景——全景转换,在弹出的窗口中将“源图像”的格式设置为Latitude/Longitude(经纬图),然后在”目标图像“中将格式设置为”Cubic Environment“

使用cmftStudio

点击顺序

按上图1234的顺序加载全景HDR贴图。

另存HDR贴图

按上图顺序另存HDR贴图格式。

使用HDRLightStudio

这个软件能够通过放置灯光而烘焙出HDR贴图。

采样立方体贴图

采样立方体

在上图中可以看出,立方体贴图的采样方式是,求摄像机到顶点的向量的反射向量,然后根据反射向量对立方体贴图进行相交检测并采样。

这种采样方式有局限性,如果是平面模型,会出现模型的面只采样立方体贴图一个点的情况,因为它们的法向量都是一致的。需要使用Local Reflection来解决。

Local Reflection在计算反射向量时,会根据起始点的位置进行偏移。从而解决上述问题。

立方体坐标系

注意看上面的坐标系,+Z(Front)视角和坐标系的+Z方向是平行的,其余的六个面视角和对应的轴都平行。

立方体贴图采样方法

计算落点的过程是:

假设我们计算出来的反射向量是上图的(-0.5,0.4,-0.2),这三个数中-0.5的绝对值最大,所以这个向量肯定落在-X平面上。

然后将-0.5归一化,将其中的系数和其余两个数相乘,就得到了(-1,0.8,-0.4),这就是在单位立方体上的落点坐标,

根据这个落点坐标,在进行一系列计算得到立方体贴图对应的uv坐标再采样颜色即可。这其中的计算包括y反向(DirectX中的uv坐标标准是左上为零点),yz交换,然后限制在(0,1)范围内。

Unity中声明和采样立方体贴图的方法是:

1
2
3
4
5
6
7
_CubeMap ("CubeMap", Cube) = "white" {}//属性声明方式

samplerCUBE _CubeMap;//Pass中的声明方式
float4 _CubeMap_HDR;//声明Unity在移动端解码HDR用的RGBM格式,这个命名也是约定好的

half4 color_cubemap = texCUBE(_Cubemap,reflect_dir);//计算出reflect_dir之后,计算反射颜色
color_cubemap.xyz = DecodeHDR(color_cubemap, _CubeMap_HDR);//移动端使用RGBM解码,确保能拿到HDR信息

HDR在移动端的问题

一般来讲,一个HDR的全景贴图的后缀是.exr,Unity导入它时,会将它编辑为RGB HDR Compressed BC6H 格式,这个格式在移动端并不支持读取,Unity提供了RGBM编码用来解决这个问题。

HDR贴图的后处理

如果我们的HDR贴图使用了其他的压缩格式(在贴图的导入设置中勾选Override for PC,Mac & Standalone,然后在Format中选择其他格式),会让Bloom和Color Gradient(ACES)后处理失效,因为其他的压缩格式会让HDR的亮度限制在0到1范围内。

环境贴图的旋转矩阵

如果想要旋转环境贴图,直观的做法就是旋转反射向量reflect_dir,让它沿着Y轴旋转,一个单位向量沿着Y轴旋转时,它的Y坐标不变,XZ坐标进行旋转。

这里我们要实现一个绕Y轴的旋转矩阵,也就是XZ平面的旋转矩阵。假设旋转的角度为θ,那么我们可以构造一个2x2矩阵

1
2
cosθ,-sinθ
sinθ, cosθ

这种矩阵可以表示任意单轴的旋转,只要让其余两个轴左乘这个矩阵即可。例如这里让Y轴旋转,就让reflect_dir的XZ坐标左乘这个矩阵。

1
2
3
4
5
6
7
8
_Rotate("Rotate", Range(0,360)) = 0
float _Rotate;

float rad = _Rotate * UNITY_PI / 100;//将角度转换为弧度
float2x2 m_rotate = float2x2(cos(rad), -sin(rad), sin(rad), cos(rad));//计算旋转矩阵

float2 dir_rotate = mul(m_rotate, reflect_dir.xz);//旋转XZ坐标
reflect_dir = float3(dir_rotate.x, reflect_dir.y, dir_rotate.y);//重新赋值反射方向。

如果想绕X轴旋转可以修改成:

1
2
float2 dir_rotate = mul(m_rotate, reflect_dir.yz);//旋转YZ坐标
reflect_dir = float3(reflect_dir.x, dir_rotate.x, dir_rotate.y);//重新赋值反射方向。

完整Shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
Shader "KerryTA/Code/301CubeMap"
{
Properties
{
//_MainTex ("Texture", 2D) = "white" {}
_CubeMap ("CubeMap", Cube) = "white" {}
_Rotate("Rotate", Range(0,360)) = 0
_NormalMap("NormalMap", 2D) = "bump" {}
_NormalIntensity("NormalIntensity", Range(0.0, 5.0)) = 0.5
_AOMap("AOMap", 2D) = "white" {}
_Tint("Tint", Color) = (1,1,1,1)
_Expose("Expose", Float) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
float3 normal_world : TEXCOORD1;
float3 pos_world : TEXCOORD2;
float3 tangent_world : TEXCOORD3;
float3 binormal_world : TEXCOORD4;
};

//sampler2D _MainTex;
//float4 _MainTex_ST;
samplerCUBE _CubeMap;
float4 _CubeMap_HDR;
sampler2D _NormalMap;
float4 _NormalMap_ST;
float _NormalIntensity;
sampler2D _AOMap;
float4 _Tint;
float _Expose;
float _Rotate;

float3 RotateYAround(float degree, float3 target)
{
float rad = degree * UNITY_PI / 100;
float2x2 m_rotate = float2x2(cos(rad), -sin(rad), sin(rad), cos(rad));
float2 dir_rotate = mul(m_rotate, target.xz);//旋转XZ坐标
target = float3(dir_rotate.x, target.y, dir_rotate.y);//重新赋值反射方向。
return target;
}

v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord * _NormalMap_ST.xy + _NormalMap_ST.zw;
o.normal_world = normalize(mul(float4(v.normal, 0.0), unity_WorldToObject).xyz);
o.tangent_world = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz);
o.binormal_world = normalize(cross(o.normal_world, o.tangent_world)) * v.tangent.w;
o.pos_world = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}

half4 frag (v2f i) : SV_Target
{
half3 normal_dir = normalize(i.normal_world);
half3 tangent_dir = normalize(i.tangent_world);
half3 binormal_dir = normalize(i.binormal_world);
float3x3 TBN = float3x3(tangent_dir, binormal_dir, normal_dir);
half3 normaldata = UnpackNormal(tex2D(_NormalMap, i.uv));
normaldata.xy = normaldata.xy * _NormalIntensity;
normal_dir = normalize(mul(normaldata, TBN));

half ao = tex2D(_AOMap, i.uv).r;
half3 view_dir = normalize(_WorldSpaceCameraPos.xyz - i.pos_world);
half3 reflect_dir = reflect(-view_dir, normal_dir);//需要将view_dir反向,从摄像机指向顶点。

reflect_dir = RotateYAround(_Rotate, reflect_dir);

half4 color_cubemap = texCUBE(_CubeMap, reflect_dir);
half3 env_color = DecodeHDR(color_cubemap, _CubeMap_HDR);
half3 final_color = env_color * ao * _Tint.rgb * _Expose;
return half4(final_color, 1.0);
}
ENDCG
}
}
}

采样经纬度贴图

Unity还提供了采样经纬度贴图的方法,它将反射向量转换为二维uv坐标(利用到了球面化的计算),来采样经纬度贴图。

1
2
3
4
5
6
7
8
9
10
11
12
13
_PanoramaMap("PanoramaMap", 2D) = "white" {}//2D格式

sampler2D _PanoramaMap;
float4 _PanoramaMap_HDR;

float3 normalizedCoords = normalize(reflect_dir);
float latitude = acos(normalizedCoords.y);
float longitude = atan2(normalizedCoords.z, normalizedCoords.x);
float2 sphereCoords = float2(longitude, latitude) * float2(0.5/UNITY_PI, 1.0/UNITY_PI);
float2 uv_panorama = float2(0.5,1.0) - sphereCoords;

half4 color_panoramaMap = tex2D(_PanoramaMap, uv_panorama);
half3 env_color = DecodeHDR(color_panoramaMap, _PanoramaMap_HDR);

如果经纬度贴图在物体表面出现接缝的话,可以在贴图的导入设置中关闭Generate Mipmap

反射探针

除了使用自定义的环境贴图,我们还可以使用反射探针生成的反射贴图。

我们在场景中放置一个物体,在这个物体的子对象上挂上Light——Reflecton Probe,并将场景中其他需要被反射的物体设置Static——Reflecton Probe Static。然后我们点击Reflection Probe的Bake按钮。Unity会在当前场景Scene所在的文件夹里生成一个同名文件夹,并在其中放置反射探针烘焙形成的环境贴图。

如果要取得反射探针产生的环境贴图,直接使用Unity内置变量即可,无需再在Property中声明CubeMap贴图。

1
2
3
//Unity提供了unity_SpecCube0和unity_SpecCube1提供混合。
half4 env_color = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, reflect_dir);
half3 env_hdr_color = DecodeHDR(env_color, unity_SpecCube0_HDR);

默认全局反射探针

如果当前场景的反射探针并没有Bake,我们还可以在Lighting面板中点击Generate Lighting,会自动Bake探针。属于默认的全局反射探针。我们可以通过在Lighting——Scene面板中修改Environment Reflections的参数来修改全局反射探针的属性。

同时注意的是,如果我们把当前场景中的反射探针Bake出来了,会自动替换掉默认的全局反射探针,Unity内置变量会自动重新引用。