Skip to content

Commit 6c9e2b5

Browse files
committed
Modernize Skeleton importing
1 parent e26c00a commit 6c9e2b5

File tree

3 files changed

+93
-40
lines changed

3 files changed

+93
-40
lines changed

Sources/GateEngine/Resources/Import & Export/Importers/GLTransmissionFormat.swift

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -837,26 +837,27 @@ extension GLTransmissionFormat: SkeletonImporter {
837837
}
838838
return gltf.scenes[gltf.scene].nodes?.first
839839
}
840-
841-
public func process(data: Data, baseURL: URL, options: SkeletonImporterOptions) async throws -> Skeleton.Joint {
842-
let gltf = try gltf(from: data, baseURL: baseURL)
840+
841+
public func loadSkeleton(options: SkeletonImporterOptions) async throws(GateEngineError) -> RawSkeleton {
843842
guard let rootNode = skeletonNode(named: options.subobjectName, in: gltf) else {
844843
throw GateEngineError.failedToDecode("Couldn't find skeleton root.")
845844
}
846-
let rootJoint = Skeleton.Joint(id: rootNode, name: gltf.nodes[rootNode].name)
847-
rootJoint.localTransform = gltf.nodes[rootNode].transform
848-
849-
func addChildren(gltfNode: Int, parentJoint: Skeleton.Joint) {
850-
for index in gltf.nodes[gltfNode].children ?? [] {
851-
let node = gltf.nodes[index]
852-
let joint = Skeleton.Joint(id: index, name: node.name)
853-
joint.localTransform = node.transform
854-
joint.parent = parentJoint
855-
addChildren(gltfNode: index, parentJoint: joint)
845+
var rawSkeleton: RawSkeleton = .init()
846+
rawSkeleton.joints.append(
847+
RawSkeleton.RawJoint(id: rootNode, parent: nil, name: gltf.nodes[rootNode].name, localTransform: gltf.nodes[rootNode].transform)
848+
)
849+
850+
func addChildren(of parentJointID: Int) {
851+
for childJointID in gltf.nodes[parentJointID].children ?? [] {
852+
let childNode = gltf.nodes[childJointID]
853+
rawSkeleton.joints.append(
854+
RawSkeleton.RawJoint(id: childJointID, parent: parentJointID, name: childNode.name, localTransform: childNode.transform)
855+
)
856+
addChildren(of: childJointID)
856857
}
857858
}
858-
addChildren(gltfNode: rootNode, parentJoint: rootJoint)
859-
return rootJoint
859+
addChildren(of: rootNode)
860+
return rawSkeleton
860861
}
861862
}
862863

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright © 2025 Dustin Collins (Strega's Gate)
3+
* All Rights Reserved.
4+
*
5+
* http://stregasgate.com
6+
*/
7+
8+
public struct RawSkeleton: Equatable, Hashable, Codable, Sendable {
9+
public var joints: [RawJoint] = []
10+
11+
public struct RawJoint: Equatable, Hashable, Identifiable, Codable, Sendable {
12+
public typealias ID = Int
13+
public let id: ID
14+
public var parent: ID?
15+
public let name: String?
16+
public var localTransform: Transform3
17+
18+
public nonisolated func hash(into hasher: inout Hasher) {
19+
hasher.combine(id)
20+
}
21+
22+
public nonisolated static func == (lhs: RawJoint, rhs: RawJoint) -> Bool {
23+
lhs.id == rhs.id
24+
}
25+
}
26+
}

Sources/GateEngine/Resources/Skinning/Skeleton.swift

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,26 @@ final class SkeletonBackend {
123123
self.rootJoint = joint
124124
self.bindPose = Skeleton.Pose(joint)
125125
}
126+
127+
init(rawSkeleton: RawSkeleton) {
128+
func createJoint(from rawJoint: RawSkeleton.RawJoint) -> Skeleton.Joint {
129+
var joint = Skeleton.Joint(rawJoint: rawJoint)
130+
joint.localTransform = rawJoint.localTransform
131+
for rawJoint in rawSkeleton.joints {
132+
if rawJoint.parent == joint.id {
133+
let child = createJoint(from: rawJoint)
134+
child.parent = joint
135+
}
136+
}
137+
return joint
138+
}
139+
let rootJoint = createJoint(from: rawSkeleton.joints.first(where: {$0.parent == nil})!)
140+
self.rootJoint = rootJoint
141+
self.bindPose = Skeleton.Pose(rootJoint)
142+
}
143+
}
144+
fileprivate extension Skeleton.Joint {
145+
126146
}
127147

128148
extension Skeleton {
@@ -328,6 +348,12 @@ extension Skeleton {
328348
self.id = id
329349
self.name = name
330350
}
351+
352+
/// Convenince for constructing a Skeleton from a RawSkeleton
353+
fileprivate convenience init(rawJoint: RawSkeleton.RawJoint) {
354+
self.init(id: rawJoint.id, name: rawJoint.name)
355+
self.localTransform = rawJoint.localTransform
356+
}
331357
}
332358
}
333359

@@ -473,12 +499,8 @@ extension Skeleton.Pose.Joint: Hashable {
473499

474500
// MARK: - Resource Manager
475501

476-
public protocol SkeletonImporter: AnyObject {
477-
init()
478-
479-
func process(data: Data, baseURL: URL, options: SkeletonImporterOptions) async throws -> Skeleton.Joint
480-
481-
static func supportedFileExtensions() -> [String]
502+
public protocol SkeletonImporter: ResourceImporter {
503+
func loadSkeleton(options: SkeletonImporterOptions) async throws(GateEngineError) -> RawSkeleton
482504
}
483505

484506
public struct SkeletonImporterOptions: Equatable, Hashable, Sendable {
@@ -498,13 +520,11 @@ extension ResourceManager {
498520
guard importers.skeletonImporters.contains(where: { $0 == type }) == false else { return }
499521
importers.skeletonImporters.insert(type, at: 0)
500522
}
501-
502-
fileprivate func importerForFileType(_ file: String) -> (any SkeletonImporter)? {
523+
524+
func skeletonImporterForPath(_ path: String) async throws -> (any SkeletonImporter)? {
503525
for type in self.importers.skeletonImporters {
504-
if type.supportedFileExtensions().contains(where: {
505-
$0.caseInsensitiveCompare(file) == .orderedSame
506-
}) {
507-
return type.init()
526+
if type.canProcessFile(path) {
527+
return try await self.importers.getImporter(path: path, type: type)
508528
}
509529
}
510530
return nil
@@ -556,6 +576,22 @@ extension ResourceManager.Cache {
556576
}
557577
}
558578

579+
extension RawSkeleton {
580+
public init(path: String, options: SkeletonImporterOptions = .none) async throws {
581+
guard
582+
let importer: any SkeletonImporter = try await Game.unsafeShared.resourceManager.skeletonImporterForPath(path)
583+
else {
584+
throw GateEngineError.failedToLoad("No importer for \(URL(fileURLWithPath: path).pathExtension).")
585+
}
586+
587+
do {
588+
self = try await importer.loadSkeleton(options: options)
589+
} catch {
590+
throw GateEngineError(error)
591+
}
592+
}
593+
}
594+
559595
@MainActor
560596
extension ResourceManager {
561597
func changeCacheHint(_ cacheHint: CacheHint, for key: Cache.SkeletonKey) {
@@ -626,23 +662,13 @@ extension ResourceManager {
626662
let path = key.requestedPath
627663

628664
do {
629-
guard let fileExtension = path.components(separatedBy: ".").last else {
630-
throw GateEngineError.failedToLoad("Unknown file type.")
631-
}
632-
guard let importer: any SkeletonImporter = Game.unsafeShared.resourceManager.importerForFileType(fileExtension) else {
633-
throw GateEngineError.failedToLoad("No importer for \(fileExtension).")
634-
}
635-
636-
let data = try await Platform.current.loadResource(from: path)
637-
let rootJoint: Skeleton.Joint = try await importer.process(
638-
data: data,
639-
baseURL: URL(string: path)!.deletingLastPathComponent(),
665+
let rawSkeleton = try await RawSkeleton(
666+
path: key.requestedPath,
640667
options: key.options
641668
)
642-
643669
Task { @MainActor in
644670
if let cache = cache.skeletons[key] {
645-
cache.skeletonBackend = SkeletonBackend(rootJoint: rootJoint)
671+
cache.skeletonBackend = SkeletonBackend(rawSkeleton: rawSkeleton)
646672
cache.state = .ready
647673
}else{
648674
Log.warn("Resource \"\(path)\" was deallocated before being " + (isFirstLoad ? "loaded." : "re-loaded."))

0 commit comments

Comments
 (0)