La conversión de modelos sobre la marcha es una práctica común en la simulación de la física de las deformaciones, así como en juegos con contenido generado y modificable dinámicamente. En tales casos, es conveniente aplicar métodos de edición de procedimientos y creación de geometría. Este último a menudo permite guardar bytes apreciados cuando se transmiten datos descargados de la red. Además, es divertido!
El artículo está dirigido a bombear las habilidades de procesamiento procesal de mallas en Unity. Hablaremos sobre las operaciones de transformación y generación de partes de una malla.

Nuestro kit de caballeros para la edición de procedimientos de modelos 3D incluye tres operaciones básicas: triangulación, movimiento de puntos, extrusión. Hablaremos en detalle sobre los dos últimos. Primero, considere las operaciones de movimiento más simples: vértices en movimiento, rotación y escala de bordes y triángulos. Luego trataremos uno de los métodos para generar nueva geometría: la operación de extrusión.
En una publicación anterior, describimos nuestra estructura para un trabajo conveniente con datos de modelos 3D.Código de estructurapublic 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; } } }
Como puede ver, aquí se usa PLINQ. Esto se debe a que los algoritmos de geometría computacional a menudo se pueden optimizar a través de subprocesos múltiples.
Por supuesto, se crean más construcciones LINQ durante la ejecución de las construcciones LINQ que cuando se ejecuta el código "manual". Sin embargo, este inconveniente se ve compensado en gran medida por la brevedad de dichos diseños, así como por la presencia de herramientas integradas de gestión de recursos en PLINQ. Además, la transición entre implementaciones de subprocesos simples y múltiples se lleva a cabo con un solo comando, lo que facilita enormemente el proceso de depuración.Me giro, me giro, quiero confundir
Procedemos a las operaciones del movimiento. No hay nada complicado en mover vértices. Simplemente no se olvide de los picos coincidentes: si es necesario, su posición también debería cambiar.
El algoritmo se implementa agregando un vector de movimiento a la posición del vértice. El cambio se produce en relación con el origen del modelo (
pivote ). Vale la pena señalar que la posición de los polígonos durante tales transformaciones puede cambiar, pero las normales de sus vértices no. Sin embargo, para simplificar la presentación, no consideraremos este matiz.
Las herramientas CAD tienen una función para recalcular las normales, que generalmente se activa después de aplicar las transformaciones requeridas. Hay diferentes formas de hacer esta asignación. El más común calcula la normal al plano de cada triángulo, y luego asigna una normal a cada vértice como el promedio de la normal a los triángulos a los que pertenece este vértice.En general, no hay una buena razón para complicar el código y aplicar la matriz de transformación. El resultado de agregar un vector de movimiento a la posición del vértice corresponde a una idea intuitiva de su movimiento.

| 
|
Listado de métodos para mover un vértice 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; } }
El movimiento de bordes y triángulos se implementa de la misma manera: agregando un vector de desplazamiento.
Listado de métodos para mover triángulos y bordes 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); } } }
Pero es más conveniente rotar y escalar usando
la matriz de transformación . Lo más probable es que el resultado de realizar estas operaciones en relación con el origen de las coordenadas del modelo no sea lo que esperaba o quería ver. El punto de referencia de rotación y escala generalmente se toma como el centro del objeto, como el más comprensible para los humanos.
Listado de métodos para rotar y escalar triángulos y bordes 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; } } }
Enjambrar un agujero limpio para nosotros
En el modelado 3D, a menudo se usa una operación de extrusión. Para su implementación, deben conocerse el vector de movimiento (desplazamiento) y el conjunto de polígonos. El proceso de extrusión se puede descomponer en dos pasos:
1. El desplazamiento de los polígonos por un vector de movimiento dado (
desplazamiento ). En este caso, es necesario duplicar los vértices separados por polígonos de límite para no perturbar la posición de aquellos elementos que no pertenecen a la parte desplazada. En otras palabras, debe rasgar y mover la pieza seleccionada. Si este paso se completa primero, entonces el modelo probablemente se caerá en pedazos que deberán unirse en el futuro.

2. Agregar nueva geometría entre el límite de la parte desplazada y el límite que se formó durante la extrusión. El espacio entre las partes principales y desplazadas del modelo se llena con polígonos que forman una pared.

En la implementación, es más conveniente construir primero el muro, porque antes del cambio tenemos la posición inicial de los bordes en el borde y podemos usar estos datos de inmediato. De lo contrario, tendría que invertir la dirección del vector de corte o guardar parte de la información sobre el estado inicial de la malla.
El modelo y sus partes con las que trabajamos están formados por conjuntos de polígonos adyacentes por pares (triángulos). Llamamos a cada conjunto de este tipo un
clúster .
Dos grupos dedicados en Blender
Primero, necesitamos obtener todos los bordes de los contornos que unen los grupos seleccionados. Para hacer esto, simplemente agregue los bordes a la lista secuencialmente. Si se encuentra un borde coincidente, debe eliminarse sin agregar el actual. Para el correcto funcionamiento de dicho algoritmo, es necesario introducir una restricción: en el conjunto seleccionado de triángulos no existen más de dos bordes coincidentes. En los casos en que
se usa
Extruir , los modelos a menudo satisfacen esta condición, y un algoritmo más complejo requiere grandes recursos computacionales.
Listado de métodos para obtener aristas que pertenecen a contornos 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; } }
Después de recibir todos los bordes del contorno, debe construir las paredes correspondientes. Hay muchas opciones para la implementación, pero decidimos tomar el camino de menor resistencia: generar paralelogramos en la dirección del vector de movimiento en función de los bordes por separado. Como todos tenemos un desplazamiento, como resultado de esta acción, los paralelogramos formarán una pared sólida y cerrada para cada grupo. Queda por determinar la orientación de los elementos de la pared.
La pared, como toda la malla, consta de triángulos.
Según la convención OpenGL , se representa un triángulo separado en la pantalla si, al proyectar sus puntos en el plano de la pantalla, dar la vuelta en orden corresponde a caminar en sentido horario:

Entonces, un triángulo corresponde a un determinado vector normal que define el lado frontal. Cada triángulo está delimitado por un contorno convexo que consta de tres bordes. Cada arista tiene dos vértices representados en nuestra estructura como
v0 y
v1 . Definimos la dirección del borde para que
v0 sea el comienzo,
v1 sea el final. Ahora, si la dirección de los bordes del triángulo se establece de acuerdo con el desvío de sus vértices, entonces cualquier contorno externo del clúster debe tener un desvío en sentido horario o antihorario, y uno interno, y viceversa. Implementamos constructores
CustomMesh y
Triangle para que el recorrido de los vértices de todos los triángulos corresponda a la dirección de las agujas del reloj.


Teniendo la dirección de eludir el contorno, podemos decir con certeza qué lado de la costilla es la parte interna del contorno y cuál es el externo. Según esta información, elegiremos la orientación del muro. Sea (
v0, v1 ) la arista sobre la base de la cual se debe generar el paralelogramo deseado. Tomamos los dos puntos
v2 y
v3 como las posiciones de desplazamiento
v0 y
v1 . Luego construimos dos triángulos de acuerdo con el siguiente esquema:

Y así, para cada borde del contorno.
Listado de un método para construir muros usando una lista de bordes 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(); } }
Con este enfoque, la parte frontal de las paredes generadas será correcta para los toboganes y los hoyos. Solo hay una limitación importante: el conjunto de triángulos sobre los que se realiza la operación de
extrusión no debe envolverse alrededor del vector de movimiento.
Un subconjunto de polígonos que no es válido con respecto al desplazamiento. Incluso en Blender con una extrusión de este tipo, no será posible evitar una curva de geometría.
Subconjuntos válidos de polígonosLa pared está lista, queda por cambiar los triángulos. Este paso del algoritmo es fácil de entender, aunque la implementación resultó ser engorrosa.
En nuestro caso, debemos asegurarnos de que cada vértice del grupo pertenezca solo a sus triángulos. Si no se cumple la condición, algunos polígonos vecinos pueden alcanzar el clúster. La solución a esta situación es duplicar cada vértice que pertenece tanto al clúster como al resto del modelo. Luego, para todos los polígonos en el grupo, reemplace el índice de este vértice con el índice duplicado. Cuando se cumple la condición, movemos todos los vértices del grupo al vector de movimiento.
Listado de un método para desplazar un grupo de polígonos 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); } } } }
Listo Ahora, sumando los resultados de todos los pasos, obtenemos un hoyo o una colina.
Listado del método final para la operación Extrusión 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); } }
Después de haber jugado con las coordenadas del escaneo de textura y el desplazamiento de los puntos de contorno, puede obtener tal receso:

Y eso no es todo
Además de las operaciones de edición discutidas anteriormente, también utilizamos otros métodos convenientes para trabajar con modelos.
Por ejemplo, también escribimos el método
Combine () para combinar dos
CustomMesh . La diferencia clave entre nuestra implementación y
UnityEngine.Mesh.CombineMeshes () es que si algunos vértices son completamente equivalentes al combinar las mallas, dejamos solo uno de ellos, evitando así geometrías innecesarias.
En el mismo módulo, implementamos
el algoritmo de triangulación del plano de Delaunay . Utilizándolo, puede, por ejemplo, cerrar un gran agujero creado usando
Extrusión con una tapa plana con textura de agua y obtener un lago:

Bueno, lo solucioné! En el próximo artículo, consideraremos las características de importar
.fbx a
Unity y los métodos de validación del modelo en un proyecto.
Para un aperitivo (solo para lulz)