在 WebGL 中使用紋理

既然我們的示例程式已經有了旋轉的 3D 立方體,那我們就用紋理來對映它,而不是讓它的面是純色。

載入紋理

首先要做的是新增程式碼來載入紋理。在我們的例子中,我們將使用一個單一的紋理,對映到我們旋轉立方體的所有六個面上,但相同的技術可以用於任何數量的紋理。

注意:重要的是要注意,紋理的載入遵循 跨域規則;也就是說,您只能從您的內容獲得 CORS 批准的站點載入紋理。有關詳細資訊,請參閱 下面的跨域紋理

注意:將這兩個函式新增到您的“webgl-demo.js”指令碼中

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;
}

loadTexture() 例程首先透過呼叫 WebGL 的 createTexture() 函式來建立一個 WebGL 紋理物件 texture。然後它使用 texImage2D() 上傳單個藍色畫素。即使可能需要一些時間才能下載我們的影像,這也能使紋理立即可用作純藍色。

要從影像檔案載入紋理,它會建立一個 Image 物件並將 src 設定為我們希望用作紋理的影像的 URL。一旦影像下載完成,就會呼叫分配給 image.onload 的函式。屆時,我們將再次呼叫 texImage2D(),這次使用影像作為紋理的來源。之後,我們將根據下載的影像的兩個維度是否都是 2 的冪來設定紋理的過濾和環繞方式。

WebGL1 只能使用非 2 的冪紋理,並且過濾設定為 NEARESTLINEAR,並且無法為它們生成 mipmap。它們的環繞模式也必須設定為 CLAMP_TO_EDGE。另一方面,如果紋理的兩個維度都是 2 的冪,那麼 WebGL 可以進行更高質量的過濾,可以使用 mipmap,並且可以將環繞模式設定為 REPEATMIRRORED_REPEAT

重複紋理的一個例子是將幾塊磚的影像平鋪以覆蓋磚牆。

可以使用 texParameteri() 停用 mipmapping 和 UV 重複。這將允許使用非 2 的冪 (NPOT) 紋理,但會犧牲 mipmapping、UV 環繞、UV 平鋪以及您對裝置如何處理紋理的控制。

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);

同樣,使用這些引數,相容的 WebGL 裝置將自動接受該紋理的任何解析度(在其最大尺寸範圍內)。如果不執行上述配置,WebGL 會要求 NPOT 紋理的所有采樣都失敗,返回透明黑色:rgb(0 0 0 / 0%)

要載入影像,請在我們的 main() 函式中新增一個對 loadTexture() 函式的呼叫。這可以新增到 initBuffers(gl) 呼叫之後。

但也要注意:瀏覽器按從上到下的順序複製載入影像的畫素——從左上角開始;但 WebGL 希望畫素按從下到上的順序排列——從左下角開始。(有關更多詳細資訊,請參閱 為什麼我的 WebGL 紋理是顛倒的?。)

因此,為了防止生成的影像紋理在渲染時方向不正確,我們還需要呼叫 pixelStorei(),並將 gl.UNPACK_FLIP_Y_WEBGL 引數設定為 true——這樣畫素就會被翻轉為 WebGL 所期望的從下到上的順序。

注意:將以下程式碼新增到您的 main() 函式中,緊跟在 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);

注意:最後,將 cubetexture.png 檔案下載到與您的 JavaScript 檔案相同的本地目錄。

將紋理對映到面上

此時,紋理已載入並準備就緒。但在使用它之前,我們需要建立紋理座標與我們立方體面的頂點之間的對映關係。這將替換 initBuffers() 中所有先前存在的用於為立方體每個面配置顏色的程式碼。

注意:將此函式新增到您的“init-buffer.js”模組中

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;
}

首先,此程式碼建立一個 WebGL 緩衝區,我們將在此緩衝區中儲存每個面的紋理座標,然後我們將該緩衝區繫結為我們將要寫入的陣列。

textureCoordinates 陣列定義了與每個面上的每個頂點對應的紋理座標。請注意,紋理座標的範圍是 0.0 到 1.0;紋理的尺寸在紋理對映的目的是被歸一化到 0.0 到 1.0 的範圍內,而不管它們的實際大小。

設定好紋理對映陣列後,我們就將陣列傳遞到緩衝區中,以便 WebGL 可以準備好使用該資料。

然後我們返回新的緩衝區。

接下來,我們需要更新 initBuffers() 以建立並返回紋理座標緩衝區,而不是顏色緩衝區。

注意:在您的“init-buffers.js”模組的 initBuffers() 函式中,將對 initColorBuffer() 的呼叫替換為以下行

js
const textureCoordBuffer = initTextureBuffer(gl);

注意:在您的“init-buffers.js”模組的 initBuffers() 函式中,將 return 語句替換為以下內容

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

更新著色器

著色器程式也需要更新,以便使用紋理而不是純色。

頂點著色器

我們需要替換頂點著色器,以便它能獲取紋理座標資料而不是獲取顏色資料。

注意:在您的 main() 函式中,像這樣更新 vsSource 宣告

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;
    }
  `;

這裡的關鍵變化是,我們獲取的是紋理座標並將其傳遞給片段著色器,而不是獲取頂點顏色;這將指示紋理中與頂點對應的位置。

片段著色器

片段著色器同樣需要更新。

注意:在您的 main() 函式中,像這樣更新 fsSource 宣告

js
const fsSource = `
    varying highp vec2 vTextureCoord;

    uniform sampler2D uSampler;

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

片段的顏色不是透過分配顏色值來確定的,而是透過根據 vTextureCoord 的值(該值與顏色一樣,在頂點之間進行插值)來獲取 紋素(即紋理中的畫素)來計算的。

屬性和統一變數位置

由於我們更改了一個屬性並添加了一個統一變數,因此我們需要查詢它們的位置。

注意:在您的 main() 函式中,像這樣更新 programInfo 宣告

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"),
  },
};

繪製紋理化的立方體

drawScene() 函式的更改很簡單。

注意:在您的“draw-scene.js”模組的 drawScene() 函式中,新增以下函式

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);
}

注意:在您的“draw-scene.js”模組的 drawScene() 函式中,將對 setColorAttribute() 的呼叫替換為以下行

js
setTextureAttribute(gl, buffers, programInfo);

然後新增程式碼來指定要對映到面上的紋理。

注意:在您的 drawScene() 函式中,在兩次呼叫 gl.uniformMatrix4fv() 之後,立即新增以下程式碼

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 提供了至少 8 個紋理單元;第一個是 gl.TEXTURE0。我們告訴 WebGL 我們要影響單元 0。然後我們呼叫 bindTexture(),它將紋理繫結到紋理單元 0 的 TEXTURE_2D 繫結點。然後我們告訴著色器 uSampler 使用紋理單元 0。

最後,將 texture 新增為 drawScene() 函式的引數,包括定義它的地方和呼叫它的地方。

更新您 drawScene() 函式的宣告以新增新引數

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

更新您 main() 函式中呼叫 drawScene() 的地方

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

至此,旋轉的立方體應該準備就緒。

檢視完整程式碼 | 在新頁面開啟此演示

跨域紋理

WebGL 紋理的載入受跨域訪問控制的約束。為了讓您的內容載入另一個域的紋理,需要獲得 CORS 批准。有關 CORS 的詳細資訊,請參閱 HTTP 訪問控制

由於 WebGL 現在要求紋理必須從安全上下文載入,因此您不能在 WebGL 中使用從 file:/// URL 載入的紋理。這意味著您需要一個安全的 Web 伺服器來測試和部署您的程式碼。有關本地測試,請參閱我們的指南 如何設定本地測試伺服器?

有關如何使用 CORS 批准的影像作為 WebGL 紋理的解釋,請參閱這篇 hacks.mozilla.org 文章

被標記(僅寫入)的 2D 畫布不能用作 WebGL 紋理。例如,當一個跨域影像被繪製到 2D <canvas> 上時,該畫布會被標記。