| 143 | * FBX 模型加载器 |
| 144 | */ |
| 145 | export class FBXLoader implements IAssetLoader<IGLTFAsset> { |
| 146 | readonly supportedType = AssetType.Model3D; |
| 147 | readonly supportedExtensions = ['.fbx']; |
| 148 | readonly contentType: AssetContentType = 'binary'; |
| 149 | |
| 150 | // Parsing state |
| 151 | private buffer: ArrayBuffer = new ArrayBuffer(0); |
| 152 | private view: DataView = new DataView(this.buffer); |
| 153 | private offset = 0; |
| 154 | private version = 0; |
| 155 | |
| 156 | /** |
| 157 | * Parse FBX content |
| 158 | * 解析 FBX 内容 |
| 159 | */ |
| 160 | async parse(content: IAssetContent, context: IAssetParseContext): Promise<IGLTFAsset> { |
| 161 | const buffer = content.binary; |
| 162 | if (!buffer) { |
| 163 | throw new Error('FBX loader requires binary content'); |
| 164 | } |
| 165 | |
| 166 | // Detect format (binary or ASCII) |
| 167 | // 检测格式(二进制或 ASCII) |
| 168 | // FBX binary header is "Kaydara FBX Binary \0" (21 bytes + null) |
| 169 | // FBX 二进制头是 "Kaydara FBX Binary \0"(21字节 + 空字节) |
| 170 | const headerBytes = new Uint8Array(buffer, 0, Math.min(21, buffer.byteLength)); |
| 171 | const headerString = String.fromCharCode(...headerBytes); |
| 172 | const isBinary = headerString.startsWith('Kaydara FBX Binary'); |
| 173 | |
| 174 | let geometries: FBXGeometry[]; |
| 175 | let models: FBXModel[]; |
| 176 | let materials: FBXMaterial[]; |
| 177 | let animStacks: FBXAnimationStack[] = []; |
| 178 | let deformers: FBXDeformer[] = []; |
| 179 | let connections: FBXConnection[] = []; |
| 180 | |
| 181 | if (isBinary) { |
| 182 | const result = this.parseBinary(buffer); |
| 183 | geometries = result.geometries; |
| 184 | models = result.models; |
| 185 | materials = result.materials; |
| 186 | animStacks = result.animStacks; |
| 187 | deformers = result.deformers; |
| 188 | connections = result.connections; |
| 189 | } else { |
| 190 | // Try ASCII parsing (no animation support for ASCII yet) |
| 191 | // 尝试 ASCII 解析(ASCII 格式暂不支持动画) |
| 192 | const text = new TextDecoder().decode(buffer); |
| 193 | const result = this.parseASCII(text); |
| 194 | geometries = result.geometries; |
| 195 | models = result.models; |
| 196 | materials = result.materials; |
| 197 | } |
| 198 | |
| 199 | // Build skeleton data FIRST to get cluster -> joint index mapping |
| 200 | // 先构建骨骼数据以获取簇->关节索引映射 |
| 201 | const clusterToJointIndex = new Map<bigint, number>(); |
| 202 | const skeleton = this.buildSkeletonData(deformers, models, connections, clusterToJointIndex) ?? undefined; |
nothing calls this directly
no outgoing calls
no test coverage detected