zoukankan      html  css  js  c++  java
  • 大地形初探小结一

    自定义地形数据
    噪声优化地形理论

    1 地形背后的算法-CLOD简介

    CLOD是continuous level of detail的缩写-连续细节级别构造算法构建地形网格。给terrain中需要更多细节的地方更多的三角形,而平坦的地方就减少三角形的数量。同时这个过程是动态的、连续的,所以称为CLOD。因此可以优化地形网格存储。

    image_thumb[2]

    地形高低由被称为高度图或灰度图:heightMap,来决定。颜色范围[0,1] = [黑,白]。最好用双精度浮点数。

    image image_thumb[21] 

    image_thumb[20] image_thumb[22]

    举例[0,1]映射为[0,1000],如果一点高度值为0.5,那么该点地形高度就是500,该点的顶点坐标Y就是250,

    注意分辨率:mesh网格size最好为2的幂次,高度图size为2的幂次+1。

    image

    分层图:splatMap、splatAlpha、AlphaMap,格式RGBA32。Unity提供了TerrainData接口供我们读写,它类似一个Tuti矩阵float[,,]image_thumb[4],

    2 地形设计原则

    Contrast:鲜明对比,吸引观众注意的特殊元素。

    Repetition:重复。降低加工需求;玩家潜意识的预测。

    Alignment:对齐。通常与重复并行。元素位置可能影响其他元素展示,冲击、震撼、吸引。

    Proximity:临近。相互关联的元素最好在一起。

    Conherence:连贯。尽量使用风格相近的元素。树林包、建筑包、贴花包。

    3 自定义TerrainData

    地形数据需要设置许多参数:高度height、分层splat、贴图texture、细节表现等等

    3.1 设置高度数据

    核心代码,image

    高度数据来源是.r16或.r8或raw格式的数据,是二进制格式它有大小端顺序。//注意Windows字节序,//注意右手系转换到左手系

    比如从WM中导出一张分辨率为1024*1024的heightmap,格式存储为.r16,这样每个高度2byte,共2048kb,查看一下.r16文件大小果然是这样。image,那么使用.NET平台的库函数可以很方便地将这些字节读取到内存中,存储为一个字节数组。

    3.1.1 手动计算无缝高度图

    无缝纹理创建两种方法,1、图像上下边缘没有明显区别,则可以将其镜像翻转并拼接,缺点是中心点看起来很怪.2、图像水平或垂直一切为二,然后从相反方向拼接(上拼下),缺点太费手工。

    在unity用噪声算法生成splat:相邻图块边缘颜色相同,但是也会出现问题:边缘像素的连续性会被中断。解决办法就是在图块中将边缘像素与整个纹理混合,这就要考虑该像素在纹理上的位置距离。计算该像素与相邻像素的噪声值再相加,然后在纹理中的位置缩放。如果该像素更靠近边缘,则会混合偏向相邻图块中的噪声值;如果更靠近中心,变化则很小。

    image image image

    float u = (float)x / (float)w;
    float v = (float)y / (float)h;
    float noise00 = Utils.fBM((x + perlinOffsetX) * perlinXScale,
                                (y + perlinOffsetY) * perlinYScale,
                        perlinOctaves,
                        perlinPersistance) * perlinHeightScale;
    float noise01 = Utils.fBM((x + perlinOffsetX) * perlinXScale,
                                (y + perlinOffsetY + h) * perlinYScale,
                        perlinOctaves,
                        perlinPersistance) * perlinHeightScale;
    float noise10 = Utils.fBM((x + perlinOffsetX + w) * perlinXScale,
                                (y + perlinOffsetY) * perlinYScale,
                        perlinOctaves,
                        perlinPersistance) * perlinHeightScale;
    float noise11 = Utils.fBM((x + perlinOffsetX + w) * perlinXScale,
                                (y + perlinOffsetY + h) * perlinYScale,
                        perlinOctaves,
                        perlinPersistance) * perlinHeightScale;
    float noiseTotal =  u * v * noise00 +
                    u * (1 - v) * noise01 +
                    (1 - u) * v * noise10 +
                    (1 - u) * (1 - v) * noise11;
    
    float value = (int)(256 * noiseTotal) + 50;
    float r = Mathf.Clamp((int) noise00,0,255);
    float g = Mathf.Clamp(value, 0, 255);
    float b = Mathf.Clamp(value + 50, 0, 255);
    float a = Mathf.Clamp(value + 100, 0, 255);
    
    pValue = (r + g + b) / (3 * 255.0f);

    3.1.2 噪声理论与高度数据

    有一篇文章第8节介绍了振幅函数,与噪声类似。

    使用噪声算法创建起伏的地形最大好处是它具有平滑度和可预测性,是创建山峦的最佳方法。

    image

    image

    image

    假如地形是2x2的,那么就有4个高度值存在二维数组;假如是1000x1000,就有100,0000个高度值。这里用一个简单公式可以表示上述的二维数组:

    Height(y) = Cos(x) + Sin(Z) * 16

    image

    如果增量越小曲线就平滑,频率较低。增量越大曲线越陡峭,频率越高。也可以组合多个噪声。

    image

    //简单版本
    Mathf.PerlinNoise(x * frequency, y * frequency)
    //布朗运动算法
    //xy坐标,octave阶梯,连续性距离
    public static float fBM(float x, float y, int oct, float persistance)
    {
            float total = 0;
            float frequency = 1;//频率
            float amplitude = 1;//振幅
            float maxValue = 0;
            for (int i = 0; i < oct; i++)
            {
                total += Mathf.PerlinNoise(x * frequency, y * frequency) * amplitude;
                maxValue += amplitude;
                amplitude *= persistance;
                frequency *= 2;
            }
    
            return total / maxValue;
    }

    3.1.3 沃罗诺伊分布(Voronoi Tessellation)

    image

    每个区域都有一个种子,按照种子所在的位置,将空间划分成不同的区域。如果该区域被限制在一个有限的域中(如一个正方形),那么这些划分的区域全部都是封闭区域。

    public void Voronoi()
        {
            //获取高度图
            float[,] heightMap = GetHeightMap();
            //要生成的个数
            for (int p = 0; p < voronoiPeaks; p++)
            {
                //在地形size内随机一个点
                Vector3 peak = new Vector3(UnityEngine.Random.Range(0, terrainData.heightmapWidth),
                                           UnityEngine.Random.Range(voronoiMinHeight, voronoiMaxHeight),
                                           UnityEngine.Random.Range(0, terrainData.heightmapHeight)
                                          );
                //测试已存在的与随机的高度,避免突然凹陷
                if (heightMap[(int)peak.x, (int)peak.z] < peak.y)
                    heightMap[(int)peak.x, (int)peak.z] = peak.y;
                else
                    continue;
                //得到山顶坐标
                Vector2 peakLocation = new Vector2(peak.x, peak.z);
                float maxDistance = Vector2.Distance(new Vector2(0, 0), new Vector2(terrainData.heightmapWidth,
                                                                                  terrainData.heightmapHeight));
                for (int y = 0; y < terrainData.heightmapHeight; y++)
                {
                    for (int x = 0; x < terrainData.heightmapWidth; x++)
                    {
                        if (!(x == peak.x && y == peak.z))
                        {
                            //与山顶的距离
                            float distanceToPeak = Vector2.Distance(peakLocation, new Vector2(x, y)) / maxDistance;
                            float h;
    
                            if (voronoiType == VoronoiType.Combined)
                            {
                                //直线下降 + 曲线下降
                                h = peak.y - distanceToPeak * voronoiFallOff -
                                    Mathf.Pow(distanceToPeak, voronoiDropOff); //combined
                            }
                            else if (voronoiType == VoronoiType.Power)
                            {
                                //曲线下降:高-距离的幂次 * 斜率,其中幂数>1为凸曲线,<1为凹曲线,=1为直线
                                h = peak.y - Mathf.Pow(distanceToPeak, voronoiDropOff) * voronoiFallOff; //power
                            }
                            else if (voronoiType == VoronoiType.SinPow)
                            {
                                //波形下降,3和2也参与控制陡峭程度
                                h = peak.y - Mathf.Pow(distanceToPeak*3, voronoiFallOff) -
                                        Mathf.Sin(distanceToPeak*2*Mathf.PI)/voronoiDropOff; //sin pow
                            }
                            else
                            {
                                //直线下降:高-距离*斜率
                                h = peak.y - distanceToPeak * voronoiFallOff; //linear
                            }
    
                            //测试已存在高度必须小于随机的高度,才能正确下降
                            if (heightMap[x,y] < h)
                                heightMap[x, y] = h;
                        }
                    }
                }
            }
            terrainData.SetHeights(0, 0, heightMap);
        }

    image image image image

    liner、power、combined、sin pow

    3.1.4 中点位移法

    基本思想就是
    先矩形区域中心点高度:四个角高度之和再取平均。

    image

    然后计算四条边中心点的高度:

    image

        public void MidPointDisplacement()
        {
            float[,] heightMap = GetHeightMap();
    
            int width = terrainData.heightmapWidth - 1;
            int squareSize = width;
            float heightMin = MPDheightMin;
            float heightMax = MPDheightMax;
            //阻尼器
            float heightDampener = (float)Mathf.Pow(MPDheightDampenerPower, -1 * MPDroughness);
    
    
            int cornerX, cornerY;
            int midX, midY;
            int pmidXL, pmidXR, pmidYU, pmidYD;
    
            //这段代码,是随机在地形选取一矩形区域
           /* heightMap[0, 0] = UnityEngine.Random.Range(0f, 0.2f);
            heightMap[0, terrainData.heightmapHeight - 2] = UnityEngine.Random.Range(0f, 0.2f);
            heightMap[terrainData.heightmapWidth - 2, 0] = UnityEngine.Random.Range(0f, 0.2f);
            heightMap[terrainData.heightmapWidth - 2, terrainData.heightmapHeight - 2] =
                                                        UnityEngine.Random.Range(0f, 0.2f);*/
            while (squareSize > 0)
            {
                for (int x = 0; x < width; x += squareSize)
                {
                    for (int y = 0; y < width; y += squareSize)
                    {
                        //计算区域右上角坐标
                        cornerX = (x + squareSize);
                        cornerY = (y + squareSize);
                        //中点坐标
                        midX = (int)(x + squareSize / 2.0f);
                        midY = (int)(y + squareSize / 2.0f);
                        //得到区域中心点的高度
                        //高度就是四个角相加,再取平均值,再加上一个随机高度
                        heightMap[midX, midY] = (float)((heightMap[x, y] +
                                                         heightMap[cornerX, y] +
                                                         heightMap[x, cornerY] +
                                                         heightMap[cornerX, cornerY]) / 4.0f +
                                                        UnityEngine.Random.Range(heightMin, heightMax));
                    }
                }
    
               for (int x = 0; x < width; x += squareSize)
                {
                    for (int y = 0; y < width; y += squareSize)
                    {
                        cornerX = (x + squareSize);
                        cornerY = (y + squareSize);
                        midX = (int)(x + squareSize / 2.0f);
                        midY = (int)(y + squareSize / 2.0f);
    
                        pmidXR = (int)(midX + squareSize);
                        pmidYU = (int)(midY + squareSize);
                        pmidXL = (int)(midX - squareSize);
                        pmidYD = (int)(midY - squareSize);
    
                        //地形边界检查
                        if (pmidXL <= 0 || pmidYD <= 0
                            || pmidXR >= width - 1 || pmidYU >= width - 1) continue;
    
                        //分别计算四条边的中点高度
    
                        //计算矩形的低边中点高度
                        heightMap[midX, y] = (float)((heightMap[midX, midY] +
                                                      heightMap[x, y] +
                                                      heightMap[midX, pmidYD] +
                                                      heightMap[cornerX, y]) / 4.0f +
                                                     UnityEngine.Random.Range(heightMin, heightMax));
                        //计算矩形的顶边中点高度
                        heightMap[midX, cornerY] = (float)((heightMap[x, cornerY] +
                                                                heightMap[midX, midY] +
                                                                heightMap[cornerX, cornerY] +
                                                            heightMap[midX, pmidYU]) / 4.0f +
                                                           UnityEngine.Random.Range(heightMin, heightMax));
    
                        //计算矩形的左边中点高度
                        heightMap[x, midY] = (float)((heightMap[x, y] +
                                                                heightMap[pmidXL, midY] +
                                                                heightMap[x, cornerY] +
                                                      heightMap[midX, midY]) / 4.0f +
                                                     UnityEngine.Random.Range(heightMin, heightMax));
                        //计算矩形的右边边中点高度
                        heightMap[cornerX, midY] = (float)((heightMap[midX, y] +
                                                                heightMap[midX, midY] +
                                                                heightMap[cornerX, cornerY] +
                                                                heightMap[pmidXR, midY]) / 4.0f +
                                                           UnityEngine.Random.Range(heightMin, heightMax));
    
                    }
                }
    
                //向内收缩
                squareSize = (int)(squareSize / 2.0f);
                //阻尼
                heightMin *= heightDampener;
                heightMax *= heightDampener;
            }
    
            terrainData.SetHeights(0, 0, heightMap);
        }

    image

    3.1.5 基于模糊的平滑算法

    通过上面几种方法生成高度后,需要做一次平滑处理。

    基于模糊的平滑算法:
    给定一点,求该点及附近点的高度和的平均值

    image

    height = (height(x,y) + height(x-1,y+1) + height(x,y+1) + height(x+1,y+1) + height(x-1,y) + height(x+1,y) + height(x-1,y-1) + height(x,y-1) + height(x+1,y-1)) / 9; //高度会均匀下降

    image

    3.2 设置地形颜色数据Splatmap

    Splatmap也被称为weightmap、alphamap;TerrainData给出的接口是SetAlphamaps,rgb通道和Splatmap的r+b+g+r<=1

    image
    image

    设置完毕后,增加细节贴图,TerrainDaTa接口:

    image

    3.2.1 手动计算splatmap

    for (int y = 0; y < terrainData.alphamapHeight; y++)
    {
        for (int x = 0; x < terrainData.alphamapWidth; x++)
        {
            //terrainLayer
            float[] splat = new float[terrainData.alphamapLayers];
            for (int i = 0; i < splatHeights.Count; i++)
            {
                //用噪声优化边界锯齿,增加混合效果
                float noise = Mathf.PerlinNoise
                (
                    x * splatHeights[i].splatNoiseXScale,
                    y * splatHeights[i].splatNoiseYScale
                ) * splatHeights[i].splatNoiseScaler;
                float offset = splatHeights[i].splatOffset + noise;
                //start - stop,是在一个高度范围条带内设置相同纹理,用噪声优化了边界
                float thisHeightStart = splatHeights[i].minHeight - offset;
                float thisHeightStop = splatHeights[i].maxHeight + offset;
                /*
                float thisHeightStart = splatHeights[i].minHeight;
                float thisHeightStop = splatHeights[i].maxHeight;*/
    
                /*手写坡度值:分别计算该点与上一点在x、y轴上的增量,取模
                 * |dy    .(nx,ny)
                 * |
                 * |______dx
                 *(x,y)
                */
                /*float steepness = GetSteepness(heightMap, x, y,
                                               terrainData.heightmapWidth,
                                               terrainData.heightmapHeight);*/
                //自带坡度值
                float steepness = terrainData.GetSteepness
                (
                    y / (float)terrainData.alphamapHeight,
                    x / (float)terrainData.alphamapWidth
                );
                //测试是否处于同一高度范围内及给定坡度范围内
                if ((heightMap[x, y] >= thisHeightStart && heightMap[x, y] <= thisHeightStop) &&
                    (steepness >= splatHeights[i].minSlope && steepness <= splatHeights[i].maxSlope))
                {
                    splat[i] = 1;
                }
            }
            //要满足r+g+b+r=1,每个通道对应一张细节图。一个splatmap最多对应4张细节图
            NormalizeVector(splat);
            for (int j = 0; j < splatHeights.Count; j++)
            {
                splatmapData[x, y, j] = splat[j];
            }
        }
    }
    terrainData.SetAlphamaps(0, 0, splatmapData);

    如何用很少的细节贴图,增强表现地形变化?

    每块区域有一定高度、坡度,从高度与坡度数值结合铺开纹理,即使高度都在同一范围内也可用坡度数值限定。同时也可使用tileOffset参数参与显示。

    3.2.2 外部导入splatmap

    从world machine生成splat

    Color col = splatmapColors[((w - 1) - x) * w + y];
    Color col_b = splatmapColors_b[((w - 1) - x) * w + y];
    float sum = col.r+col.g+col.b;
    if (sum>1.0f) {
        splatmapData[x, y, 0] = col.r / sum;
        splatmapData[x, y, 1] = col.g / sum;
        splatmapData[x, y, 2] = col.b / sum;
        splatmapData[x, y, 3] = 0.0f;
    } else{
        splatmapData[x, y, 0] = col.r;
        splatmapData[x, y, 1] = col.g;
        splatmapData[x, y, 2] = col.b;
        splatmapData[x, y, 3] = 1.0f - sum;
    }

    细节图设置:

    SplatPrototype[] newSplatPrototypes;
    newSplatPrototypes = new SplatPrototype[splatHeights.Count];
    int spindex = 0;
    foreach (SplatHeights sh in splatHeights)
    {
        newSplatPrototypes[spindex] = new SplatPrototype();
        newSplatPrototypes[spindex].texture = sh.texture;
        newSplatPrototypes[spindex].tileOffset = sh.tileOffset;
        newSplatPrototypes[spindex].tileSize = sh.tileSize;
        newSplatPrototypes[spindex].texture.Apply(true);
        spindex++;
    }
    //splatPrototypes在新版本被标记过时了,要用terrainLayer数据
    terrainData.splatPrototypes = newSplatPrototypes;

    项目地址github

  • 相关阅读:
    jquery和js常用的遍历数组的方法
    jquery获取某组件距离边框的距离
    jquery获取鼠标移动时的位置
    springboot整合websocket实现登录挤退现象
    使用Gitblit搭建Git服务器教程
    Gradle史上最详细解析
    推荐5个提高Java开发效率的工具
    JBoss7详细配置指南
    XMLBEANS的使用总结
    使用XMLBeans 处理XML
  • 原文地址:https://www.cnblogs.com/baolong-chen/p/13307356.html
Copyright © 2011-2022 走看看