Unity 六边形地图系列(十四) : 地形纹理
翻译:王成林(麦克斯韦的麦斯威尔 ) 审校:黄秀美(厚德载物)
使用顶点颜色设计一个splat贴图。
创建一个纹理数组资源
在网格中加入地形索引
混合不同的地形纹理
此教程为六边形地图教程系列的第十四篇。到目前为止,我们都是使用纯色为地图着色。现在我们将使用纹理代替它。
使用纹理着色
1 混合三种类型的纹理
虽然单一颜色非常清楚,可以使地图正常显示,不过它们显得很无趣。将其替换为纹理会极大地提升地图的吸引力。当然,这需要我们对纹理进行混合来取代纯色。渲染3,,融合纹理教程展示了如何使用splat贴图混合多个纹理。我们可以为六边形地图采用类似的方法。
渲染3教程只混合了四个纹理,对一张splat贴图至多支持五个。我们当前使用了五种不同的颜色,所以可以直接使用。不过我们也许在后面想要加入更多的类型。所以我们应该支持任意数量的地形类型。不能使用固定数量的纹理属性。因此我们必须使用纹理数组来代替它。我们在后面将建立一个。
当使用纹理数组时,我们需要以某种方式告诉着色器要混合哪些纹理。角部三角形所需的混合最复杂,因为它可能位于三个拥有不同地形类型的单元格之间。因此我们必须支持三种不同类型之间的混合。
1.1 使用顶点颜色作为Splat贴图
假设我们可以通知着色器要混合哪些纹理,我们可以使用顶点颜色为每个三角形建立一个splat贴图。由于一次最多有三个纹理,我们仅需要三个颜色通道。红色代表第一个纹理,绿色对应第二个,蓝色对应第三个。
Splat贴图三角形
Splat贴图三角形相加和总为1吗?
是的。三个颜色通道在三角形的表面定义了一个三线性插值。它们起到了重心坐标的作用。例如,在角部有三种(1,0,0)可能的排列,在边上的中点处有三种(½, ½, 0) 的变化,中心为(⅓, ⅓, ⅓)。
如果一个三角形只需要一个纹理,我们就使用第一个通道。所以它的颜色为纯红色。如果要混合两个不同的类型,我们就使用第一个和第二个通道,那样三角形的颜色即为红色和绿色的混合。而如果三种类型都有,那么就是红绿蓝的混合。
三种splat贴图布局
无论实际上混合的是哪些纹理,我们使用的都是这些splat贴图配置,所以splat贴图总是相同的。改变的是纹理。稍后我们会讲到如何改变纹理。
我们需要修改HexGridChunk使其创建这些splat贴图,而不是使用单元格的颜色。因为我们将多次使用这三种颜色,所以将它们设置为静态变量。
1 2 3 | staticColor color1 = newColor(1f, 0f, 0f); staticColor color2 = newColor(0f, 1f, 0f); staticColor color3 = newColor(0f, 0f, 1f); |
1.2 单元格的中心
首先替换默认单元格中心的颜色。这一步骤没有混合处理,所以我们简单地使用第一个颜色,即红色。
1 2 3 4 5 6 7 | void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, color1); … } |
红色的单元格中心
单元格的中心现在为红色了。它们都使用了三个纹理中的第一个,无论那个纹理是什么。它们的splat贴图完全相同,无论你使用什么颜色为单元着色。
1.3 靠近河流的部分
我们只改变了没有河流经过的单元格。对于靠近河流的单元格也要这样做。这种情况下,它既是边上的条状又是扇状。同样我们只需要红色。
1 2 3 4 5 6 7 8 9 10 | void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateEdgeStrip(m, color1, e, color1); TriangulateEdgeFan(center, m, color1); … } |
靠近河流的红色部分
1.4 河流
接下来我们处理单元格内部的河流形状。它们也都应为红色。首先是河流的开始和结束部分:
1 2 3 4 5 6 7 8 9 10 | void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateEdgeStrip(m, color1, e, color1); TriangulateEdgeFan(center, m, color1); … } |
然后是构成河床和河道的部分。另外我还将颜色方法的调用放在了一起以便代码更加清晰易读:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateEdgeStrip(m, color1, e, color1); terrain.AddTriangle(centerL, m.v1, m.v2); // terrain.AddTriangleColor(cell.Color); terrain.AddQuad(centerL, center, m.v2, m.v3); // terrain.AddQuadColor(cell.Color); terrain.AddQuad(center, centerR, m.v3, m.v4); // terrain.AddQuadColor(cell.Color); terrain.AddTriangle(centerR, m.v4, m.v5); // terrain.AddTriangleColor(cell.Color); terrain.AddTriangleColor(color1); terrain.AddQuadColor(color1); terrain.AddQuadColor(color1); terrain.AddTriangleColor(color1); … } |
穿过单元格的红色河流
1.5 边部
边部是不同的,因为它们在两个单元格之间,而这两个单元格的地形类型可能不一样。我们对当前单元类型使用第一个颜色,对它邻居的类型使用第二个颜色。因此splat贴图是一个红-绿梯度,即使两个单元的类型恰巧相同。如果两个单元格使用相同的纹理,那么它就会简化为相同纹理的混合。
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 | void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor, hasRoad); } else { TriangulateEdgeStrip(e1, color1, e2, color2, hasRoad); } … } |
红-绿边,阶地除外
红绿之间的硬过渡会成为问题吗?
虽然边部的过渡是从红色到绿色,但是两侧的单元中心都是红色。所以在边的一侧会出现不连贯的地方。但是,这仅是splat贴图的问题。相邻三角形的颜色不需要连接到同一纹理。这里,一侧绿色对应的内容在另一侧对应着红色。
注意如果三角形共用顶点这不就成立了。
阶地的边部就更加复杂了,因为它们有额外的顶点。幸运的是,现有的插值代码也适用于splat贴图颜色。直接使用第一个和第二个颜色来代替开始和结束的单元格的颜色。
1 2 3 4 5 6 7 8 9 10 11 | void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(color1, color2, 1); TriangulateEdgeStrip(begin, color1, e2, c2, hasRoad); for ( int i = 2; i |
红-绿边部阶地
1.6 角部
单元格的颜色非常复杂,因为它们需要混合三种不同的纹理。我们对底部顶点使用红色,对左边顶点使用绿色,对右边顶点使用蓝色。我们首先从单一三角形角部开始。
1 2 3 4 5 6 7 8 9 10 11 12 13 | void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … else { terrain.AddTriangle(bottom, left, right); terrain.AddTriangleColor(color1, color2, color3); } features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); } |
红-绿-蓝角部,阶地除外
同上,我们可以为带有阶地的角部使用现有的颜色插值代码。只不过是在三个颜色之间插值,而不是两个。首先是不靠近陡坡的阶地。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color c3 = HexMetrics.TerraceLerp(color1, color2, 1); Color c4 = HexMetrics.TerraceLerp(color1, color3, 1); terrain.AddTriangle(begin, v3, v4); terrain.AddTriangleColor(color1, c3, c4); for ( int i = 2; i |
红-绿-蓝角部阶地,陡坡除外
当需要考虑陡坡时,我们必须使用TriangulateBoundaryTriangle方法。该方法之前将开始单元格和左侧单元格作为参数。但是我们现在需要相关的splat颜色,它可能会根据不同的拓扑而有所差异。所以我们将这些参数改为颜色。
1 2 3 4 5 6 7 8 9 10 11 12 | void TriangulateBoundaryTriangle ( Vector3 begin, Color beginColor, Vector3 left, Color leftColor, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color c2 = HexMetrics.TerraceLerp(beginColor, leftColor, 1); terrain.AddTriangleUnperturbed(HexMetrics.Perturb(begin), v2, boundary); terrain.AddTriangleColor(beginColor, c2, boundaryColor); for ( int i = 2; i |
调整TriangulateCornerTerracesCliff使它使用正确的颜色。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … Color boundaryColor = Color.Lerp(color1, color3, b); TriangulateBoundaryTriangle( begin, color1, left, color2, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); } } |
对TriangulateCornerCliffTerraces也这样做。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … Color boundaryColor = Color.Lerp(color1, color2, b); TriangulateBoundaryTriangle( right, color3, begin, color1, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); } } |
完整的地形splat贴图
2 纹理数组
现在我们的地形已经有了一张splat贴图,接下来我们必须为着色器提供多个纹理。我们不能直接为着色器分配一个C#数组,因为数组在GPU内存中必须作为一个单独的实体存在。我们必须使用一个特殊的Texture2DArray对象,Unity从5.4版本开始支持该对象。
所有的GPU都支持纹理数组吗?
现代GPU都支持,但是旧一些的GPU以及一些移动端的GPU不支持。根据Unity官方文档,这是所有支持平台的名单:
· Direct3D 11/12(Windows,XboxOne)
· OpenGLCore(MacOSX,Linux)
· Metal(iOS,MacOSX)
· OpenGLES 3.0(Android,iOS,WebGL 2.0)
· PlayStation 4
2.1 向导
不幸的是,Unity5.5版本中编辑器对于纹理数组的支持是很有限的。我们不能简单地创建一个纹理数组资源然后为其分配纹理。我们需要手动完成。我们可以在播放模式中创建一个纹理数组,或者在编辑器中创建一个资源。让我们创建一个资源吧。
为什么创建一个资源?
使用资源的优势在于我们不需要在播放模式中再花时间创建纹理数组了。我们不需要在构建中包含单独的纹理,只复制它们然后不再使用了。
劣势是自定义资源是固定的。Unity不会根据构建目标自动改变它的纹理格式。所以你需要确保使用正确的纹理格式创建资源,并当你需要一个不同的格式时要手动重新创建。当然,你可以使用一个构建脚本自动完成这点。
为了创建我们的纹理数组,我们要构建一个自定义向导。新建一个TextureArrayWizard脚本并将其放在Editor文件夹中。它应拓展UnityEditor命名空间中的ScriptableWizard类型,而不是MonoBehavior。
1 2 3 4 5 | using UnityEditor; using UnityEngine; publicclassTextureArrayWizard : ScriptableWizard { } |
我们可以使用静态类方法ScriptableWizard.DisplayWizard打开我们的向导。它的参数为向导窗口的名称和它的创建按钮。在静态方法CrateWizard中调用该方法。
1 2 3 4 5 | staticvoid CreateWizard () { ScriptableWizard.DisplayWizard<texturearraywizard>( "Create Texture Array" , "Create" ); }</texturearraywizard> |
为了在编辑器中使用该向导,我们需要将该方法添加到Unity的菜单中。通过将MenuItem属性添加到方法中,我们可以实现该功能。我们将其加入到Assets菜单中,即Assets/Create/TextureArray。
1 2 3 4 | [MenuItem( "Assets/Create/Texture Array" )] staticvoid CreateWizard () { … } |
我们的自定义向导
使用新的菜单项目,我们得到了我们自定义向导的弹出窗口。它不太好看,不过它完成了任务。但它现在还是空的。为了创建纹理数组,我们需要一组纹理。为此在向导中添加一个公有域。默认的向导GUI会显示它,就像一个默认的审查器(inspector)那样。
1 | publicTexture2D[] textures; |
带有纹理的向导
2.2 建立一些内容
当你按下向导的创建按钮时,它会消失。另外,Unity会警告说没有发现OnWizardCreate方法。当创建按钮被按下时该方法会被调用,所以我们应该将其添加到我们的向导中。
1 2 3 | void OnWizardCreate () { } |
如果用户在向导中添加了至少一个纹理的话,我们在这里创建纹理数组。如果没有的话,那么我们无需创建任何内容,所以我们应终止它。
1 2 3 4 5 6 7 8 9 | void OnWizardCreate () { if (textures.Length == 0) { return ; } } |
下一步是询问用户将纹理数组资源存在哪里。我们可以使用EditorUtility.SaveFilePanelInProject方法打开一个保存文件面板。它的参数决定了面板名称,默认文件名称,文件拓展名,以及描述信息。纹理数组使用一般的.asset拓展名。
1 2 3 4 5 6 | if (textures.Length == 0) { return ; } EditorUtility.SaveFilePanelInProject( "Save Texture Array" , "Texture Array" , "asset" , "Save Texture Array" ); |
SaveFilePanelInProject返回了用户选择的文件路径。如果用户取消了面板,那么路径会变为空字符串,那种情况我们应该终止。
1 2 3 4 5 6 | string path =EditorUtility.SaveFilePanelInProject( "Save Texture Array" , "Texture Array" , "asset" , "Save Texture Array" ); if (path.Length == 0) { return ; } |
2.3 创建一个纹理数组
如果我们得到了一个有效的路径,我们可以继续创建一个·新的Texture2DArray对象。它的构造函数方法需要纹理的宽度和高度,数组的长度,纹理格式,以及是否有mipmap。这些设置项对于数组中的所有纹理都应是相同的。我们将使用第一个纹理来配置对象。用户应确保所有的纹理都有相同的格式。
1 2 3 4 5 6 7 8 | if (path.Length == 0) { return ; } Texture2D t = textures[0]; Texture2DArray textureArray = newTexture2DArray( t.width, t.height, textures.Length, t.format, t.mipmapCount >1 ); |
由于纹理数组是一个单一GPU资源,它对所有的纹理都使用相同的过滤器和循环模式。同上,我们还使用第一个纹理进行配置。
1 2 3 4 5 6 | Texture2DArray textureArray = newTexture2DArray( t.width, t.height, textures.Length, t.format, t.mipmapCount >1 ); textureArray.anisoLevel = t.anisoLevel; textureArray.filterMode = t.filterMode; textureArray.wrapMode = t.wrapMode; |
现在我们可以使用Graphics.CopyTexture方法将纹理复制到数组中。该方法复制了原始纹理数据,一次一个mip级别。所以我们必须对所有的纹理以及它们的mip级别进行循环。该方法的参数为两个集合,包括一个纹理资源,索引和mip级别。由于源纹理不是数组,它们的索引总为0。
1 2 3 4 5 6 7 | textureArray.wrapMode = t.wrapMode; for ( int i = 0; i < textures.Length; i ) { for ( int m = 0; m < t.mipmapCount; m ) { Graphics.CopyTexture(textures[i], 0, m, textureArray, i, m); } } |
现在我们在内存中有一个有效的纹理数组了,但是它还不是资源。最后一步是使用数组和它的路径调用AssetDatabase.CreateAsset。这会将数据写入我们项目的一个文件中,它会出现在项目窗口中。
1 2 3 4 5 | for ( int i = 0; i < textures.Length; i ) { … } AssetDatabase.CreateAsset(textureArray, path); |
2.4 纹理
为了实际创建一个纹理数组,我们需要源纹理。这五张纹理对应着我们到目前为止使用的颜色。黄色变为沙子,绿色变为草,蓝色变为泥土,橙色变为石头,白色变为雪。
沙子,草,泥土,石头,和雪的纹理
注意这些纹理不是真实地形的照片。它们的图案为伪随机图案,我使用了NumberFlow创建它们。我的目标是做出能使人识别出的地形类型及细节,同时不与抽象多面体地形发生冲突。超级现实主义使人感觉不适用于此。另外,虽然那些(指超现实主义)图案加入了多样性,但是它们缺少别人一眼能将它们区分出来的明显的特征。
将这些纹理添加到向导的数组中,确保它们的顺序和颜色相对应。首先是沙子,然后是草,泥土,石头,最后是雪。
创建纹理数组
在创建纹理数组资源后,选择它并在检视面板中为它加入一个外观。
纹理数组检视面板
这里展示了纹理数组中的一些数据。注意这里有一个IsReadable复选框,初始是开启的。由于不需要从数组中读取像素数据,我们关闭它。我们不能通过向导这样做,因为Texture2DArray没有任何方法或者属性可以处理该设置。
另外注意有一个ColorSpace域,它被设置为1。这意味着我们假设纹理是在gamma空间中的,这是我们想要的。如果它们需要在线性空间中,我们应将该值设为0。Texture2DArray的构建函数实际上有一个额外的参数可以设置颜色空间,但是Texture2D不会显示它是否在线性空间中,所以无论如何你都要自己设置它。
2.5 着色器
由于我们已经有了纹理数组了,是时候制作使用它的着色器了。当前我们使用VertexColors着色器渲染我们的地形。由于我们将使用纹理代替颜色,我们将其重命名为Terrain。然后将它的_MainTex参数改为一个纹理数组,然后为其分配我们的资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | Shader "Custom/Terrain" { Properties { _Color ( "Color" , Color) = (1,1,1,1) _MainTex ( "Terrain Texture Array" , 2DArray) = "white" {} _Glossiness ( "Smoothness" , Range(0,1)) = 0.5 _Metallic ( "Metallic" , Range(0,1)) = 0.0 } … } |
带有纹理数组的地形材料
为了在所有支持纹理数组的平台上使用它,我们需要将着色器目标等级从3.0调整到3.5。
1 | #pragma target 3.5 |
由于_MainTex变量现在引用了一个纹理数组,我们需要改变它的类型。准确的类型取决于目标平台,UNITY_DECLARE_TEXT2DARRAY宏负责搞定这一点。
1 2 3 | // sampler2D _MainTex; UNITY_DECLARE_TEX2DARRAY(_MainTex); |
和其它着色器一样,我们需要XZ世界坐标对我们的地形纹理进行取样。因此将世界坐标添加到表面着色器输入结构体中。另外删除默认的UV坐标,因为我们不需要它们。
1 2 3 4 5 6 7 8 9 | structInput { // float2 uv_MainTex; float4 color : COLOR; float3 worldPos; }; |
为了对纹理数组进行取样,我们必须使用UNITY_SAMPLE_TEXT2DARRAY宏。它需要三个坐标对数组进行取样。前两个为普通的UV坐标。我们将使用缩放为0.02的世界XZ坐标。这样当镜头完全拉近时,我们会得到一个不错的纹理分辨率,大约每四个单元格组成一个纹理。
第三个坐标被用来标记纹理数组的索引,就像一个普通的数组那样。由于坐标为浮点型数,GPU在标记数组索引前对它们进行取整。由于我们还不知道需要哪张纹理,让我们永远使用第一张。另外,不要再对最终结果乘以顶点颜色,splat贴图才会那样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | void surf (Input IN, inoutSurfaceOutputStandard o) { float2 uv = IN.worldPos.xz * 0.02; fixed4 c = UNITY_SAMPLE_TEX2DARRAY(_MainTex, float3(uv, 0)); Albedo = c.rgb * _Color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = c.a; } |
一切都是沙子
3 选择纹理
我们有一个地形splat贴图,它在每个三角形中混合了三种类型。我们有一个含有每种地形纹理的纹理数组。我们有一个可以对纹理数组取样的着色器。但是我们还没有手段来通知着色器在每个三角形中选择哪些纹理。
由于每个三角形至多混合三种类型,我们需要为每个三角形关联三个索引。由于我们不能保存每个三角形的信息,我们将保存每个顶点的索引。三角形的所有三个顶点都只会保存相同的索引,就像一个纯色那样。
3.1 网格数据
我们可以使用网格的UV集中的一个来存储索引。因为每个顶点有三个索引,现有的2D UV集不够用。幸运的是,UV集可以包括至多四个坐标。所以我们向HexMesh添加第二个Vector3列表,我们使用它代表地形类型。
1 2 3 4 | publicbool useCollider, useColors, useUVCoordinates, useUV2Coordinates; publicbool useTerrainTypes; [NonSerialized] List<vector3> vertices, terrainTypes;</vector3> |
在HexGridChunk预设体的Terrain子对象中开启地形类型。
使用地形类型
如果需要的话,当清除网格时我们可以为地形类型获取另一个Vector3列表。
1 2 3 4 5 6 7 | publicvoid Clear () { … if (useTerrainTypes) { terrainTypes = ListPool<vector3>.Get(); } triangles = ListPool< int >.Get(); }</ int ></vector3> |
当我们应用网格数据时,将地形类型存储在第三个UV集中。那样的话即便我们一起使用它们,它也不会和另外两个集发生冲突了。
1 2 3 4 5 6 7 8 9 | publicvoid Apply () { … if (useTerrainTypes) { hexMesh.SetUVs(2, terrainTypes); ListPool<vector3>.Add(terrainTypes); } hexMesh.SetTriangles(triangles, 0); … }</vector3> |
我们使用一个Vector3设置三角形的地形类型。由于它们在三角形上是统一的,我们只需添加三次同样的数据。
1 2 3 4 5 | publicvoid AddTriangleTerrainTypes (Vector3 types) { terrainTypes.Add(types); terrainTypes.Add(types); terrainTypes.Add(types); } |
我们采用同样的方法在四边形中混合。所有四个顶点都有相同的类型。
1 2 3 4 5 6 | publicvoid AddQuadTerrainTypes (Vector3 types) { terrainTypes.Add(types); terrainTypes.Add(types); terrainTypes.Add(types); terrainTypes.Add(types); } |
3.2边上的扇形部分
现在我们需要在HexGridChunk中的网格数据中加入类型。我们从TriangulateEdgeFan开始。首先,为了代码的易读性,我们将顶点和颜色的方法调用区分开来。另外记得在所有该方法的调用中都要提供color1。这样我们可以直接使用该颜色,而不需要依靠参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, Color color) { terrain.AddTriangle(center, edge.v1, edge.v2); // terrain.AddTriangleColor(color); terrain.AddTriangle(center, edge.v2, edge.v3); // terrain.AddTriangleColor(color); terrain.AddTriangle(center, edge.v3, edge.v4); // terrain.AddTriangleColor(color); terrain.AddTriangle(center, edge.v4, edge.v5); // terrain.AddTriangleColor(color); terrain.AddTriangleColor(color1); terrain.AddTriangleColor(color1); terrain.AddTriangleColor(color1); terrain.AddTriangleColor(color1); } |
在颜色之后我们加入地形类型。由于每个三角形的类型都不一样,我们应该使用一个参数代替颜色。使用这个单一类型来构建一个Vector3。我们只在乎第一个通道,因为在这种情况中splat贴图永远为红色。由于三个向量分量都应被设置为某个值,我们将它们都设置为相同的类型。
1 2 3 4 5 6 7 8 9 10 | void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, float type) { … Vector3 types; types.x = types.y = types.z = type; terrain.AddTriangleTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); } |
现在我们必须调整所有该方法的调用,要将颜色参数替换成单元格的地形类型索引。在TriangulateWithoutRiver,TriangulateAdjacentToRiver,和TriangulateWithRiverBeginOrEnd中做这项调整。
1 2 3 | // TriangulateEdgeFan(center, e, color1); TriangulateEdgeFan(center, e, cell.TerrainTypeIndex); |
目前为止,进入播放模式会报错,称网格的第三个UV集越界了。这是因为我们还没有将地形类型添加到所有三角形和四边形中。那么让我们继续更新HexGridChunk。
3.3 边上的条形部分
当创建一个边上的条形部分时,我们需要知道两侧的地形都是什么类型。因此将它们添加到参数中。然后构建一个类型向量,将它的前两个通道设置为这些类型。第三个通道无所谓,只需使它等于第一个即可。在添加颜色后将类型添加到四边形中。
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 | void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, float type1, EdgeVertices e2, Color c2, float type2, bool hasRoad = false ) { terrain.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); terrain.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); terrain.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); terrain.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); terrain.AddQuadColor(c1, c2); terrain.AddQuadColor(c1, c2); terrain.AddQuadColor(c1, c2); terrain.AddQuadColor(c1, c2); Vector3 types; types.x = types.z = type1; types.y = type2; terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); if (hasRoad) { TriangulateRoadSegment(e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4); } } |
现在我们必须更新TriangulateEdgeStrip的调用。首先,TriangulateAdjacentToRiver,TriangulateWithRiverBeginOrEnd以及TriangulateWithRiver必须对边上条形部分的两侧都使用单元格的类型。
1 2 3 4 5 6 7 8 9 | // TriangulateEdgeStrip(m, color1, e, color1); TriangulateEdgeStrip( m, color1, cell.TerrainTypeIndex, e, color1, cell.TerrainTypeIndex ); |
接下来,TriangulateConnections最简单的边部情况必须对近边使用单元格的类型,对远边使用邻居的类型。它们也许是相同的,但也可能不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor, hasRoad); } else { // TriangulateEdgeStrip(e1, color1, e2, color2, hasRoad); TriangulateEdgeStrip( e1, color1, cell.TerrainTypeIndex, e2, color2, neighbor.TerrainTypeIndex, hasRoad ); } … } |
在TriangulateEdgeTerraces中也是这样,它调用了TriangulateEdgeStrip三次。在阶地中这些类型是统一的。
1 2 3 4 5 6 7 8 9 10 11 12 13 | void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(color1, color2, 1); float t1 = beginCell.TerrainTypeIndex; float t2 = endCell.TerrainTypeIndex; TriangulateEdgeStrip(begin, color1, t1, e2, c2, t2, hasRoad); for ( int i = 2; i |
3.4 角部
最简单的角部情况是一个单一的三角形。底部单元格提供了第一种类型,左边单元格提供第二种,右边是第三种。使用它们构建一个名为types的向量,并将其添加到三角形中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … else { terrain.AddTriangle(bottom, left, right); terrain.AddTriangleColor(color1, color2, color3); Vector3 types; types.x = bottomCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; terrain.AddTriangleTerrainTypes(types); } features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); } |
我们在TriangulateCornerTerraces中使用同样的方法,此外我们还创建了一些四边形。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color c3 = HexMetrics.TerraceLerp(color1, color2, 1); Color c4 = HexMetrics.TerraceLerp(color1, color3, 1); Vector3 types; types.x = beginCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; terrain.AddTriangle(begin, v3, v4); terrain.AddTriangleColor(color1, c3, c4); terrain.AddTriangleTerrainTypes(types); for ( int i = 2; i |
当阶地和陡坡混合时,我们需要使用TriangulateBoundaryTriangle。直接给它一个名为types的向量参数,并将其加入到它所有的三角形中。
1 2 3 4 5 6 7 8 9 10 11 12 13 | void TriangulateBoundaryTriangle ( Vector3 begin, Color beginColor, Vector3 left, Color leftColor, Vector3 boundary, Color boundaryColor, Vector3 types ) { Vector3 v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color c2 = HexMetrics.TerraceLerp(beginColor, leftColor, 1); terrain.AddTriangleUnperturbed(HexMetrics.Perturb(begin), v2, boundary); terrain.AddTriangleColor(beginColor, c2, boundaryColor); terrain.AddTriangleTerrainTypes(types); for ( int i = 2; i |
在TriangulateCornerTerracesCliff中,使用给定的单元格创建一个名为types的向量。然后将其添加到单个三角形中并将其传递给TriangulateBoundaryTriangle。
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 28 29 30 31 32 33 34 35 | void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); if (b <0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(right), b ); Color boundaryColor = Color.Lerp(color1, color3, b); Vector3 types; types.x = beginCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; TriangulateBoundaryTriangle( begin, color1, left, color2, boundary, boundaryColor, types ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor, types ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); terrain.AddTriangleTerrainTypes(types); } } |
对TriangulateCornerCliffTerraces也进行同样处理。
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 28 29 30 31 32 33 34 35 | void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); if (b <0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(left), b ); Color boundaryColor = Color.Lerp(color1, color2, b); Vector3 types; types.x = beginCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; TriangulateBoundaryTriangle( right, color3, begin, color1, boundary, boundaryColor, types ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor, types ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); terrain.AddTriangleTerrainTypes(types); } } |
3.5 河流
最后一个需要更新的方法是TriangulateWithRiver。这里我们是在单元中心内部的位置,因此我们只处理当前单元格的类型。因此我们创建一个向量并将其加入到三角形和四边形中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … terrain.AddTriangleColor(color1); terrain.AddQuadColor(color1); terrain.AddQuadColor(color1); terrain.AddTriangleColor(color1); Vector3 types; types.x = types.y = types.z = cell.TerrainTypeIndex; terrain.AddTriangleTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); … } |
3.6 混合各种类型
至此网格已经包含了所需的地形索引。剩下的就是让Terrain着色器实际使用它们了。为了让片段着色器使用索引,我们需要先通过顶点着色器传递它们。为此我们可以使用一个自定义顶点函数,就像我们在Estuary着色器中那样。在这种情况中,我们在输入结构体中添加一个float3名为terrain的域并将v.texcoord2.xyz复制给它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.5 … structInput { float4 color : COLOR; float3 worldPos; float3 terrain; }; void vert (inoutappdata_full v, outInput data) { UNITY_INITIALIZE_OUTPUT(Input, data); data.terrain = v.texcoord2.xyz; } |
在每个片段中我们需要对纹理数组取三次样。所以让我们创建一个简便的函数来构建纹理坐标,对数组取样,并对一个索引使用splat贴图调节样本。
1 2 3 4 5 6 7 8 9 | float4 GetTerrainColor (Input IN, int index) { float3 uvw = float3(IN.worldPos.xz * 0.02, IN.terrain[index]); float4 c = UNITY_SAMPLE_TEX2DARRAY(_MainTex, uvw); return c * IN.color[index]; } void surf (Input IN, inoutSurfaceOutputStandard o) { … } |
你可以将向量当作数组吗?
是的。当使用一个常量索引时,color[0]等价于color.r。color[1]等价于color.g等等。
使用该函数,我们可以轻松地对纹理数组进行三次取样并合并结果。
1 2 3 4 5 6 7 8 9 10 11 | void surf (Input IN, inoutSurfaceOutputStandard o) { // float2 uv = IN.worldPos.xz * 0.02; fixed4 c = GetTerrainColor(IN, 0) GetTerrainColor(IN, 1) GetTerrainColor(IN, 2); o.Albedo = c.rgb * _Color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = c.a; } |
使用纹理绘制的地形
我们现在可以使用纹理绘制地形了。它们混合的方式就像纯色那样。因为我们使用了世界位置作为UV坐标,它们不会随着高度变化而改变。因此纹理会沿着陡坡的斜面被拉伸。如果纹理足够素净,且有大量的多样性,结果还是可以接受的。否则你会得到巨大的丑陋的条纹。你可以试试使用额外的几何体或者陡坡纹理隐藏它,但这不属于这篇教程的范畴。
3.7 收尾
由于我们使用纹理代替颜色,我们应该调整一下编辑器面板。你可以制作一个整洁的界面,它甚至会显示地形纹理,但是我还是按照当前布局简单一些。
地形选项
另外,HexCell不再需要颜色属性了,所以将它删除,
1 2 3 4 5 | // public Color Color { // get { // return HexMetrics.colors[terrainTypeIndex]; // } // } |
还可以将颜色数组和相关代码从HexGrid中删除。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // public Color[] colors; … voidAwake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); // HexMetrics.colors = colors; CreateMap(cellCountX, cellCountZ); } … … voidOnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); // HexMetrics.colors = colors; } } |
最后,HexMetrics也不再需要颜色数组了。
1 | // public static Color[] colors; |
下一篇教程是距离。
【版权声明】
原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权。