// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using FlaxEditor.Content; using FlaxEditor.Gizmo; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.SceneGraph; using FlaxEditor.Scripting; using FlaxEditor.Viewport.Modes; using FlaxEditor.Windows; using FlaxEngine; using FlaxEngine.GUI; using Object = FlaxEngine.Object; namespace FlaxEditor.Viewport { /// /// Main editor gizmo viewport used by the . /// /// public class MainEditorGizmoViewport : EditorGizmoViewport, IEditorPrimitivesOwner { private readonly Editor _editor; private readonly ContextMenuButton _showGridButton; private readonly ContextMenuButton _showNavigationButton; private SelectionOutline _customSelectionOutline; /// /// The editor sprites rendering effect. /// /// [HideInEditor] public class EditorSpritesRenderer : PostProcessEffect { /// /// The rendering task. /// public SceneRenderTask Task; /// public EditorSpritesRenderer() { Order = -10000000; UseSingleTarget = true; } /// public override bool CanRender() { return (Task.View.Flags & ViewFlags.EditorSprites) == ViewFlags.EditorSprites && Level.ScenesCount != 0 && base.CanRender(); } /// public override void Render(GPUContext context, ref RenderContext renderContext, GPUTexture input, GPUTexture output) { Profiler.BeginEventGPU("Editor Primitives"); // Prepare var renderList = RenderList.GetFromPool(); var prevList = renderContext.List; renderContext.List = renderList; renderContext.View.Pass = DrawPass.Forward; // Bind output float width = input.Width; float height = input.Height; context.SetViewport(width, height); var depthBuffer = renderContext.Buffers.DepthBuffer; var depthBufferHandle = depthBuffer.View(); if ((depthBuffer.Flags & GPUTextureFlags.ReadOnlyDepthView) == GPUTextureFlags.ReadOnlyDepthView) depthBufferHandle = depthBuffer.ViewReadOnlyDepth(); context.SetRenderTarget(depthBufferHandle, input.View()); // Collect draw calls Draw(ref renderContext); // Sort draw calls renderList.SortDrawCalls(ref renderContext, true, DrawCallsListType.Forward); // Perform the rendering renderList.ExecuteDrawCalls(ref renderContext, DrawCallsListType.Forward); // Cleanup RenderList.ReturnToPool(renderList); renderContext.List = prevList; Profiler.EndEventGPU(); } /// /// Draws the icons. /// protected virtual void Draw(ref RenderContext renderContext) { for (int i = 0; i < Level.ScenesCount; i++) { var scene = Level.GetScene(i); ViewportIconsRenderer.DrawIcons(ref renderContext, scene); } } } private bool _lockedFocus; private double _lockedFocusOffset; private readonly ViewportDebugDrawData _debugDrawData = new ViewportDebugDrawData(32); private EditorSpritesRenderer _editorSpritesRenderer; /// /// Drag and drop handlers /// public readonly ViewportDragHandlers DragHandlers; /// /// The transform gizmo. /// public readonly TransformGizmo TransformGizmo; /// /// The grid gizmo. /// public readonly GridGizmo Grid; /// /// The selection outline postFx. /// public SelectionOutline SelectionOutline; /// /// The editor primitives postFx. /// public EditorPrimitives EditorPrimitives; /// /// Gets or sets a value indicating whether draw shapes. /// public bool DrawDebugDraw = true; /// /// Gets the debug draw data for the viewport. /// public ViewportDebugDrawData DebugDrawData => _debugDrawData; /// /// Gets or sets a value indicating whether show navigation mesh. /// public bool ShowNavigation { get => _showNavigationButton.Checked; set => _showNavigationButton.Checked = value; } /// /// The sculpt terrain gizmo. /// public Tools.Terrain.SculptTerrainGizmoMode SculptTerrainGizmo; /// /// The paint terrain gizmo. /// public Tools.Terrain.PaintTerrainGizmoMode PaintTerrainGizmo; /// /// The edit terrain gizmo. /// public Tools.Terrain.EditTerrainGizmoMode EditTerrainGizmo; /// /// The paint foliage gizmo. /// public Tools.Foliage.PaintFoliageGizmoMode PaintFoliageGizmo; /// /// The edit foliage gizmo. /// public Tools.Foliage.EditFoliageGizmoMode EditFoliageGizmo; /// /// Initializes a new instance of the class. /// /// Editor instance. public MainEditorGizmoViewport(Editor editor) : base(Object.New(), editor.Undo, editor.Scene.Root) { _editor = editor; DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType, ValidateDragScriptItem); // Prepare rendering task Task.ActorsSource = ActorsSources.Scenes; Task.ViewFlags = ViewFlags.DefaultEditor; Task.Begin += OnBegin; Task.CollectDrawCalls += OnCollectDrawCalls; Task.PostRender += OnPostRender; // Render task after the main game task so streaming and render state data will use main game task instead of editor preview Task.Order = 1; // Create post effects SelectionOutline = Object.New(); SelectionOutline.SelectionGetter = () => TransformGizmo.SelectedParents; Task.AddCustomPostFx(SelectionOutline); EditorPrimitives = Object.New(); EditorPrimitives.Viewport = this; Task.AddCustomPostFx(EditorPrimitives); _editorSpritesRenderer = Object.New(); _editorSpritesRenderer.Task = Task; Task.AddCustomPostFx(_editorSpritesRenderer); // Add transformation gizmo TransformGizmo = new TransformGizmo(this); TransformGizmo.ApplyTransformation += ApplyTransform; TransformGizmo.Duplicate += _editor.SceneEditing.Duplicate; Gizmos.Active = TransformGizmo; // Add grid Grid = new GridGizmo(this); Grid.EnabledChanged += gizmo => _showGridButton.Icon = gizmo.Enabled ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; editor.SceneEditing.SelectionChanged += OnSelectionChanged; // Gizmo widgets AddGizmoViewportWidgets(this, TransformGizmo); // Show grid widget _showGridButton = ViewWidgetShowMenu.AddButton("Grid", () => Grid.Enabled = !Grid.Enabled); _showGridButton.Icon = Style.Current.CheckBoxTick; _showGridButton.CloseMenuOnClick = false; // Show navigation widget _showNavigationButton = ViewWidgetShowMenu.AddButton("Navigation", () => ShowNavigation = !ShowNavigation); _showNavigationButton.CloseMenuOnClick = false; // Create camera widget ViewWidgetButtonMenu.AddSeparator(); ViewWidgetButtonMenu.AddButton("Create camera here", CreateCameraAtView); // Init gizmo modes { // Add default modes used by the editor Gizmos.AddMode(new TransformGizmoMode()); Gizmos.AddMode(new NoGizmoMode()); Gizmos.AddMode(SculptTerrainGizmo = new Tools.Terrain.SculptTerrainGizmoMode()); Gizmos.AddMode(PaintTerrainGizmo = new Tools.Terrain.PaintTerrainGizmoMode()); Gizmos.AddMode(EditTerrainGizmo = new Tools.Terrain.EditTerrainGizmoMode()); Gizmos.AddMode(PaintFoliageGizmo = new Tools.Foliage.PaintFoliageGizmoMode()); Gizmos.AddMode(EditFoliageGizmo = new Tools.Foliage.EditFoliageGizmoMode()); // Activate transform mode first Gizmos.SetActiveMode(); } // Setup input actions InputActions.Add(options => options.LockFocusSelection, LockFocusSelection); InputActions.Add(options => options.FocusSelection, FocusSelection); InputActions.Add(options => options.RotateSelection, RotateSelection); InputActions.Add(options => options.Delete, _editor.SceneEditing.Delete); } /// public override void Update(float deltaTime) { base.Update(deltaTime); var selection = TransformGizmo.SelectedParents; var requestUnlockFocus = FlaxEngine.Input.Mouse.GetButtonDown(MouseButton.Right) || FlaxEngine.Input.Mouse.GetButtonDown(MouseButton.Left); if (TransformGizmo.SelectedParents.Count == 0 || (requestUnlockFocus && ContainsFocus)) { UnlockFocusSelection(); } else if (_lockedFocus) { var selectionBounds = BoundingSphere.Empty; for (int i = 0; i < selection.Count; i++) { selection[i].GetEditorSphere(out var sphere); BoundingSphere.Merge(ref selectionBounds, ref sphere, out selectionBounds); } if (ContainsFocus) { var viewportFocusDistance = Vector3.Distance(ViewPosition, selectionBounds.Center) / 10f; _lockedFocusOffset -= FlaxEngine.Input.Mouse.ScrollDelta * viewportFocusDistance; } var focusDistance = Mathf.Max(selectionBounds.Radius * 2d, 100d); ViewPosition = selectionBounds.Center + (-ViewDirection * (focusDistance + _lockedFocusOffset)); } } /// /// Overrides the selection outline effect or restored the default one. /// /// The custom selection outline or null if use default one. public void OverrideSelectionOutline(SelectionOutline customSelectionOutline) { if (_customSelectionOutline != null) { Task.RemoveCustomPostFx(_customSelectionOutline); Object.Destroy(ref _customSelectionOutline); Task.AddCustomPostFx(customSelectionOutline ? customSelectionOutline : SelectionOutline); } else if (customSelectionOutline != null) { Task.RemoveCustomPostFx(SelectionOutline); Task.AddCustomPostFx(customSelectionOutline); } _customSelectionOutline = customSelectionOutline; } private void CreateCameraAtView() { if (!Level.IsAnySceneLoaded) return; // Create actor var parent = Level.GetScene(0); var actor = new Camera { StaticFlags = StaticFlags.None, Name = Utilities.Utils.IncrementNameNumber("Camera", x => parent.GetChild(x) == null), Transform = ViewTransform, NearPlane = NearPlane, FarPlane = FarPlane, OrthographicScale = OrthographicScale, UsePerspective = !UseOrthographicProjection, FieldOfView = FieldOfView }; // Spawn _editor.SceneEditing.Spawn(actor, parent); } private void OnBegin(RenderTask task, GPUContext context) { _debugDrawData.Clear(); // Collect selected objects debug shapes and visuals var selectedParents = TransformGizmo.SelectedParents; if (selectedParents.Count > 0) { for (int i = 0; i < selectedParents.Count; i++) { if (selectedParents[i].IsActiveInHierarchy) selectedParents[i].OnDebugDraw(_debugDrawData); } } } private void OnCollectDrawCalls(ref RenderContext renderContext) { DragHandlers.CollectDrawCalls(_debugDrawData, ref renderContext); if (ShowNavigation) Editor.Internal_DrawNavMesh(); _debugDrawData.OnDraw(ref renderContext); } /// public void DrawEditorPrimitives(GPUContext context, ref RenderContext renderContext, GPUTexture target, GPUTexture targetDepth) { // Draw gizmos for (int i = 0; i < Gizmos.Count; i++) { Gizmos[i].Draw(ref renderContext); } // Draw selected objects debug shapes and visuals if (DrawDebugDraw && (renderContext.View.Flags & ViewFlags.DebugDraw) == ViewFlags.DebugDraw) { unsafe { fixed (IntPtr* actors = _debugDrawData.ActorsPtrs) { DebugDraw.DrawActors(new IntPtr(actors), _debugDrawData.ActorsCount, true); } } DebugDraw.Draw(ref renderContext, target.View(), targetDepth.View(), true); } } private void OnPostRender(GPUContext context, ref RenderContext renderContext) { bool renderPostFx = true; switch (renderContext.View.Mode) { case ViewMode.Default: case ViewMode.PhysicsColliders: renderPostFx = false; break; } if (renderPostFx) { var task = renderContext.Task; // Render editor primitives, gizmo and debug shapes in debug view modes // Note: can use Output buffer as both input and output because EditorPrimitives is using an intermediate buffer if (EditorPrimitives && EditorPrimitives.CanRender()) { EditorPrimitives.Render(context, ref renderContext, task.Output, task.Output); } // Render editor sprites if (_editorSpritesRenderer && _editorSpritesRenderer.CanRender()) { _editorSpritesRenderer.Render(context, ref renderContext, task.Output, task.Output); } // Render selection outline var selectionOutline = _customSelectionOutline ?? SelectionOutline; if (selectionOutline && selectionOutline.CanRender()) { // Use temporary intermediate buffer var desc = task.Output.Description; var temp = RenderTargetPool.Get(ref desc); selectionOutline.Render(context, ref renderContext, task.Output, temp); // Copy the results back to the output context.CopyTexture(task.Output, 0, 0, 0, 0, temp, 0); RenderTargetPool.Release(temp); } } } private void OnSelectionChanged() { var selection = _editor.SceneEditing.Selection; Gizmos.ForEach(x => x.OnSelectionChanged(selection)); } /// /// Press "R" to rotate the selected gizmo objects 45 degrees. /// public void RotateSelection() { var win = (WindowRootControl)Root; var selection = _editor.SceneEditing.Selection; var isShiftDown = win.GetKey(KeyboardKeys.Shift); Quaternion rotationDelta; if (isShiftDown) rotationDelta = Quaternion.Euler(0.0f, -45.0f, 0.0f); else rotationDelta = Quaternion.Euler(0.0f, 45.0f, 0.0f); bool useObjCenter = TransformGizmo.ActivePivot == TransformGizmoBase.PivotType.ObjectCenter; Vector3 gizmoPosition = TransformGizmo.Position; // Rotate selected objects bool isPlayMode = _editor.StateMachine.IsPlayMode; TransformGizmo.StartTransforming(); for (int i = 0; i < selection.Count; i++) { var obj = selection[i]; if (isPlayMode && obj.CanTransform == false) continue; var trans = obj.Transform; var pivotOffset = trans.Translation - gizmoPosition; if (useObjCenter || pivotOffset.IsZero) { trans.Orientation *= Quaternion.Invert(trans.Orientation) * rotationDelta * trans.Orientation; } else { Matrix.RotationQuaternion(ref trans.Orientation, out var transWorld); Matrix.RotationQuaternion(ref rotationDelta, out var deltaWorld); Matrix world = transWorld * Matrix.Translation(pivotOffset) * deltaWorld * Matrix.Translation(-pivotOffset); trans.SetRotation(ref world); trans.Translation += world.TranslationVector; } if (obj is ActorNode actorNode) { actorNode.Actor.Position = trans.Translation; actorNode.Actor.Orientation = trans.Orientation; } else obj.Transform = trans; } TransformGizmo.EndTransforming(); } /// /// Focuses the viewport on the current selection of the gizmo. /// public void FocusSelection() { var orientation = ViewOrientation; FocusSelection(ref orientation); } /// /// Lock focus on the current selection gizmo. /// public void LockFocusSelection() { _lockedFocus = true; } /// /// Unlock focus on the current selection. /// public void UnlockFocusSelection() { _lockedFocus = false; _lockedFocusOffset = 0f; } /// /// Focuses the viewport on the current selection of the gizmo. /// /// The target view orientation. public void FocusSelection(ref Quaternion orientation) { ViewportCamera.FocusSelection(Gizmos, ref orientation); } /// /// Applies the transform to the collection of scene graph nodes. /// /// The selection. /// The translation delta. /// The rotation delta. /// The scale delta. public void ApplyTransform(List selection, ref Vector3 translationDelta, ref Quaternion rotationDelta, ref Vector3 scaleDelta) { bool applyTranslation = !translationDelta.IsZero; bool applyRotation = !rotationDelta.IsIdentity; bool applyScale = !scaleDelta.IsZero; bool useObjCenter = TransformGizmo.ActivePivot == TransformGizmoBase.PivotType.ObjectCenter; Vector3 gizmoPosition = TransformGizmo.Position; // Transform selected objects bool isPlayMode = _editor.StateMachine.IsPlayMode; for (int i = 0; i < selection.Count; i++) { var obj = selection[i]; // Block transforming static objects in play mode if (isPlayMode && obj.CanTransform == false) continue; var trans = obj.Transform; // Apply rotation if (applyRotation) { Vector3 pivotOffset = trans.Translation - gizmoPosition; if (useObjCenter || pivotOffset.IsZero) { //trans.Orientation *= rotationDelta; trans.Orientation *= Quaternion.Invert(trans.Orientation) * rotationDelta * trans.Orientation; } else { Matrix.RotationQuaternion(ref trans.Orientation, out var transWorld); Matrix.RotationQuaternion(ref rotationDelta, out var deltaWorld); Matrix world = transWorld * Matrix.Translation(pivotOffset) * deltaWorld * Matrix.Translation(-pivotOffset); trans.SetRotation(ref world); trans.Translation += world.TranslationVector; } } // Apply scale if (applyScale) { const float scaleLimit = 99_999_999.0f; trans.Scale = Float3.Clamp(trans.Scale + scaleDelta, new Float3(-scaleLimit), new Float3(scaleLimit)); ; } // Apply translation if (applyTranslation) { trans.Translation += translationDelta; } if (obj is ActorNode actorNode) { if (applyTranslation) actorNode.Actor.Position = trans.Translation; if (applyRotation) actorNode.Actor.Orientation = trans.Orientation; if (applyScale) actorNode.Actor.Scale = trans.Scale; } else obj.Transform = trans; } } /// protected override void OrientViewport(ref Quaternion orientation) { if (TransformGizmo.SelectedParents.Count != 0) FocusSelection(ref orientation); else base.OrientViewport(ref orientation); } /// protected override void OnLeftMouseButtonUp() { // Skip if was controlling mouse or mouse is not over the area if (_prevInput.IsControllingMouse || !Bounds.Contains(ref _viewMousePos)) return; // Try to pick something with the current gizmo Gizmos.Active?.Pick(); // Keep focus Focus(); base.OnLeftMouseButtonUp(); } /// public override DragDropEffect OnDragEnter(ref Float2 location, DragData data) { DragHandlers.ClearDragEffects(); var result = base.OnDragEnter(ref location, data); if (result != DragDropEffect.None) return result; return DragHandlers.DragEnter(ref location, data); } private bool ValidateDragItem(ContentItem contentItem) { if (!Level.IsAnySceneLoaded) return false; if (contentItem is AssetItem assetItem) { if (assetItem.OnEditorDrag(this)) return true; if (assetItem.IsOfType()) return true; if (assetItem.IsOfType()) return true; } return false; } private static bool ValidateDragActorType(ScriptType actorType) { return Level.IsAnySceneLoaded; } private static bool ValidateDragScriptItem(ScriptItem script) { return Editor.Instance.CodeEditing.Actors.Get(script) != ScriptType.Null; } /// public override DragDropEffect OnDragMove(ref Float2 location, DragData data) { DragHandlers.ClearDragEffects(); var result = base.OnDragMove(ref location, data); if (result != DragDropEffect.None) return result; return DragHandlers.DragEnter(ref location, data); } /// public override void OnDragLeave() { DragHandlers.ClearDragEffects(); DragHandlers.OnDragLeave(); base.OnDragLeave(); } /// public override DragDropEffect OnDragDrop(ref Float2 location, DragData data) { DragHandlers.ClearDragEffects(); var result = base.OnDragDrop(ref location, data); if (result != DragDropEffect.None) return result; return DragHandlers.DragDrop(ref location, data); } /// public override void Select(List nodes) { _editor.SceneEditing.Select(nodes); } /// public override void Spawn(Actor actor) { var parent = actor.Parent ?? Level.GetScene(0); actor.Name = Utilities.Utils.IncrementNameNumber(actor.Name, x => parent.GetChild(x) == null); _editor.SceneEditing.Spawn(actor); } /// public override void OpenContextMenu() { var mouse = PointFromWindow(Root.MousePosition); _editor.Windows.SceneWin.ShowContextMenu(this, mouse); } /// public override void OnDestroy() { if (IsDisposing) return; _debugDrawData.Dispose(); if (_task != null) { // Release if task is not used to save screenshot for project icon Object.Destroy(ref _task); ReleaseResources(); } base.OnDestroy(); } private RenderTask _savedTask; private GPUTexture _savedBackBuffer; internal void SaveProjectIcon() { TakeScreenshot(StringUtils.CombinePaths(Globals.ProjectCacheFolder, "icon.png")); _savedTask = _task; _savedBackBuffer = _backBuffer; _task = null; _backBuffer = null; } internal void SaveProjectIconEnd() { if (_savedTask) { _savedTask.Enabled = false; Object.Destroy(_savedTask); ReleaseResources(); _savedTask = null; } Object.Destroy(ref _savedBackBuffer); } private void ReleaseResources() { if (Task) { Task.RemoveCustomPostFx(SelectionOutline); Task.RemoveCustomPostFx(EditorPrimitives); Task.RemoveCustomPostFx(_editorSpritesRenderer); Task.RemoveCustomPostFx(_customSelectionOutline); } Object.Destroy(ref SelectionOutline); Object.Destroy(ref EditorPrimitives); Object.Destroy(ref _editorSpritesRenderer); Object.Destroy(ref _customSelectionOutline); } } }