DXライブラリでデカールを実装してみる②(弾痕デカール)
この記事は
「DXライブラリでデカールを実装してみる①」
https://qiita.com/tsuchinokoman/items/307f8709f862a9fb9642
の続きです。
前提となる前段階がありますので、そちらを見てない人はまずはそちらを見てから、この記事をお読みください。
3Dデカールの理屈
自前で3Dデカールを実装することについて、あまりこれといった文献も、Web記事もないためあくまでも「こうなんちゃう?」で実装してるにすぎない事はご了承ください。
もし「これがデファクトスタンダードなやり方なんDA!」というやり方をご存じの方がおられましたら、こそーっと教えてくだちゃい
はい、では、説明していきます。前回の2Dとも通ずるところがあるのですが、まずデカールのための直方体を考えます。
この直方体は
図のような、座標軸付きの直方体を考えます。そしてモデルのピクセルシェーダ内で描画する際に、この直方体の座標軸に「射影」します。
分かりづらいため1次元的に説明しますね
丸っこい方がモデル側で、四角い方がデカール側だと思ってください。モデル側を描画する時に赤い矢印に対して射影を行うとデカール側の座標系と重なります。モデルのワールド座標をPとして、デカール座標軸のX軸ベクトルをXとすると射影は
射影=\vec{P}\cdot\vec{X}
で、デカールボックスの中心点Cからピクセルシェーダ内でのワールド座標Pへのベクトルを$\vec{V}$とし、これをデカールが持っている三軸$$\vec{X},\vec{Y},\vec{Z}$$ベクトルにそれぞれ対して内積して射影長を求めます。射影長をそれぞれ$d_x , d_y , d_z$とすると
\displaylines{
d_x=\vec{V}\cdot\vec{X}\\
d_y=\vec{V}\cdot\vec{Y}\\
d_z=\vec{V}\cdot\vec{Z}
}
で、この射影長のなかの$d_x$および$d_y$はあらかじめ設定しておいた直方体のサイズでそれぞれ割ってやれば対応するUVが得られます。ただし中心からのズレがあるため0.5を足すことになります。また、Y方向は上下逆転する必要がありますね。つまり
\displaylines{
U=\frac{d_x}{W}+0.5\\
V=0.5-\frac{d_y}{H}\\
}
では、$d_z$は何に使うのかというと、現在のピクセルのワールド座標がこの範囲内に入っていたらデカールを描画し、入っていなければデカールは描画しないようにします。
3Dデカールの実装
では、この理屈をもとにデカールを実装していきましょう
C++側
デカール構造体の作成
まず、デカール直方体(軸つき)に必要な構造体を定義しましょう。この情報はコンスタントバッファを通じてシェーダ側にも送られるため、シェーダ側にも同じ構造体を定義することになります。
//デカール直方体
//定数バッファでHLSLに渡すときは16byteパッキングされてしまうため
//このように16バイト区切りになるように並べている
struct DecalBox {
Position3 pos;//デカール
int enable;//有効フラグ
Vector3 right;//右ベクトル
float width;//幅
Vector3 up;//上ベクトル
float height;//高さ
Vector3 front;//前ベクトル
float depth;//奥行
};
コメントにも書いてあるように、コンスタントバッファに乗せてシェーダ側に渡すために16byte境界にする必要がありますのでご注意ください
https://learn.microsoft.com/ja-jp/windows/win32/direct3dhlsl/dx-graphics-hlsl-packing-rules
デカール用コンスタントバッファ作成
んで、このデカールを数個(今回は最大16個)貼り付けたいため、GPU側のメモリを最大値ぶん確保しておきます
constexpr int bullet_hole_num = 16;//弾痕デカール最大値
(中略)
//コンスタントバッファ用のメモリ領域を確保(CPU/GPU両方から見れている)
int decalCBuffH= DxLib::CreateShaderConstantBuffer(sizeof(DecalBox)*bullet_hole_num);
//とりあえずCPU側から見れるようにしておく(実際はGPU側のメモリを見てるわけではなく、
//キャッシュコヒーレンシによって連動しているCPU側のメモリを見ている)
auto decalCBuff= (DecalBox*)DxLib::GetBufferShaderConstantBuffer(decalCBuffH);
//初期化
for(int i=0;i<bullet_hole_num;++i){
decalCBuff[i] = {};
}
UpdateShaderConstantBuffer(decalCBuffH);
レイキャストで当たった場所にデカールを配置
はい、あとはレイキャストで当たり判定取って、その場所にデカール直方体をくっつける処理を書きます(これ自体はデカール処理の本質じゃないですが、ひとまずどこに配置するかは決めなきゃいけないので)
DxLibには「レイキャスト」なんて機能はないためMV1CollCheck_Line関数を使用します。
https://dxlib.xsrv.jp/function/dxfunc_3d_model_3.html#R9N4
当たった場所にデカールボックスを配置するため、デカールのPositionはHitPositionをそのまま持ってくればいいですが、問題は軸の方ですね。
軸に関してはとりあえず、カメラの向きをZ方向(front)として3軸作ります。
通常のカメラ行列を作る時と同様に「何となく上(0,1,0)」をup_vecとしています。これと外積をとる事で、前方ベクトルと上ベクトル双方に直交する「右ベクトル」を作ります。
で、右ベクトルができたら真の上ベクトルを前方ベクトルと右ベクトルから作ります。これで三軸できます。
\displaylines{
\vec{Axis_Z} = front \\
\vec{Y'}=(0,1,0)\\
\vec{Axis_X} = \vec{Y'}\times\vec{Axis_Z}\\
\vec{Axis_Y} = \vec{Axis_Z}\times{Axis_X}
}
というわけです。で、これをvector配列にブッ込んでいきます。listでも構いません。
お尻から突っ込んで、頭から取り去る所謂FILOですね。どっちが頭でもお尻でもいいんですが、トコロテン式になってればいいです。
あ、とりあえずデカールのワールド上の幅と高さと奥行は20.0の決め打ちにしています。もし自分のゲームで使いたい時はそれに合わせて変えてください
//左クリックでクリックした部分にデカールを貼り付ける
if (!(lastMouseInput & MOUSE_INPUT_LEFT) && mouseInput) {
int mx, my;
GetMousePoint(&mx, &my);//クリックした座標
auto spos = ConvScreenPosToWorldPos(VGet(mx, my, 0.0f));//クリックした座標をワールド座標に変換(Near)
auto epos = ConvScreenPosToWorldPos(VGet(mx, my, 1.0f));//クリックした座標をワールド座標に変換(Far)
//MV1CollCheck_Lineはモデル全体ではなくフレームごとに分かれているため
//ループさせとく。どれか一つに当たればループは抜ける
auto framenum = MV1GetFrameNum(model);
for(int i=0;i<framenum;++i){
auto collResult = DxLib::MV1CollCheck_Line(model,i,spos,epos);
if (collResult.HitFlag) {//どれかに当たった
auto hitpos = GetVector3(collResult.HitPosition);//当たった座標を取得
auto front = GetVector3(VSub(epos , spos)).Normalized();//クリックした座標のレイをfrontとする
auto right = mgt_lib::Cross(up_vec, front).Normalized();//外積で右ベクトルを計算
auto up = mgt_lib::Cross(front, right).Normalized();//外積で上ベクトルを計算
boxes.push_back(mgt_lib::Cuboid(hitpos, { right,up,front}, Size3D(20.0f,20.0f,20.0f)));
if(boxes.size()>bullet_hole_num){//最大値を超えたら最初のデカールは削除
boxes.pop_front();
}
//定数バッファは最後のデカールのみ更新する
auto idx = boxes.size() - 1;
const auto& box = boxes.back();
decalCBuff[idx].enable = 1;
decalCBuff[idx].pos = box.GetPos();
decalCBuff[idx].right = box.GetAxis().right;
decalCBuff[idx].up = box.GetAxis().up;
decalCBuff[idx].front = box.GetAxis().front;
decalCBuff[idx].width= box.GetSize().x;
decalCBuff[idx].height= box.GetSize().y;
decalCBuff[idx].depth= box.GetSize().z;
UpdateShaderConstantBuffer(decalCBuffH);
break;
}
}
}
はい、これで中心座標と軸ベクトルとサイズがHLSL側から参照できる状態になりました。
次はピクセルシェーダ側を見ていきましょう
ピクセルシェーダ側
デカール構造体と、定数バッファ受け取り部分
といってもC++側と同じ構造ですが、float4を使用する所だけ異なりますね。それぞれの軸ベクトルのw部分をサイズとしています。
//デカール直方体
struct DecalBox
{
float3 pos;//中心点
int enable;//有効フラグ
float4 right;//右(および幅)
float4 up;//上(および高さ)
float4 front;//前(および奥行)
};
有効フラグはintで受け取っています。はい、これが配列になっていますので
#define bullet_hole_num 16
cbuffer ConstatnBuffer : register(b4)
{
DecalBox g_decalBox[bullet_hole_num];
}
ピクセルシェーダ関数
はい、では一気にプログラムを書いていきましょう
float4 main(PS_INPUT psinput) : SV_TARGET
{
float3 light = normalize(float3(1, -1, 1));//とりあえず簡易ライト
float3 uvw=0;//デカール直方体のサイズ
float4 decal = 0;
//最大デカール数だけぶんまわす
//現在のピクセルに対応するデカールがあったら
//ループは抜ける
for (int i = 0; i < bullet_hole_num; ++i)
{
if (g_decalBox[i].enable)
{
float3 vec = psinput.pos - g_decalBox[i].pos;//デカール中心点から現在のピクセルワールド座標へのベクトルを作る
//3軸に対して内積(射影)をとる
uvw = float3(dot(vec, g_decalBox[i].right.xyz), dot(vec, g_decalBox[i].up.xyz), dot(vec, g_decalBox[i].front.xyz));
//三軸への射影長を、直方体のサイズで割る
uvw /= float3(g_decalBox[i].right.w, g_decalBox[i].up.w, g_decalBox[i].front.w);
//割った結果が-1~1以内であればデカール対象
if (abs(uvw.x) < 1.0 && abs(uvw.y) < 1.0 && abs(uvw.z) < 1.0)
{
//UVに使用するのはXYのみ。-1~+1⇒0~1変換を行う
uvw.xy = float2(uvw.x,-uvw.y)+1.0;//Yだけ上下反転させておく
uvw.xy /= 2.0;//
decal = decalTex.Sample(smp, uvw.xy);//デカール側のテクスチャを取得
break;
}
}
}
//デカール側のテクスチャ内容が入っていなければ元の絵を表示する
if (decal.a == 0)
{
decal = tex.Sample(smp, psinput.TexCoords0)*psinput.Diffuse;
}
//あとはまぁなんかシェーディング
float bright = max(saturate(dot(-light, psinput.Normal)), 0.65);
float3 vec = normalize(g_decal.eye - psinput.pos);
float spec = pow(saturate(dot(reflect(light, psinput.Normal), vec)), 20.0);
return float4(decal.rgb*bright+spec, 1.0);
}
結果発表
はい、ここまでやればこんな結果になります
動画だとこう
https://x.com/CTsuchinoko/status/1880220439961886751
ここで終わりッ!と言いたいのですが、ここまでだと静的なオブジェクトにしか使えないんですよね。
どういう事かというと、つまり動く奴には使えない。何でかというとデカールボックスをワールドに配置しているだけだから。
建物や地面などの静的な背景オブジェクトに対する弾痕ッてな用途ならこれでいいと思いますが、動くキャラクターには使えないのです
それを解決するには、キャラクターからのオフセットをとる必要がありますし、スケルタルメッシュに対してはボーンまで考慮しなければいけなくなります。
それやるとまた長くなりそうなので、それはまた次の機会にしましょう。今回はここまで。お疲れさまでした。