Guide · Gaussian splats 18 min read

Make a Gaussian splatting game with Blender 5.3

In Dustlight you're a small paper-lantern spirit on a sky island at night, and the embers you find pull its landmarks back together out of drifting dust. We built that world from Gaussian splats in Blender 5.3, starting from drawings Codex made, and none of it came from a scan.

A floating island at night with a striped lighthouse shining its lamp, a mossy stone arch and a pink blossom tree, as a blue whale with cyan freckles swims overhead, its tail still a trail of star motes
On this page

In Dustlight, sky islands gather out of drifting dust at night, drained of color, while a lighthouse, a cottage, a blossom tree and a mossy shrine arch still swirl over the main island as neon motes. You’re a small paper-lantern spirit, and color comes back wherever your light falls.

Each of the first four embers pulls a landmark back together, splat by splat. The fifth lights the lighthouse lamp, and a whale made of splats streams in from the stars.

In this Gaussian splatting game, the islands, landmarks, spirit, whale and even the clouds are splats, about 1.7 million of them. Codex drew the models, Blender 5.3 made them into splats, the game makes its clouds in code, and Three.js with the Spark library draws it all.

DustlightCarry a paper lantern and pull a sky island back together out of glowing dust.Claude Opus 5.5 · Adventure
Dustlight's island at night with the lighthouse lamp lit and its beam sweeping out over a glowing cloud sea, next to the mossy shrine arch and the pink blossom tree, with the keeper's subtitle at the bottom
The lighthouse lit at the end of Dustlight, with all five embers found.

Blender 5.3 is due on November 10, says its release page, so we tested a development build from October 3. Six tools did the main work, three of them AI.

Made with What it did
Codex Drew a two-view reference sheet for each of the seven models
Blender 5.3 Turned the sheets into painted meshes and the meshes into splats
Claude Code Wrote the Blender Python, the exporter, the game (about 1,700 lines of JavaScript) and the sound code
Three.js 0.186.1 Runs the scene, the camera and the collision rays
Spark 2.3.1 Draws and sorts the splats and runs the per-splat effects
VoxCPM2 Designed the lighthouse keeper’s voice and spoke her six lines

Can Gaussian splats be used in games?

They can, and our catalog already has one. Gaussian Splatting FPS is a first-person level built in PlayCanvas from a splat scan of a real place. A scan has no surfaces to stand on, so as its creator explains on the PlayCanvas blog, the collision comes from splat-transform --collision-mesh, which voxelizes the splat into a mesh.

Every splat game we found starts from a capture or a generated world. Dustlight takes a more unusual route, from drawings to meshes to splats, so the meshes are still there to walk on.

What can Blender 5.3 do with Gaussian splats?

It imports PLY and SPZ splats and renders them in Workbench, EEVEE and Cycles, but it can’t export them. The release notes cover SPZ up to version 4 and add, “There is currently no export support.”

Inside Blender, a splat cloud is a point cloud whose new Type setting says 3D Gaussian Splats, plus a few named attributes on every point. That’s a big deal here, since Geometry Nodes can write named attributes and switch the type.

Attribute What it holds
position The splat’s center
scale Its size along each local axis, as a plain standard deviation (PLY files store the natural log)
rotation A quaternion, in w, x, y, z order
radiance:base The color as a spherical-harmonics DC value, plus opacity from 0 to 1
radiance:sh_0 and up Optional higher bands, for color that shifts with the viewing angle

With no material, a splat glows in its stored color through a built-in emission shader. The notes also list four limits. Performance isn’t ideal, splats are assumed to be trained in sRGB, rendering in linear space isn’t fully correct, and Apply Transform can’t handle a splat’s scale and rotation yet, so our exporter bakes transforms itself.

1. Draw each model twice with Codex

Each model starts as one Codex picture, front view on the left and right side on the right. The brief asks for both views orthographic, at one scale on one baseline, with flat light and no shadows or background, and each sheet took Codex under a minute.

It’s a fiddly brief, and the reason is that the outlines become the model’s shape and the drawing becomes its paint, where a painted shadow would stick from every angle. Only the subject changed across the seven sheets.

Composition: a model reference sheet with exactly two views of the same object side by
side at the same scale, both standing on the same invisible baseline: on the left the
FRONT view, on the right the SIDE view seen from the right. Both views orthographic
(no perspective), perfectly level camera, the object fully visible with a generous
margin, the two views clearly separated by empty space. Light: flat, even, diffuse
light from the front, no cast shadows, no ground shadow, no ambient occlusion halos.
Codex reference sheet of a cream stone lighthouse with coral-red bands, a teal gallery and a glowing lamp, drawn from the front and from the sideThe same lighthouse as Gaussian splats in Blender 5.3, rendered in EEVEE against a dusk sky
The lighthouse as Codex drew it, and the same lighthouse as splats in Blender 5.3.

See how to make game assets with AI for keeping AI art in one style.

2. Build a mesh from the two silhouettes

Two outlines leave plenty of room for guessing depth, so the script guesses in one of two ways. Claude Code wrote it as Python that Blender runs headless, and it builds a model in under 20 seconds plus start-up.

Round things like the lighthouse, the cottage and the spirit are lathed. Per pixel row, the script keeps the smallest of four half-widths from the two views, so a detail seen in one view only drops out instead of becoming a ring, and a Screw modifier spins the profile.

The island, the tree, the arch and the whale become rounded visual hulls. At each height, the script fills the ellipse that fits the front outline’s width and the side outline’s depth, piece by piece so the arch’s pillars stay apart. Points to Volume, Volume to Mesh and Blur Attribute then smooth the voxels into a surface.

Thin vines would turn into plates, because a strand that’s thin in one view meets a wide outline in the other, so the script erodes them away and adds the island’s vines and roots back as rows of upright splats. Small parts like the chimney, the bell and the whale’s fins come back as simple shapes merged in with Mesh Boolean.

Our MCP servers guide covers driving a running Blender from your agent.

3. Paint the drawing back onto the mesh

Splats take their color from the surface under them, so the mesh needs the drawing’s paint first. A second Geometry Nodes group projects the front view along Y and the side view along X, blending the two by the surface normal.

Tops are the tricky part, since neither view shows them. Faces pointing up take the color just under the outline’s top edge, but only close to it, so the island gets its green top while the tree’s trunk under the canopy keeps its bark.

The back can mirror the front or take the side view, which comes in handy when a door shouldn’t show up twice. The paint lands in a float color attribute, Col, which Blender keeps linear.

The floating island as a painted mesh in Blender's Workbench renderer, with a grassy top over cream, ochre and violet rock layersThe same island as soft Gaussian splats in EEVEE, with grass splats standing up on its top and vines and roots hanging underneath
The island as a painted mesh in Workbench, and as splats in EEVEE.

4. Turn the mesh into Gaussian splats in Geometry Nodes

Mesh to Splats, the group that does it, uses only stock nodes. This slim version has 13 between its input and output and takes its color from an 8-bit texture through the UV map.

  1. Points on the surface. Distribute Points on Faces, set to Random, with Density as splats per square unit. It carries the UV map over to the points.

  2. Color from the texture. Named Attribute reads the UV map (UVMap here) as a Vector, and Image Texture samples there. Vector Math on Multiply Add turns sRGB into the splat’s DC value, (sRGB - 0.5) / 0.28209479, by multiplying by 3.545 and adding -1.772, and Separate XYZ splits the result.

  3. Turn each splat to face out. Align Rotation to Vector, on the Z axis, lays the splat’s thin axis along the Distribute node’s Normal.

  4. Size and opacity. Inverse Square Root of the Density gives the typical point spacing. A Vector Math Scale of (1, 1, 0.12) sets each splat’s spread to that spacing across and 0.12 of it through, and Combine Color adds an alpha of 1.

  5. Make them splats. Three Store Named Attribute nodes on the Point domain write rotation (Quaternion), scale (Vector) and radiance:base (Color), and Set Point Cloud Type, on 3D Gaussian Splat, comes last.

Blender 5.3's Geometry Nodes editor with the slim Mesh to Splats group in five labeled frames, from Points on the surface to Make them splats, next to the lighthouse drawn as splats in the viewport
The slim Mesh to Splats group in Blender 5.3, with the lighthouse as splats on the right.

At full opacity, that spread covered our models with no gaps and left them a bit painterly, with brush-like edges.

The game’s own group can also read Col, and adds size jitter, a random twist and a material, yet both gave the lighthouse the same 225,912 splats. It’s fast, too, evaluating a million splats in about 25 milliseconds in a sphere-and-Suzanne test.

Each model’s Density is its splat budget over its surface area, about 150,000 splats for the lighthouse. The tree’s pink faces get a second copy of the group with bigger, rounder splats, and the island gets upright grass blades.

Codex reference sheet of a twisted old tree with a pink blossom canopy and small glowing paper lanterns, drawn from the front and from the sideThe blossom tree as Gaussian splats in EEVEE, with a soft, puffy pink canopy over a twisted trunk and spreading roots
The blossom tree's sheet, and the tree as splats, with bigger, rounder splats in its canopy.

Two Python details in this build are easy to miss. Menu inputs take their display names, and modifier inputs live in mod.properties.inputs.

import bpy

set_type = group.nodes.new("GeometryNodePointsSetType")
set_type.inputs["Type"].default_value = "3D Gaussian Splat"  # display name, not "GAUSSIAN_SPLAT"

def set_input(mod, name, value):
    # Blender 5.3 keeps modifier inputs here; mod["Socket_1"] = value raises a TypeError
    ident = next(i.identifier for i in mod.node_group.interface.items_tree
                 if i.item_type == "SOCKET" and i.in_out == "INPUT" and i.name == name)
    getattr(mod.properties.inputs, ident).value = value

mod = obj.modifiers.new("Mesh to Splats", "NODES")
mod.node_group = group
set_input(mod, "Density", 3000.0)
set_input(mod, "Image", bpy.data.images["lighthouse_paint.png"])

Why aren’t Gaussian splats showing in Blender 5.3?

Usually the type, an attribute or the opacity is off, so go through these checks in order.

Close-up of the last nodes in Blender 5.3, with Store Named Attribute set to Vector for scale and to Color for radiance:base, then Set Point Cloud Type with its menu open on 3D Gaussian Splat
The end of the group, with radiance:base stored as Color and the type set to 3D Gaussian Splat.
  • Is the type set? Without Set Point Cloud Type on 3D Gaussian Splat, Blender draws plain points.

  • Is the opacity really stored? In our build, Store Named Attribute’s 4D Vector type oddly has only a three-part Value socket, so opacity comes out as 0 and every splat is invisible. Store radiance:base as Color instead.

  • Are the attribute names exact? A point without scale falls back to a tiny 0.001, and one without radiance:base to mid gray at half opacity.

  • Do the splats cover the surface? If they’re smaller than the point spacing, the far side shows through as colored speckles.

  • Was the mesh’s scale applied? Density counts per square unit in object space, so apply scale first or the splats scale with the object.

  • Do the colors match? Switch the view transform from AgX to Standard, which matched Spark for us. Image Texture in Geometry Nodes also returns an 8-bit PNG’s raw sRGB bytes, while color attributes are linear and need converting.

The opacity socket and raw bytes are development-build behavior, so retest them in your build.

5. Export the splats with a short script

Blender 5.3 can’t export splats, and in our build its PLY exporter even reports success while writing zero vertices. Luckily the format is pretty simple, so our script reads the evaluated point cloud and writes the standard 3DGS PLY that both Spark and Blender read. It’s basically Blender’s importer run backwards.

Blender attribute PLY property
position x, y, z, turned from Blender’s Z-up to Y-up as (x, z, -y)
radiance:base color f_dc_0 to f_dc_2, unchanged
radiance:base alpha opacity, as a logit, ln(a / (1 - a))
scale scale_0 to scale_2, as a natural log
rotation rot_0 to rot_3 in w, x, y, z order, turned with the axes

This core covers splats from the group above, without object transforms or higher spherical-harmonics bands. The full script bakes transforms in, which is how the three small islands became smaller, turned copies of the main one.

import bpy
import numpy as np

def read(pc, name, n, prop):
    a = pc.attributes[name]
    out = np.empty(len(a.data) * n, np.float32)
    a.data.foreach_get(prop, out)
    return out.reshape(-1, n)

def export_ply(obj, path):
    geo = obj.evaluated_get(bpy.context.evaluated_depsgraph_get()).evaluated_geometry()
    pc = geo.pointcloud                                # read it while geo is still alive
    pos = read(pc, "position", 3, "vector")
    base = read(pc, "radiance:base", 4, "color")       # DC red, green, blue, opacity 0..1
    scale = read(pc, "scale", 3, "vector")             # linear standard deviation
    w, x, y, z = read(pc, "rotation", 4, "value").T    # quaternion

    # Blender is Z-up, Spark and Three.js are Y-up: (x, y, z) becomes (x, z, -y),
    # a -90 degree turn around X that turns every splat's rotation with it
    pos = np.stack([pos[:, 0], pos[:, 2], -pos[:, 1]], 1)
    k = np.sqrt(0.5)
    rot = np.stack([k * (w + x), k * (x - w), k * (y + z), k * (z - y)], 1)

    alpha = np.clip(base[:, 3], 1e-6, 1 - 1e-6)
    data = np.concatenate([pos, np.zeros_like(pos), base[:, :3],
                           np.log(alpha / (1 - alpha))[:, None],  # opacity as a logit
                           np.log(np.maximum(scale, 1e-12)),      # scale as a natural log
                           rot], 1).astype("<f4")
    names = ["x", "y", "z", "nx", "ny", "nz", "f_dc_0", "f_dc_1", "f_dc_2", "opacity",
             "scale_0", "scale_1", "scale_2", "rot_0", "rot_1", "rot_2", "rot_3"]
    head = ["ply", "format binary_little_endian 1.0", f"element vertex {len(data)}"]
    head += [f"property float {n}" for n in names] + ["end_header"]
    with open(path, "wb") as f:
        f.write(("\n".join(head) + "\n").encode("ascii"))
        f.write(data.tobytes())

We ran this exact code on the 225,912-splat lighthouse and read the file back with Blender’s importer. Every splat came back, positions and colors exactly, and everything else within about a millionth.

PLY or SPZ for a browser game?

SPZ, at least for models like ours. Our PLY layout spends 68 bytes per splat on plain floats, so it’s no wonder a million splats come to 67.9 MB, while SPZ quantizes and compresses them.

Splats PLY PLY, gzip -9 SPZ
299,751 20.4 MB not measured 3.74 MB
999,104 67.9 MB 45.4 MB 11.67 MB

Dustlight’s ten SPZ files total 21.8 MB, and Spark’s own transcodeSpz converts PLY to SPZ offline in Node in about 2 seconds per million splats.

// node ply2spz.mjs in.ply out.spz   (Node 22, @sparkjsdev/spark 2.3.1)
import fs from 'node:fs';
import { transcodeSpz } from '@sparkjsdev/spark';

const [, , input, output] = process.argv;
const { fileBytes } = await transcodeSpz({
  inputs: [{ fileBytes: new Uint8Array(fs.readFileSync(input)), fileType: 'ply', pathOrUrl: input }],
  maxSh: 0,
});
fs.writeFileSync(output, fileBytes);

SPZ does lose a little. Back in Blender, positions had moved by up to 0.00012 units and sizes by up to 3.4%, yet PLY and SPZ looked the same in Spark.

Which engine should a Gaussian splatting game use?

Spark, if your game runs on Three.js and WebGL2, which is why we picked it over two other options.

Option Runs on What it gives you
Spark 2.3.1 Three.js with WebGL2 Several splat objects sorted together, PLY, SPZ, SPLAT and KSPLAT files, and your own per-splat GPU code (dyno)
Three.js r186 GaussianSplat Three.js with WebGPURenderer only Splats built into Three.js, but no LOD streaming or spatial culling, so a city-sized capture needs tiling or trimming by hand
PlayCanvas Its own engine, on WebGL and WebGPU Splats inside a full engine and editor, plus splat-transform for collision meshes

Spark let us write the dust and light effects as dyno programs, while Three.js’s own splats would have meant moving the whole game to WebGPURenderer, which is kind of a big step for one feature. Our engine comparison puts Three.js and PlayCanvas side by side.

6. Load the splats in Three.js with Spark

Pin your versions. Dustlight uses Three.js 0.186.1 and Spark 2.3.1, and Spark asks for Three.js 0.180.0 or newer.

// npm install three@0.186.1 @sparkjsdev/spark@2.3.1
import * as THREE from 'three';
import { SparkRenderer, SplatMesh } from '@sparkjsdev/spark';

const renderer = new THREE.WebGLRenderer({ canvas });
const scene = new THREE.Scene();
scene.add(new SparkRenderer({ renderer }));  // one SparkRenderer sorts every SplatMesh together

const lighthouse = new SplatMesh({ url: './splats/lighthouse/lighthouse.spz' });
scene.add(lighthouse);
await lighthouse.initialized;

Our Y-up files load upright, so skip the 180-degree turn Spark’s examples give their own sample captures.

Spark also stores splat centers as half floats by default, and a cloud 300 units from the origin broke into visible steps in our test. Keep each SplatMesh near its own origin and place it with mesh.position, or pass extSplats: true. Our Three.js and Claude Code guide covers the rest, from pinning r186 to testing WebGL headless.

Move every splat on the GPU with dyno

With Spark’s dyno system you compose an effect in JavaScript, and Spark turns it into a small GLSL function that runs on every splat, with its center, scales, rotation, color and index to work with.

The dust effect gives each splat a stable random number from its index. Until its ember arrives, a landmark shows only a few thousand splats as swirling neon motes, and then every splat spirals home, lowest first, growing from a speck into its painted color. The whale’s version streams in from the stars.

The lighthouse in Dustlight partway through assembling, a tall column of pink, cyan, lime and yellow motes that gets denser near its base, over a dark island and a glowing cloud sea
The lighthouse 1.4 seconds into its assembly, landing from the ground up.

The light reveal is short enough to show whole, simplified here to gray out the island everywhere except near the lantern.

import { dyno } from '@sparkjsdev/spark';

const lantern = dyno.dynoVec3(new THREE.Vector3());  // the lantern's position, set every frame
const radius = dyno.dynoFloat(5);

island.worldModifier = dyno.dynoBlock({ gsplat: dyno.Gsplat }, { gsplat: dyno.Gsplat }, ({ gsplat }) => {
  const reveal = new dyno.Dyno({
    inTypes: { gsplat: dyno.Gsplat, lantern: 'vec3', radius: 'float' },
    outTypes: { gsplat: dyno.Gsplat },
    statements: ({ inputs, outputs }) => dyno.unindentLines(`
      ${outputs.gsplat} = ${inputs.gsplat};
      vec3 c = ${inputs.gsplat}.rgba.rgb;
      float d = distance(${inputs.gsplat}.center, ${inputs.lantern});
      float k = 1.0 - smoothstep(0.3 * ${inputs.radius}, ${inputs.radius}, d);
      vec3 gray = vec3(dot(c, vec3(0.2126, 0.7152, 0.0722)));
      ${outputs.gsplat}.rgba.rgb = mix(gray, c, k);
    `),
  });
  return { gsplat: reveal.apply({ gsplat, lantern, radius }).gsplat };
});
island.updateGenerator();

// every frame
lantern.value.copy(player.position);
island.updateVersion();

In an earlier 1080p test, a million splats running both effects took about a millisecond of GPU time a frame, the same as with none, not counting Spark’s sort in its worker.

On our RTX 3080 Ti, the finished game’s frame loop kept pace with a 144 Hz display at 1080p, finale included. We haven’t measured a laptop or a phone yet, and touch screens get half the splats, each a little larger.

7. Use the source meshes as colliders

This is where building splats from meshes really pays off. The asset script remeshes every island and landmark, decimates each to about 6,000 triangles and saves it as a 72 KB GLB.

The game loads these with GLTFLoader but never draws them. It casts one ray down for the ground and two ahead to slide along walls, and the camera uses them to pull in. A landmark’s collider joins the world only once it has assembled, so you can’t bump into a lighthouse made of dust.

8. Make the sound in code and the voice with VoxCPM2

Every sound effect comes from one seeded Python script that builds it out of sine waves and noise, and every run writes identical WAV files. The ember chime uses a struck bar’s overtones, at 2.76 and 5.40 times its pitch where a string’s would be whole numbers, and that slightly wrong set is what makes it sound like glass.

The whale’s call is a low hum shaped by a moving resonance, the thing that makes “oo” sound different from “ah”. Our sound guide covers code-made effects with ZzFX next to the AI tools.

The keeper’s voice comes from VoxCPM2, an open-source text-to-speech model that can design a voice from a description. Ours began “An old woman in her seventies, a kind lighthouse keeper”, and it turns out four seeds give you four quite different women. We picked ours by ear once Whisper had checked every word.

Her six lines were cloned from that voice, each with a short direction like “tender and encouraging, like sharing a secret with a child, slow”, and a take didn’t count unless Whisper heard exactly the right words. As you set off, she says “Oh… a light. It has been so long.”

Is Gaussian splatting AI?

Not by itself, though the two are easy to mix up. Kate Yurkova writes in Towards Data Science that “Gaussian splatting doesn’t involve any neural network at all.”

Splats are usually fitted to photos with the kind of optimization machine learning uses, while ours never saw a photo, because Geometry Nodes and the game’s code placed every one. The AI in Dustlight worked around them, from Codex’s drawings to Claude Code’s scripts and VoxCPM2’s voice.

You can play Dustlight in your browser and rate it there. If you’d rather start from a single picture, see image to 3D for games, and how to make a 3D game with AI builds a Three.js game around your models.

Games in this post

More sources

  1. projects.blender.org/blender/blender/pulls/163102

Keep reading

All posts