Architecture
Loom is a two-process live link. A Blender add-on (the producer) talks directly to a consumer editor plugin, the Unity package or the Unreal plugin, over a single TCP socket. There is no relay server and no database, the two most expensive design mistakes in comparable tools.
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ Blender (producer) │ TCP 8787 │ Unity Editor (consumer) │
│ │ binary frames │ │
│ depsgraph_update_post ─────┼───────────────────▶│ listener thread (parse) │
│ → dirty set (main) │ │ → coalescing queue │
│ debounce timer (main): │ │ EditorApplication.update: │
│ build numpy buffers │ │ → drain queue (main) │
│ → send queue │ │ → Mesh.Set* / reuse by id │
│ sender thread → socket │ │ │
└─────────────────────────────┘ └─────────────────────────────┘
Two consumers, one producer
The diagram shows the Unity consumer, Loom’s reference implementation. The Unreal consumer is a peer on the same wire. The Blender producer is byte-for-byte identical, and the Unreal plugin parses the same binary frames (and the same golden tests/fixtures/*.bin) as Unity. The only consumer-side differences are a coordinate re-base and idiomatic target mapping. Unreal converts the Unity-space wire data to Unreal space (Z-up, centimetres) on apply and maps each Unity concept to its UE equivalent (UDynamicMeshComponent, a LoomLit UMaterial, UInstancedStaticMeshComponent, a BP_LoomBake Blueprint).
Threading model
Both bpy and the Unity API are single-threaded. Loom respects that and pushes only the network I/O off the main thread on each side.
Blender (producer)
depsgraph_update_post(main thread) classifies each updated mesh object and records its dirty flags in a set. This handler does no serialization, so it returns instantly.- A debounced
bpy.app.timerstick (main thread) reads current object state, builds the binary message with numpy, and enqueues the bytes. Because it builds from live state, repeated edits between ticks coalesce for free. - A sender thread owns the socket: connect, handshake, drain the queue, write frames, reconnect with backoff. The UI reads a single atomic
connectedflag.
Unity (consumer)
- A listener thread accepts a connection and reads framed messages, parsing each into a small managed struct, and pushes it onto a bounded coalescing queue. Superseded realtime deform and transform frames for one id collapse to the latest, while guaranteed mesh, remove and material frames keep their order. No Unity API is touched here.
EditorApplication.update(main thread) drains the queue with a per-tick budget and applies changes. It refills a reusedMesh, reuses theMeshandGameObjectkeyed byobject_id, and applies a transform update as a cheapTransformwrite.
Running in the background
The live workflow drives one app while the other is unfocused. Both sides keep working in the background. The Blender producer needs no change, its main loop runs timers every iteration with no focus check. The Unity consumer keeps applying frames in edit mode, though Unity throttles its background repaint rate (raise it under Preferences, General, Interaction Mode, “No Throttling”). In Play mode, a live session forces Application.runInBackground on so the playtest keeps running when you click into Blender, and restores the prior value afterward.
Object identity
Objects are keyed by Blender Object.session_uid, a stable uint64 for the session, not by name. Renames do not orphan meshes, and two objects never collide. Unity keeps a dictionary mapping id to its GameObject, Mesh and cached vertex count. A remove frame tears the entry down.
The one-way overwrite contract
Loom is a one-way live preview. Blender is the single source of truth and the engine mirrors it. Three consequences are deliberate and worth stating plainly.
- One-way only. All data frames flow Blender to the engine (only the heartbeat is bidirectional). There is no geometry or transform channel back by design, because the dedup, delta-normal, skinning and resync machinery all assume a single authority.
- Overwrite, no merge, no undo, but hand-edits are protected by default. A manual edit to a synced object in the engine would be clobbered by the next update for that id. To stop an unintentional overwrite, the builder remembers the local transform it last wrote per object, and if a later update finds the object moved in between, it protects the object: the update is skipped and further updates pause until you release it. Turning protection off restores strict one-way, but counts and warns the overwrite rather than being silent.
- Ephemeral by default. The
Loomroot and every synced object are flagged so they are never serialized into a saved scene. Saving while connected does not bake the hierarchy in, and reopening cannot leave orphaned duplicates. Persisting the result is an explicit bake step, never a side effect of saving.
Game-ready conventions, no wire
Two consumer passes turn the synced scene game-ready from object names alone, with no extra wire. The same conventions drive both the live stream and the bake, sharing one parser so they never disagree. Both are opt-in.
- Colliders. An object whose name carries a collision token becomes the matching collider:
UCX_to convex,UBX_to box,USP_to sphere,UCP_to capsule,UMC_to concave mesh. - LOD groups. Sibling objects
<base>_LOD0..n(two or more levels) form aLODGroupwith gently decreasing transition heights.
Update classification
On each depsgraph update, per dirty object:
- New id, or topology signature changed (vertex, loop or triangle counts differ): send a full mesh.
- Geometry changed, topology same: send positions, plus normals when needed.
- Only transform changed: send a 40-byte transform packet.
This is what turns “drag one object” from a full-scene re-upload into a tiny packet, and “play an animation” from re-sending UVs every frame into a positions-only stream.
Materials
Materials are off the realtime path by design. The depsgraph stream stays geometry-only, and a material frame is pushed by an explicit Sync Materials action. The producer extracts the Principled BSDF into factors and encoded image maps, hash-caches per material so unchanged materials are not re-sent, and binds objects with a tiny per-object material frame. Textures cross the wire as encoded PNG bytes (or raw half-float pixels for genuinely HDR maps), deduped by content hash, so a material is self-contained rather than relying on a shared filesystem.
Non-goals for v1
- No materials on the realtime path. Material sync is manual and hash-cached, separate from the geometry stream.
- Node-graph to shader translation covers a curated node whitelist (20+ nodes, including Noise and Voronoi ported from Blender’s own source). Unsupported topology falls back to the Principled path. Multiple BSDFs and volumetrics remain out of scope.
- No multi-client fan-out, no remote or non-loopback transport, no auth. It is a localhost developer tool.