Unity — Drawing Custom Debug Shapes — Part 7
Draw Debug Spheres
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 2 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°/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.
Please leave a comment if you have any questions or suggestions, or if you have some topic that you would like to to cover in the future.
In the meanwhile, you can also reach me on LinkedIn.
The following tutorial covers how to draw debug cubes and boxes (rectangular cuboids).