キャラクターの真下にShaderを使って丸い影を描く(投影丸影)
🎯 この影の作り方のイメージ
プレイヤーの下に「黒い丸いライト」を当てて、地面をちょっとだけ暗く見せるという工夫です。
ライトなので、授業でやった点光源がわかっていれば全く同じように実装できます。
しかも、現在のシェーダーにコンスタントバッファと、影付け部分を足すだけでできます。
こんな感じの影です👇
👦 ← プレイヤー(ジャンプ中でもOK)
↓
黒い光を下に照らす
↓
 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
● ← 黒い影(地面に丸く表示される)
 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄🧠 なぜこんな影を作るの?
本物の影は、光を遮ってリアルタイムに計算するので**とても重たい(処理が大変)**です。
でも、このやり方は:
項目 | 内容 |
|---|---|
🎮 ゲーム性能 | とても軽い!(超高速) |
🧠 理解しやすさ | 仕組みが簡単! |
📦 実装方法 | ライトと同じように扱える |
✅ どうやって作るの?
①丸影(Blob まShadow)導入フロー
1. 🎯 目的整理
- 丸影を「地面に貼り付けるような」影として合成する
- 地面用の描画パスに統合せず、「影合成処理」は別パスとして独立させたい
- プレイヤーの現在位置(XZ)と高さを
HLSLに送る
使って「影の位置・サイズ・濃さ」を計算
casterPos.xz2. という変数で、📦 影定数バッファの中心の位置設計(CBShadow)を指定します。これはプレイヤーの真下の位置(XZ座標)です。
cbufferstruct ShadowParam : register(b2)CBShadow {
float4XMFLOAT4 casterPos; // プレイヤーのXZ位置XZ座標(影Yは使わない:お空の中心方向を表すから)
float4XMFLOAT4 shadowParams; // x:半径,(softness, y:ぼかし,alphaScale, z:濃さ,unused, w:playerHeightY)
};.wに高さを埋めることで1スロットで完結b2スロットなど空いてる定数バッファにバインド
3. }
🧠 HLSL シェーダー統合 (3Dのhlslの後ろに追加するなど?)
②⚙ 丸影のアルファ合成ロジックをピクセルシェーダーで、今の描画場所とプレイヤーの距離を調べる末尾に追加
float2 dcasterXZ = casterPos.xz;
float2 pixelXZ = inData.wpos.xzxz;
float2 diff = pixelXZ - casterPos.xz;casterXZ;
float distSq = dot(d,diff, d)diff);
// 二乗距離(速い)これで「このピクセルが影の中心からどれくらい離れてるか」が分かります。
③ 半径の中にあるなら、少し黒くする
float radiussoftness = shadowParams.x;
float softnessalphaScale = shadowParams.y;
float alphaScaleheightY = shadowParams.z;w;
float intensityheightRatio = saturate(heightY / 2.0f); // 最大ジャンプ2.0f想定
float radius = lerp(0.4f, 1.0f, heightRatio);
float alpha = lerp(0.6f, 0.1f, heightRatio);
float shadowAlpha = saturate((radius * radius - distSq) * softness);
float shadowAlpha = intensity * alphaScale;
|
|
|---|---|
| alpha;
// 丸影 |
|
|
|
|
④ 影の濃さを、黒で合成!
result.rgb(必要に応じて色も乗せられる)
float4 shadowColor = lerp(result.rgb, float3(float4(0, 0, 0),0, shadowAlpha);
return lerp(resultColor, shadowColor, shadowAlpha);これで、
4. 🧩 C++側:描画前にプレイヤーの情報を渡す
Stage::Draw や Stage::Update にて:
CBShadow shadowCB;
shadowCB.casterPos = XMFLOAT4(playerPos.x, 0.0f, playerPos.z, 1.0f);
shadowCB.shadowParams = XMFLOAT4(softness, alphaScale, 0.0f, playerHeightY);
// 書き込み → バッファ更新
context->UpdateSubresource(pCBShadow, 0, nullptr, &shadowCB, 0, 0);
context->PSSetConstantBuffers(2, 1, &pCBShadow);shadowAlphaplayerHeightY = playerPos.y - Stage::GetTerrainHeight(playerPos.x, playerPos.z)- プレイヤーの高さを計算して
.wに渡すのがポイント
5. 🛠 描画順序
- 通常の地面 + モデル描画(
Model::Draw()) - その後
Stage.hlslの強さだけ黒くなります。ピクセルシェーダー内で影を合成(クワッド描画不要)
⑤6. 高さに応じて🧪 確認と調整する(ジャンプ中も自然に)
float heightRatio = saturate(shadowParams.w * 0.3f);
float dynamicRadius = lerp(radius, radius * 1.8f, heightRatio);
float dynamicAlphaScale = lerp(alphaScale, alphaScale * 0.3f, heightRatio);
|
|
|---|---|
|
|
📝 最後にまとめると
テスト項目 | 備考 |
|---|---|
影が正しい位置に出るか | XZが正しく渡っているか |
高さでサイズ変化するか |
の補間式が効いているか |
透明度が変化しているか |
の計算式の係数調整 |
カメラを回しても違和感ないか | 視差が出ないか、影が地面に貼りついて見えるか |
✅ 最終的な関数と構造の一覧
要素 | 内容 |
|---|---|
|
|
| 定数バッ |
|
|
|
|
|
|
📌 図解(かんたんに)
👦
↑ プレイヤー
| 高さ(距離)
↓
┌──────────────────┐
│ ●●●●●●●●●●● │ ← 黒い影(中心からだんだん薄く)
└──────────────────┘コンスタントバッファへの毎フレームのプレイヤーの位置とパラメタの渡し方
💡 1. プレイヤーの位置と影の設定を準備
float4 casterPos = { playerX, playerY, playerZ, 1.0f };
float4 shadowParams = { radius, softness, alphaScale, height };casterPos→ 影を作りたいプレイヤーの位置(XZだけ使う)shadowParams→ 影の大きさ・ぼかし具合・濃さ・高さ
🔧 2. それを GPU 用のバッファに入れて…
device->CreateBuffer(...); // ID3D11Buffer* cbShadow
context->UpdateSubresource(cbShadow, ...);🚀 3. ピクセルシェーダーのスロットb2に渡す
context->PSSetConstantBuffers(2, 1, &cbShadow);🎨 4. HLSL の cbuffer ShadowParam : register(b2) に届く
これで casterPos と shadowParams がシェーダーの中で使えるようになります!
具体的な成コード
// 影の中心位置とパラメータ(半径、ソフトネス、アルファスケール)
struct CBShadow {
XMFLOAT4 casterPos; // 足元中心位置 (x, 0.0f, z, 1.0f)
XMFLOAT4 shadowParams; // casterPos: 足元中心位置, shadowParams: 半径, フェード係数, 濃度, 予備
} cbShadow;
XMFLOAT3 pos = player_->GetShadowCasterPos(); //プレイヤー位置の取得
float dist = player_->GetDistanceToGround(); //プレイヤーの床からの高さの取得
if (dist > 500) dist = 0.0f; //距離デカすぎたら距離0を入れておく(HLSLの方で影が出ないように)
cbShadow.casterPos = XMFLOAT4(pos.x, 0.0f, pos.z, 1.0f); // 足元中心
cbShadow.shadowParams = XMFLOAT4(0.4f, 8.0f, 0.5f, dist); // 半径・フェード係数・濃度
D3D11_MAPPED_SUBRESOURCE res;
Direct3D::pContext_->Map(pCBShadow_, 0, D3D11_MAP_WRITE_DISCARD, 0, &res);
memcpy(res.pData, &cbShadow, sizeof(cbShadow));
Direct3D::pContext_->Unmap(pCBShadow_, 0);
// 送信 バッファのスロット2番に影の定数バッファをセット
Direct3D::pContext_->PSSetConstantBuffers(2, 1, &pCBShadow_);あとはHLSLに、影付け部分を追加
//───────────────────────────────────────
// テクスチャ&サンプラーデータのグローバル変数定義
//───────────────────────────────────────
Texture2D g_texture : register(t0); //テクスチャー
SamplerState g_sampler : register(s0); //サンプラー
//───────────────────────────────────────
// コンスタントバッファ
// DirectX 側から送信されてくる、ポリゴン頂点以外の諸情報の定義
//───────────────────────────────────────
cbuffer global : register(b0)
{
float4x4 g_matWVP; // ワールド・ビュー・プロジェクションの合成行列
float4x4 g_matNormalTrans; // 法線の変換行列(回転行列と拡大の逆行列)
float4x4 g_matWorld; // ワールド変換行列
float4 g_vecLightDir; // ライトの方向ベクトル
float4 g_vecDiffuse; // ディフューズカラー(マテリアルの色)
float4 g_vecAmbient; // アンビエントカラー(影の色)
float4 g_vecSpeculer; // スペキュラーカラー(ハイライトの色)
float4 g_vecCameraPosition; // 視点(カメラの位置)
float g_shuniness; // ハイライトの強さ(テカリ具合)
bool g_isTexture; // テクスチャ貼ってあるかどうか
};
// b2: プレイヤー影
cbuffer ShadowParam : register(b2)
{
float4 casterPos; // プレイヤーのXZ位置
float4 shadowParams; // (radius, softness, alphaScale, unused)
}
//───────────────────────────────────────
// 頂点シェーダー出力&ピクセルシェーダー入力データ構造体
//───────────────────────────────────────
struct VS_OUT
{
float4 wpos : TEXCOORD3; //ワールド座標(ピクセルシェーダーで影を計算するため内)GetTerrainHeight(x, float4 pos : SV_POSITION; //ピクセル位置
float4 normal : TEXCOORD2; //法線
float2 uv : TEXCOORD0; //UV座標
float4 eye : TEXCOORD1; //視線
};
//───────────────────────────────────────
// 頂点シェーダ
//───────────────────────────────────────
VS_OUT VS(float4 pos : POSITION, float4 Normal : NORMAL, float2 Uv : TEXCOORD)
{
//ピクセルシェーダーへ渡す情報
VS_OUT outData;
//ローカル座標に、ワールド・ビュー・プロジェクション行列をかけて
//スクリーン座標に変換し、ピクセルシェーダーへ
outData.pos = mul(pos, g_matWVP);
outData.wpos = mul(pos, g_matWorld); //ワールド座標に変換
//法線の変形
Normal.w = 0; //4次元目は使わないので0
Normal = mul(Normal, g_matNormalTrans); //オブジェクトが変形すれば法線も変形
outData.normal = Normal; //これをピクセルシェーダーへ
//視線ベクトル(ハイライトの計算に必要
//float4 worldPos = mul(pos, g_matWorld); //ローカル座標にワールド行列をかけてワールド座標へ
outData.eye = normalize(g_vecCameraPosition - outData.wpos); //視点から頂点位置を引き算し視線を求めてピクセルシェーダーへ
//UV「座標
outData.uv = Uv.xy; //そのままピクセルシェーダーへ
//まとめて出力
return outData;
}
//───────────────────────────────────────
// ピクセルシェーダ
//───────────────────────────────────────
float4 PS(VS_OUT inData) : SV_Target
{
//ライトの向き
float4 lightDir = g_vecLightDir; //グルーバル変数は変更できないので、いったんローカル変数へ
lightDir = normalize(lightDir); //向きだけが必要なので正規化
//法線はピクセルシェーダーに持ってきた時点で補完され長さが変わっている
//正規化しておかないと面の明るさがおかしくなる
inData.normal = normalize(inData.normal);
//拡散反射光(ディフューズ)
//法線と光のベクトルの内積が、そこの明るさになる
float4 shade = saturate(dot(inData.normal, -lightDir));
shade.a = 1; //暗いところが透明になるので、強制的にアルファは1
float4 diffuse;
//テクスチャ有無
if (g_isTexture == true)
{
//テクスチャの色
diffuse = g_texture.Sample(g_sampler, inData.uv);
}
else
{
//マテリアルの色
diffuse = g_vecDiffuse;
//diffuse = float4(0, 0, 0, 0);
}
//環境光(アンビエント)
//これはMaya側で指定し、グローバル変数で受け取ったものをそのまま
float4 ambient = g_vecAmbient;
//鏡面反射光(スペキュラー)
float4 speculer = float4(0, 0, 0, 0); //とりあえずハイライトは無しにしておいて…
if (g_vecSpeculer.a != 0) //スペキュラーの情報があれば
{
float4 R = reflect(lightDir, inData.normal); //正反射ベクトル
speculer = pow(saturate(dot(R, inData.eye)), g_shuniness) * g_vecSpeculer; //ハイライトを求める
}
float4 result = diffuse * shade + diffuse * ambient + speculer;
//最終的な色
//ここまでいつものSimple3D
//ここから下、点光源使った影の描画
// --- z)
高さに応じたサイズ&濃さ調整 ---
float heightRatio = saturate(shadowParams.w * 0.3f); // 高さ正規化推定関数(0.0〜1.0)
// 高さに応じて影を大きく&薄くする
float dynamicRadius = lerp(shadowParams.x, shadowParams.x * 1.8f, heightRatio); // 半径拡大
float dynamicAlphaScale = lerp(shadowParams.z, shadowParams.z * 0.3f, heightRatio); // α濃度減衰
// プレイヤーとピクセル間のXZ距離
float2 d = inData.wpos.xz - casterPos.xz;
float distSq = dot(d, d);
// 距離に応じたフェードアウト
float intensity = saturate((dynamicRadius * dynamicRadius - distSq) * shadowParams.y);
// 影の濃さ(高さにも応じて変化Stageが提供)
float shadowAlpha = intensity * dynamicAlphaScale;
// 合成(黒く塗る)
result.rgb = lerp(result.rgb, float3(0, 0, 0), shadowAlpha);
return clamp(result, 0.0f, 1.0f);
}