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
+7 -30
View File
@@ -1,4 +1,4 @@
[gd_scene load_steps=57 format=3]
[gd_scene load_steps=58 format=3]
[ext_resource type="Terrain3DAssets" path="res://terrain/jajce/assets.tres" id="1_assets"]
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
@@ -25,6 +25,7 @@
[ext_resource type="PackedScene" path="res://assets/foliage/tree-high.glb" id="23_conifer_high"]
[ext_resource type="PackedScene" path="res://world/jajce/CozyButterfly.tscn" id="24_butterfly"]
[ext_resource type="Material" path="res://world/jajce/materials/cozy_grass_process_material.tres" id="25_grass_process"]
[ext_resource type="PackedScene" path="res://world/jajce/RiverbankResourceCluster.tscn" id="26_resource_cluster"]
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
_shader_parameters = {
@@ -410,10 +411,6 @@ position = Vector3(-4, 0, -30)
position = Vector3(19, 0, 18)
scale = Vector3(0.9, 0.9, 0.9)
[node name="Tree_River_02" parent="FoliageRoot" instance=ExtResource("4_tree")]
position = Vector3(31, 0, 14)
scale = Vector3(1.25, 1.25, 1.25)
[node name="Conifer_Citadel_01" parent="FoliageRoot" instance=ExtResource("23_conifer_high")]
position = Vector3(-39, 0, 13)
rotation_degrees = Vector3(0, 18, 0)
@@ -439,11 +436,6 @@ position = Vector3(38, 0, -8)
rotation_degrees = Vector3(0, 28, 0)
scale = Vector3(2.7, 2.7, 2.7)
[node name="Conifer_River_01" parent="FoliageRoot" instance=ExtResource("22_conifer")]
position = Vector3(38, 0, 20)
rotation_degrees = Vector3(0, -51, 0)
scale = Vector3(2.55, 2.55, 2.55)
[node name="Conifer_South_01" parent="FoliageRoot" instance=ExtResource("23_conifer_high")]
position = Vector3(-21, 0, -29)
rotation_degrees = Vector3(0, 44, 0)
@@ -566,17 +558,6 @@ yield_per_action = 2.0
safety_risk = 0.24
comfort_distance = 26.0
[node name="Tree_River_Resource_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
position = Vector3(20, 0, 18)
node_id = &"tree_river_resource_01"
action_id = &"gather_wood"
resource_id = &"wood"
initial_amount = 8.0
yield_per_action = 2.0
safety_risk = 0.22
comfort_distance = 30.0
discovery_priority = 0.1
[node name="WoodPile_Village_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
position = Vector3(5, 0, -13)
node_id = &"wood_pile_village_01"
@@ -597,15 +578,6 @@ safety_risk = 0.02
comfort_distance = 16.0
discovery_priority = 0.85
[node name="BerryBush_River_02" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
position = Vector3(28, 0, -2)
node_id = &"berry_bush_river_02"
initial_amount = 5.0
yield_per_action = 1.5
safety_risk = 0.22
comfort_distance = 28.0
discovery_priority = 0.2
[node name="Tree_Forest_Grove_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
position = Vector3(-8, 0, -22)
node_id = &"tree_forest_grove_01"
@@ -648,6 +620,11 @@ safety_risk = 0.1
comfort_distance = 20.0
discovery_priority = 0.45
[node name="ResourceClusters" type="Node3D" parent="WorldObjects"]
[node name="RiverbankResourceCluster" parent="WorldObjects/ResourceClusters" instance=ExtResource("26_resource_cluster")]
position = Vector3(33, 0, 7)
[node name="StorageSites" type="Node3D" parent="WorldObjects"]
[node name="VillagePantry" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")]
+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
@@ -0,0 +1 @@
uid://b20nnq0w3x0co
+73
View File
@@ -0,0 +1,73 @@
[gd_scene load_steps=6 format=3]
[ext_resource type="Script" path="res://world/jajce/RiverbankResourceCluster.gd" id="1_script"]
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
[ext_resource type="PackedScene" path="res://world/jajce/StylizedTree.tscn" id="3_tree"]
[ext_resource type="PackedScene" path="res://world/jajce/StylizedBerryPatch.tscn" id="4_berry"]
[ext_resource type="PackedScene" path="res://assets/foliage/tree.glb" id="5_conifer"]
[node name="RiverbankResourceCluster" type="Node3D"]
script = ExtResource("1_script")
[node name="DecorativeInstances" type="Node3D" parent="."]
[node name="Understory" type="Node3D" parent="DecorativeInstances"]
[node name="FernClumps" type="MultiMeshInstance3D" parent="DecorativeInstances/Understory"]
cast_shadow = 0
[node name="FlowerClumps" type="MultiMeshInstance3D" parent="DecorativeInstances/Understory"]
cast_shadow = 0
[node name="BankStones" type="MultiMeshInstance3D" parent="DecorativeInstances/Understory"]
[node name="Trees" type="Node3D" parent="DecorativeInstances"]
[node name="DeciduousBankTree" parent="DecorativeInstances/Trees" instance=ExtResource("3_tree")]
position = Vector3(3, 0, 4)
scale = Vector3(0.92, 0.92, 0.92)
[node name="ConiferBankTree" parent="DecorativeInstances/Trees" instance=ExtResource("5_conifer")]
position = Vector3(5, 0, 13)
rotation_degrees = Vector3(0, -51, 0)
scale = Vector3(2.55, 2.55, 2.55)
[node name="ResourceAnchors" type="Node3D" parent="."]
[node name="BerryBush_River_02" parent="ResourceAnchors" instance=ExtResource("2_resource")]
position = Vector3(0, 0, -9)
node_id = &"berry_bush_river_02"
initial_amount = 5.0
yield_per_action = 1.5
safety_risk = 0.22
comfort_distance = 28.0
discovery_priority = 0.2
[node name="MeshInstance3D" parent="ResourceAnchors/BerryBush_River_02/Visual"]
visible = false
[node name="BerryPatchPresentation" parent="ResourceAnchors/BerryBush_River_02/Visual" instance=ExtResource("4_berry")]
scale = Vector3(1.15, 1.15, 1.15)
[node name="InteractionPoint" parent="ResourceAnchors/BerryBush_River_02"]
position = Vector3(-2.5, 0, 2)
[node name="Tree_River_Resource_01" parent="ResourceAnchors" instance=ExtResource("2_resource")]
position = Vector3(0, 0, 8)
node_id = &"tree_river_resource_01"
action_id = &"gather_wood"
resource_id = &"wood"
initial_amount = 8.0
yield_per_action = 2.0
safety_risk = 0.22
comfort_distance = 30.0
discovery_priority = 0.1
[node name="MeshInstance3D" parent="ResourceAnchors/Tree_River_Resource_01/Visual"]
visible = false
[node name="TreePresentation" parent="ResourceAnchors/Tree_River_Resource_01/Visual" instance=ExtResource("3_tree")]
scale = Vector3(1.08, 1.08, 1.08)
[node name="InteractionPoint" parent="ResourceAnchors/Tree_River_Resource_01"]
position = Vector3(-4, 0, 2)
+85
View File
@@ -0,0 +1,85 @@
class_name StylizedBerryPatch
extends Node3D
const WIND_SHADER := preload("res://world/jajce/materials/wind.gdshader")
const LEAF_POSITIONS := [
Vector3(-0.52, 0.42, 0.08),
Vector3(0.08, 0.48, -0.14),
Vector3(0.58, 0.4, 0.18),
Vector3(-0.18, 0.7, 0.18),
Vector3(0.38, 0.67, -0.22),
]
const LEAF_SCALES := [
Vector3(0.72, 0.38, 0.62),
Vector3(0.78, 0.44, 0.68),
Vector3(0.66, 0.36, 0.58),
Vector3(0.56, 0.32, 0.52),
Vector3(0.5, 0.28, 0.46),
]
const BERRY_POSITIONS := [
Vector3(-0.7, 0.64, 0.2),
Vector3(-0.43, 0.82, -0.34),
Vector3(-0.14, 0.7, 0.46),
Vector3(0.08, 0.88, -0.08),
Vector3(0.34, 0.7, 0.42),
Vector3(0.48, 0.86, -0.3),
Vector3(0.74, 0.6, 0.12),
Vector3(-0.02, 1.02, 0.22),
]
func _ready() -> void:
_build_leaves()
_build_berries()
func get_presentation_instance_count() -> int:
return LEAF_POSITIONS.size() + BERRY_POSITIONS.size()
func _build_leaves() -> void:
var mesh := SphereMesh.new()
mesh.radius = 1.0
mesh.height = 2.0
mesh.radial_segments = 9
mesh.rings = 5
var material := ShaderMaterial.new()
material.shader = WIND_SHADER
material.set_shader_parameter("albedo", Color("#426c32"))
material.set_shader_parameter("roughness", 0.92)
material.set_shader_parameter("wind_strength", 0.045)
material.set_shader_parameter("wind_speed", 0.48)
material.set_shader_parameter("wind_phase", 1.7)
mesh.material = material
var transforms: Array[Transform3D] = []
for index in LEAF_POSITIONS.size():
transforms.append(
Transform3D(Basis.IDENTITY.scaled(LEAF_SCALES[index]), LEAF_POSITIONS[index])
)
$Leaves.multimesh = _create_multimesh(mesh, transforms)
func _build_berries() -> void:
var mesh := SphereMesh.new()
mesh.radius = 0.12
mesh.height = 0.21
mesh.radial_segments = 8
mesh.rings = 4
var material := StandardMaterial3D.new()
material.albedo_color = Color("#8f3148")
material.roughness = 0.78
mesh.material = material
var transforms: Array[Transform3D] = []
for position in BERRY_POSITIONS:
transforms.append(Transform3D(Basis.IDENTITY, position))
$Berries.multimesh = _create_multimesh(mesh, transforms)
func _create_multimesh(mesh: Mesh, transforms: Array) -> MultiMesh:
var multimesh := MultiMesh.new()
multimesh.transform_format = MultiMesh.TRANSFORM_3D
multimesh.mesh = mesh
multimesh.instance_count = transforms.size()
for index in transforms.size():
multimesh.set_instance_transform(index, transforms[index])
return multimesh
+1
View File
@@ -0,0 +1 @@
uid://ctb2v3prn7ovc
+12
View File
@@ -0,0 +1,12 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://world/jajce/StylizedBerryPatch.gd" id="1_script"]
[node name="StylizedBerryPatch" type="Node3D"]
script = ExtResource("1_script")
[node name="Leaves" type="MultiMeshInstance3D" parent="."]
cast_shadow = 0
[node name="Berries" type="MultiMeshInstance3D" parent="."]
cast_shadow = 0