25.03.2020
Aktuelle Programmversion Stand 14:39 Uhr
import bpy from random import random, randint from math import * import numpy as np import time from bpy_extras.mesh_utils import ngon_tessellate # # # # KLASSEN class Sphere(object): # Klasse, die spherische Objekte beschreibt, die für das Kugelkriterium # der Delaunay-Tatraedisierung notwendig sind pos = np.array([0,0,0]) # Mittelpunkt der Sphere radius = 0 # Radius der Sphere epsilon = 0.001 # epsilon-Bereich für das Kugelkriterium def __init__(self, pos, radius): # eine Sphere wird immer mit einer Position und einem Radius initialisiert self.pos = pos self.radius = radius def punktInKugel(self, p): # diese Funktion stellt fest, ob sich ein Punkt innerhalb der Sphere befindet # (Funktion des Kugelkriteriums) if (abstandPunkte(self.pos, p) < self.radius - self.epsilon): return True else: return False class Tetraeder(object): # Klasse, die Tetraedische Objekte beschreibt, für die Delaunay-Tetraedisierung verts = [] # vertecies (Eckpunkte) des Tetraeders faces = [] # faces (Flächen) des Tetraeders (enthält Flächen-Listen, # die aus Ketten von Indexen der vertecie-Liste bestehen) sp = np.array([0,0,0]) # Schwerpunkt (= Mittelpunkt der Umkugel) -> Diese Punkte bilden die vertecies für das Voronoi-Diagramm!! name = "tetraeder" # ... umkugel = Sphere(np.array([0,0,0]), 0) # Spherenobjekt, auf dessen Oberfläche alle vertecies des Tetraeders liegen def __init__(self, verts, name): # Ein Tetraeder wird immer mit 4 Eckpunkten, vier Flächen und einem Namen initialisiert. # Außerdem berechnet der Code hier automatisch die resultierende Umkugel und den daraus # resultierenden Schwerpunkt. self.name = name self.verts = verts self.faces = [(0,1,2),(1,2,3),(2,3,0),(3,0,1)] # jedes Tetraeder hat immer dieselben 4 faces... if (self.schwerpunkt()[0] != None): self.sp = self.schwerpunkt() self.umkugel = Sphere(self.sp,abstandPunkte(self.sp,self.verts[0])) else: self.sp = np.array([None,None,None]) def schwerpunkt(self): # Funktion errechnet den Schwerpunkt des Tetraeders, indem es zwischen je # drei Eckpunkten Dreiecke aufspannt, die Schwerpunkte dieser errechnet, dann die Flächennormalen # an diesen Punkten ansetzt und den Schnittpunkt der darasu resultierenden Geraden berechnet. # Dieser Punkt ist dann der Schwerpunkt des Tetraeders (bzw. der Mittelpunkt der Umkugel) v1 = np.subtract(self.verts[1],self.verts[0]) v2 = np.subtract(self.verts[2],self.verts[0]) v3 = np.subtract(self.verts[3],self.verts[0]) sp = tetraSchwerpunkt(self.verts[0],v1,v2,v3) return sp # gibt den Schwerpunkt des Tetraeders zurück... def selfRender(self, ker, col, tetra): # falls innerhalb der Umkugel des Tetraeders kein Zellfremder Kern enthalten ist, wird das # Tetraeder als Meshobjekt generiert, in dem 3D View angezeigt und der Liste "tetra" hinzugefügt. # diese verfügt somit zum Schluss über eine Ansammlung von Referenzen aller zur Delaunay-Tetraedisierung # gehörenden Tetraeder. frei = True for m in range(0, len(ker)): if (self.umkugel.punktInKugel(ker[m]) == True): frei = False if (frei == True): tetraeder = generateObj("Tetraeder", t.verts, t.faces, (0,0,0), col) tetraeder.parent = grp_delaunay tetra.append(self) class Gerade(object): # Klasse, die Geradenformeln mithilfe von Stützvektor/Ortsvektor OV und # Richtungsvektor RV beschreibt. Das Objektattribut Kerne beschreibt, durch welche # beiden Kerne die Gerade verläuft ov = (0,0,0) rv = (0,0,0) kerne = [0,0] def __init__(self,ov,rv,kerne = [0,0]): # Jede Gerade wird mit einem Ortsvektor und Richtungsvektor initialisiert. Da nicht alle Funktionen # Geraden durch Kerne ziehen, wird das Attribut standardmäßig auf 0,0 gessetzt self.ov = ov self.rv = rv self.kerne = kerne def punktAn(self,k): # Gibt die Koordinaten eines Punktes auf der Geraden an, der mit der Koordinatenform berechnet wird # (im Detail: Stützvektor + Richtungsvektor*k) punkt = np.add(self.ov,(self.rv[0]*k,self.rv[1]*k,self.rv[2]*k)) return punkt # # # # FUNKTIONEN def dreieckSchwerpunkt(pos,a,b): # Berechnet mithilfe von drei Punkten den Schwerpunkt eines Dreieckes # unter Berücksichtigung der dreidimensionalen Lage des Dreieckes g1 = Gerade(pos,a) g2 = Gerade(pos,b) c = kreuzProdukt(a,b) d = kreuzProdukt(a,c) e = kreuzProdukt(b,c) g3 = Gerade(g1.punktAn(.5), np.divide(d, 40)) g4 = Gerade(g2.punktAn(.5), np.divide(e, 40)) schnitt = schnittPunkt(g3,g4) return schnitt def tetraSchwerpunkt(pos, v1, v2, v3): # berechnet den Schwerpunkt des Tetraeders mit Hilfe von drei Richtungsvektoren, die gemeinsam # zwei Dreiecke aufspannen (ein Vektor liegt genau an der Grenze zwischen beiden Dreiecken) sp1 = dreieckSchwerpunkt(pos, v1, v2) sp2 = dreieckSchwerpunkt(pos, v1, v3) # Es wird nach None-Type untersucht, da "falsche Dreiecke" sich einschleichen könnten # Dies sind zum Beispiel jene Dreiecke, deren Innenfläche 0 beträgt - somit wäre eine Seite gleich 0 # und es existiert kein Richtungsvektor, der sie beschreiben könnte if (sp1[0] != None and sp2[0] != None): g1 = Gerade(sp1, kreuzProdukt(v1, v2)) g2 = Gerade(sp2, kreuzProdukt(v1, v3)) schnitt = schnittPunkt(g1, g2) return schnitt else: return np.array([None,None,None]) def schnittTest(g1,g2): # testet, ob zwei Geraden sich schneiden und gibt gegebenfalls den Faktor aus, der in die Koordinatenform # der einen Geraden zur Berechnung des Schnittpunktes eingesetzt werden kann a = [[g2.rv[0],-g1.rv[0]],[g2.rv[1],-g1.rv[1]]] b = [[g1.ov[0]-g2.ov[0]],[g1.ov[1]-g2.ov[1]]] c = [[g2.rv[2],-g1.rv[2]],[g2.rv[1],-g1.rv[1]]] d = [[g1.ov[2]-g2.ov[2]],[g1.ov[1]-g2.ov[1]]] e = [[g2.rv[2],-g1.rv[2]],[g2.rv[0],-g1.rv[0]]] f = [[g1.ov[2]-g2.ov[2]],[g1.ov[0]-g2.ov[0]]] if (np.linalg.det(a) != 0): return np.linalg.solve(a,b)[0][0] elif (np.linalg.det(c) != 0): return np.linalg.solve(c,d)[0][0] elif (np.linalg.det(e) != 0): return np.linalg.solve(e,f)[0][0] else: return None def schnittPunkt(g1,g2): # setzt das Ergebnis des Schnitttests in die Koordinatengleichung der einen Gerade ein # (mithilfe der Funktion "Punkt an") und gibt den Schnittpunkt von g1 und g2 aus z = schnittTest(g1,g2) if (z != None): return g2.punktAn(z) else: return np.array([None,None,None]) def abstandPunkte(p1, p2): # gibt den Abstand zweier Punkte im dreidimensionalen Raum aus (Satz des Pythagoras) a = sqrt(((p1[0]-p2[0])**2) + ((p1[1]-p2[1])**2) + ((p1[2]-p2[2])**2)) return a def zeichnePunkt(pos, size = 0.2, name = "Punkt"): # zeichnet einen kleinen Würfel an der übergebenen Stelle, sodass dieser für Debugging-Zwecke # später leicht gefunden und nachvollzogen werden kann (wird in dieser Programmversion nicht mehr benötigt) # Größe kann angepasst werden size = size/2 verts = [(size,size,size),(-size,size,size),(-size,-size,size),(size,-size,size) ,(size,size,-size),(-size,size,-size),(-size,-size,-size),(size,-size,-size)] faces = [(0,1,2,3),(4,5,6,7),(0,1,5,4),(1,2,6,5),(2,3,7,6),(3,0,4,7)] punkt = generateObj(name,verts,faces,pos) punkt.parent = grp_kerne def kreuzProdukt(a,b): # Bildet das Kreuzprodukt der beiden übergebenen Parameter c = (a[1]*b[2]-a[2]*b[1] , a[2]*b[0]-a[0]*b[2] , a[0]*b[1]-a[1]*b[0]) return c def genKerne(anzahl, x_max, y_max, z_max): # Generiert in der übergebenen Menge zufällige Punkte und gibt diese in einer Liste zurück. # Die drei anderen Parameter sind für die Begrenzung des Raumes, in dem sich die Punkte befinden sollen ker = [] # Framing #signum = [-1, 1] #for x in signum: # for y in signum: # for z in signum: # r = randint(0,1) # ker.append(np.array([x*x_max*r,y*y_max*r,z*z_max*r])) #anzahl = anzahl - 8 #if (anzahl <= 0): return ker for n in range(0, anzahl): x = randint(-x_max, x_max)*((random()/(1+random()))) y = randint(-y_max, y_max)*((random()/(1+random()))) z = randint(-z_max, z_max)*((random()/(1+random()))) if (len(ker) != 0): neu = True # verhindert, dass kein Punkt doppelt generiert wird... hätte man auch mit if element in liste # machen können, verbesserungswürdig for m in range(0, len(ker)): if (x == ker[m][0] and y == ker[m][1] and z == ker[m][2]): neu = False if (neu == True): ker.append([x,y,z]) zeichnePunkt(ker[n], 1) else: n = n-1 else: ker.append([x,y,z]) zeichnePunkt(ker[n], 1) return ker # AB HIER ERSTELLUNG DER ZELLWÄNDE def radianBetweenVectors(v1, v2, n): # berechnet den Zwischenwinkel zweier Vektoren unter Beachtung eines Normalenvektors # (später wird für n die Flächennormale der Zellwand eingesetzt, welche die Verbindung der # beiden Knotenpunkte ist, zwischen denen sich die Zellwand befindet). a = v1[0]*v2[0] + v1[1]*v2[1] + v1[2]*v2[2] b = v1[0]*v2[1]*n[2] + v2[0]*n[1]*v1[2] + n[0]*v1[1]*v2[2] - v1[2]*v2[1]*n[0] - v2[2]*n[1]*v1[0] - n[2]*v1[1]*v2[0] rad = np.arctan2(b, a) if (rad < 0): rad = (2*np.pi) +rad deg = rad*180/np.pi return deg def polySortList(v_list, normal): # Funktion bekommt eine Liste von Punkten (mehr oder weniger in einer Ebene liegend) # übergeben und sortiert diese, sodass sie im Kreis herum nacheinander ind er Liste # angeordnet werden. Für die korrekte Drehrichtung braucht diese Funktion den Normalenvektor # der Ebene, um den sie dreht. # Könnte man wahrscheinlich deutlich kürzer schreiben, überarbeitungswürdig! # Fall die Vertexliste aus 3 Elementen besteht, handelt es sich um ein Dreieck und die Reihnefolge ist egal. if (len(v_list) == 3): return v_list geordnet = [] # Zielliste, die zum Schluss die geordneten vertecie-Koordinaten als numpy-arrays enthält start = v_list[0] # Funktion merkt sich, bei welchem Punkt sie anfängt -> weiß dann, wann sie einmal im Kreis herum iteriert ist aktu = start # aktu beinhaltet die Vertex-Daten des aktuellen Eckpunktes vv = np.subtract(v_list[0], v_list[1]) # Vergleichswinkel (zeigt immer aus dem Kreis raus), somit kann immer eine einzige Reihenfolge generiert werden geordnet.append(start) while True: # loop start values min_angle = 360 min_stelle = 0 # suche nach nächstem Punkt for v in range(0, len(v_list)): if (aktu[0] != v_list[v][0] or aktu[1] != v_list[v][1] or aktu[2] != v_list[v][2]): dir = np.subtract(v_list[v], aktu) deg = radianBetweenVectors(vv, dir, normal) if (deg < min_angle): min_angle = deg min_stelle = v geordnet.append(v_list[min_stelle]) vv = np.subtract(v_list[min_stelle], aktu) aktu = v_list[min_stelle] if (aktu[0] == start[0] and aktu[1] == start[1] and aktu[2] == start[2]): break return geordnet def genPoly(liste, normal): # generiert aus einer Liste von durcheinander angeordneten Polygon-Koordinaten ein Mesh-Flächenobjekt # VORSICHT: diese Funktion wird nur bei konvexen Polygonen zu sinnstiftenden Ergebnissen führen! geordnet = polySortList(liste, normal) f = [] # face-Datenliste, die angibt, welche Eckpunkte in welcher Reihenfolge verwendet werden sollen for i in range(0, len(geordnet)): f.append(i) # Eigenart der generateObj-Funktion, benötigt geschachtelte Liste [[faces]], um richtig zu funktionieren facelist = [] facelist.append(f) generateObj("Zellwand", geordnet, facelist) def generateObj(name, verts = [], faces = [], loc = np.array([0,0,0]), col = ( 1.0, 1.0, 1.0, 1.0 )): # generiert ein 3D-MeshObjekt mit den Übergebenen Parametern (Name, Eckpunkte, Seiten) # und gibt dieses im 3D Viewport aus mymesh = bpy.data.meshes.new(name) myobject = bpy.data.objects.new(name,mymesh) mymat = bpy.data.materials.new("Mesh_mat") mymat.diffuse_color = col mymesh.materials.append(mymat) myobject.location = loc bpy.context.collection.objects.link(myobject) mymesh.from_pydata(verts,[],faces) mymesh.update(calc_edges=True) return myobject # ZUORDNUNG DER WÄNDE ZU KOLLEKTIONEN (funkioniert lieder noch nicht komplett...) def createCollection(name): col = bpy.data.collections.new(name) bpy.context.scene.collection.children.link(col) return col def linkToCollection(obj, col): col.objects.link(obj) # LISTEN DURCHSUCHEN def inListeAnStelle(element, liste): # durchsucht eine Liste nach einem Vektor und gibt den entsprechenden Index aus, falls es enthalten ist if element in liste: for i in range(0, len(liste)): if (element == liste[i]): return i return None def inListeAnStelleNP(element, liste): # durchsucht eine Liste von np.arrays nach einem np.array und gibt die Stelle aus, # falls dieser enthalten ist for i in range(0, len(liste)): if (np.array_equal(element, liste[i])): return i return None # SCUTOID TEST!! def watertightTest(faces): # Testet, ob das Objekt mit der übergebenen Face-Liste watertight ist, also keinerlei Löcher aufweist. # wenn jede Edge mindestens zu genau zwei Flächen zugeordnet ist, ist eine Mesh watertight (in diesem Fall) for face in faces: for a in range(0, len(face)): if (a == len(face)-1): b = 0 else: b = a+1 enthalten = False for face02 in faces: if (face != face02): if ((face[a] in face02) and (face[b] in face02)): enthalten = True if (enthalten == False): return False return True def grenzenFlächenAneinander(f1, f2): # Funktion bekommt zwei Flächen übergeben und ermittelt, ob diese direkt aneinander grenzen. for a in range(0, len(f1)): for b in range(a+1, len(f1)): if ((f1[a] in f2) and (f1[b] in f2)): return True def ScutoidTest(faces): # Diese Funktion testet, ob ein Meshobjekt ein Scutoid ist oder nicht. Hierzu bekommt es eine Liste aller # faces übermittelt und testet, ob es eine Kombination aus zwei Flächen gibt (diese beiden dürfen sich nicht berühren, # sind die Deckflächen des "Prismas"), an die jeweils beide jede andere Fläche anschließt # (bis auf eine Fläche, diese ist die Dreiecksfläche) # das kleinstmöglichste Scutoid hat eine Dreiecksgrundgläche und eine vierecksgrundfläche und hat somit 6 Seiten #if (len(faces) < 6): return False for a in range(0, len(faces)): for b in range(a+1, len(faces)): scutellum = 0 #index = 0 if not (grenzenFlächenAneinander(faces[a], faces[b])): print("berühren sich nicht") for g in faces: if not (grenzenFlächenAneinander(g, faces[a]) == True and grenzenFlächenAneinander(g, faces[b]) == True): #index = g scutellum = scutellum + 1 if (scutellum == 1): #and len(faces[g]) == 3): return True return False # # # # CODE FÜR DIE ZELLGENERIERUNG # setup groups grp_alles = generateObj('Zellgeneration.000') grp_delaunay = generateObj('Delaunay') grp_delaunay.parent = grp_alles grp_kerne = generateObj('Kerne') grp_kerne.parent = grp_alles grp_randzellen = generateObj('Randzellen') grp_randzellen.parent = grp_alles grp_zellen = generateObj('Zellen') grp_zellen.parent = grp_alles grp_scutoids = generateObj('Scutoids') grp_scutoids.parent = grp_alles print(">>", grp_alles.name) # setup Größen x_max = 100 y_max = 100 z_max = 100 anzahlKerne = 25 # Minimum 8 wegen Framing #del_col = (1.0, 0.0, 0.0, 1) tetra = [] # Kernliste wird generiert ker = genKerne(anzahlKerne, x_max, y_max, z_max) # Generiert auf Basis der Kernliste die Delaunay-Tetraedisierung for i in range(0, len(ker)): for j in range(i+1, len(ker)): for k in range(j+1, len(ker)): for l in range(k+1, len(ker)): t = Tetraeder([ker[i], ker[j], ker[k], ker[l]],"Tetraeder") # random red color, weil es nice aussieht... green_tint = randint(0, 6)/100 blue_tint = randint(0,6)/100 del_col = (1.0, green_tint, blue_tint, 1.0) t.selfRender(ker, del_col, tetra) # erstellt die notwendigen Sammellisten, die die entsprechenden Information über ALLE finalen Zellen enthalten vertex = [[] for dummy in range(anzahlKerne)] # liste alle vertecies, index markiert die Zellnummer, zu der sie gehören faces = [[] for dummy in range(anzahlKerne)] # liste alle faces, index markiert die Zellnummer, zu der sie gehören # Generiert auf Basis der Delaunay-Tetraedisierung das Voronoi-Diagramm for u in range(0, len(ker)): for v in range(u+1, len(ker)): z_verts = [] # Alle Vertecies, die rund um die Verbindung beider Zellkerne liegen (Eckpunkte der Zelland zwischen ihnen) werden in z_verts abgespeichert for w in range(0, len(tetra)): if ((ker[u] in tetra[w].verts) and (ker[v] in tetra[w].verts)): z_verts.append(tetra[w].sp) if (len(z_verts) > 2): # checkt, ob eine "echte" Fläche aufgespannt wird normal = np.subtract(ker[u], ker[v]) geordnet = polySortList(z_verts, normal) temp_face1 = [] temp_face2 = [] for i in range(0, len(geordnet)): if (inListeAnStelleNP(geordnet[i], vertex[u]) != None): temp_face1.append(inListeAnStelleNP(geordnet[i], vertex[u])) else: vertex[u].append(geordnet[i]) temp_face1.append(inListeAnStelleNP(geordnet[i], vertex[u])) if (inListeAnStelleNP(geordnet[i], vertex[v]) != None): temp_face2.append(inListeAnStelleNP(geordnet[i], vertex[v])) else: vertex[v].append(geordnet[i]) temp_face2.append(inListeAnStelleNP(geordnet[i], vertex[v])) faces[u].append(temp_face1) faces[v].append(temp_face2) for i in range(0, anzahlKerne): if (watertightTest(faces[i]) == True): print("watertight") if (ScutoidTest(faces[i]) == True): yellow_tint = randint(10, 100)/100 scu = generateObj("Scutoid", vertex[i], faces[i], np.array([0,0,0]), ( 0.0, yellow_tint, 1.0, 1.0 )) scu.parent = grp_scutoids else: red_tint = randint(0, 20)/100 blue_tint = randint(0,25)/100 zelle = generateObj("Zelle", vertex[i], faces[i], np.array([0,0,0]), ( red_tint, 1.0, blue_tint, 1.0 )) zelle.parent = grp_zellen else: rand = generateObj("Randzelle", vertex[i], faces[i], np.array([0,0,0])) rand.parent = grp_randzellen