当前位置: 首页 > news >正文

Metal入门,使用Metal绘制3D图形

这次是使用Metal绘制一个立方体,并且添加旋转效果,绘制正方形的步骤很简单,我们绘制一个正方形就相当于绘制两个三角形,那么绘制一个正方体,我们很容易想到需要绘制他六个面,很显然,我们也需要把二维坐标转换为三维坐标,这些都是一些基本需要修改的。
那么再来看看核心步骤:
从渲染一个2D正方形到渲染一个3D正方体,核心的增量步骤确实主要集中在以下两个方面:

深度测试 (Depth Testing):

  • 对于2D正方形:通常不需要深度测试,因为所有顶点都在同一个平面上(z=0),或者深度信息不影响最终显示。

  • 对于3D正方体:深度测试变得至关重要。因为它确保了立方体离观察者较近的面能够正确地遮挡较远的面,从而产生正确的3D视觉效果。没有深度测试,你可能会看到立方体“透视”的错误效果,比如背后的面错误地显示在前面的面之上。

    深度测试的步骤:

  1. 设置深度缓冲区格式 (depthStencilPixelFormat):
self.depthStencilPixelFormat = .depth32Float

这行代码在 configure() 方法中。它告诉 Metal MTKView 需要一个深度缓冲区,并且每个像素的深度值将使用32位浮点数来存储。深度缓冲区就像一张与颜色缓冲区(存储屏幕上显示的颜色)大小相同的“深度图”,它记录了每个像素距离相机的远近。

  1. 创建深度测试状态 (MTLDepthStencilState):
let depthStateDescriptor = MTLDepthStencilDescriptor()
depthStateDescriptor.depthCompareFunction = .less
depthStateDescriptor.isDepthWriteEnabled = true
depthStencilState = device?.makeDepthStencilState(descriptor: depthStateDescriptor)

这段代码也在 configure() 方法中。它创建并配置了一个 MTLDepthStencilState 对象,这个对象定义了深度测试如何进行:

  • depthStateDescriptor.depthCompareFunction = .less: 这是深度比较函数。当渲染一个新的像素时,Metal会比较这个新像素的深度值和深度缓冲区中对应位置已有的深度值。如果新像素的深度值“小于”(.less)已有的深度值(意味着新像素更靠近相机),那么新像素就会通过测试。
  • depthStateDescriptor.isDepthWriteEnabled = true: 这表示如果一个像素通过了深度测试,它的深度值就会被写入到深度缓冲区中,替换掉旧的深度值。
  1. 在渲染通道描述符中清除深度 (descriptor.depthAttachment.clearDepth):
descriptor.depthAttachment.clearDepth = 1.0

这行代码在 draw() 方法中。在每一帧开始渲染之前,深度缓冲区需要被清除为一个初始值。通常这个初始值设置为 1.0,它代表最远的深度。这样,场景中的第一个被渲染的物体(只要它在裁剪范围内)的任何部分都会比这个初始值更近,从而能够被写入。

  1. 在渲染命令编码器中设置深度测试状态:
encoder.setDepthStencilState(depthStencilState)

这行代码也在 draw() 方法中。它将我们之前创建的 depthStencilState 应用到当前的渲染命令编码器。这意味着后续的所有绘制命令都会遵循这个深度测试规则。

深度测试的作用:

当渲染3D场景时,物体之间可能会有遮挡。例如,一个立方体的一个面可能在另一个面的前面。深度测试就是用来解决这个问题的。

工作流程如下:

  1. 当Metal准备渲染一个像素(或者更准确地说,一个片段)时,它会计算出这个片段的深度值(通常是其在相机Z轴上的坐标,经过变换后)。
  2. 它会查找深度缓冲区中该像素位置已经存储的深度值。
  3. 根据 depthCompareFunction(在我们的例子中是 .less),比较新片段的深度和已存储的深度。
  4. 如果新片段通过了测试(即它比已有的片段更靠近相机),那么:
  • 新片段的颜色会被写入颜色缓冲区(显示在屏幕上)。
  • 如果 isDepthWriteEnabledtrue,新片段的深度值会更新到深度缓冲区。
  1. 如果新片段没有通过测试(即它比已有的片段更远或同样远),那么这个新片段就会被丢弃,颜色缓冲区和深度缓冲区都不会被修改。

通过这种方式,深度测试确保了只有离相机最近的物体表面才会被显示出来,从而正确地处理了3D场景中的遮挡关系,使得渲染结果看起来是正确的。没有深度测试,远处的物体可能会错误地绘制在近处物体的前面。

MVP模型 (Model-View-Projection Matrices):

  • 对于2D正方形:

  • 模型矩阵:可能只需要进行2D平移和缩放,或者简单的2D旋转。

  • 视图矩阵:通常是一个单位矩阵,或者一个简单的2D相机变换。

  • 投影矩阵:最常用的是正交投影(orthographic projection),它直接将坐标映射到屏幕,没有透视效果。

  • 对于3D正方体:

  • 模型矩阵:需要处理3D空间中的平移、旋转(可能绕任意轴)和缩放。

  • 视图矩阵:定义了3D相机的位置、观察目标和上方向,这决定了你从哪个角度和位置观察立方体。

  • 投影矩阵:通常使用透视投影(perspective projection),它模拟了人眼的视觉效果,即远处的物体看起来更小,近处的物体看起来更大,这是产生3D纵深感的关键。

    // 创建简单的模型视图投影矩阵private func makeModelViewProjection() -> matrix_float4x4 {// 旋转角度rotationAngle += 0.01// 创建模型矩阵 (旋转)let rotationMatrix = matrix_float4x4([cos(rotationAngle), 0, sin(rotationAngle), 0],[0, 1, 0, 0],[-sin(rotationAngle), 0, cos(rotationAngle), 0],[0, 0, 0, 1])// 创建视图矩阵let viewMatrix = matrix_float4x4([1, 0, 0, 0],[0, 1, 0, 0],[0, 0, 1, 0],[0, 0, -3, 1]  // 相机位置在z=-3)// 创建透视投影矩阵let aspect = Float(bounds.width / bounds.height)let fov = Float.pi / 3.0  // 60度let near: Float = 0.1let far: Float = 100.0let yScale = 1.0 / tan(fov * 0.5)let xScale = yScale / aspectlet zRange = far - nearlet zScale = -(far + near) / zRangelet wzScale = -2.0 * far * near / zRangelet projectionMatrix = matrix_float4x4([xScale, 0, 0, 0],[0, yScale, 0, 0],[0, 0, zScale, -1],[0, 0, wzScale, 0])// 组合为一个MVP矩阵return matrix_multiply(projectionMatrix, matrix_multiply(viewMatrix, rotationMatrix))}

这个 makeModelViewProjection() 函数的作用是创建和组合三个核心的变换矩阵,最终生成一个单一的 模型-视图-投影(MVP)矩阵。这个MVP矩阵在3D图形渲染中至关重要,它负责将3D模型的顶点坐标从模型空间转换到屏幕空间,从而让我们能够在2D屏幕上看到3D效果。

让我们分解一下这个函数中创建的三个主要矩阵:

  1. 模型矩阵 (Model Matrix):
  • 用途: 定义了3D模型自身的位置、旋转和缩放。在这个函数中,它创建了一个绕Y轴旋转的矩阵,使得立方体能够旋转。
  • rotationMatrix 就是这里的模型矩阵。每次调用 draw() 时,rotationAngle 都会增加,导致立方体持续旋转。
  1. 视图矩阵 (View Matrix):
  • 用途: 模拟相机的位置和朝向。它定义了我们从哪个角度观察场景。
  • viewMatrix 将相机放置在Z轴的-3位置([0, 0, -3, 1]),意味着相机在原点后方,朝向原点。
  1. 投影矩阵 (Projection Matrix):
  • 用途: 将3D场景投影到2D屏幕上,并创建透视效果(远处的物体看起来更小)。
  • projectionMatrix 在这里是一个透视投影矩阵。它考虑了以下因素:
    • aspect: 视图的宽高比,确保物体不会变形。
    • fov: 视野角度(Field of View),决定了相机能看到的范围。
    • nearfar: 近裁剪面和远裁剪面,定义了可见的深度范围。

最终组合:

函数最后通过 matrix_multiply 将这三个矩阵组合起来:
projectionMatrix * viewMatrix * rotationMatrix (即 P * V * M)

这个顺序非常重要:

  1. 首先,模型矩阵 (rotationMatrix) 应用于模型的顶点,将其从模型空间转换到世界空间(在世界坐标系中的位置和方向)。
  2. 然后,视图矩阵 (viewMatrix) 应用于世界空间的顶点,将其转换到相机空间(相对于相机的位置和方向)。
  3. 最后,投影矩阵 (projectionMatrix) 应用于相机空间的顶点,将其转换到裁剪空间,并最终映射到屏幕上的2D坐标。

总结:

makeModelViewProjection() 函数的核心作用是计算出这个最终的MVP矩阵。这个矩阵随后会被传递到顶点着色器中,顶点着色器会用它来变换立方体的每一个顶点,从而实现在屏幕上正确渲染出带有旋转和透视效果的3D立方体。没有这个函数和它生成的MVP矩阵,我们就无法将3D模型正确地显示在2D屏幕上。

import MetalKit
import simdstruct Constants {var animateBy: Float = 0.0var modelMatrix: matrix_float4x4 = matrix_identity_float4x4var viewMatrix: matrix_float4x4 = matrix_identity_float4x4var projectionMatrix: matrix_float4x4 = matrix_identity_float4x4
}struct Vertex {var position: SIMD4<Float>var color: SIMD4<Float>init(position: SIMD4<Float>, color: SIMD4<Float>) {self.position = positionself.color = color}
}class MetalView: MTKView {private var commandQueue: MTLCommandQueue!private var pipelineState: MTLRenderPipelineState!private var vertexBuffer: MTLBuffer!private var indexBuffer: MTLBuffer!private var constants = Constants()private var depthStencilState: MTLDepthStencilState?private var rotationAngle: Float = 0.0// 立方体顶点数据let cubeVertices = [// 前面 (z = 1.0)Vertex(position: SIMD4<Float>(-0.5, -0.5,  0.5, 1.0), color: SIMD4<Float>(1, 0, 0, 1)),Vertex(position: SIMD4<Float>( 0.5, -0.5,  0.5, 1.0), color: SIMD4<Float>(0, 1, 0, 1)),Vertex(position: SIMD4<Float>( 0.5,  0.5,  0.5, 1.0), color: SIMD4<Float>(0, 0, 1, 1)),Vertex(position: SIMD4<Float>(-0.5,  0.5,  0.5, 1.0), color: SIMD4<Float>(1, 1, 0, 1)),// 后面 (z = -0.5)Vertex(position: SIMD4<Float>(-0.5, -0.5, -0.5, 1.0), color: SIMD4<Float>(0, 0, 1, 1)),Vertex(position: SIMD4<Float>( 0.5, -0.5, -0.5, 1.0), color: SIMD4<Float>(1, 1, 1, 1)),Vertex(position: SIMD4<Float>( 0.5,  0.5, -0.5, 1.0), color: SIMD4<Float>(1, 0, 0, 1)),Vertex(position: SIMD4<Float>(-0.5,  0.5, -0.5, 1.0), color: SIMD4<Float>(0, 1, 0, 1))]// 索引数据 - 每个面由两个三角形组成let indices: [UInt16] = [// 前面0, 1, 2,2, 3, 0,// 右面1, 5, 6,6, 2, 1,// 上面3, 2, 6,6, 7, 3,// 下面4, 5, 1,1, 0, 4,// 左面4, 0, 3,3, 7, 4,// 后面5, 4, 7,7, 6, 5]override init(frame: CGRect, device: MTLDevice?) {super.init(frame: frame, device: device)self.device = device ?? MTLCreateSystemDefaultDevice()configure()}required init(coder: NSCoder) {fatalError("init(coder:) has not been implemented")}private func configure() {// 设置刷新率和渲染控制self.colorPixelFormat = .bgra8Unormself.isPaused = falseself.enableSetNeedsDisplay = falseself.preferredFramesPerSecond = 60// 设置深度缓冲区self.depthStencilPixelFormat = .depth32Float// 创建命令队列commandQueue = device?.makeCommandQueue()// 创建顶点缓冲区let vertexSize = MemoryLayout<Vertex>.stridelet vertexCount = cubeVertices.countvertexBuffer = device?.makeBuffer(bytes: cubeVertices, length: vertexCount * vertexSize, options: [])// 创建索引缓冲区let indexSize = MemoryLayout<UInt16>.sizelet indexCount = indices.countindexBuffer = device?.makeBuffer(bytes: indices, length: indexCount * indexSize,options: [])// 创建渲染管线guard let library = device?.makeDefaultLibrary(),let vertexFunc = library.makeFunction(name: "vertex_func"),let fragFunc = library.makeFunction(name: "fragment_func") else {fatalError("Failed to load shaders.")}let pipelineDescriptor = MTLRenderPipelineDescriptor()pipelineDescriptor.vertexFunction = vertexFunc pipelineDescriptor.fragmentFunction = fragFunc pipelineDescriptor.colorAttachments[0].pixelFormat = self.colorPixelFormat pipelineDescriptor.depthAttachmentPixelFormat = self.depthStencilPixelFormatdo {pipelineState = try device?.makeRenderPipelineState(descriptor: pipelineDescriptor)} catch {fatalError("Unable to create pipeline state: \(error)")}// 创建深度测试状态let depthStateDescriptor = MTLDepthStencilDescriptor()depthStateDescriptor.depthCompareFunction = .lessdepthStateDescriptor.isDepthWriteEnabled = truedepthStencilState = device?.makeDepthStencilState(descriptor: depthStateDescriptor)print("Metal初始化完成")}// 创建简单的模型视图投影矩阵private func makeModelViewProjection() -> matrix_float4x4 {// 旋转角度rotationAngle += 0.01// 创建模型矩阵 (旋转)let rotationMatrix = matrix_float4x4([cos(rotationAngle), 0, sin(rotationAngle), 0],[0, 1, 0, 0],[-sin(rotationAngle), 0, cos(rotationAngle), 0],[0, 0, 0, 1])// 创建视图矩阵let viewMatrix = matrix_float4x4([1, 0, 0, 0],[0, 1, 0, 0],[0, 0, 1, 0],[0, 0, -3, 1]  // 相机位置在z=-3)// 创建透视投影矩阵let aspect = Float(bounds.width / bounds.height)let fov = Float.pi / 3.0  // 60度let near: Float = 0.1let far: Float = 100.0let yScale = 1.0 / tan(fov * 0.5)let xScale = yScale / aspectlet zRange = far - nearlet zScale = -(far + near) / zRangelet wzScale = -2.0 * far * near / zRangelet projectionMatrix = matrix_float4x4([xScale, 0, 0, 0],[0, yScale, 0, 0],[0, 0, zScale, -1],[0, 0, wzScale, 0])// 组合为一个MVP矩阵return matrix_multiply(projectionMatrix, matrix_multiply(viewMatrix, rotationMatrix))}override func draw(_ rect: CGRect) {guard let commandBuffer = commandQueue.makeCommandBuffer(),let drawable = currentDrawable,let descriptor = currentRenderPassDescriptor else {return}// 计算MVP矩阵let mvpMatrix = makeModelViewProjection()// 更新常量缓冲区constants.modelMatrix = mvpMatrix// 设置清除颜色和深度descriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.2, 0.3, 0.3, 1.0)descriptor.depthAttachment.clearDepth = 1.0guard let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else {return}encoder.setRenderPipelineState(pipelineState)encoder.setDepthStencilState(depthStencilState)// 绘制立方体encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)encoder.setVertexBytes(&constants, length: MemoryLayout<Constants>.stride, index: 1)// 使用索引绘制encoder.drawIndexedPrimitives(type: .triangle,indexCount: indices.count,indexType: .uint16,indexBuffer: indexBuffer,indexBufferOffset: 0)encoder.endEncoding()commandBuffer.present(drawable)commandBuffer.commit()}
}
#include <metal_stdlib>using namespace metal;// 常量缓冲区
struct Constants {float animate_by;float4x4 modelMatrix;  // 现在这个是MVP矩阵float4x4 viewMatrix;   float4x4 projectionMatrix;
};// 顶点结构体 - 需要与Swift中的定义匹配
struct Vertex {float4 position;float4 color;
};// 顶点着色器输出/片段着色器输入结构体
struct VertexOut {float4 position [[position]];float4 color;
};vertex VertexOut vertex_func(device const Vertex* vertices [[buffer(0)]],constant Constants &constants [[buffer(1)]],uint vid [[vertex_id]])
{VertexOut out;// 使用MVP矩阵变换顶点out.position = constants.modelMatrix * vertices[vid].position;// 传递顶点颜色out.color = vertices[vid].color;return out;
}fragment float4 fragment_func(VertexOut in [[stage_in]]) {// 使用插值后的顶点颜色return in.color;
}

相关文章:

  • Java泛型 的详细知识总结
  • 【C# 自动化测试】Selenium显式等待机制详解
  • 考研系列-408真题计算机组成原理篇(2020-2023)
  • 如何利用 Java 爬虫根据 ID 获取某手商品详情:实战指南
  • Docker-Harbor 私有镜像仓库使用指南
  • 小白编程学习之巧解「消失的数字」
  • 2025年JIII SCI1区TOP,多策略霜冰优化算法IRIME+无人机路径规划,深度解析+性能实测
  • (2)JVM 内存模型更新与 G1 垃圾收集器优化
  • 电子科技大学软件工程实践期末
  • USB转TTL
  • 智能笔记助手-NotepadAI使用指南
  • 多线程(六)
  • RFID智能书柜:阅读新时代的智慧引擎
  • doris数据分片逻辑
  • Stack Queue
  • 跳空高低开策略思路
  • 蓝桥杯框架-按键数码管
  • Map更简洁的编码构建
  • 【Linux】48.高级IO(2)
  • vs2017编译ncnn库
  • 广西隆林发生一起山洪灾害,致4人遇难
  • 菲律宾华人“钢铁大王”撕票案两主谋落网,部分赎金已被提取
  • 外交部发言人就第78届世界卫生大会拒绝涉台提案发表谈话
  • 义乌至迪拜“铁海快线+中东快航”首发,物流成本降低18%
  • 国宝归来!子弹库帛书二、三卷抵达北京
  • 病愈出院、跳大神消灾也办酒,新华每日电讯:农村滥办酒席何时休