Fix fall-through: use HeightMapShape3D (concave trimesh was one-sided, player fell through from above)
This commit is contained in:
@@ -6,11 +6,12 @@ extends Node3D
|
|||||||
## _ready(). Chunk streaming, biomes and server-side determinism (see
|
## _ready(). Chunk streaming, biomes and server-side determinism (see
|
||||||
## docs/ARCHITECTURE.md §4) come in later milestones.
|
## docs/ARCHITECTURE.md §4) come in later milestones.
|
||||||
##
|
##
|
||||||
## The mesh is generated in code (no asset needed) so the project runs the moment
|
## Collision uses MeshInstance3D.create_trimesh_collision() (mesh-derived), which
|
||||||
## Godot opens it, with nothing to import.
|
## 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)
|
## Exposes `sample_height(x, z)` so other systems (player spawn, prop placement)
|
||||||
## can sit exactly on the surface instead of guessing a Y and falling through.
|
## can sit exactly on the surface.
|
||||||
|
|
||||||
signal ready_built ## emitted once the mesh + collision exist
|
signal ready_built ## emitted once the mesh + collision exist
|
||||||
|
|
||||||
@@ -25,13 +26,18 @@ var built: bool = false
|
|||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_build()
|
_build()
|
||||||
|
|
||||||
func _build() -> void:
|
func _ensure_noise() -> void:
|
||||||
|
if _noise != null:
|
||||||
|
return
|
||||||
_noise = FastNoiseLite.new()
|
_noise = FastNoiseLite.new()
|
||||||
_noise.seed = noise_seed
|
_noise.seed = noise_seed
|
||||||
_noise.noise_type = FastNoiseLite.TYPE_PERLIN
|
_noise.noise_type = FastNoiseLite.TYPE_PERLIN
|
||||||
_noise.frequency = 0.012
|
_noise.frequency = 0.012
|
||||||
_noise.fractal_octaves = 4
|
_noise.fractal_octaves = 4
|
||||||
|
|
||||||
|
func _build() -> void:
|
||||||
|
_ensure_noise()
|
||||||
|
|
||||||
var st := SurfaceTool.new()
|
var st := SurfaceTool.new()
|
||||||
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||||
|
|
||||||
@@ -39,8 +45,6 @@ func _build() -> void:
|
|||||||
var half := size * 0.5
|
var half := size * 0.5
|
||||||
var uv_scale := 1.0 / float(size)
|
var uv_scale := 1.0 / float(size)
|
||||||
|
|
||||||
# Build a grid of quads, two triangles each. Each vertex carries a planar UV
|
|
||||||
# (world-XZ mapped to 0..1) so the surface is ready for texturing later.
|
|
||||||
for z in range(resolution):
|
for z in range(resolution):
|
||||||
for x in range(resolution):
|
for x in range(resolution):
|
||||||
var x0 := x * step - half
|
var x0 := x * step - half
|
||||||
@@ -56,43 +60,60 @@ func _build() -> void:
|
|||||||
_add_tri(st, p00, p01, p11, half, uv_scale)
|
_add_tri(st, p00, p01, p11, half, uv_scale)
|
||||||
_add_tri(st, p00, p11, p10, half, uv_scale)
|
_add_tri(st, p00, p11, p10, half, uv_scale)
|
||||||
|
|
||||||
# Normals give the lighting its shape. Tangents are only needed for normal
|
|
||||||
# maps, which this low-poly material doesn't use — so we skip them.
|
|
||||||
st.generate_normals()
|
st.generate_normals()
|
||||||
|
|
||||||
var mesh := st.commit()
|
var mesh := st.commit()
|
||||||
var mi := MeshInstance3D.new()
|
var mi := MeshInstance3D.new()
|
||||||
mi.name = "TerrainMesh"
|
mi.name = "TerrainMesh"
|
||||||
mi.mesh = mesh
|
mi.mesh = mesh
|
||||||
|
|
||||||
# Simple ground material. Low-poly look = flat-ish colour + lighting does the rest.
|
|
||||||
var mat := StandardMaterial3D.new()
|
var mat := StandardMaterial3D.new()
|
||||||
mat.albedo_color = Color(0.33, 0.45, 0.22) # mossy green
|
mat.albedo_color = Color(0.33, 0.45, 0.22) # mossy green
|
||||||
mat.roughness = 0.95
|
mat.roughness = 0.95
|
||||||
mi.material_override = mat
|
mi.material_override = mat
|
||||||
add_child(mi)
|
add_child(mi)
|
||||||
|
|
||||||
# Collision so the player can walk on it. Build the body BEFORE marking built
|
# Collision: a HeightMapShape3D, NOT a concave trimesh.
|
||||||
# so anything spawning on ready_built already has ground under it.
|
#
|
||||||
|
# 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()
|
var body := StaticBody3D.new()
|
||||||
body.name = "TerrainBody"
|
body.name = "TerrainBody"
|
||||||
var shape := CollisionShape3D.new()
|
|
||||||
shape.shape = mesh.create_trimesh_shape()
|
|
||||||
body.add_child(shape)
|
|
||||||
add_child(body)
|
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
|
built = true
|
||||||
ready_built.emit()
|
ready_built.emit()
|
||||||
|
|
||||||
## World-space ground height at (x, z). Safe to call before or after _build()
|
## World-space ground height at (x, z).
|
||||||
## because it lazily creates the noise object.
|
|
||||||
func sample_height(x: float, z: float) -> float:
|
func sample_height(x: float, z: float) -> float:
|
||||||
if _noise == null:
|
_ensure_noise()
|
||||||
_noise = FastNoiseLite.new()
|
|
||||||
_noise.seed = noise_seed
|
|
||||||
_noise.noise_type = FastNoiseLite.TYPE_PERLIN
|
|
||||||
_noise.frequency = 0.012
|
|
||||||
_noise.fractal_octaves = 4
|
|
||||||
return _noise.get_noise_2d(x, z) * height_scale
|
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:
|
func _add_tri(st: SurfaceTool, a: Vector3, b: Vector3, c: Vector3, half: float, uv_scale: float) -> void:
|
||||||
|
|||||||
Reference in New Issue
Block a user