Skip to content

Polygonization

In this workshop we will learn how to convert our topological model of the building into a geometric model. This script turns a topological model of a building into a geometric model.

As input it takes an envelope lattice.

In chapter 0 and 1 you can create symmetry stencills, and save unique profile lattices. In chapter 2 you can load a sub-tile set and generate the complete tile set.

In chapter 3 you can, using the boolean marching cube algorithm create your geometry based on the tileset(s)

chapter 3 can run seperatly without chapter 0,1 and 2

It is possible to use stencills to select different parts of the building and run a different tile set on each stencill. In this code, there is a different tileset for the roof,groundfloor, facade in y direction, and 4 for the variation in the x direction.

As output this script generates a meshed geometry. Unfortunately without texture.

0. Initialization

0.0. Importing libraries

import topogenesis as tg
import numpy as np 
import pyvista as pv
import os
import copy
import trimesh as tm
import pandas as pd
import resources.boolean_marching_cubes as bmc

0.1. Generate Symmetry Stencils

sym_str = [["OO"], ["XX"], ["YY"], ["ZP"], ["ZN"]]
stencils = bmc.create_symmetry_stencils(sym_str)

for s in stencils:
    print(s)
    print("-----")

0.2. Generate lattices for all possible cubes

# generate bianary representation of all the possible cubes
l_bis = bmc.bi_cube_lattices()
l_bis[3]

1. Profiling

1.1. Catalogue the profile of all corners

# find all unique corner arrangements based on stencils
corner_profiles = bmc.extract_corner_profiles(stencils, l_bis)

1.2. Find unique corner profiles

# stack corner_profiles vertically
cp_stacked = np.vstack(corner_profiles)

# find the uniqe arangements of corners
uniq_corner_arang = np.unique(cp_stacked, axis=0)

print(cp_stacked)
#print(uniq_corner_arang)
#print(len(uniq_corner_arang))
print(len(cp_stacked))

1.3. Construct unique profile latices

# construct lattices for all unique corner profiles
(corner_loc_lattices, corner_neigh_lattices) = bmc.profiles_to_lattices(uniq_corner_arang, stencils)

1.4. Visualize unique profiles

p = pv.Plotter(notebook=True)

base_lattice = corner_neigh_lattices[0]

# Set the grid dimensions: shape + 1 because we want to inject our values on the CELL data
grid = pv.UniformGrid()
grid.dimensions = np.array(base_lattice.shape) + 1
# The bottom left corner of the data set
grid.origin = base_lattice.minbound - base_lattice.unit * 0.5
# These are the cell sizes along each axis
grid.spacing = base_lattice.unit 

# adding the boundingbox wireframe
p.add_mesh(grid.outline(), color="grey", label="Domain")

# adding axes
p.add_axes()
p.show_bounds(grid="back", location="back", color="#aaaaaa")

def create_mesh(value):
    f = int(value)
    lattice = corner_neigh_lattices[f]
    loc = corner_loc_lattices[f]

    # Add the data values to the cell data
    grid.cell_arrays["filled"] = lattice.flatten(order="F").astype(int)  # Flatten the array!
    # filtering the voxels
    threshed = grid.threshold([.9, 1.1], scalars="filled")
    # adding the voxels
    p.add_mesh(threshed, name='sphere', show_edges=True, opacity=0.7, show_scalar_bar=False)

    # Add the data values to the cell data
    grid.cell_arrays["corner"] = loc.flatten(order="F").astype(int)# Flatten the array!
    # filtering the voxels
    threshed = grid.threshold([.9, 1.1], scalars="corner")
    # adding the voxels
    p.add_mesh(threshed, name='sphere2', show_edges=True, opacity=1.0, show_scalar_bar=False, color="white")

    return

p.add_slider_widget(create_mesh, [1, len(corner_neigh_lattices)], title='Arrangements', value=0, event_type="always", style="classic", pointa=(0.1, 0.1), pointb=(0.9, 0.1))
p.show(use_ipyvtk=True)

1.5. Save unique arrangement profiles

# save all design templates into lattice CSVs
templates_path = os.path.relpath('../data/bmc/bmc_templates')
bmc.save_design_templates(corner_loc_lattices, corner_neigh_lattices, templates_path)

2. Construct the tile-set

2.1. Load sub-tile meshes

# load subtile meshes
subtile_meshes = []
for c in range(len(corner_loc_lattices)):
    corner_mesh_path = os.path.relpath('../data/bmc/balcony_top/t_' + f'{c:02}' + '.obj')
    corner_mesh = tm.load(corner_mesh_path)
    subtile_meshes.append(corner_mesh)

2.2. Combine sub-tile meshes to create tile meshes

tiles_meshes = bmc.construct_tile_meshes(subtile_meshes, corner_profiles, uniq_corner_arang, corner_loc_lattices)

2.3. Visualize tile meshes

# convert mesh to pv_mesh
def tri_to_pv(tri_mesh):
    faces = np.pad(tri_mesh.faces, ((0, 0),(1,0)), 'constant', constant_values=3)
    pv_mesh = pv.PolyData(tri_mesh.vertices, faces)
    return pv_mesh

p = pv.Plotter(notebook=True)

base_lattice = l_bis[0]

# Set the grid dimensions: shape + 1 because we want to inject our values on the CELL data
grid = pv.UniformGrid()
grid.dimensions = np.array(base_lattice.shape) + 1
# The bottom left cornerempty of the data set
grid.origin = base_lattice.minbound - base_lattice.unit * 0.5 
# These are the cell sizes along each axis
grid.spacing = base_lattice.unit *0.5

# adding the boundingbox wireframe
p.add_mesh(grid.outline(), color="grey", label="Domain")

# adding axes
p.add_axes()
p.show_bounds(grid="back", location="back", color="#aaaaaa")

def create_mesh(value):
    i = int(value)
    mesh = tiles_meshes[i]
    lattice = l_bis[i]

    # Add the data values to the cell data
    grid.cell_arrays["cube"] = lattice.flatten(order="F").astype(int)# Flatten the array!
    # filtering the voxels
    threshed = grid.threshold([.9, 1.1], scalars="cube")
    # adding the voxels
    p.add_mesh(threshed, name='sphere2', show_edges=True, opacity=0.2, show_scalar_bar=False, color="white")

    # adding the meshes
    p.add_mesh(tri_to_pv(mesh), color='#abd8ff', name="sphere")

    return

p.add_slider_widget(create_mesh, [0, len(tiles_meshes)], title='Tiles', value=1, event_type="always", style="classic", pointa=(0.1, 0.1), pointb=(0.9, 0.1))

p.show(use_ipyvtk=True)

2.4. Save the tile-set

tiles_path = os.path.relpath('../data/bmc/bmc_tiles_balconytop')
bmc.save_tile_meshes(tiles_meshes, l_bis, tiles_path)
import topogenesis as tg
import numpy as np 
import pyvista as pv
import os
import copy
import trimesh as tm
import pandas as pd
import resources.boolean_marching_cubes as bmc

3. Boolean Marching Cube

3.1. Load envelope lattice

# loading the lattice from csv
lattice_path = os.path.relpath('../data/final_lattice_ihope_x2PLS.csv')

envelope_lattice_0 = tg.lattice_from_csv(lattice_path)

Removing the interior voxels

Clean up the geometry, make the script run smoother

# create the stencil
s = tg.create_stencil("von_neumann", 1, 1)
s.set_index([0,0,0], 0)

# add the sum function to the stencil
s.function = tg.sfunc.sum 

# apply the stencil on the lattice
neighbor_sum = envelope_lattice_0.apply_stencil(s)

# remove if less than 6
new_envelope_lattice = envelope_lattice_0 * (neighbor_sum <= 5)

new_envelope_lattice_expanded = np.pad(new_envelope_lattice, ((1,1),(1,1),(1, 1)), 'constant', constant_values=0)
new_envelope_lattice_expanded = tg.to_lattice(new_envelope_lattice_expanded, new_envelope_lattice.minbound - new_envelope_lattice.unit, new_envelope_lattice.unit)

Creating the roof lattice

s = tg.create_stencil("von_neumann", 0, 2)
s.set_index([0,0,0], 0)
s.set_index([0,0,1], 1)
s.set_index([0,0,2], 1)

s.function = tg.sfunc.sum

upsum_lattice = envelope_lattice_0.apply_stencil(s)
roof_lattice = (upsum_lattice == 0) * envelope_lattice_0

roof_lattice_expanded = np.pad(roof_lattice, ((1,1),(1,1),(1, 1)), 'constant', constant_values=0)
roof_lattice_expanded = tg.to_lattice(roof_lattice_expanded, roof_lattice.minbound - roof_lattice.unit, roof_lattice.unit)
cube_roof_lattice = roof_lattice.boolean_marching_cubes()

Selecting singular facade

#every 2 other floor
avail_index = np.array(np.where(new_envelope_lattice == 1)).T

for vox_1d_ind in avail_index:
    vox_3d_ind = np.unravel_index(vox_1d_ind, new_envelope_lattice.shape)
    VX, VY, VZ = vox_3d_ind


regular_facade = new_envelope_lattice * 0
regular_facade[1,:,:] = 1
regular_facade[2,15:50,:] = 1
regular_facade[3,31:50,:] = 1
regular_facade[83,:,:] = 1

regular_facade *= new_envelope_lattice

regular_facade_expanded = np.pad(regular_facade, ((1,1),(1,1),(1, 1)), 'constant', constant_values=0)
regular_facade_expanded = tg.to_lattice(regular_facade_expanded, regular_facade.minbound - regular_facade.unit, regular_facade.unit)

Selecting Ground Floor

# Finding the index of the available voxels in avail_lattice
avail_index = np.array(np.where(new_envelope_lattice == 1)).T

for vox_1d_ind in avail_index:
    vox_3d_ind = np.unravel_index(vox_1d_ind, new_envelope_lattice.shape)
    VX, VY, VZ = vox_3d_ind

# Ground Floor
ground_floor_lattice = new_envelope_lattice * 0
ground_floor_lattice[:,:,:4] = 1
ground_floor_lattice *= new_envelope_lattice

ground_floor_lattice_expanded = np.pad(ground_floor_lattice, ((1,1),(1,1),(1, 1)), 'constant', constant_values=0)
ground_floor_lattice_expanded = tg.to_lattice(ground_floor_lattice_expanded, ground_floor_lattice.minbound - ground_floor_lattice.unit, ground_floor_lattice.unit )
envelope_lattice = new_envelope_lattice_expanded - ground_floor_lattice_expanded - regular_facade_expanded - roof_lattice_expanded
regular_facade_expanded = regular_facade_expanded - ground_floor_lattice_expanded
ground_floor_cube_lattice= ground_floor_lattice_expanded.boolean_marching_cubes()
regular_facade_cube = regular_facade_expanded.boolean_marching_cubes()

3.2. Extract the cube lattice from the envelope lattice

Facade part 1

repeats on the 1st ::4 and 2nd::4 floor and in the x direction as 2::3

repeats on the 3rd::4 and 4th::4 floor and in the x direction as 0::3

avail_index = np.array(np.where(envelope_lattice == 1)).T

for vox_1d_ind in avail_index:
    vox_3d_ind = np.unravel_index(vox_1d_ind, envelope_lattice.shape)
    VX, VY, VZ = vox_3d_ind

facade_part_1 = envelope_lattice * 0
facade_part_1[2::3,:,1::4] = 1
facade_part_1[2::3,:,2::4] = 1
facade_part_1[0::3,:,3::4] = 1
facade_part_1[0::3,:,4::4] = 1


facade_part_1 *= envelope_lattice

facade_part_1_expanded = np.pad(facade_part_1, ((1,1),(1,1),(1, 1)), 'constant', constant_values=0)
facade_part_1_expanded = tg.to_lattice(facade_part_1_expanded, facade_part_1.minbound - facade_part_1.unit, facade_part_1.unit)
facade_1_cube  = facade_part_1_expanded.boolean_marching_cubes() 

Facade part 2

repeats on the 3st ::4 floor and in the x direction as 1::3

repeats on the 1st::4 floor and in the x direction as 3::3

avail_index = np.array(np.where(envelope_lattice == 1)).T

for vox_1d_ind in avail_index:
    vox_3d_ind = np.unravel_index(vox_1d_ind, envelope_lattice.shape)
    VX, VY, VZ = vox_3d_ind

facade_part_2 = envelope_lattice * 0
facade_part_2[3::3,:,1::4] = 1
facade_part_2[1::3,:,3::4] = 1

facade_part_2 *= envelope_lattice

facade_part_2_expanded = np.pad(facade_part_2, ((1,1),(1,1),(1, 1)), 'constant', constant_values=0)
facade_part_2_expanded = tg.to_lattice(facade_part_2_expanded, facade_part_2.minbound - facade_part_2.unit, facade_part_2.unit)
facade_2_cube  = facade_part_2_expanded.boolean_marching_cubes()

Facade part 3

repeats on the 4rd ::4 floor and in the x direction as 1::3

repeats on the 2nd::4 floor and in the x direction as 3::3

avail_index = np.array(np.where(envelope_lattice == 1)).T

for vox_1d_ind in avail_index:
    vox_3d_ind = np.unravel_index(vox_1d_ind, envelope_lattice.shape)
    VX, VY, VZ = vox_3d_ind

facade_part_3 = envelope_lattice * 0
facade_part_3[1::3,:,4::4] = 1
facade_part_3[3::3,:,2::4] = 1

facade_part_3 *= envelope_lattice

facade_part_3_expanded = np.pad(facade_part_3, ((1,1),(1,1),(1, 1)), 'constant', constant_values=0)
facade_part_3_expanded = tg.to_lattice(facade_part_3_expanded, facade_part_3.minbound - facade_part_3.unit, facade_part_3.unit)
facade_3_cube  = facade_part_3_expanded.boolean_marching_cubes()

Facade part 4

repeats on the 1st::4 and 2nd::4 floor and in the x direction as 1::3

repeats on the 3rd::4 and 4th::4 floor and in the x direction as 2::3

avail_index = np.array(np.where(envelope_lattice == 1)).T

for vox_1d_ind in avail_index:
    vox_3d_ind = np.unravel_index(vox_1d_ind, envelope_lattice.shape)
    VX, VY, VZ = vox_3d_ind

facade_part_4 = envelope_lattice * 0
facade_part_4[1::3,:,1::4] = 1
facade_part_4[1::3,:,2::4] = 1
facade_part_4[2::3,:,3::4] = 1
facade_part_4[2::3,:,4::4] = 1

facade_part_4 *= envelope_lattice

facade_part_4_expanded = np.pad(facade_part_4, ((1,1),(1,1),(1, 1)), 'constant', constant_values=0)
facade_part_4_expanded = tg.to_lattice(facade_part_4_expanded, facade_part_4.minbound - facade_part_4.unit, facade_part_4.unit)   
facade_4_cube  = facade_part_4_expanded.boolean_marching_cubes()

3.3. tile the cube lattice with a tileset

#loading the tilepaths
groundfloor_tiles_path = os.path.relpath('../data/bmc/bmc_tiles_groundfloor')
roof_tiles_path = os.path.relpath('../data/bmc/bmc_tiles_roof')
doublewindow_tiles_path = os.path.relpath('../data/bmc/bmc_tiles_doublewindow')
balconybottom_tiles_path = os.path.relpath('../data/bmc/bmc_tiles_balconybottom')
balconytop_tiles_path = os.path.relpath('../data/bmc/bmc_tiles_balconytop')
flat_tiles_path = os.path.relpath('../data/bmc/bmc_flat_tiles')
regular_facade_path = os.path.relpath('../data/bmc/bmc_tiles_regular_facade')
#doing the boolean marching cube algorithm per building part with its desired tilepath
#these are split, to save RAM. 
bmc_mesh_1 = bmc.marching_cube_mesh(ground_floor_cube_lattice, groundfloor_tiles_path)
bmc_mesh_2 = bmc.marching_cube_mesh(cube_roof_lattice, roof_tiles_path)
bmc_mesh_3 = bmc.marching_cube_mesh(facade_1_cube, doublewindow_tiles_path)
bmc_mesh_4 = bmc.marching_cube_mesh(facade_2_cube, balconybottom_tiles_path)
bmc_mesh_5 = bmc.marching_cube_mesh(facade_3_cube, balconytop_tiles_path)
bmc_mesh_6 = bmc.marching_cube_mesh(facade_4_cube, flat_tiles_path)
bmc_mesh_7 = bmc.marching_cube_mesh(regular_facade_cube, regular_facade_path)

#adding the boolean marching cubes together
bmc_mesh = bmc_mesh_1 + bmc_mesh_2 + bmc_mesh_3 + bmc_mesh_4  + bmc_mesh_5 + bmc_mesh_6 + bmc_mesh_7

3.4. Visualize the final mesh

# convert mesh to pv_mesh
def tri_to_pv(tri_mesh):
    faces = np.pad(tri_mesh.faces, ((0, 0),(1,0)), 'constant', constant_values=3)
    pv_mesh = pv.PolyData(tri_mesh.vertices, faces)
    return pv_mesh

# initiating the plotter
p = pv.Plotter(notebook=True)

# adding the meshes
p.add_mesh(tri_to_pv(bmc_mesh), color='white', opacity=1 , name="sphere")

# adding axes
p.add_axes()
p.show_bounds(grid="back", location="back", color="#aaaaaa")

# fast visualization of the lattice
p = envelope_lattice_0.fast_vis(p)

# plotting
p.show(use_ipyvtk=True)

# png_path = os.path.relpath('../screen/finalmodel.png')
# p.show(screenshot = png_path)

3.5. Save the final mesh

#Saving the generated geometry as a mesh. 
final_mesh_path = os.path.relpath('../data/final_mesh.obj')

with open(final_mesh_path, 'w') as file:
        file.write(tm.exchange.obj.export_obj(bmc_mesh))

Credits

__author__ = "Shervin Azadi"
__editro__ = 'Siebren Meines'
__license__ = "MIT"
__version__ = "1.0"
__url__ = "https://github.com/shervinazadi/spatial_computing_workshops"
__summary__ = "Spatial Computing Design Studio Workshop on Polygonization"