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.

  • xyz replied to this.

    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.

      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.

        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 😃.

          A bit off topic.
          @Lethn, you'll hate me for this but someone has to tell you. And I'll be that guy 😉. I looked at your code linked earlier and... well... you won't get far with your project doing things like this. Excessively long overexplaining names that repeat on and on aside, you treat you code merely as unstructured data storage. In coding we aim to automate and avoid repetition as much as possible. Otherwise you end up typing same things over and over. This is precisely what will happen to your project if you don't start approaching it from automation point of view. Let me give you an example. In your system you have several villager classes. In each class you define same properties. This is excessive by itself as you should use class inheritance to avoid code repetition. But that's only the start of problems. So in every villager class, amongst other things, you have:

          var isWondering = false
          var isWalking = false
          var isChillingOut = false
          var isIdle = false
          var isHoused = false
          var isSleeping = false
          var hasNothingToDo = false
          var isWorshipping = false
          var isGoingToVillageStore = false
          var isGoingToWorshipSite = false
          var goingForWood = false
          var goingForGrain = false
          var isNavMeshPathFinished = false

          Now since those are mutually exclusive, you need to have a method for each state change looking like this (which is how you handle state all over your code):

          setVilagerActivityToWalking():
          	isWondering = false
          	isWalking = true
          	isChillingOut = false
          	isIdle = false
          	isHoused = false
          	isSleeping = false
          	hasNothingToDo = false
          	isWorshipping = false
          	isGoingToVillageStore = false
          	isGoingToWorshipSite = false
          	goingForWood = false
          	goingForGrain = false
          	isNavMeshPathFinished = false

          This is horrible as you'll need hundreds of lines of code just to switch states around. And whenever you choose to add a new state to the system, you'll need to intervene in all those functions, and add new such functions on top of it, in all of your villager classes. It saddens me to say it but this is an antithesis of good code.

          You need to structure your data in a way that minimizes repetition and total amount of code: Hundreds of lines of the above code can be replaced with several lines. It doesn't only make it short but also scalable as adding a new state would require minimal intervention:

          enum Activity {IDLE, WONDERING, WALKING, CHILLING, HOUSED, SLEEPING, WORSHIPPING, GOING_TO }
          enum ActivityTarget {STORE, WOODS, WORSHIP_SITE, GRAIN, NONE}
          var activity: Activity = Activity.IDLE
          var activity_target: ActivityTarget
          
          func set_activity(new_activity: Activity, new_target: ActivityTarget = ActivityTarget.NONE) -> void:
          	activity = new_activity
          	activity_target = new_target

            xyz Believe it or not Xyz I'm not bothered at all LOL and this is something I was chatting about awhile back but I'd go too off topic here as well I had made a topic on this and one of the reasons I was looking at selection boxes was because I was taking a break from my By the gods project to have a look at stuff like enums and dictionaries which I don't know enough about. The examples I often see are far too basic for the code that I'm using so I need to poke around the internet a ton and maybe practice writing stuff up on a stand alone project. It's something I had already been discussing with people generally about getting things way more efficient precisely because of the reasons you've posted.

            At the very least, I'd still write a damn glossary on my github because not enough people do that and I want my games to be as accessible to modders as possible using the Godot engine itself because as we know mods drastically increase the lifespan of a game. I'm resting my brain at the moment but I will take a look at your other post in full, it already looks a hell of a lot better just because you commented everything like mad, I realise it might come across as silly and unnecessary but I'm coming at this from the perspective of a learner, you should definitely contribute that code to the Godot documentation because it will help a lot of people. Either this will work really well with RTS style games or party RPGs and so on, but really any kind of situation where you need a proper selection box too, it will also mean you won't have go through the tedium of dealing with extremely confused noobs constantly and they can just look this up easier.

            God this was such a rabbit hole I wasn't prepared for I should have paid more attention to peoples' warnings but I've always been a lunatic 😃 AI is easy compared to this. The only time I had this much of a headache was when I was dealing with the influence ring maths in Unity and that ended up involving several people including an ex-industry dev lol. One thing I will point out, it is a fine balance for sure between making code easier to read and keeping it as efficient as possible and that's something I'm learning a lot about these days beyond just bashing stuff out and getting it functional. You'll probably see me re-write a lot of that By the gods code by the time I'm done with all of this once I figure out the intricacies of everything without breaking stuff so don't worry about that code it's early days yet, I've been fleshing everything out and getting it functional. I was concerning myself quite a bit with things like execution timing more because that was indeed breaking stuff like my day/night cycle code.

            Thank you for all this detail of course @xyz because it has really helped even if going at things bit by bit is admittedly frustrating and I'm sure it can be frustrating having to write things out, I hope this gets added to the documentation in some way because it is definitely needed for Godot 4, looking at you Megalomaniak 😃

            Edit: In fact, what might be a good idea for me would be to make another topic on using enums and dictionaries and we can generally discuss good options for coding practices with specific gaming code because to be honest that's another aspect of coding that Godot is either lacking or has a bit of out of date information on. I will make sure to double check though because I might have made an old thread on it awhile back, could all be worth a revisit.

            There's some very good reasons that I've been exploring 3D selection boxes like this and being absolutely mad about not giving up, RTS' are a great way to stress test engines because you can have lots of agents doing stuff all at once and if you really want you can up the graphical detail to be ridiculous too. I've been working on an RTS idea for a bit of a wave defence game to satisfy a long standing craving I've had because the big studio devs especially don't bother but also this is giving me all sorts of ideas for doing things like formation based RTS' with lots and lots of agents. I'll keep it relatively simple first though and go with a classic RTS experience then work my way up. The rest of the stuff I can do myself.

            I recommend the moderators have a look at @xyz 's post and put it in as a tutorial for the documentation because selection boxes seem to be a long neglected game feature that Godot could take over on quite a bit just by having up to date how to's on it.

            Was double checking I wasn't just being daft, there's a bug that points towards your array, code works perfectly when dragging top left to bottom right but then a warning pops up when dragging bottom right to top left, the code is working great otherwise and yes once I get through the comments it is in fact easy to edit up.

            make_frustum_collision_mesh(): Too few vertices per face.

            Ah! Misinterpreted this warning I think I'm getting it now thanks to the comments, the bottom right to top left dragging is a separate issue and seems to be borked somehow, the too few vertices per faces I think has a possible fix because I've seen this problem before with the 2D selection boxes and the answer seemed to be to do an if statement and check how many pixels there are and run a standard mouse click if the size is too small.

            • xyz replied to this.

              Lethn It's not an error, it's a warning. It can be eliminated by doing boundary check on rect size so it's never (0,0):

              $ReferenceRect.size.x = max(1, $ReferenceRect.size.x)
              $ReferenceRect.size.y = max(1, $ReferenceRect.size.y)

              I left it out for brevity as it's irrelevant for the main concept demonstrated in the code. You can tune rect behavior to your liking.

              Did I mix up something in the other issue? I don't know if that's due to the camera angle or what, will triple check things to make sure.

              Edit: Nope, doesn't seem to be anything to do with the setup I don't think.

              • xyz replied to this.

                Dragging from bottom right to top left doesn't seem to work for some reason, works super accurate the other way around.

                • xyz replied to this.

                  Lethn That's because it's not implemented 😉. It's not relevant for the main concept of frustum check. Rect handling is a separate thing. I just included minimal rect functionality that's enough to show that frustum check is working, simply not to bloat the example code.
                  Here's that programmer speak again: it's trivial to add. If you want me to include that too - you'll have to buy me a beer 😉

                  There you go. I edited it in. The example code above now handles reverse rects too.

                  LOL it's annoying you did that because for once I was halfway through fixing it because I had realised that this is the same sort of code as using a 2D selection box because you've done all the maths previously and like you pointed out everything back end works with the 2D rect, just those extra two lines I didn't know about, thanks. You may find this interesting what I've done because I've already tweaked things to my liking for what I want to do with my RTS and it will give more of a 'feature complete' example for people to work from.

                  But I'm sure as hell going to quadruple check everything now.

                  • xyz replied to this.

                    Lethn Yeah it's the exact same thing as 2d selection. And it's completely separate from actual 3d selection code.

                    extends Area3D
                    
                    @export var camera: Camera3D
                    const near_far_margin = .1 # frustum near/far planes distance from camera near/far planes
                    
                    var selection = []
                    
                    # RefRect = Reference Rectangle
                    # CS = Collision Shape 3D
                    
                    @onready var RefRect = $ReferenceRect
                    @onready var CS = $CollisionShape3D
                    
                    var mouse_down_pos
                    var mouse_current_pos
                    
                    
                    
                    func _ready():
                    	# initial reference rect setup
                    	RefRect.editor_only = false
                    	RefRect.visible = false
                    	
                    	
                    func _input(event):
                    	if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
                    		if event.is_pressed():
                    			
                    			mouse_down_pos = event.position
                    			mouse_current_pos = event.position
                    			RefRect.size = Vector2.ZERO
                    			RefRect.visible = true
                    		
                    		elif has_overlapping_bodies():
                    			for selected in selection:
                    				selected.DisableSelectionRing()
                    				RefRect.visible = false
                    				select()
                    		else:
                    			RefRect.size.x = max(1, RefRect.size.x)
                    			RefRect.size.y = max(1, RefRect.size.y)
                    			RefRect.visible = false
                    			# make a selection when mouse is released
                    			select()
                    			
                    	if event is InputEventMouseMotion and event.button_mask and MOUSE_BUTTON_MASK_LEFT:
                    		mouse_current_pos = event.position
                    			
                    		RefRect.position.x = min(mouse_current_pos.x, mouse_down_pos.x)
                    		RefRect.position.y = min(mouse_current_pos.y, mouse_down_pos.y)
                    		RefRect.size = (mouse_current_pos - mouse_down_pos).abs()
                    		
                    func select():
                    	# get frustum mesh and assign it as a collider and assignit to the area 3d
                    	CS.shape = make_frustum_collision_mesh(Rect2(RefRect.position, RefRect.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
                    	selection = get_overlapping_bodies()
                    	for selected in selection:
                    		selected.EnableSelectionRing()
                    	# YOUR CODE THAT DECIDES WHAT TO DO WITH THE SELECTION GOES HERE
                    
                    # function that construct a 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 rect 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 faces in total, with 2 triangles per face and 3 vertices per triangle)
                    	
                    	shape.points = PackedVector3Array([
                    		# near face
                    		pnear[0], pnear[1], pnear[2], 
                    		pnear[1], pnear[2], pnear[3],
                    		# far face
                    		pfar[2], pfar[1], pfar[0],
                    		pfar[2], pfar[0], pfar[3],
                    		#top face
                    		pnear[0], pfar[0], pfar[1],
                    		pnear[0], pfar[1], pnear[1],
                    		#bottom face
                    		pfar[2], pfar[3], pnear[3],
                    		pfar[2], pnear[3], pnear[2],
                    		#left face
                    		pnear[0], pnear[3], pfar[3],
                    		pnear[0], pfar[3], pfar[0],
                    		#right face
                    		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 camera's perspective projection
                    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

                    Okay, this is my offering, very minor tweaking of the code, this is just to show how to use it with bodies and put them into an array so there will be no confusion. I might be inclined to tweak the behaviour some more to increase accuracy and I've got some ideas already on how to do it but I'd call this finished. Will need to experiment and get things working how I like, maybe add some extra single click functionality, will update this code when I work out a fix for myself, it just looks like a basic execution order situation.

                    I think we can finally call this thread finished hurray! Now to satisfy my RTS craving 😃 Hopefully as well this will encourage many more devs to try and take up these kinds of games.

                    xyz 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 was wondering about that. There is force_raycast_update() but apparently nothing for Areas yet; it's been requested.
                    https://github.com/godotengine/godot-proposals/issues/3111

                    We'll just have to try it and see if the 2-frame delay is acceptable for UI. Probably as long as the game is running at 60+ FPS.

                    • xyz replied to this.

                      synthnostate We'll just have to try it and see if the 2-frame delay is acceptable for UI. Probably as long as the game is running at 60+ FPS.

                      It's perfectly acceptable, just doesn't look very elegant 🙂