Skip to main content
3d·November 25, 2024·6 min read

Voxel editing, layers, and the joy of spatial UX

Picking voxels as the editing primitive, building a layers panel that didn't lie, and the UX fixes that made a 3D scene feel like a tool.

J
Jean P.Founder

The first version of the canvas was a 3D scene with a grid, a camera, and a list of objects. You could put a cube in it. That was the whole interaction.

To make it a tool, I needed two things: a primitive that wasn't intimidating to non-artists, and a way to manage what was on the canvas without lying about it.

Why voxels

I picked voxels for the same reason a lot of people pick voxels: Minecraft brain. A generation of people learned that you can build serious things one cube at a time, and that intuition transfers cleanly to a 3D scene editor. You don't have to know what a normal map is. You don't have to think about topology. You point at a cell, you give it a color, you have a thing.

That mattered for the audience I was loosely imagining. Whoever ended up using this tool, they probably weren't going to be Blender users. They might be a DM building a tavern. They might be someone trying to print a small dungeon piece on an Ender 3. The further the editing primitive sat from "open a 3D modeling program," the more people the tool could meet halfway.

The implementation: a fixed-size 3D array, each cell tracking a color and an "occupied" boolean. Painting is a single cell update. Erasing is the same. Re-renders are scoped: Zustand fires only the slice that changed, React Three Fiber re-renders only the meshes that depend on it.

Voxel objects as first-class entities

Early on, voxels were just cells in the grid. There was one voxel grid per canvas, and that was it. The problem with that model is it doesn't compose. If you build a chair, the chair is just a region of the grid. You can't move it. You can't duplicate it. You can't tell the canvas "that chair belongs to layer 2 and that other chair belongs to layer 5."

So voxels became objects. A voxel object is a named bundle of cells with its own bounding region, its own visibility state, and its own place in the layer hierarchy. Building a chair means creating a voxel object, painting cells inside it, and committing it. Now the chair can move. The chair can be hidden. The chair can be on a different layer than the table.

The data model didn't change much: voxel objects still serialize to a sparse cell array. But the editing model changed completely. The canvas went from "one voxel grid" to "a tree of voxel objects, each with its own grid context."

Voxel-to-GLTF, the engineering piece

The export path was where the canvas actually had to do 3D engineering instead of UX work.

The naive version of voxel-to-GLTF creates one cube mesh per voxel. That works until you have a few hundred voxels, at which point the frame rate dies and the export file is enormous. The version that shipped does mesh merging: adjacent voxels of the same color collapse into a single buffer geometry, and it runs the conversion through a small worker so the main thread doesn't stall during export.

This is the kind of work that's only interesting in retrospect. While I was writing it, it was a day of bookkeeping: track which faces are exposed, build the geometry, weld the duplicate vertices, attach the material, ship the GLB. Most of the actual difficulty was in the helper functions, not the algorithm.

The layers panel, by a thousand small fixes

The layers panel is the file with the most touches in this stretch. Twenty-eight commits in a month. There was no single moment where the panel went from "functional list of stuff" to "this actually feels like a tool": it was a hundred small "wait, this is annoying" loops that compounded.

The list, in roughly the order I added them, of things that each made the panel feel a little less like a debugger and a little more like a tool:

  • Auto-select on add. Creating a new voxel object selects it immediately. Before this, you'd add an object and then have to find it in the panel to start editing.
  • Drag-to-reorder. Layers stack, and the stack matters for visibility. Dragging is the right gesture for changing the order. Anything else is wrong.
  • Visibility toggles per object, not just per layer. A layer might have ten objects on it. You don't always want to hide all ten.
  • Labels distinguishing voxel objects from uploaded models. A panel that calls them all "object" makes you guess. A panel that calls them "voxel" or "model" doesn't.
  • Two-column split for model details and preview. Selecting an uploaded model shows you what it looks like without having to find it on the canvas.
  • Tabs for navigation. The panel needed to do more than one thing: list layers and show the active object's properties at minimum. Tabs were the cheapest way to keep both reachable.
  • Selection state synced both ways. Click an object on the canvas, it highlights in the panel. Click an object in the panel, it highlights on the canvas. Either-or is broken.

None of these are interesting individually. The point of writing them down is that "polish" isn't a single insight. It's a queue of small things you notice are wrong and fix one at a time, and the queue is never empty.

Grid snapping and the small unification

The other thing this stretch did was clean up the relationship between the grid and the camera. Before, voxel cell size was hardcoded in three different places. Grid divisions were a constant. Camera max-distance was a magic number.

Cell size became a setting. Grid divisions derive from canvas size. Camera distance derives from grid extent. None of this is glamorous, but it's the kind of cleanup that pays back every time you change one of the inputs and the rest of the scene adjusts on its own.

The shape at the end of the stretch

A canvas you could paint in. Voxel objects you could move, hide, reorder, and label. A layers panel that told you what was actually on the canvas. A GLTF export that didn't melt the browser. A grid that adjusted to the size of the work instead of forcing the work to adjust to it.

Still no D&D. Still no AI. Still no real product. But the thing on screen had stopped feeling like a demo.

SHARE
~ 6 min read · 1,080 words
Discord

Discuss this post

Join the D3 Designs Discord to share thoughts and follow along.

Join Discord