在 WWDC 2014 上,Apple为游戏开发者推出了新的平台技术 Metal,该技术能够为3D图像提高 10 倍的渲染性能,充分利用GPU的运算能力。
在现阶段,AVFoundation、⼈脸识别等大量需要显示计算的时候,苹果采用了硬件加速器驱动GPU工作;在音视频方面,⾳频编码/解码 / 视频编码/解码 ->压缩任务都与硬件加速器分不开,苹果提供的Metal,能发挥GPU/CPU的最大性能,并且管理我们的资源,苹果想用metal替代opengl作为底层绘制框架。metal常见应用于一些游戏、滤镜、相机类的app。
设备支持:iOS 8以上,A7处理器以上,因此只有iphone5以上机型才支持metal,并且不支持模拟器运行,只支持真机。
这里的坐标系先不讲坐标空间,只是最基础的顶点坐标和纹理坐标。
跟平时我们开发写UI以左上角为原点不一样,Metal的顶点坐标系跟openGL一样,是以屏幕中心为原点,归一化的坐标系。
四维均匀向量 ( x,y,z,w) 指定一个三维点剪辑空间坐标。顶点着色器在剪辑空间坐标中生成位置。Metal分 x ,y,和z值由w将剪辑空间坐标转换为标准化设备坐标左下角位于( x,y)的坐标(- 1.0,-1.0) 而上角在(1.0,1.0) 。

如果只是绘制单色形状的话,只用顶点坐标然后填充颜色就行,但如果要绘制图片,或者给形状贴上纹理,就需要用到纹理坐标系。在metal中,纹理的原点坐标在左上角,这和openGL是不同的(OpenGL的纹理原点坐标在左下角)

metal的着色器有主要顶点着色器、片元着色器、内核计算函数
vertex: 表示该函数是一个顶点着色函数,它将为顶点数据流中的每一个顶点执行一次然后为每一个顶点生成数据输出到绘制管线。
示例代码:
vertex Vertex vertex_func(constant Vertex *vertices [[buffer(0)]],uint vid [[vertex_id]]){
return vertices[vid];
}
fragment: 表示该函数是一个片元函数,它将为片元数据流中的每一个片元和其关联执行一次然后将每一个片元的颜色数据输出到绘制管线中。
示例代码:
fragment float4 fragment_func(Vertex vert [[stage_in]]){
return float4(1.0, 1.0, 0.0, 1.0);
}
kernel:表示该函数是一个并行计算着色函数,它可以被分配在一维/二维/三维线程中去执行。
示例代码:
kernel void blend(texture2d<float, access::read> imageTexture [[texture(0)]],
texture2d<float, access::read> faceTexture [[texture(1)]],
texture2d<float, access::write> blendTexture [[texture(2)]],
uint2 gid [[thread_position_in_grid]]) {
float width = faceTexture.get_width();
float height = faceTexture.get_height();
if ((gid.x >= width) || (gid.y >= height)) {
return;
}
float4 face = faceTexture.read(gid);
if(face.a > 0.0){
blendTexture.write(face, gid);
}
else {
uint2 pos = uint2(gid.x, gid.y);
float4 image = imageTexture.read(pos);
blendTexture.write(image, gid);
}
}
着色器的基础语法规范可以参考苹果官方文档,文档比较复杂,还是全英文的,中文翻译可以参考这个专栏
Metal渲染管道流程

在OpenGLES中,图元装配有9种,在Metal中,图元装配只有五种,他们分别是:
MTLPrimitiveTypePoint = 0, 点
MTLPrimitiveTypeLine = 1, 线段
MTLPrimitiveTypeLineStrip = 2, 线环
MTLPrimitiveTypeTriangle = 3, 三角形
MTLPrimitiveTypeTriangleStrip = 4, 三角型扇
metal的驱动GPU进行绘制的流程如图所示,下面进行一些参数名词的解释

可以理解为GPU对象,可以用如下方法获得:
let device = MTLCreateSystemDefaultDevice()
guard device != nil else {
print("Metal is not supported on this device")
return
}
上面说过只有iOS8以及A7芯片以上才支持Metal,所以MTLDevice可以为空,需要判断
有了GPU之后,需要创建一个渲染队列MTLCommandQueue,队列是单一队列,确保了指令能够按顺序执行,里面的对象是需要渲染的指令MTLCommandBuffer,可以支持多个CommandBuffer同时编码。通过MTLDevice可以获取MTLCommandQueue:
let queue:MTLCommandQueue = device?.makeCommandQueue()
承接Metal绘制的视图,初始化方法为let viewiew = MTKView.init(frame: self.bounds, device: MTLCreateSystemDefaultDevice)
MTKView有两个delegate:
- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size;
- (void)drawInMTKView:(nonnull MTKView *)view;
drawInMTKView:方法是MetalKit每帧的渲染回调,可以在内部做渲染的处理
drawableSizeWillChange:方法是绘制区域发生改变的方法,在里面可以给绘图区域大小重新赋值
在demo中没有使用这两个delegate,而是直接重写了MTKView的draw方法,也能实现同样的效果
不用MTKView也可以使用CAMetalLayer,添加到当前view的layer上。
剩下的一些参数会通过一个画五角星例子具体说明。
可以用let viewiew = MTKView.init(frame: self.bounds, device: MTLCreateSystemDefaultDevice)初始化MTKView,demo中是使用storyBoard拖入使用的
demo是画一个三角形、五角星,三角形的话需要提供三个顶点的坐标,五角星的话,由于Metal绘制都是通过画三角形去绘制,所以,需要绘制下图所示的十个三角形,加上中心点总计10个顶点数据

func setFiveAngleData() {
vertexData = [0.0, 0.0, 0.0, 1.0,
0.0, BIG_R*Y_SCALE, 0.0, 1.0,
SMALL_R*cos(54*RAD), SMALL_R*sin(54*RAD)*Y_SCALE, 0.0, 1.0,
BIG_R*cos(18*RAD), BIG_R*sin(18*RAD)*Y_SCALE, 0.0, 1.0,
SMALL_R*cos(18*RAD), -SMALL_R*sin(18*RAD)*Y_SCALE, 0.0, 1.0,
BIG_R*cos(54*RAD), -BIG_R*sin(54*RAD)*Y_SCALE, 0.0, 1.0,
0.0, -SMALL_R*Y_SCALE, 0.0, 1.0,
-BIG_R*cos(54*RAD), -BIG_R*sin(54*RAD)*Y_SCALE, 0.0, 1.0,
-SMALL_R*cos(18*RAD), -SMALL_R*sin(18*RAD)*Y_SCALE, 0.0, 1.0,
-BIG_R*cos(18*RAD), BIG_R*sin(18*RAD)*Y_SCALE, 0.0, 1.0,
-SMALL_R*cos(54*RAD), SMALL_R*sin(54*RAD)*Y_SCALE, 0.0, 1.0]
indexData = [0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4 ,5, 0, 5, 6, 0, 6, 7, 0, 7, 8, 0, 8, 9, 0, 9 , 10, 0, 10, 1]
}
其中,vertexData是顶点数据,四个代表一个点(x,y,z,w);indexData是索引数据,三个为一组,代表顶点组成三角形的顺序
将一张UIImage渲染到MKTView上,需要用到纹理数据MTLTexture,可以由以下方法获取:
func getTexture() {
do {
self.imageTexture = try MTKTextureLoader(device: self.device).newTexture(cgImage: image.cgImage!, options: [MTKTextureLoader.Option.SRGB:false])
} catch {
assertionFailure("Could not create Texture - \(error) ")
}
render(texture: imageTexture)
}
然后需要将纹理坐标(二维)加入到顶点坐标中
func render() {
let library = device?.makeDefaultLibrary()!
let vertex_func = library?.makeFunction(name: "vertex_func")
let frag_func = library?.makeFunction(name: "fragment_func")
let rpld = MTLRenderPipelineDescriptor()
rpld.vertexFunction = vertex_func
rpld.fragmentFunction = frag_func
rpld.colorAttachments[0].pixelFormat = .bgra8Unorm
do{
try rps = device?.makeRenderPipelineState(descriptor: rpld)
}catch let error{
fatalError("\(error)")
}
}
其中,MTLRenderPipelineDescriptor是渲染管道的描述符,可以设置顶点处理函数、片元处理函数、输出颜色格式等;
override func draw(_ rect: CGRect) {
if let drawable = currentDrawable, let rpd = currentRenderPassDescriptor {
let dataSize = vertexData!.count * MemoryLayout<Float>.size
// 设置顶点buffer
vertexBuffer = device?.makeBuffer(bytes: vertexData!, length: dataSize, options: [])
// 设置索引buffer
indexBuffer = device?.makeBuffer(bytes: indexData!, length: MemoryLayout<UInt16>.size * indexData!.count , options: [])
// 设置背景色为红色
rpd.colorAttachments[0].clearColor = MTLClearColorMake(1.0, 0.0, 0.0, 1.0)
// 创建commandBuffer
let commandBuffer = commandQueue!.makeCommandBuffer()
let commandEncode = commandBuffer?.makeRenderCommandEncoder(descriptor: rpd)
commandEncode?.setRenderPipelineState(rps!)
// 设置顶点缓存
commandEncode?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
// 绘制
commandEncode?.drawIndexedPrimitives(type: .triangle, indexCount: indexBuffer!.length / MemoryLayout<UInt16>.size, indexType: MTLIndexType.uint16, indexBuffer: indexBuffer!, indexBufferOffset: 0)
// 结束设置
commandEncode?.endEncoding()
// 显示绘制内容
commandBuffer?.present(drawable)
// 提交命令编码器
commandBuffer?.commit()
}
}
绘制的第一步是从commandQueue里面创建commandBuffer,commandQueue是整个app绘制的队列,commandBuffer存放每次渲染的指令,commandQueue内部存在着多个commandBuffer,CommandEncoder是命令编码器
#include <metal_stdlib>
using namespace metal;
struct Vertex {
float4 position [[position]];
};
vertex Vertex vertex_func(constant Vertex *vertices [[buffer(0)]],uint vid [[vertex_id]]){
return vertices[vid];
}
fragment float4 fragment_func(Vertex vert [[stage_in]]){
return float4(1.0, 1.0, 0.0, 1.0);
}
Shader的语法与C++类似,参数名前面的是类型,后面的[[ ]]是描述符。
其中vertex函数是读取顶点信息,fragment函数是进行颜色填充处理,这里填充的是黄色。
运行出来的效果:

上面第一个例子通过一个五角星讲了最基础的Metal用法,接下来会用另一个例子来讲一下图片的一些处理效果,是将一张平面图片转化为三维图片。
二维图片顶点坐标z轴的值都为0,要变成三维,只需要z轴不为0即可,因此可以在将图片显示到MTKView上后,处理顶点着色器,给每个顶点的z轴都赋上相应的值,这里可以用每个像素点的RGB转YUV的算法,取每个像素的亮度Y的值,作为z方向上的深度值,具体转化为:
inVertex.position.z = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b
如果只给z轴不为0的值,图片虽然是三维的,但是依旧只能看到正面的部分,z轴的深度部分是看不到的,所以还需要给图片加入相应的空间矩阵变换,会用到三种矩阵MVP:分别是模型矩阵(model)、观察矩阵(view)、投影矩阵(Projection):
透视投影是模仿人眼观察物体,有远小近大的效果,所以这种投影更加真实
由于Metal并没有对应的矩阵运算的框架(不知道到底有没有,我没找到),所以这里采用的是GLKit框架里面的矩阵,下同理。
来看一下这个投影矩阵的参数,是个4X4矩阵
public func GLKMatrix4MakePerspective(_ fovyRadians: Float, _ aspect: Float, _ nearZ: Float, _ farZ: Float) -> GLKMatrix4

其中参数fovyRadians定义视野在Y-Z平面的角度,范围是[0.0,180.0];参数aspect是投影平面宽度与高度的比率;参数nearZ和farZ分别是近远裁剪面到视点(沿Z负轴)的距离,它们总为正值。
来看一下这个观察矩阵的参数,是个4X4矩阵
public func GLKMatrix4MakeLookAt(_ eyeX: Float, _ eyeY: Float, _ eyeZ: Float, _ centerX: Float, _ centerY: Float, _ centerZ: Float, _ upX: Float, _ upY: Float, _ upZ: Float) -> GLKMatrix4
这个矩阵模拟了人眼或者摄像机在空间的一些位置参数,设置这9个参数以控制摄像机从不同的角度观察物体:
eyeX, eyeY, eyeZ定义摄像机的位置;
centerX, centerY, centerZ摄像机看向的点;
相机还可以旋转360,upX, upY, upZ三个参数确定相机向上的朝向。
模型矩阵就设为单位矩阵GLKMatrix4Identity,这里可以给他加上旋转变换矩阵GLKMatrix4Rotate,给模型矩阵加上旋转矩阵是让物体自己动,如果修改上面观察矩阵的一些参数,就是摄像机或人眼围绕着物体在动,无论哪一种方法都能模拟出物体旋转的效果,这里选择的是物体自己动,也就是在模型矩阵上加入旋转变化。
然后将mvp直接相乘,结果再与顶点坐标相乘。注意相乘的顺序先进行模型矩阵变换,再是观察矩阵,最后是投影矩阵变换,所以应为
P * V * M * vertex.position。
基本流程跟上面说的大体一致,但是需要额外设置一些东西
func draw(renderEncoder: MTLRenderCommandEncoder, texture: MTLTexture, type: MTLPrimitiveType) {
self.uniforms = Uniforms.init()
self.vertexData = buildPointData()
self.indexData = buildIndexData()
// 设置顶点和索引buffer
let vertexBufferSize = MemoryLayout<Float>.stride * self.vertexData.count
let indexBufferSize = MemoryLayout<UInt32>.stride * self.indexData.count
let vertexBuffer = device.makeBuffer(bytes: self.vertexData, length: vertexBufferSize, options: MTLResourceOptions.cpuCacheModeWriteCombined)
let indexBuffer = device.makeBuffer(bytes: self.indexData, length: indexBufferSize , options: MTLResourceOptions.cpuCacheModeWriteCombined)
// 设置MVP矩阵及其buffer
let aspect = self.bounds.width / self.bounds.height
// 这里将pinch手势的缩放参数传入投影矩阵,就能进行缩放
var GLKPerspective = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(degree), Float(aspect), 0.1, 10.0)
var GLKView = GLKMatrix4MakeLookAt(0.0, 0.0, 2.0, 0, 0, 0.0, 0.0, 1.0, 0.0)
var GLKModel = GLKMatrix4Identity
// 这里将pan手势的滑动距离参数经过调整后传到旋转矩阵中,就能旋转滑动了
GLKModel = GLKMatrix4Rotate(GLKModel, centerX, 1, 0, 0)
GLKModel = GLKMatrix4Rotate(GLKModel, centerY, 0, 1, 0)
let perspectiveBuffer = device.makeBuffer(bytes: &GLKPerspective, length: MemoryLayout<float4x4>.size, options: .cpuCacheModeWriteCombined)
let viewBuffer = device.makeBuffer(bytes: &GLKView, length: MemoryLayout<float4x4>.size, options: .cpuCacheModeWriteCombined)
let modelBuffer = device.makeBuffer(bytes: &GLKModel, length: MemoryLayout<float4x4>.size, options: .cpuCacheModeWriteCombined)
// 设置顶点着色器的缓冲区,index要对应shader
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderEncoder.setVertexBuffer(modelBuffer, offset: 0, index: 1)
renderEncoder.setVertexBuffer(viewBuffer, offset: 0, index: 2)
renderEncoder.setVertexBuffer(perspectiveBuffer, offset: 0, index: 3)
let uniformBuffer = device.makeBuffer(bytes: self.uniforms.data(), length: Uniforms.sizeInBytes(), options: MTLResourceOptions.cpuCacheModeWriteCombined)
renderEncoder.setVertexBuffer(uniformBuffer, offset: 0, index: 4)
// 设置纹理缓冲区
renderEncoder.setFragmentTexture(texture, index: 0)
// 设置顶点纹理,因为顶点的z轴数据需要获取亮度值,所以需要把纹理传到顶点着色器中
renderEncoder.setVertexTexture(texture, index: 0)
// 图元装配
if type == .point {
renderEncoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: self.vertexData.count / 5)
} else if type == .triangle {
renderEncoder.drawIndexedPrimitives(type: .triangle, indexCount: self.indexData.count, indexType: MTLIndexType.uint32, indexBuffer: indexBuffer!, indexBufferOffset: 0)
} else {
renderEncoder.drawPrimitives(type: .lineStrip, vertexStart: 0, vertexCount: self.vertexData.count / 5)
}
}
着色器shader:
vertex VertexOut vertex_func(uint vid [[vertex_id]],
texture2d<float> diffuse [[texture(0)]],
// 对应缓冲区的下标
const device VertexIn* vertexIn [[buffer(0)]],
const device float4x4& model [[buffer(1)]],
const device float4x4& view [[buffer(2)]],
const device float4x4& perspective [[buffer(3)]],
const device Uniforms& uniforms [[buffer(4)]])
{
VertexOut outVertex;
VertexIn inVertex = vertexIn[vid];
float4 color = diffuse.sample(s, inVertex.uv);
// 亮度作为z轴深度值
inVertex.position.z = 0.3 * (0.299 * color.r + 0.587 * color.g + 0.114 * color.b);
outVertex.uv = inVertex.uv;
// MVP矩阵相乘
outVertex.position = perspective * view * model * float4(inVertex.position);
outVertex.pointSize = uniforms.pointSizeInPixel;
return outVertex;
};
fragment float4 fragment_func(VertexOut infrag [[stage_in]], texture2d<float> diffuse [[texture(0)]]) {
// 纹理采样
float4 imageColor = diffuse.sample(s, infrag.uv);
return imageColor;
};
运行效果

我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当
我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm
这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下
我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI
我是Rails的新手,所以请原谅简单的问题。我正在为一家公司创建一个网站。那家公司想在网站上展示它的客户。我想让客户自己管理这个。我正在为“客户”生成一个表格,我想要的三列是:公司名称、公司描述和Logo。对于名称,我使用的是name:string但不确定如何在脚本/生成脚手架终端命令中最好地创建描述列(因为我打算将其设置为文本区域)和图片。我怀疑描述(我想成为一个文本区域)应该仍然是描述:字符串,然后以实际形式进行调整。不确定如何处理图片字段。那么……说来话长:我在脚手架命令中输入什么来生成描述和图片列? 最佳答案 对于“文本”数
这似乎非常适得其反,因为太多的gem会在window上破裂。我一直在处理很多mysql和ruby-mysqlgem问题(gem本身发生段错误,一个名为UnixSocket的类显然在Windows机器上不能正常工作,等等)。我只是在浪费时间吗?我应该转向不同的脚本语言吗? 最佳答案 我在Windows上使用Ruby的经验很少,但是当我开始使用Ruby时,我是在Windows上,我的总体印象是它不是Windows原生系统。因此,在主要使用Windows多年之后,开始使用Ruby促使我切换回原来的系统Unix,这次是Linux。Rub
我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的
我正在玩HTML5视频并且在ERB中有以下片段:mp4视频从在我的开发环境中运行的服务器很好地流式传输到chrome。然而firefox显示带有海报图像的视频播放器,但带有一个大X。问题似乎是mongrel不确定ogv扩展的mime类型,并且只返回text/plain,如curl所示:$curl-Ihttp://0.0.0.0:3000/pr6.ogvHTTP/1.1200OKConnection:closeDate:Mon,19Apr201012:33:50GMTLast-Modified:Sun,18Apr201012:46:07GMTContent-Type:text/plain
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList()Obt