在 4 中成功繪製了三角形以後,下面我們來載入一個 fbx 檔案,然後構建 MVP 變換(model-view-projection)。簡單介紹一下:
上面用大白話簡單描述了一下這幾個矩陣,相關資料有很多,本系列重在實踐,因為看再多的理論,不如自己親手實踐一下印象深刻,有時候不明白的原理,動手做一下就明白了。如果希望看相關的數學推導理論,證明之類的可以搜一搜有很多。我這裡提供一下我之前寫的關於變換的兩個文章:
下面來實踐一下,程式碼基於第 4 篇文章繼續完善。
完整的程式碼:https://github.com/MangoWAY/CGLearner/tree/v0.2,tag v0.2
在第 3 篇中介紹瞭如何安裝 pyassimp,這回我們來用一下,我們先定義一個簡單的 Mesh 和 SubMesh 類儲存載入的模型的資料,然後再定義一個模型載入類,用來載入資料,程式碼如下所示,比較簡單。
# mesh.py
class SubMesh:
def __init__(self, indices) -> None:
self.indices = indices
class Mesh:
def __init__(self) -> None:
self.vertices = []
self.normals = []
self.subMeshes = []
# model_importer.py
# pyassimp 4.1.4 has some problem will lead to randomly crash, use 4.1.3 to fix
# should set link path to find the dylib
import pyassimp
import numpy as np
from .mesh import Mesh, SubMesh
class ModelImporter:
def __init__(self) -> None:
pass
def load_mesh(self, path: str):
scene = pyassimp.load(path)
mmeshes = []
for mesh in scene.meshes:
mmesh = Mesh()
mmesh.vertices = np.reshape(np.copy(mesh.vertices), (1,-1)).squeeze(0)
print(mmesh.vertices)
mmesh.normals = np.reshape(np.copy(mesh.normals),(1,-1)).squeeze(0)
mmesh.subMeshes = []
mmesh.subMeshes.append(SubMesh(np.reshape(np.copy(mesh.faces), (1,-1)).squeeze(0)))
mmeshes.append(mmesh)
return mmeshes
Transform 用來描述物體的位置、旋轉、縮放資訊,可以說是比較基礎的,所以必不可少,詳細的解釋在程式碼的註釋裡。
import numpy as np
from scipy.spatial.transform import Rotation as R
class Transform:
def __init__(self) -> None:
# 為了簡單,目前我用尤拉角來儲存旋轉資訊
self._eulerAngle = [0,0,0]
self._pos = [0,0,0]
self._scale = [1,1,1]
# -- 都是常規的 get set,這裡略去
# ......
# 這就是我們所需要的 model 矩陣,注意這裡沒有考慮的物體的層級
# 關係,預設物體都是在最頂層,所以 local 和 world 座標是一樣
# 後續的文章會把層級關係考慮進來
def localMatrix(self):
# 按照 TRS 的構建方式
# 位移矩陣 * 旋轉矩陣 * 縮放矩陣
mat = np.identity(4)
# 對角線是縮放
for i in range(3):
mat[i,i] = self._scale[i]
rot = np.identity(4)
rot[:3,:3] = R.from_euler("xyz", self._eulerAngle, degrees = True).as_matrix()
mat = rot @ mat
for i in range(3):
mat[i,3] = self._pos[i]
return mat
# 將世界座標變換到當前物體的座標系下,注意這裡也是沒有考慮層級關係的
# 這個可以用來獲得從世界座標系到相機座標系的轉換。
def get_to_Local(self):
mat = self.localMatrix()
ori = np.identity(4)
ori[:3,:3] = mat[:3,:3]
ori = np.transpose(ori)
pos = np.identity(4)
pos[0:3,3] = -mat[0:3,3]
return ori @ pos
最後我們定義相機,目前相機的 Transform 資訊可以用來定義 View 矩陣,其他例如 fov 等主要用來定義投影矩陣。
from math import cos, sin
import math
import numpy as np
class Camera:
def __init__(self) -> None:
self._fov = 60
self._near = 0.3
self._far = 1000
self._aspect = 5 / 4
# -- 都是常規的 get set,這裡略去
# ......
# 完全參照投影矩陣的公式定義
def getProjectionMatrix(self):
r = math.radians(self._fov / 2)
cotangent = cos(r) / sin(r)
deltaZ = self._near - self._far
projection = np.zeros((4,4))
projection[0,0] = cotangent / self._aspect
projection[1,1] = cotangent
projection[2,2] = (self._near + self._far) / deltaZ
projection[2,3] = 2 * self._near * self._far / deltaZ
projection[3,2] = -1
return projection
完成了上述的步驟後,我們就可以構建 MVP 矩陣了。
...
# 定義物體的 transform
trans = transform.Transform()
trans.localPosition = [0,0,0]
trans.localScale = [0.005,0.005,0.005]
trans.localEulerAngle = [0,10,0]
# 獲取 model 矩陣
model = trans.localMatrix()
# 定義相機的 transform
viewTrans = transform.Transform()
viewTrans.localPosition = [0,2,2]
viewTrans.localEulerAngle = [-40,0,0]
# 獲取 view 矩陣
view = viewTrans.get_to_Local()
# 定義相機並獲得 projection 矩陣
cam = Camera()
proj = cam.getProjectionMatrix()
# 構建 MVP 矩陣
mvp = np.transpose(proj @ view @ model)
# 作為 uniform 傳入 shader 中,然後 shader 中將頂點位置乘上mvp矩陣。
mshader.set_mat4("u_mvp", mvp)
...
然後載入模型,構建一下頂點陣列和索引陣列,我給每個頂點額外新增了隨機的顏色
importer = ModelImporter()
meshes = importer.load_mesh("box.fbx")
vert = []
for i in range(len(meshes[0].vertices)):
if i % 3 == 0:
vert.extend([meshes[0].vertices[i],meshes[0].vertices[i + 1],meshes[0].vertices[i + 2]])
vert.extend([meshes[0].normals[i],meshes[0].normals[i + 1],meshes[0].normals[i + 2]])
vert.extend([random.random(),random.random(),random.random()])
inde = meshes[0].subMeshes[0].indices
# 開一下深度測試
gl.glEnable(gl.GL_DEPTH_TEST)
我們可以看一下最終效果。