Dieser Artikel erschien erstmals im PC Magazin 10/2000. Die Wieder­veröffentlichung erfolgt mit freundlicher Genehmigung der WEKA Media Publishing GmbH.

Texturen generieren

Mit Marmor oder Holz

Mit wenigen Zeilen Programmcode gestalten Sie die schönsten Oberflächen mit Texturen aller Art. Ihrer Fantasie sind keine Grenzen gesetzt, um die Aufmerksamkeit des Betrachters zu erringen.

Carsten Dachsbacher

Generieren Sie mit wenig Aufwand schrill bunte oder realistisch anmutende Texturen. Diese verwenden Sie für eigene Grafiken, Ihre Webpages oder 3D-Modelle. Diese Texturen sind dann garantiert frei von Rechten Dritter. Das ist bei Texturen von Modelling-Programmen oder CD-ROMs nicht immer der Fall.

Die Texturen in Ihren Programmen speichern Sie über die Parameter, mit denen Sie sie angelegt haben. Damit sparen Sie viel Platz im Gegensatz zur Datenmenge, die eine fertige Textur als Bild belegt. Textur-Parameter speichern Sie mit etwa 100 Byte oder weniger.

Wir zeigen Ihnen Schritt für Schritt, wie Sie Ihren eigenen Textur-Generator bauen. Dieser arbeitet mit Layers. Das ist ein Speicher­bereich, der eine temporäre Textur oder andere für die Textur­generierung notwendige Daten enthält. Ein Layer hat die Ausmaße der Textur, die Sie generieren wollen. Er besteht aus drei Kanälen, je einen für die Grundfarben Rot, Grün und Blau. Ein Textur-Generator, wie Sie ihn programmieren, arbeitet mit vier Arbeits­schritten:
• Er generiert einfache temporäre Basistexturen,
• verzerrt Texturen,
• Farb­operationen
• und Filter­funktionen.

Notwendig ist nur der erste Schritt. Die drei anderen sorgen dafür, dass die Texturen interessant wirken. Sie generieren einfache Texturen auf einigen Layers. Dann verzerren Sie den ersten Layer mit den Daten des zweiten und verändern mit dem Resultat die Textur des dritten. Einfach generierte Texturen können Sie aneinander legen, ohne einen erkennbarer Rand zu lassen.

Zunächst definieren Sie die Daten­strukturen für Ihre Layer. Diese bestehen aus einer Struktur für die Farbkanäle eines Pixels und einer Liste aus Bildern der gewünschten Größe. Der Wertebereich unserer Texturen bewegt sich zwischen 0 bis 255, wie der verwendete unsigned char-Wert definiert ist:


// Definitionen für die Layer
typedef struct
{
	unsigned char r, g, b;
} COLOR;

COLOR layer[MAXLAYER][SIZE * SIZE];
		

Basistexturen

DIE SINUSFUNKTIONEN färbt die Textur.
DIE SINUSFUNKTIONEN färbt die Textur.

Um einfache Texturen zu erzeugen, setzen Sie Pixel auf den Layer. Am besten bestimmen Sie die Farbwerte durch zwei überlagerte Sinus­funktionen über der Textur:


// Sinusfunktion
for (y = 0; y < SIZE; y++)
	for (x = 0; x < SIZE; x++)
	{
		wert = 127 + 63.5 * sin(x * faktor) +
			63.5 * sin(y * faktor);
		layer[0][y * SIZE + x].r = wert;
	}
		

Das Resultat überzeugt nicht. Sie können Layers dieser Art aber verwenden, um andere Layers zu verzerren. Verwenden Sie die zweite einfache Basistextur, um die Helligkeit anderer Layer zu ändern. Betrachten Sie jeden Pixel, und weisen Sie ihm eine Farbe entsprechend seinem Abstand zum Mittelpunkt zu. Es gilt dann der Satz: Je kleiner der Abstand, desto heller die Farbe.

Eine etwas komplexere Methode stellen Sub-Plasmas dar. Zuerst setzen Sie einige Pixel mit zufälliger Helligkeit an bestimmte Positionen im Layer. Beispielsweise wählen Sie einen Abstand von acht Pixeln zwischen zwei Zufallswerten. Mit diesen Zufallswerten interpolieren Sie die Farbwerte aller anderen Pixel.

Dabei setzen Sie im zwei­dimensionalen Raum entsprechend alle Pixel an jeder durch acht teilbaren x- und y-Koordinate. Bei der Interpolation ist entscheidend, welches Verfahren Sie verwenden. Eine lineare Interpolation führt zu unschönen Ergebnissen. Das Bild markiert die gesetzten Zufallswerte durch rote Punkte.

DER VERGLEICH ZEIGT die Unterschiede zwischen Linear- und Kosinus-Interpolation.
DER VERGLEICH ZEIGT die Unterschiede zwischen Linear- und Kosinus-Interpolation.

Der Rest der Kurve ist gleichmäßig glatt und führt zu einem hervorragenden Sub-Plasma wie im vorigen Bild. Wer sich in die mathematische Welt der Splines wagt, verwendet für die Interpolation die Catmull-Rom-Splines. Mit deutlich weniger Rechenaufwand kommen Sie aus, wenn Sie die Kosinus-Interpolation verwenden.

Verwenden Sie das Verfahren von Ken Perlin, um Texturen zu synthetisieren. Die Homepage von Ken Perlin, auf der Sie auch seinen Artikel über prozedurale Texturen lesen, finden Sie unter http://mrl.nyu.edu/perlin.

Eine Perlin-Noise-Funktion liefert zu einem Parameter (in diesem Fall eine ganze Zahl) eine Zufallszahl zurück. Wenn Sie zweimal denselben Parameter übergeben, muss die Funktion auch zweimal dasselbe Resultat erzeugen. Anderenfalls erhalten Sie trotz gleicher Startparameter nie zweimal dieselbe Textur.

Perlin-Noise-Funktionen sind Erweiterungen der Sub-Plasmas: Mehrere Sub-Plasmas mit unter­schiedlicher Amplitude und Frequenz werden addiert. Die Amplitude bezeichnet den höchsten vorkommenden Zufallswert eines Sub-Plasmas, und mit der Frequenz bestimmen Sie den Abstand der Zufallswerte. Sie können mehrere Noise-Funktionen unter­schiedlicher Amplitude und Frequenz zu einer Perlin-Noise- Funktion summieren.

EINDIMENSIONALE NOISE-FUNKTIONEN addieren Sie zu einer Perlin-Noise-Funktion.
EINDIMENSIONALE NOISE-FUNKTIONEN addieren Sie zu einer Perlin-Noise-Funktion.

Analog können Sie auch im Zwei­dimensionalen vorgehen. Verschiedene Sub-Plasmas ergeben zusammen eine Perlin-Noise-Textur. Das Beispiel interpoliert zwischen den Zufallswerten der Sub-Plasmas nur linear, was wegen der Überlagerung der Sub-Plasmas im Endbild nicht mehr auffällt.

Die Amplitude und die Frequenz, die Sie für die einzelnen Noise Funktionen verwenden, können Sie durch die Persistence festlegen. Sie legen nur noch eine Amplitude und eine Frequenz für die erste Funktion fest. Für die jeweils nächste Funktion, verdoppeln Sie die Frequenz und multiplizieren die Amplitude mit der Persistence. Der Wert der Persistence sollte zwischen 0 und 1 liegen. Größere Werte bedeuten höhere Frequenzen, also mehr Rauschen. Verdoppelte (überlagerte) Frequenzen nennt man Oktaven, da bei Klängen eine Verdoppelung der Frequenzen einem Sprung von einer Oktave entspricht. Wie viele Oktaven Sie wählen, ist Ihre Entscheidung. Berück­sichtigen Sie nur, dass die Amplitude irgendwann so klein wird, dass die Funktion nicht mehr ins Gewicht fällt.

EINE NOISE-FUNKTION, die durch wenige Stützpunkte festgelegt ist
EINE NOISE-FUNKTION, die durch wenige Stützpunkte festgelegt ist

Wie erzeugen Sie Noise-Funktionen? Die herkömmlichen Zufallszahlen­generatoren, die Ihnen C anbietet, liefern bei jedem Aufruf eine neue Zahl. Da das Ergebnis aber reproduzierbar sein muss (weil Sie eine Noise-Funktion eventuell mehrmals an derselben Stelle berechnen müssen), können Sie diese nicht verwenden.

Sie können eine Funktion wählen, die relativ zufällig Werte liefert – meistens sehr große Primzahlen. Folgende Funktion berechnet eine Zufallszahl zu x zwischen -1 und 1:


x = (x << 13) ^ x;
return (1.0 - ((x * (x * x * 15731 + 789221) +
	1376312589) & 7fffffff) / 1073741824.0);
		

Eine andere Methode legt beim Start des Programms eine Tabelle mit Zufallszahlen mit dem Generator an. Es genügen zum Beispiel 4096 verschiedene Zahlen. Als Funktion dient dann

return randomTable[x & 4095];

Verzerrte Texturen

Aus Basistexturen können Sie interessante Texturen machen. Sie verzerren das Bild, indem Sie jeden Pixel des Bildes betrachten und ihm den Wert eines anderen Pixels zuweisen. Dieser Pixel liegt zum Beispiel drei Pixel tiefer und vier Pixel rechts.

Schwieriger ist es, einen Wert mit einer Verschiebung von 2.7 Pixel tiefer und 4.2 Pixel rechts auszulesen. Wenn Sie die Werte auf 3 und 4 aufrunden, erhalten Sie eine sehr körnige Textur mit ungewünschten Aliasing-Effekten.

Sie lösen die Aufgabe mit der bilineare Interpolation. Dazu nehmen Sie den Pixel, den Sie auslesen wollen. Sie ordnen ihm x und y-Koordinaten zu, denen Sie die gerundeten Werten von 2 und 4 zuweisen. Dann betrachten Sie die vier umliegenden Punkte:


(x, y),
(x + 1, y),
(x, y + 1)
(x + 1, y + 1)
		

Die Farbwerte dieser Pixel gewichten Sie abhängig vom Nachkomma-Anteil der Verschiebung. Für den ersten bis zum vierten Fall ergeben sich folgende Punktwerte:


0.7 * 0.2 = 0.14
0.3 * 0.2 = 0.06
0.7 * 0.8 = 0.56
0.3 * 0.8 = 0.24
		

Die vier Gewichte ergeben in der Summe den Wert 1. Mit diesen Gewichten multiplizieren Sie die Rot-, Grün- und Blau-Farbanteile der umliegenden Pixel und addieren diese. Damit erhalten Sie einen gefilterten Farbwert ohne hässliche Verzerrungen.

Texturen lassen sich nach verschiedenen Methoden verzerren:
• Bei der ersten verwenden Sie eine Funktion, die Ihnen abhängig von der Position Ihres Pixels einen Verschiebungs­vektor liefert.
• Die zweite Variante verwendet den Inhalt eines oder zweier Layers, um die Verschiebung eines Pixels zu bestimmen (Map-Distortion, siehe nächsten Abschnitt).

DIE TWIRL-FUNKTION, wie Sie sie aus Bildbearbeitungsprogrammen kennen.
DIE TWIRL-FUNKTION, wie Sie sie aus Bildbearbeitungs­programmen kennen.

• Sie können eine Textur auch verzerren, indem Sie die Verschiebung an einer Pixelposition durch zwei Sinus­funktionen berechnen:


x_move = sin(x * 0.03) * 4.0;
y_move = sin(x * 0.04) * 4.0;
		

Damit setzen Sie an der Pixelposition (x, y) den Wert, den Sie bei (x+x_move, y+y_move) auslesen.
• Eine andere Verzerrung (in Adobe Photoshop gebräuchlich) nutzt den Twirl-Effekt. Dieser dreht das Bild. Den Drehwinkel eines Pixels bestimmen Sie über seinen Abstand zum Mittelpunkt der Textur.

Map-Distortion

Bei der Map-Distortion berechnen Sie den Verschiebungs­vektor eines Pixels durch die Helligkeits­werte der entsprechenden Pixel in den anderen Layern. Die Helligkeits­werte multiplizieren Sie am besten mit einem Wert zwischen 0 und 1. Sie können diese Werte auch als Parameter einer Sinusfunktion auffassen und so eine Marmortextur erreichen, die zum Beispiel auf das 3D-Modell einer Vase passt.

Der Pseudocode für eine Map-Distortion sieht wie folgt aus. Hierbei verzerren Sie den roten Farbkanal von Layer 3 mit den Layern 1 und 2 und schreiben das Ergebnis in Layer 4:


for (y = 0; y < SIZE; y++)
	for (x = 0; x < SIZE; x++)
	{
		x_move = layer[0].r * 0.1;
		y_move = sin(layer[1].r * 0.1);
		layer[4 ][x + y * SIZE] =
			interpolatePixel(3, x + x_move, y + y_move);
	}
		

Farboperationen

Mit den Farb­operationen können Sie Ihren Texturen den letzen Schliff verpassen. Dabei verändern Sie die Helligkeit, den Kontrast oder den Farbton.

Am einfachsten ändern Sie die Farben in Ihrer Textur, indem Sie die Farbkanäle invertieren. Dabei erhalten Sie interessante Farb­kombinationen. Sie müssen lediglich für jeden Pixel die Rot-, Grün- und Blau-Werte wie folgt ändern:


layer[0][x + y * SIZE].r =
	255 - layer[0][x + y * SIZE].r;
		

Wenn Sie Finetuning an Ihren Texturen vornehmen wollen, sollten Sie das Farbmodell wechseln. Für Farb­korrekturen eignet sich das HSV-Farbmodell. Das RGB-Modell können Sie sich als einen Würfel vorstellen, dessen Kanten die Achsen der drei Farbwerte darstellen.

Beim HSV-Modell wird der Farbraum durch einen Kegel aufgespannt. Dabei geben Sie die Farben mit drei Werten an:
• Mit dem Hue-Wert bestimmen Sie den Farbton. Dieser Wert ist der Drehwinkel um die Achse des Kegels.
• Der zweite Wert, S, steht für die Sättigung (Saturation). Im Kegel interpretieren Sie ihn als Abstand zur Achse. Kleine Werte ergeben blassere Farben bis hin zu Graustufen. Mit großen Werten erzielen Sie leuchtende Farben.
• Der verbleibende dritte Parameter, V, steht für Value (Helligkeit).

Wenn Sie den Hue-Wert ändern, modifizieren Sie bei einer Farbe im HSV-Modell nur den Farbton. Helligkeit und Sättigung bleiben unverändert. So können Sie aus einer blauen Textur zum Beispiel eine rote oder eine blasse blaue Textur erzeugen, ohne den umständlichen Weg über die RGB-Farbwerte zu gehen. Dazu wandeln Sie die RGB- in HSV-Werte um, verändern diese und konvertieren sie zurück. Die Routine zu dieser Konvertierung finden Sie im Sourcecode zu dieser Ausgabe.

Filterfunktionen

EINE HOLZTEXTUR ERZEUGEN Sie durch eine einfache Bit-Operation.
EINE HOLZTEXTUR ERZEUGEN Sie durch eine einfache Bit-Operation.

Texturen bearbeiten Sie vielfältig: So können Sie aus einer Fraktalplasma-Textur eine mit Holzmaserung erzeugen. Schieben Sie die Bits der Farbwerte um drei bis fünf Bits nach links. Wenn Sie diese Bits wieder unten einmaskieren, haben Sie eine Holzmaserung:


f = layer[0][x + y * SIZE].r;
f = (f << 3) | ((f > 5) & 7);
layer[1][x + y * SIZE].r =f;
		

Besonders schöne Holztexturen erhalten Sie durch Perlin-Noise-Texturen. Wenn Sie zusätzlich eine andere Startfrequenz für die x- oder y-Achse wählen, erhalten Sie noch bessere Ergebnisse.

Eine klassische Filterfunktion besteht aus einer Matrix, die für einen Pixel angibt, wie Sie seinen Farbwert und die seiner Nachbarn gewichten müssen, um einen neuen Farbwert zu erhalten. Dies ist ein Filter für Bildglättung:


1 2 1
2 4 2
1 2 1
		

Den Farbwert an der aktuellen Position multiplizieren Sie mit 4, die der direkten Nachbarn mit 2 und die der diagonalen Nachbarn mit 1. Nachdem Sie die Farbwerte aufaddiert haben, multiplizieren Sie sie mit 1/16, um die Gesamt­helligkeit des Bildes zu erhalten. Dies ist der Emboss-Filter:


-1 0 1
-1 0 1
-1 0 1
		

Mit dem Emboss-Filter entsteht ein Beleuchtung­seffekt. Mit diesem Beleuchtungs­information können Sie die Ausgangstextur multiplizieren. Wenden Sie den Emboss-Filter auf jeden Pixel an, und addieren Sie 128, um das graue Bild zu erhalten. Mit diesem Wert skalieren Sie den Original­farbwert. Achten Sie bei den Filtern darauf, dass Sie die Pixel nicht gleich mit Ihren neuen Farbwerten überschreiben. Diese Werte benötigen Sie noch zum Filtern des Nachbarpixels. Sie müssen das Resultat immer in einen temporären Layer schreiben.

Sie können die Bildglättung auch so modifizieren, dass Sie das Bild in eine bestimmte Richtung verwischen. Wenn Sie diese Richtung von der Position des Pixels abhängig machen, erhalten Sie wieder neue interessante Effekte.