WebGL渲染2D圖形

WebGL是OpenGL ES 2.0的Web標準,它結合JavaScript在HTML5的<canvas></canvas>上渲染圖形。WebGL現在已經是HTML5 Canvas的繪製上下文之一。

因此在使用上的流程和OpenGL ES存在了諸多相似之處,函數名和參數也幾乎是相同的。

...
<canvas id="main-canvas"></canvas>
...
let gl = document.getElementById("main-canvas").getContext("webgl");

if (!gl) {
    alert("無法使用WebGL!");
} else {
    console.log(gl);
}

這樣,就獲取到了在canvas上渲染webgl的上下文。

接下來,在canvas上一切的渲染操作,最終都是要依靠這個gl變量來控制的。可以通過變量gl來使用WebGL API了。

着色器準備

首先要準備一下着色器的源碼,着色器(shader)是由專門的着色器語言(Shadering Language)編寫的,在OpenGL ES中,使用的是GLSL(OpenGL Shadering Language,一種類C++語言)。着色器是成對出現的,分別是頂點着色器:

<script type="notjs" id="vertex-shader-2d">
    attribute vec4 a_position;

    void main() {
        gl_Position = a_position;
    }
</script>

和片段着色器:

<script type="notjs" id="fragment-shader-2d">
    precision mediump float;

    void main() {
        gl_FragColor = vec4(1, 0.3, 0.5, 1);
    }
</script>

頂點着色器接受一些輸入,輸出的是裁剪平面的頂點座標。

因爲最終在canvas上渲染圖形還是要使用JavaScript而不是GLSL,所以需要獲取這些GLSL代碼,然後編譯處理成JavaScript的對象。

// 獲取着色器源碼
const vertex_shader_source = document.querySelector("#vertex-shader-2d").text,
    fragment_shader_source = document.querySelector("#fragment-shader-2d").text;

先編譯着色器:

// 編譯
function setupShader(gl, type, source) {
    let shader = gl.createShader(type);    // 先創建空着色器對象
    gl.shaderSource(shader, source);       // 載入着色器源碼
    gl.compileShader(shader);              // 編譯

    let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if (success) {
        return shader;
    }

    console.log(gl.getShaderInfoLog(shader));
    gl.deleteShader(shader);
}

然後鏈接頂點着色器和片段着色器,生成一個着色器程序的js對象:

// 鏈接
function createProgram(gl, vertexShader, fragmentShader) {
    let program = gl.createProgram();            // 先創建一個空的着色器程序
    gl.attachShader(program, vertexShader);     // 載入編譯好的頂點着色器
    gl.attachShader(program, fragmentShader);   // 載入編譯好的片段着色器
    gl.linkProgram(program);                     // 鏈接

    let success = gl.getProgramParameter(program, gl.LINK_STATUS);
    if (success) {
        return program;
    }

    console.log(gl.getProgramInfoLog(program));
    gl.deleteProgram(program);
}

 然後調用該函數,program變量就是包含了這一對着色器源碼的js對象:

let program = createProgram(gl, vertex_shader_source, fragment_shader_source);

因爲在着色器中有一個輸入值,即頂點着色器中的a_position,着色器接收輸入值必須從緩衝區中獲取,而獲取到的值需要在着色器中的正確位置放入,所以要找到頂點着色器中a_position的位置:

let position_attribute_location = gl.getAttribLocation(program, "a_position");

緩衝區準備

着色器設置好了之後,就要開始準備數據,作爲着色器的輸入了。

上文說過,作爲着色器的輸入數據,必須是在緩衝區中的數據。而現在沒有緩衝區,也沒有數據,所以接下來創建這兩個東西,然後把數據放入緩衝區中。

先創建緩衝區:

let position_buffer = gl.createBuffer();

然後使用ARRAY_BUFFER綁定點,綁定到這個position_buffer(使用ARRAY_BUFFER說明要用頂點數組方式繪圖):

gl.bindBuffer(gl.ARRAY_BUFFER, position_buffer);

 緩衝區準備好了,接下來準備數據:

let positions = [
    0, 0,
    0, 0.2,
    0.7, 0,
    -0.5, -0.5,
    -0.4, -0.2,
    -0.7, 0,
];

然後通過ARRAY_BUFFER綁定點,將數據放到緩衝區中:

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

準備畫布

由於WebGL的裁剪空間是一個橫縱座標都是在[-1, 1]範圍內的矩形,而我們的畫布的尺寸一般不是這個,所以需要作一下尺寸上的映射:

gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

 然後清空一下畫布:

gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);

渲染

要開始渲染了。首先我們要指定着色器程序:

gl.useProgram(program);

然後啓用a_position這個輸入值(屬性):

gl.enableVertexAttribArray(position_attribute_location);

 之後準備將緩衝區的數據輸送到着色器:

var size = 2;
var type = gl.FLOAT;
var normalize = false;
var stride = 0;
var offset = 0;
gl.vertexAttribPointer(position_attribute_location, size, type, normalize, stride, offset);

對這裏參數的說明:

  • size=2表示每次迭代讀取兩個數據,即x和y。由於頂點着色器中gl_Position的類型是vec4,包含x,y,z,w四個數據,而這裏只需要前兩個x和y。
  • type=gl_FLOAT表示使用的是32爲浮點類型的數據。
  • normalize=false表示不需要歸一化數據。
  • offset=0表示從緩衝區的開始位置讀取數據。

最後設置圖元類型、通過drawArrays(),使用頂點數組中的頂點數據繪製:

var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 6;
gl.drawArrays(primitiveType, offset, count);

結果:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>GLSL-test</title>
  </head>

  <body>
    <canvas id="main-canvas"></canvas>

    <!-- 頂點着色器 -->
    <script type="notjs" id="vertex-shader-2d">
      attribute vec4 a_position;

      void main() {
          gl_Position = a_position;
      }
    </script>

    <!-- 片段着色器 -->
    <script type="notjs" id="fragment-shader-2d">
      precision mediump float;

      void main() {
          gl_FragColor = vec4(1, 0.3, 0.5, 1);
      }
    </script>

    <script>
      // 全局變量
      let gl;

      // 準備WebGL上下文
      function prepareContext() {
        // 使用Canvas
        let main_canvas = document.querySelector("#main-canvas");
        main_canvas.setAttribute("width", "640");
        main_canvas.setAttribute("height", "640");
        gl = main_canvas.getContext("webgl");
        if (!gl) {
          alert("無法使用WebGL!");
        } else {
          console.log(gl);
        }
      }

      // 編譯
      function setupShader(gl, type, source) {
        let shader = gl.createShader(type);
        gl.shaderSource(shader, source);
        gl.compileShader(shader);

        let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
        if (success) {
          return shader;
        }

        console.log(gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
      }

      // 鏈接
      function linkShader(gl, vertexShader, fragmentShader) {
        let program = gl.createProgram();
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);
        gl.linkProgram(program);
        let success = gl.getProgramParameter(program, gl.LINK_STATUS);
        if (success) {
          return program;
        }

        console.log(gl.getProgramInfoLog(program));
        gl.deleteProgram(program);
      }

      // 創建緩衝區,並填入頂點數據
      function createBuffer(data) {
        gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
      }

      (function main() {
        // 環境上下文準備
        prepareContext();

        // 着色器準備
        const vertex_shader_source = document.querySelector("#vertex-shader-2d").text,
          fragment_shader_source = document.querySelector("#fragment-shader-2d").text;

        // 編譯
        let vertex_shader = setupShader(gl, gl.VERTEX_SHADER, vertex_shader_source),
          fragment_shader = setupShader(gl, gl.FRAGMENT_SHADER, fragment_shader_source);

        // 鏈接
        let shaderProgram = linkShader(gl, vertex_shader, fragment_shader);
        let position_attribute_location = gl.getAttribLocation(shaderProgram, "a_position");

        // 準備輸入數據
        createBuffer([0, 0, -0.3, 0.2, 0.2, 0.5, -0.5, -0.5, -0.4, -0.2, -0.7, 0]);

        // 準備畫布
        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
        gl.clearColor(0, 0, 0, 0);
        gl.clear(gl.COLOR_BUFFER_BIT);

        // 加載着色器,準備讀取緩衝區的數據
        gl.useProgram(shaderProgram);
        gl.enableVertexAttribArray(position_attribute_location);
        gl.vertexAttribPointer(position_attribute_location, 2, gl.FLOAT, false, 0, 0);

        // 執行繪製
        gl.drawArrays(gl.TRIANGLES, 0, 6);
      })();
    </script>
  </body>
</html>

參考

https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API/Tutorial

https://webglfundamentals.org/webgl/lessons/zh_cn/

https://thebookofshaders.com/?lan=ch

發佈了23 篇原創文章 · 獲贊 6 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章