Canvas原生API(純CPU)計算並渲染三維圖
前端工程師學圖形學:Games101 第三次作業
利用Canvas畫三維中的三角形並使用超取樣實現抗鋸齒
最終完成功能
- Canvas 原生API實現三角形柵格化演算法
- 實現 z-buffer 判斷三角形先後關係
- 使用 super-sampling 處理 Anti-aliasing,也就是超取樣實現抗鋸齒
1 整體分析
本次實驗中,首先需要進行矩陣變換,將初始傳入的三角形經過變換後到規範立方體內,這需要進行三種變換。設一個點的座標變換為(x, y, z) -> (x', y', z')
每個矩陣的求解在之前的部落格中都有講解圖形學 旋轉與投影矩陣—2 - 知乎 (zhihu.com),這是其中一篇可供參考,因此,投影矩陣,檢視矩陣和模型矩陣這裡不再求解。現狀,變換矩陣已經知道,現狀需要將轉換後的矩陣進行光柵化,在光柵化時,需要遍歷螢幕上的每個畫素點進行判斷,該點是否在三角形內,如果在,則渲染,由於本文采用的 Canvas 進行渲染,因此需要對 Canvas 上的每個畫素點進行判斷。
光柵化完成後,可得到一個充滿顏色的三角形,如果渲染多個三角形,會產生覆蓋現象,這個時候就需要判斷深度,因此我們需要維護一個深度緩衝的陣列,這個陣列的大小為 canvas 的 width*height。當渲染後面的三角形時,首先判斷該畫素的當前深度是否小於預渲染畫素的深度,如果小於,則渲染,否則,不進行處理。
上述完成後,會得到一些一個帶鋸齒的三角形,為了解決鋸齒問題,這裡進行了超取樣,即讓一個畫素點平分為 9 塊正方形區域,看九塊區域有多少在三角形內,佔比情況,憑佔比量設定該畫素的顏色,最終完成抗鋸齒的功能。
總結,完成該實驗的步驟如下
- 矩陣變換,投影,檢視,模型變換
- 光柵化,使用 Canvas 原生 Api 畫顏色
- 抗鋸齒,超取樣實現,將一個畫素點分為 9 個正方形
2 程式碼分析
第一步:矩陣變換函式
// 變換函式
function getFinalPosition(position){
const finalPosition = new THREE.Vector4().set(
position.x,
position.y,
position.z,
1
).applyMatrix4(perspMatrix).applyMatrix4(viewMatrix);
finalPosition.set(
finalPosition.x/finalPosition.w,
finalPosition.y/finalPosition.w,
finalPosition.z/finalPosition.w,
1
)
return finalPosition;
}
輸入三角形的座標即可得到轉換後的最終座標,為了簡單使用,這裡沒有使用到模型矩陣,僅僅用到了投影矩陣和檢視矩陣。經過轉換後,三角形座標 x,y,z 都被規範到 [-1, 1] 之間了
第二步:將畫素座標轉換為螢幕座標
螢幕空間內,畫素是從 (0, 0) 到 (width-1, height-1),渲染的範圍為 (0, 0) 到 (width, height),width 和 height 是 Canvas DOM 的寬和高。注意:畫素是一個一個方塊,如下圖所示。
由此可得,規範立方體到螢幕空間的座標變換矩陣為
程式碼如下
// 轉換成螢幕座標
function transScreen(positions, width, height){
const MViewPort = new THREE.Matrix4();
MViewPort.set(
width/2, 0, 0, width/2,
0, -height/2, 0, height/2,
0, 0, 1, 0,
0, 0, 0, 1
)
positions.forEach(vec => vec.applyMatrix4(MViewPort));
}
第三步:光柵化
光柵化:遍歷相應畫素點,判斷該點是否在三角形內,在的話才繼續處理,遍歷範圍是包圍三角形最大的矩形盒。
求得三個頂點的寬高的最大最小值即可求出這個包圍盒,分別為 minX, minY, maxX, maxY,開始遍歷判斷
const ctx = canvas.getContext('2d');
...
// 1. 遍歷包圍盒的每個畫素
for (let i = Math.round(minX); i < maxX; i++) {
for (let j = Math.round(minY); j < maxY; j++) {
...
// 2. 判斷畫素是否在三角形內
if((count=getInner(point1, point2, point3, pixel))!==0){
...
// 3. 建立顏色
switch (type) {
case 1:
data[0] = redColor;
data[1] = greenColor;
data[2] = blueColor;
data[3] = 透明度;
break;
case 2:
data[0] = redColor;
data[1] = greenColor;
data[2] = blueColor;
data[3] = 透明度;
break;
}
// 4. 賦予相應畫素點顏色
ctx.putImageData(myImageData, i, j);
}
}
}
執行上述程式後,Canvas 繪畫單個三角形的工作便完成了。
我們需要維護一個深度陣列,用來儲存當前畫素的深度
const z_buffer = []
for (let i = 0; i < height; i++) {
const arr = [];
z_buffer.push(arr)
for (let j = 0; j < width; j++) {
arr.push(-Number.MAX_VALUE)
}
}
設定每個數字為無窮遠,代表後續的每個三角形都比其畫素點近,如果點在三角形內,判斷當前深度並進行賦值
// getZ 表示獲取欲渲染畫素點的深度,z_buffer 儲存當前畫素點深度
const z = getZ(i+0.5, j+0.5);
if(z<z_buffer[j][i]){
// console.log('success');
continue;
}
z_buffer[j][i] = z;
第四步:抗鋸齒
將一個畫素點分為 9 份相同大小的正方形,判斷有多少份正方形在三角形內,最後憑佔比賦予顏色
// 獲得一個畫素點分成 9 份正方形後,在三角形內的個數
function getInner(point1, point2, point3, pixel){
let extend = {x:0, y:0, index: 0}
for (let i = 1/6; i < 1; i+=1/3) {
for (let j = 1/6; j < 1; j+=1/3) {
extend.x = i;
extend.y = j;
if(isInner(point1, point2, point3, pixel, extend)) extend.index++;
}
}
// 判斷當前正方形是否在三角形內
function isInner(point1, point2, point3, pixel, extend){
pixel.x += extend.x;
pixel.y += extend.y;
const ab = new THREE.Vector3().subVectors(point2, point1);
const bx = new THREE.Vector3().subVectors(pixel, point2);
const direct1 = new THREE.Vector3().crossVectors(ab, bx);
const bc = new THREE.Vector3().subVectors(point3, point2);
const cx = new THREE.Vector3().subVectors(pixel, point3);
const direct2 = new THREE.Vector3().crossVectors(bc, cx);
const ca = new THREE.Vector3().subVectors(point1, point3);
const ax = new THREE.Vector3().subVectors(pixel, point1);
const direct3 = new THREE.Vector3().crossVectors(ca, ax);
const f1 = direct1.dot(direct2);
const f2 = direct2.dot(direct3);
return Math.sign(f1) === 1 && Math.sign(f2) === 1;
}
return extend.index;
}
將畫素點平均分成九份後,每份都為正方形,找出正方形中心,判斷該中心是否在正方形內,最後總結出在三角形內的正方形個數,最後賦予顏色.
count=getInner(point1, point2, point3, pixel);
const rat = count/9;
switch (type) {
case 1:
data[0] = 255 * rat + oriData[0] * (1-rat);
data[1] = oriData[1] * (1-rat);
data[2] = oriData[2] * (1-rat);
data[3] = 255 * rat + oriData[3] * (1-rat);
break;
case 2:
data[0] = oriData[0] * (1-rat);
data[1] =255 * rat + oriData[1] * (1-rat);
data[2] =255 * rat + oriData[2] * (1-rat);
data[3] =255 * rat + oriData[3] * (1-rat);
break;
}
3. 總結
使用 Canvas 原生 API 實現三維圖形的光柵化,能夠加強我們都圖形學座標轉換的整體印象,能使我們瞭解基本原理,對我們理解遊戲等三維引擎的底層原理有很大的幫助。
我這完整程式碼沒有整理,不太好看,就不放出來了,需要原始碼交流的可以私聊,如果覺得有用,可以點個贊哦。