Skip to content

Commit

Permalink
Add RealityKit model import
Browse files Browse the repository at this point in the history
  • Loading branch information
nicklockwood committed Dec 22, 2023
1 parent 9c56a9a commit b380fc5
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 16 deletions.
4 changes: 4 additions & 0 deletions Euclid.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
0162A09623795E260078AE84 /* Euclid.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 016FAB2921BFE78100AF60DC /* Euclid.framework */; platformFilters = (ios, maccatalyst, ); };
0162A09923795EB30078AE84 /* Euclid.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 016FAB2921BFE78100AF60DC /* Euclid.framework */; };
016A77F82B2F7C7800B7AB73 /* MeshImportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016A77F72B2F7C7800B7AB73 /* MeshImportTests.swift */; };
016A77FA2B32184A00B7AB73 /* RealityKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016A77F92B32184A00B7AB73 /* RealityKitTests.swift */; };
016FAB3A21BFE78100AF60DC /* Euclid.h in Headers */ = {isa = PBXBuildFile; fileRef = 016FAB2C21BFE78100AF60DC /* Euclid.h */; settings = {ATTRIBUTES = (Public, ); }; };
016FAB4F21BFE7C200AF60DC /* Vector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016FAB4321BFE7C100AF60DC /* Vector.swift */; };
016FAB5021BFE7C200AF60DC /* Mesh+CSG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016FAB4421BFE7C100AF60DC /* Mesh+CSG.swift */; };
Expand Down Expand Up @@ -125,6 +126,7 @@
0148ECA52783796A00B3F836 /* PathPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathPoint.swift; sourceTree = "<group>"; };
014AC60D2505963800F54349 /* SceneKitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneKitTests.swift; sourceTree = "<group>"; };
016A77F72B2F7C7800B7AB73 /* MeshImportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshImportTests.swift; sourceTree = "<group>"; };
016A77F92B32184A00B7AB73 /* RealityKitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealityKitTests.swift; sourceTree = "<group>"; };
016FAB2921BFE78100AF60DC /* Euclid.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Euclid.framework; sourceTree = BUILT_PRODUCTS_DIR; };
016FAB2C21BFE78100AF60DC /* Euclid.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Euclid.h; sourceTree = "<group>"; };
016FAB2D21BFE78100AF60DC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand Down Expand Up @@ -301,6 +303,7 @@
01FAE7BB29744C22008DB288 /* PolygonCSGTests.swift */,
013312DC21CA532A00626F1B /* PlaneTests.swift */,
014AC60D2505963800F54349 /* SceneKitTests.swift */,
016A77F92B32184A00B7AB73 /* RealityKitTests.swift */,
01A429F92237A85C00C251A6 /* TextTests.swift */,
016FAB5B21BFE7CD00AF60DC /* TransformTests.swift */,
01B9F60F292BD7FE002CC3EB /* StretchableTests.swift */,
Expand Down Expand Up @@ -571,6 +574,7 @@
014AC60E2505963800F54349 /* SceneKitTests.swift in Sources */,
016FAB6121BFE7CE00AF60DC /* TransformTests.swift in Sources */,
016FAB6421BFE7CE00AF60DC /* PolygonTests.swift in Sources */,
016A77FA2B32184A00B7AB73 /* RealityKitTests.swift in Sources */,
01B299572692339200B80A0D /* MetadataTests.swift in Sources */,
0101BAF525687A450096B1E7 /* CodingTests.swift in Sources */,
016FAB6521BFE7CE00AF60DC /* MeshCSGTests.swift in Sources */,
Expand Down
230 changes: 217 additions & 13 deletions Sources/Euclid+RealityKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,12 @@ import CoreGraphics
import Metal
import RealityKit

// MARK: export

@available(macOS 10.15, iOS 13.0, *)
public extension RealityKit.Transform {
/// Creates a RealityKit`Transform` from a Euclid ``Transform``.
/// - Parameter transform: The Euclid transform to convert into a RealityKit transform.
init(_ transform: Euclid.Transform) {
self.init(
scale: .init(transform.scale),
Expand All @@ -49,8 +53,6 @@ public extension RealityKit.Transform {
@available(macOS 12.0, iOS 15.0, *)
private func defaultMaterialLookup(_ material: Polygon.Material?) -> RealityKit.Material? {
switch material {
case let material as RealityKit.Material:
return material
case let color as Color:
return defaultMaterialLookup(OSColor(color))
case let color as OSColor:
Expand Down Expand Up @@ -84,8 +86,7 @@ private func defaultMaterialLookup(_ material: Polygon.Material?) -> RealityKit.
@available(macOS 12.0, iOS 15.0, *)
public extension MeshDescriptor {
/// Creates a mesh descriptor from a ``Mesh`` using triangles.
/// - Parameters:
/// - mesh: The mesh to convert into a RealityKit mesh descriptor.
/// - Parameter triangles: The mesh to convert into a RealityKit mesh descriptor.
init(triangles mesh: Mesh) {
self.init()
var counts: [UInt8]?
Expand All @@ -95,13 +96,15 @@ public extension MeshDescriptor {
self.textureCoordinates = data.texcoords.map(MeshBuffers.TextureCoordinates.init)
self.primitives = .triangles(data.indices)
if !data.materialIndices.isEmpty {
print(data.materialIndices)
self.materials = .perFace(data.materialIndices)
} else if !mesh.materials.isEmpty {
self.materials = .allFaces(0)
}
}

/// Creates a mesh descriptor from a ``Mesh`` using convex polygons.
/// - Parameters:
/// - mesh: The mesh to convert into a RealityKit mesh descriptor.
/// - Parameter polygons: The mesh to convert into a RealityKit mesh descriptor.
init(polygons mesh: Mesh) {
self.init()
var counts: [UInt8]? = []
Expand All @@ -112,12 +115,13 @@ public extension MeshDescriptor {
self.primitives = .polygons(counts!, data.indices)
if !data.materialIndices.isEmpty {
self.materials = .perFace(data.materialIndices)
} else if !mesh.materials.isEmpty {
self.materials = .allFaces(0)
}
}

/// Creates a mesh descriptor from a ``Mesh`` using quads where possible (and triangles as required).
/// - Parameters:
/// - mesh: The mesh to convert into a RealityKit mesh descriptor.
/// - Parameter quads: The mesh to convert into a RealityKit mesh descriptor.
init(quads mesh: Mesh) {
self.init()
var counts: [UInt8]? = []
Expand All @@ -140,6 +144,8 @@ public extension MeshDescriptor {
self.primitives = .polygons(counts!, data.indices)
if perFaceMaterials {
self.materials = .perFace(data.materialIndices)
} else if !mesh.materials.isEmpty {
self.materials = .allFaces(0)
}
return
}
Expand All @@ -158,16 +164,18 @@ public extension MeshDescriptor {
}
}
self.materials = .perFace(triangles + quads)
} else if !mesh.materials.isEmpty {
self.materials = .allFaces(0)
}
}
}

@available(macOS 12.0, iOS 15.0, *)
public extension ModelEntity {
/// A closure that maps a Euclid material to a RealityKit material.
/// - Parameter m: A Euclid material to convert, or `nil` for the default material.
/// - Parameter material: A Euclid material to convert, or `nil` for the default material.
/// - Returns: A `Material` used by RealityKit.
typealias MaterialProvider = (_ m: Polygon.Material?) -> RealityKit.Material?
typealias MaterialProvider = (_ material: Polygon.Material?) -> RealityKit.Material?

/// Creates a model entity from a ``Mesh`` using the default tessellation method.
/// - Parameters:
Expand All @@ -179,7 +187,7 @@ public extension ModelEntity {

/// Creates a model entity from a ``Mesh`` using triangles.
/// - Parameters:
/// - mesh: The mesh to convert into a RealityKit model entity.
/// - triangles: The mesh to convert into a RealityKit model entity.
/// - materialLookup: A closure to map the polygon material to a RealityKit material.
convenience init(triangles mesh: Mesh, materialLookup: MaterialProvider? = nil) throws {
let descriptor = MeshDescriptor(triangles: mesh)
Expand All @@ -189,7 +197,7 @@ public extension ModelEntity {

/// Creates a model entity from a ``Mesh`` using convex polygons.
/// - Parameters:
/// - mesh: The mesh to convert into a RealityKit model entity.
/// - polygons: The mesh to convert into a RealityKit model entity.
/// - materialLookup: A closure to map the polygon material to a RealityKit material.
convenience init(polygons mesh: Mesh, materialLookup: MaterialProvider? = nil) throws {
let descriptor = MeshDescriptor(polygons: mesh)
Expand All @@ -199,7 +207,7 @@ public extension ModelEntity {

/// Creates a model entity from a ``Mesh`` using quads where possible (and triangles as required).
/// - Parameters:
/// - mesh: The mesh to convert into a RealityKit model entity.
/// - quads: The mesh to convert into a RealityKit model entity.
/// - materialLookup: A closure to map the polygon material to a RealityKit material.
convenience init(quads mesh: Mesh, materialLookup: MaterialProvider? = nil) throws {
let descriptor = MeshDescriptor(quads: mesh)
Expand Down Expand Up @@ -266,4 +274,200 @@ private extension Mesh {
}
}

// MARK: import

@available(macOS 10.15, iOS 13.0, *)
public extension Euclid.Transform {
/// Creates a Euclid``Transform`` from a RealityKit `Transform`.
/// - Parameter transform: The RealityKit transform to convert into a Euclid transform.
init(_ transform: RealityKit.Transform) {
self.init(
offset: .init(transform.translation),
rotation: .init(transform.rotation),
scale: .init(transform.scale)
)
}

/// Creates a Euclid``Transform`` from a simd matrix.
/// - Parameter matrix: The simd matrix to convert into a Euclid transform.
init(_ matrix: simd_float4x4) {
self.init(RealityKit.Transform(matrix: matrix))
}
}

private extension Array where Element == Polygon {
@available(macOS 12.0, iOS 15.0, *)
init(
meshDescriptor: MeshDescriptor,
indices: [UInt32],
counts: [UInt8],
materials: [Polygon.Material?]
) {
func materialLookup(_ index: Int) -> Polygon.Material? {
materials.isEmpty ? nil : materials[index % materials.count]
}
var materials: [Polygon.Material?]
switch meshDescriptor.materials {
case let .allFaces(index):
materials = [materialLookup(Int(index))]
case let .perFace(indices):
materials = indices.map { materialLookup(Int($0)) }
@unknown default:
materials = []
}
self.init(
positions: meshDescriptor.positions.elements,
normals: meshDescriptor.normals?.elements,
texcoords: meshDescriptor.textureCoordinates?.elements,
indices: indices,
counts: counts,
materials: materials
)
}

init(
positions: [SIMD3<Float>],
normals: [SIMD3<Float>]?,
texcoords: [SIMD2<Float>]?,
indices: [UInt32],
counts: [UInt8],
materials: [Polygon.Material?]
) {
var offset = 0
self = counts.enumerated().compactMap { index, count in
var vertices = [Vertex]()
for i in offset ..< (offset + Int(count)) {
let index = Int(indices[i])
vertices.append(Vertex(
Vector(positions[index]),
Vector(normals?[index] ?? .zero),
texcoords.map {
var texcoord = Vector($0[index])
texcoord.y = 1 - texcoord.y
return texcoord
}
))
}
offset += Int(count)
let material = materials.isEmpty ? nil : materials[index % materials.count]
return Polygon(vertices, material: material)
}
}
}

@available(macOS 12.0, iOS 15.0, *)
private extension Mesh {
/// Creates a mesh from a RealityKit `MeshResource.Model`.
/// - Parameters:
/// - model: The `MeshResource.Model` to convert into a mesh.
/// - materials: An array of materials to apply to the mesh.
init(_ model: MeshResource.Model, materials: [Polygon.Material?]) {
var polygons = [Polygon]()
for part in model.parts {
guard let indices = part.triangleIndices?.elements else {
continue
}
polygons += .init(
positions: part.positions.elements,
normals: part.normals?.elements,
texcoords: part.textureCoordinates?.elements,
indices: indices,
counts: [UInt8](repeating: 3, count: indices.count / 3),
materials: materials.isEmpty ? [] : [materials[part.materialIndex % materials.count]]
)
}
self.init(polygons)
}

/// Creates a mesh from a RealityKit `ModelComponent` with optional material mapping.
/// - Parameters:
/// - component: The `ModelComponent` to convert into a mesh.
/// - materialLookup: An optional closure to map the RealityKit materials to Euclid materials.
init(_ component: ModelComponent, materialLookup: RealityKitMaterialProvider?) {
let materialLookup = materialLookup ?? { _ in nil }
self.init(component.mesh, materials: component.materials.map { materialLookup($0) })
}
}

@available(macOS 12.0, iOS 15.0, *)
public extension Mesh {
/// A closure that converts a RealityKit material to a Euclid material.
/// - Parameter material: A RealityKit material to convert.
/// - Returns: A Euclid `Material`.
typealias RealityKitMaterialProvider = (_ material: RealityKit.Material) -> Polygon.Material?

/// Creates a mesh from a RealityKit `MeshDescriptor` with optional material.
/// - Parameters:
/// - part: The `MeshDescriptor` to convert into a mesh.
/// - materials: An array of materials to apply to the mesh.
init(_ meshDescriptor: MeshDescriptor, materials: [Polygon.Material?] = []) {
guard let primitives = meshDescriptor.primitives else {
self = .empty
return
}
let polygons: [Polygon]
switch primitives {
case let .triangles(indices):
polygons = .init(
meshDescriptor: meshDescriptor,
indices: indices,
counts: [UInt8](repeating: 3, count: indices.count / 3),
materials: materials
)
case let .trianglesAndQuads(triangles, quads):
polygons = .init(
meshDescriptor: meshDescriptor,
indices: triangles,
counts: [UInt8](repeating: 3, count: triangles.count / 3),
materials: materials
) + .init(
meshDescriptor: meshDescriptor,
indices: quads,
counts: [UInt8](repeating: 4, count: quads.count / 4),
materials: materials
)
case let .polygons(counts, indices):
polygons = .init(
meshDescriptor: meshDescriptor,
indices: indices,
counts: counts,
materials: materials
)
@unknown default:
// TODO: throw for unknown type?
polygons = []
}
self.init(polygons)
}

/// Creates a mesh from a RealityKit `MeshResource`.
/// - Parameters:
/// - model: The `MeshResource` to convert into a mesh.
/// - materials: An array of materials to apply to the mesh.
init(_ meshResource: MeshResource, materials: [Polygon.Material?] = []) {
var models = [String: Mesh]()
self.init(submeshes: meshResource.contents.instances.compactMap {
var mesh = models[$0.model]
if mesh == nil, let model = meshResource.contents.models[$0.model] {
let modelMesh = Mesh(model, materials: materials)
models[$0.model] = modelMesh
mesh = modelMesh
}
return mesh?.transformed(by: Transform($0.transform))
})
}

/// Creates a mesh from a RealityKit `ModelEntity` with optional material mapping.
/// - Parameters:
/// - modelEntity: The `ModelEntity` to convert into a mesh.
/// - materialLookup: An optional closure to map the RealityKit materials to Euclid materials.
init(_ modelEntity: ModelEntity, materialLookup: RealityKitMaterialProvider? = nil) {
guard let model = modelEntity.model else {
self = .empty
return
}
self.init(model, materialLookup: materialLookup)
}
}

#endif
Loading

0 comments on commit b380fc5

Please sign in to comment.