面向web前端的WebGL教程,網路上的教程均是假設有計算機圖形學基礎,對web開發者來說不是很友好, 故開闢此坑
最終效果
https://codepen.io/chendonmin...
滑鼠點選 畫一個點。
webGL如何展示一個點
首先得知道webGL如何展示出一個點?
webGL畫任意物體 都需要一個頂點著色器
和片元著色器
,
頂點著色器:描述頂點的特性(位置、顏色等)的程式.
片元著色器: 進行著片元處理過程的程式。
也許你會很懵,一大堆官方理論又要望而卻步了,所以我直接展示下最簡單的展示一個點的程式碼,相信你會馬上明白。
<canvas id="glcanvas" width="640" height="480">
你的瀏覽器似乎不支援或者禁用了HTML5 <code><canvas></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_SOURCE
和FSHADER_SOURCE
兩個字串,分別表示了點的座標和點的顏色。
VSHADER_SOURCE
和FSHADER_SOURCE
是屬於glsl
程式碼,
VSHADER_SOURCE
中的gl_Position
代表的就是點的位置,gl_Position
是glsl
的內建變數。
你會發現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 127 | 1 | 8 位二進位制有符號整數 | byte | int8_t |
Uint8Array | 0 to 255 | 1 | 8 位無符號整數(超出範圍後從另一邊界迴圈) | octet | uint8_t |
Uint8ClampedArray | 0 to 255 | 1 | 8 位無符號整數(超出範圍後為邊界值) | octet | uint8_t |
Int16Array | -32768 to 32767 | 2 | 16 位二進位制有符號整數 | short | int16_t |
Uint16Array | 0 to 65535 | 2 | 16 位無符號整數 | unsigned short | uint16_t |
Int32Array | -2147483648 to 2147483647 | 4 | 32 位二進位制有符號整數 | long | int32_t |
Uint32Array | 0 to 4294967295 | 4 | 32 位無符號整數 | unsigned long | uint32_t |
Float32Array | 1.2 ×10^-38 to 3.4 ×10^38 | 4 | 32 位 IEEE 浮點數(7 位有效數字,如 1.1234567 ) | unrestricted float | float |
Float64Array | 5.0 ×10^-324 to 1.8 ×10^308 | 8 | 64 位 IEEE 浮點數(16 有效數字,如 1.123...15 ) | unrestricted double | double |
BigInt64Array | -2^63 to 2^63-1 | 8 | 64 位二進位制有符號整數 | bigint | int64_t (signed long long) |
BigUint64Array | 0 to 2^64 - 1 | 8 | 64 位無符號整數 | bigint | uint64_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><canvas></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