使用 Metal 和 Swift 渲染快速变化的任意大小的网格 3

Rendering rapidly-changing arbitrary-sized meshes with Metal and Swift 3

我正在尝试以设备允许的最快速度将随机网格渲染到 MTKView。我发现的几乎所有金属示例都展示了如何绘制缓冲区大小仅定义一次的几何体(即固定):

let dataSize = vertexCount * MemoryLayout<VertexWithColor>.size // size of the vertex data in bytes
let vertexBuffer: MTLBuffer = device!.makeBuffer(bytes: verticesWithColorArray, length: dataSize, options: []) // create a new buffer on the GPU

目标是最终在给定一些点云输入的情况下动态生成网格。我已将绘图设置为通过点击触发,如下所示:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    if let touch = touches.first  {
      let touchPoint = touch.location(in: view)
      print ("...touch \(touchPoint)")

      autoreleasepool {
        delaunayView.setupTriangles()
        delaunayView.renderTriangles()
      }
    }
  }

我可以让屏幕刷新新的三角形,只要我不经常点击。但是,如果我点击得太快(比如双击),应用程序会崩溃并出现以下错误:

[CAMetalLayerDrawable texture] should not be called after presenting the drawable.

性能显然与绘制的三角形数量有关。除了让应用程序稳定运行之外,同样重要的问题是,我如何才能最好地利用 GPU 来 推送尽可能多的三角形?(在当前状态下,应用程序在 iPad Air 2) 上以 3 fps 的速度绘制大约 30,000 个三角形。

任何关于速度和帧率的 pointers/gotchas 都是最受欢迎的

整个项目可以找到here:

此外,下面是相关的更新金属class

import Metal
import MetalKit
import GameplayKit

protocol MTKViewDelaunayTriangulationDelegate: NSObjectProtocol{  
  func fpsUpdate (fps: Int)
}

class MTKViewDelaunayTriangulation: MTKView {

  //var kernelFunction: MTLFunction!
  var pipelineState: MTLComputePipelineState!
  var defaultLibrary: MTLLibrary! = nil
  var commandQueue: MTLCommandQueue! = nil
  var renderPipeline: MTLRenderPipelineState!
  var errorFlag:Bool = false

  var verticesWithColorArray : [VertexWithColor]!
  var vertexCount: Int
  var verticesMemoryByteSize:Int

  let fpsLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 400, height: 20))
  var frameCounter: Int = 0
  var frameStartTime = CFAbsoluteTimeGetCurrent()


  weak var MTKViewDelaunayTriangulationDelegate: MTKViewDelaunayTriangulationDelegate?

  ////////////////////
  init(frame: CGRect) {

    vertexCount = 100000
    //verticesMemoryByteSize = vertexCount * MemoryLayout<VertexWithColor>.size
    verticesMemoryByteSize = vertexCount * MemoryLayout<VertexWithColor>.stride // apple recommendation
    super.init(frame: frame, device: MTLCreateSystemDefaultDevice())

    setupMetal()
    //setupTriangles()
    //renderTriangles()
  }

  required init(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  /*
  override func draw(_ rect: CGRect) {

    step() // needed to update frame counter

    autoreleasepool {
      setupTriangles()
      renderTriangles()

    }

  } */


  func step() {
    frameCounter += 1
    if frameCounter == 100
    {
      let frametime = (CFAbsoluteTimeGetCurrent() - frameStartTime) / 100
      MTKViewDelaunayTriangulationDelegate?.fpsUpdate(fps: Int(1 / frametime)) // let the delegate know of the frame update
      print ("...frametime: \((Int(1/frametime)))")
      frameStartTime = CFAbsoluteTimeGetCurrent() // reset start time
      frameCounter = 0 // reset counter
    }
  }

  func setupMetal(){

    // Steps required to set up metal for rendering:

    // 1. Create a MTLDevice
    // 2. Create a Command Queue
    // 3. Access the custom shader library
    // 4. Compile shaders from library
    // 5. Create a render pipeline
    // 6. Set buffer size of objects to be drawn
    // 7. Draw to pipeline through a renderCommandEncoder


    // 1. Create a MTLDevice
    guard let device = MTLCreateSystemDefaultDevice() else {
      errorFlag = true
      //particleLabDelegate?.particleLabMetalUnavailable()
      return
    }

    // 2. Create a Command Queue
    commandQueue = device.makeCommandQueue()

    // 3. Access the custom shader library
    defaultLibrary = device.newDefaultLibrary()

    // 4. Compile shaders from library
    let fragmentProgram = defaultLibrary.makeFunction(name: "basic_fragment")
    let vertexProgram = defaultLibrary.makeFunction(name: "basic_vertex")

    // 5a. Define render pipeline settings
    let renderPipelineDescriptor = MTLRenderPipelineDescriptor()
    renderPipelineDescriptor.vertexFunction = vertexProgram
    renderPipelineDescriptor.sampleCount = self.sampleCount
    renderPipelineDescriptor.colorAttachments[0].pixelFormat = self.colorPixelFormat
    renderPipelineDescriptor.fragmentFunction = fragmentProgram

    // 5b. Compile renderPipeline with above renderPipelineDescriptor
    do {
      renderPipeline = try device.makeRenderPipelineState(descriptor: renderPipelineDescriptor)
    } catch let error as NSError {
      print("render pipeline error: " + error.description)
    }

    // initialize counter variables
    frameStartTime = CFAbsoluteTimeGetCurrent()
    frameCounter = 0

  } // end of setupMetal

  /// Generate set of vertices for our triangulation to use
  func generateVertices(_ size: CGSize, cellSize: CGFloat, variance: CGFloat = 0.75, seed: UInt64 = numericCast(arc4random())) -> [Vertex] {

    // How many cells we're going to have on each axis (pad by 2 cells on each edge)
    let cellsX = (size.width + 4 * cellSize) / cellSize
    let cellsY = (size.height + 4 * cellSize) / cellSize

    // figure out the bleed widths to center the grid
    let bleedX = ((cellsX * cellSize) - size.width)/2
    let bleedY = ((cellsY * cellSize) - size.height)/2

    let _variance = cellSize * variance / 4

    var points = [Vertex]()
    let minX = -bleedX
    let maxX = size.width + bleedX
    let minY = -bleedY
    let maxY = size.height + bleedY

    let generator = GKLinearCongruentialRandomSource(seed: seed)

    for i in stride(from: minX, to: maxX, by: cellSize) {
      for j in stride(from: minY, to: maxY, by: cellSize) {

        let x = i + cellSize/2 + CGFloat(generator.nextUniform()) + CGFloat.random(-_variance, _variance)
        let y = j + cellSize/2 + CGFloat(generator.nextUniform()) + CGFloat.random(-_variance, _variance)

        points.append(Vertex(x: Double(x), y: Double(y)))
      }
    }

    return points
  } // end of generateVertices

  func setupTriangles(){

    // generate n random triangles
    ///////////////////
    verticesWithColorArray = [] // empty out vertex array

    for _ in 0 ... vertexCount {
      //for vertex in vertices {
      let x = Float(Double.random(-1.0, 1.0))
      let y = Float(Double.random(-1.0, 1.0))
      let v = VertexWithColor(x: x, y: y, z: 0.0, r: Float(Double.random()), g: Float(Double.random()), b: Float(Double.random()), a: 0.0)

      verticesWithColorArray.append(v)
    } // end of for _ in



  } // end of setupTriangles

  func renderTriangles(){
    // 6. Set buffer size of objects to be drawn
    //let dataSize = vertexCount * MemoryLayout<VertexWithColor>.size // size of the vertex data in bytes
    let dataSize = vertexCount * MemoryLayout<VertexWithColor>.stride // apple recommendation
    let vertexBuffer: MTLBuffer = device!.makeBuffer(bytes: verticesWithColorArray, length: dataSize, options: []) // create a new buffer on the GPU
    let renderPassDescriptor: MTLRenderPassDescriptor? = self.currentRenderPassDescriptor

    // If the renderPassDescriptor is valid, begin the commands to render into its drawable
    if renderPassDescriptor != nil {
      // Create a new command buffer for each tessellation pass

      let commandBuffer: MTLCommandBuffer? = commandQueue.makeCommandBuffer()
      // Create a render command encoder
      // 7a. Create a renderCommandEncoder four our renderPipeline
      let renderCommandEncoder: MTLRenderCommandEncoder? = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor!)
      renderCommandEncoder?.label = "Render Command Encoder"
      //////////renderCommandEncoder?.pushDebugGroup("Tessellate and Render")
      renderCommandEncoder?.setRenderPipelineState(renderPipeline!)
      renderCommandEncoder?.setVertexBuffer(vertexBuffer, offset: 0, at: 0)
      // most important below: we tell the GPU to draw a set of triangles, based on the vertex buffer. Each triangle consists of three vertices, starting at index 0 inside the vertex buffer, and there are vertexCount/3 triangles total
      //renderCommandEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount, instanceCount: vertexCount/3)
      renderCommandEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount)

      ///////////renderCommandEncoder?.popDebugGroup()
      renderCommandEncoder?.endEncoding() // finalize renderEncoder set up

      commandBuffer?.present(self.currentDrawable!) // needed to make sure the new texture is presented as soon as the drawing completes

      // 7b. Render to pipeline
      commandBuffer?.commit() // commit and send task to gpu

    } // end of if renderPassDescriptor 

  }// end of func renderTriangles()


} // end of class MTKViewDelaunayTriangulation

您不应该从 init() 呼叫 setupTriangles(),尤其是 renderTriangles()。根据您的评论,也不是 touchesBegan()。通常,您应该仅在框架调用您对 draw(_:).

的覆盖时尝试绘制

如何更新用户事件取决于 MTKView 的绘图模式,如 class 概述中所述。默认情况下,您的 draw(_:) 方法会定期调用。在这种模式下,您不必对 touchesBegan() 中的绘图进行任何操作。只需更新 class 关于它应该绘制什么的内部状态。实际绘图将在短时间后自动进行。

如果您已将视图配置为在 setNeedsDisplay() 之后重绘,则 touchesBegan() 应该更新内部状态然后调用 setNeedsDisplay()。它不应该尝试立即绘制。在您 return 控制权回到框架后不久(即从 touchesBegan() 返回 return),它会为您调用 draw(_:)

如果您已将视图配置为仅在显式调用 draw() 时绘制,那么您将在更新内部状态后执行此操作。