作例解説:ホヨトゥーン (hoyo_toon)

原神・スタレ・ゼンゼロ公式モデル向け hoyo_toon の仕組み、頭部ボーン駆動 SDF 顔影技術、HLSL 実装コードを詳解します。

ホヨトゥーン (hoyo_toon) パックの紹介

hoyo_toon は、MMDX12 に標準同梱されている公式リファレンスシェーダーパックです(shaders/packs/hoyo_toon/ に配置)。原神、崩壊:スターレイル、ゼンレスゾーンゼロの公式配布 MMD モデル向けに最適化されたアニメ調セルシェーディングを提供します。

実用的なシェーダーパックに必要な以下の 3 つの重要テクニックが網羅されています:

  1. PackParam(i) によるユーザー調整スライダーの接続
  2. s.materialClass による材質分類ごとの処理分岐(顔、瞳、肌、髪、服)
  3. headPos、headRight、headForward を用いた頭部ボーン座標系ライティング

なぜ法線ベースの顔シェーディングを避けるのか?

通常の 3D レンダラーは表面の法線ベクトルと光の向き($N \cdot L$)から陰影を計算します。しかし、洗練されたアニメキャラクターの 3D モデルにこれをそのまま適用すると、以下のような問題が生じます:

  • 鼻の下や目の周りに不自然な三角影が落ちる
  • 頬の微細な曲面が拾われ、顔が彫刻像のように凹凸に見えてしまう
  • アンビエントオクルージョン(AO)が顔の溝を黒ずませてしまう

実際のゲーム内では、アニメーターが描いた 2D の SDF(符号付き距離場)マップを用いて、頭の向きと光源の角度に応じて影が顔全体をきれいに横切るように表現されています。しかし、一般配布されている MMD 用 PMX モデルにはこの SDF マップが含まれていません。

hoyo_toon は頭部ボーンの姿勢を活用した幾何学的 SDF 近似により、この課題を解決しています。

頭部ボーン基準の顔影処理 (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);
}

この計算にはピクセルの法線は一切関与しません。 光が正面から当たれば顔全体が均一に明るくなり、真横からなら正確に半分が影になり、背後からなら自然に顔全体が影に包まれます。

材質分類による分岐処理

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 {
    // 身体や衣装:ハーフランバート基準の 2 段階ランプ
    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;
}

MMD トゥーンマップの活用と影の色調

影を単なる暗色で塗りつぶすのではなく、モデル付属の 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) でクッキリと二値化し、アニメ調の光沢バンドを生成します。
  • 輪郭リムライト: フレネル反射を用いて受光側のシルエットに細く鮮明な縁取り光を与えます。顔材質(PACK_FACE, PACK_EYE)からは除外されています。