From bbbba646decab5931a16f1273b9505635bbc8fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A4r=20Winzell?= Date: Wed, 3 Oct 2018 20:05:09 -0700 Subject: [PATCH] Massive reorganisation. This finishes the first phase of the FBX2glTF refactor, breaking utility classes out where things were getting too monolithic. There is an equally important cleanup phase coming where we wrench all the various parts of this code, including the historical ones that we've rarely touched as yet, into a single C++ style paradigm, and modernise everything to C++11 at least. But for now, we're just picking the pieces back on the floor so we can push 0.9.6 out. It's been far too long since a release. --- CMakeLists.txt | 32 +- src/FBX2glTF.h | 60 +++- src/fbx/Fbx2Raw.cpp | 3 +- src/gltf/GltfModel.cpp | 70 ++++ src/gltf/GltfModel.hpp | 127 +++---- src/gltf/Raw2Gltf.cpp | 495 ++-------------------------- src/gltf/TextureBuilder.cpp | 59 ++-- src/gltf/TextureBuilder.hpp | 16 +- src/gltf/properties/SceneData.hpp | 2 + src/gltf/properties/SkinData.hpp | 2 + src/gltf/properties/TextureData.hpp | 2 + src/raw/RawModel.hpp | 57 ---- 12 files changed, 270 insertions(+), 655 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a6e838b..ba277fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -120,7 +120,7 @@ ExternalProject_Add(Fmt GIT_TAG 4.0.0 CMAKE_CACHE_ARGS "-DFMT_DOC:BOOL=OFF" "-DFMT_INSTALL:BOOL=ON" "-DFMT_TEST:BOOL=OFF" CMAKE_ARGS - -DCMAKE_INSTALL_PREFIX= + -DCMAKE_INSTALL_PREFIX= ) set(FMT_INCLUDE_DIR "${CMAKE_BINARY_DIR}/fmt/include") if (WIN32) @@ -139,13 +139,24 @@ set(LIB_SOURCE_FILES src/FBX2glTF.h src/fbx/Fbx2Raw.cpp src/fbx/Fbx2Raw.hpp - src/fbx/FbxLayerElementAccess.hpp + src/fbx/FbxBlendShapesAccess.cpp src/fbx/FbxBlendShapesAccess.hpp + src/fbx/FbxLayerElementAccess.hpp src/fbx/FbxMaterialInfo.hpp + src/fbx/FbxMaterialsAccess.cpp + src/fbx/FbxMaterialsAccess.hpp + src/fbx/FbxRoughMetMaterialInfo.cpp + src/fbx/FbxRoughMetMaterialInfo.hpp + src/fbx/FbxSkinningAccess.cpp + src/fbx/FbxSkinningAccess.hpp + src/fbx/FbxTraditionalMaterialInfo.cpp + src/fbx/FbxTraditionalMaterialInfo.hpp src/gltf/Raw2Gltf.cpp src/gltf/Raw2Gltf.hpp - src/raw/RawModel.cpp - src/raw/RawModel.hpp + src/gltf/GltfModel.cpp + src/gltf/GltfModel.hpp + src/gltf/TextureBuilder.cpp + src/gltf/TextureBuilder.hpp src/gltf/properties/AccessorData.cpp src/gltf/properties/AccessorData.hpp src/gltf/properties/AnimationData.cpp @@ -174,23 +185,14 @@ set(LIB_SOURCE_FILES src/gltf/properties/TextureData.cpp src/gltf/properties/TextureData.hpp src/mathfu.hpp + src/raw/RawModel.cpp + src/raw/RawModel.hpp src/utils/File_Utils.cpp src/utils/File_Utils.hpp src/utils/Image_Utils.cpp src/utils/Image_Utils.hpp src/utils/String_Utils.cpp src/utils/String_Utils.hpp - src/fbx/FbxBlendShapesAccess.cpp - src/fbx/FbxBlendShapesAccess.hpp - src/fbx/FbxLayerElementAccess.hpp - src/fbx/FbxMaterialsAccess.cpp - src/fbx/FbxMaterialsAccess.hpp - src/fbx/FbxRoughMetMaterialInfo.cpp - src/fbx/FbxRoughMetMaterialInfo.hpp - src/fbx/FbxSkinningAccess.cpp - src/fbx/FbxSkinningAccess.hpp - src/fbx/FbxTraditionalMaterialInfo.cpp - src/fbx/FbxTraditionalMaterialInfo.hpp ) add_library(libFBX2glTF STATIC ${LIB_SOURCE_FILES}) diff --git a/src/FBX2glTF.h b/src/FBX2glTF.h index 81a5adc..db79739 100644 --- a/src/FBX2glTF.h +++ b/src/FBX2glTF.h @@ -17,7 +17,7 @@ #include #endif -#define FBX2GLTF_VERSION std::string("0.9.5") +#define FBX2GLTF_VERSION std::string("0.9.6") #include #include @@ -39,3 +39,61 @@ using workaround_fifo_map = nlohmann::fifo_map; extern bool verboseOutput; + +/** +* The variuos situations in which the user may wish for us to (re-)compute normals for our vertices. +*/ +enum class ComputeNormalsOption { + NEVER, // do not ever compute any normals (results in broken glTF for some sources) + BROKEN, // replace zero-length normals in any mesh that has a normal layer + MISSING, // if a mesh lacks normals, compute them all + ALWAYS // compute a new normal for every vertex, obliterating whatever may have been there before +}; + +enum class UseLongIndicesOptions { + NEVER, // only ever use 16-bit indices + AUTO, // use shorts or longs depending on vertex count + ALWAYS, // only ever use 32-bit indices +}; + +/** +* User-supplied options that dictate the nature of the glTF being generated. +*/ +struct GltfOptions +{ + /** + * If negative, disabled. Otherwise, a bitfield of RawVertexAttributes that + * specify the largest set of attributes that'll ever be kept for a vertex. + * The special bit RAW_VERTEX_ATTRIBUTE_AUTO triggers smart mode, where the + * attributes to keep are inferred from which textures are supplied. + */ + int keepAttribs { -1 }; + /** Whether to output a .glb file, the binary format of glTF. */ + bool outputBinary { false }; + /** If non-binary, whether to inline all resources, for a single (large) .glTF file. */ + bool embedResources { false }; + + /** Whether and how to use KHR_draco_mesh_compression to minimize static geometry size. */ + struct { + bool enabled = false; + int compressionLevel = -1; + int quantBitsPosition = -1; + int quantBitsTexCoord = -1; + int quantBitsNormal = -1; + int quantBitsColor = -1; + int quantBitsGeneric = -1; + } draco; + + /** Whether to use KHR_materials_unlit to extend materials definitions. */ + bool useKHRMatUnlit { false }; + /** Whether to populate the pbrMetallicRoughness substruct in materials. */ + bool usePBRMetRough { false }; + /** Whether to include blend shape normals, if present according to the SDK. */ + bool useBlendShapeNormals { false }; + /** Whether to include blend shape tangents, if present according to the SDK. */ + bool useBlendShapeTangents { false }; + /** When to compute vertex normals from geometry. */ + ComputeNormalsOption computeNormals = ComputeNormalsOption::BROKEN; + /** When to use 32-bit indices. */ + UseLongIndicesOptions useLongIndices = UseLongIndicesOptions::AUTO; +}; diff --git a/src/fbx/Fbx2Raw.cpp b/src/fbx/Fbx2Raw.cpp index 8404a1f..82b7dbb 100644 --- a/src/fbx/Fbx2Raw.cpp +++ b/src/fbx/Fbx2Raw.cpp @@ -7,6 +7,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#include "Fbx2Raw.hpp" + #include #include #include @@ -24,7 +26,6 @@ #include "utils/File_Utils.hpp" #include "utils/String_Utils.hpp" #include "raw/RawModel.hpp" -#include "Fbx2Raw.hpp" #include "FbxBlendShapesAccess.hpp" #include "FbxLayerElementAccess.hpp" diff --git a/src/gltf/GltfModel.cpp b/src/gltf/GltfModel.cpp index f01f994..7f19df4 100644 --- a/src/gltf/GltfModel.cpp +++ b/src/gltf/GltfModel.cpp @@ -8,3 +8,73 @@ */ #include "GltfModel.hpp" + +std::shared_ptr GltfModel::GetAlignedBufferView(BufferData &buffer, const BufferViewData::GL_ArrayType target) +{ + unsigned long bufferSize = this->binary->size(); + if ((bufferSize % 4) > 0) { + bufferSize += (4 - (bufferSize % 4)); + this->binary->resize(bufferSize); + } + return this->bufferViews.hold(new BufferViewData(buffer, bufferSize, target)); +} + +// add a bufferview on the fly and copy data into it +std::shared_ptr GltfModel::AddRawBufferView(BufferData &buffer, const char *source, uint32_t bytes) +{ + auto bufferView = GetAlignedBufferView(buffer, BufferViewData::GL_ARRAY_NONE); + bufferView->byteLength = bytes; + + // make space for the new bytes (possibly moving the underlying data) + unsigned long bufferSize = this->binary->size(); + this->binary->resize(bufferSize + bytes); + + // and copy them into place + memcpy(&(*this->binary)[bufferSize], source, bytes); + return bufferView; +} + +std::shared_ptr GltfModel::AddBufferViewForFile(BufferData &buffer, const std::string &filename) +{ + // see if we've already created a BufferViewData for this precise file + auto iter = filenameToBufferView.find(filename); + if (iter != filenameToBufferView.end()) { + return iter->second; + } + + std::shared_ptr result; + std::ifstream file(filename, std::ios::binary | std::ios::ate); + if (file) { + std::streamsize size = file.tellg(); + file.seekg(0, std::ios::beg); + + std::vector fileBuffer(size); + if (file.read(fileBuffer.data(), size)) { + result = AddRawBufferView(buffer, fileBuffer.data(), size); + } else { + fmt::printf("Warning: Couldn't read %lu bytes from %s, skipping file.\n", size, filename); + } + } else { + fmt::printf("Warning: Couldn't open file %s, skipping file.\n", filename); + } + // note that we persist here not only success, but also failure, as nullptr + filenameToBufferView[filename] = result; + return result; +} + +void GltfModel::serializeHolders(json &glTFJson) +{ + serializeHolder(glTFJson, "buffers", buffers); + serializeHolder(glTFJson, "bufferViews", bufferViews); + serializeHolder(glTFJson, "scenes", scenes); + serializeHolder(glTFJson, "accessors", accessors); + serializeHolder(glTFJson, "images", images); + serializeHolder(glTFJson, "samplers", samplers); + serializeHolder(glTFJson, "textures", textures); + serializeHolder(glTFJson, "materials", materials); + serializeHolder(glTFJson, "meshes", meshes); + serializeHolder(glTFJson, "skins", skins); + serializeHolder(glTFJson, "animations", animations); + serializeHolder(glTFJson, "cameras", cameras); + serializeHolder(glTFJson, "nodes", nodes); +} diff --git a/src/gltf/GltfModel.hpp b/src/gltf/GltfModel.hpp index 9ec17af..f218c0a 100644 --- a/src/gltf/GltfModel.hpp +++ b/src/gltf/GltfModel.hpp @@ -9,8 +9,25 @@ #pragma once +#include + #include "FBX2glTF.h" +#include "gltf/properties/AccessorData.hpp" +#include "gltf/properties/AnimationData.hpp" +#include "gltf/properties/BufferData.hpp" +#include "gltf/properties/BufferViewData.hpp" +#include "gltf/properties/CameraData.hpp" +#include "gltf/properties/ImageData.hpp" +#include "gltf/properties/MaterialData.hpp" +#include "gltf/properties/MeshData.hpp" +#include "gltf/properties/NodeData.hpp" +#include "gltf/properties/PrimitiveData.hpp" +#include "gltf/properties/SamplerData.hpp" +#include "gltf/properties/SceneData.hpp" +#include "gltf/properties/SkinData.hpp" +#include "gltf/properties/TextureData.hpp" + /** * glTF 2.0 is based on the idea that data structs within a file are referenced by index; an accessor will * point to the n:th buffer view, and so on. The Holder class takes a freshly instantiated class, and then @@ -22,84 +39,39 @@ * struct will, by design, outlive all other activity that takes place during in a single conversion run. */ template -struct Holder +class Holder { - std::vector> ptrs; +public: std::shared_ptr hold(T *ptr) { ptr->ix = ptrs.size(); ptrs.emplace_back(ptr); return ptrs.back(); } + std::vector> ptrs; }; -struct GltfModel +class GltfModel { - explicit GltfModel(bool _isGlb) - : binary(new std::vector), - isGlb(_isGlb) +public: + explicit GltfModel(const GltfOptions &options) + : binary(new std::vector) + , isGlb(options.outputBinary) + , defaultSampler(nullptr) + , defaultBuffer(buffers.hold(buildDefaultBuffer(options))) { + defaultSampler = samplers.hold(buildDefaultSampler()); } - std::shared_ptr GetAlignedBufferView(BufferData &buffer, const BufferViewData::GL_ArrayType target) - { - unsigned long bufferSize = this->binary->size(); - if ((bufferSize % 4) > 0) { - bufferSize += (4 - (bufferSize % 4)); - this->binary->resize(bufferSize); - } - return this->bufferViews.hold(new BufferViewData(buffer, bufferSize, target)); - } - - // add a bufferview on the fly and copy data into it - std::shared_ptr AddRawBufferView(BufferData &buffer, const char *source, uint32_t bytes) - { - auto bufferView = GetAlignedBufferView(buffer, BufferViewData::GL_ARRAY_NONE); - bufferView->byteLength = bytes; - - // make space for the new bytes (possibly moving the underlying data) - unsigned long bufferSize = this->binary->size(); - this->binary->resize(bufferSize + bytes); - - // and copy them into place - memcpy(&(*this->binary)[bufferSize], source, bytes); - return bufferView; - } - - std::shared_ptr AddBufferViewForFile(BufferData &buffer, const std::string &filename) - { - // see if we've already created a BufferViewData for this precise file - auto iter = filenameToBufferView.find(filename); - if (iter != filenameToBufferView.end()) { - return iter->second; - } - - std::shared_ptr result; - std::ifstream file(filename, std::ios::binary | std::ios::ate); - if (file) { - std::streamsize size = file.tellg(); - file.seekg(0, std::ios::beg); - - std::vector fileBuffer(size); - if (file.read(fileBuffer.data(), size)) { - result = AddRawBufferView(buffer, fileBuffer.data(), size); - } else { - fmt::printf("Warning: Couldn't read %lu bytes from %s, skipping file.\n", size, filename); - } - } else { - fmt::printf("Warning: Couldn't open file %s, skipping file.\n", filename); - } - // note that we persist here not only success, but also failure, as nullptr - filenameToBufferView[filename] = result; - return result; - } - + std::shared_ptr GetAlignedBufferView(BufferData &buffer, const BufferViewData::GL_ArrayType target); + std::shared_ptr AddRawBufferView(BufferData &buffer, const char *source, uint32_t bytes); + std::shared_ptr AddBufferViewForFile(BufferData &buffer, const std::string &filename); template std::shared_ptr AddAccessorWithView( BufferViewData &bufferView, const GLType &type, const std::vector &source, std::string name) { - auto accessor = accessors.hold(new AccessorData(bufferView, type)); + auto accessor = accessors.hold(new AccessorData(bufferView, type, name)); accessor->appendAsBinaryArray(source, *binary); bufferView.byteLength = accessor->byteLength(); return accessor; @@ -138,7 +110,7 @@ struct GltfModel accessor->count = attribArr.size(); } else { auto bufferView = GetAlignedBufferView(buffer, BufferViewData::GL_ARRAY_BUFFER); - accessor = AddAccessorWithView(*bufferView, attrDef.glType, attribArr); + accessor = AddAccessorWithView(*bufferView, attrDef.glType, attribArr, std::string("")); } primitive.AddAttrib(attrDef.gltfName, *accessor); return accessor; @@ -156,30 +128,14 @@ struct GltfModel } } - void serializeHolders(json &glTFJson) - { - serializeHolder(glTFJson, "buffers", buffers); - serializeHolder(glTFJson, "bufferViews", bufferViews); - serializeHolder(glTFJson, "scenes", scenes); - serializeHolder(glTFJson, "accessors", accessors); - serializeHolder(glTFJson, "images", images); - serializeHolder(glTFJson, "samplers", samplers); - serializeHolder(glTFJson, "textures", textures); - serializeHolder(glTFJson, "materials", materials); - serializeHolder(glTFJson, "meshes", meshes); - serializeHolder(glTFJson, "skins", skins); - serializeHolder(glTFJson, "animations", animations); - serializeHolder(glTFJson, "cameras", cameras); - serializeHolder(glTFJson, "nodes", nodes); - } + void serializeHolders(json &glTFJson); const bool isGlb; // cache BufferViewData instances that've already been created from a given filename std::map> filenameToBufferView; - std::shared_ptr > binary; - + std::shared_ptr> binary; Holder buffers; Holder bufferViews; @@ -194,4 +150,17 @@ struct GltfModel Holder cameras; Holder nodes; Holder scenes; + + std::shared_ptr defaultSampler; + std::shared_ptr defaultBuffer; + +private: + SamplerData *buildDefaultSampler() { + return new SamplerData(); + } + BufferData *buildDefaultBuffer(const GltfOptions &options) { + return options.outputBinary ? + new BufferData(binary) : + new BufferData(extBufferFilename, binary, options.embedResources); + } }; diff --git a/src/gltf/Raw2Gltf.cpp b/src/gltf/Raw2Gltf.cpp index b18fb9f..467eb04 100644 --- a/src/gltf/Raw2Gltf.cpp +++ b/src/gltf/Raw2Gltf.cpp @@ -20,6 +20,7 @@ #include "utils/String_Utils.hpp" #include "utils/Image_Utils.hpp" #include + #include "raw/RawModel.hpp" #include "gltf/properties/AccessorData.hpp" @@ -37,204 +38,13 @@ #include "gltf/properties/SkinData.hpp" #include "gltf/properties/TextureData.hpp" +#include "TextureBuilder.hpp" +#include "GltfModel.hpp" + typedef uint32_t TriangleIndex; #define DEFAULT_SCENE_NAME "Root Scene" -/** - * glTF 2.0 is based on the idea that data structs within a file are referenced by index; an accessor will - * point to the n:th buffer view, and so on. The Holder class takes a freshly instantiated class, and then - * creates, stored, and returns a shared_ptr for it. - * - * The idea is that every glTF resource in the file will live as long as the Holder does, and the Holders - * are all kept in the GLTFData struct. Clients may certainly cnhoose to perpetuate the full shared_ptr - * reference counting type, but generally speaking we pass around simple T& and T* types because the GLTFData - * struct will, by design, outlive all other activity that takes place during in a single conversion run. - */ -template -struct Holder -{ - std::vector> ptrs; - std::shared_ptr hold(T *ptr) - { - ptr->ix = ptrs.size(); - ptrs.emplace_back(ptr); - return ptrs.back(); - } -}; - -struct GLTFData -{ - explicit GLTFData(bool _isGlb) - : binary(new std::vector), - isGlb(_isGlb) - { - } - - std::shared_ptr GetAlignedBufferView(BufferData &buffer, const BufferViewData::GL_ArrayType target) - { - unsigned long bufferSize = this->binary->size(); - if ((bufferSize % 4) > 0) { - bufferSize += (4 - (bufferSize % 4)); - this->binary->resize(bufferSize); - } - return this->bufferViews.hold(new BufferViewData(buffer, bufferSize, target)); - } - - // add a bufferview on the fly and copy data into it - std::shared_ptr AddRawBufferView(BufferData &buffer, const char *source, uint32_t bytes) - { - auto bufferView = GetAlignedBufferView(buffer, BufferViewData::GL_ARRAY_NONE); - bufferView->byteLength = bytes; - - // make space for the new bytes (possibly moving the underlying data) - unsigned long bufferSize = this->binary->size(); - this->binary->resize(bufferSize + bytes); - - // and copy them into place - memcpy(&(*this->binary)[bufferSize], source, bytes); - return bufferView; - } - - std::shared_ptr AddBufferViewForFile(BufferData &buffer, const std::string &filename) - { - // see if we've already created a BufferViewData for this precise file - auto iter = filenameToBufferView.find(filename); - if (iter != filenameToBufferView.end()) { - return iter->second; - } - - std::shared_ptr result; - std::ifstream file(filename, std::ios::binary | std::ios::ate); - if (file) { - std::streamsize size = file.tellg(); - file.seekg(0, std::ios::beg); - - std::vector fileBuffer(size); - if (file.read(fileBuffer.data(), size)) { - result = AddRawBufferView(buffer, fileBuffer.data(), size); - } else { - fmt::printf("Warning: Couldn't read %lu bytes from %s, skipping file.\n", size, filename); - } - } else { - fmt::printf("Warning: Couldn't open file %s, skipping file.\n", filename); - } - // note that we persist here not only success, but also failure, as nullptr - filenameToBufferView[filename] = result; - return result; - } - - template - std::shared_ptr AddAccessorWithView( - BufferViewData &bufferView, const GLType &type, const std::vector &source) - { - auto accessor = accessors.hold(new AccessorData(bufferView, type, std::string(""))); - accessor->appendAsBinaryArray(source, *binary); - bufferView.byteLength = accessor->byteLength(); - return accessor; - } - - - template - std::shared_ptr AddAccessorWithView( - BufferViewData &bufferView, const GLType &type, const std::vector &source, std::string name) - { - auto accessor = accessors.hold(new AccessorData(bufferView, type, name)); - accessor->appendAsBinaryArray(source, *binary); - bufferView.byteLength = accessor->byteLength(); - return accessor; - } - - template - std::shared_ptr AddAccessorAndView( - BufferData &buffer, const GLType &type, const std::vector &source) - { - auto bufferView = GetAlignedBufferView(buffer, BufferViewData::GL_ARRAY_NONE); - return AddAccessorWithView(*bufferView, type, source); - } - - template - std::shared_ptr AddAttributeToPrimitive( - BufferData &buffer, const RawModel &surfaceModel, PrimitiveData &primitive, - const AttributeDefinition &attrDef) - { - // copy attribute data into vector - std::vector attribArr; - surfaceModel.GetAttributeArray(attribArr, attrDef.rawAttributeIx); - - std::shared_ptr accessor; - if (attrDef.dracoComponentType != draco::DT_INVALID && primitive.dracoMesh != nullptr) { - primitive.AddDracoAttrib(attrDef, attribArr); - - accessor = accessors.hold(new AccessorData(attrDef.glType)); - accessor->count = attribArr.size(); - } else { - auto bufferView = GetAlignedBufferView(buffer, BufferViewData::GL_ARRAY_BUFFER); - accessor = AddAccessorWithView(*bufferView, attrDef.glType, attribArr); - } - primitive.AddAttrib(attrDef.gltfName, *accessor); - return accessor; - }; - - template - void serializeHolder(json &glTFJson, std::string key, const Holder holder) - { - if (!holder.ptrs.empty()) { - std::vector bits; - for (const auto &ptr : holder.ptrs) { - bits.push_back(ptr->serialize()); - } - glTFJson[key] = bits; - } - } - - void serializeHolders(json &glTFJson) - { - serializeHolder(glTFJson, "buffers", buffers); - serializeHolder(glTFJson, "bufferViews", bufferViews); - serializeHolder(glTFJson, "scenes", scenes); - serializeHolder(glTFJson, "accessors", accessors); - serializeHolder(glTFJson, "images", images); - serializeHolder(glTFJson, "samplers", samplers); - serializeHolder(glTFJson, "textures", textures); - serializeHolder(glTFJson, "materials", materials); - serializeHolder(glTFJson, "meshes", meshes); - serializeHolder(glTFJson, "skins", skins); - serializeHolder(glTFJson, "animations", animations); - serializeHolder(glTFJson, "cameras", cameras); - serializeHolder(glTFJson, "nodes", nodes); - } - - const bool isGlb; - - // cache BufferViewData instances that've already been created from a given filename - std::map> filenameToBufferView; - - std::shared_ptr > binary; - - - Holder buffers; - Holder bufferViews; - Holder accessors; - Holder images; - Holder samplers; - Holder textures; - Holder materials; - Holder meshes; - Holder skins; - Holder animations; - Holder cameras; - Holder nodes; - Holder scenes; -}; - -static void WriteToVectorContext(void *context, void *data, int size) { - auto *vec = static_cast *>(context); - for (int ii = 0; ii < size; ii ++) { - vec->push_back(((char *) data)[ii]); - } -} - /** * This method sanity-checks existance and then returns a *reference* to the *Data instance * registered under that name. This is safe in the context of this tool, where all such data @@ -311,7 +121,7 @@ ModelData *Raw2Gltf( fmt::printf("%7d animations\n", raw.GetAnimationCount()); } - std::unique_ptr gltf(new GLTFData(options.outputBinary)); + std::unique_ptr gltf(new GltfModel(options)); std::map> nodesById; std::map> materialsByName; @@ -319,10 +129,7 @@ ModelData *Raw2Gltf( std::map> meshBySurfaceId; // for now, we only have one buffer; data->binary points to the same vector as that BufferData does. - BufferData &buffer = *gltf->buffers.hold( - options.outputBinary ? - new BufferData(gltf->binary) : - new BufferData(extBufferFilename, gltf->binary, options.embedResources)); + BufferData &buffer = *gltf->defaultBuffer; { // // nodes @@ -398,232 +205,10 @@ ModelData *Raw2Gltf( // samplers // - SamplerData &defaultSampler = *gltf->samplers.hold(new SamplerData()); - - // // textures // - using pixel = std::array; // pixel components are floats in [0, 1] - using pixel_merger = std::function)>; - - auto texIndicesKey = [&](std::vector ixVec, std::string tag) -> std::string { - std::string result = tag; - for (int ix : ixVec) { - result += "_" + std::to_string(ix); - } - return result; - }; - - /** - * Create a new derived TextureData for the two given RawTexture indexes, or return a previously created one. - * Each pixel in the derived texture will be determined from its equivalent in each source pixels, as decided - * by the provided `combine` function. - */ - auto getDerivedTexture = [&]( - std::vector rawTexIndices, - const pixel_merger &combine, - const std::string &tag, - bool transparentOutput - ) -> std::shared_ptr - { - const std::string key = texIndicesKey(rawTexIndices, tag); - auto iter = textureByIndicesKey.find(key); - if (iter != textureByIndicesKey.end()) { - return iter->second; - } - - auto describeChannel = [&](int channels) -> std::string { - switch(channels) { - case 1: return "G"; - case 2: return "GA"; - case 3: return "RGB"; - case 4: return "RGBA"; - default: - return fmt::format("?%d?", channels); - } - }; - - // keep track of some texture data as we load them - struct TexInfo { - explicit TexInfo(int rawTexIx) : rawTexIx(rawTexIx) {} - - const int rawTexIx; - int width {}; - int height {}; - int channels {}; - uint8_t *pixels {}; - }; - - int width = -1, height = -1; - std::string mergedFilename = tag; - std::vector texes { }; - for (const int rawTexIx : rawTexIndices) { - TexInfo info(rawTexIx); - if (rawTexIx >= 0) { - const RawTexture &rawTex = raw.GetTexture(rawTexIx); - const std::string &fileLoc = rawTex.fileLocation; - const std::string &name = StringUtils::GetFileBaseString(StringUtils::GetFileNameString(fileLoc)); - if (!fileLoc.empty()) { - info.pixels = stbi_load(fileLoc.c_str(), &info.width, &info.height, &info.channels, 0); - if (!info.pixels) { - fmt::printf("Warning: merge texture [%d](%s) could not be loaded.\n", - rawTexIx, - name); - } else { - if (width < 0) { - width = info.width; - height = info.height; - } else if (width != info.width || height != info.height) { - fmt::printf("Warning: texture %s (%d, %d) can't be merged with previous texture(s) of dimension (%d, %d)\n", - name, - info.width, info.height, width, height); - // this is bad enough that we abort the whole merge - return nullptr; - } - mergedFilename += "_" + name; - } - } - } - texes.push_back(info); - } - // at the moment, the best choice of filename is also the best choice of name - const std::string mergedName = mergedFilename; - - if (width < 0) { - // no textures to merge; bail - return nullptr; - } - // TODO: which channel combinations make sense in input files? - - // write 3 or 4 channels depending on whether or not we need transparency - int channels = transparentOutput ? 4 : 3; - - std::vector mergedPixels(static_cast(channels * width * height)); - std::vector pixels(texes.size()); - std::vector pixelPointers(texes.size()); - for (int xx = 0; xx < width; xx ++) { - for (int yy = 0; yy < height; yy ++) { - pixels.clear(); - for (int jj = 0; jj < texes.size(); jj ++) { - const TexInfo &tex = texes[jj]; - // each texture's structure will depend on its channel count - int ii = tex.channels * (xx + yy*width); - int kk = 0; - if (tex.pixels != nullptr) { - for (; kk < tex.channels; kk ++) { - pixels[jj][kk] = tex.pixels[ii++] / 255.0f; - } - } - for (; kk < pixels[jj].size(); kk ++) { - pixels[jj][kk] = 1.0f; - } - pixelPointers[jj] = &pixels[jj]; - } - const pixel merged = combine(pixelPointers); - int ii = channels * (xx + yy*width); - for (int jj = 0; jj < channels; jj ++) { - mergedPixels[ii + jj] = static_cast(fmax(0, fmin(255.0f, merged[jj] * 255.0f))); - } - } - } - - // write a .png iff we need transparency in the destination texture - bool png = transparentOutput; - - std::vector imgBuffer; - int res; - if (png) { - res = stbi_write_png_to_func(WriteToVectorContext, &imgBuffer, - width, height, channels, mergedPixels.data(), width * channels); - } else { - res = stbi_write_jpg_to_func(WriteToVectorContext, &imgBuffer, - width, height, channels, mergedPixels.data(), 80); - } - if (!res) { - fmt::printf("Warning: failed to generate merge texture '%s'.\n", mergedFilename); - return nullptr; - } - - ImageData *image; - if (options.outputBinary) { - const auto bufferView = gltf->AddRawBufferView(buffer, imgBuffer.data(), imgBuffer.size()); - image = new ImageData(mergedName, *bufferView, png ? "image/png" : "image/jpeg"); - - } else { - const std::string imageFilename = mergedFilename + (png ? ".png" : ".jpg"); - const std::string imagePath = outputFolder + imageFilename; - FILE *fp = fopen(imagePath.c_str(), "wb"); - if (fp == nullptr) { - fmt::printf("Warning:: Couldn't write file '%s' for writing.\n", imagePath); - return nullptr; - } - - if (fwrite(imgBuffer.data(), imgBuffer.size(), 1, fp) != 1) { - fmt::printf("Warning: Failed to write %lu bytes to file '%s'.\n", imgBuffer.size(), imagePath); - fclose(fp); - return nullptr; - } - fclose(fp); - if (verboseOutput) { - fmt::printf("Wrote %lu bytes to texture '%s'.\n", imgBuffer.size(), imagePath); - } - - image = new ImageData(mergedName, imageFilename); - } - - std::shared_ptr texDat = gltf->textures.hold( - new TextureData(mergedName, defaultSampler, *gltf->images.hold(image))); - textureByIndicesKey.insert(std::make_pair(key, texDat)); - return texDat; - }; - - /** Create a new TextureData for the given RawTexture index, or return a previously created one. */ - auto getSimpleTexture = [&](int rawTexIndex, const std::string &tag) { - const std::string key = texIndicesKey({ rawTexIndex }, tag); - auto iter = textureByIndicesKey.find(key); - if (iter != textureByIndicesKey.end()) { - return iter->second; - } - - const RawTexture &rawTexture = raw.GetTexture(rawTexIndex); - const std::string textureName = StringUtils::GetFileBaseString(rawTexture.name); - const std::string relativeFilename = StringUtils::GetFileNameString(rawTexture.fileLocation); - - ImageData *image = nullptr; - if (options.outputBinary) { - auto bufferView = gltf->AddBufferViewForFile(buffer, rawTexture.fileLocation); - if (bufferView) { - std::string suffix = StringUtils::GetFileSuffixString(rawTexture.fileLocation); - image = new ImageData(relativeFilename, *bufferView, ImageUtils::suffixToMimeType(suffix)); - } - - } else if (!relativeFilename.empty()) { - image = new ImageData(relativeFilename, relativeFilename); - std::string outputPath = outputFolder + relativeFilename; - if (FileUtils::CopyFile(rawTexture.fileLocation, outputPath)) { - if (verboseOutput) { - fmt::printf("Copied texture '%s' to output folder: %s\n", textureName, outputPath); - } - } else { - // no point commenting further on read/write error; CopyFile() does enough of that, and we - // certainly want to to add an image struct to the glTF JSON, with the correct relative path - // reference, even if the copy failed. - } - } - if (!image) { - // fallback is tiny transparent PNG - image = new ImageData( - textureName, - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==" - ); - } - - std::shared_ptr texDat = gltf->textures.hold( - new TextureData(textureName, defaultSampler, *gltf->images.hold(image))); - textureByIndicesKey.insert(std::make_pair(key, texDat)); - return texDat; - }; + TextureBuilder textureBuilder(raw, options, outputFolder, *gltf); // // materials @@ -638,54 +223,15 @@ ModelData *Raw2Gltf( Vec3f emissiveFactor; float emissiveIntensity; - const Vec3f dielectric(0.04f, 0.04f, 0.04f); - // acquire the texture of a specific RawTextureUsage as *TextData, or nullptr if none exists auto simpleTex = [&](RawTextureUsage usage) -> std::shared_ptr { - return (material.textures[usage] >= 0) ? getSimpleTexture(material.textures[usage], "simple") : nullptr; + return (material.textures[usage] >= 0) ? textureBuilder.simple(material.textures[usage], "simple") : nullptr; }; TextureData *normalTexture = simpleTex(RAW_TEXTURE_USAGE_NORMAL).get(); TextureData *emissiveTexture = simpleTex(RAW_TEXTURE_USAGE_EMISSIVE).get(); TextureData *occlusionTexture = nullptr; - // acquire derived texture of single RawTextureUsage as *TextData, or nullptr if it doesn't exist - auto merge1Tex = [&]( - const std::string tag, - RawTextureUsage usage, - const pixel_merger &combine, - bool outputHasAlpha - ) -> std::shared_ptr { - return getDerivedTexture({ material.textures[usage] }, combine, tag, outputHasAlpha); - }; - - // acquire derived texture of two RawTextureUsage as *TextData, or nullptr if neither exists - auto merge2Tex = [&]( - const std::string tag, - RawTextureUsage u1, - RawTextureUsage u2, - const pixel_merger &combine, - bool outputHasAlpha - ) -> std::shared_ptr { - return getDerivedTexture( - { material.textures[u1], material.textures[u2] }, - combine, tag, outputHasAlpha); - }; - - // acquire derived texture of two RawTextureUsage as *TextData, or nullptr if neither exists - auto merge3Tex = [&]( - const std::string tag, - RawTextureUsage u1, - RawTextureUsage u2, - RawTextureUsage u3, - const pixel_merger &combine, - bool outputHasAlpha - ) -> std::shared_ptr { - return getDerivedTexture( - { material.textures[u1], material.textures[u2], material.textures[u3] }, - combine, tag, outputHasAlpha); - }; - std::shared_ptr pbrMetRough; if (options.usePBRMetRough) { // albedo is a basic texture, no merging needed @@ -702,12 +248,18 @@ ModelData *Raw2Gltf( */ RawMetRoughMatProps *props = (RawMetRoughMatProps *) material.info.get(); // merge metallic into the blue channel and roughness into the green, of a new combinatory texture - aoMetRoughTex = merge3Tex("ao_met_rough", - RAW_TEXTURE_USAGE_OCCLUSION, RAW_TEXTURE_USAGE_METALLIC, RAW_TEXTURE_USAGE_ROUGHNESS, - [&](const std::vector pixels) -> pixel { + aoMetRoughTex = textureBuilder.combine( + { + material.textures[RAW_TEXTURE_USAGE_OCCLUSION], + material.textures[RAW_TEXTURE_USAGE_METALLIC], + material.textures[RAW_TEXTURE_USAGE_ROUGHNESS], + }, + "ao_met_rough", + [&](const std::vector pixels) -> TextureBuilder::pixel { return { {(*pixels[0])[0], (*pixels[2])[0], (*pixels[1])[0], 1} }; }, false); + baseColorTex = simpleTex(RAW_TEXTURE_USAGE_ALBEDO); diffuseFactor = props->diffuseFactor; metallic = props->metallic; @@ -738,19 +290,21 @@ ModelData *Raw2Gltf( // shininess 2 -> roughness ~0.7 // shininess 6 -> roughness 0.5 // shininess 16 -> roughness ~0.33 - // as shininess ==> oo, roughness ==> 0 auto getRoughness = [&](float shininess) { return sqrtf(2.0f / (2.0f + shininess)); }; - aoMetRoughTex = merge1Tex("ao_met_rough", - RAW_TEXTURE_USAGE_SHININESS, - [&](const std::vector pixels) -> pixel { + + aoMetRoughTex = textureBuilder.combine( + { material.textures[RAW_TEXTURE_USAGE_SHININESS], }, + "ao_met_rough", + [&](const std::vector pixels) -> TextureBuilder::pixel { // do not multiply with props->shininess; that doesn't work like the other factors. float shininess = props->shininess * (*pixels[0])[0]; return { {0, getRoughness(shininess), metallic, 1} }; }, false); + if (aoMetRoughTex != nullptr) { // if we successfully built a texture, factors are just multiplicative identity metallic = roughness = 1.0f; @@ -858,7 +412,7 @@ ModelData *Raw2Gltf( } else { const AccessorData &indexes = *gltf->AddAccessorWithView( *gltf->GetAlignedBufferView(buffer, BufferViewData::GL_ELEMENT_ARRAY_BUFFER), - useLongIndices ? GLT_UINT : GLT_USHORT, getIndexArray(surfaceModel)); + useLongIndices ? GLT_UINT : GLT_USHORT, getIndexArray(surfaceModel), std::string("")); primitive.reset(new PrimitiveData(indexes, mData)); }; @@ -992,7 +546,6 @@ ModelData *Raw2Gltf( // for (int i = 0; i < raw.GetNodeCount(); i++) { - const RawNode &node = raw.GetNode(i); auto nodeData = gltf->nodes.ptrs[i]; diff --git a/src/gltf/TextureBuilder.cpp b/src/gltf/TextureBuilder.cpp index ff9c063..c3a9af4 100644 --- a/src/gltf/TextureBuilder.cpp +++ b/src/gltf/TextureBuilder.cpp @@ -14,7 +14,10 @@ #include #include +#include + #include +#include // keep track of some texture data as we load them struct TexInfo { @@ -30,7 +33,8 @@ struct TexInfo { std::shared_ptr TextureBuilder::combine( const std::vector &ixVec, const std::string &tag, - const pixel_merger &computePixel) + const pixel_merger &computePixel, + bool includeAlphaChannel) { const std::string key = texIndicesKey(ixVec, tag); auto iter = textureByIndicesKey.find(key); @@ -80,7 +84,7 @@ std::shared_ptr TextureBuilder::combine( // TODO: which channel combinations make sense in input files? // write 3 or 4 channels depending on whether or not we need transparency - int channels = transparentOutput ? 4 : 3; + int channels = includeAlphaChannel ? 4 : 3; std::vector mergedPixels(static_cast(channels * width * height)); std::vector pixels(texes.size()); @@ -112,7 +116,7 @@ std::shared_ptr TextureBuilder::combine( } // write a .png iff we need transparency in the destination texture - bool png = transparentOutput; + bool png = includeAlphaChannel; std::vector imgBuffer; int res; @@ -130,31 +134,32 @@ std::shared_ptr TextureBuilder::combine( ImageData *image; if (options.outputBinary) { - const auto bufferView = gltf->AddRawBufferView(buffer, imgBuffer.data(), imgBuffer.size()); - return std::make_unique(mergedName, *bufferView, png ? "image/png" : "image/jpeg"); - } - const std::string imageFilename = mergedFilename + (png ? ".png" : ".jpg"); - const std::string imagePath = outputFolder + imageFilename; - FILE *fp = fopen(imagePath.c_str(), "wb"); - if (fp == nullptr) { - fmt::printf("Warning:: Couldn't write file '%s' for writing.\n", imagePath); - return nullptr; - } + const auto bufferView = gltf.AddRawBufferView(*gltf.defaultBuffer, imgBuffer.data(), imgBuffer.size()); + image = new ImageData(mergedName, *bufferView, png ? "image/png" : "image/jpeg"); + } else { + const std::string imageFilename = mergedFilename + (png ? ".png" : ".jpg"); + const std::string imagePath = outputFolder + imageFilename; + FILE *fp = fopen(imagePath.c_str(), "wb"); + if (fp == nullptr) { + fmt::printf("Warning:: Couldn't write file '%s' for writing.\n", imagePath); + return nullptr; + } - if (fwrite(imgBuffer.data(), imgBuffer.size(), 1, fp) != 1) { - fmt::printf("Warning: Failed to write %lu bytes to file '%s'.\n", imgBuffer.size(), imagePath); + if (fwrite(imgBuffer.data(), imgBuffer.size(), 1, fp) != 1) { + fmt::printf("Warning: Failed to write %lu bytes to file '%s'.\n", imgBuffer.size(), imagePath); + fclose(fp); + return nullptr; + } fclose(fp); - return nullptr; + if (verboseOutput) { + fmt::printf("Wrote %lu bytes to texture '%s'.\n", imgBuffer.size(), imagePath); + } + image = new ImageData(mergedName, imageFilename); } - fclose(fp); - if (verboseOutput) { - fmt::printf("Wrote %lu bytes to texture '%s'.\n", imgBuffer.size(), imagePath); - } - std::shared_ptr texDat = gltf->textures.hold( - new TextureData(mergedName, defaultSampler, *image)); + std::shared_ptr texDat = gltf.textures.hold( + new TextureData(mergedName, *gltf.defaultSampler, *gltf.images.hold(image))); textureByIndicesKey.insert(std::make_pair(key, texDat)); - - return std::make_unique(mergedName, imageFilename); + return texDat; } /** Create a new TextureData for the given RawTexture index, or return a previously created one. */ @@ -171,7 +176,7 @@ std::shared_ptr TextureBuilder::simple(int rawTexIndex, const std:: ImageData *image = nullptr; if (options.outputBinary) { - auto bufferView = gltf->AddBufferViewForFile(buffer, rawTexture.fileLocation); + auto bufferView = gltf.AddBufferViewForFile(*gltf.defaultBuffer, rawTexture.fileLocation); if (bufferView) { std::string suffix = StringUtils::GetFileSuffixString(rawTexture.fileLocation); image = new ImageData(relativeFilename, *bufferView, ImageUtils::suffixToMimeType(suffix)); @@ -198,8 +203,8 @@ std::shared_ptr TextureBuilder::simple(int rawTexIndex, const std:: ); } - std::shared_ptr texDat = gltf->textures.hold( - new TextureData(textureName, defaultSampler, *gltf->images.hold(image))); + std::shared_ptr texDat = gltf.textures.hold( + new TextureData(textureName, *gltf.defaultSampler, *gltf.images.hold(image))); textureByIndicesKey.insert(std::make_pair(key, texDat)); return texDat; diff --git a/src/gltf/TextureBuilder.hpp b/src/gltf/TextureBuilder.hpp index 0e4bc8e..a60efe9 100644 --- a/src/gltf/TextureBuilder.hpp +++ b/src/gltf/TextureBuilder.hpp @@ -15,14 +15,18 @@ #include -using pixel = std::array; // pixel components are floats in [0, 1] -using pixel_merger = std::function)>; +#include "GltfModel.hpp" class TextureBuilder { public: - TextureBuilder(const RawModel &raw, GltfModel &gltf) + using pixel = std::array; // pixel components are floats in [0, 1] + using pixel_merger = std::function)>; + + TextureBuilder(const RawModel &raw, const GltfOptions &options, const std::string &outputFolder, GltfModel &gltf) : raw(raw) + , options(options) + , outputFolder(outputFolder) , gltf(gltf) {} ~TextureBuilder() {} @@ -30,7 +34,8 @@ public: std::shared_ptr combine( const std::vector &ixVec, const std::string &tag, - const pixel_merger &mergeFunction + const pixel_merger &mergeFunction, + bool transparency ); std::shared_ptr simple(int rawTexIndex, const std::string &tag); @@ -63,6 +68,9 @@ public: private: const RawModel &raw; + const GltfOptions &options; + const std::string outputFolder; GltfModel &gltf; + std::map> textureByIndicesKey; }; diff --git a/src/gltf/properties/SceneData.hpp b/src/gltf/properties/SceneData.hpp index d4b1777..3fbb73f 100644 --- a/src/gltf/properties/SceneData.hpp +++ b/src/gltf/properties/SceneData.hpp @@ -7,6 +7,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#pragma once + #include "gltf/Raw2Gltf.hpp" struct SceneData : Holdable diff --git a/src/gltf/properties/SkinData.hpp b/src/gltf/properties/SkinData.hpp index 3733f7e..8a3b22f 100644 --- a/src/gltf/properties/SkinData.hpp +++ b/src/gltf/properties/SkinData.hpp @@ -7,6 +7,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#pragma once + #include "gltf/Raw2Gltf.hpp" struct SkinData : Holdable diff --git a/src/gltf/properties/TextureData.hpp b/src/gltf/properties/TextureData.hpp index cdf8670..4ff74d9 100644 --- a/src/gltf/properties/TextureData.hpp +++ b/src/gltf/properties/TextureData.hpp @@ -7,6 +7,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#pragma once + #include "gltf/Raw2Gltf.hpp" struct TextureData : Holdable diff --git a/src/raw/RawModel.hpp b/src/raw/RawModel.hpp index e895870..82ed269 100644 --- a/src/raw/RawModel.hpp +++ b/src/raw/RawModel.hpp @@ -15,63 +15,6 @@ #include "FBX2glTF.h" -/** - * The variuos situations in which the user may wish for us to (re-)compute normals for our vertices. - */ -enum class ComputeNormalsOption { - NEVER, // do not ever compute any normals (results in broken glTF for some sources) - BROKEN, // replace zero-length normals in any mesh that has a normal layer - MISSING, // if a mesh lacks normals, compute them all - ALWAYS // compute a new normal for every vertex, obliterating whatever may have been there before -}; - -enum class UseLongIndicesOptions { - NEVER, // only ever use 16-bit indices - AUTO, // use shorts or longs depending on vertex count - ALWAYS, // only ever use 32-bit indices -}; - -/** - * User-supplied options that dictate the nature of the glTF being generated. - */ -struct GltfOptions -{ - /** - * If negative, disabled. Otherwise, a bitfield of RawVertexAttributes that - * specify the largest set of attributes that'll ever be kept for a vertex. - * The special bit RAW_VERTEX_ATTRIBUTE_AUTO triggers smart mode, where the - * attributes to keep are inferred from which textures are supplied. - */ - int keepAttribs { -1 }; - /** Whether to output a .glb file, the binary format of glTF. */ - bool outputBinary { false }; - /** If non-binary, whether to inline all resources, for a single (large) .glTF file. */ - bool embedResources { false }; - - /** Whether and how to use KHR_draco_mesh_compression to minimize static geometry size. */ - struct { - bool enabled = false; - int compressionLevel = -1; - int quantBitsPosition = -1; - int quantBitsTexCoord = -1; - int quantBitsNormal = -1; - int quantBitsColor = -1; - int quantBitsGeneric = -1; - } draco; - - /** Whether to use KHR_materials_unlit to extend materials definitions. */ - bool useKHRMatUnlit { false }; - /** Whether to populate the pbrMetallicRoughness substruct in materials. */ - bool usePBRMetRough { false }; - /** Whether to include blend shape normals, if present according to the SDK. */ - bool useBlendShapeNormals { false }; - /** Whether to include blend shape tangents, if present according to the SDK. */ - bool useBlendShapeTangents { false }; - /** When to compute vertex normals from geometry. */ - ComputeNormalsOption computeNormals = ComputeNormalsOption::BROKEN; - /** When to use 32-bit indices. */ - UseLongIndicesOptions useLongIndices = UseLongIndicesOptions::AUTO; -}; enum RawVertexAttribute {