基于细胞的液体模拟:局部压力模型

本文关键字:模拟 局部 模型 压力 于细胞 细胞 | 更新日期: 2023-09-27 17:57:26

我正试图将半逼真的水添加到我的基于瓷砖的2D平台生成器中。水必须表现得有点逼真,有一个完全局部运行的压力模型。(IE。只能使用附近单元格的数据)需要这个模型是因为我的游戏性质,你不能确定你需要的数据不在内存之外的区域内。

到目前为止,我已经尝试了一种方法,但我无法对其进行足够的细化,以满足我的约束。

对于该模型,每个单元都是略微可压缩的,这取决于上面单元中的水量。当细胞的含水量大于正常容量时,细胞会试图向上膨胀。这创建了一个相当不错的模拟,有时速度很慢(不是滞后;水中的变化需要一段时间才能传播。)。当我试图在我的引擎中实现这一点时,我发现我的局限性缺乏它工作所需的精度。如果你愿意,我可以提供更深入的解释或原始概念的链接。

我的限制:

  • 水位只有256个离散值。(无浮点变量:()--EDIT。漂浮物很好
  • 固定网格大小
  • 仅2D
  • U形弯头配置必须有效

我使用的语言是C#,但我可能会选择其他语言并将其翻译成C#。

问题是,有人能给我一个水的压力模型吗,尽可能地遵循我的约束条件?

基于细胞的液体模拟:局部压力模型

换一种方法怎么样?

忘掉花车吧,从长远来看,这会带来舍入问题。相反,一个单位的水怎么样?

每个细胞都含有一定数量的水。每次迭代,你都会将细胞与它的4个邻居进行比较,并将水的单位数量的差异移动10%(改变这个值以改变传播速度)。映射函数将水的单位转换为水位。

为了避免计算顺序问题,请使用两个值,一个用于旧单位,另一个用于新单位。计算所有内容,然后将更新后的值复制回来。2 int=每个单元8个字节。如果你有一百万个细胞,那仍然只有800亿。

如果你真的试图模拟波浪,你还需要存储流量——4个值,16mb。为了使波浪对水流施加一些惯性——在计算出所需的流量后,将前一个流量向所需值移动10%。

尝试将每个连续的水域视为一个单独的区域(如洪水填充),并跟踪1)水可以逸出的最低单元格和2)水可以从中流出的最高单元格,然后将水从顶部移到底部。这不是局部的,但我认为你可以将你想要影响的区域的边缘视为不连通的,并处理你想要的任何子集。重新评估每个帧上哪些区域是连续的(在每个帧上重新泛洪),以便当斑点收敛时,它们可以开始被视为一个区域。

这是我在WindowsForms演示中的代码。它可能需要一些微调,但在我的测试中似乎运行得很好:

public partial class Form1 : Form
{
  byte[,] tiles;
  const int rows = 50;
  const int cols = 50;
  public Form1()
  {
     SetStyle(ControlStyles.ResizeRedraw, true);
     InitializeComponent();
     tiles = new byte[cols, rows];
     for (int i = 0; i < 10; i++)
     {
        tiles[20, i+20] = 1;
        tiles[23, i+20] = 1;
        tiles[32, i+20] = 1;
        tiles[35, i+20] = 1;
        tiles[i + 23, 30] = 1;
        tiles[i + 23, 32] = 1;
        tiles[21, i + 15] = 2;
        tiles[21, i + 4] = 2;
        if (i % 2 == 0) tiles[22, i] = 2;
     }
     tiles[20, 30] = 1;
     tiles[20, 31] = 1;
     tiles[20, 32] = 1;
     tiles[21, 32] = 1;
     tiles[22, 32] = 1;
     tiles[33, 32] = 1;
     tiles[34, 32] = 1;
     tiles[35, 32] = 1;
     tiles[35, 31] = 1;
     tiles[35, 30] = 1;
  }
  protected override void OnPaint(PaintEventArgs e)
  {
     base.OnPaint(e);
     using (SolidBrush b = new SolidBrush(Color.White))
     {
        for (int y = 0; y < rows; y++)
        {
           for (int x = 0; x < cols; x++)
           {
              switch (tiles[x, y])
              {
                 case 0:
                    b.Color = Color.White;
                    break;
                 case 1:
                    b.Color = Color.Black;
                    break;
                 default:
                    b.Color = Color.Blue;
                    break;
              }
              e.Graphics.FillRectangle(b, x * ClientSize.Width / cols, y * ClientSize.Height / rows,
                 ClientSize.Width / cols + 1, ClientSize.Height / rows + 1);
           }
        }
     }
  }
  private bool IsLiquid(int x, int y)
  {
     return tiles[x, y] > 1;
  }
  private bool IsSolid(int x, int y)
  {
     return tiles[x, y] == 1;
  }
  private bool IsEmpty(int x, int y)
  {
     return IsEmpty(tiles, x, y);
  }
  public static bool IsEmpty(byte[,] tiles, int x, int y)
  {
     return tiles[x, y] == 0;
  }
  private void ProcessTiles()
  {
     byte processedValue = 0xFF;
     byte unprocessedValue = 0xFF;
     for (int y = 0; y < rows; y ++)
        for (int x = 0; x < cols; x++)
        {
           if (IsLiquid(x, y))
           {
              if (processedValue == 0xff)
              {
                 unprocessedValue = tiles[x, y];
                 processedValue = (byte)(5 - tiles[x, y]);
              }
              if (tiles[x, y] == unprocessedValue)
              {
                 BlobInfo blob = GetWaterAt(new Point(x, y), unprocessedValue, processedValue, new Rectangle(0, 0, 50, 50));
                 blob.ProcessMovement(tiles);
              }
           }
        }
  }
  class BlobInfo
  {
     private int minY;
     private int maxEscapeY;
     private List<int> TopXes = new List<int>();
     private List<int> BottomEscapeXes = new List<int>();
     public BlobInfo(int x, int y)
     {
        minY = y;
        maxEscapeY = -1;
        TopXes.Add(x);
     }
     public void NoteEscapePoint(int x, int y)
     {
        if (maxEscapeY < 0)
        {
           maxEscapeY = y;
           BottomEscapeXes.Clear();
        }
        else if (y < maxEscapeY)
           return;
        else if (y > maxEscapeY)
        {
           maxEscapeY = y;
           BottomEscapeXes.Clear();
        }
        BottomEscapeXes.Add(x);
     }
     public void NoteLiquidPoint(int x, int y)
     {
        if (y < minY)
        {
           minY = y;
           TopXes.Clear();
        }
        else if (y > minY)
           return;
        TopXes.Add(x);
     }
     public void ProcessMovement(byte[,] tiles)
     {
        int min = TopXes.Count < BottomEscapeXes.Count ? TopXes.Count : BottomEscapeXes.Count;
        for (int i = 0; i < min; i++)
        {
           if (IsEmpty(tiles, BottomEscapeXes[i], maxEscapeY) && (maxEscapeY > minY))
           {
              tiles[BottomEscapeXes[i], maxEscapeY] = tiles[TopXes[i], minY];
              tiles[TopXes[i], minY] = 0;
           }
        }
     }
  }
  private BlobInfo GetWaterAt(Point start, byte unprocessedValue, byte processedValue, Rectangle bounds)
  {
     Stack<Point> toFill = new Stack<Point>();
     BlobInfo result = new BlobInfo(start.X, start.Y);
     toFill.Push(start);
     do
     {
        Point cur = toFill.Pop();
        while ((cur.X > bounds.X) && (tiles[cur.X - 1, cur.Y] == unprocessedValue))
           cur.X--;
        if ((cur.X > bounds.X) && IsEmpty(cur.X - 1, cur.Y))
           result.NoteEscapePoint(cur.X - 1, cur.Y);
        bool pushedAbove = false;
        bool pushedBelow = false;
        for (; ((cur.X < bounds.X + bounds.Width) && tiles[cur.X, cur.Y] == unprocessedValue); cur.X++)
        {
           result.NoteLiquidPoint(cur.X, cur.Y);
           tiles[cur.X, cur.Y] = processedValue;
           if (cur.Y > bounds.Y)
           {
              if (IsEmpty(cur.X, cur.Y - 1))
              {
                 result.NoteEscapePoint(cur.X, cur.Y - 1);
              }
              if ((tiles[cur.X, cur.Y - 1] == unprocessedValue) && !pushedAbove)
              {
                 pushedAbove = true;
                 toFill.Push(new Point(cur.X, cur.Y - 1));
              }
              if (tiles[cur.X, cur.Y - 1] != unprocessedValue)
                 pushedAbove = false;
           }
           if (cur.Y < bounds.Y + bounds.Height - 1)
           {
              if (IsEmpty(cur.X, cur.Y + 1))
              {
                 result.NoteEscapePoint(cur.X, cur.Y + 1);
              }
              if ((tiles[cur.X, cur.Y + 1] == unprocessedValue) && !pushedBelow)
              {
                 pushedBelow = true;
                 toFill.Push(new Point(cur.X, cur.Y + 1));
              }
              if (tiles[cur.X, cur.Y + 1] != unprocessedValue)
                 pushedBelow = false;
           }
        }
        if ((cur.X < bounds.X + bounds.Width) && (IsEmpty(cur.X, cur.Y)))
        {
           result.NoteEscapePoint(cur.X, cur.Y);
        }
     } while (toFill.Count > 0);
     return result;
  }
  private void timer1_Tick(object sender, EventArgs e)
  {
     ProcessTiles();
     Invalidate();
  }
  private void Form1_MouseMove(object sender, MouseEventArgs e)
  {
     if (e.Button == MouseButtons.Left)
     {
        int x = e.X * cols / ClientSize.Width;
        int y = e.Y * rows / ClientSize.Height;
        if ((x >= 0) && (x < cols) && (y >= 0) && (y < rows))
           tiles[x, y] = 2;
     }
  }
}

从流体动力学的角度来看,一个相当流行的基于晶格的算法家族是所谓的lattice Boltzmann方法。一个简单的实现,忽略了所有让学术界感到高兴的细节,应该相对简单快速,并获得合理正确的动态。