Metal入门资料010-渲染3D物体

写在前面:

对Metal技术感兴趣的同学,可以关注我的专题:Metal专辑
也可以关注我个人的账号:张芳涛
所有的代码存储的Github地址是:Metal

正文

可能很多人都错过了MetalKit系列,所以今天我们又回到了它,我们将学习如何在Metal中绘制3D内容。 让我们继续在我们的playground上工作,并在系列文章的第8部分中找到我们离开的地方。

本篇博客最终是要渲染一个3D立方体,但首先让我们绘制一个2D正方形,然后我们可以为立方体的所有其他面重复使用正方形逻辑。 让我们修改vertex_data数组,使其保持4个顶点而不是3个三角形所需的顶点:

let vertex_data = [
Vertex(pos: [-1.0, -1.0, 0.0,  1.0], col: [1, 0, 0, 1]),
Vertex(pos: [ 1.0, -1.0, 0.0,  1.0], col: [0, 1, 0, 1]),
Vertex(pos: [ 1.0,  1.0, 0.0,  1.0], col: [0, 0, 1, 1]),
Vertex(pos: [-1.0,  1.0, 0.0,  1.0], col: [1, 1, 1, 1])
]

由于正方形和任何其他复杂几何体都是由三角形构成的,并且由于大多数顶点属于2个或更多个三角形,因此无需创建这些顶点的副本,因为我们有办法通过索引缓冲区重用它们来跟踪 通过存储顶点缓冲区中的每个顶点索引来使用顶点的顺序。 那么让我们创建一个索引列表:

let index_data: [UInt16] = [
0, 1, 2, 2, 3, 0
]
Metal入门资料010-渲染3D物体_第1张图片

因此,对于正面(正方形),我们使用存储在vertex_buffer中位置03的顶点。 稍后我们将添加其他4个顶点。 正面由两个三角形组成。 我们首先绘制使用顶点0,1和2的三角形,然后绘制使用顶点2,3和0的三角形。请注意,正如预期的那样,重复使用了两个顶点。 另请注意,绘图是顺时针完成的。 这是Metal中默认的前向卷绕顺序,但也可以改为逆时针方向。

然后,我们需要创建index_buffer

var index_buffer: MTLBuffer!

接下来,我们需要将index_data分配给createBuffers()函数内的index buffer(索引缓冲区):

index_buffer = device!.newBufferWithBytes(index_data, length: sizeof(UInt16) * index_data.count , options: [])

最后,在drawRect(:)函数内部,我们需要替换drawPrimitives调用:

command_encoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)

使用drawIndexedPrimitives调用:

command_encoder.drawIndexedPrimitives(.Triangle, indexCount: index_buffer.length / sizeof(UInt16), indexType: MTLIndexType.UInt16, indexBuffer: index_buffer, indexBufferOffset: 0)

代码写到这儿,在playground里面就可以看到生成的图像了

Metal入门资料010-渲染3D物体_第2张图片

现在我们知道如何绘制一个正方形,接下来让我们看看如何绘制多个正方形:

let vertex_data = [
Vertex(pos: [-1.0, -1.0,  1.0, 1.0], col: [1, 0, 0, 1]),
Vertex(pos: [ 1.0, -1.0,  1.0, 1.0], col: [0, 1, 0, 1]),
Vertex(pos: [ 1.0,  1.0,  1.0, 1.0], col: [0, 0, 1, 1]),
Vertex(pos: [-1.0,  1.0,  1.0, 1.0], col: [1, 1, 1, 1]),
Vertex(pos: [-1.0, -1.0, -1.0, 1.0], col: [0, 0, 1, 1]),
Vertex(pos: [ 1.0, -1.0, -1.0, 1.0], col: [1, 1, 1, 1]),
Vertex(pos: [ 1.0,  1.0, -1.0, 1.0], col: [1, 0, 0, 1]),
Vertex(pos: [-1.0,  1.0, -1.0, 1.0], col: [0, 1, 0, 1])
]
let index_data: [UInt16] = [
0, 1, 2, 2, 3, 0,   // front

1, 5, 6, 6, 2, 1,   // right

3, 2, 6, 6, 7, 3,   // top

4, 5, 1, 1, 0, 4,   // bottom

4, 0, 3, 3, 7, 4,   // left

7, 6, 5, 5, 4, 7,   // back

]

现在我们已经准备好渲染整个立方体几何体,让我们转到MathUtils.swift并在modelMatrix()中注释掉旋转和转换调用,并且仅将缩放开启0.5。 您很可能会看到这样的图像:

Metal入门资料010-渲染3D物体_第3张图片

嗯,但它仍然是一个正方形! 是的,它是一个正方形,因为我们仍然没有深度的概念,立方体看起来只是平坦的。 现在是时候调整一些数学逻辑了。 我们不再需要使用Matrix结构,因为simd框架为我们提供了类似的数据结构和我们可以轻松使用的数学函数。 我们可以轻松地重写变换函数以使用matrix_float4x4而不是我们使用的自定义Matrix结构。

但是你可能会问,3D对象如何最终出现在我们的2D屏幕上。 此过程通过一系列转换获取每个像素。 首先,modelMatrix()将像素从对象空间转换为世界空间。 这个矩阵是我们已经知道的,负责翻译,旋转和缩放的矩阵。 使用上面重新编写的新函数,modelMatrix可能如下所示:

func modelMatrix() -> matrix_float4x4 {
let scaled = scalingMatrix(0.5)
let rotatedY = rotationMatrix(Float(M_PI)/4, float3(0, 1, 0))
let rotatedX = rotationMatrix(Float(M_PI)/4, float3(1, 0, 0))
return matrix_multiply(matrix_multiply(rotatedX, rotatedY), scaled)
}

您会注意到之前我们无法使用的矩阵结构的有用的matrix_multiply函数。 此外,由于所有这些像素都将经历相同的变换,我们希望将矩阵存储为Uniform并将其传递给vertex shader(顶点着色器)。 为了这。 让我们创建一个新结构:

struct Uniforms {
var modelViewProjectionMatrix: matrix_float4x4
}

回到createBuffers()函数,让我们通过我们用于传递modelMatrix的缓冲区指针将Uniforms传递给着色器:

let modelViewProjectionMatrix = modelMatrix()
var uniforms = Uniforms(modelViewProjectionMatrix: modelViewProjectionMatrix)
memcpy(bufferPointer, &uniforms, sizeof(Uniforms))

到此为止,playground界面显示的图像应该是如下所示:

Metal入门资料010-渲染3D物体_第4张图片

为啥是这个鬼样子?

因为像素需要经历的下一个转换是从世界空间到相机空间。 我们在屏幕上看到的所有东西都是通过虚拟相机通过截头锥体(金字塔形状)观察到的,它具有近和远的平面以限制视图(相机)空间:

Metal入门资料010-渲染3D物体_第5张图片

回到MathUtils.swift,我们也创建了viewMatrix()

func viewMatrix() -> matrix_float4x4 {
let cameraPosition = vector_float3(0, 0, -3)
return translationMatrix(cameraPosition)
}

像素需要经历的下一个变换是从相机空间到剪辑空间。 这里,不在剪辑空间内的所有顶点将确定是否将剔除三角形(剪辑空间外的所有顶点)或剪切到边界(某些顶点在外部但不是全部)。 projectionMatrix()将帮助我们计算边界并确定顶点的位置:

func projectionMatrix(near: Float, far: Float, aspect: Float, fovy: Float) -> matrix_float4x4 {
let scaleY = 1 / tan(fovy * 0.5)
let scaleX = scaleY / aspect
let scaleZ = -(far + near) / (far - near)
let scaleW = -2 * far * near / (far - near)
let X = vector_float4(scaleX, 0, 0, 0)
let Y = vector_float4(0, scaleY, 0, 0)
let Z = vector_float4(0, 0, scaleZ, -1)
let W = vector_float4(0, 0, scaleW, 0)
return matrix_float4x4(columns:(X, Y, Z, W))
}

最后两个转换是从剪辑空间到标准化设备坐标(NDC)以及从NDC到屏幕空间。 这两个转换由我们的Metal框架处理。

接下来,回到createBuffers()函数,让我们将之前设置的modelViewProjectionMatrix修改为modelMatrix

let aspect = Float(drawableSize.width / drawableSize.height)
let projMatrix = projectionMatrix(1, far: 100, aspect: aspect, fovy: 1.1)
let modelViewProjectionMatrix = matrix_multiply(projMatrix, matrix_multiply(viewMatrix(), modelMatrix()))

drawRect(:)中,我们需要为剔除模式和前面板设置规则,以避免奇怪的工件,如立方体透明度:

command_encoder.setFrontFacingWinding(.CounterClockwise)

command_encoder.setCullMode(.Back)

再看看生成的图像长什么样:

Metal入门资料010-渲染3D物体_第6张图片

这终于是我们都在等着看的3D立方体! 我们还有一件事可以让它变得更加逼真和生动:给它一个旋转。 首先,让我们创建一个名为rotation的全局变量,我们希望随着时间的推移更新:

 var rotation: Float = 0

接下来,从createBuffers()函数中获取所有矩阵,然后创建一个名为update()的新矩阵。 这是我们每帧更新旋转以创建平滑旋转效果的位置:

func update() {
let scaled = scalingMatrix(0.5)
rotation += 1 / 100 * Float(M_PI) / 4
let rotatedY = rotationMatrix(rotation, float3(0, 1, 0))
let rotatedX = rotationMatrix(Float(M_PI) / 4, float3(1, 0, 0))
let modelMatrix = matrix_multiply(matrix_multiply(rotatedX, rotatedY), scaled)
let cameraPosition = vector_float3(0, 0, -3)
let viewMatrix = translationMatrix(cameraPosition)
let aspect = Float(drawableSize.width / drawableSize.height)
let projMatrix = projectionMatrix(0, far: 10, aspect: aspect, fovy: 1)
let modelViewProjectionMatrix = matrix_multiply(projMatrix, matrix_multiply(viewMatrix, modelMatrix))
let bufferPointer = uniform_buffer.contents()
var uniforms = Uniforms(modelViewProjectionMatrix: modelViewProjectionMatrix)
memcpy(bufferPointer, &uniforms, sizeof(Uniforms))
}

drawRect(:)中调用更新函数:

update()

再看看playground里面生成的图像:

搞定!

你可能感兴趣的:(Metal入门资料010-渲染3D物体)