M0.5: detailed scenery, mountain/sea-cliff boundary, Godot 4.7
Scenery (all procedural, seeded, no imported assets): - Terrain shader: grass/dry grass/dirt/sand/rock strata/snow + detail normals - 5 tree types, faceted mossy rocks, cliff outcrops, sea stacks, bushes, logs and stumps, batched as MultiMesh per 48 m cell - Wind-animated grass and flowers streamed in tiles around the player - Lake (depth-shaded water with shore foam), ocean to the horizon, cloud sky shader, volumetric fog, glow, softer shadows - Swimming: float at the surface, swim-jump to climb out World bounds: - Steep mountain rim with an invisible wall partway up; one side opens to a sheer sea cliff with the wall 1 m inside the edge; whole chunk is an island so the mesh edge is never visible - Respawn safety net if the player leaves the walkable area or the map - Fix: the cliff edge now tracks the wall in the headlands (it used to drop away inside the wall, leaving a pit with lake water hanging over it) Tests: tests/test_boundary.gd (24-direction escape attempt) and tests/test_terrain_edges.gd (no ground below waterline near the wall). Upgrades the project to Godot 4.7 (project.godot rewritten by the 4.7 importer, .uid files added) and documents it in the setup guide.
This commit is contained in:
@@ -1,94 +1,502 @@
|
||||
extends Node3D
|
||||
## M0 mood props — a few low-poly trees and rocks built from primitives in code,
|
||||
## so the scene looks "alive" with no imported assets. These are placeholders;
|
||||
## real low-poly models (free asset packs, see docs/ROADMAP.md M0) replace them.
|
||||
## Scenery: forests, rocks, cliff outcrops, bushes, logs, grass and flowers.
|
||||
##
|
||||
## place_on(terrain) is called by world.gd once the terrain exists, so each prop
|
||||
## sits on the real ground height instead of floating at Y=0.
|
||||
## All placement is seeded, so a server and its clients would agree on where
|
||||
## every tree is (docs/ARCHITECTURE.md §4). Rendering uses MultiMesh batches
|
||||
## bucketed into CELL x CELL world cells: a few hundred draw batches instead of
|
||||
## thousands of nodes, and off-screen cells get frustum-culled.
|
||||
##
|
||||
## Trees, logs and larger rocks in or near the walkable valley get simple
|
||||
## collision. Grass is streamed in tiles around the player because it's too
|
||||
## dense to place over the whole map at once.
|
||||
|
||||
var _terrain: Node3D
|
||||
const ProcMesh = preload("res://scripts/world/proc_mesh.gd")
|
||||
|
||||
func place_on(terrain: Node3D) -> void:
|
||||
_terrain = terrain
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 42
|
||||
for i in range(18):
|
||||
var x := rng.randf_range(-55.0, 55.0)
|
||||
var z := rng.randf_range(-55.0, 55.0)
|
||||
var y: float = _terrain.sample_height(x, z)
|
||||
if rng.randf() < 0.65:
|
||||
_make_tree(Vector3(x, y, z), rng)
|
||||
else:
|
||||
_make_rock(Vector3(x, y, z), rng)
|
||||
const CELL := 48.0
|
||||
const GRASS_TILE := 12.0
|
||||
const FLOWER_COLORS := [
|
||||
Color(0.96, 0.95, 0.90), Color(0.98, 0.84, 0.25), Color(0.62, 0.42, 0.86),
|
||||
Color(0.45, 0.60, 0.95), Color(0.93, 0.45, 0.55),
|
||||
]
|
||||
|
||||
func _make_tree(pos: Vector3, rng: RandomNumberGenerator) -> void:
|
||||
@export var world_seed: int = 1337
|
||||
@export var tree_spacing: float = 3.6
|
||||
@export var forest_density: float = 1.0
|
||||
@export var grass_spacing: float = 0.42
|
||||
@export var grass_view_distance: float = 55.0
|
||||
@export var grass_tiles_per_update: int = 6
|
||||
|
||||
var terrain: Node3D
|
||||
var _player: Node3D
|
||||
var _spawn := Vector3.ZERO
|
||||
var _forest_noise := FastNoiseLite.new()
|
||||
var _meadow_noise := FastNoiseLite.new()
|
||||
var _colliders: StaticBody3D
|
||||
|
||||
var _bark_mat: StandardMaterial3D
|
||||
var _leaf_mat: ShaderMaterial
|
||||
var _rock_mat: ShaderMaterial
|
||||
var _grass_mat: ShaderMaterial
|
||||
|
||||
var _grass_root: Node3D
|
||||
var _grass_tiles: Dictionary = {} # Vector2i -> Node3D (or null for empty tiles)
|
||||
var _grass_mesh: ArrayMesh
|
||||
var _flower_mesh: ArrayMesh
|
||||
var _grass_timer := 0.0
|
||||
|
||||
func place_on(t: Node3D, player: Node3D, spawn: Vector3) -> void:
|
||||
terrain = t
|
||||
_player = player
|
||||
_spawn = spawn
|
||||
_forest_noise.seed = world_seed + 11
|
||||
_forest_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
||||
_forest_noise.frequency = 0.011
|
||||
_forest_noise.fractal_octaves = 3
|
||||
_meadow_noise.seed = world_seed + 23
|
||||
_meadow_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
||||
_meadow_noise.frequency = 0.03
|
||||
_meadow_noise.fractal_octaves = 2
|
||||
|
||||
_colliders = StaticBody3D.new()
|
||||
_colliders.name = "PropColliders"
|
||||
add_child(_colliders)
|
||||
|
||||
_make_materials()
|
||||
_scatter_trees()
|
||||
_scatter_rocks()
|
||||
_scatter_bushes()
|
||||
_place_logs()
|
||||
|
||||
_grass_root = Node3D.new()
|
||||
_grass_root.name = "Grass"
|
||||
add_child(_grass_root)
|
||||
_grass_mesh = ProcMesh.grass_clump(world_seed + 5)
|
||||
_grass_mesh.surface_set_material(0, _grass_mat)
|
||||
_flower_mesh = ProcMesh.flower(world_seed + 6)
|
||||
var stem := StandardMaterial3D.new()
|
||||
stem.vertex_color_use_as_albedo = true
|
||||
stem.vertex_color_is_srgb = true
|
||||
stem.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
var head := stem.duplicate() as StandardMaterial3D
|
||||
head.roughness = 0.7
|
||||
_flower_mesh.surface_set_material(0, stem)
|
||||
_flower_mesh.surface_set_material(1, head)
|
||||
_update_grass(100000) # everything in range, before the first frame
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _player == null:
|
||||
return
|
||||
_grass_timer -= delta
|
||||
if _grass_timer > 0.0:
|
||||
return
|
||||
_grass_timer = 0.2
|
||||
_update_grass(grass_tiles_per_update)
|
||||
|
||||
# --- materials --------------------------------------------------------------
|
||||
|
||||
func _make_materials() -> void:
|
||||
var noise_tex: Texture2D = terrain.noise_tex
|
||||
_bark_mat = StandardMaterial3D.new()
|
||||
_bark_mat.vertex_color_use_as_albedo = true
|
||||
_bark_mat.vertex_color_is_srgb = true
|
||||
_bark_mat.roughness = 1.0
|
||||
|
||||
_leaf_mat = ShaderMaterial.new()
|
||||
_leaf_mat.shader = preload("res://shaders/foliage.gdshader")
|
||||
_leaf_mat.set_shader_parameter("noise_tex", noise_tex)
|
||||
|
||||
_rock_mat = ShaderMaterial.new()
|
||||
_rock_mat.shader = preload("res://shaders/rock.gdshader")
|
||||
_rock_mat.set_shader_parameter("noise_tex", noise_tex)
|
||||
_rock_mat.set_shader_parameter("snow_height", terrain.snow_height)
|
||||
|
||||
_grass_mat = ShaderMaterial.new()
|
||||
_grass_mat.shader = preload("res://shaders/grass.gdshader")
|
||||
_grass_mat.set_shader_parameter("noise_tex", noise_tex)
|
||||
_grass_mat.set_shader_parameter("grass_lush", terrain.grass_lush)
|
||||
_grass_mat.set_shader_parameter("grass_dry", terrain.grass_dry)
|
||||
|
||||
func _apply_materials(v: Dictionary) -> Dictionary:
|
||||
var mesh: ArrayMesh = v.mesh
|
||||
var surfaces: Array = v.surfaces
|
||||
for i in range(surfaces.size()):
|
||||
mesh.surface_set_material(i, _bark_mat if surfaces[i] == "bark" else _leaf_mat)
|
||||
return v
|
||||
|
||||
# --- helpers ----------------------------------------------------------------
|
||||
|
||||
## Jittered grid covering the whole chunk.
|
||||
func _jittered(spacing: float, rng: RandomNumberGenerator) -> PackedVector2Array:
|
||||
var pts := PackedVector2Array()
|
||||
var half: float = terrain.size * 0.5 - 2.0
|
||||
var z := -half
|
||||
while z < half:
|
||||
var x := -half
|
||||
while x < half:
|
||||
pts.append(Vector2(x + rng.randf() * spacing, z + rng.randf() * spacing))
|
||||
x += spacing
|
||||
z += spacing
|
||||
return pts
|
||||
|
||||
func _forest(x: float, z: float) -> float:
|
||||
return smoothstep(-0.15, 0.35, _forest_noise.get_noise_2d(x, z))
|
||||
|
||||
func _group(groups: Dictionary, x: float, z: float, vi: int, xf: Transform3D) -> void:
|
||||
var key := Vector3i(floori(x / CELL), floori(z / CELL), vi)
|
||||
if not groups.has(key):
|
||||
groups[key] = []
|
||||
groups[key].append(xf)
|
||||
|
||||
func _flush(groups: Dictionary, meshes: Array, shadows: bool, label: String) -> void:
|
||||
var root := Node3D.new()
|
||||
root.position = pos
|
||||
root.name = label
|
||||
add_child(root)
|
||||
for key: Vector3i in groups:
|
||||
root.add_child(_mm_instance(meshes[key.z], groups[key], [], shadows))
|
||||
|
||||
var trunk := MeshInstance3D.new()
|
||||
var tm := CylinderMesh.new()
|
||||
tm.top_radius = 0.18
|
||||
tm.bottom_radius = 0.28
|
||||
tm.height = 3.0
|
||||
trunk.mesh = tm
|
||||
trunk.position = Vector3(0, 1.5, 0)
|
||||
trunk.material_override = _mat(Color(0.33, 0.24, 0.16))
|
||||
root.add_child(trunk)
|
||||
func _mm_instance(mesh: Mesh, xforms: Array, colors: Array, shadows: bool) -> MultiMeshInstance3D:
|
||||
var mm := MultiMesh.new()
|
||||
mm.transform_format = MultiMesh.TRANSFORM_3D
|
||||
mm.use_colors = not colors.is_empty() # must be set before instance_count
|
||||
mm.mesh = mesh
|
||||
mm.instance_count = xforms.size()
|
||||
for i in range(xforms.size()):
|
||||
mm.set_instance_transform(i, xforms[i])
|
||||
if not colors.is_empty():
|
||||
mm.set_instance_color(i, colors[i])
|
||||
var mmi := MultiMeshInstance3D.new()
|
||||
mmi.multimesh = mm
|
||||
mmi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON if shadows else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
return mmi
|
||||
|
||||
# Collision: a capsule around the trunk so you can't walk through the tree.
|
||||
var body := StaticBody3D.new()
|
||||
root.add_child(body)
|
||||
var col := CollisionShape3D.new()
|
||||
var cap := CapsuleShape3D.new()
|
||||
cap.radius = 0.35
|
||||
cap.height = 4.0
|
||||
col.shape = cap
|
||||
col.position = Vector3(0, 2.0, 0)
|
||||
body.add_child(col)
|
||||
## Basis that tips `UP` part-way toward the ground normal, then yaws and scales.
|
||||
func _ground_basis(normal: Vector3, follow: float, yaw: float, s: float) -> Basis:
|
||||
var up := Vector3.UP.lerp(normal, follow).normalized()
|
||||
var b := Basis(Vector3.UP, yaw)
|
||||
var axis := Vector3.UP.cross(up)
|
||||
if axis.length() > 0.0001:
|
||||
b = Basis(axis.normalized(), Vector3.UP.angle_to(up)) * b
|
||||
return b * Basis.from_scale(Vector3.ONE * s)
|
||||
|
||||
# Low-poly conifer canopy: stacked cones.
|
||||
var h := 3.0
|
||||
for j in range(3):
|
||||
var cone := MeshInstance3D.new()
|
||||
var cm := CylinderMesh.new()
|
||||
cm.top_radius = 0.0
|
||||
cm.bottom_radius = 1.6 - j * 0.4
|
||||
cm.height = 1.6
|
||||
cm.radial_segments = 6 # low-poly
|
||||
cone.mesh = cm
|
||||
cone.position = Vector3(0, h + j * 1.0, 0)
|
||||
cone.material_override = _mat(Color(0.16, 0.34, 0.2))
|
||||
root.add_child(cone)
|
||||
func _add_shape(shape: Shape3D, xf: Transform3D) -> void:
|
||||
var cs := CollisionShape3D.new()
|
||||
cs.shape = shape
|
||||
cs.transform = xf
|
||||
_colliders.add_child(cs)
|
||||
|
||||
root.rotate_y(rng.randf_range(0, TAU))
|
||||
var s := rng.randf_range(0.85, 1.35)
|
||||
root.scale = Vector3(s, s, s)
|
||||
# --- trees ------------------------------------------------------------------
|
||||
|
||||
func _make_rock(pos: Vector3, rng: RandomNumberGenerator) -> void:
|
||||
var rock := MeshInstance3D.new()
|
||||
var bm := BoxMesh.new()
|
||||
var box_size := Vector3(rng.randf_range(0.8, 1.8), rng.randf_range(0.6, 1.2), rng.randf_range(0.8, 1.8))
|
||||
bm.size = box_size
|
||||
rock.mesh = bm
|
||||
rock.position = pos + Vector3(0, 0.3, 0)
|
||||
rock.rotation = Vector3(rng.randf_range(-0.2, 0.2), rng.randf_range(0, TAU), rng.randf_range(-0.2, 0.2))
|
||||
rock.material_override = _mat(Color(0.42, 0.42, 0.45))
|
||||
add_child(rock)
|
||||
func _scatter_trees() -> void:
|
||||
var variants: Array = []
|
||||
var conifers: Array[int] = []
|
||||
var broad: Array[int] = []
|
||||
var birches: Array[int] = []
|
||||
for i in range(3):
|
||||
conifers.append(variants.size()); variants.append(_apply_materials(ProcMesh.conifer(world_seed + 100 + i, false)))
|
||||
for i in range(2):
|
||||
conifers.append(variants.size()); variants.append(_apply_materials(ProcMesh.conifer(world_seed + 110 + i, true)))
|
||||
for i in range(3):
|
||||
var col: Color = ProcMesh.LEAF_GOLD if i == 2 else ProcMesh.LEAF_GREEN
|
||||
broad.append(variants.size()); variants.append(_apply_materials(ProcMesh.broadleaf(world_seed + 120 + i, col)))
|
||||
for i in range(2):
|
||||
birches.append(variants.size()); variants.append(_apply_materials(ProcMesh.birch(world_seed + 130 + i)))
|
||||
var dead := variants.size()
|
||||
variants.append(_apply_materials(ProcMesh.dead_tree(world_seed + 140)))
|
||||
|
||||
# Collision: a box matching the rock mesh.
|
||||
var body := StaticBody3D.new()
|
||||
body.position = rock.position
|
||||
body.rotation = rock.rotation
|
||||
add_child(body)
|
||||
var col := CollisionShape3D.new()
|
||||
var box := BoxShape3D.new()
|
||||
box.size = box_size
|
||||
col.shape = box
|
||||
body.add_child(col)
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = world_seed + 1
|
||||
var groups := {}
|
||||
for p in _jittered(tree_spacing, rng):
|
||||
# Draw every random number up front so the sequence never depends on
|
||||
# which branch below is taken (stable layout when rules are tweaked).
|
||||
var roll := rng.randf()
|
||||
var pick := rng.randf()
|
||||
var yaw := rng.randf() * TAU
|
||||
var s := rng.randf_range(0.75, 1.3)
|
||||
var h: float = terrain.sample_height(p.x, p.y)
|
||||
if roll > _tree_chance(p.x, p.y, h) * forest_density:
|
||||
continue
|
||||
var vi: int
|
||||
var conifer_p := lerpf(0.4, 0.97, smoothstep(2.0, 24.0, h))
|
||||
if pick < 0.025:
|
||||
vi = dead
|
||||
elif pick < conifer_p:
|
||||
vi = conifers[int(pick * 9973.0) % conifers.size()]
|
||||
elif (pick - conifer_p) / (1.0 - conifer_p) < 0.6:
|
||||
vi = broad[int(pick * 7919.0) % broad.size()]
|
||||
else:
|
||||
vi = birches[int(pick * 7919.0) % birches.size()]
|
||||
var xf := Transform3D(Basis(Vector3.UP, yaw) * Basis.from_scale(Vector3.ONE * s), Vector3(p.x, h - 0.1, p.y))
|
||||
_group(groups, p.x, p.y, vi, xf)
|
||||
if terrain.inside_boundary(p.x, p.y, -4.0):
|
||||
var v: Dictionary = variants[vi]
|
||||
var cap := CapsuleShape3D.new()
|
||||
cap.radius = v.trunk_r * s
|
||||
cap.height = maxf(v.trunk_h * s, cap.radius * 2.0 + 0.1)
|
||||
_add_shape(cap, Transform3D(Basis(), Vector3(p.x, h + cap.height * 0.5, p.y)))
|
||||
var meshes := variants.map(func(v): return v.mesh)
|
||||
_flush(groups, meshes, true, "Trees")
|
||||
|
||||
func _mat(c: Color) -> StandardMaterial3D:
|
||||
var m := StandardMaterial3D.new()
|
||||
m.albedo_color = c
|
||||
m.roughness = 0.95
|
||||
return m
|
||||
## Open grassy strip along the sea-cliff edge: no trees, bushes or boulders, so
|
||||
## the view out to sea is clear and you can walk right up to the edge.
|
||||
func _on_clifftop(x: float, z: float) -> bool:
|
||||
return terrain.sea_weight(x, z) > 0.5 and not terrain.inside_boundary(x, z, 10.0) \
|
||||
and terrain.inside_boundary(x, z, -8.0)
|
||||
|
||||
func _tree_chance(x: float, z: float, h: float) -> float:
|
||||
if h < terrain.water_level + 0.8:
|
||||
return 0.0
|
||||
if Vector2(x - _spawn.x, z - _spawn.z).length() < 9.0 or _on_clifftop(x, z):
|
||||
return 0.0
|
||||
var n: Vector3 = terrain.sample_normal(x, z)
|
||||
var slope := 1.0 - n.y
|
||||
if slope > 0.5:
|
||||
return 0.0
|
||||
var c := lerpf(0.035, 0.8, _forest(x, z))
|
||||
c *= 1.0 - smoothstep(0.25, 0.5, slope)
|
||||
c *= 1.0 - smoothstep(terrain.snow_height - 12.0, terrain.snow_height - 2.0, h)
|
||||
return c
|
||||
|
||||
# --- rocks ------------------------------------------------------------------
|
||||
|
||||
func _scatter_rocks() -> void:
|
||||
var meshes: Array = []
|
||||
var hulls: Array = []
|
||||
for i in range(7):
|
||||
var m := ProcMesh.rock(world_seed + 300 + i, i >= 5)
|
||||
m.surface_set_material(0, _rock_mat)
|
||||
meshes.append(m)
|
||||
hulls.append((m.create_convex_shape(true, true) as ConvexPolygonShape3D).points)
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = world_seed + 2
|
||||
var groups := {}
|
||||
|
||||
# Small stones: everywhere, more on slopes and in the shallows.
|
||||
for p in _jittered(6.0, rng):
|
||||
var roll := rng.randf(); var vi := rng.randi_range(0, 4); var yaw := rng.randf() * TAU
|
||||
var s := rng.randf_range(0.2, 0.75)
|
||||
var h: float = terrain.sample_height(p.x, p.y)
|
||||
var n: Vector3 = terrain.sample_normal(p.x, p.y)
|
||||
var chance := 0.1 + (1.0 - n.y) * 0.8
|
||||
if h < terrain.water_level - 2.0 or roll > chance:
|
||||
continue
|
||||
_place_rock(groups, meshes, hulls, p, h, n, vi, yaw, s, 0.6, 0.25)
|
||||
|
||||
# Boulders: sparse, a little more common in forests.
|
||||
for p in _jittered(14.0, rng):
|
||||
var roll := rng.randf(); var vi := rng.randi_range(0, 4); var yaw := rng.randf() * TAU
|
||||
var s := rng.randf_range(1.2, 3.0)
|
||||
var h: float = terrain.sample_height(p.x, p.y)
|
||||
var n: Vector3 = terrain.sample_normal(p.x, p.y)
|
||||
if h < terrain.water_level - 0.5 or Vector2(p.x - _spawn.x, p.y - _spawn.z).length() < 8.0 or _on_clifftop(p.x, p.y):
|
||||
continue
|
||||
if roll > 0.07 + _forest(p.x, p.y) * 0.08:
|
||||
continue
|
||||
_place_rock(groups, meshes, hulls, p, h, n, vi, yaw, s, 0.5, 0.3)
|
||||
|
||||
# Cliff outcrops: big slabs breaking up the steep mountain faces.
|
||||
for p in _jittered(8.0, rng):
|
||||
var roll := rng.randf(); var vi := rng.randi_range(0, 6); var yaw := rng.randf() * TAU
|
||||
var s := rng.randf_range(3.0, 8.0)
|
||||
var n: Vector3 = terrain.sample_normal(p.x, p.y)
|
||||
if 1.0 - n.y < 0.32 or roll > 0.22:
|
||||
continue
|
||||
var h: float = terrain.sample_height(p.x, p.y)
|
||||
_place_rock(groups, meshes, hulls, p, h, n, vi, yaw, s, 0.9, 0.6)
|
||||
|
||||
# Sea stacks: tall pillars standing off the cliff (no collision, unreachable).
|
||||
var placed := 0
|
||||
var tries := 0
|
||||
while placed < 9 and tries < 200:
|
||||
tries += 1
|
||||
var ang: float = terrain.sea_angle_deg + rng.randf_range(-1.0, 1.0) * (terrain.sea_half_width_deg + 8.0)
|
||||
var p: Vector2 = terrain.coast_point(ang, rng.randf_range(10.0, 48.0))
|
||||
var vi := rng.randi_range(5, 6)
|
||||
var yaw := rng.randf() * TAU
|
||||
var s := rng.randf_range(3.5, 7.0)
|
||||
var h: float = terrain.sample_height(p.x, p.y)
|
||||
if h > terrain.sea_level - 3.0:
|
||||
continue
|
||||
var b := Basis(Vector3.UP, yaw) * Basis.from_scale(Vector3(s, s * rng.randf_range(1.8, 2.6), s))
|
||||
_group(groups, p.x, p.y, vi, Transform3D(b, Vector3(p.x, h - 1.0, p.y)))
|
||||
placed += 1
|
||||
|
||||
_flush(groups, meshes, true, "Rocks")
|
||||
|
||||
func _place_rock(groups: Dictionary, meshes: Array, hulls: Array, p: Vector2, h: float, n: Vector3,
|
||||
vi: int, yaw: float, s: float, follow: float, sink: float) -> void:
|
||||
var b := _ground_basis(n, follow, yaw, s)
|
||||
var pos := Vector3(p.x, h - sink * s, p.y)
|
||||
_group(groups, p.x, p.y, vi, Transform3D(b, pos))
|
||||
if s >= 0.55 and terrain.inside_boundary(p.x, p.y, -6.0):
|
||||
var pts := PackedVector3Array()
|
||||
for q: Vector3 in hulls[vi]:
|
||||
pts.append(q * s)
|
||||
var shape := ConvexPolygonShape3D.new()
|
||||
shape.points = pts
|
||||
_add_shape(shape, Transform3D(b.orthonormalized(), pos))
|
||||
|
||||
# --- bushes, logs, stumps ---------------------------------------------------
|
||||
|
||||
func _scatter_bushes() -> void:
|
||||
var meshes: Array = []
|
||||
for i in range(3):
|
||||
meshes.append(_apply_materials(ProcMesh.bush(world_seed + 200 + i)).mesh)
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = world_seed + 3
|
||||
var groups := {}
|
||||
for p in _jittered(4.5, rng):
|
||||
var roll := rng.randf(); var vi := rng.randi_range(0, 2); var yaw := rng.randf() * TAU
|
||||
var s := rng.randf_range(0.7, 1.5)
|
||||
var h: float = terrain.sample_height(p.x, p.y)
|
||||
var n: Vector3 = terrain.sample_normal(p.x, p.y)
|
||||
if h < terrain.water_level + 0.5 or n.y < 0.75 or h > terrain.snow_height - 8.0:
|
||||
continue
|
||||
if Vector2(p.x - _spawn.x, p.y - _spawn.z).length() < 4.0 or _on_clifftop(p.x, p.y):
|
||||
continue
|
||||
var f := _forest(p.x, p.y)
|
||||
var edge := 1.0 - absf(f - 0.5) * 2.0 # forest edges get the most undergrowth
|
||||
if roll > 0.05 + 0.4 * edge:
|
||||
continue
|
||||
_group(groups, p.x, p.y, vi, Transform3D(_ground_basis(n, 0.5, yaw, s), Vector3(p.x, h - 0.1, p.y)))
|
||||
_flush(groups, meshes, true, "Bushes")
|
||||
|
||||
func _place_logs() -> void:
|
||||
var root := Node3D.new()
|
||||
root.name = "Logs"
|
||||
add_child(root)
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = world_seed + 4
|
||||
var logs := 0
|
||||
var stumps := 0
|
||||
var tries := 0
|
||||
while (logs < 22 or stumps < 30) and tries < 4000:
|
||||
tries += 1
|
||||
var x := rng.randf_range(-terrain.play_radius, terrain.play_radius)
|
||||
var z := rng.randf_range(-terrain.play_radius, terrain.play_radius)
|
||||
var yaw := rng.randf() * TAU
|
||||
var make_log := rng.randf() < 0.45
|
||||
if not terrain.inside_boundary(x, z, 6.0) or _forest(x, z) < 0.35:
|
||||
continue
|
||||
var h: float = terrain.sample_height(x, z)
|
||||
var n: Vector3 = terrain.sample_normal(x, z)
|
||||
if h < terrain.water_level + 0.6 or n.y < 0.97 or Vector2(x - _spawn.x, z - _spawn.z).length() < 8.0:
|
||||
continue
|
||||
var mi := MeshInstance3D.new()
|
||||
var shape: Shape3D
|
||||
var xf: Transform3D
|
||||
if make_log and logs < 22:
|
||||
var length := rng.randf_range(3.0, 5.0)
|
||||
var r := rng.randf_range(0.22, 0.34)
|
||||
mi.mesh = ProcMesh.log_mesh(world_seed + 400 + logs, length, r)
|
||||
xf = Transform3D(_ground_basis(n, 1.0, yaw, 1.0), Vector3(x, h + r * 0.75, z))
|
||||
var box := BoxShape3D.new()
|
||||
box.size = Vector3(length, r * 1.8, r * 1.8)
|
||||
shape = box
|
||||
logs += 1
|
||||
elif not make_log and stumps < 30:
|
||||
var sh := rng.randf_range(0.35, 0.7)
|
||||
var r := rng.randf_range(0.25, 0.4)
|
||||
mi.mesh = ProcMesh.stump_mesh(world_seed + 500 + stumps, sh, r)
|
||||
xf = Transform3D(Basis(Vector3.UP, yaw), Vector3(x, h, z))
|
||||
var cyl := CylinderShape3D.new()
|
||||
cyl.radius = r * 1.1
|
||||
cyl.height = sh + 0.3
|
||||
shape = cyl
|
||||
stumps += 1
|
||||
else:
|
||||
mi.free()
|
||||
continue
|
||||
mi.material_override = _bark_mat
|
||||
mi.transform = xf
|
||||
root.add_child(mi)
|
||||
var col_xf := xf
|
||||
if shape is CylinderShape3D:
|
||||
col_xf.origin.y += (shape as CylinderShape3D).height * 0.5 - 0.3
|
||||
_add_shape(shape, col_xf)
|
||||
|
||||
# --- grass streaming --------------------------------------------------------
|
||||
|
||||
func _update_grass(budget: int) -> void:
|
||||
var p := _player.global_position
|
||||
var reach := grass_view_distance + GRASS_TILE
|
||||
var c := Vector2i(floori(p.x / GRASS_TILE), floori(p.z / GRASS_TILE))
|
||||
var tr := int(ceil(reach / GRASS_TILE))
|
||||
var wanted := {}
|
||||
var todo: Array = []
|
||||
for dz in range(-tr, tr + 1):
|
||||
for dx in range(-tr, tr + 1):
|
||||
var k := c + Vector2i(dx, dz)
|
||||
var d := Vector2((k.x + 0.5) * GRASS_TILE, (k.y + 0.5) * GRASS_TILE).distance_to(Vector2(p.x, p.z))
|
||||
if d > reach:
|
||||
continue
|
||||
wanted[k] = true
|
||||
if not _grass_tiles.has(k):
|
||||
todo.append([d, k])
|
||||
todo.sort_custom(func(a, b): return a[0] < b[0])
|
||||
for i in range(mini(budget, todo.size())):
|
||||
var k: Vector2i = todo[i][1]
|
||||
_grass_tiles[k] = _make_grass_tile(k)
|
||||
for k: Vector2i in _grass_tiles.keys():
|
||||
if wanted.has(k):
|
||||
continue
|
||||
var d := Vector2((k.x + 0.5) * GRASS_TILE, (k.y + 0.5) * GRASS_TILE).distance_to(Vector2(p.x, p.z))
|
||||
if d > reach + GRASS_TILE * 1.5:
|
||||
var node = _grass_tiles[k]
|
||||
if node != null and is_instance_valid(node):
|
||||
node.queue_free()
|
||||
_grass_tiles.erase(k)
|
||||
|
||||
func _make_grass_tile(k: Vector2i) -> Node3D:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = hash(k) ^ world_seed
|
||||
var ox := k.x * GRASS_TILE
|
||||
var oz := k.y * GRASS_TILE
|
||||
var centre := Vector3(ox + GRASS_TILE * 0.5, 0.0, oz + GRASS_TILE * 0.5)
|
||||
var grass_xf: Array = []
|
||||
var flower_xf: Array = []
|
||||
var flower_col: Array = []
|
||||
var steps := int(GRASS_TILE / grass_spacing)
|
||||
for iz in range(steps):
|
||||
for ix in range(steps):
|
||||
var x := ox + (ix + rng.randf()) * grass_spacing
|
||||
var z := oz + (iz + rng.randf()) * grass_spacing
|
||||
var keep := rng.randf(); var yaw := rng.randf() * TAU
|
||||
var s := rng.randf_range(0.75, 1.25); var fl := rng.randf()
|
||||
if not terrain.in_bounds(x, z):
|
||||
continue
|
||||
var h: float = terrain.sample_height(x, z)
|
||||
if h < terrain.water_level + 0.25 or h > terrain.snow_height - 6.0:
|
||||
continue
|
||||
var n: Vector3 = terrain.sample_normal(x, z)
|
||||
if n.y < 0.72:
|
||||
continue
|
||||
var meadow := _meadow_noise.get_noise_2d(x, z)
|
||||
var dens := smoothstep(-0.45, 0.1, meadow) * (1.0 - smoothstep(0.85, 0.72, n.y) * 0.7)
|
||||
if keep > dens:
|
||||
continue
|
||||
var tall := lerpf(0.7, 1.35, smoothstep(-0.1, 0.4, meadow))
|
||||
var b := Basis(Vector3.UP, yaw) * Basis.from_scale(Vector3(s, s * tall, s))
|
||||
grass_xf.append(Transform3D(b, Vector3(x - centre.x, h - 0.03, z - centre.z)))
|
||||
if fl < 0.02 * smoothstep(0.0, 0.4, meadow):
|
||||
var ci := int(absf(_forest_noise.get_noise_2d(x * 3.0, z * 3.0)) * 10.0) % FLOWER_COLORS.size()
|
||||
flower_xf.append(Transform3D(Basis(Vector3.UP, yaw) * Basis.from_scale(Vector3.ONE * s), Vector3(x - centre.x, h, z - centre.z)))
|
||||
flower_col.append(FLOWER_COLORS[ci])
|
||||
if grass_xf.is_empty():
|
||||
return null
|
||||
var node := Node3D.new()
|
||||
node.position = centre
|
||||
var g := _mm_instance(_grass_mesh, grass_xf, [], false)
|
||||
_fade(g, grass_view_distance)
|
||||
node.add_child(g)
|
||||
if not flower_xf.is_empty():
|
||||
var f := _mm_instance(_flower_mesh, flower_xf, flower_col, false)
|
||||
_fade(f, grass_view_distance * 0.7)
|
||||
node.add_child(f)
|
||||
_grass_root.add_child(node)
|
||||
return node
|
||||
|
||||
func _fade(gi: GeometryInstance3D, dist: float) -> void:
|
||||
gi.visibility_range_end = dist
|
||||
gi.visibility_range_end_margin = 10.0
|
||||
gi.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF
|
||||
|
||||
Reference in New Issue
Block a user