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

作者: 张芳涛 | 来源:发表于2018-07-05 16:29 被阅读59次

    写在前面:

    对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
    ]
    

    因此,对于正面(正方形),我们使用存储在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里面就可以看到生成的图像了

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

    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。 您很可能会看到这样的图像:

    嗯,但它仍然是一个正方形! 是的,它是一个正方形,因为我们仍然没有深度的概念,立方体看起来只是平坦的。 现在是时候调整一些数学逻辑了。 我们不再需要使用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界面显示的图像应该是如下所示:

    为啥是这个鬼样子?

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

    回到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)
    

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

    这终于是我们都在等着看的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物体

        本文链接:https://www.haomeiwen.com/subject/gmpgyftx.html