Skip to content

Instantly share code, notes, and snippets.

@tomkail
Last active February 12, 2024 11:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tomkail/54063e80a56574b207cceb100ee659b4 to your computer and use it in GitHub Desktop.
Save tomkail/54063e80a56574b207cceb100ee659b4 to your computer and use it in GitHub Desktop.
Renders a Rive asset to Unity UI
// RiveUIRenderer.cs created by Tom Kail. This file is licensed under the MIT License.
// Renders a Rive asset to Unity UI using a RenderTexture in a similar manner to RawImage.
// Supports pointer events and masking.
using UnityEngine;
using UnityEngine.Rendering;
using Rive;
using UnityEngine.EventSystems;
using UnityEngine.UI;
[ExecuteAlways]
[RequireComponent(typeof(CanvasRenderer))]
public class RiveUIRenderer : MaskableGraphic, IPointerDownHandler, IPointerUpHandler, ICanvasRaycastFilter {
public RenderTexture renderTexture { get; private set; }
// Todo - ability to downscale the rendertexture for performance.
// We might want an option to automatically do this when rendertexture is larger than recttransform
// public float renderTextureScale = 1;
Vector2Int targetRenderTextureSize {
get {
if (m_artboard == null) return Vector2Int.zero;
else return new Vector2Int(Mathf.RoundToInt(m_artboard.Width), Mathf.RoundToInt(m_artboard.Height));
// var targetSize = new Vector2Int(Mathf.RoundToInt(Mathf.Abs(rectTransform.rect.width * renderTextureScale)), Mathf.RoundToInt(Mathf.Abs(rectTransform.rect.height * renderTextureScale)));
}
}
public Asset asset;
public Fit fit = Fit.contain;
public Vector2 alignment = new Vector2(0,0);
Rive.RenderQueue m_renderQueue;
Rive.Renderer m_riveRenderer;
File m_file;
Artboard m_artboard;
StateMachine m_stateMachine;
CommandBuffer m_commandBuffer;
public StateMachine stateMachine => m_stateMachine;
public override Texture mainTexture {
get {
if (renderTexture == null) {
if (material != null && material.mainTexture != null) return material.mainTexture;
return s_WhiteTexture;
}
return renderTexture;
}
}
protected RiveUIRenderer() {
useLegacyMeshGeneration = false;
}
protected override void OnEnable() {
base.OnEnable();
if (asset == null) {
Clear();
return;
}
if (!isActiveAndEnabled) return;
#if UNITY_EDITOR
// This script can run in edit mode; but don't run it on assets!
if (UnityEditor.EditorUtility.IsPersistent(this)) return;
// OnValidate and OnEnable trigger this. We only want OnEnable to fire when we ACTUALLY enter play mode!
if (!UnityEditor.EditorApplication.isPlaying && UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) return;
#endif
LoadRiveAsset();
CreateRenderTexture();
InitializeRiveRenderer();
m_riveRenderer.Submit();
}
protected override void OnDisable() {
base.OnDisable();
Clear();
}
void Update() {
if (Application.isPlaying) {
if (m_stateMachine != null) {
m_stateMachine.PointerMove(GetArtboardPointerPosition(Input.mousePosition, GetCanvasEventCamera()));
m_stateMachine.Advance(Time.deltaTime);
}
}
m_riveRenderer?.Submit();
}
#if UNITY_EDITOR
protected override void OnValidate() {
Clear();
LoadRiveAsset();
if (m_file != null) {
CreateRenderTexture();
InitializeRiveRenderer();
}
base.OnValidate();
}
#endif
void Clear() {
DeinitializeRiveRenderer();
UnloadRiveAsset();
DestroyRenderTexture();
}
Camera GetCanvasEventCamera() {
var canvas = this.canvas;
var renderMode = canvas.renderMode;
if (renderMode == RenderMode.ScreenSpaceOverlay || (renderMode == RenderMode.ScreenSpaceCamera && canvas.worldCamera == null))
return null;
return canvas.worldCamera ?? Camera.main;
}
// This is only necessary once we allow the render texture size to change dynamically based on recttransform size
// protected override void OnRectTransformDimensionsChange() {
// base.OnRectTransformDimensionsChange();
// if (gameObject.activeInHierarchy && renderTexture != null) {
// if (renderTexture.width != targetRenderTextureSize.x || renderTexture.height != targetRenderTextureSize.y) {
// DeinitializeRiveRenderer();
// ReinitializeRenderTextureWithTargetSize();
// InitializeRiveRenderer();
// m_riveRenderer.Submit();
// SetMaterialDirty();
// }
// }
// }
/// <summary>
/// Adjust the scale of the Graphic to make it pixel-perfect.
/// </summary>
/// <remarks>
/// This means setting the RiveUIRenderer's RectTransform.sizeDelta to be equal to the Texture dimensions.
/// </remarks>
public override void SetNativeSize() {
Texture tex = mainTexture;
if (tex != null)
{
int w = Mathf.RoundToInt(tex.width);
int h = Mathf.RoundToInt(tex.height);
rectTransform.anchorMax = rectTransform.anchorMin;
rectTransform.sizeDelta = new Vector2(w, h);
}
}
/// Image's dimensions used for drawing. X = left, Y = bottom, Z = right, W = top.
Vector4 GetDrawingDimensions() {
var size = m_artboard == null ? Vector2.zero : new Vector2(m_artboard.Width, m_artboard.Height);
Rect r = GetPixelAdjustedRect();
var containerSize = r.size;
if (size.sqrMagnitude > 0.0f) containerSize = Resize(containerSize, size.x/size.y, fit);
var alignmentOffset = (r.size - containerSize) * (alignment+Vector2.one) * 0.5f;
var minX = r.x + alignmentOffset.x;
var minY = r.y + alignmentOffset.y;
return new Vector4(minX, minY, minX + containerSize.x, minY + containerSize.y);
}
static Vector2 Resize(Vector2 containerSize, float contentAspect, Fit scalingMode) {
if(float.IsNaN(contentAspect)) return containerSize;
if(scalingMode == Fit.fill) return containerSize;
float containerAspect = containerSize.x / containerSize.y;
if(float.IsNaN(containerAspect)) return containerSize;
bool fillToAtLeastContainerWidth = false;
bool fillToAtLeastContainerHeight = false;
if(scalingMode == Fit.fitWidth) fillToAtLeastContainerWidth = true;
else if(scalingMode == Fit.fitHeight) fillToAtLeastContainerHeight = true;
else if(scalingMode == Fit.cover) fillToAtLeastContainerWidth = fillToAtLeastContainerHeight = true;
Vector2 destRect = containerSize;
if(containerSize.x == Mathf.Infinity) {
destRect.x = containerSize.y * contentAspect;
} else if(containerSize.y == Mathf.Infinity) {
destRect.y = containerSize.x / contentAspect;
}
if (contentAspect > containerAspect) {
// wider than high keep the width and scale the height
var scaledHeight = containerSize.x / contentAspect;
if (fillToAtLeastContainerHeight) {
float resizePerc = containerSize.y / scaledHeight;
destRect.x = containerSize.x * resizePerc;
} else {
destRect.y = scaledHeight;
}
} else {
// higher than wide – keep the height and scale the width
var scaledWidth = containerSize.y * contentAspect;
if (fillToAtLeastContainerWidth) {
float resizePerc = containerSize.x / scaledWidth;
destRect.y = containerSize.y * resizePerc;
} else {
destRect.x = scaledWidth;
}
}
return destRect;
}
protected override void OnPopulateMesh(VertexHelper vh) {
Texture tex = mainTexture;
vh.Clear();
if (tex != null) {
Rect m_UVRect = RiveFlipYOnGraphicsDevice() ? new Rect(0f, 1f, 1f, -1f) : new Rect(0f, 0f, 1f, 1f);
Vector4 v = GetDrawingDimensions();
var scaleX = tex.width * tex.texelSize.x;
var scaleY = tex.height * tex.texelSize.y;
{
var color32 = color;
vh.AddVert(new Vector3(v.x, v.y), color32, new Vector2(m_UVRect.xMin * scaleX, m_UVRect.yMin * scaleY));
vh.AddVert(new Vector3(v.x, v.w), color32, new Vector2(m_UVRect.xMin * scaleX, m_UVRect.yMax * scaleY));
vh.AddVert(new Vector3(v.z, v.w), color32, new Vector2(m_UVRect.xMax * scaleX, m_UVRect.yMax * scaleY));
vh.AddVert(new Vector3(v.z, v.y), color32, new Vector2(m_UVRect.xMax * scaleX, m_UVRect.yMin * scaleY));
vh.AddTriangle(0, 1, 2);
vh.AddTriangle(2, 3, 0);
}
}
}
protected override void OnDidApplyAnimationProperties() {
SetMaterialDirty();
SetVerticesDirty();
SetRaycastDirty();
}
public void OnPointerDown(PointerEventData eventData) {
m_stateMachine?.PointerDown(GetArtboardPointerPosition(eventData.position, eventData.pressEventCamera));
}
public void OnPointerUp(PointerEventData eventData) {
m_stateMachine?.PointerUp(GetArtboardPointerPosition(eventData.position, eventData.pressEventCamera));
}
public virtual bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera) {
var artboardPointerPosition = GetArtboardPointerPosition(screenPoint, eventCamera);
return new Rect(0, 0, m_artboard.Width, m_artboard.Height).Contains(artboardPointerPosition);
}
void LoadRiveAsset() {
if (asset != null) {
m_file = Rive.File.Load(asset);
m_artboard = m_file.Artboard(0);
m_stateMachine = m_artboard?.StateMachine();
}
}
void UnloadRiveAsset() {
m_file = null;
m_artboard = null;
m_stateMachine = null;
}
void InitializeRiveRenderer() {
m_renderQueue = new Rive.RenderQueue(renderTexture);
m_riveRenderer = m_renderQueue.Renderer();
if (m_artboard != null && renderTexture != null) {
m_riveRenderer.Align(fit, new Alignment(alignment.x,alignment.y), m_artboard);
m_riveRenderer.Draw(m_artboard);
m_commandBuffer = new CommandBuffer();
m_commandBuffer.SetRenderTarget(renderTexture);
m_commandBuffer.ClearRenderTarget(true, true, UnityEngine.Color.clear, 0.0f);
m_riveRenderer.AddToCommandBuffer(m_commandBuffer);
}
}
// No idea if this is right; there's no examples I can find.
void DeinitializeRiveRenderer() {
m_commandBuffer?.Dispose();
m_commandBuffer = null;
m_renderQueue = null;
m_riveRenderer = null;
}
public static bool RiveFlipYOnGraphicsDevice() {
switch (UnityEngine.SystemInfo.graphicsDeviceType)
{
case UnityEngine.Rendering.GraphicsDeviceType.Metal:
case UnityEngine.Rendering.GraphicsDeviceType.Direct3D11:
return true;
default:
return false;
}
}
Vector2 GetArtboardPointerPosition(Vector2 screenPosition, Camera camera) {
RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPosition, camera, out Vector2 localPoint);
if (RiveFlipYOnGraphicsDevice()) {
localPoint = Rect.PointToNormalized(rectTransform.rect, localPoint);
localPoint = new Vector2(localPoint.x, 1f - localPoint.y);
localPoint = Rect.NormalizedToPoint(rectTransform.rect, localPoint);
}
return m_artboard.LocalCoordinate(localPoint, rectTransform.rect, fit, new Alignment(alignment.x, alignment.y));
}
void CreateRenderTexture () {
if(renderTexture != null) DestroyRenderTexture();
if (targetRenderTextureSize.x == 0 || targetRenderTextureSize.y == 0) {
Debug.LogWarning($"{GetType().Name}: Target size is 0, so not creating RenderTexture.", this);
return;
}
if (targetRenderTextureSize.x > 4096 || targetRenderTextureSize.y > 4096) {
Debug.LogWarning($"{GetType().Name}: Target size is more than 4096, so not creating RenderTexture. Remove this code is you wish to allow very large render textures.", this);
return;
}
renderTexture = new RenderTexture (targetRenderTextureSize.x, targetRenderTextureSize.y, 0, RenderTextureFormat.ARGB32) {
name = $"RenderTextureCreator {transform.HierarchyPath()}",
enableRandomWrite = true,
filterMode = FilterMode.Bilinear,
hideFlags = HideFlags.HideAndDontSave
};
}
void ReinitializeRenderTextureWithTargetSize() {
ReleaseRenderTexture();
if (targetRenderTextureSize.x == 0 || targetRenderTextureSize.y == 0) {
Debug.LogWarning($"{GetType().Name}: Target size is 0, so not creating RenderTexture.", this);
return;
}
if (targetRenderTextureSize.x > 4096 || targetRenderTextureSize.y > 4096) {
Debug.LogWarning($"{GetType().Name}: Target size is more than 4096, so not creating RenderTexture. Remove this code is you wish to allow very large render textures.", this);
return;
}
renderTexture.width = targetRenderTextureSize.x;
renderTexture.height = targetRenderTextureSize.y;
renderTexture.Create();
}
void ReleaseRenderTexture () {
if(renderTexture == null) return;
if(RenderTexture.active == renderTexture) RenderTexture.active = null;
renderTexture.Release();
}
void DestroyRenderTexture() {
if(renderTexture == null) return;
if(RenderTexture.active == renderTexture) RenderTexture.active = null;
if(Application.isPlaying) Destroy(renderTexture);
else DestroyImmediate(renderTexture);
renderTexture = null;
}
}
// Copy this into its own file in a folder marked Editor!
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(RiveUIRenderer), true)]
[CanEditMultipleObjects]
public class RiveUIRendererEditor : UnityEditor.UI.GraphicEditor {
// SerializedProperty _renderTextureScale;
SerializedProperty _asset;
SerializedProperty _fit;
SerializedProperty _alignment;
SerializedProperty m_Texture;
SerializedProperty m_UVRect;
GUIContent m_UVRectContent;
protected override void OnEnable()
{
base.OnEnable();
// _renderTextureScale = serializedObject.FindProperty("renderTextureScale");
_asset = serializedObject.FindProperty("asset");
_fit = serializedObject.FindProperty("fit");
_alignment = serializedObject.FindProperty("alignment");
SetShowNativeSize(true);
}
public override void OnInspectorGUI()
{
serializedObject.Update();
// EditorGUILayout.PropertyField(_renderTextureScale);
EditorGUILayout.PropertyField(_asset);
EditorGUILayout.PropertyField(_fit);
EditorGUILayout.PropertyField(_alignment);
AppearanceControlsGUI();
RaycastControlsGUI();
MaskableControlsGUI();
SetShowNativeSize(false);
NativeSizeButtonGUI();
serializedObject.ApplyModifiedProperties();
}
public override bool RequiresConstantRepaint() => true;
public override bool HasPreviewGUI() {
return (target as RiveUIRenderer)?.renderTexture != null;
}
public override void OnPreviewGUI(Rect r, GUIStyle background) {
var renderTexture = (target as RiveUIRenderer)?.renderTexture;
if (RiveUIRenderer.RiveFlipYOnGraphicsDevice()) {
RenderTextureDescriptor descriptor = new RenderTextureDescriptor(renderTexture.width, renderTexture.height, renderTexture.graphicsFormat, 0);
var tempRT = RenderTexture.GetTemporary(descriptor);
// This operation flips the Y axis
Matrix4x4 m = Matrix4x4.identity;
// The above Blit can't flip unless using a material, so we use Graphics.DrawTexture instead
GL.InvalidateState();
RenderTexture prevRT = RenderTexture.active;
RenderTexture.active = tempRT;
GL.Clear(false, true, Color.clear);
GL.PushMatrix();
GL.LoadPixelMatrix(0f, tempRT.width, 0f, tempRT.height);
Rect sourceRect = new Rect(0f, 0f, 1f, 1f);
// NOTE: not sure why we need to set y to -1, without this there is a 1px gap at the bottom
Rect destRect = new Rect(0f, -1f, renderTexture.width, renderTexture.height);
GL.MultMatrix(m);
Graphics.DrawTexture(destRect, renderTexture, sourceRect, 0, 0, 0, 0);
GL.PopMatrix();
GL.InvalidateState();
RenderTexture.active = prevRT;
EditorGUI.DrawTextureTransparent(r, tempRT, ScaleMode.ScaleToFit);
} else {
EditorGUI.DrawTextureTransparent(r, renderTexture, ScaleMode.ScaleToFit);
}
}
public override void OnPreviewSettings() {
var renderTexture = (target as RiveUIRenderer)?.renderTexture;
if (renderTexture == null) return;
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.LabelField(new GUIContent("Size"), GUILayout.Width(40));
EditorGUILayout.Vector2IntField(GUIContent.none, new Vector2Int(renderTexture.width, renderTexture.height), GUILayout.Width(120));
EditorGUI.EndDisabledGroup();
}
/// <summary>
/// Info String drawn at the bottom of the Preview
/// </summary>
public override string GetInfoString() {
RiveUIRenderer rawImage = target as RiveUIRenderer;
string text = $"RiveUIRenderer Size: {Mathf.RoundToInt(Mathf.Abs(rawImage.rectTransform.rect.width))}x{Mathf.RoundToInt(Mathf.Abs(rawImage.rectTransform.rect.height))}\nRenderTexture Size: {Mathf.RoundToInt(Mathf.Abs(rawImage.renderTexture.width))}x{Mathf.RoundToInt(Mathf.Abs(rawImage.renderTexture.height))}";
return text;
}
void SetShowNativeSize(bool instant) {
var renderTexture = (target as RiveUIRenderer)?.renderTexture;
base.SetShowNativeSize(renderTexture != null, instant);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment