commit 276a0dfb8681bf0860180c6cb3674a3c47347098 Author: Par Winzell Date: Fri Oct 13 01:46:12 2017 -0700 Initial commit. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b883b1e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,178 @@ +cmake_minimum_required(VERSION 3.5) +project(FBX2glTF) + +if ("${CMAKE_CURRENT_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}") + message(FATAL_ERROR + "Building from within the source tree is not supported.\n" + "Hint: mkdir -p build; cmake -H. -Bbuild; make -Cbuild\n") +endif () + +set(CMAKE_CXX_STANDARD 11) +find_package(Threads REQUIRED) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}") +include(ExternalProject) + +# FBX +find_package(FBX REQUIRED) +if (NOT FBXSDK_FOUND) + message(FATAL_ERROR + "Can't find FBX SDK in either:\n" + " - Mac OS X: ${FBXSDK_APPLE_ROOT}\n" + " - Windows: ${FBXSDK_WINDOWS_ROOT}\n" + " - Linux: ${FBXSDK_LINUX_ROOT}" +) +endif() + +# DRACO +ExternalProject_Add(Draco + UPDATE_DISCONNECTED TRUE + GIT_REPOSITORY https://github.com/google/draco + PREFIX draco + INSTALL_DIR + CMAKE_ARGS + -DCMAKE_INSTALL_PREFIX= +) +set(DRACO_INCLUDE_DIR "${CMAKE_BINARY_DIR}/draco/include") +if (WIN32) + set(DRACO_LIB "${CMAKE_BINARY_DIR}/draco/lib/dracoenc.lib") +else() + set(DRACO_LIB "${CMAKE_BINARY_DIR}/draco/lib/libdracoenc.a") +endif() + +# MATHFU +set(mathfu_build_benchmarks OFF CACHE BOOL "") +set(mathfu_build_tests OFF CACHE BOOL "") +ExternalProject_Add(MathFu + UPDATE_DISCONNECTED TRUE + GIT_REPOSITORY https://github.com/google/mathfu + PREFIX mathfu + CONFIGURE_COMMAND ${CMAKE_COMMAND} -E echo "Skipping MathFu configure step." + BUILD_COMMAND ${CMAKE_COMMAND} -E echo "Skipping MathFu build step." + INSTALL_COMMAND ${CMAKE_COMMAND} -E echo "Skipping MathFu install step." +) +set(MATHFU_INCLUDE_DIRS + "${CMAKE_BINARY_DIR}/mathfu/src/MathFu/include/" + "${CMAKE_BINARY_DIR}/mathfu/src/MathFu/dependencies/vectorial/include") + +# JSON +ExternalProject_Add(Json + UPDATE_DISCONNECTED TRUE + GIT_REPOSITORY https://github.com/nlohmann/json + PREFIX json + CONFIGURE_COMMAND ${CMAKE_COMMAND} -E echo "Skipping JSON configure step." + BUILD_COMMAND ${CMAKE_COMMAND} -E echo "Skipping JSON build step." + INSTALL_COMMAND ${CMAKE_COMMAND} -E echo "Skipping JSON install step." +) +set(JSON_INCLUDE_DIR "${CMAKE_BINARY_DIR}/json/src/Json/src") + +# cppcodec +ExternalProject_Add(CPPCodec + UPDATE_DISCONNECTED TRUE + GIT_REPOSITORY https://github.com/tplgy/cppcodec + PREFIX cppcodec + CONFIGURE_COMMAND ${CMAKE_COMMAND} -E echo "Skipping CPPCodec configure step." + BUILD_COMMAND ${CMAKE_COMMAND} -E echo "Skipping CPPCodec build step." + INSTALL_COMMAND ${CMAKE_COMMAND} -E echo "Skipping CPPCodec install step." +) +set(CPPCODEC_INCLUDE_DIR "${CMAKE_BINARY_DIR}/cppcodec/src/CPPCodec") + +# CXXOPTS +ExternalProject_Add(CxxOpts + UPDATE_DISCONNECTED TRUE + GIT_REPOSITORY https://github.com/jarro2783/cxxopts + PREFIX cxxopts + CONFIGURE_COMMAND ${CMAKE_COMMAND} -E echo "Skipping cxxopts configure step." + BUILD_COMMAND ${CMAKE_COMMAND} -E echo "Skipping cxxopts build step." + INSTALL_COMMAND ${CMAKE_COMMAND} -E echo "Skipping cxxopts install step." +) +set(CXXOPTS_INCLUDE_DIR "${CMAKE_BINARY_DIR}/cxxopts/src/CxxOpts/include") + +# FMT +ExternalProject_Add(Fmt + UPDATE_DISCONNECTED TRUE + GIT_REPOSITORY https://github.com/fmtlib/fmt + CMAKE_CACHE_ARGS "-DFMT_DOC:BOOL=OFF" "-DFMT_INSTALL:BOOL=ON" "-DFMT_TEST:BOOL=OFF" + CMAKE_ARGS + -DCMAKE_INSTALL_PREFIX= + PREFIX fmt +) +set(FMT_INCLUDE_DIR "${CMAKE_BINARY_DIR}/fmt/include") +if (WIN32) + set(FMT_LIB "${CMAKE_BINARY_DIR}/fmt/lib/fmt.lib") +else() + set(FMT_LIB "${CMAKE_BINARY_DIR}/fmt/lib/libfmt.a") +endif() + +if (APPLE) + find_library(CF_FRAMEWORK CoreFoundation) + message("CoreFoundation Framework: ${CF_FRAMEWORK}") + set(FRAMEWORKS ${CF_FRAMEWORK}) +endif() + +set(SOURCE_FILES + src/utils/File_Utils.cpp + src/utils/Image_Utils.cpp + src/utils/String_Utils.cpp + src/main.cpp + src/Fbx2Raw.cpp + src/Raw2Gltf.cpp + src/RawModel.cpp + src/glTF/BufferData.cpp + src/glTF/MaterialData.cpp + src/glTF/MeshData.cpp + src/glTF/NodeData.cpp + src/glTF/PrimitiveData.cpp + src/glTF/BufferViewData.cpp + src/glTF/BufferViewData.h + src/glTF/AccessorData.cpp + src/glTF/AccessorData.h + src/glTF/ImageData.cpp + src/glTF/TextureData.cpp + src/glTF/SkinData.cpp + src/glTF/AnimationData.cpp + src/glTF/CameraData.cpp + src/glTF/SceneData.cpp +) + +add_executable(FBX2glTF ${SOURCE_FILES}) + +add_dependencies(FBX2glTF + Draco + MathFu + Json + CxxOpts + CPPCodec + Fmt +) + +if (NOT MSVC) + # Disable annoying & spammy warning from FBX SDK header file + target_compile_options(FBX2glTF PRIVATE + "-Wno-null-dereference" + "-Wunused" + ) +endif() + +target_link_libraries(FBX2glTF + ${FRAMEWORKS} + ${CMAKE_DL_LIBS} + ${CMAKE_THREAD_LIBS_INIT} + ${DRACO_LIB} + ${FMT_LIB} + optimized ${FBXSDK_LIBRARY} + debug ${FBXSDK_LIBRARY_DEBUG} +) + +target_include_directories(FBX2glTF PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${FBXSDK_INCLUDE_DIR} + ${DRACO_INCLUDE_DIR} + ${MATHFU_INCLUDE_DIRS} + ${JSON_INCLUDE_DIR} + ${CXXOPTS_INCLUDE_DIR} + ${CPPCODEC_INCLUDE_DIR} + ${FMT_INCLUDE_DIR} +) + +install (TARGETS FBX2glTF DESTINATION bin) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3e375f1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,29 @@ +# Contributing to FBX2glTF +We want to make contributing to this project as easy and transparent as +possible. + +## Pull Requests +We actively welcome your pull requests. + +1. Fork the repo and create your branch from `master`. +2. Ensure your code matches the style of existing source. +3. In case of behavioural changes, update this documentation. +4. If you haven't already, complete the Contributor License Agreement ("CLA"). + +## Contributor License Agreement ("CLA") +In order to accept your pull request, we need you to submit a CLA. You only need +to do this once to work on any of Facebook's open source projects. + +Complete your CLA here: + +## Issues +We use GitHub issues to track public bugs. Please ensure your description is +clear and has sufficient instructions to be able to reproduce the issue. + +Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe +disclosure of security bugs. In those cases, please go through the process +outlined on that page and do not file a public issue. + +## License +By contributing to FBX2glTF, you agree that your contributions will be licensed +under the LICENSE file in the root directory of this source tree. diff --git a/FindFBX.cmake b/FindFBX.cmake new file mode 100644 index 0000000..4244631 --- /dev/null +++ b/FindFBX.cmake @@ -0,0 +1,102 @@ +# +# Helper function for finding the FBX SDK. +# Cribbed & tweaked from https://github.com/floooh/fbxc/ +# +# params: FBXSDK_VERSION +# FBXSDK_SDKS +# +# sets: FBXSDK_FOUND +# FBXSDK_DIR +# FBXSDK_LIBRARY +# FBXSDK_LIBRARY_DEBUG +# FBXSDK_INCLUDE_DIR +# + +# semi-hack to detect architecture +if( CMAKE_SIZEOF_VOID_P MATCHES 8 ) + # void ptr = 8 byte --> x86_64 + set(ARCH_32 OFF) +else() + # void ptr != 8 byte --> x86 + set(ARCH_32 OFF) +endif() + +set(FBXSDK_VERSION "2018.1.1" CACHE STRING "Precise version string of FBX SDK to use.") + +set(_fbxsdk_vstudio_version "vs2015") + +message("Looking for FBX SDK version: ${FBXSDK_VERSION}") + +if (DEFINED FBXSDK_SDKS) + get_filename_component(FBXSDK_SDKS_ABS ${FBXSDK_SDKS} ABSOLUTE) + + set(FBXSDK_APPLE_ROOT "${FBXSDK_SDKS_ABS}/Darwin/${FBXSDK_VERSION}") + set(FBXSDK_LINUX_ROOT "${FBXSDK_SDKS_ABS}/Linux/${FBXSDK_VERSION}") + set(FBXSDK_WINDOWS_ROOT "${FBXSDK_SDKS_ABS}/Windows/${FBXSDK_VERSION}") +else() + set(FBXSDK_APPLE_ROOT + "/Applications/Autodesk/FBX SDK/${FBXSDK_VERSION}") + set(FBXSDK_LINUX_ROOT + "/usr") + set(FBXSDK_WINDOWS_ROOT + "C:/Program Files/Autodesk/FBX/FBX SDK/${FBXSDK_VERSION}") +endif() + +if (APPLE) + set(_fbxsdk_root "${FBXSDK_APPLE_ROOT}") + set(_fbxsdk_libdir_debug "lib/clang/debug") + set(_fbxsdk_libdir_release "lib/clang/release") + set(_fbxsdk_libname_debug "libfbxsdk.a") + set(_fbxsdk_libname_release "libfbxsdk.a") +elseif (WIN32) + set(_fbxsdk_root "${FBXSDK_WINDOWS_ROOT}") + if (ARCH_32) + set(_fbxsdk_libdir_debug "lib/${_fbxsdk_vstudio_version}/x86/debug") + set(_fbxsdk_libdir_release "lib/${_fbxsdk_vstudio_version}/x86/release") + else() + set(_fbxsdk_libdir_debug "lib/${_fbxsdk_vstudio_version}/x64/debug") + set(_fbxsdk_libdir_release "lib/${_fbxsdk_vstudio_version}/x64/release") + endif() + set(_fbxsdk_libname_debug "libfbxsdk-md.lib") + set(_fbxsdk_libname_release "libfbxsdk-md.lib") +elseif (UNIX) + set(_fbxsdk_root "${FBXSDK_LINUX_ROOT}") + if (ARCH_32) + set(_fbxsdk_libdir_debug "lib/gcc4/x86/debug") + set(_fbxsdk_libdir_release "lib/gcc4/x86/release") + else() + set(_fbxsdk_libdir_debug "lib/gcc4/x64/debug") + set(_fbxsdk_libdir_release "lib/gcc4/x64/release") + endif() + set(_fbxsdk_libname_debug "libfbxsdk.a") + set(_fbxsdk_libname_release "libfbxsdk.a") +else() + message(FATAL_ERROR, "Unknown platform. Can't find FBX SDK.") +endif() + +# should point the the FBX SDK installation dir +set(FBXSDK_ROOT "${_fbxsdk_root}") +message("FBXSDK_ROOT: ${FBXSDK_ROOT}") + +# find header dir and libs +find_path(FBXSDK_INCLUDE_DIR "fbxsdk.h" + NO_CMAKE_FIND_ROOT_PATH + PATHS ${FBXSDK_ROOT} + PATH_SUFFIXES "include") +message("FBXSDK_INCLUDE_DIR: ${FBXSDK_INCLUDE_DIR}") + +find_library(FBXSDK_LIBRARY ${_fbxsdk_libname_release} + NO_CMAKE_FIND_ROOT_PATH + PATHS "${FBXSDK_ROOT}/${_fbxsdk_libdir_release}") +message("FBXSDK_LIBRARY: ${FBXSDK_LIBRARY}") + +find_library(FBXSDK_LIBRARY_DEBUG ${_fbxsdk_libname_debug} + NO_CMAKE_FIND_ROOT_PATH + PATHS "${FBXSDK_ROOT}/${_fbxsdk_libdir_debug}") +message("FBXSDK_LIBRARY_DEBUG: ${FBXSDK_LIBRARY_DEBUG}") + +if (FBXSDK_INCLUDE_DIR AND FBXSDK_LIBRARY AND FBXSDK_LIBRARY_DEBUG) + set(FBXSDK_FOUND YES) +else() + set(FBXSDK_FOUND NO) +endif() diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be6addb --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +BSD License + +For FBX2glTF software + +Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/PATENTS b/PATENTS new file mode 100644 index 0000000..4168ccd --- /dev/null +++ b/PATENTS @@ -0,0 +1,33 @@ +Additional Grant of Patent Rights Version 2 + +"Software" means the FBX2glTF software contributed by Facebook, Inc. + +Facebook, Inc. ("Facebook") hereby grants to each recipient of the Software +("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable +(subject to the termination provision below) license under any Necessary +Claims, to make, have made, use, sell, offer to sell, import, and otherwise +transfer the Software. For avoidance of doubt, no license is granted under +Facebook’s rights in any patent claims that are infringed by (i) modifications +to the Software made by you or any third party or (ii) the Software in +combination with any software or other technology. + +The license granted hereunder will terminate, automatically and without notice, +if you (or any of your subsidiaries, corporate affiliates or agents) initiate +directly or indirectly, or take a direct financial interest in, any Patent +Assertion: (i) against Facebook or any of its subsidiaries or corporate +affiliates, (ii) against any party if such Patent Assertion arises in whole or +in part from any software, technology, product or service of Facebook or any of +its subsidiaries or corporate affiliates, or (iii) against any party relating +to the Software. Notwithstanding the foregoing, if Facebook or any of its +subsidiaries or corporate affiliates files a lawsuit alleging patent +infringement against you in the first instance, and you respond by filing a +patent infringement counterclaim in that lawsuit against that party that is +unrelated to the Software, the license granted hereunder will not terminate +under section (i) of this paragraph due to such counterclaim. + +A "Necessary Claim" is a claim of a patent owned by Facebook that is +necessarily infringed by the Software standing alone. + +A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, +or contributory infringement or inducement to infringe any patent, including a +cross-claim or counterclaim. diff --git a/README.md b/README.md new file mode 100644 index 0000000..055f8e7 --- /dev/null +++ b/README.md @@ -0,0 +1,168 @@ +# FBX2glTF + +This is a command line tool for converting 3D model assets on Autodesk's +venerable [FBX](https://www.autodesk.com/products/fbx/overview) format to +[glTF 2.0](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0), +a modern runtime asset delivery format. + +## Building & Running + +This tool has been tested on Linux, Mac OS X and Windows. It requires CMake 3.5+ +and a reasonably C++11 compliant toolchain. + +We currently depend on the open source projects +[Draco](https://github.com/google/draco), +[MathFu](https://github.com/google/mathfu), +[Json](https://github.com/nlohmann/json), +[cppcodec](https://github.com/tplgy/cppcodec), +[cxxopts](https://github.com/jarro2783/cxxopts), +and [fmt](https://github.com/fmtlib/fmt); +all of which are automatically downloaded, configured and built. + +You must manually download and install the +[Autodesk FBX SDK](https://www.autodesk.com/products/fbx/overview) 2018.1.1 and +accept its license agreement. Once installed, the build system will attempt to +find the SDK in its default location for each system. + +Once that's all done... + +### Linux and MacOS X +Compilation on Unix machines should be as simple as: + +``` + > cd + > cmake -H. -Bbuild + > make -Cbuild +``` + +If all goes well, you will end up with a statically linked executable that can +be invoked like so: +``` + > ./build/FBX2glTF ~/models/butterfly.fbx +``` + +Or perhaps, as part of a more complex pipeline: +``` + > ./build/FBX2glTF --binary --draco --flip-v \ + --khr-materials-common \ + --input ~/models/source/butterfly.fbx \ + --output ~/models/target/butterfly.glb +``` + +### Windows + +Windows users may [download](https://cmake.org/download) CMake for Windows, +install it and [run it](https://cmake.org/runningcmake/) on the FBX2glTF +checkout (choose a build directory distinct from the source). As part of this +process, you will be asked to choose which generator to use; it should be fine +to pick any recent Visual Studio option relevant to your system. + +Note that the CMAKE_BUILD_TYPE variable from the Unix Makefile system is +entirely ignored here; the Visual Studio solution that's generated handles all +the canonical build types -- Debug, Release, MinSizeRel, and so on. You will +choose which one to build in the Visual Studio IDE. + +## Conversion Process +The actual translation begins with the FBX SDK parsing the input file, and ends +with the generation of the core `JSON` description that forms the core of glTF, +along with binary buffers that hold geometry and animations (and optionally also +emedded resources such as textures.) + +In the process, each node and mesh in the FBX is ripped apart into a long list +of surfaces and associated triangles, with a material assigned to each one. A +similar process happens in reverse when we construct meshes and materials that +conform to the expectations of the glTF format. + +### Animations +Every animation in the FBX file becomes an animation in the glTF file. The +method used is one of "baking": we step through the interval of time spanned by +the animation, keyframe by keyframe, calculate the local transform of each node, +and whenever we find any node that's rotated, translated or scaled, we record +that fact in the output. + +This method has the benefit of being simple and precise. It has the drawback of +creating potentially very large files. The more complex the animation rig, the +less avoidable this situation is. + +There are two future enhancements we hope to see for animations: +- Version 2.0 of glTF brought us support for expressing quadratic animation + curves, where previously we had only had linear. Not coincidentally, quadratic + splines are one of the key ways animations are expressed inside the FBX. When + we find such a curve, it would be more efficient to output it without baking + it into a long sequence of linear approximations. +- Perhaps more useful in practice is the idea of compressing animation curves + the same way we use Draco to compress meshes (see below). Like geometry, + animations are highly redundant -- each new value is highly predictable from + preceding values. If Draco extends its support for animations (it's on their + roadmap), or if someone else develops a glTF extension for animation + compression, we will likely add support in this tool. + +### Materials + +With glTF 2.0, we leaped headlong into physically-based rendering (BPR), where +canonical way of expressing what a mesh looks like is by describing its visible +material in fundamental attributes like "how rough is this surface". + +By contrast, FBX's material support remains in the older world of Lambert and +Phong, with much simpler illumination and shading models. These are modes are +largely incompatible (for example, textures in the old workflow often contain +baked lighting that would arise naturally in a PBR environment). + +Some material settings remain well supported and transfer automatically: + - Emissive constants and textures + - Occlusion maps + - Normal maps + +This leaves the other traditional settings of Lambert: + - Ambient -- this is anathema in the PBR world, where such effects should + emerge naturally from the fundamental colour of the material and any ambient + lighting present. + - Diffuse -- the material's direction-agnostic, non-specular reflection, +and additionally, with Blinn/Phong: + - Specular -- a more polished material's direction-sensitive reflection, + - Shininess -- just how polished the material is, + +(All these can be either constants or textures.) + +Increasingly with PBR materials, those properties are just left at sensible zero +or default values in the FBX. But when they're there, and they're how you want +to define your materials, one option is to use the --khr-materials-common +command line switch, which incurs a required dependency on the glTF extension +`KHR_materials_common`. **Note that at the time of writing, this glTF extension +is still undergoing the ratification process, and is furthermore likely to +change names.** + +Given the command line flag --pbr-metallic-roughness, we accept glTF 2.0's PBR +mode, but we do so very partially, filling in a couple of reasonable constants +for metalness and roughness and using the diffuse texture, if it exists, as the +`base colour` texture. + +More work is needed to harness the power of glTF's 2.0's materials. The biggest +issue here is the lack of any obviously emerging standards to complement FBX +itself. It's not clear what format an artist can export their PBR materials on, +and when they can, how to communicate this information well to `FBX2glTF`. + +## Draco Compression +The tool will optionally apply [Draco](https://github.com/google/draco) +compression to the geometric data of each mesh (vertex indices, positions, +normals, per-vertex color, and so on). This can be dramatically effective +in reducing the size of the output file, especially for static models. + +Enabling this feature adds an expressed required dependency in the glTF on the +`KHR_draco_geometry_compression` extension, and can thus only be loaded by a +viewer that is willing and able to decompress the data. + +**Note that at the time of writing, this glTF extension is still undergoing the +ratification process.** + +## Future Improvements +This tool is under continuous development. We do not have a development roadmap +per se, but some aspirations have been noted above. + +## Authors + - Pr Winzell + - J.M.P. van Waveren + - Amanda Watson + +## License +`FBX2glTF` is BSD-licensed. We also provide an additional patent grant. diff --git a/npm/LICENSE b/npm/LICENSE new file mode 100644 index 0000000..832cda8 --- /dev/null +++ b/npm/LICENSE @@ -0,0 +1,44 @@ +BSD License + +For FBX2glTF software + +Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +This software contains Autodesk® FBX® code developed by Autodesk, Inc. Copyright +2017 Autodesk, Inc. All rights, reserved. Such code is provided “as is” and +Autodesk, Inc. disclaims any and all warranties, whether express or implied, +including without limitation the implied warranties of merchantability, fitness +for a particular purpose or non-infringement of third party rights. In no event +shall Autodesk, Inc. be liable for any direct, indirect, incidental, special, +exemplary, or consequential damages (including, but not limited to, procurement +of substitute goods or services; loss of use, data, or profits; or business +interruption) however caused and on any theory of liability, whether in +contract, strict liability, or tort (including negligence or otherwise) arising +in any way out of such code. diff --git a/npm/PATENTS b/npm/PATENTS new file mode 100644 index 0000000..4168ccd --- /dev/null +++ b/npm/PATENTS @@ -0,0 +1,33 @@ +Additional Grant of Patent Rights Version 2 + +"Software" means the FBX2glTF software contributed by Facebook, Inc. + +Facebook, Inc. ("Facebook") hereby grants to each recipient of the Software +("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable +(subject to the termination provision below) license under any Necessary +Claims, to make, have made, use, sell, offer to sell, import, and otherwise +transfer the Software. For avoidance of doubt, no license is granted under +Facebook’s rights in any patent claims that are infringed by (i) modifications +to the Software made by you or any third party or (ii) the Software in +combination with any software or other technology. + +The license granted hereunder will terminate, automatically and without notice, +if you (or any of your subsidiaries, corporate affiliates or agents) initiate +directly or indirectly, or take a direct financial interest in, any Patent +Assertion: (i) against Facebook or any of its subsidiaries or corporate +affiliates, (ii) against any party if such Patent Assertion arises in whole or +in part from any software, technology, product or service of Facebook or any of +its subsidiaries or corporate affiliates, or (iii) against any party relating +to the Software. Notwithstanding the foregoing, if Facebook or any of its +subsidiaries or corporate affiliates files a lawsuit alleging patent +infringement against you in the first instance, and you respond by filing a +patent infringement counterclaim in that lawsuit against that party that is +unrelated to the Software, the license granted hereunder will not terminate +under section (i) of this paragraph due to such counterclaim. + +A "Necessary Claim" is a claim of a patent owned by Facebook that is +necessarily infringed by the Software standing alone. + +A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, +or contributory infringement or inducement to infringe any patent, including a +cross-claim or counterclaim. diff --git a/npm/README.md b/npm/README.md new file mode 100644 index 0000000..e738940 --- /dev/null +++ b/npm/README.md @@ -0,0 +1,38 @@ +# FBX2glTF + +This is a command line tool for converting 3D model assets on the +well-established [FBX](https://www.autodesk.com/products/fbx/overview) format to +[glTF 2.0](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0), +a modern runtime asset delivery format. + +# Platform Binaries + +This package contains three versions of `FBX2glTF`, compiled for three platforms +and located in three eponymous directories: + - bin/Darwin/FBX2glTF + - bin/Linux/FBX2glTF + - bin/Windows/FBX2glTF.exe + +# Further Reading + +The home of this tool is [here](https://github.com/facebookincubator/FBX2glTF). + +# Authors + - Pär Winzell + - J.M.P. van Waveren + - Amanda Watson + +# Legal + +This software contains Autodesk® FBX® code developed by Autodesk, Inc. Copyright +2017 Autodesk, Inc. All rights, reserved. Such code is provided “as is” and +Autodesk, Inc. disclaims any and all warranties, whether express or implied, +including without limitation the implied warranties of merchantability, fitness +for a particular purpose or non-infringement of third party rights. In no event +shall Autodesk, Inc. be liable for any direct, indirect, incidental, special, +exemplary, or consequential damages (including, but not limited to, procurement +of substitute goods or services; loss of use, data, or profits; or business +interruption) however caused and on any theory of liability, whether in +contract, strict liability, or tort (including negligence or otherwise) arising +in any way out of such code. + diff --git a/npm/bin/README b/npm/bin/README new file mode 100644 index 0000000..1e5d5bf --- /dev/null +++ b/npm/bin/README @@ -0,0 +1,6 @@ +This directory must be populated with the following files prior to building the +NPM package: + +Darwin/FBX2glTF +Linux/FBX2glTF +Windows/FBX2glTF.exe diff --git a/npm/bin/fbx2glb.bat b/npm/bin/fbx2glb.bat new file mode 100644 index 0000000..827b6cb --- /dev/null +++ b/npm/bin/fbx2glb.bat @@ -0,0 +1 @@ +%~dp0\Windows\FBX2glTF --binary %1 %2 diff --git a/npm/bin/fbx2glb.sh b/npm/bin/fbx2glb.sh new file mode 100755 index 0000000..9d2bbe4 --- /dev/null +++ b/npm/bin/fbx2glb.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# +# fbx2glb.sh +# +# TODO: Pass command line switches through to binary. + +set -e + +BINDIR=`dirname $0` +BINDIR=`cd ${BINDIR} ; pwd` + +SYSTEM=`uname -s` +FBX2GLTF="${BINDIR}/${SYSTEM}/FBX2glTF" + +if [ ! -f "${FBX2GLTF}" ]; then + echo "Unable to find 'FBX2glTF' binary: ${FBX2GLTF}" + exit 1 +fi + +if [ "$#" != 2 ]; then + echo "Usage: " + exit 1 +fi + +fullpath() { + OLDPWD=$PWD + cd "$(dirname "$1")" + FULLPATH="$PWD/$(basename "$1")" + cd "$OLDPWD" + echo "$FULLPATH" +} + +INFILE=$(fullpath $1) +OUTFILE=$(fullpath $(basename $2 ".glb")) + +# construct a safe work dir +SCRIPT_BASE=`basename $0` +TEMP_DIR=`mktemp -d "/tmp/${SCRIPT_BASE}.XXXX"` +trap "rm -rf ${TEMP_DIR}" EXIT +cd ${TEMP_DIR} + +# some hard-coded defaults for now +"${FBX2GLTF}" --binary --flip-v --input "${INFILE}" --output "${OUTFILE}" diff --git a/npm/index.js b/npm/index.js new file mode 100644 index 0000000..fa8fc73 --- /dev/null +++ b/npm/index.js @@ -0,0 +1,43 @@ +const childProcess = require('child_process'); +const os = require('os'); +const path = require('path'); + +const binaries = { + 'darwin': `bin/darwin/Fbx2Gtlf`, + 'linux': `bin/linux/Fbx2Gtlf`, + 'win32': `bin\windows\Fbx2Gtlf.exe`, +}; + +function fbx2glb(srcFile, destFile, cwd) { + return new Promise((resolve, reject) => { + let script = os.type() === 'Windows_NT' ? 'fbx2glb.bat' : 'fbx2glb.sh'; + let child; + try { + let opts = {}; + cwd && (opts.cwd = cwd); + child = childProcess.spawn( + path.join(__dirname, 'bin', script), + [ srcFile, destFile ], + opts + ); + } catch (error) { + reject(error); + return; + } + let output = ''; + child.stdout.on('data', (data) => output += data); + child.stderr.on('data', (data) => output += data); + child.on('error', reject); + child.on('close', code => { + // non-zero exit code is failure + if (code != 0) { + reject(new Error(`Script ${script} output:\n` + + (output.length ? output : ""))); + } else { + resolve(destFile); + } + }); + }); +} + +module.exports = fbx2glb; diff --git a/npm/package.json b/npm/package.json new file mode 100644 index 0000000..4166690 --- /dev/null +++ b/npm/package.json @@ -0,0 +1,27 @@ +{ + "name": "fbx2gltf", + "version": "0.9.0", + "description": "Node wrapper around FBX2glTF tools.", + "main": "index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/facebookincubator/FBX2glTF.git" + }, + "contributors": [ + "Pär Winzell ", + "J.M.P. van Waveren", + "Amanda Watson" + ], + "license": "BSD-3-Clause", + "bugs": { + "url": "https://github.com/facebookincubator/FBX2glTF/issues" + }, + "homepage": "https://github.com/facebookincubator/FBX2glTF" + "files": [ + "LICENSE", + "PATENTS", + "README.md", + "bin", + "index.js" + ] +} diff --git a/src/FBX2glTF.h b/src/FBX2glTF.h new file mode 100644 index 0000000..27278eb --- /dev/null +++ b/src/FBX2glTF.h @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef __FBX2GLTF_H__ +#define __FBX2GLTF_H__ + +#if defined ( _WIN32 ) +// This can be a macro under Windows, confusing FMT +#undef isnan +// Tell Windows not to define min() and max() macros +#define NOMINMAX +#include +#endif + +#include +#include + +#include "mathfu.h" + +#endif // !__FBX2GLTF_H__ diff --git a/src/Fbx2Raw.cpp b/src/Fbx2Raw.cpp new file mode 100644 index 0000000..9f9a2a5 --- /dev/null +++ b/src/Fbx2Raw.cpp @@ -0,0 +1,1033 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "FBX2glTF.h" +#include "utils/File_Utils.h" +#include "utils/String_Utils.h" +#include "RawModel.h" +#include "Fbx2Raw.h" + +extern bool verboseOutput; + +template +class FbxLayerElementAccess +{ +public: + + FbxLayerElementAccess(const FbxLayerElementTemplate<_type_> *layer, int count) : + mappingMode(FbxGeometryElement::eNone), + elements(nullptr), + indices(nullptr) + { + if (count <= 0 || layer == nullptr) { + return; + } + const FbxGeometryElement::EMappingMode newMappingMode = layer->GetMappingMode(); + if (newMappingMode == FbxGeometryElement::eByControlPoint || + newMappingMode == FbxGeometryElement::eByPolygonVertex || + newMappingMode == FbxGeometryElement::eByPolygon) { + mappingMode = newMappingMode; + elements = &layer->GetDirectArray(); + indices = ( + layer->GetReferenceMode() == FbxGeometryElement::eIndexToDirect || + layer->GetReferenceMode() == FbxGeometryElement::eIndex) ? &layer->GetIndexArray() : nullptr; + } + } + + bool LayerPresent() const + { + return (mappingMode != FbxGeometryElement::eNone); + } + + _type_ GetElement(const int polygonIndex, const int polygonVertexIndex, const int controlPointIndex, const _type_ defaultValue) const + { + if (mappingMode != FbxGeometryElement::eNone) { + int index = (mappingMode == FbxGeometryElement::eByControlPoint) ? controlPointIndex : + ((mappingMode == FbxGeometryElement::eByPolygonVertex) ? polygonVertexIndex : polygonIndex); + index = (indices != nullptr) ? (*indices)[index] : index; + _type_ element = elements->GetAt(index); + return element; + } + return defaultValue; + } + + _type_ GetElement( + const int polygonIndex, const int polygonVertexIndex, const int controlPointIndex, const _type_ defaultValue, + const FbxMatrix &transform, const bool normalize) const + { + if (mappingMode != FbxGeometryElement::eNone) { + _type_ element = transform.MultNormalize(GetElement(polygonIndex, polygonVertexIndex, controlPointIndex, defaultValue)); + if (normalize) { + element.Normalize(); + } + return element; + } + return defaultValue; + } + +private: + FbxGeometryElement::EMappingMode mappingMode; + const FbxLayerElementArrayTemplate<_type_> *elements; + const FbxLayerElementArrayTemplate *indices; +}; + +class FbxMaterialAccess +{ +public: + + FbxMaterialAccess(const FbxMesh *pMesh) : + mappingMode(FbxGeometryElement::eNone), + mesh(nullptr), + indices(nullptr) + { + if (pMesh->GetElementMaterialCount() <= 0) { + return; + } + + const FbxGeometryElement::EMappingMode materialMappingMode = pMesh->GetElementMaterial()->GetMappingMode(); + if (materialMappingMode != FbxGeometryElement::eByPolygon && materialMappingMode != FbxGeometryElement::eAllSame) { + return; + } + + const FbxGeometryElement::EReferenceMode materialReferenceMode = pMesh->GetElementMaterial()->GetReferenceMode(); + if (materialReferenceMode != FbxGeometryElement::eIndexToDirect) { + return; + } + + mappingMode = materialMappingMode; + mesh = pMesh; + indices = &pMesh->GetElementMaterial()->GetIndexArray(); + } + + const FbxSurfaceMaterial *GetMaterial(const int polygonIndex) const + { + if (mappingMode != FbxGeometryElement::eNone) { + const int materialIndex = (mappingMode == FbxGeometryElement::eByPolygon) ? polygonIndex : 0; + const FbxSurfaceMaterial *pMaterial = mesh->GetNode()->GetSrcObject(indices->GetAt(materialIndex)); + return pMaterial; + } + return nullptr; + } + +private: + FbxGeometryElement::EMappingMode mappingMode; + const FbxMesh *mesh; + const FbxLayerElementArrayTemplate *indices; +}; + +class FbxSkinningAccess +{ +public: + + static const int MAX_WEIGHTS = 4; + + FbxSkinningAccess(const FbxMesh *pMesh, FbxScene *pScene, FbxNode *pNode) + : rootIndex(-1) + { + for (int deformerIndex = 0; deformerIndex < pMesh->GetDeformerCount(); deformerIndex++) { + FbxSkin *skin = reinterpret_cast< FbxSkin * >( pMesh->GetDeformer(deformerIndex, FbxDeformer::eSkin)); + if (skin != nullptr) { + int controlPointCount = pMesh->GetControlPointsCount(); + + vertexJointIndices.resize(controlPointCount, Vec4i(0, 0, 0, 0)); + vertexJointWeights.resize(controlPointCount, Vec4f(0.0f, 0.0f, 0.0f, 0.0f)); + + const int clusterCount = skin->GetClusterCount(); + for (int clusterIndex = 0; clusterIndex < clusterCount; clusterIndex++) { + FbxCluster *cluster = skin->GetCluster(clusterIndex); + const int indexCount = cluster->GetControlPointIndicesCount(); + const int *clusterIndices = cluster->GetControlPointIndices(); + const double *clusterWeights = cluster->GetControlPointWeights(); + + assert(cluster->GetLinkMode() == FbxCluster::eNormalize); + + // Transform link matrix. + FbxAMatrix transformLinkMatrix; + cluster->GetTransformLinkMatrix(transformLinkMatrix); + + // The transformation of the mesh at binding time + FbxAMatrix transformMatrix; + cluster->GetTransformMatrix(transformMatrix); + + // Inverse bind matrix. + FbxAMatrix globalBindposeInverseMatrix = transformLinkMatrix.Inverse() * transformMatrix; + inverseBindMatrices.emplace_back(globalBindposeInverseMatrix); + + jointNodes.push_back(cluster->GetLink()); + jointNames.push_back(*cluster->GetLink()->GetName() != '\0' ? cluster->GetLink()->GetName() : cluster->GetName()); + + const FbxAMatrix globalNodeTransform = cluster->GetLink()->EvaluateGlobalTransform(); + jointSkinningTransforms.push_back(FbxMatrix(globalNodeTransform * globalBindposeInverseMatrix)); + jointInverseGlobalTransforms.push_back(FbxMatrix(globalNodeTransform.Inverse())); + + for (int i = 0; i < indexCount; i++) { + if (clusterIndices[i] < 0 || clusterIndices[i] >= controlPointCount) { + continue; + } + if (clusterWeights[i] <= vertexJointWeights[clusterIndices[i]][MAX_WEIGHTS - 1]) { + continue; + } + vertexJointIndices[clusterIndices[i]][MAX_WEIGHTS - 1] = clusterIndex; + vertexJointWeights[clusterIndices[i]][MAX_WEIGHTS - 1] = (float) clusterWeights[i]; + for (int j = MAX_WEIGHTS - 1; j > 0; j--) { + if (vertexJointWeights[clusterIndices[i]][j - 1] >= vertexJointWeights[clusterIndices[i]][j]) { + break; + } + std::swap(vertexJointIndices[clusterIndices[i]][j - 1], vertexJointIndices[clusterIndices[i]][j]); + std::swap(vertexJointWeights[clusterIndices[i]][j - 1], vertexJointWeights[clusterIndices[i]][j]); + } + } + + } + for (int i = 0; i < controlPointCount; i++) { + vertexJointWeights[i] = vertexJointWeights[i].Normalized(); + } + } + } + + rootIndex = -1; + for (size_t i = 0; i < jointNodes.size() && rootIndex == -1; i++) { + rootIndex = (int) i; + FbxNode *parent = jointNodes[i]->GetParent(); + if (parent == nullptr) { + break; + } + for (size_t j = 0; j < jointNodes.size(); j++) { + if (jointNodes[j] == parent) { + rootIndex = -1; + break; + } + } + } + } + + bool IsSkinned() const + { + return (vertexJointWeights.size() > 0); + } + + int GetNodeCount() const + { + return (int) jointNodes.size(); + } + + FbxNode *GetJointNode(const int jointIndex) const + { + return jointNodes[jointIndex]; + } + + const char *GetJointName(const int jointIndex) const + { + return jointNames[jointIndex].c_str(); + } + + const FbxMatrix &GetJointSkinningTransform(const int jointIndex) const + { + return jointSkinningTransforms[jointIndex]; + } + + const FbxMatrix &GetJointInverseGlobalTransforms(const int jointIndex) const + { + return jointInverseGlobalTransforms[jointIndex]; + } + + const char *GetRootNode() const + { + assert(rootIndex != -1); + return jointNames[rootIndex].c_str(); + } + + const FbxAMatrix &GetInverseBindMatrix(const int jointIndex) const + { + return inverseBindMatrices[jointIndex]; + } + + const Vec4i GetVertexIndices(const int controlPointIndex) const + { + return (!vertexJointIndices.empty()) ? + vertexJointIndices[controlPointIndex] : Vec4i(0, 0, 0, 0); + } + + const Vec4f GetVertexWeights(const int controlPointIndex) const + { + return (!vertexJointWeights.empty()) ? + vertexJointWeights[controlPointIndex] : Vec4f(0, 0, 0, 0); + } + +private: + int rootIndex; + std::vector jointNames; + std::vector jointNodes; + std::vector jointSkinningTransforms; + std::vector jointInverseGlobalTransforms; + std::vector inverseBindMatrices; + std::vector vertexJointIndices; + std::vector vertexJointWeights; +}; + +static bool TriangleTexturePolarity(const Vec2f &uv0, const Vec2f &uv1, const Vec2f &uv2) +{ + const Vec2f d0 = uv1 - uv0; + const Vec2f d1 = uv2 - uv0; + + return (d0[0] * d1[1] - d0[1] * d1[0] < 0.0f); +} + +static RawMaterialType +GetMaterialType(const FbxSurfaceMaterial *pMaterial, const RawModel &raw, const int textures[RAW_TEXTURE_USAGE_MAX], const bool skinned) +{ + if ((raw.GetVertexAttributes() & RAW_VERTEX_ATTRIBUTE_COLOR) != 0) { + return skinned ? RAW_MATERIAL_TYPE_SKINNED_VERTEX_COLORED : RAW_MATERIAL_TYPE_VERTEX_COLORED; + } + + // Determine material type based on texture occlusion. + if (pMaterial != nullptr && textures[RAW_TEXTURE_USAGE_DIFFUSE] >= 0) { + switch (raw.GetTexture(textures[RAW_TEXTURE_USAGE_DIFFUSE]).occlusion) { + case RAW_TEXTURE_OCCLUSION_OPAQUE: + return skinned ? RAW_MATERIAL_TYPE_SKINNED_OPAQUE : RAW_MATERIAL_TYPE_OPAQUE; + case RAW_TEXTURE_OCCLUSION_TRANSPARENT: + return skinned ? RAW_MATERIAL_TYPE_SKINNED_TRANSPARENT : RAW_MATERIAL_TYPE_TRANSPARENT; + } + } + + // Default to simply opaque. + return skinned ? RAW_MATERIAL_TYPE_SKINNED_OPAQUE : RAW_MATERIAL_TYPE_OPAQUE; +} + +static void ReadMesh(RawModel &raw, FbxScene *pScene, FbxNode *pNode, const std::map &textureNames) +{ + FbxGeometryConverter meshConverter(pScene->GetFbxManager()); + meshConverter.Triangulate(pNode->GetNodeAttribute(), true); + const FbxMesh *pMesh = pNode->GetMesh(); + + const char *meshName = (pNode->GetName()[0] != '\0') ? pNode->GetName() : pMesh->GetName(); + const int rawSurfaceIndex = raw.AddSurface(meshName, pNode->GetName()); + + const FbxVector4 *controlPoints = pMesh->GetControlPoints(); + const FbxLayerElementAccess normalLayer(pMesh->GetElementNormal(), pMesh->GetElementNormalCount()); + const FbxLayerElementAccess binormalLayer(pMesh->GetElementBinormal(), pMesh->GetElementBinormalCount()); + const FbxLayerElementAccess tangentLayer(pMesh->GetElementTangent(), pMesh->GetElementTangentCount()); + const FbxLayerElementAccess colorLayer(pMesh->GetElementVertexColor(), pMesh->GetElementVertexColorCount()); + const FbxLayerElementAccess uvLayer0(pMesh->GetElementUV(0), pMesh->GetElementUVCount()); + const FbxLayerElementAccess uvLayer1(pMesh->GetElementUV(1), pMesh->GetElementUVCount()); + const FbxSkinningAccess skinning(pMesh, pScene, pNode); + const FbxMaterialAccess materials(pMesh); + + if (verboseOutput) { + fmt::printf( + "mesh %d: %s (skinned: %s)\n", rawSurfaceIndex, meshName, + skinning.IsSkinned() ? skinning.GetRootNode() : "NO"); + } + + // The FbxNode geometric transformation describes how a FbxNodeAttribute is offset from + // the FbxNode's local frame of reference. These geometric transforms are applied to the + // FbxNodeAttribute after the FbxNode's local transforms are computed, and are not + // inherited across the node hierarchy. + // Apply the geometric transform to the mesh geometry (vertices, normal etc.) because + // glTF does not have an equivalent to the geometric transform. + const FbxVector4 meshTranslation = pNode->GetGeometricTranslation(FbxNode::eSourcePivot); + const FbxVector4 meshRotation = pNode->GetGeometricRotation(FbxNode::eSourcePivot); + const FbxVector4 meshScaling = pNode->GetGeometricScaling(FbxNode::eSourcePivot); + const FbxAMatrix meshTransform(meshTranslation, meshRotation, meshScaling); + const FbxMatrix transform = meshTransform; + const FbxMatrix inverseTransposeTransform = transform.Inverse().Transpose(); + + raw.AddVertexAttribute(RAW_VERTEX_ATTRIBUTE_POSITION); + if (normalLayer.LayerPresent()) { raw.AddVertexAttribute(RAW_VERTEX_ATTRIBUTE_NORMAL); } + if (tangentLayer.LayerPresent()) { raw.AddVertexAttribute(RAW_VERTEX_ATTRIBUTE_TANGENT); } + if (binormalLayer.LayerPresent()) { raw.AddVertexAttribute(RAW_VERTEX_ATTRIBUTE_BINORMAL); } + if (colorLayer.LayerPresent()) { raw.AddVertexAttribute(RAW_VERTEX_ATTRIBUTE_COLOR); } + if (uvLayer0.LayerPresent()) { raw.AddVertexAttribute(RAW_VERTEX_ATTRIBUTE_UV0); } + if (uvLayer1.LayerPresent()) { raw.AddVertexAttribute(RAW_VERTEX_ATTRIBUTE_UV1); } + if (skinning.IsSkinned()) { + raw.AddVertexAttribute(RAW_VERTEX_ATTRIBUTE_JOINT_WEIGHTS); + raw.AddVertexAttribute(RAW_VERTEX_ATTRIBUTE_JOINT_INDICES); + } + + RawSurface &rawSurface = raw.GetSurface(rawSurfaceIndex); + + rawSurface.skeletonRootName = (skinning.IsSkinned()) ? skinning.GetRootNode() : pNode->GetName(); + for (int jointIndex = 0; jointIndex < skinning.GetNodeCount(); jointIndex++) { + const char *jointName = skinning.GetJointName(jointIndex); + raw.GetNode(raw.GetNodeByName(jointName)).isJoint = true; + + rawSurface.jointNames.emplace_back(jointName); + rawSurface.inverseBindMatrices.push_back(toMat4f(skinning.GetInverseBindMatrix(jointIndex))); + rawSurface.jointGeometryMins.emplace_back(FLT_MAX, FLT_MAX, FLT_MAX); + rawSurface.jointGeometryMaxs.emplace_back(-FLT_MAX, -FLT_MAX, -FLT_MAX); + } + + std::set collectedWarnings; + int polygonVertexIndex = 0; + + for (int polygonIndex = 0; polygonIndex < pMesh->GetPolygonCount(); polygonIndex++) { + FBX_ASSERT(pMesh->GetPolygonSize(polygonIndex) == 3); + + const FbxSurfaceMaterial *pMaterial = materials.GetMaterial(polygonIndex); + + // TODO: all this should happen once per material, not once per polygon! + int textures[RAW_TEXTURE_USAGE_MAX]; + std::fill_n(textures, RAW_TEXTURE_USAGE_MAX, -1); + + FbxString shadingModel, materialName; + Vec4f ambient, diffuse, specular, emissive; + float shininess; + + if (pMaterial == nullptr) { + materialName = "DefaultMaterial"; + shadingModel = "Lambert"; + + } else { + materialName = pMaterial->GetName(); + shadingModel = pMaterial->ShadingModel.Get(); + + auto getSurfaceScalar = [&](const char *propName, FbxFileTexture *&tex) + { + const FbxProperty prop = pMaterial->FindProperty(propName); + + FbxDouble val(0); + tex = prop.GetSrcObject(); + if (tex != nullptr && textureNames.find(tex) == textureNames.end()) { + tex = nullptr; + } + if (tex == nullptr && prop.IsValid()) { + val = prop.Get(); + } + return static_cast(val); + }; + + auto getSurfaceVector = [&](const char *propName, FbxFileTexture *&tex) + { + const FbxProperty prop = pMaterial->FindProperty(propName); + + FbxDouble3 val(1, 1, 1); + tex = prop.GetSrcObject(); + if (tex != nullptr && textureNames.find(tex) == textureNames.end()) { + tex = nullptr; + } + if (tex == nullptr && prop.IsValid()) { + val = prop.Get(); + } + + return Vec4f( + static_cast(val[0]), + static_cast(val[1]), + static_cast(val[2]), + 1.0); + }; + + auto getSurfaceValues = [&](const char *colName, const char *facName, FbxFileTexture *&colTex, FbxFileTexture *&facTex) + { + const FbxProperty colProp = pMaterial->FindProperty(colName); + const FbxProperty facProp = pMaterial->FindProperty(facName); + + FbxDouble3 colorVal(1, 1, 1); + FbxDouble factorVal(1); + + colTex = colProp.GetSrcObject(); + if (colTex != nullptr && textureNames.find(colTex) == textureNames.end()) { + colTex = nullptr; + } + if (colTex == nullptr && colProp.IsValid()) { + colorVal = colProp.Get(); + } + facTex = facProp.GetSrcObject(); + if (facTex != nullptr && textureNames.find(facTex) == textureNames.end()) { + facTex = nullptr; + } + if (facTex == nullptr && facProp.IsValid()) { + factorVal = facProp.Get(); + } + + return Vec4f( + static_cast(colorVal[0] * factorVal), + static_cast(colorVal[1] * factorVal), + static_cast(colorVal[2] * factorVal), + static_cast(factorVal)); + }; + + auto addTexture = [&](RawTextureUsage usage, FbxFileTexture *texture) + { + auto name = textureNames.find(texture); + assert(name != textureNames.end()); + + const char *filename = texture->GetFileName(); + const char *textureName = name->second.Buffer(); + int texIx = raw.AddTexture(textureName, filename, usage); + textures[usage] = texIx; + return texIx; + }; + + auto handleBasicProperty = [&](const char *colName, const char *facName, Vec4f &vec, RawTextureUsage usage) + { + FbxFileTexture *colTex, *facTex; + vec = getSurfaceValues(colName, facName, colTex, facTex); + if (colTex) { + addTexture(usage, colTex); + if (facTex) { + collectedWarnings.insert( + fmt::sprintf( + "Mat [%s]: Can't handle both %s and %s textures; discarding %s.", + materialName, colName, facName, facName)); + + } + } else if (facTex) { + addTexture(usage, facTex); + } + }; + + // four properties are on the same structure and follow the same rules + handleBasicProperty(FbxSurfaceMaterial::sAmbient, FbxSurfaceMaterial::sAmbientFactor, ambient, RAW_TEXTURE_USAGE_AMBIENT); + handleBasicProperty(FbxSurfaceMaterial::sSpecular, FbxSurfaceMaterial::sSpecularFactor, specular, RAW_TEXTURE_USAGE_SPECULAR); + handleBasicProperty(FbxSurfaceMaterial::sDiffuse, FbxSurfaceMaterial::sDiffuseFactor, diffuse, RAW_TEXTURE_USAGE_DIFFUSE); + handleBasicProperty(FbxSurfaceMaterial::sEmissive, FbxSurfaceMaterial::sEmissiveFactor, emissive, RAW_TEXTURE_USAGE_EMISSIVE); + + // the normal map can only ever be a map, ignore everything else + { + FbxFileTexture *tex; + getSurfaceVector(FbxSurfaceMaterial::sNormalMap, tex); + if (tex) { + addTexture(RAW_TEXTURE_USAGE_NORMAL, tex); + } + } + + // shininess can be a map or a factor + { + FbxFileTexture *tex; + shininess = getSurfaceScalar(FbxSurfaceMaterial::sShininess, tex); + if (tex) { + addTexture(RAW_TEXTURE_USAGE_SHININESS, tex); + } + } + + { + FbxFileTexture *colTex, *facTex; + Vec4f transparency = getSurfaceValues( + FbxSurfaceMaterial::sTransparentColor, FbxSurfaceMaterial::sTransparencyFactor, colTex, facTex); + if (colTex) { + collectedWarnings.insert( + fmt::sprintf( + "Mat [%s]: Can't handle texture for %s; discarding.", + materialName, FbxSurfaceMaterial::sTransparentColor)); + } + if (facTex) { + collectedWarnings.insert( + fmt::sprintf( + "Mat [%s]: Can't handle texture for %s; discarding.", + materialName, FbxSurfaceMaterial::sTransparentColor)); + } + // FBX color is RGB, so we supply the A channel from TransparenyFactor + diffuse[3] = transparency[3]; + } + } + + auto toVec3 = [](Vec4f vec4) { return Vec3f(vec4[0], vec4[1], vec4[2]); }; + + const RawMaterialType materialType = GetMaterialType(pMaterial, raw, textures, skinning.IsSkinned()); + const int rawMaterialIndex = raw.AddMaterial( + materialName, shadingModel, materialType, textures, + toVec3(ambient), diffuse, toVec3(specular), toVec3(emissive), shininess); + + RawVertex rawVertices[3]; + for (int vertexIndex = 0; vertexIndex < 3; vertexIndex++, polygonVertexIndex++) { + const int controlPointIndex = pMesh->GetPolygonVertex(polygonIndex, vertexIndex); + + // Note that the default values here must be the same as the RawVertex default values! + const FbxVector4 fbxPosition = transform.MultNormalize(controlPoints[controlPointIndex]); + const FbxVector4 fbxNormal = normalLayer.GetElement( + polygonIndex, polygonVertexIndex, controlPointIndex, FbxVector4(0.0f, 0.0f, 0.0f, 0.0f), inverseTransposeTransform, true); + const FbxVector4 fbxTangent = tangentLayer.GetElement( + polygonIndex, polygonVertexIndex, controlPointIndex, FbxVector4(0.0f, 0.0f, 0.0f, 0.0f), inverseTransposeTransform, true); + const FbxVector4 fbxBinormal = binormalLayer.GetElement( + polygonIndex, polygonVertexIndex, controlPointIndex, FbxVector4(0.0f, 0.0f, 0.0f, 0.0f), inverseTransposeTransform, true); + const FbxColor fbxColor = colorLayer + .GetElement(polygonIndex, polygonVertexIndex, controlPointIndex, FbxColor(0.0f, 0.0f, 0.0f, 0.0f)); + const FbxVector2 fbxUV0 = uvLayer0.GetElement(polygonIndex, polygonVertexIndex, controlPointIndex, FbxVector2(0.0f, 0.0f)); + const FbxVector2 fbxUV1 = uvLayer1.GetElement(polygonIndex, polygonVertexIndex, controlPointIndex, FbxVector2(0.0f, 0.0f)); + + RawVertex &vertex = rawVertices[vertexIndex]; + vertex.position[0] = (float) fbxPosition[0]; + vertex.position[1] = (float) fbxPosition[1]; + vertex.position[2] = (float) fbxPosition[2]; + vertex.normal[0] = (float) fbxNormal[0]; + vertex.normal[1] = (float) fbxNormal[1]; + vertex.normal[2] = (float) fbxNormal[2]; + vertex.tangent[0] = (float) fbxTangent[0]; + vertex.tangent[1] = (float) fbxTangent[1]; + vertex.tangent[2] = (float) fbxTangent[2]; + vertex.tangent[3] = (float) fbxTangent[3]; + vertex.binormal[0] = (float) fbxBinormal[0]; + vertex.binormal[1] = (float) fbxBinormal[1]; + vertex.binormal[2] = (float) fbxBinormal[2]; + vertex.color[0] = (float) fbxColor.mRed; + vertex.color[1] = (float) fbxColor.mGreen; + vertex.color[2] = (float) fbxColor.mBlue; + vertex.color[3] = (float) fbxColor.mAlpha; + vertex.uv0[0] = (float) fbxUV0[0]; + vertex.uv0[1] = (float) fbxUV0[1]; + vertex.uv1[0] = (float) fbxUV1[0]; + vertex.uv1[1] = (float) fbxUV1[1]; + vertex.jointIndices = skinning.GetVertexIndices(controlPointIndex); + vertex.jointWeights = skinning.GetVertexWeights(controlPointIndex); + vertex.polarityUv0 = false; + + rawSurface.bounds.AddPoint(vertex.position); + + if (skinning.IsSkinned()) { + const int jointIndices[FbxSkinningAccess::MAX_WEIGHTS] = { + vertex.jointIndices[0], + vertex.jointIndices[1], + vertex.jointIndices[2], + vertex.jointIndices[3] + }; + const float jointWeights[FbxSkinningAccess::MAX_WEIGHTS] = { + vertex.jointWeights[0], + vertex.jointWeights[1], + vertex.jointWeights[2], + vertex.jointWeights[3] + }; + const FbxMatrix skinningMatrix = + skinning.GetJointSkinningTransform(jointIndices[0]) * jointWeights[0] + + skinning.GetJointSkinningTransform(jointIndices[1]) * jointWeights[1] + + skinning.GetJointSkinningTransform(jointIndices[2]) * jointWeights[2] + + skinning.GetJointSkinningTransform(jointIndices[3]) * jointWeights[3]; + + const FbxVector4 globalPosition = skinningMatrix.MultNormalize(fbxPosition); + for (int i = 0; i < FbxSkinningAccess::MAX_WEIGHTS; i++) { + if (jointWeights[i] > 0.0f) { + const FbxVector4 localPosition = + skinning.GetJointInverseGlobalTransforms(jointIndices[i]).MultNormalize(globalPosition); + + Vec3f &mins = rawSurface.jointGeometryMins[jointIndices[i]]; + mins[0] = std::min(mins[0], (float) localPosition[0]); + mins[1] = std::min(mins[1], (float) localPosition[1]); + mins[2] = std::min(mins[2], (float) localPosition[2]); + + Vec3f &maxs = rawSurface.jointGeometryMaxs[jointIndices[i]]; + maxs[0] = std::max(maxs[0], (float) localPosition[0]); + maxs[1] = std::max(maxs[1], (float) localPosition[1]); + maxs[2] = std::max(maxs[2], (float) localPosition[2]); + } + } + } + } + + if (textures[RAW_TEXTURE_USAGE_NORMAL] != -1) { + // Distinguish vertices that are used by triangles with a different texture polarity to avoid degenerate tangent space smoothing. + const bool polarity = TriangleTexturePolarity(rawVertices[0].uv0, rawVertices[1].uv0, rawVertices[2].uv0); + rawVertices[0].polarityUv0 = polarity; + rawVertices[1].polarityUv0 = polarity; + rawVertices[2].polarityUv0 = polarity; + } + + int rawVertexIndices[3]; + for (int vertexIndex = 0; vertexIndex < 3; vertexIndex++) { + rawVertexIndices[vertexIndex] = raw.AddVertex(rawVertices[vertexIndex]); + } + + raw.AddTriangle(rawVertexIndices[0], rawVertexIndices[1], rawVertexIndices[2], rawMaterialIndex, rawSurfaceIndex); + } + for (const auto &warning : collectedWarnings) { + fmt::fprintf(stderr, "%s\n", warning); + } +} + +static void ReadCamera(RawModel &raw, FbxScene *pScene, FbxNode *pNode) +{ + const FbxCamera *pCamera = pNode->GetCamera(); + if (pCamera->ProjectionType.Get() == FbxCamera::EProjectionType::ePerspective) { + raw.AddCameraPerspective( + "", pNode->GetName(), (float) pCamera->FilmAspectRatio, + (float) pCamera->FieldOfViewX, (float) pCamera->FieldOfViewX, + (float) pCamera->NearPlane, (float) pCamera->FarPlane); + } else { + raw.AddCameraOrthographic( + "", pNode->GetName(), + (float) pCamera->OrthoZoom, (float) pCamera->OrthoZoom, + (float) pCamera->FarPlane, (float) pCamera->NearPlane); + } +} + +static void ReadNodeAttributes(RawModel &raw, FbxScene *pScene, FbxNode *pNode, const std::map &textureNames) +{ + if (!pNode->GetVisibility()) { + return; + } + + FbxNodeAttribute *pNodeAttribute = pNode->GetNodeAttribute(); + if (pNodeAttribute != nullptr) { + const FbxNodeAttribute::EType attributeType = pNodeAttribute->GetAttributeType(); + switch (attributeType) { + case FbxNodeAttribute::eMesh: + case FbxNodeAttribute::eNurbs: + case FbxNodeAttribute::eNurbsSurface: + case FbxNodeAttribute::eTrimNurbsSurface: + case FbxNodeAttribute::ePatch: { + ReadMesh(raw, pScene, pNode, textureNames); + break; + } + case FbxNodeAttribute::eCamera: { + ReadCamera(raw, pScene, pNode); + break; + } + case FbxNodeAttribute::eUnknown: + case FbxNodeAttribute::eNull: + case FbxNodeAttribute::eMarker: + case FbxNodeAttribute::eSkeleton: + case FbxNodeAttribute::eCameraStereo: + case FbxNodeAttribute::eCameraSwitcher: + case FbxNodeAttribute::eLight: + case FbxNodeAttribute::eOpticalReference: + case FbxNodeAttribute::eOpticalMarker: + case FbxNodeAttribute::eNurbsCurve: + case FbxNodeAttribute::eBoundary: + case FbxNodeAttribute::eShape: + case FbxNodeAttribute::eLODGroup: + case FbxNodeAttribute::eSubDiv: + case FbxNodeAttribute::eCachedEffect: + case FbxNodeAttribute::eLine: { + break; + } + } + } + + for (int child = 0; child < pNode->GetChildCount(); child++) { + ReadNodeAttributes(raw, pScene, pNode->GetChild(child), textureNames); + } +} + +/** + * Compute the local scale vector to use for a given node. This is an imperfect hack to cope with + * the FBX node transform's eInheritRrs inheritance type, in which ancestral scale is ignored + */ +static FbxVector4 computeLocalScale(FbxNode *pNode, FbxTime pTime = FBXSDK_TIME_INFINITE) +{ + const FbxVector4 lScale = pNode->EvaluateLocalTransform(pTime).GetS(); + + if (pNode->GetParent() == nullptr || + pNode->GetTransform().GetInheritType() != FbxTransform::eInheritRrs) { + return lScale; + } + // This is a very partial fix that is only correct for models that use identity scale in their rig's joints. + // We could write better support that compares local scale to parent's global scale and apply the ratio to + // our local translation. We'll always want to return scale 1, though -- that's the only way to encode the + // missing 'S' (parent scale) in the transform chain. + return FbxVector4(1, 1, 1, 1); +} + +static void ReadNodeHierarchy( + RawModel &raw, FbxScene *pScene, FbxNode *pNode, + const std::string &parentName, const std::string &path) +{ + const char *nodeName = pNode->GetName(); + const int nodeIndex = raw.AddNode(nodeName, parentName.c_str()); + RawNode &node = raw.GetNode(nodeIndex); + + FbxTransform::EInheritType lInheritType; + pNode->GetTransformationInheritType(lInheritType); + + std::string newPath = path + "/" + nodeName; + if (verboseOutput) { + fmt::printf("node %d: %s\n", nodeIndex, newPath.c_str()); + } + + static int warnRSrsCount = 0; + static int warnRrsCount = 0; + if (lInheritType == FbxTransform::eInheritRSrs) { + if (++warnRSrsCount == 1) { + fmt::printf("Warning: node %s uses unsupported transform inheritance type 'eInheritRSrs'.\n", newPath); + fmt::printf("Further warnings of this type squelched.\n"); + } + + } else if (lInheritType == FbxTransform::eInheritRrs) { + if (++warnRrsCount == 1) { + fmt::printf( + "Warning: node %s uses unsupported transform inheritance type 'eInheritRrs'\n" + "This tool will attempt to partially compensate, but glTF cannot truly express this mode.\n" + "If this was a Maya export, consider turning off 'Segment Scale Compensate' on all joints.\n" + "Further warnings of this type squelched.\n", + newPath); + } + } + + // Set the initial node transform. + const FbxAMatrix localTransform = pNode->EvaluateLocalTransform(); + const FbxVector4 localTranslation = localTransform.GetT(); + const FbxQuaternion localRotation = localTransform.GetQ(); + const FbxVector4 localScaling = computeLocalScale(pNode); + + node.translation = toVec3f(localTranslation); + node.rotation = toQuatf(localRotation); + node.scale = toVec3f(localScaling); + + if (parentName.size() > 0) { + RawNode &parentNode = raw.GetNode(raw.GetNodeByName(parentName.c_str())); + // Add unique child name to the parent node. + if (std::find(parentNode.childNames.begin(), parentNode.childNames.end(), nodeName) == parentNode.childNames.end()) { + parentNode.childNames.push_back(nodeName); + } + } else { + // If there is no parent then this is the root node. + raw.SetRootNode(nodeName); + } + + for (int child = 0; child < pNode->GetChildCount(); child++) { + ReadNodeHierarchy(raw, pScene, pNode->GetChild(child), nodeName, newPath); + } +} + +static void ReadAnimations(RawModel &raw, FbxScene *pScene) +{ + FbxTime::EMode eMode = FbxTime::eFrames24; + const int animationCount = pScene->GetSrcObjectCount(); + for (int i = 0; i < animationCount; i++) { + FbxAnimStack *pAnimStack = pScene->GetSrcObject(i); + FbxString animStackName = pAnimStack->GetName(); + + if (verboseOutput) { + fmt::printf("animation %d: %s (%d%%)", i, (const char *) animStackName, 0); + } + + pScene->SetCurrentAnimationStack(pAnimStack); + + FbxTakeInfo *takeInfo = pScene->GetTakeInfo(animStackName); + FbxTime start = takeInfo->mLocalTimeSpan.GetStart(); + FbxTime end = takeInfo->mLocalTimeSpan.GetStop(); + + RawAnimation animation; + animation.name = animStackName; + + FbxLongLong firstFrameIndex = start.GetFrameCount(eMode); + FbxLongLong lastFrameIndex = end.GetFrameCount(eMode); + for (FbxLongLong frameIndex = firstFrameIndex; frameIndex <= lastFrameIndex; frameIndex++) { + FbxTime pTime; + // first frame is always at t = 0.0 + pTime.SetFrame(frameIndex - firstFrameIndex, eMode); + animation.times.emplace_back((float) pTime.GetSecondDouble()); + } + + size_t totalSizeInBytes = 0; + + const int nodeCount = pScene->GetNodeCount(); + for (int nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) { + FbxNode *pNode = pScene->GetNode(nodeIndex); + const FbxAMatrix baseTransform = pNode->EvaluateLocalTransform(); + const FbxVector4 baseTranslation = baseTransform.GetT(); + const FbxQuaternion baseRotation = baseTransform.GetQ(); + const FbxVector4 baseScaling = computeLocalScale(pNode); + bool hasTranslation = false; + bool hasRotation = false; + bool hasScale = false; + + RawChannel channel; + channel.nodeIndex = raw.GetNodeByName(pNode->GetName()); + + for (FbxLongLong frameIndex = firstFrameIndex; frameIndex <= lastFrameIndex; frameIndex++) { + FbxTime pTime; + pTime.SetFrame(frameIndex, eMode); + + const FbxAMatrix localTransform = pNode->EvaluateLocalTransform(pTime); + const FbxVector4 localTranslation = localTransform.GetT(); + const FbxQuaternion localRotation = localTransform.GetQ(); + const FbxVector4 localScale = computeLocalScale(pNode, pTime); + + const double epsilon = 1e-5f; + hasTranslation |= ( + fabs(localTranslation[0] - baseTranslation[0]) > epsilon || + fabs(localTranslation[1] - baseTranslation[1]) > epsilon || + fabs(localTranslation[2] - baseTranslation[2]) > epsilon); + hasRotation |= ( + fabs(localRotation[0] - baseRotation[0]) > epsilon || + fabs(localRotation[1] - baseRotation[1]) > epsilon || + fabs(localRotation[2] - baseRotation[2]) > epsilon || + fabs(localRotation[3] - baseRotation[3]) > epsilon); + hasScale |= ( + fabs(localScale[0] - baseScaling[0]) > epsilon || + fabs(localScale[1] - baseScaling[1]) > epsilon || + fabs(localScale[2] - baseScaling[2]) > epsilon); + + channel.translations.push_back(toVec3f(localTranslation)); + channel.rotations.push_back(toQuatf(localRotation)); + channel.scales.push_back(toVec3f(localScale)); + } + + if (hasTranslation || hasRotation || hasScale) { + if (!hasTranslation) { + channel.translations.clear(); + } + if (!hasRotation) { + channel.rotations.clear(); + } + if (!hasScale) { + channel.scales.clear(); + } + + animation.channels.emplace_back(channel); + + totalSizeInBytes += channel.translations.size() * sizeof(channel.translations[0]) + + channel.rotations.size() * sizeof(channel.rotations[0]) + + channel.scales.size() * sizeof(channel.scales[0]); + } + + if (verboseOutput) { + fmt::printf("\ranimation %d: %s (%d%%)", i, (const char *) animStackName, nodeIndex * 100 / nodeCount); + } + } + + raw.AddAnimation(animation); + + if (verboseOutput) { + fmt::printf( + "\ranimation %d: %s (%d channels, %3.1f MB)\n", i, (const char *) animStackName, + (int) animation.channels.size(), (float) totalSizeInBytes * 1e-6f); + } + } +} + +static FbxString GetFileFolder(const char *fileName) +{ + std::string clean = Gltf::StringUtils::GetCleanPathString(fileName, Gltf::StringUtils::PATH_UNIX); + + FbxString folder = clean.c_str(); + folder = folder.Left(folder.ReverseFind('/') + 1); // strip the file name + if (folder.GetLen() < 2 || folder[1] != ':') { + folder = std::string(FileUtils::GetCurrentFolder() + folder.Buffer()).c_str(); + } + return folder; +} + +static std::string GetInferredFileName(const char *fbxFileName, const char *directory, const std::vector &directoryFileList) +{ + // Get the file name with file extension. + const std::string fileName = Gltf::StringUtils::GetFileNameString(Gltf::StringUtils::GetCleanPathString(fbxFileName)); + + // Try to find a match with extension. + for (const auto &file : directoryFileList) { + if (Gltf::StringUtils::CompareNoCase(fileName, file) == 0) { + return std::string(directory) + file; + } + } + + // Some FBX textures end with "_c.dds" while the source texture is a ".tga". + const bool isDDS = fileName.rfind("_c.dds") != std::string::npos; + + // Get the file name without file extension. + const std::string fileBase = isDDS ? fileName.substr(0, fileName.length() - 6) : Gltf::StringUtils::GetFileBaseString(fileName); + + // Try to find a match without file extension. + for (const auto &file : directoryFileList) { + const std::string listedFileBase = Gltf::StringUtils::GetFileBaseString(file.c_str()); + + // If the two extension-less base names match. + if (Gltf::StringUtils::CompareNoCase(fileBase, listedFileBase) == 0) { + // Return the name with extension of the file in the directory. + return std::string(directory) + file; + } + } + + // Return the original file with extension + return fbxFileName; +} + +/* + The texture file names inside of the FBX often contain some long author-specific + path with the wrong extensions. For instance, all of the art assets may be PSD + files in the FBX metadata, but in practice they are delivered as TGA or PNG files. + + This function takes a texture file name stored in the FBX, which may be an absolute + path on the author's computer such as "C:\MyProject\TextureName.psd", and matches + it to a list of existing texture files in the same directory as the FBX file. +*/ +static void +FindFbxTextures(FbxScene *pScene, const char *fbxFileName, const char *extensions, std::map &textureNames) +{ + // Get the folder the FBX file is in. + const FbxString folder = GetFileFolder(fbxFileName); + + // Check if there is a filename.fbm folder to which embedded textures were extracted. + const FbxString fbmFolderName = folder + Gltf::StringUtils::GetFileBaseString(fbxFileName).c_str() + ".fbm/"; + + // Search either in the folder with embedded textures or in the same folder as the FBX file. + const FbxString searchFolder = FileUtils::FolderExists(fbmFolderName) ? fbmFolderName : folder; + + // Get a list with all the texture files from either the folder with embedded textures or the same folder as the FBX file. + std::vector fileList; + FileUtils::ListFolderFiles(fileList, searchFolder, extensions); + + // Try to match the FBX texture names with the actual files on disk. + for (int i = 0; i < pScene->GetTextureCount(); i++) { + const FbxTexture *pTexture = pScene->GetTexture(i); + const FbxFileTexture *pFileTexture = FbxCast(pTexture); + if (pFileTexture == nullptr) { + continue; + } + const FbxString name = GetInferredFileName(pFileTexture->GetFileName(), searchFolder, fileList).c_str(); + textureNames.emplace(pTexture, name); + } +} + +bool LoadFBXFile(RawModel &raw, const char *fbxFileName, const char *textureExtensions) +{ + FbxManager *pManager = FbxManager::Create(); + FbxIOSettings *pIoSettings = FbxIOSettings::Create(pManager, IOSROOT); + pManager->SetIOSettings(pIoSettings); + + FbxImporter *pImporter = FbxImporter::Create(pManager, ""); + + if (!pImporter->Initialize(fbxFileName, -1, pManager->GetIOSettings())) { + if (verboseOutput) { + fmt::printf("%s\n", pImporter->GetStatus().GetErrorString()); + } + pImporter->Destroy(); + pManager->Destroy(); + return false; + } + + FbxScene *pScene = FbxScene::Create(pManager, "fbxScene"); + pImporter->Import(pScene); + pImporter->Destroy(); + + if (pScene == nullptr) { + pImporter->Destroy(); + pManager->Destroy(); + return false; + } + + std::map textureNames; + FindFbxTextures(pScene, fbxFileName, textureExtensions, textureNames); + + // Use Y up for glTF + FbxAxisSystem::MayaYUp.ConvertScene(pScene); + + // Use meters as the default unit for glTF + FbxSystemUnit sceneSystemUnit = pScene->GetGlobalSettings().GetSystemUnit(); + if (sceneSystemUnit != FbxSystemUnit::m) { + FbxSystemUnit::m.ConvertScene(pScene); + } + + ReadNodeHierarchy(raw, pScene, pScene->GetRootNode(), "", ""); + ReadNodeAttributes(raw, pScene, pScene->GetRootNode(), textureNames); + ReadAnimations(raw, pScene); + + pScene->Destroy(); + pManager->Destroy(); + + return true; +} diff --git a/src/Fbx2Raw.h b/src/Fbx2Raw.h new file mode 100644 index 0000000..935da9d --- /dev/null +++ b/src/Fbx2Raw.h @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef __FBX2RAW_H__ +#define __FBX2RAW_H__ + +#include "RawModel.h" + +bool LoadFBXFile(RawModel &raw, const char *fbxFileName, const char *textureExtensions); + +#endif // !__FBX2RAW_H__ diff --git a/src/Raw2Gltf.cpp b/src/Raw2Gltf.cpp new file mode 100644 index 0000000..1149fe5 --- /dev/null +++ b/src/Raw2Gltf.cpp @@ -0,0 +1,718 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include +#include +#include +#include + +#include "FBX2glTF.h" +#include "utils/String_Utils.h" +#include "RawModel.h" +#include "Raw2Gltf.h" + +#include "glTF/AccessorData.h" +#include "glTF/AnimationData.h" +#include "glTF/BufferData.h" +#include "glTF/BufferViewData.h" +#include "glTF/CameraData.h" +#include "glTF/ImageData.h" +#include "glTF/MaterialData.h" +#include "glTF/MeshData.h" +#include "glTF/NodeData.h" +#include "glTF/PrimitiveData.h" +#include "glTF/SamplerData.h" +#include "glTF/SceneData.h" +#include "glTF/SkinData.h" +#include "glTF/TextureData.h" + +typedef unsigned short TriangleIndex; + +extern bool verboseOutput; + +const static std::string defaultSceneName = "Root Scene"; + +/** + * glTF 2.0 is based on the idea that data structs within a file are referenced by index; an accessor will + * point to the n:th buffer view, and so on. The Holder class takes a freshly instantiated class, and then + * creates, stored, and returns a shared_ptr for it. + * + * The idea is that every glTF resource in the file will live as long as the Holder does, and the Holders + * are all kept in the GLTFData struct. Clients may certainly cnhoose to perpetuate the full shared_ptr + * reference counting type, but generally speaking we pass around simple T& and T* types because the GLTFData + * struct will, by design, outlive all other activity that takes place during in a single conversion run. + */ +template +struct Holder +{ + std::vector> ptrs; + std::shared_ptr hold(T *ptr) + { + ptr->ix = ptrs.size(); + ptrs.emplace_back(ptr); + return ptrs.back(); + } +}; + +struct GLTFData +{ + explicit GLTFData(bool _isGlb) + : binary(new std::vector), + isGlb(_isGlb) + { + } + + std::shared_ptr GetAlignedBufferView(BufferData &buffer, const BufferViewData::GL_ArrayType target) + { + unsigned long bufferSize = this->binary->size(); + if ((bufferSize % 4) > 0) { + bufferSize += (4 - (bufferSize % 4)); + this->binary->resize(bufferSize); + } + return this->bufferViews.hold(new BufferViewData(buffer, bufferSize, target)); + } + + // add a bufferview on the fly and copy data into it + std::shared_ptr AddRawBufferView(BufferData &buffer, const char *source, uint32_t bytes) + { + auto bufferView = GetAlignedBufferView(buffer, BufferViewData::GL_ARRAY_NONE); + bufferView->byteLength = bytes; + + // make space for the new bytes (possibly moving the underlying data) + unsigned long bufferSize = this->binary->size(); + this->binary->resize(bufferSize + bytes); + + // and copy them into place + memcpy(&(*this->binary)[bufferSize], source, bytes); + return bufferView; + } + + template + std::shared_ptr AddAccessorWithView( + BufferViewData &bufferView, const GLType &type, const std::vector &source) + { + auto accessor = accessors.hold(new AccessorData(bufferView, type)); + accessor->appendAsBinaryArray(source, *binary); + bufferView.byteLength = accessor->byteLength(); + return accessor; + } + + template + std::shared_ptr AddAccessorAndView( + BufferData &buffer, const GLType &type, const std::vector &source) + { + auto bufferView = GetAlignedBufferView(buffer, BufferViewData::GL_ARRAY_NONE); + return AddAccessorWithView(*bufferView, type, source); + } + + template + std::shared_ptr AddAttributeToPrimitive( + BufferData &buffer, const RawModel &surfaceModel, PrimitiveData &primitive, + const AttributeDefinition &attrDef) + { + // copy attribute data into vector + std::vector attribArr; + surfaceModel.GetAttributeArray(attribArr, attrDef.rawAttributeIx); + + std::shared_ptr accessor; + if (attrDef.dracoComponentType != draco::DT_INVALID && primitive.dracoMesh != nullptr) { + primitive.AddDracoAttrib(attrDef, attribArr); + + accessor = accessors.hold(new AccessorData(attrDef.glType)); + accessor->count = attribArr.size(); + } else { + auto bufferView = GetAlignedBufferView(buffer, BufferViewData::GL_ARRAY_BUFFER); + accessor = AddAccessorWithView(*bufferView, attrDef.glType, attribArr); + } + primitive.AddAttrib(attrDef.gltfName, *accessor); + return accessor; + }; + + template + void serializeHolder(json &glTFJson, std::string key, const Holder holder) + { + if (!holder.ptrs.empty()) { + std::vector bits; + for (const auto &ptr : holder.ptrs) { + bits.push_back(ptr->serialize()); + } + glTFJson[key] = bits; + } + } + + void serializeHolders(json &glTFJson) + { + serializeHolder(glTFJson, "buffers", buffers); + serializeHolder(glTFJson, "bufferViews", bufferViews); + serializeHolder(glTFJson, "scenes", scenes); + serializeHolder(glTFJson, "accessors", accessors); + serializeHolder(glTFJson, "images", images); + serializeHolder(glTFJson, "samplers", samplers); + serializeHolder(glTFJson, "textures", textures); + serializeHolder(glTFJson, "materials", materials); + serializeHolder(glTFJson, "meshes", meshes); + serializeHolder(glTFJson, "skins", skins); + serializeHolder(glTFJson, "animations", animations); + serializeHolder(glTFJson, "cameras", cameras); + serializeHolder(glTFJson, "nodes", nodes); + } + + const bool isGlb; + std::shared_ptr > binary; + + Holder buffers; + Holder bufferViews; + Holder accessors; + Holder images; + Holder samplers; + Holder textures; + Holder materials; + Holder meshes; + Holder skins; + Holder animations; + Holder cameras; + Holder nodes; + Holder scenes; +}; + +/** + * This method sanity-checks existance and then returns a *reference* to the *Data instance + * registered under that name. This is safe in the context of this tool, where all such data + * classes are guaranteed to stick around for the duration of the process. + */ +template +T &require(std::map> map, std::string key) +{ + auto iter = map.find(key); + assert(iter != map.end()); + T &result = *iter->second; + return result; +} + +static const std::vector getIndexArray(const RawModel &raw) +{ + std::vector result; + + for (int i = 0; i < raw.GetTriangleCount(); i++) { + result.push_back((TriangleIndex) raw.GetTriangle(i).verts[0]); + result.push_back((TriangleIndex) raw.GetTriangle(i).verts[1]); + result.push_back((TriangleIndex) raw.GetTriangle(i).verts[2]); + } + return result; +} + +// TODO: replace with a proper MaterialHasher class +static const std::string materialHash(const RawMaterial &m) { + return m.name + "_" + std::to_string(m.type); +} + +ModelData *Raw2Gltf( + std::ofstream &gltfOutStream, + const RawModel &raw, + const GltfOptions &options +) +{ + if (verboseOutput) { + fmt::printf("Building render model...\n"); + for (int i = 0; i < raw.GetMaterialCount(); i++) { + fmt::printf( + "Material %d: %s [shading: %s]\n", i, raw.GetMaterial(i).name.c_str(), + raw.GetMaterial(i).shadingModel.c_str()); + } + if (raw.GetVertexCount() > 2 * raw.GetTriangleCount()) { + fmt::printf( + "Warning: High vertex count. Make sure there are no unnecessary vertex attributes. (see -keepAttribute cmd-line option)"); + } + } + + std::vector materialModels; + raw.CreateMaterialModels(materialModels, (1 << (sizeof(TriangleIndex) * 8)), options.keepAttribs, true); + + if (verboseOutput) { + fmt::printf("%7d vertices\n", raw.GetVertexCount()); + fmt::printf("%7d triangles\n", raw.GetTriangleCount()); + fmt::printf("%7d textures\n", raw.GetTextureCount()); + fmt::printf("%7d nodes\n", raw.GetNodeCount()); + fmt::printf("%7d surfaces\n", (int) materialModels.size()); + fmt::printf("%7d animations\n", raw.GetAnimationCount()); + } + + std::unique_ptr gltf(new GLTFData(options.outputBinary)); + + std::map> nodesByName; + std::map> materialsByName; + std::map> meshByNodeName; + + // for now, we only have one buffer; data->binary points to the same vector as that BufferData does. + BufferData &buffer = *gltf->buffers.hold( + options.outputBinary ? + new BufferData(gltf->binary) : + new BufferData(extBufferFilename, gltf->binary, options.embedResources)); + { + // + // nodes + // + + for (int i = 0; i < raw.GetNodeCount(); i++) { + // assumption: RawNode index == NodeData index + const RawNode &node = raw.GetNode(i); + + auto nodeData = gltf->nodes.hold( + new NodeData(node.name, node.translation, node.rotation, node.scale, node.isJoint)); + + for (const auto &childName : node.childNames) { + int childIx = raw.GetNodeByName(childName.c_str()); + assert(childIx >= 0); + nodeData->AddChildNode(childIx); + } + assert(nodesByName.find(nodeData->name) == nodesByName.end()); + nodesByName.insert(std::make_pair(nodeData->name, nodeData)); + } + + // + // animations + // + + for (int i = 0; i < raw.GetAnimationCount(); i++) { + const RawAnimation &animation = raw.GetAnimation(i); + + auto accessor = gltf->AddAccessorAndView(buffer, GLT_FLOAT, animation.times); + accessor->min = { *std::min_element(std::begin(animation.times), std::end(animation.times)) }; + accessor->max = { *std::max_element(std::begin(animation.times), std::end(animation.times)) }; + + AnimationData &aDat = *gltf->animations.hold(new AnimationData(animation.name, *accessor)); + if (verboseOutput) { + fmt::printf("Animation '%s' has %lu channels:\n", animation.name.c_str(), animation.channels.size()); + } + + for (size_t j = 0; j < animation.channels.size(); j++) { + const RawChannel &channel = animation.channels[j]; + const RawNode &node = raw.GetNode(channel.nodeIndex); + + if (verboseOutput) { + fmt::printf( + " Channel %lu (%s) has translations/rotations/scales: [%lu, %lu, %lu]\n", + j, node.name.c_str(), channel.translations.size(), + channel.rotations.size(), channel.scales.size()); + } + + NodeData &nDat = require(nodesByName, node.name); + if (!channel.translations.empty()) { + aDat.AddNodeChannel(nDat, *gltf->AddAccessorAndView(buffer, GLT_VEC3F, channel.translations), "translation"); + } + if (!channel.rotations.empty()) { + aDat.AddNodeChannel(nDat, *gltf->AddAccessorAndView(buffer, GLT_QUATF, channel.rotations), "rotation"); + } + if (!channel.scales.empty()) { + aDat.AddNodeChannel(nDat, *gltf->AddAccessorAndView(buffer, GLT_VEC3F, channel.scales), "scale"); + } + } + } + + // + // samplers + // + + SamplerData &defaultSampler = *gltf->samplers.hold(new SamplerData()); + + // + // textures + // + + for (int textureIndex = 0; textureIndex < raw.GetTextureCount(); textureIndex++) { + const RawTexture &texture = raw.GetTexture(textureIndex); + std::string textureName = Gltf::StringUtils::GetFileBaseString(texture.name); + // texture.name is the inferred filename on *our* system + const std::string texFilename = texture.name; + ImageData *source = nullptr; + if (options.outputBinary) { + std::ifstream file(texFilename, std::ios::binary | std::ios::ate); + if (file) { + std::streamsize size = file.tellg(); + file.seekg(0, std::ios::beg); + + std::vector fileBuffer(size); + if (file.read(fileBuffer.data(), size)) { + auto bufferView = gltf->AddRawBufferView(buffer, fileBuffer.data(), size); + source = new ImageData(textureName, *bufferView, "image/png"); + } else { + fmt::printf("Warning: Couldn't read %lu bytes from %s, skipping\n", size, texFilename.c_str()); + } + } else { + fmt::printf("Warning: Couldn't open texture file %s, skipping\n", texFilename.c_str()); + } + } else { + // TODO: write to buffer.bin? + source = new ImageData(textureName, textureName + ".ktx"); + } + if (!source) { + // fallback is tiny transparent gif + source = new ImageData(textureName, "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="); + } + + TextureData &texDat = *gltf->textures.hold( + new TextureData(textureName, defaultSampler, *gltf->images.hold(source))); + assert(texDat.ix == textureIndex); + } + + // + // materials + // + + for (int materialIndex = 0; materialIndex < raw.GetMaterialCount(); materialIndex++) { + const RawMaterial &material = raw.GetMaterial(materialIndex); + + // find a texture by usage and return it as a TextureData*, or nullptr if none exists. + auto getTex = [&](RawTextureUsage usage) + { + // note that we depend on TextureData.ix == rawTexture's index + return (material.textures[usage] >= 0) ? gltf->textures.ptrs[material.textures[usage]].get() : nullptr; + }; + + std::shared_ptr pbrMetRough; + if (options.usePBRMetRough) { + pbrMetRough.reset(new PBRMetallicRoughness(getTex(RAW_TEXTURE_USAGE_DIFFUSE), material.diffuseFactor)); + } + std::shared_ptr pbrSpecGloss; + if (options.usePBRSpecGloss) { + pbrSpecGloss.reset( + new PBRSpecularGlossiness( + getTex(RAW_TEXTURE_USAGE_DIFFUSE), material.diffuseFactor, + getTex(RAW_TEXTURE_USAGE_SPECULAR), material.specularFactor, material.shininess)); + } + + std::shared_ptr khrComMat; + if (options.useKHRMatCom) { + auto type = KHRCommonMats::MaterialType::Constant; + if (material.shadingModel == "Lambert") { + type = KHRCommonMats::MaterialType::Lambert; + } else if (material.shadingModel == "Blinn") { + type = KHRCommonMats::MaterialType::Blinn; + } else if (material.shadingModel == "Phong") { + type = KHRCommonMats::MaterialType::Phong; + } + khrComMat.reset( + new KHRCommonMats( + type, + getTex(RAW_TEXTURE_USAGE_SHININESS), material.shininess, + getTex(RAW_TEXTURE_USAGE_AMBIENT), material.ambientFactor, + getTex(RAW_TEXTURE_USAGE_DIFFUSE), material.diffuseFactor, + getTex(RAW_TEXTURE_USAGE_SPECULAR), material.specularFactor)); + } + std::shared_ptr mData = gltf->materials.hold( + new MaterialData( + material.name, getTex(RAW_TEXTURE_USAGE_NORMAL), + getTex(RAW_TEXTURE_USAGE_EMISSIVE), material.emissiveFactor, + khrComMat, pbrMetRough, pbrSpecGloss)); + materialsByName[materialHash(material)] = mData; + } + + // + // surfaces + // + + // in GLTF 2.0, the structural limitation is that a node can + // only belong to a single mesh. A mesh can however contain any + // number of primitives, which are essentially little meshes. + // + // so each RawSurface turns into a primitive, and we sort them + // by root node using this map; one mesh per node. + + for (size_t surfaceIndex = 0; surfaceIndex < materialModels.size(); surfaceIndex++) { + const RawModel &surfaceModel = materialModels[surfaceIndex]; + assert(surfaceModel.GetSurfaceCount() == 1); + + const RawSurface &rawSurface = surfaceModel.GetSurface(0); + const std::string surfaceName = std::to_string(surfaceIndex) + "_" + rawSurface.name; + + const RawMaterial &rawMaterial = surfaceModel.GetMaterial(surfaceModel.GetTriangle(0).materialIndex); + if (rawMaterial.textures[RAW_TEXTURE_USAGE_DIFFUSE] < 0 && + (surfaceModel.GetVertexAttributes() & RAW_VERTEX_ATTRIBUTE_COLOR) == 0) { + if (verboseOutput) { + fmt::printf("Warning: surface %s has neither texture nor vertex colors.\n", surfaceName.c_str()); + } + } + const MaterialData &mData = require(materialsByName, materialHash(rawMaterial)); + + std::string nodeName = rawSurface.nodeName; + NodeData &meshNode = require(nodesByName, nodeName); + + MeshData *mesh = nullptr; + auto meshIter = meshByNodeName.find(nodeName); + if (meshIter != meshByNodeName.end()) { + mesh = meshIter->second.get(); + + } else { + auto meshPtr = gltf->meshes.hold(new MeshData(rawSurface.name)); + meshByNodeName[nodeName] = meshPtr; + meshNode.SetMesh(meshPtr->ix); + mesh = meshPtr.get(); + } + + // + // surface skin + // + if (!rawSurface.jointNames.empty()) { + if (meshNode.skin == -1) { + // glTF uses column-major matrices + std::vector inverseBindMatrices; + for (const auto &inverseBindMatrice : rawSurface.inverseBindMatrices) { + inverseBindMatrices.push_back(inverseBindMatrice.Transpose()); + } + + std::vector jointIndexes; + for (const auto &jointName : rawSurface.jointNames) { + jointIndexes.push_back(require(nodesByName, jointName).ix); + } + + // Write out inverseBindMatrices + auto accIBM = gltf->AddAccessorAndView(buffer, GLT_MAT4F, inverseBindMatrices); + + auto skeletonRoot = require(nodesByName, rawSurface.skeletonRootName); + auto skin = *gltf->skins.hold(new SkinData(jointIndexes, *accIBM, skeletonRoot)); + meshNode.SetSkin(skin.ix); + } + } + + std::shared_ptr primitive; + if (options.useDraco) { + int triangleCount = surfaceModel.GetTriangleCount(); + + // initialize Draco mesh with vertex index information + std::shared_ptr dracoMesh; + dracoMesh->SetNumFaces(static_cast(triangleCount)); + + for (uint32_t ii = 0; ii < triangleCount; ii++) { + draco::Mesh::Face face; + face[0] = surfaceModel.GetTriangle(ii).verts[0]; + face[1] = surfaceModel.GetTriangle(ii).verts[1]; + face[2] = surfaceModel.GetTriangle(ii).verts[2]; + dracoMesh->SetFace(draco::FaceIndex(ii), face); + } + + AccessorData &indexes = *gltf->accessors.hold(new AccessorData(GLT_USHORT)); + indexes.count = 3 * triangleCount; + primitive.reset(new PrimitiveData(indexes, mData, dracoMesh)); + } else { + const AccessorData &indexes = *gltf->AddAccessorWithView( + *gltf->GetAlignedBufferView(buffer, BufferViewData::GL_ELEMENT_ARRAY_BUFFER), + GLT_USHORT, getIndexArray(surfaceModel)); + primitive.reset(new PrimitiveData(indexes, mData)); + }; + + // + // surface vertices + // + { + if ((surfaceModel.GetVertexAttributes() & RAW_VERTEX_ATTRIBUTE_POSITION) != 0) { + const AttributeDefinition ATTR_POSITION("POSITION", &RawVertex::position, + GLT_VEC3F, draco::GeometryAttribute::POSITION, draco::DT_FLOAT32); + auto accessor = gltf->AddAttributeToPrimitive( + buffer, surfaceModel, *primitive, ATTR_POSITION); + + accessor->min = toStdVec(rawSurface.bounds.min); + accessor->max = toStdVec(rawSurface.bounds.max); + } + if ((surfaceModel.GetVertexAttributes() & RAW_VERTEX_ATTRIBUTE_NORMAL) != 0) { + const AttributeDefinition ATTR_NORMAL("NORMAL", &RawVertex::normal, + GLT_VEC3F, draco::GeometryAttribute::NORMAL, draco::DT_FLOAT32); + gltf->AddAttributeToPrimitive(buffer, surfaceModel, *primitive, ATTR_NORMAL); + } + if ((surfaceModel.GetVertexAttributes() & RAW_VERTEX_ATTRIBUTE_TANGENT) != 0) { + const AttributeDefinition ATTR_TANGENT("TANGENT", &RawVertex::tangent, GLT_VEC4F); + gltf->AddAttributeToPrimitive(buffer, surfaceModel, *primitive, ATTR_TANGENT); + } + if ((surfaceModel.GetVertexAttributes() & RAW_VERTEX_ATTRIBUTE_COLOR) != 0) { + const AttributeDefinition ATTR_COLOR("COLOR_0", &RawVertex::color, GLT_VEC4F, + draco::GeometryAttribute::COLOR, draco::DT_FLOAT32); + gltf->AddAttributeToPrimitive(buffer, surfaceModel, *primitive, ATTR_COLOR); + } + if ((surfaceModel.GetVertexAttributes() & RAW_VERTEX_ATTRIBUTE_UV0) != 0) { + const AttributeDefinition ATTR_TEXCOORD_0("TEXCOORD_0", &RawVertex::uv0, + GLT_VEC2F, draco::GeometryAttribute::TEX_COORD, draco::DT_FLOAT32); + gltf->AddAttributeToPrimitive(buffer, surfaceModel, *primitive, ATTR_TEXCOORD_0); + } + if ((surfaceModel.GetVertexAttributes() & RAW_VERTEX_ATTRIBUTE_UV1) != 0) { + const AttributeDefinition ATTR_TEXCOORD_1("TEXCOORD_1", &RawVertex::uv1, + GLT_VEC2F, draco::GeometryAttribute::TEX_COORD, draco::DT_FLOAT32); + gltf->AddAttributeToPrimitive(buffer, surfaceModel, *primitive, ATTR_TEXCOORD_1); + } + if ((surfaceModel.GetVertexAttributes() & RAW_VERTEX_ATTRIBUTE_JOINT_INDICES) != 0) { + const AttributeDefinition ATTR_JOINTS("JOINTS_0", &RawVertex::jointIndices, + GLT_VEC4I, draco::GeometryAttribute::GENERIC, draco::DT_UINT16); + gltf->AddAttributeToPrimitive(buffer, surfaceModel, *primitive, ATTR_JOINTS); + } + if ((surfaceModel.GetVertexAttributes() & RAW_VERTEX_ATTRIBUTE_JOINT_WEIGHTS) != 0) { + const AttributeDefinition ATTR_WEIGHTS("WEIGHTS_0", &RawVertex::jointWeights, + GLT_VEC4F, draco::GeometryAttribute::GENERIC, draco::DT_FLOAT32); + gltf->AddAttributeToPrimitive(buffer, surfaceModel, *primitive, ATTR_WEIGHTS); + } + } + if (options.useDraco) { + // Set up the encoder. + draco::Encoder encoder; + + // TODO: generalize / allow configuration + encoder.SetSpeedOptions(5, 5); + encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14); + encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 10); + encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10); + encoder.SetAttributeQuantization(draco::GeometryAttribute::COLOR, 8); + encoder.SetAttributeQuantization(draco::GeometryAttribute::GENERIC, 8); + encoder.SetEncodingMethod(draco::MeshEncoderMethod::MESH_EDGEBREAKER_ENCODING); + + draco::EncoderBuffer dracoBuffer; + draco::Status status = encoder.EncodeMeshToBuffer(*primitive->dracoMesh, &dracoBuffer); + assert(status.code() == draco::Status::OK); + + auto view = gltf->AddRawBufferView(buffer, dracoBuffer.data(), dracoBuffer.size()); + primitive->NoteDracoBuffer(*view); + } + mesh->AddPrimitive(primitive); + } + + // + // cameras + // + + for (int i = 0; i < raw.GetCameraCount(); i++) { + const RawCamera &cam = raw.GetCamera(i); + CameraData &camera = *gltf->cameras.hold(new CameraData()); + camera.name = cam.name; + + if (cam.mode == RawCamera::CAMERA_MODE_PERSPECTIVE) { + camera.type = "perspective"; + camera.aspectRatio = cam.perspective.aspectRatio; + camera.yfov = cam.perspective.fovDegreesY * ((float) M_PI / 180.0f); + camera.znear = cam.perspective.nearZ; + camera.zfar = cam.perspective.farZ; + } else { + camera.type = "orthographic"; + camera.xmag = cam.orthographic.magX; + camera.ymag = cam.orthographic.magY; + camera.znear = cam.orthographic.nearZ; + camera.zfar = cam.orthographic.farZ; + } + // Add the camera to the node hierarchy. + + auto iter = nodesByName.find(cam.nodeName); + if (iter == nodesByName.end()) { + fmt::printf("Warning: Camera node name %s does not exist.\n", cam.nodeName); + continue; + } + iter->second->AddCamera(cam.name); + } + } + + NodeData &rootNode = require(nodesByName, "RootNode"); + const SceneData &rootScene = *gltf->scenes.hold(new SceneData(defaultSceneName, rootNode)); + + if (options.outputBinary) { + // note: glTF binary is little-endian + const char glbHeader[] = { + 'g', 'l', 'T', 'F', // magic + 0x02, 0x00, 0x00, 0x00, // version + 0x00, 0x00, 0x00, 0x00, // total length: written in later + }; + gltfOutStream.write(glbHeader, 12); + + // binary glTF 2.0 has a sub-header for each of the JSON and BIN chunks + const char glb2JsonHeader[] = { + 0x00, 0x00, 0x00, 0x00, // chunk length: written in later + 'J', 'S', 'O', 'N', // chunk type: 0x4E4F534A aka JSON + }; + gltfOutStream.write(glb2JsonHeader, 8); + } + + { + std::vector extensionsUsed, extensionsRequired; + if (options.useKHRMatCom) { + extensionsUsed.push_back(KHR_MATERIALS_COMMON); + if (!options.usePBRSpecGloss && !options.usePBRMetRough) { + extensionsRequired.push_back(KHR_MATERIALS_COMMON); + } + } + if (options.usePBRSpecGloss) { + extensionsUsed.push_back(KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS); + if (!options.useKHRMatCom && !options.usePBRMetRough) { + extensionsRequired.push_back(KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS); + } + } + if (options.useDraco) { + extensionsUsed.push_back(KHR_DRACO_MESH_COMPRESSION); + extensionsRequired.push_back(KHR_DRACO_MESH_COMPRESSION); + } + + json glTFJson { + { "asset", { + { "generator", "FBX2glTF" }, + { "version", "2.0" }}}, + { "extensionsUsed", extensionsUsed }, + { "extensionsRequired", extensionsRequired }, + { "scene", rootScene.ix } + }; + + gltf->serializeHolders(glTFJson); + gltfOutStream << glTFJson.dump(options.outputBinary ? 0 : 4); + } + if (options.outputBinary) { + uint32_t jsonLength = (uint32_t) gltfOutStream.tellp() - 20; + // the binary body must begin on a 4-aligned address, so pad json with spaces if necessary + while ((jsonLength % 4) != 0) { + gltfOutStream.put(' '); + jsonLength++; + } + + uint32_t binHeader = (uint32_t) gltfOutStream.tellp(); + // binary glTF 2.0 has a sub-header for each of the JSON and BIN chunks + const char glb2BinaryHeader[] = { + 0x00, 0x00, 0x00, 0x00, // chunk length: written in later + 'B', 'I', 'N', 0x00, // chunk type: 0x004E4942 aka BIN + }; + gltfOutStream.write(glb2BinaryHeader, 8); + + // append binary buffer directly to .glb file + uint32_t binaryLength = gltf->binary->size(); + gltfOutStream.write((const char *) &(*gltf->binary)[0], binaryLength); + while ((binaryLength % 4) != 0) { + gltfOutStream.put('\0'); + binaryLength++; + } + uint32_t totalLength = (uint32_t) gltfOutStream.tellp(); + + // seek back to sub-header for json chunk + gltfOutStream.seekp(8); + + // write total length, little-endian + gltfOutStream.put((totalLength >> 0) & 0xFF); + gltfOutStream.put((totalLength >> 8) & 0xFF); + gltfOutStream.put((totalLength >> 16) & 0xFF); + gltfOutStream.put((totalLength >> 24) & 0xFF); + + // write JSON length, little-endian + gltfOutStream.put((jsonLength >> 0) & 0xFF); + gltfOutStream.put((jsonLength >> 8) & 0xFF); + gltfOutStream.put((jsonLength >> 16) & 0xFF); + gltfOutStream.put((jsonLength >> 24) & 0xFF); + + // seek back to the gltf 2.0 binary chunk header + gltfOutStream.seekp(binHeader); + + // write total length, little-endian + gltfOutStream.put((binaryLength >> 0) & 0xFF); + gltfOutStream.put((binaryLength >> 8) & 0xFF); + gltfOutStream.put((binaryLength >> 16) & 0xFF); + gltfOutStream.put((binaryLength >> 24) & 0xFF); + + // be tidy and return write pointer to end-of-file + gltfOutStream.seekp(0, std::ios::end); + } + + return new ModelData(gltf->binary); +} diff --git a/src/Raw2Gltf.h b/src/Raw2Gltf.h new file mode 100644 index 0000000..9922674 --- /dev/null +++ b/src/Raw2Gltf.h @@ -0,0 +1,204 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef __RAW2GLTF_H__ +#define __RAW2GLTF_H__ + +#include +#include + +// This can be a macro under Windows, confusing Draco +#undef ERROR +#include + +#include +using json = nlohmann::json; + +#include "FBX2glTF.h" +#include "RawModel.h" + +static const std::string KHR_DRACO_MESH_COMPRESSION = "KHR_draco_mesh_compression"; +static const std::string KHR_MATERIALS_COMMON = "KHR_materials_common"; +static const std::string KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS = "KHR_materials_pbrSpecularGlossiness"; + +static const std::string extBufferFilename = "buffer.bin"; + +/** + * User-supplied options that dictate the nature of the glTF being generated. + */ +struct GltfOptions +{ + /** + * If negative, disabled. Otherwise, a bitfield of RawVertexAttributes that + * specify the largest set of attributes that'll ever be kept for a vertex. + * The special bit RAW_VERTEX_ATTRIBUTE_AUTO triggers smart mode, where the + * attributes to keep are inferred from which textures are supplied. + */ + int keepAttribs; + /** Whether to output a .glb file, the binary format of glTF. */ + bool outputBinary; + /** If non-binary, whether to inline all resources, for a single (large) .glTF file. */ + bool embedResources; + /** Whether to use KHR_draco_mesh_compression to minimize static geometry size. */ + bool useDraco; + /** Whether to use KHR_materials_common to extend materials definitions. */ + bool useKHRMatCom; + /** Whether to populate the pbrMetallicRoughness substruct in materials. */ + bool usePBRMetRough; + /** Whether to use KHR_materials_pbrSpecularGlossiness to extend material definitions. */ + bool usePBRSpecGloss; +}; + +struct ComponentType { + // OpenGL Datatype enums + enum GL_DataType + { + GL_BYTE = 5120, + GL_UNSIGNED_BYTE, + GL_SHORT, + GL_UNSIGNED_SHORT, + GL_INT, + GL_UNSIGNED_INT, + GL_FLOAT + }; + + const GL_DataType glType; + const unsigned int size; +}; + +static const ComponentType CT_USHORT = {ComponentType::GL_UNSIGNED_SHORT, 2}; +static const ComponentType CT_FLOAT = {ComponentType::GL_FLOAT, 4}; + +// Map our low-level data types for glTF output +struct GLType { + GLType(const ComponentType &componentType, unsigned int count, const std::string dataType) + : componentType(componentType), + count(count), + dataType(dataType) + {} + + unsigned int byteStride() const { return componentType.size * count; } + + void write(uint8_t *buf, const float scalar) const { *((float *) buf) = scalar; } + void write(uint8_t *buf, const uint16_t scalar) const { *((uint16_t *) buf) = scalar; } + + template + void write(uint8_t *buf, const mathfu::Vector &vector) const { + for (int ii = 0; ii < d; ii ++) { + ((T *)buf)[ii] = vector(ii); + } + } + template + void write(uint8_t *buf, const mathfu::Matrix &matrix) const { + // three matrix types require special alignment considerations that we don't handle + // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#data-alignment + assert(!(sizeof(T) == 1 && d == 2)); + assert(!(sizeof(T) == 1 && d == 3)); + assert(!(sizeof(T) == 2 && d == 2)); + for (int col = 0; col < d; col ++) { + for (int row = 0; row < d; row ++) { + // glTF matrices are column-major + ((T *)buf)[col * d + row] = matrix(row, col); + } + } + } + template + void write(uint8_t *buf, const mathfu::Quaternion &quaternion) const { + for (int ii = 0; ii < 3; ii++) { + ((T *)buf)[ii] = quaternion.vector()(ii); + } + ((T *)buf)[3] = quaternion.scalar(); + } + + const ComponentType componentType; + const uint8_t count; + const std::string dataType; +}; + +static const GLType GLT_FLOAT = {CT_FLOAT, 1, "SCALAR"}; +static const GLType GLT_USHORT = {CT_USHORT, 1, "SCALAR"}; +static const GLType GLT_VEC2F = {CT_FLOAT, 2, "VEC2"}; +static const GLType GLT_VEC3F = {CT_FLOAT, 3, "VEC3"}; +static const GLType GLT_VEC4F = {CT_FLOAT, 4, "VEC4"}; +static const GLType GLT_VEC4I = {CT_USHORT, 4, "VEC4"}; +static const GLType GLT_MAT2F = {CT_USHORT, 4, "MAT2"}; +static const GLType GLT_MAT3F = {CT_USHORT, 9, "MAT3"}; +static const GLType GLT_MAT4F = {CT_FLOAT, 16, "MAT4"}; +static const GLType GLT_QUATF = {CT_FLOAT, 4, "VEC4"}; + +/** + * The base of any indexed glTF entity. + */ +struct Holdable +{ + uint32_t ix; + + virtual json serialize() const = 0; +}; + +template +struct AttributeDefinition +{ + const std::string gltfName; + const T RawVertex::* rawAttributeIx; + const GLType glType; + const draco::GeometryAttribute::Type dracoAttribute; + const draco::DataType dracoComponentType; + + AttributeDefinition( + const std::string gltfName, const T RawVertex::*rawAttributeIx, const GLType &_glType, + const draco::GeometryAttribute::Type dracoAttribute, const draco::DataType dracoComponentType) + : gltfName(gltfName), + rawAttributeIx(rawAttributeIx), + glType(_glType), + dracoAttribute(dracoAttribute), + dracoComponentType(dracoComponentType) {} + + AttributeDefinition( + const std::string gltfName, const T RawVertex::*rawAttributeIx, const GLType &_glType) + : gltfName(gltfName), + rawAttributeIx(rawAttributeIx), + glType(_glType), + dracoAttribute(draco::GeometryAttribute::INVALID), + dracoComponentType(draco::DataType::DT_INVALID) {} +}; + +struct AccessorData; +struct AnimationData; +struct BufferData; +struct BufferViewData; +struct CameraData; +struct GLTFData; +struct ImageData; +struct MaterialData; +struct MeshData; +struct NodeData; +struct PrimitiveData; +struct SamplerData; +struct SceneData; +struct SkinData; +struct TextureData; + +struct ModelData +{ + explicit ModelData(std::shared_ptr > const &_binary) + : binary(_binary) + { + } + + std::shared_ptr > const binary; +}; + +ModelData *Raw2Gltf( + std::ofstream &gltfOutStream, + const RawModel &raw, + const GltfOptions &options +); + +#endif // !__RAW2GLTF_H__ diff --git a/src/RawModel.cpp b/src/RawModel.cpp new file mode 100644 index 0000000..d38cb47 --- /dev/null +++ b/src/RawModel.cpp @@ -0,0 +1,538 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include +#include +#include +#include + +#if defined( __unix__ ) +#include +#endif + +#include "FBX2glTF.h" +#include "utils/String_Utils.h" +#include "utils/Image_Utils.h" +#include "RawModel.h" + +static float Log2f(float f) +{ + return logf(f) * 1.442695041f; +} + +bool RawVertex::operator==(const RawVertex &other) const +{ + return (position == other.position) && + (normal == other.normal) && + (tangent == other.tangent) && + (binormal == other.binormal) && + (color == other.color) && + (uv0 == other.uv0) && + (uv1 == other.uv1) && + (jointIndices == other.jointIndices) && + (jointWeights == other.jointWeights) && + (polarityUv0 == other.polarityUv0); +} + +size_t RawVertex::Difference(const RawVertex &other) const +{ + size_t attributes = 0; + if (position != other.position) { attributes |= RAW_VERTEX_ATTRIBUTE_POSITION; } + if (normal != other.normal) { attributes |= RAW_VERTEX_ATTRIBUTE_NORMAL; } + if (tangent != other.tangent) { attributes |= RAW_VERTEX_ATTRIBUTE_TANGENT; } + if (binormal != other.binormal) { attributes |= RAW_VERTEX_ATTRIBUTE_BINORMAL; } + if (color != other.color) { attributes |= RAW_VERTEX_ATTRIBUTE_COLOR; } + if (uv0 != other.uv0) { attributes |= RAW_VERTEX_ATTRIBUTE_UV0; } + if (uv1 != other.uv1) { attributes |= RAW_VERTEX_ATTRIBUTE_UV1; } + // Always need both or neither. + if (jointIndices != other.jointIndices) { attributes |= RAW_VERTEX_ATTRIBUTE_JOINT_INDICES | RAW_VERTEX_ATTRIBUTE_JOINT_WEIGHTS; } + if (jointWeights != other.jointWeights) { attributes |= RAW_VERTEX_ATTRIBUTE_JOINT_INDICES | RAW_VERTEX_ATTRIBUTE_JOINT_WEIGHTS; } + return attributes; +} + +RawModel::RawModel() + : vertexAttributes(0) +{ +} + +void RawModel::AddVertexAttribute(const RawVertexAttribute attrib) +{ + vertexAttributes |= attrib; +} + +int RawModel::AddVertex(const RawVertex &vertex) +{ + auto it = vertexHash.find(vertex); + if (it != vertexHash.end()) { + return it->second; + } + vertexHash.emplace(vertex, (int) vertices.size()); + vertices.push_back(vertex); + return (int) vertices.size() - 1; +} + +int RawModel::AddTriangle(const int v0, const int v1, const int v2, const int materialIndex, const int surfaceIndex) +{ + const RawTriangle triangle = {{v0, v1, v2}, materialIndex, surfaceIndex}; + triangles.push_back(triangle); + return (int) triangles.size() - 1; +} + +int RawModel::AddTexture(const char *name, const char *fileName, const RawTextureUsage usage) +{ + if (name[0] == '\0') { + return -1; + } + for (size_t i = 0; i < textures.size(); i++) { + if (Gltf::StringUtils::CompareNoCase(textures[i].name, name) == 0 && textures[i].usage == usage) { + return (int) i; + } + } + + const ImageProperties properties = GetImageProperties(name); + + RawTexture texture; + texture.name = name; + texture.width = properties.width; + texture.height = properties.height; + texture.mipLevels = (int) ceilf(Log2f(std::max((float) properties.width, (float) properties.height))); + texture.usage = usage; + texture.occlusion = (properties.occlusion == IMAGE_TRANSPARENT) ? + RAW_TEXTURE_OCCLUSION_TRANSPARENT : RAW_TEXTURE_OCCLUSION_OPAQUE; + texture.fileName = fileName; + textures.emplace_back(texture); + return (int) textures.size() - 1; +} + +int RawModel::AddMaterial(const RawMaterial &material) +{ + return AddMaterial( + material.name.c_str(), material.shadingModel.c_str(), material.type, material.textures, material.ambientFactor, + material.diffuseFactor, material.specularFactor, material.emissiveFactor, material.shininess); +} + +int RawModel::AddMaterial( + const char *name, const char *shadingModel, const RawMaterialType materialType, + const int textures[RAW_TEXTURE_USAGE_MAX], const Vec3f ambientFactor, + const Vec4f diffuseFactor, const Vec3f specularFactor, + const Vec3f emissiveFactor, float shinineness) +{ + for (size_t i = 0; i < materials.size(); i++) { + if (materials[i].name != name) { + continue; + } + if (materials[i].shadingModel != shadingModel) { + continue; + } + if (materials[i].type != materialType) { + continue; + } + if (materials[i].ambientFactor != ambientFactor || + materials[i].diffuseFactor != diffuseFactor || + materials[i].specularFactor != specularFactor || + materials[i].emissiveFactor != emissiveFactor || + materials[i].shininess != shinineness) { + continue; + } + + bool match = true; + for (int j = 0; match && j < RAW_TEXTURE_USAGE_MAX; j++) { + match = match && (materials[i].textures[j] == textures[j]); + } + if (match) { + return (int) i; + } + } + + RawMaterial material; + material.name = name; + material.shadingModel = shadingModel; + material.type = materialType; + material.ambientFactor = ambientFactor; + material.diffuseFactor = diffuseFactor; + material.specularFactor = specularFactor; + material.emissiveFactor = emissiveFactor; + material.shininess = shinineness; + + for (int i = 0; i < RAW_TEXTURE_USAGE_MAX; i++) { + material.textures[i] = textures[i]; + } + + materials.emplace_back(material); + + return (int) materials.size() - 1; +} + +int RawModel::AddSurface(const RawSurface &surface) +{ + for (size_t i = 0; i < surfaces.size(); i++) { + if (Gltf::StringUtils::CompareNoCase(surfaces[i].name, surface.name) == 0) { + return (int) i; + } + } + + surfaces.emplace_back(surface); + return (int) (surfaces.size() - 1); +} + +int RawModel::AddSurface(const char *name, const char *nodeName) +{ + assert(name[0] != '\0'); + + for (size_t i = 0; i < surfaces.size(); i++) { + if (Gltf::StringUtils::CompareNoCase(surfaces[i].name, name) == 0) { + return (int) i; + } + } + RawSurface surface; + surface.name = name; + surface.nodeName = nodeName; + surface.bounds.Clear(); + surface.discrete = false; + surface.skinRigid = false; + + surfaces.emplace_back(surface); + return (int) (surfaces.size() - 1); +} + +int RawModel::AddAnimation(const RawAnimation &animation) +{ + animations.emplace_back(animation); + return (int) (animations.size() - 1); +} + +int RawModel::AddNode(const RawNode &node) +{ + for (size_t i = 0; i < nodes.size(); i++) { + if (Gltf::StringUtils::CompareNoCase(nodes[i].name.c_str(), node.name) == 0) { + return (int) i; + } + } + + nodes.emplace_back(node); + return (int) nodes.size() - 1; +} + +int RawModel::AddCameraPerspective( + const char *name, const char *nodeName, const float aspectRatio, const float fovDegreesX, const float fovDegreesY, const float nearZ, + const float farZ) +{ + RawCamera camera; + camera.name = name; + camera.nodeName = nodeName; + camera.mode = RawCamera::CAMERA_MODE_PERSPECTIVE; + camera.perspective.aspectRatio = aspectRatio; + camera.perspective.fovDegreesX = fovDegreesX; + camera.perspective.fovDegreesY = fovDegreesY; + camera.perspective.nearZ = nearZ; + camera.perspective.farZ = farZ; + cameras.emplace_back(camera); + return (int) cameras.size() - 1; +} + +int RawModel::AddCameraOrthographic( + const char *name, const char *nodeName, const float magX, const float magY, const float nearZ, const float farZ) +{ + RawCamera camera; + camera.name = name; + camera.nodeName = nodeName; + camera.mode = RawCamera::CAMERA_MODE_ORTHOGRAPHIC; + camera.orthographic.magX = magX; + camera.orthographic.magY = magY; + camera.orthographic.nearZ = nearZ; + camera.orthographic.farZ = farZ; + cameras.emplace_back(camera); + return (int) cameras.size() - 1; +} + +int RawModel::AddNode(const char *name, const char *parentName) +{ + assert(name[0] != '\0'); + + for (size_t i = 0; i < nodes.size(); i++) { + if (Gltf::StringUtils::CompareNoCase(nodes[i].name, name) == 0) { + return (int) i; + } + } + + RawNode joint; + joint.isJoint = false; + joint.name = name; + joint.parentName = parentName; + joint.translation = Vec3f(0, 0, 0); + joint.rotation = Quatf(0, 0, 0, 1); + joint.scale = Vec3f(1, 1, 1); + + nodes.emplace_back(joint); + return (int) nodes.size() - 1; +} + +void RawModel::Condense() +{ + // Only keep surfaces that are referenced by one or more triangles. + { + std::vector oldSurfaces = surfaces; + + surfaces.clear(); + + for (auto &triangle : triangles) { + const RawSurface &surface = oldSurfaces[triangle.surfaceIndex]; + const int surfaceIndex = AddSurface(surface.name.c_str(), surface.nodeName.c_str()); + surfaces[surfaceIndex] = surface; + triangle.surfaceIndex = surfaceIndex; + } + } + + // Only keep materials that are referenced by one or more triangles. + { + std::vector oldMaterials = materials; + + materials.clear(); + + for (auto &triangle : triangles) { + const RawMaterial &material = oldMaterials[triangle.materialIndex]; + const int materialIndex = AddMaterial(material); + materials[materialIndex] = material; + triangle.materialIndex = materialIndex; + } + } + + // Only keep textures that are referenced by one or more materials. + { + std::vector oldTextures = textures; + + textures.clear(); + + for (auto &material : materials) { + for (int j = 0; j < RAW_TEXTURE_USAGE_MAX; j++) { + if (material.textures[j] >= 0) { + const RawTexture &texture = oldTextures[material.textures[j]]; + const int textureIndex = AddTexture(texture.name.c_str(), texture.fileName.c_str(), texture.usage); + textures[textureIndex] = texture; + material.textures[j] = textureIndex; + } + } + } + } + + // Only keep vertices that are referenced by one or more triangles. + { + std::vector oldVertices = vertices; + + vertexHash.clear(); + vertices.clear(); + + for (auto &triangle : triangles) { + for (int j = 0; j < 3; j++) { + triangle.verts[j] = AddVertex(oldVertices[triangle.verts[j]]); + } + } + } +} + +void RawModel::TransformTextures(const Mat2f &transform) +{ + for (size_t i = 0; i < vertices.size(); i++) { + if ((vertexAttributes & RAW_VERTEX_ATTRIBUTE_UV0) != 0) { + vertices[i].uv0 = transform * vertices[i].uv0; + } + if ((vertexAttributes & RAW_VERTEX_ATTRIBUTE_UV1) != 0) { + vertices[i].uv1 = transform * vertices[i].uv1; + } + } +} + +struct TriangleModelSortPos +{ + static bool Compare(const RawTriangle &a, const RawTriangle &b) + { + if (a.materialIndex != b.materialIndex) { + return a.materialIndex < b.materialIndex; + } + if (a.surfaceIndex != b.surfaceIndex) { + return a.surfaceIndex < b.surfaceIndex; + } + return a.verts[0] < b.verts[0]; + } +}; + +struct TriangleModelSortNeg +{ + static bool Compare(const RawTriangle &a, const RawTriangle &b) + { + if (a.materialIndex != b.materialIndex) { + return a.materialIndex < b.materialIndex; + } + if (a.surfaceIndex != b.surfaceIndex) { + return a.surfaceIndex < b.surfaceIndex; + } + return a.verts[0] > b.verts[0]; + } +}; + +void RawModel::CreateMaterialModels( + std::vector &materialModels, const int maxModelVertices, const int keepAttribs, const bool forceDiscrete) const +{ + // Sort all triangles based on material first, then surface, then first vertex index. + std::vector sortedTriangles; + + bool invertedTransparencySort = true; + if (invertedTransparencySort) { + // Split the triangles into opaque and transparent triangles. + std::vector opaqueTriangles; + std::vector transparentTriangles; + for (const auto &triangle : triangles) { + const int materialIndex = triangle.materialIndex; + if (materialIndex < 0) { + opaqueTriangles.push_back(triangle); + continue; + } + const int textureIndex = materials[materialIndex].textures[RAW_TEXTURE_USAGE_DIFFUSE]; + if (textureIndex < 0) { + if (vertices[triangle.verts[0]].color.w < 1.0f || + vertices[triangle.verts[1]].color.w < 1.0f || + vertices[triangle.verts[2]].color.w < 1.0f) { + transparentTriangles.push_back(triangle); + continue; + } + opaqueTriangles.push_back(triangle); + continue; + } + if (textures[textureIndex].occlusion == RAW_TEXTURE_OCCLUSION_TRANSPARENT) { + transparentTriangles.push_back(triangle); + } else { + opaqueTriangles.push_back(triangle); + } + } + + // Sort the opaque triangles. + std::sort(opaqueTriangles.begin(), opaqueTriangles.end(), TriangleModelSortPos::Compare); + + // Sort the transparent triangles in the reverse direction. + std::sort(transparentTriangles.begin(), transparentTriangles.end(), TriangleModelSortNeg::Compare); + + for (const auto &transparentTriangle : transparentTriangles) { + sortedTriangles.push_back(transparentTriangle); + } + // Add the triangles to the sorted list. + for (const auto &opaqueTriangle : opaqueTriangles) { + sortedTriangles.push_back(opaqueTriangle); + } + } else { + sortedTriangles = triangles; + std::sort(sortedTriangles.begin(), sortedTriangles.end(), TriangleModelSortPos::Compare); + } + + // Overestimate the number of models that will be created to avoid massive reallocation. + int discreteCount = 0; + for (const auto &surface : surfaces) { + discreteCount += (surface.discrete != false); + } + + materialModels.clear(); + materialModels.reserve(materials.size() + discreteCount); + + const RawVertex defaultVertex; + + // Create a separate model for each material. + RawModel *model; + for (size_t i = 0; i < sortedTriangles.size(); i++) { + + if (sortedTriangles[i].materialIndex < 0 || sortedTriangles[i].surfaceIndex < 0) { + continue; + } + + if (i == 0 || + model->GetVertexCount() > maxModelVertices - 3 || + sortedTriangles[i].materialIndex != sortedTriangles[i - 1].materialIndex || + (sortedTriangles[i].surfaceIndex != sortedTriangles[i - 1].surfaceIndex && + (forceDiscrete || surfaces[sortedTriangles[i].surfaceIndex].discrete || + surfaces[sortedTriangles[i - 1].surfaceIndex].discrete))) { + materialModels.resize(materialModels.size() + 1); + model = &materialModels[materialModels.size() - 1]; + } + + // FIXME: will have to unlink from the nodes, transform both surfaces into a + // common space, and reparent to a new node with appropriate transform. + + const int prevSurfaceCount = model->GetSurfaceCount(); + const int materialIndex = model->AddMaterial(materials[sortedTriangles[i].materialIndex]); + const int surfaceIndex = model->AddSurface(surfaces[sortedTriangles[i].surfaceIndex]); + RawSurface &rawSurface = model->GetSurface(surfaceIndex); + + if (model->GetSurfaceCount() > prevSurfaceCount) { + const std::vector &jointNames = surfaces[sortedTriangles[i].surfaceIndex].jointNames; + for (const auto &jointName : jointNames) { + const int nodeIndex = GetNodeByName(jointName.c_str()); + assert(nodeIndex != -1); + model->AddNode(GetNode(nodeIndex)); + } + rawSurface.bounds.Clear(); + } + + int verts[3]; + for (int j = 0; j < 3; j++) { + RawVertex vertex = vertices[sortedTriangles[i].verts[j]]; + + if (keepAttribs != -1) { + int keep = keepAttribs; + if ((keepAttribs & RAW_VERTEX_ATTRIBUTE_POSITION) != 0) { + keep |= RAW_VERTEX_ATTRIBUTE_JOINT_INDICES | RAW_VERTEX_ATTRIBUTE_JOINT_WEIGHTS; + } + if ((keepAttribs & RAW_VERTEX_ATTRIBUTE_AUTO) != 0) { + keep |= RAW_VERTEX_ATTRIBUTE_POSITION; + + const RawMaterial &mat = model->GetMaterial(materialIndex); + if (mat.textures[RAW_TEXTURE_USAGE_DIFFUSE] != -1) { + keep |= RAW_VERTEX_ATTRIBUTE_UV0; + } + if (mat.textures[RAW_TEXTURE_USAGE_NORMAL] != -1) { + keep |= RAW_VERTEX_ATTRIBUTE_NORMAL | + RAW_VERTEX_ATTRIBUTE_TANGENT | + RAW_VERTEX_ATTRIBUTE_BINORMAL | + RAW_VERTEX_ATTRIBUTE_UV0; + } + if (mat.textures[RAW_TEXTURE_USAGE_SPECULAR] != -1) { + keep |= RAW_VERTEX_ATTRIBUTE_NORMAL | + RAW_VERTEX_ATTRIBUTE_UV0; + } + if (mat.textures[RAW_TEXTURE_USAGE_EMISSIVE] != -1) { + keep |= RAW_VERTEX_ATTRIBUTE_UV1; + } + } + if ((keep & RAW_VERTEX_ATTRIBUTE_POSITION) == 0) { vertex.position = defaultVertex.position; } + if ((keep & RAW_VERTEX_ATTRIBUTE_NORMAL) == 0) { vertex.normal = defaultVertex.normal; } + if ((keep & RAW_VERTEX_ATTRIBUTE_TANGENT) == 0) { vertex.tangent = defaultVertex.tangent; } + if ((keep & RAW_VERTEX_ATTRIBUTE_BINORMAL) == 0) { vertex.binormal = defaultVertex.binormal; } + if ((keep & RAW_VERTEX_ATTRIBUTE_COLOR) == 0) { vertex.color = defaultVertex.color; } + if ((keep & RAW_VERTEX_ATTRIBUTE_UV0) == 0) { vertex.uv0 = defaultVertex.uv0; } + if ((keep & RAW_VERTEX_ATTRIBUTE_UV1) == 0) { vertex.uv1 = defaultVertex.uv1; } + if ((keep & RAW_VERTEX_ATTRIBUTE_JOINT_INDICES) == 0) { vertex.jointIndices = defaultVertex.jointIndices; } + if ((keep & RAW_VERTEX_ATTRIBUTE_JOINT_WEIGHTS) == 0) { vertex.jointWeights = defaultVertex.jointWeights; } + } + + verts[j] = model->AddVertex(vertex); + model->vertexAttributes |= vertex.Difference(defaultVertex); + + rawSurface.bounds.AddPoint(vertex.position); + } + + model->AddTriangle(verts[0], verts[1], verts[2], materialIndex, surfaceIndex); + } +} + +int RawModel::GetNodeByName(const char *name) const +{ + for (size_t i = 0; i < nodes.size(); i++) { + if (nodes[i].name == name) { + return (int) i; + } + } + return -1; +} diff --git a/src/RawModel.h b/src/RawModel.h new file mode 100644 index 0000000..83bf4fd --- /dev/null +++ b/src/RawModel.h @@ -0,0 +1,332 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef __RAWMODEL_H__ +#define __RAWMODEL_H__ + +#include + +enum RawVertexAttribute +{ + RAW_VERTEX_ATTRIBUTE_POSITION = 1 << 0, + RAW_VERTEX_ATTRIBUTE_NORMAL = 1 << 1, + RAW_VERTEX_ATTRIBUTE_TANGENT = 1 << 2, + RAW_VERTEX_ATTRIBUTE_BINORMAL = 1 << 3, + RAW_VERTEX_ATTRIBUTE_COLOR = 1 << 4, + RAW_VERTEX_ATTRIBUTE_UV0 = 1 << 5, + RAW_VERTEX_ATTRIBUTE_UV1 = 1 << 6, + RAW_VERTEX_ATTRIBUTE_JOINT_INDICES = 1 << 7, + RAW_VERTEX_ATTRIBUTE_JOINT_WEIGHTS = 1 << 8, + + RAW_VERTEX_ATTRIBUTE_AUTO = 1 << 31 +}; + +struct RawVertex +{ + RawVertex() : + polarityUv0(false), + pad1(false), + pad2(false), + pad3(false) {} + + Vec3f position { 0.0f }; + Vec3f normal { 0.0f }; + Vec3f binormal { 0.0f }; + Vec4f tangent { 0.0f }; + Vec4f color { 0.0f }; + Vec2f uv0 { 0.0f }; + Vec2f uv1 { 0.0f }; + Vec4i jointIndices { 0, 0, 0, 0 }; + Vec4f jointWeights { 0.0f }; + + bool polarityUv0; + bool pad1; + bool pad2; + bool pad3; + + bool operator==(const RawVertex &other) const; + size_t Difference(const RawVertex &other) const; +}; + +class VertexHasher +{ +public: + size_t operator()(const RawVertex &v) const + { + size_t seed = 5381; + const auto hasher = std::hash{}; + seed ^= hasher(v.position[0]) + 0x9e3779b9 + (seed<<6) + (seed>>2); + seed ^= hasher(v.position[1]) + 0x9e3779b9 + (seed<<6) + (seed>>2); + seed ^= hasher(v.position[2]) + 0x9e3779b9 + (seed<<6) + (seed>>2); + return seed; + } +}; + +struct RawTriangle +{ + int verts[3]; + int materialIndex; + int surfaceIndex; +}; + +enum RawTextureUsage +{ + RAW_TEXTURE_USAGE_AMBIENT, + RAW_TEXTURE_USAGE_DIFFUSE, + RAW_TEXTURE_USAGE_NORMAL, + RAW_TEXTURE_USAGE_SPECULAR, + RAW_TEXTURE_USAGE_SHININESS, + RAW_TEXTURE_USAGE_EMISSIVE, + RAW_TEXTURE_USAGE_REFLECTION, + RAW_TEXTURE_USAGE_MAX +}; + +inline std::string DescribeTextureUsage(int usage) +{ + if (usage < 0) { + return ""; + } + switch (static_cast(usage)) { + case RAW_TEXTURE_USAGE_AMBIENT: + return "ambient"; + case RAW_TEXTURE_USAGE_DIFFUSE: + return "diffuse"; + case RAW_TEXTURE_USAGE_NORMAL: + return "normal"; + case RAW_TEXTURE_USAGE_SPECULAR: + return "specuar"; + case RAW_TEXTURE_USAGE_SHININESS: + return "shininess"; + case RAW_TEXTURE_USAGE_EMISSIVE: + return "emissive"; + case RAW_TEXTURE_USAGE_REFLECTION: + return "reflection"; + case RAW_TEXTURE_USAGE_MAX: + default: + return "unknown"; + } +}; + +enum RawTextureOcclusion +{ + RAW_TEXTURE_OCCLUSION_OPAQUE, + RAW_TEXTURE_OCCLUSION_TRANSPARENT +}; + +struct RawTexture +{ + std::string name; + int width; + int height; + int mipLevels; + RawTextureUsage usage; + RawTextureOcclusion occlusion; + std::string fileName; +}; + +enum RawMaterialType +{ + RAW_MATERIAL_TYPE_OPAQUE, + RAW_MATERIAL_TYPE_TRANSPARENT, + RAW_MATERIAL_TYPE_VERTEX_COLORED, + RAW_MATERIAL_TYPE_SKINNED_OPAQUE, + RAW_MATERIAL_TYPE_SKINNED_TRANSPARENT, + RAW_MATERIAL_TYPE_SKINNED_VERTEX_COLORED +}; + +struct RawMaterial +{ + + std::string name; + std::string shadingModel; // typically "Surface", "Anisotropic", "Blinn", "Lambert", "Phong", "Phone E" + RawMaterialType type; + Vec3f ambientFactor; + Vec4f diffuseFactor; + Vec3f specularFactor; + Vec3f emissiveFactor; + float shininess; + int textures[RAW_TEXTURE_USAGE_MAX]; +}; + +struct RawSurface +{ + std::string name; // The name of this surface + std::string nodeName; // The node that links to this surface. + std::string skeletonRootName; // The name of the root of the skeleton. + Bounds bounds; + std::vector jointNames; + std::vector jointGeometryMins; + std::vector jointGeometryMaxs; + std::vector inverseBindMatrices; + bool discrete; + bool skinRigid; +}; + +struct RawChannel +{ + int nodeIndex; + std::vector translations; + std::vector rotations; + std::vector scales; +}; + +struct RawAnimation +{ + std::string name; + std::vector times; + std::vector channels; +}; + +struct RawCamera +{ + std::string name; + std::string nodeName; + + enum + { + CAMERA_MODE_PERSPECTIVE, + CAMERA_MODE_ORTHOGRAPHIC + } mode; + + struct + { + float aspectRatio; + float fovDegreesX; + float fovDegreesY; + float nearZ; + float farZ; + } perspective; + + struct + { + float magX; + float magY; + float nearZ; + float farZ; + } orthographic; +}; + +struct RawNode +{ + bool isJoint; + std::string name; + std::string parentName; + std::vector childNames; + Vec3f translation; + Quatf rotation; + Vec3f scale; +}; + +class RawModel +{ +public: + RawModel(); + + // Add geometry. + void AddVertexAttribute(const RawVertexAttribute attrib); + int AddVertex(const RawVertex &vertex); + int AddTriangle(const int v0, const int v1, const int v2, const int materialIndex, const int surfaceIndex); + int AddTexture(const char *name, const char *fileName, const RawTextureUsage usage); + int AddMaterial(const RawMaterial &material); + int AddMaterial( + const char *name, const char *shadingModel, RawMaterialType materialType, + const int textures[RAW_TEXTURE_USAGE_MAX], Vec3f ambientFactor, + Vec4f diffuseFactor, Vec3f specularFactor, + Vec3f emissiveFactor, float shinineness); + int AddSurface(const RawSurface &suface); + int AddSurface(const char *name, const char *nodeName); + int AddAnimation(const RawAnimation &animation); + int AddCameraPerspective( + const char *name, const char *nodeName, const float aspectRatio, const float fovDegreesX, const float fovDegreesY, + const float nearZ, const float farZ); + int + AddCameraOrthographic(const char *name, const char *nodeName, const float magX, const float magY, const float nearZ, const float farZ); + int AddNode(const RawNode &node); + int AddNode(const char *name, const char *parentName); + void SetRootNode(const char *name) { rootNodeName = name; } + const char *GetRootNode() const { return rootNodeName.c_str(); } + + // Remove unused vertices, textures or materials after removing vertex attributes, textures, materials or surfaces. + void Condense(); + + void TransformTextures(const Mat2f &transform); + + // Get the attributes stored per vertex. + int GetVertexAttributes() const { return vertexAttributes; } + + // Iterate over the vertices. + int GetVertexCount() const { return (int) vertices.size(); } + const RawVertex &GetVertex(const int index) const { return vertices[index]; } + + // Iterate over the triangles. + int GetTriangleCount() const { return (int) triangles.size(); } + const RawTriangle &GetTriangle(const int index) const { return triangles[index]; } + + // Iterate over the textures. + int GetTextureCount() const { return (int) textures.size(); } + const RawTexture &GetTexture(const int index) const { return textures[index]; } + + // Iterate over the materials. + int GetMaterialCount() const { return (int) materials.size(); } + const RawMaterial &GetMaterial(const int index) const { return materials[index]; } + + // Iterate over the surfaces. + int GetSurfaceCount() const { return (int) surfaces.size(); } + const RawSurface &GetSurface(const int index) const { return surfaces[index]; } + RawSurface &GetSurface(const int index) { return surfaces[index]; } + + // Iterate over the animations. + int GetAnimationCount() const { return (int) animations.size(); } + const RawAnimation &GetAnimation(const int index) const { return animations[index]; } + + // Iterate over the cameras. + int GetCameraCount() const { return (int) cameras.size(); } + const RawCamera &GetCamera(const int index) const { return cameras[index]; } + + // Iterate over the nodes. + int GetNodeCount() const { return (int) nodes.size(); } + const RawNode &GetNode(const int index) const { return nodes[index]; } + RawNode &GetNode(const int index) { return nodes[index]; } + int GetNodeByName(const char *name) const; + + // Create individual attribute arrays. + // Returns true if the vertices store the particular attribute. + template + void GetAttributeArray(std::vector<_attrib_type_> &out, const _attrib_type_ RawVertex::* ptr) const; + + // Create an array with a raw model for each material. + // Multiple surfaces with the same material will turn into a single model. + // However, surfaces that are marked as 'discrete' will turn into separate models. + void CreateMaterialModels( + std::vector &materialModels, const int maxModelVertices, const int keepAttribs, const bool forceDiscrete) const; + +private: + std::string rootNodeName; + int vertexAttributes; + std::unordered_map vertexHash; + std::vector vertices; + std::vector triangles; + std::vector textures; + std::vector materials; + std::vector surfaces; + std::vector animations; + std::vector cameras; + std::vector nodes; +}; + +template +void RawModel::GetAttributeArray(std::vector<_attrib_type_> &out, const _attrib_type_ RawVertex::* ptr) const +{ + out.resize(vertices.size()); + for (size_t i = 0; i < vertices.size(); i++) { + out[i] = vertices[i].*ptr; + } +} + +#endif // !__RAWMODEL_H__ diff --git a/src/glTF/AccessorData.cpp b/src/glTF/AccessorData.cpp new file mode 100644 index 0000000..1f07650 --- /dev/null +++ b/src/glTF/AccessorData.cpp @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "AccessorData.h" +#include "BufferViewData.h" + +AccessorData::AccessorData(const BufferViewData &bufferView, GLType type) + : Holdable(), + bufferView(bufferView.ix), + type(std::move(type)), + byteOffset(0), + count(0) +{ +} + +AccessorData::AccessorData(GLType type) + : Holdable(), + bufferView(-1), + type(std::move(type)), + byteOffset(0), + count(0) +{ +} + +json AccessorData::serialize() const +{ + json result { + { "componentType", type.componentType.glType }, + { "type", type.dataType }, + { "count", count } + }; + if (bufferView >= 0) { + result["bufferView"] = bufferView; + result["byteOffset"] = byteOffset; + } + if (!min.empty()) { + result["min"] = min; + } + if (!max.empty()) { + result["max"] = max; + } + return result; +} diff --git a/src/glTF/AccessorData.h b/src/glTF/AccessorData.h new file mode 100644 index 0000000..86f8f86 --- /dev/null +++ b/src/glTF/AccessorData.h @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef FBX2GLTF_ACCESSORDATA_H +#define FBX2GLTF_ACCESSORDATA_H + +#include "Raw2Gltf.h" + +struct AccessorData : Holdable +{ + AccessorData(const BufferViewData &bufferView, GLType type); + explicit AccessorData(GLType type); + + json serialize() const override; + + template + void appendAsBinaryArray(const std::vector &in, std::vector &out) + { + const unsigned int stride = type.byteStride(); + const size_t offset = out.size(); + const size_t count = in.size(); + + this->count = (unsigned int) count; + + out.resize(offset + count * stride); + for (int ii = 0; ii < count; ii ++) { + type.write(&out[offset + ii * stride], in[ii]); + } + } + + unsigned int byteLength() const { return type.byteStride() * count; } + + const int bufferView; + const GLType type; + + unsigned int byteOffset; + unsigned int count; + std::vector min; + std::vector max; +}; + +#endif //FBX2GLTF_ACCESSORDATA_H diff --git a/src/glTF/AnimationData.cpp b/src/glTF/AnimationData.cpp new file mode 100644 index 0000000..60c2f35 --- /dev/null +++ b/src/glTF/AnimationData.cpp @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "AnimationData.h" + +#include + +#include "AccessorData.h" +#include "NodeData.h" + +AnimationData::AnimationData(std::string name, const AccessorData &timeAccessor) + : Holdable(), + name(std::move(name)), + timeAccessor(timeAccessor.ix) {} + +// assumption: 1-to-1 relationship between channels and samplers; this is a simplification on what +// glTF can express, but it means we can rely on samplerIx == channelIx throughout an animation +void AnimationData::AddNodeChannel(const NodeData &node, const AccessorData &accessor, std::string path) +{ + assert(channels.size() == samplers.size()); + uint32_t ix = channels.size(); + channels.emplace_back(channel_t(ix, node, std::move(path))); + samplers.emplace_back(sampler_t(timeAccessor, accessor.ix)); +} + +json AnimationData::serialize() const +{ + return { + { "name", name }, + { "channels", channels }, + { "samplers", samplers } + }; +} + +AnimationData::channel_t::channel_t(uint32_t ix, const NodeData &node, std::string path) + : ix(ix), + node(node.ix), + path(std::move(path)) +{ +} + +AnimationData::sampler_t::sampler_t(uint32_t time, uint32_t output) + : time(time), + output(output) +{ +} + +void to_json(json &j, const AnimationData::channel_t &data) { + j = json { + { "sampler", data.ix }, + { "target", { + { "node", data.node }, + { "path", data.path }}, + } + }; +} + +void to_json(json &j, const AnimationData::sampler_t &data) { + j = json { + { "input", data.time }, + { "interpolation", "LINEAR" }, + { "output", data.output }, + }; +} diff --git a/src/glTF/AnimationData.h b/src/glTF/AnimationData.h new file mode 100644 index 0000000..cf434e7 --- /dev/null +++ b/src/glTF/AnimationData.h @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef FBX2GLTF_ANIMATIONDATA_H +#define FBX2GLTF_ANIMATIONDATA_H + +#include "Raw2Gltf.h" + +struct AnimationData : Holdable +{ + AnimationData(std::string name, const AccessorData &timeAccessor); + + // assumption: 1-to-1 relationship between channels and samplers; this is a simplification on what + // glTF can express, but it means we can rely on samplerIx == channelIx throughout an animation + void AddNodeChannel(const NodeData &node, const AccessorData &accessor, std::string path); + + json serialize() const override; + + struct channel_t + { + channel_t(uint32_t _ix, const NodeData &node, std::string path); + + const uint32_t ix; + const uint32_t node; + const std::string path; + }; + + struct sampler_t + { + sampler_t(uint32_t time, uint32_t output); + + const uint32_t time; + const uint32_t output; + }; + + const std::string name; + const uint32_t timeAccessor; + std::vector channels; + std::vector samplers; +}; + +void to_json(json &j, const AnimationData::channel_t &data); +void to_json(json &j, const AnimationData::sampler_t &data); + +#endif //FBX2GLTF_ANIMATIONDATA_H diff --git a/src/glTF/BufferData.cpp b/src/glTF/BufferData.cpp new file mode 100644 index 0000000..bac4656 --- /dev/null +++ b/src/glTF/BufferData.cpp @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include + +#include "BufferData.h" + +BufferData::BufferData(const std::shared_ptr > &binData) + : Holdable(), + isGlb(true), + binData(binData) +{ +} + +BufferData::BufferData(std::string uri, const std::shared_ptr > &binData, bool isEmbedded) + : Holdable(), + isGlb(false), + uri(isEmbedded ? "" : std::move(uri)), + binData(binData) +{ +} + +json BufferData::serialize() const +{ + json result{ + {"byteLength", binData->size()} + }; + if (!isGlb) { + if (!uri.empty()) { + result["uri"] = uri; + } else { + std::string encoded = base64::encode(*binData); + result["uri"] = "data:application/octet-stream;base64," + encoded; + } + } + return result; +} diff --git a/src/glTF/BufferData.h b/src/glTF/BufferData.h new file mode 100644 index 0000000..d46baa0 --- /dev/null +++ b/src/glTF/BufferData.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef FBX2GLTF_BUFFERDATA_H +#define FBX2GLTF_BUFFERDATA_H + +#include "Raw2Gltf.h" + +struct BufferData : Holdable +{ + explicit BufferData(const std::shared_ptr > &binData); + + BufferData(std::string uri, const std::shared_ptr > &binData, bool isEmbedded = false); + + json serialize() const override; + + const bool isGlb; + const std::string uri; + const std::shared_ptr > binData; // TODO this is just weird +}; + + +#endif //FBX2GLTF_BUFFERDATA_H diff --git a/src/glTF/BufferViewData.cpp b/src/glTF/BufferViewData.cpp new file mode 100644 index 0000000..c795bc8 --- /dev/null +++ b/src/glTF/BufferViewData.cpp @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "BufferViewData.h" +#include "BufferData.h" + +BufferViewData::BufferViewData(const BufferData &_buffer, const size_t _byteOffset, const GL_ArrayType _target) + : Holdable(), + buffer(_buffer.ix), + byteOffset((unsigned int) _byteOffset), + target(_target) +{ +} + +json BufferViewData::serialize() const +{ + json result { + { "buffer", buffer }, + { "byteLength", byteLength }, + { "byteOffset", byteOffset } + }; + if (target != GL_ARRAY_NONE) { + result["target"] = target; + } + return result; +} diff --git a/src/glTF/BufferViewData.h b/src/glTF/BufferViewData.h new file mode 100644 index 0000000..5f60300 --- /dev/null +++ b/src/glTF/BufferViewData.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef FBX2GLTF_BUFFERVIEW_H +#define FBX2GLTF_BUFFERVIEW_H + +#include "Raw2Gltf.h" + +struct BufferViewData : Holdable +{ + enum GL_ArrayType + { + GL_ARRAY_NONE = 0, // no GL buffer is being set + GL_ARRAY_BUFFER = 34962, + GL_ELEMENT_ARRAY_BUFFER = 34963 + }; + + BufferViewData(const BufferData &_buffer, const size_t _byteOffset, const GL_ArrayType _target); + + json serialize() const override; + + const unsigned int buffer; + const unsigned int byteOffset; + const GL_ArrayType target; + + unsigned int byteLength = 0; +}; + +#endif //FBX2GLTF_BUFFERVIEW_H diff --git a/src/glTF/CameraData.cpp b/src/glTF/CameraData.cpp new file mode 100644 index 0000000..2dc7220 --- /dev/null +++ b/src/glTF/CameraData.cpp @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "CameraData.h" + +CameraData::CameraData() + : Holdable(), + aspectRatio(0.0f), + yfov(0.0f), + xmag(0.0f), + ymag(0.0f), + znear(0.0f), + zfar(0.0f) +{ +} + +json CameraData::serialize() const +{ + json result { + { "name", name }, + { "type", type }, + }; + json subResult { + { "znear", znear }, + { "zfar", zfar } + }; + if (type == "perspective") { + subResult["aspectRatio"] = aspectRatio; + subResult["yfov"] = yfov; + } else { + subResult["xmag"] = xmag; + subResult["ymag"] = ymag; + } + result[type] = subResult; + return result; +} diff --git a/src/glTF/CameraData.h b/src/glTF/CameraData.h new file mode 100644 index 0000000..bafe600 --- /dev/null +++ b/src/glTF/CameraData.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef FBX2GLTF_CAMERADATA_H +#define FBX2GLTF_CAMERADATA_H + +#include "Raw2Gltf.h" + +// TODO: this class needs some work +struct CameraData : Holdable +{ + CameraData(); + json serialize() const override; + + std::string name; + std::string type; + float aspectRatio; + float yfov; + float xmag; + float ymag; + float znear; + float zfar; +}; + +#endif //FBX2GLTF_CAMERADATA_H diff --git a/src/glTF/ImageData.cpp b/src/glTF/ImageData.cpp new file mode 100644 index 0000000..98a7555 --- /dev/null +++ b/src/glTF/ImageData.cpp @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "ImageData.h" + +#include + +#include "BufferViewData.h" + +ImageData::ImageData(std::string name, std::string uri) + : Holdable(), + name(std::move(name)), + uri(std::move(uri)), + bufferView(-1) +{ +} + +ImageData::ImageData(std::string name, const BufferViewData &bufferView, std::string mimeType) + : Holdable(), + name(std::move(name)), + bufferView(bufferView.ix), + mimeType(std::move(mimeType)) +{ +} + +json ImageData::serialize() const +{ + if (mimeType.empty()) { + return { + { "uri", uri } + }; + } + return { + { "name", name }, + { "bufferView", bufferView }, + { "mimeType", mimeType } + }; +} diff --git a/src/glTF/ImageData.h b/src/glTF/ImageData.h new file mode 100644 index 0000000..0be181a --- /dev/null +++ b/src/glTF/ImageData.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef FBX2GLTF_IMAGEDATA_H +#define FBX2GLTF_IMAGEDATA_H + +#include "Raw2Gltf.h" + +struct ImageData : Holdable +{ + ImageData(std::string name, std::string uri); + ImageData(std::string name, const BufferViewData &bufferView, std::string mimeType); + + json serialize() const override; + + const std::string name; + const std::string uri; // non-empty in gltf mode + const int32_t bufferView; // non-negative in glb mode + const std::string mimeType; +}; + +#endif //FBX2GLTF_IMAGEDATA_H diff --git a/src/glTF/MaterialData.cpp b/src/glTF/MaterialData.cpp new file mode 100644 index 0000000..2c7a2ca --- /dev/null +++ b/src/glTF/MaterialData.cpp @@ -0,0 +1,201 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "MaterialData.h" +#include "TextureData.h" + +// TODO: retrieve & pass in correct UV set from FBX +std::unique_ptr Tex::ref(const TextureData *tex, uint32_t texCoord) +{ + return std::unique_ptr { (tex != nullptr) ? new Tex(tex->ix, texCoord) : nullptr }; +} + +Tex::Tex(uint32_t texRef, uint32_t texCoord) + : texRef(texRef), + texCoord(texCoord) {} + +void to_json(json &j, const Tex &data) { + j = json { + { "index", data.texRef }, + { "texCoord", data.texCoord } + }; +} + +static inline Vec4f toRGBA(const Vec3f &colour, float alpha = 1) { + return { colour[0], colour[1], colour[2], alpha }; +}; + +KHRCommonMats::KHRCommonMats( + MaterialType type, + const TextureData *shininessTexture, float shininess, + const TextureData *ambientTexture, const Vec3f &ambientFactor, + const TextureData *diffuseTexture, const Vec4f &diffuseFactor, + const TextureData *specularTexture, const Vec3f &specularFactor) + : type(type), + shininessTexture(Tex::ref(shininessTexture)), + shininess(shininess), + ambientTexture(Tex::ref(ambientTexture)), + ambientFactor(ambientFactor), + diffuseTexture(Tex::ref(diffuseTexture)), + diffuseFactor(diffuseFactor), + specularTexture(Tex::ref(specularTexture)), + specularFactor(specularFactor) +{ +} + +std::string KHRCommonMats::typeDesc(MaterialType type) +{ + switch (type) { + case Blinn: + return "commonBlinn"; + case Phong: + return "commonPhong"; + case Lambert: + return "commonLambert"; + case Constant: + return "commonConstant"; + } +} + +void to_json(json &j, const KHRCommonMats &d) +{ + j = {{"type", KHRCommonMats::typeDesc(d.type)}}; + + if (d.shininessTexture != nullptr) { + j["shininessTexture"] = *d.shininessTexture; + } + if (d.shininess != 0) { + j["shininess"] = d.shininess; + } + if (d.ambientTexture != nullptr) { + j["ambientTexture"] = *d.ambientTexture; + } + if (d.ambientFactor.LengthSquared() > 0) { + j["ambientFactor"] = toStdVec(toRGBA(d.ambientFactor)); + } + if (d.diffuseTexture != nullptr) { + j["diffuseTexture"] = *d.diffuseTexture; + } + if (d.diffuseFactor.LengthSquared() > 0) { + j["diffuseFactor"] = toStdVec(d.diffuseFactor); + } + if (d.specularTexture != nullptr) { + j["specularTexture"] = *d.specularTexture; + } + if (d.specularFactor.LengthSquared() > 0) { + j["specularFactor"] = toStdVec(toRGBA(d.specularFactor)); + } +} + +PBRSpecularGlossiness::PBRSpecularGlossiness( + const TextureData *diffuseTexture, const Vec4f &diffuseFactor, + const TextureData *specularGlossinessTexture, const Vec3f &specularFactor, + float glossinessFactor) + : diffuseTexture(Tex::ref(diffuseTexture)), + diffuseFactor(diffuseFactor), + specularGlossinessTexture(Tex::ref(specularGlossinessTexture)), + specularFactor(specularFactor), + glossinessFactor(glossinessFactor) +{ +} + +void to_json(json &j, const PBRSpecularGlossiness &d) +{ + j = {}; + if (d.diffuseTexture != nullptr) { + j["diffuseTexture"] = *d.diffuseTexture; + } + if (d.diffuseFactor.LengthSquared() > 0) { + j["diffuseFactor"] = toStdVec(d.diffuseFactor); + } + if (d.specularGlossinessTexture != nullptr) { + j["specularGlossinessTexture"] = *d.specularGlossinessTexture; + } + if (d.specularFactor.LengthSquared() > 0) { + j["specularFactor"] = toStdVec(d.specularFactor); + } + if (d.glossinessFactor != 0) { + j["glossinessFactor"] = d.glossinessFactor; + } +} + +PBRMetallicRoughness::PBRMetallicRoughness( + const TextureData *baseColorTexture, const Vec4f &baseolorFactor, + float metallic, float roughness) + : baseColorTexture(Tex::ref(baseColorTexture)), + baseColorFactor(baseolorFactor), + metallic(metallic), + roughness(roughness) +{ +} + +void to_json(json &j, const PBRMetallicRoughness &d) +{ + j = { }; + if (d.baseColorTexture != nullptr) { + j["baseColorTexture"] = *d.baseColorTexture; + } + if (d.baseColorFactor.LengthSquared() > 0) { + j["baseColorFactor"] = toStdVec(d.baseColorFactor); + } + if (d.metallic != 1.0f) { + j["metallicFactor"] = d.metallic; + } + if (d.roughness != 1.0f) { + j["roughnessFactor"] = d.roughness; + } +} + +MaterialData::MaterialData( + std::string name, const TextureData *normalTexture, + const TextureData *emissiveTexture, const Vec3f & emissiveFactor, + std::shared_ptr const khrCommonMats, + std::shared_ptr const pbrMetallicRoughness, + std::shared_ptr const pbrSpecularGlossiness) + : Holdable(), + name(std::move(name)), + normalTexture(Tex::ref(normalTexture)), + emissiveTexture(Tex::ref(emissiveTexture)), + emissiveFactor(std::move(emissiveFactor)), + khrCommonMats(khrCommonMats), + pbrMetallicRoughness(pbrMetallicRoughness), + pbrSpecularGlossiness(pbrSpecularGlossiness) {} + +json MaterialData::serialize() const +{ + json result = { + { "name", name }, + { "alphaMode", "BLEND" } + }; + + if (normalTexture != nullptr) { + result["normalTexture"] = *normalTexture; + } + if (emissiveTexture != nullptr) { + result["emissiveTexture"] = *emissiveTexture; + } + if (emissiveFactor.LengthSquared() > 0) { + result["emissiveFactor"] = toStdVec(emissiveFactor); + } + if (pbrMetallicRoughness != nullptr) { + result["pbrMetallicRoughness"] = *pbrMetallicRoughness; + } + if (khrCommonMats != nullptr || pbrSpecularGlossiness != nullptr) { + json extensions = { }; + if (khrCommonMats != nullptr) { + extensions[KHR_MATERIALS_COMMON] = *khrCommonMats; + } + if (pbrSpecularGlossiness != nullptr) { + extensions[KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS] = *pbrSpecularGlossiness; + } + + result["extensions"] = extensions; + } + return result; +} diff --git a/src/glTF/MaterialData.h b/src/glTF/MaterialData.h new file mode 100644 index 0000000..6f701e5 --- /dev/null +++ b/src/glTF/MaterialData.h @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef FBX2GLTF_MATERIALDATA_H +#define FBX2GLTF_MATERIALDATA_H + +#include + +#include "Raw2Gltf.h" + +struct Tex +{ + static std::unique_ptr ref(const TextureData *tex, uint32_t texCoord = 0); + explicit Tex(uint32_t texRef, uint32_t texCoord); + + const uint32_t texRef; + const uint32_t texCoord; +}; + +struct KHRCommonMats +{ + enum MaterialType + { + Blinn, + Phong, + Lambert, + Constant, + }; + + KHRCommonMats( + MaterialType type, + const TextureData *shininessTexture, float shininess, + const TextureData *ambientTexture, const Vec3f &ambientFactor, + const TextureData *diffuseTexture, const Vec4f &diffuseFactor, + const TextureData *specularTexture, const Vec3f &specularFactor); + + static std::string typeDesc(MaterialType type); + + const MaterialType type; + const std::unique_ptr shininessTexture; + const float shininess; + const std::unique_ptr ambientTexture; + const Vec3f ambientFactor; + const std::unique_ptr diffuseTexture; + const Vec4f diffuseFactor; + const std::unique_ptr specularTexture; + const Vec3f specularFactor; +}; + +struct PBRSpecularGlossiness +{ + PBRSpecularGlossiness( + const TextureData *diffuseTexture, const Vec4f &diffuseFactor, + const TextureData *specularGlossinessTexture, + const Vec3f &specularFactor, float glossinessFactor); + + std::unique_ptr diffuseTexture; + const Vec4f diffuseFactor; + std::unique_ptr specularGlossinessTexture; + const Vec3f specularFactor; + const float glossinessFactor; +}; + +struct PBRMetallicRoughness +{ + PBRMetallicRoughness( + const TextureData *baseColorTexture, const Vec4f &baseolorFactor, + float metallic = 0.1f, float roughness = 0.4f); + + std::unique_ptr baseColorTexture; + const Vec4f baseColorFactor; + const float metallic; + const float roughness; +}; + +struct MaterialData : Holdable +{ + MaterialData( + std::string name, const TextureData *normalTexture, + const TextureData *emissiveTexture, const Vec3f &emissiveFactor, + std::shared_ptr const khrCommonMats, + std::shared_ptr const pbrMetallicRoughness, + std::shared_ptr const pbrSpecularGlossiness); + + json serialize() const override; + + const std::string name; + const std::unique_ptr normalTexture; + const std::unique_ptr emissiveTexture; + const Vec3f emissiveFactor; + + const std::shared_ptr khrCommonMats; + const std::shared_ptr pbrMetallicRoughness; + const std::shared_ptr pbrSpecularGlossiness; +}; + +void to_json(json &j, const Tex &data); +void to_json(json &j, const KHRCommonMats &d); +void to_json(json &j, const PBRSpecularGlossiness &d); +void to_json(json &j, const PBRMetallicRoughness &d); + +#endif //FBX2GLTF_MATERIALDATA_H diff --git a/src/glTF/MeshData.cpp b/src/glTF/MeshData.cpp new file mode 100644 index 0000000..e7dae6a --- /dev/null +++ b/src/glTF/MeshData.cpp @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "MeshData.h" +#include "PrimitiveData.h" + +MeshData::MeshData(std::string name) + : Holdable(), + name(std::move(name)) +{ +} + +json MeshData::serialize() const +{ + json jsonPrimitivesArray = json::array(); + for (const auto &primitive : primitives) { + jsonPrimitivesArray.push_back(*primitive); + } + return { + { "name", name }, + { "primitives", jsonPrimitivesArray } + }; +} diff --git a/src/glTF/MeshData.h b/src/glTF/MeshData.h new file mode 100644 index 0000000..993a9bb --- /dev/null +++ b/src/glTF/MeshData.h @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef FBX2GLTF_MESHDATA_H +#define FBX2GLTF_MESHDATA_H + +#include + +#include + +#include "Raw2Gltf.h" + +#include "PrimitiveData.h" + +struct MeshData : Holdable +{ + explicit MeshData(std::string name); + + void AddPrimitive(std::shared_ptr primitive) + { + primitives.push_back(std::move(primitive)); + } + + json serialize() const override; + + const std::string name; + std::vector> primitives; +}; + +#endif //FBX2GLTF_MESHDATA_H diff --git a/src/glTF/NodeData.cpp b/src/glTF/NodeData.cpp new file mode 100644 index 0000000..f4a3efd --- /dev/null +++ b/src/glTF/NodeData.cpp @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "NodeData.h" + +NodeData::NodeData( + std::string name, const Vec3f &translation, + const Quatf &rotation, const Vec3f &scale, bool isJoint) + : Holdable(), + name(std::move(name)), + isJoint(isJoint), + translation(translation), + rotation(rotation), + scale(scale), + children(), + mesh(-1), + cameraName(""), + skin(-1) +{ +} + +void NodeData::AddChildNode(uint32_t childIx) +{ + children.push_back(childIx); +} + +void NodeData::SetMesh(uint32_t meshIx) +{ + assert(mesh < 0); + assert(!isJoint); + mesh = meshIx; +} + +void NodeData::SetSkin(uint32_t skinIx) +{ + assert(skin < 0); + assert(!isJoint); + skin = skinIx; +} + +void NodeData::AddCamera(std::string camera) +{ + assert(!isJoint); + cameraName = std::move(camera); +} + +json NodeData::serialize() const +{ + json result = { + { "name", name }, + { "translation", toStdVec(translation) }, + { "rotation", toStdVec(rotation) }, + { "scale", toStdVec(scale) } + }; + if (!children.empty()) { + result["children"] = children; + } + if (isJoint) { + // sanity-check joint node + assert(mesh < 0 && skin < 0); + } else { + // non-joint node + if (mesh >= 0) { + result["mesh"] = mesh; + } + if (!skeletons.empty()) { + result["skeletons"] = skeletons; + } + if (skin >= 0) { + result["skin"] = skin; + } + if (!cameraName.empty()) { + result["camera"] = cameraName; + } + } + return result; +} diff --git a/src/glTF/NodeData.h b/src/glTF/NodeData.h new file mode 100644 index 0000000..cd78acb --- /dev/null +++ b/src/glTF/NodeData.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef FBX2GLTF_NODEDATA_H +#define FBX2GLTF_NODEDATA_H + +#include "Raw2Gltf.h" + +struct NodeData : Holdable +{ + NodeData(std::string name, const Vec3f &translation, const Quatf &rotation, const Vec3f &scale, bool isJoint); + + void AddChildNode(uint32_t childIx); + void SetMesh(uint32_t meshIx); + void SetSkin(uint32_t skinIx); + void AddCamera(std::string camera); + + json serialize() const override; + + const std::string name; + const bool isJoint; + Vec3f translation; + Quatf rotation; + Vec3f scale; + std::vector children; + int32_t mesh; + std::string cameraName; + int32_t skin; + std::vector skeletons; +}; + +#endif //FBX2GLTF_NODEDATA_H diff --git a/src/glTF/PrimitiveData.cpp b/src/glTF/PrimitiveData.cpp new file mode 100644 index 0000000..9d6b732 --- /dev/null +++ b/src/glTF/PrimitiveData.cpp @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "PrimitiveData.h" + +#include "MaterialData.h" +#include "AccessorData.h" +#include "BufferViewData.h" + +PrimitiveData::PrimitiveData(const AccessorData &indices, const MaterialData &material, std::shared_ptr dracoMesh) + : indices(indices.ix), + material(material.ix), + mode(TRIANGLES), + dracoMesh(dracoMesh), + dracoBufferView(-1) {} + +PrimitiveData::PrimitiveData(const AccessorData &indices, const MaterialData &material) + : indices(indices.ix), + material(material.ix), + mode(TRIANGLES), + dracoMesh(nullptr), + dracoBufferView(-1) +{ +} + +void PrimitiveData::AddAttrib(std::string name, const AccessorData &accessor) +{ + attributes[name] = accessor.ix; +} + +void PrimitiveData::NoteDracoBuffer(const BufferViewData &data) +{ + dracoBufferView = data.ix; +} + +void to_json(json &j, const PrimitiveData &d) { + j = { + { "material", d.material }, + { "mode", d.mode }, + { "attributes", d.attributes } + }; + if (d.indices >= 0) { + j["indices"] = d.indices; + } + + if (!d.dracoAttributes.empty()) { + j["extensions"] = { + { KHR_DRACO_MESH_COMPRESSION, { + { "bufferView", d.dracoBufferView }, + { "attributes", d.dracoAttributes } + }} + }; + } +} diff --git a/src/glTF/PrimitiveData.h b/src/glTF/PrimitiveData.h new file mode 100644 index 0000000..5d6ffb4 --- /dev/null +++ b/src/glTF/PrimitiveData.h @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef FBX2GLTF_PRIMITIVEDATA_H +#define FBX2GLTF_PRIMITIVEDATA_H + +#include "Raw2Gltf.h" + +struct PrimitiveData +{ + enum MeshMode + { + POINTS = 0, + LINES, + LINE_LOOP, + LINE_STRIP, + TRIANGLES, + TRIANGLE_STRIP, + TRIANGLE_FAN + }; + + PrimitiveData(const AccessorData &indices, const MaterialData &material, std::shared_ptr dracoMesh); + + PrimitiveData(const AccessorData &indices, const MaterialData &material); + + void AddAttrib(std::string name, const AccessorData &accessor); + + template + void AddDracoAttrib(const AttributeDefinition attribute, const std::vector &attribArr) + { + draco::PointAttribute att; + int8_t componentCount = attribute.glType.count; + att.Init( + attribute.dracoAttribute, nullptr, componentCount, attribute.dracoComponentType, + false, componentCount * draco::DataTypeLength(attribute.dracoComponentType), 0); + + const int dracoAttId = dracoMesh->AddAttribute(att, true, attribArr.size()); + draco::PointAttribute *attPtr = dracoMesh->attribute(dracoAttId); + + std::vector buf(sizeof(T)); + for (uint32_t ii = 0; ii < attribArr.size(); ii++) { + uint8_t *ptr = &buf[0]; + attribute.glType.write(ptr, attribArr[ii]); + attPtr->SetAttributeValue(attPtr->mapped_index(draco::PointIndex(ii)), ptr); + } + + dracoAttributes[attribute.gltfName] = dracoAttId; + } + + void NoteDracoBuffer(const BufferViewData &data); + + const int indices; + const unsigned int material; + const MeshMode mode; + + std::map attributes; + std::map dracoAttributes; + + std::shared_ptr dracoMesh; + int dracoBufferView; +}; + +void to_json(json &j, const PrimitiveData &d); + +#endif //FBX2GLTF_PRIMITIVEDATA_H diff --git a/src/glTF/SamplerData.h b/src/glTF/SamplerData.h new file mode 100644 index 0000000..7804b98 --- /dev/null +++ b/src/glTF/SamplerData.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef FBX2GLTF_SAMPLERDATA_H +#define FBX2GLTF_SAMPLERDATA_H + +#include "Raw2Gltf.h" + +struct SamplerData : Holdable +{ + // this is where magFilter, minFilter, wrapS and wrapT would go, should we want it + SamplerData() + : Holdable() + { + } + + json serialize() const { + return json::object(); + } +}; + +#endif //FBX2GLTF_SAMPLERDATA_H diff --git a/src/glTF/SceneData.cpp b/src/glTF/SceneData.cpp new file mode 100644 index 0000000..2b21994 --- /dev/null +++ b/src/glTF/SceneData.cpp @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "SceneData.h" + +#include "NodeData.h" + +SceneData::SceneData(std::string name, const NodeData &rootNode) + : Holdable(), + name(std::move(name)), + nodes({rootNode.ix}) +{ +} + +json SceneData::serialize() const +{ + assert(nodes.size() <= 1); + return { + { "name", name }, + { "nodes", nodes } + }; +} diff --git a/src/glTF/SceneData.h b/src/glTF/SceneData.h new file mode 100644 index 0000000..1550eed --- /dev/null +++ b/src/glTF/SceneData.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef FBX2GLTF_SCENEDATA_H +#define FBX2GLTF_SCENEDATA_H + +#include "Raw2Gltf.h" + +struct SceneData : Holdable +{ + SceneData(std::string name, const NodeData &rootNode); + + json serialize() const override; + + const std::string name; + std::vector nodes; +}; + +#endif //FBX2GLTF_SCENEDATA_H diff --git a/src/glTF/SkinData.cpp b/src/glTF/SkinData.cpp new file mode 100644 index 0000000..7d53496 --- /dev/null +++ b/src/glTF/SkinData.cpp @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "SkinData.h" + +#include "AccessorData.h" +#include "NodeData.h" + +SkinData::SkinData( + const std::vector joints, const AccessorData &inverseBindMatricesAccessor, + const NodeData &skeletonRootNode) + : Holdable(), + joints(joints), + inverseBindMatrices(inverseBindMatricesAccessor.ix), + skeletonRootNode(skeletonRootNode.ix) +{ +} + +json SkinData::serialize() const +{ + return { + { "joints", joints }, + { "inverseBindMatrices", inverseBindMatrices }, + { "skeleton", skeletonRootNode } + }; +} diff --git a/src/glTF/SkinData.h b/src/glTF/SkinData.h new file mode 100644 index 0000000..1831a02 --- /dev/null +++ b/src/glTF/SkinData.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef FBX2GLTF_SKINDATA_H +#define FBX2GLTF_SKINDATA_H + +#include "Raw2Gltf.h" + +struct SkinData : Holdable +{ + SkinData( + const std::vector joints, const AccessorData &inverseBindMatricesAccessor, + const NodeData &skeletonRootNode); + + json serialize() const override; + + const std::vector joints; + const uint32_t skeletonRootNode; + const uint32_t inverseBindMatrices; +}; + +#endif //FBX2GLTF_SKINDATA_H diff --git a/src/glTF/TextureData.cpp b/src/glTF/TextureData.cpp new file mode 100644 index 0000000..538f1ab --- /dev/null +++ b/src/glTF/TextureData.cpp @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "TextureData.h" + +#include "ImageData.h" +#include "SamplerData.h" + +TextureData::TextureData(std::string name, const SamplerData &sampler, const ImageData &source) + : Holdable(), + name(std::move(name)), + sampler(sampler.ix), + source(source.ix) +{ +} + +json TextureData::serialize() const +{ + return { + { "name", name }, + { "sampler", sampler }, + { "source", source } + }; +} diff --git a/src/glTF/TextureData.h b/src/glTF/TextureData.h new file mode 100644 index 0000000..1106059 --- /dev/null +++ b/src/glTF/TextureData.h @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef FBX2GLTF_TEXTUREDATA_H +#define FBX2GLTF_TEXTUREDATA_H + +#include "Raw2Gltf.h" + +struct TextureData : Holdable +{ + TextureData(std::string name, const SamplerData &sampler, const ImageData &source); + + json serialize() const override; + + const std::string name; + const uint32_t sampler; + const uint32_t source; +}; + +#endif //FBX2GLTF_TEXTUREDATA_H diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..ef1fb6e --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,249 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include +#include +#include +#include +#include + +#if defined( __unix__ ) || defined( __APPLE__ ) + +#include + +#define _stricmp strcasecmp +#endif + +#include + +#include "FBX2glTF.h" +#include "utils/String_Utils.h" +#include "utils/File_Utils.h" +#include "Fbx2Raw.h" +#include "RawModel.h" +#include "Raw2Gltf.h" + +bool verboseOutput = false; + +int main(int argc, char *argv[]) +{ + cxxopts::Options options( + "FBX2glTF", + "FBX2glTF 2.0: Generate a glTF 2.0 representation of an FBX model."); + + std::string inputPath; + std::string outputPath; + + Mat2f texturesTransform(0.0f); + bool doTransformTextures = false; + + GltfOptions gltfOptions{ + -1, // keepAttribs + false, // outputBinary + false, // embedResources + false, // useDraco + false, // useKHRMatCom + false, // usePBRMetRough + false // usePBRSpecGloss + }; + + options.positional_help("[]"); + options.add_options() + ( + "i,input", "The FBX model to convert.", + cxxopts::value(inputPath)) + ( + "o,output", "Where to generate the output, without suffix.", + cxxopts::value(outputPath)) + ( + "e,embed", "Inline buffers as data:// URIs within generated non-binary glTF.", + cxxopts::value(gltfOptions.embedResources)) + ( + "b,binary", "Output a single binary format .glb file.", + cxxopts::value(gltfOptions.outputBinary)) + ( + "d,draco", "Apply Draco mesh compression to geometries.", + cxxopts::value(gltfOptions.useDraco)) + ("flip-u", "Flip all U texture coordinates.") + ("flip-v", "Flip all V texture coordinates.") + ( + "khr-materials-common", "(WIP) Use KHR_materials_common extensions to specify Unlit/Lambert/Blinn/Phong shaders.", + cxxopts::value(gltfOptions.useKHRMatCom)) + ( + "pbr-metallic-roughness", "(WIP) Try to glean glTF 2.0 native PBR attributes from the FBX.", + cxxopts::value(gltfOptions.usePBRMetRough)) + ( + "pbr-specular-glossiness", "(WIP) Experimentally fill in the KHR_materials_pbrSpecularGlossiness extension.", + cxxopts::value(gltfOptions.usePBRSpecGloss)) + ( + "k,keep-attribute", "Used repeatedly to build a limiting set of vertex attributes to keep.", + cxxopts::value()) + ("v,verbose", "Enable verbose output.") + ("h,help", "Show this help.") + ("V,version", "Display the current program version."); + + try { + options.parse_positional("input"); + options.parse(argc, argv); + + } catch (const cxxopts::OptionException &e) { + fmt::printf(options.help()); + return 1; + } + + if (options.count("version")) { + fmt::printf( + R"( +FBX2glTF version 2.0 +Copyright (c) 2016-2017 Oculus VR, LLC. +)"); + return 0; + } + + if (options.count("help")) { + fmt::printf(options.help()); + return 0; + } + + if (!options.count("input")) { + fmt::printf("You must supply a FBX file to convert.\n"); + fmt::printf(options.help()); + return 1; + } + + if (options.count("verbose")) { + verboseOutput = true; + } + + if (!gltfOptions.useKHRMatCom && !gltfOptions.usePBRSpecGloss && !gltfOptions.usePBRMetRough) { + if (verboseOutput) { + fmt::printf("Defaulting to KHR_materials_common material support.\n"); + } + gltfOptions.useKHRMatCom = true; + } + + if (options.count("flip-u") > 0) { + texturesTransform(0, 0) = -1.0f; + texturesTransform(1, 1) = 1.0f; + doTransformTextures = true; + } + if (options.count("flip-v") > 0) { + texturesTransform(0, 0) = 1.0f; + texturesTransform(1, 1) = -1.0f; + doTransformTextures = true; + } + + if (options.count("keepAttribute")) { + gltfOptions.keepAttribs = RAW_VERTEX_ATTRIBUTE_JOINT_INDICES | RAW_VERTEX_ATTRIBUTE_JOINT_WEIGHTS; + for (const auto &attribute : options["keepAttribute"].as>()) { + if (attribute == "position") { gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_POSITION; } + else if (attribute == "normal") { gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_NORMAL; } + else if (attribute == "tangent") { gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_TANGENT; } + else if (attribute == "binormal") { gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_BINORMAL; } + else if (attribute == "color") { gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_COLOR; } + else if (attribute == "uv0") { gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_UV0; } + else if (attribute == "uv1") { gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_UV1; } + else if (attribute == "auto") { gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_AUTO; } + else { + fmt::printf("Unknown --keepAttribute: %s\n", attribute); + fmt::printf("Valid choices are: position, normal, tangent, binormial, color, uv0, uv1, auto,\n"); + return 1; + } + } + } + + if (gltfOptions.embedResources && gltfOptions.outputBinary) { + fmt::printf("Note: Ignoring --embed; it's meaningless with --binary.\n"); + } + + if (options.count("output") == 0) { + // if -o is not given, default to the basename of the .fbx + outputPath = "./" + Gltf::StringUtils::GetFileBaseString(inputPath); + } + std::string outputFolder; // the output folder in .gltf mode, not used for .glb + std::string modelPath; // the path of the actual .glb or .gltf file + if (gltfOptions.outputBinary) { + // in binary mode, we write precisely where we're asked + modelPath = outputPath + ".glb"; + } else { + // in gltf mode, we create a folder and write into that + outputFolder = outputPath + "_out/"; + if (!FileUtils::CreatePath(outputFolder.c_str())) { + fmt::fprintf(stderr, "ERROR: Failed to create folder: %s'\n", outputFolder.c_str()); + return 1; + } + modelPath = outputFolder + Gltf::StringUtils::GetFileNameString(outputPath) + ".gltf"; + } + + ModelData *data_render_model = nullptr; + RawModel raw; + + if (verboseOutput) { + fmt::printf("Loading FBX File: %s\n", inputPath); + } + if (!LoadFBXFile(raw, inputPath.c_str(), "tga;bmp;png;jpg")) { + fmt::fprintf(stderr, "ERROR:: Failed to parse FBX: %s\n", inputPath); + return 1; + } + + if (doTransformTextures) { + raw.TransformTextures(texturesTransform); + } + raw.Condense(); + + std::ofstream outStream; // note: auto-flushes in destructor + const auto streamStart = outStream.tellp(); + + outStream.open(modelPath, std::ios::trunc | std::ios::ate | std::ios::out | std::ios::binary); + if (outStream.fail()) { + fmt::fprintf(stderr, "ERROR:: Couldn't open file for writing: %s\n", modelPath.c_str()); + return 1; + } + data_render_model = Raw2Gltf(outStream, raw, gltfOptions); + + if (gltfOptions.outputBinary) { + fmt::printf( + "Wrote %lu bytes of binary glTF to %s.\n", + (unsigned long) (outStream.tellp() - streamStart), modelPath); + delete data_render_model; + return 0; + } + + fmt::printf( + "Wrote %lu bytes of glTF to %s.\n", + (unsigned long) (outStream.tellp() - streamStart), modelPath); + + if (gltfOptions.embedResources) { + // we're done: everything was inlined into the glTF JSON + delete data_render_model; + return 0; + } + + assert(!outputFolder.empty()); + + const std::string binaryPath = outputFolder + extBufferFilename; + FILE *fp = fopen(binaryPath.c_str(), "wb"); + if (fp == nullptr) { + fmt::fprintf(stderr, "ERROR:: Couldn't open file '%s' for writing.\n", binaryPath); + return 1; + } + + const unsigned char *binaryData = &(*data_render_model->binary)[0]; + unsigned long binarySize = data_render_model->binary->size(); + if (fwrite(binaryData, binarySize, 1, fp) != 1) { + fmt::fprintf(stderr, "ERROR: Failed to write %lu bytes to file '%s'.\n", binarySize, binaryPath); + fclose(fp); + return 1; + } + fclose(fp); + fmt::printf("Wrote %lu bytes of binary data to %s.\n", binarySize, binaryPath); + + delete data_render_model; + return 0; +} diff --git a/src/mathfu.h b/src/mathfu.h new file mode 100644 index 0000000..a49f687 --- /dev/null +++ b/src/mathfu.h @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#ifndef FBX2GLTF_MATHFU_H +#define FBX2GLTF_MATHFU_H + +#include + +#include +#include +#include +#include + +/** + * All the mathfu:: implementations of our core data types. + */ + +template +struct Bounds +{ + mathfu::Vector min; + mathfu::Vector max; + bool initialized = false; + + void Clear() { + min = mathfu::Vector(); + max = mathfu::Vector(); + initialized = false; + } + + void AddPoint(mathfu::Vector p) { + if (initialized) { + for (int ii = 0; ii < d; ii ++) { + min(ii) = std::min(min(ii), p(ii)); + max(ii) = std::max(max(ii), p(ii)); + } + } else { + min = p; + max = p; + initialized = true; + } + } +}; + +typedef mathfu::Vector Vec4i; +typedef mathfu::Matrix Mat4i; +typedef mathfu::Vector Vec2f; +typedef mathfu::Vector Vec3f; +typedef mathfu::Vector Vec4f; +typedef mathfu::Matrix Mat2f; +typedef mathfu::Matrix Mat3f; +typedef mathfu::Matrix Mat4f; +typedef mathfu::Quaternion Quatf; +typedef Bounds Boundsf; + +template static inline std::vector toStdVec(mathfu::Vector vec) +{ + std::vector result(d); + for (int ii = 0; ii < d; ii ++) { + result[ii] = vec[ii]; + } + return result; +} + +template std::vector toStdVec(mathfu::Quaternion quat) { + return std::vector { quat.vector()[0], quat.vector()[1], quat.vector()[2], quat.scalar() }; +} + +static inline Vec3f toVec3f(const FbxVector4 &v) { + return Vec3f((float) v[0], (float) v[1], (float) v[2]); +} + +static inline Mat4f toMat4f(const FbxAMatrix &m) { + auto result = Mat4f(); + for (int row = 0; row < 4; row ++) { + for (int col = 0; col < 4; col ++) { + result(row, col) = (float) m[row][col]; + } + } + return result; +} + +static inline Quatf toQuatf(const FbxQuaternion &q) { + return Quatf((float) q[3], (float) q[0], (float) q[1], (float) q[2]); +} + +#endif //FBX2GLTF_MATHFU_H diff --git a/src/utils/File_Utils.cpp b/src/utils/File_Utils.cpp new file mode 100644 index 0000000..89cce98 --- /dev/null +++ b/src/utils/File_Utils.cpp @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include +#include + +#include +#include + +#if defined( __unix__ ) || defined ( __APPLE__ ) + +#include +#include +#include +#include +#include + +#define _getcwd getcwd +#define _mkdir(a) mkdir(a, 0777) +#elif defined( _WIN32 ) +#include +#include +#else +#include +#include +#endif + +#include + +#include "FBX2glTF.h" +#include "String_Utils.h" + +namespace FileUtils { + + std::string GetCurrentFolder() + { + char cwd[Gltf::StringUtils::MAX_PATH_LENGTH]; + if (!_getcwd(cwd, sizeof(cwd))) { + return std::string(); + } + cwd[sizeof(cwd) - 1] = '\0'; + Gltf::StringUtils::GetCleanPath(cwd, cwd, Gltf::StringUtils::PATH_UNIX); + const size_t length = strlen(cwd); + if (cwd[length - 1] != '/' && length < Gltf::StringUtils::MAX_PATH_LENGTH - 1) { + cwd[length + 0] = '/'; + cwd[length + 1] = '\0'; + } + return std::string(cwd); + } + + bool FolderExists(const char *folderPath) + { +#if defined( __unix__ ) || defined( __APPLE__ ) + DIR *dir = opendir(folderPath); + if (dir) { + closedir(dir); + return true; + } + return false; +#else + const DWORD ftyp = GetFileAttributesA( folderPath ); + if ( ftyp == INVALID_FILE_ATTRIBUTES ) + { + return false; // bad path + } + return ( ftyp & FILE_ATTRIBUTE_DIRECTORY ) != 0; +#endif + } + + bool MatchExtension(const char *fileExtension, const char *matchExtensions) + { + if (matchExtensions[0] == '\0') { + return true; + } + if (fileExtension[0] == '.') { + fileExtension++; + } + for (const char *end = matchExtensions; end[0] != '\0';) { + for (; end[0] == ';'; end++) {} + const char *ext = end; + for (; end[0] != ';' && end[0] != '\0'; end++) {} +#if defined( __unix__ ) || defined( __APPLE__ ) + if (strncasecmp(fileExtension, ext, end - ext) == 0) +#else + if ( _strnicmp( fileExtension, ext, end - ext ) == 0 ) +#endif + { + return true; + } + } + return false; + } + + void ListFolderFiles(std::vector &fileList, const char *folder, const char *matchExtensions) + { +#if defined( __unix__ ) || defined( __APPLE__ ) + DIR *dir = opendir(folder); + if (dir == nullptr) { + return; + } + for (;;) { + struct dirent *dp = readdir(dir); + if (dp == nullptr) { + break; + } + + if (dp->d_type == DT_DIR) { + continue; + } + + const char *fileName = dp->d_name; + const char *fileExt = strrchr(fileName, '.'); + + if (!fileExt || !MatchExtension(fileExt, matchExtensions)) { + continue; + } + + fileList.emplace_back(fileName); + } + + closedir(dir); +#else + std::string pathStr = folder; + pathStr += "*"; + + WIN32_FIND_DATA FindFileData; + HANDLE hFind = FindFirstFile( pathStr.c_str(), &FindFileData ); + if ( hFind != INVALID_HANDLE_VALUE ) + { + do + { + if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { + std::string fileName = FindFileData.cFileName; + std::string::size_type extPos = fileName.rfind('.'); + if (extPos != std::string::npos && + MatchExtension(fileName.substr(extPos + 1).c_str(), matchExtensions)) { + fileList.push_back(fileName); + } + } + } while ( FindNextFile( hFind, &FindFileData ) ); + + FindClose( hFind ); + } +#endif + } + + bool CreatePath(const char *path) + { +#if defined( __unix__ ) || defined( __APPLE__ ) + Gltf::StringUtils::PathSeparator separator = Gltf::StringUtils::PATH_UNIX; +#else + Gltf::StringUtils::PathSeparator separator = Gltf::StringUtils::PATH_WIN; +#endif + std::string folder = Gltf::StringUtils::GetFolderString(path); + std::string clean = Gltf::StringUtils::GetCleanPathString(folder, separator); + std::string build = clean; + for (int i = 0; i < clean.length(); i ++) { + if (clean[i] == separator && i > 0) { + build[i] = '\0'; + if (i > 1 || build[1] != ':') { + if (_mkdir(build.c_str()) != 0 && errno != EEXIST) { + return false; + } + } + } + build[i] = clean[i]; + } + return true; + } +} diff --git a/src/utils/File_Utils.h b/src/utils/File_Utils.h new file mode 100644 index 0000000..770ab13 --- /dev/null +++ b/src/utils/File_Utils.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef __FILE_UTILS_H__ +#define __FILE_UTILS_H__ + +namespace FileUtils { + std::string GetCurrentFolder(); + + bool FolderExists(const char *folderPath); + + bool MatchExtension(const char *fileExtension, const char *matchExtensions); + void ListFolderFiles(std::vector &fileList, const char *folder, const char *matchExtensions); + + bool CreatePath(const char *path); +} + +#endif // !__FILE_UTILS_H__ diff --git a/src/utils/Image_Utils.cpp b/src/utils/Image_Utils.cpp new file mode 100644 index 0000000..3966b71 --- /dev/null +++ b/src/utils/Image_Utils.cpp @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include +#include +#include +#include +#include +#include + +#include "File_Utils.h" +#include "Image_Utils.h" + +// https://www.w3.org/TR/PNG/#11IHDR +const int PNG_IHDR_CHUNK_START = 8; +const int PNG_HEADER_SIZE = PNG_IHDR_CHUNK_START + 8 + 13; + +enum PNG_ColorType +{ + Grayscale = 0, + RGB = 2, + Palette = 3, + GrayscaleAlpha = 4, + RGBA = 6 +}; + +static bool ImageIsPNG(char const *fileName, unsigned char const *buffer) +{ + if ((buffer[0] == 0x89 && buffer[1] == 0x50 && buffer[2] == 0x4E && buffer[3] == 0x47)) { + // first chunk must be an IHDR + return ( + buffer[PNG_IHDR_CHUNK_START + 4] == 'I' && + buffer[PNG_IHDR_CHUNK_START + 5] == 'H' && + buffer[PNG_IHDR_CHUNK_START + 6] == 'D' && + buffer[PNG_IHDR_CHUNK_START + 7] == 'R'); + } + return false; +} + +static ImageProperties PNGProperties(unsigned char const *buffer) +{ + // Extract (big-endian) properties from the PNG IHDR + ImageProperties properties; + properties.width = + (buffer[PNG_IHDR_CHUNK_START + 8] & 0xFF) << 24 | (buffer[PNG_IHDR_CHUNK_START + 9] & 0xFF) << 16 | + (buffer[PNG_IHDR_CHUNK_START + 10] & 0xFF) << 8 | (buffer[PNG_IHDR_CHUNK_START + 11] & 0xFF); + properties.height = + (buffer[PNG_IHDR_CHUNK_START + 12] & 0xFF) << 24 | (buffer[PNG_IHDR_CHUNK_START + 13] & 0xFF) << 16 | + (buffer[PNG_IHDR_CHUNK_START + 14] & 0xFF) << 8 | (buffer[PNG_IHDR_CHUNK_START + 15] & 0xFF); + properties.occlusion = (buffer[PNG_IHDR_CHUNK_START + 17] == RGBA) ? IMAGE_TRANSPARENT : IMAGE_OPAQUE; + + return properties; +} + +// header is broken into multiple structs because TGA headers are packed +struct TGA_HeaderStart_t +{ + char IDLength; + char ColorMapType; + char DataTypeCode; +}; + +struct TGA_HeaderColor_t +{ + short int ColorMapOrigin; + short int ColorMapLength; + char ColorMapDepth; +}; + +struct TGA_HeaderOrigin_t +{ + short int XOrigin; + short int YOrigin; + short Width; + short Height; + char BitsPerPixel; + char ImageDescriptor; +}; + +const int TGA_HEADER_SIZE = 8 + sizeof(TGA_HeaderOrigin_t); + +static bool ImageIsTGA(char const *fileName, unsigned char const *buffer) +{ + // TGA's have pretty ambiguous header so we simply check their file extension + size_t len = strlen(fileName); + if (len < 4) { + return false; + } +#if defined( __unix__ ) || defined( __APPLE__ ) + return strcasecmp(fileName + len - 4, ".tga") == 0; +#else + return _stricmp( fileName + len - 4, ".tga" ) == 0; +#endif +} + +static ImageProperties TGAProperties(unsigned char const *buffer) +{ + const TGA_HeaderOrigin_t *header = reinterpret_cast< const TGA_HeaderOrigin_t * >( &(buffer[8])); + + ImageProperties properties; + properties.width = header->Width; + properties.height = header->Height; + properties.occlusion = (header->BitsPerPixel == 32) ? IMAGE_TRANSPARENT : IMAGE_OPAQUE; + + return properties; +} + +ImageProperties GetImageProperties(char const *filePath) +{ + ImageProperties defaultProperties; + defaultProperties.width = 1; + defaultProperties.height = 1; + defaultProperties.occlusion = IMAGE_OPAQUE; + + FILE *f = fopen(filePath, "rb"); + if (f == nullptr) { + return defaultProperties; + } + + // This assumes every image file is at least as large as the largest header. + const int maxHeaderSize = std::max(PNG_HEADER_SIZE, TGA_HEADER_SIZE); + unsigned char buffer[maxHeaderSize]; + if (fread(buffer, (size_t) maxHeaderSize, 1, f) == 1) { + if (ImageIsPNG(filePath, buffer)) { + return PNGProperties(buffer); + } else if (ImageIsTGA(filePath, buffer)) { + return TGAProperties(buffer); + } + } + return defaultProperties; +} diff --git a/src/utils/Image_Utils.h b/src/utils/Image_Utils.h new file mode 100644 index 0000000..d1b3858 --- /dev/null +++ b/src/utils/Image_Utils.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef __IMAGE_UTILS_H__ +#define __IMAGE_UTILS_H__ + +enum ImageOcclusion +{ + IMAGE_OPAQUE, + IMAGE_PERFORATED, + IMAGE_TRANSPARENT +}; + +struct ImageProperties +{ + int width = 0; + int height = 0; + ImageOcclusion occlusion = IMAGE_OPAQUE; +}; + +ImageProperties GetImageProperties(char const *filePath); + +#endif // !__IMAGE_UTILS_H__ diff --git a/src/utils/String_Utils.cpp b/src/utils/String_Utils.cpp new file mode 100644 index 0000000..873e9c3 --- /dev/null +++ b/src/utils/String_Utils.cpp @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "String_Utils.h" + +namespace Gltf { + namespace StringUtils { + + PathSeparator operator!(const PathSeparator &s) + { + return (s == PATH_WIN) ? PATH_UNIX : PATH_WIN; + } + + } +}// namespace Gltf diff --git a/src/utils/String_Utils.h b/src/utils/String_Utils.h new file mode 100644 index 0000000..5c7c750 --- /dev/null +++ b/src/utils/String_Utils.h @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef _STRING_UTILS_H__ +#define _STRING_UTILS_H__ + +#include +#include +#include +#include + +#if defined( _MSC_VER ) +#define strncasecmp _strnicmp +#define strcasecmp _stricmp +#endif + +namespace Gltf // TODO replace +{ + namespace StringUtils { + + static const unsigned int MAX_PATH_LENGTH = 1024; + + enum PathSeparator + { + PATH_WIN = '\\', + PATH_UNIX = '/' + }; + + PathSeparator operator!(const PathSeparator &s); + + inline const std::string GetCleanPathString(const std::string &path, const PathSeparator separator = PATH_WIN) + { + std::string cleanPath = path; + for (size_t s = cleanPath.find(!separator, 0); s != std::string::npos; s = cleanPath.find(!separator, s)) { + cleanPath[s] = separator; + } + return cleanPath; + } + template + inline void GetCleanPath(char (&dest)[size], const char *path, const PathSeparator separator = PATH_WIN) + { + size_t len = size - 1; + strncpy(dest, path, len); + char *destPtr = dest; + while ((destPtr = strchr(destPtr, !separator)) != nullptr) { + *destPtr = separator; + } + } + + inline const std::string GetFolderString(const std::string &path) + { + size_t s = path.rfind(PATH_WIN); + s = (s != std::string::npos) ? s : path.rfind(PATH_UNIX); + return path.substr(0, s + 1); + } + + inline const std::string GetFileNameString(const std::string &path) + { + size_t s = path.rfind(PATH_WIN); + s = (s != std::string::npos) ? s : path.rfind(PATH_UNIX); + return path.substr(s + 1, std::string::npos); + } + + inline const std::string GetFileBaseString(const std::string &path) + { + const std::string fileName = GetFileNameString(path); + return fileName.substr(0, fileName.rfind('.')).c_str(); + } + + inline int CompareNoCase(const std::string &s1, const std::string &s2) + { + return strncasecmp(s1.c_str(), s2.c_str(), MAX_PATH_LENGTH); + } + + } // StringUtils + +}// namespace Gltf +#endif // _STRING_UTILS_H__ +