第1-3部分:网格,颜色和像元高度第4-7部分:颠簸,河流和道路第8-11部分:水,地貌和城墙第12-15部分:保存和加载,纹理,距离第16-19部分:找到道路,队员,动画第20-23部分:战争迷雾,地图研究,程序生成第24-27部分:水循环,侵蚀,生物群落,圆柱图第8部分:水
- 加水到细胞。
- 对水表面进行三角剖分。
- 用泡沫制作冲浪。
- 结合水和河流。
我们已经增加了河流支持,在这一部分中,我们将把细胞完全浸入水中。
水来了。水位
最简单的方法是通过将其设置在同一级别来实现水支持。 所有高度低于此水平的电池都将浸入水中。 但是,更灵活的方法是将水保持在不同的高度,因此让我们更改水位。 为此,
HexCell
需要监控其水位。
public int WaterLevel { get { return waterLevel; } set { if (waterLevel == value) { return; } waterLevel = value; Refresh(); } } int waterLevel;
如果需要,您可以确保浮雕的某些功能在水下不存在。 但是现在我不会这样做。 诸如水下道路之类的东西适合我。 它们可以被认为是最近被洪水淹没的地区。
淹没细胞
现在我们有了水位,最重要的问题是细胞是否在水下。 如果单元格的水位高于其高度,则该单元格在水下。 为了获得此信息,我们将添加一个属性。
public bool IsUnderwater { get { return waterLevel > elevation; } }
这意味着当水位和高度相等时,单元会上升到水面之上。 即,水的真实表面低于该高度。 与河流表面一样,让我们添加相同的偏移量
HexMetrics.riverSurfaceElevationOffset
。 将其名称更改为更通用的名称。
更改
HexCell.RiverSurfaceY
,使其使用新名称。 然后,我们向淹没单元的水面添加类似的属性。
public float RiverSurfaceY { get { return (elevation + HexMetrics.waterElevationOffset) * HexMetrics.elevationStep; } } public float WaterSurfaceY { get { return (waterLevel + HexMetrics.waterElevationOffset) * HexMetrics.elevationStep; } }
水编辑
编辑水位类似于更改高度。 因此,
HexMapEditor
必须监视活动水位以及是否应将其应用于单元格。
int activeElevation; int activeWaterLevel; … bool applyElevation = true; bool applyWaterLevel = true;
添加将这些参数与UI连接的方法。
public void SetApplyWaterLevel (bool toggle) { applyWaterLevel = toggle; } public void SetWaterLevel (float level) { activeWaterLevel = (int)level; }
并将水位添加到
EditCell
。
void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (applyWaterLevel) { cell.WaterLevel = activeWaterLevel; } … } }
要将水位添加到UI,请复制标签和高度滑块,然后进行更改。 请记住将其事件附加到适当的方法。
水位滑块。统一包装水三角剖分
要对水进行三角剖分,我们需要使用新材质的新网格。 首先,创建一个
Water着色器,复制
River着色器。 对其进行更改,以使其使用color属性。
Shader "Custom/Water" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags { "RenderType"="Transparent" "Queue"="Transparent" } LOD 200 CGPROGRAM #pragma surface surf Standard alpha #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; }; half _Glossiness; half _Metallic; fixed4 _Color; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = _Color; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } ENDCG } FallBack "Diffuse" }
通过复制“
水”材质并将其替换为着色器,使用此着色器创建新材质。 保留噪波纹理,因为稍后将使用它。
水材料。通过复制
Rivers子代将新子代添加到预制中。 他不需要UV坐标,并且必须使用
Water 。 与往常一样,我们将通过创建预制实例,对其进行更改,然后将所做的更改应用于预制来实现。 在那之后,摆脱实例。
儿童对象水。接下来,向
HexGridChunk
添加水网格支持。
public HexMesh terrain, rivers, roads, water; public void Triangulate () { terrain.Clear(); rivers.Clear(); roads.Clear(); water.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); roads.Apply(); water.Apply(); }
并将其连接到预制子。
水对象已连接。水六边形
由于水形成第二层,因此让我们针对每个方向使用自己的三角剖分方法。 我们仅在将电池浸入水中时才需要调用它。
void Triangulate (HexDirection direction, HexCell cell) { … if (cell.IsUnderwater) { TriangulateWater(direction, cell, center); } } void TriangulateWater ( HexDirection direction, HexCell cell, Vector3 center ) { }
与河流一样,在水位相同的情况下,水面的高度变化也不大。 因此,我们似乎不需要复杂的肋骨。 一个简单的三角形就足够了。
void TriangulateWater ( HexDirection direction, HexCell cell, Vector3 center ) { center.y = cell.WaterSurfaceY; Vector3 c1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 c2 = center + HexMetrics.GetSecondSolidCorner(direction); water.AddTriangle(center, c1, c2); }
水的六边形。水化合物
我们可以将相邻的单元格与一个四边形的水相连。
water.AddTriangle(center, c1, c2); if (direction <= HexDirection.SE) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null || !neighbor.IsUnderwater) { return; } Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 e1 = c1 + bridge; Vector3 e2 = c2 + bridge; water.AddQuad(c1, c2, e1, e2); }
水的边缘的连接。并用一个三角形填充角。
if (direction <= HexDirection.SE) { … water.AddQuad(c1, c2, e1, e2); if (direction <= HexDirection.E) { HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor == null || !nextNeighbor.IsUnderwater) { return; } water.AddTriangle( c2, e2, c2 + HexMetrics.GetBridge(direction.Next()) ); } }
水角的关节。现在,当水细胞在附近时,我们将它们连接起来。 它们与较高高度的干细胞之间留有间隙,但我们将留给以后使用。
协调水位
我们假设相邻的水下细胞具有相同的水位。 如果是这样,那么一切看起来都很好,但是如果违反了此假设,则会发生错误。
水位不一致。我们可以使水保持在同一水平上。 例如,当淹没单元的水位发生变化时,我们可以将更改传播到相邻单元,以保持水位同步。 但是,此过程应继续进行,直到遇到未浸入水中的细胞为止。 这些单元定义了水团的边界。
这种方法的危险在于它可能很快失控。 如果编辑不成功,则水可能会覆盖整个地图。 然后,所有片段都必须同时进行三角剖分,这将导致延迟的巨大跳跃。
因此,让我们暂时不做。 可以在更复杂的编辑器中添加此功能。 虽然水位的一致性,我们离开了用户的良心。
统一包装水动画
我们将创建类似于波浪的颜色,而不是均匀的颜色。 与其他着色器一样,现在我们将不再努力争取精美的图形,只需要指定波浪即可。
完全平坦的水。让我们做些与河流有关的事情。 我们根据世界的位置对噪声进行采样,并将其添加到统一的颜色中。 要设置曲面动画,请在V坐标上添加时间。
struct Input { float2 uv_MainTex; float3 worldPos; }; … void surf (Input IN, inout SurfaceOutputStandard o) { float2 uv = IN.worldPos.xz; uv.y += _Time.y; float4 noise = tex2D(_MainTex, uv * 0.025); float waves = noise.z; fixed4 c = saturate(_Color + waves); o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
滚动水,时间×10。两个方向
到目前为止,这根本不像波浪。 让我们通过添加第二个噪声样本来使图片复杂化
然后添加U坐标我们使用了一个不同的噪声通道来得到两个不同的模式 完成的波将这两个样本堆叠在一起。
float2 uv1 = IN.worldPos.xz; uv1.y += _Time.y; float4 noise1 = tex2D(_MainTex, uv1 * 0.025); float2 uv2 = IN.worldPos.xz; uv2.x += _Time.y; float4 noise2 = tex2D(_MainTex, uv2 * 0.025); float waves = noise1.z + noise2.x;
当对两个样本求和时,我们得到的结果范围为0–2,因此我们需要将其缩放回0–1。 代替简单地将波分成两半,我们可以使用
smoothstep
函数创建更有趣的结果。 我们将¾– 2放在0–1上,以使水面没有可见波。
float waves = noise1.z + noise2.x; waves = smoothstep(0.75, 2, waves);
两个方向,时间×10。混合浪潮
仍然值得注意的是,我们有两个实际上没有变化的运动噪声模式。 如果模式发生变化,那将更加合理。 我们可以通过在噪声样本的不同通道之间进行插值来实现这一点。 但这不能以相同的方式进行,否则水的整个表面将同时变化,这是非常明显的。 相反,我们将造成一波混乱。
我们将在正弦波的帮助下创建一个混合波,该混合波将沿水表面对角线移动。 为此,我们将世界坐标X和Z相加,并将总和用作
sin
函数的输入。 缩小以获取足够大的波段。 当然,让我们添加相同的值来制作动画。
float blendWave = sin((IN.worldPos.x + IN.worldPos.z) * 0.1 + _Time.y);
正弦波的范围是-1和1,我们需要一个0–1的间隔。 您可以通过平方波来获得它。 要查看隔离的结果,请使用它代替更改的颜色作为输出值。
sin((IN.worldPos.x + IN.worldPos.z) * 0.1 + _Time.y); blendWave *= blendWave; float waves = noise1.z + noise2.x; waves = smoothstep(0.75, 2, waves); fixed4 c = blendWave; //saturate(_Color + waves);
混合浪潮。为了使混合波不那么引人注目,请从两个样本中添加一些噪声。
float blendWave = sin( (IN.worldPos.x + IN.worldPos.z) * 0.1 + (noise1.y + noise2.z) + _Time.y ); blendWave *= blendWave;
扭曲的混合波。最后,我们使用混合波在两个噪声样本的两个通道之间进行内插。 为了获得最大的变化,采用四个不同的通道。
float waves = lerp(noise1.z, noise1.w, blendWave) + lerp(noise2.x, noise2.y, blendWave)
混波,时间×2。统一包装海岸
我们已经完成了开放水域,但现在我们需要填补沿海水域的空白。 由于我们必须符合土地的轮廓,因此沿海水需要采取不同的方法。 让我们将
TriangulateWater
分为两种方法-一种用于开放水域,另一种用于海岸水域。 要了解何时与海岸合作,我们需要查看邻近的单元。 也就是说,在
TriangulateWater
我们将得到一个邻居。 如果有邻居并且他不在水下,那么我们正在与海岸打交道。
void TriangulateWater ( HexDirection direction, HexCell cell, Vector3 center ) { center.y = cell.WaterSurfaceY; HexCell neighbor = cell.GetNeighbor(direction); if (neighbor != null && !neighbor.IsUnderwater) { TriangulateWaterShore(direction, cell, neighbor, center); } else { TriangulateOpenWater(direction, cell, neighbor, center); } } void TriangulateOpenWater ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { Vector3 c1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 c2 = center + HexMetrics.GetSecondSolidCorner(direction); water.AddTriangle(center, c1, c2); if (direction <= HexDirection.SE && neighbor != null) {
沿海没有三角剖分。由于海岸变形,我们必须使海岸的水三角形变形。 因此,我们需要边缘的顶部和三角形的扇形。
void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { EdgeVertices e1 = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); water.AddTriangle(center, e1.v1, e1.v2); water.AddTriangle(center, e1.v2, e1.v3); water.AddTriangle(center, e1.v3, e1.v4); water.AddTriangle(center, e1.v4, e1.v5); }
沿海岸的三角形风扇。接下来是一条肋骨,就像通常的浮雕一样。 但是,我们没有义务将自己仅限于某些区域,因为我们在遇到海岸时只呼叫
TriangulateWaterShore
,而对于该海岸而言,则始终需要该区域。
water.AddTriangle(center, e1.v4, e1.v5); Vector3 bridge = HexMetrics.GetBridge(direction); EdgeVertices e2 = new EdgeVertices( e1.v1 + bridge, e1.v5 + bridge ); water.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); water.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); water.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); water.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5);
肋骨条纹沿海。同样,我们每次也必须添加一个三角形的三角形。
water.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { water.AddTriangle( e1.v5, e2.v5, e1.v5 + HexMetrics.GetBridge(direction.Next()) ); }
沿海的肋骨的角落。现在我们已经为海岸准备了水。 它的一部分始终在浮雕网格下方,因此没有孔。
紫外线海岸
我们可以保留一切,但如果沿海水有自己的时间表,那将很有趣。 例如,泡沫的影响,在靠近海岸时会变得更大。 要实现它,着色器必须知道片段离海岸有多近。 我们可以通过UV坐标传输此信息。
开放水没有紫外线坐标,也不需要泡沫。 仅海岸附近的水才需要。 因此,对两种水的要求都大不相同。 为每种类型创建自己的网格是合乎逻辑的。 因此,我们向HexGridChunk添加了对另一个网格对象的支持。
public HexMesh terrain, rivers, roads, water, waterShore; public void Triangulate () { terrain.Clear(); rivers.Clear(); roads.Clear(); water.Clear(); waterShore.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); roads.Apply(); water.Apply(); waterShore.Apply(); }
这个新的网格将使用
TriangulateWaterShore
。
void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … waterShore.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); waterShore.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); waterShore.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); waterShore.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { waterShore.AddTriangle( e1.v5, e2.v5, e1.v5 + HexMetrics.GetBridge(direction.Next()) ); } }
复制水对象,将其连接到预制件并进行设置,以使其使用UV坐标。 我们还为沿海水创建了一个着色器和材质,复制了现有的着色器和水材质。
水岸设施和紫外线材料。更改“
水岸”着色器,以使其显示UV坐标而不是水。
fixed4 c = fixed4(IN.uv_MainTex, 1, 1)
由于尚未设置坐标,它将显示纯色。 因此,很容易看到海岸实际上使用了带有材料的单独网格。
用于海岸的单独网格。让我们将海岸信息放置在坐标V中。在水侧,将其分配为0,在陆地侧将其分配为-值1。由于我们无需传输其他任何内容,因此所有U坐标都将简单地为0。
waterShore.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); waterShore.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); waterShore.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); waterShore.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { waterShore.AddTriangle( e1.v5, e2.v5, e1.v5 + HexMetrics.GetBridge(direction.Next()) ); waterShore.AddTriangleUV( new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 0f) ); }
向海岸过渡是错误的。上面的代码适用于边缘,但是在某些角度上是错误的。 如果下一个邻居在水下,则此方法将是正确的。 但是,当下一个邻居不在水下时,三角形的第三个峰将在陆地下。
waterShore.AddTriangleUV( new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, nextNeighbor.IsUnderwater ? 0f : 1f) );
过渡到海岸是正确的。海岸上的泡沫
现在已经正确实现了向海岸的过渡,您可以使用它们创建泡沫效果。 最简单的方法是将惯性运动的值添加到统一的颜色。
void surf (Input IN, inout SurfaceOutputStandard o) { float shore = IN.uv_MainTex.y; float foam = shore; fixed4 c = saturate(_Color + foam); o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
线性泡沫。为了使泡沫更有趣,请将其乘以正弦曲线的平方。
float foam = sin(shore * 10); foam *= foam * shore;
褪色正弦方形泡沫。靠近岸边时,让泡沫前端变大。 这可以通过在使用Coast值之前取其平方根来完成。
float shore = IN.uv_MainTex.y; shore = sqrt(shore);
靠近岸边的泡沫变稠。添加变形以使其看起来更自然。 让我们在接近海岸时使变形减弱。 因此,最好沿着海岸走。
float2 noiseUV = IN.worldPos.xz; float4 noise = tex2D(_MainTex, noiseUV * 0.015); float distortion = noise.x * (1 - shore); float foam = sin((shore + distortion) * 10); foam *= foam * shore;
泡沫变形。而且,当然,我们正在为所有这些动画:正弦曲线和扭曲。
float2 noiseUV = IN.worldPos.xz + _Time.y * 0.25; float4 noise = tex2D(_MainTex, noiseUV * 0.015); float distortion = noise.x * (1 - shore); float foam = sin((shore + distortion) * 10 - _Time.y); foam *= foam * shore;
动画泡沫。除了传入的泡沫外,还有一种后退泡沫。 让我们添加第二个正弦曲线来模拟它,它以相反的方向移动。 使其变弱并添加时移。 成品泡沫将是这两个正弦波的最大值。
float distortion1 = noise.x * (1 - shore); float foam1 = sin((shore + distortion1) * 10 - _Time.y); foam1 *= foam1; float distortion2 = noise.y * (1 - shore); float foam2 = sin((shore + distortion2) * 10 + _Time.y + 2); foam2 *= foam2 * 0.7; float foam = max(foam1, foam2) * shore;
进出泡沫。波浪和泡沫的混合
在开放水域和沿海水域之间有一个急剧的过渡,因为开放水波不包括在沿海水域中。 要解决此问题,我们需要在
Water Shore着色器中包括这些波浪。
让我们将其粘贴到
Water.cginc包含文件中,而不是复制波形代码。 实际上,我们在其中插入了用于泡沫和波浪的代码,每个都是独立的功能。
float Foam (float shore, float2 worldXZ, sampler2D noiseTex) { // float shore = IN.uv_MainTex.y; shore = sqrt(shore); float2 noiseUV = worldXZ + _Time.y * 0.25; float4 noise = tex2D(noiseTex, noiseUV * 0.015); float distortion1 = noise.x * (1 - shore); float foam1 = sin((shore + distortion1) * 10 - _Time.y); foam1 *= foam1; float distortion2 = noise.y * (1 - shore); float foam2 = sin((shore + distortion2) * 10 + _Time.y + 2); foam2 *= foam2 * 0.7; return max(foam1, foam2) * shore; } float Waves (float2 worldXZ, sampler2D noiseTex) { float2 uv1 = worldXZ; uv1.y += _Time.y; float4 noise1 = tex2D(noiseTex, uv1 * 0.025); float2 uv2 = worldXZ; uv2.x += _Time.y; float4 noise2 = tex2D(noiseTex, uv2 * 0.025); float blendWave = sin( (worldXZ.x + worldXZ.y) * 0.1 + (noise1.y + noise2.z) + _Time.y ); blendWave *= blendWave; float waves = lerp(noise1.z, noise1.w, blendWave) + lerp(noise2.x, noise2.y, blendWave); return smoothstep(0.75, 2, waves); }
更改
水着色器,使其使用新的包含文件。
#include "Water.cginc" sampler2D _MainTex; … void surf (Input IN, inout SurfaceOutputStandard o) { float waves = Waves(IN.worldPos.xz, _MainTex); fixed4 c = saturate(_Color + waves); o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
在“
水岸”着色器中,将同时计算泡沫和波浪的值。 然后,当我们接近海岸时,我们将海浪消声。 最终结果将是泡沫和波浪的最大化。
#include "Water.cginc" sampler2D _MainTex; … void surf (Input IN, inout SurfaceOutputStandard o) { float shore = IN.uv_MainTex.y; float foam = Foam(shore, IN.worldPos.xz, _MainTex); float waves = Waves(IN.worldPos.xz, _MainTex); waves *= 1 - shore; fixed4 c = saturate(_Color + max(foam, waves)); o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
泡沫和波浪的混合物。统一包装再谈沿海水
海岸网格的一部分隐藏在浮雕网格下方。 这是正常现象,但是只有一小部分被隐藏。 不幸的是,陡峭的悬崖掩盖了大部分沿海水域,因此也起泡沫。
几乎隐藏的沿海水域。我们可以通过增加海岸带的大小来解决这个问题。 这可以通过减小水的六边形的半径来完成。 为此,除了完整性系数外,我们还需要
HexMetrics
水系数以及获取水角的方法。
完整性系数为0.8。 要使水化合物的大小增加一倍,我们需要将水系数设置为0.6。
public const float waterFactor = 0.6f; public static Vector3 GetFirstWaterCorner (HexDirection direction) { return corners[(int)direction] * waterFactor; } public static Vector3 GetSecondWaterCorner (HexDirection direction) { return corners[(int)direction + 1] * waterFactor; }
我们将使用这些新方法HexGridChunk
来查找水的角度。 void TriangulateOpenWater ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { Vector3 c1 = center + HexMetrics.GetFirstWaterCorner(direction); Vector3 c2 = center + HexMetrics.GetSecondWaterCorner(direction); … } void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { EdgeVertices e1 = new EdgeVertices( center + HexMetrics.GetFirstWaterCorner(direction), center + HexMetrics.GetSecondWaterCorner(direction) ); … }
使用水角。实际上,水的六边形之间的距离增加了一倍。现在HexMetrics
它也应该有一种在水中建立桥梁的方法。 public const float waterBlendFactor = 1f - waterFactor; public static Vector3 GetWaterBridge (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * waterBlendFactor; }
进行更改,HexGridChunk
以便他使用新方法。 void TriangulateOpenWater ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … if (direction <= HexDirection.SE && neighbor != null) { Vector3 bridge = HexMetrics.GetWaterBridge(direction); … if (direction <= HexDirection.E) { … water.AddTriangle( c2, e2, c2 + HexMetrics.GetWaterBridge(direction.Next()) ); } } } void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … Vector3 bridge = HexMetrics.GetWaterBridge(direction); … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { waterShore.AddTriangle( e1.v5, e2.v5, e1.v5 + HexMetrics.GetWaterBridge(direction.Next()) ); … } }
在水中的长桥。在水陆之间
尽管这为我们提供了更多的泡沫空间,但在浮雕下隐藏了更多的空间。理想情况下,我们将能够在水侧使用水肋,在陆侧使用肋水肋。如果我们从水的角落开始,我们将无法使用简单的桥梁来找到土地的相对边缘。相反,我们可以从邻居的中心向相反的方向前进。更改TriangulateWaterShore
为使用此新方法。
边角错误。这行之有效,只是现在我们再次需要考虑角度三角形的两种情况。 HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) {
边缘的正确角。这种方法效果很好,但是现在大多数泡沫可见,因此变得非常明显。为了弥补这一点,我们将通过减少着色器中的惯性运动值的比例来使效果变弱。 shore = sqrt(shore) * 0.9
准备好的泡沫。统一包装海底河流
最终,我们至少在没有河流流入的地方喝了水。由于水和河流尚未相互注意到,因此河流将在水中流动。河流在水中流动。渲染半透明对象的顺序取决于它们与相机的距离。最近的对象最后渲染,因此它们在顶部。移动相机时,这意味着有时河流和水有时会彼此叠置。首先,使渲染顺序恒定。必须在水面上绘制河流,以便正确显示瀑布。我们可以通过更改River着色器的队列来实现。 Tags { "RenderType"="Transparent" "Queue"="Transparent+1" }
我们最后画了河。隐藏水下河
尽管河床很可能在水下,并且水实际上可以流过河水,但我们不应看到这种水。更重要的是,不应将其渲染在真实的水面之上。仅当当前单元格不在水下时,才可以通过添加河段来摆脱海底河流的水。 void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater) { bool reversed = cell.HasIncomingRiver; … } } void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater) { bool reversed = cell.IncomingRiver == direction; … } }
要TriangulateConnection
开始,将增加河流的部分,当没有电流,没有相邻小区不是在水中。 if (cell.HasRiverThroughEdge(direction)) { e2.v3.y = neighbor.StreamBedY; if (!cell.IsUnderwater && !neighbor.IsUnderwater) { TriangulateRiverQuad( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, 0.8f, cell.HasIncomingRiver && cell.IncomingRiver == direction ); } }
没有更多的水下河流。瀑布
不再有水下河,但是现在我们在河与水面相遇的那些地方开了孔。与水位相同的河流会形成小洞或覆盖层。但是最引人注目的是从更高的高度流出的河流缺少的瀑布。让我们先照顾他们。一条带有瀑布的河段,用于穿过水面。结果,他发现自己部分在水上,部分在水下。我们需要保持一部分高于水位,丢弃其他所有东西。您将为此而努力,因此请创建一个单独的方法。新方法需要四个峰值,两个河流水位和一个水位。我们将对其进行设置,以使我们朝着瀑布下方的水流方向看。因此,前两个峰和左侧和右侧将在顶部,而较低的峰将跟随。 void TriangulateWaterfallInWater ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, float waterY ) { v1.y = v2.y = y1; v3.y = v4.y = y2; rivers.AddQuad(v1, v2, v3, v4); rivers.AddQuadUV(0f, 1f, 0.8f, 1f); }
TriangulateConnection
当邻居在水下并创建瀑布时,我们将调用此方法。 if (!cell.IsUnderwater) { if (!neighbor.IsUnderwater) { TriangulateRiverQuad( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, 0.8f, cell.HasIncomingRiver && cell.IncomingRiver == direction ); } else if (cell.Elevation > neighbor.WaterLevel) { TriangulateWaterfallInWater( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, neighbor.WaterSurfaceY ); } }
当当前单元格在水下,而下一个不在时,我们还需要以相反的方向处理瀑布。 if (!cell.IsUnderwater) { … } else if ( !neighbor.IsUnderwater && neighbor.Elevation > cell.WaterLevel ) { TriangulateWaterfallInWater( e2.v4, e2.v2, e1.v4, e1.v2, neighbor.RiverSurfaceY, cell.RiverSurfaceY, cell.WaterSurfaceY ); }
因此,我们再次得到了原始河的四边形。接下来,我们需要进行更改,TriangulateWaterfallInWater
以便将较低的峰升高到水位。不幸的是,仅更改Y坐标是不够的。这可以将瀑布从悬崖上推开,可能形成洞。相反,您必须使用插值将较低的顶点移动到较高的顶点。插值。要向上移动较低的峰,请将其在水面以下的距离除以瀑布的高度。这将为我们提供内插器值。 v1.y = v2.y = y1; v3.y = v4.y = y2; float t = (waterY - y2) / (y1 - y2); v3 = Vector3.Lerp(v3, v1, t); v4 = Vector3.Lerp(v4, v2, t); rivers.AddQuad(v1, v2, v3, v4); rivers.AddQuadUV(0f, 1f, 0.8f, 1f);
结果,我们得到了具有相同方向的缩短的瀑布。但是,由于下顶点的位置已更改,因此它们不会像原始顶点一样变形。这意味着最终结果仍将与原始瀑布不一致。为了解决这个问题,我们需要在插值之前手动扭曲顶点,然后添加未扭曲的四边形。 v1.y = v2.y = y1; v3.y = v4.y = y2; v1 = HexMetrics.Perturb(v1); v2 = HexMetrics.Perturb(v2); v3 = HexMetrics.Perturb(v3); v4 = HexMetrics.Perturb(v4); float t = (waterY - y2) / (y1 - y2); v3 = Vector3.Lerp(v3, v1, t); v4 = Vector3.Lerp(v4, v2, t); rivers.AddQuadUnperturbed(v1, v2, v3, v4); rivers.AddQuadUV(0f, 1f, 0.8f, 1f);
由于我们已经有了添加不变形三角形的方法,因此我们实际上不需要为四边形创建一个。因此,我们添加了必要的方法HexMesh.AddQuadUnperturbed
。 public void AddQuadUnperturbed ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4 ) { int vertexIndex = vertices.Count; vertices.Add(v1); vertices.Add(v2); vertices.Add(v3); vertices.Add(v4); triangles.Add(vertexIndex); triangles.Add(vertexIndex + 2); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 2); triangles.Add(vertexIndex + 3); }
瀑布终止于水面。统一包装河口
当河流以与水面相同的高度流动时,河流网会触及沿海网。如果它是一条流入大海或海洋的河流,那条河将与海浪相遇。因此,我们称此类区域为河口。这条河与海岸交汇而不会扭曲山峰。现在我们的嘴巴有两个问题。首先,四角河连接了排骨的第二和第四顶部,跳过了第三。由于水的海岸不使用第三个峰,因此可能会形成一个洞或重叠。我们可以通过更改嘴的几何形状来解决此问题。第二个问题是泡沫和河流材料之间存在急剧的过渡。为了解决这个问题,我们需要另一种可以将河流和水的影响混合在一起的材料。这意味着嘴需要一种特殊的方法,因此让我们为它们创建一个单独的方法。TriangulateWaterShore
当有河流沿当前方向行驶时,应调用它。 void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … if (cell.HasRiverThroughEdge(direction)) { TriangulateEstuary(e1, e2); } else { waterShore.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); waterShore.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); waterShore.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); waterShore.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); } … } void TriangulateEstuary (EdgeVertices e1, EdgeVertices e2) { }
不需要混合两种效果的区域来填充整个条带。梯形足以满足我们的需要。因此,我们可以在侧面使用两个沿海三角形。 void TriangulateEstuary (EdgeVertices e1, EdgeVertices e2) { waterShore.AddTriangle(e2.v1, e1.v2, e1.v1); waterShore.AddTriangle(e2.v5, e1.v5, e1.v4); waterShore.AddTriangleUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); waterShore.AddTriangleUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); }
梯形孔为混合区域。UV2坐标
要创建河流效果,我们需要UV坐标。但是要创建泡沫效果,还需要UV坐标。也就是说,在混合它们时,我们需要两组UV坐标。幸运的是,Unity引擎网格物体最多可以支持四个UV集。我们只需要添加HexMesh
对第二组的支持即可。 public bool useCollider, useColors, useUVCoordinates, useUV2Coordinates; [NonSerialized] List<Vector2> uvs, uv2s; public void Clear () { … if (useUVCoordinates) { uvs = ListPool<Vector2>.Get(); } if (useUV2Coordinates) { uv2s = ListPool<Vector2>.Get(); } triangles = ListPool<int>.Get(); } public void Apply () { … if (useUVCoordinates) { hexMesh.SetUVs(0, uvs); ListPool<Vector2>.Add(uvs); } if (useUV2Coordinates) { hexMesh.SetUVs(1, uv2s); ListPool<Vector2>.Add(uv2s); } … }
要添加第二组UV,我们复制使用UV的方法并更改所需的方式。 public void AddTriangleUV2 (Vector2 uv1, Vector2 uv2, Vector3 uv3) { uv2s.Add(uv1); uv2s.Add(uv2); uv2s.Add(uv3); } public void AddQuadUV2 (Vector2 uv1, Vector2 uv2, Vector3 uv3, Vector3 uv4) { uv2s.Add(uv1); uv2s.Add(uv2); uv2s.Add(uv3); uv2s.Add(uv4); } public void AddQuadUV2 (float uMin, float uMax, float vMin, float vMax) { uv2s.Add(new Vector2(uMin, vMin)); uv2s.Add(new Vector2(uMax, vMin)); uv2s.Add(new Vector2(uMin, vMax)); uv2s.Add(new Vector2(uMax, vMax)); }
河明暗器功能
由于我们将在两个着色器中使用river效果,因此将代码从River着色器移动到新的Water include file函数。 float River (float2 riverUV, sampler2D noiseTex) { float2 uv = riverUV; uv.x = uv.x * 0.0625 + _Time.y * 0.005; uv.y -= _Time.y * 0.25; float4 noise = tex2D(noiseTex, uv); float2 uv2 = riverUV; uv2.x = uv2.x * 0.0625 - _Time.y * 0.0052; uv2.y -= _Time.y * 0.23; float4 noise2 = tex2D(noiseTex, uv2); return noise.x * noise2.w; }
更改River着色器以使用此新功能。 #include "Water.cginc" sampler2D _MainTex; … void surf (Input IN, inout SurfaceOutputStandard o) { float river = River(IN.uv_MainTex, _MainTex); fixed4 c = saturate(_Color + river); … }
口对象
添加HexGridChunk
嘴以支撑网格物体。 public HexMesh terrain, rivers, roads, water, waterShore, estuaries; public void Triangulate () { terrain.Clear(); rivers.Clear(); roads.Clear(); water.Clear(); waterShore.Clear(); estuaries.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); roads.Apply(); water.Apply(); waterShore.Apply(); estuaries.Apply(); }
创建着色器,材质和嘴巴对象,复制海岸并进行更改。将其连接到片段,并使其使用UV和UV2坐标。对象河口。口三角剖分
我们可以通过在河的末端和水的边缘的中间放置一个三角形来解决孔洞或重叠的问题。由于我们的嘴部着色器是海岸着色器的副本,因此我们设置UV坐标以匹配泡沫效果。 void TriangulateEstuary (EdgeVertices e1, EdgeVertices e2) { … estuaries.AddTriangle(e1.v3, e2.v2, e2.v4); estuaries.AddTriangleUV( new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 1f) ); }
中间三角形。我们可以通过在中间三角形的两侧添加一个四边形来填充整个梯形。 estuaries.AddQuad(e1.v2, e1.v3, e2.v1, e2.v2); estuaries.AddTriangle(e1.v3, e2.v2, e2.v4); estuaries.AddQuad(e1.v3, e1.v4, e2.v4, e2.v5); estuaries.AddQuadUV(0f, 0f, 0f, 1f); estuaries.AddTriangleUV( new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 1f) ); estuaries.AddQuadUV(0f, 0f, 0f, 1f);
准备好梯形。让我们将四边形方向向左旋转,使其对角线连接缩短,结果我们得到对称的几何形状。 estuaries.AddQuad(e2.v1, e1.v2, e2.v2, e1.v3); estuaries.AddTriangle(e1.v3, e2.v2, e2.v4); estuaries.AddQuad(e1.v3, e1.v4, e2.v4, e2.v5); estuaries.AddQuadUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 0f) );
旋转四边形,对称几何河水流量
为了支持河流效果,我们需要添加UV2坐标。中间三角形的底部在河流的中间,因此其坐标U应该等于0.5。由于河流沿水的方向流动,因此左点接收的U坐标等于1,右点接收的U坐标值为0。我们将Y坐标设置为0和1,对应于水流的方向。 estuaries.AddTriangleUV2( new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f) );
三角形两侧的四边形应与此方向一致。对于超过河流宽度的点,我们保持相同的U坐标。 estuaries.AddQuadUV2( new Vector2(1f, 0f), new Vector2(1f, 1f), new Vector2(1f, 0f), new Vector2(0.5f, 1f) ); estuaries.AddTriangleUV2( new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); estuaries.AddQuadUV2( new Vector2(0.5f, 1f), new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f) );
UV2梯形。为确保正确设置UV2坐标,请使Estuary着色器渲染它们。我们可以通过添加到输入结构中来访问这些坐标float2 uv2_MainTex
。 struct Input { float2 uv_MainTex; float2 uv2_MainTex; float3 worldPos; }; … void surf (Input IN, inout SurfaceOutputStandard o) { float shore = IN.uv_MainTex.y; float foam = Foam(shore, IN.worldPos.xz, _MainTex); float waves = Waves(IN.worldPos.xz, _MainTex); waves *= 1 - shore; fixed4 c = fixed4(IN.uv2_MainTex, 1, 1); … }
UV2坐标。一切看起来都不错,您可以使用着色器创建河流效果。 void surf (Input IN, inout SurfaceOutputStandard o) { … float river = River(IN.uv2_MainTex, _MainTex); fixed4 c = saturate(_Color + river); … }
使用UV2创建河流效果。我们以如下方式创建河流:在对单元格之间的连接进行三角测量时,V河的坐标从0.8更改为1。因此,在这里,我们也应该使用此间隔,而不是从0到1。但是,沿海连接比普通单元连接多50% 。因此,为了最适合河道,我们必须将值从0.8更改为1.1。 estuaries.AddQuadUV2( new Vector2(1f, 0.8f), new Vector2(1f, 1.1f), new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f) ); estuaries.AddTriangleUV2( new Vector2(0.5f, 1.1f), new Vector2(1f, 0.8f), new Vector2(0f, 0.8f) ); estuaries.AddQuadUV2( new Vector2(0.5f, 1.1f), new Vector2(0f, 1.1f), new Vector2(0f, 0.8f), new Vector2(0f, 0.8f) );
河流与河口同步流动。流量设定
当河水在一条直线上移动时。但是,当水流入更大的区域时,它会膨胀。电流将弯曲。我们可以通过折叠UV2坐标来模拟这一点。不要将上U坐标在河的宽度之外保持恒定,而是将它们移动0.5。最左边的点是1.5,最右边的是-0.5。同时,我们通过移动左右底点的U坐标来扩展流程。将左一个从1更改为0.7,右一个从0更改为0.3。 estuaries.AddQuadUV2( new Vector2(1.5f, 0.8f), new Vector2(0.7f, 1.1f), new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f) ); … estuaries.AddQuadUV2( new Vector2(0.5f, 1.1f), new Vector2(0.3f, 1.1f), new Vector2(0f, 0.8f), new Vector2(-0.5f, 0.8f) );
扩河。要完成曲率效果,请更改相同四个点的V坐标。由于水从河的末端流走,因此我们将较高点的V坐标增加到1。为创建更好的曲线,我们将较低两个点的V坐标增加到1.15。 estuaries.AddQuadUV2( new Vector2(1.5f, 1f), new Vector2(0.7f, 1.15f), new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f) ); estuaries.AddTriangleUV2( new Vector2(0.5f, 1.1f), new Vector2(1f, 0.8f), new Vector2(0f, 0.8f) ); estuaries.AddQuadUV2( new Vector2(0.5f, 1.1f), new Vector2(0.3f, 1.15f), new Vector2(0f, 0.8f), new Vector2(-0.5f, 1f) );
这条河的弯道。河流和海岸混合
我们剩下的就是将海岸和河流的影响混合在一起。为此,我们使用线性插值,将惯性运动值用作插值器。 float shoreWater = max(foam, waves); float river = River(IN.uv2_MainTex, _MainTex); float water = lerp(shoreWater, river, IN.uv_MainTex.x); fixed4 c = saturate(_Color + water);
尽管这应该可行,但是您可能会遇到编译错误。编译器抱怨重新定义_MainTex_ST
。原因是由于同时使用uv_MainTex
和引起的Unity表面着色器编译器内部错误uv2_MainTex
。我们需要找到一种解决方法。而不是使用uv2_MainTex
它,我们将不得不手动传输辅助UV坐标。为此,请重命名uv2_MainTex
为riverUV
。然后向着色器添加一个顶点函数,该着色器为其指定坐标。 #pragma surface surf Standard alpha vertex:vert … struct Input { float2 uv_MainTex; float2 riverUV; float3 worldPos; }; … void vert (inout appdata_full v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input, o); o.riverUV = v.texcoord1.xy; } void surf (Input IN, inout SurfaceOutputStandard o) { … float river = River(IN.riverUV, _MainTex); … }
基于海岸值的插值。插值有效,但顶部的左侧和右侧顶点除外。在这些时候,河流应该消失了。因此,我们不能使用海岸的价值。我们将不得不使用一个不同的值,在这两个顶点处的值为0。幸运的是,我们仍然具有第一个UV集的U坐标,因此我们可以将其存储在那里。 estuaries.AddQuadUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(0f, 0f) ); estuaries.AddTriangleUV( new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(1f, 1f) ); estuaries.AddQuadUV( new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(0f, 1f) );
正确的搭配。现在,河水在不断扩张的河流,沿海水域和泡沫之间相互融合。尽管这并不能与瀑布完全匹配,但是这种效果在瀑布中也看起来不错。出海口统一行动来自水体的河流
我们已经有河流流入水域,但是并没有支持向不同方向流动的河流。有河流从那里流出的湖泊,因此我们也需要添加它们。当河流从水体中流出时,它实际上会流向更高的高度。目前这是不可能的。如果水位与目标点的高度相对应,我们需要例外,并允许这种情况。让我们添加一个HexCell
私有方法,该方法根据我们的新标准检查邻居是否是流出河的正确目标点。 bool IsValidRiverDestination (HexCell neighbor) { return neighbor && ( elevation >= neighbor.elevation || waterLevel == neighbor.elevation ); }
我们将使用新方法来确定是否有可能创建流出的河流。 public void SetOutgoingRiver (HexDirection direction) { if (hasOutgoingRiver && outgoingRiver == direction) { return; } HexCell neighbor = GetNeighbor(direction);
另外,在更改单元格或水位的高度时,您需要检查河流。让我们创建一个私有方法来执行此任务。 void ValidateRivers () { if ( hasOutgoingRiver && !IsValidRiverDestination(GetNeighbor(outgoingRiver)) ) { RemoveOutgoingRiver(); } if ( hasIncomingRiver && !GetNeighbor(incomingRiver).IsValidRiverDestination(this) ) { RemoveIncomingRiver(); } }
我们将在Elevation
和属性中使用此新方法WaterLevel
。 public int Elevation { … set { …
出入河湖。扭转潮流
我们创建了HexGridChunk.TriangulateEstuary
,暗示河流只能流入水域。因此,结果,河道总是沿一个方向移动。当处理从水体流出的河流时,我们需要逆向流动。为此,您需要TriangulateEstuary
了解流向。因此,我们给他一个布尔参数,该布尔参数确定我们是否在处理传入的河流。 void TriangulateEstuary ( EdgeVertices e1, EdgeVertices e2, bool incomingRiver ) { … }
当从中调用此方法时,我们将传递此信息TriangulateWaterShore
。 if (cell.HasRiverThroughEdge(direction)) { TriangulateEstuary(e1, e2, cell.IncomingRiver == direction); }
现在,我们需要通过更改UV2的坐标来扩展河流流量。需要镜像出河的U坐标:-0.5变为1.5,0变为1,1变为0,1.5变为-0.5。使用V坐标,情况会稍微复杂一些。如果您看一下我们如何处理反向河流连接,那么0.8应该是0,而1应该是-0.2。这意味着1.1变为-0.3,而1.15变为-0.35。由于每种情况下的UV2坐标都非常不同,因此让我们为它们编写一个单独的代码。 void TriangulateEstuary ( EdgeVertices e1, EdgeVertices e2, bool incomingRiver ) { … if (incomingRiver) { estuaries.AddQuadUV2( new Vector2(1.5f, 1f), new Vector2(0.7f, 1.15f), new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f) ); estuaries.AddTriangleUV2( new Vector2(0.5f, 1.1f), new Vector2(1f, 0.8f), new Vector2(0f, 0.8f) ); estuaries.AddQuadUV2( new Vector2(0.5f, 1.1f), new Vector2(0.3f, 1.15f), new Vector2(0f, 0.8f), new Vector2(-0.5f, 1f) ); } else { estuaries.AddQuadUV2( new Vector2(-0.5f, -0.2f), new Vector2(0.3f, -0.35f), new Vector2(0f, 0f), new Vector2(0.5f, -0.3f) ); estuaries.AddTriangleUV2( new Vector2(0.5f, -0.3f), new Vector2(0f, 0f), new Vector2(1f, 0f) ); estuaries.AddQuadUV2( new Vector2(0.5f, -0.3f), new Vector2(0.7f, -0.35f), new Vector2(1f, 0f), new Vector2(1.5f, -0.2f) ); } }
正确的河流路线。统一包装第9部分:救济特征
- 向浮雕中添加对象。
- 我们创建对对象密度级别的支持。
- 我们在关卡中使用各种对象。
- 混合三种不同类型的对象。
在这一部分中,我们将讨论将对象添加到地形。我们将创建诸如建筑物和树木之类的对象。森林,农业用地与城市化之间的冲突。添加对对象的支持
尽管浮雕的形状有所变化,但到目前为止,没有任何变化。这是一片死气沉沉的土地。为了让生活充满活力,您需要添加此类对象。像树木和房屋。这些对象不是浮雕网格的一部分,而是单独的对象。但这并不能阻止我们在对地形进行三角剖分时添加它们。HexGridChunk
不在乎网格如何工作。他只是命令他的一个孩子HexMesh
添加一个三角形或四边形。同样,它可以具有一个子元素,用于处理对象在其上的放置。对象管理器
让我们创建一个组件HexFeatureManager
,该组件负责单个片段中的对象。我们使用与HexMesh
给他方法相同的方案Clear
,Apply
和AddFeature
。由于需要将对象放置在某处,因此该方法将AddFeature
接收position参数。我们将从一个空白的实现开始,目前暂时什么也不做。 using UnityEngine; public class HexFeatureManager : MonoBehaviour { public void Clear () {} public void Apply () {} public void AddFeature (Vector3 position) {} }
现在,我们可以在中添加指向此类组件的链接HexGridChunk
。然后,您可以像所有子元素一样将其包括在三角测量过程中HexMesh
。 public HexFeatureManager features; public void Triangulate () { terrain.Clear(); rivers.Clear(); roads.Clear(); water.Clear(); waterShore.Clear(); estuaries.Clear(); features.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); roads.Apply(); water.Apply(); waterShore.Apply(); estuaries.Apply(); features.Apply(); }
首先,在每个单元格的中心放置一个对象 void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } features.AddFeature(cell.Position); }
现在我们需要一个真正的对象管理器。向Hex Grid Chunk预制件中添加一个子代,并给它一个组件HexFeatureManager
。然后,您可以将片段连接到它。对象管理器添加到片段预制中。预制物件
我们将创建什么地形对象?对于第一个测试,立方体非常适合。让我们创建一个足够大的多维数据集,例如,缩放比例为(3,3,3),然后将其变成预制件。也为他创造材料。我使用默认材质和红色。让我们删除它的对撞机,因为我们不需要它。预制立方体。对象管理器将需要一个指向该预制件的链接,因此将其添加到HexFeatureManager
然后进行连接。由于需要访问转换组件才能放置对象,因此我们将其用作链接的类型。 public Transform featurePrefab;
预制对象管理器。创建对象实例
结构已经准备就绪,我们可以开始添加地形要素了!只需在其中创建一个预制实例HexFeatureManager.AddFeature
并设置其位置即可。 public void AddFeature (Vector3 position) { Transform instance = Instantiate(featurePrefab); instance.localPosition = position; }
地形要素实例。从现在开始,地形将充满立方体。至少是立方体的上半部分,因为在Unity中立方体网格的本地原点在立方体的中心,而底部在凸版的表面下方。要将立方体放置在地形上,我们需要将其上移一半的高度。 public void AddFeature (Vector3 position) { Transform instance = Instantiate(featurePrefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = position; }
浮雕表面上的立方体。当然,我们的单元会变形,因此我们需要扭曲对象的位置。因此,我们摆脱了完美的网格重复性。 instance.localPosition = HexMetrics.Perturb(position);
对象的位置变形。破坏救济物
每次更新片段时,我们都会创建新的救济对象。这意味着当我们在相同位置创建越来越多的对象时。为避免重复,清洁碎片时我们需要清除旧物体。最快的方法是创建一个游戏容器对象,并将所有释放对象变成其子对象。然后,当被调用时,Clear
我们将销毁该容器并创建一个新容器。容器本身将是其管理者的孩子。 Transform container; public void Clear () { if (container) { Destroy(container.gameObject); } container = new GameObject("Features Container").transform; container.SetParent(transform, false); } … public void AddFeature (Vector3 position) { Transform instance = Instantiate(featurePrefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.SetParent(container, false); }
可能每次创建和销毁救济对象的效率都很低。, , . . . , , , . HexFeatureManager.Apply
. . , , .
统一包装救济物的放置
当我们将对象放置在每个单元格的中心时。对于空的单元格,这看起来很正常,但是在包含河流和道路以及被水淹没的单元格上,这似乎很奇怪。对象无处不在。因此,让我们在放置对象之前检查HexGridChunk.Triangulate
单元格是否为空。 if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell.Position); }
有限的住宿。每个方向一个对象
每个单元格只有一个对象不是太多。仍然有足够的空间容纳一堆物品。因此,我们向该单元格的六个三角形中的每个三角形的中心添加一个附加对象,即每个方向一个。当我们知道该单元格中没有河流时,将以另一种方法进行此操作Triangulate
。我们仍然需要检查是否在水下以及牢房中是否有道路。但是在这种情况下,我们只对沿当前方向行驶的道路感兴趣。 void Triangulate (HexDirection direction, HexCell cell) { … if (cell.HasRiver) { … } else { TriangulateWithoutRiver(direction, cell, center, e); if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) { features.AddFeature((center + e.v1 + e.v5) * (1f / 3f)); } } … }
设施很多,但是不在河流附近。这会创建更多对象!它们出现在道路附近,但仍然避开河流。要沿河流放置对象,我们还可以在其中添加对象TriangulateAdjacentToRiver
。但是,仅当三角形不在水下且上面没有路时,才再次出现。 void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) { features.AddFeature((center + e.v1 + e.v5) * (1f / 3f)); } }
物体出现在河边。可以渲染这么多对象吗?, dynamic batching Unity. , . batch. « », . instancing, dynamic batching.
统一包装各种物体
我们所有的浮雕对象都具有相同的方向,这看起来完全不自然。让我们给每个人一个随机的转折。 public void AddFeature (Vector3 position) { Transform instance = Instantiate(featurePrefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.localRotation = Quaternion.Euler(0f, 360f * Random.value, 0f); instance.SetParent(container, false); }
随机回合。因此结果变得更加多样化。不幸的是,每次更新片段时,对象都会收到新的随机旋转。编辑单元格不应更改附近的对象,因此我们需要另一种方法。我们具有始终相同的噪声纹理。但是,此纹理包含Perlin梯度噪声,并且局部一致。这正是使单元格中顶点的位置失真时所需要的。但是转弯不必保持一致。所有转弯都应同样可能且混合。因此,我们需要一个具有非渐变随机值的纹理,无需双线性滤波即可对其进行采样。本质上,这是一个哈希网格,它构成了梯度噪声的基础。创建哈希表
我们可以从浮点值数组创建哈希表,并用随机值填充一次。因此,我们根本不需要纹理。让我们将其添加到中HexMetrics
。256 x 256的大小足以实现足够的变化。 public const int hashGridSize = 256; static float[] hashGrid; public static void InitializeHashGrid () { hashGrid = new float[hashGridSize * hashGridSize]; for (int i = 0; i < hashGrid.Length; i++) { hashGrid[i] = Random.value; } }
随机值由始终提供相同结果的数学公式生成。结果序列取决于种子的数量,默认情况下,该数量等于时间的当前值。这就是为什么在每个游戏环节我们都会得到不同的结果的原因。为了确保始终创建相同的对象,我们需要将种子参数添加到初始化方法中。 public static void InitializeHashGrid (int seed) { hashGrid = new float[hashGridSize * hashGridSize]; Random.InitState(seed); for (int i = 0; i < hashGrid.Length; i++) { hashGrid[i] = Random.value; } }
现在我们已经初始化了随机数流,我们将始终从中获得相同的序列。因此,在生成地图之后发生的看似随机事件也将始终相同。我们可以通过在初始化随机数生成器之前存储其状态来避免这种情况。完成工作后,我们可以问他以前的状态。 Random.State currentState = Random.state; Random.InitState(seed); for (int i = 0; i < hashGrid.Length; i++) { hashGrid[i] = Random.value; } Random.state = currentState;
哈希表HexGrid
在分配噪声纹理的同时被初始化。也就是说,在HexGrid.Start
和中HexGrid.Awake
。我们这样做是为了避免不必要地频繁生成值。 public int seed; void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); … } void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); } }
通用种子变量允许我们选择地图的种子值。任何值都可以。我选择了1234。种子的选择。使用哈希表
要使用哈希表,请添加到HexMetrics
采样方法。像一样SampleNoise
,它使用XZ位置的坐标来获取值。通过将坐标限制为整数值,然后获取整数除以表的大小,可以找到哈希索引。 public static float SampleHashGrid (Vector3 position) { int x = (int)position.x % hashGridSize; int z = (int)position.z % hashGridSize; return hashGrid[x + z * hashGridSize]; }
%做什么?, , — . , −4, −3, −2, −1, 0, 1, 2, 3, 4 modulo 3 −1, 0, −2, −1, 0, 1, 2, 0, 1.
这适用于正坐标,但不适用于负坐标,因为对于此类数字,余数将为负。我们可以通过将表的大小添加到负面结果中来解决此问题。 int x = (int)position.x % hashGridSize; if (x < 0) { x += hashGridSize; } int z = (int)position.z % hashGridSize; if (z < 0) { z += hashGridSize; }
现在,我们为每个平方单位创建自己的价值。但是,实际上,我们不需要这样的表密度。对象彼此隔开。我们可以通过在计算索引之前减小位置比例来拉伸表格。一个4乘4平方的唯一值就足够了。 public const float hashGridScale = 0.25f; public static float SampleHashGrid (Vector3 position) { int x = (int)(position.x * hashGridScale) % hashGridSize; if (x < 0) { x += hashGridSize; } int z = (int)(position.z * hashGridScale) % hashGridSize; if (z < 0) { z += hashGridSize; } return hashGrid[x + z * hashGridSize]; }
让我们回到HexFeatureManager.AddFeature
并使用新的哈希表来获取值。应用它指定旋转后,在编辑地形时对象将保持静止。 public void AddFeature (Vector3 position) { float hash = HexMetrics.SampleHashGrid(position); Transform instance = Instantiate(featurePrefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.localRotation = Quaternion.Euler(0f, 360f * hash, 0f); instance.SetParent(container, false); }
放置阈值
尽管对象具有不同的旋转角度,但是在其位置上仍然可以看到图案。每个单元都有七个对象。我们可以给这个方案增加混乱,任意跳过一些对象。我们如何决定是否添加对象?当然,请检查另一个随机值!也就是说,现在,我们需要两个而不是一个哈希值。可以通过使用哈希代替表数组类型的float
变量来增加对它们的支持Vector2
。但是向量运算对于散列值没有意义,因此让我们为此创建一个特殊的结构。她只需要两个浮点值。让我们添加一个静态方法来创建一对随机值。 using UnityEngine; public struct HexHash { public float a, b; public static HexHash Create () { HexHash hash; hash.a = Random.value; hash.b = Random.value; return hash; } }
对其进行更改,HexMetrics
以使其使用新结构。 static HexHash[] hashGrid; public static void InitializeHashGrid (int seed) { hashGrid = new HexHash[hashGridSize * hashGridSize]; Random.State currentState = Random.state; Random.InitState(seed); for (int i = 0; i < hashGrid.Length; i++) { hashGrid[i] = HexHash.Create(); } Random.state = currentState; } public static HexHash SampleHashGrid (Vector3 position) { … }
现在HexFeatureManager.AddFeature
可以访问两个哈希值。让我们使用第一个来决定是添加对象还是跳过对象。如果该值等于或大于0.5,则跳过。这样,我们将摆脱大约一半的对象。第二个值将照常用于确定旋转角度。 public void AddFeature (Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); if (hash.a >= 0.5f) { return; } Transform instance = Instantiate(featurePrefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.localRotation = Quaternion.Euler(0f, 360f * hash.b, 0f); instance.SetParent(container, false); }
物体的密度降低了50%。统一包装绘图对象
与其将对象放置在各处,不如让它们可编辑。但是我们不会绘制单独的对象,而是将对象的级别添加到每个单元格中。此级别将控制对象出现在单元中的可能性。默认情况下,该值为零,即不存在对象。由于我们地形上的红色立方体看起来不像自然物体,因此我们将其称为建筑物。它们将代表城市化。让我们增加HexCell
城市化水平。 public int UrbanLevel { get { return urbanLevel; } set { if (urbanLevel != value) { urbanLevel = value; RefreshSelfOnly(); } } } int urbanLevel;
我们可以使水下单元格的城市化水平等于零,但这不是必需的,无论如何我们将跳过水下对象的创建。也许在某个时候,我们将添加城市化的水体,例如码头和水下建筑物。密度滑块
为了改变城市化水平,我们增加了HexMapEditor
一个滑块。 int activeUrbanLevel; … bool applyUrbanLevel; … public void SetApplyUrbanLevel (bool toggle) { applyUrbanLevel = toggle; } public void SetUrbanLevel (float level) { activeUrbanLevel = (int)level; } void EditCell (HexCell cell) { if (cell) { … if (applyWaterLevel) { cell.WaterLevel = activeWaterLevel; } if (applyUrbanLevel) { cell.UrbanLevel = activeUrbanLevel; } if (riverMode == OptionalToggle.No) { cell.RemoveRiver(); } … } }
将另一个滑块添加到UI并将其与适当的方法结合。我将在屏幕的右侧放置一个新面板,以避免溢出左侧面板。我们需要几个级别?让我们详细介绍四个,分别表示零,低,中和高密度。城市化滑块。阈值变化
现在我们已经有了城市化水平,我们需要使用它来确定是否放置物体。为此,我们需要将城市化水平作为的附加参数HexFeatureManager.AddFeature
。让我们再迈出一步,只转移单元格本身。将来,它将为我们带来更多便利。使用城市化水平的最快方法是将其乘以0.25,然后将该值用作跳过对象的新阈值。因此,每个级别的物体出现的可能性都会增加25%。 public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); if (hash.a >= cell.UrbanLevel * 0.25f) { return; } … }
为此,让我们将单元格传递给HexGridChunk
。 void Triangulate (HexCell cell) { … if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell, cell.Position); } } void Triangulate (HexDirection direction, HexCell cell) { … if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) { features.AddFeature(cell, (center + e.v1 + e.v5) * (1f / 3f)); } … } … void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) { features.AddFeature(cell, (center + e.v1 + e.v5) * (1f / 3f)); } }
绘制城市化密度水平。统一包装几种救济物预制件
物体出现可能性的差异不足以在低水平的城市化与高水平的城市化之间建立明显的隔离。在某些单元中,建筑物的数量或多或少都将少于预期数量。通过为每个级别使用我们自己的预制件,我们可以使差异更加清晰。我们将摆脱这个领域featurePrefab
,HexFeatureManager
而将其替换为用于城市化预制件的阵列。为了获得合适的预制件,我们将从城市化水平中减去一个,并将其用作指标。 <del>
创建对象的预制件的两个副本,重命名并更改它们,以便它们指示三个不同的城市化水平。级别1是低密度的,因此我们使用单位长度为边的立方体表示棚屋。我会将2层预制件的比例缩放为(1.5,2,1.5),以使其看起来像一座两层楼的建筑。对于3层较高的建筑物,我使用了比例尺(2、5、2)。为城市化的每个级别使用不同的预制件。预制混合物
我们不需要严格限制建筑物的类型。您可以将它们混在一起,就像在现实世界中一样。让我们使用三个,而不是每个级别一个阈值,每种类型的建筑物一个。在第1级,我们在40%的情况下使用棚屋布置。这里根本没有其他建筑物。对于级别,我们使用三个值(0.4、0、0)。在第2层,用更大的建筑物代替棚屋,并增加20%的机会增加棚屋。我们不会做高层建筑。也就是说,我们使用阈值三个值(0.2、0.4、0)。在第3层,我们将高层建筑替换为中型建筑,再次替换棚屋,再增加20%的棚屋机会。阈值将等于(0.2,0.2,0.4)。也就是说,我们的想法是随着城市化水平的提高,我们将升级现有建筑物并将新建筑物添加到空旷的地方。要删除现有建筑物,我们需要使用相同的哈希值间隔。如果级别1的0到0.4之间的哈希值是棚屋,那么级别3的相同间隔将创建高层建筑物。在第3层,应创建具有0-0.4范围内的哈希值,0.4-0.6范围内的两层建筑物以及0.6-0.8范围内的棚屋的高层建筑物。如果从最大到最小检查它们,则可以使用三个阈值(0.4、0.6、0.8)来完成。然后,级别2的阈值将变为(0,0.4,0.6),级别1的阈值将变为(0,0,0.4)。让我们将这些阈值保存在HexMetrics
作为数组的集合,其方法允许您获取特定级别的阈值。由于我们仅对带有对象的级别感兴趣,因此忽略级别0。 static float[][] featureThresholds = { new float[] {0.0f, 0.0f, 0.4f}, new float[] {0.0f, 0.4f, 0.6f}, new float[] {0.4f, 0.6f, 0.8f} }; public static float[] GetFeatureThresholds (int level) { return featureThresholds[level]; }
接下来,添加到HexFeatureManager
使用哈希级别和值来选择预制的方法。如果该级别大于零,那么我们使用减少了一个级别的阈值来获得阈值。然后,我们循环遍历阈值,直到其中一个阈值超过哈希值为止。这意味着我们已经找到了预制件。如果找不到,则返回null。 Transform PickPrefab (int level, float hash) { if (level > 0) { float[] thresholds = HexMetrics.GetFeatureThresholds(level - 1); for (int i = 0; i < thresholds.Length; i++) { if (hash < thresholds[i]) { return urbanPrefabs[i]; } } } return null; }
这种方法要求对预制件的链接进行重新排序,以使它们从高密度变为低密度。预制订单倒置。我们将使用新方法AddFeature
来选择预制件。如果没有收到,则跳过该对象。否则,创建它的一个实例,然后像以前一样继续。 public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position);
混合预制件。水平变化
现在我们有混合的建筑物,但是到目前为止只有三座。我们可以通过将预制件的集合与城市化密度的各个级别联系起来,进一步提高可变性。之后,可以随机选择其中之一。这将需要一个新的随机值,因此添加第三个c HexHash
。 public float a, b, c; public static HexHash Create () { HexHash hash; hash.a = Random.value; hash.b = Random.value; hash.c = Random.value; return hash; }
让我们将其HexFeatureManager.urbanPrefabs
变成一个数组数组,然后向method中添加一个PickPrefab
参数choice
。我们使用它来选择内置数组的索引,将其乘以该数组的长度,然后将其转换为整数。 public Transform[][] urbanPrefabs; … Transform PickPrefab (int level, float hash, float choice) { if (level > 0) { float[] thresholds = HexMetrics.GetFeatureThresholds(level - 1); for (int i = 0; i < thresholds.Length; i++) { if (hash < thresholds[i]) { return urbanPrefabs[i][(int)(choice * urbanPrefabs[i].Length)]; } } } return null; }
让我们根据第二个哈希(B)的值来证明我们的选择。然后,您需要从B转到C。 public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); Transform prefab = PickPrefab(cell.UrbanLevel, hash.a, hash.b); if (!prefab) { return; } Transform instance = Instantiate(prefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.localRotation = Quaternion.Euler(0f, 360f * hash.c, 0f); instance.SetParent(container, false); }
在继续之前,我们需要考虑Random.value
可能返回值1的内容。因此,数组索引可能会超出范围。为了防止这种情况的发生,让我们稍微调整一下哈希值。我们只是将它们全部缩放,以免担心我们使用的特定对象。 public static HexHash Create () { HexHash hash; hash.a = Random.value * 0.999f; hash.b = Random.value * 0.999f; hash.c = Random.value * 0.999f; return hash; }
不幸的是,检查器不会显示数组的数组。因此,我们无法对其进行配置。要解决此限制,请创建一个可序列化的结构以将内置数组封装到其中。让我们给她一个方法,该方法可以从选择转换为数组索引并返回一个预制。 using UnityEngine; [System.Serializable] public struct HexFeatureCollection { public Transform[] prefabs; public Transform Pick (float choice) { return prefabs[(int)(choice * prefabs.Length)]; } }
我们使用HexFeatureManager
此类集合的数组代替内置数组。
现在,我们可以为每个密度级别分配几座建筑物。由于它们是独立的,因此我们不必在每个级别使用相同的数量。我只在每个级别上使用了两个选项,为每个级别添加了一个更长的较低选项。我为它们选择了比例(3.5、3、2),(2.75、1.5、1.5)和(1.75、1、1)。每个密度级别有两种类型的建筑物。统一包装几种物体
在现有方案中,我们可以创建相当有价值的城市结构。但救济可能不仅包括建筑物。农场或植物怎么样?让我们HexCell
为它们添加级别。它们不是互斥的并且可以混合。 public int FarmLevel { get { return farmLevel; } set { if (farmLevel != value) { farmLevel = value; RefreshSelfOnly(); } } } public int PlantLevel { get { return plantLevel; } set { if (plantLevel != value) { plantLevel = value; RefreshSelfOnly(); } } } int urbanLevel, farmLevel, plantLevel;
当然,这需要HexMapEditor
另外两个滑块的支持。 int activeUrbanLevel, activeFarmLevel, activePlantLevel; bool applyUrbanLevel, applyFarmLevel, applyPlantLevel; … public void SetApplyFarmLevel (bool toggle) { applyFarmLevel = toggle; } public void SetFarmLevel (float level) { activeFarmLevel = (int)level; } public void SetApplyPlantLevel (bool toggle) { applyPlantLevel = toggle; } public void SetPlantLevel (float level) { activePlantLevel = (int)level; } … void EditCell (HexCell cell) { if (cell) { … if (applyUrbanLevel) { cell.UrbanLevel = activeUrbanLevel; } if (applyFarmLevel) { cell.FarmLevel = activeFarmLevel; } if (applyPlantLevel) { cell.PlantLevel = activePlantLevel; } … } }
将它们添加到用户界面。三个滑块。另外,将需要其他集合HexFeatureManager
。 public HexFeatureCollection[] urbanCollections, farmCollections, plantCollections;
救济对象的三个集合。我为农场和工厂创建了每个密度级别的两个预制件,以及建筑物集合。对于所有这些,我都使用了多维数据集。农场有浅绿色的物质,植物有深绿色的物质。我制作了高度为0.1个单位的农场立方体,以表示农田的方形分配。作为高密度标度,我选择了(2.5,0.1,2.5)和(3.5,0.1,2)。平均而言,站点的面积为1.75,大小为2.5 x 1.25。在区域1处获得的密度较低,尺寸为1.5 x 0.75。预制植物表示高大的树木和大型灌木。高密度预制件最大,分别为(1.25,4.5,1.25)和(1.5,3,1.5)。平均比例为(0.75、3、0.75)和(1、1.5、1)。最小的植物大小分别为(0.5、1.5、0.5)和(0.75、1、0.75)。浮雕特征的选择
每种类型的对象都必须接收自己的哈希值,以便它们具有不同的创建模式,并且可以混合使用它们。添加HexHash
两个附加值。 public float a, b, c, d, e; public static HexHash Create () { HexHash hash; hash.a = Random.value * 0.999f; hash.b = Random.value * 0.999f; hash.c = Random.value * 0.999f; hash.d = Random.value * 0.999f; hash.e = Random.value * 0.999f; return hash; }
现在,您必须HexFeatureManager.PickPrefab
使用不同的集合。添加参数以简化过程。另外,将所选预制件的变体使用的哈希更改为D,并将旋转的哈希更改为E。 Transform PickPrefab ( HexFeatureCollection[] collection, int level, float hash, float choice ) { if (level > 0) { float[] thresholds = HexMetrics.GetFeatureThresholds(level - 1); for (int i = 0; i < thresholds.Length; i++) { if (hash < thresholds[i]) { return collection[i].Pick(choice); } } } return null; } public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); Transform prefab = PickPrefab( urbanCollections, cell.UrbanLevel, hash.a, hash.d ); … instance.localRotation = Quaternion.Euler(0f, 360f * hash.e, 0f); instance.SetParent(container, false); }
目前AddFeature
选择预制城市化。这是正常现象,我们需要更多选择。因此,我们从农场中添加了另一个预制件。作为哈希值,请使用B。选项的选择将再次为D。 Transform prefab = PickPrefab( urbanCollections, cell.UrbanLevel, hash.a, hash.d ); Transform otherPrefab = PickPrefab( farmCollections, cell.FarmLevel, hash.b, hash.d ); if (!prefab) { return; }
结果,我们将创建哪种预制实例?如果其中之一为空,则选择是显而易见的。但是,如果两者都存在,那么我们需要做出决定。我们只添加具有最低哈希值的预制件。 Transform otherPrefab = PickPrefab( farmCollections, cell.FarmLevel, hash.b, hash.d ); if (prefab) { if (otherPrefab && hash.b < hash.a) { prefab = otherPrefab; } } else if (otherPrefab) { prefab = otherPrefab; } else { return; }
城乡混合体。接下来,使用C哈希值对植物进行相同的处理。 if (prefab) { if (otherPrefab && hash.b < hash.a) { prefab = otherPrefab; } } else if (otherPrefab) { prefab = otherPrefab; } otherPrefab = PickPrefab( plantCollections, cell.PlantLevel, hash.c, hash.d ); if (prefab) { if (otherPrefab && hash.c < hash.a) { prefab = otherPrefab; } } else if (otherPrefab) { prefab = otherPrefab; } else { return; }
但是,我们不能只复制代码。当我们选择农村而不是城市对象时,我们需要将植物的哈希与农场的哈希进行比较,而不是与城市的哈希进行比较。因此,我们需要跟踪我们决定选择的哈希并与之进行比较。 float usedHash = hash.a; if (prefab) { if (otherPrefab && hash.b < hash.a) { prefab = otherPrefab; usedHash = hash.b; } } else if (otherPrefab) { prefab = otherPrefab; usedHash = hash.b; } otherPrefab = PickPrefab( plantCollections, cell.PlantLevel, hash.c, hash.d ); if (prefab) { if (otherPrefab && hash.c < usedHash) { prefab = otherPrefab; } } else if (otherPrefab) { prefab = otherPrefab; } else { return; }
城市,农村和植物的混合物。统一包装第10部分:墙
- 我们围住单元格。
- 我们沿着细胞边缘建造墙。
- 让我们穿越河流和道路。
- 避免浇水,并与悬崖相连。
在这一部分中,我们将在墙的单元之间添加。没有什么比一堵高墙更具吸引力了。墙面编辑
为了支撑墙壁,我们需要知道将墙壁放置在何处。我们将它们沿着连接它们的边缘放在单元之间。由于已经存在的对象位于单元的中央,因此我们不必担心壁会穿过它们。沿边缘的墙壁。墙是地形物体,尽管很大。与其他对象一样,我们不会直接对其进行编辑。相反,我们将更改单元格。我们不会在墙壁上有单独的部分,但是会把整个细胞围起来。城墙财产
要支持围栅单元,请添加到HexCell
属性Walled
。这是一个简单的开关。由于墙壁位于单元之间,因此我们需要更新已编辑的单元及其相邻单元。 public bool Walled { get { return walled; } set { if (walled != value) { walled = value; Refresh(); } } } bool walled;
编辑器开关
要切换单元格的“隔离状态”,我们需要添加HexMapEditor
对切换的支持。因此,我们添加了另一个字段OptionalToggle
及其设置方法。 OptionalToggle riverMode, roadMode, walledMode; … public void SetWalledMode (int mode) { walledMode = (OptionalToggle)mode; }
与河流和道路不同,围墙不是从一个单元到另一个单元的,而是在它们之间。因此,我们无需考虑拖放。当墙壁开关处于活动状态时,我们仅基于此开关的状态来设置当前单元的隔离状态。 void EditCell (HexCell cell) { if (cell) { … if (roadMode == OptionalToggle.No) { cell.RemoveRoads(); } if (walledMode != OptionalToggle.Ignore) { cell.Walled = walledMode == OptionalToggle.Yes; } if (isDrag) { … } } }
我们复制UI开关的先前元素之一,并对其进行更改,以使它们控制“围栏”的状态。我将它们与其他对象一起放在UI面板中。开关“围栏”。统一包装创建墙
由于壁遵循单元的轮廓,因此它们不应具有恒定的形状。因此,我们不能像其他地形特征那样仅对它们使用预制件。相反,我们需要像使用浮雕一样构建一个网格。这意味着我们的预制片段需要另一个子元素HexMesh
。复制其他子网格之一,并使新的Walls对象投射阴影。除了顶点和三角形外,它们不需要任何其他内容,因此HexMesh
必须禁用所有选项。子公司预制墙。墙壁是城市物体,这是合乎逻辑的,所以对于它们,我使用建筑物的红色材料。墙面管理
由于墙壁是救济的对象,因此必须加以处理HexFeatureManager
。因此,我们将给救济对象的管理者一个指向Walls对象的链接,并使它调用Clear
和方法Apply
。 public HexMesh walls; … public void Clear () { … walls.Clear(); } public void Apply () { walls.Apply(); }
墙连接到地形管理器。Walls不应该成为Feature的子代吗?, . , Walls Hex Grid Chunk .
现在,我们需要向管理器添加一个方法,该方法允许我们向其添加墙。由于墙壁沿着单元之间的边缘,因此他需要知道边缘和单元的相应顶点。HexGridChunk
会TriangulateConnection
在对单元格及其邻居之一进行三角剖分时使其通过。从这个角度来看,当前单元格在墙的近侧,另一个在远侧。 public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell ) { }
HexGridChunk.TriangulateConnection
在完成所有其他连接工作之后以及紧接过渡到三角形三角形之前,我们将调用此新方法。我们将让救济对象的管理者自己决定墙应实际位于的位置。 void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { … } else { … } features.AddWall(e1, cell, e2, neighbor); HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { … } }
建造墙段
整个墙壁将蜿蜒穿过细胞的几个边缘。每个边缘仅包含一个墙元素。从近单元的角度来看,该段从肋的左侧开始,在右侧终止。让我们添加一个HexFeatureManager
单独的方法,该方法在一条边的角上使用四个顶点。 void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { }
近端和远端。AddWall
可以使用边缘的第一个和最后一个边缘调用此方法。但是,仅当我们在围栅单元和非围栅单元之间建立连接时,才应添加墙。哪一个单元在内部和哪个单元在外部都没有关系,仅考虑其状态的差异。 public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell ) { if (nearCell.Walled != farCell.Walled) { AddWallSegment(near.v1, far.v1, near.v5, far.v5); } }
墙的最简单部分是四分之一,位于肋骨的中间。我们会发现其较低的峰,从最近到最远的峰插值到中间。 void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { Vector3 left = Vector3.Lerp(nearLeft, farLeft, 0.5f); Vector3 right = Vector3.Lerp(nearRight, farRight, 0.5f); }
墙应该多高?让我们将其高度设置为HexMetrics
。我将它们设置为一个单元格高度级别的大小。 public const float wallHeight = 3f;
HexFeatureManager.AddWallSegment
可以使用此高度来定位四边形的第三和第四顶点,并将其添加到网格中walls
。 Vector3 left = Vector3.Lerp(nearLeft, farLeft, 0.5f); Vector3 right = Vector3.Lerp(nearRight, farRight, 0.5f); Vector3 v1, v2, v3, v4; v1 = v3 = left; v2 = v4 = right; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v1, v2, v3, v4);
现在我们可以编辑墙壁,它们将显示为四边形条纹。但是,我们不会看到连续的墙。每个四边形仅在一侧可见。它的脸朝向添加它的单元格。单面四边形墙。我们可以通过以相反的方式添加第二个四边形来快速解决此问题。 walls.AddQuad(v1, v2, v3, v4); walls.AddQuad(v2, v1, v4, v3);
双边墙。现在,所有墙壁都可以完整看到,但是在三个单元相交的单元的角落仍然有孔。我们稍后会填写。厚壁
尽管墙壁的两侧都已经可见,但是它们没有厚度。实际上,墙壁很薄,就像纸一样,并且在一定角度几乎看不见。因此,让我们通过增加厚度来使它们完整。在中设置其厚度HexMetrics
。我选择0.75单位的值,对我来说似乎很合适。 public const float wallThickness = 0.75f;
要使两堵墙变厚,需要将两个四边形分开到侧面。它们应朝相反的方向移动。一侧应移向近边缘,另一侧应移向远边缘。偏移向量是相等的far - near
,但要使墙的顶部保持平坦,我们需要将其分量Y设置为0。由于需要对壁段的左侧和右侧都进行此操作,因此,请向该HexMetrics
方法添加偏移向量以进行计算。 public static Vector3 WallThicknessOffset (Vector3 near, Vector3 far) { Vector3 offset; offset.x = far.x - near.x; offset.y = 0f; offset.z = far.z - near.z; return offset; }
为了使壁保留在肋骨的中心,沿该矢量的实际移动距离应等于每侧厚度的一半。为了确保我们确实移动了正确的距离,我们在缩放位移向量之前对其进行了归一化。 return offset.normalized * (wallThickness * 0.5f);
我们使用此方法HexFeatureManager.AddWallSegment
来更改四边形的位置。由于位移矢量从最近的单元格到最远的单元格,因此从近四边形减去它,然后加到远一单元格上。 Vector3 left = Vector3.Lerp(nearLeft, farLeft, 0.5f); Vector3 right = Vector3.Lerp(nearRight, farRight, 0.5f); Vector3 leftThicknessOffset = HexMetrics.WallThicknessOffset(nearLeft, farLeft); Vector3 rightThicknessOffset = HexMetrics.WallThicknessOffset(nearRight, farRight); Vector3 v1, v2, v3, v4; v1 = v3 = left - leftThicknessOffset; v2 = v4 = right - rightThicknessOffset; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v1, v2, v3, v4); v1 = v3 = left + leftThicknessOffset; v2 = v4 = right + rightThicknessOffset; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v2, v1, v4, v3);
具有偏移量的墙。现在,四边形有偏差,尽管这并不十分明显。壁厚是否相同?, «-» . , . . , . , . , - , . .
墙顶
为了使墙的厚度从上方可见,我们需要在墙的顶部添加一个四边形。最简单的方法是记住第一个四边形的两个上顶点,并将它们与第二个四边形的两个上顶点连接起来。 Vector3 v1, v2, v3, v4; v1 = v3 = left - leftThicknessOffset; v2 = v4 = right - rightThicknessOffset; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v1, v2, v3, v4); Vector3 t1 = v3, t2 = v4; v1 = v3 = left + leftThicknessOffset; v2 = v4 = right + rightThicknessOffset; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v2, v1, v4, v3); walls.AddQuad(t1, t2, v3, v4);
墙壁与顶部。转弯
我们在单元的角落仍然有孔。为了填充它们,我们需要在单元格之间的三角形区域添加一个线段。每个角连接三个单元。每个单元可能有也可能没有墙。即,八个配置是可能的。角度配置。我们仅将壁放置在具有不同栅栏状态的单元之间。这样可以将配置数量减少到六个。在每个单元中,一个单元位于壁的曲线内。让我们将此单元格视为壁弯曲的参考点。从该单元的角度来看,墙从与左单元共同的边缘开始,到与右单元共同的边缘结束。单元角色。也就是说,我们需要创建一个方法,AddWallSegment
其参数为拐角的三个顶点。尽管我们可以编写代码来对该部分进行三角剖分,但实际上这是该方法的特例AddWallSegment
。锚点在两个顶点附近都起作用。 void AddWallSegment ( Vector3 pivot, HexCell pivotCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { AddWallSegment(pivot, left, pivot, right); }
接下来,AddWall
为角度的三个顶点及其像元创建方法的变体。该方法的目的是确定角度(如果存在),它是参考点。因此,他必须考虑所有八个可能的配置并要求其中AddWallSegment
六个。 public void AddWall ( Vector3 c1, HexCell cell1, Vector3 c2, HexCell cell2, Vector3 c3, HexCell cell3 ) { if (cell1.Walled) { if (cell2.Walled) { if (!cell3.Walled) { AddWallSegment(c3, cell3, c1, cell1, c2, cell2); } } else if (cell3.Walled) { AddWallSegment(c2, cell2, c3, cell3, c1, cell1); } else { AddWallSegment(c1, cell1, c2, cell2, c3, cell3); } } else if (cell2.Walled) { if (cell3.Walled) { AddWallSegment(c1, cell1, c2, cell2, c3, cell3); } else { AddWallSegment(c2, cell2, c3, cell3, c1, cell1); } } else if (cell3.Walled) { AddWallSegment(c3, cell3, c1, cell1, c2, cell2); } }
要添加角线段,请在末尾调用此方法HexGridChunk.TriangulateCorner
。 void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); }
墙壁有角,但仍然有孔。封闭孔
壁上仍然有孔,因为壁段的高度是可变的。沿边缘的线段具有恒定的高度,而角线段则位于两个不同的边缘之间。由于每个边缘可以有其自己的高度,因此在拐角处会出现孔。要解决此问题,请对其进行更改AddWallSegment
,以使其分别存储左右顶部顶点的Y坐标。 float leftTop = left.y + HexMetrics.wallHeight; float rightTop = right.y + HexMetrics.wallHeight; Vector3 v1, v2, v3, v4; v1 = v3 = left - leftThicknessOffset; v2 = v4 = right - rightThicknessOffset; v3.y = leftTop; v4.y = rightTop; walls.AddQuad(v1, v2, v3, v4); Vector3 t1 = v3, t2 = v4; v1 = v3 = left + leftThicknessOffset; v2 = v4 = right + rightThicknessOffset; v3.y = leftTop; v4.y = rightTop; walls.AddQuad(v2, v1, v4, v3);
封闭的墙壁。现在,墙壁已关闭,但您可能仍会在墙壁的阴影中看到孔。这是由方向阴影设置的“ 正常偏移”参数引起的。当它大于零时,投射阴影的对象的三角形沿曲面的法线移动。这样可以避免自遮蔽,但同时在三角形朝不同方向看的情况下也会产生孔洞。在这种情况下,可以在精细几何形状的阴影(例如我们的墙)中创建孔。您可以通过将法向偏差降低为零来消除这些阴影伪影。或将Cast Shadows网格渲染器的墙模式更改为双面。这将使阴影投射对象渲染每个墙壁三角形的两侧以进行渲染,这将关闭所有孔。阴影中没有更多的孔了。统一包装壁架墙
到目前为止,我们的墙足够直。对于平坦的地形,这一点都还不错,但是当墙壁与壁架重合时,这看起来很奇怪。当墙的相对两侧的单元之间存在一个高度差时,就会发生这种情况。壁架上的直墙。跟随边缘
与其为整个边缘创建一个线段,不如为边缘条的每个部分创建一个线段。我们可以通过AddWallSegment
在AddWall
edge 版本中调用四次来做到这一点。 public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell ) { if (nearCell.Walled != farCell.Walled) { AddWallSegment(near.v1, far.v1, near.v2, far.v2); AddWallSegment(near.v2, far.v2, near.v3, far.v3); AddWallSegment(near.v3, far.v3, near.v4, far.v4); AddWallSegment(near.v4, far.v4, near.v5, far.v5); } }
弯曲的墙壁。现在,墙壁会重复变形的边缘形状。结合壁架,看起来更好。此外,它还在平坦的浮雕上创建了更多有趣的墙。将墙壁放在地面上
看着壁架上的墙壁,您会发现一个问题。墙壁悬在地面上!对于倾斜的平坦边缘来说确实如此,但通常不会那么明显。墙壁悬在空中。为了解决这个问题,我们需要降低墙壁。最简单的方法是降低整个墙壁,使其顶部保持平坦。同时,上侧的一部分墙会稍微降低到浮雕中,但这将适合我们。要降低墙的高度,我们需要确定哪一侧较低-近或远。我们可以只使用最低边的高度,而不必走得太低。您可以将Y坐标从低到高插值,其偏移量小于0.5。由于壁偶尔仅会变得高于壁架的较低台阶,因此我们可以将壁架的垂直台阶用作偏移量。壁架构造的不同壁厚可能需要不同的偏移量。降低的墙。 除了平均近顶点和远顶点的X和Z坐标外,我们还添加了处理该插值的HexMetrics
方法WallLerp
。它基于一种方法TerraceLerp
。 public const float wallElevationOffset = verticalTerraceStepSize; … public static Vector3 WallLerp (Vector3 near, Vector3 far) { near.x += (far.x - near.x) * 0.5f; near.z += (far.z - near.z) * 0.5f; float v = near.y < far.y ? wallElevationOffset : (1f - wallElevationOffset); near.y += (far.y - near.y) * v; return near; }
强制使用HexFeatureManager
此方法确定左顶点和右顶点。 void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { Vector3 left = HexMetrics.WallLerp(nearLeft, farLeft); Vector3 right = HexMetrics.WallLerp(nearRight, farRight); … }
站在地面上的墙壁。墙体变形变化
现在我们的墙壁高度一致。尽管它们靠近扭曲的边缘,但它们仍未完全对应于扭曲的边缘。发生这种情况是因为我们首先确定墙壁的顶部,然后将其变形。由于这些顶点位于近端和远端边缘的顶点之间,因此它们的变形会略有不同。壁不正确地跟随肋的事实不是问题。但是,壁顶部的变形会改变厚度相对均匀的厚度。如果我们根据变形的顶点排列墙,然后添加未变形的四边形,则其厚度应该不会有太大变化。 void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { nearLeft = HexMetrics.Perturb(nearLeft); farLeft = HexMetrics.Perturb(farLeft); nearRight = HexMetrics.Perturb(nearRight); farRight = HexMetrics.Perturb(farRight); … walls.AddQuadUnperturbed(v1, v2, v3, v4); … walls.AddQuadUnperturbed(v2, v1, v4, v3); walls.AddQuadUnperturbed(t1, t2, v3, v4); }
墙壁的顶部未变形。由于这种方法,墙壁将不再像以前那样精确地跟随边缘。但是作为回报,它们将变得不那么破裂,并且厚度将更恒定。壁厚更一致。统一包装墙上的洞
到目前为止,我们忽略了河流或道路横穿隔离墙的可能性。发生这种情况时,我们必须在墙壁上打一个洞,河流或道路可以通过。为此,添加AddWall
两个布尔参数以指示河流或道路是否通过一条边。尽管我们可以用不同的方式处理它们,但在两种情况下我们都只删除两个中间部分。 public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell, bool hasRiver, bool hasRoad ) { if (nearCell.Walled != farCell.Walled) { AddWallSegment(near.v1, far.v1, near.v2, far.v2); if (hasRiver || hasRoad) {
现在它HexGridChunk.TriangulateConnection
应该提供必要的数据。由于他已经需要相同的信息,因此让我们将其缓存在布尔变量中,并只记录一次对相应方法的调用。 void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … bool hasRiver = cell.HasRiverThroughEdge(direction); bool hasRoad = cell.HasRoadThroughEdge(direction); if (hasRiver) { … } if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor, hasRoad); } else { TriangulateEdgeStrip(e1, cell.Color, e2, neighbor.Color, hasRoad); } features.AddWall(e1, cell, e2, neighbor, hasRiver, hasRoad); … }
墙壁上的孔供河流和道路通过。我们覆盖墙壁
这些新的开口创造了完成墙壁的地方。我们需要用四边形封闭这些端点,以便我们无法看穿墙壁的侧面。让我们为此创建一个HexFeatureManager
方法AddWallCap
。它的工作方式类似于AddWallSegment
,但只需要一对近峰。让他添加一个四边形,从墙的近端到远端。 void AddWallCap (Vector3 near, Vector3 far) { near = HexMetrics.Perturb(near); far = HexMetrics.Perturb(far); Vector3 center = HexMetrics.WallLerp(near, far); Vector3 thickness = HexMetrics.WallThicknessOffset(near, far); Vector3 v1, v2, v3, v4; v1 = v3 = center - thickness; v2 = v4 = center + thickness; v3.y = v4.y = center.y + HexMetrics.wallHeight; walls.AddQuadUnperturbed(v1, v2, v3, v4); }
当AddWall
发现需要孔时,在第二对和第四对边缘之间添加一个覆盖。对于第四对顶点,您需要切换方向,否则四边形将向内看。 public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell, bool hasRiver, bool hasRoad ) { if (nearCell.Walled != farCell.Walled) { AddWallSegment(near.v1, far.v1, near.v2, far.v2); if (hasRiver || hasRoad) { AddWallCap(near.v2, far.v2); AddWallCap(far.v4, near.v4); } … } }
墙壁上的封闭孔。统一包装避免悬崖和水
最后,让我们看一下包含悬崖或水的边缘。由于悬崖本质上是大墙,因此在其上放置额外的墙是不合逻辑的。另外,它看起来会很糟糕。水下墙也是完全不合逻辑的,海岸墙的限制也是如此。墙壁在峭壁上和在水中。我们可以通过额外的检查来去除这些不必要的边缘上的墙AddWall
。墙不能在水下,与之共通的肋骨不能是悬崖。 public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell, bool hasRiver, bool hasRoad ) { if ( nearCell.Walled != farCell.Walled && !nearCell.IsUnderwater && !farCell.IsUnderwater && nearCell.GetEdgeType(farCell) != HexEdgeType.Cliff ) { … } }
去除了沿肋骨的障碍墙,但角落仍然保留。去除墙角
去除不必要的角部段将需要更多的努力。最简单的情况是支撑池在水下。这样可以确保附近没有可连接的墙段。 void AddWallSegment ( Vector3 pivot, HexCell pivotCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { if (pivotCell.IsUnderwater) { return; } AddWallSegment(pivot, left, pivot, right); }
没有更多的水下支持室。现在我们需要看看其他两个单元。如果其中之一在水下或通过中断连接到支撑单元,则沿该肋没有壁。如果至少一侧为真,则此角不应有墙段。我们分别确定是否有左墙或右墙。我们将结果放入布尔变量中,以使其更易于使用。 if (pivotCell.IsUnderwater) { return; } bool hasLeftWall = !leftCell.IsUnderwater && pivotCell.GetEdgeType(leftCell) != HexEdgeType.Cliff; bool hasRighWall = !rightCell.IsUnderwater && pivotCell.GetEdgeType(rightCell) != HexEdgeType.Cliff; if (hasLeftWall && hasRighWall) { AddWallSegment(pivot, left, pivot, right); }
去除了所有干扰角。死角
当左右边缘都没有墙时,工作就完成了。但是,如果墙壁仅在一个方向上,则意味着墙壁上还有另一个孔。因此,您需要关闭它。 if (hasLeftWall) { if (hasRighWall) { AddWallSegment(pivot, left, pivot, right); } else { AddWallCap(pivot, left); } } else if (hasRighWall) { AddWallCap(right, pivot); }
我们关闭墙壁。墙壁与悬崖的连接
在一种情况下,墙壁看起来不完美。当墙到达悬崖的底部时,它结束了。但是由于悬崖并非完全垂直,因此在墙壁和悬崖边缘之间会形成一个狭窄的孔。在悬崖的顶部,不会出现这样的问题。墙壁和悬崖壁之间的孔。如果墙一直延伸到悬崖的边缘,那就更好了。为此,我们可以在墙的当前端与悬崖的角顶之间添加另一个墙段。由于此部分的大部分将隐藏在悬崖内部,因此我们可以不将悬崖内部的壁厚减小到零。因此,对于我们来说,创建一个楔形就足够了:两个四边形指向该点,而三角形位于它们的顶部。让我们为此创建一个方法AddWallWedge
。这可以通过复制AddWallCap
并添加楔形点来完成。 void AddWallWedge (Vector3 near, Vector3 far, Vector3 point) { near = HexMetrics.Perturb(near); far = HexMetrics.Perturb(far); point = HexMetrics.Perturb(point); Vector3 center = HexMetrics.WallLerp(near, far); Vector3 thickness = HexMetrics.WallThicknessOffset(near, far); Vector3 v1, v2, v3, v4; Vector3 pointTop = point; point.y = center.y; v1 = v3 = center - thickness; v2 = v4 = center + thickness; v3.y = v4.y = pointTop.y = center.y + HexMetrics.wallHeight;
作为AddWallSegment
用于角部,我们将当壁进入仅在一个方向,并且该壁位于比另一侧的高度低调用此方法。在这些条件下,我们遇到了悬崖的边缘。 if (hasLeftWall) { if (hasRighWall) { AddWallSegment(pivot, left, pivot, right); } else if (leftCell.Elevation < rightCell.Elevation) { AddWallWedge(pivot, left, right); } else { AddWallCap(pivot, left); } } else if (hasRighWall) { if (rightCell.Elevation < leftCell.Elevation) { AddWallWedge(right, pivot, left); } else { AddWallCap(right, pivot); } }
, .unitypackage11:
.在上一部分中,我们添加了墙面支撑。这些是简单的直壁段,没有明显差异。现在,我们将通过在其上添加塔来使墙更有趣。必须按程序创建墙段以匹配浮雕。塔不是必需的,我们可以使用常规的预制件。我们可以用红色材料创建一个由两个立方体组成的简单塔。塔的底部尺寸为2 x 2个单位,高度为4个单位,也就是说,它比墙更厚且更高。在此立方体上方,我们将放置一个表示塔顶的单位立方体。像所有其他预制件一样,这些立方体不需要对撞机。由于塔模型由多个对象组成,因此我们将它们设为根对象的子代。放置它们,使根的本地原点位于塔的底部。因此,我们可以放置塔而不必担心它们的高度。预制塔。在此预制中添加链接HexFeatureManager
并进行连接。 public Transform wallTower;
链接到预制塔。建筑塔
让我们从在每个墙段的中间放置塔开始。为此,我们将在方法末尾创建一个塔AddWallSegment
。她的位置将是该段左,右点的平均值。 void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { … Transform towerInstance = Instantiate(wallTower); towerInstance.transform.localPosition = (left + right) * 0.5f; towerInstance.SetParent(container, false); }
每个墙段一塔。我们沿着墙有很多塔,但是它们的方向没有改变。我们需要更改其旋转角度,以使其与墙对齐。由于我们具有墙的左右两个点,因此我们知道哪个方向是正确的。我们可以利用这些知识来确定墙段的方向,从而确定塔的方向。无需自己计算旋转量,我们只需将一个Transform.right
向量分配给property即可。统一代码将改变对象的旋转,以使其局部方向右与所传输的矢量相对应。 Transform towerInstance = Instantiate(wallTower); towerInstance.transform.localPosition = (left + right) * 0.5f; Vector3 rightDirection = right - left; rightDirection.y = 0f; towerInstance.transform.right = rightDirection; towerInstance.SetParent(container, false);
塔与墙对齐。Transform.right分配如何工作?Quaternion.FromToRotation
. .
public Vector3 right { get { return rotation * Vector3.right; } set { rotation = Quaternion.FromToRotation(Vector3.right, value); } }
减少塔数
每个墙段一个塔太多。让我们通过向AddWallSegment
布尔值添加参数来使添加塔成为可选操作。将其设置为默认值false
。在这种情况下,所有的塔都将消失。 void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight, bool addTower = false ) { … if (addTower) { Transform towerInstance = Instantiate(wallTower); towerInstance.transform.localPosition = (left + right) * 0.5f; Vector3 rightDirection = right - left; rightDirection.y = 0f; towerInstance.transform.right = rightDirection; towerInstance.SetParent(container, false); } }
让我们将塔仅放置在牢房的角落。结果,我们得到的塔数更少,并且它们之间的距离相当恒定。 void AddWallSegment ( Vector3 pivot, HexCell pivotCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … AddWallSegment(pivot, left, pivot, right, true); … }
塔只在角落。看起来足够好,但是我们可能需要更少的塔定期放置。与其他地形特征一样,我们可以使用哈希表来决定是否将塔楼放在角落。为此,我们使用角的中心对表进行采样,然后将哈希值之一与塔的阈值进行比较。 HexHash hash = HexMetrics.SampleHashGrid( (pivot + left + right) * (1f / 3f) ); bool hasTower = hash.e < HexMetrics.wallTowerThreshold; AddWallSegment(pivot, left, pivot, right, hasTower);
阈值为HexMetrics
。值为0.5时,将在一半情况下创建塔,但是我们可以创建具有许多塔或根本没有塔的墙。 public const float wallTowerThreshold = 0.5f;
随机塔。我们从山坡上卸下塔楼
现在,无论地形如何,我们都可以放置塔。但是,在塔的斜坡上看起来不合逻辑。在这里,墙壁成一定角度,可以切穿塔顶。塔在斜坡上。为了避免倾斜,我们将检查左右角单元是否在相同的高度。仅在这种情况下才可以放置塔。 bool hasTower = false; if (leftCell.Elevation == rightCell.Elevation) { HexHash hash = HexMetrics.SampleHashGrid( (pivot + left + right) * (1f / 3f) ); hasTower = hash.e < HexMetrics.wallTowerThreshold; } AddWallSegment(pivot, left, pivot, right, hasTower);
斜坡的墙壁上不再有塔。我们将墙壁和塔楼放在地面上
尽管我们避免在倾斜的地方使用墙,但墙两侧的浮雕仍可以具有不同的高度。墙壁可以沿着壁架延伸,而相同高度的单元可以具有不同的垂直位置。因此,塔架的底部可能在空中。塔在空中。实际上,斜坡上的墙壁也可以悬挂在空中,但这并不像塔楼那么明显。墙壁在空中。这可以通过将墙壁和塔的底部拉伸到地面来解决。为此,请为中的墙添加Y偏移HexMetrics
。一单位下来就足够了。将塔的高度增加相同的数量。 public const float wallHeight = 4f; public const float wallYOffset = -1f;
我们对其进行更改,HexMetrics.WallLerp
以便在确定Y坐标时考虑新的偏移量。 public static Vector3 WallLerp (Vector3 near, Vector3 far) { near.x += (far.x - near.x) * 0.5f; near.z += (far.z - near.z) * 0.5f; float v = near.y < far.y ? wallElevationOffset : (1f - wallElevationOffset); near.y += (far.y - near.y) * v + wallYOffset; return near; }
我们还需要更改塔的预制件,因为基座现在将在地下一层。因此,我们将基础立方体的高度增加了一个单位,并相应地更改了立方体的局部位置。在地面上的墙壁和塔。统一包装桥梁
在这个阶段,我们有河流和道路,但是道路无法以任何方式穿越河流。现在是添加网桥的合适时间。让我们从一个简单的可缩放立方体开始,它将扮演预制桥梁的角色。河流的宽度各不相同,但两侧道路中心之间大约有七个距离单位。因此,我们给它一个近似的比例(3,1,7)。添加预制的红色城市材料,并摆脱其对撞机。与塔一样,将立方体按相同比例放置在根对象中。因此,桥本身的几何形状并不重要。在桥的预制件上添加一个链接,HexFeatureManager
并为其分配一个预制件。 public Transform wallTower, bridge;
已分配桥梁预制件。桥的位置
要放置桥,我们需要一种方法HexFeatureManager.AddBridge
。桥梁应位于河流中心和河流两侧之间。 public void AddBridge (Vector3 roadCenter1, Vector3 roadCenter2) { Transform instance = Instantiate(bridge); instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f; instance.SetParent(container, false); }
我们将传输未变形的道路中心,因此在放置桥梁之前,我们将必须对它们进行变形。 roadCenter1 = HexMetrics.Perturb(roadCenter1); roadCenter2 = HexMetrics.Perturb(roadCenter2); Transform instance = Instantiate(bridge);
为了正确对准桥,我们可以使用与旋转塔时相同的方法。在这种情况下,道路中心定义桥梁的前向矢量。由于我们保持在同一像元内,因此此向量肯定是水平的,因此我们不需要将其分量Y归零。 Transform instance = Instantiate(bridge); instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f; instance.forward = roadCenter2 - roadCenter1; instance.SetParent(container, false);
我们在直河上架起桥梁
唯一需要桥梁的河流构造是直的和弯曲的。道路可以通过端点,而在之字形道路只能在附近。首先,让我们找出直河。在内部,HexGridChunk.TriangulateRoadAdjacentToRiver
第一操作员else if
在这些河流附近布置道路。因此,我们将在此处添加网桥。我们在河的一侧。道路的中心从河上移开,然后单元的中心也移动了。要在另一侧找到道路的中心,我们需要将相反的方向移动相同的量。必须在更改中心本身之前完成此操作。 void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { … roadCenter += corner * 0.5f; features.AddBridge(roadCenter, center - corner * 0.5f); center += corner * 0.25f; } … }
直河上的桥梁。桥梁出现了!但是现在,在河不流经的每个方向上都有一个桥梁实例。我们需要确保在单元中仅生成一个桥实例。这可以通过选择相对于河流的一个方向并在其基础上生成桥梁来完成。您可以选择任何方向。 roadCenter += corner * 0.5f; if (cell.IncomingRiver == direction.Next()) { features.AddBridge(roadCenter, center - corner * 0.5f); } center += corner * 0.25f;
此外,我们仅在河两岸都有道路时才需要加桥。目前,我们已经确定当前的道路。因此,您需要检查在河的另一边是否有道路。 if (cell.IncomingRiver == direction.Next() && ( cell.HasRoadThroughEdge(direction.Next2()) || cell.HasRoadThroughEdge(direction.Opposite()) )) { features.AddBridge(roadCenter, center - corner * 0.5f); }
双方道路之间的桥梁。弯曲的河流上的桥梁
弯曲河流上的桥梁工作原理相似,但拓扑结构略有不同。当我们在曲线的外部时,我们将添加一个桥。这发生在最后一个块中else
。它使用中间方向来偏移道路的中心。我们将需要以不同的比例两次使用此偏移量,因此将其保存到变量中。 void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … else { HexDirection middle; if (previousHasRiver) { middle = direction.Next(); } else if (nextHasRiver) { middle = direction.Previous(); } else { middle = direction; } if ( !cell.HasRoadThroughEdge(middle) && !cell.HasRoadThroughEdge(middle.Previous()) && !cell.HasRoadThroughEdge(middle.Next()) ) { return; } Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle); roadCenter += offset * 0.25f; } … }
曲线外部的位移比例为0.25,内部为HexMetrics.innerToOuter * 0.7f
。我们用它来放置桥。 Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle); roadCenter += offset * 0.25f; features.AddBridge( roadCenter, center - offset * (HexMetrics.innerToOuter * 0.7f) );
弯曲的河流上的桥梁。再次,我们需要避免重复的桥接。我们可以通过仅从中间方向添加桥来做到这一点。 Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle); roadCenter += offset * 0.25f; if (direction == middle) { features.AddBridge( roadCenter, center - offset * (HexMetrics.innerToOuter * 0.7f) ); }
同样,您需要确保道路在对面。 if ( direction == middle && cell.HasRoadThroughEdge(direction.Opposite()) ) { features.AddBridge( roadCenter, center - offset * (HexMetrics.innerToOuter * 0.7f) ); }
双方道路之间的桥梁。桥梁缩放
由于我们扭曲了地形,因此道路中心和河流对岸之间的距离会发生变化。有时桥太短,有时太长。距离可变,但桥长不变。尽管我们创建的桥的长度为7个单位,但您可以对其进行缩放以匹配道路中心之间的真实距离。这意味着桥梁模型变形了。由于距离变化不大,因此变形比不适合长度的桥更容易接受。为了进行适当的缩放,我们需要知道桥梁预制件的初始长度。我们将此长度存储在中HexMetrics
。 public const float bridgeDesignLength = 7f;
现在,我们可以将沿桥梁Z实例的比例分配给道路中心之间的距离,再除以原始长度。由于桥的预制件的根部具有相同的比例,因此桥将正确拉伸。 public void AddBridge (Vector3 roadCenter1, Vector3 roadCenter2) { roadCenter1 = HexMetrics.Perturb(roadCenter1); roadCenter2 = HexMetrics.Perturb(roadCenter2); Transform instance = Instantiate(bridge); instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f; instance.forward = roadCenter2 - roadCenter1; float length = Vector3.Distance(roadCenter1, roadCenter2); instance.localScale = new Vector3( 1f, 1f, length * (1f / HexMetrics.bridgeDesignLength) ); instance.SetParent(container, false); }
不断变化的桥梁长度。桥梁建设
代替简单的多维数据集,我们可以使用更有趣的桥模型。例如,您可以创建由三个缩放和旋转的多维数据集组成的粗糙拱形桥。当然,您可以创建更复杂的3D模型,包括道路的一部分。但是请注意,整个对象将略微压缩和拉伸。不同长度的拱形桥。统一包装特殊物品
到目前为止,我们的单元格可以包含城市,农村和植物对象。即使它们每个都有三个级别,但与单元格的大小相比,所有对象都非常小。如果我们需要一幢大型建筑,例如城堡,该怎么办?让我们向地形添加一种特殊类型的对象。这样的物体太大,以至于占据了整个单元。这些对象中的每一个都是唯一的,并且需要自己的预制件。例如,一个简单的城堡可以由一个中央立方体和四个角楼组成。中央立方体的刻度(6、4、6)将产生足够大的锁,即使在严重变形的单元中也可以安装。城堡的预制件。另一个特殊的对象可以是之字形,例如,由三个相互叠置的立方体构成。对于下部立方体,刻度(8、2.5、8)是合适的。预制锯齿形。特殊对象可以是任何对象,不一定是建筑对象。例如,一组高达十个单位高的大树可以指示一个充满巨型植物的细胞。预制大型植物。添加到HexFeatureManager
阵列以跟踪这些预制件。 public Transform[] special;
首先,向阵列中添加一个城堡,然后添加ziggurat,然后添加大型植物。特殊对象的定制。使细胞变得特别
现在HexCell
,需要特殊对象的索引,该索引确定特殊对象的类型(如果存在)。 int specialIndex;
像其他救济对象一样,让我们为其提供接收和设置此值的能力。 public int SpecialIndex { get { return specialIndex; } set { if (specialIndex != value) { specialIndex = value; RefreshSelfOnly(); } } }
默认情况下,该单元格不包含特殊对象。我们用索引0表示它。添加使用此方法确定单元格是否特殊的属性。 public bool IsSpecial { get { return specialIndex > 0; } }
要编辑单元格,请添加对特殊对象索引的支持HexMapEditor
。它的工作方式类似于城市,农村和工厂设施的水平。 int activeUrbanLevel, activeFarmLevel, activePlantLevel, activeSpecialIndex; … bool applyUrbanLevel, applyFarmLevel, applyPlantLevel, applySpecialIndex; … public void SetApplySpecialIndex (bool toggle) { applySpecialIndex = toggle; } public void SetSpecialIndex (float index) { activeSpecialIndex = (int)index; } … void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (applyWaterLevel) { cell.WaterLevel = activeWaterLevel; } if (applySpecialIndex) { cell.SpecialIndex = activeSpecialIndex; } if (applyUrbanLevel) { cell.UrbanLevel = activeUrbanLevel; } … } }
将滑块添加到UI中以控制特殊对象。由于我们有三个对象,因此在滑块中使用间隔0–3。零表示没有物体,一个-城堡,两个-锯齿形琴,三个-巨型植物。特殊对象的滑块。添加特殊对象
现在我们可以为单元格分配特殊的对象。为了使它们出现,我们需要添加到HexFeatureManager
另一种方法。它仅创建所需特殊对象的实例并将其放置在所需位置。由于零表示不存在对象,因此在访问预制阵列之前,必须从单元的特殊对象的索引中减去单位。 public void AddSpecialFeature (HexCell cell, Vector3 position) { Transform instance = Instantiate(special[cell.SpecialIndex - 1]); instance.localPosition = HexMetrics.Perturb(position); instance.SetParent(container, false); }
让我们使用哈希表对对象进行任意旋转。 public void AddSpecialFeature (HexCell cell, Vector3 position) { Transform instance = Instantiate(special[cell.SpecialIndex - 1]); instance.localPosition = HexMetrics.Perturb(position); HexHash hash = HexMetrics.SampleHashGrid(position); instance.localRotation = Quaternion.Euler(0f, 360f * hash.e, 0f); instance.SetParent(container, false); }
对单元格进行三角剖分时,我们HexGridChunk.Triangulate
将检查该单元格是否包含特殊对象。如果是这样,那么我们就像一样调用我们的新方法AddFeature
。 void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell, cell.Position); } if (cell.IsSpecial) { features.AddSpecialFeature(cell, cell.Position); } }
特殊对象。它们比平常大得多。避开河流
由于特殊对象位于单元格的中心,因此它们不会与河流结合,因为它们会悬在河流上方。在河上的对象。为了防止在河流上创建特殊对象,我们更改了属性HexCell.SpecialIndex
。仅当单元格中没有河流时,我们才会更改索引。 public int SpecialIndex { … set { if (specialIndex != value && !HasRiver) { specialIndex = value; RefreshSelfOnly(); } } }
另外,添加河流时,我们将需要除去所有特殊对象。河水应将它们洗净。这可以通过在方法中将HexCell.SetOutgoingRiver
特殊对象的索引设置为0 来完成。 public void SetOutgoingRiver (HexDirection direction) { … hasOutgoingRiver = true; outgoingRiver = direction; specialIndex = 0; neighbor.RemoveIncomingRiver(); neighbor.hasIncomingRiver = true; neighbor.incomingRiver = direction.Opposite(); neighbor.specialIndex = 0; SetRoad((int)direction, false); }
我们避开道路
像河流一样,道路上的特殊物体也很差劲,但并非一切都那么可怕。您甚至可以原样离开道路。一些设施可能与道路兼容,而其他设施则可能不兼容。因此,可以使它们依赖于对象。但是我们将使其变得更容易。在路上的对象。在这种情况下,请让特殊的物体挡路。因此,在更改特殊对象的索引时,我们还将从单元格中删除所有道路。 public int SpecialIndex { … set { if (specialIndex != value && !HasRiver) { specialIndex = value; RemoveRoads(); RefreshSelfOnly(); } } }
此外,这意味着在添加道路时,我们将必须执行其他检查。仅当所有像元都不是具有特殊对象的像元时,我们才添加道路。 public void AddRoad (HexDirection direction) { if ( !roads[(int)direction] && !HasRiverThroughEdge(direction) && !IsSpecial && !GetNeighbor(direction).IsSpecial && GetElevationDifference(direction) <= 1 ) { SetRoad((int)direction, true); } }
避免其他物体
特殊对象不能与其他类型的对象混合。如果它们重叠,则看起来会不整洁。它也可能取决于特定的对象,但是我们将使用相同的方法。与其他对象相交的对象。在这种情况下,我们将隐藏较小的对象,就像它们在水下一样。这次我们将签入HexFeatureManager.AddFeature
。 public void AddFeature (HexCell cell, Vector3 position) { if (cell.IsSpecial) { return; } … }
避免饮水
我们也有水问题。洪水期间特殊功能会持续存在吗?由于我们要摧毁淹没单元中的小物体,因此对特殊物体也要这样做。水中的物体。由于HexGridChunk.Triangulate
我们执行了洪水和特殊,和普通对象相同的检查。 void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell, cell.Position); } if (!cell.IsUnderwater && cell.IsSpecial) { features.AddSpecialFeature(cell, cell.Position); } }
由于两个操作员if
现在都在检查电池是否在水下,因此我们可以转移测试并仅执行一次。 void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } if (!cell.IsUnderwater) { if (!cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell, cell.Position); } if (cell.IsSpecial) { features.AddSpecialFeature(cell, cell.Position); } } }
对于实验来说,这么多的对象对我们来说就足够了。统一包装