diff --git a/base/VulkanTexture.hpp b/base/VulkanTexture.hpp index 73bcbd8..b50c042 100644 --- a/base/VulkanTexture.hpp +++ b/base/VulkanTexture.hpp @@ -19,8 +19,6 @@ #include -#include "tiny_gltf.h" - #if defined(__ANDROID__) #include #endif diff --git a/base/VulkanglTFModel.hpp b/base/VulkanglTFModel.hpp index e38bd55..adc77e9 100644 --- a/base/VulkanglTFModel.hpp +++ b/base/VulkanglTFModel.hpp @@ -23,6 +23,10 @@ #include #include +#define TINYGLTF_IMPLEMENTATION +#define TINYGLTF_NO_STB_IMAGE_WRITE +#define STB_IMAGE_IMPLEMENTATION +#define STBI_MSC_SECURE_CRT #include "tiny_gltf.h" #if defined(__ANDROID__) @@ -522,12 +526,12 @@ namespace vkglTF }; struct Vertices { - VkBuffer buffer; + VkBuffer buffer = VK_NULL_HANDLE; VkDeviceMemory memory; } vertices; struct Indices { int count; - VkBuffer buffer; + VkBuffer buffer = VK_NULL_HANDLE; VkDeviceMemory memory; } indices; @@ -551,16 +555,27 @@ namespace vkglTF void destroy(VkDevice device) { - vkDestroyBuffer(device, vertices.buffer, nullptr); - vkFreeMemory(device, vertices.memory, nullptr); - vkDestroyBuffer(device, indices.buffer, nullptr); - vkFreeMemory(device, indices.memory, nullptr); + if (vertices.buffer != VK_NULL_HANDLE) { + vkDestroyBuffer(device, vertices.buffer, nullptr); + vkFreeMemory(device, vertices.memory, nullptr); + } + if (indices.buffer != VK_NULL_HANDLE) { + vkDestroyBuffer(device, indices.buffer, nullptr); + vkFreeMemory(device, indices.memory, nullptr); + } for (auto texture : textures) { texture.destroy(); } + textures.resize(0); for (auto node : nodes) { delete node; } + materials.resize(0); + animations.resize(0); + nodes.resize(0); + linearNodes.resize(0); + extensions.resize(0); + skins.resize(0); }; void loadNode(vkglTF::Node *parent, const tinygltf::Node &node, uint32_t nodeIndex, const tinygltf::Model &model, std::vector& indexBuffer, std::vector& vertexBuffer, float globalscale) @@ -809,20 +824,31 @@ namespace vkglTF } // Extensions - if (mat.extPBRValues.size() > 0) { - // KHR_materials_pbrSpecularGlossiness - if (mat.extPBRValues.find("specularGlossinessTexture") != mat.extPBRValues.end()) { - material.extension.specularGlossinessTexture = &textures[gltfModel.textures[mat.extPBRValues["specularGlossinessTexture"].TextureIndex()].source]; + // @TODO: Find out if there is a nicer way of reading these properties with recent tinygltf headers + if (mat.extensions.find("KHR_materials_pbrSpecularGlossiness") != mat.extensions.end()) { + auto ext = mat.extensions.find("KHR_materials_pbrSpecularGlossiness"); + if (ext->second.Has("specularGlossinessTexture")) { + auto index = ext->second.Get("specularGlossinessTexture").Get("index"); + material.extension.specularGlossinessTexture = &textures[gltfModel.textures[index.Get()].source]; material.pbrWorkflows.specularGlossiness = true; } - if (mat.extPBRValues.find("diffuseTexture") != mat.extPBRValues.end()) { - material.extension.diffuseTexture = &textures[gltfModel.textures[mat.extPBRValues["diffuseTexture"].TextureIndex()].source]; + if (ext->second.Has("diffuseTexture")) { + auto index = ext->second.Get("diffuseTexture").Get("index"); + material.extension.diffuseTexture = &textures[gltfModel.textures[index.Get()].source]; } - if (mat.extPBRValues.find("diffuseFactor") != mat.extPBRValues.end()) { - material.extension.diffuseFactor = glm::make_vec4(mat.extPBRValues["diffuseFactor"].ColorFactor().data()); + if (ext->second.Has("diffuseFactor")) { + auto factor = ext->second.Get("diffuseFactor"); + for (uint32_t i = 0; i < factor.ArrayLen(); i++) { + auto val = factor.Get(i); + material.extension.diffuseFactor[i] = val.IsNumber() ? (float)val.Get() : (float)val.Get(); + } } - if (mat.extPBRValues.find("specularFactor") != mat.extPBRValues.end()) { - material.extension.specularFactor = glm::vec4(glm::make_vec3(mat.extPBRValues["specularFactor"].ColorFactor().data()), 1.0); + if (ext->second.Has("specularFactor")) { + auto factor = ext->second.Get("specularFactor"); + for (uint32_t i = 0; i < factor.ArrayLen(); i++) { + auto val = factor.Get(i); + material.extension.specularFactor[i] = val.IsNumber() ? (float)val.Get() : (float)val.Get(); + } } } @@ -947,6 +973,7 @@ namespace vkglTF tinygltf::Model gltfModel; tinygltf::TinyGLTF gltfContext; std::string error; + std::string warning; this->device = device; @@ -959,10 +986,10 @@ namespace vkglTF AAsset_read(asset, fileData, size); AAsset_close(asset); std::string baseDir; - bool fileLoaded = gltfContext.LoadASCIIFromString(&gltfModel, &error, fileData, size, baseDir); + bool fileLoaded = gltfContext.LoadASCIIFromString(&gltfModel, &error, &warning, fileData, size, baseDir); free(fileData); #else - bool fileLoaded = gltfContext.LoadASCIIFromFile(&gltfModel, &error, filename.c_str()); + bool fileLoaded = gltfContext.LoadASCIIFromFile(&gltfModel, &error, &warning, filename.c_str()); #endif std::vector indexBuffer; std::vector vertexBuffer; diff --git a/base/tiny_gltf.h b/base/tiny_gltf.h index 2206057..95564fa 100644 --- a/base/tiny_gltf.h +++ b/base/tiny_gltf.h @@ -4,7 +4,7 @@ // // The MIT License (MIT) // -// Copyright (c) 2015 - 2017 Syoyo Fujita, AurĂ©lien Chatelain and many +// Copyright (c) 2015 - 2018 Syoyo Fujita, AurĂ©lien Chatelain and many // contributors. // // Permission is hereby granted, free of charge, to any person obtaining a copy @@ -26,6 +26,7 @@ // THE SOFTWARE. // Version: +// - v2.0.1 Add comparsion feature(Thanks to @Selmar). // - v2.0.0 glTF 2.0!. // // Tiny glTF loader is using following third party libraries: @@ -37,13 +38,14 @@ #ifndef TINY_GLTF_H_ #define TINY_GLTF_H_ -#include +#include #include +#include +#include #include #include #include #include -#include namespace tinygltf { @@ -134,6 +136,9 @@ namespace tinygltf { #define TINYGLTF_SHADER_TYPE_VERTEX_SHADER (35633) #define TINYGLTF_SHADER_TYPE_FRAGMENT_SHADER (35632) +#define TINYGLTF_DOUBLE_EPS (1.e-12) +#define TINYGLTF_DOUBLE_EQUAL(a, b) (std::fabs((b) - (a)) < TINYGLTF_DOUBLE_EPS) + typedef enum { NULL_TYPE = 0, NUMBER_TYPE = 1, @@ -145,8 +150,7 @@ typedef enum { OBJECT_TYPE = 7 } Type; -static inline int32_t GetComponentSizeInBytes(uint32_t componentType) -{ +static inline int32_t GetComponentSizeInBytes(uint32_t componentType) { if (componentType == TINYGLTF_COMPONENT_TYPE_BYTE) { return 1; } else if (componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) { @@ -169,8 +173,7 @@ static inline int32_t GetComponentSizeInBytes(uint32_t componentType) } } -static inline int32_t GetTypeSizeInBytes(uint32_t ty) -{ +static inline int32_t GetTypeSizeInBytes(uint32_t ty) { if (ty == TINYGLTF_TYPE_SCALAR) { return 1; } else if (ty == TINYGLTF_TYPE_VEC2) { @@ -191,6 +194,10 @@ static inline int32_t GetTypeSizeInBytes(uint32_t ty) } } +bool IsDataURI(const std::string &in); +bool DecodeDataURI(std::vector *out, std::string &mime_type, + const std::string &in, size_t reqBytes, bool checkSize); + #ifdef __clang__ #pragma clang diagnostic push // Suppress warning for : static Value null_value @@ -291,6 +298,8 @@ class Value { size_t Size() const { return (IsArray() ? ArrayLen() : Keys().size()); } + bool operator==(const tinygltf::Value &other) const; + protected: int type_; @@ -307,7 +316,6 @@ class Value { #pragma clang diagnostic pop #endif - #define TINYGLTF_VALUE_GET(ctype, var) \ template <> \ inline const ctype &Value::Get() const { \ @@ -332,47 +340,48 @@ TINYGLTF_VALUE_GET(Value::Object, object_value_) #pragma clang diagnostic ignored "-Wpadded" #endif -///Agregate object for representing a color +/// Agregate object for representing a color using ColorValue = std::array; - struct Parameter { - bool bool_value; +struct Parameter { + bool bool_value = false; + bool has_number_value = false; std::string string_value; std::vector number_array; std::map json_double_value; - - //context sensitive methods. depending the type of the Parameter you are accessing, these are either valid or not - //If this parameter represent a texture map in a material, will return the texture index - - ///Return the index of a texture if this Parameter is a texture map. - ///Returned value is only valid if the parameter represent a texture from a material + double number_value = 0.0; + // context sensitive methods. depending the type of the Parameter you are + // accessing, these are either valid or not + // If this parameter represent a texture map in a material, will return the + // texture index + + /// Return the index of a texture if this Parameter is a texture map. + /// Returned value is only valid if the parameter represent a texture from a + /// material int TextureIndex() const { const auto it = json_double_value.find("index"); - if (it != std::end(json_double_value)) - { + if (it != std::end(json_double_value)) { return int(it->second); } return -1; } - ///Material factor, like the roughness or metalness of a material - ///Returned value is only valid if the parameter represent a texture from a material - double Factor() const { - return number_array[0]; - } + /// Material factor, like the roughness or metalness of a material + /// Returned value is only valid if the parameter represent a texture from a + /// material + double Factor() const { return number_value; } - ///Return the color of a material - ///Returned value is only valid if the parameter represent a texture from a material + /// Return the color of a material + /// Returned value is only valid if the parameter represent a texture from a + /// material ColorValue ColorFactor() const { return { - { // this agregate intialize the std::array object, and uses C++11 RVO. - number_array[0], - number_array[1], - number_array[2], - (number_array.size() > 3 ? number_array[3] : 1.0) - } - }; + {// this agregate intialize the std::array object, and uses C++11 RVO. + number_array[0], number_array[1], number_array[2], + (number_array.size() > 3 ? number_array[3] : 1.0)}}; } + + bool operator==(const Parameter &) const; }; #ifdef __clang__ @@ -385,6 +394,7 @@ using ColorValue = std::array; #endif typedef std::map ParameterMap; +typedef std::map ExtensionMap; struct AnimationChannel { int sampler; // required @@ -394,6 +404,7 @@ struct AnimationChannel { Value extras; AnimationChannel() : sampler(-1), target_node(-1) {} + bool operator==(const AnimationChannel &) const; }; struct AnimationSampler { @@ -401,16 +412,20 @@ struct AnimationSampler { int output; // required std::string interpolation; // in ["LINEAR", "STEP", "CATMULLROMSPLINE", // "CUBICSPLINE"], default "LINEAR" + Value extras; AnimationSampler() : input(-1), output(-1), interpolation("LINEAR") {} + bool operator==(const AnimationSampler &) const; }; -typedef struct { +struct Animation { std::string name; std::vector channels; std::vector samplers; Value extras; -} Animation; + + bool operator==(const Animation &) const; +}; struct Skin { std::string name; @@ -422,6 +437,7 @@ struct Skin { inverseBindMatrices = -1; skeleton = -1; } + bool operator==(const Skin &) const; }; struct Sampler { @@ -440,6 +456,7 @@ struct Sampler { Sampler() : wrapS(TINYGLTF_TEXTURE_WRAP_REPEAT), wrapT(TINYGLTF_TEXTURE_WRAP_REPEAT) {} + bool operator==(const Sampler &) const; }; struct Image { @@ -449,19 +466,39 @@ struct Image { int component; std::vector image; int bufferView; // (required if no uri) - std::string mimeType; // (required if no uri) ["image/jpeg", "image/png"] - std::string uri; // (reqiored if no mimeType) + std::string mimeType; // (required if no uri) ["image/jpeg", "image/png", + // "image/bmp", "image/gif"] + std::string uri; // (required if no mimeType) Value extras; - - Image() { bufferView = -1; } + ExtensionMap extensions; + + // When this flag is true, data is stored to `image` in as-is format(e.g. jpeg + // compressed for "image/jpeg" mime) This feature is good if you use custom + // image loader function. (e.g. delayed decoding of images for faster glTF + // parsing) Default parser for Image does not provide as-is loading feature at + // the moment. (You can manipulate this by providing your own LoadImageData + // function) + bool as_is; + + Image() : as_is(false) { + bufferView = -1; + width = -1; + height = -1; + component = -1; + } + bool operator==(const Image &) const; }; struct Texture { + std::string name; + int sampler; - int source; // Required (not specified in the spec ?) + int source; Value extras; + ExtensionMap extensions; Texture() : sampler(-1), source(-1) {} + bool operator==(const Texture &) const; }; // Each extension should be stored in a ParameterMap. @@ -472,9 +509,11 @@ struct Material { ParameterMap values; // PBR metal/roughness workflow ParameterMap additionalValues; // normal/occlusion/emissive values - ParameterMap extCommonValues; // KHR_common_material extension - ParameterMap extPBRValues; + + ExtensionMap extensions; Value extras; + + bool operator==(const Material &) const; }; struct BufferView { @@ -488,6 +527,7 @@ struct BufferView { Value extras; BufferView() : byteOffset(0), byteStride(0) {} + bool operator==(const BufferView &) const; }; struct Accessor { @@ -513,7 +553,8 @@ struct Accessor { int ByteStride(const BufferView &bufferViewObject) const { if (bufferViewObject.byteStride == 0) { // Assume data is tightly packed. - int componentSizeInBytes = GetComponentSizeInBytes(static_cast(componentType)); + int componentSizeInBytes = + GetComponentSizeInBytes(static_cast(componentType)); if (componentSizeInBytes <= 0) { return -1; } @@ -525,8 +566,10 @@ struct Accessor { return componentSizeInBytes * typeSizeInBytes; } else { - // Check if byteStride is a mulple of the size of the accessor's component type. - int componentSizeInBytes = GetComponentSizeInBytes(static_cast(componentType)); + // Check if byteStride is a mulple of the size of the accessor's component + // type. + int componentSizeInBytes = + GetComponentSizeInBytes(static_cast(componentType)); if (componentSizeInBytes <= 0) { return -1; } @@ -541,34 +584,37 @@ struct Accessor { } Accessor() { bufferView = -1; } + bool operator==(const tinygltf::Accessor &) const; }; struct PerspectiveCamera { - float aspectRatio; // min > 0 - float yfov; // required. min > 0 - float zfar; // min > 0 - float znear; // required. min > 0 + double aspectRatio; // min > 0 + double yfov; // required. min > 0 + double zfar; // min > 0 + double znear; // required. min > 0 PerspectiveCamera() - : aspectRatio(0.0f), - yfov(0.0f), - zfar(0.0f) // 0 = use infinite projecton matrix + : aspectRatio(0.0), + yfov(0.0), + zfar(0.0) // 0 = use infinite projecton matrix , - znear(0.0f) {} + znear(0.0) {} + bool operator==(const PerspectiveCamera &) const; - ParameterMap extensions; + ExtensionMap extensions; Value extras; }; struct OrthographicCamera { - float xmag; // required. must not be zero. - float ymag; // required. must not be zero. - float zfar; // required. `zfar` must be greater than `znear`. - float znear; // required + double xmag; // required. must not be zero. + double ymag; // required. must not be zero. + double zfar; // required. `zfar` must be greater than `znear`. + double znear; // required - OrthographicCamera() : xmag(0.0f), ymag(0.0f), zfar(0.0f), znear(0.0f) {} + OrthographicCamera() : xmag(0.0), ymag(0.0), zfar(0.0), znear(0.0) {} + bool operator==(const OrthographicCamera &) const; - ParameterMap extensions; + ExtensionMap extensions; Value extras; }; @@ -580,8 +626,9 @@ struct Camera { OrthographicCamera orthographic; Camera() {} + bool operator==(const Camera &) const; - ParameterMap extensions; + ExtensionMap extensions; Value extras; }; @@ -604,16 +651,19 @@ struct Primitive { material = -1; indices = -1; } + bool operator==(const Primitive &) const; }; -typedef struct { +struct Mesh { std::string name; std::vector primitives; std::vector weights; // weights to be applied to the Morph Targets std::vector > targets; - ParameterMap extensions; + ExtensionMap extensions; Value extras; -} Mesh; + + bool operator==(const Mesh &) const; +}; class Node { public: @@ -632,11 +682,11 @@ class Node { matrix = rhs.matrix; weights = rhs.weights; + extensions = rhs.extensions; extras = rhs.extras; - extLightsValues = rhs.extLightsValues; } - ~Node() {} + bool operator==(const Node &) const; int camera; // the index of the camera referenced by this node @@ -650,45 +700,54 @@ class Node { std::vector matrix; // length must be 0 or 16 std::vector weights; // The weights of the instantiated Morph Target + ExtensionMap extensions; Value extras; - ParameterMap extLightsValues; // KHR_lights_cmn extension }; -typedef struct { +struct Buffer { std::string name; std::vector data; std::string uri; // considered as required here but not in the spec (need to clarify) Value extras; -} Buffer; -typedef struct { + bool operator==(const Buffer &) const; +}; + +struct Asset { std::string version; // required std::string generator; std::string minVersion; std::string copyright; - ParameterMap extensions; + ExtensionMap extensions; Value extras; -} Asset; + + bool operator==(const Asset &) const; +}; struct Scene { std::string name; std::vector nodes; - ParameterMap extensions; - ParameterMap extras; + ExtensionMap extensions; + Value extras; + + bool operator==(const Scene &) const; }; struct Light { std::string name; std::vector color; std::string type; + + bool operator==(const Light &) const; }; class Model { public: Model() {} ~Model() {} + bool operator==(const Model &) const; std::vector accessors; std::vector animations; @@ -704,6 +763,7 @@ class Model { std::vector cameras; std::vector scenes; std::vector lights; + ExtensionMap extensions; int defaultScene; std::vector extensionsUsed; @@ -725,17 +785,91 @@ enum SectionCheck { REQUIRE_ALL = 0x3f }; +/// +/// LoadImageDataFunction type. Signature for custom image loading callbacks. +/// +typedef bool (*LoadImageDataFunction)(Image *, std::string *, std::string *, + int, int, const unsigned char *, int, + void *); + +/// +/// WriteImageDataFunction type. Signature for custom image writing callbacks. +/// +typedef bool (*WriteImageDataFunction)(const std::string *, const std::string *, + Image *, bool, void *); + +#ifndef TINYGLTF_NO_STB_IMAGE +// Declaration of default image loader callback +bool LoadImageData(Image *image, std::string *err, std::string *warn, + int req_width, int req_height, const unsigned char *bytes, + int size, void *); +#endif + +#ifndef TINYGLTF_NO_STB_IMAGE_WRITE +// Declaration of default image writer callback +bool WriteImageData(const std::string *basepath, const std::string *filename, + Image *image, bool embedImages, void *); +#endif + +/// +/// FilExistsFunction type. Signature for custom filesystem callbacks. +/// +typedef bool (*FileExistsFunction)(const std::string &abs_filename, void *); + +/// +/// ExpandFilePathFunction type. Signature for custom filesystem callbacks. +/// +typedef std::string (*ExpandFilePathFunction)(const std::string &, void *); + +/// +/// ReadWholeFileFunction type. Signature for custom filesystem callbacks. +/// +typedef bool (*ReadWholeFileFunction)(std::vector *, + std::string *, const std::string &, + void *); + +/// +/// WriteWholeFileFunction type. Signature for custom filesystem callbacks. +/// +typedef bool (*WriteWholeFileFunction)(std::string *, const std::string &, + const std::vector &, + void *); + +/// +/// A structure containing all required filesystem callbacks and a pointer to +/// their user data. +/// +struct FsCallbacks { + FileExistsFunction FileExists; + ExpandFilePathFunction ExpandFilePath; + ReadWholeFileFunction ReadWholeFile; + WriteWholeFileFunction WriteWholeFile; + + void *user_data; // An argument that is passed to all fs callbacks +}; + +#ifndef TINYGLTF_NO_FS +// Declaration of default filesystem callbacks + +bool FileExists(const std::string &abs_filename, void *); + +std::string ExpandFilePath(const std::string &filepath, void *); + +bool ReadWholeFile(std::vector *out, std::string *err, + const std::string &filepath, void *); + +bool WriteWholeFile(std::string *err, const std::string &filepath, + const std::vector &contents, void *); +#endif class TinyGLTF { public: - #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wc++98-compat" #endif - TinyGLTF() : bin_data_(nullptr), bin_size_(0), is_binary_(false) { - } + TinyGLTF() : bin_data_(nullptr), bin_size_(0), is_binary_(false) {} #ifdef __clang__ #pragma clang diagnostic pop @@ -745,36 +879,40 @@ class TinyGLTF { /// /// Loads glTF ASCII asset from a file. + /// Set warning message to `warn` for example it fails to load asserts. /// Returns false and set error string to `err` if there's an error. /// - bool LoadASCIIFromFile(Model *model, std::string *err, + bool LoadASCIIFromFile(Model *model, std::string *err, std::string *warn, const std::string &filename, unsigned int check_sections = REQUIRE_ALL); /// /// Loads glTF ASCII asset from string(memory). /// `length` = strlen(str); + /// Set warning message to `warn` for example it fails to load asserts. /// Returns false and set error string to `err` if there's an error. /// - bool LoadASCIIFromString(Model *model, std::string *err, const char *str, - const unsigned int length, + bool LoadASCIIFromString(Model *model, std::string *err, std::string *warn, + const char *str, const unsigned int length, const std::string &base_dir, unsigned int check_sections = REQUIRE_ALL); /// /// Loads glTF binary asset from a file. + /// Set warning message to `warn` for example it fails to load asserts. /// Returns false and set error string to `err` if there's an error. /// - bool LoadBinaryFromFile(Model *model, std::string *err, + bool LoadBinaryFromFile(Model *model, std::string *err, std::string *warn, const std::string &filename, unsigned int check_sections = REQUIRE_ALL); /// /// Loads glTF binary asset from memory. /// `length` = strlen(str); + /// Set warning message to `warn` for example it fails to load asserts. /// Returns false and set error string to `err` if there's an error. /// - bool LoadBinaryFromMemory(Model *model, std::string *err, + bool LoadBinaryFromMemory(Model *model, std::string *err, std::string *warn, const unsigned char *bytes, const unsigned int length, const std::string &base_dir = "", @@ -783,38 +921,86 @@ class TinyGLTF { /// /// Write glTF to file. /// - bool WriteGltfSceneToFile( - Model *model, - const std::string & - filename /*, bool embedImages, bool embedBuffers, bool writeBinary*/); + bool WriteGltfSceneToFile(Model *model, const std::string &filename, + bool embedImages, + bool embedBuffers, + bool prettyPrint, + bool writeBinary); + + /// + /// Set callback to use for loading image data + /// + void SetImageLoader(LoadImageDataFunction LoadImageData, void *user_data); + + /// + /// Set callback to use for writing image data + /// + void SetImageWriter(WriteImageDataFunction WriteImageData, void *user_data); + + /// + /// Set callbacks to use for filesystem (fs) access and their user data + /// + void SetFsCallbacks(FsCallbacks callbacks); private: /// /// Loads glTF asset from string(memory). /// `length` = strlen(str); + /// Set warning message to `warn` for example it fails to load asserts /// Returns false and set error string to `err` if there's an error. /// - bool LoadFromString(Model *model, std::string *err, const char *str, - const unsigned int length, const std::string &base_dir, - unsigned int check_sections); + bool LoadFromString(Model *model, std::string *err, std::string *warn, + const char *str, const unsigned int length, + const std::string &base_dir, unsigned int check_sections); const unsigned char *bin_data_; size_t bin_size_; bool is_binary_; + + FsCallbacks fs = { +#ifndef TINYGLTF_NO_FS + &tinygltf::FileExists, &tinygltf::ExpandFilePath, + &tinygltf::ReadWholeFile, &tinygltf::WriteWholeFile, + + nullptr // Fs callback user data +#else + nullptr, nullptr, nullptr, nullptr, + + nullptr // Fs callback user data +#endif + }; + + LoadImageDataFunction LoadImageData = +#ifndef TINYGLTF_NO_STB_IMAGE + &tinygltf::LoadImageData; +#else + nullptr; +#endif + void *load_image_user_data_ = reinterpret_cast(&fs); + + WriteImageDataFunction WriteImageData = +#ifndef TINYGLTF_NO_STB_IMAGE_WRITE + &tinygltf::WriteImageData; +#else + nullptr; +#endif + void *write_image_user_data_ = reinterpret_cast(&fs); }; #ifdef __clang__ -#pragma clang diagnostic pop // -Wpadded +#pragma clang diagnostic pop // -Wpadded #endif } // namespace tinygltf #endif // TINY_GLTF_H_ -#ifdef TINYGLTF_IMPLEMENTATION +#if defined(TINYGLTF_IMPLEMENTATION) || defined(__INTELLISENSE__) #include //#include +#ifndef TINYGLTF_NO_FS #include +#endif #include #ifdef __clang__ @@ -824,17 +1010,20 @@ class TinyGLTF { #pragma clang diagnostic ignored "-Wexit-time-destructors" #pragma clang diagnostic ignored "-Wconversion" #pragma clang diagnostic ignored "-Wold-style-cast" -#pragma clang diagnostic ignored "-Wdouble-promotion" #pragma clang diagnostic ignored "-Wglobal-constructors" #pragma clang diagnostic ignored "-Wreserved-id-macro" #pragma clang diagnostic ignored "-Wdisabled-macro-expansion" #pragma clang diagnostic ignored "-Wpadded" #pragma clang diagnostic ignored "-Wc++98-compat" +#pragma clang diagnostic ignored "-Wc++98-compat-pedantic" #pragma clang diagnostic ignored "-Wdocumentation-unknown-command" #pragma clang diagnostic ignored "-Wswitch-enum" #pragma clang diagnostic ignored "-Wimplicit-fallthrough" #pragma clang diagnostic ignored "-Wweak-vtables" #pragma clang diagnostic ignored "-Wcovered-switch-default" +#if __has_warning("-Wdouble-promotion") +#pragma clang diagnostic ignored "-Wdouble-promotion" +#endif #if __has_warning("-Wcomma") #pragma clang diagnostic ignored "-Wcomma" #endif @@ -844,10 +1033,30 @@ class TinyGLTF { #if __has_warning("-Wcast-qual") #pragma clang diagnostic ignored "-Wcast-qual" #endif +#if __has_warning("-Wmissing-variable-declarations") +#pragma clang diagnostic ignored "-Wmissing-variable-declarations" +#endif +#if __has_warning("-Wmissing-prototypes") +#pragma clang diagnostic ignored "-Wmissing-prototypes" +#endif +#if __has_warning("-Wcast-align") +#pragma clang diagnostic ignored "-Wcast-align" +#endif +#if __has_warning("-Wnewline-eof") +#pragma clang diagnostic ignored "-Wnewline-eof" +#endif #endif #include "./json.hpp" + +#ifndef TINYGLTF_NO_STB_IMAGE #include "./stb_image.h" +#endif + +#ifndef TINYGLTF_NO_STB_IMAGE_WRITE +#include "./stb_image_write.h" +#endif + #ifdef __clang__ #pragma clang diagnostic pop #endif @@ -869,7 +1078,7 @@ class TinyGLTF { using nlohmann::json; #ifdef __APPLE__ - #include "TargetConditionals.h" +#include "TargetConditionals.h" #endif #ifdef __clang__ @@ -879,85 +1088,230 @@ using nlohmann::json; namespace tinygltf { -static void swap4(unsigned int *val) { -#ifdef TINYGLTF_LITTLE_ENDIAN - (void)val; -#else - unsigned int tmp = *val; - unsigned char *dst = reinterpret_cast(val); - unsigned char *src = reinterpret_cast(&tmp); +// Equals function for Value, for recursivity +static bool Equals(const tinygltf::Value &one, const tinygltf::Value &other) { + if (one.Type() != other.Type()) return false; - dst[0] = src[3]; - dst[1] = src[2]; - dst[2] = src[1]; - dst[3] = src[0]; -#endif + switch (one.Type()) { + case NULL_TYPE: + return true; + case BOOL_TYPE: + return one.Get() == other.Get(); + case NUMBER_TYPE: + return TINYGLTF_DOUBLE_EQUAL(one.Get(), other.Get()); + case INT_TYPE: + return one.Get() == other.Get(); + case OBJECT_TYPE: { + auto oneObj = one.Get(); + auto otherObj = other.Get(); + if (oneObj.size() != otherObj.size()) return false; + for (auto &it : oneObj) { + auto otherIt = otherObj.find(it.first); + if (otherIt == otherObj.end()) return false; + + if (!Equals(it.second, otherIt->second)) return false; + } + return true; + } + case ARRAY_TYPE: { + if (one.Size() != other.Size()) return false; + for (int i = 0; i < int(one.Size()); ++i) + if (Equals(one.Get(i), other.Get(i))) return false; + return true; + } + case STRING_TYPE: + return one.Get() == other.Get(); + case BINARY_TYPE: + return one.Get >() == + other.Get >(); + default: { + // unhandled type + return false; + } + } } -static bool FileExists(const std::string &abs_filename) { - bool ret; -#ifdef _WIN32 - FILE *fp; - errno_t err = fopen_s(&fp, abs_filename.c_str(), "rb"); - if (err != 0) { - return false; - } -#else - FILE *fp = fopen(abs_filename.c_str(), "rb"); -#endif - if (fp) { - ret = true; - fclose(fp); - } else { - ret = false; +// Equals function for std::vector using TINYGLTF_DOUBLE_EPSILON +static bool Equals(const std::vector &one, + const std::vector &other) { + if (one.size() != other.size()) return false; + for (int i = 0; i < int(one.size()); ++i) { + if (!TINYGLTF_DOUBLE_EQUAL(one[size_t(i)], other[size_t(i)])) return false; } - - return ret; + return true; } -static std::string ExpandFilePath(const std::string &filepath) { -#ifdef _WIN32 - DWORD len = ExpandEnvironmentStringsA(filepath.c_str(), NULL, 0); - char *str = new char[len]; - ExpandEnvironmentStringsA(filepath.c_str(), str, len); - - std::string s(str); - - delete[] str; +bool Accessor::operator==(const Accessor &other) const { + return this->bufferView == other.bufferView && + this->byteOffset == other.byteOffset && + this->componentType == other.componentType && + this->count == other.count && this->extras == other.extras && + Equals(this->maxValues, other.maxValues) && + Equals(this->minValues, other.minValues) && this->name == other.name && + this->normalized == other.normalized && this->type == other.type; +} +bool Animation::operator==(const Animation &other) const { + return this->channels == other.channels && this->extras == other.extras && + this->name == other.name && this->samplers == other.samplers; +} +bool AnimationChannel::operator==(const AnimationChannel &other) const { + return this->extras == other.extras && + this->target_node == other.target_node && + this->target_path == other.target_path && + this->sampler == other.sampler; +} +bool AnimationSampler::operator==(const AnimationSampler &other) const { + return this->extras == other.extras && this->input == other.input && + this->interpolation == other.interpolation && + this->output == other.output; +} +bool Asset::operator==(const Asset &other) const { + return this->copyright == other.copyright && + this->extensions == other.extensions && this->extras == other.extras && + this->generator == other.generator && + this->minVersion == other.minVersion && this->version == other.version; +} +bool Buffer::operator==(const Buffer &other) const { + return this->data == other.data && this->extras == other.extras && + this->name == other.name && this->uri == other.uri; +} +bool BufferView::operator==(const BufferView &other) const { + return this->buffer == other.buffer && this->byteLength == other.byteLength && + this->byteOffset == other.byteOffset && + this->byteStride == other.byteStride && this->name == other.name && + this->target == other.target && this->extras == other.extras; +} +bool Camera::operator==(const Camera &other) const { + return this->name == other.name && this->extensions == other.extensions && + this->extras == other.extras && + this->orthographic == other.orthographic && + this->perspective == other.perspective && this->type == other.type; +} +bool Image::operator==(const Image &other) const { + return this->bufferView == other.bufferView && + this->component == other.component && this->extras == other.extras && + this->height == other.height && this->image == other.image && + this->mimeType == other.mimeType && this->name == other.name && + this->uri == other.uri && this->width == other.width; +} +bool Light::operator==(const Light &other) const { + return Equals(this->color, other.color) && this->name == other.name && + this->type == other.type; +} +bool Material::operator==(const Material &other) const { + return this->additionalValues == other.additionalValues && + this->extensions == other.extensions && this->extras == other.extras && + this->name == other.name && this->values == other.values; +} +bool Mesh::operator==(const Mesh &other) const { + return this->extensions == other.extensions && this->extras == other.extras && + this->name == other.name && this->primitives == other.primitives && + this->targets == other.targets && Equals(this->weights, other.weights); +} +bool Model::operator==(const Model &other) const { + return this->accessors == other.accessors && + this->animations == other.animations && this->asset == other.asset && + this->buffers == other.buffers && + this->bufferViews == other.bufferViews && + this->cameras == other.cameras && + this->defaultScene == other.defaultScene && + this->extensions == other.extensions && + this->extensionsRequired == other.extensionsRequired && + this->extensionsUsed == other.extensionsUsed && + this->extras == other.extras && this->images == other.images && + this->lights == other.lights && this->materials == other.materials && + this->meshes == other.meshes && this->nodes == other.nodes && + this->samplers == other.samplers && this->scenes == other.scenes && + this->skins == other.skins && this->textures == other.textures; +} +bool Node::operator==(const Node &other) const { + return this->camera == other.camera && this->children == other.children && + this->extensions == other.extensions && this->extras == other.extras && + Equals(this->matrix, other.matrix) && this->mesh == other.mesh && + this->name == other.name && Equals(this->rotation, other.rotation) && + Equals(this->scale, other.scale) && this->skin == other.skin && + Equals(this->translation, other.translation) && + Equals(this->weights, other.weights); +} +bool OrthographicCamera::operator==(const OrthographicCamera &other) const { + return this->extensions == other.extensions && this->extras == other.extras && + TINYGLTF_DOUBLE_EQUAL(this->xmag, other.xmag) && + TINYGLTF_DOUBLE_EQUAL(this->ymag, other.ymag) && + TINYGLTF_DOUBLE_EQUAL(this->zfar, other.zfar) && + TINYGLTF_DOUBLE_EQUAL(this->znear, other.znear); +} +bool Parameter::operator==(const Parameter &other) const { + if (this->bool_value != other.bool_value || + this->has_number_value != other.has_number_value) + return false; - return s; -#else + if (!TINYGLTF_DOUBLE_EQUAL(this->number_value, other.number_value)) + return false; -#if defined(TARGET_OS_IPHONE) || defined(TARGET_IPHONE_SIMULATOR) || defined(__ANDROID__) - // no expansion - std::string s = filepath; -#else - std::string s; - wordexp_t p; + if (this->json_double_value.size() != other.json_double_value.size()) + return false; + for (auto &it : this->json_double_value) { + auto otherIt = other.json_double_value.find(it.first); + if (otherIt == other.json_double_value.end()) return false; - if (filepath.empty()) { - return ""; + if (!TINYGLTF_DOUBLE_EQUAL(it.second, otherIt->second)) return false; } - // char** w; - int ret = wordexp(filepath.c_str(), &p, 0); - if (ret) { - // err - s = filepath; - return s; - } + if (!Equals(this->number_array, other.number_array)) return false; - // Use first element only. - if (p.we_wordv) { - s = std::string(p.we_wordv[0]); - wordfree(&p); - } else { - s = filepath; - } + if (this->string_value != other.string_value) return false; -#endif + return true; +} +bool PerspectiveCamera::operator==(const PerspectiveCamera &other) const { + return TINYGLTF_DOUBLE_EQUAL(this->aspectRatio, other.aspectRatio) && + this->extensions == other.extensions && this->extras == other.extras && + TINYGLTF_DOUBLE_EQUAL(this->yfov, other.yfov) && + TINYGLTF_DOUBLE_EQUAL(this->zfar, other.zfar) && + TINYGLTF_DOUBLE_EQUAL(this->znear, other.znear); +} +bool Primitive::operator==(const Primitive &other) const { + return this->attributes == other.attributes && this->extras == other.extras && + this->indices == other.indices && this->material == other.material && + this->mode == other.mode && this->targets == other.targets; +} +bool Sampler::operator==(const Sampler &other) const { + return this->extras == other.extras && this->magFilter == other.magFilter && + this->minFilter == other.minFilter && this->name == other.name && + this->wrapR == other.wrapR && this->wrapS == other.wrapS && + this->wrapT == other.wrapT; +} +bool Scene::operator==(const Scene &other) const { + return this->extensions == other.extensions && this->extras == other.extras && + this->name == other.name && this->nodes == other.nodes; + ; +} +bool Skin::operator==(const Skin &other) const { + return this->inverseBindMatrices == other.inverseBindMatrices && + this->joints == other.joints && this->name == other.name && + this->skeleton == other.skeleton; +} +bool Texture::operator==(const Texture &other) const { + return this->extensions == other.extensions && this->extras == other.extras && + this->name == other.name && this->sampler == other.sampler && + this->source == other.source; +} +bool Value::operator==(const Value &other) const { + return Equals(*this, other); +} - return s; +static void swap4(unsigned int *val) { +#ifdef TINYGLTF_LITTLE_ENDIAN + (void)val; +#else + unsigned int tmp = *val; + unsigned char *dst = reinterpret_cast(val); + unsigned char *src = reinterpret_cast(&tmp); + + dst[0] = src[3]; + dst[1] = src[2]; + dst[2] = src[1]; + dst[3] = src[0]; #endif } @@ -977,10 +1331,17 @@ static std::string JoinPath(const std::string &path0, } static std::string FindFile(const std::vector &paths, - const std::string &filepath) { + const std::string &filepath, FsCallbacks *fs) { + if (fs == nullptr || fs->ExpandFilePath == nullptr || + fs->FileExists == nullptr) { + // Error, fs callback[s] missing + return std::string(); + } + for (size_t i = 0; i < paths.size(); i++) { - std::string absPath = ExpandFilePath(JoinPath(paths[i], filepath)); - if (FileExists(absPath)) { + std::string absPath = + fs->ExpandFilePath(JoinPath(paths[i], filepath), fs->user_data); + if (fs->FileExists(absPath, fs->user_data)) { return absPath; } } @@ -988,12 +1349,11 @@ static std::string FindFile(const std::vector &paths, return std::string(); } -// std::string GetFilePathExtension(const std::string& FileName) -//{ -// if(FileName.find_last_of(".") != std::string::npos) -// return FileName.substr(FileName.find_last_of(".")+1); -// return ""; -//} +static std::string GetFilePathExtension(const std::string &FileName) { + if (FileName.find_last_of(".") != std::string::npos) + return FileName.substr(FileName.find_last_of(".") + 1); + return ""; +} static std::string GetBaseDir(const std::string &filepath) { if (filepath.find_last_of("/\\") != std::string::npos) @@ -1001,7 +1361,12 @@ static std::string GetBaseDir(const std::string &filepath) { return ""; } -// std::string base64_encode(unsigned char const* , unsigned int len); +// https://stackoverflow.com/questions/8520560/get-a-file-name-from-a-path +static std::string GetBaseFilename(const std::string &filepath) { + return filepath.substr(filepath.find_last_of("/\\") + 1); +} + +std::string base64_encode(unsigned char const *, unsigned int len); std::string base64_decode(std::string const &s); /* @@ -1047,6 +1412,46 @@ static inline bool is_base64(unsigned char c) { return (isalnum(c) || (c == '+') || (c == '/')); } +std::string base64_encode(unsigned char const *bytes_to_encode, + unsigned int in_len) { + std::string ret; + int i = 0; + int j = 0; + unsigned char char_array_3[3]; + unsigned char char_array_4[4]; + + while (in_len--) { + char_array_3[i++] = *(bytes_to_encode++); + if (i == 3) { + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = + ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = + ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (i = 0; (i < 4); i++) ret += base64_chars[char_array_4[i]]; + i = 0; + } + } + + if (i) { + for (j = i; j < 3; j++) char_array_3[j] = '\0'; + + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = + ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = + ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + + for (j = 0; (j < i + 1); j++) ret += base64_chars[char_array_4[j]]; + + while ((i++ < 3)) ret += '='; + } + + return ret; +} + std::string base64_decode(std::string const &encoded_string) { int in_len = static_cast(encoded_string.size()); int i = 0; @@ -1097,48 +1502,53 @@ std::string base64_decode(std::string const &encoded_string) { #endif static bool LoadExternalFile(std::vector *out, std::string *err, - const std::string &filename, - const std::string &basedir, size_t reqBytes, - bool checkSize) { + std::string *warn, const std::string &filename, + const std::string &basedir, bool required, + size_t reqBytes, bool checkSize, FsCallbacks *fs) { + if (fs == nullptr || fs->FileExists == nullptr || + fs->ExpandFilePath == nullptr || fs->ReadWholeFile == nullptr) { + // This is a developer error, assert() ? + if (err) { + (*err) += "FS callback[s] not set\n"; + } + return false; + } + + std::string *failMsgOut = required ? err : warn; + out->clear(); std::vector paths; paths.push_back(basedir); paths.push_back("."); - std::string filepath = FindFile(paths, filename); + std::string filepath = FindFile(paths, filename, fs); if (filepath.empty() || filename.empty()) { - if (err) { - (*err) += "File not found : " + filename + "\n"; + if (failMsgOut) { + (*failMsgOut) += "File not found : " + filename + "\n"; } return false; } - std::ifstream f(filepath.c_str(), std::ifstream::binary); - if (!f) { - if (err) { - (*err) += "File open error : " + filepath + "\n"; + std::vector buf; + std::string fileReadErr; + bool fileRead = + fs->ReadWholeFile(&buf, &fileReadErr, filepath, fs->user_data); + if (!fileRead) { + if (failMsgOut) { + (*failMsgOut) += + "File read error : " + filepath + " : " + fileReadErr + "\n"; } return false; } - f.seekg(0, f.end); - size_t sz = static_cast(f.tellg()); - if (int(sz) < 0) { - // Looks reading directory, not a file. - return false; - } - + size_t sz = buf.size(); if (sz == 0) { - // Invalid file size. + if (failMsgOut) { + (*failMsgOut) += "File is empty : " + filepath + "\n"; + } return false; } - std::vector buf(sz); - - f.seekg(0, f.beg); - f.read(reinterpret_cast(&buf.at(0)), - static_cast(sz)); - f.close(); if (checkSize) { if (reqBytes == sz) { @@ -1148,8 +1558,8 @@ static bool LoadExternalFile(std::vector *out, std::string *err, std::stringstream ss; ss << "File size mismatch : " << filepath << ", requestedBytes " << reqBytes << ", but got " << sz << std::endl; - if (err) { - (*err) += ss.str(); + if (failMsgOut) { + (*failMsgOut) += ss.str(); } return false; } @@ -1159,24 +1569,37 @@ static bool LoadExternalFile(std::vector *out, std::string *err, return true; } -static bool LoadImageData(Image *image, std::string *err, int req_width, - int req_height, const unsigned char *bytes, - int size) { - //std::cout << "size " << size << std::endl; +void TinyGLTF::SetImageLoader(LoadImageDataFunction func, void *user_data) { + LoadImageData = func; + load_image_user_data_ = user_data; +} + +#ifndef TINYGLTF_NO_STB_IMAGE +bool LoadImageData(Image *image, std::string *err, std::string *warn, + int req_width, int req_height, const unsigned char *bytes, + int size, void *) { + (void)warn; + + int w, h, comp, req_comp; + + // force 32-bit textures for common Vulkan compatibility. It appears that + // some GPU drivers do not support 24-bit images for Vulkan + req_comp = 4; - int w, h, comp; // if image cannot be decoded, ignore parsing and keep it by its path // don't break in this case // FIXME we should only enter this function if the image is embedded. If // image->uri references // an image file, it should be left as it is. Image loading should not be // mandatory (to support other formats) - unsigned char *data = stbi_load_from_memory(bytes, size, &w, &h, &comp, 0); + unsigned char *data = + stbi_load_from_memory(bytes, size, &w, &h, &comp, req_comp); if (!data) { + // NOTE: you can use `warn` instead of `err` if (err) { (*err) += "Unknown image format.\n"; } - return true; + return false; } if (w < 1 || h < 1) { @@ -1184,7 +1607,7 @@ static bool LoadImageData(Image *image, std::string *err, int req_width, if (err) { (*err) += "Invalid image data.\n"; } - return true; + return false; } if (req_width > 0) { @@ -1209,42 +1632,312 @@ static bool LoadImageData(Image *image, std::string *err, int req_width, image->width = w; image->height = h; - image->component = comp; - image->image.resize(static_cast(w * h * comp)); - std::copy(data, data + w * h * comp, image->image.begin()); + image->component = req_comp; + image->image.resize(static_cast(w * h * req_comp)); + std::copy(data, data + w * h * req_comp, image->image.begin()); free(data); return true; } +#endif -static bool IsDataURI(const std::string &in) { - std::string header = "data:application/octet-stream;base64,"; - if (in.find(header) == 0) { - return true; - } +void TinyGLTF::SetImageWriter(WriteImageDataFunction func, void *user_data) { + WriteImageData = func; + write_image_user_data_ = user_data; +} - header = "data:image/png;base64,"; - if (in.find(header) == 0) { - return true; - } +#ifndef TINYGLTF_NO_STB_IMAGE_WRITE +static void WriteToMemory_stbi(void *context, void *data, int size) { + std::vector *buffer = + reinterpret_cast *>(context); - header = "data:image/jpeg;base64,"; - if (in.find(header) == 0) { - return true; - } + unsigned char *pData = reinterpret_cast(data); + + buffer->insert(buffer->end(), pData, pData + size); +} + +bool WriteImageData(const std::string *basepath, const std::string *filename, + Image *image, bool embedImages, void *fsPtr) { + const std::string ext = GetFilePathExtension(*filename); + + // Write image to temporary buffer + std::string header; + std::vector data; + + if (ext == "png") { + if (!stbi_write_png_to_func(WriteToMemory_stbi, &data, image->width, + image->height, image->component, + &image->image[0], 0)) { + return false; + } + header = "data:image/png;base64,"; + } else if (ext == "jpg") { + if (!stbi_write_jpg_to_func(WriteToMemory_stbi, &data, image->width, + image->height, image->component, + &image->image[0], 100)) { + return false; + } + header = "data:image/jpeg;base64,"; + } else if (ext == "bmp") { + if (!stbi_write_bmp_to_func(WriteToMemory_stbi, &data, image->width, + image->height, image->component, + &image->image[0])) { + return false; + } + header = "data:image/bmp;base64,"; + } else if (!embedImages) { + // Error: can't output requested format to file + return false; + } + + if (embedImages) { + // Embed base64-encoded image into URI + if (data.size()) { + image->uri = + header + + base64_encode(&data[0], static_cast(data.size())); + } else { + // Throw error? + } + } else { + // Write image to disc + FsCallbacks *fs = reinterpret_cast(fsPtr); + if ((fs != nullptr) && (fs->WriteWholeFile != nullptr)) { + const std::string imagefilepath = JoinPath(*basepath, *filename); + std::string writeError; + if (!fs->WriteWholeFile(&writeError, imagefilepath, data, + fs->user_data)) { + // Could not write image file to disc; Throw error ? + return false; + } + } else { + // Throw error? + } + image->uri = *filename; + } + + return true; +} +#endif + +void TinyGLTF::SetFsCallbacks(FsCallbacks callbacks) { fs = callbacks; } + +#ifndef TINYGLTF_NO_FS +// Default implementations of filesystem functions + +bool FileExists(const std::string &abs_filename, void *) { + bool ret; +#ifdef _WIN32 + FILE *fp; + errno_t err = fopen_s(&fp, abs_filename.c_str(), "rb"); + if (err != 0) { + return false; + } +#else + FILE *fp = fopen(abs_filename.c_str(), "rb"); +#endif + if (fp) { + ret = true; + fclose(fp); + } else { + ret = false; + } + + return ret; +} + +std::string ExpandFilePath(const std::string &filepath, void *) { +#ifdef _WIN32 + DWORD len = ExpandEnvironmentStringsA(filepath.c_str(), NULL, 0); + char *str = new char[len]; + ExpandEnvironmentStringsA(filepath.c_str(), str, len); + + std::string s(str); + + delete[] str; + + return s; +#else + +#if defined(TARGET_OS_IPHONE) || defined(TARGET_IPHONE_SIMULATOR) || \ + defined(__ANDROID__) || defined(__EMSCRIPTEN__) + // no expansion + std::string s = filepath; +#else + std::string s; + wordexp_t p; + + if (filepath.empty()) { + return ""; + } + + // char** w; + int ret = wordexp(filepath.c_str(), &p, 0); + if (ret) { + // err + s = filepath; + return s; + } + + // Use first element only. + if (p.we_wordv) { + s = std::string(p.we_wordv[0]); + wordfree(&p); + } else { + s = filepath; + } + +#endif + + return s; +#endif +} + +bool ReadWholeFile(std::vector *out, std::string *err, + const std::string &filepath, void *) { + std::ifstream f(filepath.c_str(), std::ifstream::binary); + if (!f) { + if (err) { + (*err) += "File open error : " + filepath + "\n"; + } + return false; + } + + f.seekg(0, f.end); + size_t sz = static_cast(f.tellg()); + f.seekg(0, f.beg); + + if (int(sz) < 0) { + if (err) { + (*err) += "Invalid file size : " + filepath + + " (does the path point to a directory?)"; + } + return false; + } else if (sz == 0) { + if (err) { + (*err) += "File is empty : " + filepath + "\n"; + } + return false; + } + + out->resize(sz); + f.read(reinterpret_cast(&out->at(0)), + static_cast(sz)); + f.close(); + + return true; +} + +bool WriteWholeFile(std::string *err, const std::string &filepath, + const std::vector &contents, void *) { + std::ofstream f(filepath.c_str(), std::ofstream::binary); + if (!f) { + if (err) { + (*err) += "File open error for writing : " + filepath + "\n"; + } + return false; + } + + f.write(reinterpret_cast(&contents.at(0)), + static_cast(contents.size())); + if (!f) { + if (err) { + (*err) += "File write error: " + filepath + "\n"; + } + return false; + } + + f.close(); + return true; +} + +#endif // TINYGLTF_NO_FS + +static std::string MimeToExt(const std::string &mimeType) { + if (mimeType == "image/jpeg") { + return "jpg"; + } else if (mimeType == "image/png") { + return "png"; + } else if (mimeType == "image/bmp") { + return "bmp"; + } else if (mimeType == "image/gif") { + return "gif"; + } + + return ""; +} + +static void UpdateImageObject(Image &image, std::string &baseDir, int index, + bool embedImages, + WriteImageDataFunction *WriteImageData = nullptr, + void *user_data = nullptr) { + std::string filename; + std::string ext; + + // If image have uri. Use it it as a filename + if (image.uri.size()) { + filename = GetBaseFilename(image.uri); + ext = GetFilePathExtension(filename); + + } else if (image.name.size()) { + ext = MimeToExt(image.mimeType); + // Otherwise use name as filename + filename = image.name + "." + ext; + } else { + ext = MimeToExt(image.mimeType); + // Fallback to index of image as filename + filename = std::to_string(index) + "." + ext; + } + + // If callback is set, modify image data object + if (*WriteImageData != nullptr) { + std::string uri; + (*WriteImageData)(&baseDir, &filename, &image, embedImages, user_data); + } +} + +bool IsDataURI(const std::string &in) { + std::string header = "data:application/octet-stream;base64,"; + if (in.find(header) == 0) { + return true; + } + + header = "data:image/jpeg;base64,"; + if (in.find(header) == 0) { + return true; + } + + header = "data:image/png;base64,"; + if (in.find(header) == 0) { + return true; + } + + header = "data:image/bmp;base64,"; + if (in.find(header) == 0) { + return true; + } + + header = "data:image/gif;base64,"; + if (in.find(header) == 0) { + return true; + } header = "data:text/plain;base64,"; if (in.find(header) == 0) { return true; } + header = "data:application/gltf-buffer;base64,"; + if (in.find(header) == 0) { + return true; + } + return false; } -static bool DecodeDataURI(std::vector *out, - const std::string &in, size_t reqBytes, - bool checkSize) { +bool DecodeDataURI(std::vector *out, std::string &mime_type, + const std::string &in, size_t reqBytes, bool checkSize) { std::string header = "data:application/octet-stream;base64,"; std::string data; if (in.find(header) == 0) { @@ -1254,6 +1947,7 @@ static bool DecodeDataURI(std::vector *out, if (data.empty()) { header = "data:image/jpeg;base64,"; if (in.find(header) == 0) { + mime_type = "image/jpeg"; data = base64_decode(in.substr(header.size())); // cut mime string. } } @@ -1261,12 +1955,37 @@ static bool DecodeDataURI(std::vector *out, if (data.empty()) { header = "data:image/png;base64,"; if (in.find(header) == 0) { + mime_type = "image/png"; + data = base64_decode(in.substr(header.size())); // cut mime string. + } + } + + if (data.empty()) { + header = "data:image/bmp;base64,"; + if (in.find(header) == 0) { + mime_type = "image/bmp"; + data = base64_decode(in.substr(header.size())); // cut mime string. + } + } + + if (data.empty()) { + header = "data:image/gif;base64,"; + if (in.find(header) == 0) { + mime_type = "image/gif"; data = base64_decode(in.substr(header.size())); // cut mime string. } } if (data.empty()) { header = "data:text/plain;base64,"; + if (in.find(header) == 0) { + mime_type = "text/plain"; + data = base64_decode(in.substr(header.size())); + } + } + + if (data.empty()) { + header = "data:application/gltf-buffer;base64,"; if (in.find(header) == 0) { data = base64_decode(in.substr(header.size())); } @@ -1288,32 +2007,48 @@ static bool DecodeDataURI(std::vector *out, return true; } -static void ParseObjectProperty(Value *ret, const json &o) { - tinygltf::Value::Object vo; - json::const_iterator it(o.begin()); - json::const_iterator itEnd(o.end()); - - for (; it != itEnd; it++) { - json v = it.value(); - - if (v.is_boolean()) { - vo[it.key()] = tinygltf::Value(v.get()); - } else if (v.is_number()) { - vo[it.key()] = tinygltf::Value(v.get()); - } else if (v.is_number_integer()) { - vo[it.key()] = - tinygltf::Value(static_cast(v.get())); // truncate - } else if (v.is_string()) { - vo[it.key()] = tinygltf::Value(v.get()); - } else if (v.is_object()) { - tinygltf::Value child_value; - ParseObjectProperty(&child_value, v); - vo[it.key()] = child_value; - } - // TODO(syoyo) binary, array +static bool ParseJsonAsValue(Value *ret, const json &o) { + Value val{}; + switch (o.type()) { + case json::value_t::object: { + Value::Object value_object; + for (auto it = o.begin(); it != o.end(); it++) { + Value entry; + ParseJsonAsValue(&entry, it.value()); + if (entry.Type() != NULL_TYPE) value_object[it.key()] = entry; + } + if (value_object.size() > 0) val = Value(value_object); + } break; + case json::value_t::array: { + Value::Array value_array; + for (auto it = o.begin(); it != o.end(); it++) { + Value entry; + ParseJsonAsValue(&entry, it.value()); + if (entry.Type() != NULL_TYPE) value_array.push_back(entry); + } + if (value_array.size() > 0) val = Value(value_array); + } break; + case json::value_t::string: + val = Value(o.get()); + break; + case json::value_t::boolean: + val = Value(o.get()); + break; + case json::value_t::number_integer: + case json::value_t::number_unsigned: + val = Value(static_cast(o.get())); + break; + case json::value_t::number_float: + val = Value(o.get()); + break; + case json::value_t::null: + case json::value_t::discarded: + // default: + break; } + if (ret) *ret = val; - (*ret) = tinygltf::Value(vo); + return val.Type() != NULL_TYPE; } static bool ParseExtrasProperty(Value *ret, const json &o) { @@ -1322,18 +2057,10 @@ static bool ParseExtrasProperty(Value *ret, const json &o) { return false; } - // FIXME(syoyo) Currently we only support `object` type for extras property. - if (!it.value().is_object()) { - return false; - } - - ParseObjectProperty(ret, it.value()); - - return true; + return ParseJsonAsValue(ret, it.value()); } -static bool ParseBooleanProperty(bool *ret, std::string *err, - const json &o, +static bool ParseBooleanProperty(bool *ret, std::string *err, const json &o, const std::string &property, const bool required, const std::string &parent_node = "") { @@ -1367,8 +2094,7 @@ static bool ParseBooleanProperty(bool *ret, std::string *err, return true; } -static bool ParseNumberProperty(double *ret, std::string *err, - const json &o, +static bool ParseNumberProperty(double *ret, std::string *err, const json &o, const std::string &property, const bool required, const std::string &parent_node = "") { @@ -1403,8 +2129,8 @@ static bool ParseNumberProperty(double *ret, std::string *err, } static bool ParseNumberArrayProperty(std::vector *ret, std::string *err, - const json &o, - const std::string &property, bool required, + const json &o, const std::string &property, + bool required, const std::string &parent_node = "") { json::const_iterator it = o.find(property); if (it == o.end()) { @@ -1434,7 +2160,8 @@ static bool ParseNumberArrayProperty(std::vector *ret, std::string *err, } ret->clear(); - for (json::const_iterator i = it.value().begin(); i != it.value().end(); i++) { + for (json::const_iterator i = it.value().begin(); i != it.value().end(); + i++) { if (!i.value().is_number()) { if (required) { if (err) { @@ -1482,7 +2209,7 @@ static bool ParseStringProperty( } if (ret) { - (*ret) = it.value(); + (*ret) = it.value().get(); } return true; @@ -1490,13 +2217,15 @@ static bool ParseStringProperty( static bool ParseStringIntProperty(std::map *ret, std::string *err, const json &o, - const std::string &property, bool required, const std::string &parent = "") { + const std::string &property, bool required, + const std::string &parent = "") { json::const_iterator it = o.find(property); if (it == o.end()) { if (required) { if (err) { if (!parent.empty()) { - (*err) += "'" + property + "' property is missing in " + parent + ".\n"; + (*err) += + "'" + property + "' property is missing in " + parent + ".\n"; } else { (*err) += "'" + property + "' property is missing.\n"; } @@ -1565,126 +2294,200 @@ static bool ParseJSONProperty(std::map *ret, json::const_iterator itEnd(obj.end()); for (; it2 != itEnd; it2++) { if (it2.value().is_number()) - ret->insert(std::pair(it2.key(), - it2.value())); + ret->insert(std::pair(it2.key(), it2.value())); + } + + return true; +} + +static bool ParseParameterProperty(Parameter *param, std::string *err, + const json &o, const std::string &prop, + bool required) { + // A parameter value can either be a string or an array of either a boolean or + // a number. Booleans of any kind aren't supported here. Granted, it + // complicates the Parameter structure and breaks it semantically in the sense + // that the client probably works off the assumption that if the string is + // empty the vector is used, etc. Would a tagged union work? + if (ParseStringProperty(¶m->string_value, err, o, prop, false)) { + // Found string property. + return true; + } else if (ParseNumberArrayProperty(¶m->number_array, err, o, prop, + false)) { + // Found a number array. + return true; + } else if (ParseNumberProperty(¶m->number_value, err, o, prop, false)) { + return param->has_number_value = true; + } else if (ParseJSONProperty(¶m->json_double_value, err, o, prop, + false)) { + return true; + } else if (ParseBooleanProperty(¶m->bool_value, err, o, prop, false)) { + return true; + } else { + if (required) { + if (err) { + (*err) += "parameter must be a string or number / number array.\n"; + } + } + return false; } +} +static bool ParseExtensionsProperty(ExtensionMap *ret, std::string *err, + const json &o) { + (void)err; + + json::const_iterator it = o.find("extensions"); + if (it == o.end()) { + return false; + } + if (!it.value().is_object()) { + return false; + } + ExtensionMap extensions; + json::const_iterator extIt = it.value().begin(); + for (; extIt != it.value().end(); extIt++) { + if (!extIt.value().is_object()) continue; + if (!ParseJsonAsValue(&extensions[extIt.key()], extIt.value())) { + if (!extIt.key().empty()) { + // create empty object so that an extension object is still of type object + extensions[extIt.key()] = Value{ Value::Object{} }; + } + } + } + if (ret) { + (*ret) = extensions; + } return true; } -static bool ParseAsset(Asset *asset, std::string *err, - const json &o) { +static bool ParseAsset(Asset *asset, std::string *err, const json &o) { ParseStringProperty(&asset->version, err, o, "version", true, "Asset"); ParseStringProperty(&asset->generator, err, o, "generator", false, "Asset"); ParseStringProperty(&asset->minVersion, err, o, "minVersion", false, "Asset"); + ParseExtensionsProperty(&asset->extensions, err, o); + // Unity exporter version is added as extra here ParseExtrasProperty(&(asset->extras), o); return true; } -static bool ParseImage(Image *image, std::string *err, +static bool ParseImage(Image *image, std::string *err, std::string *warn, const json &o, const std::string &basedir, - bool is_binary, const unsigned char *bin_data, - size_t bin_size) { + FsCallbacks *fs, + LoadImageDataFunction *LoadImageData = nullptr, + void *load_image_user_data = nullptr) { // A glTF image must either reference a bufferView or an image uri - double bufferView = -1; - bool isEmbedded = - ParseNumberProperty(&bufferView, err, o, "bufferView", false); - std::string uri; - std::string tmp_err; - if (!ParseStringProperty(&uri, &tmp_err, o, "uri", false) && !isEmbedded) { + // schema says oneOf [`bufferView`, `uri`] + // TODO(syoyo): Check the type of each parameters. + bool hasBufferView = (o.find("bufferView") != o.end()); + bool hasURI = (o.find("uri") != o.end()); + + if (hasBufferView && hasURI) { + // Should not both defined. if (err) { - (*err) += "`bufferView` or `uri` required for Image.\n"; + (*err) += + "Only one of `bufferView` or `uri` should be defined, but both are " + "defined for Image.\n"; } return false; } - ParseStringProperty(&image->name, err, o, "name", false); + if (!hasBufferView && !hasURI) { + if (err) { + (*err) += "Neither required `bufferView` nor `uri` defined for Image.\n"; + } + return false; + } - std::vector img; + ParseStringProperty(&image->name, err, o, "name", false); + ParseExtensionsProperty(&image->extensions, err, o); + ParseExtrasProperty(&image->extras, o); - if (is_binary) { - // Still binary glTF accepts external dataURI. First try external resources. - bool loaded = false; - if (IsDataURI(uri)) { - loaded = DecodeDataURI(&img, uri, 0, false); - } else { - // Assume external .bin file. - loaded = LoadExternalFile(&img, err, uri, basedir, 0, false); + if (hasBufferView) { + double bufferView = -1; + if (!ParseNumberProperty(&bufferView, err, o, "bufferView", true)) { + if (err) { + (*err) += "Failed to parse `bufferView` for Image.\n"; + } + return false; } - if (!loaded) { - // load data from (embedded) binary data + std::string mime_type; + ParseStringProperty(&mime_type, err, o, "mimeType", false); - if ((bin_size == 0) || (bin_data == nullptr)) { - if (err) { - (*err) += "Invalid binary data.\n"; - } - return false; - } + double width = 0.0; + ParseNumberProperty(&width, err, o, "width", false); - double buffer_view = -1.0; - if (!ParseNumberProperty(&buffer_view, err, o, "bufferView", true, "Image")) { - return false; - } + double height = 0.0; + ParseNumberProperty(&height, err, o, "height", false); - std::string mime_type; - ParseStringProperty(&mime_type, err, o, "mimeType", false); + // Just only save some information here. Loading actual image data from + // bufferView is done after this `ParseImage` function. + image->bufferView = static_cast(bufferView); + image->mimeType = mime_type; + image->width = static_cast(width); + image->height = static_cast(height); - double width = 0.0; - ParseNumberProperty(&width, err, o, "width", false); + return true; + } - double height = 0.0; - ParseNumberProperty(&height, err, o, "height", false); + // Parse URI & Load image data. + + std::string uri; + std::string tmp_err; + if (!ParseStringProperty(&uri, &tmp_err, o, "uri", true)) { + if (err) { + (*err) += "Failed to parse `uri` for Image.\n"; + } + return false; + } - // Just only save some information here. Loading actual image data from - // bufferView is done in other place. - image->bufferView = static_cast(buffer_view); - image->mimeType = mime_type; - image->width = static_cast(width); - image->height = static_cast(height); + std::vector img; - return true; + if (IsDataURI(uri)) { + if (!DecodeDataURI(&img, image->mimeType, uri, 0, false)) { + if (err) { + (*err) += "Failed to decode 'uri' for image parameter.\n"; + } + return false; } } else { - if (IsDataURI(uri)) { - if (!DecodeDataURI(&img, uri, 0, false)) { - if (err) { - (*err) += "Failed to decode 'uri' for image parameter.\n"; - } - return false; + // Assume external file + // Keep texture path (for textures that cannot be decoded) + image->uri = uri; +#ifdef TINYGLTF_NO_EXTERNAL_IMAGE + return true; +#endif + if (!LoadExternalFile(&img, err, warn, uri, basedir, false, 0, false, fs)) { + if (warn) { + (*warn) += "Failed to load external 'uri' for image parameter\n"; } - } else { - // Assume external file - - // Keep texture path (for textures that cannot be decoded) - image->uri = uri; + // If the image cannot be loaded, keep uri as image->uri. + return true; + } - if (!LoadExternalFile(&img, err, uri, basedir, 0, false)) { - if (err) { - (*err) += "Failed to load external 'uri' for image parameter\n"; - } - // If the image cannot be loaded, keep uri as image->uri. - return true; - } - if (img.empty()) { - if (err) { - (*err) += "Image is empty.\n"; - } - return false; + if (img.empty()) { + if (warn) { + (*warn) += "Image is empty.\n"; } + return false; } } - return LoadImageData(image, err, 0, 0, &img.at(0), - static_cast(img.size())); + if (*LoadImageData == nullptr) { + if (err) { + (*err) += "No LoadImageData callback specified.\n"; + } + return false; + } + return (*LoadImageData)(image, err, warn, 0, 0, &img.at(0), + static_cast(img.size()), load_image_user_data); } -static bool ParseTexture(Texture *texture, std::string *err, - const json &o, +static bool ParseTexture(Texture *texture, std::string *err, const json &o, const std::string &basedir) { (void)basedir; double sampler = -1.0; @@ -1696,11 +2499,16 @@ static bool ParseTexture(Texture *texture, std::string *err, texture->sampler = static_cast(sampler); texture->source = static_cast(source); + ParseExtensionsProperty(&texture->extensions, err, o); + ParseExtrasProperty(&texture->extras, o); + + ParseStringProperty(&texture->name, err, o, "name", false); + return true; } -static bool ParseBuffer(Buffer *buffer, std::string *err, - const json &o, const std::string &basedir, +static bool ParseBuffer(Buffer *buffer, std::string *err, const json &o, + FsCallbacks *fs, const std::string &basedir, bool is_binary = false, const unsigned char *bin_data = nullptr, size_t bin_size = 0) { @@ -1710,11 +2518,11 @@ static bool ParseBuffer(Buffer *buffer, std::string *err, } // In glTF 2.0, uri is not mandatory anymore - std::string uri; - ParseStringProperty(&uri, err, o, "uri", false, "Buffer"); + buffer->uri.clear(); + ParseStringProperty(&buffer->uri, err, o, "uri", false, "Buffer"); // having an empty uri for a non embedded image should not be valid - if (!is_binary && uri.empty()) { + if (!is_binary && buffer->uri.empty()) { if (err) { (*err) += "'uri' is missing from non binary glTF file buffer.\n"; } @@ -1732,11 +2540,26 @@ static bool ParseBuffer(Buffer *buffer, std::string *err, size_t bytes = static_cast(byteLength); if (is_binary) { - // Still binary glTF accepts external dataURI. First try external resources. - - if (!uri.empty()) { - // External .bin file. - LoadExternalFile(&buffer->data, err, uri, basedir, bytes, true); + // Still binary glTF accepts external dataURI. + if (!buffer->uri.empty()) { + // First try embedded data URI. + if (IsDataURI(buffer->uri)) { + std::string mime_type; + if (!DecodeDataURI(&buffer->data, mime_type, buffer->uri, bytes, + true)) { + if (err) { + (*err) += + "Failed to decode 'uri' : " + buffer->uri + " in Buffer\n"; + } + return false; + } + } else { + // External .bin file. + if (!LoadExternalFile(&buffer->data, err, /* warn */ nullptr, + buffer->uri, basedir, true, bytes, true, fs)) { + return false; + } + } } else { // load data from (embedded) binary data @@ -1764,16 +2587,18 @@ static bool ParseBuffer(Buffer *buffer, std::string *err, } } else { - if (IsDataURI(uri)) { - if (!DecodeDataURI(&buffer->data, uri, bytes, true)) { + if (IsDataURI(buffer->uri)) { + std::string mime_type; + if (!DecodeDataURI(&buffer->data, mime_type, buffer->uri, bytes, true)) { if (err) { - (*err) += "Failed to decode 'uri' : " + uri + " in Buffer\n"; + (*err) += "Failed to decode 'uri' : " + buffer->uri + " in Buffer\n"; } return false; } } else { // Assume external .bin file. - if (!LoadExternalFile(&buffer->data, err, uri, basedir, bytes, true)) { + if (!LoadExternalFile(&buffer->data, err, /* warn */ nullptr, buffer->uri, + basedir, true, bytes, true, fs)) { return false; } } @@ -1846,8 +2671,7 @@ static bool ParseBufferView(BufferView *bufferView, std::string *err, return true; } -static bool ParseAccessor(Accessor *accessor, std::string *err, - const json &o) { +static bool ParseAccessor(Accessor *accessor, std::string *err, const json &o) { double bufferView = -1.0; if (!ParseNumberProperty(&bufferView, err, o, "bufferView", true, "Accessor")) { @@ -1956,9 +2780,9 @@ static bool ParsePrimitive(Primitive *primitive, std::string *err, // Look for morph targets json::const_iterator targetsObject = o.find("targets"); - if ((targetsObject != o.end()) && - targetsObject.value().is_array()) { - for (json::const_iterator i = targetsObject.value().begin(); i != targetsObject.value().end(); i++) { + if ((targetsObject != o.end()) && targetsObject.value().is_array()) { + for (json::const_iterator i = targetsObject.value().begin(); + i != targetsObject.value().end(); i++) { std::map targetAttribues; const json &dict = i.value(); @@ -1966,8 +2790,7 @@ static bool ParsePrimitive(Primitive *primitive, std::string *err, json::const_iterator dictItEnd(dict.end()); for (; dictIt != dictItEnd; ++dictIt) { - targetAttribues[dictIt.key()] = - static_cast(dictIt.value()); + targetAttribues[dictIt.key()] = static_cast(dictIt.value()); } primitive->targets.push_back(targetAttribues); } @@ -1984,10 +2807,10 @@ static bool ParseMesh(Mesh *mesh, std::string *err, const json &o) { mesh->primitives.clear(); json::const_iterator primObject = o.find("primitives"); if ((primObject != o.end()) && primObject.value().is_array()) { - for (json::const_iterator i = primObject.value().begin(); i != primObject.value().end(); i++) { + for (json::const_iterator i = primObject.value().begin(); + i != primObject.value().end(); i++) { Primitive primitive; - if (ParsePrimitive(&primitive, err, - i.value())) { + if (ParsePrimitive(&primitive, err, i.value())) { // Only add the primitive if the parsing succeeds. mesh->primitives.push_back(primitive); } @@ -1996,9 +2819,9 @@ static bool ParseMesh(Mesh *mesh, std::string *err, const json &o) { // Look for morph targets json::const_iterator targetsObject = o.find("targets"); - if ((targetsObject != o.end()) && - targetsObject.value().is_array()) { - for (json::const_iterator i = targetsObject.value().begin(); i != targetsObject.value().end(); i++) { + if ((targetsObject != o.end()) && targetsObject.value().is_array()) { + for (json::const_iterator i = targetsObject.value().begin(); + i != targetsObject.value().end(); i++) { std::map targetAttribues; const json &dict = i.value(); @@ -2006,8 +2829,7 @@ static bool ParseMesh(Mesh *mesh, std::string *err, const json &o) { json::const_iterator dictItEnd(dict.end()); for (; dictIt != dictItEnd; ++dictIt) { - targetAttribues[dictIt.key()] = - static_cast(dictIt.value()); + targetAttribues[dictIt.key()] = static_cast(dictIt.value()); } mesh->targets.push_back(targetAttribues); } @@ -2016,46 +2838,12 @@ static bool ParseMesh(Mesh *mesh, std::string *err, const json &o) { // Should probably check if has targets and if dimensions fit ParseNumberArrayProperty(&mesh->weights, err, o, "weights", false); + ParseExtensionsProperty(&mesh->extensions, err, o); ParseExtrasProperty(&(mesh->extras), o); return true; } -static bool ParseParameterProperty(Parameter *param, std::string *err, - const json &o, - const std::string &prop, bool required) { - double num_val; - - // A parameter value can either be a string or an array of either a boolean or - // a number. Booleans of any kind aren't supported here. Granted, it - // complicates the Parameter structure and breaks it semantically in the sense - // that the client probably works off the assumption that if the string is - // empty the vector is used, etc. Would a tagged union work? - if (ParseStringProperty(¶m->string_value, err, o, prop, false)) { - // Found string property. - return true; - } else if (ParseNumberArrayProperty(¶m->number_array, err, o, prop, - false)) { - // Found a number array. - return true; - } else if (ParseNumberProperty(&num_val, err, o, prop, false)) { - param->number_array.push_back(num_val); - return true; - } else if (ParseJSONProperty(¶m->json_double_value, err, o, prop, - false)) { - return true; - } else if (ParseBooleanProperty(¶m->bool_value, err, o, prop, false)) { - return true; - } else { - if (required) { - if (err) { - (*err) += "parameter must be a string or number / number array.\n"; - } - } - return false; - } -} - static bool ParseLight(Light *light, std::string *err, const json &o) { ParseStringProperty(&light->name, err, o, "name", false); ParseNumberArrayProperty(&light->color, err, o, "color", false); @@ -2087,59 +2875,29 @@ static bool ParseNode(Node *node, std::string *err, const json &o) { node->children.clear(); json::const_iterator childrenObject = o.find("children"); - if ((childrenObject != o.end()) && - childrenObject.value().is_array()) { - for (json::const_iterator i = childrenObject.value().begin(); i != childrenObject.value().end(); i++) { + if ((childrenObject != o.end()) && childrenObject.value().is_array()) { + for (json::const_iterator i = childrenObject.value().begin(); + i != childrenObject.value().end(); i++) { if (!i.value().is_number()) { if (err) { (*err) += "Invalid `children` array.\n"; } return false; } - const int &childrenNode = - static_cast(i.value()); + const int &childrenNode = static_cast(i.value()); node->children.push_back(childrenNode); } } + ParseExtensionsProperty(&node->extensions, err, o); ParseExtrasProperty(&(node->extras), o); - json::const_iterator extensions_object = o.find("extensions"); - if ((extensions_object != o.end()) && - extensions_object.value().is_object()) { - const json &ext_values_object = - extensions_object.value(); - - json::const_iterator it(ext_values_object.begin()); - json::const_iterator itEnd(ext_values_object.end()); - - for (; it != itEnd; it++) { - if ((it.key().compare("KHR_lights_cmn") == 0) && - it.value().is_object()) { - const json &light_values_object = - it.value(); - - json::const_iterator itVal(light_values_object.begin()); - json::const_iterator itValEnd(light_values_object.end()); - - for (; itVal != itValEnd; itVal++) { - Parameter param; - if (ParseParameterProperty(¶m, err, light_values_object, itVal.key(), - false)) { - node->extLightsValues[itVal.key()] = param; - } - } - } - } - } - return true; } -static bool ParseMaterial(Material *material, std::string *err, - const json &o) { +static bool ParseMaterial(Material *material, std::string *err, const json &o) { material->values.clear(); - material->extPBRValues.clear(); + material->extensions.clear(); material->additionalValues.clear(); json::const_iterator it(o.begin()); @@ -2148,8 +2906,7 @@ static bool ParseMaterial(Material *material, std::string *err, for (; it != itEnd; it++) { if (it.key() == "pbrMetallicRoughness") { if (it.value().is_object()) { - const json &values_object = - it.value(); + const json &values_object = it.value(); json::const_iterator itVal(values_object.begin()); json::const_iterator itValEnd(values_object.end()); @@ -2162,29 +2919,11 @@ static bool ParseMaterial(Material *material, std::string *err, } } } - } else if (it.key() == "extensions") { - if (it.value().is_object()) { - const json &extension = - it.value(); - - json::const_iterator extIt = extension.begin(); - if (!extIt.value().is_object()) continue; - - const json &values_object = - extIt.value(); - - json::const_iterator itVal(values_object.begin()); - json::const_iterator itValEnd(values_object.end()); - - for (; itVal != itValEnd; itVal++) { - Parameter param; - if (ParseParameterProperty(¶m, err, values_object, itVal.key(), - false)) { - material->extPBRValues[itVal.key()] = param; - } - } - } - } else { + } else if (it.key() == "extensions" || it.key() == "extras") { + // done later, skip, otherwise poorly parsed contents will be saved in the + // parametermap and serialized again later + } + else { Parameter param; if (ParseParameterProperty(¶m, err, o, it.key(), false)) { material->additionalValues[it.key()] = param; @@ -2192,6 +2931,7 @@ static bool ParseMaterial(Material *material, std::string *err, } } + ParseExtensionsProperty(&material->extensions, err, o); ParseExtrasProperty(&(material->extras), o); return true; @@ -2201,7 +2941,8 @@ static bool ParseAnimationChannel(AnimationChannel *channel, std::string *err, const json &o) { double samplerIndex = -1.0; double targetIndex = -1.0; - if (!ParseNumberProperty(&samplerIndex, err, o, "sampler", true, "AnimationChannel")) { + if (!ParseNumberProperty(&samplerIndex, err, o, "sampler", true, + "AnimationChannel")) { if (err) { (*err) += "`sampler` field is missing in animation channels\n"; } @@ -2210,8 +2951,7 @@ static bool ParseAnimationChannel(AnimationChannel *channel, std::string *err, json::const_iterator targetIt = o.find("target"); if ((targetIt != o.end()) && targetIt.value().is_object()) { - const json &target_object = - targetIt.value(); + const json &target_object = targetIt.value(); if (!ParseNumberProperty(&targetIndex, err, target_object, "node", true)) { if (err) { @@ -2242,10 +2982,10 @@ static bool ParseAnimation(Animation *animation, std::string *err, { json::const_iterator channelsIt = o.find("channels"); if ((channelsIt != o.end()) && channelsIt.value().is_array()) { - for (json::const_iterator i = channelsIt.value().begin(); i != channelsIt.value().end(); i++) { + for (json::const_iterator i = channelsIt.value().begin(); + i != channelsIt.value().end(); i++) { AnimationChannel channel; - if (ParseAnimationChannel(&channel, err, - i.value())) { + if (ParseAnimationChannel(&channel, err, i.value())) { // Only add the channel if the parsing succeeds. animation->channels.push_back(channel); } @@ -2256,8 +2996,7 @@ static bool ParseAnimation(Animation *animation, std::string *err, { json::const_iterator samplerIt = o.find("samplers"); if ((samplerIt != o.end()) && samplerIt.value().is_array()) { - const json &sampler_array = - samplerIt.value(); + const json &sampler_array = samplerIt.value(); json::const_iterator it = sampler_array.begin(); json::const_iterator itEnd = sampler_array.end(); @@ -2289,6 +3028,7 @@ static bool ParseAnimation(Animation *animation, std::string *err, } sampler.input = static_cast(inputIndex); sampler.output = static_cast(outputIndex); + ParseExtrasProperty(&(sampler.extras), s); animation->samplers.push_back(sampler); } } @@ -2301,8 +3041,7 @@ static bool ParseAnimation(Animation *animation, std::string *err, return true; } -static bool ParseSampler(Sampler *sampler, std::string *err, - const json &o) { +static bool ParseSampler(Sampler *sampler, std::string *err, const json &o) { ParseStringProperty(&sampler->name, err, o, "name", false); double minFilter = @@ -2369,11 +3108,12 @@ static bool ParsePerspectiveCamera(PerspectiveCamera *camera, std::string *err, double zfar = 0.0; // = invalid ParseNumberProperty(&zfar, err, o, "zfar", false, "PerspectiveCamera"); - camera->aspectRatio = float(aspectRatio); - camera->zfar = float(zfar); - camera->yfov = float(yfov); - camera->znear = float(znear); + camera->aspectRatio = aspectRatio; + camera->zfar = zfar; + camera->yfov = yfov; + camera->znear = znear; + ParseExtensionsProperty(&camera->extensions, err, o); ParseExtrasProperty(&(camera->extras), o); // TODO(syoyo): Validate parameter values. @@ -2382,8 +3122,7 @@ static bool ParsePerspectiveCamera(PerspectiveCamera *camera, std::string *err, } static bool ParseOrthographicCamera(OrthographicCamera *camera, - std::string *err, - const json &o) { + std::string *err, const json &o) { double xmag = 0.0; if (!ParseNumberProperty(&xmag, err, o, "xmag", true, "OrthographicCamera")) { return false; @@ -2405,20 +3144,20 @@ static bool ParseOrthographicCamera(OrthographicCamera *camera, return false; } + ParseExtensionsProperty(&camera->extensions, err, o); ParseExtrasProperty(&(camera->extras), o); - camera->xmag = float(xmag); - camera->ymag = float(ymag); - camera->zfar = float(zfar); - camera->znear = float(znear); + camera->xmag = xmag; + camera->ymag = ymag; + camera->zfar = zfar; + camera->znear = znear; // TODO(syoyo): Validate parameter values. return true; } -static bool ParseCamera(Camera *camera, std::string *err, - const json &o) { +static bool ParseCamera(Camera *camera, std::string *err, const json &o) { if (!ParseStringProperty(&camera->type, err, o, "type", true, "Camera")) { return false; } @@ -2443,8 +3182,7 @@ static bool ParseCamera(Camera *camera, std::string *err, return false; } - if (!ParseOrthographicCamera(&camera->orthographic, err, - v.get())) { + if (!ParseOrthographicCamera(&camera->orthographic, err, v.get())) { return false; } } else if (camera->type.compare("perspective") == 0) { @@ -2467,8 +3205,7 @@ static bool ParseCamera(Camera *camera, std::string *err, return false; } - if (!ParsePerspectiveCamera(&camera->perspective, err, - v.get())) { + if (!ParsePerspectiveCamera(&camera->perspective, err, v.get())) { return false; } } else { @@ -2483,13 +3220,15 @@ static bool ParseCamera(Camera *camera, std::string *err, ParseStringProperty(&camera->name, err, o, "name", false); + ParseExtensionsProperty(&camera->extensions, err, o); ParseExtrasProperty(&(camera->extras), o); return true; } -bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, - unsigned int length, const std::string &base_dir, +bool TinyGLTF::LoadFromString(Model *model, std::string *err, std::string *warn, + const char *str, unsigned int length, + const std::string &base_dir, unsigned int check_sections) { if (length < 4) { if (err) { @@ -2500,7 +3239,9 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, json v; -#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && not defined(TINYGLTF_NOEXCEPTION) +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || \ + defined(_CPPUNWIND)) && \ + not defined(TINYGLTF_NOEXCEPTION) try { v = json::parse(str, str + length); @@ -2512,7 +3253,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, } #else { - v = json::parse(str, str + length, nullptr, /* exception */false); + v = json::parse(str, str + length, nullptr, /* exception */ false); if (!v.is_object()) { // Assume parsing was failed. @@ -2536,7 +3277,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, // FIXME Maybe a better way to handle it than removing the code { - json::const_iterator it = v.find("scenes"); + json::const_iterator it = v.find("scenes"); if ((it != v.end()) && it.value().is_array()) { // OK } else if (check_sections & REQUIRE_SCENES) { @@ -2548,7 +3289,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, } { - json::const_iterator it = v.find("nodes"); + json::const_iterator it = v.find("nodes"); if ((it != v.end()) && it.value().is_array()) { // OK } else if (check_sections & REQUIRE_NODES) { @@ -2560,7 +3301,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, } { - json::const_iterator it = v.find("accessors"); + json::const_iterator it = v.find("accessors"); if ((it != v.end()) && it.value().is_array()) { // OK } else if (check_sections & REQUIRE_ACCESSORS) { @@ -2572,7 +3313,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, } { - json::const_iterator it = v.find("buffers"); + json::const_iterator it = v.find("buffers"); if ((it != v.end()) && it.value().is_array()) { // OK } else if (check_sections & REQUIRE_BUFFERS) { @@ -2584,7 +3325,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, } { - json::const_iterator it = v.find("bufferViews"); + json::const_iterator it = v.find("bufferViews"); if ((it != v.end()) && it.value().is_array()) { // OK } else if (check_sections & REQUIRE_BUFFER_VIEWS) { @@ -2603,11 +3344,12 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, model->nodes.clear(); model->extensionsUsed.clear(); model->extensionsRequired.clear(); + model->extensions.clear(); model->defaultScene = -1; // 1. Parse Asset { - json::const_iterator it = v.find("asset"); + json::const_iterator it = v.find("asset"); if ((it != v.end()) && it.value().is_object()) { const json &root = it.value(); @@ -2617,7 +3359,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, // 2. Parse extensionUsed { - json::const_iterator it = v.find("extensionsUsed"); + json::const_iterator it = v.find("extensionsUsed"); if ((it != v.end()) && it.value().is_array()) { const json &root = it.value(); for (unsigned int i = 0; i < root.size(); ++i) { @@ -2627,7 +3369,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, } { - json::const_iterator it = v.find("extensionsRequired"); + json::const_iterator it = v.find("extensionsRequired"); if ((it != v.end()) && it.value().is_array()) { const json &root = it.value(); for (unsigned int i = 0; i < root.size(); ++i) { @@ -2638,7 +3380,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, // 3. Parse Buffer { - json::const_iterator rootIt = v.find("buffers"); + json::const_iterator rootIt = v.find("buffers"); if ((rootIt != v.end()) && rootIt.value().is_array()) { const json &root = rootIt.value(); @@ -2652,7 +3394,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, return false; } Buffer buffer; - if (!ParseBuffer(&buffer, err, it->get(), base_dir, + if (!ParseBuffer(&buffer, err, it->get(), &fs, base_dir, is_binary_, bin_data_, bin_size_)) { return false; } @@ -2661,10 +3403,10 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, } } } - + // 4. Parse BufferView { - json::const_iterator rootIt = v.find("bufferViews"); + json::const_iterator rootIt = v.find("bufferViews"); if ((rootIt != v.end()) && rootIt.value().is_array()) { const json &root = rootIt.value(); @@ -2689,7 +3431,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, // 5. Parse Accessor { - json::const_iterator rootIt = v.find("accessors"); + json::const_iterator rootIt = v.find("accessors"); if ((rootIt != v.end()) && rootIt.value().is_array()) { const json &root = rootIt.value(); @@ -2714,7 +3456,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, // 6. Parse Mesh { - json::const_iterator rootIt = v.find("meshes"); + json::const_iterator rootIt = v.find("meshes"); if ((rootIt != v.end()) && rootIt.value().is_array()) { const json &root = rootIt.value(); @@ -2739,7 +3481,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, // 7. Parse Node { - json::const_iterator rootIt = v.find("nodes"); + json::const_iterator rootIt = v.find("nodes"); if ((rootIt != v.end()) && rootIt.value().is_array()) { const json &root = rootIt.value(); @@ -2764,11 +3506,10 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, // 8. Parse scenes. { - json::const_iterator rootIt = v.find("scenes"); + json::const_iterator rootIt = v.find("scenes"); if ((rootIt != v.end()) && rootIt.value().is_array()) { const json &root = rootIt.value(); - json::const_iterator it(root.begin()); json::const_iterator itEnd(root.end()); for (; it != itEnd; it++) { @@ -2792,6 +3533,9 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, } scene.nodes = nodesIds; + ParseExtensionsProperty(&scene.extensions, err, o); + ParseExtrasProperty(&scene.extras, o); + model->scenes.push_back(scene); } } @@ -2799,8 +3543,8 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, // 9. Parse default scenes. { - json::const_iterator rootIt = v.find("scene"); - if ((rootIt != v.end()) && rootIt.value().is_number_integer()) { + json::const_iterator rootIt = v.find("scene"); + if ((rootIt != v.end()) && rootIt.value().is_number()) { const int defaultScene = rootIt.value(); model->defaultScene = static_cast(defaultScene); @@ -2809,7 +3553,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, // 10. Parse Material { - json::const_iterator rootIt = v.find("materials"); + json::const_iterator rootIt = v.find("materials"); if ((rootIt != v.end()) && rootIt.value().is_array()) { const json &root = rootIt.value(); @@ -2838,7 +3582,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, // 11. Parse Image { - json::const_iterator rootIt = v.find("images"); + json::const_iterator rootIt = v.find("images"); if ((rootIt != v.end()) && rootIt.value().is_array()) { const json &root = rootIt.value(); @@ -2852,8 +3596,8 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, return false; } Image image; - if (!ParseImage(&image, err, it.value(), base_dir, - is_binary_, bin_data_, bin_size_)) { + if (!ParseImage(&image, err, warn, it.value(), base_dir, &fs, + &this->LoadImageData, load_image_user_data_)) { return false; } @@ -2873,9 +3617,16 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, model->bufferViews[size_t(image.bufferView)]; const Buffer &buffer = model->buffers[size_t(bufferView.buffer)]; - bool ret = LoadImageData(&image, err, image.width, image.height, + if (*LoadImageData == nullptr) { + if (err) { + (*err) += "No LoadImageData callback specified.\n"; + } + return false; + } + bool ret = LoadImageData(&image, err, warn, image.width, image.height, &buffer.data[bufferView.byteOffset], - static_cast(bufferView.byteLength)); + static_cast(bufferView.byteLength), + load_image_user_data_); if (!ret) { return false; } @@ -2888,7 +3639,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, // 12. Parse Texture { - json::const_iterator rootIt = v.find("textures"); + json::const_iterator rootIt = v.find("textures"); if ((rootIt != v.end()) && rootIt.value().is_array()) { const json &root = rootIt.value(); @@ -2913,7 +3664,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, // 13. Parse Animation { - json::const_iterator rootIt = v.find("animations"); + json::const_iterator rootIt = v.find("animations"); if ((rootIt != v.end()) && rootIt.value().is_array()) { const json &root = rootIt.value(); @@ -2938,7 +3689,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, // 14. Parse Skin { - json::const_iterator rootIt = v.find("skins"); + json::const_iterator rootIt = v.find("skins"); if ((rootIt != v.end()) && rootIt.value().is_array()) { const json &root = rootIt.value(); @@ -2963,7 +3714,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, // 15. Parse Sampler { - json::const_iterator rootIt = v.find("samplers"); + json::const_iterator rootIt = v.find("samplers"); if ((rootIt != v.end()) && rootIt.value().is_array()) { const json &root = rootIt.value(); @@ -2988,7 +3739,7 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, // 16. Parse Camera { - json::const_iterator rootIt = v.find("cameras"); + json::const_iterator rootIt = v.find("cameras"); if ((rootIt != v.end()) && rootIt.value().is_array()) { const json &root = rootIt.value(); @@ -3012,8 +3763,11 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, } // 17. Parse Extensions + ParseExtensionsProperty(&model->extensions, err, v); + + // 18. Specific extension implementations { - json::const_iterator rootIt = v.find("extensions"); + json::const_iterator rootIt = v.find("extensions"); if ((rootIt != v.end()) && rootIt.value().is_object()) { const json &root = rootIt.value(); @@ -3021,7 +3775,8 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, json::const_iterator itEnd(root.end()); for (; it != itEnd; ++it) { // parse KHR_lights_cmn extension - if ((it.key().compare("KHR_lights_cmn") == 0) && it.value().is_object()) { + if ((it.key().compare("KHR_lights_cmn") == 0) && + it.value().is_object()) { const json &object = it.value(); json::const_iterator itLight(object.find("lights")); json::const_iterator itLightEnd(object.end()); @@ -3048,38 +3803,52 @@ bool TinyGLTF::LoadFromString(Model *model, std::string *err, const char *str, } } + // 19. Parse Extras + ParseExtrasProperty(&model->extras, v); + return true; } bool TinyGLTF::LoadASCIIFromString(Model *model, std::string *err, - const char *str, unsigned int length, + std::string *warn, const char *str, + unsigned int length, const std::string &base_dir, unsigned int check_sections) { is_binary_ = false; bin_data_ = nullptr; bin_size_ = 0; - return LoadFromString(model, err, str, length, base_dir, check_sections); + return LoadFromString(model, err, warn, str, length, base_dir, + check_sections); } bool TinyGLTF::LoadASCIIFromFile(Model *model, std::string *err, - const std::string &filename, + std::string *warn, const std::string &filename, unsigned int check_sections) { std::stringstream ss; - std::ifstream f(filename.c_str()); - if (!f) { - ss << "Failed to open file: " << filename << std::endl; + if (fs.ReadWholeFile == nullptr) { + // Programmer error, assert() ? + ss << "Failed to read file: " << filename + << ": one or more FS callback not set" << std::endl; if (err) { (*err) = ss.str(); } return false; } - f.seekg(0, f.end); - size_t sz = static_cast(f.tellg()); - std::vector buf(sz); + std::vector data; + std::string fileerr; + bool fileread = fs.ReadWholeFile(&data, &fileerr, filename, fs.user_data); + if (!fileread) { + ss << "Failed to read file: " << filename << ": " << fileerr << std::endl; + if (err) { + (*err) = ss.str(); + } + return false; + } + size_t sz = data.size(); if (sz == 0) { if (err) { (*err) = "Empty file."; @@ -3087,20 +3856,17 @@ bool TinyGLTF::LoadASCIIFromFile(Model *model, std::string *err, return false; } - f.seekg(0, f.beg); - f.read(&buf.at(0), static_cast(sz)); - f.close(); - std::string basedir = GetBaseDir(filename); - bool ret = LoadASCIIFromString(model, err, &buf.at(0), - static_cast(buf.size()), basedir, - check_sections); + bool ret = LoadASCIIFromString( + model, err, warn, reinterpret_cast(&data.at(0)), + static_cast(data.size()), basedir, check_sections); return ret; } bool TinyGLTF::LoadBinaryFromMemory(Model *model, std::string *err, + std::string *warn, const unsigned char *bytes, unsigned int size, const std::string &base_dir, @@ -3137,7 +3903,10 @@ bool TinyGLTF::LoadBinaryFromMemory(Model *model, std::string *err, memcpy(&model_format, bytes + 16, 4); swap4(&model_format); - if ((20 + model_length >= size) || (model_length < 1) || + // In case the Bin buffer is not present, the size is exactly 20 + size of + // JSON contents, + // so use "greater than" operator. + if ((20 + model_length > size) || (model_length < 1) || (model_format != 0x4E4F534A)) { // 0x4E4F534A = JSON format. if (err) { (*err) = "Invalid glTF binary."; @@ -3155,9 +3924,9 @@ bool TinyGLTF::LoadBinaryFromMemory(Model *model, std::string *err, bin_size_ = length - (20 + model_length); // extract header + JSON scene data. - bool ret = - LoadFromString(model, err, reinterpret_cast(&bytes[20]), - model_length, base_dir, check_sections); + bool ret = LoadFromString(model, err, warn, + reinterpret_cast(&bytes[20]), + model_length, base_dir, check_sections); if (!ret) { return ret; } @@ -3166,32 +3935,37 @@ bool TinyGLTF::LoadBinaryFromMemory(Model *model, std::string *err, } bool TinyGLTF::LoadBinaryFromFile(Model *model, std::string *err, + std::string *warn, const std::string &filename, unsigned int check_sections) { std::stringstream ss; - std::ifstream f(filename.c_str(), std::ios::binary); - if (!f) { - ss << "Failed to open file: " << filename << std::endl; + if (fs.ReadWholeFile == nullptr) { + // Programmer error, assert() ? + ss << "Failed to read file: " << filename + << ": one or more FS callback not set" << std::endl; if (err) { (*err) = ss.str(); } return false; } - f.seekg(0, f.end); - size_t sz = static_cast(f.tellg()); - std::vector buf(sz); - - f.seekg(0, f.beg); - f.read(&buf.at(0), static_cast(sz)); - f.close(); + std::vector data; + std::string fileerr; + bool fileread = fs.ReadWholeFile(&data, &fileerr, filename, fs.user_data); + if (!fileread) { + ss << "Failed to read file: " << filename << ": " << fileerr << std::endl; + if (err) { + (*err) = ss.str(); + } + return false; + } std::string basedir = GetBaseDir(filename); - bool ret = LoadBinaryFromMemory( - model, err, reinterpret_cast(&buf.at(0)), - static_cast(buf.size()), basedir, check_sections); + bool ret = LoadBinaryFromMemory(model, err, warn, &data.at(0), + static_cast(data.size()), + basedir, check_sections); return ret; } @@ -3200,14 +3974,15 @@ bool TinyGLTF::LoadBinaryFromFile(Model *model, std::string *err, // GLTF Serialization /////////////////////// -//typedef std::pair json_object_pair; +// typedef std::pair json_object_pair; template static void SerializeNumberProperty(const std::string &key, T number, json &obj) { - //obj.insert( + // obj.insert( // json_object_pair(key, json(static_cast(number)))); - obj[key] = static_cast(number); + // obj[key] = static_cast(number); + obj[key] = number; } template @@ -3218,15 +3993,15 @@ static void SerializeNumberArrayProperty(const std::string &key, json vals; for (unsigned int i = 0; i < value.size(); ++i) { - vals.push_back(static_cast(value[i])); + vals.push_back(static_cast(value[i])); + } + if (!vals.is_null()) { + obj[key] = vals; } - - obj[key] = vals; } static void SerializeStringProperty(const std::string &key, - const std::string &value, - json &obj) { + const std::string &value, json &obj) { obj[key] = value; } @@ -3243,36 +4018,73 @@ static void SerializeStringArrayProperty(const std::string &key, obj[key] = vals; } -static void SerializeValue(const std::string &key, const Value &value, - json &obj) { - if (value.IsArray()) { - json jsonValue; - for (unsigned int i = 0; i < value.ArrayLen(); ++i) { - Value elementValue = value.Get(int(i)); - if (elementValue.IsString()) - jsonValue.push_back(elementValue.Get()); - } - obj[key] = jsonValue; - } else { - json jsonValue; - std::vector valueKeys; - for (unsigned int i = 0; i < valueKeys.size(); ++i) { - Value elementValue = value.Get(valueKeys[i]); - if (elementValue.IsInt()) - jsonValue[valueKeys[i]] = - static_cast(elementValue.Get()); +static bool ValueToJson(const Value &value, json *ret) { + json obj; + switch (value.Type()) { + case NUMBER_TYPE: + obj = json(value.Get()); + break; + case INT_TYPE: + obj = json(value.Get()); + break; + case BOOL_TYPE: + obj = json(value.Get()); + break; + case STRING_TYPE: + obj = json(value.Get()); + break; + case ARRAY_TYPE: { + for (unsigned int i = 0; i < value.ArrayLen(); ++i) { + Value elementValue = value.Get(int(i)); + json elementJson; + if (ValueToJson(value.Get(int(i)), &elementJson)) + obj.push_back(elementJson); + } + break; } - - obj[key] = jsonValue; + case BINARY_TYPE: + // TODO + // obj = json(value.Get>()); + return false; + break; + case OBJECT_TYPE: { + Value::Object objMap = value.Get(); + for (auto &it : objMap) { + json elementJson; + if (ValueToJson(it.second, &elementJson)) obj[it.first] = elementJson; + } + break; + } + case NULL_TYPE: + default: + return false; } + if (ret) *ret = obj; + return true; +} + +static void SerializeValue(const std::string &key, const Value &value, + json &obj) { + json ret; + if (ValueToJson(value, &ret)) obj[key] = ret; } static void SerializeGltfBufferData(const std::vector &data, - const std::string &binFilePath) { - std::ofstream output(binFilePath.c_str(), std::ofstream::binary); + 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); +} + +static bool SerializeGltfBufferData(const std::vector &data, + const std::string &binFilename) { + std::ofstream output(binFilename.c_str(), std::ofstream::binary); + if(!output.is_open()) return false; output.write(reinterpret_cast(&data[0]), std::streamsize(data.size())); output.close(); + return true; } static void SerializeParameterMap(ParameterMap ¶m, json &o) { @@ -3283,22 +4095,50 @@ static void SerializeParameterMap(ParameterMap ¶m, json &o) { paramIt->second.number_array, o); } else if (paramIt->second.json_double_value.size()) { json json_double_value; - for (std::map::iterator it = paramIt->second.json_double_value.begin(); it != paramIt->second.json_double_value.end(); ++it) { - json_double_value[it->first] = it->second; + if (it->first == "index") { + json_double_value[it->first] = paramIt->second.TextureIndex(); + } else { + json_double_value[it->first] = it->second; + } } o[paramIt->first] = json_double_value; } else if (!paramIt->second.string_value.empty()) { SerializeStringProperty(paramIt->first, paramIt->second.string_value, o); + } else if (paramIt->second.has_number_value) { + o[paramIt->first] = paramIt->second.number_value; } else { o[paramIt->first] = paramIt->second.bool_value; } } } +static void SerializeExtensionMap(ExtensionMap &extensions, json &o) { + if (!extensions.size()) return; + + json extMap; + for (ExtensionMap::iterator extIt = extensions.begin(); + extIt != extensions.end(); ++extIt) { + json extension_values; + + // Allow an empty object for extension(#97) + json ret; + if (ValueToJson(extIt->second, &ret)) { + extMap[extIt->first] = ret; + } + if(ret.is_null()) { + if (!(extIt->first.empty())) { // name should not be empty, but for sure + // create empty object so that an extension name is still included in json. + extMap[extIt->first] = json({}); + } + } + } + o["extensions"] = extMap; +} + static void SerializeGltfAccessor(Accessor &accessor, json &o) { SerializeNumberProperty("bufferView", accessor.bufferView, o); @@ -3335,27 +4175,39 @@ static void SerializeGltfAccessor(Accessor &accessor, json &o) { } SerializeStringProperty("type", type, o); + if (!accessor.name.empty()) SerializeStringProperty("name", accessor.name, o); + + if (accessor.extras.Type() != NULL_TYPE) { + SerializeValue("extras", accessor.extras, o); + } } -static void SerializeGltfAnimationChannel(AnimationChannel &channel, - json &o) { +static void SerializeGltfAnimationChannel(AnimationChannel &channel, json &o) { SerializeNumberProperty("sampler", channel.sampler, o); json target; SerializeNumberProperty("node", channel.target_node, target); SerializeStringProperty("path", channel.target_path, target); o["target"] = target; + + if (channel.extras.Type() != NULL_TYPE) { + SerializeValue("extras", channel.extras, o); + } } -static void SerializeGltfAnimationSampler(AnimationSampler &sampler, - json &o) { +static void SerializeGltfAnimationSampler(AnimationSampler &sampler, json &o) { SerializeNumberProperty("input", sampler.input, o); SerializeNumberProperty("output", sampler.output, o); SerializeStringProperty("interpolation", sampler.interpolation, o); + + if (sampler.extras.Type() != NULL_TYPE) { + SerializeValue("extras", sampler.extras, o); + } } static void SerializeGltfAnimation(Animation &animation, json &o) { - SerializeStringProperty("name", animation.name, o); + if (!animation.name.empty()) + SerializeStringProperty("name", animation.name, o); json channels; for (unsigned int i = 0; i < animation.channels.size(); ++i) { json channel; @@ -3374,6 +4226,10 @@ static void SerializeGltfAnimation(Animation &animation, json &o) { } o["samplers"] = samplers; + + if (animation.extras.Type() != NULL_TYPE) { + SerializeValue("extras", animation.extras, o); + } } static void SerializeGltfAsset(Asset &asset, json &o) { @@ -3388,48 +4244,79 @@ static void SerializeGltfAsset(Asset &asset, json &o) { if (asset.extras.Keys().size()) { SerializeValue("extras", asset.extras, o); } + + SerializeExtensionMap(asset.extensions, o); +} + +static void SerializeGltfBuffer(Buffer &buffer, json &o) { + SerializeNumberProperty("byteLength", buffer.data.size(), o); + SerializeGltfBufferData(buffer.data, o); + + 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, - const std::string &binFilePath) { - SerializeGltfBufferData(buffer.data, binFilePath); +static bool SerializeGltfBuffer(Buffer &buffer, json &o, + const std::string &binFilename, + const std::string &binBaseFilename) { + if(!SerializeGltfBufferData(buffer.data, binFilename)) return false; SerializeNumberProperty("byteLength", buffer.data.size(), o); - SerializeStringProperty("uri", binFilePath, o); + SerializeStringProperty("uri", binBaseFilename, o); if (buffer.name.size()) SerializeStringProperty("name", buffer.name, o); + + if (buffer.extras.Type() != NULL_TYPE) { + SerializeValue("extras", buffer.extras, o); + } + return true; } -static void SerializeGltfBufferView(BufferView &bufferView, - json &o) { +static void SerializeGltfBufferView(BufferView &bufferView, json &o) { SerializeNumberProperty("buffer", bufferView.buffer, o); SerializeNumberProperty("byteLength", bufferView.byteLength, o); - SerializeNumberProperty("byteStride", bufferView.byteStride, o); - SerializeNumberProperty("byteOffset", bufferView.byteOffset, o); - SerializeNumberProperty("target", bufferView.target, o); + // byteStride is optional, minimum allowed is 4 + if (bufferView.byteStride >= 4) { + SerializeNumberProperty("byteStride", bufferView.byteStride, o); + } + // byteOffset is optional, default is 0 + if (bufferView.byteOffset > 0) { + SerializeNumberProperty("byteOffset", bufferView.byteOffset, o); + } + // Target is optional, check if it contains a valid value + if (bufferView.target == TINYGLTF_TARGET_ARRAY_BUFFER || + bufferView.target == TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER) { + SerializeNumberProperty("target", bufferView.target, o); + } if (bufferView.name.size()) { SerializeStringProperty("name", bufferView.name, o); } + + if (bufferView.extras.Type() != NULL_TYPE) { + SerializeValue("extras", bufferView.extras, o); + } } -// Only external textures are serialized for now static void SerializeGltfImage(Image &image, json &o) { SerializeStringProperty("uri", image.uri, o); if (image.name.size()) { SerializeStringProperty("name", image.name, o); } + + if (image.extras.Type() != NULL_TYPE) { + SerializeValue("extras", image.extras, o); + } + + SerializeExtensionMap(image.extensions, o); } static void SerializeGltfMaterial(Material &material, json &o) { - if (material.extPBRValues.size()) { - // Serialize PBR specular/glossiness material - json values; - SerializeParameterMap(material.extPBRValues, values); - - json extension; - o["extensions"] = extension; - } + if (material.extras.Size()) SerializeValue("extras", material.extras, o); + SerializeExtensionMap(material.extensions, o); if (material.values.size()) { json pbrMetallicRoughness; @@ -3437,12 +4324,15 @@ static void SerializeGltfMaterial(Material &material, json &o) { o["pbrMetallicRoughness"] = pbrMetallicRoughness; } - json additionalValues; SerializeParameterMap(material.additionalValues, o); if (material.name.size()) { SerializeStringProperty("name", material.name, o); } + + if (material.extras.Type() != NULL_TYPE) { + SerializeValue("extras", material.extras, o); + } } static void SerializeGltfMesh(Mesh &mesh, json &o) { @@ -3458,8 +4348,16 @@ static void SerializeGltfMesh(Mesh &mesh, json &o) { } primitive["attributes"] = attributes; - SerializeNumberProperty("indices", gltfPrimitive.indices, primitive); - SerializeNumberProperty("material", gltfPrimitive.material, primitive); + + // Indicies is optional + if (gltfPrimitive.indices > -1) { + SerializeNumberProperty("indices", gltfPrimitive.indices, primitive); + } + // Material is optional + if (gltfPrimitive.material > -1) { + SerializeNumberProperty("material", gltfPrimitive.material, + primitive); + } SerializeNumberProperty("mode", gltfPrimitive.mode, primitive); // Morph targets @@ -3479,6 +4377,10 @@ static void SerializeGltfMesh(Mesh &mesh, json &o) { primitive["targets"] = targets; } + if (gltfPrimitive.extras.Type() != NULL_TYPE) { + SerializeValue("extras", gltfPrimitive.extras, primitive); + } + primitives.push_back(primitive); } @@ -3490,10 +4392,14 @@ static void SerializeGltfMesh(Mesh &mesh, json &o) { if (mesh.name.size()) { SerializeStringProperty("name", mesh.name, o); } + + if (mesh.extras.Type() != NULL_TYPE) { + SerializeValue("extras", mesh.extras, o); + } } static void SerializeGltfLight(Light &light, json &o) { - SerializeStringProperty("name", light.name, o); + if (!light.name.empty()) SerializeStringProperty("name", light.name, o); SerializeNumberArrayProperty("color", light.color, o); SerializeStringProperty("type", light.type, o); } @@ -3523,24 +4429,25 @@ static void SerializeGltfNode(Node &node, json &o) { SerializeNumberProperty("camera", node.camera, o); } - if (node.extLightsValues.size()) { - json values; - SerializeParameterMap(node.extLightsValues, values); - json lightsExt; - lightsExt["KHR_lights_cmn"] = values; - o["extensions"] = lightsExt; + if (node.extras.Type() != NULL_TYPE) { + SerializeValue("extras", node.extras, o); } - - SerializeStringProperty("name", node.name, o); + SerializeExtensionMap(node.extensions, o); + if (!node.name.empty()) SerializeStringProperty("name", node.name, o); SerializeNumberArrayProperty("children", node.children, o); } static void SerializeGltfSampler(Sampler &sampler, json &o) { SerializeNumberProperty("magFilter", sampler.magFilter, o); SerializeNumberProperty("minFilter", sampler.minFilter, o); + SerializeNumberProperty("wrapR", sampler.wrapR, o); SerializeNumberProperty("wrapS", sampler.wrapS, o); SerializeNumberProperty("wrapT", sampler.wrapT, o); + + if (sampler.extras.Type() != NULL_TYPE) { + SerializeValue("extras", sampler.extras, o); + } } static void SerializeGltfOrthographicCamera(const OrthographicCamera &camera, @@ -3549,6 +4456,10 @@ static void SerializeGltfOrthographicCamera(const OrthographicCamera &camera, SerializeNumberProperty("znear", camera.znear, o); SerializeNumberProperty("xmag", camera.xmag, o); SerializeNumberProperty("ymag", camera.ymag, o); + + if (camera.extras.Type() != NULL_TYPE) { + SerializeValue("extras", camera.extras, o); + } } static void SerializeGltfPerspectiveCamera(const PerspectiveCamera &camera, @@ -3562,12 +4473,16 @@ static void SerializeGltfPerspectiveCamera(const PerspectiveCamera &camera, if (camera.yfov > 0) { SerializeNumberProperty("yfov", camera.yfov, o); } + + if (camera.extras.Type() != NULL_TYPE) { + SerializeValue("extras", camera.extras, o); + } } static void SerializeGltfCamera(const Camera &camera, json &o) { SerializeStringProperty("type", camera.type, o); if (!camera.name.empty()) { - SerializeStringProperty("name", camera.type, o); + SerializeStringProperty("name", camera.name, o); } if (camera.type.compare("orthographic") == 0) { @@ -3589,6 +4504,10 @@ static void SerializeGltfScene(Scene &scene, json &o) { if (scene.name.size()) { SerializeStringProperty("name", scene.name, o); } + if (scene.extras.Type() != NULL_TYPE) { + SerializeValue("extras", scene.extras, o); + } + SerializeExtensionMap(scene.extensions, o); } static void SerializeGltfSkin(Skin &skin, json &o) { @@ -3603,26 +4522,60 @@ static void SerializeGltfSkin(Skin &skin, json &o) { } static void SerializeGltfTexture(Texture &texture, json &o) { - SerializeNumberProperty("sampler", texture.sampler, o); - SerializeNumberProperty("source", texture.source, o); - - if (texture.extras.Size()) { - json extras; + if (texture.sampler > -1) { + SerializeNumberProperty("sampler", texture.sampler, o); + } + if (texture.source > -1) { + SerializeNumberProperty("source", texture.source, o); + } + if (texture.extras.Type() != NULL_TYPE) { SerializeValue("extras", texture.extras, o); - o["extras"] = extras; } + SerializeExtensionMap(texture.extensions, o); } -static void WriteGltfFile(const std::string &output, +static bool WriteGltfFile(const std::string &output, const std::string &content) { std::ofstream gltfFile(output.c_str()); + if (!gltfFile.is_open()) return false; gltfFile << content << std::endl; + return true; +} + +static void WriteBinaryGltfFile(const std::string &output, + const std::string &content) { + std::ofstream gltfFile(output.c_str(), std::ios::binary); + + 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 + content.size() + padding_size; + + gltfFile.write(header.c_str(), header.size()); + gltfFile.write(reinterpret_cast(&version), sizeof(version)); + gltfFile.write(reinterpret_cast(&length), sizeof(length)); + + // JSON chunk info, then JSON data + const int model_length = content.size() + padding_size; + const int model_format = 0x4E4F534A; + gltfFile.write(reinterpret_cast(&model_length), sizeof(model_length)); + gltfFile.write(reinterpret_cast(&model_format), sizeof(model_format)); + gltfFile.write(content.c_str(), content.size()); + + // Chunk must be multiplies of 4, so pad with spaces + if (padding_size > 0) { + const std::string padding = std::string(padding_size, ' '); + gltfFile.write(padding.c_str(), padding.size()); + } } -bool TinyGLTF::WriteGltfSceneToFile( - Model *model, - const std::string - &filename /*, bool embedImages, bool embedBuffers, bool writeBinary*/) { +bool TinyGLTF::WriteGltfSceneToFile(Model *model, const std::string &filename, + bool embedImages = false, + bool embedBuffers = false, + bool prettyPrint = true, + bool writeBinary = false) { json output; // ACCESSORS @@ -3652,21 +4605,53 @@ bool TinyGLTF::WriteGltfSceneToFile( SerializeGltfAsset(model->asset, asset); output["asset"] = asset; - std::string binFilePath = filename; - std::string ext = ".bin"; - std::string::size_type pos = binFilePath.rfind('.', binFilePath.length()); + std::string defaultBinFilename = GetBaseFilename(filename); + std::string defaultBinFileExt = ".bin"; + std::string::size_type pos = defaultBinFilename.rfind('.', defaultBinFilename.length()); if (pos != std::string::npos) { - binFilePath = binFilePath.substr(0, pos) + ext; - } else { - binFilePath = "./" + binFilePath + ".bin"; + defaultBinFilename = defaultBinFilename.substr(0, pos); + } + std::string baseDir = GetBaseDir(filename); + if (baseDir.empty()) { + baseDir = "./"; } - // BUFFERS (We expect only one buffer here) + // BUFFERS + std::vector usedUris; json buffers; for (unsigned int i = 0; i < model->buffers.size(); ++i) { json buffer; - SerializeGltfBuffer(model->buffers[i], buffer, binFilePath); + if (embedBuffers) { + SerializeGltfBuffer(model->buffers[i], buffer); + } else { + std::string binSavePath; + std::string binUri; + if (!model->buffers[i].uri.empty() + && !IsDataURI(model->buffers[i].uri)) { + binUri = model->buffers[i].uri; + } + else { + binUri = defaultBinFilename + defaultBinFileExt; + bool inUse = true; + int numUsed = 0; + while(inUse) { + inUse = false; + for (const std::string& usedName : usedUris) { + if (binUri.compare(usedName) != 0) continue; + inUse = true; + binUri = defaultBinFilename + std::to_string(numUsed++) + defaultBinFileExt; + break; + } + } + } + usedUris.push_back(binUri); + binSavePath = JoinPath(baseDir, binUri); + if(!SerializeGltfBuffer(model->buffers[i], buffer, binSavePath, + binUri)) { + return false; + } + } buffers.push_back(buffer); } output["buffers"] = buffers; @@ -3693,52 +4678,67 @@ bool TinyGLTF::WriteGltfSceneToFile( } // IMAGES - json images; - for (unsigned int i = 0; i < model->images.size(); ++i) { - json image; - SerializeGltfImage(model->images[i], image); - images.push_back(image); + if (model->images.size()) { + json images; + for (unsigned int i = 0; i < model->images.size(); ++i) { + json image; + + UpdateImageObject(model->images[i], baseDir, int(i), embedImages, + &this->WriteImageData, this->write_image_user_data_); + SerializeGltfImage(model->images[i], image); + images.push_back(image); + } + output["images"] = images; } - output["images"] = images; // MATERIALS - json materials; - for (unsigned int i = 0; i < model->materials.size(); ++i) { - json material; - SerializeGltfMaterial(model->materials[i], material); - materials.push_back(material); + if (model->materials.size()) { + json materials; + for (unsigned int i = 0; i < model->materials.size(); ++i) { + json material; + SerializeGltfMaterial(model->materials[i], material); + materials.push_back(material); + } + output["materials"] = materials; } - output["materials"] = materials; // MESHES - json meshes; - for (unsigned int i = 0; i < model->meshes.size(); ++i) { - json mesh; - SerializeGltfMesh(model->meshes[i], mesh); - meshes.push_back(mesh); + if (model->meshes.size()) { + json meshes; + for (unsigned int i = 0; i < model->meshes.size(); ++i) { + json mesh; + SerializeGltfMesh(model->meshes[i], mesh); + meshes.push_back(mesh); + } + output["meshes"] = meshes; } - output["meshes"] = meshes; // NODES - json nodes; - for (unsigned int i = 0; i < model->nodes.size(); ++i) { - json node; - SerializeGltfNode(model->nodes[i], node); - nodes.push_back(node); + if (model->nodes.size()) { + json nodes; + for (unsigned int i = 0; i < model->nodes.size(); ++i) { + json node; + SerializeGltfNode(model->nodes[i], node); + nodes.push_back(node); + } + output["nodes"] = nodes; } - output["nodes"] = nodes; // SCENE - SerializeNumberProperty("scene", model->defaultScene, output); + if (model->defaultScene > -1) { + SerializeNumberProperty("scene", model->defaultScene, output); + } // SCENES - json scenes; - for (unsigned int i = 0; i < model->scenes.size(); ++i) { - json currentScene; - SerializeGltfScene(model->scenes[i], currentScene); - scenes.push_back(currentScene); + if (model->scenes.size()) { + json scenes; + for (unsigned int i = 0; i < model->scenes.size(); ++i) { + json currentScene; + SerializeGltfScene(model->scenes[i], currentScene); + scenes.push_back(currentScene); + } + output["scenes"] = scenes; } - output["scenes"] = scenes; // SKINS if (model->skins.size()) { @@ -3752,42 +4752,73 @@ bool TinyGLTF::WriteGltfSceneToFile( } // TEXTURES - json textures; - for (unsigned int i = 0; i < model->textures.size(); ++i) { - json texture; - SerializeGltfTexture(model->textures[i], texture); - textures.push_back(texture); + if (model->textures.size()) { + json textures; + for (unsigned int i = 0; i < model->textures.size(); ++i) { + json texture; + SerializeGltfTexture(model->textures[i], texture); + textures.push_back(texture); + } + output["textures"] = textures; } - output["textures"] = textures; // SAMPLERS - json samplers; - for (unsigned int i = 0; i < model->samplers.size(); ++i) { - json sampler; - SerializeGltfSampler(model->samplers[i], sampler); - samplers.push_back(sampler); + if (model->samplers.size()) { + json samplers; + for (unsigned int i = 0; i < model->samplers.size(); ++i) { + json sampler; + SerializeGltfSampler(model->samplers[i], sampler); + samplers.push_back(sampler); + } + output["samplers"] = samplers; } - output["samplers"] = samplers; // CAMERAS - json cameras; - for (unsigned int i = 0; i < model->cameras.size(); ++i) { - json camera; - SerializeGltfCamera(model->cameras[i], camera); - cameras.push_back(camera); + if (model->cameras.size()) { + json cameras; + for (unsigned int i = 0; i < model->cameras.size(); ++i) { + json camera; + SerializeGltfCamera(model->cameras[i], camera); + cameras.push_back(camera); + } + output["cameras"] = cameras; + } + + // EXTENSIONS + SerializeExtensionMap(model->extensions, output); + + // LIGHTS as KHR_lights_cmn + if (model->lights.size()) { + json lights; + for (unsigned int i = 0; i < model->lights.size(); ++i) { + json light; + SerializeGltfLight(model->lights[i], light); + lights.push_back(light); + } + json khr_lights_cmn; + khr_lights_cmn["lights"] = lights; + json ext_j; + + if (output.find("extensions") != output.end()) { + ext_j = output["extensions"]; + } + + ext_j["KHR_lights_cmn"] = khr_lights_cmn; + + output["extensions"] = ext_j; + } + + // EXTRAS + if (model->extras.Type() != NULL_TYPE) { + SerializeValue("extras", model->extras, output); } - output["cameras"] = cameras; - // LIGHTS - json lights; - for (unsigned int i = 0; i < model->lights.size(); ++i) { - json light; - SerializeGltfLight(model->lights[i], light); - lights.push_back(light); + if (writeBinary) { + WriteBinaryGltfFile(filename, output.dump()); + } else { + WriteGltfFile(filename, output.dump(prettyPrint ? 2 : 0)); } - output["lights"] = lights; - WriteGltfFile(filename, output.dump()); return true; }