I’ve open-sourced a 3D world engine. It’s data-first and realtime-multiplayer: a game is a JSON-LD file, and the renderer, the systems, and the network layer all read from the graph. MIT-licensed, at github.com/trentbrew/world-engine, with a live demo at trellis-sync-3d-playground.vercel.app.
The constraint that shaped it
The naive way to build a 3D multiplayer space is code-first. Write components, wire a network layer, drag scenes together in an editor. That works until the content outgrows the author.
Almost all the work in a game like this is content: objects, levels, the little rules that make them feel alive. If every object requires engine code, then content production is bottlenecked on whoever writes the engine. That’s a hard ceiling: the game grows as fast as you can code, which is the wrong ratio.
So I made a different bet:
The world is the data. The engine is just a viewer for a graph.
Worlds as data
A world is a JSON-LD document. An entity is an @id with a bag of components. A component is a named bag of typed fields. A type is a reusable composition of components.
{
"@id": "type:Enemy",
"@type": "EntityType",
"components": ["Transform", "Render", "Health"],
"defaults": { "Render": { "color": "#c0392b" } }
}
Add a component to an entity and its view appears. No per-type components, no new .svelte files. You can define brand-new components and types inside the world file itself: the ontology, the renderer, the systems, and the network layer all read from the same graph.
That means the durable artifact is the world file, not a binary scene. A world can be diffed, versioned, generated, and written by agents. The last one matters most to me.


Sync the few, derive the rest
Multiplayer is where data-first pays off hardest. Every field declares how it travels:
- durable: authored, part of the rules. Sent once, then static.
- realtime: streamed at ~20 Hz by the entity’s owner.
- derived: a formula, never stored, never sent. Every client recomputes it.
Formulas are pure and deterministic, so every client agrees on the result without a byte over the wire:
"position": "=vec(cos(t * Orbit.speed) * Orbit.radius, 0.6, sin(t) * Orbit.radius)"
"grounded": "=Transform.position.y <= 0.55"
"tint": "=Health.current > 25 ? 'ok' : 'hurt'"
The bandwidth argument is simple: sync the few authoritative inputs, derive the rest. The worked example is three cubes orbiting each other. An Orbit component and an Orbiter type, defined in data, animated entirely by one formula, zero engine code.
Multiplayer itself is peer-to-peer: each tab owns its Player, the host owns the rest of the world, entities spawn and despawn as tabs open and close. BroadcastChannel by default; a WebSocket relay for real cross-machine sessions.
Behaviors are the one place code is required. A component plus a system that reads it each tick. Most of the time you don’t even need one. A derived formula field covers it.
Generate entire scenes with AI
The world-is-data bet pays off in the last place you’d expect: you don’t have to build the scene at all. World Labs generates photoreal 3D spaces from a text prompt — Gaussian splats, not meshes, spaces you can actually walk through. The engine treats them as data like everything else. Drop the export in static/worldlabs/{slug}/, point a GaussianSplat component at it, and it’s in the world:
"GaussianSplat": { "src": "/worldlabs/townsquare/hall.spz" }
The splat is the look. The collider export from the same generation is the law: collider.glb gives the space physical presence, so walls stop you and stairs hold you. Sight and physics stay in sync because they come from the same prompt.
And because a splat is data, it plays by the same rules as everything else. The Frame Shift museum is one hall at two decades — 1890 and 1960 — and the era dial just rewrites GaussianSplat.src. The Solar Punk town square isn’t behind a loading screen; it’s a statue you walk into, a RoomPortal carrying you to another generated world.
That’s the loop: describe a place, walk into it, invite someone in, change it with a line of JSON.
What shipped
The whole engine is out: renderer, ontology loader, formula evaluator, the sync and ownership layers, the player controller and animation system. Native gamepad support is built in — left stick and d-pad move, right stick looks, a face button jumps, and in multiplayer each client’s pad binds by member slot, so the host takes the first controller and the next peer the second. MIT, fresh history, no vendored assets. github.com/trentbrew/world-engine. Drop a world file in static/games/ and you have a game.
Where its going
I like the idea of treating the web as an open game platform. No app stores, no walled gardens, just data and code that anyone can inspect, modify, and share.
Making games with GameMaker is how I learned to code, and I hope this can be a similar gateway drug for other folks.
This project is still brewing…A few features I’m itching to include:
- Animation tool for rigged models
- Generative 3d character creation
- Procedural terrain generation
- More AI integration (image generation, voice synthesis, etc.)
- Publishing platform for sharing cross platform
- Tauri mobile app for creating iOS and Android games