3D Archives - dev-tut.com https://dev-tut.com/tag/3d/ Bite-sized software development tutorials Sat, 16 Apr 2022 14:55:27 +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 3D Archives - dev-tut.com https://dev-tut.com/tag/3d/ 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 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