Created
March 15, 2025 23:14
-
-
Save jamonholmgren/3694ecdade20647da5ad30124380f065 to your computer and use it in GitHub Desktop.
Godot 4.4 MultiMeshInstance3D Tree scattering implementation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@tool | |
class_name Trees extends MultiMeshInstance3D | |
# Dimensions of the area to scatter the trees | |
@export var width: float = 2000.0 | |
@export var height: float = 2000.0 | |
@export var terrain: Terrain3D = null | |
@export var tree_spread_region_size: float = 20000.0 | |
# For detecting nearby vehicles and converting meshes to the TreeCopse-01 scene | |
var proximity_detectors: Array[Area3D] = [] | |
@export var tree_copse_scene: PackedScene = null | |
@export var place_trees: bool = false: | |
set(v): | |
place_trees = v | |
placed_trees = 0 | |
set_process(v) | |
var placed_trees: int = 0 | |
var instance_metadata: Array[Dictionary] = [] | |
func _ready() -> void: | |
setup_proximity_detectors() | |
# Generate more Tree multimesh instances? | |
if name == "Trees" and not Engine.is_editor_hint(): | |
generate_all_tree_multimeshes.call_deferred() | |
place_trees = true | |
func _process(_delta: float) -> void: | |
if not place_trees: return | |
placed_trees += 1 | |
if placed_trees >= multimesh.instance_count: | |
place_trees = false | |
set_process(false) | |
return | |
place_tree(placed_trees - 1) | |
func setup_proximity_detectors() -> void: | |
instance_metadata.resize(multimesh.instance_count) | |
proximity_detectors.resize(multimesh.instance_count) | |
func setup_proximity_detector(i: int, pos: Vector3) -> void: | |
var detector: Area3D = Area3D.new() | |
detector.body_entered.connect(on_body_entered.bind(i)) | |
detector.body_exited.connect(on_body_exited.bind(i)) | |
detector.monitorable = false | |
detector.monitoring = false | |
detector.set_collision_mask_value(1, true) # turn on collision with vehicles | |
detector.set_collision_mask_value(9, true) # turn on collision with projectiles | |
# Add a sphere3d collision shape to the area3d | |
var shape: CollisionShape3D = CollisionShape3D.new() | |
var sphere: SphereShape3D = SphereShape3D.new() | |
sphere.radius = 80.0 | |
shape.shape = sphere | |
detector.add_child(shape) | |
proximity_detectors[i] = detector | |
add_child(detector) | |
proximity_detectors[i].global_transform = global_transform.translated(pos) | |
proximity_detectors[i].monitoring = true | |
func generate_all_tree_multimeshes() -> void: | |
# Make sure we cover an area of 20k by 20k | |
var w: int = int(tree_spread_region_size / width) | |
var h: int = int(tree_spread_region_size / height) | |
var w2: int = w / 2 | |
var h2: int = h / 2 | |
var origin_x: float = global_position.x | |
var origin_z: float = global_position.z | |
for x in range(-w2, w2): | |
for z in range(-h2, h2): | |
var dup: MultiMeshInstance3D = duplicate() | |
dup.multimesh = multimesh.duplicate(true) | |
dup.name = "TreesCopy-%d-%d" % [x, z] | |
get_parent().add_child(dup) | |
# Add some variation to the position | |
var rand_x: float = randf_range(-width / 2.0, width / 2.0) | |
var rand_z: float = randf_range(-height / 2.0, height / 2.0) | |
dup.global_position = Vector3(origin_x + (x * width) + rand_x, 0.0, origin_z + (z * height) + rand_z) | |
func place_tree(i: int) -> void: | |
# Set metadata for this tree instance | |
instance_metadata[i] = { | |
"nearby_bodies": 0, | |
"rigidbody_hit": false, | |
"rigidbody": null | |
} | |
# scatters trees around my position | |
var ho: Vector3 = global_position | |
# Generate random x,z coordinates within width/height bounds | |
var x: float = randf_range(ho.x - width / 2.0, ho.x + width / 2.0) | |
var z: float = randf_range(ho.z - height / 2.0, ho.z + height / 2.0) | |
var pos: Vector3 = Vector3(x, 0.0, z) | |
# Get terrain height at position | |
var ht: float = terrain.data.get_height(pos) | |
if ht == NAN: return | |
pos.y = ht | |
# Verify that we are not too close to any vehicles | |
for h in get_tree().get_nodes_in_group("vehicles"): | |
var dist: float = 120.0 if h is Helicopter else 30.0 | |
if pos.distance_to(h.global_position) < dist: | |
placed_trees -= 1 # do-over | |
return | |
# Create transform with random rotation around Y axis | |
var t: Transform3D = Transform3D() | |
t = t.rotated(Vector3.UP, randf_range(0, PI * 2)) | |
# # Apply random scale variation | |
# # Disabled [doesn't work when I replace instances with rigidbodies for dynamic collisions] | |
# var sc: float = randf_range(3.0, 5.0) | |
# t = t.scaled(Vector3(sc, sc, sc)) | |
t = t.scaled(Vector3(3.0, 3.0, 3.0)) # Fixed 3.0 scale for now | |
t = t.translated(pos) | |
t = t.translated(-global_position) # remove current offset | |
multimesh.set_instance_transform(i, t) | |
# Now also set the area3d and turn it on | |
setup_proximity_detector(i, t.origin) | |
func enable_rigidbody(i: int) -> void: | |
var treecopse: Node3D = instance_metadata[i].get("rigidbody", null) | |
if treecopse: return # already enabled | |
# Get the transform for this instance, and then add the treecopse instance there | |
var t: Transform3D = multimesh.get_instance_transform(i) | |
treecopse = tree_copse_scene.instantiate() | |
Game.pausable_root.add_child(treecopse) | |
treecopse.transform.origin = global_transform.origin + t.origin | |
treecopse.transform.basis = t.basis | |
treecopse.scale = Vector3(1.0, 1.0, 1.0) | |
instance_metadata[i]["rigidbody"] = treecopse | |
# Also subscribe to any RigidBody3D collisions within the treecopse | |
for child_tree in treecopse.get_children(): | |
# child_tree.set_collision_mask_value(3, false) # turn off collision with other trees, for now | |
# child_tree.set_collision_mask_value(16, false) # turn off collision with terrain, for now | |
child_tree.body_entered.connect(on_collision_treecopse.bind(i, child_tree)) | |
child_tree.sleeping = true | |
child_tree.get_node("Area3D").monitoring = true | |
child_tree.get_node("Area3D").body_entered.connect(on_proximity_tree_entered.bind(i, child_tree)) | |
# child_tree.freeze = true | |
# Move the instance underground by 1000 meters | |
var t3d: Transform3D = multimesh.get_instance_transform(i) | |
t3d.origin.y -= 1000.0 | |
multimesh.set_instance_transform(i, t3d) | |
func disable_rigidbody(i: int) -> void: | |
var rigidbody_hit: bool = instance_metadata[i].get("rigidbody_hit", false) | |
if rigidbody_hit: return # it's been modified, so we can't use the fake trees anymore | |
var treecopse: Node3D = instance_metadata[i].get("rigidbody", null) | |
if treecopse: treecopse.queue_free() | |
instance_metadata[i]["rigidbody"] = null | |
# Move the instance back up by 1000 meters to the original position | |
var t3d: Transform3D = multimesh.get_instance_transform(i) | |
t3d.origin.y += 1000.0 | |
multimesh.set_instance_transform(i, t3d) | |
func on_proximity_tree_entered(other: Node3D, _i: int, tree: RigidBody3D) -> void: | |
if not other is Vehicle: return # ignore collisions other than vehicles | |
# We know something is about to hit the tree, so wake up the rigidbody | |
tree.sleeping = false | |
tree.freeze = false | |
set_axis_lock(tree, false) | |
func set_axis_lock(tree: RigidBody3D, lock: bool) -> void: | |
tree.axis_lock_angular_x = lock | |
tree.axis_lock_angular_y = lock | |
tree.axis_lock_angular_z = lock | |
tree.axis_lock_linear_x = lock | |
tree.axis_lock_linear_y = lock | |
tree.axis_lock_linear_z = lock | |
func on_collision_treecopse(other: Node3D, i: int, tree: RigidBody3D) -> void: | |
if other is Terrain3D or other is Terrain3DObjects: return # ignore collisions with the terrain | |
# Now that something hit something in the treecopse, | |
# we will never disable the rigidbody again, because | |
# we need to preserve the state for the physics. | |
instance_metadata[i]["rigidbody_hit"] = true | |
# Wake it up, so it can fall down | |
tree.sleeping = false | |
tree.freeze = false | |
set_axis_lock(tree, false) | |
tree.set_collision_mask_value(3, true) # turn on collision with other trees | |
tree.set_collision_mask_value(16, true) # turn on collision with terrain | |
tree.set_collision_mask_value(1, false) # turn off collision with vehicles | |
tree.set_collision_mask_value(9, false) # turn off collision with projectiles | |
# get rid of the proximity detector entirely | |
if proximity_detectors[i]: | |
proximity_detectors[i].monitoring = false | |
proximity_detectors[i].queue_free() | |
proximity_detectors[i] = null | |
# Wait 20 seconds, and then freeze the rigidbody again | |
await get_tree().create_timer(20.0).timeout | |
# TODO: just replace with a mesh instance or multimesh instance? | |
tree.sleeping = true | |
tree.freeze = true | |
set_axis_lock(tree, true) | |
# Also turn off all collisions | |
tree.set_collision_mask_value(3, false) # turn off collision with other trees | |
tree.set_collision_mask_value(16, false) # turn off collision with terrain | |
tree.set_collision_mask_value(1, false) # turn off collision with vehicles | |
tree.set_collision_mask_value(9, false) # turn off collision with projectiles | |
tree.get_node("CollisionShape3D").disabled = true | |
tree.get_node("Area3D").monitoring = false | |
func on_body_entered(_other: Node3D, i: int) -> void: | |
# Someone nearby, record in metadata | |
instance_metadata[i]["nearby_bodies"] += 1 | |
enable_rigidbody(i) | |
func on_body_exited(_other: Node3D, i: int) -> void: | |
# Someone nearby, record in metadata | |
var nearby_bodies: int = instance_metadata[i].get("nearby_bodies", 0) | |
nearby_bodies -= 1 | |
instance_metadata[i]["nearby_bodies"] = nearby_bodies | |
if nearby_bodies == 0: disable_rigidbody(i) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment