次表面散射

10k words

Desc:

皮肤渲染的渲染过程可由两个分量组成:
镜面反射+次表面散射

镜面反射项(specular reflection) 相对而言很简单,Gems 3中推荐Kelemen and Szirmay-Kalos specular BRDF用于皮肤镜面反射项的计算。因为Kelemen and Szirmay-Kalos specular BRDF在实现和Torrance-Sparrow模型一样的渲染效果时,计算量要小得多。而现阶段基于物理的一些其他高光模型或改进方案也应该会得到不错的效果

次表面散射麻烦很多

总纲

现在学会的方式:

预积分的皮肤渲染(Pre-Integrated Skin Rendering)

来源
实现效果
Image text

贴图生成

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

[MenuItem("Tools/Texture/CreateCurveTexture")]
public static void CreateCurveTexture()
{
Mesh mesh;
Material material;
try
{
mesh = Selection.activeGameObject.GetComponent<SkinnedMeshRenderer>().sharedMesh;
material = UnityEngine.Object.Instantiate(Selection.activeGameObject.GetComponent<SkinnedMeshRenderer>().sharedMaterial);
}
catch(Exception e)
{
Debug.Log(e);
return;
}

Texture texture = material.mainTexture;
RenderTexture renderTexture = new RenderTexture(texture.width, texture.height, 0, RenderTextureFormat.ARGB32);
Graphics.SetRenderTarget(renderTexture);
material.shader = Shader.Find("SSS/SkinBake");
material.SetFloat("_CurveFactor", 3.2f);
material.SetPass(0);
Graphics.DrawMeshNow(mesh, Matrix4x4.identity);
RenderTexture renderTexture1 = new RenderTexture(texture.width, texture.height, 0, RenderTextureFormat.ARGB32);
Graphics.SetRenderTarget(renderTexture1);
Graphics.Blit(renderTexture, material, 1);

Texture2D texture2D = new Texture2D(texture.width, texture.height, TextureFormat.ARGB32, false);
texture2D.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0);
texture2D.Apply();

System.IO.File.WriteAllBytes("Assets/CreateOutput/curve.png", texture2D.EncodeToPNG());
renderTexture.Release();
renderTexture1.Release();
AssetDatabase.Refresh();
}

[MenuItem("Tools/Texture/CreateLUTTexture")]
public static void CreateLUT()
{
Mesh mesh;
Material material;
try
{
mesh = Selection.activeGameObject.GetComponent<SkinnedMeshRenderer>().sharedMesh;
material = UnityEngine.Object.Instantiate(Selection.activeGameObject.GetComponent<SkinnedMeshRenderer>().sharedMaterial);
}
catch (Exception e)
{
Debug.Log(e);
return;
}

Texture texture = material.mainTexture;
RenderTexture renderTexture = new RenderTexture(texture.width, texture.height, 0, RenderTextureFormat.ARGB32);
Graphics.SetRenderTarget(renderTexture);
material.shader = Shader.Find("SSS/LUTSkin");
material.SetPass(0);
Graphics.DrawMeshNow(mesh, Matrix4x4.identity);
RenderTexture renderTexture1 = new RenderTexture(texture.width, texture.height, 0, RenderTextureFormat.ARGB32);
Graphics.SetRenderTarget(renderTexture1);
Graphics.Blit(renderTexture, material);

Texture2D texture2D = new Texture2D(texture.width, texture.height, TextureFormat.ARGB32, false);
texture2D.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0);
texture2D.Apply();

System.IO.File.WriteAllBytes("Assets/CreateOutput/lut.png", texture2D.EncodeToPNG());
renderTexture.Release();
renderTexture1.Release();
AssetDatabase.Refresh();
}

[MenuItem("Tools/Texture/CreateKSTexture")]
public static void CreateKSTexture()
{
Mesh mesh;
Material material;
try
{
mesh = Selection.activeGameObject.GetComponent<MeshFilter>().sharedMesh;
material = UnityEngine.Object.Instantiate(Selection.activeGameObject.GetComponent<MeshRenderer>().sharedMaterial);
}
catch (Exception e)
{
Debug.Log(e);
return;
}

Texture texture = material.mainTexture;
RenderTexture renderTexture = new RenderTexture(texture.width, texture.height, 0, RenderTextureFormat.ARGB32);
Graphics.SetRenderTarget(renderTexture);
material.shader = Shader.Find("SSS/KSTextureBake");
material.SetPass(0);
Graphics.DrawMeshNow(mesh, Matrix4x4.identity);
RenderTexture renderTexture1 = new RenderTexture(texture.width, texture.height, 0, RenderTextureFormat.ARGB32);
Graphics.SetRenderTarget(renderTexture1);
Graphics.Blit(renderTexture, material);

Texture2D texture2D = new Texture2D(texture.width, texture.height, TextureFormat.ARGB32, false);
texture2D.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0);
texture2D.Apply();

System.IO.File.WriteAllBytes("Assets/CreateOutput/KsTexture.png", texture2D.EncodeToPNG());
renderTexture.Release();
renderTexture1.Release();
AssetDatabase.Refresh();
}

效果

1
2
3
4
5
6
if(_IsSSS){
curve = length(fwidth(N)) / (length(fwidth(lightData.positionWS)) * _CurveFactor);
curve=saturate(curve);
sssColor=tex2D(_SSSLUTTex,float2(HL,curve));
litOrShadowColor=lerp(sssColor*_SSSScale+_ShadowMapColor,1,litOrShadowArea);
}

可分离的次表面散射(SSSS , Separable Subsurface Scattering)

来源
Image text
知乎作者
link
网易雷火工作室

卷积分离的优化方法,把横向坐标U和纵向坐标V分开卷积,再做合成
为了给实时渲染加速,还需要预积分分离的卷积核,利用奇异值分离的方法将其分解成一个行向量和列向量

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
两个通道

shader:

#define DistanceToProjectionWindow 5.671281819617709 //1.0 / tan(0.5 * radians(20));
#define DPTimes300 1701.384545885313 //DistanceToProjectionWindow * 300
#define SamplerSteps 25
uniform sampler2D _CameraDepthTexture;
float4 _CameraDepthTexture_TexelSize;

float4 SSS(float4 SceneColor, float2 UV, float2 SSSIntencity) {
float SceneDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, UV));
float BlurLength = DistanceToProjectionWindow / SceneDepth;
float2 UVOffset = SSSIntencity * BlurLength;
float4 BlurSceneColor = SceneColor;
BlurSceneColor.rgb *= _Kernel[0].rgb;

[loop]
for (int i = 1; i < SamplerSteps; i++) {
float2 SSSUV = UV + _Kernel[i].a * UVOffset;
float4 SSSSceneColor = tex2D(_MainTex, SSSUV);
float SSSDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, SSSUV)).r;
float SSSScale = saturate(DPTimes300 * SSSIntencity * abs(SceneDepth - SSSDepth));
SSSSceneColor.rgb = lerp(SSSSceneColor.rgb, SceneColor.rgb, SSSScale);
BlurSceneColor.rgb += _Kernel[i].rgb * SSSSceneColor.rgb;
}
return BlurSceneColor;
}

float4 frag(VertexOutput i) : SV_TARGET {
float4 SceneColor = tex2D(_MainTex, i.uv);
float SSSIntencity = (_SSSScale * _CameraDepthTexture_TexelSize.x);
float3 XBlur = SSS(SceneColor, i.uv, float2(SSSIntencity, 0) ).rgb;
return float4(XBlur, SceneColor.a);
}

float4 frag(VertexOutput i) : COLOR {
float4 SceneColor = tex2D(_MainTex, i.uv);
float SSSIntencity = (_SSSScale * _CameraDepthTexture_TexelSize.y);
float3 YBlur = SSS(SceneColor, i.uv, float2(0, SSSIntencity)).rgb;
return float4(YBlur, SceneColor.a);
}


cs:


public static class SeparableSSS {
public static void CalculateKernel(List<Vector4> kernel, int nSamples, Vector3 strength, Vector3 falloff){
float RANGE = nSamples > 20 ? 3.0f : 2.0f;
float EXPONENT = 2.0f;
kernel.Clear();

// Calculate the SSS_Offset_UV:
float step = 2.0f * RANGE / (nSamples - 1);
for (int i = 0; i < nSamples; i++){
float o = -RANGE + i * step;
float sign = o < 0.0f ? -1.0f : 1.0f;
float w = RANGE * sign *Mathf.Abs(Mathf.Pow(o, EXPONENT)) / Mathf.Pow(RANGE, EXPONENT);
kernel.Add(new Vector4(0, 0, 0, w));
}
// Calculate the SSS_Scale:
for (int i = 0; i < nSamples; i++){
float w0 = i > 0 ? Mathf.Abs(kernel[i].w - kernel[i - 1].w) : 0.0f;
float w1 = i < nSamples - 1 ? Mathf.Abs(kernel[i].w - kernel[i + 1].w) : 0.0f;
float area = (w0 + w1) / 2.0f;
Vector3 temp = profile(kernel[i].w, falloff);
Vector4 tt = new Vector4(area * temp.x, area * temp.y, area * temp.z, kernel[i].w);
kernel[i] = tt;
}
Vector4 t = kernel[nSamples / 2];
for (int i = nSamples / 2; i > 0; i--)
kernel[i] = kernel[i - 1];
kernel[0] = t;
Vector4 sum = Vector4.zero;

for (int i = 0; i < nSamples; i++){
sum.x += kernel[i].x;
sum.y += kernel[i].y;
sum.z += kernel[i].z;
}

for (int i = 0; i < nSamples; i++){
Vector4 vecx = kernel[i];
vecx.x /= sum.x;
vecx.y /= sum.y;
vecx.z /= sum.z;
kernel[i] = vecx;
}

Vector4 vec = kernel[0];
vec.x = (1.0f - strength.x) * 1.0f + strength.x * vec.x;
vec.y = (1.0f - strength.y) * 1.0f + strength.y * vec.y;
vec.z = (1.0f - strength.z) * 1.0f + strength.z * vec.z;
kernel[0] = vec;

for (int i = 1; i < nSamples; i++){
var vect = kernel[i];
vect.x *= strength.x;
vect.y *= strength.y;
vect.z *= strength.z;
kernel[i] = vect;
}
}


private static Vector3 gaussian(float variance, float r, Vector3 falloff){
Vector3 g;

float rr1 = r / (0.001f + falloff.x);
g.x = Mathf.Exp((-(rr1 * rr1)) / (2.0f * variance)) / (2.0f * 3.14f * variance);

float rr2 = r / (0.001f + falloff.y);
g.y = Mathf.Exp((-(rr2 * rr2)) / (2.0f * variance)) / (2.0f * 3.14f * variance);

float rr3 = r / (0.001f + falloff.z);
g.z = Mathf.Exp((-(rr3 * rr3)) / (2.0f * variance)) / (2.0f * 3.14f * variance);

return g;
}
private static Vector3 profile(float r, Vector3 falloff){
return 0.100f * gaussian(0.0484f, r, falloff) +
0.118f * gaussian(0.187f, r, falloff) +
0.113f * gaussian(0.567f, r, falloff) +
0.358f * gaussian(1.99f, r, falloff) +
0.078f * gaussian(7.41f, r, falloff);
}
}

///SSS Color
Vector3 SSSC = Vector3.Normalize(new Vector3 (SubsurfaceColor.r, SubsurfaceColor.g, SubsurfaceColor.b));
Vector3 SSSFC = Vector3.Normalize(new Vector3 (SubsurfaceFalloff.r, SubsurfaceFalloff.g, SubsurfaceFalloff.b));
SeparableSSS.CalculateKernel(KernelArray, 25, SSSC, SSSFC);
SubsurfaceEffects.SetVectorArray (Kernel, KernelArray);
SubsurfaceEffects.SetFloat (SSSScaler, SubsurfaceScaler);
///SSS Buffer
SubsurfaceBuffer.Clear();
SubsurfaceBuffer.GetTemporaryRT (SceneColorID, RenderCamera.pixelWidth, RenderCamera.pixelHeight, 0, FilterMode.Trilinear, RenderTextureFormat.DefaultHDR);

SubsurfaceBuffer.BlitStencil(BuiltinRenderTextureType.CameraTarget, SceneColorID, BuiltinRenderTextureType.CameraTarget, SubsurfaceEffects, 0);
SubsurfaceBuffer.BlitSRT(SceneColorID, BuiltinRenderTextureType.CameraTarget, SubsurfaceEffects, 1);

总结

皮肤渲染过程可以由两个分量组成:镜面反射、次表面散射

镜面反射

Kelemen and Szirmay-Kalos specular BRDF在实现和Torrance-Sparrow模型一样的渲染效果时,计算量要小得多

Torrance-Sparrow

Kelemem and Szirmay-Kalos specular BRDF

基于物理其他高光模型

次表面散射部分

扩散剖面

扩散剖面(diffusion profile)是描述光线如何在半透明物体中进行扩散和分布的函数。link
与扩散剖面常一起出现的三种方法:

偶极子(Dipole)方法

多级子(Multipole)方法

高斯和(Sum-of-Gaussians)方法

它们都是更好地描述出扩散剖面(Diffusion Profiles)的一些策略。得到扩散剖面(diffusion profile),即光线是如何在半透明物体中进行扩散和分布之后,接下来就可以对附近的像素进行加权求和,以模拟次表面散射的效果。这个求和的过程其实是根据扩散剖面(diffusion profile)的指导,对周围像素进行模糊操作

按照操作空间划分,常规的思路:
· 纹理空间模糊
· 屏幕空间模糊 (sssss)

其他皮肤渲染技术

半透明阴影贴图 (TSMS)

路径跟踪次表面散射 (Path-Traced Subsurface Scattering) 与光线步进 (Ray Marching)

Deferred Single Scattering

发展史

• 次表面光照传输模型(Subsurface Light Transport, SSLT)[2001]
• 扩散剖面(Diffusion Profile) [2001]
• 偶极子(dipole) [2001]
• 纹理空间模糊(Texture Space Blur)[2003]
• 多极子(multipole) [2005]
• 屏幕空间模糊(Screen Space Blur)或屏幕空间次表面散射(SSSSS,Screen Space SubSurface Scattering)[2009]
• 路径追踪次表面散射(Path-Traced Subsurface Scattering)与光线步进(Ray Marching)[2009]
• 预积分的皮肤着色(Pre-Integrated Skin Shading)[2010]
• 可分离的次表面散射(SSSS,Separable Subsurface Scattering)[2015]
link

链接

推导