Scatterbox wasn’t working for Terrain3D so I added a Raycast function … I only added this to the ScatterScene3D.gd not to the other files. The scatter scene now has an @export variable that takes a Terrain3D object as a parameter.
Tested on Godot 4.6.2 stable, with Terrain3D 1.01 Nightly build from yesterday.
The static collision option wasnt viable because the editor kept freezing and building the collision took too much memory for an 8k map.
Any tips on improving the custom ray cast function would be welcome. Simply takes the rays and walks along them 1m per step until the Terrain3D height is greater than the ray height and that’s the intersection point. (NOT vibe coded, just ray casting from memory and I’m rusty, so please leave any tips in comments below)
To use the plugin, make sure Terrain3D has the editor/dynamic collision feature set, and also I like to start by moving the DrawPointer node until its at the height of the terrain in a visible location. Dont move the ScatterScene3D node, that has to remain at the origin. Then I select the ScatterScene3D node, add the scenes I want to scatter, set the collision mask, then make sure the Terrain3D @export is set, then I scale up the DrawPointer using the scroll wheel - it should start to follow the mouse and align with the terrain normal, then click to scatter some scenes.
DarkGrey456/Scatterbox_For_Terrain3D
modifications …
@tool
extends ScatterBox
class_name ScatterScene3D
@export var scenes: Array[PackedScene]
@export_flags_3d_physics var instanced_scene_collision_layers
@export var terrain3D:Terrain3D
class COLLISION:
var position:Vector3
var normal:Vector3
func ray_intersect_terrain(start:Vector3, end:Vector3)->COLLISION:
if terrain3D == null:
var result = _space.intersect_ray(PhysicsRayQueryParameters3D.create(start, end, ~instanced_scene_collision_layers))
var collision:COLLISION = COLLISION.new()
if result:
collision.position = result.position
collision.normal = result.normal
return collision
return null
else:
var ray_cast:Vector3 = start
var intersect = false
var del = 1.0
var collision:COLLISION = COLLISION.new()
var MAX_STEPS = 200
var curr_step = 0
while (intersect == false) and curr_step < MAX_STEPS:
ray_cast += (end - start).normalized() * del
var location = Vector3(ray_cast.x, 0.0, ray_cast.z)
var height = terrain3D.data.get_height(location)
curr_step += 1
if height > ray_cast.y:
intersect = true
var norm = terrain3D.data.get_normal(location)
collision.position = ray_cast
collision.normal = norm
return collision
return null
#overwrite default code
func move_to_mouse(camera, mouse: Vector2):
var start = camera.project_ray_origin(mouse)
var end = start + camera.project_ray_normal(mouse) * 1000
if terrain3D:
terrain3D.set_camera(camera)
if not _space:
_space = get_parent_node_3d().get_world_3d().direct_space_state
var result = ray_intersect_terrain(start,end)
if result == null:
print("EMPTY RESULT")
return true
var t := Transform3D()
t.origin = result.position
t.origin += offset_position
#align mesh with floor nomral
t.basis = Basis(result.normal.cross(global_transform.basis.z),
result.normal,
global_transform.basis.x.cross(result.normal),
).orthonormalized()
draw_pointer.basis = t.basis
draw_pointer.global_transform.origin = result.position
return true
func refresh():
pass
func scatter_obj():
for i in range(count):
var pos := draw_pointer.global_position
pos += Vector3(
_rng.randf_range(-placement_size.x / 2.0, placement_size.x / 2.0),
0,
_rng.randf_range(-placement_size.z / 2.0, placement_size.z / 2.0))
pos = pos + Vector3(
_rng.randf_range(min_random_size.x, max_random_size.x),
0,
_rng.randf_range(min_random_size.z, max_random_size.z))
var startPos = pos
startPos.y += placement_size.y
var endPos = pos
endPos.y -= placement_size.y
var hit = ray_intersect_terrain(startPos,endPos)
#var ray = PhysicsRayQueryParameters3D.create(startPos, endPos, ~instanced_scene_collision_layers)
#var hit = _space.intersect_ray(ray)
if(hit==null): continue
var t := Transform3D()
t.origin = hit.position
t.origin += offset_position
#https://kidscancode.org/godot_recipes/3.x/3d/3d_align_surface/index.html
t.basis.y = hit.normal
t.basis.x = -t.basis.z.cross(hit.normal)
t.basis = t.basis.orthonormalized()
var scale = _rng.randf_range(min_random_size.x, max_random_size.x)
t.basis = t.basis.scaled(Vector3(
scale,
scale,
scale))\
.rotated(Vector3.RIGHT, deg_to_rad(_rng.randf_range(-random_rotation.x, random_rotation.x)))\
.rotated(Vector3.UP, deg_to_rad(_rng.randf_range(-random_rotation.y, random_rotation.y)))\
.rotated(Vector3.FORWARD, deg_to_rad(_rng.randf_range(-random_rotation.z, random_rotation.z)))
var rand_scene = _rng.randi_range(0, scenes.size()-1)
var scene_inst = scenes[rand_scene].instantiate()
scene_inst.global_transform = t
if(scene_inst is StaticBody3D):
scene_inst.collision_layer = instanced_scene_collision_layers
object_parent.add_child(scene_inst)
scene_inst.set_owner(get_tree().edited_scene_root)
func erase_obj():
var pos := draw_pointer.global_position
var start_pos = pos
start_pos -= Vector3(placement_size.x/2, placement_size.y/2, placement_size.z/2)
var box = AABB(start_pos, placement_size)
for child in object_parent.get_children():
if(box.has_point(child.global_transform.origin)):
child.queue_free()