using System; using UnityEngine; using System.Linq; using System.Collections.Generic; namespace UnityEngine.ProBuilder { /// /// Functions for projecting 3d points to 2d space. /// public static class Projection { /// /// Project a collection of 3d positions to a 2d plane. The direction from which the vertices are projected /// is calculated using . /// /// A collection of positions to project based on a direction. /// /// The positions array projected into 2d coordinates. public static Vector2[] PlanarProject(IList positions, IList indexes = null) { return PlanarProject(positions, indexes, FindBestPlane(positions, indexes).normal); } /// /// Project a collection of 3d positions to a 2d plane. /// /// A collection of positions to project based on a direction. /// /// A collection of indices to project. The returned array will match the length of indices. /// /// The direction from which vertex positions are projected into 2d space. /// The positions array projected into 2d coordinates. public static Vector2[] PlanarProject(IList positions, IList indexes, Vector3 direction) { List results = new List(indexes != null ? indexes.Count : positions.Count); PlanarProject(positions, indexes, direction, results); return results.ToArray(); } internal static void PlanarProject(IList positions, IList indexes, Vector3 direction, List results) { if(positions == null) throw new ArgumentNullException("positions"); if(results == null) throw new ArgumentNullException("results"); var nrm = direction; var axis = VectorToProjectionAxis(nrm); var prj = GetTangentToAxis(axis); var len = indexes == null ? positions.Count : indexes.Count; results.Clear(); var u = Vector3.Cross(nrm, prj); var v = Vector3.Cross(u, nrm); u.Normalize(); v.Normalize(); if (indexes != null) { for (int i = 0, ic = len; i < ic; ++i) results.Add(new Vector2(Vector3.Dot(u, positions[indexes[i]]), Vector3.Dot(v, positions[indexes[i]]))); } else { for (int i = 0, ic = len; i < ic; ++i) results.Add(new Vector2(Vector3.Dot(u, positions[i]), Vector3.Dot(v, positions[i]))); } } internal static void PlanarProject(ProBuilderMesh mesh, int textureGroup, AutoUnwrapSettings unwrapSettings) { var worldSpace = unwrapSettings.useWorldSpace; var trs = (Transform)null; var faces = mesh.facesInternal; // Get a projection direction by averaging the normals of all selected faces var projectionDirection = Vector3.zero; for (int f = 0, fc = faces.Length; f < fc; ++f) { if (faces[f].textureGroup != textureGroup) continue; var nrm = Math.Normal(mesh, faces[f]); projectionDirection += nrm; } if (worldSpace) { trs = mesh.transform; projectionDirection = trs.TransformDirection(projectionDirection); } var axis = VectorToProjectionAxis(projectionDirection); var prj = GetTangentToAxis(axis); var u = Vector3.Cross(projectionDirection, prj); var v = Vector3.Cross(u, projectionDirection); u.Normalize(); v.Normalize(); var positions = mesh.positionsInternal; var textures = mesh.texturesInternal; for (int f = 0, fc = faces.Length; f < fc; ++f) { if (faces[f].textureGroup != textureGroup) continue; var indexes = faces[f].distinctIndexesInternal; for (int i = 0, ic = indexes.Length; i < ic; ++i) { var p = worldSpace ? trs.TransformPoint(positions[indexes[i]]) : positions[indexes[i]]; textures[indexes[i]].x = Vector3.Dot(u, p); textures[indexes[i]].y = Vector3.Dot(v, p); } } } internal static void PlanarProject(ProBuilderMesh mesh, Face face) { var nrm = Math.Normal(mesh, face); var trs = (Transform)null; var worldSpace = face.uv.useWorldSpace; if (worldSpace) { trs = mesh.transform; nrm = trs.TransformDirection(nrm); } var axis = VectorToProjectionAxis(nrm); var prj = GetTangentToAxis(axis); var uAxis = Vector3.Cross(nrm, prj); var vAxis = Vector3.Cross(uAxis, nrm); uAxis.Normalize(); vAxis.Normalize(); var positions = mesh.positionsInternal; var textures = mesh.texturesInternal; int[] indexes = face.distinctIndexesInternal; for (int i = 0, ic = indexes.Length; i < ic; ++i) { var p = worldSpace ? trs.TransformPoint(positions[indexes[i]]) : positions[indexes[i]]; textures[indexes[i]].x = Vector3.Dot(uAxis, p); textures[indexes[i]].y = Vector3.Dot(vAxis, p); } } internal static Vector2[] SphericalProject(IList vertices, IList indexes = null) { int len = indexes == null ? vertices.Count : indexes.Count; Vector2[] uv = new Vector2[len]; Vector3 cen = Math.Average(vertices, indexes); for (int i = 0; i < len; i++) { int indx = indexes == null ? i : indexes[i]; Vector3 p = (vertices[indx] - cen); p.Normalize(); uv[i].x = .5f + (Mathf.Atan2(p.z, p.x) / (2f * Mathf.PI)); uv[i].y = .5f - (Mathf.Asin(p.y) / Mathf.PI); } return uv; } /// /// Returns a new set of points wound as a contour counter-clockwise. /// /// /// /// internal static IList Sort(IList verts, SortMethod method = SortMethod.CounterClockwise) { Vector2 cen = Math.Average(verts); Vector2 up = Vector2.up; int count = verts.Count; List> angles = new List>(count); for (int i = 0; i < count; i++) angles.Add(new SimpleTuple(Math.SignedAngle(up, verts[i] - cen), verts[i])); angles.Sort((a, b) => { return a.item1 < b.item1 ? -1 : 1; }); IList values = angles.Select(x => x.item2).ToList(); if (method == SortMethod.Clockwise) values = values.Reverse().ToList(); return values; } internal static Vector3 GetTangentToAxis(ProjectionAxis axis) { // old probuilder didn't respect project axis settings properly, and changing it to the correct version // (ProjectionAxisToVector) would break existing models. switch (axis) { case ProjectionAxis.X: case ProjectionAxis.XNegative: return Vector3.up; case ProjectionAxis.Y: case ProjectionAxis.YNegative: return Vector3.forward; case ProjectionAxis.Z: case ProjectionAxis.ZNegative: return Vector3.up; default: return Vector3.up; } } /// /// Given a ProjectionAxis, return the appropriate Vector3 conversion. /// /// /// internal static Vector3 ProjectionAxisToVector(ProjectionAxis axis) { switch (axis) { case ProjectionAxis.X: return Vector3.right; case ProjectionAxis.Y: return Vector3.up; case ProjectionAxis.Z: return Vector3.forward; case ProjectionAxis.XNegative: return -Vector3.right; case ProjectionAxis.YNegative: return -Vector3.up; case ProjectionAxis.ZNegative: return -Vector3.forward; default: return Vector3.zero; } } /// /// Returns a projection axis based on which axis is the largest /// /// /// internal static ProjectionAxis VectorToProjectionAxis(Vector3 direction) { float x = System.Math.Abs(direction.x); float y = System.Math.Abs(direction.y); float z = System.Math.Abs(direction.z); if (x > y && x > z) return direction.x > 0 ? ProjectionAxis.X : ProjectionAxis.XNegative; if (y > z) return direction.y > 0 ? ProjectionAxis.Y : ProjectionAxis.YNegative; return direction.z > 0 ? ProjectionAxis.Z : ProjectionAxis.ZNegative; } /// /// Find a plane that best fits a set of 3d points. /// /// http://www.ilikebigbits.com/blog/2015/3/2/plane-from-points /// The points to find a plane for. Order does not matter. /// If provided, only the vertices referenced by the indexes array will be considered. /// A plane that best matches the layout of the points array. public static Plane FindBestPlane(IList points, IList indexes = null) { float xx = 0f, xy = 0f, xz = 0f, yy = 0f, yz = 0f, zz = 0f; if (points == null) throw new System.ArgumentNullException("points"); bool ind = indexes != null && indexes.Count > 0; int len = ind ? indexes.Count : points.Count; Vector3 c = Vector3.zero, n = Vector3.zero; for (int i = 0; i < len; i++) { c.x += points[ind ? indexes[i] : i].x; c.y += points[ind ? indexes[i] : i].y; c.z += points[ind ? indexes[i] : i].z; } c.x /= (float)len; c.y /= (float)len; c.z /= (float)len; for (int i = 0; i < len; i++) { Vector3 r = points[ind ? indexes[i] : i] - c; xx += r.x * r.x; xy += r.x * r.y; xz += r.x * r.z; yy += r.y * r.y; yz += r.y * r.z; zz += r.z * r.z; } float det_x = yy * zz - yz * yz; float det_y = xx * zz - xz * xz; float det_z = xx * yy - xy * xy; if (det_x > det_y && det_x > det_z) { n.x = 1f; n.y = (xz * yz - xy * zz) / det_x; n.z = (xy * yz - xz * yy) / det_x; } else if (det_y > det_z) { n.x = (yz * xz - xy * zz) / det_y; n.y = 1f; n.z = (xy * xz - yz * xx) / det_y; } else { n.x = (yz * xy - xz * yy) / det_z; n.y = (xz * xy - yz * xx) / det_z; n.z = 1f; } n.Normalize(); return new Plane(n, c); } /// /// Find a plane that best fits a set of faces within a texture group. /// /// A plane that best matches the layout of the points array. internal static Plane FindBestPlane(ProBuilderMesh mesh, int textureGroup) { float xx = 0f, xy = 0f, xz = 0f, yy = 0f, yz = 0f, zz = 0f; if (mesh == null) throw new System.ArgumentNullException("mesh"); Vector3 c = Vector3.zero; int len = 0; Vector3[] positions = mesh.positionsInternal; int faceCount = mesh.faceCount; Face[] faces = mesh.facesInternal; for (int faceIndex = 0; faceIndex < faceCount; faceIndex++) { if (faces[faceIndex].textureGroup != textureGroup) continue; int[] indexes = faces[faceIndex].indexesInternal; for (int index = 0, indexCount = indexes.Length; index < indexCount; index++) { c.x += positions[indexes[index]].x; c.y += positions[indexes[index]].y; c.z += positions[indexes[index]].z; len++; } } c.x /= len; c.y /= len; c.z /= len; for (int faceIndex = 0; faceIndex < faceCount; faceIndex++) { if (faces[faceIndex].textureGroup != textureGroup) continue; int[] indexes = faces[faceIndex].indexesInternal; for (int index = 0, indexCount = indexes.Length; index < indexCount; index++) { Vector3 r = positions[indexes[index]] - c; xx += r.x * r.x; xy += r.x * r.y; xz += r.x * r.z; yy += r.y * r.y; yz += r.y * r.z; zz += r.z * r.z; } } float det_x = yy * zz - yz * yz; float det_y = xx * zz - xz * xz; float det_z = xx * yy - xy * xy; Vector3 n = Vector3.zero; if (det_x > det_y && det_x > det_z) { n.x = 1f; n.y = (xz * yz - xy * zz) / det_x; n.z = (xy * yz - xz * yy) / det_x; } else if (det_y > det_z) { n.x = (yz * xz - xy * zz) / det_y; n.y = 1f; n.z = (xy * xz - yz * xx) / det_y; } else { n.x = (yz * xy - xz * yy) / det_z; n.y = (xz * xy - yz * xx) / det_z; n.z = 1f; } n.Normalize(); return new Plane(n, c); } } }