JavaScript WebGL 繪製一條直線

XXHolic發表於2021-12-08

引子

接著 WebGL 基礎概念,做一個繪製直線的簡單示例。

主要參考以下兩篇文章:

繪製一條線

下面不會對每個使用的函式進行詳細的解釋,個人比較喜歡先對整體邏輯有個感覺,實際使用時再按需去查資料。

建立 WebGL 上下文

基礎概念中有提過是通過 Canvas 元素使用 WebGL :

  <canvas id="demo" width="300" height="200"></canvas>
  const canvasObj = document.querySelector("#demo");
  const glContext = canvasObj.getContext("webgl");

  if (!glContext) {
    alert("瀏覽器不支援 WebGL");
    return;
  }

接著準備頂點資料。

準備頂點資料並緩衝

在 WebGL 中所有實物都是在 3D 空間中,繪製一條線需要兩個頂點,每個頂點都有一個 3D 座標:

let vertices = [
    -0.5, -0.5, 0.0,
    0.5, -0.5, 0.0
  ];

緩衝有多種型別,頂點緩衝物件的型別是 gl.ARRAY_BUFFER

  /**
   * 設定緩衝
   * @param {*} gl WebGL 上下文
   * @param {*} vertexData 頂點資料
   */
  function setBuffers(gl, vertexData) {
    // 建立空白的緩衝物件
    const buffer = gl.createBuffer();
    // 繫結目標
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    // WebGL 不支援直接使用 JavaScript 原始陣列型別,需要轉換
    const dataFormat = new Float32Array(vertexData);
    // 初始化資料儲存
    gl.bufferData(gl.ARRAY_BUFFER, dataFormat, gl.STATIC_DRAW);
  },

bufferData 方法會把資料複製到當前繫結緩衝物件,該方法提供了管理給定資料的引數:

  • STATIC_DRAW : 緩衝區的內容可能經常使用,不會經常更改。
  • DYNAMIC_DRAW : 緩衝區的內容可能經常被使用,並且經常更改。
  • STREAM_DRAW : 緩衝區的內容可能不會經常使用。

直線的資料不會改變,每次渲染都保持不變,所以這裡使用的型別是 STATIC_DRAW 。現在已經把頂點資料儲存在顯示卡的記憶體中,接著開始準備頂點著色器。

頂點著色器

頂點著色器需要用 GLSL ES 語言編寫,在前端書寫形式有兩種:

  • script 標籤包裹,使用時像獲取 DOM 物件一樣。
  • 純字串。
<script id="shader" type="x-shader/x-vertex">
  attribute vec3 vertexPos;
  void main(void){
    gl_Position = vec4(vertexPos, 1);
  }
</script>

<script>
  const shader = document.getElementById('shader').innerHTML,
</script>

每個頂點都有一個 3D 座標,建立了一個 vec3 型別輸入變數 vertexPosvec3 表示三元組浮點數向量。

main 是入口函式,gl_Position 是著色器內建的變數,GLSL 中一個變數最多 4 個分量,最後一個分量是用在透視除法上。gl_Position 設定的值會成為該頂點著色器的輸出。這裡請回想一下基礎概念中提到的狀態機。

下面是純字元形式:

  /**
   * 建立頂點著色器
   * @param {*} gl WebGL 上下文
   */
  function createVertexShader(gl) {
    // 頂點著色器 glsl 程式碼
    const source = `
      attribute vec3 vertexPos;
      void main(void){
        gl_Position = vec4(vertexPos, 1);
      }
    `;

    // 建立著色器
    const shader = gl.createShader(gl.VERTEX_SHADER);

    // 設定頂點著色器程式碼
    gl.shaderSource(shader, source);

    // 編譯
    gl.compileShader(shader);

    // 判斷是否編譯成功
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      alert("編譯著色器報錯: " + gl.getShaderInfoLog(shader));
      gl.deleteShader(shader);
      return null;
    }

    return shader;
  }

為了讓 WebGL 使用該著色器,必須在執行時動態編譯它的原始碼。

  1. createShader 函式建立型別為 gl.VERTEX_SHADER 的著色器物件;
  2. compileShader 函式進行編譯。

接著準備片段著色器。

片段著色器

片段著色器也是用 GLSL ES 語言編寫。片段著色器所做的是計算畫素最後的顏色輸出,這裡直接簡化指定輸出白色。gl_FragColor 是內建變數,表示顏色,4 個分量分別對應 R、G、B、A。

  /**
   * 建立片段著色器
   * @param {*} gl WebGL 上下文
   */
  function createFragmentShader(gl) {
    // 片段著色器 glsl 程式碼
    const source = `
      void main(void){
        gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
      }
    `;

    // 建立著色器
    const shader = gl.createShader(gl.FRAGMENT_SHADER);

    // 設定片段著色器程式碼
    gl.shaderSource(shader, source);

    // 編譯
    gl.compileShader(shader);

    // 判斷是否編譯成功
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      alert("編譯著色器報錯: " + gl.getShaderInfoLog(shader));
      gl.deleteShader(shader);
      return null;
    }

    return shader;
  },

兩個著色器都準備好後,需要進行連結合併才能使用。

著色器程式

著色器程式物件是多個著色器合併之後並最終連結完成的版本。當連結著色器至一個程式的時候,它會把每個著色器的輸出連結到下個著色器的輸入。當輸出和輸入不匹配的時候,會得到一個連線錯誤。

當需要啟用這個著色器的時候,把該物件作為引數呼叫 useProgram 函式。

  /**
   * 初始化著色器程式
   * @param {*} gl WebGL 上下文
   * @param {*} vertexShader 頂點著色器
   * @param {*} fragmentShader 片段著色器
   */
  function initShaderProgram(gl, vertexShader, fragmentShader) {
    // 建立著色器物件
    const shaderProgram = gl.createProgram();
    // 新增著色器
    gl.attachShader(shaderProgram, vertexShader);
    gl.attachShader(shaderProgram, fragmentShader);
    // 多個著色器合併連結
    gl.linkProgram(shaderProgram);
    // 建立是否成功檢查
    if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
      alert("無法初始化著色器程式: " + gl.getProgramInfoLog(shaderProgram));
      return null;
    }

    return shaderProgram;
  }

目前為止,已經把輸入頂點資料傳送給了 GPU ,並指示了 GPU 如何在頂點和片段著色器中處理它。最後就剩下繪製了。

繪製

  • vertexAttribPointer 函式告訴 WebGL 如何解釋頂點資料;
  • enableVertexAttribArray 函式啟用頂點屬性,頂點屬性預設是禁用的;
  • useProgram 函式啟用著色器;
  • drawArrays 函式進行繪製,第一個引數是繪製的圖元的型別,繪製的是直線,所以是 gl.LINE_STRIP

    /**
     * 初始化著色器程式
     * @param {*} gl WebGL 上下文
     * @param {*} shaderProgram 著色器程式物件
     */
    function draw(gl, shaderProgram) {
      // 獲取對應資料索引
      const vertexPos = gl.getAttribLocation(shaderProgram, "vertexPos");
      // 解析頂點資料
      gl.vertexAttribPointer(vertexPos, 3, gl.FLOAT, false, 0, 0);
      // 啟用頂點屬性,頂點屬性預設是禁用的。
      gl.enableVertexAttribArray(vertexPos);
      // 啟用著色器
      gl.useProgram(shaderProgram);
      // 繪製
      gl.drawArrays(gl.LINE_STRIP, 0, 2);
    }

這是示例,整體的邏輯大概是這樣的:

  const canvasObj = document.querySelector("#demo");
  const glContext = canvasObj.getContext("webgl");
  let vertices = [-0.5, -0.5, 0.0, 0.5, -0.5, 0.0]; // 頂點資料

  setBuffers(glContext, vertices); // 緩衝資料
  const vertexShader = createVertexShader(glContext); // 頂點著色器
  const fragmentShader = createFragmentShader(glContext); // 片段著色器
  const shaderProgram = initShaderProgram(
    glContext,
    vertexShader,
    fragmentShader
  ); // 著色器程式物件
  draw(glContext, shaderProgram); // 繪製

這裡面涉及很多的方法和變數,一開始的時候真的懵,多看幾次親自敲下程式碼後會慢慢習慣。

接下來會對期間產生的一些疑問進行總結,見 JavaScript WebGL 基礎疑惑點

參考資料

相關文章