diff --git a/.clang-format b/.clang-format new file mode 100755 index 0000000..9e76eb0 --- /dev/null +++ b/.clang-format @@ -0,0 +1,40 @@ +Language: Cpp +Standard: Cpp11 +IndentWidth: 4 +TabWidth: 4 +UseTab: Always +ColumnLimit: 120 +AccessModifierOffset: 0 +AlignEscapedNewlinesLeft: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: true +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: true +BinPackParameters: false +BreakBeforeBinaryOperators: false +BreakBeforeTernaryOperators: false +BreakConstructorInitializersBeforeComma: true +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +IndentCaseLabels: true +IndentWrappedFunctionNames: true +KeepEmptyLinesAtTheStartOfBlocks: false +MaxEmptyLinesToKeep: 3 +NamespaceIndentation: All +PointerAlignment: Right +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: Never +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 4 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: true +SpacesInParentheses: false \ No newline at end of file diff --git a/.clang-tidy b/.clang-tidy new file mode 100755 index 0000000..34f5282 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,8 @@ +--- +Checks: 'clang-diagnostic-*,clang-analyzer-*,clang-analyzer-alpha*,cppcoreguidelines-*,modernize-*,performance-*,readability-*,-readability-named-parameter,misc-*,google-runtime-int,llvm-include-order' + +WarningsAsErrors: '' +HeaderFilterRegex: '' +AnalyzeTemporaryDtors: false +... + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..118a35d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build*/ +cmake-build*/ +.idea/ +.directory +.kdev4/ +*.kdev4 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a15e961 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "3rdparty/boost.outcome"] + path = 3rdparty/boost.outcome + url = https://github.com/ned14/boost.outcome.git +[submodule "3rdparty/GSL"] + path = 3rdparty/GSL + url = https://github.com/Microsoft/GSL.git +[submodule "3rdparty/json"] + path = 3rdparty/json + url = https://github.com/nlohmann/json diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt new file mode 100644 index 0000000..9a3e01a --- /dev/null +++ b/3rdparty/CMakeLists.txt @@ -0,0 +1,9 @@ +add_library(nlohmann_json INTERFACE) +target_include_directories(nlohmann_json SYSTEM INTERFACE json/src) + +add_library(microsoft_gsl INTERFACE) +target_include_directories(microsoft_gsl SYSTEM INTERFACE GSL/include) + +add_library(boost_outcome INTERFACE) +target_include_directories(boost_outcome SYSTEM INTERFACE "boost.outcome/include") +target_compile_definitions(boost_outcome INTERFACE BOOST_OUTCOME_ENABLE_ADVANCED) diff --git a/3rdparty/GSL b/3rdparty/GSL new file mode 160000 index 0000000..8b320e3 --- /dev/null +++ b/3rdparty/GSL @@ -0,0 +1 @@ +Subproject commit 8b320e3f5d016f953e55dfc7ec8694c1349d3fe4 diff --git a/3rdparty/boost.outcome b/3rdparty/boost.outcome new file mode 160000 index 0000000..bdfae06 --- /dev/null +++ b/3rdparty/boost.outcome @@ -0,0 +1 @@ +Subproject commit bdfae0601dda1d4ade1db57357ebc5b351927255 diff --git a/3rdparty/json b/3rdparty/json new file mode 160000 index 0000000..c42273d --- /dev/null +++ b/3rdparty/json @@ -0,0 +1 @@ +Subproject commit c42273d2a052545b34f1d10d6a78696c2e641b7b diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..4f0ebaa --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 3.8) +project(gltfpp VERSION 1.0) + +set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") +include(GenerateClangTidyTarget) +include(ExternalProject) + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +if(UNIX OR MINGW) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic -fdiagnostics-color=always") + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -flto -march=native") +endif() +if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fmodules -fimplicit-module-maps -fbuiltin-module-map -Wdocumentation -Wthread-safety") +endif() + +# find_package(Doxygen) +# if(Doxygen_FOUND) +# file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/doc) +# if(DOXYGEN_DOT_FOUND) +# set(USE_DOT "YES") +# else() +# set(USE_DOT "NO") +# endif() +# configure_file("${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in" "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile" @ONLY) +# add_custom_target(gltfpp_doc +# ${DOXYGEN_EXECUTABLE} "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile" +# SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in" +# DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile" +# WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/doc") +# endif() + +add_subdirectory(3rdparty) +add_subdirectory(gltfpp) +enable_testing() +add_subdirectory(test test EXCLUDE_FROM_ALL) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..127a5bc --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5be07eb --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +gltfpp +====== +gltfpp is a glTF 2.0 loader written in C++14 targeting desktop platforms and WebAssembly with a focus on ease of use, type safety and extensibility. + +**WORK IN PROGRESS!** This library is not finished yet. + +Requirements +------------ +- gcc >= 6 or clang >= 3.5 +- CMake >= 3.8 + +When using clang, modules are used to decrease build time. + +Finished tasks +------ +- Parsing infrastructure (trivial properties can be parsed simply by declaring their fields) +- Finished properties: `Asset` + +TODO +---- +- generalized (and type safe) enum parsing +- deserialization of buffer data (embedded base64, GLB) +- Networking (HTTP on desktop, XHR on WebAssembly), asynchronous loading +- CMake support for Emscripten +- Proper documentation +- out of tree extensions? +- a small viewer based on the loader diff --git a/cmake/FindCatch.cmake b/cmake/FindCatch.cmake new file mode 100644 index 0000000..68fac5b --- /dev/null +++ b/cmake/FindCatch.cmake @@ -0,0 +1,9 @@ +find_path(CATCH_INCLUDE_DIR NAMES catch.hpp) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Catch REQUIRED_VARS CATCH_INCLUDE_DIR) + +add_library(Catch::Catch INTERFACE IMPORTED) +set_target_properties(Catch::Catch PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CATCH_INCLUDE_DIR}) + +mark_as_advanced(CATCH_INCLUDE_DIR) \ No newline at end of file diff --git a/cmake/GenerateClangTidyTarget.cmake b/cmake/GenerateClangTidyTarget.cmake new file mode 100755 index 0000000..0839a64 --- /dev/null +++ b/cmake/GenerateClangTidyTarget.cmake @@ -0,0 +1,18 @@ +function(add_clang_tidy_check target) + get_target_property(rel_source_files ${target} SOURCES) + + foreach(f ${rel_source_files}) + get_filename_component(abs_file ${f} ABSOLUTE) + list(APPEND source_files ${abs_file}) + endforeach() + + add_custom_target(${target}_check) + add_custom_command(TARGET ${target}_check POST_BUILD + COMMAND clang-tidy "-p=${PROJECT_BINARY_DIR}" + "-config='${ARGV1}'" + "-extra-arg='-fdiagnostics-color=always'" + "-export-fixes='${CMAKE_CURRENT_BINARY_DIR}/${target}_suggested_fixes.yaml'" + ${source_files} + BYPRODUCTS "${CMAKE_CURRENT_BINARY_DIR}/${target}_suggested_fixes.yaml" + ) +endfunction() \ No newline at end of file diff --git a/format.sh b/format.sh new file mode 100755 index 0000000..c1beb04 --- /dev/null +++ b/format.sh @@ -0,0 +1,2 @@ +#!/bin/sh +for x in $(find test/ gltfpp/ -name '*.cpp' -o -name '*.h'); do clang-format -style=file -i $x; done diff --git a/gltfpp/CMakeLists.txt b/gltfpp/CMakeLists.txt new file mode 100644 index 0000000..febd4f0 --- /dev/null +++ b/gltfpp/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(src) diff --git a/gltfpp/include/Accessor.h b/gltfpp/include/Accessor.h new file mode 100644 index 0000000..c269f26 --- /dev/null +++ b/gltfpp/include/Accessor.h @@ -0,0 +1,11 @@ +#pragma once +#include "BufferView.h" +#include + +namespace gltfpp { + inline namespace v1 { + struct Accessor { + BOOST_HANA_DEFINE_STRUCT(Accessor, (BufferView, bufferView)); + }; + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/Animation.h b/gltfpp/include/Animation.h new file mode 100644 index 0000000..b77659e --- /dev/null +++ b/gltfpp/include/Animation.h @@ -0,0 +1,32 @@ +#pragma once +#include "Error.h" +#include +#include + +namespace gltfpp { + inline namespace v1 { + struct Channel { // TODO generic enum parsing + BOOST_HANA_DEFINE_STRUCT(Channel + + ); + }; + + struct AnimationSampler { + BOOST_HANA_DEFINE_STRUCT(AnimationSampler, + (int, input), + (int, output), + (option, interpolation), // TODO default = linear + (option, extensions), + (option, extras)); + }; + + struct Animation { + BOOST_HANA_DEFINE_STRUCT(Animation, + (std::vector, channels), + (std::vector, samplers), + (option, name), + (option, extensions), + (option, extras)); + }; + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/Asset.h b/gltfpp/include/Asset.h new file mode 100644 index 0000000..349b7b9 --- /dev/null +++ b/gltfpp/include/Asset.h @@ -0,0 +1,19 @@ +#pragma once +#include "Error.h" +#include +#include + +namespace gltfpp { + inline namespace v1 { + struct Asset { + BOOST_HANA_DEFINE_STRUCT(Asset, + (option, copyright), + (option, generator), + (option, minVersion), + (std::string, version), + + (option, extensions), + (option, extras)); + }; + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/Buffer.h b/gltfpp/include/Buffer.h new file mode 100644 index 0000000..cd40f33 --- /dev/null +++ b/gltfpp/include/Buffer.h @@ -0,0 +1,29 @@ +#pragma once +#include "Error.h" +#include "detail/Byte.h" +#include +#include + +namespace gltfpp { + inline namespace v1 { + class Buffer { + public: + Buffer() = default; + + std::vector &operator*(); + const std::vector &operator*() const; + std::vector *operator->(); + const std::vector *operator->() const; + std::vector &get(); + const std::vector &get() const; + + option uri; + option name; + option extensions; + option extras; + std::vector data; + }; + + auto parse(Buffer &b); + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/BufferView.h b/gltfpp/include/BufferView.h new file mode 100644 index 0000000..7babc32 --- /dev/null +++ b/gltfpp/include/BufferView.h @@ -0,0 +1,29 @@ +#pragma once +#include "Buffer.h" +#include "Error.h" +#include +#include +#include + +namespace gltfpp { + inline namespace v1 { + struct BufferView { + BufferView() + : byteStride{0} { + } + + enum Target { ARRAY_BUFFER = 34962, ELEMENT_ARRAY_BUFFER = 34963 }; + + BOOST_HANA_DEFINE_STRUCT(BufferView, + (int, target), // TODO enum parsing + (option, name), + (uint8_t, byteStride), + (option, extensions), + (option, extras)); + gsl::span span; + const Buffer *buffer; + }; + + auto parse(BufferView &); + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/Camera.h b/gltfpp/include/Camera.h new file mode 100644 index 0000000..a45f1d9 --- /dev/null +++ b/gltfpp/include/Camera.h @@ -0,0 +1,10 @@ +#pragma once +#include + +namespace gltfpp { + inline namespace v1 { + struct Camera { + BOOST_HANA_DEFINE_STRUCT(Camera); + }; + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/Error.h b/gltfpp/include/Error.h new file mode 100644 index 0000000..0652150 --- /dev/null +++ b/gltfpp/include/Error.h @@ -0,0 +1,83 @@ +#pragma once +#include +#include + +namespace gltfpp { + inline namespace v1 { + struct gltf_error : std::error_code { + enum cases { key_not_found, index_out_of_range }; + + gltf_error() = default; + inline gltf_error(cases error); + }; + + namespace detail { + struct gltf_error_category final : std::error_category { + const char *name() const noexcept override { + return "glTF error"; + } + + std::string message(int error) const noexcept override { + switch(error) { + case gltf_error::key_not_found: + return "Key not found"; + case gltf_error::index_out_of_range: + return "Index out of range"; + default: + return "Unknown Error"; + } + } + + std::error_condition default_error_condition(int error) const noexcept override { + switch(error) { + case gltf_error::key_not_found: + return std::errc::bad_message; + case gltf_error::index_out_of_range: + return std::errc::result_out_of_range; + default: + return std::error_condition(error, *this); + } + } + }; + } // namespace detail + + inline const detail::gltf_error_category &gltf_error_category() { + static detail::gltf_error_category c; + return c; + } + + gltf_error::gltf_error(gltf_error::cases e) + : std::error_code{e, gltf_error_category()} { + } + + inline gltf_error make_error_code(gltf_error::cases e) { + return gltf_error{e}; + } + + using BOOST_OUTCOME_V1_NAMESPACE::expected; + using BOOST_OUTCOME_V1_NAMESPACE::make_unexpected; + using BOOST_OUTCOME_V1_NAMESPACE::option; + using BOOST_OUTCOME_V1_NAMESPACE::empty_t; + + template + using gltf_result = expected; + + template + constexpr gltf_result make_expected(T &&v) { + return BOOST_OUTCOME_V1_NAMESPACE::make_expected(std::forward(v)); + } + + template + constexpr option as_option(expected ex) { + if(ex) { + return *ex; + } + return empty_t{}; + } + } // namespace v1 +} // namespace gltfpp + +namespace std { + template <> + struct is_error_code_enum : std::true_type {}; +} // namespace std diff --git a/gltfpp/include/Image.h b/gltfpp/include/Image.h new file mode 100644 index 0000000..14d5254 --- /dev/null +++ b/gltfpp/include/Image.h @@ -0,0 +1,10 @@ +#pragma once +#include + +namespace gltfpp { + inline namespace v1 { + struct Image { + BOOST_HANA_DEFINE_STRUCT(Image); + }; + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/Material.h b/gltfpp/include/Material.h new file mode 100644 index 0000000..276661c --- /dev/null +++ b/gltfpp/include/Material.h @@ -0,0 +1,12 @@ +#pragma once +#include "Texture.h" +#include +#include + +namespace gltfpp { + inline namespace v1 { + struct Material { + BOOST_HANA_DEFINE_STRUCT(Material(std::vector, textures)); + }; + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/Mesh.h b/gltfpp/include/Mesh.h new file mode 100644 index 0000000..5390706 --- /dev/null +++ b/gltfpp/include/Mesh.h @@ -0,0 +1,10 @@ +#pragma once +#include + +namespace gltfpp { + inline namespace v1 { + struct Mesh { + BOOST_HANA_DEFINE_STRUCT(Mesh); + }; + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/Meta.h b/gltfpp/include/Meta.h new file mode 100644 index 0000000..ae27bff --- /dev/null +++ b/gltfpp/include/Meta.h @@ -0,0 +1,21 @@ +#pragma once +#include +#include + +namespace gltfpp { + inline namespace v1 { + template + struct disjunction : std::false_type {}; + template + struct disjunction : B1 {}; + template + struct disjunction : std::conditional_t> {}; + + template + struct is_fundamental_json_type + : disjunction, std::is_same, std::is_same> {}; + + template + struct is_fundamental_json_type> : is_fundamental_json_type {}; + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/Node.h b/gltfpp/include/Node.h new file mode 100644 index 0000000..8e33946 --- /dev/null +++ b/gltfpp/include/Node.h @@ -0,0 +1,18 @@ +#pragma once +#include "Camera.h" +#include "Mesh.h" +#include "Skin.h" +#include +#include + +namespace gltfpp { + inline namespace v1 { + struct Node { + BOOST_HANA_DEFINE_STRUCT(Node, + (std::vector, nodes), + (std::vector, meshes), + (Skin, skin), + (Camera, camera)); + }; + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/Sampler.h b/gltfpp/include/Sampler.h new file mode 100644 index 0000000..90b229c --- /dev/null +++ b/gltfpp/include/Sampler.h @@ -0,0 +1,10 @@ +#pragma once +#include + +namespace gltfpp { + inline namespace v1 { + struct Sampler { + BOOST_HANA_DEFINE_STRUCT(Sampler); + }; + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/Scene.h b/gltfpp/include/Scene.h new file mode 100644 index 0000000..dda24c3 --- /dev/null +++ b/gltfpp/include/Scene.h @@ -0,0 +1,12 @@ +#pragma once +#include "Node.h" +#include +#include + +namespace gltfpp { + inline namespace v1 { + struct Scene { + BOOST_HANA_DEFINE_STRUCT(Scene, (std::vector, nodes)); + }; + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/Skin.h b/gltfpp/include/Skin.h new file mode 100644 index 0000000..9e3e413 --- /dev/null +++ b/gltfpp/include/Skin.h @@ -0,0 +1,12 @@ +#pragma once +#include "Accessor.h" +#include +#include + +namespace gltfpp { + inline namespace v1 { + struct Skin { + BOOST_HANA_DEFINE_STRUCT(Skin, (std::vector, accessors)); + }; + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/Texture.h b/gltfpp/include/Texture.h new file mode 100644 index 0000000..ad32c56 --- /dev/null +++ b/gltfpp/include/Texture.h @@ -0,0 +1,12 @@ +#pragma once +#include "Image.h" +#include "Sampler.h" +#include + +namespace gltfpp { + inline namespace v1 { + struct Texture { + BOOST_HANA_DEFINE_STRUCT(Texture, (Image, image), (Sampler, sampler)); + }; + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/detail/Byte.h b/gltfpp/include/detail/Byte.h new file mode 100644 index 0000000..b027f56 --- /dev/null +++ b/gltfpp/include/detail/Byte.h @@ -0,0 +1,65 @@ +#pragma once +#include + +namespace gltfpp { + inline namespace v1 { + enum class byte : unsigned char {}; + + constexpr byte operator""_b(unsigned long long b) { + return static_cast(b); + } + + template + constexpr auto to_integer(byte &b) noexcept -> std::enable_if_t::value, Integer> { + return Integer(b); + } + + template + constexpr auto operator<<=(byte &b, Integer shift) noexcept + -> std::enable_if_t::value, byte &> { + return b = byte(static_cast(b) << shift); + } + + template + constexpr auto operator>>=(byte &b, Integer shift) noexcept + -> std::enable_if_t::value, byte &> { + return b = byte(static_cast(b) >> shift); + } + + template + constexpr auto operator<<(byte &b, Integer shift) noexcept + -> std::enable_if_t::value, byte> { + return byte(static_cast(b) << shift); + } + + template + constexpr auto operator>>(byte &b, Integer shift) noexcept + -> std::enable_if_t::value, byte> { + return byte(static_cast(b) >> shift); + } + + constexpr byte &operator|=(byte &l, byte r) noexcept { + return l = byte(static_cast(l) | static_cast(r)); + } + + constexpr byte &operator&=(byte &l, byte r) noexcept { + return l = byte(static_cast(l) & static_cast(r)); + } + + constexpr byte &operator^=(byte &l, byte r) noexcept { + return l = byte(static_cast(l) ^ static_cast(r)); + } + + constexpr byte operator|(byte l, byte r) noexcept { + return byte(static_cast(l) | static_cast(r)); + } + + constexpr byte operator&(byte l, byte r) noexcept { + return byte(static_cast(l) | static_cast(r)); + } + + constexpr byte operator^(byte l, byte r) noexcept { + return byte(static_cast(l) | static_cast(r)); + } + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/glTF.h b/gltfpp/include/glTF.h new file mode 100644 index 0000000..75c0149 --- /dev/null +++ b/gltfpp/include/glTF.h @@ -0,0 +1,47 @@ +#pragma once +#include "Accessor.h" +#include "Animation.h" +#include "Asset.h" +#include "Buffer.h" +#include "BufferView.h" +#include "Camera.h" +#include "Error.h" +#include "Image.h" +#include "Material.h" +#include "Mesh.h" +#include "Node.h" +#include "Sampler.h" +#include "Scene.h" +#include "Skin.h" +#include "Texture.h" +#include +#include +#include + +namespace gltfpp { + inline namespace v1 { + struct glTF { + BOOST_HANA_DEFINE_STRUCT(glTF, + (Asset, asset), + (option>, animations), + (option>, buffers), + //(option>, bufferViews), + (option, extensions), + (option, extras)); + /* + std::vector cameras; + std::vector images; + std::vector materials; + std::vector meshes; + std::vector nodes; + std::vector samplers; + std::vector scenes; + std::vector skins; + std::vector textures; + Scene *scene; + */ + }; + + gltf_result from_json(const nlohmann::json &j, glTF &a); + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/module.modulemap b/gltfpp/include/module.modulemap new file mode 100644 index 0000000..e85d166 --- /dev/null +++ b/gltfpp/include/module.modulemap @@ -0,0 +1,28 @@ +module thirdparty [system] { + module GSL [system] { + umbrella header "../../3rdparty/GSL/include/gsl/gsl" + export * + } + + module boost_outcome [system] { + umbrella header "../../3rdparty/boost.outcome/include/boost/outcome.hpp" + export * + } + + module json [system] { + umbrella header "../../3rdparty/json/src/json.hpp" + export * + } + + export * +} + +module glTF { + module parsers { + umbrella header "parsers/all.h" + } + + use thirdparty + umbrella header "glTF.h" + export * +} diff --git a/gltfpp/include/parsers/Buffer.h b/gltfpp/include/parsers/Buffer.h new file mode 100644 index 0000000..5a59e55 --- /dev/null +++ b/gltfpp/include/parsers/Buffer.h @@ -0,0 +1,25 @@ +#pragma once +#include "../Buffer.h" +#include "../Error.h" +#include "Parsing.h" + +namespace gltfpp { + inline namespace v1 { + inline auto parse(Buffer &b) { + return [&](const ParseContext &ctx) -> gltf_result { + std::size_t byteLength{}; + + auto result = field(b.uri, "uri")(ctx) >> field(b.name, "name") >> field(byteLength, "byteLength") >> + field(b.extras, "extras") >> field(b.extensions, "extensions"); + + if(result) { + b.data.resize(byteLength); + } + + // TODO deserialize or download actual data + + return ctx; + }; + } + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/parsers/BufferView.h b/gltfpp/include/parsers/BufferView.h new file mode 100644 index 0000000..0080d51 --- /dev/null +++ b/gltfpp/include/parsers/BufferView.h @@ -0,0 +1,35 @@ +#pragma once +#include "../BufferView.h" +#include "../Error.h" +#include "../glTF.h" +#include "Parsing.h" + +namespace gltfpp { + inline namespace v1 { + inline auto parse(BufferView &view) { + return [&](ParseContext &ctx) -> gltf_result { + std::size_t bufferIdx{}; + std::ptrdiff_t offset{}; + std::ptrdiff_t length{}; + + auto result = field(bufferIdx, "buffer")(ctx) >> field(offset, "byteOffset") >> + field(length, "byteLength") >> aggregate(view); + + if(!result) { + return result; + } + + if(!ctx.root->buffers || bufferIdx > ctx.root->buffers->size()) { + return make_unexpected(gltf_error::index_out_of_range); + } + + auto &buffer = ctx.root->buffers.value()[bufferIdx]; + + view.buffer = std::addressof(buffer); + view.span = {buffer.data.data() + offset, length}; // TODO range check + + return ctx; + }; + } + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/parsers/Parsing.h b/gltfpp/include/parsers/Parsing.h new file mode 100644 index 0000000..b097d29 --- /dev/null +++ b/gltfpp/include/parsers/Parsing.h @@ -0,0 +1,133 @@ +#pragma once +#include "../Error.h" +#include "../Meta.h" +#include +#include +#include +#include + +namespace gltfpp { + inline namespace v1 { + struct glTF; + + struct ParseContext { + glTF *root = nullptr; + const nlohmann::json *json = nullptr; + }; + + template + auto parse(T &s); + + template + auto parse(std::vector &s); + + template + auto field(option &target, const char *key) { + return [&, key](ParseContext ctx) -> gltf_result { + auto valIt = ctx.json->find(key); + if(valIt != ctx.json->end()) { + target.set_value(); + auto newCtx = ParseContext{ctx.root, std::addressof(*valIt)}; + auto res = parse(target.value())(newCtx); + static_assert(std::is_same, decltype(res)>{}, + "Return type of the parser function must be gltf_result"); + } else { + target = {}; + } + return ctx; + }; + } + + template + auto field(T &target, const char *key) { + return [&, key](ParseContext ctx) -> gltf_result { + auto valIt = ctx.json->find(key); + if(valIt != ctx.json->end()) { + auto newCtx = ParseContext{ctx.root, std::addressof(*valIt)}; + auto res = parse(target)(newCtx); + static_assert(std::is_same, decltype(res)>{}, + "Return type of the parser function must be gltf_result"); + if(!res) { + return res; + } + return ctx; + } + return make_unexpected(gltf_error::key_not_found); + }; + } + + template + auto aggregate(T &target) { + return [&](ParseContext ctx) -> gltf_result { + constexpr auto accessor = boost::hana::accessors(); + auto names = boost::hana::transform(accessor, boost::hana::first); + auto members = boost::hana::transform(accessor, boost::hana::second); + auto refs = boost::hana::transform(members, [&](auto acc) { return std::ref(acc(target)); }); + + auto res = boost::hana::fold( + boost::hana::zip(names, refs), gltf_result{ctx}, [&](auto c, auto entry) { + if(!c) { + return c; // I hope the optimizer understands that... + } + auto name = boost::hana::to(entry[boost::hana::size_c<0>]); + auto &member = entry[boost::hana::size_c<1>].get(); + return c >> field(member, name); + }); + if(!res) { + return res; + } + return ctx; + }; + } + + namespace detail { + template + auto parseImpl(std::false_type, T &target) { + return aggregate(target); + } + + template + auto parseImpl(std::true_type, T &s) { + return [&](ParseContext ctx) -> gltf_result { + // TODO this is not a complete check + if(ctx.json) { + s = ctx.json->template get(); + return ctx; + } + return make_unexpected(gltf_error::key_not_found); + }; + } + } // namespace detail + + template + auto parse(T &s) { + return detail::parseImpl(is_fundamental_json_type{}, s); + } + + template + auto parse(std::vector &s) { + return [&](ParseContext ctx) -> gltf_result { + if(!ctx.json) { + return make_unexpected(gltf_error::key_not_found); + } + if(!ctx.json->is_array()) { + return make_unexpected(gltf_error::key_not_found); // FIXME type error + } + + s.resize(ctx.json->size()); + + auto in = ctx.json; + auto out = s.begin(); + for(; out != s.end(); ++in, ++out) { + auto res = parse(*out)({ctx.root, in}); + static_assert(std::is_same, decltype(res)>{}, + "Return type of the parser function must be gltf_result"); + if(!res) { + return res; + } + } + return ctx; + }; + } + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/include/parsers/all.h b/gltfpp/include/parsers/all.h new file mode 100644 index 0000000..6d15aa6 --- /dev/null +++ b/gltfpp/include/parsers/all.h @@ -0,0 +1,4 @@ +#pragma once +#include "Buffer.h" +#include "BufferView.h" +#include "Parsing.h" diff --git a/gltfpp/src/Buffer.cpp b/gltfpp/src/Buffer.cpp new file mode 100644 index 0000000..fb4e984 --- /dev/null +++ b/gltfpp/src/Buffer.cpp @@ -0,0 +1,29 @@ +#include "Buffer.h" + +namespace gltfpp { + inline namespace v1 { + std::vector &Buffer::operator*() { + return data; + } + + const std::vector &Buffer::operator*() const { + return data; + } + + std::vector *Buffer::operator->() { + return &data; + } + + const std::vector *Buffer::operator->() const { + return &data; + } + + std::vector &Buffer::get() { + return data; + } + + const std::vector &Buffer::get() const { + return data; + } + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/src/CMakeLists.txt b/gltfpp/src/CMakeLists.txt new file mode 100644 index 0000000..984301c --- /dev/null +++ b/gltfpp/src/CMakeLists.txt @@ -0,0 +1,22 @@ +find_package(GSL REQUIRED) + +add_library(gltfpp + Buffer.cpp + glTF.cpp +) + +target_link_libraries(gltfpp PUBLIC nlohmann_json microsoft_gsl boost_outcome) +target_include_directories(gltfpp PUBLIC "../include") +target_compile_options(gltfpp PRIVATE -fno-exceptions) +if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + target_compile_options(gltfpp PRIVATE -fmodules-decluse -fmodule-name=glTF + -Weverything -Wno-c++98-compat -Wno-c++98-compat-pedantic) +endif() +#cotire(gltfpp) +add_clang_tidy_check(gltfpp) + +install(TARGETS gltfpp + RUNTIME DESTINATION bin + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib +) diff --git a/gltfpp/src/glTF.cpp b/gltfpp/src/glTF.cpp new file mode 100644 index 0000000..d586bd8 --- /dev/null +++ b/gltfpp/src/glTF.cpp @@ -0,0 +1,16 @@ +#include "glTF.h" +#include "parsers/all.h" + +namespace gltfpp { + inline namespace v1 { + gltf_result from_json(const nlohmann::json &j, glTF &g) { + ParseContext ctx{&g, &j}; + auto result = aggregate(g)(ctx); + + if(!result) { + return result.error(); + } + return {}; + } + } // namespace v1 +} // namespace gltfpp diff --git a/gltfpp/src/module.private.modulemap b/gltfpp/src/module.private.modulemap new file mode 100644 index 0000000..d6c421d --- /dev/null +++ b/gltfpp/src/module.private.modulemap @@ -0,0 +1,5 @@ +explicit module glTF.Private { + //header "Utils.h" + //header "Iterator.h" + export * +} diff --git a/pack.sh b/pack.sh new file mode 100755 index 0000000..a4e0807 --- /dev/null +++ b/pack.sh @@ -0,0 +1,2 @@ +#!/bin/sh +tar c 3rdparty .clang-format .clang-tidy cmake CMakeLists.txt format.sh .git .gitignore pack.sh README.md gltfpp test | zstd --ultra -22 > gltfpp.tar.zst diff --git a/test/Asset.cpp b/test/Asset.cpp new file mode 100644 index 0000000..cd8c0c5 --- /dev/null +++ b/test/Asset.cpp @@ -0,0 +1,24 @@ +#include "Buffer.h" +#include "Error.h" +#include "glTF.h" +#include +#include + +using namespace gltfpp; +using nlohmann::json; + +namespace { + const auto minimalAsset = R"({ + "version": "2.0" + })"_json; +} + +TEST_CASE("Asset_Minimal", "[Asset]") { + glTF g; + ParseContext ctx{&g, &minimalAsset}; + + Asset a; + auto success = parse(a)(ctx); + REQUIRE(success); + REQUIRE(a.version == "2.0"); +} diff --git a/test/Buffer.cpp b/test/Buffer.cpp new file mode 100644 index 0000000..5a212dc --- /dev/null +++ b/test/Buffer.cpp @@ -0,0 +1,75 @@ +#include "Buffer.h" +#include "Error.h" +#include "glTF.h" +#include "parsers/Buffer.h" +#include +#include + +using namespace gltfpp; +using nlohmann::json; + +TEST_CASE("Buffer_Minimal", "[Buffer]") { + const auto input = R"({ + "byteLength": 50 + })"_json; + + glTF gltf; + Buffer buffer; + auto success = parse(buffer)({&gltf, &input}); + REQUIRE(success); + + REQUIRE(!buffer.name); + REQUIRE(!buffer.uri); + REQUIRE(buffer.extras.empty()); + REQUIRE(buffer.extensions.empty()); + REQUIRE(buffer->size() == 50); +} + +TEST_CASE("Buffer_Complete", "[Buffer]") { + auto input = R"({ + "uri": "https://example.com/teapot.bin", + "byteLength": 123, + "name": "Utah Teapot", + "extras": ["I", "am", "an", "array"] + })"_json; + + const json extension{{"KHR_materials_common", {"technique", "LAMBERT"}}}; + const json extra{0, 1, 2, 3}; + + input["extensions"] = extension; + input["extras"] = extra; + + glTF gltf; + Buffer buffer; + auto success = parse(buffer)({&gltf, &input}); + REQUIRE(success); + + REQUIRE(buffer.name); + REQUIRE(buffer.name.get() == "Utah Teapot"); + REQUIRE(buffer.uri); + REQUIRE(buffer.uri.get() == "https://example.com/teapot.bin"); + REQUIRE(buffer.extras.get() == extra); + REQUIRE(buffer.extensions.get() == extension); + REQUIRE(buffer->size() == 123); +} + +TEST_CASE("Buffer_MissingLength", "[Buffer]") { + auto input = R"({ + "uri": "https://example.com/teapot.bin", + "name": "Utah Teapot", + "extras": ["I", "am", "an", "array"] + })"_json; + + const json extension{{"KHR_materials_common", {"technique", "LAMBERT"}}}; + const json extra{0, 1, 2, 3}; + + input["extensions"] = extension; + input["extras"] = extra; + + glTF gltf; + Buffer buffer; + auto result = parse(buffer)({&gltf, &input}); + + REQUIRE(result.has_error()); + REQUIRE(result.get_error() == gltf_error::key_not_found); +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..db7a7a5 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,20 @@ +find_package(Catch REQUIRED) + +add_executable(gltfpp_test + Asset.cpp + Buffer.cpp + glTF.cpp + testmain.cpp +) + +add_executable(simple_consumer + simple_consumer.cpp +) +target_compile_options(simple_consumer PRIVATE -fno-exceptions) +if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + target_compile_options(simple_consumer PRIVATE -fmodules-decluse + -Weverything -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-non-modular-include-in-module) +endif() + +target_link_libraries(gltfpp_test PRIVATE Catch::Catch gltfpp) +target_link_libraries(simple_consumer PRIVATE gltfpp) diff --git a/test/glTF.cpp b/test/glTF.cpp new file mode 100644 index 0000000..6310bec --- /dev/null +++ b/test/glTF.cpp @@ -0,0 +1,38 @@ +#include "glTF.h" +#include + +using namespace gltfpp; +using nlohmann::json; +using namespace std::literals; + +namespace { + const auto minimalglTF = R"({ + "asset": { + "version": "2.0", + "generator": "collada2gltf@f356b99aef8868f74877c7ca545f2cd206b9d3b7", + "copyright": "2017 (c) Khronos Group" + } + })"_json; +} + +TEST_CASE("glTF_Minimal", "[glTF]") { + glTF target; + const auto success = from_json(minimalglTF, target); + REQUIRE(success); + REQUIRE(target.asset.version == "2.0"); + REQUIRE(target.asset.generator.get() == "collada2gltf@f356b99aef8868f74877c7ca545f2cd206b9d3b7"); + REQUIRE(target.asset.copyright.get() == "2017 (c) Khronos Group"); +} + +TEST_CASE("glTF_Buffer", "[glTF]") { + glTF target; + auto source = minimalglTF; + json minimalBuffer{"byteLength", 42}; + source["buffer"] = {minimalBuffer, minimalBuffer, minimalBuffer}; + + const auto success = from_json(source, target); + REQUIRE(success); +} + +TEST_CASE("glTF_BufferView", "[glTF]") { +} diff --git a/test/simple_consumer.cpp b/test/simple_consumer.cpp new file mode 100644 index 0000000..4499fb1 --- /dev/null +++ b/test/simple_consumer.cpp @@ -0,0 +1,9 @@ +#include +#include + +int main(int argc, char **argv) { + gltfpp::glTF g; + nlohmann::json j(argv[1]); + from_json(j, g); + printf("%s", g.asset.copyright->c_str()); +} diff --git a/test/testmain.cpp b/test/testmain.cpp new file mode 100644 index 0000000..b3143fb --- /dev/null +++ b/test/testmain.cpp @@ -0,0 +1,2 @@ +#define CATCH_CONFIG_MAIN +#include