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.

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
  • 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 absolutely, centered)
  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);

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 };

UIOverlaySettings Reference

PropertyTypeDefaultDescription
WidthLength100%Overlay width relative to parent
HeightLength100%Overlay height relative to parent
OffsetXLength0Horizontal offset from center
OffsetYLength0Vertical offset from center
PreserveAspectRatiobooltrueLock overlay aspect ratio to RenderTexture dimensions

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
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);

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

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, Release, Frame, and Debug
  • UIRenderOverlayDemo: Showcases the PlayOverlay feature with three configurations — default full-size overlay, custom 50% size, and offset position

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