毛发渲染总结

毛发的渲染技法有很多。我们主要讨论模型和着色的方案。

一.毛发模型方案

1.1 Shell Based Fur

这类毛发可以参考GFur Pro插件。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210602145811.png

https://www.unrealengine.com/marketplace/zh-CN/product/gfur-pro

这类毛发技术使用的是多层渲染。如下图所示,只要这些面片距离足够的近,我们就有一种是单根头发的错觉。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210602150050.png

这类技术的问题是如果我们需要长的毛发,那么我们就需要非常多的截面,性能受到的影响就越大。

当然我们并不是一个面片一个面片进行绘制,密集毛发是通过在单个多边形上绘制多个毛发横截面来实现的。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210602150357.png

如果我们以一个球体为例,我们想用毛皮覆盖它,我们需要复制球体并稍微膨胀它的表面,以便我们得到第一层,我们在其上绘制毛皮的第一个横截面。然后我们继续添加更多层,每一层都略高于另一层,直到我们到达毛皮的预期尖端。这些层也被称为Shell,因此得名这项技术。

我们可以在顶点着色器中移动shell的顶点。 https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210602150455.png

1.2 面片(card)与Mesh

传统而言,虽然存在个体差异,但是人体发量大体在10w左右。使用真实意义上的strands去渲染对性能影响巨大。因此划分面片或者Mesh 的操作将是一种妥协式的解决方案。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210602112848.png

Epic放出的老的数字人的demo和Paragon都是使用的这种方案,只不过在面片的精度上Paragon要稍微低一点。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210602141946.png

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210602174600.png

《最后生还者》即采用了这种方式来模拟头发。

根据画面风格的不同,我们将选取Mesh或者Card的方式进行。针对偏向卡通风格可能更适合Mesh,而写实风格则使用面片。

1.3 HairWorks&TressFX

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210602113337.png

英伟达的HairWorks,最早的Hairwork用于2013年的《使命召唤:幽灵》上,用于模拟剧中那种狗狗“莱利”的毛发。虽然从效果上看一般,和《古墓丽影》中的柔丝顺滑相比视觉上并不吸引眼球。后来又用在了又叫好又叫座的惊世大作《巫师3:狂猎》上。HairWorks 基于曲面细分。对HairWorks的官方支持在虚幻引擎4.16版就停止了。现在已经开源。

AMD的TressFX,开源。2013年新版《古墓丽影》第一个引入了这种技术。

1.4 Strand

sig19,frostbite提出的基于strand的方式以及进一步处理single/multiple scattering。虚幻在4.24提供的Groom作为解决方案。4.26放出的MeerkatDemo里面,也使用了这个技术。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210602152116.png

1.5其他

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210602113351.png

二.着色模型

2.1 kajiya-kay(1989)

kajiya是基于经验的着色模型,也就是并不是完全的着色物理正确。不过对于大多数硬件性能较低的平台也算够用。

在Kajiya-Kay模型中,头发纤维被抽象化为一个不透明的圆柱体,不能够透射和产生内部反射。因此,Kajiya-Kay模型不能表现一些肉眼观察到的头发效果,同时也是能量不守恒的。在kajiya中,光照主要分为diffuse和Specular。

$$ Color=Diffuse+Specular $$

其中Diffuse使用Lambert,而Specular使用Phong。如下图所示黄色的粗圆柱表示一根头发,T是其切线,V是视线,L是光源方向, H是L和V之间夹角的一半。 https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210602161216.png

我们首先看一下diffuse 的计算。

$$ Diffuse=K_d sin(T,L) $$

这里可以看出,其跟Lambert并不是完全一致。

这是因为我们可以把这种微观的细丝看成直径非常小长度很长的圆柱,那么我们宏观上看到的这类表面上某一点的光照实际上是一圈圆柱上的点的光照的总和,这一圈点有无数个方向不同的法线,因此需要对这一圈点的入射光和BRDF的乘积进行一个半圆形的积分。

因此,我们在某一个点的漫反射是这个点的的法线半球上的积分,但是对于头发来说,我们计算的是这个点的切面半圆上的积分。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210602163303.png

可以很明显看出来高光变成了环形,且有发丝的感觉,纵向上处于同一圈内的点可以当做是一根头发,计算出每根头发的高光效果,纵向环绕一圈后自然就成为了圆环。

$$ Specular=K_d sin(T,H)^{specularity} $$

同样的,我们的在Phong模型中,对于一个点,我们使用的是镜面反射方向。但是在Kajiya中,光照击中头发之后,我们求的依旧是切面的所有反射方向。反射方向是沿着切线而不是法线以镜面反射角度射出的,这样所有的反射光线均位于以切线为轴线,角度为θ(光照与切线之间的夹角)的圆锥体上。我们数学推导后的积分计算就是上述式子。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210602164726.png

Kajiya-Kay模型存在的主要问题是,它不是基于物理的,并且它将头发建模成不透明的圆柱体,因此不能模拟光线可能穿过头发或者在头发中传播的情况,这就导致了其不能模拟出背光以及二次高光等效果。

2.2 Marschner(siggraph03)

Marschner模型是除Kajiya-Kay模型外另一个应用广泛的头发着色模型。UE4 在Groom之前的中的头发渲染(Paragon)就采用了Marschner模型。Marschner模型则是基于对头发纤维的物理分析,归纳推导出来的模型。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210602165615.png

头发是十分透明的(黑色除外),因此Marschner模型和Kajiya-Kay模型不同的一点就是,Marschner模型将头发纤维抽象为一个透明的椭圆柱体。其最主要的三个光路如下: https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210602171652.png

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210602171702.png

R :光线到达头发纤维角质层直接被反射。 https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603104306.png

TRT:光线透射角质层进入皮层,角质层内层折射,又透射到空气中。可以知道,出射点距离入射点已经产生了偏移。 https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603104422.png

TT:光线透射进入皮层,又从中直接透射出去 https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603104400.png

我们当然还可以继续进行更多的内部反弹,但这些通常是相当微弱,因为大部分能量都被吸收掉了。

因此,我们的公式可以写为: $$ S=S_R+S_{TT}+S_{TRT} $$

并且我们的S方程可以写为允许我们把S方程以写作横轴和方位散射函数N,纵向散射函数M的组合: https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603110718.png

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603110651.png

总结:我们对头发着色时,对于每个像素P都有R,TT,TRT 三项。然后把每项分解成 M 和 N,其中 M 项是纵切面的散射分布(Longitudinal scattering),N 项是横切面的散射分布(Azimuthal scattering)。

由于Marschner极其的复杂,实时的实现基本无法使用,因此我们有了之后的尝试。

2.3 UE4对Marschner的拟合

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603152933.png

Epic在Siggraph 2016 的Physically Based Hair Shading in Unreal发布了新一代的PBR毛发着色模型。将Marschner做了一些近似逼近,能realtime展现的比较好。

M项的逼近

不论是R,TT,还是TRT,其M项都类似的,使用高斯分布来进行替代。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603111744.png

其中Alpha一个Shift。R ,TT和TRT分别使用不同的Shift值 。

1
2
3
4
5
6
7
	float Shift = 0.035;
	float Alpha[] =
	{
		-Shift * 2,
		Shift,
		Shift * 4,
	};	

其中Beta与Roughness相关。

1
2
3
4
5
6
	float B[] =
	{
		Area + Pow2(ClampedRoughness),
		Area + Pow2(ClampedRoughness) / 2,
		Area + Pow2(ClampedRoughness) * 2,
	};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

float Hair_g(float B, float Theta)
{
	return exp(-0.5 * Pow2(Theta) / (B * B)) / (sqrt(2 * PI) * B);
}
//R
float Mp = Hair_g(B[0] * BScale, SinThetaL + SinThetaV - Shift);
// TT
float Mp = Hair_g( B[1], SinThetaL + SinThetaV - Alpha[1] );
//TRT
float Mp = Hair_g( B[2], SinThetaL + SinThetaV - Alpha[2] );

R Path的逼近

对于R Path 的N项,直接使用的菲涅尔项和普通镜面反射相乘。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603113441.png

1
2
3
4
5
6
7
8
	const float sa = sin(Alpha[0]);
	const float ca = cos(Alpha[0]);
	float Shift = 2 * sa * (ca * CosHalfPhi * sqrt(1 - SinThetaV * SinThetaV) + sa * SinThetaV);
	float BScale = HairTransmittance.bUseSeparableR ? sqrt(2.0) * CosHalfPhi : 1;
	float Mp = Hair_g(B[0] * BScale, SinThetaL + SinThetaV - Shift);
	float Np = 0.25 * CosHalfPhi;
	float Fp = Hair_F(sqrt(saturate(0.5 + 0.5 * VoL)));
	S += Mp * Np * Fp * (GBuffer.Specular * 2) * lerp(1, Backlit,saturate(-VoL));

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603114420.png

TT Path的逼近

R Path的N项过于简单了,但是其他Path 的N项并没有这么简单。分为衰减因子A和分布D,A用于解释毛发中的菲涅耳反射和吸收,分布D用于模拟光在反射或离开纤维时如何散射。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603145615.png

TT Path 的A和D是很复杂的。 https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603142448.png

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603143327.png

我们可以看出,这里的计算量非常的大。我们要对他进行简化。我们需要了解h,这是纤维中心的偏移量,Eta 是修正折射率。我们需要简化这个运算。首先我们近似得到h!图中黄黑两条曲线是实际和近似的图形化表示。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603143403.png

然后近似拿到Eta。如果我们将原始的折射率固定在1.55。注意到cosθd的函数看起来非常像1/x。添加一个线性项使拟合效果更好。调整常量,直到最大相对误差较低。或者,用x代替cosθd的分子的泰勒级数。前两项的形式相同,但常数略有不同。我这里的常数更合适。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603143637.png

于是我们拿到了T的近似表示:

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603143849.png

然后我们改进D函函数。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603144003.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
		float Mp = Hair_g( B[1], SinThetaL + SinThetaV - Alpha[1] );

		float a = 1 / n_prime;
		float h = CosHalfPhi * ( 1 + a * ( 0.6 - 0.8 * CosPhi ) );
		float f = Hair_F( CosThetaD * sqrt( saturate( 1 - h*h ) ) );
		float Fp = Pow2(1 - f);
		float3 Tp = 0;
		if (HairTransmittance.bUseLegacyAbsorption)
		{
			Tp = pow(GBuffer.BaseColor, 0.5 * sqrt(1 - Pow2(h * a)) / CosThetaD);
		}
		else
		{
			const float3 AbsorptionColor = HairColorToAbsorption(GBuffer.BaseColor);
			Tp = exp(-AbsorptionColor * 2 * abs(1 - Pow2(h * a) / CosThetaD));
		}
		float Np = exp( -3.65 * CosPhi - 3.98 );

		S += Mp * Np * Fp * Tp * Backlit;

TRT Path的逼近

我们对TRT,随便搞搞就行。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603144259.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
		float Mp = Hair_g( B[2], SinThetaL + SinThetaV - Alpha[2] );
		
		//float h = 0.75;
		float f = Hair_F( CosThetaD * 0.5 );
		float Fp = Pow2(1 - f) * f;
		//float3 Tp = pow( GBuffer.BaseColor, 1.6 / CosThetaD );
		float3 Tp = pow( GBuffer.BaseColor, 0.8 / CosThetaD );

		//float s = 0.15;
		//float Np = 0.75 * exp( Phi / s ) / ( s * Pow2( 1 + exp( Phi / s ) ) );
		float Np = exp( 17 * CosPhi - 16.78 );
		S += Mp * Np * Fp * Tp;

Multiple scattering

尤其是对于浅色头发,因为吸收率低,因此多次散射非常重要。与在单个纤维中的单次散射不同,多次散射试图模拟光在多个光纤中传播时的效果。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603153116.png

这意味着我们需要评估光在光源和相机之间传播的多条路径。这对于实时渲染来说当然是不可行的,所以我们也需要近似这种效果。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603154816.png

首先使用了假的Normal,散射近似的其余部分是Lambert,以及一个基于通过头发体积的直接光路径长度的吸收。这个路径长度是由指数阴影值推导出来的。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603153652.png

完全是假的,不是基于物理的!

2.4 Frostbite对Marschner的拟合

寒霜也是对Marschner拟合改进。并且在PPT中不停跟UE4的效果做了对比

M项

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603145528.png

R Path 的N项

R path 的N项跟UE4的类似。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603150156.png

对于TT的A项。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603150444.png

通过分析传输的衰减项,我们可以注意到主要贡献来自直接通过纤维中间传输的光。也就是h参数为0时所代表的。如果我们比较仅在h0处计算的衰减值和完整积分,我们可以看到这实际上是一个非常好的近似值。也就是说Frostbite认为在h0处的拟合更准确更为重要。

这是一个曲线图,显示了三个不同吸收值的近似值,参考积分画为十字,近似值画为实线。 右图显示UE4的近似值是如何叠加的,我们可以看到它在某些方面有一些问题,特别是在更透明、更明亮的头发上。

对于TT的D项。使用LUT,也就是其取决于 roughness, azimuthal outgoing angle and the longitudinal outgoing angle 。是个三维的。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603151319.png

但没有存储整个3D纹理,而是通过对每个方位角切片拟合高斯函数,将其重新参数化为2D。 因此,参数a和b,在高斯中,被拟合到积分中,然后我们将它们存储在一个双通道2D纹理中。 https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603151458.png

TRT Path 的N项

TRT随便搞搞就行。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603151553.png

Multiple scattering

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603155032.png

在Frostbite的实现中,使用一种称为双重散射的近似。双重散射的要点是将多重散射近似为两个分量的组合。局部散射和全局散射。

  1. 局部散射是指在阴影点附近的散射,并导致大量的可见头发的染色。
  2. 全局散射是为了获得通过头发体积的外部光线的效果。
  3. 双重散射近似对头发有效的原因是因为大多数光只在向前方向散射。所以基本上是因为TT比TRT贡献更多。通过只考虑沿阴影路径或光方向的散射来估计全局散射。

因此,我们需要某种方法来估计光照方向头发体积中两点之间的头发数量。其使用Deep Opacity Maps。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603155415.png

Deep Opacity Maps类似于Opacity shadow maps,这是一种将体积对象的阴影贴图生成为对象上的许多切片的技术。不过它不是线性的分层,而是根据一张Front Depth Texture来分层,统计每层之间有多少头发。Deep Opacity Maps的好处是它需要更少的层。其使用4个deep opacity map layers来累积头发的透光率。然后计算散射引起的衰减,求平均值并存储到LUT中。Deep Opacity Maps也用于确定阴影。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20210603155303.png