一起學 WebGL:圖形變形以及矩陣變換
之前繪制了三角形,我們現(xiàn)在給它做一個變形操作。
對一個三角形進行變形,其實就是重新這個三角形的三個頂點的位置,計算完后再繪制出來,相比原來就發(fā)生了變形。
變形常見的有位移、選擇、縮放。位移,其實就是給每個頂點的各個坐標值加上偏移量 dx、dy、dz。旋轉(zhuǎn)稍微復雜些,用到了三角函數(shù)。最后是縮放,就是簡單地各個分量乘以縮放比例系數(shù)。
這些變換可以抽象簡化成對應(yīng)的變換矩陣,方便我們用統(tǒng)一的方式作表達,并配合矩陣乘法的結(jié)合律,將多個變形矩陣合并成一個復合矩陣,減少計算量。
直接進入正題,看看怎么用 WebGL 實現(xiàn)矩陣變換。
繪制三角形
我們先繪制一個普通的沒做過變形的三角形。
demo 地址:
https://codesandbox.io/s/gbh1xf。
代碼:
/** @type {HTMLCanvasElement} */
const canvas = document.querySelector("canvas");
const gl = canvas.getContext("webgl");
const vertexShaderSrc = `
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
gl_PointSize = 10.0;
}
`;
const fragmentShaderSrc = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
/**** 渲染器生成處理 ****/
// 創(chuàng)建頂點渲染器
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSrc);
gl.compileShader(vertexShader);
// 創(chuàng)建片元渲染器
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSrc);
gl.compileShader(fragmentShader);
// 程序?qū)ο?const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
gl.program = program;
// 頂點數(shù)據(jù)
const vertices = new Float32Array([
// 第一個點
0,
0.5,
// 第二個點
-0.5,
-0.5,
// 第三個點
0.5,
-0.5
]);
// 創(chuàng)建緩存對象
const vertexBuffer = gl.createBuffer();
// 綁定緩存對象到上下文
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 向緩存區(qū)寫入數(shù)據(jù)
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 獲取 a_Position 變量地址
const a_Position = gl.getAttribLocation(gl.program, "a_Position");
// 將緩沖區(qū)對象分配給 a_Position 變量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// 允許訪問緩存區(qū)
gl.enableVertexAttribArray(a_Position);
/*** 繪制 ***/
// 清空畫布,并指定顏色
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// 繪制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);
渲染效果:
位移
位移,最簡單的方式是再聲明一個 u_Translation 向量,和 a_Position 相加就完事了:
但還是矩陣比較方便,具有可以統(tǒng)一格式,計算復合矩陣等優(yōu)勢。通常變形都是復雜的,旋轉(zhuǎn)后平移然后再縮放一套下來,矩陣還是很重要的。
頂點著色器的代碼修改為:
const vertexShaderSrc = `
attribute vec4 a_Position;
uniform mat4 u_xformMatrix;
void main() {
gl_Position = u_xformMatrix * a_Position;
}
`;
西瓜哥在這里加多了一個 u_xformMatrix 變量。
首先用了 uniform 類型修飾符,表示這個變量不會逐頂點發(fā)生變化,是固定的。mat4 表示一個 4x4 矩陣,一個有 16 個浮點數(shù)的一維數(shù)組。
來看看通過矩陣進行位移的實現(xiàn)。位移矩陣如下:
對應(yīng)的 Float32Array 數(shù)組為:
/****** 位移矩陣 ****/
const dx = 0.5; // 向右移動
const dy = -0.3; // 向下移動
// z 先不管,沒用到透視矩陣,設(shè)置值也看不到效果
const xformMatrix = new Float32Array([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
dx, dy, 0, 1
]);
WebGL 用的是按列主序(column major order)規(guī)則,即按列填充矩陣,從左往右,屬于主流。
還有一種是按行主序(row major order)的,也就是將遍歷數(shù)組一行行填充到矩陣,從上往下。比較少見。
接著把這個數(shù)組懟到前面頂點著色器聲明的 u_xformMatrix 變量中。
const u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix");
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);
這里是用 gl.uniformMatrix4fv 來設(shè)置 4x4 矩陣的值。
完整代碼:
/** @type {HTMLCanvasElement} */
const canvas = document.querySelector("canvas");
const gl = canvas.getContext("webgl");
const vertexShaderSrc = `
attribute vec4 a_Position;
uniform mat4 u_xformMatrix;
void main() {
gl_Position = u_xformMatrix * a_Position;
}
`;
const fragmentShaderSrc = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
/**** 渲染器生成處理 ****/
// 創(chuàng)建頂點渲染器
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSrc);
gl.compileShader(vertexShader);
// 創(chuàng)建片元渲染器
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSrc);
gl.compileShader(fragmentShader);
// 程序?qū)ο?const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
gl.program = program;
// 頂點數(shù)據(jù)
const vertices = new Float32Array([
// 第一個點
0,
0.5,
// 第二個點
-0.5,
-0.5,
// 第三個點
0.5,
-0.5
]);
// 創(chuàng)建緩存對象
const vertexBuffer = gl.createBuffer();
// 綁定緩存對象到上下文
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 向緩存區(qū)寫入數(shù)據(jù)
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 獲取 a_Position 變量地址
const a_Position = gl.getAttribLocation(gl.program, "a_Position");
// 將緩沖區(qū)對象分配給 a_Position 變量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// 允許訪問緩存區(qū)
gl.enableVertexAttribArray(a_Position);
/****** 位移矩陣 ****/
const dx = 0.5; // 向右移動
const dy = -0.3; // 向下移動
// z 先不管,沒用到透視矩陣,設(shè)置值也看不到效果
const xformMatrix = new Float32Array([
1,
0,
0,
0,
0,
1,
0,
0,
0,
0,
1,
0,
dx,
dy,
0,
1
]);
const u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix");
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);
/*** 繪制 ***/
// 清空畫布,并指定顏色
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// 繪制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);
demo 地址:
https://codesandbox.io/s/09ujp5?file=/index.js。
看看效果:
成功向右并向下移動了一段距離。
旋轉(zhuǎn)
頂點著色器的代碼不用改,這次我們傳一個旋轉(zhuǎn)矩陣進去,就逆時針旋轉(zhuǎn) 90 度吧,沿著 z 軸作旋轉(zhuǎn)。
公式為:
這個公示是幾何數(shù)學推導出來的。
數(shù)組數(shù)據(jù)為:
/****** 旋轉(zhuǎn)矩陣 ****/
const angle = 90;
const radian = (angle * Math.PI) / 180;
const cos = Math.cos(radian);
const sin = Math.sin(radian);
const xformMatrix = new Float32Array([
cos, sin, 0, 0,
-sin, cos, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]);
因為很多 API 只支持弧度制,所以我們需要將角度轉(zhuǎn)弧度。
然后是旋轉(zhuǎn)方向,提供一個正數(shù),WebGL 是沿著逆時針旋轉(zhuǎn)的。順帶一提, Canvas 2D 是順時針旋轉(zhuǎn)的。
完整代碼:
/** @type {HTMLCanvasElement} */
const canvas = document.querySelector("canvas");
const gl = canvas.getContext("webgl");
const vertexShaderSrc = `
attribute vec4 a_Position;
uniform mat4 u_xformMatrix;
void main() {
gl_Position = u_xformMatrix * a_Position;
}
`;
const fragmentShaderSrc = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
/**** 渲染器生成處理 ****/
// 創(chuàng)建頂點渲染器
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSrc);
gl.compileShader(vertexShader);
// 創(chuàng)建片元渲染器
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSrc);
gl.compileShader(fragmentShader);
// 程序?qū)ο?const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
gl.program = program;
// 頂點數(shù)據(jù)
const vertices = new Float32Array([
// 第一個點
0,
0.5,
// 第二個點
-0.5,
-0.5,
// 第三個點
0.5,
-0.5
]);
// 創(chuàng)建緩存對象
const vertexBuffer = gl.createBuffer();
// 綁定緩存對象到上下文
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 向緩存區(qū)寫入數(shù)據(jù)
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 獲取 a_Position 變量地址
const a_Position = gl.getAttribLocation(gl.program, "a_Position");
// 將緩沖區(qū)對象分配給 a_Position 變量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// 允許訪問緩存區(qū)
gl.enableVertexAttribArray(a_Position);
/****** 旋轉(zhuǎn)矩陣 ****/
const angle = 90;
const radian = (angle * Math.PI) / 180;
const cos = Math.cos(radian);
const sin = Math.sin(radian);
const xformMatrix = new Float32Array([
cos,
sin,
0,
0,
-sin,
cos,
0,
0,
0,
0,
1,
0,
0,
0,
0,
1
]);
const u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix");
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);
/*** 繪制 ***/
// 清空畫布,并指定顏色
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// 繪制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);
demo 地址:
https://codesandbox.io/s/wx44l0?file=/index.js。
渲染效果:
縮放
縮放公式為:
數(shù)組為:
/****** 縮放矩陣 ****/
const sx = 2;
const sy = 2;
const sz = 1;
// sz 先不管,沒用到透視矩陣,設(shè)置了值也看不到效果
const xformMatrix = new Float32Array([
sx, 0, 0, 0,
0, sy, 0, 0,
0, 0, sz, 0,
0, 0, 0, 1
]);
完整代碼:
/** @type {HTMLCanvasElement} */
const canvas = document.querySelector("canvas");
const gl = canvas.getContext("webgl");
const vertexShaderSrc = `
attribute vec4 a_Position;
uniform mat4 u_xformMatrix;
void main() {
gl_Position = u_xformMatrix * a_Position;
}
`;
const fragmentShaderSrc = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
/**** 渲染器生成處理 ****/
// 創(chuàng)建頂點渲染器
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSrc);
gl.compileShader(vertexShader);
// 創(chuàng)建片元渲染器
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSrc);
gl.compileShader(fragmentShader);
// 程序?qū)ο?const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
gl.program = program;
// 頂點數(shù)據(jù)
const vertices = new Float32Array([
// 第一個點
0,
0.5,
// 第二個點
-0.5,
-0.5,
// 第三個點
0.5,
-0.5
]);
// 創(chuàng)建緩存對象
const vertexBuffer = gl.createBuffer();
// 綁定緩存對象到上下文
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 向緩存區(qū)寫入數(shù)據(jù)
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 獲取 a_Position 變量地址
const a_Position = gl.getAttribLocation(gl.program, "a_Position");
// 將緩沖區(qū)對象分配給 a_Position 變量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// 允許訪問緩存區(qū)
gl.enableVertexAttribArray(a_Position);
/****** 縮放矩陣 ****/
const sx = 2;
const sy = 2;
const sz = 2;
// z 先不管,沒用到透視矩陣,設(shè)置了值也看不到效果
const xformMatrix = new Float32Array([
sx,
0,
0,
0,
0,
sy,
0,
0,
0,
0,
sz,
0,
0,
0,
0,
1
]);
const u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix");
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);
/*** 繪制 ***/
// 清空畫布,并指定顏色
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// 繪制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);
demo 地址:
https://codesandbox.io/s/jsfdtr?file=/index.js。
繪制效果:
結(jié)尾
矩陣變換是 WebGL 非常重要的一部分。
本節(jié)介紹了三種常見的變形矩陣,并展示了各自的效果,下節(jié)我們講多個矩陣的組合,復合矩陣。