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 absolutely, centered)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);UIOverlaySettings 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
};| 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 center |
OffsetY | Length | 0 | Vertical offset from center |
PreserveAspectRatio | bool | true | Lock overlay aspect ratio to RenderTexture dimensions |
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 |
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);// 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)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:
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