Skip to content

Polygonization

In this notebook we will import a subtile set, which is based on a certain symmetry stencil. With this subtile set we will create a tile set containing 256 tiles. With these tiles the boolean marching cube algorithm will create a facade for a few lattices.

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
from scipy.spatial import KDTree
np.random.seed(0)

0.1. Generate Symmetry Stencils

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

0.2. Generate lattices for all possible cubes

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

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)

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=1, event_type="always", style="classic", pointa=(0.1, 0.1), pointb=(0.9, 0.1))
p.show(use_ipyvtk=True)
[(4.363703305156274, 4.363703305156274, 4.363703305156274),
 (0.5, 0.5, 0.5),
 (0.0, 0.0, 1.0)]

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/bmc_subtiles/t_' + f'{c:02}' + '.obj')
    corner_mesh = tm.load(corner_mesh_path)
    subtile_meshes.append(corner_mesh)
faces have mixed data, using slow fallback!
faces have mixed data, using slow fallback!

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 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 *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)
[(1.9318516525781368, 1.9318516525781368, 1.9318516525781368),
 (0.0, 0.0, 0.0),
 (0.0, 0.0, 1.0)]

2.4. Save the tile-set

tiles_path = os.path.relpath('../data/bmc/bmc_tiles_new')
bmc.save_tile_meshes(tiles_meshes, l_bis, tiles_path)

3. Boolean Marching Cube

3.1. Load lattices

# extra import function
def lattice_from_csv(file_path):
    # read metadata
    meta_df = pd.read_csv(file_path, nrows=3)

    shape = np.array(meta_df['shape'])
    unit = np.array(meta_df['unit'])
    minbound = np.array(meta_df['minbound'])

    # read lattice
    lattice_df = pd.read_csv(file_path, skiprows=5)

    # create the buffer
    buffer = np.array(lattice_df['value']).reshape(shape)

    # create the lattice
    l = tg.to_lattice(buffer, minbound=minbound, unit=unit)

    return l

# loading the lattice from csv
lattice_path = os.path.relpath('../data/final_envelope_new.csv')
envelope_lattice = tg.lattice_from_csv(lattice_path)

#loading the lattice from csv
inner_part_path = os.path.relpath('../data/negative_opening.csv')
inner_part_lattice = lattice_from_csv(inner_part_path)

3.2 Load environment

# loading the lattice from csv
sun_acc_path = os.path.relpath('../data/sun_access_highres.csv')
sun_acc_lattice = lattice_from_csv(sun_acc_path)

# list the environment information layers (lattices)
# the order should match the program matrix
env_info = [sun_acc_lattice]

3.3 Creating lattices

3.3.1 Facade lattice

# 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.apply_stencil(s)

# remove if 6
facade_lattice = envelope_lattice * (neighbor_sum <= 5)

3.3.2 Ground Floor Lattice

# Finding the index of the available voxels in avail_lattice
avail_index = np.array(np.where(facade_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


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

3.3.3 Inner part lattice

# Inner part lattice
g_0 = np.roll(inner_part_lattice, [0,2,0], [0,1,1])
g_1 = np.roll(inner_part_lattice, [0,-2,0], [0,1,1])
g_2 = np.roll(inner_part_lattice, [2,0,0], [0,1,1])
g_3 = np.roll(inner_part_lattice, [-2,0,0], [0,1,1])

inner_part_lattice = facade_lattice * (g_0 + g_1 + g_2 + g_3)
inner_part_lattice[inner_part_lattice>1] = 1

3.3.4 Main outer part lattice

# Main outer lattice by substracting the previous lattices from the facade lattice
main_outer_lattice = facade_lattice - ground_floor_lattice - inner_part_lattice

3.3.5 Balcony lattice

main_outer_flat = main_outer_lattice.flatten()
main_outer_index = np.array(np.where(main_outer_lattice == 1)).T

balcony_lattice = main_outer_lattice * 0 - 1

# Randomly choosing three available voxels
balcony_num = 50
select_id = np.random.choice(len(main_outer_index), balcony_num)

balc_origins = []
for id in select_id:
    balc_origins.append(main_outer_index[id])

# adding the origins to the agents locations
balc_locs = []
# for each agent origin ... 
for a_origin in balc_origins:

    # add the origin to the list of agent locations
    balc_locs.append([a_origin])

    # set the origin in availablity lattice as 0 (UNavailable)
    main_outer_lattice[tuple(a_origin)] = False

    # set the origin in occupation lattice as the agent id (a_id)
    balcony_lattice[tuple(a_origin)] = 1

3.3.extract cube lattice

# Ground Floor
ground_floor_cube_lattice = ground_floor_lattice.boolean_marching_cubes()

# Opening
inner_part_cube_lattice = inner_part_lattice.boolean_marching_cubes()

# Main outer 
main_outer_cube_lattice = main_outer_lattice.boolean_marching_cubes()

# Balcony
balcony_cube_lattice = balcony_lattice.boolean_marching_cubes()

3.4 Load the tile sets

Here we load all the tile sets. In the first part of this notebook we locally have saved, for every tile sets, two materials. Therefor we can now load all the materials seperately so it will be easier to model textures.

# Main outer part facade
tiles_path_main_outer = os.path.relpath('../data/bmc/bmc_tiles_main')
# Brick
tiles_path_main_outer_brick = os.path.relpath('../data/bmc/bmc_tiles_main_brick')
# Glass
tiles_path_main_outer_glass = os.path.relpath('../data/bmc/bmc_tiles_main_glass')

# Ground floor facade
tiles_path_ground_floor = os.path.relpath('../data/bmc/bmc_tiles_ground_floor')
# Brick
tiles_path_ground_floor_brick = os.path.relpath('../data/bmc/bmc_tiles_ground_floor_brick')
# Glass
tiles_path_ground_floor_glass = os.path.relpath('../data/bmc/bmc_tiles_ground_floor_glass')

# Balony 
tiles_path_balcony = os.path.relpath('../data/bmc/bmc_tiles_balcony')
# Brick
tiles_path_balcony_brick = os.path.relpath('../data/bmc/bmc_tiles_balcony_brick')
# Glass
tiles_path_balcony_glass = os.path.relpath('../data/bmc/bmc_tiles_balcony_glass')

# Inner part facade
tiles_path_inner_part = os.path.relpath('../data/bmc/bmc_tiles_inner')
# Metal
tiles_path_inner_part_metal = os.path.relpath('../data/bmc/bmc_tiles_inner_metal')
# Glass
tiles_path_inner_part_glass = os.path.relpath('../data/bmc/bmc_tiles_inner_glass')

3.5 Boolean marching cubes

# Ground Floor
bmc_mesh_ground_floor = bmc.marching_cube_mesh(ground_floor_cube_lattice, tiles_path_ground_floor)
# Brick
bmc_mesh_ground_floor_brick = bmc.marching_cube_mesh(ground_floor_cube_lattice, tiles_path_ground_floor_brick) 
# Glass
bmc_mesh_ground_floor_glass = bmc.marching_cube_mesh(ground_floor_cube_lattice, tiles_path_ground_floor_glass) 

# Inner part
bmc_mesh_inner = bmc.marching_cube_mesh(inner_part_cube_lattice, tiles_path_inner_part) 
# Metal
bmc_mesh_inner_metal = bmc.marching_cube_mesh(inner_part_cube_lattice, tiles_path_inner_part_metal) 
# Glass
bmc_mesh_inner_glass = bmc.marching_cube_mesh(inner_part_cube_lattice, tiles_path_inner_part_glass) 

# # Balcony
# bmc_mesh_balcony = bmc.marching_cube_mesh(balcony_cube_lattice, tiles_path_balcony)
# # Brick
# bmc_mesh_balcony_brick = bmc.marching_cube_mesh(balcony_cube_lattice, tiles_path_balcony_brick)
# # Glass
# bmc_mesh_balcony_glass = bmc.marching_cube_mesh(balcony_cube_lattice, tiles_path_balcony_glass)

# Main outer part
bmc_mesh_main_outer = bmc.marching_cube_mesh(main_outer_cube_lattice, tiles_path_main_outer) 
# Brick
bmc_mesh_main_outer_brick = bmc.marching_cube_mesh(main_outer_cube_lattice, tiles_path_main_outer_brick) 
# Glass
bmc_mesh_main_outer_glass = bmc.marching_cube_mesh(main_outer_cube_lattice, tiles_path_main_outer_glass) 

# Final
bmc_mesh_final = bmc_mesh_ground_floor + bmc_mesh_main_outer + bmc_mesh_inner

3.6. Visualize the final mesh

# convert mesh to pv_mesh
def tri_to_pv(tri_mesh):
    faces = np.pad(tri_mesh.faces, ((0, 0),(0,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_final), color='#abd8ff', name="sphere")

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

# plotting
p.show(use_ipyvtk=True)

3.7. Save the final 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_final))

Credits

__author__ = "Shervin Azadi"
__changes_made_by__ = "Frank Vahstal"
__license__ = "MIT"
__version__ = "1.0"
__url__ = "https://github.com/frankvahstal/spatial_computing_workshops"
__summary__ = "Spatial Computing Design Studio Workshop on Polygonization"