extends Node3D ## Terrain generator — M0 placeholder. ## ## Builds ONE heightmap chunk from deterministic noise, so the world already ## looks like rolling ground rather than a flat plane. Single chunk, generated at ## _ready(). Chunk streaming, biomes and server-side determinism (see ## docs/ARCHITECTURE.md §4) come in later milestones. ## ## Collision uses MeshInstance3D.create_trimesh_collision() (mesh-derived), which ## registers reliably in the physics space. A hand-built ConcavePolygonShape3D ## via set_faces() did NOT register (player/rays passed through) — avoid it. ## ## Exposes `sample_height(x, z)` so other systems (player spawn, prop placement) ## can sit exactly on the surface. signal ready_built ## emitted once the mesh + collision exist @export var size: int = 128 # chunk is size x size metres @export var resolution: int = 96 # vertices per side (higher = smoother/heavier) @export var height_scale: float = 10.0 @export var noise_seed: int = 1337 var _noise: FastNoiseLite var built: bool = false func _ready() -> void: _build() func _ensure_noise() -> void: if _noise != null: return _noise = FastNoiseLite.new() _noise.seed = noise_seed _noise.noise_type = FastNoiseLite.TYPE_PERLIN _noise.frequency = 0.012 _noise.fractal_octaves = 4 func _build() -> void: _ensure_noise() var st := SurfaceTool.new() st.begin(Mesh.PRIMITIVE_TRIANGLES) var step := float(size) / float(resolution) var half := size * 0.5 var uv_scale := 1.0 / float(size) for z in range(resolution): for x in range(resolution): var x0 := x * step - half var z0 := z * step - half var x1 := (x + 1) * step - half var z1 := (z + 1) * step - half var p00 := Vector3(x0, sample_height(x0, z0), z0) var p10 := Vector3(x1, sample_height(x1, z0), z0) var p01 := Vector3(x0, sample_height(x0, z1), z1) var p11 := Vector3(x1, sample_height(x1, z1), z1) _add_tri(st, p00, p01, p11, half, uv_scale) _add_tri(st, p00, p11, p10, half, uv_scale) st.generate_normals() var mesh := st.commit() var mi := MeshInstance3D.new() mi.name = "TerrainMesh" mi.mesh = mesh var mat := StandardMaterial3D.new() mat.albedo_color = Color(0.33, 0.45, 0.22) # mossy green mat.roughness = 0.95 mi.material_override = mat add_child(mi) # Collision: a HeightMapShape3D, NOT a concave trimesh. # # Root cause of the fall-through (confirmed by instrumented tests): a concave # polygon shape — whether hand-built via set_faces() or mesh-derived via # create_trimesh_collision() — is ONE-SIDED. It only collides on the front of # each triangle, so with this mesh's winding the player dropped straight # through from above. A box on the same StaticBody collided fine, which pinned # the cause to the concave shape, not layers/timing/registration. # # HeightMapShape3D is the correct primitive for heightmap terrain: a grid of # height samples, solid (two-sided), and far cheaper than a trimesh. var hm := HeightMapShape3D.new() hm.map_width = resolution + 1 hm.map_depth = resolution + 1 var data := PackedFloat32Array() data.resize((resolution + 1) * (resolution + 1)) var hi := 0 for hz in range(resolution + 1): for hx in range(resolution + 1): var wx := hx * step - half var wz := hz * step - half data[hi] = sample_height(wx, wz) hi += 1 hm.map_data = data var body := StaticBody3D.new() body.name = "TerrainBody" add_child(body) var col := CollisionShape3D.new() col.name = "CollisionShape3D" col.shape = hm body.add_child(col) # HeightMapShape3D samples are spaced 1 unit apart; scale the body so the # collision grid covers the same `size` metres as the visual mesh. body.scale = Vector3(step, 1.0, step) built = true ready_built.emit() ## World-space ground height at (x, z). func sample_height(x: float, z: float) -> float: _ensure_noise() return _noise.get_noise_2d(x, z) * height_scale func _add_tri(st: SurfaceTool, a: Vector3, b: Vector3, c: Vector3, half: float, uv_scale: float) -> void: st.set_uv(Vector2((a.x + half) * uv_scale, (a.z + half) * uv_scale)) st.add_vertex(a) st.set_uv(Vector2((b.x + half) * uv_scale, (b.z + half) * uv_scale)) st.add_vertex(b) st.set_uv(Vector2((c.x + half) * uv_scale, (c.z + half) * uv_scale)) st.add_vertex(c)