-
-
Save w0wca7a/8b962afea4afb0904c88d48a7df32ea7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Copyright (c) Xenko contributors (https://xenko.com) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) | |
| // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. | |
| using System; | |
| using System.Collections.Generic; | |
| using System.Threading.Tasks; | |
| using Xenko.Core.Mathematics; | |
| using Xenko.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Game; | |
| using Xenko.Assets.Presentation.AssetEditors.GameEditor; | |
| using Xenko.Assets.Presentation.AssetEditors.GameEditor.Game; | |
| using Xenko.Engine; | |
| using Xenko.Engine.Processors; | |
| using Xenko.Graphics; | |
| using Xenko.Input; | |
| using Xenko.Rendering; | |
| namespace Xenko.Assets.Presentation.AssetEditors.Gizmos | |
| { | |
| /// <summary> | |
| /// Base class for all gizmo that applies a transformation on entity | |
| /// </summary> | |
| public abstract class TransformationGizmo : AxialGizmo | |
| { | |
| public const float TransformationStartPixelThreshold = 8; | |
| protected const float MinimumRayAngle = 2.5f * MathUtil.Pi / 180f; | |
| protected struct InitialTransformation | |
| { | |
| public Vector3 Translation; | |
| public Vector3 Scale; | |
| public Quaternion Rotation; | |
| public Matrix InverseParentMatrix; | |
| public bool IsIdentity() | |
| { | |
| return Translation == Vector3.Zero && Rotation == Quaternion.Identity && Scale == Vector3.One; | |
| } | |
| } | |
| public const RenderGroup TransformationGizmoGroup = RenderGroup.Group4; | |
| public const RenderGroupMask TransformationGizmoGroupMask = RenderGroupMask.Group4; | |
| private bool transformationInitialized; | |
| private bool transformationStarted; | |
| private bool duplicationDone; | |
| /// <summary> | |
| /// Event triggered when a gizmo transformation finishes. | |
| /// </summary> | |
| public event EventHandler TransformationEnded; | |
| /// <summary> | |
| /// Gets the gizmo default scale in ratio of screen height ( 1 => full screen vertically ) | |
| /// </summary> | |
| public float DefaultScale => GizmoDefaultSize / GraphicsDevice.Presenter.BackBuffer.Height; | |
| /// <summary> | |
| /// The default material for the origin elements | |
| /// </summary> | |
| protected Material DefaultOriginMaterial; | |
| /// <summary> | |
| /// The default material for a selected element | |
| /// </summary> | |
| protected Material ElementSelectedMaterial; | |
| /// <summary> | |
| /// The default material for a transparent selected element | |
| /// </summary> | |
| protected Material TransparentElementSelectedMaterial; | |
| /// <summary> | |
| /// The transformations of the selected entities at the beginning of the transformations. | |
| /// </summary> | |
| protected Dictionary<Entity, InitialTransformation> InitialTransformations = new Dictionary<Entity, InitialTransformation>(); | |
| /// <summary> | |
| /// The position of the first click of the transformation on the screen. | |
| /// </summary> | |
| protected Vector2 StartMousePosition; | |
| /// <summary> | |
| /// The value of the gizmo world matrix at the beginning of the transformation. | |
| /// </summary> | |
| protected Matrix StartWorldMatrix = Matrix.Identity; | |
| /// <summary> | |
| /// The projection plane of the transformation. | |
| /// </summary> | |
| protected Plane ProjectionPlane; | |
| /// <summary> | |
| /// The position of the first click on the projection plane (world space) | |
| /// </summary> | |
| protected Vector3 StartClickPoint; | |
| /// <summary> | |
| /// The direction of the transformation on the screen | |
| /// </summary> | |
| protected Vector2 TransformationDirection; | |
| /// <summary> | |
| /// The center of the selection. | |
| /// </summary> | |
| protected Vector3 SelectionCenter; | |
| /// <summary> | |
| /// Gets or sets the snap value. | |
| /// </summary> | |
| public float SnapValue { get; set; } | |
| /// <summary> | |
| /// Gets or sets whether to snap entities using the <see cref="SnapValue"/>. | |
| /// </summary> | |
| public bool UseSnap { get; set; } | |
| /// <summary> | |
| /// Gets or sets the working space of the gizmo. | |
| /// </summary> | |
| public TransformationSpace Space { get; set; } | |
| /// <summary> | |
| /// Gets or sets the entity with is modified by the <see cref="TransformationGizmo"/> | |
| /// </summary> | |
| public Entity AnchorEntity { get; set; } | |
| /// <summary> | |
| /// Gets or sets the entity modified by the gizmo. | |
| /// </summary> | |
| public IReadOnlyCollection<Entity> ModifiedEntities { get; set; } | |
| /// <summary> | |
| /// Gets or sets the origin mode. | |
| /// </summary> | |
| public OriginMode OriginMode { get; set; } | |
| /// <summary> | |
| /// Gets or sets the scale origin. If null, defaults to the origin mode. | |
| /// </summary> | |
| public Vector3? ScaleOrigin { get; set; } = null; | |
| protected TransformationGizmo() | |
| { | |
| RenderGroup = TransformationGizmoGroup; | |
| } | |
| protected override Entity Create() | |
| { | |
| base.Create(); | |
| DefaultOriginMaterial = CreateUniformColorMaterial(Color.White); | |
| ElementSelectedMaterial = CreateUniformColorMaterial(Color.Gold); | |
| TransparentElementSelectedMaterial = CreateUniformColorMaterial(Color.Gold.WithAlpha(86)); | |
| return null; | |
| } | |
| /// <summary> | |
| /// Gets the gizmo transformation axes. | |
| /// </summary> | |
| public GizmoTransformationAxes TransformationAxes { get; protected set; } | |
| public override bool IsUnderMouse(int pickedComponentId) | |
| { | |
| return IsUnderMouse(); | |
| } | |
| public bool IsUnderMouse() | |
| { | |
| return TransformationAxes != GizmoTransformationAxes.None; | |
| } | |
| /// <summary> | |
| /// Gets the world matrix of the gizmo | |
| /// </summary> | |
| protected Matrix WorldMatrix => GizmoRootEntity.Transform.WorldMatrix; | |
| protected static void UpdateSelectionOnCloserIntersection(BoundingBox box, Ray clickRay, GizmoTransformationAxes axes, ref float minHitDistance, ref GizmoTransformationAxes newSelection) | |
| { | |
| float hitDistance; | |
| if (box.Intersects(ref clickRay, out hitDistance) && hitDistance < minHitDistance) | |
| { | |
| minHitDistance = hitDistance; | |
| newSelection = axes; | |
| } | |
| } | |
| private Matrix GetWorldMatrix(IEditorGameCameraService cameraService) | |
| { | |
| Matrix worldMatrix = Matrix.Identity; | |
| Vector3 anchorLocation; | |
| if (OriginMode == OriginMode.SelectionCenter) | |
| { | |
| RecalculateCenter(); // maybe only do this when needed? | |
| anchorLocation = SelectionCenter; | |
| } | |
| else | |
| { | |
| anchorLocation = AnchorEntity.Transform.Position; | |
| } | |
| switch (Space) | |
| { | |
| case TransformationSpace.WorldSpace: | |
| worldMatrix.TranslationVector = anchorLocation; | |
| break; | |
| case TransformationSpace.ObjectSpace: | |
| var parentMatrix = Matrix.Identity; | |
| if (AnchorEntity.GetParent() != null) | |
| parentMatrix = AnchorEntity.TransformValue.Parent.WorldMatrix; | |
| // We don't use the entity's "WorldMatrix" because it's scale could be zero, which would break the gizmo. | |
| // Note: Blender uses last selected object's (AnchorEntity) rotation, Unity uses average | |
| // For simplicity we'll just use the last selected object | |
| worldMatrix = Matrix.RotationQuaternion(AnchorEntity.Transform.Rotation) * | |
| Matrix.Translation(anchorLocation) * | |
| parentMatrix; | |
| break; | |
| case TransformationSpace.ViewSpace: | |
| worldMatrix = Matrix.Invert(cameraService.ViewMatrix); | |
| worldMatrix.TranslationVector = anchorLocation; | |
| break; | |
| default: | |
| throw new ArgumentOutOfRangeException(); | |
| } | |
| return worldMatrix; | |
| } | |
| private float GetTargetedScale(IEditorGameCameraService cameraService) | |
| { | |
| if (cameraService.Component.Projection == CameraProjectionMode.Perspective) | |
| { | |
| Vector3 anchorLocation; | |
| if (OriginMode == OriginMode.SelectionCenter) | |
| anchorLocation = SelectionCenter; | |
| else | |
| anchorLocation = AnchorEntity.Transform.Position; | |
| var distanceToSelectedEntity = Math.Abs(Vector3.TransformCoordinate(anchorLocation, cameraService.ViewMatrix).Z); | |
| return SizeFactor * DefaultScale * 2f * (float)Math.Tan(MathUtil.DegreesToRadians(cameraService.VerticalFieldOfView / 2)) * distanceToSelectedEntity; | |
| } | |
| return SizeFactor * DefaultScale * cameraService.Component.OrthographicSize; | |
| } | |
| protected virtual void UpdateTransformation() | |
| { | |
| if (AnchorEntity == null) | |
| return; | |
| // force to recalculate the entity world matrix to avoid the gizmo to have one frame of delay. | |
| AnchorEntity.Transform.UpdateWorldMatrix(); // TODO perform this computation only when necessary? | |
| var cameraService = Game.EditorServices.Get<IEditorGameCameraService>(); | |
| Matrix worldMatrix = GetWorldMatrix(cameraService); | |
| float targetedScale = GetTargetedScale(cameraService); | |
| // Now scale the matrix so the gizmo always has the same on-screen size: | |
| worldMatrix.Row1 *= targetedScale / worldMatrix.Row1.Length(); // Normalize the axes and scale them by "targetedScale". | |
| worldMatrix.Row2 *= targetedScale / worldMatrix.Row2.Length(); | |
| worldMatrix.Row3 *= targetedScale / worldMatrix.Row3.Length(); | |
| GizmoRootEntity.Transform.UseTRS = false; | |
| GizmoRootEntity.Transform.LocalMatrix = worldMatrix; | |
| GizmoRootEntity.Transform.UpdateWorldMatrix(); | |
| } | |
| private void UpdateTransformationAxisBase() | |
| { | |
| if (Game.EditorServices.Get<IEditorGameCameraService>().IsMoving) | |
| return; | |
| if (duplicationDone) | |
| return; | |
| UpdateTransformationAxis(); | |
| } | |
| protected abstract void UpdateTransformationAxis(); | |
| protected abstract InitialTransformation CalculateTransformation(); | |
| protected virtual void OnTransformationFinished() | |
| { | |
| duplicationDone = false; | |
| if (InitialTransformations.Count > 0) | |
| { | |
| InitialTransformations.Clear(); | |
| TransformationEnded?.Invoke(this, EventArgs.Empty); | |
| } | |
| } | |
| protected void RecalculateCenter() | |
| { | |
| var totalPositions = new Vector3(); | |
| foreach (var entity in ModifiedEntities) | |
| { | |
| totalPositions += entity.Transform.Position; | |
| } | |
| SelectionCenter = totalPositions / ModifiedEntities.Count; | |
| } | |
| /// <summary> | |
| /// Initialize a new transformation on the selected entities. | |
| /// </summary> | |
| protected virtual void InitializeTransformation() | |
| { | |
| StartMousePosition = Input.MousePosition; | |
| var cameraService = Game.EditorServices.Get<IEditorGameCameraService>(); | |
| // calculate the un-projection plane for 2D transformations | |
| var planeNormal = Vector3.Zero; | |
| StartWorldMatrix = WorldMatrix; | |
| var gizmoViewInverse = Matrix.Invert(StartWorldMatrix * cameraService.ViewMatrix); | |
| if (EditorGameComponentGizmoService.PlaneToIndex.ContainsKey(TransformationAxes)) | |
| { | |
| planeNormal[EditorGameComponentGizmoService.PlaneToIndex[TransformationAxes]] = 1f; | |
| } | |
| else if (TransformationAxes == GizmoTransformationAxes.XYZ) | |
| { | |
| planeNormal = Vector3.Normalize(Vector3.TransformNormal(Vector3.UnitZ, gizmoViewInverse)); | |
| } | |
| else | |
| { | |
| var axisVector = Vector3.Zero; | |
| for (int i = 0; i < 3; i++) | |
| { | |
| if (((int)TransformationAxes & (1 << i)) != 0) | |
| axisVector[i] = 1; | |
| } | |
| var cameraVector = (Vector3)gizmoViewInverse.Row3; | |
| var planeVector = Vector3.Normalize(Vector3.Cross(axisVector, cameraVector)); | |
| planeNormal = Vector3.Cross(planeVector, axisVector); | |
| //This is a temporary fix for weird rotation behavior, it's not working for translation tho | |
| if (MathUtil.NearEqual(Math.Abs(Vector3.Dot(axisVector, Vector3.Normalize(cameraVector))), 1.0f)) | |
| { | |
| planeNormal = axisVector; | |
| } | |
| } | |
| ProjectionPlane = new Plane(Vector3.Zero, planeNormal); | |
| // determine the position of the start click in the world space | |
| var ray = EditorGameHelper.CalculateRayFromMousePosition(cameraService.Component, StartMousePosition, gizmoViewInverse); | |
| transformationInitialized = ProjectionPlane.Intersects(ref ray, out StartClickPoint); | |
| } | |
| protected virtual void OnTransformationStarted(Vector2 mouseDragPixel) | |
| { | |
| transformationStarted = true; | |
| // keep in memory all initial transformation states | |
| InitialTransformations.Clear(); | |
| foreach (var entity in ModifiedEntities) | |
| { | |
| // Ensure world matrix is computed | |
| entity.Transform.UpdateWorldMatrix(); | |
| InitialTransformations[entity] = new InitialTransformation | |
| { | |
| Scale = entity.Transform.Scale, | |
| Translation = entity.Transform.Position, | |
| Rotation = entity.Transform.Rotation, | |
| InverseParentMatrix = entity.Transform.Parent != null ? Matrix.Invert(entity.Transform.Parent.WorldMatrix) : Matrix.Identity | |
| }; | |
| } | |
| } | |
| /// <summary> | |
| /// Update the transformation of the selected entity. The transformation applied depends on the current TransformationAxes. | |
| /// For all types of transformations, we calculate the change between the start click position and the current mouse position instead of working with delta changes. | |
| /// This ensures that when the user returns to start click position the transformation is as it was at the beginning of the transformation. | |
| /// The transformation direction is either horizontal (left->right) or vertical (bottom->top) and is determined at the beginning of the gesture depending on the user move direction. | |
| /// </summary> | |
| private async Task TransformSceneEntityBase() | |
| { | |
| if (!Input.IsKeyDown(Keys.LeftCtrl) && !Input.IsKeyDown(Keys.RightCtrl)) | |
| duplicationDone = false; | |
| // skip the update if no transformation is currently performed | |
| if (!IsEnabled || AnchorEntity == null || TransformationAxes == GizmoTransformationAxes.None || !Input.IsMouseButtonDown(MouseButton.Left)) | |
| { | |
| if (transformationInitialized) | |
| OnTransformationFinished(); | |
| transformationInitialized = false; | |
| transformationStarted = false; | |
| return; | |
| } | |
| // initialize the start values at the beginning of the transformation | |
| if (!transformationInitialized) | |
| { | |
| InitializeTransformation(); | |
| } | |
| // calculate the current drag translation in the screen normalized space | |
| var mousePosition = Input.MousePosition; | |
| var mouseDrag = mousePosition - StartMousePosition; | |
| // start the transformation only if user has dragged from a given amount of pixel. Determine direction of the transformation. | |
| if (!transformationStarted) | |
| { | |
| // ensure that the mouse cursor has been moved enough | |
| var screenSize = new Vector2(GraphicsDevice.Presenter.BackBuffer.Width, GraphicsDevice.Presenter.BackBuffer.Height); | |
| var mouseDragPixel = mouseDrag * screenSize; | |
| if (mouseDragPixel.Length() < TransformationStartPixelThreshold) | |
| return; | |
| TransformationDirection = Math.Abs(mouseDragPixel.X) > Math.Abs(mouseDragPixel.Y) ? Vector2.UnitX : Vector2.UnitY; | |
| // ensure that the current transformation is not the identity (due to snap it might require more mouse movement to actually start the transformation) | |
| var currentTransformation = CalculateTransformation(); | |
| if (currentTransformation.IsIdentity()) | |
| return; | |
| // check if Ctrl is pressed and initiate a duplication in this case. | |
| if (ModifiedEntities.Count > 0 && !duplicationDone && Input.IsKeyDown(Keys.LeftCtrl) || Input.IsKeyDown(Keys.RightCtrl)) | |
| { | |
| duplicationDone = true; | |
| await Game.EditorServices.Get<IEditorGameEntitySelectionService>().DuplicateSelection(); | |
| } | |
| OnTransformationStarted(mouseDragPixel); | |
| } | |
| // determine the transformation to apply | |
| var transformation = CalculateTransformation(); | |
| // apply the transformations on all selected root entities | |
| foreach (var entity in ModifiedEntities) | |
| { | |
| var initialTransfo = InitialTransformations[entity]; | |
| var entityTransfo = entity.Transform; | |
| if (initialTransfo.InverseParentMatrix == Matrix.Zero) | |
| { | |
| // This usually occurs when at least one axis is scaled to zero (because the matrix inversion | |
| // function returns Matrix.Zero if the determinant is too small). | |
| // TODO: I added this fix because I didn't know how else to make this case work. | |
| // It might break in some cases, which I haven't come across yet. | |
| // But at least it now lets you transform an object that has at least one axis scaled to zero. | |
| // - Mirsad | |
| // Note: -> This does not work in the case one of the parent entity have a rotation. | |
| // To completely fix the problem of 0-scaled objects, gizmo motion projections should be calculated in the object space. | |
| // But this required a deeper modification of the current source code (transformation projection per object, etc.) | |
| // - Pierre | |
| initialTransfo.InverseParentMatrix = Matrix.Identity; | |
| } | |
| // calculate the gizmo to parent space matrix | |
| Matrix gizmoToParentMatrix; | |
| Matrix.Multiply(ref StartWorldMatrix, ref initialTransfo.InverseParentMatrix, out gizmoToParentMatrix); | |
| var initialTranslation = initialTransfo.Translation; | |
| var scaledScale = transformation.Scale * initialTransfo.Scale; | |
| // the scale | |
| if (transformation.Scale != Vector3.One) | |
| { | |
| entityTransfo.Scale = initialTransfo.Scale + scaledScale; | |
| if (UseSnap) | |
| entityTransfo.Scale = MathUtil.Snap(entityTransfo.Scale, SnapValue); | |
| var scaleLocation = ScaleOrigin ?? (OriginMode == OriginMode.LastSelected | |
| ? AnchorEntity.Transform.Position | |
| : SelectionCenter); | |
| var position = Vector3.TransformNormal(initialTranslation, Matrix.Invert(gizmoToParentMatrix)); | |
| var scaleOrigin = Vector3.TransformNormal(scaleLocation, Matrix.Invert(gizmoToParentMatrix)); | |
| var offset = GetScaledLocation(position, scaleOrigin, entityTransfo.Scale / initialTransfo.Scale); | |
| initialTranslation += Vector3.TransformNormal(offset, gizmoToParentMatrix); | |
| } | |
| // translation (transform the translation from gizmo space to the selected root's parent space) | |
| entityTransfo.Position = initialTranslation + Vector3.TransformNormal(transformation.Translation, gizmoToParentMatrix); | |
| // the rotation | |
| if (transformation.Rotation != Quaternion.Identity) | |
| { | |
| var entityToGizmoMatrix = Matrix.Invert(gizmoToParentMatrix * Matrix.Translation(-initialTransfo.Translation)); | |
| var entityToRotatedGizmoMatrix = entityToGizmoMatrix * Matrix.RotationQuaternion(transformation.Rotation); | |
| var elementTranslationGizmo = entityToRotatedGizmoMatrix.TranslationVector - entityToGizmoMatrix.TranslationVector; | |
| entityTransfo.Position = initialTransfo.Translation + Vector3.TransformNormal(elementTranslationGizmo, gizmoToParentMatrix); | |
| var rotationAxisParent = Vector3.Normalize(Vector3.TransformNormal(transformation.Rotation.Axis, gizmoToParentMatrix)); | |
| entityTransfo.Rotation = initialTransfo.Rotation * Quaternion.RotationAxis(rotationAxisParent, transformation.Rotation.Angle); | |
| } | |
| } | |
| } | |
| protected Vector3 GetScaledLocation(Vector3 position, Vector3 scaleOrigin, Vector3 scale) | |
| { | |
| var vec = new Vector3(); | |
| vec.X = GetScaledAxis(position.X, scaleOrigin.X, scale.X); | |
| vec.Y = GetScaledAxis(position.Y, scaleOrigin.Y, scale.Y); | |
| vec.Z = GetScaledAxis(position.Z, scaleOrigin.Z, scale.Z); | |
| return vec; | |
| } | |
| protected float GetScaledAxis(float position, float scaleOrigin, float scale) | |
| { | |
| return (scaleOrigin + (position - scaleOrigin) * scale) - position; | |
| } | |
| public virtual async Task Update() | |
| { | |
| if (!IsEnabled) | |
| return; | |
| UpdateShape(); | |
| UpdateTransformationAxisBase(); | |
| UpdateColors(); | |
| await TransformSceneEntityBase(); | |
| UpdateTransformation(); | |
| } | |
| /// <summary> | |
| /// Update the shape of the gizmo depending on the angle of view. | |
| /// </summary> | |
| protected virtual void UpdateShape() | |
| { | |
| } | |
| public void ClearTransformationAxes() | |
| { | |
| if (!duplicationDone) | |
| TransformationAxes = GizmoTransformationAxes.None; | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment