
こんにちは、ハブラフチャン!
シーンのジオメトリを考慮して、2D空間で照明とシェーディングをレンダリングする方法の1つについてお話したいと思います。 GishとSuper MeatBoyの照明の実装は本当に気に入っていますが、mitbaでは、崩壊または移動するプラットフォームの動的レベルでしか見ることができず、Guicheではどこにでもあります。 そのようなゲームの照明はとても「暖かく」ランプなので、自分自身に似たものを実装したいと思っていました。 そして、ここからそれが生まれました。
何が何であり、何をする必要があるかは明らかです。
- 動的なライティングとシェーディングを組み込む必要がある特定の2Dワールドがあります。 すべてのジオメトリから、世界は必ずしもタイルではありません。
- 光源は、原則として、無制限の数でなければなりません(システムのパフォーマンスによってのみ制限されます)。
- 一点に多数の光源が存在する場合、または「照明」係数が大きい光源が1つある場合、エリアを100%照らすだけでなく、照らす必要があります。
- もちろん、すべてをリアルタイムで計算する必要があります。

これにはすべて、OpenGL、GLSL、FrameBufferテクノロジー、および少しの数学が必要でした。 以降のOpenGL 3.3およびGLSL 3.30のバージョンに限定 私のシステムのビデオカードは今日の標準(GeForce 310)で非常に古く、2Dではこれで十分です(そして、以前のバージョンはOpenGLとGLSLの一貫性のないバージョンにより拒否を引き起こします)。 アルゴリズム自体は複雑ではなく、3段階で行われます。
- 黒のレンダリング領域のサイズのテクスチャを生成し、その領域に照明領域(いわゆる照明マップ)を描画し、すべてのポイントの照明係数を累積します。
- シーンを個別のテクスチャにレンダリングします。
- レンダリングのコンテキストでは、それを完全に覆うクワッドを表示し、フラグメントシェーダーでは結果のテクスチャを減らします。 この段階では、フラグメントシェーダーで「遊ぶ」ことができます。たとえば、水/火からの屈折の効果、レンズ、あらゆる味の色補正、その他の後処理を追加できます。
1.照明マップ
最も一般的なテクノロジーの1つを使用します-
私はこのアプローチを合理的かつ楽観的にするつもりはありません。十分な照明アルゴリズムがあり、それぞれにプラスとマイナスがあります。 ブレーンストーミング中、この方法は非常に命にふさわしいと思われたので、私はそれを実装することにしました。 記事を書いている時点で、私はこの方法で見つけたことに注意してください...まあまあまあ、自転車は自転車です。
1.1 Zバッファー別名深度バッファー
Zバッファの本質は、カメラからのシーン要素の遠隔性を保存することです。これにより、近くのオブジェクトの背後に見えないピクセルをカットできます。 3Dシーンで深度バッファが平面の場合

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

バッファアルゴリズム:
- 光源の半径の値(光の強度がゼロに達する距離)を入力します。
- 光源の半径内にある各オブジェクトについて、前面を光源に向けたエッジを取ります。 裏側でエッジを回転させると、オブジェクトは自動的に強調表示されますが、隣にあるオブジェクトには問題があります。
非表示のテキスト
- 結果のエッジのリストを投影し、デカルト座標を極光源に変換します。 (φ; r)の点(x; y)を再計算します。
φ= arccos(xAxis•正規化(ポイント))
ここで:
•-ベクトルのスカラー積。
xAxisは、x軸(1; 0)に対応する単位ベクトルです。 0度は、円の中心から右のポイントに対応します。
point-光源の中心からエッジに属するポイントに向けられたベクトル(光源の座標系におけるエッジのポイントの座標);
normalize-ベクトルの正規化。
r = |ポイント| -ポイントまでの距離;
エッジと中間の2つの極端なポイントを投影します。 再カウントに必要なポイントの数は、エッジの投影によってカバーされるバッファーセルの数に対応します。
角度φに対応するバッファーインデックスの計算:
インデックス=φ/(2 *π)*バッファサイズ。
したがって、エッジの極値に対応するバッファーの2つの極値インデックスが見つかります。 各中間インデックスについて、角度値に変換します。
φ=インデックス* 2 *π/バッファサイズ
、光源の半径以上の長さで、この角度で(0; 0)からベクトルセグメントを構築します。
v = vec2(cos(φ)、sin(φ))*半径
そして、例えば次のように、取得したセグメントとエッジの交点を見つけます:
- 係数A 1 、B 1 、C 1およびA 2 、B 2 、C 2の2行
- Cramerメソッドを使用してこの連立方程式を解くと、交点が得られます。
- 分母がゼロの場合(この場合、分母の値が浮動小数点のためエラーの絶対値よりも小さい場合)、解はありません-線は一致するか平行です。
- 両方のセグメント内の結果のポイントの位置を確認してください。
最後のステップは、取得したすべての中間点を極座標に変換することです。 ポイントまでの距離が現在のインデックスのバッファの値より小さい場合、バッファに書き込みます。 これで、バッファを使用する準備が整いました。 これで、原則として、すべての数学が終了します。
- 係数A 1 、B 1 、C 1およびA 2 、B 2 、C 2の2行
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 ); ... }
ゼロインデックスを複製してチェーンを閉じます。 すべてのポイントの色は、明るさの
この段階で、取得したポイントを特定の値だけ強制的に遅らせて、光線が当たる表面を照らし、ボリュームの外観を作成することもできます。
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.参照
- 結果のビデオデモ 。
- 深度バッファのソースコード(github) ;
- 非ゲームデモのソースコード(github)(ゲームの現時点では、特別に調整されたよりドープされたバージョンを使用しています);
- Win32ビルドミニデモ ;
- OpenGL 3.3仕様 。
- GLSL 3.3仕様 。