Procedural Generation 之前研究了一下程序化生成,这是个很复杂的技术,开放世界游戏最需要的,就是可以很方便快速且随机地去生成地形,然后美术可以用很多接口去控制地形的细节,然后给地形上植被、建筑等等。这个系列文章会总结一下我学到的相关技术,会基于噪声去生成高度图,自己去储存mesh的三角面然后给Mesh Filter,这样我们可以控制地形的细分程度,然后控制地形的细节,然后会用LOD的思想去做无尽的地形,进行地形分块。
这篇文章我会使用噪声去生成一张高度图,然后储存mesh,根据高度图去生成高低起伏的地图。
生成Noise Map 首先要通过噪声生成高度图,我用曲线表示地形,不同高度的曲线叠加,去增加地形的细节。将单独的一个波形称为Octave,将多个Octave叠加起来,就可以得到我想要的效果。然后另外用两个变量去控制叠加的波形的波峰高度和频率。
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 for (int y = 0 ; y < height; y++){ for (int x = 0 ; x < width; x++) { float amplitude = 1 ; float frequency = 1 ; float noiseHeight = 0 ; for (int i = 0 ; i < octaves; i++) { float samplerX = (x - halfWidth) / scale * frequency + octaveOffsets[i].x; float samplerY = (y - halfHeight) / scale * frequency + octaveOffsets[i].y; float perlinValue = Mathf.PerlinNoise(samplerX, samplerY) * 2 - 1 ; noiseHeight += perlinValue * amplitude; amplitude *= persistance; frequency *= lacunarity; } if (noiseHeight > maxNoiseHeight) maxNoiseHeight = noiseHeight; else if (noiseHeight < minNoiseHeight) minNoiseHeight = noiseHeight; noiseMap[x, y] = noiseHeight; } }
其中,Lacunarity表示频率的增加量,Persistence是一个0-1的数,它表示增幅的减小量,也就是后面叠加的波峰的最大高度,octave的个数就代表着地形细节的多少。
然后再根据这个噪点图给一个texture上色,每一个点的颜色则取决于相同大小的噪点图的这个点对应的值,那我就从黑到白做一个lerp,lerp的值就是noiseMap[x, y]存的值。然后我们把这个texture通过renderer组件的sharedMaterial给平面材质的texture。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int width = heightMap.GetLength(0 );int height = heightMap.GetLength(1 );Color[] colorMap = new Color[width * height]; for (int x = 0 ; x < width; x++){ for (int y = 0 ; y < height; y++) { colorMap[y * width + x] = Color.Lerp(Color.black, Color.white, heightMap[x, y]); } } Texture2D texture = new Texture2D(width, height); texture.filterMode = FilterMode.Point; texture.wrapMode = TextureWrapMode.Clamp; texture.SetPixels(colorMap); texture.Apply(); textureRenderer.sharedMaterial.mainTexture = texture; textureRenderer.transform.localScale = new Vector3(texture.width, 1 , texture.height);
这样就可以生成一个可以调整细节的噪声图:
根据高度划分地形 首先建立一个地形数组,根据高度给地形赋予不同的颜色:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Color[] colorMap = new Color[mapHeight * mapWidth]; for (int x = 0 ; x < mapHeight; x++){ for (int y = 0 ; y < mapWidth; y++) { float currentHeight = generatedNoise[x, y]; for (int i = 0 ; i < regions.Length; i++) { if (currentHeight<=regions[i].height) { colorMap[y * mapWidth + x] = regions[i].color; break ; } } } }
然后用同样的方法画到一个texture上:
可以看到已经得到了一个类似的2D的地形图片
储存Mesh,LOD,赋予高度 3D地形的生成主要是生成mesh,然后对每一个vertex做高度的变化,对于一个mesh我们需要存储它的uv、vetices和normals。而具体来说就是存储组成这个mash的三角形的过程,每个三角形都包含了每个顶点的坐标,顶点顺序,法线,uv坐标,顶点颜色。现在每一个像素点就是一个三角形的顶点,每三年个顶点就决定了一个三角形,就要想办法把三角形存储起来。
我使用了一个数组去存所有的三角形,采用顺时针的方式
所有三角形的数量为 (width - 1)* (h - 1)* 2,所以需要储存的三角形顶点为(width - 1)* (h - 1)* 6,意味着需要开一个这么大的数组去存这些有序顶点。
我用一个MeshData类去存储一个Mesh需要的数据:
1 2 3 4 5 public Vector3[] vertices;public int [] triangles;public Vector2[] uvs;int triangleIndex;
这个类包含了创建一个Mesh的初始化数据,以及储存三角形顶点的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public MeshData (int meshWidth, int meshHeight ) { vertices = new Vector3[meshWidth * meshHeight]; uvs = new Vector2[meshWidth * meshHeight]; triangles = new int [(meshWidth - 1 ) * (meshHeight - 1 ) * 6 ]; } public void StoreTriangles (int a, int b,int c ) { triangles[triangleIndex] = a; triangles[triangleIndex + 1 ] = b; triangles[triangleIndex + 2 ] = c; triangleIndex += 3 ; } public Mesh CreateMesh ( ) { Mesh mesh = new Mesh(); mesh.vertices = vertices; mesh.triangles = triangles; mesh.uv = uvs; mesh.RecalculateNormals(); return mesh; }
储存三角形的方法为:
也就是:
1 2 meshData.StoreTriangles(vertexIndex, vertexIndex + verticesPerLine + 1 , vertexIndex + verticesPerLine); meshData.StoreTriangles(vertexIndex + verticesPerLine + 1 , vertexIndex, vertexIndex + 1 );
然后实现高低起伏的地形,我们使用heightMultiplier这个参数去乘每个顶点的y值,大小就由噪点的值确定,为了更好地控制地形,避免低的海面也被乘上这个数值,我是用了一个curve去进行控制,curve的参数就是一个0~1的数,某个值以下就置0,这样y就不会被heightMultiplier影响。
然后我们去实现mesh的细分程度,我们要可以自己去调变三角面的个数,也就是地形的细分程度。正常情况下在遍历所有顶点时,是用++,如果我们每次加2,或者说每次跨度是宽度的一个因数,那么这个跨度越大,存下来的三角面就越大,也就是说它的level of detail越低。而受限于unity支持的顶点数65000,使用的plane最大为254×254,我选择使用241大小的正方形,因为它的因数较多,能有更多的跨度,也即是level数:
核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 int width = heightMap.GetLength(0 );int height = heightMap.GetLength(1 );float leftmostX = (width - 1 ) / -2f ;float rightmostZ = (height - 1 ) / 2f ;int simplificationIncreament = (levelOfDetail == 0 ) ? 1 : levelOfDetail * 2 ;int verticesPerLine = (width - 1 ) / simplificationIncreament + 1 ;MeshData meshData = new MeshData(width, height); int vertexIndex = 0 ;for (int y = 0 ; y < height; y += simplificationIncreament){ for (int x = 0 ; x < width; x += simplificationIncreament) { meshData.vertices[vertexIndex] = new Vector3(leftmostX + x, heightCurve.Evaluate(heightMap[x, y]) * heightMultiplier, rightmostZ - y); meshData.uvs[vertexIndex] = new Vector2(x / (float )width, y / (float )height); if (x < width - 1 && y < height - 1 ) { meshData.StoreTriangles(vertexIndex, vertexIndex + verticesPerLine + 1 , vertexIndex + verticesPerLine); meshData.StoreTriangles(vertexIndex + verticesPerLine + 1 , vertexIndex, vertexIndex + 1 ); } vertexIndex++; } }
创建mesh之后通过meshFilter.sharedMesh = meshData.CreateMesh()把mesh赋给目标就ok。
不同的level of detail: