From 0209c6149a3f1aed06a62cd2d7a38590886badd0 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Fri, 6 Nov 2026 21:26:15 +0000 Subject: [PATCH] The seam and the sea cave built from real boulders (rock_pile.gd: the scenery's rocks with matching colliders), the seam a cleft between two slabs with pick marks and loose stones --- scripts/world/rock_pile.gd | 39 ++++++++++++++ scripts/world/rock_pile.gd.uid | 1 + scripts/world/sea_cave.gd | 58 ++++++++++----------- scripts/world/tunnel.gd | 94 ++++++++++++++++++++++------------ 4 files changed, 128 insertions(+), 64 deletions(-) create mode 100644 scripts/world/rock_pile.gd create mode 100644 scripts/world/rock_pile.gd.uid diff --git a/scripts/world/rock_pile.gd b/scripts/world/rock_pile.gd new file mode 100644 index 0000000..b0c0ada --- /dev/null +++ b/scripts/world/rock_pile.gd @@ -0,0 +1,39 @@ +extends RefCounted +## Boulders for hand-placed rockwork (the cliff tunnel's seam and its faces, +## the Cove's sea cave): the same low-poly rocks as the scenery and the wolf +## den (ProcMesh.rock, rock.gdshader: moss on top, snow up high), each with a +## convex collider that matches it. + +const ProcMesh = preload("res://scripts/world/proc_mesh.gd") + +## The scenery's rock material, for a world's terrain. +static func material(terrain: Node3D) -> ShaderMaterial: + var m := ShaderMaterial.new() + m.shader = preload("res://shaders/rock.gdshader") + m.set_shader_parameter("noise_tex", terrain.noise_tex) + m.set_shader_parameter("snow_height", terrain.snow_height) + return m + +## One boulder under `parent` at local `at`, `radii` across (a rock mesh is +## about 1 m round before scaling), turned `turn` radians. Its collider goes +## on `body` (which must share `parent`'s transform), unless `body` is null. +static func add(parent: Node3D, body: CollisionObject3D, seed: int, at: Vector3, radii: Vector3, turn: float, + mat: Material, tall: bool = false) -> MeshInstance3D: + var mesh := ProcMesh.rock(seed, tall) + var b := Basis(Vector3.UP, turn).scaled(radii) + var mi := MeshInstance3D.new() + mi.mesh = mesh + mi.material_override = mat + mi.transform = Transform3D(b, at) + parent.add_child(mi) + if body != null: + var hull := mesh.create_convex_shape(true, true) as ConvexPolygonShape3D + var pts := PackedVector3Array() + for p: Vector3 in hull.points: + pts.append(b * p) + hull.points = pts + var cs := CollisionShape3D.new() + cs.shape = hull + cs.position = at + body.add_child(cs) + return mi diff --git a/scripts/world/rock_pile.gd.uid b/scripts/world/rock_pile.gd.uid new file mode 100644 index 0000000..98bc893 --- /dev/null +++ b/scripts/world/rock_pile.gd.uid @@ -0,0 +1 @@ +uid://ijajgdb0unan diff --git a/scripts/world/sea_cave.gd b/scripts/world/sea_cave.gd index 2df520d..cc59026 100644 --- a/scripts/world/sea_cave.gd +++ b/scripts/world/sea_cave.gd @@ -8,6 +8,8 @@ extends Node3D ## Placed from the terrain alone, like the pier, so it's the same for ## everyone; nothing about it is saved here (the tunnel is the Valley's). +const RockPile = preload("res://scripts/world/rock_pile.gd") + var world: Node3D var to := "valley" var front := Vector3.ZERO ## where you stand in front of it (arriving from the tunnel) @@ -66,44 +68,36 @@ func interact(_player: Node) -> void: Actions.notify.emit(interact_prompt(null)) func _build(w: Node3D) -> void: - var rock := StandardMaterial3D.new() - rock.albedo_color = Color(0.42, 0.40, 0.38) - rock.roughness = 1.0 - if w.terrain.noise_tex != null: - rock.albedo_texture = w.terrain.noise_tex - rock.uv1_triplanar = true - rock.uv1_scale = Vector3(0.18, 0.18, 0.18) + var rock: ShaderMaterial = RockPile.material(w.terrain) var body := StaticBody3D.new() add_child(body) - # (local -Z faces the beach; the rocks are behind, +Z) - for part in [[Vector3(-2.0, 1.6, 1.0), Vector3(1.8, 3.6, 3.0)], [Vector3(2.0, 1.4, 1.0), Vector3(1.8, 3.2, 3.0)], - [Vector3(0, 3.5, 1.0), Vector3(5.8, 1.4, 3.2)], [Vector3(0, 1.4, 2.4), Vector3(2.4, 3.0, 0.6)], - [Vector3(-3.4, 1.0, 2.0), Vector3(2.0, 2.4, 3.0)], [Vector3(3.2, 0.8, 2.2), Vector3(1.8, 2.0, 2.6)]]: - var mi := MeshInstance3D.new() - var bm := BoxMesh.new() - bm.size = part[1] - mi.mesh = bm - mi.material_override = rock - mi.position = part[0] - mi.rotation.y = part[0].x * 0.06 - add_child(mi) - var cs := CollisionShape3D.new() - var box := BoxShape3D.new() - box.size = part[1] - cs.shape = box - cs.position = part[0] - cs.rotation.y = part[0].x * 0.06 - body.add_child(cs) + # A heap of big boulders at the foot of the headland, with a dark mouth + # between them (local -Z faces the beach; the heap is behind, +Z), and + # seaweed-dark stones scattered on the sand in front. + var s: int = w.terrain.noise_seed + 5100 + for part in [[Vector3(-2.4, 1.3, 1.0), Vector3(1.3, 2.1, 1.7), 0.5, true], + [Vector3(2.4, 1.15, 1.0), Vector3(1.25, 1.9, 1.6), 2.0, true], + [Vector3(0.0, 3.75, 1.2), Vector3(2.9, 1.0, 1.9), 0.3, false], + [Vector3(0.0, 1.8, 3.2), Vector3(2.4, 2.4, 1.4), 1.1, false], + [Vector3(-4.3, 0.8, 2.0), Vector3(1.8, 1.5, 1.8), 2.6, false], + [Vector3(4.1, 0.6, 2.2), Vector3(1.6, 1.3, 1.7), 0.7, false], + [Vector3(-2.0, 4.4, 2.8), Vector3(1.8, 1.2, 1.6), 1.7, false], + [Vector3(2.3, 4.1, 3.0), Vector3(1.6, 1.1, 1.5), 2.9, false]]: + RockPile.add(self, body, s, part[0], part[1], part[2], rock, part[3]) + s += 1 + for stone in [[Vector3(-1.4, 0.08, -1.6), 0.4], [Vector3(1.6, 0.06, -2.2), 0.3], + [Vector3(-3.2, 0.06, -1.0), 0.35], [Vector3(0.4, 0.05, -2.9), 0.22], [Vector3(3.4, 0.05, -1.2), 0.28]]: + RockPile.add(self, null, s, stone[0], Vector3.ONE * float(stone[1]), float(s), rock) + s += 1 var dark := MeshInstance3D.new() - var q := QuadMesh.new() - q.size = Vector2(2.2, 2.8) - dark.mesh = q + var bm := BoxMesh.new() + bm.size = Vector3(2.0, 2.6, 0.1) + dark.mesh = bm var m := StandardMaterial3D.new() - m.albedo_color = Color(0.02, 0.02, 0.025) + m.albedo_color = Color(0.015, 0.014, 0.018) m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED dark.material_override = m - dark.position = Vector3(0, 1.4, 2.08) - dark.rotation.y = PI + dark.position = Vector3(0, 1.3, 1.6) add_child(dark) # What you press E on: the opening (interactables layer: you walk into it). var spot := StaticBody3D.new() diff --git a/scripts/world/tunnel.gd b/scripts/world/tunnel.gd index 3656c30..a7909d6 100644 --- a/scripts/world/tunnel.gd +++ b/scripts/world/tunnel.gd @@ -21,6 +21,7 @@ extends Node3D ## for now: on a server it isn't built (J3 brings travel online). const TunnelFace = preload("res://scripts/world/tunnel_face.gd") +const RockPile = preload("res://scripts/world/rock_pile.gd") const ORIGIN := Vector3(0.0, -260.0, 0.0) ## where the tunnel space sits (well under the island) const STRETCHES := 6 @@ -54,8 +55,9 @@ var bottom := Vector3.ZERO ## inside, at the bottom by the cave var cave_mouth := Vector3.ZERO ## the far end of the sea cave var aabb := AABB() -var _rock_mat: StandardMaterial3D -var _face_mat: StandardMaterial3D +var _rock_mat: StandardMaterial3D ## the tunnel's walls +var _boulder_mat: ShaderMaterial ## the seam's outcrop (rock_pile.gd) +var _face_mat: ShaderMaterial ## the faces you dig var _wood_mat: StandardMaterial3D var _rock: CSGCombiner3D var _cuts: Node3D @@ -212,8 +214,8 @@ func _make_materials() -> void: _rock_mat.albedo_texture = terrain.noise_tex _rock_mat.uv1_triplanar = true _rock_mat.uv1_scale = Vector3(0.18, 0.18, 0.18) - _face_mat = _rock_mat.duplicate() - _face_mat.albedo_color = Color(0.30, 0.27, 0.25) + _boulder_mat = RockPile.material(terrain) + _face_mat = RockPile.material(terrain) # (the same rock: the seam's cleft and pick marks set it apart) _wood_mat = StandardMaterial3D.new() _wood_mat.albedo_color = Color(0.42, 0.29, 0.17) _wood_mat.roughness = 0.9 @@ -322,13 +324,37 @@ func _make_face(stage: int, center: Vector3, size: Vector3) -> StaticBody3D: box.size = size cs.shape = box f.add_child(cs) - var mi := MeshInstance3D.new() - var bm := BoxMesh.new() - bm.size = size - mi.mesh = bm - mi.material_override = _face_mat - f.add_child(mi) add_child(f) + var s: int = terrain.noise_seed + 4200 + stage * 7 + if stage == 0: + # The seam: two slabs of rock with a dark cleft between them, and + # old pick marks (pale chips) on either side. + var gap := MeshInstance3D.new() + var gm := BoxMesh.new() + gm.size = Vector3(0.28, size.y * 0.85, 0.3) + gap.mesh = gm + var dm := StandardMaterial3D.new() + dm.albedo_color = Color(0.02, 0.018, 0.015) + dm.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + gap.material_override = dm + gap.position = Vector3(0.05, -0.05, 0.05) + f.add_child(gap) + RockPile.add(f, null, s, Vector3(-0.52, 0.0, 0.1), Vector3(0.55, size.y * 0.5, 0.42), 0.3, _face_mat, true) + RockPile.add(f, null, s + 1, Vector3(0.58, -0.05, 0.12), Vector3(0.52, size.y * 0.47, 0.4), 2.1, _face_mat, true) + var chip := StandardMaterial3D.new() + chip.albedo_color = Color(0.62, 0.58, 0.52) + for c in [Vector3(-0.32, 0.35, -0.3), Vector3(0.36, -0.2, -0.3), Vector3(-0.25, -0.45, -0.28)]: + var m := MeshInstance3D.new() + var cm := BoxMesh.new() + cm.size = Vector3(0.12, 0.05, 0.03) + m.mesh = cm + m.material_override = chip + m.position = c + m.rotation.z = c.x * 2.0 + f.add_child(m) + else: + # A face down the tunnel: rough rock filling the passage. + RockPile.add(f, null, s, Vector3(-0.05, 0.0, 0.0), Vector3(0.45, size.y * 0.55, size.z * 0.55), 0.1, _face_mat) f.global_position = center return f @@ -344,35 +370,39 @@ func _build_seam() -> void: # (local -Z faces inland, towards the valley; the outcrop is behind, +Z) var body := StaticBody3D.new() _seam.add_child(body) - for part in [[Vector3(-1.5, 1.2, 0.4), Vector3(1.0, 2.6, 2.0)], [Vector3(1.5, 1.1, 0.4), Vector3(1.0, 2.4, 2.0)], - [Vector3(0, 2.6, 0.4), Vector3(4.0, 0.9, 2.2)], [Vector3(0, 1.1, 1.1), Vector3(2.2, 2.4, 0.6)]]: - var mi := MeshInstance3D.new() - var bm := BoxMesh.new() - bm.size = part[1] - mi.mesh = bm - mi.material_override = _rock_mat - mi.position = part[0] - _seam.add_child(mi) - var cs := CollisionShape3D.new() - var box := BoxShape3D.new() - box.size = part[1] - cs.shape = box - cs.position = part[0] - body.add_child(cs) + # An outcrop of boulders round a cleft: two tall ones either side, one + # across the top, a big one behind and two lower ones at the shoulders, + # with loose stones fallen at its foot. (Each sits a little into the + # ground, which isn't quite flat.) + var s: int = terrain.noise_seed + 4100 + for part in [[Vector3(-2.05, 0.95, 0.5), Vector3(1.0, 1.55, 1.3), 0.4, true], + [Vector3(2.05, 0.85, 0.55), Vector3(0.95, 1.45, 1.25), 1.9, true], + [Vector3(0.0, 2.95, 0.65), Vector3(2.3, 0.7, 1.4), 0.2, false], + [Vector3(0.0, 1.4, 1.9), Vector3(2.3, 2.3, 1.3), 3.0, false], + [Vector3(-1.2, 2.6, 2.4), Vector3(1.7, 1.5, 1.3), 1.3, false], + [Vector3(1.3, 2.5, 2.3), Vector3(1.6, 1.4, 1.3), 2.5, false], + [Vector3(-3.3, 0.35, 1.2), Vector3(1.2, 0.9, 1.2), 2.2, false], + [Vector3(3.2, 0.3, 1.0), Vector3(1.0, 0.8, 1.1), 0.9, false]]: + RockPile.add(_seam, body, s, part[0], part[1], part[2], _boulder_mat, part[3]) + s += 1 + for stone in [[Vector3(-0.8, 0.06, -1.0), 0.28], [Vector3(1.1, 0.05, -1.3), 0.22], + [Vector3(0.35, 0.04, -1.7), 0.18], [Vector3(-1.6, 0.04, -1.6), 0.2]]: + RockPile.add(_seam, null, s, stone[0], Vector3.ONE * float(stone[1]), float(s), _boulder_mat) + s += 1 if not opened: - _face = _make_face(0, _seam.global_transform * Vector3(0, 1.1, -0.35), Vector3(2.0, 2.2, 0.5)) + _face = _make_face(0, _seam.global_transform * Vector3(0, 1.1, -0.2), Vector3(2.0, 2.2, 0.6)) _face.global_basis = _seam.global_basis else: + # The cleft dug open: black inside, set back between the boulders. var dark := MeshInstance3D.new() - var q := QuadMesh.new() - q.size = Vector2(2.0, 2.1) - dark.mesh = q + var bm := BoxMesh.new() + bm.size = Vector3(1.6, 2.0, 0.1) + dark.mesh = bm var m := StandardMaterial3D.new() - m.albedo_color = Color(0.02, 0.02, 0.02) + m.albedo_color = Color(0.015, 0.012, 0.01) m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED dark.material_override = m - dark.position = Vector3(0, 1.05, 0.78) - dark.rotation.y = PI # (a quad faces +Z: turn it to face inland) + dark.position = Vector3(0, 1.0, 0.45) _seam.add_child(dark) var spot := _spot_body(Vector3(2.0, 2.1, 0.4), "in", 0) _seam.add_child(spot)