UI panel used as a still backdrop for the character editor plaza. It captures the plaza camera to a GPU render target texture, suppresses skinned model renderers while capturing, stores the snapshot as the panel background, and restores renderer state when done or deleted.
using System;
using Sandbox.UI;
namespace CharacterEditor;
/// <summary>The plaza is a still backdrop while editing, and resumes rendering when opened.</summary>
public sealed class StudioBackdrop : Panel
{
public StudioSession Session { get; set; }
readonly Dictionary<SkinnedModelRenderer, bool> visibility = new();
Texture snapshot;
Vector2 size;
int revision = -1;
float retryAt;
float finishAt;
bool finishPending;
ulong captureFrame = ulong.MaxValue;
public override void Tick()
{
base.Tick();
var plaza = Session?.Plaza;
if (!plaza.IsValid() || Session.Embedded || !plaza.Camera.IsValid()) return;
// Keep the actors visible for the frame that captures them, then suppress
// them even while the remaining resources load. The opaque collection has
// no visible backdrop and does not need a capture at all.
if (captureFrame == Application.FrameCount) return;
SuppressCurrentRenderers(plaza);
if (Session.View == "library") return;
var currentSize = Box.Rect.Size;
if (currentSize.x < 1 || currentSize.y < 1) return;
if (snapshot is not null && size == currentSize && revision == Session.Library.Revision && (!finishPending || RealTime.Now < finishAt))
{
return;
}
if (RealTime.Now < retryAt) return;
if (plaza.Residents.Any(a => !a.Body.IsValid() || a.Error is not null)) return;
Restore();
// Keep the capture on the GPU. RenderToBitmap forced a synchronous GPU
// readback and then uploaded another texture during UI Tick. Reuse one
// render target instead, with UI excluded to avoid capturing this panel.
snapshot = Texture.CreateRenderTarget("Character studio backdrop", ImageFormat.RGBA8888, currentSize, snapshot);
var setup = new Sandbox.Rendering.ViewSetup { EnableUI = false, EnableDebugOverlays = false };
if (!plaza.Camera.RenderToTexture(snapshot, setup))
{
SuppressCurrentRenderers(plaza);
retryAt = RealTime.Now + .5f;
return;
}
Style.BackgroundImage = snapshot;
captureFrame = Application.FrameCount;
var changed = revision != Session.Library.Revision || size != currentSize;
size = currentSize;
revision = Session.Library.Revision;
if (changed || !finishPending && finishAt == 0)
{
finishPending = true;
finishAt = RealTime.Now + 3.5f;
}
else finishPending = false;
retryAt = RealTime.Now + .5f;
}
void SuppressCurrentRenderers(CharacterPlaza plaza)
{
foreach (var renderer in visibility.Keys.Where(r => !r.IsValid()).ToArray()) visibility.Remove(renderer);
foreach (var avatar in plaza.Residents)
foreach (var renderer in avatar.GameObject.GetComponentsInChildren<SkinnedModelRenderer>(true, true))
{
if (!visibility.ContainsKey(renderer)) visibility[renderer] = renderer.Enabled;
if (renderer.Enabled) renderer.Enabled = false;
}
}
void Restore()
{
foreach (var pair in visibility)
if (pair.Key.IsValid()) pair.Key.Enabled = pair.Value;
visibility.Clear();
}
public override void Delete(bool immediate = false)
{
Restore();
Style.BackgroundImage = null;
snapshot?.Dispose();
base.Delete(immediate);
}
}