464 lines
14 KiB
Swift
464 lines
14 KiB
Swift
/// AssetLoader - Asset management and loading system
|
|
/// NO PLATFORM-SPECIFIC IMPORTS ALLOWED IN THIS MODULE
|
|
/// Handles loading of 3D models, textures, animations, and audio files
|
|
|
|
import Foundation
|
|
|
|
// MARK: - Asset Types
|
|
|
|
/// Asset loading result
|
|
public enum AssetResult<T> {
|
|
case success(T)
|
|
case failure(AssetError)
|
|
}
|
|
|
|
/// Asset loading errors
|
|
public enum AssetError: Error, LocalizedError {
|
|
case fileNotFound(String)
|
|
case invalidFormat(String)
|
|
case corruptedData(String)
|
|
case unsupportedVersion(String)
|
|
case outOfMemory
|
|
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .fileNotFound(let path):
|
|
return "Asset file not found: \(path)"
|
|
case .invalidFormat(let format):
|
|
return "Invalid asset format: \(format)"
|
|
case .corruptedData(let reason):
|
|
return "Corrupted asset data: \(reason)"
|
|
case .unsupportedVersion(let version):
|
|
return "Unsupported asset version: \(version)"
|
|
case .outOfMemory:
|
|
return "Out of memory while loading asset"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Asset handle for reference counting and management
|
|
public struct AssetHandle<T>: Hashable, Sendable where T: Hashable {
|
|
public let id: UUID
|
|
public let path: String
|
|
|
|
public init(id: UUID = UUID(), path: String) {
|
|
self.id = id
|
|
self.path = path
|
|
}
|
|
|
|
public func hash(into hasher: inout Hasher) {
|
|
hasher.combine(id)
|
|
}
|
|
}
|
|
|
|
// MARK: - Mesh Data
|
|
|
|
/// Mesh data structure (vertex and index buffers)
|
|
public struct MeshData: Sendable, Hashable {
|
|
public var name: String
|
|
public var vertices: [MeshVertex]
|
|
public var indices: [UInt32]
|
|
public var bounds: BoundingBox
|
|
|
|
public init(name: String, vertices: [MeshVertex], indices: [UInt32], bounds: BoundingBox) {
|
|
self.name = name
|
|
self.vertices = vertices
|
|
self.indices = indices
|
|
self.bounds = bounds
|
|
}
|
|
|
|
public func hash(into hasher: inout Hasher) {
|
|
hasher.combine(name)
|
|
}
|
|
}
|
|
|
|
/// Vertex structure for mesh data
|
|
public struct MeshVertex: Sendable, Hashable {
|
|
public var position: SIMD3<Float>
|
|
public var normal: SIMD3<Float>
|
|
public var uv: SIMD2<Float>
|
|
public var tangent: SIMD3<Float>
|
|
public var boneIndices: SIMD4<UInt8> // For skeletal animation
|
|
public var boneWeights: SIMD4<Float> // For skeletal animation
|
|
|
|
public init(position: SIMD3<Float>, normal: SIMD3<Float>, uv: SIMD2<Float>,
|
|
tangent: SIMD3<Float> = .zero, boneIndices: SIMD4<UInt8> = .zero,
|
|
boneWeights: SIMD4<Float> = .zero) {
|
|
self.position = position
|
|
self.normal = normal
|
|
self.uv = uv
|
|
self.tangent = tangent
|
|
self.boneIndices = boneIndices
|
|
self.boneWeights = boneWeights
|
|
}
|
|
}
|
|
|
|
/// Bounding box for culling
|
|
public struct BoundingBox: Sendable, Hashable {
|
|
public var min: SIMD3<Float>
|
|
public var max: SIMD3<Float>
|
|
|
|
public init(min: SIMD3<Float>, max: SIMD3<Float>) {
|
|
self.min = min
|
|
self.max = max
|
|
}
|
|
}
|
|
|
|
// MARK: - Texture Data
|
|
|
|
/// Texture data structure
|
|
public struct TextureData: Sendable, Hashable {
|
|
public var name: String
|
|
public var width: Int
|
|
public var height: Int
|
|
public var format: TextureFormat
|
|
public var data: Data
|
|
public var mipLevels: Int
|
|
|
|
public init(name: String, width: Int, height: Int, format: TextureFormat, data: Data, mipLevels: Int = 1) {
|
|
self.name = name
|
|
self.width = width
|
|
self.height = height
|
|
self.format = format
|
|
self.data = data
|
|
self.mipLevels = mipLevels
|
|
}
|
|
|
|
public func hash(into hasher: inout Hasher) {
|
|
hasher.combine(name)
|
|
}
|
|
}
|
|
|
|
/// Texture format
|
|
public enum TextureFormat: Sendable, Hashable {
|
|
case rgba8
|
|
case rgba16f
|
|
case rgba32f
|
|
case dxt1
|
|
case dxt5
|
|
case bc7
|
|
}
|
|
|
|
// MARK: - Animation Data
|
|
|
|
/// Skeletal animation data
|
|
public struct AnimationData: Sendable, Hashable {
|
|
public var name: String
|
|
public var duration: Float
|
|
public var ticksPerSecond: Float
|
|
public var channels: [AnimationChannel]
|
|
|
|
public init(name: String, duration: Float, ticksPerSecond: Float, channels: [AnimationChannel]) {
|
|
self.name = name
|
|
self.duration = duration
|
|
self.ticksPerSecond = ticksPerSecond
|
|
self.channels = channels
|
|
}
|
|
|
|
public func hash(into hasher: inout Hasher) {
|
|
hasher.combine(name)
|
|
}
|
|
}
|
|
|
|
/// Animation channel (per-bone)
|
|
public struct AnimationChannel: Sendable, Hashable {
|
|
public var boneName: String
|
|
public var positionKeys: [KeyFrame<SIMD3<Float>>]
|
|
public var rotationKeys: [KeyFrame<SIMD4<Float>>]
|
|
public var scaleKeys: [KeyFrame<SIMD3<Float>>]
|
|
|
|
public init(boneName: String, positionKeys: [KeyFrame<SIMD3<Float>>],
|
|
rotationKeys: [KeyFrame<SIMD4<Float>>], scaleKeys: [KeyFrame<SIMD3<Float>>]) {
|
|
self.boneName = boneName
|
|
self.positionKeys = positionKeys
|
|
self.rotationKeys = rotationKeys
|
|
self.scaleKeys = scaleKeys
|
|
}
|
|
}
|
|
|
|
/// Animation keyframe
|
|
public struct KeyFrame<T: Sendable & Hashable>: Sendable, Hashable {
|
|
public var time: Float
|
|
public var value: T
|
|
|
|
public init(time: Float, value: T) {
|
|
self.time = time
|
|
self.value = value
|
|
}
|
|
}
|
|
|
|
// MARK: - Skeleton Data
|
|
|
|
/// Skeletal hierarchy for character animation
|
|
public struct SkeletonData: Sendable, Hashable {
|
|
public var bones: [Bone]
|
|
public var rootBoneIndex: Int
|
|
|
|
public init(bones: [Bone], rootBoneIndex: Int = 0) {
|
|
self.bones = bones
|
|
self.rootBoneIndex = rootBoneIndex
|
|
}
|
|
}
|
|
|
|
/// Individual bone in skeleton
|
|
public struct Bone: Sendable, Hashable {
|
|
public var name: String
|
|
public var parentIndex: Int?
|
|
public var offsetMatrix: float4x4
|
|
|
|
public init(name: String, parentIndex: Int?, offsetMatrix: float4x4) {
|
|
self.name = name
|
|
self.parentIndex = parentIndex
|
|
self.offsetMatrix = offsetMatrix
|
|
}
|
|
}
|
|
|
|
// MARK: - Asset Manager
|
|
|
|
/// Main asset management system
|
|
public actor AssetManager {
|
|
|
|
private var meshCache: [String: MeshData] = [:]
|
|
private var textureCache: [String: TextureData] = [:]
|
|
private var animationCache: [String: AnimationData] = [:]
|
|
private var skeletonCache: [String: SkeletonData] = [:]
|
|
|
|
private var loadedAssets: Set<String> = []
|
|
|
|
public init() {}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
public func initialize() {
|
|
print(" ✓ Asset manager initialized")
|
|
}
|
|
|
|
public func shutdown() {
|
|
meshCache.removeAll()
|
|
textureCache.removeAll()
|
|
animationCache.removeAll()
|
|
skeletonCache.removeAll()
|
|
loadedAssets.removeAll()
|
|
|
|
print(" ✓ Asset manager shutdown (cleared caches)")
|
|
}
|
|
|
|
// MARK: - Mesh Loading
|
|
|
|
/// Load a 3D mesh from file
|
|
public func loadMesh(path: String) async -> AssetResult<MeshData> {
|
|
// Check cache first
|
|
if let cached = meshCache[path] {
|
|
return .success(cached)
|
|
}
|
|
|
|
// Load from disk
|
|
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
|
|
return .failure(.fileNotFound(path))
|
|
}
|
|
|
|
// Parse mesh format (OBJ, FBX, GLTF, etc.)
|
|
guard let mesh = parseMeshData(data: data, path: path) else {
|
|
return .failure(.invalidFormat(path))
|
|
}
|
|
|
|
// Cache and return
|
|
meshCache[path] = mesh
|
|
loadedAssets.insert(path)
|
|
|
|
return .success(mesh)
|
|
}
|
|
|
|
private func parseMeshData(data: Data, path: String) -> MeshData? {
|
|
// Simplified placeholder - would parse actual formats
|
|
// For sports games: support FBX (player models), GLTF (stadiums), custom formats
|
|
|
|
// Create a simple test mesh (placeholder)
|
|
return createTestMesh(name: path)
|
|
}
|
|
|
|
private func createTestMesh(name: String) -> MeshData {
|
|
// Create a simple cube for testing
|
|
let vertices: [MeshVertex] = [
|
|
MeshVertex(position: SIMD3<Float>(-1, -1, -1), normal: SIMD3<Float>(0, 0, -1), uv: SIMD2<Float>(0, 0)),
|
|
MeshVertex(position: SIMD3<Float>(1, -1, -1), normal: SIMD3<Float>(0, 0, -1), uv: SIMD2<Float>(1, 0)),
|
|
MeshVertex(position: SIMD3<Float>(1, 1, -1), normal: SIMD3<Float>(0, 0, -1), uv: SIMD2<Float>(1, 1)),
|
|
MeshVertex(position: SIMD3<Float>(-1, 1, -1), normal: SIMD3<Float>(0, 0, -1), uv: SIMD2<Float>(0, 1)),
|
|
]
|
|
|
|
let indices: [UInt32] = [0, 1, 2, 0, 2, 3]
|
|
|
|
let bounds = BoundingBox(
|
|
min: SIMD3<Float>(-1, -1, -1),
|
|
max: SIMD3<Float>(1, 1, 1)
|
|
)
|
|
|
|
return MeshData(name: name, vertices: vertices, indices: indices, bounds: bounds)
|
|
}
|
|
|
|
// MARK: - Texture Loading
|
|
|
|
/// Load a texture from file
|
|
public func loadTexture(path: String) async -> AssetResult<TextureData> {
|
|
// Check cache first
|
|
if let cached = textureCache[path] {
|
|
return .success(cached)
|
|
}
|
|
|
|
// Load from disk
|
|
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
|
|
return .failure(.fileNotFound(path))
|
|
}
|
|
|
|
// Parse texture format (PNG, JPG, DDS, KTX, etc.)
|
|
guard let texture = parseTextureData(data: data, path: path) else {
|
|
return .failure(.invalidFormat(path))
|
|
}
|
|
|
|
// Cache and return
|
|
textureCache[path] = texture
|
|
loadedAssets.insert(path)
|
|
|
|
return .success(texture)
|
|
}
|
|
|
|
private func parseTextureData(data: Data, path: String) -> TextureData? {
|
|
// Simplified placeholder - would use image decoding libraries
|
|
// For sports games: support DDS (compressed), PNG, JPG, HDR textures
|
|
|
|
// Create a simple test texture
|
|
return createTestTexture(name: path)
|
|
}
|
|
|
|
private func createTestTexture(name: String) -> TextureData {
|
|
// Create a 2x2 test texture (white)
|
|
let width = 2
|
|
let height = 2
|
|
var data = Data(count: width * height * 4)
|
|
for i in 0..<(width * height * 4) {
|
|
data[i] = 255
|
|
}
|
|
|
|
return TextureData(name: name, width: width, height: height, format: .rgba8, data: data)
|
|
}
|
|
|
|
// MARK: - Animation Loading
|
|
|
|
/// Load skeletal animation from file
|
|
public func loadAnimation(path: String) async -> AssetResult<AnimationData> {
|
|
// Check cache first
|
|
if let cached = animationCache[path] {
|
|
return .success(cached)
|
|
}
|
|
|
|
// Load from disk
|
|
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
|
|
return .failure(.fileNotFound(path))
|
|
}
|
|
|
|
// Parse animation format
|
|
guard let animation = parseAnimationData(data: data, path: path) else {
|
|
return .failure(.invalidFormat(path))
|
|
}
|
|
|
|
// Cache and return
|
|
animationCache[path] = animation
|
|
loadedAssets.insert(path)
|
|
|
|
return .success(animation)
|
|
}
|
|
|
|
private func parseAnimationData(data: Data, path: String) -> AnimationData? {
|
|
// Simplified placeholder - would parse FBX/GLTF animations
|
|
// For sports games: running, shooting, passing, tackling animations
|
|
return createTestAnimation(name: path)
|
|
}
|
|
|
|
private func createTestAnimation(name: String) -> AnimationData {
|
|
// Create a simple test animation
|
|
let channel = AnimationChannel(
|
|
boneName: "Root",
|
|
positionKeys: [
|
|
KeyFrame(time: 0.0, value: SIMD3<Float>(0, 0, 0)),
|
|
KeyFrame(time: 1.0, value: SIMD3<Float>(1, 0, 0))
|
|
],
|
|
rotationKeys: [
|
|
KeyFrame(time: 0.0, value: SIMD4<Float>(0, 0, 0, 1)),
|
|
KeyFrame(time: 1.0, value: SIMD4<Float>(0, 0, 0, 1))
|
|
],
|
|
scaleKeys: [
|
|
KeyFrame(time: 0.0, value: SIMD3<Float>(1, 1, 1)),
|
|
KeyFrame(time: 1.0, value: SIMD3<Float>(1, 1, 1))
|
|
]
|
|
)
|
|
|
|
return AnimationData(name: name, duration: 1.0, ticksPerSecond: 30.0, channels: [channel])
|
|
}
|
|
|
|
// MARK: - Skeleton Loading
|
|
|
|
/// Load skeletal hierarchy from file
|
|
public func loadSkeleton(path: String) async -> AssetResult<SkeletonData> {
|
|
// Check cache first
|
|
if let cached = skeletonCache[path] {
|
|
return .success(cached)
|
|
}
|
|
|
|
// Load from disk
|
|
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
|
|
return .failure(.fileNotFound(path))
|
|
}
|
|
|
|
// Parse skeleton format
|
|
guard let skeleton = parseSkeletonData(data: data, path: path) else {
|
|
return .failure(.invalidFormat(path))
|
|
}
|
|
|
|
// Cache and return
|
|
skeletonCache[path] = skeleton
|
|
loadedAssets.insert(path)
|
|
|
|
return .success(skeleton)
|
|
}
|
|
|
|
private func parseSkeletonData(data: Data, path: String) -> SkeletonData? {
|
|
// Simplified placeholder
|
|
return createTestSkeleton()
|
|
}
|
|
|
|
private func createTestSkeleton() -> SkeletonData {
|
|
let rootBone = Bone(name: "Root", parentIndex: nil, offsetMatrix: matrix_identity_float4x4)
|
|
return SkeletonData(bones: [rootBone], rootBoneIndex: 0)
|
|
}
|
|
|
|
// MARK: - Cache Management
|
|
|
|
public func unloadAsset(path: String) {
|
|
meshCache.removeValue(forKey: path)
|
|
textureCache.removeValue(forKey: path)
|
|
animationCache.removeValue(forKey: path)
|
|
skeletonCache.removeValue(forKey: path)
|
|
loadedAssets.remove(path)
|
|
}
|
|
|
|
public func clearCache() {
|
|
meshCache.removeAll()
|
|
textureCache.removeAll()
|
|
animationCache.removeAll()
|
|
skeletonCache.removeAll()
|
|
loadedAssets.removeAll()
|
|
}
|
|
|
|
public func getCacheSize() -> (meshes: Int, textures: Int, animations: Int, skeletons: Int) {
|
|
return (meshCache.count, textureCache.count, animationCache.count, skeletonCache.count)
|
|
}
|
|
}
|
|
|
|
// MARK: - Matrix Helper
|
|
|
|
extension float4x4 {
|
|
static var identity: float4x4 {
|
|
return matrix_identity_float4x4
|
|
}
|
|
}
|
|
|