Phaserの別の影の征服者、または自転車の使用

2年前、私は既にPhaser 2Dで影の物質を試していました。 最後のLudum Dareで、私たちは突然ホラーを作ることにしましたが、影と光のないホラーです! 指の関節を割って......



...そして、LDに間に合うようにいまいましいものではない もちろん、ゲームには光と影が少しありますが、これは本来あるべきものの悲惨な見た目です。



コンテストにゲームを送った後、家に戻って、私は「ゲシュタルトを閉じて」、これらの不幸な影を終えることに決めました。 何が起こった-あなたはゲーム感じ 、デモ遊んで、写真を見て、記事を読むことができます。







このような場合は常にそうですが、一般的なソリューションを作成しようとしても意味がありません。特定の状況に集中する必要があります。 ゲームの世界はセグメントの形で表すことができます-少なくとも影を落とすエンティティ。 壁は長方形、人は長方形、回転のみ、地獄のスポイラーは円形ですが、カットオフモデルでは、光線に常に垂直な直径の長さに簡略化できます。







いくつかの光源(20〜30)があり、それらはすべて円形(スポットライト)であり、照明されたオブジェクトよりも条件的に低い位置にあります(したがって、影は無限になります)。



私は頭の中で問題を解決するために次の方法を見ました:



  1. 各光源について、画面サイズのテクスチャを作成します(2〜4倍小さい)。 このテクスチャでは、台形BCC'D 'を単純に描画します。ここで、Aは光源、BCはセグメント、B'C'はテクスチャのエッジへのセグメントの投影です。 その後、これらのテクスチャはシェーダーに送られ、そこで単一の画像にミックスされます。



    Celesteプラットフォーマーの作成者は次のようなことをしました。これは、メディアに関する記事でよく書かれています: medium.com/@NoelFB/remaking-celestes-lighting-3478d6f10bf



    問題:20〜30画面サイズのテクスチャで、ほぼすべてのフレームを再描画してGPUにロードする必要があります。 これは非常に高速なプロセスであったことを覚えています。







  2. habr-habr.com/post/272233の投稿で説明されている方法。 各光源について、「深度マップ」、つまり このようなテクスチャ。x=光源からの「ビーム」の角度、y =光源の番号、および色==光源から最も近い障害物までの距離。 0.7度(360/512)のステップと32の光源を使用すると、512 x 32のテクスチャが1つ得られますが、これはそれほど長く更新されていません。





    (45度のステップのテクスチャの例)

  3. 最後に説明する秘密の方法


最後に、方法2に決めました。しかし、記事に記載されていることは、最後まで私には向いていませんでした。 そこでは、シェークの中でテクスチャもレーキキャストを使用して構築されました。サイクル内のシェーダは光源からビームの方向に進み、障害物を探しました。 過去の実験では、シェーダーでrakecastも作成しましたが、普遍的ではありますが非常に高価でした。



「モデルにはセグメントのみがあります」と私は考えました。「10〜20のセグメントは、あらゆる光源の半径に該当します。 これに基づいて距離マップをすばやく計算できませんか?”



だから私はそうすることにしました。



まず、画面に壁、条件付きの「主人公」、光源を表示しました。 光源の周りには、暗闇の中ではっきりとした透明な光の輪が刻まれていました。 これを取得するには:







デモ



私はすぐにリラックスしないようにシェーダーを使い始めました。 光源ごとに座標と動作半径(光が届かない範囲)を渡す必要がありましたが、これは単純に均一な配列で行われます。 そして、シェーダー(画面上の各ピクセルに対して実行される断片的)では、現在のピクセルが照らされた円内にあるかどうかを理解するために残りました。

class SimpleLightShader extends Phaser.Filter { constructor(game) { super(game); let lightsArray = new Array(MAX_LIGHTS*4); lightsArray.fill(0, 0, lightsArray.length); this.uniforms.lightsCount = {type: '1i', value: 0}; this.uniforms.lights = {type: '4fv', value: lightsArray}; this.fragmentSrc = ` precision highp float; uniform int lightsCount; uniform vec4 lights[${MAX_LIGHTS}]; void main() { float lightness = 0.; for (int i = 0; i < ${MAX_LIGHTS}; i++) { if (i >= lightsCount) break; vec4 light = lights[i]; lightness += step(length(light.xy - gl_FragCoord.xy), light.z); } lightness = clamp(0., 1., lightness); gl_FragColor = mix(vec4(0,0,0,0.5), vec4(0,0,0,0), lightness); } `; } updateLights(lightSources) { this.uniforms.lightsCount.value = lightSources.length; let i = 0; let array = this.uniforms.lights.value; for (let light of lightSources) { array[i++] = light.x; array[i++] = game.world.height - light.y; array[i++] = light.radius; i++; } } }
      
      





次に、光源ごとにどのセグメントが影を落とすかを理解する必要があります。 むしろ、セグメントのどの部分-下の図では、セグメントの「赤い」部分には関心がありません。 光はまだ届きません。



注:交差点の定義は、予備的な最適化の一種です。 さらなる処理の時間を短縮し、光源の半径を超える大きなセグメントを排除するために必要です。 これは、長さが「グロー」の半径よりはるかに大きいセグメントが多数ある場合に意味があります。 これが当てはまらず、多くの短いセグメントがある場合、交差点を決定してセグメント全体を処理する時間を無駄にしないことが正しいことが判明する可能性があります。 時間の節約はまだ機能しません。







これを行うために、直線と円の交点を見つけるためのよく知られた公式を使用しました。誰もが誰かの想像上の世界の幾何学の学校のコースから心から覚えています。 彼女のことを思い出せなかったので、 グーグルで検索しなければなりませんでし



エンコードして、何が起こったのか見てみましょう。





デモ

それが当たり前のようです。 これで、どのセグメントがシャドウを投影でき、rakecastを実行できるかがわかりました。



ここにもオプションがあります:



  1. 円の中を一周し、光線を投げて交差点を探すだけです。 最も近い交差点までの距離は、必要な値です
  2. セグメントに分類されるコーナーにのみ移動できます。 結局のところ、ポイントはすでにわかっているので、角度を計算することは難しくありません。
  3. さらに、セグメントに沿って進む場合、光線を投じて交差点を計算する必要はありません。必要なステップでセグメントに沿って移動できます。 仕組みは次のとおりです。








ここに AB -セグメント(壁)、 C 光源の中心です Cd -セグメントに垂直。



させる x -法線に対する角度。ソースからセグメントまでの距離を知る必要があるため、 X1 -セグメント上のポイント AB ビームが落ちる場所。 トライアングル CDX1 -長方形、 Cd -脚、およびその長さは既知であり、このセグメントでは一定です。 CX1 -希望の長さ。 CX1= fracCDcosx 。 事前にステップを知っている(そしてそれを知っている)場合は、逆余弦のテーブルを事前に計算して、非常に迅速に距離を探すことができます。



そのようなテーブルのコード例を示します。 コーナーを扱うほとんどすべての作業は、インデックスを使用する作業に置き換えられます。 0〜Nの整数。N=円のステップ数(つまり、ステップ角=  frac2 piN



 class HypTable { constructor(steps = 512, stepAngle = 2*Math.PI/steps) { this.perAngleStep = [1]; for (let i = 1; i < steps/4; i++) { //   pi/2 let ang = i*stepAngle; this.perAngleStep[i] = 1/Math.cos(ang); } this.stepAngle = stepAngle; } /** * @param distancesMap -  ,    * @param angle1 -           * @param angle2 -           * @param normalFromLight - ,      */ fillDistancesForArc(distancesMap, angle1, angle2, normalFromLight) { const D = Math.hypot(normalFromLight.x, normalFromLight.y); const normalAngle = Phaser.Math.normalizeAngle(Math.atan2(normalFromLight.y, normalFromLight.x)); const normalAngleIndex = (normalAngle / this.stepAngle)|0; const index1 = (angle1 / this.stepAngle)|0; const index2 = (angle2 / this.stepAngle)|0; for (let angleIndex = index1; angleIndex <= index2; angleIndex++) { let distanceForAngle = D * this.perAngleStep[normalize(angleIndex - normalAngleIndex)]; distancesMap.set(angleIndex, distanceForAngle); } } }
      
      





もちろん、この方法では、初期角度ACDがステップの倍数でない場合にエラーが発生します。 しかし、512ステップについては、視覚的に違いはありません。



だから私たちはすでに方法を知っています:

  1. 影を落とすことができる光源の範囲内のセグメントを見つける

  2. ステップtについては、dist(角度)テーブルを作成し、各セグメントを通過して距離を計算します。




光線で描くと、この表は次のようになります。







デモ



テクスチャで記述されている場合、10個の光源をどのように表示するかを以下に示します。







ここで、各水平ピクセルは角度に対応し、色はピクセル単位の距離に対応します。

これはすべて、 imageDataを使用してこのようにjsで記述されます

  fillBitmap(data, index) { let total = index + this.steps*4; let d1, d2; let i = 0; //data[index] = Red //data[index+1] = Green //data[index+2] = Blue //data[index+3] = Alpha for (; index < total; index+=4, i++) { //  512,    R     2. d1 = (this.distances[i]/2)|0; data[index] = d1; d1 = this.distances[i] - d1*2; d2 = (d1*128)|0; //   G -     2. data[index+1] = d2; //  B  A  255,     . data[index+2] = 255; data[index+3] = 255; } }
      
      







次に、テクスチャをシェーダーに渡します。シェーダーには、光源の座標と半径が既にあります。 そして、次のように処理します:



 //      uniform sampler2D iChannel0; #define STRENGTH 0.3 #define MAX_DARK 0.7 #define M_PI 3.141592653589793 #define M_PI2 6.283185307179586 //       float decodeDist(vec4 color) { return color.r*255.*2. + color.g*2.; } float getShadow(int i, float angle, float distance) { //   x   ==  float u = angle/M_PI2; //   y   ==     float v = float(i)/${MAX_LIGHTS}.; float shadowAfterDistance = decodeDist(texture2D(iChannel0, vec2(u, v))); //  1   ,  0  . return step(shadowAfterDistance, distance); } void main() { float lightness = 0.; for (int i = 0; i < ${MAX_LIGHTS}; i++) { if (i >= lightsCount) break; vec4 light = lights[i]; //       vec2 light2point = gl_FragCoord.xy - light.xy; float radius = light.z; float distance = length(light2point); float inLight = step(distance, radius); //      ,       //  . //      , //    ,          //           //     ,    if (inLight == 0.) continue; float angle = mod(-atan(light2point.y, light2point.x), M_PI2); // 1     0   float thisLightness = (1. - getShadow(i, angle, distance)); //,   “”  ,   ,  //    lightness += thisLightness*STRENGTH; } lightness = clamp(0., 1., lightness); gl_FragColor = mix(vec4(0,0,0,MAX_DARK), vec4(0,0,0,0), lightness); }
      
      







結果:





デモ

今、あなたは少しの美しさをもたらすことができます。 光が距離とともにフェードすると、影がぼやけます。







ぼかしについては、次のように、隣接するコーナー、+-ステップを見てください。



 thisLightness = (1. - getShadow(i, angle, distance)) * 0.4 + (1. - getShadow(i, angle-SMOOTH_STEP, distance)) * 0.2 + (1. - getShadow(i, angle+SMOOTH_STEP, distance)) * 0.2 + (1. - getShadow(i, angle-SMOOTH_STEP*2., distance)) * 0.1 + (1. - getShadow(i, angle+SMOOTH_STEP*2., distance)) * 0.1;
      
      







すべてをまとめてFPSを測定すると、次のようになります。







この結果は私にぴったりでした。 あなたはまだ照明の色で遊ぶことができましたが、私はしませんでした。 少しひねり、法線マップを追加して、NOPEの更新バージョンを投稿しました。 彼女は今このように見えた:









それから彼は記事を準備し始めました。 私はそのようなgifを見て考えました。







「だから、ウルフェンシュタインのように、ほぼ3Dの見た目です」と私は叫んだ(そう、想像力はある)。 実際、すべての壁が同じ高さであると仮定すると、距離マップはシーンを構築するのに十分です。 試してみませんか?



シーンは次のようになります。









だから私たちのタスク:



  1. 画面上のポイントで、壁がない場合のケースのワールド座標を取得します。



    これを検討します。

    • 最初に、画面上の点の座標を正規化して、画面の中心と、それぞれ(-1、-1)と(1,1)の角に点(0,0)があるようにします。





    • x座標は、ビューの方向からの角度になります。Aを2倍するだけです。Aは表示角度です。

    • y座標は、観測者からポイントまでの距離を決定します。一般的な場合は、d〜1 / yです。 画面の下端にあるポイントの場合、距離= 1、画面の中央にあるポイントの場合、距離=無限。





    • したがって、壁を考慮しない場合、世界の各表示ポイントに対して、画面上に2つのポイントがあります-1つは中央(「天井」)の上に、もう1つは下(「床」)にあります

  2. これで距離の表を見ることができます。 ポイントよりも近い壁がある場合は、壁を描く必要があります。 そうでない場合は、床または天井を意味します



注文どおりに取得します。





デモ

照明を追加します-同様に、光源を反復処理し、世界座標を確認します。 そして-最後の仕上げ-テクスチャを追加します。 これを行うには、距離のあるテクスチャで、この時点で壁テクスチャのオフセットuを書き込む必要があります。 ここで、チャンネルbが役に立ちました。





デモ

パーフェクト。



冗談です。



もちろん不完全です。 しかし、地獄、私はまだ約15年前にrakecastで私のWolfensteinを作成する方法について読みました、そして、私はそれをすべてやりたかったです、そして、ここにそのような機会があります!



結論の代わりに



記事の冒頭で、別の秘密の方法に言及しました。 ここにあります:



すでに方法を知っているエンジンを使用してください。


実際、ゲームを作成する必要がある場合、これが最も正確で最速の方法になります。 自転車をフェンスで囲み、長年の問題を解決する必要があるのはなぜですか?



しかし、なぜ。



10年生の時、私は別の学校に移り、数学の問題に遭遇しました。 正確な例を覚えていませんが、それは度の方程式であり、あらゆる点で単純化する必要がありましたが、成功しませんでした。 必死に、姉と相談したところ、彼女は言った:「両側に× 2を加えると、すべてが分解されます。」 それが解決策でした。そこになかったものを追加します。



ずっと後に友人が私の家を建てるのを手伝ったとき、ニッチを埋めるためにしきい値にブロックを置く必要がありました。 そして今、私は立ち上がってバーのスクラップを整理しています。 1つは収まるようですが、完全ではありません。 その他ははるかに小さいです。 私はここで幸福という言葉を集める方法を考えています、そして友人が言います:「彼らはそれが干渉する溝を飲んだ」。 そして今、大きなバーはすでに静止しています。



これらのストーリーは、このような効果によって統合されます。これを「在庫効果」と呼びます。 既存のパーツから決定を下そうとすると、これらのパーツで処理および改良できる素材は表示されません。 数字は木材、お金、またはコードです。



私は仲間のプログラマの間で同じ効果を何度も観察しました。 素材に自信を持たず、非標準的なコントロールなどを行う必要がある場合に、彼らは時々屈服します。 または、単体テストを追加してください。 または、クラスの設計時にすべてを提供しようとすると、次のようなダイアログが表示されます。

-これは今は必要ありません

-必要になったらどうしますか?

-次に追加します。 拡張ポイントはそのままにしておきます。 コードは花崗岩ではなく、粘土です。



そして、私たちが扱う素材を見て、感じることを学ぶためには、自転車も必要です。



これは単なる心のトレーニングやトレーニングではありません。 これは、コードを使用して質的に異なるレベルの作業に到達する方法です。



読んでくれてありがとう。



どこかをクリックするのを忘れた場合のリンク:




All Articles