diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..ea6f283 --- /dev/null +++ b/.clang-format @@ -0,0 +1,8 @@ + +BasedOnStyle: LLVM + +AllowShortFunctionsOnASingleLine: false + +CommentPragmas: '^/ .+' + +BinPackParameters: false \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f3ed6f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ + +.vs/ + +CMakeSettings.json + +out/ + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b893636 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required (VERSION 3.8) + +project ("FBXInglTF") + +include ("./Cli/CMakeLists.txt") diff --git a/Cli/CMakeLists.txt b/Cli/CMakeLists.txt new file mode 100644 index 0000000..42b59a2 --- /dev/null +++ b/Cli/CMakeLists.txt @@ -0,0 +1,19 @@ + +include ("${CMAKE_CURRENT_LIST_DIR}/../Core/CMakeLists.txt") + +add_executable (FBXInglTFCli "${CMAKE_CURRENT_LIST_DIR}/Cli.cpp") + +set_target_properties (FBXInglTFCli PROPERTIES CXX_STANDARD 17) + +target_include_directories (FBXInglTFCli PRIVATE ${BeeCoreIncludeDirectories}) + +target_link_libraries (FBXInglTFCli PRIVATE BeeCore) + +find_package(clipp CONFIG REQUIRED) + +target_link_libraries(FBXInglTFCli PRIVATE clipp::clipp) + +add_custom_command(TARGET FBXInglTFCli POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${FbxSdkHome}/lib/vs2017/x64/release/libfbxsdk.dll" + $) \ No newline at end of file diff --git a/Cli/Cli.cpp b/Cli/Cli.cpp new file mode 100644 index 0000000..8755c5f --- /dev/null +++ b/Cli/Cli.cpp @@ -0,0 +1,137 @@ + +#include +#include +#include +#include +#include +#include +#include +#include + +std::string relativeUriBetweenPath(std::filesystem::path &from_, + std::filesystem::path &to_) { + namespace fs = std::filesystem; + auto rel = to_.lexically_relative(from_); + return std::accumulate(rel.begin(), rel.end(), std::string{}, + [](const std::string &init_, const fs::path &seg_) { + return init_ + (init_.size() > 0 ? "/" : "") + + seg_.string(); + }); +} + +int main(int argc_, char *argv_[]) { + namespace fs = std::filesystem; + + std::string inputFile; + std::string outFile; + std::string fbmDir; + bee::ConvertOptions convertOptions; + + auto cli = ( + + clipp::value("input file", inputFile), + + clipp::option("--out").set(outFile).doc( + "The output path to the .gltf or .glb file. Defaults to " + "`/.gltf`"), + + clipp::option("--fbm-dir") + .set(fbmDir) + .doc("The directory to store the embedded media."), + + clipp::option("--no-flip-v") + .set(convertOptions.noFlipV) + .doc("Do not flip V texture coordinates."), + + clipp::option("--animation-bake-rate") + .set(convertOptions.animationBakeRate) + .doc("Animation bake rate(in FPS)."), + + clipp::option("--suspected-animation-duration-limit") + .set(convertOptions.suspectedAnimationDurationLimit) + .doc("The suspected animation duration limit.") + + ); + + if (!clipp::parse(argc_, argv_, cli)) { + std::cout << make_man_page(cli, argv_[0]); + return -1; + } + if (!fbmDir.empty()) { + convertOptions.fbmDir = fbmDir; + } + + if (outFile.empty()) { + const auto inputFilePath = fs::path{inputFile}; + const auto inputBaseNameNoExt = inputFilePath.stem().string(); + auto outFilePath = fs::current_path() / (inputBaseNameNoExt + "_glTF") / + (inputBaseNameNoExt + ".gltf"); + fs::create_directories(outFilePath.parent_path()); + outFile = outFilePath.string(); + } + convertOptions.out = outFile; + + class MyWriter : public bee::GLTFWriter { + public: + MyWriter(std::string_view in_file_, std::string_view out_file_) + : _inFile(in_file_), _outFile(out_file_) { + } + + virtual std::optional buffer(const std::byte *data_, + std::size_t size_, + std::uint32_t index_, + bool multi_) { + auto glTFOutBaseName = fs::path(_outFile).stem(); + auto bufferOutPath = + fs::path(_outFile).parent_path() / + (multi_ ? (glTFOutBaseName.string() + std::to_string(index_) + ".bin") + : (glTFOutBaseName.string() + ".bin")); + fs::create_directories(bufferOutPath.parent_path()); + + std::ofstream ofs(bufferOutPath, std::ios::binary); + ofs.exceptions(std::ios::badbit | std::ios::failbit); + ofs.write(reinterpret_cast(data_), size_); + ofs.flush(); + + auto outFileDir = fs::path(_outFile).parent_path(); + return relativeUriBetweenPath(outFileDir, bufferOutPath); + } + + private: + std::string_view _inFile; + std::string_view _outFile; + bool _dataUriForBuffers; + }; + + MyWriter writer{inputFile, outFile}; + convertOptions.useDataUriForBuffers = false; + convertOptions.writer = &writer; + + const auto imageSearchDepth = 2; + const std::array searchDirName = {"texture", "textures", + "material", "materials"}; + auto searchParentDir = fs::path(inputFile).parent_path(); + for (int i = 0; i < imageSearchDepth && !searchParentDir.empty(); + ++i, searchParentDir = searchParentDir.parent_path()) { + convertOptions.textureSearch.locations.push_back(searchParentDir.string()); + for (auto &name : searchDirName) { + convertOptions.textureSearch.locations.push_back( + (searchParentDir / name).string()); + } + } + + convertOptions.pathMode = bee::ConvertOptions::PathMode::copy; + + try { + auto glTFJson = bee::convert(inputFile, convertOptions); + + std::ofstream glTFJsonOStream(outFile); + glTFJsonOStream.exceptions(std::ios::badbit | std::ios::failbit); + glTFJsonOStream << glTFJson; + glTFJsonOStream.flush(); + } catch (const std::exception &exception) { + std::cerr << exception.what() << "\n"; + } + + return 0; +} \ No newline at end of file diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt new file mode 100644 index 0000000..ada88a5 --- /dev/null +++ b/Core/CMakeLists.txt @@ -0,0 +1,61 @@ + +set (BeeCoreIncludeDirectories "${CMAKE_CURRENT_LIST_DIR}/Source") + +set (BeeCoreSource + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/BEE_API.h" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/Converter.h" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/Converter.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/GLTFBuilder.h" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/GLTFBuilder.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/UntypedVertex.h" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/UntypedVertex.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/GLTFUtilities.h" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/GLTFUtilities.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/Convert/fbxsdk/ObjectDestroyer.h" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/Convert/fbxsdk/LayerelementAccessor.h" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/Convert/fbxsdk/Spreader.h" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/Convert/SceneConverter.h" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/Convert/SceneConverter.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/Convert/SceneConverter.Mesh.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/Convert/SceneConverter.BlendShape.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/Convert/SceneConverter.Skin.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/Convert/SceneConverter.Animation.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/Convert/SceneConverter.Material.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/Convert/SceneConverter.Texture.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/Convert/GLTFSamplerHash.h" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/Convert/FbxMeshVertexLayout.h" + "${CMAKE_CURRENT_LIST_DIR}/Source/bee/Convert/DirectSpreader.h" + ) + +add_library (BeeCore SHARED ${BeeCoreSource}) + +set_target_properties (BeeCore PROPERTIES CXX_STANDARD 20) + +target_compile_definitions (BeeCore PRIVATE BEE_EXPORT FBXSDK_SHARED NOMINMAX) + +target_include_directories (BeeCore PRIVATE ${BeeCoreIncludeDirectories} "${CMAKE_CURRENT_LIST_DIR}/fx/include") + +message (STATUS "FBXSDK: ${FbxSdkHome}") + +set (FbxSdkIncludeDirectories "${FbxSdkHome}/include") +set (FbxSdkLibraries "${FbxSdkHome}/lib/vs2017/x64/release/libfbxsdk.lib") +set (FbxSdkDynLibraries "${FbxSdkHome}/lib/vs2017/x64/release/libfbxsdk.dll") + +target_include_directories (BeeCore PRIVATE ${FbxSdkIncludeDirectories}) + +target_link_libraries (BeeCore PRIVATE ${FbxSdkLibraries}) + +find_package(nlohmann_json CONFIG REQUIRED) +target_link_libraries(BeeCore PRIVATE nlohmann_json nlohmann_json::nlohmann_json) + +find_package(fmt CONFIG REQUIRED) +target_link_libraries(BeeCore PRIVATE fmt::fmt fmt::fmt-header-only) + +find_package(skyr-url CONFIG REQUIRED) +## https://github.com/cpp-netlib/url/issues/143 +set_property(TARGET skyr::skyr-url APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG) +set_target_properties(skyr::skyr-url PROPERTIES + IMPORTED_LINK_INTERFACE_LANGUAGES_DEBUG "CXX" + IMPORTED_LOCATION_DEBUG "${_VCPKG_ROOT_DIR}/installed/x64-windows/debug/lib/skyr-urld.lib" +) +target_link_libraries(BeeCore PRIVATE skyr::skyr-url skyr::skyr-json skyr::skyr-filesystem) \ No newline at end of file diff --git a/Core/Source/bee/BEE_API.h b/Core/Source/bee/BEE_API.h new file mode 100644 index 0000000..45fd8f7 --- /dev/null +++ b/Core/Source/bee/BEE_API.h @@ -0,0 +1,8 @@ + +#pragma once + +#ifdef BEE_EXPORT + #define BEE_API __declspec(dllexport) +#else + #define BEE_API __declspec(dllimport) +#endif diff --git a/Core/Source/bee/Convert/DirectSpreader.h b/Core/Source/bee/Convert/DirectSpreader.h new file mode 100644 index 0000000..339d42a --- /dev/null +++ b/Core/Source/bee/Convert/DirectSpreader.h @@ -0,0 +1,17 @@ + +#pragma once + +#include + +namespace bee { +template struct DirectSpreader { + using type = Ty_; + + constexpr static std::size_t size = 1; + + template + static void spread(const type &in_, TargetTy_ *out_) { + *out_ = static_cast(in_); + } +}; +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/Convert/FbxMeshVertexLayout.h b/Core/Source/bee/Convert/FbxMeshVertexLayout.h new file mode 100644 index 0000000..bc5a1b5 --- /dev/null +++ b/Core/Source/bee/Convert/FbxMeshVertexLayout.h @@ -0,0 +1,61 @@ + +#pragma once + +#include +#include +#include +#include +#include + +namespace bee { +template struct FbxMeshAttributeLayout { + std::uint32_t offset = 0; + Element_ element; + + FbxMeshAttributeLayout() = default; + + FbxMeshAttributeLayout(std::uint32_t offset_, Element_ element_) + : offset(offset_), element(element_) { + } +}; + +struct FbxMeshVertexLayout { + std::uint32_t size = sizeof(NeutralVertexComponent) * 3; + + std::optional>> + normal; + + std::vector>> + uvs; + + std::vector>> + colors; + + struct Skinning { + std::uint32_t channelCount; + /// + /// Layout offset. + /// + std::uint32_t joints; + /// + /// Layout offset. + /// + std::uint32_t weights; + }; + + std::optional skinning; + + struct ShapeLayout { + FbxMeshAttributeLayout constrolPoints; + + std::optional>> + normal; + }; + + std::vector shapes; +}; +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/Convert/GLTFSamplerHash.h b/Core/Source/bee/Convert/GLTFSamplerHash.h new file mode 100644 index 0000000..fc6558e --- /dev/null +++ b/Core/Source/bee/Convert/GLTFSamplerHash.h @@ -0,0 +1,33 @@ + +#pragma once + +#include + +namespace bee { +struct GLTFSamplerKeys { + fx::gltf::Sampler::MagFilter magFilter{fx::gltf::Sampler::MagFilter::None}; + fx::gltf::Sampler::MinFilter minFilter{fx::gltf::Sampler::MinFilter::None}; + fx::gltf::Sampler::WrappingMode wrapS{ + fx::gltf::Sampler::WrappingMode::Repeat}; + fx::gltf::Sampler::WrappingMode wrapT{ + fx::gltf::Sampler::WrappingMode::Repeat}; + + bool operator==(const GLTFSamplerKeys &rhs_) const { + return minFilter == rhs_.minFilter && magFilter == rhs_.magFilter && + wrapS == rhs_.wrapS && wrapT == rhs_.wrapT; + } + + void set(fx::gltf::Sampler &sampler_) { + sampler_.minFilter = minFilter; + sampler_.magFilter = magFilter; + sampler_.wrapS = wrapS; + sampler_.wrapT = wrapT; + } +}; + +struct GLTFSamplerHash { + std::size_t operator()(const GLTFSamplerKeys &sampler_) const { + return 0; + } +}; +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/Convert/NeutralType.h b/Core/Source/bee/Convert/NeutralType.h new file mode 100644 index 0000000..60bc0ad --- /dev/null +++ b/Core/Source/bee/Convert/NeutralType.h @@ -0,0 +1,18 @@ + +#pragma once + +#include + +namespace bee { +using NeutralVertexComponent = float; + +using NeutralNormalComponent = float; + +using NeutralUVComponent = float; + +using NeutralVertexColorComponent = float; + +using NeutralVertexWeightComponent = float; + +using NeutralVertexJointComponent = std::uint32_t; +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/Convert/SceneConverter.Animation.cpp b/Core/Source/bee/Convert/SceneConverter.Animation.cpp new file mode 100644 index 0000000..cef8634 --- /dev/null +++ b/Core/Source/bee/Convert/SceneConverter.Animation.cpp @@ -0,0 +1,395 @@ + +#include +#include +#include +#include + +namespace bee { +void SceneConverter::_convertAnimation(fbxsdk::FbxScene &fbx_scene_) { + const auto nAnimStacks = fbx_scene_.GetSrcObjectCount(); + for (std::remove_const_t iAnimStack = 0; + iAnimStack < nAnimStacks; ++iAnimStack) { + const auto animStack = + fbx_scene_.GetSrcObject(iAnimStack); + const auto nAnimLayers = animStack->GetMemberCount(); + if (!nAnimLayers) { + continue; + } + + const auto timeSpan = _getAnimStackTimeSpan(*animStack); + if (timeSpan.GetDuration() == 0) { + continue; + } + const auto usedAnimationTimeMode = _animationTimeMode; + const auto firstFrame = + timeSpan.GetStart().GetFrameCount(usedAnimationTimeMode); + const auto lastFrame = + timeSpan.GetStop().GetFrameCount(usedAnimationTimeMode); + assert(lastFrame >= firstFrame); + AnimRange animRange{_animationTimeMode, firstFrame, lastFrame}; + + fx::gltf::Animation glTFAnimation; + const auto animName = _convertName(animStack->GetName()); + glTFAnimation.name = animName; + fbx_scene_.SetCurrentAnimationStack(animStack); + for (std::remove_const_t iAnimLayer = 0; + iAnimLayer < nAnimLayers; ++iAnimLayer) { + const auto animLayer = + animStack->GetMember(iAnimLayer); + _convertAnimationLayer(glTFAnimation, *animLayer, fbx_scene_, animRange); + } + if (!glTFAnimation.samplers.empty()) { + _glTFBuilder.add(&fx::gltf::Document::animations, + std::move(glTFAnimation)); + } + } +} + +fbxsdk::FbxTimeSpan SceneConverter::_getAnimStackTimeSpan( + const fbxsdk::FbxAnimStack &fbx_anim_stack_) { + const auto nAnimLayers = + fbx_anim_stack_.GetMemberCount(); + if (!nAnimLayers) { + return {}; + } + fbxsdk::FbxTimeSpan animTimeSpan; + for (std::remove_const_t iAnimLayer = 0; + iAnimLayer < nAnimLayers; ++iAnimLayer) { + const auto animLayer = + fbx_anim_stack_.GetMember(iAnimLayer); + const auto nCurveNodes = + animLayer->GetMemberCount(); + for (std::remove_const_t iCurveNode = 0; + iCurveNode < nCurveNodes; ++iCurveNode) { + const auto curveNode = + animLayer->GetMember(iCurveNode); + if (!curveNode->IsAnimated()) { + continue; + } + fbxsdk::FbxTimeSpan curveNodeInterval; + if (!curveNode->GetAnimationInterval(curveNodeInterval)) { + continue; + } + animTimeSpan.UnionAssignment(curveNodeInterval); + } + } + return animTimeSpan; +} + +void SceneConverter::_convertAnimationLayer( + fx::gltf::Animation &glTF_animation_, + fbxsdk::FbxAnimLayer &fbx_anim_layer_, + fbxsdk::FbxScene &fbx_scene_, + const AnimRange &anim_range_) { + const auto nNodes = fbx_scene_.GetNodeCount(); + for (std::remove_const_t iNode = 0; iNode < nNodes; + ++iNode) { + auto fbxNode = fbx_scene_.GetNode(iNode); + _convertAnimationLayer(glTF_animation_, fbx_anim_layer_, *fbxNode, + anim_range_); + } +} + +void SceneConverter::_convertAnimationLayer( + fx::gltf::Animation &glTF_animation_, + fbxsdk::FbxAnimLayer &fbx_anim_layer_, + fbxsdk::FbxNode &fbx_node_, + const AnimRange &anim_range_) { + if (_options.export_trs_animation) { + _extractTrsAnimation(glTF_animation_, fbx_anim_layer_, fbx_node_, + anim_range_); + } + + if (_options.export_blend_shape_animation) { + _extractWeightsAnimation(glTF_animation_, fbx_anim_layer_, fbx_node_, + anim_range_); + } +} + +void SceneConverter::_extractWeightsAnimation( + fx::gltf::Animation &glTF_animation_, + fbxsdk::FbxAnimLayer &fbx_anim_layer_, + fbxsdk::FbxNode &fbx_node_, + const AnimRange &anim_range_) { + auto rNodeBumpMeta = _nodeDumpMetaMap.find(&fbx_node_); + if (rNodeBumpMeta == _nodeDumpMetaMap.end()) { + return; + } + + const auto &nodeBumpMeta = rNodeBumpMeta->second; + if (!nodeBumpMeta.meshes) { + return; + } + + auto &blendShapeMeta = nodeBumpMeta.meshes->blendShapeMeta; + if (!blendShapeMeta) { + return; + } + + auto &fbxMeshes = nodeBumpMeta.meshes->meshes; + if (!fbxMeshes.empty()) { + std::vector morphAnimations{fbxMeshes.size()}; + for (decltype(fbxMeshes.size()) iMesh = 0; iMesh < fbxMeshes.size(); + ++iMesh) { + const auto &blendShapeData = blendShapeMeta->blendShapeDatas[iMesh]; + morphAnimations[iMesh] = _extractWeightsAnimation( + fbx_anim_layer_, fbx_node_, *fbxMeshes[iMesh], blendShapeData, + anim_range_); + } + + if (const auto &first = morphAnimations.front(); std::all_of( + std::next(morphAnimations.begin()), morphAnimations.end(), + [&first](const MorphAnimation &anim_) { + return anim_.times == first.times && anim_.values == first.values; + })) { + _writeMorphAnimtion(glTF_animation_, first, nodeBumpMeta.glTFNodeIndex, + fbx_node_); + } else { + _warn(fmt::format("Sub-meshes use different morph animation. We can't " + "handle that case.")); + } + } +} + +void SceneConverter::_writeMorphAnimtion(fx::gltf::Animation &glTF_animation_, + const MorphAnimation &morph_animtion_, + std::uint32_t glTF_node_index_, + const fbxsdk::FbxNode &fbx_node_) { + auto timeAccessorIndex = _glTFBuilder.createAccessor< + fx::gltf::Accessor::Type::Scalar, + fx::gltf::Accessor::ComponentType::Float, + DirectSpreader>( + std::span{morph_animtion_.times}, 0, 0, true); + _glTFBuilder.get(&fx::gltf::Document::accessors)[timeAccessorIndex].name = + fmt::format("{}/weights/Input", fbx_node_.GetName()); + + auto weightsAccessorIndex = _glTFBuilder.createAccessor< + fx::gltf::Accessor::Type::Scalar, + fx::gltf::Accessor::ComponentType::Float, + DirectSpreader>( + morph_animtion_.values, 0, 0); + _glTFBuilder.get(&fx::gltf::Document::accessors)[weightsAccessorIndex].name = + fmt::format("{}/weights/Output", fbx_node_.GetName()); + + fx::gltf::Animation::Sampler sampler; + sampler.input = timeAccessorIndex; + sampler.output = weightsAccessorIndex; + auto samplerIndex = glTF_animation_.samplers.size(); + glTF_animation_.samplers.emplace_back(std::move(sampler)); + fx::gltf::Animation::Channel channel; + channel.target.node = glTF_node_index_; + channel.target.path = "weights"; + channel.sampler = static_cast(samplerIndex); + glTF_animation_.channels.push_back(channel); +} + +SceneConverter::MorphAnimation SceneConverter::_extractWeightsAnimation( + fbxsdk::FbxAnimLayer &fbx_anim_layer_, + const fbxsdk::FbxNode &fbx_node_, + fbxsdk::FbxMesh &fbx_mesh_, + const FbxBlendShapeData &blend_shape_data_, + const AnimRange &anim_range_) { + using WeightType = decltype(MorphAnimation::values)::value_type; + + MorphAnimation morphAnimation; + + using TargetWeightsCount = std::size_t; + auto nTargetWeights = std::accumulate( + blend_shape_data_.channels.begin(), blend_shape_data_.channels.end(), + static_cast(0), + [](TargetWeightsCount sum_, const auto &channel_) { + return sum_ + channel_.targetShapes.size(); + }); + + const auto nFrames = static_cast( + anim_range_.frames_count()); + const auto timeMode = anim_range_.timeMode; + morphAnimation.times.resize(nFrames, 0.0); + morphAnimation.values.resize(nTargetWeights * nFrames); + + auto extractFrame = + [](decltype(MorphAnimation::values)::iterator out_weights_, + fbxsdk::FbxTime time_, fbxsdk::FbxAnimCurve *shape_channel_, + const decltype( + FbxBlendShapeData::Channel::targetShapes) &target_shapes_) { + if (target_shapes_.empty()) { + return; + } + + constexpr auto zeroWeight = static_cast(0.0); + constexpr fbxsdk::FbxDouble defaultWeight = 0; + + const auto iFrameWeightsBeg = out_weights_; + const auto iFrameWeightsEnd = iFrameWeightsBeg + target_shapes_.size(); + + const auto animWeight = + shape_channel_ ? shape_channel_->Evaluate(time_) : defaultWeight; + + // The target shape 'fullWeight' values are + // a strictly ascending list of floats (between 0 and 100), forming a + // sequence of intervals. + assert(std::is_sorted(target_shapes_.begin(), target_shapes_.end(), + [](const auto &lhs_, const auto &rhs_) { + return std::get<1>(lhs_) < std::get<1>(rhs_); + })); + const auto firstNotLessThan = std::find_if( + target_shapes_.begin(), target_shapes_.end(), + [animWeight]( + const std::decay_t::value_type + &target_) { + const auto &[targetShape, targetWeightThreshold] = target_; + return targetWeightThreshold >= animWeight; + }); + + if (firstNotLessThan == target_shapes_.begin()) { + const auto firstThreshold = std::get<1>(target_shapes_.front()); + *iFrameWeightsBeg = animWeight / firstThreshold; + } else if (firstNotLessThan != target_shapes_.end()) { + const auto iRight = firstNotLessThan - target_shapes_.begin(); + assert(iRight); + const auto iLeft = iRight - 1; + const auto leftWeight = std::get<1>(target_shapes_[iLeft]); + const auto rightWeight = std::get<1>(target_shapes_[iRight]); + const auto ratio = + (animWeight - leftWeight) / (rightWeight - leftWeight); + iFrameWeightsBeg[iLeft] = static_cast(ratio); + iFrameWeightsBeg[iRight] = static_cast(1.0 - ratio); + } + }; + + for (decltype(morphAnimation.times.size()) iFrame = 0; + iFrame < morphAnimation.times.size(); ++iFrame) { + const auto fbxFrame = anim_range_.firstFrame + iFrame; + fbxsdk::FbxTime time; + time.SetFrame(fbxFrame, timeMode); + + morphAnimation.times[iFrame] = time.GetSecondDouble(); + + TargetWeightsCount offset = 0; + for (const auto &[blendShapeIndex, blendShapeChannelIndex, name, + deformPercent, targetShapes] : + blend_shape_data_.channels) { + auto shapeChannel = fbx_mesh_.GetShapeChannel( + blendShapeIndex, blendShapeChannelIndex, &fbx_anim_layer_); + const auto outWeights = + morphAnimation.values.begin() + nTargetWeights * iFrame + offset; + extractFrame(outWeights, time, shapeChannel, targetShapes); + offset += targetShapes.size(); + } + } + + return morphAnimation; +} + +void SceneConverter::_extractTrsAnimation(fx::gltf::Animation &glTF_animation_, + fbxsdk::FbxAnimLayer &fbx_anim_layer_, + fbxsdk::FbxNode &fbx_node_, + const AnimRange &anim_range_) { + const auto glTFNodeIndex = _getNodeMap(fbx_node_); + if (!glTFNodeIndex) { + return; + } + + const auto isTranslationAnimated = + fbx_node_.LclTranslation.IsAnimated(&fbx_anim_layer_); + const auto isRotationAnimated = + fbx_node_.LclRotation.IsAnimated(&fbx_anim_layer_); + const auto isScaleAnimated = + fbx_node_.LclScaling.IsAnimated(&fbx_anim_layer_); + if (!isTranslationAnimated && !isRotationAnimated && !isScaleAnimated) { + return; + } + + const auto nFrames = static_cast( + anim_range_.lastFrame - anim_range_.firstFrame); + std::vector times; + std::vector translations; + std::vector rotations; + std::vector scales; + + for (std::remove_const_t iFrame = 0; iFrame < nFrames; + ++iFrame) { + const auto fbxFrame = anim_range_.firstFrame + iFrame; + fbxsdk::FbxTime time; + time.SetFrame(fbxFrame, anim_range_.timeMode); + + const auto localTransform = fbx_node_.EvaluateLocalTransform(time); + bool mayBeOptOut = false; + + fbxsdk::FbxVector4 translation; + if (isTranslationAnimated) { + translation = localTransform.GetT(); + } + fbxsdk::FbxQuaternion rotation; + if (isRotationAnimated) { + rotation = localTransform.GetQ(); + rotation.Normalize(); + } + fbxsdk::FbxVector4 scale; + if (isScaleAnimated) { + scale = localTransform.GetS(); + } + + if (!mayBeOptOut) { + times.push_back(time.GetSecondDouble()); + if (isTranslationAnimated) { + translations.push_back(translation); + } + if (isRotationAnimated) { + rotations.push_back(rotation); + } + if (isScaleAnimated) { + scales.push_back(scale); + } + } + } + + auto timeAccessorIndex = + _glTFBuilder.createAccessor>( + times, 0, 0, true); + _glTFBuilder.get(&fx::gltf::Document::accessors)[timeAccessorIndex].name = + fmt::format("{}/Trs/Input", fbx_node_.GetName()); + auto addChannel = [timeAccessorIndex, &glTF_animation_, glTFNodeIndex, this, + &fbx_node_](std::string_view path_, + std::uint32_t value_accessor_index_) { + _glTFBuilder.get(&fx::gltf::Document::accessors)[value_accessor_index_] + .name = fmt::format("{}/{}/Output", fbx_node_.GetName(), path_); + fx::gltf::Animation::Sampler sampler; + sampler.input = timeAccessorIndex; + sampler.output = value_accessor_index_; + auto samplerIndex = glTF_animation_.samplers.size(); + glTF_animation_.samplers.emplace_back(std::move(sampler)); + fx::gltf::Animation::Channel channel; + channel.target.node = *glTFNodeIndex; + channel.target.path = path_; + channel.sampler = static_cast(samplerIndex); + glTF_animation_.channels.push_back(channel); + }; + + using ComponentType = + GLTFComponentTypeStorage; + if (isTranslationAnimated) { + auto valueAccessorIndex = + _glTFBuilder.createAccessor(translations, 0, 0); + addChannel("translation", valueAccessorIndex); + } + if (isRotationAnimated) { + auto valueAccessorIndex = + _glTFBuilder.createAccessor(rotations, 0, 0); + addChannel("rotation", valueAccessorIndex); + } + if (isScaleAnimated) { + auto valueAccessorIndex = + _glTFBuilder.createAccessor(scales, 0, 0); + addChannel("scale", valueAccessorIndex); + } +} +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/Convert/SceneConverter.BlendShape.cpp b/Core/Source/bee/Convert/SceneConverter.BlendShape.cpp new file mode 100644 index 0000000..db92fc1 --- /dev/null +++ b/Core/Source/bee/Convert/SceneConverter.BlendShape.cpp @@ -0,0 +1,108 @@ + +#include +#include + +namespace bee { +std::optional +SceneConverter::_extractdBlendShapeData(const fbxsdk::FbxMesh &fbx_mesh_) { + FbxBlendShapeData blendShapeData; + + const auto nBlendShape = fbx_mesh_.GetDeformerCount( + fbxsdk::FbxDeformer::EDeformerType::eBlendShape); + + for (std::remove_const_t iBlendShape = 0; + iBlendShape < nBlendShape; ++iBlendShape) { + const auto fbxBlendShape = + static_cast(fbx_mesh_.GetDeformer( + iBlendShape, fbxsdk::FbxDeformer::EDeformerType::eBlendShape)); + const auto nChannels = fbxBlendShape->GetBlendShapeChannelCount(); + for (std::remove_const_t iChannel = 0; + iChannel < nChannels; ++iChannel) { + const auto blendShapeChannel = + fbxBlendShape->GetBlendShapeChannel(iChannel); + auto fullWeights = blendShapeChannel->GetTargetShapeFullWeights(); + if (const auto nTargetShapes = blendShapeChannel->GetTargetShapeCount()) { + decltype(FbxBlendShapeData::Channel::targetShapes) targetShapes( + nTargetShapes); + for (std::remove_const_t iTargetShape = 0; + iTargetShape < nTargetShapes; ++iTargetShape) { + auto targetShape = blendShapeChannel->GetTargetShape(iTargetShape); + targetShapes[iTargetShape] = {targetShape, fullWeights[iTargetShape]}; + } + blendShapeData.channels.push_back(FbxBlendShapeData::Channel{ + iBlendShape, iChannel, _convertName(blendShapeChannel->GetName()), + blendShapeChannel->DeformPercent.Get(), std::move(targetShapes)}); + } + } + } + + if (blendShapeData.channels.empty()) { + return {}; + } + + return blendShapeData; +} + +std::optional +SceneConverter::_extractNodeMeshesBlendShape( + const std::vector &fbx_meshes_) { + if (fbx_meshes_.empty()) { + return {}; + } + + std::vector> blendShapeDatas; + blendShapeDatas.reserve(fbx_meshes_.size()); + std::transform(fbx_meshes_.begin(), fbx_meshes_.end(), + std::back_inserter(blendShapeDatas), + [this](fbxsdk::FbxMesh *fbx_mesh_) { + return _extractdBlendShapeData(*fbx_mesh_); + }); + + const auto &firstBlendShapeData = blendShapeDatas.front(); + + const auto hasSameStruct = + [&](const std::optional &blend_shape_data_) { + if (blend_shape_data_.has_value() != firstBlendShapeData.has_value()) { + return false; + } + if (!firstBlendShapeData) { + return true; + } + if (firstBlendShapeData->channels.size() != + blend_shape_data_->channels.size()) { + return false; + } + if (!std::equal(firstBlendShapeData->channels.begin(), + firstBlendShapeData->channels.end(), + blend_shape_data_->channels.begin(), + blend_shape_data_->channels.end(), + [](const FbxBlendShapeData::Channel &lhs_, + const FbxBlendShapeData::Channel &rhs_) { + return lhs_.name == rhs_.name && + lhs_.targetShapes.size() == + rhs_.targetShapes.size(); + })) { + return false; + } + return true; + }; + + if (!std::all_of(std::next(blendShapeDatas.begin()), blendShapeDatas.end(), + hasSameStruct)) { + _warn(fmt::format( + "glTF does not allow sub-meshes have different number of targets.")); + } + + if (!firstBlendShapeData) { + return {}; + } + + FbxNodeMeshesBumpMeta::BlendShapeDumpMeta myMeta; + myMeta.blendShapeDatas.reserve(blendShapeDatas.size()); + std::transform(blendShapeDatas.begin(), blendShapeDatas.end(), + std::back_inserter(myMeta.blendShapeDatas), + [](auto &v_) { return std::move(*v_); }); + + return myMeta; +} +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/Convert/SceneConverter.Material.cpp b/Core/Source/bee/Convert/SceneConverter.Material.cpp new file mode 100644 index 0000000..29d93e3 --- /dev/null +++ b/Core/Source/bee/Convert/SceneConverter.Material.cpp @@ -0,0 +1,117 @@ + +#include + +namespace bee { +std::optional +SceneConverter::_convertMaterial(fbxsdk::FbxSurfaceMaterial &fbx_material_) { + if (fbx_material_.Is()) { + return _convertLambertMaterial( + static_cast(fbx_material_)); + } else { + return {}; + } +} + +std::optional +SceneConverter::_convertLambertMaterial(fbxsdk::FbxSurfaceLambert &fbx_material_) { + const auto materialName = std::string{fbx_material_.GetName()}; + + const auto fbxTransparentColor = fbx_material_.TransparentColor.Get(); + // FBX color is RGB, so we calculate the A channel as the average of the FBX + // transparency color + const auto glTFTransparency = + 1.0 - fbx_material_.TransparencyFactor.Get() * + (fbxTransparentColor[0] + fbxTransparentColor[1] + + fbxTransparentColor[2]); + + fx::gltf::Material glTFMaterial; + glTFMaterial.name = materialName; + auto &glTFPbrMetallicRoughness = glTFMaterial.pbrMetallicRoughness; + + // Base color + { + const auto diffuseFactor = fbx_material_.DiffuseFactor.Get(); + for (int i = 0; i < 3; ++i) { + glTFPbrMetallicRoughness.baseColorFactor[i] = + static_cast(diffuseFactor); + } + if (const auto glTFDiffuseTextureIndex = + _convertTextureProperty(fbx_material_.Diffuse)) { + glTFPbrMetallicRoughness.baseColorTexture.index = + *glTFDiffuseTextureIndex; + } else { + const auto fbxDiffuseColor = fbx_material_.Diffuse.Get(); + for (int i = 0; i < 3; ++i) { + // TODO: should we multiply with transparent color? + glTFPbrMetallicRoughness.baseColorFactor[i] = + static_cast(fbxDiffuseColor[i] * diffuseFactor); + } + glTFPbrMetallicRoughness.baseColorFactor[3] = + static_cast(glTFTransparency); + } + } + + // Normal map + if (const auto glTFNormalMapIndex = + _convertTextureProperty(fbx_material_.NormalMap)) { + glTFMaterial.normalTexture.index = *glTFNormalMapIndex; + } + + // Bump map + if (const auto glTFBumpMapIndex = + _convertTextureProperty(fbx_material_.Bump)) { + glTFMaterial.normalTexture.index = *glTFBumpMapIndex; + } + + // Emissive + { + const auto emissiveFactor = fbx_material_.EmissiveFactor.Get(); + if (const auto glTFEmissiveTextureIndex = + _convertTextureProperty(fbx_material_.Emissive)) { + glTFMaterial.emissiveTexture.index = *glTFEmissiveTextureIndex; + for (int i = 0; i < 3; ++i) { + glTFMaterial.emissiveFactor[i] = static_cast(emissiveFactor); + } + } else { + const auto emissive = fbx_material_.Emissive.Get(); + for (int i = 0; i < 3; ++i) { + glTFMaterial.emissiveFactor[i] = + static_cast(emissive[i] * emissiveFactor); + } + } + } + + if (fbx_material_.Is()) { + const auto &fbxPhong = + static_cast(fbx_material_); + + // Metallic factor + auto fbxSpecular = fbxPhong.Specular.Get(); + std::array specular{fbxSpecular[0], fbxSpecular[1], + fbxSpecular[2]}; + const auto fbxSpecularFactor = fbxPhong.SpecularFactor.Get(); + for (auto &c : specular) { + c *= fbxSpecularFactor; + } + glTFPbrMetallicRoughness.metallicFactor = + 0.4f; // static_cast(_getMetalnessFromSpecular(specular.data())); + + auto getRoughness = [&](float shininess_) { + return std::sqrt(2.0f / (2.0f + shininess_)); + }; + + // Roughness factor + glTFPbrMetallicRoughness.roughnessFactor = + getRoughness(static_cast(fbxPhong.Shininess.Get())); + } + + auto glTFMaterailIndex = + _glTFBuilder.add(&fx::gltf::Document::materials, std::move(glTFMaterial)); + return glTFMaterailIndex; +} + +template +static T_ SceneConverter::_getMetalnessFromSpecular(const T_ *specular_) { + return static_cast(specular_[0] > 0.5 ? 1 : 0); +} +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/Convert/SceneConverter.Mesh.cpp b/Core/Source/bee/Convert/SceneConverter.Mesh.cpp new file mode 100644 index 0000000..e0e065b --- /dev/null +++ b/Core/Source/bee/Convert/SceneConverter.Mesh.cpp @@ -0,0 +1,652 @@ + +#include +#include +#include +#include +#include + +namespace bee { +template +static void untypedVertexCopy(std::byte *out_, const std::byte *in_) { + auto in = reinterpret_cast(in_); + auto out = reinterpret_cast(out_); + for (int i = 0; i < N_; ++i) { + out[i] = static_cast(in[i]); + } +} + +template +static bee::SceneConverter::VertexBulk::ChannelWriter +makeUntypedVertexCopyN(std::size_t n_) { + return [n_](std::byte *out_, const std::byte *in_) { + auto in = reinterpret_cast(in_); + auto out = reinterpret_cast(out_); + for (int i = 0; i < n_; ++i) { + out[i] = static_cast(in[i]); + } + }; +} + +std::optional +SceneConverter::_convertNodeMeshes( + FbxNodeDumpMeta &node_meta_, + const std::vector &fbx_meshes_, + fbxsdk::FbxNode &fbx_node_) { + assert(!fbx_meshes_.empty()); + + auto meshName = _getName(*fbx_meshes_.front(), fbx_node_); + auto [vertexTransform, normalTransform] = _getGeometrixTransform(fbx_node_); + auto vertexTransformX = + (vertexTransform == fbxsdk::FbxMatrix{}) ? nullptr : &vertexTransform; + auto normalTransformX = + (normalTransform == fbxsdk::FbxMatrix{}) ? nullptr : &normalTransform; + + std::optional nodeMeshesSkinData; + nodeMeshesSkinData = _extractNodeMeshesSkinData(fbx_meshes_); + + FbxNodeMeshesBumpMeta myMeta; + if (_options.export_blend_shape) { + myMeta.blendShapeMeta = _extractNodeMeshesBlendShape(fbx_meshes_); + } + + std::optional glTFSkinIndex; + + fx::gltf::Mesh glTFMesh; + glTFMesh.name = meshName; + + for (decltype(fbx_meshes_.size()) iFbxMesh = 0; iFbxMesh < fbx_meshes_.size(); + ++iFbxMesh) { + const auto fbxMesh = fbx_meshes_[iFbxMesh]; + + std::vector fbxShapes; + if (myMeta.blendShapeMeta) { + fbxShapes = myMeta.blendShapeMeta->blendShapeDatas[iFbxMesh].getShapes(); + } + + std::span skinInfluenceChannels; + if (nodeMeshesSkinData) { + skinInfluenceChannels = nodeMeshesSkinData->meshChannels[iFbxMesh]; + } + + auto glTFPrimitive = _convertMeshAsPrimitive( + *fbxMesh, meshName, vertexTransformX, normalTransformX, fbxShapes, + skinInfluenceChannels); + + auto materialIndex = _getTheUniqueMaterialIndex(*fbxMesh); + if (materialIndex >= 0) { + const auto fbxMaterial = fbx_node_.GetMaterial(materialIndex); + if (auto glTFMaterialIndex = _convertMaterial(*fbxMaterial)) { + glTFPrimitive.material = *glTFMaterialIndex; + } + } + + glTFMesh.primitives.emplace_back(std::move(glTFPrimitive)); + } + + if (myMeta.blendShapeMeta && + !myMeta.blendShapeMeta->blendShapeDatas.empty()) { + // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#morph-targets + // > Implementation note: A significant number of authoring and client + // implementations associate names with morph targets. > While the + // glTF 2.0 specification currently does not provide a way to specify + // names, most tools use an array of strings, > + // mesh.extras.targetNames, for this purpose. The targetNames array + // and all primitive targets arrays must have the same length. + const auto fbxShapeNames = + myMeta.blendShapeMeta->blendShapeDatas.front().getShapeNames(); + glTFMesh.extensionsAndExtras["extras"]["targetNames"] = fbxShapeNames; + } + + const auto glTFMeshIndex = + _glTFBuilder.add(&fx::gltf::Document::meshes, std::move(glTFMesh)); + + ConvertMeshResult convertMeshResult; + convertMeshResult.glTFMeshIndex = glTFMeshIndex; + if (nodeMeshesSkinData) { + const auto glTFSkinIndex = _createGLTFSkin(*nodeMeshesSkinData); + convertMeshResult.glTFSkinIndex = glTFSkinIndex; + } + + myMeta.meshes = fbx_meshes_; + node_meta_.meshes = myMeta; + + return convertMeshResult; +} + +std::string SceneConverter::_getName(fbxsdk::FbxMesh &fbx_mesh_, + fbxsdk::FbxNode &fbx_node_) { + auto meshName = std::string{fbx_mesh_.GetName()}; + if (!meshName.empty()) { + return meshName; + } else { + return _getName(fbx_node_); + } +} + +std::tuple +SceneConverter::_getGeometrixTransform(fbxsdk::FbxNode &fbx_node_) { + const auto meshTranslation = fbx_node_.GetGeometricTranslation( + fbxsdk::FbxNode::EPivotSet::eSourcePivot); + const auto meshRotation = + fbx_node_.GetGeometricRotation(fbxsdk::FbxNode::EPivotSet::eSourcePivot); + const auto meshScaling = + fbx_node_.GetGeometricScaling(fbxsdk::FbxNode::EPivotSet::eSourcePivot); + const fbxsdk::FbxMatrix vertexTransform = + fbxsdk::FbxAMatrix{meshTranslation, meshRotation, meshScaling}; + const fbxsdk::FbxMatrix normalTransform = + fbxsdk::FbxAMatrix{fbxsdk::FbxVector4(), meshRotation, meshScaling}; + auto normalTransformIT = normalTransform.Inverse().Transpose(); + return {vertexTransform, normalTransformIT}; +} + +fx::gltf::Primitive SceneConverter::_convertMeshAsPrimitive( + fbxsdk::FbxMesh &fbx_mesh_, + std::string_view mesh_name_, + fbxsdk::FbxMatrix *vertex_transform_, + fbxsdk::FbxMatrix *normal_transform_, + std::span fbx_shapes_, + std::span skin_influence_channels_) { + auto vertexLayout = + _getFbxMeshVertexLayout(fbx_mesh_, fbx_shapes_, skin_influence_channels_); + const auto vertexSize = vertexLayout.size; + + using UniqueVertexIndex = std::uint32_t; + + UntypedVertexVector untypedVertexAllocator{vertexSize}; + std::unordered_map + uniqueVertices({}, 0, UntypedVertexHasher{}, + UntypedVertexEqual{vertexSize}); + + const auto nMeshPolygonVertices = fbx_mesh_.GetPolygonVertexCount(); + auto meshPolygonVertices = fbx_mesh_.GetPolygonVertices(); + auto controlPoints = fbx_mesh_.GetControlPoints(); + auto stagingVertex = untypedVertexAllocator.allocate(); + auto processPolygonVertex = + [&](int polygon_vertex_index_) -> UniqueVertexIndex { + auto [stagingVertexData, stagingVertexIndex] = stagingVertex; + auto iControlPoint = meshPolygonVertices[polygon_vertex_index_]; + + fbxsdk::FbxVector4 transformedBasePosition; + fbxsdk::FbxVector4 transformedBaseNormal; + + // Position + { + auto position = controlPoints[iControlPoint]; + if (vertex_transform_) { + position = vertex_transform_->MultNormalize(position); + } + transformedBasePosition = position; + auto pPosition = + reinterpret_cast(stagingVertexData); + FbxVec3Spreader::spread(position, pPosition); + } + + // Normal + if (vertexLayout.normal) { + auto [offset, element] = *vertexLayout.normal; + auto normal = element(iControlPoint, polygon_vertex_index_); + if (normal_transform_) { + normal = normal_transform_->MultNormalize(normal); + } + transformedBaseNormal = normal; + auto pNormal = reinterpret_cast( + stagingVertexData + offset); + FbxVec3Spreader::spread(normal, pNormal); + } + + // UV + for (auto [offset, element] : vertexLayout.uvs) { + auto uv = element(iControlPoint, polygon_vertex_index_); + if (!_options.noFlipV) { + uv[1] = 1.0 - uv[1]; + } + auto pUV = + reinterpret_cast(stagingVertexData + offset); + FbxVec2Spreader::spread(uv, pUV); + } + + // Vertex color + for (auto [offset, element] : vertexLayout.colors) { + auto color = element(iControlPoint, polygon_vertex_index_); + auto pColor = reinterpret_cast( + stagingVertexData + offset); + FbxColorSpreader::spread(color, pColor); + } + + // Skinning + if (vertexLayout.skinning) { + auto [nChannels, jointsOffset, weightsOffset] = *vertexLayout.skinning; + auto pJoints = reinterpret_cast( + stagingVertexData + jointsOffset); + auto pWeights = reinterpret_cast( + stagingVertexData + weightsOffset); + for (decltype(nChannels) iChannel = 0; iChannel < nChannels; ++iChannel) { + pJoints[iChannel] = + skin_influence_channels_[iChannel].joints[iControlPoint]; + pWeights[iChannel] = + skin_influence_channels_[iChannel].weights[iControlPoint]; + } + } + + // Shapes + for (auto &[controlPoints, normalElement] : vertexLayout.shapes) { + { + auto shapePosition = controlPoints.element[iControlPoint]; + if (vertex_transform_) { + shapePosition = vertex_transform_->MultNormalize(shapePosition); + } + auto shapeDiff = shapePosition - transformedBasePosition; + auto pPosition = reinterpret_cast( + stagingVertexData + controlPoints.offset); + FbxVec3Spreader::spread(shapeDiff, pPosition); + } + + if (normalElement && vertexLayout.normal) { + auto [offset, element] = *normalElement; + auto normal = element(iControlPoint, polygon_vertex_index_); + if (normal_transform_) { + normal = normal_transform_->MultNormalize(normal); + } + auto normalDiff = normal - transformedBaseNormal; + auto pNormal = reinterpret_cast( + stagingVertexData + offset); + FbxVec3Spreader::spread(normalDiff, pNormal); + } + } + + auto [rInserted, success] = + uniqueVertices.try_emplace(stagingVertexData, stagingVertexIndex); + auto [insertedVertex, uniqueVertexIndex] = *rInserted; + if (success) { + stagingVertex = untypedVertexAllocator.allocate(); + } + + return uniqueVertexIndex; + }; + + std::vector indices(nMeshPolygonVertices); + for (std::remove_const_t iPolygonVertex = 0; + iPolygonVertex < nMeshPolygonVertices; ++iPolygonVertex) { + indices[iPolygonVertex] = processPolygonVertex(iPolygonVertex); + } + + untypedVertexAllocator.pop_back(); + const auto nUniqueVertices = untypedVertexAllocator.size(); + auto uniqueVerticesData = untypedVertexAllocator.merge(); + + // Debug blend shape data + /*std::vector>> + shapePositions; + std::vector>> + shapeNormals; + if (!vertexLayout.shapes.empty()) { + shapePositions.resize(nUniqueVertices); + shapeNormals.resize(nUniqueVertices); + for (std::remove_const_t iVertex = 0; + iVertex < nUniqueVertices; ++iVertex) { + shapePositions[iVertex].resize(vertexLayout.shapes.size()); + shapeNormals[iVertex].resize(vertexLayout.shapes.size()); + for (int iShape = 0; iShape < vertexLayout.shapes.size(); ++iShape) { + auto p = reinterpret_cast( + uniqueVerticesData.get() + vertexLayout.size * iVertex + + vertexLayout.shapes[iShape].constrolPoints.offset); + shapePositions[iVertex][iShape] = {p[0], p[1], p[2]}; + if (vertexLayout.shapes[iShape].normal) { + auto n = reinterpret_cast( + uniqueVerticesData.get() + vertexLayout.size * iVertex + + vertexLayout.shapes[iShape].normal->offset); + shapeNormals[iVertex][iShape] = {n[0], n[1], n[2]}; + } + } + } + }*/ + + auto bulks = _typeVertices(vertexLayout); + auto glTFPrimitive = _createPrimitive( + bulks, static_cast(fbx_shapes_.size()), nUniqueVertices, + uniqueVerticesData.get(), vertexLayout.size, indices, mesh_name_); + + return glTFPrimitive; +} + +FbxMeshVertexLayout SceneConverter::_getFbxMeshVertexLayout( + fbxsdk::FbxMesh &fbx_mesh_, + std::span fbx_shapes_, + std::span skin_influence_channels_) { + FbxMeshVertexLayout vertexLaytout; + + auto normalElement0 = fbx_mesh_.GetElementNormal(0); + if (normalElement0) { + vertexLaytout.normal.emplace(vertexLaytout.size, + makeFbxLayerElementAccessor(*normalElement0)); + vertexLaytout.size += sizeof(NeutralNormalComponent) * 3; + } + + { + auto nUVElements = fbx_mesh_.GetElementUVCount(); + if (nUVElements) { + vertexLaytout.uvs.resize(nUVElements); + for (decltype(nUVElements) iUVElement = 0; iUVElement < nUVElements; + ++iUVElement) { + vertexLaytout.uvs[iUVElement] = { + vertexLaytout.size, + makeFbxLayerElementAccessor(*fbx_mesh_.GetElementUV(iUVElement))}; + vertexLaytout.size += sizeof(NeutralUVComponent) * 2; + } + } + } + + { + auto nVertexColorElements = fbx_mesh_.GetElementVertexColorCount(); + if (nVertexColorElements) { + vertexLaytout.colors.resize(nVertexColorElements); + for (decltype(nVertexColorElements) iVertexColorElement = 0; + iVertexColorElement < nVertexColorElements; ++iVertexColorElement) { + vertexLaytout.colors[iVertexColorElement] = { + vertexLaytout.size, + makeFbxLayerElementAccessor( + *fbx_mesh_.GetElementVertexColor(iVertexColorElement))}; + vertexLaytout.size += sizeof(NeutralVertexColorComponent) * 4; + } + } + } + + if (!skin_influence_channels_.empty()) { + vertexLaytout.skinning.emplace(); + + const auto nChannels = + static_cast(skin_influence_channels_.size()); + vertexLaytout.skinning->channelCount = + static_cast(nChannels); + + vertexLaytout.skinning->joints = vertexLaytout.size; + vertexLaytout.size += sizeof(NeutralVertexJointComponent) * nChannels; + + vertexLaytout.skinning->weights = vertexLaytout.size; + vertexLaytout.size += sizeof(NeutralVertexWeightComponent) * nChannels; + } + + vertexLaytout.shapes.reserve(fbx_shapes_.size()); + for (std::remove_cv_t iShape = 0; + iShape < fbx_shapes_.size(); ++iShape) { + auto fbxShape = fbx_shapes_[iShape]; + FbxMeshVertexLayout::ShapeLayout shapeLayout; + + shapeLayout.constrolPoints = {vertexLaytout.size, + fbxShape->GetControlPoints()}; + vertexLaytout.size += sizeof(NeutralVertexComponent) * 3; + + if (auto normalLayer = fbxShape->GetElementNormal()) { + shapeLayout.normal.emplace(vertexLaytout.size, + makeFbxLayerElementAccessor(*normalLayer)); + vertexLaytout.size += sizeof(NeutralNormalComponent) * 3; + } + vertexLaytout.shapes.emplace_back(std::move(shapeLayout)); + } + + return vertexLaytout; +} + +fx::gltf::Primitive +SceneConverter::_createPrimitive(std::list &bulks_, + std::uint32_t target_count_, + std::uint32_t vertex_count_, + std::byte *untyped_vertices_, + std::uint32_t vertex_size_, + std::span indices_, + std::string_view primitive_name_) { + fx::gltf::Primitive glTFPrimitive; + glTFPrimitive.targets.resize(target_count_); + + for (const auto &bulk : bulks_) { + auto [bufferViewData, bufferViewIndex] = + _glTFBuilder.createBufferView(bulk.stride * vertex_count_, 0, 0); + auto &glTFBufferView = + _glTFBuilder.get(&fx::gltf::Document::bufferViews)[bufferViewIndex]; + if (bulk.morphTargetHint) { + glTFBufferView.name = + fmt::format("{}/Target-{}", primitive_name_, *bulk.morphTargetHint); + } else { + glTFBufferView.name = + fmt::format("{}", primitive_name_, *bulk.morphTargetHint); + } + if (bulk.vertexBuffer) { + glTFBufferView.target = fx::gltf::BufferView::TargetType::ArrayBuffer; + } + glTFBufferView.byteStride = bulk.stride; + + for (const auto &channel : bulk.channels) { + for (decltype(vertex_count_) iVertex = 0; iVertex < vertex_count_; + ++iVertex) { + channel.writer( + bufferViewData + bulk.stride * iVertex + channel.outOffset, + untyped_vertices_ + vertex_size_ * iVertex + channel.inOffset); + } + + fx::gltf::Accessor glTFAccessor; + glTFAccessor.name = fmt::format( + "{0}{1}/{2}", primitive_name_, + channel.target ? fmt::format("/Target-{}", *channel.target) : "", + channel.name); + glTFAccessor.bufferView = bufferViewIndex; + glTFAccessor.byteOffset = channel.outOffset; + glTFAccessor.count = vertex_count_; + glTFAccessor.type = channel.type; + glTFAccessor.componentType = channel.componentType; + + if (channel.name == "POSITION") { + std::array minPos, maxPos; + std::fill(minPos.begin(), minPos.end(), + std::numeric_limits::infinity()); + std::fill(maxPos.begin(), maxPos.end(), + -std::numeric_limits::infinity()); + for (decltype(vertex_count_) iVertex = 0; iVertex < vertex_count_; + ++iVertex) { + auto pPosition = reinterpret_cast( + untyped_vertices_ + vertex_size_ * iVertex + channel.inOffset); + for (auto i = 0; i < 3; ++i) { + minPos[i] = std::min(pPosition[i], minPos[i]); + maxPos[i] = std::max(pPosition[i], maxPos[i]); + } + } + auto typeCast = [](NeutralVertexComponent v_) { + return static_cast(v_); + }; + glTFAccessor.min.resize(minPos.size()); + std::transform(minPos.begin(), minPos.end(), glTFAccessor.min.begin(), + typeCast); + glTFAccessor.max.resize(maxPos.size()); + std::transform(maxPos.begin(), maxPos.end(), glTFAccessor.max.begin(), + typeCast); + } + + auto glTFAccessorIndex = _glTFBuilder.add(&fx::gltf::Document::accessors, + std::move(glTFAccessor)); + + if (!channel.target) { + glTFPrimitive.attributes.emplace(channel.name, glTFAccessorIndex); + } else { + const auto targetIndex = *channel.target; + auto &target = glTFPrimitive.targets[targetIndex]; + target.emplace(channel.name, glTFAccessorIndex); + } + } + } + + { + using IndexUnit = GLTFComponentTypeStorage< + fx::gltf::Accessor::ComponentType::UnsignedInt>; + auto [bufferViewData, bufferViewIndex] = _glTFBuilder.createBufferView( + static_cast(indices_.size_bytes()), 0, 0); + std::memcpy(bufferViewData, indices_.data(), indices_.size_bytes()); + auto &glTFBufferView = + _glTFBuilder.get(&fx::gltf::Document::bufferViews)[bufferViewIndex]; + + fx::gltf::Accessor glTFAccessor; + glTFAccessor.name = fmt::format("{0}/INDICES", primitive_name_); + glTFAccessor.bufferView = bufferViewIndex; + glTFAccessor.count = static_cast(indices_.size()); + glTFAccessor.type = fx::gltf::Accessor::Type::Scalar; + glTFAccessor.componentType = fx::gltf::Accessor::ComponentType::UnsignedInt; + + auto glTFAccessorIndex = _glTFBuilder.add(&fx::gltf::Document::accessors, + std::move(glTFAccessor)); + glTFPrimitive.indices = glTFAccessorIndex; + } + + return glTFPrimitive; +} + +std::list +SceneConverter::_typeVertices(const FbxMeshVertexLayout &vertex_layout_) { + std::list bulks; + + auto &defaultBulk = bulks.emplace_back(); + defaultBulk.vertexBuffer = true; + + { + defaultBulk.addChannel( + "POSITION", // name + fx::gltf::Accessor::Type::Vec3, // type + fx::gltf::Accessor::ComponentType::Float, // component type + 0, // in offset + untypedVertexCopy< + GLTFComponentTypeStorage, + NeutralVertexComponent, 3> // writer + ); + } + + if (vertex_layout_.normal) { + defaultBulk.addChannel( + "NORMAL", // name + fx::gltf::Accessor::Type::Vec3, // type + fx::gltf::Accessor::ComponentType::Float, // component type + vertex_layout_.normal->offset, // in offset + untypedVertexCopy< + GLTFComponentTypeStorage, + NeutralNormalComponent, 3> // writer + ); + } + + { + auto nUV = vertex_layout_.uvs.size(); + for (decltype(nUV) iUV = 0; iUV < nUV; ++iUV) { + auto &uvLayout = vertex_layout_.uvs[iUV]; + defaultBulk.addChannel( + "TEXCOORD_" + std::to_string(iUV), // name + fx::gltf::Accessor::Type::Vec2, // type + fx::gltf::Accessor::ComponentType::Float, // component type + uvLayout.offset, // in offset + untypedVertexCopy, + NeutralUVComponent, 2> // writer + ); + } + } + + { + auto nColor = vertex_layout_.colors.size(); + for (decltype(nColor) iColor = 0; iColor < nColor; ++iColor) { + auto &colorLayout = vertex_layout_.colors[iColor]; + defaultBulk.addChannel( + "COLOR_" + std::to_string(iColor), // name + fx::gltf::Accessor::Type::Vec4, // type + fx::gltf::Accessor::ComponentType::Float, // component type + colorLayout.offset, // in offset + untypedVertexCopy, + NeutralVertexColorComponent, 4> // writer + ); + } + } + + if (vertex_layout_.skinning) { + const auto &[channelCount, jointsOffset, weightsOffset] = + *vertex_layout_.skinning; + constexpr std::uint32_t setCapacity = 4; + constexpr auto glTFType = fx::gltf::Accessor::Type::Vec4; + const auto set = channelCount % setCapacity == 0 + ? channelCount / setCapacity + : (channelCount / setCapacity + 1); + for (std::remove_const_t iSet = 0; iSet < set; ++iSet) { + const auto setElement = + (iSet == set - 1) ? channelCount % setCapacity : setCapacity; + defaultBulk.addChannel( + "JOINTS_" + std::to_string(iSet), // name + glTFType, // type + fx::gltf::Accessor::ComponentType::UnsignedShort, // component type + jointsOffset + sizeof(NeutralVertexJointComponent) * setCapacity * + iSet, // in offset + makeUntypedVertexCopyN< + GLTFComponentTypeStorage< + fx::gltf::Accessor::ComponentType::UnsignedShort>, + NeutralVertexJointComponent>(setElement) // writer + ); + defaultBulk.addChannel( + "WEIGHTS_" + std::to_string(iSet), // name + glTFType, // type + fx::gltf::Accessor::ComponentType::Float, // component type + weightsOffset + sizeof(NeutralVertexWeightComponent) * setCapacity * + iSet, // in offset + makeUntypedVertexCopyN, + NeutralVertexWeightComponent>( + setElement) // writer + ); + } + } + + for (decltype(vertex_layout_.shapes.size()) iShape = 0; + iShape < vertex_layout_.shapes.size(); ++iShape) { + auto &shape = vertex_layout_.shapes[iShape]; + auto &shapeBulk = bulks.emplace_back(); + shapeBulk.morphTargetHint = static_cast(iShape); + + { + shapeBulk.addChannel( + "POSITION", // name + fx::gltf::Accessor::Type::Vec3, // type + fx::gltf::Accessor::ComponentType::Float, // component type + shape.constrolPoints.offset, // in offset + untypedVertexCopy, + NeutralVertexComponent, 3>, // writer + static_cast(iShape) // target index + ); + } + + if (shape.normal) { + shapeBulk.addChannel( + "NORMAL", // name + fx::gltf::Accessor::Type::Vec3, // type + fx::gltf::Accessor::ComponentType::Float, // component type + shape.normal->offset, // in offset + untypedVertexCopy, + NeutralNormalComponent, 3>, // writer + static_cast(iShape) // target index + ); + } + } + + return bulks; +} + +int SceneConverter::_getTheUniqueMaterialIndex(fbxsdk::FbxMesh &fbx_mesh_) { + const auto nElementMaterialCount = fbx_mesh_.GetElementMaterialCount(); + if (!nElementMaterialCount) { + return -1; + } + if (nElementMaterialCount > 1) { + _warn("We're unable to process multi material layers"); + } + auto elementMaterial0 = fbx_mesh_.GetElementMaterial(0); + if (elementMaterial0->GetMappingMode() != fbxsdk::FbxLayerElement::eAllSame) { + throw std::runtime_error("Mesh is not splitted correctly!"); + } + auto &indexArray = elementMaterial0->GetIndexArray(); + assert(indexArray.GetCount()); + return indexArray.GetAt(0); +} +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/Convert/SceneConverter.Skin.cpp b/Core/Source/bee/Convert/SceneConverter.Skin.cpp new file mode 100644 index 0000000..57d5da0 --- /dev/null +++ b/Core/Source/bee/Convert/SceneConverter.Skin.cpp @@ -0,0 +1,248 @@ + +#include +#include +#include + +namespace bee { +struct SceneConverter::MeshSkinData::Bone::IBMSpreader { + using type = Bone; + + constexpr static auto size = FbxAMatrixSpreader::size; + + template + static void spread(const type &in_, TargetTy_ *out_) { + return FbxAMatrixSpreader::spread(in_.inverseBindMatrix, out_); + } +}; + +/// +/// Things get even more complicated if there are more than one mesh attached to a node. +/// Usually this happened since the only mesh originally attached use multiple materials +/// and we have to split it. +/// +/// Because limits of glTF. There can be only one skin bound to all primitives of a mesh. +/// So we here try to merge all skin data of each mesh into one. +/// The main task is to remap joints in each node mesh. +/// As metioned above, if the multiple meshes were generated because of splitting, +/// their corresponding joint should have equal inverse bind matrices. +/// But if they are multiple in in nature, the inverse bind matrices may differ from each other. +/// In such cases, we do warn. +/// +std::optional +SceneConverter::_extractNodeMeshesSkinData( + const std::vector &fbx_meshes_) { + NodeMeshesSkinData nodeMeshesSkinData; + nodeMeshesSkinData.meshChannels.resize(fbx_meshes_.size()); + + auto &newBones = nodeMeshesSkinData.bones; + auto &meshChannels = nodeMeshesSkinData.meshChannels; + + decltype(fbx_meshes_.size()) nSkinnedMeshes = 0; + for (decltype(fbx_meshes_.size()) iFbxMesh = 0; iFbxMesh < fbx_meshes_.size(); + ++iFbxMesh) { + const auto &fbxMesh = *fbx_meshes_[iFbxMesh]; + auto meshSkinData = _extractSkinData(fbxMesh); + if (!meshSkinData || meshSkinData->bones.empty()) { + continue; + } + ++nSkinnedMeshes; + auto &partBones = meshSkinData->bones; + auto &partChannels = meshSkinData->channels; + if (nSkinnedMeshes == 1) { + newBones = std::move(partBones); + meshChannels[iFbxMesh] = std::move(partChannels); + } else { + // Merge into new skin + for (decltype(partBones.size()) iBone = 0; iBone < partBones.size(); + ++iBone) { + const auto &partBone = partBones[iBone]; + auto rNewBone = + std::find_if(newBones.begin(), newBones.end(), + [&partBone](const MeshSkinData::Bone &new_bone_) { + return partBone.glTFNode == new_bone_.glTFNode; + }); + NeutralVertexJointComponent newIndex = 0; + if (rNewBone != newBones.end()) { + newIndex = + static_cast(rNewBone - newBones.begin()); + if (rNewBone->inverseBindMatrix != partBone.inverseBindMatrix) { + _warn(fmt::format( + "Joint {} has different inverse bind matrices " + "for different meshes.", + _glTFBuilder.get(&fx::gltf::Document::nodes)[partBone.glTFNode] + .name)); + } + } else { + newIndex = static_cast(newBones.size()); + newBones.emplace_back(partBone); + } + for (auto &partChannel : partChannels) { + for (auto &i : partChannel.joints) { + if (i == iBone) { + i = newIndex; + } + } + } + } + // Remap joint indices in channel to new + meshChannels[iFbxMesh] = std::move(partChannels); + } + } + + if (nodeMeshesSkinData.bones.empty()) { + return {}; + } else { + return nodeMeshesSkinData; + } +} + +std::optional +SceneConverter::_extractSkinData(const fbxsdk::FbxMesh &fbx_mesh_) { + using ResultJointType = + decltype(MeshSkinData::InfluenceChannel::joints)::value_type; + using ResultWeightType = + decltype(MeshSkinData::InfluenceChannel::weights)::value_type; + + MeshSkinData skinData; + auto &[skinName, skinJoints, skinChannels] = skinData; + + const auto nControlPoints = fbx_mesh_.GetControlPointsCount(); + std::vector channelsCount( + nControlPoints); + + auto allocateOneChannel = [&skinData, nControlPoints]() { + auto &channel = skinData.channels.emplace_back(); + channel.joints.resize(nControlPoints); + channel.weights.resize(nControlPoints); + }; + + const auto nSkinDeformers = + fbx_mesh_.GetDeformerCount(fbxsdk::FbxDeformer::EDeformerType::eSkin); + for (std::remove_const_t iSkinDeformer = 0; + iSkinDeformer < nSkinDeformers; ++iSkinDeformer) { + const auto skinDeformer = + static_cast(fbx_mesh_.GetDeformer( + iSkinDeformer, fbxsdk::FbxDeformer::EDeformerType::eSkin)); + + skinName = _convertName(skinDeformer->GetName()); + + const auto nClusters = skinDeformer->GetClusterCount(); + for (std::remove_const_t iCluster = 0; + iCluster < nClusters; ++iCluster) { + const auto cluster = skinDeformer->GetCluster(iCluster); + + const auto jointNode = cluster->GetLink(); + const auto glTFNodeIndex = _getNodeMap(*jointNode); + if (!glTFNodeIndex) { + // TODO: may be we should do some work here?? + _warn(fmt::format("The joint node \"{}\" is used for skinning but " + "missed in scene graph.It will be ignored.", + jointNode->GetName())); + continue; + } + + switch (const auto linkMode = cluster->GetLinkMode()) { + case fbxsdk::FbxCluster::eAdditive: + _warn(fmt::format("Unsupported cluster mode \"additive\" [Mesh: {}; " + "ClusterLink: {}]", + fbx_mesh_.GetName(), jointNode->GetName())); + break; + case fbxsdk::FbxCluster::eNormalize: + case fbxsdk::FbxCluster::eTotalOne: + default: + break; + } + + // Index this node to joint array + auto rJointIndex = + std::find_if(skinJoints.begin(), skinJoints.end(), + [&glTFNodeIndex](const MeshSkinData::Bone &joint_) { + return joint_.glTFNode == *glTFNodeIndex; + }); + if (rJointIndex == skinJoints.end()) { + fbxsdk::FbxAMatrix transformMatrix; + cluster->GetTransformMatrix(transformMatrix); + fbxsdk::FbxAMatrix transformLinkMatrix; + cluster->GetTransformLinkMatrix(transformLinkMatrix); + // http://blog.csdn.net/bugrunner/article/details/7232291 + // http://help.autodesk.com/view/FBX/2017/ENU/?guid=__cpp_ref__view_scene_2_draw_scene_8cxx_example_html + const auto inverseBindMatrix = + transformLinkMatrix.Inverse() * transformMatrix; + + skinJoints.emplace_back( + MeshSkinData::Bone{*glTFNodeIndex, inverseBindMatrix}); + rJointIndex = std::prev(skinJoints.end()); + } + const auto jointId = + static_cast(rJointIndex - skinJoints.begin()); + + const auto nControlPointIndices = cluster->GetControlPointIndicesCount(); + const auto controlPointIndices = cluster->GetControlPointIndices(); + const auto controlPointWeights = cluster->GetControlPointWeights(); + for (std::remove_const_t + iControlPointIndex = 0; + iControlPointIndex < nControlPointIndices; ++iControlPointIndex) { + const auto controlPointIndex = controlPointIndices[iControlPointIndex]; + auto &nChannels = channelsCount[controlPointIndex]; + assert(nChannels <= skinData.channels.size()); + if (nChannels == skinData.channels.size()) { + allocateOneChannel(); + } + auto &[joints, weights] = skinData.channels[nChannels]; + joints[controlPointIndex] = jointId; + weights[controlPointIndex] = static_cast( + controlPointWeights[iControlPointIndex]); + ++nChannels; + } + } + } + + if (skinJoints.empty()) { + return {}; + } + + // Normalize weights + for (std::remove_const_t iControlPoint = 0; + iControlPoint < nControlPoints; ++iControlPoint) { + const auto nChannels = channelsCount[iControlPoint]; + if (nChannels > 0) { + auto sum = static_cast(0.0); + for (std::remove_const_t iChannel = 0; + iChannel < nChannels; ++iChannel) { + sum += skinData.channels[iChannel].weights[iControlPoint]; + } + if (sum != 0.0) { + for (std::remove_const_t iChannel = 0; + iChannel < nChannels; ++iChannel) { + skinData.channels[iChannel].weights[iControlPoint] /= sum; + } + } + } + } + + return skinData; +} + +std::uint32_t +SceneConverter::_createGLTFSkin(const NodeMeshesSkinData &skin_data_) { + fx::gltf::Skin glTFSkin; + glTFSkin.joints.resize(skin_data_.bones.size()); + std::transform( + skin_data_.bones.begin(), skin_data_.bones.end(), glTFSkin.joints.begin(), + [](const MeshSkinData::Bone &bone_) { return bone_.glTFNode; }); + + const auto ibmAccessorIndex = + _glTFBuilder.createAccessor( + skin_data_.bones, 0, 0); + auto &ibmAccessor = + _glTFBuilder.get(&fx::gltf::Document::accessors)[ibmAccessorIndex]; + ibmAccessor.name = fmt::format("{}/InverseBindMatrices", skin_data_.name); + glTFSkin.inverseBindMatrices = ibmAccessorIndex; + + const auto glTFSkinIndex = + _glTFBuilder.add(&fx::gltf::Document::skins, std::move(glTFSkin)); + return glTFSkinIndex; +} +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/Convert/SceneConverter.Texture.cpp b/Core/Source/bee/Convert/SceneConverter.Texture.cpp new file mode 100644 index 0000000..5643f1f --- /dev/null +++ b/Core/Source/bee/Convert/SceneConverter.Texture.cpp @@ -0,0 +1,266 @@ + +#include +#include +#include +#include + +namespace bee { +std::optional +SceneConverter::_convertTextureProperty(fbxsdk::FbxProperty &fbx_property_) { + const auto fbxFileTexture = + fbx_property_.GetSrcObject(); + if (!fbxFileTexture) { + return {}; + } else { + auto fbxTextureId = fbxFileTexture->GetUniqueID(); + if (auto r = _textureMap.find(fbxTextureId); r != _textureMap.end()) { + return r->second; + } else { + auto glTFTextureIndex = _convertFileTexture(*fbxFileTexture); + _textureMap.emplace(fbxTextureId, glTFTextureIndex); + return glTFTextureIndex; + } + } +} + +std::optional SceneConverter::_convertFileTexture( + const fbxsdk::FbxFileTexture &fbx_texture_) { + const auto textureName = fbx_texture_.GetName(); + + fx::gltf::Texture glTFTexture; + glTFTexture.name = textureName; + + if (auto glTFSamplerIndex = _convertTextureSampler(fbx_texture_)) { + glTFTexture.sampler = *glTFSamplerIndex; + } + + if (const auto glTFImageIndex = _convertTextureSource(fbx_texture_)) { + glTFTexture.source = *glTFImageIndex; + } + + auto glTFTextureIndex = + _glTFBuilder.add(&fx::gltf::Document::textures, std::move(glTFTexture)); + return glTFTextureIndex; +} + +bool SceneConverter::_hasValidImageExtension( + const std::filesystem::path &path_) { + const auto extName = path_.extension().string(); + const std::array validExtensions{".jpg", ".jpeg", ".png"}; + return std::any_of(validExtensions.begin(), validExtensions.end(), + [&extName](const std::string &valid_extension_) { + return std::equal( + valid_extension_.begin(), valid_extension_.end(), + extName.begin(), extName.end(), [](char a, char b) { + return tolower(a) == tolower(b); + }); + }); +} + +std::optional SceneConverter::_convertTextureSource( + const fbxsdk::FbxFileTexture &fbx_texture_) { + namespace fs = std::filesystem; + + const auto imageName = fbx_texture_.GetName(); + const auto imageFileName = _convertFileName(fbx_texture_.GetFileName()); + const auto imageFileNameRelative = + _convertFileName(fbx_texture_.GetRelativeFileName()); + + std::optional imageFilePath; + if (!imageFileNameRelative.empty()) { + imageFilePath = fs::path(_fbxFileName) / imageFileNameRelative; + } else if (!imageFileName.empty()) { + imageFilePath = imageFileName; + } + + if (imageFilePath) { + std::error_code err; + auto status = fs::status(*imageFilePath, err); + if (err || status.type() != fs::file_type::regular) { + auto image = _searchImage(imageFilePath->stem().string()); + if (image) { + imageFilePath = image; + } + } + } + + if (imageFilePath && !_hasValidImageExtension(*imageFilePath)) { + imageFilePath.reset(); + } + + fx::gltf::Image glTFImage; + glTFImage.name = imageName; + if (imageFilePath) { + auto reference = _processPath(*imageFilePath); + if (reference) { + glTFImage.uri = *reference; + } + } + + if (glTFImage.uri.empty()) { + // Or we got `bufferView: 0`. + // glTFImage.bufferView = -1; + glTFImage.uri = "data:image/" + "png;base64," + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42m" + "P8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="; + } + + glTFImage.extensionsAndExtras["extras"]["fileName"] = imageFileName; + glTFImage.extensionsAndExtras["extras"]["relativeFileName"] = + imageFileNameRelative; + + auto glTFImageIndex = + _glTFBuilder.add(&fx::gltf::Document::images, std::move(glTFImage)); + + return glTFImageIndex; +} + +std::optional +SceneConverter::_searchImage(const std::string_view name_) { + namespace fs = std::filesystem; + for (const auto &location : _options.textureSearch.locations) { + std::error_code err; + fs::directory_iterator dirIter{fs::path{location}, err}; + if (err) { + continue; + } + for (const auto &dirEntry : dirIter) { + if (dirEntry.is_regular_file()) { + if (auto path = dirEntry.path(); + path.stem() == name_ && _hasValidImageExtension(path)) { + return path.string(); + } + } + } + } + return {}; +} + +std::optional +SceneConverter::_processPath(const std::filesystem::path &path_) { + namespace fs = std::filesystem; + + const auto getOutDirNormalized = [this]() { + return fs::path(_options.out).parent_path().lexically_normal(); + }; + + const auto normalizedPath = path_.lexically_normal(); + + const auto toRelative = [getOutDirNormalized](const fs::path &to_) { + const auto outDir = getOutDirNormalized(); + const auto relativePath = to_.lexically_relative(outDir); + auto relativeUrl = relativePath.string(); + std::replace(relativeUrl.begin(), relativeUrl.end(), '\\', '/'); + return relativeUrl; + }; + + switch (_options.pathMode) { + case ConvertOptions::PathMode::absolute: { + return normalizedPath.string(); + } + case ConvertOptions::PathMode::relative: { + return toRelative(normalizedPath); + } + case ConvertOptions::PathMode::strip: { + const auto fileName = normalizedPath.filename(); + return fileName.string(); + } + case ConvertOptions::PathMode::copy: { + const auto outDir = getOutDirNormalized(); + const auto target = outDir / normalizedPath.filename(); + std::error_code err; + fs::create_directories(target.parent_path(), err); + if (err) { + return {}; + } + fs::copy_file(normalizedPath, target, fs::copy_options::overwrite_existing, + err); + if (err) { + return {}; + } + return toRelative(target); + } + case ConvertOptions::PathMode::embedded: { + std::ifstream ifstream(normalizedPath); + if (!ifstream.good()) { + return {}; + } + std::vector fileContent; + ifstream.seekg(0, ifstream.end); + auto fileSize = + static_cast(ifstream.tellg()); + ifstream.seekg(0, ifstream.beg); + fileContent.resize(fileSize); + ifstream.read(fileContent.data(), fileContent.size()); + const auto base64Data = cppcodec::base64_rfc4648::encode( + reinterpret_cast(fileContent.data()), fileContent.size()); + const auto mimeType = + _getMimeTypeFromExtension(normalizedPath.extension().string()); + return fmt::format("data:{};base64,{}", mimeType, base64Data); + } + case ConvertOptions::PathMode::prefer_relative: { + const auto relativePath = + normalizedPath.lexically_relative(getOutDirNormalized()); + auto relativePathStr = relativePath.string(); + const auto dotdot = std::string_view{".."}; + if (relativePath.is_relative() && + !relativePathStr.compare(0, dotdot.size(), dotdot.data())) { + std::replace(relativePathStr.begin(), relativePathStr.end(), '\\', '/'); + return relativePathStr; + } else { + return normalizedPath.string(); + } + break; + } + default: { + assert(false); + break; + } + } + return {}; +} + +std::string +SceneConverter::_getMimeTypeFromExtension(std::string_view ext_name_) { + auto lower = std::string{ext_name_}; + std::transform(lower.begin(), lower.end(), lower.begin(), + [](char c_) { return static_cast(std::tolower(c_)); }); + if (lower == ".jpg" || lower == ".jpeg") { + return "image/jpeg"; + } else if (lower == ".png") { + return "image/png"; + } else { + return "application/octet-stream"; + } +} + +std::optional SceneConverter::_convertTextureSampler( + const fbxsdk::FbxFileTexture &fbx_texture_) { + GLTFSamplerKeys samplerKeys; + samplerKeys.wrapS = _convertWrapMode(fbx_texture_.GetWrapModeU()); + samplerKeys.wrapT = _convertWrapMode(fbx_texture_.GetWrapModeV()); + + auto r = _uniqueSamplers.find(samplerKeys); + if (r == _uniqueSamplers.end()) { + fx::gltf::Sampler glTFSampler; + samplerKeys.set(glTFSampler); + auto glTFSamplerIndex = + _glTFBuilder.add(&fx::gltf::Document::samplers, std::move(glTFSampler)); + r = _uniqueSamplers.emplace(samplerKeys, glTFSamplerIndex).first; + } + + return r->second; +} + +fx::gltf::Sampler::WrappingMode +SceneConverter::_convertWrapMode(fbxsdk::FbxTexture::EWrapMode fbx_wrap_mode_) { + switch (fbx_wrap_mode_) { + case fbxsdk::FbxTexture::EWrapMode::eRepeat: + return fx::gltf::Sampler::WrappingMode::Repeat; + default: + assert(fbx_wrap_mode_ == fbxsdk::FbxTexture::EWrapMode::eClamp); + return fx::gltf::Sampler::WrappingMode::ClampToEdge; + } +} +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/Convert/SceneConverter.cpp b/Core/Source/bee/Convert/SceneConverter.cpp new file mode 100644 index 0000000..0e85efe --- /dev/null +++ b/Core/Source/bee/Convert/SceneConverter.cpp @@ -0,0 +1,203 @@ + +#include +#include +#include +#include + +namespace bee { +SceneConverter::SceneConverter(fbxsdk::FbxManager &fbx_manager_, + fbxsdk::FbxScene &fbx_scene_, + const ConvertOptions &options_, + std::string_view fbx_file_name_, + GLTFBuilder &glTF_builder_) + : _glTFBuilder(glTF_builder_), _fbxManager(fbx_manager_), + _fbxScene(fbx_scene_), _options(options_), _fbxFileName(fbx_file_name_), + _fbxGeometryConverter(&fbx_manager_) { +} + +void SceneConverter::convert() { + _prepareScene(); + _announceNodes(_fbxScene); + for (auto fbxNode : _anncouncedfbxNodes) { + _convertNode(*fbxNode); + } + _convertScene(_fbxScene); + _convertAnimation(_fbxScene); +} + +void SceneConverter::_warn(std::string_view message_) { + std::cout << message_ << "\n"; +} + +fbxsdk::FbxGeometryConverter &SceneConverter::_getGeometryConverter() { + return _fbxGeometryConverter; +} + +void SceneConverter::_prepareScene() { + // Convert axis system + fbxsdk::FbxAxisSystem::OpenGL.ConvertScene(&_fbxScene); + + // Convert system unit + if (_fbxScene.GetGlobalSettings().GetSystemUnit() != + fbxsdk::FbxSystemUnit::m) { + fbxsdk::FbxSystemUnit::ConversionOptions conversionOptions; + conversionOptions.mConvertRrsNodes = false; + conversionOptions.mConvertLimits = true; + conversionOptions.mConvertClusters = true; + conversionOptions.mConvertLightIntensity = true; + conversionOptions.mConvertPhotometricLProperties = true; + conversionOptions.mConvertCameraClipPlanes = true; + fbxsdk::FbxSystemUnit::m.ConvertScene(&_fbxScene, conversionOptions); + } + + // Trianglute the whole scene + _fbxGeometryConverter.Triangulate(&_fbxScene, true); + + // Split meshes per material + _fbxGeometryConverter.SplitMeshesPerMaterial(&_fbxScene, true); +} + +void SceneConverter::_announceNodes(const fbxsdk::FbxScene &fbx_scene_) { + auto rootNode = fbx_scene_.GetRootNode(); + auto nChildren = rootNode->GetChildCount(); + for (auto iChild = 0; iChild < nChildren; ++iChild) { + _announceNode(*rootNode->GetChild(iChild)); + } +} + +void SceneConverter::_announceNode(fbxsdk::FbxNode &fbx_node_) { + _anncouncedfbxNodes.push_back(&fbx_node_); + fx::gltf::Node glTFNode; + auto glTFNodeIndex = + _glTFBuilder.add(&fx::gltf::Document::nodes, std::move(glTFNode)); + _setNodeMap(fbx_node_, glTFNodeIndex); + + auto nChildren = fbx_node_.GetChildCount(); + for (auto iChild = 0; iChild < nChildren; ++iChild) { + _announceNode(*fbx_node_.GetChild(iChild)); + } +} + +void SceneConverter::_setNodeMap(const fbxsdk::FbxNode &fbx_node_, + GLTFBuilder::XXIndex glTF_node_index_) { + _fbxNodeMap.emplace(fbx_node_.GetUniqueID(), glTF_node_index_); +} + +std::optional +SceneConverter::_getNodeMap(const fbxsdk::FbxNode &fbx_node_) { + auto r = _fbxNodeMap.find(fbx_node_.GetUniqueID()); + if (r == _fbxNodeMap.end()) { + return {}; + } else { + return r->second; + } +} + +std::string SceneConverter::_convertName(const char *fbx_name_) { + return fbx_name_; +} + +std::string SceneConverter::_convertFileName(const char *fbx_file_name_) { + return fbx_file_name_; +} + +GLTFBuilder::XXIndex +SceneConverter::_convertScene(fbxsdk::FbxScene &fbx_scene_) { + auto sceneName = _convertName(fbx_scene_.GetName()); + + fx::gltf::Scene glTFScene; + glTFScene.name = sceneName; + + auto rootNode = fbx_scene_.GetRootNode(); + auto nChildren = rootNode->GetChildCount(); + for (auto iChild = 0; iChild < nChildren; ++iChild) { + auto glTFNodeIndex = _getNodeMap(*rootNode->GetChild(iChild)); + assert(glTFNodeIndex); + glTFScene.nodes.push_back(*glTFNodeIndex); + } + + auto glTFSceneIndex = + _glTFBuilder.add(&fx::gltf::Document::scenes, std::move(glTFScene)); + return glTFSceneIndex; +} + +void SceneConverter::_convertNode(fbxsdk::FbxNode &fbx_node_) { + auto glTFNodeIndexX = _getNodeMap(fbx_node_); + assert(glTFNodeIndexX); + auto glTFNodeIndex = *glTFNodeIndexX; + auto &glTFNode = _glTFBuilder.get(&fx::gltf::Document::nodes)[glTFNodeIndex]; + + auto nodeName = _convertName(fbx_node_.GetName()); + glTFNode.name = nodeName; + + auto nChildren = fbx_node_.GetChildCount(); + for (auto iChild = 0; iChild < nChildren; ++iChild) { + auto glTFNodeIndex = _getNodeMap(*fbx_node_.GetChild(iChild)); + assert(glTFNodeIndex); + glTFNode.children.push_back(*glTFNodeIndex); + } + + fbxsdk::FbxTransform::EInheritType inheritType; + fbx_node_.GetTransformationInheritType(inheritType); + if (inheritType == fbxsdk::FbxTransform::eInheritRrSs) { + if (fbx_node_.GetParent() != nullptr) { + _warn(fmt::format("Node {} uses unsupported transform " + "inheritance type 'eInheritRrSs'", + fbx_node_.GetName())); + } + } else if (inheritType == fbxsdk::FbxTransform::eInheritRrs) { + _warn(fmt::format("Node {} uses unsupported transform " + "inheritance type 'eInheritRrs'", + fbx_node_.GetName())); + } + + if (auto fbxLocalTransform = fbx_node_.EvaluateLocalTransform(); + !fbxLocalTransform.IsIdentity()) { + if (auto fbxT = fbxLocalTransform.GetT(); !fbxT.IsZero(3)) { + FbxVec3Spreader::spread(fbxT, glTFNode.translation.data()); + } + if (auto fbxR = fbxLocalTransform.GetQ(); + fbxR.Compare(fbxsdk::FbxQuaternion{})) { + FbxQuatSpreader::spread(fbxR, glTFNode.rotation.data()); + } + if (auto fbxS = fbxLocalTransform.GetS(); + fbxS[0] != 1. || fbxS[1] != 1. || fbxS[2] != 1.) { + FbxVec3Spreader::spread(fbxS, glTFNode.scale.data()); + } + } + + FbxNodeDumpMeta nodeBumpData; + nodeBumpData.glTFNodeIndex = glTFNodeIndex; + + std::vector fbxMeshes; + for (auto nNodeAttributes = fbx_node_.GetNodeAttributeCount(), + iNodeAttribute = 0; + iNodeAttribute < nNodeAttributes; ++iNodeAttribute) { + auto nodeAttribute = fbx_node_.GetNodeAttributeByIndex(iNodeAttribute); + switch (auto attributeType = nodeAttribute->GetAttributeType()) { + case fbxsdk::FbxNodeAttribute::EType::eMesh: + fbxMeshes.push_back(static_cast(nodeAttribute)); + break; + default: + break; + } + } + + if (!fbxMeshes.empty()) { + auto convertMeshResult = + _convertNodeMeshes(nodeBumpData, fbxMeshes, fbx_node_); + if (convertMeshResult) { + glTFNode.mesh = convertMeshResult->glTFMeshIndex; + if (convertMeshResult->glTFSkinIndex) { + glTFNode.skin = *convertMeshResult->glTFSkinIndex; + } + } + } + + _nodeDumpMetaMap.emplace(&fbx_node_, nodeBumpData); +} + +std::string SceneConverter::_getName(fbxsdk::FbxNode &fbx_node_) { + return fbx_node_.GetName(); +} +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/Convert/SceneConverter.h b/Core/Source/bee/Convert/SceneConverter.h new file mode 100644 index 0000000..2a94d82 --- /dev/null +++ b/Core/Source/bee/Convert/SceneConverter.h @@ -0,0 +1,343 @@ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bee { +class SceneConverter { +public: + SceneConverter(fbxsdk::FbxManager &fbx_manager_, + fbxsdk::FbxScene &fbx_scene_, + const ConvertOptions &options_, + std::string_view fbx_file_name_, + GLTFBuilder &glTF_builder_); + + void convert(); + +private: + struct FbxBlendShapeData { + struct Channel { + /// + /// Blend shape deformer index. + /// + int blendShapeIndex; + + /// + /// Channel index. + /// + int blendShapeChannelIndex; + std::string name; + fbxsdk::FbxDouble deformPercent; + std::vector> + targetShapes; + }; + + std::vector channels; + + std::vector getShapes() const { + std::vector shapes; + for (const auto &channelData : channels) { + for (auto &[fbxShape, weight] : channelData.targetShapes) { + shapes.push_back(fbxShape); + } + } + return shapes; + } + + std::vector getShapeNames() const { + std::vector shapeNames; + for (const auto &channelData : channels) { + for (auto &[fbxShape, weight] : channelData.targetShapes) { + shapeNames.push_back(channelData.name); + } + } + return shapeNames; + } + }; + + struct MeshSkinData { + struct Bone { + std::uint32_t glTFNode; + fbxsdk::FbxAMatrix inverseBindMatrix; + + struct IBMSpreader; + }; + + struct InfluenceChannel { + std::vector joints; + std::vector weights; + }; + + std::string name; + + std::vector bones; + + std::vector channels; + }; + + struct NodeMeshesSkinData { + std::string name; + + std::vector bones; + + /// + /// Channels of each mesh. + /// + std::vector> meshChannels; + }; + + struct FbxNodeMeshesBumpMeta { + std::vector meshes; + + struct BlendShapeDumpMeta { + std::vector blendShapeDatas; + }; + + std::optional blendShapeMeta; + }; + + struct FbxNodeDumpMeta { + std::uint32_t glTFNodeIndex; + std::optional meshes; + }; + + struct ConvertMeshResult { + GLTFBuilder::XXIndex glTFMeshIndex; + std::optional glTFSkinIndex; + }; + + struct VertexBulk { + using ChannelWriter = + std::function; + + struct Channel { + std::string name; + fx::gltf::Accessor::Type type; + fx::gltf::Accessor::ComponentType componentType; + std::size_t inOffset; + std::uint32_t outOffset; + ChannelWriter writer; + std::optional target; + }; + + std::optional morphTargetHint; + std::uint32_t stride; + std::list channels; + bool vertexBuffer = false; + + void addChannel(const std::string &name_, + fx::gltf::Accessor::Type type_, + fx::gltf::Accessor::ComponentType component_type_, + std::uint32_t in_offset_, + ChannelWriter writer_, + std::optional target_ = {}) { + channels.emplace_back(Channel{name_, type_, component_type_, in_offset_, + stride, writer_, target_}); + stride += countBytes(type_, component_type_); + } + }; + + struct AnimRange { + fbxsdk::FbxTime::EMode timeMode; + fbxsdk::FbxLongLong firstFrame; + /// + /// Last frame(include). + /// + fbxsdk::FbxLongLong lastFrame; + + fbxsdk::FbxLongLong frames_count() const { + return lastFrame - firstFrame + 1; + } + }; + + struct MorphAnimation { + std::vector times; + std::vector values; + }; + + GLTFBuilder &_glTFBuilder; + fbxsdk::FbxManager &_fbxManager; + fbxsdk::FbxGeometryConverter _fbxGeometryConverter; + fbxsdk::FbxScene &_fbxScene; + const ConvertOptions &_options; + const std::string _fbxFileName; + fbxsdk::FbxTime::EMode _animationTimeMode = fbxsdk::FbxTime::EMode::eFrames24; + std::map _fbxNodeMap; + std::vector _anncouncedfbxNodes; + std::unordered_map + _uniqueSamplers; + std::unordered_map> + _textureMap; + std::unordered_map _nodeDumpMetaMap; + + void _warn(std::string_view message_); + + fbxsdk::FbxGeometryConverter &_getGeometryConverter(); + + void _prepareScene(); + + void _announceNodes(const fbxsdk::FbxScene &fbx_scene_); + + void _announceNode(fbxsdk::FbxNode &fbx_node_); + + void _setNodeMap(const fbxsdk::FbxNode &fbx_node_, + GLTFBuilder::XXIndex glTF_node_index_); + + std::optional + _getNodeMap(const fbxsdk::FbxNode &fbx_node_); + + std::string _convertName(const char *fbx_name_); + + std::string _convertFileName(const char *fbx_file_name_); + + GLTFBuilder::XXIndex _convertScene(fbxsdk::FbxScene &fbx_scene_); + + void _convertNode(fbxsdk::FbxNode &fbx_node_); + + std::string _getName(fbxsdk::FbxNode &fbx_node_); + + std::optional + _convertNodeMeshes(FbxNodeDumpMeta &node_meta_, + const std::vector &fbx_meshes_, + fbxsdk::FbxNode &fbx_node_); + + std::string _getName(fbxsdk::FbxMesh &fbx_mesh_, fbxsdk::FbxNode &fbx_node_); + + std::tuple + _getGeometrixTransform(fbxsdk::FbxNode &fbx_node_); + + fx::gltf::Primitive _convertMeshAsPrimitive( + fbxsdk::FbxMesh &fbx_mesh_, + std::string_view mesh_name_, + fbxsdk::FbxMatrix *vertex_transform_, + fbxsdk::FbxMatrix *normal_transform_, + std::span fbx_shapes_, + std::span skin_influence_channels_); + + FbxMeshVertexLayout _getFbxMeshVertexLayout( + fbxsdk::FbxMesh &fbx_mesh_, + std::span fbx_shapes_, + std::span skin_influence_channels_); + + fx::gltf::Primitive _createPrimitive(std::list &bulks_, + std::uint32_t target_count_, + std::uint32_t vertex_count_, + std::byte *untyped_vertices_, + std::uint32_t vertex_size_, + std::span indices_, + std::string_view primitive_name_); + + std::list + _typeVertices(const FbxMeshVertexLayout &vertex_layout_); + + int _getTheUniqueMaterialIndex(fbxsdk::FbxMesh &fbx_mesh_); + + /// + /// Things get even more complicated if there are more than one mesh attached to a node. + /// Usually this happened since the only mesh originally attached use multiple materials + /// and we have to split it. + /// + /// Because limits of glTF. There can be only one skin bound to all primitives of a mesh. + /// So we here try to merge all skin data of each mesh into one. + /// The main task is to remap joints in each node mesh. + /// As metioned above, if the multiple meshes were generated because of splitting, + /// their corresponding joint should have equal inverse bind matrices. + /// But if they are multiple in in nature, the inverse bind matrices may differ from each other. + /// In such cases, we do warn. + /// + std::optional + _extractNodeMeshesSkinData(const std::vector &fbx_meshes_); + + std::optional + _extractSkinData(const fbxsdk::FbxMesh &fbx_mesh_); + + std::uint32_t _createGLTFSkin(const NodeMeshesSkinData &skin_data_); + + std::optional + _extractdBlendShapeData(const fbxsdk::FbxMesh &fbx_mesh_); + + std::optional + _extractNodeMeshesBlendShape( + const std::vector &fbx_meshes_); + + std::optional + _convertMaterial(fbxsdk::FbxSurfaceMaterial &fbx_material_); + + std::optional + _convertLambertMaterial(fbxsdk::FbxSurfaceLambert &fbx_material_); + + template + static T_ _getMetalnessFromSpecular(const T_ *specular_); + + std::optional + _convertTextureProperty(fbxsdk::FbxProperty &fbx_property_); + + std::optional + _convertFileTexture(const fbxsdk::FbxFileTexture &fbx_texture_); + + static bool _hasValidImageExtension(const std::filesystem::path &path_); + + std::optional + _convertTextureSource(const fbxsdk::FbxFileTexture &fbx_texture_); + + std::optional _searchImage(const std::string_view name_); + + std::optional _processPath(const std::filesystem::path &path_); + + static std::string _getMimeTypeFromExtension(std::string_view ext_name_); + + std::optional + _convertTextureSampler(const fbxsdk::FbxFileTexture &fbx_texture_); + + fx::gltf::Sampler::WrappingMode + _convertWrapMode(fbxsdk::FbxTexture::EWrapMode fbx_wrap_mode_); + + void _convertAnimation(fbxsdk::FbxScene &fbx_scene_); + + fbxsdk::FbxTimeSpan + _getAnimStackTimeSpan(const fbxsdk::FbxAnimStack &fbx_anim_stack_); + + void _convertAnimationLayer(fx::gltf::Animation &glTF_animation_, + fbxsdk::FbxAnimLayer &fbx_anim_layer_, + fbxsdk::FbxScene &fbx_scene_, + const AnimRange &anim_range_); + + void _convertAnimationLayer(fx::gltf::Animation &glTF_animation_, + fbxsdk::FbxAnimLayer &fbx_anim_layer_, + fbxsdk::FbxNode &fbx_node_, + const AnimRange &anim_range_); + + void _extractWeightsAnimation(fx::gltf::Animation &glTF_animation_, + fbxsdk::FbxAnimLayer &fbx_anim_layer_, + fbxsdk::FbxNode &fbx_node_, + const AnimRange &anim_range_); + + void _writeMorphAnimtion(fx::gltf::Animation &glTF_animation_, + const MorphAnimation &morph_animtion_, + std::uint32_t glTF_node_index_, + const fbxsdk::FbxNode &fbx_node_); + + MorphAnimation + _extractWeightsAnimation(fbxsdk::FbxAnimLayer &fbx_anim_layer_, + const fbxsdk::FbxNode &fbx_node_, + fbxsdk::FbxMesh &fbx_mesh_, + const FbxBlendShapeData &blend_shape_data_, + const AnimRange &anim_range_); + + void _extractTrsAnimation(fx::gltf::Animation &glTF_animation_, + fbxsdk::FbxAnimLayer &fbx_anim_layer_, + fbxsdk::FbxNode &fbx_node_, + const AnimRange &anim_range_); +}; +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/Convert/fbxsdk/LayerelementAccessor.h b/Core/Source/bee/Convert/fbxsdk/LayerelementAccessor.h new file mode 100644 index 0000000..82c2f79 --- /dev/null +++ b/Core/Source/bee/Convert/fbxsdk/LayerelementAccessor.h @@ -0,0 +1,55 @@ + +#pragma once + +#include +#include + +namespace bee { +template +using FbxLayerElementAccessor = std::function; + +template +FbxLayerElementAccessor makeFbxLayerElementAccessor( + const fbxsdk::FbxLayerElementTemplate &layer_element_) { + const auto mappingMode = layer_element_.GetMappingMode(); + const auto referenceMode = layer_element_.GetReferenceMode(); + if (referenceMode == fbxsdk::FbxLayerElement::EReferenceMode::eDirect) { + auto &directArray = layer_element_.GetDirectArray(); + switch (mappingMode) { + case fbxsdk::FbxLayerElement::EMappingMode::eByControlPoint: + return + [&directArray](int control_point_index_, int polygon_vertex_index_) { + return directArray[control_point_index_]; + }; + case fbxsdk::FbxLayerElement::EMappingMode::eByPolygonVertex: + return + [&directArray](int control_point_index_, int polygon_vertex_index_) { + return directArray[polygon_vertex_index_]; + }; + default: + throw std::runtime_error("Unknown mapping mode"); + } + } else if (referenceMode == + fbxsdk::FbxLayerElement::EReferenceMode::eIndexToDirect) { + auto &directArray = layer_element_.GetDirectArray(); + auto &indexArray = layer_element_.GetIndexArray(); + switch (mappingMode) { + case fbxsdk::FbxLayerElement::EMappingMode::eByControlPoint: + return [&directArray, &indexArray](int control_point_index_, + int polygon_vertex_index_) { + return directArray[indexArray[control_point_index_]]; + }; + case fbxsdk::FbxLayerElement::EMappingMode::eByPolygonVertex: + return [&directArray, &indexArray](int control_point_index_, + int polygon_vertex_index_) { + return directArray[indexArray[polygon_vertex_index_]]; + }; + default: + throw std::runtime_error("Unknown mapping mode"); + } + } else { + throw std::runtime_error("Unknown reference mode"); + } +} +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/Convert/fbxsdk/ObjectDestroyer.h b/Core/Source/bee/Convert/fbxsdk/ObjectDestroyer.h new file mode 100644 index 0000000..4b87b95 --- /dev/null +++ b/Core/Source/bee/Convert/fbxsdk/ObjectDestroyer.h @@ -0,0 +1,23 @@ + +#pragma once + +#include + +namespace bee { +class FbxObjectDestroyer { +private: + FbxObject *_object; + +public: + FbxObjectDestroyer(FbxObject *object_) : _object(object_) { + } + FbxObjectDestroyer(FbxObjectDestroyer &&) = delete; + FbxObjectDestroyer(const FbxObjectDestroyer &) = delete; + ~FbxObjectDestroyer() { + if (_object) { + _object->Destroy(); + _object = nullptr; + } + } +}; +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/Convert/fbxsdk/Spreader.h b/Core/Source/bee/Convert/fbxsdk/Spreader.h new file mode 100644 index 0000000..14f3ab9 --- /dev/null +++ b/Core/Source/bee/Convert/fbxsdk/Spreader.h @@ -0,0 +1,43 @@ + +#pragma once + +#include +#include + +namespace bee { +template +struct ArraySpreader { + using type = Ty_; + + constexpr static SizeType_ size = Size_; + + template + static void spread(const type &in_, TargetTy_ *out_) { + for (std::remove_const_t i = 0; i < size; ++i) { + out_[i] = static_cast(in_[i]); + } + } +}; + +struct FbxVec2Spreader : ArraySpreader {}; + +struct FbxVec3Spreader : ArraySpreader {}; + +struct FbxQuatSpreader : ArraySpreader {}; + +struct FbxColorSpreader : ArraySpreader {}; + +struct FbxAMatrixSpreader { + using type = fbxsdk::FbxAMatrix; + + constexpr static int size = 16; + + template + static void spread(const type &in_, TargetTy_ *out_) { + for (std::remove_const_t i = 0; i < size; ++i) { + out_[i] = static_cast( + static_cast(in_)[i]); + } + } +}; +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/Converter.cpp b/Core/Source/bee/Converter.cpp new file mode 100644 index 0000000..647e3f7 --- /dev/null +++ b/Core/Source/bee/Converter.cpp @@ -0,0 +1,146 @@ + +#include +#include +#include +#include +#if defined(_MSC_VER) +#undef snprintf +#endif +#include +#include +#include +#include +#include +#include +#include + +namespace bee { +class Converter { +public: + Converter(const ConvertOptions &options_) { + _fbxManager = fbxsdk::FbxManager::Create(); + if (!_fbxManager) { + throw std::runtime_error("Failed to initialize FBX SDK."); + } + + if (options_.fbmDir) { + std::string fbmDirCStr{*options_.fbmDir}; + auto &xRefManager = _fbxManager->GetXRefManager(); + if (!xRefManager.AddXRefProject( + fbxsdk::FbxXRefManager::sEmbeddedFileProject, + fbmDirCStr.data())) { + _warn("Failed to set .fbm dir"); + } + } + } + + ~Converter() { + _fbxManager->Destroy(); + } + + std::string BEE_API convert(std::string_view file_, + const ConvertOptions &options_) { + auto fbxScene = _import(file_); + FbxObjectDestroyer fbxSceneDestroyer{fbxScene}; + GLTFBuilder glTFBuilder; + SceneConverter sceneConverter{*_fbxManager, *fbxScene, options_, file_, + glTFBuilder}; + sceneConverter.convert(); + + GLTFBuilder::BuildOptions buildOptions; + buildOptions.generator = "Cocos FBX to glTF"; + buildOptions.copyright = + "Copyright (c) 2018-2020 Chukong Technologies Inc."; + auto glTFBuildResult = glTFBuilder.build(buildOptions); + auto &glTFDocument = glTFBuilder.document(); + + GLTFWriter defaultWriter; + auto glTFWriter = options_.writer ? options_.writer : &defaultWriter; + + { + const auto nBuffers = + static_cast(glTFDocument.buffers.size()); + for (std::remove_const_t iBuffer = 0; + iBuffer < nBuffers; ++iBuffer) { + auto &glTFBuffer = glTFDocument.buffers[iBuffer]; + const auto &bufferData = glTFBuildResult.buffers[iBuffer]; + std::optional uri; + if (!options_.useDataUriForBuffers) { + uri = glTFWriter->buffer(bufferData.data(), bufferData.size(), + iBuffer, nBuffers != 1); + } + if (!uri) { + auto base64Data = cppcodec::base64_rfc4648::encode( + reinterpret_cast(bufferData.data()), + bufferData.size()); + uri = "data:application/octet-stream;base64," + base64Data; + } + glTFBuffer.uri = *uri; + } + } + + { + const auto nImages = glTFDocument.images.size(); + for (std::remove_const_t iImage = 0; iImage < nImages; + ++iImage) { + } + } + + nlohmann::json glTFJson; + fx::gltf::to_json(glTFJson, glTFDocument); + + return glTFJson.dump(2); + } + +private: + fbxsdk::FbxManager *_fbxManager = nullptr; + + void _warn(std::string_view message_) { + std::cout << message_ << "\n"; + } + + FbxScene *_import(std::string_view file_) { + auto ioSettings = fbxsdk::FbxIOSettings::Create(_fbxManager, IOSROOT); + _fbxManager->SetIOSettings(ioSettings); + + auto fbxImporter = fbxsdk::FbxImporter::Create(_fbxManager, ""); + FbxObjectDestroyer fbxImporterDestroyer{fbxImporter}; + + auto inputFileCStr = std::string(file_); + auto importInitOk = fbxImporter->Initialize(inputFileCStr.c_str(), -1, + _fbxManager->GetIOSettings()); + if (!importInitOk) { + const auto status = fbxImporter->GetStatus(); + throw std::runtime_error("Failed to initialize FBX importer: " + + std::string() + status.GetErrorString()); + } + + if (fbxImporter->IsFBX()) { + fbxImporter->GetIOSettings()->SetBoolProp(EXP_FBX_MODEL, true); + fbxImporter->GetIOSettings()->SetBoolProp(EXP_FBX_MATERIAL, true); + fbxImporter->GetIOSettings()->SetBoolProp(EXP_FBX_TEXTURE, true); + fbxImporter->GetIOSettings()->SetBoolProp(EXP_FBX_EMBEDDED, true); + fbxImporter->GetIOSettings()->SetBoolProp(EXP_FBX_SHAPE, true); + fbxImporter->GetIOSettings()->SetBoolProp(EXP_FBX_GOBO, true); + fbxImporter->GetIOSettings()->SetBoolProp(EXP_FBX_ANIMATION, true); + fbxImporter->GetIOSettings()->SetBoolProp(EXP_FBX_GLOBAL_SETTINGS, true); + } + + auto fbxScene = fbxsdk::FbxScene::Create(_fbxManager, ""); + auto importOk = fbxImporter->Import(fbxScene); + if (!importOk) { + const auto status = fbxImporter->GetStatus(); + throw std::runtime_error("Failed to import scene." + std::string() + + status.GetErrorString()); + } + + return fbxScene; + } +}; + +std::string BEE_API convert(std::string_view file_, + const ConvertOptions &options_) { + Converter converter(options_); + return converter.convert(file_, options_); +} +} // namespace bee diff --git a/Core/Source/bee/Converter.h b/Core/Source/bee/Converter.h new file mode 100644 index 0000000..79b78db --- /dev/null +++ b/Core/Source/bee/Converter.h @@ -0,0 +1,86 @@ + +#pragma once + +#include +#include +#include +#include + +namespace bee { +class GLTFWriter { +public: + virtual std::optional buffer(const std::byte *data_, + std::size_t size_, + std::uint32_t index_, + bool multi_) { + return {}; + } +}; + +struct ConvertOptions { + std::string out; + + GLTFWriter *writer = nullptr; + + std::optional fbmDir; + + bool useDataUriForBuffers = true; + + bool noFlipV = false; + + std::uint32_t animationBakeRate = 30; + + std::uint32_t suspectedAnimationDurationLimit = + 60 * 10; // I think 10 minutes is extraordinary enough... + + struct TextureSearch { + std::vector locations; + } textureSearch; + + enum class PathMode { + /// + /// Uses relative paths for files which are in a subdirectory of the exported location, absolute for any directories outside that. + /// + prefer_relative, + + /// + /// Uses relative paths. + /// + relative, + + /// + /// Uses full paths. + /// + absolute, + + /// + /// Only write the filename and omit the path component. + /// + strip, + + /// + /// Copy the file into output folder and reference using relative path. + /// + copy, + + /// + /// Embed the file. + /// + embedded, + }; + + PathMode pathMode = PathMode::prefer_relative; + + bool export_skin = true; + + bool export_blend_shape = true; + + bool export_trs_animation = true; + + bool export_blend_shape_animation = true; +}; + +std::string BEE_API convert(std::string_view file_, + const ConvertOptions &options_); + +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/GLTFBuilder.cpp b/Core/Source/bee/GLTFBuilder.cpp new file mode 100644 index 0000000..78bf680 --- /dev/null +++ b/Core/Source/bee/GLTFBuilder.cpp @@ -0,0 +1,83 @@ + +#include +#include + +namespace bee { +GLTFBuilder::GLTFBuilder() { + _bufferKeeps.emplace_back(); +} + +GLTFBuilder::BuildResult GLTFBuilder::build(BuildOptions options) { + BuildResult buildResult; + + if (options.copyright) { + _glTFDocument.asset.copyright = *options.copyright; + } + if (options.generator) { + _glTFDocument.asset.generator = *options.generator; + } + + const auto nBuffers = static_cast(_bufferKeeps.size()); + _glTFDocument.buffers.resize(nBuffers); + buildResult.buffers.resize(nBuffers); + for (std::remove_const_t iBuffer = 0; iBuffer < nBuffers; + ++iBuffer) { + const auto &bufferKeep = _bufferKeeps[iBuffer]; + std::uint32_t bufferByteLength = 0; + for (const auto &bufferViewKeep : bufferKeep.bufferViews) { + bufferByteLength += + static_cast(bufferViewKeep.data.size()); + } + + std::vector bufferStorage(bufferByteLength); + std::uint32_t bufferOffset = 0; + for (const auto &bufferViewKeep : bufferKeep.bufferViews) { + auto &bufferView = _glTFDocument.bufferViews[bufferViewKeep.index]; + auto bufferViewSize = + static_cast(bufferViewKeep.data.size()); + bufferView.byteOffset = bufferOffset; + bufferView.buffer = iBuffer; + std::memcpy(bufferStorage.data() + bufferOffset, + bufferViewKeep.data.data(), bufferViewSize); + bufferOffset += bufferViewSize; + } + buildResult.buffers[iBuffer] = std::move(bufferStorage); + + fx::gltf::Buffer glTFBuffer; + glTFBuffer.byteLength = bufferByteLength; + _glTFDocument.buffers[iBuffer] = glTFBuffer; + } + + return buildResult; +} + +const GLTFBuilder::BufferViewInfo GLTFBuilder::createBufferView( + std::uint32_t byte_length_, std::uint32_t align_, XXIndex buffer_) { + assert(buffer_ < _bufferKeeps.size()); + auto &bufferKeep = _bufferKeeps[buffer_]; + auto index = static_cast(_glTFDocument.bufferViews.size()); + fx::gltf::BufferView bufferView; + bufferView.byteLength = byte_length_; + _glTFDocument.bufferViews.push_back(std::move(bufferView)); + std::vector data(byte_length_); + auto pData = data.data(); + BufferViewKeep bufferViewKeep; + bufferViewKeep.index = index; + bufferViewKeep.align = align_; + bufferViewKeep.data = std::move(data); + bufferKeep.bufferViews.push_back(std::move(bufferViewKeep)); + BufferViewInfo bufferViewInfo; + bufferViewInfo.data = pData; + bufferViewInfo.index = index; + return bufferViewInfo; +} + +void GLTFBuilder::useExtension(std::string_view extension_name_) { + std::string extName{extension_name_}; + if (_glTFDocument.extensionsUsed.cend() == + std::find(_glTFDocument.extensionsUsed.cbegin(), + _glTFDocument.extensionsUsed.cend(), extName)) { + _glTFDocument.extensionsUsed.push_back(std::move(extName)); + } +} +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/GLTFBuilder.h b/Core/Source/bee/GLTFBuilder.h new file mode 100644 index 0000000..31c5327 --- /dev/null +++ b/Core/Source/bee/GLTFBuilder.h @@ -0,0 +1,147 @@ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bee { +class GLTFBuilder { +public: + using XXIndex = std::uint32_t; + + using NoImageAttach = std::monostate; + + struct EmbeddedImage { + std::string mimeType; + std::vector data; + }; + + using ImageData = std::variant; + + struct BufferViewInfo { + std::byte *data; + XXIndex index; + }; + + GLTFBuilder(); + + struct BuildOptions { + std::optional copyright; + std::optional generator; + }; + + struct BuildResult { + std::vector> buffers; + }; + + fx::gltf::Document &document() { + return _glTFDocument; + } + + BuildResult build(BuildOptions options = {}); + + const BufferViewInfo createBufferView(std::uint32_t byte_length_, + std::uint32_t align_, + XXIndex buffer_); + + template + XXIndex createAccessor(std::span values_, + std::uint32_t align_, + std::uint32_t buffer_index_, + bool min_max_ = false) { + using SourceTy = typename Spreader_::type; + using TargetTy = GLTFComponentTypeStorage; + + constexpr auto nComponents = countComponents(Type_); + static_assert(Spreader_::size == nComponents); + + auto [bufferViewData, bufferViewIndex] = + createBufferView(countBytes(ComponentType_) * nComponents * + static_cast(values_.size()), + align_, buffer_index_); + for (decltype(values_.size()) i = 0; i < values_.size(); ++i) { + Spreader_::spread(values_[i], + reinterpret_cast(bufferViewData) + + nComponents * i); + } + + fx::gltf::Accessor glTFAccessor; + glTFAccessor.bufferView = bufferViewIndex; + glTFAccessor.count = static_cast(values_.size()); + glTFAccessor.type = Type_; + glTFAccessor.componentType = ComponentType_; + if (min_max_) { + glTFAccessor.min.resize(nComponents); + glTFAccessor.max.resize(nComponents); + for (std::remove_const_t iComp = 0; + iComp < nComponents; ++iComp) { + auto minVal = std::numeric_limits::max(); + auto maxVal = -std::numeric_limits::max(); + for (decltype(values_.size()) iVal = iComp; iVal < values_.size(); + iVal += nComponents) { + const auto val = static_cast(reinterpret_cast( + bufferViewData)[nComponents * iVal + iComp]); + minVal = std::min(minVal, val); + maxVal = std::max(maxVal, val); + } + glTFAccessor.min[iComp] = minVal; + glTFAccessor.max[iComp] = maxVal; + } + } + + auto glTFAccessorIndex = + add(&fx::gltf::Document::accessors, std::move(glTFAccessor)); + return glTFAccessorIndex; + } + + void useExtension(std::string_view extension_name_); + + template struct GLTFDocumentMemberPtrValueType {}; + template + struct GLTFDocumentMemberPtrValueType { + using type = typename T::value_type; + }; + + template < + typename MemberPtr, + typename = std::enable_if_t>> + XXIndex add(MemberPtr where_, + typename GLTFDocumentMemberPtrValueType::type what_) { + auto &set = _glTFDocument.*where_; + auto index = static_cast(set.size()); + set.push_back(std::move(what_)); + return index; + } + + template < + typename MemberPtr, + typename = std::enable_if_t>> + decltype(auto) get(MemberPtr where_) { + return _glTFDocument.*where_; + } + +private: + struct BufferViewKeep { + std::size_t index; + std::size_t align; + std::vector data; + }; + + struct BufferKeep { + std::list bufferViews; + }; + + fx::gltf::Document _glTFDocument; + std::vector _bufferKeeps; + std::list _images; +}; +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/GLTFUtilities.cpp b/Core/Source/bee/GLTFUtilities.cpp new file mode 100644 index 0000000..e69de29 diff --git a/Core/Source/bee/GLTFUtilities.h b/Core/Source/bee/GLTFUtilities.h new file mode 100644 index 0000000..4599b16 --- /dev/null +++ b/Core/Source/bee/GLTFUtilities.h @@ -0,0 +1,76 @@ + +#pragma once + +#include +#include + +namespace bee { +inline constexpr std::uint32_t countComponents(fx::gltf::Accessor::Type type_) { + switch (type_) { + default: + case fx::gltf::Accessor::Type::None: + assert(false); + return 0; + case fx::gltf::Accessor::Type::Scalar: + return 1; + case fx::gltf::Accessor::Type::Vec2: + return 2; + case fx::gltf::Accessor::Type::Vec3: + return 3; + case fx::gltf::Accessor::Type::Vec4: + return 4; + case fx::gltf::Accessor::Type::Mat2: + return 4; + case fx::gltf::Accessor::Type::Mat3: + return 9; + case fx::gltf::Accessor::Type::Mat4: + return 16; + } +} + +inline constexpr std::uint32_t +countBytes(fx::gltf::Accessor::ComponentType component_type_) { + switch (component_type_) { + default: + case fx::gltf::Accessor::ComponentType::None: + assert(false); + return 0; + case fx::gltf::Accessor::ComponentType::Byte: + case fx::gltf::Accessor::ComponentType::UnsignedByte: + return 1; + case fx::gltf::Accessor::ComponentType::Short: + case fx::gltf::Accessor::ComponentType::UnsignedShort: + return 2; + case fx::gltf::Accessor::ComponentType::UnsignedInt: + case fx::gltf::Accessor::ComponentType::Float: + return 4; + } +} + +inline std::uint32_t +countBytes(fx::gltf::Accessor::Type type_, + fx::gltf::Accessor::ComponentType component_type_) { + return countBytes(component_type_) * countComponents(type_); +} + +template +struct GetGLTFComponentTypeStorage {}; +template <> +struct GetGLTFComponentTypeStorage { + using type = float; +}; +template <> +struct GetGLTFComponentTypeStorage< + fx::gltf::Accessor::ComponentType::UnsignedInt> { + using type = std::uint32_t; +}; +template <> +struct GetGLTFComponentTypeStorage< + fx::gltf::Accessor::ComponentType::UnsignedShort> { + using type = std::uint16_t; +}; + +template +using GLTFComponentTypeStorage = + typename GetGLTFComponentTypeStorage::type; +} // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/GLTFWriter.h b/Core/Source/bee/GLTFWriter.h new file mode 100644 index 0000000..e69de29 diff --git a/Core/Source/bee/UntypedVertex.cpp b/Core/Source/bee/UntypedVertex.cpp new file mode 100644 index 0000000..6c94337 --- /dev/null +++ b/Core/Source/bee/UntypedVertex.cpp @@ -0,0 +1,52 @@ +#include +#include + +namespace bee { +std::tuple UntypedVertexVector::allocate() { + if (_nLastPageVertices >= _nVerticesPerPage || _vertexPages.empty()) { + _vertexPages.emplace_back( + std::make_unique(_vertexSize * _nVerticesPerPage)); + _nLastPageVertices = 0; + } + return {_vertexPages.back().get() + _vertexSize * _nLastPageVertices++, + _nextVertexIndex++}; +} + +void UntypedVertexVector::pop_back() { + assert((!_vertexPages.empty()) && _nLastPageVertices); + --_nLastPageVertices; + if (_nLastPageVertices == 0) { + _vertexPages.pop_back(); + _nLastPageVertices = _nVerticesPerPage; + } + --_nextVertexIndex; +} + +std::unique_ptr UntypedVertexVector::merge() { + auto data = std::make_unique(_vertexSize * size()); + if (!_vertexPages.empty()) { + auto iPage = _vertexPages.begin(); + auto iLastFullPage = _vertexPages.end(); + --iLastFullPage; + auto pData = data.get(); + const auto fullPageBytes = _vertexSize * _nVerticesPerPage; + for (; iPage != iLastFullPage; ++iPage, pData += fullPageBytes) { + std::memcpy(pData, (*iPage).get(), fullPageBytes); + } + std::memcpy(pData, (*iPage).get(), _vertexSize * _nLastPageVertices); + } + return data; +} + +std::size_t UntypedVertexHasher::operator()(const UntypedVertex &vertex_) const { + // Referenced from the excellent fbx2glTF VertexHasher + auto position = reinterpret_cast(vertex_); + std::size_t seed = 5381; + const auto hasher = std::hash< + std::remove_const_t>>{}; + seed ^= hasher(position[0]) + 0x9e3779b9 + (seed << 6) + (seed >> 2); + seed ^= hasher(position[1]) + 0x9e3779b9 + (seed << 6) + (seed >> 2); + seed ^= hasher(position[2]) + 0x9e3779b9 + (seed << 6) + (seed >> 2); + return seed; +} +} // namespace bee diff --git a/Core/Source/bee/UntypedVertex.h b/Core/Source/bee/UntypedVertex.h new file mode 100644 index 0000000..87f99b8 --- /dev/null +++ b/Core/Source/bee/UntypedVertex.h @@ -0,0 +1,55 @@ + +#pragma once + +#include +#include +#include +#include +#include + +namespace bee { +using UntypedVertex = std::byte *; + +class UntypedVertexVector { +public: + UntypedVertexVector(std::uint32_t vertex_size_) : _vertexSize(vertex_size_) { + } + + std::uint32_t size() const { + return _nextVertexIndex; + } + + std::tuple allocate(); + + void pop_back(); + + std::unique_ptr merge(); + +private: + using VertexPage = std::unique_ptr; + std::uint32_t _vertexSize; + std::list _vertexPages; + std::uint32_t _nVerticesPerPage = 1024; + std::uint32_t _nLastPageVertices = 0; + std::uint32_t _nextVertexIndex = 0; +}; + +class UntypedVertexHasher { +public: + std::size_t operator()(const UntypedVertex &vertex_) const; +}; + +class UntypedVertexEqual { +public: + UntypedVertexEqual(std::size_t vertex_size_) : _vertexSize(vertex_size_) { + } + + bool operator()(const UntypedVertex &lhs_, const UntypedVertex &rhs_) const { + return 0 == std::memcmp(reinterpret_cast(lhs_), + reinterpret_cast(rhs_), _vertexSize); + } + +private: + std::size_t _vertexSize; +}; +} // namespace bee \ No newline at end of file diff --git a/Core/fx/include/fx/gltf.h b/Core/fx/include/fx/gltf.h new file mode 100644 index 0000000..3ed72a1 --- /dev/null +++ b/Core/fx/include/fx/gltf.h @@ -0,0 +1,1938 @@ +// ------------------------------------------------------------ +// Copyright(c) 2019 Jesse Yurkovich +// Licensed under the MIT License . +// See the LICENSE file in the repo root for full license information. +// ------------------------------------------------------------ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(_MSC_VER) +#undef snprintf +#endif +#include + +#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_MSVC_LANG) && (_MSVC_LANG >= 201703L) && (_MSC_VER >= 1911)) +#define FX_GLTF_HAS_CPP_17 +#include +#endif + +namespace fx +{ +namespace base64 +{ + namespace detail + { + // clang-format off + constexpr std::array EncodeMap = + { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' + }; + + constexpr std::array DecodeMap = + { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + // clang-format on + } // namespace detail + + inline std::string Encode(std::vector const & bytes) + { + const std::size_t length = bytes.size(); + if (length == 0) + { + return {}; + } + + std::string out{}; + out.reserve(((length * 4 / 3) + 3) & (~3u)); // round up to nearest 4 + + uint32_t value = 0; + int32_t bitCount = -6; + for (const uint8_t c : bytes) + { + value = (value << 8u) + c; + bitCount += 8; + while (bitCount >= 0) + { + const uint32_t shiftOperand = bitCount; + out.push_back(detail::EncodeMap.at((value >> shiftOperand) & 0x3fu)); + bitCount -= 6; + } + } + + if (bitCount > -6) + { + const uint32_t shiftOperand = bitCount + 8; + out.push_back(detail::EncodeMap.at(((value << 8u) >> shiftOperand) & 0x3fu)); + } + + while (out.size() % 4 != 0) + { + out.push_back('='); + } + + return out; + } + +#if defined(FX_GLTF_HAS_CPP_17) + inline bool TryDecode(std::string_view in, std::vector & out) +#else + inline bool TryDecode(std::string const & in, std::vector & out) +#endif + { + out.clear(); + + const std::size_t length = in.length(); + if (length == 0) + { + return true; + } + + if (length % 4 != 0) + { + return false; + } + + out.reserve((length / 4) * 3); + + bool invalid = false; + uint32_t value = 0; + int32_t bitCount = -8; + for (std::size_t i = 0; i < length; i++) + { + const uint8_t c = static_cast(in[i]); + const char map = detail::DecodeMap.at(c); + if (map == -1) + { + if (c != '=') // Non base64 character + { + invalid = true; + } + else + { + // Padding characters not where they should be + const std::size_t remaining = length - i - 1; + if (remaining > 1 || (remaining == 1 ? in[i + 1] != '=' : false)) + { + invalid = true; + } + } + + break; + } + + value = (value << 6u) + map; + bitCount += 6; + if (bitCount >= 0) + { + const uint32_t shiftOperand = bitCount; + out.push_back(static_cast(value >> shiftOperand)); + bitCount -= 8; + } + } + + if (invalid) + { + out.clear(); + } + + return !invalid; + } +} // namespace base64 + +namespace gltf +{ + class invalid_gltf_document : public std::runtime_error + { + public: + explicit invalid_gltf_document(char const * message) + : std::runtime_error(message) + { + } + + invalid_gltf_document(char const * message, std::string const & extra) + : std::runtime_error(CreateMessage(message, extra).c_str()) + { + } + + private: + std::string CreateMessage(char const * message, std::string const & extra) + { + return std::string(message).append(" : ").append(extra); + } + }; + + namespace detail + { +#if defined(FX_GLTF_HAS_CPP_17) + template + inline void ReadRequiredField(std::string_view key, nlohmann::json const & json, TTarget & target) +#else + template + inline void ReadRequiredField(TKey && key, nlohmann::json const & json, TTarget & target) +#endif + { + const nlohmann::json::const_iterator iter = json.find(key); + if (iter == json.end()) + { + throw invalid_gltf_document("Required field not found", std::string(key)); + } + + target = iter->get(); + } + +#if defined(FX_GLTF_HAS_CPP_17) + template + inline void ReadOptionalField(std::string_view key, nlohmann::json const & json, TTarget & target) +#else + template + inline void ReadOptionalField(TKey && key, nlohmann::json const & json, TTarget & target) +#endif + { + const nlohmann::json::const_iterator iter = json.find(key); + if (iter != json.end()) + { + target = iter->get(); + } + } + + inline void ReadExtensionsAndExtras(nlohmann::json const & json, nlohmann::json & extensionsAndExtras) + { + const nlohmann::json::const_iterator iterExtensions = json.find("extensions"); + const nlohmann::json::const_iterator iterExtras = json.find("extras"); + if (iterExtensions != json.end()) + { + extensionsAndExtras["extensions"] = *iterExtensions; + } + + if (iterExtras != json.end()) + { + extensionsAndExtras["extras"] = *iterExtras; + } + } + + template + inline void WriteField(std::string const & key, nlohmann::json & json, TValue const & value) + { + if (!value.empty()) + { + json[key] = value; + } + } + + template + inline void WriteField(std::string const & key, nlohmann::json & json, TValue const & value, TValue const & defaultValue) + { + if (value != defaultValue) + { + json[key] = value; + } + } + + inline void WriteExtensions(nlohmann::json & json, nlohmann::json const & extensionsAndExtras) + { + if (!extensionsAndExtras.empty()) + { + for (nlohmann::json::const_iterator it = extensionsAndExtras.begin(); it != extensionsAndExtras.end(); ++it) + { + json[it.key()] = it.value(); + } + } + } + + inline std::string GetDocumentRootPath(std::string const & documentFilePath) + { + const std::size_t pos = documentFilePath.find_last_of("/\\"); + if (pos != std::string::npos) + { + return documentFilePath.substr(0, pos); + } + + return {}; + } + + inline std::string CreateBufferUriPath(std::string const & documentRootPath, std::string const & bufferUri) + { + // Prevent simple forms of path traversal from malicious uri references... + if (bufferUri.empty() || bufferUri.find("..") != std::string::npos || bufferUri.front() == '/' || bufferUri.front() == '\\') + { + throw invalid_gltf_document("Invalid buffer.uri value", bufferUri); + } + + std::string documentRoot = documentRootPath; + if (documentRoot.length() > 0) + { + if (documentRoot.back() != '/') + { + documentRoot.push_back('/'); + } + } + + return documentRoot + bufferUri; + } + + struct ChunkHeader + { + uint32_t chunkLength{}; + uint32_t chunkType{}; + }; + + struct GLBHeader + { + uint32_t magic{}; + uint32_t version{}; + uint32_t length{}; + + ChunkHeader jsonHeader{}; + }; + + constexpr uint32_t DefaultMaxBufferCount = 8; + constexpr uint32_t DefaultMaxMemoryAllocation = 32 * 1024 * 1024; + constexpr std::size_t HeaderSize{ sizeof(GLBHeader) }; + constexpr std::size_t ChunkHeaderSize{ sizeof(ChunkHeader) }; + constexpr uint32_t GLBHeaderMagic = 0x46546c67u; + constexpr uint32_t GLBChunkJSON = 0x4e4f534au; + constexpr uint32_t GLBChunkBIN = 0x004e4942u; + + constexpr char const * const MimetypeApplicationOctet = "data:application/octet-stream;base64"; + constexpr char const * const MimetypeImagePNG = "data:image/png;base64"; + constexpr char const * const MimetypeImageJPG = "data:image/jpeg;base64"; + } // namespace detail + + namespace defaults + { + constexpr std::array IdentityMatrix{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }; + constexpr std::array IdentityRotation{ 0, 0, 0, 1 }; + constexpr std::array IdentityVec4{ 1, 1, 1, 1 }; + constexpr std::array IdentityVec3{ 1, 1, 1 }; + constexpr std::array NullVec3{ 0, 0, 0 }; + constexpr float IdentityScalar = 1; + constexpr float FloatSentinel = 10000; + + constexpr bool AccessorNormalized = false; + + constexpr float MaterialAlphaCutoff = 0.5f; + constexpr bool MaterialDoubleSided = false; + } // namespace defaults + + using Attributes = std::unordered_map; + + struct NeverEmpty + { + bool empty() const noexcept + { + return false; + } + }; + + struct Accessor + { + enum class ComponentType : uint16_t + { + None = 0, + Byte = 5120, + UnsignedByte = 5121, + Short = 5122, + UnsignedShort = 5123, + UnsignedInt = 5125, + Float = 5126 + }; + + enum class Type : uint8_t + { + None, + Scalar, + Vec2, + Vec3, + Vec4, + Mat2, + Mat3, + Mat4 + }; + + struct Sparse + { + struct Indices : NeverEmpty + { + uint32_t bufferView{}; + uint32_t byteOffset{}; + ComponentType componentType{ ComponentType::None }; + + nlohmann::json extensionsAndExtras{}; + }; + + struct Values : NeverEmpty + { + uint32_t bufferView{}; + uint32_t byteOffset{}; + + nlohmann::json extensionsAndExtras{}; + }; + + int32_t count{}; + Indices indices{}; + Values values{}; + + nlohmann::json extensionsAndExtras{}; + + bool empty() const noexcept + { + return count == 0; + } + }; + + int32_t bufferView{ -1 }; + uint32_t byteOffset{}; + uint32_t count{}; + bool normalized{ defaults::AccessorNormalized }; + + ComponentType componentType{ ComponentType::None }; + Type type{ Type::None }; + Sparse sparse{}; + + std::string name; + std::vector max{}; + std::vector min{}; + + nlohmann::json extensionsAndExtras{}; + }; + + struct Animation + { + struct Channel + { + struct Target : NeverEmpty + { + int32_t node{ -1 }; + std::string path{}; + + nlohmann::json extensionsAndExtras{}; + }; + + int32_t sampler{ -1 }; + Target target{}; + + nlohmann::json extensionsAndExtras{}; + }; + + struct Sampler + { + enum class Type + { + Linear, + Step, + CubicSpline + }; + + int32_t input{ -1 }; + int32_t output{ -1 }; + + Type interpolation{ Sampler::Type::Linear }; + + nlohmann::json extensionsAndExtras{}; + }; + + std::string name{}; + std::vector channels{}; + std::vector samplers{}; + + nlohmann::json extensionsAndExtras{}; + }; + + struct Asset : NeverEmpty + { + std::string copyright{}; + std::string generator{}; + std::string minVersion{}; + std::string version{ "2.0" }; + + nlohmann::json extensionsAndExtras{}; + }; + + struct Buffer + { + uint32_t byteLength{}; + + std::string name; + std::string uri; + + nlohmann::json extensionsAndExtras{}; + + std::vector data{}; + + bool IsEmbeddedResource() const noexcept + { + return uri.find(detail::MimetypeApplicationOctet) == 0; + } + + void SetEmbeddedResource() + { + uri = std::string(detail::MimetypeApplicationOctet).append(",").append(base64::Encode(data)); + } + }; + + struct BufferView + { + enum class TargetType : uint16_t + { + None = 0, + ArrayBuffer = 34962, + ElementArrayBuffer = 34963 + }; + + std::string name; + + int32_t buffer{ -1 }; + uint32_t byteOffset{}; + uint32_t byteLength{}; + uint32_t byteStride{}; + + TargetType target{ TargetType::None }; + + nlohmann::json extensionsAndExtras{}; + }; + + struct Camera + { + enum class Type + { + None, + Orthographic, + Perspective + }; + + struct Orthographic : NeverEmpty + { + float xmag{ defaults::FloatSentinel }; + float ymag{ defaults::FloatSentinel }; + float zfar{ -defaults::FloatSentinel }; + float znear{ -defaults::FloatSentinel }; + + nlohmann::json extensionsAndExtras{}; + }; + + struct Perspective : NeverEmpty + { + float aspectRatio{}; + float yfov{}; + float zfar{}; + float znear{}; + + nlohmann::json extensionsAndExtras{}; + }; + + std::string name{}; + Type type{ Type::None }; + + Orthographic orthographic; + Perspective perspective; + + nlohmann::json extensionsAndExtras{}; + }; + + struct Image + { + int32_t bufferView{}; + + std::string name; + std::string uri; + std::string mimeType; + + nlohmann::json extensionsAndExtras{}; + + bool IsEmbeddedResource() const noexcept + { + return uri.find(detail::MimetypeImagePNG) == 0 || uri.find(detail::MimetypeImageJPG) == 0; + } + + void MaterializeData(std::vector & data) const + { + char const * const mimetype = uri.find(detail::MimetypeImagePNG) == 0 ? detail::MimetypeImagePNG : detail::MimetypeImageJPG; + const std::size_t startPos = std::char_traits::length(mimetype) + 1; + +#if defined(FX_GLTF_HAS_CPP_17) + const std::size_t base64Length = uri.length() - startPos; + const bool success = base64::TryDecode({ &uri[startPos], base64Length }, data); +#else + const bool success = base64::TryDecode(uri.substr(startPos), data); +#endif + if (!success) + { + throw invalid_gltf_document("Invalid buffer.uri value", "malformed base64"); + } + } + }; + + struct Material + { + enum class AlphaMode : uint8_t + { + Opaque, + Mask, + Blend + }; + + struct Texture + { + int32_t index{ -1 }; + int32_t texCoord{}; + + nlohmann::json extensionsAndExtras{}; + + bool empty() const noexcept + { + return index == -1; + } + }; + + struct NormalTexture : Texture + { + float scale{ defaults::IdentityScalar }; + }; + + struct OcclusionTexture : Texture + { + float strength{ defaults::IdentityScalar }; + }; + + struct PBRMetallicRoughness + { + std::array baseColorFactor = { defaults::IdentityVec4 }; + Texture baseColorTexture; + + float roughnessFactor{ defaults::IdentityScalar }; + float metallicFactor{ defaults::IdentityScalar }; + Texture metallicRoughnessTexture; + + nlohmann::json extensionsAndExtras{}; + + bool empty() const + { + return baseColorTexture.empty() && metallicRoughnessTexture.empty() && metallicFactor == 1.0f && roughnessFactor == 1.0f && baseColorFactor == defaults::IdentityVec4; + } + }; + + float alphaCutoff{ defaults::MaterialAlphaCutoff }; + AlphaMode alphaMode{ AlphaMode::Opaque }; + + bool doubleSided{ defaults::MaterialDoubleSided }; + + NormalTexture normalTexture; + OcclusionTexture occlusionTexture; + PBRMetallicRoughness pbrMetallicRoughness; + + Texture emissiveTexture; + std::array emissiveFactor = { defaults::NullVec3 }; + + std::string name; + nlohmann::json extensionsAndExtras{}; + }; + + struct Primitive + { + enum class Mode : uint8_t + { + Points = 0, + Lines = 1, + LineLoop = 2, + LineStrip = 3, + Triangles = 4, + TriangleStrip = 5, + TriangleFan = 6 + }; + + int32_t indices{ -1 }; + int32_t material{ -1 }; + + Mode mode{ Mode::Triangles }; + + Attributes attributes{}; + std::vector targets{}; + + nlohmann::json extensionsAndExtras{}; + }; + + struct Mesh + { + std::string name; + + std::vector weights{}; + std::vector primitives{}; + + nlohmann::json extensionsAndExtras{}; + }; + + struct Node + { + std::string name; + + int32_t camera{ -1 }; + int32_t mesh{ -1 }; + int32_t skin{ -1 }; + + std::array matrix{ defaults::IdentityMatrix }; + std::array rotation{ defaults::IdentityRotation }; + std::array scale{ defaults::IdentityVec3 }; + std::array translation{ defaults::NullVec3 }; + + std::vector children{}; + std::vector weights{}; + + nlohmann::json extensionsAndExtras{}; + }; + + struct Sampler + { + enum class MagFilter : uint16_t + { + None, + Nearest = 9728, + Linear = 9729 + }; + + enum class MinFilter : uint16_t + { + None, + Nearest = 9728, + Linear = 9729, + NearestMipMapNearest = 9984, + LinearMipMapNearest = 9985, + NearestMipMapLinear = 9986, + LinearMipMapLinear = 9987 + }; + + enum class WrappingMode : uint16_t + { + ClampToEdge = 33071, + MirroredRepeat = 33648, + Repeat = 10497 + }; + + std::string name; + + MagFilter magFilter{ MagFilter::None }; + MinFilter minFilter{ MinFilter::None }; + + WrappingMode wrapS{ WrappingMode::Repeat }; + WrappingMode wrapT{ WrappingMode::Repeat }; + + nlohmann::json extensionsAndExtras{}; + + bool empty() const noexcept + { + return name.empty() && magFilter == MagFilter::None && minFilter == MinFilter::None && wrapS == WrappingMode::Repeat && wrapT == WrappingMode::Repeat && extensionsAndExtras.empty(); + } + }; + + struct Scene + { + std::string name; + + std::vector nodes{}; + + nlohmann::json extensionsAndExtras{}; + }; + + struct Skin + { + int32_t inverseBindMatrices{ -1 }; + int32_t skeleton{ -1 }; + + std::string name; + std::vector joints{}; + + nlohmann::json extensionsAndExtras{}; + }; + + struct Texture + { + std::string name; + + int32_t sampler{ -1 }; + int32_t source{ -1 }; + + nlohmann::json extensionsAndExtras{}; + }; + + struct Document + { + Asset asset; + + std::vector accessors{}; + std::vector animations{}; + std::vector buffers{}; + std::vector bufferViews{}; + std::vector cameras{}; + std::vector images{}; + std::vector materials{}; + std::vector meshes{}; + std::vector nodes{}; + std::vector samplers{}; + std::vector scenes{}; + std::vector skins{}; + std::vector textures{}; + + int32_t scene{ -1 }; + std::vector extensionsUsed{}; + std::vector extensionsRequired{}; + + nlohmann::json extensionsAndExtras{}; + }; + + struct ReadQuotas + { + uint32_t MaxBufferCount{ detail::DefaultMaxBufferCount }; + uint32_t MaxFileSize{ detail::DefaultMaxMemoryAllocation }; + uint32_t MaxBufferByteLength{ detail::DefaultMaxMemoryAllocation }; + }; + + inline void from_json(nlohmann::json const & json, Accessor::Type & accessorType) + { + std::string type = json.get(); + if (type == "SCALAR") + { + accessorType = Accessor::Type::Scalar; + } + else if (type == "VEC2") + { + accessorType = Accessor::Type::Vec2; + } + else if (type == "VEC3") + { + accessorType = Accessor::Type::Vec3; + } + else if (type == "VEC4") + { + accessorType = Accessor::Type::Vec4; + } + else if (type == "MAT2") + { + accessorType = Accessor::Type::Mat2; + } + else if (type == "MAT3") + { + accessorType = Accessor::Type::Mat3; + } + else if (type == "MAT4") + { + accessorType = Accessor::Type::Mat4; + } + else + { + throw invalid_gltf_document("Unknown accessor.type value", type); + } + } + + inline void from_json(nlohmann::json const & json, Accessor::Sparse::Values & values) + { + detail::ReadRequiredField("bufferView", json, values.bufferView); + + detail::ReadOptionalField("byteOffset", json, values.byteOffset); + + detail::ReadExtensionsAndExtras(json, values.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Accessor::Sparse::Indices & indices) + { + detail::ReadRequiredField("bufferView", json, indices.bufferView); + detail::ReadRequiredField("componentType", json, indices.componentType); + + detail::ReadOptionalField("byteOffset", json, indices.byteOffset); + + detail::ReadExtensionsAndExtras(json, indices.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Accessor::Sparse & sparse) + { + detail::ReadRequiredField("count", json, sparse.count); + detail::ReadRequiredField("indices", json, sparse.indices); + detail::ReadRequiredField("values", json, sparse.values); + + detail::ReadExtensionsAndExtras(json, sparse.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Accessor & accessor) + { + detail::ReadRequiredField("componentType", json, accessor.componentType); + detail::ReadRequiredField("count", json, accessor.count); + detail::ReadRequiredField("type", json, accessor.type); + + detail::ReadOptionalField("bufferView", json, accessor.bufferView); + detail::ReadOptionalField("byteOffset", json, accessor.byteOffset); + detail::ReadOptionalField("max", json, accessor.max); + detail::ReadOptionalField("min", json, accessor.min); + detail::ReadOptionalField("name", json, accessor.name); + detail::ReadOptionalField("normalized", json, accessor.normalized); + detail::ReadOptionalField("sparse", json, accessor.sparse); + + detail::ReadExtensionsAndExtras(json, accessor.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Animation::Channel::Target & animationChannelTarget) + { + detail::ReadRequiredField("path", json, animationChannelTarget.path); + + detail::ReadOptionalField("node", json, animationChannelTarget.node); + + detail::ReadExtensionsAndExtras(json, animationChannelTarget.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Animation::Channel & animationChannel) + { + detail::ReadRequiredField("sampler", json, animationChannel.sampler); + detail::ReadRequiredField("target", json, animationChannel.target); + + detail::ReadExtensionsAndExtras(json, animationChannel.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Animation::Sampler::Type & animationSamplerType) + { + std::string type = json.get(); + if (type == "LINEAR") + { + animationSamplerType = Animation::Sampler::Type::Linear; + } + else if (type == "STEP") + { + animationSamplerType = Animation::Sampler::Type::Step; + } + else if (type == "CUBICSPLINE") + { + animationSamplerType = Animation::Sampler::Type::CubicSpline; + } + else + { + throw invalid_gltf_document("Unknown animation.sampler.interpolation value", type); + } + } + + inline void from_json(nlohmann::json const & json, Animation::Sampler & animationSampler) + { + detail::ReadRequiredField("input", json, animationSampler.input); + detail::ReadRequiredField("output", json, animationSampler.output); + + detail::ReadOptionalField("interpolation", json, animationSampler.interpolation); + + detail::ReadExtensionsAndExtras(json, animationSampler.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Animation & animation) + { + detail::ReadRequiredField("channels", json, animation.channels); + detail::ReadRequiredField("samplers", json, animation.samplers); + + detail::ReadOptionalField("name", json, animation.name); + + detail::ReadExtensionsAndExtras(json, animation.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Asset & asset) + { + detail::ReadRequiredField("version", json, asset.version); + detail::ReadOptionalField("copyright", json, asset.copyright); + detail::ReadOptionalField("generator", json, asset.generator); + detail::ReadOptionalField("minVersion", json, asset.minVersion); + + detail::ReadExtensionsAndExtras(json, asset.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Buffer & buffer) + { + detail::ReadRequiredField("byteLength", json, buffer.byteLength); + + detail::ReadOptionalField("name", json, buffer.name); + detail::ReadOptionalField("uri", json, buffer.uri); + + detail::ReadExtensionsAndExtras(json, buffer.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, BufferView & bufferView) + { + detail::ReadRequiredField("buffer", json, bufferView.buffer); + detail::ReadRequiredField("byteLength", json, bufferView.byteLength); + + detail::ReadOptionalField("byteOffset", json, bufferView.byteOffset); + detail::ReadOptionalField("byteStride", json, bufferView.byteStride); + detail::ReadOptionalField("name", json, bufferView.name); + detail::ReadOptionalField("target", json, bufferView.target); + + detail::ReadExtensionsAndExtras(json, bufferView.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Camera::Type & cameraType) + { + std::string type = json.get(); + if (type == "orthographic") + { + cameraType = Camera::Type::Orthographic; + } + else if (type == "perspective") + { + cameraType = Camera::Type::Perspective; + } + else + { + throw invalid_gltf_document("Unknown camera.type value", type); + } + } + + inline void from_json(nlohmann::json const & json, Camera::Orthographic & camera) + { + detail::ReadRequiredField("xmag", json, camera.xmag); + detail::ReadRequiredField("ymag", json, camera.ymag); + detail::ReadRequiredField("zfar", json, camera.zfar); + detail::ReadRequiredField("znear", json, camera.znear); + + detail::ReadExtensionsAndExtras(json, camera.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Camera::Perspective & camera) + { + detail::ReadRequiredField("yfov", json, camera.yfov); + detail::ReadRequiredField("znear", json, camera.znear); + + detail::ReadOptionalField("aspectRatio", json, camera.aspectRatio); + detail::ReadOptionalField("zfar", json, camera.zfar); + + detail::ReadExtensionsAndExtras(json, camera.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Camera & camera) + { + detail::ReadRequiredField("type", json, camera.type); + + detail::ReadOptionalField("name", json, camera.name); + + detail::ReadExtensionsAndExtras(json, camera.extensionsAndExtras); + + if (camera.type == Camera::Type::Perspective) + { + detail::ReadRequiredField("perspective", json, camera.perspective); + } + else if (camera.type == Camera::Type::Orthographic) + { + detail::ReadRequiredField("orthographic", json, camera.orthographic); + } + } + + inline void from_json(nlohmann::json const & json, Image & image) + { + detail::ReadOptionalField("bufferView", json, image.bufferView); + detail::ReadOptionalField("mimeType", json, image.mimeType); + detail::ReadOptionalField("name", json, image.name); + detail::ReadOptionalField("uri", json, image.uri); + + detail::ReadExtensionsAndExtras(json, image.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Material::AlphaMode & materialAlphaMode) + { + std::string alphaMode = json.get(); + if (alphaMode == "OPAQUE") + { + materialAlphaMode = Material::AlphaMode::Opaque; + } + else if (alphaMode == "MASK") + { + materialAlphaMode = Material::AlphaMode::Mask; + } + else if (alphaMode == "BLEND") + { + materialAlphaMode = Material::AlphaMode::Blend; + } + else + { + throw invalid_gltf_document("Unknown material.alphaMode value", alphaMode); + } + } + + inline void from_json(nlohmann::json const & json, Material::Texture & materialTexture) + { + detail::ReadRequiredField("index", json, materialTexture.index); + detail::ReadOptionalField("texCoord", json, materialTexture.texCoord); + + detail::ReadExtensionsAndExtras(json, materialTexture.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Material::NormalTexture & materialTexture) + { + from_json(json, static_cast(materialTexture)); + detail::ReadOptionalField("scale", json, materialTexture.scale); + + detail::ReadExtensionsAndExtras(json, materialTexture.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Material::OcclusionTexture & materialTexture) + { + from_json(json, static_cast(materialTexture)); + detail::ReadOptionalField("strength", json, materialTexture.strength); + + detail::ReadExtensionsAndExtras(json, materialTexture.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Material::PBRMetallicRoughness & pbrMetallicRoughness) + { + detail::ReadOptionalField("baseColorFactor", json, pbrMetallicRoughness.baseColorFactor); + detail::ReadOptionalField("baseColorTexture", json, pbrMetallicRoughness.baseColorTexture); + detail::ReadOptionalField("metallicFactor", json, pbrMetallicRoughness.metallicFactor); + detail::ReadOptionalField("metallicRoughnessTexture", json, pbrMetallicRoughness.metallicRoughnessTexture); + detail::ReadOptionalField("roughnessFactor", json, pbrMetallicRoughness.roughnessFactor); + + detail::ReadExtensionsAndExtras(json, pbrMetallicRoughness.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Material & material) + { + detail::ReadOptionalField("alphaMode", json, material.alphaMode); + detail::ReadOptionalField("alphaCutoff", json, material.alphaCutoff); + detail::ReadOptionalField("doubleSided", json, material.doubleSided); + detail::ReadOptionalField("emissiveFactor", json, material.emissiveFactor); + detail::ReadOptionalField("emissiveTexture", json, material.emissiveTexture); + detail::ReadOptionalField("name", json, material.name); + detail::ReadOptionalField("normalTexture", json, material.normalTexture); + detail::ReadOptionalField("occlusionTexture", json, material.occlusionTexture); + detail::ReadOptionalField("pbrMetallicRoughness", json, material.pbrMetallicRoughness); + + detail::ReadExtensionsAndExtras(json, material.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Mesh & mesh) + { + detail::ReadRequiredField("primitives", json, mesh.primitives); + + detail::ReadOptionalField("name", json, mesh.name); + detail::ReadOptionalField("weights", json, mesh.weights); + + detail::ReadExtensionsAndExtras(json, mesh.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Node & node) + { + detail::ReadOptionalField("camera", json, node.camera); + detail::ReadOptionalField("children", json, node.children); + detail::ReadOptionalField("matrix", json, node.matrix); + detail::ReadOptionalField("mesh", json, node.mesh); + detail::ReadOptionalField("name", json, node.name); + detail::ReadOptionalField("rotation", json, node.rotation); + detail::ReadOptionalField("scale", json, node.scale); + detail::ReadOptionalField("skin", json, node.skin); + detail::ReadOptionalField("translation", json, node.translation); + + detail::ReadExtensionsAndExtras(json, node.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Primitive & primitive) + { + detail::ReadRequiredField("attributes", json, primitive.attributes); + + detail::ReadOptionalField("indices", json, primitive.indices); + detail::ReadOptionalField("material", json, primitive.material); + detail::ReadOptionalField("mode", json, primitive.mode); + detail::ReadOptionalField("targets", json, primitive.targets); + + detail::ReadExtensionsAndExtras(json, primitive.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Sampler & sampler) + { + detail::ReadOptionalField("magFilter", json, sampler.magFilter); + detail::ReadOptionalField("minFilter", json, sampler.minFilter); + detail::ReadOptionalField("name", json, sampler.name); + detail::ReadOptionalField("wrapS", json, sampler.wrapS); + detail::ReadOptionalField("wrapT", json, sampler.wrapT); + + detail::ReadExtensionsAndExtras(json, sampler.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Scene & scene) + { + detail::ReadOptionalField("name", json, scene.name); + detail::ReadOptionalField("nodes", json, scene.nodes); + + detail::ReadExtensionsAndExtras(json, scene.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Skin & skin) + { + detail::ReadRequiredField("joints", json, skin.joints); + + detail::ReadOptionalField("inverseBindMatrices", json, skin.inverseBindMatrices); + detail::ReadOptionalField("name", json, skin.name); + detail::ReadOptionalField("skeleton", json, skin.skeleton); + + detail::ReadExtensionsAndExtras(json, skin.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Texture & texture) + { + detail::ReadOptionalField("name", json, texture.name); + detail::ReadOptionalField("sampler", json, texture.sampler); + detail::ReadOptionalField("source", json, texture.source); + + detail::ReadExtensionsAndExtras(json, texture.extensionsAndExtras); + } + + inline void from_json(nlohmann::json const & json, Document & document) + { + detail::ReadRequiredField("asset", json, document.asset); + + detail::ReadOptionalField("accessors", json, document.accessors); + detail::ReadOptionalField("animations", json, document.animations); + detail::ReadOptionalField("buffers", json, document.buffers); + detail::ReadOptionalField("bufferViews", json, document.bufferViews); + detail::ReadOptionalField("cameras", json, document.cameras); + detail::ReadOptionalField("materials", json, document.materials); + detail::ReadOptionalField("meshes", json, document.meshes); + detail::ReadOptionalField("nodes", json, document.nodes); + detail::ReadOptionalField("images", json, document.images); + detail::ReadOptionalField("samplers", json, document.samplers); + detail::ReadOptionalField("scene", json, document.scene); + detail::ReadOptionalField("scenes", json, document.scenes); + detail::ReadOptionalField("skins", json, document.skins); + detail::ReadOptionalField("textures", json, document.textures); + + detail::ReadOptionalField("extensionsUsed", json, document.extensionsUsed); + detail::ReadOptionalField("extensionsRequired", json, document.extensionsRequired); + detail::ReadExtensionsAndExtras(json, document.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Accessor::ComponentType const & accessorComponentType) + { + if (accessorComponentType == Accessor::ComponentType::None) + { + throw invalid_gltf_document("Unknown accessor.componentType value"); + } + + json = static_cast(accessorComponentType); + } + + inline void to_json(nlohmann::json & json, Accessor::Type const & accessorType) + { + switch (accessorType) + { + case Accessor::Type::Scalar: + json = "SCALAR"; + break; + case Accessor::Type::Vec2: + json = "VEC2"; + break; + case Accessor::Type::Vec3: + json = "VEC3"; + break; + case Accessor::Type::Vec4: + json = "VEC4"; + break; + case Accessor::Type::Mat2: + json = "MAT2"; + break; + case Accessor::Type::Mat3: + json = "MAT3"; + break; + case Accessor::Type::Mat4: + json = "MAT4"; + break; + default: + throw invalid_gltf_document("Unknown accessor.type value"); + } + } + + inline void to_json(nlohmann::json & json, Accessor::Sparse::Values const & values) + { + detail::WriteField("bufferView", json, values.bufferView, static_cast(-1)); + detail::WriteField("byteOffset", json, values.byteOffset, {}); + detail::WriteExtensions(json, values.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Accessor::Sparse::Indices const & indices) + { + detail::WriteField("componentType", json, indices.componentType, Accessor::ComponentType::None); + detail::WriteField("bufferView", json, indices.bufferView, static_cast(-1)); + detail::WriteField("byteOffset", json, indices.byteOffset, {}); + detail::WriteExtensions(json, indices.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Accessor::Sparse const & sparse) + { + detail::WriteField("count", json, sparse.count, -1); + detail::WriteField("indices", json, sparse.indices); + detail::WriteField("values", json, sparse.values); + detail::WriteExtensions(json, sparse.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Accessor const & accessor) + { + detail::WriteField("bufferView", json, accessor.bufferView, -1); + detail::WriteField("byteOffset", json, accessor.byteOffset, {}); + detail::WriteField("componentType", json, accessor.componentType, Accessor::ComponentType::None); + detail::WriteField("count", json, accessor.count, {}); + detail::WriteField("max", json, accessor.max); + detail::WriteField("min", json, accessor.min); + detail::WriteField("name", json, accessor.name); + detail::WriteField("normalized", json, accessor.normalized, false); + detail::WriteField("sparse", json, accessor.sparse); + detail::WriteField("type", json, accessor.type, Accessor::Type::None); + detail::WriteExtensions(json, accessor.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Animation::Channel::Target const & animationChannelTarget) + { + detail::WriteField("node", json, animationChannelTarget.node, -1); + detail::WriteField("path", json, animationChannelTarget.path); + detail::WriteExtensions(json, animationChannelTarget.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Animation::Channel const & animationChannel) + { + detail::WriteField("sampler", json, animationChannel.sampler, -1); + detail::WriteField("target", json, animationChannel.target); + detail::WriteExtensions(json, animationChannel.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Animation::Sampler::Type const & animationSamplerType) + { + switch (animationSamplerType) + { + case Animation::Sampler::Type::Linear: + json = "LINEAR"; + break; + case Animation::Sampler::Type::Step: + json = "STEP"; + break; + case Animation::Sampler::Type::CubicSpline: + json = "CUBICSPLINE"; + break; + } + } + + inline void to_json(nlohmann::json & json, Animation::Sampler const & animationSampler) + { + detail::WriteField("input", json, animationSampler.input, -1); + detail::WriteField("interpolation", json, animationSampler.interpolation, Animation::Sampler::Type::Linear); + detail::WriteField("output", json, animationSampler.output, -1); + detail::WriteExtensions(json, animationSampler.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Animation const & animation) + { + detail::WriteField("channels", json, animation.channels); + detail::WriteField("name", json, animation.name); + detail::WriteField("samplers", json, animation.samplers); + detail::WriteExtensions(json, animation.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Asset const & asset) + { + detail::WriteField("copyright", json, asset.copyright); + detail::WriteField("generator", json, asset.generator); + detail::WriteField("minVersion", json, asset.minVersion); + detail::WriteField("version", json, asset.version); + detail::WriteExtensions(json, asset.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Buffer const & buffer) + { + detail::WriteField("byteLength", json, buffer.byteLength, {}); + detail::WriteField("name", json, buffer.name); + detail::WriteField("uri", json, buffer.uri); + detail::WriteExtensions(json, buffer.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, BufferView const & bufferView) + { + detail::WriteField("buffer", json, bufferView.buffer, -1); + detail::WriteField("byteLength", json, bufferView.byteLength, {}); + detail::WriteField("byteOffset", json, bufferView.byteOffset, {}); + detail::WriteField("byteStride", json, bufferView.byteStride, {}); + detail::WriteField("name", json, bufferView.name); + detail::WriteField("target", json, bufferView.target, BufferView::TargetType::None); + detail::WriteExtensions(json, bufferView.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Camera::Type const & cameraType) + { + switch (cameraType) + { + case Camera::Type::Orthographic: + json = "orthographic"; + break; + case Camera::Type::Perspective: + json = "perspective"; + break; + default: + throw invalid_gltf_document("Unknown camera.type value"); + } + } + + inline void to_json(nlohmann::json & json, Camera::Orthographic const & camera) + { + detail::WriteField("xmag", json, camera.xmag, defaults::FloatSentinel); + detail::WriteField("ymag", json, camera.ymag, defaults::FloatSentinel); + detail::WriteField("zfar", json, camera.zfar, -defaults::FloatSentinel); + detail::WriteField("znear", json, camera.znear, -defaults::FloatSentinel); + detail::WriteExtensions(json, camera.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Camera::Perspective const & camera) + { + detail::WriteField("aspectRatio", json, camera.aspectRatio, {}); + detail::WriteField("yfov", json, camera.yfov, {}); + detail::WriteField("zfar", json, camera.zfar, {}); + detail::WriteField("znear", json, camera.znear, {}); + detail::WriteExtensions(json, camera.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Camera const & camera) + { + detail::WriteField("name", json, camera.name); + detail::WriteField("type", json, camera.type, Camera::Type::None); + detail::WriteExtensions(json, camera.extensionsAndExtras); + + if (camera.type == Camera::Type::Perspective) + { + detail::WriteField("perspective", json, camera.perspective); + } + else if (camera.type == Camera::Type::Orthographic) + { + detail::WriteField("orthographic", json, camera.orthographic); + } + } + + inline void to_json(nlohmann::json & json, Image const & image) + { + detail::WriteField("bufferView", json, image.bufferView, image.uri.empty() ? -1 : 0); // bufferView or uri need to be written; even if default 0 + detail::WriteField("mimeType", json, image.mimeType); + detail::WriteField("name", json, image.name); + detail::WriteField("uri", json, image.uri); + detail::WriteExtensions(json, image.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Material::AlphaMode const & materialAlphaMode) + { + switch (materialAlphaMode) + { + case Material::AlphaMode::Opaque: + json = "OPAQUE"; + break; + case Material::AlphaMode::Mask: + json = "MASK"; + break; + case Material::AlphaMode::Blend: + json = "BLEND"; + break; + } + } + + inline void to_json(nlohmann::json & json, Material::Texture const & materialTexture) + { + detail::WriteField("index", json, materialTexture.index, -1); + detail::WriteField("texCoord", json, materialTexture.texCoord, 0); + detail::WriteExtensions(json, materialTexture.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Material::NormalTexture const & materialTexture) + { + to_json(json, static_cast(materialTexture)); + detail::WriteField("scale", json, materialTexture.scale, defaults::IdentityScalar); + detail::WriteExtensions(json, materialTexture.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Material::OcclusionTexture const & materialTexture) + { + to_json(json, static_cast(materialTexture)); + detail::WriteField("strength", json, materialTexture.strength, defaults::IdentityScalar); + detail::WriteExtensions(json, materialTexture.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Material::PBRMetallicRoughness const & pbrMetallicRoughness) + { + detail::WriteField("baseColorFactor", json, pbrMetallicRoughness.baseColorFactor, defaults::IdentityVec4); + detail::WriteField("baseColorTexture", json, pbrMetallicRoughness.baseColorTexture); + detail::WriteField("metallicFactor", json, pbrMetallicRoughness.metallicFactor, defaults::IdentityScalar); + detail::WriteField("metallicRoughnessTexture", json, pbrMetallicRoughness.metallicRoughnessTexture); + detail::WriteField("roughnessFactor", json, pbrMetallicRoughness.roughnessFactor, defaults::IdentityScalar); + detail::WriteExtensions(json, pbrMetallicRoughness.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Material const & material) + { + detail::WriteField("alphaCutoff", json, material.alphaCutoff, defaults::MaterialAlphaCutoff); + detail::WriteField("alphaMode", json, material.alphaMode, Material::AlphaMode::Opaque); + detail::WriteField("doubleSided", json, material.doubleSided, defaults::MaterialDoubleSided); + detail::WriteField("emissiveTexture", json, material.emissiveTexture); + detail::WriteField("emissiveFactor", json, material.emissiveFactor, defaults::NullVec3); + detail::WriteField("name", json, material.name); + detail::WriteField("normalTexture", json, material.normalTexture); + detail::WriteField("occlusionTexture", json, material.occlusionTexture); + detail::WriteField("pbrMetallicRoughness", json, material.pbrMetallicRoughness); + + detail::WriteExtensions(json, material.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Mesh const & mesh) + { + detail::WriteField("name", json, mesh.name); + detail::WriteField("primitives", json, mesh.primitives); + detail::WriteField("weights", json, mesh.weights); + detail::WriteExtensions(json, mesh.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Node const & node) + { + detail::WriteField("camera", json, node.camera, -1); + detail::WriteField("children", json, node.children); + detail::WriteField("matrix", json, node.matrix, defaults::IdentityMatrix); + detail::WriteField("mesh", json, node.mesh, -1); + detail::WriteField("name", json, node.name); + detail::WriteField("rotation", json, node.rotation, defaults::IdentityRotation); + detail::WriteField("scale", json, node.scale, defaults::IdentityVec3); + detail::WriteField("skin", json, node.skin, -1); + detail::WriteField("translation", json, node.translation, defaults::NullVec3); + detail::WriteField("weights", json, node.weights); + detail::WriteExtensions(json, node.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Primitive const & primitive) + { + detail::WriteField("attributes", json, primitive.attributes); + detail::WriteField("indices", json, primitive.indices, -1); + detail::WriteField("material", json, primitive.material, -1); + detail::WriteField("mode", json, primitive.mode, Primitive::Mode::Triangles); + detail::WriteField("targets", json, primitive.targets); + detail::WriteExtensions(json, primitive.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Sampler const & sampler) + { + if (!sampler.empty()) + { + detail::WriteField("name", json, sampler.name); + detail::WriteField("magFilter", json, sampler.magFilter, Sampler::MagFilter::None); + detail::WriteField("minFilter", json, sampler.minFilter, Sampler::MinFilter::None); + detail::WriteField("wrapS", json, sampler.wrapS, Sampler::WrappingMode::Repeat); + detail::WriteField("wrapT", json, sampler.wrapT, Sampler::WrappingMode::Repeat); + detail::WriteExtensions(json, sampler.extensionsAndExtras); + } + else + { + // If a sampler is completely empty we still need to write out an empty object for the encompassing array... + json = nlohmann::json::object(); + } + } + + inline void to_json(nlohmann::json & json, Scene const & scene) + { + detail::WriteField("name", json, scene.name); + detail::WriteField("nodes", json, scene.nodes); + detail::WriteExtensions(json, scene.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Skin const & skin) + { + detail::WriteField("inverseBindMatrices", json, skin.inverseBindMatrices, -1); + detail::WriteField("name", json, skin.name); + detail::WriteField("skeleton", json, skin.skeleton, -1); + detail::WriteField("joints", json, skin.joints); + detail::WriteExtensions(json, skin.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Texture const & texture) + { + detail::WriteField("name", json, texture.name); + detail::WriteField("sampler", json, texture.sampler, -1); + detail::WriteField("source", json, texture.source, -1); + detail::WriteExtensions(json, texture.extensionsAndExtras); + } + + inline void to_json(nlohmann::json & json, Document const & document) + { + detail::WriteField("accessors", json, document.accessors); + detail::WriteField("animations", json, document.animations); + detail::WriteField("asset", json, document.asset); + detail::WriteField("buffers", json, document.buffers); + detail::WriteField("bufferViews", json, document.bufferViews); + detail::WriteField("cameras", json, document.cameras); + detail::WriteField("images", json, document.images); + detail::WriteField("materials", json, document.materials); + detail::WriteField("meshes", json, document.meshes); + detail::WriteField("nodes", json, document.nodes); + detail::WriteField("samplers", json, document.samplers); + detail::WriteField("scene", json, document.scene, -1); + detail::WriteField("scenes", json, document.scenes); + detail::WriteField("skins", json, document.skins); + detail::WriteField("textures", json, document.textures); + + detail::WriteField("extensionsUsed", json, document.extensionsUsed); + detail::WriteField("extensionsRequired", json, document.extensionsRequired); + detail::WriteExtensions(json, document.extensionsAndExtras); + } + + namespace detail + { + struct DataContext + { + std::string bufferRootPath{}; + ReadQuotas readQuotas; + + std::vector * binaryData{}; + std::size_t binaryOffset{}; + }; + + inline std::size_t GetFileSize(std::ifstream & file) + { + file.seekg(0, std::ifstream::end); + const std::streampos fileSize = file.tellg(); + file.seekg(0, std::ifstream::beg); + + if (fileSize < 0) + { + throw std::system_error(std::make_error_code(std::errc::io_error)); + } + + return static_cast(fileSize); + } + + inline void MaterializeData(Buffer & buffer) + { + const std::size_t startPos = std::char_traits::length(detail::MimetypeApplicationOctet) + 1; + const std::size_t base64Length = buffer.uri.length() - startPos; + const std::size_t decodedEstimate = base64Length / 4 * 3; + if ((decodedEstimate - 2) > buffer.byteLength) // we need to give room for padding... + { + throw invalid_gltf_document("Invalid buffer.uri value", "malformed base64"); + } + +#if defined(FX_GLTF_HAS_CPP_17) + const bool success = base64::TryDecode({ &buffer.uri[startPos], base64Length }, buffer.data); +#else + const bool success = base64::TryDecode(buffer.uri.substr(startPos), buffer.data); +#endif + if (!success) + { + throw invalid_gltf_document("Invalid buffer.uri value", "malformed base64"); + } + } + + inline Document Create(nlohmann::json const & json, DataContext const & dataContext) + { + Document document = json; + + if (document.buffers.size() > dataContext.readQuotas.MaxBufferCount) + { + throw invalid_gltf_document("Quota exceeded : number of buffers > MaxBufferCount"); + } + + for (auto & buffer : document.buffers) + { + if (buffer.byteLength == 0) + { + throw invalid_gltf_document("Invalid buffer.byteLength value : 0"); + } + + if (buffer.byteLength > dataContext.readQuotas.MaxBufferByteLength) + { + throw invalid_gltf_document("Quota exceeded : buffer.byteLength > MaxBufferByteLength"); + } + + if (!buffer.uri.empty()) + { + if (buffer.IsEmbeddedResource()) + { + detail::MaterializeData(buffer); + } + else + { + std::ifstream fileData(detail::CreateBufferUriPath(dataContext.bufferRootPath, buffer.uri), std::ios::binary); + if (!fileData.good()) + { + throw invalid_gltf_document("Invalid buffer.uri value", buffer.uri); + } + + buffer.data.resize(buffer.byteLength); + fileData.read(reinterpret_cast(&buffer.data[0]), buffer.byteLength); + } + } + else if (dataContext.binaryData != nullptr) + { + std::vector & binary = *dataContext.binaryData; + + const std::size_t HeaderEndOffset = dataContext.binaryOffset + detail::ChunkHeaderSize; + if (binary.size() < HeaderEndOffset) + { + throw invalid_gltf_document("Invalid GLB buffer data"); + } + + detail::ChunkHeader header; + std::memcpy(&header, &binary[dataContext.binaryOffset], detail::ChunkHeaderSize); + + const std::size_t BufferEndOffset = HeaderEndOffset + buffer.byteLength; + if (header.chunkType != detail::GLBChunkBIN || header.chunkLength < buffer.byteLength || binary.size() < BufferEndOffset) + { + throw invalid_gltf_document("Invalid GLB buffer data"); + } + + buffer.data.resize(buffer.byteLength); + std::memcpy(&buffer.data[0], &binary[HeaderEndOffset], buffer.byteLength); + } + } + + return document; + } + + inline void ValidateBuffers(Document const & document, bool useBinaryFormat) + { + if (document.buffers.empty()) + { + throw invalid_gltf_document("Invalid glTF document. A document must have at least 1 buffer."); + } + + bool foundBinaryBuffer = false; + for (std::size_t bufferIndex = 0; bufferIndex < document.buffers.size(); bufferIndex++) + { + Buffer const & buffer = document.buffers[bufferIndex]; + if (buffer.byteLength == 0) + { + throw invalid_gltf_document("Invalid buffer.byteLength value : 0"); + } + + if (buffer.byteLength != buffer.data.size()) + { + throw invalid_gltf_document("Invalid buffer.byteLength value : does not match buffer.data size"); + } + + if (buffer.uri.empty()) + { + foundBinaryBuffer = true; + if (bufferIndex != 0) + { + throw invalid_gltf_document("Invalid glTF document. Only 1 buffer, the very first, is allowed to have an empty buffer.uri field."); + } + } + } + + if (useBinaryFormat && !foundBinaryBuffer) + { + throw invalid_gltf_document("Invalid glTF document. No buffer found which can meet the criteria for saving to a .glb file."); + } + } + + inline void Save(Document const & document, std::string const & documentFilePath, bool useBinaryFormat) + { + nlohmann::json json = document; + + std::size_t externalBufferIndex = 0; + if (useBinaryFormat) + { + detail::GLBHeader header{ detail::GLBHeaderMagic, 2, 0, { 0, detail::GLBChunkJSON } }; + detail::ChunkHeader binHeader{ 0, detail::GLBChunkBIN }; + + std::string jsonText = json.dump(); + + Buffer const & binBuffer = document.buffers.front(); + const uint32_t binPaddedLength = ((binBuffer.byteLength + 3) & (~3u)); + const uint32_t binPadding = binPaddedLength - binBuffer.byteLength; + binHeader.chunkLength = binPaddedLength; + + header.jsonHeader.chunkLength = ((jsonText.length() + 3) & (~3u)); + const uint32_t headerPadding = static_cast(header.jsonHeader.chunkLength - jsonText.length()); + header.length = detail::HeaderSize + header.jsonHeader.chunkLength + detail::ChunkHeaderSize + binHeader.chunkLength; + + std::ofstream fileData(documentFilePath, std::ios::binary); + if (!fileData.good()) + { + throw std::system_error(std::make_error_code(std::errc::io_error)); + } + + const char spaces[3] = { ' ', ' ', ' ' }; + const char nulls[3] = { 0, 0, 0 }; + + fileData.write(reinterpret_cast(&header), detail::HeaderSize); + fileData.write(jsonText.c_str(), jsonText.length()); + fileData.write(&spaces[0], headerPadding); + fileData.write(reinterpret_cast(&binHeader), detail::ChunkHeaderSize); + fileData.write(reinterpret_cast(&binBuffer.data[0]), binBuffer.byteLength); + fileData.write(&nulls[0], binPadding); + + externalBufferIndex = 1; + } + else + { + std::ofstream file(documentFilePath); + if (!file.is_open()) + { + throw std::system_error(std::make_error_code(std::errc::io_error)); + } + + file << json.dump(2); + } + + // The glTF 2.0 spec allows a document to have more than 1 buffer. However, only the first one will be included in the .glb + // All others must be considered as External/Embedded resources. Process them if necessary... + std::string documentRootPath = detail::GetDocumentRootPath(documentFilePath); + for (; externalBufferIndex < document.buffers.size(); externalBufferIndex++) + { + Buffer const & buffer = document.buffers[externalBufferIndex]; + if (!buffer.IsEmbeddedResource()) + { + std::ofstream fileData(detail::CreateBufferUriPath(documentRootPath, buffer.uri), std::ios::binary); + if (!fileData.good()) + { + throw invalid_gltf_document("Invalid buffer.uri value", buffer.uri); + } + + fileData.write(reinterpret_cast(&buffer.data[0]), buffer.byteLength); + } + } + } + } // namespace detail + + inline Document LoadFromText(std::string const & documentFilePath, ReadQuotas const & readQuotas = {}) + { + try + { + nlohmann::json json; + { + std::ifstream file(documentFilePath); + if (!file.is_open()) + { + throw std::system_error(std::make_error_code(std::errc::no_such_file_or_directory)); + } + + file >> json; + } + + return detail::Create(json, { detail::GetDocumentRootPath(documentFilePath), readQuotas }); + } + catch (invalid_gltf_document &) + { + throw; + } + catch (std::system_error &) + { + throw; + } + catch (...) + { + std::throw_with_nested(invalid_gltf_document("Invalid glTF document. See nested exception for details.")); + } + } + + inline Document LoadFromBinary(std::string const & documentFilePath, ReadQuotas const & readQuotas = {}) + { + try + { + std::vector binary{}; + { + std::ifstream file(documentFilePath, std::ios::binary); + if (!file.is_open()) + { + throw std::system_error(std::make_error_code(std::errc::no_such_file_or_directory)); + } + + const std::size_t fileSize = detail::GetFileSize(file); + if (fileSize < detail::HeaderSize) + { + throw invalid_gltf_document("Invalid GLB file"); + } + + if (fileSize > readQuotas.MaxFileSize) + { + throw invalid_gltf_document("Quota exceeded : file size > MaxFileSize"); + } + + binary.resize(fileSize); + file.read(reinterpret_cast(&binary[0]), fileSize); + } + + detail::GLBHeader header; + std::memcpy(&header, &binary[0], detail::HeaderSize); + if (header.magic != detail::GLBHeaderMagic || + header.jsonHeader.chunkType != detail::GLBChunkJSON || + header.jsonHeader.chunkLength + detail::HeaderSize > header.length || + header.jsonHeader.chunkLength + detail::HeaderSize > binary.size()) + { + throw invalid_gltf_document("Invalid GLB header"); + } + + return detail::Create( + nlohmann::json::parse({ &binary[detail::HeaderSize], header.jsonHeader.chunkLength }), + { detail::GetDocumentRootPath(documentFilePath), readQuotas, &binary, header.jsonHeader.chunkLength + detail::HeaderSize }); + } + catch (invalid_gltf_document &) + { + throw; + } + catch (std::system_error &) + { + throw; + } + catch (...) + { + std::throw_with_nested(invalid_gltf_document("Invalid glTF document. See nested exception for details.")); + } + } + + inline void Save(Document const & document, std::string const & documentFilePath, bool useBinaryFormat) + { + try + { + detail::ValidateBuffers(document, useBinaryFormat); + + detail::Save(document, documentFilePath, useBinaryFormat); + } + catch (invalid_gltf_document &) + { + throw; + } + catch (std::system_error &) + { + throw; + } + catch (...) + { + std::throw_with_nested(invalid_gltf_document("Invalid glTF document. See nested exception for details.")); + } + } +} // namespace gltf + +// A general-purpose utility to format an exception hierarchy into a string for output +inline void FormatException(std::string & output, std::exception const & ex, int level = 0) +{ + output.append(std::string(level, ' ')).append(ex.what()); + try + { + std::rethrow_if_nested(ex); + } + catch (std::exception const & e) + { + FormatException(output.append("\n"), e, level + 2); + } +} + +} // namespace fx + +#undef FX_GLTF_HAS_CPP_17 diff --git a/Node.js-Port/CMakeLists.txt b/Node.js-Port/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..9b045e1 --- /dev/null +++ b/README.rst @@ -0,0 +1,22 @@ + +# FBX in(the name of) glTF + +This is a FBX to glTF file format converter. + +# Why + +This tool is essentially used as a part of the Cocos Creator. +In former, Cocos Creator supports FBX file format through the excellent [FBX2glTF](https://github.com/facebookincubator/FBX2glTF). + +But Cocos team has to find another approach because: +- FBX2glTF store the glTF result files onto disk and Creator read the files. This is the only way that Creator can communicate with FBX2glTF. + File system I/O is slow. +- Author of FBX2glTF is tired. +- FBX is complex and all exporters working for it are buggy. We usually need to fix strange issues. + +# Thanks + +Again, the FBX is complex and specification-less. In development, we often reference from or are inspired from the following predecessors: + +- [FBX2glTF](https://github.com/facebookincubator/FBX2glTF) +- [claygl](https://github.com/pissang/claygl) \ No newline at end of file