• 设计一个通用的网格Job框架。
  • 定义单独的网格流和生成器。
  • 禁用对 native container 访问的限制。
  • 在 XZ 平面上创建一个四边形网格。
  • 生成四边形行而不是单个四边形。

  这是关于程序网格的系列教程中的第二篇。上一篇教程介绍了高级 Mesh API。这次我们将使用该 API 制作一个 Burst Job,该Job生成一个由多个四边形组成的方形网格。

本教程使用 Unity 2020.3.18f1 制作。


unity 2d 网格地图_数据

1 程序网格Job框架

  方形网格只是我们可以生成的许多程序网格之一。因此,我们将首先设计一个支持通用方法的框架,而不是直接从方形网格Job开始。这将有点类似于我们在伪随机噪声系列中使用的通用方法,但有一些不同之处。

1.1 通用顶点

  我们要做的第一件事是定义一个通用的 Vertex 结构类型来保存顶点数据。让我们将其资产文件放在 Scripts / Procedural Meshes 子文件夹中。


unity 2d 网格地图_unity_02

  Vertex 的内容与 AdvancedSingleStreamProceduralMesh.Vertex 相同,只是我们不会为最小化它的大小而烦恼,因此为所有内容赋予适当的浮点类型。

using Unity.Mathematics;

public struct Vertex {
	public float3 position, normal;
	public float4 tangent;
	public float2 texCoord0;
}

  在 Pseudorandom Noise 系列中,我们将所有与噪声相关的类型放在一个类中,并使用部分类将代码拆分为多个文件。这次我们将使用不同的方法:自定义命名空间,我们将其命名为 ProceduralMeshes。

  要使类型成为命名空间的一部分,必须在具有适当名称的命名空间块内定义它,就好像它嵌套在类块内一样。对顶点执行此操作。

using Unity.Mathematics;

namespace ProceduralMeshes {

	public struct Vertex {
		public float3 position, normal;
		public float4 tangent;
		public float2 texCoord0;
	}
}

  我们也将把所有其他 ProceduralMeshes 类型的资源放在 Procedural Meshes 文件夹中。

1.2 网格流

  为了存储网格数据,我们需要定义顶点和索引缓冲区,并以适当的格式复制相关数据。我们将通过引入 ProceduralMeshes.IMeshStreams 接口来隔离此代码,而不是为每个Job显式定义此代码。它将负责设置顶点和索引缓冲区,隐藏有多少流的详细信息以及确切的数据格式是什么。

using Unity.Mathematics;
using UnityEngine;

namespace ProceduralMeshes {

	public interface IMeshStreams { }
}

  它的首要职责是初始化网格数据。我们将为此定义一个 Setup 方法,将网格数据作为参数,以及所需的顶点数和索引数。

void Setup(Mesh.MeshData data, int vertexCount, int indexCount);

  它还负责将顶点复制到网格的顶点缓冲区,无论流的数量和数据格式如何。为此,我们将使用 SetVertex 方法,并将顶点索引和数据设置为参数。

void SetVertex(int index, Vertex data);

  我们对索引缓冲区也要这样做。由于使用三角形而不是单个索引更方便,我们将定义一个SetTriangle方法,将三角形索引和int3顶点索引三合一作为参数。

void SetTriangle(int index, int3 triangle);

  该接口最直接的实现是单流方法。我们将这种类型命名为 SingleStream,它必须是一个结构才能处理 Burst Job。我们还将在 ProceduralMeshes.Streams 嵌套命名空间中对流实现进行分组。我还将他们的资产放在 Scripts / Procedural Meshes / Streams 子文件夹中。

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Rendering;

namespace ProceduralMeshes.Streams {

    public struct SingleStream : IMeshStreams {}
}

  添加Setup方法,用它来定义网格的缓冲区,就像我们在AdvancedSingleStreamProceduralMesh中做的那样,只是我们将在所有地方使用32位浮点。同时立即设置单个子网格,还不用担心它的边界。

public void Setup (Mesh.MeshData meshData, int vertexCount, int indexCount) {
			var descriptor = new NativeArray<VertexAttributeDescriptor>(
				4, Allocator.Temp, NativeArrayOptions.UninitializedMemory
			);
			descriptor[0] = new VertexAttributeDescriptor(dimension: 3);
			descriptor[1] = new VertexAttributeDescriptor(
				VertexAttribute.Normal, dimension: 3
			);
			descriptor[2] = new VertexAttributeDescriptor(
				VertexAttribute.Tangent, dimension: 4
			);
			descriptor[3] = new VertexAttributeDescriptor(
				VertexAttribute.TexCoord0, dimension: 2
			);
			meshData.SetVertexBufferParams(vertexCount, descriptor);
			descriptor.Dispose();

			meshData.SetIndexBufferParams(indexCount, IndexFormat.UInt32);
			
			meshData.subMeshCount = 1;
			meshData.SetSubMesh(0, new SubMeshDescriptor(0, indexCount));
		}

  为了在单个流中存储顶点数据,引入一个私有的嵌套Stream0类型。它与顶点完全匹配,只是在这里我们应该确保字段的顺序是固定的,将StructLayout(LayoutKind.Sequential)属性附加到它。用它来为这个流定义一个本地数组字段,并在Setup的最后检索它。

[StructLayout(LayoutKind.Sequential)]
		struct Stream0 {
			public float3 position, normal;
			public float4 tangent;
			public float2 texCoord0;
		}
		
		NativeArray<Stream0> stream0;
	
		public void Setup (Mesh.MeshData meshData, int vertexCount, int indexCount) {
			…

			stream0 = meshData.GetVertexData<Stream0>();
		}

  SetVertex的实现包括将顶点数据复制到一个Stream0值,并将其存储在流中的适当索引处。

public void SetVertex (int index, Vertex vertex) => stream0[index] = new Stream0 {
			position = vertex.position,
			normal = vertex.normal,
			tangent = vertex.tangent,
			texCoord0 = vertex.texCoord0
		};

  我们对SetVertex的实现是微不足道的,但它可能要复杂得多,例如,如果我们决定将部分数据存储为16位值,需要进行转换。在这种情况下,Burst可能决定只包含一次SetVertex代码,并在每次顶点被设置时插入一条调用指令–方法调用。这种方法很慢,而且无法进行积极的代码优化。因此,我们将指示Burst总是在线插入整个代码,而不是去调用。这是通过给方法附加MethodImpl属性来实现的,MethodImplOptions.AggressiveInlining是其参数。这些类型是System.Runtime.CompilerServices命名空间的一部分。

[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void SetVertex (int index, Vertex vertex) => stream0[index] = new Stream0 {
			…
		};

我们不应该总是建议对Burst代码进行积极的内联吗?
  作为经验法则,你确实可以一直这样做,但大多数情况下没有必要。小的方法会被自动内联,那些只使用一个的方法也会被内联。为了确定这一点,请检查由Burst生成的代码,看看是否存在不需要的调用指令。
  在我们的具体案例中,我们将在每个四边形中调用SetVertex四次。如果我们在SetVertex中加入从浮点转换为半数的代码,那么Burst很可能不会内联这个方法。在这一点上,SetVertex不需要这个属性,但是我把它作为一个示范。

  最后,我们可以直接将三角形数据复制到索引缓冲区,将索引数据重新解释为int3三角形数据。将本地数组存储在Setup末尾的一个字段中,并在SetTriangle中执行复制。

NativeArray<int3> triangles;
		
		public void Setup (Mesh.MeshData meshData, int vertexCount, int indexCount) {
			…
			
			stream0 = meshData.GetVertexData<Stream0>();
			triangles = meshData.GetIndexData<int>().Reinterpret<int3>(4);
		}
		
		…
		
		public void SetTriangle (int index, int3 triangle) => triangles[index] = triangle;
1.3 网格生成器

  我们还将为负责生成网格的那部分代码引入一个接口,将其命名为ProceduralMeshes.IMeshGenerator。它定义了被Job执行的代码,所以它需要一个带有索引参数的Execute方法。我们也给它第二个参数,用于存储流。这必须是一个通用参数,被限制为一个实现IMeshStreams的结构。我们不需要让整个接口都是通用的,我们可以只限制在Execute方法上

using UnityEngine;
						
namespace ProceduralMeshes {

	public interface IMeshGenerator {

		void Execute<S> (int i, S streams) where S : struct, IMeshStreams;
	}
}

  我们需要知道被生成的网格的顶点数量,生成器可以通过VertexCount getter属性来提供。我们可以通过写int VertexCount { get; }将其添加到接口中。同时包括一个索引计数的getter属性。

int VertexCount { get; }
		
		int IndexCount { get; }

  除此之外,在调度Job时也必须知道Job的长度。添加一个JobLength getter属性来提供这个信息。

int JobLength { get; }

  为了生成我们的方形网格,我们必须通过定义ProceduralMeshes.Generators.SquareGrid结构类型来实现这个接口,再一次在一个嵌套的命名空间和独立的子文件夹中。

using Unity.Mathematics;
using UnityEngine;

using static Unity.Mathematics.math;

namespace ProceduralMeshes.Generators {

	public struct SquareGrid : IMeshGenerator {}
}

  我们现在还不会生成网格,重点是先完成框架。所以现在只提供一个最小的实现,生成一个空网格,什么都不做。

public int VertexCount => 0;

		public int IndexCount => 0;

		public int JobLength => 0;

		public void Execute<S> (int z, S streams) where S : struct, IMeshStreams {}
1.4 Mesh Job

  下一步是定义一个Burst Job来生成网格,为此我们引入了ProceduralMeshes.MeshJob类型。这是一个通用的IJobFor结构,带有IMeshGenerator和IMeshStreams的类型参数。

using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;

namespace ProceduralMeshes {

	[BurstCompile(FloatPrecision.Standard, FloatMode.Fast, CompileSynchronously = true)]
	public struct MeshJob<G, S> : IJobFor
		where G : struct, IMeshGenerator
		where S : struct, IMeshStreams {}
}

  给它的生成器和流的私有字段。它的Execute方法只是将调用转发给生成器,将索引和流都传给它。

G generator;

		S streams;

		public void Execute (int i) => generator.Execute(i, streams);

  因为我们只在生成网格时向流写入,而不从流中读取,所以我们可以给流附加WriteOnly属性。这将间接地将只写状态应用到IMeshStreams实现所包含的本地数组上。

[WriteOnly]
		S streams;

  就像我们在伪随机噪声系列中所做的那样,我们也给这个Job提供了自己的公共静态ScheduleParallel方法,用于创建和调度这个Job,并返回其Job句柄。它需要网格数据和Job时间作为参数。在这种情况下,我们必须在调度前调用Job流的Setup,将网格数据与我们从Job生成器中获取的顶点和索引计数一起传递给它。

public static JobHandle ScheduleParallel (
			Mesh.MeshData meshData, JobHandle dependency
		) {
			var job = new MeshJob<G, S>();
			job.streams.Setup(
				meshData, job.generator.VertexCount, job.generator.IndexCount
			);
			return job.ScheduleParallel(job.generator.JobLength, 1, dependency);
		}
1.5 程序网格组件

  为了尝试我们的框架,我们将创建一个ProceduralMesh组件类型,它将设置其MeshFilter组件的网格,就像之前教程中的组件一样。这个类型并不是框架本身的一部分,所以我们将把它的资产放在Scripts文件夹中。另外,由于它不是我们命名空间的一部分,我们必须将它们全部导入。

using ProceduralMeshes;
using ProceduralMeshes.Generators;
using ProceduralMeshes.Streams;
using UnityEngine;
using UnityEngine.Rendering;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class ProceduralMesh : MonoBehaviour {}

  这一次我们将在Awake方法中创建Mesh对象,生成Mesh,并将其分配给MeshFilter。我们把生成Mesh的代码放在一个单独的GenerateMesh方法中,并通过一个字段来跟踪Mesh的情况。

Mesh mesh;

	void Awake () {
		mesh = new Mesh {
			name = "Procedural Mesh"
		};
		GenerateMesh();
		GetComponent<MeshFilter>().mesh = mesh;
	}
	
	void GenerateMesh () {}

  生成Mesh包括分配可写的Mesh数据,然后为其调度并立即完成一个MeshJob–使用我们的SquareGrid和SingleStream类型–然后应用于Mesh。

void GenerateMesh () {
		Mesh.MeshDataArray meshDataArray = Mesh.AllocateWritableMeshData(1);
		Mesh.MeshData meshData = meshDataArray[0];

		MeshJob<SquareGrid, SingleStream>.ScheduleParallel(
			meshData, default
		).Complete();

		Mesh.ApplyAndDisposeWritableMeshData(meshDataArray, mesh);
	}

  现在创建一个程序网格游戏对象,无论是在新场景中,还是替换上一教程中现有的四边形生成游戏对象


unity 2d 网格地图_数据_03

1.6 生成四边形

  在这一点上,当进入播放模式时,会产生一个空网格。


unity 2d 网格地图_unity_04

  我们得到一个空网格,因为我们的Job还没有做任何事情。目前该Job根本没有安排,因为它的长度为零。我们通过让 SquareGrid.JobLength 返回 1 来激活Job。

public int JobLength => 1;

  这导致我们的Job被安排,但是当进入播放模式时,我们现在得到一个无效的操作异常,抱怨说两个容器可能是同一个东西。这指的是SingleStream的两个本地数组。Unity抱怨说它们可能是别名,这意味着本地数组可能代表重叠的数据。原因是所有的网格数据都是一个未被管理的内存块。我们的工作试图同时访问这些数据的两个部分–顶点部分和三角形索引部分,Unity不允许这样做,因为它可能产生错误的结果。

  一般来说,Unity 的安全检查是有效的,应该注意,但在这种情况下,我们确定顶点和索引数据永远不会重叠。因此,我们将通过将 Unity.Collections.LowLevel.Unsafe 命名空间中的 NativeDisableContainerSafetyRestriction 属性附加到两个原生数组字段来禁用安全性。

using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Mathematics;
…

namespace ProceduralMeshes.Streams {
	
	public struct SingleStream : IMeshStreams {
		
		…
		
		[NativeDisableContainerSafetyRestriction]
		NativeArray<Stream0> stream0;

		[NativeDisableContainerSafetyRestriction]
		NativeArray<int3> triangles;
		
		…
	}
}

  为了测试我们的框架,我们将让SquareGrid暂时只生成一个四边形,与我们在上一个教程中生成的四边形完全一样。所以它的顶点数量必须变成四个。

public int VertexCount => 4;

  在执行中,首先创建一个通用顶点值,并设置它的法线和切线向量,这对于所有顶点都是相同的。由于所有值都初始化为零,我们只需设置非零分量就足够了。因此,法线 Z 分量变为 -1,切线 XW 分量变为 1 和 -1。

public void Execute (int i, S streams) {
			var vertex = new Vertex();
			vertex.normal.z = -1f;
			vertex.tangent.xw = float2(1f, -1f);
		}

xw 的分配是如何工作的?
这是一个swizzle操作,它允许我们按照我们想要的顺序对向量的一个子集进行赋值。swizzle操作也可以用来提取组件的子集,不管我们喜欢什么顺序,甚至是重复的。例如,我们也可以访问.yx、.zy、.xx、.xy、.zxxy等等,而不是.xy。数学类型通过属性实现这些。

  然后我们可以设置第一个顶点,索引为零。我们忽略传递给 Execute 方法的索引,因为我们将一次性生成整个四边形。我们可以这样做,因为我们禁用了native arrays的安全限制。

var vertex = new Vertex();
			vertex.normal.z = -1f;
			vertex.tangent.xw = float2(1f, -1f);

			streams.SetVertex(0, vertex);

我们不需要 NativeDisableParallelForRestriction 属性来写入任何索引吗?
是的,但是NativeDisableContainerSafetyRestriction属性禁用了所有限制,所以我们不需要同时应用NativeDisableParallelForRestriction。

  通过调整位置和纹理坐标并设置其他三个顶点来完成这个四边形。

streams.SetVertex(0, vertex);
			
			vertex.position = right();
			vertex.texCoord0 = float2(1f, 0f);
			streams.SetVertex(1, vertex);

			vertex.position = up();
			vertex.texCoord0 = float2(0f, 1f);
			streams.SetVertex(2, vertex);

			vertex.position = float3(1f, 1f, 0f);
			vertex.texCoord0 = 1f;
			streams.SetVertex(3, vertex);

  我们还需要两个三角形,因此将索引计数设置为 6。

public int IndexCount => 6;

  然后在Execute的末尾设置两个三角形。

public void Execute (int i, S streams) {
			…

			streams.SetTriangle(0, int3(0, 2, 1));
			streams.SetTriangle(1, int3(1, 2, 3));
		}

  这应该产生一个四边形,但相反,我们甚至在Job被安排之前就得到一个参数异常。这发生在SingleStream.Setup中设置子网格的时候。当我们调用SetSubMesh时,它立即验证了三角形的指数并重新计算了边界。这几乎可以保证会失败,因为这时Job还没有运行,所以索引缓冲区包含了任意的数据。我们必须提供MeshUpdateFlags来表明SetSubMesh不应该对这些数据做任何事情。在之前的教程中我们已经使用了DontRecalculateBounds。这一次我们也必须使用DontValidateIndices。我们用二进制的OR |操作符来合并这两个标志。

meshData.SetSubMesh(
				0, new SubMeshDescriptor(0, indexCount),
				MeshUpdateFlags.DontRecalculateBounds |
				MeshUpdateFlags.DontValidateIndices
			);


unity 2d 网格地图_unity_05

1.7 Bounds

  我们的Mesh唯一缺少的是有效的边界。生成器应该提供这些边界,所以在IMeshGenerator接口上添加一个属性来获取这些边界。

Bounds Bounds { get; }

  然后将实现添加到 SquareGrid。

public Bounds Bounds => new Bounds(new Vector3(0.5f, 0.5f), new Vector3(1f, 1f));

  要设置边界,在MeshJob.ScheduleParallel中添加一个Mesh的参数。我把它作为第一个参数。这样我们就可以在创建Job后立即设置Mesh的边界。

public static JobHandle ScheduleParallel (
			Mesh mesh, Mesh.MeshData meshData, JobHandle dependency
		) {
			var job = new MeshJob<G, S>();
			mesh.bounds = job.generator.Bounds;
			…
		}

在ProceduralMesh.GenerateMesh中传递网格。

MeshJob<SquareGrid, SingleStream>.ScheduleParallel(
			mesh, meshData, default
		).Complete();

  我们还应该设置子网格的边界。为了实现这一点,我们将把边界作为第二个参数添加到IMeshStreams.Setup中。

void Setup(
			Mesh.MeshData meshData, Bounds bounds, int vertexCount, int indexCount
		);

  调整SingleStream.Setup,使其设置子网格的边界和顶点计数。

public void Setup (
			Mesh.MeshData meshData, Bounds bounds, int vertexCount, int indexCount
		) {
			…
			meshData.SetSubMesh(
				0, new SubMeshDescriptor(0, indexCount) {
					bounds = bounds,
					vertexCount = vertexCount
				},
				MeshUpdateFlags.DontRecalculateBounds |
				MeshUpdateFlags.DontValidateIndices
			);

			…
		}

  最后,在MeshJob.ScheduleParallel中设置流的时候,要包括边界。我们可以将边界存储在一个变量中,或者直接使用Mesh边界赋值表达式的结果作为Setup的一个参数。我采用了后者来演示这个用法。

public static JobHandle ScheduleParallel (
			Mesh mesh, Mesh.MeshData meshData, JobHandle dependency
		) {
			var job = new MeshJob<G, S>();
			//mesh.bounds = job.generator.Bounds;
			job.streams.Setup(
				meshData,
				mesh.bounds = job.generator.Bounds,
				job.generator.VertexCount,
				job.generator.IndexCount
			);
			return job.ScheduleParallel(job.generator.JobLength, 1, dependency);
		}
1.8 16 位索引

  在之前的教程中,我们将三角形的索引从32位减少到16位,因为这样可以将索引缓冲区的大小减半。让我们对我们的框架也做同样的事情。一个方便的方法是在ProceduralMeshes.Streams命名空间中定义一个TriangleUInt16类型。它是一个包含三个ushort值的连续结构。给它一个从int3到TriangleUInt16的隐式转换操作符。

using System.Runtime.InteropServices;
using Unity.Mathematics;

namespace ProceduralMeshes.Streams {

	[StructLayout(LayoutKind.Sequential)]
	public struct TriangleUInt16 {
		
		public ushort a, b, c;

		public static implicit operator TriangleUInt16 (int3 t) => new TriangleUInt16 {
			a = (ushort)t.x,
			b = (ushort)t.y,
			c = (ushort)t.z
		};
	}
}

  现在我们可以通过改变三角形索引元素类型和索引缓冲区格式,将SingleStream切换到16位索引。

[NativeDisableContainerSafetyRestriction]
		NativeArray<TriangleUInt16> triangles;
		
		public void Setup (
			Mesh.MeshData meshData, Bounds bounds, int vertexCount, int indexCount
		) {
			…
			
			meshData.SetIndexBufferParams(indexCount, IndexFormat.UInt16);
						
			…
			triangles = meshData.GetIndexData<ushort>().Reinterpret<TriangleUInt16>(2);
		}


unity 2d 网格地图_mesh_06

1.9 多重顶点流(Multiple Vertex Streams)

  作为一个不同的IMeshStreams实现的例子,让我们包括一个多流的方法,如AdvancedMultiStreamProceduralMesh。复制SingleStream并将其重命名为MultiStream。用四个流来代替它的单个顶点属性的单流。

public struct MultiStream : IMeshStreams {

		//[StructLayout(LayoutKind.Sequential)]
		//struct Stream0 {
		//	…
		//}

		//[NativeDisableContainerSafetyRestriction]
		//NativeArray<Stream0> stream0;

		[NativeDisableContainerSafetyRestriction]
		NativeArray<float3> stream0, stream1;

		[NativeDisableContainerSafetyRestriction]
		NativeArray<float4> stream2;

		[NativeDisableContainerSafetyRestriction]
		NativeArray<float2> stream3;

		…

		public void Setup (
			Mesh.MeshData meshData, Bounds bounds, int vertexCount, int indexCount
		) {
			…
			descriptor[1] = new VertexAttributeDescriptor(
				VertexAttribute.Normal, dimension: 3, stream: 1
			);
			descriptor[2] = new VertexAttributeDescriptor(
				VertexAttribute.Tangent, dimension: 4, stream: 2
			);
			descriptor[3] = new VertexAttributeDescriptor(
				VertexAttribute.TexCoord0, dimension: 2, stream: 3
			);
			…

			stream0 = meshData.GetVertexData<float3>();
			stream1 = meshData.GetVertexData<float3>(1);
			stream2 = meshData.GetVertexData<float4>(2);
			stream3 = meshData.GetVertexData<float2>(3);
			triangles = meshData.GetIndexData<ushort>().Reinterpret<TriangleUInt16>(2);
		}
		
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void SetVertex (int index, Vertex vertex) {
			stream0[index] = vertex.position;
			stream1[index] = vertex.normal;
			stream2[index] = vertex.tangent;
			stream3[index] = vertex.texCoord0;
		}

		…
	}

  现在可以通过在ProceduralMesh.GenerateMesh中用MultiStream代替SingleStream来切换到多流方法。

MeshJob<SquareGrid, MultiStream>.ScheduleParallel(
			mesh, meshData, default
		).Complete();

  请注意,生成器代码只知道通用顶点的情况。它完全不知道顶点数据是如何被存储的。甚至有可能只存储部分数据,例如省略法线和切线。Burst会优化掉不需要的代码。

2 四边形网格

  现在我们有了一个功能框架,我们继续生成一个由多个四边形组成的网格,将它们放置在一起,形成一个规则的正方形网格。与单个四边形相比,这样的网格本身并没有任何好处,但它可以作为更复杂的网格的基础,而不是完全的平面。在本教程中,我们将把自己限制在简单的网格上。

2.1 网格分辨率

  我们将调整我们的代码,使其能够产生一个R×R方格的网格,其中R代表网格的分辨率。网格的分辨率是一个一般的概念,为此我们可以给IMeshGenerator添加一个属性。在这种情况下,该属性应该是可设置的,我们通过在其块中加入set;来强制执行。

int Resolution { get; set; }

  我们可以在SquareGrid中实现这个属性,包括同样的一行代码,只是添加了公共访问修改器。这就产生了一个微不足道的自动属性,其中隐含了一个属性所使用的字段。

public int Resolution { get; set; }

  顶点数、索引数和工作长度现在取决于分辨率。四边形的数量等于分辨率的平方,所以必须全部乘以这个。

public int VertexCount => 4 * Resolution * Resolution ;

		public int IndexCount => 6 * Resolution * Resolution;

		public int JobLength => Resolution * Resolution;

  在MeshJob.ScheduleParallel中添加一个分辨率参数,并在创建Job后立即使用它来设置生成器的分辨率。

public static JobHandle ScheduleParallel (
			Mesh mesh, Mesh.MeshData meshData, int resolution, JobHandle dependency
		) {
			var job = new MeshJob<G, S>();
			job.generator.Resolution = resolution;
			…
		}

  然后给ProceduralMesh添加一个分辨率滑块,在生成网格的时候使用。最小应该是1,最大我用10。

[SerializeField, Range(1, 10)]
	int resolution = 1;

	…

	void GenerateMesh () {
		…

		MeshJob<SquareGrid, MultiStream>.ScheduleParallel(
			mesh, meshData, resolution, default
		).Complete();

		…
	}

  为了支持在游戏模式下改变分辨率时重新生成网格,我们必须做一些改变。我们这次的做法是加入一个更新方法,生成网格,然后关闭组件。这样Update就不会在每一帧都被无谓地调用。我们在一个新的OnValidate方法中启用该组件。这意味着我们不再需要在 Awake中生成网格。

void Awake () {
		…
		//GenerateMesh();
		GetComponent<MeshFilter>().mesh = mesh;
	}
	
	void OnValidate () => enabled = true;

	void Update () {
		GenerateMesh();
		enabled = false;
	}


unity 2d 网格地图_数据_07

2.2 生成所有的四边形

  为了确保我们为所有四边形设置数据,我们将在Execute开始时确定正确的顶点和三角形索引。传递给Execute的工作索引代表四边形索引。所以它的第一个顶点索引是四边形的,第一个三角形索引是双倍的。

public void Execute (int i, S streams) {
			int vi = 4 * i, ti = 2 * i;

			…
		}

  我们通过在第一个顶点上增加一个偏移量来找到其他的顶点指数。为了清楚起见,我包括了零偏移,尽管它并不影响代码。

streams.SetVertex(vi + 0, vertex);
			
			vertex.position = right();
			vertex.texCoord0 = float2(1f, 0f);
			streams.SetVertex(vi + 1, vertex);

			vertex.position = up();
			vertex.texCoord0 = float2(0f, 1f);
			streams.SetVertex(vi + 2, vertex);

			vertex.position = float3(1f, 1f, 0f);
			vertex.texCoord0 = 1f;
			streams.SetVertex(vi + 3, vertex);

  三角形的情况也是如此。在这种情况下,我们还必须将第一个顶点索引添加到定义三角形的相对顶点索引中,以保持它们的相对性。

streams.SetTriangle(ti + 0, vi + int3(0, 2, 1));
			streams.SetTriangle(ti + 1, vi + int3(1, 2, 3));

  我们还必须确定四边形的位置偏移,相对于它们的左下角。我们通过用四边形索引的整数除以分辨率来找到Y偏移。然后通过用四边形索引减去Y乘以分辨率来找到X偏移。

int vi = 4 * i, ti = 2 * i;

			int y = i / Resolution;
			int x = i - Resolution * y;

  我们可以在一个float4值中定义四边形所需的所有四个坐标,包含X、X+1、Y和Y+1。但我们最初只加0.9,以便在四边形之间留出可见的间隙。

int y = i / Resolution;
			int x = i - Resolution * y;

			var coordinates = float4(x, x + 0.9f, y, y + 0.9f);

  我们可以通过对坐标进行swizzle操作来正确设置位置,为每个位置选择合适的两个坐标。

vertex.position.xy = coordinates.xz;
			streams.SetVertex(vi + 0, vertex);
			
			vertex.position.xy = coordinates.yz;
			vertex.texCoord0 = float2(1f, 0f);
			streams.SetVertex(vi + 1, vertex);

			vertex.position.xy = coordinates.xw;
			vertex.texCoord0 = float2(0f, 1f);
			streams.SetVertex(vi + 2, vertex);

			vertex.position.xy = coordinates.yw;
			vertex.texCoord0 = 1f;
			streams.SetVertex(vi + 3, vertex);


unity 2d 网格地图_unity_08

2.3 一个面

  网格通常用于平面,所以让我们调整我们的网格,使其位于XZ平面内。首先,将y重命名为z,同时关闭四边形之间的间隙。

int z = i / Resolution;
			int x = i - Resolution * z;
			
			var coordinates = float4(x, x + 1f, z, z + 1f);

  我们通过分配给顶点位置的XZ分量而不是XY分量来改变网格的方向。

vertex.position.xz = coordinates.xz;
			streams.SetVertex(vi + 0, vertex);
			
			vertex.position.xz = coordinates.yz;
			vertex.texCoord0 = float2(1f, 0f);
			streams.SetVertex(vi + 1, vertex);

			vertex.position.xz = coordinates.xw;
			vertex.texCoord0 = float2(0f, 1f);
			streams.SetVertex(vi + 2, vertex);

			vertex.position.xz = coordinates.yw;

  我们还必须改变法线向量,使其指向上方。

vertex.normal.y = 1f;


unity 2d 网格地图_unity_09

  如果平面以原点为中心,并且有一个固定的尺寸,无论其分辨率如何,这也很方便。我们可以通过将所有坐标除以分辨率,然后减去1/2来实现这一点。

var coordinates = float4(x, x + 1f, z, z + 1f) / Resolution - 0.5f;

  调整边界以匹配。

public Bounds Bounds => new Bounds(Vector3.zero, new Vector3(1f, 0f, 1f));
2.4 生成四边形行

  我们的工作目前是孤立地生成网格的每一个象限。创建一个四边形并不费力,但顶点数据不能被矢量化。因此,所有的东西都必须按四边形计算,Unity的Job框架增加了额外的开销。我们可以通过在一次调用Execute的过程中合并生成多个四边形来提高效率。最有意义的是将单行的所有四边形一起生成。这将使Job的长度与分辨率相等,不再是平方。

public int JobLength => Resolution;

  我们将让每次调用Execute来处理沿X轴的一整行四边形。因此,工作索引将代表该行的Z偏移,而不是四边形索引。让我们相应地重命名它。另外,该行的第一个四边形索引因此等于分辨率乘以Z。

public void Execute (int z, S streams) {
			int vi = 4 * Resolution * z, ti = 2 * Resolution * z;

			//int z = i / Resolution;
			…
		}

  现在,我们没有使用固定的X偏移量,而是为整个行引入了一个循环,它包围了填充流的代码。

//int x = i - Resolution * z;
			
			for (int x = 0; x < Resolution; x++) {
				var coordinates = float4(x, x + 1f, z, z + 1f) / Resolution - 0.5f;

				…

				streams.SetTriangle(ti + 0, vi + int3(0, 2, 1));
				streams.SetTriangle(ti + 1, vi + int3(1, 2, 3));
			}

  为了设置正确的四边形,在循环的每一次迭代之后,我们必须将顶点指数增加4,将三角形指数增加2。

for (int x = 0; x < Resolution; x++, vi += 4, ti += 2) { … }

  最后,Burst可以检测到循环内从未改变的代码,并自动将其从循环中拉出。然而,它不会分割向量,所以我们可以通过手动将坐标向量分割成独立的X和Z对来优化一下。Z坐标的计算是恒定的,因此会被拉出循环。

//var coordinates = float4(x, x + 1f, z, z + 1f) / Resolution - 0.5f;
				var xCoordinates = float2(x, x + 1f) / Resolution - 0.5f;
				var zCoordinates = float2(z, z + 1f) / Resolution - 0.5f;
			
				//vertex.position.xz = coordinates.xz;
				vertex.position.x = xCoordinates.x;
				vertex.position.z = zCoordinates.x;
				streams.SetVertex(vi + 0, vertex);

				//vertex.position.xz = coordinates.yz;
				vertex.position.x = xCoordinates.y;
				vertex.texCoord0 = float2(1f, 0f);
				streams.SetVertex(vi + 1, vertex);

				//vertex.position.xz = coordinates.xw;
				vertex.position.x = xCoordinates.x;
				vertex.position.z = zCoordinates.y;
				vertex.texCoord0 = float2(0f, 1f);
				streams.SetVertex(vi + 2, vertex);

				//vertex.position.xz = coordinates.yw;
				vertex.position.x = xCoordinates.y;
				vertex.texCoord0 = 1f;
				streams.SetVertex(vi + 3, vertex);