说明
学习ARKit前,需要先学习SceneKit,参考SceneKit系列文章目录
更多iOS相关知识查看github上WeekWeekUpProject
通过顶点,法线,纹理坐标数组加载
1. 创建Source
默认有三种快速创建的方法,可以分别创建vertices,normals和textureCoordinates(即UVs):
let vertexSource = SCNGeometrySource(vertices:cubeVertices())
let normalSource = SCNGeometrySource(normals:cubeNormals())
let uvSource = SCNGeometrySource(textureCoordinates:cubeUVs())
复制代码
如果想要创建更多类型,或者数据混在同一个数组中,需要用另一个方法:
SCNGeometrySource(data: Data, semantic: SCNGeometrySource.Semantic, vectorCount: Int, usesFloatComponents floatComponents: Bool, componentsPerVector: Int, bytesPerComponent: Int, dataOffset offset: Int, dataStride stride: Int)
// 如
/// 创建顶点坐标
let vertex:[Float] = [-1,1,-5,
1,1,-5,
1,-1,-5,
-1,-1,-5]
/// 创建接受顶点的对象
let vertexSource = SCNGeometrySource(data: getData(array: vertex), semantic: SCNGeometrySource.Semantic.vertex, vectorCount: 4, usesFloatComponents: true, componentsPerVector: 3, bytesPerComponent: MemoryLayout<Float>.size, dataOffset: 0, dataStride: MemoryLayout<Float>.size*3)
print(vertexSource.debugDescription)
/// 获取数据
func getData<T>(array:[T])->Data{
let data:UnsafeMutableRawPointer = malloc(MemoryLayout<T>.size*array.count)
data.initializeMemory(as: T.self, from: array)
return NSData(bytesNoCopy: data, length: MemoryLayout<T>.size*array.count, freeWhenDone: true) as Data
}
复制代码
2. 创建Element
仿照以前OC的写法为:
//`[UInt8]`时
let solidIndexData = Data(bytes: cubeSolidIndices())
let lineElement2 = SCNGeometryElement(data: solidIndexData, primitiveType: .triangles, primitiveCount: 12, bytesPerIndex: MemoryLayout<UInt8>.size)
//`[UInt32]`时(即GLuint),或参照第1步中`func getData<T>(array:[T])->Data`方法
let ptr = UnsafeBufferPointer(start: solidIndices, count: solidIndices.count)
let solidIndexData = Data(buffer: ptr)
let solidElement = SCNGeometryElement(data: solidIndexData, primitiveType: .triangles, primitiveCount: 12, bytesPerIndex: MemoryLayout<UInt32>.size)
复制代码
需要注意的是cubeSolidIndices()
返回类型为[UInt8]
(UInt32/GLuint也可以,但不能用Int),相应的,bytesPerIndex
应为MemoryLayout<UInt8>.size
.
但swift中其实有更简单写法:
let solidElement = SCNGeometryElement(indices: cubeSolidIndices(), primitiveType: .triangles)
复制代码
3. 创建Geometry
将顶点,法线,uv数据传入,以及实体element:
let geometry = SCNGeometry(sources: [vertexSource, normalSource, uvSource], elements: [solidElement,lineElement])
复制代码
4. 设置Material
//设置材质,面/线
let solidMaterial = SCNMaterial()
solidMaterial.diffuse.contents = UIColor(red: 4/255.0, green: 120.0/255.0, blue: 255.0/255.0, alpha: 1.0)
solidMaterial.locksAmbientWithDiffuse = true
let lineMaterial = SCNMaterial()
lineMaterial.diffuse.contents = UIColor.white
lineMaterial.lightingModel = .constant
//设置到几何体上
geometry.materials = [solidMaterial,lineMaterial]
复制代码
5. 创建Node
根据geometry创建Node,并添加到根节点上:
let cubeNode = SCNNode(geometry: geometry)
scene.rootNode.addChildNode(cubeNode)
复制代码
最终结果
顶点顺序与对应关系
cubeVertices重复三次
正方体中明明只有8个顶点,为什么cubeVertices()
中要重复三次?
这是因为,一个顶点被三个面共用,而每个面的法线和UV贴图不同,直接共用顶点的话,法线和贴图会出错.
cubeVertices中顶点顺序
cubeVertices()
中顶点的顺序如下图:
而法线cubeNormals()
和贴图cubeUVs()
中也是按相同顺序排列的.
- 同一个面的法线方向一致,都是朝外的(从中心指向四周);
- UV也对应同一个面的四个点,刚好能铺满整个UV空间(0~1);
三角形正反面
默认情况下只能看到顶点组成的三角形的正面,即:面对镜头,顶点逆时针为正;背对镜头,顶点排列顺时针为正;
可以通过SCNMaterial中的
cullMode
和isDoubleSided
来控制显示.
因此,顶点索引cubeSolidIndices()
中以底面为例:
// bottom
0, 2, 1,
1, 2, 3,
复制代码
这个面是背对镜头的,所以组成的两个三角形均是顺时针的,即正面是朝下,就是说可以从下面看到:
其余面同理.
加载外部工具创建的几何形状
在SceneKit和ARKit中,如果将外部3D模型拖拽到Xcode中再通过代码加载,是可行的;但是如果模型的格式Xcode不支持或者需要app在发布后再联网下载,那么用代码直接打开就不可行.
因为3D拖拽到Xcode中时苹果做了一些格式处理才能在Xcode中用代码加载,瑞联网下载的没有处理过,不可行.
网上也有一些方法通过调用Xcode的copySceneKitAssets
、scntool
来手动处理再下发的,但这其实属于非正规方法.正确的方法是使用ModelIO框架,它支持的格式有:
支持导入格式:
- Alembic .abc
- Polygon .ply
- Triangles .stl
- Wavefront .obj
输出格式:
- Triangles .stl
- Wavefront .obj
ModelIO的功能也非常强大,模型导入/导出,烘焙灯光,处理纹理,改变形状等等:
此处我们只介绍模型加载功能,需要先导入头文件:
import ModelIO
import SceneKit.ModelIO
复制代码
// 加载 .OBJ文件
guard let url = NSBundle.mainBundle().URLForResource("Fighter", withExtension: "obj") else {
fatalError("Failed to find model file.")
}
let asset = MDLAsset(URL:url)
guard let object = asset.objectAtIndex(0) as? MDLMesh else {
fatalError("Failed to get mesh from asset.")
}
// 将ModelIO对象包装成SceneKit对象,调整大小和位置
let node = SCNNode(mdlObject: object) //需要导入头文件`import SceneKit.ModelIO`
node.scale = SCNVector3Make(0.05, 0.05, 0.05)
node.position = SCNVector3Make(0, -20, 0)
cubeView.scene?.rootNode.addChildNode(node)
复制代码
效果如图:
还需要加载图片纹理:
extension MDLMaterial {
func setTextureProperties(textures: [MDLMaterialSemantic:String]) -> Void {
for (key,value) in textures {
guard let url = NSBundle.mainBundle().URLForResource(value, withExtension: "") else {
fatalError("Failed to find URL for resource \(value).")
}
let property = MDLMaterialProperty(name:value, semantic: key, URL: url)
self.setProperty(property)
}
}
}
// 创建各种纹理的材质
let scatteringFunction = MDLScatteringFunction()
let material = MDLMaterial(name: "baseMaterial", scatteringFunction: scatteringFunction)
material.setTextureProperties([
.baseColor:"Fighter_Diffuse_25.jpg",
.specular:"Fighter_Specular_25.jpg",
.emission:"Fighter_Illumination_25.jpg"])
// 将纹理应用到每个网格上
for submesh in object.submeshes! {
if let submesh = submesh as? MDLSubmesh {
submesh.material = material
}
}
复制代码
最终结果: