using System.Collections.Generic; using System.Linq; namespace UnityEngine.ProBuilder { /// /// UV actions. /// static partial class UvUnwrapping { /// /// Sets the passed faces to use Auto or Manual UVs, and (if previously manual) splits any vertex connections. /// internal static void SetAutoUV(ProBuilderMesh mesh, Face[] faces, bool auto) { if (auto) { SetAutoAndAlignUnwrapParamsToUVs(mesh, faces.Where(x => x.manualUV)); } else { foreach (Face f in faces) { f.textureGroup = -1; f.manualUV = true; } } } /// /// Reset the AutoUnwrapParameters of a set of faces to best match their current UV coordinates. /// /// /// Auto UVs do not support distortion, so this conversion process cannot be loss-less. However as long as there /// is minimal skewing the results are usually very close. /// internal static void SetAutoAndAlignUnwrapParamsToUVs(ProBuilderMesh mesh, IEnumerable facesToConvert) { var destinationTextures = mesh.textures.ToArray(); var faces = facesToConvert as Face[] ?? facesToConvert.ToArray(); foreach (var face in faces) { face.uv = AutoUnwrapSettings.defaultAutoUnwrapSettings; face.elementGroup = -1; face.textureGroup = -1; face.manualUV = false; } mesh.RefreshUV(faces); var unmodifiedProjection = mesh.texturesInternal; foreach (var face in faces) { var trs = CalculateDelta(unmodifiedProjection, face.indexesInternal, destinationTextures, face.indexesInternal); var uv = face.uv; // offset is flipped for legacy reasons. people were confused that positive offsets moved the texture // in negative directions in the scene-view, but positive in UV window. if changed we would need to // write some way of upgrading the unwrap settings to account for this. uv.offset = -trs.translation; uv.rotation = trs.rotation; uv.scale = trs.scale; face.uv = uv; } mesh.RefreshUV(faces); } internal struct UVTransform { public Vector2 translation; public float rotation; public Vector2 scale; public override string ToString() { return translation.ToString("F2") + ", " + rotation + ", " + scale.ToString("F2"); } } static List s_UVTransformProjectionBuffer = new List(8); #if UNITY_EDITOR [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] static void ResetStaticsOnLoad_UvAutoManualConversion() { s_UVTransformProjectionBuffer.Clear(); } #endif /// /// Returns the auto unwrap settings for a face. In cases where the face is auto unwrapped (manualUV = false), /// this returns an unmodified copy of the AutoUnwrapSettings. If the face is manually unwrapped, it returns /// the auto unwrap settings computed from GetUVTransform. /// /// internal static AutoUnwrapSettings GetAutoUnwrapSettings(ProBuilderMesh mesh, Face face) { if (!face.manualUV) return new AutoUnwrapSettings(face.uv); var trs = GetUVTransform(mesh, face); var uvSettings = AutoUnwrapSettings.defaultAutoUnwrapSettings; uvSettings.offset = trs.translation; uvSettings.rotation = 360 - trs.rotation; uvSettings.scale = uvSettings.scale / trs.scale; return uvSettings; } /// /// Attempt to calculate the UV transform for a face. In cases where the face is auto unwrapped /// (manualUV = false), this returns the offset, rotation, and scale from . If the face is /// manually unwrapped, a transform will be calculated by trying to match an unmodified planar projection to the /// current UVs. The results /// /// internal static UVTransform GetUVTransform(ProBuilderMesh mesh, Face face) { Projection.PlanarProject(mesh.positionsInternal, face.indexesInternal, Math.Normal(mesh, face), s_UVTransformProjectionBuffer); return CalculateDelta(mesh.texturesInternal, face.indexesInternal, s_UVTransformProjectionBuffer, null); } // messy hack to support cases where you want to iterate a collection of values with an optional collection of // indices. do not make public. static int GetIndex(IList collection, int index) { return collection == null ? index : collection[index]; } // indices arrays are optional - if null is passed the index will be 0, 1, 2... up to values array length. // this is done to avoid allocating a separate array just to pass linear indices internal static UVTransform CalculateDelta(IList src, IList srcIndices, IList dst, IList dstIndices) { // rotate to match target points by comparing the angle between old UV and new auto projection Vector2 srcAngle = src[GetIndex(srcIndices, 1)] - src[GetIndex(srcIndices, 0)]; Vector2 dstAngle = dst[GetIndex(dstIndices, 1)] - dst[GetIndex(dstIndices, 0)]; float rotation = Vector2.Angle(dstAngle, srcAngle); if (Vector2.Dot(Vector2.Perpendicular(dstAngle), srcAngle) < 0) rotation = 360f - rotation; Vector2 dstCenter = dstIndices == null ? Bounds2D.Center(dst) : Bounds2D.Center(dst, dstIndices); // inverse the rotation to get an axis-aligned scale Vector2 dstSize = GetRotatedSize(dst, dstIndices, dstCenter, -rotation); Bounds2D srcBounds = srcIndices == null ? new Bounds2D(src) : new Bounds2D(src, srcIndices); Vector2 scale = dstSize.DivideBy(srcBounds.size); // Calculate new bounds after rotation Vector2 min = new Vector2(float.MaxValue, float.MaxValue); Vector2 max = new Vector2(float.MinValue, float.MinValue); int count = srcIndices?.Count ?? src.Count; for (int i = 0; i < count; i++) { int index = GetIndex(srcIndices, i); Vector2 rotated = Math.RotateAroundPoint(src[index], srcBounds.center, rotation); min.x = Mathf.Min(min.x, rotated.x); min.y = Mathf.Min(min.y, rotated.y); max.x = Mathf.Max(max.x, rotated.x); max.y = Mathf.Max(max.y, rotated.y); } // Calculate center of rotated bounds, and apply the calculated scale afterwards Vector2 rotatedCenter = (min + max) * 0.5f; Vector2 srcTransformedCenter = rotatedCenter * scale; return new UVTransform() { translation = dstCenter - srcTransformedCenter, rotation = rotation, scale = dstSize.DivideBy(srcBounds.size) }; } static Vector2 GetRotatedSize(IList points, IList indices, Vector2 center, float rotation) { int size = indices == null ? points.Count : indices.Count; Vector2 point = points[GetIndex(indices, 0)].RotateAroundPoint(center, rotation); float xMin = point.x; float yMin = point.y; float xMax = xMin; float yMax = yMin; for (int i = 1; i < size; i++) { point = points[GetIndex(indices, i)].RotateAroundPoint(center, rotation); float x = point.x; float y = point.y; if (x < xMin) xMin = x; if (x > xMax) xMax = x; if (y < yMin) yMin = y; if (y > yMax) yMax = y; } return new Vector2(xMax - xMin, yMax - yMin); } } }