diff --git a/Source/Editor/Gizmo/IGizmoOwner.cs b/Source/Editor/Gizmo/IGizmoOwner.cs index 3ab0a917b..af3043fd6 100644 --- a/Source/Editor/Gizmo/IGizmoOwner.cs +++ b/Source/Editor/Gizmo/IGizmoOwner.cs @@ -51,6 +51,11 @@ namespace FlaxEditor.Gizmo /// bool SnapToGround { get; } + /// + /// Gets a value indicating whether to use vertex snapping (check if user pressed the given input key to call action). + /// + bool SnapToVertex { get; } + /// /// Gets the view forward direction. /// diff --git a/Source/Editor/Gizmo/TransformGizmo.cs b/Source/Editor/Gizmo/TransformGizmo.cs index 741b89d2d..c000f4a30 100644 --- a/Source/Editor/Gizmo/TransformGizmo.cs +++ b/Source/Editor/Gizmo/TransformGizmo.cs @@ -269,7 +269,13 @@ namespace FlaxEditor.Gizmo protected override int SelectionCount => _selectionParents.Count; /// - protected override Transform GetSelectedObject(int index) + protected override SceneGraphNode GetSelectedObject(int index) + { + return _selectionParents[index]; + } + + /// + protected override Transform GetSelectedTransform(int index) { return _selectionParents[index].Transform; } diff --git a/Source/Editor/Gizmo/TransformGizmoBase.Draw.cs b/Source/Editor/Gizmo/TransformGizmoBase.Draw.cs index 0d421a17e..cb7a7600c 100644 --- a/Source/Editor/Gizmo/TransformGizmoBase.Draw.cs +++ b/Source/Editor/Gizmo/TransformGizmoBase.Draw.cs @@ -72,6 +72,12 @@ namespace FlaxEditor.Gizmo const float gizmoModelsScale2RealGizmoSize = 0.075f; Mesh sphereMesh, cubeMesh; + + Matrix.Scaling(gizmoModelsScale2RealGizmoSize, out m3); + Matrix.Multiply(ref m3, ref world, out m1); + mx1 = m1; + mx1.M41 += 0.05f; + switch (_activeMode) { case Mode.Translate: @@ -81,10 +87,6 @@ namespace FlaxEditor.Gizmo var transAxisMesh = _modelTranslationAxis.LODs[0].Meshes[0]; cubeMesh = _modelCube.LODs[0].Meshes[0]; sphereMesh = _modelSphere.LODs[0].Meshes[0]; - Matrix.Scaling(gizmoModelsScale2RealGizmoSize, out m3); - Matrix.Multiply(ref m3, ref world, out m1); - mx1 = m1; - mx1.M41 += 0.05f; // X axis Matrix.RotationY(-Mathf.PiOverTwo, out m2); @@ -130,10 +132,6 @@ namespace FlaxEditor.Gizmo break; var rotationAxisMesh = _modelRotationAxis.LODs[0].Meshes[0]; sphereMesh = _modelSphere.LODs[0].Meshes[0]; - Matrix.Scaling(gizmoModelsScale2RealGizmoSize, out m3); - Matrix.Multiply(ref m3, ref world, out m1); - mx1 = m1; - mx1.M41 += 0.05f; // X axis Matrix.RotationZ(Mathf.PiOverTwo, out m2); @@ -163,10 +161,6 @@ namespace FlaxEditor.Gizmo var scaleAxisMesh = _modelScaleAxis.LODs[0].Meshes[0]; cubeMesh = _modelCube.LODs[0].Meshes[0]; sphereMesh = _modelSphere.LODs[0].Meshes[0]; - Matrix.Scaling(gizmoModelsScale2RealGizmoSize, out m3); - Matrix.Multiply(ref m3, ref world, out m1); - mx1 = m1; - mx1.M41 -= 0.05f; // X axis Matrix.RotationY(-Mathf.PiOverTwo, out m2); @@ -206,6 +200,26 @@ namespace FlaxEditor.Gizmo break; } } + + // Vertex snapping + if (verts != null && SelectedModel != null && selectedvert != -1) + { + if (!_modelCube || !_modelCube.IsLoaded) + return; + cubeMesh = _modelCube.LODs[0].Meshes[0]; + + Transform t = SelectedModel.Transform; + Vector3 selected = ((verts[selectedvert].Position * t.Orientation) * t.Scale) + t.Translation; + Matrix matrix = new Transform(selected, t.Orientation, new Float3(gizmoModelsScale2RealGizmoSize)).GetWorld(); + cubeMesh.Draw(ref renderContext, _materialSphere, ref matrix); + + if (otherVerts != null && otherSelectedvert != -1) + { + t = otherTransform; + matrix = new Transform(selected, t.Orientation, new Float3(gizmoModelsScale2RealGizmoSize)).GetWorld(); + cubeMesh.Draw(ref renderContext, _materialSphere, ref matrix); + } + } } } } diff --git a/Source/Editor/Gizmo/TransformGizmoBase.Selection.cs b/Source/Editor/Gizmo/TransformGizmoBase.Selection.cs index 86c0a6afe..2b9eae7b9 100644 --- a/Source/Editor/Gizmo/TransformGizmoBase.Selection.cs +++ b/Source/Editor/Gizmo/TransformGizmoBase.Selection.cs @@ -27,7 +27,7 @@ namespace FlaxEditor.Gizmo // Get center point Vector3 center = Vector3.Zero; for (int i = 0; i < count; i++) - center += GetSelectedObject(i).Translation; + center += GetSelectedTransform(i).Translation; // Return arithmetic average or whatever it means return center / count; @@ -36,11 +36,9 @@ namespace FlaxEditor.Gizmo private bool IntersectsRotateCircle(Vector3 normal, ref Ray ray, out Real distance) { var plane = new Plane(Vector3.Zero, normal); - if (!plane.Intersects(ref ray, out distance)) return false; Vector3 hitPoint = ray.Position + ray.Direction * distance; - Real distanceNormalized = hitPoint.Length / RotateRadiusRaw; return Mathf.IsInRange(distanceNormalized, 0.9f, 1.1f); } diff --git a/Source/Editor/Gizmo/TransformGizmoBase.Types.cs b/Source/Editor/Gizmo/TransformGizmoBase.Types.cs index 377bd28a0..66acb4c2b 100644 --- a/Source/Editor/Gizmo/TransformGizmoBase.Types.cs +++ b/Source/Editor/Gizmo/TransformGizmoBase.Types.cs @@ -12,42 +12,42 @@ namespace FlaxEditor.Gizmo /// /// None. /// - None, + None = 0, /// /// The X axis. /// - X, + X = 1, /// /// The Y axis. /// - Y, + Y = 2, /// /// The Z axis. /// - Z, + Z = 4, /// /// The XY plane. /// - XY, + XY = X | Y, /// /// The ZX plane. /// - ZX, + ZX = Z | X, /// /// The YZ plane. /// - YZ, + YZ = Y | Z, /// /// The center point. /// - Center, + Center = 8, }; /// diff --git a/Source/Editor/Gizmo/TransformGizmoBase.cs b/Source/Editor/Gizmo/TransformGizmoBase.cs index 6123d348a..3e788d28f 100644 --- a/Source/Editor/Gizmo/TransformGizmoBase.cs +++ b/Source/Editor/Gizmo/TransformGizmoBase.cs @@ -53,6 +53,15 @@ namespace FlaxEditor.Gizmo private Vector3 _translationDelta; private Vector3 _translationScaleSnapDelta; + //vertex snaping stff + private Mesh.Vertex[] verts; + private Mesh.Vertex[] otherVerts; + private Transform otherTransform; + private StaticModel SelectedModel; + private bool hasSelectedVertex; + private int selectedvert; + private int otherSelectedvert; + /// /// Gets the gizmo position. /// @@ -108,7 +117,7 @@ namespace FlaxEditor.Gizmo _startTransforms.Capacity = Mathf.NextPowerOfTwo(count); for (var i = 0; i < count; i++) { - _startTransforms.Add(GetSelectedObject(i)); + _startTransforms.Add(GetSelectedTransform(i)); } GetSelectedObjectsBounds(out _startBounds, out _navigationDirty); @@ -135,11 +144,12 @@ namespace FlaxEditor.Gizmo private void UpdateGizmoPosition() { + // Get gizmo pivot switch (_activePivotType) { case PivotType.ObjectCenter: if (SelectionCount > 0) - Position = GetSelectedObject(0).Translation; + Position = GetSelectedTransform(0).Translation; break; case PivotType.SelectionCenter: Position = GetSelectionCenter(); @@ -148,6 +158,16 @@ namespace FlaxEditor.Gizmo Position = Vector3.Zero; break; } + + // Apply vertex snapping + if (verts != null && SelectedModel != null) + { + Transform t = SelectedModel.Transform; + Vector3 selected = ((verts[selectedvert].Position * t.Orientation) * t.Scale) + t.Translation; + Position += -(Position - selected); + } + + // Apply current movement Position += _translationDelta; } @@ -179,8 +199,9 @@ namespace FlaxEditor.Gizmo float gizmoSize = Editor.Instance.Options.Options.Visual.GizmoSize; _screenScale = (float)(vLength.Length / GizmoScaleFactor * gizmoSize); } + // Setup world - Quaternion orientation = GetSelectedObject(0).Orientation; + Quaternion orientation = GetSelectedTransform(0).Orientation; _gizmoWorld = new Transform(position, orientation, new Float3(_screenScale)); if (_activeTransformSpace == TransformSpace.World && _activeMode != Mode.Scale) { @@ -421,8 +442,12 @@ namespace FlaxEditor.Gizmo { switch (_activeMode) { - case Mode.Scale: case Mode.Translate: + UpdateTranslateScale(); + if (Owner.SnapToVertex) + UpdateVertexSnapping(); + break; + case Mode.Scale: UpdateTranslateScale(); break; case Mode.Rotate: @@ -434,7 +459,12 @@ namespace FlaxEditor.Gizmo { // If nothing selected, try to select any axis if (!isLeftBtnDown && !Owner.IsRightMouseButtonDown) - SelectAxis(); + { + if (Owner.SnapToVertex) + SelectVertexSnapping(); + else + SelectAxis(); + } } // Set positions of the gizmo @@ -503,6 +533,7 @@ namespace FlaxEditor.Gizmo // Deactivate _isActive = false; _activeAxis = Axis.None; + EndVertexSnapping(); return; } @@ -517,6 +548,137 @@ namespace FlaxEditor.Gizmo UpdateMatrices(); } + private void SelectVertexSnapping() + { + // Find the closest object in selection that is hit by the mouse ray + var ray = new SceneGraphNode.RayCastData + { + Ray = Owner.MouseRay, + }; + var closestDistance = Real.MaxValue; + StaticModel closestModel = null; + for (int i = 0; i < SelectionCount; i++) + { + var obj = GetSelectedObject(i); + if (obj.EditableObject is StaticModel model) + { + if (obj.RayCastSelf(ref ray, out var distance, out var normal) && distance < closestDistance) + { + closestDistance = distance; + closestModel = model; + } + } + } + if (closestModel == null) + { + // Find the closest object in selection (in case ray didn't hit anything) + for (int i = 0; i < SelectionCount; i++) + { + var obj = GetSelectedObject(i); + if (obj.EditableObject is StaticModel model) + { + var bounds = model.Box; + CollisionsHelper.ClosestPointBoxPoint(ref bounds, ref ray.Ray.Position, out var point); + var distance = Vector3.Distance(ref point, ref ray.Ray.Position); + if (distance < closestDistance) + { + closestDistance = distance; + closestModel = model; + } + } + } + } + SelectedModel = closestModel; + if (closestModel == null) + return; + + // Find the closest vertex to bounding box point (collision detection approximation) + // TODO: replace this with collision detection which supports concave shapes (compute shader) + var hitPoint = SelectedModel.Transform.WorldToLocal(ray.Ray.GetPoint(closestDistance)); + // TODO: support multi-mesh models + verts = closestModel.Model.LODs[0].Meshes[0].DownloadVertexBuffer(); + closestDistance = Vector3.Distance(hitPoint, verts[0].Position); + for (int j = 0; j < verts.Length; j++) + { + var distance = Vector3.Distance(hitPoint, verts[j].Position); + if (distance <= closestDistance) + { + closestDistance = distance; + selectedvert = j; + } + } + } + + private void EndVertexSnapping() + { + // Clear current vertex snapping data + SelectedModel = null; + verts = null; + otherVerts = null; + } + + private void UpdateVertexSnapping() + { + if (Owner.SceneGraphRoot == null) + return; + Profiler.BeginEvent("VertexSnap"); + + // Ray cast others + if (verts != null) + { + var ray = Owner.MouseRay; + var rayCast = new SceneGraphNode.RayCastData + { + Ray = ray, + Flags = SceneGraphNode.RayCastData.FlagTypes.SkipColliders | SceneGraphNode.RayCastData.FlagTypes.SkipEditorPrimitives, + ExcludeObjects = new(), + }; + for (int i = 0; i < SelectionCount; i++) + rayCast.ExcludeObjects.Add(GetSelectedObject(i)); + + // Raycast objects + var hit = Owner.SceneGraphRoot.RayCast(ref rayCast, out var distance, out var _); + if (hit != null && hit.EditableObject is StaticModel model) + { + otherTransform = model.Transform; + Vector3 point = rayCast.Ray.Position + (rayCast.Ray.Direction * distance); + + //[To Do] comlite this there is not suport for multy mesh model + otherVerts = model.Model.LODs[0].Meshes[0].DownloadVertexBuffer(); + + //find closest vertex to bounding box point (collision detection approximation) + //[ToDo] replace this with collision detection with is suporting concave shapes (compute shader) + point = hit.Transform.WorldToLocal(point); + var closestDistance = Vector3.Distance(point, otherVerts[0].Position); + for (int i = 0; i < otherVerts.Length; i++) + { + distance = Vector3.Distance(point, otherVerts[i].Position); + if (distance < closestDistance) + { + closestDistance = distance; + otherSelectedvert = i; + } + } + + if (closestDistance > 25) + { + otherSelectedvert = -1; + otherVerts = null; + } + } + } + + if (verts != null && SelectedModel != null && otherVerts != null) + { + // Snap current vertex to the other vertex + Vector3 selected = SelectedModel.Transform.LocalToWorld(verts[selectedvert].Position); + Vector3 other = otherTransform.LocalToWorld(otherVerts[otherSelectedvert].Position); + _translationDelta = other - selected; + } + + Profiler.EndEvent(); + } + /// /// Gets a value indicating whether this tool can transform objects. /// @@ -532,11 +694,19 @@ namespace FlaxEditor.Gizmo /// protected abstract int SelectionCount { get; } + /// + /// Gets the selected object. + /// + /// The selected object index. + /// The selected object (eg. actor node). + protected abstract SceneGraphNode GetSelectedObject(int index); + /// /// Gets the selected object transformation. /// /// The selected object index. - protected abstract Transform GetSelectedObject(int index); + /// The transformation of the selected object. + protected abstract Transform GetSelectedTransform(int index); /// /// Gets the selected objects bounding box (contains the whole selection). diff --git a/Source/Editor/Options/InputOptions.cs b/Source/Editor/Options/InputOptions.cs index e2e7d0e71..ec50d1501 100644 --- a/Source/Editor/Options/InputOptions.cs +++ b/Source/Editor/Options/InputOptions.cs @@ -112,6 +112,10 @@ namespace FlaxEditor.Options [EditorDisplay("Scene", "Snap To Ground"), EditorOrder(500)] public InputBinding SnapToGround = new InputBinding(KeyboardKeys.End); + [DefaultValue(typeof(InputBinding), "End")] + [EditorDisplay("Scene", "Vertex Snapping"), EditorOrder(550)] + public InputBinding SnapToVertex = new InputBinding(KeyboardKeys.V); + [DefaultValue(typeof(InputBinding), "F5")] [EditorDisplay("Scene", "Play/Stop"), EditorOrder(510)] public InputBinding Play = new InputBinding(KeyboardKeys.F5); diff --git a/Source/Editor/SceneGraph/Actors/BoxBrushNode.cs b/Source/Editor/SceneGraph/Actors/BoxBrushNode.cs index da28dad2b..3d5bd9ed2 100644 --- a/Source/Editor/SceneGraph/Actors/BoxBrushNode.cs +++ b/Source/Editor/SceneGraph/Actors/BoxBrushNode.cs @@ -200,12 +200,8 @@ namespace FlaxEditor.SceneGraph.Actors : base(actor) { var id = ID; - var bytes = id.ToByteArray(); for (int i = 0; i < 6; i++) - { - bytes[0] += 1; - AddChildNode(new SideLinkNode(this, new Guid(bytes), i)); - } + AddChildNode(new SideLinkNode(this, GetSubID(id, i), i)); } /// diff --git a/Source/Editor/SceneGraph/Actors/BoxVolumeNode.cs b/Source/Editor/SceneGraph/Actors/BoxVolumeNode.cs index 92850857b..a2265e3bf 100644 --- a/Source/Editor/SceneGraph/Actors/BoxVolumeNode.cs +++ b/Source/Editor/SceneGraph/Actors/BoxVolumeNode.cs @@ -169,12 +169,8 @@ namespace FlaxEditor.SceneGraph.Actors : base(actor) { var id = ID; - var bytes = id.ToByteArray(); for (int i = 0; i < 6; i++) - { - bytes[0] += 1; - AddChildNode(new SideLinkNode(this, new Guid(bytes), i)); - } + AddChildNode(new SideLinkNode(this, GetSubID(id, i), i)); } /// diff --git a/Source/Editor/SceneGraph/Actors/FoliageNode.cs b/Source/Editor/SceneGraph/Actors/FoliageNode.cs index 9cb405092..9e24d5b66 100644 --- a/Source/Editor/SceneGraph/Actors/FoliageNode.cs +++ b/Source/Editor/SceneGraph/Actors/FoliageNode.cs @@ -16,4 +16,62 @@ namespace FlaxEditor.SceneGraph.Actors { } } + + /// + /// Scene tree node for instance of . + /// + [HideInEditor] + public sealed class FoliageInstanceNode : SceneGraphNode + { + /// + /// The foliage actor that owns this instance. + /// + public Foliage Actor; + + /// + /// Index of the foliage instance. + /// + public int Index; + + /// + public FoliageInstanceNode(Foliage actor, int index) + : base(GetSubID(actor.ID, index)) + { + Actor = actor; + Index = index; + } + + /// + public override string Name => "Foliage Instance"; + + /// + public override SceneNode ParentScene + { + get + { + var scene = Actor ? Actor.Scene : null; + return scene != null ? SceneGraphFactory.FindNode(scene.ID) as SceneNode : null; + } + } + + /// + public override Transform Transform + { + get => Actor.GetInstance(Index).Transform; + set => Actor.SetInstanceTransform(Index, ref value); + } + + /// + public override bool IsActive => Actor.IsActive; + + /// + public override bool IsActiveInHierarchy => Actor.IsActiveInHierarchy; + + /// + public override int OrderInParent + { + get => Index; + set { } + } + } } diff --git a/Source/Editor/SceneGraph/Actors/NavLinkNode.cs b/Source/Editor/SceneGraph/Actors/NavLinkNode.cs index a0e90622f..4872c312a 100644 --- a/Source/Editor/SceneGraph/Actors/NavLinkNode.cs +++ b/Source/Editor/SceneGraph/Actors/NavLinkNode.cs @@ -77,11 +77,9 @@ namespace FlaxEditor.SceneGraph.Actors public NavLinkNode(Actor actor) : base(actor) { - var bytes = ID.ToByteArray(); - bytes[0] += 1; - AddChildNode(new LinkNode(this, new Guid(bytes), true)); - bytes[0] += 1; - AddChildNode(new LinkNode(this, new Guid(bytes), false)); + var id = ID; + AddChildNode(new LinkNode(this, GetSubID(id, 0), true)); + AddChildNode(new LinkNode(this, GetSubID(id, 1), false)); } /// diff --git a/Source/Editor/SceneGraph/SceneGraphNode.cs b/Source/Editor/SceneGraph/SceneGraphNode.cs index 672a239dc..a8a26a1b3 100644 --- a/Source/Editor/SceneGraph/SceneGraphNode.cs +++ b/Source/Editor/SceneGraph/SceneGraphNode.cs @@ -146,7 +146,6 @@ namespace FlaxEditor.SceneGraph { if (ChildNodes.Contains(node)) return true; - return ChildNodes.Any(x => x.ContainsInHierarchy(node)); } @@ -216,6 +215,18 @@ namespace FlaxEditor.SceneGraph /// The flags. /// public FlagTypes Flags; + + /// + /// The list of objects to exclude from tracing against. Null if unused. + /// + public List ExcludeObjects; + + /// + /// Initializes a new instance of the struct. + /// + public RayCastData() + { + } } /// @@ -276,7 +287,7 @@ namespace FlaxEditor.SceneGraph SceneGraphNode minTarget = null; Real minDistance = Real.MaxValue; Vector3 minDistanceNormal = Vector3.Up; - if (RayCastSelf(ref ray, out distance, out normal)) + if (RayMask(ref ray) && RayCastSelf(ref ray, out distance, out normal)) { minTarget = this; minDistance = distance; @@ -301,6 +312,25 @@ namespace FlaxEditor.SceneGraph return minTarget; } + private bool RayMask(ref RayCastData ray) + { + if (ray.ExcludeObjects != null) + { + for (int j = 0; j < ray.ExcludeObjects.Count; j++) + { + if (ray.ExcludeObjects[j] == EditableObject) + { + // Remove form exclude because it is passed by ref and function is recursive it will slowly shrink the list until nothing is left as a micro optimization + ray.ExcludeObjects.RemoveAt(j); + if (ray.ExcludeObjects.Count == 0) + ray.ExcludeObjects = null; + return false; + } + } + } + return true; + } + /// /// Checks if given ray intersects with the node. /// @@ -439,5 +469,20 @@ namespace FlaxEditor.SceneGraph protected virtual void OnParentChanged() { } + + /// + /// Randomizes the owner node identifier. + /// + /// The owner node ID. + /// The sub-object index. + /// The sub-object ID. + protected static unsafe Guid GetSubID(Guid ownerId, int index) + { + var id = ownerId; + var idPtr = (FlaxEngine.Json.JsonSerializer.GuidInterop*)&id; + idPtr->B ^= (uint)(index * 387); + idPtr->D += (uint)(index + 1); + return id; + } } } diff --git a/Source/Editor/Tools/Foliage/EditFoliageGizmo.cs b/Source/Editor/Tools/Foliage/EditFoliageGizmo.cs index 0c0172b36..0955213d6 100644 --- a/Source/Editor/Tools/Foliage/EditFoliageGizmo.cs +++ b/Source/Editor/Tools/Foliage/EditFoliageGizmo.cs @@ -3,6 +3,7 @@ using System; using FlaxEditor.Gizmo; using FlaxEditor.SceneGraph; +using FlaxEditor.SceneGraph.Actors; using FlaxEditor.Tools.Foliage.Undo; using FlaxEngine; @@ -69,7 +70,19 @@ namespace FlaxEditor.Tools.Foliage } /// - protected override Transform GetSelectedObject(int index) + protected override SceneGraphNode GetSelectedObject(int index) + { + var foliage = GizmoMode.SelectedFoliage; + if (!foliage) + throw new InvalidOperationException("No foliage selected."); + var instanceIndex = GizmoMode.SelectedInstanceIndex; + if (instanceIndex < 0 || instanceIndex >= foliage.InstancesCount) + throw new InvalidOperationException("No foliage instance selected."); + return new FoliageInstanceNode(foliage, instanceIndex); + } + + /// + protected override Transform GetSelectedTransform(int index) { var foliage = GizmoMode.SelectedFoliage; if (!foliage) diff --git a/Source/Editor/Viewport/EditorGizmoViewport.cs b/Source/Editor/Viewport/EditorGizmoViewport.cs index fe579e7e5..89f8e82ef 100644 --- a/Source/Editor/Viewport/EditorGizmoViewport.cs +++ b/Source/Editor/Viewport/EditorGizmoViewport.cs @@ -68,6 +68,9 @@ namespace FlaxEditor.Viewport /// public bool SnapToGround => Editor.Instance.Options.Options.Input.SnapToGround.Process(Root); + /// + public bool SnapToVertex => Editor.Instance.Options.Options.Input.SnapToVertex.Process(Root); + /// public Float2 MouseDelta => _mouseDelta * 1000; diff --git a/Source/Editor/Viewport/PrefabWindowViewport.cs b/Source/Editor/Viewport/PrefabWindowViewport.cs index 6c1509de8..8fb671017 100644 --- a/Source/Editor/Viewport/PrefabWindowViewport.cs +++ b/Source/Editor/Viewport/PrefabWindowViewport.cs @@ -337,6 +337,9 @@ namespace FlaxEditor.Viewport /// public bool SnapToGround => false; + /// + public bool SnapToVertex => Editor.Instance.Options.Options.Input.SnapToVertex.Process(Root); + /// public Float2 MouseDelta => _mouseDelta * 1000;