DXLibのMV1でなんちゃって金属表現
今回、金属表現に挑戦してみますが、なんちゃってなので物理ベースレンダリング(PBR)は取り扱いません。
思想そのものには触れますが、DXライブラリとしては難しい気がするので、ホントに「なんちゃって」でやっていきます。
ガチでPBRやりたい人は、Qiitaでも色んな人が解説してますので、そっちを見たほうがいいと思います。
とりあえず実例を見て判断してもらったほうがいいかなと思います。
例
動画はTwitter(X)に上げておきました。
はい、多分DxLibでMV1表示したにしては金属感が出ていると感じるのではないでしょうか?
とりあえず感じない人はブラウザバックなりタブを閉じるなりしてください。たぶんこれ以上読んでも意味はないかなと思いますので。
そもそも何で金属感があまり出ないのか
基本的に現状のDXライブラリのシェーディング方式は「古典的 シェーディング」とも呼べるもので
- ディフューズ(拡散反射)
- スペキュラ(鏡面反射)
- アンビエント(環境光)
- エミッシブ(発光)
でシェーディングしています。なおDXライブラリにおいては、3と4のパラメータは共有と言うか同一視してるっぽいです。
で、さきほど古典的と書きましたが、このdiffuse,specular,ambientでのシェーディングは現代のUnrealEngineのレンダリングでは使われておりません。デフォルトで「物理ベースレンダリング(PhysicallyBasedRendering)」というのでレンダリングされています。
まぁ、それはともかく、この古典的シェーディングでは、特に鏡面反射の部分の表現力が足りなくて 「つるっつる」か「がっさがさ」の区別しかつきません。
かつては「金属だったらスペキュラ上げておけばいいじゃん」が通用したかもしれませんが、ご存じの通り金属でもざらざらなものはありますし、プラスチックでもツルツルの物質はあります。
そうなんです。なんか昔のレンダリングって、金属感がないんですよね・・・ちなみに比較画像ですが、左が従来の古典的シェーディングで、右側がPBRに基づいてレンダリングです。
まあ確かに言われてみれば右側に金属っぽさが感じられるかもしれません。
でも決定打じゃないよなあ・・・って思います。
そもそもPBRのシェーダを書こうとするとBRDFだのBSDFだのPDFだのマイクロファセットだのフレネルだのと、まぁなんというかヤヤコシイ事を言われてしまいます。
そんなのを初心者に押し付けるのもなんだかな、と思いますが金属感は出したい。そういったワガママなご要望にお応えするためのお話ですね。
金属と非金属の違いって何?(ものすごく大雑把に)
ものすご~~く大雑把に言うと 「金属は鏡面反射しかしない」「非金属は自身の色が出ちゃう(光を吸収しちゃう)」ってことです(PBRガチ勢から怒られそうな説明ですが、「なんちゃって」なのでご勘弁を)。
勿論、つるっつるのプラスチック表面のように周囲を反射する「非金属」はあります。ただしこの場合でも反射光に自身の色が混ざって見えます。
ところが、つるっつるの金属の場合は、鏡のように周囲を反射するわけです。
ちなみに、金属・非金属と言いましたが実際には「誘電体(電気を通さない)」「伝導体(電気を通す)」の違いです。電気を通すという事は「自由電子」があって、これがあると「電子ガス」(そういう名前の気体があるわけではなく、自由電子があるために、物質表面に一様に電荷がちりばめられている状態を言います)が、光をそのまま反射してしまうため、伝導体(金属)では鏡面反射しかないというわけです。
「いやいや、金とか銅には色がついてるじゃーん」と思うでしょう。それはその通りです。
通常この電子ガスに光が当たると、同じ振動数(波長)の光を返すのですが、金や銅の場合は高周波を吸収してしまいます。これによって吸収されなかった赤や黄色が跳ね返って、独特の色が見えるわけです。
非金属の場合は電子ガス的なものがないので、表面で跳ね返る光と中に入ってしまう光があります。光が中に入ってしまう事をスキャッタリング(物質内部に入って乱反射する)って言います。
というわけで、「仮に表面がつるつるでも」 以下のように反射特性に違いが出てきます
非金属の反射がなんかヌルっとしてるのはこのためです。
まとめた結論を言うと
- 金属は自分の色が殆ど出ない(金とか銅が特有の波長を吸収するくらい)で、周囲の光をほとんど反射する
- 非金属は自分の色が出る。つるつるな表面の場合は非金属でも周囲の色を反射するが、物質の色が混ざる
金属も非金属も持ってる特性「つるつる」「ざらざら」
で、更に話をややこしくしているのが、金属非金属とは別の特性「表面のザラザラさ」です。
こいつはツールによって、ざらざらさ=Roughnessって言うたり、つるつるさ=Smoothnessって言うたり、それぞれなんやけど、ようは表面がざらざらかつるつるかって事やね。
で、ある程度の大きさ以上(要は表面の凸凹がテクセルより大きい)であれば「法線マップ」でつるつるかざらざらか、を表現できるのですがそれよりちっちゃい凸凹は法線マップじゃどうしようもありません。
つるつるだと入射光ベクトルと法線ベクトルから反射ベクトルを作って、それによってハイライト(スペキュラ)を表現すればいい。
で、これが少しずつざらざらになるにつれて、反射ベクトルがあちこち向いて光が拡散してしまう ため、ハイライトがほとんど出ません。
もうちょっというとつるつるだと周囲の環境が映り込み、ザラザラだと映り込みもありません(映り込みがないというより、乱反射し過ぎてボケまくった結果として映り込みがない状態)
まとめた結論を言うと
- 表面がつるつるだと、周囲の光がきれいに反射し、スペキュラが出る
- 表面がざらざらだと、周囲の光が拡散され、スペキュラも出ない
はい、ここまでが前提知識です。
結論から言うとMV1情報「だけ」では金属感は出ません
身もふたもないですが、そもそもMV1は古典的シェーディング用の情報なのでそのままでは金属感が出ません。じゃあどうすればいいのでしょうか・・・・?
手っ取り早く金属感を出す方法
MMDからそれっぽい素材を持ってきてスフィアマップ(MatCap)する
素材を拾ってくる
MMDの中にはデフォルトで、MetalMiku用のMatCap画像が入っています。
https://sites.google.com/view/vpvp/
ここからMMD本体をダウンロードしてください(64bitのやつを落としたほうがいいと思います)
そしてプリセットのモデルデータの中にテクスチャデータが入っていますが、その中にmetal.sphというのがあります。
こいつの中身はただのbmp(pngかな?)なので、拡張子を変更するなりすれば…
こういう画像が出てきます。もうこのテクスチャじたいが金属っぽいですよね?
スフィアマップ用のUVを計算する
スフィアマップ(MatCap)をモデルのUV値をもとに貼り付けてはいけません。これは「周囲の環境の映り込み」なので、物質の表面がどちらを向いているのか(法線ベクトル)によって決めなければいけません。
まぁ、なんちゃってなので、簡単に考えましょう。まず物質表面の法線ベクトルを取得します。ひとまず法線マップアリ設定だと、ピクセルシェーダに渡ってきた時点で
struct PSInput {
float4 svpos:SV_POSITION;
float3 pos:POSITION;
float3 norm:NORMAL;
float3 tan:TANGENT;
float3 bin:BINORMAL;
float2 uv:TECOORD;
float3 col:COLOR;
};
このようになっているはずなので、この中のnormを利用します。こいつは3次元ですが、必要なのはx方向とy方向だけです。
ここからUVを算出します。法線ベクトルが正規化されているとすると
\displaylines{
\vec{N}=(n_x,n_y,n_z) \\
n_x^2+n_y^2+n_z^2=1\\
(-1.0\leq n_x\leq1.0 ,\\ -1.0\leq n_y\leq1.0 ,\\ -1.0\leq n_z\leq1.0 )
}
ですね。しかしUV値の範囲は$0.0\leq u,v \leq 1.0$です。そこで範囲の変換を行います。大したことはしませんが
x : -1~1 ⇒ 0~1
y : -1~1 ⇒ 1~0
とします。y方向がひっくり返ってるのは、3Dのとき上が+ですが、UV座標系では下が+だからです。
はい、ここで連立方程式を立てます。どちらも直線の方程式f(x)=ax+bに見立てて考えます。まずxから
\begin{equation}
n_x\Rightarrow u:\left\{ \,
\begin{aligned}
& 0.0 = a(-1.0)+b \\
& 1.0 = a(1.0)+b\\
\end{aligned}
\right.
\end{equation}
こんなもんパパッと2b=1.0からb=0.5が出てきます。もちろんa=0.5もすぐですね。同様にyもやります。
\begin{equation}
n_y\Rightarrow v:\left\{ \,
\begin{aligned}
& 0.0 = c(1.0)+d \\
& 1.0 = c(-1.0)+d\\
\end{aligned}
\right.
\end{equation}
先ほどと同様に2d=1.0からd=0.5が出てきてc=-0.5であることがわかります。つまり
\begin{equation}
\left\{ \,
\begin{aligned}
& u=0.5n_x + 0.5 \\
& v = -0.5n_y +0.5 \\
\end{aligned}
\right.
\end{equation}
プログラムにすると、こう
float2 uv = input.norm.xy * float2(0.5, -0.5) + 0.5;
return float4(uv, 1, 1);
一見良さそうだけど、真正面から見た時にしか適用されない。重要なのは「反射ベクトルが向いてる方向の色」を取ってくる事だ。
つまり
float3 ray = input.pos-cameraPos;//視線ベクトル
ray = normalize(ray);
float3 refRay = reflect(ray, n);
float2 uv = refRay.xy*float2(0.5,-0.5) + 0.5;
はい、こうすることでなんかその、めっちゃ金属っぽくなります
とはいえ、これは当たり前ですね。
ちなみに、非金属の表現ですが、通常の色(ディフューズ?アルベド?)とα合成してやれば、非金属に周囲が移りこんでるように見えます(そらPBRには劣りますが)
float4 matcap = sph.Sample(sam, uv);
float4 red = float4(1, 0, 0, 1);
return lerp(matcap, red, 0.75)+specular;
このlerpに入れてる値を0に近づけるとより「金属っぽく」見えるわけですね。
もうちょっと整理して、金属の時は環境をアルファブレンディングではなく乗算にするようにすると
float4 matcap = sph.Sample(sam, uv);
float4 albedo = float4(1, 1, 0, 1);//黄色
float fmetalic = 0.25;//金属っぽさ
float ommetalic = 1.0 - fmetalic;//1.0-メタリック
return lerp(matcap * fmetalic * albedo + matcap * ommetalic, albedo, ommetalic) + specular;
なんとなくそれっぽい違いが出てるのが分かると思います。
なんちゃってなので、それっぽければ、ヨシ!
外部テクスチャとして「ラフネス」「メタリック」を用意する
あ、もちろん通常のテクスチャとノーマルマップはある前提ですよ?
次に外部テクスチャとしてラフネステクスチャとメタリックテクスチャを用意します。
MV1だけだと、ディフューズとノーマル(法線マップ)しかないので、外部から読み込みます。
とはいえ、DXライブラリユーザはPBRマテリアルなんて作った事がないと思いますので、
MaterialMaker
https://rodzilla.itch.io/material-maker
とか
TextureLab
https://njbrown.itch.io/texturelab
を使って自分で作るか、
PBR Materials
https://pbrmaterials.com/
こういう所から落としてくるかします。もちろん「なんちゃって」なので、ここで作ったり入手したりしたものと全く同じ質感が出るなどとは思わないでください。
あくまでも「それっぽく」です。なんかまぁ、色々なテクスチャが得られますが、そんな凝ったことをしないので必要なのは
- ベースカラー(MV1に内包されてるか、外から取ってきてもいい)
- 法線マップ(MV1に内包されてるか、外から取ってきてもいい)
- メタリック(外部から)
- ラフネス(外部から)
ですね。本当はラフネスに関しては、これを上げれば上げるほど周囲の反射を拡散させなければならないのですが今回のラフネスの役割は
- スペキュラが出ない度
- スフィアマップを反映しない度
とします。
ピクセルシェーダの実装
はい、ここまでの事をまとめてピクセルシェーダを一気に書きます。
法線マップやらビュー行列やら何やら一気に出てきますがこのコードは参考程度に考えてください。
コピペして動作するなんてゆめゆめ思わぬことです・・・。
//ボーンなし、法線マップアリでピクセルシェーダに持ってこれる情報
struct PSInput {
float4 svpos:SV_POSITION;
float3 pos:POSITION;
float3 norm:NORMAL;
float3 tan:TANGENT;
float3 bin:BINORMAL;
float2 uv:TECOORD;
float3 col:COLOR;
};
cbuffer CBuffer : register(b0) {
float4 cameraPos;//カメラ座標
matrix vmat;//ビュー行列(WVPではなく、ビュー行列単独で持ってくる)
}
SamplerState sam:register(s0);//普通のサンプラ(DXLIBデフォルトでOK)
Texture2D<float4> tex : register(t0);//ベーステクスチャ(今回はこう書いてるが、DXライブラリのサンプルに則って持ってきた方がいい)
Texture2D<float4> norm : register(t1); //法線マップ(今回はこう書いてるが、DXライブラリのサンプルに則って持ってきた方がいい)
Texture2D<float4> rough:register(t2);//ラフネス(これは外部から追加で持ってくるテクスチャ)
Texture2D<float4> metallic:register(t3);//メタリック(これも外部から追加で持ってくるテクスチャ)
Texture2D<float4> sph:register(t4);//スフィアマップ(勿論外部から)
float4 main(PSInput input) : SV_TARGET
{
float roughness= rough.Sample(sam,input.uv).r;
//光ベクトル(実験なので固定値。実際には定数バッファから取得すべき)
float3 light = normalize(float3(1, -1, 1));
float3 ray = input.pos-cameraPos;//視線ベクトル
ray = normalize(ray);
light=normalize(light);//光線ベクトル()
//法線マップを法線ベクトルに
float3 nmap = norm.Sample(sam, input.uv);
nmap = nmap * 2 - 1;
nmap.y = -nmap.y;//Yだけ反転
float3 nm = normalize(nmap);
float3 n=
input.norm+
normalize(
(nm.x*input.tan)+//tanが接空間のXベクトル
(nm.y*input.bin)+//binが接空間のYベクトル
(nm.z*input.norm)//
);
n = normalize(n);
float diffuse = saturate(dot(n, -light));//普通にディフューズ計算
float3 ref=reflect(light, n);//光線の反射ベクトル
float spec=pow(saturate(dot(-ray, ref)),20);//光線の反射ベクトルからスペキュラ計算
float3 refRay = reflect(ray, n);//視線の反射ベクトル
refRay = mul(refRay, vmat);//カメラ方向を考慮して視線ベクトルを回転
refRay.xy = refRay.xy*float2(0.5,-0.5) + 0.5;//反射ベクトルからスフィアマップ用UVを作成
float4 spCol = sph.Sample(sam, refRay.xy);//スフィアマップカラー取得
float3 col = tex.Sample(sam,input.uv);
float met = metallic.Sample(sam, input.uv);
float ambient = 0.25f;
float3 rgb = col * max(diffuse, ambient);
float oneminusMet = 1.0 - met; //1.0-メタリック
return float4(
lerp(spCol.rgb * met * rgb + spCol.rgb * oneminusMet, rgb, oneminusMet)
+ spec, 1);
}
はい、ここまで書ければ、最初に見てもらったような表現ができるわけです。
右のボールのテクスチャだけ色々変えてみました。
まぁ、いい感じでしょ?
ある程度の不満は残りますが、DXライブラリならこのくらいでいいでしょ。よくない?
ということで、パパパッと考えて作った。金属表現でした。
DXライブラリでもキューブマップ使えるらしいので、もうちょっと時間があればそっちもやりたいんですが、今回はお手軽さ重視でこの辺で~。