Files

449 lines
18 KiB
C#

#if CINEMACHINE_UGUI
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Serialization;
namespace Unity.Cinemachine
{
/// <summary>
/// An add-on module for CinemachineCamera that places an image in screen space
/// over the camera's output.
/// </summary>
[SaveDuringPlay]
[AddComponentMenu("Cinemachine/Procedural/Extensions/Cinemachine Storyboard")]
[ExecuteAlways]
[DisallowMultipleComponent]
[HelpURL(Documentation.BaseURL + "manual/CinemachineStoryboard.html")]
public class CinemachineStoryboard : CinemachineExtension
{
/// <summary>
/// If checked, all storyboards are globally muted
/// </summary>
[Tooltip("If checked, all storyboards are globally muted")]
public static bool s_StoryboardGlobalMute;
/// <summary>
/// If checked, the specified image will be displayed as an overlay over the virtual camera's output
/// </summary>
[Tooltip("If checked, the specified image will be displayed as an overlay over the virtual camera's output")]
[FormerlySerializedAs("m_ShowImage")]
public bool ShowImage = true;
/// <summary>
/// The image to display
/// </summary>
[Tooltip("The image to display")]
[FormerlySerializedAs("m_Image")]
public Texture Image;
/// <summary>How to fit the image in the frame, in the event that the aspect ratios don't match</summary>
public enum FillStrategy
{
/// <summary>Image will be as large as possible on the screen, without being cropped</summary>
BestFit,
/// <summary>Image will be cropped if necessary so that the screen is entirely filled</summary>
CropImageToFit,
/// <summary>Image will be stretched to cover any aspect mismatch with the screen</summary>
StretchToFit
};
/// <summary>
/// How to handle differences between image aspect and screen aspect
/// </summary>
[Tooltip("How to handle differences between image aspect and screen aspect")]
[FormerlySerializedAs("m_Aspect")]
public FillStrategy Aspect = FillStrategy.BestFit;
/// <summary>
/// The opacity of the image. 0 is transparent, 1 is opaque
/// </summary>
[Tooltip("The opacity of the image. 0 is transparent, 1 is opaque")]
[FormerlySerializedAs("m_Alpha")]
[Range(0, 1)]
public float Alpha = 1;
/// <summary>
/// The screen-space position at which to display the image. Zero is center
/// </summary>
[Tooltip("The screen-space position at which to display the image. Zero is center")]
[FormerlySerializedAs("m_Center")]
public Vector2 Center = Vector2.zero;
/// <summary>
/// The screen-space rotation to apply to the image
/// </summary>
[Tooltip("The screen-space rotation to apply to the image")]
[FormerlySerializedAs("m_Rotation")]
public Vector3 Rotation = Vector3.zero;
/// <summary>
/// The screen-space scaling to apply to the image
/// </summary>
[Tooltip("The screen-space scaling to apply to the image")]
[FormerlySerializedAs("m_Scale")]
public Vector2 Scale = Vector3.one;
/// <summary>
/// If checked, X and Y scale are synchronized
/// </summary>
[Tooltip("If checked, X and Y scale are synchronized")]
[FormerlySerializedAs("m_SyncScale")]
public bool SyncScale = true;
/// <summary>
/// If checked, Camera transform will not be controlled by this virtual camera
/// </summary>
[Tooltip("If checked, Camera transform will not be controlled by this virtual camera")]
[FormerlySerializedAs("m_MuteCamera")]
public bool MuteCamera;
/// <summary>
/// Wipe the image on and off horizontally
/// </summary>
[Range(-1, 1)]
[Tooltip("Wipe the image on and off horizontally")]
[FormerlySerializedAs("m_SplitView")]
public float SplitView = 0f;
/// <summary>
/// The render mode of the canvas on which the storyboard is drawn.
/// </summary>
[Tooltip("The render mode of the canvas on which the storyboard is drawn.")]
[FormerlySerializedAs("m_RenderMode")]
public StoryboardRenderMode RenderMode = StoryboardRenderMode.ScreenSpaceOverlay;
/// <summary>
/// Allows ordering canvases to render on top or below other canvases.
/// </summary>
[Tooltip("Allows ordering canvases to render on top or below other canvases.")]
[FormerlySerializedAs("m_SortingOrder")]
public int SortingOrder;
/// <summary>
/// How far away from the camera is the storyboard's canvas generated.
/// </summary>
[Tooltip("How far away from the camera is the Canvas generated.")]
[FormerlySerializedAs("m_PlaneDistance")]
public float PlaneDistance = 100;
class CanvasInfo
{
public GameObject Canvas;
public Canvas CanvasComponent;
public CinemachineBrain CanvasParent;
public RectTransform Viewport; // for mViewport clipping
public UnityEngine.UI.RawImage RawImage;
}
List<CanvasInfo> m_CanvasInfo = new List<CanvasInfo>();
/// <summary>Callback to display the image</summary>
/// <param name="vcam">The virtual camera being processed</param>
/// <param name="stage">The current pipeline stage</param>
/// <param name="state">The current virtual camera state</param>
/// <param name="deltaTime">The current applicable deltaTime</param>
protected override void PostPipelineStageCallback(
CinemachineVirtualCameraBase vcam,
CinemachineCore.Stage stage, ref CameraState state, float deltaTime)
{
// Apply to this vcam only, not the children
if (vcam != ComponentOwner || stage != CinemachineCore.Stage.Finalize)
return;
UpdateRenderCanvas();
if (ShowImage)
state.AddCustomBlendable(new CameraState.CustomBlendableItems.Item { Custom = this, Weight = 1});
if (MuteCamera)
state.BlendHint |= CameraState.BlendHints.NoTransform | CameraState.BlendHints.NoLens;
}
/// <summary>
/// Camera render modes supported by CinemachineStoryboard.
/// </summary>
public enum StoryboardRenderMode
{
/// <summary>
/// Renders in camera screen space. This means, that the storyboard is going to be displayed in front of
/// any objects in the scene. Equivalent to Unity's RenderMode.ScreenSpaceOverlay.
/// </summary>
ScreenSpaceOverlay = UnityEngine.RenderMode.ScreenSpaceOverlay,
/// <summary>
/// Render using the vcam on which the storyboard is on. This is useful, if you'd like to render the
/// storyboard at a specific distance from the vcam. Equivalent to Unity's RenderMode.ScreenSpaceCamera.
/// </summary>
ScreenSpaceCamera = UnityEngine.RenderMode.ScreenSpaceCamera
};
void UpdateRenderCanvas()
{
for (int i = 0; i < m_CanvasInfo.Count; ++i)
{
if (m_CanvasInfo[i] == null || m_CanvasInfo[i].CanvasComponent == null)
m_CanvasInfo.RemoveAt(i--);
else
{
m_CanvasInfo[i].CanvasComponent.renderMode = (RenderMode)RenderMode;
m_CanvasInfo[i].CanvasComponent.planeDistance = PlaneDistance;
m_CanvasInfo[i].CanvasComponent.sortingOrder = SortingOrder;
}
}
}
/// <summary>Connect to virtual camera. Adds/removes listener</summary>
/// <param name="connect">True if connecting, false if disconnecting</param>
protected override void ConnectToVcam(bool connect)
{
base.ConnectToVcam(connect);
CinemachineCore.CameraUpdatedEvent.RemoveListener(CameraUpdatedCallback);
if (connect)
CinemachineCore.CameraUpdatedEvent.AddListener(CameraUpdatedCallback);
else
DestroyCanvas();
}
string CanvasName => "_CM_canvas" + gameObject.GetInstanceID();
void CameraUpdatedCallback(CinemachineBrain brain)
{
var owner = ComponentOwner;
if (owner == null)
return;
var showIt = enabled && ShowImage && CinemachineCore.IsLive(owner);
var channel = (uint)owner.OutputChannel;
if (s_StoryboardGlobalMute || ((uint)brain.ChannelMask & channel) == 0)
showIt = false;
var ci = LocateMyCanvas(brain, showIt);
if (ci != null && ci.Canvas != null)
ci.Canvas.SetActive(showIt);
}
CanvasInfo LocateMyCanvas(CinemachineBrain parent, bool createIfNotFound)
{
CanvasInfo ci = null;
for (int i = 0; ci == null && i < m_CanvasInfo.Count; ++i)
if (m_CanvasInfo[i] != null && m_CanvasInfo[i].CanvasParent == parent)
ci = m_CanvasInfo[i];
if (createIfNotFound)
{
if (ci == null)
{
ci = new CanvasInfo() { CanvasParent = parent };
int numChildren = parent.transform.childCount;
for (int i = 0; ci.Canvas == null && i < numChildren; ++i)
{
var child = parent.transform.GetChild(i) as RectTransform;
if (child != null && child.name == CanvasName)
{
ci.Canvas = child.gameObject;
var kids = ci.Canvas.GetComponentsInChildren<RectTransform>();
ci.Viewport = kids.Length > 1 ? kids[1] : null; // 0 is mCanvas
ci.RawImage = ci.Canvas.GetComponentInChildren<UnityEngine.UI.RawImage>();
ci.CanvasComponent = ci.Canvas.GetComponent<Canvas>();
}
}
m_CanvasInfo.Add(ci);
}
if (ci.Canvas == null || ci.Viewport == null || ci.RawImage == null || ci.CanvasComponent == null)
CreateCanvas(ci);
}
return ci;
}
void CreateCanvas(CanvasInfo ci)
{
ci.Canvas = new GameObject(CanvasName, typeof(RectTransform));
ci.Canvas.layer = gameObject.layer;
ci.Canvas.hideFlags = HideFlags.HideAndDontSave;
ci.Canvas.transform.SetParent(ci.CanvasParent.transform);
#if UNITY_EDITOR
// Workaround for Unity bug case Case 1004117
CanvasesAndTheirOwners.AddCanvas(ci.Canvas, this);
#endif
var c = ci.CanvasComponent = ci.Canvas.AddComponent<Canvas>();
c.renderMode = (RenderMode)RenderMode;
c.sortingOrder = SortingOrder;
c.planeDistance = PlaneDistance;
c.worldCamera = ci.CanvasParent.OutputCamera;
var go = new GameObject("Viewport", typeof(RectTransform));
go.transform.SetParent(ci.Canvas.transform);
ci.Viewport = (RectTransform)go.transform;
go.AddComponent<UnityEngine.UI.RectMask2D>();
go = new GameObject("RawImage", typeof(RectTransform));
go.transform.SetParent(ci.Viewport.transform);
ci.RawImage = go.AddComponent<UnityEngine.UI.RawImage>();
}
void DestroyCanvas()
{
int numBrains = CinemachineBrain.ActiveBrainCount;
for (int i = 0; i < numBrains; ++i)
{
var parent = CinemachineBrain.GetActiveBrain(i);
int numChildren = parent.transform.childCount;
for (int j = numChildren - 1; j >= 0; --j)
{
var child = parent.transform.GetChild(j) as RectTransform;
if (child != null && child.name == CanvasName)
{
var canvas = child.gameObject;
RuntimeUtility.DestroyObject(canvas);
#if UNITY_EDITOR
// Workaround for Unity bug case Case 1004117
CanvasesAndTheirOwners.RemoveCanvas(canvas);
#endif
}
}
}
m_CanvasInfo.Clear();
}
void PlaceImage(CanvasInfo ci, float alpha)
{
if (ci.RawImage != null && ci.Viewport != null)
{
var screen = new Rect(0, 0, Screen.width, Screen.height);
if (ci.CanvasParent.OutputCamera != null)
screen = ci.CanvasParent.OutputCamera.pixelRect;
screen.x -= (float)Screen.width/2;
screen.y -= (float)Screen.height/2;
// Apply Split View
var wipeAmount = -Mathf.Clamp(SplitView, -1, 1) * screen.width;
var pos = screen.center;
pos.x -= wipeAmount/2;
ci.Viewport.localPosition = pos;
ci.Viewport.localRotation = Quaternion.identity;
ci.Viewport.localScale = Vector3.one;
ci.Viewport.ForceUpdateRectTransforms();
ci.Viewport.sizeDelta = new Vector2(screen.width + 1 - Mathf.Abs(wipeAmount), screen.height + 1);
var scale = Vector2.one;
if (Image != null
&& Image.width > 0 && Image.width > 0
&& screen.width > 0 && screen.height > 0)
{
float f = (screen.height * Image.width) / (screen.width * Image.height);
switch (Aspect)
{
case FillStrategy.BestFit:
if (f >= 1)
scale.y /= f;
else
scale.x *= f;
break;
case FillStrategy.CropImageToFit:
if (f >= 1)
scale.x *= f;
else
scale.y /= f;
break;
case FillStrategy.StretchToFit:
break;
}
}
scale.x *= Scale.x;
scale.y *= SyncScale ? Scale.x : Scale.y;
ci.RawImage.texture = Image;
var tintColor = Color.white;
tintColor.a = Alpha * alpha;
ci.RawImage.color = tintColor;
pos = new Vector2(screen.width * Center.x, screen.height * Center.y);
pos.x += wipeAmount/2;
ci.RawImage.rectTransform.localPosition = pos;
ci.RawImage.rectTransform.localRotation = Quaternion.Euler(Rotation);
ci.RawImage.rectTransform.localScale = scale;
ci.RawImage.rectTransform.ForceUpdateRectTransforms();
ci.RawImage.rectTransform.sizeDelta = screen.size;
}
}
static void StaticBlendingHandler(CinemachineBrain brain)
{
var state = brain.State;
int numBlendables = state.GetNumCustomBlendables();
for (int i = 0; i < numBlendables; ++i)
{
var b = state.GetCustomBlendable(i);
var src = b.Custom as CinemachineStoryboard;
if (src != null && src.ComponentOwner != null) // in case it was deleted
{
bool showIt = true;
var channel = (uint)src.ComponentOwner.OutputChannel;
if (s_StoryboardGlobalMute || ((uint)brain.ChannelMask & channel) == 0)
showIt = false;
var ci = src.LocateMyCanvas(brain, showIt);
if (ci != null)
src.PlaceImage(ci, b.Weight);
}
}
}
#if UNITY_EDITOR
[UnityEditor.InitializeOnLoad]
class EditorInitialize { static EditorInitialize() { InitializeModule(); } }
#endif
[RuntimeInitializeOnLoadMethod]
static void InitializeModule()
{
CinemachineCore.CameraUpdatedEvent.RemoveListener(StaticBlendingHandler);
CinemachineCore.CameraUpdatedEvent.AddListener(StaticBlendingHandler);
}
#if UNITY_EDITOR
// Workaround for the Unity bug where OnDestroy doesn't get called if Undo
// bug case Case 1004117
[UnityEditor.InitializeOnLoad]
class CanvasesAndTheirOwners
{
static Dictionary<UnityEngine.Object, UnityEngine.Object> s_CanvasesAndTheirOwners;
static CanvasesAndTheirOwners()
{
UnityEditor.Undo.undoRedoPerformed -= OnUndoRedoPerformed;
UnityEditor.Undo.undoRedoPerformed += OnUndoRedoPerformed;
}
static void OnUndoRedoPerformed()
{
if (s_CanvasesAndTheirOwners != null)
{
List<UnityEngine.Object> toDestroy = null;
var iter = s_CanvasesAndTheirOwners.GetEnumerator();
while (iter.MoveNext())
{
var v = iter.Current;
if (v.Value == null)
{
toDestroy ??= new List<UnityEngine.Object>();
toDestroy.Add(v.Key);
}
}
for (int i = 0; toDestroy != null && i < toDestroy.Count; ++i)
{
var o = toDestroy[i];
RemoveCanvas(o);
RuntimeUtility.DestroyObject(o);
}
}
}
public static void RemoveCanvas(UnityEngine.Object canvas)
{
if (s_CanvasesAndTheirOwners != null && s_CanvasesAndTheirOwners.ContainsKey(canvas))
s_CanvasesAndTheirOwners.Remove(canvas);
}
public static void AddCanvas(UnityEngine.Object canvas, UnityEngine.Object owner)
{
s_CanvasesAndTheirOwners ??= new ();
s_CanvasesAndTheirOwners.Add(canvas, owner);
}
}
#endif
}
}
#endif