構成三維模型的基本圖形是三角形,所以接下來就從如何繪制一個三角形開始,之后涉及到圖形的變換和動畫。
1. 圖形繪制
先回顧以下繪制單個點的方式:通過gl.getAttribLocati on()
獲得了GLSL
中的Vertex著色器的屬性值,并利用gl.vertexArrib[1234]f()
方法簇給著色器屬性賦值,并將值傳遞給GLSL
的內置變量gl_Position
,之后調用gl.drawArrays(gl.POINT, 0, 1)
的方法繪制點。
如果要繪制多個點怎么辦?當然,我們可以設置多次調用gl.vertexAttrib[1234]f
和gl.drawArrays(gl.POINT, 0, 1)
的方式來實現繪制多個點,例如:
html:
<canvas id="glCanvas" width="640" height="480"></canvas>
Javascript:
const canvas = document.querySelector('#glCanvas');
const gl = canvas.getContext('webgl');
// 著色器程序
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute float a_PointSize;
void main() {
gl_Position = a_Position;
gl_PointSize = a_PointSize;
}
`;
const FSHADER_SOURCE = `
precision mediump float;
uniform vec4 u_FragColor;
void main() {
gl_FragColor = u_FragColor;
}
`
let program = init(gl, VSHADER_SOURCE, FSHADER_SOURCE);
// 獲取頂點位置的屬性
let a_Position = gl.getAttribLocation(program, 'a_Position');
if (a_Position < 0) {
console.log('Cant find the position');
return;
}
// 設置頂點點的尺寸
let a_PointSize = gl.getAttribLocation(program, 'a_PointSize');
if (a_PointSize < 0) {
console.log('Cant find the pointsize');
return;
}
gl.vertexAttrib1f(a_PointSize, 10.0);
// 設置頂點顏色
let u_FragColor = gl.getUniformLocation(program, 'u_FragColor');
gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 設置并繪制多個頂點
drawPoint();
// 繪制多個點的方法
function drawPoint(gl, a_Position) {
gl.vertexAttrib3f(a_Position, -0.5, -0.5, 0.0);
gl.drawArrays(gl.POINT, 0, 1);
gl.vertexAttrib3f(a_Position, 0.0, 0.5, 0.0);
gl.drawArrays(gl.POINT, 0, 1);
gl.vertexAttrib3f(a_Position, 0.5, -0.5, 0.0);
gl.drawArrays(gl.POINT, 0, 1);
}
這樣反復調用繪制的方案明顯效率低下(反復重繪整個畫布),另外如果我們想繪制其他圖形,建立點之間點連接關系,似乎就沒有辦法了(因為每次繪制的點都是獨立的)。為了解決著兩個問題,就需要用到WebGL提供的緩沖區來一次性存儲多種信息(不僅僅存儲頂點坐標,還可以一次性存入頂點顏色等信息,后面再討論)
1.1 利用緩沖區繪制多個點
WebGL中要使用緩沖區,主要有以下五個步驟:
- 利用
gl.createBuffer()
創建緩沖區對象- 利用
gl.bindBuffer()
將創建的緩沖區對象和WebGL中的內置對象gl.ARRAY_BUFFER
進行綁定- 利用
gl.bufferData()
給gl.ARRAY_BUFFER
內置對象傳遞數據(數據不能直接傳遞給緩沖區對象,要通過gl.ARRAY_BUFFER
來進行)- 利用
gl.vertexAttribPointer()
將緩沖區數據分配給GLSL
中的變量- 最后利用
gl.enableVertexAttribArray()
開啟緩沖區對變量的使用,開啟后gl.vertexAttrib[1234]f()
方法簇的賦值將失效
完成上述操作后,調用gl.drawArrays()
來就可以一次繪制緩沖區數據了,這時只要將第三個參數變為所需要繪制點的數目就可以了。根據前一個例子,這里將drawPoint()
進行調整為使用緩沖區
function initPoint(gl, a_Position) {
let pointData = new Float32Array([
-0.5, -0.5,
0, 0.5,
0.5, -0.5
]);
// 創建緩沖區對象
let buffer = gl.createBuffer();
// 綁定緩沖區對象
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// 將數據傳遞到緩沖區
gl.bufferData(gl.ARRAY_BUFFER, pointData, gl.STATIC_DRAW);
// 將緩沖區數據傳遞給頂點著色器attribute屬性
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// 激活頂點和緩沖區到連接
gl.enableVertexAttribArray(a_Position);
// 一次性繪制多個點
gl.drawArrays(gl.POINT, 0, 3);
}
PS:代碼中使用了Float32Array
來創建點的數據對象,它是Javascript
提供的一種類型化數組,目的是為了說明數組中的所有數據都是同一種數據類型的特殊數組,使處理數組效率更快(可能不用做類型判斷和轉化了),其中類型化數組提供了很多方法和屬性,尤其BYTES_PER_ELEMENT
屬性在之后會有很大用處的。
1.2 利用mode控制圖形繪制
在使用緩沖區的基礎上,繪制圖形就很簡單了,只需要改變gl.drawArrays()
中的第一個參數就可以了
WebGL的第一個參數mode,提供了7種值:
gl.POINT
: 繪制點gl.LINES
: 繪制線段gl.LINE_STRIP
:繪制連續線段,例如傳入[A0, A1, A2, A3]四個坐標信息,那么繪制結果為[A0, A1], [A1, A2], [A2, A3]gl.LINE_LOOP
:首位兩個點會連接起來gl.TRIANGLES
:繪制三角形gl.TRIANGLE_STRIP
:繪制一系列三角形,例如傳入[A0, A1, A2, A3, A4, A5]五個坐標信息,那么繪制結果為[A0, A1, A2], [A2, A1, A3], [A2, A3, A4], [A4, A3, A5](webGL中繪制是按照逆時針方式進行繪制的)gl.TRIANGLE_FAN
:以第一個點為所有三角形頂點,繪制三角扇
2. 圖形變換
我們經常會將繪制的圖形進行平移,旋轉,縮放的操作,這些操作統一被稱為仿射變化。
2.1 基本變換
2.1.1 平移
平移要實現的其實是方向坐標上的位移:
x' = x + ux;
y' = y + uy;
因為GLSL中矢量可以直接進行加減運算,所以修改頂點著色器程序:
// 著色器程序
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute float a_PointSize;
void main() {
// 矢量可以直接進行運算
gl_Position = a_Position + vec4(0.1, 0.1, 0.0, 0.0);
gl_PointSize = a_PointSize;
}
`;
如果想要自定義平移距離,那么可以在頂點著色器程序中新增一個uniform
變量(使用uniform
是因為變量本身與頂點無關)
// 著色器程序
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute float a_PointSize;
unifrom vec4 u_Translate;
void main() {
// 矢量可以直接進行運算
gl_Position = a_Position + u_Translate;
gl_PointSize = a_PointSize;
}
`;
// 設置平移距離
let u_Translate = gl.getUniformLocation(program, 'u_Translate');
gl.uniform4f(u_Translate, 0.1, 0.1, 0.0, 0.0);
PS:要注意因為是使用齊次坐標,所以最后一個變量值要傳遞為0.0(因為默認齊次坐標的第四個變量為1.0,矢量計算后最后一個變量仍然應該為1.0)
2.1.2 縮放
縮放要實現的是方向坐標上的比例變化
x' = ux;
y' = uy;
實現縮放的關鍵是要獲取坐標在某些方向上的分量,可以通過a_Position.x
和a_Position.y
來分別獲取x
和y
方向上的分量,修改著色器程序:
// 著色器程序
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute float a_PointSize;
void main() {
// 分別設置四個方向上的分量信息
gl_Position.x = a_Position.x * 0.5;
gl_Position.y = a_Position.y * 0.5;
gl_Position.z = a_Position.z;
gl_Position.w = 1.0;
gl_PointSize = a_PointSize;
}
`;
2.1.3 旋轉
旋轉相比起來就復雜的多了,旋轉必須指明三個要素,旋轉軸,旋轉方向和旋轉角度。
WebGL中旋轉正方向是逆時針方向,遵循右手旋轉法則,也就是大拇指朝向軸的正方向,四指方向就是旋轉正方向。
以僅繞z軸旋轉為例,那么如果坐標系中的點的原角度為a(與x軸正方向角度),轉動角度為b,我們可以利用三角函數得到
x = r*cos(a);
y = r*sin(a);
// 利用三角函數和角公式進行變化
x' = r*cos(a+b) = r*cos(a)*cos(b) - r*sin(a)*sin(b) = x*cos(b) - y* sin(b);
y' = r*sin(a+b) = r*cos(a)*sin(b) + r*sin(a)*cos(b) = x*sin(b) + y*cos(b)
于是同樣通過獲取分量的方式,將頂點著色器程序進行修改
// 著色器程序
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute float a_PointSize;
uniform float u_Cosb, u_Sinb;
void main() {
// 分別設置四個方向上的分量信息
gl_Position.x = a_Position.x * u_Cosb - a_Position.y * u_Sinb
gl_Position.y = a_Position.x * * u_Sinb + a_Position.y * u_Cosb;
gl_Position.z = a_Position.z;
gl_Position.w = 1.0;
gl_PointSize = a_PointSize;
}
`;
// 轉動30度
let rad = 90 / 180 * Math.PI;
let u_Sinb = gl.getUniformLocation(program, 'u_Sinb');
gl.uniform1f(u_Sinb, Math.sin(rad));
let u_Cosb = gl.getUniformLocation(program, 'u_Cosb');
gl.uniform1f(u_Cosb, Math.cos(rad));
2.2 矩陣變換
所有的變換其實都是平移,旋轉,縮放的疊加,但是按照之前的方式來進行那么在變換疊加的時候,計算過程就變得麻煩了,例如,我們要先旋轉后平移再縮放。。。由于WebGL支持矩陣運算,所以變換可以使用矩陣變化來處理。
具體矩陣運算可以參見矩陣(其實也就是將方程轉換為了矩陣的方式)
在WebGL中,要使用變換矩陣主要要進行四步驟:
- 頂點著色器程序中增加矩陣變量
uniform mat4 u_Matrix
- 頂點著色器程序中修改
gl_Position = u_Matrix * a_Position
(注意矩陣計算時的順序)- 創建矩陣變換的數組
new Float32Array()
- 使用
gl.unifromMatrix[1234]fv
的方法設置矩陣變換uniform變量的值
以旋轉變化為例,那么:
// 著色器程序
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute float a_PointSize;
uniform mat4 u_Matrix;
void main() {
gl_Position = u_Matrix * a_Position;
gl_PointSize = a_PointSize;
}
`;
let rad = 90 / 180 * Math.PI;
let u_Matrix = gl.getUniformLocation(program, 'u_Matrix');
let matrix = new Float32Array([
Math.cos(rad), Math.sin(rad), 0.0, 0.0,
-Math.sin(rad), Math.cos(rad), 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
])
gl.uniformMatrix4fv(u_Matrix, false, matrix);
PS:特別注意,數組中存儲二維數組有兩種順序,按列主序和按行主序,WebGL和OpenGL中都是按列主序,所以,注意數組中的數據和真是矩陣中數據位置存在轉置(gl.uniformMatrix4fv
的第二個參數可以實現矩陣轉置,但是WebGL中并沒有實現轉置操作所以始終默認為false
)
3. 動畫
3.1 利用requestAnimationFrame進行動畫
動畫基本上就是基于圖形的各種變換的持續執行過程,于是在webGL中要實現動畫,實際上就是在圖形變換的基礎上,調用了動畫函數進行循環調用,不斷重新繪制圖形。可以使用setInterval
函數也可以使用requestAnimationFrame
來實現動畫的循環調用,不過建議使用后者,因為后者只有在瀏覽器tab頁激活的時候才會執行,而前者會一直執行。還是以旋轉為例,下面是持續旋轉的例子:
// 著色器程序
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute float a_PointSize;
uniform mat4 u_Matrix;
void main() {
gl_Position = u_Matrix * a_Position;
gl_PointSize = a_PointSize;
}
`;
let start = Date.now();
let rad = 0;
animation();
// 旋轉動畫
function animation() {
let now = Date.now();
let offsetTime = now - start;
start = now;
// 假設每秒鐘轉動30度
rad += offsetTime * 30 / 360 / 1000 * Math.PI;
let u_Matrix = gl.getUniformLocation(program, 'u_Matrix');
let matrix = new Float32Array([
Math.cos(rad), Math.sin(rad), 0.0, 0.0,
-Math.sin(rad), Math.cos(rad), 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
])
gl.uniformMatrix4fv(u_Matrix, false, matrix);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.LINE_LOOP, 0, 3);
requestAnimationFrame(animation);
}
}
- 總結
一開始我總覺得自己在旋轉的過程中圖形形狀發生了變化,很奇怪,最后花了很多事件才發現原來demo中的canvas不是正方形。。。所以在進行旋轉操作的時候,x軸和y軸并不是相同的單位比例。 - 參考
《WebGL編程指南》