Level design was becoming a nightmare. I had this outdoor area with rocks, shrubs, and a bunch of tree variants, and I was manually positioning every single one in the Godot editor. Click, drag, rotate, scale, repeat. Over a hundred objects. It looked too uniform and took forever.
So I wrote a small @tool EditorPlugin that adds a scatter brush to the 3D viewport. Drop a list of PackedScenes into a dock, set radius and density, then click on any surface to place randomized instances. Random Y rotation, optional scale jitter, raycasted to conform to whatever geometry is underneath.
@tool
extends EditorPlugin
var brush_radius := 3.0
var brush_density := 5
var scatter_items: Array[PackedScene] = []
var brush_active := false
func _forward_3d_gui_input(viewport_camera: Camera3D, event: InputEvent) -> int:
if not brush_active or scatter_items.is_empty():
return EditorPlugin.AFTER_GUI_INPUT_PASS
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
_scatter_at_mouse(viewport_camera, event.position)
return EditorPlugin.AFTER_GUI_INPUT_STOP
return EditorPlugin.AFTER_GUI_INPUT_PASS
func _scatter_at_mouse(camera: Camera3D, mouse_pos: Vector2) -> void:
var space_state := get_viewport().world_3d.direct_space_state
var from := camera.project_ray_origin(mouse_pos)
var to := from + camera.project_ray_normal(mouse_pos) * 1000.0
var result := space_state.intersect_ray(PhysicsRayQueryParameters3D.create(from, to))
if result.is_empty():
return
for i in brush_density:
var offset := Vector3(randf_range(-brush_radius, brush_radius), 0.0,
randf_range(-brush_radius, brush_radius))
var instance := scatter_items.pick_random().instantiate() as Node3D
get_editor_interface().get_edited_scene_root().add_child(instance)
instance.owner = get_editor_interface().get_edited_scene_root()
instance.global_position = result["position"] + offset
instance.rotate_y(randf() * TAU)
instance.scale = Vector3.ONE * randf_range(0.8, 1.2)
The thing that tripped me up the longest: you have to set instance.owner to the edited scene root, otherwise all your placed objects silently vanish when you save. Godot only serializes nodes owned by the root. Cost me a good 45 minutes the first time.
What I still need: undo support via EditorUndoRedoManager. Right now a brush stroke is permanent the moment you lift the mouse button, which is painful. The API exists, I just haven't worked out how to batch a full stroke, potentially 30+ node additions, into a single undoable step cleanly. The docs are pretty sparse on bulk operations.
Also no slope-angle filtering yet, so it'll happily place trees on a vertical cliff face if your raycast hits one. Anyone done something similar and figured out the undo batching, or have a smarter approach to the collision mask so newly-placed objects don't immediately become raycast targets?