WebGL編程指南筆記二 第三章第四章 繪制和變換

參考
【《WebGL編程指南》讀書筆記-繪制和變換三角形】

關(guān)鍵詞:

  • 頂點(diǎn)緩沖區(qū)
一、一次繪制多個(gè)點(diǎn)

在第二章的ClickedPoints例子中,只繪制了一個(gè)點(diǎn)。對那些由多個(gè)頂點(diǎn)組成的圖形,比如三角形、矩形和立方體來說,你需要一次性地將圖形的頂點(diǎn)全部傳入頂點(diǎn)著色器,然后才能把圖形畫出來。

WebGL提供了一種很方便的機(jī)制,即緩沖區(qū)對象(buffer object),它可以一次性地向著色器傳入多個(gè)頂點(diǎn)的數(shù)據(jù)。緩沖區(qū)對象是WebGL系統(tǒng)中的一塊內(nèi)存區(qū)域,我們可以一次性地向緩沖區(qū)對象中填充大量的頂點(diǎn)數(shù)據(jù),然后將這些數(shù)據(jù)保存在其中,供頂點(diǎn)著色器使用。

//MultiPoint.js
function main() {
  // Retrieve <canvas> element
  var canvas = document.getElementById('webgl');

  // Get the rendering context for WebGL
  var gl = getWebGLContext(canvas);
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL');
    return;
  }

  // Initialize shaders
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to intialize shaders.');
    return;
  }

  // Write the positions of vertices to a vertex shader
  var n = initVertexBuffers(gl);
  if (n < 0) {
    console.log('Failed to set the positions of the vertices');
    return;
  }

  // Specify the color for clearing <canvas>
  gl.clearColor(0, 0, 0, 1);

  // Clear <canvas>
  gl.clear(gl.COLOR_BUFFER_BIT);

  // Draw three points
  gl.drawArrays(gl.POINTS, 0, n);
}

initVertexBuffers創(chuàng)建了頂點(diǎn)緩沖區(qū)對象,并將多個(gè)頂點(diǎn)的數(shù)據(jù)保存在緩沖區(qū)中,然后將緩沖區(qū)傳給頂點(diǎn)著色器。函數(shù)返回值是待繪制頂點(diǎn)的數(shù)量,保存在變量n當(dāng)中。使用drawArrays函數(shù),傳入的第三個(gè)參數(shù)是n。WebGL系統(tǒng)并不知道緩沖區(qū)中有多少個(gè)頂點(diǎn)的數(shù)據(jù),即使它知道也不能確定是否要全部畫出,所以我們要顯式地告訴它要繪制多少個(gè)頂點(diǎn)。

function initVertexBuffers(gl) {
  var vertices = new Float32Array([
    0.0, 0.5,   -0.5, -0.5,   0.5, -0.5
  ]);
  var n = 3; // The number of vertices

  // Create a buffer object
  var vertexBuffer = gl.createBuffer();
  if (!vertexBuffer) {
    console.log('Failed to create the buffer object');
    return -1;
  }

  // Bind the buffer object to target
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  // Write date into the buffer object
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

  var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position');
    return -1;
  }
  // Assign the buffer object to a_Position variable
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);

  // Enable the assignment to a_Position variable
  gl.enableVertexAttribArray(a_Position);

  return n;
}

這里向緩沖區(qū)對象寫入的頂點(diǎn)坐標(biāo)是一種特殊的JS類型化數(shù)組(Float32Array),創(chuàng)建的唯一方法是使用new運(yùn)算符,不能使用[]運(yùn)算符(那樣創(chuàng)建的就是普通數(shù)組)。關(guān)于JS的Float32Array可以參考jsmpeg系列一 基礎(chǔ)知識 字符處理 ArrayBuffer TypedArray

1.緩沖區(qū)

緩沖區(qū)對象是WebGL系統(tǒng)中的一塊存儲區(qū),我們可以在緩沖區(qū)對象中保存想要繪制的所有頂點(diǎn)的數(shù)據(jù)。如下圖所示:

image.png

使用緩沖區(qū)對象向頂點(diǎn)著色器傳入多個(gè)頂點(diǎn)的數(shù)據(jù),需要遵循以下五個(gè)步驟。處理其他對象,如紋理對象(第4章)、幀緩沖區(qū)對象(第8章 光照)時(shí)的步驟也比較類似:

  • 創(chuàng)建緩沖區(qū)對象 gl.createBuffer
  • 綁定緩沖區(qū)對象 gl.bindBuffer
  • 將數(shù)據(jù)寫入緩沖區(qū)對象 gl.bufferData
  • 將緩沖區(qū)對象分配給一個(gè)attribute變量 gl.vertexAttribPointer
  • 開啟attribute變量 gl.enableVertexAttribArray
2.var vertexBuffer = gl.createBuffer();

相應(yīng)的,也有g(shù)l.deleteBuffer(buffer)用來刪除創(chuàng)建的緩沖區(qū)對象。

創(chuàng)建緩沖區(qū)前后WebGL系統(tǒng)的變化如下圖所示:執(zhí)行后WebGL系統(tǒng)中多出了“緩沖區(qū)對象”


image.png
3. gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)

創(chuàng)建緩沖區(qū)對象后,需要將其綁定到WebGL系統(tǒng)中已經(jīng)存在的“目標(biāo)”(target)上。“目標(biāo)”表示緩沖區(qū)對象的用途(示例中為向頂點(diǎn)著色器提供傳給attribute變量的數(shù)據(jù)),這樣WebGL才能夠正確處理其中內(nèi)容。

綁定緩沖區(qū)對象采用的函數(shù)為gl.bindBuffer(),函數(shù)規(guī)范如下:
gl.bindBuffer(target, buffer):允許使用buffer表示的緩沖區(qū)對象并將其綁定到target表示的目標(biāo)上。

參數(shù):

  • target參數(shù)可以是以下中的一個(gè):
    • gl.ARRAY_BUFFER 表示緩沖區(qū)對象中包含了頂點(diǎn)的數(shù)據(jù)
    • gl.ELEMENT_ARRAY_BUFFER 表示緩沖區(qū)對象中包含了頂點(diǎn)的索引值(參見第6章著色器語言GLSL ES)
  • buffer:待刪除的緩沖區(qū)對象
  • 返回值: 無
  • 錯(cuò)誤:
    INVALID_ENUM target不是上述值之一,這時(shí)將保持原有的綁定情況不變

示例程序中,我們將創(chuàng)建的緩沖區(qū)對象綁定到gl.ARRAY_BUFFER目標(biāo)上,代碼執(zhí)行完畢后,WebGL系統(tǒng)內(nèi)如下圖所示:


image.png
3.gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

這一步將vertices中的數(shù)據(jù)寫入到綁定在gl.ARRAY_BUFFER目標(biāo)的緩沖區(qū)對象。此處不能直接向緩沖區(qū)寫入數(shù)據(jù),而是依據(jù)target寫入數(shù)據(jù),所以在此之前target下需要有綁定的緩沖區(qū)對象。

gl.bufferData()的函數(shù)規(guī)范如下:

  • gl.bufferData(target, data, usage):開辟存儲空間,向綁定在target上的緩沖區(qū)對象中寫入數(shù)據(jù)。
    參數(shù):

  • target:gl.ARRAY_BUFFER 或 gl.ELEMENT_ARRAY_BUFFER

  • data:寫入緩沖區(qū)對象的數(shù)據(jù)(類型化數(shù)組)

  • usage:表示程序?qū)⑷绾问褂么鎯υ诰彌_區(qū)對象中的數(shù)據(jù)。該參數(shù)將幫助WebGL優(yōu)化操作,但是就算你傳入了錯(cuò)誤的值,也不會終止程序(僅僅是降低程序的效率)

    • gl.STATIC_DRAW 只會向緩沖區(qū)對象中寫入一次數(shù)據(jù),但需要繪制很多次(many times)
    • gl.STREAM_DRAW 只會向緩沖區(qū)對象中寫入一次數(shù)據(jù),然后繪制若干次(at most a few times)
    • gl.DYNAMIC_DRAW 會向緩沖區(qū)對象中多次寫入數(shù)據(jù),并繪制很多次(many times)
  • 返回值: 無

  • 錯(cuò)誤:
    INVALID_ENUM target不是上述值之一,這時(shí)將保持原有的綁定情況不變

寫入數(shù)據(jù)后,WebGL系統(tǒng)如下:


image.png
4.gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);

緩沖區(qū)對象準(zhǔn)備好之后,需要獲取attribute變量地址,再向attribute變量傳遞參數(shù)。第二章中使用了gl.vertexAttrib[1234]f[v]系列函數(shù)來傳遞數(shù)據(jù),但此方法一次只能傳遞一個(gè)值,此時(shí)需要一次傳遞多個(gè)值,示例中采用gl.vertexAttribPointer()方法。
該函數(shù)的規(guī)范如下:

gl.vertexAttribPointer(location, size, type, normalized, stride, offset):
將綁定到gl.ARRAY_BUFFER的緩沖區(qū)對象(實(shí)際上是其引用或指針)分配給由location指定的attribute變量。
參數(shù):

  • location:指定待分配attribute變量的存儲位置
  • size:指定緩沖區(qū)中每個(gè)頂點(diǎn)的分量個(gè)數(shù)(1到4)。若size比attribute變量需要的分量數(shù)小,缺失分量將按照與gl.vertexAttrib[1234]f()相同的規(guī)則補(bǔ)全。比如,如果size為1,那么第2、3分量自動設(shè)為0,第4分量為1。
  • type:用以下類型之一來指定數(shù)據(jù)格式
    • gl.UNSIGNED_BYTE 無符號字節(jié),Uint8Array
    • gl.SHORT 短整型,Int16Array
    • gl.INT 整型,Int32Array
    • gl.UNSIGNED_INT 無符號整型,UInt32Array
    • gl.FLOAT 浮點(diǎn)型,F(xiàn)loat32Array
  • normalize:傳入true或false,表明是否將非浮點(diǎn)型的數(shù)據(jù)歸一化到[0,1]或[-1,1]區(qū)間(正則化)
  • stride:指定相鄰兩個(gè)頂點(diǎn)間的字節(jié)數(shù),默認(rèn)為0(參見第5章)
  • offset:指定緩沖區(qū)對象中的偏移量(以字節(jié)為單位),即attribute變量從緩沖區(qū)中的何處開始存儲。如果是從起始位置開始的,offset設(shè)為0。
  • 返回值: 無
  • 錯(cuò)誤:
  • INVALID_OPERATION 不存在當(dāng)前程序?qū)ο?/li>
  • INVALID_VALUE location大于等于attribute變量的最大數(shù)目(默認(rèn)為8)?;蛘遱tride或offset是負(fù)值。

執(zhí)行完畢后,gl.ARRAY_BUFFER緩沖區(qū)對象被分配給attribute變量,此時(shí)緩沖區(qū)對象還不可用,如下圖所示:


image.png
5.gl.enableVertexAttribArray(a_Position);

雖然前一步已經(jīng)將attribute變量指向了緩沖區(qū)對象,但此時(shí)著色器還不能訪問緩沖區(qū)內(nèi)的數(shù)據(jù),需要使用先開啟attribute變量。

相關(guān)函數(shù)的函數(shù)規(guī)范如下:

gl.enableVertexAttrirbArray(location):
開啟location指定的attribute變量(實(shí)際處理對象是緩沖區(qū))。
參數(shù):

  • location:指定attribute變量的存儲位置
  • 返回值: 無
  • 錯(cuò)誤:
    • INVALID_VALUE location大于等于attribute變量的最大數(shù)目(默認(rèn)為8)。

開啟attribute變量之后,緩沖區(qū)對象和attribute變量之間的連接就真正建立起來了,如下圖所示:


image.png

開啟attribute變量之后,我們就不能通過gl.vertexAttrib[1234]f()來向該變量傳遞數(shù)據(jù)了,實(shí)際上也不應(yīng)該同時(shí)通過兩種方式傳遞數(shù)據(jù)。
于是,我們也可以通過gl.disableVertexAttribArray()來關(guān)閉分配,函數(shù)規(guī)范如下:

gl.enableVertexAttrirbArray(location):
關(guān)閉location指定的attribute變量。
參數(shù):

  • location:指定attribute變量的存儲位置
  • 返回值: 無
  • 錯(cuò)誤:
    • INVALID_VALUE location大于等于attribute變量的最大數(shù)目(默認(rèn)為8)。
6.開始繪制

通過上面的函數(shù),我們已經(jīng)配置好了緩沖區(qū)和著色器,可以開始繪制:

  // 設(shè)置背景色
  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  // 清空canvas
  gl.clear(gl.COLOR_BUFFER_BIT)
  // 繪制
  gl.drawArrays(gl.POINTS, 0, n)

繪制同樣采用gl.drawArrays()方法,該方法函數(shù)規(guī)范在第二章已經(jīng)給出,此處給出函數(shù)語法作為提示:

gl.drawArrays(mode, first, count)

示例中n=3,count為3,所以頂點(diǎn)著色器實(shí)際執(zhí)行了3次。

在建立attribute變量和緩沖區(qū)聯(lián)系時(shí),函數(shù)gl.vertexAttribPointer()中的參數(shù)size為2,表示緩沖區(qū)每個(gè)頂點(diǎn)有2個(gè)分量值。所以每次著色器運(yùn)行前,gl_Position都被提供了兩個(gè)分量(通過attribute變量a_Position),其他值按照規(guī)則填充為0.0和1.0。

繪制過程如下圖所示:


image.png
二、繪制三角形

與上面繪制多點(diǎn)的MultiPoint例子相比,只改了兩處:

// MultiPoint.js (c) 2012 matsuda
// Vertex shader program
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'void main() {\n' +
  '  gl_Position = a_Position;\n' +
  '  gl_PointSize = 10.0;\n' +
  '}\n';

// HelloTriangle.js (c) 2012 matsuda
// Vertex shader program
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'void main() {\n' +
  '  gl_Position = a_Position;\n' +
  '}\n';

第一處,將gl_PointSize=10.0刪去了,該語句只有在繪制單個(gè)點(diǎn)的時(shí)候才起作用。

  // Draw three points
  gl.drawArrays(gl.POINTS, 0, n);

  // Draw the rectangle
  gl.drawArrays(gl.TRIANGLES, 0, n);

第二處,drawArrays的第一個(gè)參數(shù)發(fā)生了變化,這個(gè)參數(shù)提供了7種mode:

  • gl.POINTS 一系列點(diǎn)
  • gl.LINES 線段
  • gl.LINE_STRIP 線條
  • gl.LINE_LOOP 回路
  • gl.TRIANGLES 三角形
  • gl.TRIANGLE_STRIP 三角帶
  • gl.TRIANGLE_FAN 三角扇
image.png

這7種基本圖形是WebGL繪制其他更加復(fù)雜的圖形的基礎(chǔ),正如本章開頭所說,從球體到立方體,再到游戲中的三維角色,都可以由小的三角形組成。

1.用三角形繪制矩形(HelloQuad)

既然任何東西都可以由基本圖形構(gòu)成,那么我們來繪制一個(gè)矩形,體會這種“構(gòu)成”的方式。

矩形可以由兩個(gè)三角形組成,繪制方式可以采用gl.TRAINGLES、gl.TRIANGLE_STRIP、gl.TRIANGLE_FAN三種方法,第一種方法需要用到6個(gè)頂點(diǎn),后兩種需要4個(gè)頂點(diǎn),每種方法的頂點(diǎn)順序都不相同。此處采用gl.TRIANGLE_STRIP方法進(jìn)行繪制,相比于上個(gè)示例,此處改動如下:


image.png
let vertices = new Float32Array([-0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5]) ;
let n = 4 // 點(diǎn)的個(gè)數(shù)
gl.drawArrays(gl.TRIANGLE_STRIP, 0, n)

此處如果把HelloQuad中g(shù)l.drawArrays()的mode參數(shù)改為gl.TRIANGLE_FAN,會出現(xiàn):


image.png
三、移動、旋轉(zhuǎn)和縮放

基礎(chǔ)概念部分,可以參考
圖形學(xué)筆記一 仿射變換和齊次坐標(biāo)
圖形學(xué)筆記二 正交矩陣、轉(zhuǎn)置矩陣和旋轉(zhuǎn)

相關(guān)內(nèi)容:1.表達(dá)式方式進(jìn)行仿射變換;2.變換矩陣進(jìn)行仿射變換;3.將矩陣傳遞給uniform變量;4.按列主序
相關(guān)函數(shù):gl.uniformMatrix4fv()

小結(jié):
本節(jié)通過表達(dá)式展示了仿射變換的坐標(biāo)轉(zhuǎn)換過程,主要為了引出變換矩陣。變換矩陣是常用的對圖形處理的方法,多種變換能夠糅合到一個(gè)矩陣中進(jìn)行操作,對于代碼編寫非常遍歷,當(dāng)然也需要一定數(shù)學(xué)基礎(chǔ)。許多語言對于矩陣運(yùn)算都有額外的設(shè)計(jì),這些設(shè)計(jì)大大提高了矩陣運(yùn)算的效率,WebGL中也支持矩陣和矢量的運(yùn)算,這大大提高了三維圖像處理的效率。
本節(jié)用到的方法多為前面已經(jīng)講述過的內(nèi)容,包括著色器中attribute變量和uniform變量、緩沖區(qū)對象的使用。關(guān)于矩陣的運(yùn)算可以直接寫在著色器語言中,不需要額外函數(shù),構(gòu)建矩陣除了類型化數(shù)組之外,還需要注意WebGL(OpenGL)中按列主序來保存內(nèi)容。本節(jié)提到了一個(gè)新的函數(shù)來向unform傳遞矩陣數(shù)據(jù)。
從數(shù)據(jù)傳遞的過程可以看出,WebGL系統(tǒng)要求接收的數(shù)據(jù)一般需要嚴(yán)格定義類型,在傳輸?shù)臅r(shí)候還需要告訴該系統(tǒng)使用方式,很多函數(shù)都是如此設(shè)計(jì),以最新的函數(shù)gl.uniformMatrix4fv()為例,單看函數(shù)名,它傳遞的數(shù)據(jù)為float型數(shù)組,數(shù)組元素共4×4個(gè),它告訴WebGL以4×4矩陣的方式使用該數(shù)組并將其賦值給uniform變量,傳遞數(shù)據(jù)的類型(數(shù)組)和使用數(shù)據(jù)的類型(矩陣)并不一致。
這一節(jié)有所局限的地方在于,所有的變換都是單一類型僅僅一步的變換,之后多半會提及的復(fù)雜類型的變換可能還涉及變換矩陣之間的運(yùn)算,敬請期待。

現(xiàn)在,你已經(jīng)掌握了繪制圖形的方法。讓我們再進(jìn)一步,嘗試平移、旋轉(zhuǎn)和縮放三角形,這樣的操作稱為變換(transformations)或仿射變換(affine transformations)。

1.平移

我們只需要著色器為頂點(diǎn)坐標(biāo)的每個(gè)分量加上一個(gè)常量就可以實(shí)現(xiàn)平移,顯然這是一個(gè)逐頂點(diǎn)操作,發(fā)生在頂點(diǎn)著色器中。

// TranslatedTriangle.js (c) 2012 matsuda
// Vertex shader program
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'uniform vec4 u_Translation;\n' +
  'void main() {\n' +
  '  gl_Position = a_Position + u_Translation;\n' +
  '}\n';

// Fragment shader program
var FSHADER_SOURCE =
  'void main() {\n' +
  '  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' +
  '}\n';

// The translation distance for x, y, and z direction
var Tx = 0.5, Ty = 0.5, Tz = 0.0;

function main() {
...
  // Pass the translation distance to the vertex shader
  var u_Translation = gl.getUniformLocation(gl.program, 'u_Translation');
  if (!u_Translation) {
    console.log('Failed to get the storage location of u_Translation');
    return;
  }
  gl.uniform4f(u_Translation, Tx, Ty, Tz, 0.0);
...

在第二章中有提到:

  • attribute變量 傳輸?shù)氖悄切┡c頂點(diǎn)相關(guān)的數(shù)據(jù)
  • uniform變量 傳輸那些對所有頂點(diǎn)都相同(或與頂點(diǎn)無關(guān))的數(shù)據(jù)

所以這里使用了uniform變量u_Translation來表示三角形的平移距離。因?yàn)閍_Position和u_Translation都是vec4類型的,所以可以直接使用+號,兩個(gè)矢量的對應(yīng)分量會被同時(shí)相加。

下面解釋一下齊次坐標(biāo)的第4分量。在第2章中有提到,如果齊次坐標(biāo)第4分量是1.0,那么它的前三個(gè)分量就可以表示一個(gè)點(diǎn)的三維坐標(biāo)。在本例中,平移后點(diǎn)坐標(biāo)第4分量w1+w2必須是1.0,因?yàn)辄c(diǎn)的位置坐標(biāo)平移之后還是一個(gè)點(diǎn)位置坐標(biāo),而w1是1.0(它是平移前點(diǎn)坐標(biāo)第4分量),所以w2只能是0.0,這就是為什么gl.uniform4f()的最后一個(gè)參數(shù)為0.0

2.旋轉(zhuǎn)
image.png
x = r cos α
y = r sin α

x' = r cos(α+β)
y' = r sin(α+β)

利用三角函數(shù)公式可得

x' = x cos β - y sin β
y' = x sin β + y cos β

在RotatedTriangle.js中,實(shí)現(xiàn)了一個(gè)三角形繞Z軸逆時(shí)針旋轉(zhuǎn)90度的例子。

// RotatedTriangle.js (c) 2012 matsuda
// Vertex shader program
var VSHADER_SOURCE =
  // x' = x cosβ - y sinβ
  // y' = x sinβ + y cosβ Equation 3.3
  // z' = z
  'attribute vec4 a_Position;\n' +
  'uniform float u_CosB, u_SinB;\n' +
  'void main() {\n' +
  '  gl_Position.x = a_Position.x * u_CosB - a_Position.y * u_SinB;\n' +
  '  gl_Position.y = a_Position.x * u_SinB + a_Position.y * u_CosB;\n' +
  '  gl_Position.z = a_Position.z;\n' +
  '  gl_Position.w = 1.0;\n' +
  '}\n';

// Fragment shader program
var FSHADER_SOURCE =
  'void main() {\n' +
  '  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' +
  '}\n';

// The rotation angle
var ANGLE = 90.0; 

...
  // // Pass the data required to rotate the shape to the vertex shader
  var radian = Math.PI * ANGLE / 180.0; // Convert to radians
  var cosB = Math.cos(radian);
  var sinB = Math.sin(radian);

  var u_CosB = gl.getUniformLocation(gl.program, 'u_CosB');
  var u_SinB = gl.getUniformLocation(gl.program, 'u_SinB');
  if (!u_CosB || !u_SinB) {
    console.log('Failed to get the storage location of u_CosB or u_SinB');
    return;
  }
  gl.uniform1f(u_CosB, cosB);
  gl.uniform1f(u_SinB, sinB);
...

也可以將旋轉(zhuǎn)的角度傳入頂點(diǎn)著色器,并在著色器中計(jì)算正弦值和余弦值。但是,實(shí)際上所有頂點(diǎn)旋轉(zhuǎn)的角度是一樣的,在JS中算好正弦值和余弦值,再傳遞進(jìn)去,只需要計(jì)算一次,效率更高。

在進(jìn)行平移變換時(shí),齊次坐標(biāo)的4個(gè)分量是作為整體進(jìn)行加法運(yùn)算的。而旋轉(zhuǎn)操作,需要單獨(dú)訪問每個(gè)分量。

對于簡單的變換,可以用上述方式來實(shí)現(xiàn)。但是當(dāng)情形逐漸復(fù)雜時(shí),比如一個(gè)旋轉(zhuǎn)后再平移,就不能每次變換都用一個(gè)新的著色器,此時(shí)就需要變換矩陣。


image.png
x'=ax+by+cz
y'=dx+ey+fz
z'=gx+hy+iz

與上述公式對比

x' = x cosβ - y sinβ
y' = x sinβ + y cosβ
z' = z

可知讓a=cosβ,b=-sinβ,c=0即可。


image.png

完整矩陣如上,它將右側(cè)的矢量(x,y,z)變換成了左側(cè)的(x',y',z'),可以看作是一個(gè)旋轉(zhuǎn)矩陣。

x'=ax+by+cz
x'=x+Tx

對比上述變換,發(fā)現(xiàn)無法通過3*3的矩陣來表示平移,所以我們需要一個(gè)4*4的矩陣。平移矩陣推導(dǎo)方式類似:


image.png

下面先給出RotatedTriangle_Matrix.js的完整代碼:

// RotatedTriangle_Matrix.js
// 頂點(diǎn)著色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'uniform mat4 u_xformMatrix;\n' +
  'void main(){\n' +
  '  gl_Position = u_xformMatrix * a_Position;\n' +
  '}\n'
// 片元著色器
var FSHADER_SOURCE =
  'void main(){\n' + ' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + '}\n'
// 旋轉(zhuǎn)角度
var ANGLE = 90.0
// 主函數(shù)
function main() {
  // 獲取canvas元素
  let canvas = document.getElementById('webgl')
  // 獲取WebGL上下文
  let gl = getWebGLContext(canvas)
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL')
    return
  }
  // 初始化著色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to initialize shaders')
    return
  }
  // 設(shè)置頂點(diǎn)位置
  let n = initVertexBuffers(gl)
  if (n < 0) {
    console.log('Failed to set the positions of the vertices')
    return
  }

  // 創(chuàng)建旋轉(zhuǎn)矩陣
  let radian = (Math.PI * ANGLE) / 180.0 // 轉(zhuǎn)換為弧度制
  let cosB = Math.cos(radian)
  let sinB = Math.sin(radian)
  // 注意WebGL中矩陣是列主序的
  let xformMatrix = new Float32Array([
    cosB,    sinB,    0.0,    0.0,
    -sinB,    cosB,    0.0,    0.0,
    0.0,    0.0,    1.0,    0.0,
    0.0,    0.0,    0.0,    1.0,
  ])
  // 將旋轉(zhuǎn)圖形所需數(shù)據(jù)傳輸給頂點(diǎn)著色器
  let u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix')
  if (!u_xformMatrix) {
    console.log('Failed to get the storage location of u_xformMatrix')
  }
  gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix)

  // 設(shè)置背景色
  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  // 清空繪圖區(qū)
  gl.clear(gl.COLOR_BUFFER_BIT)
  // 繪制三角形
  gl.drawArrays(gl.TRIANGLES, 0, n)
}

function initVertexBuffers(gl) {
  // 設(shè)置類型化數(shù)組和頂點(diǎn)數(shù)
  let vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5])
  let n = 3
  // 創(chuàng)建緩沖區(qū)對象
  let vertexBuffer = gl.createBuffer()
  if (!vertexBuffer) {
    console.log('Failed to create the buffer object')
    return -1
  }
  // 綁定緩沖區(qū)
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
  // 緩沖區(qū)寫入數(shù)據(jù)
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STREAM_DRAW)

  let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position')
    return -1
  }
  // 將緩沖區(qū)分配給attribute變量
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
  // 開啟attribute變量(連接)
  gl.enableVertexAttribArray(a_Position)

  return n
}

以下為頂點(diǎn)著色器代碼:

// 頂點(diǎn)著色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'uniform mat4 u_xformMatrix;\n' +
  'void main(){\n' +
  '  gl_Position = u_xformMatrix * a_Position;\n' +
  '}\n'
  • mat4類型的變量是4×4的矩陣。
  • WebGL頂點(diǎn)著色器中的’*'操作符支持矩陣乘法。著色器內(nèi)置了常用的矢量和矩陣運(yùn)算功能,我們可以用基本的操作符,如示例中出現(xiàn)過的‘+’和‘*’直接完成矢量和矩陣的運(yùn)算,這種強(qiáng)大特性正是專為三維計(jì)算機(jī)圖形學(xué)而設(shè)計(jì)的。

概念參考行主序 列主序

  // 創(chuàng)建旋轉(zhuǎn)矩陣
  let radian = (Math.PI * ANGLE) / 180.0 // 轉(zhuǎn)換為弧度制
  let cosB = Math.cos(radian)
  let sinB = Math.sin(radian)
  // 注意WebGL中矩陣是列主序的
  let xformMatrix = new Float32Array([
    cosB,    sinB,    0.0,    0.0,
    -sinB,    cosB,    0.0,    0.0,
    0.0,    0.0,    1.0,    0.0,
    0.0,    0.0,    0.0,    1.0,
  ])

向uniform變量傳遞矩陣

gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix)

該函數(shù)規(guī)范如下:

gl.uniformMatrix4fv(location, transpose, array):
將array表示的4×4矩陣分配給由location指定的uniform變量。

  • 參數(shù):
  • location:uniform變量的存儲位置。
  • transpose:是否轉(zhuǎn)置矩陣,在WebGL中沒有轉(zhuǎn)置矩陣的方法,必須指定為false。
  • array:待傳輸?shù)念愋突瘮?shù)組,4×4矩陣按列主序存儲在其中。
  • 返回值: 無
  • 錯(cuò)誤:
    • INVALID_OPERATION 不存在當(dāng)前程序?qū)ο?/li>
    • INVALID_VALUE transpose不為false,或者數(shù)組的長度小于16。
3.平移 矩陣
// 創(chuàng)建平移矩陣
  let xformMatrix = new Float32Array([
    1.0,    0.0,    0.0,    0.0,
    0.0,    1.0,    0.0,    0.0,
    0.0,    0.0,    1.0,    0.0,
    Tx,    Ty,    Tz,    1.0,
  ])
4.縮放 矩陣
  // 創(chuàng)建縮放矩陣
  let xformMatrix = new Float32Array([
    Sx,    0.0,    0.0,    0.0,
    0.0,    Sy,    0.0,    0.0,
    0.0,    0.0,    Sz,    0.0,
    0.0,    0.0,    0.0,    1.0,
  ])
四、第四章 高級變換與動畫基礎(chǔ)

雖然平移、旋轉(zhuǎn)、縮放等變換操作都可以用一個(gè)4×4的矩陣表示,但在寫圖像處理程序時(shí),手動計(jì)算每一個(gè)矩陣很耗費(fèi)時(shí)間,大多數(shù)的開發(fā)者都使用矩陣操作函數(shù)庫來簡化矩陣有關(guān)的操作。

OpenGL提供了一系列有用的函數(shù)來幫助我們創(chuàng)建變換矩陣。比如,通過調(diào)用glTranslate()函數(shù)并傳入X、Y、Z軸上的平移的距離,就可以創(chuàng)建一個(gè)平移矩陣,如下圖所示:


image.png

可惜的是,WebGL沒有提供類似的矩陣函數(shù)。當(dāng)然,目前有很多開源的矩陣庫可以使用,本書使用的是書的作者編寫的JavaScript函數(shù)庫cuon-matrix.js,為了學(xué)習(xí)此書,我們首先要了解一下函數(shù)庫中的內(nèi)容。

在看書中內(nèi)容之前,我們可以大致瀏覽一下函數(shù)庫源碼。可以發(fā)現(xiàn),函數(shù)庫主要創(chuàng)建了一個(gè)Matrix4類(構(gòu)造函數(shù)),在該類原型函數(shù)下綁定了眾多方法,許多函數(shù)之前都留有注釋,我們主要看一下代碼最前面的注釋:

/** 
 * This is a class treating 4x4 matrix.
 * This class contains the function that is equivalent to OpenGL matrix stack.
 * The matrix after conversion is calculated by multiplying a conversion matrix from the right.
 * The matrix is replaced by the calculated result.
 */

根據(jù)注釋可知,該函數(shù)庫主要處理4×4的矩陣,對標(biāo)OpenGL中矩陣處理函數(shù)。函數(shù)庫提供了一個(gè)名為Matrix4的對象(構(gòu)造函數(shù)),我們可以通過new方法創(chuàng)建它的實(shí)例,對象內(nèi)部掛載了許多關(guān)于矩陣計(jì)算的方法。

以上一章RotatedTriangle_Matrix.js示例改造如下:

1.創(chuàng)建矩陣
// RotatedTriangle_Matrix.js
...
  // 創(chuàng)建旋轉(zhuǎn)矩陣
  let radian = (Math.PI * ANGLE) / 180.0 // 轉(zhuǎn)換為弧度制
  let cosB = Math.cos(radian)
  let sinB = Math.sin(radian)
  // 注意WebGL中矩陣是列主序的
  let xformMatrix = new Float32Array([
    cosB,    sinB,    0.0,    0.0,
    -sinB,    cosB,    0.0,    0.0,
    0.0,    0.0,    1.0,    0.0,
    0.0,    0.0,    0.0,    1.0,
  ])
  ...

改造之后

// RotatedTriangle_Matrix4.js
...
  // 創(chuàng)建旋轉(zhuǎn)矩陣
  // 為旋轉(zhuǎn)矩陣創(chuàng)建Matrix4對象
  let xformMatrix = new Matrix4()
  // 將xformMatrix設(shè)置為旋轉(zhuǎn)矩陣
  xformMatrix.setRotate(ANGLE, 0, 0, 1)
...
2.傳輸矩陣數(shù)據(jù)
// RotatedTriangle_Matrix.js
...
  // 將旋轉(zhuǎn)圖形所需數(shù)據(jù)傳輸給頂點(diǎn)著色器
  let u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix')
  if (!u_xformMatrix) {
    console.log('Failed to get the storage location of u_xformMatrix')
  }
  gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix)
...

改造之后

// RotatedTriangle_Matrix4.js
...
  let u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix')
  if (!u_xformMatrix) {
    console.log('Failed to get the storage location of u_xformMatrix')
  }
  gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix.elements)
...

可見,在新的RotatedTriangle_Matrix4.js中,首先創(chuàng)建Matrix4實(shí)例,調(diào)用實(shí)例的setRotate()方法,在實(shí)例下elements屬性上創(chuàng)建了變換矩陣,最后把element屬性的內(nèi)容傳輸給著色器。

3.復(fù)合變換和動畫

對上述知識的一個(gè)綜合應(yīng)用,參見原書,或者【《WebGL編程指南》讀書筆記-繪制和變換三角形】

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容