diff --git a/README.md b/README.md index 05da7e5..b5d99a2 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,11 @@ pygltflib ``` ## Usage: -`python3 gds2gltf.py file.gds` +**For the SKY130 PDK**
+`python3 gds2gltf_sky130.py file.gds` + +**For the GF180 PDK**
+`python3 gds2gltf_gf180.py file.gds` Outputs a `file.gds.gltf` in the same folder as the original gds file diff --git a/gds2gltf_gf180.py b/gds2gltf_gf180.py new file mode 100644 index 0000000..508ccec --- /dev/null +++ b/gds2gltf_gf180.py @@ -0,0 +1,443 @@ +#!/usr/bin/env python3 +""" +This program converts a GDSII 2D layout file to a glTF 3D file + +USAGE: + - edit the "layerstack" variable in the "CONFIGURATION" section below + - run "python3 gds2gtlf_gf180.py file.gds" +OUTPUT: + - the files file.gds.gltf + +The program takes one argument, a path to a GDSII file. It reads shapes from +each layer of the GDSII file, converts them to polygon boundaries, then makes +a triangle mesh for each GDSII layer by extruding the polygons to given sizes. + +All units, including the units of the exported file, are the GDSII file's +user units (often microns). + +Contribution from © ChipInventor by: + Lucas Daudt Franck + William Carrara Orlato + +""" + +import sys # read command-line arguments +import gdspy # open gds file +import numpy as np # fast math on lots of points +import triangle # triangulate polygons + + + +import pygltflib +from pygltflib import BufferFormat +from pygltflib.validator import validate, summary + +# get the input file name +if len(sys.argv) < 2: # sys.argv[0] is the name of the program + print("Error: need exactly one file as a command line argument.") + sys.exit(0) +gdsii_file_path = sys.argv[1] + +########## CONFIGURATION (EDIT THIS PART) ##################################### + +# choose which GDSII layers to use + +layerstack = { + (0,0): {'name':'substrate', 'zmin':-2, 'zmax':0, 'color':[ 0.2, 0.2, 0.2, 1.0]}, + (21,0): {'name':'nwell', 'zmin':-0.5, 'zmax':0.01, 'color':[ 0.4, 0.4, 0.4, 1.0]}, + (22,0): {'name':'diff', 'zmin':-0.12, 'zmax':0.02, 'color':[ 0.9, 0.9, 0.9, 1.0]}, + (30,0): {'name':'poly', 'zmin':0, 'zmax':0.2, 'color':[ 0.75, 0.35, 0.46, 1.0]}, + (33,0): {'name':'contact', 'zmin':0, 'zmax':0.91, 'color':[ 0.2, 0.2, 0.2, 1.0]}, + (34,0): {'name':'met1', 'zmin':0.91, 'zmax':1.46, 'color':[ 0.16, 0.38, 0.83, 1.0]}, + (35,0): {'name':'via1', 'zmin':1.46,'zmax':2.06, 'color':[ 0.2, 0.2, 0.2, 1.0]}, + (36,0): {'name':'met2', 'zmin':2.06, 'zmax':2.61, 'color':[ 0.65, 0.75, 0.9, 1.0]}, + (38,0): {'name':'via2', 'zmin':2.61, 'zmax':3.21, 'color':[ 0.2, 0.2, 0.2, 1.0]}, + (42,0): {'name':'met3', 'zmin':3.21, 'zmax':3.76, 'color':[ 0.2, 0.62, 0.86, 1.0]}, + (40,0): {'name':'via3', 'zmin':3.76, 'zmax':4.36, 'color':[ 0.2, 0.2, 0.2, 1.0]}, + (46,0): {'name':'met4', 'zmin':4.36, 'zmax':4.91, 'color':[ 0.15, 0.11, 0.38, 1.0]}, + (41,0): {'name':'via4', 'zmin':4.91, 'zmax':5.51, 'color':[ 0.2, 0.2, 0.2, 1.0]}, + (81,0): {'name':'met5', 'zmin':5.51, 'zmax':6.06, 'color':[ 0.4, 0.4, 0.4, 1.0]}, +} + +########## INPUT ############################################################## + +# First, the input file is read using the gdspy library, which interprets the +# GDSII file and formats the data Python-style. +# See https://gdspy.readthedocs.io/en/stable/index.html for documentation. +# Second, the boundaries of each shape (polygon or path) are extracted for +# further processing. + +print('Reading GDSII file {}...'.format(gdsii_file_path)) +gdsii = gdspy.GdsLibrary() +gdsii.read_gds(gdsii_file_path, units='import') + + +gltf = pygltflib.GLTF2() +scene = pygltflib.Scene() +gltf.scenes.append(scene) +buffer = pygltflib.Buffer() +gltf.buffers.append(buffer) + +for layer in layerstack: + mainMaterial = pygltflib.Material() + mainMaterial.doubleSided = False + mainMaterial.name = layerstack[layer]['name'] + mainMaterial.pbrMetallicRoughness = { + "baseColorFactor": layerstack[layer]['color'], + "metallicFactor": 0.5, + "roughnessFactor": 0.5 + } + gltf.materials.append(mainMaterial) + +binaryBlob = bytes() + +print('Extracting polygons...') + +meshes_lib = {} + +for cell in gdsii.cells.values(): # loop through cells to read paths and polygons + layers = {} # array to hold all geometry, sorted into layers + + print ("\nProcessing cell: ", cell.name) + + # $$$CONTEXT_INFO$$$ is a separate, non-standard compliant cell added + # optionally by KLayout to store extra information not needed here. + # see https://www.klayout.de/forum/discussion/1026/very- + # important-gds-exported-from-k-layout-not-working-on-cadence-at-foundry + if cell.name == '$$$CONTEXT_INFO$$$': + continue # skip this cell + + print ("\tpaths loop. total paths:" , len(cell.paths)) + # loop through paths in cell + for path in cell.paths: + lnum = (path.layers[0],path.datatypes[0]) # GDSII layer number + + if not lnum in layerstack.keys(): + continue + + layers[lnum] = [] if not lnum in layers else layers[lnum] + # add paths (converted to polygons) that layer + for poly in path.get_polygons(): + layers[lnum].append((poly, None, False)) + + print ("\tpolygons loop. total polygons:" , len(cell.polygons)) + # loop through polygons (and boxes) in cell + for polygon in cell.polygons: + lnum = (polygon.layers[0],polygon.datatypes[0]) # same as before... + + if not lnum in layerstack.keys(): + continue + + layers[lnum] = [] if not lnum in layers else layers[lnum] + for poly in polygon.polygons: + layers[lnum].append((poly, None, False)) + + + + + """ + At this point, "layers" is a Python dictionary structured as follows: + + layers = { + 0 : [ ([[x1, y1], [x2, y2], ...], None, False), ... ] + 1 : [ ... ] + 2 : [ ... ] + ... + } + + Each dictionary key is a GDSII layer number (0-255), and the value of the + dictionary at that key (if it exists; keys were only created for layers with + geometry) is a list of polygons in that GDSII layer. Each polygon is a 3-tuple + whose first element is a list of points (2-element lists with x and y + coordinates), second element is None (for the moment; this will be used later), + and third element is False (whether the polygon is clockwise; will be updated). + """ + + ########## TRIANGULATION ###################################################### + + # An STL file is a list of triangles, so the polygons need to be filled with + # triangles. This is a surprisingly hard algorithmic problem, especially since + # there are few limits on what shapes GDSII file polygons can be. So we use the + # Python triangle library (documentation is at https://rufat.be/triangle/), + # which is a Python interface to a fast and well-written C library also called + # triangle (with documentation at https://www.cs.cmu.edu/~quake/triangle.html). + + print('\tTriangulating polygons...') + + + num_triangles = {} # will store the number of triangles for each layer + + # loop through all layers + for layer_number, polygons in layers.items(): + + # but skip layer if it won't be exported + if not layer_number in layerstack.keys(): + continue + + num_triangles[layer_number] = 0 + + # loop through polygons in layer + for index, (polygon, _, _) in enumerate(polygons): + + num_polygon_points = len(polygon) + + # determine whether polygon points are CW or CCW + area = 0 + for i, v1 in enumerate(polygon): # loop through vertices + v2 = polygon[(i+1) % num_polygon_points] + area += (v2[0]-v1[0])*(v2[1]+v1[1]) # integrate area + + clockwise = area > 0 + + # GDSII implements holes in polygons by making the polygon edge + # wrap into the hole and back out along the same line. However, + # this confuses the triangulation library, which fills the holes + # with extra triangles. Avoid this by moving each edge back a + # very small amount so that no two edges of the same polygon overlap. + delta = 0.00001 # inset each vertex by this much (smaller has broken one file) + points_i = polygon # get list of points + points_j = np.roll(points_i, -1, axis=0) # shift by 1 + points_k = np.roll(points_i, 1, axis=0) # shift by -1 + # calculate normals for each edge of each vertex (in parallel, for speed) + normal_ij = np.stack((points_j[:, 1]-points_i[:, 1], + points_i[:, 0]-points_j[:, 0]), axis=1) + normal_ik = np.stack((points_i[:, 1]-points_k[:, 1], + points_k[:, 0]-points_i[:, 0]), axis=1) + length_ij = np.linalg.norm(normal_ij, axis=1) + length_ik = np.linalg.norm(normal_ik, axis=1) + normal_ij /= np.stack((length_ij, length_ij), axis=1) + normal_ik /= np.stack((length_ik, length_ik), axis=1) + if clockwise: + normal_ij = -1*normal_ij + normal_ik = -1*normal_ik + # move each vertex inward along its two edge normals + polygon = points_i - delta*normal_ij - delta*normal_ik + + # In an extreme case of the above, the polygon edge doubles back on + # itself on the same line, resulting in a zero-width segment. I've + # seen this happen, e.g., with a capital "N"-shaped hole, where + # the hole split line cuts out the "N" shape but splits apart to + # form the triangle cutout in one side of the shape. In any case, + # simply moving the polygon edges isn't enough to deal with this; + # we'll additionally mark points just outside of each edge, between + # the original edge and the delta-shifted edge, as outside the polygon. + # These parts will be removed from the triangulation, and this solves + # just this case with no adverse affects elsewhere. + hole_delta = 0.00001 # small fraction of delta + holes = 0.5*(points_j+points_i) - hole_delta*delta*normal_ij + # HOWEVER: sometimes this causes a segmentation fault in the triangle + # library. I've observed this as a result of certain various polygons. + # Frustratingly, the fault can be bypassed by *rotating the polygons* + # by like 30 degrees (exact angle seems to depend on delta values) or + # moving one specific edge outward a bit. I have absolutely no idea + # what is wrong. In the interest of stability over full functionality, + # this is disabled. TODO: figure out why this happens and fix it. + use_holes = False + + # triangulate: compute triangles to fill polygon + point_array = np.arange(num_polygon_points) + edges = np.transpose(np.stack((point_array, np.roll(point_array, 1)))) + if use_holes: + triangles = triangle.triangulate(dict(vertices=polygon, + segments=edges, + holes=holes), opts='p') + else: + triangles = triangle.triangulate(dict(vertices=polygon, + segments=edges), opts='p') + + if not 'triangles' in triangles.keys(): + triangles['triangles'] = [] + + # each line segment will make two triangles (for a rectangle), and the polygon + # triangulation will be copied on the top and bottom of the layer. + num_triangles[layer_number] += num_polygon_points*2 + \ + len(triangles['triangles'])*2 + polygons[index] = (polygon, triangles, clockwise) + + + + # glTF Mesh creation + + zmin = layerstack[layer_number]['zmin'] + zmax = layerstack[layer_number]['zmax'] + layername = layerstack[layer_number]['name'] + node_name = cell.name + "_" + layername + + gltf_positions = [] + gltf_indices = [] + indices_offset = 0 + for i,(_, poly_data, clockwise) in enumerate(polygons): + + + p_positions_top = np.insert(poly_data['vertices'], 2, zmax, axis=1) + p_positions_bottom = np.insert( poly_data['vertices'] , 2, zmin, axis=1) + + p_positions = np.concatenate( (p_positions_top, p_positions_bottom) ) + p_indices_top = poly_data['triangles'] + p_indices_bottom = np.flip ((p_indices_top+len(p_positions_top)), axis=1 ) + + ind_list_top = np.arange(len(p_positions_top)) + ind_list_bottom = np.arange(len(p_positions_top)) + len(p_positions_top) + + if(clockwise): + ind_list_top = np.flip(ind_list_top, axis=0) + ind_list_bottom = np.flip(ind_list_bottom, axis=0) + + p_indices_right = np.stack( (ind_list_bottom, np.roll(ind_list_bottom, -1, axis=0) , np.roll(ind_list_top, -1, axis=0)), axis=1 ) + p_indices_left = np.stack( ( np.roll(ind_list_top, -1, axis=0), ind_list_top , ind_list_bottom ) , axis=1) + + p_indices = np.concatenate( (p_indices_top, p_indices_bottom, p_indices_right, p_indices_left) ) + + if(len(gltf_positions)==0): + gltf_positions = p_positions + else: + gltf_positions = np.append(gltf_positions , p_positions, axis=0) + if(len(gltf_indices)==0): + gltf_indices = p_indices + else: + gltf_indices = np.append(gltf_indices, p_indices + indices_offset, axis=0) + indices_offset += len(p_positions) + + + + indices_binary_blob = gltf_indices.astype(np.uint32).flatten().tobytes() #triangles.flatten().tobytes() + positions_binary_blob = gltf_positions.astype(np.float32).tobytes() #points.tobytes() + + bufferView1 = pygltflib.BufferView() + bufferView1.buffer = 0 + bufferView1.byteOffset = len(binaryBlob) + bufferView1.byteLength = len(indices_binary_blob) + bufferView1.target = pygltflib.ELEMENT_ARRAY_BUFFER + gltf.bufferViews.append(bufferView1) + + accessor1 = pygltflib.Accessor() + accessor1.bufferView = len(gltf.bufferViews)-1 + accessor1.byteOffset = 0 + accessor1.componentType = pygltflib.UNSIGNED_INT + accessor1.type = pygltflib.SCALAR + accessor1.count = gltf_indices.size + accessor1.max = [int(gltf_indices.max())] + accessor1.min = [int(gltf_indices.min())] + gltf.accessors.append(accessor1) + + binaryBlob = binaryBlob + indices_binary_blob + + bufferView2 = pygltflib.BufferView() + bufferView2.buffer = 0 + bufferView2.byteOffset = len(binaryBlob) + bufferView2.byteLength = len(positions_binary_blob) + bufferView2.target = pygltflib.ARRAY_BUFFER + gltf.bufferViews.append(bufferView2) + + positions_count = len(gltf_positions) + accessor2 = pygltflib.Accessor() + accessor2.bufferView = len(gltf.bufferViews)-1 + accessor2.byteOffset = 0 + accessor2.componentType = pygltflib.FLOAT + accessor2.count = positions_count + accessor2.type = pygltflib.VEC3 + accessor2.max = gltf_positions.max(axis=0).tolist() + accessor2.min = gltf_positions.min(axis=0).tolist() + gltf.accessors.append(accessor2) + + binaryBlob = binaryBlob + positions_binary_blob + + mesh = pygltflib.Mesh() + mesh_primitive = pygltflib.Primitive() + mesh_primitive.indices = len(gltf.accessors)-2 + mesh_primitive.attributes.POSITION = len(gltf.accessors)-1 + + mesh_primitive.material = list(layerstack).index(layer_number) + + mesh.primitives.append(mesh_primitive) + + + gltf.meshes.append(mesh) + meshes_lib[node_name] = len(gltf.meshes)-1 + + + + +gltf.set_binary_blob(binaryBlob) +buffer.byteLength = len(binaryBlob) +gltf.convert_buffers(BufferFormat.DATAURI) + + + + + +def add_cell_node(c, parent_node, prefix): + for ref in c.references: + instance_node = pygltflib.Node() + instance_node.extras = {} + instance_node.extras["type"] = ref.ref_cell.name; + if(ref.properties.get(61)==None): + # ref.ref_cell.name + instance_node.name = "???"; + else: + instance_node.name = ref.properties[61] + + print(prefix, instance_node.name, "(", ref.ref_cell.name + ")") + instance_node.translation = [ref.origin[0], ref.origin[1], 0] + if(ref.rotation!=None): + if(ref.rotation==90): + instance_node.rotation = [ 0, 0, 0.7071068, 0.7071068 ] + elif(ref.rotation==180): + instance_node.rotation = [ 0, 0, 1, 0 ] + elif(ref.rotation==270): + instance_node.rotation = [ 0, 0, 0.7071068, -0.7071068 ] + if(ref.x_reflection): + instance_node.scale = [1,-1,1] + + for layer in layerstack.values(): + lib_name = ref.ref_cell.name + "_" + layer['name'] + if(meshes_lib.get(lib_name)!=None): + layer_node = pygltflib.Node() + layer_node.name = lib_name + layer_node.mesh = meshes_lib[lib_name] + gltf.nodes.append(layer_node) + instance_node.children.append(len(gltf.nodes)-1) + + if(len(ref.ref_cell.references)>0): + add_cell_node(ref.ref_cell, instance_node, prefix + "\t") + + gltf.nodes.append(instance_node) + parent_node.children.append(len(gltf.nodes)-1) + + +main_cell = gdsii.top_level()[0] + +root_node = pygltflib.Node() +root_node.name = main_cell.name #"ROOT" +gltf.nodes.append(root_node) + +print ("\nBuilding Scenegraph:") +print(root_node.name) + +add_cell_node(main_cell, root_node, "\t") + + +for layer in layerstack.values(): + lib_name = main_cell.name + "_" + layer['name'] + if(meshes_lib.get(lib_name)!=None): + layer_node = pygltflib.Node() + layer_node.name =lib_name + layer_node.mesh = meshes_lib[lib_name] + gltf.nodes.append(layer_node) + root_node.children.append(len(gltf.nodes)-1) + + + + +scene.nodes.append(0) +gltf.scene = 0 + +# validate(gltf) # will throw an error depending on the problem +# summary(gltf) + + +print ("\nWriting glTF file:") +gltf.save(gdsii_file_path + ".gltf") +# gltf.save("output.gltf") + +print('Done.') diff --git a/gds2gltf.py b/gds2gltf_sky130.py similarity index 100% rename from gds2gltf.py rename to gds2gltf_sky130.py