Dieser Inhalt wurde automatisch aus dem Englischen übersetzt, und kann Fehler enthalten. Erfahre mehr über dieses Experiment.

View in English Always switch to English

Verwendung von Texturen in WebGL

Da unser Beispielprogramm nun einen rotierenden 3D-Würfel hat, lassen Sie uns eine Textur darauf abbilden, anstatt dass seine Flächen einfarbig sind.

Laden von Texturen

Das erste, was zu tun ist, ist Code hinzuzufügen, um die Texturen zu laden. In unserem Fall werden wir eine einzelne Textur verwenden, die auf alle sechs Seiten unseres rotierenden Würfels abgebildet wird, aber dieselbe Technik kann für eine beliebige Anzahl von Texturen verwendet werden.

Hinweis: Es ist wichtig zu beachten, dass das Laden von Texturen den Cross-Domain-Regeln folgt; das heißt, Sie können nur Texturen von Seiten laden, für die Ihre Inhalte eine CORS-Genehmigung haben. Details finden Sie unter Cross-domain Texturen unten.

Hinweis: Fügen Sie diese beiden Funktionen zu Ihrem "webgl-demo.js"-Skript hinzu:

js
//
// Initialize a texture and load an image.
// When the image finished loading copy it into the texture.
//
function loadTexture(gl, url) {
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // Because images have to be downloaded over the internet
  // they might take a moment until they are ready.
  // Until then put a single pixel in the texture so we can
  // use it immediately. When the image has finished downloading
  // we'll update the texture with the contents of the image.
  const level = 0;
  const internalFormat = gl.RGBA;
  const width = 1;
  const height = 1;
  const border = 0;
  const srcFormat = gl.RGBA;
  const srcType = gl.UNSIGNED_BYTE;
  const pixel = new Uint8Array([0, 0, 255, 255]); // opaque blue
  gl.texImage2D(
    gl.TEXTURE_2D,
    level,
    internalFormat,
    width,
    height,
    border,
    srcFormat,
    srcType,
    pixel,
  );

  const image = new Image();
  image.onload = () => {
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(
      gl.TEXTURE_2D,
      level,
      internalFormat,
      srcFormat,
      srcType,
      image,
    );

    // WebGL1 has different requirements for power of 2 images
    // vs. non power of 2 images so check if the image is a
    // power of 2 in both dimensions.
    if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
      // Yes, it's a power of 2. Generate mips.
      gl.generateMipmap(gl.TEXTURE_2D);
    } else {
      // No, it's not a power of 2. Turn off mips and set
      // wrapping to clamp to edge
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    }
  };
  image.src = url;

  return texture;
}

function isPowerOf2(value) {
  return (value & (value - 1)) === 0;
}

Die Routine loadTexture() beginnt mit der Erstellung eines WebGL-Texturobjekts texture durch den Aufruf der WebGL-Funktion createTexture(). Sie lädt dann ein einzelnes blaues Pixel mit texImage2D() hoch. Dadurch ist die Textur sofort als einfarbiges Blau nutzbar, obwohl es einige Momente dauern kann, bis unser Bild heruntergeladen ist.

Um die Textur aus der Bilddatei zu laden, erstellt sie ein Image-Objekt und weist src die URL des Bildes zu, das wir als unsere Textur verwenden möchten. Die Funktion, die wir image.onload zuweisen, wird aufgerufen, sobald das Bild vollständig heruntergeladen ist. An diesem Punkt rufen wir erneut texImage2D() auf, diesmal mit dem Bild als Quelle für die Textur. Danach richten wir das Filtern und Wrapping für die Textur basierend darauf ein, ob das heruntergeladene Bild in beiden Dimensionen eine Zweierpotenz ist oder nicht.

WebGL1 kann nur Texturen, deren Abmessungen keine Zweierpotenz sind, mit Filtern NEAREST oder LINEAR verwenden, und es kann kein Mipmap für sie generieren. Ihr Wrapping-Modus muss auch auf CLAMP_TO_EDGE gesetzt werden. Andererseits, wenn die Textur in beiden Dimensionen eine Zweierpotenz ist, kann WebGL eine höhere Qualität beim Filtern verwenden, es kann Mipmap verwenden und es kann den Wrapping-Modus auf REPEAT oder MIRRORED_REPEAT setzen.

Ein Beispiel für eine wiederholte Textur ist das Kacheln eines Bildes von ein paar Ziegelsteinen, um eine Ziegelwand zu bedecken.

Mipmapping und UV-Wiederholung können mit texParameteri() deaktiviert werden. Dies ermöglicht Texturen mit nicht-Zweierpotenz (NPOT) auf Kosten von Mipmapping, UV-Wrapping, UV-Tiling und Ihrer Kontrolle darüber, wie das Gerät Ihre Textur handhaben wird.

js
// gl.NEAREST is also allowed, instead of gl.LINEAR, as neither mipmap.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// Prevents s-coordinate wrapping (repeating).
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
// Prevents t-coordinate wrapping (repeating).
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

Wiederum, mit diesen Parametern werden kompatible WebGL-Geräte automatisch jede Auflösung für diese Textur akzeptieren (bis zu ihren maximalen Abmessungen). Ohne die oben genannte Konfiguration erfordert WebGL, dass alle Beispiele von NPOT-Texturen durch die Rückgabe von transparentem Schwarz fehlschlagen: rgb(0 0 0 / 0%).

Um das Bild zu laden, fügen Sie einen Aufruf unserer Funktion loadTexture() in unsere main()-Funktion ein. Dies kann nach dem Aufruf von initBuffers(gl) hinzugefügt werden.

Beachten Sie jedoch auch: Browser kopieren Pixel aus dem geladenen Bild von oben nach unten — beginnend in der oberen linken Ecke; WebGL möchte jedoch, dass die Pixel von unten nach oben angeordnet sind — beginnend in der unteren linken Ecke. (Weitere Details finden Sie unter Warum ist meine WebGL-Textur auf dem Kopf?.)

Um zu verhindern, dass die resultierende Bildtextur beim Rendern die falsche Ausrichtung aufweist, müssen wir auch pixelStorei() mit dem Parameter gl.UNPACK_FLIP_Y_WEBGL auf true aufrufen — um die Pixel in die von WebGL erwartete Anordnung von unten nach oben zu kippen.

Hinweis: Fügen Sie den folgenden Code zu Ihrer main()-Funktion hinzu, direkt nach dem Aufruf von initBuffers():

js
// Load texture
const texture = loadTexture(gl, "cubetexture.png");
// Flip image pixels into the bottom-to-top order that WebGL expects.
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);

Hinweis: Laden Sie schließlich die Datei cubetexture.png in das gleiche lokale Verzeichnis herunter wie Ihre JavaScript-Dateien.

Abbildung der Textur auf die Flächen

An diesem Punkt ist die Textur geladen und einsatzbereit. Bevor wir sie verwenden können, müssen wir jedoch die Zuordnung der Texturkoordinaten zu den Eckpunkten der Flächen unseres Würfels einrichten. Dies ersetzt den gesamten zuvor vorhandenen Code zur Konfiguration der Farben für jede der Würfelflächen in initBuffers().

Hinweis: Fügen Sie diese Funktion zu Ihrem Modul "init-buffer.js" hinzu:

js
function initTextureBuffer(gl) {
  const textureCoordBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);

  const textureCoordinates = [
    // Front
    0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
    // Back
    0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
    // Top
    0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
    // Bottom
    0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
    // Right
    0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
    // Left
    0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
  ];

  gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array(textureCoordinates),
    gl.STATIC_DRAW,
  );

  return textureCoordBuffer;
}

Zunächst erstellt dieser Code einen WebGL-Puffer, in den wir die Texturkoordinaten für jede Fläche speichern werden, und binden diesen Puffer als das Array, in das wir schreiben werden.

Das Array textureCoordinates definiert die Texturkoordinaten, die jedem Eckpunkt jeder Fläche entsprechen. Beachten Sie, dass die Texturkoordinaten von 0.0 bis 1.0 reichen; die Abmessungen von Texturen werden für die Zwecke der Texturabbildung auf einen Bereich von 0.0 bis 1.0 normalisiert, unabhängig von ihrer tatsächlichen Größe.

Sobald wir das Texturzuordnungs-Array eingerichtet haben, übergeben wir das Array in den Puffer, sodass WebGL diese Daten für seine Verwendung bereit hat.

Dann geben wir den neuen Puffer zurück.

Als Nächstes müssen wir initBuffers() aktualisieren, um den Texturkoordinatenpuffer anstelle des Farbenspeichers zu erstellen und zurückzugeben.

Hinweis: Ersetzen Sie in der initBuffers()-Funktion Ihres Moduls "init-buffers.js" den Aufruf von initColorBuffer() durch die folgende Zeile:

js
const textureCoordBuffer = initTextureBuffer(gl);

Hinweis: Ersetzen Sie in der initBuffers()-Funktion Ihres Moduls "init-buffers.js" die return-Anweisung durch die folgende:

js
return {
  position: positionBuffer,
  textureCoord: textureCoordBuffer,
  indices: indexBuffer,
};

Aktualisieren der Shader

Das Shader-Programm muss ebenfalls aktualisiert werden, um die Texturen anstelle von einfarbigen Farben zu verwenden.

Der Vertex-Shader

Wir müssen den Vertex-Shader ersetzen, damit er anstatt Farbdaten die Texturkoordinatendaten abruft.

Hinweis: Aktualisieren Sie die vsSource-Deklaration in Ihrer main()-Funktion wie folgt:

js
const vsSource = `
    attribute vec4 aVertexPosition;
    attribute vec2 aTextureCoord;

    uniform mat4 uModelViewMatrix;
    uniform mat4 uProjectionMatrix;

    varying highp vec2 vTextureCoord;

    void main(void) {
      gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
      vTextureCoord = aTextureCoord;
    }
  `;

Die wesentliche Änderung hier ist, dass wir anstelle der Abrufung der Eckpunktfarben die Texturkoordinaten abrufen und an den Fragment-Shader übergeben; dies zeigt den zum Eckpunkt passenden Ort innerhalb der Textur an.

Der Fragment-Shader

Der Fragment-Shader muss ebenfalls aktualisiert werden.

Hinweis: Aktualisieren Sie die fsSource-Deklaration in Ihrer main()-Funktion wie folgt:

js
const fsSource = `
    varying highp vec2 vTextureCoord;

    uniform sampler2D uSampler;

    void main(void) {
      gl_FragColor = texture2D(uSampler, vTextureCoord);
    }
  `;

Anstatt einen Farbwert der Fragmentfarbe zuzuweisen, wird die Fragmentfarbe durch Abrufung des Texels (das heißt, des Pixels innerhalb der Textur) basierend auf dem Wert von vTextureCoord berechnet, der wie die Farben zwischen den Eckpunkten interpoliert wird.

Attribut- und Uniform-Standorte

Da wir ein Attribut geändert und ein Uniform hinzugefügt haben, müssen wir ihre Standorte nachschlagen.

Hinweis: Aktualisieren Sie die programInfo-Deklaration in Ihrer main()-Funktion wie folgt:

js
const programInfo = {
  program: shaderProgram,
  attribLocations: {
    vertexPosition: gl.getAttribLocation(shaderProgram, "aVertexPosition"),
    textureCoord: gl.getAttribLocation(shaderProgram, "aTextureCoord"),
  },
  uniformLocations: {
    projectionMatrix: gl.getUniformLocation(shaderProgram, "uProjectionMatrix"),
    modelViewMatrix: gl.getUniformLocation(shaderProgram, "uModelViewMatrix"),
    uSampler: gl.getUniformLocation(shaderProgram, "uSampler"),
  },
};

Zeichnen des texturierten Würfels

Die Änderungen an der Funktion drawScene() sind einfach.

Hinweis: Fügen Sie die folgende Funktion zur drawScene()-Funktion Ihres Moduls "draw-scene.js" hinzu:

js
// tell webgl how to pull out the texture coordinates from buffer
function setTextureAttribute(gl, buffers, programInfo) {
  const num = 2; // every coordinate composed of 2 values
  const type = gl.FLOAT; // the data in the buffer is 32-bit float
  const normalize = false; // don't normalize
  const stride = 0; // how many bytes to get from one set to the next
  const offset = 0; // how many bytes inside the buffer to start from
  gl.bindBuffer(gl.ARRAY_BUFFER, buffers.textureCoord);
  gl.vertexAttribPointer(
    programInfo.attribLocations.textureCoord,
    num,
    type,
    normalize,
    stride,
    offset,
  );
  gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord);
}

Hinweis: Ersetzen Sie in der drawScene()-Funktion Ihres Moduls "draw-scene.js" den Aufruf von setColorAttribute() durch die folgende Zeile:

js
setTextureAttribute(gl, buffers, programInfo);

Fügen Sie dann Code hinzu, um die Textur zu spezifizieren, die auf die Flächen abgebildet werden soll.

Hinweis: Fügen Sie in Ihrer drawScene()-Funktion, direkt nach den beiden Aufrufen von gl.uniformMatrix4fv(), den folgenden Code hinzu:

js
// Tell WebGL we want to affect texture unit 0
gl.activeTexture(gl.TEXTURE0);

// Bind the texture to texture unit 0
gl.bindTexture(gl.TEXTURE_2D, texture);

// Tell the shader we bound the texture to texture unit 0
gl.uniform1i(programInfo.uniformLocations.uSampler, 0);

WebGL bietet mindestens 8 Textur-Einheiten; die erste davon ist gl.TEXTURE0. Wir sagen WebGL, dass wir Einheit 0 beeinflussen wollen. Wir rufen dann bindTexture() auf, das die Textur an den TEXTURE_2D-Bindungspunkt der Texteinheit 0 bindet. Dann sagen wir dem Shader, dass er für den uSampler Texteinheit 0 verwenden soll.

Zuletzt fügen Sie texture als Parameter zur Funktion drawScene() hinzu, sowohl dort, wo sie definiert, als auch dort, wo sie aufgerufen wird.

Aktualisieren Sie die Deklaration Ihrer drawScene()-Funktion, um den neuen Parameter hinzuzufügen:

js
function drawScene(gl, programInfo, buffers, texture, cubeRotation) {
  // …
}

Aktualisieren Sie die Stelle in Ihrer main()-Funktion, wo Sie drawScene() aufrufen:

js
drawScene(gl, programInfo, buffers, texture, cubeRotation);

An diesem Punkt sollte der rotierende Würfel einsatzbereit sein.

Sehen Sie sich den kompletten Code an | Öffnen Sie dieses Demo auf einer neuen Seite

Cross-domain Texturen

Das Laden von WebGL-Texturen unterliegt den Zugriffskontrollen für Cross-Domains. Damit Ihre Inhalte eine Textur von einer anderen Domain laden können, muss eine CORS-Genehmigung eingeholt werden. Siehe HTTP-Zugriffssteuerung für Details zu CORS.

Moderne Browser behandeln normalerweise den Ursprung von Dateien, die mit dem file:///-Schema geladen werden, als nicht-eindeutige Ursprünge. Selbst wenn eine Datei andere Dateien aus demselben Ordner enthält, wird nicht angenommen, dass sie aus demselben Ursprung stammen, und es können CORS-Fehler ausgelöst werden (siehe Same-origin policy#File origins). Das bedeutet, dass Sie keine Texturen von file:///-URLs in WebGL verwenden können und einen Webserver benötigen, um Ihren Code zu testen und bereitzustellen. Für lokale Tests siehe unseren Leitfaden Wie richten Sie einen lokalen Testserver ein? für Hilfe.

Schauen Sie sich diesen hacks.mozilla.org Artikel für eine Erklärung an, wie Sie CORS-genehmigte Bilder als WebGL-Texturen verwenden können.

Beschädigte (schreibgeschützte) 2D-Canvas-Elemente können nicht als WebGL-Texturen verwendet werden. Ein 2D-<canvas>-Element wird beispielsweise dann beschädigt, wenn ein Cross-Domain-Bild darauf gezeichnet wird.