环境贴图技术

环境贴图技术

四月 01, 2022

大纲

在这里插入图片描述

环境贴图

请添加图片描述

立方体贴图采样如上

采样缺陷

当同一角度,不同位置看过去会得到同样采样,所以不适合平面采样,解决方案后续说。

在这里插入图片描述

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
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.normal_world = normalize(mul((v.normal),unity_WorldToObject).xyz);
o.tangent_world = normalize(mul((v.tangent),unity_WorldToObject).xyz);
o.binormal_world=normalize(cross(o.normal_world,o.tangent_world) * v.tangent.w);
o.pos_world = normalize(mul((v.vertex),unity_WorldToObject).xyz);
o.uv = v.uv*_NormalMap_ST.xy+_NormalMap_ST.zw;

return o;
}

fixed4 frag (v2f i) : SV_Target
{
// sample the texture
//fixed4 base_col = tex2D(_MainTex, i.uv);
fixed4 base_col = fixed4(0,0,0,0);
half3 normaldata = UnpackNormal( tex2D(_NormalMap, i.uv));
half3 normal_world = normalize(i.normal_world);
half3 tangent_world = normalize(i.tangent_world);
half3 binormal_world = normalize(i.binormal_world);
normal_world=mul(normaldata, float3x3(tangent_world,binormal_world,normal_world));

half3 view_dir = normalize(_WorldSpaceCameraPos.xyz-i.pos_world.xyz);


half3 reflect_dir =reflect(-view_dir,normal_world);
half4 finish_col = base_col;

half4 Cube_col=texCUBE(_CubeMap,reflect_dir);
half3 CubeHDR_col=DecodeHDR(Cube_col,_CubeMap_HDR);
finish_col.rgb+=CubeHDR_col;
return finish_col;
}

IBL 基于图像的光照技术

预计算卷积

通过模糊,得到不同层次的mip Map,便于模拟相应的磨砂层次。

1
half4 Cube_col=texCUBElod(_CubeMap,float4(reflect_dir,_Roughness))

粗糙度变成非线性

1
roughness=roughness*(1.7-0.7*roughness);

反射探针

1
half4 Cube_col = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0,reflect_dir,_Roughness);

为了用反射探针

UNITY_SAMPLE_TEXCUBE_LOD(反射贴图,反射向量,mip)

UNITY_SAMPLE_TEXCUBE(反射贴图,反射向量)

反射探针贴图

unity_SpecCube0

SH球谐光照

造轮子

shader

将光照信息记录下来,实现间接光照。

属性

1
2
3
4
5
6
7
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)

片元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
float4 normalForSH = float4(normal_dir, 1.0);
//SHEvalLinearL0L1
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;

部分函数可以放在顶点着色器中

获取属性工具

SphericalHarmonicsCoefficient

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;
}
}

CubemapSHProjector

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
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);
}
}

Unity内置函数

换上小皮肤

1
2
3
4
5
6
7
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "AutoLight.cginc"
#include "UnityCG.cginc"

片元

1
half3 env_color = ShadeSH9(float4(normal_dir,1.0));

完成

光照探测器

光照探测器的小球记录下问题的球谐数值,然后物体在范围内时,用物体周围小球的球谐数值做插值。