From fdf7bb3336c0b580b0dba73af6a3e7299580bf4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A4r=20Winzell?= Date: Thu, 30 Nov 2017 22:22:31 -0800 Subject: [PATCH] Support for Stingray PBS material definitions (#47) This adds the first FBX PBR import path. Materials that have been exported via the Stingray PBS preset should be picked up as native metallic/roughness, and exported essentially 1:1 to the glTF output. In more detail, this commit: - (Re)introduces the STB header libraries as a dependency. We currently use it for reading and writing images. In time we may need a more dedicated PNG compression library. - Generalizes FbxMaterialAccess to return different subclasses of FbxMaterialInfo; currently FbxRoughMetMaterialInfo and FbxTraditionalMaterialInfo. - FbxTraditionalMaterialInfo is populated from the canonical FbxSurfaceMaterial classes. - FbxRoughMetMaterialInfo is currently populated through the Stingray PBS set of properties, further documented in the code. - RawMaterial was in turn generalized to feature a pluggable, type-specific RawMatProps struct; current implementations are, unsurprisingly, RawTraditionalMatProps and RawMetRoughMatProps. These are basically just lists of per-surface constants, e.g. diffuseFactor or roughness. - In the third phase, glTF generation, the bulk of the changes are concerned with creating packed textures of the type needed by e.g. the metallic-roughness struct, where one colour channel holds roughness and the other metallic. This is done with a somewhat pluggable "map source pixels to destination pixel" mechanism. More work will likely be needed here in the future to accomodate more demanding mappings. There's also a lot of code to convert from one representation to another. The most useful, but also the least well-supported conversion, is from old workflow (diffuse, specular, shininess) to metallic/roughness. Going from PBR spec/gloss to PBR met/rough is hard enough, but we go one step sillier and treat shininess as if it were glossiness, which it certainly isn't. More work is needed here! But it's still a fun proof of concept of sorts, and perhaps for some people it's useful to just get *something* into the PBR world. --- CMakeLists.txt | 12 + README.md | 65 +++-- src/Fbx2Raw.cpp | 367 +++++++++++++++++---------- src/Raw2Gltf.cpp | 504 ++++++++++++++++++++++++++++++++++---- src/RawModel.cpp | 33 +-- src/RawModel.h | 163 +++++++++--- src/glTF/MaterialData.cpp | 17 +- src/glTF/MaterialData.h | 5 +- 8 files changed, 903 insertions(+), 263 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 87ccd41..2bc327b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,16 @@ ExternalProject_Add(Json ) set(JSON_INCLUDE_DIR "${CMAKE_BINARY_DIR}/json/src/Json/src") +# stb +ExternalProject_Add(STB + PREFIX stb + GIT_REPOSITORY https://github.com/nothings/stb + CONFIGURE_COMMAND ${CMAKE_COMMAND} -E echo "Skipping STB configure step." + BUILD_COMMAND ${CMAKE_COMMAND} -E echo "Skipping STB build step." + INSTALL_COMMAND ${CMAKE_COMMAND} -E echo "Skipping STB install step." +) +set(STB_INCLUDE_DIR "${CMAKE_BINARY_DIR}/stb/src/STB") + # cppcodec ExternalProject_Add(CPPCodec PREFIX cppcodec @@ -151,6 +161,7 @@ add_dependencies(FBX2glTF MathFu FiFoMap Json + STB CxxOpts CPPCodec Fmt @@ -182,6 +193,7 @@ target_include_directories(FBX2glTF PUBLIC ${FIFO_MAP_INCLUDE_DIR} ${JSON_INCLUDE_DIR} ${CXXOPTS_INCLUDE_DIR} + ${STB_INCLUDE_DIR} ${CPPCODEC_INCLUDE_DIR} ${FMT_INCLUDE_DIR} ) diff --git a/README.md b/README.md index aa2ec16..baedf00 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,10 @@ Usage: --khr-materials-common (WIP) Use KHR_materials_common extensions to specify Unlit/Lambert/Blinn/Phong shaders. --pbr-metallic-roughness (WIP) Try to glean glTF 2.0 native PBR - attributes from the FBX. + attributes from the FBX, or make a best effort + to convert from traditional shader models. --pbr-specular-glossiness - (WIP) Experimentally fill in the + (WIP) Very experimentally employ the KHR_materials_pbrSpecularGlossiness extension. --blend-shape-normals Include blend shape normals, if reported present by the FBX SDK. @@ -192,50 +193,68 @@ With glTF 2.0, we leaped headlong into physically-based rendering (BPR), where the canonical way of expressing what a mesh looks like is by describing its visible material in fundamental attributes like "how rough is this surface". -By contrast, FBX's material support remains in the older world of Lambert and -Phong, with simpler and more direct illumination and shading models. These modes -are largely incompatible — for example, textures in the old workflow often -contain baked lighting, which would arise naturally in a PBR environment. +By contrast, FBX's material support remains largely in the older world of +Lambert and Phong, with simpler and more direct illumination and shading +models. These modes are inherently incompatible — for example, textures in the +old workflow often contain baked lighting of the type that would arise naturally +in a PBR environment. Some material settings remain well supported and transfer automatically: - Emissive constants and textures - Occlusion maps - Normal maps -This leaves the other traditional settings of Lambert: +This leaves the other traditional settings, first of Lambert: - Ambient — this is anathema in the PBR world, where such effects should emerge naturally from the fundamental colour of the material and any ambient lighting present. - Diffuse — the material's direction-agnostic, non-specular reflection, and additionally, with Blinn/Phong: - Specular — a more polished material's direction-sensitive reflection, - - Shininess — just how polished the material is, + - Shininess — just how polished the material is; a higher value here yields a + more mirror-like surface. (All these can be either constants or textures.) #### Exporting as Unlit/Lambert/Phong -Increasingly with PBR materials, these properties are just left at zero or -default values in the FBX. But when they're there, and they're how you want the -glTF materials generated, one option is to use the --khr-materials-common -command line switch, with the awareness that this incurs a required dependency -on the glTF extension `KHR_materials_common`. +If you have a model was constructed using the traditional workflow, you may +choose to export it using the --khr-materials-common switch. This incurs a +dependency on the glTF extension 'KHR_materials_common'; a client that accepts +that extension is making a promise it'll do its best to render i.e. Lambert or +Phong. + +You can use this flag even for PBR models, but the conversion is imperfect to +say the least, and there is no reason why you would ever want to do such a +thing. **Note that at the time of writing, this glTF extension is still undergoing the ratification process, and is furthermore likely to change names.** #### Exporting as Metallic-Roughness PBR -Given the command line flag --pbr-metallic-roughness, we accept glTF 2.0's PBR -mode, but we do so very partially, filling in a couple of reasonable constants -for metalness and roughness and using the diffuse texture, if it exists, as the -`base colour` texture. +Given the command line flag --pbr-metallic-roughness, we throw ourselves into +the warm embrace of glTF 2.0's PBR preference. -More work is needed to harness the power of glTF's 2.0's materials. The biggest -issue here is the lack of any obviously emerging standards to complement FBX -itself. It's not clear what format an artist can export their PBR materials on, -and when they can, how to communicate this information well to `FBX2glTF`. +As mentioned above, there is lilttle consensus in the world on how PBR should be +represented in FBX. At present, we support only one format: Stingray PBS. This +is a featue that comes bundled with Maya, and any PBR model exported through +that route should be digested propertly by FBX2glTF. -(*Stingray PBS* support is -[high on the TODO list](https://github.com/facebookincubator/FBX2glTF/issues/12).) +(A happy note: Allegorithmic's Susbstance Painter also exports Stingray PBS, +when hooked up to Maya.) + +If your model is not a Stingray PBS one, but you still wish to export PBR +(perhaps you want to generate only core glTF wirhout reliance on extensions), +this converter will try its best to convert your old textures. It calculates, on +a pixel by pixel basis, reasonable values for base colour, metallicness and +roughness, using your model's diffuse, specular, and shinines textures. + +It should noted here that this process cannot ever be perfect; this is very much +an apples and oranges situation. + +A note of gratitude here to Gary Hsu who developed the formulae we use for this +process. They can be eyeballed +[here](https://github.com/KhronosGroup/glTF/blob/master/extensions/Khronos/KHR_materials_pbrSpecularGlossiness/examples/convert-between-workflows/js/three.pbrUtilities.js) +for the curious. ## Draco Compression The tool will optionally apply [Draco](https://github.com/google/draco) diff --git a/src/Fbx2Raw.cpp b/src/Fbx2Raw.cpp index 9355faa..08b1440 100644 --- a/src/Fbx2Raw.cpp +++ b/src/Fbx2Raw.cpp @@ -88,46 +88,181 @@ private: const FbxLayerElementArrayTemplate *indices; }; -class FbxMaterialAccess -{ - struct FbxMaterialProperties { - FbxFileTexture *texAmbient {}; - FbxVector4 colAmbient {}; - FbxFileTexture *texSpecular {}; - FbxVector4 colSpecular {}; - FbxFileTexture *texDiffuse {}; - FbxVector4 colDiffuse {}; - FbxFileTexture *texEmissive {}; - FbxVector4 colEmissive {}; - FbxFileTexture *texNormal {}; - FbxFileTexture *texShininess {}; - FbxDouble shininess {}; - }; - -private: - const FbxSurfaceMaterial *fbxMaterial; - const std::map &textureLocations; - -public: +struct FbxMaterialInfo { + FbxMaterialInfo(const FbxString &name, const FbxString &shadingModel) + : name(name), + shadingModel(shadingModel) + {} const FbxString name; const FbxString shadingModel; +}; - const struct FbxMaterialProperties props; +struct FbxRoughMetMaterialInfo : FbxMaterialInfo { + static constexpr const char *FBX_SHADER_METROUGH = "MetallicRoughness"; - explicit FbxMaterialAccess( - const FbxSurfaceMaterial *fbxMaterial, const std::map &textureNames) : - fbxMaterial(fbxMaterial), - name(fbxMaterial->GetName()), - shadingModel(fbxMaterial->ShadingModel), - textureLocations(textureNames), - props(extractTextures()) + FbxRoughMetMaterialInfo(const FbxString &name, const FbxString &shadingModel) + : FbxMaterialInfo(name, shadingModel) + {} + const FbxFileTexture *texColor {}; + FbxVector4 colBase {}; + const FbxFileTexture *texNormal {}; + const FbxFileTexture *texMetallic {}; + FbxDouble metallic {}; + const FbxFileTexture *texRoughness {}; + FbxDouble roughness {}; + const FbxFileTexture *texEmissive {}; + FbxVector4 colEmissive {}; + FbxDouble emissiveIntensity {}; + const FbxFileTexture *texAmbientOcclusion {}; + + static std::unique_ptr From( + FbxSurfaceMaterial *fbxMaterial, + const std::map &textureLocations) + { + std::unique_ptr res(new FbxRoughMetMaterialInfo(fbxMaterial->GetName(), FBX_SHADER_METROUGH)); + + const FbxProperty mayaProp = fbxMaterial->FindProperty("Maya"); + if (mayaProp.GetPropertyDataType() != FbxCompoundDT) { + return nullptr; + } + if (!fbxMaterial->ShadingModel.Get().IsEmpty()) { + fmt::printf("Warning: Material %s has surprising shading model: %s\n", + fbxMaterial->GetName(), fbxMaterial->ShadingModel.Get()); + } + + auto getTex = [&](std::string propName) { + const FbxFileTexture *ptr = nullptr; + + const FbxProperty useProp = mayaProp.FindHierarchical(("use_" + propName + "_map").c_str()); + if (useProp.IsValid() && useProp.Get()) { + const FbxProperty texProp = mayaProp.FindHierarchical(("TEX_" + propName + "_map").c_str()); + if (texProp.IsValid()) { + ptr = texProp.GetSrcObject(); + if (ptr != nullptr && textureLocations.find(ptr) == textureLocations.end()) { + ptr = nullptr; + } + } + } else if (verboseOutput && useProp.IsValid()) { + fmt::printf("Note: Property '%s' of material '%s' exists, but is flagged as 'do not use'.\n", + propName, fbxMaterial->GetName()); + } + return ptr; + }; + + auto getVec = [&](std::string propName) -> FbxDouble3 { + const FbxProperty vecProp = mayaProp.FindHierarchical(propName.c_str()); + return vecProp.IsValid() ? vecProp.Get() : FbxDouble3(1, 1, 1); + }; + + auto getVal = [&](std::string propName) -> FbxDouble { + const FbxProperty vecProp = mayaProp.FindHierarchical(propName .c_str()); + return vecProp.IsValid() ? vecProp.Get() : 0; + }; + + res->texNormal = getTex("normal"); + res->texColor = getTex("color"); + res->colBase = getVec("base_color"); + res->texAmbientOcclusion = getTex("ao"); + res->texEmissive = getTex("emissive"); + res->colEmissive = getVec("emissive"); + res->emissiveIntensity = getVal("emissive_intensity"); + res->texMetallic = getTex("metallic"); + res->metallic = getVal("metallic"); + res->texRoughness = getTex("roughness"); + res->roughness = getVal("roughness"); + + return res; + } +}; + +struct FbxTraditionalMaterialInfo : FbxMaterialInfo { + static constexpr const char *FBX_SHADER_LAMBERT = "Lambert"; + static constexpr const char *FBX_SHADER_BLINN = "Blinn"; + static constexpr const char *FBX_SHADER_PHONG = "Phong"; + + FbxTraditionalMaterialInfo(const FbxString &name, const FbxString &shadingModel) + : FbxMaterialInfo(name, shadingModel) {} - struct FbxMaterialProperties extractTextures() { - struct FbxMaterialProperties res; + FbxFileTexture *texAmbient {}; + FbxVector4 colAmbient {}; + FbxFileTexture *texSpecular {}; + FbxVector4 colSpecular {}; + FbxFileTexture *texDiffuse {}; + FbxVector4 colDiffuse {}; + FbxFileTexture *texEmissive {}; + FbxVector4 colEmissive {}; + FbxFileTexture *texNormal {}; + FbxFileTexture *texShininess {}; + FbxDouble shininess {}; + + static std::unique_ptr From( + FbxSurfaceMaterial *fbxMaterial, + const std::map &textureLocations) + { + auto getSurfaceScalar = [&](const char *propName) -> std::tuple { + const FbxProperty prop = fbxMaterial->FindProperty(propName); + + FbxDouble val(0); + FbxFileTexture *tex = prop.GetSrcObject(); + if (tex != nullptr && textureLocations.find(tex) == textureLocations.end()) { + tex = nullptr; + } + if (tex == nullptr && prop.IsValid()) { + val = prop.Get(); + } + return std::make_tuple(val, tex); + }; + + auto getSurfaceVector = [&](const char *propName) -> std::tuple { + const FbxProperty prop = fbxMaterial->FindProperty(propName); + + FbxDouble3 val(1, 1, 1); + FbxFileTexture *tex = prop.GetSrcObject(); + if (tex != nullptr && textureLocations.find(tex) == textureLocations.end()) { + tex = nullptr; + } + if (tex == nullptr && prop.IsValid()) { + val = prop.Get(); + } + return std::make_tuple(val, tex); + }; + + auto getSurfaceValues = [&](const char *colName, const char *facName) -> std::tuple { + const FbxProperty colProp = fbxMaterial->FindProperty(colName); + const FbxProperty facProp = fbxMaterial->FindProperty(facName); + + FbxDouble3 colorVal(1, 1, 1); + FbxDouble factorVal(1); + + FbxFileTexture *colTex = colProp.GetSrcObject(); + if (colTex != nullptr && textureLocations.find(colTex) == textureLocations.end()) { + colTex = nullptr; + } + if (colTex == nullptr && colProp.IsValid()) { + colorVal = colProp.Get(); + } + FbxFileTexture *facTex = facProp.GetSrcObject(); + if (facTex != nullptr && textureLocations.find(facTex) == textureLocations.end()) { + facTex = nullptr; + } + if (facTex == nullptr && facProp.IsValid()) { + factorVal = facProp.Get(); + } + + auto val = FbxVector4( + colorVal[0] * factorVal, + colorVal[1] * factorVal, + colorVal[2] * factorVal, + factorVal); + return std::make_tuple(val, colTex, facTex); + }; + + std::string name = fbxMaterial->GetName(); + std::unique_ptr res(new FbxTraditionalMaterialInfo(name.c_str(), fbxMaterial->ShadingModel.Get())); // four properties are on the same structure and follow the same rules - auto handleBasicProperty = [&](const char *colName, const char *facName) { + auto handleBasicProperty = [&](const char *colName, const char *facName) -> std::tuple{ FbxFileTexture *colTex, *facTex; FbxVector4 vec; @@ -141,20 +276,20 @@ public: return std::make_tuple(vec, facTex); }; - std::tie(res.colAmbient, res.texAmbient) = + std::tie(res->colAmbient, res->texAmbient) = handleBasicProperty(FbxSurfaceMaterial::sAmbient, FbxSurfaceMaterial::sAmbientFactor); - std::tie(res.colSpecular, res.texSpecular) = + std::tie(res->colSpecular, res->texSpecular) = handleBasicProperty(FbxSurfaceMaterial::sSpecular, FbxSurfaceMaterial::sSpecularFactor); - std::tie(res.colDiffuse, res.texDiffuse) = + std::tie(res->colDiffuse, res->texDiffuse) = handleBasicProperty(FbxSurfaceMaterial::sDiffuse, FbxSurfaceMaterial::sDiffuseFactor); - std::tie(res.colEmissive, res.texEmissive) = + std::tie(res->colEmissive, res->texEmissive) = handleBasicProperty(FbxSurfaceMaterial::sEmissive, FbxSurfaceMaterial::sEmissiveFactor); // the normal map can only ever be a map, ignore everything else - std::tie(std::ignore, res.texNormal) = getSurfaceVector(FbxSurfaceMaterial::sNormalMap); + std::tie(std::ignore, res->texNormal) = getSurfaceVector(FbxSurfaceMaterial::sNormalMap); // shininess can be a map or a factor - std::tie(res.shininess, res.texShininess) = getSurfaceScalar(FbxSurfaceMaterial::sShininess); + std::tie(res->shininess, res->texShininess) = getSurfaceScalar(FbxSurfaceMaterial::sShininess); // for transparency we just want a constant vector value; FbxVector4 transparency; @@ -169,73 +304,24 @@ public: fmt::printf("Warning: Mat [%s]: Can't handle texture for %s; discarding.\n", name, FbxSurfaceMaterial::sTransparencyFactor); } // FBX color is RGB, so we calculate the A channel as the average of the FBX transparency color vector - res.colDiffuse[3] = 1.0 - (transparency[0] + transparency[1] + transparency[2])/3.0; + res->colDiffuse[3] = 1.0 - (transparency[0] + transparency[1] + transparency[2])/3.0; return res; } - - std::tuple getSurfaceScalar(const char *propName) const - { - const FbxProperty prop = fbxMaterial->FindProperty(propName); - - FbxDouble val(0); - FbxFileTexture *tex = prop.GetSrcObject(); - if (tex != nullptr && textureLocations.find(tex) == textureLocations.end()) { - tex = nullptr; - } - if (tex == nullptr && prop.IsValid()) { - val = prop.Get(); - } - return std::make_tuple(val, tex); - } - - std::tuple getSurfaceVector(const char *propName) const - { - const FbxProperty prop = fbxMaterial->FindProperty(propName); - - FbxDouble3 val(1, 1, 1); - FbxFileTexture *tex = prop.GetSrcObject(); - if (tex != nullptr && textureLocations.find(tex) == textureLocations.end()) { - tex = nullptr; - } - if (tex == nullptr && prop.IsValid()) { - val = prop.Get(); - } - return std::make_tuple(val, tex); - } - - std::tuple getSurfaceValues(const char *colName, const char *facName) const - { - const FbxProperty colProp = fbxMaterial->FindProperty(colName); - const FbxProperty facProp = fbxMaterial->FindProperty(facName); - - FbxDouble3 colorVal(1, 1, 1); - FbxDouble factorVal(1); - - FbxFileTexture *colTex = colProp.GetSrcObject(); - if (colTex != nullptr && textureLocations.find(colTex) == textureLocations.end()) { - colTex = nullptr; - } - if (colTex == nullptr && colProp.IsValid()) { - colorVal = colProp.Get(); - } - FbxFileTexture *facTex = facProp.GetSrcObject(); - if (facTex != nullptr && textureLocations.find(facTex) == textureLocations.end()) { - facTex = nullptr; - } - if (facTex == nullptr && facProp.IsValid()) { - factorVal = facProp.Get(); - } - - auto val = FbxVector4( - colorVal[0] * factorVal, - colorVal[1] * factorVal, - colorVal[2] * factorVal, - factorVal); - return std::make_tuple(val, colTex, facTex); - }; }; + +std::unique_ptr +GetMaterialInfo(FbxSurfaceMaterial *material, const std::map &textureLocations) +{ + std::unique_ptr res; + res = FbxRoughMetMaterialInfo::From(material, textureLocations); + if (!res) { + res = FbxTraditionalMaterialInfo::From(material, textureLocations); + } + return res; +} + class FbxMaterialsAccess { public: @@ -273,14 +359,14 @@ public: } auto summary = summaries[materialNum]; if (summary == nullptr) { - summary = summaries[materialNum] = std::make_shared( + summary = summaries[materialNum] = GetMaterialInfo( mesh->GetNode()->GetSrcObject(materialNum), textureLocations); } } } - const std::shared_ptr GetMaterial(const int polygonIndex) const + const std::shared_ptr GetMaterial(const int polygonIndex) const { if (mappingMode != FbxGeometryElement::eNone) { const int materialNum = indices->GetAt((mappingMode == FbxGeometryElement::eByPolygon) ? polygonIndex : 0); @@ -293,10 +379,10 @@ public: } private: - FbxGeometryElement::EMappingMode mappingMode; - std::vector> summaries {}; - const FbxMesh *mesh; - const FbxLayerElementArrayTemplate *indices; + FbxGeometryElement::EMappingMode mappingMode; + std::vector> summaries {}; + const FbxMesh *mesh; + const FbxLayerElementArrayTemplate *indices; }; class FbxSkinningAccess @@ -699,27 +785,23 @@ static void ReadMesh(RawModel &raw, FbxScene *pScene, FbxNode *pNode, const std: for (int polygonIndex = 0; polygonIndex < pMesh->GetPolygonCount(); polygonIndex++) { FBX_ASSERT(pMesh->GetPolygonSize(polygonIndex) == 3); - - const std::shared_ptr fbxMaterial = materials.GetMaterial(polygonIndex); + const std::shared_ptr fbxMaterial = materials.GetMaterial(polygonIndex); int textures[RAW_TEXTURE_USAGE_MAX]; - std::fill_n(textures, (int)RAW_TEXTURE_USAGE_MAX, -1); + std::fill_n(textures, (int) RAW_TEXTURE_USAGE_MAX, -1); - FbxString shadingModel, materialName; - FbxVector4 ambient, specular, diffuse, emissive; - FbxDouble shininess; + std::shared_ptr rawMatProps; + FbxString materialName; if (fbxMaterial == nullptr) { materialName = "DefaultMaterial"; - shadingModel = "Lambert"; + rawMatProps.reset(new RawTraditionalMatProps(RAW_SHADING_MODEL_LAMBERT, + Vec3f(0, 0, 0), Vec4f(.5, .5, .5, 1), Vec3f(0, 0, 0), Vec3f(0, 0, 0), 0.5)); } else { materialName = fbxMaterial->name; - shadingModel = fbxMaterial->shadingModel; - const auto &matProps = fbxMaterial->props; - - const auto maybeAddTexture = [&](FbxFileTexture *tex, RawTextureUsage usage) { + const auto maybeAddTexture = [&](const FbxFileTexture *tex, RawTextureUsage usage) { if (tex != nullptr) { // dig out the inferred filename from the textureLocations map FbxString inferredPath = textureLocations.find(tex)->second; @@ -727,19 +809,44 @@ static void ReadMesh(RawModel &raw, FbxScene *pScene, FbxNode *pNode, const std: } }; - ambient = matProps.colAmbient; - maybeAddTexture(matProps.texAmbient, RAW_TEXTURE_USAGE_AMBIENT); - specular = matProps.colSpecular; - maybeAddTexture(matProps.texSpecular, RAW_TEXTURE_USAGE_SPECULAR); - diffuse = matProps.colDiffuse; - maybeAddTexture(matProps.texDiffuse, RAW_TEXTURE_USAGE_DIFFUSE); - emissive = matProps.colEmissive; - maybeAddTexture(matProps.texEmissive, RAW_TEXTURE_USAGE_EMISSIVE); + std::shared_ptr matInfo; + if (fbxMaterial->shadingModel == FbxRoughMetMaterialInfo::FBX_SHADER_METROUGH) { + FbxRoughMetMaterialInfo *fbxMatInfo = static_cast(fbxMaterial.get()); - maybeAddTexture(matProps.texNormal, RAW_TEXTURE_USAGE_NORMAL); + maybeAddTexture(fbxMatInfo->texColor, RAW_TEXTURE_USAGE_ALBEDO); + maybeAddTexture(fbxMatInfo->texNormal, RAW_TEXTURE_USAGE_NORMAL); + maybeAddTexture(fbxMatInfo->texEmissive, RAW_TEXTURE_USAGE_EMISSIVE); + maybeAddTexture(fbxMatInfo->texRoughness, RAW_TEXTURE_USAGE_ROUGHNESS); + maybeAddTexture(fbxMatInfo->texMetallic, RAW_TEXTURE_USAGE_METALLIC); + maybeAddTexture(fbxMatInfo->texAmbientOcclusion, RAW_TEXTURE_USAGE_OCCLUSION); + rawMatProps.reset(new RawMetRoughMatProps( + RAW_SHADING_MODEL_PBR_MET_ROUGH, toVec4f(fbxMatInfo->colBase), toVec3f(fbxMatInfo->colEmissive), + fbxMatInfo->emissiveIntensity, fbxMatInfo->metallic, fbxMatInfo->roughness)); + } else { - shininess = matProps.shininess; - maybeAddTexture(matProps.texShininess, RAW_TEXTURE_USAGE_SHININESS); + FbxTraditionalMaterialInfo *fbxMatInfo = static_cast(fbxMaterial.get()); + RawShadingModel shadingModel; + if (fbxMaterial->shadingModel == "Lambert") { + shadingModel = RAW_SHADING_MODEL_LAMBERT; + } else if (fbxMaterial->shadingModel == "Blinn") { + shadingModel = RAW_SHADING_MODEL_BLINN; + } else if (fbxMaterial->shadingModel == "Phong") { + shadingModel = RAW_SHADING_MODEL_PHONG; + } else if (fbxMaterial->shadingModel == "Constant") { + shadingModel = RAW_SHADING_MODEL_PHONG; + } else { + shadingModel = RAW_SHADING_MODEL_UNKNOWN; + } + maybeAddTexture(fbxMatInfo->texDiffuse, RAW_TEXTURE_USAGE_DIFFUSE); + maybeAddTexture(fbxMatInfo->texNormal, RAW_TEXTURE_USAGE_NORMAL); + maybeAddTexture(fbxMatInfo->texEmissive, RAW_TEXTURE_USAGE_EMISSIVE); + maybeAddTexture(fbxMatInfo->texShininess, RAW_TEXTURE_USAGE_SHININESS); + maybeAddTexture(fbxMatInfo->texAmbient, RAW_TEXTURE_USAGE_AMBIENT); + maybeAddTexture(fbxMatInfo->texSpecular, RAW_TEXTURE_USAGE_SPECULAR); + rawMatProps.reset(new RawTraditionalMatProps(shadingModel, + toVec3f(fbxMatInfo->colAmbient), toVec4f(fbxMatInfo->colDiffuse), toVec3f(fbxMatInfo->colEmissive), + toVec3f(fbxMatInfo->colSpecular), fbxMatInfo->shininess)); + } } RawVertex rawVertices[3]; @@ -867,9 +974,7 @@ static void ReadMesh(RawModel &raw, FbxScene *pScene, FbxNode *pNode, const std: } const RawMaterialType materialType = GetMaterialType(raw, textures, vertexTransparency, skinning.IsSkinned()); - const int rawMaterialIndex = raw.AddMaterial( - materialName, shadingModel, materialType, textures, - toVec3f(ambient), toVec4f(diffuse), toVec3f(specular), toVec3f(emissive), shininess); + const int rawMaterialIndex = raw.AddMaterial(materialName, materialType, textures, rawMatProps); raw.AddTriangle(rawVertexIndices[0], rawVertexIndices[1], rawVertexIndices[2], rawMaterialIndex, rawSurfaceIndex); } diff --git a/src/Raw2Gltf.cpp b/src/Raw2Gltf.cpp index 19f2e96..3860c95 100644 --- a/src/Raw2Gltf.cpp +++ b/src/Raw2Gltf.cpp @@ -12,6 +12,11 @@ #include #include +#define STB_IMAGE_IMPLEMENTATION +#include +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include + #include "FBX2glTF.h" #include "utils/String_Utils.h" #include "utils/Image_Utils.h" @@ -217,13 +222,21 @@ struct GLTFData Holder scenes; }; +template +static void WriteToVectorContext(void *context, void *data, int size) { + std::vector *vec = static_cast *>(context); + for (int ii = 0; ii < size; ii ++) { + vec->emplace_back(((T *) 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 * classes are guaranteed to stick around for the duration of the process. */ template -T &require(std::map> map, std::string key) +T &require(std::map> map, const std::string &key) { auto iter = map.find(key); assert(iter != map.end()); @@ -269,7 +282,7 @@ ModelData *Raw2Gltf( for (int i = 0; i < raw.GetMaterialCount(); i++) { fmt::printf( "Material %d: %s [shading: %s]\n", i, raw.GetMaterial(i).name.c_str(), - raw.GetMaterial(i).shadingModel.c_str()); + Describe(raw.GetMaterial(i).info->shadingModel)); } if (raw.GetVertexCount() > 2 * raw.GetTriangleCount()) { fmt::printf( @@ -293,6 +306,7 @@ ModelData *Raw2Gltf( std::map> nodesByName; std::map> materialsByName; + std::map> textureByIndicesKey; std::map> meshBySurfaceId; // for now, we only have one buffer; data->binary points to the same vector as that BufferData does. @@ -378,24 +392,189 @@ ModelData *Raw2Gltf( // // textures // + typedef std::array pixel; // pixel components are floats in [0, 1] + typedef std::function)> pixel_merger; - for (int textureIndex = 0; textureIndex < raw.GetTextureCount(); textureIndex++) { - const RawTexture &texture = raw.GetTexture(textureIndex); - const std::string textureName = Gltf::StringUtils::GetFileBaseString(texture.name); - const std::string relativeFilename = Gltf::StringUtils::GetFileNameString(texture.fileLocation); + auto texIndicesKey = [&](std::vector ixVec, std::string tag) -> std::string { + std::string result = tag; + for (int ix : ixVec) { + result += "_"; + result += ix; + } + return result; + }; - ImageData *source = nullptr; + /** + * 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 + ) -> 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 mergedName = tag; + 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 &fileLocBase = Gltf::StringUtils::GetFileBaseString(Gltf::StringUtils::GetFileNameString(fileLoc)); + if (!fileLoc.empty()) { + info.pixels = stbi_load(fileLoc.c_str(), &info.width, &info.height, &info.channels, 4); + if (!info.pixels) { + fmt::printf("Warning: merge texture [%d](%s) could not be loaded.\n", + rawTexIx, + Gltf::StringUtils::GetFileBaseString(Gltf::StringUtils::GetFileNameString(fileLoc))); + } 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", + Gltf::StringUtils::GetFileBaseString(Gltf::StringUtils::GetFileNameString(fileLoc)), + info.width, info.height, width, height); + // this is bad enough that we abort the whole merge + return nullptr; + } + mergedName += "_" + rawTex.fileName; + mergedFilename += "_" + fileLocBase; + } + } + } + texes.push_back(info); + } + + if (width < 0) { + // no textures to merge; bail + return nullptr; + } + // TODO: which channel combinations make sense in input files? + + std::vector mergedPixels(4 * width * height); + std::vector pixels(texes.size()); + std::vector pixelPointers(texes.size()); + for (int ii = 0; ii < mergedPixels.size(); ii += 4) { + pixels.clear(); + for (int jj = 0; jj < texes.size(); jj ++) { + const TexInfo &tex = texes[jj]; + if (tex.pixels != nullptr) { + pixels[jj] = { tex.pixels[ii+0]/255.0f, tex.pixels[ii+1]/255.0f, tex.pixels[ii+2]/255.0f, tex.pixels[ii+3]/255.0f }; + } else { + // absent textures are to be treated as all ones + pixels[jj] = { 1.0f, 1.0f, 1.0f, 1.0f }; + } + pixelPointers[jj] = &pixels[jj]; + } + const pixel &merged = combine(pixelPointers); + for (int jj = 0; jj < 4; jj ++) { + mergedPixels[ii + jj] = static_cast(fmax(0, fmin(255.0f, merged[jj] * 255.0f))); + } + } + + bool png = true; + std::vector imgBuffer; + int res; + if (png) { + res = stbi_write_png_to_func(WriteToVectorContext, &imgBuffer, width, height, 4, mergedPixels.data(), width * 4); + } else { + res = stbi_write_jpg_to_func(WriteToVectorContext, &imgBuffer, width, height, 4, mergedPixels.data(), 80); + } + if (!res) { + fmt::printf("Warning: failed to generate merge texture '%s'.\n", mergedFilename); + return nullptr; + } + + ImageData *image; if (options.outputBinary) { - auto bufferView = gltf->AddBufferViewForFile(buffer, texture.fileLocation); + 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 = Gltf::StringUtils::GetFileBaseString(rawTexture.name); + const std::string relativeFilename = Gltf::StringUtils::GetFileNameString(rawTexture.fileLocation); + + ImageData *image = nullptr; + if (options.outputBinary) { + auto bufferView = gltf->AddBufferViewForFile(buffer, rawTexture.fileLocation); if (bufferView) { - std::string suffix = Gltf::StringUtils::GetFileSuffixString(texture.fileLocation); - source = new ImageData(relativeFilename, *bufferView, suffixToMimeType(suffix)); + std::string suffix = Gltf::StringUtils::GetFileSuffixString(rawTexture.fileLocation); + image = new ImageData(relativeFilename, *bufferView, suffixToMimeType(suffix)); } } else if (!relativeFilename.empty()) { - source = new ImageData(relativeFilename, relativeFilename); + image = new ImageData(relativeFilename, relativeFilename); std::string outputPath = outputFolder + relativeFilename; - if (FileUtils::CopyFile(texture.fileLocation, outputPath)) { + if (FileUtils::CopyFile(rawTexture.fileLocation, outputPath)) { if (verboseOutput) { fmt::printf("Copied texture '%s' to output folder: %s\n", textureName, outputPath); } @@ -405,15 +584,16 @@ ModelData *Raw2Gltf( // reference, even if the copy failed. } } - if (!source) { + if (!image) { // fallback is tiny transparent gif - source = new ImageData(textureName, "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="); + image = new ImageData(textureName, "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="); } - const TextureData &texDat = *gltf->textures.hold( - new TextureData(textureName, defaultSampler, *gltf->images.hold(source))); - assert(texDat.ix == textureIndex); - } + std::shared_ptr texDat = gltf->textures.hold( + new TextureData(textureName, defaultSampler, *gltf->images.hold(image))); + textureByIndicesKey.insert(std::make_pair(key, texDat)); + return texDat; + }; // // materials @@ -425,47 +605,289 @@ ModelData *Raw2Gltf( material.type == RAW_MATERIAL_TYPE_TRANSPARENT || material.type == RAW_MATERIAL_TYPE_SKINNED_TRANSPARENT; - // find a texture by usage and return it as a TextureData*, or nullptr if none exists. - auto getTex = [&](RawTextureUsage usage) - { - // note that we depend on TextureData.ix == rawTexture's index - return (material.textures[usage] >= 0) ? gltf->textures.ptrs[material.textures[usage]].get() : nullptr; + Vec3f emissiveFactor; + float emissiveIntensity; + + const Vec3f dielectric(0.04f, 0.04f, 0.04f); + const float epsilon = 1e-6f; + + // 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; + }; + + // 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 + ) -> std::shared_ptr { + return getDerivedTexture({ material.textures[u1], material.textures[u2] }, combine, tag); + }; + + // 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 + ) -> std::shared_ptr { + return getDerivedTexture({ material.textures[u1], material.textures[u2], material.textures[u3] }, combine, tag); + }; + + auto getMaxComponent = [&](const Vec3f &color) { + return fmax(color.x, fmax(color.y, color.z)); + }; + auto getPerceivedBrightness = [&](const Vec3f &color) { + return sqrt(0.299 * color.x * color.x + 0.587 * color.y * color.y + 0.114 * color.z * color.z); + }; + auto toVec3f = [&](const pixel &pix) -> const Vec3f { + return Vec3f(pix[0], pix[1], pix[2]); + }; + auto toVec4f = [&](const pixel &pix) -> const Vec4f { + return Vec4f(pix[0], pix[1], pix[2], pix[3]); }; std::shared_ptr pbrMetRough; if (options.usePBRMetRough) { - pbrMetRough.reset(new PBRMetallicRoughness(getTex(RAW_TEXTURE_USAGE_DIFFUSE), material.diffuseFactor)); + // albedo is a basic texture, no merging needed + std::shared_ptr baseColorTex, metRoughTex; + + Vec4f diffuseFactor; + float metallic, roughness; + if (material.info->shadingModel == RAW_SHADING_MODEL_PBR_MET_ROUGH) { + /** + * PBR FBX Material -> PBR Met/Rough glTF. + * + * METALLIC and ROUGHNESS textures are packed in G and B channels of a rough/met texture. + * Other values translate directly. + */ + RawMetRoughMatProps *props = (RawMetRoughMatProps *) material.info.get(); + // merge metallic into the blue channel and roughness into the green, of a new combinatory texture + metRoughTex = merge2Tex("met_rough", + RAW_TEXTURE_USAGE_METALLIC, RAW_TEXTURE_USAGE_ROUGHNESS, + [&](const std::vector pixels) -> pixel { return { 0, (*pixels[1])[0], (*pixels[0])[0], 0 }; }); + baseColorTex = simpleTex(RAW_TEXTURE_USAGE_ALBEDO); + diffuseFactor = props->diffuseFactor; + metallic = props->metallic; + roughness = props->roughness; + emissiveFactor = props->emissiveFactor; + emissiveIntensity = props->emissiveIntensity; + } else { + /** + * Traditional FBX Material -> PBR Met/Rough glTF. + * + * Diffuse channel is used as base colour. No metallic/roughness texture is attempted. Constant + * metallic/roughness values are hard-coded. + * + * TODO: If we have specular & optional shininess map, we can generate a reasonable rough/met map. + */ + const RawTraditionalMatProps *props = ((RawTraditionalMatProps *) material.info.get()); + diffuseFactor = props->diffuseFactor; + // TODO: make configurable on the command line, or do a better job guessing from other, supplied params + if (material.info->shadingModel == RAW_SHADING_MODEL_LAMBERT) { + metallic = 0.6f; + roughness = 1.0f; + } else { + metallic = 0.2f; + roughness = 0.6f; + } + auto solveMetallic = [&](float pDiff, float pSpec, float oneMinusSpecularStrength) + { + if (pSpec < dielectric.x) { + return 0.0; + } + + float a = dielectric.x; + float b = pDiff * oneMinusSpecularStrength / (1 - dielectric.x) + pSpec - 2 * dielectric.x; + float c = dielectric.x - pSpec; + float D = fmax(b * b - 4 * a * c, 0); + return fmax(0.0, fmin(1.0, (-b + sqrt(D)) / (2 * a))); + }; + metRoughTex = merge3Tex("rough_met", + RAW_TEXTURE_USAGE_SPECULAR, RAW_TEXTURE_USAGE_SHININESS, RAW_TEXTURE_USAGE_DIFFUSE, + [&](const std::vector pixels) -> pixel { + const Vec3f specular = pixels[0] ? toVec3f(*pixels[0]) : props->specularFactor; + float shininess = pixels[1] ? (*pixels[1])[0] : props->shininess; + const Vec4f diffuse = pixels[2] ? toVec4f(*pixels[2]) : props->diffuseFactor; + + float pixelMet = solveMetallic( + getPerceivedBrightness(diffuse.xyz()), + getPerceivedBrightness(specular), + 1 - getMaxComponent(specular)); + float pixelRough = 1 - shininess; + + return { 0, pixelRough, pixelMet, 0 }; + }); + if (material.textures[RAW_TEXTURE_USAGE_DIFFUSE] >= 0) { + baseColorTex = merge2Tex("base_col", + RAW_TEXTURE_USAGE_DIFFUSE, RAW_TEXTURE_USAGE_SPECULAR, + [&](const std::vector pixels) -> pixel { + const Vec4f diffuse = pixels[0] ? toVec4f(*pixels[0]) : props->diffuseFactor; + const Vec3f specular = pixels[1] ? toVec3f(*pixels[1]) : props->specularFactor; + + float oneMinus = 1 - getMaxComponent(specular); + + float pixelMet = solveMetallic( + getPerceivedBrightness(diffuse.xyz()), + getPerceivedBrightness(specular), + oneMinus); + + Vec3f fromDiffuse = diffuse.xyz() * (oneMinus / (1.0 - dielectric.x) / fmax(1.0 - pixelMet, epsilon)); + Vec3f fromSpecular = specular - dielectric * (1.0 - pixelMet) * (1.0 / fmax(pixelMet, epsilon)); + Vec3f baseColor = Vec3f::Lerp(fromDiffuse, fromSpecular, pixelMet * pixelMet); + + return { baseColor[0], baseColor[1], baseColor[2], diffuse[3] }; + }); + } + emissiveFactor = props->emissiveFactor; + emissiveIntensity = 1.0f; + } + pbrMetRough.reset(new PBRMetallicRoughness(baseColorTex.get(), metRoughTex.get(), diffuseFactor, metallic, roughness)); } + std::shared_ptr pbrSpecGloss; if (options.usePBRSpecGloss) { + Vec4f diffuseFactor; + Vec3f specularFactor; + float glossiness; + std::shared_ptr specGlossTex; + std::shared_ptr diffuseTex; + + const Vec3f dielectric(0.04f, 0.04f, 0.04f); + const float epsilon = 1e-6f; + if (material.info->shadingModel == RAW_SHADING_MODEL_PBR_MET_ROUGH) { + /** + * PBR FBX Material -> PBR Spec/Gloss glTF. + * + * TODO: A complete mess. Low priority. + */ + RawMetRoughMatProps *props = (RawMetRoughMatProps *) material.info.get(); + // we can estimate spec/gloss from met/rough by between Vec4f(0.04, 0.04, 0.04) and baseColor + // according to metalness, and then taking gloss to be the inverse of roughness + specGlossTex = merge3Tex("specgloss", + RAW_TEXTURE_USAGE_ALBEDO, RAW_TEXTURE_USAGE_METALLIC, RAW_TEXTURE_USAGE_ROUGHNESS, + [&](const std::vector pixels) -> pixel { + Vec3f baseColor(1.0f); + if (pixels[0]) { + baseColor.x = (*pixels[0])[0]; + baseColor.y = (*pixels[0])[1]; + baseColor.z = (*pixels[0])[2]; + } + float metallic = pixels[1] ? (*pixels[1])[0] : 1.0f; + float roughness = pixels[2] ? (*pixels[2])[0] : 1.0f; + Vec3f spec = Vec3f::Lerp(dielectric, baseColor, metallic); + return { spec[0], spec[1], spec[2], 1.0f - roughness }; + }); + diffuseTex = merge2Tex("albedo", + RAW_TEXTURE_USAGE_ALBEDO, RAW_TEXTURE_USAGE_METALLIC, + [&](const std::vector pixels) -> pixel { + Vec3f baseColor(1.0f); + float alpha = 1.0f; + if (pixels[0]) { + baseColor[0] = (*pixels[0])[0]; + baseColor[1] = (*pixels[0])[1]; + baseColor[2] = (*pixels[0])[2]; + alpha = (*pixels[0])[3]; + } + float metallic = pixels[1] ? (*pixels[1])[0] : 1.0f; + Vec3f spec = Vec3f::Lerp(dielectric, baseColor, metallic); + float maxSpecComp = fmax(fmax(spec.x, spec.y), spec.z); + // attenuate baseColor to get specgloss-compliant diffuse; for details consult + // https://github.com/KhronosGroup/glTF/tree/master/extensions/Khronos/KHR_materials_pbrSpecularGlossiness + Vec3f diffuse = baseColor * (1 - dielectric[0]) * (1 - metallic) * fmax(1.0f - maxSpecComp, epsilon); + return { diffuse[0], diffuse[1], diffuse[2], alpha }; + }); + diffuseFactor = props->diffuseFactor; + specularFactor = Vec3f::Lerp(dielectric, props->diffuseFactor.xyz(), props->metallic); + glossiness = 1.0f - props->roughness; + + } else { + /** + * Traditional FBX Material -> PBR Spec/Gloss glTF. + * + * TODO: A complete mess. Low priority. + */ + // TODO: this section a ludicrous over-simplifictation; we can surely do better. + const RawTraditionalMatProps *props = ((RawTraditionalMatProps *) material.info.get()); + specGlossTex = merge2Tex("specgloss", + RAW_TEXTURE_USAGE_SPECULAR, RAW_TEXTURE_USAGE_SHININESS, + [&](const std::vector pixels) -> pixel { + const auto &spec = *(pixels[0]); + const auto &shine = *(pixels[1]); + return { spec[0], spec[1], spec[2], shine[0] }; + }); + diffuseTex = simpleTex(RAW_TEXTURE_USAGE_DIFFUSE); + diffuseFactor = props->diffuseFactor; + specularFactor = props->specularFactor; + glossiness = props->shininess; + } + pbrSpecGloss.reset( new PBRSpecularGlossiness( - getTex(RAW_TEXTURE_USAGE_DIFFUSE), material.diffuseFactor, - getTex(RAW_TEXTURE_USAGE_SPECULAR), material.specularFactor, material.shininess)); + diffuseTex.get(), diffuseFactor, specGlossTex.get(), specularFactor, glossiness)); } std::shared_ptr khrComMat; if (options.useKHRMatCom) { - auto type = KHRCommonMats::MaterialType::Constant; - if (material.shadingModel == "Lambert") { - type = KHRCommonMats::MaterialType::Lambert; - } else if (material.shadingModel == "Blinn") { - type = KHRCommonMats::MaterialType::Blinn; - } else if (material.shadingModel == "Phong") { - type = KHRCommonMats::MaterialType::Phong; + float shininess; + Vec3f ambientFactor, specularFactor; + Vec4f diffuseFactor; + std::shared_ptr diffuseTex; + auto type = KHRCommonMats::MaterialType::Constant; + + if (material.info->shadingModel == RAW_SHADING_MODEL_PBR_MET_ROUGH) { + /** + * PBR FBX Material -> KHR Common Materials glTF. + * + * TODO: We can use the specularFactor calculation below to generate a reasonable specular map, too. + */ + const RawMetRoughMatProps *props = (RawMetRoughMatProps *) material.info.get(); + shininess = 1.0f - props->roughness; + ambientFactor = Vec3f(0.0f, 0.0f, 0.0f); + diffuseTex = simpleTex(RAW_TEXTURE_USAGE_ALBEDO); + diffuseFactor = props->diffuseFactor; + specularFactor = Vec3f::Lerp(Vec3f(0.04f, 0.04f, 0.04f), props->diffuseFactor.xyz(), props->metallic); + // render as Phong if there's any specularity, otherwise Lambert + type = (specularFactor.LengthSquared() > 1e-4) ? + KHRCommonMats::MaterialType::Phong : KHRCommonMats::MaterialType::Lambert; + // TODO: convert textures + + } else { + /** + * Traditional FBX Material -> KHR Common Materials glTF. + * + * Should be in good shape. Essentially pass-through. + */ + const RawTraditionalMatProps *props = (RawTraditionalMatProps *) material.info.get(); + shininess = props->shininess; + ambientFactor = props->ambientFactor; + diffuseTex = simpleTex(RAW_TEXTURE_USAGE_DIFFUSE); + diffuseFactor = props->diffuseFactor; + specularFactor = props->specularFactor; + + if (material.info->shadingModel == RAW_SHADING_MODEL_LAMBERT) { + type = KHRCommonMats::MaterialType::Lambert; + } else if (material.info->shadingModel == RAW_SHADING_MODEL_BLINN) { + type = KHRCommonMats::MaterialType::Blinn; + } else if (material.info->shadingModel == RAW_SHADING_MODEL_PHONG) { + type = KHRCommonMats::MaterialType::Phong; + } } khrComMat.reset( - new KHRCommonMats( - type, - getTex(RAW_TEXTURE_USAGE_SHININESS), material.shininess, - getTex(RAW_TEXTURE_USAGE_AMBIENT), material.ambientFactor, - getTex(RAW_TEXTURE_USAGE_DIFFUSE), material.diffuseFactor, - getTex(RAW_TEXTURE_USAGE_SPECULAR), material.specularFactor)); + new KHRCommonMats(type, + simpleTex(RAW_TEXTURE_USAGE_SHININESS).get(), shininess, + simpleTex(RAW_TEXTURE_USAGE_AMBIENT).get(), ambientFactor, + diffuseTex.get(), diffuseFactor, + simpleTex(RAW_TEXTURE_USAGE_SPECULAR).get(), specularFactor)); } std::shared_ptr mData = gltf->materials.hold( new MaterialData( - material.name, isTransparent, getTex(RAW_TEXTURE_USAGE_NORMAL), - getTex(RAW_TEXTURE_USAGE_EMISSIVE), material.emissiveFactor, + material.name, isTransparent, + simpleTex(RAW_TEXTURE_USAGE_NORMAL).get(), simpleTex(RAW_TEXTURE_USAGE_EMISSIVE).get(), + emissiveFactor * emissiveIntensity, khrComMat, pbrMetRough, pbrSpecGloss)); materialsByName[materialHash(material)] = mData; } diff --git a/src/RawModel.cpp b/src/RawModel.cpp index 3dcd051..712c799 100644 --- a/src/RawModel.cpp +++ b/src/RawModel.cpp @@ -111,35 +111,25 @@ int RawModel::AddTexture(const std::string &name, const std::string &fileName, c int RawModel::AddMaterial(const RawMaterial &material) { - return AddMaterial( - material.name.c_str(), material.shadingModel.c_str(), material.type, material.textures, material.ambientFactor, - material.diffuseFactor, material.specularFactor, material.emissiveFactor, material.shininess); + return AddMaterial(material.name.c_str(), material.type, material.textures, material.info); } int RawModel::AddMaterial( - const char *name, const char *shadingModel, const RawMaterialType materialType, - const int textures[RAW_TEXTURE_USAGE_MAX], const Vec3f ambientFactor, - const Vec4f diffuseFactor, const Vec3f specularFactor, - const Vec3f emissiveFactor, float shinineness) + const char *name, + const RawMaterialType materialType, + const int textures[RAW_TEXTURE_USAGE_MAX], + std::shared_ptr materialInfo) { for (size_t i = 0; i < materials.size(); i++) { if (materials[i].name != name) { continue; } - if (materials[i].shadingModel != shadingModel) { - continue; - } if (materials[i].type != materialType) { continue; } - if (materials[i].ambientFactor != ambientFactor || - materials[i].diffuseFactor != diffuseFactor || - materials[i].specularFactor != specularFactor || - materials[i].emissiveFactor != emissiveFactor || - materials[i].shininess != shinineness) { + if (*(materials[i].info) != *materialInfo) { continue; } - bool match = true; for (int j = 0; match && j < RAW_TEXTURE_USAGE_MAX; j++) { match = match && (materials[i].textures[j] == textures[j]); @@ -150,14 +140,9 @@ int RawModel::AddMaterial( } RawMaterial material; - material.name = name; - material.shadingModel = shadingModel; - material.type = materialType; - material.ambientFactor = ambientFactor; - material.diffuseFactor = diffuseFactor; - material.specularFactor = specularFactor; - material.emissiveFactor = emissiveFactor; - material.shininess = shinineness; + material.name = name; + material.type = materialType; + material.info = materialInfo; for (int i = 0; i < RAW_TEXTURE_USAGE_MAX; i++) { material.textures[i] = textures[i]; diff --git a/src/RawModel.h b/src/RawModel.h index 3077bbb..f729b6f 100644 --- a/src/RawModel.h +++ b/src/RawModel.h @@ -95,8 +95,32 @@ struct RawTriangle int surfaceIndex; }; +enum RawShadingModel +{ + RAW_SHADING_MODEL_UNKNOWN = -1, + RAW_SHADING_MODEL_CONSTANT, + RAW_SHADING_MODEL_LAMBERT, + RAW_SHADING_MODEL_BLINN, + RAW_SHADING_MODEL_PHONG, + RAW_SHADING_MODEL_PBR_MET_ROUGH, + RAW_SHADING_MODEL_MAX +}; + +static inline std::string Describe(RawShadingModel model) { + switch(model) { + case RAW_SHADING_MODEL_UNKNOWN: return ""; + case RAW_SHADING_MODEL_CONSTANT: return "Constant"; + case RAW_SHADING_MODEL_LAMBERT: return "Lambert"; + case RAW_SHADING_MODEL_BLINN: return "Blinn"; + case RAW_SHADING_MODEL_PHONG: return "Phong"; + case RAW_SHADING_MODEL_PBR_MET_ROUGH: return "Metallic/Roughness"; + case RAW_SHADING_MODEL_MAX: default: return ""; + } +} + enum RawTextureUsage { + RAW_TEXTURE_USAGE_NONE = -1, RAW_TEXTURE_USAGE_AMBIENT, RAW_TEXTURE_USAGE_DIFFUSE, RAW_TEXTURE_USAGE_NORMAL, @@ -104,32 +128,28 @@ enum RawTextureUsage RAW_TEXTURE_USAGE_SHININESS, RAW_TEXTURE_USAGE_EMISSIVE, RAW_TEXTURE_USAGE_REFLECTION, + RAW_TEXTURE_USAGE_ALBEDO, + RAW_TEXTURE_USAGE_OCCLUSION, + RAW_TEXTURE_USAGE_ROUGHNESS, + RAW_TEXTURE_USAGE_METALLIC, RAW_TEXTURE_USAGE_MAX }; -inline std::string DescribeTextureUsage(int usage) +static inline std::string Describe(RawTextureUsage usage) { - if (usage < 0) { - return ""; - } - switch (static_cast(usage)) { - case RAW_TEXTURE_USAGE_AMBIENT: - return "ambient"; - case RAW_TEXTURE_USAGE_DIFFUSE: - return "diffuse"; - case RAW_TEXTURE_USAGE_NORMAL: - return "normal"; - case RAW_TEXTURE_USAGE_SPECULAR: - return "specuar"; - case RAW_TEXTURE_USAGE_SHININESS: - return "shininess"; - case RAW_TEXTURE_USAGE_EMISSIVE: - return "emissive"; - case RAW_TEXTURE_USAGE_REFLECTION: - return "reflection"; - case RAW_TEXTURE_USAGE_MAX: - default: - return "unknown"; + switch (usage) { + case RAW_TEXTURE_USAGE_NONE: return ""; + case RAW_TEXTURE_USAGE_AMBIENT: return "ambient"; + case RAW_TEXTURE_USAGE_DIFFUSE: return "diffuse"; + case RAW_TEXTURE_USAGE_NORMAL: return "normal"; + case RAW_TEXTURE_USAGE_SPECULAR: return "specuar"; + case RAW_TEXTURE_USAGE_SHININESS: return "shininess"; + case RAW_TEXTURE_USAGE_EMISSIVE: return "emissive"; + case RAW_TEXTURE_USAGE_REFLECTION: return "reflection"; + case RAW_TEXTURE_USAGE_OCCLUSION: return "occlusion"; + case RAW_TEXTURE_USAGE_ROUGHNESS: return "roughness"; + case RAW_TEXTURE_USAGE_METALLIC: return "metallic"; + case RAW_TEXTURE_USAGE_MAX:default: return "unknown"; } }; @@ -159,18 +179,91 @@ enum RawMaterialType RAW_MATERIAL_TYPE_SKINNED_TRANSPARENT, }; +struct RawMatProps { + explicit RawMatProps(RawShadingModel shadingModel) + : shadingModel(shadingModel) + {} + const RawShadingModel shadingModel; + + virtual bool operator!=(const RawMatProps &other) const { return !(*this == other); } + virtual bool operator==(const RawMatProps &other) const { return shadingModel == other.shadingModel; }; +}; + +struct RawTraditionalMatProps : RawMatProps { + RawTraditionalMatProps( + RawShadingModel shadingModel, + const Vec3f &&ambientFactor, + const Vec4f &&diffuseFactor, + const Vec3f &&emissiveFactor, + const Vec3f &&specularFactor, + const float shininess + ) : RawMatProps(shadingModel), + ambientFactor(ambientFactor), + diffuseFactor(diffuseFactor), + emissiveFactor(emissiveFactor), + specularFactor(specularFactor), + shininess(shininess) + {} + + const Vec3f ambientFactor; + const Vec4f diffuseFactor; + const Vec3f emissiveFactor; + const Vec3f specularFactor; + const float shininess; + + bool operator==(const RawMatProps &other) const override { + if (RawMatProps::operator==(other)) { + const auto &typed = (RawTraditionalMatProps &) other; + return ambientFactor == typed.ambientFactor && + diffuseFactor == typed.diffuseFactor && + specularFactor == typed.specularFactor && + emissiveFactor == typed.emissiveFactor && + shininess == typed.shininess; + } + return false; + } +}; + +struct RawMetRoughMatProps : RawMatProps { + RawMetRoughMatProps( + RawShadingModel shadingModel, + const Vec4f &&diffuseFactor, + const Vec3f &&emissiveFactor, + float emissiveIntensity, + float metallic, + float roughness + ) : RawMatProps(shadingModel), + diffuseFactor(diffuseFactor), + emissiveFactor(emissiveFactor), + emissiveIntensity(emissiveIntensity), + metallic(metallic), + roughness(roughness) + {} + const Vec4f diffuseFactor; + const Vec3f emissiveFactor; + const float emissiveIntensity; + const float metallic; + const float roughness; + + bool operator==(const RawMatProps &other) const override { + if (RawMatProps::operator==(other)) { + const auto &typed = (RawMetRoughMatProps &) other; + return diffuseFactor == typed.diffuseFactor && + emissiveFactor == typed.emissiveFactor && + emissiveIntensity == typed.emissiveIntensity && + metallic == typed.metallic && + roughness == typed.roughness; + } + return false; + } +}; + struct RawMaterial { - - std::string name; - std::string shadingModel; // typically "Surface", "Anisotropic", "Blinn", "Lambert", "Phong", "Phone E" - RawMaterialType type; - Vec3f ambientFactor; - Vec4f diffuseFactor; - Vec3f specularFactor; - Vec3f emissiveFactor; - float shininess; - int textures[RAW_TEXTURE_USAGE_MAX]; + std::string name; + RawMaterialType type; + std::shared_ptr info; + int textures[RAW_TEXTURE_USAGE_MAX]; }; struct RawBlendChannel @@ -263,10 +356,8 @@ public: int AddTexture(const std::string &name, const std::string &fileName, const std::string &fileLocation, RawTextureUsage usage); int AddMaterial(const RawMaterial &material); int AddMaterial( - const char *name, const char *shadingModel, RawMaterialType materialType, - const int textures[RAW_TEXTURE_USAGE_MAX], Vec3f ambientFactor, - Vec4f diffuseFactor, Vec3f specularFactor, - Vec3f emissiveFactor, float shinineness); + const char *name, const RawMaterialType materialType, const int textures[RAW_TEXTURE_USAGE_MAX], + std::shared_ptr materialInfo); int AddSurface(const RawSurface &suface); int AddSurface(const char *name, long surfaceId); int AddAnimation(const RawAnimation &animation); diff --git a/src/glTF/MaterialData.cpp b/src/glTF/MaterialData.cpp index 60984cc..af76401 100644 --- a/src/glTF/MaterialData.cpp +++ b/src/glTF/MaterialData.cpp @@ -126,10 +126,11 @@ void to_json(json &j, const PBRSpecularGlossiness &d) } PBRMetallicRoughness::PBRMetallicRoughness( - const TextureData *baseColorTexture, const Vec4f &baseolorFactor, - float metallic, float roughness) + const TextureData *baseColorTexture, const TextureData *metRoughTexture, + const Vec4f &baseColorFactor, float metallic, float roughness) : baseColorTexture(Tex::ref(baseColorTexture)), - baseColorFactor(baseolorFactor), + metRoughTexture(Tex::ref(metRoughTexture)), + baseColorFactor(baseColorFactor), metallic(metallic), roughness(roughness) { @@ -144,10 +145,14 @@ void to_json(json &j, const PBRMetallicRoughness &d) if (d.baseColorFactor.LengthSquared() > 0) { j["baseColorFactor"] = toStdVec(d.baseColorFactor); } - if (d.metallic != 1.0f) { + if (d.metRoughTexture != nullptr) { + j["metallicRoughnessTexture"] = *d.metRoughTexture; + // if a texture is provided, throw away metallic/roughness values + j["roughnessFactor"] = 1.0f; + j["metallicFactor"] = 1.0f; + } else { + // without a texture, however, use metallic/roughness as constants j["metallicFactor"] = d.metallic; - } - if (d.roughness != 1.0f) { j["roughnessFactor"] = d.roughness; } } diff --git a/src/glTF/MaterialData.h b/src/glTF/MaterialData.h index 3c45ed1..afcfcd7 100644 --- a/src/glTF/MaterialData.h +++ b/src/glTF/MaterialData.h @@ -70,10 +70,11 @@ struct PBRSpecularGlossiness struct PBRMetallicRoughness { PBRMetallicRoughness( - const TextureData *baseColorTexture, const Vec4f &baseolorFactor, - float metallic = 0.1f, float roughness = 0.4f); + const TextureData *baseColorTexture, const TextureData *metRoughTexture, + const Vec4f &baseColorFactor, float metallic = 0.1f, float roughness = 0.6f); std::unique_ptr baseColorTexture; + std::unique_ptr metRoughTexture; const Vec4f baseColorFactor; const float metallic; const float roughness;