C# Archives - dev-tut.com https://dev-tut.com/tag/c-sharp/ Bite-sized software development tutorials Sat, 16 Apr 2022 15:02:15 +0000 en-US hourly 1 https://wordpress.org/?v=6.5.3 https://i0.wp.com/dev-tut.com/wp-content/uploads/2022/01/cropped-fav.png?fit=32%2C32&ssl=1 C# Archives - dev-tut.com https://dev-tut.com/tag/c-sharp/ 32 32 201541480 Unity – Draw A Debug Cylinder and Capsule https://dev-tut.com/2022/unity-draw-a-debug-cylinder-and-capsule/ Sat, 16 Apr 2022 14:53:23 +0000 https://dev-tut.com/?p=849 This tutorial is the last part of the Unity – Draw Custom Debug Shapes series. In this part we will cover two more shapes that are commonly used for debugging in game development projects. Since some of the concepts are already explained in the previous parts, I suggest you to go over those as well. […]

The post Unity – Draw A Debug Cylinder and Capsule appeared first on dev-tut.com.

]]>
This tutorial is the last part of the Unity – Draw Custom Debug Shapes series. In this part we will cover two more shapes that are commonly used for debugging in game development projects. Since some of the concepts are already explained in the previous parts, I suggest you to go over those as well.

Cylinder

Cylinder can be defined as a prism with circle as it’s base. In their most basic form cylinders can be defined by the radius of their base and their height. In the following image you can see a cylinder defined by two circles (base circle originating in point A, and top circle originating in point B), with radius r and height AB.

In the context of drawing a debug cylinder shape we can replace the tube connecting two circles with multiple lines. Traditionally this is done using four lines connecting points at 0°, 90°, 180° and 270° along each circle:

In the context of game development, position of the circles can be defined in two way:

a) Base circle is positioned at the desired location, and a top circle is positioned height units away, parallel to the base circle, or

b) Both base and top circles are 1/2 height units away from the desired location (center) but in the opposite directions.

Traditionally, in a context of game development, cylinders are used to visualize location and orientation of AI agents, but, of course they can be used in any number of ways.

Drawing a cylinder

To draw a debug cylinder for its base we need to do the following steps:

  • Draw a base circle,
  • Draw a top circle parallel to the base circle and height units away along the Y-axis,
  • Define connecting points along the top and base circle and draw lines between the parallel pairs (A to A’ etc.), perpendicular to both circles.

Drawing a circle is covered in detail through a four-part tutorial series, so if you haven’t seen it so far I strongly suggest visiting those first.

As a first step we need to define which parameters define a cylinder, as those will need to be passed by the user of the drawing method. Those are:

  • position,
  • orientation,
  • height,
  • radius and
  • drawing color

Additionally, we need one more parameter, namely drawFromBase boolean, which we can set to default to true (of course this can be changed to fit the needs of the project in which the method would be used).

Therefore our DrawCylinder method’s signature will look like this:

public static void DrawCylinder(Vector3 position, Quaternion orientation, float height, float radius, Color color, bool drawFromBase = true)

As a first step, we can draw the two parallel circles with the given radius, offset and color. Since the circles need to be draw in the XY plane, we need to pass that information to our DrawCircle method as orientation. For now we can define that orientation using Euler angles (90°, 0, 0).

public static void DrawCylinder(Vector3 position, Quaternion orientation, float height, float radius, Color color, bool drawFromBase = true)
{
    Vector3 basePosition = position;
    Vector3 topPosition = basePosition + Vector3.up * height;
        
    Quaternion circleOrientation = Quaternion.Euler(90, 0, 0);

    DrawCircle(basePosition, circleOrientation, radius, color, 32);
    DrawCircle(topPosition, circleOrientation, radius, color, 32);
}

While copying position parameter in an additional variable might seem wasteful at the moment, it is just a groundwork for the following steps, when we get to drawing a cylinder from a central position. In any case, calling this method from our Example.cs Update method (covered in Part 1 of the Draw a Debug Circle tutorial), we get the following:

void Update()
{
    Debug.DrawCylinder(transform.position, transform.rotation, 2.0f, 0.5f, Color.green, true);
}

Next, we need to define connecting points and lines (in this case rays) originating at those points. Based on the image below, we can define these points as following (r = circle radius):

A = [r, 0, 0]
B =  [0, 0, r]
C = [-r, 0, 0]
D = [0, 0, -r]

Unity already provides us with predefined vectors Vector3.forward and Vector3.right, so we can use those as well (it will also come handy when we introduce orientation parameter). In that sense, these points can be defined as following:

Vector3 pointA = basePosition + Vector3.right * radius;
Vector3 pointB = basePosition + Vector3.forward * radius;
Vector3 pointC = basePosition - Vector3.right * radius;
Vector3 pointD = basePosition - Vector3.forward * radius;

From those points, we can draw rays towards the top circle with the length equal to the cylinder’s height:

public static void DrawCylinder(Vector3 position, Quaternion orientation, float height, float radius, Color color, bool drawFromBase = true)
{
    Vector3 basePosition = position;
    Vector3 topPosition = basePosition + Vector3.up * height;
        
    Quaternion circleOrientation = Quaternion.Euler(90, 0, 0);

    Vector3 pointA = basePosition + Vector3.right * radius;
    Vector3 pointB = basePosition + Vector3.forward * radius;
    Vector3 pointC = basePosition - Vector3.right * radius;
    Vector3 pointD = basePosition - Vector3.forward * radius;

    DrawRay(pointA, Vector3.up * height, color);
    DrawRay(pointB, Vector3.up * height, color);
    DrawRay(pointC, Vector3.up * height, color);
    DrawRay(pointD, Vector3.up * height, color);

    DrawCircle(basePosition, circleOrientation, radius, color, 32);
    DrawCircle(topPosition, circleOrientation, radius, color, 32);
}

By calling this updated version of DrawCylinder method in our Example.cs Update, we get the following result (camera has been offset for better visibility):

The next factor we need to introduce is orientation. This topic is covered in detail in Draw A Circle – Part 3 tutorial, so we will just leverage those concepts to modify our directional vectors and use those modified versions for drawing the cylinder. What we need to do is multiply the orientation quaternion with each direction vector (right, forward and up) to get the local-space versions and simply pass them to the drawing methods. Also, base orientation needs to be multiplied with the provided orientation parameter.

public static void DrawCylinder(Vector3 position, Quaternion orientation, float height, float radius, Color color, bool drawFromBase = true)
{
    Vector3 localUp = orientation * Vector3.up;
    Vector3 localRight = orientation * Vector3.right;
    Vector3 localForward = orientation * Vector3.forward;

    Vector3 basePosition = position;
    Vector3 topPosition = basePosition + localUp * height;
        
    Quaternion circleOrientation = orientation * Quaternion.Euler(90, 0, 0);

    Vector3 pointA = basePosition + localRight * radius;
    Vector3 pointB = basePosition + localForward * radius;
    Vector3 pointC = basePosition - localRight * radius;
    Vector3 pointD = basePosition - localForward * radius;

    DrawRay(pointA, localUp * height, color);
    DrawRay(pointB, localUp * height, color);
    DrawRay(pointC, localUp * height, color);
    DrawRay(pointD, localUp * height, color);

    DrawCircle(basePosition, circleOrientation, radius, color, 32);
    DrawCircle(topPosition, circleOrientation, radius, color, 32);
}

In the following image you can see a few cylinders drawing using arbitrary orientations and positions (cylinders have the same size, but they appear different due to the camera settings):

The last step we need to do is to introduce the bDrawFromBase flag. To make it clearer, the following image shows two cylinders, one drawn from its base (green), and the second one drawn from its center (red). This position is also visualized using a tiny sphere:

To implement this, the only thing we need to modify is basePosition (hence the copy of the passed position). To do this in a clean manner, we can use ternary or conditional operator (? 🙂. Ternary operator evaluates a condition and returns the result of one of the two expressions that follow:

x = condition ? expressionA : expressionB

In this case, if the condition is met, result of expressionA will be assigned to x. Otherwise, if the condition is not met, result of the expressionB will be assigned.

In our example, we can use ternary operator to modify base position using the bDrawFromBase parameter as a condition. If the bDrawFrom base is true, offset will be zero, and if it is false (meaning that we want to draw from a center), the half-height units offset along the local up-axis will be added:

Vector3 basePositionOffset = drawFromBase ? Vector3.zero : (localUp * height * 0.5f);
Vector3 basePosition = position - basePositionOffset;

Below you can see the final version of our DrawCylinderMethod:


public static void DrawCylinder(Vector3 position, Quaternion orientation, float height, float radius, Color color, bool drawFromBase = true)
{
    Vector3 localUp = orientation * Vector3.up;
    Vector3 localRight = orientation * Vector3.right;
    Vector3 localForward = orientation * Vector3.forward;

    Vector3 basePositionOffset = drawFromBase ? Vector3.zero : (localUp * height * 0.5f);
    Vector3 basePosition = position - basePositionOffset;
    Vector3 topPosition = basePosition + localUp * height;
        
    Quaternion circleOrientation = orientation * Quaternion.Euler(90, 0, 0);

    Vector3 pointA = basePosition + localRight * radius;
    Vector3 pointB = basePosition + localForward * radius;
    Vector3 pointC = basePosition - localRight * radius;
    Vector3 pointD = basePosition - localForward * radius;

    DrawRay(pointA, localUp * height, color);
    DrawRay(pointB, localUp * height, color);
    DrawRay(pointC, localUp * height, color);
    DrawRay(pointD, localUp * height, color);

    DrawCircle(basePosition, circleOrientation, radius, color, 32);
    DrawCircle(topPosition, circleOrientation, radius, color, 32);
}

Drawing A Capsule

To draw a capsule we need to make just a few additions to the DrawCylinder method. We can imagine that the capsule is a cylinder with two hemispheres at the base and the top. Since we already have base and top circles, we need to add two arcs to each to visualize the hemispheres. We have already covered how to draw arcs in one of the previous tutorials, so we will simply utilize that knowledge.

One important thing to figure out is how the height of a capsule and its radius influences the height of the cylinder that is used as its base. The following image displays that relationship (for clarity it is done using a 2D projection of the said shapes):

It is possible to see that the height of the capsules “core” cylinder (red) is two radii smaller than the height of the comparable cylinder (blue). It is important to note that the total height should be at least twice as large as the radius (in which case we will get a sphere). With all that in mind we can define the following steps required to draw a capsule from its base (note that the arcs are using the same orientation like the base circles of the cylinder):

  • Draw a base hemisphere using two arcs with radius-units offset along the up-axis from the base,
  • Draw a cylinder using the same base, but with the height which is two radii lesser than the total capsule height,
  • Draw a top hemisphere cylinder-height offset along the up-axis from the cylinder/base arc base:
public static void DrawCapsule(Vector3 position, Quaternion orientation, float height, float radius, Color color, bool drawFromBase = true)
{
    // Clamp the radius to a half of the capsule's height
    radius = Mathf.Clamp(radius, 0, height * 0.5f);
    Vector3 localUp = orientation * Vector3.up;
    Quaternion arcOrientation = orientation * Quaternion.Euler(0, 90, 0);

    Vector3 baseArcPosition = position + localUp * radius;
    DrawArc(180, 360, baseArcPosition, orientation, radius, color);
    DrawArc(180, 360, baseArcPosition, arcOrientation, radius, color);

    float cylinderHeight = height - radius * 2.0f;
    DrawCylinder(baseArcPosition, orientation, cylinderHeight, radius, color, true);

    Vector3 topArcPosition = baseArcPosition + localUp * cylinderHeight;
        
    DrawArc(0, 180, topArcPosition, orientation, radius, color);
    DrawArc(0, 180, topArcPosition, arcOrientation, radius, color);
}

Please notice that the “core” cylinder is drawn with bDrawFromBase parameter hardcoded to true. This will be important in the later steps.

Calling this new method in our Example class’s Update, we get the following. In addition to the capsule, a green cylinder is drawn for comparison (note that the green cylinder has a slightly larger radius for clarity).:

The last factor we need to include is the bDrawFromBase flag. Like with the cylinder, it is sufficient just to offset the base position by that offset, and everything else will fall in place (this is where the hardcoded value mentioned earlier comes in play, since otherwise the offset would be added twice when drawing the core cylinder, fine the bDrawFromBase is set to false):

public static void DrawCapsule(Vector3 position, Quaternion orientation, float height, float radius, Color color, bool drawFromBase = true)
{
    // Clamp the radius to a half of the capsule's height
    radius = Mathf.Clamp(radius, 0, height * 0.5f);
    Vector3 localUp = orientation * Vector3.up;
    Quaternion arcOrientation = orientation * Quaternion.Euler(0, 90, 0);

    Vector3 basePositionOffset = drawFromBase ? Vector3.zero : (localUp * height * 0.5f);
    Vector3 baseArcPosition = position + localUp * radius - basePositionOffset;
    DrawArc(180, 360, baseArcPosition, orientation, radius, color);
    DrawArc(180, 360, baseArcPosition, arcOrientation, radius, color);

    float cylinderHeight = height - radius * 2.0f;
    DrawCylinder(baseArcPosition, orientation, cylinderHeight, radius, color, true);

    Vector3 topArcPosition = baseArcPosition + localUp * cylinderHeight;
        
    DrawArc(0, 180, topArcPosition, orientation, radius, color);
    DrawArc(0, 180, topArcPosition, arcOrientation, radius, color);
}

With this modification we can set the bDrawFromBase parameter to false and get the following result:

With this step we have concluded this tutorial and with it the whole Unity – Draw custom Debug Shapes series. Next tutorial series will focus more on some basic gameplay programming concepts that can be applied in a wide range of game development project. Stay tuned!

The post Unity – Draw A Debug Cylinder and Capsule appeared first on dev-tut.com.

]]>
849
Unity – Draw A Debug Cube https://dev-tut.com/2022/unity-draw-a-debug-cube/ Sat, 02 Apr 2022 15:10:36 +0000 https://dev-tut.com/?p=831 Like the previous tutorial (Unity – Draw a Debug Sphere), this one continues expanding our understanding of drawing 3D debug shapes. This tutorial will cover how to draw debug cubes and rectangular cuboids using a few different approaches, while still keeping the code clean and easy to understand. Cubes are often seen in, and associated […]

The post Unity – Draw A Debug Cube appeared first on dev-tut.com.

]]>
Like the previous tutorial (Unity – Draw a Debug Sphere), this one continues expanding our understanding of drawing 3D debug shapes. This tutorial will cover how to draw debug cubes and rectangular cuboids using a few different approaches, while still keeping the code clean and easy to understand. Cubes are often seen in, and associated with, games and 3D graphics, so it only makes sense to have a way of debug-drawing them during the development of a game. Typically, cubes and rectangular cuboids (boxes) are used to describe certain volumes of influence or importance.

Drawing a Cube

Cube is a regular hexahedron (a polyhedron with six faces), whose faces are squares. By definition, like any cuboid, a cube is defined by three pairs of parallel faces. Those faces share certain edges, and therefore for drawing a debug cube, we can do a slight optimization, and instead of drawing 6 individual squares, we can draw two (ABCD and EFGH), and connect them with four lines/edges (AE, BF, CG, DH) as shown in the image below:

Since we have already covered how to draw a rectangle/square in one of the previous tutorials, we can start drawing a box by calling the DrawRectangle method twice, with an offset along the z-axis. This offset should be added to the origin of the rectangle. If we label the length of one of edges as L, we can say that the blue rectangle is -L/2 units away from the origin along the z-axis, and that the red rectangle is L/2 units away along the z-axis from the graph’s origin. In Unity, to offset something along the z axis, we can use the built-in Vector3.forward vector. Since DrawRectangle method requires a rotation parameter, for now we can simply use Quaternion.identity. In the later steps, like with the previous tutorials, we will introduce the orientation.

public static void DrawCube(Vector3 position, float size)
{
    Vector3 offsetZ = Vector3.forward * size * 0.5f;

    DrawRectangle(position - offsetZ, Quaternion.identity, Vector2.one * size, Color.red);
    DrawRectangle(position + offsetZ, Quaternion.identity, Vector2.one * size, Color.blue);
}

By calling this function in our Example class Update method, we get the following (for clarity, a tiny debug sphere is added at the origin):

void Update()
{
    Debug.DrawCube(transform.position, 1.0f);
    Debug.DrawSphere(transform.position, transform.rotation, 0.05f, Color.green, 4);
}

Next, we need to connect the two rectangles, as shown in the first image. To do so, we need to calculate where the points A-H are located, and use Debug.DrawLine method to draw lines between them. To do so, first we need to add two more offset values, offset along the x-axis and offset along the y-axis. Using different combinations of those offsets we can get the points A-H. Since the points of one pair (for example, A and E) only differ in the z-axis offset, we can shorten the code by defining only the initial four points (A-D) with z-axis set to zero, and offsetting them while drawing (calling the Debug.DrawLine method):

public static void DrawCube(Vector3 position, float size)
{
    Vector3 offsetX = Vector3.right * size * 0.5f;
    Vector3 offsetY = Vector3.up * size * 0.5f;
    Vector3 offsetZ = Vector3.forward * size * 0.5f;

    Vector3 pointA = -offsetX + offsetY;
    Vector3 pointB = offsetX + offsetY;
    Vector3 pointC = offsetX - offsetY;
    Vector3 pointD = -offsetX - offsetY;

    DrawRectangle(position - offsetZ, Quaternion.identity, Vector2.one * size, Color.red);
    DrawRectangle(position + offsetZ, Quaternion.identity, Vector2.one * size, Color.blue);

    DrawLine(pointA - offsetZ, pointA + offsetZ, Color.green);
    DrawLine(pointB - offsetZ, pointB + offsetZ, Color.green);
    DrawLine(pointC - offsetZ, pointC + offsetZ, Color.green);
    DrawLine(pointD - offsetZ, pointD + offsetZ, Color.green);
}

This extension gives us the following result:

Like with the previous tutorials, we can introduce the orientation. Specifics on how vector rotation using quaternions is handled is already explained in more detail in Draw a Debug Circle – Part 3 tutorial, so I would suggest you to check it if you want to gain a bit deeper understanding. To avoid repetition, we can simply utilize it in our DrawCube method. Also, we can already introduce the color parameter to our method, so that the cube is drawn using a specific color:

public static void DrawCube(Vector3 position, Quaternion orientation, float size, Color color)
{
    Vector3 offsetX = orientation * Vector3.right * size * 0.5f;
    Vector3 offsetY = orientation * Vector3.up * size * 0.5f;
    Vector3 offsetZ = orientation * Vector3.forward * size * 0.5f;

    Vector3 pointA = -offsetX + offsetY;
    Vector3 pointB = offsetX + offsetY;
    Vector3 pointC = offsetX - offsetY;
    Vector3 pointD = -offsetX - offsetY;

    DrawRectangle(position - offsetZ, orientation, Vector2.one * size, color);
    DrawRectangle(position + offsetZ, orientation, Vector2.one * size, color);

    DrawLine(pointA - offsetZ, pointA + offsetZ, color);
    DrawLine(pointB - offsetZ, pointB + offsetZ, color);
    DrawLine(pointC - offsetZ, pointC + offsetZ, color);
    DrawLine(pointD - offsetZ, pointD + offsetZ, color);
}

We can call this updated version of the DrawCube method in our Example class’ Update method:

void Update()
{
    Debug.DrawCube(transform.position, transform.rotation, 1.0f, Color.cyan);
}

Drawing a Rectangular Cuboid

To draw a rectangular cuboid (for simplicity, I will continue by using the term “box” instead), we simply need to extend the DrawCube method by introducing the size as a 3D vector instead of a scalar. To do so, we can create a copy of the method, and change its signature as shown below. Each offset needs to be multiplied with corresponding dimension of the new size vector, and also, two rectangles need to be drawn with the x and y dimensions of the new size vector instead of using Vector2.one multiplied by the size:

public static void DrawBox(Vector3 position, Quaternion orientation, Vector3 size, Color color)
{
    Vector3 offsetX = orientation * Vector3.right * size.x * 0.5f;
    Vector3 offsetY = orientation * Vector3.up * size.y * 0.5f;
    Vector3 offsetZ = orientation * Vector3.forward * size.z * 0.5f;

    Vector3 pointA = -offsetX + offsetY;
    Vector3 pointB = offsetX + offsetY;
    Vector3 pointC = offsetX - offsetY;
    Vector3 pointD = -offsetX - offsetY;

    DrawRectangle(position - offsetZ, orientation, new Vector2(size.x, size.y), color);
    DrawRectangle(position + offsetZ, orientation, new Vector2(size.x, size.y), color);

    DrawLine(pointA - offsetZ, pointA + offsetZ, color);
    DrawLine(pointB - offsetZ, pointB + offsetZ, color);
    DrawLine(pointC - offsetZ, pointC + offsetZ, color);
    DrawLine(pointD - offsetZ, pointD + offsetZ, color);
}

By calling this function in our Example class Update method, with size vector set to [3, 2, 1] we get the following result:

void Update()
{
    Debug.DrawBox(transform.position, transform.rotation, new Vector3(3.0f, 2.0f, 1.0f), Color.cyan);
}

Wrap-up

To finalize this tutorial, we can do one simple cleanup of the code (to, again enforce the DRY principle). Namely, since the cube is technically a rectangular cuboid with equally sized sides, we can simply call the DrawBox method from DrawCube method and pass the size as a unit-sized vector (Vector3.one) multiplied by the size parameter:

public static void DrawCube(Vector3 position, Quaternion orientation, float size, Color color)
{
    DrawBox(position, orientation, Vector3.one * size, color);
}

Calling both DrawBox (using the size [3, 2, 1]) and DrawCube (using the size 0.5) in our Example class, we get the following result:

void Update()
{
    Debug.DrawBox(transform.position, transform.rotation, new Vector3(3.0f, 2.0f, 1.0f), Color.cyan);
    Debug.DrawCube(transform.position, transform.rotation, 0.5f, Color.magenta);
}

With this implemented, we have concluded this tutorial. The following tutorial cover how to draw debug cylinders and capsules, which are commonly used to visualize characters and their colliders in game development project, as well as AI agents used in 3D space, and each of them can be extremely useful and common in game development.

The post Unity – Draw A Debug Cube appeared first on dev-tut.com.

]]>
831
Unity – Draw A Debug Sphere https://dev-tut.com/2022/unity-draw-a-debug-sphere/ Sun, 20 Mar 2022 23:25:14 +0000 https://dev-tut.com/?p=808 Through the previous tutorials, we have covered how to draw 2D shapes in 3D space. Now, we are encroaching into the realm of 3D shapes. First shape (and potentially the most useful) is a sphere. In a context of game development, a number of use-cases is almost unlimited, including explosion radius, hearing radius of some […]

The post Unity – Draw A Debug Sphere appeared first on dev-tut.com.

]]>
Through the previous tutorials, we have covered how to draw 2D shapes in 3D space. Now, we are encroaching into the realm of 3D shapes. First shape (and potentially the most useful) is a sphere. In a context of game development, a number of use-cases is almost unlimited, including explosion radius, hearing radius of some AI agent, “safe” area, etc.

Sphere Approximation

As with the debug circle tutorial, for “technical sanity” reasons, we will not draw a near-perfect sphere, but rather a set of polygons used to approximate its shape. The end result of the effort is show in the image below (including the number of segments underneath each sphere):

As you can notice, each sphere is composed from a set number of circles. We can use geographical terms to describe them, namely vertical circles as “meridians” (technically pair of meridians since in geographical terms, meridian is an arc that connect two poles, not a full circle) and horizontal circles as “parallels”. Number of segments mentioned above is actually a number of “meridians”. As a first step we can handle exactly that, drawing the appropriate number of vertical circles to approximate the sphere.

Drawing Meridians

To draw the meridian we simply need to draw multiple circles rotated around the vertical axis. Number of meridians is equal to the number of segments provided by the user. Based on these two statements we can conclude that a meridian need to be drawn for each segment, and every 180 degrees divided by the number of segments. Since we have covered the rotation in 3D space in the Draw Debug Circle – Part 3 tutorial, the new code should be relatively simple.

First we need to calculate a “meridian step”, this value will tell us how much each new circle needs to rotate around the vertical axis in comparison to the previous one. As mentioned above this value is equal to 180 degrees divided by the number of segments.

meridianStep = {180° \over segments}

Next, we need to iterate through the number of steps, and draw a circle during the each step, which is “meridian step” rotated from the previous one. We can achieve this by multiplying the “meridian step” by the current step number. For convenience, we will immediately include the orientation and radius parameters in the DrawSphere method, but those will be covered a bit later. One more thing to note is that, while this method is drawing multiple debug circles, number of segments needed for each circle is actually double the number of segments of the sphere. Therefore a value called doubleSegments is introduced:

public static void DrawSphere(Vector3 position, Quaternion orientation, float radius, Color color, int segments = 4)
{
    if(segments < 2)
    {
        segments = 2;
    }

    int doubleSegments = segments * 2;
    float meridianStep = 180.0f / segments;

    for(int i = 0; i < segments; i++)
    {
        DrawCircle(position, Quaternion.Euler(0, meridianStep * i, 0), radius, color, doubleSegments);
    }
}

We can use this method in our Example.cs class Update method (described in more detail in one of the previous tutorials). For convenience I have made multiple copies of the Example GameObject, so we can test multiple variations of the sphere (which differ in location and a number of segments):

public int segments = 4;
    
void Update()
{
    Debug.DrawSphere(transform.position, transform.rotation, 1, Color.green, segments);
}

By making the segments value public, we are able to change in from the Editor UI, so we don’t need to hardcode it. The result of this code can be seen in the following image (number of segments goes from 2 to 10):

Drawing Parallels

Drawing the sphere parallels is a bit more difficult, since we will need to consider the following:

  • They are not equally distributed along the vertical axis,
  • Radius of each parallel is different from the previous one,
  • Their size grows from 0 at the spheres south pole, to 1 at the equator, back to 0 at the north pole.

We can start with the vertical distribution first. Taking a look at this 16-segment polygon that we use for approximating a circle, we can draw lines where the parallels should be:

If we project these lines on a Y axis of a XY coordinate system, and increment the X axis value in regular steps between 0 and 1, and connect those new points, we get the following:

This shape might look familiar to some. It is actually a cosine the angle between the circle origin (O) and any of the points of the polygon (A-Q), with E being considered as 0° and M as 180°.

We can also notice that the parallel at the poles resolve into a a single point each, so their radius will be zero, and therefore we can skip drawing them. We can name the angle used for calculating each parallel vertical location as parallelAngleStep. As mentioned above, this value can be calculated as 360 degrees divided by the number of segments. For each parallel (starting from the south pole), we can say that the vertical offset equals to a cosine of that angle, namely:

verticalOffset = cos(parallelAngleStep * parallelNumber) * radius

Converting that in C#, we get the following. Please note that each parallel is currently of equal radius (since we still need to calculate it), and that the first (index equals zero) and the last one (index equals number of segments) are skipped since, like mentioned above, radius at those points is zero, so we don’t need to waste resources drawing those. To keep the code cleaner, we are immediately using radians when calculating the parallelAngleStep (180 degrees equals PI). It is also important to note that these new circles are drawn at a different plane, oriented by 90 degrees over the Y-axis. Additionally a red circle with the same number of segments is drawn as a guideline:

public static void DrawSphere(Vector3 position, Quaternion orientation, float radius, Color color, int segments = 4)
{
    if(segments < 2)
    {
        segments = 2;
    }

    int doubleSegments = segments * 2;
        
    // Guidance circle
    DrawCircle(position, Quaternion.Euler(0, 0, 0), radius, Color.red, doubleSegments);

    Vector3 verticalOffset = Vector3.zero;
    float parallelAngleStep = Mathf.PI / segments;
    for (int i = 1; i < segments; i++)
    {
        verticalOffset = Vector3.up * Mathf.Cos(parallelAngleStep * i) * radius;
        DrawCircle(position + verticalOffset, Quaternion.Euler(90.0f, 0, 0), radius, color, doubleSegments);
    }
}

Using this code in our Example class, we get the following result (like before, using a varied number of segments):

As with the vertical offset, now we need to calculate the radius. As mentioned in the list of “problems” above, radius is changing between zero at the poles and one at the equator. If we think in the terms of trigonometry, we can remember that one function behaves like that, namely sine function. Therefore we can use the sine function value for each step to receive radius:

public static void DrawSphere(Vector3 position, Quaternion orientation, float radius, Color color, int segments = 4)
{
    if(segments < 2)
    {
        segments = 2;
    }

    int doubleSegments = segments * 2;
        
    // Guidance circle
    DrawCircle(position, Quaternion.Euler(0, 0, 0), radius, Color.red, doubleSegments);

    Vector3 verticalOffset = Vector3.zero;
    float parallelAngleStep = Mathf.PI / segments;
    float stepRadius = 0.0f;
    float stepAngle = 0.0f;

    for (int i = 1; i < segments; i++)
    {
        stepAngle = parallelAngleStep * i;
        verticalOffset = Vector3.up * Mathf.Cos(stepAngle) * radius;
        stepRadius = Mathf.Sin(stepAngle) * radius;

        DrawCircle(position + verticalOffset, Quaternion.Euler(90.0f, 0, 0), stepRadius, color, doubleSegments);
    }
}

Using this new method in our Example class, we receive the following result:

Last thing we need to do, before introducing the rotation, is to merge these two steps, and draw both meridians and parallels:

public static void DrawSphere(Vector3 position, Quaternion orientation, float radius, Color color, int segments = 4)
{
    if(segments < 2)
    {
        segments = 2;
    }

    int doubleSegments = segments * 2;
        
    // Draw meridians

    float meridianStep = 180.0f / segments;

    for (int i = 0; i < segments; i++)
    {
        DrawCircle(position, Quaternion.Euler(0, meridianStep * i, 0), radius, color, doubleSegments);
    }

    // Draw parallels

    Vector3 verticalOffset = Vector3.zero;
    float parallelAngleStep = Mathf.PI / segments;
    float stepRadius = 0.0f;
    float stepAngle = 0.0f;

    for (int i = 1; i < segments; i++)
    {
        stepAngle = parallelAngleStep * i;
        verticalOffset = Vector3.up * Mathf.Cos(stepAngle) * radius;
        stepRadius = Mathf.Sin(stepAngle) * radius;

        DrawCircle(position + verticalOffset, Quaternion.Euler(90.0f, 0, 0), stepRadius, color, doubleSegments);
    }
}

Running this code in our Example class, we finally get the completed spheres:

Sphere Orientation

Contrary to Euler rotation, Quaternions are combined by multiplication, therefore to calculate QuatC which is a combination of two quaternions (QuatA and QuatB), we need to use the following equation:

QuatC = QuatA * QuatB

In the context of our sphere-drawing method, we need to consider that in a few places:

  • Meridians orientation,
  • Orientation of the vertical axis used to calculate parallels’ vertical offset,
  • Parallels orientation (since their polygons’ points need to match the points of meridians’ polygons).

Therefore, our final DrawSphere code will look like this:

public static void DrawSphere(Vector3 position, Quaternion orientation, float radius, Color color, int segments = 4)
{
    if(segments < 2)
    {
        segments = 2;
    }

    int doubleSegments = segments * 2;
        
    // Draw meridians

    float meridianStep = 180.0f / segments;

    for (int i = 0; i < segments; i++)
    {
        DrawCircle(position, orientation * Quaternion.Euler(0, meridianStep * i, 0), radius, color, doubleSegments);
    }

    // Draw parallels

    Vector3 verticalOffset = Vector3.zero;
    float parallelAngleStep = Mathf.PI / segments;
    float stepRadius = 0.0f;
    float stepAngle = 0.0f;

    for (int i = 1; i < segments; i++)
    {
        stepAngle = parallelAngleStep * i;
        verticalOffset = (orientation * Vector3.up) * Mathf.Cos(stepAngle) * radius;
        stepRadius = Mathf.Sin(stepAngle) * radius;

        DrawCircle(position + verticalOffset, orientation * Quaternion.Euler(90.0f, 0, 0), stepRadius, color, doubleSegments);
    }
}

I have extended the Example class a bit, so it takes radius and drawing color as user-defined values, so we can preview different orientations and radii, using the same number of segments:

This tutorial was probably the most complex when it comes to drawing various 3D debug shapes. The following tutorial covers how to draw debug cubes and boxes (rectangular cuboids).

The post Unity – Draw A Debug Sphere appeared first on dev-tut.com.

]]>
808
Unity – Draw A Debug Rectangle https://dev-tut.com/2022/unity-draw-a-debug-rectangle/ Sun, 13 Mar 2022 00:22:45 +0000 https://dev-tut.com/?p=780 After covering circle and triangle in previous tutorials, we can continue with another geometric shape commonly used in game development, a rectangle. In this tutorial you will learn how to debug draw a rectangle in 3D space using various input parameters. This tutorial is a part of Unity– Draw Custom Debug Shapes series. I would […]

The post Unity – Draw A Debug Rectangle appeared first on dev-tut.com.

]]>
After covering circle and triangle in previous tutorials, we can continue with another geometric shape commonly used in game development, a rectangle. In this tutorial you will learn how to debug draw a rectangle in 3D space using various input parameters.

This tutorial is a part of Unity– Draw Custom Debug Shapes series. I would suggest to check the previous tutorials too, since they explain some of the basic geometry and Unity concepts in more detail.

Rectangle 101

Rectangle is a quadrilateral (a four-sided polygon) defined by four vertices (A, B, C, D) connected by four sides (AB, BC, CD, DA) so that the opposite sides are equally long and parallel, with angles between any two sides being right (90°). Consequently, diagonals (AC, BD) have equal lengths and bisect each other. Intersection point of rectangle’s diagonals defines its center. These properties can be seen in the following image:

To draw such a rectangle in Unity engine using the Debug.DrawLine method, we can use one of two approaches:

  • Drawing a rectangle based on its origin and extent (sides lengths)
  • Drawing a rectangle based on two points

Origin and Extent

To draw a rectangle we need to calculate points A, B, C, D based on the provided origin and extent. Origin is provided as position in 3D space, while the extent consists of lengths along the x and y axis. Extent x defines the length of sides AB and CD, while extent y defines the length of sides BC and DA.

Based on the image above, we can conclude that each point is distanced half of extent along each axis, in different directions, from the origin. Therefore we can define them as follows:

A = O +  {extent_x \over 2} + {extent_y \over 2}
B = O -  {extent_x \over 2} + {extent_y \over 2}
C = O -  {extent_x \over 2} - {extent_y \over 2}
B = O +  {extent_x \over 2} - {extent_y \over 2}

With this out of the way, we can continue in Unity. Our code will need to perform the following:

  • Calculate points A, B, C and D
  • Connect them using the Debug.DrawLine method to form a rectangle

Our method signature will need to contain the following parameters:

  • position (origin),
  • extent,
  • color
public static void DrawRectangle(Vector3 position, Vector2 extent, Color color)

Next, we need to calculate offsets along the x (right) and y (up) axes:

Vector3 rightOffset = Vector3.right * extent.x * 0.5f;
Vector3 upOffset = Vector3.up * extent.y * 0.5f;

The next step is to calculate the offsets of each points (although, we could have already calculate the position of each point, this will make sense a bit later down the line when we introduce orientation):

Vector3 offsetA = rightOffset + upOffset;
Vector3 offsetB = -rightOffset + upOffset;
Vector3 offsetC = -rightOffset - upOffset;
Vector3 offsetD = rightOffset - upOffset;

Finally, we can connect the points using the Debug.DrawLine and the given color:

DrawLine(position + offsetA, position + offsetB, color);
DrawLine(position + offsetB, position + offsetC, color);
DrawLine(position + offsetC, position + offsetD, color);
DrawLine(position + offsetD, position + offsetA, color);

Below you can find the full Debug.DrawRectangle method:

public static void DrawRectangle(Vector3 position, Vector2 extent, Color color)
{
    Vector3 rightOffset = Vector3.right * extent.x * 0.5f;
    Vector3 upOffset = Vector3.up * extent.y * 0.5f;

    Vector3 offsetA = rightOffset + upOffset;
    Vector3 offsetB = -rightOffset + upOffset;
    Vector3 offsetC = -rightOffset - upOffset;
    Vector3 offsetD = rightOffset - upOffset;

    DrawLine(position + offsetA, position + offsetB, color);
    DrawLine(position + offsetB, position + offsetC, color);
    DrawLine(position + offsetC, position + offsetD, color);
    DrawLine(position + offsetD, position + offsetA, color);
}

We can use this new method in our Example.cs class Update method:

void Update()
{
    Debug.DrawRectangle(transform.position, new Vector2(2.0f, 1.0f), Color.magenta);
}

As we have done in the previous tutorials, we can also introduce the orientation. This part is explained in much more detail in the Draw A Debug Circle – Part 3.

To do so, we need to extend the DrawRectangle method signature:

public static void DrawRectangle(Vector3 position, Quaternion orientation, Vector2 extent, Color color)

And, we need to include that orientation in the calculation of each point’s offset:

Vector3 offsetA = orientation * (rightOffset + upOffset);
Vector3 offsetB = orientation * (-rightOffset + upOffset);
Vector3 offsetC = orientation * (-rightOffset - upOffset);
Vector3 offsetD = orientation * (rightOffset - upOffset);

Using this extended functionality in the Example.cs we get the result shown in the following image:

void Update()
{
    Debug.DrawRectangle(transform.position, transform.rotation, new Vector2(2.0f, 1.0f), Color.magenta);
}

Two Points

An alternative way of defining/drawing a rectangle is by providing two diagonal points, as seen in the image below (for convenience I have named them P1 and P2, and also tagged each rectangle point with A, B, C and D respectively):

From the image above we can conclude the following:

  • Since they are identical, it can be said that the point A is P1 units distanced from the origin,
  • Extent along the x-axis is a distance between the P1 and P2 along that axis,
  • Extent along the y-axis is a distance between the P1 and P2 along that axis,
  • Point B is extentx units away from the point A, along the x-axis,
  • Point C is extenty units away from the point B, along the y-axis,
  • Point D is extenty units away from the point B, along the y-axis.

We can use that information to calculate each point of the rectangle:

A = O + P_1
B = A + (extent_x, 0)
C = B + (0, extent_y)
D = A + (0, extent_y)

Like with the previous method, we can already include the orientation parameter. To incorporate orientation we also need to rotate the x- and y-axis and add extent for each point along those rotated axes. This translates in C# code as follows:

public static void DrawRectangle(Vector2 point1, Vector2 point2, Vector3 origin, Quaternion orientation, Color color)
{
    // Calculate extent as a distance between point1 and point2
    float extentX = Mathf.Abs(point1.x - point2.x);
    float extentY = Mathf.Abs(point1.y - point2.y);

    // Calculate rotated axes
    Vector3 rotatedRight = orientation * Vector3.right;
    Vector3 rotatedUp = orientation * Vector3.up;
        
    // Calculate each rectangle point
    Vector3 pointA = origin + rotatedRight * point1.x + rotatedUp * point1.y;
    Vector3 pointB = pointA + rotatedRight * extentX;
    Vector3 pointC = pointB + rotatedUp * extentY;
    Vector3 pointD = pointA + rotatedUp * extentY;

    // Draw lines between the points
    DrawLine(pointA, pointB, color);
    DrawLine(pointB, pointC, color);
    DrawLine(pointC, pointD, color);
    DrawLine(pointD, pointA, color);
}

Using this new method in Update of our Example.cs class, we get the following:

void Update()
{
    Vector2 pointA = new Vector2(-0.50f, -0.50f);
    Vector2 pointB = new Vector2(2.0f, 1.0f);

    Debug.DrawRectangle(pointA, pointB, transform.position, transform.rotation, Color.magenta);
}

DRY – Don’t Repeat Yourself

To improve the Debug.DrawRectangle methods, we can use DRY principle. DRY is a coding principle aimed at reducing repetition of code. Since both of our methods are drawing lines between four points to form a quadrilateral, we can extract that part of both methods into a new method called DrawQuad which takes these four points as parameters, as well as the drawing color:

public static void DrawQuad(Vector3 pointA, Vector3 pointB, Vector3 pointC, Vector3 pointD, Color color)
{
    // Draw lines between the points
    DrawLine(pointA, pointB, color);
    DrawLine(pointB, pointC, color);
    DrawLine(pointC, pointD, color);
    DrawLine(pointD, pointA, color);
}

With that method in place, we can include it in our DrawRectangle methods as follows:

// Draw a rectangle defined by its position, orientation and extent
public static void DrawRectangle(Vector3 position, Quaternion orientation, Vector2 extent, Color color)
{
    Vector3 rightOffset = Vector3.right * extent.x * 0.5f;
    Vector3 upOffset = Vector3.up * extent.y * 0.5f;

    Vector3 offsetA = orientation * (rightOffset + upOffset);
    Vector3 offsetB = orientation * (-rightOffset + upOffset);
    Vector3 offsetC = orientation * (-rightOffset - upOffset);
    Vector3 offsetD = orientation * (rightOffset - upOffset);

    DrawQuad(position + offsetA,
                position + offsetB,
                position + offsetC,
                position + offsetD, 
                color);
}
// Draw a rectangle defined by two points, origin and orientation
public static void DrawRectangle(Vector2 point1, Vector2 point2, Vector3 origin, Quaternion orientation, Color color)
{
    // Calculate extent as a distance between point1 and point2
    float extentX = Mathf.Abs(point1.x - point2.x);
    float extentY = Mathf.Abs(point1.y - point2.y);

    // Calculate rotated axes
    Vector3 rotatedRight = orientation * Vector3.right;
    Vector3 rotatedUp = orientation * Vector3.up;
        
    // Calculate each rectangle point
    Vector3 pointA = origin + rotatedRight * point1.x + rotatedUp * point1.y;
    Vector3 pointB = pointA + rotatedRight * extentX;
    Vector3 pointC = pointB + rotatedUp * extentY;
    Vector3 pointD = pointA + rotatedUp * extentY;

    DrawQuad(pointA, pointB, pointC, pointD, color);
}

With this, we have concluded the “Draw a Debug Rectangle” tutorial . The following part of this series covers how to combine previously described shapes to create more elaborate ones which can help you in your game development journey even more.

The post Unity – Draw A Debug Rectangle appeared first on dev-tut.com.

]]>
780
Unity – Draw A Debug Triangle https://dev-tut.com/2022/unity-draw-a-debug-triangle/ Sun, 27 Feb 2022 16:57:18 +0000 https://dev-tut.com/?p=744 As many of you know, a triangle drawn in three-dimensional space is a foundation of 3D graphics. As such, it can be useful to be able to draw different types of triangles within your Unity project and to understand their properties. Through this tutorial we will cover four different approaches for drawing triangles: Triangle defined […]

The post Unity – Draw A Debug Triangle appeared first on dev-tut.com.

]]>
As many of you know, a triangle drawn in three-dimensional space is a foundation of 3D graphics. As such, it can be useful to be able to draw different types of triangles within your Unity project and to understand their properties.

Through this tutorial we will cover four different approaches for drawing triangles:

  • Triangle defined by three points in 3D space,
  • Isosceles triangle defined by its base length and height,
  • Equilateral triangle defined by its origin and height.
  • Equilateral triangle defined by its origin and size of the side(s).

Each approach will also include a possibility to add a linear offset and orientation.

This tutorial is a part of Unity– Draw Custom Debug Shapes series. I would suggest to check the previous tutorials too, since they explain some of the basic geometry and Unity concepts in more detail.

Three Points Triangle

Drawing a triangle using the three points, although by far the simplest, will be a base for each following approach. The only work this method needs to perform is to connect three points using the Unity’s built-in Debug method DrawLine. As the title suggest, the method needs to receive four arguments (points A, B and C) and a drawing color. Afterwards, it simply needs to connect the following points, A-B, B-C and C-A. Source code of this method can be seen below:

// Draw a triangle defined by three points
public static void DrawTriangle(Vector3 pointA, Vector3 pointB, Vector3 pointC, Color color)
{
    // Connect pointA and pointB
    Debug.DrawLine(pointA, pointB, color);

    // Connect pointB and pointC
    Debug.DrawLine(pointB, pointC, color);

    // Connect pointC and pointA
    Debug.DrawLine(pointC, pointA, color);
}

As in the Draw a Debug Circle – Part 2 tutorial, we can use Example class’ Update method to test this method:

void Update()
{
    Debug.DrawTriangle(new Vector3(0, 0, 0), new Vector3(0.25f, 1, 0), new Vector3(0.5f, 0.5f), Color.red);
}

The result of this code can be seen in the following image:

Additionally, we can increase the usefulness of this method by extending it with two parameters: linear offset and orientation. This could, for example make it possible to “attach” a certain triangle to a moving object and draw the triangle in that object’s space. To do so, we can use overloaded method (a method with the same name, but with a different set of parameters). As explained in Unity – Draw a Debug Circle – Part 3, we can offset and rotate the triangle by using some basic linear algebra operations. The source code of this “enhanced” method is show below:

public static void DrawTriangle(Vector3 pointA, Vector3 pointB, Vector3 pointC, Vector3 offset, Quaternion orientation, Color color)
{
    pointA = offset + orientation * pointA;
    pointB = offset + orientation * pointB;
    pointC = offset + orientation * pointC;

    DrawTriangle(pointA, pointB, pointC, color);
}

As you can see, the new, overloading version of the DrawTriangle is calling the original version of the DrawTriangle method, but in such case it is not starting a recursive behavior. For an example, we can draw a triangle with the same points as the previous one, but this time “attached” to the Example GameObject’s position and orientation, and for easier differentiation, drawn using green color:

void Update()
{
    Debug.DrawTriangle(new Vector3(0, 0, 0), new Vector3(0.25f, 1, 0), new Vector3(0.5f, 0.5f), Color.red);

    Debug.DrawTriangle(new Vector3(0, 0, 0), new Vector3(0.25f, 1, 0), new Vector3(0.5f, 0.5f), 
        transform.position, transform.rotation, Color.green);
}

By offsetting the Example GameObject by (1.0, 0, 0) and rotating it by (0°, 0°, -30°) we receive the following result (manipulation handles are added for increased clarity on what is happening):

Isosceles Triangle

Isosceles triangle is a triangle with two equal sides. Usually, in a context of game development its use-cases are more limited than the ones of a triangle defined by three points, but in the context of debugging it can be extremely useful when it comes to debugging various directions or “heading” of certain objects, as well as drawing arrows which describe vector’s directions and their magnitude.

In their basic form, isosceles triangles are defined by the length of their base, and the length of the equal sides or by it’s height. Since the approach using a base length and triangle height is generally more useful, we will focus on implementing that one. As with the previous example, we will also incorporate linear offset and orientation as the additional parameters.

To calculate the points of isosceles triangle, we need to make some assumptions first:

  • Base of the triangle is defined by the points A and C,
  • Base of the triangle is bisected by the point D,
  • Distance between the point B and bisector point D is triangle’s height
  • Segment B-D is perpendicular to the triangle base (A-C)

Using Unity’s built-in math methods makes calculating these points relatively easy, by simply offsetting the origin location in different direction, by certain lengths:

  • We can consider the point D as the origin of the triangle and set it to location (0, 0, 0),
  • Point A is a half base-length units away to the right,
  • Point C is a half base-length units away to the left (negative right),
  • Point B is height units away above the point D.

The following code shows how to do that. As you can see, point D calculation is skipped since it is simply a zero-vector (Vector3.zero). After calculating points A, B and C, they are passed to a method used to draw the three-point triangle with linear and angular offset, as well as the drawing color:

public static void DrawTriangle(Vector3 origin, Quaternion orientation, float baseLength, float height, Color color)
{
    Vector3 pointA = Vector3.right * baseLength * 0.5f;
    Vector3 pointC = Vector3.left * baseLength * 0.5f;
    Vector3 pointB = Vector3.up * height;

    DrawTriangle(pointA, pointB, pointC, origin, orientation, color);
}

And we can call this new method from our Example class:

void Update()
{
    Debug.DrawTriangle(transform.position, transform.rotation, 1.0f, 2.0f, Color.yellow);
}

Which results in the following image (using a zero and non-zero offset and orientation):

Equilateral Triangle

Equilateral triangle is a triangle whose sides’ lengths and consequently angles between them are equal. One way of defining an equilateral triangle is by using it’s center and lengths of the sides. Each of the points of the equilateral triangle is also equally distant from the triangle’s center.

One of the ways to calculate the points A, B and C that are equally distant from the center (O), is to utilize the same trigonometry technique as show in the Unity – Draw a Debug Circle – Part 2 tutorial. In a sense a equilateral triangle defined in this way can be understood as a circle-approximation with only three sides. In that sense the points are calculated as follows (for now, we can assume that O = [0, 0]). First we need to calculate the mentioned circle. In this case it will be a distance connecting the origin O with any of the triangle’s points. From the image above it is possible to construct a triangle defined by the points O, A and O’, which is a projection of the point O on the segment CA. Since the O’ is bisecting the segment CA, we can conclude that the segment O’A is half of the side length. From the following image we can see that the angle CAO is 30°, and we can use it to calculate the length of OA, which is in this case a hypothenuse of the triangle O’OA. For convenience, we can call the segment OA “radius”.

cos(30°) = \frac{0.5*length}{radius}
radius = \frac{0.5 * length}{cos(30°)}
A = [cos(330°), sin(330°)] * radius
B=[cos(90°), sin(90°)] * radius
C = [cos(210°), sin (210°)] * radius

After calculating these three, we can simply pass them to the existing three-point triangle drawing method. The following implementation does exactly that (as with the previous examples, linear offset and rotation are passed too):

public static void DrawTriangle(float length, Vector3 center, Quaternion orientation, Color color)
{
    float radius = length / Mathf.Cos(30.0f * Mathf.Deg2Rad) * 0.5f;
    Vector3 pointA = new Vector3(Mathf.Cos(330.0f * Mathf.Deg2Rad), Mathf.Sin(330.0f * Mathf.Deg2Rad), 0.0f) * radius;
    Vector3 pointB = new Vector3(Mathf.Cos(90.0f * Mathf.Deg2Rad), Mathf.Sin(90.0f * Mathf.Deg2Rad), 0.0f) * radius;
    Vector3 pointC = new Vector3(Mathf.Cos(210.0f * Mathf.Deg2Rad), Mathf.Sin(210.0f * Mathf.Deg2Rad), 0.0f) * radius;

    DrawTriangle(pointA, pointB, pointC, center, orientation, color);
}

By calling this new method in our Example class, we receive the following:

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

This concludes the “triangle” section of this tutorial series. The following tutorial covers how to draw debug rectangles.

The post Unity – Draw A Debug Triangle appeared first on dev-tut.com.

]]>
744
Unity – Draw A Debug Circle – Wrap-up https://dev-tut.com/2022/unity-draw-a-debug-circle-part-5/ Sun, 20 Feb 2022 01:38:47 +0000 https://dev-tut.com/?p=730 Through the previous four parts of this tutorial we have seen how to draw a debug circle in Unity, how to rotate it in 3D space and how to draw arcs, segments and sectors of a circle. In this part we will cover some basic C# concepts to make the extension functions a bit more […]

The post Unity – Draw A Debug Circle – Wrap-up appeared first on dev-tut.com.

]]>
Through the previous four parts of this tutorial we have seen how to draw a debug circle in Unity, how to rotate it in 3D space and how to draw arcs, segments and sectors of a circle.

In this part we will cover some basic C# concepts to make the extension functions a bit more useful and convenient. Examples show in this tutorial are just a first step from which you should be able to modify the code according to your needs.

Drawing on a Plane

In many cases (at least in my experience) drawing a shape is handled within an existing plane of the 3D space. For convenience, these planes can be named based on the axes that define them, namely: XY, XZ and YZ.

This can be accomplished in two ways, either having three dedicated methods, or by passing the desired plane identifier as a parameter. For the sake of explaining the both approaches we can also do a hybrid of those two.

Let’s start with drawing a circle in XY plane (as it is also the most common in first person, and 2D games made in Unity, since Unity uses Y as vertical axis). First thing to do is to modify the signature of the DrawCircle method, so that the number of segments has a default value of 16. This is done purely for convenience, but also to match the signature of the DrawArc method:

public static void DrawCircle(Vector3 position, Quaternion orientation, float radius, Color color, int segments = 16)

With this modification out of the way, we can continue with the next step and create the DrawCircleXY method. The only role of this method is to call the existing DrawCircle method and pass the Quaternion.idenitity as the orientation parameter. Since this orientation will be constant, the signature of DrawCircleXY does not need to contain it:

public static void DrawCircleXY(Vector3 position, float radius, Color color, int segments = 16)
{
    DrawCircle(position, Quaternion.identity, radius, color, segments);
}

Same approach can be used for the remaining two planes XZ and YZ, where the only difference is that we need to pass a different orientation. In the case of XZ, we need to use (0, -90°, 0°) and in the case of YZ we need to use (90°, 0°, 0°).

public static void DrawCircleXZ(Vector3 position, float radius, Color color, int segments = 16)
{
    DrawCircle(position, Quaternion.Euler(0, -90, 0), radius, color, segments);
}

public static void DrawCircleYZ(Vector3 position, float radius, Color color, int segments = 16)
{
    DrawCircle(position, Quaternion.Euler(90, 0, 0), radius, color, segments);
}

The second approach, as previously mentioned, is to create a method which takes the plane identifier as an parameter. The most common way of defining this parameter would be to use enum. An enum is a value type that defines a number of named constants. In this case we need to create an enum called DrawingPlane and its constants can be named simply to match the planes: XY, XZ, YZ. By default, these constants will have an underlaying type int.

public enum DrawingPlane
{
    XY,
    XZ, 
    YZ
}

Next thing that we need to do is to overload the DrawCircle method. Simply described, method overloading is a way to implement multiple methods with the same name, but with a different signature. This can be useful if we need methods with the same name, but with a different number, type or an order of parameters. Overloaded version of DrawCircle method needs to include the DrawingPlane enum, and based on its value call the appropriate method for drawing the circle in a specific plane. The decision which method to call is handled through a switch statement (default case of this statement calls the same method as the DrawingPlane.XY case):

public static void DrawCircle(Vector3 position, DrawingPlane drawingPlane, float radius, Color color, int segments = 16)
{
    switch (drawingPlane)
    {
        case DrawingPlane.XY:
            DrawCircleXY(position, radius, color, segments);
            break;

        case DrawingPlane.XZ:
            DrawCircleXZ(position, radius, color, segments);
            break;

        case DrawingPlane.YZ:
            DrawCircleYZ(position, radius, color, segments);
            break;

        default:
            DrawCircleXY(position, radius, color, segments);
            break;
    }
}

To test this functionality, we can use the following code in the Example class:

void Update()
{
    Debug.DrawCircleXY(Vector3.zero, 1, Color.blue, 32);
    Debug.DrawCircleXZ(Vector3.zero, 1, Color.red, 32);
    Debug.DrawCircleYZ(Vector3.zero, 1, Color.green, 32);

    Debug.DrawCircle(Vector3.zero, DrawingPlane.XY, 0.5f, Color.blue, 32);
    Debug.DrawCircle(Vector3.zero, DrawingPlane.XZ, 0.5f, Color.red, 32);
    Debug.DrawCircle(Vector3.zero, DrawingPlane.YZ, 0.5f, Color.green, 32);
}

The result of this code can be seen in the following image (for better preview, camera location is set to (5, 4, -5), rotation to (30, -45, 0) and FOV to 30 degrees):

Same logic can be applied to the DrawArc method. The following code implements exactly the same concepts as the previous code, and it can be seen below:

public static void DrawArcXY(float startAngle, float endAngle, 
    Vector3 position, float radius, Color color, bool drawChord = false, 
    bool drawSector = false, int arcSegments = 16)
{
    DrawArc(startAngle, endAngle, position, Quaternion.identity, radius, 
        color, drawChord, drawSector, arcSegments);
}

public static void DrawArcXZ(float startAngle, float endAngle, 
    Vector3 position, float radius, Color color, bool drawChord = false, 
    bool drawSector = false, int arcSegments = 16)
{
    DrawArc(startAngle, endAngle, position, Quaternion.Euler(0, -90, 0), radius, 
        color, drawChord, drawSector, arcSegments);
}

public static void DrawArcYZ(float startAngle, float endAngle, 
    Vector3 position, float radius, Color color, bool drawChord = false, 
    bool drawSector = false, int arcSegments = 16)
{
    DrawArc(startAngle, endAngle, position, Quaternion.Euler(90, 0, 0), radius, 
        color, drawChord, drawSector, arcSegments);
}

public static void DrawArc(float startAngle, float endAngle, 
    Vector3 position, DrawingPlane drawingPlane, float radius, 
    Color color, bool drawChord = false, bool drawSector = false, 
    int arcSegments = 16)
{
    switch (drawingPlane)
    {
        case DrawingPlane.XY:
            DrawArcXY(startAngle, endAngle, position, radius, color, 
                drawChord, drawSector, arcSegments);
            break;

        case DrawingPlane.XZ:
            DrawArcXZ(startAngle, endAngle, position, radius, color, 
                drawChord, drawSector, arcSegments);
            break;

        case DrawingPlane.YZ:
            DrawArcYZ(startAngle, endAngle, position, radius, color, 
                drawChord, drawSector, arcSegments);
            break;

        default:
            DrawArcXY(startAngle, endAngle, position, radius, color, 
                drawChord, drawSector, arcSegments);
            break;
    }
}

We can test this new code using our Example class:

void Update()
{
    Debug.DrawArcXY(0.0f, 90.0f, Vector3.zero, 1.0f, Color.blue, false, true, 16);
    Debug.DrawArcXZ(90.0f, 180.0f, Vector3.zero, 1.0f, Color.red, false, true, 16);
    Debug.DrawArcYZ(-90.0f, 0.0f, Vector3.zero, 1.0f, Color.green, false, true, 16);

    Debug.DrawArc(0.0f, 90.0f, Vector3.zero, DrawingPlane.XY, 0.5f, Color.blue, false, true, 16);
    Debug.DrawArc(90.0f, 180.0f, Vector3.zero, DrawingPlane.XZ, 0.5f, Color.red, false, true, 16);
    Debug.DrawArc(-90.0f, 0.0f, Vector3.zero, DrawingPlane.YZ, 0.5f, Color.green, false, true, 16);
}

The result of this code can be seen in the following image:

Coming Up!

The following tutorials will address how to expand the Debug class with multiple methods for drawing triangles and rectangles. Stay tuned!

The post Unity – Draw A Debug Circle – Wrap-up appeared first on dev-tut.com.

]]>
730
Unity – Draw A Debug Circle – Part 4 https://dev-tut.com/2022/unity-draw-a-debug-circle-part-4/ Sun, 13 Feb 2022 14:17:58 +0000 https://dev-tut.com/?p=702 In previous part we have seen how to draw a debug circle with rotation in 3D space in Unity project. This tutorial will introduce concepts of circle arc, segment and sector which can be particularly interesting for debugging multiple gameplay elements, for example, NPC field of view, radar cone, circle overlaps, but MOST importantly, they […]

The post Unity – Draw A Debug Circle – Part 4 appeared first on dev-tut.com.

]]>
In previous part we have seen how to draw a debug circle with rotation in 3D space in Unity project. This tutorial will introduce concepts of circle arc, segment and sector which can be particularly interesting for debugging multiple gameplay elements, for example, NPC field of view, radar cone, circle overlaps, but MOST importantly, they will allow us to draw a Debug Pizza.

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

Circle Arc

Circle arc is part of the circumference of a circle. Although arc is usually defined with two points, in the context of game development it is more useful to define it using two angles. Given a circle and two angles along its circumference, it is possible to define two arcs:

  • Minor arc – shorter arc between two points
  • Major arc – longer arc between two points

Minor arc (blue) and major arc (red) are shown on the following image:

By default, arcs between two angles are drawn in counter-clockwise direction. This might not be optimal in most cases, so we will need to introduce a way to to select direction. Additionally, this will introduce a small issue when drawing an arc which starts at an angle greater than 180° and ends at the angle lesser than 180° (as show on the previous image).

To prevent this issue, while still maintaining some degree of convenience for end user (potentially game designer, or another developer), our method will internally calculate the arc between 0° and the span between the defined start and end angle, and then offset it by the start angle.

As with the previous tutorials, we will start with the most basic implementation of the arc drawing method, which only takes start angle, end angle, radius and drawing color as parameters. For direction, it is dictated by the difference between starting and ending angle. If (mapped to 0°-360° range) start angle is lesser than end angle, arc will be drawn counterclockwise, otherwise, it will be drawn clockwise.

public static void DrawArc(float startAngle, float endAngle, float radius, Color color, int arcSegments = 32)
{
    float arcSpan = Mathf.DeltaAngle(startAngle, endAngle);

    // Since Mathf.DeltaAngle returns a signed angle of the shortest path between two angles, it 
    // is necessary to offset it by 360.0 degrees to get a positive value
    if (arcSpan <= 0)
    {
        arcSpan += 360.0f;
    }

    // angle step is calculated by dividing the arc span by number of approximation segments
    float angleStep = (arcSpan / arcSegments) * Mathf.Deg2Rad;
    float stepOffset = startAngle * Mathf.Deg2Rad;

    // stepStart, stepEnd, lineStart and lineEnd variables are declared outside of the following for loop
    float stepStart = 0.0f;
    float stepEnd = 0.0f;
    Vector3 lineStart = Vector3.zero;
    Vector3 lineEnd = Vector3.zero;

    for (int i = 0; i < arcSegments; i++)
    {
        // Calculate approximation segment start and end, and offset them by start angle
        stepStart = angleStep * i + stepOffset;
        stepEnd = angleStep * (i + 1) + stepOffset;

        lineStart.x = Mathf.Cos(stepStart);
        lineStart.y = Mathf.Sin(stepStart);
        lineStart *= radius;

        lineEnd.x = Mathf.Cos(stepEnd);
        lineEnd.y = Mathf.Sin(stepEnd);
        lineEnd *= radius;

        DrawLine(lineStart, lineEnd, color);
    }
} 

To make a few examples, we can use this method in the Update of Example class:

void Update()
{
    Debug.DrawArc(-45, 45, 1.0f, Color.green);
    Debug.DrawArc(45, -45, 0.9f, Color.magenta);
    Debug.DrawArc(45, 135, 0.8f, Color.red);
    Debug.DrawArc(135, 45, 0.7f, Color.yellow);
    Debug.DrawArc(180, 0, 0.6f, Color.cyan);
    Debug.DrawArc(0, 180, 0.5f, Color.white);
}

This example results in the following image:

Circle Segment

Circle segment is a region bounded by an arc of the circle and a line segment connecting the arc’s starting and ending point. This line segment is also named chord.

Depending on which arc is used to define a segment, segments are also split in two types: minor and major. These segments are shown on the following image:

We can extend the DrawArc method to include a flag if the chord should be drawn, and therefore the segment. Besides extending the method signature with the drawChord argument is is necessary to store arc start point (arcStart) and arc end point (arcEnd), and simply connect them using the Debug.DrawLine method.

public static void DrawArc(float startAngle, float endAngle, float radius, Color color, bool drawChord = false, int arcSegments = 32)
{
    float arcSpan = Mathf.DeltaAngle(startAngle, endAngle);

    // Since Mathf.DeltaAngle returns a signed angle of the shortest path between two angles, it 
    // is necessary to offset it by 360.0 degrees to get a positive value
    if (arcSpan <= 0)
    {
        arcSpan += 360.0f;
    }

    // angle step is calculated by dividing the arc span by number of approximation segments
    float angleStep = (arcSpan / arcSegments) * Mathf.Deg2Rad;
    float stepOffset = startAngle * Mathf.Deg2Rad;

    // stepStart, stepEnd, lineStart and lineEnd variables are declared outside of the following for loop
    float stepStart = 0.0f;
    float stepEnd = 0.0f;
    Vector3 lineStart = Vector3.zero;
    Vector3 lineEnd = Vector3.zero;

    // arcStart and arcEnd need to be stored to be able to draw segment chord
    Vector3 arcStart = Vector3.zero;
    Vector3 arcEnd = Vector3.zero;

    for (int i = 0; i < arcSegments; i++)
    {
        // Calculate approximation segment start and end, and offset them by start angle
        stepStart = angleStep * i + stepOffset;
        stepEnd = angleStep * (i + 1) + stepOffset;

        lineStart.x = Mathf.Cos(stepStart);
        lineStart.y = Mathf.Sin(stepStart);
        lineStart *= radius;

        lineEnd.x = Mathf.Cos(stepEnd);
        lineEnd.y = Mathf.Sin(stepEnd);
        lineEnd *= radius;

        // If this is the first iteration, set the chordStart
        if(i == 0)
        {
            arcStart = lineStart;
        }

        // If this is the last iteration, set the chordEnd
        if(i == arcSegments - 1)
        {
            arcEnd = lineEnd;
        }

        DrawLine(lineStart, lineEnd, color);
    }

    if (drawChord)
    {
        DrawLine(arcStart, arcEnd, color);
    }
}

Using the Example class we can demonstrate this new functionality:

void Update()
{
    Debug.DrawArc(-35, 45, 1.0f, Color.cyan, true);
    Debug.DrawArc(45, -35, 1.0f, Color.red, false);
}

Following image displays the result:

Circle sector

Similar to the circle segment, circle sector represents a region which is bounded with a circle arc, and (instead of a single) two line segments named rays. Circle sector requires two ray:

  • Connecting arc start with the circle origin,
  • Connecting arc end with the circle origin.

As with the circle arcs and segments, sectors too can be minor and major, depending on which arc defines them.

Similarly to the circle segment implementation, we need to extend the signature of DrawArc method with a drawSector argument. Only change that is needed in the method implementation, if the drawSector is true, to connect arcStart and arcEnd with the circle origin (at this moment it is still (0,0,0)).

public static void DrawArc(float startAngle, float endAngle, float radius, Color color, bool drawChord = false, bool drawSector = false, int arcSegments = 32)
{
    float arcSpan = Mathf.DeltaAngle(startAngle, endAngle);

    // Since Mathf.DeltaAngle returns a signed angle of the shortest path between two angles, it 
    // is necessary to offset it by 360.0 degrees to get a positive value
    if (arcSpan <= 0)
    {
        arcSpan += 360.0f;
    }

    // angle step is calculated by dividing the arc span by number of approximation segments
    float angleStep = (arcSpan / arcSegments) * Mathf.Deg2Rad;
    float stepOffset = startAngle * Mathf.Deg2Rad;

    // stepStart, stepEnd, lineStart and lineEnd variables are declared outside of the following for loop
    float stepStart = 0.0f;
    float stepEnd = 0.0f;
    Vector3 lineStart = Vector3.zero;
    Vector3 lineEnd = Vector3.zero;

    // arcStart and arcEnd need to be stored to be able to draw segment chord
    Vector3 arcStart = Vector3.zero;
    Vector3 arcEnd = Vector3.zero;

    // arcOrigin represents an origin of a circle which defines the arc
    Vector3 arcOrigin = Vector3.zero;

    for (int i = 0; i < arcSegments; i++)
    {
        // Calculate approximation segment start and end, and offset them by start angle
        stepStart = angleStep * i + stepOffset;
        stepEnd = angleStep * (i + 1) + stepOffset;

        lineStart.x = Mathf.Cos(stepStart);
        lineStart.y = Mathf.Sin(stepStart);
        lineStart *= radius;

        lineEnd.x = Mathf.Cos(stepEnd);
        lineEnd.y = Mathf.Sin(stepEnd);
        lineEnd *= radius;

        // If this is the first iteration, set the chordStart
        if(i == 0)
        {
            arcStart = lineStart;
        }

        // If this is the last iteration, set the chordEnd
        if(i == arcSegments - 1)
        {
            arcEnd = lineEnd;
        }

        DrawLine(lineStart, lineEnd, color);
    }

    if (drawChord)
    {
        DrawLine(arcStart, arcEnd, color);
    }
    if (drawSector)
    {
        DrawLine(arcStart, arcOrigin, color);
        DrawLine(arcEnd, arcOrigin, color);
    }
}

Using the Example class we can demonstrate this new functionality:

void Update()
{
    Debug.DrawArc(-35, 45, 1.0f, Color.cyan, false, true);
    Debug.DrawArc(45, -35, 1.0f, Color.red);
}

Following image displays the result:

Position and Rotation

To make the newly created DrawArc method, we can extend it with position and orientation arguments. Details of this implementation are discussed in detail the previous tutorial, and the same implementation is still valid for this example:

public static void DrawArc(float startAngle, float endAngle, 
    Vector3 position, Quaternion orientation, float radius, 
    Color color, bool drawChord = false, bool drawSector = false, 
    int arcSegments = 32)
{
    float arcSpan = Mathf.DeltaAngle(startAngle, endAngle);

    // Since Mathf.DeltaAngle returns a signed angle of the shortest path between two angles, it 
    // is necessary to offset it by 360.0 degrees to get a positive value
    if (arcSpan <= 0)
    {
        arcSpan += 360.0f;
    }

    // angle step is calculated by dividing the arc span by number of approximation segments
    float angleStep = (arcSpan / arcSegments) * Mathf.Deg2Rad;
    float stepOffset = startAngle * Mathf.Deg2Rad;

    // stepStart, stepEnd, lineStart and lineEnd variables are declared outside of the following for loop
    float stepStart = 0.0f;
    float stepEnd = 0.0f;
    Vector3 lineStart = Vector3.zero;
    Vector3 lineEnd = Vector3.zero;

    // arcStart and arcEnd need to be stored to be able to draw segment chord
    Vector3 arcStart = Vector3.zero;
    Vector3 arcEnd = Vector3.zero;

    // arcOrigin represents an origin of a circle which defines the arc
    Vector3 arcOrigin = position;

    for (int i = 0; i < arcSegments; i++)
    {
        // Calculate approximation segment start and end, and offset them by start angle
        stepStart = angleStep * i + stepOffset;
        stepEnd = angleStep * (i + 1) + stepOffset;

        lineStart.x = Mathf.Cos(stepStart);
        lineStart.y = Mathf.Sin(stepStart);
        lineStart.z = 0.0f;

        lineEnd.x = Mathf.Cos(stepEnd);
        lineEnd.y = Mathf.Sin(stepEnd);
        lineEnd.z = 0.0f;

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

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

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

        // If this is the first iteration, set the chordStart
        if (i == 0)
        {
            arcStart = lineStart;
        }

        // If this is the last iteration, set the chordEnd
        if(i == arcSegments - 1)
        {
            arcEnd = lineEnd;
        }

        DrawLine(lineStart, lineEnd, color);
    }

    if (drawChord)
    {
        DrawLine(arcStart, arcEnd, color);
    }
    if (drawSector)
    {
        DrawLine(arcStart, arcOrigin, color);
        DrawLine(arcEnd, arcOrigin, color);
    }
}

Debug Draw Pizza

With all technicalities out of the way, we can make the pièce de résistance of this tutorial: Drawing a debug pizza!

We need the following ingredients:

  • Pizza slice (minor sector)
  • Rest of the pie (major sector)
  • Toppings (minor and major segments, depending on the location)
void DrawDebugPizza()
{
    // Draw a slice
    Vector3 sliceOffset = (Vector3.up + Vector3.right) * 0.16f;
    Debug.DrawArc(20, 75, transform.position + sliceOffset, transform.rotation, 1.0f, Color.yellow, false, true);
    Debug.DrawArc(20, 75, transform.position + sliceOffset, transform.rotation, 0.85f, Color.yellow, false, true);

    // Draw the rest of the pie
    Debug.DrawArc(75, 20, transform.position, transform.rotation, 1.0f, Color.yellow, false, true);
    Debug.DrawArc(75, 20, transform.position, transform.rotation, 0.85f, Color.yellow, false, true);

    // Draw full toppings
    Debug.DrawCircle(transform.position + new Vector3(-0.4f, 0.2f,0), transform.rotation, 0.22f, 16, Color.red);
    Debug.DrawCircle(transform.position + new Vector3(0.5f, -0.35f,0), transform.rotation, 0.17f, 16, Color.red);
    Debug.DrawCircle(transform.position + new Vector3(-0.1f, -0.5f,0), transform.rotation, 0.22f, 16, Color.red);

    // Draw sliced toppings
    Debug.DrawArc(242, 88, transform.position + sliceOffset + new Vector3(0.2f, 0.55f, 0), transform.rotation, 0.20f, Color.red, true);
    Debug.DrawArc(88, 242, transform.position + new Vector3(0.2f, 0.55f, 0), transform.rotation, 0.20f, Color.red, true);

    Debug.DrawArc(29, 192, transform.position + sliceOffset + new Vector3(0.5f, 0.15f, 0), transform.rotation, 0.20f, Color.red, true);
    Debug.DrawArc(192, 29, transform.position + new Vector3(0.5f, 0.15f, 0), transform.rotation, 0.20f, Color.red, true);

}

And finally, we can by calling this method from Update of the Example class, we get our own Debug Pizza:

The following part covers some basic C# concepts to make the extension functions a bit more useful and convenient.

The post Unity – Draw A Debug Circle – Part 4 appeared first on dev-tut.com.

]]>
702
Unity – Draw A Debug Circle – Part 3 https://dev-tut.com/2022/unity-draw-a-circle-part3/ Sat, 29 Jan 2022 16:48:34 +0000 https://dev-tut.com/?p=600 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 […]

The post Unity – Draw A Debug Circle – Part 3 appeared first on dev-tut.com.

]]>
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!

The post Unity – Draw A Debug Circle – Part 3 appeared first on dev-tut.com.

]]>
600
Unity – Draw A Debug Circle – Part 2 https://dev-tut.com/2022/unity-draw-a-circle-part2/ Sat, 22 Jan 2022 15:58:44 +0000 https://dev-tut.com/?p=556 In previous part we have seen how a circle can be defined using the basic trigonometry equations. In this part we will see how to transform that knowledge into code and use it in an actual Unity project. This tutorial is a part of tutorial series Unity – Draw Custom Debug Shapes Extending Debug Class […]

The post Unity – Draw A Debug Circle – Part 2 appeared first on dev-tut.com.

]]>
In previous part we have seen how a circle can be defined using the basic trigonometry equations. In this part we will see how to transform that knowledge into code and use it in an actual Unity project.

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

Extending Debug Class

In this tutorial we will create one additional Debug Draw method. To make future usage of that method as streamlined as possible, it is necessary to override/extend Unity’s Debug class. To do so, first create a new C# class and name it Debug.cs (personally I prefer to store them in Assets/Scripts folder, but for this example it is not crucial).

Inside of that file insert the following code:

using UnityEngine;

class Debug : UnityEngine.Debug
{
    // UnityEngine class extensions
}

This new Debug class will be used as a base for all following extensions and tutorials.

As discussed in previous tutorial, circle approximation can be described with following properties:

  • Center/origin
  • Radius
  • Number of segments

These properties need to be passed to the drawing function. Additionally, to make things prettier (and more useful), we can also pass desired drawing color. Therefore, a signature of the most basic version of the circle drawing method can look like this:

public static class DrawCircle(Vector3 position, float radius, float segments, Color color)

For the most basic implementation of this function, circle can be drawn in the XY plane (following tutorials will cover how to draw circles on different planes, as well as how to handle rotation).

To draw a circle, the method needs perform following steps:

  • Check if radius is greater than zero,
  • Check if number of segments is greater than zero,
  • Calculate how many degrees each segment covers,
  • For each segment, calculate start and end points,
  • Offset those points by the location of origin/center,
  • Connect those points using built-in Debug.DrawLine method, and using the passed color
public static void DrawCircle(Vector3 position, 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);

        // 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));

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

        // 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 test the functionality of this method, we can create a simple example MonoBehavior class. To do so, create a new C# file named Example.cs, empty and an empty GameObject named Example, and place the newly created C# script on it. Also, reset its transform to make sure it is located at (0,0,0).

Inside of the Example.cs script change the Update method to following:

void Update()
{
    // Draw circles at the position of the GameObject, with different radii, number of segments and colors
    Debug.DrawCircle(transform.position, 2.0f, 32, Color.red);
    Debug.DrawCircle(transform.position, 3.0f, 16, Color.yellow);
    Debug.DrawCircle(transform.position, 4.0f, 8, Color.green);

    // Draw circles with a certain offset from the position of the GameObject
    Debug.DrawCircle(transform.position + new Vector3(-5.0f, -5.0f, 0), 0.5f, 16, Color.white);
    Debug.DrawCircle(transform.position + new Vector3(5.0f, 5.0f, 0), 0.5f, 16, Color.white);
    Debug.DrawCircle(transform.position + new Vector3(-5.0f, 5.0f, 0), 0.5f, 16, Color.white);
    Debug.DrawCircle(transform.position + new Vector3(5.0f, -5.0f, 0), 0.5f, 16, Color.white);
}

As you may notice, transform.position of this GameObject is used as the origin of the circles. If everything was done correctly, after starting the game, you should receive similar results as shown below (please not that the location of the MainCamera is (0, 0, -10)):

The following tutorial will cover how to implement custom rotation of the circles, so they can be drawn in any plane.

The post Unity – Draw A Debug Circle – Part 2 appeared first on dev-tut.com.

]]>
556
Unity – Draw A Debug Circle – Part 1 https://dev-tut.com/2022/unity-draw-a-circle-part1/ Sat, 22 Jan 2022 13:41:48 +0000 https://dev-tut.com/?p=537 While developing a game, drawing a debug circle can be immensely helpful. Inspired by DrawDebugHelpers class of Unreal Engine, I have decided to extend (override) Unity’s Debug class to make that possible. This tutorial is a part of tutorial series Unity – Draw Custom Debug Shapes How to Draw a Circle Drawing a sphere can […]

The post Unity – Draw A Debug Circle – Part 1 appeared first on dev-tut.com.

]]>
While developing a game, drawing a debug circle can be immensely helpful. Inspired by DrawDebugHelpers class of Unreal Engine, I have decided to extend (override) Unity’s Debug class to make that possible.

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

How to Draw a Circle

Drawing a sphere can be done using some basic trigonometry operations. Instead of a “perfect” circle we will draw a polygon with an adjustable number of edges/segments which gives us “good enough” approximation of the said circle. This approach is also useful since the same method can be used to draw any regular polygon (triangle, quad, pentagon, hexagon, etc.).

Main Ingredients: SIne and Cosine

Sine and cosine functions are cornerstones of this tutorial. As a basic example, let’s start with a circle defined in a XY coordinate space, with a radius 1.0 and with four points (A, B, C, D) marked on it. Additionally, we can check an angle of vectors connecting circle’s origin with each of the points, as well as a right-angle triangle formed by circle’s origin (O), A and A’ (projection of the point A on the X-axis).

Given the triangle (O, A, A’) it is possible to determine that the segment (A, A’) can be considered as “opposite” to the angle of the vector (O, A) and segment (O, A’) can be considered as “adjacent”.
Also, length of the hypothenuse of this triangle (O,A) is equal to the radius of the circle, which is in this case 1.0.

To calculate the length of “opposite” segment, we can use the equation:

sin(\alpha)=opposite/hypothenuse

Since hypothenuse of the triangle is equal to 1.0, we can simply rearrange the equation as:

opposite = sin(\alpha)

Same applies for “adjacent”, where the following equation is true:

cos(\alpha)=adjacent/hypothenuse

and by using the same analogy, we get:

adjacent=cos(\alpha)

Length of the “adjacent” side can be viewed as X coordinate of the point A, and length of the “opposite” side can be viewed as Y coordinate of the point A.

Using this relationship, each of the given points can be defined in (X, Y) format as follows:

A = (cos(45\degree), sin(45\degree))

B = (cos(135\degree), sin(135\degree))

C = (cos(225\degree), sin(225\degree))

D = (cos(315\degree), sin(315\degree))

This property gives us a possibility to find coordinates of any point on the circle, as long as we know the angle of the vector between the origin of the circle and the said point. Based on that, it is possible to define, for example, eight points on the circle, and by connecting them receive an inscribed octagon which is an approximation of the said circle:

As you can assume, these points could also be defined inside of the Unity project, and connected using the Debug.DrawLine function. Next part of this tutorial explains how to do that.

The post Unity – Draw A Debug Circle – Part 1 appeared first on dev-tut.com.

]]>
537