Anatomie eines Buchstabens


Wie ich schon einmal erwähnt hatte, habe ich mir vorgenommen, bei der Scala-3D-Engine Sgine mitzuhelfen. Während sich meine bisherigen Beiträge hauptsächlich auf ein paar Grundkörper wie Quader oder Ellipsoide beschränkten, gedachte ich vor den Kegeln und Zylindern mal etwas interessanteres einzuschieben: 3D-Schrift. Bis es dahin kommt, ist noch ein ganzes Stückchen Arbeit, aber heute habe ich mit dem Basteln angefangen. Im Wesentlichen gibt es drei Schritte, um eine 3D-Schrift anzeigen zu können:

  • Man muss sich irgendwo die Rohdaten für die Außenkanten der Schrift besorgen
  • Die einzelnen Flächen zwischen den Kanten müssen in Dreiecke zerlegt werden
  • Die Schrift muss noch in die Tiefe verlängert werden und Texturkoordinaten verpasst bekommen

Die Hauptarbeit ist nach meiner Erfahrung der zweite Schritt. Eine gegebene nicht-konvexe Fläche in Dreiecke zu zerlegen (zu triangulieren) ist nicht trivial, wobei 3D-Grafik noch eine zusätzliche Anforderung stellt: Generell sollten Dreiecke nicht zu spitzwinklig werden, das mögen die Render-Algorithmen wegen drohender Rundungsfehler nämlich überhaupt nicht.

Aber bis dahin ist noch ein langer Weg, heute ging es mir erst einmal nur um die Form der Schrift. Die gute Nachricht ist, dass man sich die Schrift-Daten von Java besorgen kann. Nicht so schön ist dagegen das „wie“ – die API dafür ist nämlich gelinde gesagt gewöhnungsbedürftig. Hier ist der Code:

import java.awt.Font
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.Polygon
import java.awt.geom.FlatteningPathIterator
import java.awt.geom.PathIterator._
import java.awt.image.BufferedImage
import javax.swing.JComponent
import javax.swing.JOptionPane

object FontDemo {

  def extract(font: Font, text: String): List[Polygon] = {
    val g2 = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB).getGraphics.asInstanceOf[Graphics2D]
    val fontRenderContext = g2.getFontRenderContext
    val glyphVector = font.createGlyphVector(fontRenderContext, text)
    val shape = glyphVector.getOutline
    val path = shape.getPathIterator(null, 0).asInstanceOf[FlatteningPathIterator]
    val coords = Array(0f, 0f)
    var coordList = List[(Float, Float)]()
    var polygons = List[Polygon]()
    while (! path.isDone) {
      path.currentSegment(coords) match {
        case SEG_MOVETO => coordList =  (coords(0), coords(1)) :: Nil
        case SEG_LINETO => coordList =  (coords(0), coords(1)) :: coordList
        case SEG_CLOSE =>
          polygons = new Polygon(coordList.map(_._1.toInt).toArray,
                coordList.map(_._2.toInt).toArray, coordList.size) :: polygons
      }
      path.next
    }
    return polygons
  }
 
  ... // Test-Code
} 

Als erstes fällt auf, dass ich Handstände machen muss, um an ein Graphics2D-Objekt zu kommen. Normalerweise liefert mir das meine JComponent frei Haus, aber hier haben wir ja keine, also leiere ich sie einem BufferedImage (das ich ansonsten nicht benutze) aus dem Kreuz. Vom Graphics2D-Objekt brauche ich ebenfalls nur ein einziges Feature, nämlich den FontRenderContext. Damit kann ich wiederum meine Font überreden, mir einen GlyphVector zurückzuliefern. Der GlyphVector enthält die komplette geometrische Beschreibung der Font, man kann sich z.B. einzelne Glyphen (meistens einzelne Buchstaben, aber auch Ligaturen, Zeichen für Akzente u.s.w.) zurückliefern lassen. Aber das brauchen wir an dieser Stelle eigentlich nicht, denn wir wollen ja den kompletten Text darstellen. Die Außenkante ist als Shape verfügbar, dieser nützt allerdings an sich recht wenig, denn bei Schriftarten werden beknntlich Bezier-Kurven oder Splines eingesetzt, während eine 3D-Engine mit geraden Linien gefüttert werden muss. Aber auch für solche Bedürfnisse ist gesorgt: Man kann sich einen FlatteningPathIterator zurückgeben lassen, der genau das tut: Statt irgendwelcher Kurven gibt er uns interpolierte Linienstücke zurück. Die Bedienung dieses Dingens ist nicht schwierig, aber schwerfällig: Man fragt Punkt für Punkt ab, und ob es der erste (SEG_MOVETO), ein mittlerer (SEG_LINETO) oder der letzte (SEG_CLOSE) Punkt – der dann wieder mit dem ersten verbunden werden muss – eines Polygons ist. Ich fülle die Daten zu Demonstrationszwecken in „normale“ Polygone (was bei der fertigen 3D-Schrift nicht besonders praktisch wäre) und gebe diese zurück.

Ein kleiner Test sollte natürlich nicht fehlen:

  ...
  // Test-Code
  def main(args: Array[String]) {
    val font = new Font("Arial Unicode MS", Font.BOLD, 40)
    val polygons = extract(font, "eSCALAtion!")
    JOptionPane.showMessageDialog(null, new JComponent{
        setSize(300, 100)
        setPreferredSize(getSize)
        setMinimumSize(getSize)
        override def paintComponent(g: Graphics) {
          g.translate(30, 50)
          polygons.foreach(g.drawPolygon(_))
        }
      })
  }
 ...

Und das ist das Ergebnis:

Demo zum Ermitteln der Umrisse einer Schrift

Das Resultat sieht noch recht bescheiden aus, aber bevor jemand mit Tomaten wirft, möchte ich zu bedenken geben, dass die Pixel notgedrungen auf Int gerundet worden sind (während bei Java2D ausgefeilte Glättungsstrategien am werkeln sind), was bei der fertigen 3D-Schrift natürlich nicht der Fall ist.

So, das ist genug schwarze Kunst für heute. Für Verbesserungsvorschläge bin ich wie immer dankbar…

Advertisements

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s