Luna provides a pooled Camera → RenderTexture → UI Toolkit pipeline for rendering any 3D content (particle systems, models, VFX Graph effects) directly onto UI elements. The system manages isolated render slots, handles lifecycle automatically, and supports multiple content modes for different use cases.

UI Render Demo - Modes UI Render Demo - Overlay

Features

  • Pooled Architecture: Pre-warmed pool of Camera + RenderTexture slots with automatic grow/shrink
  • Multiple Content Modes: Prefab instantiation, scene instance borrowing, and in-place observation
  • RenderElement: Custom VisualElement with built-in render resolution and convenience API
  • PlayOverlay: Layer 3D effects on top of existing UI elements without replacing their visuals
  • PlayOverlayInstance: Overlay with an existing scene instance instead of a prefab copy
  • Overlay Anchoring: Position overlays at 9 anchor points within the parent element
  • Handle Replay: Re-instantiate content in the same slot without pool churn
  • Pluggable Effect Handlers: Strategy pattern for particle, VFX, and custom effect lifecycle tracking
  • Auto-Release: Slots automatically return to pool when effects finish playing
  • Camera Controls: Auto-framing, runtime camera/content transforms, orthographic and perspective modes
  • Debug Tools: RT-to-PNG export, detailed logging of camera, bounds, and slot state

Architecture Overview

┌─────────────────────────────────────────────────────────┐ │ UIRenderManager │ │ (Singleton, pool owner) │ ├─────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │ │ │ UIRenderSlot │ │ UIRenderSlot │ │ ... │ │ │ │ Camera + RT │ │ Camera + RT │ │ │ │ │ └──────┬───────┘ └──────┬───────┘ └────────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │UIRenderHandle│ │UIRenderHandle│ ← caller-facing │ │ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ └─────────┼──────────────────┼────────────────────────────┘ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │RenderElement│ │VisualElement│ │(custom VE) │ │(any element)│ └─────────────┘ └─────────────┘

Each UIRenderSlot contains an isolated Camera rendering to its own RenderTexture on a dedicated layer. The UIRenderHandle is the caller-facing API for stopping, releasing, and adjusting the render at runtime.

Quick Start

Setup

  1. Create a dedicated layer in Edit > Project Settings > Tags and Layers (e.g. layer 31 named UIRender)
  2. Add UIRenderManager to a GameObject in your scene
  3. Configure pool settings in the Inspector

Add a RenderElement to your UXML:

xml
<CupkekGames.Luna.RenderElement name="MyRender" RenderWidth="512" RenderHeight="512" />

Play a prefab on it from code:

csharp
RenderElement renderElement = root.Q<RenderElement>("MyRender"); UIRenderHandle handle = renderElement.Play(myParticlePrefab); // Stop effects (auto-releases when particles drain) handle.Stop(); // Or release immediately handle.Release();

Using Any VisualElement

You can render onto any VisualElement using the manager API directly:

csharp
VisualElement target = root.Q<VisualElement>("MyElement"); UIRenderHandle handle = UIRenderManager.Instance.Play(myPrefab, target);

Content Modes

The system uses a strategy pattern for content lifecycle. Each mode determines how content is placed into a render slot and what happens on release.

Prefab Mode (Play)

Instantiates a copy of the prefab into the render slot. The copy is destroyed on release. Safe for auto-destroy particles.

csharp
// Via RenderElement handle = renderElement.Play(prefab, settings); // Via Manager handle = UIRenderManager.Instance.Play(prefab, target, settings);

Instance Mode (PlayInstance)

Borrows an existing scene GameObject by reparenting it into the render slot and changing its layer. The original parent, position, rotation, scale, and layer are restored on release. The object is not destroyed.

csharp
// Via RenderElement handle = renderElement.PlayInstance(sceneObject, settings); // Via Manager handle = UIRenderManager.Instance.PlayInstance(sceneObject, target, settings);

Observe Mode

Watches a scene GameObject in-place without moving, reparenting, or changing its layer. A camera is positioned to render the object where it sits and follows it every frame.

csharp
// Via RenderElement handle = renderElement.Observe(sceneObject, settings); // Via Manager handle = UIRenderManager.Instance.Observe(sceneObject, target, settings);

Activate (Generic)

Uses whatever content mode is configured in UIRenderSettings.ContentMode. Useful when the mode is set via the Inspector on a serialized UIRenderSettings asset.

csharp
handle = renderElement.Activate(content, settings);

PlayOverlay

PlayOverlay renders 3D effects as an overlay on top of an existing UI element — without replacing the element's background. This is ideal for layering particle effects over icons, buttons, or cards.

How It Works

  1. A transparent child VisualElement is created inside the target element (positioned at the configured anchor)
  2. The RenderTexture is applied to this overlay child — the original element's visuals remain untouched
  3. The overlay's aspect-ratio is set from the RenderTexture dimensions to prevent stretching
  4. When the handle is released, the overlay is automatically removed from the hierarchy

Basic Usage

csharp
// Default: 100% size, centered, aspect-ratio preserved UIRenderHandle handle = UIRenderManager.Instance.PlayOverlay(effectPrefab, myIcon); // Via RenderElement (independent of main render handle) UIRenderHandle overlayHandle = renderElement.PlayOverlay(effectPrefab);

PlayOverlayInstance

PlayOverlayInstance works like PlayOverlay but borrows an existing scene GameObject instead of instantiating a prefab copy. The instance is reparented into the render slot and restored to its original parent on release.

csharp
// Overlay an existing scene object onto a UI element UIRenderHandle handle = UIRenderManager.Instance.PlayOverlayInstance( existingParticleSystem, myIcon, settings, overlaySettings ); // On release, the instance is returned to its original parent — not destroyed

Custom Size

csharp
UIOverlaySettings settings = new UIOverlaySettings { Width = Length.Percent(50), Height = Length.Percent(50) }; UIRenderHandle handle = UIRenderManager.Instance.PlayOverlay(effectPrefab, myIcon, overlaySettings: settings);

Custom Position

csharp
UIOverlaySettings settings = new UIOverlaySettings { OffsetX = new Length(30, LengthUnit.Pixel), OffsetY = new Length(-20, LengthUnit.Pixel) }; UIRenderHandle handle = UIRenderManager.Instance.PlayOverlay(effectPrefab, myIcon, overlaySettings: settings);

Without Aspect Ratio

csharp
UIOverlaySettings settings = new UIOverlaySettings { PreserveAspectRatio = false };

Overflow Visible

By default, the overlay is clipped by the parent element's overflow USS property. If the overlay (especially with PreserveAspectRatio) is larger than the parent bounds, it will be cut off. Set OverflowVisible to automatically switch the parent's overflow to Visible while the overlay is active. The original overflow value is restored when the handle is released.

csharp
UIOverlaySettings settings = new UIOverlaySettings { OverflowVisible = true }; UIRenderHandle handle = UIRenderManager.Instance.PlayOverlay(effectPrefab, myElement, overlaySettings: settings); // When handle.Release() is called, the parent's overflow is restored to its original value.

UIOverlaySettings Reference

PropertyTypeDefaultDescription
WidthLength100%Overlay width relative to parent
HeightLength100%Overlay height relative to parent
OffsetXLength0Horizontal offset from anchor position
OffsetYLength0Vertical offset from anchor position
PreserveAspectRatiobooltrueLock overlay aspect ratio to RenderTexture dimensions
OverflowVisibleboolfalseSet parent element's overflow to Visible while overlay is active. Restores original value on release
AnchorOverlayAnchorCenterAnchor position within the parent element

OverlayAnchor

The OverlayAnchor enum controls where the overlay is positioned relative to its parent:

ValuePosition
CenterCentered (default)
TopLeftTop-left corner
TopCenterTop edge, centered
TopRightTop-right corner
MiddleLeftLeft edge, vertically centered
MiddleRightRight edge, vertically centered
BottomLeftBottom-left corner
BottomCenterBottom edge, centered
BottomRightBottom-right corner

OffsetX and OffsetY are applied as margin offsets on top of the anchor position.

csharp
UIOverlaySettings settings = new UIOverlaySettings { Anchor = OverlayAnchor.TopRight, Width = Length.Percent(60), Height = Length.Percent(60), OverflowVisible = true };

UIOverlaySettings Builder

Use the fluent Builder for cleaner creation:

csharp
UIOverlaySettings settings = new UIOverlaySettings.Builder() .WithSize(50, LengthUnit.Percent) .WithAnchor(OverlayAnchor.TopRight) .WithOffset(10f, -5f) .WithOverflowVisible(true) .WithPreserveAspectRatio(true) .Build();

Builder Methods

MethodDescription
WithWidth(Length)Set overlay width
WithWidth(float, LengthUnit)Set overlay width with unit
WithHeight(Length)Set overlay height
WithHeight(float, LengthUnit)Set overlay height with unit
WithSize(float, LengthUnit)Set both width and height to the same value
WithSize(float, float, LengthUnit)Set width and height separately
WithOffsetX(Length) / WithOffsetX(float)Set horizontal offset
WithOffsetY(Length) / WithOffsetY(float)Set vertical offset
WithOffset(float, float)Set both offsets at once
WithAnchor(OverlayAnchor)Set the anchor position
WithPreserveAspectRatio(bool)Toggle aspect ratio preservation
WithOverflowVisible(bool)Toggle overflow visibility

You can also modify existing settings:

csharp
UIOverlaySettings modified = existingSettings.ToBuilder() .WithAnchor(OverlayAnchor.BottomCenter) .Build();

SerializedOverlaySettings

SerializedOverlaySettings is an Inspector-serializable class that mirrors UIOverlaySettings. Use it as a [SerializeField] on any MonoBehaviour or ScriptableObject to configure overlay settings from the Inspector. Call Build() at runtime to produce a UIOverlaySettings instance.

csharp
[SerializeField] private SerializedOverlaySettings _overlaySettings; void Start() { UIOverlaySettings settings = _overlaySettings.Build(); UIRenderManager.Instance.PlayOverlay(prefab, target, renderSettings, settings); }

Serialized Fields

FieldTypeDefaultDescription
AnchorOverlayAnchorCenterAnchor position within the parent element
Size UnitLengthUnitPercentUnit for width and height (Percent or Pixel)
Widthfloat100Overlay width in the configured unit
Heightfloat100Overlay height in the configured unit
Offset Xfloat0Horizontal offset from anchor (pixels)
Offset Yfloat0Vertical offset from anchor (pixels)
Preserve Aspect RatiobooltrueLock aspect ratio to RenderTexture dimensions
Overflow VisibleboolfalseSet parent overflow to Visible while overlay is active

No-Code Components

Luna provides two MonoBehaviour components that expose the UIRender system in the Inspector — no C# required. They follow the same target-resolution pattern as TransitionAnimator: assign a UIViewComponent or UIDocument, specify an element name, and choose when to activate.

UIRenderActivator

Renders 3D content into a UI element's background. Supports three activation modes:

ModeDescription
PlayInstantiates a prefab clone into a render slot
PlayInstanceReparents an existing scene GameObject into a render slot (borrowed)
ObserveWatches a scene GameObject in-place without moving it

Inspector Properties

PropertyDescription
ContentThe GameObject to render (prefab for Play, scene instance for PlayInstance/Observe)
ModeUIRenderActivationMode — Play, PlayInstance, or Observe
Render SettingsEmbedded UIRenderSettings (resolution, camera, auto-release, etc.)
UI View ComponentOptional. Resolves the target element from a UIView hierarchy
UI DocumentFallback UIDocument reference (hidden when UIViewComponent is set)
Element NameName of the target VisualElement. Leave empty for root
Fade TriggerWhich UIView fade event triggers activation (visible when UIViewComponent is set)
Activate On EnableAuto-activate when the component is enabled (visible when no UIViewComponent)
Release On DisableAuto-release when the component is disabled
On Activated / On ReleasedUnityEvent callbacks

Code API

csharp
UIRenderActivator activator = GetComponent<UIRenderActivator>(); // Activate on configured target activator.Activate(); // Activate on a specific element activator.Activate(myVisualElement); // Re-instantiate content in the same slot activator.Replay(); // Release the handle activator.Release(); // Check state bool active = activator.IsActive; UIRenderHandle handle = activator.Handle;

UIOverlayActivator

Renders 3D content as a transparent overlay on top of a UI element — ideal for fire-and-forget VFX like particle bursts on buttons or cards.

Inspector Properties

PropertyDescription
ContentThe GameObject to render
Use InstanceWhen true, uses PlayOverlayInstance (borrow). When false, uses PlayOverlay (clone)
Render SettingsEmbedded UIRenderSettings
Overlay SettingsEmbedded SerializedOverlaySettings — anchor, size unit (Percent/Pixel), width, height, offsets, aspect ratio, overflow
UI View ComponentOptional UIView target resolution
UI DocumentFallback UIDocument reference
Element NameTarget element name
Fade TriggerUIView fade event trigger
Play On EnableAuto-play when enabled
Release On DisableAuto-release when disabled
On Activated / On ReleasedUnityEvent callbacks

Code API

csharp
UIOverlayActivator overlay = GetComponent<UIOverlayActivator>(); // Play on configured target overlay.Play(); // Play on a specific element overlay.Play(myVisualElement); // Re-instantiate content in the same overlay slot overlay.Replay(); // Release the overlay overlay.Release(); // Check state bool active = overlay.IsActive; UIRenderHandle handle = overlay.Handle;

Target Resolution

Both components resolve the target VisualElement using the same priority chain:

  1. UIViewComponent → uses ParentElement, then queries by element name
  2. UIDocument → uses rootVisualElement, then queries by element name
  3. GetComponent / GetComponentInParent → auto-discovers a UIDocument on the same GameObject or parents

When a UIViewComponent is assigned, the Fade Trigger field appears and Activate On Enable / UI Document are hidden — activation is driven by UIView lifecycle events instead.

UIRenderSettings

Per-request configuration for the render system. Use the Builder for fluent creation or serialize directly in the Inspector.

Builder API

csharp
UIRenderSettings settings = new UIRenderSettings.Builder() .WithResolution(512, 512) .WithCameraMode(UIRenderCameraMode.Perspective) .WithCameraSize(60f) .WithCameraDistance(3f) .WithCameraBackgroundColor(Color.clear) .WithAutoRelease(true) .WithAutoReleaseTimeout(5f) .WithAntiAliasing(4) .WithHDR(true) .WithRenderMode(UIRenderMode.Continuous) .WithAutoFrame(true) .WithAutoFramePadding(1.2f) .WithCameraOffset(Vector3.zero) .WithCameraRotation(new Vector3(15f, 30f, 0f)) .WithDebug(true) .Build();

Modify Existing Settings

csharp
UIRenderSettings modified = existingSettings.ToBuilder() .WithResolution(1024, 1024) .WithAutoFrame(true) .Build();

Settings Reference

PropertyTypeDefaultDescription
ResolutionVector2Int256×256RenderTexture dimensions in pixels
CameraModeUIRenderCameraModePerspectiveCamera projection mode
CameraSizefloat60Orthographic size or field of view
CameraDistancefloat3Distance from content to camera
CameraBackgroundColorColorclearCamera background. Use Color.clear for transparent
Depthint16RenderTexture depth buffer bits (0, 16, 24, 32)
AutoReleasebooltrueAuto-return slot to pool when content finishes
AutoReleaseTimeoutfloat0Fallback timeout in seconds. 0 = no timeout
Layerint-1Layer for render isolation. -1 uses manager default
AntiAliasingint1MSAA sample count (1, 2, 4, 8)
HDRboolfalseUse HDR RenderTexture format
RenderModeUIRenderModeContinuousCamera update mode
AutoFrameboolfalseAuto-frame camera to fit content bounds
AutoFramePaddingfloat1.2Extra padding for auto-framing (1.0 = tight)
CameraOffsetVector3zeroOffset applied to camera look-at target. When used with AutoFrame, the offset is applied after bounds are calculated — shifting the framing center without affecting bounds computation
CameraRotationVector3zeroEuler angles for camera orbit around content
DebugboolfalseEmit debug logs during activation and framing
ContentModeUIRenderContentModePrefabContentModeStrategy for content activation/deactivation

Render Modes

ModeDescription
ContinuousCamera renders every frame
OnceCamera renders a single frame then disables. RT retains the image
ManualCamera only renders when handle.Render() is called

UIRenderHandle

The caller-facing handle returned by all activation methods. Controls the lifecycle of a single render instance.

Properties

csharp
bool IsActive // Whether the handle is currently active and rendering RenderTexture RenderTexture // The active RenderTexture being written to UIRenderSettings Settings // The settings used for this render instance

Lifecycle Methods

csharp
// Stop all effects. Auto-releases when effects drain. // If no trackable effects exist, releases immediately. handle.Stop(); // Immediately release the handle and return the slot to pool. handle.Release(); // Toggle auto-release at runtime handle.SetAutoRelease(true);

Replay

Re-instantiates the content in the same render slot without returning it to the pool. The overlay element (if any) is preserved. Useful for restarting particle effects or VFX without pool churn.

csharp
// Restart the effect in the same slot handle.Replay();

Note: Replay() requires the handle to still be active. It destroys the current content, re-instantiates from the original source, and reapplies the RenderTexture to the target element.

Camera Controls

csharp
// Auto-frame camera to fit content bounds handle.FrameContent(padding: 1.2f, offset: Vector3.zero); // Runtime camera adjustments handle.SetCameraDistance(5f); handle.SetCameraSize(45f); handle.SetCameraLocalPosition(new Vector3(0, 1, -3)); handle.SetCameraLocalRotation(Quaternion.Euler(15, 30, 0)); // Runtime content adjustments handle.SetContentLocalPosition(Vector3.zero); handle.SetContentLocalRotation(Quaternion.identity); // Manual render (only for UIRenderMode.Manual) handle.Render();

Events

csharp
// Fired when the handle is released and the slot returns to pool handle.OnReleased += () => Debug.Log("Released!");

Debug Methods

csharp
// Save current RenderTexture to PNG (Editor/Development builds only) handle.DebugSaveRT("debug_output.png"); // Log detailed info about camera, content, bounds, and RT handle.DebugLogInfo();

IDisposable

UIRenderHandle implements IDisposable for use in using blocks:

csharp
using (UIRenderHandle handle = renderElement.Play(prefab)) { // handle is released when scope exits }

RenderElement

Custom VisualElement designed for 3D rendering. Provides UXML attributes for render resolution and convenience methods.

UXML Usage

xml
<CupkekGames.Luna.RenderElement name="MyRender" RenderWidth="512" RenderHeight="512" style="width: 100%; flex-grow: 1;" />

UXML Attributes

AttributeTypeDefaultDescription
RenderWidthint256RenderTexture width in pixels
RenderHeightint256RenderTexture height in pixels

Convenience Methods

csharp
RenderElement element = root.Q<RenderElement>("MyRender"); // Play a prefab UIRenderHandle handle = element.Play(prefab, settings); // Play an existing scene instance UIRenderHandle handle = element.PlayInstance(instance, settings); // Observe a scene object in-place UIRenderHandle handle = element.Observe(target, settings); // Play an overlay effect (independent of main handle) UIRenderHandle overlayHandle = element.PlayOverlay(effectPrefab, settings, overlaySettings); // Activate with settings-defined content mode UIRenderHandle handle = element.Activate(content, settings); // Stop / Release element.Stop(); element.Release(); // Frame content element.FrameContent(1.2f); // Access current handle UIRenderHandle current = element.CurrentHandle;

When a RenderElement is detached from the panel (removed from the visual tree), it automatically releases any active handle.

UIRenderManager

Singleton manager that owns the render slot pool. Add this to a GameObject in your scene.

Inspector Properties

PropertyTypeDefaultDescription
Default Layerint31Layer index for render isolation
Default Capacityint3Initial number of pre-created slots
Max Capacityint10Maximum pool size
PrewarmbooltruePre-create slots on Awake

Layer Setup

The render system requires a dedicated Unity layer to isolate render slot cameras from the main scene:

  1. Open Edit > Project Settings > Tags and Layers
  2. Assign a name to the layer configured in Default Layer (e.g. layer 31 → UIRender)
  3. Ensure your main camera's culling mask excludes this layer

URP / HDRP Warning: If you are using the Universal Render Pipeline (URP) or HD Render Pipeline (HDRP), you must also ensure the UIRender layer is included in the Filtering > Opaque/Transparent Layer Mask of your active Universal Renderer Data (or Custom Pass volume for HDRP). The slot cameras use this layer to render content — if the renderer asset excludes it, nothing will appear in the RenderTexture even though the camera and layer are configured correctly.

Effect Handlers

The system uses pluggable effect handlers to track content lifecycle (e.g. when particles finish). Built-in handlers:

HandlerTracksCondition Define
ParticleEffectHandlerParticleSystemAlways available
VFXEffectHandlerVisualEffect (VFX Graph)UNITY_VFX

Custom Effect Handlers

Extend UIRenderEffectHandler and register via the static factory:

csharp
public class MyCustomEffectHandler : UIRenderEffectHandler { public override string DisplayName => "MyEffect"; public override bool CanHandle(GameObject root) { return root.GetComponentInChildren<MyComponent>() != null; } public override void Initialize(GameObject root) { // Cache references } public override void Stop() { // Stop effects } public override bool IsAlive() { // Return true while effect is still running return false; } } // Register at startup UIRenderEffectHandler.Register(() => new MyCustomEffectHandler());

Example: Particle Effect on UI Icon

csharp
public class GoldIconEffect : MonoBehaviour { [SerializeField] private UIDocument _uiDocument; [SerializeField] private GameObject _starDustPrefab; private UIRenderHandle _overlayHandle; void Start() { VisualElement goldIcon = _uiDocument.rootVisualElement.Q("GoldIcon"); // Play star dust overlay — gold icon stays visible underneath _overlayHandle = UIRenderManager.Instance.PlayOverlay(_starDustPrefab, goldIcon); } void OnDestroy() { _overlayHandle?.Release(); } }

Example: 3D Character Preview

csharp
public class CharacterPreview : MonoBehaviour { [SerializeField] private UIDocument _uiDocument; [SerializeField] private GameObject _characterPrefab; void Start() { RenderElement preview = _uiDocument.rootVisualElement.Q<RenderElement>("CharacterPreview"); UIRenderSettings settings = new UIRenderSettings.Builder() .WithResolution(1024, 1024) .WithCameraMode(UIRenderCameraMode.Perspective) .WithCameraDistance(2.5f) .WithCameraRotation(new Vector3(10f, 180f, 0f)) .WithAutoFrame(true) .WithAntiAliasing(4) .WithAutoRelease(false) // Keep rendering until manually released .Build(); UIRenderHandle handle = preview.Play(_characterPrefab, settings); } }

UIOverlay (Standalone Utility)

UIOverlay is a standalone static utility class for creating overlay VisualElements. While used internally by PlayOverlay, it is designed for reuse by other features.

csharp
// Create an overlay VisualElement overlay = UIOverlay.Create(parentElement, new UIOverlaySettings { Width = Length.Percent(80), OffsetY = new Length(-10, LengthUnit.Pixel) }); // Apply aspect ratio from a RenderTexture UIOverlay.ApplyAspectRatio(overlay, rtWidth, rtHeight); // Remove overlay UIOverlay.Remove(overlay);

Demo Scenes

The Components sample includes two demo scenes:

  • UIRenderDemo: Demonstrates all four content modes (Prefab, Instance, Generic, Observe) with interactive controls for Play, Stop, Replay, Release, Frame, and Debug
  • UIRenderOverlayDemo: Showcases the PlayOverlay feature with five configurations — default full-size overlay, custom 50% size, offset position, anchored overlay (TopRight with overflow visible), and an instance overlay using PlayOverlayInstance. Includes a Replay All button to re-trigger all overlays.

Import the Components sample to explore these demos.

Settings

Theme

Light

Contrast

Material

Dark

Dim

Material Dark

System

Sidebar(Light & Contrast only)

Light
Dark

Font Family

DM Sans

Wix

Inclusive Sans

AR One Sans

Direction

LTR
RTL