[MetalKit]The Model I/O framework ModelI/Osalk

本系列文章是对 http://metalkit.org 上面MetalKit内容的全面翻译和学习.

MetalKit系统文章目录


Model I/O2015年被引入到iOS 9OS X 10.11中的,这个框架帮助我们创建更真实更有交互性的图形.我们可以用它来导入/导出3D素材,来描述灯光,材料和环境,来烘焙灯光,来细分及体素化网格,来提供基于物理效果的渲染.Model I/O用一些3D API轻易地将我们的资源融入到代码里:

modelio_1.png

要导入一个资源,我们只需要做:

var url = URL(string: "/Users/YourUsername/Desktop/imported.obj")
let asset = MDLAsset(url: url!)

要导出一个素材我们只要做:

url = URL(string: "/Users/YourUsername/Desktop/exported.obj")
try! asset.export(to: url!)

Model I/O会保存.obj文件和一个额外的.mtl文件,其中包含了物体材质的信息,比如这个例子:

# Apple ModelI/O MTL File: exported.mtl

newmtl material_1
    Kd 0.8 0.8 0.8
    Ka 0 0 0
    Ks 0 0 0
    ao 0 0 0
    subsurface 0 0 0
    metallic 0 0 0
    specularTint 0 0 0
    roughness 0.9 0 0
    anisotropicRotation 0 0 0
    sheen 0.05 0 0
    sheenTint 0 0 0
    clearCoat 0 0 0
    clearCoatGloss 0 0 0

Model I/OMetal融合只需要四步:

modelio_2.png

Step 1: set up the render pipeline state创建渲染管线状态

首先我们创建一个顶点描述符来传递输入项到顶点函数.顶点描述符是用来描述输入到渲染状态管线的顶点属性.我们需要3 x 4字节给顶点位置,4 x 1字节给颜色,2 x 2字节给纹理坐标,4 x 1字节给AO环境光遮蔽.最后我们告诉描述符,总的stride步幅是多长(24):

let vertexDescriptor = MTLVertexDescriptor()
vertexDescriptor.attributes[0].offset = 0
vertexDescriptor.attributes[0].format = MTLVertexFormat.float3 // position

vertexDescriptor.attributes[1].offset = 12
vertexDescriptor.attributes[1].format = MTLVertexFormat.uChar4 // color

vertexDescriptor.attributes[2].offset = 16
vertexDescriptor.attributes[2].format = MTLVertexFormat.half2 // texture

vertexDescriptor.attributes[3].offset = 20
vertexDescriptor.attributes[3].format = MTLVertexFormat.float // occlusion

vertexDescriptor.layouts[0].stride = 24
let renderPipelineDescriptor = MTLRenderPipelineDescriptor()
renderPipelineDescriptor.vertexDescriptor = vertexDescriptor
let rps = device.newRenderPipelineStateWithDescriptor(renderPipelineDescriptor)

Step 2: set up the asset initialization建立素材初始化

我们还需要创建一个Model I/O的顶点描述符来描述顶点属性在网格中的布局.我们使用一个名为Farmhouse.obj的模型,它有一个Farmhouse.png纹理(都已经添加到项目中了):

let desc = MTKModelIOVertexDescriptorFromMetal(vertexDescriptor)
var attribute = desc.attributes[0] as! MDLVertexAttribute
attribute.name = MDLVertexAttributePosition
attribute = desc.attributes[1] as! MDLVertexAttribute
attribute.name = MDLVertexAttributeColor
attribute = desc.attributes[2] as! MDLVertexAttribute
attribute.name = MDLVertexAttributeTextureCoordinate
attribute = desc.attributes[3] as! MDLVertexAttribute
attribute.name = MDLVertexAttributeOcclusionValue
let mtkBufferAllocator = MTKMeshBufferAllocator(device: device!)
let url = Bundle.main.url(forResource: "Farmhouse", withExtension: "obj")
let asset = MDLAsset(url: url!, vertexDescriptor: desc, bufferAllocator: mtkBufferAllocator)

下一步,为素材加载纹理:

let loader = MTKTextureLoader(device: device)
let file = Bundle.main.path(forResource: "Farmhouse", ofType: "png")
let data = try Data(contentsOf: URL(fileURLWithPath: file))
let texture = try loader.newTexture(with: data, options: nil)

Step 3: set up MetalKit mesh and submesh objects建立MetalKit网格和子网格对象

我们现在正在创建在最后一步,第四步中用到的网格和子网格.我们还要计算Ambient Occlusion环境光遮蔽,它是对几何体遮断的度量,它告诉我们环境光有多少到达了我们物体的各个像素或点,以及光线被周围的网格阻碍了多少.Model I/O提供了一个UV制图器来创建2D纹理并将其包裹在物体的3D网格上.我们为纹理中的每个像素计算其环境光遮蔽数值,这个值是添加一每个顶点上的额外的浮点数:

let mesh = asset.object(at: 0) as? MDLMesh
mesh.generateAmbientOcclusionVertexColors(withQuality: 1, attenuationFactor: 0.98, objectsToConsider: [mesh], vertexAttributeNamed: MDLVertexAttributeOcclusionValue)
let meshes = try MTKMesh.newMeshes(from: asset, device: device!, sourceMeshes: nil)

Step 4: set up Metal rendering and drawing of meshes建立Metal渲染和绘图网格

最后,我们用网格数据来配置绘图所需的命令编码器:

let mesh = (meshes?.first)!
let vertexBuffer = mesh.vertexBuffers[0]
commandEncoder.setVertexBuffer(vertexBuffer.buffer, offset: vertexBuffer.offset, at: 0)
let submesh = mesh.submeshes.first!
commandEncoder.drawIndexedPrimitives(submesh.primitiveType, indexCount: submesh.indexCount, indexType: submesh.indexType, indexBuffer: submesh.indexBuffer.buffer, indexBufferOffset: submesh.indexBuffer.offset)

下一步,我们将致力于我们的着色器函数.首先我们为顶点和uniforms建立自己的结构体:

struct VertexIn {
    float4 position [[attribute(0)]];
    float4 color [[attribute(1)]];
    float2 texCoords [[attribute(2)]];
    float occlusion [[attribute(3)]];
};

struct VertexOut {
    float4 position [[position]];
    float4 color;
    float2 texCoords;
    float occlusion;
};

struct Uniforms {
    float4x4 modelViewProjectionMatrix;
};

注意,我让顶点描述符中的信息和VertexIn结构体相匹配.对于顶点函数,我们使用了一个** [[stage_in]]**属性,因为我们将把每个顶点的输入值作为一个参数传递到该函数:

vertex VertexOut vertex_func(const VertexIn vertices [[stage_in]],
                             constant Uniforms &uniforms [[buffer(1)]],
                             uint vertexId [[vertex_id]])
{
    float4x4 mvpMatrix = uniforms.modelViewProjectionMatrix;
    float4 position = vertices.position;
    VertexOut out;
    out.position = mvpMatrix * position;
    out.color = float4(1);
    out.texCoords = vertices.texCoords;
    out.occlusion = vertices.occlusion;
    return out;
}

片段函数读取从顶点函数中传递过来的每个片段作为输入值,并通过命令编码器处理我们传递过去的纹理:

fragment half4 fragment_func(VertexOut fragments [[stage_in]],
                             texture2d<float> textures [[texture(0)]])
{
    float4 baseColor = fragments.color;
    return half4(baseColor);
}

如果你运行playground,你会看到这样的输出图片:

modelio_3.png

这是个相当无趣的纯白模型.让我们给它应用上环境光遮蔽,只要在片段函数中用下面几行替换最后一行就行了:

float4 occlusion = fragments.occlusion;
return half4(baseColor * occlusion);

如果你运行playground,你会看到这样的输出图片:

modelio_4.png

环境光遮蔽看上去有点不成熟,这是因为我们的模型是扁平的,没有任何的曲线或表面不规则,所以环境光遮蔽不能让它更真实.下一步,我们用上纹理.用下面几行替换片段函数中的最后一行:

constexpr sampler samplers;
float4 texture = textures.sample(samplers, fragments.texCoords);
return half4(baseColor * texture);

如果你再运行playground,你会看到这样的输出图片:

modelio_5.png

模型上的纹理看起来好多了,但如果我们将环境光遮蔽也用上它会显得更真实.用下面这行替换片段函数中的最后一行:

return half4(baseColor * occlusion * texture);

如果你再运行playground,你会看到这样的输出图片:

modelio_6.png

几行代码效果不错,对吧?Model I/O对于3D图形和游戏开发者来说是个很棒的框架.网上也有很多关于Model I/OSceneKit协同使用的文章,但是,我认为将其和Metal协同使用会更有意思!
源代码 source code 已发布在Github上.
下次见!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339

推荐阅读更多精彩内容