JavaScript WebGL 使用圖片

XXHolic發表於2022-01-08

引子

JavaScript WebGL 設定顏色效果始終有限,這時候就會想到使用圖片,這就涉及到 WebGL 中的紋理使用,比預想中要麻煩的多。

使用圖片

紋理(texture)可以用來新增模擬物體的細節,在 3D 遊戲中各種模擬的物體都使用了紋理。在繪製矩形的基礎上主要有以下幾個方面的變化:

  • 資料
  • 頂點著色器
  • 片元著色器
  • 緩衝紋理座標資料
  • 載入並建立紋理
  • 繪製

資料

先準備一張圖片,然後為了把紋理對映到對應的矩形上,需要指定矩形每個頂點各自對應紋理的那個位置。

紋理二維座標在 x 和 y 軸上,範圍為 0 到 1 之間。紋理座標起於(0, 0),對應圖片的左下角,終於(1, 1),對應圖片的右上角。所以對應的紋理座標為:

  const texCoords = [
    1.0,
    1.0, // 右上角
    0.0,
    1.0, // 左上角
    0.0,
    0.0, // 左下角
    1.0,
    0.0, // 右下角
  ];

頂點著色器

紋理座標需要進行緩衝並進行傳遞,在頂點著色器中增加了變數 aVertexTextureCoord ,其值會傳遞到片元著色器。

  const source = `
    attribute vec3 aVertexPos;
    attribute vec2 aVertexTextureCoord;

    varying highp vec2 vTextureCoord;
    void main(void){
      gl_Position = vec4(aVertexPos, 1);
      vTextureCoord = aVertexTextureCoord;
    }
  `;

片元著色器

在片元著色器中接受紋理座標,定義紋理取樣器 uSampler ,注意這個是全域性變數,在任何階段都可以訪問,目前還沒有值。內建的方法 texture2D 獲得最終的顏色。

  const fragmentSource = `
    varying highp vec2 vTextureCoord;
    uniform sampler2D uSampler;
    void main(void){
      gl_FragColor = texture2D(uSampler, vTextureCoord);
    }
  `;

緩衝紋理座標資料

紋理座標資料同樣需要進入緩衝。

/**
 * 緩衝紋理座標資料並啟用
 * @param {*} gl WebGL 上下文
 * @param {*} shaderProgram 著色器程式
 * @param {*} data 紋理座標資料
 */
function setTextureBuffers(gl, shaderProgram, data) {
  // 建立空白的緩衝物件
  const buffer = gl.createBuffer();
  // 繫結目標
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  // WebGL 不支援直接使用 JavaScript 原始陣列型別,需要轉換
  const dataFormat = new Float32Array(data);
  // 初始化資料儲存
  gl.bufferData(gl.ARRAY_BUFFER, dataFormat, gl.STATIC_DRAW);

  // 獲取對應資料索引
  const texCoord = gl.getAttribLocation(
    shaderProgram,
    "aVertexTextureCoord"
  );
  // 解析頂點資料
  gl.vertexAttribPointer(texCoord, 2, gl.FLOAT, false, 0, 0);
  // 啟用頂點屬性,頂點屬性預設是禁用的。
  gl.enableVertexAttribArray(texCoord);
}

載入並建立紋理

要保證圖片載入完成後才能使用。得到圖片資料後需要建立紋理物件。

function loadImage(gl) {
  var img = new Image();
  img.onload = (e) => {
    createTexture(gl, e.target);
  };
  img.src = "./1.jpg";
}

function createTexture(gl, source) {
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  // 反轉圖片 Y 軸方向
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
  // 紋理座標水平填充 s
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  // 紋理座標垂直填充 t
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  // 紋理放大處理
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  // 紋理縮小處理
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  // 圖片資料賦給紋理物件
  gl.texImage2D(
    gl.TEXTURE_2D,
    0,
    gl.RGBA,
    gl.RGBA,
    gl.UNSIGNED_BYTE,
    source
  );
  // 啟用紋理
  gl.activeTexture(gl.TEXTURE0);
}

createTexture 建立紋理物件,接著使用 bindTexture 並繫結到對應的目標,這裡是二維的圖片,第一個引數值取 gl.TEXTURE_2D 表示二維紋理,第二個引數是紋理物件,當為 null 時表示取消繫結。繫結之後才能對紋理進行進一步操作。

pixelStorei 方法對影像進行反轉 Y 方向座標,這是因為圖片的座標系統和紋理參考的座標系不一樣。

texParameteri 方法設定紋理的各種引數,這裡需要特地說明一下,如果希望使用各種尺寸的圖片,需要對水平和垂直填充進行上面的設定,否則只能顯示特定尺寸的圖片。

texImage2D 方法把紋理源賦值給紋理物件,這裡就是把影像的畫素資料傳給紋理物件,這樣才能在繪製紋理的時候看到影像。

activeTexture 方法啟用指定的紋理,紋理單元的範圍是 0 到 gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS - 1 ,這裡只有一個,取值為 gl.TEXTUREI0 。預設的第一個紋理單元總是啟用的,所以這行程式碼可以去掉。

繪製

在片元著色器中宣告的全域性變數,繪製時使用 uniform1i 方法指定對應的值,第二個參數列示紋理單元,這裡 0 就是第一個紋理單元。

/**
 * 繪製
 * @param {*} gl WebGL 上下文
 * @param {*} shaderProgram 著色器程式
 */
function draw(gl, shaderProgram) {
  // 獲取紋理取樣器
  const samplerUniform = gl.getUniformLocation(shaderProgram, "uSampler");
  // 指定全域性變數關聯的紋理單元
  gl.uniform1i(samplerUniform, 0);
  gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
}

效果

這是示例, 效果如下:

96-result

如果對比原圖的話,可以發現這張圖片變形了,並沒有自適應。

參考資料

相關文章