Three months before my Steam demo I didn't know what gsplat was. I make games alone, I can't draw, and I needed bosses whose damage you could read on the hull itself, not on a health bar. Splats turned out to be the answer — just not in the way this field usually uses them.

In AsterMaster, a beat-driven roguelite on a hex grid built in Godot 4.6, the player's drone and the bosses are not meshes. Each one is a cloud of about 6,000 Gaussians, trained from Blender renders and drawn as a single instanced mesh. When a boss takes a hit, splats are pushed inward into craters, the same spot deepens layer by layer, and whole pieces tear off as rigid bodies — all in the vertex shader. RadianceFields covered the game in October; this piece is about the part a trailer doesn't show: what happens to 3DGS when the ground truth is a render, the budget is fixed in advance, and the result has to live inside a game engine.
1. A particle system in the shape of a model
On screen my drone is 72 pixels tall and a boss is about 112. Photorealism is not the goal; readability is. A mesh at that size dies boringly: play an animation, dissolve, gone. I wanted a dented side to tell the story of the fight.

Seen from a game developer's side, a trained splat cloud is something very specific: thousands of instances, each with its own position, orientation, size and colour, already arranged into the shape of the model. That is a particle system. The same data structure draws the hull, takes damage and becomes the debris. And it is small: the source meshes are 25–55 MB each, while the trained drone ships at about 0.5 MB and a boss at about 0.4 MB.

2. When the ground truth is a render
Training from synthetic renders removes the hardest parts of capture. Poses are exact, so there is no structure-from-motion step. Alpha masks come for free. You can render as many views as you like, under any lighting you choose. A headless Blender script renders 120–200 views at 1024² with alpha in about 30 seconds per model.
It also lets you shape the camera distribution to match how the asset will be seen. My game camera looks down at 32–50° and models only rotate around the vertical axis, so training views sit on a golden spiral inside that elevation belt. The underside is never seen in the game — which later let me delete about 10% of the splats outright.
But renders bring their own failure modes, and they are easy to miss because renders always look clean:
The splat learns your lighting. I trained one drone on three material variants: a toon shader baked in Blender, flat albedo, and diffuse shading under a white dome. The toon version doubled up with the game's own cel bands; flat albedo came out as a pancake; the dome-lit version kept its volume without fighting the runtime light. In capture, lighting is a given. In synthesis it is a design decision with consequences downstream.

The dataset is only as good as the file you render. For weeks the bosses "lacked detail". I blamed the splat budget and the trainer. The real cause: the datasets had been rendered from game-ready files whose textures an old optimisation pass had squeezed to 128². The 2048² sources sat in the next folder. Retraining from them brought back panel lines and teeth that no budget increase could have recovered.

Perfect poses don't kill floaters. With a black background, a dark splat hanging under a gun barrel costs almost nothing in the loss and even helps shade the hull — a "proxy shadow". Compositing over a random grey background at every step makes any splat outside the silhouette expensive from every view. What helped most was in the dataset again: extending the camera belt down to 14° and raising train views from 120 to 200, so low grazing views supervise the silhouette from below. Splats sticking out of the silhouette went from 18 to 9 after the retrain, and to 7 after an export-time cull. One instrumentation lesson cost me more than any of these: dark floaters are invisible on a black stand. I now judge silhouettes only on grey, and only at game scale.

3. The budget is an input, and opacity is binary
Games need exactly N splats per model, not "however many the densification heuristic decides". gsplat's MCMCStrategy gives exactly that: instead of cloning Gaussians it relocates dead ones onto live ones, so the count never exceeds cap_max. Bosses and the drone get 6,000. A hologram decoy that copies the player's drone uses a separate cloud retrained with cap_max=1800 — taking the top 1,800 splats from the 6,000 model left holes; letting MCMC distribute a smaller budget did not.
The trainer is about 150 lines on top of gsplat, with direct RGB instead of spherical harmonics (the style comes from the data). A run is 6,000 steps at 400² and takes 21–22 seconds on an RTX 3060; retraining every boss is a few minutes. That short loop is what made dozens of experiments with datasets, budgets and penalties possible.

The most consequential trick is an entropy penalty on opacity, o·(1−o), switched on only for the last 40% of training. It pushes opacities to 0 or 1, and about 98% of splats end up fully opaque. Switch it on from the start and it fights the soft silhouette edges; the shape never settles.
Binary opacity is what makes the runtime cheap. Splats above 0.9 opacity form an opaque core, drawn as octagons fitted to each splat's visible disc, with an ordinary z-buffer: no sorting, no discard, MSAA works. The remaining ~200 splats form a thin semi-transparent shell that does need back-to-front order — but since the camera tilt is fixed and models only turn in yaw, the order depends on one angle, so I precompute 64 orders at export. The whole thing runs on Godot's Compatibility renderer without compute shaders, and a full scene with a boss and a crowd holds a 4.6–5.4 ms median frame on an RTX 3060. The trade is explicit: you give up the soft, view-dependent look of a capture and get a depth buffer back.
4. Bake what a camera can't see
In capture, the photos are the only truth. In a game pipeline the splat is a cache of a richer source — the mesh — and anything you can compute from the mesh can be baked into the splats after training:
Normals, snapped from the nearest face. They take no part in the loss and cost nothing, but give cel lighting and muzzle flashes from a point light.
Part ID (clusters over mesh islands, or hand-labelled parts): rigid part animation and per-part damage.
Ambient occlusion from a 24-ray hemisphere: a "depth" mask that decides what glows through the seams and what is inside the hull.
Emission, chosen by rarity. "Saturated and bright" lit up 2,572 of 4,200 splats on a red boss; "far from the model's median colour" left 877 — the eyes, the lightning, the jaw.
Material class and chunk ID for destruction (below).
Everything fits into one vec4 of per-instance data plus the colour alpha. One engine detail worth knowing: in Godot's Compatibility renderer that per-instance data is stored as half-float, so integers above 2048 are silently rounded (4482 becomes 4480). Every baked channel in the project now stays at or below 255.
Baking from a mesh also means inheriting its bugs. The generated source meshes are assembled from hundreds of open "plates" — Crimson Skull has 558 — and about 40% of them were wound inward. The snapped normals inherited the flip, and craters bulged outward. Volume-based repair doesn't work on open shells; a twenty-line area-weighted vote per component did, and flipped 56% of the Skull's splat normals.
5. Destruction is where splats pay
Destruction treats the opaque core as an array of slots. A hit picks candidates by attack type — a narrow band for a shot, a wide one for a blast, a channel for a railgun slug. A knocked-out splat is not removed: it is pushed inward, which gives a crater a dark bottom instead of a see-through hole. Hit the same spot again and the slot reincarnates one layer deeper, through procedural layers (underlay, frame, innards, a smouldering core). These layers are invented — a generated mesh has no ground truth for its insides — and that is fine for a game.

A neighbourhood graph built once at load time catches islands cut off from the hull and tears them off. The step that made destruction readable was moving from splats to chunks: material- and part-aware k-means++ with equal area per chunk, so a plate breaks as a plate. Size was the key number: 25 splats (about 10 pixels on screen) read as confetti; about 100 splats (20 pixels) read as a piece.

Animation is just as simple. Each model has up to eight rigid parts driven by uniform arrays — one bone per part, binary weights. A rigid rotation of trained Gaussians is exact, so wings flap and barrels recoil with no loss of quality and no skinning.
6. Where splats didn't pay
Not everything belongs on splats. For one week every enemy was a splat cloud; then ordinary enemies went back to meshes with a matching toon shader. At 60–90 pixels, splat destruction doesn't read and doesn't pay for its content cost. Splats stayed where damage is visible: the player's drone, the bosses, and — since the latest build — the larger turrets.
Two research branches also lost to plain Gaussians. A custom differentiable superellipsoid rasterizer gave charming 500-primitive models but ran at 0.7 s per step against gsplat's 21 seconds per run, with worse loss (0.0137 vs 0.008), and at runtime it needed either ray marching or depth output from the fragment shader. A sparse volumetric lattice was more stable over time, but its SDF variant broke in the renderer, where a ray passes through 15–20 overlapping cells. Half of what they taught me survived as baked data: analytic normals became mesh normals, "panel equals primitive" became part ID, and exact depth without sorting became binarized opacity.
7. Splats as a delivery format for games
One trained cloud now feeds three consumers. The game loads a small custom binary with the opaque core, the shell, 64 sort orders and the baked channels. The game's website renders the same models in the browser with Spark on top of three.js: the drone there is a 16k-splat cloud packed into SPZ at 229 KB, bestiary enemies are about 39 KB each, and Crimson Skull is 85 KB — while the game-ready meshes, with their 128² textures, did not survive close-ups at all. The hologram decoy is the third consumer, with its own budget.
That experience leaves me with a short wish list for the tools around 3DGS:
Per-splat custom attributes as a first-class idea. The KHR_gaussian_splatting release candidate standardises the core. glTF already allows application-specific attributes with a leading underscore; a shared convention for things like part, occlusion and material would let a game ship splats in glTF instead of a private binary like mine.
Synthetic training as a first-class path. Most tools assume photos and structure-from-motion. Rendering from a DCC tool with exact poses, alpha and a camera distribution taken from the target view deserves its own presets.
Budgets and opacity regularisation as standard knobs. For games, an exact splat count and opaque splats matter more than spherical harmonics.
An engine path without compute shaders. Most engine integrations I found are viewers built around compute-based sorting. Opaque splats with a small presorted shell run on much more modest rendering paths.
If you are building tools in this space, I would be glad to compare notes.

AsterMaster launches on Steam on January 12; the demo is free now. The full technical write-up, with code and the numbers behind everything above, is on the game's devlog.
Transparency: as disclosed on the Steam page, some promo art and item icons, a few source 3D meshes, the background music and some flavor text were made with generative-AI tools and curated by me; nothing is generated at runtime. The splat pipeline, shaders and destruction described here are my own work, with part of the code written in pair with an AI coding agent.

