Files
FlaxEngine/Source/Editor/GUI/Timeline/Tracks/CameraCutTrack.cs
W2Wizard d54efc73f1 Adapt to new spritehandle names
Lots of files! However only change was renaming the icons overall.
2021-05-13 15:16:45 +02:00

781 lines
27 KiB
C#

// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using FlaxEngine;
using FlaxEngine.GUI;
using Object = FlaxEngine.Object;
namespace FlaxEditor.GUI.Timeline.Tracks
{
/// <summary>
/// The timeline media that represents an camera cut media event.
/// </summary>
/// <seealso cref="FlaxEditor.GUI.Timeline.Media" />
public class CameraCutMedia : Media
{
private sealed class Proxy : ProxyBase<CameraCutTrack, CameraCutMedia>
{
public Proxy(CameraCutTrack track, CameraCutMedia media)
: base(track, media)
{
}
}
private Image[] _thumbnails = new Image[2];
/// <summary>
/// Initializes a new instance of the <see cref="CameraCutMedia"/> class.
/// </summary>
public CameraCutMedia()
{
ClipChildren = true;
}
/// <summary>
/// Updates the thumbnails.
/// </summary>
/// <param name="indices">The icon indices to update (null if update all of them).</param>
public void UpdateThumbnails(int[] indices = null)
{
if (Timeline == null)
return;
if (((CameraCutTrack)Track).Camera)
{
if (Timeline.CameraCutThumbnailRenderer == null)
Timeline.CameraCutThumbnailRenderer = new CameraCutThumbnailRenderer();
if (indices == null)
{
for (int i = 0; i < _thumbnails.Length; i++)
Timeline.CameraCutThumbnailRenderer.AddRequest(new CameraCutThumbnailRenderer.Request(this, i));
}
else
{
foreach (var i in indices)
Timeline.CameraCutThumbnailRenderer.AddRequest(new CameraCutThumbnailRenderer.Request(this, i));
}
}
else if (Timeline.CameraCutThumbnailRenderer != null)
{
if (indices == null)
{
for (int i = 0; i < _thumbnails.Length; i++)
{
var image = _thumbnails[i];
if (image?.Brush != null)
{
Timeline.CameraCutThumbnailRenderer.ReleaseThumbnail(((SpriteBrush)image.Brush).Sprite);
image.Brush = null;
}
}
}
else
{
foreach (var i in indices)
{
var image = _thumbnails[i];
if (image?.Brush != null)
{
Timeline.CameraCutThumbnailRenderer.ReleaseThumbnail(((SpriteBrush)image.Brush).Sprite);
image.Brush = null;
}
}
}
}
}
/// <summary>
/// Called when thumbnail rendering begins.
/// </summary>
/// <param name="task">The scene rendering task to customize.</param>
/// <param name="context">The GPU rendering context.</param>
/// <param name="req">The request data.</param>
public void OnThumbnailRenderingBegin(SceneRenderTask task, GPUContext context, ref CameraCutThumbnailRenderer.Request req)
{
RenderView view = new RenderView();
var track = (CameraCutTrack)Track;
Camera cam = track.Camera;
var viewport = new FlaxEngine.Viewport(Vector2.Zero, task.Buffers.Size);
Quaternion orientation = Quaternion.Identity;
view.Near = 10.0f;
view.Far = 20000.0f;
bool usePerspective = true;
float orthoScale = 1.0f;
float fov = 60.0f;
float customAspectRatio = 0.0f;
// Try to evaluate camera properties based on the initial camera state
if (cam)
{
view.Position = cam.Position;
orientation = cam.Orientation;
view.Near = cam.NearPlane;
view.Far = cam.FarPlane;
usePerspective = cam.UsePerspective;
orthoScale = cam.OrthographicScale;
fov = cam.FieldOfView;
customAspectRatio = cam.CustomAspectRatio;
}
// Try to evaluate camera properties based on the animated tracks
float time = req.ThumbnailIndex == 0 ? Start : Start + Duration;
foreach (var subTrack in track.SubTracks)
{
if (subTrack is MemberTrack memberTrack)
{
object value = memberTrack.Evaluate(time);
if (value != null)
{
// TODO: try to make it better
if (memberTrack.MemberName == "Position" && value is Vector3 asPosition)
view.Position = asPosition;
else if (memberTrack.MemberName == "Orientation" && value is Quaternion asRotation)
orientation = asRotation;
else if (memberTrack.MemberName == "NearPlane" && value is float asNearPlane)
view.Near = asNearPlane;
else if (memberTrack.MemberName == "FarPlane" && value is float asFarPlane)
view.Far = asFarPlane;
else if (memberTrack.MemberName == "UsePerspective" && value is bool asUsePerspective)
usePerspective = asUsePerspective;
else if (memberTrack.MemberName == "FieldOfView" && value is float asFieldOfView)
fov = asFieldOfView;
else if (memberTrack.MemberName == "CustomAspectRatio" && value is float asCustomAspectRatio)
customAspectRatio = asCustomAspectRatio;
else if (memberTrack.MemberName == "OrthographicScale" && value is float asOrthographicScale)
orthoScale = asOrthographicScale;
}
}
}
// Build view
view.Direction = Vector3.Forward * orientation;
if (usePerspective)
{
float aspect = customAspectRatio <= 0.0f ? viewport.AspectRatio : customAspectRatio;
view.Projection = Matrix.PerspectiveFov(fov * Mathf.DegreesToRadians, aspect, view.Near, view.Far);
}
else
{
view.Projection = Matrix.Ortho(viewport.Width * orthoScale, viewport.Height * orthoScale, view.Near, view.Far);
}
Vector3 target = view.Position + view.Direction;
var up = Vector3.Transform(Vector3.Up, orientation);
view.View = Matrix.LookAt(view.Position, target, up);
view.NonJitteredProjection = view.Projection;
view.TemporalAAJitter = Vector4.Zero;
view.ModelLODDistanceFactor = 100.0f;
view.Flags = ViewFlags.DefaultGame & ~(ViewFlags.MotionBlur | ViewFlags.EyeAdaptation);
view.UpdateCachedData();
task.View = view;
}
/// <summary>
/// Called when thumbnail rendering ends. The task output buffer contains ready frame.
/// </summary>
/// <param name="task">The scene rendering task to customize.</param>
/// <param name="context">The GPU rendering context.</param>
/// <param name="req">The request data.</param>
/// <param name="sprite">The thumbnail sprite.</param>
public void OnThumbnailRenderingEnd(SceneRenderTask task, GPUContext context, ref CameraCutThumbnailRenderer.Request req, ref SpriteHandle sprite)
{
var image = _thumbnails[req.ThumbnailIndex];
if (image == null)
{
if (req.ThumbnailIndex == 0)
{
image = new Image
{
AnchorPreset = AnchorPresets.MiddleLeft,
Parent = this,
Bounds = new Rectangle(2, 2, CameraCutThumbnailRenderer.Width, CameraCutThumbnailRenderer.Height),
};
}
else
{
image = new Image
{
AnchorPreset = AnchorPresets.MiddleRight,
Parent = this,
Bounds = new Rectangle(Width - 2 - CameraCutThumbnailRenderer.Width, 2, CameraCutThumbnailRenderer.Width, CameraCutThumbnailRenderer.Height),
};
}
image.UnlockChildrenRecursive();
_thumbnails[req.ThumbnailIndex] = image;
UpdateUI();
}
else if (image.Brush != null)
{
Timeline.CameraCutThumbnailRenderer.ReleaseThumbnail(((SpriteBrush)image.Brush).Sprite);
image.Brush = null;
}
image.Brush = new SpriteBrush(sprite);
}
private void UpdateUI()
{
var width = Mathf.Min(CameraCutThumbnailRenderer.Width, (Width - 6.0f) * 0.5f);
for (int i = 0; i < _thumbnails.Length; i++)
{
var image = _thumbnails[i];
if (image != null)
{
image.Width = width;
image.Visible = width >= 10.0f;
}
}
}
/// <inheritdoc />
public override void OnTimelineChanged(Track track)
{
base.OnTimelineChanged(track);
PropertiesEditObject = new Proxy(Track as CameraCutTrack, this);
UpdateThumbnails();
}
/// <inheritdoc />
protected override void OnStartFrameChanged()
{
base.OnStartFrameChanged();
UpdateThumbnails();
}
/// <inheritdoc />
protected override void OnDurationFramesChanged()
{
base.OnDurationFramesChanged();
UpdateThumbnails(new[] { 1 });
}
/// <inheritdoc />
protected override void OnSizeChanged()
{
base.OnSizeChanged();
UpdateUI();
}
/// <inheritdoc />
public override void OnTimelineShowContextMenu(ContextMenu.ContextMenu menu, Control controlUnderMouse)
{
base.OnTimelineShowContextMenu(menu, controlUnderMouse);
if (((CameraCutTrack)Track).Camera)
menu.AddButton("Refresh thumbnails", () => UpdateThumbnails());
}
/// <inheritdoc />
public override void OnDestroy()
{
Timeline?.CameraCutThumbnailRenderer?.RemoveRequest(this);
_thumbnails = null;
base.OnDestroy();
}
}
/// <summary>
/// The helper utility for rendering camera cuts tracks thumbnails.
/// </summary>
[HideInEditor]
public class CameraCutThumbnailRenderer
{
/// <summary>
/// The camera cut thumbnail rendering request.
/// </summary>
public struct Request : IEquatable<Request>
{
/// <summary>
/// The media.
/// </summary>
public CameraCutMedia Media;
/// <summary>
/// The thumbnail index.
/// </summary>
public int ThumbnailIndex;
/// <summary>
/// Initializes a new instance of the <see cref="Request"/> struct.
/// </summary>
/// <param name="media">The media.</param>
/// <param name="thumbnailIndex"> The index of the thumbnail.</param>
public Request(CameraCutMedia media, int thumbnailIndex)
{
Media = media;
ThumbnailIndex = thumbnailIndex;
}
/// <inheritdoc />
public bool Equals(Request other)
{
return Equals(Media, other.Media) && ThumbnailIndex == other.ThumbnailIndex;
}
/// <inheritdoc />
public override bool Equals(object obj)
{
return obj is Request other && Equals(other);
}
/// <inheritdoc />
public override int GetHashCode()
{
unchecked
{
return ((Media != null ? Media.GetHashCode() : 0) * 397) ^ ThumbnailIndex;
}
}
}
/// <summary>
/// The thumbnails atlas.
/// </summary>
private struct Atlas
{
/// <summary>
/// The atlas texture.
/// </summary>
public SpriteAtlas Texture;
/// <summary>
/// The slots usage flags.
/// </summary>
public BitArray SlotsUsage;
/// <summary>
/// The used slots count.
/// </summary>
public int Count;
/// <summary>
/// Gets a value indicating whether this instance is full.
/// </summary>
public bool IsFull => SlotsUsage.Length == Count;
}
/// <summary>
/// The thumbnail height.
/// </summary>
public static int Height => 64;
/// <summary>
/// The thumbnail width.
/// </summary>
public static int Width => (int)(Height * (16.0f / 9.0f));
private List<Request> _queue;
private SceneRenderTask _task;
private GPUTexture _output;
private List<Atlas> _atlases;
/// <summary>
/// Initializes a new instance of the <see cref="CameraCutThumbnailRenderer"/> class.
/// </summary>
public CameraCutThumbnailRenderer()
{
_queue = new List<Request>();
FlaxEngine.Scripting.Update += OnUpdate;
}
/// <summary>
/// Adds the request for thumbnail rendering.
/// </summary>
/// <param name="req">The request.</param>
public void AddRequest(Request req)
{
if (!_queue.Contains(req))
_queue.Add(req);
}
/// <summary>
/// Removes all the requests that are related to the given media.
/// </summary>
/// <param name="media">The media.</param>
public void RemoveRequest(CameraCutMedia media)
{
_queue.RemoveAll(x => x.Media == media);
// End rendering if queue is not empty
if (_queue.Count == 0 && _task != null && _task.Enabled)
_task.Enabled = false;
}
/// <summary>
/// Releases the thumbnail ans frees the sprite slot used by it.
/// </summary>
/// <param name="sprite">The sprite.</param>
public void ReleaseThumbnail(SpriteHandle sprite)
{
if (!sprite.IsValid)
return;
for (var i = 0; i < _atlases.Count; i++)
{
var atlas = _atlases[i];
if (atlas.Texture == sprite.Atlas)
{
atlas.Count--;
atlas.SlotsUsage[sprite.Index] = false;
_atlases[i] = atlas;
break;
}
}
}
/// <summary>
/// Releases object resources.
/// </summary>
public void Dispose()
{
FlaxEngine.Scripting.Update -= OnUpdate;
_queue.Clear();
_queue = null;
Object.Destroy(ref _task);
Object.Destroy(ref _output);
if (_atlases != null)
{
foreach (var atlas in _atlases)
Object.Destroy(atlas.Texture);
_atlases.Clear();
_atlases = null;
}
}
private void OnUpdate()
{
if (_queue.Count == 0 || (_task != null && _task.Enabled))
return;
// TODO: add delay when processing the requests to reduce perf impact (eg. 0.2s before actual rendering)
// Setup pipeline
if (_atlases == null)
_atlases = new List<Atlas>(4);
if (_output == null)
{
_output = GPUDevice.Instance.CreateTexture();
var desc = GPUTextureDescription.New2D(Width, Height, PixelFormat.R8G8B8A8_UNorm);
_output.Init(ref desc);
}
if (_task == null)
{
_task = Object.New<SceneRenderTask>();
_task.Output = _output;
_task.Begin += OnBegin;
_task.End += OnEnd;
}
// Kick off the rendering
_task.Enabled = true;
}
private void OnBegin(RenderTask task, GPUContext context)
{
// Setup
var req = _queue[0];
req.Media.OnThumbnailRenderingBegin((SceneRenderTask)task, context, ref req);
}
private void OnEnd(RenderTask task, GPUContext context)
{
// Pick the atlas or create a new one
int atlasIndex = -1;
for (int i = 0; i < _atlases.Count; i++)
{
if (!_atlases[i].IsFull)
{
atlasIndex = i;
break;
}
}
if (atlasIndex == -1)
{
// Setup configuration
var atlasSize = 1024;
var atlasFormat = PixelFormat.R8G8B8A8_UNorm;
var width = (float)Width;
var height = (float)Height;
var countX = Mathf.FloorToInt(atlasSize / width);
var countY = Mathf.FloorToInt(atlasSize / height);
var count = countX * countY;
// Create sprite atlas texture
var spriteAtlas = FlaxEngine.Content.CreateVirtualAsset<SpriteAtlas>();
var data = new byte[atlasSize * atlasSize * PixelFormatExtensions.SizeInBytes(atlasFormat)];
var initData = new TextureBase.InitData
{
Width = atlasSize,
Height = atlasSize,
ArraySize = 1,
Format = atlasFormat,
Mips = new[]
{
new TextureBase.InitData.MipData
{
Data = data,
RowPitch = data.Length / atlasSize,
SlicePitch = data.Length
},
},
};
spriteAtlas.Init(ref initData);
// Setup sprite atlas slots (each per thumbnail)
var thumbnailSizeUV = new Vector2(width / atlasSize, height / atlasSize);
for (int i = 0; i < count; i++)
{
var x = i % countX;
var y = i / countX;
var s = new Sprite
{
Name = string.Empty,
Area = new Rectangle(new Vector2(x, y) * thumbnailSizeUV, thumbnailSizeUV),
};
spriteAtlas.AddSprite(s);
}
// Add atlas to the cached ones
atlasIndex = _atlases.Count;
_atlases.Add(new Atlas
{
Texture = spriteAtlas,
SlotsUsage = new BitArray(count, false),
Count = 0,
});
}
// Skip ending if the atlas is not loaded yet (streaming backend uploads texture to GPU or sth)
var atlas = _atlases[atlasIndex];
if (atlas.Texture.ResidentMipLevels == 0)
return;
// Pick the sprite slot from the atlas
var spriteIndex = -1;
for (int i = 0; i < atlas.SlotsUsage.Count; i++)
{
if (atlas.SlotsUsage[i] == false)
{
atlas.SlotsUsage[i] = true;
spriteIndex = i;
break;
}
}
if (spriteIndex == -1)
throw new FlaxException();
atlas.Count++;
_atlases[atlasIndex] = atlas;
var sprite = new SpriteHandle(atlas.Texture, spriteIndex);
// Copy output frame to the sprite atlas slot
var spriteLocation = sprite.Location;
context.CopyTexture(atlas.Texture.Texture, 0, (uint)spriteLocation.X, (uint)spriteLocation.Y, 0, _output, 0);
// Link sprite to the UI
var req = _queue[0];
req.Media.OnThumbnailRenderingEnd((SceneRenderTask)task, context, ref req, ref sprite);
// End
_queue.RemoveAt(0);
task.Enabled = false;
}
}
/// <summary>
/// The timeline track for animating <see cref="FlaxEngine.Camera"/> objects.
/// </summary>
/// <seealso cref="ActorTrack" />
public class CameraCutTrack : ActorTrack
{
/// <summary>
/// Gets the archetype.
/// </summary>
/// <returns>The archetype.</returns>
public new static TrackArchetype GetArchetype()
{
return new TrackArchetype
{
TypeId = 16,
Name = "Camera Cut",
Create = options => new CameraCutTrack(ref options),
Load = LoadTrack,
Save = SaveTrack,
};
}
private static void LoadTrack(int version, Track track, BinaryReader stream)
{
var e = (CameraCutTrack)track;
e.ActorID = new Guid(stream.ReadBytes(16));
var m = e.TrackMedia;
m.StartFrame = stream.ReadInt32();
m.DurationFrames = stream.ReadInt32();
}
private static void SaveTrack(Track track, BinaryWriter stream)
{
var e = (CameraCutTrack)track;
stream.Write(e.ActorID.ToByteArray());
if (e.Media.Count != 0)
{
var m = e.TrackMedia;
stream.Write(m.StartFrame);
stream.Write(m.DurationFrames);
}
else
{
stream.Write(0);
stream.Write(track.Timeline.DurationFrames);
}
}
private Image _pilotCamera;
/// <summary>
/// Gets the camera object instance (it might be missing).
/// </summary>
public Camera Camera => Actor as Camera;
/// <summary>
/// Gets the camera track media.
/// </summary>
public CameraCutMedia TrackMedia
{
get
{
CameraCutMedia media;
if (Media.Count == 0)
{
media = new CameraCutMedia
{
StartFrame = 0,
DurationFrames = Timeline != null ? (int)(Timeline.FramesPerSecond * 2) : 60,
};
AddMedia(media);
}
else
{
media = (CameraCutMedia)Media[0];
}
return media;
}
}
/// <inheritdoc />
public CameraCutTrack(ref TrackCreateOptions options)
: base(ref options)
{
Height = CameraCutThumbnailRenderer.Height + 4 + 4;
// Pilot Camera button
const float buttonSize = 14;
var icons = Editor.Instance.Icons;
_pilotCamera = new Image
{
TooltipText = "Starts piloting camera (in scene edit window)",
AutoFocus = true,
AnchorPreset = AnchorPresets.MiddleRight,
IsScrollable = false,
Color = Style.Current.ForegroundGrey,
Margin = new Margin(1),
Brush = new SpriteBrush(icons.Camera64),
Offsets = new Margin(-buttonSize - 2 + _selectActor.Offsets.Left, buttonSize, buttonSize * -0.5f, buttonSize),
Parent = this,
};
_pilotCamera.Clicked += OnClickedPilotCamera;
}
private void OnClickedPilotCamera(Image image, MouseButton button)
{
if (button == MouseButton.Left)
{
var camera = Camera;
if (camera)
{
Expand();
var editWin = Editor.Instance.Windows.EditWin;
editWin.PilotActor(camera);
var time = Timeline.CurrentTime;
var hasPositionTrack = false;
var hasOrientationTrack = false;
foreach (var subTrack in SubTracks)
{
if (subTrack is MemberTrack memberTrack)
{
object value = memberTrack.Evaluate(time);
if (value != null)
{
if (memberTrack.MemberName == "Position" && value is Vector3 asPosition)
{
editWin.Viewport.ViewPosition = asPosition;
hasPositionTrack = true;
}
else if (memberTrack.MemberName == "Orientation" && value is Quaternion asRotation)
{
editWin.Viewport.ViewOrientation = asRotation;
hasOrientationTrack = true;
}
}
}
}
if (!hasPositionTrack)
AddPropertyTrack(camera.GetType().GetProperty("Position"));
if (!hasOrientationTrack)
AddPropertyTrack(camera.GetType().GetProperty("Orientation"));
}
}
}
/// <inheritdoc />
protected override void OnObjectExistenceChanged(object obj)
{
base.OnObjectExistenceChanged(obj);
TrackMedia.UpdateThumbnails();
}
/// <inheritdoc />
protected override bool IsActorValid(Actor actor)
{
return base.IsActorValid(actor) && actor is Camera;
}
/// <inheritdoc />
public override void OnSpawned()
{
// Ensure to have valid media added
// ReSharper disable once UnusedVariable
var media = TrackMedia;
base.OnSpawned();
}
/// <inheritdoc />
public override void OnDestroy()
{
_pilotCamera = null;
base.OnDestroy();
}
}
}