[ARKit]5-載入自定義幾何體

蘋果API搬運工發表於2018-02-27

說明

本文程式碼地址

ARKit系列文章目錄

通過頂點,法線,紋理座標陣列載入

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)
複製程式碼

最終結果

Simulator Screen Shot.png

頂點順序與對應關係

cubeVertices重複三次

正方體中明明只有8個頂點,為什麼cubeVertices()中要重複三次?
這是因為,一個頂點被三個面共用,而每個面的法線和UV貼圖不同,直接共用頂點的話,法線和貼圖會出錯.

cubeVertices中頂點順序

cubeVertices()中頂點的順序如下圖:

WX20180227-215641@2x.png

而法線cubeNormals()和貼圖cubeUVs()中也是按相同順序排列的.

  • 同一個面的法線方向一致,都是朝外的(從中心指向四周);
  • UV也對應同一個面的四個點,剛好能鋪滿整個UV空間(0~1);

三角形正反面

預設情況下只能看到頂點組成的三角形的正面,即:面對鏡頭,頂點逆時針為正;背對鏡頭,頂點排列順時針為正;

可以通過SCNMaterial中的cullModeisDoubleSided來控制顯示.

因此,頂點索引cubeSolidIndices()中以底面為例:

// bottom
0, 2, 1,
1, 2, 3,
複製程式碼

這個面是背對鏡頭的,所以組成的兩個三角形均是順時針的,即正面是朝下,就是說可以從下面看到:

WX20180227-221103@2x.png

其餘面同理.

載入外部工具建立的幾何形狀

在SceneKit和ARKit中,如果將外部3D模型拖拽到Xcode中再通過程式碼載入,是可行的;但是如果模型的格式Xcode不支援或者需要app在釋出後再聯網下載,那麼用程式碼直接開啟就不可行.

因為3D拖拽到Xcode中時蘋果做了一些格式處理才能在Xcode中用程式碼載入,瑞聯網下載的沒有處理過,不可行.

網上也有一些方法通過呼叫Xcode的copySceneKitAssetsscntool來手動處理再下發的,但這其實屬於非正規方法.正確的方法是使用ModelIO框架,它支援的格式有:

支援匯入格式:

  • Alembic .abc
  • Polygon .ply
  • Triangles .stl
  • Wavefront .obj

輸出格式:

  • Triangles .stl
  • Wavefront .obj

ModelIO的功能也非常強大,模型匯入/匯出,烘焙燈光,處理紋理,改變形狀等等:

WX20180304-094002@2x.png
WX20180304-095126@2x.png

此處我們只介紹模型載入功能,需要先匯入標頭檔案:

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)
複製程式碼

效果如圖:

FighterNoTexture.jpg

還需要載入圖片紋理:

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
      }
}
複製程式碼

最終結果:

Fighter.jpg

相關文章