目录
什么是Wrinkle maps
通常当人物要表达非常强烈的情绪时,由于五官的挤压会导致面部出现明显的褶皱,比如说抬头纹、脸颊的酒窝等等,这些细节是非常重要的,并且细节的不同甚至会直接导致人物表达的情绪不同。举个例子,当一个人开怀大笑时,影响的肌肉范围不仅有嘴部,眼睛周围也会发生细微的变化。因此,如何自如去控制这些褶皱,并能够根据当前人物的情绪状态自定义褶皱(包括褶皱的强度、开启关闭、blend pose等等)是要解决的关键问题。
Wrinkle maps实现原理
为了能够让美术师(动画师)自由调节褶皱的深浅,开启关闭、或者和其他表情融合(blend),但是又不能影响原来的base normal map。这里采取一种方法:将wrinkle map与传统的base normal map独立出来,专门存储面部褶皱信息。这也是为什么wrinkle map不会烘焙到normal map里去,自由度也可以得到大幅度的增加。
它的计算原理是在base normal map之上直接叠加wrinkle map。
//Add wrinkle map to normal map
float3 vWrinkleNormal = normalize( float3(vWrinkleTS.xy + vNormalTS.xy, vWrinkleTS.z * vNormalTS.z))
有了wrinkle map,这时候人们依然还不满足,有没有办法细分wrinkle map,方便独立控制更多细节呢?
借助mask方法可以实现这点。一张texture map拥有4个颜色通道,每个通道能存一张mask。那么使用不同的mask就能在wrinkle map上划分出不同区域进行控制。将mask贴图采样得到的向量和4个mroph target的权重组合成的向量做个点积,就能计算出mask贴图对wrinkle map实际的影响范围。
//Calculate final normal weight
float w = blend.r + foreheadWeight * blend.g + cheekWeight * blend.b;
metahuman的实现过程
(1)从头部材质球中,能找到存有四个表情的贴图信息。
(2)找到MF_AnimatedMaps这个材质函数,关键信息就隐藏在这里。
(4)mroph target权重是标量,存到四维向量里。再与受影响的mask做个点积。得到最后的结果。
(5)判断是否有权重影响值。有权重,就输出,反之,就不输出。
附上完整代码
Sampler2D sWrinkleMask1; // <LBrow, RBrow, MidBrow, Lips>
sampler2D sWrinkleMask2; // <LeftCheek, RightCheek, UpLeftCheek, UpRightCheek>
sampler2D sWrinkleMapSampler1; // Stretch wrinkles map sampler
sampler2D sWrinkleMapSampler2; // Compress wrinkles map sampler
sampler2D sNormalMapSampler; // Normal map sampler
float4 vWrinkleMaskWeights1; // <LeftBrow, RightBrow, MidBrow, Lips>
float4 vWrinkleMaskWeights2; // <LCheek, RCheek, UpLeftCheek, UpRightCheek>
// Compute tangent space wrinkled normal
float4 ComputeWrinkledNormal ( float2 vUV )
{ // Sample the mask textures
float4 vMask1 = tex2D( sWrinkleMask1, vUV );
float4 vMask2 = tex2D( sWrinkleMask2, vUV );
// Mask the weights to get each wrinkle map's influence
float fInfluence1 = dot(vMask1, max(0, -vWrinkleMaskWeights1)) +
dot(vMask2, max(0, -vWrinkleMaskWeights2));
float fInfluence2 = dot(vMask1, max(0, vWrinkleMaskWeights1)) + dot(vMask2, max(0, vWrinkleMaskWeights2));
// Clamp the influence [0,1]. This is only necessary if
// there are overlapping mask regions.
fInfluence1 = min(fInfluence1, 1);
fInfluence2 = min(fInfluence2, 1);
// Sample the normal & wrinkle maps (we could branch here
// if both influences are zero). Scale and bias to get
// vectors into [-1, 1] range.
float3 vNormalTS = tex2D(sNormalMapSampler, vUV)*2-1; // Normal map
float3 vWrink1TS = tex2D(sWrinkleMapSampler1, vUV)*2-1;// Wrinkle map 1
float3 vWrink2TS = tex2D(sWrinkleMapSampler2, vUV)*2-1;// Wrinkle map 2
// Composite the weighted wrinkle maps to get a final wrinkle
float3 vWrinkleTS;
vWrinkleTS = lerp( float3(0,0,1), vWrink1TS, fInfluence1 );
vWrinkleTS = lerp( vWrinkleTS, vWrink2TS, fInfluence2 );
// Add final wrinkle to the base normal map
vNormalTS = normalize( float3( vWrinkleTS.xy + vNormalTS.xy, vNormalTS.z * vWrinkleTS.z ) );
return vNormalTS;
}
Reference
https://www.chrisoat.com/papers/Oat-Wrinkles(Siggraph07).pdf
https://www.chrisoat.com/papers/Chapter4-Oat-Animated_Wrinkle_Maps.pdf
Carl Granberg - Character Animation With Direct3D (2009).pdf