More information about using 3D selection boxes?
Out of sight out of mind LOL
- Edited
In my defence, what I've been doing most of the time with my stuff is working out game mechanics in general and what can work well versus what can't and how to implement it. Now people are going to see a hell of a lot more from me when it comes to posting up proper projects and won't be able to avoid the spam lol I've got some special things planned for Godot, this engine is impressive overall and I'm going to throw the kitchen sink at it to see how it handles. Looking at this 3D selection box code that @xyz has provided yeah, it needs breaking down way more to be understandable. The theory is fine but definitely some collaboration on making sure there's some sort of up to date documentation is in order and I'll be happy to go through it with people to help lighten the burden a tad, I think it's the frustum stuff that's hurting my poor brain the most.
xyz Exactly, let the engine's battle-tested native code core do the work and keep your code simple. It's probably optimized to skip the convex mesh test when the bboxes don't intersect, and no doubt it uses spatial partitioning to skip the bbox test for objects that aren't even close. And this is cheap compared to full-blown physics, which constantly tests ALL collision meshes against each other.
Remember, a bbox test alone isn't accurate. For example if you have a spherical object and you drag your selection box over the corner of its bbox, it'll be selected even though you're not actually touching the sphere. That's not necessarily better than a centerpoint test. You might as well use collision system and get partitioning and bbox optimizations "for free".
- Edited
synthnostate BBoxes with margins are quite sufficient for most cases in practice, and sphere check against a frustum is even less expensive. But for a large number of units it'd surely pay off to use engine's broadphase optimizations, especially if a large number of selectables is in play. I'd love to see this benchmarked in various scenarios.
synthnostate battle-tested native code
Not sure Godot 4 physics is actually battle-tested though
- Edited
xyz One of the reasons I've been exploring selection boxes so much is I'm doing something of a wave defence/base building game by the way, should be no trouble attempting to break Godot. I've been experimenting with my villagers so far in one of my other projects and Godot 4 has been handling it like a dream, definite performance boost I can confirm compared to Godot 3.5.
- Edited
Or you can just use it by calling the top-level function as you would with other engine functionality. In this thread, you've got everything you need to quickly make a selection system. With the project_rect()
function from my example and going with @synthnostate suggestion to use convex mesh collider for the frustum, it's almost trivial to set up.
xyz Not sure Godot 4 physics is actually battle-tested though
lol, true. The physics part needs work. I think the parts we're talking about here are pretty solid though. I'm using Area3D.get_overlapping_bodies() for other purposes with no issues.
- Edited
It's mainly working through all of the shorthand you've written so I understand what I'm even doing here, I need to focus you guys to make this thread more noob friendly lol. I see you've declared three custom variables but aside from the camera and the rect I don't know how to use this. I also don't know what sort of hierarchy setup I'm supposed to be looking at. That's something you definitely have to explain before it goes anywhere near documentation.
For instance, do I take the rect from my previous selection box code and plug that into the rect you've defined? I take it the AABB is supposed to the the collision? I don't know how to use this in relation to search for a group for example, you write it's easy but I think this is a classic example of you looking at more complex stuff on a daily basis lol.
- Edited
Lethn Well, this wasn't supposed to be a tutorial. We're is a discussion format here so if anything is not clear you can always ask. There's no setup needed at all. It just does 3d math. You can put the code in any node you like (or a standalone static class) and you only need to interact with one function, namely aabb_intersects_rect()
. I'm sure you know how to get a selection rect and a reference to the camera in your scene. Now if you for example want to test if a mesh instance is inside the selection area you just get its aabb (axis-aligned bounding box), transform it to world space and pass it to aabb_intersects_rect()
. It's literally one line of code to use this.
if aabb_intersects_rect($mesh_instance.global_transform * $mesh_instance.get_aabb(), selection_rect, $my_camera):
print("INSIDE AREA")
Same goes for @synthnostate's approach, only that it actually needs some setup but it's a trivial common setup that I think is self evident from the description they provided.
- Edited
Okay phew! Slugging through it a bit more now, thanks for the post, I think I've managed to get the rect variable setup correctly, here's how my code is looking so far with the 2D box, let me know if it's the right idea, I've just taken the selection box code I found previously and plugged in the rect to a variable you can grab from elsewhere as a minimal example for people to work from.
extends Control
var isDragging = false
var dragStart = Vector2()
var dragEnd = Vector2()
var selectionBoxRect
func _physics_process(delta):
if isDragging == true:
queue_redraw()
func _unhandled_input(event):
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.is_pressed():
isDragging = true
dragStart = get_global_mouse_position()
elif isDragging == true:
isDragging = false
var dragEnd = get_global_mouse_position()
queue_redraw()
func _draw():
if isDragging == true:
selectionBoxRect = Rect2 (dragStart, get_global_mouse_position() - dragStart)
draw_rect(selectionBoxRect, Color.DARK_GREEN, false, 2.0)
I'm thinking I just plug the selectionBoxRect from the control node into the selection_rect yes? Now I need to look at how to use the axis aligned bounding box too, I hadn't come across this little bit of code before, also here's how my hierarchy is looking at the moment.
- Edited
Right, now I've had some sleep after all that I'm going to try working through this final part but I do have some more questions @xyz I get where you're coming from but at the same time this stuff needs explaining because of all the shorthand. I was thinking that something you could do which would instantly improve the code you've posted is post up a mini-glossary of your shorthand up at the top and commented if that would be okay? This would help people like me properly get where everything is linked from. Programmers do this all the time and when you're a noob you look this stuff up and most people are just going to see the matrix not knowing what's going on.
I was thinking too how does this code execute? If it's running off the rect does that mean while the rect is active is going to be constantly checking through the AABB? I'm wanting to add the selection to an array so I'll need to know about this, I suppose a little boolean check of isSelected on the objects themselves would probably work then I can add to the array that way to make sure the code only executes once per unit.
I've also been searching up about AABB generally because this is my first time even reading the acronym lol.
https://docs.godotengine.org/en/stable/classes/class_aabb.html
https://godotengine.org/qa/52026/what-is-aabb
This is probably going to be the only up to date thread on the whole of the internet to do with Godot selection boxes so I want to make sure the information is all correct too even if it doesn't make it's way to the documentation.
- Edited
Lethn What do you mean by "shorthand"?
The code I posted is a simple API (we know what that is I hope ). It consists of only one public function;
aabb_intersects_rect()
. Everything else is helper code used by that main function and can be considered private to API. The API doesn't "run". It's an utility function you call when you need it, it runs on demand. The name of the function is self evident, I hope. It checks if an axis aligned bounding box intersects a frustum. The frustum is a pyramidal viewing volume defined by a 2d rect projected into space using camera's perspective projection parameters. It returns true if intersection happened. Otherwise it returns false. This test is the only "hard" problem when making a 3d selection system. Everything else is pretty much boilerplate and can vary depending on the specifics of a particular system.
How you use this is up to you. I didn't concern myself with architecture of any particular system. I just wrote you a useful general function that handles the hard math part of it. This function runs whenever you call it. You can call it every frame, or you can call it on mouse release. You can call it for a single mesh's bounding box, or you can call it in a loop for every mesh in the scene. Depends on the needs of your system. My code doesn't handle selecting logic itself, or selected/unselected arrays or any "high level" stuff like that. There's no selection management code, just this single intersection check. Do with it what you will.
To point out the obvious again - If you don't call this function, none of the posted code will ever run
- Edited
Okay, I think I'm slowly understanding the logic albeit very slowly, when aabb_intersects_rect() you're intersecting with the mesh using the AABB class and grabbing the mesh you're not using a collider, I think that was the thing that threw me off the most because I've not seen code used like that in Godot often.
When I post shorthand I mean when you use letters etc. as shortened versions of your custom variables and names that you've declared. This isn't me having a go at you in particular I see way too many posts like this of programmers who post up their code to be helpful of something they've made and even to other programmers who are trying to work out what the hell they've done and part of the problem is they've dumped code online with lots of p's and t's and think that's perfectly straightforward and understandable lol. It's just something of a major pet peeve of mine, I think shader code is the worse example of this I've seen and it really helps more when programmers are more verbose with their code for the sake of helping others or at least stick up a mini glossary of the meanings up top so people can look through it line by line without getting a headache. As an example I'm pretty verbose with my code because I know that people are going to potentially want to look through my projects when I'm done with them, so with that in mind I write my code that way.
shameless plug example: https://github.com/1Lethn1/By-the-gods-0.1/blob/main/MaleAdultVillager
It's definitely not as complex as yours but there's a lot there and I also want people to be able to mod all of this if they want to, so I've made sure to write it so they can get through it all and make sure anything I've written that's custom code is understandable, but I just thought I'd bring this up as a general thing because again to stress, this is the only proper thread on 3D selection boxes I've seen so far lol.
If you don't mind me asking more questions about the actual grabbing of the meshes because I am getting a headache even though I've got the general idea, you've declared a mesh_instance in relation to my own code would this be a mesh belonging to a character within the scene for example? I have these bean soldiers I've got up and I want to enable their selection rings and then also add them to an array so they've been selected. I also have an idea thanks to this code you've posted of how I'm going to do selection groups as well, that stuff should be easy and I'll post it up here as well for the sake of helping others get something properly functional.
- Edited
Lethn There's a place for long descriptive names and there's a place for short names. Local variables seldom have business bearing long names, especially if they represent abstract values or repeat frequently in statements with lots of operators (which is often the case in shaders). It just makes code hard to read. There are also some widely accepted conventions, most of them carried over from standard math notation. The most common ones:
p - point
v - vertex, velocity
n - normal
a - acceleration
m - matrix
i - iterator, counter
x, y, z - you know those
a, b, c, d - parameters of a plane or polynomial equation
d - distance, difference (delta), diameter
t - normalized parameter, time, tangent
r - radius
Insisting on long names for everything can produce some really hard to read code.
Compare for example:
var d = -p1.x * n.x - p1.y * n.y - p1.z * n.z
With:
var fourth_plane_parameter = -plane_point_1.x * plane_normal.x - plane_point_1.y * plane_normal.y - plane_point_1.z * plane_normal.z
It's better to provide verbal explanations or code comments rather than to force long names on everything.
- Edited
Yeah I get that, I just wish people actually did that more often in working examples of code lol thanks for the glossary it has actually really helped with the legibility of the code.
xyz x, y, z - you know those
x, y, z, w - when dealing with something 4D.
- Edited
- Best Answerset by Lethn
Ok. Here's the implementation that uses a convex collider for frustum. It needs some setup but it's not much. We use convenient ReferenceRect
node for the selection rectangle. I kept the default node names for clarity. Rest of it is self evident I hope:
And here's the script attached to the area node. I commented it heavily:
extends Area3D
@export var camera: Camera3D
const near_far_margin = .1 # frustum near/far planes distance from camera near/far planes
# mouse dragging position
var mouse_down_pos: Vector2
var mouse_current_pos: Vector2
func _ready():
# initial reference rect setup
$ReferenceRect.editor_only = false
$ReferenceRect.visible = false
func _input(event):
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.is_pressed():
# initialize the rect when mouse is pressed
mouse_down_pos = event.position
mouse_current_pos = event.position
$ReferenceRect.position = event.position
$ReferenceRect.size = Vector2.ZERO
$ReferenceRect.visible = true
else:
$ReferenceRect.visible = false
# make a scelection when mouse is released
select()
if event is InputEventMouseMotion and event.button_mask & MOUSE_BUTTON_MASK_LEFT:
# set rect size when mouse is dragged
mouse_current_pos = event.position
$ReferenceRect.position.x = min(mouse_current_pos.x, mouse_down_pos.x)
$ReferenceRect.position.y = min(mouse_current_pos.y, mouse_down_pos.y)
$ReferenceRect.size = (event.position - mouse_down_pos).abs()
func select():
# get frustum mesh and assign it as a collider and assignit to the area 3d
$ReferenceRect.size.x = max(1, $ReferenceRect.size.x)
$ReferenceRect.size.y = max(1, $ReferenceRect.size.y)
$CollisionShape3D.shape = make_frustum_collision_mesh(Rect2($ReferenceRect.position, $ReferenceRect.size))
# wait for collider asignment to take effect
await get_tree().physics_frame
await get_tree().physics_frame
# actually get areas that intersest the frustum
var selection = get_overlapping_areas()
print("SELECTION: ", selection)
# YOUR CODE THAT DECIDES WHAT TO DO WITH THE SELECTION GOES HERE
# function that construct frustum mesh collider
func make_frustum_collision_mesh(rect: Rect2) -> ConvexPolygonShape3D:
# create a convex polygon collision shape
var shape = ConvexPolygonShape3D.new()
# project 4 corners of the rect to the camera near plane
var pnear = project_rect(rect, camera, camera.near + near_far_margin)
# project 4 corners of the rext to the camera far plane
var pfar = project_rect(rect, camera, camera.far - near_far_margin)
# create a frustum mesh from 8 projected points, 6 planes, 2 triangles per plane, 3 vertices per triangle
shape.points = PackedVector3Array([
# near plane
pnear[0], pnear[1], pnear[2],
pnear[1], pnear[2], pnear[3],
# far plane
pfar[2], pfar[1], pfar[0],
pfar[2], pfar[0], pfar[3],
#top plane
pnear[0], pfar[0], pfar[1],
pnear[0], pfar[1], pnear[1],
#bottom plane
pfar[2], pfar[3], pnear[3],
pfar[2], pnear[3], pnear[2],
#left plane
pnear[0], pnear[3], pfar[3],
pnear[0], pfar[3], pfar[0],
#right plane
pnear[1], pfar[1], pfar[2],
pnear[1], pfar[2], pnear[2]
])
return shape
# helper function that projects 4 rect corners into space, onto a viewing plane at z distance from the given camera
# projection is done using given camera's perspective projection settings
func project_rect(rect: Rect2, cam: Camera3D, z: float) -> PackedVector3Array:
var p = PackedVector3Array() # our projected points
p.resize(4)
p[0] = cam.project_position(rect.position, z)
p[1] = cam.project_position(rect.position + Vector2(rect.size.x, 0.0), z)
p[2] = cam.project_position(rect.position + Vector2(rect.size.x, rect.size.y), z)
p[3] = cam.project_position(rect.position + Vector2(0.0, rect.size.y), z)
return p
Any Area3D node in the scene that intersects rect/frustum will be reported. So just assign the camera to our Area3D, put some colliders into the scene and you're good to go. I don't know if this can get any simpler.
The script now uses Godot's collision system to test for intersections between frustum and area colliders. The good thing is it can handle any collision shape. There are couple of drawbacks:
- It cannot discriminate between full and partial intersection. Every slight intersection will be reported.
- You need to wait for 2 physics frames (when triggered from the event callback) for newly replaced frustum collider to become active. If anyone knows a way to force it to be immediate - let us know.
I think my job in this thread is more than done .