YAML-driven house composition with pluggable generators and asset.luckyrobots.com integration
The original Procedural Building Generation proposal is a solid C++ mesh-generation system for walls, doors, windows, and floors -- and we want to keep all of that code. The problem is its architecture: BuildingGenerator sits at the top and owns the whole pipeline, and room-specific things like the KitchenLayoutBuilder get bolted on as post-materialization hooks dispatched by room type. That means every new room feature (kitchen cabinets, garage doors, sliding doors, closet shelving) needs its own special-case integration path into the building generator.
In practice, all of these are the same class of problem -- composing assets and procedural geometry inside a room. A kitchen cabinet run along a wall is structurally no different from a garage door filling an opening or a sliding door on a rail. They all need room bounds, wall references, and access to both procedural mesh generation and downloadable assets. Making each one a separate post-hoc dispatch creates a system that gets harder to extend with every new room feature.
What we propose instead: flip the hierarchy. Instead of the building generator being the top-level system, make it one generator among many, all driven by a single YAML house definition file. The YAML describes the entire house -- structure, rooms, furniture, generators -- and a HouseLoader orchestrates everything in one pass. The original gist's wall/floor/ceiling code becomes the structural_generator. Kitchen cabinets become the cabinet_generator. Garage doors, sliding doors, future closets and staircases -- they're all just generators registered in the same registry, invoked by the same YAML syntax. No special cases, no post-materialization hacks, and artists can define entire houses without touching C++.
Concretely, this document proposes:
- A YAML house definition format where the house is a tree of rooms, each typed (kitchen, living_room, garage, etc.)
- Rooms contain asset placements (positioned references to assets from
assets.luckyrobots.comwithbakedandmujoco_enabledflags) and generator invocations (pluggable helper systems like cabinet_generator, garage_door_generator, sliding_door_generator) - The procedural building code from the original gist becomes the structural generator -- all its classes (MeshBuilder, WallGenerator, etc.) are preserved, just wrapped in the generator interface
- A generator registry so new generators can be added with one class + one registration call, no changes to the loader or room types
# house.yaml โ complete house definition
house:
name: "Suburban Home v1"
version: 1
# Structural shell โ walls, floors, ceilings generated procedurally
structure:
floors:
- id: ground_floor
height: 2.7
wall_thickness: 0.15
ceiling_thickness: 0.12
footprint: # outer boundary polygon (meters, CW winding)
- [0, 0]
- [12, 0]
- [12, 8]
- [0, 8]
# Room definitions โ children of a floor
rooms:
# โโ Kitchen โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- id: kitchen
type: kitchen # triggers kitchen-specific generators
floor: ground_floor
bounds:
origin: [0, 0]
size: [4, 3.5]
# Generator invocation โ kitchen cabinets generator fills the walls
generators:
- type: cabinet_generator
config:
layout: L-shape # L-shape | U-shape | galley | single-wall
counter_height: 0.9
upper_cabinet_height: 0.7
walls: [north, west] # which walls get cabinets
countertop_material: granite_dark
cabinet_style: shaker
appliance_slots:
- wall: north
offset: 1.2
appliance: sink
- wall: north
offset: 2.4
appliance: dishwasher
- wall: west
offset: 0.6
appliance: oven
# Explicit asset placements
assets:
- slug: modern-refrigerator # fetched from assets.luckyrobots.com
position: [3.5, 0, 0.4]
rotation: [0, 90, 0]
baked: true # pre-baked lighting/textures
mujoco_enabled: false # no physics sim needed
- slug: kitchen-island-marble
position: [2.0, 0, 1.8]
rotation: [0, 0, 0]
baked: false
mujoco_enabled: true # interactive in sim
# Openings โ doors/windows cut into the structural walls
openings:
- wall: south
offset: 1.5
type: doorway # open archway, no door asset
width: 0.9
height: 2.1
- wall: east
offset: 0.5
type: window
width: 1.2
height: 1.0
sill_height: 0.9
# โโ Living Room โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- id: living_room
type: living_room
floor: ground_floor
bounds:
origin: [4, 0]
size: [5, 5]
assets:
- slug: modern-sofa-grey
position: [2.5, 0, 3.5]
rotation: [0, 180, 0]
baked: true
mujoco_enabled: false
- slug: coffee-table-wood
position: [2.5, 0, 2.0]
baked: true
mujoco_enabled: true
- slug: tv-65inch-mounted
position: [2.5, 1.4, 0.05] # wall-mounted
rotation: [0, 0, 0]
baked: true
mujoco_enabled: false
- slug: bookshelf-walnut
position: [0.1, 0, 2.5]
rotation: [0, 90, 0]
baked: false
mujoco_enabled: false
openings:
- wall: south
offset: 1.5
type: sliding_door
width: 2.4
height: 2.1
generator: sliding_door_generator # delegates to helper generator
config:
panels: 2
frame_material: aluminum_black
- wall: west # shared wall with kitchen
offset: 1.5
type: doorway
width: 0.9
height: 2.1
# โโ Garage โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- id: garage
type: garage
floor: ground_floor
bounds:
origin: [9, 0]
size: [3, 5]
generators:
- type: garage_door_generator
config:
wall: south
width: 2.6
height: 2.3
style: sectional # sectional | roll-up | swing
material: steel_white
mujoco_enabled: true # door is openable in sim
assets:
- slug: metal-shelf-unit
position: [0.2, 0, 4.5]
baked: false
mujoco_enabled: false
- slug: workbench-heavy-duty
position: [1.5, 0, 4.5]
baked: false
mujoco_enabled: true
openings:
- wall: west
offset: 2.0
type: door
width: 0.85
height: 2.1
config:
style: panel
handle_side: left
# โโ Hallway โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- id: hallway
type: hallway
floor: ground_floor
bounds:
origin: [4, 5]
size: [5, 3]
assets:
- slug: coat-rack-modern
position: [0.3, 0, 1.5]
baked: true
mujoco_enabled: false
openings:
- wall: north
offset: 2.0
type: door
width: 1.0
height: 2.1
config:
style: front_door
material: wood_oakflowchart TD
YAML["house.yaml"]
HL["HouseLoader"]
GR["GeneratorRegistry"]
AR["AssetResolver"]
SCENE["Scene (Entity Hierarchy)"]
YAML -->|"parsed by"| HL
HL -->|"looks up generators"| GR
HL -->|"resolves asset slugs"| AR
HL -->|"builds"| SCENE
subgraph Generators
SG["StructuralGenerator"]
CG["CabinetGenerator"]
SDG["SlidingDoorGenerator"]
GDG["GarageDoorGenerator"]
end
GR --> SG
GR --> CG
GR --> SDG
GR --> GDG
SG -->|"creates walls, floors, ceilings"| SCENE
CG -->|"creates cabinets, countertops"| SCENE
SDG -->|"creates sliding doors"| SCENE
GDG -->|"creates garage doors"| SCENE
AR -->|"downloads GLBs, creates entities"| SCENE
API["assets.luckyrobots.com"]
CACHE["Local GLB Cache"]
AR <-->|"HTTP fetch"| API
AR <-->|"read/write"| CACHE
classDiagram
class IGenerator {
<<interface>>
+GetType() string
+Generate(ctx GeneratorContext, config YAML::Node) void
}
class GeneratorContext {
+Room : RoomDefinition&
+Floor : FloorDefinition&
+ParentEntity : Entity
+Scene : Scene*
}
class GeneratorRegistry {
-m_Generators : map~string, unique_ptr~IGenerator~~
+Register(type string, gen unique_ptr~IGenerator~) void
+Get(type string) IGenerator*
}
class HouseLoader {
-m_Registry : GeneratorRegistry
-m_AssetResolver : AssetResolver
+LoadFromYAML(path string, scene Scene*) Entity
-ParseStructure(node YAML::Node) HouseDefinition
-BuildRoom(def RoomDefinition, parent Entity) void
}
class AssetResolver {
+Resolve(slug string) AssetHandle
-DownloadGlb(slug string) path
-CheckCache(slug string) optional~path~
}
class StructuralGenerator {
+GetType() string
+Generate(ctx, config) void
-m_WallGen : WallGenerator
-m_CeilingGen : CeilingGenerator
-m_DoorGen : DoorGenerator
-m_WindowGen : WindowGenerator
}
class CabinetGenerator {
+GetType() string
+Generate(ctx, config) void
-GenerateLowerCabinets(wall, config) void
-GenerateUpperCabinets(wall, config) void
-GenerateCountertop(wall, config) void
-PlaceApplianceSlots(slots) void
}
class SlidingDoorGenerator {
+GetType() string
+Generate(ctx, config) void
}
class GarageDoorGenerator {
+GetType() string
+Generate(ctx, config) void
}
IGenerator <|.. StructuralGenerator
IGenerator <|.. CabinetGenerator
IGenerator <|.. SlidingDoorGenerator
IGenerator <|.. GarageDoorGenerator
GeneratorRegistry o-- IGenerator : stores
HouseLoader *-- GeneratorRegistry
HouseLoader *-- AssetResolver
HouseLoader ..> GeneratorContext : creates
class WallGenerator {
+Generate(walls, openings) GeneratedMeshData[]
}
class CeilingGenerator {
+Generate(bounds, thickness) GeneratedMeshData
}
class DoorGenerator {
+Generate(opening OpeningSpec) GeneratedMeshData[]
}
class WindowGenerator {
+Generate(opening OpeningSpec) GeneratedMeshData[]
}
class MeshBuilder {
+AddQuad(verts) void
+AddBox(origin, size) void
+AddPlaneXZ(origin, size) void
+Build() GeneratedMeshData
}
StructuralGenerator *-- WallGenerator
StructuralGenerator *-- CeilingGenerator
StructuralGenerator *-- DoorGenerator
StructuralGenerator *-- WindowGenerator
WallGenerator ..> MeshBuilder : uses
CeilingGenerator ..> MeshBuilder : uses
DoorGenerator ..> MeshBuilder : uses
WindowGenerator ..> MeshBuilder : uses
CabinetGenerator ..> MeshBuilder : uses
classDiagram
class HouseDefinition {
+name : string
+version : int
+structure : StructureDefinition
+rooms : RoomDefinition[]
}
class StructureDefinition {
+floors : FloorDefinition[]
}
class FloorDefinition {
+id : string
+height : float
+wall_thickness : float
+ceiling_thickness : float
+footprint : vec2[]
}
class RoomDefinition {
+id : string
+type : string
+floor : string
+bounds : RoomBounds
+generators : GeneratorInvocation[]
+assets : AssetPlacement[]
+openings : OpeningSpec[]
}
class RoomBounds {
+origin : vec2
+size : vec2
}
class GeneratorInvocation {
+type : string
+config : YAML::Node
}
class AssetPlacement {
+slug : string
+position : vec3
+rotation : vec3
+baked : bool
+mujoco_enabled : bool
}
class OpeningSpec {
+wall : string
+offset : float
+type : string
+width : float
+height : float
+sill_height : float
+generator : string
+config : YAML::Node
}
HouseDefinition *-- StructureDefinition
HouseDefinition *-- RoomDefinition
StructureDefinition *-- FloorDefinition
RoomDefinition *-- RoomBounds
RoomDefinition *-- GeneratorInvocation
RoomDefinition *-- AssetPlacement
RoomDefinition *-- OpeningSpec
graph TD
H["House (Entity)"]
GF["GroundFloor"]
W["Walls (procedural mesh)"]
F["Floor (procedural mesh)"]
C["Ceiling (procedural mesh)"]
K["Kitchen"]
CAB["Cabinets (generated)"]
UC0["UpperCabinet_N_0"]
UC1["UpperCabinet_N_1"]
LC0["LowerCabinet_N_0"]
CT["Countertop_N"]
FRIDGE["Refrigerator (asset, baked)"]
ISLAND["KitchenIsland (asset, mujoco)"]
WIN["Window_E_0 (procedural)"]
LR["LivingRoom"]
SOFA["Sofa (asset, baked)"]
TABLE["CoffeeTable (asset, mujoco)"]
TV["TV (asset, baked)"]
SLIDE["SlidingDoor_S (generated, mujoco)"]
G["Garage"]
GDOOR["GarageDoor_S (generated, mujoco)"]
SHELF["ShelfUnit (asset)"]
BENCH["Workbench (asset, mujoco)"]
H --> GF
GF --> W
GF --> F
GF --> C
GF --> K
GF --> LR
GF --> G
K --> CAB
CAB --> UC0
CAB --> UC1
CAB --> LC0
CAB --> CT
K --> FRIDGE
K --> ISLAND
K --> WIN
LR --> SOFA
LR --> TABLE
LR --> TV
LR --> SLIDE
G --> GDOOR
G --> SHELF
G --> BENCH
sequenceDiagram
participant User
participant HL as HouseLoader
participant GR as GeneratorRegistry
participant SG as StructuralGenerator
participant RG as Room Generators
participant AR as AssetResolver
participant API as assets.luckyrobots.com
participant S as Scene
User->>HL: LoadFromYAML("house.yaml")
HL->>HL: Parse YAML โ HouseDefinition
loop Each Floor
HL->>GR: Get("structural")
GR-->>HL: StructuralGenerator
HL->>SG: Generate(floor footprint, all room openings)
SG->>S: Create Wall/Floor/Ceiling entities
loop Each Room
HL->>S: Create room parent entity
loop Each Generator Invocation
HL->>GR: Get(generator.type)
GR-->>HL: e.g. CabinetGenerator
HL->>RG: Generate(room context, config)
RG->>S: Create generated entities
end
loop Each Asset Placement
HL->>AR: Resolve(slug)
alt Cache hit
AR-->>HL: cached AssetHandle
else Cache miss
AR->>API: GET /api/download?slug=...
API-->>AR: .glb file
AR->>AR: Cache locally
AR-->>HL: new AssetHandle
end
HL->>S: Create entity (position, rotation, baked, mujoco flags)
end
end
end
HL-->>User: Root house Entity
Room types serve as semantic tags + default behavior, not rigid classes. A room type can:
- Suggest which generators are available (kitchen -> cabinet_generator)
- Provide defaults (garage -> overhead lighting, concrete floor material)
- Enable type-specific validation (kitchen must have at least one sink slot)
graph LR
subgraph Room_Types["Room Types (templates)"]
kitchen
living_room
bedroom
bathroom
garage
hallway
dining_room
laundry_room
office
end
subgraph Generators["Helper Generators (pluggable)"]
cab[cabinet_generator]
slide[sliding_door_generator]
gdoor[garage_door_generator]
closet[closet_generator ๐ฎ]
shelf[shelf_generator ๐ฎ]
stair[staircase_generator ๐ฎ]
rail[railing_generator ๐ฎ]
end
kitchen -.->|"default"| cab
living_room -.-> slide
garage -.->|"default"| gdoor
bedroom -.-> closet
bathroom -.-> shelf
style closet stroke-dasharray: 5 5
style shelf stroke-dasharray: 5 5
style stair stroke-dasharray: 5 5
style rail stroke-dasharray: 5 5
Dashed generators are future additions. The dashed arrows show default associations -- any room can invoke any generator explicitly in YAML.
Room types are NOT code classes. They're string tags in the YAML. The engine uses them to:
- Pick defaults when a field is omitted
- Validate that generator configs make sense
- Provide editor UX hints (e.g. show cabinet config panel when room type = kitchen)
Every generator implements a simple contract:
namespace Hazel::ProceduralBuilding {
struct GeneratorContext
{
const RoomDefinition& Room; // parsed from YAML
const FloorDefinition& Floor; // parent floor
Entity ParentEntity; // scene entity to attach children to
Scene* Scene;
};
// All generators implement this
class IGenerator
{
public:
virtual ~IGenerator() = default;
virtual std::string GetType() const = 0;
virtual void Generate(const GeneratorContext& ctx, const YAML::Node& config) = 0;
};
}| Generator | What it produces | Inputs |
|---|---|---|
structural_generator |
Walls, floor slab, ceiling slab, wall cutouts for openings | Floor footprint, room bounds, openings list |
cabinet_generator |
Upper/lower cabinets, countertops, filler panels | Layout shape, wall list, appliance slots, style |
garage_door_generator |
Sectional/roll-up door with tracks | Wall, dimensions, style, mujoco flag |
sliding_door_generator |
Multi-panel sliding door with frame and rail | Panel count, dimensions, frame material |
// In HouseLoader initialization
GeneratorRegistry::Register("structural", std::make_unique<StructuralGenerator>());
GeneratorRegistry::Register("cabinet_generator", std::make_unique<CabinetGenerator>());
GeneratorRegistry::Register("garage_door_generator", std::make_unique<GarageDoorGenerator>());
GeneratorRegistry::Register("sliding_door_generator", std::make_unique<SlidingDoorGenerator>());This is the existing "kitchen configurator" concept, scoped properly as a generator:
generators:
- type: cabinet_generator
config:
layout: L-shape
walls: [north, west]
counter_height: 0.9
upper_cabinet_height: 0.7
upper_gap: 0.45 # gap between counter and upper cabinets
cabinet_depth: 0.6
upper_cabinet_depth: 0.35
countertop_overhang: 0.02
countertop_material: granite_dark
cabinet_style: shaker # shaker | flat | raised_panel
appliance_slots:
- wall: north
offset: 1.2
appliance: sink
width: 0.8
- wall: north
offset: 2.4
appliance: dishwasher
width: 0.6
- wall: west
offset: 0.6
appliance: oven
width: 0.6
- wall: west
offset: 1.8
appliance: range_hood
width: 0.6
mount: upper # replaces upper cabinet at this positionThe generator:
- Walks each specified wall segment
- Places lower cabinets at regular intervals, skipping appliance slots
- Places countertop as continuous slab with sink cutout
- Places upper cabinets, skipping range_hood / window positions
- Each piece is either a procedural mesh (MeshBuilder boxes) or an asset from
assets.luckyrobots.com
Every slug in the YAML refers to an asset on assets.luckyrobots.com. The system needs to know two things about each asset:
| Field | Meaning |
|---|---|
baked: true/false |
Whether the asset has pre-baked lighting/textures. Baked assets skip dynamic lighting passes. |
mujoco_enabled: true/false |
Whether the asset participates in MuJoCo physics. If true, it gets a MuJoCo body + geom at export time. |
1. YAML references: slug: "modern-refrigerator"
2. HouseLoader checks local GLB cache (~/.cache/LuckyEngine/AssetLibrary/glb/)
3. Cache miss โ HTTP GET assets.luckyrobots.com/api/search?slug=modern-refrigerator
4. Download .glb to cache
5. Import into engine asset system (AssetManager::ImportStaticMesh)
6. Create entity with StaticMeshComponent
7. Tag with baked/mujoco_enabled metadata
This reuses the existing AssetLibraryBrowser download pipeline (StartGlbDownload, GetGlbCacheDir) but drives it programmatically instead of through the UI.
The gist proposes these classes:
MeshBuilder, WallGenerator, DoorGenerator, WindowGenerator,
CeilingGenerator, FloorGenerator, BuildingGenerator
In this system, all of those become the structural_generator:
| Gist Class | Role in This System |
|---|---|
BuildingGenerator |
HouseLoader (top-level orchestrator) |
FloorGenerator |
Part of structural_generator -- generates one floor's shell |
WallGenerator |
Part of structural_generator -- generates wall panels with cutouts |
CeilingGenerator |
Part of structural_generator -- generates floor/ceiling slabs |
DoorGenerator |
Invoked by structural_generator for standard doors; sliding_door_generator and garage_door_generator handle specialized doors |
WindowGenerator |
Part of structural_generator |
MeshBuilder |
Shared utility used by ALL generators |
The gist's RoomType enum and post-materialization kitchen dispatch are replaced by the generator system. Instead of materializing the building first and then hacking in kitchens after the fact, rooms declare their generators upfront and everything is built in one pass.
Hazel/src/Hazel/ProceduralGeneration/
โโโ HouseDefinition.h # YAML schema structs (HouseDef, RoomDef, AssetPlacement, etc.)
โโโ HouseLoader.h/.cpp # Parses YAML, orchestrates generation
โโโ GeneratorRegistry.h/.cpp # Maps type strings to IGenerator instances
โโโ IGenerator.h # Generator interface
โโโ AssetResolver.h/.cpp # Slug โ cached GLB โ imported asset handle
โ
โโโ Structural/ # The gist's building generation code
โ โโโ MeshBuilder.h/.cpp
โ โโโ WallGenerator.h/.cpp
โ โโโ DoorGenerator.h/.cpp
โ โโโ WindowGenerator.h/.cpp
โ โโโ CeilingGenerator.h/.cpp
โ โโโ StructuralGenerator.h/.cpp # IGenerator impl, orchestrates the above
โ
โโโ Kitchen/
โ โโโ CabinetGenerator.h/.cpp # IGenerator impl
โ
โโโ Doors/
โ โโโ SlidingDoorGenerator.h/.cpp # IGenerator impl
โ โโโ GarageDoorGenerator.h/.cpp # IGenerator impl
โ
โโโ (future)/
โโโ ClosetGenerator.h/.cpp
โโโ StaircaseGenerator.h/.cpp
โโโ ...
- Define
HouseDefinition.hstructs (YAML schema โ C++ structs) - Implement
HouseLoaderYAML parsing (using existing yaml-cpp dependency) - Implement
IGeneratorinterface andGeneratorRegistry - Implement
AssetResolver(extract download logic fromAssetLibraryBrowserinto reusable service)
- Port
MeshBuilder,WallGenerator,CeilingGeneratorfrom the gist - Implement
StructuralGeneratoras anIGenerator - Wall cutout algorithm (gap approach from gist)
DoorGenerator+WindowGeneratorfor standard openings
- Implement
CabinetGeneratoras anIGenerator - Layout algorithms: single-wall, L-shape, U-shape, galley
- Appliance slot system
- Countertop generation with sink cutouts
- Material assignment
SlidingDoorGeneratorโ multi-panel sliding doors with rail systemGarageDoorGeneratorโ sectional door with tracks, MuJoCo-enabled
- "Load House YAML" in editor menu
- Per-room property panel showing generator config
- Live preview: edit YAML โ regenerate โ see changes
- Export to
.hscenefor runtime use
-
YAML over C++ config structs โ Houses are data, not code. Artists and level designers edit YAML files, not recompile C++.
-
Generators are plugins, not inheritance โ Adding a new generator (e.g.
staircase_generator) means writing one class and registering it. No modification to HouseLoader or room types. -
Kitchen cabinets are a generator, not a separate system โ The "kitchen configurator" is just
cabinet_generatorinvoked inside a room of typekitchen. Same pattern for garage doors, sliding doors, closets, etc. -
bakedandmujoco_enabledare per-asset flags โ A baked sofa and a physics-enabled coffee table can coexist in the same room. These flags travel with the asset placement, not the room. -
Structural generation is one generator among many โ The gist's
BuildingGeneratordoesn't own the world. It generates the shell (walls/floors/ceilings), and other generators fill the rooms. -
Asset slugs resolve at load time โ The YAML doesn't embed geometry. It references
assets.luckyrobots.comslugs that get downloaded and cached. This keeps house files tiny and assets centrally managed.