Posted at

oFでDeferred Renderingしてみよう

More than 1 year has passed since last update.


はじめに

 ここでは、以下2つを取り扱います。


  1. ディファードレンダリングの概要解説

  2. openFrameworksによる実装解説

 巻末の参考資料をベースにしているので、詳細が気になる方はそちらも見てみてださい。ソースコードもGithubにアップしています。


ソースコード

https://github.com/yumataesu/of-DeferredRendering


ディファードレンダリングの前に、フォワードレンダリングを理解する

 以下、「ゲームエンジンアーキテクチャ第2版(著:ジェイソン・グレゴリー)」から丸々引用します。


昔ながらの、三角形のラスタライズに基づいたレンダリングでは、ライティングやシェーディングの計算はすべて、ワールド空間、ビュー空間、接空間の三角形のフラグメントに対して行われている。この方法の問題は、本質的に非効率だという点だ。1つには、やらなくていい処理をやっている可能性がある。三角形の頂点のシェーディングを実行し、ラスタライズのステージになってようやく、zテストの結果、三角形全体が深度で除去されてしまうということがわかる。早期Zテストを行うことで、実行する必要のないピクセルシェーダの評価を除外することができるが、それでも完壁ではない。さらには、光源の多い複雑なシーンを処理するために、異なる数の光源、異なる種類の光源、異なる種類のスキニングの重み係数、などを処理するための、さまざまなバージョンの頂点シェーダやピクセルシェーダが無意味に増えることになる。


 同本では言及していませんが、この"昔ながらの三角形のラスタライズに基づいたレンダリング"は、「フォワードレンダリング(前方レンダリング)」と言います。ライトの反射の計算がされた上で、最後に深度テストが実行されます。カメラから写っていない奥のフラグメントは破棄され、最終的な結果がレンダリングされます。

ゲームエンジンアーキテクチャでは、このフォワードレンダリングは、以下の問題点があると指摘しています。


  • 余計な計算をめっちゃしている(最終的に深度テストで破棄するフラグメントの計算もしている)

  • シーンが複雑になるほど、シェーダーも複雑になり無意味な計算も増える


ディファードレンダリングとは

 上記の問題点を解決する手段が、ディファードレンダリングです。ここでも、ゲームエンジンアーキテクチャを引用します。


ディファードレンダリングは、シーンのシェーディングをする場合に、上記の問題点の多くを解決する代替手段だ。ディフアードレンダリングでは、ライティングの計算の大部分は、ビュー空間ではなくスクリーン空間で実行されるため、ライティングのことを気にせずに、シーンを効率的にレンダリングできる。このフェーズでは、ピクセルのライティングに必要な情報をすべて、Gバッファと呼ばれる、「深い」フレームバッファに格納する。そして、シーンがすべてレンダリングされると、Gバッフアの情報を使ってライティングとシェーディングを計算する。通常これは、ビュー空間のライティングよりもはるかに効率的で、シェーダのバリエーションの不要な増加を防ぎ、非常に良い効果が比較的簡単にレンダリングできる。Gバッファは物理的にはバッファの集合体として実装されることがあるが、概念的には、シーンのオブジェクトについて、スクリーンのピクセルごとにライティングと表面属性の情報を豊富に持った、単一のフレームバッファである。典型的なGバッファには、深度、ビュー空間やワールド空間の面法線、拡散色、反射光強度、あるいは事前計算済み放射輝度伝播(PRT)係数までも、ピクセルごとの属性として保持されている。


文字だけでは分からないですね。図説してまとめました。

1.GBufferを作成する

 まず、GBufferという複数のテクスチャを格納できるフレームバッファを生成します。テクスチャは3つ生成し、ここにそれぞれ「位置」「法線」「色」を保存します。

DF.001.jpeg

 

 

2.「位置」「法線」「色」情報ををgBuffer内に"テクスチャ"としてレンダリング(Geometry Pass)

 次の段階で、ジオメトリの「位置」「法線」「色」の3つの情報をこのGBufferに"テクスチャ"として一旦レンダリングし保存します。

DF.002.jpeg

 

 

3. レンダリングされた各テクスチャを元にライティングの計算を行う(Lighting Pass)

 最後は、ジオメトリの「位置」、「法線」、「色」とライトの色、位置、距離減衰係数を組み合わせてライティングの計算をおこないます。

DF.003.jpeg

 ディファードレンダリングの特徴は、シェーダーが深度テストに合格したフラグメントのみライティングの計算を考慮するということです。720pの場合1280×720個のピクセルだけ考えれば良いことになります。これにより、余計な計算を省略することができ、レンダリングの効率を大幅に上げることができます。

 また、SSAOやモーションブラー、グローエフェクトなどといった効果もディファードレンダリングであれば、比較的容易に実装することが可能になります。


openFrameworksで実装してみよう

 さて、openFrameworksでディファードレンダリングを実装してみましょう。最終的にこんな感じになります。

DF2.gif

glsl部分は、参考*2のサイトのコードをほとんど引用したので、ここではoF側のみの解説をしていきます。

僕の環境は以下


  • openFrameworrks 0.9.7

  • OpenGL 3.3

  • Xcode 8

  • MacOX 10.11.6


1. GBufferを作成する

 まず、GBufferを作成します。ofFboには、createAndAttachTexture()と呼ばれるMRTのためのメソッドが用意されていますが、参考のLearn OpenGLから丸々拝借して&問題く使えたので、ofFboは使わず、OpenGLから直接作成しています。


setup

glGenFramebuffers(1, &gBuffer);

glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);

glGenTextures(1, &gPosition);
glBindTexture(GL_TEXTURE_2D, gPosition);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGBA, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gPosition, 0);

glGenTextures(1, &gNormal);
glBindTexture(GL_TEXTURE_2D, gNormal);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, gNormal, 0);

glGenTextures(1, &gAlbedo);
glBindTexture(GL_TEXTURE_2D, gAlbedo);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, gAlbedo, 0);

GLuint attachments[3] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 };
glDrawBuffers(3, attachments);

GLuint rboDepth;
glGenRenderbuffers(1, &rboDepth);
glBindRenderbuffer(GL_RENDERBUFFER, rboDepth);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth);

// - Finally check if framebuffer is complete
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
std::cout << "Framebuffer not complete!" << std::endl;

glBindFramebuffer(GL_FRAMEBUFFER, 0);



2. Geometry Pass:MRTを用いて、ポジション・ノーマル・アルベドマップをGBuffer内の各テクスチャにレンダリング

 oFは自動的にModelViewMatrixやViewMatrixなどをシェーダーに渡してくれる仕様になっていますが、ややこしいので自分で定義して渡しています。

シェーダーに渡す情報は以下。


  • カメラのビュー行列

  • カメラのプロジェクション行列

  • gAlbedo用のカラーマップ

  • gNormal用の法線マップ

  • オブジェクトのモデル行列


draw

glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
cam.begin();
ofMatrix4x4 viewMatrix = ofGetCurrentViewMatrix();
cam.end();

GeometryPass.begin();
GeometryPass.setUniformMatrix4f("view", viewMatrix);
GeometryPass.setUniformMatrix4f("projection", cam.getProjectionMatrix());
GeometryPass.setUniformTexture("tex", texture, 0);
GeometryPass.setUniformTexture("normal", normal, 1);
for(int i = 0; i < NUM; i++)
{
ofMatrix4x4 model;
model.translate(pos[i]);
GeometryPass.setUniformMatrix4f("model", model);

cone.draw();
}
GeometryPass.end();
glBindFramebuffer(GL_FRAMEBUFFER, 0);


Geometry Passの処理により、GBuffer内の各テクスチャに

以下のようなレンダリング結果が保存されます。


gPosition

gPosition.gif


gNormal

gNormal.gif


gAlbedo

gAlbedo.gif


3. Lightting Pass:2で生成したテクスチャを元にライティングの計算を行う

 2で作成したテクスチャを用いてライトの計算を行います。

シェーダーに渡す情報は以下。


  • gPositionテクスチャ

  • gNormalテクスチャ

  • gAlbedoテクスチャ

  • カメラの位置

  • ライトのスクリーン空間における位置

  • ライトの色

  • ライトの距離減衰係数


draw

LightingPass.begin();

LightingPass.setUniformTexture("gPosition", GL_TEXTURE_2D, gPosition, 0);
LightingPass.setUniformTexture("gNormal", GL_TEXTURE_2D, gNormal, 1);
LightingPass.setUniformTexture("gAlbedo", GL_TEXTURE_2D, gAlbedo, 2);
LightingPass.setUniform3f("ViewPos", cam.getPosition());

for(int i = 0; i < LightNUM; i++)
{
ofVec3f lightPos = viewMatrix.preMult(pointlight[i].getPosition()); // equal to pointlight.getPosition() * viewMatrix
LightingPass.setUniform3fv("light["+to_string(i)+"].position", &lightPos[0], 1);
LightingPass.setUniform3fv("light["+to_string(i)+"].ambient", &pointlight[i].getAmbient()[0], 1);
LightingPass.setUniform3fv("light["+to_string(i)+"].diffuse", &pointlight[i].getDiffuse()[0], 1);
LightingPass.setUniform3fv("light["+to_string(i)+"].specular", &pointlight[i].getSpecular()[0], 1);
}

LightingPass.setUniform1f("constant", constant);
LightingPass.setUniform1f("linear", linear);
LightingPass.setUniform1f("quadratic", quadratic);
LightingPass.setUniform1i("DebugMode", DebugMode);

 quad.draw(OF_MESH_FILL);
LightingPass.end();


これで、以下のような結果がレンダリングされるはずです。

DF2.gif

oFによるディファードレンダリングの実装解説は以上です。

ライト30個あたりからfpsが怪しくなってきますが、十分いい感じの結果が得られたと思っています。マシンのスペックが良ければもっと出せるはず。


おまけ:oF側の座標変換はvec × matの順

 今回一番ハマったのが、ofMatrixの仕様。

oF公式ドキュメントによると、ofMatrixは行ベクトルスタイルで実装されており対して、glslの行列は列ベクトルスタイルで実装されているとあります。

glslでは、mat×vecの順番で座標変換をかけますが、oFで座標変換をかける場合はvec × matになるという.......。ライティングパスにライトの位置を渡す際にハマりました。


NG

ofVec3f lightPos = viewMatrix * pointlight[i].getPosition();



OK

ofVec3f lightPos = pointlight[i].getPosition() * viewMatrix;

もしくは
ofVec3f lightPos = viewMatrix.preMult(pointlight[i].getPosition());


参考資料