1. 程式人生 > 實用技巧 >圖形學-從畫點到三維模型

圖形學-從畫點到三維模型

圖形學

圖形學就是在一個二維的平面上展示三維模型.
這裡我們用H5的canvas來演示. 我們不會用canvas的任何畫圖的方法, 只是把他當作一個螢幕來使用.

三維模型

要將三維物體,表現在二維平面上, 首先我們要有一個三維的模型.我們要先知道,如何在一個三維座標系中構造一個物體的模型.
這個模型是怎麼做的呢. 他一般包含一下幾點

中心點和旋轉角度.

中心點一般用來確定一個模型在三維座標系中的具體位置. 一般用 x, y, z 表示.中心點移動, 模型上的所有座標跟著移動. 模型旋轉時,繞中心點旋轉.

三角形網路 和 頂點.

模型的具體形狀是由三角形拼成的. 如圖所示

所有的模型都是有一個一個三角形拼成的. 三角形少, 精度就小.三角形多,精度就大. 也就越接近顯示中的物體. 以一個正方體為例, 6個面, 一個面需要兩個三角形拼成, 一共12個三角形.
三角形的點就是頂點. 一般有很多三角形的頂點是重複的. 立方體一共八個頂點. 程式碼實踐中我們是頂點做一個數組. 三角形做一個數組.

	 let points = [
            -1, 1,  -1,     // 0
            1,  1,  -1,     // 1
            -1, -1, -1,     // 2
            1,  -1, -1,     // 3
            -1, 1,  1,      // 4
            1,  1,  1,      // 5
            -1, -1, 1,      // 6
            1,  -1, 1,      // 7
        ]

        let vertices = []
        for (let i = 0; i < points.length; i += 3) {
            let v = GuaVector.new(points[i], points[i+1], points[i+2])
            // let c = GuaColor.randomColor()
            let c = GuaColor.red()
            vertices.push(GuaVertex.new(v, c))
        }

        // 12 triangles * 3 vertices each = 36 vertex indices
        let indices = [
            // 12
            [0, 1, 2],
            [1, 3, 2],
            [1, 7, 3],
            [1, 5, 7],
            [5, 6, 7],
            [5, 4, 6],
            [4, 0, 6],
            [0, 2, 6],
            [0, 4, 5],
            [5, 1, 0],
            [2, 3, 7],
            [2, 7, 6],
        ]

在上述程式碼中, points就是頂點的陣列, 每三個元素是一組座標,表示該頂點的x,y,z.
indices是三角形的陣列, 第一個元素[0, 1, 2] 表示第一個三角形. 這裡的0表示 頂點數組裡的第0個頂點.

頂點的屬性.

頂點的屬性包括,座標(x, y, z), 顏色(rgba), 法向量(fx, fy, fz), 貼圖.

  • 座標就是這個點在三維座標系中的座標.
  • 顏色就是rgba顏色.
  • 法向量就是這個點的垂直方向. 法向量屬性這裡和我們的數學嘗試有些區別. 一個點為什麼會有法向量呢?
    原因在於我們這裡的頂點並不是數學意義上的點. 數學上的點是沒有大小,寬高,方向的. 但是我們這裡的頂點有. 嚴格意義上來說 這裡的頂點是一個長寬都是1畫素的面,是面的最小組成單元
    .他有座標, 有寬高都是1,既然是面當然有法向量. 又因為他足夠小.能夠近似的看做點.
  • 貼圖. 貼圖指的是, 這個模型的顏色不是單一的顏色, 而是一張圖. 一般會有一個貼圖檔案. 貼圖檔案裡儲存的是每一個頂點上應該填充的顏色. 頂點的貼圖屬性裡儲存的就是這個顏色值在貼圖檔案中的位置.

視角

視角就是三維空間中我們觀察模型時的視線. 視角有三個屬性.
  • position: 視角的座標,就是眼睛的座標
  • target: 我們的視角觀察的目標的座標. position和target連起來就是視線
  • up: 是position到target的距離.
    這三個屬性確定一個視角. 視角移動的話, 三維模型的投影也會發生變化.

光照陰影

光照陰影(shading). 在二維座標系中模擬顯示中的物體還要考慮光照問題. 我們看到的顏色是物體反射光的顏色. 光線照射到物體上的角度不同, 物體上光線的強度就不同.
就是有一個光源, 他發射除了光線, 光線照射到 物體上, 由於角度不同導致了, 雖然物體上的顏色相同, 但是看起來顏色有區別, 有高光, 有陰影.比如物體模型是紅色, 光線與三角形是垂直關係90°展示正紅色, 不是垂直是60°, 是暗紅色, 隨著角度越來越小,顏色越來越暗. 也就是說點的顏色要根據法向量和光線的夾角來計算.
根據計算方式不同, 會有不同效果.
如下圖所示

這是不同的光照模型和不同精度下模型的效果.  從上到下是精度越來越高, 從左到右是 光照的計算越來越精細.
  • 左1(a1), (flat shading)是一個三角形用一個法向量, 所以一個平面的光照是一樣的.
  • 左2(b1),(goraud shading) 是一個頂點一個法向量, 一個三角形就有三個法向量. 其他點用頂點的光照計算插值.
  • 左3(c1), (phong shading) 是先 一個頂點一個法向量, 然後,用這三個頂點的法向量 插值計算其他點的法向量, 然後再計算光照. 所以c1即便模型精度很低, 但是也依然有了一個精度相對高的光照.

這是不同光照下的法向量方向.

二維平面

畫素

要在二維平面上展示影象, 基礎是畫點. 一張圖片是由畫素組成的. 比如 一張解析度為50*100的png圖片. 那麼這張圖長寬有50個畫素, 高有100個畫素.畫素就是圖片最基礎的組成單元. 所有的圖片都是由畫素組成. 
畫素的屬性包括: 座標x, y表示畫素的位置. rgba表示畫素的顏色.比如紅色用rgba表示就是(255, 0, 0, 255). rgba的每一個值都是0-255. 用二進位制表示剛好一個位元組的資料量. 

以H5中的canvas圖片的畫素為例.

	  var c=document.getElementById("myCanvas");
        var ctx=c.getContext("2d");
        ctx.fillStyle="red";
        ctx.fillRect(0, 0, 50, 50);
        
        // 獲取canvas上的畫素資料. 引數(x, y, w, h)
        var imgData=ctx.getImageData(0, 0, 50, 50);

        imgData.data[1] =  255
        ctx.putImageData(imgData,10,10);
        // 使用putImageData之後, 會直接改變canvas上的畫素顏色.

這裡的imgData就是canvas中的畫素資料. 格式如下

		{
			height: 50,
	        width: 50,
	        data: [
	            255, 0,0, 255,
	            255, 0,0, 255,
	            255, 0,0, 255,
	            ......
	            ]
		}

這裡的imgData表示這張canvas圖片, 高50畫素, 寬50畫素. data裡面的資料是從左上角開始,從左到右, 從上到下依次排列. 每4個元素表示該畫素的顏色資訊. 比如第一個畫素的rgba是(255, 0, 0, 255).
一共由 50 * 50 = 2500個畫素, 每個畫素用4個數據表示顏色. 所以在data裡由10000個元素.

所以只要我們有了一張圖的畫素資訊, 就能直接把這張圖展示出來.

光柵化

我們現在有了一個三維的立體模型, 螢幕是二維的, 要在一個二維螢幕上展示三維圖形, 就需要把三維圖形投影到二維平面上, 用畫素展示出來, 稱為為光柵化.
比如:

上圖就是一個立方體在二維平面中的投影. 如果我們轉變觀察這個立方體的角度, 這個投影的形狀也會發生改變. 從三維立體圖形, 到二維平面的投影圖, 我們可以通過三角函式計算, 獲得最終的尺寸. 那麼針對一個三維模型, 我們要考慮他的旋轉角度, 相對於遠點的位移, 觀察的視角, 以及投影面. 這些都考慮到後, 我們就能得到一個貼近於真是的投影圖.

矩陣

上面說了, 要從三維模型的座標, 考慮很多條件, 用三角函式來計算出最終投影到平面上的二維座標. 但是這樣一個個計算太過複雜, 一般都是通過矩陣來直接計算的. 矩陣能將三維模型的座標點, 直接轉換成要投影的二維座標, 常用的矩陣包括:

  • 計算旋轉角度的旋轉矩陣rotation
  • 計算模型位移的位移矩陣translation
  • 確認觀察視角的視角矩陣view
  • 把三維座標轉換成二維座標的投影矩陣projuction

矩陣是跟著模型走的, 一個單獨的模型, 比如說房間裡的一張床, 一個桌子, 當他移動的時候, 所有的點都跟著移動. 當他旋轉的時候, 所有的點都跟著旋轉.這個模型有一箇中心點和旋轉角度來確定他在三維空間的位置, 比如水平旋轉了30°, 根據中心點, 和 旋轉角度, 一個桌子上的點就能算出旋轉後的位置, 旋轉矩陣就是做這個地, 位移矩陣就是計算當中心點移動了10個畫素後, 其他點的位置. 旋轉矩陣和位移矩陣是計算模型在移動後的三維空間的座標, 視角矩陣是當三維空間的視角變化後, 模型在三維空間的座標變化. 投影矩陣是將三維座標轉換成二維座標.

	    // 視角矩陣
	    const view = Matrix.lookAtLH(position, target, up)
        // 投影矩陣
        const projection = Matrix.perspectiveFovLH(0.8, w / h, 0.1, 1)

        // 得到 mesh 中點在世界中的座標
        const rotation = Matrix.rotation(mesh.rotation)	//旋轉矩陣
        const translation = Matrix.translation(mesh.position)	//位移矩陣
        const world = rotation.multiply(translation)	// 世界矩陣

        // transform就是最後的矩陣.
        const transform = world.multiply(view).multiply(projection)

矩陣可以相乘, 相乘的效果與依次使用是一樣的.

深度

一個三維模型投影到二維平面, 因為我們看到是投影, 看到正面的話,就不能看到背面. 而且正面和背面雖然在二維平面的座標是一樣的, 但是顏色是不一樣的. 那麼我們怎麼確定具體畫哪個點呢?
就用深度.
一個三維的頂點經過矩陣計算後得到的除了x,y二維座標, 還有一個深度. 這個深度是和視角的距離. 如果和視角的距離近, 那麼就展示,如果相對較遠, 就不畫. 一般來說是儲存一個 二維平面上已經畫好的點的陣列 .這個點的資訊就包括他在二維平面上的座標,顏色,深度. 再畫點時, 先判斷這個點的座標是否已經畫過了, 如果沒有,就畫上. 如果畫過了, 對比深度.如果當前點的深度更近,就把原來的覆蓋掉, 否則不畫.

插值

我們的圖形是以三角形為基礎的, 影象資訊都是儲存在頂點上的, 頂點上的座標, 顏色都有了, 但是我們不能只畫頂點, 那麼兩個頂點之間的點的影象資訊是怎麼得到的呢?
就是用插值計算得到的.
比如說有兩個點.

  • A點,座標(10, 10).顏色紅色(255, 0, 0, 255)
  • B點, 座標(50, 50). 顏色綠色(0, 255, 0, 255)

現在要問處於這兩點之間的C點的顏色和座標.
首先計算factor, 過渡因子.
首先計算AB的長度.

var x1 = 10, y1 = 10, x2 = 50, y2 = 50 
// 長度s
var s = (  (x2 - x1) ** 2 + (y2 - y1) ** 2 ) ** 0.5

// 假設我們要畫AB上距離A點10畫素的點, 根據點距A的距離佔總距離的比例,計算到他的座標
var factor = 10 / s

var x = factor * (x2 - x1) + x1
var y = factor * (y2 - y1) + y1

顏色的插值計算與座標一樣.
C點是AB的中心點, 那麼C的座標就是(30, 30), 顏色(128, 128, 0, 255).所有的計算中有小數的四捨五入.
畫線就是把AB兩點之間的所有的點, 根據這個點的所在位置,計算插值, 然後把所有的點都畫出來.

畫三角形

現在二維平面上的三角形點的資訊有了, 根據插值我們能畫線了. 那麼三角形這個面怎麼畫呢?

首先我們有三角形的三個頂點A B C按Y軸座標排列, a.y > b.y > c.y.
對AC邊求得M點(m.y == b.y). 三角形就被劃分成了AMB, MBC兩個三角形.
然後先畫AMB.
從上到下, 從A點開始畫平行線. 根據距離A點的高度, 算出factor,計算插值, 然後找出AM上的sx點和 AB上的ex點. sx-ex線與MB平行. 這算是在AMB三角形中填了一條線. 然後再用平行線一條一條把整個AMB填滿, 從上到下, 如果A距離MB的高度為10, 那就是10條線.
AMB畫完了再畫MBC.
一個三角形畫完了, 把所有的三角形都畫完.整個立體圖形就都出來了.