范例解析:米哈游卡通 (hoyo_toon)
深度剖析专为原神·星穹铁道·绝区零模型设计的 hoyo_toon 架构、头部骨骼驱动 SDF 脸部阴影及 HLSL 源码。
hoyo_toon 着色器包简介
hoyo_toon 是随 MMDX12 一同分发的官方教学参考着色器包(存放于 shaders/packs/hoyo_toon/)。它针对原神(Genshin Impact)、崩坏:星穹铁道(Honkai: Star Rail)、绝区零(Zenless Zone Zero)官方发布的 MMD 模型进行了深度定制与调优。
该包全面展示了制作高质量着色器包不可或缺的 3 大核心技术:
- 通过
PackParam(i)映射可调参数滑块 - 依据材质分类
s.materialClass差异化着色(脸部、眼部、皮肤、头发、服饰) - 利用头部骨骼基底(
headPos、headRight、headForward)进行光照几何计算
为何规避基于表面法线的脸部光照?
常规物理渲染器依赖表面法线向量与光线向量的点积($N \cdot L$)计算明暗交界。但将其直接用于追求 2D 手绘质感的美术模型时,会产生严重的结构瑕疵:
- 鼻梁下侧或眼窝出现杂乱的三角形折叠阴影
- 脸颊微妙的曲面起伏被过度刻画,使二次元脸庞变成凹凸不平的 3D 石膏像
- 屏幕空间环境光遮蔽(AO)会在面部留下脏黑印记
在米哈游的原作游戏中,美术团队使用 2D SDF(有符号距离场)贴图,仅通过头部朝向与光线夹角驱动阴影边缘扫过面部。但公开分发的 MMD 模型中并未包含游戏内部的原版 SDF 贴图。
hoyo_toon 创新性地引入了基于头部骨骼姿态的几何 SDF 近似计算(Geometric SDF Stand-in)。
核心算法:头部骨骼驱动的脸部阴影(FaceLight)
将光线向量投影到头部骨骼的水平面上,计算光源方位角(theta),再根据像素相对于面部中心的左右偏位(side)平滑滑动阴影分界线:
float FaceLight(PackSurface s) {
if (!s.hasHead) return 1.0;
// 将光线方向投影到头部水平坐标系
float lx = dot(s.L, s.headRight);
float lz = dot(s.L, s.headForward);
float theta = atan2(abs(lx), lz); // 0 (正面) .. pi (背面)
// 阴影边界随光照角度扫过的位置
float boundary = lerp(-1.15, 1.15, theta / 3.14159265);
// 像素偏离面部中心轴的相对距离 (-1 .. 1)
float side = dot(s.worldPos - s.headPos, s.headRight) / (P_FACE_WIDTH * s.headScale);
side = clamp(side, -1.0, 1.0) * (lx >= 0.0 ? 1.0 : -1.0);
return smoothstep(boundary - P_FACE_SOFTNESS, boundary + P_FACE_SOFTNESS, side);
}
整套运算完全绕开像素表面法线。正光照射时面部洁净无暇,侧光照射时精准覆盖半边脸庞,背光时面部整体均匀暗下,避免了一切凹凸杂乱瑕疵。
材质分类与分支处理
在 surface.hlsl 的 PackShade 中,通过 s.materialClass 进行精准材质分支:
PackResult r;
r.alpha = s.alpha;
r.reflectivity = 0.0;
r.noAo = false;
float term; // 0: 阴影 .. 1: 受光面
float flatFill; // 1: 辅助光无视法线平面照亮 (面部)
float warmth = 0.0;
if (s.materialClass == PACK_FACE || s.materialClass == PACK_EYE) {
term = FaceLight(s) * lerp(1.0, s.shadow, P_FACE_CAST);
flatFill = 1.0;
warmth = 0.15;
r.noAo = true; // 关键:关闭环境遮蔽 SSAO/RTAO 保持面部透亮
if (s.materialClass == PACK_EYE) {
term = lerp(term, 1.0, 0.6); // 保证眼瞳在阴影中依然清晰明亮
}
} else {
// 躯体与服饰:基于半兰伯特 (Half-Lambert) 的二阶色带阶梯
float halfLambert = dot(s.N, s.L) * 0.5 + 0.5;
float soft = P_RAMP_SOFTNESS * (s.materialClass == PACK_SKIN ? 2.0 : 1.0);
term = smoothstep(P_RAMP_THRESHOLD - soft, P_RAMP_THRESHOLD + soft, halfLambert) * s.shadow;
flatFill = 0.0;
if (s.materialClass == PACK_SKIN) warmth = 0.2;
}
采样模型原生色带与阴影温润色调
若阴影直接叠乘灰色会使画面沉闷。hoyo_toon 提取模型自带的 MMD 渐变贴图暗端颜色(PackSampleToon(1.0)),并融入温暖的淡粉色相(P_SHADOW_WARMTH),渲染出兼具透明感与生机的动漫阴影:
float3 ShadowColour(float3 lit, float extraWarmth) {
float3 toonDark = PackSampleToon(1.0);
float3 warm = lerp(float3(1, 1, 1), float3(1.0, 0.82, 0.84), saturate(P_SHADOW_WARMTH + extraWarmth));
return lit * lerp(float3(1, 1, 1), toonDark * warm, P_SHADOW_DARKNESS);
}
阶梯状发丝高光与边缘剪影光
- 发丝天使环:利用
smoothstep(0.45, 0.55, spec)将原本渐变的镜面高光阶梯化为清晰的高光光环。 - 边缘光(Rim Light):基于菲涅尔反射在模型受光边缘叠加锐利的轮廓光带,同时剔除脸部材质(
PACK_FACE与PACK_EYE),确保面部五官整洁。