第四节 Octave倍频分层叠加地形设计原理及基本实现

__hakamada_hinata_rou_kyuu_bu_drawn_by_tinker_bell__sample-5c4bc2e82dc7f83cc95edae1f0644f93.jpg
上一篇:


对noiseMap数值获取进行合理化控制,获得合适的自然变化数据


这次我们对上一节GenerateNoiseMap方法进行完善,添加我们的octave层数,persistance持续度,lacunarity孔隙度三个变量。
我们对Noise脚本原方法进行更新:
修改为

public static float[,] GenerateNoiseMap(int mapWidth, int mapHight,float scale,int octaves,float persistance,float lacunarity){//先在此新加三个变量传入对应参数
    float [,] noiseMap = new float[mapWidth,mapHeight];
    if (scale<=0){
    scale=0.0001f;
    }

    for (int y=0; y<mapHeight;y++){
        for(int x =0;x<mapWidth;x++){
        float amplitude =1;  //给初始振幅和频率赋值为1,初始高度为0,通过下方noiseHeight累加
        float frequency = 1;//获得加持了振幅后的柏林噪音高度。
        float noiseHeight =0;

        for (int i=0; i< octaves;i++){ //对不同层进行不同叠加处理,先写好遍历框架,后面补充内部代码,Ctrl+E,D:自动对齐框选内容的格式
            float smapleX =x/scale*frequency;//再让频率限制因素也起效,在此对每个样本像素点加入频率因子,频率越大,每个样本点数值变化越剧烈。
            float sampleY=y/scale*frequency;
            float perlinValue=MathfPerlinNoise (sampleX,sampleY);
            noiseMap[x,y]=perlinValue;
            noiseHeight +=perlinValue*amplitude;//原本柏林噪音地图仅仅存储默认的柏林噪音值,我们添加振幅因子进行高低范围波动控制。

            amplitude *= persistance;//每轮循环后,让对应两个因子累乘,实现我们示意图所示的控制效果,振幅和频率分别受到持久度和间隙度的影响
            frequency *= lacunarity;//对不同octave进行不同系数控制对应层数的振幅频率
            }
        noiseMap [x,y] = noiseHeight;//将每个像素点地图数值存储到数组里
        }
    }
    return noiseMap;
}

如果要想让起伏高低效果比较好看的话,我们最好让 float perlinValue=MathfPerlinNoise (sampleX,sampleY);这从 [-1,1]随机取值,默认的柏林噪音取值是 [0,1]随机出来都是正数,那么我们的地形3个octave全是正方向向上叠加,最后的效果就是一个0高度平地出现一些高山起伏,而要形成比较自然的山谷甚至海洋低位置自然起伏效果,应该有相应的往下方向的叠加,我们将核心柏林噪音函数取值范围修改为-1到+1区间:

float perlinValue=MathfPerlinNoise (sampleX,sampleY)*2-1;

这样,我们的Noise脚本就能存储到想要的noiseMap数据了

接下来,我们要在x,y loop遍历中找到最低和最高高度:再通过InverseLerp反插约束,将数据范围调整回[0,1]区间。

float maxNoiseHeight = float.MinValue;
float minNoiseHeight = float.MaxValue;

然后在遍历循环里处理完毕octaves每层地图数据后,做个判断,更新当前最大最小高度值:

if (noiseHeight > maxNoiseHeight)
{
maxNoiseHeight = noiseHeight;
}
else if (noiseHeight < minNoiseHeight)
{
minNoiseHeight = noiseHeight;
}

这样我们在遍历的过程中,会不断记录当前最大最小高度值,最后结束获得整个地图的最大最小值,
整个遍历结束,每个有效的柏林噪音数值都已经存储完毕,最后我们再遍历一遍noiseMap,通过 Mathf.InverseLerp对所有数值进行范围框定,将所有高度数值再次缩至[0,1]范围内,具体代码为:

for (int y=0; y<mapHeight; y++){
for(int x =0; x< mapWidth; x++)
{
noiseMap [x,y]=Mathf.InverseLerp (minNoiseHeight,maxNoiseHeight, noiseMap[x,y]);
}
}
//这段遍历插入到刚才最后的return返回map前,对地图数据进行范围约束。
return noiseMap;

解释一下InverseLerp反插值什么原理,之前我们在城市建造demo里采用普通Lerp指令

transform.position = Vector3.Lerp(startPos, targetPos, percentage);

对镜头当前位置和目标位置做时间变化百分比线性插值,从开始点到目标点,percentage花费的时间占百分之几,就插值这段向量距离的百分之几,那么反插值和这个普通线性插值比较类似
在这里,反插值计算公式:(noiseMap-min)/(max-min),相当于我们noiseMap在前两个限制参数间的比例值,如果最大高度为100,最低高度为50,那么当我们实际noiseMap高度为80的时候,我们这个80位于50-100之间的比例范围就是80间隔50距离为30,总取值范围是100-50=50,30距离相对于总共50的总距离,比例值为0.6,即位于中间偏右一些。
接下来对地图生成器也进行完善,将新的noise信息进行分析加工
补充增加3个变量:

public int octaves;
public float persistance;
public float lacunarity;

然后在float[,]noiseMap数组获取数据传入这些数值,修改原:

float[,] noiseMap=Noise.GenerateNoiseMap(mapWidth,mapHeight,noiseScale);

 float[,] noiseMap=Noise.GenerateNoiseMap(mapWidth,mapHeight,noiseScale,octaves,persistance,lacunarity);

保存后,来到Hierarchy面板选择Map Generator物体,看Inspector面板,应该会新生成3项参数,我们对它们进行赋值Octaves 4 ,Persistance为0.5,Lacunarity 为2,随意拖动后它们的值,查看效果变化
发现基本的生成功能没有问题了,我们最终目的是生成合适的地图后进行保存,当我们需要的时候,调用这个保存的地图就好,那么我们需要对随机结果进行存储,通常我们对这种随机后数据包命名为seed,种子,很多rpg游戏装备库,掉落率,随机生成的地牢信息,都是一个seed,读取seed内信息就可以获得相应配置数据,当然这种随机产生的seed经常会导致一个伪随机问题,比方说泰坦之旅里面生成一个新的角色后,seed固定,就决定了哪些装备你一辈子可能都刷不出来,最后含泪开新号继续测试非洲人。
好了,简单说下为什么命名为seed,我们接下来去Noise脚本对应功能添加传入一个int seed值,并调取下系统的随机方法获得seed:
首先Noise.GenerateNoiseMap方法添加传入一个int seed,在方法内新建一个系统随机数(Pseudorandom number generator):

System.Random prng = new System.Random (seed);

接下来我们还想让每一层octave从不同区域取值,需要加入一个偏移量:

Vector2[] octaveOffsets = new Vector2[octaves];
for (int i= 0;i <octaves;i++){
    float offsetX = prng.Next(-100000,100000);//偏移量随机取值控制一下取值范围,不需要太奇葩的数据
    float offsetY= prng.Next(-100000,100000);
    octaveOffsets[i]=new Vector2(offsetX,offsetY);//获得对应每个octave层随机偏移向量

然后我们需要将此随机生成的偏移量加入我们的smapleX,Y样本像素获取式子中,让我们的样本像素针对不同octave层数做出对应层的随机偏移。

float smapleX =x/scale*frequency+octaveOffsets[i].x;
float sampleY=y/scale*frequency+octaveOffsets[i].y;

这样我们每一层octave都会获得一个X,Y轴方向随机的偏移量,随机范围则是-10万到10万,

如果我们想通过手动修改Offset偏移量的x,y轴值,产生平移或者上下拖动效果,那么需要对此方法新增传入Vector2 offset参数,再通过Inspector面板进行调节。修改我们octaveOFfsets的offsetX,offsetY获取式子,加入offset.x,y,使之生效:

float offsetX = prng.Next(-100000,100000)+offset.x;
float offsetY= prng.Next(-100000,100000)+offset.y;

这样我们在随机产生的偏移量基础上,还增加了手动产生偏移量的面板操作入口。
Noise获取已经修改完毕,我们去Generator脚本进行同步扩充。

public int seed;
public Vector2 offset;

同时将调用的Noise脚本下的GeneratoNoiseMap方法新增传入seed和offset值,最后效果为:

 float[,] noiseMap=Noise.GenerateNoiseMap(mapWidth,mapHeight,seed,noiseScale,octaves,persistance.lacunarity,offset);

保存后测试,发现调节seed值,我们可以获得完全随机的不同柏林噪音地图了
然后手动修改offset的x,y值,也可以顺利实现对生成的地图进行水平和竖直方向平移,
最后我们可以做个小优化,当我们改变Noise Scale的时候,尺寸的缩放是相对于地图右上角进行的,如果是针对地图中心进行缩放,效果会更理想,我们对代码进行下改进:
观察Noise脚本里面我们每个样本像素点获取式子:

float smapleX =x/scale*frequency+octaveOffsets[i].x;
float sampleY=y/scale*frequency+octaveOffsets[i].y;

我们的x,y取值范围是map总Width和Height,那么除以scale后,scale变化,我们地图会以整个Height和Width随着scale变化而发生同比例变化,缩放的参照点就是坐标(mapWidth,mapHeight)
我们要将参照点移至map中心,最简单的方法就是让参照点相对于50%的width和height进行平移,计算式子改为

float smapleX =(x-halfWidth)/scale*frequency+octaveOffsets[i].x;
float sampleY=(y-halfHeight)/scale*frequency+octaveOffsets[i].y;

同时此for 循环外部补充两个变量定义:

float halfWidth=mapWidth/2f;
float halfHeight=mapHeight/2f;

保存,测试,发现缩放效果就ok了
最后我们做一点收尾优化,对面板数值调整范围做一下约束:
我们对map大小数值进行范围框定,height和width是应该大于0的
进入mapGenerator脚本

void OnValidate(){
    if(mapWidth<1){
        mapWidth=1;
        }
    if(mapHeight<1){
        mapHeight=1;
        }

Onvalidate方法就是Inspector面板发生数值改变时候自动调用,这个范围限制实现也有其他方法可以约束,不过这里采用比较简单的方法进行实现。
保存,测试,发现控制成功
还剩下其他的数值,也进行下范围控制,间隙度是控制频率的,需要大于1:

if(lacunarity<1){
lacunarity=1;
}
if(octaves<0){
octaves=0; //octave倍频,不能为负数。
}

最后持久度persistance应该在(0-1)之间,控制振幅的范围,那么
public float persistance定义前加入 [Range(0,1)]即可
我们的基本的柏林噪音地图生成算是基本完善了
最后提醒一点,我们回到

float smapleX =(x-halfWidth)/scale*frequency+octaveOffsets[i].x;
float sampleY=(y-halfHeight)/scale*frequency+octaveOffsets[i].y;

这个表达式中,我们的Offset并没有受到scale和frequency的约束,导致左右横移或者上下滚动的时候,会在外部直接破坏原scale*frequency约束下形成的整体框架布局,将+octaveOffsets[i].x,.y放置于前方括号内部,改成:

float smapleX =(x+octaveOffsets[i].x)/scale*frequency;
float sampleY=(y+octaveOffsets[i].y)/scale*frequency;

这样我们调整offset值后,最后偏移效果就是在/scale*frequency这个框架内,做出纯粹的x,y方向移动对应数值距离的效果,具体效果我们在后面上色彩图后能够看出明显区别,到时候考虑到无限地形生成,我们mesh网格绘制时需要保持每个区块已经生成地形不再发生变化,就要修正这个式子了,有兴趣的可以不做这个修正,在后面第十一章节发现问题后再修复对比下
下一篇:

Last modification:August 12, 2020
If you think my article is useful to you, please feel free to appreciate