Spatial Grids
In polygon modeling, objects exist anywhere in smooth, continuous space. In Voxelization, we force the world into a rigid, discrete Grid (like Minecraft blocks or a 3D spreadsheet). This is called Discretization.
Defining the Boundaries:
1# Calculating the Grid Extents2def create_grid(points, cell_size):3 """Defines the boundaries of our Minecraft world."""45 # 1. Find the Bounding Box of the entire point cloud6 min_x = min([p.x for p in points])7 max_x = max([p.x for p in points])8 # ... repeat for Y and Z910 # 2. Divide by the cell size to find out how many11 # rows, columns, and layers we need!12 count_x = math.ceil((max_x - min_x) / cell_size)13 count_y = math.ceil((max_y - min_y) / cell_size)14 count_z = math.ceil((max_z - min_z) / cell_size)1516 return min_x, min_y, min_z, count_x, count_y, count_z
A standard 3D point has floating-point coordinates like X: 1.2345, Y: -4.9876. These numbers are infinitely precise. Grids, however, are made of solid, countable blocks. We must convert these floats into integers. This is called Quantization.
Flooring to Integers:
5.5 and Cell Size is 2.0, we get 2.75. 3. Apply a mathematical Floor function (drop the decimal). 2.75 becomes integer 2! 4. The point now officially belongs to Column 2 of the grid. Any other point that falls between 4.0 and 5.999 will also be quantized to Column 2.1# Quantizing Floating Point to Integers2def get_cell_coordinates(point, min_corner, cell_size):3 """Converts a float (1.234, 4.567) into an integer cell index (1, 4)."""45 # 1. Shift the point relative to the bottom-left corner of the grid6 local_x = point.x - min_corner.x7 local_y = point.y - min_corner.y8 local_z = point.z - min_corner.z910 # 2. Divide by the size of the cells11 # 3. Floor the result! (Chop off all decimal places)12 grid_x = math.floor(local_x / cell_size)13 grid_y = math.floor(local_y / cell_size)14 grid_z = math.floor(local_z / cell_size)1516 return grid_x, grid_y, grid_z
Computer memory (RAM) is not 3D. RAM is a single, massive, 1-dimensional line of data. We cannot ask the computer to "store this in box 2, 4, 3". We have to convert our 3D grid coordinate into a 1D Index.
Flattening Space:
count_x * count_y). 3. We multiply that by our grid_z to skip all the layers below us. 4. We calculate how many boxes are in a single row (count_x) and multiply by our grid_y to skip all rows below us on this layer. 5. Finally, we add our grid_x position. 6. We have converted (2, 4, 3) into a single ID number like 342.1# Flat Array Indexing2def get_hash_key(grid_x, grid_y, grid_z, count_x, count_y):3 """Turns a 3D index into a single 1D array index."""45 # Imagine a bookshelf:6 # 1. Skip over all the completed 'shelves' below you (grid_z)7 # 2. Skip over all the completed 'rows' on your shelf (grid_y)8 # 3. Add your position in the current row (grid_x)910 index = grid_x + (grid_y * count_x) + (grid_z * count_x * count_y)1112 return index
Now that every possible 3D box has a unique 1D ID number, we create a giant List in the computer's memory. The List has exactly as many slots as there are boxes. Each slot is an empty list, waiting to act as a Bucket.
Filling the Buckets:
1# Creating the Storage Array2def insert_points_into_grid(points, total_cell_count):3 """Sorts all points into their respective boxes."""45 # 1. Create a giant empty list of lists (Buckets)6 grid_buckets = [ [] for _ in range(total_cell_count) ]78 # 2. Put every point into a bucket based on its Hash ID9 for pt in points:10 # Get coordinates11 gx, gy, gz = get_cell_coordinates(pt)1213 # Get ID14 bucket_id = get_hash_key(gx, gy, gz)1516 # Add the point to the bucket17 grid_buckets[bucket_id].append(pt)1819 return grid_buckets
Why did we do all of this math just to put points in boxes? Speed.If you have a 1-Million point cloud, and you want to find all points within a 2-meter radius of your mouse cursor, checking every single point would require 1 million distance calculations per frame. Your computer would freeze.
Skipping 99% of the Work:
1# Extremely Fast Searching2def find_points_in_radius(query_pt, radius, grid_buckets, cell_size):3 """How to search 1 million points without lagging."""45 found_points = []67 # 1. Which box is our query point in?8 qx, qy, qz = get_cell_coordinates(query_pt)910 # 2. How many boxes do we need to check? (Radius / CellSize)11 box_range = math.ceil(radius / cell_size)1213 # 3. Only look inside the nearby boxes!14 for x in range(qx - box_range, qx + box_range + 1):15 for y in range(qy - box_range, qy + box_range + 1):16 for z in range(qz - box_range, qz + box_range + 1):1718 bucket_id = get_hash_key(x, y, z)19 points_to_check = grid_buckets[bucket_id]2021 # Check actual distance (Math.sqrt) only for these few points22 for pt in points_to_check:23 if distance(query_pt, pt) <= radius:24 found_points.append(pt)2526 return found_points
Spatial Grids are not just for searching. They are the first step in converting a Point Cloud (like LiDAR scan data) back into a solid 3D mesh. To do this, we need to know which parts of the grid are "Solid" and which are "Empty Air".
Thresholding:
1# Calculating Grid Density2def calculate_voxel_density(grid_buckets):3 """How full is each box?"""45 dense_voxels = []67 for bucket in grid_buckets:8 # Check how many points fell into this box9 point_count = len(bucket.points)1011 # If it has more than 5 points, we consider it "Solid"12 if point_count > 5:13 dense_voxels.append(bucket)1415 return dense_voxels
When we combine Discretization, Quantization, Hash Mapping, and Density Thresholding, we get a completeVoxelization pipeline. We can take any raw, messy Point Cloud scan and turn it into a solid, organized block structure.
Volumetric Data:
1# Point Cloud Voxelization2def voxelize_bunny(bunny_points, resolution):3 """Minecraft-ifies a high-res 3D scan."""45 # 1. Create the grid based on the Bunny's bounding box6 grid = create_grid(bunny_points, resolution)78 # 2. Toss all the points into the buckets9 buckets = insert_points(bunny_points, grid)1011 # 3. Find the dense boxes12 solid_voxels = calculate_voxel_density(buckets)1314 return solid_voxels