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.
Projecting collision shape from camera viewport
luckily there's google, first result for 'rectangle frustum selection site:godotforums.org':
https://godotforums.org/d/34220-more-information-about-using-3d-selection-boxes
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?
- Edited
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.
- Edited
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
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()
- Edited
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.
- Edited
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.
- Edited
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
- Edited
xyz I had the thought that maybe it had something to do with the frustum collider, so I tried to play with it and the first move I made "corrected," the issue, but if it's a memory leak, then that's not a solution. My intention was to just see if I could manually reset it, and although now the fps doesn't drop, I don't want memory leaks.
What if I constructed a node with new() whenever the mouse button is pressed, then did queue_free() after the mouse button is released? It's less performant than your initial solution, but maybe then the FPS won't drop.
I'd say it was likely my laptop (i5 + GTX 1650 Ti + 8GB + Debian Stable), but constructing a new() node made the FPS not drop so IDK the cause.
- Edited
Lousifr If the existence of this collider is causing slowdowns then you likely have too many other physics object in the scene. Run the profiler and identify the bottleneck. No sense in blind guessing while you have tools to measure the performance of every part of the engine.
If you want the collider to be ignored, simply disable it. Or assign null to the shape property.