Skip to content

Instantly share code, notes, and snippets.

@jamonholmgren
Created March 15, 2025 23:14
Show Gist options
  • Save jamonholmgren/3694ecdade20647da5ad30124380f065 to your computer and use it in GitHub Desktop.
Save jamonholmgren/3694ecdade20647da5ad30124380f065 to your computer and use it in GitHub Desktop.
Godot 4.4 MultiMeshInstance3D Tree scattering implementation
@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