Syncing Voxels and Sprites

voxsprite

We have wanted to sync voxels with sprites since our second game, Guulbusters, but we hadn’t figured out how to sync things perfectly until just today.

Let’s specify exactly what the goal was:

Using an orthographic camera we wanted to sync a pixel to each face of a single voxel. That means a 16^3 cube of voxels will match identically with two 16×16 sprites. Essentially the game can be pixel perfect while also being 3D. We can use 3D lighting effects, 3D shaders, and whatnot.

Before I go into how I did what I did in Unity3D, here are a few things I have yet to implement and won’t be able to discuss in this thread: shadows cast from and onto sprites or normal mapping,

angles

I can’t take all the credit for this technique. This video from Amplitude, the developers of Dungeon of the Endless illuminated a lot of what is going on.

First, the assets are brought into the scene at 8 pixels per Unity unit. And the voxels we bring in are set to 1/8 scale in each X, Y, and Z. You can keep the assets at 100 ppu and then set the voxels to 1/100. I used 8 because it helps my math later.

Now that we have our assets synced for our scene, we have to set the camera to see the voxels properly. The size for the orthographic camera is half of the height of what the camera sees in the gameworld. We want the pixel size of our game to be 384×216. At 8ppu that means that the vertical size of the screen should be 27, and the orthographic size of the camera should be 13.5.

The camera angle is rotated down at 60° on the X axis. At this angle your vertical sprites are shrunk in half. The solution we discovered for this was to place our game objects inside of a container. We attached a script to stretch the World Container 2 in the Y direction.

At this point you can use this to do cool stuff like Final Fantasy Tactics type games. You can test it out, but you will have to move your camera to get a better angle.

But there are some problems! A square on the XZ plane is not displaying as a square on the final image. It is slightly short compared to a square on the XY (sprite) plane. Stretching the Z by (2/sqrt(3)) will solve this problem. (You can use 45° rotation on the X axis and stretch the Y and Z both by sqrt(2), but I like stretching the sprites by integers). I do this via a script and the function called is:


float zStretch = 2f / (Mathf.Sqrt(3f));
float yStretch = 2f;
void StretchWorld() {
transform.localScale = new Vector3 (1, yStretch, zStretch);
}

I call the StretchWorld function on Awake, so now we have the XY plane synced with the XZ plane. You might want to place your variables somewhere else, though.

But there is one last problem. The camera isn’t synced with the squares. The hard part here is that things are not synced on a 1:1, so voxels exist partially in some pixels and partially in other pixels.

In order to sync the camera properly to the grid I used this function:

void ZAdjust() {

float xNow = transform.localPosition.x;
float zNew = -startingHeight / Mathf.Sqrt(3.0f);
transform.localPosition = new Vector3(xNow, startingHeight, zNew);

}

This function is called on Awake. From this point forward you’ll want to stop your camera in increments that sync up with your voxel size. In my case I would only move the camera by 1/8.

The last thing we need to figure out is how to scale the game properly for different screen sizes. Our screen size is 384×216 which is 1/(5^2) 1080p, and we need to scale up the screen an integer amount to retain pixel perfect sprites. I think it’s a different problem than the one we just solved, so we’ll wait on this for next time…