diff --git a/data/test_assets/screenshots/floorplanner-json/102343992.png b/data/test_assets/screenshots/floorplanner-json/102343992.png new file mode 100644 index 0000000000..b318a8e067 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/102343992.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/102344022.png b/data/test_assets/screenshots/floorplanner-json/102344022.png new file mode 100644 index 0000000000..a0917e4039 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/102344022.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/102344049.png b/data/test_assets/screenshots/floorplanner-json/102344049.png new file mode 100644 index 0000000000..a010c7ca9c Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/102344049.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/102344094.png b/data/test_assets/screenshots/floorplanner-json/102344094.png new file mode 100644 index 0000000000..c74d93e00f Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/102344094.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/102344115.png b/data/test_assets/screenshots/floorplanner-json/102344115.png new file mode 100644 index 0000000000..808e8c1cdc Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/102344115.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/102344193.png b/data/test_assets/screenshots/floorplanner-json/102344193.png new file mode 100644 index 0000000000..98081a3878 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/102344193.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/102344250.png b/data/test_assets/screenshots/floorplanner-json/102344250.png new file mode 100644 index 0000000000..e58989f61c Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/102344250.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/102344280.png b/data/test_assets/screenshots/floorplanner-json/102344280.png new file mode 100644 index 0000000000..7d1b8043f8 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/102344280.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/102344307.png b/data/test_assets/screenshots/floorplanner-json/102344307.png new file mode 100644 index 0000000000..a91a4bf0b7 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/102344307.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/102344328.png b/data/test_assets/screenshots/floorplanner-json/102344328.png new file mode 100644 index 0000000000..40e739f141 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/102344328.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/102344349.png b/data/test_assets/screenshots/floorplanner-json/102344349.png new file mode 100644 index 0000000000..19ae9a7a3f Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/102344349.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/102344403.png b/data/test_assets/screenshots/floorplanner-json/102344403.png new file mode 100644 index 0000000000..51bbe2d72d Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/102344403.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/102344439.png b/data/test_assets/screenshots/floorplanner-json/102344439.png new file mode 100644 index 0000000000..82c0879237 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/102344439.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/102344457.png b/data/test_assets/screenshots/floorplanner-json/102344457.png new file mode 100644 index 0000000000..2da7384d62 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/102344457.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/102344469.png b/data/test_assets/screenshots/floorplanner-json/102344469.png new file mode 100644 index 0000000000..2ff5ed4ceb Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/102344469.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/102344529.png b/data/test_assets/screenshots/floorplanner-json/102344529.png new file mode 100644 index 0000000000..fcfae9271a Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/102344529.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/103997478_171030525.png b/data/test_assets/screenshots/floorplanner-json/103997478_171030525.png new file mode 100644 index 0000000000..79b2fd1391 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/103997478_171030525.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/103997562_171030642.png b/data/test_assets/screenshots/floorplanner-json/103997562_171030642.png new file mode 100644 index 0000000000..b1e07e747c Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/103997562_171030642.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/106879044_174887172.png b/data/test_assets/screenshots/floorplanner-json/106879044_174887172.png new file mode 100644 index 0000000000..4e8f33994e Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/106879044_174887172.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/108294465_176709960.png b/data/test_assets/screenshots/floorplanner-json/108294465_176709960.png new file mode 100644 index 0000000000..1d18793800 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/108294465_176709960.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/108294624_176710203.png b/data/test_assets/screenshots/floorplanner-json/108294624_176710203.png new file mode 100644 index 0000000000..a78b56fa4b Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/108294624_176710203.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/108294816_176710461.png b/data/test_assets/screenshots/floorplanner-json/108294816_176710461.png new file mode 100644 index 0000000000..35765c6900 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/108294816_176710461.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/108736611_177263226.png b/data/test_assets/screenshots/floorplanner-json/108736611_177263226.png new file mode 100644 index 0000000000..2d95f2e425 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/108736611_177263226.png differ diff --git a/data/test_assets/screenshots/floorplanner-json/108736722_177263382.png b/data/test_assets/screenshots/floorplanner-json/108736722_177263382.png new file mode 100644 index 0000000000..d5d33b5d9f Binary files /dev/null and b/data/test_assets/screenshots/floorplanner-json/108736722_177263382.png differ diff --git a/data/test_assets/screenshots/floorplanner/102343992.png b/data/test_assets/screenshots/floorplanner/102343992.png new file mode 100644 index 0000000000..a52dece895 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/102343992.png differ diff --git a/data/test_assets/screenshots/floorplanner/102344022.png b/data/test_assets/screenshots/floorplanner/102344022.png new file mode 100644 index 0000000000..adb6460eb3 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/102344022.png differ diff --git a/data/test_assets/screenshots/floorplanner/102344049.png b/data/test_assets/screenshots/floorplanner/102344049.png new file mode 100644 index 0000000000..e846b6a213 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/102344049.png differ diff --git a/data/test_assets/screenshots/floorplanner/102344094.png b/data/test_assets/screenshots/floorplanner/102344094.png new file mode 100644 index 0000000000..ca5960d08d Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/102344094.png differ diff --git a/data/test_assets/screenshots/floorplanner/102344115.png b/data/test_assets/screenshots/floorplanner/102344115.png new file mode 100644 index 0000000000..621b4c1042 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/102344115.png differ diff --git a/data/test_assets/screenshots/floorplanner/102344193.png b/data/test_assets/screenshots/floorplanner/102344193.png new file mode 100644 index 0000000000..3a1e528f2b Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/102344193.png differ diff --git a/data/test_assets/screenshots/floorplanner/102344250.png b/data/test_assets/screenshots/floorplanner/102344250.png new file mode 100644 index 0000000000..7b35ce67d8 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/102344250.png differ diff --git a/data/test_assets/screenshots/floorplanner/102344280.png b/data/test_assets/screenshots/floorplanner/102344280.png new file mode 100644 index 0000000000..23e1dde7e4 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/102344280.png differ diff --git a/data/test_assets/screenshots/floorplanner/102344307.png b/data/test_assets/screenshots/floorplanner/102344307.png new file mode 100644 index 0000000000..63a8492fb3 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/102344307.png differ diff --git a/data/test_assets/screenshots/floorplanner/102344328.png b/data/test_assets/screenshots/floorplanner/102344328.png new file mode 100644 index 0000000000..dd79f0a221 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/102344328.png differ diff --git a/data/test_assets/screenshots/floorplanner/102344349.png b/data/test_assets/screenshots/floorplanner/102344349.png new file mode 100644 index 0000000000..f1673bcb72 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/102344349.png differ diff --git a/data/test_assets/screenshots/floorplanner/102344403.png b/data/test_assets/screenshots/floorplanner/102344403.png new file mode 100644 index 0000000000..a408f2be84 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/102344403.png differ diff --git a/data/test_assets/screenshots/floorplanner/102344439.png b/data/test_assets/screenshots/floorplanner/102344439.png new file mode 100644 index 0000000000..b2fb2a02b3 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/102344439.png differ diff --git a/data/test_assets/screenshots/floorplanner/102344457.png b/data/test_assets/screenshots/floorplanner/102344457.png new file mode 100644 index 0000000000..1332f51955 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/102344457.png differ diff --git a/data/test_assets/screenshots/floorplanner/102344469.png b/data/test_assets/screenshots/floorplanner/102344469.png new file mode 100644 index 0000000000..d61a6f2b3a Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/102344469.png differ diff --git a/data/test_assets/screenshots/floorplanner/102344529.png b/data/test_assets/screenshots/floorplanner/102344529.png new file mode 100644 index 0000000000..0e1645a78c Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/102344529.png differ diff --git a/data/test_assets/screenshots/floorplanner/103997478_171030525.png b/data/test_assets/screenshots/floorplanner/103997478_171030525.png new file mode 100644 index 0000000000..8a6d0be206 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/103997478_171030525.png differ diff --git a/data/test_assets/screenshots/floorplanner/103997562_171030642.png b/data/test_assets/screenshots/floorplanner/103997562_171030642.png new file mode 100644 index 0000000000..8a54dbc8f5 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/103997562_171030642.png differ diff --git a/data/test_assets/screenshots/floorplanner/106879044_174887172.png b/data/test_assets/screenshots/floorplanner/106879044_174887172.png new file mode 100644 index 0000000000..7135c56a77 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/106879044_174887172.png differ diff --git a/data/test_assets/screenshots/floorplanner/108294465_176709960.png b/data/test_assets/screenshots/floorplanner/108294465_176709960.png new file mode 100644 index 0000000000..bfeaf22253 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/108294465_176709960.png differ diff --git a/data/test_assets/screenshots/floorplanner/108294624_176710203.png b/data/test_assets/screenshots/floorplanner/108294624_176710203.png new file mode 100644 index 0000000000..36572c18ee Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/108294624_176710203.png differ diff --git a/data/test_assets/screenshots/floorplanner/108294816_176710461.png b/data/test_assets/screenshots/floorplanner/108294816_176710461.png new file mode 100644 index 0000000000..61cdbd6a56 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/108294816_176710461.png differ diff --git a/data/test_assets/screenshots/floorplanner/108736611_177263226.png b/data/test_assets/screenshots/floorplanner/108736611_177263226.png new file mode 100644 index 0000000000..2226a3f9a0 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/108736611_177263226.png differ diff --git a/data/test_assets/screenshots/floorplanner/108736722_177263382.png b/data/test_assets/screenshots/floorplanner/108736722_177263382.png new file mode 100644 index 0000000000..b06fb36741 Binary files /dev/null and b/data/test_assets/screenshots/floorplanner/108736722_177263382.png differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9b71200c35..f8f0553b98 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -213,3 +213,7 @@ if(BUILD_GUI_VIEWERS) add_subdirectory(utils/replayer) endif() endif() + +# TODO some options? reuse BUILD_DATATOOL? +add_subdirectory(utils/compositor) +add_subdirectory(utils/importer) diff --git a/src/cmake/dependencies.cmake b/src/cmake/dependencies.cmake index 4d0c09ef54..2bac7cf231 100644 --- a/src/cmake/dependencies.cmake +++ b/src/cmake/dependencies.cmake @@ -199,13 +199,11 @@ if(NOT USE_SYSTEM_MAGNUM) # These are enabled by default but we don't need them for anything yet set(MAGNUM_WITH_SHADERTOOLS OFF CACHE BOOL "" FORCE) - set(MAGNUM_WITH_MATERIALTOOLS OFF CACHE BOOL "" FORCE) # These are enabled by default but we don't need them if not building GUI # viewers -- disabling for slightly faster builds. If you need any of these # always, simply delete a line. set(MAGNUM_WITH_TEXT OFF CACHE BOOL "" FORCE) - set(MAGNUM_WITH_TEXTURETOOLS OFF CACHE BOOL "" FORCE) set(MAGNUM_WITH_STBTRUETYPEFONT OFF CACHE BOOL "" FORCE) # These are not enabled by default but we need them @@ -224,10 +222,6 @@ if(NOT USE_SYSTEM_MAGNUM) set(MAGNUM_WITH_EMSCRIPTENAPPLICATION OFF CACHE BOOL "" FORCE) set(MAGNUM_WITH_GLFWAPPLICATION OFF CACHE BOOL "" FORCE) set(MAGNUM_WITH_EIGEN ON CACHE BOOL "" FORCE) # Eigen integration - # GltfSceneConverter and KtxImageConverter are needed only by - # BatchRendererTest and are optional - #set(MAGNUM_WITH_GLTFSCENECONVERTER ON CACHE BOOL "" FORCE) - #set(MAGNUM_WITH_KTXIMAGECONVERTER ON CACHE BOOL "" FORCE) if(BUILD_PYTHON_BINDINGS) set(MAGNUM_WITH_PYTHON ON CACHE BOOL "" FORCE) # Python bindings endif() @@ -239,6 +233,18 @@ if(NOT USE_SYSTEM_MAGNUM) set(MAGNUM_WITH_OPENGLTESTER ON CACHE BOOL "" FORCE) endif() + # GltfSceneConverter and KtxImageConverter are needed by BatchRendererTest + # and composite file creating scripts + # TODO have an option for this? or enable always? + set(MAGNUM_WITH_GLTFSCENECONVERTER ON CACHE BOOL "" FORCE) + set(MAGNUM_WITH_KTXIMAGECONVERTER ON CACHE BOOL "" FORCE) + # These are needed by composite file creating scripts + # TODO build only if those utils are built + set(MAGNUM_WITH_MATERIALTOOLS ON CACHE BOOL "" FORCE) + set(MAGNUM_WITH_TEXTURETOOLS ON CACHE BOOL "" FORCE) + set(MAGNUM_WITH_STLIMPORTER ON CACHE BOOL "" FORCE) + set(MAGNUM_WITH_STBRESIZEIMAGECONVERTER ON CACHE BOOL "" FORCE) + # Basis Universal. The repo is extremely huge and so instead of a Git # submodule we bundle just the transcoder files, and only a subset of the # formats (BC7 mode 6 has > 1 MB tables, ATC/FXT1/PVRTC2 are quite rare and diff --git a/src/esp/gfx_batch/Renderer.cpp b/src/esp/gfx_batch/Renderer.cpp index 2588df1e3a..f673e6912f 100644 --- a/src/esp/gfx_batch/Renderer.cpp +++ b/src/esp/gfx_batch/Renderer.cpp @@ -762,7 +762,7 @@ bool Renderer::addFile(const Cr::Containers::StringView filename, Mn::UnsignedInt offset = meshViewOffset; for (Mn::UnsignedLong root : scene->childrenFor(-1)) { Cr::Containers::Array children = - scene->childrenFor(root); + scene->childrenFor(root); // TODO ugh slow AF Cr::Containers::String name = importer->objectName(root); if (!name) { @@ -819,7 +819,7 @@ bool Renderer::addFile(const Cr::Containers::StringView filename, {{}, Mn::Shaders::PhongGL::Flag::VertexColor}) { Mn::Shaders::PhongGL::Flags shaderFlags = extraFlags | Mn::Shaders::PhongGL::Flag::MultiDraw | - Mn::Shaders::PhongGL::Flag::UniformBuffers | + Mn::Shaders::PhongGL::Flag::ShaderStorageBuffers | Mn::Shaders::PhongGL::Flag::NoSpecular | Mn::Shaders::PhongGL::Flag::LightCulling; if (!(state_->flags >= RendererFlag::NoTextures)) { diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 98f84a80e9..35b9c1317c 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -8,9 +8,20 @@ find_package( OPTIONAL_COMPONENTS GltfSceneConverter KtxImageConverter ) -configure_file( - ${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/configure.h -) +# If Magnum is built as static (i.e., the bundled submodule), the plugin is +# static as well, and gets linked to the test. Otherwise it's dynamic and +# loaded through its filename. +if(MAGNUM_BUILD_STATIC) + set(HABITAT_IMPORTER_PLUGIN "HabitatImporter") +else() + set(HABITAT_IMPORTER_PLUGIN $) +endif() + +# First replace ${} variables, then $<> generator expressions +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake + ${CMAKE_CURRENT_BINARY_DIR}/configure.h.in) +file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/$/configure.h + INPUT ${CMAKE_CURRENT_BINARY_DIR}/configure.h.in) corrade_add_test( AttributesConfigsTest @@ -20,7 +31,7 @@ corrade_add_test( assets metadata ) -target_include_directories(AttributesConfigsTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(AttributesConfigsTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) corrade_add_test( AttributesManagersTest @@ -30,7 +41,7 @@ corrade_add_test( assets metadata ) -target_include_directories(AttributesManagersTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(AttributesManagersTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) corrade_add_test( GfxBatchRendererTest @@ -43,7 +54,7 @@ corrade_add_test( MagnumPlugins::KtxImporter MagnumPlugins::StbImageImporter ) -target_include_directories(GfxBatchRendererTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(GfxBatchRendererTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) if(BUILD_WITH_CUDA) target_include_directories( GfxBatchRendererTest PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES} @@ -56,10 +67,26 @@ if(MagnumPlugins_KtxImageConverter_FOUND) target_link_libraries(GfxBatchRendererTest PRIVATE MagnumPlugins::KtxImageConverter) endif() +corrade_add_test( + FloorplannerProcessingTest + FloorplannerProcessingTest.cpp + LIBRARIES + gfx_batch + Magnum::DebugTools + Magnum::AnySceneImporter + MagnumPlugins::GltfImporter + MagnumPlugins::KtxImporter + MagnumPlugins::StbImageImporter +) +target_include_directories(FloorplannerProcessingTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) +if(MAGNUM_BUILD_STATIC) + target_link_libraries(FloorplannerProcessingTest PRIVATE HabitatImporter) +endif() + corrade_add_test(CoreTest CoreTest.cpp LIBRARIES core io) corrade_add_test(CullingTest CullingTest.cpp LIBRARIES gfx) -target_include_directories(CullingTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(CullingTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) corrade_add_test( DepthUnprojectionTest @@ -73,7 +100,7 @@ corrade_add_test( ) corrade_add_test(DrawableTest DrawableTest.cpp LIBRARIES gfx) -target_include_directories(DrawableTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(DrawableTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) corrade_add_test(GeoTest GeoTest.cpp LIBRARIES geo) @@ -85,7 +112,7 @@ corrade_add_test( gfx sim ) -target_include_directories(GfxReplayTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(GfxReplayTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) if(BUILD_WITH_BACKGROUND_RENDERER) corrade_add_test( @@ -98,18 +125,18 @@ if(BUILD_WITH_BACKGROUND_RENDERER) sim ) target_include_directories( - BatchReplayRendererTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR} + BatchReplayRendererTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$ ) endif() corrade_add_test(GibsonSceneTest GibsonSceneTest.cpp LIBRARIES scene sim) -target_include_directories(GibsonSceneTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(GibsonSceneTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) corrade_add_test(HM3DSceneTest HM3DSceneTest.cpp LIBRARIES scene sim) -target_include_directories(HM3DSceneTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(HM3DSceneTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) corrade_add_test(IOTest IOTest.cpp LIBRARIES io metadata) -target_include_directories(IOTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(IOTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) corrade_add_test(LoggingTest LoggingTest.cpp LIBRARIES core) set_tests_properties(LoggingTest PROPERTIES ENVIRONMENT HABITAT_SIM_LOG="") @@ -117,10 +144,10 @@ set_tests_properties(LoggingTest PROPERTIES ENVIRONMENT HABITAT_SIM_LOG="") corrade_add_test( MetadataMediatorTest MetadataMediatorTest.cpp LIBRARIES assets metadata ) -target_include_directories(MetadataMediatorTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(MetadataMediatorTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) corrade_add_test(Mp3dTest Mp3dTest.cpp LIBRARIES scene) -target_include_directories(Mp3dTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(Mp3dTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) corrade_add_test( NavTest @@ -131,7 +158,7 @@ corrade_add_test( io assets ) -target_include_directories(NavTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(NavTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) corrade_add_test( PathFinderTest @@ -141,10 +168,10 @@ corrade_add_test( io Corrade::Utility ) -target_include_directories(PathFinderTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(PathFinderTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) corrade_add_test(PhysicsTest PhysicsTest.cpp LIBRARIES physics) -target_include_directories(PhysicsTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(PhysicsTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) corrade_add_test( ReplicaSceneTest @@ -154,10 +181,10 @@ corrade_add_test( assets sim ) -target_include_directories(ReplicaSceneTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(ReplicaSceneTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) corrade_add_test(ResourceManagerTest ResourceManagerTest.cpp LIBRARIES assets) -target_include_directories(ResourceManagerTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(ResourceManagerTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) corrade_add_test(SceneGraphTest SceneGraphTest.cpp LIBRARIES scene) @@ -172,7 +199,7 @@ corrade_add_test( Magnum::AnyImageConverter MagnumPlugins::StbImageImporter ) -target_include_directories(SimTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(SimTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) if(BUILD_WITH_VHACD) corrade_add_test( @@ -186,7 +213,7 @@ if(BUILD_WITH_VHACD) Magnum::AnyImageConverter MagnumPlugins::StbImageImporter ) - target_include_directories(VoxelGridTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + target_include_directories(VoxelGridTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) endif() # Some tests are LOUD, we don't want to include their full log (but OTOH we diff --git a/src/tests/FloorplannerProcessingTest.cpp b/src/tests/FloorplannerProcessingTest.cpp new file mode 100644 index 0000000000..c3483783cc --- /dev/null +++ b/src/tests/FloorplannerProcessingTest.cpp @@ -0,0 +1,818 @@ +// Copyright (c) Meta Platforms, Inc. and its affiliates. +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /* just for MAGNUM_VERIFY_NO_GL_ERROR() */ +#include +#include +#include +#include + +#include "esp/gfx_batch/RendererStandalone.h" + +#include "configure.h" + +namespace { + +namespace Cr = Corrade; +namespace Mn = Magnum; +using namespace Cr::Containers::Literals; +using namespace Mn::Math::Literals; + +struct FloorplannerProcessingTest : Cr::TestSuite::Tester { + explicit FloorplannerProcessingTest(); + + void sceneListSortedUnique(); + + void original(); /* 1 to 210 */ + void json(); /* 211 to 419 */ + void jsonComposite(); /* 420 to 629 */ +}; + +constexpr Mn::Vector2i BaseRenderSize{1024, 1024}; + +/* Having all images the same size would mean different comparison thresholds + get applied for the same feature across diffently sized scenes, which is not + wanted. Instead, each image is rendered with a different FoV to a different + size. */ +const struct { + Cr::TestSuite::TestCaseDescriptionSourceLocation name; + Mn::Vector2 translation; + Mn::Vector2 scale; + Mn::Float maxThreshold, meanThreshold; + std::size_t expectedNodeCount, expectedDrawCount, expectedBatchCount; +} Data[]{ + /* The names should be sorted and unique. Checked in + sceneListSortedUnique(). */ + {"102343992", {-10.0f, 0.0f}, {1.25f, 1.5f}, + 256.0f, 2.0f, 0, 0, 2}, + {"102344022", {9.0f, -4.0f}, {0.875f, 0.75f}, + 256.0f, 2.0f, 0, 0, 2}, + {"102344049", {-2.0f, -7.0f}, {0.875f, 0.75f}, + 256.0f, 2.0f, 0, 0, 2}, + {"102344094", {7.0f, -2.0f}, {0.375f, 0.375f}, + 256.0f, 2.0f, 0, 0, 2}, + + {"102344115", {4.0f, -3.0f}, {0.25f, 0.375f}, + 256.0f, 2.0f, 0, 0, 2}, + {"102344193", {2.0f, -5.0f}, {0.625f, 0.375f}, + 256.0f, 2.0f, 0, 0, 2}, + {"102344250", {9.0f, -8.0f}, {0.75f, 0.625f}, + 256.0f, 2.0f, 0, 0, 2}, + {"102344280", {1.0f, 3.0f}, {0.875f, 0.75f}, + 256.0f, 2.0f, 0, 0, 2}, + + {"102344307", {9.0f, -9.0f}, {0.75f, 0.625f}, + 256.0f, 2.0f, 0, 0, 2}, + {"102344328", {-5.0f, -6.0f}, {1.625f, 0.625f}, + 256.0f, 2.0f, 0, 0, 2}, + {"102344349", {8.0f, -6.0f}, {0.875f, 0.625f}, + 256.0f, 2.0f, 0, 0, 2}, + {"102344403", {-1.0f, 4.0f}, {1.875f, 1.75f}, + 256.0f, 2.0f, 0, 0, 2}, + + {"102344439", {9.0f, 6.0f}, {1.125f, 1.0f}, + 256.0f, 2.0f, 0, 0, 2}, + {"102344457", {-9.0f, -14.0f}, {0.75f, 0.625f}, + 256.0f, 2.0f, 0, 0, 2}, + {"102344469", {20.0f, -2.0f}, {1.375f, 0.375f}, + 256.0f, 2.0f, 0, 0, 2}, + {"102344529", {2.0f, -2.0f}, {0.875f, 0.625f}, + 256.0f, 2.0f, 0, 0, 2}, + + // TODO add images and translation/scale for the rest + {"102815835", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"102815859", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"102815859_169535055", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"102816009", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"102816036", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"102816051", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"102816066", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"102816114", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"102816150", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"102816201", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"102816216", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"102816600", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"102816615", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"102816627", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"102816729", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"102816756", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"102816786", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"102816852", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"102817053", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"102817119", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"102817140", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"102817161", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"102817200", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"103997403_171030405", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"103997424_171030444", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"103997445_171030492", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"103997460_171030507", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"103997478_171030525", {-5.0f, 0.0f}, {1.125f, 0.875f}, + 256.0f, 2.1f, 4517, 4328, 8}, // TODO median higher because window z fighting + + {"103997478_171030528", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"103997541_171030615", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"103997562_171030642", {8.0f, -11.0f}, {1.375f, 0.75f}, + 256.0f, 4.0f, 3783, 3532, 8}, + {"103997586_171030666", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"103997586_171030669", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"103997613_171030702", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"103997643_171030747", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"103997718_171030855", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"103997730_171030885", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"103997781_171030978", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"103997799_171031002", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"103997865_171031116", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"103997895_171031182", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"103997919_171031233", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"103997940_171031257", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"103997970_171031287", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"103997994_171031320", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104348010_171512832", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104348028_171512877", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104348037_171512898", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"104348064_171512940", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104348082_171512994", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104348103_171513021", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104348133_171513054", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"104348160_171513093", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104348181_171513120", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104348202_171513150", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104348253_171513237", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"104348289_171513294", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104348328_171513363", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104348361_171513414", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104348394_171513453", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"104348463_171513588", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104348478_171513603", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104348511_171513654", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104862345_172226274", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"104862369_172226304", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104862384_172226319", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104862396_172226349", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104862417_172226382", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"104862474_172226496", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104862501_172226556", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104862513_172226580", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104862534_172226625", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"104862558_172226664", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104862573_172226682", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104862579_172226694", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104862609_172226751", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"104862621_172226772", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104862639_172226823", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104862660_172226844", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104862669_172226853", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"104862681_172226874", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104862687_172226883", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"104862726_172226952", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"105515151_173104068", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"105515160_173104077", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + // TODO this one is a three-story building, needs the roof and upper floors removed (thus rendering three times? or adding it three times next to itself into a single image, each time with a different clipping plane?) + // {"105515175_173104107", {0.0f, 6.0f}, {2.125f, 2.375f}, + // 256.0f, 2.0f, 0, 0, 2}, + {"105515184_173104128", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"105515211_173104173", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"105515211_173104179", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"105515211_173104185", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"105515235_173104215", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"105515265_173104248", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"105515286_173104287", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"105515301_173104305", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"105515307_173104317", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"105515322_173104332", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"105515337_173104347", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"105515364_173104374", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"105515379_173104395", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"105515403_173104449", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"105515430_173104494", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"105515448_173104512", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"105515490_173104566", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"105515505_173104584", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"105515523_173104614", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"105515541_173104641", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106365897_174225969", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106365897_174225972", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"106366104_174226320", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106366104_174226329", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106366104_174226332", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106366134_174226362", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"106366173_174226431", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106366185_174226443", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106366233_174226506", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106366248_174226527", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"106366263_174226557", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106366293_174226605", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106366302_174226617", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106366323_174226647", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"106366335_174226662", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106366353_174226695", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106366371_174226743", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106366386_174226770", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"106366410_174226806", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106366413_174226809", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106366434_174226881", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106878840_174886947", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"106878858_174886965", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106878867_174886977", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106878915_174887025", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106878945_174887058", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"106878960_174887073", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106878975_174887088", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106879005_174887124", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106879023_174887148", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"106879044_174887172", {8.0f, -8.0f}, {0.5f, 0.5f}, + 256.0f, 2.0f, 0, 0, 2}, + {"106879080_174887211", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"106879104_174887235", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"107733912_175999623", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"107733945_175999674", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"107733960_175999701", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"107733978_175999737", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"107734017_175999794", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"107734056_175999839", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + // TODO also roof + // {"107734080_175999881", {0.0f, 0.0f}, {3.0f, 3.0f}, + // 256.0f, 2.0f, 0, 0, 2}, + {"107734110_175999914", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"107734110_175999917", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"107734119_175999932", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"107734119_175999935", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"107734119_175999938", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"107734146_175999971", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"107734158_175999998", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"107734176_176000019", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"107734188_176000034", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"107734227_176000091", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"107734254_176000121", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"107734287_176000160", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"107734338_176000244", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"107734410_176000355", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"107734449_176000403", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"107734479_176000442", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108294417_176709879", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108294465_176709960", {5.0f, -5.0f}, {0.25f, 0.375f}, + 256.0f, 2.0f, 0, 0, 2}, + + {"108294492_176709993", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108294537_176710050", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108294558_176710095", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108294573_176710113", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"108294600_176710152", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108294624_176710203", {-1.0f, -4.0f}, {0.75f, 0.875f}, + 256.0f, 2.0f, 0, 0, 2}, + {"108294684_176710278", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108294765_176710386", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"108294783_176710410", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108294798_176710428", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108294816_176710461", {9.0f, -5.0f}, {1.5f, 0.5f}, + 256.0f, 2.0f, 0, 0, 2}, + {"108294846_176710506", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"108294870_176710551", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108294897_176710602", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108294915_176710620", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108294927_176710650", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"108294939_176710668", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108736611_177263226", {4.0f, -9.0f}, {0.75f, 0.875f}, + 256.0f, 2.0f, 0, 0, 2}, + {"108736635_177263256", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108736656_177263304", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"108736677_177263328", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108736689_177263340", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108736704_177263361", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108736722_177263382", {7.0f, -1.0f}, {0.5f, 0.375f}, + 256.0f, 2.0f, 0, 0, 2}, + + {"108736737_177263406", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108736779_177263484", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108736800_177263517", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108736824_177263559", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + + {"108736851_177263586", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108736872_177263607", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, + {"108736884_177263634", {0.0f, 0.0f}, {1.0f, 1.0f}, + 256.0f, 2.0f, 0, 0, 1}, +}; + +FloorplannerProcessingTest::FloorplannerProcessingTest() { + addTests({&FloorplannerProcessingTest::sceneListSortedUnique}); + + addInstancedTests({&FloorplannerProcessingTest::original}, + Cr::Containers::arraySize(Data)); + addInstancedTests({&FloorplannerProcessingTest::json}, + Cr::Containers::arraySize(Data)); + addInstancedTests({&FloorplannerProcessingTest::jsonComposite}, + Cr::Containers::arraySize(Data)); +} + +void FloorplannerProcessingTest::sceneListSortedUnique() { + Cr::Containers::StringView prev; + + CORRADE_COMPARE_AS(Cr::Containers::arraySize(Data), 211, + Cr::TestSuite::Compare::LessOrEqual); + if(Cr::Containers::arraySize(Data) < 211) + CORRADE_WARN("Only" << Cr::Containers::arraySize(Data) << "scenes tested out of 211"); + + for(std::size_t i = 0; i != Cr::Containers::arraySize(Data); ++i) { + CORRADE_COMPARE_AS(Data[i].name, prev, Cr::TestSuite::Compare::Greater); + prev = Data[i].name; + + /* The translation should have no fractional part */ + CORRADE_COMPARE(Mn::Math::round(Data[i].translation), Data[i].translation); + + /* The scale should be 1/8ths at least */ + CORRADE_COMPARE_AS(Mn::Int(Data[i].scale.x()*1000), 125, + Cr::TestSuite::Compare::Divisible); + CORRADE_COMPARE_AS(Mn::Int(Data[i].scale.y()*1000), 125, + Cr::TestSuite::Compare::Divisible); + } +} + +void FloorplannerProcessingTest::original() { + auto&& data = Data[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + const Mn::Vector2i renderSize = BaseRenderSize*data.scale; + + // clang-format off + esp::gfx_batch::RendererStandalone renderer{ + esp::gfx_batch::RendererConfiguration{} + .setTileSizeCount(renderSize, {1, 1}), + esp::gfx_batch::RendererStandaloneConfiguration{} + .setFlags(esp::gfx_batch::RendererStandaloneFlag::QuietLog) + }; + // clang-format on + + const Cr::Containers::String filename = Cr::Utility::Path::join(DATA_DIR, Cr::Utility::format("fp/scenes/original/{}.glb", Cr::Containers::StringView{data.name})); + if(!Cr::Utility::Path::exists(filename)) + CORRADE_SKIP(filename << "doesn't exist"); + + /* GenerateMipmap to avoid signficant aliasing and thus potentially a lot of + comparison differences */ + CORRADE_VERIFY(renderer.addFile(filename, esp::gfx_batch::RendererFileFlag::Whole|esp::gfx_batch::RendererFileFlag::GenerateMipmap)); + CORRADE_COMPARE(renderer.addNodeHierarchy(0, filename, {}), 0); + + /* Perspective projection to be able to see walls also a bit */ + renderer.camera(0) = + Mn::Matrix4::perspectiveProjection(35.0_degf*data.scale.x(), Mn::Vector2{data.scale}.aspectRatio(), 1.0f, 100.0f)* + Mn::Matrix4::translation({data.translation, -50.0f})* + Mn::Matrix4::rotationX(90.0_degf); + + renderer.draw(); + Mn::Image2D color = renderer.colorImage(); + MAGNUM_VERIFY_NO_GL_ERROR(); + + CORRADE_COMPARE_WITH(color, + Cr::Utility::Path::join({TEST_ASSETS, + "screenshots/floorplanner/", data.name + ".png"_s}), + (Mn::DebugTools::CompareImageToFile{})); + + /* Verify that there isn't anything out of view, i.e. that a strip along the + edges has just the background color */ + constexpr std::size_t EdgeSize = 10; + for(auto edge: { + color.pixels().prefix(EdgeSize), /* bottom */ + color.pixels().flipped<0>().prefix(EdgeSize), /* top */ + color.pixels().transposed<0, 1>().prefix(EdgeSize), /* left */ + color.pixels().transposed<0, 1>().flipped<0>().prefix(EdgeSize), /* right */ + }) { + for(auto row: edge) + for(auto pixel: row) + CORRADE_FAIL_IF(pixel != 0x1f1f1fff_rgba, + "Render is out of view"); + } +} + +void FloorplannerProcessingTest::json() { + auto&& data = Data[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + const Mn::Vector2i renderSize = BaseRenderSize*data.scale; + + // clang-format off + esp::gfx_batch::RendererStandalone renderer{ + esp::gfx_batch::RendererConfiguration{} + .setTileSizeCount(renderSize, {1, 1}), + esp::gfx_batch::RendererStandaloneConfiguration{} + .setFlags(esp::gfx_batch::RendererStandaloneFlag::QuietLog) + }; + // clang-format on + + /* Skip if the JSON isn't available (i.e., the dataset isn't present) */ + const Cr::Containers::String basePath = Cr::Utility::Path::join(DATA_DIR, "fphab/scenes/"); + const Cr::Containers::String filename = Cr::Utility::Path::join(basePath, data.name + ".scene_instance.json"_s); + if(!Cr::Utility::Path::exists(filename)) + CORRADE_SKIP(filename << "doesn't exist"); + + /* Parse the JSON for external filenames + transformations */ + Cr::PluginManager::Manager manager; + Cr::Containers::Pointer importer = manager.loadAndInstantiate(HABITAT_IMPORTER_PLUGIN); + CORRADE_VERIFY(importer); + CORRADE_VERIFY(importer->openFile(filename)); + Cr::Containers::Optional scene = importer->scene(0); + CORRADE_VERIFY(scene); + + /* Query the external file field */ + const Mn::Trade::SceneField sceneFieldExternalFile = importer->sceneFieldForName("externalFile"); + CORRADE_VERIFY(sceneFieldExternalFile != Mn::Trade::SceneField{}); + const Cr::Containers::Optional externalFileFieldId = scene->findFieldId(sceneFieldExternalFile); + CORRADE_VERIFY(externalFileFieldId); + + /* Gather transformations for all objects. Objects that don't have it are + kept at the default identity transformation. */ + Cr::Containers::Array transformations{Cr::ValueInit, std::size_t(importer->objectCount())}; + for(Cr::Containers::Pair transformation: scene->transformations3DAsArray()) { + transformations[transformation.first()] = transformation.second(); + } + + /* Go through all referenced external files and add them with their + transformation */ + { + CORRADE_COMPARE(scene->mappingType(), Mn::Trade::SceneMappingType::UnsignedInt); + Cr::Containers::StridedArrayView1D mappings = scene->mapping(*externalFileFieldId); + Cr::Containers::StringIterable files = scene->fieldStrings(*externalFileFieldId); + for(std::size_t i = 0; i != scene->fieldSize(*externalFileFieldId); ++i) { + const Cr::Containers::String filename = Cr::Utility::Path::join(basePath, files[i]); + + /* Add the file only if it's not already present (such as the same chair + used multiple times in different locations) */ + if(!renderer.hasNodeHierarchy(filename)) + /* GenerateMipmap to avoid signficant aliasing and thus potentially a + lot of comparison differences */ + CORRADE_VERIFY(renderer.addFile(filename, esp::gfx_batch::RendererFileFlag::Whole|esp::gfx_batch::RendererFileFlag::GenerateMipmap)); + renderer.addNodeHierarchy(0, filename, transformations[mappings[i]]); + } + } + + /* Perspective projection to be able to see walls also a bit */ + renderer.camera(0) = + Mn::Matrix4::perspectiveProjection(35.0_degf*data.scale.x(), Mn::Vector2{data.scale}.aspectRatio(), 1.0f, 100.0f)* + Mn::Matrix4::translation({data.translation, -50.0f})* + Mn::Matrix4::rotationX(90.0_degf); + + renderer.draw(); + Mn::Image2D color = renderer.colorImage(); + MAGNUM_VERIFY_NO_GL_ERROR(); + + // TODO fix these + { + CORRADE_EXPECT_FAIL("The JSON-produced datasets are currently significantly different from the original files."); + CORRADE_COMPARE_WITH(color, + Cr::Utility::Path::join({TEST_ASSETS, + "screenshots/floorplanner/", data.name + ".png"_s}), + (Mn::DebugTools::CompareImageToFile{})); + } { + /* They should be not too different, though */ + CORRADE_COMPARE_WITH(color, + Cr::Utility::Path::join({TEST_ASSETS, + "screenshots/floorplanner/", data.name + ".png"_s}), + (Mn::DebugTools::CompareImageToFile{256.0f, 20.0f})); + } + + CORRADE_COMPARE_WITH(color, + Cr::Utility::Path::join({TEST_ASSETS, + "screenshots/floorplanner-json/", data.name + ".png"_s}), + (Mn::DebugTools::CompareImageToFile{})); +} + +void FloorplannerProcessingTest::jsonComposite() { + auto&& data = Data[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + const Mn::Vector2i renderSize = BaseRenderSize*data.scale; + + // clang-format off + esp::gfx_batch::RendererStandalone renderer{ + esp::gfx_batch::RendererConfiguration{} + .setTileSizeCount(renderSize, {1, 1}), + esp::gfx_batch::RendererStandaloneConfiguration{} + .setFlags(esp::gfx_batch::RendererStandaloneFlag::QuietLog) + }; + // clang-format on + + /* Skip if the JSON isn't available (i.e., the dataset isn't present) */ + const Cr::Containers::String basePath = Cr::Utility::Path::join(DATA_DIR, "fphab/scenes/"); + const Cr::Containers::String filename = Cr::Utility::Path::join(basePath, data.name + ".scene_instance.json"_s); + if(!Cr::Utility::Path::exists(filename)) + CORRADE_SKIP(filename << "doesn't exist"); + + /* Skip if the composite files aren't available */ + // TODO ugh something stable instead + const Cr::Containers::String compositeFilename = Cr::Utility::Path::join(DATA_DIR, "floorplanner-full/composite-1000-split.gltf"); + if(!Cr::Utility::Path::exists(compositeFilename)) + CORRADE_SKIP(compositeFilename << "doesn't exist"); + // const Cr::Containers::String compositeFurnitureFilename = Cr::Utility::Path::join(DATA_DIR, "composite-fp-dedup2/furniture-subset-ca7f00d.gltf"); + // if(!Cr::Utility::Path::exists(compositeFurnitureFilename)) + // CORRADE_SKIP(compositeFurnitureFilename << "doesn't exist"); + + /* Load the composites */ + // TODO have them contain mipmaps, otherwise it'll be aliased hell + CORRADE_VERIFY(renderer.addFile(compositeFilename)); + // CORRADE_VERIFY(renderer.addFile(compositeFurnitureFilename)); + + /* Parse the JSON for external filenames + transformations */ + Cr::PluginManager::Manager manager; + Cr::Containers::Pointer importer = manager.loadAndInstantiate(HABITAT_IMPORTER_PLUGIN); + CORRADE_VERIFY(importer); + CORRADE_VERIFY(importer->openFile(filename)); + Cr::Containers::Optional scene = importer->scene(0); + CORRADE_VERIFY(scene); + + /* Query the external file field */ + const Mn::Trade::SceneField sceneFieldExternalFile = importer->sceneFieldForName("externalFile"); + CORRADE_VERIFY(sceneFieldExternalFile != Mn::Trade::SceneField{}); + const Cr::Containers::Optional externalFileFieldId = scene->findFieldId(sceneFieldExternalFile); + CORRADE_VERIFY(externalFileFieldId); + + /* Gather transformations for all objects. Objects that don't have it are + kept at the default identity transformation. */ + Cr::Containers::Array transformations{Cr::ValueInit, std::size_t(importer->objectCount())}; + for(Cr::Containers::Pair transformation: scene->transformations3DAsArray()) { + transformations[transformation.first()] = transformation.second(); + } + + /* Go through all referenced external files and add them with their + transformation */ + { + CORRADE_COMPARE(scene->mappingType(), Mn::Trade::SceneMappingType::UnsignedInt); + Cr::Containers::StridedArrayView1D mappings = scene->mapping(*externalFileFieldId); + Cr::Containers::StringIterable files = scene->fieldStrings(*externalFileFieldId); + for(std::size_t i = 0; i != scene->fieldSize(*externalFileFieldId); ++i) { + if(files[i].hasPrefix("../objects/decomposed")) { + CORRADE_WARN("Skipping" << files[i] << "not currently present in the composite due to bugs in source data"); + continue; + } + + /* Add the file only if it's not already present (such as the same chair + used multiple times in different locations) */ + CORRADE_FAIL_IF(!renderer.hasNodeHierarchy(files[i]), + files[i] << "not present in the composite"); + + renderer.addNodeHierarchy(0, files[i], transformations[mappings[i]]); + } + } + + /* Perspective projection to be able to see walls also a bit */ + renderer.camera(0) = + Mn::Matrix4::perspectiveProjection(35.0_degf*data.scale.x(), Mn::Vector2{data.scale}.aspectRatio(), 1.0f, 100.0f)* + Mn::Matrix4::translation({data.translation, -50.0f})* + Mn::Matrix4::rotationX(90.0_degf); + + renderer.draw(); + Mn::Image2D color = renderer.colorImage(); + MAGNUM_VERIFY_NO_GL_ERROR(); + + // TODO compare against the originals once the process is fixed + CORRADE_COMPARE_WITH(color, + Cr::Utility::Path::join({TEST_ASSETS, + "screenshots/floorplanner-json/", data.name + ".png"_s}), + (Mn::DebugTools::CompareImageToFile{data.maxThreshold, data.meanThreshold})); + + esp::gfx_batch::SceneStats stats = renderer.sceneStats(0); + // TODO use three separate comparisons with .fallthrough() instead once + // implemented in TestSuite + CORRADE_COMPARE(Cr::Containers::triple(stats.nodeCount, stats.drawCount, stats.drawBatchCount), Cr::Containers::triple(data.expectedNodeCount, data.expectedDrawCount, data.expectedBatchCount)); +} + +} + +CORRADE_TEST_MAIN(FloorplannerProcessingTest) diff --git a/src/tests/GfxBatchRendererTest.cpp b/src/tests/GfxBatchRendererTest.cpp index 3d8cda7fda..a88ff8dfd9 100644 --- a/src/tests/GfxBatchRendererTest.cpp +++ b/src/tests/GfxBatchRendererTest.cpp @@ -1571,8 +1571,7 @@ void GfxBatchRendererTest::singleMesh() { /* Check that texture coordinates, image data or whatever else didn't get flipped -- there should be a red pixel on the bottom left and grey pixel on the top left. */ - CORRADE_COMPARE_WITH( - renderer.colorImage(), + CORRADE_COMPARE_WITH(color, Cr::Utility::Path::join(TEST_ASSETS, "screenshots/GfxBatchRendererTestSingleMesh.png"), (Mn::DebugTools::CompareImageToFile{data.maxThreshold, @@ -1638,8 +1637,7 @@ void GfxBatchRendererTest::meshHierarchy() { /* The square at bottom left should be the usual checkerboard, square at bottom right should be cyan, top left magenta and top right yellow */ - CORRADE_COMPARE_AS( - renderer.colorImage(), + CORRADE_COMPARE_AS(color, Cr::Utility::Path::join({TEST_ASSETS, "screenshots", data.filename}), Mn::DebugTools::CompareImageToFile); CORRADE_COMPARE(color.size(), (Mn::Vector2i{128, 96})); @@ -1701,8 +1699,7 @@ void GfxBatchRendererTest::multipleMeshes() { /* Square should be the usual checkerboard, circle should be cyan, triangle magenta */ - CORRADE_COMPARE_WITH( - renderer.colorImage(), + CORRADE_COMPARE_WITH(color, Cr::Utility::Path::join( TEST_ASSETS, "screenshots/GfxBatchRendererTestMultipleMeshes.png"), (Mn::DebugTools::CompareImageToFile{data.maxThreshold, @@ -1801,8 +1798,7 @@ void GfxBatchRendererTest::multipleScenes() { /* Just a visual test, the texture transformation and material aspects were tested well enough above */ - CORRADE_COMPARE_WITH( - renderer.colorImage(), + CORRADE_COMPARE_WITH(color, Cr::Utility::Path::join( TEST_ASSETS, "screenshots/GfxBatchRendererTestMultipleScenes.png"), (Mn::DebugTools::CompareImageToFile{data.maxThreshold, diff --git a/src/tests/configure.h.cmake b/src/tests/configure.h.cmake index 79dfe49af3..0fc2dfcd72 100644 --- a/src/tests/configure.h.cmake +++ b/src/tests/configure.h.cmake @@ -7,3 +7,4 @@ #define DATA_DIR "${DATA_DIR}" #define MAGNUMRENDERERTEST_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}" +#define HABITAT_IMPORTER_PLUGIN "${HABITAT_IMPORTER_PLUGIN}" diff --git a/src/utils/compositor/CMakeLists.txt b/src/utils/compositor/CMakeLists.txt new file mode 100644 index 0000000000..3bbcf952be --- /dev/null +++ b/src/utils/compositor/CMakeLists.txt @@ -0,0 +1,88 @@ +# Copyright (c) Meta Platforms, Inc. and its affiliates. +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +find_package(Magnum REQUIRED + # Tools, tools, tools everywhere + MaterialTools + MeshTools + SceneTools + TextureTools + Trade) + +# This is where static plugins are really a pain to deal with as we need +# basically all of them. With dynamic plugins none of this would be needed. +find_package(Magnum REQUIRED + AnySceneImporter) +find_package(MagnumPlugins REQUIRED + AssimpImporter + GltfImporter + StlImporter + StbImageImporter + StbResizeImageConverter + KtxImageConverter + GltfSceneConverter) + +add_library(CompositorObjects OBJECT + CompositorState.cpp + CompositorState.h) +target_include_directories(CompositorObjects PRIVATE $) + +add_executable(compositor-replicacad compositor-replicacad.cpp $) +target_link_libraries(compositor-replicacad PRIVATE + Magnum::MaterialTools + Magnum::MeshTools + Magnum::SceneTools + Magnum::TextureTools + Magnum::Trade + Magnum::AnySceneImporter + MagnumPlugins::GltfImporter + MagnumPlugins::StbImageImporter + MagnumPlugins::KtxImageConverter + MagnumPlugins::GltfSceneConverter) + +add_executable(compositor-ycb compositor-ycb.cpp $) +target_link_libraries(compositor-ycb PRIVATE + Magnum::MeshTools + Magnum::SceneTools + Magnum::TextureTools + Magnum::Trade + Magnum::AnySceneImporter + MagnumPlugins::GltfImporter + # TODO Gotta admit, this plugin is awfully slow at opening all these + # uselessly huge 4K textures. Or maybe it's the resizing operation taking + # too long? With an external (release) Magnum build and libpng-based + # importers it's not this awful. + MagnumPlugins::StbImageImporter + MagnumPlugins::StbResizeImageConverter + MagnumPlugins::KtxImageConverter + MagnumPlugins::GltfSceneConverter) + +add_executable(compositor-fetch compositor-fetch.cpp $) +target_link_libraries(compositor-fetch PRIVATE + Magnum::MaterialTools + Magnum::MeshTools + Magnum::SceneTools + Magnum::TextureTools + Magnum::Trade + Magnum::AnySceneImporter + MagnumPlugins::AssimpImporter + MagnumPlugins::GltfImporter + MagnumPlugins::StlImporter + MagnumPlugins::StbImageImporter + MagnumPlugins::StbResizeImageConverter + MagnumPlugins::KtxImageConverter + MagnumPlugins::GltfSceneConverter) + +add_executable(compositor-floorplanner compositor-floorplanner.cpp $) +target_link_libraries(compositor-floorplanner PRIVATE + Magnum::MaterialTools + Magnum::MeshTools + Magnum::SceneTools + Magnum::TextureTools + Magnum::Trade + Magnum::AnySceneImporter + MagnumPlugins::GltfImporter + MagnumPlugins::StbImageImporter + MagnumPlugins::KtxImageConverter + MagnumPlugins::GltfSceneConverter) diff --git a/src/utils/compositor/CompositorState.cpp b/src/utils/compositor/CompositorState.cpp new file mode 100644 index 0000000000..fc490d925b --- /dev/null +++ b/src/utils/compositor/CompositorState.cpp @@ -0,0 +1,259 @@ +// Copyright (c) Meta Platforms, Inc. and its affiliates. +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +#include "CompositorState.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace esp { + +namespace Cr = Corrade; +namespace Mn = Magnum; +using namespace Cr::Containers::Literals; +using namespace Mn::Math::Literals; + +CompositorState::CompositorState(const Corrade::Containers::StringView output) { + converterManager.registerExternalManager(imageConverterManager); + + /* Reasonable config defaults */ + if(Cr::PluginManager::PluginMetadata* m = importerManager.metadata("GltfImporter")) { + /* Don't need any of this */ + m->configuration().setValue("phongMaterialFallback", false); + m->configuration().setValue("compatibilitySkinningAttributes", false); + } + if(Cr::PluginManager::PluginMetadata* m = imageConverterManager.metadata("BasisImageConverter")) { + // TODO ... actually, do we want it at all? it's crap slow + } + if(Cr::PluginManager::PluginMetadata* m = converterManager.metadata("GltfSceneConverter")) { + m->configuration().setValue("experimentalKhrTextureKtx", true); + // TODO BasisKtxImageConverter but again, do we want basis at all?? it's SLOW + m->configuration().setValue("imageConverter", "KtxImageConverter"); + } + + /* Magnum's OBJ importer is ... well, not great. It'll get replaced + eventually. Assimp is not great either, tho, UFBX would be much nicer. */ + if(importerManager.loadState("ObjImporter") != Cr::PluginManager::LoadState::NotFound) + importerManager.setPreferredPlugins("ObjImporter", {"AssimpImporter"}); + + /* Use StbImageImporter because for it we can override channel count */ + // TODO channel count option on (S)PngImporter itself, some have just 1 channel (ffs) + // TODO what about transparent things? + { + #if 1 + Cr::PluginManager::PluginMetadata* m = importerManager.metadata("StbImageImporter"); + CORRADE_INTERNAL_ASSERT(m); + m->configuration().setValue("forceChannelCount", 3); + importerManager.setPreferredPlugins("PngImporter", {"StbImageImporter"}); + importerManager.setPreferredPlugins("JpegImporter", {"StbImageImporter"}); +#else + importerManager.setPreferredPlugins("PngImporter", {"SpngImporter"}); +#endif + + } + + // TODO configurable? + converter = converterManager.loadAndInstantiate("GltfSceneConverter"); + + /* To prevent the file from being opened by unsuspecting libraries */ + converter->configuration().addValue("extensionUsed", "MAGNUMX_mesh_views"); + converter->configuration().addValue("extensionRequired", "MAGNUMX_mesh_views"); + + /* Create the output directory if it doesn't exist yet */ + CORRADE_INTERNAL_ASSERT(Cr::Utility::Path::make(Cr::Utility::Path::split(output).first())); + + /* Begin file conversion */ + converter->beginFile(output); + converter->setSceneFieldName(SceneFieldMeshViewIndexOffset, "meshViewIndexOffset"); + converter->setSceneFieldName(SceneFieldMeshViewIndexCount, "meshViewIndexCount"); + converter->setSceneFieldName(SceneFieldMeshViewMaterial, "meshViewMaterial"); +} + +Mn::Trade::SceneData CompositorSceneState::finalizeScene() const { + /* Combine the SceneData. In case of glTF the SceneData could be just a view + on the whole memory, with no combining, but this future-proofs it for + dumping into a binary representation */ + // TODO use SceneTools::combine() instead once it's public + Cr::Containers::StridedArrayView1D outputParents; + Cr::Containers::StridedArrayView1D outputTransformations; + Cr::Containers::StridedArrayView1D outputMeshes; + Cr::Containers::ArrayTuple data{ + {Cr::NoInit, parents.size(), outputParents}, + {Cr::NoInit, transformations.size(), outputTransformations}, + {Cr::NoInit, meshes.size(), outputMeshes}, + }; + Cr::Utility::copy(parents, outputParents); + Cr::Utility::copy(transformations, outputTransformations); + Cr::Utility::copy(meshes, outputMeshes); + Mn::Trade::SceneData scene{Mn::Trade::SceneMappingType::UnsignedInt, parents.size(), std::move(data), { + Mn::Trade::SceneFieldData{Mn::Trade::SceneField::Parent, + outputParents.slice(&Parent::mapping), + outputParents.slice(&Parent::parent)}, + Mn::Trade::SceneFieldData{Mn::Trade::SceneField::Transformation, + outputTransformations.slice(&Transformation::mapping), + outputTransformations.slice(&Transformation::transformation)}, + Mn::Trade::SceneFieldData{Mn::Trade::SceneField::Mesh, + outputMeshes.slice(&Mesh::mapping), + outputMeshes.slice(&Mesh::mesh)}, + Mn::Trade::SceneFieldData{SceneFieldMeshViewIndexOffset, + outputMeshes.slice(&Mesh::mapping), + outputMeshes.slice(&Mesh::meshIndexOffset)}, + Mn::Trade::SceneFieldData{SceneFieldMeshViewIndexCount, + outputMeshes.slice(&Mesh::mapping), + outputMeshes.slice(&Mesh::meshIndexCount)}, + Mn::Trade::SceneFieldData{SceneFieldMeshViewMaterial, + outputMeshes.slice(&Mesh::mapping), + outputMeshes.slice(&Mesh::meshMaterial)} + }}; + return scene; +} + +CompositorDataState::CompositorDataState(const Mn::Vector2i& textureAtlasSize): textureAtlasSize{textureAtlasSize} { + constexpr Mn::Color3ub WhitePixel[]{0xffffff_rgb}; +#if 0 + arrayAppend(inputImages, Mn::Trade::ImageData2D{ + Mn::PixelStorage{}.setAlignment(1), + Mn::PixelFormat::RGB8Unorm, {1, 1}, + Mn::Trade::DataFlags{}, WhitePixel}); + +#else + + // TODO figure out why the above doesn't work with zero scale for repeated textures + // TODO abiity to switch between one and the other + Cr::Containers::Array data{Cr::NoInit, std::size_t(textureAtlasSize.product())*sizeof(Mn::Color3ub)}; + Cr::Utility::copy( + // TODO conversion of Vector2i to Size/Stride + Cr::Containers::StridedArrayView2D{WhitePixel, + {std::size_t(textureAtlasSize.y()), std::size_t(textureAtlasSize.x())}, + {0, 0}}, + Cr::Containers::StridedArrayView2D{Cr::Containers::arrayCast(data), {std::size_t(textureAtlasSize.y()), std::size_t(textureAtlasSize.x())}}); + arrayAppend(inputImages, Mn::Trade::ImageData2D{ + Mn::PixelStorage{}.setAlignment(1), + Mn::PixelFormat::RGB8Unorm, textureAtlasSize, + std::move(data)}); +#endif + + arrayAppend(inputMaterials, Mn::Trade::MaterialData{Mn::Trade::MaterialType::PbrMetallicRoughness, { + {Mn::Trade::MaterialAttribute::BaseColorTexture, 0u}, + /* The layer ID and matrix translation get updated based on where the 1x1 + image ends up being in the atlas */ + {Mn::Trade::MaterialAttribute::BaseColorTextureLayer, 0u}, + // {Mn::Trade::MaterialAttribute::BaseColorTextureMatrix, + // Mn::Matrix3::scaling(Mn::Vector2{1.0f}/Mn::Vector2(textureAtlasSize))} + }}); +} + +Mn::Trade::MeshData CompositorDataState::finalizeMesh() const { + /* Target layout for the mesh. So far just normals, no tangents for normal + mapping. */ + // TODO pack normals to 16bit and texcoords to half-floats(gltf extension?) + Mn::Trade::MeshData mesh{Mn::MeshPrimitive::Triangles, nullptr, { + Mn::Trade::MeshAttributeData{Mn::Trade::MeshAttribute::Position, Mn::VertexFormat::Vector3, nullptr}, + Mn::Trade::MeshAttributeData{Mn::Trade::MeshAttribute::Normal, Mn::VertexFormat::Vector3, nullptr}, + Mn::Trade::MeshAttributeData{Mn::Trade::MeshAttribute::TextureCoordinates, Mn::VertexFormat::Vector2, nullptr}, + }}; + // TODO generate normals for meshes that don't have them if there are any + // TODO this should have been gradual to avoid too high peak mem usage + Mn::MeshTools::concatenateInto(mesh, inputMeshes); + + return mesh; +} + +Mn::Trade::ImageData3D CompositorDataState::finalizeImage(const Cr::Containers::ArrayView inputMaterials) const { + /* Just set the limit to the total image count -- that'll make all reference + a single texture */ + return finalizeImage(inputMaterials, inputImages.size()); +} + +Mn::Trade::ImageData3D CompositorDataState::finalizeImage(const Cr::Containers::ArrayView inputMaterials, Mn::Int layerCountLimit) const { + /* Pack input images into an atlas */ + Cr::Containers::Pair> layerCountOffsets = + Mn::TextureTools::atlasArrayPowerOfTwo(textureAtlasSize, stridedArrayView(inputImages).slice(&Mn::Trade::ImageData2D::size)); + + /* A combined 2D array image */ + // TODO document why the alignemnt + Mn::Trade::ImageData3D image{Mn::PixelStorage{}.setAlignment(1), Mn::PixelFormat::RGB8Unorm, + {textureAtlasSize, layerCountOffsets.first()}, + Cr::Containers::Array{Cr::NoInit, std::size_t(textureAtlasSize.product()*layerCountOffsets.first()*3)}, Mn::ImageFlag3D::Array}; + /* Copy the images to their respective locations, calculate waste ratio + during the process */ + std::size_t inputImageArea = 0; + for(std::size_t i = 0; i != inputImages.size(); ++i) { + inputImageArea += inputImages[i].size().product(); + /* This should have been ensured at the import time already, RGBA is for + Basis (sigh) */ + CORRADE_ASSERT( + inputImages[i].format() == Mn::PixelFormat::RGB8Unorm || + inputImages[i].format() == Mn::PixelFormat::RGB8Srgb || + inputImages[i].format() == Mn::PixelFormat::RGBA8Unorm || + inputImages[i].format() == Mn::PixelFormat::RGBA8Srgb, + "Unexpected" << inputImages[i].format() << "in image" << i, (Mn::Trade::ImageData3D{Mn::PixelFormat::R8I, {}, nullptr})); + Cr::Utility::copy(inputImages[i].pixels().prefix({ + std::size_t(inputImages[i].size().y()), + std::size_t(inputImages[i].size().x()), + 3 /* to strip off the alpha channel if present */ + }), + // TODO have implicit conversion of Vector to StridedDimensions, FINALLY + image.mutablePixels()[layerCountOffsets.second()[i].z()].sliceSize({ + std::size_t(layerCountOffsets.second()[i].y()), + std::size_t(layerCountOffsets.second()[i].x()), + 0 + }, { + std::size_t(inputImages[i].size().y()), + std::size_t(inputImages[i].size().x()), + std::size_t(3) + })); + + /* Free the input image right after the copy to reduce peak memory use */ + // inputImages[i] = Mn::Trade::ImageData3D{Mn::PixelFormat::RGB8Unorm, {}, nullptr}; + } + + Mn::Debug{} << inputImages.size() << "images packed to" << layerCountOffsets.first() << "layers," << Cr::Utility::format("{:.2f}", 100.0f - 100.0f*inputImageArea/(textureAtlasSize.product()*layerCountOffsets.first())) << Mn::Debug::nospace << "% area wasted"; + + /* Update layer and offset info in the materials */ + for(Mn::Trade::MaterialData& inputMaterial: inputMaterials) { + Mn::UnsignedInt& texture = inputMaterial.mutableAttribute(Mn::Trade::MaterialAttribute::BaseColorTexture); + Mn::UnsignedInt& layer = inputMaterial.mutableAttribute(Mn::Trade::MaterialAttribute::BaseColorTextureLayer); + const Mn::UnsignedInt imageId = layer; + + // TODO the separation to textures would probably make more sense done + // spatially, i.e. meshes rendered together being in the same layer .. but + // who cares for now + texture = layerCountOffsets.second()[imageId].z()/layerCountLimit; + layer = layerCountOffsets.second()[imageId].z()%layerCountLimit; + + /* If the material has a texture matrix (textures that are same as atlas + layer size don't have it), update the offset there */ + if(const Cr::Containers::Optional baseColorTextureMatrixAttributeId = inputMaterial.findAttributeId(Mn::Trade::MaterialAttribute::BaseColorTextureMatrix)) { + Mn::Matrix3& matrix = inputMaterial.mutableAttribute(*baseColorTextureMatrixAttributeId); + matrix = Mn::Matrix3::translation(Mn::Vector2{layerCountOffsets.second()[imageId].xy()}/Mn::Vector2{textureAtlasSize})*matrix; + } + } + + return image; +} + +Mn::Trade::TextureData CompositorDataState::finalizeTexture() const { + return Mn::Trade::TextureData{Mn::Trade::TextureType::Texture2DArray, + Mn::SamplerFilter::Linear, Mn::SamplerFilter::Linear, Mn::SamplerMipmap::Linear, + Mn::SamplerWrapping::Repeat, 0}; +} + +} diff --git a/src/utils/compositor/CompositorState.h b/src/utils/compositor/CompositorState.h new file mode 100644 index 0000000000..fd438e74ee --- /dev/null +++ b/src/utils/compositor/CompositorState.h @@ -0,0 +1,99 @@ +// Copyright (c) Meta Platforms, Inc. and its affiliates. +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +#ifndef ESP_COMPOSITOR_H_ +#define ESP_COMPOSITOR_H_ + +#include +#include +#include +#include +#include +#include +#include + +namespace esp { + +// TODO make these builtin +constexpr Magnum::Trade::SceneField SceneFieldMeshViewIndexOffset = Magnum::Trade::sceneFieldCustom(0); +constexpr Magnum::Trade::SceneField SceneFieldMeshViewIndexCount = Magnum::Trade::sceneFieldCustom(1); +constexpr Magnum::Trade::SceneField SceneFieldMeshViewMaterial = Magnum::Trade::sceneFieldCustom(2); + +struct CompositorState { + explicit CompositorState(Corrade::Containers::StringView output); + + Corrade::PluginManager::Manager importerManager; + Corrade::PluginManager::Manager imageConverterManager; + Corrade::PluginManager::Manager converterManager; + + Corrade::Containers::Pointer converter; +}; + +/* Parent, present for all objects */ +struct Parent { + Magnum::UnsignedInt mapping; + Magnum::Int parent; +}; + +/* Transformation, present only for nested objects */ +struct Transformation { + Magnum::UnsignedInt mapping; + Magnum::Matrix4 transformation; +}; + +/* Mesh assignment, present for all objects except a "template root" that + contains multiple meshes as children */ +struct Mesh { + Magnum::UnsignedInt mapping; + Magnum::UnsignedInt mesh; /* always 0 in this case */ + Magnum::UnsignedInt meshIndexOffset; + Magnum::UnsignedInt meshIndexCount; + Magnum::Int meshMaterial; +}; + +struct CompositorSceneState { + Magnum::Trade::SceneData finalizeScene() const; + + Corrade::Containers::Array parents; + Corrade::Containers::Array transformations; + Corrade::Containers::Array meshes; +}; + +/* Meshes, textures & materials get collected, then joined / packed, then added + to the converter. One CompositorDataState is one mesh/image/texture in the + output. */ +struct CompositorDataState { + explicit CompositorDataState(const Magnum::Vector2i& textureAtlasSize); + + Magnum::Trade::MeshData finalizeMesh() const; + + Magnum::Trade::ImageData3D finalizeImage(Corrade::Containers::ArrayView inputMaterials) const; + Magnum::Trade::ImageData3D finalizeImage(Corrade::Containers::ArrayView inputMaterials, Magnum::Int layerCountLimit) const; + + Magnum::Trade::TextureData finalizeTexture() const; + + Magnum::Vector2i textureAtlasSize; + + Corrade::Containers::Array inputMeshes; + /* There's one implicit 1x1 white image for textureless materials */ + // TODO what if nothing needs it? it may cause an extra texture layer with + // nothing but a single pixel, which is wasteful -- maybe just drop it if + // not referenced? or add it only if needed and remember the index? + Corrade::Containers::Array inputImages; + + /* As textures get packed into an atlas, the materials will need to be + updated with final layer IDs and offsets. Store them temporarily in an + array, using the imported image index and zero offset as placeholders. + + There's one implicit all-white material for materialless meshes. */ + // TODO what if everything has a material?? + Corrade::Containers::Array inputMaterials; + + /* Index offset for currently added mesh */ + Magnum::UnsignedInt indexOffset = 0; +}; + +} + +#endif diff --git a/src/utils/compositor/compositor-fetch.cpp b/src/utils/compositor/compositor-fetch.cpp new file mode 100644 index 0000000000..2ce2451f6c --- /dev/null +++ b/src/utils/compositor/compositor-fetch.cpp @@ -0,0 +1,297 @@ +// Copyright (c) Meta Platforms, Inc. and its affiliates. +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CompositorState.h" + +namespace Cr = Corrade; +namespace Mn = Magnum; +using namespace Cr::Containers::Literals; +using namespace Mn::Math::Literals; + +int main(int argc, char** argv) { + Cr::Utility::Arguments args; + args.addArgument("input").setHelp("input", "input file prefix") + .addArgument("output").setHelp("output", "output file") + .parse(argc, argv); + + constexpr Mn::Vector2i TextureAtlasSize{512}; + esp::CompositorState s{args.value("output")}; + esp::CompositorSceneState ss; + esp::CompositorDataState ds{TextureAtlasSize}; + Cr::Containers::Pointer importer = s.importerManager.loadAndInstantiate("AnySceneImporter"); + CORRADE_INTERNAL_ASSERT(importer); + + Cr::Containers::Pointer imageResizer = s.imageConverterManager.loadAndInstantiate("StbResizeImageConverter"); + CORRADE_INTERNAL_ASSERT(imageResizer); + imageResizer->configuration().setValue("size", Mn::Vector2i{512}); + imageResizer->configuration().setValue("upsample", false); + + std::unordered_map> uniqueMeshes; + const auto importSingleMesh = [&]( + Cr::Containers::StringView filename, + Cr::Containers::StringView name, + const Mn::Color4& color + ) { + CORRADE_INTERNAL_ASSERT_OUTPUT(importer->openFile(filename)); + CORRADE_INTERNAL_ASSERT(importer->meshCount() == 1); + + esp::Parent root; + root.mapping = ss.parents.size(); + root.parent = -1; + arrayAppend(ss.parents, root); + s.converter->setObjectName(root.mapping, name); + + esp::Parent o; + o.mapping = ss.parents.size(); + o.parent = root.mapping; + arrayAppend(ss.parents, o); + + Cr::Containers::Optional mesh = importer->mesh(0); + CORRADE_INTERNAL_ASSERT(mesh && mesh->primitive() == Mn::MeshPrimitive::Triangles); + /* Assuming STL files, which are not indexed and thus with many duplicate + vertices. Create an index buffer by deduplicating them. */ + // TODO possibly useful to filter away and recreate normals also + // --> disable perFaceToPerVertex for stlimporter? + mesh = Mn::MeshTools::removeDuplicates(*mesh); + + esp::Mesh m; + m.mapping = o.mapping; + m.mesh = 0; + m.meshIndexCount = mesh->indexCount(); + m.meshMaterial = ds.inputMaterials.size(); + m.meshIndexOffset = ds.indexOffset; + ds.indexOffset += mesh->indexCount()*sizeof(Mn::UnsignedInt); + + arrayAppend(ds.inputMeshes, *std::move(mesh)); + + /* Add an empty colored material */ + arrayAppend(ds.inputMaterials, Mn::Trade::MaterialData{Mn::Trade::MaterialType::PbrMetallicRoughness, { + {Mn::Trade::MaterialAttribute::BaseColor, color}, + {Mn::Trade::MaterialAttribute::BaseColorTexture, 0u}, + {Mn::Trade::MaterialAttribute::BaseColorTextureLayer, 0u}, + {Mn::Trade::MaterialAttribute::BaseColorTextureMatrix, + Mn::Matrix3::scaling(Mn::Vector2{1}/Mn::Vector2(TextureAtlasSize))} + }}); + + arrayAppend(ss.meshes, m); + }; + + const auto import = [&]( + Cr::Containers::StringView filename, + Cr::Containers::StringView name) { + CORRADE_INTERNAL_ASSERT_OUTPUT(importer->openFile(filename)); + CORRADE_INTERNAL_ASSERT(importer->sceneCount() == 1); + + Cr::Containers::Optional scene = importer->scene(0); + CORRADE_INTERNAL_ASSERT(scene); + + /* Top-level object, parent of the others */ + esp::Parent root; + root.mapping = ss.parents.size(); + root.parent = -1; + arrayAppend(ss.parents, root); + s.converter->setObjectName(root.mapping, name); + + /* Assuming materials are shared among meshes, remember the ID of already + imported materials */ + Cr::Containers::Array> importedMaterialIds{importer->materialCount()}; + + /* Node mesh/material assignments. Each entry will be one child of the + top-level object. */ + for(Cr::Containers::Triple transformationMeshMaterial: Mn::SceneTools::flattenMeshHierarchy3D(*scene)) { + Cr::Containers::Optional mesh = importer->mesh(transformationMeshMaterial.first()); + // TODO drop the names, useless + Cr::Containers::String meshName = importer->meshName(transformationMeshMaterial.first()); + + /* Skip non-triangle meshes */ + if(mesh->primitive() != Mn::MeshPrimitive::Triangles && + mesh->primitive() != Mn::MeshPrimitive::TriangleFan && + mesh->primitive() != Mn::MeshPrimitive::TriangleStrip) { + Mn::Warning{} << "Mesh" << meshName << "in" << Cr::Utility::Path::split(filename).second() << "is" << mesh->primitive() << Mn::Debug::nospace << ", skipping"; + continue; + } + + CORRADE_INTERNAL_ASSERT(mesh && mesh->isIndexed() && mesh->primitive() == Mn::MeshPrimitive::Triangles); + + esp::Parent o; + o.mapping = ss.parents.size(); + o.parent = root.mapping; + arrayAppend(ss.parents, o); + arrayAppend(ss.transformations, Cr::InPlaceInit, + o.mapping, + transformationMeshMaterial.third()); + /* Save the nested object name as well, for debugging purposes. It'll be + duplicated among different scenes but that's no problem. */ + s.converter->setObjectName(o.mapping, meshName); + + esp::Mesh m; + m.mapping = o.mapping; + m.mesh = 0; + m.meshIndexCount = mesh->indexCount(); + + Mn::Debug{} << "New mesh" << meshName << "in" << Cr::Utility::Path::split(filename).second(); + uniqueMeshes.insert({std::move(meshName), {ds.indexOffset, mesh->indexCount()}}); + + m.meshIndexOffset = ds.indexOffset; + ds.indexOffset += mesh->indexCount()*4; // TODO ints hardcoded + + arrayAppend(ds.inputMeshes, *std::move(mesh)); + + /* If the material is already parsed, reuse its ID */ + // TODO drop material deduplication, useless here + if(const Cr::Containers::Optional materialId = importedMaterialIds[transformationMeshMaterial.second()]) { + m.meshMaterial = *materialId; + + /* Otherwise parse it. If textured, extract the image as well. */ + } else { + Cr::Containers::Optional material = importer->material(transformationMeshMaterial.second()); + CORRADE_INTERNAL_ASSERT(material); + + /* Convert to a PBR metallic/roughness if it's a Phong material */ + if(material->types() & Mn::Trade::MaterialType::Phong) + material = Mn::MaterialTools::phongToPbrMetallicRoughness(*material, + Mn::MaterialTools::PhongToPbrMetallicRoughnessFlag::DropUnconvertableAttributes); + + /* Not calling releaseAttributeData() yet either, as we need to ask + hasAttribute() first */ + Cr::Containers::Array attributes; + if(const Cr::Containers::Optional baseColorTextureAttributeId = material->findAttributeId(Mn::Trade::MaterialAttribute::BaseColorTexture)) { + Mn::Debug{} << "New textured material for" << meshName << "in" << Cr::Utility::Path::split(filename).second(); + + Mn::UnsignedInt& textureId = material->mutableAttribute(*baseColorTextureAttributeId); + + Cr::Containers::Optional texture = importer->texture(textureId); + CORRADE_INTERNAL_ASSERT(texture && texture->type() == Mn::Trade::TextureType::Texture2D); + + // TODO textures referencing the same image ID -> deduplicate; + Cr::Containers::Optional image = importer->image2D(texture->image()); + + // TODO hash & deduplicate image data + + /* Resize the image if it's too big */ + CORRADE_INTERNAL_ASSERT(image); + image = imageResizer->convert(*image); + CORRADE_INTERNAL_ASSERT(image); + + // TODO detection of single-color images here? + + /* Add texture scaling if the image is smaller */ + if((image->size() < TextureAtlasSize).any()) { + const Mn::Matrix3 matrix = Mn::Matrix3::scaling(Mn::Vector2(image->size())/Mn::Vector2(TextureAtlasSize)); + // TODO MaterialTools::merge() instead!!! + if(const Cr::Containers::Optional baseColorTextureMatrixAttributeId = material->findAttributeId(Mn::Trade::MaterialAttribute::BaseColorTextureMatrix)) { + Mn::Matrix3& existingMatrix = + material->mutableAttribute(*baseColorTextureMatrixAttributeId); + existingMatrix = matrix*existingMatrix; + } else { + arrayAppend(attributes, Mn::InPlaceInit, Mn::Trade::MaterialAttribute::BaseColorTextureMatrix, matrix); + } + } + + /* Patch the material to use the zero texture but add a layer as + well, referencing the just-added image */ + textureId = 0; + arrayAppend(attributes, Cr::InPlaceInit, Mn::Trade::MaterialAttribute::BaseColorTextureLayer, Mn::UnsignedInt(ds.inputImages.size())); + + arrayAppend(ds.inputImages, *std::move(image)); + + /* Otherwise make it reference the first image, which is a 1x1 white + pixel */ + } else { + Mn::Debug{} << "New untextured material for" << meshName << "in" << Cr::Utility::Path::split(filename).second(); + + arrayAppend(attributes, { + {Mn::Trade::MaterialAttribute::BaseColorTexture, 0u}, + {Mn::Trade::MaterialAttribute::BaseColorTextureLayer, 0u}, + {Mn::Trade::MaterialAttribute::BaseColorTextureMatrix, + Mn::Matrix3::scaling(Mn::Vector2{1}/Mn::Vector2(TextureAtlasSize))} + }); + } + + // TODO MaterialTools::merge() instead of this!!!! + CORRADE_INTERNAL_ASSERT(material->layerCount() == 1); + arrayAppend(attributes, material->attributeData()); + + material = Mn::Trade::MaterialData{material->types(), std::move(attributes)}; + + importedMaterialIds[transformationMeshMaterial.second()] = m.meshMaterial = ds.inputMaterials.size(); + arrayAppend(ds.inputMaterials, *std::move(material)); + } + + arrayAppend(ss.meshes, m); + } + }; + + Cr::Containers::String fetchPath = Cr::Utility::Path::join(args.value("input"), "hab_fetch_v1.0/meshes/"); + // TODO fetch (HAHA) the colors automagically from the URDF, this is insufferable + for(auto i: std::initializer_list>{ + {"bellows_link.STL", {0.0f, 0.0f, 0.0f}}, + {"estop_link.STL", {0.8f, 0.0f, 0.0f}}, + {"laser_link.STL", {0.792156862745098f, 0.819607843137255f, 0.933333333333333f}}, + {"l_gripper_finger_link_opt.stl", {0.356f, 0.361f, 0.376f}}, + {"l_gripper_finger_link.STL", {0.356f, 0.361f, 0.376f}}, + {"l_wheel_link.STL", {0.086f, 0.506f, 0.767f}}, + {"r_gripper_finger_link_opt.stl", {0.356f, 0.361f, 0.376f}}, + {"r_gripper_finger_link.STL", {0.356f, 0.361f, 0.376f}}, + {"r_wheel_link.STL", {0.086f, 0.506f, 0.767f}}, + }) + importSingleMesh(Cr::Utility::Path::join(fetchPath, i.first()), "./data/robots/hab_fetch/robots/../meshes/"_s + i.first(), i.second()); + + // TODO remove! + // importSingleMesh(Cr::Utility::Path::join(args.value("input"), "cubeSolid.gltf"), "./data/cubeSolid.gltf", 0xffffff_rgbf); + + for(const char* name: { + "base_link.dae", + "elbow_flex_link.dae", + "estop_link.dae", + "forearm_roll_link.dae", + "gripper_link.dae", + "head_pan_link.dae", + "head_tilt_link.dae", + "shoulder_lift_link.dae", + "shoulder_pan_link.dae", + "suctionCup.glb", + "torso_fixed_link.dae", + "torso_lift_link.dae", + "upperarm_roll_link.dae", + "wrist_flex_link.dae", + "wrist_roll_link.dae", + }) + import(Cr::Utility::Path::join(fetchPath, name), "./data/robots/hab_fetch/robots/../meshes/"_s + name); + + // TODO some mesh optimization first? + CORRADE_INTERNAL_ASSERT(s.converter->add(ds.finalizeMesh())); + + // TODO dxt compression first? + CORRADE_INTERNAL_ASSERT(s.converter->add(ds.finalizeImage(ds.inputMaterials))); + CORRADE_INTERNAL_ASSERT(s.converter->add(ds.finalizeTexture())); + + // TODO deduplication first? + for(const Mn::Trade::MaterialData& i: ds.inputMaterials) + CORRADE_INTERNAL_ASSERT(s.converter->add(i)); + + CORRADE_INTERNAL_ASSERT(s.converter->add(ss.finalizeScene())); + + return s.converter->endFile() ? 0 : 1; +} diff --git a/src/utils/compositor/compositor-floorplanner.cpp b/src/utils/compositor/compositor-floorplanner.cpp new file mode 100644 index 0000000000..8421e49227 --- /dev/null +++ b/src/utils/compositor/compositor-floorplanner.cpp @@ -0,0 +1,531 @@ +// Copyright (c) Meta Platforms, Inc. and its affiliates. +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CompositorState.h" + +namespace Cr = Corrade; +namespace Mn = Magnum; +using namespace Cr::Containers::Literals; +using namespace Mn::Math::Literals; + +namespace { + +Cr::Utility::Sha1::Digest imageChecksum(const Mn::Trade::ImageData2D& image) { + union Metadata { + explicit Metadata() {} + struct { + Mn::Vector2i size; + Mn::PixelFormat format; + } data; + char bytes[12]; + } metadata; + metadata.data.size = image.size(); + metadata.data.format = image.format(); + + Cr::Utility::Sha1 hasher; + hasher << Cr::Containers::ArrayView{metadata.bytes}; + + for(Cr::Containers::StridedArrayView2D row: image.pixels()) + hasher << row.asContiguous(); + + return hasher.digest(); +} + +} + +int main(int argc, char** argv) { + Cr::Utility::Arguments args; + args.addArgument("input").setHelp("input", "input file prefix") + .addArgument("output").setHelp("output", "output file") + .parse(argc, argv); + + // TODO make higher once we don't need repeat for all + constexpr Mn::Vector2i TextureAtlasSize{128}; + constexpr Mn::Int ImageLayerCountLimit = 2048; + esp::CompositorState s{args.value("output")}; + esp::CompositorSceneState ss; + esp::CompositorDataState ds{TextureAtlasSize}; + Cr::Containers::Pointer importer = s.importerManager.loadAndInstantiate("GltfImporter"); + CORRADE_INTERNAL_ASSERT(importer); + + Cr::Containers::Pointer imageResizer = s.imageConverterManager.loadAndInstantiate("StbResizeImageConverter"); + CORRADE_INTERNAL_ASSERT(imageResizer); + // TODO some images need a repeat, so (up)scaling them all to 256x256 to make + // them render properly + // TODO split itno two textures, one a larger atlas with non-repeated and one + // a smaller with repeated + imageResizer->configuration().setValue("size", TextureAtlasSize); + // TODO have a processing step to figure out which images need repeat instead + // of upsampling all + imageResizer->configuration().setValue("upsample", true); + + Cr::Containers::Pointer imageCompressor = s.imageConverterManager.loadAndInstantiate("StbDxtImageConverter"); + CORRADE_INTERNAL_ASSERT(imageCompressor); + imageCompressor->configuration().setValue("highQuality", true); + + Cr::Containers::Pointer meshOptimizer = s.converterManager.loadAndInstantiate("MeshOptimizerSceneConverter"); + CORRADE_INTERNAL_ASSERT(meshOptimizer); + /* Sloppy optimization is enabled only after all arch files get processed, + for those it'd cause a mess */ + meshOptimizer->configuration().setValue("simplify", true); + meshOptimizer->configuration().setValue("simplifyTargetError", 1.0e-1); + + // constexpr Mn::Float MeshDensityThreshold = 42*42*4; + constexpr Mn::Float MeshDensityThreshold = 1000; + + // TODO use something else than a std::string to represent digests + std::unordered_map uniqueImages; + + Mn::UnsignedInt inputImageCount = 0; + + const auto import = [&]( + Cr::Containers::StringView filename, + Cr::Containers::StringView name) { + CORRADE_INTERNAL_ASSERT_OUTPUT(importer->openFile(filename)); + CORRADE_INTERNAL_ASSERT(importer->sceneCount() == 1); + + Mn::Debug{} << "Importing" << Cr::Utility::Path::split(filename).second() << "with" << importer->meshCount() << "meshes and" << importer->image2DCount() << "images"; + + Cr::Containers::Optional scene = importer->scene(0); + CORRADE_INTERNAL_ASSERT(scene); + + /* Top-level object, parent of the others */ + esp::Parent root; + root.mapping = ss.parents.size(); + root.parent = -1; + arrayAppend(ss.parents, root); + s.converter->setObjectName(root.mapping, name); + + /* Assuming materials are shared among meshes, remember the ID of already + imported materials */ + Cr::Containers::Array> importedMaterialIds{importer->materialCount()}; + + Cr::Containers::Array>> importedMeshes{Cr::ValueInit, importer->meshCount()}; + + /* Node mesh/material assignments. Each entry will be one child of the + top-level object. */ + for(Cr::Containers::Triple transformationMeshMaterial: Mn::SceneTools::flattenMeshHierarchy3D(*scene)) { + // TODO drop the names? + Cr::Containers::String meshName = importer->meshName(transformationMeshMaterial.first()); + + /* Process the mesh, if not imported yet */ + if(!importedMeshes[transformationMeshMaterial.first()]) { + Cr::Containers::Optional mesh = importer->mesh(transformationMeshMaterial.first()); + CORRADE_INTERNAL_ASSERT(mesh); + /* Skip non-triangle meshes */ + if(mesh->primitive() != Mn::MeshPrimitive::Triangles && + mesh->primitive() != Mn::MeshPrimitive::TriangleFan && + mesh->primitive() != Mn::MeshPrimitive::TriangleStrip) { + Mn::Warning{} << "Mesh" << meshName << "in" << Cr::Utility::Path::split(filename).second() << "is" << mesh->primitive() << Mn::Debug::nospace << ", skipping"; + continue; + } + + /* Needed by some furniture */ + if(!mesh->isIndexed()) + mesh = Mn::MeshTools::removeDuplicates(*mesh); + CORRADE_INTERNAL_ASSERT(mesh->primitive() == Mn::MeshPrimitive::Triangles); + + /* Calculate total triangle area / count ratio and simplify if it's too + much */ + { + // TODO do w/o allocation if possible? + const Cr::Containers::Array indices = mesh->indicesAsArray(); + const Cr::Containers::Array positions = mesh->positions3DAsArray(); + const std::size_t triangleCount = indices.size()/3; + Mn::Float totalArea = 0.0f; + for(std::size_t i = 0; i != triangleCount; ++i) { + const Mn::Vector3 a = positions[indices[i*3 + 0]]; + const Mn::Vector3 b = positions[indices[i*3 + 1]]; + const Mn::Vector3 c = positions[indices[i*3 + 2]]; + + /* Triangle area is half of the cross product of its two vectors */ + totalArea += Mn::Math::cross(b - a, c - a).length()*0.5f; + } + + const Mn::Float target = MeshDensityThreshold/(triangleCount/totalArea); + if(target < 1.0f) { + meshOptimizer->configuration().setValue("simplifyTargetIndexCountThreshold", target); + Mn::UnsignedInt vertexCount = mesh->vertexCount(); + mesh = meshOptimizer->convert(*mesh); + Mn::Debug{} << "Decimated mesh" << transformationMeshMaterial.first() << "to" << Cr::Utility::format("{:.1f}%", mesh->vertexCount()*100.0f/vertexCount); + } + // TODO if >= 1, do at least an optimization (as a subsequent composite + // version, so it can be seen how it improves the overall perf) + } + + importedMeshes[transformationMeshMaterial.first()] = Cr::Containers::pair(ds.indexOffset, mesh->indexCount()); + + ds.indexOffset += mesh->indexCount()*4; // TODO ints hardcoded + + arrayAppend(ds.inputMeshes, *std::move(mesh)); + } + + esp::Parent o; + o.mapping = ss.parents.size(); + o.parent = root.mapping; + arrayAppend(ss.parents, o); + arrayAppend(ss.transformations, Cr::InPlaceInit, + o.mapping, + transformationMeshMaterial.third()); + /* Save the nested object name as well, for debugging purposes. It'll be + duplicated among different scenes but that's no problem. */ + // TODO not doing this, too much noise that's obscuring errors in the top-level names + // s.converter->setObjectName(o.mapping, meshName); + + esp::Mesh m; + m.mapping = o.mapping; + m.mesh = 0; + m.meshIndexOffset = importedMeshes[transformationMeshMaterial.first()]->first(); + m.meshIndexCount = importedMeshes[transformationMeshMaterial.first()]->second(); + + /* If the material is already parsed, reuse its ID */ + if(const Cr::Containers::Optional materialId = importedMaterialIds[transformationMeshMaterial.second()]) { + m.meshMaterial = *materialId; + + /* Otherwise parse it. If textured, extract the image as well. */ + } else { + Cr::Containers::Optional material = importer->material(transformationMeshMaterial.second()); + CORRADE_INTERNAL_ASSERT(material); + + // TODO for filtering, add everything there + // Cr::Containers::BitArray attributesToKeep{Cr::DirectInit, material->attributeData().size(), true}; + + /* Not calling releaseAttributeData() yet either, as we need to ask + hasAttribute() first TODO drop, merge() */ + Cr::Containers::Array attributes; + if(const Cr::Containers::Optional baseColorTextureAttributeId = material->findAttributeId(Mn::Trade::MaterialAttribute::BaseColorTexture)) { + // Mn::Debug{} << "New textured material for" << meshName << "in" << Cr::Utility::Path::split(filename).second(); + + Mn::UnsignedInt& textureId = material->mutableAttribute(*baseColorTextureAttributeId); + + Cr::Containers::Optional texture = importer->texture(textureId); + CORRADE_INTERNAL_ASSERT(texture && texture->type() == Mn::Trade::TextureType::Texture2D); + + ++inputImageCount; + Cr::Containers::Optional image = importer->image2D(texture->image()); + CORRADE_INTERNAL_ASSERT(image); + + /* Check if we already have the same image contents */ + std::string sha1 = imageChecksum(*image).hexString(); + + auto found = uniqueImages.find(sha1); + if(found == uniqueImages.end()) { + image = imageResizer->convert(*image); + CORRADE_INTERNAL_ASSERT(image); + + found = uniqueImages.emplace(sha1, ds.inputImages.size()).first; + arrayAppend(ds.inputImages, *std::move(image)); + } else { + Mn::Debug{} << "Reusing existing image #" << Mn::Debug::nospace << found->second << sha1; + } + + const Mn::UnsignedInt uniqueImageId = found->second; + const Mn::Trade::ImageData2D& uniqueImage = ds.inputImages[found->second]; + + /* Resize the image if it's too big */ + + // CORRADE_ASSERT((image->size() & (image->size() - Mn::Vector2i{1})).isZero(), + // "Image not power-of-two:" << image->size(), ); + + // TODO detection of single-color images here? + + /* Add texture scaling if the image is smaller */ + if((uniqueImage.size() < TextureAtlasSize).any()) { + // TODO this shouldn't be used right now + CORRADE_ASSERT_UNREACHABLE("This shouldn't be used right now, all images have the same size", ); + + const Mn::Matrix3 matrix = Mn::Matrix3::scaling(Mn::Vector2(uniqueImage.size())/Mn::Vector2(TextureAtlasSize)); + // TODO MaterialTools::merge() instead!!! + if(const Cr::Containers::Optional baseColorTextureMatrixAttributeId = material->findAttributeId(Mn::Trade::MaterialAttribute::BaseColorTextureMatrix)) { + Mn::Matrix3& existingMatrix = + material->mutableAttribute(*baseColorTextureMatrixAttributeId); + existingMatrix = matrix*existingMatrix; + } else { + arrayAppend(attributes, Mn::InPlaceInit, Mn::Trade::MaterialAttribute::BaseColorTextureMatrix, matrix); + } + } + + /* Patch the material to use the zero texture but add a layer as + well, referencing the just-added image */ + textureId = 0; + arrayAppend(attributes, Cr::InPlaceInit, Mn::Trade::MaterialAttribute::BaseColorTextureLayer, uniqueImageId); + + #if 1 + /* Otherwise make it reference the first image, which is all plain + white */ + } else { + // Mn::Debug{} << "New untextured material for" << meshName << "in" << Cr::Utility::Path::split(filename).second(); + + arrayAppend(attributes, { + Mn::Trade::MaterialAttributeData{Mn::Trade::MaterialAttribute::BaseColorTexture, 0u}, + Mn::Trade::MaterialAttributeData{Mn::Trade::MaterialAttribute::BaseColorTextureLayer, 0u}, + // Mn::Trade::MaterialAttributeData{Mn::Trade::MaterialAttribute::BaseColorTextureMatrix, + // Mn::Matrix3::scaling(Mn::Vector2{0.0f})} + }); + } + #else + /* Otherwise add a 1x1 image containing the base color and make the + material use it as a texture instead. Set the texture matrix scaling + to zero to make it work for repeated textures as well. */ + } else { + // // Mn::Debug{} << "New untextured material for" << meshName << "in" << Cr::Utility::Path::split(filename).second() << ; + + // TODO this loses alpha + Mn::Trade::ImageData2D image{Mn::PixelFormat::RGB8Unorm, {1, 1}, Cr::Containers::Array{Cr::NoInit, 4}}; + if(const Cr::Containers::Optional baseColorId = material->findAttributeId(Mn::Trade::MaterialAttribute::BaseColor)) { + image.mutablePixels()[0][0] = Mn::Math::pack(material->attribute(*baseColorId).rgb()); + attributesToKeep.reset(*baseColorId); + } else + image.mutablePixels()[0][0] = 0xffffff_rgb; + + // TODO deduplicate those similarly to above + const Mn::UnsignedInt uniqueImageId = ds.inputImages.size(); + arrayAppend(ds.inputImages, std::move(image)); + arrayAppend(attributes, { + {Mn::Trade::MaterialAttribute::BaseColorTexture, 0u}, + {Mn::Trade::MaterialAttribute::BaseColorTextureLayer, uniqueImageId}, + {Mn::Trade::MaterialAttribute::BaseColorTextureMatrix, Mn::Matrix3::scaling(Mn::Vector2{0.0f})} + }); + + } + + /* And filter away the base color attribute */ + // TODO and others + material = Mn::MaterialTools::filterAttributes(*material, attributesToKeep); + #endif + + // TODO MaterialTools::merge() instead of this!!!! + // CORRADE_INTERNAL_ASSERT(material->layerCount() == 1); // TODO is broken for furniture!! + // arrayAppend(attributes, material->attributeData().prefix(material->attributeCount(0))); + + material = Mn::MaterialTools::merge(*material, Mn::Trade::MaterialData{{}, Mn::Trade::DataFlags{}, attributes}); + + // material = Mn::Trade::MaterialData{material->types(), std::move(attributes)}; + + importedMaterialIds[transformationMeshMaterial.second()] = m.meshMaterial = ds.inputMaterials.size(); + arrayAppend(ds.inputMaterials, *std::move(material)); + } + + arrayAppend(ss.meshes, m); + } + }; + + #if 1 + // TODO use the "really no doors" instead + Cr::Containers::String archPath = Cr::Utility::Path::join(args.value("input"), "glb-arch-only"); + Cr::Containers::Optional> archFiles = Cr::Utility::Path::list(archPath, Cr::Utility::Path::ListFlag::SkipDirectories|Cr::Utility::Path::ListFlag::SortAscending); + for(Cr::Containers::String name: (*archFiles)) {//->prefix(10)) { // TODO remove the limit + CORRADE_INTERNAL_ASSERT(name.hasSuffix(".glb"_s)); + // TODO what's the path prefix used by Habitat / gfx-replay? + import(Cr::Utility::Path::join(archPath, name), "../stages/"_s + name); + } + #elif 0 + // TODO this was used back when SSBOs weren't a thing + /* https://cvmlp.slack.com/archives/C0460NTKM4G/p1675443726846979?thread_ts=1675358050.326359&cid=C0460NTKM4G */ + for(const char* name: { // TODO use all instead + "103997478_171030525.glb", + "108736611_177263226.glb", + "105515175_173104107.glb", + "107734080_175999881.glb", + "103997562_171030642.glb", + "108294624_176710203.glb", + "108294816_176710461.glb", + "108736722_177263382.glb", + "108294465_176709960.glb", + "106879044_174887172.glb" + }) { + // TODO what's the path prefix used by Habitat / gfx-replay? + import(Cr::Utility::Path::join(archPath, name), "../stages/"_s + name); + } + #endif + + /* Enabled for furniture only, for the arch files it makes a mess */ + meshOptimizer->configuration().setValue("simplifySloppy", true); + + #if 1 + // TODO use the original below once we have time to wait for slow resizes + #if 1 + Cr::Containers::String furniturePath = Cr::Utility::Path::join(args.value("input"), "object-noq-tex256"); + Cr::Containers::Optional> furnitureFiles = Cr::Utility::Path::list(furniturePath, Cr::Utility::Path::ListFlag::SkipDirectories|Cr::Utility::Path::ListFlag::SortAscending); + for(Cr::Containers::StringView name: *furnitureFiles) { + CORRADE_INTERNAL_ASSERT(name.hasSuffix(".glb"_s)); + /* This one contains also the openings, skip them */ + if(name.size() < 40) + continue; + // TODO what's the path prefix used by Habitat / gfx-replay? + import(Cr::Utility::Path::join(furniturePath, name), + Cr::Utility::format("../objects/{}/{}.glb", name.prefix(1), name.prefix(40))); + } + #elif 0 + // Cr::Containers::String furniturePath = Cr::Utility::Path::join(args.value("input"), "furniture-original"); + Cr::Containers::Optional> furnitureFiles = Cr::Utility::Path::list(furniturePath, Cr::Utility::Path::ListFlag::SkipDirectories|Cr::Utility::Path::ListFlag::SortAscending); + for(Cr::Containers::StringView name: *furnitureFiles) { + CORRADE_INTERNAL_ASSERT(name.hasSuffix(".glb"_s)); + // TODO what's the path prefix used by Habitat / gfx-replay? + import(Cr::Utility::Path::join(furniturePath, name), + Cr::Utility::format("../objects/{}/{}", name.prefix(1), name)); + } + #endif + + // TODO these generate an awful lot of data because most of them contain the + // meshes an insane amount of times, such as + // 2a6fa282275e6ddb1fc35ebdc6a9ef91ae93a8ae_part_16.glb containing 934 + // meshes for a plant that should have just SEVEN + #if 0 + // duplicated for no reason + Cr::Containers::String furnitureDecomposedPath = Cr::Utility::Path::join(args.value("input"), "decomposed"); // TODO ugh + Cr::Containers::Optional> furnitureDecomposedDirs = Cr::Utility::Path::list(furnitureDecomposedPath, Cr::Utility::Path::ListFlag::SkipDotAndDotDot|Cr::Utility::Path::ListFlag::SkipFiles|Cr::Utility::Path::ListFlag::SortAscending); + { + for(Cr::Containers::StringView dir: *furnitureDecomposedDirs) { + Cr::Containers::Optional> furnitureDecomposedFiles = Cr::Utility::Path::list(Cr::Utility::Path::join(furnitureDecomposedPath, dir), Cr::Utility::Path::ListFlag::SkipDirectories|Cr::Utility::Path::ListFlag::SortAscending); + for(Cr::Containers::StringView name: *furnitureDecomposedFiles) { + if(name.contains("dedup")) + continue; + if(!name.hasSuffix(".glb"_s)) // TODO UGH + continue; + // TODO what's the path prefix used by Habitat / gfx-replay? + import(Cr::Utility::Path::join({furnitureDecomposedPath, dir, name}), + Cr::Utility::format("../objects/decomposed/{}/{}", dir, name)); + } + } + } + #endif + + #if 1 + Cr::Containers::String furnitureOpeningsPath = Cr::Utility::Path::join(args.value("input"), "openings-variants"); // TODO ugh + Cr::Containers::Optional> furnitureOpeningsFiles = Cr::Utility::Path::list(furnitureOpeningsPath, Cr::Utility::Path::ListFlag::SkipDirectories|Cr::Utility::Path::ListFlag::SortAscending); + for(Cr::Containers::StringView name: *furnitureOpeningsFiles) { + CORRADE_INTERNAL_ASSERT(name.hasSuffix(".glb"_s)); + // TODO what's the path prefix used by Habitat / gfx-replay? + import(Cr::Utility::Path::join(furnitureOpeningsPath, name), + "../objects/openings/"_s + name); + } + #endif + #endif + + Mn::Debug{} << inputImageCount << "images deduplicated to" << uniqueImages.size(); + + /* Convert images first so we can free them and have more memory for mesh + concatenation. Split them into multiple as this goes over the max GPU + texture array layer count. */ + Mn::Trade::ImageData3D image = ds.finalizeImage(ds.inputMaterials, ImageLayerCountLimit); + ds.inputImages = {}; + for(Mn::Int i = 0, iMax = (image.size().z() + ImageLayerCountLimit - 1)/ImageLayerCountLimit; i != iMax; ++i) { + Mn::ImageView3D imagePart{ + Mn::PixelStorage{}.setSkip({0, 0, Mn::Int(i*ImageLayerCountLimit)}), + image.format(), + {image.size().xy(), Mn::Math::min(image.size().z() - i*ImageLayerCountLimit, ImageLayerCountLimit)}, + image.data(), + Mn::ImageFlag3D::Array + }; + + /* Compress the image */ + Cr::Containers::Optional imageCompressed = imageCompressor->convert(imagePart); + Mn::Debug{} << "Image" << i << "of size" << imagePart.size() << "compressed to" << imageCompressed->compressedFormat(); + + CORRADE_INTERNAL_ASSERT(s.converter->add(*imageCompressed)); + // TODO why even have this?! + // CORRADE_INTERNAL_ASSERT(s.converter->add(ds.finalizeTexture())); + CORRADE_INTERNAL_ASSERT(s.converter->add( Mn::Trade::TextureData{Mn::Trade::TextureType::Texture2DArray, + Mn::SamplerFilter::Linear, Mn::SamplerFilter::Linear, Mn::SamplerMipmap::Linear, + Mn::SamplerWrapping::Repeat, Mn::UnsignedInt(i)})); + } + image = Mn::Trade::ImageData3D{Mn::PixelFormat::R8I, {}, nullptr}; + + /* Concatenate meshes and free them right after as well */ + // TODO some mesh optimization first? + CORRADE_INTERNAL_ASSERT(s.converter->add(ds.finalizeMesh())); + ds.inputMeshes = {}; + + /* Filter away unused material attributes */ + for(Mn::Trade::MaterialData& material: ds.inputMaterials) { + Cr::Containers::BitArray attributesToKeep{Cr::DirectInit, material.attributeData().size(), true}; + // TODO 623 + // for(const char* name: {"fp_class_name", "fp_name"}) { + // if(const Cr::Containers::Optional attributeId = material.findAttributeId(name)) + // attributesToKeep.reset(*attributeId); + // } + // TODO this is just to check if moving the atlas transform to the scene + // helped with anything + // for(Mn::Trade::MaterialAttribute name: {Mn::Trade::MaterialAttribute::BaseColorTextureMatrix, Mn::Trade::MaterialAttribute::BaseColorTextureLayer}) { + // if(const Cr::Containers::Optional attributeId = material.findAttributeId(name)) + // attributesToKeep.reset(*attributeId); + // } + for(Mn::Trade::MaterialAttribute name: {Mn::Trade::MaterialAttribute::NoneRoughnessMetallicTexture, + Mn::Trade::MaterialAttribute::NormalTexture, + Mn::Trade::MaterialAttribute::NormalTextureScale, + Mn::Trade::MaterialAttribute::EmissiveTexture, + Mn::Trade::MaterialAttribute::OcclusionTexture, + Mn::Trade::MaterialAttribute::Metalness, + Mn::Trade::MaterialAttribute::Roughness, + }) { + if(const Cr::Containers::Optional attributeId = material.findAttributeId(name)) + attributesToKeep.reset(*attributeId); + } + Cr::Containers::BitArray layersToKeep{Cr::DirectInit, material.layerCount(), false}; + layersToKeep.set(0); // TODO only base layer now + material = Mn::MaterialTools::filterAttributesLayers(material, attributesToKeep, layersToKeep); + // material = Mn::MaterialTools::filterAttributes(material, attributesToKeep); + } + + #if 0 + /* Canonicalize materials */ + std::size_t attributesBefore = 0; + std::size_t attributesAfter = 0; + for(Mn::Trade::MaterialData& material: ds.inputMaterials) { + attributesBefore += material.attributeData().size(); + material = Mn::MaterialTools::canonicalize(material); + attributesAfter += material.attributeData().size(); + } + Mn::Debug{} << ds.inputMaterials.size() << "materials canonicalized from" << attributesBefore << "to" << attributesAfter << "attributes in total"; + + /* Deduplicate materials, update their indices */ + Cr::Containers::Pair, std::size_t> materialDuplicates = + Mn::MaterialTools::removeDuplicatesInPlace(ds.inputMaterials); + for(esp::Mesh& mesh: ss.meshes) + if(mesh.meshMaterial != -1) + mesh.meshMaterial = materialDuplicates.first()[mesh.meshMaterial];; + Mn::Debug{} << ds.inputMaterials.size() << "materials deduplicated to" << materialDuplicates.second(); + + for(const Mn::Trade::MaterialData& i: ds.inputMaterials.prefix(materialDuplicates.second())) + CORRADE_INTERNAL_ASSERT(s.converter->add(i)); + #else + for(const Mn::Trade::MaterialData& i: ds.inputMaterials) + CORRADE_INTERNAL_ASSERT(s.converter->add(i)); + #endif + + CORRADE_INTERNAL_ASSERT(s.converter->add(ss.finalizeScene())); + + return s.converter->endFile() ? 0 : 1; +} diff --git a/src/utils/compositor/compositor-replicacad.cpp b/src/utils/compositor/compositor-replicacad.cpp new file mode 100644 index 0000000000..2e41514e11 --- /dev/null +++ b/src/utils/compositor/compositor-replicacad.cpp @@ -0,0 +1,379 @@ +// Copyright (c) Meta Platforms, Inc. and its affiliates. +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CompositorState.h" + +namespace Cr = Corrade; +namespace Mn = Magnum; +using namespace Cr::Containers::Literals; +using namespace Mn::Math::Literals; + +int main(int argc, char** argv) { + Cr::Utility::Arguments args; + args.addArgument("input").setHelp("input", "input file prefix") + .addArgument("output").setHelp("output", "output file") + .parse(argc, argv); + + constexpr Mn::Vector2i TextureAtlasSize{4096}; // TODO resize this!! + esp::CompositorState s{args.value("output")}; + esp::CompositorSceneState ss; + esp::CompositorDataState ds{TextureAtlasSize}; + Cr::Containers::Pointer importer = s.importerManager.loadAndInstantiate("AnySceneImporter"); + CORRADE_INTERNAL_ASSERT(importer); + + std::unordered_map> uniqueMeshes; + + const auto import = [&]( + Cr::Containers::StringView filename, + Cr::Containers::StringView name, + Mn::Trade::MaterialType materialType) + { + CORRADE_INTERNAL_ASSERT_OUTPUT(importer->openFile(filename)); + CORRADE_INTERNAL_ASSERT(importer->sceneCount() == 1); + + Cr::Containers::Optional scene = importer->scene(0); + CORRADE_INTERNAL_ASSERT(scene); + + /* Top-level object, parent of the others */ + esp::Parent root; + root.mapping = ss.parents.size(); + root.parent = -1; + arrayAppend(ss.parents, root); + s.converter->setObjectName(root.mapping, name); + + /* Meshes are unfortunately named in a useless way, so override them with + names from the objects referencing them instead */ + // TODO this isn't strictly needed, the names are just for debugging + // purposes ... but deduplication relies on them :/ + Cr::Containers::Array meshNames{importer->meshCount()}; + for(Cr::Containers::Pair> objectMeshMaterial: scene->meshesMaterialsAsArray()) { + const Mn::UnsignedInt meshId = objectMeshMaterial.second().first(); + Cr::Containers::String objectName = importer->objectName(objectMeshMaterial.first()); + if(!objectName) + Mn::Fatal{} << "No name found for object" << objectMeshMaterial.first() << "referencing mesh" << importer->meshName(meshId) << "in" << Cr::Utility::Path::split(filename).second(); + + if(meshNames[meshId] && meshNames[meshId] != objectName) + Mn::Fatal{} << "Conflicting name for mesh" << importer->meshName(meshId) << Mn::Debug::nospace << ":" << meshNames[meshId] << "vs" << objectName << "in" << Cr::Utility::Path::split(filename).second(); + + meshNames[meshId] = std::move(objectName); + } + + /* Assuming materials are shared among meshes, remember the ID of already + imported materials */ + Cr::Containers::Array> importedMaterialIds{importer->materialCount()}; + + /* Node mesh/material assignments. Each entry will be one child of the + top-level object. */ + for(Cr::Containers::Triple transformationMeshMaterial: Mn::SceneTools::flattenMeshHierarchy3D(*scene)) { + Cr::Containers::Optional mesh = importer->mesh(transformationMeshMaterial.first()); + Cr::Containers::StringView meshName = meshNames[transformationMeshMaterial.first()]; + + /* Skip non-triangle meshes */ + if(mesh->primitive() != Mn::MeshPrimitive::Triangles && + mesh->primitive() != Mn::MeshPrimitive::TriangleFan && + mesh->primitive() != Mn::MeshPrimitive::TriangleStrip) { + Mn::Warning{} << "Mesh" << meshName << "in" << Cr::Utility::Path::split(filename).second() << "is" << mesh->primitive() << Mn::Debug::nospace << ", skipping"; + continue; + } + + // TODO convert from a strip/... if not triangles + CORRADE_INTERNAL_ASSERT(mesh && mesh->isIndexed() && mesh->primitive() == Mn::MeshPrimitive::Triangles); + + esp::Parent o; + o.mapping = ss.parents.size(); + o.parent = root.mapping; + arrayAppend(ss.parents, o); + arrayAppend(ss.transformations, Cr::InPlaceInit, + o.mapping, + transformationMeshMaterial.third()); + /* Save the nested object name as well, for debugging purposes. It'll be + duplicated among different scenes but that's no problem. */ + s.converter->setObjectName(o.mapping, meshName); + + esp::Mesh m; + m.mapping = o.mapping; + m.mesh = 0; + m.meshIndexCount = mesh->indexCount(); + + /* Check if a mesh of the same name is already present and reuse it in + that case, otherwise add to the map. Not using + `importer->meshName(transformationMeshMaterial.first())` + because the mesh names are useless `Mesh.XYZ` that don't match between + files. */ + // TODO avoid string copies by making the map over StringViews? but then + // it'd have to be changed again once we don't have the extra meshNames + // array + bool duplicate = false; + std::unordered_map>::iterator found = uniqueMeshes.find(meshName); + if(found != uniqueMeshes.end()) { + if(mesh->indexCount() == found->second.second()) { + m.meshIndexOffset = found->second.first(); + duplicate = true; + } else { + Mn::Warning{} << "Mesh" << meshName << "in" << Cr::Utility::Path::split(filename).second() << "has" << mesh->indexCount() << "indices but expected" << found->second.second() << Mn::Debug::nospace << ", adding a new copy"; + } + } + + // TODO broken (meshes with the same name being totally different across + // the door files), use this only for the Stage_* files + if(true || !duplicate) { + Mn::Debug{} << "New mesh" << meshName << "in" << Cr::Utility::Path::split(filename).second(); + uniqueMeshes.insert({std::move(meshName), {ds.indexOffset, mesh->indexCount()}}); + + m.meshIndexOffset = ds.indexOffset; + ds.indexOffset += mesh->indexCount()*4; // TODO ints hardcoded + + // TODO convert from a strip/... if not triangles + arrayAppend(ds.inputMeshes, *std::move(mesh)); + } else { + Mn::Debug{} << "Mesh" << meshName << "in" << Cr::Utility::Path::split(filename).second() << "already present"; + } + + /* If the material is already parsed, reuse its ID */ + if(const Cr::Containers::Optional materialId = importedMaterialIds[transformationMeshMaterial.second()]) { + m.meshMaterial = *materialId; + + /* Otherwise parse it. If textured, extract the image as well. */ + } else { + Cr::Containers::Optional material = importer->material(transformationMeshMaterial.second()); + CORRADE_INTERNAL_ASSERT(material); + + /* Convert to a PBR metallic/roughness if it's a Phong material */ + if(material->types() & Mn::Trade::MaterialType::Phong) + material = Mn::MaterialTools::phongToPbrMetallicRoughness(*material, + Mn::MaterialTools::PhongToPbrMetallicRoughnessFlag::DropUnconvertableAttributes); + + /* Not calling releaseAttributeData() yet either, as we need to ask + hasAttribute() first */ + Cr::Containers::Array attributes; + if(const Cr::Containers::Optional baseColorTextureAttributeId = material->findAttributeId(Mn::Trade::MaterialAttribute::BaseColorTexture)) { + Mn::Debug{} << "New textured material for" << meshName << "in" << Cr::Utility::Path::split(filename).second(); + + Mn::UnsignedInt& textureId = material->mutableAttribute(*baseColorTextureAttributeId); + + Cr::Containers::Optional texture = importer->texture(textureId); + CORRADE_INTERNAL_ASSERT(texture && texture->type() == Mn::Trade::TextureType::Texture2D); + + // TODO textures referencing the same image ID -> deduplicate; + Cr::Containers::Optional image = importer->image2D(texture->image()); + // TODO detection of single-color images here + // TODO StbResizeImageConverter instead of the assert + CORRADE_INTERNAL_ASSERT((image->size() <= TextureAtlasSize).all()); + /* Add texture scaling if the image is smaller than 2K */ + if((image->size() < TextureAtlasSize).any()) { + const Mn::Matrix3 matrix = Mn::Matrix3::scaling(Mn::Vector2(image->size())/Mn::Vector2(TextureAtlasSize)); + // TODO MaterialTools::merge() instead!!! + if(const Cr::Containers::Optional baseColorTextureMatrixAttributeId = material->findAttributeId(Mn::Trade::MaterialAttribute::BaseColorTextureMatrix)) { + Mn::Matrix3& existingMatrix = + material->mutableAttribute(*baseColorTextureMatrixAttributeId); + existingMatrix = matrix*existingMatrix; + } else { + arrayAppend(attributes, Mn::InPlaceInit, Mn::Trade::MaterialAttribute::BaseColorTextureMatrix, matrix); + } + } + + /* Patch the material to use the zero texture but add a layer as + well, referencing the just-added image */ + textureId = 0; + arrayAppend(attributes, Cr::InPlaceInit, Mn::Trade::MaterialAttribute::BaseColorTextureLayer, Mn::UnsignedInt(ds.inputImages.size())); + + arrayAppend(ds.inputImages, *std::move(image)); + + /* Otherwise make it reference the first image, which is a 1x1 white + pixel */ + } else { + Mn::Debug{} << "New untextured material for" << meshName << "in" << Cr::Utility::Path::split(filename).second(); + + arrayAppend(attributes, { + {Mn::Trade::MaterialAttribute::BaseColorTexture, 0u}, + {Mn::Trade::MaterialAttribute::BaseColorTextureLayer, 0u}, + {Mn::Trade::MaterialAttribute::BaseColorTextureMatrix, + Mn::Matrix3::scaling(Mn::Vector2{1}/Mn::Vector2(TextureAtlasSize))} + }); + } + + // TODO MaterialTools::merge() instead of this!!!! + CORRADE_INTERNAL_ASSERT(material->layerCount() == 1); + arrayAppend(attributes, material->attributeData()); + + /* The material type is overriden to Flat for some */ + material = Mn::Trade::MaterialData{materialType, std::move(attributes)}; + + importedMaterialIds[transformationMeshMaterial.second()] = m.meshMaterial = ds.inputMaterials.size(); + arrayAppend(ds.inputMaterials, *std::move(material)); + } + + arrayAppend(ss.meshes, m); + } + }; + + Cr::Containers::String replicaPath = Cr::Utility::Path::join(args.value("input"), "ReplicaCAD_dataset_v1.5"); + + /* Stage_v3_sc*_staging are already pre-baked so Flat, frl_apartment_stage + is not */ + import(Cr::Utility::Path::join({replicaPath, "stages/frl_apartment_stage.glb"}), "data/replica_cad/configs/stages/../../stages/frl_apartment_stage.glb", Mn::Trade::MaterialType::PbrMetallicRoughness); + for(const char* name: { + "Stage_v3_sc0_staging.glb", + "Stage_v3_sc1_staging.glb", + "Stage_v3_sc2_staging.glb", + "Stage_v3_sc3_staging.glb" + }) + import(Cr::Utility::Path::join({replicaPath, "stages", name}), "data/replica_cad/configs/stages/../../stages/"_s + name, Mn::Trade::MaterialType::Flat); + + for(const char* name: { + "frl_apartment_basket.glb", + "frl_apartment_beanbag.glb", + "frl_apartment_bike_01.glb", + "frl_apartment_bike_02.glb", + "frl_apartment_bin_01.glb", + "frl_apartment_bin_02.glb", + "frl_apartment_bin_03.glb", + "frl_apartment_book_01.glb", + "frl_apartment_book_02.glb", + "frl_apartment_book_03.glb", + "frl_apartment_book_04.glb", + "frl_apartment_book_05.glb", + "frl_apartment_book_06.glb", + "frl_apartment_bowl_01.glb", + "frl_apartment_bowl_02.glb", + "frl_apartment_bowl_03.glb", + "frl_apartment_bowl_06.glb", + "frl_apartment_bowl_07.glb", + "frl_apartment_box.glb", + "frl_apartment_cabinet.glb", + "frl_apartment_camera_02.glb", + "frl_apartment_clock.glb", + "frl_apartment_clothes_hanger_01.glb", + "frl_apartment_clothes_hanger_02.glb", + "frl_apartment_cloth_01.glb", + "frl_apartment_cloth_02.glb", + "frl_apartment_cloth_03.glb", + "frl_apartment_cloth_04.glb", + "frl_apartment_cup_01.glb", + "frl_apartment_cup_02.glb", + "frl_apartment_cup_03.glb", + "frl_apartment_cup_05.glb", + "frl_apartment_cushion_01.glb", + "frl_apartment_cushion_03.glb", + "frl_apartment_handbag.glb", + "frl_apartment_chair_01.glb", + "frl_apartment_chair_04.glb", + "frl_apartment_chair_05.glb", + "frl_apartment_choppingboard_02.glb", + "frl_apartment_indoor_plant_01.glb", + "frl_apartment_indoor_plant_02.glb", + "frl_apartment_kitchen_utensil_01.glb", + "frl_apartment_kitchen_utensil_02.glb", + "frl_apartment_kitchen_utensil_03.glb", + "frl_apartment_kitchen_utensil_04.glb", + "frl_apartment_kitchen_utensil_05.glb", + "frl_apartment_kitchen_utensil_06.glb", + "frl_apartment_kitchen_utensil_08.glb", + "frl_apartment_kitchen_utensil_09.glb", + "frl_apartment_knifeblock.glb", + "frl_apartment_lamp_01.glb", + "frl_apartment_lamp_02.glb", + "frl_apartment_mat.glb", + "frl_apartment_monitor.glb", + "frl_apartment_monitor_stand.glb", + "frl_apartment_pan_01.glb", + "frl_apartment_picture_01.glb", + "frl_apartment_picture_02.glb", + "frl_apartment_picture_03.glb", + "frl_apartment_picture_04.glb", + "frl_apartment_plate_01.glb", + "frl_apartment_plate_02.glb", + "frl_apartment_rack_01.glb", + "frl_apartment_refrigerator.glb", + "frl_apartment_remote-control_01.glb", + "frl_apartment_rug_01.glb", + "frl_apartment_rug_02.glb", + "frl_apartment_setupbox.glb", + "frl_apartment_shoebox_01.glb", + "frl_apartment_shoe_01.glb", + "frl_apartment_shoe_02.glb", + "frl_apartment_shoe_03.glb", + "frl_apartment_shoe_04.glb", + "frl_apartment_small_appliance_01.glb", + "frl_apartment_small_appliance_02.glb", + "frl_apartment_sofa.glb", + "frl_apartment_sponge_dish.glb", + "frl_apartment_stool_02.glb", + "frl_apartment_table_01.glb", + "frl_apartment_table_02.glb", + "frl_apartment_table_03.glb", + "frl_apartment_table_04.glb", + "frl_apartment_towel.glb", + "frl_apartment_tv_object.glb", + "frl_apartment_tv_screen.glb", + "frl_apartment_tvstand.glb", + "frl_apartment_umbrella.glb", + "frl_apartment_vase_01.glb", + "frl_apartment_vase_02.glb", + "frl_apartment_wall_cabinet_01.glb", + "frl_apartment_wall_cabinet_02.glb", + "frl_apartment_wall_cabinet_03.glb" + }) + import(Cr::Utility::Path::join({replicaPath, "objects", name}), "data/replica_cad/configs/objects/../../objects/"_s + name, Mn::Trade::MaterialType::PbrMetallicRoughness); + + for(const char* name: { + "doors/double_door_R.glb", + "doors/door2.glb", + "doors/door1.glb", + "doors/door3.glb", + "doors/double_door_L.glb", + "cabinet/door.glb", + "cabinet/cabinet.glb", + "chest_of_drawers/chestOfDrawers_DrawerBot.glb", + "chest_of_drawers/chestOfDrawers_base.glb", + "chest_of_drawers/chestOfDrawers_DrawerTop.glb", + "chest_of_drawers/chestOfDrawers_DrawerMid.glb", + "kitchen_counter/drawer1.glb", + "kitchen_counter/kitchen_counter.glb", + "kitchen_counter/drawer4.glb", + "kitchen_counter/drawer2.glb", + "kitchen_counter/drawer3.glb", + "fridge/bottom_door.glb", + "fridge/body.glb", + "fridge/top_door.glb", + "kitchen_cupboards/kitchencupboard_doorWindow_R.glb", + "kitchen_cupboards/kitchencupboard_base.glb", + "kitchen_cupboards/kitchencupboard_doorWhole_R.glb", + "kitchen_cupboards/kitchencupboard_doorWhole_L.glb", + "kitchen_cupboards/kitchencupboard_doorWindow_L.glb" + }) + import(Cr::Utility::Path::join({replicaPath, "urdf", name}), "data/replica_cad/urdf/"_s + name, Mn::Trade::MaterialType::PbrMetallicRoughness); + + // TODO some mesh optimization first? + CORRADE_INTERNAL_ASSERT(s.converter->add(ds.finalizeMesh())); + + // TODO dxt compression first? + CORRADE_INTERNAL_ASSERT(s.converter->add(ds.finalizeImage(ds.inputMaterials))); + CORRADE_INTERNAL_ASSERT(s.converter->add(ds.finalizeTexture())); + + // TODO deduplication first? + for(const Mn::Trade::MaterialData& i: ds.inputMaterials) + CORRADE_INTERNAL_ASSERT(s.converter->add(i)); + + CORRADE_INTERNAL_ASSERT(s.converter->add(ss.finalizeScene())); + + return s.converter->endFile() ? 0 : 1; +} diff --git a/src/utils/compositor/compositor-ycb.cpp b/src/utils/compositor/compositor-ycb.cpp new file mode 100644 index 0000000000..731933087d --- /dev/null +++ b/src/utils/compositor/compositor-ycb.cpp @@ -0,0 +1,289 @@ +// Copyright (c) Meta Platforms, Inc. and its affiliates. +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CompositorState.h" + +namespace Cr = Corrade; +namespace Mn = Magnum; +using namespace Cr::Containers::Literals; +using namespace Mn::Math::Literals; + +int main(int argc, char** argv) { + Cr::Utility::Arguments args; + args.addArgument("input").setHelp("input", "input file prefix") + .addArgument("output").setHelp("output", "output file") + .parse(argc, argv); + + constexpr Mn::Vector2i TextureAtlasSize{512}; + esp::CompositorState s{args.value("output")}; + esp::CompositorSceneState ss; + esp::CompositorDataState ds{TextureAtlasSize}; + // TODO instead of forcing the importer name, it should fall back to + // detection based on file magic if the `.glb.orig` is unrecognizable + Cr::Containers::Pointer importer = s.importerManager.loadAndInstantiate("GltfImporter"); + CORRADE_INTERNAL_ASSERT(importer); + + Cr::Containers::Pointer imageResizer = s.imageConverterManager.loadAndInstantiate("StbResizeImageConverter"); + CORRADE_INTERNAL_ASSERT(imageResizer); + imageResizer->configuration().setValue("size", Mn::Vector2i{256}); // TODO even less? + imageResizer->configuration().setValue("upsample", false); + + const auto import = [&]( + Cr::Containers::StringView filename, + Cr::Containers::StringView name) { + CORRADE_INTERNAL_ASSERT_OUTPUT(importer->openFile(filename)); + CORRADE_INTERNAL_ASSERT(importer->sceneCount() == 1); + + Cr::Containers::Optional scene = importer->scene(0); + CORRADE_INTERNAL_ASSERT(scene); + + /* Top-level object, parent of the others */ + esp::Parent root; + root.mapping = ss.parents.size(); + root.parent = -1; + arrayAppend(ss.parents, root); + s.converter->setObjectName(root.mapping, name); + + /* Assuming materials are shared among meshes, remember the ID of already + imported materials */ + Cr::Containers::Array> importedMaterialIds{importer->materialCount()}; + + /* Node mesh/material assignments. Each entry will be one child of the + top-level object. */ + for(Cr::Containers::Triple transformationMeshMaterial: Mn::SceneTools::flattenMeshHierarchy3D(*scene)) { + Cr::Containers::Optional mesh = importer->mesh(transformationMeshMaterial.first()); + // TODO drop the names, useless + Cr::Containers::String meshName = importer->meshName(transformationMeshMaterial.first()); + + /* Skip non-triangle meshes */ + if(mesh->primitive() != Mn::MeshPrimitive::Triangles && + mesh->primitive() != Mn::MeshPrimitive::TriangleFan && + mesh->primitive() != Mn::MeshPrimitive::TriangleStrip) { + Mn::Warning{} << "Mesh" << meshName << "in" << Cr::Utility::Path::split(filename).second() << "is" << mesh->primitive() << Mn::Debug::nospace << ", skipping"; + continue; + } + + // TODO convert from a strip/... if not triangles + CORRADE_INTERNAL_ASSERT(mesh && mesh->isIndexed() && mesh->primitive() == Mn::MeshPrimitive::Triangles); + + esp::Parent o; + o.mapping = ss.parents.size(); + o.parent = root.mapping; + arrayAppend(ss.parents, o); + arrayAppend(ss.transformations, Cr::InPlaceInit, + o.mapping, + transformationMeshMaterial.third()); + /* Save the nested object name as well, for debugging purposes. It'll be + duplicated among different scenes but that's no problem. */ + s.converter->setObjectName(o.mapping, meshName); + + esp::Mesh m; + m.mapping = o.mapping; + m.mesh = 0; + m.meshIndexCount = mesh->indexCount(); + + Mn::Debug{} << "New mesh" << meshName << "in" << Cr::Utility::Path::split(filename).second(); + + m.meshIndexOffset = ds.indexOffset; + ds.indexOffset += mesh->indexCount()*4; // TODO ints hardcoded + + arrayAppend(ds.inputMeshes, *std::move(mesh)); + + /* If the material is already parsed, reuse its ID */ + // TODO drop material deduplication, useless + if(const Cr::Containers::Optional materialId = importedMaterialIds[transformationMeshMaterial.second()]) { + m.meshMaterial = *materialId; + + /* Otherwise parse it. If textured, extract the image as well. */ + } else { + Cr::Containers::Optional material = importer->material(transformationMeshMaterial.second()); + CORRADE_INTERNAL_ASSERT(material); + + /* Not calling releaseAttributeData() yet either, as we need to ask + hasAttribute() first */ + Cr::Containers::Array attributes; + if(const Cr::Containers::Optional baseColorTextureAttributeId = material->findAttributeId(Mn::Trade::MaterialAttribute::BaseColorTexture)) { + Mn::Debug{} << "New textured material for" << meshName << "in" << Cr::Utility::Path::split(filename).second(); + + Mn::UnsignedInt& textureId = material->mutableAttribute(*baseColorTextureAttributeId); + + Cr::Containers::Optional texture = importer->texture(textureId); + CORRADE_INTERNAL_ASSERT(texture && texture->type() == Mn::Trade::TextureType::Texture2D); + + // TODO textures referencing the same image ID -> deduplicate; + Cr::Containers::Optional image = importer->image2D(texture->image()); + + /* Resize the image if it's too big */ + CORRADE_INTERNAL_ASSERT(image); + image = imageResizer->convert(*image); + CORRADE_INTERNAL_ASSERT(image); + + // TODO detection of single-color images here? + + /* Add texture scaling if the image is smaller */ + if((image->size() < TextureAtlasSize).any()) { + const Mn::Matrix3 matrix = Mn::Matrix3::scaling(Mn::Vector2(image->size())/Mn::Vector2(TextureAtlasSize)); + // TODO MaterialTools::merge() instead!!! + if(const Cr::Containers::Optional baseColorTextureMatrixAttributeId = material->findAttributeId(Mn::Trade::MaterialAttribute::BaseColorTextureMatrix)) { + Mn::Matrix3& existingMatrix = + material->mutableAttribute(*baseColorTextureMatrixAttributeId); + existingMatrix = matrix*existingMatrix; + } else { + arrayAppend(attributes, Mn::InPlaceInit, Mn::Trade::MaterialAttribute::BaseColorTextureMatrix, matrix); + } + } + + /* Patch the material to use the zero texture but add a layer as + well, referencing the just-added image */ + textureId = 0; + arrayAppend(attributes, Cr::InPlaceInit, Mn::Trade::MaterialAttribute::BaseColorTextureLayer, Mn::UnsignedInt(ds.inputImages.size())); + + arrayAppend(ds.inputImages, *std::move(image)); + + /* Otherwise make it reference the first image, which is a 1x1 white + pixel */ + } else { + // TODO not needed here, clean up + CORRADE_INTERNAL_ASSERT_UNREACHABLE(); + Mn::Debug{} << "New untextured material for" << meshName << "in" << Cr::Utility::Path::split(filename).second(); + // + // arrayAppend(attributes, { + // Mn::Trade::MaterialAttributeData{Mn::Trade::MaterialAttribute::BaseColorTexture, 0u}, + // Mn::Trade::MaterialAttributeData{Mn::Trade::MaterialAttribute::BaseColorTextureLayer, 0u}, + // Mn::Trade::MaterialAttributeData{Mn::Trade::MaterialAttribute::BaseColorTextureMatrix, + // Mn::Matrix3::scaling(Mn::Vector2{1}/Mn::Vector2(TextureAtlasSize))} + // }); + } + + // TODO MaterialTools::merge() instead of this!!!! + CORRADE_INTERNAL_ASSERT(material->layerCount() == 1); + arrayAppend(attributes, material->attributeData()); + + material = Mn::Trade::MaterialData{material->types(), std::move(attributes)}; + + importedMaterialIds[transformationMeshMaterial.second()] = m.meshMaterial = ds.inputMaterials.size(); + arrayAppend(ds.inputMaterials, *std::move(material)); + } + + arrayAppend(ss.meshes, m); + } + }; + + Cr::Containers::String ycbPath = Cr::Utility::Path::join(args.value("input"), "hab_ycb_v1.2/meshes"); + for(const char* name: { + "002_master_chef_can/google_16k/textured.glb", + "003_cracker_box/google_16k/textured.glb", + "004_sugar_box/google_16k/textured.glb", + "005_tomato_soup_can/google_16k/textured.glb", + "006_mustard_bottle/google_16k/textured.glb", + "007_tuna_fish_can/google_16k/textured.glb", + "008_pudding_box/google_16k/textured.glb", + "009_gelatin_box/google_16k/textured.glb", + "010_potted_meat_can/google_16k/textured.glb", + "011_banana/google_16k/textured.glb", + "012_strawberry/google_16k/textured.glb", + "013_apple/google_16k/textured.glb", + "014_lemon/google_16k/textured.glb", + "015_peach/google_16k/textured.glb", + "016_pear/google_16k/textured.glb", + "017_orange/google_16k/textured.glb", + "018_plum/google_16k/textured.glb", + "019_pitcher_base/google_16k/textured.glb", + "021_bleach_cleanser/google_16k/textured.glb", + "022_windex_bottle/google_16k/textured.glb", + "024_bowl/google_16k/textured.glb", + "025_mug/google_16k/textured.glb", + "026_sponge/google_16k/textured.glb", + "027_skillet/google_16k/textured.glb", + "028_skillet_lid/google_16k/textured.glb", + "029_plate/google_16k/textured.glb", + "030_fork/google_16k/textured.glb", + "031_spoon/google_16k/textured.glb", + "032_knife/google_16k/textured.glb", + "033_spatula/google_16k/textured.glb", + "035_power_drill/google_16k/textured.glb", + "036_wood_block/google_16k/textured.glb", + "037_scissors/google_16k/textured.glb", + "038_padlock/google_16k/textured.glb", + "040_large_marker/google_16k/textured.glb", + "042_adjustable_wrench/google_16k/textured.glb", + "043_phillips_screwdriver/google_16k/textured.glb", + "044_flat_screwdriver/google_16k/textured.glb", + "048_hammer/google_16k/textured.glb", + "050_medium_clamp/google_16k/textured.glb", + "051_large_clamp/google_16k/textured.glb", + "052_extra_large_clamp/google_16k/textured.glb", + "053_mini_soccer_ball/google_16k/textured.glb", + "054_softball/google_16k/textured.glb", + "055_baseball/google_16k/textured.glb", + "056_tennis_ball/google_16k/textured.glb", + "057_racquetball/google_16k/textured.glb", + "058_golf_ball/google_16k/textured.glb", + "059_chain/google_16k/textured.glb", + "061_foam_brick/google_16k/textured.glb", + "062_dice/google_16k/textured.glb", + "063-a_marbles/google_16k/textured.glb", + "063-b_marbles/google_16k/textured.glb", + "065-a_cups/google_16k/textured.glb", + "065-b_cups/google_16k/textured.glb", + "065-c_cups/google_16k/textured.glb", + "065-d_cups/google_16k/textured.glb", + "065-e_cups/google_16k/textured.glb", + "065-f_cups/google_16k/textured.glb", + "065-g_cups/google_16k/textured.glb", + "065-h_cups/google_16k/textured.glb", + "065-i_cups/google_16k/textured.glb", + "065-j_cups/google_16k/textured.glb", + "070-a_colored_wood_blocks/google_16k/textured.glb", + "070-b_colored_wood_blocks/google_16k/textured.glb", + "071_nine_hole_peg_test/google_16k/textured.glb", + "072-a_toy_airplane/google_16k/textured.glb", + "072-b_toy_airplane/google_16k/textured.glb", + "072-c_toy_airplane/google_16k/textured.glb", + "072-d_toy_airplane/google_16k/textured.glb", + "072-e_toy_airplane/google_16k/textured.glb", + "073-a_lego_duplo/google_16k/textured.glb", + "073-b_lego_duplo/google_16k/textured.glb", + "073-c_lego_duplo/google_16k/textured.glb", + "073-d_lego_duplo/google_16k/textured.glb", + "073-e_lego_duplo/google_16k/textured.glb", + "073-f_lego_duplo/google_16k/textured.glb", + "073-g_lego_duplo/google_16k/textured.glb", + "077_rubiks_cube/google_16k/textured.glb" + }) + import(Cr::Utility::Path::join(ycbPath, name + ".orig"_s), "data/objects/ycb/configs/../meshes/"_s + name); + + // TODO some mesh optimization first? + CORRADE_INTERNAL_ASSERT(s.converter->add(ds.finalizeMesh())); + + // TODO dxt compression first? + CORRADE_INTERNAL_ASSERT(s.converter->add(ds.finalizeImage(ds.inputMaterials))); + CORRADE_INTERNAL_ASSERT(s.converter->add(ds.finalizeTexture())); + + // TODO deduplication first? + for(const Mn::Trade::MaterialData& i: ds.inputMaterials) + CORRADE_INTERNAL_ASSERT(s.converter->add(i)); + + CORRADE_INTERNAL_ASSERT(s.converter->add(ss.finalizeScene())); + + return s.converter->endFile() ? 0 : 1; +} diff --git a/src/utils/importer/CMakeLists.txt b/src/utils/importer/CMakeLists.txt new file mode 100644 index 0000000000..d575f98f66 --- /dev/null +++ b/src/utils/importer/CMakeLists.txt @@ -0,0 +1,16 @@ +# Copyright (c) Meta Platforms, Inc. and its affiliates. +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +find_package(Magnum REQUIRED Trade) + +# If Magnum is built as static (i.e., the bundled submodule), the plugin is +# static as well, to avoid unnecessary pain with duplicated symbols. With +# dynamic (and thus probably external) Magnum it's dynamic so it can be loaded +# by external tools such as magnum-player as well. +if(MAGNUM_BUILD_STATIC) + corrade_add_static_plugin(HabitatImporter ${CMAKE_CURRENT_BINARY_DIR} "" HabitatImporter.conf HabitatImporter.cpp) +else() + corrade_add_plugin(HabitatImporter ${CMAKE_CURRENT_BINARY_DIR} "" HabitatImporter.conf HabitatImporter.cpp) +endif() +target_link_libraries(HabitatImporter PUBLIC Magnum::Trade) diff --git a/src/utils/importer/HabitatImporter.conf b/src/utils/importer/HabitatImporter.conf new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/utils/importer/HabitatImporter.conf @@ -0,0 +1 @@ + diff --git a/src/utils/importer/HabitatImporter.cpp b/src/utils/importer/HabitatImporter.cpp new file mode 100644 index 0000000000..49a896775f --- /dev/null +++ b/src/utils/importer/HabitatImporter.cpp @@ -0,0 +1,369 @@ +// Copyright (c) Meta Platforms, Inc. and its affiliates. +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Cr = Corrade; +namespace Mn = Magnum; +using namespace Cr::Containers::Literals; +using namespace Mn::Math::Literals; + +namespace { + +struct ImporterState { + Cr::Containers::String basePath; + Cr::Utility::Json json; + const Cr::Utility::JsonToken& jsonStage; + Cr::Containers::Array> jsonObjects; +}; + +class HabitatImporter: public Mn::Trade::AbstractImporter { + public: + explicit HabitatImporter(Cr::PluginManager::AbstractManager& manager, const Cr::Containers::StringView plugin): Mn::Trade::AbstractImporter{manager, plugin} {} + + private: + Mn::Trade::ImporterFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return !!_state; } + void doClose() override { _state = Cr::Containers::NullOpt; } + void doOpenFile(Cr::Containers::StringView filename) override; + + Mn::UnsignedInt doSceneCount() const override { return 1; } + Cr::Containers::Optional doScene(Mn::UnsignedInt id) override; + Mn::Trade::SceneField doSceneFieldForName(Cr::Containers::StringView name) override; + Cr::Containers::String doSceneFieldName(Mn::UnsignedInt name) override; + + Mn::UnsignedLong doObjectCount() const override; + Mn::Long doObjectForName(Cr::Containers::StringView name) override; + Cr::Containers::String doObjectName(Mn::UnsignedLong id) override; + + Cr::Containers::Optional _state; +}; + +void HabitatImporter::doOpenFile(Cr::Containers::StringView filename) { + Cr::Containers::Optional json = Cr::Utility::Json::fromFile(filename, Cr::Utility::Json::Option::ParseFloats|Cr::Utility::Json::Option::ParseLiterals|Cr::Utility::Json::Option::ParseStrings); + if(!json) { + Mn::Error{} << "HabitatImporter::openFile(): can't open the top-level JSON"; + return; + } + + /* Gather references to all stages and objects in the JSON */ + // TODO support also loading the scene/object JSONs directly? + const Cr::Utility::JsonToken& stage = json->root()["stage_instance"_s]; + Cr::Containers::Array> objects; + for(const Cr::Utility::JsonToken& object: json->root()["object_instances"_s].asArray()) { + arrayAppend(objects, object); + } + + _state.emplace( + Cr::Utility::Path::split(filename).first(), + *std::move(json), + stage, + std::move(objects)); +} + +/* Underscore to have them start with a lowercase */ +constexpr Mn::Trade::SceneField SceneField_externalFile = Mn::Trade::sceneFieldCustom(0); +constexpr Mn::Trade::SceneField SceneField_semanticId = Mn::Trade::sceneFieldCustom(1); +constexpr Mn::Trade::SceneField SceneField_up = Mn::Trade::sceneFieldCustom(2); +constexpr Mn::Trade::SceneField SceneField_front = Mn::Trade::sceneFieldCustom(3); +constexpr Mn::Trade::SceneField SceneField_requiresLighting = Mn::Trade::sceneFieldCustom(4); + +Cr::Containers::String HabitatImporter::doSceneFieldName(const Mn::UnsignedInt name) { + switch(name) { + #define _c(field) \ + case Mn::Trade::sceneFieldCustom(SceneField_ ## field): return #field; + _c(externalFile) + _c(semanticId) + _c(up) + _c(front) + _c(requiresLighting) + #undef _c + } + + return {}; +} + +Mn::Trade::SceneField HabitatImporter::doSceneFieldForName(const Cr::Containers::StringView name) { + #define _c(field) \ + if(name == #field) return SceneField_ ## field; + _c(externalFile) + _c(semanticId) + _c(up) + _c(front) + _c(requiresLighting) + #undef _c + + return {}; +} + +Cr::Containers::Optional HabitatImporter::doScene(Mn::UnsignedInt) { + struct Object { + Mn::UnsignedInt mapping; + Mn::Vector3 translation; + Mn::Quaternion rotation; + Mn::Vector3 scaling; + Mn::Vector3 up; + Mn::Vector3 front; + }; + Cr::Containers::Array objects{Cr::NoInit, _state->jsonObjects.size()}; + + struct SemanticId { + Mn::UnsignedInt mapping; + Mn::UnsignedInt semanticId; + }; + Cr::Containers::Array semanticIds; + + struct ExternalFile { + Mn::UnsignedInt mapping; + Mn::UnsignedInt stringOffset; + }; + Cr::Containers::Array externalFiles{Cr::NoInit, _state->jsonObjects.size() + 1}; + Cr::Containers::Array externalFileStrings; + /* This shares the mapping with externalFiles */ + Cr::Containers::BitArray requiresLighting{Cr::NoInit, _state->jsonObjects.size() + 1}; + + // TODO there's `"translation_origin": "asset_local"` specified for the whole + // file, what to do with it? why there has to be several ways to achieve the + // same?! + + /* Parse the stage file, which is object 0 */ + { + Cr::Containers::Pair nestedPathName = Cr::Utility::Path::split(_state->jsonStage["template_name"_s].asString()); + Cr::Containers::String nestedPath = Cr::Utility::Path::join(".."_s, nestedPathName.first()); + Cr::Containers::String nestedFilename = Cr::Utility::Path::join({_state->basePath, nestedPath, nestedPathName.second() + ".stage_config.json"_s}); + + /* Parse the nested JSON */ + const Cr::Containers::Optional jsonStageNested = Cr::Utility::Json::fromFile(nestedFilename, Cr::Utility::Json::Option::ParseFloats|Cr::Utility::Json::Option::ParseLiterals|Cr::Utility::Json::Option::ParseStrings); + if(!jsonStageNested) { + Mn::Error{} << "HabitatImporter::scene(): can't open the stage JSON"; + return {}; + } + + /* External file path. It's relative to the path the JSON was found in, + convert that to be relative to the top-level JSON */ + arrayAppend(externalFileStrings, Cr::Utility::Path::join(nestedPath, jsonStageNested->root()["render_asset"_s].asString())); + externalFiles[0].mapping = 0; + externalFiles[0].stringOffset = externalFileStrings.size(); + + /* Other attributes */ + requiresLighting.set(0, jsonStageNested->root()["requires_lighting"_s].asBool()); + // TODO margin, friction/restitution coefficient -- used only for the arch + // file? + } + + /* Parse all objects */ + for(std::size_t i = 0; i != _state->jsonObjects.size(); ++i) { + const Mn::UnsignedInt objectId = i + 1; + const Cr::Utility::JsonToken& jsonObject = _state->jsonObjects[i]; + const Cr::Containers::StringView templateName = jsonObject["template_name"_s].asString(); + + /* There's a ... uh ... few possible cases where the nested JSONs could be. + Some paths are listed in the top-level `*.scene_dataset_config.json` but + it suffers from a similar problem -- it's either in ../../ or in ../, + and it doesn't have a fixed name so I'd have to glob for it. + + And even that doesn't help, as the nested JSON could be also in any + arbitrary subdirectory. For now, to get things done, I'm just hardcoding + some possibilities, this needs to be cleaned up. */ + // TODO clean up this mess in the datasets themselves!! + Cr::Containers::String nestedPath; + Cr::Containers::String nestedFilename; + for(Cr::Containers::String path: { + /* In the 0.2.0 version of FP the files were directly in root and the + template_name contained the objects/ or scenes/ prefix */ + Cr::Containers::String::nullTerminatedGlobalView(".."_s), + /* In the HF main the template_name is just the hash, so we need to + prepend objects/ to it, and additionally, because there are too many, + they're put into subdirectories based on the first letter of the hash + (so 1efdc3d37dfab1eb9f99117bb84c59003d684811 is in + objects/1/1efdc3d37dfab1eb9f99117bb84c59003d684811.object_config.json) */ + Cr::Utility::Path::join("../objects"_s, templateName.prefix(1)), + /* Some objects are also inside openings/ */ + Cr::Containers::String::nullTerminatedGlobalView("../objects/openings"_s), + /* And some in decomposed/ with additional subdirectories named after the + template base hash */ + Cr::Utility::Path::join("../objects/decomposed"_s, templateName.prefix(Mn::Math::min(std::size_t{40}, templateName.size()))), + }) { + const Cr::Containers::String filename = Cr::Utility::Path::join({_state->basePath, path, templateName + ".object_config.json"_s}); + if(Cr::Utility::Path::exists(filename)) { + nestedPath = path; + nestedFilename = filename; + break; + } + } + + if(!nestedFilename) { + Mn::Error{} << "HabitatImporter::scene(): can't find object JSON file for" << templateName; + return {}; + } + + /* Parse the nested JSON */ + Cr::Containers::Optional jsonObjectNested = Cr::Utility::Json::fromFile(nestedFilename, Cr::Utility::Json::Option::ParseLiterals|Cr::Utility::Json::Option::ParseFloats|Cr::Utility::Json::Option::ParseStrings); + if(!jsonObjectNested) { + Mn::Error{} << "HabitatImporter::scene(): can't open the object JSON"; + return {}; + } + + /* External file path. It's relative to the path the JSON was found in, + convert that to be relative to the top-level JSON */ + arrayAppend(externalFileStrings, Cr::Utility::Path::join(nestedPath, jsonObjectNested->root()["render_asset"_s].asString())); + externalFiles[i + 1].mapping = objectId; + externalFiles[i + 1].stringOffset = externalFileStrings.size(); + + /* Other attributes. Some stored in the top-level JSON, some in the + nested. */ + objects[i].mapping = objectId; + Cr::Utility::copy(jsonObject["translation"_s].asFloatArray(3), + objects[i].translation.data()); + Cr::Utility::copy(jsonObject["rotation"_s].asFloatArray(4), + objects[i].rotation.data()); + // TODO "uniform_scale"?! why is there two things for doing the same, and + // non_uniform_scale being usually [1, 1, 1]?! + Cr::Utility::copy(jsonObject["non_uniform_scale"_s].asFloatArray(3), + objects[i].scaling.data()); + // TODO are these ever set to anything else than Y up and Z backward? like, + // why not have them a part of the rotation matrix?! + Cr::Utility::copy(jsonObjectNested->root()["up"_s].asFloatArray(3), + objects[i].up.data()); + Cr::Utility::copy(jsonObjectNested->root()["front"_s].asFloatArray(3), + objects[i].front.data()); + requiresLighting.set(objectId, jsonObjectNested->root()["requires_lighting"_s].asBool()); + // TODO motion type (STATIC, some other?) + // TODO rotation + + /* For some reason, the quaternion is written as WXYZ?! And additionally + the rotation is done in some completely crazy coordinate system that has + no relation whatsoever to the up/front vectors above, nor to the + coordinate system that translation and scaling is done in. IMAGINE such + a mess happening intentionally. */ + Mn::Vector4::from(objects[i].rotation.data()) = Mn::Math::gather<'w', 'x', 'y', 'z'>(Mn::Vector4::from(objects[i].rotation.data())); + objects[i].rotation = + Mn::Quaternion::rotation(180.0_degf, Mn::Vector3::yAxis())* + // TODO rotating around x instead of z works as well, why? can't this be + // simplified further? + Mn::Quaternion::rotation(-180.0_degf, Mn::Vector3::zAxis())* + objects[i].rotation* + Mn::Quaternion::rotation(180.0_degf, Mn::Vector3::zAxis()); + + /* Semantic ID. Present only for furniture etc., not the openings */ + if(const Cr::Utility::JsonToken* jsonObjectNestedSemanticId = jsonObjectNested->root().find("semantic_id"_s)) { + arrayAppend(semanticIds, Cr::InPlaceInit, objectId, *jsonObjectNested->parseUnsignedInt(*jsonObjectNestedSemanticId)); + } + } + + Cr::Containers::StridedArrayView1D outputObjects; + Cr::Containers::StridedArrayView1D outputSemanticIds; + Cr::Containers::StridedArrayView1D outputExternalFiles; + Cr::Containers::MutableStringView outputExternalFileStrings; + Cr::Containers::MutableBitArrayView outputRequiresLighting; + Cr::Containers::Array data = Cr::Containers::ArrayTuple{ + {Cr::NoInit, objects.size(), outputObjects}, + {Cr::NoInit, semanticIds.size(), outputSemanticIds}, + {Cr::NoInit, externalFiles.size(), outputExternalFiles}, + {Cr::NoInit, externalFileStrings.size(), outputExternalFileStrings}, + {Cr::NoInit, requiresLighting.size(), outputRequiresLighting} + }; + // TODO have some DirectInit instead or, better, have SceneTools::combine() + Cr::Utility::copy(objects, outputObjects); + Cr::Utility::copy(semanticIds, outputSemanticIds); + Cr::Utility::copy(externalFiles, outputExternalFiles); + Cr::Utility::copy(externalFileStrings, outputExternalFileStrings); + // TODO have a copy() for bit arrays, finally + Cr::Utility::copy( + Cr::Containers::arrayView(requiresLighting.data(), (requiresLighting.size() + 7)/8), + Cr::Containers::arrayView(static_cast(outputRequiresLighting.data()), (outputRequiresLighting.size() + 7)/8)); + + return Mn::Trade::SceneData{Mn::Trade::SceneMappingType::UnsignedInt, _state->jsonObjects.size() + 1, std::move(data), { + Mn::Trade::SceneFieldData{Mn::Trade::SceneField::Translation, + outputObjects.slice(&Object::mapping), + outputObjects.slice(&Object::translation), + Mn::Trade::SceneFieldFlag::OrderedMapping}, + Mn::Trade::SceneFieldData{Mn::Trade::SceneField::Rotation, + outputObjects.slice(&Object::mapping), + outputObjects.slice(&Object::rotation), + Mn::Trade::SceneFieldFlag::OrderedMapping}, + Mn::Trade::SceneFieldData{Mn::Trade::SceneField::Scaling, + outputObjects.slice(&Object::mapping), + outputObjects.slice(&Object::scaling), + Mn::Trade::SceneFieldFlag::OrderedMapping}, + Mn::Trade::SceneFieldData{SceneField_up, + outputObjects.slice(&Object::mapping), + outputObjects.slice(&Object::up), + Mn::Trade::SceneFieldFlag::OrderedMapping}, + Mn::Trade::SceneFieldData{SceneField_front, + outputObjects.slice(&Object::mapping), + outputObjects.slice(&Object::front), + Mn::Trade::SceneFieldFlag::OrderedMapping}, + Mn::Trade::SceneFieldData{SceneField_semanticId, + outputSemanticIds.slice(&SemanticId::mapping), + outputSemanticIds.slice(&SemanticId::semanticId), + Mn::Trade::SceneFieldFlag::OrderedMapping}, + Mn::Trade::SceneFieldData{SceneField_externalFile, + outputExternalFiles.slice(&ExternalFile::mapping), + outputExternalFileStrings.data(), Mn::Trade::SceneFieldType::StringOffset32, + outputExternalFiles.slice(&ExternalFile::stringOffset), + Mn::Trade::SceneFieldFlag::ImplicitMapping}, + Mn::Trade::SceneFieldData{SceneField_requiresLighting, + outputExternalFiles.slice(&ExternalFile::mapping), + outputRequiresLighting, + Mn::Trade::SceneFieldFlag::ImplicitMapping}, + }}; +} + +Mn::UnsignedLong HabitatImporter::doObjectCount() const { + return _state->jsonObjects.size() + 1; +} + +Cr::Containers::String HabitatImporter::doObjectName(Mn::UnsignedLong id) { + // TODO there's fpss/metadata/objects.json containing the actual names, use + // that instead? is that a "standard" location? + + if(id == 0) + return _state->jsonStage["template_name"_s].asString().exceptPrefix("stages/"_s); + + const Cr::Containers::StringView templateName = (*_state->jsonObjects[id - 1])["template_name"_s].asString(); + // TODO which of the two is correct? old (0.2.0) had a prefix as well as + // ReplicaCAD etc, new doesn't + return templateName.hasPrefix("objects/"_s) ? templateName.exceptPrefix("objects/"_s) : templateName; +} + +Mn::Long HabitatImporter::doObjectForName(Cr::Containers::StringView name) { + { + if(name == _state->jsonStage["template_name"_s].asString().exceptPrefix("stages/"_s)) + return 0; + } + + /* Yes, linear, do not use in perf-critical code */ + for(std::size_t i = 0; i != _state->jsonObjects.size(); ++i) { + const Cr::Utility::JsonToken& object = _state->jsonObjects[i]; + const Cr::Containers::StringView templateName = object["template_name"_s].asString(); + + // TODO which of the two is correct? old (0.2.0) had a prefix as well as + // ReplicaCAD etc, new doesn't + if(name == (templateName.hasPrefix("objects/"_s) ? templateName.exceptPrefix("objects/"_s) : templateName)) + return i + 1; + } + + return -1; +} + +} + +CORRADE_PLUGIN_REGISTER(HabitatImporter, HabitatImporter, + "cz.mosra.magnum.Trade.AbstractImporter/0.5") diff --git a/src/utils/replayer/replayer.cpp b/src/utils/replayer/replayer.cpp index 85c6ce6cc3..3a30c18105 100644 --- a/src/utils/replayer/replayer.cpp +++ b/src/utils/replayer/replayer.cpp @@ -224,6 +224,9 @@ void Replayer::drawEvent() { eyePos + Mn::Vector3(2.f - (float)environmentFrameIndex * 0.002f, -0.5f, 1.f), {0.f, 1.f, 0.f}); + transform = + Mn::Matrix4::translation(Mn::Vector3::yAxis(2.0f))* + Mn::Matrix4::rotationX(-90.0_degf); replayRenderer_->setSensorTransform(envIndex, "my_rgb", transform); }