Blenderexporter: Unterschied zwischen den Versionen

Aus DGL Wiki
Wechseln zu: Navigation, Suche
(Exportieren der Daten)
(Daten für indizierte VBOs exportieren)
 
(58 dazwischenliegende Versionen von 6 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
{{Offline}}
 
 
 
==Vorwort==
 
==Vorwort==
  
Hier werde ich einige Codeschnipsel zeigen, mit denen man sich einen eigenen Blenderexporter bauen kann. Der Schwerpunkt liegt dabei darauf, die Daten so vorzubereiten, dass sie direkt als VertexBufferObjekt in die Grafikkarte hochgeladen werden können.
+
Hier werde ich einige Codeschnipsel zeigen, mit denen man sich einen eigenen Blenderexporter bauen kann. Der Schwerpunkt liegt dabei darauf, die Daten so vorzubereiten, dass sie direkt als Vertexbufferobjekt in die Grafikkarte hochgeladen werden können.
  
Dieses Tutorial, soll kein festes Format beschreiben. Es ist sowohl möglich die Daten sauber in XML zu kapseln, als auch ganz dirty mal eben ein Includierbares C oder Pascalfile zu erzeugen. Der Compiler wird einen dafür aber mit erheblich längeren compelierungszeiten bestrafen.
+
Dieses Tutorial, soll kein festes Format beschreiben. Es ist sowohl möglich die Daten sauber in XML zu kapseln, als auch ganz dreckig mal eben ein includierbares C oder Pascalfile zu erzeugen. Der Compiler wird einen dafür aber mit erheblich längeren Zeiten fürs kompilieren bestrafen.
  
Prinzipell bin ich der Meinung, das eine eigene Engine nicht zwangshaft mit einem Universalformat wie Collada verwendet werden muss. Auch beliebte Formate wie 3DS haben gewaltige Nachteile, da sie nicht alle Daten speichern können, die beim Arbeiten mit Shadern benötigt werden. 3DS ist eine gut wahl, solang man nicht viel mehr als Vertices, Normals und UV Coordinaten benötigt. Sobald Bones Vertexgruppen, TBN Matrizen und möglicherweise weitere eigene Daten gespeichert werden müssen gibt es große Probleme.
+
Prinzipiell bin ich der Meinung, das eine eigene Engine nicht Zwangshaft mit einem Universalformat wie Collada verwendet werden muss. Auch beliebte Formate wie 3DS haben gewaltige Nachteile, da sie nicht alle Daten speichern können, die beim Arbeiten mit Shadern benötigt werden. 3DS ist eine gute Wahl, solang man nicht viel mehr als Vertices, Normals und UV Koordinaten benötigt. Sobald Bones Vertexgruppen, TBN Matrizen und möglicherweise weitere eigene Daten gespeichert werden müssen gibt es große Probleme.
  
 
==Aufbau eines Exporters==
 
==Aufbau eines Exporters==
  
Jeder Blenderexporter verfügt über einen Header, in dem Daten stehen, wie er in die Menüstruktur eingefügt wird. Anschließen folt der Python code. Näheres dazu steht in den Wikibooks.
+
Jeder Blenderexporter verfügt über einen Header, in dem Daten stehen, wie er in die Menüstruktur eingefügt wird. Anschließend folgt der Pythoncode. Näheres dazu steht in den Wikibooks.
 +
<source lang="python">
 +
#!BPY
 +
"""
 +
Name: 'DGL Wiki'
 +
Blender: 241
 +
Group: 'Export'
 +
Tooltip: 'DGL Wiki Exporter'
 +
"""
 +
import Blender
 +
 
 +
#Hier werden zusätzliche Funktionen eingefügt, die zum schreiben benötigt werden
 +
 
 +
def write(filename):
 +
  out = file(filename, 'w')
 +
  obj = Blender.Object.GetSelected()[0]
 +
  msh = obj.getData()
 +
 
 +
  #hier wird Code eingefügt, der die zu exportierende Daten schreibt
  
   #!BPY
+
   out.close()
  """
+
Blender.Window.FileSelector(write, "Export")
  Name: 'DGL Wiki'
+
</source>
  Blender: 241
+
code getestet
  Group: 'Export'
 
  Tooltip: 'DGL Wiki Exporter'
 
  """
 
  import Blender
 
  def write(file):
 
        out = file(file, 'w')
 
        obj = Blender.Object.GetSelected()[0]
 
        msh = obj.getData()
 
        #hier wird Code eingefügt, der die zu exportierenden Daten in das File schreibt
 
        out.close()
 
  Blender.Window.FileSelector(write, "Export")
 
  
Wichtig ist, das man bei Python die Einrückungen beachtet. Als erstes wird eine Funktion definiert, die vom Fileexportdialog aus aufgerufen wird. Dabei wird der Filename als Argument übergeben. Die folgenden Zeilen öffnen ein File zum schreiben und hohlen sich die Meshdaten des erstem selektiertem Objektes.
+
Wichtig ist, das man bei Python die Einrückungen beachtet. Als erstes wird eine Funktion definiert, die vom Fileexportdialog aus aufgerufen wird. Dabei wird der Filename als Argument übergeben. Die folgenden Zeilen öffnen eine Datei zum schreiben und holen sich die [[Mesh]]daten des ersten selektierten Objektes.
 
Starten tut das Script eigendlich erst in der letzten Zeile, die den Exportdialog aufruft.
 
Starten tut das Script eigendlich erst in der letzten Zeile, die den Exportdialog aufruft.
 +
 +
==Vorbereiten der Daten==
 +
 +
Da OpenGL nicht gut auf die gemischten Quads und Triangles klar kommt ist es sehr sinnvoll sie gleich am Anfang vor dem Exportieren umzuwandeln:
 +
<source lang="python">
 +
def quad2tri(msh):
 +
  flist=[]
 +
  for face in msh.faces:
 +
    if (len(face.v)==3):
 +
      flist += [face]
 +
    else:
 +
      d1 =(msh.verts[face.v[0].index].co.x - msh.verts[face.v[2].index].co.x) ** 2
 +
      d1+=(msh.verts[face.v[0].index].co.y - msh.verts[face.v[2].index].co.y) ** 2
 +
      d1+=(msh.verts[face.v[0].index].co.z - msh.verts[face.v[2].index].co.z) ** 2
 +
      d2 =(msh.verts[face.v[1].index].co.x - msh.verts[face.v[3].index].co.x) ** 2
 +
      d2+=(msh.verts[face.v[1].index].co.y - msh.verts[face.v[3].index].co.y) ** 2
 +
      d2+=(msh.verts[face.v[1].index].co.z - msh.verts[face.v[3].index].co.z) ** 2
 +
      if (d1<d2):
 +
        flist += [Blender.NMesh.Face([face.v[0],face.v[1],face.v[2]])]
 +
flist[len(flist)-1].uv=[face.uv[0],face.uv[1],face.uv[2]]
 +
flist[len(flist)-1].col=[face.col[0],face.col[1],face.col[2]]
 +
        flist[len(flist)-1].smooth=face.smooth
 +
 +
flist += [Blender.NMesh.Face([face.v[0],face.v[2],face.v[3]])]
 +
        flist[len(flist)-1].uv=[face.uv[0],face.uv[2],face.uv[3]]
 +
flist[len(flist)-1].col=[face.col[0],face.col[1],face.col[2]]
 +
flist[len(flist)-1].smooth=face.smooth
 +
      else:
 +
flist += [Blender.NMesh.Face([face.v[0],face.v[1],face.v[3]])]
 +
        flist[len(flist)-1].uv=[face.uv[0],face.uv[1],face.uv[3]]
 +
flist[len(flist)-1].col=[face.col[0],face.col[1],face.col[3]]
 +
flist[len(flist)-1].smooth=face.smooth
 +
 +
flist += [Blender.NMesh.Face([face.v[1],face.v[2],face.v[3]])]
 +
        flist[len(flist)-1].uv=[face.uv[1],face.uv[2],face.uv[3]]
 +
flist[len(flist)-1].col=[face.col[1],face.col[2],face.col[3]]
 +
flist[len(flist)-1].smooth=face.smooth
 +
  msh.faces=flist
 +
  return msh
 +
 +
</source>
 +
Das Prinzip ist relativ leicht zu verstehen. "flist" ist eine Liste in der die neuen Dreiecke zwischengespeichert werden. Dreiecke werden einfach kopiert. Bei Quads wird die kürzere Diagonale zum Teilen gesucht und aus den Daten des Quads zwei neue Triangles erzeugt. Zum Schluss wird noch die temporäre Liste im [[Mesh]] gespeichert und das ganze zurückgegeben.
  
 
==Exportieren der Daten==
 
==Exportieren der Daten==
  
hier beschreiben ich wie man die wichtigsten Daten exportieren kann. Um ein anderes Format zu erhalten, müssen die Stings entsprechend angepasst werden. Eine neue Zeile erhält man durch "\n" In den Beispielen werde ich mich weitgehend an Werten gekapselt in einem XMLformat halten. Durch simple Modifikationen sind auch andere Formate kein problem.
+
Hier beschreibe ich wie man die wichtigsten Daten exportieren kann. Um ein anderes Format zu erhalten, müssen die Strings entsprechend angepasst werden. Eine neue Zeile erhält man durch "\n" In den Beispielen werde ich mich weitgehend an Werten gekapselt in einem XML-Format halten. Durch simple Modifikationen sind auch andere Formate kein Problem.
  
 
Es gibt zwei Möglichkeiten die Vertexdaten zu speichern: Interleaved oder getrennte Blöcke. Beim interleaved Format verliert man die Flexibilität und die Daten werden noch unübersichtlicher. Ein Geschwindigkeitsgewinn ist auch nicht zu erwarten.
 
Es gibt zwei Möglichkeiten die Vertexdaten zu speichern: Interleaved oder getrennte Blöcke. Beim interleaved Format verliert man die Flexibilität und die Daten werden noch unübersichtlicher. Ein Geschwindigkeitsgewinn ist auch nicht zu erwarten.
  
In einigen Codestücken wird auffallen, das die Convertierung von Quads nach Triangles immer wieder vorgenommen wird. In einem komplettem Exporten macht es durchaus Sinn, alle Daten erst zu sammel, dann zu sortieren, umwandeln und erst zum Schluss zu schreiben. Dabei wäre es allerdings nicht mehr möglich einzelne Beispiele zu liefern.
+
In einigen Codestücken wird auffallen, das die Konvertierung von Quads nach Triangles immer wieder vorgenommen wird. In einem komplettem Exporten macht es durchaus Sinn, alle Daten erst zu sammeln, dann zu sortieren, umzuwandeln und erst zum Schluss zu schreiben. Dabei wäre es allerdings nicht mehr möglich einzelne Beispiele zu liefern.
  
Eine besonder Problematik ist, dass Blender Quads beforzugt, jedoch Quads und Triangles gemischt vorliegen können. Um diese Mischung zu vermeiden sollte man entweder den Exporter so schreiben, dass er die Quads in 3 Triangles zerlegt oder vor dem Exportieren alle Quads in Triangles umwandelt und Speichert (Intern bleiben die Daten sonst anscheined immer noch Quads)
+
Eine besondere Problematik ist, dass Blender Quads bevorzugt, jedoch Quads und Dreiecke gemischt vorliegen können. Um diese Mischung zu vermeiden sollte man entweder den Exporter so schreiben, dass er die Quads in 2 Dreiecke zerlegt oder vor dem Exportieren alle Quads in Dreiecke umwandelt und Speichert (Intern bleiben die Daten sonst anscheined immer noch Quads)
  
Einfachen Text, der keine zu exportierenden Daten enhält lässt sich so in die Datei schreiben. Hier ein möglicher begin der XML Datei:
+
Einfachen Text, der keine zu exportierenden Daten enhält lässt sich so in die Datei schreiben. Hier ein möglicher Beginn der XML-Datei:
 
 
  out.write('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<vbo>\n')
 
  
Die letzte Zeile vor dem schließen der Datei sollte das VBO tag wieder schließen:
+
<source lang="python">
 +
  out.write('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n')
 +
</source>
  
  out.write('</vbo>\n')
 
  
Wichtig ist hier wieder die Einrückung. Sie muss genauso weit eingerück sein wie die vorige Zeile, oder weiter eingerück werden wenn es sich um eine Schleife handelt.
+
Wichtig ist hier wieder die Einrückung. Sie muss genauso weit Eingerückt sein wie die vorherige Zeile, oder weiter Eingerück werden wenn es sich um eine Schleife handelt.
  
 
Eine kleines Problem kann entstehen, wenn die letzte Zeile eines Datensatzes kein Komma enthalten darf. Der einfachste Weg ist eine Dummyzeile mit Nullwerten anzuhängen.
 
Eine kleines Problem kann entstehen, wenn die letzte Zeile eines Datensatzes kein Komma enthalten darf. Der einfachste Weg ist eine Dummyzeile mit Nullwerten anzuhängen.
  
===Name des Objektes===
+
===Name des Objektes und Anzahl der Vertices===
 
 
In einem XML format ist es nicht alzuwichtig den Objektnamen aus Blender zu behalten. Jedoch kann es sehr nützlich sein wenn man andere Formate schreibt oder mehrere Objekte in einer Datei speichert:
 
  
  out.write('<name>%s</name>\n' % (mesh.name))
+
In einem XML-Format ist es nicht allzu wichtig den Objektnamen aus Blender beizubehalten. Jedoch kann es sehr nützlich sein, wenn man andere Formate schreibt oder mehrere Objekte in einer Datei speichert. Wir bringen diese Daten einfach im VBO-Tag unter, welches alle unsere Vertexdaten kapseln wird:
 
 
===Anzahl der Dreiecke===
 
  
Wenn nur Dreiecke vorhanden sind kann man so die Anzahl der Dreiecke speichern:
+
<source lang="python">
 +
out.write('<vbo name="%s" verts="%i" type="GL_TRIANGLE">\n' % (msh.name, len(msh.faces)*3))
 +
</source>
  
   out.write('<triangles>%i</triangles>\n' % (len(mesh.faces)))
+
Zum Schluss sollte noch folgende Zeile angehängt werden um den VBO tag wieder zu schließen:
 
+
<source lang="python">
Mit folgendem Code kann man berücksichtigen, wie viele Triangles entstehen wenn die Quads gesplittet werden:
+
   out.write('</vbo>')  
 
+
</source>
count = 0
 
for face in msh.faces:
 
      count += len (face.v)-2
 
out.write('<triangles>%i</triangles>\n' % (count)))
 
  
 
===Vertices===
 
===Vertices===
Da man den Index eines Vertexarray nicht in einem VBO verpacken kann. Ist es besser die Triangles nicht indiziert in einem VBO zu speichern. Der Speicherbedarf ist hier Allerdings höher. Trotz der erhöten Datenmenge scheint diese Variante schneller zu sein, da hier keine Array vom Prozessor abgearbeitet werden müssen.
+
Da man den Index eines Vertexarray nicht in einem VBO verpacken kann, ist es besser die Triangles nicht zu indizieren und in einem VBO zu speichern. Der Speicherbedarf ist hier allerdings ein wenig höher. Trotz der erhöhten Datenmenge scheint diese Variante schneller zu sein, da hier keine Arrays vom Prozessor abgearbeitet werden müssen.
Unter der Annahme, das nur Triangles vorhanden sind reicht dieser Code:
+
Unter der Annahme, dass nur Triangles vorhanden sind, reicht dieser Code:
  
  out.write('<vertices>\n')
+
<source lang="python">
 +
out.write('<vertices comp="3">\n')
 
   for face in msh.faces:
 
   for face in msh.faces:
      for vert in face.v:
+
    for vert in face.v:
        out.write( ' %f %f %f' % (msh.verts[vert.index].co.x, msh.verts[vert.index].co.y, msh.verts[vert.index].co.z) )
+
      out.write( ' %f %f %f' % (msh.verts[vert.index].co.x, msh.verts[vert.index].co.y, msh.verts[vert.index].co.z))
      out.write('\n')
+
    out.write('\n')
 
   out.write('</vertices>\n')
 
   out.write('</vertices>\n')
 +
</source>
  
Komplizierter wird es wenn möglicher weise auch Quads vorhanden sind. Es gibt zwei Möglichkeiten sie zu splitten. Die bessere Möglichkeit ist, die kürzere Diagolnale zu suchen und den Quad dor in zwei Triangles zu splitten:
 
  
  out.write('<vertices>\n')
 
  for face in msh.faces:
 
    #Ein einfaches Triangle
 
    if (len(face.v) == 3)
 
        for vert in face.v:
 
            out.write( ' %f %f %f' % (msh.verts[vert.index].co.x, msh.verts[vert.index].co.y, msh.verts[vert.index].co.z))
 
        out.write('\n')
 
    #Quads   
 
    else
 
        #ermitteln der kürzeren diagonale durch pytagoras ohne Wurzel
 
        d1 =(msh.verts[face.v[0]].co.x - msh.verts[face.v[2]].co.x) ** 2
 
        d1+=(msh.verts[face.v[0]].co.y - msh.verts[face.v[2]].co.y) ** 2
 
        d1+=(msh.verts[face.v[0]].co.z - msh.verts[face.v[2]].co.z) ** 2
 
        d2 =(msh.verts[face.v[1]].co.x - msh.verts[face.v[3]].co.x) ** 2
 
        d2+=(msh.verts[face.v[1]].co.y - msh.verts[face.v[3]].co.y) ** 2
 
        d2+=(msh.verts[face.v[1]].co.z - msh.verts[face.v[3]].co.z) ** 2
 
        if (d1<d2)
 
          out.write( ' %f %f %f' % (msh.verts[face.v[0]].co.x, msh.verts[face.v[0]].co.y, msh.verts[face.v[0]].co.z))
 
          out.write( ' %f %f %f' % (msh.verts[face.v[1]].co.x, msh.verts[face.v[1]].co.y, msh.verts[face.v[1]].co.z))
 
          out.write( ' %f %f %f' % (msh.verts[face.v[2]].co.x, msh.verts[face.v[2]].co.y, msh.verts[face.v[2]].co.z))
 
          out.write('\n')
 
          out.write( ' %f %f %f' % (msh.verts[face.v[0]].co.x, msh.verts[face.v[0]].co.y, msh.verts[face.v[0]].co.z))
 
          out.write( ' %f %f %f' % (msh.verts[face.v[2]].co.x, msh.verts[face.v[2]].co.y, msh.verts[face.v[2]].co.z))
 
          out.write( ' %f %f %f' % (msh.verts[face.v[3]].co.x, msh.verts[face.v[3]].co.y, msh.verts[face.v[3]].co.z))
 
          out.write('\n')
 
        else
 
          out.write( ' %f %f %f' % (msh.verts[face.v[0]].co.x, msh.verts[face.v[0]].co.y, msh.verts[face.v[0]].co.z))
 
          out.write( ' %f %f %f' % (msh.verts[face.v[1]].co.x, msh.verts[face.v[1]].co.y, msh.verts[face.v[1]].co.z))
 
          out.write( ' %f %f %f' % (msh.verts[face.v[3]].co.x, msh.verts[face.v[3]].co.y, msh.verts[face.v[3]].co.z))
 
          out.write('\n')
 
          out.write( ' %f %f %f' % (msh.verts[face.v[1]].co.x, msh.verts[face.v[1]].co.y, msh.verts[face.v[1]].co.z))
 
          out.write( ' %f %f %f' % (msh.verts[face.v[2]].co.x, msh.verts[face.v[2]].co.y, msh.verts[face.v[2]].co.z))
 
          out.write( ' %f %f %f' % (msh.verts[face.v[3]].co.x, msh.verts[face.v[3]].co.y, msh.verts[face.v[3]].co.z))
 
          out.write('\n')
 
    out.write('</vertices>\n')
 
  
===Textur-Koordinaten===
+
===Texturkoordinaten===
  
Kaum schwerer als die Vertexdaten sind die Texturkoordinaten zu exportieren. Da die Texturkoordinaten innerhalb der Faces gespeichert werden. Ist der Code abgesehen von der Quadkonvertierung sogar etwas leichter. Wieder erst der einfache Code:
+
Kaum schwerer als die Vertexdaten sind die Texturkoordinaten zu exportieren. Da die Texturkoordinaten innerhalb der Faces gespeichert werden. Sinnvollerweise überprüfen wir voher noch, ob überhaupt Texturdaten vorhanden sind:
  
   out.write('<texturecoords>\n')
+
<source lang="python">
  for face in msh.faces:
+
   if (msh.hasFaceUV()==1):
    out.write( ' %f %f %f %f %f %f\n' % (face.uv[0][0],face.uv[0][1],face.uv[1][0],face.uv[1][1],face.uv[2][0],face.uv[2][1]) )
+
    out.write('<texturecoords comp="2">\n')
  out.write('</texturecoords>\n')
+
    for face in msh.faces:
 +
      for vert in face.v:
 +
        out.write( ' %f %f %f %f %f %f\n' % (face.uv[0][0],face.uv[0][1],face.uv[1][0],face.uv[1][1],face.uv[2][0],face.uv[2][1]) )
 +
    out.write('</texturecoords>\n')
 +
</source>
  
Um die konvertierung der Quads in Triangles auch in den Texturkoordinaten zu berücksichtigen wird der Code etwas umfangreicher:
+
===Normalen===
  out.write('<texturecoords>\n'>
 
  for face in msh.faces:
 
    #Ein einfaches Triangle
 
    if (len(face.v) == 3)
 
      for vert in face.v:
 
        out.write( ' %f %f %f %f %f %f\n' % (face.uv[0][0],face.uv[0][1],face.uv[1][0],face.uv[1][1],face.uv[2][0],face.uv[2][1]) )
 
    #Quads   
 
    else
 
      #ermitteln der kürzeren diagonale durch pytagoras ohne Wurzel
 
      d1 =(msh.verts[face.v[0]].co.x - msh.verts[face.v[2]].co.x) ** 2
 
      d1+=(msh.verts[face.v[0]].co.y - msh.verts[face.v[2]].co.y) ** 2
 
      d1+=(msh.verts[face.v[0]].co.z - msh.verts[face.v[2]].co.z) ** 2
 
      d2 =(msh.verts[face.v[1]].co.x - msh.verts[face.v[3]].co.x) ** 2
 
      d2+=(msh.verts[face.v[1]].co.y - msh.verts[face.v[3]].co.y) ** 2
 
      d2+=(msh.verts[face.v[1]].co.z - msh.verts[face.v[3]].co.z) ** 2
 
      if (d1<d2)
 
          out.write( ' %f %f %f %f %f %f\n' % (face.uv[0][0],face.uv[0][1],face.uv[1][0],face.uv[1][1],face.uv[2][0],face.uv[2][1]) )
 
          out.write( ' %f %f %f %f %f %f\n' % (face.uv[0][0],face.uv[0][1],face.uv[2][0],face.uv[2][1],face.uv[3][0],face.uv[3][1]) )
 
      else
 
          out.write( ' %f %f %f %f %f %f\n' % (face.uv[0][0],face.uv[0][1],face.uv[1][0],face.uv[1][1],face.uv[3][0],face.uv[3][1]) )
 
          out.write( ' %f %f %f %f %f %f\n' % (face.uv[1][0],face.uv[1][1],face.uv[2][0],face.uv[2][1],face.uv[3][0],face.uv[3][1]) )
 
  out.write('</texturecoords>\n')
 
  
 
+
Normalvektoren sind in Blender sowohl per Vertex als auch per Face abgespeichert. Abhängig davon, ob eine Fläche als Smooth (per Vertex) oder Solid (per Face) dargestellt wird, werden die die entsprechenden Normalvektoren ausgewählt. Damit auch Klötze mit Kanten dargestellt werden können müssen beide Fälle berücksichtigt werden. Im Fall Solid müssen für alle Vertices der Fläche der Normalvektor der Fläche verwendet werden. Im Fall Smooth muss hier der Normalvektor der Vertices benutz werden.
 
+
Auf keinen Fall sollte man nun die Vervielfachung der Vertices als Problem ansehen, da die verschiedenen Normalvektoren, diesen Nachteil wieder vollkommen ausgleichen.
===Normals===
 
 
 
Normalvektoren sind in Blender sowohl per Vertex als auch per Face abgespeichert. Abhängig, davon ob eine Fläche als Smooth (per Vertex) oder Solid (per Face) dargestellt wird, werden die die entsprechenden Normalvektoren ausgewählt. Damit auch Klötze mit Kanten dargestellt werden können müssen beide Fälle berücksichtigt werden. Im Fall Solid müssen für alle Vertices der Fläche der Normalvektor der Fläche verwendet werden. Im Fall Smooth muss hier der Normalvektor der Vertices benutz werden.
 
Auf keinen Fall sollte man nun die Vervielfachung der Vertices als Problem ansehen, da die verschiedenen Normalvektoren, diesen Nachteil wieder volkommen ausgleichen.
 
 
   
 
   
Tip: Da ganze Flächen als Solid keinerlei Optische Rundungen zulassen, ist der einzige Weg, echte Kanten an runden Objekten zu erzeugen in dem man das Mesch an der Stelle splittet. Leider muss man zum Splitten einen vollständigen Edgeloop auswählen. An Stellen die nicht kantig sein sollen muss man dann beide Vertices Makieren und mit "remove doubles" vereinen.  
+
Tipp: Da ganze Flächen als Solid keinerlei optische Rundungen zulassen, ist der einzige Weg, echte Kanten an runden Objekten zu erzeugen in dem man das [[Mesh]] an der Stelle aufteilt. Leider muss man zum aufteilen einen vollständigen Edgeloop auswählen. An Stellen die nicht kantig sein sollen muss man dann beide Vertices markieren und mit "remove doubles" vereinen.  
  
Auch hier ersst einmal ein einfaches Beispiel was haufig schon genügen sollte:
+
Auch hier erst einmal ein einfaches Beispiel was häufig schon genügen sollte:
  
   out.write('<vertices>\n')
+
<source lang="python">
 +
   out.write('<normals>\n')
 
   for face in msh.faces:
 
   for face in msh.faces:
 
     for vert in face.v:
 
     for vert in face.v:
 
         out.write( ' %f, %f, %f,' % (msh.verts[vert.index].no.x, msh.verts[vert.index].no.y, msh.verts[vert.index].no.z) )
 
         out.write( ' %f, %f, %f,' % (msh.verts[vert.index].no.x, msh.verts[vert.index].no.y, msh.verts[vert.index].no.z) )
 
     out.write('\n')
 
     out.write('\n')
   out.write('</vertices>\n')
+
   out.write('</normals>\n')
 +
</source>
  
 
Besser wird es, wenn Smooth/Solid berücksichtigt wird.  
 
Besser wird es, wenn Smooth/Solid berücksichtigt wird.  
   out.write('<vertices>\n')
+
<source lang="python">
 +
   out.write('<normals>\n')
 
   for face in msh.faces:
 
   for face in msh.faces:
    if (face.smooth == 1)
+
    if (face.smooth==1):
      for vert in face.v:
+
      for vert in face.v:
          out.write( ' %f, %f, %f,' % (msh.verts[vert.index].no.x, msh.verts[vert.index].no.y, msh.verts[vert.index].no.z))
+
        out.write( ' %f %f %f' % (msh.verts[vert.index].no.x, msh.verts[vert.index].no.y, msh.verts[vert.index].no.z))
      out.write('\n')
+
      out.write('\n')
    else
+
    else:
      for vert in range(0,3):
+
      for vert in range(0,3):
          out.write( ' %f, %f, %f,' % (face.no.x, face.no.y, face.no.z))
+
        out.write( ' %f, %f, %f,' % (face.no.x, face.no.y, face.no.z))
      out.write('\n')
+
      out.write('\n')
   out.write('</vertices>\n')
+
   out.write('</normals>\n')
 +
</source>
 +
 
 +
 
 +
 
 +
 
 +
 
 +
===Tangenvektor / TBN-Matrix ===
 +
 
 +
Ohne die TBN-Matrix lässt sich in Shadern kein sinnvolles Bumpmapping durchführen. Weder Dot3, Offset, Parallax  noch Reliefmapping sind ohne TBN-Matrix möglich.
 +
Eine TBN-Matrix besteht aus drei Vektoren, die den Texturespace aufspannen:
 +
 
 +
Normale
 +
 
 +
Er steht immer senkrecht zur Oberfläche. Er entspricht genau dem Normalvektor wie wir ihn kennen.
 +
Beim Paralax- und Reliefmapping repräsentiert seine Länge den Masstab für die dritte Texturkoordinate (Im Editor wäre sie W, auch wenn die Texturkoordinaten in OpenGL s, t, p und q sind).
 +
 
 +
Tangente
 +
 
 +
Er liegt tangential auf der Textur und ist dort zur U Achse ausgerichtet. Wieder einmal kann man überlegen, ob es nun bedeutet senkrecht zum Normal oder parallel zur Fläche des Dreiecks. Wer keine Kopfschmerzen hat macht sich welche.
 +
 
 +
Bitangente
  
Nun bleibt noch der Fall in dem noch Triangles und Quads gemischt sind. Die Entscheidung wie das Quad gesplitted wird muss die gleiche sein, wie bei den Vertices. Der Code wird allerdings noch ein wenig länger, da noch mehr Fälle vorkommen können:
+
Häufig wird er auch Binormale genannt, jedoch beschreibt Bitangente die Funktion besser. Er ist wie der Tangent tangential zur V Achse der Textur ausgerichtet.
 +
Wer meint, die Bitangente lässt sich durch das Kreuzprodukt von Normal und Tangend berechnen, hat nur Recht, wenn die Textur keine Scherung enthält.
  
  out.write('<normals>\n')
+
Solange die Textur nicht verzerrt wird, ist eine normalisierte TBN-Matrix problemlos verwendbar, wenn jedoch die Tiefenkomponente der Textur nicht auf kubischen Texeln basiert, ist es sinnvoller die Korrektur im Shader vorzunehmen, als die Komponenten der TBN- Matrix zu verzerren.
  for face in mesh.faces:
 
    if (face.smooth==1) 
 
      #Ein einfaches Triangle
 
      if (len(face.v) == 3)
 
        for vert in face.v:
 
            out.write( ' %f %f %f' % (msh.verts[vert.index].no.x, msh.verts[vert.index].no.y, msh.verts[vert.index].no.z))
 
        out.write('\n')
 
      #Quads   
 
      else
 
        #ermitteln der kürzeren diagonale durch pytagoras ohne Wurzel
 
        d1 =(msh.verts[face.v[0]].co.x - msh.verts[face.v[2]].co.x) ** 2
 
        d1+=(msh.verts[face.v[0]].co.y - msh.verts[face.v[2]].co.y) ** 2
 
        d1+=(msh.verts[face.v[0]].co.z - msh.verts[face.v[2]].co.z) ** 2
 
        d2 =(msh.verts[face.v[1]].co.x - msh.verts[face.v[3]].co.x) ** 2
 
        d2+=(msh.verts[face.v[1]].co.y - msh.verts[face.v[3]].co.y) ** 2
 
        d2+=(msh.verts[face.v[1]].co.z - msh.verts[face.v[3]].co.z) ** 2
 
        if (d1<d2)
 
          out.write( ' %f %f %f' % (msh.verts[face.v[0]].no.x, msh.verts[face.v[0]].no.y, msh.verts[face.v[0]].no.z))
 
          out.write( ' %f %f %f' % (msh.verts[face.v[1]].no.x, msh.verts[face.v[1]].no.y, msh.verts[face.v[1]].no.z))
 
          out.write( ' %f %f %f' % (msh.verts[face.v[2]].no.x, msh.verts[face.v[2]].no.y, msh.verts[face.v[2]].no.z))
 
          out.write('\n')
 
          out.write( ' %f %f %f' % (msh.verts[face.v[0]].no.x, msh.verts[face.v[0]].no.y, msh.verts[face.v[0]].no.z))
 
          out.write( ' %f %f %f' % (msh.verts[face.v[2]].no.x, msh.verts[face.v[2]].no.y, msh.verts[face.v[2]].no.z))
 
          out.write( ' %f %f %f' % (msh.verts[face.v[3]].no.x, msh.verts[face.v[3]].no.y, msh.verts[face.v[3]].no.z))
 
          out.write('\n')
 
        else
 
          out.write( ' %f %f %f' % (msh.verts[face.v[0]].no.x, msh.verts[face.v[0]].no.y, msh.verts[face.v[0]].no.z))
 
          out.write( ' %f %f %f' % (msh.verts[face.v[1]].no.x, msh.verts[face.v[1]].no.y, msh.verts[face.v[1]].no.z))
 
          out.write( ' %f %f %f' % (msh.verts[face.v[3]].no.x, msh.verts[face.v[3]].no.y, msh.verts[face.v[3]].no.z))
 
          out.write('\n')
 
          out.write( ' %f %f %f' % (msh.verts[face.v[1]].no.x, msh.verts[face.v[1]].no.y, msh.verts[face.v[1]].no.z))
 
          out.write( ' %f %f %f' % (msh.verts[face.v[2]].no.x, msh.verts[face.v[2]].no.y, msh.verts[face.v[2]].no.z))
 
          out.write( ' %f %f %f' % (msh.verts[face.v[3]].no.x, msh.verts[face.v[3]].no.y, msh.verts[face.v[3]].no.z))
 
          out.write('\n')
 
      else
 
        #Drei Normalvektoren für ein Solid Triangle
 
        for vert in range(0,3):
 
          out.write( ' %f, %f, %f,' % (face.no.x, face.no.y, face.no.z))
 
        out.write('\n')
 
        # Drei zusätzliche Normalvektoren für ein gesplitetes Solid Quad
 
        if (len(face.v)==4)
 
          for vert in range(0,3)
 
            out.write( ' %f, %f, %f,' % (face.no.x, face.no.y, face.no.z))
 
          out.write('\n')
 
  out.write('</normals>\n')
 
  
===Tangenvektor / TBN Matrix ===
+
Da wir es jetzt anscheinend verstanden haben, wie wir den zusätzlichen Komponenten der TBN-Matrix berechnen können, kommt hier der Code zum Erzeugen der Tangent und Bitangentvektoren pro Face und Vertex:
 +
<source lang="python">
 +
def createFaceTan(msh):
 +
  ftan = []
 +
  for face in msh.faces:
 +
    tan = msh.verts[face.v[2].index].co - msh.verts[face.v[0].index].co
 +
    if (face.uv[2][1] != face.uv[1][1]):
 +
      m = (face.uv[2][1]-face.uv[0][1])/(face.uv[2][1]-face.uv[1][1])
 +
      tan += (msh.verts[face.v[1].index].co - msh.verts[face.v[2].index].co) * m
 +
    tan.normalize()
 +
    ftan += [tan]
 +
  return ftan
  
Ohne die TBN Matrix lässt sich in den shadern kein sinvolles Bumpmapping durchführen...
+
def createFaceBit(msh):
 +
  fbit = []
 +
  for face in msh.faces:
 +
    bit = msh.verts[face.v[1].index].co - msh.verts[face.v[0].index].co
 +
    if (face.uv[1][0] != face.uv[2][0]):
 +
      m = (face.uv[1][0]-face.uv[0][0])/(face.uv[1][0]-face.uv[2][0])
 +
      bit += (msh.verts[face.v[2].index].co - msh.verts[face.v[1].index].co) * m
 +
    bit.normalize()
 +
    fbit += [bit]
 +
  return fbit
  
===Vertuxgruppen / Bones ===
+
def interpolate(msh,ftan):
 +
  vtan = []
 +
  for vert in msh.verts:
 +
    vtan += [Blender.NMesh.Vert().co]
 +
  for face in msh.faces:
 +
    if (face.smooth == 1):
 +
      for vert in face.v:
 +
        vtan[vert.index] += ftan[vert.index]
 +
  for i in range(0,len(msh.verts)):
 +
    vtan[i].normalize()
 +
  return vtan
  
Ohne Vertexgruppen und Bones lassen sich keine Animationen mit hilfe des Vertexshaders realisieren. Diesen Teil können alle überspringen, die keine Vertexshader zum Animieren benutzen wollen. Die Jenigen, die diese Daten im Vertexshader benötigen, traue ich auch zu durch diesen etwas umfangreicheren Code durchzusteigen.
+
ftan = createFaceTan(msh)
 +
vtan = interpolate(msh,ftan)
 +
fbit = createFaceBit(msh)
 +
vbit = interpolate(msh,fbit)
 +
</source>
 +
Hier noch der Link zum Artikel wo ich die Berechnung erkläre:
 +
http://wiki.delphigl.com/index.php/TBN_Matrix
  
Leider liegen die Daten in Blender in einer für Vertexshader total unbrauchbaren Form vor. In jeder Vertexgruppe, auf die man auch noch über Ihren Namen zugreifen muss, liegt eine Liste mit der Vertexnummer und der Gewichtung.
 
Für einen Vertexshader benötigen wir pro Vertex einen Index, der die Vertexgruppe/Bone beschreibt und die Gewichtung. Außerdem müssen wir uns voher entscheiden, wie viele Bones wir maximal pro Vertex berücksichtigen.
 
  
Da das umformen der Daten etwas komplizierter ist erst mal ein wenig Code:
+
Mit folgenden Codeschnipseln lassen sich Tangente, Bitangete und die TBN-Matrix ausgeben. Sinnvoll ist es natürlich nur die Einzelkomponenten zusätzlich zum Normalvektor zu schreiben oder die TBN-Matrix ohne Normalen zu verwenden.
  
   out.write('<vertexgroups>\n')
+
<source lang="python">
     groups = msh.getVertGroupNames()
+
   out.write('<tan>\n')
     for face in msh.faces:
+
  for i in range(0,len(msh.faces)):
 +
     face = msh.faces[i]
 +
    if (face.smooth==1):
 +
      for vert in face.v:
 +
        out.write( ' %f %f %f' % (vtan[vert.index].x, vtan[vert.index].y, vtan[vert.index].z))
 +
      out.write('\n')
 +
     else:
 +
      for vert in range(0,3):
 +
        out.write( ' %f %f %f' % (ftan[i].x, ftan[i].y, ftan[i].z))
 +
      out.write('\n')
 +
  out.write('</tan>\n')
 +
 
 +
  out.write('<bit>\n')
 +
  for i in range(0,len(msh.faces)):
 +
    face = msh.faces[i]
 +
    if (face.smooth==1):
 +
      for vert in face.v:
 +
        out.write( ' %f %f %f' % (vbit[vert.index].x, vbit[vert.index].y, vbit[vert.index].z))
 +
      out.write('\n')
 +
    else:
 +
      for vert in range(0,3):
 +
        out.write( ' %f %f %f' % (fbit[i].x, fbit[i].y, fbit[i].z))
 +
      out.write('\n')
 +
  out.write('</bit>\n')
 +
 
 +
  out.write('<tbn>\n')
 +
  for i in range(0,len(msh.faces)):
 +
    face = msh.faces[i]
 +
    if (face.smooth==1):
 
       for vert in face.v:
 
       for vert in face.v:
        list = []
+
out.write( ' %f %f %f' % (vtan[vert.index].x, vtan[vert.index].y, vtan[vert.index].z))
        count = 1 
+
        out.write( ' %f %f %f' % (vbit[vert.index].x, vbit[vert.index].y, vbit[vert.index].z))
        for group in groups:
+
        out.write( ' %f %f %f\n' % (msh.verts[vert.index].no.x, msh.verts[vert.index].no.y, msh.verts[vert.index].no.z))
            if (len(msh.getVertsFromGroup(group,0,[vert.index]))==1)
+
    else:
              list += [(msh.getVertsFromGroup(group,1,[vert.index])[0][1],count)]
+
      for vert in range(0,3):
            count ++
+
        out.write( ' %f %f %f' % (ftan[i].x, ftan[i].y, ftan[i].z))
        list.sort()
+
        out.write( ' %f %f %f' % (fbit[i].x, fbit[i].y, fbit[i].z))
        list.reverse()
+
out.write( ' %f %f %f\n' % (face.no[0], face.no[1], face.no[2]))
        #liste auf minstesanzahl mit nulldaten auffüllen zuviel schadet nicht
+
   out.write('</tbn>\n')
        for vert in range(0,4):
+
</source>
            list += [(0.0, 0)]
+
{{Warnung| Die Vektoren scheinen alle Senkrecht zu einander zu stehen, jedoch ist noch nicht überprüft, ob die Matrizen wirklich brauchbar sind. Es wäre durchaus möglich, dass noch Komponenten verdreht oder gespiegelt sind.}}
        #Ausgabe Die Indizies könnten noch von den Gewichtungen getrennt werden...
+
 
        out.write( ' %f %f %f %f' % (list[0][0],list[1][0],list[2][0],list[3][0]))
+
Aufgrund des Mangels an Attributevariablen, ist es nicht sehr sinnvoll die volle TBN-Matrix zu übergeben. Sinnvoller ist es, nur den Tangendvektor (Bitangent kann durch ein Kreuzprodukt neu berechnet werden) oder den Tangentvektor zusammen mit dem Bitangentvektor als Attribute zu übergeben.
        out.write( ' %i %i %i %i \n' % (list[0][1],list[1][1],list[2][1],list[3][1]))
 
   out.write('</vertexgroups>\n')
 
  
Bei Mangel an "attribute" Variablen macht es Sinn, die Gewichtung (muss kleiner als 1 sein) mit den Indizies zu addieren. Im Shader kann man die dann wieder herausrechnen.
+
===Vertuxgruppen / Bones ===
Da der Code nun schon recht unüberschau bar ist und es ein völliges Caos geben würden wenn noch die Quads in Triangles zerlegt werden, werden im nächstem Codesample die Vertexgruppen vorab zusammengesammlt:
 
  
 +
Ohne Vertexgruppen und Bones lassen sich keine Animationen mit Hilfe des Vertexshaders realisieren. Diesen Teil können alle überspringen, die keine Vertexshader zum Animieren benutzen wollen.
 +
Die Daten liegen in Blender in einer für Vertexshader ungünstigen Form vor und müssen etwas aufbereitet werden. In jeder Vertexgruppe, auf die man auch noch über Ihren Namen zugreifen muss, liegt eine Liste mit der Vertexnummer und der Gewichtung.
 +
Für einen Vertexshader benötigen wir pro Vertex einen Index, der die Vertexgruppe/Bone beschreibt und die Gewichtung. Außerdem müssen wir uns voher entscheiden, wie viele Bones wir maximal pro Vertex berücksichtigen.
 +
 +
Bei Mangel an "attribute"-Variablen macht es Sinn, die Gewichtung (muss kleiner als 1 sein) mit den Indizes zu addieren. Im Shader könnte man die dann wieder herausrechnen. Auf heutiger Hardware sind 16 Attribute-Variablen möglich.
 +
 +
Als erstes den Codeblock zum Aufbereiten der Daten. Es ist wichtig, dass die Variable num_of_vertexgroups.val die gewünschte Anzahl von Vertexgruppen enthält:
 +
<source lang="python">
 
   vertGroupData = []
 
   vertGroupData = []
 
   groups = msh.getVertGroupNames()
 
   groups = msh.getVertGroupNames()
   for vert in mesh.verts:
+
   for vert in msh.verts:
    list = []
+
    list = []
    count = 1
+
    count = 1
    for group in groups:
+
    for group in groups:
      if (len(msh.getVertsFromGroup(group,0,[vert.index]))==1)
+
      if (len(msh.getVertsFromGroup(group,0,[vert.index]))==1):
        list += [(msh.getVertsFromGroup(group,1,[vert.index])[0][1],count)]
+
        list += [(msh.getVertsFromGroup(group,1,[vert.index])[0][1],count)]
      count ++
+
      count += 1
      list.sort()
+
      list.sort()
      list.reverse()
+
      list.reverse()
      list += [(0.0, 0),(0.0, 0),(0.0, 0),(0.0, 0)]
+
      for i in range(0,num_of_vertexgroups.val):
      vertGroupData += (list[0],list[1],list[2],list[3])
+
        list += [(0.0, 0)]
 +
      vertGroupData += [(list[0:num_of_vertexgroups.val])]
 +
</source>
 +
Hinweis: Es gibt mit getVertexInfluences() eine noch bessere Methode, um zu ermitteln, welche Vertexgruppe zu einem Vertex gehört. Dann wäre jedoch der Einsatz eines Dictonarys nötig.
 +
 +
Nun noch der Code zum schreiben:
 +
<source lang="python">
 +
  out.write('<vertexgroups comp="%i">\n<weight>\n' % num_of_vertexgroups.val)
 +
  for face in msh.faces:
 +
    for vert in face.v:
 +
      for group in vertGroupData[vert.index]:
 +
        out.write( ' %f' % (group[0]))
 +
    out.write('\n')
 +
  out.write('</weight>\n<index>\n')
 +
  for face in msh.faces:
 +
    for vert in face.v:
 +
      for group in vertGroupData[vert.index]:
 +
        out.write( ' %i' % (group[1]))
 +
    out.write('\n')
 +
  out.write('</index>\n</vertexgroups>\n')
 +
</source>
  
Zu jedem Vertex liegen nun die vier Vertexgruppen mit der höchsten Gewichtung als Listen in vertGroupData. Das schreiben in eine Datei ist nun sehr ähnlich zu den vorigen Codesamples aufgrund  der Datenmenge allerdings etwas länger:
+
==Optimierung der Daten==
 +
 
 +
Bisher haben wir uns hauptsächlich damit beschäftig, wie wir die Daten aus der Blenderstruktur auslesen und in VBO-kompatible Daten umformen. Wenn die Modelle größer werden oder komplette Level in Blender realisiert werden sollen macht es Sinn die Daten so umzusortieren, das Triangles mit ähnlichen Eigenschaften wie Ort oder Ausrichtung nah im Vertexbufferobjekt beieinander liegen.
 +
 
 +
===Octree===
 +
 
 +
Beim Octree werden nur die Teile gerendert, bei denen voher bekannt ist, dass sie sichtbar sind, leider ist die Baumstruktur eines Octrees auf dem erstem Blick völlig inkompatibel zu dem linearen Verlauf des Vertexbuffersobjektes.
  
  out.write('<verrtexgroups>\n')
+
Um die Faces in den Octree einzusortieren, muss von jedem der Schwerpunkt berechnet werden. Die Position im Octree wird mit einer Art Hashfunktion berechnet, die erst aus den X Y und Z Koordinaten Integerwerte von 0 bis 1023 bildet und deren Bits so zusammensortiert, dass ein einziger Wert von 0 bis 2^30-1 entsteht, nach dem die Faces sortiert werden:
   out.write('<weight>\n')
+
 
 +
<source lang="python">
 +
def octree(msh):
 +
   # Boundingbox for Octree.  
 +
  # Warnung: bei mehreren VBOs sollte die gleiche Boundingbox verwendet werden,
 +
  # damit nur einmal auf sichbarkeit überprüft werden muss
 +
  mini = msh.verts[0].co * 1.0
 +
  maxi = msh.verts[0].co * 1.0
 +
  for vert in msh.verts:
 +
    mini.x = minimum (mini.x,vert.co.x)
 +
    mini.y = minimum (mini.y,vert.co.y)
 +
    mini.z = minimum (mini.z,vert.co.z)
 +
    maxi.x = maximum (maxi.x,vert.co.x)
 +
    maxi.y = maximum (maxi.y,vert.co.y)
 +
    maxi.z = maximum (maxi.z,vert.co.z)
 +
  size = max (maxi.x-mini.x,maxi.y-mini.y,maxi.z-mini.z)
 +
  #Hashberechnung
 +
  optindex = []
 +
  count = 0
 
   for face in msh.faces:
 
   for face in msh.faces:
      #Ein einfaches Triangle
+
    center = msh.verts[0].co * 0.0
      if (len(face.v) == 3)
+
    for vert in face.v:
        for vert in face.v:
+
      center += msh.verts[vert.index].co
            out.write( ' %f %f' % (vertGroupData[vert.index][0][0],vertGroupData[vert.index][1][0]))
+
    #range 0.0 ... 1.0
            out.write( ' %f %f\n' % (vertGroupData[vert.index][2][0],vertGroupData[vert.index][3][0]))
+
    center *= 1.0 /( len(face.v) * size * 2.0)
      #Quads      
+
    center.x += 0.5
      else
+
     center.y += 0.5
        #ermitteln der kürzeren diagonale durch pytagoras ohne Wurzel
+
    center.z += 0.5
        d1 =(msh.verts[face.v[0]].co.x - msh.verts[face.v[2]].co.x) ** 2
+
    ix = int (center.x * 1023)
        d1+=(msh.verts[face.v[0]].co.y - msh.verts[face.v[2]].co.y) ** 2
+
    iy = int (center.y * 1023)
        d1+=(msh.verts[face.v[0]].co.z - msh.verts[face.v[2]].co.z) ** 2 
+
    iz = int (center.z * 1023)
        d2 =(msh.verts[face.v[1]].co.x - msh.verts[face.v[3]].co.x) ** 2
+
    sortby  = (ix&512)<<18 | (ix&256)<<16 |(ix&128)<<14 | (ix&64)<<12 |(ix&32)<<10 | (ix&16)<<8 |(ix&8)<<6 | (ix&4)<<4 |(ix&2)<<2 | (ix&1)<<0
        d2+=(msh.verts[face.v[1]].co.y - msh.verts[face.v[3]].co.y) ** 2
+
    sortby |= (iy&512)<<19 | (iy&256)<<17 |(iy&128)<<15 | (iy&64)<<13 |(iy&32)<<11 | (iy&16)<<9 |(iy&8)<<7 | (iy&4)<<5 |(iy&2)<<3 | (iy&1)<<1
        d2+=(msh.verts[face.v[1]].co.z - msh.verts[face.v[3]].co.z) ** 2
+
    sortby |= (iz&512)<<20 | (iz&256)<<18 |(iz&128)<<16 | (iz&64)<<14 |(iz&32)<<12 | (iz&16)<<10|(iz&8)<<8 | (iz&4)<<6 |(iz&2)<<4 | (iz&1)<<2
        if (d1<d2)
+
    optindex += [(sortby,count)]
            out.write( ' %f %f' % (vertGroupData[face.v[0]][0][0],vertGroupData[face.v[0]][1][0]))
+
    count += 1
            out.write( ' %f %f\n' % (vertGroupData[face.v[0]][2][0],vertGroupData[face.v[0]][3][0]))
+
  #Umsortierung der Daten
            out.write( ' %f %f' % (vertGroupData[face.v[1]][0][0],vertGroupData[face.v[1]][1][0]))
+
  optindex.sort()
            out.write( ' %f %f\n' % (vertGroupData[face.v[1]][2][0],vertGroupData[face.v[1]][3][0]))
+
  flist=[]
            out.write( ' %f %f' % (vertGroupData[face.v[2]][0][0],vertGroupData[face.v[2]][1][0]))
+
  for i in optindex:
            out.write( ' %f %f\n' % (vertGroupData[face.v[2]][2][0],vertGroupData[face.v[2]][3][0]))
+
    flist += [msh.faces[i[1]]]
            out.write( ' %f %f' % (vertGroupData[face.v[0]][0][0],vertGroupData[face.v[0]][1][0]))
+
  msh.faces=flist
            out.write( ' %f %f\n' % (vertGroupData[face.v[0]][2][0],vertGroupData[face.v[0]][3][0]))
+
  #Hier sollten noch die Verwaltungsdaten für den Octree geschrieben werden
            out.write( ' %f %f' % (vertGroupData[face.v[2]][0][0],vertGroupData[face.v[2]][1][0]))
+
  #Diese Code fehl allderings noch
            out.write( ' %f %f\n' % (vertGroupData[face.v[2]][2][0],vertGroupData[face.v[2]][3][0]))
+
 
            out.write( ' %f %f' % (vertGroupData[face.v[3]][0][0],vertGroupData[face.v[3]][1][0]))
+
  return msh
            out.write( ' %f %f\n' % (vertGroupData[face.v[3]][2][0],vertGroupData[face.v[3]][3][0]))
+
</source>
        else
+
Diese Funktion sollte direkt nach dem Umwandeln der Quads in Triangles aufgerufen werden, da die Reihenfolge der Dreiecke geändert wird.
            out.write( ' %f %f' % (vertGroupData[face.v[0]][0][0],vertGroupData[face.v[0]][1][0]))
+
 
            out.write( ' %f %f\n' % (vertGroupData[face.v[0]][2][0],vertGroupData[face.v[0]][3][0]))
+
Prinzipiell gibt es für jedes würfelförmige Volumen nur eine Start und Stopindexnummer die beim Rendern übergeben werden muss:
            out.write( ' %f %f' % (vertGroupData[face.v[1]][0][0],vertGroupData[face.v[1]][1][0]))
+
<source lang="cpp">
            out.write( ' %f %f\n' % (vertGroupData[face.v[1]][2][0],vertGroupData[face.v[1]][3][0]))
+
glDrawArrays( GL_TRIANGLES, start, stop - start);
            out.write( ' %f %f' % (vertGroupData[face.v[3]][0][0],vertGroupData[face.v[3]][1][0]))
+
</source>
            out.write( ' %f %f\n' % (vertGroupData[face.v[3]][2][0],vertGroupData[face.v[3]][3][0]))
+
Grundsätzlich sollte beachtet werden, dass es bei der Verwendung von VBOs möglichst viele Triangles auf einmal gerendert werden sollten. Wo die Untergrenze pro Volumen liegt, lässt sich schwer sagen, es ist aber sinnvoll, diese jenseits der 1000-10000 anzusetzten.
            out.write( ' %f %f' % (vertGroupData[face.v[1]][0][0],vertGroupData[face.v[1]][1][0]))
+
 
            out.write( ' %f %f\n' % (vertGroupData[face.v[1]][2][0],vertGroupData[face.v[1]][3][0]))
+
===Vorsortierung nach Vertexgruppen/Bones===
            out.write( ' %f %f' % (vertGroupData[face.v[2]][0][0],vertGroupData[face.v[2]][1][0]))
+
 
            out.write( ' %f %f\n' % (vertGroupData[face.v[2]][2][0],vertGroupData[face.v[2]][3][0]))
+
Es ist zwar noch kein Algoritmus zum Sortieren vorhanden, jedoch ist es bei animierten Modellen sinnvoll, möglichst ganze Bones auf einmal abzuarbeiten, da ein Bone ganze 12 Uniform Floats im Vertexshader verbraucht (9 für eine Rotationsmatrix und 3 für das Gelenk). Laut GLSL Spezifikation soll man minimal 512 davon haben, was maximal 40 Bones entspricht. Dummerweise scheint es so, als stellten einige Grafikkarten nur 256 Uniform Floats zur Verfügung, was zur Folge hat, das nur noch 20 Bones möglich wären.
            out.write( ' %f %f' % (vertGroupData[face.v[3]][0][0],vertGroupData[face.v[3]][1][0]))
+
Eine primitive Optimierung wäre nur einen halbes Model zu speichen und für die zweite Hälfte das gleiche Mesh gespiegelt mit neuen Bones zu verwenden.
            out.write( ' %f %f\n' % (vertGroupData[face.v[3]][2][0],vertGroupData[face.v[3]][3][0]))
+
 
  out.write('</weight>\n')
+
Die sinnvollste Variante ist die schon erzeugten Vertexgruppendaten zum Sortieren zu benutzen: Die Gruppe mit der höchsten Gewichtung wird zum Einsortieren benutzt. Alle Vertices die nicht in einer Gruppe sind, landen in einer Extra-Gruppe. Diese Gruppen werde nun so umsortiert, dass Gruppen mit gegenseitigem Einfluss nah beieinander landen und Vertexgruppen die sich nicht beeinflussen, weit von einander im VBO entfernt liegen.
  out.write('<index>\n')
+
 
  for face in msh.faces:
+
==Daten für indizierte VBOs exportieren==
      #Ein einfaches Triangle
+
 
      if (len(face.v) == 3)
+
Lange Zeit hab ich dieses Thema umgangen, da der Export relativ komplex ist. Alle Daten aus Blender müssen komplett neu umgeordnet und neu indiziert werden, da Blender die Texturkoordinaten nicht per Vertex, sondern per Face abspeichert. Als Beispiel wird diesmal ein vereinfachter OBJ-Exporter dienen, der allerdings um Tangentenvektoren ergänzt wird.
        for vert in face.v:
+
 
            out.write( ' %i %i' % (vertGroupData[vert.index][0][1],vertGroupData[vert.index][1][1]))
+
<source lang="python">
            out.write( ' %i %i\n' % (vertGroupData[vert.index][2][1],vertGroupData[vert.index][3][1]))
+
#!BPY
      #Quads   
+
"""
      else
+
Name: 'VBO Wiki'
        #ermitteln der kürzeren diagonale durch pytagoras ohne Wurzel
+
Blender: 241
        d1 =(msh.verts[face.v[0]].co.x - msh.verts[face.v[2]].co.x) ** 2
+
Group: 'Export'
        d1+=(msh.verts[face.v[0]].co.y - msh.verts[face.v[2]].co.y) ** 2
+
Tooltip: 'VBO Wiki Exporter 3 C version'
        d1+=(msh.verts[face.v[0]].co.z - msh.verts[face.v[2]].co.z) ** 2 
+
"""
        d2 =(msh.verts[face.v[1]].co.x - msh.verts[face.v[3]].co.x) ** 2
+
import Blender
        d2+=(msh.verts[face.v[1]].co.y - msh.verts[face.v[3]].co.y) ** 2
+
from Blender import NMesh
        d2+=(msh.verts[face.v[1]].co.z - msh.verts[face.v[3]].co.z) ** 2
+
from Blender.BGL import *
        if (d1<d2)
+
from Blender.Draw import *
            #Vertex 0 Triangle 0
+
import math
            out.write( ' %i %i' % (vertGroupData[face.v[0]][0][1],vertGroupData[face.v[0]][1][1]))
+
from math import *
            out.write( ' %i %i\n' % (vertGroupData[face.v[0]][2][1],vertGroupData[face.v[0]][3][1]))
+
 
            #Vertex 1 Triangle 0
+
#leicht verständliche funktion jedoch sehr ineffezient
            out.write( ' %i %i' % (vertGroupData[face.v[1]][0][1],vertGroupData[face.v[1]][1][1]))
+
def lfind(liste,key):
            out.write( ' %i %i\n' % (vertGroupData[face.v[1]][2][1],vertGroupData[face.v[1]][3][1]))
+
    for i in range(0, len(liste)):
            out.write( ' %i %i' % (vertGroupData[face.v[2]][0][1],vertGroupData[face.v[2]][1][1]))
+
if liste[i]==key: return i
            out.write( ' %i %i\n' % (vertGroupData[face.v[2]][2][1],vertGroupData[face.v[2]][3][1]))
+
 
            out.write( ' %i %i' % (vertGroupData[face.v[0]][0][1],vertGroupData[face.v[0]][1][1]))
+
def write(filename):
            out.write( ' %i %i\n' % (vertGroupData[face.v[0]][2][1],vertGroupData[face.v[0]][3][1]))
+
  global num_of_vertexgroups,TBNMenu,Optimise
            out.write( ' %i %i' % (vertGroupData[face.v[2]][0][1],vertGroupData[face.v[2]][1][1]))
+
  filename=replace(filename,".blend",".obj")
            out.write( ' %i %i\n' % (vertGroupData[face.v[2]][2][1],vertGroupData[face.v[2]][3][1]))
+
  out = file(filename, 'w')
            out.write( ' %i %i' % (vertGroupData[face.v[3]][0][1],vertGroupData[face.v[3]][1][1]))
+
 
            out.write( ' %i %i\n' % (vertGroupData[face.v[3]][2][1],vertGroupData[face.v[3]][3][1]))
+
  objs = Blender.Object.GetSelected()
        else
+
  if (len(objs)==0):
            out.write( ' %i %i' % (vertGroupData[face.v[0]][0][1],vertGroupData[face.v[0]][1][1]))
+
    objs = Blender.Object.Get()
            out.write( ' %i %i\n' % (vertGroupData[face.v[0]][2][1],vertGroupData[face.v[0]][3][1]))
+
  #eigendlich nur 1 durchlauf sinnvoll
            out.write( ' %i %i' % (vertGroupData[face.v[1]][0][1],vertGroupData[face.v[1]][1][1]))
+
  for obj in objs:
            out.write( ' %i %i\n' % (vertGroupData[face.v[1]][2][1],vertGroupData[face.v[1]][3][1]))
+
    if (obj.getType()=='Mesh'):
            out.write( ' %i %i' % (vertGroupData[face.v[3]][0][1],vertGroupData[face.v[3]][1][1]))
+
   
            out.write( ' %i %i\n' % (vertGroupData[face.v[3]][2][1],vertGroupData[face.v[3]][3][1]))
+
      msh = obj.getData()
            out.write( ' %i %i' % (vertGroupData[face.v[1]][0][1],vertGroupData[face.v[1]][1][1]))
+
      # quad2tri könnte von obern hier eingesetzt werden
            out.write( ' %i %i\n' % (vertGroupData[face.v[1]][2][1],vertGroupData[face.v[1]][3][1]))
+
      # msh = quad2tri(msh)
            out.write( ' %i %i' % (vertGroupData[face.v[2]][0][1],vertGroupData[face.v[2]][1][1]))
+
      raw_index = []
            out.write( ' %i %i\n' % (vertGroupData[face.v[2]][2][1],vertGroupData[face.v[2]][3][1]))
+
      for face in msh.faces:
            out.write( ' %i %i' % (vertGroupData[face.v[3]][0][1],vertGroupData[face.v[3]][1][1]))
+
        for i in range (0,len (face.v)):
            out.write( ' %i %i\n' % (vertGroupData[face.v[3]][2][1],vertGroupData[face.v[3]][3][1]))
+
          vert = []
  out.write('</index>\n')
+
  vert += [face.v[i].index]
  out.write('</vertexgroups>\n')
+
  vert += [face.uv[i][0]]
 +
  vert += [face.uv[i][1]]
 +
  raw_index += [vert]
 +
 
 +
      raw_index.sort()
 +
      last =raw_index[-1]
 +
      for i in range(len(raw_index)-2, -1, -1):
 +
      if last==raw_index[i]:
 +
del raw_index[i]
 +
      else: last=raw_index[i]
 +
   
 +
      out.write('g Index\n')
 +
     
 +
      for i in raw_index:
 +
v = msh.verts[i[0]].co
 +
        out.write('v %f %f %f\n' % (v.x,v.y,v.z))
 +
     
 +
      for i in raw_index:
 +
v = msh.verts[i[0]].no
 +
        out.write('vn %f %f %f\n' % (v.x,v.y,v.z))
 +
     
 +
      for i in raw_index:
 +
out.write('vt %f %f\n' % (i[1],i[2]))
  
==Optimierung der Daten==
+
      tan = [Blender.Mathutils.Vector(0,0,0) for i in range(0,len(raw_index))]
 +
      for face in msh.faces:
 +
        v1 = face.v[1].co - face.v[0].co
 +
v2 = face.v[2].co - face.v[0].co
 +
t1 = face.uv[1][1] - face.uv[0][1]
 +
t2 = face.uv[2][1] - face.uv[0][1]
 +
 +
sdir = v1 * t2 - v2 * t1
 +
sdir.normalize()
 +
for i in range (0,len (face.v)):
 +
  vert = [face.v[i].index,face.uv[i][0],face.uv[i][1]]
 +
  tan[lfind(raw_index, vert)] += sdir
 +
     
 +
      for i in range (0,len(raw_index)):
 +
normal = msh.verts[raw_index[i][0]].no
 +
otan = tan[i] - normal *  Blender.Mathutils.DotVecs( tan[i], normal )
 +
otan.normalize()
 +
out.write('tan %f %f %f\n' % (otan.x,otan.y,otan.z))
  
Bisher haben wir uns Hauptsachlich damit beschäftig wie wir die Daten aus der Blenderstruktur auszulesen und in ein Vertexbufferobjekt kompatible daten unzuformen. Wen die Modelle größer werden oder komplette Level in Blender realisiert werden sollen macht es Sinn die Daten so umzusortieren, das Triangles mit ähnlichen Eigenschaften wie Ort oder Ausrichtung nah im Vertexbufferobjekt beieinander liegen.
+
      for face in msh.faces:
 +
        out.write('f')
 +
for i in range (0,len (face.v)):
 +
  vert = [face.v[i].index,face.uv[i][0],face.uv[i][1]]
 +
    out.write(' %i' % (lfind(raw_index, vert)))
 +
out.write('\n')
 +
 +
  out.close()
 +
  Exit()
  
===Quad/Octree===
+
#Blender.Window.FileSelector(write, "Export")
 +
write ("out.obj")
 +
</source>
  
Beim Octree werden nur die Teile gerender, bei denen voher bekannt ist, dass sie sichtbar sind, leider ist die Baumstruktur eines Octtrees auf dem erstem Blick völlig inkompatibel zu dem linearem Verlauf des Vertexbuffersobjektes.
+
==Zusätzliche Optionen abfragen==
  
Um die Faces in den Octree einzusortieren muss von jedem der Schwerpunkt berechnet werden. Dabei sollte ermittelt werden wie groß das größte Face ist und welche Ausmaße der Octtree hat.
+
Ein Exporter ist schön, etwas unschön ist es bis jetzt, das die Ausgabe immer statisch nach dem gleichem Schema erfolgt. Um bestimmte Optionen wählbar zu machen, ist es nötig einen Dialog zu öffnen, dessen Eingaben in globalen Variablen abgelegt werden. Aus codetechnischer Sicht ist es einfacher die Optionen vor dem Filedialog abzufragen.
 +
Damit ein Dialog verwendet werden kann, sind ein paar Änderungen am Code nötig.
  
 +
Folgende Funktionen und globale Variablen müssen eingefügt werden:
  
 +
<source lang="python">
 +
num_of_vertexgroups = Create(4)
 +
TMenu = Create(1)
 +
EVENT_NOEVENT = 1
 +
EVENT_EXPORT  = 2
 +
EVENT_CANCEL  = 3
  
 +
def draw():
 +
  global num_of_vertexgroups,TMenu
 +
  global EVENT_NOEVENT,EVENT_EXPORT
 +
  glClear(GL_COLOR_BUFFER_BIT)
 +
  glRasterPos2d(10, 125)
 +
  Text("DGL Exporter Options")
 +
 
 +
  TMemu = Menu("Normal|Normal+Tangent|TBN-Matrix",EVENT_NOEVENT,10,75,210,18,  1)
 +
  num_of_vertexgroups= Number("No of Vertgroups: ", EVENT_NOEVENT, 10, 55, 210, 18,num_of_vertexgroups.val, 0, 10, "Number of Vertgroups per Vertex");
 +
  Button("Export",EVENT_EXPORT, 140, 10, 80, 18)
 +
  Button("Cancel",EVENT_CANCEL, 10, 10, 80, 18)
 +
 
 +
def event(evt, val):
 +
  if (evt == QKEY and not val):
 +
      Exit()
  
Jede der bisherigen exportfunktionen began mit folgender Schleife:
+
def bevent(evt):
 +
  global EVENT_NOEVENT,EVENT_EXPORT
 +
  if (evt== EVENT_EXPORT):
 +
    Blender.Window.FileSelector(write, "Export")
 +
  elif (evt == EVENT_CANCEL):
 +
    Exit()
 +
</source>
 +
Code noch nicht fertig
  
  for face in msh.faces:
+
Die letzte Zeile, welche den Filedialog geöffnet hat, wird durch folgende ersetzt:
 +
<source lang="python">
 +
Register(draw, event, bevent)
 +
</source>
  
Sie arbeitet die Faces ohne Optimierung in der Reinfolge ab, wie Blender sie im Speicher hällt. Diese Schleife kann nun einfach ersetzt werden:
+
Damit die write-Funktion keine Endloschleife verursacht, sollte sie noch um Exit() ergänzt werden.
  
 
==Externe Links==
 
==Externe Links==
  
 
http://en.wikibooks.org/wiki/Blender_3D:_Noob_to_Pro/Advanced_Tutorials/Python_Scripting/Export_scripts
 
http://en.wikibooks.org/wiki/Blender_3D:_Noob_to_Pro/Advanced_Tutorials/Python_Scripting/Export_scripts
 +
[[Kategorie:Anleitung]]

Aktuelle Version vom 18. März 2012, 14:21 Uhr

Vorwort

Hier werde ich einige Codeschnipsel zeigen, mit denen man sich einen eigenen Blenderexporter bauen kann. Der Schwerpunkt liegt dabei darauf, die Daten so vorzubereiten, dass sie direkt als Vertexbufferobjekt in die Grafikkarte hochgeladen werden können.

Dieses Tutorial, soll kein festes Format beschreiben. Es ist sowohl möglich die Daten sauber in XML zu kapseln, als auch ganz dreckig mal eben ein includierbares C oder Pascalfile zu erzeugen. Der Compiler wird einen dafür aber mit erheblich längeren Zeiten fürs kompilieren bestrafen.

Prinzipiell bin ich der Meinung, das eine eigene Engine nicht Zwangshaft mit einem Universalformat wie Collada verwendet werden muss. Auch beliebte Formate wie 3DS haben gewaltige Nachteile, da sie nicht alle Daten speichern können, die beim Arbeiten mit Shadern benötigt werden. 3DS ist eine gute Wahl, solang man nicht viel mehr als Vertices, Normals und UV Koordinaten benötigt. Sobald Bones Vertexgruppen, TBN Matrizen und möglicherweise weitere eigene Daten gespeichert werden müssen gibt es große Probleme.

Aufbau eines Exporters

Jeder Blenderexporter verfügt über einen Header, in dem Daten stehen, wie er in die Menüstruktur eingefügt wird. Anschließend folgt der Pythoncode. Näheres dazu steht in den Wikibooks.

#!BPY
"""
Name: 'DGL Wiki'
Blender: 241
Group: 'Export'
Tooltip: 'DGL Wiki Exporter'
"""
import Blender

#Hier werden zusätzliche Funktionen eingefügt, die zum schreiben benötigt werden

def write(filename):
  out = file(filename, 'w')
  obj = Blender.Object.GetSelected()[0]
  msh = obj.getData()

  #hier wird Code eingefügt, der die zu exportierende Daten schreibt

  out.close()
Blender.Window.FileSelector(write, "Export")

code getestet

Wichtig ist, das man bei Python die Einrückungen beachtet. Als erstes wird eine Funktion definiert, die vom Fileexportdialog aus aufgerufen wird. Dabei wird der Filename als Argument übergeben. Die folgenden Zeilen öffnen eine Datei zum schreiben und holen sich die Meshdaten des ersten selektierten Objektes. Starten tut das Script eigendlich erst in der letzten Zeile, die den Exportdialog aufruft.

Vorbereiten der Daten

Da OpenGL nicht gut auf die gemischten Quads und Triangles klar kommt ist es sehr sinnvoll sie gleich am Anfang vor dem Exportieren umzuwandeln:

def quad2tri(msh):
  flist=[]
  for face in msh.faces:
    if (len(face.v)==3):
      flist += [face]
    else:
      d1 =(msh.verts[face.v[0].index].co.x - msh.verts[face.v[2].index].co.x) ** 2 
      d1+=(msh.verts[face.v[0].index].co.y - msh.verts[face.v[2].index].co.y) ** 2 
      d1+=(msh.verts[face.v[0].index].co.z - msh.verts[face.v[2].index].co.z) ** 2 
      d2 =(msh.verts[face.v[1].index].co.x - msh.verts[face.v[3].index].co.x) ** 2 
      d2+=(msh.verts[face.v[1].index].co.y - msh.verts[face.v[3].index].co.y) ** 2 
      d2+=(msh.verts[face.v[1].index].co.z - msh.verts[face.v[3].index].co.z) ** 2 
      if (d1<d2):
        flist += [Blender.NMesh.Face([face.v[0],face.v[1],face.v[2]])]
	flist[len(flist)-1].uv=[face.uv[0],face.uv[1],face.uv[2]]
	flist[len(flist)-1].col=[face.col[0],face.col[1],face.col[2]]
        flist[len(flist)-1].smooth=face.smooth
	
	flist += [Blender.NMesh.Face([face.v[0],face.v[2],face.v[3]])]
        flist[len(flist)-1].uv=[face.uv[0],face.uv[2],face.uv[3]]
	flist[len(flist)-1].col=[face.col[0],face.col[1],face.col[2]]
	flist[len(flist)-1].smooth=face.smooth
      else:
	flist += [Blender.NMesh.Face([face.v[0],face.v[1],face.v[3]])]
        flist[len(flist)-1].uv=[face.uv[0],face.uv[1],face.uv[3]]
	flist[len(flist)-1].col=[face.col[0],face.col[1],face.col[3]]
	flist[len(flist)-1].smooth=face.smooth
	
	flist += [Blender.NMesh.Face([face.v[1],face.v[2],face.v[3]])]
        flist[len(flist)-1].uv=[face.uv[1],face.uv[2],face.uv[3]]
	flist[len(flist)-1].col=[face.col[1],face.col[2],face.col[3]]
	flist[len(flist)-1].smooth=face.smooth
  msh.faces=flist
  return msh

Das Prinzip ist relativ leicht zu verstehen. "flist" ist eine Liste in der die neuen Dreiecke zwischengespeichert werden. Dreiecke werden einfach kopiert. Bei Quads wird die kürzere Diagonale zum Teilen gesucht und aus den Daten des Quads zwei neue Triangles erzeugt. Zum Schluss wird noch die temporäre Liste im Mesh gespeichert und das ganze zurückgegeben.

Exportieren der Daten

Hier beschreibe ich wie man die wichtigsten Daten exportieren kann. Um ein anderes Format zu erhalten, müssen die Strings entsprechend angepasst werden. Eine neue Zeile erhält man durch "\n" In den Beispielen werde ich mich weitgehend an Werten gekapselt in einem XML-Format halten. Durch simple Modifikationen sind auch andere Formate kein Problem.

Es gibt zwei Möglichkeiten die Vertexdaten zu speichern: Interleaved oder getrennte Blöcke. Beim interleaved Format verliert man die Flexibilität und die Daten werden noch unübersichtlicher. Ein Geschwindigkeitsgewinn ist auch nicht zu erwarten.

In einigen Codestücken wird auffallen, das die Konvertierung von Quads nach Triangles immer wieder vorgenommen wird. In einem komplettem Exporten macht es durchaus Sinn, alle Daten erst zu sammeln, dann zu sortieren, umzuwandeln und erst zum Schluss zu schreiben. Dabei wäre es allerdings nicht mehr möglich einzelne Beispiele zu liefern.

Eine besondere Problematik ist, dass Blender Quads bevorzugt, jedoch Quads und Dreiecke gemischt vorliegen können. Um diese Mischung zu vermeiden sollte man entweder den Exporter so schreiben, dass er die Quads in 2 Dreiecke zerlegt oder vor dem Exportieren alle Quads in Dreiecke umwandelt und Speichert (Intern bleiben die Daten sonst anscheined immer noch Quads)

Einfachen Text, der keine zu exportierenden Daten enhält lässt sich so in die Datei schreiben. Hier ein möglicher Beginn der XML-Datei:

  out.write('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n')


Wichtig ist hier wieder die Einrückung. Sie muss genauso weit Eingerückt sein wie die vorherige Zeile, oder weiter Eingerück werden wenn es sich um eine Schleife handelt.

Eine kleines Problem kann entstehen, wenn die letzte Zeile eines Datensatzes kein Komma enthalten darf. Der einfachste Weg ist eine Dummyzeile mit Nullwerten anzuhängen.

Name des Objektes und Anzahl der Vertices

In einem XML-Format ist es nicht allzu wichtig den Objektnamen aus Blender beizubehalten. Jedoch kann es sehr nützlich sein, wenn man andere Formate schreibt oder mehrere Objekte in einer Datei speichert. Wir bringen diese Daten einfach im VBO-Tag unter, welches alle unsere Vertexdaten kapseln wird:

 out.write('<vbo name="%s" verts="%i" type="GL_TRIANGLE">\n' % (msh.name, len(msh.faces)*3))

Zum Schluss sollte noch folgende Zeile angehängt werden um den VBO tag wieder zu schließen:

  out.write('</vbo>')

Vertices

Da man den Index eines Vertexarray nicht in einem VBO verpacken kann, ist es besser die Triangles nicht zu indizieren und in einem VBO zu speichern. Der Speicherbedarf ist hier allerdings ein wenig höher. Trotz der erhöhten Datenmenge scheint diese Variante schneller zu sein, da hier keine Arrays vom Prozessor abgearbeitet werden müssen. Unter der Annahme, dass nur Triangles vorhanden sind, reicht dieser Code:

 out.write('<vertices comp="3">\n')
  for face in msh.faces:
    for vert in face.v:
      out.write( ' %f %f %f' % (msh.verts[vert.index].co.x, msh.verts[vert.index].co.y, msh.verts[vert.index].co.z))
    out.write('\n')
  out.write('</vertices>\n')


Texturkoordinaten

Kaum schwerer als die Vertexdaten sind die Texturkoordinaten zu exportieren. Da die Texturkoordinaten innerhalb der Faces gespeichert werden. Sinnvollerweise überprüfen wir voher noch, ob überhaupt Texturdaten vorhanden sind:

  if (msh.hasFaceUV()==1):
    out.write('<texturecoords comp="2">\n')
    for face in msh.faces:
      for vert in face.v:
        out.write( ' %f %f %f %f %f %f\n' % (face.uv[0][0],face.uv[0][1],face.uv[1][0],face.uv[1][1],face.uv[2][0],face.uv[2][1]) )
    out.write('</texturecoords>\n')

Normalen

Normalvektoren sind in Blender sowohl per Vertex als auch per Face abgespeichert. Abhängig davon, ob eine Fläche als Smooth (per Vertex) oder Solid (per Face) dargestellt wird, werden die die entsprechenden Normalvektoren ausgewählt. Damit auch Klötze mit Kanten dargestellt werden können müssen beide Fälle berücksichtigt werden. Im Fall Solid müssen für alle Vertices der Fläche der Normalvektor der Fläche verwendet werden. Im Fall Smooth muss hier der Normalvektor der Vertices benutz werden. Auf keinen Fall sollte man nun die Vervielfachung der Vertices als Problem ansehen, da die verschiedenen Normalvektoren, diesen Nachteil wieder vollkommen ausgleichen.

Tipp: Da ganze Flächen als Solid keinerlei optische Rundungen zulassen, ist der einzige Weg, echte Kanten an runden Objekten zu erzeugen in dem man das Mesh an der Stelle aufteilt. Leider muss man zum aufteilen einen vollständigen Edgeloop auswählen. An Stellen die nicht kantig sein sollen muss man dann beide Vertices markieren und mit "remove doubles" vereinen.

Auch hier erst einmal ein einfaches Beispiel was häufig schon genügen sollte:

  out.write('<normals>\n')
  for face in msh.faces:
     for vert in face.v:
        out.write( ' %f, %f, %f,' % (msh.verts[vert.index].no.x, msh.verts[vert.index].no.y, msh.verts[vert.index].no.z) )
     out.write('\n')
  out.write('</normals>\n')

Besser wird es, wenn Smooth/Solid berücksichtigt wird.

  out.write('<normals>\n')
  for face in msh.faces:
    if (face.smooth==1):
      for vert in face.v:
        out.write( ' %f %f %f' % (msh.verts[vert.index].no.x, msh.verts[vert.index].no.y, msh.verts[vert.index].no.z))
      out.write('\n')
    else:
      for vert in range(0,3):
        out.write( ' %f, %f, %f,' % (face.no.x, face.no.y, face.no.z))
      out.write('\n')
  out.write('</normals>\n')



Tangenvektor / TBN-Matrix

Ohne die TBN-Matrix lässt sich in Shadern kein sinnvolles Bumpmapping durchführen. Weder Dot3, Offset, Parallax noch Reliefmapping sind ohne TBN-Matrix möglich. Eine TBN-Matrix besteht aus drei Vektoren, die den Texturespace aufspannen:

Normale

Er steht immer senkrecht zur Oberfläche. Er entspricht genau dem Normalvektor wie wir ihn kennen. Beim Paralax- und Reliefmapping repräsentiert seine Länge den Masstab für die dritte Texturkoordinate (Im Editor wäre sie W, auch wenn die Texturkoordinaten in OpenGL s, t, p und q sind).

Tangente

Er liegt tangential auf der Textur und ist dort zur U Achse ausgerichtet. Wieder einmal kann man überlegen, ob es nun bedeutet senkrecht zum Normal oder parallel zur Fläche des Dreiecks. Wer keine Kopfschmerzen hat macht sich welche.

Bitangente

Häufig wird er auch Binormale genannt, jedoch beschreibt Bitangente die Funktion besser. Er ist wie der Tangent tangential zur V Achse der Textur ausgerichtet. Wer meint, die Bitangente lässt sich durch das Kreuzprodukt von Normal und Tangend berechnen, hat nur Recht, wenn die Textur keine Scherung enthält.

Solange die Textur nicht verzerrt wird, ist eine normalisierte TBN-Matrix problemlos verwendbar, wenn jedoch die Tiefenkomponente der Textur nicht auf kubischen Texeln basiert, ist es sinnvoller die Korrektur im Shader vorzunehmen, als die Komponenten der TBN- Matrix zu verzerren.

Da wir es jetzt anscheinend verstanden haben, wie wir den zusätzlichen Komponenten der TBN-Matrix berechnen können, kommt hier der Code zum Erzeugen der Tangent und Bitangentvektoren pro Face und Vertex:

def createFaceTan(msh):
  ftan = []
  for face in msh.faces:
    tan = msh.verts[face.v[2].index].co - msh.verts[face.v[0].index].co
    if (face.uv[2][1] != face.uv[1][1]):
      m = (face.uv[2][1]-face.uv[0][1])/(face.uv[2][1]-face.uv[1][1])
      tan += (msh.verts[face.v[1].index].co - msh.verts[face.v[2].index].co) * m
    tan.normalize()
    ftan += [tan]
  return ftan

def createFaceBit(msh):
  fbit = []
  for face in msh.faces:
    bit = msh.verts[face.v[1].index].co - msh.verts[face.v[0].index].co
    if (face.uv[1][0] != face.uv[2][0]):
      m = (face.uv[1][0]-face.uv[0][0])/(face.uv[1][0]-face.uv[2][0])
      bit += (msh.verts[face.v[2].index].co - msh.verts[face.v[1].index].co) * m
    bit.normalize()
    fbit += [bit]
  return fbit

def interpolate(msh,ftan):
  vtan = []
  for vert in msh.verts:
    vtan += [Blender.NMesh.Vert().co]
  for face in msh.faces:
    if (face.smooth == 1):
      for vert in face.v:
        vtan[vert.index] += ftan[vert.index]
  for i in range(0,len(msh.verts)):
    vtan[i].normalize()
  return vtan	

ftan = createFaceTan(msh)
vtan = interpolate(msh,ftan)
fbit = createFaceBit(msh)
vbit = interpolate(msh,fbit)

Hier noch der Link zum Artikel wo ich die Berechnung erkläre: http://wiki.delphigl.com/index.php/TBN_Matrix


Mit folgenden Codeschnipseln lassen sich Tangente, Bitangete und die TBN-Matrix ausgeben. Sinnvoll ist es natürlich nur die Einzelkomponenten zusätzlich zum Normalvektor zu schreiben oder die TBN-Matrix ohne Normalen zu verwenden.

  out.write('<tan>\n')
  for i in range(0,len(msh.faces)):
    face = msh.faces[i]
    if (face.smooth==1):
      for vert in face.v:
        out.write( ' %f %f %f' % (vtan[vert.index].x, vtan[vert.index].y, vtan[vert.index].z))
      out.write('\n')
    else:
      for vert in range(0,3):
        out.write( ' %f %f %f' % (ftan[i].x, ftan[i].y, ftan[i].z))
      out.write('\n')
  out.write('</tan>\n')
  
  out.write('<bit>\n')
  for i in range(0,len(msh.faces)):
    face = msh.faces[i]
    if (face.smooth==1):
      for vert in face.v:
        out.write( ' %f %f %f' % (vbit[vert.index].x, vbit[vert.index].y, vbit[vert.index].z))
      out.write('\n')
    else:
      for vert in range(0,3):
        out.write( ' %f %f %f' % (fbit[i].x, fbit[i].y, fbit[i].z))
      out.write('\n')
  out.write('</bit>\n')
  
  out.write('<tbn>\n')
  for i in range(0,len(msh.faces)):
    face = msh.faces[i]
    if (face.smooth==1):
      for vert in face.v:
	out.write( ' %f %f %f' % (vtan[vert.index].x, vtan[vert.index].y, vtan[vert.index].z))
        out.write( ' %f %f %f' % (vbit[vert.index].x, vbit[vert.index].y, vbit[vert.index].z))
        out.write( ' %f %f %f\n' % (msh.verts[vert.index].no.x, msh.verts[vert.index].no.y, msh.verts[vert.index].no.z))
    else:
      for vert in range(0,3):
        out.write( ' %f %f %f' % (ftan[i].x, ftan[i].y, ftan[i].z))
        out.write( ' %f %f %f' % (fbit[i].x, fbit[i].y, fbit[i].z))
	out.write( ' %f %f %f\n' % (face.no[0], face.no[1], face.no[2]))
  out.write('</tbn>\n')
Warnung.png Die Vektoren scheinen alle Senkrecht zu einander zu stehen, jedoch ist noch nicht überprüft, ob die Matrizen wirklich brauchbar sind. Es wäre durchaus möglich, dass noch Komponenten verdreht oder gespiegelt sind.

Aufgrund des Mangels an Attributevariablen, ist es nicht sehr sinnvoll die volle TBN-Matrix zu übergeben. Sinnvoller ist es, nur den Tangendvektor (Bitangent kann durch ein Kreuzprodukt neu berechnet werden) oder den Tangentvektor zusammen mit dem Bitangentvektor als Attribute zu übergeben.

Vertuxgruppen / Bones

Ohne Vertexgruppen und Bones lassen sich keine Animationen mit Hilfe des Vertexshaders realisieren. Diesen Teil können alle überspringen, die keine Vertexshader zum Animieren benutzen wollen. Die Daten liegen in Blender in einer für Vertexshader ungünstigen Form vor und müssen etwas aufbereitet werden. In jeder Vertexgruppe, auf die man auch noch über Ihren Namen zugreifen muss, liegt eine Liste mit der Vertexnummer und der Gewichtung. Für einen Vertexshader benötigen wir pro Vertex einen Index, der die Vertexgruppe/Bone beschreibt und die Gewichtung. Außerdem müssen wir uns voher entscheiden, wie viele Bones wir maximal pro Vertex berücksichtigen.

Bei Mangel an "attribute"-Variablen macht es Sinn, die Gewichtung (muss kleiner als 1 sein) mit den Indizes zu addieren. Im Shader könnte man die dann wieder herausrechnen. Auf heutiger Hardware sind 16 Attribute-Variablen möglich.

Als erstes den Codeblock zum Aufbereiten der Daten. Es ist wichtig, dass die Variable num_of_vertexgroups.val die gewünschte Anzahl von Vertexgruppen enthält:

  vertGroupData = []
  groups = msh.getVertGroupNames()
  for vert in msh.verts:
    list = []
    count = 1
    for group in groups:
      if (len(msh.getVertsFromGroup(group,0,[vert.index]))==1):
        list += [(msh.getVertsFromGroup(group,1,[vert.index])[0][1],count)]
      count += 1
      list.sort()
      list.reverse()
      for i in range(0,num_of_vertexgroups.val):
        list += [(0.0, 0)]
      vertGroupData += [(list[0:num_of_vertexgroups.val])]

Hinweis: Es gibt mit getVertexInfluences() eine noch bessere Methode, um zu ermitteln, welche Vertexgruppe zu einem Vertex gehört. Dann wäre jedoch der Einsatz eines Dictonarys nötig.

Nun noch der Code zum schreiben:

  out.write('<vertexgroups comp="%i">\n<weight>\n' % num_of_vertexgroups.val)
  for face in msh.faces:
    for vert in face.v:
      for group in vertGroupData[vert.index]:
        out.write( ' %f' % (group[0]))
    out.write('\n')
  out.write('</weight>\n<index>\n')
  for face in msh.faces:
    for vert in face.v:
      for group in vertGroupData[vert.index]:
        out.write( ' %i' % (group[1]))
    out.write('\n')
  out.write('</index>\n</vertexgroups>\n')

Optimierung der Daten

Bisher haben wir uns hauptsächlich damit beschäftig, wie wir die Daten aus der Blenderstruktur auslesen und in VBO-kompatible Daten umformen. Wenn die Modelle größer werden oder komplette Level in Blender realisiert werden sollen macht es Sinn die Daten so umzusortieren, das Triangles mit ähnlichen Eigenschaften wie Ort oder Ausrichtung nah im Vertexbufferobjekt beieinander liegen.

Octree

Beim Octree werden nur die Teile gerendert, bei denen voher bekannt ist, dass sie sichtbar sind, leider ist die Baumstruktur eines Octrees auf dem erstem Blick völlig inkompatibel zu dem linearen Verlauf des Vertexbuffersobjektes.

Um die Faces in den Octree einzusortieren, muss von jedem der Schwerpunkt berechnet werden. Die Position im Octree wird mit einer Art Hashfunktion berechnet, die erst aus den X Y und Z Koordinaten Integerwerte von 0 bis 1023 bildet und deren Bits so zusammensortiert, dass ein einziger Wert von 0 bis 2^30-1 entsteht, nach dem die Faces sortiert werden:

def octree(msh):
  # Boundingbox for Octree. 
  # Warnung: bei mehreren VBOs sollte die gleiche Boundingbox verwendet werden,
  # damit nur einmal auf sichbarkeit überprüft werden muss
  mini = msh.verts[0].co * 1.0
  maxi = msh.verts[0].co * 1.0
  for vert in msh.verts:
    mini.x = minimum (mini.x,vert.co.x)
    mini.y = minimum (mini.y,vert.co.y)
    mini.z = minimum (mini.z,vert.co.z)
    maxi.x = maximum (maxi.x,vert.co.x)
    maxi.y = maximum (maxi.y,vert.co.y)
    maxi.z = maximum (maxi.z,vert.co.z)
  size = max (maxi.x-mini.x,maxi.y-mini.y,maxi.z-mini.z)
  #Hashberechnung
  optindex = []
  count = 0
  for face in msh.faces:
    center = msh.verts[0].co * 0.0
    for vert in face.v:
       center += msh.verts[vert.index].co
    #range 0.0 ... 1.0
    center *= 1.0 /( len(face.v) * size * 2.0)
    center.x += 0.5
    center.y += 0.5
    center.z += 0.5
    ix = int (center.x * 1023)
    iy = int (center.y * 1023)
    iz = int (center.z * 1023)
    sortby  = (ix&512)<<18 | (ix&256)<<16 |(ix&128)<<14 | (ix&64)<<12 |(ix&32)<<10 | (ix&16)<<8 |(ix&8)<<6 | (ix&4)<<4 |(ix&2)<<2 | (ix&1)<<0
    sortby |= (iy&512)<<19 | (iy&256)<<17 |(iy&128)<<15 | (iy&64)<<13 |(iy&32)<<11 | (iy&16)<<9 |(iy&8)<<7 | (iy&4)<<5 |(iy&2)<<3 | (iy&1)<<1
    sortby |= (iz&512)<<20 | (iz&256)<<18 |(iz&128)<<16 | (iz&64)<<14 |(iz&32)<<12 | (iz&16)<<10|(iz&8)<<8 | (iz&4)<<6 |(iz&2)<<4 | (iz&1)<<2
    optindex += [(sortby,count)]
    count += 1
  #Umsortierung der Daten
  optindex.sort()
  flist=[]
  for i in optindex:
    flist += [msh.faces[i[1]]]
  msh.faces=flist
  #Hier sollten noch die Verwaltungsdaten für den Octree geschrieben werden
  #Diese Code fehl allderings noch

  return msh

Diese Funktion sollte direkt nach dem Umwandeln der Quads in Triangles aufgerufen werden, da die Reihenfolge der Dreiecke geändert wird.

Prinzipiell gibt es für jedes würfelförmige Volumen nur eine Start und Stopindexnummer die beim Rendern übergeben werden muss:

glDrawArrays( GL_TRIANGLES, start, stop - start);

Grundsätzlich sollte beachtet werden, dass es bei der Verwendung von VBOs möglichst viele Triangles auf einmal gerendert werden sollten. Wo die Untergrenze pro Volumen liegt, lässt sich schwer sagen, es ist aber sinnvoll, diese jenseits der 1000-10000 anzusetzten.

Vorsortierung nach Vertexgruppen/Bones

Es ist zwar noch kein Algoritmus zum Sortieren vorhanden, jedoch ist es bei animierten Modellen sinnvoll, möglichst ganze Bones auf einmal abzuarbeiten, da ein Bone ganze 12 Uniform Floats im Vertexshader verbraucht (9 für eine Rotationsmatrix und 3 für das Gelenk). Laut GLSL Spezifikation soll man minimal 512 davon haben, was maximal 40 Bones entspricht. Dummerweise scheint es so, als stellten einige Grafikkarten nur 256 Uniform Floats zur Verfügung, was zur Folge hat, das nur noch 20 Bones möglich wären. Eine primitive Optimierung wäre nur einen halbes Model zu speichen und für die zweite Hälfte das gleiche Mesh gespiegelt mit neuen Bones zu verwenden.

Die sinnvollste Variante ist die schon erzeugten Vertexgruppendaten zum Sortieren zu benutzen: Die Gruppe mit der höchsten Gewichtung wird zum Einsortieren benutzt. Alle Vertices die nicht in einer Gruppe sind, landen in einer Extra-Gruppe. Diese Gruppen werde nun so umsortiert, dass Gruppen mit gegenseitigem Einfluss nah beieinander landen und Vertexgruppen die sich nicht beeinflussen, weit von einander im VBO entfernt liegen.

Daten für indizierte VBOs exportieren

Lange Zeit hab ich dieses Thema umgangen, da der Export relativ komplex ist. Alle Daten aus Blender müssen komplett neu umgeordnet und neu indiziert werden, da Blender die Texturkoordinaten nicht per Vertex, sondern per Face abspeichert. Als Beispiel wird diesmal ein vereinfachter OBJ-Exporter dienen, der allerdings um Tangentenvektoren ergänzt wird.

#!BPY
"""
Name: 'VBO Wiki'
Blender: 241
Group: 'Export'
Tooltip: 'VBO Wiki Exporter 3 C version'
"""
import Blender
from Blender import NMesh
from Blender.BGL import *
from Blender.Draw import *
import math
from math import *

#leicht verständliche funktion jedoch sehr ineffezient
def lfind(liste,key):
    for i in range(0, len(liste)):
	if liste[i]==key: return i

def write(filename):
  global num_of_vertexgroups,TBNMenu,Optimise
  filename=replace(filename,".blend",".obj")
  out = file(filename, 'w')
  
  objs = Blender.Object.GetSelected()
  if (len(objs)==0):
    objs = Blender.Object.Get()
  #eigendlich nur 1 durchlauf sinnvoll
  for obj in objs:
    if (obj.getType()=='Mesh'):
    
      msh = obj.getData()
      # quad2tri könnte von obern hier eingesetzt werden
      # msh = quad2tri(msh)
      raw_index = []
      for face in msh.faces:
        for i in range (0,len (face.v)):
           vert = []
	   vert += [face.v[i].index]
	   vert += [face.uv[i][0]]
	   vert += [face.uv[i][1]]
	   raw_index += [vert]

      raw_index.sort()
      last =raw_index[-1]
      for i in range(len(raw_index)-2, -1, -1):
       if last==raw_index[i]:
	 del raw_index[i]
       else: last=raw_index[i]
     
      out.write('g Index\n')
      
      for i in raw_index:
	 v = msh.verts[i[0]].co
         out.write('v %f %f %f\n' % (v.x,v.y,v.z))
      
      for i in raw_index:
	 v = msh.verts[i[0]].no
         out.write('vn %f %f %f\n' % (v.x,v.y,v.z))
      
      for i in raw_index:
	 out.write('vt %f %f\n' % (i[1],i[2]))

      tan = [Blender.Mathutils.Vector(0,0,0) for i in range(0,len(raw_index))]
      for face in msh.faces:
        v1 = face.v[1].co - face.v[0].co 
	v2 = face.v[2].co - face.v[0].co
	t1 = face.uv[1][1] - face.uv[0][1]
	t2 = face.uv[2][1] - face.uv[0][1]
	
	sdir = v1 * t2 - v2 * t1 
	sdir.normalize()
	for i in range (0,len (face.v)):
	  vert = [face.v[i].index,face.uv[i][0],face.uv[i][1]]
	  tan[lfind(raw_index, vert)] += sdir
      
      for i in range (0,len(raw_index)):
	 normal = msh.verts[raw_index[i][0]].no
	 otan = tan[i] - normal *  Blender.Mathutils.DotVecs( tan[i], normal )
	 otan.normalize()
	 out.write('tan %f %f %f\n' % (otan.x,otan.y,otan.z))

      for face in msh.faces:
        out.write('f')
	for i in range (0,len (face.v)):
	   vert = [face.v[i].index,face.uv[i][0],face.uv[i][1]]
  	   out.write(' %i' % (lfind(raw_index, vert)))
	out.write('\n')
	
  out.close()
  Exit()

#Blender.Window.FileSelector(write, "Export")
write ("out.obj")

Zusätzliche Optionen abfragen

Ein Exporter ist schön, etwas unschön ist es bis jetzt, das die Ausgabe immer statisch nach dem gleichem Schema erfolgt. Um bestimmte Optionen wählbar zu machen, ist es nötig einen Dialog zu öffnen, dessen Eingaben in globalen Variablen abgelegt werden. Aus codetechnischer Sicht ist es einfacher die Optionen vor dem Filedialog abzufragen. Damit ein Dialog verwendet werden kann, sind ein paar Änderungen am Code nötig.

Folgende Funktionen und globale Variablen müssen eingefügt werden:

num_of_vertexgroups = Create(4)
TMenu = Create(1)
EVENT_NOEVENT = 1
EVENT_EXPORT  = 2
EVENT_CANCEL  = 3

def draw():
  global num_of_vertexgroups,TMenu
  global EVENT_NOEVENT,EVENT_EXPORT
  glClear(GL_COLOR_BUFFER_BIT)
  glRasterPos2d(10, 125)
  Text("DGL Exporter Options")
  
  TMemu = Menu("Normal|Normal+Tangent|TBN-Matrix",EVENT_NOEVENT,10,75,210,18,  1)
  num_of_vertexgroups= Number("No of Vertgroups: ", EVENT_NOEVENT, 10, 55, 210, 18,num_of_vertexgroups.val, 0, 10, "Number of Vertgroups per Vertex");
  Button("Export",EVENT_EXPORT, 140, 10, 80, 18)
  Button("Cancel",EVENT_CANCEL, 10, 10, 80, 18)
  
def event(evt, val):
   if (evt == QKEY and not val): 
      Exit()

def bevent(evt):
  global EVENT_NOEVENT,EVENT_EXPORT
  if (evt== EVENT_EXPORT): 
    Blender.Window.FileSelector(write, "Export")
  elif (evt == EVENT_CANCEL):
    Exit()

Code noch nicht fertig

Die letzte Zeile, welche den Filedialog geöffnet hat, wird durch folgende ersetzt:

Register(draw, event, bevent)

Damit die write-Funktion keine Endloschleife verursacht, sollte sie noch um Exit() ergänzt werden.

Externe Links

http://en.wikibooks.org/wiki/Blender_3D:_Noob_to_Pro/Advanced_Tutorials/Python_Scripting/Export_scripts