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

时间:2017-05-24 08:24:40

标签: swift3 triangulation metal frame-rate render-to-texture

我正在尝试将随机网格物体渲染到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来尽可能多地推送三角形?(在目前的状态下,应用程序吸引了大约30,000个在iPad Air 2)上以3 fps的三角形。

速度和帧率的任何指针/陷阱都是最受欢迎的

可以找到整个项目here

此外,以下是相关的更新金属类

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

1 个答案:

答案 0 :(得分:3)

您不应该从setupTriangles()致电renderTriangles()或特别是init()。根据你的评论,touchesBegan()也不是。通常,您只应在框架调用覆盖draw(_:)

时尝试绘制

如何更新用户事件取决于MTKView的绘图模式,如课程概述中所述。默认情况下,会定期调用draw(_:)方法。在这种模式下,您不必对touchesBegan()中的绘图做任何事情。只需更新课程内部状态,了解应该绘制的内容。实际绘图将在不久后自动发生。

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

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