Guidance for AI coding agents (Codex, Cursor, GitHub Copilot, Claude Code, Windsurf, Aider, Zed, and others) working in this repository or wiring this tool into another project. Humans: this is a fast, accurate map. Deeper docs are linked at the bottom.
A drop-in runtime prefab → Texture2D thumbnail pipeline for Unity 6 + URP. Queue character / item / vehicle / ship prefabs once and get cached thumbnails — static, yaw-rotating, or animator-clip animated — rendered by an offscreen camera with async GPU readback, one frame per tick (no spikes), plus a lazy-load UI Toolkit tile and a built-in spinner + greyscale post-process. The runtime is five C# files under Assets/PrefabThumbnails/Runtime/. Namespace: PrefabThumbnails. License: MIT. Battle-tested in Leap of Legends, where every cosmetic thumbnail in the store, equip screen, and reward chests is rendered through this pipeline.
PrefabThumbnailRendereris a plain C#IDisposable, not aMonoBehaviour. Nothing renders unless something drives it. The lifecycle is exact:new PrefabThumbnailRenderer(size)→ set config fields →Setup()(once; idempotent) → callTick()every frame from one hubMonoBehaviour.Update()→Dispose()inOnDestroy(). EachTick()captures at most one frame, so a 24-frame animated request completes after 24 ticks (~400 ms at 60 fps).- Results are null until ready.
GetThumbnail(key)/GetAnimated(key)returnnullwhile a render is pending. Prefer subscribing toOnThumbnailReady(int key, AnimatedThumbnail anim)over polling.Staticthumbnails are a one-frameAnimatedThumbnail— read.FirstFrame. - The RenderTexture, camera, and light are created in
Setup()and freed only inDispose(). Do not callSetup()per render, and never skipDispose()— it leaks the RenderTexture and the offscreen scene root. Per-frameTexture2Ds are freed byAnimatedThumbnail.Dispose()/Evict()/ rendererDispose(). - To change a cached thumbnail,
Evict(key)then re-Queue. The cache dedupes by key and a post-process mutates textures in place, so a plain re-Queueon a cached key is a no-op.ThumbnailTile.Refresh()does the evict + re-queue for you. AnimatorClipmode needsAnimatorSampleTargetPathset to the rig root (e.g."Root"for Kenney AC2 rigs), or every animation curve binds against the wrong transform and silently no-ops (frames come out at bind pose).PostProcessCallbackruns on EVERY captured frame, not just the first, viaGetPixels32/SetPixels32(CPU). Cheap for hundreds of frames, not thousands.- URP post-FX auto-disable is behind
#if UNITY_RENDER_PIPELINE_UNIVERSAL, which this repo's project does not define. If you copy onlyRuntime/into a URP project that has a global post-processVolume, defineUNITY_RENDER_PIPELINE_UNIVERSAL(Player → Scripting Define Symbols) or wrap the tool in an.asmdefreferencing URP with aversionDefine, or thumbnails bake in bloom / tonemap. - No editor-only behaviour in runtime code (no
#if UNITY_EDITORinPrefabThumbnailRenderer.csor its helpers) and nousing LeapOfLegends.*. Runtime code lives underPrefabThumbnails; demo code underPrefabThumbnailDemo.
The runtime is Assets/PrefabThumbnails/ dropped into your Assets/. using PrefabThumbnails; then:
var renderer = new PrefabThumbnailRenderer(thumbnailSize: 256);
renderer.Setup();
renderer.OnThumbnailReady += (key, anim) => UpdateCard(key, anim.FirstFrame);
renderer.Queue(new ThumbnailRequest { // Static — one frame, cached forever (the 99% case)
Key = item.Id, Prefab = item.Prefab,
PreRenderCallback = inst => ApplySkin(inst, item.SkinMaterial),
PostProcessCallback = item.Owned ? null : ThumbnailPostProcess.GreyscaleDim,
});
void Update() => renderer.Tick(); // drive it — at most one render per frame
void OnDestroy() => renderer.Dispose(); // release the RenderTexture + offscreen rootYawRotation and AnimatorClip add AnimationMode, FrameCount, PlaybackFps (and, for clips, AnimationClip + AnimatorSampleTargetPath). For UI Toolkit, drop a ThumbnailTile into a grid and call tile.Bind(renderer, request) — it lazy-queues only once layout puts it on screen. The canonical examples are the README "Quick start" and Assets/Demo/Runtime/DemoUI.cs.
PrefabThumbnailRenderer : IDisposable (Assets/PrefabThumbnails/Runtime/PrefabThumbnailRenderer.cs):
PrefabThumbnailRenderer(int thumbnailSize = 256);
// config — set before Setup(): OffscreenOrigin, CameraFOV, FramingPadding, BackgroundColor,
// DefaultPrefabRotation, LightRotation, LightIntensity
void Setup(); // idempotent; builds offscreen camera + light + RT
void Queue(ThumbnailRequest request);
void Cancel(int key);
void Tick(); // call every frame; renders <= 1 frame
Texture2D GetThumbnail(int key); // first frame, null until ready
AnimatedThumbnail GetAnimated(int key); // full strip, null until ready
bool IsCached(int key); bool IsPending(int key);
void Evict(int key); void EvictWhere(Predicate<int> predicate);
void Dispose();
event Action<int, AnimatedThumbnail> OnThumbnailReady;ThumbnailRequest (struct — ThumbnailRequest.cs):
int Key; GameObject Prefab; string ResourcePath; // Prefab OR ResourcePath (Prefab wins)
Vector3 PrefabRotation; // zero = DefaultPrefabRotation
Action<GameObject> PreRenderCallback; // skin / attach, before framing
Action<Texture2D> PostProcessCallback; // runs on every captured frame
ThumbnailAnimationMode AnimationMode; // Static / YawRotation / AnimatorClip
int FrameCount; // default 24, ignored for Static
float PlaybackFps; // playback hint, default 12
AnimationClip AnimationClip; // AnimatorClip mode
string AnimatorSampleTargetPath; // AnimatorClip mode — the rig root, e.g. "Root"AnimatedThumbnail : IDisposable (AnimatedThumbnail.cs): Texture2D[] Frames; float Fps; ThumbnailAnimationMode Mode; int FrameCount; Texture2D FirstFrame; Texture2D GetFrameAt(float playbackSeconds); (loops modulo strip length).
ThumbnailPostProcess (static — ThumbnailPostProcess.cs): void Greyscale(Texture2D), void GreyscaleDim(Texture2D), Action<Texture2D> Tint(Color).
ThumbnailTile : VisualElement ([UxmlElement], ThumbnailTile.cs): void Bind(PrefabThumbnailRenderer, ThumbnailRequest), void Refresh(), void Unqueue(), VisualElement Image, ThumbnailPlaybackTrigger PlaybackTrigger { get; set; }; enum ThumbnailPlaybackTrigger { Continuous, OnHover }. Also enum ThumbnailAnimationMode { Static, YawRotation, AnimatorClip }.
Assets/PrefabThumbnails/— the shippable tool, and the only folder a consumer copies.Runtime/= 5 files (renderer, request, animated-thumbnail, tile, post-process). NamespacePrefabThumbnails.Assets/Demo/— the WebGL 25-tile showcase host project (all three modes, category filter, lazy tiles, spinners, mobile drawer). NamespacePrefabThumbnailDemo. Not part of the tool.Assets/Editor/,Assets/Settings/,Assets/WebGLTemplates/— build + URP scaffolding for the demo.Tools/— the shared build orchestrator (Tools/.orchestratorsubmodule) + a thinBuild-Demo.ps1shim.Vendor/— the design-system submodule (itsds-spinnerpowers the tile), used only by the demo.
- Comments answer "why", not "what". e.g. why the offscreen camera renders at depth −20 (before main cameras, so it can't pick up their post-FX).
- Framing is yaw-invariant. The camera frames the model once at frame 0 using a
sqrt(x² + z²)diagonal, so a spinning model doesn't pulse in size — don't switch it to a per-frame.xdistance. - Skinned meshes need
updateWhenOffscreen = trueat the far origin, or Unity culls them by pre-deform bounds and captures the bind pose. The renderer sets this on every SMR; keep it. Texture2D.Apply(false, true)marks frames non-readable to free the CPU copy — keep themarkNoLongerReadableflag.
Windows-first Unity 6 project (host editor 6000.3.8f1). There is no unit-test suite; validation is visual, through the demo (CONTRIBUTING: "the demo is the test suite").
- Editor preview: open
Assets/Demo/Scenes/PrefabThumbnailDemo.unityand press Play (the scene auto-spawns viaDemoBootstrap). Tiles fill in with spinners, then frames. - WebGL build, from the repo root in PowerShell:
git submodule update --init --recursive copy Tools\Build\config.example.json Tools\Build\config.local.json .\Tools\Build\Build-Demo.ps1 -Serve # builds to build/WebGL/ and serves http://localhost:3000
-Serveserves locally,-Deployforce-pushes togh-pages,-ClearCacherecovers from a stale Burst-AOT cache. - Verify all three modes (characters animate, ships spin, props are static) and the mobile reflow at 360×640 after any change.
- Demo still works end-to-end (open
PrefabThumbnailDemo.unity, Play, tiles fill with spinners then frames). - All three modes still render (Characters animate, Ships spin, Props are still).
- Mobile reflow still works (Game view 360×640 — title / promo / tabs / panel stack in one column).
- No
using LeapOfLegends.*or product-specific imports; no#if UNITY_EDITORin runtime code. - Comments answer why, not what.
CHANGELOG.mdupdated; README updated if the public API or behaviour changed.
- Full walkthrough, three-mode examples, API, tuning:
README.md - Contribution rules and PR checklist:
CONTRIBUTING.md - Version history:
CHANGELOG.md - Machine-readable index:
llms.txt - Sibling tools: design system · mesh fracturer · 3D-to-sprite baker · build orchestrator