In the previous tutorial, Rendering Earth in Godot 3.4, I showed how to make a simple Earth model in Blender and import it to Godot. In this post, we’ll explore how to rotate the globe, thus making our “game” interactive.
This is how the node tree and resource tree should look like (adjusted from the previous post to follow Godot’s file naming conventions).
The uneverse.tscn
file contains our “Universe” spatial node, and the
globe.glb
(imported) file contains the base class for our “Globe” spatial
node. We have also added a camera in the previous tutorial.
Adding simple mouse-click interaction
Now to add some interaction! For simplicity, we’ll keep the globe fixed at the world origin, and instead move our camera around. This will make calculations easier for now.
First, let’s add an “Area” to our “Globe” spatial. This will allow us to listen on mouse click events. Right-click the “Globe” spatial in the node tree and select Add Child Node (Ctrl+A). Locate the “Area” class:
Once we add the Area, we’ll immediately get a node configuration warning, telling us that the node has no shape and thus it can’t interact with other objects. Let’s fix that by adding a shape: add a new “CollisionShape” object as a child node of the Area, the same way we added the Area node.
Once we do that, the previous warning will go away, just to be replaced by a new warning on the CollisionShape node, telling us that there is no shape provided. To specify a shape, select the CollisionShape node and pick a shape from the dropdown for the Shape property in the Inspector tab.
Select New SphereShape to create a sphere-shaped collision object. Leave the radius at 1 unit, since that is the radius of our globe.
Now that we have a collision shape, we’ll want to connect its input signal to a script where we’ll handle it by transforming the camera. To do that, let’s create a script first by selecting the Camera node in the node tree and pressing the “Attach Script” icon, or selecting Attach Script from the context menu.
Leave the defaults, except rename the script file to camera.gd
, because,
naming conventions.
Now select the Area node, and go to the Node panel, which is the tab right
next to the Inspector panel on the right. Under Signals »
CollisionObject, double-click the input_event(…)
signal. Click
Connect to connect it to the Camera node.
Once we connect the signal to the Camera node, we’ll get an empty function in
the script that is called every time an input event occurs on the collision
object. In this function, we have a reference to our Camera node (the camera
parameter) and the position on the sphere where the mouse click happened (the
position
vector).
We can easily re-orient the camera by calling the
look_at_from_position(…)
method of the camera, which takes three arguments: the new position to move the
camera to, the position to look at, and the “up” orientation.
The new camera position can be obtained by scaling the position
vector to the
length of the vector specifying the camera origin:
Note that we use position.normalized()
to re-scale the position to a unit
vector. This wouldn’t be necessary if our collision object was a perfect
sphere, but it isn’t: like everything in our 3D game, it is secretly a
collection of triangles. To avoid small changes in the camera distance, we
normalise the position vector, which really just means we’re interested in the
orientation, but not length of the vector.
Now all that’s left is to call camera.look_at_from_position(…)
. We want to
keep looking at the globe, so we’ll pass the world origin Vector3.ZERO
as the
second argument. To keep the camera orientation so that north is always up and
south is down, we pass Vector3.UP
as the third argument.
The final script should look something like this:
Now we can hit F5 to test our game. Let’s try to cli… Oh no! What’s happening? The Earth is going nuts!
OK, time to calm down, this is progress. The crazy camera movement is because
all mouse movement is treated as input events, not just the clicks. To ignore
events other than left mouse-click, we can add an if
guard:
Re-run the game with F5 and now we can click around the globe, looking at it from various positions.
Animating camera movement
We can now look at the globe from all directions, but wouldn’t it be even nicer
if we could animate the camera movement, instead of jumping to the new position
immediately? Luckily Godot provides a simple helper to do such animations: the
Tween
node.
Go ahead and add one to the Camera node. We’ll call it tween and access it via a property in our Camera script:
To use the script, we’ll replace the camera.look_at_from_position(…)
call
with a call to tween.interpolate_method(…)
. Note that we cannot easily use
interpolate_property(…)
here because we want to change not just the position,
but also the rotation of the camera transform, while the Tween
class only
knows how to interpolate vectors.
First we move our camera transform code to its own method:
Note that we’re accessing look_at_from_position(…)
directly, without using a
reference to the Camera node. This is possible because our class extends the
Camera
class, so we can implicitly access its members, just like in Ruby or
C++ (but unlike in Python, where an explicit self.look_at_position(…)
would
be needed). This works because we attached our script to the Camera node.
Now, change the event handling code to use the tween
object:
The arguments here are:
self
is simply a reference to the current Camera node."look_from"
is the name of the method to call.camera.transform.origin
(or justtransform.origin
) is the current camera position.new_camera_position
is where we want to move the camera.0.5
is how many seconds to anemate; half a second seems good enough.Tween.TRANS_CUBIC
is the transition type. A cubic transition makes for a nice and smooth camera movement.Tween.EASE_OUT
makes the camera slow down when near the target position.
Improving camera animation
Now if we press F5 again, we’ll see that now the camera moves nicely, but there are still a couple of issues:
- The camera gets closer to the Earth as it moves, then back further again.
- As a result, clicking again during the animation progress the animation is restarted from a closer start position, allowing us to drift very close to the earth, or even get inside it with some fast clicking!
- Also, animating around the North and South poles gives for a sudden camera rotation as the camera flies past the poles.
The problems are due to the fact that the interpolated positions are along the closest line from the two endpoints. What we want instead is to keep the camera at a constant distance from the globe.
An easy way to fix that is to interpolate between two unit vectors, and using the result only as an orientation, re-scaling the result each time to keep the distance. Let’s change the input handling code again.
In fact, let’s move all that business out to yet another function, to keep the
handler code simple and clean. At the same time, we’ll change the tween
parameters to interpolate between two unit vectors, and change the look_from
method to treat its input as an orientation, not a position vector.
The final script file now looks like this:
F5 again, and yay! We now have a much smoother camera movement, that goes around the globe. It is also no longer possible to clip into the globe by clicking too quickly.
Even better camera animation
One problem still remains: when the camera moves too close to the poles, it does a very quick rotation. This is because the interpolated path between the start and end points go close to the pole where the “up” direction changes rapidly, as the camera flies by.
Let’s look at how Google Maps solves this issue. When we rotate the globe, it seems to animate the latitude and longitude in a way that we don’t actually fly by the poles. We should do this in our game as well, but interpolating two values gets more complicated. Ideally, we’d just use the interpolated orientation vector we already have, but then instead of scaling it up to the current camera distance, we’d first “normalise” it in a way where we only scale up the X and Z components. The Y (“up”) component should stay the same, so we don’t get closer to the poles than the two endpoints of the interpolated vectors.
To do that, we can imagine shearing our vector “outwards”, away from the Y axis, until the point ends up on the unit sphere (i.e. we end up with a unit vector). If \((x, y, z)\) is the current interpolated vector, and \((x_1, y_1, z_1)\) is the unit vector we’re trying to calculate at each frame, we can observe the following relations:
\[\begin{align} y_1 &= y \\ \frac x x_1 &= \frac z z_1 \\ x_1^2 + y^2 + z_1^2 &= 1 \end{align}\]We can extract for example \(z_1\) from that second equation, and use it to extract \(x_1\) from the third equation. The result is:
\[\begin{align} z_1 &= \frac {x_1} x z \\ x_1 &= \pm \sqrt {\frac {1 - y^2} {1 + \left( \frac z x \right)^2}} \end{align}\]Note that in the solution above the sign of \(x_1\) (and consequently the
sign of \(z_1\)) can be positive or negative, but for our camera motion we
want to keep the sign of all coordinates the same. The simplest way to do that
in GDScript is using the
sign
built-in.
Putting it all together, this is how the script file looks like:
Hit F5 one last time, and finally! We’ve got some nice, smooth camera movement that doesn’t go wonkers near the North and South pole.
I hope this tutorial was a nice example of how even some basic algebra can come in handy when doing game development. A future post will go into more detail about field of view, focal length and other camera settings, so stay tuned.