obOB STUDIO
Dev Log

Zero External Assets — Generating All Art from Code

5 min read

I don't know Blender and I'm not sorry about it

Every time I open Blender, I accidentally extrude a face, rotate the viewport into oblivion, and close the application. I've tried the donut tutorial three times. I never finished the donut.

So when I started building Oliver's Planes, I made a decision: no external 3D models. No texture files. No asset packs. Every single mesh would be built from Godot's built-in primitives — BoxMesh, CylinderMesh, ConeMesh, SphereMesh. If I couldn't make it from those, it wasn't going in the game.

What started as a limitation became the game's defining aesthetic.

How to build a plane from boxes and cones

A plane is surprisingly simple when you break it down into geometric primitives:

func build_plane():
    # Fuselage: a stretched cylinder
    var fuselage = MeshInstance3D.new()
    var fuse_mesh = CylinderMesh.new()
    fuse_mesh.top_radius = 0.3
    fuse_mesh.bottom_radius = 0.35
    fuse_mesh.height = 3.0
    fuselage.mesh = fuse_mesh
    fuselage.rotation_degrees.x = 90  # Lay it on its side

    # Nose cone
    var nose = MeshInstance3D.new()
    var nose_mesh = ConeMesh.new()
    nose_mesh.radius = 0.35
    nose_mesh.height = 0.8
    nose.mesh = nose_mesh
    nose.position = Vector3(0, 0, -1.9)
    nose.rotation_degrees.x = -90

    # Wings: flat stretched boxes
    var wing = MeshInstance3D.new()
    var wing_mesh = BoxMesh.new()
    wing_mesh.size = Vector3(5.0, 0.08, 0.8)
    wing.mesh = wing_mesh
    wing.position = Vector3(0, 0, 0.2)

    # Tail: smaller box
    var tail = MeshInstance3D.new()
    var tail_mesh = BoxMesh.new()
    tail_mesh.size = Vector3(2.0, 0.06, 0.5)
    tail.mesh = tail_mesh
    tail.position = Vector3(0, 0, 1.4)

    add_child(fuselage)
    add_child(nose)
    add_child(wing)
    add_child(tail)

A cylinder fuselage, a cone nose, a flat box for wings, another flat box for the tail. Add a vertical stabilizer (another thin box, rotated 90°) and landing gear (two small cylinders) and you've got a recognizable airplane.

The magic is in the proportions. Get the wing span right relative to the fuselage length, and the silhouette reads immediately as "airplane." People don't need texture details to recognize shapes — our brains are pattern-matching machines.

Cars from stacked boxes

Oliver's Racers uses the same approach. A car is:

  • Body: A box, slightly wider than it is tall
  • Cabin: A smaller box on top, set back slightly
  • Wheels: Four cylinders, rotated 90° on the Z axis
  • Bumpers: Thin boxes at front and rear
func build_car(color: Color):
    # Body
    var body = MeshInstance3D.new()
    var body_mesh = BoxMesh.new()
    body_mesh.size = Vector3(1.0, 0.4, 2.2)
    body.mesh = body_mesh

    # Cabin (windshield area)
    var cabin = MeshInstance3D.new()
    var cabin_mesh = BoxMesh.new()
    cabin_mesh.size = Vector3(0.9, 0.35, 1.0)
    cabin.mesh = cabin_mesh
    cabin.position = Vector3(0, 0.35, -0.1)

    # Wheels
    for pos in [
        Vector3(-0.5, -0.15, 0.7),  # front-left
        Vector3(0.5, -0.15, 0.7),   # front-right
        Vector3(-0.5, -0.15, -0.7), # rear-left
        Vector3(0.5, -0.15, -0.7),  # rear-right
    ]:
        var wheel = MeshInstance3D.new()
        var wheel_mesh = CylinderMesh.new()
        wheel_mesh.top_radius = 0.18
        wheel_mesh.bottom_radius = 0.18
        wheel_mesh.height = 0.15
        wheel.mesh = wheel_mesh
        wheel.position = pos
        wheel.rotation_degrees.z = 90
        add_child(wheel)

    # Color via material
    var mat = StandardMaterial3D.new()
    mat.albedo_color = color
    body.material_override = mat
    cabin.material_override = mat

Different car types vary the proportions. A sports car has a lower, wider body with a tiny cabin. A truck has a tall cabin and a long rear box. An F1 car is extremely flat with a cone nose. Same primitives, different dimensions.

Trees, terrain, and everything else

Pine trees are two or three stacked cones (biggest at the bottom, smallest on top) on a cylinder trunk. Deciduous trees are a sphere on a cylinder. Both look charming at the scale they appear in the game.

Terrain is the most complex generated asset. It's a subdivided plane mesh with vertices displaced by layered noise:

func generate_terrain(size: int, resolution: int) -> ArrayMesh:
    var noise = FastNoiseLite.new()
    noise.seed = randi()
    noise.frequency = 0.01

    var verts = PackedVector3Array()
    var uvs = PackedVector2Array()

    for z in range(resolution):
        for x in range(resolution):
            var px = (float(x) / resolution - 0.5) * size
            var pz = (float(z) / resolution - 0.5) * size
            var py = noise.get_noise_2d(px, pz) * 15.0
            verts.append(Vector3(px, py, pz))
            uvs.append(Vector2(float(x) / resolution, float(z) / resolution))

    # ... build index array, calculate normals, create mesh

The noise frequency controls how hilly the terrain is. I layer two noise samples — one at low frequency for broad hills, one at high frequency for fine detail. The result looks surprisingly natural.

Buildings in the raceway are just boxes with different aspect ratios. Some have a smaller box on top (air conditioning unit?). A cylinder becomes a water tower. A flat box with stripes becomes a crosswalk.

Why the aesthetic works

There's a word for this kind of look: paper craft. Like someone made a model out of construction paper and geometric shapes. It has the same charm as early PlayStation 1 games or Katamari Damacy — you know it's stylized, and that's part of the appeal.

Players have compared the look to wooden toys, LEGO, and low-poly indie games. These are all compliments in my book. The constraint of "only primitives" forced a consistent visual language across the entire game. Everything looks like it belongs together because everything is made from the same five shapes.

No Blender. No texture files. No asset store purchases. No legal ambiguity about asset licenses. Just code and primitives. And honestly? The games look better for it.

ob

Written by Oliver

Indie game developer building 3D web experiences in Godot and Next.js. I write about creative coding, procedural generation, and game feel.