如何在正方形网格上创建类似 vein/river 的分支结构

how to create a branching vein/river like structure on a square grid

我正在尝试按程序生成一些河流。

我有一个平面(没有高程概念)正方形网格作为基础,想在其上绘制一个分支结构,如图所示。

您能否分享完成此操作的步骤?

我不是在寻找最快的实现,因为没有实时生成,但更简单的实现将是首选。 Lua 是我的语言,但任何东西都可以。

还有一些事情:

  1. 形状应该是算法盟友生成的。
  2. 形状应该是 可使用种子值进行控制。

你的河流三角洲看起来很像一棵树。这是一些 Python 代码,使用 turtle for Graphics 绘制树。

# 您可以在浏览器中编辑此代码 运行! # 尝试更改颜色或添加您自己的形状。

import turtle
from random import randint

def tree(length,n, ps):
    """ paints a branch of a tree with 2 smaller branches, like an Y"""
    if length < (length/n):
           return       # escape the function
    turtle.pensize(max(ps,1))     
    turtle.forward(length)        # paint the thik branch of the tree
    lb = 45+randint(-20,20)
    turtle.left(lb)          # rotate left for smaller "fork" branch
    tree(length * 0.5*(1+randint(-20,20)/100),length/n,ps-1) # create a smaller branch with 1/2 the lenght of the parent branch
    rb = 45+randint(-20,20)
    turtle.right(lb+rb)         # rotoate right for smaller "fork" branch
    tree(length * 0.6,length/n,ps-1)      # create second smaller branch
    turtle.left(rb)          # rotate back to original heading
    rt = randint(-20,20)
    turtle.right(rt)
    tree(length * 0.45,length/n,ps-1)
    turtle.left(rt)
    turtle.backward(length)       # move back to original position
    return              # leave the function, continue with calling program
turtle.left(90)
turtle.penup()
turtle.backward(250)
turtle.pendown()
tree(150,5,5)

我认为生成河流是一种落后的方法,因为你需要根据它们的形状调整很多东西,这会很难。相反,我会创建随机地形高度图并从中提取特征(就像在现实世界中一样),这更容易也更接近现实。在最终地图中,您忽略高度并使用平面图(如果您真的想要平面图)。以下是您可以从高度图中提取的一些内容:

  1. 江湖

    通过播种随机高海拔点并跟随它下坡到海平面或地图边缘。

  2. 植被或地面

    根据坡度和海拔高度,您可以确定地面是沙子、泥土还是岩石。如果有树木,灌木,草地什么的。

这里看看这个QA

和一些河流概况:

调整地形生成的方式也会影响河流的形状(不需要只生成岛屿)。

Seeds 也在为这种方法工作。

[Edit1] 承诺的 C++ 代码

这基本上是生成随机高度图,然后沿着河流进行种子和下坡(如果地形阻挡下坡水流,则会自动生成湖泊)。地形类型也是由坡度和海拔决定的。

//---------------------------------------------------------------------------
picture pic;
//---------------------------------------------------------------------------
void map_random(int _xs,int _ys)
    {
    // config
    int   h0=-1000,h1=3000;     // [m] terrain elevation range
    int   h_water= 0;           // [m] sea level
    int   h_sand=15;            // [m] sand level
    int   h_evergreen=1500;     // [m] evergreen level
    int   h_snow=2000;          // [m] snow level
    int   h_rock=1800;          // [m] mountine rock level
    float a_rock=60.0;          // [deg] mountine rock slope
    float d_pixel=35.0;         // [m] pixel size
    int   d_river_w=5;          // [pixel] river max width
    int   d_river_l=150;        // [pixel] river base length per width increase
    bool _island=true;

    // types
    enum _cover_enum
        {
        _cover_none=0,
        _cover_water,   // sea
        _cover_snow,
        _covers,
        _cover_shift=0,
        _cover_mask=15,
        };
    DWORD _cover[_covers]=
        {
        //  RRGGBB
        0x00000000,     // none
        0x00003080,     // watter (sea)
        0x00EEEEEE,     // snow
        };
    enum _terrain_enum
        {
        _terrain_dirt=0,
        _terrain_sand,
        _terrain_rock,
        _terrain_water, // streams,rivers,lakes
        _terrain_temp,  // temp
        _terrains,
        _terrain_shift=4,
        _terrain_mask=15,
        };
    DWORD _terrain[_terrains]=
        {
        //  RRGGBB
        0x00301510,     // dirt
        0x00EEC49A,     // sand
        0x006F6F6F,     // rock
        0x00006080,     // water (streams,rivers,lakes)
        0x00006080,     // temp
        };
    enum _flora_enum
        {
        _flora_none=0,
        _flora_grass,
        _flora_hardwood,
        _flora_evergreen,
        _flora_deadwood,
        _floras,
        _flora_shift=8,
        _flora_mask=15,
        };
    DWORD _flora[_floras]=
        {
        //  RRGGBB
        0x00000000,     // none
        0x007F7F3F,     // grass
        0x001FFF1F,     // hardwood
        0x00007F00,     // evergreen
        0x007F3F1F,     // deadwood
        };

    // variables
    float a,b,da; int c,t,f;
    int x,y,z,xx,yy,mxs,mys,dx,dy,dx2,dy2,r,r2,ix,l;
    int xh1,yh1;    // topest hill position
    int **ter=NULL,**typ=NULL;
    Randomize();
    // align resolution to power of 2
    for (mxs=1;mxs+1<_xs;mxs<<=1); if (mxs<3) mxs=3;
    for (mys=1;mys+1<_ys;mys<<=1); if (mys<3) mys=3;
    ter=new int*[mys+1]; for (y=0;y<=mys;y++) ter[y]=new int[mxs+1];
    typ=new int*[mys+1]; for (y=0;y<=mys;y++) typ[y]=new int[mxs+1];

    // [Terrain]
    for (;;)
        {
        // diamond & square random height map -> ter[][]
        dx=mxs; dx2=dx>>1; r=(mxs+mys)<<1;          // init step,half step and randomness
        dy=mys; dy2=dy>>1; r2=r>>1;
        // set corners values
        if (_island)
            {
            t=-r2;
            ter[  0][  0]=t;
            ter[  0][mxs]=t;
            ter[mys][  0]=t;
            ter[mys][mxs]=t;
            ter[dy2][dx2]=r+r;  // top of central hill
            }
        else{
            ter[  0][  0]=Random(r);
            ter[  0][mxs]=Random(r);
            ter[mys][  0]=Random(r);
            ter[mys][mxs]=Random(r);
            }
        for (;dx2|dy2;dx=dx2,dx2>>=1,dy=dy2,dy2>>=1)    // subdivide step until full image is filled
            {
            if (!dx) dx=1;
            if (!dy) dy=1;
            // diamond (skip first one for islands)
            if ((!_island)||(dx!=mxs))
             for (y=dy2,yy=mys-dy2;y<=yy;y+=dy)
              for (x=dx2,xx=mxs-dx2;x<=xx;x+=dx)
               ter[y][x]=((ter[y-dy2][x-dx2]+ter[y-dy2][x+dx2]+ter[y+dy2][x-dx2]+ter[y+dy2][x+dx2])>>2)+Random(r)-r2;
            // square
            for (y=dy2,yy=mys-dy2;y<=yy;y+=dy)
             for (x=dx ,xx=mxs-dx ;x<=xx;x+=dx)
              ter[y][x]=((ter[y][x-dx2]+ter[y][x+dx2]+ter[y-dy2][x]+ter[y+dy2][x])>>2)+Random(r)-r2;
            for (y=dy ,yy=mys-dy ;y<=yy;y+=dy)
             for (x=dx2,xx=mxs-dx2;x<=xx;x+=dx)
              ter[y][x]=((ter[y][x-dx2]+ter[y][x+dx2]+ter[y-dy2][x]+ter[y+dy2][x])>>2)+Random(r)-r2;
            for (x=dx2,xx=mxs-dx2;x<=xx;x+=dx)
                {
                y=  0; ter[y][x]=((ter[y][x-dx2]+ter[y][x+dx2]+ter[y+dy2][x])/3)+Random(r)-r2;
                y=mys; ter[y][x]=((ter[y][x-dx2]+ter[y][x+dx2]+ter[y-dy2][x])/3)+Random(r)-r2;
                }
            for (y=dy2,yy=mys-dy2;y<=yy;y+=dy)
                {
                x=  0; ter[y][x]=((ter[y][x+dx2]+ter[y-dy2][x]+ter[y+dy2][x])/3)+Random(r)-r2;
                x=mxs; ter[y][x]=((ter[y][x-dx2]+ter[y-dy2][x]+ter[y+dy2][x])/3)+Random(r)-r2;
                }
            if (_island)
                {
                // recompute middle position after first pass so there can be more central hills
                if (dx==mxs) ter[dy2][dx2]=Random(r2);
                // adjust border to underwatter
                for (y=0;y<=mys;y+=dy2) { ter[y][0]=t; ter[y][mxs]=t; }
                for (x=0;x<=mxs;x+=dx2) { ter[0][x]=t; ter[mys][x]=t; }
                }
            // adjust randomness
            r>>=1; if (r<2) r=2; r2=r>>1;
            }
        // rescale to <h0,h1>
        xx=ter[0][0]; yy=xx;
        for (y=0;y<=mys;y++)
         for (x=0;x<=mxs;x++)
            {
            z=ter[y][x];
            if (xx>z)  xx=z;
            if (yy<z){ yy=z; xh1=x; yh1=y; }
            }
        for (y=0;y<=mys;y++)
         for (x=0;x<=mxs;x++)
          ter[y][x]=h0+(((ter[y][x]-xx)*(h1-h0))/(yy-xx));
        // test for correctness
        if (_island)
            {
            l=0;
            for (x=0;x<=mxs;x++) { if (ter[0][x]>h_water) l++; if (ter[mys][x]>h_water) l++; }
            for (y=0;y<=mys;y++) { if (ter[y][0]>h_water) l++; if (ter[y][mxs]>h_water) l++; }
            if (l>1+((mxs+mys)>>3)) continue;
            }
        break;
        }

    // [Surface]
    for (y=0;y<mys;y++)
     for (x=0;x<mxs;x++)
        {
        z=ter[y][x];
        // max slope [deg]
        a=atan2(ter[y][x+1]-z,d_pixel);
        b=atan2(ter[y+1][x]-z,d_pixel);
        if (a<b) a=b; a*=180.0/M_PI;

        c=_cover_none;
        if (z<=h_water) c=_cover_water;
        if (z>=h_snow ) c=_cover_snow;

        t=_terrain_dirt;
        if (z<=h_sand)  t=_terrain_sand;
        if (z>=h_rock)  t=_terrain_rock;
        if (a>=a_rock)  t=_terrain_rock;

        f=_flora_none;
        if (t==_terrain_dirt)
            {
            r=Random(100);
            if (r>10) f=_flora_grass;
            if (r>50)
                {
                if (z>h_evergreen) f=_flora_evergreen;
                else{
                    r=Random(h_evergreen);
                    if (r<=z) f=_flora_evergreen;
                    else      f=_flora_hardwood;
                    }
                }
            if (r<5) f=_flora_deadwood;
            }
        typ[y][x]=(c<<_cover_shift)|(t<<_terrain_shift)|(f<<_flora_shift);
        }

    // [Rivers]
    for (ix=10+Random(5),a=0.0,da=2.0*M_PI/float(ix);ix;ix--)
        {
        // random start around topest hill
        a+=da*(0.75+(0.50*Random()));
        for (l=0;l<10;l++)
            {
            b=Random(mxs>>3);
            x=xh1; x+=float(b*cos(a));
            y=yh1; y+=float(b*sin(a));
            if ((x<1)||(x>=mxs)) continue;
            if ((y<1)||(y>=mys)) continue;
            if (typ[y][x]&0x00F==_cover_water) continue;
            l=-1;
            break;
            } if (l>=0) continue; // safety check
        for (l=0,r2=0;;)
            {
            // stop on map edge
            if ((x<=0)||(x>=mxs-1)||(y<=0)||(y>=mys-1)) break;
            // decode generated surface
            r=typ[y][x];
            c=(r>>  _cover_shift)&  _cover_mask;
            t=(r>>_terrain_shift)&_terrain_mask;
            f=(r>>  _flora_shift)&  _flora_mask;
            // stop if reached sea
            if (c==_cover_water) break;
            // insert river dot radius = r2
            dx=x-r2; if (dx<0) dx=0; dx2=x+r2; if (dx2>=mxs) dx2=mxs-1;
            dy=y-r2; if (dy<0) dy=0; dy2=y+r2; if (dy2>=mys) dy2=mys-1;
            for (yy=dy;yy<=dy2;yy++)
             for (xx=dx;xx<=dx2;xx++)
              if (((xx-x)*(xx-x))+((yy-y)*(yy-y))<=r2*r2)
               if (((typ[yy][xx]>>_terrain_shift)&_terrain_mask)!=_terrain_water)
                typ[yy][xx]=(typ[yy][xx]&0x00F)|(_terrain_temp<<_terrain_shift);
            // step to smalest elevation neighbor
            dx=x;   dy=y; z=h1; typ[y][x]=(typ[y][x]&0x00F)|(_terrain_water<<_terrain_shift); xx=x; yy=y;
            xx--; r=ter[yy][xx]; if ((z>=r)&&(((typ[yy][xx]>>_terrain_shift)&_terrain_mask)!=_terrain_water)) { z=r; dx=xx; dy=yy; }
            yy--; r=ter[yy][xx]; if ((z>=r)&&(((typ[yy][xx]>>_terrain_shift)&_terrain_mask)!=_terrain_water)) { z=r; dx=xx; dy=yy; }
            xx++; r=ter[yy][xx]; if ((z>=r)&&(((typ[yy][xx]>>_terrain_shift)&_terrain_mask)!=_terrain_water)) { z=r; dx=xx; dy=yy; }
            xx++; r=ter[yy][xx]; if ((z>=r)&&(((typ[yy][xx]>>_terrain_shift)&_terrain_mask)!=_terrain_water)) { z=r; dx=xx; dy=yy; }
            yy++; r=ter[yy][xx]; if ((z>=r)&&(((typ[yy][xx]>>_terrain_shift)&_terrain_mask)!=_terrain_water)) { z=r; dx=xx; dy=yy; }
            yy++; r=ter[yy][xx]; if ((z>=r)&&(((typ[yy][xx]>>_terrain_shift)&_terrain_mask)!=_terrain_water)) { z=r; dx=xx; dy=yy; }
            xx--; r=ter[yy][xx]; if ((z>=r)&&(((typ[yy][xx]>>_terrain_shift)&_terrain_mask)!=_terrain_water)) { z=r; dx=xx; dy=yy; }
            xx--; r=ter[yy][xx]; if ((z>=r)&&(((typ[yy][xx]>>_terrain_shift)&_terrain_mask)!=_terrain_water)) { z=r; dx=xx; dy=yy; }
            if ((dx==x)&&(dy==y))
                {
                // handle invalid path or need for a lake!!!
                if (dx>mxs>>1) dx++; else dx--;
                if (dy>mys>>1) dy++; else dy--;
                }
            x=dx; y=dy;
            // increase river volume with length
            l++; if (l>d_river_l*(r2+1)) { l=0; if (r2<d_river_w) r2++; }
            }
        // make merging of rivers possible
        for (y=0;y<=mys;y++)
         for (x=0;x<=mxs;x++)
          if (((typ[y][x]>>_terrain_shift)&_terrain_mask)==_terrain_water)
           typ[y][x]=(typ[y][x]&0x00F)|(_terrain_temp<<_terrain_shift);
        }
    for (y=0;y<=mys;y++)
     for (x=0;x<=mxs;x++)
      if (((typ[y][x]>>_terrain_shift)&_terrain_mask)==_terrain_temp)
       typ[y][x]=(typ[y][x]&0x00F)|(_terrain_water<<_terrain_shift);


    // [copy data] rewrite this part to suite your needs
    for (y=1;y<_ys;y++)
     for (x=1;x<_xs;x++)
        {
        float nx,ny,nz,x0,y0,z0,x1,y1,z1;
        // (nx,ny,nz) = surface normal
        nx=0.0;      ny=0.0; nz=ter[y][x];
        x0=-d_pixel; y0=0.0; z0=ter[y][x-1];
        x1=0.0; y1=-d_pixel; z1=ter[y-1][x];
        x0-=nx; x1-=nx;
        y0-=ny; y1-=ny;
        z0-=nz; z1-=nz;
        nx=(y0*z1)-(z0*y1);
        ny=(z0*x1)-(x0*z1);
        nz=(x0*y1)-(y0*x1);
        x0=1.0/sqrt((nx*nx)+(ny*ny)+(nz*nz));
        nx*=x0;
        ny*=x0;
        nz*=x0;
        // z = ambient light + normal shading
        nz=(+0.7*nx)+(-0.7*ny)+(+0.7*nz);
        if (nz<0.0) nz=0.0;
        nz=255.0*(0.2+(0.8*nz)); z=nz;
        // r = base color
        r=typ[y][x];
        c=(r>>  _cover_shift)&  _cover_mask;
        t=(r>>_terrain_shift)&_terrain_mask;
        f=(r>>  _flora_shift)&  _flora_mask;
               r=_terrain[t];
        if (c) r=  _cover[c];
        if (f){ if (c) r|=_flora[f]; else r=_flora[f]; };
        // sea color is depending on depth not surface normal
        if (c==_cover_water) z=256-((ter[y][x]<<7)/h0);
        // apply lighting z to color r
        yy=int(r>>16)&255; yy=(yy*z)>>8; if (yy>255) yy=255; r=(r&0x0000FFFF)|(yy<<16);
        yy=int(r>> 8)&255; yy=(yy*z)>>8; if (yy>255) yy=255; r=(r&0x00FF00FF)|(yy<< 8);
        yy=int(r    )&255; yy=(yy*z)>>8; if (yy>255) yy=255; r=(r&0x00FFFF00)|(yy    );
        // set pixel to target image
        pic.p[y][x].dd=r;
        }

    // free ter[][],typ[][]
    for (y=0;y<=mys;y++) delete[] ter[y]; delete[] ter; ter=NULL;
    for (y=0;y<=mys;y++) delete[] typ[y]; delete[] typ; typ=NULL;
    }
//---------------------------------------------------------------------------

该代码基于我的链接答案中的代码,但增加了功能(包括河流)。我使用自己的图片 class 作为图片,所以一些成员是:

  • xs,ys 图像大小(以像素为单位)
  • p[y][x].dd(x,y)位置的像素,为32位整数类型
  • clear(color) - 清除整个图像
  • resize(xs,ys) - 将图像调整为新分辨率
  • bmp - VCL 封装的 GDI 位图 Canvas 访问

您可以调整 Diamond&Square 中的 adjust randomness 来改变地形平滑度。高度限制和阈值也可以被篡改。

要像河流一样实现更多分支,请在集群中播种更多起点,以便它们及时合并到一条或多条河流中。