Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush Python API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Recent
    • Tags
    • Users
    • Login

    Image2Vertex map

    Scheduled Pinned Locked Moved PYTHON Development
    3 Posts 0 Posters 370 Views
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • H Offline
      Helper
      last edited by

      On 08/06/2015 at 16:39, xxxxxxxx wrote:

      Because i get so much help in the internet I want to share my plugin here.
      I used code fragments from here and there...
      It is a Bitmap to Vertexmap helper.
      The Script load a Bitmap from a texture tag. From color channel or alpha channel, or from a seperate file.
      Then it convert the bitmap to a vertexmap.

      Code is comment in german.

      Maybe someone helps:
      http://www.total3d.de/?page_id=550

      Code can surely be much better. (Like my english)
      But it works 😉

        
      import c4d   
      import os   
      from c4d import bitmaps, storage   
      from c4d import utils,gui   
      from c4d.gui import GeDialog   
      from c4d.modules import render   
      import math   
        
        
      chlist = []   
      scaleart = 0   
      button = 0   
      alpha = 0   
      orig = bitmaps.BaseBitmap()   
      objekttext = ""   
      showo = False   
      shows = False   
      bitmapfile = ""   
        
        
      # Dialog erstellen   
      class Dialog(GeDialog) :   
          global objekttext, showo, shows   
          def __init__(self) :   
              pass   
             
          # Layout definieren   
          def CreateLayout(self) :   
              # Objektname in Beschreibungstext einfügen   
              objekttext = "Selected object is: "+op.GetName()   
              self.SetTitle("RenameTexture")    # Titel definieren   
                
              self.GroupBegin(1000,c4d.BFH_CENTER|c4d.BFH_SCALE|c4d.BFH_FIT,0,8)   
              self.AddStaticText(1001,c4d.BFH_CENTER|c4d.BFH_SCALE|c4d.BFH_FIT,800,10,"This is a simple image to vertex map converter!",0);   
              self.AddStaticText(1007,c4d.BFH_CENTER|c4d.BFH_SCALE|c4d.BFH_FIT,800,10,"Use at your own risk - Please save Document before use!",0);   
              self.AddSeparatorH(0,c4d.BFV_FIT)   
              self.AddStaticText(1009,c4d.BFH_CENTER|c4d.BFH_SCALE|c4d.BFH_FIT,800,10,"1) Please select an object.",0);   
              self.AddStaticText(1011,c4d.BFH_CENTER|c4d.BFH_SCALE|c4d.BFH_FIT,800,10,"2) Plugin use the first uvw tag of the select polygon object.",0);   
              self.AddStaticText(1012,c4d.BFH_CENTER|c4d.BFH_SCALE|c4d.BFH_FIT,800,10,"3) Please select a textur tag at the object.",0);   
              self.AddStaticText(1012,c4d.BFH_CENTER|c4d.BFH_SCALE|c4d.BFH_FIT,800,10,"4) Choose a button for use color map, alpha map or load a bitmap file.",0);   
              self.AddSeparatorH(0,c4d.BFV_FIT)   
              self.GroupEnd()   
        
              self.GroupBegin(1010,c4d.BFH_LEFT,1,1)   
              self.AddStaticText(1008,c4d.BFH_LEFT,800,10,objekttext,0);   
              self.AddSeparatorH(0,c4d.BFV_FIT)   
              self.GroupEnd()   
              self.GroupBegin(1010,c4d.BFH_LEFT,2,4)   
              self.AddCheckbox(1003,c4d.BFH_LEFT,10,10,"Bitmap COLOR Channel\n To Vertex Map!")   
              self.AddStaticText(1012,c4d.BFH_LEFT,800,10,"Show Bitmap in the picture manager",0);   
              self.AddCheckbox(1004,c4d.BFH_LEFT,10,10,"Bitmap COLOR Channel\n To Vertex Map!")   
              self.AddStaticText(1014,c4d.BFH_LEFT,800,10,"Show scaled bitmap in picture manager",0);   
              self.GroupEnd()   
        
              self.GroupBegin(1000,c4d.BFH_LEFT|c4d.BFH_SCALE|c4d.BFH_FIT,1,3)   
              self.AddStaticText(1001,c4d.BFH_LEFT,800,10,"How much scale the bitmap before check color :",0);   
              self.AddEditSlider(1002,c4d.BFH_LEFT|c4d.BFH_SCALE|c4d.BFH_FIT, 10, 0)         
              self.GroupEnd()   
        
              self.AddSeparatorV(0,c4d.BFV_FIT)   
                
              self.GroupBegin(1020,c4d.BFV_CENTER,3,1)   
              self.AddButton(1022,c4d.BFH_CENTER,0,40,"Bitmap COLOR Channel\n To Vertex Map!")   
              self.AddButton(1023,c4d.BFH_CENTER,0,40,"Bitmap ALPHA Channel\n To Vertex Map!")   
              self.AddButton(1024,c4d.BFH_CENTER,0,40,"Load a Bitmap File\n To Vertex Map!")   
              self.GroupEnd()   
              return True   
        
             
          # Dialog Verarbeitung definieren   
          def Command(self,id,msg) :   
              global chlist, scaleart, orig, alpha, button, showo, shows, bitmapfile   
                
              button = id    # Variable button für Benutzung im Hauptprogramm speichern   
              if(id < 1002 or id > 1005) :# Wurde nur der Slider, oder die Checkboxen verändert? Dann mit True zurückgeben   
                  if(id == 1022 or id == 1023) : # Button links oder mitte (Color oder Alpha) wurde gedrückt   
                     # Button 1 (Alpha Channel) gedrückt   
                     if (id == 1023) : alpha = 1                 
                     #Scalierungs Slider speichern   
                     scaleart = int (self.GetReal(1002) * 0.1)       
                     showo= self.GetBool(1003)          
                     shows= self.GetBool(1004)          
                     self.Close()    # Fenster schließen und weiter im Hauptprogramm   
                     return True   
                  else:   
                     if (id == 1024) :# Der Button rechts für die Datei-Auswahl wurde gedrückt   
                          # Datei-Dialog öffnen   
                          bitmapfile = c4d.storage.LoadDialog (type=c4d.FILESELECTTYPE_IMAGES, title="Select a bitmap file for mapping!", flags=c4d.FILESELECT_LOAD)   
                          if not bitmapfile:   
                              gui.MessageDialog("no File loading!")   
                              return False   
                          scaleart = int (self.GetReal(1002) * 0.1)       
                          showo= self.GetBool(1003)          
                          shows= self.GetBool(1004)      
                          self.Close()   
                          return True   
                     else:   
                          self.Close()   
                          return False   
              return True   
        
        
        
        
      def main() :   
          # globale Variablen übernehmen   
          global scaleart, orig, alpha, button, showo, shows, bitmapfile   
        
          c4d.CallCommand (13957) # Pythonkonsole leeren   
             
          # Objekt ausgewählt? Sonst Fehlerausgabe und beenden   
          if not op:   
              gui.MessageDialog("Please select a object!")   
              return False                        # EXIT   
             
          # Prüfen ob das Objekt ein Polygonobjekt ist   
          if (op.GetType()== 5100) :           # 5100 ist der Type für das Polygonobjekt   
              print   
          else:   
              gui.MessageDialog("ERROR - Object is not a polygon object?")   
              return False   
                
          # Erstes UVTag auslesen   
          uvtag = op.GetTag(c4d.Tuvw)   
          # Kein UVW Tag? Sonst Fehlerausgabe und beenden   
          if not uvtag:   
              gui.MessageDialog("Please assign UVW to the object")   
              return   
             
          # Dialog aufrufen   
          dlg = Dialog();   
          dlg.Open(c4d.DLG_TYPE_MODAL)   
          if (button == 0) : return False    # Kein Button gedrückt (z.B. beim Fenster schließen über Fensterelement X)   
             
          # Bitmap Variablen vorbereiten   
          smallbitm = bitmaps.BaseBitmap()   
          bitmap = bitmaps.BaseBitmap()          
          bitmapPath = bitmapfile   
        
          if (button == 1024) :   
              abspath = bitmapfile   
              bitmapPath = bitmapfile   
          else:   
              # Selektiertes Tag auslesen   
              ttag = doc.GetActiveTag()   
              if (not ttag or not ttag.CheckType(c4d.Ttexture) ) : # Falls kein Material Tag vorhanden oder/und ausgewählt - abbrechen   
                  gui.MessageDialog("Please attach AND SELECT a Material to the object!")   
                  return False   
        
              # Material der Variable mat zuweisen   
              mat = ttag.GetMaterial()   
             
              # Kein Material übernehmen können? Dann Fehlerausgabe und beenden   
              if not mat:   
                  gui.MessageDialog("Please attach a Material with simple bitmap in color Channel to the object!")   
                  return False # EXIT   
        
              # Farbkanal auslesen falls Variable alpha = 0 (Button links gedrückt)   
              if (alpha == 0) :   
                  if (mat[c4d.MATERIAL_USE_COLOR] == True) : # ist der Color kanal des Materials aktiv?   
                     shader = mat[c4d.MATERIAL_COLOR_SHADER] # dann Shader auslesen   
                  else:   
                     gui.MessageDialog("Miss COLOR Channel!")   
                     return False # Exit   
              # Alphakanal auslesen falls Variable alpha = 1 (Button mitte gedrückt)   
              if (alpha == 1) :   
                  if (mat[c4d.MATERIAL_USE_ALPHA] == True) : # ist der Alpha kanal des Materials aktiv?   
                     shader = mat[c4d.MATERIAL_ALPHA_SHADER] # dann Shader auslesen   
                  else:   
                     gui.MessageDialog("Miss ALPHA Channel!")   
                     return False # Exit   
                
              # Farbkanal / Alphakanal konnte nicht gelesen werden?   
              if shader is None:   
                  if (alpha == 0) : gui.MessageDialog("Can't get the Shader!\nPlease check that the COLOR Channel is not empty!")   
                  if (alpha == 1) : gui.MessageDialog("Can't get the Shader!\nPlease check that the ALPHA Channel is not empty!")   
                  return False # EXIT   
                
                
              # Shader vor dem auslesen erst abrufbar machen!!!   
              bitmapPath = shader[c4d.BITMAPSHADER_FILENAME]   
              # Shadertyp und Filename bei Bitmapshader auslesen   
              name, path = shader.GetName(), shader[c4d.BITMAPSHADER_FILENAME]   
                     
              #if bitmapPath is´nt absolute   
              if not os.path.dirname(bitmapPath) :   
                  #the document has´nt been saved already ->picture in user library   
                  if not doc.GetDocumentPath() :   
                     abspath= c4d.storage.GeGetStartupWritePath()+"/"+"tex"+"/"+bitmapPath   
                  #the picture should be inside the asset´s texture folder       
                  else:   
                     abspath= doc.GetDocumentPath()+"/"+bitmapPath   
              else:   
                  abspath = bitmapPath   
        
             
             
          #   
          if op is not None:       
                
              # Bitmap mit vollem Pfad und Dateinamen Initialisieren:   
              result = bitmap.InitWith(abspath)   
        
              if (button <> 1024) : irs = render.InitRenderStruct()   
              else: irs = True   
              if (button <> 1024) : shader.InitRender(irs)   
              if irs:   
        
                     
                  # Bitmapdateien in Shadern können absolut oder relativ gespeichert werden.   
                  # Also muss geprüft werden ob der komplette Pfad ausgelesen wurde!   
        
        
                  # Falls Bitmap ordnungsgemäß verarbeitet wurde:   
                  if result:   
                       
                     # Die Variable bitmap wurde jetzt auf eine von 3 Arten befüllt:   
                     if bitmap is not None: # hats geklappt? Dann weiter:   
                          width, height = bitmap.GetSize()       # Pixel-Maße der Bilddatei auslesen   
                          bits = bitmap.GetBt()                  # Bittiefe auslesen   
                          pixelzahl = width*height               # Gesamtpixelanzahl ermitteln   
                          punktezahl = uvtag.GetDataCount()      # Punkteanzahl der UVW-Map ermitteln   
        
        
                          # Scalierung auf basis des Skalierungs-Sliders berechnen   
                          # Aufgrund der Differenz der Auflösung der Bilddatei   
                          # und der Auflösung des Polygonmesh (bzw. der UVW-Map)   
                          # berechnet sich die Scalierung der Bilddatei.   
                          # Die Bilddatei wird skaliert damit in einer hohen Bildauflösung   
                          # ein einzelner ausgerissener Pixel den Punkt in einem niedrig aufgelösen Mesh                        # nicht so stark beeinflusst beeinflusst. nur weil der UV-Punkt genau auf diesem Pixel liegt.   
                             
                          differenz = pixelzahl - punktezahl             # Differenz ermitteln   
                          if scaleart == 10: diffadd = float(differenz)/1000    # Neue Bildgröße = Anzahl der Punkte der UV-Map + Differenz geteilt durch 1000   
                          if scaleart == 9: diffadd = float(differenz)/500     # Neue Bildgröße = Anzahl der Punkte der UV-Map + Differenz geteilt durch 500   
                          if scaleart == 8: diffadd = float(differenz)/100      # Neue Bildgröße = Anzahl der Punkte der UV-Map + Differenz geteilt durch 100   
                          if scaleart == 7: diffadd = float(differenz)/50       # Neue Bildgröße = Anzahl der Punkte der UV-Map + Differenz geteilt durch 50                           
                          if scaleart == 6: diffadd = float(differenz)/20       # Neue Bildgröße = Anzahl der Punkte der UV-Map + Differenz geteilt durch 20   
                          if scaleart == 5: diffadd = float(differenz)/10       # Neue Bildgröße = Anzahl der Punkte der UV-Map + Differenz geteilt durh 10   
                          if scaleart == 4: diffadd = float(differenz)/5        # Neue Bildgröße = Anzahl der Punkte der UV-Map + Differenz geteilt durch 5   
                          if scaleart == 3: diffadd = float(differenz)/3        # Neue Bildgröße = Anzahl der Punkte der UV-Map + ein Drittel der Differenz   
                          if scaleart == 1: diffadd = float(differenz)/2        # Neue Bildgröße = Anzahl der Punkte der UV-Map + die Hälfte der Differenz   
                          if scaleart == 2: diffadd = float((differenz/3))*2    # Neue Bildgröße = Anzahl der Punkte der UV-Map + Zwei Drittel der Differenz   
                          if scaleart == 0: diffadd = differenz          # Neue Bildgröße = Anzahl der Punkte der UV-Map + komplette Defferenz - Also volle Auflösung!   
        
                          # Geladenes Bild bestimmt das Seitenverhältnis. Anhand der alten Menge an Pixel,   
                          # der zur Verfügung stehenden Punkte im UV-Mesh und dem Grad der Skalierung (Slider)   
                          # wird die neue Bildgröße berechnet. mit gleich bleibenden Seitenverhältnis.   
        
                          # Scalierungsfaktor berechnen: Punkte+Pixelreduzierung geteilt durch Pixelanzahl   
                          scalefaktor = float(punktezahl+diffadd) / (pixelzahl)   
                          # Anhand des Skalierungsfaktors der Fläche die Seitenlängen berechnen                           
                          scale_width = int(width * math.sqrt(scalefaktor))              
                          scale_height = int(height * math.sqrt(scalefaktor))   
                                                        
        
                          # Kleinere Version der Bitmap erstellen   
                          smallbitm.Init(scale_width, scale_height, bits)    # Kleinere Maße, gleiche Bittiefe, gleiches Seitenverhältnis   
        
                          # Copy&Scale; Original Bitmap zu Kleine Bitmap   
                          bitmap.ScaleBicubic(smallbitm, 0, 0, width-1, height-1, 0, 0, scale_width-1, scale_height-1)   
                             
                          if (showo == True) : bitmaps.ShowBitmap(bitmap)   
                          if (shows == True) : bitmaps.ShowBitmap(smallbitm)   
                          bitmap = smallbitm   
        
                     else:   
                          gui.MessageDialog("Bitmap-Error")   
                          return False   
        
                     if (button <> 1024) : shader.FreeRender()   
                       
                     # Abbrechen falls Bitmap nicht übergeben werden konnte   
                     if bitmap is None:   
                          gui.MessageDialog("Bitmap can't load")   
                          return False   
                                 
                  else:   
                     # Im Kanal steckt keine einfache Bitmap-Datei. Wahrscheinlich ein prozeduraler Shader, oder Ebenen ect.   
                     gui.MessageDialog("In channel ist not a simple bitmap")   
                     return False                           # EXIT   
                       
                  # Es wird eine Vertexmap erstellt die gleichviele Punkte enthält wie das Polygonobjekt   
                  vtag = c4d.VariableTag(c4d.Tvertexmap, op.GetPointCount())   
                  bitmap_w = bitmap.GetBw()              # Breite der Bitmap auslesen   
                  bitmap_h = bitmap.GetBh()              # Höhe der Bitmap auslesen   
                  pixel = (bitmap_w * bitmap_h)          # Pixelanzahl des Skalierten Bildes   
                  points = uvtag.GetDataCount()          # Punktanzahl des UVW-Tags   
                  if (op.GetPointCount() > 0) :           # Prüfen ob das Objekte Punkte zum auslesen hat   
                     vertexList = op.GetPointCount() * [0] # Die Variable für die Vertagsmap-Befüllung mit der Anzahl an Punkten des selektierten Objektesvorbereiten   
                  else:   
                     gui.MessageDialog("Object have no read able points! Is it a polygon object?")   
                     return False   
                     
                  # Schleife liest jedes UVW-tag Polygon aus. Ein Punkt eines UVW-Tags kann mehreren Polygonobjekt-Punkten zugewisen sein   
                  # Es wird der Reihe nach jedes UV-Polygon ausgelesen. Dann wird das zugehörige Polygon des Objektes ausgelesen und   
                  # dessen Punkte mit den Farbwerten der Bitmap gefüllt. Die Anzahl der Polygonpunkte müssen nämlich gleich sein mit der Anzahl der Vertexmap-Punkte   
                     
                  # Laut Wiki: Grauwert = 0,299 × Rotanteil + 0,587 × Grünanteil + 0,114 × Blauanteil   
                     
                  for i in xrange(uvtag.GetDataCount()) : # Schleife durchläuft Polygone des UVW-Tag   
                          uvwdict = uvtag.GetSlow(i)     # UVW-Polygon wird eingelesen   
                          objpolygon = op.GetPolygon(i) # Objekt-Polygon wird eingelesen   
                          point_a = op.GetPoint(objpolygon.a) # Objekt Polygon-PunktA ID wird ausgelesen   
                          point_b = op.GetPoint(objpolygon.b) # Objekt Polygon-PunktB ID wird ausgelesen   
                          point_c = op.GetPoint(objpolygon.c) # Objekt Polygon-PunktC ID wird ausgelesen   
                          point_d = op.GetPoint(objpolygon.d) # Objekt Polygon-PunktD ID wird ausgelesen   
                          u = uvwdict["a"].x                  # UVW-Koordinate des Punktes A wird ermittelt   
                          v = uvwdict["a"].y                  # UVW-Koordinate des Punktes A wird ermittelt   
                          w = uvwdict["a"].z   
                          ux = bitmap_w*u                     # UV-Koordinate liegt zwischen 0-1 und wird mit der Bildbreite multipliziert   
                          uy = bitmap_h*v                     # UV-Koordinate liegt zwischen 0-1 und wird mit der Bildhöhe multipliziert   
                          color = bitmap.GetPixel(int(ux), int(uy))   
                          # "ubjektiver" Helligkeitswert aus RGB ermitteln   
                          weight_a = 0.299 * (float(color[0])/256) + 0.587 * (float(color[1])/256) + 0.114 * (float(color[2])/256)   
        
                          u = uvwdict["b"].x                  # UVW-Koordinate des Punktes B wird ermittelt   
                          v = uvwdict["b"].y                  # UVW-Koordinate des Punktes B wird ermittelt   
                          w = uvwdict["b"].z   
                          ux = bitmap_w*u                     # UV-Koordinate liegt zwischen 0-1 und wird mit der Bildbreite multipliziert   
                          uy = bitmap_h*v                     # UV-Koordinate liegt zwischen 0-1 und wird mit der Bildhöhe multipliziert   
                          color = bitmap.GetPixel(int(ux), int(uy))   
                          # "ubjektiver" Helligkeitswert aus RGB ermitteln   
                          weight_b = 0.299 * (float(color[0])/256) + 0.587 * (float(color[1])/256) + 0.114 * (float(color[2])/256)   
        
                          u = uvwdict["c"].x                  # UVW-Koordinate des Punktes C wird ermittelt   
                          v = uvwdict["c"].y                  # UVW-Koordinate des Punktes C wird ermittelt   
                          w = uvwdict["c"].z   
                          ux = bitmap_w*u                     # UV-Koordinate liegt zwischen 0-1 und wird mit der Bildbreite multipliziert   
                          uy = bitmap_h*v                     # UV-Koordinate liegt zwischen 0-1 und wird mit der Bildhöhe multipliziert   
                          color = bitmap.GetPixel(int(ux), int(uy))   
                          # "ubjektiver" Helligkeitswert aus RGB ermitteln   
                          weight_c = 0.299 * (float(color[0])/256) + 0.587 * (float(color[1])/256) + 0.114 * (float(color[2])/256)   
        
                          u = uvwdict["d"].x                  # UVW-Koordinate des Punktes D wird ermittelt   
                          v = uvwdict["d"].y                  # UVW-Koordinate des Punktes D wird ermittelt   
                          w = uvwdict["d"].z   
                          ux = bitmap_w*u                     # UV-Koordinate liegt zwischen 0-1 und wird mit der Bildbreite multipliziert   
                          uy = bitmap_h*v                     # UV-Koordinate liegt zwischen 0-1 und wird mit der Bildhöhe multipliziert   
                          color = bitmap.GetPixel(int(ux), int(uy))   
                          # "ubjektiver" Helligkeitswert aus RGB ermitteln   
                          weight_d = 0.299 * (float(color[0])/256) + 0.587 * (float(color[1])/256) + 0.114 * (float(color[2])/256)   
        
                          # Helligkeitswerte der Vertexmap Wert-Variable zuweisen   
                          vertexList[objpolygon.a] = weight_a   
                          vertexList[objpolygon.b] = weight_b   
                          vertexList[objpolygon.c] = weight_c   
                          vertexList[objpolygon.d] = weight_d   
                                 
                  if vtag: # falls die Vertexmap auch wirlich erstellt wurde kann es weiter gehen:   
                     op.InsertTag(vtag) # Vertexmap wird an dem objekt erstellt   
        
                     vtag.SetAllHighlevelData(vertexList) # erstellte Vertexmap wird mit den ermittelten Werten gefüllt   
                                 
                  c4d.EventAdd() # Veränderung in der Szene mitteilen, damit C4D aktualisiert.   
              else:   
                  # Das Vorbereiten der Bitmapinitialisierung hat fehlgeschlagen:   
                  gui.MessageDialog("Can Not Init Shader")   
                  if irs == c4d.INITRENDERRESULT_OUTOFMEMORY: gui.MessageDialog("Out Of Memory")   
                  if irs == c4d.INITRENDERRESULT_ASSETMISSING: gui.MessageDialog("Texture Not Assigned")   
                  if irs == c4d.INITRENDERRESULT_UNKNOWNERROR: gui.MessageDialog("Unknown Error")   
                  if irs == c4d.INITRENDERRESULT_THREADEDLOCK: gui.MessageDialog("Threaded Lock")   
                
              gui.MessageDialog(" - Finish - ")   
        
      if __name__=='__main__':   
          main()   
        
      
      1 Reply Last reply Reply Quote 0
      • H Offline
        Helper
        last edited by

        On 09/06/2015 at 01:09, xxxxxxxx wrote:

        Hi,

        thanks for sharing!
        As I wrote a similar plugin, last year, for an in house production I want to ask if you feel like working together and publish an optimized version with a few more features?
        If so, feel free to send me a pm.

        Best wishes
        Martin

        1 Reply Last reply Reply Quote 0
        • H Offline
          Helper
          last edited by

          On 09/06/2015 at 08:11, xxxxxxxx wrote:

          Hello,

          thank you for sharing your work!

          Best wishes,
          Sebastian

          1 Reply Last reply Reply Quote 0
          • First post
            Last post