diff --git a/README.md b/README.md index 1a4c33c..bd476d5 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,11 @@ If you are looking for old, C++03 version, please use `devel-picojson` branch. * Moderate parsing time and memory consumption. * glTF specification v2.0.0 * [x] ASCII glTF + * [x] Load + * [x] Save * [x] Binary glTF(GLB) - * [x] PBR material description + * [x] Load + * [x] Save(.bin embedded .glb) * Buffers * [x] Parse BASE64 encoded embedded buffer data(DataURI). * [x] Load `.bin` file. @@ -55,6 +58,7 @@ If you are looking for old, C++03 version, please use `devel-picojson` branch. * [x] Image save * Extensions * [x] Draco mesh decoding + * [ ] Draco mesh encoding ## Note on extension property @@ -75,9 +79,10 @@ In extension(`ExtensionMap`), JSON number value is parsed as int or float(number * GLTF loader plugin for OGRE 2.1. Support for PBR materials via HLMS/PBS https://github.com/Ybalrid/Ogre_glTF * [TinyGltfImporter](http://doc.magnum.graphics/magnum/classMagnum_1_1Trade_1_1TinyGltfImporter.html) plugin for [Magnum](https://github.com/mosra/magnum), a lightweight and modular C++11/C++14 graphics middleware for games and data visualization. * [Diligent Engine](https://github.com/DiligentGraphics/DiligentEngine) - A modern cross-platform low-level graphics library and rendering framework -* Lighthouse 2: a rendering framework for real-time ray tracing / path tracing experiments. https://github.com/jbikker/lighthouse2 +* Lighthouse 2: a rendering framework for real-time ray tracing / path tracing experiments. https://github.com/jbikker/lighthouse2 * [QuickLook GLTF](https://github.com/toshiks/glTF-quicklook) - quicklook plugin for macos. Also SceneKit wrapper for tinygltf. * [GlslViewer](https://github.com/patriciogonzalezvivo/glslViewer) - live GLSL coding for MacOS and Linux +* [Vulkan-Samples](https://github.com/KhronosGroup/Vulkan-Samples) - The Vulkan Samples is collection of resources to help you develop optimized Vulkan applications. * Your projects here! (Please send PR) ## TODOs @@ -159,14 +164,17 @@ if (!ret) { ### Saving gltTF 2.0 model -* [ ] Buffers. + +* Buffers. * [x] To file * [x] Embedded * [ ] Draco compressed? * [x] Images * [x] To file * [x] Embedded -* [ ] Binary(.glb) +* Binary(.glb) + * [x] .bin embedded single .glb + * [ ] External .bin ## Running tests. @@ -194,6 +202,10 @@ $ ./tester $ ./tester_noexcept ``` +### Fuzzing tests + +See `tests/fuzzer` for details. + ## Third party licenses * json.hpp : Licensed under the MIT License . Copyright (c) 2013-2017 Niels Lohmann . diff --git a/models/CubeImageUriSpaces/ 2x2 image has multiple spaces.png b/models/CubeImageUriSpaces/ 2x2 image has multiple spaces.png new file mode 100644 index 0000000..67d8040 Binary files /dev/null and b/models/CubeImageUriSpaces/ 2x2 image has multiple spaces.png differ diff --git a/models/CubeImageUriSpaces/2x2 image has spaces.png b/models/CubeImageUriSpaces/2x2 image has spaces.png new file mode 100644 index 0000000..67d8040 Binary files /dev/null and b/models/CubeImageUriSpaces/2x2 image has spaces.png differ diff --git a/models/CubeImageUriSpaces/CubeImageUriMultipleSpaces.gltf b/models/CubeImageUriSpaces/CubeImageUriMultipleSpaces.gltf new file mode 100644 index 0000000..e10b904 --- /dev/null +++ b/models/CubeImageUriSpaces/CubeImageUriMultipleSpaces.gltf @@ -0,0 +1,171 @@ +{ + "asset": { + "version": "2.0" + }, + "scenes": [ + { + "nodes": [ + 0 + ] + } + ], + "scene": 0, + "nodes": [ + { + "mesh": 0 + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "NORMAL": 2, + "POSITION": 1, + "TEXCOORD_0": 3 + }, + "indices": 0, + "mode": 4, + "material": 0 + } + ] + } + ], + "materials": [ + { + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 0, + "texCoord": 0 + }, + "baseColorFactor": [ + 1, + 1, + 1, + 1 + ], + "metallicFactor": 1, + "roughnessFactor": 1 + }, + "emissiveFactor": [ + 0, + 0, + 0 + ], + "alphaMode": "OPAQUE" + } + ], + "textures": [ + { + "source": 0, + "sampler": 0 + } + ], + "samplers": [ + { + "wrapS": 33071, + "wrapT": 33071 + } + ], + "images": [ + { + "uri": " 2x2 image has multiple spaces.png" + } + ], + "accessors": [ + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 5121, + "count": 36, + "normalized": false, + "max": [ + 23 + ], + "min": [ + 0 + ], + "type": "SCALAR" + }, + { + "bufferView": 1, + "byteOffset": 0, + "componentType": 5126, + "count": 24, + "normalized": false, + "max": [ + 0.5, + 0.5, + 0.5 + ], + "min": [ + -0.5, + -0.5, + -0.5 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 0, + "componentType": 5126, + "count": 24, + "normalized": false, + "max": [ + 1, + 1, + 1 + ], + "min": [ + -1, + -1, + -1 + ], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 0, + "componentType": 5126, + "count": 24, + "normalized": false, + "max": [ + 1, + 1 + ], + "min": [ + 0, + 0 + ], + "type": "VEC2" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 36 + }, + { + "buffer": 0, + "byteOffset": 36, + "byteLength": 288 + }, + { + "buffer": 0, + "byteOffset": 324, + "byteLength": 288 + }, + { + "buffer": 0, + "byteOffset": 612, + "byteLength": 192 + } + ], + "buffers": [ + { + "byteLength": 804, + "uri": "CubeImageUriSpaces.bin" + } + ] +} diff --git a/models/CubeImageUriSpaces/CubeImageUriSpaces.bin b/models/CubeImageUriSpaces/CubeImageUriSpaces.bin new file mode 100644 index 0000000..2059867 Binary files /dev/null and b/models/CubeImageUriSpaces/CubeImageUriSpaces.bin differ diff --git a/models/CubeImageUriSpaces/CubeImageUriSpaces.gltf b/models/CubeImageUriSpaces/CubeImageUriSpaces.gltf new file mode 100644 index 0000000..82c166d --- /dev/null +++ b/models/CubeImageUriSpaces/CubeImageUriSpaces.gltf @@ -0,0 +1,171 @@ +{ + "asset": { + "version": "2.0" + }, + "scenes": [ + { + "nodes": [ + 0 + ] + } + ], + "scene": 0, + "nodes": [ + { + "mesh": 0 + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "NORMAL": 2, + "POSITION": 1, + "TEXCOORD_0": 3 + }, + "indices": 0, + "mode": 4, + "material": 0 + } + ] + } + ], + "materials": [ + { + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 0, + "texCoord": 0 + }, + "baseColorFactor": [ + 1, + 1, + 1, + 1 + ], + "metallicFactor": 1, + "roughnessFactor": 1 + }, + "emissiveFactor": [ + 0, + 0, + 0 + ], + "alphaMode": "OPAQUE" + } + ], + "textures": [ + { + "source": 0, + "sampler": 0 + } + ], + "samplers": [ + { + "wrapS": 33071, + "wrapT": 33071 + } + ], + "images": [ + { + "uri": "2x2 image has spaces.png" + } + ], + "accessors": [ + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 5121, + "count": 36, + "normalized": false, + "max": [ + 23 + ], + "min": [ + 0 + ], + "type": "SCALAR" + }, + { + "bufferView": 1, + "byteOffset": 0, + "componentType": 5126, + "count": 24, + "normalized": false, + "max": [ + 0.5, + 0.5, + 0.5 + ], + "min": [ + -0.5, + -0.5, + -0.5 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 0, + "componentType": 5126, + "count": 24, + "normalized": false, + "max": [ + 1, + 1, + 1 + ], + "min": [ + -1, + -1, + -1 + ], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 0, + "componentType": 5126, + "count": 24, + "normalized": false, + "max": [ + 1, + 1 + ], + "min": [ + 0, + 0 + ], + "type": "VEC2" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 36 + }, + { + "buffer": 0, + "byteOffset": 36, + "byteLength": 288 + }, + { + "buffer": 0, + "byteOffset": 324, + "byteLength": 288 + }, + { + "buffer": 0, + "byteOffset": 612, + "byteLength": 192 + } + ], + "buffers": [ + { + "byteLength": 804, + "uri": "CubeImageUriSpaces.bin" + } + ] +} diff --git a/tests/fuzzer/README.md b/tests/fuzzer/README.md new file mode 100644 index 0000000..b7c5223 --- /dev/null +++ b/tests/fuzzer/README.md @@ -0,0 +1,46 @@ +# Fuzzing test + +Do fuzzing test for TinyGLTF API. + +## Supported API + +* [x] LoadASCIIFromMemory +* [ ] LoadBinaryFromMemory + +## Requirements + +* meson +* clang with fuzzer support(`-fsanitize=fuzzer`. at least clang 8.0 should work) + +## Setup + +### Ubuntu 18.04 + +``` +$ sudo apt install clang++-8 +$ sudo apt install libfuzzer-8-dev +``` + +Optionally, if you didn't set `update-alternatives` you can set `clang++` to point to `clang++8` + +``` +$ sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-8 10 +$ sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-8 10 +``` + +## How to compile + +``` +$ CXX=clang++ CC=clang meson build +$ cd build +$ ninja +``` + +## How to run + +Increase memory limit. e.g. `-rss_limit_mb=50000` + +``` +$ ./fuzz_gltf -rss_limit_mb=20000 -jobs 4 +``` + diff --git a/tests/fuzzer/fuzz_gltf.cc b/tests/fuzzer/fuzz_gltf.cc new file mode 100644 index 0000000..73c87b1 --- /dev/null +++ b/tests/fuzzer/fuzz_gltf.cc @@ -0,0 +1,33 @@ +#include +#include +#include +#include +#include + +#define STB_IMAGE_IMPLEMENTATION +#define STB_IMAGE_WRITE_IMPLEMENTATION +#define TINYGLTF_IMPLEMENTATION +#include "tiny_gltf.h" + +static void parse_intCoding4(const uint8_t *data, size_t size) +{ + + tinygltf::Model model; + tinygltf::TinyGLTF ctx; + std::string err; + std::string warn; + + const char *str = reinterpret_cast(data); + + bool ret = ctx.LoadASCIIFromString(&model, &err, &warn, str, size, /* base_dir */"" ); + (void)ret; + +} + +extern "C" +int LLVMFuzzerTestOneInput(std::uint8_t const* data, std::size_t size) +{ + parse_intCoding4(data, size); + return 0; +} + diff --git a/tests/fuzzer/meson.build b/tests/fuzzer/meson.build new file mode 100644 index 0000000..c69ed13 --- /dev/null +++ b/tests/fuzzer/meson.build @@ -0,0 +1,9 @@ +project('fuzz_tinygltf', 'cpp', default_options : ['cpp_std=c++11']) + +incdirs = include_directories('../../') +executable('fuzz_gltf', + 'fuzz_gltf.cc', + include_directories : incdirs, + cpp_args : '-fsanitize=address,fuzzer', + link_args : '-fsanitize=address,fuzzer' ) + diff --git a/tests/tester.cc b/tests/tester.cc index 58ec000..c6d31b0 100644 --- a/tests/tester.cc +++ b/tests/tester.cc @@ -333,3 +333,29 @@ TEST_CASE("pbr-khr-texture-transform", "[material]") { REQUIRE(scale[1] == Approx(-1.0)); } + +TEST_CASE("image-uri-spaces", "[issue-236]") { + + tinygltf::Model model; + tinygltf::TinyGLTF ctx; + std::string err; + std::string warn; + + // Test image file with single spaces. + bool ret = ctx.LoadASCIIFromFile(&model, &err, &warn, "../models/CubeImageUriSpaces/CubeImageUriSpaces.gltf"); + if (!err.empty()) { + std::cerr << err << std::endl; + } + + REQUIRE(true == ret); + + // Test image file with a beginning space, trailing space, and greater than + // one consecutive spaces. + ret = ctx.LoadASCIIFromFile(&model, &err, &warn, "../models/CubeImageUriSpaces/CubeImageUriMultipleSpaces.gltf"); + if (!err.empty()) { + std::cerr << err << std::endl; + } + + REQUIRE(true == ret); +} + diff --git a/tiny_gltf.h b/tiny_gltf.h index 4f26438..5b0aca5 100644 --- a/tiny_gltf.h +++ b/tiny_gltf.h @@ -799,12 +799,12 @@ struct Material { struct BufferView { std::string name; - int buffer; // Required - size_t byteOffset; // minimum 0, default 0 - size_t byteLength; // required, minimum 1 - size_t byteStride; // minimum 4, maximum 252 (multiple of 4), default 0 = + int buffer{-1}; // Required + size_t byteOffset{0}; // minimum 0, default 0 + size_t byteLength{0}; // required, minimum 1. 0 = invalid + size_t byteStride{0}; // minimum 4, maximum 252 (multiple of 4), default 0 = // understood to be tightly packed - int target; // ["ARRAY_BUFFER", "ELEMENT_ARRAY_BUFFER"] + int target{0}; // ["ARRAY_BUFFER", "ELEMENT_ARRAY_BUFFER"] for vertex indices or atttribs. Could be 0 for other data Value extras; ExtensionMap extensions; @@ -812,9 +812,9 @@ struct BufferView { std::string extras_json_string; std::string extensions_json_string; - bool dracoDecoded; // Flag indicating this has been draco decoded + bool dracoDecoded{false}; // Flag indicating this has been draco decoded - BufferView() : byteOffset(0), byteStride(0), dracoDecoded(false) {} + BufferView() : buffer(-1), byteOffset(0), byteLength(0), byteStride(0), target(0), dracoDecoded(false) {} DEFAULT_METHODS(BufferView) bool operator==(const BufferView &) const; }; @@ -889,8 +889,13 @@ struct Accessor { // unreachable return 0; } - Accessor() { - bufferView = -1; + Accessor() : + bufferView(-1), + byteOffset(0), + normalized(false), + componentType(-1), + count(0), + type(-1){ sparse.isSparse = false; } DEFAULT_METHODS(Accessor) @@ -1138,7 +1143,7 @@ class Model { std::vector scenes; std::vector lights; - int defaultScene; + int defaultScene = -1; std::vector extensionsUsed; std::vector extensionsRequired; @@ -1417,6 +1422,7 @@ class TinyGLTF { #include //#include #ifndef TINYGLTF_NO_FS +#include #include #endif #include @@ -1543,6 +1549,13 @@ class TinyGLTF { #undef NOMINMAX #endif +#if defined(__GLIBCXX__) // mingw + +#include // fstream (all sorts of IO stuff) + stdio_filebuf (=streambuf) +#include // _O_RDONLY + +#endif + #elif !defined(__ANDROID__) #include #endif @@ -2394,11 +2407,20 @@ bool FileExists(const std::string &abs_filename, void *) { } #else #ifdef _WIN32 - FILE *fp; +#if defined(_MSC_VER) || defined(__GLIBCXX__) + FILE *fp = nullptr; errno_t err = _wfopen_s(&fp, UTF8ToWchar(abs_filename).c_str(), L"rb"); if (err != 0) { return false; } +#else + FILE *fp = nullptr; + errno_t err = fopen_s(&fp, abs_filename.c_str(), "rb"); + if (err != 0) { + return false; + } +#endif + #else FILE *fp = fopen(abs_filename.c_str(), "rb"); #endif @@ -2438,8 +2460,10 @@ std::string ExpandFilePath(const std::string &filepath, void *) { return ""; } + // Quote the string to keep any spaces in filepath intact. + std::string quoted_path = "\"" + filepath + "\""; // char** w; - int ret = wordexp(filepath.c_str(), &p, 0); + int ret = wordexp(quoted_path.c_str(), &p, 0); if (ret) { // err s = filepath; @@ -2491,7 +2515,15 @@ bool ReadWholeFile(std::vector *out, std::string *err, } #else #ifdef _WIN32 +#if defined(__GLIBCXX__) // mingw + int file_descriptor = _wopen(UTF8ToWchar(filepath).c_str(), _O_RDONLY | _O_BINARY); + __gnu_cxx::stdio_filebuf wfile_buf(file_descriptor, std::ios_base::in); + std::istream f(&wfile_buf); +#elif defined(_MSC_VER) std::ifstream f(UTF8ToWchar(filepath).c_str(), std::ifstream::binary); +#else // clang? + std::ifstream f(filepath.c_str(), std::ifstream::binary); +#endif #else std::ifstream f(filepath.c_str(), std::ifstream::binary); #endif @@ -2522,7 +2554,6 @@ bool ReadWholeFile(std::vector *out, std::string *err, out->resize(sz); f.read(reinterpret_cast(&out->at(0)), static_cast(sz)); - f.close(); return true; #endif @@ -2531,7 +2562,15 @@ bool ReadWholeFile(std::vector *out, std::string *err, bool WriteWholeFile(std::string *err, const std::string &filepath, const std::vector &contents, void *) { #ifdef _WIN32 +#if defined(__GLIBCXX__) // mingw + int file_descriptor = _wopen(UTF8ToWchar(filepath).c_str(), _O_WRONLY | _O_BINARY); + __gnu_cxx::stdio_filebuf wfile_buf(file_descriptor, std::ios_base::in); + std::ostream f(&wfile_buf); +#elif defined(_MSC_VER) std::ofstream f(UTF8ToWchar(filepath).c_str(), std::ofstream::binary); +#else // clang? + std::ofstream f(filepath.c_str(), std::ofstream::binary); +#endif #else std::ofstream f(filepath.c_str(), std::ofstream::binary); #endif @@ -2551,7 +2590,6 @@ bool WriteWholeFile(std::string *err, const std::string &filepath, return false; } - f.close(); return true; } @@ -2694,6 +2732,7 @@ bool DecodeDataURI(std::vector *out, std::string &mime_type, } } + // TODO(syoyo): Allow empty buffer? #229 if (data.empty()) { return false; } @@ -5225,7 +5264,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, std::string *warn, #if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || \ defined(_CPPUNWIND)) && \ - not defined(TINYGLTF_NOEXCEPTION) + !defined(TINYGLTF_NOEXCEPTION) try { JsonParse(v, json_str, json_str_length, true); @@ -5498,7 +5537,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, std::string *warn, // Assign missing bufferView target types // - Look for missing Mesh indices - // - Look for missing bufferView targets + // - Look for missing Mesh attributes for (auto &mesh : model->meshes) { for (auto &primitive : mesh.primitives) { if (primitive.indices > @@ -5526,14 +5565,11 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, std::string *warn, // we could optionally check if acessors' bufferView type is Scalar, as // it should be } - } - } - // find any missing targets, must be an array buffer type if not fulfilled - // from previous check - for (auto &bufferView : model->bufferViews) { - if (bufferView.target == 0) // missing target type - { - bufferView.target = TINYGLTF_TARGET_ARRAY_BUFFER; + + for (auto &attribute : primitive.attributes) { + model->bufferViews[size_t(model->accessors[size_t(attribute.second)].bufferView)] + .target = TINYGLTF_TARGET_ARRAY_BUFFER; + } } } @@ -6251,22 +6287,44 @@ static void SerializeValue(const std::string &key, const Value &value, static void SerializeGltfBufferData(const std::vector &data, json &o) { std::string header = "data:application/octet-stream;base64,"; - std::string encodedData = - base64_encode(&data[0], static_cast(data.size())); - SerializeStringProperty("uri", header + encodedData, o); + if (data.size() > 0) { + std::string encodedData = + base64_encode(&data[0], static_cast(data.size())); + SerializeStringProperty("uri", header + encodedData, o); + } else { + // Issue #229 + // size 0 is allowd. Just emit mime header. + SerializeStringProperty("uri", header, o); + } } static bool SerializeGltfBufferData(const std::vector &data, const std::string &binFilename) { #ifdef _WIN32 +#if defined(__GLIBCXX__) // mingw + int file_descriptor = _wopen(UTF8ToWchar(binFilename).c_str(), _O_WRONLY | _O_BINARY); + __gnu_cxx::stdio_filebuf wfile_buf(file_descriptor, std::ios_base::in); + std::ostream output(&wfile_buf); + if (!wfile_buf.is_open()) return false; +#elif defined(_MSC_VER) std::ofstream output(UTF8ToWchar(binFilename).c_str(), std::ofstream::binary); + if (!output.is_open()) return false; #else std::ofstream output(binFilename.c_str(), std::ofstream::binary); -#endif if (!output.is_open()) return false; - output.write(reinterpret_cast(&data[0]), - std::streamsize(data.size())); - output.close(); +#endif +#else + std::ofstream output(binFilename.c_str(), std::ofstream::binary); + if (!output.is_open()) return false; +#endif + if (data.size() > 0) { + output.write(reinterpret_cast(&data[0]), + std::streamsize(data.size())); + } else { + // Issue #229 + // size 0 will be still valid buffer data. + // write empty file. + } return true; } @@ -6337,7 +6395,8 @@ static void SerializeGltfAccessor(Accessor &accessor, json &o) { SerializeNumberProperty("count", accessor.count, o); SerializeNumberArrayProperty("min", accessor.minValues, o); SerializeNumberArrayProperty("max", accessor.maxValues, o); - SerializeValue("normalized", Value(accessor.normalized), o); + if (accessor.normalized) + SerializeValue("normalized", Value(accessor.normalized), o); std::string type; switch (accessor.type) { case TINYGLTF_TYPE_SCALAR: @@ -6419,6 +6478,7 @@ static void SerializeGltfAnimation(Animation &animation, json &o) { { json samplers; + JsonReserveArray(samplers, animation.samplers.size()); for (unsigned int i = 0; i < animation.samplers.size(); ++i) { json sampler; AnimationSampler gltfSampler = animation.samplers[i]; @@ -6455,6 +6515,18 @@ static void SerializeGltfAsset(Asset &asset, json &o) { SerializeExtensionMap(asset.extensions, o); } + static void SerializeGltfBufferBin(Buffer &buffer, json &o, + std::vector &binBuffer) { + SerializeNumberProperty("byteLength", buffer.data.size(), o); + binBuffer=buffer.data; + + if (buffer.name.size()) SerializeStringProperty("name", buffer.name, o); + + if (buffer.extras.Type() != NULL_TYPE) { + SerializeValue("extras", buffer.extras, o); + } +} + static void SerializeGltfBuffer(Buffer &buffer, json &o) { SerializeNumberProperty("byteLength", buffer.data.size(), o); SerializeGltfBufferData(buffer.data, o); @@ -6734,7 +6806,7 @@ static void SerializeGltfMesh(Mesh &mesh, json &o) { JsonAddMember(primitive, "targets", std::move(targets)); } - SerializeExtensionMap(gltfPrimitive.extensions, o); + SerializeExtensionMap(gltfPrimitive.extensions, primitive); if (gltfPrimitive.extras.Type() != NULL_TYPE) { SerializeValue("extras", gltfPrimitive.extras, primitive); @@ -7124,31 +7196,57 @@ static bool WriteGltfStream(std::ostream &stream, const std::string &content) { static bool WriteGltfFile(const std::string &output, const std::string &content) { #ifdef _WIN32 +#if defined(_MSC_VER) std::ofstream gltfFile(UTF8ToWchar(output).c_str()); +#elif defined(__GLIBCXX__) + int file_descriptor = _wopen(UTF8ToWchar(output).c_str(), _O_WRONLY | _O_BINARY); + __gnu_cxx::stdio_filebuf wfile_buf(file_descriptor, std::ios_base::in); + std::ostream gltfFile(&wfile_buf); + if (!wfile_buf.is_open()) return false; #else std::ofstream gltfFile(output.c_str()); -#endif if (!gltfFile.is_open()) return false; +#endif +#else + std::ofstream gltfFile(output.c_str()); + if (!gltfFile.is_open()) return false; +#endif return WriteGltfStream(gltfFile, content); } static void WriteBinaryGltfStream(std::ostream &stream, - const std::string &content) { + const std::string &content, + const std::vector &binBuffer) { const std::string header = "glTF"; const int version = 2; - const int padding_size = content.size() % 4; - // 12 bytes for header, JSON content length, 8 bytes for JSON chunk info, - // padding - const int length = 12 + 8 + int(content.size()) + padding_size; + // https://stackoverflow.com/questions/3407012/c-rounding-up-to-the-nearest-multiple-of-a-number + auto roundUp = [](uint32_t numToRound, uint32_t multiple) + { + if (multiple == 0) + return numToRound; + + uint32_t remainder = numToRound % multiple; + if (remainder == 0) + return numToRound; + + return numToRound + multiple - remainder; + }; + + const uint32_t padding_size = roundUp(uint32_t(content.size()), 4) - uint32_t(content.size()); + + // 12 bytes for header, JSON content length, 8 bytes for JSON chunk info. + // Chunk data must be located at 4-byte boundary. + const uint32_t length = 12 + 8 + roundUp(uint32_t(content.size()), 4)+ + (binBuffer.size()?(8+roundUp(uint32_t(binBuffer.size()),4)) : 0); stream.write(header.c_str(), std::streamsize(header.size())); stream.write(reinterpret_cast(&version), sizeof(version)); stream.write(reinterpret_cast(&length), sizeof(length)); // JSON chunk info, then JSON data - const int model_length = int(content.size()) + padding_size; - const int model_format = 0x4E4F534A; + const uint32_t model_length = uint32_t(content.size()) + padding_size; + const uint32_t model_format = 0x4E4F534A; stream.write(reinterpret_cast(&model_length), sizeof(model_length)); stream.write(reinterpret_cast(&model_format), @@ -7160,16 +7258,41 @@ static void WriteBinaryGltfStream(std::ostream &stream, const std::string padding = std::string(size_t(padding_size), ' '); stream.write(padding.c_str(), std::streamsize(padding.size())); } + if (binBuffer.size() > 0){ + const uint32_t bin_padding_size = roundUp(uint32_t(binBuffer.size()), 4) - uint32_t(binBuffer.size()); + // BIN chunk info, then BIN data + const uint32_t bin_length = uint32_t(binBuffer.size()) + bin_padding_size; + const uint32_t bin_format = 0x004e4942; + stream.write(reinterpret_cast(&bin_length), + sizeof(bin_length)); + stream.write(reinterpret_cast(&bin_format), + sizeof(bin_format)); + stream.write(reinterpret_cast(binBuffer.data()), std::streamsize(binBuffer.size())); + // Chunksize must be multiplies of 4, so pad with zeroes + if (bin_padding_size > 0) { + const std::vector padding = std::vector(size_t(bin_padding_size), 0); + stream.write(reinterpret_cast(padding.data()), std::streamsize(padding.size())); + } + } } static void WriteBinaryGltfFile(const std::string &output, - const std::string &content) { + const std::string &content, + const std::vector &binBuffer) { #ifdef _WIN32 +#if defined(_MSC_VER) std::ofstream gltfFile(UTF8ToWchar(output).c_str(), std::ios::binary); +#elif defined(__GLIBCXX__) + int file_descriptor = _wopen(UTF8ToWchar(output).c_str(), _O_WRONLY | _O_BINARY); + __gnu_cxx::stdio_filebuf wfile_buf(file_descriptor, std::ios_base::in); + std::ostream gltfFile(&wfile_buf); #else std::ofstream gltfFile(output.c_str(), std::ios::binary); #endif - WriteBinaryGltfStream(gltfFile, content); +#else + std::ofstream gltfFile(output.c_str(), std::ios::binary); +#endif + WriteBinaryGltfStream(gltfFile, content,binBuffer); } bool TinyGLTF::WriteGltfSceneToStream(Model *model, std::ostream &stream, @@ -7182,11 +7305,16 @@ bool TinyGLTF::WriteGltfSceneToStream(Model *model, std::ostream &stream, // BUFFERS std::vector usedUris; + std::vector binBuffer; json buffers; JsonReserveArray(buffers, model->buffers.size()); for (unsigned int i = 0; i < model->buffers.size(); ++i) { json buffer; - SerializeGltfBuffer(model->buffers[i], buffer); + if (writeBinary && i==0 && model->buffers[i].uri.empty()){ + SerializeGltfBufferBin(model->buffers[i], buffer,binBuffer); + } else { + SerializeGltfBuffer(model->buffers[i], buffer); + } JsonPushBack(buffers, std::move(buffer)); } JsonAddMember(output, "buffers", std::move(buffers)); @@ -7211,7 +7339,7 @@ bool TinyGLTF::WriteGltfSceneToStream(Model *model, std::ostream &stream, } if (writeBinary) { - WriteBinaryGltfStream(stream, JsonToString(output)); + WriteBinaryGltfStream(stream, JsonToString(output),binBuffer); } else { WriteGltfStream(stream, JsonToString(output, prettyPrint ? 2 : -1)); } @@ -7242,11 +7370,14 @@ bool TinyGLTF::WriteGltfSceneToFile(Model *model, const std::string &filename, // BUFFERS std::vector usedUris; + std::vector binBuffer; json buffers; JsonReserveArray(buffers, model->buffers.size()); for (unsigned int i = 0; i < model->buffers.size(); ++i) { json buffer; - if (embedBuffers) { + if (writeBinary && i==0 && model->buffers[i].uri.empty()){ + SerializeGltfBufferBin(model->buffers[i], buffer,binBuffer); + } else if (embedBuffers) { SerializeGltfBuffer(model->buffers[i], buffer); } else { std::string binSavePath; @@ -7295,7 +7426,7 @@ bool TinyGLTF::WriteGltfSceneToFile(Model *model, const std::string &filename, } if (writeBinary) { - WriteBinaryGltfFile(filename, JsonToString(output)); + WriteBinaryGltfFile(filename, JsonToString(output),binBuffer); } else { WriteGltfFile(filename, JsonToString(output, (prettyPrint ? 2 : -1))); }