{"id":412,"date":"2021-06-13T00:58:02","date_gmt":"2021-06-13T00:58:02","guid":{"rendered":"https:\/\/bronsonzgeb.com\/?p=412"},"modified":"2021-06-14T18:15:09","modified_gmt":"2021-06-14T18:15:09","slug":"gpu-mesh-voxelizer-part-4","status":"publish","type":"post","link":"https:\/\/bronsonzgeb.com\/index.php\/2021\/06\/13\/gpu-mesh-voxelizer-part-4\/","title":{"rendered":"GPU Mesh Voxelizer Part 4: Mesh Translation and Rotation"},"content":{"rendered":"\n<p>In this article, we continue developing our GPU-based mesh voxelizer. The previous posts in the series are available here:&nbsp;<a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/bronsonzgeb.com\/index.php\/2021\/05\/22\/gpu-mesh-voxelizer-part-1\/\">part1<\/a>,&nbsp;<a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/bronsonzgeb.com\/index.php\/2021\/05\/29\/gpu-mesh-voxelizer-part-2\/\">part2<\/a>, and&nbsp;<a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/bronsonzgeb.com\/index.php\/2021\/06\/05\/gpu-mesh-voxelizer-part-3-render-tons-of-voxels-with-drawmeshinstancedindirect\/\">part3<\/a>. Previously our model had to be placed at the origin of the scene, with no rotation. We&#8217;ll start by fixing that by cleaning up our different space conversions. After that, we&#8217;ll refactor our voxels to use a custom struct rather than a generic <code>float4<\/code>. Doing so allows us to be more descriptive with our code and makes it easier to add new data.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Allowing rotation and translation<\/h2>\n\n\n\n<p>Until now, I&#8217;ve been fast and loose about local space versus world space. That&#8217;s because I&#8217;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&#8217;t familiar with local space (also known as object space) versus world space, I&#8217;ll give a brief explanation.<\/p>\n\n\n\n<p>The space you&#8217;re in determines the position of your origin. For example, every mesh has an origin (also knows as a pivot), where each vertex&#8217;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&#8217;s origin. That means the vertices are relative to the object&#8217;s origin relative to the scene&#8217;s origin. So, when positions are relative to the mesh&#8217;s pivot, we call that local space (or object space). Then, when positions are relative to the scene&#8217;s pivot, we call that world space.<\/p>\n\n\n\n<p>So back to our voxels, as I mentioned, we&#8217;re going to store the voxel positions directly in world space. However, the triangles we&#8217;re checking against are in local space. So we&#8217;ll calculate each voxel&#8217;s AABB in world space, convert it to local space, and test for an intersection. Then if the test succeeds, we&#8217;ll store the world space position. So, we&#8217;ll pass the world space position of the minimum bounds to calculate the voxel position and the <code>WorldToLocal<\/code> matrix to convert it to local space for testing. Let&#8217;s fix the code.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>void VoxelizeMeshWithGPU()\n{\n    Profiler.BeginSample(\"Voxelize Mesh (GPU)\");\n\n    Bounds bounds = _meshCollider.bounds;\n    <strong>_boundsMin = bounds.min;<\/strong>\n...<\/code><\/pre>\n\n\n\n<p>In the previous version, we converted <code>bounds.min<\/code> to local space. Let&#8217;s remove that conversion. Later, when we pass all the variables to the compute shader, let&#8217;s pass the <code>WorldToLocal<\/code> matrix.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>var voxelizeKernel = _voxelizeComputeShader.FindKernel(\"VoxelizeMesh\");\n_voxelizeComputeShader.SetInt(\"_GridWidth\", xGridSize);\n_voxelizeComputeShader.SetInt(\"_GridHeight\", yGridSize);\n_voxelizeComputeShader.SetInt(\"_GridDepth\", zGridSize);\n\n_voxelizeComputeShader.SetFloat(\"_CellHalfSize\", _halfSize);\n\n<strong>_voxelizeComputeShader.SetMatrix(\"_WorldToLocalMatrix\", transform.worldToLocalMatrix);<\/strong>\n_voxelizeComputeShader.SetBuffer(voxelizeKernel, Voxels, _voxelsBuffer);\n_voxelizeComputeShader.SetBuffer(voxelizeKernel, \"_MeshVertices\", _meshVerticesBuffer);\n_voxelizeComputeShader.SetBuffer(voxelizeKernel, \"_MeshTriangleIndices\", _meshTrianglesBuffer);\n_voxelizeComputeShader.SetInt(\"_TriangleCount\", _meshFilter.sharedMesh.triangles.Length);\n\n_voxelizeComputeShader.SetVector(BoundsMin, _boundsMin);<\/code><\/pre>\n\n\n\n<p>Next, in the compute shader, declare the <code>_WorldToLocal<\/code> matrix.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>...\nRWStructuredBuffer&lt;Voxel&gt; _Voxels;\nStructuredBuffer&lt;float3&gt; _MeshVertices;\nStructuredBuffer&lt;int&gt; _MeshTriangleIndices;\n\n<strong>float4x4 _WorldToLocalMatrix;<\/strong>\n\nint _TriangleCount;\n\nfloat4 _BoundsMin;\n\nfloat _CellHalfSize;\nint _GridWidth;\nint _GridHeight;\nint _GridDepth;\n...<\/code><\/pre>\n\n\n\n<p>Then, use it to convert the AABB center to local space.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>...\nvoid VoxelizeMesh(uint3 id : SV_DispatchThreadID)\n{\n    if (id.x &gt;= _GridWidth || id.y &gt;= _GridHeight || id.z &gt;= _GridDepth) return;\n\n    const float cellSize = _CellHalfSize * 2.0;\n\n    const float3 centerPos = float3(\n\t\tid.x * cellSize + _CellHalfSize + _BoundsMin.x,\n                id.y * cellSize + _CellHalfSize + _BoundsMin.y,\n                id.z * cellSize + _CellHalfSize + _BoundsMin.z);\n\n    AABB aabb;\n    <strong>aabb.center = mul(_WorldToLocalMatrix, float4(centerPos.xyz, 1.0));<\/strong>\n    aabb.extents = float3(_CellHalfSize, _CellHalfSize, _CellHalfSize);\n...<\/code><\/pre>\n\n\n\n<p>So now, our voxel\/triangle intersections are calculated in object space, but are voxels are stored in world space. It won&#8217;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 <code>centerPos<\/code> into a <code>float4<\/code> before performing the matrix multiplication. That&#8217;s because the transformation matrix is 4 by 4, so you must multiply it by a vector of length 4. Additionally, because it&#8217;s a position, the <code>w<\/code> 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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Rendering the voxels in world space<\/h2>\n\n\n\n<p>Let&#8217;s start with the points in the <code>Voxel.shader<\/code> file. Previously we used a matrix to convert them to world space. Now we can skip that step. Let&#8217;s remove the <code>_LocalToWorld<\/code> matrix altogether.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>StructuredBuffer&lt;float4&gt; _Positions;\nfloat4 _Color;\nfloat4 _CollisionColor;\n\nv2f vert(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceID)\n{\n    v2f o;\n    float4 pos = _Positions&#91;instance_id];\n    float isSolid = pos.w;\n    o.color = lerp(_Color, _CollisionColor, isSolid);\n    <strong>o.position = UnityWorldToClipPos(float4(pos.xyz, 1.0));<\/strong>\n    o.size = 5;\n    return o;\n}<\/code><\/pre>\n\n\n\n<p>Let&#8217;s fix the <code>VoxelBlock.shader<\/code> next. This shader is a bit trickier because it&#8217;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 <code>unity_ObjectToWorld<\/code> matrix. Since our vertices will already be in world space, we don&#8217;t want that. So we&#8217;ll fix it by overriding the <code>unity_ObjectToWorld<\/code> matrix by the identity matrix, which results in no transformation.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>void setup()\n{\n    #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED\n    float4 position = _Positions&#91;unity_InstanceID];\n    float isSolid = position.w;\n\n    _Matrix = float4x4(\n        1, 0, 0, position.x,\n        0, 1, 0, position.y,\n        0, 0, 1, position.z,\n        0, 0, 0, 1\n    );\n    \n    _Clip = -1.0 + isSolid;\n    \n    <strong>unity_ObjectToWorld = float4x4(\n        1, 0, 0, 0,\n        0, 1, 0, 0,\n        0, 0, 1, 0,\n        0, 0, 0, 1\n    );<\/strong>\n    #endif\n}<\/code><\/pre>\n\n\n\n<p>We make the switch in the <code>setup<\/code> function since that runs first. Now we can move and rotate our mesh freely without breaking the voxelizer. While we&#8217;re in the spirit of cleaning up, let&#8217;s refactor our voxels to use a custom struct.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Using a custom voxel struct<\/h2>\n\n\n\n<p>Until now, we&#8217;ve packed all our data into a <code>float4<\/code>. Let&#8217;s create a custom <code>Voxel<\/code> 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 <code>VoxelCommon.cginc<\/code> file. Here is where we&#8217;ll declare our Voxel type.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ VoxelCommon.cginc\nstruct Voxel\n{\n    float3 position;\n    float isSolid;\n};<\/code><\/pre>\n\n\n\n<p>And in <code>VoxelizedMesh.cs<\/code>, we&#8217;ll declare an identical type that lives in the C# side.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ VoxelizedMesh.cs\npublic struct Voxel\n{\n    Vector3 position;\n    float isSolid;\n}<\/code><\/pre>\n\n\n\n<p>Again on the C# side, let&#8217;s change everything from <code>Vector4<\/code> to <code>Voxel<\/code>, rename <code>_gridPoints<\/code> to <code>_voxels<\/code> and finally renamed <code>_Positions<\/code> to <code>_Voxels<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ VoxelizedMesh.cs\n...\n\/\/Renamed VoxelGridPoints and _Positions\n<strong>static readonly int Voxels = Shader.PropertyToID(\"_Voxels\");<\/strong>\n\n\/\/Renamed _gridPoints\n<strong>Voxel&#91;] _voxels;<\/strong>\n...<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ VoxelizedMesh.cs\n...\nif (_voxels == null || _voxels.Length != xGridSize * yGridSize * zGridSize ||\n    _voxelsBuffer == null)\n{\n    <strong>_voxels = new Voxel&#91;xGridSize * yGridSize * zGridSize];<\/strong>\n    resizeVoxelPointsBuffer = true;\n}\n...<\/code><\/pre>\n\n\n\n<p>In the shaders, include <code>VoxelCommon.cginc<\/code> file and rename the <code>_Positions<\/code> buffer.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ VoxelizeMesh.compute\n#pragma kernel VoxelizeMesh\n<strong>#include \"VoxelCommon.cginc\"<\/strong>\n\n<strong>RWStructuredBuffer&lt;Voxel&gt; _Voxels;<\/strong>\nStructuredBuffer&lt;float3&gt; _MeshVertices;\nStructuredBuffer&lt;int&gt; _MeshTriangleIndices;\n...<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Voxel.shader\nCGPROGRAM\n#pragma vertex vert\n#pragma fragment frag\n\n#include \"UnityCG.cginc\"\n<strong>#include \"VoxelCommon.cginc\"<\/strong>\n...\n<strong>StructuredBuffer&lt;Voxel&gt; _Voxels;<\/strong>\nfloat4 _Color;\nfloat4 _CollisionColor;<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ VoxelBlock.shader\n#pragma surface surf Standard vertex:vert fullforwardshadows addshadow\n#pragma instancing_options procedural:setup\n<strong>#include \"VoxelCommon.cginc\"<\/strong>\n...\n#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED\n<strong>StructuredBuffer&lt;Voxel&gt; _Voxels;<\/strong>\nfloat4x4 _Matrix;\n#endif<\/code><\/pre>\n\n\n\n<p>Finally, modify all the shaders to store position in <code>.position<\/code> and whether the voxel is solid in <code>.isSolid<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ VoxelizeMesh.compute\nconst int voxelIndex = id.x + _GridWidth * (id.y + _GridHeight * id.z);\n\nconst float3 position = float3(_BoundsMin.x + id.x * cellSize,\n                               _BoundsMin.y + id.y * cellSize,\n                               _BoundsMin.z + id.z * cellSize);\n\n<strong>_Voxels&#91;voxelIndex].position = position;<\/strong>\n<strong>_Voxels&#91;voxelIndex].isSolid = intersects ? 1.0 : 0.0;<\/strong><\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Voxel.shader\nv2f vert(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceID)\n{\n    v2f o;\n    <strong>float3 pos = _Voxels&#91;instance_id].position;<\/strong>\n    <strong>float isSolid = _Voxels&#91;instance_id].isSolid;<\/strong>\n    o.color = lerp(_Color, _CollisionColor, isSolid);\n    o.position = UnityWorldToClipPos(float4(pos.xyz, 1.0));\n    o.size = 5;\n    return o;\n}<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ VoxelBlock.shader\nvoid setup()\n{\n    #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED\n    <strong>float3 position = _Voxels&#91;unity_InstanceID].position;<\/strong>\n    <strong>float isSolid = _Voxels&#91;unity_InstanceID].isSolid;<\/strong>\n\n    _Matrix = float4x4(\n        1, 0, 0, position.x,\n        0, 1, 0, position.y,\n        0, 0, 1, position.z,\n        0, 0, 0, 1\n    );\n    \n    _Clip = -1.0 + isSolid;\n    \n    unity_ObjectToWorld = float4x4(\n        1, 0, 0, 0,\n        0, 1, 0, 0,\n        0, 0, 1, 0,\n        0, 0, 0, 1\n    );\n    #endif\n}<\/code><\/pre>\n\n\n\n<p>Great! All that refactoring will make our lives easier in the future. With that out of the way, I&#8217;m ready to start filling the inside voxels. However, I haven&#8217;t found my ideal solution yet, and this article is already long, so we&#8217;ll cover that next time.<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"547\" height=\"434\" src=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/06\/RotatedVoxelMonkey.png\" alt=\"Rotated Voxel Monkey\" class=\"wp-image-419\" srcset=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/06\/RotatedVoxelMonkey.png 547w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/06\/RotatedVoxelMonkey-300x238.png 300w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/06\/RotatedVoxelMonkey-270x215.png 270w\" sizes=\"auto, (max-width: 547px) 100vw, 547px\" \/><\/figure><\/div>\n\n\n\n<p><strong>The complete project is <\/strong><a href=\"https:\/\/github.com\/bzgeb\/UnityGPUMeshVoxelizerPart4\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>here<\/strong><\/a><strong>&nbsp;on GitHub. If you like this kind of thing,&nbsp;<\/strong><a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/bronsonzgeb.com\/index.php\/join-my-mailing-list\/\"><strong>join my mailing list<\/strong><\/a><strong>&nbsp;to be notified when the next part is released.<\/strong><\/p>\n","protected":false},"excerpt":{"rendered":"<p>In this article, we continue developing our GPU-based mesh voxelizer. The previous posts in the series are available here:&nbsp;part1,&nbsp;part2, and&nbsp;part3. Previously our model had to be placed at the origin of the scene, with no rotation. We&#8217;ll start by fixing that by cleaning up our different space conversions. After that, we&#8217;ll refactor our voxels to [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":419,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[25,17,1],"tags":[26,8,5,34],"class_list":["post-412","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-compute-shaders","category-graphics","category-unity-programming","tag-compute-shaders","tag-game-development","tag-unity","tag-voxelization"],"_links":{"self":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/412","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/comments?post=412"}],"version-history":[{"count":7,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/412\/revisions"}],"predecessor-version":[{"id":421,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/412\/revisions\/421"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/media\/419"}],"wp:attachment":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/media?parent=412"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/categories?post=412"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/tags?post=412"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}