#はじめに
誤字脱字や無駄な部分、間違いなどが必ずあると思うので、気づいた方はどんどんツッコミをください。また、「なぜか実装できている」という可能性も大いにあるので、あまりこの記事を信用しないでください…
#概要
D3D12でTBDRを実装したときのことをまとめただけの記事です。半分備忘録的なところがあります。
#参考資料
実装にあたり、参考にさせていただいた資料やサイトのリンクです。
もんしょの巣穴 DirectXの話 第125回 Tile-based Rendering
そろそろいい加減Tile-based Deferred Renderingの解説書きます
Clip Space Approach – Extracting the Planes
西川善司の3Dゲームファンのための「KILLZONE 2」グラフィックス講座(前編)
上記のリンクの記事は自分のような初心者でもわかりやすくとても参考になりました。これからTBDRを実装しようと思っている方は是非上記の記事をのぞいてみるといいと思います。
#TBDRとは
スクリーンをタイル状に分割して、それぞれのタイルとライトの接触判定を行ってから、タイルと接触しているライトのみでライト計算する技法のことで、遅延レンダリングにおいてライト計算の高速化の手法の一つとして挙げられている技法らしいです。
#TBDRの長所と短所
#####長所
- ライトをたくさん使いやすい
- ライト計算が早い
- ComputeShader一つで計算できる
#####短所
- G-Bufferが肥大化する
- マテリアルの種類を増やしにくい
- 半透明オブジェクトには使えない
- MSAAが使いづらい
TBDRの長所・短所はまだあると思いますが、おおよそこんな感じでしょうか。
短所に関しては遅延シェーディングと一緒ですね。
#実装
自分がTBDRを実装した際の手順を記述します。
もっとこうしたらいいよーとかあったら教えてください。お願いします。
###大まかな実装手順
1. G-Buffer用のテクスチャリソースを枚数ぶんID3D12Device::CreateCommittedResourceで作る。この時、レンダリング結果のテクスチャリソースはUAVにリソース遷移できるように設定してから作成する。
2. 各モデルの描画パイプラインの設定を変更。MRTに対応させるためにピクセルシェーダーの出力をGBufferの枚数分出力できるようにする。(ここではモデルのビュースペース法線・ハイライト強度・ディフューズカラーの3つを出力できればいいのでD3D12_GRAPHICS_PIPELINE_STATE_DESC::NumRenderTargetsに3を設定する。場合によってはもっと増える…かもしれない。基本的には複数の情報をできるだけ一つのテクスチャの余分な部分に入れるようにすることで必要なメモリが減る)。
3. コンピュートシェーダーの記述。また、コンピュートシェーダー用のパイプラインステートの作成。(コンピュートシェーダー用のパイプラインステートはD3D12_GRAPHICS_PIPELINE_STATE_DESCではなくD3D12_COMPUTE_PIPELINE_STATE_DESCで作らないといけない)
###各実装手順の詳細
#####手順1
G-Bufferに必要なリソースとデスクリプタを作っていきます。
#include <wrl.h>
#include <d3d12.h>
#include <dxgi1_6.h>
#include <d3dcompiler.h>
//リソースの作成
D3D12_HEAP_PROPERTIES props = {};
props.Type = D3D12_HEAP_TYPE_DEFAULT;
props.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
props.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
props.CreationNodeMask = 0;
props.VisibleNodeMask = 0;
D3D12_RESOURCE_DESC resDesc = {};
resDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
resDesc.Width = windowWidth; //テクスチャ幅
resDesc.Height = windowHeight; //テクスチャ高さ
resDesc.DepthOrArraySize = 1;
resDesc.MipLevels = 1;
resDesc.Format = DXGI_FORMAT_R16G16_FLOAT;
resDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
resDesc.SampleDesc.Count = 1;
resDesc.SampleDesc.Quality = 0;
resDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET |
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
D3D12_CLEAR_VALUE clearValue;
clearValue.Format = DXGI_FORMAT_R16G16_FLOAT;
clearValue.Color[0] = 0.0f;
clearValue.Color[1] = 0.0f;
clearValue.Color[2] = 0.0f;
clearValue.Color[3] = 1.0f;
Microsoft::WRL::ComPtr<ID3D12Resource> _normalResource; //法線
Microsoft::WRL::ComPtr<ID3D12Resource> _diffuseColorResource; //拡散反射光
Microsoft::WRL::ComPtr<ID3D12Resource> _specularPowerResource; //ハイライト強度
Microsoft::WRL::ComPtr<ID3D12Resource> _resultResource; //レンダリング結果
_device->CreateCommittedResource(&props,
D3D12_HEAP_FLAG_NONE,
&resDesc,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
&clearValue,
IID_PPV_ARGS(&_normalResource)); //法線バッファの作成
clearValue.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
resDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
_device->CreateCommittedResource(&props,
D3D12_HEAP_FLAG_NONE,
&resDesc,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
&clearValue,
IID_PPV_ARGS(&_diffuseColorResource)); //拡散反射光バッファの作成
_device->CreateCommittedResource(&props,
D3D12_HEAP_FLAG_NONE,
&resDesc,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
&clearValue,
IID_PPV_ARGS(&_specularPowerResource)); //ハイライト強度バッファの作成
_device->CreateCommittedResource(&props,
D3D12_HEAP_FLAG_NONE,
&resDesc,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
&clearValue,
IID_PPV_ARGS(&_resultResource)); //レンダリング結果バッファの作成
//RTVデスクリプタヒープの作成
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> _rtvHeap;
D3D12_DESCRIPTOR_HEAP_DESC desc;
desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
desc.NumDescriptors = 3; //法線、アルベド、ハイライト強度、ディフューズカラーの3つ
desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; //RTVのデスクリプタをこのヒープに作る
desc.NodeMask = 0;
//ID3D12Device::CreateDescriptorHeapでヒープを作る
_device->CreateDescriptorHeap(desc,IID_PPV_ARGS(&_rtvHeap));
//デスクリプタヒープに対してRTVビューを作っていく
unsigned int _incrementSize = _device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
D3D12_CPU_DESCRIPTOR_HANDLE handle = _rtvHeap->GetCPUDescriptorHandleForHeapStart();
D3D12_RENDER_TARGET_VIEW_DESC rtDesc = {};
rtDesc.Format = DXGI_FORMAT_R16G16_FLOAT;
rtDesc.ViewDimension = D3D12_RTV_DIMENSION::D3D12_RTV_DIMENSION_TEXTURE2D;
rtDesc.Texture2D.MipSlice = 0;
rtDesc.Texture2D.PlaneSlice = 0;
_device->CreateRenderTargetView(_normalResource.Get(), &rtDesc, handle );
rtDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
handle.ptr += (_incrementSize);
_device->CreateRenderTargetView(_diffuseColorResource.Get(), &rtDesc, handle );
handle.ptr += (_incrementSize);
_device->CreateRenderTargetView(_specularPowerResource.Get(), &rtDesc, handle );
//SRVデスクリプタヒープの作成
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> _srvHeap;
D3D12_DESCRIPTOR_HEAP_DESC desc;
desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
desc.NumDescriptors = 5; //法線、アルベド、ハイライト強度、ディフューズカラー、結果のSRV、結果のUAVの5つ
desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; //SRV,UAVのデスクリプタをこのヒープに作る
desc.NodeMask = 0;
//ID3D12Device::CreateDescriptorHeapでヒープを作る
_device->CreateDescriptorHeap(desc,IID_PPV_ARGS(&_srvHeap));
//デスクリプタヒープに対してSRVビュー/UAVビューを作っていく
_incrementSize = _device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
handle = _srvHeap->GetCPUDescriptorHandleForHeapStart();
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Format = DXGI_FORMAT_R16G16_FLOAT;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = 1;
srvDesc.Texture2D.MostDetailedMip = 0;
srvDesc.Texture2D.PlaneSlice = 0;
srvDesc.Texture2D.ResourceMinLODClamp = 0.0F;
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
_device->CreateShaderResourceView(_normalResource.Get(), &srvDesc, handle);
handle.ptr += (_incrementSize);
srvDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
_device->CreateShaderResourceView(_diffuseColorResource.Get(), &srvDesc, handle);
handle.ptr += (_incrementSize);
_device->CreateShaderResourceView(_specularPowerResource.Get(), &srvDesc, handle);
handle.ptr += (_incrementSize);
_device->CreateShaderResourceView(_resultResource.Get(), &srvDesc, handle);
handle.ptr += (_incrementSize);
D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc = {};
uavDesc.Format = DXGI_FORMAT::DXGI_FORMAT_UNKNOWN;
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D;
uavDesc.Texture2D.MipSlice = 0;
uavDesc.Texture2D.PlaneSlice = 0;
_device->CreateUnorderedAccessView(_resultResource.Get(), nullptr, &uavDesc, handle);
クソ長いですね。本当はクラス化したりしてかなり短いコードにできるんですが、あえて生でコード書いてます。ID3D12Deviceのオブジェクトは作っている体でコードを書いてます。上記コードの*_device*がそれにあたります。
#####手順2
モデル描画用のパイプラインステート、およびシェーダーをMRTに対応する形に変更します。
MRTとはマルチレンダーターゲットの略で、一回の出力でたくさんのレンダーターゲットに書き込もうぜっていうやつです。
例えば…
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
このようなモデル描画用のパイプラインステートがあったとして、こいつの出力レンダーターゲット数とフォーマットをジオメトリにあった形で設定しないといけないわけです。
今回は法線バッファ、ハイライト強度、アルベド値の順で出力するので出力レンダーターゲット数は3つ、フォーマットはリソースを作った時のもので出力順にそって設定します。
psoDesc.NumRenderTargets = 3;
psoDesc.RTVFormats[0] = DXGI_FORMAT_R16G16_FLOAT;
psoDesc.RTVFormats[1] = DXGI_FORMAT_R16G16B16A16_FLOAT;
psoDesc.RTVFormats[2] = DXGI_FORMAT_R16G16B16A16_FLOAT;
こんな感じで設定してあげるといいです。
シェーダー側はピクセルシェーダーの出力を構造体でまとめて、それをピクセルシェーダーで返却するようにします。
//こんな感じで出力を構造体でまとめて、ピクセルシェーダーで返却してあげます
struct PSOutput
{
float2 normalMap : SV_Target0; //法線
float4 specularPowerMap : SV_Target1; //ハイライト強度
float4 albedoMap : SV_Target2; //アルベド
};
//ピクセルシェーダー VSOuntputは頂点シェーダーの結果の情報をまとめた構造体。VSOutputおよび頂点シェーダーは省略しています。
PSOutput PsMain(VSOutput input)
{
PSOutput output = (PSOutput) 0;
//アルベドカラー 頂点のUVからサンプリング
float4 albedoColor = StanderdTex.Sample(Sampler, input.texCoord);
float2 viewNorm = mul(camera.View,input.normal).xy; //法線をviewスペースに変換
viewNorm = float2(0.5 * (viewNorm + 1.0f)); //法線を0.0f~1.0fにパッキング
output.normalMap = viewNorm;
output.specularPowerMap = float4(Specular.x, (Specular.w / 255.0f), emissive.x, 1.0f);
output.albedoMap = albedoColor;
//出力をまとめた構造体を結果として返却してあげる
return output;
}
#####手順3
ライティング用のシェーダーを書いていきます。
各ジオメトリはテクスチャで、ライティング結果を保持するバッファはUAVで、ライトはStructuredBufferでシェーダーに渡しています。
忘れがちなんですが、コンピュートシェーダーにリソースを渡す前には*ID3D12GraphicsCommandList::ResourceBarrier()*でリソースバリアを張る必要があります。例えば、ジオメトリバッファは前のステップ(モデルのジオメトリ描画など)でレンダーターゲットとして扱っている場合、各ジオメトリの状態をレンダーターゲットからシェーダーリソースに状態を遷移する必要があります。
#define ComputeRootSignature "RootFlags(0),"\
"CBV(b0),"\
"DescriptorTable(SRV(t0,numDescriptors = 1,space = 0)," \
"visibility = SHADER_VISIBILITY_ALL),"\
"DescriptorTable(SRV(t1,numDescriptors = 1,space = 0)," \
"visibility = SHADER_VISIBILITY_ALL),"\
"DescriptorTable(SRV(t2,numDescriptors = 1,space = 0)," \
"visibility = SHADER_VISIBILITY_ALL),"\
"DescriptorTable(SRV(t3,numDescriptors = 1,space = 0)," \
"visibility = SHADER_VISIBILITY_ALL),"\
"DescriptorTable(SRV(t4,numDescriptors = 1,space = 0)," \
"visibility = SHADER_VISIBILITY_ALL),"\
"DescriptorTable(UAV(u0,numDescriptors = 1,space = 0)," \
"visibility = SHADER_VISIBILITY_ALL),"\
struct SurfaceData
{
float3 posView;
float3 normalView;
float4 albedo;
float specular;
};
struct LightParamater
{
float3 color;
float3 pos;
float3 direction;
float range;
float attenuation;
float intensity;
int type; // 0 = point,1 = Direction
};
cbuffer CameraData : register(b0)
{
matrix View;
matrix Proj;
}
Texture2D<float2> NormalMapTexture : register(t0);
Texture2D<float4> SpecularPowerMapTexture : register(t1);
Texture2D<float4> AlbeboMapTexture : register(t2);
Texture2D<float> DepthTexture : register(t3);
StructuredBuffer<LightParamater> Lights : register(t4);
RWTexture2D<float4> ResultTexture : register(u0);
#define threadX 16
#define threadY 16
#define threadDimension 16
#define ComputeTileSize (threadX*threadY)
#define LightNumMax 1024 //ライトを1024までサポート
groupshared uint sMinZ; //タイルの最小深度
groupshared uint sMaxZ; //タイルの最大深度
groupshared uint sTileLightIndices[LightNumMax]; //タイルが接触しているライトのインデックス
groupshared uint sTileNumLights; //タイルが接触しているライトの数
//タイルのフラスタムを取得する
void GetTileFrustumPlane(out float4 frustumPlanes[6], uint3 groupId , float screenWidth,float screenHeight)
{
// タイルの最大・最小深度を浮動小数点に変換
float minTileZ = asfloat(sMinZ);
float maxTileZ = asfloat(sMaxZ);
float width = screenWidth;
float height = screenHeight;
float2 screenSize = float2(width, height);
float2 tileScale = screenSize * rcp(float(2 * threadDimension));
float2 tileBias = tileScale - groupId.xy;
float4 c1 = float4(Proj._11 * tileScale.x, 0.0, tileBias.x, 0.0);
float4 c2 = float4(0.0, -Proj._22 * tileScale.y, tileBias.y, 0.0);
float4 c4 = float4(0.0, 0.0, 1.0, 0.0);
frustumPlanes[0] = c4 - c1; // 右平面
frustumPlanes[1] = c1; // 左平面
frustumPlanes[2] = c4 - c2; // 上平面
frustumPlanes[3] = c2; // 底平面
//ニア・ファーの平面
frustumPlanes[4] = float4(0.0, 0.0, 1.0, -minTileZ);
frustumPlanes[5] = float4(0.0, 0.0, -1.0, maxTileZ);
// 法線が正規化されていない4面についてだけ正規化する
[unroll(4)]
for (uint i = 0; i < 4; ++i)
{
frustumPlanes[i] *= rcp(length(frustumPlanes[i].xyz));
}
}
//! サーフェイス情報を取得する
SurfaceData GetSurfaceData(uint2 uv)
{
float depth = DepthTexture[uv];
float4 albedo = AlbeboMapTexture[uv];
float4 specPower = SpecularPowerMapTexture[uv];
float2 norm = 2.0f * NormalMapTexture.Load(int3(uv.x, uv.y, 0)) - 1.0f; //法線のデコード
float sqrZ = 0.0f;
//sqrt(1.0-X^2-Y^2)
sqrZ = sqrt(1.0f - (norm.x * norm.x) - (norm.y * norm.y));
float3 normal = float3(norm.x, norm.y, -sqrZ); //Z軸ベクトルの算出
float screenSopaceDepth = 0.0f;
//深さ取得
screenSopaceDepth = Proj._34 / (depth - Proj._33);
float2 gbufferDim = float2(0, 0);
uint dummy = 0;
uint dummy2 = 0;
NormalMapTexture.GetDimensions(dummy2, gbufferDim.x, gbufferDim.y, dummy);
// ビュー空間での座標を求める
float2 screenPixelOffset = (float2(2.0, -2.0)) / gbufferDim;
float2 positionScreen = (float2(uv) + float2(0.5f, 0.5f)) * screenPixelOffset.xy + float2(-1.0f, 1.0f);
float2 viewRay = positionScreen.xy / float2(Proj._11, Proj._22);
float3 pos = (float3) 0;
pos.z = screenSopaceDepth;
pos.xy = viewRay.xy * screenSopaceDepth;
SurfaceData ret =
{
pos,
normal,
albedo.xyzw,
specPower.y
};
return ret;
}
float4 PointLightBRDF(float3 pos, float3 normal, float3 lightPos, float3 lightColor, float range, float lightAttenuation, float specularPower, float f0)
{
float3 viewDir = normalize(-pos);
//減衰計算
float3 lightDir = lightPos - pos;
float lightLength = length(lightDir);
float lightRatio = lightLength / (range);
float attenuation = max(1.0 - (lightRatio * lightRatio), 0.0);
if (attenuation == 0.0)
{
float4(0.0f, 0.0f, 0.0f, 0.0f);
}
lightDir = normalize(lightDir);
//ライト計算
float nl = saturate(dot(normal, lightDir));
float3 halfVec = normalize(viewDir + lightDir);
float nh = saturate(dot(normal, halfVec));
float power = lightAttenuation;
float spec = 0.0f;
if (power > 0.0)
{
float nv = dot(normal, viewDir);
float sn = pow(2.0, 13.0 * specularPower) * power;
float D = (sn + 2.0) / (2.0 * 3.1415926f) * pow(nh, sn);
float F = f0 + (1.0 - f0) * pow((1.0 - nv), 5.0);
float dv = dot(viewDir, (viewDir + lightDir));
float V = 4.0 / (dv * dv);
float nlnv = nl * nv;
float G = V * nlnv;
spec = max(F * G * D / (4.0 * nlnv), 0.0);
}
return float4(lightColor.rgb * nl * attenuation, spec * attenuation * attenuation);
}
float4 CalcDirectionalLightBRDF(float3 objPos, float3 objNormal, float3 litDir, float3 litColor, float litPower, float gloss, float f0)
{
float3 eyeVec = normalize(objPos);
litDir *= -1;
// ライト計算
float nl = dot(objNormal, litDir);
float3 halfVec = normalize(eyeVec + litDir);
float nh = dot(objNormal, halfVec);
float power = litPower;
float spec = 0.0f;
if (power > 0.0f)
{
float nv = dot(objNormal, eyeVec);
float sn = pow(2.0, 13.0 * gloss) * power;
float D = (sn + 2.0) * pow(nh, sn) / (2.0 * 3.1415926);
float F = f0 + (1.0 - f0) * pow((1.0 - nv), 5.0);
float dv = dot(eyeVec, (eyeVec + litDir));
float V = 4.0 / (dv * dv);
float nl_nv = nl * nv;
float G = V * nl_nv;
spec = F * G * D / (4.0 * nl_nv);
}
// 出力
return saturate(float4(litColor.rgb * nl, spec));
}
[RootSignature(ComputeRootSignature)]
[numthreads(threadX, threadY, 1)]
void CSMain(uint3 groupeID : SV_GroupID, uint3 dispatchThreadID : SV_DispatchThreadID, uint3 groupThreadID : SV_GroupThreadID)
{
uint groupIndex = groupThreadID.y * threadDimension + groupThreadID.x;
uint2 globalIndex = dispatchThreadID.xy;
//共有メモリの初期化
if (groupIndex == 0)
{
sTileNumLights = 0;
sMinZ = 0x7F7FFFFF; // floatの最大値
sMaxZ = 0;
}
//最小距離(プロジェクション行列から取得)
float minZSample = -Proj._34 / (Proj._33 - 1.0f);
//最大距離(プロジェクション行列から取得)
float maxZSample = -Proj._34 / Proj._33;
//サーフェス情報を取得
SurfaceData surfaceData = GetSurfaceData(globalIndex);
bool validPixel =
surfaceData.posView.z >= -Proj._34 / Proj._33 &&
surfaceData.posView.z <= -Proj._34 / (Proj._33 - 1.0f);
[flatten]
if (validPixel)
{
minZSample = min(minZSample, surfaceData.posView.z);
maxZSample = max(maxZSample, surfaceData.posView.z);
}
//ビュースペースでの深度
//初期化同期
GroupMemoryBarrierWithGroupSync();
//同期処理
if (maxZSample >= minZSample)
{
InterlockedMin(sMinZ, asuint(minZSample));
InterlockedMax(sMaxZ, asuint(maxZSample));
}
//同期処理
GroupMemoryBarrierWithGroupSync();
float2 gbufferDim = float2(0, 0);
uint dummy = 0;
uint dummy2 = 0;
NormalMapTexture.GetDimensions(dummy2, gbufferDim.x, gbufferDim.y, dummy);
float4 frustumPlanes[6];
GetTileFrustumPlane(frustumPlanes, groupeID, gbufferDim.x, gbufferDim.y);
//ライトの総数およびストライド数(ストライドはダミー)
//GetDimensionでライトの数とライトのストライド値を取得。
uint strid = 0;
uint lightNum = 0;
Lights.GetDimensions(lightNum, strid);
//ライトカリング
{
uint threadCount = ComputeTileSize;
uint passCount = (lightNum + threadCount - 1) / threadCount;
for (uint passIt = 0; passIt < passCount; ++passIt)
{
uint lightIndex = passIt * threadCount + groupIndex;
lightIndex = min(lightIndex, lightNum);
//ライトの取得
LightParamater light = Lights[lightIndex];
//ライトをカメラ原点のビュー座標にもっていく
light.pos = mul(View, float4(light.pos.xyz, 1.0)).xyz;
//フラスタムとの当たり判定の結果
bool inFrustum = true;
//実行を停止するまでFor文を展開
[unroll]
for (uint i = 0; i < 6; ++i)
{
float4 lp = float4(light.pos.xyz, 1.0);
float d = dot(frustumPlanes[i], lp);
//もし平行光源ライト(サンライト)ならば
if (light.type == 1)
{
//無条件でOK
inFrustum = true;
break;
}
inFrustum = inFrustum && (d >= -light.range);
}
if (inFrustum)
{
uint offset = 0;
InterlockedAdd(sTileNumLights, 1, offset);
sTileLightIndices[offset] = lightIndex;
}
}
}
//ここでスレッド間で同期をとるので、タイルと衝突しているライトの検出が可能になっているはず
GroupMemoryBarrierWithGroupSync();
//ライトインデックスを出力バッファに出力
//ライティング
float3 diffuse = (float3) 0.0;
float specular = 0.0f;
float4 result;
for (uint i = 0; i < sTileNumLights; ++i)
{
uint lightIndex = sTileLightIndices[i];
LightParamater light = Lights[lightIndex];
float attenuation = light.attenuation;
if (surfaceData.specular <= 0.0f)
{
light.attenuation = 0.0f;
}
//ポジションをカメラ空間にもっていく
float3 lightViewPos = mul(View, float4(light.pos.xyz, 1.0f)).xyz;
//ベクトルをview空間にもっていく
float3 lightViewDirection = mul(View, float4(light.direction.xyz, 0.0f)).xyz;
//タイプで分岐 かなりよろしくない
if (light.type == 0)
{
result = PointLightBRDF(surfaceData.posView, surfaceData.normalView, lightViewPos, light.color.xyz, light.range, light.attenuation, surfaceData.specular, 0.5f);
}
else
{
result = CalcDirectionalLightBRDF(surfaceData.posView, surfaceData.normalView, lightViewDirection, light.color, light.attenuation, surfaceData.specular, 0.5f);
}
diffuse = diffuse + result.rgb;
specular = specular + result.a;
}
//適当アンビエント
float4 ambient = surfaceData.albedo * 0.1f;
//もしアルベドのアルファ値が1.0f未満なら
if (surfaceData.albedo.w < 1.0f)
{
surfaceData.albedo = float4(0.0f, 0.0f, 0.0f, 0.0f);
specular = 0.0f;
ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
}
//結果をUAV化したテクスチャに書き込んでいく。
ResultTexture[globalIndex] = float4(saturate(diffuse * surfaceData.albedo.rgb + ambient.rgb + specular), surfaceData.albedo.w);
}
完全に自分のコードのコピペです。書き直すのが面倒だったとか言えない
このシェーダーはライトごと、ピクセルごとに並行で処理しています。
流れを簡潔に説明するとこんな感じになります。
- タイル内のピクセルからタイルの最小深度と最大深度を求める
- シェーダーに渡されたデータからピクセルのデータを取得
- タイルのフラスタムをつくる。
- タイルフラスタムでライトとの当たり判定をおこなう。(ライトカリング)
- タイルと当たっているライト情報からそのピクセルのライティングをおこなう。
コンピュートシェーダーはID3D12GraphicsCommandList::Dispatch()で実行することができます。
//commandListはMicrosoft::WRL::ComPtr<ID3D12GraphicsCommandList>型の変数
//TileSizeはスクリーンの分割数
//ScreenSizeX,ScreenSizeYはそれぞれ画面幅、画面高さ
commandList->Dispatch((ScreenSizeX + TileSize - 1) / TileSize, (ScreenSizeY + TileSize - 1) / TileSize, 1);
###追記
なぜシェーダー内でRootSignatureを定義しているかというと、CPP側でコンピュートシェーダ用のRootSignatureを作るとなぜか作成に失敗したからです。原因はよくわかんなかったです。なので、HLSL内でRootSignatureを定義して、シェーダーバイナリからRootSignatureをフェッチするように変更したらうまくいったのでそうしてます。
D3D12でHLSL内にRootSignatureを定義してフェッチしてみる
以上が手順の詳細になります。無い知恵を絞って実装したものなので、無駄な部分や間違っている部分が多々あると思います。特にサンプリング周りやライトカリング周りとか…
#ライティング結果
床面はスペキュラを切ってライティングしてます。
表示モデルは「まんぞく茜ちゃん」です。
「まんぞく琴葉姉妹」 / 金子卵黄 さんの作品 - ニコニ立体
モデルの色がおかしいのはちゃんと半透明オブジェクトや半透明テクスチャの処理を行っていなかったり、ガンマ補正をかけてなかったりしてるからです。
#おわりに
本当は普通のフォワードレンダリング、ディファードレンダリングとの速度比較などを実験してみたかったのですがD3D12で文字を動的に生成する方法がわからなかったのでやってません。あとアルゴリズムの説明もこの記事ではまったく触れていないので別の記事に書きます。たぶん。