using System; using System.Collections.Generic; using System.Linq; namespace UnityEngine.ProBuilder.MeshOperations { /// /// UV actions. /// static partial class UVEditing { /// /// Get a reference to the mesh UV array at index. /// /// /// The zero-indexed UV channel. /// internal static Vector2[] GetUVs(ProBuilderMesh mesh, int channel) { switch (channel) { case 1: { Mesh m = mesh.mesh; if (m == null) return null; return mesh.mesh.uv2; } case 2: case 3: { if (channel == 2 ? mesh.HasArrays(MeshArrays.Texture2) : mesh.HasArrays(MeshArrays.Texture3)) { List uvs = new List(); mesh.GetUVs(channel, uvs); return uvs.Select(x => (Vector2)x).ToArray(); } return null; } default: return mesh.texturesInternal; } } /// /// Sets an array to the appropriate UV channel, but don't refresh the Mesh. /// internal static void ApplyUVs(ProBuilderMesh mesh, Vector2[] uvs, int channel, bool applyToMesh = true) { switch (channel) { case 0: mesh.texturesInternal = uvs; if (applyToMesh && mesh.mesh != null) mesh.mesh.uv = uvs; break; case 1: if (applyToMesh && mesh.mesh != null) mesh.mesh.uv2 = uvs; break; case 2: case 3: int vc = mesh.vertexCount; if (vc != uvs.Length) throw new IndexOutOfRangeException("uvs"); List list = new List(vc); for (int i = 0; i < vc; i++) list.Add(uvs[i]); mesh.SetUVs(channel, list); if (applyToMesh && mesh.mesh != null) mesh.mesh.SetUVs(channel, list); break; } } /// /// Sews (welds) a UV seam using delta to determine which UVs are close enough to be merged. /// /// /// /// /// public static void SewUVs(this ProBuilderMesh mesh, int[] indexes, float delta) { Vector2[] uvs = mesh.texturesInternal; if (uvs == null || uvs.Length != mesh.vertexCount) uvs = new Vector2[mesh.vertexCount]; var lookup = mesh.sharedTextureLookup; for (int i = 0; i < indexes.Length - 1; i++) { for (int n = i + 1; n < indexes.Length; n++) { int a, b; if (!lookup.TryGetValue(indexes[i], out a)) lookup.Add(indexes[i], a = lookup.Count); if (!lookup.TryGetValue(indexes[n], out b)) lookup.Add(indexes[n], b = lookup.Count); if (a == b) continue; if (Vector2.Distance(uvs[indexes[i]], uvs[indexes[n]]) < delta) { Vector3 cen = (uvs[indexes[i]] + uvs[indexes[n]]) / 2f; uvs[indexes[i]] = cen; uvs[indexes[n]] = cen; // ToArray prevents delayed execution of linq actions, which cause trouble when modifying the // dictionary values var merge = lookup.Where(x => x.Value == b).Select(y => y.Key).ToArray(); foreach (var key in merge) lookup[key] = a; } } } mesh.SetSharedTextures(lookup); } /// /// Similar to Sew, except Collapse just flattens all UVs to the center point no matter the distance. /// /// /// public static void CollapseUVs(this ProBuilderMesh mesh, int[] indexes) { Vector2[] uvs = mesh.texturesInternal; // set the shared indexes cache to a unique non-used index Vector2 cen = Math.Average(ArrayUtility.ValuesWithIndexes(uvs, indexes)); foreach (int i in indexes) uvs[i] = cen; mesh.SetTexturesCoincident(indexes); } /// /// Creates separate entries in shared indexes cache for all passed indexes. If indexes are not present in pb_IntArray[], don't do anything with them. /// /// /// public static void SplitUVs(this ProBuilderMesh mesh, IEnumerable indexes) { var lookup = mesh.sharedTextureLookup; var index = lookup.Count; foreach (var vertex in indexes) { int a; if (lookup.TryGetValue(vertex, out a)) lookup[vertex] = index++; } mesh.SetSharedTextures(lookup); } /// /// Creates separate entries in shared indexes cache for all passed indexes. /// internal static void SplitUVs(ProBuilderMesh mesh, IEnumerable faces) { var lookup = mesh.sharedTextureLookup; var index = lookup.Count; foreach(var face in faces) { foreach (var vertex in face.distinctIndexesInternal) { int a; if (lookup.TryGetValue(vertex, out a)) lookup[vertex] = index++; } } mesh.SetSharedTextures(lookup); } /// /// Projects UVs on all passed faces, automatically updating the sharedIndexesUV table as required (only associates /// vertices that share a seam). /// /// /// /// internal static void ProjectFacesAuto(ProBuilderMesh mesh, Face[] faces, int channel) { if (faces.Length < 1) return; int[] ind = faces.SelectMany(x => x.distinctIndexesInternal).ToArray(); // Get a projection direction by averaging the normals of all selected faces var projectionDirection = Vector3.zero; foreach (var face in faces) { var nrm = Math.Normal(mesh, face); projectionDirection += nrm; } projectionDirection /= (float) faces.Length; // project uv coordinates Vector2[] uvs = Projection.PlanarProject(mesh.positionsInternal, ind, projectionDirection); // re-assign new projected coords back into full uv array Vector2[] rebuiltUVs = GetUVs(mesh, channel); for (int i = 0; i < ind.Length; i++) rebuiltUVs[ind[i]] = uvs[i]; // and set the msh uv array using the new coordintaes ApplyUVs(mesh, rebuiltUVs, channel); // now go trhough and set all adjacent face groups to use matching element groups foreach (Face f in faces) { f.elementGroup = -1; SplitUVs(mesh, f.distinctIndexesInternal); } mesh.SewUVs(faces.SelectMany(x => x.distinctIndexesInternal).ToArray(), .001f); } /// /// Projects UVs for each face using the closest normal on a box. /// /// /// /// public static void ProjectFacesBox(ProBuilderMesh mesh, Face[] faces, int channel = 0) { Vector2[] uv = GetUVs(mesh, channel); Dictionary> sorted = new Dictionary>(); for (int i = 0; i < faces.Length; i++) { Vector3 nrm = Math.Normal(mesh, faces[i]); ProjectionAxis axis = Projection.VectorToProjectionAxis(nrm); if (sorted.ContainsKey(axis)) sorted[axis].Add(faces[i]); else sorted.Add(axis, new List() { faces[i] }); // clean up UV stuff - no shared UV indexes and remove element group faces[i].elementGroup = -1; faces[i].manualUV = true; } foreach (KeyValuePair> kvp in sorted) { int[] distinct = kvp.Value.SelectMany(x => x.distinctIndexesInternal).ToArray(); Vector2[] uvs = Projection.PlanarProject(mesh.positionsInternal, distinct, Projection.ProjectionAxisToVector(kvp.Key)); for (int n = 0; n < distinct.Length; n++) uv[distinct[n]] = uvs[n]; SplitUVs(mesh, distinct); } /* and set the msh uv array using the new coordintaes */ ApplyUVs(mesh, uv, channel); } /// /// Finds the minimal U and V coordinate of a set of an array of UVs /// internal static Vector2 FindMinimalUV(Vector2[] uvs, int[] indices = null, float xMin = 0f, float yMin = 0f) { int nbElements = (indices == null ? uvs.Length : indices.Length); bool first = (xMin == 0f && yMin == 0f); for (int i = 0; i < nbElements; ++i) { int currentIndex = (indices == null ? i : indices[i]); if (first) { xMin = uvs[currentIndex].x; yMin = uvs[currentIndex].y; first = false; } else { if (uvs[currentIndex].x < xMin) { xMin = uvs[currentIndex].x; } if (uvs[currentIndex].y < yMin) { yMin = uvs[currentIndex].y; } } } return new Vector2(xMin, yMin); } /// /// Projects UVs for each face using the closest normal on a box and then place the lower left coordinate at the anchor position. /// /// /// /// /// public static void ProjectFacesBox(ProBuilderMesh mesh, Face[] faces, Vector2 lowerLeftAnchor, int channel = 0) { Vector2[] uv = GetUVs(mesh, channel); Dictionary> sorted = new Dictionary>(); for (int i = 0; i < faces.Length; i++) { Vector3 nrm = Math.Normal(mesh, faces[i]); ProjectionAxis axis = Projection.VectorToProjectionAxis(nrm); if (sorted.ContainsKey(axis)) sorted[axis].Add(faces[i]); else sorted.Add(axis, new List() { faces[i] }); // clean up UV stuff - no shared UV indexes and remove element group faces[i].elementGroup = -1; faces[i].manualUV = true; } foreach (KeyValuePair> kvp in sorted) { int[] distinct = kvp.Value.SelectMany(x => x.distinctIndexesInternal).ToArray(); Vector2[] uvs = Projection.PlanarProject(mesh.positionsInternal, distinct, Projection.ProjectionAxisToVector(kvp.Key)); Vector2 minimalUV = FindMinimalUV(uvs); for (int n = 0; n < distinct.Length; n++) uv[distinct[n]] = uvs[n] - minimalUV; SplitUVs(mesh, distinct); } /* and set the msh uv array using the new coordintaes */ ApplyUVs(mesh, uv, channel); } /// /// Projects UVs for each face using the closest normal on a sphere. /// /// /// /// public static void ProjectFacesSphere(ProBuilderMesh pb, int[] indexes, int channel = 0) { foreach (Face f in pb.facesInternal) { if (ArrayUtility.ContainsMatch(f.distinctIndexesInternal, indexes)) { f.elementGroup = -1; f.manualUV = true; } } SplitUVs(pb, indexes); Vector2[] projected = Projection.SphericalProject(pb.positionsInternal, indexes); Vector2[] uv = GetUVs(pb, channel); for (int i = 0; i < indexes.Length; i++) uv[indexes[i]] = projected[i]; /* and set the msh uv array using the new coordintaes */ ApplyUVs(pb, uv, channel); } /* * Returns normalized UV values for a mesh uvs (0,0) - (1,1) */ public static Vector2[] FitUVs(Vector2[] uvs) { // shift UVs to zeroed coordinates Vector2 smallestVector2 = Math.SmallestVector2(uvs); int i; for (i = 0; i < uvs.Length; i++) uvs[i] -= smallestVector2; float scale = Math.MakeNonZero(Math.LargestValue(Math.LargestVector2(uvs))); for (i = 0; i < uvs.Length; i++) uvs[i] /= scale; return uvs; } } }