kleiner "Wavefront (.obj) to OGRE (.mesh)" - Konverter

Fragen zu Grafik- & Soundproblemen und zur Spieleprogrammierung haben hier ihren Platz.
Benutzeravatar
Sunny
Beiträge: 290
Registriert: 19.02.2009 06:02

kleiner "Wavefront (.obj) to OGRE (.mesh)" - Konverter

Beitrag von Sunny »

Da ich seit gestern ein wenig mit den 3D-Befehlen von PureBasic rumspiele ist mir mal ein kleiner Geistesblitz gekommen ^^
Ich erstelle liebend gern 3D-Objekte mit dem Programm Cinema 4D. Leider ist es nur äußerst umständlich und über viele Umwege möglich diese Objekte in .mesh-Dateien umzuwandeln, um sie in PB-Projekten nutzen zu können.
Deshalb habe ich mal einen kleinen Konverter gebastelt, welcher .obj-Dateien in .mesh-Dateien umwandelt.
Ich hab erst überlegt, ob ich ihn in der Kategorie "Code, Tipps und Tricks" Poste aber das habe ich aufgrund der Kategorie-Beschreibung dann doch lieber sein lassen.
("Hier könnt Ihr gute, von Euch geschriebene Codes posten. Sie müssen auf jeden Fall funktionieren und sollten möglichst effizient, elegant und beispielhaft oder einfach nur cool sein.")
Da mein Code allerdings alles andere als effizient, elegant und beispielhaft ist, sondern einfach nur mal so für mich sein sollte, stelle ich ihn einfach mal hier rein.
Vieleicht dient die Idee ja anderen als kleine Anregung für eigene Projekte.

Bevor ich den Code Poste allerdings noch ein paar kleine Hinweise.
-Das Programm wurde bis jetzt nur mit .obj-Dateien angetestet, die mittels Cinema 4D exportiert wurden.
-Die Faces (/Flächen/Polygone) müssen trianguliert sein (sie dürfen nur aus Dreiecken bestehen).
-Der ungefähre Mittelpunkt des erstellten Objekts sollte sich ca. auf den Koordinaten X = 0, Y = 0 und Z = 0 befinden (das ist allerdings nur für eine saubere Vorschau von bedeutung).
-Wie schon erwähnt ist der Quellcode ziemlich unsauber, weil ich ihn anfangs nur mal schnell für mich selbst gecoded hab (also bitte nich meckern ^^).
-Da ich noch keine ausführlichen Tests gemacht habe kann ich nichts genauers über irgendwelche Bugs sagen.

So... hier jetzt mal der Code:

Code: Alles auswählen

If InitEngine3D() = 0
  End
EndIf

If InitSprite() = 0
  End
EndIf

Structure vtx
  x.f
  y.f
  z.f
EndStructure

Structure fce
  a.i
  b.i
  c.i
EndStructure

Structure obj
  name.s
  level.i
  List vertex.vtx()
  List face.fce()
EndStructure

NewList object.obj()
NewList tosort.f()

Procedure ReadObject(ObjFile.s, List object.obj())
  Protected.s Row, Type, Pointer
  Protected.i Counter
  
  If OpenFile(0, ObjFile)
    While Eof(0) = 0
      Row = ReadString(0)
      
      Type = StringField(Row, 1, " ")
      
      If Type = "g"
        AddElement(object())
        object()\name = StringField(Row, CountString(Row, " ") + 1, " ")
        object()\level = CountString(Row, " ") - 1
      ElseIf Type = "v"
        AddElement(object()\vertex())
        object()\vertex()\x = ValF(StringField(Row, 2, " "))
        object()\vertex()\y = ValF(StringField(Row, 3, " "))
        object()\vertex()\z = ValF(StringField(Row, 4, " "))
        
      ElseIf Type = "f"
        
        AddElement(object()\face())
        Pointer = StringField(Row, 2, " ")
        object()\face()\a = Val(StringField(Pointer, 1, "/"))
        Pointer = StringField(Row, 3, " ")
        object()\face()\b = Val(StringField(Pointer, 1, "/"))
        Pointer = StringField(Row, 4, " ")
        object()\face()\c = Val(StringField(Pointer, 1, "/"))
        
      EndIf
      
    Wend
    
    CloseFile(0)
  EndIf
EndProcedure

If OpenWindow(0, 0, 0, 400, 448, "2 Mesh", #PB_Window_SystemMenu|#PB_Window_ScreenCentered)
  
  If OpenWindowedScreen(WindowID(0), 0, 24, 400, 400, 0, 0, 0)
    ButtonGadget(0, 0, 0, 400, 24, ".obj-Datei öffnen")
    ButtonGadget(1, 0, 424, 400, 24, ".obj zu .mesh konvertieren")
    
    CreateCamera(0, 0, 0, 100, 100)
    
    CreateLight(0, RGB(0, 0, 200), 1.5, 1.5, -2)
    CreateLight(1, RGB(255, 200, 80), -3, -1, -2)
    
    CreateTexture(0, 256, 256)

    StartDrawing(TextureOutput(0))
    Box(0, 0, 256, 256, RGB(200, 200, 200))
    StopDrawing()
    
    CreateMaterial(0, TextureID(0))
    
    Repeat
      event = WaitWindowEvent(20)
      
      If event = #PB_Event_Gadget
        If EventGadget() = 0
          file$ = OpenFileRequester("Datei öffnen...", "C:\", "Wavefront (*.obj)|*.obj", 0)
          
          If file$
            ClearList(object())
            ReadObject(file$, object())
            
            ClearList(tosort())
            
            CreateMesh(0)
            Xall.f = 0
            Yall.f = 0
            Zall.f = 0
            All = 0
            
              ForEach object()\vertex()
                AddMeshVertex(object()\vertex()\x, object()\vertex()\y, object()\vertex()\z)
                
                Xall.f + object()\vertex()\x
                Yall.f + object()\vertex()\y
                Zall.f + object()\vertex()\z
                All + 1
                
                AddElement(tosort())
                tosort() = object()\vertex()\x
                AddElement(tosort())
                tosort() = object()\vertex()\y
                AddElement(tosort())
                tosort() = object()\vertex()\z
              Next
              
              ForEach object()\face()
                AddMeshFace(object()\face()\a - 1, object()\face()\b - 1, object()\face()\c - 1)
              Next
            FinishMesh()
            
            SortList(tosort(), #PB_Sort_Ascending)
            
            SelectElement(tosort(), 0)
            smallest.f = tosort()
            
            SelectElement(tosort(), ListSize(tosort()) - 1)
            biggest.f = tosort()
            
            NormalizeMesh(0)
            
            CreateEntity(0, MeshID(0), MaterialID(0))
            
            CameraLocate(0, 0, (smallest - biggest) * -1 * 2, (smallest - biggest) * 3)
            CameraLookAt(0, Xall / All, Yall / All, Zall / All)
            
            LightLocate(0, (smallest - biggest) * -1 * 1.5, (smallest - biggest) * -1 * 1.5, (smallest - biggest) * -1 * -2)
            LightLocate(1, (smallest - biggest) * -1 * -3, (smallest - biggest) * -1 * -1, (smallest - biggest) * -1 * -2)
            
            MeshCreate = 1
            
          EndIf
        ElseIf EventGadget() = 1
          savefile$ = SaveFileRequester("Datei speichern...", "C:\Unbenannt.mesh", "Alle Dateien (*.*)|*.*", 0)
          
          If savefile$
            If Right(savefile$, 5) <> ".mesh"
              savefile$ + ".mesh"
            EndIf
            
            SaveMesh(0, savefile$)
            
          EndIf
        EndIf
      EndIf
      
      If MeshCreate = 1
        RotateEntity(0, 1, 2, 0, #PB_Relative)
        
      EndIf
      
      ClearScreen(RGB(0,0,0))
      RenderWorld()
      FlipBuffers()
    Until event = #PB_Event_CloseWindow
    
    End
    
  EndIf
  
EndIf
Benutzeravatar
Danilo
-= Anfänger =-
Beiträge: 2284
Registriert: 29.08.2004 03:07

Re: kleiner "Wavefront (.obj) to OGRE (.mesh)" - Konverter

Beitrag von Danilo »

Crasht hier immer mit C4D R14. Kannst Du mal ein paar nachvollziehbare Schritte angeben, was Du genau machst?
cya,
...Danilo
"Ein Genie besteht zu 10% aus Inspiration und zu 90% aus Transpiration" - Max Planck
Benutzeravatar
Sunny
Beiträge: 290
Registriert: 19.02.2009 06:02

Re: kleiner "Wavefront (.obj) to OGRE (.mesh)" - Konverter

Beitrag von Sunny »

Wo genau Crasht denn das Prog?
Hab nämlich grade mitbekommen, dass es bei dem Befehl NormalizeMesh(#Mesh) abschmiert, wenn die .obj-Datei mehr als ein Objekt enthält. Das ist natürlich ziemlich doof aber es ist wie schon erwähnt nur mal eben schnell von hingeklatschter Quellcode.
Hast du mal geguckt, ob es Funktioniert, wenn du dir nur ein einziges objekt erstellst?



Edit:
bzgl. der nachvollziehbaren Schritte...

1. Ich starte C4D ^^
2. Ich erstelle ein Objekt
3. Sollte das Objekt kein Polygon-Objekt sein, Convertiere ich es zu einem (dazu wähle ich das Objekt aus und drücke die Taste "C" auf der Tastatur")
4. Wenn das Objekt fertig modelliert ist, wähle ich alle Flächen aus (dazu gehe ich in den Polygon-Bearbeiten-Modus und drücke die Tastenkombination "Strg + A" auf der Tastatur)
5. Danach mache ich einen Rechtsklick in einer der Perspektivansichten und wähle dann in dem Popup-Menü "Triangulieren" aus, damit alle Polygone, welche nicht aus Dreiecken bestehen in solche umgewandelt werden.
6. Ich exportire das Objekt als .obj-Datei "Datei > Exportieren > Wavefront (*.obj)"
7. Zu guterletzt starte ich den Quellcode, klicke auf den Button ".obj-Datei öffnen", wähle mir die gewünschte Datei aus und klicke dann auf den Button ".obj zu .mesh konvertieren"
Benutzeravatar
Danilo
-= Anfänger =-
Beiträge: 2284
Registriert: 29.08.2004 03:07

Re: kleiner "Wavefront (.obj) to OGRE (.mesh)" - Konverter

Beitrag von Danilo »

Sunny hat geschrieben:1. Ich starte C4D ^^
2. Ich erstelle ein Objekt
3. Sollte das Objekt kein Polygon-Objekt sein, Convertiere ich es zu einem (dazu wähle ich das Objekt aus und drücke die Taste "C" auf der Tastatur")
4. Wenn das Objekt fertig modelliert ist, wähle ich alle Flächen aus (dazu gehe ich in den Polygon-Bearbeiten-Modus und drücke die Tastenkombination "Strg + A" auf der Tastatur)
5. Danach mache ich einen Rechtsklick in einer der Perspektivansichten und wähle dann in dem Popup-Menü "Triangulieren" aus, damit alle Polygone, welche nicht aus Dreiecken bestehen in solche umgewandelt werden.
6. Ich exportire das Objekt als .obj-Datei "Datei > Exportieren > Wavefront (*.obj)"
7. Zu guterletzt starte ich den Quellcode, klicke auf den Button ".obj-Datei öffnen", wähle mir die gewünschte Datei aus und klicke dann auf den Button ".obj zu .mesh konvertieren"
OK, mit einer Kugel funktioniert es. Nehme ich das Standard-Objekt "Figur" in C4D, crasht es beim laden in PB.

Genau solche Dinge fehlen bei PB/OGRE mMn noch. Verschiedene Formate laden und ein richtiger WorldEditor.
Alles ziemlich aufwendig und undokumentiert. IMO ein Grund warum es kaum genutzt wird.
cya,
...Danilo
"Ein Genie besteht zu 10% aus Inspiration und zu 90% aus Transpiration" - Max Planck
Benutzeravatar
Sunny
Beiträge: 290
Registriert: 19.02.2009 06:02

Re: kleiner "Wavefront (.obj) to OGRE (.mesh)" - Konverter

Beitrag von Sunny »

Ja, mit dem Figur-Objekt in C4D funktioniert es leider nicht, denn wenn du es in ein Polygon-Objekt umwandelst, wirst du feststellen, dass es aus mehreren Objekten besteht. Mal sehn... Wenn ich mal Zeit habe, werde ich das Prog vieleicht selber verbessern, denn einen Lösungsansatz habe ich schon im Kopf.
Benutzeravatar
Danilo
-= Anfänger =-
Beiträge: 2284
Registriert: 29.08.2004 03:07

Re: kleiner "Wavefront (.obj) to OGRE (.mesh)" - Konverter

Beitrag von Danilo »

Sunny hat geschrieben:Ja, mit dem Figur-Objekt in C4D funktioniert es leider nicht, denn wenn du es in ein Polygon-Objekt umwandelst, wirst du feststellen, dass es aus mehreren Objekten besteht.
Das ist ja kein Problem. Du hast zwar alle Objekte geladen, aber nur das letzte Objekt dem Mesh hinzugefügt.
Habe ein "ForEach object()" rein gemacht, und schon ging es.
C4D scheint auch beim Speichern automatisch zu triangulieren. Es speichert entweder 1 Dreieck, oder 2 Dreiecke.
Habe das in ReadObject() mal geändert.

In C4D eine Figur laden/erstellen und dann als Wavefront Obj exportieren. Muss man nicht erst
in Polygon umwandeln oder triangulieren.

Sieht dann so aus:
Bild Bild
Bild Bild
Bild Bild

Rotieren mit Tasten 1/2, 3/4 und 5/6, +/- für Zoom:

Code: Alles auswählen

If InitEngine3D() = 0
  End
EndIf

If InitSprite() = 0
  End
EndIf

If InitKeyboard() = 0
  End
EndIf


Structure vtx
  x.f
  y.f
  z.f
EndStructure

Structure fce
  a.i
  b.i
  c.i
EndStructure

Structure obj
  name.s
  level.i
  List vertex.vtx()
  List face.fce()
EndStructure

NewList object.obj()
NewList tosort.f()

Procedure ReadObject(ObjFile.s, List object.obj())
  Protected.s Row, Type, Pointer
  Protected.i Counter
 
  If OpenFile(0, ObjFile)
    While Eof(0) = 0
      Row = ReadString(0)
     
      Type = StringField(Row, 1, " ")
     
      If Type = "g"
        AddElement(object())
        object()\name = StringField(Row, CountString(Row, " ") + 1, " ")
        object()\level = CountString(Row, " ") - 1
        ;Debug object()\name
      ElseIf Type = "v"
        AddElement(object()\vertex())
        object()\vertex()\x = ValF(StringField(Row, 2, " "))
        object()\vertex()\y = ValF(StringField(Row, 3, " "))
        object()\vertex()\z = ValF(StringField(Row, 4, " "))
       
      ElseIf Type = "f"
       
        ; 1. dreieck
        AddElement(object()\face())
        Pointer = StringField(Row, 2, " ")
        object()\face()\a = Val(StringField(Pointer, 1, "/"))
        Pointer = StringField(Row, 3, " ")
        object()\face()\b = Val(StringField(Pointer, 1, "/"))
        Pointer = StringField(Row, 4, " ")
        object()\face()\c = Val(StringField(Pointer, 1, "/"))

        Pointer = Trim(StringField(Row, 5, " "))
        If Pointer ; wenn vorhanden, dann sind es 2 dreiecke

            AddElement(object()\face())
            Pointer = StringField(Row, 4, " ")
            object()\face()\a = Val(StringField(Pointer, 1, "/"))
            Pointer = StringField(Row, 5, " ")
            object()\face()\b = Val(StringField(Pointer, 1, "/"))
            Pointer = StringField(Row, 2, " ")
            object()\face()\c = Val(StringField(Pointer, 1, "/"))
        EndIf

      EndIf
     
    Wend
   
    CloseFile(0)
  EndIf
EndProcedure

If OpenWindow(0, 0, 0, 800, 600, "2 Mesh", #PB_Window_SystemMenu|#PB_Window_ScreenCentered);|#PB_Window_Maximize)
 
  If OpenWindowedScreen(WindowID(0), 0, 24, WindowWidth(0), WindowHeight(0)-24, 0, 0, 0)
    ButtonGadget(0, 0, 0, WindowWidth(0)*0.5, 24, ".obj-Datei öffnen")
    ButtonGadget(1, WindowWidth(0)*0.5, 0, WindowWidth(0)*0.5, 24, ".obj zu .mesh konvertieren")
   
    CreateCamera(0, 0, 0, 100, 100)
   
    CreateLight(0, RGB(0, 0, 200), 1.5, 1.5, -2)
    CreateLight(1, RGB(255, 200, 80), -3, -1, -2)
   
    CreateTexture(0, 256, 256)

    StartDrawing(TextureOutput(0))
    Box(0, 0, 256, 256, RGB(200, 200, 200))
    StopDrawing()
   
    CreateMaterial(0, TextureID(0))
   
    Repeat
      event = WaitWindowEvent(20)
     
      If event = #PB_Event_Gadget
        If EventGadget() = 0
          file$ = OpenFileRequester("Datei öffnen...", "", "Wavefront (*.obj)|*.obj", 1)
         
          If file$
            ClearList(object())
            ReadObject(file$, object())
           
            ClearList(tosort())
           
            FirstElement(object())
           
            CreateMesh(0)
            Xall.f = 0
            Yall.f = 0
            Zall.f = 0
            All = 0
           
           ForEach object() ; alle objekte zum mesh hinzufügen
              ForEach object()\vertex()
                AddMeshVertex(object()\vertex()\x, object()\vertex()\y, object()\vertex()\z)
               
                Xall.f + object()\vertex()\x
                Yall.f + object()\vertex()\y
                Zall.f + object()\vertex()\z
                All + 1
               
                AddElement(tosort())
                tosort() = object()\vertex()\x
                AddElement(tosort())
                tosort() = object()\vertex()\y
                AddElement(tosort())
                tosort() = object()\vertex()\z
              Next
             
              ForEach object()\face()
                AddMeshFace(object()\face()\a - 1, object()\face()\b - 1, object()\face()\c - 1)
              Next
            Next
            FinishMesh()
           
            SortList(tosort(), #PB_Sort_Ascending)
           
            SelectElement(tosort(), 0)
            smallest.f = tosort()
           
            SelectElement(tosort(), ListSize(tosort()) - 1)
            biggest.f = tosort()
           
            NormalizeMesh(0)
           
            CreateEntity(0, MeshID(0), MaterialID(0))
           
            CameraLocate(0, 0, (smallest - biggest) * -1 * 2, (smallest - biggest) * 2)
            CameraLookAt(0, Xall / All, Yall / All, Zall / All)
           
            LightLocate(0, (smallest - biggest) * -1 * 1.5, (smallest - biggest) * -1 * 1.5, (smallest - biggest) * -1 * -2)
            LightLocate(1, (smallest - biggest) * -1 * -3, (smallest - biggest) * -1 * -1, (smallest - biggest) * -1 * -2)
           
            MeshCreate = 1
           
          EndIf
        ElseIf EventGadget() = 1
          savefile$ = SaveFileRequester("Datei speichern...", "Unbenannt.mesh", "Alle Dateien (*.*)|*.*", 0)
         
          If savefile$
            If Right(savefile$, 5) <> ".mesh"
              savefile$ + ".mesh"
            EndIf
           
            SaveMesh(0, savefile$)
           
          EndIf
        EndIf
      EndIf

      If ExamineKeyboard()
        x_rotation = 0
        y_rotation = 0
        z_rotation = 0
        If KeyboardPushed(#PB_Key_2) : y_rotation =  1 : EndIf
        If KeyboardPushed(#PB_Key_1) : y_rotation = -1 : EndIf

        If KeyboardPushed(#PB_Key_3) : z_rotation =  1 : EndIf
        If KeyboardPushed(#PB_Key_4) : z_rotation = -1 : EndIf

        If KeyboardPushed(#PB_Key_5) : x_rotation =  1 : EndIf
        If KeyboardPushed(#PB_Key_6) : x_rotation = -1 : EndIf

        If KeyboardPushed(#PB_Key_Add) : zoom.f - 0.01 : EndIf
        If KeyboardPushed(#PB_Key_Subtract) : zoom.f + 0.01 : EndIf

        If MeshCreate = 1
          RotateEntity(0, x_rotation, y_rotation, z_rotation, #PB_Relative)
          CameraLocate(0, 0, (smallest - biggest) * -1 * (2+zoom), (smallest - biggest) * (2+zoom))
          CameraLookAt(0, Xall / All, Yall / All, Zall / All)
        EndIf
      EndIf
     
      ClearScreen(RGB(0,0,0))
      RenderWorld()
      FlipBuffers()
    Until event = #PB_Event_CloseWindow
   
    End
   
  EndIf
 
EndIf
cya,
...Danilo
"Ein Genie besteht zu 10% aus Inspiration und zu 90% aus Transpiration" - Max Planck
Benutzeravatar
Sunny
Beiträge: 290
Registriert: 19.02.2009 06:02

Re: kleiner "Wavefront (.obj) to OGRE (.mesh)" - Konverter

Beitrag von Sunny »

C4D scheint auch beim Speichern automatisch zu triangulieren. Es speichert entweder 1 Dreieck, oder 2 Dreiecke.
Wenn du das auf die exportierten .obj-Datein beziehst, mus ich dir da leider wiedersprechen.
Dazu hier mal die .obj-Datei eines Würfels:

Code: Alles auswählen

# WaveFront *.obj file (generated by CINEMA 4D)

g W_rfel
v -100 -100 100
v -100 100 100
v 100 -100 100
v 100 100 100
v 100 -100 -100
v 100 100 -100
v -100 -100 -100
v -100 100 -100

vt 0 1 0
vt 1 0 0
vt 0 0 0
vt 0 0 0
vt 1 1 0
vt 0 1 0
vt 1 1 0
vt 0 0 0
vt 1 0 0
vt 1 0 0
vt 0 1 0
vt 1 1 0
vt 1 0 0
vt 0 0 0
vt 1 1 0
vt 0 1 0
vt 0 0 0
vt 1 0 0
vt 0 1 0
vt 1 1 0

f 3/9 4/12 2/6 1/3 
f 5/13 6/15 4/11 3/8 
f 7/18 8/20 6/16 5/14 
f 1/2 2/5 8/19 7/17 
f 4/10 6/15 8/19 2/4 
f 5/13 3/7 1/1 7/17 

Da legen wir mal unser Hauptaugenmerk mal auf folgende Zeilen.

Code: Alles auswählen

f 3/9 4/12 2/6 1/3 
f 5/13 6/15 4/11 3/8 
f 7/18 8/20 6/16 5/14 
f 1/2 2/5 8/19 7/17 
f 4/10 6/15 8/19 2/4 
f 5/13 3/7 1/1 7/17 
Die ersten Zahlen links neben den Slashes sind die Eckpunkte.
In der 1. Zeile wären das also die Punkte 3, 4, 2 und 1 somit besteht dieses Polygon schonmal aus 4 Eckpunkten und ist daher für eine .mesh-Datei unbrauchbar.
Du kannst ja auch mal einen Zylinder exportieren ohne ihn vorher trianguliert zu haben und dann mit deinem Programm öffnen, dann sollte schon die Darstellung im Vorschaufenster fehlerhaft sein.

Jetzt bin ich schon am überlegen, wie ich die Polygone, welche mehr als 3 Eckpunkte haben in Dreiecke zu unterteilen aber ich glaube für dieses spezielle Thema sollte in folgendem Thread weiter diskutiert werden:
http://forums.purebasic.com/german/view ... 4ec01bb6ed
Benutzeravatar
Danilo
-= Anfänger =-
Beiträge: 2284
Registriert: 29.08.2004 03:07

Re: kleiner "Wavefront (.obj) to OGRE (.mesh)" - Konverter

Beitrag von Danilo »

Sunny hat geschrieben:
C4D scheint auch beim Speichern automatisch zu triangulieren. Es speichert entweder 1 Dreieck, oder 2 Dreiecke.
Wenn du das auf die exportierten .obj-Datein beziehst, mus ich dir da leider wiedersprechen.
Dazu hier mal die .obj-Datei eines Würfels:

Code: Alles auswählen

# WaveFront *.obj file (generated by CINEMA 4D)

g W_rfel
v -100 -100 100
v -100 100 100
v 100 -100 100
v 100 100 100
v 100 -100 -100
v 100 100 -100
v -100 -100 -100
v -100 100 -100

vt 0 1 0
vt 1 0 0
vt 0 0 0
vt 0 0 0
vt 1 1 0
vt 0 1 0
vt 1 1 0
vt 0 0 0
vt 1 0 0
vt 1 0 0
vt 0 1 0
vt 1 1 0
vt 1 0 0
vt 0 0 0
vt 1 1 0
vt 0 1 0
vt 0 0 0
vt 1 0 0
vt 0 1 0
vt 1 1 0

f 3/9 4/12 2/6 1/3 
f 5/13 6/15 4/11 3/8 
f 7/18 8/20 6/16 5/14 
f 1/2 2/5 8/19 7/17 
f 4/10 6/15 8/19 2/4 
f 5/13 3/7 1/1 7/17 

Da legen wir mal unser Hauptaugenmerk mal auf folgende Zeilen.

Code: Alles auswählen

f 3/9 4/12 2/6 1/3 
f 5/13 6/15 4/11 3/8 
f 7/18 8/20 6/16 5/14 
f 1/2 2/5 8/19 7/17 
f 4/10 6/15 8/19 2/4 
f 5/13 3/7 1/1 7/17 
Die ersten Zahlen links neben den Slashes sind die Eckpunkte.
In der 1. Zeile wären das also die Punkte 3, 4, 2 und 1 somit besteht dieses Polygon schonmal aus 4 Eckpunkten und ist daher für eine .mesh-Datei unbrauchbar.
Du kannst ja auch mal einen Zylinder exportieren ohne ihn vorher trianguliert zu haben und dann mit deinem Programm öffnen, dann sollte schon die Darstellung im Vorschaufenster fehlerhaft sein.
Hast Du es mal probiert? :roll:

Ich speichere die obige Datei als Wuerfel.obj und es wird mit meinem letzten Code wunderbar angezeigt.

Deshalb schrieb ich auch: "Es speichert entweder 1 Dreieck, oder 2 Dreiecke":

Code: Alles auswählen

Ein Dreieck:

1        2
 ________
 \      |
  \     |
   \    |
    \   |
     \  |
      \ |
       \|
         3

Dreieck: Punkte 1, 2, 3

         
         
2 Dreiecke:

1        2
 ________
 \      |
 |\     |
 | \    |
 |  \   |
 |   \  |
 |    \ |
 |_____\|
4        3

Dreieck 1: Punkte 1, 2, 3
Dreieck 2: Punkte 3, 4, 1
Das habe ich gemerkt als ich eine un-triangulierte C4D-Figur geladen habe. Dein erster Code
hat nur 3 Punkte ausgelesen, also hatte die Figur Löcher, weil das 2.Dreieck fehlte.

Dann habe ich das zweite Dreieck hinzugefügt, falls es einen 4. Punkt gibt:

Code: Alles auswählen

Procedure ReadObject(...
...
        ; 1. dreieck
        AddElement(object()\face())
        Pointer = StringField(Row, 2, " ")
        object()\face()\a = Val(StringField(Pointer, 1, "/"))
        Pointer = StringField(Row, 3, " ")
        object()\face()\b = Val(StringField(Pointer, 1, "/"))
        Pointer = StringField(Row, 4, " ")
        object()\face()\c = Val(StringField(Pointer, 1, "/"))

        Pointer = Trim(StringField(Row, 5, " "))
        If Pointer ; wenn vorhanden, dann sind es 2 dreiecke

            AddElement(object()\face())
            Pointer = StringField(Row, 4, " ")
            object()\face()\a = Val(StringField(Pointer, 1, "/"))
            Pointer = StringField(Row, 5, " ")
            object()\face()\b = Val(StringField(Pointer, 1, "/"))
            Pointer = StringField(Row, 2, " ")
            object()\face()\c = Val(StringField(Pointer, 1, "/"))
        EndIf
Getestet mit vielen Objekten, und funktioniert wunderbar. Probier es mal selbst.
In C4D das Figur-Objekt oder ein Terrain erstellen... ohne es zu Polygonen zu wandeln
und ohne es zu triangulieren. Nur Objekt erstellen und dann als Wavefront OBJ exportieren.

Funktioniert hier bisher alles wunderbar, auch mit einem Olivenbaum, den C4D mit 485.992 Dreiecken exportiert.
Bild

Übrigens noch vielen Dank für Deinen Code! Geht ja nach den kleinen Änderungen wunderbar... ;)

PS: Laut OBJ Spezifikation können auch noch weitere Punkte kommen. 5 Punkte ergeben dann 3 Dreiecke,
6 Punkte 4 Dreiecke usw.
Ein Dreieck nimmt immer 2 Punkte vom vorhergehenden Dreieck mit, braucht also selbst nur einen neuen Punkt.
Meine OBJ von C4D nutzen aber bisher immer nur 1 oder 2 Dreiecke. Wenn Du eine OBJ mit mehr als 4 Punkten
hast, dann kannst Du ja noch hinzufügen das weitere Dreiecke ausgelesen werden.
cya,
...Danilo
"Ein Genie besteht zu 10% aus Inspiration und zu 90% aus Transpiration" - Max Planck
Benutzeravatar
Sunny
Beiträge: 290
Registriert: 19.02.2009 06:02

Re: kleiner "Wavefront (.obj) to OGRE (.mesh)" - Konverter

Beitrag von Sunny »

Hab mir deinen Code grade mal ein bischen angeschaut und dabei is mir aufgefallen, das noch etwas fehlt...
Soweit klappt das ja alles super allerdings muss du noch prüfen, ob es sich um ein konvexes Polygon handelt und wenn das nicht der fall ist, musst du es in mehrere konvexe Polygone unterteilen, die du dann triangulieren kannst.
Mehr zu dem Thema in diesem Thread.

Da versuche ich grade diesen Satz zu verstehen:
Der Vektor aus dem Kreuzprodukt der Vektoren P1->P2 × P2->P3 zeigt in genau die entgegen gesetzte Richtung wie der Vektor aus dem Kreuzprodukt P2->P3 × P3->P4.
und das dann auf das Programm umzusetzen ^^
Benutzeravatar
Danilo
-= Anfänger =-
Beiträge: 2284
Registriert: 29.08.2004 03:07

Re: kleiner "Wavefront (.obj) to OGRE (.mesh)" - Konverter

Beitrag von Danilo »

Sunny hat geschrieben:Hab mir deinen Code grade mal ein bischen angeschaut und dabei is mir aufgefallen, das noch etwas fehlt...
Soweit klappt das ja alles super allerdings muss du noch prüfen, ob es sich um ein konvexes Polygon handelt und wenn das nicht der fall ist, musst du es in mehrere konvexe Polygone unterteilen, die du dann triangulieren kannst.
Hier mal eine Routine zum triangulieren aus dem Buch "Computer Graphics for JAVA Programmers":

Code: Alles auswählen

If InitEngine3D() = 0
  End
EndIf

If InitSprite() = 0
  End
EndIf

If InitKeyboard() = 0
  End
EndIf


Structure vtx
  x.f
  y.f
  z.f
EndStructure

Structure vtx_fce
  x.f
  y.f
  z.f
  face.i
EndStructure

Structure triangle
  p1.vtx_fce
  p2.vtx_fce
  p3.vtx_fce
EndStructure

Structure fce
  a.i
  b.i
  c.i
EndStructure

Structure obj
  name.s
  level.i
  List vertex.vtx()
  List face.fce()
  List normals.vtx()
EndStructure

Global NewList object.obj()
Global NewList tosort.f()

Global triangles

;--------------------------------------------------------------------------------------
; Triangulation of Polygons
; 
; Book: Computer Graphics for JAVA Programmers
;
; by: Leen Ammeraal
;
; ISBN: 0-471-98142-7
;
; http://www.amazon.com/Computer-Graphics-Java-Programmers-Ammeraal/dp/0471981427/
; 
;--------------------------------------------------------------------------------------

Procedure.f Area(*A.vtx_fce, *B.vtx_fce, *C.vtx_fce)
  ProcedureReturn ( (*A\x - *C\x) * (*B\y - *C\y) - (*A\y - *C\y) * (*B\x - *C\x) )
EndProcedure

Procedure InsideTriangle(*A.vtx_fce, *B.vtx_fce, *C.vtx_fce, *P.vtx_fce)
    ; *A, *B, *C is assumed to be counter-clockwise
    If Area(*A,*B,*P) >= 0 And Area(*B,*C,*P) >= 0 And Area(*C,*A,*P) >= 0
        ProcedureReturn #True
    EndIf
EndProcedure

Procedure ccw(Array P.vtx_fce(1))
    Protected n = ArraySize( P() )
    Protected k = 0, i = 1
    While i < n
      If (P(i)\x <= P(k)\x And (P(i)\x < P(k)\x Or P(i)\y < P(k)\y))
        k = i
      EndIf
      i + 1
    Wend
    ; P(k) is a convex vertex.
    Protected _prev = k - 1, _next = k + 1
    If (_prev = -1) : _prev = n - 1 : EndIf
    If (_next =  n) : _next = 0     : EndIf
    If Area(@P(_prev), @P(k), @P(_next)) > 0.0
        ProcedureReturn #True
    EndIf
    ProcedureReturn #False
EndProcedure

Procedure Triangulate(Array P.vtx_fce(1), List tri.triangle())
  ; P contains all n polygon verices in CCW order.
  ; the resulting triangles will be stored in linked list tri().
  Protected n = ArraySize( P() )

  If n < 3
    ProcedureReturn #False
  EndIf

  ClearList( tri() )

  Protected i = 0, j = n - 1
  Protected iA, iB, iC

  Dim _next.i(n)
  
  While i < n
    _next(j) = i
    j = i
    i + 1
  Wend
  
  Protected k
  
  While k < n-2
    ; Find a suitable triangle, consisting of two edges
    ; and an internal diagonal:
    Protected.vtx_fce A, B, C
    Protected triaFound
    Protected count

    triaFound = #False
    count = 0

    While triaFound=#False And count < n
      iB = _next(iA)
      iC = _next(iB)
      A = P(iA) : B = P(iB) : C = P(iC)
      
      If Area(@A,@B,@C) >= 0.0
        ; Edges AB and BC; diagonal AC.
        ; Test to see of no other polygon vertex
        ; lies within triangle ABC:
        j = _next(iC)
        While j <> iA And InsideTriangle(@A, @B, @C, @P(j))=0
            j = _next(j)
        Wend
        If j = iA
            AddElement(tri())
            tri()\p1 = A
            tri()\p2 = B
            tri()\p3 = C
            _next(iA) = iC
            triaFound = #True
        EndIf
      EndIf
      iA = _next(iA)
      count + 1
    Wend
    If count = n
        ;Debug "Not a simple polygon"
        ;ProcedureReturn
    EndIf

    k + 1
  Wend

EndProcedure

;--------------------------------------------------------------------------------------


Procedure ReadObject(ObjFile.s, List object.obj())
  Protected.s Row, Type, Pointer
  Protected.i Counter
  Protected Dim vertex.vtx(0)
  Protected NewList tr.triangle()
  Protected NewList all_triangles.triangle()

  triangles = 0
  
  If OpenFile(0, ObjFile)
    While Eof(0) = 0
      Row = ReadString(0)
     
      Type = StringField(Row, 1, " ")
     
      If Type = "g"
        AddElement(object())
        object()\name = StringField(Row, CountString(Row, " ") + 1, " ")
        object()\level = CountString(Row, " ") - 1
        ;Debug object()\name
      ElseIf Type = "v"
        AddElement(object()\vertex())
        object()\vertex()\x = ValF(StringField(Row, 2, " "))
        object()\vertex()\y = ValF(StringField(Row, 3, " "))
        object()\vertex()\z = ValF(StringField(Row, 4, " "))
        ;Debug ArraySize( vertex() )
        vertex(ArraySize( vertex() )) = object()\vertex()
        ReDim vertex( ArraySize( vertex() ) + 1)
      ElseIf Type = "vn"
        ;AddElement(object()\normals())
        ;object()\normals()\x = ValF(StringField(Row, 2, " "))
        ;object()\normals()\y = ValF(StringField(Row, 3, " "))
        ;object()\normals()\z = ValF(StringField(Row, 4, " "))
       
      ElseIf Type = "f"

        Pointer = Trim(StringField(Row, 6, " "))
        If Pointer = ""                                 ; nur 1 oder 2 Dreieck(e)
       
            AddElement(object()\face())
            Pointer = StringField(Row, 2, " ")
            object()\face()\a = Val(StringField(Pointer, 1, "/")) ; 1
            Pointer = StringField(Row, 3, " ")
            object()\face()\b = Val(StringField(Pointer, 1, "/")) ; 2
            Pointer = StringField(Row, 4, " ")
            object()\face()\c = Val(StringField(Pointer, 1, "/")) ; 3
            triangles + 1

            Pointer = Trim(StringField(Row, 5, " "))
            If Pointer                                  ; 2. Dreieck, falls vorhanden
           
                AddElement(object()\face())
                Pointer = StringField(Row, 2, " ")
                object()\face()\a = Val(StringField(Pointer, 1, "/")) ; 1
                Pointer = StringField(Row, 4, " ")
                object()\face()\b = Val(StringField(Pointer, 1, "/")) ; 2
                Pointer = StringField(Row, 5, " ")
                object()\face()\c = Val(StringField(Pointer, 1, "/")) ; 3
                triangles + 1

            EndIf
        Else                                            ; Polygon
            line.s = Trim(Row)
            count = CountString(line," ")
            Dim face_vtx.vtx_fce(count)
            For i = 0 To count - 1
                Pointer          = StringField(line, i+2, " ")
                face_vtx(i)\face = Val(StringField(Pointer,1,"/"))
                face_vtx(i)\x    = vertex( face_vtx(i)\face -1)\x
                face_vtx(i)\y    = vertex( face_vtx(i)\face -1)\y
                face_vtx(i)\z    = vertex( face_vtx(i)\face -1)\z
            Next

            reversed = #False
            If Not ccw(face_vtx())                                ; Wenn nicht entgegen dem Uhrzeigersinn,
                Protected Dim face_vtx_reverse.vtx_fce(count)     ; dann umdrehen
                For i = 0 To count - 1                            ;
                    face_vtx_reverse(i) = face_vtx(count-i-1)     ;
                Next                                              ;
                For i = 0 To count - 1
                    face_vtx(i) = face_vtx_reverse(i)
                Next
                reversed = #True
            EndIf

            Triangulate(face_vtx(),tr())                          ; triangulieren

            ;Debug Str( ListSize(tr()) ) + " triangles generated"
            ForEach tr()
                AddElement( all_triangles() ) ; gefundene dreiecke speichern
                all_triangles() = tr()
                all_triangles()\p1\x = 0       ; \x missbrauchen wir hier um
                If reversed = #True            ; "reversed" zu uebergeben ;)
                    all_triangles()\p1\x = 100 ;
                EndIf
            Next

        EndIf

      EndIf
     
    Wend
   
    CloseFile(0)

    ;
    ; alle neuen dreiecke hinzufügen
    ;
    If ListSize( all_triangles() ) > 0
        AddElement(object())
        object()\name = "generated triangles"
        ForEach all_triangles()
            AddElement(object()\face())
            triangles + 1
            If all_triangles()\p1\x < 50  ; nicht umgedreht
                object()\face()\a = all_triangles()\p1\face
                object()\face()\b = all_triangles()\p2\face
                object()\face()\c = all_triangles()\p3\face
            Else
                object()\face()\c = all_triangles()\p1\face
                object()\face()\b = all_triangles()\p2\face
                object()\face()\a = all_triangles()\p3\face
            EndIf
        Next
     EndIf

  EndIf
EndProcedure

If OpenWindow(0, 0, 0, 800, 600, "2 Mesh", #PB_Window_SystemMenu|#PB_Window_ScreenCentered);|#PB_Window_Maximize)
 
  If OpenWindowedScreen(WindowID(0), 0, 24, WindowWidth(0), WindowHeight(0)-24, 0, 0, 0)
    ButtonGadget(0, 0, 0, WindowWidth(0)*0.5, 24, ".obj-Datei öffnen")
    ButtonGadget(1, WindowWidth(0)*0.5, 0, WindowWidth(0)*0.5, 24, ".obj zu .mesh konvertieren")
   
    CreateCamera(0, 0, 0, 100, 100)
   
    CreateLight(0, RGB(0, 0, 200), 1.5, 1.5, -2)
    CreateLight(1, RGB(255, 200, 80), -3, -1, -2)
   
    CreateTexture(0, 256, 256)

    StartDrawing(TextureOutput(0))
        Box(0, 0, 256, 256, RGB(200, 200, 200))
    StopDrawing()
   
    CreateMaterial(0, TextureID(0))
       
    Repeat
      event = WaitWindowEvent(20)
     
      If event = #PB_Event_Gadget
        If EventGadget() = 0
          file$ = OpenFileRequester("Datei öffnen...", "", "Wavefront (*.obj)|*.obj", 1)
         
          If file$
            ClearList(object())
            ReadObject(file$, object())
            Debug Str(triangles) + " triangles"
           
            ClearList(tosort())
           
            FirstElement(object())
           
            CreateMesh(0)
            Xall.f = 0
            Yall.f = 0
            Zall.f = 0
            All = 0
                      
           ForEach object() ; alle objekte zum mesh hinzufügen
              FirstElement(object()\normals())
              ForEach object()\vertex()
                AddMeshVertex(object()\vertex()\x, object()\vertex()\y, object()\vertex()\z)
                If ListSize(object()\normals())
                    MeshVertexNormal(object()\normals()\x,object()\normals()\y,object()\normals()\z)
                    NextElement(object()\normals())
                EndIf
                
                Xall.f + object()\vertex()\x
                Yall.f + object()\vertex()\y
                Zall.f + object()\vertex()\z
                All + 1
               
                AddElement(tosort())
                tosort() = object()\vertex()\x
                AddElement(tosort())
                tosort() = object()\vertex()\y
                AddElement(tosort())
                tosort() = object()\vertex()\z
              Next
             
              ForEach object()\face()
                AddMeshFace(object()\face()\a - 1, object()\face()\b - 1, object()\face()\c - 1)
              Next

              ;AddSubMesh()
            Next
            FinishMesh()
           
            SortList(tosort(), #PB_Sort_Ascending)
           
            SelectElement(tosort(), 0)
            smallest.f = tosort()
           
            SelectElement(tosort(), ListSize(tosort()) - 1)
            biggest.f = tosort()
           
            NormalizeMesh(0)
            
            ;BuildMeshShadowVolume(0)
           
            CreateEntity(0, MeshID(0), MaterialID(0))
           
            CameraLocate(0, 0, (smallest - biggest) * -1 * 2, (smallest - biggest) * 2)
            CameraLookAt(0, Xall / All, Yall / All, Zall / All)
           
            LightLocate(0, (smallest - biggest) * -1 * 1.5, (smallest - biggest) * -1 * 1.5, (smallest - biggest) * -1 * -2)
            LightLocate(1, (smallest - biggest) * -1 * -3, (smallest - biggest) * -1 * -1, (smallest - biggest) * -1 * -2)
           
            MeshCreate = 1
           
          EndIf
        ElseIf EventGadget() = 1
          savefile$ = SaveFileRequester("Datei speichern...", "Unbenannt.mesh", "Alle Dateien (*.*)|*.*", 0)
         
          If savefile$
            If Right(savefile$, 5) <> ".mesh"
              savefile$ + ".mesh"
            EndIf
           
            SaveMesh(0, savefile$)
           
          EndIf
        EndIf
      EndIf

      If ExamineKeyboard()
        x_rotation = 0
        y_rotation = 0
        z_rotation = 0
        If KeyboardPushed(#PB_Key_2) : y_rotation =  1 : EndIf
        If KeyboardPushed(#PB_Key_1) : y_rotation = -1 : EndIf

        If KeyboardPushed(#PB_Key_3) : z_rotation =  1 : EndIf
        If KeyboardPushed(#PB_Key_4) : z_rotation = -1 : EndIf

        If KeyboardPushed(#PB_Key_5) : x_rotation =  1 : EndIf
        If KeyboardPushed(#PB_Key_6) : x_rotation = -1 : EndIf

        If KeyboardPushed(#PB_Key_Add) : zoom.f - 0.01 : EndIf
        If KeyboardPushed(#PB_Key_Subtract) : zoom.f + 0.01 : EndIf

        If MeshCreate = 1
          RotateEntity(0, x_rotation, y_rotation, z_rotation, #PB_Relative)
          CameraLocate(0, 0, (smallest - biggest) * -1 * (2+zoom), (smallest - biggest) * (2+zoom))
          CameraLookAt(0, Xall / All, Yall / All, Zall / All)
        EndIf
      EndIf
     
      ClearScreen(RGB(0,0,0))
      RenderWorld()
      FlipBuffers()
    Until event = #PB_Event_CloseWindow
   
    End
   
  EndIf
 
EndIf
Zum testen: Stern.obj mit 2 Deckflächen aus Polygonen

Code: Alles auswählen

# WaveFront *.obj file (generated by CINEMA 4D)

g Extrude-NURBS
v 200 0 0
v 92.387953 38.268343 0
v 141.421356 141.421356 0
v 38.268343 92.387953 0
v 0 200 0
v -38.268343 92.387953 0
v -141.421356 141.421356 0
v -92.387953 38.268343 0
v -200 0 0
v -92.387953 -38.268343 0
v -141.421356 -141.421356 0
v -38.268343 -92.387953 0
v 0 -200 0
v 38.268343 -92.387953 0
v 141.421356 -141.421356 0
v 92.387953 -38.268343 0
v 200 0 -20
v 92.387953 38.268343 -20
v 141.421356 141.421356 -20
v 38.268343 92.387953 -20
v 0 200 -20
v -38.268343 92.387953 -20
v -141.421356 141.421356 -20
v -92.387953 38.268343 -20
v -200 0 -20
v -92.387953 -38.268343 -20
v -141.421356 -141.421356 -20
v -38.268343 -92.387953 -20
v 0 -200 -20
v 38.268343 -92.387953 -20
v 141.421356 -141.421356 -20
v 92.387953 -38.268343 -20

vt 1 1 0
vt 0 1 0
vt 0.0625 1 0
vt 0.125 1 0
vt 0.1875 1 0
vt 0.25 1 0
vt 0.3125 1 0
vt 0.375 1 0
vt 0.4375 1 0
vt 0.5 1 0
vt 0.5625 1 0
vt 0.625 1 0
vt 0.6875 1 0
vt 0.75 1 0
vt 0.8125 1 0
vt 0.875 1 0
vt 0.9375 1 0
vt 1 0 0
vt 0 0 0
vt 0.0625 0 0
vt 0.125 0 0
vt 0.1875 0 0
vt 0.25 0 0
vt 0.3125 0 0
vt 0.375 0 0
vt 0.4375 0 0
vt 0.5 0 0
vt 0.5625 0 0
vt 0.625 0 0
vt 0.6875 0 0
vt 0.75 0 0
vt 0.8125 0 0
vt 0.875 0 0
vt 0.9375 0 0

f 17/19 18/20 2/3 1/2 
f 18/20 19/21 3/4 2/3 
f 19/21 20/22 4/5 3/4 
f 20/22 21/23 5/6 4/5 
f 21/23 22/24 6/7 5/6 
f 22/24 23/25 7/8 6/7 
f 23/25 24/26 8/9 7/8 
f 24/26 25/27 9/10 8/9 
f 25/27 26/28 10/11 9/10 
f 26/28 27/29 11/12 10/11 
f 27/29 28/30 12/13 11/12 
f 28/30 29/31 13/14 12/13 
f 29/31 30/32 14/15 13/14 
f 30/32 31/33 15/16 14/15 
f 31/33 32/34 16/17 15/16 
f 32/34 17/18 1/1 16/17 

g Extrude-NURBS Deckfl_che_1
v 200 0 0
v 92.387953 38.268343 0
v 141.421356 141.421356 0
v 38.268343 92.387953 0
v 0 200 0
v -38.268343 92.387953 0
v -141.421356 141.421356 0
v -92.387953 38.268343 0
v -200 0 0
v -92.387953 -38.268343 0
v -141.421356 -141.421356 0
v -38.268343 -92.387953 0
v 0 -200 0
v 38.268343 -92.387953 0
v 141.421356 -141.421356 0
v 92.387953 -38.268343 0

f 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 33

g Extrude-NURBS Deckfl_che_2
v 200 0 -20
v 92.387953 38.268343 -20
v 141.421356 141.421356 -20
v 38.268343 92.387953 -20
v 0 200 -20
v -38.268343 92.387953 -20
v -141.421356 141.421356 -20
v -92.387953 38.268343 -20
v -200 0 -20
v -92.387953 -38.268343 -20
v -141.421356 -141.421356 -20
v -38.268343 -92.387953 -20
v 0 -200 -20
v 38.268343 -92.387953 -20
v 141.421356 -141.421356 -20
v 92.387953 -38.268343 -20

f 64 63 62 61 60 59 58 57 56 55 54 53 52 51 50 49

"f 64 63 62 61 60 59 58 57 56 55 54 53 52 51 50 49" ist eine Deckfläche als Polygon.

Bild
Zuletzt geändert von Danilo am 08.02.2013 11:53, insgesamt 1-mal geändert.
cya,
...Danilo
"Ein Genie besteht zu 10% aus Inspiration und zu 90% aus Transpiration" - Max Planck
Antworten