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 {