2D指向性照明とシェーディング



こんにちは、ハブラフチャン!

シーンのジオメトリを考慮して、2D空間で照明とシェーディングをレンダリングする方法の1つについてお話したいと思います。 GishとSuper MeatBoyの照明の実装は本当に気に入っていますが、mitbaでは、崩壊または移動するプラットフォームの動的レベルでしか見ることができず、Guicheではどこにでもあります。 そのようなゲームの照明はとても「暖かく」ランプなので、自分自身に似たものを実装したいと思っていました。 そして、ここからそれが生まれました。



何が何であり、何をする必要があるかは明らかです。







これにはすべて、OpenGL、GLSL、FrameBufferテクノロジー、および少しの数学が必要でした。 以降のOpenGL 3.3およびGLSL 3.30のバージョンに限定 私のシステムのビデオカードは今日の標準(GeForce 310)で非常に古く、2Dではこれで十分です(そして、以前のバージョンはOpenGLとGLSLの一貫性のないバージョンにより拒否を引き起こします)。 アルゴリズム自体は複雑ではなく、3段階で行われます。

  1. 黒のレンダリング領域のサイズのテクスチャを生成し、その領域に照明領域(いわゆる照明マップ)を描画し、すべてのポイントの照明係数を累積します。
  2. シーンを個別のテクスチャにレンダリングします。
  3. レンダリングのコンテキストでは、それを完全に覆うクワッドを表示し、フラグメントシェーダーでは結果のテクスチャを減らします。 この段階では、フラグメントシェーダーで「遊ぶ」ことができます。たとえば、水/火からの屈折の効果、レンズ、あらゆる味の色補正、その他の後処理を追加できます。




1.照明マップ



最も一般的なテクノロジーの1つを使用します- 遅延シェーディング 、2Dに移植します(PS、謝罪、遅延シェーディングではなく、 シャドウマップ 、修正、ありがとう)。 このメソッドの本質は、深度バッファを取得して、カメラを光源の位置に移動することでシーンをレンダリングすることです。 カメラのマトリックスとシェーダーの光源を使用した簡単な操作で、レンダリングされたシーンのピクセル座標をバッファーのテクスチャ座標に変換することにより、シェーディングについてピクセルごとに学習できます。 3Dではz-bufferが使用されますが、ここではCPU上に1次元の深度バッファーを作成することにしました。

私はこのアプローチを合理的かつ楽観的にするつもりはありません。十分な照明アルゴリズムがあり、それぞれにプラスとマイナスがあります。 ブレーンストーミング中、この方法は非常に命にふさわしいと思われたので、私はそれを実装することにしました。 記事を書いている時点で、私はこの方法で見つけたことに注意してください...まあまあまあ、自転車は自転車です。



1.1 Zバッファー別名深度バッファー



Zバッファの本質は、カメラからのシーン要素の遠隔性を保存することです。これにより、近くのオブジェクトの背後に見えないピクセルをカットできます。 3Dシーンで深度バッファが平面の場合



、その後、フラットな世界では、線または1次元配列になります。 光源-中心からすべての方向に光を放射するポイント。 したがって、バッファのインデックスと値は、ソースに最も近いオブジェクトの位置の極座標に対応します。 経験的にバッファのサイズを決定しました。その結果、1024で停止しました(もちろん、ウィンドウのサイズに依存します)。 バッファサイズが小さいほど、オブジェクトの境界と照らされた領域との間の不一致が顕著になります。特に小さなオブジェクトが存在する場合、および完全に許容できないアーティファクトが表示される場合があります。

非表示のテキスト








バッファアルゴリズム:





1.2頂点フレーム



次に、深度バッファのデータに従って、光源を照らす領域全体をカバーするポリゴンモデルを構築する必要があります。 このためには、 三角形ファン方式を使用すると便利です。



ポリゴンは、最初のポイント、前のポイント、および現在のポイントから形成されます。 したがって、最初の点は光源の中心であり、残りの点の座標は次のとおりです。

for( unsigned int index = 0; index < bufferSize; ++index ) { float alpha = float( index ) / float( bufferSize ) * Math::TWO_PI; float value = buffer[ index ]; Vec2 point( Math::Cos( alpha ) * value, Math::Sin( alpha ) * value ); Vec4 pointColor( color.R. color.G, color.B, ( 1.0f - value / range ) * color.A ); ... }
      
      





ゼロインデックスを複製してチェーンを閉じます。 すべてのポイントの色は、明るさの透明度の値の違いの後は同じです-中央は、光源の半径(範囲)0.0での最大の明るさです。 透明度の値は、ソースの中心からのポイントの距離のインジケーターとしてフラグメントシェーダーでも役立ちます。したがって、テクスチャの使用まで、距離に対する照明の線形依存性をより興味深いものに置き換えることができます。

この段階で、取得したポイントを特定の値だけ強制的に遅らせて、光線が当たる表面を照らし、ボリュームの外観を作成することもできます。



1.3フレームバッファ



フレームバッファーにバインドされた1つのテクスチャ-GL_RGBA16F形式で十分です。この形式では、[0.0; 1.0]半精度浮動小数点の精度。

少しの「擬似コード」
  GLuint textureId; GLuint frameBufferObject; //. width  height -   glGenTextures( 1, &textureId ); glBindTexture( GL_TEXTURE_2D, textureId ); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL ); glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); glBindTexture( GL_TEXTURE_2D, 0 ); // glGenFramebuffers( 1, frameBufferObject ); glBindFramebuffer( GL_FRAMEBUFFER, frameBufferObject ); //    glFramebufferTexture2D( GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureId, 0 ); //    glBindFramebuffer( GL_FRAMEBUFFER, 0 ); //    ,  -   ... if( glCheckFramebufferStatus( GL_FRAMEBUFFER_EXT ) != GL_FRAMEBUFFER_COMPLETE ) { ... } ...
      
      







バッファーをビンディムし、添加剤ブレンドglBlendFunc(GL_ONE、GL_ONE)を設定し、照らされた領域を「描画」します。 したがって、アルファチャネルは照明の度合いを蓄積します。 ウィンドウ全体に四角形を描画することで、グローバル照明を追加することもできます。



1.4シェーダー



カメラの位置を考慮して、光源からの光線をレンダリングするための頂点シェーダーが標準であり、フラグメントシェーダーでは明るさを考慮して色を蓄積します。

  layout(location = 0) out vec4 fragData; in vec4 vColor; ... void main() { fragData = vColor * vColor.a; }
      
      





最終的に、次のようになります。





2.シーンをテクスチャにレンダリングします



シーンを別のテクスチャにレンダリングする必要があります。そのために別のフレームバッファを作成し、通常のGL_RGBAテクスチャをアタッチして通常の方法でレンダリングします。

悪名高いプラットフォーマーからそのようなシーンがあるとしましょう:





3.ライティングマップとシーンの組み合わせ



フラグメントシェーダーは次のようになります。

  uniform sampler2D texture0; uniform sampler2D texture1; ... vec4 color0 = texture( texture0, texCoords ); //     vec4 color1 = texture( texture1, texCoords ); //    fragData0 = color0 * color1;
      
      





どこも簡単です。 ここで、乗算の前に、ゲーム設定が非常に暗く、光線を見る必要がある場合に、特定の係数をシーンcolor0の色に追加できます。

非表示のテキスト
 fragData0 = ( color0 + vec4( 0.05, 0.05, 0.05, 0.0 ) ) * color1;
      
      





そして...



キャラクターが単純なジオメトリで記述されていない場合、そのキャラクターからの影は非常に間違っています。 影はそれぞれジオメトリから作成され、スプライトキャラクターからの影は正方形から取得されます(ふと、Mitboy、正方形はどのような考慮事項からですか?)。 スプライトテクスチャは、できる限り正方形に描画し、エッジの周囲にできるだけ透明な領域を残さないようにしますか? これはオプションの1つです。 アニメーションの各フレームの同じジオメトリではなく、角を滑らかにすることで、キャラクターのジオメトリをより詳細に説明できますか? 角を滑らかにすると、キャラクターはほぼ楕円形になります。 シーンが完全に暗い場合、そのような影が目立ちます。 ライティングマップとグローバルライティングのスムージングを追加すると、画像はより受け入れやすくなります。

  vec2 offset = oneByWindowCoeff.xy * 1.5f; //  fragData = ( texture( texture1, texCoords ) + texture( texture1, vec2( texCoords.x - offset.x, texCoords.y - offset.y ) ).r + texture( texture1, vec2( texCoords.x, texCoords.y - offset.y ) ).r + texture( texture1, vec2( texCoords.x + offset.x, texCoords.y - offset.y ) ).r + texture( texture1, vec2( texCoords.x - offset.x, texCoords.y ) ).r + texture( texture1, vec2( texCoords.x + offset.x, texCoords.y ) ).r + texture( texture1, vec2( texCoords.x - offset.x, texCoords.y + offset.y ) ).r + texture( texture1, vec2( texCoords.x, texCoords.y + offset.y ) ).r + texture( texture1, vec2( texCoords.x + offset.x, texCoords.y + offset.y ) ).r ) / 9.0;
      
      





ここで、oneByWindowCoeffはピクセル座標をテクセルに変換するための係数です。

グローバルイルミネーションが存在しない場合、そのような「キャラクター」のシャドウをオフにするか、それらを光らせる(私の意見では理想的なオプション)か、混乱してすべてのアニメーションのオブジェクトのジオメトリを記述する方がよい場合があります。



私はこれらのすべての反省と完成から出てきた小さなデモを記録しました:





4.最適化



sayingにもあるように、「最初に記述してから最適化してください。」 最初のコードはすばやく大まかにスケッチされたため、最適化の余地が十分にありました。 最初に思いついたのは、照らされた領域を描く過剰な数のポリゴンを取り除くことでした。 光源の半径に障害物がない場合は、1000以上のポリゴンを描画しても意味がありません。そのような完全な円は必要ありません。目は違いを認識しません(または、このモニターは私にとっては汚れすぎです)。

たとえば、最適化のない次元1024の深度バッファの場合:

非表示のテキスト






最適化あり:

非表示のテキスト






多数の静的オブジェクトを含むシーンの場合、オブジェクトの投影をバッファに計算した結果をキャッシュできます。これにより、余弦/ルートおよびその他の高価な数学の数が減少するため、大幅に増加します。 したがって、各バッファーについて、オブジェクトへのポインターのリストを開始し、位置または形状に影響するパラメーターの変更を確認してから、バッファーに直接キャッシュを入れるか、オブジェクトを完全に再カウントします。



5.結論



この照明技術は、最適化、高速化、正確化のふりをするものではなく、目標は実装の事実でした。 シャドウだけを構築するなどのさまざまな手法があります(ライティングは理解しているように、追加でドッピングされます )。私が使用しました)。

一般に、計画されていたことが実現され、オブジェクトが影を落とし、ゲームに必要な抑圧的な雰囲気が作り出され、私の意見では絵がより快適になりました。



6.参照






All Articles