2Dマジックの詳細。 パート4。 水



-プロジェクトのために水を洗いました。

-ああ、かっこいい! なぜフラットなのですか? 波をあげて!

...

-聞いて、それからあなたは波について話しました、覚えていますか? それをチェックしてください!

「はい、良い波ですが、まだ屈折とコースティクスを行っていますか?」

...

-こんにちは、私はUnityで一晩中遊んでいます。反射とコード化されたコースティクスを見てください!

-ダロバ、本当にいい! そして、あなたの水が沸騰しているとき、反射はバグではありませんか?

...

-ハイ、ついに沸騰を実現しました。

-そうだね! ほら、沸騰する波を凍らせたらどれだけクールなの?

...

-写真をキャッチして、氷が何かを思いついたようでしたか?

-普通に聞いてください。氷が凍っていますが、体積が増えていますか? ところで、いつからゲームプレイを始めますか?

友人とのログのテーマのバリエーション。

はい、あなたはすでに理解しました、最後に私はプロジェクトでの水の実施についてお話します。 始めましょうか?







前の記事



パート1 光。

パート2 構造。

パート3。 グローバルなカバレッジ。

パート4。







目次



  1. 3Dのぞき見
  2. ウィッシュリスト
  3. 最初のパンケーキはゴツゴツしている
  4. 静力学
  5. 水生成
  6. ダイナミクス
  7. 温度
  8. 結論と陰謀


3Dのぞき見



まず、「大人の男」があらゆる種類の大規模な3Dプロジェクトから水を作る方法を見てみましょう。

一般に、3Dからアイデアをドラッグすることは優れた計画であり、2Dクラスアルゴリズムでは大幅に少なくなります。







したがって、最も単純なものからより複雑なものへ:









ウィッシュリスト



大規模なプロジェクトからの参照ができたので、プロジェクトに何を実装したいかを夢見ます。







記事の冒頭のユーモラスな紹介を覚えていますか? これは冗談ではありません-別のデモからスクリーンショットやビデオを友人に投げるたびに、彼は完全にクレイジーなアイデアを提供してくれました。 また、これらのアイデアのほとんどが実装され、開発の観点からプロジェクトがさらに面白くなったため、これは素晴らしいことです。 したがって、以下のリストは、メールストーンと計画なしで繰り返しコンパイルされました。

そして、これがウィッシュリストのリストです。







  1. 新しいボリュームの水を追加したり、既存のボリュームを「蒸発」させるリアルタイムの機能。
  2. 水面の波。
  3. 温度、凍結および沸騰水の可能性。
  4. 他のモジュールとの相互作用:風、身体、天気。
  5. 照明システムとの密接な相互作用:腐食性、光散乱、反射。
  6. エディターでカスタマイズする機能。


そして、モジュールとの相互作用について-トピックはほぼ無限です。 自分の目で確かめてください-光に関する記事で、私はプロジェクトに神の光線を含める機会があると言いました-空気中の塵のために目に見える光線。 そして今、私が水を沸騰し終わったとき、これらの神の光線が見える蒸気粒子を生成することを妨げるものは何もありません! しかし、風は粒子と相互作用する可能性があります。つまり、お湯の上にある蒸気が美しく消散します。 そして、そのような相互作用が非常に多いため、プロトタイプを作成するのではなく、記録する時間がありません。


最初のパンケーキはゴツゴツしている



Hydrophobiaが液体を「正直に」シミュレートするようにしたチームとは一緒に行きません。この方法はリソースを必要とします。 簡素化する必要があります。 pixelartはプロジェクト内にあるため、すべての平面は水平または垂直です。 また、レベルの空きスペースは長方形のセットとして表現できます。 ここで彼らと協力します。

レベルの最初の前処理で:







  1. レベル上のすべての空きスペースを長方形のばらばらのボリュームに分割します。
  2. ボリュームの相互接続を見つけ、水の「流れ」のグラフを作成します。


リアルタイムで:







  1. 各ボリュームについて、表面の波を個別に計算します(波の助けを借りて水がボリュームからボリュームへ流れることができるという事実を無視します)。
  2. グラフを使用して水の流れを計算します。




水柱







しかし、実を言うと、何も起こりませんでした。 落とし穴が非常に多いので、5階建ての建物をいくつか建てることができます。 たとえば、波を同期したり、通信する船舶の圧力に対処したりする必要があります。 しかし、銀色の裏地があります-リージョンツリーの実装は、このメソッドの実装のために、かつてプロジェクトのどこででも使用されていました。







静力学



完全に動的な水ではうまくいかなかったので、私たちの生活を簡素化しましょう-エディターで水を調整し、ダイナミクスの幻想のために波を残します。 実際、オプションのリストからオプション3に進みましょう。







水は流れる方法を知っているため、非常に物議を醸すものです。「これらのポイントに水があるだろう」と言うことはできず、そこに液体メッシュを置き、良い写真を撮ります。 さて、レベルデザイナー(将来的には)が水が確実にあるべき重要なポイントをいくつか配置すると、エンジンはその予備から正確にキーポイントのレベルまで水を注ぎます。 もちろん、1つの「アンカー」を別の「アンカー」の下に置くことができますが、これはエンジンの問題ではなく、私たちにとって未知のレベル設計者の問題です。







別の機能は、「バブル」を作成するための個別のアンカーです。 アルゴリズムがそのようなアンカーを見つけると、それは水で満たさないようにし、きちんとした空洞に空気を残します。







どんな結果が得られるのか理解できたので、今度はアルゴリズムを開発します。







水生成



一般に、水はレベル全体を埋めることができます。 壁は長方形の破片で構成され、どこにでも配置できるため、水量は穴のない非凸面の長方形ポリゴンです。 なんてこった。 もっとシンプルな形に分けましょう。







明確にするための小さなスポイラー。 前処理の結果として、波を簡単に計算できる水量を取得する必要があります。 実際、1つの水量は1ピクセルの厚さの水柱のセットです。

液体が存在する可能性のある空きスペースを扱います。 過去の記事で話した壮大な地域ツリーは、多くの助けになります。 壁で占められていないすべての長方形のボリュームを取得します。







  1. ツリーの一番下(最初は左)のコーナーから始めて、上から最初の空の領域を探します。
  2. 見つかった空の領域から始めて、上から最初の空でない領域を探します。
  3. 結果のセグメントをリストに追加します。
  4. ツリーの最上部(幾何学的な意味)の境界に到達したら、1ピクセル右に移動します。
  5. ツリーの右側の境界線に到達していない場合は、ポイント1に進みます。
  6. セグメントのリストから長方形を収集します(そして、それらはかなり正常にソートされます)。
  7. 隣接する長方形間の接続を構築します。




千の言葉の代わりに







ほとんどの場合、これらの長方形をより美しい方法で取得できます。 このトピックについてコメントさせていただきます。

実際、長方形は最良の選択ではありません。 パーツの数が多いため(タワーの歯など)、長方形の水量が多くなります。 そして、これはパフォーマンスを低下させます。 穏やかな水は平らであり、底部の形状は私たちにとって重要ではないため、次のように隣接する長方形を組み合わせます。







A ( B), B - ( A) - .









この段階の後、リブ付きの底のある少量の水が残ります。下の図では、はっきりと見えています。









マップ上の空きスペースのマークアップ(リンク付き)







次に、レベルマップ上で設計者が残したすべてのマークを見つけ、レベルごとにワークピースを「トリミング」する必要があります。 このようなアルゴリズムはこれを処理します:







  1. すべての水位マーク( )、値の降順でy座標で並べ替えます。
  2. 各ラベルについて:

    2.1。 ラベルが属する長方形を見つけます。

    2.2。 長方形が見つからない場合、ラベルは壁にあり、無視してptに進みます。 2。

    2.2。 長方形の高さをラベルのレベルまで下げます。

    2.3。 長方形の高さがゼロになった場合-長方形を削除します。

    2.4。 下端がマークの上にあるすべての隣接する長方形を見つけて削除します。

    2.5。 他のすべての隣接する長方形を見つけ、ptに進みます。 2.3(水位に従ってすべての長方形を再帰的に切り取ります)。
  3. 気泡のすべてのラベルを収集します( )、値の降順でy座標で並べ替えます。
  4. 各ラベルについて:

    4.1。 ラベルが属する長方形を見つけます。

    4.2。 長方形が見つからない場合、ラベルは壁にあり、無視してptに進みます。 4。

    4.2。 長方形の高さをラベルのレベルまで下げます。

    4.3。 長方形の高さがゼロになる場合-長方形を削除します。

    4.4。 下端がマークの上にあるすべての隣接する長方形を見つけて削除します。

    4.5。 ラベルのレベルの上に上端がある他のすべての隣接する長方形を見つけ、n 4.3に進みます(気泡のレベルに応じてすべての長方形を再帰的にカットします)。


ご覧のとおり、ウォーターマークと気泡マークの違いは、前者がすべての隣人を再帰的に通過し、後者が隣人が完全にバブルレベルを下回ると停止することです。







そして今、写真で:









マップに水位マーカーを追加しました









リンクと同じこと









水位でクリップされた長方形









余分なリンクと長方形を削除しました









受け取った水の量を格安で視覚化







小さいが重要なメモ

マークのレベルに応じて長方形を切り取るという事実にもかかわらず、それぞれの長方形で元の高さの配列(水面上の天井のy座標)を保持します。 将来的には、波を制限するためにこれらの高さが必要になります-さもなければ、波は壁を通過できます。







エディタでどのように見えるかを見てみましょう。









ダイナミクス



現在のリアルタイムの水を放棄しているので、少なくとも美しい波を作りましょう。 水を生成すると、次のような水管理者が得られます。







 namespace NewEngine.Core.Water { public class WaterManager : MonoBehaviour { //     // ... WaterPolygon[] waters; CombineInstance[] combineInstances = null; Mesh mesh; public void Generate() { if (tree == null) return; waters = WaterGenerator.Generate(tree, waveCeil); Debug.Log("Regenerate water"); } void FixedUpdate() { if (waters == null) return; var viewportRect = cameraManager.ViewRect; WaterPolygon.Update(waters, ref combineInstances, viewportRect, /*   ,      ,    */); mesh.Clear(); mesh.CombineMeshes(combineInstances); } } }
      
      





これまでのところ、このすべてのコードの中で、私たち( またはストーリーテラーとしての私 )はWaterPolygonにのみ興味があります。 これらは、あなたが波を作ることができる非常に最小限の水です。 また、これらの要素は相互に接続されています(グラフに関する情報は、これらのポリゴンで引き続き利用可能です)。 重要でない詳細を見逃した場合、このクラスは次のようになります。







途方もないコード
 namespace NewEngine.Core.Water { public class WaterPolygon { //  "" -       //    (   y, ),   (,  ) class Line { public int min; // y-    public int max; // y-    public int target; // y-   ,     public int height; // y-     (,    MAX_WAVE_HEIGHT,  ) public float speed; float lastDelta; public void Add(float additionalWater); public void Sleep(); public void Update(float tension, float dampening, float foamThreshold, float foamForce, float foamDampening, float airTemperature, float airTranscalency, float verticalTranscalency, float minBoilTemperature, float minBoilBubble, float maxBoilBubble, float boilFrequency); } //   y   Line,    ,         x int x; //   ;      WaterPolygon[] left; WaterPolygon[] right; //   "" Line[] lines; //      bool sleeping; int sleepingFrames; //      AABB int minWaterY; float maxWaterY; //    UnityEngine Mesh mesh; //     ,      ,  :) public static void Update(WaterPolygon[] water, ref CombineInstance[] combineInstances, Geom.IntRect viewportRect, int outsideCameraSleepOffset, float heightSleepThreshold, float speedSleepThreshold, float tension, float dampening, float spread, int steps, float foamThreshold, float foamForce, float foamDampening, float airTemperature, float airTranscalency, float verticalTranscalency, float horisontalTranscalency, float minBoilTemperature, float minBoilBubble, float maxBoilBubble, float boilFrequency, int sleepingUpdateFrames); //  AABB    offset public bool IsOutside(Geom.IntRect viewportRect, int offset); //    - ,        lines void UpdateFirst(Geom.IntRect viewportRect, int outsideCameraSleepOffset, float tension, float dampening, int steps, float foamThreshold, float foamForce, float foamDampening, float airTemperature, float airTranscalency, float verticalTranscalency, float minBoilTemperature, float minBoilBubble, float maxBoilBubble, float boilFrequency, int sleepingUpdateFrames); //    -     ,       (left  right) void UpdateSecond(float heightSleepThreshold, float speedSleepThreshold, float spread, int steps, float foamThreshold, float foamForce, float foamDampening, float horisontalTranscalency, float airTemperature, float minBoilTemperature); //        void Sleep(bool withClear); //      (    ),    void FindLine(bool isRight, out Line line, out WaterPolygon water); //       public void CreateMesh(ref CombineInstance combineInstance); } }
      
      



、フロート緊張、フロート湿し、int型の段階、フロートfoamThreshold、フロートfoamForce、フロートfoamDampening、フロート空気温度、フロートairTranscalency、フロートverticalTranscalency、フロートminBoilTemperature、フロートminBoilBubble、フロートmaxBoilBubble、フロートboilFrequency、 namespace NewEngine.Core.Water { public class WaterPolygon { // "" - // ( y, ), (, ) class Line { public int min; // y- public int max; // y- public int target; // y- , public int height; // y- (, MAX_WAVE_HEIGHT, ) public float speed; float lastDelta; public void Add(float additionalWater); public void Sleep(); public void Update(float tension, float dampening, float foamThreshold, float foamForce, float foamDampening, float airTemperature, float airTranscalency, float verticalTranscalency, float minBoilTemperature, float minBoilBubble, float maxBoilBubble, float boilFrequency); } // y Line, , x int x; // ; WaterPolygon[] left; WaterPolygon[] right; // "" Line[] lines; // bool sleeping; int sleepingFrames; // AABB int minWaterY; float maxWaterY; // UnityEngine Mesh mesh; // , , :) public static void Update(WaterPolygon[] water, ref CombineInstance[] combineInstances, Geom.IntRect viewportRect, int outsideCameraSleepOffset, float heightSleepThreshold, float speedSleepThreshold, float tension, float dampening, float spread, int steps, float foamThreshold, float foamForce, float foamDampening, float airTemperature, float airTranscalency, float verticalTranscalency, float horisontalTranscalency, float minBoilTemperature, float minBoilBubble, float maxBoilBubble, float boilFrequency, int sleepingUpdateFrames); // AABB offset public bool IsOutside(Geom.IntRect viewportRect, int offset); // - , lines void UpdateFirst(Geom.IntRect viewportRect, int outsideCameraSleepOffset, float tension, float dampening, int steps, float foamThreshold, float foamForce, float foamDampening, float airTemperature, float airTranscalency, float verticalTranscalency, float minBoilTemperature, float minBoilBubble, float maxBoilBubble, float boilFrequency, int sleepingUpdateFrames); // - , (left right) void UpdateSecond(float heightSleepThreshold, float speedSleepThreshold, float spread, int steps, float foamThreshold, float foamForce, float foamDampening, float horisontalTranscalency, float airTemperature, float minBoilTemperature); // void Sleep(bool withClear); // ( ), void FindLine(bool isRight, out Line line, out WaterPolygon water); // public void CreateMesh(ref CombineInstance combineInstance); } }





1つの優れたチュートリアルで波を作ったので、流体物理学については説明しません。 簡単に言うと、現在の高さ、速度、最適な高さがバネでわかっている接続されたバネの形で水を表します。 ばねは振動し、振動を隣人に伝えます。 デザインのスプリングの幅は1ピクセルです。

ただし、重要な追加事項が1つあります。 チュートリアルには「水トレーニングの場」が1つしかありませんが、グラフ全体があります。 そして、波を正しく同期する必要があります。 例はより明確です:









このレベルがあるとしましょう









3つの水域で構成されます









ある時点で、青いポリゴンに波が現れます









アルゴリズムは、左の列の高さに基づいて適切な近傍を見つけ、波を同期します









非常に大きな波では、アルゴリズムは別のネイバーを選択します







最適化の数分。 次の波を計算することは意味がありません。







  1. 画面外のポリゴン。
  2. 波のないポリゴン。


次の波の計算では、すべての波がしきい値よりも小さいかどうかをチェックし、そうであれば、就寝時間を確認します! ポリゴンが画面から完全に外れている場合、しきい値はわずかに高くなります。 したがって、接続されたポリゴンまたは他のモジュール(物理学、風など)の影響により、水を起こすことができます。







これまでのところ、水にheする理由はありません。 彼女の平和、戦争魔術師の戦い、平凡なハリケーンを妨げるものは何もありません。 Morraの中心としての寒さ。 氷を溶かす時間。







温度



不必要なささいなことをややman病な研究にもかかわらず、私は現実の熱力学にさらに6か月の開発を費やしたくはありません。 したがって、完全に単純化します。 そしてもう少し。







ウェザーマネージャーから始めましょう。 いつの日か嵐と穏やかさがそこにありますが、今はこれだけです:







 namespace NewEngine.Core.Weather { public class WeatherManager : MonoBehaviour { [SerializeField, Range(-100, 200)] float airTemperature; public float AirTemperature { get { return airTemperature; } } } }
      
      





コンセプトは次のとおりです。







  1. 水温は最初は気温と同じです。
  2. 0°C未満の温度では、水が凍結します。
  3. 100°Cの温度では、水が沸騰します。
  4. 水の体積は互いに温度を交換します。
  5. 水の体積は、大気の体積(幾何学的な意味で)を考慮して、大気と温度を交換します。
  6. 最適化と最適化解除:

    6.1沸騰した水は決して眠りません。

    6.2。 水が「目覚める」と、睡眠中の液体の温度変化の簡単な計算が行われます。


WaterPoligon.Lineの温度は、 水柱の上端と下端にのみ保存されます。







 class Line { public int min; public int max; ... public float minTemperature; public float maxTemperature; ... }
      
      





非常に不正確な方法-結局のところ、すべての投稿の高さが異なりますが、それは私たちに合っています。







最初は、列をいくつかの断片に分割し、これらの断片間の熱伝達を計算するというアイデアがありました(1つの列と隣接する列の両方)。 この場合、現在の実装では不可能な「層状の水」(氷/水/氷)を作成することができます。

温度伝達は、3つの異なる「方向」で実行されます。 最初の明白なことは、水柱の端の間です。







 if (length > 0) { ... float avgTemperature = (maxTemperature + minTemperature) * 0.5f; float ratioTranscalency = verticalTranscalency / length; maxTemperature = maxTemperature + (avgTemperature - maxTemperature) * ratioTranscalency; minTemperature = minTemperature + (avgTemperature - minTemperature) * ratioTranscalency; }
      
      





二番目はもっと面白いです。 空気とカラムの上端の間の熱伝達。 水を生成するとき、実際には、天井の座標である高さを取得しました。 したがって、いつでも、水面上の空隙の高さを取得できます。 水面上の空気が多いほど、熱伝達が速くなります。 物議を醸す声明。 しかし、戸外では、水は低い洞窟よりも速く冷却/暖まります:







 float length = height - min; if (length > 0) { if (height < max) { float airVolume = Mathf.Min(max - height, MAX_AIR_VOLUME); maxTemperature = maxTemperature + (airTemperature - maxTemperature) * Mathf.Clamp01(airTranscalency * airVolume); } ... }
      
      





隣接する列間の熱伝達を維持します。 温度情報は柱の端にのみ保存されるため、線形補間がシーンに入ります。









写真好きの方へ







コード愛好家向け
 static void UpdateTemperatureDelta(float horisontalTranscalency, float[] minTemperatureDelta, float[] maxTemperatureDelta, int i, Line line, Line other) { if (line.height <= line.min || other.height <= other.min) { minTemperatureDelta[i] = 0; maxTemperatureDelta[i] = 0; return; } float height = line.height - line.min; float otherHeight = other.height - other.min; if (Mathf.Max(line.height, other.height) - Mathf.Min(line.min, other.min) >= height + otherHeight) { minTemperatureDelta[i] = 0; maxTemperatureDelta[i] = 0; return; } float minY = Mathf.Max(line.min, other.min); float maxY = Mathf.Min(line.height, other.height); float minT = (minY - line.min) / height; float maxT = (maxY - line.min) / height; float otherMinT = (minY - other.min) / otherHeight; float otherMaxT = (maxY - other.min) / otherHeight; float minTemperature = Mathf.Lerp(line.minTemperature, line.maxTemperature, minT); float maxTemperature = Mathf.Lerp(line.minTemperature, line.maxTemperature, maxT); float otherMinTemperature = Mathf.Lerp(other.minTemperature, other.maxTemperature, otherMinT); float otherMaxTemperature = Mathf.Lerp(other.minTemperature, other.maxTemperature, otherMaxT); float ratio = horisontalTranscalency * Mathf.Clamp01(height / otherHeight); minTemperatureDelta[i] = ratio * (minTemperature - otherMinTemperature); maxTemperatureDelta[i] = ratio * (maxTemperature - otherMaxTemperature); }
      
      





温度の交換の構造は波の交換に似ていますが、波は各側の隣接する1つのWaterPolygonにのみ送信され、隣接するそれぞれに熱が送信されます。







そして最後の仕上げは、水を沸騰させて凍らせることです。 温度が100°C以上の場合-ランダムなスプリング速度を液体カラムに追加し、温度が高いほど速度分散が大きくなります(実際、100°Cではなく、水が少し早く泡立ち始めます)。







さて、温度が0°C以下の場合、スプリング速度をゼロに保ち、隣接するスプリングコラムからの速度の伝達への応答を停止します。







または言い換えると:







 if (height < max) { if (maxTemperature >= minBoilTemperature) { float depth = target - min; float ratio = Mathf.Clamp01((maxTemperature - minBoilTemperature) / (100 - minBoilTemperature)); float heightValue = Mathf.Min(depth * 2, Mathf.Max(minBoilBubble, ratio * maxBoilBubble)); float frequency = Mathf.Lerp(0, boilFrequency, ratio * ratio); if (Random.value > 1 - frequency) height += Random.Range(-heightValue, heightValue); } } if (maxTemperature <= 0) { speed = 0; return; }
      
      







徐々に沸騰する水







結論と陰謀



上記のすべての後、水がプロジェクトに現れ、エディターのレベルに「注ぐ」ことができます。 寒い生命を与える湿気で刺激的、沸騰、硬化!







たくさんのコードを書いてい画像を取得するのは残念です。 反射、屈折、コースティクス-私たちのすべて! そのため、ここでも、太いシェーダー、照明システムとの相互作用、テクスチャへのレンダリングなどすべてが行われます。







しかし、それについては次の記事で詳しく説明します。 :)








All Articles