对于大部分透明物体(牛奶、大理石等)用一个Dipole Profile就够了,但是对于皮肤这种拥有多层结构的材质,用一个Dipole Profile不能达到理想的效果,可以通过3个Dipole接近Jensen论文中的根据测量得出的皮肤Profile数据。
实验发现,3个Dipole曲线可通过以下6个高斯函数拟合得到(具体的拟合推导过程参见:《GPU Gems 3》:真实感皮肤渲染技术总结):
\[
\begin{eqnarray}
R(r) &=& 0.233\cdot G(0.0064,r) + 0.1\cdot G(0.0484,r) + 0.118\cdot G(0.187,r) \\
&+& 0.113\cdot G(0.567,r) + 0.358\cdot G(1.99,r) + 0.078\cdot G(7.41,r)
\end{eqnarray}
\]
上述公式是红通道Red的模拟,绿通道Green和蓝通道Blue的参数不一样,见下表:
R、G、B通道拟合出的曲线有所不同(下图),可见R通道曲线的扩散范围最远,这也是皮肤显示出红色的原因。
2.3.2.2 源码分析首先分析SeparableSSS_Gaussian:
// 这个就是上一小节提到的G(v,r)的高斯函数,增加了FalloffColor颜色,对应不同颜色通道的值。 inline FVector SeparableSSS_Gaussian(float variance, float r, FLinearColor FalloffColor) { FVector Ret; // 对每个颜色通道做一次高斯函数技术 for (int i = 0; i < 3; i++) { float rr = r / (0.001f + FalloffColor.Component(i)); Ret[i] = exp((-(rr * rr)) / (2.0f * variance)) / (2.0f * 3.14f * variance); } return Ret; }再分析SeparableSSS_Profile:
// 天啦噜,这不正是上一小节提到的通过6个高斯函数拟合得到3个dipole曲线的公式么?参数一毛一样有木有? // 其中r是次表面散射的最大影响距离,单位是mm,可由UE编辑器的Subsurface Profile界面设置。 inline FVector SeparableSSS_Profile(float r, FLinearColor FalloffColor) { // 需要注意的是,UE4将R、G、B通道的参数都统一使用了R通道的参数,它给出的理由是FalloffColor已经包含了不同的值,并且方便模拟出不同肤色的材质。 return // 0.233f * SeparableSSS_Gaussian(0.0064f, r, FalloffColor) + // UE4屏蔽掉了第一个高斯函数,理由是这个是直接反射光,并且考虑了strength参数。(We consider this one to be directly bounced light, accounted by the strength parameter) 0.100f * SeparableSSS_Gaussian(0.0484f, r, FalloffColor) + 0.118f * SeparableSSS_Gaussian(0.187f, r, FalloffColor) + 0.113f * SeparableSSS_Gaussian(0.567f, r, FalloffColor) + 0.358f * SeparableSSS_Gaussian(1.99f, r, FalloffColor) + 0.078f * SeparableSSS_Gaussian(7.41f, r, FalloffColor); }接着分析如何利用上面的接口进行离线计算Kernel的权重:
// 由于高斯函数具体各向同性、中心对称性,所以横向卷积和纵向卷积一样,通过镜像的数据减少一半计算量。 void ComputeMirroredSSSKernel(FLinearColor* TargetBuffer, uint32 TargetBufferSize, FLinearColor SubsurfaceColor, FLinearColor FalloffColor) { check(TargetBuffer); check(TargetBufferSize > 0); uint32 nNonMirroredSamples = TargetBufferSize; int32 nTotalSamples = nNonMirroredSamples * 2 - 1; // we could generate Out directly but the original code form SeparableSSS wasn't done like that so we convert it later // .A is in mm check(nTotalSamples < 64); FLinearColor kernel[64]; { // 卷积核时先给定一个默认的半径范围,不能太大也不能太小,根据nTotalSamples数量调整Range是必要的。(单位是毫米mm) const float Range = nTotalSamples > 20 ? 3.0f : 2.0f; // tweak constant const float Exponent = 2.0f; // Calculate the offsets: float step = 2.0f * Range / (nTotalSamples - 1); for (int i = 0; i < nTotalSamples; i++) { float o = -Range + float(i) * step; float sign = o < 0.0f ? -1.0f : 1.0f; // 将当前的range和最大的Range的比值存入alpha通道,以便在shader中快速应用。 kernel[i].A = Range * sign * FMath::Abs(FMath::Pow(o, Exponent)) / FMath::Pow(Range, Exponent); } // 计算Kernel权重 for (int32 i = 0; i < nTotalSamples; i++) { // 分别取得i两边的.A值做模糊,存入area float w0 = i > 0 ? FMath::Abs(kernel[i].A - kernel[i - 1].A) : 0.0f; float w1 = i < nTotalSamples - 1 ? FMath::Abs(kernel[i].A - kernel[i + 1].A) : 0.0f; float area = (w0 + w1) / 2.0f; // 将模糊后的权重与6个高斯函数的拟合结果相乘,获得RGB的最终权重。 FVector t = area * SeparableSSS_Profile(kernel[i].A, FalloffColor); kernel[i].R = t.X; kernel[i].G = t.Y; kernel[i].B = t.Z; } // 将offset为0.0(即中心采样点)的值移到位置0. FLinearColor t = kernel[nTotalSamples / 2]; for (int i = nTotalSamples / 2; i > 0; i--) { kernel[i] = kernel[i - 1]; } kernel[0] = t; // 规范化权重,使得权重总和为1,保持颜色能量守恒. { FVector sum = FVector(0, 0, 0); for (int i = 0; i < nTotalSamples; i++) { sum.X += kernel[i].R; sum.Y += kernel[i].G; sum.Z += kernel[i].B; } for (int i = 0; i < nTotalSamples; i++) { kernel[i].R /= sum.X; kernel[i].G /= sum.Y; kernel[i].B /= sum.Z; } } /* we do that in the shader for better quality with half res // Tweak them using the desired strength. The first one is: // lerp(1.0, kernel[0].rgb, strength) kernel[0].R = FMath::Lerp(1.0f, kernel[0].R, SubsurfaceColor.R); kernel[0].G = FMath::Lerp(1.0f, kernel[0].G, SubsurfaceColor.G); kernel[0].B = FMath::Lerp(1.0f, kernel[0].B, SubsurfaceColor.B); for (int i = 1; i < nTotalSamples; i++) { kernel[i].R *= SubsurfaceColor.R; kernel[i].G *= SubsurfaceColor.G; kernel[i].B *= SubsurfaceColor.B; }*/ } // 将正向权重结果输出到TargetBuffer,删除负向结果。 { check(kernel[0].A == 0.0f); // center sample TargetBuffer[0] = kernel[0]; // all positive samples for (uint32 i = 0; i < nNonMirroredSamples - 1; i++) { TargetBuffer[i + 1] = kernel[nNonMirroredSamples + i]; } } }