前回の振り返り
今回の記事は前に投稿した記事の続きのものになります。とは言っても内容としては前回のものを見ないと分からないといったものはないのですが、もし良ければ見てあげてください。
ちなみに今回の話は前回の話と比べ物にならないくらいレベルが上がります。
自作のシェーダーで影を描画する
前回はDXライブラリにあるMakeShadowMapという関数を用いて影を描画しました。そちらでも影の実装自体はできたのですが少しばかり問題があるのと(前回の記事をご覧ください)、自分で実装したほうがいろいろと出来ることが多いとのことで今回は自作で実装していきたいと思います。
この記事を通して少しでも実装のヒントになれば幸いなのですが、今回ばかりは実装するうえで以下のスキルが必須となってきます。
実装するうえで必須スキル
①シェーダーの知識がある程度ある
②ピクセルシェーダーのコードが書ける
③頂点シェーダーのコードが書ける
最低限上の内容は身に着けてないと挫折してしまうと思います。投降者自身もある程度シェーダーを触って月日が経っているのでいきなりシェーダーノータッチで挑戦するのは控えておいたほうが良いと思います。
もし投稿者と同じ学校に通っている方だったら学校の教材でシェーダーに関するものがたくさんあるのでそちらから挑戦してみるのが吉です。
実行物の紹介
先に前回のモデルで影の実装に成功したものがこちらです。
良くないですか^^
画面についてクオリティを上げるためにいろいろしてますが(ポストエフェクトなど)とりあえず今回は影だけお話します。
影のテクスチャを作る
基本的にはこんな感じです。
手作りの資料でなんだこれって感じだと思いますが、とりあえず影を作るうえではこの影の深度値を記録したテクスチャってのを作る必要があります。
影の深度値っていうのは光源からの距離(奥行)のことを言い、この深度値を使って影を描画するかを判定を行います。
ちなみにこの流れに関してはDXライブラリのMakeShadowMapも同じだと思われます。
こちら影のテクスチャの制作フローになります。
①描画先を影のテクスチャへ設定(画面の初期化も行う)
②光源用にカメラ位置を設定
③オリジナルシェーダーの設定
④影を作りたいモデルを描画
⑤シェーダー、カメラ、描画先の設定を元に戻す
コードにするとこんな感じです。
// シャドウマップの作成
void SceneGame::DrawShadowMap()
{
SetDrawScreen(shadowMapTex_);
// スクリーンを真っ白に初期化
SetBackgroundColor(255, 255, 255);
ClearDrawScreen();
SetBackgroundColor(0, 0, 0);
// 影用に光源の設定
mainCamera.CameraSettingShadow();
// 設定したカメラのビュー行列と射影行列を保持
lightViewMatrix_ = GetCameraViewMatrix();
lightProjectionMatrix_ = GetCameraProjectionMatrix();
// オリジナルのシェーダーを設定
MV1SetUseOrigShader(TRUE);
// 深度記録画像へのメッシュ描画用の頂点シェーダーを設定
SetUseVertexShader(shadow0vertexShader_);
// 深度記録画像への描画用のピクセルシェーダーを設定
SetUsePixelShader(shadow0pixelShader_);
// ステージの描画
MV1DrawModel(stage_->GetTransform().modelId);
// シェーダー解除
MV1SetUseOrigShader(FALSE);
SetUseVertexShader(-1);
SetUsePixelShader(-1);
SetUseTextureToShader(0, -1);
// オリジナルシェーダー使用の再設定
MV1SetUseOrigShader(TRUE);
// 深度記録画像へのスキンメッシュ描画用の頂点シェーダーをセット
SetUseVertexShader(shadow6vertexShader_);
// キャラクターの描画
for (auto& player : players_)
{
MV1DrawModel(player->GetTransform().modelId);
}
// 敵の描画
for (auto& enemy : enemies_)
{
MV1DrawModel(enemy->GetTransform().modelId);
}
// シェーダー解除
MV1SetUseOrigShader(FALSE);
SetUseVertexShader(-1);
SetUsePixelShader(-1);
SetUseTextureToShader(0, -1);
// 描画スクリーンを元に戻す
SetDrawScreen(scnMng_.GetMainScreen());
// 画面初期化
ClearDrawScreen();
// カメラ設定を元に戻す
SceneManager::GetInstance().GetCamera().CameraSetting();
}
通所のモデルの描画前にこちらの関数を呼び出すことで影のテクスチャを用意して影を描画することが出来ます。
そして関数内で設定している深度値を記録する用のシェーダーコードはこちらになります。
VS_OUTPUT main(VS_INPUT vIn)
{
VS_OUTPUT ret;
// 頂点座標変換
float4 lLocalPosition;
float4 lWorldPosition;
float4 lViewPosition;
lLocalPosition.xyz = vIn.pos;
lLocalPosition.w = 1.0f;
// ローカル座標をワールド座標に変換
lWorldPosition.w = 1.0f;
lWorldPosition.xyz = mul(lLocalPosition, g_base.localWorldMatrix);
// ワールド座標をビュー座標に変換
lViewPosition.w = 1.0f;
lViewPosition.xyz = mul(lWorldPosition, g_base.viewMatrix);
// ビュー座標を射影座標に変換
ret.svPos = mul(lViewPosition, g_base.projectionMatrix);
ret.svPosRead = ret.svPos;
// 法線をローカル空間からワールド空間へ変換
ret.normal = normalize(mul(vIn.norm, (float3x3) g_base.localWorldMatrix));
// UV
ret.uv.xy = vIn.uv0.xy;
// 出力パラメータを返す
return ret;
}
SamplerState g_SrcSampler : register(s0);
Texture2D g_SrcTexture : register(t0);
float4 main(PS_INPUT pIn) : SV_TARGET
{
// Z値
float4 shadowDepth;
shadowDepth.rgb = pIn.svPosRead.z;
shadowDepth.a = 1.0f;
// 完全透過は描画しない
float4 color = g_SrcTexture.Sample(g_SrcSampler, pIn.uv);
if (color.a < 0.0001f)
{
discard;
}
return shadowDepth;
}
以上のコードを先ほどの関数内のSetUseVertexShader(shadow0vertexShader_)、SetUsePixelShader(shadow0pixelShader_)で適用してあげれば良いと思います。スキンメッシュのほうは頂点シェーダーのほうのみ変更すればすぐに実装できると思いますのでそこは是非ご自身で挑戦してみてください!
影のついたモデルの描画
ここからは先ほど作ったテクスチャを用いたモデルの描画方法について紹介していきます。
C++のほうではモデルの描画前に以下の情報を渡します。
・深度値を記録したテクスチャ
・設定した光源のカメラビュー行列
・設定した光源のカメラ射影行列
した二つの情報に関しては先ほどの関数内で保持したlightViewMatrix_、とlightProjectionMatrix_を渡してあげます。
で、肝心なシェーダーのコードが以下になります。
// 定数バッファ:スロット8番目
cbuffer cbParamShadow : register(b8)
{
float4x4 g_light_viewmatrix;
float4x4 g_light_projectionMatrix;
};
VS_OUTPUT main(VS_INPUT VSInput)
{
VS_OUTPUT ret;
// 頂点座標変換
float4 lLocalPosition;
float4 lWorldPosition;
float4 lViewPosition;
lLocalPosition.xyz = VSInput.pos;
lLocalPosition.w = 1.0f;
// ローカル座標をワールド座標に変換(剛体)
lWorldPosition.w = 1.0f;
lWorldPosition.xyz = mul(lLocalPosition, g_base.localWorldMatrix);
// ワールド座標をビュー座標に変換
lViewPosition.w = 1.0f;
lViewPosition.xyz = mul(lWorldPosition, g_base.viewMatrix);
ret.vwPos.xyz = lViewPosition.xyz;
// ビュー座標を射影座標に変換
ret.svPos = mul(lViewPosition, g_base.projectionMatrix);
// ライトのビュー座標をライトの射影座標に変換
float4 lLViewPosition = mul(g_light_viewmatrix, lWorldPosition);
// ライトのビュー座標をライトの射影座標に変換
ret.lightAtPos = mul(g_light_projectionMatrix, lLViewPosition).xyz;
// UV座標
ret.uv.x = VSInput.uv0.x;
ret.uv.y = VSInput.uv0.y;
// 法線をローカル空間からワールド空間へ変換
ret.normal = normalize(mul(VSInput.norm, (float3x3) g_base.localWorldMatrix));
// ディフューズカラー
ret.diffuse = VSInput.diffuse;
// 出力パラメータを返す
return ret;
}
float4 main(PS_INPUT PSInput) : SV_TARGET0
{
float4 color;
// テクスチャーの色を取得
color = diffuseMapTexture.Sample(diffuseMapSampler, PSInput.uv);
// マテリアルの保持
float3 material = color.rgb;
// 深度テクスチャの座標を算出
float2 depthUV;
depthUV.x = (PSInput.lightAtPos.x + 1.0f) / 2.0f;
// yは更に上下反転
depthUV.y = 1.0f - (PSInput.lightAtPos.y + 1.0f) / 2.0f;
// 深度バッファテクスチャから深度を取得
float depth = shadowMap0Texture.Sample(shadowMap0Sampler, depthUV).r;
// テクスチャに記録されている深度よりZ値が大きかったら奥にあるため輝度を半分にする
if (PSInput.lightAtPos.z > depth + 0.001f)
{
material *= 0.5f;
}
return float4(material, color.a);
}
ざっくりではあるのですが以上が影を反映したモデル描画になります。
問題と解決
ちなみに今回の実装に関して以下のように影が格子状に描画されることがありました

こちらに関してはシンプルに光源位置の問題だったため、デバッグで光源位置を自由に動かせる機能を作り、調整した位置を反映することで解決しました。
他に別プロジェクトで実装したときには、ジャギーが起きたり、影がストライプ状に描画されることがありました。
その時はシャドウマップの解像度を上げたり、描画するモデルのフレームを指定することで多少違和感なく描画させることが出来るようになりました。

上の画像では影の描画をする際にブラーをすることでジャギーが気にならないような工夫も行ったりしています。
実装して思ったこと
めっちゃムズイ!!!
正直に言うと私は今専門学校に通っていて先生にかなり支援していただいたおかげでやっと実装することが出来た感じになります。もちろんアドバイスをいただいて最終的には自分で実装はしたのですが、このサポートがなかったら実装することは難しかっただろうと感じます。
とはいえ自分の中では満足のいくレベルまで実装をすることが出来たため良かったなと思います。
影の実装者シリーズはこれで終了ですね。
この記事をどれだけの人が見てくれてるか分かりませんが少なくともこれを見てくれた後輩や同級生の方々へ私から言いたいこととしては、是非周りの人に頼ってほしいと思います。
AIを使うのも全然いいと思うのですがせっかく学校に高いお金を払って来ているので、先生方を利用するといった気持ちで制作をしてほしいと思います。特に自分たちの学校は高い技術力を持つ某S先生と某K先生がいらっしゃるので利用しないとホントにもったいないです。
後このように先生方に頼ることで大人の人とのコミュニケーション能力が身に着けられるので将来の就職活動でも役立つと思います!それが厳しいという人でもS先生だったらかなりフランクに話しても対応してくださるのでまずは相談だけでもしましょう(流石に態度はしっかりしようね)
後半何の話だよって感じですが自分が今ここまで実装できるようになったのは間違いなく先生に相談してきたからなので皆さんも是非周りの人に頼ることも視野に入れて実装してほしいと思います。
もし今回の影の実装についてもこの記事だけだと分からないことだらけだと思うので、私でよければ気軽に質問して頂けたらと思います!
おわり!!!

