diff options
55 files changed, 3922 insertions, 7 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 372fa8fa784..e3e6f8c0c66 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -265,6 +265,9 @@ option(WITH_CODEC_SNDFILE "Enable libsndfile Support (http://www.mega-nerd option(WITH_ALEMBIC "Enable Alembic Support" ON) option(WITH_ALEMBIC_HDF5 "Enable Legacy Alembic Support (not officially supported)" OFF) +# Universal Scene Description support +option(WITH_USD "Enable Universal Scene Description (USD) Support" OFF) + # 3D format support # Disable opencollada when we don't have precompiled libs option(WITH_OPENCOLLADA "Enable OpenCollada Support (http://www.opencollada.org)" ON) @@ -1728,6 +1731,7 @@ if(FIRST_RUN) info_cfg_option(WITH_OPENVDB) info_cfg_option(WITH_ALEMBIC) info_cfg_option(WITH_QUADRIFLOW) + info_cfg_option(WITH_USD) info_cfg_text("Compiler Options:") info_cfg_option(WITH_BUILDINFO) diff --git a/build_files/build_environment/CMakeLists.txt b/build_files/build_environment/CMakeLists.txt index cdfa18ff4ff..fb32d2218b8 100644 --- a/build_files/build_environment/CMakeLists.txt +++ b/build_files/build_environment/CMakeLists.txt @@ -92,6 +92,7 @@ include(cmake/python.cmake) include(cmake/python_site_packages.cmake) include(cmake/package_python.cmake) include(cmake/numpy.cmake) +include(cmake/usd.cmake) if(UNIX AND NOT APPLE) # Rely on PugiXML compiled with OpenImageIO else() diff --git a/build_files/build_environment/cmake/harvest.cmake b/build_files/build_environment/cmake/harvest.cmake index 89eec7cf72f..cc93db7de64 100644 --- a/build_files/build_environment/cmake/harvest.cmake +++ b/build_files/build_environment/cmake/harvest.cmake @@ -197,6 +197,9 @@ harvest(x264/lib ffmpeg/lib "*.a") harvest(xvidcore/lib ffmpeg/lib "*.a") harvest(embree/include embree/include "*.h") harvest(embree/lib embree/lib "*.a") +harvest(usd/include usd/include "*.h") +harvest(usd/lib/usd usd/lib/usd "*") +harvest(usd/plugin usd/plugin "*") if(UNIX AND NOT APPLE) harvest(libglu/lib mesa/lib "*.so*") diff --git a/build_files/build_environment/cmake/usd.cmake b/build_files/build_environment/cmake/usd.cmake new file mode 100644 index 00000000000..c3594390f80 --- /dev/null +++ b/build_files/build_environment/cmake/usd.cmake @@ -0,0 +1,101 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENSE BLOCK ***** + +set(USD_EXTRA_ARGS + -DBoost_COMPILER:STRING=${BOOST_COMPILER_STRING} + -DBoost_USE_MULTITHREADED=ON + -DBoost_USE_STATIC_LIBS=ON + -DBoost_USE_STATIC_RUNTIME=OFF + -DBOOST_ROOT=${LIBDIR}/boost + -DTBB_INCLUDE_DIRS=${LIBDIR}/tbb/include + -DTBB_LIBRARIES=${LIBDIR}/tbb/lib/${LIBPREFIX}tbb_static${LIBEXT} + -DTbb_TBB_LIBRARY=${LIBDIR}/tbb/lib/${LIBPREFIX}tbb_static${LIBEXT} + + # This is a preventative measure that avoids possible conflicts when add-ons + # try to load another USD library into the same process space. + -DPXR_SET_INTERNAL_NAMESPACE=usdBlender + + -DPXR_ENABLE_PYTHON_SUPPORT=OFF + -DPXR_BUILD_IMAGING=OFF + -DPXR_BUILD_TESTS=OFF + -DBUILD_SHARED_LIBS=OFF + -DPYTHON_EXECUTABLE=${PYTHON_BINARY} + -DPXR_BUILD_MONOLITHIC=ON + + # The PXR_BUILD_USD_TOOLS argument is patched-in by usd.diff. An upstream pull request + # can be found at https://github.com/PixarAnimationStudios/USD/pull/1048. + -DPXR_BUILD_USD_TOOLS=OFF + + -DCMAKE_DEBUG_POSTFIX=_d + # USD is hellbound on making a shared lib, unless you point this variable to a valid cmake file + # doesn't have to make sense, but as long as it points somewhere valid it will skip the shared lib. + -DPXR_MONOLITHIC_IMPORT=${BUILD_DIR}/usd/src/external_usd/cmake/defaults/Version.cmake +) + +ExternalProject_Add(external_usd + URL ${USD_URI} + DOWNLOAD_DIR ${DOWNLOAD_DIR} + URL_HASH MD5=${USD_HASH} + PREFIX ${BUILD_DIR}/usd + PATCH_COMMAND ${PATCH_CMD} -p 1 -d ${BUILD_DIR}/usd/src/external_usd < ${PATCH_DIR}/usd.diff + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${LIBDIR}/usd -Wno-dev ${DEFAULT_CMAKE_FLAGS} ${USD_EXTRA_ARGS} + INSTALL_DIR ${LIBDIR}/usd +) + +add_dependencies( + external_usd + external_tbb + external_boost +) + +if(WIN32) + # USD currently demands python be available at build time + # and then proceeds not to use it, but still checks that the + # version of the interpreter it is not going to use is atleast 2.7 + # so we need this dep currently since there is no system python + # on windows. + add_dependencies( + external_usd + external_python + ) + if(BUILD_MODE STREQUAL Release) + ExternalProject_Add_Step(external_usd after_install + COMMAND ${CMAKE_COMMAND} -E copy_directory ${LIBDIR}/usd/ ${HARVEST_TARGET}/usd + COMMAND ${CMAKE_COMMAND} -E copy ${BUILD_DIR}/usd/src/external_usd-build/pxr/Release/libusd_m.lib ${HARVEST_TARGET}/usd/lib/libusd_m.lib + DEPENDEES install + ) + endif() + if(BUILD_MODE STREQUAL Debug) + ExternalProject_Add_Step(external_usd after_install + COMMAND ${CMAKE_COMMAND} -E copy_directory ${LIBDIR}/usd/lib ${HARVEST_TARGET}/usd/lib + COMMAND ${CMAKE_COMMAND} -E copy ${BUILD_DIR}/usd/src/external_usd-build/pxr/Debug/libusd_m_d.lib ${HARVEST_TARGET}/usd/lib/libusd_m_d.lib + DEPENDEES install + ) + endif() +else() + # USD has two build options. The default build creates lots of small libraries, + # whereas the 'monolithic' build produces only a single library. The latter + # makes linking simpler, so that's what we use in Blender. However, running + # 'make install' in the USD sources doesn't install the static library in that + # case (only the shared library). As a result, we need to grab the `libusd_m.a` + # file from the build directory instead of from the install directory. + ExternalProject_Add_Step(external_usd after_install + COMMAND ${CMAKE_COMMAND} -E copy ${BUILD_DIR}/usd/src/external_usd-build/pxr/libusd_m.a ${HARVEST_TARGET}/usd/lib/libusd_m.a + DEPENDEES install + ) +endif() diff --git a/build_files/build_environment/cmake/versions.cmake b/build_files/build_environment/cmake/versions.cmake index 9cbf104e842..2b08a74c1aa 100644 --- a/build_files/build_environment/cmake/versions.cmake +++ b/build_files/build_environment/cmake/versions.cmake @@ -307,6 +307,10 @@ set(EMBREE_VERSION 3.2.4) set(EMBREE_URI https://github.com/embree/embree/archive/v${EMBREE_VERSION}.zip) set(EMBREE_HASH 3d4a1147002ff43939d45140aa9d6fb8) +set(USD_VERSION 19.11) +set(USD_URI https://github.com/PixarAnimationStudios/USD/archive/v${USD_VERSION}.tar.gz) +set(USD_HASH 79ff176167b3fe85f4953abd6cc5e0cc) + set(OIDN_VERSION 1.0.0) set(OIDN_URI https://github.com/OpenImageDenoise/oidn/releases/download/v${OIDN_VERSION}/oidn-${OIDN_VERSION}.src.zip) set(OIDN_HASH 19fe67b0164e8f020ac8a4f520defe60) diff --git a/build_files/build_environment/patches/usd.diff b/build_files/build_environment/patches/usd.diff new file mode 100644 index 00000000000..8afd9700e59 --- /dev/null +++ b/build_files/build_environment/patches/usd.diff @@ -0,0 +1,111 @@ +diff -x .git -ur usd.orig/cmake/defaults/Options.cmake external_usd/cmake/defaults/Options.cmake +--- usd.orig/cmake/defaults/Options.cmake 2019-10-24 22:39:53.000000000 +0200 ++++ external_usd/cmake/defaults/Options.cmake 2019-11-28 13:00:33.197957712 +0100 +@@ -25,6 +25,7 @@ + option(PXR_VALIDATE_GENERATED_CODE "Validate script generated code" OFF) + option(PXR_HEADLESS_TEST_MODE "Disallow GUI based tests, useful for running under headless CI systems." OFF) + option(PXR_BUILD_TESTS "Build tests" ON) ++option(PXR_BUILD_USD_TOOLS "Build commandline tools" ON) + option(PXR_BUILD_IMAGING "Build imaging components" ON) + option(PXR_BUILD_EMBREE_PLUGIN "Build embree imaging plugin" OFF) + option(PXR_BUILD_OPENIMAGEIO_PLUGIN "Build OpenImageIO plugin" OFF) +diff -x .git -ur usd.orig/cmake/defaults/Packages.cmake external_usd/cmake/defaults/Packages.cmake +--- usd.orig/cmake/defaults/Packages.cmake 2019-10-24 22:39:53.000000000 +0200 ++++ external_usd/cmake/defaults/Packages.cmake 2019-11-28 13:00:33.185957483 +0100 +@@ -64,7 +64,7 @@ + endif() + + # --TBB +-find_package(TBB REQUIRED COMPONENTS tbb) ++find_package(TBB) + add_definitions(${TBB_DEFINITIONS}) + + # --math +diff -x .git -ur usd.orig/pxr/base/lib/plug/initConfig.cpp external_usd/pxr/base/lib/plug/initConfig.cpp +--- usd.orig/pxr/base/lib/plug/initConfig.cpp 2019-10-24 22:39:53.000000000 +0200 ++++ external_usd/pxr/base/lib/plug/initConfig.cpp 2019-12-11 11:00:37.643323127 +0100 +@@ -69,8 +69,38 @@ + + ARCH_CONSTRUCTOR(Plug_InitConfig, 2, void) + { ++ /* The contents of this constructor have been moved to usd_initialise_plugin_path(...) */ ++} ++ ++}; // end of anonymous namespace ++ ++/** ++ * The contents of this function used to be in the static constructor Plug_InitConfig. ++ * This static constructor made it impossible for Blender to pass a path to the USD ++ * library at runtime, as the constructor would run before Blender's main() function. ++ * ++ * This function is wrapped in a C function of the same name (defined below), ++ * so that it can be called from Blender's main() function. ++ * ++ * The datafiles_usd_path path is used to point to the USD plugin path when Blender ++ * has been installed. The fallback_usd_path path should point to the build-time ++ * location of the USD plugin files so that Blender can be run on a development machine ++ * without requiring an installation step. ++ */ ++void ++usd_initialise_plugin_path(const char *datafiles_usd_path) ++{ + std::vector<std::string> result; + ++ // Add Blender-specific paths. They MUST end in a slash, or symlinks will not be treated as directory. ++ if (datafiles_usd_path != NULL && datafiles_usd_path[0] != '\0') { ++ std::string datafiles_usd_path_str(datafiles_usd_path); ++ if (datafiles_usd_path_str.back() != '/') { ++ datafiles_usd_path_str += "/"; ++ } ++ result.push_back(datafiles_usd_path_str); ++ } ++ + // Determine the absolute path to the Plug shared library. + // Any relative paths specified in the plugin search path will be + // anchored to this directory, to allow for relocatability. +@@ -94,9 +124,24 @@ + _AppendPathList(&result, installLocation, sharedLibPath); + #endif // PXR_INSTALL_LOCATION + +- Plug_SetPaths(result); +-} ++ if (!TfGetenv("PXR_PATH_DEBUG").empty()) { ++ printf("USD Plugin paths: (%zu in total):\n", result.size()); ++ for(const std::string &path : result) { ++ printf(" %s\n", path.c_str()); ++ } ++ } + ++ Plug_SetPaths(result); + } + + PXR_NAMESPACE_CLOSE_SCOPE ++ ++/* Workaround to make it possible to pass a path at runtime to USD. */ ++extern "C" { ++void ++usd_initialise_plugin_path( ++ const char *datafiles_usd_path) ++{ ++ PXR_NS::usd_initialise_plugin_path(datafiles_usd_path); ++} ++} +diff -x .git -ur usd.orig/pxr/usd/CMakeLists.txt external_usd/pxr/usd/CMakeLists.txt +--- usd.orig/pxr/usd/CMakeLists.txt 2019-10-24 22:39:53.000000000 +0200 ++++ external_usd/pxr/usd/CMakeLists.txt 2019-11-28 13:00:33.197957712 +0100 +@@ -1,6 +1,5 @@ + set(DIRS + lib +- bin + plugin + ) + +@@ -8,3 +7,8 @@ + add_subdirectory(${d}) + endforeach() + ++if (PXR_BUILD_USD_TOOLS) ++ add_subdirectory(bin) ++else() ++ message(STATUS "Skipping commandline tools because PXR_BUILD_USD_TOOLS=OFF") ++endif() diff --git a/build_files/cmake/Modules/FindUSD.cmake b/build_files/cmake/Modules/FindUSD.cmake new file mode 100644 index 00000000000..3ebcbb178c6 --- /dev/null +++ b/build_files/cmake/Modules/FindUSD.cmake @@ -0,0 +1,75 @@ +# - Find Universal Scene Description (USD) library +# Find the native USD includes and libraries +# This module defines +# USD_INCLUDE_DIRS, where to find USD headers, Set when +# USD_INCLUDE_DIR is found. +# USD_LIBRARIES, libraries to link against to use USD. +# USD_ROOT_DIR, The base directory to search for USD. +# This can also be an environment variable. +# USD_FOUND, If false, do not try to use USD. +# + +#============================================================================= +# Copyright 2019 Blender Foundation. +# +# Distributed under the OSI-approved BSD License (the "License"); +# see accompanying file Copyright.txt for details. +# +# This software is distributed WITHOUT ANY WARRANTY; without even the +# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the License for more information. +#============================================================================= + +# If USD_ROOT_DIR was defined in the environment, use it. +IF(NOT USD_ROOT_DIR AND NOT $ENV{USD_ROOT_DIR} STREQUAL "") + SET(USD_ROOT_DIR $ENV{USD_ROOT_DIR}) +ENDIF() + +SET(_usd_SEARCH_DIRS + ${USD_ROOT_DIR} + /usr/local + /opt/lib/usd + /opt/usd +) + +FIND_PATH(USD_INCLUDE_DIR + NAMES + pxr/usd/usd/api.h + HINTS + ${_usd_SEARCH_DIRS} + PATH_SUFFIXES + include + DOC "Universal Scene Description (USD) header files" +) + +FIND_LIBRARY(USD_LIBRARY + NAMES + usd_m + HINTS + ${_usd_SEARCH_DIRS} + PATH_SUFFIXES + lib64 lib lib/static + DOC "Universal Scene Description (USD) monolithic library" +) + +IF(${USD_LIBRARY_NOTFOUND}) + set(USD_FOUND FALSE) +ELSE() + # handle the QUIETLY and REQUIRED arguments and set USD_FOUND to TRUE if + # all listed variables are TRUE + INCLUDE(FindPackageHandleStandardArgs) + FIND_PACKAGE_HANDLE_STANDARD_ARGS(USD DEFAULT_MSG USD_LIBRARY USD_INCLUDE_DIR) + + IF(USD_FOUND) + get_filename_component(USD_LIBRARY_DIR ${USD_LIBRARY} DIRECTORY) + SET(USD_INCLUDE_DIRS ${USD_INCLUDE_DIR}) + set(USD_LIBRARIES ${USD_LIBRARY}) + ENDIF(USD_FOUND) +ENDIF() + +MARK_AS_ADVANCED( + USD_INCLUDE_DIR + USD_LIBRARY_DIR +) + +UNSET(_usd_SEARCH_DIRS) diff --git a/build_files/cmake/config/blender_full.cmake b/build_files/cmake/config/blender_full.cmake index fae15ea979b..2425354aa76 100644 --- a/build_files/cmake/config/blender_full.cmake +++ b/build_files/cmake/config/blender_full.cmake @@ -47,6 +47,7 @@ set(WITH_PYTHON_INSTALL ON CACHE BOOL "" FORCE) set(WITH_QUADRIFLOW ON CACHE BOOL "" FORCE) set(WITH_SDL ON CACHE BOOL "" FORCE) set(WITH_TBB ON CACHE BOOL "" FORCE) +set(WITH_USD ON CACHE BOOL "" FORCE) set(WITH_MEM_JEMALLOC ON CACHE BOOL "" FORCE) diff --git a/build_files/cmake/config/blender_release.cmake b/build_files/cmake/config/blender_release.cmake index 07d95a84112..88285e07375 100644 --- a/build_files/cmake/config/blender_release.cmake +++ b/build_files/cmake/config/blender_release.cmake @@ -48,6 +48,7 @@ set(WITH_PYTHON_INSTALL ON CACHE BOOL "" FORCE) set(WITH_QUADRIFLOW ON CACHE BOOL "" FORCE) set(WITH_SDL ON CACHE BOOL "" FORCE) set(WITH_TBB ON CACHE BOOL "" FORCE) +set(WITH_USD ON CACHE BOOL "" FORCE) set(WITH_MEM_JEMALLOC ON CACHE BOOL "" FORCE) set(WITH_CYCLES_CUDA_BINARIES ON CACHE BOOL "" FORCE) diff --git a/build_files/cmake/macros.cmake b/build_files/cmake/macros.cmake index 3944a8b3c56..3b6b4720a7c 100644 --- a/build_files/cmake/macros.cmake +++ b/build_files/cmake/macros.cmake @@ -466,6 +466,22 @@ function(setup_liblinks if(WITH_OPENVDB) target_link_libraries(${target} ${OPENVDB_LIBRARIES} ${BLOSC_LIBRARIES}) endif() + if(WITH_USD) + # Source: https://github.com/PixarAnimationStudios/USD/blob/master/BUILDING.md#linking-whole-archives + if(WIN32) + target_link_libraries(${target} ${USD_LIBRARIES}) + set_property(TARGET ${target} APPEND_STRING PROPERTY LINK_FLAGS_DEBUG " /WHOLEARCHIVE:libusd_m_d.lib") + set_property(TARGET ${target} APPEND_STRING PROPERTY LINK_FLAGS_RELEASE " /WHOLEARCHIVE:libusd_m.lib") + set_property(TARGET ${target} APPEND_STRING PROPERTY LINK_FLAGS_RELWITHDEBINFO " /WHOLEARCHIVE:libusd_m.lib") + set_property(TARGET ${target} APPEND_STRING PROPERTY LINK_FLAGS_MINSIZEREL " /WHOLEARCHIVE:libusd_m.lib") + elseif(CMAKE_COMPILER_IS_GNUCXX) + target_link_libraries(${target} -Wl,--whole-archive ${USD_LIBRARIES} -Wl,--no-whole-archive) + elseif("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") + target_link_libraries(${target} -Wl,-force_load ${USD_LIBRARIES}) + else() + message(FATAL_ERROR "Unknown how to link USD with your compiler ${CMAKE_CXX_COMPILER_ID}") + endif() + endif() if(WITH_OPENIMAGEIO) target_link_libraries(${target} ${OPENIMAGEIO_LIBRARIES}) endif() @@ -1220,10 +1236,10 @@ macro(openmp_delayload else() set(OPENMP_DLL_NAME "vcomp140") endif() - SET_TARGET_PROPERTIES(${projectname} PROPERTIES LINK_FLAGS_RELEASE "/DELAYLOAD:${OPENMP_DLL_NAME}.dll delayimp.lib") - SET_TARGET_PROPERTIES(${projectname} PROPERTIES LINK_FLAGS_DEBUG "/DELAYLOAD:${OPENMP_DLL_NAME}d.dll delayimp.lib") - SET_TARGET_PROPERTIES(${projectname} PROPERTIES LINK_FLAGS_RELWITHDEBINFO "/DELAYLOAD:${OPENMP_DLL_NAME}.dll delayimp.lib") - SET_TARGET_PROPERTIES(${projectname} PROPERTIES LINK_FLAGS_MINSIZEREL "/DELAYLOAD:${OPENMP_DLL_NAME}.dll delayimp.lib") + set_property(TARGET ${projectname} APPEND_STRING PROPERTY LINK_FLAGS_RELEASE " /DELAYLOAD:${OPENMP_DLL_NAME}.dll delayimp.lib") + set_property(TARGET ${projectname} APPEND_STRING PROPERTY LINK_FLAGS_DEBUG " /DELAYLOAD:${OPENMP_DLL_NAME}d.dll delayimp.lib") + set_property(TARGET ${projectname} APPEND_STRING PROPERTY LINK_FLAGS_RELWITHDEBINFO " /DELAYLOAD:${OPENMP_DLL_NAME}.dll delayimp.lib") + set_property(TARGET ${projectname} APPEND_STRING PROPERTY LINK_FLAGS_MINSIZEREL " /DELAYLOAD:${OPENMP_DLL_NAME}.dll delayimp.lib") endif() endif() endmacro() diff --git a/build_files/cmake/platform/platform_apple.cmake b/build_files/cmake/platform/platform_apple.cmake index 578ad143d59..d99d7ec3c0c 100644 --- a/build_files/cmake/platform/platform_apple.cmake +++ b/build_files/cmake/platform/platform_apple.cmake @@ -56,6 +56,11 @@ if(WITH_ALEMBIC) set(ALEMBIC_FOUND ON) endif() +if(WITH_USD) + set(USD_LIBRARIES ${LIBDIR}/usd/lib/libusd_m.a) + SET(USD_INCLUDE_DIRS ${LIBDIR}/usd/include) +endif() + if(WITH_OPENSUBDIV) set(OPENSUBDIV ${LIBDIR}/opensubdiv) set(OPENSUBDIV_LIBPATH ${OPENSUBDIV}/lib) diff --git a/build_files/cmake/platform/platform_unix.cmake b/build_files/cmake/platform/platform_unix.cmake index c48780ebd6a..d4a75e5e5c0 100644 --- a/build_files/cmake/platform/platform_unix.cmake +++ b/build_files/cmake/platform/platform_unix.cmake @@ -285,6 +285,14 @@ if(WITH_ALEMBIC) endif() endif() +if(WITH_USD) + find_package_wrapper(USD) + + if(NOT USD_FOUND) + set(WITH_USD OFF) + endif() +endif() + if(WITH_BOOST) # uses in build instructions to override include and library variables if(NOT BOOST_CUSTOM) diff --git a/build_files/cmake/platform/platform_win32.cmake b/build_files/cmake/platform/platform_win32.cmake index 9061e1fcf82..b1d1942598d 100644 --- a/build_files/cmake/platform/platform_win32.cmake +++ b/build_files/cmake/platform/platform_win32.cmake @@ -113,7 +113,7 @@ set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} /SAFESEH:NO") list(APPEND PLATFORM_LINKLIBS ws2_32 vfw32 winmm kernel32 user32 gdi32 comdlg32 Comctl32 - advapi32 shfolder shell32 ole32 oleaut32 uuid psapi Dbghelp + advapi32 shfolder shell32 ole32 oleaut32 uuid psapi Dbghelp Shlwapi ) if(WITH_INPUT_IME) @@ -651,6 +651,18 @@ if(WITH_CYCLES_EMBREE) endif() endif() +if(WITH_USD) + windows_find_package(USD) + if(NOT USD_FOUND) + set(USD_FOUND ON) + set(USD_INCLUDE_DIRS ${LIBDIR}/usd/include) + set(USD_LIBRARIES + debug ${LIBDIR}/usd/lib/libusd_m_d.lib + optimized ${LIBDIR}/usd/lib/libusd_m.lib + ) + endif() +endif() + if(WINDOWS_PYTHON_DEBUG) # Include the system scripts in the blender_python_system_scripts project. FILE(GLOB_RECURSE inFiles "${CMAKE_SOURCE_DIR}/release/scripts/*.*" ) diff --git a/release/scripts/startup/bl_ui/space_topbar.py b/release/scripts/startup/bl_ui/space_topbar.py index 2e2c5adb970..e69a28d69bf 100644 --- a/release/scripts/startup/bl_ui/space_topbar.py +++ b/release/scripts/startup/bl_ui/space_topbar.py @@ -435,11 +435,13 @@ class TOPBAR_MT_file_export(Menu): bl_idname = "TOPBAR_MT_file_export" bl_label = "Export" - def draw(self, _context): + def draw(self, context): if bpy.app.build_options.collada: self.layout.operator("wm.collada_export", text="Collada (Default) (.dae)") if bpy.app.build_options.alembic: self.layout.operator("wm.alembic_export", text="Alembic (.abc)") + if bpy.app.build_options.usd and context.preferences.experimental.use_usd_exporter: + self.layout.operator("wm.usd_export", text="Universal Scene Description (.usd, .usdc, .usda)") class TOPBAR_MT_file_external_data(Menu): diff --git a/release/scripts/startup/bl_ui/space_userpref.py b/release/scripts/startup/bl_ui/space_userpref.py index bf39cbda391..68581ee2ad8 100644 --- a/release/scripts/startup/bl_ui/space_userpref.py +++ b/release/scripts/startup/bl_ui/space_userpref.py @@ -2218,6 +2218,20 @@ class USERPREF_PT_experimental_virtual_reality(ExperimentalPanel, Panel): """ +class USERPREF_PT_experimental_usd(ExperimentalPanel, Panel): + bl_label = "Universal Scene Description" + + def draw_props(self, context, layout): + prefs = context.preferences + + split = layout.split(factor=0.66) + col = split.split() + col.prop(prefs.experimental, "use_usd_exporter", text="USD Exporter") + col = split.split() + url = "https://devtalk.blender.org/t/universal-scene-description-usd-exporter-feedback/10920" + col.operator("wm.url_open", text='Give Feedback', icon='URL').url = url + + # Order of registration defines order in UI, # so dynamically generated classes are 'injected' in the intended order. classes = ( @@ -2300,6 +2314,7 @@ classes = ( USERPREF_PT_studiolight_world, USERPREF_PT_experimental_ui, + USERPREF_PT_experimental_usd, # Popovers. USERPREF_PT_ndof_settings, diff --git a/source/blender/CMakeLists.txt b/source/blender/CMakeLists.txt index 62923d18b70..68d7b6d9b2d 100644 --- a/source/blender/CMakeLists.txt +++ b/source/blender/CMakeLists.txt @@ -158,3 +158,6 @@ endif() if(WIN32) add_subdirectory(blendthumb) endif() +if(WITH_USD) + add_subdirectory(usd) +endif() diff --git a/source/blender/editors/io/CMakeLists.txt b/source/blender/editors/io/CMakeLists.txt index 5a35b251d0c..5afe348158f 100644 --- a/source/blender/editors/io/CMakeLists.txt +++ b/source/blender/editors/io/CMakeLists.txt @@ -26,6 +26,7 @@ set(INC ../../depsgraph ../../makesdna ../../makesrna + ../../usd ../../windowmanager ../../../../intern/guardedalloc ) @@ -39,11 +40,13 @@ set(SRC io_cache.c io_collada.c io_ops.c + io_usd.c io_alembic.h io_cache.h io_collada.h io_ops.h + io_usd.h ) set(LIB @@ -69,6 +72,13 @@ if(WITH_ALEMBIC) endif() endif() +if(WITH_USD) + list(APPEND LIB + bf_usd + ) + add_definitions(-DWITH_USD) +endif() + if(WITH_INTERNATIONAL) add_definitions(-DWITH_INTERNATIONAL) endif() diff --git a/source/blender/editors/io/io_ops.c b/source/blender/editors/io/io_ops.c index e04fe4a20c0..acb511a414d 100644 --- a/source/blender/editors/io/io_ops.c +++ b/source/blender/editors/io/io_ops.c @@ -33,6 +33,10 @@ # include "io_alembic.h" #endif +#ifdef WITH_USD +# include "io_usd.h" +#endif + #include "io_cache.h" void ED_operatortypes_io(void) @@ -46,6 +50,9 @@ void ED_operatortypes_io(void) WM_operatortype_append(WM_OT_alembic_import); WM_operatortype_append(WM_OT_alembic_export); #endif +#ifdef WITH_USD + WM_operatortype_append(WM_OT_usd_export); +#endif WM_operatortype_append(CACHEFILE_OT_open); WM_operatortype_append(CACHEFILE_OT_reload); diff --git a/source/blender/editors/io/io_usd.c b/source/blender/editors/io/io_usd.c new file mode 100644 index 00000000000..524d8d50b7b --- /dev/null +++ b/source/blender/editors/io/io_usd.c @@ -0,0 +1,240 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup editor/io + */ + +#ifdef WITH_USD +# include "DNA_space_types.h" + +# include "BKE_context.h" +# include "BKE_main.h" +# include "BKE_report.h" + +# include "BLI_path_util.h" +# include "BLI_string.h" +# include "BLI_utildefines.h" + +# include "MEM_guardedalloc.h" + +# include "RNA_access.h" +# include "RNA_define.h" + +# include "UI_interface.h" +# include "UI_resources.h" + +# include "WM_api.h" +# include "WM_types.h" + +# include "DEG_depsgraph.h" + +# include "io_usd.h" +# include "usd.h" + +const EnumPropertyItem rna_enum_usd_export_evaluation_mode_items[] = { + {DAG_EVAL_RENDER, + "RENDER", + 0, + "Render", + "Use Render settings for object visibility, modifier settings, etc"}, + {DAG_EVAL_VIEWPORT, + "VIEWPORT", + 0, + "Viewport", + "Use Viewport settings for object visibility, modifier settings, etc"}, + {0, NULL, 0, NULL, NULL}, +}; + +/* Stored in the wmOperator's customdata field to indicate it should run as a background job. + * This is set when the operator is invoked, and not set when it is only executed. */ +enum { AS_BACKGROUND_JOB = 1 }; +typedef struct eUSDOperatorOptions { + bool as_background_job; +} eUSDOperatorOptions; + +static int wm_usd_export_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event)) +{ + eUSDOperatorOptions *options = MEM_callocN(sizeof(eUSDOperatorOptions), "eUSDOperatorOptions"); + options->as_background_job = true; + op->customdata = options; + + if (!RNA_struct_property_is_set(op->ptr, "filepath")) { + Main *bmain = CTX_data_main(C); + char filepath[FILE_MAX]; + const char *main_blendfile_path = BKE_main_blendfile_path(bmain); + + if (main_blendfile_path[0] == '\0') { + BLI_strncpy(filepath, "untitled", sizeof(filepath)); + } + else { + BLI_strncpy(filepath, main_blendfile_path, sizeof(filepath)); + } + + BLI_path_extension_replace(filepath, sizeof(filepath), ".usdc"); + RNA_string_set(op->ptr, "filepath", filepath); + } + + WM_event_add_fileselect(C, op); + + return OPERATOR_RUNNING_MODAL; +} + +static int wm_usd_export_exec(bContext *C, wmOperator *op) +{ + if (!RNA_struct_property_is_set(op->ptr, "filepath")) { + BKE_report(op->reports, RPT_ERROR, "No filename given"); + return OPERATOR_CANCELLED; + } + + char filename[FILE_MAX]; + RNA_string_get(op->ptr, "filepath", filename); + + eUSDOperatorOptions *options = (eUSDOperatorOptions *)op->customdata; + const bool as_background_job = (options != NULL && options->as_background_job); + MEM_SAFE_FREE(op->customdata); + + const bool selected_objects_only = RNA_boolean_get(op->ptr, "selected_objects_only"); + const bool visible_objects_only = RNA_boolean_get(op->ptr, "visible_objects_only"); + const bool export_animation = RNA_boolean_get(op->ptr, "export_animation"); + const bool export_hair = RNA_boolean_get(op->ptr, "export_hair"); + const bool export_uvmaps = RNA_boolean_get(op->ptr, "export_uvmaps"); + const bool export_normals = RNA_boolean_get(op->ptr, "export_normals"); + const bool export_materials = RNA_boolean_get(op->ptr, "export_materials"); + const bool use_instancing = RNA_boolean_get(op->ptr, "use_instancing"); + const bool evaluation_mode = RNA_enum_get(op->ptr, "evaluation_mode"); + + struct USDExportParams params = { + export_animation, + export_hair, + export_uvmaps, + export_normals, + export_materials, + selected_objects_only, + visible_objects_only, + use_instancing, + evaluation_mode, + }; + + bool ok = USD_export(C, filename, ¶ms, as_background_job); + + return as_background_job || ok ? OPERATOR_FINISHED : OPERATOR_CANCELLED; +} + +static void wm_usd_export_draw(bContext *UNUSED(C), wmOperator *op) +{ + uiLayout *layout = op->layout; + uiLayout *col; + struct PointerRNA *ptr = op->ptr; + + uiLayoutSetPropSep(layout, true); + + col = uiLayoutColumn(layout, true); + uiItemR(col, ptr, "selected_objects_only", 0, NULL, ICON_NONE); + uiItemR(col, ptr, "visible_objects_only", 0, NULL, ICON_NONE); + + col = uiLayoutColumn(layout, true); + uiItemR(col, ptr, "export_animation", 0, NULL, ICON_NONE); + uiItemR(col, ptr, "export_hair", 0, NULL, ICON_NONE); + uiItemR(col, ptr, "export_uvmaps", 0, NULL, ICON_NONE); + uiItemR(col, ptr, "export_normals", 0, NULL, ICON_NONE); + uiItemR(col, ptr, "export_materials", 0, NULL, ICON_NONE); + uiItemR(col, ptr, "use_instancing", 0, NULL, ICON_NONE); + + col = uiLayoutColumn(layout, true); + uiItemR(col, ptr, "evaluation_mode", 0, NULL, ICON_NONE); +} + +void WM_OT_usd_export(struct wmOperatorType *ot) +{ + ot->name = "Export USD"; + ot->description = "Export current scene in a USD archive"; + ot->idname = "WM_OT_usd_export"; + + ot->invoke = wm_usd_export_invoke; + ot->exec = wm_usd_export_exec; + ot->poll = WM_operator_winactive; + ot->ui = wm_usd_export_draw; + + WM_operator_properties_filesel(ot, + FILE_TYPE_FOLDER | FILE_TYPE_USD, + FILE_BLENDER, + FILE_SAVE, + WM_FILESEL_FILEPATH, + FILE_DEFAULTDISPLAY, + FILE_SORT_ALPHA); + + RNA_def_boolean(ot->srna, + "selected_objects_only", + false, + "Only Export Selected Objects", + "Only selected objects are exported. Unselected parents of selected objects are " + "exported as empty transform"); + + RNA_def_boolean(ot->srna, + "visible_objects_only", + true, + "Only Export Visible Objects", + "Only visible objects are exported. Invisible parents of visible objects are " + "exported as empty transform"); + + RNA_def_boolean(ot->srna, + "export_animation", + false, + "Export Animation", + "When checked, the render frame range is exported. When false, only the current " + "frame is exported"); + RNA_def_boolean(ot->srna, + "export_hair", + false, + "Export Hair", + "When checked, hair is exported as USD curves"); + RNA_def_boolean(ot->srna, + "export_uvmaps", + true, + "Export UV Maps", + "When checked, all UV maps of exported meshes are included in the export"); + RNA_def_boolean(ot->srna, + "export_normals", + true, + "Export Normals", + "When checked, normals of exported meshes are included in the export"); + RNA_def_boolean(ot->srna, + "export_materials", + true, + "Export Materials", + "When checked, the viewport settings of materials are exported as USD preview " + "materials, and material assignments are exported as geometry subsets"); + + RNA_def_boolean(ot->srna, + "use_instancing", + false, + "Use Instancing (EXPERIMENTAL)", + "When true, dupli-objects are written as instances of the original in USD. " + "Experimental feature, not working perfectly"); + + RNA_def_enum(ot->srna, + "evaluation_mode", + rna_enum_usd_export_evaluation_mode_items, + DAG_EVAL_RENDER, + "Evaluation Mode", + "Determines visibility of objects and modifier settings"); +} + +#endif /* WITH_USD */ diff --git a/source/blender/editors/io/io_usd.h b/source/blender/editors/io/io_usd.h new file mode 100644 index 00000000000..4738e1c348d --- /dev/null +++ b/source/blender/editors/io/io_usd.h @@ -0,0 +1,31 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ + +#ifndef __IO_USD_H__ +#define __IO_USD_H__ + +/** \file + * \ingroup editor/io + */ + +struct wmOperatorType; + +void WM_OT_usd_export(struct wmOperatorType *ot); + +#endif /* __IO_USD_H__ */ diff --git a/source/blender/editors/space_file/filelist.c b/source/blender/editors/space_file/filelist.c index 1ea7d81f9da..a567aeed826 100644 --- a/source/blender/editors/space_file/filelist.c +++ b/source/blender/editors/space_file/filelist.c @@ -1001,6 +1001,9 @@ static int filelist_geticon_ex(const int typeflag, else if (typeflag & FILE_TYPE_ALEMBIC) { return ICON_FILE_3D; } + else if (typeflag & FILE_TYPE_USD) { + return ICON_FILE_3D; + } else if (typeflag & FILE_TYPE_OBJECT_IO) { return ICON_FILE_3D; } @@ -2130,6 +2133,9 @@ int ED_path_extension_type(const char *path) else if (BLI_path_extension_check(path, ".abc")) { return FILE_TYPE_ALEMBIC; } + else if (BLI_path_extension_check_n(path, ".usd", ".usda", ".usdc", NULL)) { + return FILE_TYPE_USD; + } else if (BLI_path_extension_check(path, ".zip")) { return FILE_TYPE_ARCHIVE; } diff --git a/source/blender/editors/space_file/filesel.c b/source/blender/editors/space_file/filesel.c index 99e4fc62980..02fb98aa7d7 100644 --- a/source/blender/editors/space_file/filesel.c +++ b/source/blender/editors/space_file/filesel.c @@ -215,6 +215,9 @@ short ED_fileselect_set_params(SpaceFile *sfile) if ((prop = RNA_struct_find_property(op->ptr, "filter_alembic"))) { params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_ALEMBIC : 0; } + if ((prop = RNA_struct_find_property(op->ptr, "filter_usd"))) { + params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_USD : 0; + } if ((prop = RNA_struct_find_property(op->ptr, "filter_glob"))) { /* Protection against pyscripts not setting proper size limit... */ char *tmp = RNA_property_string_get_alloc( diff --git a/source/blender/makesdna/DNA_space_types.h b/source/blender/makesdna/DNA_space_types.h index 2fb439c8074..8261b9e678b 100644 --- a/source/blender/makesdna/DNA_space_types.h +++ b/source/blender/makesdna/DNA_space_types.h @@ -859,6 +859,7 @@ typedef enum eFileSel_File_Types { FILE_TYPE_ALEMBIC = (1 << 16), /** For all kinds of recognized import/export formats. No need for specialized types. */ FILE_TYPE_OBJECT_IO = (1 << 17), + FILE_TYPE_USD = (1 << 18), /** An FS directory (i.e. S_ISDIR on its path is true). */ FILE_TYPE_DIR = (1 << 30), diff --git a/source/blender/makesdna/DNA_userdef_types.h b/source/blender/makesdna/DNA_userdef_types.h index c378f52d7ba..fb23b39a616 100644 --- a/source/blender/makesdna/DNA_userdef_types.h +++ b/source/blender/makesdna/DNA_userdef_types.h @@ -602,8 +602,9 @@ typedef struct UserDef_FileSpaceData { typedef struct UserDef_Experimental { char use_tool_fallback; + char use_usd_exporter; - char _pad0[7]; + char _pad0[6]; } UserDef_Experimental; #define USER_EXPERIMENTAL_TEST(userdef, member) \ diff --git a/source/blender/makesrna/intern/rna_userdef.c b/source/blender/makesrna/intern/rna_userdef.c index 1267cfed3d8..f1aaa13fcf5 100644 --- a/source/blender/makesrna/intern/rna_userdef.c +++ b/source/blender/makesrna/intern/rna_userdef.c @@ -583,6 +583,7 @@ static void rna_userdef_autosave_update(Main *bmain, Scene *scene, PointerRNA *p } RNA_USERDEF_EXPERIMENTAL_BOOLEAN_GET(use_tool_fallback) +RNA_USERDEF_EXPERIMENTAL_BOOLEAN_GET(use_usd_exporter) static bAddon *rna_userdef_addon_new(void) { @@ -5863,6 +5864,12 @@ static void rna_def_userdef_experimental(BlenderRNA *brna) RNA_def_property_boolean_funcs(prop, "rna_userdef_experimental_use_tool_fallback_get", NULL); RNA_def_property_ui_text(prop, "Fallback Tool Support", "Allow selection with an active tool"); RNA_def_property_update(prop, 0, "rna_userdef_update"); + + prop = RNA_def_property(srna, "use_usd_exporter", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, NULL, "use_usd_exporter", 1); + RNA_def_property_boolean_funcs(prop, "rna_userdef_experimental_use_usd_exporter_get", NULL); + RNA_def_property_ui_text(prop, "USD Exporter", "Enable exporting to the USD format"); + RNA_def_property_update(prop, 0, "rna_userdef_update"); } static void rna_def_userdef_addon_collection(BlenderRNA *brna, PropertyRNA *cprop) diff --git a/source/blender/python/intern/CMakeLists.txt b/source/blender/python/intern/CMakeLists.txt index a5f71e92438..d5f0e2fb863 100644 --- a/source/blender/python/intern/CMakeLists.txt +++ b/source/blender/python/intern/CMakeLists.txt @@ -299,6 +299,10 @@ if(WITH_ALEMBIC) ) endif() +if(WITH_USD) + add_definitions(-DWITH_USD) +endif() + if(WITH_OPENIMAGEIO) add_definitions(-DWITH_OPENIMAGEIO) list(APPEND INC diff --git a/source/blender/python/intern/bpy_app_build_options.c b/source/blender/python/intern/bpy_app_build_options.c index afb2f6b3636..ee6a3fd41d8 100644 --- a/source/blender/python/intern/bpy_app_build_options.c +++ b/source/blender/python/intern/bpy_app_build_options.c @@ -60,6 +60,7 @@ static PyStructSequence_Field app_builtopts_info_fields[] = { {(char *)"openmp", NULL}, {(char *)"openvdb", NULL}, {(char *)"alembic", NULL}, + {(char *)"usd", NULL}, {NULL}, }; @@ -275,6 +276,12 @@ static PyObject *make_builtopts_info(void) SetObjIncref(Py_False); #endif +#ifdef WITH_USD + SetObjIncref(Py_True); +#else + SetObjIncref(Py_False); +#endif + #undef SetObjIncref return builtopts_info; diff --git a/source/blender/usd/CMakeLists.txt b/source/blender/usd/CMakeLists.txt new file mode 100644 index 00000000000..12d281f643d --- /dev/null +++ b/source/blender/usd/CMakeLists.txt @@ -0,0 +1,81 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# The Original Code is Copyright (C) 2019, Blender Foundation +# All rights reserved. +# ***** END GPL LICENSE BLOCK ***** + +# This suppresses the warning "This file includes at least one deprecated or antiquated +# header which may be removed without further notice at a future date", which is caused +# by the USD library including <ext/hash_set> on Linux. This has been reported at: +# https://github.com/PixarAnimationStudios/USD/issues/1057. +if(UNIX AND NOT APPLE) + add_definitions(-D_GLIBCXX_PERMIT_BACKWARD_HASH) +endif() +if(WIN32) + add_definitions(-DNOMINMAX) +endif() +add_definitions(-DPXR_STATIC) + +set(INC + . + ../blenkernel + ../blenlib + ../blenloader + ../bmesh + ../depsgraph + ../editors/include + ../makesdna + ../makesrna + ../windowmanager + ../../../intern/guardedalloc + ../../../intern/utfconv +) + +set(INC_SYS + ${USD_INCLUDE_DIRS} + ${BOOST_INCLUDE_DIR} + ${TBB_INCLUDE_DIR} +) + +set(SRC + intern/abstract_hierarchy_iterator.cc + intern/usd_capi.cc + intern/usd_hierarchy_iterator.cc + intern/usd_writer_abstract.cc + intern/usd_writer_camera.cc + intern/usd_writer_hair.cc + intern/usd_writer_light.cc + intern/usd_writer_mesh.cc + intern/usd_writer_transform.cc + + usd.h + intern/abstract_hierarchy_iterator.h + intern/usd_hierarchy_iterator.h + intern/usd_writer_abstract.h + intern/usd_writer_camera.h + intern/usd_writer_hair.h + intern/usd_writer_light.h + intern/usd_writer_mesh.h + intern/usd_writer_transform.h +) + +set(LIB + bf_blenkernel + bf_blenlib +) + +blender_add_lib(bf_usd "${SRC}" "${INC}" "${INC_SYS}" "${LIB}") diff --git a/source/blender/usd/intern/abstract_hierarchy_iterator.cc b/source/blender/usd/intern/abstract_hierarchy_iterator.cc new file mode 100644 index 00000000000..3ad2b2ce5d8 --- /dev/null +++ b/source/blender/usd/intern/abstract_hierarchy_iterator.cc @@ -0,0 +1,559 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#include "abstract_hierarchy_iterator.h" + +#include <iostream> +#include <limits.h> +#include <sstream> +#include <string> + +extern "C" { +#include "BKE_anim.h" +#include "BKE_particle.h" + +#include "BLI_assert.h" +#include "BLI_listbase.h" +#include "BLI_math_matrix.h" + +#include "DNA_ID.h" +#include "DNA_layer_types.h" +#include "DNA_object_types.h" +#include "DNA_particle_types.h" + +#include "DEG_depsgraph_query.h" +} + +namespace USD { + +const HierarchyContext *HierarchyContext::root() +{ + return nullptr; +} + +bool HierarchyContext::operator<(const HierarchyContext &other) const +{ + if (object != other.object) { + return object < other.object; + } + if (duplicator != NULL && duplicator == other.duplicator) { + // Only resort to string comparisons when both objects are created by the same duplicator. + return export_name < other.export_name; + } + + return export_parent < other.export_parent; +} + +bool HierarchyContext::is_instance() const +{ + return !original_export_path.empty(); +} +void HierarchyContext::mark_as_instance_of(const std::string &reference_export_path) +{ + original_export_path = reference_export_path; +} +void HierarchyContext::mark_as_not_instanced() +{ + original_export_path.clear(); +} + +AbstractHierarchyWriter::~AbstractHierarchyWriter() +{ +} + +AbstractHierarchyIterator::AbstractHierarchyIterator(Depsgraph *depsgraph) + : depsgraph_(depsgraph), writers_() +{ +} + +AbstractHierarchyIterator::~AbstractHierarchyIterator() +{ +} + +void AbstractHierarchyIterator::iterate_and_write() +{ + export_graph_construct(); + export_graph_prune(); + determine_export_paths(HierarchyContext::root()); + determine_duplication_references(HierarchyContext::root(), ""); + make_writers(HierarchyContext::root()); + export_graph_clear(); +} + +void AbstractHierarchyIterator::release_writers() +{ + for (WriterMap::value_type it : writers_) { + delete_object_writer(it.second); + } + writers_.clear(); +} + +std::string AbstractHierarchyIterator::make_valid_name(const std::string &name) const +{ + return name; +} + +std::string AbstractHierarchyIterator::get_id_name(const ID *id) const +{ + if (id == nullptr) { + return ""; + } + + return make_valid_name(std::string(id->name + 2)); +} + +std::string AbstractHierarchyIterator::get_object_data_path(const HierarchyContext *context) const +{ + BLI_assert(!context->export_path.empty()); + BLI_assert(context->object->data); + + return path_concatenate(context->export_path, get_object_data_name(context->object)); +} + +void AbstractHierarchyIterator::debug_print_export_graph() const +{ + size_t total_graph_size = 0; + for (const ExportGraph::value_type &map_iter : export_graph_) { + const DupliAndDuplicator &parent_info = map_iter.first; + Object *const export_parent = parent_info.first; + Object *const duplicator = parent_info.second; + + if (duplicator != nullptr) { + printf(" DU %s (as dupped by %s):\n", + export_parent == nullptr ? "-null-" : (export_parent->id.name + 2), + duplicator->id.name + 2); + } + else { + printf(" OB %s:\n", export_parent == nullptr ? "-null-" : (export_parent->id.name + 2)); + } + + total_graph_size += map_iter.second.size(); + for (HierarchyContext *child_ctx : map_iter.second) { + if (child_ctx->duplicator == nullptr) { + printf(" - %s%s%s\n", + child_ctx->object->id.name + 2, + child_ctx->weak_export ? " (weak)" : "", + child_ctx->original_export_path.size() ? + (std::string("ref ") + child_ctx->original_export_path).c_str() : + ""); + } + else { + printf(" - %s (dup by %s%s) %s\n", + child_ctx->object->id.name + 2, + child_ctx->duplicator->id.name + 2, + child_ctx->weak_export ? ", weak" : "", + child_ctx->original_export_path.size() ? + (std::string("ref ") + child_ctx->original_export_path).c_str() : + ""); + } + } + } + printf(" (Total graph size: %lu objects\n", total_graph_size); +} + +void AbstractHierarchyIterator::export_graph_construct() +{ + Scene *scene = DEG_get_evaluated_scene(depsgraph_); + + DEG_OBJECT_ITER_BEGIN (depsgraph_, + object, + DEG_ITER_OBJECT_FLAG_LINKED_DIRECTLY | + DEG_ITER_OBJECT_FLAG_LINKED_VIA_SET) { + if (object->base_flag & BASE_HOLDOUT) { + visit_object(object, object->parent, true); + continue; + } + + // Non-instanced objects always have their object-parent as export-parent. + const bool weak_export = mark_as_weak_export(object); + visit_object(object, object->parent, weak_export); + + if (weak_export) { + // If a duplicator shouldn't be exported, its duplilist also shouldn't be. + continue; + } + + // Export the duplicated objects instanced by this object. + ListBase *lb = object_duplilist(depsgraph_, scene, object); + if (lb) { + // Construct the set of duplicated objects, so that later we can determine whether a parent + // is also duplicated itself. + std::set<Object *> dupli_set; + LISTBASE_FOREACH (DupliObject *, dupli_object, lb) { + if (!should_visit_dupli_object(dupli_object)) { + continue; + } + dupli_set.insert(dupli_object->ob); + } + + LISTBASE_FOREACH (DupliObject *, dupli_object, lb) { + if (!should_visit_dupli_object(dupli_object)) { + continue; + } + + visit_dupli_object(dupli_object, object, dupli_set); + } + } + + free_object_duplilist(lb); + } + DEG_OBJECT_ITER_END; +} + +static bool remove_weak_subtrees(const HierarchyContext *context, + AbstractHierarchyIterator::ExportGraph &clean_graph, + const AbstractHierarchyIterator::ExportGraph &input_graph) +{ + bool all_is_weak = context != nullptr && context->weak_export; + Object *object = context != nullptr ? context->object : nullptr; + Object *duplicator = context != nullptr ? context->duplicator : nullptr; + + const AbstractHierarchyIterator::DupliAndDuplicator map_key = std::make_pair(object, duplicator); + AbstractHierarchyIterator::ExportGraph::const_iterator child_iterator; + + child_iterator = input_graph.find(map_key); + if (child_iterator != input_graph.end()) { + for (HierarchyContext *child_context : child_iterator->second) { + bool child_tree_is_weak = remove_weak_subtrees(child_context, clean_graph, input_graph); + all_is_weak &= child_tree_is_weak; + + if (child_tree_is_weak) { + // This subtree is all weak, so we can remove it from the current object's children. + clean_graph[map_key].erase(child_context); + delete child_context; + } + } + } + + if (all_is_weak) { + // This node and all its children are weak, so it can be removed from the export graph. + clean_graph.erase(map_key); + } + + return all_is_weak; +} + +void AbstractHierarchyIterator::export_graph_prune() +{ + // Take a copy of the map so that we can modify while recursing. + ExportGraph unpruned_export_graph = export_graph_; + remove_weak_subtrees(HierarchyContext::root(), export_graph_, unpruned_export_graph); +} + +void AbstractHierarchyIterator::export_graph_clear() +{ + for (ExportGraph::iterator::value_type &it : export_graph_) { + for (HierarchyContext *context : it.second) { + delete context; + } + } + export_graph_.clear(); +} + +void AbstractHierarchyIterator::visit_object(Object *object, + Object *export_parent, + bool weak_export) +{ + HierarchyContext *context = new HierarchyContext(); + context->object = object; + context->export_name = get_object_name(object); + context->export_parent = export_parent; + context->duplicator = nullptr; + context->weak_export = weak_export; + context->animation_check_include_parent = false; + context->export_path = ""; + context->original_export_path = ""; + copy_m4_m4(context->matrix_world, object->obmat); + + export_graph_[std::make_pair(export_parent, nullptr)].insert(context); +} + +void AbstractHierarchyIterator::visit_dupli_object(DupliObject *dupli_object, + Object *duplicator, + const std::set<Object *> &dupli_set) +{ + ExportGraph::key_type graph_index; + bool animation_check_include_parent = false; + + HierarchyContext *context = new HierarchyContext(); + context->object = dupli_object->ob; + context->duplicator = duplicator; + context->weak_export = false; + context->export_path = ""; + context->original_export_path = ""; + + /* If the dupli-object's parent is also instanced by this object, use that as the + * export parent. Otherwise use the dupli-parent as export parent. */ + Object *parent = dupli_object->ob->parent; + if (parent != nullptr && dupli_set.find(parent) != dupli_set.end()) { + // The parent object is part of the duplicated collection. + context->export_parent = parent; + graph_index = std::make_pair(parent, duplicator); + } + else { + /* The parent object is NOT part of the duplicated collection. This means that the world + * transform of this dupliobject can be influenced by objects that are not part of its + * export graph. */ + animation_check_include_parent = true; + context->export_parent = duplicator; + graph_index = std::make_pair(duplicator, nullptr); + } + + context->animation_check_include_parent = animation_check_include_parent; + copy_m4_m4(context->matrix_world, dupli_object->mat); + + // Construct export name for the dupli-instance. + std::stringstream suffix_stream; + suffix_stream << std::hex; + for (int i = 0; i < MAX_DUPLI_RECUR && dupli_object->persistent_id[i] != INT_MAX; i++) { + suffix_stream << "-" << dupli_object->persistent_id[i]; + } + context->export_name = make_valid_name(get_object_name(context->object) + suffix_stream.str()); + + export_graph_[graph_index].insert(context); +} + +AbstractHierarchyIterator::ExportChildren &AbstractHierarchyIterator::graph_children( + const HierarchyContext *context) +{ + if (context == nullptr) { + return export_graph_[std::make_pair(nullptr, nullptr)]; + } + + return export_graph_[std::make_pair(context->object, context->duplicator)]; +} + +void AbstractHierarchyIterator::determine_export_paths(const HierarchyContext *parent_context) +{ + const std::string &parent_export_path = parent_context ? parent_context->export_path : ""; + + for (HierarchyContext *context : graph_children(parent_context)) { + context->export_path = path_concatenate(parent_export_path, context->export_name); + + if (context->duplicator == nullptr) { + /* This is an original (i.e. non-instanced) object, so we should keep track of where it was + * exported to, just in case it gets instanced somewhere. */ + ID *source_ob = &context->object->id; + duplisource_export_path_[source_ob] = context->export_path; + + if (context->object->data != nullptr) { + ID *object_data = static_cast<ID *>(context->object->data); + ID *source_data = object_data; + duplisource_export_path_[source_data] = get_object_data_path(context); + } + } + + determine_export_paths(context); + } +} + +void AbstractHierarchyIterator::determine_duplication_references( + const HierarchyContext *parent_context, std::string indent) +{ + ExportChildren children = graph_children(parent_context); + + for (HierarchyContext *context : children) { + if (context->duplicator != nullptr) { + ID *source_id = &context->object->id; + const ExportPathMap::const_iterator &it = duplisource_export_path_.find(source_id); + + if (it == duplisource_export_path_.end()) { + // The original was not found, so mark this instance as "the original". + context->mark_as_not_instanced(); + duplisource_export_path_[source_id] = context->export_path; + } + else { + context->mark_as_instance_of(it->second); + } + + if (context->object->data) { + ID *source_data_id = (ID *)context->object->data; + const ExportPathMap::const_iterator &it = duplisource_export_path_.find(source_data_id); + + if (it == duplisource_export_path_.end()) { + // The original was not found, so mark this instance as "original". + std::string data_path = get_object_data_path(context); + context->mark_as_not_instanced(); + duplisource_export_path_[source_id] = context->export_path; + duplisource_export_path_[source_data_id] = data_path; + } + } + } + + determine_duplication_references(context, indent + " "); + } +} + +void AbstractHierarchyIterator::make_writers(const HierarchyContext *parent_context) +{ + AbstractHierarchyWriter *transform_writer = nullptr; + float parent_matrix_inv_world[4][4]; + + if (parent_context) { + invert_m4_m4(parent_matrix_inv_world, parent_context->matrix_world); + } + else { + unit_m4(parent_matrix_inv_world); + } + + const std::string &parent_export_path = parent_context ? parent_context->export_path : ""; + + for (HierarchyContext *context : graph_children(parent_context)) { + copy_m4_m4(context->parent_matrix_inv_world, parent_matrix_inv_world); + + // Get or create the transform writer. + transform_writer = ensure_writer(context, &AbstractHierarchyIterator::create_transform_writer); + if (transform_writer == nullptr) { + // Unable to export, so there is nothing to attach any children to; just abort this entire + // branch of the export hierarchy. + return; + } + + BLI_assert(DEG_is_evaluated_object(context->object)); + /* XXX This can lead to too many XForms being written. For example, a camera writer can refuse + * to write an orthographic camera. By the time that this is known, the XForm has already been + * written. */ + transform_writer->write(*context); + + if (!context->weak_export) { + make_writers_particle_systems(context); + make_writer_object_data(context); + } + + // Recurse into this object's children. + make_writers(context); + } + + // TODO(Sybren): iterate over all unused writers and call unused_during_iteration() or something. +} + +void AbstractHierarchyIterator::make_writer_object_data(const HierarchyContext *context) +{ + if (context->object->data == nullptr) { + return; + } + + HierarchyContext data_context = *context; + data_context.export_path = get_object_data_path(context); + + /* data_context.original_export_path is just a copy from the context. It points to the object, + * but needs to point to the object data. */ + if (data_context.is_instance()) { + ID *object_data = static_cast<ID *>(context->object->data); + data_context.original_export_path = duplisource_export_path_[object_data]; + + /* If the object is marked as an instance, so should the object data. */ + BLI_assert(data_context.is_instance()); + } + + AbstractHierarchyWriter *data_writer; + data_writer = ensure_writer(&data_context, &AbstractHierarchyIterator::create_data_writer); + if (data_writer == nullptr) { + return; + } + + data_writer->write(data_context); +} + +void AbstractHierarchyIterator::make_writers_particle_systems( + const HierarchyContext *transform_context) +{ + Object *object = transform_context->object; + ParticleSystem *psys = static_cast<ParticleSystem *>(object->particlesystem.first); + for (; psys; psys = psys->next) { + if (!psys_check_enabled(object, psys, true)) { + continue; + } + + HierarchyContext hair_context = *transform_context; + hair_context.export_path = path_concatenate(transform_context->export_path, + get_id_name(&psys->part->id)); + hair_context.particle_system = psys; + + AbstractHierarchyWriter *writer = nullptr; + switch (psys->part->type) { + case PART_HAIR: + writer = ensure_writer(&hair_context, &AbstractHierarchyIterator::create_hair_writer); + break; + case PART_EMITTER: + writer = ensure_writer(&hair_context, &AbstractHierarchyIterator::create_particle_writer); + break; + } + + if (writer != nullptr) { + writer->write(hair_context); + } + } +} + +std::string AbstractHierarchyIterator::get_object_name(const Object *object) const +{ + return get_id_name(&object->id); +} + +std::string AbstractHierarchyIterator::get_object_data_name(const Object *object) const +{ + ID *object_data = static_cast<ID *>(object->data); + return get_id_name(object_data); +} + +AbstractHierarchyWriter *AbstractHierarchyIterator::get_writer(const std::string &export_path) +{ + WriterMap::iterator it = writers_.find(export_path); + + if (it == writers_.end()) { + return nullptr; + } + return it->second; +} + +AbstractHierarchyWriter *AbstractHierarchyIterator::ensure_writer( + HierarchyContext *context, AbstractHierarchyIterator::create_writer_func create_func) +{ + AbstractHierarchyWriter *writer = get_writer(context->export_path); + if (writer != nullptr) { + return writer; + } + + writer = (this->*create_func)(context); + if (writer == nullptr) { + return nullptr; + } + + writers_[context->export_path] = writer; + + return writer; +} + +std::string AbstractHierarchyIterator::path_concatenate(const std::string &parent_path, + const std::string &child_path) const +{ + return parent_path + "/" + child_path; +} + +bool AbstractHierarchyIterator::mark_as_weak_export(const Object * /*object*/) const +{ + return false; +} +bool AbstractHierarchyIterator::should_visit_dupli_object(const DupliObject *dupli_object) const +{ + // Removing dupli_object->no_draw hides things like custom bone shapes. + return !dupli_object->no_draw; +} + +} // namespace USD diff --git a/source/blender/usd/intern/abstract_hierarchy_iterator.h b/source/blender/usd/intern/abstract_hierarchy_iterator.h new file mode 100644 index 00000000000..db1e4248cab --- /dev/null +++ b/source/blender/usd/intern/abstract_hierarchy_iterator.h @@ -0,0 +1,248 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ + +/* + * This file contains the AbstractHierarchyIterator. It is intended for exporters for file + * formats that concern an entire hierarchy of objects (rather than, for example, an OBJ file that + * contains only a single mesh). Examples are Universal Scene Description (USD) and Alembic. + * AbstractHierarchyIterator is intended to be subclassed to support concrete file formats. + * + * The AbstractHierarchyIterator makes a distinction between the actual object hierarchy and the + * export hierarchy. The former is the parent/child structure in Blender, which can have multiple + * parent-like objects. For example, a duplicated object can have both a duplicator and a parent, + * both determining the final transform. The export hierarchy is the hierarchy as written to the + * file, and every object has only one export-parent. + * + * Currently the AbstractHierarchyIterator does not make any decisions about *what* to export. + * Selections like "selected only" or "no hair systems" are left to concrete subclasses. + */ + +#ifndef __USD__ABSTRACT_HIERARCHY_ITERATOR_H__ +#define __USD__ABSTRACT_HIERARCHY_ITERATOR_H__ + +#include <map> +#include <string> +#include <set> + +struct Base; +struct Depsgraph; +struct DupliObject; +struct ID; +struct Object; +struct ParticleSystem; +struct ViewLayer; + +namespace USD { + +class AbstractHierarchyWriter; + +/* HierarchyContext structs are created by the AbstractHierarchyIterator. Each HierarchyContext + * struct contains everything necessary to export a single object to a file. */ +struct HierarchyContext { + /*********** Determined during hierarchy iteration: ***************/ + Object *object; + Object *export_parent; + Object *duplicator; + float matrix_world[4][4]; + std::string export_name; + + /* When weak_export=true, the object will be exported only as transform, and only if is an + * ancestor of an object with weak_export=false. + * + * In other words: when weak_export=true but this object has no children, or all decendants also + * have weak_export=true, this object (and by recursive reasoning all its decendants) will be + * excluded from the export. + * + * The export hierarchy is kept as close to the the hierarchy in Blender as possible. As such, an + * object that serves as a parent for another object, but which should NOT be exported itself, is + * exported only as transform (i.e. as empty). This happens with objects that are part of a + * holdout collection (which prevents them from being exported) but also parent of an exported + * object. */ + bool weak_export; + + /* When true, this object should check its parents for animation data when determining whether + * it's animated. This is necessary when a parent object in Blender is not part of the export. */ + bool animation_check_include_parent; + + /*********** Determined during writer creation: ***************/ + float parent_matrix_inv_world[4][4]; // Inverse of the parent's world matrix. + std::string export_path; // Hierarchical path, such as "/grandparent/parent/objectname". + ParticleSystem *particle_system; // Only set for particle/hair writers. + + /* Hierarchical path of the object this object is duplicating; only set when this object should + * be stored as a reference to its original. It can happen that the original is not part of the + * exported objects, in which case this string is empty even though 'duplicator' is set. */ + std::string original_export_path; + + bool operator<(const HierarchyContext &other) const; + + /* Return a HierarchyContext representing the root of the export hierarchy. */ + static const HierarchyContext *root(); + + /* For handling instanced collections, instances created by particles, etc. */ + bool is_instance() const; + void mark_as_instance_of(const std::string &reference_export_path); + void mark_as_not_instanced(); +}; + +/* Abstract writer for objects. Create concrete subclasses to write to USD, Alembic, etc. + * + * Instantiated by the AbstractHierarchyIterator on the first frame an object exists. Generally + * that's the first frame to be exported, but can be later, for example when objects are + * instantiated by particles. The AbstractHierarchyWriter::write() function is called on every + * frame the object exists in the dependency graph and should be exported. + */ +class AbstractHierarchyWriter { + public: + virtual ~AbstractHierarchyWriter(); + virtual void write(HierarchyContext &context) = 0; + // TODO(Sybren): add function like absent() that's called when a writer was previously created, + // but wasn't used while exporting the current frame (for example, a particle-instanced mesh of + // which the particle is no longer alive). +}; + +/* AbstractHierarchyIterator iterates over objects in a dependency graph, and constructs export + * writers. These writers are then called to perform the actual writing to a USD or Alembic file. + * + * Dealing with file- and scene-level data (for example, creating a USD scene, setting the frame + * rate, etc.) is not part of the AbstractHierarchyIterator class structure, and should be done + * in separate code. + */ +class AbstractHierarchyIterator { + public: + /* Mapping from export path to writer. */ + typedef std::map<std::string, AbstractHierarchyWriter *> WriterMap; + /* Pair of a duplicated object and its duplicator, typically a pair of HierarchyContext::object + * and HierarchyContext::duplicator. */ + typedef std::pair<Object *, Object *> DupliAndDuplicator; + /* All the children of some object, as per the export hierarchy. */ + typedef std::set<HierarchyContext *> ExportChildren; + /* Mapping from an object and its duplicator to the object's export-children. */ + typedef std::map<DupliAndDuplicator, ExportChildren> ExportGraph; + /* Mapping from (potential) duplicator ID to export path. */ + typedef std::map<ID *, std::string> ExportPathMap; + + protected: + ExportGraph export_graph_; + ExportPathMap duplisource_export_path_; + Depsgraph *depsgraph_; + WriterMap writers_; + + public: + explicit AbstractHierarchyIterator(Depsgraph *depsgraph); + virtual ~AbstractHierarchyIterator(); + + /* Iterate over the depsgraph, create writers, and tell the writers to write. + * Main entry point for the AbstractHierarchyIterator, must be called for every to-be-exported + * frame. */ + void iterate_and_write(); + + /* Release all writers. Call after all frames have been exported. */ + void release_writers(); + + /* Convert the given name to something that is valid for the exported file format. + * This base implementation is a no-op; override in a concrete subclass. */ + virtual std::string make_valid_name(const std::string &name) const; + + /* Return the name of this ID datablock that is valid for the exported file format. Overriding is + * only necessary if make_valid_name(id->name+2) is not suitable for the exported file format. + * NULL-safe: when `id == nullptr` this returns an empty string. */ + virtual std::string get_id_name(const ID *id) const; + + /* Given a HierarchyContext of some Object *, return an export path that is valid for its + * object->data. Overriding is necessary when the exported format does NOT expect the object's + * data to be a child of the object. */ + virtual std::string get_object_data_path(const HierarchyContext *context) const; + + private: + void debug_print_export_graph() const; + + void export_graph_construct(); + void export_graph_prune(); + void export_graph_clear(); + + void visit_object(Object *object, Object *export_parent, bool weak_export); + void visit_dupli_object(DupliObject *dupli_object, + Object *duplicator, + const std::set<Object *> &dupli_set); + + ExportChildren &graph_children(const HierarchyContext *parent_context); + + void determine_export_paths(const HierarchyContext *parent_context); + void determine_duplication_references(const HierarchyContext *parent_context, + std::string indent); + + void make_writers(const HierarchyContext *parent_context); + void make_writer_object_data(const HierarchyContext *context); + void make_writers_particle_systems(const HierarchyContext *context); + + /* Convenience wrappers around get_id_name(). */ + std::string get_object_name(const Object *object) const; + std::string get_object_data_name(const Object *object) const; + + AbstractHierarchyWriter *get_writer(const std::string &export_path); + + typedef AbstractHierarchyWriter *(AbstractHierarchyIterator::*create_writer_func)( + const HierarchyContext *); + /* Ensure that a writer exists; if it doesn't, call create_func(context). The create_func + * function should be one of the create_XXXX_writer(context) functions declared below. */ + AbstractHierarchyWriter *ensure_writer(HierarchyContext *context, + create_writer_func create_func); + + protected: + /* Construct a valid path for the export file format. This class concatenates by using '/' as a + * path separator, which is valid for both Alembic and USD. */ + virtual std::string path_concatenate(const std::string &parent_path, + const std::string &child_path) const; + + /* Return whether this object should be marked as 'weak export' or not. + * + * When this returns false, writers for the transform and data are created, + * and dupli-objects dupli-object generated from this object will be passed to + * should_visit_dupli_object(). + * + * When this returns true, only a transform writer is created and marked as + * 'weak export'. In this case, the transform writer will be removed before + * exporting starts, unless a decendant of this object is to be exported. + * Dupli-object generated from this object will also be skipped. + * + * See HierarchyContext::weak_export. + */ + virtual bool mark_as_weak_export(const Object *object) const; + + virtual bool should_visit_dupli_object(const DupliObject *dupli_object) const; + + /* These functions should create an AbstractHierarchyWriter subclass instance, or return + * nullptr if the object or its data should not be exported. Returning a nullptr for + * data/hair/particle will NOT prevent the transform to be written. + * + * The returned writer is owned by the AbstractHierarchyWriter, and should be freed in + * delete_object_writer(). */ + virtual AbstractHierarchyWriter *create_transform_writer(const HierarchyContext *context) = 0; + virtual AbstractHierarchyWriter *create_data_writer(const HierarchyContext *context) = 0; + virtual AbstractHierarchyWriter *create_hair_writer(const HierarchyContext *context) = 0; + virtual AbstractHierarchyWriter *create_particle_writer(const HierarchyContext *context) = 0; + + /* Called by release_writers() to free what the create_XXX_writer() functions allocated. */ + virtual void delete_object_writer(AbstractHierarchyWriter *writer) = 0; +}; + +} // namespace USD + +#endif /* __USD__ABSTRACT_HIERARCHY_ITERATOR_H__ */ diff --git a/source/blender/usd/intern/usd_capi.cc b/source/blender/usd/intern/usd_capi.cc new file mode 100644 index 00000000000..502f8677174 --- /dev/null +++ b/source/blender/usd/intern/usd_capi.cc @@ -0,0 +1,218 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ + +#include "usd.h" +#include "usd_hierarchy_iterator.h" + +#include <pxr/usd/usd/stage.h> +#include <pxr/usd/usdGeom/tokens.h> + +extern "C" { +#include "DEG_depsgraph.h" +#include "DEG_depsgraph_build.h" +#include "DEG_depsgraph_query.h" + +#include "DNA_scene_types.h" + +#include "BKE_blender_version.h" +#include "BKE_context.h" +#include "BKE_global.h" +#include "BKE_scene.h" + +#include "BLI_fileops.h" +#include "BLI_path_util.h" +#include "BLI_string.h" + +#include "MEM_guardedalloc.h" + +#include "WM_api.h" +#include "WM_types.h" +} + +namespace USD { + +struct ExportJobData { + ViewLayer *view_layer; + Main *bmain; + Depsgraph *depsgraph; + wmWindowManager *wm; + + char filename[FILE_MAX]; + USDExportParams params; + + short *stop; + short *do_update; + float *progress; + + bool was_canceled; + bool export_ok; +}; + +static void export_startjob(void *customdata, short *stop, short *do_update, float *progress) +{ + ExportJobData *data = static_cast<ExportJobData *>(customdata); + + data->stop = stop; + data->do_update = do_update; + data->progress = progress; + data->was_canceled = false; + + G.is_rendering = true; + WM_set_locked_interface(data->wm, true); + G.is_break = false; + + // Construct the depsgraph for exporting. + Scene *scene = DEG_get_input_scene(data->depsgraph); + ViewLayer *view_layer = DEG_get_input_view_layer(data->depsgraph); + DEG_graph_build_from_view_layer(data->depsgraph, data->bmain, scene, view_layer); + BKE_scene_graph_update_tagged(data->depsgraph, data->bmain); + + *progress = 0.0f; + *do_update = true; + + // For restoring the current frame after exporting animation is done. + const int orig_frame = CFRA; + + pxr::UsdStageRefPtr usd_stage = pxr::UsdStage::CreateNew(data->filename); + if (!usd_stage) { + /* This happens when the USD JSON files cannot be found. When that happens, + * the USD library doesn't know it has the functionality to write USDA and + * USDC files, and creating a new UsdStage fails. */ + WM_reportf( + RPT_ERROR, "USD Export: unable to find suitable USD plugin to write %s", data->filename); + data->export_ok = false; + return; + } + + usd_stage->SetMetadata(pxr::UsdGeomTokens->upAxis, pxr::VtValue(pxr::UsdGeomTokens->z)); + usd_stage->SetMetadata(pxr::UsdGeomTokens->metersPerUnit, + pxr::VtValue(scene->unit.scale_length)); + usd_stage->GetRootLayer()->SetDocumentation(std::string("Blender ") + versionstr); + + // Set up the stage for animated data. + if (data->params.export_animation) { + usd_stage->SetTimeCodesPerSecond(FPS); + usd_stage->SetStartTimeCode(scene->r.sfra); + usd_stage->SetEndTimeCode(scene->r.efra); + } + + USDHierarchyIterator iter(data->depsgraph, usd_stage, data->params); + + if (data->params.export_animation) { + // Writing the animated frames is not 100% of the work, but it's our best guess. + float progress_per_frame = 1.0f / std::max(1, (scene->r.efra - scene->r.sfra + 1)); + + for (float frame = scene->r.sfra; frame <= scene->r.efra; frame++) { + if (G.is_break || (stop != nullptr && *stop)) { + break; + } + + // Update the scene for the next frame to render. + scene->r.cfra = static_cast<int>(frame); + scene->r.subframe = frame - scene->r.cfra; + BKE_scene_graph_update_for_newframe(data->depsgraph, data->bmain); + + iter.set_export_frame(frame); + iter.iterate_and_write(); + + *progress += progress_per_frame; + *do_update = true; + } + } + else { + // If we're not animating, a single iteration over all objects is enough. + iter.iterate_and_write(); + } + + iter.release_writers(); + usd_stage->GetRootLayer()->Save(); + + // Finish up by going back to the keyframe that was current before we started. + if (CFRA != orig_frame) { + CFRA = orig_frame; + BKE_scene_graph_update_for_newframe(data->depsgraph, data->bmain); + } + + data->export_ok = !data->was_canceled; + + *progress = 1.0f; + *do_update = true; +} + +static void export_endjob(void *customdata) +{ + ExportJobData *data = static_cast<ExportJobData *>(customdata); + + DEG_graph_free(data->depsgraph); + + if (data->was_canceled && BLI_exists(data->filename)) { + BLI_delete(data->filename, false, false); + } + + G.is_rendering = false; + WM_set_locked_interface(data->wm, false); +} + +} // namespace USD + +bool USD_export(bContext *C, + const char *filepath, + const USDExportParams *params, + bool as_background_job) +{ + ViewLayer *view_layer = CTX_data_view_layer(C); + Scene *scene = CTX_data_scene(C); + + USD::ExportJobData *job = static_cast<USD::ExportJobData *>( + MEM_mallocN(sizeof(USD::ExportJobData), "ExportJobData")); + + job->bmain = CTX_data_main(C); + job->wm = CTX_wm_manager(C); + job->export_ok = false; + BLI_strncpy(job->filename, filepath, sizeof(job->filename)); + + job->depsgraph = DEG_graph_new(job->bmain, scene, view_layer, params->evaluation_mode); + job->params = *params; + + bool export_ok = false; + if (as_background_job) { + wmJob *wm_job = WM_jobs_get( + job->wm, CTX_wm_window(C), scene, "USD Export", WM_JOB_PROGRESS, WM_JOB_TYPE_ALEMBIC); + + /* setup job */ + WM_jobs_customdata_set(wm_job, job, MEM_freeN); + WM_jobs_timer(wm_job, 0.1, NC_SCENE | ND_FRAME, NC_SCENE | ND_FRAME); + WM_jobs_callbacks(wm_job, USD::export_startjob, NULL, NULL, USD::export_endjob); + + WM_jobs_start(CTX_wm_manager(C), wm_job); + } + else { + /* Fake a job context, so that we don't need NULL pointer checks while exporting. */ + short stop = 0, do_update = 0; + float progress = 0.f; + + USD::export_startjob(job, &stop, &do_update, &progress); + USD::export_endjob(job); + export_ok = job->export_ok; + + MEM_freeN(job); + } + + return export_ok; +} diff --git a/source/blender/usd/intern/usd_exporter_context.h b/source/blender/usd/intern/usd_exporter_context.h new file mode 100644 index 00000000000..aaa0121807c --- /dev/null +++ b/source/blender/usd/intern/usd_exporter_context.h @@ -0,0 +1,44 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#ifndef __USD__USD_EXPORTER_CONTEXT_H__ +#define __USD__USD_EXPORTER_CONTEXT_H__ + +#include "usd.h" + +#include <pxr/usd/sdf/path.h> +#include <pxr/usd/usd/common.h> + +struct Depsgraph; +struct Object; + +namespace USD { + +class USDHierarchyIterator; + +struct USDExporterContext { + const Depsgraph *depsgraph; + const pxr::UsdStageRefPtr stage; + const pxr::SdfPath usd_path; + const USDHierarchyIterator *hierarchy_iterator; + const USDExportParams &export_params; +}; + +} // namespace USD + +#endif /* __USD__USD_EXPORTER_CONTEXT_H__ */ diff --git a/source/blender/usd/intern/usd_hierarchy_iterator.cc b/source/blender/usd/intern/usd_hierarchy_iterator.cc new file mode 100644 index 00000000000..f53cba8b2c6 --- /dev/null +++ b/source/blender/usd/intern/usd_hierarchy_iterator.cc @@ -0,0 +1,150 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#include "usd.h" + +#include "usd_hierarchy_iterator.h" +#include "usd_writer_abstract.h" +#include "usd_writer_camera.h" +#include "usd_writer_hair.h" +#include "usd_writer_light.h" +#include "usd_writer_mesh.h" +#include "usd_writer_transform.h" + +#include <string> + +#include <pxr/base/tf/stringUtils.h> + +extern "C" { +#include "BKE_anim.h" + +#include "BLI_assert.h" + +#include "DEG_depsgraph_query.h" + +#include "DNA_ID.h" +#include "DNA_layer_types.h" +#include "DNA_object_types.h" +} + +namespace USD { + +USDHierarchyIterator::USDHierarchyIterator(Depsgraph *depsgraph, + pxr::UsdStageRefPtr stage, + const USDExportParams ¶ms) + : AbstractHierarchyIterator(depsgraph), stage_(stage), params_(params) +{ +} + +bool USDHierarchyIterator::mark_as_weak_export(const Object *object) const +{ + if (params_.selected_objects_only && (object->base_flag & BASE_SELECTED) == 0) { + return true; + } + if (params_.visible_objects_only && (object->base_flag & BASE_VISIBLE_VIEWLAYER) == 0) { + return true; + } + return false; +} + +void USDHierarchyIterator::delete_object_writer(AbstractHierarchyWriter *writer) +{ + delete static_cast<USDAbstractWriter *>(writer); +} + +std::string USDHierarchyIterator::make_valid_name(const std::string &name) const +{ + return pxr::TfMakeValidIdentifier(name); +} + +void USDHierarchyIterator::set_export_frame(float frame_nr) +{ + // The USD stage is already set up to have FPS timecodes per frame. + export_time_ = pxr::UsdTimeCode(frame_nr); +} + +const pxr::UsdTimeCode &USDHierarchyIterator::get_export_time_code() const +{ + return export_time_; +} + +USDExporterContext USDHierarchyIterator::create_usd_export_context(const HierarchyContext *context) +{ + return USDExporterContext{depsgraph_, stage_, pxr::SdfPath(context->export_path), this, params_}; +} + +AbstractHierarchyWriter *USDHierarchyIterator::create_transform_writer( + const HierarchyContext *context) +{ + return new USDTransformWriter(create_usd_export_context(context)); +} + +AbstractHierarchyWriter *USDHierarchyIterator::create_data_writer(const HierarchyContext *context) +{ + USDExporterContext usd_export_context = create_usd_export_context(context); + USDAbstractWriter *data_writer = nullptr; + + switch (context->object->type) { + case OB_MESH: + data_writer = new USDMeshWriter(usd_export_context); + break; + case OB_CAMERA: + data_writer = new USDCameraWriter(usd_export_context); + break; + case OB_LAMP: + data_writer = new USDLightWriter(usd_export_context); + break; + + case OB_EMPTY: + case OB_CURVE: + case OB_SURF: + case OB_FONT: + case OB_MBALL: + case OB_SPEAKER: + case OB_LIGHTPROBE: + case OB_LATTICE: + case OB_ARMATURE: + case OB_GPENCIL: + return nullptr; + case OB_TYPE_MAX: + BLI_assert(!"OB_TYPE_MAX should not be used"); + return nullptr; + } + + if (!data_writer->is_supported(context)) { + delete data_writer; + return nullptr; + } + + return data_writer; +} + +AbstractHierarchyWriter *USDHierarchyIterator::create_hair_writer(const HierarchyContext *context) +{ + if (!params_.export_hair) { + return nullptr; + } + return new USDHairWriter(create_usd_export_context(context)); +} + +AbstractHierarchyWriter *USDHierarchyIterator::create_particle_writer(const HierarchyContext *) +{ + return nullptr; +} + +} // namespace USD diff --git a/source/blender/usd/intern/usd_hierarchy_iterator.h b/source/blender/usd/intern/usd_hierarchy_iterator.h new file mode 100644 index 00000000000..a937df8f15b --- /dev/null +++ b/source/blender/usd/intern/usd_hierarchy_iterator.h @@ -0,0 +1,71 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#ifndef __USD__USD_HIERARCHY_ITERATOR_H__ +#define __USD__USD_HIERARCHY_ITERATOR_H__ + +#include "abstract_hierarchy_iterator.h" +#include "usd_exporter_context.h" +#include "usd.h" + +#include <string> + +#include <pxr/usd/usd/common.h> +#include <pxr/usd/usd/timeCode.h> + +struct Depsgraph; +struct ID; +struct Object; + +namespace USD { + +class USDHierarchyIterator : public AbstractHierarchyIterator { + private: + const pxr::UsdStageRefPtr stage_; + pxr::UsdTimeCode export_time_; + const USDExportParams ¶ms_; + + public: + USDHierarchyIterator(Depsgraph *depsgraph, + pxr::UsdStageRefPtr stage, + const USDExportParams ¶ms); + + void set_export_frame(float frame_nr); + const pxr::UsdTimeCode &get_export_time_code() const; + + virtual std::string make_valid_name(const std::string &name) const override; + + protected: + virtual bool mark_as_weak_export(const Object *object) const override; + + virtual AbstractHierarchyWriter *create_transform_writer( + const HierarchyContext *context) override; + virtual AbstractHierarchyWriter *create_data_writer(const HierarchyContext *context) override; + virtual AbstractHierarchyWriter *create_hair_writer(const HierarchyContext *context) override; + virtual AbstractHierarchyWriter *create_particle_writer( + const HierarchyContext *context) override; + + virtual void delete_object_writer(AbstractHierarchyWriter *writer) override; + + private: + USDExporterContext create_usd_export_context(const HierarchyContext *context); +}; + +} // namespace USD + +#endif /* __USD__USD_HIERARCHY_ITERATOR_H__ */ diff --git a/source/blender/usd/intern/usd_writer_abstract.cc b/source/blender/usd/intern/usd_writer_abstract.cc new file mode 100644 index 00000000000..4d0b4364fb5 --- /dev/null +++ b/source/blender/usd/intern/usd_writer_abstract.cc @@ -0,0 +1,147 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#include "usd_writer_abstract.h" +#include "usd_hierarchy_iterator.h" + +#include <pxr/base/tf/stringUtils.h> + +extern "C" { +#include "BKE_animsys.h" +#include "BKE_key.h" + +#include "DNA_modifier_types.h" +} + +/* TfToken objects are not cheap to construct, so we do it once. */ +namespace usdtokens { +// Materials +static const pxr::TfToken diffuse_color("diffuseColor", pxr::TfToken::Immortal); +static const pxr::TfToken metallic("metallic", pxr::TfToken::Immortal); +static const pxr::TfToken preview_shader("previewShader", pxr::TfToken::Immortal); +static const pxr::TfToken preview_surface("UsdPreviewSurface", pxr::TfToken::Immortal); +static const pxr::TfToken roughness("roughness", pxr::TfToken::Immortal); +static const pxr::TfToken surface("surface", pxr::TfToken::Immortal); +} // namespace usdtokens + +namespace USD { + +USDAbstractWriter::USDAbstractWriter(const USDExporterContext &usd_export_context) + : usd_export_context_(usd_export_context), + usd_value_writer_(), + frame_has_been_written_(false), + is_animated_(false) +{ +} + +USDAbstractWriter::~USDAbstractWriter() +{ +} + +bool USDAbstractWriter::is_supported(const HierarchyContext * /*context*/) const +{ + return true; +} + +pxr::UsdTimeCode USDAbstractWriter::get_export_time_code() const +{ + if (is_animated_) { + return usd_export_context_.hierarchy_iterator->get_export_time_code(); + } + // By using the default timecode USD won't even write a single `timeSample` for non-animated + // data. Instead, it writes it as non-timesampled. + static pxr::UsdTimeCode default_timecode = pxr::UsdTimeCode::Default(); + return default_timecode; +} + +void USDAbstractWriter::write(HierarchyContext &context) +{ + if (!frame_has_been_written_) { + is_animated_ = usd_export_context_.export_params.export_animation && + check_is_animated(context); + } + else if (!is_animated_) { + /* A frame has already been written, and without animation one frame is enough. */ + return; + } + + do_write(context); + + frame_has_been_written_ = true; +} + +bool USDAbstractWriter::check_is_animated(const HierarchyContext &context) const +{ + const Object *object = context.object; + + if (BKE_animdata_id_is_animated(static_cast<ID *>(object->data))) { + return true; + } + if (BKE_key_from_object(object) != nullptr) { + return true; + } + + /* Test modifiers. */ + /* TODO(Sybren): replace this with a check on the depsgraph to properly check for dependency on + * time. */ + ModifierData *md = static_cast<ModifierData *>(object->modifiers.first); + while (md) { + if (md->type != eModifierType_Subsurf) { + return true; + } + md = md->next; + } + + return false; +} + +const pxr::SdfPath &USDAbstractWriter::usd_path() const +{ + return usd_export_context_.usd_path; +} + +pxr::UsdShadeMaterial USDAbstractWriter::ensure_usd_material(Material *material) +{ + static pxr::SdfPath material_library_path("/_materials"); + pxr::UsdStageRefPtr stage = usd_export_context_.stage; + + // Construct the material. + pxr::TfToken material_name(usd_export_context_.hierarchy_iterator->get_id_name(&material->id)); + pxr::SdfPath usd_path = material_library_path.AppendChild(material_name); + pxr::UsdShadeMaterial usd_material = pxr::UsdShadeMaterial::Get(stage, usd_path); + if (usd_material) { + return usd_material; + } + usd_material = pxr::UsdShadeMaterial::Define(stage, usd_path); + + // Construct the shader. + pxr::SdfPath shader_path = usd_path.AppendChild(usdtokens::preview_shader); + pxr::UsdShadeShader shader = pxr::UsdShadeShader::Define(stage, shader_path); + shader.CreateIdAttr(pxr::VtValue(usdtokens::preview_surface)); + shader.CreateInput(usdtokens::diffuse_color, pxr::SdfValueTypeNames->Color3f) + .Set(pxr::GfVec3f(material->r, material->g, material->b)); + shader.CreateInput(usdtokens::roughness, pxr::SdfValueTypeNames->Float).Set(material->roughness); + shader.CreateInput(usdtokens::metallic, pxr::SdfValueTypeNames->Float).Set(material->metallic); + + // Connect the shader and the material together. + usd_material.CreateSurfaceOutput().ConnectToSource(shader, usdtokens::surface); + + return usd_material; +} + +} // namespace USD diff --git a/source/blender/usd/intern/usd_writer_abstract.h b/source/blender/usd/intern/usd_writer_abstract.h new file mode 100644 index 00000000000..cea4685c806 --- /dev/null +++ b/source/blender/usd/intern/usd_writer_abstract.h @@ -0,0 +1,78 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#ifndef __USD__USD_WRITER_ABSTRACT_H__ +#define __USD__USD_WRITER_ABSTRACT_H__ + +#include "usd_exporter_context.h" +#include "abstract_hierarchy_iterator.h" + +#include <pxr/usd/sdf/path.h> +#include <pxr/usd/usd/stage.h> +#include <pxr/usd/usdShade/material.h> +#include <pxr/usd/usdUtils/sparseValueWriter.h> + +#include <vector> + +extern "C" { +#include "DEG_depsgraph_query.h" +#include "DNA_material_types.h" +} + +struct Main; +struct Material; +struct Object; + +namespace USD { + +class USDAbstractWriter : public AbstractHierarchyWriter { + protected: + const USDExporterContext usd_export_context_; + pxr::UsdUtilsSparseValueWriter usd_value_writer_; + + bool frame_has_been_written_; + bool is_animated_; + + public: + USDAbstractWriter(const USDExporterContext &usd_export_context); + virtual ~USDAbstractWriter(); + + virtual void write(HierarchyContext &context) override; + + /* Returns true if the data to be written is actually supported. This would, for example, allow a + * hypothetical camera writer accept a perspective camera but reject an orthogonal one. + * + * Returning false from a transform writer will prevent the object and all its decendants from + * being exported. Returning false from a data writer (object data, hair, or particles) will + * only prevent that data from being written (and thus cause the object to be exported as an + * Empty). */ + virtual bool is_supported(const HierarchyContext *context) const; + + const pxr::SdfPath &usd_path() const; + + protected: + virtual void do_write(HierarchyContext &context) = 0; + virtual bool check_is_animated(const HierarchyContext &context) const; + pxr::UsdTimeCode get_export_time_code() const; + + pxr::UsdShadeMaterial ensure_usd_material(Material *material); +}; + +} // namespace USD + +#endif /* __USD__USD_WRITER_ABSTRACT_H__ */ diff --git a/source/blender/usd/intern/usd_writer_camera.cc b/source/blender/usd/intern/usd_writer_camera.cc new file mode 100644 index 00000000000..9b85d69559c --- /dev/null +++ b/source/blender/usd/intern/usd_writer_camera.cc @@ -0,0 +1,111 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#include "usd_writer_camera.h" +#include "usd_hierarchy_iterator.h" + +#include <pxr/usd/usdGeom/camera.h> +#include <pxr/usd/usdGeom/tokens.h> + +extern "C" { +#include "BKE_camera.h" +#include "BLI_assert.h" + +#include "DNA_camera_types.h" +#include "DNA_scene_types.h" +} + +namespace USD { + +USDCameraWriter::USDCameraWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx) +{ +} + +bool USDCameraWriter::is_supported(const HierarchyContext *context) const +{ + Camera *camera = static_cast<Camera *>(context->object->data); + return camera->type == CAM_PERSP; +} + +static void camera_sensor_size_for_render(const Camera *camera, + const struct RenderData *rd, + float *r_sensor_x, + float *r_sensor_y) +{ + /* Compute the final image size in pixels. */ + float sizex = rd->xsch * rd->xasp; + float sizey = rd->ysch * rd->yasp; + + int sensor_fit = BKE_camera_sensor_fit(camera->sensor_fit, sizex, sizey); + + switch (sensor_fit) { + case CAMERA_SENSOR_FIT_HOR: + *r_sensor_x = camera->sensor_x; + *r_sensor_y = camera->sensor_x * sizey / sizex; + break; + case CAMERA_SENSOR_FIT_VERT: + *r_sensor_x = camera->sensor_y * sizex / sizey; + *r_sensor_y = camera->sensor_y; + break; + case CAMERA_SENSOR_FIT_AUTO: + BLI_assert(!"Camera fit should be either horizontal or vertical"); + break; + } +} + +void USDCameraWriter::do_write(HierarchyContext &context) +{ + pxr::UsdTimeCode timecode = get_export_time_code(); + pxr::UsdGeomCamera usd_camera = pxr::UsdGeomCamera::Define(usd_export_context_.stage, + usd_export_context_.usd_path); + + Camera *camera = static_cast<Camera *>(context.object->data); + Scene *scene = DEG_get_evaluated_scene(usd_export_context_.depsgraph); + + usd_camera.CreateProjectionAttr().Set(pxr::UsdGeomTokens->perspective); + + /* USD stores the focal length in "millimeters or tenths of world units", because at some point + * they decided world units might be centimeters. Quite confusing, as the USD Viewer shows the + * correct FoV when we write millimeters and not "tenths of world units". + */ + usd_camera.CreateFocalLengthAttr().Set(camera->lens, timecode); + + float aperture_x, aperture_y; + camera_sensor_size_for_render(camera, &scene->r, &aperture_x, &aperture_y); + + float film_aspect = aperture_x / aperture_y; + usd_camera.CreateHorizontalApertureAttr().Set(aperture_x, timecode); + usd_camera.CreateVerticalApertureAttr().Set(aperture_y, timecode); + usd_camera.CreateHorizontalApertureOffsetAttr().Set(aperture_x * camera->shiftx, timecode); + usd_camera.CreateVerticalApertureOffsetAttr().Set(aperture_y * camera->shifty * film_aspect, + timecode); + + usd_camera.CreateClippingRangeAttr().Set( + pxr::VtValue(pxr::GfVec2f(camera->clip_start, camera->clip_end)), timecode); + + // Write DoF-related attributes. + if (camera->dof.flag & CAM_DOF_ENABLED) { + usd_camera.CreateFStopAttr().Set(camera->dof.aperture_fstop, timecode); + + float focus_distance = scene->unit.scale_length * + BKE_camera_object_dof_distance(context.object); + usd_camera.CreateFocusDistanceAttr().Set(focus_distance, timecode); + } +} + +} // namespace USD diff --git a/source/blender/usd/intern/usd_writer_camera.h b/source/blender/usd/intern/usd_writer_camera.h new file mode 100644 index 00000000000..86b5ed464b0 --- /dev/null +++ b/source/blender/usd/intern/usd_writer_camera.h @@ -0,0 +1,38 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#ifndef __USD__USD_WRITER_CAMERA_H__ +#define __USD__USD_WRITER_CAMERA_H__ + +#include "usd_writer_abstract.h" + +namespace USD { + +/* Writer for writing camera data to UsdGeomCamera. */ +class USDCameraWriter : public USDAbstractWriter { + public: + USDCameraWriter(const USDExporterContext &ctx); + + protected: + virtual bool is_supported(const HierarchyContext *context) const override; + virtual void do_write(HierarchyContext &context) override; +}; + +} // namespace USD + +#endif /* __USD__USD_WRITER_CAMERA_H__ */ diff --git a/source/blender/usd/intern/usd_writer_hair.cc b/source/blender/usd/intern/usd_writer_hair.cc new file mode 100644 index 00000000000..d09922be908 --- /dev/null +++ b/source/blender/usd/intern/usd_writer_hair.cc @@ -0,0 +1,84 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#include "usd_writer_hair.h" +#include "usd_hierarchy_iterator.h" + +#include <pxr/usd/usdGeom/basisCurves.h> +#include <pxr/usd/usdGeom/tokens.h> + +extern "C" { +#include "BKE_particle.h" + +#include "DNA_particle_types.h" +} + +namespace USD { + +USDHairWriter::USDHairWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx) +{ +} + +void USDHairWriter::do_write(HierarchyContext &context) +{ + ParticleSystem *psys = context.particle_system; + ParticleCacheKey **cache = psys->pathcache; + if (cache == nullptr) { + return; + } + + pxr::UsdTimeCode timecode = get_export_time_code(); + pxr::UsdGeomBasisCurves curves = pxr::UsdGeomBasisCurves::Define(usd_export_context_.stage, + usd_export_context_.usd_path); + + // TODO(Sybren): deal with (psys->part->flag & PART_HAIR_BSPLINE) + curves.CreateBasisAttr(pxr::VtValue(pxr::UsdGeomTokens->bspline)); + curves.CreateTypeAttr(pxr::VtValue(pxr::UsdGeomTokens->cubic)); + + pxr::VtArray<pxr::GfVec3f> points; + pxr::VtIntArray curve_point_counts; + curve_point_counts.reserve(psys->totpart); + + ParticleCacheKey *strand; + for (int strand_index = 0; strand_index < psys->totpart; ++strand_index) { + strand = cache[strand_index]; + + int point_count = strand->segments + 1; + curve_point_counts.push_back(point_count); + + for (int point_index = 0; point_index < point_count; ++point_index, ++strand) { + points.push_back(pxr::GfVec3f(strand->co)); + } + } + + curves.CreatePointsAttr().Set(points, timecode); + curves.CreateCurveVertexCountsAttr().Set(curve_point_counts, timecode); + + if (psys->totpart > 0) { + pxr::VtArray<pxr::GfVec3f> colors; + colors.push_back(pxr::GfVec3f(cache[0]->col)); + curves.CreateDisplayColorAttr(pxr::VtValue(colors)); + } +} + +bool USDHairWriter::check_is_animated(const HierarchyContext &) const +{ + return true; +} + +} // namespace USD diff --git a/source/blender/usd/intern/usd_writer_hair.h b/source/blender/usd/intern/usd_writer_hair.h new file mode 100644 index 00000000000..ccb5af76099 --- /dev/null +++ b/source/blender/usd/intern/usd_writer_hair.h @@ -0,0 +1,38 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#ifndef __USD__USD_WRITER_HAIR_H__ +#define __USD__USD_WRITER_HAIR_H__ + +#include "usd_writer_abstract.h" + +namespace USD { + +/* Writer for writing hair particle data as USD curves. */ +class USDHairWriter : public USDAbstractWriter { + public: + USDHairWriter(const USDExporterContext &ctx); + + protected: + virtual void do_write(HierarchyContext &context) override; + virtual bool check_is_animated(const HierarchyContext &context) const override; +}; + +} // namespace USD + +#endif /* __USD__USD_WRITER_HAIR_H__ */ diff --git a/source/blender/usd/intern/usd_writer_light.cc b/source/blender/usd/intern/usd_writer_light.cc new file mode 100644 index 00000000000..e13e2c58a79 --- /dev/null +++ b/source/blender/usd/intern/usd_writer_light.cc @@ -0,0 +1,112 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#include "usd_writer_light.h" +#include "usd_hierarchy_iterator.h" + +#include <pxr/usd/usdLux/diskLight.h> +#include <pxr/usd/usdLux/distantLight.h> +#include <pxr/usd/usdLux/rectLight.h> +#include <pxr/usd/usdLux/sphereLight.h> + +extern "C" { +#include "BLI_assert.h" +#include "BLI_utildefines.h" + +#include "DNA_light_types.h" +#include "DNA_object_types.h" +} + +namespace USD { + +USDLightWriter::USDLightWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx) +{ +} + +bool USDLightWriter::is_supported(const HierarchyContext *context) const +{ + Light *light = static_cast<Light *>(context->object->data); + return ELEM(light->type, LA_AREA, LA_LOCAL, LA_SUN); +} + +void USDLightWriter::do_write(HierarchyContext &context) +{ + pxr::UsdStageRefPtr stage = usd_export_context_.stage; + const pxr::SdfPath &usd_path = usd_export_context_.usd_path; + pxr::UsdTimeCode timecode = get_export_time_code(); + + Light *light = static_cast<Light *>(context.object->data); + pxr::UsdLuxLight usd_light; + + switch (light->type) { + case LA_AREA: + switch (light->area_shape) { + case LA_AREA_DISK: + case LA_AREA_ELLIPSE: { /* An ellipse light will deteriorate into a disk light. */ + pxr::UsdLuxDiskLight disk_light = pxr::UsdLuxDiskLight::Define(stage, usd_path); + disk_light.CreateRadiusAttr().Set(light->area_size, timecode); + usd_light = disk_light; + break; + } + case LA_AREA_RECT: { + pxr::UsdLuxRectLight rect_light = pxr::UsdLuxRectLight::Define(stage, usd_path); + rect_light.CreateWidthAttr().Set(light->area_size, timecode); + rect_light.CreateHeightAttr().Set(light->area_sizey, timecode); + usd_light = rect_light; + break; + } + case LA_AREA_SQUARE: { + pxr::UsdLuxRectLight rect_light = pxr::UsdLuxRectLight::Define(stage, usd_path); + rect_light.CreateWidthAttr().Set(light->area_size, timecode); + rect_light.CreateHeightAttr().Set(light->area_size, timecode); + usd_light = rect_light; + break; + } + } + break; + case LA_LOCAL: { + pxr::UsdLuxSphereLight sphere_light = pxr::UsdLuxSphereLight::Define(stage, usd_path); + sphere_light.CreateRadiusAttr().Set(light->area_size, timecode); + usd_light = sphere_light; + break; + } + case LA_SUN: + usd_light = pxr::UsdLuxDistantLight::Define(stage, usd_path); + break; + default: + BLI_assert(!"is_supported() returned true for unsupported light type"); + } + + /* Scale factor to get to somewhat-similar illumination. Since the USDViewer had similar + * over-exposure as Blender Internal with the same values, this code applies the reverse of the + * versioning code in light_emission_unify(). */ + float usd_intensity; + if (light->type == LA_SUN) { + /* Untested, as the Hydra GL viewport of USDViewer doesn't support distant lights. */ + usd_intensity = light->energy; + } + else { + usd_intensity = light->energy / 100.f; + } + usd_light.CreateIntensityAttr().Set(usd_intensity, timecode); + + usd_light.CreateColorAttr().Set(pxr::GfVec3f(light->r, light->g, light->b), timecode); + usd_light.CreateSpecularAttr().Set(light->spec_fac, timecode); +} + +} // namespace USD diff --git a/source/blender/usd/intern/usd_writer_light.h b/source/blender/usd/intern/usd_writer_light.h new file mode 100644 index 00000000000..3813412c330 --- /dev/null +++ b/source/blender/usd/intern/usd_writer_light.h @@ -0,0 +1,37 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#ifndef __USD__USD_WRITER_LIGHT_H__ +#define __USD__USD_WRITER_LIGHT_H__ + +#include "usd_writer_abstract.h" + +namespace USD { + +class USDLightWriter : public USDAbstractWriter { + public: + USDLightWriter(const USDExporterContext &ctx); + + protected: + virtual bool is_supported(const HierarchyContext *context) const override; + virtual void do_write(HierarchyContext &context) override; +}; + +} // namespace USD + +#endif /* __USD__USD_WRITER_LIGHT_H__ */ diff --git a/source/blender/usd/intern/usd_writer_mesh.cc b/source/blender/usd/intern/usd_writer_mesh.cc new file mode 100644 index 00000000000..ca0171fd60e --- /dev/null +++ b/source/blender/usd/intern/usd_writer_mesh.cc @@ -0,0 +1,463 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#include "usd_writer_mesh.h" +#include "usd_hierarchy_iterator.h" + +#include <pxr/usd/usdGeom/mesh.h> +#include <pxr/usd/usdShade/material.h> +#include <pxr/usd/usdShade/materialBindingAPI.h> + +extern "C" { +#include "BLI_assert.h" +#include "BLI_math_vector.h" + +#include "BKE_anim.h" +#include "BKE_customdata.h" +#include "BKE_library.h" +#include "BKE_material.h" +#include "BKE_mesh.h" +#include "BKE_modifier.h" +#include "BKE_object.h" + +#include "DEG_depsgraph.h" + +#include "DNA_layer_types.h" +#include "DNA_mesh_types.h" +#include "DNA_meshdata_types.h" +#include "DNA_modifier_types.h" +#include "DNA_object_fluidsim_types.h" +#include "DNA_particle_types.h" +} + +namespace USD { + +USDGenericMeshWriter::USDGenericMeshWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx) +{ +} + +bool USDGenericMeshWriter::is_supported(const HierarchyContext *context) const +{ + Object *object = context->object; + bool is_dupli = context->duplicator != nullptr; + int base_flag; + + if (is_dupli) { + /* Construct the object's base flags from its dupliparent, just like is done in + * deg_objects_dupli_iterator_next(). Without this, the visiblity check below will fail. Doing + * this here, instead of a more suitable location in AbstractHierarchyIterator, prevents + * copying the Object for every dupli. */ + base_flag = object->base_flag; + object->base_flag = context->duplicator->base_flag | BASE_FROM_DUPLI; + } + + int visibility = BKE_object_visibility(object, + usd_export_context_.export_params.evaluation_mode); + + if (is_dupli) { + object->base_flag = base_flag; + } + + return (visibility & OB_VISIBLE_SELF) != 0; +} + +void USDGenericMeshWriter::do_write(HierarchyContext &context) +{ + Object *object_eval = context.object; + bool needsfree = false; + Mesh *mesh = get_export_mesh(object_eval, needsfree); + + if (mesh == NULL) { + return; + } + + try { + write_mesh(context, mesh); + + if (needsfree) { + free_export_mesh(mesh); + } + } + catch (...) { + if (needsfree) { + free_export_mesh(mesh); + } + throw; + } +} + +void USDGenericMeshWriter::free_export_mesh(Mesh *mesh) +{ + BKE_id_free(NULL, mesh); +} + +struct USDMeshData { + pxr::VtArray<pxr::GfVec3f> points; + pxr::VtIntArray face_vertex_counts; + pxr::VtIntArray face_indices; + std::map<short, pxr::VtIntArray> face_groups; + + /* The length of this array specifies the number of creases on the surface. Each element gives + * the number of (must be adjacent) vertices in each crease, whose indices are linearly laid out + * in the 'creaseIndices' attribute. Since each crease must be at least one edge long, each + * element of this array should be greater than one. */ + pxr::VtIntArray crease_lengths; + /* The indices of all vertices forming creased edges. The size of this array must be equal to the + * sum of all elements of the 'creaseLengths' attribute. */ + pxr::VtIntArray crease_vertex_indices; + /* The per-crease or per-edge sharpness for all creases (Usd.Mesh.SHARPNESS_INFINITE for a + * perfectly sharp crease). Since 'creaseLengths' encodes the number of vertices in each crease, + * the number of elements in this array will be either len(creaseLengths) or the sum over all X + * of (creaseLengths[X] - 1). Note that while the RI spec allows each crease to have either a + * single sharpness or a value per-edge, USD will encode either a single sharpness per crease on + * a mesh, or sharpnesses for all edges making up the creases on a mesh. */ + pxr::VtFloatArray crease_sharpnesses; +}; + +void USDGenericMeshWriter::write_uv_maps(const Mesh *mesh, pxr::UsdGeomMesh usd_mesh) +{ + pxr::UsdTimeCode timecode = get_export_time_code(); + + const CustomData *ldata = &mesh->ldata; + for (int layer_idx = 0; layer_idx < ldata->totlayer; layer_idx++) { + const CustomDataLayer *layer = &ldata->layers[layer_idx]; + if (layer->type != CD_MLOOPUV) { + continue; + } + + /* UV coordinates are stored in a Primvar on the Mesh, and can be referenced from materials. + * The primvar name is the same as the UV Map name. This is to allow the standard name "st" + * for texture coordinates by naming the UV Map as such, without having to guess which UV Map + * is the "standard" one. */ + pxr::TfToken primvar_name(pxr::TfMakeValidIdentifier(layer->name)); + pxr::UsdGeomPrimvar uv_coords_primvar = usd_mesh.CreatePrimvar( + primvar_name, pxr::SdfValueTypeNames->TexCoord2fArray, pxr::UsdGeomTokens->faceVarying); + + MLoopUV *mloopuv = static_cast<MLoopUV *>(layer->data); + pxr::VtArray<pxr::GfVec2f> uv_coords; + for (int loop_idx = 0; loop_idx < mesh->totloop; loop_idx++) { + uv_coords.push_back(pxr::GfVec2f(mloopuv[loop_idx].uv)); + } + uv_coords_primvar.Set(uv_coords, timecode); + } +} + +void USDGenericMeshWriter::write_mesh(HierarchyContext &context, Mesh *mesh) +{ + pxr::UsdTimeCode timecode = get_export_time_code(); + pxr::UsdStageRefPtr stage = usd_export_context_.stage; + const pxr::SdfPath &usd_path = usd_export_context_.usd_path; + + pxr::UsdGeomMesh usd_mesh = pxr::UsdGeomMesh::Define(stage, usd_path); + USDMeshData usd_mesh_data; + get_geometry_data(mesh, usd_mesh_data); + + if (usd_export_context_.export_params.use_instancing && context.is_instance()) { + // This object data is instanced, just reference the original instead of writing a copy. + if (context.export_path == context.original_export_path) { + printf("USD ref error: export path is reference path: %s\n", context.export_path.c_str()); + BLI_assert(!"USD reference error"); + return; + } + pxr::SdfPath ref_path(context.original_export_path); + if (!usd_mesh.GetPrim().GetReferences().AddInternalReference(ref_path)) { + /* See this URL for a description fo why referencing may fail" + * https://graphics.pixar.com/usd/docs/api/class_usd_references.html#Usd_Failing_References + */ + printf("USD Export warning: unable to add reference from %s to %s, not instancing object\n", + context.export_path.c_str(), + context.original_export_path.c_str()); + return; + } + /* The material path will be of the form </_materials/{material name}>, which is outside the + subtree pointed to by ref_path. As a result, the referenced data is not allowed to point out + of its own subtree. It does work when we override the material with exactly the same path, + though.*/ + if (usd_export_context_.export_params.export_materials) { + assign_materials(context, usd_mesh, usd_mesh_data.face_groups); + } + return; + } + + pxr::UsdAttribute attr_points = usd_mesh.CreatePointsAttr(); + pxr::UsdAttribute attr_face_vertex_counts = usd_mesh.CreateFaceVertexCountsAttr(); + pxr::UsdAttribute attr_face_vertex_indices = usd_mesh.CreateFaceVertexIndicesAttr(); + + usd_value_writer_.SetAttribute(attr_points, pxr::VtValue(usd_mesh_data.points), timecode); + usd_value_writer_.SetAttribute( + attr_face_vertex_counts, pxr::VtValue(usd_mesh_data.face_vertex_counts), timecode); + usd_value_writer_.SetAttribute( + attr_face_vertex_indices, pxr::VtValue(usd_mesh_data.face_indices), timecode); + + if (!usd_mesh_data.crease_lengths.empty()) { + pxr::UsdAttribute attr_crease_lengths = usd_mesh.CreateCreaseLengthsAttr(); + pxr::UsdAttribute attr_crease_indices = usd_mesh.CreateCreaseIndicesAttr(); + pxr::UsdAttribute attr_crease_sharpness = usd_mesh.CreateCreaseSharpnessesAttr(); + + usd_value_writer_.SetAttribute( + attr_crease_lengths, pxr::VtValue(usd_mesh_data.crease_lengths), timecode); + usd_value_writer_.SetAttribute( + attr_crease_indices, pxr::VtValue(usd_mesh_data.crease_vertex_indices), timecode); + usd_value_writer_.SetAttribute( + attr_crease_sharpness, pxr::VtValue(usd_mesh_data.crease_sharpnesses), timecode); + } + + if (usd_export_context_.export_params.export_uvmaps) { + write_uv_maps(mesh, usd_mesh); + } + if (usd_export_context_.export_params.export_normals) { + write_normals(mesh, usd_mesh); + } + write_surface_velocity(context.object, mesh, usd_mesh); + + // TODO(Sybren): figure out what happens when the face groups change. + if (frame_has_been_written_) { + return; + } + + usd_mesh.CreateSubdivisionSchemeAttr().Set(pxr::UsdGeomTokens->none); + + if (usd_export_context_.export_params.export_materials) { + assign_materials(context, usd_mesh, usd_mesh_data.face_groups); + } +} + +static void get_vertices(const Mesh *mesh, USDMeshData &usd_mesh_data) +{ + usd_mesh_data.points.reserve(mesh->totvert); + + const MVert *verts = mesh->mvert; + for (int i = 0; i < mesh->totvert; ++i) { + usd_mesh_data.points.push_back(pxr::GfVec3f(verts[i].co)); + } +} + +static void get_loops_polys(const Mesh *mesh, USDMeshData &usd_mesh_data) +{ + /* Only construct face groups (a.k.a. geometry subsets) when we need them for material + * assignments. */ + bool construct_face_groups = mesh->totcol > 1; + + usd_mesh_data.face_vertex_counts.reserve(mesh->totpoly); + usd_mesh_data.face_indices.reserve(mesh->totloop); + + MLoop *mloop = mesh->mloop; + MPoly *mpoly = mesh->mpoly; + for (int i = 0; i < mesh->totpoly; ++i, ++mpoly) { + MLoop *loop = mloop + mpoly->loopstart; + usd_mesh_data.face_vertex_counts.push_back(mpoly->totloop); + for (int j = 0; j < mpoly->totloop; ++j, ++loop) { + usd_mesh_data.face_indices.push_back(loop->v); + } + + if (construct_face_groups) { + usd_mesh_data.face_groups[mpoly->mat_nr].push_back(i); + } + } +} + +static void get_creases(const Mesh *mesh, USDMeshData &usd_mesh_data) +{ + const float factor = 1.0f / 255.0f; + + MEdge *edge = mesh->medge; + float sharpness; + for (int edge_idx = 0, totedge = mesh->totedge; edge_idx < totedge; ++edge_idx, ++edge) { + if (edge->crease == 0) { + continue; + } + + if (edge->crease == 255) { + sharpness = pxr::UsdGeomMesh::SHARPNESS_INFINITE; + } + else { + sharpness = static_cast<float>(edge->crease) * factor; + } + + usd_mesh_data.crease_vertex_indices.push_back(edge->v1); + usd_mesh_data.crease_vertex_indices.push_back(edge->v2); + usd_mesh_data.crease_lengths.push_back(2); + usd_mesh_data.crease_sharpnesses.push_back(sharpness); + } +} + +void USDGenericMeshWriter::get_geometry_data(const Mesh *mesh, USDMeshData &usd_mesh_data) +{ + get_vertices(mesh, usd_mesh_data); + get_loops_polys(mesh, usd_mesh_data); + get_creases(mesh, usd_mesh_data); +} + +void USDGenericMeshWriter::assign_materials(const HierarchyContext &context, + pxr::UsdGeomMesh usd_mesh, + const MaterialFaceGroups &usd_face_groups) +{ + if (context.object->totcol == 0) { + return; + } + + /* Binding a material to a geometry subset isn't supported by the Hydra GL viewport yet, + * which is why we always bind the first material to the entire mesh. See + * https://github.com/PixarAnimationStudios/USD/issues/542 for more info. */ + bool mesh_material_bound = false; + for (short mat_num = 0; mat_num < context.object->totcol; mat_num++) { + Material *material = give_current_material(context.object, mat_num + 1); + if (material == nullptr) { + continue; + } + + pxr::UsdShadeMaterial usd_material = ensure_usd_material(material); + usd_material.Bind(usd_mesh.GetPrim()); + + /* USD seems to support neither per-material nor per-face-group double-sidedness, so we just + * use the flag from the first non-empty material slot. */ + usd_mesh.CreateDoubleSidedAttr( + pxr::VtValue((material->blend_flag & MA_BL_CULL_BACKFACE) == 0)); + + mesh_material_bound = true; + break; + } + + if (!mesh_material_bound) { + /* Blender defaults to double-sided, but USD to single-sided. */ + usd_mesh.CreateDoubleSidedAttr(pxr::VtValue(true)); + } + + if (!mesh_material_bound || usd_face_groups.size() < 2) { + /* Either all material slots were empty or there is only one material in use. As geometry + * subsets are only written when actually used to assign a material, and the mesh already has + * the material assigned, there is no need to continue. */ + return; + } + + // Define a geometry subset per material. + for (const MaterialFaceGroups::value_type &face_group : usd_face_groups) { + short material_number = face_group.first; + const pxr::VtIntArray &face_indices = face_group.second; + + Material *material = give_current_material(context.object, material_number + 1); + if (material == nullptr) { + continue; + } + + pxr::UsdShadeMaterial usd_material = ensure_usd_material(material); + pxr::TfToken material_name = usd_material.GetPath().GetNameToken(); + + pxr::UsdShadeMaterialBindingAPI api = pxr::UsdShadeMaterialBindingAPI(usd_mesh); + pxr::UsdGeomSubset usd_face_subset = api.CreateMaterialBindSubset(material_name, face_indices); + usd_material.Bind(usd_face_subset.GetPrim()); + } +} + +void USDGenericMeshWriter::write_normals(const Mesh *mesh, pxr::UsdGeomMesh usd_mesh) +{ + pxr::UsdTimeCode timecode = get_export_time_code(); + const float(*lnors)[3] = static_cast<float(*)[3]>(CustomData_get_layer(&mesh->ldata, CD_NORMAL)); + + pxr::VtVec3fArray loop_normals; + loop_normals.reserve(mesh->totloop); + + if (lnors != nullptr) { + /* Export custom loop normals. */ + for (int loop_idx = 0, totloop = mesh->totloop; loop_idx < totloop; ++loop_idx) { + loop_normals.push_back(pxr::GfVec3f(lnors[loop_idx])); + } + } + else { + /* Compute the loop normals based on the 'smooth' flag. */ + float normal[3]; + MPoly *mpoly = mesh->mpoly; + const MVert *mvert = mesh->mvert; + for (int poly_idx = 0, totpoly = mesh->totpoly; poly_idx < totpoly; ++poly_idx, ++mpoly) { + MLoop *mloop = mesh->mloop + mpoly->loopstart; + + if ((mpoly->flag & ME_SMOOTH) == 0) { + /* Flat shaded, use common normal for all verts. */ + BKE_mesh_calc_poly_normal(mpoly, mloop, mvert, normal); + pxr::GfVec3f pxr_normal(normal); + for (int loop_idx = 0; loop_idx < mpoly->totloop; ++loop_idx) { + loop_normals.push_back(pxr_normal); + } + } + else { + /* Smooth shaded, use individual vert normals. */ + for (int loop_idx = 0; loop_idx < mpoly->totloop; ++loop_idx, ++mloop) { + normal_short_to_float_v3(normal, mvert[mloop->v].no); + loop_normals.push_back(pxr::GfVec3f(normal)); + } + } + } + } + + pxr::UsdAttribute attr_normals = usd_mesh.CreateNormalsAttr(pxr::VtValue(loop_normals), true); + attr_normals.Set(loop_normals, timecode); + usd_mesh.SetNormalsInterpolation(pxr::UsdGeomTokens->faceVarying); +} + +void USDGenericMeshWriter::write_surface_velocity(Object *object, + const Mesh *mesh, + pxr::UsdGeomMesh usd_mesh) +{ + /* Only velocities from the fluid simulation are exported. This is the most important case, + * though, as the baked mesh changes topology all the time, and thus computing the velocities + * at import time in a post-processing step is hard. */ + ModifierData *md = modifiers_findByType(object, eModifierType_Fluidsim); + if (md == nullptr) { + return; + } + + /* Check that the fluid sim modifier is enabled and has useful data. */ + const bool use_render = (DEG_get_mode(usd_export_context_.depsgraph) == DAG_EVAL_RENDER); + const ModifierMode required_mode = use_render ? eModifierMode_Render : eModifierMode_Realtime; + const Scene *scene = DEG_get_evaluated_scene(usd_export_context_.depsgraph); + if (!modifier_isEnabled(scene, md, required_mode)) { + return; + } + FluidsimModifierData *fsmd = reinterpret_cast<FluidsimModifierData *>(md); + if (!fsmd->fss || fsmd->fss->type != OB_FLUIDSIM_DOMAIN) { + return; + } + FluidsimSettings *fss = fsmd->fss; + if (!fss->meshVelocities) { + return; + } + + /* Export per-vertex velocity vectors. */ + pxr::VtVec3fArray usd_velocities; + usd_velocities.reserve(mesh->totvert); + + FluidVertexVelocity *mesh_velocities = fss->meshVelocities; + for (int vertex_idx = 0, totvert = mesh->totvert; vertex_idx < totvert; + ++vertex_idx, ++mesh_velocities) { + usd_velocities.push_back(pxr::GfVec3f(mesh_velocities->vel)); + } + + pxr::UsdTimeCode timecode = get_export_time_code(); + usd_mesh.CreateVelocitiesAttr().Set(usd_velocities, timecode); +} + +USDMeshWriter::USDMeshWriter(const USDExporterContext &ctx) : USDGenericMeshWriter(ctx) +{ +} + +Mesh *USDMeshWriter::get_export_mesh(Object *object_eval, bool & /*r_needsfree*/) +{ + return object_eval->runtime.mesh_eval; +} + +} // namespace USD diff --git a/source/blender/usd/intern/usd_writer_mesh.h b/source/blender/usd/intern/usd_writer_mesh.h new file mode 100644 index 00000000000..0f03c8d20ae --- /dev/null +++ b/source/blender/usd/intern/usd_writer_mesh.h @@ -0,0 +1,66 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#ifndef __USD__USD_WRITER_MESH_H__ +#define __USD__USD_WRITER_MESH_H__ + +#include "usd_writer_abstract.h" + +#include <pxr/usd/usdGeom/mesh.h> + +namespace USD { + +struct USDMeshData; + +/* Writer for USD geometry. Does not assume the object is a mesh object. */ +class USDGenericMeshWriter : public USDAbstractWriter { + public: + USDGenericMeshWriter(const USDExporterContext &ctx); + + protected: + virtual bool is_supported(const HierarchyContext *context) const override; + virtual void do_write(HierarchyContext &context) override; + + virtual Mesh *get_export_mesh(Object *object_eval, bool &r_needsfree) = 0; + virtual void free_export_mesh(Mesh *mesh); + + private: + /* Mapping from material slot number to array of face indices with that material. */ + typedef std::map<short, pxr::VtIntArray> MaterialFaceGroups; + + void write_mesh(HierarchyContext &context, Mesh *mesh); + void get_geometry_data(const Mesh *mesh, struct USDMeshData &usd_mesh_data); + void assign_materials(const HierarchyContext &context, + pxr::UsdGeomMesh usd_mesh, + const MaterialFaceGroups &usd_face_groups); + void write_uv_maps(const Mesh *mesh, pxr::UsdGeomMesh usd_mesh); + void write_normals(const Mesh *mesh, pxr::UsdGeomMesh usd_mesh); + void write_surface_velocity(Object *object, const Mesh *mesh, pxr::UsdGeomMesh usd_mesh); +}; + +class USDMeshWriter : public USDGenericMeshWriter { + public: + USDMeshWriter(const USDExporterContext &ctx); + + protected: + virtual Mesh *get_export_mesh(Object *object_eval, bool &r_needsfree); +}; + +} // namespace USD + +#endif /* __USD__USD_WRITER_MESH_H__ */ diff --git a/source/blender/usd/intern/usd_writer_transform.cc b/source/blender/usd/intern/usd_writer_transform.cc new file mode 100644 index 00000000000..321b516221a --- /dev/null +++ b/source/blender/usd/intern/usd_writer_transform.cc @@ -0,0 +1,64 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#include "usd_writer_transform.h" +#include "usd_hierarchy_iterator.h" + +#include <pxr/base/gf/matrix4f.h> +#include <pxr/usd/usdGeom/xform.h> + +extern "C" { +#include "BKE_object.h" + +#include "BLI_math_matrix.h" + +#include "DNA_layer_types.h" +} + +namespace USD { + +USDTransformWriter::USDTransformWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx) +{ +} + +void USDTransformWriter::do_write(HierarchyContext &context) +{ + float parent_relative_matrix[4][4]; // The object matrix relative to the parent. + mul_m4_m4m4(parent_relative_matrix, context.parent_matrix_inv_world, context.matrix_world); + + // Write the transform relative to the parent. + pxr::UsdGeomXform xform = pxr::UsdGeomXform::Define(usd_export_context_.stage, + usd_export_context_.usd_path); + if (!xformOp_) { + xformOp_ = xform.AddTransformOp(); + } + xformOp_.Set(pxr::GfMatrix4d(parent_relative_matrix), get_export_time_code()); +} + +bool USDTransformWriter::check_is_animated(const HierarchyContext &context) const +{ + if (context.duplicator != NULL) { + /* This object is being duplicated, so could be emitted by a particle system and thus + * influenced by forces. TODO(Sybren): Make this more strict. Probably better to get from the + * depsgraph whether this object instance has a time source. */ + return true; + } + return BKE_object_moves_in_time(context.object, context.animation_check_include_parent); +} + +} // namespace USD diff --git a/source/blender/usd/intern/usd_writer_transform.h b/source/blender/usd/intern/usd_writer_transform.h new file mode 100644 index 00000000000..fb591998e6a --- /dev/null +++ b/source/blender/usd/intern/usd_writer_transform.h @@ -0,0 +1,42 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#ifndef __USD__USD_WRITER_TRANSFORM_H__ +#define __USD__USD_WRITER_TRANSFORM_H__ + +#include "usd_writer_abstract.h" + +#include <pxr/usd/usdGeom/xform.h> + +namespace USD { + +class USDTransformWriter : public USDAbstractWriter { + private: + pxr::UsdGeomXformOp xformOp_; + + public: + USDTransformWriter(const USDExporterContext &ctx); + + protected: + void do_write(HierarchyContext &context) override; + bool check_is_animated(const HierarchyContext &context) const override; +}; + +} // namespace USD + +#endif /* __USD__USD_WRITER_TRANSFORM_H__ */ diff --git a/source/blender/usd/usd.h b/source/blender/usd/usd.h new file mode 100644 index 00000000000..412a51b8061 --- /dev/null +++ b/source/blender/usd/usd.h @@ -0,0 +1,62 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ + +#ifndef __BLENDER_USD_H__ +#define __BLENDER_USD_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "DEG_depsgraph.h" + +struct Scene; +struct bContext; + +struct USDExportParams { + bool export_animation; + bool export_hair; + bool export_uvmaps; + bool export_normals; + bool export_materials; + bool selected_objects_only; + bool visible_objects_only; + bool use_instancing; + enum eEvaluationMode evaluation_mode; +}; + +/* The USD_export takes a as_background_job parameter, and returns a boolean. + * + * When as_background_job=true, returns false immediately after scheduling + * a background job. + * + * When as_background_job=false, performs the export synchronously, and returns + * true when the export was ok, and false if there were any errors. + */ + +bool USD_export(struct bContext *C, + const char *filepath, + const struct USDExportParams *params, + bool as_background_job); + +#ifdef __cplusplus +} +#endif + +#endif /* __BLENDER_USD_H__ */ diff --git a/source/blender/windowmanager/intern/wm_operator_props.c b/source/blender/windowmanager/intern/wm_operator_props.c index 9aefb4f68cb..2a40fb138c0 100644 --- a/source/blender/windowmanager/intern/wm_operator_props.c +++ b/source/blender/windowmanager/intern/wm_operator_props.c @@ -157,6 +157,8 @@ void WM_operator_properties_filesel(wmOperatorType *ot, RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE); prop = RNA_def_boolean( ot->srna, "filter_alembic", (filter & FILE_TYPE_ALEMBIC) != 0, "Filter Alembic files", ""); + prop = RNA_def_boolean( + ot->srna, "filter_usd", (filter & FILE_TYPE_USD) != 0, "Filter USD files", ""); RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE); prop = RNA_def_boolean( ot->srna, "filter_folder", (filter & FILE_TYPE_FOLDER) != 0, "Filter folders", ""); diff --git a/source/creator/CMakeLists.txt b/source/creator/CMakeLists.txt index 96ceac5c4d4..a7a816385ad 100644 --- a/source/creator/CMakeLists.txt +++ b/source/creator/CMakeLists.txt @@ -1045,6 +1045,19 @@ unset(LIB) setup_liblinks(blender) +# ----------------------------------------------------------------------------- +# USD registry. +# USD requires a set of JSON files that define the standard schemas. These +# files are required at runtime. +if (WITH_USD) + add_definitions(-DWITH_USD) + install(DIRECTORY + ${LIBDIR}/usd/lib/usd + DESTINATION "${TARGETDIR_VER}/datafiles" + ) +endif() + + # vcpkg substitutes our libs with theirs, which will cause issues when you # you run these builds on other systems due to missing dlls. So we opt out # the use of vcpkg diff --git a/source/creator/creator.c b/source/creator/creator.c index f4f5e3dcbde..32377da5284 100644 --- a/source/creator/creator.c +++ b/source/creator/creator.c @@ -112,6 +112,20 @@ int main_python_enter(int argc, const char **argv); void main_python_exit(void); #endif +#ifdef WITH_USD +/* Workaround to make it possible to pass a path at runtime to USD. + * + * USD requires some JSON files, and it uses a static constructor to determine the possible + * filesystem paths to find those files. This made it impossible for Blender to pass a path to the + * USD library at runtime, as the constructor would run before Blender's main() function. We have + * patched USD (see usd.diff) to avoid that particular static constructor, and have an + * initialisation function instead. + * + * This function is implemented in the USD source code, pxr/base/lib/plug/initConfig.cpp. + */ +void usd_initialise_plugin_path(const char *datafiles_usd_path); +#endif + /* written to by 'creator_args.c' */ struct ApplicationState app_state = { .signal = @@ -411,6 +425,13 @@ int main(int argc, init_def_material(); +#ifdef WITH_USD + /* Tell USD which directory to search for its JSON files. If datafiles/usd + * does not exist, the USD library will not be able to read or write any files. + */ + usd_initialise_plugin_path(BKE_appdir_folder_id(BLENDER_DATAFILES, "usd")); +#endif + if (G.background == 0) { #ifndef WITH_PYTHON_MODULE BLI_argsParse(ba, 2, NULL, NULL); diff --git a/tests/gtests/CMakeLists.txt b/tests/gtests/CMakeLists.txt index 54a1ee41198..7da65bcc8b9 100644 --- a/tests/gtests/CMakeLists.txt +++ b/tests/gtests/CMakeLists.txt @@ -19,4 +19,7 @@ if(WITH_GTESTS) if(WITH_ALEMBIC) add_subdirectory(alembic) endif() + if(WITH_USD) + add_subdirectory(usd) + endif() endif() diff --git a/tests/gtests/usd/CMakeLists.txt b/tests/gtests/usd/CMakeLists.txt new file mode 100644 index 00000000000..e2768509ec4 --- /dev/null +++ b/tests/gtests/usd/CMakeLists.txt @@ -0,0 +1,87 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# The Original Code is Copyright (C) 2019, Blender Foundation +# All rights reserved. +# ***** END GPL LICENSE BLOCK ***** + +# This suppresses the warning "This file includes at least one deprecated or antiquated +# header which may be removed without further notice at a future date", which is caused +# by the USD library including <ext/hash_set> on Linux. This has been reported at: +# https://github.com/PixarAnimationStudios/USD/issues/1057. +if(UNIX AND NOT APPLE) + add_definitions(-D_GLIBCXX_PERMIT_BACKWARD_HASH) +endif() +if(WIN32) + add_definitions(-DNOMINMAX) +endif() +add_definitions(-DPXR_STATIC) + +set(INC + . + .. + ../../../source/blender/blenlib + ../../../source/blender/blenkernel + ../../../source/blender/usd + ../../../source/blender/makesdna + ../../../source/blender/depsgraph + ${USD_INCLUDE_DIRS} + ${BOOST_INCLUDE_DIR} + ${TBB_INCLUDE_DIR} +) + +set(LIB + bf_blenloader_test + bf_blenloader + + # Should not be needed but gives windows linker errors if the ocio libs are linked before this: + bf_intern_opencolorio + bf_gpu + + bf_usd +) + +include_directories(${INC}) + +setup_libdirs() +get_property(BLENDER_SORTED_LIBS GLOBAL PROPERTY BLENDER_SORTED_LIBS_PROP) + +set(SRC + abstract_hierarchy_iterator_test.cc + hierarchy_context_order_test.cc +) + +if(UNIX AND NOT APPLE) + # TODO(Sybren): This unit test has only been tested on Linux, and should possibly be + # restructured to support other platforms as well. + list(APPEND SRC usd_stage_creation_test.cc) +endif() + + +if(WITH_BUILDINFO) + list(APPEND SRC "$<TARGET_OBJECTS:buildinfoobj>") +endif() + +BLENDER_SRC_GTEST_EX( + NAME usd + SRC "${SRC}" + EXTRA_LIBS "${LIB}" + COMMAND_ARGS + --test-assets-dir "${CMAKE_SOURCE_DIR}/../lib/tests" + --test-blender-executable-dir "${EXECUTABLE_OUTPUT_PATH}" +) + +setup_liblinks(usd_test) diff --git a/tests/gtests/usd/abstract_hierarchy_iterator_test.cc b/tests/gtests/usd/abstract_hierarchy_iterator_test.cc new file mode 100644 index 00000000000..e87ef547052 --- /dev/null +++ b/tests/gtests/usd/abstract_hierarchy_iterator_test.cc @@ -0,0 +1,202 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#include "blenloader/blendfile_loading_base_test.h" +#include "intern/abstract_hierarchy_iterator.h" + +extern "C" { +#include "BLI_math.h" +#include "DEG_depsgraph.h" +#include "DNA_object_types.h" +} + +#include <map> +#include <set> + +/* Mapping from ID.name to set of export hierarchy path. Duplicated objects can be exported + * multiple times, hence the set. */ +typedef std::map<std::string, std::set<std::string>> created_writers; + +using namespace USD; + +class TestHierarchyWriter : public AbstractHierarchyWriter { + public: + created_writers &writers_map; + + TestHierarchyWriter(created_writers &writers_map) : writers_map(writers_map) + { + } + + void write(HierarchyContext &context) override + { + const char *id_name = context.object->id.name; + created_writers::mapped_type &writers = writers_map[id_name]; + + BLI_assert(writers.find(context.export_path) == writers.end()); + writers.insert(context.export_path); + } +}; + +void debug_print_writers(const char *label, const created_writers &writers_map) +{ + printf("%s:\n", label); + for (auto idname_writers : writers_map) { + printf(" %s:\n", idname_writers.first.c_str()); + for (const std::string &export_path : idname_writers.second) { + printf(" - %s\n", export_path.c_str()); + } + } +} + +class TestingHierarchyIterator : public AbstractHierarchyIterator { + public: /* Public so that the test cases can directly inspect the created writers. */ + created_writers transform_writers; + created_writers data_writers; + created_writers hair_writers; + created_writers particle_writers; + + public: + explicit TestingHierarchyIterator(Depsgraph *depsgraph) : AbstractHierarchyIterator(depsgraph) + { + } + virtual ~TestingHierarchyIterator() + { + } + + protected: + AbstractHierarchyWriter *create_transform_writer(const HierarchyContext *context) override + { + return new TestHierarchyWriter(transform_writers); + } + AbstractHierarchyWriter *create_data_writer(const HierarchyContext *context) override + { + return new TestHierarchyWriter(data_writers); + } + AbstractHierarchyWriter *create_hair_writer(const HierarchyContext *context) override + { + return new TestHierarchyWriter(hair_writers); + } + AbstractHierarchyWriter *create_particle_writer(const HierarchyContext *context) override + { + return new TestHierarchyWriter(particle_writers); + } + + void delete_object_writer(AbstractHierarchyWriter *writer) override + { + delete writer; + } +}; + +class USDHierarchyIteratorTest : public BlendfileLoadingBaseTest { + protected: + TestingHierarchyIterator *iterator; + + virtual void SetUp() + { + BlendfileLoadingBaseTest::SetUp(); + iterator = nullptr; + } + + virtual void TearDown() + { + iterator_free(); + BlendfileLoadingBaseTest::TearDown(); + } + + /* Create a test iterator. */ + void iterator_create() + { + iterator = new TestingHierarchyIterator(depsgraph); + } + /* Free the test iterator if it is not nullptr. */ + void iterator_free() + { + if (iterator == nullptr) { + return; + } + delete iterator; + iterator = nullptr; + } +}; + +TEST_F(USDHierarchyIteratorTest, ExportHierarchyTest) +{ + /* Load the test blend file. */ + if (!blendfile_load("usd/usd_hierarchy_export_test.blend")) { + return; + } + depsgraph_create(DAG_EVAL_RENDER); + iterator_create(); + + iterator->iterate_and_write(); + + // Mapping from object name to set of export paths. + created_writers expected_transforms = { + {"OBCamera", {"/Camera"}}, + {"OBDupli1", {"/Dupli1"}}, + {"OBDupli2", {"/ParentOfDupli2/Dupli2"}}, + {"OBGEO_Ear_L", + {"/Dupli1/GEO_Head-0/GEO_Ear_L-1", + "/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L", + "/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_L-1"}}, + {"OBGEO_Ear_R", + {"/Dupli1/GEO_Head-0/GEO_Ear_R-2", + "/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R", + "/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_R-2"}}, + {"OBGEO_Head", + {"/Dupli1/GEO_Head-0", + "/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head", + "/ParentOfDupli2/Dupli2/GEO_Head-0"}}, + {"OBGEO_Nose", + {"/Dupli1/GEO_Head-0/GEO_Nose-3", + "/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose", + "/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Nose-3"}}, + {"OBGround plane", {"/Ground plane"}}, + {"OBOutsideDupliGrandParent", {"/Ground plane/OutsideDupliGrandParent"}}, + {"OBOutsideDupliParent", {"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent"}}, + {"OBParentOfDupli2", {"/ParentOfDupli2"}}}; + EXPECT_EQ(expected_transforms, iterator->transform_writers); + + created_writers expected_data = { + {"OBCamera", {"/Camera/Camera"}}, + {"OBGEO_Ear_L", + {"/Dupli1/GEO_Head-0/GEO_Ear_L-1/Ear", + "/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L/Ear", + "/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_L-1/Ear"}}, + {"OBGEO_Ear_R", + {"/Dupli1/GEO_Head-0/GEO_Ear_R-2/Ear", + "/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R/Ear", + "/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_R-2/Ear"}}, + {"OBGEO_Head", + {"/Dupli1/GEO_Head-0/Face", + "/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/Face", + "/ParentOfDupli2/Dupli2/GEO_Head-0/Face"}}, + {"OBGEO_Nose", + {"/Dupli1/GEO_Head-0/GEO_Nose-3/Nose", + "/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose/Nose", + "/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Nose-3/Nose"}}, + {"OBGround plane", {"/Ground plane/Plane"}}, + {"OBParentOfDupli2", {"/ParentOfDupli2/Icosphere"}}, + }; + + EXPECT_EQ(expected_data, iterator->data_writers); + + // The scene has no hair or particle systems. + EXPECT_EQ(0, iterator->hair_writers.size()); + EXPECT_EQ(0, iterator->particle_writers.size()); +} diff --git a/tests/gtests/usd/hierarchy_context_order_test.cc b/tests/gtests/usd/hierarchy_context_order_test.cc new file mode 100644 index 00000000000..ce3b43484e7 --- /dev/null +++ b/tests/gtests/usd/hierarchy_context_order_test.cc @@ -0,0 +1,123 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#include "intern/abstract_hierarchy_iterator.h" + +#include "testing/testing.h" + +extern "C" { +#include "BLI_utildefines.h" +} + +using namespace USD; + +class HierarchyContextOrderTest : public testing::Test { +}; + +static Object *fake_pointer(int value) +{ + return static_cast<Object *>(POINTER_FROM_INT(value)); +} + +TEST_F(HierarchyContextOrderTest, ObjectPointerTest) +{ + HierarchyContext ctx_a; + ctx_a.object = fake_pointer(1); + ctx_a.duplicator = nullptr; + + HierarchyContext ctx_b; + ctx_b.object = fake_pointer(2); + ctx_b.duplicator = nullptr; + + EXPECT_EQ(true, ctx_a < ctx_b); + EXPECT_EQ(false, ctx_b < ctx_a); + EXPECT_EQ(false, ctx_a < ctx_a); +} + +TEST_F(HierarchyContextOrderTest, DuplicatorPointerTest) +{ + HierarchyContext ctx_a; + ctx_a.object = fake_pointer(1); + ctx_a.duplicator = fake_pointer(1); + ctx_a.export_name = "A"; + + HierarchyContext ctx_b; + ctx_b.object = fake_pointer(1); + ctx_b.duplicator = fake_pointer(1); + ctx_b.export_name = "B"; + + EXPECT_EQ(true, ctx_a < ctx_b); + EXPECT_EQ(false, ctx_b < ctx_a); + EXPECT_EQ(false, ctx_a < ctx_a); +} + +TEST_F(HierarchyContextOrderTest, ExportParentTest) +{ + HierarchyContext ctx_a; + ctx_a.object = fake_pointer(1); + ctx_a.export_parent = fake_pointer(1); + + HierarchyContext ctx_b; + ctx_b.object = fake_pointer(1); + ctx_b.export_parent = fake_pointer(2); + + EXPECT_EQ(true, ctx_a < ctx_b); + EXPECT_EQ(false, ctx_b < ctx_a); + EXPECT_EQ(false, ctx_a < ctx_a); +} + +TEST_F(HierarchyContextOrderTest, TransitiveTest) +{ + HierarchyContext ctx_a; + ctx_a.object = fake_pointer(1); + ctx_a.export_parent = fake_pointer(1); + ctx_a.duplicator = nullptr; + ctx_a.export_name = "A"; + + HierarchyContext ctx_b; + ctx_b.object = fake_pointer(2); + ctx_b.export_parent = nullptr; + ctx_b.duplicator = fake_pointer(1); + ctx_b.export_name = "B"; + + HierarchyContext ctx_c; + ctx_c.object = fake_pointer(2); + ctx_c.export_parent = fake_pointer(2); + ctx_c.duplicator = fake_pointer(1); + ctx_c.export_name = "C"; + + HierarchyContext ctx_d; + ctx_d.object = fake_pointer(2); + ctx_d.export_parent = fake_pointer(3); + ctx_d.duplicator = nullptr; + ctx_d.export_name = "D"; + + EXPECT_EQ(true, ctx_a < ctx_b); + EXPECT_EQ(true, ctx_a < ctx_c); + EXPECT_EQ(true, ctx_a < ctx_d); + EXPECT_EQ(true, ctx_b < ctx_c); + EXPECT_EQ(true, ctx_b < ctx_d); + EXPECT_EQ(true, ctx_c < ctx_d); + + EXPECT_EQ(false, ctx_b < ctx_a); + EXPECT_EQ(false, ctx_c < ctx_a); + EXPECT_EQ(false, ctx_d < ctx_a); + EXPECT_EQ(false, ctx_c < ctx_b); + EXPECT_EQ(false, ctx_d < ctx_b); + EXPECT_EQ(false, ctx_d < ctx_c); +} diff --git a/tests/gtests/usd/usd_stage_creation_test.cc b/tests/gtests/usd/usd_stage_creation_test.cc new file mode 100644 index 00000000000..fcf1e93ea7d --- /dev/null +++ b/tests/gtests/usd/usd_stage_creation_test.cc @@ -0,0 +1,72 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ +#include "testing/testing.h" +#include <pxr/usd/usd/stage.h> + +#include <string> + +extern "C" { +#include "BLI_path_util.h" +#include "BLI_utildefines.h" + +#include "BKE_appdir.h" + +/* Workaround to make it possible to pass a path at runtime to USD. See creator.c. */ +void usd_initialise_plugin_path(const char *datafiles_usd_path); +} + +DEFINE_string(test_blender_executable_dir, "", "Blender's installation directory."); + +class USDStageCreationTest : public testing::Test { +}; + +TEST_F(USDStageCreationTest, JSONFileLoadingTest) +{ + std::string filename = "usd-stage-creation-test.usdc"; + + if (FLAGS_test_blender_executable_dir.empty()) { + FAIL() << "Pass the flag"; + } + + /* Required on Linux to make BKE_appdir_folder_id() find the datafiles. + * Without going to this directory, Blender looks for the datafiles in + * .../bin/tests instead of .../bin */ + const char *blender_executable_dir = FLAGS_test_blender_executable_dir.c_str(); + if (chdir(blender_executable_dir) < 0) { + FAIL() << "unable to change directory to " << FLAGS_test_blender_executable_dir; + } + + const char *usd_datafiles_relpath = BKE_appdir_folder_id(BLENDER_DATAFILES, "usd"); + EXPECT_NE(usd_datafiles_relpath, nullptr) << "Unable to find datafiles/usd"; + + char usd_datafiles_abspath[FILE_MAX]; + BLI_path_join(usd_datafiles_abspath, + sizeof(usd_datafiles_abspath), + blender_executable_dir, + usd_datafiles_relpath, + NULL); + + usd_initialise_plugin_path(usd_datafiles_abspath); + + /* Simply the ability to create a USD Stage for a specific filename means that the extension has + * been recognised by the USD library, and that a USD plugin has been loaded to write such files. + * Practically, this is a test to see whether the USD JSON files can be found and loaded. */ + pxr::UsdStageRefPtr usd_stage = pxr::UsdStage::CreateNew(filename); + EXPECT_TRUE(usd_stage) << "unable to find suitable USD plugin to write " << filename; +} |