feat: author resource-aware riverbank grove

This commit is contained in:
Rijad Zuzo
2026-07-17 13:57:57 +02:00
parent 145b90eb97
commit 04c5371b6b
23 changed files with 823 additions and 63 deletions
+210
View File
@@ -0,0 +1,210 @@
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