キャラクターの真下にShaderを使って丸い影を描く(投影丸影)
🎯 この影の作り方のイメージ
プレイヤーの下に「黒い丸いライト」を当てて、地面をちょっとだけ暗く見せるという工夫です。
ライトなので、授業でやった点光源がわかっていれば全く同じように実装できます。
しかも、現在のシェーダーにコンスタントバッファと、影付け部分を足すだけでできます。
こんな感じの影です👇
👦 ← プレイヤー(ジャンプ中でもOK)
↓
黒い光を下に照らす
↓
 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
● ← 黒い影(地面に丸く表示される)
 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄🧠 なぜこんな影を作るの?
本物の影は、光を遮ってリアルタイムに計算するので**とても重たい(処理が大変)**です。
でも、このやり方は:
項目 | 内容 |
|---|---|
🎮 ゲーム性能 | とても軽い!(超高速) |
🧠 理解しやすさ | 仕組みが簡単! |
📦 実装方法 | ライトと同じように扱える |
✅ どうやって作るの?
① まず、プレイヤーの位置(XZ)をHLSLに送る
casterPos.xzという変数で、影の中心の位置を指定します。- これはプレイヤーの真下の位置(XZ座標)です。
cbuffer ShadowParam : register(b2)
{
float4 casterPos; // プレイヤーのXZ位置(影の中心)
float4 shadowParams; // x:半径, y:ぼかし, z:濃さ, w:高さ
}② ピクセルシェーダーで、今の描画場所とプレイヤーの距離を調べる
float2 d = inData.wpos.xz - casterPos.xz;
float distSq = dot(d, d); // 二乗距離(速い)これで「このピクセルが影の中心からどれくらい離れてるか」が分かります。
③ 半径の中にあるなら、少し黒くする
float radius = shadowParams.x;
float softness = shadowParams.y;
float alphaScale = shadowParams.z;
float intensity = saturate((radius * radius - distSq) * softness);
float shadowAlpha = intensity * alphaScale;名前 | 意味 |
|---|---|
| 影の半径(大きさ) |
| 境界のぼやけ具合 |
| 影の濃さ |
④ 影の濃さを、黒で合成!
result.rgb = lerp(result.rgb, float3(0, 0, 0), shadowAlpha);これで、shadowAlpha の強さだけ黒くなります。
⑤ 高さに応じて調整する(ジャンプ中も自然に)
float heightRatio = saturate(shadowParams.w * 0.3f);
float dynamicRadius = lerp(radius, radius * 1.8f, heightRatio);
float dynamicAlphaScale = lerp(alphaScale, alphaScale * 0.3f, heightRatio);高さが低いとき(地面) | 小さくて濃い影 |
|---|---|
高さが高いとき(ジャンプ中) | 大きくて薄い影 |
📝 最後にまとめると
項目 | 内容 |
|---|---|
💡 本質 | 黒いスポットライトの逆(黒く照らす) |
🎮 パフォーマンス | 超軽い。リアル影の100倍速いことも |
🧠 理解度 | 円の距離と濃さを調べて、ちょっと黒くするだけ |
📏 調整可能 | 半径、ぼかし、濃さ、高さに応じて調整できる |
📌 図解(かんたんに)
👦
↑ プレイヤー
| 高さ(距離)
↓
┌──────────────────┐
│ ●●●●●●●●●●● │ ← 黒い影(中心からだんだん薄く)
└──────────────────┘コンスタントバッファへの毎フレームのプレイヤーの位置とパラメタの渡し方
💡 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; //ワールド座標(ピクセルシェーダーで影を計算するため)
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
//ここから下、点光源使った影の描画
// --- 高さに応じたサイズ&濃さ調整 ---
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);
// 影の濃さ(高さにも応じて変化)
float shadowAlpha = intensity * dynamicAlphaScale;
// 合成(黒く塗る)
result.rgb = lerp(result.rgb, float3(0, 0, 0), shadowAlpha);
return clamp(result, 0.0f, 1.0f);
}