Techniken für das Rendern von Text mit WebGL

Daniel Velasquez - 6. Dez. 2019

Wie es die Regel in WebGL ist, ist alles, was einfach erscheinen mag, tatsächlich ziemlich kompliziert. Linien zeichnen, Shader debuggen, Text-Rendering… all das ist in WebGL verdammt schwer gut zu machen.

Ist das nicht seltsam? WebGL hat keine integrierte Funktion zum Rendern von Text. Dabei scheint Text eine der grundlegendsten Funktionalitäten zu sein. Wenn es jedoch darum geht, ihn tatsächlich zu rendern, wird es kompliziert. Wie berücksichtigt man die immense Menge an Glyphen für jede Sprache? Wie arbeitet man mit Schriftarten mit fester Breite oder proportionaler Breite? Was macht man, wenn Text von oben nach unten, von links nach rechts oder von rechts nach links gerendert werden muss? Mathematische Gleichungen, Diagramme, Noten?

Plötzlich ergibt es Sinn, warum Text-Rendering keinen Platz in einer Low-Level-Grafik-API wie WebGL hat. Text-Rendering ist ein komplexes Problem mit vielen Nuancen. Wenn wir Text rendern wollen, müssen wir kreativ werden. Glücklicherweise haben sich viele kluge Leute bereits eine breite Palette von Techniken für all unsere WebGL-Text-Bedürfnisse ausgedacht.

Wir werden einige dieser Techniken in diesem Artikel kennenlernen, einschließlich der Erzeugung der benötigten Assets und der Verwendung mit ThreeJS, einer JavaScript-3D-Bibliothek, die einen WebGL-Renderer enthält. Als Bonus wird jede Technik eine Demo mit Anwendungsfällen enthalten.

Inhaltsverzeichnis


Eine kurze Anmerkung zu Text außerhalb von WebGL

Obwohl sich dieser Artikel ausschließlich mit Text in WebGL beschäftigt, sollten Sie zuerst überlegen, ob Sie stattdessen HTML-Text oder Canvas-Overlays über Ihrem WebGL-Canvas verwenden können. Der Text kann nicht mit der 3D-Geometrie als Overlay verdeckt werden, aber Sie erhalten sofortige Stile und Barrierefreiheit. Das ist in vielen Fällen alles, was Sie brauchen.

Schriftart-Geometrien

Eine der gängigsten Methoden zum Rendern von Text ist der Aufbau von Glyphen mit einer Reihe von Dreiecken, ähnlich wie bei einem normalen Modell. Schließlich sind das Rendern von Punkten, Linien und Dreiecken eine Stärke von WebGL.

Beim Erstellen einer Zeichenkette wird jede Glyphe durch Lesen der Dreiecke aus einer Schriftdatei mit triangulierten Punkten erstellt. Von dort aus können Sie die Glyphe extrudieren, um sie 3D zu machen, oder die Glyphen per Matrixoperation skalieren.

Reguläre Schriftartdarstellung (links) und Schriftart-Geometrie (rechts)

Die Schriftart-Geometrie eignet sich am besten für eine kleine Textmenge. Das liegt daran, dass jede Glyphe viele Dreiecke enthält, was das Zeichnen problematisch macht.

Das Rendern dieses exakten Absatzes, den Sie gerade lesen, mit Schriftart-Geometrie erzeugt 185.084 Dreiecke und 555.252 Vertices. Das sind nur 259 Buchstaben. Schreiben Sie den ganzen Artikel mit einer Schriftart-Geometrie und Ihr Computerlüfter könnte sich genauso gut in ein Flugzeugtriebwerk verwandeln.

Obwohl die Anzahl der Dreiecke je nach Präzision der Triangulierung und der verwendeten Schriftart variiert, wird das Rendern vieler Texte wahrscheinlich immer ein Engpass bei der Arbeit mit Schriftart-Geometrie sein.

Wie man eine Schriftart-Geometrie aus einer Schriftdatei erstellt

Wenn es so einfach wäre, die gewünschte Schriftart auszuwählen und den Text zu rendern. Würde ich diesen Artikel nicht schreiben. Reguläre Schriftformate definieren Glyphen mit Bézier-Kurven. Auf der anderen Seite ist das Zeichnen davon in WebGL extrem teuer für die CPU und auch kompliziert zu machen. Wenn wir Text rendern wollen, müssen wir Dreiecke (Triangulierung) aus Bézier-Kurven erstellen.

Ich habe ein paar Triangulierungsmethoden gefunden, aber keine davon ist perfekt und sie funktionieren möglicherweise nicht mit jeder Schriftart. Aber sie werden Ihnen zumindest den Einstieg in die Triangulierung Ihrer eigenen Schriftarten erleichtern.

Methode 1: Automatisch und einfach

Wenn Sie ThreeJS verwenden, übergeben Sie Ihre Schriftart an FaceType.js, um die parametrischen Kurven aus Ihrer Schriftdatei zu lesen und in eine .json-Datei zu schreiben. Die Schriftart-Geometrie-Funktionen in ThreeJS kümmern sich um die Triangulierung der Punkte für Sie.

const geometry = new THREE.FontGeometry("Hello There", {font: font, size: 80})

Alternativ, wenn Sie ThreeJS nicht verwenden und keine Echtzeit-Triangulierung benötigen. Sie könnten sich den Schmerz eines manuellen Prozesses ersparen, indem Sie ThreeJS verwenden, um die Schriftart für Sie zu triangulieren. Dann können Sie die Vertices und Indizes aus der Geometrie extrahieren und sie in Ihre WebGL-Anwendung Ihrer Wahl laden.

Methode 2: Manuell und schmerzhaft

Die manuelle Option zur Triangulierung einer Schriftdatei ist extrem kompliziert und umständlich, zumindest anfangs. Es würde einen ganzen Artikel erfordern, um sie im Detail zu erklären. Dennoch werden wir kurz die Schritte einer grundlegenden Implementierung durchgehen, die ich von StackOverflow übernommen habe.

Siehe den Pen
Triangulating Fonts
von Daniel Velasquez (@Anemolo)
auf CodePen.

Die Implementierung lässt sich im Grunde wie folgt zusammenfassen:

  1. Fügen Sie OpenType.js und Earcut.js zu Ihrem Projekt hinzu.
  2. Holen Sie sich Bézier-Kurven aus Ihrer .tff-Schriftdatei mit OpenType.js.
  3. Wandeln Sie Bézier-Kurven in geschlossene Formen um und sortieren Sie sie nach absteigender Fläche.
  4. Bestimmen Sie die Indizes für die Löcher, indem Sie ermitteln, welche Formen sich innerhalb anderer Formen befinden.
  5. Senden Sie alle Punkte an Earcut mit den Lochindizes als zweitem Parameter.
  6. Verwenden Sie das Ergebnis von Earcut als Indizes für Ihre Geometrie.
  7. Atmen Sie auf.

Ja, das ist viel. Und diese Implementierung funktioniert möglicherweise nicht für alle Schriftarten. Sie wird Ihnen trotzdem den Einstieg erleichtern.

Verwendung von Text-Geometrien in ThreeJS

Glücklicherweise unterstützt ThreeJS Text-Geometrien sofort. Geben Sie ihm eine .json-Datei mit den Bézier-Kurven Ihrer bevorzugten Schriftart, und ThreeJS kümmert sich um die Triangulierung der Vertices zur Laufzeit.

var loader = new THREE.FontLoader();
var font;
var text = "Hello World"
var loader = new THREE.FontLoader();
loader.load('fonts/helvetiker_regular.typeface.json', function (helvetiker) {
  font = helvetiker;
  var geometry = new THREE.TextGeometry(text, {
    font: font,
    size: 80,
    height: 5,
  });
}

Vorteile

  • Sie lässt sich leicht extrudieren, um 3D-Strings zu erstellen.
  • Die Skalierung wird durch Matrixoperationen erleichtert.
  • Sie bietet eine hohe Qualität, abhängig von der Anzahl der verwendeten Dreiecke.

Nachteile

  • Dies skaliert nicht gut bei großen Textmengen aufgrund der hohen Dreiecksanzahl. Da jeder Buchstabe durch viele Dreiecke definiert ist, führt bereits das Rendern von etwas so Kurzem wie "Hallo Welt" zu 7.396 Dreiecken und 22.188 Vertices.
  • Dies eignet sich nicht für gängige Texteffekte.
  • Anti-Aliasing hängt von Ihrem Post-Processing-Aliasing oder den Standardeinstellungen Ihres Browsers ab.
  • Zu große Skalierungen können die Dreiecke sichtbar machen.

Demo: Fading Letters

In der folgenden Demo habe ich die Einfachheit der Erzeugung von 3D-Text mit Schriftart-Geometrien ausgenutzt. Innerhalb eines Vertex-Shaders wird die Extrusion im Laufe der Zeit erhöht und verringert. Kombinieren Sie das mit Nebel und Standardmaterial, und Sie erhalten diese geisterhaften Buchstaben, die aus der Leere kommen und gehen.

Beachten Sie, dass bei einer geringen Anzahl von Buchstaben die Anzahl der Dreiecke bereits im Zehntausenderbereich liegt!

Text- (und Canvas-) Texturen

Das Erstellen von Texttexturen ist wahrscheinlich die einfachste und älteste Methode, um Text in WebGL zu zeichnen. Öffnen Sie Photoshop oder einen anderen Rastergrafikeditor, zeichnen Sie ein Bild mit etwas Text darauf und rendern Sie diese Texturen auf ein Quad, und fertig!

Alternativ können Sie das Canvas verwenden, um die Texturen bei Bedarf zur Laufzeit zu erstellen. Sie können das Canvas auch als Textur auf ein Quad rendern.

Abgesehen davon, dass es die am wenigsten komplizierte Technik von allen ist. Texttexturen und Canvas-Texturen haben den Vorteil, dass sie nur ein Quad pro Textur oder ein gegebenes Textstück benötigen. Wenn Sie wirklich wollten, könnten Sie die gesamte britische Enzyklopädie auf eine einzige Textur schreiben. Auf diese Weise müssen Sie nur ein einziges Quad, sechs Vertices und zwei Flächen rendern. Natürlich würden Sie es in einem Maßstab tun, aber die Idee bleibt bestehen: Sie können mehrere Glyphen in dasselbe Quad stapeln. Sowohl Text- als auch Canvas-Texturen haben Probleme mit der Skalierung, insbesondere bei viel Text.

Bei Texttexturen muss der Benutzer alle Texturen herunterladen, aus denen sich der Text zusammensetzt, und sie dann im Speicher behalten. Bei Canvas-Texturen muss der Benutzer nichts herunterladen – aber jetzt muss der Computer des Benutzers die gesamte Rasterung zur Laufzeit durchführen, und Sie müssen verfolgen, wo sich jedes Wort im Canvas befindet. Außerdem kann das Aktualisieren eines großen Canvas sehr langsam sein.

Wie man eine Texttextur erstellt und verwendet

Texttexturen haben nichts Besonderes. Öffnen Sie Ihren bevorzugten Rastergrafikeditor, zeichnen Sie etwas Text auf das Canvas und exportieren Sie ihn als Bild. Dann können Sie ihn als Textur laden und auf eine Ebene abbilden.

// Load texture
let texture = ;
const geometry = new THREE.PlaneBufferGeometry();
const material new THREE.MeshBasicMaterial({map: texture});
this.scene.add(new Mesh(geometry,material));

Wenn Ihre WebGL-App viel Text hat, ist das Herunterladen eines riesigen Sprite-Sheets mit Text möglicherweise nicht ideal, insbesondere für Benutzer mit langsamen Verbindungen. Um die Downloadzeit zu vermeiden, können Sie Dinge bei Bedarf mit einem Offscreen-Canvas rasterisieren und dann dieses Canvas als Textur abtasten.

Tauschen wir Downloadzeit gegen Leistung, da das Rasterisieren vieler Texte mehr als einen Moment dauert.

function createTextCanvas(string, parameters = {}){
    
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");
    
    // Prepare the font to be able to measure
    let fontSize = parameters.fontSize || 56;
    ctx.font = `${fontSize}px monospace`;
    
    const textMetrics = ctx.measureText(text);
    
    let width = textMetrics.width;
    let height = fontSize;
    
    // Resize canvas to match text size 
    canvas.width = width;
    canvas.height = height;
    canvas.style.width = width + "px";
    canvas.style.height = height + "px";
    
    // Re-apply font since canvas is resized.
    ctx.font = `${fontSize}px monospace`;
    ctx.textAlign = parameters.align || "center" ;
    ctx.textBaseline = parameters.baseline || "middle";
    
    // Make the canvas transparent for simplicity
    ctx.fillStyle = "transparent";
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    
    ctx.fillStyle = parameters.color || "white";
    ctx.fillText(text, width / 2, height / 2);
    
    return canvas;
}

let texture = new THREE.Texture(createTextCanvas("This is text"));

Jetzt können Sie die Textur auf einer Ebene verwenden, wie im vorherigen Snippet. Oder Sie können stattdessen einen Sprite erstellen.

Als Alternative können Sie effizientere Bibliotheken zur Erstellung von Texturen oder Sprites verwenden, wie z. B. three-text2d oder three-spritetext. Und wenn Sie mehrzeiligen Text wünschen, sollten Sie sich dieses erstaunliche Tutorial ansehen.

Vorteile

  • Dies bietet eine hohe 1:1-Qualität bei statischem Text.
  • Es gibt eine geringe Vertex-/Flächenanzahl. Jede Zeichenkette kann so wenig wie sechs Vertices und zwei Flächen verwenden.
  • Es ist einfach, Texturen auf ein Quad zu implementieren.
  • Es ist ziemlich trivial, Effekte wie Rahmen und Glanzlichter mit Canvas oder einem Grafikeditor hinzuzufügen.
  • Canvas erleichtert die Erstellung von mehrzeiligem Text.

Nachteile

  • Sieht verschwommen aus, wenn sie nach der Rasterung skaliert, gedreht oder transformiert wird.
  • Auf nicht-Retina-Bildschirmen sieht Text körnig aus.
  • Sie müssen alle verwendeten Zeichenketten rasterisieren. Viele Zeichenketten bedeuten viele Daten zum Herunterladen.
  • Die On-Demand-Rasterung mit Canvas kann langsam sein, wenn Sie das Canvas ständig aktualisieren.

Demo: Canvas-Textur

Canvas-Texturen funktionieren gut mit einer begrenzten Menge an Text, der sich nicht oft ändert. Also habe ich eine einfache Textwand mit den Quads gebaut, die dieselbe Textur wiederverwenden.

Bitmap-Schriftarten

Sowohl Schriftart-Geometrien als auch Texttexturen haben die gleichen Probleme bei der Handhabung von viel Text. Eine Million Vertices pro Textstück ist super ineffizient, und das Erstellen einer Textur pro Textstück skaliert nicht wirklich.

Bitmap-Schriftarten lösen dieses Problem, indem sie alle eindeutigen Glyphen in eine einzige Textur rasterisieren, die als Texture Atlas bezeichnet wird. Das bedeutet, dass Sie jede gegebene Zeichenkette zur Laufzeit zusammensetzen können, indem Sie für jede Glyphe ein Quad erstellen und den Bereich des Texture Atlas abtasten.

Das bedeutet, dass Benutzer nur eine einzige Textur für den gesamten Text herunterladen und verwenden müssen. Es bedeutet auch, dass Sie nur so wenig wie ein Quad pro Glyphe rendern müssen.

Eine visuelle Darstellung der Bitmap-Schriftart-Abtastung

Das Rendern dieses gesamten Artikels würde ungefähr 117.272 Vertices und 58.636 Dreiecke ergeben. Das ist 3,1-mal effizienter im Vergleich zu einer Schriftart-Geometrie, die nur einen einzigen Absatz rendert. Das ist eine enorme Verbesserung!

Da Bitmap-Schriftarten die Glyphe in eine Textur rasterisieren, leiden sie unter demselben Problem wie normale Bilder. Zoomen Sie hinein oder skalieren Sie, und Sie beginnen, ein pixeliges und verschwommenes Durcheinander zu sehen. Wenn Sie Text in einer anderen Größe wünschen, sollten Sie eine sekundäre Bitmap mit den Glyphen in dieser spezifischen Größe senden. Oder Sie könnten ein Signed Distance Field (SDF) verwenden, das wir im nächsten Abschnitt behandeln werden.

Wie man Bitmap-Schriftarten erstellt

Es gibt viele Werkzeuge zur Erzeugung von Bitmaps. Hier sind einige der relevanteren Optionen:

  • Angelcode’s bmfont – Dies ist von den Entwicklern des Bitmap-Formats.
  • Hiero – Dies ist ein Java-Open-Source-Tool. Es ist sehr ähnlich wie Anglecode’s bmfont, ermöglicht aber das Hinzufügen von Texteffekten.
  • Glyphs Designer – Dies ist eine kostenpflichtige MacOS-App.
  • ShoeBox – Dies ist ein Tool für den Umgang mit Sprites, einschließlich Bitmap-Schriftarten.

Wir verwenden Anglecode’s bmfont für unser Beispiel, da es meiner Meinung nach am einfachsten zu bedienen ist. Am Ende dieses Abschnitts finden Sie andere Werkzeuge, falls Sie der Meinung sind, dass es die gewünschte Funktionalität nicht bietet.

Wenn Sie die App öffnen, sehen Sie einen Bildschirm voller Buchstaben, die Sie zum Verwenden auswählen können. Das Schöne daran ist, dass Sie nur die benötigten Glyphen auswählen können, anstatt griechische Symbole zu senden.

Die Seitenleiste der App ermöglicht es Ihnen, Gruppen von Glyphen auszuwählen und zu markieren.

Die BmFont-Anwendung

Bereit zum Exportieren? Gehen Sie zu OptionenBitmap speichern als. Fertig!

Aber wir greifen ein wenig zu weit vor. Bevor wir exportieren, gibt es ein paar wichtige Einstellungen, die Sie überprüfen sollten.

Export- und Schriftart-Optionseinstellungen
  • Schriftarteinstellungen: Hier können Sie die gewünschte Schriftart und Größe auswählen. Der wichtigste Punkt ist "Zeichenhöhe anpassen". Standardmäßig verwendet die Option "Größe" des Programms Pixel anstelle von Punkten. Sie werden einen erheblichen Unterschied zwischen der Schriftgröße Ihres Grafikeditors und der generierten Schriftgröße feststellen. Wählen Sie die Option "Zeichenhöhe anpassen", wenn Ihre Glyphen Sinn ergeben sollen.
  • Exporteinstellungen: Stellen Sie für den Export sicher, dass die Texturgröße eine Zweierpotenz ist (z. B. 16×16, 32×32, 64×64 usw.). Dann können Sie bei Bedarf "Linear Mipmap linear" Filterung nutzen.

Am Ende der Einstellungen sehen Sie den Abschnitt "Dateiformat". Die Wahl einer der Optionen ist in Ordnung, solange Sie die Datei lesen und die Glyphen erstellen können.

Wenn Sie die kleinste Dateigröße suchen. Ich habe einen ultra-nicht-wissenschaftlichen Test durchgeführt, bei dem ich eine Bitmap aller Klein- und Großbuchstaben des lateinischen Alphabets erstellt und jede Option verglichen habe. Für Schriftart-Deskriptoren ist das effizienteste Format **Binär**.

Schriftart-Deskriptor-Format Dateigröße
Binär 3 KB
Rohtext 11 KB
XML 12 KB
Texturformat Dateigröße
PNG 7 KB
Targa 64 KB
DirectDraw Surface 65 KB

PNG hat die kleinste Dateigröße für Texttexturen.

Natürlich ist es ein wenig komplizierter als nur die Dateigrößen. Um eine bessere Vorstellung davon zu bekommen, welche Option Sie verwenden sollen, sollten Sie sich mit der Parsing-Zeit und der Laufzeit-Performance befassen. Wenn Sie die Vor- und Nachteile jeder Formatierung wissen möchten, schauen Sie sich diese Diskussion an.

Wie man Bitmap-Schriftarten verwendet

Das Erstellen von Bitmap-Schriftart-Geometrie ist etwas aufwendiger als die reine Verwendung einer Textur, da wir die Zeichenkette selbst konstruieren müssen. Jede Glyphe hat ihre eigene Höhe und Breite und entnimmt einen anderen Bereich der Textur. Wir müssen für jede Glyphe in unserer Zeichenkette ein Quad erstellen, damit wir ihr die richtigen UVs geben können, um ihre Glyphe abzutasten.

Sie können three-bmfont-text in ThreeJS verwenden, um Zeichenketten mit Bitmaps, SDFs und MSDFs zu erstellen. Es kümmert sich um mehrzeiligen Text und das Stapeln aller Glyphen in einer einzigen Geometrie. Beachten Sie, dass es in einem Projekt von npm installiert werden muss.

var createGeometry = require('three-bmfont-text')
var loadFont = require('load-bmfont')

loadFont('fonts/Arial.fnt', function(err, font) {
  // create a geometry of packed bitmap glyphs, 
  // word wrapped to 300px and right-aligned
  var geometry = createGeometry({
    font: font,
    text: "My Text"
  })
    
  var textureLoader = new THREE.TextureLoader();
  textureLoader.load('fonts/Arial.png', function (texture) {
    // we can use a simple ThreeJS material
    var material = new THREE.MeshBasicMaterial({
      map: texture,
      transparent: true,
      color: 0xaaffff
    })

    // now do something with our mesh!
    var mesh = new THREE.Mesh(geometry, material)
  })
})

Verwenden Sie je nachdem, ob Ihr Text als ganz schwarz oder ganz weiß gezeichnet wird, die Invertierungsoption.

Vorteile

  • Es ist schnell und einfach zu rendern.
  • Es ist ein 1:1-Verhältnis und auflösungsunabhängig.
  • Es kann jede Zeichenkette rendern, vorausgesetzt, die Glyphen sind vorhanden.
  • Es ist gut für viel Text, der sich oft ändern muss.
  • Es funktioniert extrem gut mit einer begrenzten Anzahl von Glyphen.
  • Es beinhaltet Unterstützung für Dinge wie Kerning, Zeilenhöhe und Wortumbruch zur Laufzeit.

Nachteile

  • Es akzeptiert nur eine begrenzte Auswahl an Zeichen und Stilen.
  • Es erfordert eine Vorrasterung von Glyphen und eine zusätzliche Bin-Packung für optimale Nutzung.
  • Es ist unscharf und verpixelt bei großen Skalen und kann auch gedreht oder transformiert werden.
  • Es gibt nur ein Quad pro gerenderter Glyphe.

Interaktive Demo: The Shining Credits

Gerasterte Bitmap-Schriftarten eignen sich hervorragend für Film-Credits, da wir nur wenige Größen und Stile benötigen. Der Nachteil ist, dass der Text bei responsiven Designs nicht gut ist, da er bei größeren Größen unscharf und verpixelt aussieht.

Für den Maus-Effekt führe ich Berechnungen durch, indem ich die Mausposition auf die Größe der Ansicht abbilde, dann die Entfernung von der Maus zur Textposition berechne. Ich drehe den Text auch, wenn er bestimmte Punkte auf der Z-Achse und Y-Achse erreicht.

Signed Distance Fields (SDF)

Ähnlich wie Bitmap-Schriftarten sind auch Signed Distance Field (SDF)-Schriftarten ein Texture Atlas. Eindeutige Glyphen werden zu einem einzigen "Texture Atlas" gebündelt, mit dem jede beliebige Zeichenkette zur Laufzeit erstellt werden kann.

Aber anstatt die *gerasterte* Glyphe auf der Textur zu speichern, wie es Bitmap-Schriftarten tun, wird stattdessen das SDF der Glyphe generiert und gespeichert, was eine hochauflösende Form aus einem niedrigauflösenden Bild ermöglicht.

Ähnlich wie polygonale Meshes (Schriftart-Geometrien) *stellen SDFs Formen dar*. Jedes Pixel in einem SDF speichert die Entfernung zur *nächstgelegenen Oberfläche*. Das Vorzeichen gibt an, ob sich das Pixel innerhalb oder außerhalb der Form befindet. Wenn das Vorzeichen negativ ist, ist das Pixel innen; wenn es positiv ist, ist das Pixel außen. Dieses Video veranschaulicht das Konzept anschaulich.

SDFs werden auch häufig für Raytracing und volumetrisches Rendering verwendet.

Da ein SDF die Entfernung in jedem Pixel speichert, sieht das rohe Ergebnis wie eine verschwommene Version der ursprünglichen Form aus. Um die harte Form auszugeben, müssen Sie sie bei 0,5 per Alpha-Test prüfen, was der Rand der Glyphe ist. Schauen Sie sich an, wie das SDF des Buchstabens "A" mit seinem regulären Rasterbild verglichen wird.

Rastertext neben einem rohen und einem alpha-getesteten SDF

Wie ich bereits erwähnt habe, ist der große Vorteil von SDFs die Möglichkeit, hochauflösende Formen aus niedrigauflösenden SDFs zu rendern. Das bedeutet, Sie können ein 16-Punkt-Schriftart-SDF erstellen und den Text auf 100 Punkte oder mehr skalieren, ohne viel Schärfe zu verlieren.

SDFs eignen sich gut zum Skalieren, da Sie die Entfernung mit bilinearer Interpolation fast perfekt rekonstruieren können, was eine schicke Art ist, Werte zwischen zwei Punkten zu erhalten. In diesem Fall ergibt die bilineare Interpolation zwischen zwei Pixeln einer regulären Bitmap-Schriftart die Zwischenfarbe, was zu einem linearen Flimmern führt.

Bei einem SDF liefert die bilineare Interpolation zwischen zwei Pixeln die Zwischenentfernung zur nächsten Kante. Da diese beiden Pixelentfernungen anfangs ähnlich sind, verliert der resultierende Wert wenig Informationen über die Glyphe. Das bedeutet auch, je größer das SDF, desto genauer und desto weniger Informationen gehen verloren.

Dieser Prozess hat jedoch einen Haken. Wenn die Änderungsrate zwischen den Pixeln nicht linear ist – wie bei *scharfen Ecken* – liefert die bilineare Interpolation einen ungenauen Wert, was zu abgebrochenen oder abgerundeten Ecken führt, wenn ein SDF viel stärker als seine ursprüngliche Größe skaliert wird.

SDF abgerundete Ecken

Abgesehen vom Erhöhen der Texturgröße ist die einzige wirkliche Lösung die Verwendung von Multi-Channel-SDFs, die wir im nächsten Abschnitt behandeln werden.

Wenn Sie tiefer in die Wissenschaft hinter SDFs eintauchen möchten, lesen Sie die Masterarbeit von Chris Green (PDF) zu diesem Thema.

Vorteile

  • Sie behalten ihre Schärfe, auch wenn sie gedreht, skaliert oder transformiert werden.
  • Sie sind ideal für dynamischen Text.
  • Sie bieten ein gutes Verhältnis von Qualität zu Größe. Eine einzelne Textur kann verwendet werden, um winzige und riesige Schriftgrößen zu rendern, ohne viel Qualität zu verlieren.
  • Sie haben eine geringe Vertexanzahl von nur vier Vertices pro Glyphe.
  • Anti-Aliasing ist kostengünstig, ebenso wie das Erstellen von Rändern, Schatten und allen Arten von Effekten mit Alpha-Testing.
  • Sie sind kleiner als MSDFs (die wir gleich sehen werden).

Nachteile

  • Sie können zu abgerundeten oder abgebrochenen Ecken führen, wenn die Textur über ihre Auflösung hinaus abgetastet wird. (Auch hier sehen wir, wie MSDFs das verhindern können.)
  • Sie sind bei winzigen Schriftgrößen ineffektiv.
  • Sie können nur mit monochromen Glyphen verwendet werden.

Multi-Channel Signed Distance Fields (MSDF)

Multi-Channel Signed Distance Field (MSDF)-Schriftarten sind ein Zungenbrecher und eine ziemlich neue Variante von SDFs, die durch die Verwendung aller drei Farbkanäle nahezu perfekte scharfe Ecken erzeugen kann. Sie sehen anfangs ziemlich umwerfend aus, aber **lassen Sie sich davon nicht abschrecken**, denn sie sind einfacher zu verwenden, als sie erscheinen.

Eine Multi-Channel Signed Distance Field-Datei kann anfangs etwas gruselig aussehen.

Die Verwendung aller drei Farbkanäle führt zu einem schwereren Bild, aber das ist es, was MSDFs ein weitaus besseres Qualitäts-zu-Raum-Verhältnis als reguläre SDFs verleiht. Das folgende Bild zeigt den Unterschied zwischen einem SDF und einem MSDF für eine Schriftart, die auf 50 Pixel skaliert wurde.

Die SDF-Schriftart führt zu abgerundeten Ecken, selbst bei 1-facher Vergrößerung, während die MSDF-Schriftart scharfe Kanten beibehält, selbst bei 5-facher Vergrößerung.

Wie ein reguläres SDF speichert ein MSDF die Entfernung zur nächsten Kante, ändert aber die Farbkanäle, wann immer eine scharfe Ecke gefunden wird. Wir erhalten die Form, indem wir dort zeichnen, wo zwei oder mehr Farbkanäle übereinstimmen. Obwohl etwas mehr Technik dahinter steckt. Schauen Sie sich das README für diesen MSDF-Generator für eine gründlichere Erklärung an.

Vorteile

  • Sie unterstützen ein höheres Qualitäts-zu-Raum-Verhältnis als SDFs und sind oft die bessere Wahl.
  • Sie behalten scharfe Ecken, wenn sie skaliert werden.

Nachteile

  • Sie können kleine Artefakte enthalten, diese können aber durch Erhöhen der Texturgröße der Glyphe vermieden werden.
  • Sie erfordern zur Laufzeit eine Filterung des Medians der drei Werte, was etwas aufwendig ist.
  • Sie sind nur mit monochromen Glyphen kompatibel.

Erstellen von MSDF-Schriftarten

Der schnellste Weg, eine MSDF-Schriftart zu erstellen, ist die Verwendung des Tools msdf-bmfont-web. Es verfügt über die meisten relevanten Anpassungsoptionen und erledigt die Aufgabe in Sekundenschnelle, alles im Browser. Alternativ gibt es eine Reihe von Google Fonts, die bereits in MSDF konvertiert wurden von den Leuten bei A-Frame.

Wenn Sie auch SDFs oder Ihre Schriftart generieren möchten, erfordert dies aufgrund einiger problematischer Glyphen einige spezielle Anpassungen. Das CLI-Tool msdf-bmfont-xml bietet Ihnen eine breite Palette von Optionen, ohne die Dinge übermäßig kompliziert zu machen. Lassen Sie uns einen Blick darauf werfen, wie Sie es verwenden würden.

Zuerst müssen Sie es global von npm installieren

npm install msdf-bmfont-xml -g

Geben Sie ihm als Nächstes eine .ttf-Schriftartdatei mit Ihren Optionen

msdf-bmfont ./Open-Sans-Black.ttf --output-type json --font-size 76 --texture-size 512,512

Diese Optionen sind es wert, etwas genauer betrachtet zu werden. Obwohl msdf-bmfont-xml viele Optionen zur Feinabstimmung Ihrer Schriftart bietet, gibt es wirklich nur wenige Optionen, die Sie wahrscheinlich benötigen, um eine MSDF korrekt zu generieren.

  • -t <typ> oder <code>--field-type <msdf oder sdf>: msdf-bmfont-xml generiert standardmäßig MSDF-Glyphen-Atlanten. Wenn Sie stattdessen eine SDF generieren möchten, müssen Sie dies mit -t sdf angeben.
  • -f <xml oder json> oder --output-type <xml oder json>: msdf-bmfont-xml generiert eine XML-Schriftartdatei, die Sie zur Laufzeit in JSON parsen müssten. Sie können diesen Parsingschritt vermeiden, indem Sie JSON direkt exportieren.
  • -s, --font-size <fontSize>: Einige Artefakte und Unvollkommenheiten können auftreten, wenn die Schriftgröße sehr klein ist. Eine Erhöhung der Schriftgröße beseitigt diese meistens. Dieses Beispiel zeigt eine kleine Unvollkommenheit im Buchstaben „M.“
  • -m <b,h> oder --texture-size <b,h>: Wenn nicht alle Ihre Zeichen in dieselbe Textur passen, wird eine zweite Textur erstellt, um sie aufzunehmen. Sofern Sie nicht versuchen, einen Multi-Page-Glyphen-Atlas zu nutzen, empfehle ich, die Texturgröße zu erhöhen, damit sie über alle Zeichen auf einer Textur passt, um zusätzlichen Aufwand zu vermeiden.

Es gibt andere Tools, die beim Erstellen von MSDF und SDF-Schriftarten helfen

  • msdf-bmfont-web: Ein Web-Tool zum schnellen und einfachen Erstellen von MSDFs (aber nicht SDFs)
  • msdf-bmfont: Ein Node-Tool, das Cairo und node-canvas verwendet
  • msdfgen: Das ursprüngliche Befehlszeilentool, auf dem alle anderen MSDF-Tools basieren
  • Hiero: Ein Tool zum Generieren von Bitmap- und SDF-Schriftarten

Verwendung von SDF- und MSDF-Schriftarten

Da SDF- und MSDF-Schriftarten ebenfalls Glyphen-Atlanten sind, können wir three-bmfont-text wie bei Bitmap-Schriftarten verwenden. Der einzige Unterschied besteht darin, dass wir die Glyphen mithilfe eines Fragment-Shaders aus den Distanzfeldern abrufen müssen.

So funktioniert das für SDF-Schriftarten. Da unser Distanzfeld einen Wert größer als 0,5 außerhalb unseres Glyphen und kleiner als 0,5 innerhalb unseres Glyphen hat, müssen wir in einem Fragment-Shader für jedes Pixel einen Alpha-Test durchführen, um sicherzustellen, dass wir nur Pixel mit einem Abstand von weniger als 0,5 rendern, und nur die Innenseite der Glyphen rendern.

const fragmentShader = `

  uniform vec3 color;
  uniform sampler2D map;
  varying vec2 vUv;
  
  void main(){
    vec4 texColor = texture2D(map, vUv);
    // Only render the inside of the glyph.
    float alpha = step(0.5, texColor.a);

    gl_FragColor = vec4(color, alpha);
    if (gl_FragColor.a < 0.0001) discard;
  }
`;

const vertexShader = `
  varying vec2 vUv;   
  void main {
    gl_Position = projectionMatrix * modelViewMatrix * position;
    vUv = uv;
  }
`;

let material = new THREE.ShaderMaterial({
  fragmentShader, vertexShader,
  uniforms: {
    map: new THREE.Uniform(glyphs),
    color: new THREE.Uniform(new THREE.Color(0xff0000))
  }
})

Ähnlich können wir die Schriftart aus three-bmfont-text importieren, das standardmäßig Antialiasing bietet. Dann können wir es direkt auf einem RawShaderMaterial verwenden

let SDFShader = require('three-bmfont-text/shaders/sdf');
let material = new THREE.RawShaderMaterial(MSDFShader({
  map: texture,
  transparent: true,
  color: 0x000000
}));

MSDF-Schriftarten sind etwas anders. Sie stellen scharfe Ecken durch die Schnittpunkte zweier Farbkanäle wieder her. Zwei oder mehr Farbkanäle müssen dem zustimmen. Bevor wir einen Alpha-Test durchführen, müssen wir den Median der drei Farbkanäle ermitteln, um zu sehen, wo sie übereinstimmen.

const fragmentShader = `

  uniform vec3 color;
  uniform sampler2D map;
  varying vec2 vUv;

  float median(float r, float g, float b) {
    return max(min(r, g), min(max(r, g), b));
  }
  
  void main(){
    vec4 texColor = texture2D(map, vUv);
    // Only render the inside of the glyph.
    float sigDist = median(texColor.r, texColor.g, texColor.b) - 0.5;
    float alpha = step(0.5, sigDist);
    gl_FragColor = vec4(color, alpha);
    if (gl_FragColor.a < 0.0001) discard;
  }
`;
const vertexShader = `
  varying vec2 vUv;   
  void main {
    gl_Position = projectionMatrix * modelViewMatrix * position;
    vUv = uv;
  }
`;

let material = new THREE.ShaderMaterial({
  fragmentShader, vertexShader,
  uniforms: {
    map: new THREE.Uniform(glyphs),
    color: new THREE.Uniform(new THREE.Color(0xff0000))
  }
})

Auch hier können wir aus three-bmfont-text importieren, indem wir dessen MSDFShader verwenden, der ebenfalls standardmäßig Antialiasing bietet. Dann können wir es direkt auf einem RawShaderMaterial verwenden.

let MSDFShader = require('three-bmfont-text/shaders/msdf');
let material = new THREE.RawShaderMaterial(MSDFShader({
  map: texture,
  transparent: true,
  color: 0x000000
}));

Demo: Star Wars Intro

Das Star Wars Drawl Intro ist ein gutes Beispiel, bei dem MSDF- und SDF-Schriftarten gut funktionieren, da der Effekt erfordert, dass der Text in verschiedenen Größen erscheint. Wir können ein einzelnes MSDF verwenden und der Text sieht immer scharf aus! Obwohl three-bm-font leider noch keinen Blocksatz unterstützt. Linksbündige Ausrichtung würde eine ausgewogenere Darstellung ergeben.

Für den Lichtschwert-Effekt strahle ich eine unsichtbare Ebene in der Größe der Ebene ab, zeichne darauf auf eine Leinwand, die die gleiche Größe hat, und sampliere diese Leinwand, indem ich die Szenenposition auf die Texturkoordinaten abbilde.

Bonus-Tipp: Generieren von 3D-Text mit Höhenkarten

Abgesehen von Schriftgeometrien generieren alle bisher behandelten Techniken Zeichenfolgen oder Glyphen auf einem einzigen Quad. Wenn Sie 3D-Geometrien aus einer flachen Textur erstellen möchten, ist die Verwendung einer Höhenkarte Ihre beste Wahl.

Eine Höhenkarte ist eine Technik, bei der die Geometriehöhe mithilfe einer Textur angehoben wird. Dies wird normalerweise verwendet, um Berge in Spielen zu generieren, erweist sich aber auch beim Rendern von Text als nützlich.

Der einzige Nachteil ist, dass Sie viele Flächen benötigen, damit der Text glatt aussieht.

Weitere Lektüre

Unterschiedliche Situationen erfordern unterschiedliche Techniken. Nichts, was wir hier gesehen haben, ist eine Wunderwaffe, und sie alle haben ihre Vor- und Nachteile.

Es gibt viele Tools und Bibliotheken, die Ihnen helfen, das Beste aus WebGL-Text herauszuholen, von denen die meisten tatsächlich außerhalb von WebGL stammen. Wenn Sie weiter lernen möchten, empfehle ich Ihnen dringend, über WebGL hinauszugehen und sich einige dieser Links anzusehen.