I asked ChatGPT for a voxelization tool

This tool is particularly useful for creating closed 3D printable meshes from one or several open meshes.
I made it using ChatGPT and it took a couple of hours of optimizing and testing.
It can be used as an alternative to ShrinkWrap.

Initial input, open mesh:

Voxel size = 5mm
Total computing time = 3.56 sec

Voxel size = 1mm
Total computing time = 41.51 sec

Voxel size = 0.5mm
Total computing time = 203.36 sec

# VOXEL TOOL V3
import Rhino
import rhinoscriptsyntax as rs
import scriptcontext as sc
import System

import math
import time

from concurrent.futures import ThreadPoolExecutor
from System.Collections.Generic import List


# ================================================================
# SETTINGS
# ================================================================

DEFAULT_VOXEL_SIZE = 100.0

WORKERS = 5

# Sampling spacing relative to voxel size.
# 0.50 = reliable
SAMPLE_FACTOR = 0.50

# Number of voxel expansion layers
DILATION_STEPS = 1

# Bounding-box padding
PADDING_VOXELS = 2


# ================================================================
# INPUT
# ================================================================

def get_mesh_ids():

    ids = rs.SelectedObjects()

    if ids:

        result = []

        for obj_id in ids:

            obj = sc.doc.Objects.FindId(obj_id)

            if (
                obj and
                isinstance(
                    obj.Geometry,
                    Rhino.Geometry.Mesh
                )
            ):
                result.append(obj_id)

        if result:
            return result

    return rs.GetObjects(
        "Select meshes to voxelize",
        rs.filter.mesh,
        preselect=True
    )


# ================================================================
# COMBINE INPUT MESHES
# ================================================================

def combine_meshes(ids):

    combined = Rhino.Geometry.Mesh()

    for obj_id in ids:

        obj = sc.doc.Objects.FindId(obj_id)

        if not obj:
            continue

        mesh = obj.Geometry

        if not isinstance(
            mesh,
            Rhino.Geometry.Mesh
        ):
            continue

        combined.Append(mesh)

    combined.Vertices.CullUnused()

    # Temporary triangulation only.
    # Original Rhino meshes are untouched.
    combined.Faces.ConvertQuadsToTriangles()

    combined.Compact()

    return combined


# ================================================================
# VOXEL INTEGER KEYS
# ================================================================

def voxel_key(i, j, k, nx, ny):

    return i + nx * (j + ny * k)


# ================================================================
# EXTRACT TRIANGLES
# ================================================================

def extract_triangles(mesh):

    vertices = mesh.Vertices

    triangles = []

    append = triangles.append

    for index in range(mesh.Faces.Count):

        f = mesh.Faces[index]

        a = vertices[f.A]
        b = vertices[f.B]
        c = vertices[f.C]

        append(
            (
                float(a.X),
                float(a.Y),
                float(a.Z),

                float(b.X),
                float(b.Y),
                float(b.Z),

                float(c.X),
                float(c.Y),
                float(c.Z)
            )
        )

    return triangles


# ================================================================
# TRIANGLE RASTERIZER
# ================================================================

def rasterize_chunk(
    triangles,
    voxel_size,
    xmin,
    ymin,
    zmin,
    nx,
    ny,
    nz
):

    occupied = set()

    add_voxel = occupied.add

    sample_spacing = (
        voxel_size *
        SAMPLE_FACTOR
    )

    inv_voxel = 1.0 / voxel_size


    for tri in triangles:

        ax = tri[0]
        ay = tri[1]
        az = tri[2]

        bx = tri[3]
        by = tri[4]
        bz = tri[5]

        cx = tri[6]
        cy = tri[7]
        cz = tri[8]


        # --------------------------------------------------------
        # EDGE VECTORS
        # --------------------------------------------------------

        abx = bx - ax
        aby = by - ay
        abz = bz - az

        acx = cx - ax
        acy = cy - ay
        acz = cz - az

        bcx = cx - bx
        bcy = cy - by
        bcz = cz - bz


        # --------------------------------------------------------
        # EDGE LENGTHS
        # --------------------------------------------------------

        lab = math.sqrt(
            abx * abx +
            aby * aby +
            abz * abz
        )

        lac = math.sqrt(
            acx * acx +
            acy * acy +
            acz * acz
        )

        lbc = math.sqrt(
            bcx * bcx +
            bcy * bcy +
            bcz * bcz
        )


        longest = max(
            lab,
            lac,
            lbc
        )


        divisions = max(
            1,
            int(
                math.ceil(
                    longest /
                    sample_spacing
                )
            )
        )

        inv_div = 1.0 / divisions


        # ========================================================
        # BARYCENTRIC SAMPLING
        # ========================================================

        for ui in range(divisions + 1):

            u = ui * inv_div

            remaining = divisions - ui

            for vi in range(remaining + 1):

                v = vi * inv_div

                w = 1.0 - u - v


                x = (
                    w * ax +
                    u * bx +
                    v * cx
                )

                y = (
                    w * ay +
                    u * by +
                    v * cy
                )

                z = (
                    w * az +
                    u * bz +
                    v * cz
                )


                i = int(
                    math.floor(
                        (x - xmin) *
                        inv_voxel
                    )
                )

                j = int(
                    math.floor(
                        (y - ymin) *
                        inv_voxel
                    )
                )

                k = int(
                    math.floor(
                        (z - zmin) *
                        inv_voxel
                    )
                )


                if (
                    0 <= i < nx and
                    0 <= j < ny and
                    0 <= k < nz
                ):

                    add_voxel(
                        i +
                        nx *
                        (
                            j +
                            ny * k
                        )
                    )

    return occupied


# ================================================================
# MULTITHREADED TRIANGLE RASTERIZATION
# ================================================================

def rasterize_triangles(
    triangles,
    voxel_size,
    xmin,
    ymin,
    zmin,
    nx,
    ny,
    nz
):

    count = len(triangles)

    worker_count = min(
        WORKERS,
        count
    )

    if worker_count < 1:
        return set()


    chunk_size = int(
        math.ceil(
            float(count) /
            worker_count
        )
    )


    chunks = []

    for start in range(
        0,
        count,
        chunk_size
    ):

        chunks.append(
            triangles[
                start:
                start + chunk_size
            ]
        )


    print("")
    print(
        "Rasterizing with {} workers...".format(
            len(chunks)
        )
    )


    occupied = set()


    with ThreadPoolExecutor(
        max_workers=worker_count
    ) as executor:

        futures = []

        for chunk in chunks:

            futures.append(
                executor.submit(
                    rasterize_chunk,
                    chunk,
                    voxel_size,
                    xmin,
                    ymin,
                    zmin,
                    nx,
                    ny,
                    nz
                )
            )


        for index, future in enumerate(futures):

            result = future.result()

            occupied.update(result)

            print(
                "Worker {} / {} complete   "
                "surface voxels: {:,}".format(
                    index + 1,
                    len(futures),
                    len(occupied)
                )
            )


    return occupied


# ================================================================
# FAST 6-NEIGHBOUR DILATION
# ================================================================

def dilate_voxels(
    occupied,
    nx,
    ny,
    nz,
    steps
):

    if steps <= 0:
        return occupied


    print("")
    print(
        "Expanding voxel shell by {} layer(s)...".format(
            steps
        )
    )


    current = occupied

    layer = nx * ny


    for step in range(steps):

        expanded = set(current)

        add = expanded.add


        for key in current:

            k = key // layer

            rem = key - k * layer

            j = rem // nx

            i = rem - j * nx


            if i > 0:
                add(key - 1)

            if i < nx - 1:
                add(key + 1)

            if j > 0:
                add(key - nx)

            if j < ny - 1:
                add(key + nx)

            if k > 0:
                add(key - layer)

            if k < nz - 1:
                add(key + layer)


        current = expanded


        print(
            "  layer {}: {:,} voxels".format(
                step + 1,
                len(current)
            )
        )


    return current


# ================================================================
# OPTIMIZED OUTPUT MESH
#
# Main improvements:
#
# - final mesh vertex indices are assigned DURING boundary scan
# - no used_vertices set
# - no second vertex-map construction
# - faces already contain final mesh vertex indices
# - Point3f instead of Point3d
# - bulk AddVertices()
# - bulk AddFaces()
# ================================================================

def build_voxel_mesh_fast(
    occupied,
    xmin,
    ymin,
    zmin,
    voxel_size,
    nx,
    ny,
    nz
):

    print("")
    print("Building optimized boundary mesh...")


    total_start = time.time()


    # Grid vertex dimensions
    gvx = nx + 1
    gvy = ny + 1

    gv_layer = gvx * gvy

    voxel_layer = nx * ny


    # ------------------------------------------------------------
    # GRID VERTEX KEY -> FINAL MESH INDEX
    # ------------------------------------------------------------

    vertex_map = {}

    # Store only the grid keys in final vertex-index order.
    vertex_keys = []

    vertex_append = vertex_keys.append


    # Faces already reference final mesh indices.
    face_indices = []

    face_append = face_indices.append


    contains = occupied.__contains__

    get_existing = vertex_map.get


    # ------------------------------------------------------------
    # FAST VERTEX INDEX CREATION
    # ------------------------------------------------------------

    def get_vertex_index(ix, iy, iz):

        grid_key = (
            ix +
            gvx *
            (
                iy +
                gvy * iz
            )
        )

        existing = get_existing(grid_key)

        if existing is not None:
            return existing

        index = len(vertex_keys)

        vertex_map[grid_key] = index

        vertex_append(grid_key)

        return index


    # ============================================================
    # SINGLE BOUNDARY SCAN
    # ============================================================

    boundary_start = time.time()


    for key in occupied:

        k = key // voxel_layer

        rem = key - k * voxel_layer

        j = rem // nx

        i = rem - j * nx


        # ========================================================
        # -X
        # ========================================================

        if (
            i == 0 or
            not contains(key - 1)
        ):

            a = get_vertex_index(
                i,
                j,
                k
            )

            b = get_vertex_index(
                i,
                j,
                k + 1
            )

            c = get_vertex_index(
                i,
                j + 1,
                k + 1
            )

            d = get_vertex_index(
                i,
                j + 1,
                k
            )

            face_append(
                (a, b, c, d)
            )


        # ========================================================
        # +X
        # ========================================================

        if (
            i == nx - 1 or
            not contains(key + 1)
        ):

            a = get_vertex_index(
                i + 1,
                j,
                k
            )

            b = get_vertex_index(
                i + 1,
                j + 1,
                k
            )

            c = get_vertex_index(
                i + 1,
                j + 1,
                k + 1
            )

            d = get_vertex_index(
                i + 1,
                j,
                k + 1
            )

            face_append(
                (a, b, c, d)
            )


        # ========================================================
        # -Y
        # ========================================================

        if (
            j == 0 or
            not contains(key - nx)
        ):

            a = get_vertex_index(
                i,
                j,
                k
            )

            b = get_vertex_index(
                i + 1,
                j,
                k
            )

            c = get_vertex_index(
                i + 1,
                j,
                k + 1
            )

            d = get_vertex_index(
                i,
                j,
                k + 1
            )

            face_append(
                (a, b, c, d)
            )


        # ========================================================
        # +Y
        # ========================================================

        if (
            j == ny - 1 or
            not contains(key + nx)
        ):

            a = get_vertex_index(
                i,
                j + 1,
                k
            )

            b = get_vertex_index(
                i,
                j + 1,
                k + 1
            )

            c = get_vertex_index(
                i + 1,
                j + 1,
                k + 1
            )

            d = get_vertex_index(
                i + 1,
                j + 1,
                k
            )

            face_append(
                (a, b, c, d)
            )


        # ========================================================
        # -Z
        # ========================================================

        if (
            k == 0 or
            not contains(
                key - voxel_layer
            )
        ):

            a = get_vertex_index(
                i,
                j,
                k
            )

            b = get_vertex_index(
                i,
                j + 1,
                k
            )

            c = get_vertex_index(
                i + 1,
                j + 1,
                k
            )

            d = get_vertex_index(
                i + 1,
                j,
                k
            )

            face_append(
                (a, b, c, d)
            )


        # ========================================================
        # +Z
        # ========================================================

        if (
            k == nz - 1 or
            not contains(
                key + voxel_layer
            )
        ):

            a = get_vertex_index(
                i,
                j,
                k + 1
            )

            b = get_vertex_index(
                i + 1,
                j,
                k + 1
            )

            c = get_vertex_index(
                i + 1,
                j + 1,
                k + 1
            )

            d = get_vertex_index(
                i,
                j + 1,
                k + 1
            )

            face_append(
                (a, b, c, d)
            )


    boundary_time = (
        time.time() -
        boundary_start
    )


    print(
        "Boundary scan   : {:.2f} sec".format(
            boundary_time
        )
    )

    print(
        "Vertices        : {:,}".format(
            len(vertex_keys)
        )
    )

    print(
        "Exposed faces   : {:,}".format(
            len(face_indices)
        )
    )


    # vertex_map no longer needed
    vertex_map = None


    # ============================================================
    # CREATE Point3f ARRAY
    # ============================================================

    print("")
    print("Preparing compact vertex array...")


    vertex_start = time.time()


    points = List[
        Rhino.Geometry.Point3f
    ]()

    points.Capacity = len(
        vertex_keys
    )


    point_add = points.Add


    # Cache locals
    sx = float(voxel_size)

    ox = float(xmin)
    oy = float(ymin)
    oz = float(zmin)


    # Decode grid keys directly
    for key in vertex_keys:

        iz = key // gv_layer

        rem = key - iz * gv_layer

        iy = rem // gvx

        ix = rem - iy * gvx


        point_add(
            Rhino.Geometry.Point3f(
                float(
                    ox +
                    ix * sx
                ),
                float(
                    oy +
                    iy * sx
                ),
                float(
                    oz +
                    iz * sx
                )
            )
        )


    vertex_keys = None


    vertex_time = (
        time.time() -
        vertex_start
    )


    print(
        "Vertex array    : {:.2f} sec".format(
            vertex_time
        )
    )


    # ============================================================
    # CREATE MeshFace ARRAY
    # ============================================================

    print("")
    print("Preparing face array...")


    face_start = time.time()


    mesh_faces = List[
        Rhino.Geometry.MeshFace
    ]()

    mesh_faces.Capacity = len(
        face_indices
    )


    face_add = mesh_faces.Add


    for a, b, c, d in face_indices:

        face_add(
            Rhino.Geometry.MeshFace(
                a,
                b,
                c,
                d
            )
        )


    face_indices = None


    face_time = (
        time.time() -
        face_start
    )


    print(
        "Face array      : {:.2f} sec".format(
            face_time
        )
    )


    # ============================================================
    # BULK CREATE RHINO MESH
    # ============================================================

    print("")
    print("Creating Rhino mesh...")


    rhino_start = time.time()


    result = Rhino.Geometry.Mesh()


    result.Vertices.Capacity = (
        points.Count
    )

    result.Faces.Capacity = (
        mesh_faces.Count
    )


    result.Vertices.AddVertices(
        points
    )

    result.Faces.AddFaces(
        mesh_faces
    )


    points = None
    mesh_faces = None


    result.FaceNormals.ComputeFaceNormals()

    result.Normals.ComputeNormals()

    result.Compact()


    rhino_time = (
        time.time() -
        rhino_start
    )


    print(
        "Rhino mesh      : {:.2f} sec".format(
            rhino_time
        )
    )


    print(
        "Output building : {:.2f} sec".format(
            time.time() -
            total_start
        )
    )


    return result


# ================================================================
# MAIN
# ================================================================

def main():

    ids = get_mesh_ids()

    if not ids:
        return


    voxel_size = rs.GetReal(
        "VoxelSize",
        DEFAULT_VOXEL_SIZE,
        0.000001
    )

    if voxel_size is None:
        return


    start_time = time.time()

    old_redraw = rs.EnableRedraw(False)


    try:

        print("")
        print("FAST VOXEL RASTERIZER")
        print("=====================")

        print(
            "Meshes      : {}".format(
                len(ids)
            )
        )

        print(
            "VoxelSize   : {}".format(
                voxel_size
            )
        )

        print(
            "Workers     : {}".format(
                WORKERS
            )
        )

        print(
            "Sample step : {:.3f}".format(
                voxel_size *
                SAMPLE_FACTOR
            )
        )


        # ========================================================
        # COMBINE
        # ========================================================

        print("")
        print("Combining meshes...")


        mesh = combine_meshes(
            ids
        )


        if mesh.Faces.Count == 0:

            print(
                "No valid mesh faces."
            )

            return


        print(
            "Triangles: {:,}".format(
                mesh.Faces.Count
            )
        )


        # ========================================================
        # BOUNDING BOX
        # ========================================================

        bbox = mesh.GetBoundingBox(
            True
        )


        padding = (
            voxel_size *
            PADDING_VOXELS
        )


        xmin = (
            bbox.Min.X -
            padding
        )

        ymin = (
            bbox.Min.Y -
            padding
        )

        zmin = (
            bbox.Min.Z -
            padding
        )


        xmax = (
            bbox.Max.X +
            padding
        )

        ymax = (
            bbox.Max.Y +
            padding
        )

        zmax = (
            bbox.Max.Z +
            padding
        )


        nx = max(
            1,
            int(
                math.ceil(
                    (
                        xmax -
                        xmin
                    ) /
                    voxel_size
                )
            )
        )


        ny = max(
            1,
            int(
                math.ceil(
                    (
                        ymax -
                        ymin
                    ) /
                    voxel_size
                )
            )
        )


        nz = max(
            1,
            int(
                math.ceil(
                    (
                        zmax -
                        zmin
                    ) /
                    voxel_size
                )
            )
        )


        print("")

        print(
            "Grid: {} x {} x {}".format(
                nx,
                ny,
                nz
            )
        )


        print(
            "Full grid would contain {:,} voxels".format(
                nx *
                ny *
                nz
            )
        )


        # ========================================================
        # EXTRACT TRIANGLES
        # ========================================================

        print("")
        print(
            "Extracting triangle coordinates..."
        )


        triangles = extract_triangles(
            mesh
        )


        mesh = None


        # ========================================================
        # RASTERIZATION
        # ========================================================

        surface_voxels = rasterize_triangles(
            triangles,
            voxel_size,
            xmin,
            ymin,
            zmin,
            nx,
            ny,
            nz
        )


        triangles = None


        print("")

        print(
            "Initial surface voxels: {:,}".format(
                len(
                    surface_voxels
                )
            )
        )


        if not surface_voxels:

            print(
                "No voxels generated."
            )

            return


        # ========================================================
        # DILATION
        # ========================================================

        occupied = dilate_voxels(
            surface_voxels,
            nx,
            ny,
            nz,
            DILATION_STEPS
        )


        surface_voxels = None


        occupied_count = len(
            occupied
        )


        print("")

        print(
            "Final occupied voxels: {:,}".format(
                occupied_count
            )
        )


        # ========================================================
        # OUTPUT
        # ========================================================

        output = build_voxel_mesh_fast(
            occupied,
            xmin,
            ymin,
            zmin,
            voxel_size,
            nx,
            ny,
            nz
        )


        occupied = None


        # ========================================================
        # ADD TO RHINO
        # ========================================================

        print("")
        print(
            "Adding result to Rhino..."
        )


        result_id = sc.doc.Objects.AddMesh(
            output
        )


        if (
            result_id ==
            System.Guid.Empty
        ):

            print(
                "Could not add output mesh."
            )

            return


        elapsed = (
            time.time() -
            start_time
        )


        # ========================================================
        # REPORT
        # ========================================================

        print("")
        print("DONE")
        print("====")


        print(
            "Occupied voxels : {:,}".format(
                occupied_count
            )
        )


        print(
            "Vertices        : {:,}".format(
                output.Vertices.Count
            )
        )


        print(
            "Faces           : {:,}".format(
                output.Faces.Count
            )
        )


        print(
            "Closed          : {}".format(
                output.IsClosed
            )
        )


        print(
            "Valid           : {}".format(
                output.IsValid
            )
        )


        print(
            "TOTAL TIME      : {:.2f} sec".format(
                elapsed
            )
        )


    finally:

        rs.EnableRedraw(
            old_redraw
        )

        sc.doc.Views.Redraw()


if __name__ == "__main__":
    main()
2 Likes