bgen/demo/scripts/mesh_library_builder.gd

200 lines
8.2 KiB
GDScript3
Raw Permalink Normal View History

2026-04-16 21:04:50 -04:00
class_name MeshLibraryBuilder
extends RefCounted
# Builds a MeshLibrary at runtime for the 3D dungeon demo. No art assets —
# BoxMesh / PlaneMesh primitives with tinted StandardMaterial3D. Each tile
# ID here matches a terrain enum the renderer cares about.
# Tile IDs (exposed as constants so demo_3d.gd can pass them to
# GridMap.set_cell_item).
const TILE_FLOOR := 0
const TILE_WALL := 1
const TILE_DOOR := 2
const TILE_CORRIDOR := 3
const TILE_WATER := 4
const TILE_LAVA := 5
const TILE_BRIMSTONE := 6
const TILE_BRIDGE := 7
const TILE_STAIRS_UP := 8
const TILE_STAIRS_DOWN := 9
# Floors and liquid surfaces are thin slabs centered at the cell origin.
# Walls are tall enough that the player (camera ~2.6u) can't see over them.
# Doors are chest-height so they read as entry markers without blocking view
# along corridors — can raise to full height once we have swinging doors.
const CELL_SIZE := 1.0
const WALL_HEIGHT := 3.0
const FLOOR_THICKNESS := 0.05
const LIQUID_THICKNESS := 0.05
const DOOR_HEIGHT := 0.8
const BRIDGE_THICKNESS := 0.1
const STAIRS_HEIGHT := 0.7
static func build(with_collision: bool = true) -> MeshLibrary:
var lib := MeshLibrary.new()
_add_slab(lib, TILE_FLOOR, "Floor", _mat(Color(0.64, 0.54, 0.42)), FLOOR_THICKNESS, 0.0)
_add_cube(lib, TILE_WALL, "Wall", _mat(Color(0.28, 0.28, 0.32)), WALL_HEIGHT, WALL_HEIGHT * 0.5)
_add_cube(lib, TILE_DOOR, "Door", _mat(Color(0.72, 0.36, 0.20)), DOOR_HEIGHT, DOOR_HEIGHT * 0.5)
_add_slab(lib, TILE_CORRIDOR, "Corridor", _mat(Color(0.44, 0.37, 0.29)), FLOOR_THICKNESS, 0.0)
_add_slab(lib, TILE_WATER, "Water", _mat_trans(Color(0.18, 0.42, 0.78, 0.75)), LIQUID_THICKNESS, -0.05)
_add_slab(lib, TILE_LAVA, "Lava", _mat_emissive(Color(0.90, 0.30, 0.12), Color(1.00, 0.45, 0.15), 1.5), LIQUID_THICKNESS, -0.05)
_add_slab(lib, TILE_BRIMSTONE, "Brimstone", _mat_emissive(Color(0.72, 0.50, 0.22), Color(0.95, 0.65, 0.25), 0.8), LIQUID_THICKNESS, -0.05)
_add_slab(lib, TILE_BRIDGE, "Bridge", _mat(Color(0.55, 0.38, 0.22)), BRIDGE_THICKNESS, 0.0)
_add_ramp(lib, TILE_STAIRS_UP, "StairsUp", _mat(Color(0.74, 0.88, 0.94)), true)
_add_ramp(lib, TILE_STAIRS_DOWN, "StairsDown", _mat(Color(0.20, 0.44, 0.80)), false)
if with_collision:
_attach_collision(lib)
return lib
# Bake the library to a .tres so the CLI and runtime can share the same asset
# instead of rebuilding it each time a scene loads.
static func save_resource(path: String, with_collision: bool = true) -> Error:
return ResourceSaver.save(build(with_collision), path)
# --- collision shapes ---
# Every tile except chasm (which has no tile at all) gets a box collider
# sized to match its mesh. Chasms therefore let the player fall through
# cleanly onto the level below. Stairs are treated as solid blocks; walking
# onto them works well enough with gravity + move_and_slide.
static func _attach_collision(lib: MeshLibrary) -> void:
# Shape, y_offset pairs, in the same geometry as the meshes above.
var floor_shape := _box_shape(Vector3(CELL_SIZE, FLOOR_THICKNESS, CELL_SIZE))
var wall_shape := _box_shape(Vector3(CELL_SIZE, WALL_HEIGHT, CELL_SIZE))
var door_shape := _box_shape(Vector3(CELL_SIZE, DOOR_HEIGHT, CELL_SIZE))
var liquid_shape := _box_shape(Vector3(CELL_SIZE, LIQUID_THICKNESS, CELL_SIZE))
var bridge_shape := _box_shape(Vector3(CELL_SIZE, BRIDGE_THICKNESS, CELL_SIZE))
var stairs_shape := _box_shape(Vector3(CELL_SIZE, STAIRS_HEIGHT, CELL_SIZE))
_set_shape(lib, TILE_FLOOR, floor_shape, FLOOR_THICKNESS * 0.5)
_set_shape(lib, TILE_WALL, wall_shape, WALL_HEIGHT * 0.5)
_set_shape(lib, TILE_DOOR, door_shape, DOOR_HEIGHT * 0.5)
_set_shape(lib, TILE_CORRIDOR, floor_shape, FLOOR_THICKNESS * 0.5)
_set_shape(lib, TILE_WATER, liquid_shape, -0.05 + LIQUID_THICKNESS * 0.5)
_set_shape(lib, TILE_LAVA, liquid_shape, -0.05 + LIQUID_THICKNESS * 0.5)
_set_shape(lib, TILE_BRIMSTONE, liquid_shape, -0.05 + LIQUID_THICKNESS * 0.5)
_set_shape(lib, TILE_BRIDGE, bridge_shape, BRIDGE_THICKNESS * 0.5)
_set_shape(lib, TILE_STAIRS_UP, stairs_shape, STAIRS_HEIGHT * 0.5)
_set_shape(lib, TILE_STAIRS_DOWN, stairs_shape, STAIRS_HEIGHT * 0.5)
static func _box_shape(size: Vector3) -> BoxShape3D:
var b := BoxShape3D.new()
b.size = size
return b
static func _set_shape(lib: MeshLibrary, id: int, shape: Shape3D, y: float) -> void:
var t := Transform3D.IDENTITY
t.origin = Vector3(0, y, 0)
lib.set_item_shapes(id, [shape, t])
# --- material helpers ---
static func _mat(base: Color) -> StandardMaterial3D:
var m := StandardMaterial3D.new()
m.albedo_color = base
m.roughness = 0.8
m.metallic = 0.0
return m
static func _mat_trans(base: Color) -> StandardMaterial3D:
var m := _mat(base)
m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
return m
static func _mat_emissive(base: Color, emis: Color, strength: float) -> StandardMaterial3D:
var m := _mat(base)
m.emission_enabled = true
m.emission = emis
m.emission_energy_multiplier = strength
return m
# --- mesh builders ---
# A flat slab filling the cell's XZ footprint, with the given thickness and
# base at y_base (the slab extends UP from y_base by `thickness`).
static func _add_slab(lib: MeshLibrary, id: int, name: String,
mat: StandardMaterial3D,
thickness: float, y_base: float) -> void:
var mesh := BoxMesh.new()
mesh.size = Vector3(CELL_SIZE, thickness, CELL_SIZE)
mesh.material = mat
var t := Transform3D.IDENTITY
t.origin = Vector3(0, y_base + thickness * 0.5, 0)
_install(lib, id, name, mesh, t)
# A cube centered on the cell. y_center is the Y of the cube's center.
static func _add_cube(lib: MeshLibrary, id: int, name: String,
mat: StandardMaterial3D,
height: float, y_center: float) -> void:
var mesh := BoxMesh.new()
mesh.size = Vector3(CELL_SIZE, height, CELL_SIZE)
mesh.material = mat
var t := Transform3D.IDENTITY
t.origin = Vector3(0, y_center, 0)
_install(lib, id, name, mesh, t)
# Simple visual ramp using two cubes at different heights. Not geometrically
# correct stairs — just enough to read as "stairs" from flight.
# going_up=true means the tall step is at +Z; false means the tall step is
# at -Z (visually going down).
static func _add_ramp(lib: MeshLibrary, id: int, name: String,
mat: StandardMaterial3D, going_up: bool) -> void:
# GridMap.set_mesh expects a single Mesh. Fake the steps by stacking two
# BoxMeshes into an ArrayMesh via SurfaceTool.
var st := SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
st.set_material(mat)
var tall := STAIRS_HEIGHT
var short := STAIRS_HEIGHT * 0.35
var half := CELL_SIZE * 0.25
# Step 1: back (taller going up, shorter going down).
var z_back := half if going_up else -half
_append_box(st, Vector3(0, (tall if going_up else short) * 0.5, z_back),
Vector3(CELL_SIZE, (tall if going_up else short), CELL_SIZE * 0.5))
# Step 2: front.
var z_front := -half if going_up else half
_append_box(st, Vector3(0, (short if going_up else tall) * 0.5, z_front),
Vector3(CELL_SIZE, (short if going_up else tall), CELL_SIZE * 0.5))
var arr := st.commit()
_install(lib, id, name, arr, Transform3D.IDENTITY)
static func _append_box(st: SurfaceTool, center: Vector3, size: Vector3) -> void:
var h := size * 0.5
var c := center
var v := [
c + Vector3(-h.x, -h.y, -h.z), # 0
c + Vector3( h.x, -h.y, -h.z), # 1
c + Vector3( h.x, h.y, -h.z), # 2
c + Vector3(-h.x, h.y, -h.z), # 3
c + Vector3(-h.x, -h.y, h.z), # 4
c + Vector3( h.x, -h.y, h.z), # 5
c + Vector3( h.x, h.y, h.z), # 6
c + Vector3(-h.x, h.y, h.z), # 7
]
# 6 faces, each 2 tris, wound CCW with outward normals.
var faces := [
[0, 1, 2, 3, Vector3(0, 0, -1)],
[5, 4, 7, 6, Vector3(0, 0, 1)],
[1, 5, 6, 2, Vector3( 1, 0, 0)],
[4, 0, 3, 7, Vector3(-1, 0, 0)],
[3, 2, 6, 7, Vector3(0, 1, 0)],
[4, 5, 1, 0, Vector3(0, -1, 0)],
]
for f in faces:
var n: Vector3 = f[4]
for i in [0, 1, 2, 0, 2, 3]:
st.set_normal(n)
st.add_vertex(v[f[i]])
# Register a mesh + transform with the library under the given ID.
static func _install(lib: MeshLibrary, id: int, name: String,
mesh: Mesh, t: Transform3D) -> void:
lib.create_item(id)
lib.set_item_name(id, name)
lib.set_item_mesh(id, mesh)
lib.set_item_mesh_transform(id, t)