Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved binary read and write support #7

Merged
merged 9 commits into from
Sep 12, 2017
59 changes: 59 additions & 0 deletions glTFLoader/Interface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ public static class Interface
const uint JSON = 0x4E4F534A;
const uint BIN = 0x004E4942;

const string EMBEDDEDOCTETSTREAM = "data:application/octet-stream;base64";
Copy link
Contributor

@bghgary bghgary Sep 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend adding the trailing comma to this constant so that you are actually checking for the comma instead of ignoring the character using + 1.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okey, I'll do that change.

Btw, detecting embedded images is quite tricky and I'm aware there's room for improvement. For example, I'm sure some people will want to use hardware specific images, like DDS or ASTC , I don't know if there's specific mime types for these formats... in which case, is it possible to find images stored as data:application/octet-stream;base64 ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There have been lots of discussions around this already, but the short answer, at least for core glTF 2.0, is that hardware specific textures are not supported. Thus, you don't have to worry about them for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okey then.

About the Interface API, there's a few more methods I would like to add, but I don't know if you think it's too much already...

Usually, when I work with file formats, I try to make everything to work with Streams, certainly, it gets complicated when you have multiple files around. The use case is when you try to load a file from within a ZIP or any other package container (which is quite common to use in videogames)

The idea I had in mind was to replicate the previous methods used to load buffers and images, but using lambdas, so the end user could intercept/inject file accesses to whatever package container is using.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do that in a separate change.

const string EMBEDDEDPNG = "data:image/png;base64";
const string EMBEDDEDBMP = "data:image/bmp;base64";
const string EMBEDDEDGIF = "data:image/gif;base64";
const string EMBEDDEDJPEG = "data:image/jpeg;base64";
const string EMBEDDEDTIFF = "data:image/tiff;base64";

public static Gltf LoadModel(string filePath)
{
var path = Path.GetFullPath(filePath);
Expand Down Expand Up @@ -129,6 +136,23 @@ public static Byte[] LoadBinaryBuffer(Stream stream)
}
}

public static Byte[] LoadBinaryBuffer(this Gltf model, string gltfFilePath, int bufferIndex)
{
var buffer = model.Buffers[bufferIndex];

if (buffer.Uri == null) return LoadBinaryBuffer(gltfFilePath);

if (buffer.Uri.StartsWith("data:application/octet-stream;base64"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to replace this with EMBEDDEDOCTETSTREAM?

{
var content = buffer.Uri.Substring(EMBEDDEDOCTETSTREAM.Length + 1);
return Convert.FromBase64String(content);
}

var bufferFilePath = Path.Combine(Path.GetDirectoryName(gltfFilePath), buffer.Uri);

return File.ReadAllBytes(bufferFilePath);
}

private static void ReadBinaryHeader(BinaryReader binaryReader)
{
uint magic = binaryReader.ReadUInt32();
Expand All @@ -151,6 +175,39 @@ private static void ReadBinaryHeader(BinaryReader binaryReader)
}
}

public static Stream OpenImageFile(this Gltf model, string gltfFilePath, int imageIndex)
{
var image = model.Images[imageIndex];

if (image.BufferView.HasValue)
{
var bufferView = model.BufferViews[image.BufferView.Value];

var bufferBytes = model.LoadBinaryBuffer(gltfFilePath, bufferView.Buffer);

return new MemoryStream(bufferBytes, bufferView.ByteOffset, bufferView.ByteLength);
}

if (image.Uri.StartsWith("data:image/"))
{
string content = null;

if (image.Uri.StartsWith(EMBEDDEDPNG)) content = image.Uri.Substring(EMBEDDEDPNG.Length + 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

glTF only support PNG and JPG. None of the other formats are supported, so you don't need the other mime types.

https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#file-extensions-and-mime-types

if (image.Uri.StartsWith(EMBEDDEDBMP)) content = image.Uri.Substring(EMBEDDEDBMP.Length + 1);
if (image.Uri.StartsWith(EMBEDDEDGIF)) content = image.Uri.Substring(EMBEDDEDGIF.Length + 1);
if (image.Uri.StartsWith(EMBEDDEDJPEG)) content = image.Uri.Substring(EMBEDDEDJPEG.Length + 1);
if (image.Uri.StartsWith(EMBEDDEDTIFF)) content = image.Uri.Substring(EMBEDDEDTIFF.Length + 1);

var bytes = Convert.FromBase64String(content);
return new MemoryStream(bytes);
}
else
{
var imageFilePath = Path.Combine(Path.GetDirectoryName(gltfFilePath), image.Uri);
return File.OpenRead(imageFilePath);
}
}

public static Gltf DeserializeModel(string fileData)
{
return JsonConvert.DeserializeObject<Gltf>(fileData);
Expand Down Expand Up @@ -223,6 +280,8 @@ public static void SaveBinaryModel(this Gltf model, byte[] buffer, BinaryWriter
for (int i = 0; i < binPadding; ++i) binaryWriter.Write((Byte)0);
}



}


Expand Down
187 changes: 80 additions & 107 deletions glTFLoaderUnitTests/SampleModelsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,53 @@ public void Init()
AbsolutePathToSchemaDir = Path.Combine(TestContext.CurrentContext.TestDirectory, RelativePathToSchemaDir);
}

private glTFLoader.Schema.Gltf TestLoadFile(string filePath)
{
if (!filePath.EndsWith("gltf") && !filePath.EndsWith("glb")) return null;

try
{
var deserializedFile = Interface.LoadModel(filePath);
Assert.IsNotNull(deserializedFile);

// read all buffers
for(int i=0; i < deserializedFile.Buffers?.Length; ++i)
{
var bufferBytes = deserializedFile.LoadBinaryBuffer(filePath, i);
Assert.IsNotNull(bufferBytes);
Assert.IsTrue(deserializedFile.Buffers[i].ByteLength <= bufferBytes.Length);
}

// open all images
for(int i=0; i < deserializedFile.Images?.Length; ++i)
{
using (var s = deserializedFile.OpenImageFile(filePath, i))
{
Assert.IsNotNull(s);

var imageHeader = new Byte[16];
s.Read(imageHeader, 0, 16);

// TODO: here we could check actual image headers against the expected mime type.
}
}

return deserializedFile;
}
catch (Exception e)
{
throw new Exception(filePath, e);
}
}

[Test]
public void SchemaLoad()
{
foreach (var dir in Directory.EnumerateDirectories(Path.GetFullPath(AbsolutePathToSchemaDir)))
{
foreach (var file in Directory.EnumerateFiles(Path.Combine(dir, "glTF")))
{
if (file.EndsWith("gltf"))
{
try
{
var deserializedFile = Interface.LoadModel(file);
Assert.IsNotNull(deserializedFile);
}
catch (Exception e)
{
throw new Exception(file, e);
}
}
TestLoadFile(file);
}
}
}
Expand Down Expand Up @@ -83,8 +111,40 @@ public void BinarySchemaLoad()
{
try
{
var deserializedFile = Interface.LoadModel(file);
var len = new FileInfo(file).Length;
Assert.IsTrue((len & 3) == 0);

var deserializedFile = TestLoadFile(file);
Assert.IsNotNull(deserializedFile);

var jsonChunk = Interface.LoadModel(file);
Assert.IsNotNull(jsonChunk);

var binChunk = Interface.LoadBinaryBuffer(file);

Assert.IsNotNull(binChunk);
Assert.IsTrue((binChunk.Length & 3) == 0);

Assert.IsTrue(jsonChunk.Buffers[0].ByteLength <= binChunk.Length);
// should we check padding as with jsonChunk? some reference files fail!

// write to memory and reload again
using (var wm = new MemoryStream())
{
jsonChunk.SaveBinaryModel(binChunk, wm);

using (var rm = new MemoryStream(wm.ToArray()))
{
Interface.LoadModel(rm);
}

using (var rm = new MemoryStream(wm.ToArray()))
{
Interface.LoadBinaryBuffer(rm);
}
}


}
catch (Exception e)
{
Expand All @@ -99,25 +159,16 @@ public void BinarySchemaLoad()
[Test]
public void EmbeddedSchemaLoad()
{
TestLoadFile(@"D:\(_GitHub_)\glTF-Sample-Models\2.0\BoxTextured\glTF-Embedded\BoxTextured.gltf");

foreach (var dir in Directory.EnumerateDirectories(Path.GetFullPath(AbsolutePathToSchemaDir)))
{
string path = Path.Combine(dir, "glTF-Embedded");
if (Directory.Exists(path))
{
foreach (var file in Directory.EnumerateFiles(path))
{
if (file.EndsWith("gltf"))
{
try
{
var deserializedFile = Interface.LoadModel(file);
Assert.IsNotNull(deserializedFile);
}
catch (Exception e)
{
throw new Exception(file, e);
}
}
TestLoadFile(file);
}
}
}
Expand All @@ -133,18 +184,7 @@ public void MaterialsSchemaLoad()
{
foreach (var file in Directory.EnumerateFiles(path))
{
if (file.EndsWith("gltf"))
{
try
{
var deserializedFile = Interface.LoadModel(file);
Assert.IsNotNull(deserializedFile);
}
catch (Exception e)
{
throw new Exception(file, e);
}
}
TestLoadFile(file);
}
}
}
Expand All @@ -160,18 +200,7 @@ public void PBRSchemaLoad()
{
foreach (var file in Directory.EnumerateFiles(path))
{
if (file.EndsWith("gltf"))
{
try
{
var deserializedFile = Interface.LoadModel(file);
Assert.IsNotNull(deserializedFile);
}
catch (Exception e)
{
throw new Exception(file, e);
}
}
TestLoadFile(file);
}
}
}
Expand All @@ -187,66 +216,10 @@ public void WebGLSchemaLoad()
{
foreach (var file in Directory.EnumerateFiles(path))
{
if (file.EndsWith("gltf"))
{
try
{
var deserializedFile = Interface.LoadModel(file);
Assert.IsNotNull(deserializedFile);
}
catch (Exception e)
{
throw new Exception(file, e);
}
}
TestLoadFile(file);
}
}
}
}

[Test]
public void BinaryLoad()
{
foreach (var file in Directory.EnumerateFiles(AbsolutePathToSchemaDir, "*.glb", SearchOption.AllDirectories))
{
try
{
var len = new FileInfo(file).Length;

Assert.IsTrue((len & 3) == 0);

var jsonChunk = Interface.LoadModel(file);
Assert.IsNotNull(jsonChunk);

var binChunk = Interface.LoadBinaryBuffer(file);
Assert.IsNotNull(binChunk);
Assert.IsTrue((binChunk.Length & 3) == 0);

var buffer = jsonChunk.Buffers[0];
Assert.IsTrue(buffer.ByteLength <= binChunk.Length);
// should we check padding as with jsonChunk? some reference files fail!

// write to memory and reload again
using (var wm = new MemoryStream())
{
jsonChunk.SaveBinaryModel(binChunk, wm);

using (var rm = new MemoryStream(wm.ToArray()))
{
Interface.LoadModel(rm);
}

using (var rm = new MemoryStream(wm.ToArray()))
{
Interface.LoadBinaryBuffer(rm);
}
}
}
catch (Exception e)
{
throw new Exception(file, e);
}
}
}
}
}
}