[MetalKit]26-The-Model-I-O-framework-ModelI-O框架

蘋果API搬運工發表於2017-12-14

本系列文章是對 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上.

下次見!

相關文章