///<summary> ///Align a group of bricks on any intersecting geometry with a collider. Provide a fallback plane if nothing is hit ///</summary> ///<param name="sourceBrick">The brick we center this aligning around <paramref name="pickupOffset"/></param> ///<param name="bricks">The list of bricks</param> ///<param name="bounds">The current world space bounds of the bricks</param> ///<param name="pivot">The pivot used for rotating the bricks into an aligned orientation</param> ///<param name="pickupOffset">The offset at which we picked up the source brick</param> ///<param name="ray">The mouse ray we shoot into the scene</param> ///<param name="fallbackWorldPlane">A fallback plane if no geometry is hit</param> ///<param name="offset">Out parameter for the offset needed to place bricks on intersecting geometry</param> ///<param name="alignedOffset">Out parameter for the offset aligned to LU_10</param> ///<param name="rotation">Out parameter for the rotation needed to align</param> ///<param name="hit">Output parameter for hit information</param> public static void AlignBricks(Brick sourceBrick, HashSet <Brick> bricks, Bounds bounds, Vector3 pivot, Vector3 pickupOffset, Ray ray, Plane fallbackWorldPlane, float maxDistance, out Vector3 offset, out Vector3 alignedOffset, out Quaternion rotation, out RaycastHit hit) { // Steps for placing selected bricks on intersecting geometry: // 1. Find a hit point (Raycast) on either geometry or fallback plane // 3. Find the closest axis of the focusbrick to the normal of the plane // 4. Orient all bricks around a pivot so that the found axis of the focusbrick aligns with the plane normal // 5. Compute bounds in the space of the plane (So they are properly aligned) // 6. Find out how much we need to offset to get above the grid/hit plane if (GetGridAlignedPosition(ray, fallbackWorldPlane, sourceBrick.gameObject.scene.GetPhysicsScene(), maxDistance, out hit)) { // Any hit will have a normal (either geometry hit or fallback plane) var normal = hit.normal; // Check all possible world axes of source brick transform and find the one that is // closest (by angle) to the plane normal // 1. Get rotation to align up with normal // 2. Cache rotation and apply aligned rotation to sourceBrick temporarily // 3. Get rotation needed to align with forward/right vectors // 4. Revert sourceBrick to cached rotation // Compute the rotation required to get the closest axis aligned to the plane normal var closestAxis = MathUtils.FindClosestAxis(sourceBrick.transform, normal, out MathUtils.VectorDirection vectorDirection); Quaternion rot = Quaternion.FromToRotation(closestAxis, normal); var cachedSourceRot = sourceBrick.transform.rotation; sourceBrick.transform.rotation = rot * sourceBrick.transform.rotation; var axesToAlign = MathUtils.GetRelatedAxes(sourceBrick.transform, vectorDirection); // Compute the transformation matrix for the plane var origin = Vector3.zero; var up = Vector3.zero; var right = Vector3.zero; var forward = Vector3.zero; if (hit.collider != null) { var hitTransform = hit.collider.transform; // Find the axis closest to the normal on the transform // to make sure we don't choose to align all bricks to the normal again var hitRightAngle = Vector3.Angle(normal, hitTransform.right); var hitUpAngle = Vector3.Angle(normal, hitTransform.up); var hitForwardAngle = Vector3.Angle(normal, hitTransform.forward); var hitLeftAngle = Vector3.Angle(normal, -hitTransform.right); var hitDownAngle = Vector3.Angle(normal, -hitTransform.up); var hitBackAngle = Vector3.Angle(normal, -hitTransform.forward); var transformAxes = new List <Vector3>(); // Align the rotation of the transform to the normal if (hitRightAngle <= hitUpAngle && hitRightAngle <= hitForwardAngle && hitRightAngle <= hitLeftAngle && hitRightAngle <= hitDownAngle && hitRightAngle <= hitBackAngle) { // normal points right var fromTo = Quaternion.FromToRotation(hitTransform.right, normal); transformAxes.Add(fromTo * hitTransform.up); transformAxes.Add(fromTo * hitTransform.forward); } else if (hitUpAngle <= hitRightAngle && hitUpAngle <= hitForwardAngle && hitUpAngle <= hitLeftAngle && hitUpAngle <= hitDownAngle && hitUpAngle <= hitBackAngle) { // normal points up transformAxes.Add(hitTransform.right); transformAxes.Add(hitTransform.forward); } else if (hitForwardAngle <= hitRightAngle && hitForwardAngle <= hitUpAngle && hitForwardAngle <= hitLeftAngle && hitForwardAngle <= hitDownAngle && hitForwardAngle <= hitBackAngle) { // normal points forward var fromTo = Quaternion.FromToRotation(hitTransform.forward, normal); transformAxes.Add(fromTo * hitTransform.up); transformAxes.Add(fromTo * hitTransform.right); } else if (hitLeftAngle <= hitUpAngle && hitLeftAngle <= hitForwardAngle && hitLeftAngle <= hitRightAngle && hitLeftAngle <= hitDownAngle && hitLeftAngle <= hitBackAngle) { // normal points left var fromTo = Quaternion.FromToRotation(-hitTransform.right, normal); transformAxes.Add(fromTo * -hitTransform.up); transformAxes.Add(fromTo * -hitTransform.forward); } else if (hitDownAngle <= hitRightAngle && hitDownAngle <= hitForwardAngle && hitDownAngle <= hitLeftAngle && hitDownAngle <= hitUpAngle && hitDownAngle <= hitBackAngle) { // normal points down var fromTo = Quaternion.FromToRotation(-hitTransform.up, normal); transformAxes.Add(fromTo * -hitTransform.right); transformAxes.Add(fromTo * -hitTransform.forward); } else if (hitBackAngle <= hitRightAngle && hitBackAngle <= hitUpAngle && hitBackAngle <= hitLeftAngle && hitBackAngle <= hitDownAngle && hitBackAngle <= hitForwardAngle) { // normal points back var fromTo = Quaternion.FromToRotation(-hitTransform.forward, normal); transformAxes.Add(fromTo * -hitTransform.up); transformAxes.Add(fromTo * -hitTransform.right); } up = normal * hitTransform.localScale.y; right = transformAxes[0] * hitTransform.localScale.x; forward = transformAxes[1] * hitTransform.localScale.z; var m = Matrix4x4.TRS(hitTransform.position, Quaternion.identity, Vector3.one); m.SetColumn(0, forward); m.SetColumn(1, up); m.SetColumn(2, right); rot = MathUtils.AlignRotation(new Vector3[] { axesToAlign.Item1, axesToAlign.Item2 }, m) * rot; // We want to find a common origin for all bricks hitting this specific transform // The origin needs to align with some common point. It doesn't matter what that is, as long // as it is common for this specific transform. // The alignment should only happen in the XZ plane of the transform, which means alignment // on the local Y-axis is always 0 in plane space. var localOrigin = m.inverse.MultiplyPoint(hitTransform.position); var localHit = m.inverse.MultiplyPoint(hit.point); localOrigin.y = localHit.y; origin = m.MultiplyPoint(localOrigin); } else { rot = MathUtils.AlignRotation(new Vector3[] { axesToAlign.Item1, axesToAlign.Item2 }, Matrix4x4.identity) * rot; forward = Vector3.forward; up = Vector3.up; right = Vector3.right; } sourceBrick.transform.rotation = cachedSourceRot; rotation = rot; rot.ToAngleAxis(out float angle, out Vector3 axis); var oldPositions = new List <Vector3>(); var oldRotations = new List <Quaternion>(); foreach (var brick in bricks) { oldPositions.Add(brick.transform.position); oldRotations.Add(brick.transform.rotation); brick.transform.RotateAround(pivot, axis, angle); } var planeTRS = Matrix4x4.TRS(origin, Quaternion.identity, Vector3.one); planeTRS.SetColumn(0, forward); planeTRS.SetColumn(1, up); planeTRS.SetColumn(2, right); // Now compute how much we need to offset the aligned brick selection to get it above the intersected area offset = GetOffsetToGrid(sourceBrick.transform.position, bricks, ray, pickupOffset, planeTRS.inverse); // Find out how far we are dragging the bricks along the ray to align with the plane. var localBoundsMin = planeTRS.inverse.MultiplyPoint(bounds.min); var boundsPos = localBoundsMin + offset; // If it is too far, drag it along the normal instead. if (Vector3.Distance(localBoundsMin, boundsPos) > 10.0f) { var newRay = new Ray(hit.point + normal * 20.0f, -normal); offset = GetOffsetToGrid(sourceBrick.transform.position, bricks, newRay, pickupOffset, planeTRS.inverse); } // Transform to world space offset = planeTRS.MultiplyVector(offset); var newPos = offset + sourceBrick.transform.position; offset = newPos - sourceBrick.transform.position; var transformation = Matrix4x4.TRS(origin, Quaternion.identity, Vector3.one); transformation.SetColumn(0, forward.normalized); transformation.SetColumn(1, up.normalized); transformation.SetColumn(2, right.normalized); AlignToGrid(ref newPos, transformation, LU_10); alignedOffset = newPos - sourceBrick.transform.position; var j = 0; foreach (var brick in bricks) { brick.transform.position = oldPositions[j]; brick.transform.rotation = oldRotations[j++]; } } else { // In case there was no hit, get the offset required to place the bricks at a fixed distance along the ray var pointOnRay = ray.GetPoint(maxDistance); offset = pointOnRay - sourceBrick.transform.position; AlignToGrid(ref pointOnRay, Matrix4x4.identity, LU_10); alignedOffset = pointOnRay - sourceBrick.transform.position; rotation = Quaternion.identity; } }
/// <summary> /// Recompute the pivot of a transform containing bricks. /// </summary> /// <param name="parent">The transform to recompute the pivot for</param> /// <param name="pivotType">The pivot type</param> /// <param name="alignRotation">Whether we also want to align rotation relative to bricks</param> /// <param name="undoBehavior">Whether or not to register undo for this action</param> public static void RecomputePivot(Transform parent, Model.Pivot pivotType = Model.Pivot.BottomCenter, bool alignRotation = true, UndoBehavior undoBehavior = UndoBehavior.withUndo) { if (pivotType == Model.Pivot.Original) { return; } var bricks = parent.GetComponentsInChildren <Brick>(); Brick referenceBrick = null; var closestAngleToUp = 100000.0f; foreach (var brick in bricks) { var newAngle = Vector3.Angle(brick.transform.up, parent.up); if (newAngle < closestAngleToUp) { closestAngleToUp = newAngle; referenceBrick = brick; } } if (!referenceBrick) { return; } var oldRotations = new List <Quaternion>(); var oldPositions = new List <Vector3>(); Matrix4x4 transformation = referenceBrick.transform.localToWorldMatrix.inverse; var bounds = BrickBuildingUtility.ComputeBounds(bricks, transformation); var pivot = bounds.center; if (pivotType == Model.Pivot.BottomCenter) { pivot += Vector3.down * bounds.extents.y; } pivot = referenceBrick.transform.TransformPoint(pivot); if (Vector3.Distance(parent.position, pivot) < 0.00001f) { return; } if (undoBehavior == UndoBehavior.withUndo) { var collectedTransforms = new List <Transform>(); collectedTransforms.Add(parent); foreach (Transform child in parent) { collectedTransforms.Add(child); } Undo.RegisterCompleteObjectUndo(collectedTransforms.ToArray(), "Recording groups before moving model group"); } var difference = parent.position - pivot; parent.position = pivot; foreach (Transform child in parent) { child.transform.position += difference; if (alignRotation) { oldRotations.Add(child.transform.rotation); oldPositions.Add(child.transform.position); } } if (alignRotation) { var closest = MathUtils.FindClosestAxis(parent, referenceBrick.transform.up, out MathUtils.VectorDirection direction); var rot = Quaternion.FromToRotation(closest, referenceBrick.transform.up); var forward = referenceBrick.transform.forward; var right = referenceBrick.transform.right; var oldRot = parent.rotation; parent.rotation = rot * parent.rotation; var m = Matrix4x4.TRS(parent.position, Quaternion.identity, Vector3.one); m.SetColumn(0, forward); switch (direction) { case MathUtils.VectorDirection.up: m.SetColumn(1, parent.up); break; case MathUtils.VectorDirection.right: m.SetColumn(1, parent.right); break; case MathUtils.VectorDirection.forward: m.SetColumn(1, parent.forward); break; case MathUtils.VectorDirection.down: m.SetColumn(1, -parent.up); break; case MathUtils.VectorDirection.left: m.SetColumn(1, -parent.right); break; case MathUtils.VectorDirection.back: m.SetColumn(1, -parent.forward); break; } m.SetColumn(2, right); var related = MathUtils.GetRelatedAxes(parent, direction); rot = MathUtils.AlignRotation(new Vector3[] { related.Item1, related.Item2 }, m) * rot; rot.ToAngleAxis(out float angle, out Vector3 axis); parent.rotation = oldRot; parent.RotateAround(parent.position, axis, angle); var i = 0; foreach (Transform child in parent) { child.transform.rotation = oldRotations[i]; child.transform.position = oldPositions[i++]; } } }
public static void RecomputePivot(Transform parent, Model.Pivot pivotType = Model.Pivot.BottomCenter, bool alignRotation = true, UndoBehavior undoBehavior = UndoBehavior.withUndo) { if (pivotType == Model.Pivot.Original) { return; } var bricks = parent.GetComponentsInChildren <Brick>(); var referenceBrick = bricks.FirstOrDefault(); if (!referenceBrick) { return; } var oldRotations = new List <Quaternion>(); var oldPositions = new List <Vector3>(); Matrix4x4 transformation = referenceBrick.transform.localToWorldMatrix.inverse; var bounds = BrickBuildingUtility.ComputeBounds(bricks, transformation); var pivot = bounds.center; if (pivotType == Model.Pivot.BottomCenter) { pivot += Vector3.down * bounds.extents.y; } pivot = referenceBrick.transform.TransformPoint(pivot); if (Vector3.Distance(parent.position, pivot) < float.Epsilon) { return; } if (undoBehavior == UndoBehavior.withUndo) { var collectedTransforms = new List <Transform>(); collectedTransforms.Add(parent); for (var i = 0; i < parent.childCount; i++) { var child = parent.GetChild(i); collectedTransforms.Add(child); } Undo.RegisterCompleteObjectUndo(collectedTransforms.ToArray(), "Recording groups before moving model group"); } var difference = parent.position - pivot; parent.position = pivot; for (var i = 0; i < parent.childCount; i++) { var child = parent.GetChild(i); child.transform.position += difference; if (alignRotation) { oldRotations.Add(child.transform.rotation); oldPositions.Add(child.transform.position); } } if (alignRotation) { var rot = Quaternion.FromToRotation(parent.up, referenceBrick.transform.up); var forward = referenceBrick.transform.forward; var right = referenceBrick.transform.right; var oldRot = parent.rotation; parent.rotation = rot * parent.rotation; var m = Matrix4x4.TRS(parent.position, Quaternion.identity, Vector3.one); m.SetColumn(0, forward); m.SetColumn(1, parent.up); m.SetColumn(2, right); rot = MathUtils.AlignRotation(new Vector3[] { parent.right, parent.forward }, m) * rot; rot.ToAngleAxis(out float angle, out Vector3 axis); parent.rotation = oldRot; parent.RotateAround(parent.position, axis, angle); for (var i = 0; i < parent.childCount; i++) { var child = parent.GetChild(i); child.transform.rotation = oldRotations[i]; child.transform.position = oldPositions[i]; } } }