Files
Cinderwild/scripts/world/terrain.gd

130 lines
4.5 KiB
GDScript

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)
# Winding order chosen so generate_normals() produces UPWARD normals.
# (The reverse winding gave downward normals, so the sun lit the
# underside and the top surface rendered black — confirmed by an
# unshaded render showing the true bright-green albedo.)
_add_tri(st, p00, p11, p01, half, uv_scale)
_add_tri(st, p00, p10, p11, 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.36, 0.47, 0.26) # mossy green (lit, not fluorescent)
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)