Project hub

Navigate projects by number

Project 03.
A Cube-Bound Game Engine Prototype, Built with Three.js
Film Database Development Project
Ecommerce Web Development Project

Project Insights Navigation Links

A Cube-Bound Game Engine Prototype, Built with Three.js

TL;DR: Built a custom Three.js game-engine prototype around a cube-based colony-management concept. It features six independent world surfaces, a fixed-step simulation loop, procedural geometry, PBR lighting, raycast-based placement, a typed grid system, and a React-to-engine architecture designed around strict ownership boundaries.

The Idea: A Colony Game on a Cube

Fallout Shelter was the initial spark, but the actual goal was an excuse to build something from the ground up in Three.js and learn the engine layer. Blender came with it, since the prototype needed assets. Game development was an interest, not a goal. Its colony-management concept gave the project a clear target to build toward.

A flat management game solves a lot of spatial problems for you: everything shares one plane, one coordinate space, one set of adjacencies. Putting the world on a cube takes those assumptions away. In the concept, threats would arrive on any of six faces with directional indication of their origin, and construction would be spread across separate surfaces. None of that is implemented. It is the target the engine work was aimed at, and the reason the spatial problems below were worth solving.

Learning the Asset Pipeline

The first step was into Blender, building facility and character models to support the vision of the project.

Early Blender modeling and sculpting experimentsEarly Blender modeling and sculpting experiments
Early Blender experiments. The obligatory donut, followed by a first attempt at sculpting. Valuable experience, but also a reminder that modeling is its own specialization.

After learning the shortcuts and fundamentals, it became clear that asset creation was not a supporting task, but its own discipline requiring a time investment that could not be justified. Since the goal was to explore engine and simulation development, the project pivoted to CC0 assets and focused on the systems that would make the prototype feel complete.

Building the Engine Layer

The real challenge came next: bridging the gap between application development and real-time rendering. Concepts that had never come up in application work became foundational: coordinate spaces, camera trade-offs, texture pipelines, buffers, and navigation meshes. Resources from YouTubers SimonDev and Vercidium, alongside Three.js documentation, helped build that mental model. Once those fundamentals were in place, building a scene, camera, renderer, and mesh came together quickly.

The engine runs on an internal fixed-step simulation loop. The loop accumulates real elapsed time and drains it in discrete steps, keeping simulation updates independent from frame rate while capping any single delta to prevent a backgrounded tab from forcing an excessive backlog of updates when it returns. Work is separated based on whether it requires determinism: the day/night cycle advances on the fixed step, while parallax scrolling, light orientation, and rendering update once per frame on the variable step.

The camera is orthographic rather than perspective, a deliberate fit for a management surface: it holds cell scale constant across the entire grid and removes the foreshortening that would otherwise distort the faces angled away from the viewer. That choice helped later, too. With no perspective in the projection, a rendering artifact that looked like a camera trick could be ruled out as one on sight.

Representing a World with Six Surfaces

Representing the cube as a single continuous navigation mesh is a deceptively awkward problem. Every system that reasons about position, movement, placement, or adjacency must also understand face transitions. Those concerns were kept local instead: each face would own its own coordinate space, so systems could reason about individual surfaces and not one continuous world.

Although the mesh never made it into the prototype, its sizing was worked out first: derived from human-scale movement, using a 2.5 ft step against 2 ft cells. This keeps polygon density tied to the floor geometry and keeps it deterministic, since it is never influenced by the number or size of entities moving across it.

The approach settled on was simpler: positioning and rotating six independent 2D grids inside a parent group to represent the surface of a single solid cube. The trade-off was that every system now operates across six independent planar coordinate spaces, which keeps the architecture local but makes a single continuous rounded-cube representation impractical.

Each face therefore owns its own placement mesh, parallax layers, and placed facilities. This architecture exposed another constraint: a cube assembled this way does not naturally maintain a consistent center point. Each face carries a transform offset that maps local grid coordinates into the shared world coordinate system.

That constraint pushed the codebase toward a strict ownership model, where every value has a single source of truth and reusable resources such as geometry, textures, and meshes are created once and cloned rather than rebuilt.

Procedural Geometry: Closing the Cube

A consequence of placing 3D objects on independent 2D surfaces is that their depth creates protrusions, leaving gaps between neighboring faces. To close them, a rounded cube frame was built. Each of the twelve edges is modeled as a quarter cylinder, at a radius set by the room depth so the curve naturally meets the adjacent faces.

The vertices are filled with SphereGeometry at identical radii to close the surface. Segment counts for both are derived from a tolerance value instead of being hardcoded, so the curve stays just as smooth if the radius changes.

The frame also leaves room for a future directional threat indicator, where the structure itself communicates which face is under attack.

Lighting a Floating World

Lighting took the longest to get right. A sun and moon follow a day/night cycle. Because the cube floats in open space with no ground to anchor them to, both track the camera so every face stays legible from any angle.

The frame's metal shows almost no colour of its own, so with nothing around to reflect it renders nearly black. A procedurally generated room is used as the reflection source, which avoids authoring a lighting environment by hand. ACES tone mapping is enabled on the renderer so bright highlights roll off instead of clipping to flat white, the same way a photo holds detail in a bright sky.

Getting there meant working through a chain of defects that could only be resolved by measuring, not guessing. The sun and moon had initially both been ambient lights, so nothing in the scene was directionally lit at all. Once that was fixed, they crossed the horizon at the same instant, flattening the whole cube twice a cycle.

The same day/night cycle reaches past the frame to the backdrop. Behind each face, a nine-layer parallax skyline scrolls at staggered speeds to fake depth, and its tint shifts with the cycle so the city behind the grid reads as dawn, day, or dusk along with everything in front of it.

State, Placement, and the React Boundary

Facility placement introduced the need for explicit boundaries, which meant defining a logical cell grid. The visible grid is represented as a single line-segment mesh, with positions packed into aFloat32Array and indices into a Uint16Array, wide enough for the grid's few hundred vertices and lighter than the 32-bit index buffer that would otherwise be the default.

Alongside the render mesh sits a flat array of logical cells that owns occupancy state, keeping collision and bounds checks independent from the scene graph. The visual representation can change without changing the underlying placement rules, keeping rendering concerns separate from application state.

Placement is handled by dragging from the interface onto the canvas, raycasting against an invisible plane for the active face, and converting the hit position into a grid coordinate.

The React interface and the engine mount as separate modules that share no state, bridged by two narrow channels. The engine reads from those and nothing else, so the entire interface could be swapped out without the renderer or simulation layer noticing.

Fixing the First Placement Stall

The first facility placement stalled, and only the first. Every reusable resource is built once at startup and cloned on each placement, so that drop was expected to be as cheap as the rest. Building geometry and materials in JavaScript never touches the GPU. Three.js defers shader compilation and texture uploads to the first frame an object is drawn, so the entire cost for the facility walls, several 2K maps among them, landed the moment the first room appeared.

The fix moves that work to load time. Without rendering a frame, compileAsync links the shaders ahead of time, then the materials are walked and initTexture is called on each map to force the uploads compilation does not trigger. Every placement clones those same materials, so the first is no longer distinguishable from the rest.

Future Work

This is a prototype and an engine, not a finished game. With that said, I would revisit this project to pursue systems such as raytraced indicator mesh models for incoming threats.