Godot Version
4.4
Question
I have a Sprite3D that I want to place at random positions on the screen. How can I ensure the random position is always on the screen and not cut off?
4.4
I have a Sprite3D that I want to place at random positions on the screen. How can I ensure the random position is always on the screen and not cut off?
Are you putting this sprite on a plane that’s parallel to the screen, or is this sprite being placed randomly in three dimensions and you’re trying to constrain it to the view frustum?
Right now I am placing the sprite in 3D space, but when I resize the window my apple sprite is positioned off the screen.
func move_apple():
var viewport = get_viewport().size
var top_left = camera.project_position(Vector2(0, 0), 1.0)
var bottom_right = camera.project_position(Vector2(viewport.x, viewport.y), 1.0)
var randx = randf_range(top_left.x, bottom_right.x + 0.4)
var randy = randf_range(top_left.y - 0.2, bottom_right.y - 0.2)
position.x = randx
position.y = randy
As you move closer to the camera, the projection reduces the coordinate space that’s visible (at least, in a perspective camera). So, you can’t just constrain to the viewport if you aren’t putting the sprites on the backplane.
The really easy fix is switch to an ortho camera, but that may not be the look you’re going for.
Solving this in a perspective view is basically a similar triangles trig problem; there are several ways you could solve it. Probably the easiest would be to generate a random pair of angles constrained to the vertical and horizontal extents of the frustum. So, for example, if you have a FOV of 60°, generate an angle of +/- 30° vertical and an equivalent horizontal angular range adjusted for the aspect ratio. You can then project along those angles into the scene and as long as you don’t undershoot the front clip plane or overshoot the far plane, you have a position that’s onscreen.
That seems promising. I’m new to game development. How can I project along a random angle?
Formally, you could take a normalized vector like Vector3.FORWARD
, scale it by distance from the camera and then rotate it by each of the two angles. That would look something like:
var distvec = Vector3.FORWARD * dist_from_camera
var scrpos = distvec.rotated_by(Vector3.RIGHT random_vertical_angle)
scrpos = scrpos.rotated_by(Vector3.UP, random_horizontal_angle)
Bear in mind that the rotation angles need to be in radians, which have 2π°
in a circle rather than 360°
in a circle. There are conversion functions if you need them, though personally I found once I had some practice it was easier to think in radians.
Really, though, what you probably want to do is simpler. Rather than actually rotate a vector, you just want to get the x and y location, which we can do with simple trigonometry. For each of the two screen dimensions we have an angle and a depth into the screen. We can build a right angle triangle out of those; our angle is at the camera, the adjacent side is the depth into the scene. We need the opposite side’s length, and tan(angle) = opp/adj
:
func screen_pos(angle_x: float, angle_y: float, depth: float) -> Vector3:
var x = depth * tan(angle_x)
var y = depth * tan(angle_y)
return Vector3(x, y, depth)