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.

┌─────────────────────────────────────────────────────────┐
│ 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.
UIRender)UIRenderManager to a GameObject in your sceneAdd a RenderElement to your UXML:
<CupkekGames.Luna.RenderElement
name="MyRender"
RenderWidth="512"
RenderHeight="512" />Play a prefab on it from code:
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();You can render onto any VisualElement using the manager API directly:
VisualElement target = root.Q<VisualElement>("MyElement");
UIRenderHandle handle = UIRenderManager.Instance.Play(myPrefab, target);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.
Instantiates a copy of the prefab into the render slot. The copy is destroyed on release. Safe for auto-destroy particles.
// Via RenderElement
handle = renderElement.Play(prefab, settings);
// Via Manager
handle = UIRenderManager.Instance.Play(prefab, target, settings);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.
// Via RenderElement
handle = renderElement.PlayInstance(sceneObject, settings);
// Via Manager
handle = UIRenderManager.Instance.PlayInstance(sceneObject, target, settings);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.
// Via RenderElement
handle = renderElement.Observe(sceneObject, settings);
// Via Manager
handle = UIRenderManager.Instance.Observe(sceneObject, target, settings);Uses whatever content mode is configured in UIRenderSettings.ContentMode. Useful when the mode is set via the Inspector on a serialized UIRenderSettings asset.
handle = renderElement.Activate(content, settings);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.
VisualElement is created inside the target element (positioned at the configured anchor)aspect-ratio is set from the RenderTexture dimensions to prevent stretching// 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 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.
// 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 destroyedUIOverlaySettings settings = new UIOverlaySettings
{
Width = Length.Percent(50),
Height = Length.Percent(50)
};
UIRenderHandle handle = UIRenderManager.Instance.PlayOverlay(effectPrefab, myIcon, overlaySettings: settings);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);UIOverlaySettings settings = new UIOverlaySettings
{
PreserveAspectRatio = false
};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.
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.| Property | Type | Default | Description |
|---|---|---|---|
Width | Length | 100% | Overlay width relative to parent |
Height | Length | 100% | Overlay height relative to parent |
OffsetX | Length | 0 | Horizontal offset from anchor position |
OffsetY | Length | 0 | Vertical offset from anchor position |
PreserveAspectRatio | bool | true | Lock overlay aspect ratio to RenderTexture dimensions |
OverflowVisible | bool | false | Set parent element's overflow to Visible while overlay is active. Restores original value on release |
Anchor | OverlayAnchor | Center | Anchor position within the parent element |
The OverlayAnchor enum controls where the overlay is positioned relative to its parent:
| Value | Position |
|---|---|
Center | Centered (default) |
TopLeft | Top-left corner |
TopCenter | Top edge, centered |
TopRight | Top-right corner |
MiddleLeft | Left edge, vertically centered |
MiddleRight | Right edge, vertically centered |
BottomLeft | Bottom-left corner |
BottomCenter | Bottom edge, centered |
BottomRight | Bottom-right corner |
OffsetX and OffsetY are applied as margin offsets on top of the anchor position.
UIOverlaySettings settings = new UIOverlaySettings
{
Anchor = OverlayAnchor.TopRight,
Width = Length.Percent(60),
Height = Length.Percent(60),
OverflowVisible = true
};Use the fluent Builder for cleaner creation:
UIOverlaySettings settings = new UIOverlaySettings.Builder()
.WithSize(50, LengthUnit.Percent)
.WithAnchor(OverlayAnchor.TopRight)
.WithOffset(10f, -5f)
.WithOverflowVisible(true)
.WithPreserveAspectRatio(true)
.Build();| Method | Description |
|---|---|
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:
UIOverlaySettings modified = existingSettings.ToBuilder()
.WithAnchor(OverlayAnchor.BottomCenter)
.Build();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.
[SerializeField] private SerializedOverlaySettings _overlaySettings;
void Start()
{
UIOverlaySettings settings = _overlaySettings.Build();
UIRenderManager.Instance.PlayOverlay(prefab, target, renderSettings, settings);
}| Field | Type | Default | Description |
|---|---|---|---|
Anchor | OverlayAnchor | Center | Anchor position within the parent element |
Size Unit | LengthUnit | Percent | Unit for width and height (Percent or Pixel) |
Width | float | 100 | Overlay width in the configured unit |
Height | float | 100 | Overlay height in the configured unit |
Offset X | float | 0 | Horizontal offset from anchor (pixels) |
Offset Y | float | 0 | Vertical offset from anchor (pixels) |
Preserve Aspect Ratio | bool | true | Lock aspect ratio to RenderTexture dimensions |
Overflow Visible | bool | false | Set parent overflow to Visible while overlay is active |
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.
Renders 3D content into a UI element's background. Supports three activation modes:
| Mode | Description |
|---|---|
Play | Instantiates a prefab clone into a render slot |
PlayInstance | Reparents an existing scene GameObject into a render slot (borrowed) |
Observe | Watches a scene GameObject in-place without moving it |
| Property | Description |
|---|---|
| Content | The GameObject to render (prefab for Play, scene instance for PlayInstance/Observe) |
| Mode | UIRenderActivationMode — Play, PlayInstance, or Observe |
| Render Settings | Embedded UIRenderSettings (resolution, camera, auto-release, etc.) |
| UI View Component | Optional. Resolves the target element from a UIView hierarchy |
| UI Document | Fallback UIDocument reference (hidden when UIViewComponent is set) |
| Element Name | Name of the target VisualElement. Leave empty for root |
| Fade Trigger | Which UIView fade event triggers activation (visible when UIViewComponent is set) |
| Activate On Enable | Auto-activate when the component is enabled (visible when no UIViewComponent) |
| Release On Disable | Auto-release when the component is disabled |
| On Activated / On Released | UnityEvent callbacks |
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;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.
| Property | Description |
|---|---|
| Content | The GameObject to render |
| Use Instance | When true, uses PlayOverlayInstance (borrow). When false, uses PlayOverlay (clone) |
| Render Settings | Embedded UIRenderSettings |
| Overlay Settings | Embedded SerializedOverlaySettings — anchor, size unit (Percent/Pixel), width, height, offsets, aspect ratio, overflow |
| UI View Component | Optional UIView target resolution |
| UI Document | Fallback UIDocument reference |
| Element Name | Target element name |
| Fade Trigger | UIView fade event trigger |
| Play On Enable | Auto-play when enabled |
| Release On Disable | Auto-release when disabled |
| On Activated / On Released | UnityEvent callbacks |
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;Both components resolve the target VisualElement using the same priority chain:
ParentElement, then queries by element namerootVisualElement, then queries by element nameWhen 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.
Per-request configuration for the render system. Use the Builder for fluent creation or serialize directly in the Inspector.
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();UIRenderSettings modified = existingSettings.ToBuilder()
.WithResolution(1024, 1024)
.WithAutoFrame(true)
.Build();| Property | Type | Default | Description |
|---|---|---|---|
Resolution | Vector2Int | 256×256 | RenderTexture dimensions in pixels |
CameraMode | UIRenderCameraMode | Perspective | Camera projection mode |
CameraSize | float | 60 | Orthographic size or field of view |
CameraDistance | float | 3 | Distance from content to camera |
CameraBackgroundColor | Color | clear | Camera background. Use Color.clear for transparent |
Depth | int | 16 | RenderTexture depth buffer bits (0, 16, 24, 32) |
AutoRelease | bool | true | Auto-return slot to pool when content finishes |
AutoReleaseTimeout | float | 0 | Fallback timeout in seconds. 0 = no timeout |
Layer | int | -1 | Layer for render isolation. -1 uses manager default |
AntiAliasing | int | 1 | MSAA sample count (1, 2, 4, 8) |
HDR | bool | false | Use HDR RenderTexture format |
RenderMode | UIRenderMode | Continuous | Camera update mode |
AutoFrame | bool | false | Auto-frame camera to fit content bounds |
AutoFramePadding | float | 1.2 | Extra padding for auto-framing (1.0 = tight) |
CameraOffset | Vector3 | zero | Offset 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 |
CameraRotation | Vector3 | zero | Euler angles for camera orbit around content |
Debug | bool | false | Emit debug logs during activation and framing |
ContentMode | UIRenderContentMode | PrefabContentMode | Strategy for content activation/deactivation |
| Mode | Description |
|---|---|
Continuous | Camera renders every frame |
Once | Camera renders a single frame then disables. RT retains the image |
Manual | Camera only renders when handle.Render() is called |
The caller-facing handle returned by all activation methods. Controls the lifecycle of a single render instance.
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// 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);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.
// 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.
// 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();// Fired when the handle is released and the slot returns to pool
handle.OnReleased += () => Debug.Log("Released!");// 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();UIRenderHandle implements IDisposable for use in using blocks:
using (UIRenderHandle handle = renderElement.Play(prefab))
{
// handle is released when scope exits
}Custom VisualElement designed for 3D rendering. Provides UXML attributes for render resolution and convenience methods.
<CupkekGames.Luna.RenderElement
name="MyRender"
RenderWidth="512"
RenderHeight="512"
style="width: 100%; flex-grow: 1;" />| Attribute | Type | Default | Description |
|---|---|---|---|
RenderWidth | int | 256 | RenderTexture width in pixels |
RenderHeight | int | 256 | RenderTexture height in pixels |
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.
Singleton manager that owns the render slot pool. Add this to a GameObject in your scene.
| Property | Type | Default | Description |
|---|---|---|---|
Default Layer | int | 31 | Layer index for render isolation |
Default Capacity | int | 3 | Initial number of pre-created slots |
Max Capacity | int | 10 | Maximum pool size |
Prewarm | bool | true | Pre-create slots on Awake |
The render system requires a dedicated Unity layer to isolate render slot cameras from the main scene:
Default Layer (e.g. layer 31 → UIRender)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.
The system uses pluggable effect handlers to track content lifecycle (e.g. when particles finish). Built-in handlers:
| Handler | Tracks | Condition Define |
|---|---|---|
ParticleEffectHandler | ParticleSystem | Always available |
VFXEffectHandler | VisualEffect (VFX Graph) | UNITY_VFX |
Extend UIRenderEffectHandler and register via the static factory:
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());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();
}
}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 is a standalone static utility class for creating overlay VisualElements. While used internally by PlayOverlay, it is designed for reuse by other features.
// 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);The Components sample includes two demo scenes:
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)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction