引言
Hi, 大家好,我是一牛。相信认真阅读过我之前的 Metal 系列文章的朋友们,已经打下了扎实的 Metal 基础。这些基础知识将成为我们深入学习更高级内容的坚实基石。因此,如果同学们还不太理解这些基础内容,请再好好的研究一番。从现在起,我们要进入3D 渲染的学习,探索更真实的世界!
今天,我将教会大家如何实现正交投影,将三维世界的物体显示在屏幕上。
模型矩阵
模型矩阵常用于将顶点从模型空间转换到世界坐标。它包含了以下变换
-
平移
-
旋转
-
缩放
在本例中我们将图片的顶点绕Y轴转动,对应的矩阵是:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> R y ( θ ) = [ cos ( θ ) 0 sin ( θ ) 0 0 1 0 0 − sin ( θ ) 0 cos ( θ ) 0 0 0 0 1 ] \mathbf{R}_y(\theta) = \begin{bmatrix} \cos(\theta) & 0 & \sin(\theta) & 0 \\ 0 & 1 & 0 & 0 \\ -\sin(\theta) & 0 & \cos(\theta) & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} </math>Ry(θ)= cos(θ)0−sin(θ)00100sin(θ)0cos(θ)00001
swift
// angle 是弧度
func rotateY(angle: Float) -> simd_float4x4 {
let cos = cosf(angle)
let sin = sinf(angle)
var matrix = matrix_identity_float4x4
matrix.columns.0 = simd_float4(cos, 0, -sin, 0)
matrix.columns.2 = simd_float4(sin, 0, cos, 0)
return matrix
}
视图矩阵
视图矩阵用于将顶点从世界空间转换到相机空间(观察空间)。描述了相机的位置和方向。因此视图矩阵需要以下步骤构建:
- 相机的位置 eye
- 相机视线方向 g
- 相机的向上方向 up
我们需要将相机的位置eye 挪到原点 (0,0,0) ,将相机的视线方向指向 +Z (0,0,1) 方向,将相机的向上方向 up 指向 +Y (0,1,0) , 最后将 g 叉乘 up (使用r向量表示 )指向+X (1,0,0) 方向。
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> V = [ r x r y r z − ( r ⋅ e y e ) u p x u p y u p z − ( u p ⋅ e y e ) g x g y g z − ( g ⋅ e y e ) 0 0 0 1 ] \mathbf{V} = \begin{bmatrix} r_x & r_y & r_z & -(\mathbf{r} \cdot \mathbf{eye}) \\ up_x & up_y & up_z & -(\mathbf{up} \cdot \mathbf{eye}) \\ g_x & g_y & g_z & -(\mathbf{g} \cdot \mathbf{eye}) \\ 0 & 0 & 0 & 1 \end{bmatrix} </math>V= rxupxgx0ryupygy0rzupzgz0−(r⋅eye)−(up⋅eye)−(g⋅eye)1
swift
private func viewMatrix(eye: SIMD3<Float>, center: SIMD3<Float>, up: SIMD3<Float>) -> matrix_float4x4 {
let g = normalize(center - eye)
let gazeCrossUp = normalize(cross(g, up))
var matrix = matrix_identity_float4x4
matrix.columns.0 = vector_float4(gazeCrossUp.x, up.x, g.x, 0)
matrix.columns.1 = vector_float4(gazeCrossUp.y, up.y, g.y, 0)
matrix.columns.2 = vector_float4(gazeCrossUp.z, up.z, g.z, 0)
matrix.columns.3 = vector_float4(-dot(gazeCrossUp, eye),-dot(up, eye), -dot(g, eye), 1)
return matrix
}
正交投影矩阵
所谓正交投影,也叫平行投影,我们在中学也学过,用于将三维场景中的点映射到二维平面上。它的特点是投影过程中不会产生透视效果,物体的比例在投影前后保持不变,与距离无关。
正交投影矩阵通常需要以下步骤得到:
-
创建一个长方体,使用left、right、top、bottom、far、near分别表示左平面、右平面、上平面、下平面、近平面和远平面。
-
将长方体的近平面中心点平移到原点。
-
将长方体做缩放变化,X方向缩放到[-1, 1]、Y方向缩放到[-1, 1]、Z方向[0, 1]
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> [ 2.0 r − l 0 0 − r + l r − l 0 2.0 t − b 0 − t + b t − b 0 0 1.0 f − n − n f − n 0 0 0 1.0 ] \begin{bmatrix} \frac{2.0}{\text{r} - \text{l}} & 0 & 0 & -\frac{\text{r} + \text{l}}{\text{r} - \text{l}} \\ 0 & \frac{2.0}{\text{t} - \text{b}} & 0 & -\frac{\text{t} + \text{b}}{\text{t} - \text{b}} \\ 0 & 0 & \frac{1.0}{\text{f} - \text{n}} & -\frac{\text{n}}{\text{f} - \text{n}} \\ 0 & 0 & 0 & 1.0 \end{bmatrix} </math> r−l2.00000t−b2.00000f−n1.00−r−lr+l−t−bt+b−f−nn1.0
swift
func createOrthographicMatrix(left: Float, right: Float, bottom: Float, top: Float, nearPlane: Float, farPlane: Float) -> simd_float4x4 {
var matrix = simd_float4x4(0.0)
matrix[0][0] = 2.0 / (right - left)
matrix[0][3] = -(right + left) / (right - left)
matrix[1][1] = 2.0 / (top - bottom)
matrix[1][3] = -(top + bottom) / (top - bottom)
matrix[2][2] = 1.0 / (farPlane - nearPlane)
matrix[2][3] = -nearPlane / (farPlane - nearPlane)
matrix[3][3] = 1.0
return matrix
}
应用投影变换
swift
let radians = rotationAngle * .pi / 180
let projectionMatrix = createOrthographicMatrix(left: -1, right: 1, bottom: -1, top: 1, nearPlane: 0.1, farPlane: 100)
let rotationMatrix = rotateY(angle: radians)
let viewMatrix = viewMatrix(eye: SIMD3<Float>(0, 0, -5), center: SIMD3<Float>(0, 0, 0), up: SIMD3<Float>(0, 1, 0))
var uniform = Uniforms(projectionMatrix: projectionMatrix, rotationMatrix: rotationMatrix, viewMatrix: viewMatrix)
commanderEncoder?.setVertexBytes(&uniform, length: MemoryLayout<Uniforms>.stride, index: 1)
c++
// 着色器函数
struct Uniforms {
float4x4 projectionMatrix;
float4x4 rotationMatrix;
float4x4 viewMatrix;
};
vertex RasterizerData vertexShader(uint vertexID [[vertex_id]],
constant Vertex *vertices [[buffer(0)]],
constant Uniforms &uniforms [[buffer(1)]]
) {
RasterizerData out;
out.position = uniforms.projectionMatrix * uniforms.viewMatrix * uniforms.rotationMatrix * vertices[vertexID].position;
out.textureCoordinate = vertices[vertexID].textureCoordinate;
return out;
}
将Model-View-Projection矩阵传递给着色器,按照顺序 模型变换 -> 视图变换 -> 投影变换。
结语
通过Model-View-Projection变换,我们可以将三维世界的点映射到二维屏幕上。而学好正交投影变换也为我们继续学习透视投影变换的打下扎实基础。
本项目已开源 欢迎大家点赞、收藏。