2011年6月23日木曜日

SSAO僕にも出来たよ!

シェーダプログラムをやっておかないとなと思って、さしあたってSSAOにロマンを感じていたので挑戦してみました。
シェーダの前提が全然わからず、かなり辛かったのですがいろいろ協力いただいてひとまず表示までこぎつけました。

ランバートライティングのみで光源がちょうど反対側でしょうか。
のっぺりしているところを撮りました。
同じ角度でSSAOを利かせますと、陰影がディティール感出ますね。
実装方法です。初心者向けです。
まずフレームワークというかウインドウの初期化やDirectXの初期化は適当に済ませてください。
まるぺけさんのhttp://marupeke296.com/TIPS_UltraShortDirectXProg.htmlとかいいと思います。

プログラムの方は以下の雰囲気です。
ID3DXEffect* pEffect;
D3DXMATRIX View, Proj;
LPDIRECT3DSURFACE9       backbuffer;
LPDIRECT3DTEXTURE9       texColor;
LPDIRECT3DSURFACE9       surColor;
LPDIRECT3DTEXTURE9       texNormalDepth;
LPDIRECT3DSURFACE9       surNormalDepth;
LPDIRECT3DTEXTURE9       rayMap;

// エフェクトの読み込み
LPD3DXBUFFER pErr = NULL;
HRESULT hr = D3DXCreateEffectFromFile(
 d3ddev,
 _T("SSAO.fx"),
 NULL,
 NULL,
 D3DXSHADER_DEBUG,
 NULL,
 &pEffect,
 &pErr);
if( FAILED( hr ) )
{
 if( pErr != NULL )
 { // コンパイルエラー出力
  const char *err = (char*)pErr->GetBufferPointer();
  pErr->Release();
 }
}
// バックバッファを覚えとく
d3ddev->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &backbuffer);
// SSAOで使う。レイマップ/法線深度マップ/通常のカラーマップを作成します
// GetScreenWidth/GetScreenHeightはスクリーンサイズを取ってくるのを作ってください
d3ddev->CreateTexture(GetScreenWidth(), GetScreenHeight(), 1, D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8,D3DPOOL_DEFAULT, &texColor, 0);
texColor->GetSurfaceLevel(0, &surColor);
d3ddev->CreateTexture(GetScreenWidth(), GetScreenHeight(), 1, D3DUSAGE_RENDERTARGET, D3DFMT_A32B32G32R32F,D3DPOOL_DEFAULT, &texNormalDepth, 0);
texNormalDepth->GetSurfaceLevel(0, &surNormalDepth);

// 16x16のレイマップを作成しますmakeRayMapという
// コールバック関数を自分で用意してテクスチャに書き込んじゃいます
d3ddev->CreateTexture(16, 16, 1, 0, D3DFMT_A32B32G32R32F, D3DPOOL_MANAGED, &rayMap, 0);
D3DXFillTexture(rayMap, makeRayMap, 0);

// ビュー変換・射影変換
D3DXMatrixPerspectiveFovLH( &Proj, D3DXToRadian(45), static_cast(GetScreenWidth())/static_cast(GetScreenHeight()), 1.0f, 10000.0f);
で初期化をしておきます。
makeRayMap関数は以下のとおりです。
コメントアウトしている方が本来のレイの作成方法なのですが、拾ってきたベクトル群の方が見た目いい感じだったので、そっちにしました。
static void WINAPI makeRayMap(D3DXVECTOR4* pOut, const D3DXVECTOR2* pTexCoord, const D3DXVECTOR2* pTexelSize, void* data)
{
/*
 float r = 1.0f * (float)rand() / (float)RAND_MAX;
 float t = 6.2831853f * (float)rand() / ((float)RAND_MAX + 1.0f);
 float cp = 2.0f * (float)rand() / (float)RAND_MAX - 1.0f;
 float sp = sqrt(1.0f - cp * cp);
 float ct = cos(t), st = sin(t);

 pOut->x = abs(r * sp * ct);
 pOut->y = abs(r * sp * st);
 pOut->z = abs(r * cp);
 pOut->w = 0;
*/
 static int i=0;

 D3DXVECTOR3 vec;
 switch(i%16)
 {
 case 0: vec = D3DXVECTOR3(0.53812504f, 0.18565957f, -0.43192f); break;
 case 1: vec = D3DXVECTOR3(0.13790712f, 0.24864247f, 0.44301823f); break;
 case 2: vec = D3DXVECTOR3(0.33715037f, 0.56794053f, -0.005789503f); break;
 case 3: vec = D3DXVECTOR3(-0.6999805f, -0.04511441f, -0.0019965635f); break;
 case 4: vec = D3DXVECTOR3(0.06896307f, -0.15983082f, -0.85477847f); break;
 case 5: vec = D3DXVECTOR3(0.056099437f, 0.006954967f, -0.1843352f); break;
 case 6: vec = D3DXVECTOR3(-0.014653638f, 0.14027752f, 0.0762037f); break;
 case 7: vec = D3DXVECTOR3(0.010019933f, -0.1924225f, -0.034443386f); break;
 case 8: vec = D3DXVECTOR3(-0.35775623f, -0.5301969f, -0.43581226f); break;
 case 9: vec = D3DXVECTOR3(-0.3169221f, 0.106360726f, 0.015860917f); break;
 case 10:vec = D3DXVECTOR3(0.010350345f, -0.58698344f, 0.0046293875f); break;
 case 11:vec = D3DXVECTOR3(-0.08972908f, -0.49408212f, 0.3287904f); break;
 case 12:vec = D3DXVECTOR3(0.7119986f, -0.0154690035f, -0.09183723f); break;
 case 13:vec = D3DXVECTOR3(-0.053382345f, 0.059675813f, -0.5411899f); break;
 case 14:vec = D3DXVECTOR3(0.035267662f, -0.063188605f, 0.54602677f); break;
 case 15:vec = D3DXVECTOR3(-0.47761092f, 0.2847911f, -0.0271716f); break;
 }
  // テクスチャなので0-1の値しか持てないので、-1,1を0,1へ変換
 pOut->x = (vec.x+1.f)*0.5f;
 pOut->y = (vec.y+1.f)*0.5f;
 pOut->z = (vec.z+1.f)*0.5f;
 pOut->w = 0.f;
 ++i;
}
毎フレのループが以下の感じです。
float f,l; // 毎フレ動かしたかったので適当な変数
struct d3dverts {
 float x, y, z, w;
 float u, v;
 enum { fvf = D3DFVF_XYZRHW | D3DFVF_TEX1 };
};

d3ddev->BeginScene();
f+=dt; // もちろん適当でdtはデルタタイムをどっかから持ってくる
l=sin(f)*2.f+300.f;

D3DXMATRIX mat;
D3DXMatrixLookAtLH( &View, &D3DXVECTOR3(l*sin(f),5.f,-l*cos(f)), &D3DXVECTOR3(0,0,0), &D3DXVECTOR3(0,1,0) );
D3DXMatrixIdentity( &mat );
mat = mat * View * Proj;
// シェーダ内で使う変数更新
pEffect->SetMatrix( "m_WVP", &mat );
pEffect->SetVector( "m_LightDir", &D3DXVECTOR4(1,1,1,0) );
pEffect->SetVector( "m_Ambient" , &D3DXVECTOR4(0.5,0.5,0.5,0));

// MRTをつかって普通のカラーレンダリングと法線と深度値のテクスチャを
// それぞれ surColor(texColor)/surNormalDepth(texNormalDepth)に出力する
d3ddev->SetRenderTarget(0, surColor);
d3ddev->SetRenderTarget(1, surNormalDepth);
d3ddev->Clear( 0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, 0, 1.0f, 0);

// 描画開始
pEffect->SetTechnique( "RenderScene0" );
UINT numPass;
pEffect->Begin( &numPass, 0 );

pEffect->BeginPass(0);
// ここはどうにかメッシュでもモデルデータを表示する処理を作っておいてください
for(auto ite = m_kMeshObjectList.begin(); m_kMeshObjectList.end()!=ite; ite++)
{
 (*ite)->Draw();
}
pEffect->EndPass();
// ここまででカラーと法線深度テクスチャの完成

// ここから上で作ったテクスチャをもとに画面に描画する
d3ddev->SetRenderTarget(0, backbuffer);
d3ddev->SetRenderTarget(1, NULL);  // これやらないと表示されない!!
d3ddev->Clear( 0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, 0, 1.0f, 0);
// それぞれ0,1,2の順番でそれぞれのテクスチャを設定する
d3ddev->SetTexture(0, rayMap );
d3ddev->SetTexture(1, texNormalDepth );
d3ddev->SetTexture(2, texColor );
        
// SSAOはピクセルシェーダだけなのでバーテックスシェーダは走らせない
pEffect->BeginPass(1);
// 画面いっぱいに4角ポリゴンを表示してピクセルシェーダを走らせる
// この時 d3dverts で適宜した D3DFVF_XYZRHW がヴァーテックスシェーダ走らせなくても計算済みの頂点(4角ポリ)
// だよと定義するものらしいです。
d3ddev->SetFVF( d3dverts::fvf  );
const d3dverts quadverts[4] = {
{ -0.5f+0.f,   -0.5f,                   0.0f, 1.0f, 0.0f, 0.0f},
{ -0.5f+GetScreenWidth(), -0.5f,                   0.0f, 1.0f, 1.0f, 0.0f},
{ -0.5f+0.f,   -0.5f+GetScreenHeight(), 0.0f, 1.0f, 0.0f, 1.0f},
{ -0.5f+GetScreenWidth(), -0.5f+GetScreenHeight(), 0.0f, 1.0f, 1.0f, 1.0f},
};
d3ddev->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, quadverts, sizeof(d3dverts));
pEffect->EndPass();
pEffect->End();
d3ddev->EndScene();
d3ddev->Present( NULL, NULL, NULL, NULL );
以下がシェーダです。
これは
http://d.hatena.ne.jp/shuichi_h/20100318
から拝借したもので、コードの説明自体は同じことです。
SSAO.fx
はじめのバーテックスシェーダとピクセルシェーダは通常のモデル描画&カラー、法線深度値出力用

float4x4 m_WVP;
float4 m_LightDir;
float4 m_Ambient = 0.0f;
struct VS_OUTPUT
{
  float4 Pos   : POSITION;     //頂点の座標
  float4 Col   : COLOR0;       //頂点カラー
  float4 depth : COLOR1 ;
  float3 normal : COLOR2 ;
};
VS_OUTPUT VS( float4 Pos     : POSITION,   //頂点の座標
              float4 Normal  : NORMAL )    //法線ベクトル
{
   VS_OUTPUT Out = (VS_OUTPUT)0;
   Out.Pos    = mul( Pos, m_WVP );
   float3 L = -normalize( m_LightDir.xyz );
   float3 N = normalize( Normal.xyz );
   Out.Col = max( m_Ambient, dot(N, L) );
   // 深度値は座標そのもの
   Out.depth = Out.Pos;
   // 法線
   Out.normal = Normal.xyz;
   Out.normal = normalize(Out.normal);
   return Out;
}
struct SP_OUTPUT
{
  float4 Color  : COLOR0; // カラー
  float4 NormalDepth   : COLOR1; // 法線と深度
};
SP_OUTPUT PS( VS_OUTPUT In ): COLOR0
{  
  SP_OUTPUT Out = (SP_OUTPUT)0;
  Out.Color = In.Col;
  Out.NormalDepth.xyz = In.normal;    // xyzが法線で
  Out.NormalDepth.w = In.depth.z/In.depth.w;  // wに深度
  return Out;
}
float totStrength = 1.38;
float strength = 0.0007;
float offset = 18.0;
float falloff = 0.000002;
float rad = 0.03;

#define SAMPLES 16
const float invSamples = 1.0/SAMPLES;

sampler rayMap : register(s0) = sampler_state 
{ 
  MipFilter = NONE;
  MinFilter = POINT;
  MagFilter = POINT;
}; 
sampler normalMap : register(s1) = sampler_state
{
  MipFilter = NONE;
  MinFilter = LINEAR;
  MagFilter = LINEAR;
};
sampler colorMap : register(s2) = sampler_state
{
  MipFilter = NONE;
  MinFilter = LINEAR;
  MagFilter = LINEAR;
};
float4 SSAO( float2 uv : TEXCOORD0) : COLOR0
{
  float4 Output = (Float4)0;

  float4 currentPixelSample = tex2D(normalMap, uv);
  float currentPixelDepth = currentPixelSample.a;
  float3 norm = currentPixelSample.xyz * 2.0f - 1.0f;

  float bl = 0.0;
  float radD = rad / currentPixelDepth;

  float3 ray, occNorm;
  float2 se;
  float occluderDepth, depthDifference, normDiff;

  for(int i=0; i'<'SAMPLES; ++i)
  {
    float3 fres = tex2D(rayMap, float2(float(i)/16.f,0)).xyz*2.f-1.f;
    ray = radD * fres;
    se = uv + sign(dot(ray,norm)) * ray * float2(1.0f, -1.0f);
    float4 occluderFragment = tex2D(normalMap, se.xy);
    occNorm = occluderFragment.xyz * 2.0f - 1.0f;
    depthDifference = currentPixelDepth - occluderFragment.a;
    normDiff = (1.0 - dot(normalize(occNorm), normalize(norm)));
    bl += step(falloff, depthDifference) * normDiff * (1.0 - smoothstep(falloff, strength, depthDifference));
  }
  float ao = 1.0 - totStrength * bl * invSamples;
  Output = tex2D(colorMap, uv)*ao;
  return Output;

}
technique RenderScene0
{
  pass P0
  {
    VertexShader = compile vs_3_0 VS();
    PixelShader  = compile ps_3_0 PS();   
  }
  pass P1
  {
    PixelShader  = compile ps_3_0 RenderScenePS0();
  }
}

2011年6月10日金曜日

winmainの隠蔽工作

全体を通しての技術経験をつけたいと思い、
フレームワークを作っていこうと思います。
まずはじめに、プログラムを実行するにあたって一番初めに俺のフレームワークのエントリーポイント的なものを考えます。
マルチプラットホーム向けに作るつもりは無いのですが、ひとまずwinmainから書くのが嫌なので隠蔽します。
どこかのサイトで調べたのですが忘れたので、見つけたら改めて記述します。
こうすると出来るそうです。
まあ、動いています。

とにかく使い方です。
ヘッダファイルにクラス定義
class Prototype : public Framework
{
public:
 Prototype(void){}
 virtual ~Prototype(void){}
 virtual void MainLoop()
 {
 //ここに書いたものが自動で実行される
 }
}
cppファイル
Prototype theApp;


これで、
winmainなど書かなくても、自分のクラスのインスタンスをnewして誰かに渡さなくても、これだけでいきなりPrototypeのMainLoopが実行されるようになります。
からくりは以下です。
Prototypeは以下のFrameworkを継承しています。
こちらにはwinmainが書いてあり以下のようになっています。

ヘッダファイル
class Framework
{
public:
 static Framework* ms_pInstance;
 Framework(void);
 virtual ~Framework(void);
 int Boot();
}
cppファイル
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE , LPTSTR lpCmdLine, int nCmdShow)
{
 return Framework::ms_pInstance->Boot();
}

Framework* Framework::ms_pInstance = NULL;
Framework::Framework(void)
{
 ms_pInstance = this;
}
Framework::~Framework(void){}

int Framework::Boot()
{
 // おれのエントリーポイント
 MainLoop();
 return 0;
}

とにかくどこかにあるwinmainが呼ばれて起動します、そこでいきなり
Framework::ms_pInstance->Boot();
を呼ぶので継承したPrototypeのMainLoopまで来ます。
しかし、FrameworkはPrototypeが自分を継承していることがわからないのでwinmainで呼ばれるFrameworkは本来誰だかわからないはずです。
ですが、どこかで
Prototype theApp;
と記述しておくとグローバル定義のインスタンスになって、これはもちろんコンストラクタよりも早くメモリに存在するわけで。するともちろん、Prototypeのコンストラクタが動くので必然的に親クラスのコンストラクタが呼ばれ、そこでこれまたグローバルな
ms_pInstance = this;
Frameworkのポインタに継承した自分(Prototype theApp;で起動したコンストラクタから)のポインタが入って
めでたくこのグローバルなFrameworkのポインタは継承先のPrototypeを差してくれます。
なので、誰が継承したのか知らない親クラスが継承先のPrototypeに乗っ取られてうごきだしちゃいます。的なイメージでしょうか。
以降、Frameworkを継承したクラスだけ書けば勝手に動いてくれます。
めでたし。

ああ、思い出したMFCの実装がこれです。

2011年6月8日水曜日

SSDO Screen Space Directional Occlusion スクリーンスペース近似大域照明

http://www.mpi-inf.mpg.de/~ritschel/SSDO/

概要

リアルタイムで実現できる物理的に妥当な照明は、多くの場合近似を使って実現されています。
ひとつのポピュラーな例としてアンビエントオクルージョン(AO)があり、非常にシンプルでかつ効果的な実装は多くの製品で採用されています。
最近はスクリーンスペースでの近いジオメトリを使用したAOを近似する方法が採られています。

今回この論文で注目すべき点は、この手法が、スクリーンスペースで計算される遮蔽情報から、
例えば方向を考慮した影や間接的な色のにじみなど、より多くの影響を与える遮蔽情報を取得する為に使用することが出来ることです。

今回提案された手法は、スクリーンスペースで処理し従来のSSAOに比べてわずかなオーバーヘッドで直接光、単反射光を近似し、マクロ構造の過程をシミュレートするための別の手法と結合することができます。
そして、最悪のケースでも今回の手法を用いなかったSSAOと見た目的には同等になります。

この手法はスクリーンスペースでの処理になるため、ジオメトリ形状の複雑さに依存しません。
妥当な方向遮蔽と間接照明の効果はリアルタイムに、完全にランタイムで大規模なシーンを表現することが出来ます。

1はじめに

リアルタイムな大域照明は未だ、大規模で動的なシーンに於いて未解決の問題を持っています。
現在のところ十分なフレームレートを達成しているのは、近似を使う方法のみです。
そのような近似には、高速性と、実装の簡易さから映画やゲームなどに使われているAOがあります。

ただしAOは視界と照明を切り離しており、実際の照明の荒い近似のみをおこなっています。
AOは通常、空洞物の暗域を表現できますが、入射光の方向は無視されます。

今回Screen Space Directional Occlusion(SSDO)と名付けた、よりリアルな照明へ向けてSSAOを拡張しました。

本研究ではSSDOが
・どのように入射光の方向の集計を行うか
・どのように環境光の単反射を含むか
・どのように標準的なオブジェクトベースの大域照明の代わりをするか
・どのように少ない計算コストで済ませるか

の説明をしています。
構成は以下のとおりである。
まず、第2節で既存の動作を確認します。
第3節では、中央構造照明のAOの一般化を説明します。
第4節では、画質を向上させるための拡張機能について説明します。
第5節では完全なグローバルイルミネーションのシミュレーションと、本手法との統合が記載されています。
第6節で結果の提示。
第8章で結論づける前に第7章にて議論をしています。

2準備

物理的に妥当な照明は、全体の可視領域と照明の方向について計算をする時、 AOは可視領域と照明の2つの個別の積を積算します。
静的なシーンでは、AOは可視性を事前計算し、(頂点やテクスチャを使用して)表面上のスカラー場としてそれを保存することができます。

単純な乗算を使用した静的なAOと動的な照明の組み合わせは、高フレームレートで見た目に妥当な結果をもたらします。
AOフィールドの導入によってオブジェクトの並行移動、回転とアニメションキャラクターに特化した解決方法も存在します。

変形する面と間接光の反射は、Bunnell [2005]のディスクセットを使用したジオメトリの近似によって解決されます。
より堅牢なものはHoberock and Jia [2007]によって提示され、さらにChristensen [2008]によってポイントベースのアンビエントオクルージョンと相互反射に拡張されました。
Mendez et al. [2006]ジオメトリ周辺の平均アルベドを使用した単純な色にじみ効果を計算します。
この手法は表面の離散化もしくはレイトレーシングへのどちらかを使用しています。
これはゲームのような現在のインタラクティブなアプリケーションで使用される動的なジオメトリの量に比例して増大してしまいます。
したがって、最近の方法は代わりにスクリーンスペースの表面を使って近似された遮蔽を計算します。[Shanmugam and Arikan 2007; Mittring 2007; Bavoil et al. 2008; Filion and McNaughton 2008].

SSAOの好まれる点はその単純な実装とハイパフォーマンスにあります。
これは、出力が感覚的で、ポストプロセスとして処理され、追加データ(例えば、表面の定義、BVH、kD-trees、シャドウマップのような空間の表現に使われる構造)が必要なく、さまざまなタイプのジオメトリで動作します(例:ディスプレースメントや法線マップ、頂点またはジオメトリシェーダ、等値面の光線と平面の交点を求めるレイキャスティング)。
イメージ空間の手法はまた効率的に地下散乱をシミュレートすることができます。[Mertens et al. 2005].
同時にSSAOは様々な制限に対して近似を求められます。
これも今回の研究であり、後のセクションで詳しく説明します。

AOは一般的な光伝播に対しては粗い近似値になります。
例えば、PRT [Lehtinen and Kautz 2003]の指向性遮蔽(DO)と相互反射をサポートし、
事前計算では、しばしば空間や方向の解像度増大の制限のために圧縮形式のデータを大量に格納する必要が出てきてしまいます。
我々のアプローチは、非常に小さいサーフェイスの詳細、全方位の解像度のどちらも解決することができます:"無周波数"AO、全周波数イメージベースの照明やポイントライトからシャープな影。

PRTは広い照明範囲とそれなりの複雑さの静的なジオメトリとうまく動作しますが、
SSAOは妥協のないシンプルさがゆえに実際のアプリケーションに適応されています。

要約すると私たちの研究は、あらかじめ計算されている、
以前は動的には不可能だった最終的なリアリティーに影響する指向性遮蔽と間接的反射の2つ両方の近似計算をリアルタイムに、よりリッチなジオメトリに対してSSAOプロセス[Shanmugam and Arikan 2007] を用いることで活用しています。

3 イメージベースの近接場光伝搬

画像空間の光伝搬を計算するために、本手法は位置と法線のフレームバッファ[Segovia et al. 2006]を入力として使用しています。
フレームバッファは直接光及び間接バウンスの2つのレンダリングパス使用して照明ピクセルを出力します。

DOを用いた指向性照明

通常のSSAOは隣接するピクセルから、最初の計算で可視値の平均を求めピクセルの色を決定します。
この遮蔽の値はすべての入射方向からくる非遮蔽照明を乗算して求めます。

遮蔽と照明の関連性の低くさを取り除く処理を行う方法を次のように提案します。

3次元上に法線nの座標Pのピクセルがあり
方向ωiのN個のサンプリングから算出される直接光輝度をLdirとし、
均一に⊿ω=2π/Nで半球状に分散していると定義したとき

Ldir(P) = Σ(i:1->N)ρ/πLin(ωi)V(ωi)cosθi⊿ω.

各サンプルは、入射輝度Linと可視ベクトルVとディフューズBRDFρ/πの積を計算します。
我々は、Linを効率的にポイントライトや環境マップから計算することができることを前提としています。
スクリーンスペースで遮蔽物を近似する事で可視ベクトルVの算出に、レイトレーシング計算の使用を回避します。
点Pから方向ωiのランダムな距離λi [0...rmax]にあるすべてのサンプルを算出します。
rmaxは任意に定義した半径です。
これは結果、Pを中心とする半球に内包するサンプリング点P + λiωiと法線n方向との組みとなります。
続いて、3次元の点として点P周辺での局所的なサンプリングポイントを生成します。
そのうちのいくつかは表面の上、またいくつかは下に位置します。
サンプリングポイントが隣接するジオメトリの表面より下にあるものはすべて遮蔽物として近似の可視性判定として扱われます。

Fig.2:左 指向性遮蔽による直接光 互いのサンプル点は遮蔽物かどうかのテストに使われる。上記の例では、点Pは方向Cからのみ照明される。
右 間接光 直接光がフレームバッファに置いた、小さい範囲が互いの遮蔽物として表面上に配置されたものであり、対象点の輝度として使われる
Fig. 2 (左) はサンプリングポイントABCDのN=4の例です。サンプル点ABDは表面の下に位置している為点Pの遮蔽物として分類される、サンプル点Cは表面の上に位置しているので可視点として分類されます。
サンプル点が表面の下に位置するかテストして、イメージ上に投影されます。

今3次元の位置情報はポジションバッファから取得でき、その点がジオメトリの表面上に投影されます(赤の矢印)。
もし、このジオメトリ表面への投影によって観測点からサンプル点への距離が減少すれば、サンプル点は表面の下に位置すると分類することが出来ます。
例えば Fig. 2において点ABDは観測点に近づいたので表面の下に位置し、点Cは観測点から遠ざかります。
SSAOと対照的に、すべての点から照明の計算をせずに見えている方向(サンプル点C)からのみにします。
特に入射光照明が違う方向から、違う色でやってくる場合、この指向性の情報は結果を大幅に向上させることが出来ると言うことになります。
Fig.3の通り、SSAOでは単純にグレーカラーで影を表示するだけですが、こちらは正しい色で影を表示することが出来ます。
Fig3

Fig3

間接反射
1つ前のパスで、直接光がフレームバッファに書き込んだ単反射のライト情報を含むことで以下のことが可能になる。

遮蔽物として扱われる各サンプル点(ABD)に対応するピクセルカラーLpixelは表面上に配置される単位面の発光輝度として使用される。(Fig2右)
ここで発光単位面の裏側からの色漏れを避けるために発光側の法線を考慮する。

ジオメトリ周囲の付加輝度は以下の式で近似できます。

Lind(P) = Σ(i:1->N)ρ/πLpixel(1-V(ωi))(As cosθsi cosθri)/di^2

diは点Pと遮蔽物iとの距離(diは特異点問題を避けるために1でクランプする)
θsiとθriはそれぞれ発光者(sender)と受光者(receiver)の法線と透過方向の角度です。
Asは発光者単位面に関連する領域です。
単位面領域の前提値として半球内の平面を想定しています。
基礎円はN個の領域に分割されているので、As=πr^2max/Nの領域をお互いにカバーします。
半球内の斜面の分布に応じて、実際の値は高くすることが出来ます。
このパラメータを色にじみ強度の手動のコントロールとしてに使うことが出来ます。
例として、fig2において単位平面Aは裏を向いているために間接光計算は何も寄与していません。
単位平面Cは点Pの半球の反対側にありますのでこれもまた関与しません。
単位平面BCは点Pに対して間接光の発光者となります。
Fig3は間接光の反射を標示しています。

細部の実装
従来のSSAO [Shanmugam and Arikan 2007] は同様の手順と計算コストがかかります。
今回の手法は可視判定に相当するシェーディングモデルを判断する為により多くの計算を必要とします。
今回は太陽光のような既知の重要な光源には、スクリーンスペースでの手法ではなく、代わりにジオメトリの距離から影をキャプチャーするシャドウマップを適応する追加のサンプルを使います。  
N個の事前計算された食い違いの少ないサンプルλiωiのセットM個を格納するためにMxNのテクスチャを使います。
各ピクセルは実行時にMセット外の一つを使っています。
最後のパスでノイズを取り除くために微細なジオメトリブラー[Segovia et al. 2006]を適応します。
これはピクセルあたりのサンプルのリダクションによって提案されています。

4 マルチピクセル
Fig.4
スクリーンスペースでの処理なので、すべての遮蔽物や間接光源が見えているわけではありません。
Fig.4の例は間接照明の光源が遮蔽されていくにつれて、色のにじみがフェードアウトしていくのを示しています。
それらは、付属のビデオのとおり見た目にひどくがっかりするような見た目ではありません。しかし、実際の結果とは違っているのです。
この差異を減らす措置として、そのような照明結果を克服する2つのアプローチ、深度値の剥離と追加カメラを提示します

単一深度の限界
Fig.5
スクリーンスペースでの可視判定問題(左):可視判定のサンプル点Aは投映位置が観測位置に近いため遮蔽物として分類されます。サンプル点Bは表面の上にあるので、対応する方向は閉鎖されていても点Pはこの方向から誤って照明されてしまいます。
解決法(右): 2つのレイヤーを使用して深度乖離します。サンプル点Aは第一第二深度値の間にいないため可視と分類することが出来ます。B方向からのサンプル点を増やすことで、遮蔽物を見つける事が出来ます。
前のセクションで提示した遮蔽物テストは近似です。単一のフレームバッファでは、最初の深度値のみ知ることができ、遮蔽されたジオメトリ情報は失われてしまいます。
Fig. 5(左)に示すように、サンプリング点はそれゆえに間違って判断されます。
いくつかの状況では、入射光を乖離判定してしまったり、本来遮蔽されるべき方向を可視判定してしまういます。
サンプル点Bの遮蔽判定ミスは、この方向のサンプル点を増やすことよって訂正することが出来ますが、サンプル点Aの乖離(そして、反射光の入射位置を近くしてしまう)は単一の深度値z1の背後のシーンについての情報が無い為に単一の視点からでは訂正することができません。

深度値の剥離
剥離深度[Everitt 2001]は、nレンダリングパスのあとに互いにフレームバッファへ単一の深度値nが書き込まれます。
これは、このシーンに関してより多くの情報を持つことで遮蔽物判定を改善する猶予があります。
もしサンプル点が第一深度z1の背後にあるならば、そのまま判定せずに、第二の深度値z2の前にあるかテストをする。

第一第二深度はオブジェクトの表面と裏面にそれぞれ対応し、2つの集合管ジオメトリを使用する場合、2つの面の間のサンプルはこのオブジェクトの内側にしなければならない。(Fig.5右)
複雑さの高い深度のシーンのすべての影を再構築する為には、第3、第4深度値と連続する深度値のすべての組みは同じ方法で評価しなければならない[Lischinski and Rappoport 1998]

2011年6月4日土曜日

コンパイルタイム文字列ハッシュ化

こちらの記事に
http://d.hatena.ne.jp/yupo5656/20040613/p1
コンパイル時に文字列リテラルに指定のアルゴリズムによる計算を施し、整数に変換するには?
と言うのがあったので参考にしてみました。

template <std::size_t N> inline hash32 hashStringStatic(const char (&str)[N]) { return crc32(N-1, reinterpret_cast<const char (&)[N-1]>(str)); } template <std::size_t N> inline hash32 crc32(hash32 hash, const char (&str)[N]) { return crc32( (hash >> 8) ^ crctbl[(unsigned char)((hash & 0xff) ^ str[N-1])], reinterpret_cast<const char (&)[N-1]>(str) );< } template<> inline hash32 crc32<1>(hash32 hash, const char (&str)[1]) { return (hash >> 8) ^ crctbl[(unsigned char)((hash & 0xff) ^ str[0])]; } typedef unsigned int hash32; // ハッシュテーブル static const hash32 crctbl[256] = { 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
です。
crc32の処理を再帰テンプレートで展開するわけなのですが、最適化でたしかに直値になっているかんじでした。
このアルゴリズムの都合のいいところは、単純な再帰型に出来たので楽でした。
他のハッシュアルゴリズムは展開できるか知りません。

ちなみにコンパイルタイムではないものも用意してみました。
inline hash32 hashStringDynamic(const std::string str)
{
const std::size_t len = str.length();
hash32 hash = len;
for ( std::size_t i = 0; i < len; i++ )
{
// static版に揃えるためけつから求める
const std::size_t index = len-1-i;
hash = (hash >> 8) ^ crctab[(unsigned char)((hash & 0xff) ^ str[index])];
}
return hash;
}
そんでですね、

ソース内に
 hash32 hash = hashStringStatic("sh/bokenasu.fx");
とかやっとくと数字になって入ってるみたいな。
こいつをフライウエイトのキーに使えば見やすく処理も速いのかなと思ったのです。
ただこれはもちろんリテラル文字にしか使えないので使うときにはどうなるのかとても注意が必要です。ただ、これだと、オリジナルの文字列が失われるので実際にファイル名等の文字列が必要なときに困ります。
そこで

struct hashString
{
hashString(hash32 hash_, char* str_)
: hash(hash_)
, str(str_)
{}
const hash32 hash;
const char* str;
};
#define HashString(x) hashString(hashStringStatic(x), x)

っつーのを作ってみたのですが、どうですか?
instance func(hashString hash)
{
 auto ite = m_kMap.find(hash.hash);
 if(m_kMap.end()!=ite) return ite->second;
 m_kMap[hash.hash]=new instance(hash.str);
 return m_kMap[hash.hash];
}
instance = func(HashString("sh/bokenasubi.fx"));
みたいな事やれば良いんじゃないでしょうか?

つか本当にアセンブラレベルで直値になっているかが重要なんですけど、どうなんですかね?

ぶっちゃけ、アセットなんかは全部外部の定義ファイルに書いて読み込む事になるので、リテラル文字限定ってのはまあ使えないわけですが。。。
外部ファイルから読み込む場合は予めもっとシノニムの起きづらいmd5で文字列をハッシュ化したものを文字列とペアで記述しておけばいいですかね。