Browse Source

Added tutorial for using transforms in Godot

Juan Linietsky 7 years ago
parent
commit
378584531d

BIN
tutorials/3d/img/transforms_euler.png


BIN
tutorials/3d/img/transforms_euler_himself.png


BIN
tutorials/3d/img/transforms_gizmo.png


BIN
tutorials/3d/img/transforms_interpolate1.gif


BIN
tutorials/3d/img/transforms_interpolate2.gif


BIN
tutorials/3d/img/transforms_rotate1.gif


BIN
tutorials/3d/img/transforms_rotate2.gif


+ 1 - 0
tutorials/3d/index.rst

@@ -6,6 +6,7 @@
    :name: toc-learn-features-3d
    :name: toc-learn-features-3d
 
 
    introduction_to_3d
    introduction_to_3d
+   using_transforms
    3d_performance_and_limitations
    3d_performance_and_limitations
    spatial_material
    spatial_material
    lights_and_shadows
    lights_and_shadows

+ 219 - 0
tutorials/3d/using_transforms.rst

@@ -0,0 +1,219 @@
+.. _doc_using_transforms:
+
+Using transforms for 3D games in Godot
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Introduction
+------------
+
+If you have never made 3D games before, the way to approach rotations with three dimensions can be very confusing at first.
+Coming from 2D, the natural way of thinking is along the lines of *"Oh, it's just like roating in 2D, except now rotations happen in X, Y and Z"*.
+
+At first this seems easy and, for simple games, this way of thinking may even be enough. Unfortunately, It's just very limiting and most often incorrect.
+
+Angles in three dimensions are most commonly refered to as "Euler Angles".
+
+.. image:: img/transforms_euler_angles.png
+
+Euler Angles were introduced by mathematician Leonhard Euler in the early 1700s.
+
+.. image:: img/transforms_euler_himself.png
+
+This way of representing a 3D rotation has several shortcomings when used in game development (which is to be expected from a guy with a funny hat), and
+the idea of this document is to explain why, as well as outlining best practices for dealing with transforms when programming 3D games.
+
+
+Problems of Euler Angles
+------------------------
+
+While it may seem very intuitive that each axis has a rotation, the truth is that it's just not practical.
+
+Axis Order
+==========
+
+The main reason for this is that there isn't a *unique* way to construct an orientation from the angles. There isn't a standard mathematical function that 
+takes all the angles togehter and produces an actual 3D rotation. The only way an orientation can be produced from angles is to rotate the object angle
+by angle, in an *arbitrary order*.
+
+This could be done by first rotating in *X*, then *Y* and then in *Z*. Alternatively, you could first rotate in *Y*, then in *Z* and finally in *X*. Anything really works,
+but depending on the order, the final orientation of the object will *not necesarily be the same*. Indeed, this means that there are several ways to construct an orientation
+from 3 different angles, depending on *the order the rotations happen*.
+
+Following is a visualization of rotation axes (in X,Y,Z order) in a gimbal (from Wikipedia). As it can be appreciated, the orientation of each axis depends on the rotation of the previous one:
+
+.. image:: img/transforms_gimbal.gif
+
+You may be wondering how this might affect you, though. Let's go to a practical example, then.
+
+Imagine you are working on first person controls (FPS game). Moving the mouse left and right (2D screen X axis) controls your view angle based on the ground, while moving it up and down
+makes the player head look actually up and down. 
+
+In this case, to achieve the desired effect, rotation should be applied first in *Y* axis (Up in our case, as Godot uses Y-Up), and then in *X* axis.
+
+.. image:: img/transforms_rotate1.gif
+
+If we were to simply apply rotation in *X* axis first, then in *Y*, the effect would be undesired:
+
+.. image:: img/transforms_rotate2.gif
+
+Depending on the type of game or effect desired, the order in which you want axis rotations to be applied may differ. Just accessing rotations as X,Y and Z is not enough, you need a *rotation order*.
+
+
+Interpolation
+=============
+
+Another problem of using euler angles is interpolation. Imagine you want to transition between two different camera or enemy positions (including rotations). The logical way one may
+approach is is to just interpolate the angles from one position to to the next. One would expect it to look like this:
+
+.. image:: img/transforms_interpolate1.gif
+
+
+But this does not always have the expected effect when using angles:
+
+.. image:: img/transforms_interpolate2.gif
+
+The camera actually rotated the opposite direction! 
+
+There are reasons for this to have happened:
+
+* Rotations dont linearly map to orientation, so interpolating them does not always result in the closes path (ie, to go from 270 to 0 degrees is no the same as going from 270 to 360, even though angles are equivalent).
+* Gimbal lock is at play (first and last rotated axis align, so a degree of freedom is lost).
+
+Say no to Euler Angles
+======================
+
+This means, pretty much, just **don't use** the *rotation* property of :ref:`class_Spatial` nodes in Godot for games. It's there to be used mainly fromt the editor, coherence with the 2D engine and for very simple rotations (generally just 1 axis, 2 in limited cases). As much as it tempts you, don't use it. 
+
+There is always a better way around Euler Angles for your specific problem waiting to be found by you.
+
+Introducing Transforms
+----------------------
+
+Godot uses the :ref:`class_Transform` datatype for orientations. Each :ref:`class_Spatial` node contains one of those transforms (via *transform* property), which is relative to the parent transform (in case the parent is of Spatial or derived type too).
+
+It is also possible to access the world coordinate transform (via *global_transform* property). 
+
+A transform has a :ref:`class_Basis` (transform.basis sub-property), which consists of 3 :ref:`class_Vector3` vectors (transform.basis.x to transform.basis.z). Each points to the direction where each actual axis is rotated to, so they effectively contain a roatation. The scale (as long as it's uniform) can be also be inferred from the length of the axes. A *Basis* can also be interpreted as a 3x3 matrix (used as transform.basis[x][y]).
+
+A default basis (unmodified) is Matrix3( x=Vector3(1,0,0), y=Vector3(0,1,0), z=Vector3(0,0,1) ). Each axis is pointing to their respective axis (with a vector length of 1.0, hence unscaled). This is also analog to an 3x3 identity matrix.
+
+Together with the *Basis*, a transform also has an *origin*. This is a *Vector3* specifying how far away from the actual origin (0,0,0 in xyz) this transform is. Together with the *basis*, a *Transform* efficiently represents a unique position and orientation in space.
+
+A simple way to visualize a transform is to just look at an object transform gizmo (in local mode). It will show the X, Y and Z axes of the basis as the arrows, while the origin is just the position of the object.
+
+.. image:: img/transforms_gizmo.png
+
+For more information on the mathematics of vectors and transforms, please read the :ref:`vector_math` tutorials.
+
+Manipulating Transforms
+=======================
+
+Of course, transforms are not nearly as easy to manipulate as angles and have problems of their own.
+
+It is possible to rotate a transform, as it has rotation methods.
+
+
+.. code-block:: python
+
+    # Rotate the transform in X axis
+    transform = Basis( Vector3(1,0,0), PI ) * transform
+    # Simplified
+    transform = transform.rotated( Vector3(1,0,0), PI )
+
+A method in Spatial simplifies this:
+
+.. code-block:: python
+
+    # Rotate the transform in X axis
+    rotate( Vector3(1,0,0), PI )
+    # or, just shortened 
+    rotate_x( PI )
+
+This will rotate the node relative to the parent node space. 
+To rotate relative to object space (node's own transform) the following must be done.
+
+.. code-block:: python
+    # Rotate locally, notice multiplication order is inverted
+    transform = transform * Basis( Vector3(1,0,0), PI )
+    # or, shortened
+    rotate_object_local( Vector3(1,0,0), PI )
+
+Precision Errors
+================
+
+Doing successive operations on transforms will result in a precision degradation due to floating point error. This means scale of each axis may no longer be exactly 1.0, and not exactly 90 degrees from each other.
+
+If a transform is rotated every frame, it will eventually start deforming slightly long term. This is unavoidable. 
+
+There are however, two different ways to handle this. The first is to orthonormalize the transform after a while (maybe once per frame if you modify it every frame):
+
+.. code-block:: python
+    transform = transform.orthonormalized()
+
+This will make all axes have 1.0 length again and be 90 degrees from each other. If the transform had scale, it will be lost, though. 
+
+It is recommended you don't scale nodes that are going to be manipulated, scale their children nodes instead (like MeshInstance). If you absolutely must have scale, then re-apply it in the end:
+
+.. code-block:: python
+    transform = transform.orthonormalized()
+    transform = transform.scaled( scale )
+
+
+Obtaining Information
+=====================
+
+Many are probably thinking at this point: **"Ok, but how do I get angles from a transform?"**. Answer is again, you don't. You must do your best to stop thinking in angles. 
+
+Imagine you need to shoot a bullet in the direction your player is looking towards to. Just use the forward axis (commonly Z or -Z for this).
+
+.. code-block:: python
+    bullet.transform = transform
+    bullet.speed = transform.basis.z * BULLET_SPEED
+
+So, is the enemy looking at my player? you can use dot product for this (as explained in the tutorial before)
+
+.. code-block:: python
+    if (enemy.transform.origin - player.transform.origin). dot( enemy.transform.basis.z ) > 0 ):
+	enemy.im_watching_you(player)
+
+Let's strafe left if key is pressed!
+
+.. code-block:: python
+    if (Input.is_key_pressed("strafe_left")):
+	translate( -transform.basis.x )
+
+All common behaviors and logic can be done with just vectors.
+
+Setting Information
+===================
+
+There are, of course, cases where you want to set information to a transform. Imagine a first person controller or orbiting camera. Those are definitely done using angles, because you *do want*
+the transforms to happen on a specific order.
+
+For such cases, just keep the angles and rotations *outside* the transform and set them every frame. Don't try retrieve them and re-use them because the transform is not meant to be used this way.
+
+Example of looking around, FPS style:
+
+.. code-block:: python
+    # accumulators
+    var rot_x = 0
+    var rot_y = 0
+    
+    func _input(ev):
+    	
+        if (ev is InputEventMouseMotion and ev.button_mask & 1):
+            # modify accumulated mouse rotation
+            rot_x += ev.relative.x * LOOKAROUND_SPEED
+            rot_y += ev.relative.y * LOOKAROUND_SPEED
+            transform.basis = Basis() # reset rotation
+            rotate_object_local( Vector3(0,1,0), rot_x ) # first rotate in Y
+            rotate_object_local( Vector3(1,0,0), rot_y ) # then rotate in X
+
+As you can see, in such cases it's even simpler to keep the rotation outside and use the transform as the *final* orientation.
+
+Transforms are your friend!
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Once you get used to transforms, you will appreciate their simplicity and power. Of course, for most starting with 3D games, getting used to them can take a while and it can be a bit tricky.
+Don't hesitate to ask for help in this topic in many of our online communities and, once you become confident enough, please help others!
+