Stories

Detail Return Return

可視化學習:WebGL實現簡易的局部“馬賽克” - Stories Detail

前言

接觸過Canvas的小夥伴應該都知道,在Canvas2D中我們要加載一個圖片很簡單,通過調用drawImage API就能將圖像繪製到畫布上,當然在WebGL中我們也可以繪製圖像,在繪製時我們需要用到WebGL中的紋理對象,在之前WebGL實現網格背景的文章中,我使用了一個叫做紋理座標的配置,現在要完成紋理的加載我們也需要用到紋理座標,並且我們可以通過對紋理座標處理實現簡單的”馬賽克“效果。通過對紋理的使用學習,我感覺自己對紋理座標的認知,和之前學習網格背景時,有點不一樣了,這大概就是學習的過程吧,不斷更新自己的認知。

接下來我會介紹紋理的基礎使用,並在此基礎上實現簡單的局部“馬賽克”效果。

創建紋理並綁定到上下文

在Shader中使用紋理之前,我們需要先在JavaScript中創建紋理對象。

// 創建紋理對象
const texture = gl.createTexture();
// 座標翻轉
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
// 將紋理綁定到當前上下文
gl.bindTexture(gl.TEXTURE_2D, texture);
// 在圖片加載完畢後,指定紋理圖像
gl.texImage2D(
    gl.TEXTURE_2D,
    level,
    internalFormat,
    srcFormat,
    srcType,
    image,
);
// 設置紋理的一些參數
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);

上述代碼中pixelStorei方法設置了像素存儲格式,它的作用是將圖片座標進行翻轉,因為GIF、JPEG和PNG圖片使用的座標系統以左上角為原點,X軸水平向右,Y軸垂直向下,而紋理座標是左下角為原點,Y軸垂直向上,兩者Y軸的方向相反,所以為了使圖片在WebGL中正常顯示,需要這一步操作。

還有一點需要注意的是,在WebGL中使用的圖片需要和當前頁面同源。

使用紋理

完成紋理的創建後,我們就可以通過紋理單元編號激活指定紋理,來在WebGL中使用。

// 按照單元編號激活紋理
gl.activeTexture(gl.TEXTURE0);
// 將紋理綁定到上下文
gl.bindTexture(gl.TEXTURE_2D, texture);
// 獲取Shader中紋理變量
const loc = gl.getUniformLocation(program, "tMap");
// 將對應的紋理單元寫入Shader變量
gl.uniform1i(loc, 0);

默認情況下WebGL會使用第一個紋理,所以如果我們只有一個紋理的話,不調用activeTexture方法也能正常使用紋理。

激活紋理後,我們就可以將對應的紋理單元寫入Shader變量,這樣就可以在Shader中使用紋理了,可以看到這裏向Shader傳遞的並不是紋理對象,而是紋理單元編號。

加載紋理

等到JavaScript傳遞紋理信息後,Shader就可以使用這個紋理了。

precision mediump float; // 添加如下精度描述
uniform sampler2D tMap; // 紋理相關

varying vec2 vUv;

void main() {
  gl_FragColor = texture2D(tMap, vUv);
}

在片元着色器中,我們使用sampler2D類型的變量接收紋理信息。

可以看到在GLSL代碼中,我們使用了一個叫texture2D的函數,這個函數的作用是根據紋理座標,從紋理中提取顏色。

通過對紋理的簡單使用,我目前的理解是,紋理座標是與頂點座標存在一個對應的關係。

// ...
let vertices = new Float32Array([ // 頂點座標
      [-1, -1],
      [-1, 1],
      [1, 1],
      [1, -1]
    ].flat()),
// ...
let vertices = new Float32Array([ // 紋理座標
      [0, 0],
      [0, 1],
      [1, 1],
      [1, 0]
    ].flat()),

兩組座標是對應的,所以在頂點着色器中雖然我們只是指定了頂點,但根據對應關係,此時頂點對應的紋理座標也是已知的。

因為前面對紋理的參數設置(gl.CLAMP_TO_EDGE),紋理是拉伸鋪在整個紋理座標上。(我還沒深入學習紋理的參數,這裏我只是根據單詞意思做的猜測。)

所以就可以通過texture2D函數根據紋理座標提取到圖像對應位置的像素信息了,也就是顏色色值,並將它賦值給gl_FragColor常量、給片元上色。

至此我們就實現了簡單的紋理加載,將圖像繪製到WebGL的畫布上了。

接下去我們就來實現圖片的局部“馬賽克”效果。

因為對於紋理的具體使用步驟我們已經知道了,所以在接下去的例子中,我就使用課程提供的gl-renderer庫來簡化紋理的加載使用操作,專注於效果的實現。

實現局部“馬賽克”

在處理照片時,我們常常需要將一些敏感的或者是不想展示的信息使用馬賽克的效果處理掉,那麼在WebGL中我們要怎麼去實現呢?

  • 首先我們設置馬賽克效果的中心點,對應的是紋理座標的值。

    renderer.uniforms.center = [-2.0, -2.0];

    初始中心點隨意設置一個在0-1之外的位置。

  • 接着設置馬賽克的範圍。

    const radiusPX = 100;
    renderer.uniforms.radiusX = radiusPX / canvasRef.value.width;
    renderer.uniforms.radiusY = radiusPX / canvasRef.value.height;

    我們將馬賽克的半徑範圍設置為100px,並將它轉換為WebGL內對應的數值,使用uniform傳遞給Shader。

  • 然後我們添加鼠標點擊事件的監聽。

    const clickHandler = e => {
      e.preventDefault();
      const {width, height} = canvasRef.value.getBoundingClientRect();
      const {offsetX: x, offsetY: y} = e;
      // 轉換為紋理座標上的值
      const center = [];
      center[0] = x / width;
      center[1] = (height - y) / height;
      renderer.uniforms.center = center;
    };

    offsetX和offsetY分別表示鼠標位置距離元素左邊和頂部的距離,所以height-y表示鼠標位置距離元素底部多遠,通過分別除以寬和高獲得鼠標在WebGL紋理座標上的值。

    這樣通過監聽鼠標點擊事件,我們就可以動態更新馬賽克的位置。

  • 完成uniform的傳遞後,我們就可以在片元着色器中使用了。

    首先我們對片元對應的紋理座標進行縮放,X座標放大50倍,Y座標放大27.7倍,與畫布的寬高比例一致,得到50乘以27.7,也就是1350個20x20大小的方格;同時獲取到X和Y座標的整數部分,整數部分相當於片元所在方格在橫縱座標方向的索引。

    vec2 st = vUv * vec2(50, 27.7);
    vec2 uv = floor(st);

    接着根據原始紋理座標的位置與中心點的距離,我們使用橢圓的公式來判斷片元是否在馬賽克範圍內。(因為畫布寬高不一樣,所以這裏我們用橢圓公式判斷。)

    // 中心點座標
    float x0 = center.x;
    float y0 = center.y;
    
    if (pow(abs(vUv.x - x0), 2.0) / pow(radiusX, 2.0) + pow(abs(vUv.y - y0), 2.0) / pow(radiusY, 2.0) <= 1.0) {
      color = texture2D(tMap, vec2(uv.x / 50.0, uv.y / 27.7));
    } else {
      color = texture2D(tMap, vUv);
    }

    如果是在範圍內的片元,我們就將方格的索引進行縮放,對應到原始紋理座標上的值,因為一個方格內所有的片元對應的索引值一致,所以按照這個值提取到的顏色是一樣的,也就是一個方格內是一種顏色。

    如果不在馬賽克範圍內,就按照普通的提取紋理色值的方式。

  • 最後就是將顏色值賦值給常量gl_FragColor,完成了給片元上色。

到這裏我們就實現了一個簡單的馬賽克效果,可以通過點擊鼠標給圖片指定位置添加馬賽克效果,是一個圓形的樣子,我們可以通過使用不同的公式判斷,呈現不同的馬賽克形狀,比如正方形。

總結

在WebGL中紋理也是比較重要的內容,可以讓我們使用圖片,最早我是在JavaScript高級程序設計這本書中接觸到紋理的,但是因為書裏給出的代碼並不完整,並且我當時也沒去深入瞭解,所以當時的代碼並沒有跑起來,現在我通過學習一個可視化教程才知道説紋理要怎麼去用,瞭解到通過不同參數的設置可以實現不同的紋理表現,呈現不同的視覺效果,本期內容只是簡單的紋理使用,對紋理感興趣的小夥伴可以再自己深入研究。

user avatar cynthia_59675eba1a2ee Avatar mi2nagemao Avatar tinygeeker Avatar lxlu Avatar zengh Avatar laomao_5902e12974409 Avatar guixudepubu Avatar xishui_5ac9a340a5484 Avatar weishiledanhe Avatar papermoon Avatar jiang_rong Avatar webinfoq Avatar
Favorites 19 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.