feat(environment): add painterly skies and living meadows
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
class_name PainterlyMeadow
|
||||
extends Node3D
|
||||
## Decorative flower drifts. Local seeded placement never touches simulation RNG.
|
||||
|
||||
const FLOWER_SCENES: Array[PackedScene] = [
|
||||
preload("res://assets/meadow/cream_daisy.glb"),
|
||||
preload("res://assets/meadow/pink_cosmos.glb"),
|
||||
preload("res://assets/meadow/yellow_buttercup.glb"),
|
||||
preload("res://assets/meadow/lavender_spire.glb"),
|
||||
]
|
||||
const FLOWER_SHADER := preload("res://world/jajce/materials/meadow_flowers.gdshader")
|
||||
const PROFILE_DENSITY := [1.0, 0.65, 0.32]
|
||||
const PROFILE_DISTANCE := [56.0, 46.0, 34.0]
|
||||
|
||||
@export var terrain_path := NodePath("../TerrainRoot/Terrain3D")
|
||||
@export_range(0, 450, 1) var clumps_per_species := 420
|
||||
@export var placement_seed := 57091
|
||||
## Local x/z center, then ellipse x/z radii. Each drift favors one flower color.
|
||||
@export var drifts: Array[Vector4] = [
|
||||
Vector4(7.0, 20.0, 7.0, 3.5),
|
||||
Vector4(15.0, 13.0, 2.4, 4.0),
|
||||
Vector4(-5.0, 20.0, 5.0, 3.0),
|
||||
Vector4(-18.0, -21.0, 4.5, 3.2),
|
||||
Vector4(14.0, -10.0, 3.8, 4.5),
|
||||
Vector4(-25.0, -17.0, 3.5, 4.0),
|
||||
Vector4(8.0, -20.0, 5.0, 2.5),
|
||||
Vector4(-2.0, -24.0, 6.0, 2.2),
|
||||
Vector4(17.0, 24.0, 4.0, 3.0),
|
||||
Vector4(-9.0, 21.0, 3.6, 2.5),
|
||||
Vector4(9.0, 7.2, 3.2, 1.8),
|
||||
Vector4(-16.0, 19.0, 3.0, 2.4),
|
||||
]
|
||||
|
||||
var _terrain: Terrain3D
|
||||
var _batches: Array[MultiMeshInstance3D] = []
|
||||
var _material: ShaderMaterial
|
||||
var _quality := 0
|
||||
var _clearings: Array[Vector3] = []
|
||||
var _path_strips: Array[MeshInstance3D] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
call_deferred("_build_meadow")
|
||||
|
||||
|
||||
func apply_presentation_quality(quality: int) -> void:
|
||||
_quality = clampi(quality, 0, PROFILE_DENSITY.size() - 1)
|
||||
for batch in _batches:
|
||||
batch.multimesh.visible_instance_count = floori(
|
||||
batch.multimesh.instance_count * PROFILE_DENSITY[_quality]
|
||||
)
|
||||
if _material != null:
|
||||
_material.set_shader_parameter("fade_begin", PROFILE_DISTANCE[_quality] - 12.0)
|
||||
_material.set_shader_parameter("fade_end", PROFILE_DISTANCE[_quality])
|
||||
|
||||
|
||||
func get_presentation_stats() -> Dictionary:
|
||||
var total := 0
|
||||
var visible_count := 0
|
||||
for batch in _batches:
|
||||
total += batch.multimesh.instance_count
|
||||
visible_count += batch.multimesh.visible_instance_count
|
||||
return {
|
||||
"flower_clump_count": total,
|
||||
"visible_flower_clump_count": visible_count,
|
||||
"multimesh_batch_count": _batches.size(),
|
||||
}
|
||||
|
||||
|
||||
func _build_meadow() -> void:
|
||||
if not _batches.is_empty():
|
||||
return
|
||||
_terrain = get_node_or_null(terrain_path) as Terrain3D
|
||||
if _terrain == null or _terrain.data == null or drifts.is_empty():
|
||||
return
|
||||
_collect_clearings()
|
||||
_material = ShaderMaterial.new()
|
||||
_material.shader = FLOWER_SHADER
|
||||
for species in FLOWER_SCENES.size():
|
||||
var source := FLOWER_SCENES[species].instantiate()
|
||||
var mesh := _find_mesh(source)
|
||||
if mesh != null:
|
||||
_add_batch(mesh, species)
|
||||
source.free()
|
||||
apply_presentation_quality(_quality)
|
||||
|
||||
|
||||
func _find_mesh(node: Node) -> Mesh:
|
||||
if node is MeshInstance3D:
|
||||
return (node as MeshInstance3D).mesh
|
||||
for child in node.get_children():
|
||||
var mesh := _find_mesh(child)
|
||||
if mesh != null:
|
||||
return mesh
|
||||
return null
|
||||
|
||||
|
||||
func _add_batch(mesh: Mesh, species: int) -> void:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = placement_seed + species * 173
|
||||
var transforms: Array[Transform3D] = []
|
||||
var variations: Array[Color] = []
|
||||
for _attempt in clumps_per_species * 18:
|
||||
if transforms.size() >= clumps_per_species:
|
||||
break
|
||||
var drift_index := rng.randi_range(0, drifts.size() - 1)
|
||||
# Broad color masses with a few companion flowers at the edges.
|
||||
if drift_index % FLOWER_SCENES.size() != species and rng.randf() > 0.12:
|
||||
continue
|
||||
var drift := drifts[drift_index]
|
||||
var angle := rng.randf_range(0.0, TAU)
|
||||
var radius := pow(rng.randf(), 0.7) * (0.9 + 0.12 * sin(angle * 3.0 + drift_index))
|
||||
var offset := Vector3(
|
||||
drift.x + cos(angle) * radius * drift.z, 0.0, drift.y + sin(angle) * radius * drift.w
|
||||
)
|
||||
var world_sample := to_global(offset)
|
||||
if _is_cleared(world_sample):
|
||||
continue
|
||||
var height := _terrain.data.get_height(world_sample)
|
||||
if is_nan(height):
|
||||
continue
|
||||
var nearby := _terrain.data.get_height(world_sample + Vector3(0.35, 0.0, 0.35))
|
||||
if is_nan(nearby) or absf(height - nearby) > 0.32:
|
||||
continue
|
||||
var local_position := to_local(Vector3(world_sample.x, height - 0.018, world_sample.z))
|
||||
var scale_variation := rng.randf_range(0.80, 1.28)
|
||||
var basis := Basis(Vector3.UP, rng.randf_range(0.0, TAU)).scaled(
|
||||
Vector3.ONE * scale_variation
|
||||
)
|
||||
transforms.append(Transform3D(basis, local_position))
|
||||
variations.append(Color(rng.randf(), 0.0, 0.0, 1.0))
|
||||
var multimesh := MultiMesh.new()
|
||||
multimesh.transform_format = MultiMesh.TRANSFORM_3D
|
||||
# Compatibility needs an explicit neutral color alongside the custom buffer
|
||||
# to preserve the GLB's painted vertex colors.
|
||||
multimesh.use_colors = true
|
||||
multimesh.use_custom_data = true
|
||||
multimesh.mesh = mesh
|
||||
multimesh.instance_count = transforms.size()
|
||||
for index in transforms.size():
|
||||
multimesh.set_instance_transform(index, transforms[index])
|
||||
multimesh.set_instance_color(index, Color.WHITE)
|
||||
multimesh.set_instance_custom_data(index, variations[index])
|
||||
var batch := MultiMeshInstance3D.new()
|
||||
batch.name = "FlowerDrifts_%d" % species
|
||||
batch.multimesh = multimesh
|
||||
batch.material_override = _material
|
||||
batch.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
batch.extra_cull_margin = 0.4
|
||||
add_child(batch)
|
||||
_batches.append(batch)
|
||||
|
||||
|
||||
func _collect_clearings() -> void:
|
||||
var village := get_node_or_null("../VillageRoot")
|
||||
if village != null:
|
||||
for child in village.get_children():
|
||||
if child is MeshInstance3D and str(child.name).begins_with("Path_"):
|
||||
_path_strips.append(child)
|
||||
elif child is Node3D:
|
||||
var radius := 4.3 if str(child.name).begins_with("House_") else 4.5
|
||||
_add_clearing(child, radius)
|
||||
var fortress := get_node_or_null("../FortressBlockout") as Node3D
|
||||
if fortress != null:
|
||||
_add_clearing(fortress, 11.0)
|
||||
for root_path in [
|
||||
"../WorldObjects/ResourceNodes",
|
||||
"../WorldObjects/StorageSites",
|
||||
"../WorldObjects/ActivitySites",
|
||||
"../WorldObjects/AnimalHabitats",
|
||||
]:
|
||||
var root := get_node_or_null(root_path)
|
||||
if root != null:
|
||||
for child in root.get_children():
|
||||
if child is Node3D:
|
||||
_add_worksite_clearing(child)
|
||||
var clusters := get_node_or_null("../WorldObjects/ResourceClusters")
|
||||
if clusters != null:
|
||||
for cluster in clusters.get_children():
|
||||
var anchors := cluster.get_node_or_null("ResourceAnchors")
|
||||
if anchors == null:
|
||||
continue
|
||||
for anchor in anchors.get_children():
|
||||
if anchor is Node3D:
|
||||
_add_worksite_clearing(anchor)
|
||||
|
||||
|
||||
func _add_worksite_clearing(node: Node3D) -> void:
|
||||
_add_clearing(node, 2.0)
|
||||
var interaction := node.get_node_or_null("InteractionPoint") as Node3D
|
||||
if interaction != null:
|
||||
_add_clearing(interaction, 1.2)
|
||||
|
||||
|
||||
func _add_clearing(node: Node3D, radius: float) -> void:
|
||||
_clearings.append(Vector3(node.global_position.x, node.global_position.z, radius))
|
||||
|
||||
|
||||
func _is_cleared(point: Vector3) -> bool:
|
||||
var flat := Vector2(point.x, point.z)
|
||||
for clearing in _clearings:
|
||||
if flat.distance_squared_to(Vector2(clearing.x, clearing.y)) < clearing.z * clearing.z:
|
||||
return true
|
||||
for strip in _path_strips:
|
||||
var box := strip.mesh as BoxMesh
|
||||
if box == null:
|
||||
continue
|
||||
var local := strip.to_local(point)
|
||||
var half_width := box.size.x * 0.5 + 0.35 / strip.global_basis.x.length()
|
||||
if absf(local.x) < half_width and absf(local.z) < box.size.z * 0.5 + 0.08:
|
||||
return true
|
||||
var river_distance := absf(point.x - JajceWatercourse.downstream_center_x(point.z))
|
||||
return (
|
||||
point.z >= JajceWatercourse.DOWNSTREAM_START_Z - 3.0
|
||||
and point.z <= JajceWatercourse.DOWNSTREAM_END_Z
|
||||
and river_distance < JajceWatercourse.downstream_half_width(point.z) + 1.7
|
||||
)
|
||||
Reference in New Issue
Block a user