范例解析:米哈游卡通 (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 大核心技术:

  1. 通过 PackParam(i) 映射可调参数滑块
  2. 依据材质分类 s.materialClass 差异化着色(脸部、眼部、皮肤、头发、服饰)
  3. 利用头部骨骼基底(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),确保面部五官整洁。