Linked Listを利用したOIT - 4

 カテゴリー: OIT(Japanese) — Kaori @ 5 月 8th, 2010

[前回のつづき]

今回は2パス目、フラグメントのソートと描画パスの実装についての解説です。


3. ソートと描画パスの実装

このパスではピクセルごとにLinked Listをたどって全てのフラグメントデータを取り出し、深度値でソートして奥のフラグメントから順にブレンディング、結果をフレームバッファに出力します。

3-a. シェーダの実装

新しいhlslファイルを作成します。今回はピクセルシェーダだけでなくフルスクリーンを描画するための頂点シェーダも必要です。
ピクセルシェーダでは全てのフラグメントデータをテンポラリ配列に一度コピーし、それから深度値でソートします。これはメモリアクセスを抑えるためです。


StructuredBuffer<SFragmentLink> FragmentLinkSRV : register(t0);
Buffer StartOffsetSRV : register(t1);

struct QuadVS_Output
{
    float4 pos : SV_POSITION;
};


float4 SortFragmentsPS( QuadVS_Output _input ) : SV_Target0
{
    uint uIndex = (uint)_input.pos.y * g_nFrameWidth + (uint)_input.pos.x;

    // Read and store linked list data to the tempolary buffer.
    SFragment aData[32];
    int anIndex[32];
    uint uNumFragment = 0;    
    uint uNext = StartOffsetSRV[uIndex];
    
    while ( uNext != 0xFFFFFFFF ) {
        SFragmentLink element = FragmentLinkSRV[uNext];

        aData[uNumFragment] = element.fragment;
        anIndex[uNumFragment] = uNumFragment;
        ++uNumFragment;
        uNext = element.uNext;
    }


    uint N2 = 1 << (int)(ceil(log2(uNumFragment)));
    // fill initial data
    for(int i = uNumFragment; i < N2; i++)
    {
        anIndex[i] = i;
        aData[i].fDepth = 1.1f;
    }

    // Bitonic sort. copied from OIT_CS.hlsl
    for( int k = 2; k >1; j > 0 ; j = j>>1 )
        {
            for( int i = 0; i i )
                {
                    float dixj = aData[ anIndex[ ixj ] ].fDepth;
                    if ( ( i&k ) == 0 && di > dixj )
                    {
                        int temp = anIndex[ i ];
                        anIndex[ i ] = anIndex[ ixj ];
                        anIndex[ ixj ] = temp;
                    }
                    if ( ( i&k ) != 0 && di < dixj )
                    {
                        int temp = anIndex[ i ];
                        anIndex[ i ] = anIndex[ ixj ];
                        anIndex[ ixj ] = temp;
                    }
                }
            }
        }
    }

    // Output the final result to the frame buffer
    // Accumulate fragments into final result
    float4 result = 0.0f;
    for( int x = uNumFragment-1; x >= 0; x-- )
    {
        uint uColor = aData[ anIndex[ x ] ].uColor;
        float4 color;
        color.r = ( (uColor >> 0) & 0xFF ) / 255.0f;
        color.g = ( (uColor >> 8) & 0xFF ) / 255.0f;
        color.b = ( (uColor >> 16) & 0xFF ) / 255.0f;
        color.a = ( (uColor >> 24) & 0xFF ) / 255.0f;
        result = lerp( result, color, color.a );
    }
    result.a = 1.0f;

    return result;
}


Start Offset バッファはここではふつうにuintバッファとして参照されます。OIT11サンプルでは不透明プリミティブが存在しません。なので、ブレンディングの初期値はクリアカラー=0となっています。実際には不透明プリミティブを描画したテクスチャを初期値とするべきです。また、テンポラリ配列を使用しているため、1つのピクセルが対処できるフラグメントの数に制限がかかります。つまりこの例だと32個しかフラグメントを持つことはできません。このプログラムはかなり適当なのですが、本当はフラグメントデータのコピー時にセーフティーを入れる必要があります。でないとオーバーランする可能性があります。


3-b. Shader Resource Viewの追加

このパスでは2つのバッファはShader Resource Viewとして参照されます。OIT::OnD3D11ResizedSwapChain関数に以下のコードを追加して、SRVを作っておきます。

// Create Shader Resource Views
D3D11_SHADER_RESOURCE_VIEW_DESC descSRV;
descSRV.ViewDimension = D3D11_SRV_DIMENSION_BUFFER;
descSRV.Buffer.FirstElement = 0;

descSRV.Format = DXGI_FORMAT_UNKNOWN;
descSRV.Buffer.NumElements = pBackBufferSurfaceDesc->Width * pBackBufferSurfaceDesc->Height * 8;
V_RETURN( pDevice->CreateShaderResourceView( m_pFragmentLinkBuffer, &descSRV, &m_pFragmentLinkSRV ) );

descSRV.Format = DXGI_FORMAT_R32_UINT;
descSRV.Buffer.NumElements = pBackBufferSurfaceDesc->Width * pBackBufferSurfaceDesc->Height;
V_RETURN( pDevice->CreateShaderResourceView( m_pStartOffsetBuffer, &descSRV, &m_pStartOffsetSRV ) );


ついでに、フルスクリーン描画のための頂点バッファと入力レイアウトをOIT::OnD3D11CreateDevice関数内で作っておきましょう。


3-c.ソートと描画関数の実装

OIT::SortAndRender関数に作成した頂点バッファと入力レイアウト、Shader Resource Viewをセットして描画するコードを実装します。



ID3D11ShaderResourceView* ppSRVs[] = {
    m_pFragmentLinkSRV,
    m_pStartOffsetSRV,
};
pD3DContext->PSSetShaderResources( 0, sizeof(ppSRVs)/sizeof(ppSRVs[0]), ppSRVs );

pD3DContext->VSSetShader( m_pSortAndRenderVS, NULL, 0 );
pD3DContext->PSSetShader( m_pSortAndRenderPS, NULL, 0 );


// Draw a screen quad by a large triangle.
pD3DContext->IASetInputLayout( m_pIL );
UINT uStrides = sizeof( SQuadVertex );
UINT uOffsets = 0;
pD3DContext->IASetVertexBuffers( 0, 1, &m_pVB, &uStrides, &uOffsets );
pD3DContext->IASetIndexBuffer( NULL, DXGI_FORMAT_R32_UINT, 0 );
pD3DContext->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP );

pD3DContext->Draw( 3, 0 );

// Unbind SRVs
ID3D11ShaderResourceView* ppSRVNULL[] = {
    NULL,
    NULL,
};
pD3DContext->PSSetShaderResources( 0, sizeof(ppSRVs)/sizeof(ppSRVs[0]), ppSRVNULL );

あともちろんこのパスではZテストを無効にしましょう。


4. まとめ
Linked List OITは高速でしかも実装が簡単でした。ただし、今回の実装はOIT11との速度比較が目的だったこともあり、あまり実用的とはいえません。たとえば不透明プリミティブを無視している、ブレンディングモードが1種類に限定されている、アンチエイリアシングはサポートしていないなどです。今後時間があればもうすこし実用的な実装も試してみたいです。


 Linked Listを利用したOIT - 3

 カテゴリー: OIT(Japanese) — Kaori @ 5 月 7th, 2010

[前回のつづき]


今回はLinked List OITの1パスめ、Linked Listの作成パスの実装について解説します。


2.Linked Listの作成パスの実装

OIT11サンプルでは3つのバッファと1つの2Dテクスチャを利用していますが、Linked List OITでは2つのバッファを使用します。

・Fragment Link バッファ
全てのフラグメントデータを格納するためのStructured バッファ。
各フラグメントデータはカラー、深度値に加えて次のフラグメントデータのインデックスを持っています。次のフラグメントデータがない場合、リストの終わりを示す特定の値を格納することになります。今回の実装では0xffffffffを使います。
ピクセルシェーダにおけるフラグメントデータの宣言は以下のようになります。カラーはuintにパックしています。


struct SFragment {
    uint uColor;
    float fDepth;
};

struct SFragmentLink {
    SFragment fragment;
    uint uNext;
};


・Start Offset バッファ
各ピクセルにおけるリストの先頭を指すuintバッファ。毎フレーム描画の前に0xffffffffで初期化されます。


Linked Listの作成パスでは、追加するフラグメントデータをリストの先頭として挿入していきます。つまり、追加されるフラグメントデータが指す次のインデックスにはStart Offsetバッファに含まれていた値を入れ、Start Offset バッファには追加されたフラグメントデータのインデックスを入れます。この部分はスライドのアニメーション"Linked List Creation(2a)~(2d)"を見ると非常にわかりやすいと思います。 


2-a. Linked List作成シェーダの実装

Constant Bufferがそのまま使えるので、Linked Listの作成パスはOIT_PS.hlslを改造して実装することにします。Fragment Link バッファとStart Offsetは以下のように宣言します。両方とも書き込みが必要なのでRW…がつきます。

// Fragment And Link Buffer
RWStructuredBuffer FLBuffer<SFragmentLink> : register( u0 );
// Start Offset Buffer
RWByteAddressBuffer StartOffsetBuffer : register( u1 );

Fragment Linkバッファは構造体のバッファなので、RWStructuredBufferとなります。
Start OffsetバッファはRWByteAddressBufferとして宣言します。描画対象ピクセルの現在の値(リストの先頭のインデックス)を読み、追加するフラグメントデータのインデックスを書き込む(更新する)際にRWByteAddressBuffer::InterlockedExchangeを利用するためです。

つづいてエントリポイント関数の実装です。

[earlydepthstencil]
void StoreFragments( SceneVS_Output input )
{
    uint x = input.pos.x;
    uint y = input.pos.y;

    // Create fragment data.
    uint4 ucolor = saturate( input.color ) * 255;
    SFragmentLink element;
    element.fragment.uColor = (ucolor.x) | (ucolor.y << 8) | (ucolor.z << 16) | (ucolor.a << 24);
    element.fragment.fDepth = input.pos.z;

    // Increment and get current pixel count.
    uint uPixelCount = FLBuffer.IncrementCounter();

    // Read and update Start Offset Buffer.
    uint uIndex = y * g_nFrameWidth + x;
    uint uStartOffsetAddress = 4 * uIndex;
    uint uOldStartOffset;
    StartOffsetBuffer.InterlockedExchange(
        uStartOffsetAddress, uPixelCount, uOldStartOffset );

    // Store fragment link.
    element.uNext = uOldStartOffset;
    FLBuffer[uPixelCount] = element;
}

Fragment Linkバッファの書き込み先のインデックスは、カウンターサポートを利用して計算しています。
また、Start Offset バッファはByteAddressBufferなので、インデックスはバイトオフセットで指定する点に注意が必要です。


2-b. バッファの作成

シェーダができたので、このシェーダに必要なバッファを新たに作成します。バッファ本体とバッファのViewを作成します。RW…バッファはUnordered Access Viewとなります。

以下のようなコードをOIT::OnD3D11ResizedSwapChainに実装して各バッファとUAVを作成します。

// Create Fragment and Link buffer.
descBuf.StructureByteStride = sizeof(float) + sizeof(BYTE) * 4 * 2;
descBuf.ByteWidth = pBackBufferSurfaceDesc->Width * pBackBufferSurfaceDesc->Height * 8 * descBuf.StructureByteStride;
descBuf.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;
V_RETURN( pDevice->CreateBuffer( &descBuf, NULL, &m_pFragmentLinkBuffer ));

// Create Start Offset buffer
descBuf.StructureByteStride = 4 * sizeof(BYTE);
descBuf.ByteWidth = pBackBufferSurfaceDesc->Width * pBackBufferSurfaceDesc->Height * descBuf.StructureByteStride;
descBuf.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS;
V_RETURN( pDevice->CreateBuffer( &descBuf, NULL, &m_pStartOffsetBuffer ));

// Create Unordered Access Views
D3D11_UNORDERED_ACCESS_VIEW_DESC descUAV;
descUAV.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
descUAV.Buffer.FirstElement = 0;

descUAV.Format = DXGI_FORMAT_UNKNOWN;
descUAV.Buffer.NumElements = pBackBufferSurfaceDesc->Width * pBackBufferSurfaceDesc->Height * 8;
descUAV.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_COUNTER;
V_RETURN( pDevice->CreateUnorderedAccessView( m_pFragmentLinkBuffer, &descUAV, &m_pFragmentLinkUAV ) );

descUAV.Format = DXGI_FORMAT_R32_TYPELESS;
descUAV.Buffer.NumElements = pBackBufferSurfaceDesc->Width * pBackBufferSurfaceDesc->Height;
descUAV.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_RAW;
V_RETURN( pDevice->CreateUnorderedAccessView( m_pStartOffsetBuffer, &descUAV, &m_pStartOffsetUAV ) );

Fragment Linkバッファは全てのフラグメントデータを含まなければならないので、スクリーンサイズよりも大きなサイズが必要です。ここではもともとのOIT11サンプルと同じ、スクリーンサイズの8倍のフラグメントデータを格納できるようにしています。また、Fragment LinkバッファはStructured Bufferなので、作成時にD3D11_RESOURCE_MISC_BUFFER_STRUCTUREDを指定します。

Start Offsetバッファはスクリーンサイズと同じです。また、ByteAddressBufferとして使用するため、D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWSフラグが必要です。

Fragment LinkバッファのUAVはカウンターサポートが必要なので、D3D11_BUFFER_UAV_FLAG_COUNTERを指定します。さらに、StructuredBufferのUAVフォーマットはDXGI_FORMAT_UNKNOWNでなければなりません。

同様に、Start OffsetバッファのUAVはByteAddressBufferとなるのでD3D11_BUFFER_UAV_FLAG_RAWDXGI_FORMAT_R32_TYPELESSを指定します。


2-c. Linked List作成関数の実装

Linked List作成関数はOIT::FillDeepBufferを改造して実装します。Constant Bufferをそのまま利用するためです。
Start Offsetバッファを0xffffffffで初期化し、2つのUnordered Access Viewをシェーダに記述した順にセットします。
カラーレンダーターゲットをセットする必要はありません。

// Clear the start offset buffer by magic value.
// Start Offsetバッファを初期化
static const UINT clearValueUINT[1] = { 0xffffffff };
pD3DContext->ClearUnorderedAccessViewUint( m_pStartOffsetUAV, clearValueUINT );

// Bind UAVs.
// No render target is required.
// UAVのバインド。レンダーターゲットは不要
ID3D11UnorderedAccessView* pUAVs[] = {
m_pFragmentLinkUAV,
m_pStartOffsetUAV,
};
// Initialize the counter with 0.
// カウンターを0で初期化.
UINT anInitIndices[] = { 0, 0 };
pD3DContext->OMSetRenderTargetsAndUnorderedAccessViews( 0, NULL, pDSV, 0, sizeof(pUAVs)/sizeof(pUAVs[0]), pUAVs, anInitIndices );

// Set Pixel Shader and shader constants.
pD3DContext->PSSetShader( m_pCreateFragmentLinkPS, NULL, 0 );

HRESULT hr;
D3D11_MAPPED_SUBRESOURCE MappedResource;
V( pD3DContext->Map( m_pPS_CB, 0, D3D11_MAP_WRITE_DISCARD, 0, &MappedResource ) );
PS_CB* pPS_CB = ( PS_CB* )MappedResource.pData;
pPS_CB->nFrameWidth = m_nFrameWidth;
pPS_CB->nFrameHeight = m_nFrameHeight;
pD3DContext->Unmap( m_pPS_CB, 0 );
pD3DContext->PSSetConstantBuffers( 0, 1, &m_pPS_CB );

pScene->D3D11Render( mWorldViewProjection, pD3DContext );

// Unbind UAVs.
ID3D11UnorderedAccessView* pUAVsNULL[] = { NULL, NULL, NULL, NULL };
pD3DContext->OMSetRenderTargetsAndUnorderedAccessViews( 0, NULL, pDSV, 0, sizeof(pUAVs)/sizeof(pUAVs[0]), pUAVsNULL, NULL );

Unordered Access Viewのセットの際に、カウンターを0で初期化するのを忘れないでください。また、シェーダコードで[earlydepthstencil]を指定した場合、描画の前にdepth writeをOFFにする必要があります。これをしないと一部のフラグメントデータがZテストではじかれてしまい、正しく描画できません。



これでLinked Listの作成ができました。次回は2パス目、Linked Listを読んでソートとブレンディングを行うパスについて解説します。


[つづく]


 Linked Listを利用したOIT - 2

 カテゴリー: OIT(Japanese) — Kaori @ 5 月 6th, 2010

[前回の続き]

今回からはOIT11をどのように改造してLinked List OITを実装するのかを説明していきます。
説明が足りないところは前述のスライドやOIT11サンプルを参照していただけるとわかると思います。特にスライドはアニメーションがあったりするので英語が読めなくてもなんとなくわかります。また、スライドにはシェーダコードも記載されています。実装は非常に簡単なのでスライドが読めればできたも同然です。


1. 描画フローの変更

通常の半透明プリミティブの描画フローでは、
半透明プリミティブの描画時にZテストをパスしたらアルファブレンドしてフレームバッファを更新
という流れですが、
OITではプリミティブの描画時にはZテストは行わず、フレームバッファも更新しません。その代わり、全ての半透明のフラグメントデータを別の大きなバッファに格納します。フラグメントデータは、最低でも深度値とカラーを持ち、スクリーン上のどこかのピクセルに属しています。半透明ポリゴンが重なるところでは、1つのピクセルに複数のフラグメントデータが存在する状態になります。
半透明の描画終了後、各ピクセルごとにフラグメントデータを深度値でソートしてブレンディングを行い、結果のカラーをフレームバッファに描きこみます。

この「別の大きなバッファに格納する」方法がDirectXのOIT11サンプルとLinked List版とで異なります。

OIT11サンプルではフラグメントデータを各ピクセルごとに順番につめていきます。
つまり、スクリーンの左上から
0番目のピクセルに描画されるフラグメントデータ1,2,3…
1番目のピクセルに描画されるフラグメントデータ1,2,…
というようにピクセル順に複数のフラグメントデータをバッファに格納します。

このため、フラグメントデータを保存する前に、あらかじめ各ピクセルに描画されるフラグメントデータの数や、各ピクセルの格納先インデックスがわかっていなければなりません。
OIT::Render関数を見てみると全体の流れがわかります。

// Create a count of the number of fragments at each pixel location
// 1.各ピクセルに描画されるフラグメントデータの数を数える
CreateFragmentCount( pD3DContext, pScene, mWorldViewProjection, pRTV, pDSV );

// Create a prefix sum of the fragment counts. Each pixel location will hold
// a count of the total number of fragments of every preceding pixel location.
// 2.各ピクセルにおけるはじめのインデックスを計算
CreatePrefixSum( pD3DContext );

// Fill in the deep frame buffer with depth and color values. Use the prefix
// sum to determine where in the deep buffer to place the current fragment.
// 3.フラグメントデータを保存
FillDeepBuffer( pD3DContext, pRTV, pDSV, pScene, mWorldViewProjection );

// Sort and render the fragments. Use the prefix sum to determine where the
// fragments for each pixel reside.
// 4.保存してあるフラグメントデータをピクセルごとにソートして描画
SortAndRenderFragments( pD3DContext, pDevice, pRTV );

1と3はプリミティブを描画するため、ピクセルシェーダで実装されています。その他のパスはCompute Shaderで実装されています。

Linked Listを利用したOITでは各フラグメントデータを描画された順にバッファにつめこんでいきます。なので、上記の1、2の処理は必要ありません。その代わり、フラグメントデータ自体に、そのピクセルにおける次のフラグメントデータのインデックスを含めておき、1つのピクセルに属するフラグメントデータが1つのリストとしてつながるように格納します。
ということで、OIT::Render関数は以下のように書き換えられます。

// Fragment and Link creation.
// 1.Linked Listの作成
CreateFragmentLink( pD3DContext, pRTV, pDSV, pScene, mWorldViewProjection );

// Sort and render the fragments.
// 2.保存してあるフラグメントデータをピクセルごとにソートして描画
SortAndRenderFragments( pD3DContext, pDevice, pRTV );

パス2はスクリーンの各ピクセルごとにシェーダを走らせるのでピクセルシェーダでの実装となります。つまり、Linked List OITではCompute Shaderを使用しません。





[つづく]


 Linked Listを利用したOIT

 カテゴリー: OIT(Japanese) — Kaori @ 5 月 4th, 2010

GDC 2010にてLinked List を利用したOrder Independent Transparency の紹介がありました。

スライドはこちら。
GDC 2010: OIT and GI using DX11 linked lists (Nick Thibieroz & Holger Grün)
http://developer.amd.com/documentation/presentations/Pages/default.aspx

Order Independent Transparency (以後OIT)というのは、半透明の描画順ソートを行う技術です。通常のZテストとアルファブレンディングで描画順のソートを完全に正しく行うのには限界があり、実質不可能です。たとえばポリゴンが他のポリゴンに突き抜けている場合などはかなり無理です。しかし、OITを利用すればそのような状態でも正しく半透明ポリゴンを重ねて描画することができます。

DirectX SDKサンプルにもOIT11というサンプルがあり、ポリゴンが突き抜けているような状態でも正しくブレンドすることができています。しかし残念ながら非常に重くて使い物になりません。一方、同じOITを行うATI のRadeon™ HD 5000 シリーズの「Mecha」デモはOIT11に比べてはるかに複雑なシーンなのにわりとさくさく動いています。GDCで紹介されたOITの技術はMechaデモで使用しているものと同じ、とういことで、OIT11サンプルを改造してLinked Listを実装し、速度を比較してみることにしました。


実装結果

オリジナルのOIT11サンプルはRADEON HD5800上で11.3fpsで動いています。


Linked List OITを実装後、fpsは1800まであがりました。
実に約160倍です。DirectXのサンプルはあまり参考にしないほうがいいですね…


実装方法はおいおい投稿していきます。

[つづく]

ホットワード 利用 カテゴリー 前回 フラグメント 実装
割引クーポンまとめ情報 - クー割