Unity: Editing Jaring Prosedural

Mengkonversi model dengan cepat adalah praktik umum dalam mensimulasikan fisika deformasi, serta dalam game dengan konten yang dibuat dan dimodifikasi secara dinamis. Dalam kasus seperti itu, akan lebih mudah untuk menerapkan metode pengeditan prosedural dan pembuatan geometri. Yang terakhir sering memungkinkan penghematan byte yang dihargai ketika mengirimkan data yang diunduh dari jaringan. Plus, itu menyenangkan!

Artikel ini bertujuan memompa keterampilan pemrosesan prosedural jerat di Unity. Kami akan berbicara tentang operasi mengubah dan menghasilkan bagian-bagian dari jaring.



Kit sopan kami untuk mengedit prosedural model 3D mencakup tiga operasi dasar: triangulasi, pergerakan titik, ekstrusi. Kami akan berbicara secara rinci tentang dua yang terakhir. Pertama, pertimbangkan operasi gerakan yang paling sederhana - memindahkan simpul, memutar dan menskalakan tepi dan segitiga. Kemudian kita akan berurusan dengan salah satu metode untuk menghasilkan geometri baru - operasi mengusir.

Dalam publikasi sebelumnya, kami menggambarkan struktur kami untuk pekerjaan mudah dengan data dari model 3D.

Kode Struktur
public static class CustomMeshPool { private static List<CustomMesh> Pool; private static int pointer; public static CustomMesh GetMesh(int id) { return Pool[id]; } public static int Push(CustomMesh customMesh) { if (Pool == null) Pool = new List<CustomMesh>(); pointer = GetAvailableIndex(); if (pointer < Pool.Count) Pool[pointer] = customMesh; else Pool.Add(customMesh); return pointer; } public static bool Remove(int index) { if (Pool == null) return false; var b = Pool[index] == null; Pool[index] = null; return b; } public static int GetAvailableIndex() { if (Pool == null) return 0; var availableIndex = Pool.FindIndex(mesh => mesh == null); return availableIndex != -1 ? availableIndex : Pool.Count; } public static void Flush() { if (Pool != null) Pool.Clear(); } } public class CustomMesh { public int Id; public Triangle[] Triangles; public Vector3[] vertices; public Vector3[] normals; public Vector2[] uv0, uv2; public Color[] colors; public CustomMesh(Vector3[] vertices, int[] triangles, Vector3[] normals, Vector2[] uv0, Vector2[] uv2, Color[] colors) { this.vertices = vertices; this.normals = normals; if (normals != null) for (var i = 0; i < this.normals.Length; i++) { this.normals[i] = this.normals[i].normalized; } this.uv0 = uv0; this.uv2 = uv2; this.colors = colors; var ptr = CustomMeshPool.GetAvailableIndex(); CustomMeshPool.Push(this); Id = ptr; Triangles = new Triangle[triangles.Length / 3]; Triangles = Triangles .AsParallel() .Select((t, i) => new Triangle(ptr, i, triangles[i * 3], triangles[i * 3 + 1], triangles[i * 3 + 2])) .ToArray(); } } public struct Triangle { private int _index; public int Index { get { return _index; } set { _index = value; if (_edges != null) { _edges[0].TriangleIndex = value; _edges[1].TriangleIndex = value; _edges[2].TriangleIndex = value; } } } private int _meshId; public int MeshId { get { return _meshId; } internal set { _meshId = value; } } private Edge[] _edges; public Edge[] Edges { get { return _edges; } set { if (value.Length == 3) { _edges = value; for (var i = 0; i < 3; i++) { _edges[i].TriangleIndex = _index; } } else throw new IndexOutOfRangeException(); } } public Vertex V0 { get { return Edges[0].v0; } set { if (value.MeshId != MeshId) throw new Exception("Not the same mesh"); Edges[0].v0 = value; Edges[2].v1 = value; } } public Vertex V1 { get { return Edges[1].v0; } set { if (value.MeshId != MeshId) throw new Exception("Not the same mesh"); Edges[1].v0 = value; Edges[0].v1 = value; } } public Vertex V2 { get { return Edges[2].v0; } set { if (value.MeshId != MeshId) throw new Exception("Not the same mesh"); Edges[2].v0 = value; Edges[1].v1 = value; } } public Triangle(int meshId, int index, int v0, int v1, int v2) { _index = index; _meshId = meshId; var edges = new Edge[3]; edges[0] = new Edge(meshId, index, v0, v1); edges[1] = new Edge(meshId, index, v1, v2); edges[2] = new Edge(meshId, index, v2, v0); _edges = edges; } } public struct Edge { public Vertex v0; public Vertex v1; private int _meshId; public int MeshId { get { return _meshId; } internal set { _meshId = value; } } private int _triangleIndex; public int TriangleIndex { get { return _triangleIndex; } internal set { _triangleIndex = value; } } public Edge(int meshId, int triangleIndex, int v0Index, int v1Index) { _meshId = meshId; _triangleIndex = triangleIndex; v0 = new Vertex() { MeshId = meshId, Index = v0Index }; v1 = new Vertex() { MeshId = meshId, Index = v1Index }; } } public struct Vertex { public int Index; private int _meshId; public int MeshId { get { return _meshId; } internal set { _meshId = value; } } public Vector3 position { get { return CustomMeshPool.GetMesh(_meshId).vertices[Index]; } set { CustomMeshPool.GetMesh(_meshId).vertices[Index] = value; } } public Vector3 normal { get { return CustomMeshPool.GetMesh(_meshId).normals[Index]; } set { CustomMeshPool.GetMesh(_meshId).normals[Index] = value; } } public Vector2 uv0 { get { return CustomMeshPool.GetMesh(_meshId).uv0[Index]; } set { CustomMeshPool.GetMesh(_meshId).uv0[Index] = value; } } public Vector2 uv2 { get { return CustomMeshPool.GetMesh(_meshId).uv2[Index]; } set { CustomMeshPool.GetMesh(_meshId).uv2[Index] = value; } } public Color color { get { return CustomMeshPool.GetMesh(_meshId).colors[Index]; } set { CustomMeshPool.GetMesh(_meshId).colors[Index] = value; } } } 


Seperti yang Anda lihat, PLINQ digunakan di sini. Ini karena algoritma geometri komputasi seringkali dapat dioptimalkan melalui multithreading.
Tentu saja, lebih banyak konstruksi LINQ dibuat selama pelaksanaan konstruksi LINQ daripada ketika mengeksekusi kode "manual". Namun, kelemahan ini sebagian besar diimbangi oleh singkatnya desain semacam itu, serta keberadaan alat manajemen sumber daya bawaan di PLINQ. Selain itu, transisi antara implementasi single-threaded dan multi-threaded dilakukan hanya dengan satu perintah, yang sangat memudahkan proses debugging.

Aku memelintir, memelintir, aku ingin bingung


Kami melanjutkan ke operasi gerakan. Tidak ada yang rumit tentang memindahkan simpul. Hanya saja, jangan lupakan puncak kebetulan: jika diperlukan, posisi mereka juga harus berubah.

Algoritma ini diimplementasikan dengan menambahkan vektor gerak ke posisi titik. Pergeseran terjadi relatif terhadap asal model ( pivot ). Perlu dicatat bahwa posisi poligon selama transformasi tersebut dapat berubah, tetapi normalnya dari simpulnya tidak bisa. Namun, untuk menyederhanakan presentasi, kami tidak akan mempertimbangkan nuansa ini.

Alat CAD memiliki fungsi untuk menghitung ulang normals, yang biasanya dipanggil setelah menerapkan transformasi yang diperlukan. Ada berbagai cara untuk melakukan alokasi ini. Yang paling umum menghitung normal ke bidang setiap segitiga, dan kemudian menetapkan normal untuk setiap simpul sebagai rata-rata dari normals dari segitiga tempat verteks ini berada.

Secara umum, tidak ada alasan yang baik untuk menyulitkan kode dan menerapkan matriks transformasi. Hasil menambahkan vektor gerakan ke posisi puncak sesuai dengan ide intuitif gerakannya.



Metode pencatatan untuk memindahkan satu titik
 public struct Vertex { ... public void Translate(Vector3 movement, bool withCoincident = false) { var newPosition = position + movement; if (withCoincident) { var vertices = CustomMeshPool.GetMesh(_meshId).vertices; var mask = CustomMeshPool.GetMesh(_meshId).GetVerticesInPosition(position); for (int i = 0; i < vertices.Length; i++) if (mask[i]) vertices[i] = newPosition; } else { position = newPosition; } } } public class CustomMesh { … public bool[] GetVerticesInPosition(Vector3 position) { bool[] buffer = new bool[vertices.Length]; for (int i = 0; i < buffer.Length; i++) { buffer[i] = Mathf.Abs(position.x - vertices[i].x) < Mathf.Epsilon && Mathf.Abs(position.y - vertices[i].y) < Mathf.Epsilon && Mathf.Abs(position.z - vertices[i].z) < Mathf.Epsilon; } return buffer; } } 


Pergerakan tepi dan segitiga diimplementasikan dengan cara yang sama - dengan menambahkan vektor perpindahan.

Masih ada gif






Mendaftar metode untuk memindahkan segitiga dan tepi
 public struct Edge { … public void Translate(Vector3 movement, bool withCoincident = false) { if (withCoincident) { var vertices = CustomMeshPool.GetMesh(MeshId).vertices; var newV0Position = v0.position + movement; var newV1Position = v1.position + movement; var maskV0 = CustomMeshPool.GetMesh(MeshId).GetVerticesInPosition(v0.position); var maskV1 = CustomMeshPool.GetMesh(MeshId).GetVerticesInPosition(v1.position); for (int i = 0; i < vertices.Length; i++) { if (maskV0[i]) vertices[i] = newV0Position; else if (maskV1[i]) vertices[i] = newV1Position; } } else { v0.Translate(movement); v1.Translate(movement); } } } public struct Triangle { … public void Translate(Vector3 movement, bool withCoincident = false) { if (withCoincident) { var vertices = CustomMeshPool.GetMesh(_meshId).vertices; var newV0Position = V0.position + movement; var newV1Position = V1.position + movement; var newV2Position = V2.position + movement; var maskV0 = CustomMeshPool.GetMesh(_meshId).GetVerticesInPosition(V0.position); var maskV1 = CustomMeshPool.GetMesh(_meshId).GetVerticesInPosition(V1.position); var maskV2 = CustomMeshPool.GetMesh(_meshId).GetVerticesInPosition(V2.position); for (int i = 0; i < vertices.Length; i++) { if (maskV0[i]) vertices[i] = newV0Position; else if (maskV1[i]) vertices[i] = newV1Position; else if (maskV2[i]) vertices[i] = newV2Position; } } else { V0.Translate(movement); V1.Translate(movement); V2.Translate(movement); } } } 


Tetapi lebih mudah untuk memutar dan skala menggunakan matriks transformasi . Hasil dari melakukan operasi ini relatif terhadap asal koordinat model kemungkinan besar tidak akan seperti yang Anda harapkan atau ingin lihat. Titik referensi rotasi dan penskalaan biasanya diambil sebagai bagian tengah objek - sebagai yang paling dimengerti oleh manusia.

Banyak GIF










Mendaftar metode untuk memutar dan menskalakan segitiga dan tepi
 public struct Edge { … public void Rotate(Quaternion rotation, bool withCoincident = false) { var pivot = (v0.position + v1.position) * 0.5f; var matrix = Matrix4x4.TRS(pivot, rotation, Vector3.one); var newV0Position = matrix.MultiplyPoint(v0.position - pivot); var newV1Position = matrix.MultiplyPoint(v1.position - pivot); if (withCoincident) { var vertices = CustomMeshPool.GetMesh(MeshId).vertices; var maskV0 = CustomMeshPool.GetMesh(MeshId).GetVerticesInPosition(v0.position); var maskV1 = CustomMeshPool.GetMesh(MeshId).GetVerticesInPosition(v1.position); for (int i = 0; i < vertices.Length; i++) { if (maskV0[i]) vertices[i] = newV0Position; else if (maskV1[i]) vertices[i] = newV1Position; } } else { v0.position = newV0Position; v1.position = newV1Position; } } public void Scale(Vector3 scale, bool withCoincident = false) { var pivot = (v0.position + v1.position) * 0.5f; var matrix = Matrix4x4.TRS(pivot, Quaternion.identity, scale); var newV0Position = matrix.MultiplyPoint(v0.position - pivot); var newV1Position = matrix.MultiplyPoint(v1.position - pivot); if (withCoincident) { var vertices = CustomMeshPool.GetMesh(MeshId).vertices; var maskV0 = CustomMeshPool.GetMesh(MeshId).GetVerticesInPosition(v0.position); var maskV1 = CustomMeshPool.GetMesh(MeshId).GetVerticesInPosition(v1.position); for (int i = 0; i < vertices.Length; i++) { if (maskV0[i]) vertices[i] = newV0Position; else if (maskV1[i]) vertices[i] = newV1Position; } } else { v0.position = newV0Position; v1.position = newV1Position; } } } public struct Triangle { … public void Rotate(Quaternion rotation, bool withCoincident = false) { var pivot = (V0.position + V1.position + V2.position) / 3; var matrix = Matrix4x4.TRS(Vector3.zero, rotation, Vector3.one); var newV0Position = matrix.MultiplyPoint(V0.position - pivot) + pivot; var newV1Position = matrix.MultiplyPoint(V1.position - pivot) + pivot; var newV2Position = matrix.MultiplyPoint(V2.position - pivot) + pivot; if (withCoincident) { var vertices = CustomMeshPool.GetMesh(_meshId).vertices; var maskV0 = CustomMeshPool.GetMesh(_meshId).GetVerticesInPosition(V0.position); var maskV1 = CustomMeshPool.GetMesh(_meshId).GetVerticesInPosition(V1.position); var maskV2 = CustomMeshPool.GetMesh(_meshId).GetVerticesInPosition(V2.position); for (int i = 0; i < vertices.Length; i++) { if (maskV0[i]) vertices[i] = newV0Position; else if (maskV1[i]) vertices[i] = newV1Position; else if (maskV2[i]) vertices[i] = newV2Position; } } else { Edges[0].v0.position = newV0Position; Edges[1].v0.position = newV1Position; Edges[2].v0.position = newV2Position; } Edges[0].v0.normal = matrix.MultiplyPoint(V0.normal); Edges[1].v0.normal = matrix.MultiplyPoint(V1.normal); Edges[2].v0.normal = matrix.MultiplyPoint(V2.normal); } public void Scale(Vector3 scale, bool withCoincident = false) { var pivot = (V0.position + V1.position + V2.position) / 3; var matrix = Matrix4x4.TRS(pivot, Quaternion.identity, scale); var newV0Position = matrix.MultiplyPoint(V0.position - pivot); var newV1Position = matrix.MultiplyPoint(V1.position - pivot); var newV2Position = matrix.MultiplyPoint(V2.position - pivot); if (withCoincident) { var vertices = CustomMeshPool.GetMesh(_meshId).vertices; var maskV0 = CustomMeshPool.GetMesh(_meshId).GetVerticesInPosition(V0.position); var maskV1 = CustomMeshPool.GetMesh(_meshId).GetVerticesInPosition(V1.position); var maskV2 = CustomMeshPool.GetMesh(_meshId).GetVerticesInPosition(V2.position); for (int i = 0; i < vertices.Length; i++) { if (maskV0[i]) vertices[i] = newV0Position; else if (maskV1[i]) vertices[i] = newV1Position; else if (maskV2[i]) vertices[i] = newV2Position; } } else { Edges[0].v0.position = newV0Position; Edges[1].v0.position = newV1Position; Edges[2].v0.position = newV2Position; } } } 


Berkerumun lubang yang rapi untuk diri kita sendiri


Dalam pemodelan 3D, operasi ekstrusi sering digunakan. Untuk implementasinya, vektor gerak (perpindahan) dan himpunan poligon harus diketahui. Proses ekstrusi dapat didekomposisi menjadi dua langkah:

1. Offset poligon dengan vektor gerakan yang diberikan ( offset ). Dalam hal ini, perlu untuk menduplikasi simpul yang dipisahkan oleh poligon batas agar tidak mengganggu posisi elemen-elemen yang bukan milik bagian yang dipindahkan. Dengan kata lain, Anda perlu merobek dan memindahkan bagian yang dipilih. Jika langkah ini selesai terlebih dahulu, maka model itu mungkin akan jatuh berkeping-keping yang harus bergabung di masa depan.



2. Menambahkan geometri baru antara batas bagian yang dipindahkan dan batas yang dibentuk selama ekstrusi. Kesenjangan antara bagian-bagian utama dan bergeser dari model diisi dengan poligon yang membentuk dinding.



Dalam implementasinya, lebih mudah untuk terlebih dahulu membangun dinding, karena sebelum shift kita memiliki posisi awal tepi di perbatasan dan kita dapat menggunakan data ini segera. Jika tidak, Anda harus membalik arah vektor geser, atau menyimpan beberapa informasi tentang keadaan awal mesh.

Model dan bagian-bagiannya yang kami kerjakan terdiri dari set poligon yang berdekatan berpasangan (segitiga). Kami menyebut masing-masing set cluster .


Dua kelompok khusus di Blender

Pertama, kita perlu mendapatkan semua tepi kontur yang mengikat kluster yang dipilih. Untuk melakukan ini, cukup tambahkan tepi ke daftar secara berurutan. Jika tepi yang cocok ditemukan, maka itu harus dihapus tanpa menambahkan yang saat ini. Untuk operasi yang benar dari algoritma seperti itu, perlu untuk memperkenalkan batasan: pada set segitiga yang dipilih, tidak ada lebih dari dua tepi yang bertepatan. Dalam kasus di mana Extrude digunakan, model sering memenuhi kondisi ini, dan algoritma yang lebih kompleks membutuhkan sumber daya komputasi yang besar.

Metode daftar untuk mendapatkan tepi milik kontur
 internal static class LinkedListExtension { internal static IEnumerable<LinkedListNode<T>> Nodes<T>(this LinkedList<T> list) { for (var node = list.First; node != null; node = node.Next) { yield return node; } } } public struct Vertex { … public bool IsInPosition(Vector3 other) { return Mathf.Abs(position.x - other.x) < Mathf.Epsilon && Mathf.Abs(position.y - other.y) < Mathf.Epsilon && Mathf.Abs(position.z - other.z) < Mathf.Epsilon; } } public struct Edge { … public bool Coincides(Edge other, bool includeDirection = false) { return v0.IsInPosition(other.v0.position) && v1.IsInPosition(other.v1.position) || !includeDirection && v1.IsInPosition(other.v0.position) && v0.IsInPosition(other.v1.position); } } public class CustomMesh { … private LinkedList<Edge> ObtainHullEdges(int[] triIndices) { var edges = new LinkedList<Edge>(); for (var i = 0; i < triIndices.Length; i++) { var edge = edges.Nodes().FirstOrDefault(e => e.Value.Coincides(Triangles[triIndices[i]].Edges[0])); if (edge != null) edges.Remove(edge); else edges.AddFirst(Triangles[triIndices[i]].Edges[0]); edge = edges.Nodes().FirstOrDefault(e => e.Value.Coincides(Triangles[triIndices[i]].Edges[1])); if (edge != null) edges.Remove(edge); else edges.AddFirst(Triangles[triIndices[i]].Edges[1]); edge = edges.Nodes().FirstOrDefault(e => e.Value.Coincides(Triangles[triIndices[i]].Edges[2])); if (edge != null) edges.Remove(edge); else edges.AddFirst(Triangles[triIndices[i]].Edges[2]); } return edges; } } 


Setelah menerima semua tepi kontur, Anda harus membuat dinding yang sesuai. Ada banyak opsi untuk implementasi, tetapi kami memutuskan untuk mengambil jalan yang paling tidak resistan - hasilkan jajaran genjang ke arah vektor gerakan berdasarkan tepi secara terpisah. Karena kita semua memiliki satu perpindahan, sebagai akibat dari tindakan ini, jajaran genjang akan membentuk dinding yang solid dan tertutup untuk setiap gugus. Tetap menentukan orientasi elemen dinding.

Dinding, seperti seluruh jala, terdiri dari segitiga. Menurut konvensi OpenGL , segitiga terpisah ditampilkan di layar jika, ketika memproyeksikan titik-titiknya ke bidang layar, mengitari mereka sesuai dengan berjalan searah jarum jam:



Jadi, segitiga berhubungan dengan vektor normal tertentu yang mendefinisikan sisi depan. Setiap segitiga dibatasi oleh kontur cembung yang terdiri dari tiga tepi. Setiap tepi memiliki dua simpul yang diwakili dalam struktur kita sebagai v0 dan v1 . Kami mendefinisikan arah tepi sehingga v0 adalah awal, v1 adalah akhirnya. Sekarang, jika arah tepi segitiga diatur sesuai dengan memotong simpulnya, maka setiap kontur eksternal dari cluster harus memiliki memotong baik searah jarum jam atau berlawanan arah jarum jam, dan salah satu internal - sebaliknya. Kami menerapkan konstruktor CustomMesh dan Triangle sehingga lintasan simpul semua segitiga sesuai dengan arah searah jarum jam.





Dengan arah menghindari kontur, kita dapat mengatakan dengan pasti sisi rusuk mana yang merupakan bagian dalam kontur, dan mana yang eksternal. Berdasarkan informasi ini, kami akan memilih orientasi dinding. Biarkan ( v0, v1 ) menjadi tepi atas dasar yang mana jajar genjang yang diinginkan harus dihasilkan. Kami mengambil dua poin v2 dan v3 sebagai posisi offset v0 dan v1 . Kemudian kami membangun dua segitiga sesuai dengan skema berikut:



Dan untuk setiap tepi kontur.

Membuat daftar metode untuk membangun dinding menggunakan daftar tepi
 public class CustomMesh { … private void ExtrudeEdgesSet(Edge[] edges, Vector3 offset) { if (offset == Vector3.zero || edges == null || edges.Length == 0) return; var initVerticesLength = vertices.Length; Array.Resize(ref vertices, initVerticesLength + edges.Length * 4); if (normals != null && normals.Length == initVerticesLength) { Array.Resize(ref normals, vertices.Length); } if (uv0 != null && uv0.Length == initVerticesLength) { Array.Resize(ref uv0, vertices.Length); } if (uv2 != null && uv2.Length == initVerticesLength) { Array.Resize(ref uv2, vertices.Length); } if (colors != null && colors.Length == initVerticesLength) { Array.Resize(ref colors, vertices.Length); } var initTrianglesLength = Triangles.Length; Array.Resize(ref Triangles, initTrianglesLength + edges.Length * 2); edges .AsParallel() .Select((edge, i) => { int j = initVerticesLength + i * 4; vertices[j] = edge.v0.position; vertices[j + 1] = edge.v1.position; vertices[j + 2] = edge.v0.position + offset; vertices[j + 3] = edge.v1.position + offset; if (normals != null && normals.Length == vertices.Length) { var normal = Vector3.Cross(vertices[j + 1] - vertices[j], offset); normals[j] = normals[j + 1] = normals[j + 2] = normals[j + 3] = normal; } if (uv0 != null && uv0.Length == vertices.Length) { uv0[j] = uv0[j + 2] = edge.v0.uv0; uv0[j + 1] = uv0[j + 3] = edge.v1.uv0; } if (uv2 != null && uv2.Length == vertices.Length) { uv2[j] = uv2[j + 2] = edge.v0.uv2; uv2[j + 1] = uv2[j + 3] = edge.v1.uv2; } if (colors != null && colors.Length == vertices.Length) { colors[j] = colors[j + 2] = edge.v0.color; colors[j + 1] = colors[j + 3] = edge.v1.color; } Triangles[initTrianglesLength + i * 2] = new Triangle( initTrianglesLength + i * 2, Id, j, j + 1, j + 2 ); Triangles[initTrianglesLength + i * 2 + 1] = new Triangle( initTrianglesLength + i * 2 + 1, Id, j + 3, j + 2, j + 1 ); return true; }).ToArray(); } } 


Dengan pendekatan ini, sisi depan dinding yang dihasilkan akan tepat untuk slide dan lubang. Hanya ada satu batasan signifikan: himpunan segitiga di mana operasi Extrude dilakukan tidak boleh dililitkan dengan memperhatikan vektor gerak.


Subset poligon yang tidak valid sehubungan dengan offset. Bahkan di Blender dengan Extrude semacam itu, Anda tidak bisa lepas dari kurva geometri.


Subset poligon yang valid

Dinding sudah siap, masih untuk menggeser segitiga. Langkah algoritma ini mudah dimengerti, meskipun implementasinya ternyata tidak praktis.

Dalam kasus kami, kami perlu memastikan bahwa setiap simpul dari cluster hanya milik segitiga. Jika kondisi tidak terpenuhi, maka beberapa poligon tetangga dapat meraih kluster. Solusi untuk situasi ini adalah untuk menduplikasi masing-masing simpul yang menjadi milik baik cluster dan seluruh model. Kemudian, untuk semua poligon di kluster, ganti indeks titik ini dengan indeks duplikat. Ketika kondisi terpenuhi, kami memindahkan semua simpul cluster ke vektor gerak.

Membuat daftar metode untuk memindahkan sekelompok poligon
 public class CustomMesh { … private void TranslateTrianglesHard(int[] triIndices, Vector3 offset, int[] hullVerts) { var newVertexIndices = new Dictionary<int, int>(); var initVerticesCount = vertices.Length; Triangles.Where((t, i) => !triIndices.Contains(i)).Select(t => { if (hullVerts.Contains(t.V0.Index) && !newVertexIndices.ContainsKey(t.V0.Index)) newVertexIndices.Add(t.V0.Index, initVerticesCount + newVertexIndices.Count); if (hullVerts.Contains(t.V1.Index) && !newVertexIndices.ContainsKey(t.V1.Index)) newVertexIndices.Add(t.V1.Index, initVerticesCount + newVertexIndices.Count); if (hullVerts.Contains(t.V2.Index) && !newVertexIndices.ContainsKey(t.V2.Index)) newVertexIndices.Add(t.V2.Index, initVerticesCount + newVertexIndices.Count); return false; }).ToArray(); Array.Resize(ref vertices, initVerticesCount + newVertexIndices.Count); foreach (var pair in newVertexIndices) vertices[pair.Value] = vertices[pair.Key] + offset; if (normals != null && normals.Length == initVerticesCount) { Array.Resize(ref normals, vertices.Length); foreach (var pair in newVertexIndices) normals[pair.Value] = normals[pair.Key]; } if (uv0 != null && uv0.Length == initVerticesCount) { Array.Resize(ref uv0, vertices.Length); foreach (var pair in newVertexIndices) uv0[pair.Value] = uv0[pair.Key]; } if (uv2 != null && uv2.Length == initVerticesCount) { Array.Resize(ref uv2, vertices.Length); foreach (var pair in newVertexIndices) uv2[pair.Value] = uv2[pair.Key]; } if (colors != null && colors.Length == initVerticesCount) { Array.Resize(ref colors, vertices.Length); foreach (var pair in newVertexIndices) colors[pair.Value] = colors[pair.Key]; } var alreadyMoved = new HashSet<int>(); for (var i = 0; i < triIndices.Length; i++) { if (newVertexIndices.ContainsKey(Triangles[triIndices[i]].V0.Index)) { var index = newVertexIndices[Triangles[triIndices[i]].V0.Index]; Triangles[triIndices[i]].Edges[0].v0.Index = index; Triangles[triIndices[i]].Edges[2].v1.Index = index; } else if (!alreadyMoved.Contains(Triangles[triIndices[i]].V0.Index)) { vertices[Triangles[triIndices[i]].V0.Index] += offset; alreadyMoved.Add(Triangles[triIndices[i]].V0.Index); } if (newVertexIndices.ContainsKey(Triangles[triIndices[i]].V1.Index)) { var index = newVertexIndices[Triangles[triIndices[i]].V1.Index]; Triangles[triIndices[i]].Edges[0].v1.Index = index; Triangles[triIndices[i]].Edges[1].v0.Index = index; } else if (!alreadyMoved.Contains(Triangles[triIndices[i]].V1.Index)) { vertices[Triangles[triIndices[i]].V1.Index] += offset; alreadyMoved.Add(Triangles[triIndices[i]].V1.Index); } if (newVertexIndices.ContainsKey(Triangles[triIndices[i]].V2.Index)) { var index = newVertexIndices[Triangles[triIndices[i]].V2.Index]; Triangles[triIndices[i]].Edges[1].v1.Index = index; Triangles[triIndices[i]].Edges[2].v0.Index = index; } else if (!alreadyMoved.Contains(Triangles[triIndices[i]].V2.Index)) { vertices[Triangles[triIndices[i]].V2.Index] += offset; alreadyMoved.Add(Triangles[triIndices[i]].V2.Index); } } } } 


Selesai Sekarang, dengan menambahkan hasil dari semua langkah, kita mendapatkan lubang atau bukit.

Mendaftarkan metode terakhir untuk operasi Extrude
 public class CustomMesh { … public void ExtrudeTriangles(int[] triIndices, Vector3 offset) { var edges = ObtainHullEdges(triIndices); ExtrudeEdgesSet(edges.ToArray(), offset); var hullVertices = edges.Select(edge => edge.v0.Index).ToArray(); TranslateTrianglesHard(triIndices, offset, hullVertices); } } 


Setelah bermain dengan koordinat pemindaian tekstur dan perpindahan titik kontur, Anda bisa mendapatkan ceruk seperti itu:



Dan itu belum semuanya


Selain operasi pengeditan yang dibahas di atas, kami juga menggunakan metode mudah lainnya untuk bekerja dengan model.

Sebagai contoh, kami juga menulis metode Combine () untuk menggabungkan dua CustomMesh . Perbedaan utama antara implementasi kami dan UnityEngine.Mesh.CombineMeshes () adalah bahwa jika beberapa simpul benar-benar setara ketika menggabungkan jerat, kami hanya menyisakan satu dari mereka, sehingga menghindari geometri yang tidak perlu.

Dalam modul yang sama, kami menerapkan algoritma triangulasi pesawat Delaunay . Dengan menggunakannya, Anda dapat, misalnya, menutup lubang besar yang dibuat menggunakan Extrude dengan tutup datar dengan tekstur air dan mendapatkan danau:



Nah, bereskan! Pada artikel selanjutnya, kita akan mempertimbangkan fitur-fitur pengimporan .fbx ke Unity dan metode validasi model dalam sebuah proyek.

Untuk camilan (hanya untuk lulz)




Source: https://habr.com/ru/post/id443870/


All Articles