本系列文章是对 metalkit.org 上面MetalKit内容的全面翻译和学习.c++
Model I/O是2015
年被引入到iOS 9
和OS X 10.11
中的,这个框架帮助咱们建立更真实更有交互性的图形.咱们能够用它来导入/导出3D
素材,来描述灯光,材料和环境,来烘焙灯光,来细分及体素化网格,来提供基于物理效果的渲染.Model I/O用一些3D API
轻易地将咱们的资源融入到代码里:github
要导入一个资源,咱们只须要作:swift
var url = URL(string: "/Users/YourUsername/Desktop/imported.obj")
let asset = MDLAsset(url: url!)
复制代码
要导出一个素材咱们只要作:bash
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/O
和Metal
融合只须要四步:ide
首先咱们建立一个顶点描述符来传递输入项到顶点函数.顶点描述符是用来描述输入到渲染状态管线的顶点属性.咱们须要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)
复制代码
咱们还须要建立一个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)
复制代码
下一步,为素材加载纹理:post
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)
复制代码
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)
复制代码
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,你会看到这样的输出图片:
这是个至关无趣的纯白模型.让咱们给它应用上环境光遮蔽,只要在片断函数中用下面几行替换最后一行就好了:
float4 occlusion = fragments.occlusion;
return half4(baseColor * occlusion);
复制代码
若是你运行playground,你会看到这样的输出图片:
环境光遮蔽看上去有点不成熟,这是由于咱们的模型是扁平的,没有任何的曲线或表面不规则,因此环境光遮蔽不能让它更真实.下一步,咱们用上纹理.用下面几行替换片断函数中的最后一行:
constexpr sampler samplers;
float4 texture = textures.sample(samplers, fragments.texCoords);
return half4(baseColor * texture);
复制代码
若是你再运行playground,你会看到这样的输出图片:
模型上的纹理看起来好多了,但若是咱们将环境光遮蔽也用上它会显得更真实.用下面这行替换片断函数中的最后一行:
return half4(baseColor * occlusion * texture);
复制代码
若是你再运行playground,你会看到这样的输出图片:
几行代码效果不错,对吧?Model I/O
对于3D
图形和游戏开发者来讲是个很棒的框架.网上也有不少关于Model I/O
与SceneKit
协同使用的文章,可是,我认为将其和Metal
协同使用会更有意思! 源代码 source code 已发布在Github上.
下次见!