使用IBL计算出来的间接光漫反射其实还是有一些粗糙,这里我们使用SH球谐光照来计算漫反射,性能更好。

计算球谐光照

球谐光照不是通过贴图来实现的,而是通过计算得到七个四维参数(SHAr、SHAg、SHAb、SHBr、SHBg、SHBb、SHC)计算得来的。

SH计算工具

在Editor文件夹内,分别放置下面两个脚本:

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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

public class CubemapSHProjector : EditorWindow
{
//PUBLIC FIELDS
public Texture envMap;
public Transform go;

//PRIVATE FIELDS
private Material view_mat;
private float view_mode;
private Vector4[] coefficients;

private SerializedObject so;
private SerializedProperty sp_input_cubemap;

private Texture2D tmp = null;

private static void CheckAndConvertEnvMap(ref Texture envMap, ref Vector4[] sh_out)
{
if (!envMap) return;

string map_path = AssetDatabase.GetAssetPath(envMap);

if (string.IsNullOrEmpty(map_path)) return;

TextureImporter ti = AssetImporter.GetAtPath(map_path) as TextureImporter;
if (!ti) return;

bool read_able = ti.isReadable;
bool need_reimport = false;

if (ti.textureShape != TextureImporterShape.TextureCube)
{
ti.textureShape = TextureImporterShape.TextureCube;
need_reimport = true;
}

if (!ti.mipmapEnabled)
{
ti.mipmapEnabled = true;
need_reimport = true;
}

if (!ti.sRGBTexture)
{
ti.sRGBTexture = true;
need_reimport = true;
}

if (ti.filterMode != FilterMode.Trilinear)
{
ti.filterMode = FilterMode.Trilinear;
need_reimport = true;
}

TextureImporterSettings tis = new TextureImporterSettings();
ti.ReadTextureSettings(tis);
if (tis.cubemapConvolution != TextureImporterCubemapConvolution.Specular)
{
tis.cubemapConvolution = TextureImporterCubemapConvolution.Specular;
ti.SetTextureSettings(tis);
need_reimport = true;
}

//if (ti.GetDefaultPlatformTextureSettings().maxTextureSize != 128)
//{
// TextureImporterPlatformSettings tips = new TextureImporterPlatformSettings();
// tips.maxTextureSize = 128;
// ti.SetPlatformTextureSettings(tips);
// ti.maxTextureSize = 128;
// need_reimport = true;
//}

if (!read_able)
{
ti.isReadable = true;
need_reimport = true;
}

if (need_reimport)
{
ti.SaveAndReimport();
}

envMap = AssetDatabase.LoadAssetAtPath<Texture>(map_path);
if (!envMap) return;

Vector3[] sh = new Vector3[9];
SphericalHarmonicsCoefficient.sphericalHarmonicsFromCubemap9((Cubemap)envMap, ref sh);
SphericalHarmonicsCoefficient.ConvertSHConstants(sh, ref sh_out);


if (ti.isReadable != read_able)
{
ti.isReadable = read_able;
ti.SaveAndReimport();
envMap = AssetDatabase.LoadAssetAtPath<Texture>(map_path);
}
}

[MenuItem("美术/SH系数生成", false, 2100)]
static void Init()
{
CubemapSHProjector window = (CubemapSHProjector)EditorWindow.GetWindow(typeof(CubemapSHProjector));
window.Show();
window.titleContent = new GUIContent("SH生成器");
}

private void OnFocus()
{
Initialize();
}

private void OnEnable()
{
Initialize();
}

private void Initialize()
{
so = new SerializedObject(this);
sp_input_cubemap = so.FindProperty("input_cubemap");
}

private void OnGUI()
{
EditorGUI.BeginChangeCheck();
envMap = EditorGUILayout.ObjectField("环境图", envMap, typeof(Texture), false) as Texture;

if (envMap != null)
{
EditorGUILayout.Space();

if (GUILayout.Button("Calc"))
{
if (envMap != null)
{
coefficients = new Vector4[7];
CheckAndConvertEnvMap(ref envMap, ref coefficients);
}
SceneView.RepaintAll();
}

EditorGUILayout.Space();

go = EditorGUILayout.ObjectField("Obj", go, typeof(Transform), true) as Transform;
if (go != null)
{
if (GUILayout.Button("Apply"))
{
List<Material> mat_list = new List<Material>();
var renders = go.GetComponentsInChildren<Renderer>();
foreach (var render in renders)
{
mat_list.AddRange(render.sharedMaterials);
}
foreach (var mat in mat_list)
{
mat.SetVector("custom_SHAr", coefficients[0]);
mat.SetVector("custom_SHAg", coefficients[1]);
mat.SetVector("custom_SHAb", coefficients[2]);
mat.SetVector("custom_SHBr", coefficients[3]);
mat.SetVector("custom_SHBg", coefficients[4]);
mat.SetVector("custom_SHBb", coefficients[5]);
mat.SetVector("custom_SHC", coefficients[6]);
}
mat_list.Clear();
SceneView.RepaintAll();
}
}

EditorGUILayout.Space();

//print the 9 coefficients
if (coefficients != null)
{
EditorGUILayout.LabelField("custom_SHAr" + ": " + coefficients[0].ToString("F4"));
EditorGUILayout.LabelField("custom_SHAg" + ": " + coefficients[1].ToString("F4"));
EditorGUILayout.LabelField("custom_SHAb" + ": " + coefficients[2].ToString("F4"));
EditorGUILayout.LabelField("custom_SHBr" + ": " + coefficients[3].ToString("F4"));
EditorGUILayout.LabelField("custom_SHBg" + ": " + coefficients[4].ToString("F4"));
EditorGUILayout.LabelField("custom_SHBb" + ": " + coefficients[5].ToString("F4"));
EditorGUILayout.LabelField("custom_SHC" + ": " + coefficients[6].ToString("F4"));
}
}

EditorGUILayout.Space();
if (tmp != null)
GUILayout.Label(tmp);
}
}
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
/// <summary>
/// 球谐光照因子计算方法,结果可以跟Unity内部的算法匹配上
/// http://sunandblackcat.com/tipFullView.php?l=eng&topicid=32&topic=Spherical-Harmonics-From-Cube-Texture
/// https://github.com/Microsoft/DirectXMath
/// http://www.ppsloan.org/publications/StupidSH36.pdf
/// </summary>

using UnityEngine;
using UnityEditor;

public static class SphericalHarmonicsCoefficient
{
public static void sphericalHarmonicsFromCubemap9(Cubemap cubeTexture, ref Vector3[] output)
{
// allocate memory for calculations
float[] resultR = new float[9];
float[] resultG = new float[9];
float[] resultB = new float[9];

// initialize values
float fWt = 0.0f;
for (uint i = 0; i < 9; i++)
{
resultR[i] = 0;
resultG[i] = 0;
resultB[i] = 0;
}

float[] shBuff = new float[9];
float[] shBuffB = new float[9];

// for each face of cube texture
for (int face = 0; face < 6; face++)
{
// step between two texels for range [0, 1]
float invWidth = 1.0f / cubeTexture.width;
// initial negative bound for range [-1, 1]
float negativeBound = -1.0f + invWidth;
// step between two texels for range [-1, 1]
float invWidthBy2 = 2.0f / cubeTexture.width;

Color[] data = cubeTexture.GetPixels((CubemapFace)face);

for (int y = 0; y < cubeTexture.width; y++)
{
// texture coordinate V in range [-1 to 1]
float fV = negativeBound + y * invWidthBy2;

for (int x = 0; x < cubeTexture.width; x++)
{
// texture coordinate U in range [-1 to 1]
float fU = negativeBound + x * invWidthBy2;

// determine direction from center of cube texture to current texel
Vector3 dir;

switch ((CubemapFace)face)
{
case CubemapFace.PositiveX:
dir.x = 1.0f;
dir.y = 1.0f - (invWidthBy2 * y + invWidth);
dir.z = 1.0f - (invWidthBy2 * x + invWidth);
break;
case CubemapFace.NegativeX:
dir.x = -1.0f;
dir.y = 1.0f - (invWidthBy2 * y + invWidth);
dir.z = -1.0f + (invWidthBy2 * x + invWidth);
break;
case CubemapFace.PositiveY:
dir.x = -1.0f + (invWidthBy2 * x + invWidth);
dir.y = 1.0f;
dir.z = -1.0f + (invWidthBy2 * y + invWidth);
break;
case CubemapFace.NegativeY:
dir.x = -1.0f + (invWidthBy2 * x + invWidth);
dir.y = -1.0f;
dir.z = 1.0f - (invWidthBy2 * y + invWidth);
break;
case CubemapFace.PositiveZ:
dir.x = -1.0f + (invWidthBy2 * x + invWidth);
dir.y = 1.0f - (invWidthBy2 * y + invWidth);
dir.z = 1.0f;
break;
case CubemapFace.NegativeZ:
dir.x = 1.0f - (invWidthBy2 * x + invWidth);
dir.y = 1.0f - (invWidthBy2 * y + invWidth);
dir.z = -1.0f;
break;
default:
return;
}

// normalize direction
dir = dir.normalized;

// scale factor depending on distance from center of the face
float fDiffSolid = 4.0f / ((1.0f + fU * fU + fV * fV) * Mathf.Sqrt(1.0f + fU * fU + fV * fV));
fWt += fDiffSolid;

// calculate coefficients of spherical harmonics for current direction
sphericalHarmonicsEvaluateDirection9(ref shBuff, dir);
//XMSHEvalDirection(dir, ref shBuff);

// index of texel in texture
int pixOffsetIndex = x + y * cubeTexture.width;
// get color from texture and map to range [0, 1]
Vector3 clr= new Vector3(data[pixOffsetIndex].r, data[pixOffsetIndex].g, data[pixOffsetIndex].b);
//if (data[pixOffsetIndex].a == 1)
//{
// clr = new Vector3(data[pixOffsetIndex].r, data[pixOffsetIndex].g, data[pixOffsetIndex].b);
//}
//else
//{
// clr = DecodeHDR(data[pixOffsetIndex]);
//}
if (PlayerSettings.colorSpace == ColorSpace.Gamma)
{
clr.x = Mathf.GammaToLinearSpace(clr.x);
clr.y = Mathf.GammaToLinearSpace(clr.y);
clr.z = Mathf.GammaToLinearSpace(clr.z);
}
// scale color and add to previously accumulated coefficients
sphericalHarmonicsScale9(ref shBuffB, shBuff, clr.x * fDiffSolid);
sphericalHarmonicsAdd9(ref resultR, resultR, shBuffB);
sphericalHarmonicsScale9(ref shBuffB, shBuff, clr.y * fDiffSolid);
sphericalHarmonicsAdd9(ref resultG, resultG, shBuffB);
sphericalHarmonicsScale9(ref shBuffB, shBuff, clr.z * fDiffSolid);
sphericalHarmonicsAdd9(ref resultB, resultB, shBuffB);
}
}
}

// final scale for coefficients
float fNormProj = (4.0f * Mathf.PI) / fWt;
sphericalHarmonicsScale9(ref resultR, resultR, fNormProj);
sphericalHarmonicsScale9(ref resultG, resultG, fNormProj);
sphericalHarmonicsScale9(ref resultB, resultB, fNormProj);

// save result
for (uint i = 0; i < 9; i++)
{
output[i].x = resultR[i];
output[i].y = resultG[i];
output[i].z = resultB[i];
}
}

private static Vector3 DecodeHDR(Color clr)
{
return new Vector3(clr.r, clr.g, clr.b) * clr.a;// * Mathf.Pow(clr.a, 2);// * (Mathf.Pow(clr.a, 0.1f) * 1);
}

private static void sphericalHarmonicsEvaluateDirection9(ref float[] outsh, Vector3 dir)
{
// 86 clocks
// Make sure all constants are never computed at runtime
const float kInv2SqrtPI = 0.28209479177387814347403972578039f; // 1 / (2*sqrt(kPI))
const float kSqrt3Div2SqrtPI = 0.48860251190291992158638462283835f; // sqrt(3) / (2*sqrt(kPI))
const float kSqrt15Div2SqrtPI = 1.0925484305920790705433857058027f; // sqrt(15) / (2*sqrt(kPI))
const float k3Sqrt5Div4SqrtPI = 0.94617469575756001809268107088713f; // 3 * sqrtf(5) / (4*sqrt(kPI))
const float kSqrt15Div4SqrtPI = 0.54627421529603953527169285290135f; // sqrt(15) / (4*sqrt(kPI))
const float kOneThird = 0.3333333333333333333333f; // 1.0/3.0
outsh[0] = kInv2SqrtPI;
outsh[1] = -dir.y * kSqrt3Div2SqrtPI;
outsh[2] = dir.z * kSqrt3Div2SqrtPI;
outsh[3] = -dir.x * kSqrt3Div2SqrtPI;
outsh[4] = dir.x * dir.y * kSqrt15Div2SqrtPI;
outsh[5] = -dir.y * dir.z * kSqrt15Div2SqrtPI;
outsh[6] = (dir.z * dir.z - kOneThird) * k3Sqrt5Div4SqrtPI;
outsh[7] = -dir.x * dir.z * kSqrt15Div2SqrtPI;
outsh[8] = (dir.x * dir.x - dir.y * dir.y) * kSqrt15Div4SqrtPI;
}

private static void sphericalHarmonicsAdd9(ref float[] result, float[] inputA, float[] inputB)
{
for (int i = 0; i < 9; i++)
{
result[i] = inputA[i] + inputB[i];
}
}

private static void sphericalHarmonicsScale9(ref float[] result, float[] input, float scale)
{
for (int i = 0; i < 9; i++)
{
result[i] = input[i] * scale;
}
}

public static readonly float s_fSqrtPI = Mathf.Sqrt(Mathf.PI);
public static readonly float fC0 = 1.0f / (2.0f * s_fSqrtPI);
public static readonly float fC1 = Mathf.Sqrt(3.0f) / (3.0f * s_fSqrtPI);
public static readonly float fC2 = Mathf.Sqrt(15.0f) / (8.0f * s_fSqrtPI);
public static readonly float fC3 = Mathf.Sqrt(5.0f) / (16.0f * s_fSqrtPI);
public static readonly float fC4 = 0.5f * fC2;
public static void ConvertSHConstants(Vector3[] sh, ref Vector4[] SHArBrC)
{
int iC;
for (iC = 0; iC < 3; iC++)
{
SHArBrC[iC].x = -fC1 * sh[3][iC];
SHArBrC[iC].y = -fC1 * sh[1][iC];
SHArBrC[iC].z = fC1 * sh[2][iC];
SHArBrC[iC].w = fC0 * sh[0][iC] - fC3 * sh[6][iC];
}

for (iC = 0; iC < 3; iC++)
{
SHArBrC[iC + 3].x = fC2 * sh[4][iC];
SHArBrC[iC + 3].y = -fC2 * sh[5][iC];
SHArBrC[iC + 3].z = 3.0f * fC3 * sh[6][iC];
SHArBrC[iC + 3].w = -fC2 * sh[7][iC];
}

SHArBrC[6].x = fC4 * sh[8][0];
SHArBrC[6].y = fC4 * sh[8][1];
SHArBrC[6].z = fC4 * sh[8][2];
SHArBrC[6].w = 1.0f;
}
}

首先确定计算球谐光照的环境贴图的Convolution Type为None,压缩选项中Max Size设为512。

在菜单栏里点击“美术——SH系数生成”,将环境贴图放入弹出的界面中,点击Clac,就能生成七个系数了。

SH的Shader

准备SH的Shader接受这些参数并计算球谐光照:

主要的Property:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
custom_SHAr("Custom_SHAr", Vector) = (0,0,0,0)
custom_SHAg("custom_SHAg", Vector) = (0,0,0,0)
custom_SHAb("custom_SHAb", Vector) = (0,0,0,0)
custom_SHBr("custom_SHBr", Vector) = (0,0,0,0)
custom_SHBg("custom_SHBg", Vector) = (0,0,0,0)
custom_SHBb("custom_SHBb", Vector) = (0,0,0,0)
custom_SHC("Custom_SHC", Vector) = (0,0,0,1)

half4 custom_SHAr;
half4 custom_SHAg;
half4 custom_SHAb;
half4 custom_SHBr;
half4 custom_SHBg;
half4 custom_SHBb;
half4 custom_SHC;

主要的计算函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
float4 normalForSH = float4(normal_dir, 1.0);
//SHEvalLinearLOL1
half3 x;
x.r = dot(custom_SHAr, normalForSH);
x.g = dot(custom_SHAg, normalForSH);
x.b = dot(custom_SHAb, normalForSH);
//SHEvalLinearL2
half3 x1, x2;
//4 of the quadratic(L2)polynomials
half4 vB = normalForSH.xyzz * normalForSH.yzzx;
x1.r = dot(custom_SHBr, vB);
x1.g = dot(custom_SHBg, vB);
x1.b = dot(custom_SHBb, vB);
//Final(5th) quadratic(L2)polynomial
half vC = normalForSH.x * normalForSH.x - normalForSH.y * normalForSH.y;
x2 = custom_SHC.rgb * vC;
float3 sh = max(float3(0.0, 0.0, 0.0),(x + x1 + x2));
sh = pow(sh, 1.0 / 2.2);

完整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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
Shader "KerryTA/Code/302SHDiffuse"
{
Properties
{
_Tint("Tint", Color) = (1,1,1,1)
_Expose("Expose", Float) = 1
_NormalMap("NormalMap", 2D) = "bump" {}
_NormalIntensity("NormalIntensity", Range(0.0, 5.0)) = 0.5
_AOMap("AOMap", 2D) = "white" {}
_AOAdjust("AOAdjust", Range(0,1)) = 1

custom_SHAr("Custom_SHAr", Vector) = (0,0,0,0)
custom_SHAg("custom_SHAg", Vector) = (0,0,0,0)
custom_SHAb("custom_SHAb", Vector) = (0,0,0,0)
custom_SHBr("custom_SHBr", Vector) = (0,0,0,0)
custom_SHBg("custom_SHBg", Vector) = (0,0,0,0)
custom_SHBb("custom_SHBb", Vector) = (0,0,0,0)
custom_SHC("Custom_SHC", Vector) = (0,0,0,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 _NormalMap;
float4 _NormalMap_ST;
float _NormalIntensity;
sampler2D _AOMap;
float _AOAdjust;
float4 _Tint;
float _Expose;

half4 custom_SHAr;
half4 custom_SHAg;
half4 custom_SHAb;
half4 custom_SHBr;
half4 custom_SHBg;
half4 custom_SHBb;
half4 custom_SHC;


float3 ACESFilm(float3 x)
{
float a = 2.51f;
float b = 0.03f;
float c = 2.43f;
float d = 0.59f;
float e = 0.14f;
return saturate((x*(a*x + b)) / (x*(c*x + d) + e));
}

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;
ao = lerp(1.0, ao, _AOAdjust);

float4 normalForSH = float4(normal_dir, 1.0);
//SHEvalLinearLOL1
half3 x;
x.r = dot(custom_SHAr, normalForSH);
x.g = dot(custom_SHAg, normalForSH);
x.b = dot(custom_SHAb, normalForSH);
//SHEvalLinearL2
half3 x1, x2;
//4 of the quadratic(L2)polynomials
half4 vB = normalForSH.xyzz * normalForSH.yzzx;
x1.r = dot(custom_SHBr, vB);
x1.g = dot(custom_SHBg, vB);
x1.b = dot(custom_SHBb, vB);
//Final(5th) quadratic(L2)polynomial
half vC = normalForSH.x * normalForSH.x - normalForSH.y * normalForSH.y;
x2 = custom_SHC.rgb * vC;
float3 sh = max(float3(0.0, 0.0, 0.0),(x + x1 + x2));
sh = pow(sh, 1.0 / 2.2);


half3 env_color = sh;
half3 final_color = env_color * ao * _Tint.rgb * _Expose;

return half4(final_color, 1.0);
}
ENDCG
}
}
}

本例中的球谐光照是在片元Shader中计算的,其中部分计算在顶点Shader中计算就可以,更省性能。

将使用了此Shader的物体放进“美术——SH系数生成”弹出的界面中计算好SH后的Obj框内。点击Apply

SH的应用

SH大量应用在光照探针中,来采集间接漫反射光

Light Probe

使用默认Light Probe

Unity会给应用了Light Probe但是场景中没有放置Light Probe的物体使用默认的SH光照信息,默认的SH信息就是从场景天空盒中自动生成的。但是注意如果场景中有其他的灯光的话默认的Light Probe的SH信息不会包含进去。

想要应用Unity的Light Probe,必须在Shader中声明为ForwardBase Pass,然后调用ShadeSH9方法取得SH光,这个方法需要传入一个float4(normal_dir, 1.0),注意传入的第四维是1.0,这算一个特殊用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 Pass
{
Tags { "LightMode"="ForwardBase"}//+++
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase//+++

#include "AutoLight.cginc"//+++
#include "UnityCG.cginc"

//....
half4 frag (v2f i) : SV_Target
{
//...

half3 env_color = ShadeSH9(float4(normal_dir, 1.0));
half3 final_color = env_color * ao * _Tint.rgb * _Expose;

return half4(final_color, 1.0);
}
ENDCG
}

在FrameDebugger中查看默认SH值

在FrameDebugger的Drawing——Render.OpaqueGeometry——RenderForwardOpaque.Render——RenderForward.RenderLoopJob——Draw Mesh 4_LightProbe中可以查看到。

如何控制Unity提供的默认Light Probe

在Lighting面板中Environment选项卡里设置Environment Lighting即可。或者修改天空盒的材质属性后再次点击Generate Light。

创建Light Probe Group

灯光探针是一种动态漫反射光照的解决方案。

在Hierarchy内选择Light——Light Probe Group(不需要是对象的子级),然后在Inspector面板中选择Edit Light Probes,在Scene中框选Probe,Ctrl + D复制一些出来并排列好。

创建几盏点光,并且改为Bake模式。放在探针中间。

进入Lighting面板,进入Scene选项卡,点击New Light Settings,在Mixed Lighting里面,勾选Baked Global Illumination,这时点击Generate Lighting就能将点光信息烘焙进Light Probe中。