Unity – Draw A Debug Circle – Part 3

U

In previous part we have seen how to draw a simple debug circle in Unity project. This tutorial will extend that functionality and show how to draw circle with any rotation or axle alignment. Additionally this tutorial introduces some basic concepts about the rotation/orientation in a context of game development.

This tutorial is a part of tutorial series Unity – Draw Custom Debug Shapes

Understanding Rotation

One of many tasks most (if not all) modern game engines, including Unity, are able to handle is rotation of a body/object in three dimensions. This rotation is most often parametrized in two main ways (one common way of storing rotation is by using 4×4 matrices, but that will be covered in one of the future tutorials):

  • Euler angles,
  • Unit quaternions (versors)

Euler angles

Swiss mathematician and physicist Leonhard Euler has introduced Euler angles as a way to describe orientation of a body in a fixed coordinate space using three angles (nomenclature may differ in different environments, but usually those angles are represented by X, Y, Z or roll, pitch and yaw). Although Euler angles are more intuitive (for humans) and allow us to deconstruct orientation in the said components, they introduce (at least) two issues:

  • Ambiguity – different component values (triplets) might represent the same rotation.
  • Gimbal lock – during rotation sequences, it is possible that two of the angles might cause rotation around the same axle (aligned gimbals).

One of the advantages of Euler angles is that they can parametrize scale of the rotation. For example, [-630°, 0°, 0°] and [450°, 0°, 0°] produce the identical Quaternions, but they might be interpreted differently, since the information about the scale might be used as, for example, angular velocity of a body.

Unit Quaternion

Alternative way of parametrizing body orientation is by using unit quaternions. Quaternions in a context of game development are mainly used to perform operations on and with rotations, particularly rotations sequences (addition, subtraction) and interpolation. Quaternions are represented with four scalars x, y, z and w.

Main disadvantage of quaternions is a lack of intuitiveness from human perspective, and potentially higher memory requirements (using four scalars instead of three), as well as lack of information about the rotation magnitude.

Conversion from Euler angles [roll, pitch, yaw] to quaternion [x, y, z, w] can be performed through the following steps:

x=sin(roll/2) \times cos(pitch/2) \times cos(yaw/2) - cos(roll/2) \times sin(pitch/2) \times sin(yaw/2) \newline

y = cos(roll/2) \times sin(pitch/2) \times cos(yaw/2) + sin(roll/2) \times cos(pitch/2) \times sin(yaw/2) \newline

z = cos(roll/2) \times cos(pitch/2) \times sin(yaw/2) - sin(roll/2) \times sin(pitch/2) \times cos(yaw/2) \newline

w = cos(roll/2) \times cos(pitch/2) \times cos(yaw/2) + sin(roll/2) \times sin(pitch/2) \times sin(yaw/2) \newline

Rotating a vector

After this (bit long) intro, we can return to the main problem of this tutorial. How to rotate a location of a point (in a context of predefined coordinate system). Luckily Unity (as well as many other game engines) are overloading quaternion operators in a way to make this really convenient. More specifically, to rotate a vector it is enough to multiply rotation quaternion by it, for example, given the starting vector (v) and quaternion (q), we can calculate the rotated vector (vr) as :

vr=q \times v

To visualize this, we can use the following code where the original vector (Vector3.right) is rotated by 15 degrees and drawn as a ray (rayColor hue value is equal to the current angle):

Color rayColor;
Quaternion rotationQuaternion;

for(float i = 0; i < 360.0f; i += 15)
{
    rotationQuaternion = Quaternion.Euler(0, 0, i);
    rayColor = Color.HSVToRGB(i/360.0f, 1.0f, 1.0f);

    Debug.DrawRay(Vector3.zero, rotationQuaternion * Vector3.right, rayColor);
}

Result of the code can be seen in the following image:

With this concept in our toolset, we can continue to the main topic of this tutorial.

Rotating the Debug Circle

To extend our Debug.DrawCircle function, its signature is extended with Quaternion rotation parameter. Additionally, on each iteration of the for loop, z value of the lineStart and lineEnd needs to be reset to 0.0f, since the rotation of the vector might modify that value (lines 26 and 31). Rotation of lineStart and lineEnd points (which can be considered as vectors from the circles origin) are performed by multiplying the rotation quaternion with their respective values, and reassigning them (lines 40 and 41).

public static void DrawCircle(Vector3 position, Quaternion rotation, float radius, int segments, Color color)
{
    // If either radius or number of segments are less or equal to 0, skip drawing
    if (radius <= 0.0f || segments <= 0)
    {
        return;
    }

    // Single segment of the circle covers (360 / number of segments) degrees
    float angleStep = (360.0f / segments);

    // Result is multiplied by Mathf.Deg2Rad constant which transforms degrees to radians
    // which are required by Unity's Mathf class trigonometry methods

    angleStep *= Mathf.Deg2Rad;

    // lineStart and lineEnd variables are declared outside of the following for loop
    Vector3 lineStart = Vector3.zero;
    Vector3 lineEnd = Vector3.zero;

    for (int i = 0; i < segments; i++)
    {
        // Line start is defined as starting angle of the current segment (i)
        lineStart.x = Mathf.Cos(angleStep * i);
        lineStart.y = Mathf.Sin(angleStep * i);
        lineStart.z = 0.0f;

        // Line end is defined by the angle of the next segment (i+1)
        lineEnd.x = Mathf.Cos(angleStep * (i + 1));
        lineEnd.y = Mathf.Sin(angleStep * (i + 1));
        lineEnd.z = 0.0f;

        // Results are multiplied so they match the desired radius
        lineStart *= radius;
        lineEnd *= radius;

        // Results are multiplied by the rotation quaternion to rotate them 
        // since this operation is not commutative, result needs to be
        // reassigned, instead of using multiplication assignment operator (*=)
        lineStart = rotation * lineStart;
        lineEnd = rotation * lineEnd;

        // Results are offset by the desired position/origin 
        lineStart += position;
        lineEnd += position;

        // Points are connected using DrawLine method and using the passed color
        DrawLine(lineStart, lineEnd, color);
    }
}

To demonstrate this change, following code is used in the Example.cs script (old Update method is replaced since the Debug.DrawCircle method signature was changed, although, in one of the following tutorials we will see how this issue can be mitigated by the use of method overloading, which is one of the C#’s most useful features):

void Update()
{
    Color rayColor;
    Quaternion rotationQuaternion;

    for(float i = 0; i < 360.0f; i += 30)
    {
        rotationQuaternion = Quaternion.Euler(0, i * 0.5f, 0);
        rayColor = Color.HSVToRGB(i/360.0f, 1.0f, 1.0f);

        Debug.DrawCircle(transform.position, rotationQuaternion, 1.0f, 32, rayColor);
    }
}

The result of the example code can be seen in the following image (note that the camera was rotated by 45 degrees over the x-axis, to increase clarity on what is happening). As in the previous example, color hue is matching the rotation angle:

Even better example, how this extended Debug.DrawCircle function can be used is if we pass Example GameObject rotation as the rotation parameter. What way circle rotation will describe the rotation of (in this case invisible) GameObject, which can greatly help us with debugging (as this rotation is currently directionally ambiguous, in one of the future tutorials, we will see how to include a normal ray which will inform us about the “forward” direction of the object).

void Update()
{
    Debug.DrawCircle(transform.position, transform.rotation, 1.0f, 32, Color.magenta);
}

Now, position and orientation of the circle matches the position and orientation of the Example GameObject, as can be seen in the following images:

With this, main topic of the tutorial is finished. The following part covers how to modify this functionality to draw arcs, segments, sectors, and… pizzas!

About the author

David Zulic
By David Zulic

David Zulic

Get in touch

Quickly communicate covalent niche markets for maintainable sources. Collaboratively harness resource sucking experiences whereas cost effective meta-services.