three.js + three-vrm で、VRoid 製アバターの服と髪の色をゲーム内で変える機能を作った。白い T シャツは狙い通り色が変わるのに、短パンは何色を指定しても真っ黒になる。

原因は MToon マテリアルの色の掛け算の仕組みにある。同じ色替えコードでも、元テクスチャの明暗で挙動が真逆になる。

ハマった順に原因と直し方を書く。

three.js + three-vrm で、VRoid 製アバターの服と髪の色をゲーム内で変える機能を作った。白い T シャツは狙い通り色が変わるのに、短パンは何色を指定しても真っ黒になる。

原因: MToon の色は「掛け算」

MToon の litFactor (基本色) は baseColorTexture に乗算される。

  • 白い布のテクスチャ × 指定色 = 指定色が出る
  • 暗い布のテクスチャ × 指定色 = 黒く潰れる

さらに陰側の shadeMultiplyTexture も暗い布だと、陰が全部黒に落ちる。つまり同じ色替えコードでも、元テクスチャの明暗で結果が真逆になる。白 T シャツで動いたからと全マテリアルに一般化したのが敗因だった。

直し方: 暗い布だけテクスチャを外す (flatten)

暗い布のマテリアルに限って、map と shadeMultiplyTexture を null にして litFactor の平面色を主役にする:

function tintBaked(mat, color, flatten) {
  mat.color.set(color);
  if (flatten) {
    mat.map = null;
    mat.shadeMultiplyTexture = null;
  }
}

布の織り目は消えるが、色は正しく出る。元に戻せるように、外したテクスチャは復元用に控えておく。

ついでに踏んだ罠 2 つ

1. Blender を通した GLB はマテリアル名にサフィックスが付く。 同名マテリアルが統合できない時に tint が tint.005 になる。色替え対象を name === 'tint' の完全一致で探していると .005 だけ塗り残る。正規表現 (/^tint\.\d+$/) で拾う。

2. VRoid 書き出し VRM から顔のスライダー値は読めない。 Face の morph は 57 個あるが表情用で、体型・顔つきのパラメータは書き出し時に頂点へ焼かれていて値が残っていない。顔パーツをいじりたい場合はマテリアル名で mesh を特定して実行時に頂点を動かすしかない。

まとめ

  • MToon の色替えは乗算。暗いテクスチャは何色を掛けても黒
  • 暗い布は map / shadeMultiplyTexture を外して litFactor を主役に
  • 1 つのマテリアルで動いた色替えを全マテリアルに一般化しない
  • マテリアル名の完全一致比較は Blender の .00N サフィックスで漏れる