一步步学Metal图形引擎4-《OBJ模型加载》
Posted Mr_厚厚
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一步步学Metal图形引擎4-《OBJ模型加载》相关的知识,希望对你有一定的参考价值。
教程 4
OBJ模型加载
教程源码下载地址: https://github.com/jiangxh1992/MetalTutorialDemos
CSDN完整版专栏: https://blog.csdn.net/cordova/category_9734156.html
一、知识点
- OBJ格式模型
- AAPLMesh
- MDLVertexDescriptor
- MDLMesh
二、背景
前面教程3中我们分析了使用MTLVertexDescriptor来描述配置顶点数据流的原理,以及顶点buffer的两种组织结构。之前我们是在程序内部自定义了简单的顶点buffer,一个三角形,而实际应用中我们会面对大量非常复杂的模型,这些模型由美术使用maya、3d max等建模工具制作,模型的面数可能数以千计,因此需要特定的数据格式将模型数据保存成文件,然后交给程序绘制。程序这边拿到模型数据则要根据模型的格式解析模型数据并读取到工程中进行绘制。
OBJ是一种模型的数据格式,存储了模型的顶点、法线、uv等顶点buffer的数据,以及模型组织的索引数据、图元数据等,另外描述了对应.mtl文件中的贴图资源路径、光照参数数据等,具体这里不作详细讨论。
模型加载并不是我们学习图形渲染的重点,只是将模型数据加载到引擎中,为我们的渲染提供数据对象,因此没必要重复造轮子,尽可能使用已有的工具即可。
Metal中保存mesh数据的容器是MDLMesh类,每个MDLMesh可能包含多个MDLSubmesh,每个MDLSubmesh中保存了模型顶点的index buffer数据,描述如何组织mesh顶点的绘制,另外还保存了模型的材质贴图信息,用于对应的模型纹理贴图。
AAPLAMesh是官方demo中使用的一个模型加载工具类,用于加载demo中的OBJ模型。实际开发中模型的设置规范要和美术制作统一,顶点buffer的数据格式,数据组织等可能改变。这里只是学习展示,就是用了官方demo中的模型,直接用APPLMesh来加载OBJ模型数据,不关心内部的模型解析机制。
另外MDLVertexDescriptor是在我们将模型数据解析到MDLMesh中的一个描述类,配置顶点的属性结构、格式和数据布局等,从而将mesh数据读取到顶点bufer中。
三、源码分析
首先我们直接将AAPLMesh工具类文件导入到我们的工程中使用。同时导入官方demo中的建筑模型Temple.obj及其对应的mtl文件,并导入模型对应的贴图资源。
3.1 Render.m
NSArray<AAPLMesh *> *_meshes;
建筑模型由神庙建筑和一棵树两个Mesh组合而成,这里定义了一个AAPLMesh数据来保存。
MDLVertexDescriptor *modelIOVertexDescriptor =
MTKModelIOVertexDescriptorFromMetal(_defaultVertexDescriptor);
modelIOVertexDescriptor.attributes[0].name = MDLVertexAttributePosition;
modelIOVertexDescriptor.attributes[1].name = MDLVertexAttributeTextureCoordinate;
//modelIOVertexDescriptor.attributes[2].name = MDLVertexAttributeNormal;
//modelIOVertexDescriptor.attributes[3].name = MDLVertexAttributeTangent;
//modelIOVertexDescriptor.attributes[4].name = MDLVertexAttributeBitangent;
在loadAssets函数中我们将OBJ模型加载到我们的_meshes保存。这里MDLVertexDescriptor来配置我们要读取的顶点属性数据,这里我们只是绘制模型并纹理贴图,所以只读取前面的顶点位置属性和uv坐标属性,后面的法线和切线数据在在之后的光照计算中才会用到,后面到时再继续读进来即可。
_defaultVertexDescriptor = [[MTLVertexDescriptor alloc] init];
// Positions.
_defaultVertexDescriptor.attributes[0].format = MTLVertexFormatFloat3;
_defaultVertexDescriptor.attributes[0].offset = 0;
_defaultVertexDescriptor.attributes[0].bufferIndex = 0;
// Texture coordinates.
_defaultVertexDescriptor.attributes[1].format = MTLVertexFormatFloat2;
_defaultVertexDescriptor.attributes[1].offset = 12;
_defaultVertexDescriptor.attributes[1].bufferIndex = 0;
// ...
_defaultVertexDescriptor.layouts[0].stride = 44;
_defaultVertexDescriptor.layouts[0].stepRate = 1;
_defaultVertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;
这里我们对VertexDescriptor的配置根据这里的模型做了调整,模型中每个顶点包含position(float3)、uv(float2)、normal(half4)、tangent(half4)、bitangent(half4)五个属性,数据长度为:3x4 + 2x4 + 4x2 + 4x2 + 4x2 = 44。注意虽然我们只读取了position和uv,但是;layout的stride依然要设置为44,这是GPU从buffer中读取每个顶点数据的固定跨度。
NSURL *modelFileURL = [[NSBundle mainBundle] URLForResource:@"Temple.obj" withExtension:nil];
NSAssert(modelFileURL, @"Could not find model (%@) file in bundle", modelFileURL.absoluteString);
_meshes = [AAPLMesh newMeshesFromURL:modelFileURL
modelIOVertexDescriptor:modelIOVertexDescriptor
metalDevice:_device
error:&error];
这里我们直接调用AAPLMesh的newMeshesFromURL函数来加载模型数据,这样我们的mesh数据就保存到了工程内的_meshes中,里面包含了顶点buffer数据,索引buffer数据以及模型贴图的引用。
/// Draw the mesh objects with the given render command encoder.
- (void)drawMeshes:(id<MTLRenderCommandEncoder>)renderEncoder
for (__unsafe_unretained AAPLMesh *mesh in _meshes)
__unsafe_unretained MTKMesh *metalKitMesh = mesh.metalKitMesh;
// Set the mesh's vertex buffers.
for (NSUInteger bufferIndex = 0; bufferIndex < metalKitMesh.vertexBuffers.count; bufferIndex++)
__unsafe_unretained MTKMeshBuffer *vertexBuffer = metalKitMesh.vertexBuffers[bufferIndex];
if((NSNull*)vertexBuffer != [NSNull null])
[renderEncoder setVertexBuffer:vertexBuffer.buffer
offset:vertexBuffer.offset
atIndex:bufferIndex];
// Draw each submesh of the mesh.
for(AAPLSubmesh *submesh in mesh.submeshes)
// Set any textures that you read or sample in the render pipeline.
[renderEncoder setFragmentTexture:submesh.textures[0]
atIndex:0];
[renderEncoder setFragmentTexture:submesh.textures[1]
atIndex:1];
[renderEncoder setFragmentTexture:submesh.textures[2]
atIndex:2];
MTKSubmesh *metalKitSubmesh = submesh.metalKitSubmmesh;
[renderEncoder drawIndexedPrimitives:metalKitSubmesh.primitiveType
indexCount:metalKitSubmesh.indexCount
indexType:metalKitSubmesh.indexType
indexBuffer:metalKitSubmesh.indexBuffer.buffer
indexBufferOffset:metalKitSubmesh.indexBuffer.offset];
drawMeshes函数中,我们遍历mesh数组数据,依次将顶点buffer传递给顶点着色器,贴图数据传送给片段着色器,并调用draw call绘制模型。这样模型顶点buffer的设置,贴图的设置都放在了drawMeshes函数中。
// ...
[self drawMeshes:renderEncoder];
// ...
最后在每一帧绘制的时候,设置完pipeLineState,调用一次drawMeshes即可。这样我们完成了从之前的自定义顶点buffer绘制,到现在的从外部模型文件加载buffer数据的流程转变。
Shader.metal
shader文件中代码没有大的变动,只是根据我们现在的模型数据格式做了对应的调整。
typedef struct
float3 position [[attribute(0)]];
float2 texCoord [[attribute(1)]];
Vertex;
顶点的数据结构与要和VertexDescriptor中描述的一致。
vertex ColorInOut vertexShader(Vertex in [[ stage_in ]])
ColorInOut out;
float4 position = vector_float4(in.position/500.0f + float3(0,-0.3,0), 1.0);
out.position = position;
out.texCoord = in.texCoord;
return out;
顶点着色器中我们强行对顶点坐标做了调整,让模型显示到屏幕内。因为这里我们还没有对模型坐标进行坐标系变换,顶点数据是定义在模型空间的,下个教程我们会使用UniformBuffer传进坐标变换矩阵,将模型坐标变换到投影空间。
片段着色器没有改动,依然是读取纹理贴图,进行采样然后返回最终颜色。
四、运行效果
结果看上去有些奇怪,因为我们还没有进行矩阵变换,显示的只是一个角度。坐标又是在0-1范围,映射到宽高比不同的屏幕上会被按宽高比拉伸。
以上是关于一步步学Metal图形引擎4-《OBJ模型加载》的主要内容,如果未能解决你的问题,请参考以下文章
一步步学Metal图形引擎9-《Blinn-Phong光照模型》