Last active
September 27, 2024 02:28
-
-
Save tomkail/54063e80a56574b207cceb100ee659b4 to your computer and use it in GitHub Desktop.
Renders a Rive asset to Unity UI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 Rive; | |
using UnityEditor; | |
using UnityEngine; | |
using UnityEngine.EventSystems; | |
using UnityEngine.Rendering; | |
using UnityEngine.UI; | |
using Color = UnityEngine.Color; | |
using Renderer = Rive.Renderer; | |
using RenderQueue = Rive.RenderQueue; | |
[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(0,0); | |
RenderQueue m_renderQueue; | |
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; | |
} | |
// When enabling the component, reload the Rive asset and initialize the renderer. | |
// We may only want to do this on Start instead? | |
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 (EditorUtility.IsPersistent(this)) return; | |
// OnValidate and OnEnable trigger this. We only want OnEnable to fire when we ACTUALLY enter play mode! | |
if (!EditorApplication.isPlaying && EditorApplication.isPlayingOrWillChangePlaymode) return; | |
#endif | |
LoadRiveAsset(); | |
CreateRenderTexture(); | |
InitializeRiveRenderer(); | |
m_riveRenderer.Submit(); | |
} | |
// Clear the renderer when disabled. | |
protected override void OnDisable() { | |
base.OnDisable(); | |
Clear(); | |
} | |
// TO TEST | |
// This is necessary to prevent memory leaks when using [ExecuteAlways] | |
// protected override void OnDestroy() { | |
// base.OnDestroy(); | |
// Clear(); | |
// } | |
// Reset the current Rive animation. | |
// There's no proper support for this in the API so it works by reloading the Rive asset and reinitializing the renderer. | |
public void ResetRive() { | |
Clear(); | |
if (!isActiveAndEnabled) return; | |
LoadRiveAsset(); | |
CreateRenderTexture(); | |
InitializeRiveRenderer(); | |
m_riveRenderer.Submit(); | |
} | |
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 = 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 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, 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 (SystemInfo.graphicsDeviceType) | |
{ | |
case GraphicsDeviceType.Metal: | |
case 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; | |
} | |
RenderTextureDescriptor descriptor = new RenderTextureDescriptor(targetRenderTextureSize.x, targetRenderTextureSize.y, RenderTextureFormat.ARGB32, 0) { | |
enableRandomWrite = true | |
}; | |
renderTexture = new RenderTexture (descriptor) { | |
name = $"{GetType().Name} RT ({asset.name})", | |
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; | |
RenderTexture tempRT; | |
protected override void OnEnable() | |
{ | |
base.OnEnable(); | |
// _renderTextureScale = serializedObject.FindProperty("renderTextureScale"); | |
_asset = serializedObject.FindProperty("asset"); | |
_fit = serializedObject.FindProperty("fit"); | |
_alignment = serializedObject.FindProperty("alignment"); | |
SetShowNativeSize(true); | |
} | |
protected override void OnDisable() { | |
base.OnDisable(); | |
if(tempRT != null) RenderTexture.ReleaseTemporary(tempRT); | |
} | |
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); | |
if(tempRT == null) | |
tempRT = RenderTexture.GetTemporary(descriptor); | |
else if (tempRT != null && tempRT.width != renderTexture.width || tempRT.height != renderTexture.height) { | |
RenderTexture.ReleaseTemporary(tempRT); | |
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); | |
} | |
} |
I’m sorry I don’t have an android device. You should ask the Rive
developers on their discord, I only wrote this because they didn’t have it
already, and they probably do need something like this script in their code.
…On Thu, Sep 26, 2024 at 16:34 Dong Yihui ***@***.***> wrote:
***@***.**** commented on this gist.
------------------------------
hi, this code is very useful for us, but It cannot work on Android, only
IOS.
Unity: 2022.3.42f1c1
Rive: v0.1.276
Run on android phone just can see a black screen.
image.png (view on web)
<https://gist.github.com/user-attachments/assets/d41675d4-21b4-4cb7-8044-bdc563069b85>
can you have a look about this issue? or Is there some setting problem?
we are not use vulkan
image.png (view on web)
<https://gist.github.com/user-attachments/assets/1f25f03f-7276-4c22-8714-cb4f62b08783>
—
Reply to this email directly, view it on GitHub
<https://gist.github.com/tomkail/54063e80a56574b207cceb100ee659b4#gistcomment-5208660>
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAJR3UFGOHGKH67DJOZHBOTZYQSPZBFKMF2HI4TJMJ2XIZLTSKBKK5TBNR2WLJDUOJ2WLJDOMFWWLO3UNBZGKYLEL5YGC4TUNFRWS4DBNZ2F6YLDORUXM2LUPGBKK5TBNR2WLJDHNFZXJJDOMFWWLK3UNBZGKYLEL52HS4DFVRZXKYTKMVRXIX3UPFYGLK2HNFZXIQ3PNVWWK3TUUZ2G64DJMNZZDAVEOR4XAZNEM5UXG5FFOZQWY5LFVEYTEOBSGAZDMOJXU52HE2LHM5SXFJTDOJSWC5DF>
.
You are receiving this email because you authored the thread.
Triage notifications on the go with GitHub Mobile for iOS
<https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
or Android
<https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>
.
ok, thanks for your quickly response.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
hi, this code is very useful for us, but It cannot work on Android, only IOS.
Unity: 2022.3.42f1c1
Rive: v0.1.276
Run on android phone just can see a black screen.

can you have a look about this issue? or Is there some setting problem?

we are not use vulkan