200 lines
8.2 KiB
GDScript3
200 lines
8.2 KiB
GDScript3
|
|
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)
|