在之前的文章中,我們使用WebGL繪制了很多二維的圖形和圖像,在學習2D繪圖的時候,我們提過很多次關于GPU的高效渲染,但是2D圖形的繪制只展示了WebGL部分的能力,WebGL更強大的地方在于,它可以繪制各種3D圖形,而3D圖形能夠極大地增強可視化的表現能力。
相信很多小伙伴都對此有所耳聞,也有不少人學習WebGL,就是沖著它的3D繪圖能力。
接下來,我就用一個簡單的正立方體的例子來演示在WebGL中如何繪制3D物體。
從二維到三維
首先,我們先來繪制一個熟悉的2D圖形,正方形。
// vertex
attribute vec2 a_vertexPosition;
attribute vec4 color;varying vec4 vColor;void main() {gl_PointSize = 1.0;vColor = color;gl_Position = vec4(a_vertexPosition, 1, 1);
}// fragment
#ifdef GL_ES
precision highp float;
#endifvarying vec4 vColor;void main() {gl_FragColor = vColor;
}
// ...
renderer.setMeshData([{positions: [[-0.5, -0.5],[-0.5, 0.5],[0.5, 0.5],[0.5, -0.5]],attributes: {color: [[1, 0, 0, 1],[1, 0, 0, 1],[1, 0, 0, 1],[1, 0, 0, 1],]},cells: [[0, 1, 2], [2, 0, 3]]
}]);
// ...
上述這些代碼比較簡單,我就不過多解釋了。
在畫布上我們看到,繪制了一個紅色的正方形,它是一個平面圖形。
接下來,我們就在這個圖形的基礎上,將它拓展為3D的正立方體。
要想把2維圖形拓展為3維幾何體,第一步就是要把頂點擴展到3維。也就是把vec2擴展為vec3。
// vertex
attribute vec3 a_vertexPosition;
attribute vec4 color;varying vec4 vColor;void main() {gl_PointSize = 1.0;vColor = color;gl_Position = vec4(a_vertexPosition, 1);
}
當然僅僅修改Shader是不夠的,因為數據是從JavaScript傳遞過來的,所以我們需要在JavaScript中計算立方體的頂點數據,然后再傳遞給Shader。
一個立方體有8個頂點,能組成6個面。在WebGL中需要用12個三角形來繪制它。
如果6個面的屬性相同的話,我們可以復用8個頂點來繪制;
但如果屬性不完全相同,比如每個面要繪制成不同的顏色,或者添加不同的紋理圖片,就得把每個面的頂點分開。這樣的話,就需要24個頂點來分別處理6個面。
為了方便使用,我們可以定義一個JavaScript函數,用來生成立方體6個面的24個頂點,以及12個三角形的索引,并且定義每個面的顏色。
/*** 生成立方體6個面的24個頂點,12個三角形的索引,定義每個面的顏色信息* @param size* @param colors* @returns {{cells: *[], color: *[], positions: *[]}}*/
export function cube(size = 1.0, colors = [[1, 0, 0, 1]]) {const h = 0.5 * size;const vertices = [[-h, -h, -h],[-h, h, -h],[h, h, -h],[h, -h, -h],[-h, -h, h],[-h, h, h],[h, h, h],[h, -h, h]];const positions = [];const color = [];const cells = [];let colorIdx = 0;let cellsIdx = 0;const colorLen = colors.length;function quad(a, b, c, d) {[a, b, c, d].forEach(item => {positions.push(vertices[item]);color.push(colors[colorIdx % colorLen]);});cells.push([0, 1, 2].map(i => i + cellsIdx),[0, 2, 3].map(i => i + cellsIdx));colorIdx ++;cellsIdx += 4;}quad(1, 0, 3, 2); // 內quad(4, 5, 6, 7); // 外quad(2, 3, 7, 6); // 右quad(5, 4, 0, 1); // 左quad(3, 0, 4, 7); // 下quad(6, 5, 1, 2); // 上return {positions, color, cells};
}
現在我們就可以通過調用cube這個函數,構建出立方體的頂點信息。
const geometry = cube(1.0, [[1, 0, 0, 1], // 紅[0, 0.5, 0, 1], // 綠[0, 0, 1, 1] // 藍
]);
通過這段代碼,我們就能創建出一個棱長為1的立方體,并且六個面的顏色分別是“紅、綠、藍、紅、綠、藍”。
接下來我們就要把這個立方體的頂點信息傳遞給Shader。
在傳遞數據之前,我們需要先了解一個知識點,是關于繪制3D圖形與2D圖形存在的一點不同,那就是繪制3D圖形時,必須要開啟深度檢測和啟用深度緩沖區。
在WebGL中,我們可以通過gl.enable(gl.DEPTH_TEST);
這段代碼來開啟深度檢測;在清空畫布的時候,也要用gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
這段代碼來同時清空顏色緩沖區和深度緩沖區。
啟動和清空深度檢測和深度緩沖區這兩個步驟,非常重要。但是一般情況下,我們幾乎不會用原生的方式來編寫代碼,所以了解一下即可。為了方便使用,在本文演示的例子中,我們還是直接使用gl-renderer這個庫,它封裝了深度檢測,我們在使用時,在創建renderer的時候配置一個參數depth: true
就可以了。
現在我們就把這個三維立方體用gl-renderer渲染出來。
// ...
renderer = new GlRenderer(glRef.value, {depth: true // 開啟深度檢測
});
const program = renderer.compileSync(fragment, vertex);
renderer.useProgram(program);
renderer.setMeshData([{positions: geometry.positions,attributes: {color: geometry.color},cells: geometry.cells
}]);
renderer.render();
現在我們在畫布上看到的是一個紅色正方形,這是因為其他面被遮擋住了。
投影矩陣:變換WebGL坐標系
但是,等等,為什么我們看到的是紅色的一面呢?按照我們所編寫的代碼,預期看到的應該是綠色的一面,也就是說我們預期Z軸是向外的,因為規范的直角坐標系是右手坐標系。所以按照現在的繪制結果,我們發現WebGL的坐標系其實是左手系的?
但一般來說,不管什么圖形庫或者圖形框架,在繪圖的時候,都會默認將坐標系從左手系轉換為右手系,因為這更符合我們的使用習慣。所以這里,我們也去把WebGL的坐標系從左手系轉換為右手系,簡單來說,就是將Z軸坐標方向反轉。關于坐標轉換,可以通過齊次矩陣來完成。對坐標轉換不熟悉的小伙伴,可以參考我之前的一篇關于仿射變換的文章。
將Z軸坐標方向反轉,對應的齊次矩陣是這樣的:
[1, 0, 0, 0,0, 1, 0, 0,0, 0, -1, 0,0, 0, 0, 1
]
這種轉換坐標的齊次矩陣,也被稱為投影矩陣,ProjectionMatrix。
現在我們修改一下頂點著色器,把這個投影矩陣添加進去。
// vertex
attribute vec3 a_vertexPosition; // 1:把頂點從vec2擴展到vec3
attribute vec4 color; // 四維向量varying vec4 vColor;
uniform mat4 projectionMatrix; // 2:投影矩陣-變換坐標系void main() {gl_PointSize = 1.0;vColor = color;gl_Position = projectionMatrix * vec4(a_vertexPosition, 1.0);
}
現在我們就能看到畫布上顯示的是綠色的正方形了。
模型矩陣:讓立方體旋轉起來
現在我們只能看到立方體的一個面,因為Z軸是垂直于屏幕的,這樣子從視覺上看好像和2維圖形沒什么區別,沒法讓人很直觀地聯想、感受到這是一個三維的幾何體,為了將其他的面露出來,我們可以去旋轉立方體。
要想旋轉立方體,我們同樣可以通過矩陣運算來實現。這個矩陣叫做模型矩陣,ModelMatrix,它定義了被繪制的物體變換。
把模型矩陣加入到頂點著色器中,將它與投影矩陣相乘,再乘上齊次坐標,就得到最終的頂點坐標了。
attribute vec3 a_vertexPosition; // 1:把頂點從vec2擴展到vec3
attribute vec4 color; // 四維向量varying vec4 vColor;
uniform mat4 projectionMatrix; // 2:投影矩陣-變換坐標系
uniform mat4 modelMatrix; // 3:模型矩陣-使幾何體旋轉void main() {gl_PointSize = 1.0;vColor = color;gl_Position = projectionMatrix * modelMatrix * vec4(a_vertexPosition, 1.0);
}
現在我們定義一個JavaScript函數,用立方體沿x、y、z軸的旋轉來生成模型矩陣。
以x、y、z三個方向的旋轉得到三個齊次矩陣,然后將它們相乘,就能得到最終的模型矩陣。
import { multiply } from 'ogl/src/math/functions/Mat4Func.js';
// ...
export function fromRotation(rotationX, rotationY, rotationZ) {let c = Math.cos(rotationX);let s = Math.sin(rotationX);const rx = [1, 0, 0, 0, // 繞X軸旋轉0, c, s, 0,0, -s, c, 0,0, 0, 0, 1];c = Math.cos(rotationY);s = Math.sin(rotationY);const ry = [c, 0, s, 0,0, 1, 0, 0, // 繞Y軸旋轉-s, 0, c, 0,0, 0, 0, 1];c = Math.cos(rotationZ);s = Math.sin(rotationZ);const rz = [c, s, 0, 0,-s, c, 0, 0,0, 0, 1, 0, // 繞Z軸旋轉0, 0, 0, 1];const ret = [];multiply(ret, rx, ry);multiply(ret, ret, rz);return ret;
}
我們把模型矩陣傳給頂點著色器,不斷更新三個旋轉角度,就能實現立方體旋轉的效果。
// ...
let rotationX = 0;
let rotationY = 0;
let rotationZ = 0;
function update() {rotationX += 0.003;rotationY += 0.005;rotationZ += 0.007;renderer.uniforms.modelMatrix = fromRotation(rotationX, rotationY, rotationZ);requestAnimationFrame(update);
}
update();
// ...
現在我們就能在旋轉中看到立方體的其他幾個面了,能更直觀地感受到這是一個三維物體。
總結
至此,我們就實現了正立方體的繪制。在3D物體的繪制中,正立方體屬于是比較簡單的一類,屏幕前的小伙伴們都可以來動手嘗試下,感興趣的小伙伴,還可以嘗試去實現圓柱體、正四面體等等這些幾何體的繪制。
文章轉載自:beckyye
原文鏈接:https://www.cnblogs.com/beckyyyy/p/18293794
體驗地址:引邁 - JNPF快速開發平臺_低代碼開發平臺_零代碼開發平臺_流程設計器_表單引擎_工作流引擎_軟件架構