程序水文学:河流和湖泊的动态模拟

注意:该项目完整源代码发布在Github [ 这里 ]。该存储库还包含有关如何阅读和使用代码的详细信息。

实施基于粒子的水力侵蚀模拟后,我决定可以扩展该概念以模拟地表水文学的其他方面。

我研究了现有的江湖程序生成方法,但发现的结果不适合我。

许多方法的主要目的是使用各种算法(有时基于先前创建的海拔图或逆问题)来创建河流系统(非常漂亮),但是它们在地形和水文之间缺乏牢固的现实关系。

另外,在某些资源上考虑了对整个浮雕上水模型的处理,并使用了高度复杂的流体的模拟。

在本文中,我将展示一种尝试通过扩展模拟基于粒子的水力侵蚀能力的技术来克服这些问题的尝试。我还将说明一般如何解决“浮雕上的水”的问题。

在我的方法中,我为简单性和现实性而努力,其代价是潜在侵蚀系统的复杂性略有增加。我建议你阅读我以前关于该系统的[文章在这里翻译对哈布雷],因为新的模型是基于它。


该系统能够通过水文学快速生成非常逼真的地形。该视频是实时渲染的。该系统能够生成无限数量的此类景观。

说明:我不是地质学家,所以我根据自己的知识创建了系统。

水文学概念


我想创建一个可以模拟许多地理现象的生成系统,包括:

  • 河流和溪流迁移
  • 天然瀑布
  • 峡谷形成
  • 土壤膨胀和洪泛区

因此,水文和地形系统必须是动态的并且紧密相关。基于粒子的水力侵蚀系统已经具有为此所需的基本方面:

  • 浮雕影响水的流动
  • 侵蚀和沉积影响地形

实际上,此系统模拟了雨水造成的侵蚀,但无法传达许多其他影响:

  • 在流动的溪流中,水的行为有所不同。
  • 在立式游泳池中,水的行为有所不同

注意:我经常会提到streampools在模型中假设这些是二维的大规模现象。它们大大降低了模型的复杂性。

上面的大多数地理现象都可以使用流量和盆地模型来传达。理想情况下,他们应该借鉴并增强基于粒子的系统的真实性。

简化的水文模型


在一个或多个数据结构(图形,对象等)中存储有关流和池的信息太复杂了,这限制了我们的能力。

因此,我们的水文模型由两个图组成:流量流域图

注意:不要忘记将它们建模为2D系统。

流和池地图


流程图描述了地表(河流和河流)上的流动水。它将时间平均的粒子位置存储在地图上。旧信息正在慢慢删除。

盆地图描述了表面上的静止水(水坑,池塘,湖泊,海洋)。它将水深存储在地图的相应位置。

注意:流图和池图是与高度图相同大小的数组。


减轻水文影响。渲染中的水层取自水文地图。


组合水文地图。浅蓝色是流图;深蓝色是池图。

这些地图是由沿着地形移动水的粒子生成和连接的。添加这些水文图还为我们提供了与时间无关的信息,该信息允许粒子与其模拟分别进行交互。粒子可以通过使用这些卡进行交互以获得影响其运动的参数。

注意:流地图在通过缓入功能后显示,并基于此在地形上渲染。为了获得更细/更锐利的流量(或忽略宽阔的平坦区域上的较低值),可以修改此显示或为其设置阈值。

水为颗粒


水以离散的物质(“颗粒”)的形式存在,该物质具有一定的体积并沿着浮雕的表面移动。它具有几个影响其运动的参数(摩擦,蒸发速率,沉积速率等)。

这是用于模拟水文学的基本数据结构。不会存储粒子,而只会将其用于高度,流量和池图之间的交互。

注意:粒子概念在上一篇文章 [ 和Habré 翻译 ](以及无数其他资源)中有更详细的解释

水文循环与地图互动


地图通过水文循环相互影响。水文循环包括以下步骤:

  • 在地形上创建粒子
  • (.. ).
  • , .
  • , .
  • , ( ) .
  • .

在整个系统中,只有两种算法:下降(下降)洪水(洪水)下降的粒子改变了流量图,而洪水的粒子改变了盆地图。这些算法将在下面详细描述。


水文模型的一维图。粒子是在地形上创建的,并通过两种算法进行循环处理:下降和洪水。在此过程中,流域和流域的图谱发生了变化,进而影响了粒子的运动。

实作


下面,我将解释用于生成结果的系统的完整实现,并提供代码示例。

注意:我只会显示相关的代码段。可以在Github上的存储库中找到更多信息。该代码的所有相关部分都在“ water.h”文件中。

粒子类


Drop粒子的结构与先前系统的结构相同。下降和泛洪现在是结构的成员,因为它们一次仅作用于一个粒子。

struct Drop{
  
  //... constructors

  int index;                         //Flat Array Index
  glm::vec2 pos;                     //2D Position
  glm::vec2 speed = glm::vec2(0.0);
  double volume = 1.0;
  double sediment = 0.0;

  //... parameters
  const double volumeFactor = 100.0; //"Water Deposition Rate"

  //Hydrological Cycle Functions
  void descend(double* h, double* stream, double* pool, bool* track, glm::ivec2 dim, double scale);
  void flood(double* h, double* pool, glm::ivec2 dim);
};

另一个参数是体积系数,它确定洪水如何将体积转移到水位。

下降算法


下降算法与简单粒子腐蚀算法几乎相同。他收到其他输入“轨迹”-一个数组,其中写入了他访问过的所有位置。将来需要一个数组来构建流图。

void Drop::descend(double* h, double* stream, double* pool, bool* track, glm::ivec2 dim, double scale){

  glm::ivec2 ipos; 

  while(volume > minVol){

    ipos = pos; //Initial Position
    int ind = ipos.x*dim.y+ipos.y; //Flat Array Index

    //Register Position
    track[ind] = true;

    //...
  }
};

通过流图和池图修改参数集:

//...
  //Effective Parameter Set
  double effD = depositionRate;
  double effF = friction*(1.0-0.5*stream[ind]);
  double effR = evapRate*(1.0-0.2*stream[ind]);
//...

注意:我发现这样的参数更改效果很好。

在以前的系统中,粒子可能脱离此循环,只有在完全蒸发或超出浮雕边界的情况下才会被破坏。现在,这里添加了两个附加的退出条件:

//... nind is the next position after moving the particle
  
  //Out-Of-Bounds
  if(!glm::all(glm::greaterThanEqual(pos, glm::vec2(0))) ||
     !glm::all(glm::lessThan((glm::ivec2)pos, dim))){
       volume = 0.0;
       break;
  }

  //Slow-Down
  if(stream[nind] > 0.5 && length(acc) < 0.01)
    break;

  //Enter Pool
  if(pool[nind] > 0.0)
    break;

//...

如果粒子没有足够的加速度并且被其他粒子包围,或者直接进入池中,则它会以其所有剩余体积过早地完成下降,然后继续进行洪水算法。

注意:溢出条件还会重置音量,以使粒子不会移入泛洪算法。

泛洪算法


剩余体积的粒子可能会从其当前位置溢出。如果它停止下降(没有加速度)或进入现有池,则会发生这种情况。

洪水算法将颗粒的体积转移到增加的水位,从而改变盆地的图。该技术是使用“测试平面”将水位增量增加到颗粒体积的一小部分。随着水位的升高,颗粒体积减小。


泛洪算法动画。测试平面和水位逐渐增加,减少了颗粒体积。如果发现泄漏,则剩余体积将移至泄漏点以执行下降。

在每个步骤中,我们从粒子的位置开始进行洪水填充(也就是说,我们递归检查邻居的位置),并将位于原始平面上方(当前水位)和测试平面下方的所有位置添加到“洪水集”中。这是浮雕的一部分,是游泳池的一部分。

在填充过程中,我们检查是否有泄漏。这些是淹没集中在测试平面AND原始平面以下的点。如果发现多个泄漏点,则选择最低的泄漏点。

void Drop::flood(double* height, double* pool, glm::ivec2 dim){

  index = (int)pos.x*dim.y + (int)pos.y;
  double plane = height[index] + pool[index];  //Testing Plane
  double initialplane = plane;                 //Water Level

  //Flood Set
  std::vector<int> set;
  int fail = 10; //Just in case...

  //Iterate while particle still has volume
  while(volume > minVol && fail){

    set.clear();
    bool tried[dim.x*dim.y] = {false};

    //Lowest Drain
    int drain;
    bool drainfound = false;

    //Recursive Flood-Fill Function
    std::function<void(int)> fill = [&](int i){

      //Out of Bounds
      if(i/dim.y >= dim.x || i/dim.y < 0) return;
      if(i%dim.y >= dim.y || i%dim.y < 0) return;

      //Position has been tried
      if(tried[i]) return;
      tried[i] = true;

      //Wall / Boundary of the Pool
      if(plane < height[i] + pool[i]) return;

      //Drainage Point
      if(initialplane > height[i] + pool[i]){

        //No Drain yet
        if(!drainfound)
          drain = i;

        //Lower Drain
        else if( pool[drain] + height[drain] < pool[i] + height[i] )
          drain = i;

        drainfound = true;
        return; //No need to flood from here
      }

      //Part of the Pool
      set.push_back(i);
      fill(i+dim.y);    //Fill Neighbors
      fill(i-dim.y);
      fill(i+1);
      fill(i-1);
      fill(i+dim.y+1);  //Diagonals (Improves Drainage)
      fill(i-dim.y-1);
      fill(i+dim.y-1);
      fill(i-dim.y+1);
    };

    //Perform Flood
    fill(index);

    //...

注意:为简单起见,此处使用八向填充算法将来,将有可能更有效地实施它。

确定了许多洪水和泄漏点后,我们更改了水位和水池图。

如果找到泄漏点,我们将粒子(及其“溢出”体积)移至泄漏点,以便其可以再次开始下降。然后水位下降到泄漏点的高度。

    //...

    //Drainage Point
    if(drainfound){

      //Set the Particle Position
      pos = glm::vec2(drain/dim.y, drain%dim.y);

      //Set the New Waterlevel (Slowly)
      double drainage = 0.001;
      plane = (1.0-drainage)*initialplane + drainage*(height[drain] + pool[drain]);

      //Compute the New Height
      for(auto& s: set) //Iterate over Set
        pool[s] = (plane > height[s])?(plane-height[s]):0.0;

      //Remove some sediment
      sediment *= 0.1;
      break;
    }

    //...

注意:当水位因泄漏而下降时,我发现这在泄漏率低的情况下效果最佳。另外,去除部分沉积岩有助于实施。

因此,进入填充池的新粒子会立即将其体积移动到泄漏点,因为向池中添加体积会从其中置换相同的体积。

如果找不到泄漏点,则我们将计算测试平面下的体积并将其与颗粒的体积进行比较。如果小于,则我们从颗粒中除去体积并调整水位。然后测试平面上升。如果较大,则测试平面降低。重复该过程,直到粒子空间不足或发现泄漏点。

    //...

    //Get Volume under Plane
    double tVol = 0.0;
    for(auto& s: set)
      tVol += volumeFactor*(plane - (height[s]+pool[s]));

    //We can partially fill this volume
    if(tVol <= volume && initialplane < plane){

      //Raise water level to plane height
      for(auto& s: set)
        pool[s] = plane - height[s];

      //Adjust Drop Volume
      volume -= tVol;
      tVol = 0.0;
    }

    //Plane was too high and we couldn't fill it
    else fail--;

    //Adjust Planes
    float approach = 0.5;
    initialplane = (plane > initialplane)?plane:initialplane;
    plane += approach*(volume-tVol)/(double)set.size()/volumeFactor;
  }

  //Couldn't place the volume (for some reason)- so ignore this drop.
  if(fail == 0)
    volume = 0.0;

} //End of Flood Algorithm

平面的高度与池的表面积(即集合的大小)缩放的体积差异成比例地进行调整。通过使用接近系数,可以提高飞机达到正确水位的稳定性。

侵蚀膜


world类包含以普通数组形式的所有三个映射:

class World {

public:
  void generate();            //Initialize
  void erode(int cycles);     //Erode with N Particles

  //...

  double heightmap[256*256] = {0.0};
  double waterstream[256*256] = {0.0};
  double waterpool[256*256] = {0.0};

};

注意:高度图是使用Perlin噪声初始化的。

单个粒子的每个水文步骤均包含以下内容:

//...

//Spawn Particle
glm::vec2 newpos = glm::vec2(rand()%(int)dim.x, rand()%(int)dim.y);
Drop drop(newpos);

int spill = 5;
while(drop.volume > drop.minVol && spill != 0){

  drop.descend(heightmap, waterstream, waterpool, track, dim, scale);

  if(drop.volume > drop.minVol)
    drop.flood(heightmap, waterpool, dim);

  spill--;
}

//...

溢出参数确定粒子可以进入池中并再次离开的次数,然后再将其简单破坏。否则,当其体积耗尽时,颗粒会死亡。

注意:颗粒很少进入池中,并在下降阶段完全蒸发之前留出一到两次以上,但为防万一,我添加了这一点。

侵蚀函数包装此代码并为N个粒子执行水文步骤,直接更改流图:

void World::erode(int N){

  //Track the Movement of all Particles
  bool track[dim.x*dim.y] = {false};

  //Simulate N Particles
  for(int i = 0; i < N; i++){
   
    //... simulate individual particle

  }

  //Update Path
  double lrate = 0.01;  //Adaptation Rate
  for(int i = 0; i < dim.x*dim.y; i++)
    waterstream[i] = (1.0-lrate)*waterstream[i] + lrate*((track[i])?1.0:0.0);

}

在这里,磁道阵列被传递给下降函数。我发现同时跟踪几个粒子的运动和相应的变化可以改善流图的结果。适应率决定了删除旧信息的速度。

树木


只是为了好玩,我添加了树木以查看是否可以进一步改善侵蚀模拟。它们作为矢量存储在世界教室中。

树木是在地图上随机创建的,那里没有水池和强流,地形也不是很陡峭。他们还有机会在自己周围创建其他树木。

在创建树木的过程中,它们会在树木周围一定半径内写入植被的密度图。高植被密度降低了下降粒子与地形之间的质量传递。这是为了模拟根部如何将土壤固定在适当的位置。

//... descend function
double effD = depositionRate*max(0.0, 1.0-treedensity[ind]);
//...

如果树木在游泳池中,或者树木下方的溪流过于强大,它们就会死亡。此外,他们有随机的死亡概率。


多亏了阴影和法线贴图,即使是非常简单的树精灵也可以使浮雕更加美观。

注意:可在文件“ vegetation.h”和函数“ World :: grow()”中找到树模型。

其他详情


使用自制的OpenGL包装器将结果可视化,该包装器在此处布置

结果


可以根据您需要的任何分布在地图上创建粒子。在演示中,我在256×256张卡上均匀分布地创建了它们。

如下所示,最终的模拟高度依赖于原始浮雕的选择。该系统能够模拟大量自然现象。我不能只得到峡谷。他们可能需要很长很慢的模拟。

可以在系统中观察到其他现象,例如瀑布,曲折和三角洲,湖泊,土壤膨胀等。

该系统还可以很好地管理流量和水池的分布,这些地方的雨水积聚在很多雨水聚集的地方,而不是在随机的地方。因此,生成的水文与地形密切相关。


在256×256网格上进行实时水文模拟。原始浮雕相对较平滑,可以使流快速出现。从一开始,就可以观察到池和泄漏的最简单的创建,此后便出现并保留了大流量。

比较缩小流量的效果


要比较通过将地图链接到侵蚀系统而产生的差异,您可以在同一地图上模拟水文状况,包括并禁用各种效果。

我模拟了相同的地形三遍:

  • 基于粒子的侵蚀(基本侵蚀)接收流量和池图。池仍会影响生成
  • 基本侵蚀,其参数由水文地图更改(结合侵蚀)
  • 结合侵蚀与水文地图更改的参数并影响树木的侵蚀

注意:这是一个相当混乱的系统,出现的水文类型高度依赖于地形。很难找到救济的“非常揭示”的例子。


基本系统的浮雕渲染


组合系统立面渲染


带有树木

系统的地形渲染有趣的观察:将水文地图与颗粒参数结合起来实际上会使河床变窄。特别是在平面区域中,粒子分布较少,并合并为少量更强的流动。

减少的摩擦和蒸发成功地解决了颗粒开始偏爱现有通道的事实。

直接观察浮雕,其他效果更明显。


基本系统的水文图


组合系统的水文图


带有树木的系统的水文图

注意:这些结果是在模拟时间的正好60秒内生成的。

树木还会影响收窄。它们增强了在陡峭区域开辟清晰道路的过程。它们迫使流保留在已经铺设的通道中,因此降低了曲折度。因此,树木的位置会影响水的流动。


记录树木位置如何帮助维持溪流位置的示例。与所有激活效果一样,这与以前相同。

池影响


用于创建水池的系统运行良好,并允许在同一浮雕上存在多个具有不同高度的水体。它们也可以相互扩散并排空。


一个视频示例,该视频显示在海拔高度差较大的较粗糙基线地形上形成水池。上层湖泊实际上位于下层湖泊上方,并将生成的水直接排入下层湖泊。

注意:我得到了几粒种子,其中三个湖泊依次流入,但是我不想花很多时间为本文找到合适的种子。我已经生成了太多的图片和视频。

游泳池的高度有时会跳跃。我认为,当水位接近泄漏水平并添加大量水时,会发生这种情况。可以通过降低溢流功能中的泄漏率来减小此影响。


再过一分钟后,便会出现几个新池。


在更陡的角度,盆高的差异更加明显。


水文图清楚地表明,中部盆地合并为下部盆地。

泛洪算法的执行导致以下事实:池在地图的边界处看到“墙”。在上面显示的图像中这很明显。

另一个可能的改进可能是在世界上增加了海平面,以便这些池在海平面的地图边缘观察泄漏,否则它们只会溢出。

仿真速度


下降和淹没的每个步骤的时间因粒子而异,但大约保留一个数量级(大约1微秒)。有了稳定的流动,粒子可以更快地在地图上移动。

浇筑时间是最昂贵的步骤,因此浇筑时间与池的大小成正比。水池越大,您需要提高水位的面积就越大。大型池绝对是系统的瓶颈。如果您有提高速度的想法,请告诉我。

下降时间与地图的大小和许多其他参数(包括摩擦和蒸发速率)成比例变化。

本文中的所有视频都是实时录制的,也就是说,模拟速度通常很快。

注意:在本文发表后不久,我对计算表面法线的方法进行了小改动,从而提高了仿真速度。在仿真过程中,这种效果非常明显,但是由于启动和泛洪所需的时间差异很大,因此很难对其进行基准测试。据我估计,仿真速度提高了一倍。

更漂亮的视频



在不平坦的垂直地形上进行水文模拟。


稍微平滑一点。有些湖泊有些波动。

在平坦平坦的地形上进行水文模拟。

提速后录制了后续视频。


更平坦,更平滑的地形。


视频由几条相连的溪流组成。主要河流有两个汇流点,汇入的流量较小,我们看到了它的大小。


随着河流形成,浮雕更加不均匀。

我可以永远继续下去。

结论与未来工作


这种简单的技术仍然基于粒子,但是成功地传达了许多附加效果。它提供了一种简单的方法来模拟大规模的水运动,而无需完全模拟流体动力学。此外,她还使用直观的水模型。

在模拟中,我们可以观察到蜿蜒曲折的河流,瀑布,湖泊和湖泊输水,将平坦区域分成三角洲的河流等的出现。

以土壤图的形式添加不同类型的土壤是很有趣的,可以从中获取侵蚀参数。

您还可以轻松地更改树木系统,以创建将土壤固定到位的不同类型的树木。

很难在几张图片和视频中传达各种生成的结果,因此您应该自己动手做,这很有趣。我建议尝试使用原始的地图参数,包括八度音阶噪声和地图比例。

如果您有任何疑问或意见,可以与我联系。我在这个项目上工作非常努力,所以我的大脑在烧结,我确定我错过了一些有趣的方面。

All Articles