---页首---

Metal 练习:第三篇-添加Texture

Metal 练习:第三篇-添加Texture

此篇练习是基于前一篇 Metal 练习:第二篇-3D 的拓展

此篇练习完成后,将会学到如何给立方体添加Texture,过程中还会学到:

  • 如何重用 uniform buffers
  • 如何给3D模型使用Texture
  • 如何给应用添加触控输入
  • 如何调试Metal

第一步

打开Metal 练习:第二篇-3D实现的工程,虽然前面已经对ViewController类作了重构,但仍然分工不是很明确,在此在其拆分为两个类:

MetalViewController:包含通用的Metal设置代码的基类
ViewController:包含创建和渲染模型代码特定用于当前应用的子类
拆分之后这里新增加了一个协议MetalViewControllerDelegate,让ViewController作为代理并实现方法,这是就是渲染和更新要处理的地方

protocol MetalViewControllerDelegate: class {
   func updateLogic(timeSinceLastUpdate: CFTimeInterval)
   func renderObjects(drawable: CAMetalDrawable)
}

重用 uniform buffers

问题

在上一篇练习中,知道如何给每一帧分配新的uniform buffers,但这样做的效率不好。主要是我们的帧率是60FPS,也就就Node.swift中的render()方法每秒会调用60次,其中uniformBuffer就会创建多少次,如下图可见相关数据
Metal-Uniform-Buffers-Pre

方案

用一个缓存池来代替每次分配一个缓存,为代码耦合度低,可以封装所有创建和重用的逻辑为一个类BufferProvider,负责创建一个缓存池,同时提供一个方法去获取下一个可重用的buffer,此类的功能如下图
Metal-Buffer-Provider

// BufferProvider 类添加如下代码
import Metal
class BufferProvider {
    // 1. 缓存池存放buffer的数量
    let inflightBuffersCount: Int
    // 2. 用来存储自己的buffers
    private var uniformsBuffers: [MTLBuffer]
    // 3. 下一个可用的buffer的index
    private var avaliableBufferIndex: Int = 0

    init(device: MTLDevice, inflightBuffersCount: Int, sizeOfUniformsBuffer: Int) {
        self.inflightBuffersCount = inflightBuffersCount
        uniformsBuffers = [MTLBuffer]()
        
        for _ in 0 ... inflightBuffersCount - 1 {
            let uniformsBuffer = device.makeBuffer(length: sizeOfUniformsBuffer, options: [])!
            uniformsBuffers.append(uniformsBuffer)
        }
    }
     // 获取下一个可用的buffer
    func nextUniformsBuffer(projectionMatrix: Matrix4, modelViewMatrix: Matrix4) -> MTLBuffer {
        // 1.从缓存数组中获取buffer
        let buffer = uniformsBuffers[avaliableBufferIndex]
        // 2.获取 `void *` 指针
        let bufferPointer = buffer.contents()
        // 3.将传进的矩阵拷贝到buffer中
        memcpy(bufferPointer, modelViewMatrix.raw(), MemoryLayout<Float>.size * Matrix4.numberOfElements())
        memcpy(bufferPointer + MemoryLayout<Float>.size * Matrix4.numberOfElements(), projectionMatrix.raw(), MemoryLayout<Float>.size * Matrix4.numberOfElements())
        // 4.更新avaliableBufferIndex
        avaliableBufferIndex += 1
        if avaliableBufferIndex == inflightBuffersCount {
            avaliableBufferIndex = 0
        }
        return buffer
    }
}
// 在 Node.swift 文件中
// 添加一个属性
var bufferProvider: BufferProvider
// 在 init 方法末尾添加
self.bufferProvider = BufferProvider(device: device, inflightBuffersCount: 3, sizeOfUniformsBuffer: MemoryLayout<Float>.size * Matrix4.numberOfElements() * 2)
// 然后将以下几代码
let uniformBuffer = device.makeBuffer(length: MemoryLayout<Float>.size * Matrix4.numberOfElements() * 2, options: [])!
let bufferPointer = uniformBuffer.contents()
memcpy(bufferPointer, nodeModelMatrix.raw(), MemoryLayout<Float>.size * Matrix4.numberOfElements())
memcpy(bufferPointer + MemoryLayout<Float>.size * Matrix4.numberOfElements(), projectionMatrix.raw(), MemoryLayout<Float>.size * Matrix4.numberOfElements())
// 替换为下面这句
let uniformBuffer = bufferProvider.nextUniformsBuffer(projectionMatrix: projectionMatrix, modelViewMatrix: nodeModelMatrix)

竞争

到此程序可以正常且丝滑的运行起来,但这里隐藏了一个问题,如下图
Metal-CPU-GPU-Competition

当CPU获取到下一个可用的buffer,填充到数据,然后发送到GPU处理,但此时可能数据还在上一轮的处理没有结束。因为CPU的处理速度远比GPU的要快的多。当然可以增加缓存的数量来减少这种竞争,但这也不能100%避免。对于此种情况,最好的办法就是使用信号量。信号量允许你跟踪有限可用资源的数量,当没有更多可用资源时阻塞。在本例中使用信号量如下:

在初始化buffers的数量时,初始化信号量
访问buffer前要等待。当访问buffer时,要先询问下信号量是否等待,如果所有的buffers都在使用,那此时会阻塞,直到有可用的buffer,当释放一个buffer信号量也会-1。
当处理完一个buffer就发一个信号出来,又有可用的buffer了

// 添加信号量的代码
// 在 BufferProvider.swift 中添加一个属性并在 init 方法中初始化
var avaliableResourcesSemaphore: DispatchSemaphore
self.avaliableResourcesSemaphore = DispatchSemaphore(value: inflightBuffersCount)
// 然后在 Node.swift 中的 render() 方法最上面加上下面这句,将会使CPU等待到有空闲的资源
_ = bufferProvider.avaliableResourcesSemaphore.wait(timeout: .distantFuture)
// 还在 render() 方法中 let commandBuffer = commandQueue.makeCommandBuffer()! 下面加上一句
commandBuffer.addCompletedHandler { (_) in
    self.bufferProvider.avaliableResourcesSemaphore.signal()
}
// 当对象销毁时要记得清空信号量,否则当信号量一直在等待,你销毁了对象,就会crash
 deinit {
    for _ in 0 ... self.inflightBuffersCount {
        self.avaliableResourcesSemaphore.signal()
    }
}

Texture

什么是Texture?简单来说,就是映射到3D模型的2D图像。
与OpenGL左下角原点相反,Metal的原点在右左上角。通常坐标轴记为s,t。如下图
Metal-Texture-Coordinate-Label

为了区分iOS设备的像素与纹理像素,称纹理像素为纹理元素,纹理有512x512个纹理元素,且使用归一化坐标系(范围0~1)。因此 左上角:(0.0, 0.0),左下角:(0.0, 1.0),右上角:(1.0, 0.0),右下角:(1.0, 1.0)。当然不强制使用归一化坐标系,但使用这个归一化坐标系有好处。例如,当你想转换纹理的分辨率为256x256时,只要新的纹理映射正确,就会正常工作。

在Metal中使用Texture

在Metal中,只要遵循MTLTexture协议的任何对象都可代表纹理。Metal中有无数类型的纹理,但是现在我们只需要MTLTexture2D这个类型。
另外一个重要的协议MTLSamplerState,遵循此协议的对象指导CPU如何使用纹理。当传入一个纹理时,同时也会传入一个取样器。
下图简单说明如何使用texture
Metal-Texture-Use

MetalTexture

为了方便使用texture,创建了一个MetalTexture类,可以参考MetalByExample.com。类中有两个重要的方法

init(resourceName: String, ext: String, mipmaped: Bool):传入文件名及扩展名,然后是否要mipmaps
func loadTexture(device: MTLDevice, commandQ: MTLCommandQueue, flip: Bool): 当实际要创建MTLTexture时调用。

What is mipmap ??? 当mipmap = true时加载texture,会生成一个图像数组代替单个图像,并且数组中的每个图像是前面一个的1/2。GPU会自动选择最佳的mip级别来读取像素

整合代码

// 在Node.swift中添加两个新的成员
var texture: MTLTexture
lazy var samplerState: MTLSamplerState? = Node.defaultSampler(device: self.device)
// 添加一个类方法,生成一个简单的纹理采样器,包含一堆标志
class func defaultSampler(device: MTLDevice) -> MTLSamplerState {
  let sampler = MTLSamplerDescriptor()
  sampler.minFilter             = MTLSamplerMinMagFilter.nearest
  sampler.magFilter             = MTLSamplerMinMagFilter.nearest
  sampler.mipFilter             = MTLSamplerMipFilter.nearest
  sampler.maxAnisotropy         = 1
  sampler.sAddressMode          = MTLSamplerAddressMode.clampToEdge
  sampler.tAddressMode          = MTLSamplerAddressMode.clampToEdge
  sampler.rAddressMode          = MTLSamplerAddressMode.clampToEdge
  sampler.normalizedCoordinates = true
  sampler.lodMinClamp           = 0
  sampler.lodMaxClamp           = FLT_MAX
  return device.makeSamplerState(descriptor: sampler)!
}
// 在该类的 render() 方法中的  renderEncoder.setVertexBuffer(self.vertexBuffer, offset: 0, index: 0) 语句下面添加
renderEncoder.setFragmentTexture(texture, index: 0)
renderEncoder.setFragmentSamplerState(samplerState, index: 0)
// 修改 init 方法
init(name: String, vertices: Array<Vertex>, device: MTLDevice, texture: MTLTexture) {
// 且在最后给 texture 赋值
self.texture = texture
// 修改 Vertex 类如下
struct Vertex{
  
  var x,y,z: Float     // position data
  var r,g,b,a: Float   // color data
  var s,t: Float       // texture coordinates
  
  func floatBuffer() -> [Float] {
    return [x,y,z,r,g,b,a,s,t]
  }
  
}
// 修改 Cubic 的 init 方法
init(device: MTLDevice, commandQ: MTLCommandQueue){
  // 1

  //Front
  let A = Vertex(x: -1.0, y:   1.0, z:   1.0, r:  1.0, g:  0.0, b:  0.0, a:  1.0, s: 0.25, t: 0.25)
  let B = Vertex(x: -1.0, y:  -1.0, z:   1.0, r:  0.0, g:  1.0, b:  0.0, a:  1.0, s: 0.25, t: 0.50)
  let C = Vertex(x:  1.0, y:  -1.0, z:   1.0, r:  0.0, g:  0.0, b:  1.0, a:  1.0, s: 0.50, t: 0.50)
  let D = Vertex(x:  1.0, y:   1.0, z:   1.0, r:  0.1, g:  0.6, b:  0.4, a:  1.0, s: 0.50, t: 0.25)

  //Left
  let E = Vertex(x: -1.0, y:   1.0, z:  -1.0, r:  1.0, g:  0.0, b:  0.0, a:  1.0, s: 0.00, t: 0.25)
  let F = Vertex(x: -1.0, y:  -1.0, z:  -1.0, r:  0.0, g:  1.0, b:  0.0, a:  1.0, s: 0.00, t: 0.50)
  let G = Vertex(x: -1.0, y:  -1.0, z:   1.0, r:  0.0, g:  0.0, b:  1.0, a:  1.0, s: 0.25, t: 0.50)
  let H = Vertex(x: -1.0, y:   1.0, z:   1.0, r:  0.1, g:  0.6, b:  0.4, a:  1.0, s: 0.25, t: 0.25)

  //Right
  let I = Vertex(x:  1.0, y:   1.0, z:   1.0, r:  1.0, g:  0.0, b:  0.0, a:  1.0, s: 0.50, t: 0.25)
  let J = Vertex(x:  1.0, y:  -1.0, z:   1.0, r:  0.0, g:  1.0, b:  0.0, a:  1.0, s: 0.50, t: 0.50)
  let K = Vertex(x:  1.0, y:  -1.0, z:  -1.0, r:  0.0, g:  0.0, b:  1.0, a:  1.0, s: 0.75, t: 0.50)
  let L = Vertex(x:  1.0, y:   1.0, z:  -1.0, r:  0.1, g:  0.6, b:  0.4, a:  1.0, s: 0.75, t: 0.25)

  //Top
  let M = Vertex(x: -1.0, y:   1.0, z:  -1.0, r:  1.0, g:  0.0, b:  0.0, a:  1.0, s: 0.25, t: 0.00)
  let N = Vertex(x: -1.0, y:   1.0, z:   1.0, r:  0.0, g:  1.0, b:  0.0, a:  1.0, s: 0.25, t: 0.25)
  let O = Vertex(x:  1.0, y:   1.0, z:   1.0, r:  0.0, g:  0.0, b:  1.0, a:  1.0, s: 0.50, t: 0.25)
  let P = Vertex(x:  1.0, y:   1.0, z:  -1.0, r:  0.1, g:  0.6, b:  0.4, a:  1.0, s: 0.50, t: 0.00)

  //Bot
  let Q = Vertex(x: -1.0, y:  -1.0, z:   1.0, r:  1.0, g:  0.0, b:  0.0, a:  1.0, s: 0.25, t: 0.50)
  let R = Vertex(x: -1.0, y:  -1.0, z:  -1.0, r:  0.0, g:  1.0, b:  0.0, a:  1.0, s: 0.25, t: 0.75)
  let S = Vertex(x:  1.0, y:  -1.0, z:  -1.0, r:  0.0, g:  0.0, b:  1.0, a:  1.0, s: 0.50, t: 0.75)
  let T = Vertex(x:  1.0, y:  -1.0, z:   1.0, r:  0.1, g:  0.6, b:  0.4, a:  1.0, s: 0.50, t: 0.50)

  //Back
  let U = Vertex(x:  1.0, y:   1.0, z:  -1.0, r:  1.0, g:  0.0, b:  0.0, a:  1.0, s: 0.75, t: 0.25)
  let V = Vertex(x:  1.0, y:  -1.0, z:  -1.0, r:  0.0, g:  1.0, b:  0.0, a:  1.0, s: 0.75, t: 0.50)
  let W = Vertex(x: -1.0, y:  -1.0, z:  -1.0, r:  0.0, g:  0.0, b:  1.0, a:  1.0, s: 1.00, t: 0.50)
  let X = Vertex(x: -1.0, y:   1.0, z:  -1.0, r:  0.1, g:  0.6, b:  0.4, a:  1.0, s: 1.00, t: 0.25)

  // 2
  let verticesArray:Array<Vertex> = [
    A,B,C ,A,C,D,   //Front
    E,F,G ,E,G,H,   //Left
    I,J,K ,I,K,L,   //Right
    M,N,O ,M,O,P,   //Top
    Q,R,S ,Q,S,T,   //Bot
    U,V,W ,U,W,X    //Back
  ]

  // 3
  let texture = MetalTexture(resourceName: "cube", ext: "png", mipmaped: true)
  texture.loadTexture(device: device, commandQ: commandQ, flip: true)

  super.init(name: "Cube", vertices: verticesArray, device: device, texture: texture.texture)
}

Metal-Texture-Coordinate-Label

先看图,然后对上面的几步做简单解释

  1. 创建每个顶点同时指定纹理坐标,具体坐标值看上面的图。要注意,你要为每个面都要创建独立的顶点,因为纹理的坐标可能不匹配
  2. 按逆时针组装三角形
  3. MetalTexture类创建和加载纹理

GPU上处理Texture

// 用下面的代码替换Shaders.metal的内容
#include <metal_stdlib>
using namespace metal;

// 1. 添加纹理坐标
struct VertexIn{
  packed_float3 position;
  packed_float4 color;
  packed_float2 texCoord;  
};

struct VertexOut{
  float4 position [[position]];
  float4 color;
  float2 texCoord; 
};

struct Uniforms{
  float4x4 modelMatrix;
  float4x4 projectionMatrix;
};

vertex VertexOut basic_vertex(
                              const device VertexIn* vertex_array [[ buffer(0) ]],
                              const device Uniforms&  uniforms    [[ buffer(1) ]],
                              unsigned int vid [[ vertex_id ]]) {
  
  float4x4 mv_Matrix = uniforms.modelMatrix;
  float4x4 proj_Matrix = uniforms.projectionMatrix;
  
  VertexIn VertexIn = vertex_array[vid];
  
  VertexOut VertexOut;
  VertexOut.position = proj_Matrix * mv_Matrix * float4(VertexIn.position,1);
  VertexOut.color = VertexIn.color;
  // 2. 将纹理坐标传给 VertexO
  VertexOut.texCoord = VertexIn.texCoord; 
  
  return VertexOut;
}

// 3. 接收传入的纹理
fragment float4 basic_fragment(VertexOut interpolated [[stage_in]],
                              texture2d<float>  tex2D     [[ texture(0) ]],    
// 4. 接收取样器
                              sampler           sampler2D [[ sampler(0) ]]) {  
// 5. 在纹理上用 sample() 获取指定纹理坐标
  float4 color = tex2D.sample(sampler2D, interpolated.texCoord);               
  return color;
}

添加用户输入

// 在 ViewController 类中添加属性和方法
let panSensivity: Float = 5.0
var lastPanLocation: CGPoint!
func setupGestures() {
    let pan = UIPanGestureRecognizer(target: self, action: #selector(ViewController.pan))
    self.view.addGestureRecognizer(pan)
}

@objc func pan(panGesture: UIPanGestureRecognizer) {
    if panGesture.state == .changed {
        let pointInView = panGesture.location(in: self.view)
        let xDelta = Float((lastPanLocation.x - pointInView.x) / self.view.bounds.width) * panSensivity
        let yDelta = Float((lastPanLocation.y - pointInView.y) / self.view.bounds.height) * panSensivity
        objectToDraw.rotationX -= xDelta
        objectToDraw.rotationY -= yDelta
        lastPanLocation = pointInView
    } else if panGesture.state == .began {
        lastPanLocation = panGesture.location(in: self.view)
    }
}
// ----------------------------------------------------
// 然后在 viewDidLoad 中调用 setupGestures 方法
// 记得将 Cubic 中的 updataWithDelta 方法注掉或删掉
// ----------------------------------------------------

此时工程可以正常运行,但会发现好像有点一对劲,横屏及边缘锯齿

// 解决方法就是旋转屏幕时及时更新,重写下面的方法
override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    
    if let window = view.window {
        let scale = window.screen.nativeScale
        let layerSize = view.bounds.size
        // 获取设备的拉伸因子(有的为2,有的为3),为了优化锯齿
        view.contentScaleFactor = scale
        metalLayer.frame = CGRect(x: 0, y: 0, width: layerSize.width, height: layerSize.height)
        metalLayer.drawableSize = CGSize(width: layerSize.width * scale, height: layerSize.height * scale)
    }
    projectionMatrix = Matrix4.makePerspectiveViewAngle(Matrix4.degrees(toRad: 85.0), aspectRatio: Float(self.view.bounds.size.width / self.view.bounds.size.height), nearZ: 0.01, farZ: 100.0)
    
}

Well Done !!!

参考及更多资料

posted @ 2020-07-31 20:47  20190311  阅读(592)  评论(0编辑  收藏  举报
---页脚---