之前的Phong光照模型,计算都是直接光照。除此之外还有间接光照:
- 间接光漫反射:光照被二次反弹之后的漫反射
- 烘焙Lightmap
- 使用Light Probe,它用到了下面介绍的SH球谐光照
- 间接光镜面反射:光照被二次反弹之后的镜面反射
- 使用Reflection Probe,它用到了下面介绍的环境贴图和IBL
本节和接下来两节介绍三种技术:
- 环境贴图:
- 实现间接光镜面反射,和Refection Probe差不多
- IBL基于图像的照明:基于环境贴图技术,在贴图的基础上通过一系列计算将环境贴图的光照信息提取出来。并将这些信息重新储存回环境贴图中。
- 实现间接光镜面反射
- 实现间接光漫反射
- SH球谐光照:通过球谐函数提取一张贴图的漫反射信息,它不会生成一张新的贴图,而是生成一些球谐数据,计算更快。

- 用来实现实现间接光漫反射
间接光照知识大纲

环境贴图
环境贴图有以下类型:

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贴图格式。
使用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; float4 _CubeMap_HDR;
half4 color_cubemap = texCUBE(_Cubemap,reflect_dir); color_cubemap.xyz = DecodeHDR(color_cubemap, _CubeMap_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矩阵
这种矩阵可以表示任意单轴的旋转,只要让其余两个轴左乘这个矩阵即可。例如这里让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); reflect_dir = float3(dir_rotate.x, reflect_dir.y, dir_rotate.y);
|
如果想绕X轴旋转可以修改成:
1 2
| float2 dir_rotate = mul(m_rotate, reflect_dir.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 { _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; };
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); 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);
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" {}
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
| 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内置变量会自动重新引用。