(Writing)

3D snap-to-grid with rotation (RXE)

When I was in high school I made a few games using the Unity engine and C#. They weren't very polished, but at the least they were finished projects which I was pretty happy about as a kid.

The second of these games, RXE, had a 3D building system that allowed you to "snap" parts together. Here's an example of three parts attached to a "chassis":

It was pretty obvious from the start that parts would have to snap to a grid in the world (e.g. the x-coordinate might need to be a multiple of 0.125 meters). Imagine how hard it would be otherwise to manually align four wheels perfectly!

My first solution was to look at the point the cursor was aiming at, and then use float-to-integer truncation to snap to a grid. (Finer grids can be done by scaling the coordinates up, truncating, and then scaling back down...a hint of what would come next.) In the end it looked something like this:

This isn't particularly difficult to do when the original object is un-rotated (like in the above images), but it gets harder when you have to snap to something like this:

As soon as the object was rotated, I couldn't just truncate the xyz-coordinates, since this only allowed me to snap along the cardinal axes.

The solution I came up with involved some basic linear algebra. There are three high-level steps here:

  1. Determine a basis that "aligns" with the parent object (this wasn't hard since Unity already provided "up"/"forwards"/"right" vectors for each object);
  2. Transform the point (what cursor is looking at) from world space basis into the parent object's basis;
  3. Snap the point to a grid in the new basis;
  4. Reverse-transform the point back into world space.

First I had to create my own 3x3 matrix class, since Unity did not have one. The code for that is here: Matrix3x3.cs

In the end, I had a function called SnapVector3ToCustomGrid(...) which took as inputs:

and returned the adjusted vector as output:

public static Vector3 SnapVector3ToCustomGrid(Vector3 input, Grid3D grid, Vector3SnapTypes v3st) {

    Matrix3x3 matCoB = Grid3D.GetChangeOfBasisMatrixFromStandardToGrid(grid);
    
    Vector3 snappedInBasis = SnapVector3ToGrid(matCoB * input, v3st);
    
    return Matrix3x3.Inverse(matCoB) * snappedInBasis;
}

This funcition called the "snap" code internally:

private static Vector3 SnapVector3ToGrid(Vector3 input, Vector3SnapTypes v3st) {
    switch (v3st) {
        case Vector3SnapTypes.SnapXY:
            input.x = (int)((Mathf.Abs(input.x) + minGridDelta * 0.5f) / minGridDelta) * minGridDelta * Mathf.Sign(input.x);
            input.y = (int)((Mathf.Abs(input.y) + minGridDelta * 0.5f) / minGridDelta) * minGridDelta * Mathf.Sign(input.y);
            break;
        case Vector3SnapTypes.SnapXZ:
            input.x = (int)((Mathf.Abs(input.x) + minGridDelta * 0.5f) / minGridDelta) * minGridDelta * Mathf.Sign(input.x);
            input.z = (int)((Mathf.Abs(input.z) + minGridDelta * 0.5f) / minGridDelta) * minGridDelta * Mathf.Sign(input.z);
            break;
        case Vector3SnapTypes.SnapYZ:
            input.y = (int)((Mathf.Abs(input.y) + minGridDelta * 0.5f) / minGridDelta) * minGridDelta * Mathf.Sign(input.y);
            input.z = (int)((Mathf.Abs(input.z) + minGridDelta * 0.5f) / minGridDelta) * minGridDelta * Mathf.Sign(input.z);
            break;
        default:
            // (debug log error handling; removed in this article)
            break;
    }

    return input;
}

The final result works pretty well, and as a result the player doesn't have to waste time painstakingly aligning objects exactly in the right positions:

I am leaving out a few details, but this was a neat chance to use linear algebra in one of my own projects.

The tutorial for RXE is here, but I wouldn't recommend actually playing the game since I never properly finished it.

(Writing)