Unity – Draw A Debug Cylinder and Capsule

U

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!

About the author

David Zulic
By David Zulic

David Zulic

Get in touch

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