WebGL基礎(一): 從一個滑鼠畫點開始瞭解原生webGL

undefined發表於2022-03-14
面向web前端的WebGL教程,網路上的教程均是假設有計算機圖形學基礎,對web開發者來說不是很友好, 故開闢此坑

最終效果

https://codepen.io/chendonmin...

滑鼠點選 畫一個點。

webGL如何展示一個點

首先得知道webGL如何展示出一個點?

webGL畫任意物體 都需要一個頂點著色器片元著色器,

頂點著色器:描述頂點的特性(位置、顏色等)的程式.

片元著色器: 進行著片元處理過程的程式。

也許你會很懵,一大堆官方理論又要望而卻步了,所以我直接展示下最簡單的展示一個點的程式碼,相信你會馬上明白。

<canvas id="glcanvas" width="640" height="480">
    你的瀏覽器似乎不支援或者禁用了HTML5 <code>&lt;canvas&gt;</code> 元素.
</canvas>

首先,我需要一些簡單的封裝函式:

function initShaders(gl, vshader, fshader) {
  var program = createProgram(gl, vshader, fshader);
  if (!program) {
    console.log('Failed to create program');
    return false;
  }

  gl.useProgram(program);
  gl.program = program;

  return true;
}

/**
 * Create the linked program object
 * @param gl GL context
 * @param vshader a vertex shader program (string)
 * @param fshader a fragment shader program (string)
 * @return created program object, or null if the creation has failed
 */
function createProgram(gl, vshader, fshader) {
  // Create shader object
  var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
  var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
  if (!vertexShader || !fragmentShader) {
    return null;
  }

  // Create a program object
  var program = gl.createProgram();
  if (!program) {
    return null;
  }

  // Attach the shader objects
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);

  // Link the program object
  gl.linkProgram(program);

  // Check the result of linking
  var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (!linked) {
    var error = gl.getProgramInfoLog(program);
    console.log('Failed to link program: ' + error);
    gl.deleteProgram(program);
    gl.deleteShader(fragmentShader);
    gl.deleteShader(vertexShader);
    return null;
  }
  return program;
}

/**
 * Create a shader object
 * @param gl GL context
 * @param type the type of the shader object to be created
 * @param source shader program (string)
 * @return created shader object, or null if the creation has failed.
 */
function loadShader(gl, type, source) {
  // Create shader object
  var shader = gl.createShader(type);
  if (shader == null) {
    console.log('unable to create shader');
    return null;
  }

  // Set the shader program
  gl.shaderSource(shader, source);

  // Compile the shader
  gl.compileShader(shader);

  // Check the result of compilation
  var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  if (!compiled) {
    var error = gl.getShaderInfoLog(shader);
    console.log('Failed to compile shader: ' + error);
    gl.deleteShader(shader);
    return null;
  }

  return shader;
}
這是一段初始化著色器的函式

初始化webgl:

const canvas = document.querySelector("#glcanvas");
// 初始化WebGL上下文
const gl = canvas.getContext("webgl");

// 確認WebGL支援性
if (!gl) {
    alert("無法初始化WebGL,你的瀏覽器、作業系統或硬體等可能不支援WebGL。");
    return;
}
// 使用完全不透明的黑色清除所有影像
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 用上面指定的顏色清除緩衝區
gl.clear(gl.COLOR_BUFFER_BIT);

呼叫初始化著色器函式。

const VSHADER_SOURCE = `
    void main() {
        gl_Position = vec4(0.0 ,0.0 ,0.0 , 1.0);
        gl_PointSize = 10.0;
    }
`;
const FSHADER_SOURCE = `
    void main() {
        gl_FragColor = vec4(1.0 ,0.0 ,0.0 ,1.0);
    }
`;
 initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);
 gl.drawArrays(gl.POINTS, 0, 1);

OK,目前為止,你應該能看到黑色canvas中間有個紅色的點了。

解析

最關鍵的部分,其實就是VSHADER_SOURCEFSHADER_SOURCE兩個字串,分別表示了點的座標和點的顏色。

VSHADER_SOURCEFSHADER_SOURCE是屬於glsl程式碼,

VSHADER_SOURCE中的gl_Position代表的就是點的位置,gl_Positionglsl的內建變數。

你會發現gl_Position的值是個vec4型別,座標居然有4個值?其實這個是齊次座標.

對於vec4(x, y, z, w), 真實的世界座標是 (x/w, y/w, z/w), 所以一般vec4第四個引數我們設定為1

為什麼需要齊次座標呢,因為三維世界中,向量也是三個座標表示的, 所以為了區分向量和真實位置引入了第四個引數,向量的第四個引數是0.

js和GLSL通訊

上面程式碼確實畫出一個點, 但是是寫在一個字串中的,這肯定不方便我們進行操作啊,所以操作glsl中的變數就很有必要了。

const VSHADER_SOURCE = `
    attribute vec4 a_Position;
    void main() {
        gl_Position = a_Position;
        gl_PointSize = 10.0;
    }
`;

如上圖,我們對頂點著色器程式碼 加入了一個attribute, 然後attribute a_Position賦值給glsl內建變數gl_Position,這是否意味著我改動a_Position的值,gl_Position也會改變呢?

js獲取並修改attribute

需要的API:

gl.getAttribLocation(gl.program, attribute);
gl.vertexAttrib3f(index, x, y, z);

getAttribLocation方法返回了給定WebGLProgram物件中某屬性的下標指向位置

vertexAttrib3f可以為頂點attibute變數賦值

現在只需要在gl.drawArrays(gl.POINTS, 0, 1);之前修改attribute即可

var a_Position = gl.getAttribLocation(gl.program, "a_Position");
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);

目前為止,完整程式碼如下:

    const canvas = document.querySelector("#glcanvas");
    // 初始化WebGL上下文
    const gl = canvas.getContext("webgl");

    // 確認WebGL支援性
    if (!gl) {
        alert("無法初始化WebGL,你的瀏覽器、作業系統或硬體等可能不支援WebGL。");
        return;
    }
    // 使用完全不透明的黑色清除所有影像
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    // 用上面指定的顏色清除緩衝區
    gl.clear(gl.COLOR_BUFFER_BIT);

    const VSHADER_SOURCE = `
        attribute vec4 a_Position;
        void main() {
            gl_Position = a_Position;
            gl_PointSize = 10.0;
        }
    `;
    const FSHADER_SOURCE = `
        void main() {
             gl_FragColor = vec4(1.0 ,0.0 ,0.0 ,1.0);
         }
    `;

  //初始化著色器
  initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);

  var a_Position = gl.getAttribLocation(gl.program, "a_Position");
  gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);

  //畫點
  gl.drawArrays(gl.POINTS, 0, 1);

畫多個點

gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);
gl.drawArrays(gl.POINTS, 0, 1);

gl.vertexAttrib3f(a_Position, 0.5, 0.5, 0.0);
gl.drawArrays(gl.POINTS, 0, 1);

你會發現螢幕上存在了兩個紅點。

drawArrays方法用於從向量陣列中繪製圖元,每執行一次就要通知GPU渲染圖元。

現在兩個點還好,如果是成千上萬個點呢?我們需要一次性畫多個點,這樣才能保持效能。

型別化陣列TypedArray

對於多個點,我們需要把點的位置存在變數中,我們選擇了TypedArray,
它相比普通Array有幾個好處:效能 效能 還tm是效能。
對於typedArray介紹看如下程式碼:

// 下面程式碼是語法格式,不能直接執行,
// TypedArray 關鍵字需要替換為底部列出的建構函式。
new TypedArray(); // ES2017中新增
new TypedArray(length);
new TypedArray(typedArray);
new TypedArray(object);
new TypedArray(buffer [, byteOffset [, length]]);

// TypedArray 指的是以下的其中之一:

Int8Array();
Uint8Array();
Uint8ClampedArray();
Int16Array();
Uint16Array();
Int32Array();
Uint32Array();
Float32Array();
Float64Array();

那怎麼選擇呢, 看如下列表:

型別單個元素值的範圍大小(bytes)描述Web IDL 型別C 語言中的等價型別
Int8Array-128 to 12718 位二進位制有符號整數byteint8_t
Uint8Array0 to 25518 位無符號整數(超出範圍後從另一邊界迴圈)octetuint8_t
Uint8ClampedArray0 to 25518 位無符號整數(超出範圍後為邊界值)octetuint8_t
Int16Array-32768 to 32767216 位二進位制有符號整數shortint16_t
Uint16Array0 to 65535216 位無符號整數unsigned shortuint16_t
Int32Array-2147483648 to 2147483647432 位二進位制有符號整數longint32_t
Uint32Array0 to 4294967295432 位無符號整數unsigned longuint32_t
Float32Array1.2×10^-38 to 3.4×10^38432 位 IEEE 浮點數(7 位有效數字,如 1.1234567unrestricted floatfloat
Float64Array5.0×10^-324 to 1.8×10^308864 位 IEEE 浮點數(16 有效數字,如 1.123...15)unrestricted doubledouble
BigInt64Array-2^63 to 2^63-1864 位二進位制有符號整數bigintint64_t (signed long long)
BigUint64Array0 to 2^64 - 1864 位無符號整數bigintuint64_t (unsigned long long)

對於這個教程,因為等會我們會使用浮點數,又因為資料不大,所以選擇Float32Array.

嘗試繪製兩個點

  • 將兩個點的座標儲存到變數中

    const verties = new Float32Array([0.0, 0.5, -0.5, -0.5]);
  • 把資料掛到緩衝區某個記憶體位置,寫入資料

      const vertexBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
      // verties就是我們自己建立的Float32Array資料
      gl.bufferData(gl.ARRAY_BUFFER, verties, gl.STATIC_DRAW);
  • 讀取資料並修改attribute

     const a_Position = gl.getAttribLocation(gl.program, "a_Position");
      gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
      gl.enableVertexAttribArray(a_Position);
    理解vertexAttribPointer函式可以看我的這篇筆記
    https://note.youdao.com/s/c5E...
  • 修改了attribute,下一步呼叫繪製命令

    // 因為是繪製兩個點,第三個引數輸入2
    gl.drawArrays(gl.POINTS, 0, 2);

完整程式碼

除去初始化webGL和工具函式initShaders, 因為每次都寫 沒有變化...

  initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);

  const verties = new Float32Array([0.0, 0.5, -0.5, -0.5]);

  const vertexBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, verties, gl.STATIC_DRAW);

  const a_Position = gl.getAttribLocation(gl.program, "a_Position");

  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(a_Position);
  gl.drawArrays(gl.POINTS, 0, 2);

滑鼠監聽座標並寫入

如果上面的都理解了話,第三步反而是最簡單的了(對於web開發人員來說).
具體功能上面程式碼都是實現了,只需要:

  • 點選的時候把螢幕座標轉成webGL座標
  • 把座標存入Float32Array資料
  • 修改attribute,渲染。

轉成webGl座標

const x = (e.offsetX - 320) / 320;
const y = -(e.offsetY - 240) / 240;

其中 320 = 640/2
240 = 480/2

320代表canvas元素的寬, 240代表canvas元素高

存入Float32Array資料

首先Float32Array是固定長度的,無法動態修改,所以需要新建一個Float32Array

const newArr = new Float32Array(length + 2)
for (let i = 0; i < arrayBuffer.length; i++) {
    newArr[i] = arrayBuffer[i]
}
newArr[arrayBuffer.length] = x;
newArr[arrayBuffer.length + 1] = y;

最終程式碼

程式碼可以在codePen裡檢視, 如果無法開啟的話, 我將程式碼展示出來:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>test</title>
</head>

<body onload="main()">
    <canvas id="glcanvas" width="640" height="480">
        你的瀏覽器似乎不支援或者禁用了HTML5 <code>&lt;canvas&gt;</code> 元素.
    </canvas>
</body>
<script src="utils/cuon-utils.js"></script>
<script>
    let arrayBuffer = new Float32Array()
    function main() {
        const canvas = document.querySelector("#glcanvas");
        // 初始化WebGL上下文
        const gl = canvas.getContext("webgl");

        // 確認WebGL支援性
        if (!gl) {
            alert("無法初始化WebGL,你的瀏覽器、作業系統或硬體等可能不支援WebGL。");
            return;
        }
        // 使用完全不透明的黑色清除所有影像
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        // 用上面指定的顏色清除緩衝區
        gl.clear(gl.COLOR_BUFFER_BIT);

        const VSHADER_SOURCE = `
            attribute vec4 a_Position;
            void main() {
                gl_Position = a_Position;
                gl_PointSize = 10.0;
            }
        `;
        const FSHADER_SOURCE = `
            void main() {
                 gl_FragColor = vec4(1.0 ,0.0 ,0.0 ,1.0);
             }
        `;
        initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);

        // 監聽點選事件
        document.getElementById('glcanvas').addEventListener('mousedown', e => {
            clear(gl);
            // 左上角原點座標
            const x = (e.offsetX - 320) / 320;
            const y = -(e.offsetY - 240) / 240;
            let length = arrayBuffer.length;
            const newArr = new Float32Array(length + 2)
            for (let i = 0; i < arrayBuffer.length; i++) {
                newArr[i] = arrayBuffer[i]
            }
            newArr[arrayBuffer.length] = x;
            newArr[arrayBuffer.length + 1] = y;
            const len = initVertexBuffer(gl, newArr);
            gl.drawArrays(gl.POINTS, 0, len);
            arrayBuffer = newArr;
        })
    }

    function initVertexBuffer(gl, verties) {
        const vertexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, verties, gl.STATIC_DRAW);

        const a_Position = gl.getAttribLocation(gl.program, "a_Position");
        const FSIZE = verties.BYTES_PER_ELEMENT
        gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 2 * FSIZE, 0);
        gl.enableVertexAttribArray(a_Position);
        return verties.length / 2;
    }

    function clear(gl) {
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT);
    }
</script>

</html>

其中cuon-utils.js是封裝的一個小工具函式

// cuon-utils.js (c) 2012 kanda and matsuda
/**
 * Create a program object and make current
 * @param gl GL context
 * @param vshader a vertex shader program (string)
 * @param fshader a fragment shader program (string)
 * @return true, if the program object was created and successfully made current 
 */
 function initShaders(gl, vshader, fshader) {
    var program = createProgram(gl, vshader, fshader);
    if (!program) {
      console.log('Failed to create program');
      return false;
    }
  
    gl.useProgram(program);
    gl.program = program;
  
    return true;
  }
  
  /**
   * Create the linked program object
   * @param gl GL context
   * @param vshader a vertex shader program (string)
   * @param fshader a fragment shader program (string)
   * @return created program object, or null if the creation has failed
   */
  function createProgram(gl, vshader, fshader) {
    // Create shader object
    var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
    var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
    if (!vertexShader || !fragmentShader) {
      return null;
    }
  
    // Create a program object
    var program = gl.createProgram();
    if (!program) {
      return null;
    }
  
    // Attach the shader objects
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
  
    // Link the program object
    gl.linkProgram(program);
  
    // Check the result of linking
    var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
    if (!linked) {
      var error = gl.getProgramInfoLog(program);
      console.log('Failed to link program: ' + error);
      gl.deleteProgram(program);
      gl.deleteShader(fragmentShader);
      gl.deleteShader(vertexShader);
      return null;
    }
    return program;
  }
  
  /**
   * Create a shader object
   * @param gl GL context
   * @param type the type of the shader object to be created
   * @param source shader program (string)
   * @return created shader object, or null if the creation has failed.
   */
  function loadShader(gl, type, source) {
    // Create shader object
    var shader = gl.createShader(type);
    if (shader == null) {
      console.log('unable to create shader');
      return null;
    }
  
    // Set the shader program
    gl.shaderSource(shader, source);
  
    // Compile the shader
    gl.compileShader(shader);
  
    // Check the result of compilation
    var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if (!compiled) {
      var error = gl.getShaderInfoLog(shader);
      console.log('Failed to compile shader: ' + error);
      gl.deleteShader(shader);
      return null;
    }
  
    return shader;
  }
  
  /** 
   * Initialize and get the rendering for WebGL
   * @param canvas <cavnas> element
   * @param opt_debug flag to initialize the context for debugging
   * @return the rendering context for WebGL
   */
  function getWebGLContext(canvas, opt_debug) {
    // Get the rendering context for WebGL
    var gl = WebGLUtils.setupWebGL(canvas);
    if (!gl) return null;
  
    // if opt_debug is explicitly false, create the context for debugging
    if (arguments.length < 2 || opt_debug) {
      gl = WebGLDebugUtils.makeDebugContext(gl);
    }
  
    return gl;
  }
happy

相關文章