How To Use The Batch Transform And Batch Gizmo Drawing APIs

Posted by : on

Category : Unity

Introduction

Starting from Unity version 2023.1, two new APIs have been introduced. The first one enables the batch transformation of an arbitrary number of points, vectors, or directions from local to world space, or the inverse transformation from world to local. The second API facilitates the batch drawing of any number of lines using the existing Gizmos class.

These additions offer significant performance improvements by reducing the need for data marshalling between the C# and C++ spaces. However, performance is not the only benefit; they also provide more convenience when dealing with a large amount of data.

These two new APIs have been backported to versions 2022.2 and 2022.3 (which is the current LTS version at the time of writing this post).

Batch Gizmo Line Drawing

The Gizmos.DrawLine function has been a part of Unity for a long time. It is commonly utilized within the OnDrawGizmos and OnDrawGizmosSelected MonoBehaviour event methods, allowing us to draw a line between two points. These lines serve various purposes in the Unity editor, such as indicating interesting areas for our game objects.

Previously, if we needed to draw more than one line, we had to call Gizmos.DrawLine separately for each line. However, with the introduction of two new APIs, Gizmos.DrawLineList and Gizmos.DrawLineStrip, this process has been streamlined. Both of these APIs accept a ReadOnlySpan<Vector3> parameter and draw multiple lines with a single call, although there are some distinctions between them.

Let’s consider a scenario where we need to draw three lines in one of our objects. All of these lines would start at x=-1 and end at x=1. The first line would be at the center of our object (y=0), and the other two would be one unit above and one unit below our object (y=1 and y=-1).

Here’s how we would achieve this using the Gizmos.DrawLine method before the introduction of the new APIs:

private void OnDrawGizmosSelected()
{
    Gizmos.color = Color.blue;
    Gizmos.DrawLine(new Vector3(-1, 1, 0), new Vector3(1, 1, 0));
    Gizmos.DrawLine(new Vector3(-1, 0, 0), new Vector3(1, 0, 0));
    Gizmos.DrawLine(new Vector3(-1, -1, 0), new Vector3(1, -1, 0)); 
}

If our game object is at the position (0,0,0) and has a Sprite Renderer component with a capsule sprite we will see something like this:

Multiple Gizmo.DrawLine

The Gizmos.DrawLineList

We can achieve the same effect by utilizing the Gizmos.DrawLineList method. This method accepts a ReadOnlySpan<Vector3> that must contain an even number of elements. It draws one line for every two elements in the span, where each line starts at an indexed element and ends at the subsequent indexed element. If we supply a span with an odd number of elements, Unity will throw an exception.

The previous code can be rewritten as follows:

private readonly Vector3[] _points = {
    new(-1, 1, 0),
    new(1, 1, 0),
    new(-1, 0, 0),
    new(1, 0, 0),
    new(-1, -1, 0), 
    new(1, -1, 0)  
};

private void OnDrawGizmosSelected()
{
    Gizmos.color = Color.blue;
    Gizmos.DrawLineList(_points);
}

The Gizmos.DrawLineStrip

The Gizmos.DrawLineStrip method also draws multiple lines, where each line starts at the end point of the previous one. Consequently, there is no requirement for an even number of elements in our span parameter.

Additionally, there is a second boolean parameter that indicates whether the line should form a loop. If this parameter is set to true, the last element will serve as both the end and start point of a line, connecting it to the first element of our span.

Here is the result of the previous example when using the Gizmos.DrawLineStrip method with a looped parameter set to false:

private readonly Vector3[] _points = {
    new(-1, 1, 0),
    new(1, 1, 0),
    new(-1, 0, 0),
    new(1, 0, 0),
    new(-1, -1, 0), 
    new(1, -1, 0)  
};

private void OnDrawGizmosSelected()
{
    Gizmos.color = Color.blue;
    Gizmos.DrawLineStrip(_points, false);
}

Gizmos.DrawLineStrip with false looped parameter

and here is the result that we will have, by having the looped parameter true:

Gizmos.DrawLineStrip with false looped parameter

Batch Transforms

The batch transforms comprise three new methods capable of performing batch transformations of points, vectors, and directions from local space to world space. Additionally, there are three methods available to perform the inverse transformation from world to local space.

The six methods are:

Transform.TransformPoints

Transform.TransformVectors

Transform.TransformDirections

Transform.InverseTransformPoints

Transform.InverseTransformVectors

Transform.InverseTransformDirections

All six of these methods can accept either a single parameter, which is a span of Vector3, or two parameters, both of which are spans of Vector3. In the first case, the transformations will overwrite the provided parameter; in the second case, the result will be returned in the second parameter. The second parameter must be of the same length as the first, or an exception will be thrown.

Although all three types of batch methods take Vector3 parameters, they represent different concepts the same way their non-batch counterparts do. For this reason, each transformation yields a different result.

I believe a visual representation would better illustrate the transformations, so below, I will provide small scripts utilizing the transformations along with images showing the points, vectors, or directions before and after the transformations have been applied.

Before the transformations, the result is depicted in blue, and after the transformations, in green.

For all the following examples, our game object is a 2D capsule positioned at (2,1,0), rotated 30 degrees on the z-axis, and scaled by 0.5 along both the x-axis and the y-axis.

The TransformPoints Transformation

The TransformPoints method transforms multiple points from local to world space. In the following script, blue lines represent the points before transformation, while green lines depict them after the transformation.

Here’s the script:

public class GizmoLines : MonoBehaviour
{
    private readonly Vector3[] _points = {
        new(-1, 1, 0),
        new(1, 1, 0),
        new(-1, 0, 0),
        new(1, 0, 0),
        new(-1, -1, 0),
        new(1, -1, 0)
    };
    private readonly Vector3[] _transformedPoints = new Vector3[6];

    private void OnDrawGizmosSelected()
    {
        transform.TransformPoints(_points, _transformedPoints);
        
        Gizmos.color = Color.blue;
        Gizmos.DrawLineList(_points);
        
        Gizmos.color = Color.green;
        Gizmos.DrawLineList(_transformedPoints);
    }
}

The result will look like this:

TransformPoints result

The points, have moved, rotated and scaled according to the game object’s values from local to world space.

The TransformVectors Transformation

The TransformVectors operation will not be affected by the position, but it will be influenced by the scale, as vectors undergo a transformation based on their magnitude.

Here’s the script:

public class GizmoVectors : MonoBehaviour
{
    private readonly Vector3[] _directionsPoints = {
        Vector3.zero,
        Vector3.right,
        Vector3.zero,
        Vector3.up,
        Vector3.zero,
        new(1f,1f,1f)
    };
    private readonly Vector3[] _transformedDirectionsPoints = new Vector3[6];

    private void OnDrawGizmosSelected()
    {
        transform.TransformVectors(_directionsPoints, _transformedDirectionsPoints);
        
        Gizmos.color = Color.blue;
        Gizmos.DrawLineList(_directionsPoints);
        
        Gizmos.color = Color.green;
        Gizmos.DrawLineList(_transformedDirectionsPoints);
    }
}

and here’s the result:

TransformVectors result

As we can see, the green lines have not changed position, but have rotated and changed scale accordingly.

The TransformDirections Transformation

Finally, the TransformDirections operation will not be affected by the position or the scale of the transform, as the vectors represent a direction that theoretically extends to infinity.

Here’s the same script, with the only change being the call from TransformVectors to TransformDirections:

public class GizmoDirections : MonoBehaviour
{
    private readonly Vector3[] _directionsPoints = {
        Vector3.zero,
        Vector3.right,
        Vector3.zero,
        Vector3.up,
        Vector3.zero,
        new(1f,1f,1f)
    };
    private readonly Vector3[] _transformedDirectionsPoints = new Vector3[6];

    private void OnDrawGizmosSelected()
    {
        transform.TransformDirections(_directionsPoints, _transformedDirectionsPoints);
        
        Gizmos.color = Color.blue;
        Gizmos.DrawLineList(_directionsPoints);
        
        Gizmos.color = Color.green;
        Gizmos.DrawLineList(_transformedDirectionsPoints);
    }
}

and here’s the result:

TransformDirections result

Measuring Performance

The above transformations are not only convenient because we need only one call with one array of Vectors, but, as mentioned, they are incredibly performant compared to their non-batched methods.

Here’s a script that checks that performance using Unity’s performance testing extension for the Unity Test Runner. In my tests, I experienced performance gains from 15x to 50x, but as this will vary for each machine, you are welcome to copy/paste it and try it yourselves, or if you prefer, you can get it from this gist.

using NUnit.Framework;
using Unity.PerformanceTesting;
using UnityEngine;
using Vector3 = UnityEngine.Vector3;

namespace Batch_Transforms_Gizmos.Tests
{
public class BenchMarking
{
    private const int NUMBER_OF_POINTS = 1_000_000;
    
    private readonly Vector3[] pointsArray = new Vector3[NUMBER_OF_POINTS];
    private readonly Vector3[] transformedPoints = new Vector3[NUMBER_OF_POINTS];
    
    private GameObject _cube;
    
    [OneTimeSetUp]
    public void OneTimeSetUp()
    {
        for (int i = 0; i < NUMBER_OF_POINTS; i++)
            pointsArray[i] = new Vector3(Random.Range(float.MinValue,float.MaxValue), 
                                        Random.Range(float.MinValue,float.MaxValue), 
                                        Random.Range(float.MinValue,float.MaxValue));
        
        _cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
    }
    
    [Test, Performance]
    public void MultipleTransformPoint() =>
        Measure.Method(() =>
            {
                for (int i = 0; i < NUMBER_OF_POINTS; i++)
                    transformedPoints[i] = _cube.transform.TransformPoint(pointsArray[i]);
            })
            .Run();

    [Test, Performance]
    public void TransformPoints() =>
        Measure.Method(() => _cube.transform.TransformPoints(pointsArray, transformedPoints))
            .Run();

    [Test, Performance]
    public void MultipleTransformVector() =>
        Measure.Method(() =>
            {
                for (int i = 0; i < NUMBER_OF_POINTS; i++)
                    transformedPoints[i] = _cube.transform.TransformVector(pointsArray[i]);
            })
            .Run();

    [Test, Performance]
    public void TransformVectors() => 
        Measure.Method(() => _cube.transform.TransformVectors(pointsArray, transformedPoints))
            .Run();

    [Test, Performance]
    public void MultipleTransformDirection() =>
        Measure.Method(() =>
        {
            for (int i = 0; i < NUMBER_OF_POINTS; i++)
                transformedPoints[i] = _cube.transform.TransformDirection(pointsArray[i]);
        }).Run();

    [Test, Performance]
    public void TransformDirections() => 
        Measure.Method(() => _cube.transform.TransformDirections(pointsArray, transformedPoints))
            .Run();

    [OneTimeTearDown]
    public void OneTimeTearDown() => Object.DestroyImmediate(_cube);
}
}

Conclusion

These were the new batch APIs in Unity. Although the performance benefits are more important for the batch transformation methods, as the gizmo draw methods are not for use at runtime, both are a welcome addition not only for the performance boost they offer, but also for the convenience of performing multiple transformations or drawing multiple lines with one statement.

Thank you for reading, and as always, if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also, if you don’t want to miss any of the new blog posts, you can subscribe to my newsletter or the RSS feed.


Follow me: