ポイントライト
ポイントライトはディレクショナルライトとは異なり、下記点を考慮する必要がある。
- 入射してくる光の方向
- 光源との距離による光の減衰。
◇入射してくる光の方向
ポイントライトでも光の方向ベクトルの計算を行います。
ポイントライトのワールド座標とサーフェースのワールド座標を使用して光の方向を求めます。その際結果を正規化します。
float3 LightDirection=surface.worldPosition-pointLight.Position;
LightDirection=Normalize(LightDirection);
◇光源との距離による光の減衰
ポイントライトの光の影響はこうげんからの距離に応じて減衰する。
今回実装するポイントライトは距離に比例して影響力が0になるようにする。
ポイントライトとサーフェースの距離をD、ポイントライトが影響を与える範囲をRとしたとき、影響力Aをこのように求める
$$
A=1-1/R×D
$$
この計算をさせることで離れれば離れるほど比例して影響力が減ります。
ただこの結果だと線形な変化なので指数関数的な変化をさせるようにするために、pow関数を使用してリアルな表現にしていく。
ポイントライトの実装
C++での実装内容は変数を増やしたりポイントライト用の設定をするだけなので説明を省きます。
◇HLSLでの実装
■定数バッファーの受け取り
// ライトデータにアクセスするための定数バッファーを用意する
cbuffer DirectionLightCb : register(b1)
{
// ディレクションライト用のデータ
float3 dirDirection; // ライトの方向
float3 dirColor; // ライトのカラー
// step-6 定数バッファーにポイントライト用の変数を追加
float3 pointPosition; // ライトの位置
float3 pointColor; // ライトのカラー
float pointRange; // ライトの有効範囲
float3 eyePos; // 視点の位置
float3 ambientLight; // アンビエントライト
};
■サーフェースに入射するポイントライトの光の向きを計算
// step-7 サーフェイスに入射するポイントライトの光の向きを計算する
float3 LightDirector = psIn.worldPos - pointPosition;
LightDirector = normalize(LightDirector);
■減衰なしの拡散反射光と鏡面反射光の計算
// step-8 減衰なしのLambert拡散反射光を計算する
float3 diffusePointLight = CalcLambertDiffuse(LightDirector, pointColor, psIn.normal);
// step-9 減衰なしのPhong鏡面反射光を計算する
float3 specularLight = CalcPhongSpecular(LightDirector, pointColor, psIn.worldPos, psIn.normal);
■距離による影響率の計算しディレクショナルライトの計算結果と合成
// step-10 距離による影響率を計算する
float distance = length(psIn.worldPos - pointPosition);
float affect = 1.0f - (1.0f / pointRange) * distance;
if(affect<0.0f)
{
affect = 0.0f;
}
affect = pow(affect, 3.0f);
// step-11 拡散反射光と鏡面反射光に影響率を乗算して影響を弱める
diffusePointLight *= affect;
specularLight *= affect;
// step-12 2つの反射光を合算して最終的な反射光を求める
float3 diffuseLig = diffusePointLight + diffDirection;
float3 specularLig = specularLight + specDirection;
スポットライト
スポットライトとは、3Dグラフィクスにおいて懐中電灯のように特定の方向を円錐状に照らす表現のこと。
引用:https://becgartist.com/redshift-spot-light-with-environment/
現実だと、光の筋が見えるようにもなっていますが、今回は物体に照射された光のみを実装していきます。
◇実装の流れ
ポイントライトと実装内容はほとんど同じです。ポイントライトのデータに光の放射方向と投射角度を追加するだけ。
- スポットライトを光源とみなしてポイントライトを計算する。
- スポットライトの位置から、サーフェースに向かって伸びるベクトルを計算
- 求めたベクトルとスポットライトの射出方向の内積を使って、角度を求める。
- 求めた角度を使ってライトの影響率を計算する。
◇1.スポットライトの位置を光源とみなして、ポイントライトの計算する
この処理はポイントライトの処理と全く一緒です。スポットライトの位置と照射範囲を使ってポイントライトと同じ処理を実装します。
◇2.スポットライトの位置からサーフェースに向かって伸びるベクトルを計算する
各サーフェースに延びるベクトルの計算をします。このベクトルは、サーフェースのワールド座標から、スポットライト座標を引くことで求められる。このベクトルは正規化を行う必要があります。
◇3.求めたベクトルとスポットライトの射出方向とに内積を使って角度を求める
「正規化されたベクトル同士の内積の結果は、その2つのベクトルのなす角度の余弦の値になる」という性質があります。
地面に向かって伸びるベクトルと射出方向を用いることで射出角度が求められる。
◇4.求めた角度を使って、ライトの影響率を計算をする
最後に3.で求めたライトの影響を減衰させる。角度が大きくなるにつ入れてライトの影響を下げることを実現させる。
スポットライトの実装
詳細な実装内容は載せませんが、スポットライトを実装するうえで重要な点を記載していきます。
◇C++側
■スポットライト用のメンバー変数を追加
// step-1 ライト構造体にスポットライト用のメンバ変数を追加
Vector3 spPosition; // 位置
float pad3; // パディング
Vector3 spColor; // カラー
float spRange; // 影響範囲
Vector3 spDirection; // 方向
float spAngle; // スポットライトの角度
◇HLSL
■スポットライトのデータにアクセスするための変数を追加する
// step-5 スポットライトのデータにアクセスするための変数を追加する
float3 spPosition; // スポットライトの位置
float3 spColor; // スポットライトのカラー
float spRange; // スポットライトの影響範囲
float3 spDirection; // スポットライトの方向
float spAngle; // スポットライトの角度
■サーフェースに入射するスポットライトの光の向きを計算する
float3 LightDirection = psIn.worldPos - spPosition;
LightDirection = normalize(LightDirection);
■減衰なしのLambert拡散反射光を計算する
float3 DiffuseSpotLight=CalcLambertDiffuse(
LightDirection, // ライトの方向
spColor, // ライトのカラー
psIn.normal // サーフェイスの法線
);
■減衰なしのPhong鏡面反射光を計算する
float3 SpecularSpotLight=CalcPhongSpecular(
LightDirection, // ライトの方向
spColor, // ライトのカラー
psIn.worldPos, // サーフェイズのワールド座標
psIn.normal // サーフェイズの法線
);
■距離による影響率を計算
// step-9 距離による影響率を計算する
float3 distance = length(psIn.worldPos - spPosition);
float affect = 1.0f - 1.0f / spRange * distance;
if(affect<0.0f)
{
affect = 0.0f;
}
affect = pow(affect, 3.0f);
■拡散反射光・鏡面反射光に減衰率を適用
// step-10 影響率を乗算して反射光を弱める
DiffuseSpotLight *= affect;
SpecularSpotLight *= affect;
■入射光と射出方向の角度を求める
float angle = dot(LightDirection, spDirection);
angle = abs(acos(angle));
■角度による影響率を求める
// step-12 角度による影響率を求める
affect = 1.0f - 1.0f / spAngle * angle;
if (affect < 0.0f)
{
affect = 0.0f;
}
■角度による影響率を反射光に乗算する
DiffuseSpotLight *= affect;
SpecularSpotLight *= affect;
これで、スポットライトの実装が出来ました。
普段Unreal Engineで開発をしているのですが、このようにしてスポットライトを作成していることが学べてよかったです。
リムライト
リムライトは逆行ライトともよばれるライトです。輪郭表現をする際に使えるので、フレネル表現に近しそうです。
引用:https://assets.st-note.com/img/1702914588649-ROtDPYwTLv.png?width=1200
◇実装に必要なこと
- サーフェースの法線と光の入射方向
- サーフェースの法線と視線の方向
■サーフェースの法線と光の入射方向
リムライトは、光の向きとサーフェースの法線が垂直に近い個所で強く発生する。
リムライトの強さは、光の向きとサーフェースの法線とで内積を利用すると求められる。
二つのベクトルのなす角が0°なら1.0、90°なら0.0、180°なら-1.0となる。
- V1×V2=1
- V1×V3=-1
- V1×V4=0
各ベクトルを内積すると上記のような結果になる。今回は、法線が垂直に近づくほどリムライトの影響を受けるようにしたいので、以下のような計算でリムライトの強さを求める。
$$
1-max(0,ライトの方向 ×法線)
$$
この計算式であれば、ライトの方向と法線の角度が垂直の場合内積の結果が0になるため計算結果が1となり、逆にライトの方向と法線が同じ向きの場合は1になるため結果0となる。
■サーフェースの法線と視線の方向
リムライトの強度は視点の位置によって変わってくる。
この視点と法線の方向の強度の求め方は、以下の通り
$$
1-max(0,視線の方向・法線×-1)
$$
SPSIn VSMain(SVSIn vsIn, uniform bool hasSkin)
{
SPSIn psIn;
psIn.pos = mul(mWorld, vsIn.pos); // モデルの頂点をワールド座標系に変換
psIn.worldPos = psIn.pos;
psIn.pos = mul(mView, psIn.pos); // ワールド座標系からカメラ座標系に変換
psIn.pos = mul(mProj, psIn.pos); // カメラ座標系からスクリーン座標系に変換
// 頂点法線をピクセルシェーダーに渡す
psIn.normal = mul(mWorld, vsIn.normal); // 法線を回転させる
psIn.uv = vsIn.uv;
// step-2 カメラ空間の法線を求める
psIn.normalInView = mul(mView, vsIn.normal);
return psIn;
}
/// <summary>
/// モデル用のピクセルシェーダーのエントリーポイント
/// </summary>
float4 PSMain(SPSIn psIn) : SV_Target0
{
// ディレクションライトによるライティングを計算する
float3 directionLig = CalcLigFromDirectionLight(psIn);
// リムライトの強さを求める
// step-3 サーフェイスの法線と光の入射方向に依存するリムの強さを求める
float1 power1 = 1.0f - max(0.0f, dot(dirDirection, psIn.normal));
// step-4 サーフェイスの法線と視線の方向に依存するリムの強さを求める
float2 power2 = 1.0f - max(0.0f, psIn.normalInView.z - 1.0f);
// step-5 最終的なリムの強さを求める
float limPower = power1 * power2;
limPower = pow(limPower, 1.3f);
// 最終的な反射光を求める
float3 finalLig = directionLig + ambientLight;
// step-6 最終的な反射光にリムライトの反射光を合算する
float4 finalColor = g_texture.Sample(g_sampler, psIn.uv);
// テクスチャカラーに求めた光を乗算して最終出力カラーを求める
finalColor.xyz *= finalLig;
return finalColor;
}