Twitter上でモーフで色調節がしたいとの話が出て、それができるマテリアルfxを作ったのでそれの解説をしたいと思う。
重要なのはこの部分
#define ALBEDO_MAP_FROM 3
#define ALBEDO_MAP_UV_FLIP 0
#define ALBEDO_MAP_APPLY_SCALE 1
#define ALBEDO_MAP_APPLY_DIFFUSE 1
#define ALBEDO_MAP_APPLY_MORPH_COLOR 0
#define ALBEDO_MAP_FILE "albedo.png"
static const string pmxName = "モデル.pmx";
float mR : CONTROLOBJECT<string name=pmxName; string item = "赤";>;
float mG : CONTROLOBJECT<string name=pmxName; string item = "緑";>;
float mB : CONTROLOBJECT<string name=pmxName; string item = "青";>;
float mS : CONTROLOBJECT<string name=pmxName; string item = "彩度";>;
static const float3 albedo = float3(1+mR, 1+mG, 1+mB) * (1-mS);
const float2 albedoMapLoopNum = 1.0;
#define ALBEDO_SUB_ENABLE 4
#define ALBEDO_SUB_MAP_FROM 0
#define ALBEDO_SUB_MAP_UV_FLIP 0
#define ALBEDO_SUB_MAP_APPLY_SCALE 0
#define ALBEDO_SUB_MAP_FILE "albedo.png"
float mC : CONTROLOBJECT<string name=pmxName; string item = "シアン";>;
float mM : CONTROLOBJECT<string name=pmxName; string item = "マゼンタ";>;
float mY : CONTROLOBJECT<string name=pmxName; string item = "イエロー";>;
float mV : CONTROLOBJECT<string name=pmxName; string item = "濃度";>;
static const float3 albedoSub = float3(mC, mM, mY) * (1-mV);
const float2 albedoSubMapLoopNum = 1.0;
重要な行をかいつまんで説明していこう。
この記事では最終的に
albedo = float3(1+mR, 1+mG, 1+mB) * (1-mS) と
albedoSub = float3(mC, mM, mY) * (1-mV) の意図と効果を理解し、
自分で好きにこれらの式の形を変えられるようになることを目指している。
マテリアルのパラメータ
ここでは ALBEDO と ALBEDO_SUB のメラニンについて詳細に説明することにしよう。
ALBEDO
ALBEDOとは材質の色みたいなものだ。
ALBEDO_MAP_FROM
材質の色をどこから持ってくるかをこのあとの数値で決めている。
ALBEDOなら大抵は 3 が設定されるだろう。これはpmxに設定されたテクスチャをそのまま持ってくるという設定だ。
float3
#define ALBEDO_MAP_FROM 0 の時 const float3 albedo = のあとの数値で色が決定される。
ここでこの形式について詳しく見ておきたい。
const float3 albedo = 1.0;
これは material_2.0.fx から抜粋してきた文だ。これを例に解説しよう。
const は定数という意味だ。今回はそこまで関係ないので気にせずとも良い。
float3 は実数を要素として持つ3次元ベクトルを意味する。要は小数で表される数値3つをまとめて扱うということだ。
albedo は変数名なので、この文は「実数3つから成るalbedoという名前の定数である変数を宣言する」という意味になる。
さて、そこで右辺を見てみると、1.0 と書いてある。これでは実数一つだ。話が違うと思いそうになるが、これは(1.0, 1.0, 1.0)と同じになる。
もし3つの要素それぞれの値を指定したければ、このように書くと良い。
const float3 albedo = float3(0.25, 0.8, 0.63);
これらの要素に一括で数値を掛けたいなら
const float3 albedo = float3(0.25, 0.8, 0.63) * 0.5;
のような書き方も可能だ。
さて、マテリアルfxで float3 が使われている時は、ほぼ(赤, 緑, 青)の意味で使われていると思っていい。
そして色情報は[0.0, 1.0]の区間で表されている。例を挙げよう。
(0.0, 0.0, 0.0) → 黒(0.6, 0.6, 0.6) → 灰(1.0, 1.0, 1.0) → 白(1.0, 0.0, 0.0) → 赤(1.0, 0.0, 0.6) → 赤紫
(1.0, 0.5, 0.6) → ピンクよくわからなければ、#define ALBEDO_MAP_FROM 0 にして適当なモデルに適用し、albedo = の後の数値を色々変えて試してみるといい。
ALBEDO_MAP_APPLY_SCALE
前の記事でなんでAlphaにこれが無いんやと言ってたアレ。
#define ALBEDO_MAP_APPLY_SCALE 1 とすることで、albedo 変数の値を MAP_FROM で取ってきた色に掛ける。
つまりMAP_FROM 3 の時、
albedo = 1.0 でテクスチャの色そのままが材質の色となる。
albedo = 0.0 なら材質は真っ黒になる。
albedo = float3(1.0, 0.0, 0.0) ではテクスチャの赤色成分のみを反映した色になる。つまり真っ赤ではあるが、それでもテクスチャの赤みの多さを反映して、黒から赤の原色の間の色になるということだ。
百聞は一見にしかず
画像で見てみよう。モデルはつみ式ミクさんにした。
サムネイルをクリックすると大きい画像で表示できる。
まずはmaterial_2.0.fxそのままを当てた状態。
ALBEDO_MAP_FROM 0 にした時。
左が albedo = 0.0 右が albedo = 0.0。
テクスチャ関係なく一色で塗りつぶされているのがわかるだろう。
ALBEDO_MAP_FROM 3 に戻して、 ALBEDO_MAP_APPLY_SCALE 1 にした時
左から albedo=0.0, albedo=1.0, albedo=2.0。
ApplyScaleはテクスチャの色の数値にalbedoの値を掛けるのだった。
albedo=0.0 は色に0を掛けるので真っ黒になる。
albedo=1.0 は色に1を掛けるので通常時と何も変わらない。
albedo=2.0 は少しわかりにくいが色が白っぽくなっている。もっと数値を大きくすればどんどん真っ白に近づいていくだろう。
次は要素ごとに違う数値を掛けてみよう。
albedo=float3(1.0, 0.0, 0.0)にした。
テクスチャの赤要素だけが取り出されている。
黒が青っぽく見えるのは目の錯覚だ。
最後に同じ albedo の数値にした MAP_FROM 0 と APPLY_SCALE 1 を並べてみてみよう。
albedo = float3(0.0, 1.0, 0.7)
左のは MAP_FROM 0 で、テクスチャが一切適用されていないので、ミク色一色になっている。
右はテクスチャに数値が掛かっているので、全体的にミク色になっているものの、テクスチャがちゃんと適用されているのがわかるだろう。
このように、簡単な色調整ならテクスチャをいじらずともAPPLY_SCALE 1 にしておけばalbedoの値を色々変えて材質を好きな色味に変えられる。
ALBEDO SUB
ALBEDOの下にはこいつがいる。
材質の色をいくつかの方法で変えられる。
掛け算とかALBEDOのAPPLY_SCALEでええやんけとつい思ってしまうが、数値で一括指定しかできないあちらと違って、マップ画像が使えるという利点がある。
テクスチャのどの部分にあるかで適用する色を変えられるわけだ。
メラニン
Sub の種類の内、数値計算では無い名前のものが2つ混ざっている。アルファブレンドとメラニンだ。
アルファブレンドはまあ分かるだろう。要するに透かし合成のことだ。わからないならググってほしい。
ではメラニンとはなんだろうか?普通に考えたら肌の色素の名前だが、色指定で使うとはどういう事なのか。
これはあくまで数字を色々弄り回して得た推測なのだが、多分メラニンというのは指定された色を減衰させるような処理をしているらしい。
それもただ線形に(大体直線的にみたいな意味)やっているのではなく、何かしらの階調補正が掛かっているようだ。
なんというかやや具体性に欠ける口ぶりになってしまった。具体例で考えよう。
float3(1, 0, 0)をメラニンに指定したとする。これは赤色を減衰させる。
色味の想像がつきにくいだろうか?逆に考えてみよう。赤が減ると何色に見えるのか?
青と緑の合成色、つまりはシアンになる。同じように緑が欠けるとマゼンタに、青が欠けるとイエローがかることになる。要は減色混合の三原色だ。
この辺を頭に置いておけば値をこう変えると大体こうなるやろ、という予測もつけられるようになるだろう。
百聞は(ry
ではメラニンの数値を色々変えてみたときの画像を見てみよう。
尚、わかりやすくするために ALBEDO_MAP_FROM 3 、 ALBEDO_MAP_APPLY_SCALE 0 とした。
まずは albedoSub=0, albedoSub=1, albedoSub=2 から見てみよう。
メラニンの色補正が線形でないことはこの画像からわかる。(画像名はスカラーだが)
白い部分はずっと白いまま、色味のメリハリがついているのがわかる。
次は float3(1, 0, 0), float3(0, 1, 0), float3(0, 0, 1)。
(1, 0, 0)は赤が減衰された分シアンがかっている。
他も同様に、(0, 1, 0)ではマゼンタがかり、(0, 0, 1)では黄みがかっている。
最後に float3(0, 1, 1), float3(1, 0, 1), float3(1, 1, 0)。
0にした色が残っている。メラニンは色の減衰らしいと考えた理由がわかってもらえただろうか。
コントロールオブジェクト
これの概要はすでに以前の記事で書いている。再掲しておこう。
モデルのモーフの値を持ってくるにはどうしたらいいだろうか。
ray-mmd-1.5.2/Materials/Editor/ に数値パラメータをコントローラpmxを使って動的に制御できるfxファイルが入っている。
コントローラpmxからモーフの値を持ってくる部分は material_editor.fxsub 内に記述されている。
float 「変数」 : CONTROLOBJECT<string name="「コントローラPMXのファイル名」"; string item = "「値を取得したいモーフの名前」";>;
という形式になっているようだ。なお「」で囲ってある日本語部分は意味を表すので、実際に使うときは適切な表記で置き換えられねばならない。
最終的に「変数」にモーフの値が代入される。
「コントローラPMXのファイル名」は文字通りファイル名のみで、パスを指定してやる必要はないようだ。おそらくMMDに読み込んだpmx名から取ってきているからだろう。
実際の所言えることはこれで全部なのだが、もう少し言葉を尽くして説明してみよう。そんなんいらんわという人はここの部分は飛ばしてほしい。
さて、この文は大きく分けて3つの要素からなる。
float 「変数」
: CONTROLOBJECT
<string name="「コントローラPMXのファイル名」"; string item = "「値を取得したいモーフの名前」";>
の3つだ。順番に説明しよう。
まず float 「変数」について。これは特に難しくない。ただの(浮動小数点数で)実数を格納する変数を宣言しているだけだ。
変数の名前は適当に決めよう。Materials/Editor/ に入っているやつでは大体「m内容」という命名がなされていたので、それに倣ってもいいだろう。たとえば赤色を制御するモーフの値を格納する変数なら mR なんて名前にするとか。
次に : CONTROLOBJECT これはMMEから提供されているセマンティクスと言うやつらしい。
つまり float mR : CONTROLOBJECT とは、MMDから持ってきた浮動小数点数型の値をmRという変数に代入してやる、という意味になる。
さて、ここからは上で示したMMEリファレンスの内容ほぼそのままだが、一応私の言葉でも説明しておこう。
さて、MMDからは一体どのような値を持ってくることができるのだろうか。そしてどのようにして持ってくる値を指定するのだろうか。
どのような、はMMEリファレンスを見てほしい。自分の言葉でとか言っておきながらではあるが、こればっかりはリファレンス様の羅列でしか言いようがないと思うので…
気を取り直して。指定する部分は2箇所ある。変数の型(今回はfloat)と、<string name="「コントローラPMXのファイル名」"; string item = "「値を取得したいモーフの名前」";> の部分だ。
MMEリファレンス曰くfloat変数に持ってこれるのは「指定したオブジェクトのスケーリング値」とのこと。
こう言われるとよくわからんが、どうやら単なるアクセサリ操作のXYZ,RxRyRz,SiTrとモーフの設定値らしい。ビームマンP氏のエフェクトでSi値とかTr値とかで色々制御できるようになってるが、あれはこうやって値を取得していたらしい。
今回はモデルのモーフを使って数値制御をしたいので、変数の型はfloat型ということになる。
次。<string name="「コントローラPMXのファイル名」"; string item = "「値を取得したいモーフの名前」";>。
中を見ると name と item の2つの値がある(stringというのは変数の型(文字列型)だ)。どうやらアノテーションと呼ばれているらしい。
name には持ってきたい値を持っているであろうモデル/アクセサリの名前を入れる。
item には持ってきたい値の名前を入れる。モーフ名とか"X"とか"Ry"とか"Tr"とか。
使い方
文の意味を理解できたところで、使い方についてちょっとした具体例を示しておこう。
まずは条件設定。
"初音ミク.pmx"というモデルの肌の色と髪の色を別々に制御するにはどうすればいいか?
ここで"初音ミク.pmx"は"赤"、"緑"、"青"のモーフを持っており、 material_肌.fx と material_髪.fx をそれぞれの材質に当てているとする。
まずは肌と髪、両方のfxにコントロールオブジェクトを設定しよう。
float mR : CONTROLOBJECT<string name="初音ミク.pmx"; string item = "赤";>;
float mG : CONTROLOBJECT<string name="初音ミク.pmx"; string item = "緑";>;
float mB : CONTROLOBJECT<string name="初音ミク.pmx"; string item = "青";>;
これだと肌と髪の色が連動してしまうことになる。同じモデルの同じモーフを参照しているからだ。
では対処法は2つに1つだ。同じモデルでも違うモーフを参照するか、違うモデルのモーフを参照するかだ。
同モデル異モーフなら、新しく"髪R"、"髪G"、"髪B"あたりでも追加して、material_髪.fxではそっちを読んでやればいい。string item = "髪R" みたいに変えるわけだ。
異モデルを使うのも別に難しくはない。PMXエディタでモーフだけ用意した空っぽのモデルを作り、それをコントローラとして使えばいい。更科氏の初心者マテリアルに入っているpmxファイルはこれである。
さて、これはこれでいいのだが、最後にもう一つ。上の例を見るとわかるが、"初音ミク.pmx"と同じ文字列を3回も使っている。もし違うモデルにfxを使いまわしたいと思ったとき、いちいち書き換えるのは面倒だし、ミスも誘発する。一つにまとめてやろう。
方法は2つある。#define するか変数を作るかだ。
#define CONTROLLER_NAME "初音ミク.pmx"
float mR : CONTROLOBJECT<string name=CONTROLLER_NAME; string item = "赤";>;
float mG : CONTROLOBJECT<string name=CONTROLLER_NAME; string item = "緑";>;
float mB : CONTROLOBJECT<string name=CONTROLLER_NAME; string item = "青";>;
これで #define の"初音ミク.pmx"を書き換えるだけで全てに反映させられる。
変数の方もそう変わらない。
static const string pmxName = "初音ミク.pmx";
float mR : CONTROLOBJECT<string name=pmxName; string item = "赤";>;
float mG : CONTROLOBJECT<string name=pmxName; string item = "緑";>;
float mB : CONTROLOBJECT<string name=pmxName; string item = "青";>;
どちらがいいのかと言われると、HLSLらしい書き方は #define の方なのかもしれない。
冒頭で紹介したmaterial_color.fxは変数でやっているが、これはなにか考えがあったわけではなく、単に手癖でそう書いただけだ。だって普段#defineとか使わんし…constexprしろ
const stringではなくstatic const string な理由は知らない。一応HLSLの static とか const の説明は読んだのだがやっぱりわからない。
あとついでに const だと初期値はリテラルでしか入れられないのにstaticだと変数で入れられるのもわからない。Microsoftは「グローバル変数が static でマークされると、アプリケーション側から見えなくなります」とか言ってるが、これはつまりプログラムにはstaticが付いてるからリテラルに見えてるってことなのか。俺は雰囲気でHLSLをやっている(勉強してないのだから当たり前だ)。
色調整のための計算式
さて、これで必要な基礎知識は話したので、ようやく本題だ。とは言っても大したことをしているわけではないのでこれはすぐ終わるだろう。
ALBEDO
さて、material_color.fxの
static const float3 albedo = float3(1+mR, 1+mG, 1+mB) * (1-mS);
の各単語の意味はもう分かるだろう(static constは置いといて)。
ではこの式は何を意図しているのだろうか?
とりあえず状況を確認しよう。
#define ALBEDO_MAP_FROM 3
#define ALBEDO_MAP_APPLY_SCALE 1
なので albedo はテクスチャにかける数値を決めているらしい。
さて、こういうものを考えるときは上限と下限で考えるといい。
モーフの数値は[0, 1]である(手入力すれば超えられるが、スライダーはそうだ)。
そういえばなんの説明もなしに[数値A, 数値B]という表記を使っているが、これは閉区間の記号である。数値A以上、数値B以下という意味だ。(数値A, 数値B)なら開区間といい、数値A超、数値B未満という意味になる。
今回だと単体なら[モーフが0の時の値, モーフが1の時の値]になると考えるとわかりやすいだろう。
mR,mG,mBはモーフの値に格納されているので、こいつらも[0, 1]だ。これらは全部対称に(同じ式で)運用するのが良いだろう。というわけで今後mRGBと書く。
1+mRGBは[1,2]となる。
そして1-mSは[1,0]だ。一応書くと、[1,0]はモーフの値が0の時1、1の時0になるということだ。
最終的にalbedoの値域は[0,2]となる。
mS == 1 の時 albedo == 0
mS == 0 かつ mRGB == 1 の時 albedo == 2
となるからだ。
そしてこれはAPPLY_SCALEなのでテクスチャの色に乗算される。
テクスチャの色(RGB)は[0,1]だ。よって
albedo = float3(1+mR, 1+mG, 1+mB) * (1-mS)
の式はテクスチャの色に一括して0倍から2倍の間の任意の倍率をかけられることになる。
ここで[0,2]の範囲を実現したいだけなら albedo= 2 * mRGB にすれば良いのでは?と思ったかもしれない。実際それでもうまくいく。というか正味こっちのほうが0 から 1.0 の範囲を直感的に扱える。
ただ、1つ欠点がある。初期値(すべての値が0)の時、材質が真っ黒になることだ。元の色にするためにはmRGBを0.5にしてやる必要がある。
これが気に入らなかったので、初期値で albedo == 1 になり[0,2]であるような式を考えたのだ。
ただまあ、考えたときは悦に入っていたのだが、今考えたら最終的なRGB値に0から1の値を含ませようと思うとやたら使いにくい((0.1,0.8,0.75)とか作れないのでは…?)ので、
albedo = float3(2*mR, 2*mG, 2*mB) * (1-mS)
にするべきだと思った。
なおmSは全部の数値を一括で操作するために置いてある。
(1-mS)だとmSを大きくするほど色が黒くなる。
2*mSにするとmSを大きくするほど色が明るくなるが、初期値で0がかかってしまうので忘れた頃にRGBを動かしても反応せえへんぞ!ってパニックを引き起こすかもしれない。真っ先に0.5にするのを忘れないように。
2をかけているのは、ただのmSだともっと一括で明るくしたいと思った時に値の直接入力に頼る必要があるからだ。
最終的な色は[0,2]になるようにするのがおすすめだ。
色の値を負にする必要性は感じないので下限は0
色はあまり大きくできても結局白飛びするだけなので2倍もあれば十分だろう。なので上限は2
それ以上は直接入力で、ということになる。
もっとも式の変更はテキストエディタで弄るだけだし、変更は即時反映されるので大きい値が欲しくなったときに式をちょちょっと書き換えれば済む話ではある。
メラニン
メラニンはずっと話が簡単だ。
0で何もしない。値が大きくなるほど効果が強まる。
(アルベドは1で何もせず、[0,1)と(1,2]にも意味があるので範囲に含めなければならなかったから話がややこしくなった)
albedoSub = float3(mC, mM, mY) * (1-mV)
mCMYは2*mCMYにしてもいいだろう。
式を考えるときの形式
最後に式を考える時一般に使える考え方を書いておこう。
- 値域の上下限で考えよう
- 意味のある値域を考慮して値域を決めよう
- 値域内の任意の値を簡単に得られるような軌道をもつ式にしよう
- 0は何をかけても0。値域を拡大縮小したい時は、まず加減算で上下限どちらかを0にして掛け算、その後また加減算で0にしたほうを元に戻そう。
3.の反面教師はfloat3(1+mR, 1+mG, 1+mB) * (1-mS)
4.は
前の記事の alphaValue = alpha * (alphaValue - 1) + 1 なんかがそうだ。
おわりに
なんかやたら長い記事になってしまった。最後の方はちょっと力尽き気味でわりと適当感があるがまあ赦してほしい。
わからんぞってなったら言って貰えれば質問に答えたり記事の加筆修正なんかをすると思う。