using GLTF.Schema; using System; using System.Collections.Generic; using System.IO; namespace GLTF { public enum ChunkFormat : uint { JSON = 0x4e4f534a, BIN = 0x004e4942 } /// /// Information containing parsed GLB Header /// public struct GLBHeader { public uint Version { get; set; } public uint FileLength { get; set; } } /// /// Infomration that contains parsed chunk /// public struct ChunkInfo { public long StartPosition; public uint Length; public ChunkFormat Type; } public class GLTFParser { public static readonly uint HEADER_SIZE = 12; public static readonly uint CHUNK_HEADER_SIZE = 8; public static readonly uint MAGIC_NUMBER = 0x46546c67; public static void ParseJson(Stream stream, out GLTFRoot gltfRoot, long startPosition = 0) { stream.Position = startPosition; bool isGLB = IsGLB(stream); // Check for binary format magic bytes if (isGLB) { ParseJsonChunk(stream, startPosition); } else { stream.Position = startPosition; } gltfRoot = GLTFRoot.Deserialize(new StreamReader(stream)); gltfRoot.IsGLB = isGLB; } // todo: this needs reimplemented. There is no such thing as a binary chunk index, and the chunk may not be in 0, 1, 2 order // Moves stream position to binary chunk location public static ChunkInfo SeekToBinaryChunk(Stream stream, int binaryChunkIndex, long startPosition = 0) { stream.Position = startPosition + 4; // start after magic number chunk GLBHeader header = ParseGLBHeader(stream); uint chunkOffset = 12; // sizeof(GLBHeader) + magic number uint chunkLength = 0; for (int i = 0; i < binaryChunkIndex + 2; ++i) { chunkOffset += chunkLength; stream.Position = chunkOffset; chunkLength = GetUInt32(stream); chunkOffset += 8; // to account for chunk length (4 bytes) and type (4 bytes) } // Load Binary Chunk if (chunkOffset + chunkLength <= header.FileLength) { ChunkFormat chunkType = (ChunkFormat)GetUInt32(stream); if (chunkType != ChunkFormat.BIN) { throw new GLTFHeaderInvalidException("Second chunk must be of type BIN if present"); } return new ChunkInfo { StartPosition = stream.Position - CHUNK_HEADER_SIZE, Length = chunkLength, Type = chunkType }; } // Be aware that File length does not match header when MeshOpt compression is used! //throw new GLTFHeaderInvalidException("File length does not match chunk header."); return new ChunkInfo { StartPosition = stream.Position - CHUNK_HEADER_SIZE, Length = chunkLength, Type = ChunkFormat.BIN }; } public static GLBHeader ParseGLBHeader(Stream stream) { uint version = GetUInt32(stream); // 4 uint length = GetUInt32(stream); // 8 return new GLBHeader { Version = version, FileLength = length }; } public static bool IsGLB(Stream stream) { return GetUInt32(stream) == 0x46546c67; // 0 } public static ChunkInfo ParseChunkInfo(Stream stream) { ChunkInfo chunkInfo = new ChunkInfo { StartPosition = stream.Position }; chunkInfo.Length = GetUInt32(stream); // 12 chunkInfo.Type = (ChunkFormat)GetUInt32(stream); // 16 return chunkInfo; } public static List FindChunks(Stream stream, long startPosition = 0) { stream.Position = startPosition + 4; // start after magic number bytes (4 bytes past) ParseGLBHeader(stream); List allChunks = new List(); // we only need to search for top two chunks (the JSON and binary chunks are guarenteed to be the top two chunks) // other chunks can be in the file but we do not care about them for (int i = 0; i < 2; ++i) { if (stream.Position == stream.Length) { break; } ChunkInfo chunkInfo = ParseChunkInfo(stream); allChunks.Add(chunkInfo); stream.Position += chunkInfo.Length; } return allChunks; } private static void ParseJsonChunk(Stream stream, long startPosition) { GLBHeader header = ParseGLBHeader(stream); // 4, 8 if (header.Version != 2) { throw new GLTFHeaderInvalidException("Unsupported glTF version"); }; if (header.FileLength > (stream.Length - startPosition)) { throw new GLTFHeaderInvalidException("File length does not match header."); } ChunkInfo chunkInfo = ParseChunkInfo(stream); if (chunkInfo.Type != ChunkFormat.JSON) { throw new GLTFHeaderInvalidException("First chunk must be of type JSON"); } } private static uint GetUInt32(Stream stream) { var uintSize = sizeof(uint); byte[] headerBuffer = new byte[uintSize]; stream.Read(headerBuffer, 0, uintSize); return BitConverter.ToUInt32(headerBuffer, 0); } } }