Webgl中的基礎模型繪製

開篇

本篇博文對繪製webgl中基礎圖形做說明。閱讀本文時,你需要對基本的webgl有一定認識,並且熟悉中學的基本數學公式。不過這些公式都非常簡單,只要你學過,使用起來就沒有問題。本文將持續更新,但是如果你需要繪製複雜的圖形,我建議你使用建模軟件構建完後導出到webgl中。

基礎圖元

我們的世界的物體都是有形狀的,有些是圓的,有些是方的,還有些則是一些不規則的形狀。計算機就需要用特定的繪製方法模擬現實世界的各種圖形。在計算機中,基本的繪製只能繪製三種幾何體,點,線和麪,其他所有的一切形狀都是由這三種基本的圖像通過在空間排列的方式組合而成的。我們如果要畫任意一個物體,總共就兩個步驟,第一畫出基本的圖像,第二確定他們再空間中的位置,剩下的就交給GPU給我們進行裝配和光柵化。其中起一點很簡單,下面我的主要是將第二點,如何通過數據計算,安排這些基礎形狀的位置。

在webgl中點是構成任何元素的圖形的基本要素,通常畫一個點比較簡單,我們只需要初始化着色器,建立上下文即可。點的繪製使用drawArrays方法,傳入webgl.POINTS常量。畫一個點是webgl入門的基本操作之一。

const vertex = new Float32Array([0.0, 0.0, 0.0]);
...
webgl.drawArrays(webgl.POINTS, 0, 1);

線是由無數個點構成的,在webgl中畫線也非常簡單,只需要制定一個起始點和一個結束點即可。下面我們就來畫兩條直線。

// 初始化兩條點 A1 A2
const vertex = new Float32Array([0.5, 0.0, 0.0, -0.5, 0.0, 0.0, 0.0, 0.5, 0.0]);
const color = new Float32Array([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]);
...
webgl.drawArrays(webgl.LINES, 0, 4);

三角形

三角形是基礎圖元的最後一種圖形,也是繪製所有複雜圖形的基礎。計算機的所有圖形都是有三角形繪製的,這是因爲在一個空間中,三個點只能確定一個面,而面是構成體的基本要素。三角形的繪製也非常簡單,定義三個頂點。

const vertex = new Float32Array([
 0.5, 0.0, 0.0,
 -0.5, 0.0, 0.0,
 0.0, 0.5, 0.0
]);
...
webgl.drawArrays(webgl.TRIANGLES, 0, 3);

平面圖形-2D

結束完基本圖形的繪製,現在開始畫質由基礎圖元拼接組成的平面。需要說明的是複雜的圖形的繪製方式有許許多多種,我們只展示其中一種即可。你也可以按照不同的方法去繪製圖形。一般來說你繪製的圖形最好是越少佔用存儲空間越好。通過設置drawArrays的繪製方式我們可以決定如何去繪製我們的圖形。下面的這些方式的截圖:123456.png

矩形

矩形繪製很簡單,原理是:任何的矩形都是由兩個三角形拼接而成。我們只需要按照順序,畫出兩個三角形即可。這裏我們使用的是wegbl.TRIANGLE_STRIP方式繪製。它的繪製步驟是0,1,2畫出第一個三角形,然後是1,2,3繪製出第二個三角形。

const vertex = new Float32Array([ 
		0.0, 0.3, 0.0, // 1
		0.0, 0.0, 0.0, // 2
		0.3, 0.3, 0.0, // 3
		0.3, -0.3, 0.0 // 4
	]);
...
 webgl.drawArrays(webgl.TRIANGLE_STRIP, 0, 4);

五角星

五角星的思路和扇形的思路是一致的,不過,我們仔細觀察五角星就會發現,一個五角星其實是有10個頂點的。我們以五角星的中心爲原點,鏈接每個頂點,就會畫出五條短線和長線。最遠的點就是五角星的五個角,最短的點就是離中心最近的那五個頂點。利用簡單的數學公式,就可以求出每個頂點的位置。

//一共十個點
	const counts = 10,
		// 最遠的點和最短的點到中心的距離
		radius = 0.45,
		min_radis = 0.25,
		//將夾角轉換成弧度
		radiation = (Math.PI / 180) * (360 / 10),
		//中心位置
		center = [0.0, 0.0];

	let vertexs: number[] = center;
	let color: number[] = [1.0, 1.0, 0.0];
	for (let index = 0; index <= counts; index++) {
		// 頂點的位置
		let x = Math.sin(radiation * index) * radius;
		let y = Math.cos(radiation * index) * radius;
		// 內圈頂點的位置
		if (index % 2 === 0) {
			x = Math.sin(radiation * index) * min_radis;
			y = Math.cos(radiation * index) * min_radis;
		}
		vertexs.push(x);
		vertexs.push(y);
		color.push(...[1.0, 1.0, 0.0]);
	}

...

 webgl.drawArrays(webgl.TRIANGLE_FAN, 0, count);

窗格

描述三維物體時需要有地面參照,一個格子地板通常是很好的選擇。假設我們需要畫一個10 * 10 的地板,由100個格子組成,每個格子就是1 * 1 的寬和高。我們實際上是要畫 10 * 10 * 2 個三角形。三角剖分如下圖所示:

我們採用triangle-strip的方式,畫質三角形,代碼如下所示:

function createVertex (square:number) {
    square =  square * 10;
    let vertex:number[] = [];
    let pointer: number[] = [];
    let linePointer: number[] = [];
  	// 畫出每個格子在x和z軸上的點
    for (let indexX = 0; indexX < square; indexX++) {// x 
        for (let indexZ = 0; indexZ < square; indexZ++) {// z 
            vertex.push(indexX * 0.1,  0, -indexZ * 0.1);
        }
        linePointer.push(indexX * square, (indexX + 1) * square - 1);
    }
		// 畫出通過TRIANGLE_STRIP 的方式指定索引
    for (let indexX = 0; indexX < Math.pow(square, 2) - square; indexX++) {// z 
        pointer.push(indexX, indexX + square);
       
    }
		
   // 三角形描邊
    linePointer = linePointer.concat(pointer);

	return {
        vertexArray: new Float32Array(vertex),
        pointerArray: new Uint16Array(pointer),
        pointerLineArray: new Uint16Array(linePointer),
        count: pointer.length,
        lineCount: linePointer.length
    };
}

....
webgl.drawElements(webgl.TRIANGLE_STRIP, count, webgl.UNSIGNED_SHORT, 0);
webgl.drawElements(webgl.LINES, lineCount, webgl.UNSIGNED_SHORT, 0);

畫出最終圖形後,我們需要把地板進行平移,以保證我們的地板是在畫布中居中顯示的。

圓形

圓形的畫法有很多種,我們用最簡單的,即五角星的翻版,把五角星的短邊都拉長到長邊的長度,就可以畫出一個圓了。此外我們將resolution設置爲60,這樣就能畫出更多的三角形,而三角形的個數越多,標識這個圓越接近一個完美的圓形。(事實上是我們不可能畫出完美的圓形,只要接近它就可以了。)

const radius: number = 0.5, resolution: number = 60;
const count = resolution + 2;

//將夾角轉換成弧度
	const radiation = (Math.PI / 180) * (360 / resolution),
		//中心位置
		center = [0.0, 0.0];
	let vertexs: number[] = center;
	let color: number[] = [0.0, 0.0, 1.0];
	for (let index = 0; index <= resolution; index++) {
		let x = Math.sin(radiation * index) * radius;
		let y = Math.cos(radiation * index) * radius;
		vertexs.push(x);
		vertexs.push(y);
		color.push(0.0, 0.0, 1.0);
	}
...
webgl.drawArrays(webgl.TRIANGLE_FAN, 0, count);

立體模型-3D

在繪製完各種二維圖形之後,我們開始來繪製三維圖形,這也是webgl的主要作用——構建三維的世界。構建三維立體模型與構建二維圖形本質上並沒有什麼差別,只是我們在繪製三維圖形時多了一個深度z,這個值標識了對象在深度上的信息。然後剩下的也和二維圖形一樣進行三角形的拼接組裝。只不過在三維圖形裏面,拼接三維圖形需要使用更多的技巧以及一點點的額外的工作計算。此外,在繪製三維圖形時,我們開始使用drawElements替代drawArrays,前者需要傳入頂點索引緩存,當然你也可以繼續使用drawArrays,究竟使用哪種方式我個人認爲需要根據以下條件決定。

  1. 內存使用大小,使用drawElements會帶來額外的索引字節存儲空間,但是使用drawArrays則需要更多的頂點字節存儲空間。所以你需要綜合考量。
  2. 方法的靈活性。就我而言使用drawElements更能幫助我任性定位頂點索引,不會因爲某些混亂的判斷而畫錯圖形。簡單來說drawElements在繪製三維圖形時根據有靈活性。
  3. 自己的習慣。在充分考慮前面兩個因素後,你最後只需要決定你喜歡的方式來繪製即可,根據自己的習慣也能介紹你的開發時間。

立方體

立方體是我們畫出的第一個三維圖形。立方體的繪製方式也有多種,我們根據評估來用最省內存大額方式來畫出一個立方體。首先立方體有2 * 4 個頂點,其次我們按照順序,在每個頂點鏈接成不同的三角形,最後畫出立方體的每一個面。

  //    v6----- v5
  //   /|      /|
  //  v1------v0|
  //  | |     | |
  //  | |v7---|-|v4
  //  |/      |/
  //  v2------v3

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

const pointer = new Uint16Array([
		0, 1, 2, 2, 0, 3, // FRONT,
		0, 4, 1, 1, 4, 5, // LEFT
		2, 3, 7, 2, 7, 6, // RIGHT
		4, 5, 6, 4, 6, 7, // BACK
		4, 0, 7, 0, 7, 3, // BOTTOM
		1, 5, 6, 1, 6, 2 // TOP
]);
...
webgl.drawElements(webgl.TRIANGLES, count, webgl.UNSIGNED_SHORT, 0);

在繪製體的時候我強烈建議你在白紙上先畫出座標和草稿圖,立方體是最基礎的三維模型,它的頂點非常少,並不會耽誤你太多的時間。這樣做對培養你的三維知覺能力有幫助。

cube.png
請諸位原諒我這雙狗爪子o(╥﹏╥)o

圓柱體

圓柱體的繪製方式類似。不同的是頂部的頂點變成了頂部的一個圓形,相當於我們需要繪製兩個圓形,並且將他們連接起來形成側邊。我們首先要畫出來的是上下兩個圓,圓形繪製的方法以及在前面講過了圓柱體:

const HEIGHT = height,
		TOP = [0, HEIGHT, 0],
		RESOLUTION = 50,
		BOTTOM = [0, -1, 0],
		theta = ((360 / RESOLUTION) * Math.PI) / 180;
	let vertexs: number[] = [];
// 分別計算出上下表面圓邊上的點
	for (let index = 0; index < RESOLUTION; index++) {
		// top circle
		const x = Math.cos(theta * index) * radiusB;
		const z = Math.sin(theta * index) * radiusB;
		// bottom circle
		const x1 = Math.cos(theta * index) * radiusT;
		const z1 = Math.sin(theta * index) * radiusT;
		// 上面的圓點每一隔得y軸高度都是統一的,同理,下表面的的圓的y軸也是固定的。
		vertexs.push(x, HEIGHT, z, x1, -1, z1);
	}
	// 其他點1~resolution 底部中心點的位置 resolution + 1; 頂點位置 resolution,
	vertexs.push(...BOTTOM, ...TOP);

現在我們已經把頂點求出來,他們的順序是這樣的一種關係[(頂圓頂點),(底圓頂點),(頂圓頂點),(底圓頂點),...(底部中心頂點),(頂部中心頂點)],每一組的頂圓頂點和底圓頂點都在X和Z軸是一致的。現在我們在繪製立方體三維圖形的時候使用drawElements方法來給三角形排列。

let pointer: number[] = [];
//斜邊
for (let index = 0; index < RESOLUTION * 2; index++) {
  pointer.push(index); // 頂部點的位;
  /* 通過 % 實現當Y Z大於resultion的時候取絕對值,實現點位的循環。
        如:x =40 時 x 爲 0  或者x = 41時,x 爲 1;
        因爲矩形的最後一個三角麪點需要和第一個點和第二個點進行合併。
        */
  pointer.push((index + 1) % (RESOLUTION * 2), (index + 2) % (RESOLUTION * 2));
}

//底邊
for (let index = 0; index < RESOLUTION; index++) {
  const step = (2 * index + 1) % (2 * RESOLUTION);
  const step2 = (2 * (index + 1) + 1) % (2 * RESOLUTION);
  // 永遠是底部中心點開始的
  pointer.push(step);
  pointer.push(RESOLUTION + 1); // 頂部中心點的在vertexs中的位置 即 1 + RESOLUTION
  pointer.push(step2);
}

//頂邊
for (let index = 0; index < RESOLUTION; index++) {
  const step = (2 * index + 2) % (2 * RESOLUTION);
  const step2 = (2 * (index + 2)) % (2 * RESOLUTION);
  // 永遠是底部中心點開始的
  pointer.push(step);
  pointer.push(RESOLUTION); // 底部中心點的在vertexs中的位置 即 RESOLUTION
  pointer.push(step2);
}

...
webgl.drawElements(webgl.TRIANGLES, pointer.length, webgl.UNSIGNED_SHORT, 0);

我們繪製的方式是,在斜邊上繪製三角形,然後繪製上表面和下表面兩個圓形(已經講過,不再重複),斜邊的繪製思路未:組成斜邊三角形的點分別爲(上邊圓頂點v1), 對應的(下邊圓頂點v2),最後是(上邊圓的接下一個點v3)。請看下圖的示意:
cylinder.png
最後我們需要把最後的點和第一個點銜接上,所以使用了取餘數%的方式,來判斷是否已經經過了一個輪迴。這樣我們就實現了一個矩形的繪製了。

球體

球體的面積需要我們理解一些數學公式和一定的集合空間觀察才能較好的畫出來,當然這些公式都是初中書序的知識,非常簡單,如果你會正弦、餘弦這些概念,那麼知道如何畫一個球形了。爲了搞明白我們來看球體的示意圖:
sphere.jpg
我們需要計算的就是A的距離,因爲半徑和分割角度都是我們自己定義的,那麼只需要通過公式,我們就可以把A點的x,y,z的左邊計算出來。(上圖的Y 在實際中應該是Z,Z軸則是Y軸)

const RADIUS = radius, RESOLUTION = resolution;
const theta = (180 / RESOLUTION) * (Math.PI / 180);
const beta = (360 / RESOLUTION) * (Math.PI / 180);
//計算出圓體以及表面線條的各個點的位置
let vertexs:number[] = [];
for (let index = 0; index <= RESOLUTION; index++) {
         // 同等高度的Y值 O1-O2
        const y = Math.cos(theta * index) * RADIUS;
        // 底邊作爲斜邊的長度 O2-A
        const d = Math.sin(theta * index) * RADIUS;
        for (let index1 = 0; index1 <= RESOLUTION; index1++) {
            // 斜邊的餘弦即是x軸的距離 O1-c
            const x = Math.cos(beta * index1) * d;
            // 斜邊的正弦即是Z軸的距離 B-C
            const z = Math.sin(beta * index1) * d;
            vertexs.push(x, y, z);
        }
     }

計算出頂點之後,我們再來計算三角形平面的索引。球體可以看成是被很多三角形圍成的一個立方體,每個三角形對應的點的計算類似於圓柱體斜邊的計算,區別就是圓柱形只需要計算一條區間的三角形數量,而球體需要計算多條區間(多條緯度)的三角形數量:

    /* 計算出頂點的位置爲 [0, 1,..... 一個循環之後, RESOLUTION, RESOLUTION + 1]
     我們需要連接的是 0, 1, RESOLUTION 頂點的位置拼湊成一個三角形
    */
    for(var index = 0; index < Math.pow(RESOLUTION, 2); index ++)
    {
            pointer.push(index); // 本行第一個
            pointer.push(index + RESOLUTION + 1); // 下一行第一個
            pointer.push(index + 1); // 本行第二個

            pointer.push(index + 1); // 本行第二個
            pointer.push(index + RESOLUTION + 1); // 下一行第一個
            pointer.push(index + RESOLUTION + 2); // 下一行第二個

            //到此,一個四邊形被拼湊成功
    }


 webgl.drawElements(webgl.TRIANGLES, pointer.length, webgl.UNSIGNED_SHORT, 0);

總結

在開發3D應用中,我們可以通過第三方封裝的方法創建3D模型,或者利用blender等建模軟件幫助我們構建3D圖形。因此基礎圖形的設計往往被人忽視了。說道底,在webgl中構建3D模型的本質就是處理點線和麪三個元素之間的位置關係。對於繪製複雜的模型來說,自己動手構建實在不是一個好法子,最好的方式是去可視化3D建模軟件中構建後加載進你的程序。然後正如所有技術的本質一樣,在運用之前,瞭解底層的邏輯是十分必要的。以上只是我對畫圖的總結,這其中設計的webgl一些底層的東西需要你自己去學習。所以在閱讀這篇文章的時候,你需要一些webgljavascript的基礎。這篇博文作爲持續更新的作品,主要是用來記錄自己在使用各種框架畫出繽紛世界時,不應該忘記這些構建這個世界的基石。本篇博文的所有示例均在這個網站展示,所有源碼也會該網站上貼出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章