# GPU Mesh Voxelizer Part 4: Mesh Translation and Rotation

In this article, we continue developing our GPU-based mesh voxelizer. The previous posts in the series are available here: part1part2, and part3. Previously our model had to be placed at the origin of the scene, with no rotation. We’ll start by fixing that by cleaning up our different space conversions. After that, we’ll refactor our voxels to use a custom struct rather than a generic `float4`. Doing so allows us to be more descriptive with our code and makes it easier to add new data.

## Allowing rotation and translation

Until now, I’ve been fast and loose about local space versus world space. That’s because I’ve been working with a model centred at the origin with no rotation, which makes the two spaces identical. However, now seems like a good time to fix that. So, after experimenting with several possibilities, I decided to store the voxels in world space. That means we can do all the conversions in our compute shader, and the renderer will render them as-is. By the way, if you aren’t familiar with local space (also known as object space) versus world space, I’ll give a brief explanation.

The space you’re in determines the position of your origin. For example, every mesh has an origin (also knows as a pivot), where each vertex’s position is relative to that origin. However, your scene also has an origin. Once you drag a mesh into the scene, its origin becomes relative to the scene’s origin. That means the vertices are relative to the object’s origin relative to the scene’s origin. So, when positions are relative to the mesh’s pivot, we call that local space (or object space). Then, when positions are relative to the scene’s pivot, we call that world space.

So back to our voxels, as I mentioned, we’re going to store the voxel positions directly in world space. However, the triangles we’re checking against are in local space. So we’ll calculate each voxel’s AABB in world space, convert it to local space, and test for an intersection. Then if the test succeeds, we’ll store the world space position. So, we’ll pass the world space position of the minimum bounds to calculate the voxel position and the `WorldToLocal` matrix to convert it to local space for testing. Let’s fix the code.

``````void VoxelizeMeshWithGPU()
{
Profiler.BeginSample("Voxelize Mesh (GPU)");

Bounds bounds = _meshCollider.bounds;
_boundsMin = bounds.min;
...``````

In the previous version, we converted `bounds.min` to local space. Let’s remove that conversion. Later, when we pass all the variables to the compute shader, let’s pass the `WorldToLocal` matrix.

``````var voxelizeKernel = _voxelizeComputeShader.FindKernel("VoxelizeMesh");

Next, in the compute shader, declare the `_WorldToLocal` matrix.

``````...
RWStructuredBuffer<Voxel> _Voxels;
StructuredBuffer<float3> _MeshVertices;
StructuredBuffer<int> _MeshTriangleIndices;

float4x4 _WorldToLocalMatrix;

int _TriangleCount;

float4 _BoundsMin;

float _CellHalfSize;
int _GridWidth;
int _GridHeight;
int _GridDepth;
...``````

Then, use it to convert the AABB center to local space.

``````...
{
if (id.x >= _GridWidth || id.y >= _GridHeight || id.z >= _GridDepth) return;

const float cellSize = _CellHalfSize * 2.0;

const float3 centerPos = float3(
id.x * cellSize + _CellHalfSize + _BoundsMin.x,
id.y * cellSize + _CellHalfSize + _BoundsMin.y,
id.z * cellSize + _CellHalfSize + _BoundsMin.z);

AABB aabb;
aabb.center = mul(_WorldToLocalMatrix, float4(centerPos.xyz, 1.0));
aabb.extents = float3(_CellHalfSize, _CellHalfSize, _CellHalfSize);
...``````

So now, our voxel/triangle intersections are calculated in object space, but are voxels are stored in world space. It won’t look right, though, because our shaders expect them to be in local space when they render them. By the way, you might be wondering why I turn `centerPos` into a `float4` before performing the matrix multiplication. That’s because the transformation matrix is 4 by 4, so you must multiply it by a vector of length 4. Additionally, because it’s a position, the `w` component of the vector is 1. If it were 0, it would be a direction because when performing the multiplication, the 0 would cancel out the translation portion of the transformation matrix.

## Rendering the voxels in world space

Let’s start with the points in the `Voxel.shader` file. Previously we used a matrix to convert them to world space. Now we can skip that step. Let’s remove the `_LocalToWorld` matrix altogether.

``````StructuredBuffer<float4> _Positions;
float4 _Color;
float4 _CollisionColor;

v2f vert(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceID)
{
v2f o;
float4 pos = _Positions[instance_id];
float isSolid = pos.w;
o.color = lerp(_Color, _CollisionColor, isSolid);
o.position = UnityWorldToClipPos(float4(pos.xyz, 1.0));
o.size = 5;
return o;
}``````

Let’s fix the `VoxelBlock.shader` next. This shader is a bit trickier because it’s a surface shader. If you surface shaders automatically generate a bunch of boilerplate code. The problem is that part of the generated code multiplies the vertex position by the `unity_ObjectToWorld` matrix. Since our vertices will already be in world space, we don’t want that. So we’ll fix it by overriding the `unity_ObjectToWorld` matrix by the identity matrix, which results in no transformation.

``````void setup()
{
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
float4 position = _Positions[unity_InstanceID];
float isSolid = position.w;

_Matrix = float4x4(
1, 0, 0, position.x,
0, 1, 0, position.y,
0, 0, 1, position.z,
0, 0, 0, 1
);

_Clip = -1.0 + isSolid;

unity_ObjectToWorld = float4x4(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
);
#endif
}``````

We make the switch in the `setup` function since that runs first. Now we can move and rotate our mesh freely without breaking the voxelizer. While we’re in the spirit of cleaning up, let’s refactor our voxels to use a custom struct.

## Using a custom voxel struct

Until now, we’ve packed all our data into a `float4`. Let’s create a custom `Voxel` type instead. Doing so will make our code a little less cryptic and make it easier to add more data later if needed. First, I moved all my shaders and compute shader into the same folder, which simplifies including common files. Now, create a new `VoxelCommon.cginc` file. Here is where we’ll declare our Voxel type.

``````// VoxelCommon.cginc
struct Voxel
{
float3 position;
float isSolid;
};``````

And in `VoxelizedMesh.cs`, we’ll declare an identical type that lives in the C# side.

``````// VoxelizedMesh.cs
public struct Voxel
{
Vector3 position;
float isSolid;
}``````

Again on the C# side, let’s change everything from `Vector4` to `Voxel`, rename `_gridPoints` to `_voxels` and finally renamed `_Positions` to `_Voxels`.

``````// VoxelizedMesh.cs
...
//Renamed VoxelGridPoints and _Positions

//Renamed _gridPoints
Voxel[] _voxels;
...``````
``````// VoxelizedMesh.cs
...
if (_voxels == null || _voxels.Length != xGridSize * yGridSize * zGridSize ||
_voxelsBuffer == null)
{
_voxels = new Voxel[xGridSize * yGridSize * zGridSize];
resizeVoxelPointsBuffer = true;
}
...``````

In the shaders, include `VoxelCommon.cginc` file and rename the `_Positions` buffer.

``````// VoxelizeMesh.compute
#pragma kernel VoxelizeMesh
#include "VoxelCommon.cginc"

RWStructuredBuffer<Voxel> _Voxels;
StructuredBuffer<float3> _MeshVertices;
StructuredBuffer<int> _MeshTriangleIndices;
...``````
``````// Voxel.shader
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
#include "VoxelCommon.cginc"
...
StructuredBuffer<Voxel> _Voxels;
float4 _Color;
float4 _CollisionColor;``````
``````// VoxelBlock.shader
#pragma instancing_options procedural:setup
#include "VoxelCommon.cginc"
...
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
StructuredBuffer<Voxel> _Voxels;
float4x4 _Matrix;
#endif``````

Finally, modify all the shaders to store position in `.position` and whether the voxel is solid in `.isSolid`.

``````// VoxelizeMesh.compute
const int voxelIndex = id.x + _GridWidth * (id.y + _GridHeight * id.z);

const float3 position = float3(_BoundsMin.x + id.x * cellSize,
_BoundsMin.y + id.y * cellSize,
_BoundsMin.z + id.z * cellSize);

_Voxels[voxelIndex].position = position;
_Voxels[voxelIndex].isSolid = intersects ? 1.0 : 0.0;``````
``````// Voxel.shader
v2f vert(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceID)
{
v2f o;
float3 pos = _Voxels[instance_id].position;
float isSolid = _Voxels[instance_id].isSolid;
o.color = lerp(_Color, _CollisionColor, isSolid);
o.position = UnityWorldToClipPos(float4(pos.xyz, 1.0));
o.size = 5;
return o;
}``````
``````// VoxelBlock.shader
void setup()
{
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
float3 position = _Voxels[unity_InstanceID].position;
float isSolid = _Voxels[unity_InstanceID].isSolid;

_Matrix = float4x4(
1, 0, 0, position.x,
0, 1, 0, position.y,
0, 0, 1, position.z,
0, 0, 0, 1
);

_Clip = -1.0 + isSolid;

unity_ObjectToWorld = float4x4(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
);
#endif
}``````

Great! All that refactoring will make our lives easier in the future. With that out of the way, I’m ready to start filling the inside voxels. However, I haven’t found my ideal solution yet, and this article is already long, so we’ll cover that next time.

The complete project is here on GitHub. If you like this kind of thing, join my mailing list to be notified when the next part is released.