class_name RiverbankResourceCluster extends Node3D const FERN_OFFSETS := [ Vector3(-0.8, 0.0, -10.8), Vector3(1.1, 0.0, -8.7), Vector3(2.9, 0.0, -6.8), Vector3(-1.4, 0.0, -4.6), Vector3(1.8, 0.0, -2.7), Vector3(3.6, 0.0, -0.5), Vector3(-0.5, 0.0, 1.2), Vector3(2.4, 0.0, 3.4), Vector3(-1.8, 0.0, 5.7), Vector3(1.4, 0.0, 7.6), Vector3(3.8, 0.0, 9.4), Vector3(0.2, 0.0, 11.8), ] const FLOWER_OFFSETS := [ Vector3(0.2, 0.0, -10.2), Vector3(2.2, 0.0, -7.5), Vector3(-0.4, 0.0, -5.5), Vector3(3.4, 0.0, -3.2), Vector3(0.8, 0.0, 0.1), Vector3(2.8, 0.0, 2.1), Vector3(-0.7, 0.0, 4.4), Vector3(2.0, 0.0, 6.4), Vector3(0.4, 0.0, 9.8), ] const STONE_OFFSETS := [ Vector3(3.9, 0.0, -9.6), Vector3(-1.2, 0.0, -7.2), Vector3(3.2, 0.0, -4.3), Vector3(-0.7, 0.0, -1.2), Vector3(3.7, 0.0, 4.6), Vector3(-1.1, 0.0, 8.4), Vector3(2.7, 0.0, 11.2), ] @export var terrain_path := NodePath("../../../TerrainRoot/Terrain3D") var terrain: Terrain3D func _ready() -> void: call_deferred("_initialize_cluster") func get_resource_anchors() -> Array[ResourceNode]: var anchors: Array[ResourceNode] = [] for child in $ResourceAnchors.get_children(): if child is ResourceNode: anchors.append(child) return anchors func get_presentation_stats() -> Dictionary: var understory_instances := 0 var multimesh_batches := 0 for child in $DecorativeInstances/Understory.get_children(): var instance := child as MultiMeshInstance3D if instance == null or instance.multimesh == null: continue understory_instances += instance.multimesh.instance_count multimesh_batches += 1 return { "understory_instance_count": understory_instances, "multimesh_batch_count": multimesh_batches, "decorative_tree_count": $DecorativeInstances/Trees.get_child_count(), "resource_anchor_count": get_resource_anchors().size(), } func _initialize_cluster() -> void: terrain = get_node_or_null(terrain_path) as Terrain3D if terrain == null: push_warning("RiverbankResourceCluster: Terrain3D is unavailable") return _snap_authored_children($DecorativeInstances/Trees) for anchor in get_resource_anchors(): _snap_resource_anchor(anchor) _build_understory() func _snap_authored_children(parent: Node) -> void: for child in parent.get_children(): if child is Node3D: _snap_to_terrain(child) func _snap_to_terrain(node: Node3D) -> void: var position := node.global_position var height := terrain.data.get_height(position) if not is_nan(height): node.global_position.y = height func _snap_resource_anchor(anchor: ResourceNode) -> void: var anchor_position := anchor.global_position var anchor_height := terrain.data.get_height(anchor_position) if is_nan(anchor_height): return var interaction_position := anchor.global_transform * anchor.interaction_point.position var interaction_height := terrain.data.get_height(interaction_position) if not is_nan(interaction_height): anchor.interaction_point.position.y = interaction_height - anchor_height + 0.05 anchor.global_position.y = anchor_height func _build_understory() -> void: var fern_material := StandardMaterial3D.new() fern_material.albedo_color = Color("#315f35") fern_material.roughness = 0.94 fern_material.cull_mode = BaseMaterial3D.CULL_DISABLED _assign_multimesh( $DecorativeInstances/Understory/FernClumps, _create_fern_mesh(fern_material), FERN_OFFSETS, Vector3(0.8, 0.8, 0.8), 0.03 ) var flower_mesh := SphereMesh.new() flower_mesh.radius = 0.13 flower_mesh.height = 0.22 flower_mesh.radial_segments = 7 flower_mesh.rings = 4 var flower_material := StandardMaterial3D.new() flower_material.albedo_color = Color("#efc55a") flower_material.roughness = 0.82 flower_mesh.material = flower_material _assign_multimesh( $DecorativeInstances/Understory/FlowerClumps, flower_mesh, FLOWER_OFFSETS, Vector3.ONE, 0.2 ) var stone_mesh := SphereMesh.new() stone_mesh.radius = 1.0 stone_mesh.height = 2.0 stone_mesh.radial_segments = 8 stone_mesh.rings = 4 var stone_material := StandardMaterial3D.new() stone_material.albedo_color = Color("#77786b") stone_material.roughness = 0.96 stone_mesh.material = stone_material _assign_multimesh( $DecorativeInstances/Understory/BankStones, stone_mesh, STONE_OFFSETS, Vector3(0.62, 0.24, 0.44), 0.22 ) func _assign_multimesh( target: MultiMeshInstance3D, mesh: Mesh, offsets: Array, base_scale: Vector3, vertical_offset: float ) -> void: var multimesh := MultiMesh.new() multimesh.transform_format = MultiMesh.TRANSFORM_3D multimesh.mesh = mesh multimesh.instance_count = offsets.size() for index in offsets.size(): var offset: Vector3 = offsets[index] var world_sample := to_global(offset) var height := terrain.data.get_height(world_sample) if is_nan(height): height = global_position.y var local_position := to_local(Vector3(world_sample.x, height, world_sample.z)) local_position.y += vertical_offset var scale_variation := 0.84 + float((index * 37) % 5) * 0.07 var rotation := deg_to_rad(float((index * 53) % 360)) var basis := Basis(Vector3.UP, rotation).scaled(base_scale * scale_variation) multimesh.set_instance_transform(index, Transform3D(basis, local_position)) target.multimesh = multimesh func _create_fern_mesh(material: Material) -> ArrayMesh: var vertices := PackedVector3Array() var normals := PackedVector3Array() var uvs := PackedVector2Array() for frond_index in 5: var angle := float(frond_index) / 5.0 * TAU + 0.3 var direction := Vector3(cos(angle), 0.0, sin(angle)) var side := Vector3(-direction.z, 0.0, direction.x) * 0.16 var base := Vector3(0.0, 0.02, 0.0) var middle := direction * 0.38 + Vector3.UP * 0.14 var tip := direction * 0.82 + Vector3.UP * 0.24 vertices.append_array([base, middle + side, tip, base, tip, middle - side]) for _vertex in 6: normals.append(Vector3.UP) for uv in [ Vector2(0.5, 0.0), Vector2(1.0, 0.5), Vector2(0.5, 1.0), Vector2(0.5, 0.0), Vector2(0.5, 1.0), Vector2(0.0, 0.5), ]: uvs.append(uv) var arrays := [] arrays.resize(Mesh.ARRAY_MAX) arrays[Mesh.ARRAY_VERTEX] = vertices arrays[Mesh.ARRAY_NORMAL] = normals arrays[Mesh.ARRAY_TEX_UV] = uvs var mesh := ArrayMesh.new() mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) mesh.surface_set_material(0, material) return mesh