You're breathtaking!

This commit is contained in:
Wojtek Figat
2020-12-07 23:40:54 +01:00
commit 6fb9eee74c
5143 changed files with 1153594 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
namespace FlaxEditor.Viewport.Cameras
{
/// <summary>
/// Implementation of <see cref="ViewportCamera"/> that orbits around the fixed location.
/// </summary>
/// <seealso cref="FlaxEditor.Viewport.Cameras.ViewportCamera" />
[HideInEditor]
public class ArcBallCamera : ViewportCamera
{
private Vector3 _orbitCenter;
private float _orbitRadius;
/// <summary>
/// Gets or sets the orbit center.
/// </summary>
public Vector3 OrbitCenter
{
get => _orbitCenter;
set
{
_orbitCenter = value;
UpdatePosition();
}
}
/// <summary>
/// Gets or sets the orbit radius.
/// </summary>
public float OrbitRadius
{
get => _orbitRadius;
set
{
_orbitRadius = Mathf.Max(value, 0.0001f);
UpdatePosition();
}
}
/// <inheritdoc />
public override bool UseMovementSpeed => false;
/// <summary>
/// Initializes a new instance of the <see cref="ArcBallCamera"/> class.
/// </summary>
public ArcBallCamera()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ArcBallCamera"/> class.
/// </summary>
/// <param name="orbitCenter">The orbit center.</param>
/// <param name="radius">The orbit radius.</param>
public ArcBallCamera(Vector3 orbitCenter, float radius)
{
_orbitCenter = orbitCenter;
_orbitRadius = radius;
}
/// <summary>
/// Sets view direction.
/// </summary>
/// <param name="direction">The view direction.</param>
public void SetView(Vector3 direction)
{
// Rotate
Viewport.ViewDirection = direction;
// Update view position
UpdatePosition();
}
/// <summary>
/// Sets view orientation.
/// </summary>
/// <param name="orientation">The view rotation.</param>
public void SetView(Quaternion orientation)
{
// Rotate
Viewport.ViewOrientation = orientation;
// Update view position
UpdatePosition();
}
private void UpdatePosition()
{
// Move camera to look at orbit center point
Vector3 localPosition = Viewport.ViewDirection * (-1 * _orbitRadius);
Viewport.ViewPosition = _orbitCenter + localPosition;
}
/// <inheritdoc />
public override void UpdateView(float dt, ref Vector3 moveDelta, ref Vector2 mouseDelta, out bool centerMouse)
{
centerMouse = true;
EditorViewport.Input input;
Viewport.GetInput(out input);
// Rotate
Viewport.YawPitch += mouseDelta;
// Zoom
if (input.IsZooming)
{
_orbitRadius = Mathf.Clamp(_orbitRadius - (Viewport.MouseWheelZoomSpeedFactor * input.MouseWheelDelta * 25.0f), 0.001f, 10000.0f);
}
// Update view position
UpdatePosition();
}
}
}

View File

@@ -0,0 +1,245 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using FlaxEditor.Gizmo;
using FlaxEditor.SceneGraph;
using FlaxEngine;
namespace FlaxEditor.Viewport.Cameras
{
/// <summary>
/// Implementation of <see cref="ViewportCamera"/> that simulated the first-person camera which can fly though the scene.
/// </summary>
/// <seealso cref="FlaxEditor.Viewport.Cameras.ViewportCamera" />
[HideInEditor]
public class FPSCamera : ViewportCamera
{
private Transform _startMove;
private Transform _endMove;
private float _moveStartTime = -1;
/// <summary>
/// Gets a value indicating whether this viewport is animating movement.
/// </summary>
public bool IsAnimatingMove => _moveStartTime > Mathf.Epsilon;
/// <summary>
/// The target point location. It's used to orbit around it whe user clicks Alt+LMB.
/// </summary>
public Vector3 TargetPoint = new Vector3(-200);
/// <summary>
/// Sets view.
/// </summary>
/// <param name="position">The view position.</param>
/// <param name="direction">The view direction.</param>
public void SetView(Vector3 position, Vector3 direction)
{
if (IsAnimatingMove)
return;
// Rotate and move
Viewport.ViewPosition = position;
Viewport.ViewDirection = direction;
}
/// <summary>
/// Sets view.
/// </summary>
/// <param name="position">The view position.</param>
/// <param name="orientation">The view rotation.</param>
public void SetView(Vector3 position, Quaternion orientation)
{
if (IsAnimatingMove)
return;
// Rotate and move
Viewport.ViewPosition = position;
Viewport.ViewOrientation = orientation;
}
/// <summary>
/// Start animating viewport movement to the target transformation.
/// </summary>
/// <param name="position">The target position.</param>
/// <param name="orientation">The target orientation.</param>
public void MoveViewport(Vector3 position, Quaternion orientation)
{
MoveViewport(new Transform(position, orientation));
}
/// <summary>
/// Start animating viewport movement to the target transformation.
/// </summary>
/// <param name="target">The target transform.</param>
public void MoveViewport(Transform target)
{
_startMove = Viewport.ViewTransform;
_endMove = target;
_moveStartTime = Time.UnscaledGameTime;
}
/// <summary>
/// Moves the viewport to visualize the actor.
/// </summary>
/// <param name="actor">The actor to preview.</param>
public void ShowActor(Actor actor)
{
BoundingSphere sphere;
Editor.GetActorEditorSphere(actor, out sphere);
ShowSphere(ref sphere);
}
/// <summary>
/// Moves the viewport to visualize selected actors.
/// </summary>
/// <param name="actors">The actors to show.</param>
public void ShowActors(List<SceneGraphNode> actors)
{
if (actors.Count == 0)
return;
BoundingSphere mergesSphere = BoundingSphere.Empty;
for (int i = 0; i < actors.Count; i++)
{
if (actors[i] is ActorNode actor)
{
BoundingSphere sphere;
Editor.GetActorEditorSphere(actor.Actor, out sphere);
BoundingSphere.Merge(ref mergesSphere, ref sphere, out mergesSphere);
}
}
ShowSphere(ref mergesSphere);
}
private void ShowSphere(ref BoundingSphere sphere)
{
// Calculate view transform
Quaternion orientation = new Quaternion(0.424461186f, -0.0940724313f, 0.0443938486f, 0.899451137f);
Vector3 position = sphere.Center - Vector3.Forward * orientation * (sphere.Radius * 2.5f);
// Move viewport
TargetPoint = sphere.Center;
MoveViewport(position, orientation);
}
/// <inheritdoc />
public override void Update(float deltaTime)
{
// Update animated movement
if (IsAnimatingMove)
{
// Calculate linear progress
float animationDuration = 0.5f;
float time = Time.UnscaledGameTime;
float progress = (time - _moveStartTime) / animationDuration;
// Check for end
if (progress >= 1.0f)
{
// Animation has been finished
_moveStartTime = -1;
}
// Animate camera
float a = Mathf.Saturate(progress);
a = a * a * a;
Transform targetTransform = Transform.Lerp(_startMove, _endMove, a);
targetTransform.Scale = Vector3.Zero;
Viewport.ViewPosition = targetTransform.Translation;
Viewport.ViewOrientation = targetTransform.Orientation;
}
}
/// <inheritdoc />
public override void UpdateView(float dt, ref Vector3 moveDelta, ref Vector2 mouseDelta, out bool centerMouse)
{
centerMouse = true;
if (IsAnimatingMove)
return;
EditorViewport.Input input;
Viewport.GetInput(out input);
var mainViewport = Viewport as MainEditorGizmoViewport;
bool isUsingGizmo = mainViewport != null && mainViewport.TransformGizmo.ActiveAxis != TransformGizmo.Axis.None;
// Get current view properties
float yaw = Viewport.Yaw;
float pitch = Viewport.Pitch;
var position = Viewport.ViewPosition;
var rotation = Viewport.ViewOrientation;
// Compute base vectors for camera movement
var forward = Vector3.Forward * rotation;
var up = Vector3.Up * rotation;
var right = Vector3.Cross(forward, up);
// Dolly
if (input.IsPanning || input.IsMoving || input.IsRotating)
{
Vector3 move;
Vector3.Transform(ref moveDelta, ref rotation, out move);
position += move;
}
// Pan
if (input.IsPanning)
{
var panningSpeed = 0.8f;
position -= right * (mouseDelta.X * panningSpeed);
position -= up * (mouseDelta.Y * panningSpeed);
}
// Move
if (input.IsMoving)
{
// Move camera over XZ plane
var projectedForward = Vector3.Normalize(new Vector3(forward.X, 0, forward.Z));
position -= projectedForward * mouseDelta.Y;
yaw += mouseDelta.X;
}
// Rotate or orbit
if (input.IsRotating || (input.IsOrbiting && !isUsingGizmo))
{
yaw += mouseDelta.X;
pitch += mouseDelta.Y;
}
// Zoom in/out
if (input.IsZooming && !input.IsRotating)
{
position += forward * (Viewport.MouseWheelZoomSpeedFactor * input.MouseWheelDelta * 25.0f);
if (input.IsAltDown)
{
position += forward * (Viewport.MouseSpeed * 40 * Viewport.MouseDeltaRight.ValuesSum);
}
}
// Move camera with the gizmo
if (input.IsOrbiting && isUsingGizmo)
{
centerMouse = false;
Viewport.ViewPosition += mainViewport.TransformGizmo.LastDelta.Translation;
return;
}
// Update view
Viewport.Yaw = yaw;
Viewport.Pitch = pitch;
if (input.IsOrbiting)
{
float orbitRadius = Vector3.Distance(ref position, ref TargetPoint);
Vector3 localPosition = Viewport.ViewDirection * (-1 * orbitRadius);
Viewport.ViewPosition = TargetPoint + localPosition;
}
else
{
TargetPoint += position - Viewport.ViewPosition;
Viewport.ViewPosition = position;
}
}
}
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
namespace FlaxEditor.Viewport.Cameras
{
/// <summary>
/// The interface for the editor viewport camera controllers. Handles the input logic updates and the preview rendering viewport.
/// </summary>
[HideInEditor]
public interface IViewportCamera
{
/// <summary>
/// Updates the camera.
/// </summary>
/// <param name="deltaTime">The delta time (in seconds).</param>
void Update(float deltaTime);
/// <summary>
/// Updates the view.
/// </summary>
/// <param name="dt">The delta time (in seconds).</param>
/// <param name="moveDelta">The move delta (scaled).</param>
/// <param name="mouseDelta">The mouse delta (scaled).</param>
/// <param name="centerMouse">True if center mouse after the update.</param>
void UpdateView(float dt, ref Vector3 moveDelta, ref Vector2 mouseDelta, out bool centerMouse);
}
}

View File

@@ -0,0 +1,83 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
namespace FlaxEditor.Viewport.Cameras
{
/// <summary>
/// Base class for <see cref="EditorViewport"/> view controllers.
/// </summary>
/// <seealso cref="FlaxEditor.Viewport.Cameras.IViewportCamera" />
[HideInEditor]
public abstract class ViewportCamera : IViewportCamera
{
private EditorViewport _viewport;
/// <summary>
/// Gets the parent viewport.
/// </summary>
public EditorViewport Viewport
{
get => _viewport;
internal set => _viewport = value;
}
/// <summary>
/// Gets a value indicating whether the viewport camera uses movement speed.
/// </summary>
public virtual bool UseMovementSpeed => true;
/// <summary>
/// Sets view orientation and position to match the arc ball camera style view for the given target object bounds.
/// </summary>
/// <param name="objectBounds">The target object bounds.</param>
/// <param name="marginDistanceScale">The margin distance scale of the orbit radius.</param>
public void SerArcBallView(BoundingBox objectBounds, float marginDistanceScale = 2.0f)
{
SerArcBallView(BoundingSphere.FromBox(objectBounds), marginDistanceScale);
}
/// <summary>
/// Sets view orientation and position to match the arc ball camera style view for the given target object bounds.
/// </summary>
/// <param name="objectBounds">The target object bounds.</param>
/// <param name="marginDistanceScale">The margin distance scale of the orbit radius.</param>
public void SerArcBallView(BoundingSphere objectBounds, float marginDistanceScale = 2.0f)
{
SerArcBallView(new Quaternion(-0.08f, -0.92f, 0.31f, -0.23f), objectBounds.Center, objectBounds.Radius * marginDistanceScale);
}
/// <summary>
/// Sets view orientation and position to match the arc ball camera style view for the given orbit radius.
/// </summary>
/// <param name="orbitRadius">The orbit radius.</param>
public void SerArcBallView(float orbitRadius)
{
SerArcBallView(new Quaternion(-0.08f, -0.92f, 0.31f, -0.23f), Vector3.Zero, orbitRadius);
}
/// <summary>
/// Sets view orientation and position to match the arc ball camera style view.
/// </summary>
/// <param name="orientation">The view rotation.</param>
/// <param name="orbitCenter">The orbit center location.</param>
/// <param name="orbitRadius">The orbit radius.</param>
public void SerArcBallView(Quaternion orientation, Vector3 orbitCenter, float orbitRadius)
{
// Rotate
Viewport.ViewOrientation = orientation;
// Move camera to look at orbit center point
Vector3 localPosition = Viewport.ViewDirection * (-1 * orbitRadius);
Viewport.ViewPosition = orbitCenter + localPosition;
}
/// <inheritdoc />
public virtual void Update(float deltaTime)
{
}
/// <inheritdoc />
public abstract void UpdateView(float dt, ref Vector3 moveDelta, ref Vector2 mouseDelta, out bool centerMouse);
}
}

View File

@@ -0,0 +1,92 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.Gizmo;
using FlaxEditor.Viewport.Cameras;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Viewport
{
/// <summary>
/// Viewport with free camera and gizmo tools.
/// </summary>
/// <seealso cref="FlaxEditor.Viewport.EditorViewport" />
/// <seealso cref="IGizmoOwner" />
public class EditorGizmoViewport : EditorViewport, IGizmoOwner
{
private UpdateDelegate _update;
/// <summary>
/// Initializes a new instance of the <see cref="EditorGizmoViewport"/> class.
/// </summary>
/// <param name="task">The task.</param>
/// <param name="undo">The undo.</param>
public EditorGizmoViewport(SceneRenderTask task, Undo undo)
: base(task, new FPSCamera(), true)
{
Undo = undo;
SetUpdate(ref _update, OnUpdate);
}
private void OnUpdate(float deltaTime)
{
for (int i = 0; i < Gizmos.Count; i++)
{
Gizmos[i].Update(deltaTime);
}
}
/// <inheritdoc />
public GizmosCollection Gizmos { get; } = new GizmosCollection();
/// <inheritdoc />
public SceneRenderTask RenderTask => Task;
/// <inheritdoc />
public float ViewFarPlane => FarPlane;
/// <inheritdoc />
public bool IsLeftMouseButtonDown => _input.IsMouseLeftDown;
/// <inheritdoc />
public bool IsRightMouseButtonDown => _input.IsMouseRightDown;
/// <inheritdoc />
public bool IsAltKeyDown => _input.IsAltDown;
/// <inheritdoc />
public bool IsControlDown => _input.IsControlDown;
/// <inheritdoc />
public bool SnapToGround => Editor.Instance.Options.Options.Input.SnapToGround.Process(Root);
/// <inheritdoc />
public Vector2 MouseDelta => _mouseDeltaLeft * 1000;
/// <inheritdoc />
public bool UseSnapping => Root.GetKey(KeyboardKeys.Control);
/// <inheritdoc />
public bool UseDuplicate => Root.GetKey(KeyboardKeys.Shift);
/// <inheritdoc />
public Undo Undo { get; }
/// <inheritdoc />
protected override void AddUpdateCallbacks(RootControl root)
{
base.AddUpdateCallbacks(root);
root.UpdateCallbacksToAdd.Add(_update);
}
/// <inheritdoc />
protected override void RemoveUpdateCallbacks(RootControl root)
{
base.RemoveUpdateCallbacks(root);
root.UpdateCallbacksToRemove.Add(_update);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,158 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEditor.Viewport.Modes;
namespace FlaxEditor.Viewport
{
public partial class MainEditorGizmoViewport
{
private EditorGizmoMode _activeMode;
private readonly List<EditorGizmoMode> _modes = new List<EditorGizmoMode>();
/// <summary>
/// Gets the active gizmo mode.
/// </summary>
public EditorGizmoMode ActiveMode => _activeMode;
/// <summary>
/// Occurs when active mode gets changed.
/// </summary>
public event Action<EditorGizmoMode> ActiveModeChanged;
/// <summary>
/// The sculpt terrain gizmo.
/// </summary>
public Tools.Terrain.SculptTerrainGizmoMode SculptTerrainGizmo;
/// <summary>
/// The paint terrain gizmo.
/// </summary>
public Tools.Terrain.PaintTerrainGizmoMode PaintTerrainGizmo;
/// <summary>
/// The edit terrain gizmo.
/// </summary>
public Tools.Terrain.EditTerrainGizmoMode EditTerrainGizmo;
/// <summary>
/// The paint foliage gizmo.
/// </summary>
public Tools.Foliage.PaintFoliageGizmoMode PaintFoliageGizmo;
/// <summary>
/// The edit foliage gizmo.
/// </summary>
public Tools.Foliage.EditFoliageGizmoMode EditFoliageGizmo;
private void InitModes()
{
// Add default modes used by the editor
_modes.Add(new TransformGizmoMode());
_modes.Add(new NoGizmoMode());
_modes.Add(SculptTerrainGizmo = new Tools.Terrain.SculptTerrainGizmoMode());
_modes.Add(PaintTerrainGizmo = new Tools.Terrain.PaintTerrainGizmoMode());
_modes.Add(EditTerrainGizmo = new Tools.Terrain.EditTerrainGizmoMode());
_modes.Add(PaintFoliageGizmo = new Tools.Foliage.PaintFoliageGizmoMode());
_modes.Add(EditFoliageGizmo = new Tools.Foliage.EditFoliageGizmoMode());
for (int i = 0; i < _modes.Count; i++)
{
_modes[i].Init(this);
}
// Activate transform mode first
_activeMode = _modes[0];
}
private void DisposeModes()
{
// Cleanup
_activeMode = null;
for (int i = 0; i < _modes.Count; i++)
_modes[i].Dispose();
_modes.Clear();
}
/// <summary>
/// Adds the mode to the viewport.
/// </summary>
/// <param name="mode">The mode.</param>
public void AddMode(EditorGizmoMode mode)
{
if (mode == null)
throw new ArgumentNullException(nameof(mode));
if (_modes.Contains(mode))
throw new ArgumentException("Already added.");
if (mode.Viewport != null)
throw new ArgumentException("Already added to other viewport.");
_modes.Add(mode);
mode.Init(this);
}
/// <summary>
/// Removes the mode from the viewport.
/// </summary>
/// <param name="mode">The mode.</param>
public void RemoveMode(EditorGizmoMode mode)
{
if (mode == null)
throw new ArgumentNullException(nameof(mode));
if (!_modes.Contains(mode))
throw new ArgumentException("Not added.");
if (mode.Viewport != this)
throw new ArgumentException("Not added to this viewport.");
if (_activeMode == mode)
SetActiveMode(null);
_modes.Remove(mode);
}
/// <summary>
/// Sets the active mode.
/// </summary>
/// <param name="mode">The mode.</param>
public void SetActiveMode(EditorGizmoMode mode)
{
if (mode == _activeMode)
return;
if (mode != null)
{
if (!_modes.Contains(mode))
throw new ArgumentException("Not added.");
if (mode.Viewport != this)
throw new ArgumentException("Not added to this viewport.");
}
_activeMode?.OnDeactivated();
Gizmos.Active = null;
_activeMode = mode;
_activeMode?.OnActivated();
ActiveModeChanged?.Invoke(mode);
}
/// <summary>
/// Sets the active mode.
/// </summary>
/// <typeparam name="T">The mode type.</typeparam>
/// <returns>The activated mode.</returns>
public T SetActiveMode<T>() where T : EditorGizmoMode
{
for (int i = 0; i < _modes.Count; i++)
{
if (_modes[i] is T mode)
{
SetActiveMode(mode);
return mode;
}
}
throw new ArgumentException("Not added mode to activate.");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,67 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEngine;
namespace FlaxEditor.Viewport.Modes
{
/// <summary>
/// Editor viewport gizmo mode descriptor. Defines which gizmo tools to show and how to handle scene editing.
/// </summary>
/// <remarks>
/// Only one gizmo mode can be active at the time. It defines the viewport toolset usage.
/// </remarks>
[HideInEditor]
public abstract class EditorGizmoMode
{
private MainEditorGizmoViewport _viewport;
/// <summary>
/// Gets the viewport.
/// </summary>
public MainEditorGizmoViewport Viewport => _viewport;
/// <summary>
/// Occurs when mode gets activated.
/// </summary>
public event Action Activated;
/// <summary>
/// Occurs when mode gets deactivated.
/// </summary>
public event Action Deactivated;
/// <summary>
/// Initializes the specified mode and links it to the viewport.
/// </summary>
/// <param name="viewport">The viewport.</param>
public virtual void Init(MainEditorGizmoViewport viewport)
{
_viewport = viewport;
}
/// <summary>
/// Releases the mode. Called on editor exit or when mode gets removed from the current viewport.
/// </summary>
public virtual void Dispose()
{
_viewport = null;
}
/// <summary>
/// Called when mode gets activated.
/// </summary>
public virtual void OnActivated()
{
Activated?.Invoke();
}
/// <summary>
/// Called when mode gets deactivated.
/// </summary>
public virtual void OnDeactivated()
{
Deactivated?.Invoke();
}
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
namespace FlaxEditor.Viewport.Modes
{
/// <summary>
/// The editor gizmo editor mode that does nothing. Can be used to hide other gizmos when using a specific editor tool.
/// </summary>
/// <seealso cref="FlaxEditor.Viewport.Modes.EditorGizmoMode" />
public class NoGizmoMode : EditorGizmoMode
{
/// <inheritdoc />
public override void OnActivated()
{
base.OnActivated();
Viewport.Gizmos.Active = null;
}
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
namespace FlaxEditor.Viewport.Modes
{
/// <summary>
/// The default editor mode that uses <see cref="FlaxEditor.Gizmo.TransformGizmo"/> for objects transforming.
/// </summary>
/// <seealso cref="FlaxEditor.Viewport.Modes.EditorGizmoMode" />
public class TransformGizmoMode : EditorGizmoMode
{
/// <inheritdoc />
public override void OnActivated()
{
base.OnActivated();
Viewport.Gizmos.Active = Viewport.TransformGizmo;
}
}
}

View File

@@ -0,0 +1,797 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEditor.Content;
using FlaxEditor.Gizmo;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Drag;
using FlaxEditor.SceneGraph;
using FlaxEditor.SceneGraph.Actors;
using FlaxEditor.Scripting;
using FlaxEditor.Viewport.Cameras;
using FlaxEditor.Viewport.Previews;
using FlaxEditor.Viewport.Widgets;
using FlaxEditor.Windows.Assets;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Viewport
{
/// <summary>
/// Editor viewport used by the <see cref="PrefabWindow"/>
/// </summary>
/// <seealso cref="PrefabWindow" />
/// <seealso cref="PrefabPreview" />
/// <seealso cref="IGizmoOwner" />
public class PrefabWindowViewport : PrefabPreview, IEditorPrimitivesOwner
{
private readonly PrefabWindow _window;
private UpdateDelegate _update;
private readonly ViewportWidgetButton _gizmoModeTranslate;
private readonly ViewportWidgetButton _gizmoModeRotate;
private readonly ViewportWidgetButton _gizmoModeScale;
private ViewportWidgetButton _translateSnappng;
private ViewportWidgetButton _rotateSnapping;
private ViewportWidgetButton _scaleSnapping;
private readonly DragAssets _dragAssets = new DragAssets(ValidateDragItem);
private readonly DragActorType _dragActorType = new DragActorType(ValidateDragActorType);
private readonly DragHandlers _dragHandlers = new DragHandlers();
/// <summary>
/// The transform gizmo.
/// </summary>
public readonly TransformGizmo TransformGizmo;
/// <summary>
/// The selection outline postFx.
/// </summary>
public SelectionOutline SelectionOutline;
/// <summary>
/// The editor primitives postFx.
/// </summary>
public EditorPrimitives EditorPrimitives;
/// <summary>
/// Initializes a new instance of the <see cref="PrefabWindowViewport"/> class.
/// </summary>
/// <param name="window">Editor window.</param>
public PrefabWindowViewport(PrefabWindow window)
: base(true)
{
_window = window;
_window.SelectionChanged += OnSelectionChanged;
Undo = window.Undo;
ViewportCamera = new FPSCamera();
// Prepare rendering task
Task.ActorsSource = ActorsSources.CustomActors;
Task.ViewFlags = ViewFlags.DefaultEditor & ~ViewFlags.EditorSprites;
Task.PostRender += OnPostRender;
// Create post effects
SelectionOutline = FlaxEngine.Object.New<SelectionOutline>();
SelectionOutline.SelectionGetter = () => TransformGizmo.SelectedParents;
Task.CustomPostFx.Add(SelectionOutline);
EditorPrimitives = FlaxEngine.Object.New<EditorPrimitives>();
EditorPrimitives.Viewport = this;
Task.CustomPostFx.Add(EditorPrimitives);
// Add transformation gizmo
TransformGizmo = new TransformGizmo(this);
TransformGizmo.ApplyTransformation += ApplyTransform;
TransformGizmo.ModeChanged += OnGizmoModeChanged;
TransformGizmo.Duplicate += _window.Duplicate;
Gizmos.Active = TransformGizmo;
// Transform space widget
var transformSpaceWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
var transformSpaceToggle = new ViewportWidgetButton(string.Empty, window.Editor.Icons.World16, null, true)
{
Checked = TransformGizmo.ActiveTransformSpace == TransformGizmo.TransformSpace.World,
TooltipText = "Gizmo transform space (world or local)",
Parent = transformSpaceWidget
};
transformSpaceToggle.Toggled += OnTransformSpaceToggle;
transformSpaceWidget.Parent = this;
// Scale snapping widget
var scaleSnappingWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
var enableScaleSnapping = new ViewportWidgetButton(string.Empty, window.Editor.Icons.ScaleStep16, null, true)
{
Checked = TransformGizmo.ScaleSnapEnabled,
TooltipText = "Enable scale snapping",
Parent = scaleSnappingWidget
};
enableScaleSnapping.Toggled += OnScaleSnappingToggle;
var scaleSnappingCM = new ContextMenu();
_scaleSnapping = new ViewportWidgetButton(TransformGizmo.ScaleSnapValue.ToString(), SpriteHandle.Invalid, scaleSnappingCM);
_scaleSnapping.TooltipText = "Scale snapping values";
for (int i = 0; i < EditorViewportScaleSnapValues.Length; i++)
{
var v = EditorViewportScaleSnapValues[i];
var button = scaleSnappingCM.AddButton(v.ToString());
button.Tag = v;
}
scaleSnappingCM.ButtonClicked += OnWidgetScaleSnapClick;
scaleSnappingCM.VisibleChanged += OnWidgetScaleSnapShowHide;
_scaleSnapping.Parent = scaleSnappingWidget;
scaleSnappingWidget.Parent = this;
// Rotation snapping widget
var rotateSnappingWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
var enableRotateSnapping = new ViewportWidgetButton(string.Empty, window.Editor.Icons.RotateStep16, null, true)
{
Checked = TransformGizmo.RotationSnapEnabled,
TooltipText = "Enable rotation snapping",
Parent = rotateSnappingWidget
};
enableRotateSnapping.Toggled += OnRotateSnappingToggle;
var rotateSnappingCM = new ContextMenu();
_rotateSnapping = new ViewportWidgetButton(TransformGizmo.RotationSnapValue.ToString(), SpriteHandle.Invalid, rotateSnappingCM);
_rotateSnapping.TooltipText = "Rotation snapping values";
for (int i = 0; i < EditorViewportRotateSnapValues.Length; i++)
{
var v = EditorViewportRotateSnapValues[i];
var button = rotateSnappingCM.AddButton(v.ToString());
button.Tag = v;
}
rotateSnappingCM.ButtonClicked += OnWidgetRotateSnapClick;
rotateSnappingCM.VisibleChanged += OnWidgetRotateSnapShowHide;
_rotateSnapping.Parent = rotateSnappingWidget;
rotateSnappingWidget.Parent = this;
// Translation snapping widget
var translateSnappingWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
var enableTranslateSnapping = new ViewportWidgetButton(string.Empty, window.Editor.Icons.Grid16, null, true)
{
Checked = TransformGizmo.TranslationSnapEnable,
TooltipText = "Enable position snapping",
Parent = translateSnappingWidget
};
enableTranslateSnapping.Toggled += OnTranslateSnappingToggle;
var translateSnappingCM = new ContextMenu();
_translateSnappng = new ViewportWidgetButton(TransformGizmo.TranslationSnapValue.ToString(), SpriteHandle.Invalid, translateSnappingCM);
_translateSnappng.TooltipText = "Position snapping values";
for (int i = 0; i < EditorViewportTranslateSnapValues.Length; i++)
{
var v = EditorViewportTranslateSnapValues[i];
var button = translateSnappingCM.AddButton(v.ToString());
button.Tag = v;
}
translateSnappingCM.ButtonClicked += OnWidgetTranslateSnapClick;
translateSnappingCM.VisibleChanged += OnWidgetTranslateSnapShowHide;
_translateSnappng.Parent = translateSnappingWidget;
translateSnappingWidget.Parent = this;
// Gizmo mode widget
var gizmoMode = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
_gizmoModeTranslate = new ViewportWidgetButton(string.Empty, window.Editor.Icons.Translate16, null, true)
{
Tag = TransformGizmo.Mode.Translate,
TooltipText = "Translate gizmo mode",
Checked = true,
Parent = gizmoMode
};
_gizmoModeTranslate.Toggled += OnGizmoModeToggle;
_gizmoModeRotate = new ViewportWidgetButton(string.Empty, window.Editor.Icons.Rotate16, null, true)
{
Tag = TransformGizmo.Mode.Rotate,
TooltipText = "Rotate gizmo mode",
Parent = gizmoMode
};
_gizmoModeRotate.Toggled += OnGizmoModeToggle;
_gizmoModeScale = new ViewportWidgetButton(string.Empty, window.Editor.Icons.Scale16, null, true)
{
Tag = TransformGizmo.Mode.Scale,
TooltipText = "Scale gizmo mode",
Parent = gizmoMode
};
_gizmoModeScale.Toggled += OnGizmoModeToggle;
gizmoMode.Parent = this;
_dragHandlers.Add(_dragActorType);
_dragHandlers.Add(_dragAssets);
// Setup input actions
InputActions.Add(options => options.TranslateMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate);
InputActions.Add(options => options.RotateMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate);
InputActions.Add(options => options.ScaleMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale);
InputActions.Add(options => options.FocusSelection, ShowSelectedActors);
SetUpdate(ref _update, OnUpdate);
}
private void OnUpdate(float deltaTime)
{
for (int i = 0; i < Gizmos.Count; i++)
{
Gizmos[i].Update(deltaTime);
}
}
private void OnPostRender(GPUContext context, RenderContext renderContext)
{
if (renderContext.View.Mode != ViewMode.Default)
{
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 a intermediate buffers
EditorPrimitives.Render(context, ref renderContext, task.Output, task.Output);
}
}
/// <summary>
/// Moves the viewport to visualize selected actors.
/// </summary>
public void ShowSelectedActors()
{
((FPSCamera)ViewportCamera).ShowActors(TransformGizmo.SelectedParents);
}
/// <inheritdoc />
public GizmosCollection Gizmos { get; } = new GizmosCollection();
/// <inheritdoc />
public SceneRenderTask RenderTask => Task;
/// <inheritdoc />
public float ViewFarPlane => FarPlane;
/// <inheritdoc />
public bool IsLeftMouseButtonDown => _input.IsMouseLeftDown;
/// <inheritdoc />
public bool IsRightMouseButtonDown => _input.IsMouseRightDown;
/// <inheritdoc />
public bool IsAltKeyDown => _input.IsAltDown;
/// <inheritdoc />
public bool IsControlDown => _input.IsControlDown;
/// <inheritdoc />
public bool SnapToGround => false;
/// <inheritdoc />
public Vector2 MouseDelta => _mouseDeltaLeft * 1000;
/// <inheritdoc />
public bool UseSnapping => Root.GetKey(KeyboardKeys.Control);
/// <inheritdoc />
public bool UseDuplicate => Root.GetKey(KeyboardKeys.Shift);
/// <inheritdoc />
public Undo Undo { get; }
/// <inheritdoc />
protected override void AddUpdateCallbacks(RootControl root)
{
base.AddUpdateCallbacks(root);
root.UpdateCallbacksToAdd.Add(_update);
}
/// <inheritdoc />
protected override void RemoveUpdateCallbacks(RootControl root)
{
base.RemoveUpdateCallbacks(root);
root.UpdateCallbacksToRemove.Add(_update);
}
private void OnGizmoModeToggle(ViewportWidgetButton button)
{
TransformGizmo.ActiveMode = (TransformGizmo.Mode)(int)button.Tag;
}
private void OnTranslateSnappingToggle(ViewportWidgetButton button)
{
TransformGizmo.TranslationSnapEnable = !TransformGizmo.TranslationSnapEnable;
}
private void OnRotateSnappingToggle(ViewportWidgetButton button)
{
TransformGizmo.RotationSnapEnabled = !TransformGizmo.RotationSnapEnabled;
}
private void OnScaleSnappingToggle(ViewportWidgetButton button)
{
TransformGizmo.ScaleSnapEnabled = !TransformGizmo.ScaleSnapEnabled;
}
private void OnTransformSpaceToggle(ViewportWidgetButton button)
{
TransformGizmo.ToggleTransformSpace();
}
private void OnGizmoModeChanged()
{
// Update all viewport widgets status
var mode = TransformGizmo.ActiveMode;
_gizmoModeTranslate.Checked = mode == TransformGizmo.Mode.Translate;
_gizmoModeRotate.Checked = mode == TransformGizmo.Mode.Rotate;
_gizmoModeScale.Checked = mode == TransformGizmo.Mode.Scale;
}
private static readonly float[] EditorViewportScaleSnapValues =
{
0.05f,
0.1f,
0.25f,
0.5f,
1.0f,
2.0f,
4.0f,
6.0f,
8.0f,
};
private void OnWidgetScaleSnapClick(ContextMenuButton button)
{
var v = (float)button.Tag;
TransformGizmo.ScaleSnapValue = v;
_scaleSnapping.Text = v.ToString();
}
private void OnWidgetScaleSnapShowHide(Control control)
{
if (control.Visible == false)
return;
var ccm = (ContextMenu)control;
foreach (var e in ccm.Items)
{
if (e is ContextMenuButton b)
{
var v = (float)b.Tag;
b.Icon = Mathf.Abs(TransformGizmo.ScaleSnapValue - v) < 0.001f
? Style.Current.CheckBoxTick
: SpriteHandle.Invalid;
}
}
}
private static readonly float[] EditorViewportRotateSnapValues =
{
1.0f,
5.0f,
10.0f,
15.0f,
30.0f,
45.0f,
60.0f,
90.0f,
};
private void OnWidgetRotateSnapClick(ContextMenuButton button)
{
var v = (float)button.Tag;
TransformGizmo.RotationSnapValue = v;
_rotateSnapping.Text = v.ToString();
}
private void OnWidgetRotateSnapShowHide(Control control)
{
if (control.Visible == false)
return;
var ccm = (ContextMenu)control;
foreach (var e in ccm.Items)
{
if (e is ContextMenuButton b)
{
var v = (float)b.Tag;
b.Icon = Mathf.Abs(TransformGizmo.RotationSnapValue - v) < 0.001f
? Style.Current.CheckBoxTick
: SpriteHandle.Invalid;
}
}
}
private static readonly float[] EditorViewportTranslateSnapValues =
{
0.1f,
0.5f,
1.0f,
5.0f,
10.0f,
100.0f,
1000.0f,
};
private void OnWidgetTranslateSnapClick(ContextMenuButton button)
{
var v = (float)button.Tag;
TransformGizmo.TranslationSnapValue = v;
_translateSnappng.Text = v.ToString();
}
private void OnWidgetTranslateSnapShowHide(Control control)
{
if (control.Visible == false)
return;
var ccm = (ContextMenu)control;
foreach (var e in ccm.Items)
{
if (e is ContextMenuButton b)
{
var v = (float)b.Tag;
b.Icon = Mathf.Abs(TransformGizmo.TranslationSnapValue - v) < 0.001f
? Style.Current.CheckBoxTick
: SpriteHandle.Invalid;
}
}
}
private void OnSelectionChanged()
{
Gizmos.ForEach(x => x.OnSelectionChanged(_window.Selection));
}
/// <summary>
/// Applies the transform to the collection of scene graph nodes.
/// </summary>
/// <param name="selection">The selection.</param>
/// <param name="translationDelta">The translation delta.</param>
/// <param name="rotationDelta">The rotation delta.</param>
/// <param name="scaleDelta">The scale delta.</param>
public void ApplyTransform(List<SceneGraphNode> selection, ref Vector3 translationDelta, ref Quaternion rotationDelta, ref Vector3 scaleDelta)
{
bool applyRotation = !rotationDelta.IsIdentity;
bool useObjCenter = TransformGizmo.ActivePivot == TransformGizmo.PivotType.ObjectCenter;
Vector3 gizmoPosition = TransformGizmo.Position;
// Transform selected objects
for (int i = 0; i < selection.Count; i++)
{
var obj = selection[i];
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
const float scaleLimit = 99_999_999.0f;
trans.Scale = Vector3.Clamp(trans.Scale + scaleDelta, new Vector3(-scaleLimit), new Vector3(scaleLimit));
// Apply translation
trans.Translation += translationDelta;
obj.Transform = trans;
}
}
/// <inheritdoc />
protected override void OnLeftMouseButtonUp()
{
// Skip if was controlling mouse or mouse is not over the area
if (_prevInput.IsControllingMouse || !Bounds.Contains(ref _viewMousePos))
return;
if (TransformGizmo.IsActive)
{
// Ensure player is not moving objects
if (TransformGizmo.ActiveAxis != TransformGizmo.Axis.None)
return;
}
else
{
// For now just pick objects in transform gizmo mode
return;
}
// Get mouse ray and try to hit any object
var ray = MouseRay;
var view = new Ray(ViewPosition, ViewDirection);
var hit = _window.Graph.Root.RayCast(ref ray, ref view, out _, SceneGraphNode.RayCastData.FlagTypes.SkipColliders);
// Update selection
if (hit != null)
{
// For child actor nodes (mesh, link or sth) we need to select it's owning actor node first or any other child node (but not a child actor)
if (hit is ActorChildNode actorChildNode)
{
var parentNode = actorChildNode.ParentNode;
bool canChildBeSelected = _window.Selection.Contains(parentNode);
if (!canChildBeSelected)
{
for (int i = 0; i < parentNode.ChildNodes.Count; i++)
{
if (_window.Selection.Contains(parentNode.ChildNodes[i]))
{
canChildBeSelected = true;
break;
}
}
}
if (!canChildBeSelected)
{
// Select parent
hit = parentNode;
}
}
bool addRemove = Root.GetKey(KeyboardKeys.Control);
bool isSelected = _window.Selection.Contains(hit);
if (addRemove)
{
if (isSelected)
_window.Deselect(hit);
else
_window.Select(hit, true);
}
else
{
_window.Select(hit);
}
}
else
{
_window.Deselect();
}
// Keep focus
Focus();
base.OnLeftMouseButtonUp();
}
/// <inheritdoc />
public override DragDropEffect OnDragEnter(ref Vector2 location, DragData data)
{
var result = base.OnDragEnter(ref location, data);
if (result != DragDropEffect.None)
return result;
return _dragHandlers.OnDragEnter(data);
}
private static bool ValidateDragItem(ContentItem contentItem)
{
if (contentItem is AssetItem assetItem)
{
if (assetItem.IsOfType<ParticleSystem>())
return true;
if (assetItem.IsOfType<MaterialBase>())
return true;
if (assetItem.IsOfType<ModelBase>())
return true;
if (assetItem.IsOfType<AudioClip>())
return true;
if (assetItem.IsOfType<Prefab>())
return true;
}
return false;
}
private static bool ValidateDragActorType(ScriptType actorType)
{
return true;
}
/// <inheritdoc />
public override DragDropEffect OnDragMove(ref Vector2 location, DragData data)
{
var result = base.OnDragMove(ref location, data);
if (result != DragDropEffect.None)
return result;
return _dragHandlers.Effect;
}
/// <inheritdoc />
public override void OnDragLeave()
{
_dragHandlers.OnDragLeave();
base.OnDragLeave();
}
private Vector3 PostProcessSpawnedActorLocation(Actor actor, ref Vector3 hitLocation)
{
BoundingBox box;
Editor.GetActorEditorBox(actor, out box);
// Place the object
var location = hitLocation - (box.Size.Length * 0.5f) * ViewDirection;
// Apply grid snapping if enabled
if (UseSnapping || TransformGizmo.TranslationSnapEnable)
{
float snapValue = TransformGizmo.TranslationSnapValue;
location = new Vector3(
(int)(location.X / snapValue) * snapValue,
(int)(location.Y / snapValue) * snapValue,
(int)(location.Z / snapValue) * snapValue);
}
return location;
}
private void Spawn(AssetItem item, SceneGraphNode hit, ref Vector2 location, ref Vector3 hitLocation)
{
if (item is BinaryAssetItem binaryAssetItem)
{
if (binaryAssetItem.Type == typeof(ParticleSystem))
{
var particleSystem = FlaxEngine.Content.LoadAsync<ParticleSystem>(item.ID);
var actor = new ParticleEffect();
actor.Name = item.ShortName;
actor.ParticleSystem = particleSystem;
actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation);
Spawn(actor);
return;
}
if (typeof(MaterialBase).IsAssignableFrom(binaryAssetItem.Type))
{
if (hit is StaticModelNode staticModelNode)
{
var staticModel = (StaticModel)staticModelNode.Actor;
var ray = ConvertMouseToRay(ref location);
if (staticModel.IntersectsEntry(ref ray, out _, out _, out var entryIndex))
{
var material = FlaxEngine.Content.LoadAsync<MaterialBase>(item.ID);
using (new UndoBlock(Undo, staticModel, "Change material"))
staticModel.SetMaterial(entryIndex, material);
}
}
return;
}
if (typeof(SkinnedModel).IsAssignableFrom(binaryAssetItem.Type))
{
var model = FlaxEngine.Content.LoadAsync<SkinnedModel>(item.ID);
var actor = new AnimatedModel();
actor.Name = item.ShortName;
actor.SkinnedModel = model;
actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation);
Spawn(actor);
return;
}
if (typeof(Model).IsAssignableFrom(binaryAssetItem.Type))
{
var model = FlaxEngine.Content.LoadAsync<Model>(item.ID);
var actor = new StaticModel();
actor.Name = item.ShortName;
actor.Model = model;
actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation);
Spawn(actor);
return;
}
if (typeof(AudioClip).IsAssignableFrom(binaryAssetItem.Type))
{
var clip = FlaxEngine.Content.LoadAsync<AudioClip>(item.ID);
var actor = new AudioSource();
actor.Name = item.ShortName;
actor.Clip = clip;
actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation);
Spawn(actor);
return;
}
if (typeof(Prefab).IsAssignableFrom(binaryAssetItem.Type))
{
var prefab = FlaxEngine.Content.LoadAsync<Prefab>(item.ID);
var actor = PrefabManager.SpawnPrefab(prefab, null);
actor.Name = item.ShortName;
actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation);
Spawn(actor);
return;
}
}
}
private void Spawn(Actor actor)
{
_window.Spawn(actor);
}
private void Spawn(ScriptType item, SceneGraphNode hit, ref Vector3 hitLocation)
{
var actor = item.CreateInstance() as Actor;
if (actor == null)
{
Editor.LogWarning("Failed to spawn actor of type " + item.TypeName);
return;
}
actor.Name = item.Name;
actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation);
Spawn(actor);
}
/// <inheritdoc />
public override DragDropEffect OnDragDrop(ref Vector2 location, DragData data)
{
var result = base.OnDragDrop(ref location, data);
if (result != DragDropEffect.None)
return result;
// Check if drag sth
Vector3 hitLocation = ViewPosition;
SceneGraphNode hit = null;
if (_dragHandlers.HasValidDrag)
{
// Get mouse ray and try to hit any object
var ray = ConvertMouseToRay(ref location);
var view = new Ray(ViewPosition, ViewDirection);
hit = _window.Graph.Root.RayCast(ref ray, ref view, out var closest, SceneGraphNode.RayCastData.FlagTypes.SkipColliders);
if (hit != null)
{
// Use hit location
hitLocation = ray.Position + ray.Direction * closest;
}
else
{
// Use area in front of the viewport
hitLocation = ViewPosition + ViewDirection * 100;
}
}
// Drag assets
if (_dragAssets.HasValidDrag)
{
result = _dragAssets.Effect;
// Process items
for (int i = 0; i < _dragAssets.Objects.Count; i++)
{
var item = _dragAssets.Objects[i];
Spawn(item, hit, ref location, ref hitLocation);
}
}
// Drag actor type
else if (_dragActorType.HasValidDrag)
{
result = _dragActorType.Effect;
// Process items
for (int i = 0; i < _dragActorType.Objects.Count; i++)
{
var item = _dragActorType.Objects[i];
Spawn(item, hit, ref hitLocation);
}
}
return result;
}
/// <inheritdoc />
public override void OnDestroy()
{
FlaxEngine.Object.Destroy(ref SelectionOutline);
FlaxEngine.Object.Destroy(ref EditorPrimitives);
base.OnDestroy();
}
/// <inheritdoc />
public void DrawEditorPrimitives(GPUContext context, ref RenderContext renderContext, GPUTexture target, GPUTexture targetDepth)
{
}
}
}

View File

@@ -0,0 +1,258 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using FlaxEngine;
using FlaxEditor.GUI.Input;
using Object = FlaxEngine.Object;
namespace FlaxEditor.Viewport.Previews
{
/// <summary>
/// Animated model asset preview editor viewport.
/// </summary>
/// <seealso cref="AssetPreview" />
public class AnimatedModelPreview : AssetPreview
{
private AnimatedModel _previewModel;
private StaticModel _previewNodesActor;
private Model _previewNodesModel;
private int _previewNodesCounter;
private List<Vector3> _previewNodesVB;
private List<int> _previewNodesIB;
/// <summary>
/// Gets or sets the skinned model asset to preview.
/// </summary>
public SkinnedModel SkinnedModel
{
get => _previewModel.SkinnedModel;
set => _previewModel.SkinnedModel = value;
}
/// <summary>
/// Gets the skinned model actor used to preview selected asset.
/// </summary>
public AnimatedModel PreviewActor => _previewModel;
/// <summary>
/// Gets or sets a value indicating whether play the animation in editor.
/// </summary>
public bool PlayAnimation { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether show animated model skeleton nodes debug view.
/// </summary>
public bool ShowNodes { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether scale the model to the normalized bounds.
/// </summary>
public bool ScaleToFit { get; set; } = true;
/// <summary>
/// Gets or sets the custom mask for the skeleton nodes. Nodes missing from this list will be skipped during rendering. Works only if <see cref="ShowNodes"/> is set to true and the array matches the attached <see cref="SkinnedModel"/> nodes hierarchy.
/// </summary>
public bool[] NodesMask { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="AnimatedModelPreview"/> class.
/// </summary>
/// <param name="useWidgets">if set to <c>true</c> use widgets.</param>
public AnimatedModelPreview(bool useWidgets)
: base(useWidgets)
{
Task.Begin += OnBegin;
// Setup preview scene
_previewModel = new AnimatedModel();
_previewModel.UseTimeScale = false;
_previewModel.UpdateWhenOffscreen = true;
//_previewModel.BoundsScale = 1000.0f;
_previewModel.UpdateMode = AnimatedModel.AnimationUpdateMode.Manual;
_previewNodesModel = FlaxEngine.Content.CreateVirtualAsset<Model>();
_previewNodesModel.SetupLODs(new[] { 1 });
_previewNodesActor = new StaticModel();
_previewNodesActor.Model = _previewNodesModel;
_previewNodesActor.SetMaterial(0, FlaxEngine.Content.LoadAsyncInternal<MaterialBase>(EditorAssets.WiresDebugMaterial));
// Link actors for rendering
Task.AddCustomActor(_previewModel);
Task.AddCustomActor(_previewNodesActor);
if (useWidgets)
{
// Preview LOD
{
var previewLOD = ViewWidgetButtonMenu.AddButton("Preview LOD");
var previewLODValue = new IntValueBox(-1, 75, 2, 50.0f, -1, 10, 0.02f);
previewLODValue.Parent = previewLOD;
previewLODValue.ValueChanged += () => _previewModel.ForcedLOD = previewLODValue.Value;
ViewWidgetButtonMenu.VisibleChanged += control => previewLODValue.Value = _previewModel.ForcedLOD;
}
}
}
private void OnBegin(RenderTask task, GPUContext context)
{
if (!ScaleToFit)
{
_previewModel.Scale = Vector3.One;
_previewModel.Position = Vector3.Zero;
return;
}
// Update preview model scale to fit the preview
var skinnedModel = SkinnedModel;
if (skinnedModel && skinnedModel.IsLoaded)
{
float targetSize = 50.0f;
BoundingBox box = skinnedModel.GetBox();
float maxSize = Mathf.Max(0.001f, box.Size.MaxValue);
float scale = targetSize / maxSize;
_previewModel.Scale = new Vector3(scale);
_previewModel.Position = box.Center * (-0.5f * scale) + new Vector3(0, -10, 0);
}
}
/// <inheritdoc />
public override void Update(float deltaTime)
{
base.Update(deltaTime);
// Manually update animation
if (PlayAnimation)
{
_previewModel.UpdateAnimation();
}
// Update the nodes debug (once every few frames)
_previewNodesActor.Transform = _previewModel.Transform;
var updateNodesCount = PlayAnimation || _previewNodesVB?.Count == 0 ? 1 : 10;
_previewNodesActor.IsActive = ShowNodes;
if (_previewNodesCounter++ % updateNodesCount == 0 && ShowNodes)
{
_previewModel.GetCurrentPose(out var pose);
var nodes = _previewModel.SkinnedModel?.Nodes;
if (pose == null || pose.Length == 0 || nodes == null)
{
_previewNodesActor.IsActive = false;
}
else
{
if (_previewNodesVB == null)
_previewNodesVB = new List<Vector3>(1024 * 2);
else
_previewNodesVB.Clear();
if (_previewNodesIB == null)
_previewNodesIB = new List<int>(1024 * 3);
else
_previewNodesIB.Clear();
// Draw bounding box at the node locations
var nodesMask = NodesMask != null && NodesMask.Length == nodes.Length ? NodesMask : null;
var localBox = new OrientedBoundingBox(new Vector3(-1.0f), new Vector3(1.0f));
for (int nodeIndex = 0; nodeIndex < pose.Length; nodeIndex++)
{
if (nodesMask != null && !nodesMask[nodeIndex])
continue;
var transform = pose[nodeIndex];
transform.Decompose(out var scale, out Matrix _, out _);
transform = Matrix.Invert(Matrix.Scaling(scale)) * transform;
// Some inlined code to improve performance
var box = localBox * transform;
//
var iStart = _previewNodesVB.Count;
box.GetCorners(_previewNodesVB);
//
_previewNodesIB.Add(iStart + 0);
_previewNodesIB.Add(iStart + 1);
_previewNodesIB.Add(iStart + 0);
//
_previewNodesIB.Add(iStart + 0);
_previewNodesIB.Add(iStart + 4);
_previewNodesIB.Add(iStart + 0);
//
_previewNodesIB.Add(iStart + 1);
_previewNodesIB.Add(iStart + 2);
_previewNodesIB.Add(iStart + 1);
//
_previewNodesIB.Add(iStart + 1);
_previewNodesIB.Add(iStart + 5);
_previewNodesIB.Add(iStart + 1);
//
_previewNodesIB.Add(iStart + 2);
_previewNodesIB.Add(iStart + 3);
_previewNodesIB.Add(iStart + 2);
//
_previewNodesIB.Add(iStart + 2);
_previewNodesIB.Add(iStart + 6);
_previewNodesIB.Add(iStart + 2);
//
_previewNodesIB.Add(iStart + 3);
_previewNodesIB.Add(iStart + 7);
_previewNodesIB.Add(iStart + 3);
//
_previewNodesIB.Add(iStart + 4);
_previewNodesIB.Add(iStart + 5);
_previewNodesIB.Add(iStart + 4);
//
_previewNodesIB.Add(iStart + 4);
_previewNodesIB.Add(iStart + 7);
_previewNodesIB.Add(iStart + 4);
//
_previewNodesIB.Add(iStart + 5);
_previewNodesIB.Add(iStart + 6);
_previewNodesIB.Add(iStart + 5);
//
_previewNodesIB.Add(iStart + 6);
_previewNodesIB.Add(iStart + 7);
_previewNodesIB.Add(iStart + 6);
//
}
// Nodes connections
for (int nodeIndex = 0; nodeIndex < nodes.Length; nodeIndex++)
{
int parentIndex = nodes[nodeIndex].ParentIndex;
if (parentIndex != -1)
{
if (nodesMask != null && (!nodesMask[nodeIndex] || !nodesMask[parentIndex]))
continue;
var parentPos = pose[parentIndex].TranslationVector;
var bonePos = pose[nodeIndex].TranslationVector;
var iStart = _previewNodesVB.Count;
_previewNodesVB.Add(parentPos);
_previewNodesVB.Add(bonePos);
_previewNodesIB.Add(iStart + 0);
_previewNodesIB.Add(iStart + 1);
_previewNodesIB.Add(iStart + 0);
}
}
if (_previewNodesIB.Count > 0)
_previewNodesModel.LODs[0].Meshes[0].UpdateMesh(_previewNodesVB, _previewNodesIB);
else
_previewNodesActor.IsActive = false;
}
}
}
/// <inheritdoc />
public override void OnDestroy()
{
// Ensure to cleanup created actor objects
_previewNodesActor.Model = null;
Object.Destroy(ref _previewModel);
Object.Destroy(ref _previewNodesActor);
Object.Destroy(ref _previewNodesModel);
NodesMask = null;
base.OnDestroy();
}
}
}

View File

@@ -0,0 +1,144 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.Viewport.Cameras;
using FlaxEngine;
using Object = FlaxEngine.Object;
namespace FlaxEditor.Viewport.Previews
{
/// <summary>
/// Generic asset preview editor viewport base class.
/// </summary>
/// <seealso cref="FlaxEditor.Viewport.EditorViewport" />
public abstract class AssetPreview : EditorViewport
{
private ContextMenuButton _showDefaultSceneButton;
/// <summary>
/// The preview light. Allows to modify rendering settings.
/// </summary>
public DirectionalLight PreviewLight;
/// <summary>
/// The env probe. Allows to modify rendering settings.
/// </summary>
public EnvironmentProbe EnvProbe;
/// <summary>
/// The sky. Allows to modify rendering settings.
/// </summary>
public Sky Sky;
/// <summary>
/// The sky light. Allows to modify rendering settings.
/// </summary>
public SkyLight SkyLight;
/// <summary>
/// Gets the post fx volume. Allows to modify rendering settings.
/// </summary>
public PostFxVolume PostFxVolume;
/// <summary>
/// Gets or sets a value indicating whether show default scene actors (sky, env probe, skylight, directional light, etc.).
/// </summary>
public bool ShowDefaultSceneActors
{
get => PreviewLight.IsActive;
set
{
if (ShowDefaultSceneActors != value)
{
PreviewLight.IsActive = value;
EnvProbe.IsActive = value;
Sky.IsActive = value;
SkyLight.IsActive = value;
if (_showDefaultSceneButton != null)
_showDefaultSceneButton.Checked = value;
}
}
}
/// <summary>
/// Initializes a new instance of the <see cref="AssetPreview"/> class.
/// </summary>
/// <param name="useWidgets">If set to <c>true</c> use widgets for viewport, otherwise hide them.</param>
/// <param name="orbitRadius">The initial orbit radius.</param>
protected AssetPreview(bool useWidgets, float orbitRadius = 50.0f)
: this(useWidgets, new ArcBallCamera(Vector3.Zero, orbitRadius))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AssetPreview"/> class.
/// </summary>
/// <param name="useWidgets">If set to <c>true</c> use widgets for viewport, otherwise hide them.</param>
/// <param name="camera">The camera controller.</param>
protected AssetPreview(bool useWidgets, ViewportCamera camera)
: base(Object.New<SceneRenderTask>(), camera, useWidgets)
{
Task.ViewFlags = ViewFlags.DefaultAssetPreview;
Task.AllowGlobalCustomPostFx = false;
var orbitRadius = 200.0f;
if (camera is ArcBallCamera arcBallCamera)
orbitRadius = arcBallCamera.OrbitRadius;
camera.SerArcBallView(new Quaternion(-0.08f, -0.92f, 0.31f, -0.23f), Vector3.Zero, orbitRadius);
if (useWidgets)
{
// Show Default Scene
_showDefaultSceneButton = ViewWidgetShowMenu.AddButton("Default Scene", () => ShowDefaultSceneActors = !ShowDefaultSceneActors);
_showDefaultSceneButton.Checked = true;
}
// Setup preview scene
PreviewLight = new DirectionalLight();
PreviewLight.Brightness = 8.0f;
PreviewLight.ShadowsMode = ShadowsCastingMode.None;
PreviewLight.Orientation = Quaternion.Euler(new Vector3(52.1477f, -109.109f, -111.739f));
//
EnvProbe = new EnvironmentProbe();
EnvProbe.AutoUpdate = false;
EnvProbe.CustomProbe = FlaxEngine.Content.LoadAsyncInternal<CubeTexture>(EditorAssets.DefaultSkyCubeTexture);
//
Sky = new Sky();
Sky.SunLight = PreviewLight;
Sky.SunPower = 9.0f;
//
SkyLight = new SkyLight();
SkyLight.Mode = SkyLight.Modes.CustomTexture;
SkyLight.Brightness = 2.1f;
SkyLight.CustomTexture = EnvProbe.CustomProbe;
//
PostFxVolume = new PostFxVolume();
PostFxVolume.IsBounded = false;
// Link actors for rendering
Task.ActorsSource = ActorsSources.CustomActors;
Task.AddCustomActor(PreviewLight);
Task.AddCustomActor(EnvProbe);
Task.AddCustomActor(Sky);
Task.AddCustomActor(SkyLight);
Task.AddCustomActor(PostFxVolume);
}
/// <inheritdoc />
public override bool HasLoadedAssets => base.HasLoadedAssets && Sky.HasContentLoaded && EnvProbe.Probe.IsLoaded && PostFxVolume.HasContentLoaded;
/// <inheritdoc />
public override void OnDestroy()
{
// Ensure to cleanup created actor objects
Object.Destroy(ref PreviewLight);
Object.Destroy(ref EnvProbe);
Object.Destroy(ref Sky);
Object.Destroy(ref SkyLight);
Object.Destroy(ref PostFxVolume);
base.OnDestroy();
}
}
}

View File

@@ -0,0 +1,236 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Threading.Tasks;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Viewport.Previews
{
/// <summary>
/// Audio clip PCM data editor preview.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
public class AudioClipPreview : ContainerControl
{
/// <summary>
/// The audio clip drawing modes.
/// </summary>
public enum DrawModes
{
/// <summary>
/// Fills the whole control area with the full clip duration.
/// </summary>
Fill,
/// <summary>
/// Draws single audio clip. Uses the view scale parameter.
/// </summary>
Single,
/// <summary>
/// Draws the looped audio clip. Uses the view scale parameter.
/// </summary>
Looped,
};
private readonly object _locker = new object();
private AudioClip _asset;
private float[] _pcmData;
private AudioDataInfo _pcmInfo;
/// <summary>
/// Gets or sets the clip to preview.
/// </summary>
public AudioClip Asset
{
get => _asset;
set
{
lock (_locker)
{
if (_asset == value)
return;
_asset = value;
_pcmData = null;
if (_asset)
{
// Use async task to gather PCM data (engine loads it from the asset)
Task.Run(DownloadData);
}
}
}
}
/// <summary>
/// Gets a value indicating whether audio data has been fetched from the asset (done as an async task). It is required to be valid in order to draw the audio buffer preview.
/// </summary>
public bool HasData
{
get
{
lock (_locker)
{
return _pcmData != null;
}
}
}
/// <summary>
/// The draw mode.
/// </summary>
public DrawModes DrawMode = DrawModes.Fill;
/// <summary>
/// The view scale parameter. Increase it to zoom in the audio. Usage depends on the current <see cref="DrawMode"/>.
/// </summary>
public float ViewScale = 1.0f;
/// <summary>
/// The color of the audio PCM data spectrum.
/// </summary>
public Color Color = Color.White;
/// <summary>
/// The audio units per second (on time axis).
/// </summary>
public static readonly float UnitsPerSecond = 100.0f;
/// <inheritdoc />
public override void Draw()
{
base.Draw();
lock (_locker)
{
var info = _pcmInfo;
if (_asset == null || _pcmData == null || info.NumSamples == 0)
return;
var height = Height;
var width = Width;
var samplesPerChannel = info.NumSamples / info.NumChannels;
var length = (float)samplesPerChannel / info.SampleRate;
var color = Color;
if (!EnabledInHierarchy)
color *= 0.4f;
// Compute the scaled y-value used to render the channel data
float sampleYScale = height / info.NumChannels;
// Compute amount of samples that are contained in the view
float unitsPerSecond = UnitsPerSecond * ViewScale;
float clipDefaultWidth = length * unitsPerSecond;
float clipsInView = width / clipDefaultWidth;
float clipWidth;
uint samplesPerIndex;
switch (DrawMode)
{
case DrawModes.Fill:
clipsInView = 1.0f;
clipWidth = width;
samplesPerIndex = (uint)(samplesPerChannel / width);
break;
case DrawModes.Single:
clipsInView = Mathf.Min(clipsInView, 1.0f);
clipWidth = clipDefaultWidth;
samplesPerIndex = (uint)(info.SampleRate / unitsPerSecond) * info.NumChannels;
break;
case DrawModes.Looped:
clipWidth = width / clipsInView;
samplesPerIndex = (uint)(info.SampleRate / unitsPerSecond) * info.NumChannels;
break;
default: throw new ArgumentOutOfRangeException();
}
const uint maxSamplesPerIndex = 64;
uint samplesPerIndexDiff = Math.Max(1, samplesPerIndex / Math.Min(samplesPerIndex, maxSamplesPerIndex));
// Render each clip separately
for (uint clipIndex = 0; clipIndex < Mathf.CeilToInt(clipsInView); clipIndex++)
{
var clipX = clipWidth * clipIndex;
var clipRight = Mathf.Min(width, clipX + clipWidth);
// Render each channel separately so outer loop is the sound wave channel index
for (uint channelIndex = 0; channelIndex < info.NumChannels; channelIndex++)
{
uint currentSample = channelIndex;
float yCenter = Y + ((2 * channelIndex) + 1) * height / (2.0f * info.NumChannels);
// Loop through each pixel (in x direction)
for (float pixelX = clipX; pixelX < clipRight; pixelX++)
{
// Reset the sample sum and num samples in pixel for each pixel
float samplesSum = 0;
int samplesInPixel = 0;
// Loop through all pixels in this x-frame and sum all audio data
uint samplesEnd = Math.Min(currentSample + samplesPerIndex, info.NumSamples);
for (uint sampleIndex = currentSample; sampleIndex < samplesEnd; sampleIndex += samplesPerIndexDiff)
{
samplesSum += Mathf.Abs(_pcmData[sampleIndex]);
samplesInPixel++;
}
currentSample = samplesEnd;
if (samplesInPixel > 0)
{
float averageSampleValue = samplesSum / samplesInPixel;
float averageSampleValueScaled = averageSampleValue * sampleYScale;
// Don't try to draw anything if the audio data was too quiet
if (averageSampleValueScaled > 0.1f)
{
// Draw vertical line mirrored around x-axis for channel equal to average sample value height
Render2D.DrawLine(new Vector2(pixelX, yCenter - averageSampleValueScaled), new Vector2(pixelX, yCenter + averageSampleValueScaled), color);
}
}
}
}
}
}
}
/// <summary>
/// Downloads the audio clip raw PCM data. Use it from async thread to prevent blocking,
/// </summary>
private void DownloadData()
{
var asset = _asset;
if (!asset)
{
Editor.LogWarning("Failed to get audio clip PCM data. Missing asset.");
return;
}
float[] data;
AudioDataInfo dataInfo;
try
{
asset.ExtractDataFloat(out data, out dataInfo);
}
catch (Exception ex)
{
Editor.LogWarning("Failed to get audio clip PCM data. " + ex.Message);
Editor.LogWarning(ex);
return;
}
if (data.Length != dataInfo.NumSamples)
{
Editor.LogWarning("Failed to get audio clip PCM data. Invalid samples count. Returned buffer has other size.");
}
lock (_locker)
{
// If asset has been modified during data fetching, ignore it
if (_asset == asset)
{
_pcmData = data;
_pcmInfo = dataInfo;
}
}
}
}
}

View File

@@ -0,0 +1,251 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.Viewport.Widgets;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Viewport.Previews
{
/// <summary>
/// Cube Texture asset preview editor viewport.
/// </summary>
/// <seealso cref="FlaxEditor.Viewport.Previews.MaterialPreview" />
public class CubeTexturePreview : MaterialPreview
{
private ChannelFlags _channelFlags = ChannelFlags.All;
private bool _usePointSampler = false;
private float _mipLevel = -1;
private ContextMenu _mipWidgetMenu;
private ContextMenuButton _filterWidgetPointButton;
private ContextMenuButton _filterWidgetLinearButton;
/// <summary>
/// The preview material instance used to draw texture.
/// </summary>
protected MaterialInstance _previewMaterial;
/// <summary>
/// Sets the cube texture to preview.
/// </summary>
/// <value>
/// The cube texture.
/// </value>
public CubeTexture CubeTexture
{
set
{
// Prepare material and assign texture asset as a parameter
if (_previewMaterial == null || _previewMaterial.WaitForLoaded())
{
// Error
Editor.LogError("Cannot load preview material.");
return;
}
var baseMaterial = _previewMaterial.BaseMaterial;
if (baseMaterial == null || baseMaterial.WaitForLoaded())
{
// Error
Editor.LogError("Cannot load base material for preview material.");
return;
}
_previewMaterial.SetParameterValue("CubeTexture", value);
}
}
/// <summary>
/// Gets or sets the view channels to show.
/// </summary>
public ChannelFlags ViewChannels
{
get => _channelFlags;
set
{
if (_channelFlags != value)
{
_channelFlags = value;
UpdateMask();
}
}
}
/// <summary>
/// Gets or sets a value indicating whether use point sampler when drawing the texture. The default value is false.
/// </summary>
public bool UsePointSampler
{
get => _usePointSampler;
set
{
if (_usePointSampler != value)
{
_usePointSampler = value;
_previewMaterial.SetParameterValue("PointSampler", value);
}
}
}
/// <summary>
/// Gets or sets the mip level to show. The default value is -1.
/// </summary>
public float MipLevel
{
get => _mipLevel;
set
{
if (!Mathf.NearEqual(_mipLevel, value))
{
_mipLevel = value;
_previewMaterial.SetParameterValue("Mip", value);
}
}
}
/// <inheritdoc />
public CubeTexturePreview(bool useWidgets)
: base(useWidgets)
{
// Create virtual material material
_previewMaterial = FlaxEngine.Content.CreateVirtualAsset<MaterialInstance>();
if (_previewMaterial != null)
_previewMaterial.BaseMaterial = FlaxEngine.Content.LoadAsyncInternal<Material>("Editor/CubeTexturePreviewMaterial");
// Add widgets
if (useWidgets)
{
// Channels widget
var channelsWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperLeft);
//
var channelR = new ViewportWidgetButton("R", SpriteHandle.Invalid, null, true)
{
Checked = true,
TooltipText = "Show/hide texture red channel",
Parent = channelsWidget
};
channelR.Toggled += button => ViewChannels = button.Checked ? ViewChannels | ChannelFlags.Red : (ViewChannels & ~ChannelFlags.Red);
var channelG = new ViewportWidgetButton("G", SpriteHandle.Invalid, null, true)
{
Checked = true,
TooltipText = "Show/hide texture green channel",
Parent = channelsWidget
};
channelG.Toggled += button => ViewChannels = button.Checked ? ViewChannels | ChannelFlags.Green : (ViewChannels & ~ChannelFlags.Green);
var channelB = new ViewportWidgetButton("B", SpriteHandle.Invalid, null, true)
{
Checked = true,
TooltipText = "Show/hide texture blue channel",
Parent = channelsWidget
};
channelB.Toggled += button => ViewChannels = button.Checked ? ViewChannels | ChannelFlags.Blue : (ViewChannels & ~ChannelFlags.Blue);
var channelA = new ViewportWidgetButton("A", SpriteHandle.Invalid, null, true)
{
Checked = true,
TooltipText = "Show/hide texture alpha channel",
Parent = channelsWidget
};
channelA.Toggled += button => ViewChannels = button.Checked ? ViewChannels | ChannelFlags.Alpha : (ViewChannels & ~ChannelFlags.Alpha);
//
channelsWidget.Parent = this;
// Mip widget
var mipWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperLeft);
_mipWidgetMenu = new ContextMenu();
_mipWidgetMenu.VisibleChanged += OnMipWidgetMenuOnVisibleChanged;
var mipWidgetButton = new ViewportWidgetButton("Mip", SpriteHandle.Invalid, _mipWidgetMenu)
{
TooltipText = "The mip level to show. The default is -1.",
Parent = mipWidget
};
//
mipWidget.Parent = this;
// Filter widget
var filterWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperLeft);
var filterWidgetMenu = new ContextMenu();
filterWidgetMenu.VisibleChanged += OnFilterWidgetMenuVisibleChanged;
_filterWidgetPointButton = filterWidgetMenu.AddButton("Point", () => UsePointSampler = true);
_filterWidgetLinearButton = filterWidgetMenu.AddButton("Linear", () => UsePointSampler = false);
var filterWidgetButton = new ViewportWidgetButton("Filter", SpriteHandle.Invalid, filterWidgetMenu)
{
TooltipText = "The texture preview filtering mode. The default is Linear.",
Parent = filterWidget
};
//
filterWidget.Parent = this;
}
// Link it
Material = _previewMaterial;
}
private void OnFilterWidgetMenuVisibleChanged(Control control)
{
if (!control.Visible)
return;
_filterWidgetPointButton.Checked = UsePointSampler;
_filterWidgetLinearButton.Checked = !UsePointSampler;
}
private void OnMipWidgetMenuOnVisibleChanged(Control control)
{
if (!control.Visible)
return;
var textureObj = _previewMaterial.GetParameterValue("CubeTexture");
if (textureObj is TextureBase texture && !texture.WaitForLoaded())
{
_mipWidgetMenu.ItemsContainer.DisposeChildren();
var mipLevels = texture.MipLevels;
for (int i = -1; i < mipLevels; i++)
{
var button = _mipWidgetMenu.AddButton(i.ToString(), OnMipWidgetClicked);
button.Tag = i;
button.Checked = Mathf.Abs(MipLevel - i) < 0.9f;
}
}
}
private void OnMipWidgetClicked(ContextMenuButton button)
{
MipLevel = (int)button.Tag;
}
private void UpdateMask()
{
Vector4 mask = Vector4.One;
if ((_channelFlags & ChannelFlags.Red) == 0)
mask.X = 0;
if ((_channelFlags & ChannelFlags.Green) == 0)
mask.Y = 0;
if ((_channelFlags & ChannelFlags.Blue) == 0)
mask.Z = 0;
if ((_channelFlags & ChannelFlags.Alpha) == 0)
mask.W = 0;
_previewMaterial.SetParameterValue("Mask", mask);
}
/// <inheritdoc />
protected override void PerformLayoutBeforeChildren()
{
base.PerformLayoutBeforeChildren();
ViewportWidgetsContainer.ArrangeWidgets(this);
}
/// <inheritdoc />
public override bool HasLoadedAssets => base.HasLoadedAssets && _previewMaterial.IsLoaded && _previewMaterial.BaseMaterial.IsLoaded;
/// <inheritdoc />
public override void OnDestroy()
{
Material = null;
Object.Destroy(ref _previewMaterial);
base.OnDestroy();
}
}
}

View File

@@ -0,0 +1,72 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
namespace FlaxEditor.Viewport.Previews
{
/// <summary>
/// Preview control for <see cref="IESProfile"/> asset.
/// </summary>
/// <seealso cref="FlaxEditor.Viewport.Previews.TexturePreviewBase" />
public class IESProfilePreview : TexturePreviewBase
{
private IESProfile _asset;
private MaterialInstance _previewMaterial;
/// <summary>
/// Gets or sets the asset to preview.
/// </summary>
public IESProfile Asset
{
get => _asset;
set
{
if (_asset != value)
{
_asset = value;
_previewMaterial.SetParameterValue("Texture", value);
UpdateTextureRect();
}
}
}
/// <summary>
/// Initializes a new instance of the <see cref="IESProfilePreview"/> class.
/// </summary>
public IESProfilePreview()
{
var baseMaterial = FlaxEngine.Content.LoadAsyncInternal<Material>(EditorAssets.IesProfilePreviewMaterial);
// Wait for base (don't want to async material parameters set due to async loading)
if (baseMaterial == null || baseMaterial.WaitForLoaded())
throw new FlaxException("Cannot load IES Profile preview material.");
// Create preview material (virtual)
_previewMaterial = baseMaterial.CreateVirtualInstance();
}
/// <inheritdoc />
public override void OnDestroy()
{
Object.Destroy(ref _previewMaterial);
base.OnDestroy();
}
/// <inheritdoc />
protected override void CalculateTextureRect(out Rectangle rect)
{
CalculateTextureRect(new Vector2(256), Size, out rect);
}
/// <inheritdoc />
protected override void DrawTexture(ref Rectangle rect)
{
// Check if has loaded asset
if (_asset && _asset.IsLoaded)
{
Render2D.DrawMaterial(_previewMaterial, rect);
}
}
}
}

View File

@@ -0,0 +1,342 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.Surface;
using FlaxEngine;
using FlaxEngine.GUI;
using Object = FlaxEngine.Object;
namespace FlaxEditor.Viewport.Previews
{
/// <summary>
/// Material or Material Instance asset preview editor viewport.
/// </summary>
/// <seealso cref="AssetPreview" />
public class MaterialPreview : AssetPreview, IVisjectSurfaceOwner
{
private static readonly string[] Models =
{
"Sphere",
"Cube",
"Plane",
"Cylinder",
"Cone"
};
private StaticModel _previewModel;
private Decal _decal;
private Terrain _terrain;
private ParticleEffect _particleEffect;
private MaterialBase _particleEffectMaterial;
private ParticleEmitter _particleEffectEmitter;
private ParticleSystem _particleEffectSystem;
private ParticleEmitterSurface _particleEffectSurface;
private MaterialBase _material;
private int _selectedModelIndex;
private Image _guiMaterialControl;
private readonly MaterialBase[] _postFxMaterialsCache = new MaterialBase[1];
/// <summary>
/// Gets or sets the material asset to preview. It can be <see cref="FlaxEngine.Material"/> or <see cref="FlaxEngine.MaterialInstance"/>.
/// </summary>
public MaterialBase Material
{
get => _material;
set
{
if (_material != value)
{
_material = value;
UpdateMaterial();
}
}
}
/// <summary>
/// Gets or sets the selected preview model index.
/// </summary>
public int SelectedModelIndex
{
get => _selectedModelIndex;
set
{
if (value < 0 || value > Models.Length)
throw new ArgumentOutOfRangeException();
_selectedModelIndex = value;
_previewModel.Model = FlaxEngine.Content.LoadAsyncInternal<Model>("Editor/Primitives/" + Models[value]);
}
}
/// <summary>
/// Initializes a new instance of the <see cref="MaterialPreview"/> class.
/// </summary>
/// <param name="useWidgets">if set to <c>true</c> use widgets.</param>
public MaterialPreview(bool useWidgets)
: base(useWidgets)
{
// Setup preview scene
_previewModel = new StaticModel();
_previewModel.Transform = new Transform(Vector3.Zero, Quaternion.RotationY(Mathf.Pi), new Vector3(0.45f));
SelectedModelIndex = 0;
// Link actors for rendering
Task.AddCustomActor(_previewModel);
// Create context menu for primitive switching
if (useWidgets && ViewWidgetButtonMenu != null)
{
ViewWidgetButtonMenu.AddSeparator();
var modelSelect = ViewWidgetButtonMenu.AddChildMenu("Model").ContextMenu;
// Fill out all models
for (int i = 0; i < Models.Length; i++)
{
var button = modelSelect.AddButton(Models[i]);
button.Tag = i;
}
// Link the action
modelSelect.ButtonClicked += (button) => SelectedModelIndex = (int)button.Tag;
}
}
/// <inheritdoc />
public override bool HasLoadedAssets
{
get
{
if (!base.HasLoadedAssets)
return false;
UpdateMaterial();
return true;
}
}
/// <inheritdoc />
public override void Update(float deltaTime)
{
base.Update(deltaTime);
UpdateMaterial();
}
private void UpdateMaterial()
{
// If material is a surface link it to the preview model.
// Otherwise use postFx volume to render custom postFx material.
MaterialBase surfaceMaterial = null;
MaterialBase postFxMaterial = null;
MaterialBase decalMaterial = null;
MaterialBase guiMaterial = null;
MaterialBase terrainMaterial = null;
MaterialBase particleMaterial = null;
bool usePreviewActor = true;
if (_material != null)
{
if (_material is MaterialInstance materialInstance && materialInstance.BaseMaterial == null)
{
// Material instance without a base material should not be used
}
else
{
switch (_material.Info.Domain)
{
case MaterialDomain.Surface:
surfaceMaterial = _material;
break;
case MaterialDomain.PostProcess:
postFxMaterial = _material;
break;
case MaterialDomain.Decal:
decalMaterial = _material;
break;
case MaterialDomain.GUI:
usePreviewActor = false;
guiMaterial = _material;
break;
case MaterialDomain.Terrain:
usePreviewActor = false;
terrainMaterial = _material;
break;
case MaterialDomain.Particle:
usePreviewActor = false;
particleMaterial = _material;
break;
default: throw new ArgumentOutOfRangeException();
}
}
}
// Surface
if (_previewModel.Model == null)
throw new Exception("Missing preview model asset.");
if (_previewModel.Model.WaitForLoaded())
throw new Exception("Preview model asset failed to load.");
_previewModel.SetMaterial(0, surfaceMaterial);
_previewModel.IsActive = usePreviewActor;
// PostFx
_postFxMaterialsCache[0] = postFxMaterial;
PostFxVolume.PostFxMaterials = new PostFxMaterialsSettings
{
Materials = _postFxMaterialsCache,
};
// Decal
if (decalMaterial && _decal == null)
{
_decal = new Decal();
_decal.Size = new Vector3(100.0f);
_decal.LocalOrientation = Quaternion.RotationZ(Mathf.PiOverTwo);
Task.AddCustomActor(_decal);
}
if (_decal)
{
_decal.Material = decalMaterial;
}
// GUI
if (guiMaterial && _guiMaterialControl == null)
{
_guiMaterialControl = new Image
{
AnchorPreset = AnchorPresets.StretchAll,
KeepAspectRatio = false,
Brush = new MaterialBrush(),
Parent = this,
IndexInParent = 0,
};
}
if (_guiMaterialControl != null)
{
((MaterialBrush)_guiMaterialControl.Brush).Material = guiMaterial;
_guiMaterialControl.Enabled = _guiMaterialControl.Visible = guiMaterial != null;
}
// Terrain
if (terrainMaterial && _terrain == null)
{
_terrain = new Terrain();
_terrain.Setup(1, 63);
var chunkSize = _terrain.ChunkSize;
var heightMapSize = chunkSize * Terrain.PatchEdgeChunksCount + 1;
var heightMapLength = heightMapSize * heightMapSize;
var heightmap = new float[heightMapLength];
var patchCoord = new Int2(0, 0);
_terrain.AddPatch(ref patchCoord);
_terrain.SetupPatchHeightMap(ref patchCoord, heightmap, null, true);
_terrain.LocalPosition = new Vector3(-1000, 0, -1000);
Task.AddCustomActor(_terrain);
}
if (_terrain != null)
{
_terrain.IsActive = terrainMaterial != null;
_terrain.Material = terrainMaterial;
}
// Particle
if (particleMaterial && _particleEffect == null)
{
_particleEffect = new ParticleEffect();
_particleEffect.IsLooping = true;
_particleEffect.UseTimeScale = false;
Task.AddCustomActor(_particleEffect);
}
if (_particleEffect != null)
{
_particleEffect.IsActive = particleMaterial != null;
if (particleMaterial)
_particleEffect.UpdateSimulation();
if (_particleEffectMaterial != particleMaterial && particleMaterial)
{
_particleEffectMaterial = particleMaterial;
if (!_particleEffectEmitter)
{
var srcAsset = FlaxEngine.Content.LoadInternal<ParticleEmitter>("Editor/Particles/Particle Material Preview");
Editor.Instance.ContentEditing.FastTempAssetClone(srcAsset.Path, out var clonedPath);
_particleEffectEmitter = FlaxEngine.Content.Load<ParticleEmitter>(clonedPath);
}
if (_particleEffectSurface == null)
_particleEffectSurface = new ParticleEmitterSurface(this, null, null);
if (_particleEffectEmitter)
{
if (!_particleEffectSurface.Load())
{
var spriteModuleNode = _particleEffectSurface.FindNode(15, 400);
spriteModuleNode.Values[2] = particleMaterial.ID;
_particleEffectSurface.Save();
}
}
}
}
}
/// <inheritdoc />
public override void OnDestroy()
{
_material = null;
if (_guiMaterialControl != null)
{
_guiMaterialControl.Dispose();
_guiMaterialControl = null;
}
Object.Destroy(ref _previewModel);
Object.Destroy(ref _decal);
Object.Destroy(ref _terrain);
Object.Destroy(ref _particleEffect);
Object.Destroy(ref _particleEffectEmitter);
Object.Destroy(ref _particleEffectSystem);
_particleEffectMaterial = null;
_particleEffectSurface = null;
base.OnDestroy();
}
/// <inheritdoc />
string ISurfaceContext.SurfaceName => string.Empty;
/// <inheritdoc />
byte[] ISurfaceContext.SurfaceData
{
get => _particleEffectEmitter.LoadSurface(false);
set
{
_particleEffectEmitter.SaveSurface(value);
_particleEffectEmitter.Reload();
if (!_particleEffectSystem)
{
_particleEffectSystem = FlaxEngine.Content.CreateVirtualAsset<ParticleSystem>();
_particleEffectSystem.Init(_particleEffectEmitter, 5.0f);
_particleEffect.ParticleSystem = _particleEffectSystem;
}
}
}
/// <inheritdoc />
void ISurfaceContext.OnContextCreated(VisjectSurfaceContext context)
{
}
/// <inheritdoc />
public Undo Undo => null;
/// <inheritdoc />
void IVisjectSurfaceOwner.OnSurfaceEditedChanged()
{
}
/// <inheritdoc />
void IVisjectSurfaceOwner.OnSurfaceGraphEdited()
{
}
/// <inheritdoc />
void IVisjectSurfaceOwner.OnSurfaceClose()
{
}
}
}

View File

@@ -0,0 +1,95 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.GUI.Input;
using FlaxEngine;
using Object = FlaxEngine.Object;
namespace FlaxEditor.Viewport.Previews
{
/// <summary>
/// Model asset preview editor viewport.
/// </summary>
/// <seealso cref="AssetPreview" />
public class ModelPreview : AssetPreview
{
private StaticModel _previewModel;
/// <summary>
/// Gets or sets the model asset to preview.
/// </summary>
public Model Model
{
get => _previewModel.Model;
set => _previewModel.Model = value;
}
/// <summary>
/// Gets the model actor used to preview selected asset.
/// </summary>
public StaticModel PreviewActor => _previewModel;
/// <summary>
/// Gets or sets a value indicating whether scale the model to the normalized bounds.
/// </summary>
public bool ScaleToFit { get; set; } = true;
/// <summary>
/// Initializes a new instance of the <see cref="ModelPreview"/> class.
/// </summary>
/// <param name="useWidgets">if set to <c>true</c> use widgets.</param>
public ModelPreview(bool useWidgets)
: base(useWidgets)
{
Task.Begin += OnBegin;
// Setup preview scene
_previewModel = new StaticModel();
// Link actors for rendering
Task.AddCustomActor(_previewModel);
if (useWidgets)
{
// Preview LOD
{
var previewLOD = ViewWidgetButtonMenu.AddButton("Preview LOD");
var previewLODValue = new IntValueBox(-1, 75, 2, 50.0f, -1, 10, 0.02f);
previewLODValue.Parent = previewLOD;
previewLODValue.ValueChanged += () => _previewModel.ForcedLOD = previewLODValue.Value;
ViewWidgetButtonMenu.VisibleChanged += control => previewLODValue.Value = _previewModel.ForcedLOD;
}
}
}
private void OnBegin(RenderTask task, GPUContext context)
{
if (!ScaleToFit)
{
_previewModel.Scale = Vector3.One;
_previewModel.Position = Vector3.Zero;
return;
}
// Update preview model scale to fit the preview
var model = Model;
if (model && model.IsLoaded)
{
float targetSize = 50.0f;
BoundingBox box = model.GetBox();
float maxSize = Mathf.Max(0.001f, box.Size.MaxValue);
float scale = targetSize / maxSize;
_previewModel.Scale = new Vector3(scale);
_previewModel.Position = box.Center * (-0.5f * scale) + new Vector3(0, -10, 0);
}
}
/// <inheritdoc />
public override void OnDestroy()
{
// Ensure to cleanup created actor objects
Object.Destroy(ref _previewModel);
base.OnDestroy();
}
}
}

View File

@@ -0,0 +1,90 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.GUI.Input;
using FlaxEngine;
using Object = FlaxEngine.Object;
namespace FlaxEditor.Viewport.Previews
{
/// <summary>
/// Particle Emitter asset preview editor viewport.
/// </summary>
/// <seealso cref="AssetPreview" />
public class ParticleEmitterPreview : ParticleSystemPreview
{
private ParticleEmitter _emitter;
private ParticleSystem _system;
private float _playbackDuration = 5.0f;
/// <summary>
/// Gets or sets the particle emitter asset to preview.
/// </summary>
public ParticleEmitter Emitter
{
get => _emitter;
set
{
if (_emitter != value)
{
_emitter = value;
_system.Init(value, _playbackDuration);
PreviewActor.ResetSimulation();
}
}
}
/// <summary>
/// Gets or sets the duration of the emitter playback (in seconds).
/// </summary>
public float PlaybackDuration
{
get => _playbackDuration;
set
{
value = Mathf.Clamp(value, 0.1f, 100000000000.0f);
if (Mathf.NearEqual(_playbackDuration, value))
return;
_playbackDuration = value;
if (_system != null)
{
_system.Init(_emitter, _playbackDuration);
PreviewActor.ResetSimulation();
}
}
}
/// <summary>
/// Initializes a new instance of the <see cref="ParticleEmitterPreview"/> class.
/// </summary>
/// <param name="useWidgets">if set to <c>true</c> use widgets.</param>
public ParticleEmitterPreview(bool useWidgets)
: base(useWidgets)
{
_system = FlaxEngine.Content.CreateVirtualAsset<ParticleSystem>();
System = _system;
if (useWidgets)
{
var playbackDuration = ViewWidgetButtonMenu.AddButton("Duration");
var playbackDurationValue = new FloatValueBox(_playbackDuration, 75, 2, 50.0f, 0.1f, 1000000.0f, 0.1f);
playbackDurationValue.Parent = playbackDuration;
playbackDurationValue.ValueChanged += () => PlaybackDuration = playbackDurationValue.Value;
ViewWidgetButtonMenu.VisibleChanged += control => playbackDurationValue.Value = PlaybackDuration;
}
}
/// <inheritdoc />
public override bool HasLoadedAssets => (_emitter == null || _emitter.IsLoaded) && base.HasLoadedAssets;
/// <inheritdoc />
public override void OnDestroy()
{
// Cleanup objects
_emitter = null;
Object.Destroy(ref _system);
base.OnDestroy();
}
}
}

View File

@@ -0,0 +1,240 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.Viewport.Cameras;
using FlaxEngine;
using FlaxEngine.GUI;
using Object = FlaxEngine.Object;
namespace FlaxEditor.Viewport.Previews
{
/// <summary>
/// Particle System asset preview editor viewport.
/// </summary>
/// <seealso cref="AssetPreview" />
public class ParticleSystemPreview : AssetPreview
{
private ParticleEffect _previewEffect;
private ContextMenuButton _showBoundsButton;
private ContextMenuButton _showOriginButton;
private ContextMenuButton _showParticleCounterButton;
private StaticModel _boundsModel;
private StaticModel _originModel;
private bool _showParticlesCounter;
/// <summary>
/// Gets or sets the particle system asset to preview.
/// </summary>
public ParticleSystem System
{
get => _previewEffect.ParticleSystem;
set => _previewEffect.ParticleSystem = value;
}
/// <summary>
/// Gets the particle effect actor used to preview selected asset.
/// </summary>
public ParticleEffect PreviewActor => _previewEffect;
/// <summary>
/// Gets or sets a value indicating whether to play the particles simulation in editor.
/// </summary>
public bool PlaySimulation { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether to show particle effect bounding box.
/// </summary>
public bool ShowBounds
{
get => _boundsModel?.IsActive ?? false;
set
{
if (value == ShowBounds)
return;
if (value)
{
if (!_boundsModel)
{
_boundsModel = new StaticModel();
_boundsModel.Model = FlaxEngine.Content.LoadAsyncInternal<Model>("Editor/Gizmo/WireBox");
_boundsModel.Model.WaitForLoaded();
_boundsModel.SetMaterial(0, FlaxEngine.Content.LoadAsyncInternal<MaterialBase>("Editor/Gizmo/MaterialWireFocus"));
Task.AddCustomActor(_boundsModel);
}
else if (!_boundsModel.IsActive)
{
_boundsModel.IsActive = true;
Task.AddCustomActor(_boundsModel);
}
UpdateBoundsModel();
}
else
{
_boundsModel.IsActive = false;
Task.RemoveCustomActor(_boundsModel);
}
if (_showBoundsButton != null)
_showBoundsButton.Checked = value;
}
}
/// <summary>
/// Gets or sets a value indicating whether to show particle effect origin point.
/// </summary>
public bool ShowOrigin
{
get => _originModel?.IsActive ?? false;
set
{
if (value == ShowOrigin)
return;
if (value)
{
if (!_originModel)
{
_originModel = new StaticModel();
_originModel.Model = FlaxEngine.Content.LoadAsyncInternal<Model>("Editor/Primitives/Sphere");
_originModel.Model.WaitForLoaded();
_originModel.SetMaterial(0, FlaxEngine.Content.LoadAsyncInternal<MaterialBase>("Editor/Gizmo/MaterialAxisFocus"));
_originModel.Position = _previewEffect.Position;
_originModel.Scale = new Vector3(0.1f);
Task.AddCustomActor(_originModel);
}
else if (!_originModel.IsActive)
{
_originModel.IsActive = true;
Task.AddCustomActor(_originModel);
}
}
else
{
_originModel.IsActive = false;
Task.RemoveCustomActor(_originModel);
}
if (_showOriginButton != null)
_showOriginButton.Checked = value;
}
}
/// <summary>
/// Gets or sets a value indicating whether to show spawned particles counter (for CPU particles only).
/// </summary>
public bool ShowParticlesCounter
{
get => _showParticlesCounter;
set
{
if (value == _showParticlesCounter)
return;
_showParticlesCounter = value;
if (_showParticleCounterButton != null)
_showParticleCounterButton.Checked = value;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="ParticleSystemPreview"/> class.
/// </summary>
/// <param name="useWidgets">if set to <c>true</c> use widgets.</param>
public ParticleSystemPreview(bool useWidgets)
: base(useWidgets, new FPSCamera())
{
// Setup preview scene
_previewEffect = new ParticleEffect();
_previewEffect.UseTimeScale = false;
_previewEffect.IsLooping = true;
_previewEffect.CustomViewRenderTask = Task;
// Link actors for rendering
Task.AddCustomActor(_previewEffect);
if (useWidgets)
{
_showBoundsButton = ViewWidgetShowMenu.AddButton("Bounds", () => ShowBounds = !ShowBounds);
_showOriginButton = ViewWidgetShowMenu.AddButton("Origin", () => ShowOrigin = !ShowOrigin);
_showParticleCounterButton = ViewWidgetShowMenu.AddButton("Particles Counter", () => ShowParticlesCounter = !ShowParticlesCounter);
}
}
private void UpdateBoundsModel()
{
var bounds = _previewEffect.Box;
Transform t = Transform.Identity;
t.Translation = bounds.Center;
t.Scale = bounds.Size;
_boundsModel.Transform = t;
}
/// <summary>
/// Fits the particle system into view (scales the emitter based on the current bounds of the system).
/// </summary>
/// <param name="targetSize">The target size of the effect.</param>
public void FitIntoView(float targetSize = 300.0f)
{
_previewEffect.Scale = Vector3.One;
float maxSize = Mathf.Max(0.001f, _previewEffect.Box.Size.MaxValue);
_previewEffect.Scale = new Vector3(targetSize / maxSize);
}
/// <inheritdoc />
public override bool HasLoadedAssets => _previewEffect.HasContentLoaded && base.HasLoadedAssets;
/// <inheritdoc />
public override void Update(float deltaTime)
{
base.Update(deltaTime);
// Manually update simulation
if (PlaySimulation)
{
_previewEffect.UpdateSimulation();
}
// Keep bounds matching the model
if (_boundsModel && _boundsModel.IsActive)
{
UpdateBoundsModel();
}
}
/// <inheritdoc />
public override void Draw()
{
base.Draw();
if (_showParticlesCounter)
{
var count = _previewEffect.ParticlesCount;
Render2D.DrawText(
Style.Current.FontSmall,
"Particles: " + count,
new Rectangle(Vector2.Zero, Size),
Color.Wheat,
TextAlignment.Near,
TextAlignment.Far);
}
}
/// <inheritdoc />
public override void OnDestroy()
{
// Cleanup objects
_previewEffect.ParticleSystem = null;
Object.Destroy(ref _previewEffect);
Object.Destroy(ref _boundsModel);
Object.Destroy(ref _originModel);
_showBoundsButton = null;
_showOriginButton = null;
_showParticleCounterButton = null;
base.OnDestroy();
}
}
}

View File

@@ -0,0 +1,96 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
using FlaxEngine.GUI;
using Object = FlaxEngine.Object;
namespace FlaxEditor.Viewport.Previews
{
/// <summary>
/// Prefab asset preview editor viewport.
/// </summary>
/// <seealso cref="AssetPreview" />
public class PrefabPreview : AssetPreview
{
/// <summary>
/// The preview that is during prefab instance spawning. Used to link some actors such as UIControl to preview scene and view.
/// </summary>
internal static PrefabPreview LoadingPreview;
private Prefab _prefab;
private Actor _instance;
internal Control customControlLinked;
/// <summary>
/// Gets or sets the prefab asset to preview.
/// </summary>
public Prefab Prefab
{
get => _prefab;
set
{
if (_prefab != value)
{
if (_instance)
{
if (customControlLinked != null)
{
customControlLinked.Parent = null;
customControlLinked = null;
}
Task.RemoveCustomActor(_instance);
Object.Destroy(_instance);
}
_prefab = value;
if (_prefab)
{
_prefab.WaitForLoaded(); // TODO: use lazy prefab spawning to reduce stalls
var prevPreview = LoadingPreview;
LoadingPreview = this;
_instance = PrefabManager.SpawnPrefab(_prefab, null);
LoadingPreview = prevPreview;
if (_instance == null)
{
_prefab = null;
throw new FlaxException("Failed to spawn a prefab for the preview.");
}
Task.AddCustomActor(_instance);
}
}
}
}
/// <summary>
/// Gets the instance of the prefab spawned for the preview.
/// </summary>
public Actor Instance
{
get => _instance;
internal set => _instance = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="PrefabPreview"/> class.
/// </summary>
/// <param name="useWidgets">if set to <c>true</c> use widgets.</param>
public PrefabPreview(bool useWidgets)
: base(useWidgets)
{
}
/// <inheritdoc />
public override void OnDestroy()
{
// Cleanup
Prefab = null;
base.OnDestroy();
}
}
}

View File

@@ -0,0 +1,677 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.Viewport.Widgets;
using FlaxEngine;
using FlaxEngine.GUI;
using Object = FlaxEngine.Object;
namespace FlaxEditor.Viewport.Previews
{
/// <summary>
/// Base class for texture previews. Draws a surface in the UI and supports view moving/zooming.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
public abstract class TexturePreviewBase : ContainerControl
{
private Rectangle _textureRect;
private Vector2 _lastMosuePos;
private Vector2 _viewPos;
private float _viewScale = 1.0f;
private bool _isMouseDown;
/// <inheritdoc />
protected TexturePreviewBase()
{
AnchorPreset = AnchorPresets.StretchAll;
Offsets = Margin.Zero;
}
/// <summary>
/// Moves the view to the center.
/// </summary>
public void CenterView()
{
_viewScale = 1.0f;
_viewPos = Vector2.Zero;
}
/// <summary>
/// Updates the texture rectangle.
/// </summary>
protected void UpdateTextureRect()
{
CalculateTextureRect(out _textureRect);
}
/// <summary>
/// Calculates the texture rectangle.
/// </summary>
/// <param name="rect">The rectangle.</param>
protected abstract void CalculateTextureRect(out Rectangle rect);
/// <summary>
/// Calculates the texture rect fr the given texture and the view size.
/// </summary>
/// <param name="textureSize">Size of the texture.</param>
/// <param name="viewSize">Size of the view.</param>
/// <param name="result">The result.</param>
protected static void CalculateTextureRect(Vector2 textureSize, Vector2 viewSize, out Rectangle result)
{
Vector2 size = Vector2.Max(textureSize, Vector2.One);
float aspectRatio = size.X / size.Y;
float h = viewSize.X / aspectRatio;
float w = viewSize.Y * aspectRatio;
if (w > h)
{
float diff = (viewSize.Y - h) * 0.5f;
result = new Rectangle(0, diff, viewSize.X, h);
}
else
{
float diff = (viewSize.X - w) * 0.5f;
result = new Rectangle(diff, 0, w, viewSize.Y);
}
}
/// <summary>
/// Draws the texture.
/// </summary>
/// <param name="rect">The target texture view rectangle.</param>
protected abstract void DrawTexture(ref Rectangle rect);
/// <summary>
/// Gets the texture view rect (scaled and offseted).
/// </summary>
protected Rectangle TextureViewRect => (_textureRect + _viewPos) * _viewScale;
/// <inheritdoc />
public override void Draw()
{
Render2D.PushClip(new Rectangle(Vector2.Zero, Size));
// Calculate texture view rectangle
UpdateTextureRect();
var textureRect = TextureViewRect;
// Call drawing
DrawTexture(ref textureRect);
// Add overlay during debugger breakpoint hang
if (Editor.Instance.Simulation.IsDuringBreakpointHang)
{
var bounds = new Rectangle(Vector2.Zero, Size);
Render2D.FillRectangle(bounds, new Color(0.0f, 0.0f, 0.0f, 0.2f));
Render2D.DrawText(Style.Current.FontLarge, "Debugger breakpoint hit...", bounds, Color.White, TextAlignment.Center, TextAlignment.Center);
}
Render2D.PopClip();
base.Draw();
}
/// <inheritdoc />
public override void OnMouseEnter(Vector2 location)
{
// Store mouse position
_lastMosuePos = location;
base.OnMouseEnter(location);
}
/// <inheritdoc />
public override void OnMouseMove(Vector2 location)
{
// Check if mouse is down
if (_isMouseDown)
{
// Calculate mouse delta
Vector2 delta = location - _lastMosuePos;
// Move view
_viewPos += delta;
}
// Store mouse position
_lastMosuePos = location;
base.OnMouseMove(location);
}
/// <inheritdoc />
public override void OnMouseLeave()
{
// Clear flag
_isMouseDown = false;
Cursor = CursorType.Default;
base.OnMouseLeave();
}
/// <inheritdoc />
public override bool OnMouseWheel(Vector2 location, float delta)
{
if (base.OnMouseWheel(location, delta))
return true;
// Zoom
float prevScale = _viewScale;
_viewScale = Mathf.Clamp(_viewScale + delta * 0.24f, 0.001f, 20.0f);
// Move view to make use of the control much more soother
//float coeff = (prevScale + (_viewScale - prevScale)) / prevScale;
//_viewPos += (location * coeff - location) / _viewScale;
//_viewPos += location / _viewScale;
Vector2 sizeDelta = (_viewScale - prevScale) * _textureRect.Size;
_viewPos += sizeDelta * 0.5f;
return true;
}
/// <inheritdoc />
public override bool OnMouseDown(Vector2 location, MouseButton button)
{
if (base.OnMouseDown(location, button))
return true;
// Set flag
_isMouseDown = true;
_lastMosuePos = location;
Cursor = CursorType.SizeAll;
return true;
}
/// <inheritdoc />
public override bool OnMouseUp(Vector2 location, MouseButton button)
{
if (base.OnMouseUp(location, button))
return true;
// Clear flag
_isMouseDown = false;
Cursor = CursorType.Default;
return true;
}
/// <inheritdoc />
protected override void OnSizeChanged()
{
base.OnSizeChanged();
// Update texture rectangle and move view to the center
UpdateTextureRect();
CenterView();
}
}
/// <summary>
/// Texture channel flags.
/// </summary>
[Flags, HideInEditor]
public enum ChannelFlags
{
/// <summary>
/// The none.
/// </summary>
None = 0,
/// <summary>
/// The red channel.
/// </summary>
Red = 1,
/// <summary>
/// The green channel.
/// </summary>
Green = 2,
/// <summary>
/// The blue channel.
/// </summary>
Blue = 4,
/// <summary>
/// The alpha channel.
/// </summary>
Alpha = 8,
/// <summary>
/// All texture channels.
/// </summary>
All = Red | Green | Blue | Alpha
}
/// <summary>
/// Base class for texture previews with custom drawing features. Uses in-build postFx material to render a texture.
/// </summary>
/// <seealso cref="TexturePreviewBase" />
public abstract class TexturePreviewCustomBase : TexturePreviewBase
{
private ChannelFlags _channelFlags = ChannelFlags.All;
private bool _usePointSampler = false;
private float _mipLevel = -1;
private ContextMenu _mipWidgetMenu;
private ContextMenuButton _filterWidgetPointButton;
private ContextMenuButton _filterWidgetLinearButton;
/// <summary>
/// The preview material instance used to draw texture.
/// </summary>
protected MaterialInstance _previewMaterial;
/// <summary>
/// Gets or sets the view channels to show.
/// </summary>
public ChannelFlags ViewChannels
{
get => _channelFlags;
set
{
if (_channelFlags != value)
{
_channelFlags = value;
UpdateMask();
}
}
}
/// <summary>
/// Gets or sets a value indicating whether use point sampler when drawing the texture. The default value is false.
/// </summary>
public bool UsePointSampler
{
get => _usePointSampler;
set
{
if (_usePointSampler != value)
{
_usePointSampler = value;
_previewMaterial.SetParameterValue("PointSampler", value);
}
}
}
/// <summary>
/// Gets or sets the mip level to show. The default value is -1.
/// </summary>
public float MipLevel
{
get => _mipLevel;
set
{
if (!Mathf.NearEqual(_mipLevel, value))
{
_mipLevel = value;
_previewMaterial.SetParameterValue("Mip", value);
}
}
}
/// <inheritdoc />
/// <param name="useWidgets">True if show viewport widgets.</param>
protected TexturePreviewCustomBase(bool useWidgets)
{
// Create preview material (virtual)
var baseMaterial = FlaxEngine.Content.LoadAsyncInternal<Material>("Editor/TexturePreviewMaterial");
if (baseMaterial == null)
throw new FlaxException("Cannot load texture preview material.");
_previewMaterial = baseMaterial.CreateVirtualInstance();
if (_previewMaterial == null)
throw new FlaxException("Failed to create virtual material instance for preview material.");
// Add widgets
if (useWidgets)
{
// Channels widget
var channelsWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperLeft);
//
var channelR = new ViewportWidgetButton("R", SpriteHandle.Invalid, null, true)
{
Checked = true,
TooltipText = "Show/hide texture red channel",
Parent = channelsWidget
};
channelR.Toggled += button => ViewChannels = button.Checked ? ViewChannels | ChannelFlags.Red : (ViewChannels & ~ChannelFlags.Red);
var channelG = new ViewportWidgetButton("G", SpriteHandle.Invalid, null, true)
{
Checked = true,
TooltipText = "Show/hide texture green channel",
Parent = channelsWidget
};
channelG.Toggled += button => ViewChannels = button.Checked ? ViewChannels | ChannelFlags.Green : (ViewChannels & ~ChannelFlags.Green);
var channelB = new ViewportWidgetButton("B", SpriteHandle.Invalid, null, true)
{
Checked = true,
TooltipText = "Show/hide texture blue channel",
Parent = channelsWidget
};
channelB.Toggled += button => ViewChannels = button.Checked ? ViewChannels | ChannelFlags.Blue : (ViewChannels & ~ChannelFlags.Blue);
var channelA = new ViewportWidgetButton("A", SpriteHandle.Invalid, null, true)
{
Checked = true,
TooltipText = "Show/hide texture alpha channel",
Parent = channelsWidget
};
channelA.Toggled += button => ViewChannels = button.Checked ? ViewChannels | ChannelFlags.Alpha : (ViewChannels & ~ChannelFlags.Alpha);
//
channelsWidget.Parent = this;
// Mip widget
var mipWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperLeft);
_mipWidgetMenu = new ContextMenu();
_mipWidgetMenu.VisibleChanged += OnMipWidgetMenuOnVisibleChanged;
var mipWidgetButton = new ViewportWidgetButton("Mip", SpriteHandle.Invalid, _mipWidgetMenu)
{
TooltipText = "The mip level to show. The default is -1.",
Parent = mipWidget
};
//
mipWidget.Parent = this;
// Filter widget
var filterWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperLeft);
var filterWidgetMenu = new ContextMenu();
filterWidgetMenu.VisibleChanged += OnFilterWidgetMenuVisibleChanged;
_filterWidgetPointButton = filterWidgetMenu.AddButton("Point", () => UsePointSampler = true);
_filterWidgetLinearButton = filterWidgetMenu.AddButton("Linear", () => UsePointSampler = false);
var filterWidgetButton = new ViewportWidgetButton("Filter", SpriteHandle.Invalid, filterWidgetMenu)
{
TooltipText = "The texture preview filtering mode. The default is Linear.",
Parent = filterWidget
};
//
filterWidget.Parent = this;
}
// Wait for base (don't want to async material parameters set due to async loading)
baseMaterial.WaitForLoaded();
}
private void OnFilterWidgetMenuVisibleChanged(Control control)
{
if (!control.Visible)
return;
_filterWidgetPointButton.Checked = UsePointSampler;
_filterWidgetLinearButton.Checked = !UsePointSampler;
}
/// <summary>
/// Sets the texture to draw (material parameter).
/// </summary>
/// <param name="value">The value.</param>
protected void SetTexture(object value)
{
_previewMaterial.SetParameterValue("Texture", value);
UpdateTextureRect();
}
private void OnMipWidgetMenuOnVisibleChanged(Control control)
{
if (!control.Visible)
return;
var textureObj = _previewMaterial.GetParameterValue("Texture");
if (textureObj is TextureBase texture && !texture.WaitForLoaded())
{
_mipWidgetMenu.ItemsContainer.DisposeChildren();
var mipLevels = texture.MipLevels;
for (int i = -1; i < mipLevels; i++)
{
var button = _mipWidgetMenu.AddButton(i.ToString(), OnMipWidgetClicked);
button.Tag = i;
if (i == -1)
button.TooltipText = "Default mip.";
button.Checked = Mathf.Abs(MipLevel - i) < 0.9f;
}
}
}
private void OnMipWidgetClicked(ContextMenuButton button)
{
MipLevel = (int)button.Tag;
}
private void UpdateMask()
{
Vector4 mask = Vector4.One;
if ((_channelFlags & ChannelFlags.Red) == 0)
mask.X = 0;
if ((_channelFlags & ChannelFlags.Green) == 0)
mask.Y = 0;
if ((_channelFlags & ChannelFlags.Blue) == 0)
mask.Z = 0;
if ((_channelFlags & ChannelFlags.Alpha) == 0)
mask.W = 0;
_previewMaterial.SetParameterValue("Mask", mask);
}
/// <inheritdoc />
protected override void PerformLayoutBeforeChildren()
{
base.PerformLayoutBeforeChildren();
ViewportWidgetsContainer.ArrangeWidgets(this);
}
/// <inheritdoc />
public override void OnDestroy()
{
Object.Destroy(ref _previewMaterial);
base.OnDestroy();
}
}
/// <summary>
/// Texture preview GUI control. Draws <see cref="FlaxEngine.Texture"/> in the UI and supports view moving/zomming.
/// </summary>
/// <seealso cref="TexturePreviewBase" />
public class SimpleTexturePreview : TexturePreviewBase
{
private Texture _asset;
/// <summary>
/// Gets or sets the asset to preview.
/// </summary>
public Texture Asset
{
get => _asset;
set
{
if (_asset != value)
{
_asset = value;
UpdateTextureRect();
}
}
}
/// <summary>
/// Gets or sets the color used to multiply texture colors.
/// </summary>
public Color Color { get; set; } = Color.White;
/// <inheritdoc />
protected override void CalculateTextureRect(out Rectangle rect)
{
CalculateTextureRect(_asset?.Size ?? new Vector2(100), Size, out rect);
}
/// <inheritdoc />
protected override void DrawTexture(ref Rectangle rect)
{
// Background
Render2D.FillRectangle(rect, Color.Gray);
// Check if has loaded asset
if (_asset && _asset.IsLoaded)
{
Render2D.DrawTexture(_asset, rect, Color);
}
}
}
/// <summary>
/// Sprite atlas preview GUI control. Draws <see cref="SpriteAtlas"/> in the UI and supports view moving/zomming.
/// </summary>
/// <seealso cref="TexturePreviewBase" />
public class SimpleSpriteAtlasPreview : TexturePreviewBase
{
private SpriteAtlas _asset;
/// <summary>
/// Gets or sets the asset to preview.
/// </summary>
public SpriteAtlas Asset
{
get => _asset;
set
{
if (_asset != value)
{
_asset = value;
UpdateTextureRect();
}
}
}
/// <summary>
/// Gets or sets the color used to multiply texture colors.
/// </summary>
public Color Color { get; set; } = Color.White;
/// <inheritdoc />
protected override void CalculateTextureRect(out Rectangle rect)
{
CalculateTextureRect(_asset?.Size ?? new Vector2(100), Size, out rect);
}
/// <inheritdoc />
protected override void DrawTexture(ref Rectangle rect)
{
// Background
Render2D.FillRectangle(rect, Color.Gray);
// Check if has loaded asset
if (_asset && _asset.IsLoaded)
{
Render2D.DrawTexture(_asset, rect, Color);
}
}
}
/// <summary>
/// Texture preview GUI control. Draws <see cref="FlaxEngine.Texture"/> in the UI and supports view moving/zooming.
/// Supports texture channels masking and color transformations.
/// </summary>
/// <seealso cref="TexturePreviewCustomBase" />
public class TexturePreview : TexturePreviewCustomBase
{
private TextureBase _asset;
/// <summary>
/// Gets or sets the texture to preview.
/// </summary>
public TextureBase Asset
{
get => _asset;
set
{
if (_asset != value)
{
_asset = value;
SetTexture(_asset);
}
}
}
/// <summary>
/// Initializes a new instance of the <see cref="TexturePreview"/> class.
/// </summary>
/// <param name="useWidgets">True if show viewport widgets.</param>
/// <inheritdoc />
public TexturePreview(bool useWidgets)
: base(useWidgets)
{
}
/// <inheritdoc />
protected override void CalculateTextureRect(out Rectangle rect)
{
CalculateTextureRect(_asset?.Size ?? new Vector2(100), Size, out rect);
}
/// <inheritdoc />
protected override void DrawTexture(ref Rectangle rect)
{
// Background
Render2D.FillRectangle(rect, Color.Gray);
// Check if has loaded asset
if (_asset && _asset.IsLoaded)
{
Render2D.DrawMaterial(_previewMaterial, rect);
}
}
}
/// <summary>
/// Sprite atlas preview GUI control. Draws <see cref="FlaxEngine.SpriteAtlas"/> in the UI and supports view moving/zomming.
/// Supports texture channels masking and color transformations.
/// </summary>
/// <seealso cref="TexturePreviewCustomBase" />
public class SpriteAtlasPreview : TexturePreviewCustomBase
{
private SpriteAtlas _asset;
/// <summary>
/// Gets or sets the sprite atlas to preview.
/// </summary>
public SpriteAtlas Asset
{
get => _asset;
set
{
if (_asset != value)
{
_asset = value;
SetTexture(_asset);
}
}
}
/// <summary>
/// Initializes a new instance of the <see cref="SpriteAtlasPreview"/> class.
/// </summary>
/// <param name="useWidgets">True if show viewport widgets.</param>
/// <inheritdoc />
public SpriteAtlasPreview(bool useWidgets)
: base(useWidgets)
{
}
/// <inheritdoc />
protected override void CalculateTextureRect(out Rectangle rect)
{
CalculateTextureRect(_asset?.Size ?? new Vector2(100), Size, out rect);
}
/// <inheritdoc />
protected override void DrawTexture(ref Rectangle rect)
{
// Background
Render2D.FillRectangle(rect, Color.Gray);
// Check if has loaded asset
if (_asset && _asset.IsLoaded)
{
Render2D.DrawMaterial(_previewMaterial, rect);
}
}
}
}

View File

@@ -0,0 +1,162 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.GUI.ContextMenu;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Viewport.Widgets
{
/// <summary>
/// Viewport Widget Button class.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.Control" />
[HideInEditor]
public class ViewportWidgetButton : Control
{
private string _text;
private SpriteHandle _icon;
private ContextMenu _cm;
private bool _checked;
private bool _autoCheck;
private bool _isMosueDown;
/// <summary>
/// Event fired when user toggles checked state.
/// </summary>
public event Action<ViewportWidgetButton> Toggled;
/// <summary>
/// Event fired when user click the button.
/// </summary>
public event Action<ViewportWidgetButton> Clicked;
/// <summary>
/// Gets or sets the text.
/// </summary>
public string Text
{
get => _text;
set
{
_text = value;
PerformLayout();
}
}
/// <summary>
/// Gets or sets a value indicating whether this <see cref="ViewportWidgetButton"/> is checked.
/// </summary>
public bool Checked
{
get => _checked;
set => _checked = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="ViewportWidgetButton"/> class.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="icon">The icon.</param>
/// <param name="contextMenu">The context menu.</param>
/// <param name="autoCheck">if set to <c>true</c> will be automatic checked on mouse click.</param>
public ViewportWidgetButton(string text, SpriteHandle icon, ContextMenu contextMenu = null, bool autoCheck = false)
: base(0, 0, CalculateButtonWidth(0, icon.IsValid), ViewportWidgetsContainer.WidgetsHeight)
{
_text = text;
_icon = icon;
_cm = contextMenu;
_autoCheck = autoCheck;
if (_cm != null)
_cm.VisibleChanged += CmOnVisibleChanged;
}
private void CmOnVisibleChanged(Control control)
{
if (_cm != null && !_cm.IsOpened)
{
if (HasParent && Parent.HasParent)
{
// Focus viewport
Parent.Parent.Focus();
}
}
}
private static float CalculateButtonWidth(float textWidth, bool hasIcon)
{
return (hasIcon ? ViewportWidgetsContainer.WidgetsIconSize : 0) + (textWidth > 0 ? textWidth + 8 : 0);
}
/// <inheritdoc />
public override void Draw()
{
// Cache data
var style = Style.Current;
const float iconSize = ViewportWidgetsContainer.WidgetsIconSize;
var iconRect = new Rectangle(0, (Height - iconSize) / 2, iconSize, iconSize);
var textRect = new Rectangle(0, 0, Width + 1, Height + 1);
// Check if is checked or mouse is over and auto check feature is enabled
if (_checked)
Render2D.FillRectangle(textRect, style.BackgroundSelected * (IsMouseOver ? 0.9f : 0.6f));
else if (_autoCheck && IsMouseOver)
Render2D.FillRectangle(textRect, style.BackgroundHighlighted);
// Check if has icon
if (_icon.IsValid)
{
// Draw icon
Render2D.DrawSprite(_icon, iconRect, style.Foreground);
// Update text rectangle
textRect.Location.X += iconSize;
textRect.Size.X -= iconSize;
}
// Draw text
Render2D.DrawText(style.FontMedium, _text, textRect, style.Foreground * (IsMouseOver ? 1.0f : 0.9f), TextAlignment.Center, TextAlignment.Center);
}
/// <inheritdoc />
public override bool OnMouseDown(Vector2 location, MouseButton button)
{
if (button == MouseButton.Left)
{
_isMosueDown = true;
}
if (_autoCheck)
{
// Toggle
Checked = !_checked;
Toggled?.Invoke(this);
}
_cm?.Show(this, new Vector2(-1, Height + 2));
return base.OnMouseDown(location, button);
}
/// <inheritdoc />
public override bool OnMouseUp(Vector2 location, MouseButton button)
{
if (button == MouseButton.Left && _isMosueDown)
{
_isMosueDown = false;
Clicked?.Invoke(this);
}
return base.OnMouseUp(location, button);
}
/// <inheritdoc />
public override void PerformLayout(bool force = false)
{
var style = Style.Current;
if (style != null && style.FontMedium)
Width = CalculateButtonWidth(style.FontMedium.MeasureText(_text).X, _icon.IsValid);
}
}
}

View File

@@ -0,0 +1,140 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Viewport.Widgets
{
/// <summary>
/// The viewport widget location.
/// </summary>
[HideInEditor]
public enum ViewportWidgetLocation
{
/// <summary>
/// The upper left corner of the parent container.
/// </summary>
UpperLeft,
/// <summary>
/// The upper right corner of the parent container.
/// </summary>
UpperRight,
}
/// <summary>
/// Viewport Widgets Container control
/// </summary>
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
[HideInEditor]
public class ViewportWidgetsContainer : ContainerControl
{
/// <summary>
/// The widgets margin.
/// </summary>
public const float WidgetsMargin = 4;
/// <summary>
/// The widgets height.
/// </summary>
public const float WidgetsHeight = 18;
/// <summary>
/// The widgets icon size.
/// </summary>
public const float WidgetsIconSize = 16;
/// <summary>
/// Gets the widget location.
/// </summary>
public ViewportWidgetLocation WidgetLocation { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ViewportWidgetsContainer"/> class.
/// </summary>
/// <param name="location">The location.</param>
public ViewportWidgetsContainer(ViewportWidgetLocation location)
: base(0, WidgetsMargin, 64, WidgetsHeight + 2)
{
AutoFocus = false;
WidgetLocation = location;
}
/// <inheritdoc />
public override void Draw()
{
// Cache data
var style = Style.Current;
var clientRect = new Rectangle(Vector2.Zero, Size);
// Draw background
Render2D.FillRectangle(clientRect, style.LightBackground * (IsMouseOver ? 0.3f : 0.2f));
base.Draw();
// Draw frame
Render2D.DrawRectangle(clientRect, style.BackgroundSelected * (IsMouseOver ? 1.0f : 0.6f));
}
/// <inheritdoc />
public override void OnChildResized(Control control)
{
base.OnChildResized(control);
PerformLayout();
}
/// <inheritdoc />
protected override void PerformLayoutBeforeChildren()
{
base.PerformLayoutBeforeChildren();
float x = 1;
for (int i = 0; i < _children.Count; i++)
{
var c = _children[i];
var w = c.Width;
c.Bounds = new Rectangle(x, 1, w, Height - 2);
x += w;
}
Width = x + 1;
}
/// <summary>
/// Arranges the widgets of the control.
/// </summary>
/// <param name="control">The control.</param>
public static void ArrangeWidgets(ContainerControl control)
{
// Arrange viewport widgets
const float margin = ViewportWidgetsContainer.WidgetsMargin;
float left = margin;
float right = control.Width - margin;
for (int i = 0; i < control.ChildrenCount; i++)
{
if (control.Children[i] is ViewportWidgetsContainer widget && widget.Visible)
{
float x;
switch (widget.WidgetLocation)
{
case ViewportWidgetLocation.UpperLeft:
x = left;
left += widget.Width + margin;
break;
case ViewportWidgetLocation.UpperRight:
x = right - widget.Width;
right = x - margin;
break;
default:
x = 0;
break;
}
widget.Location = new Vector2(x, margin);
}
}
}
}
}