I'd like to have a collider for an Area3D that projects from the camera to capture all visible, selectable units, to be placed in a dictionary. It looks like I can use a CollisionPolygon3D to create the shape programmatically. How would I calculate the position of the vertices according to where a ray cast shot from each corner of the viewport lands?

  • xyz replied to this.

    How far you want to project? If the len is defined then just create the shape yourself in blender or something and add it to camera so that it rotates with the camera.

    Lousifr You need an arbitrary concave polygon shape? Typically a rectangle is enough for most games.

      xyz I guess they are really asking how to transform it from camera to world space, probably. I also have a sense of deja vu where I'm pretty sure you've provided the answer to this question before... and I might have tried to prod you to contribute the answer into the docs too. Or something. Eh, probably just an odd fever dream.

      • xyz replied to this.

        Megalomaniak Yeah I think I remember it. It was a classis rectangle/frustum 3d selection example, with and without Godot's physics engine involvement. I actually looked for a good place in the docs to cram this into, but in the end couldn't decide where it should go and then forgot about it.
        I cant find the thread now though. This forum's search capabilities could use some improvements.

          Megalomaniak luckily there's google

          I'll have to remember that 🙂

          I tried searching the forum with google on several occasions before but still couldn't find things I was looking for. Although truth be told, my patience with google is very limited these days. The web altogether seems to be turning into mush. But I digress...

            xyz I actually looked for a good place in the docs to cram this into, but in the end couldn't decide where it should go and then forgot about it.

            I believe your solutions in general should be put together in one place as an example. It will of course compete with GDQuest. But competition is a good thing?

            Megalomaniak the code provided in that thread handles everything I wanted and more, and seems cleaner than my methodology. Thanks, @xyz, for being so willing to help noobs make games.

              xyz One issue with the code, it works, however, after I drag a selection box, the FPS will tank and not recover. Also the fans on my laptop will fire up.
              I'm using your code verbatim. I tried to mash it all into a spoiler so it isn't taking up space in this post, but that wasn't working, so sorry.

              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
              • xyz replied to this.

                Lousifr Looks like fps starts dropping after you release the mouse, so the problem can't be in the code you posted

                Maybe it has to do with the mesh generation code, because I noticed that the StaticBody3D nor the CollisionShape3D show up in the debug console:

                	# 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
                func generate_mesh(i: int):  
                	mesh = ArrayMesh.new()
                	var plane = PlaneMesh.new()
                	plane.size = chunk_size
                	plane.subdivide_width = chunk_size.x
                	plane.subdivide_depth = chunk_size.y
                	mesh.clear_surfaces()
                	mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, plane.get_mesh_arrays())
                
                	md.create_from_surface(mesh, 0)
                	md.set_material(preload("res://Shaders/terrain_shader_material.tres"))
                	for v in md.get_vertex_count():
                		var cyl_coord = cylinder_uv_to_xyz(md.get_vertex_uv(v) * (Vector2(1.0 / num_chunks, 1.0) * uv_scale) + Vector2(i * (1.0 / num_chunks), 0.0))
                		var plains = plain_noise.get_noise_3dv(cyl_coord) * (plain_scale)
                		var oceans = ocean_noise.get_noise_3dv(cyl_coord) * (ocean_scale)
                		var mountains = mountain_noise.get_noise_3dv(cyl_coord) * (mountain_scale)
                		var elevation = plains - (oceans * 1.5)
                		elevation = elevation + (mountains - elevation) * (mountains * 0.5)
                		if elevation < 0.0:
                			elevation *= 1.5
                		md.set_vertex(v, md.get_vertex(v) + Vector3(0.0, elevation, 0.0))
                	mesh.clear_surfaces()
                	md.commit_to_surface(mesh)
                	st.create_from(mesh, 0)
                	st.generate_normals()
                	mesh = st.commit()
                	collision_shape.shape = mesh.create_trimesh_shape()
                • xyz replied to this.

                  Lousifr Likely. Watch out not to re-generate meshes each frame. Put some print statements into all generating functions to see when they get called. You should get into habit of planting prints all over your code when something fishy starts to happen. It's the first line of defense against bugs.

                    xyz I commented that code out because it was taxing my system, and I found variables that work with what I want in terms of the mesh generation, so no need for mesh regeneration every frame. Also, the code uses get_overlapping_areas(), not get_overlapping_bodies(), so the StaticBody3D wouldn't show up anyway. When I add an Area3D to the building, it does show up in the debug console. Honestly, I might dig up the HexMap I created using C++ last year, convert it back to GDScript and save the mesh generation code in a new project for later. The only issue being the fact that I iterate through all hexes every frame, which I could probably solve with an if hex_distance < map_size / 2: return at the top of the function. The only thing being I'd constantly be reparenting units to different hexes in order to have proper unit placement despite hex location.

                    • xyz replied to this.

                      Lousifr I think you just need to work a bit on your debugging skills 😃 I suggested to re-generate mesh every frame in a simple separate project just so you can observe in real time what happens with sampled texture as you change the uv and tiling parameters. In your actual game each mesh should be generated only once.

                      So check that you don't accidentally do something performance intensive (like mesh generation) every frame. Print statements are best for detecting that. Put a print in each function that does some heavy job. It you get endless printout - it means that it gets called every frame. Easy peasy.

                        xyz I went through most of the functions I created and they're not being called unless necessary, no mass output to the console except for in generate_mesh(), but that's because it's going through each vertex at creation, but when I placed the print() after the displacement of vertices, the map is only generating once.

                        SOLVED: It seems to have been an issue where the CollisionShape3D for the selection wasn't being reset.

                        func select():
                        	...
                        	# actually get areas that intersest the frustum
                        	var selection = get_overlapping_areas()
                        	print("SELECTION: ", selection)
                        	$CollisionShape3D.shape = CollisionShape3D.new()
                        	# YOUR CODE THAT DECIDES WHAT TO DO WITH THE SELECTION GOES HERE
                        • xyz replied to this.

                          Lousifr $CollisionShape3D.shape = CollisionShape3D.new()

                          Would that even work? Even if it would, it'd cause a memory leak. CollisionShape3D is a node type and you're trying to assign it to a property that expect Shape3D resource type.

                            xyz It worked to stop the drop in FPS, but I see what you're saying. I'm away from my computer for a few hours, but I'll work on it when I get back.

                            xyz This should be safe, no?

                            $CollisionShape3D.shape = CollisionShape3D.new().shape
                            • xyz replied to this.