USD: Introducing a simple USD Exporter
authorSybren A. Stüvel <sybren@blender.org>
Fri, 13 Dec 2019 09:27:40 +0000 (10:27 +0100)
committerSybren A. Stüvel <sybren@blender.org>
Fri, 13 Dec 2019 09:27:40 +0000 (10:27 +0100)
This commit introduces the first version of an exporter to Pixar's
Universal Scene Description (USD) format.

Reviewed By: sergey, LazyDodo

Differential Revision: https://developer.blender.org/D6287

- The USD libraries are built by `make deps`, but not yet built by
  install_deps.sh.
- Only experimental support for instancing; by default all duplicated
  objects are made real in the USD file. This is fine for exporting a
  linked-in posed character, not so much for thousands of pebbles etc.
- The way materials and UV coordinates and Normals are exported is going
  to change soon.
- This patch contains LazyDodo's fixes for building on Windows in D5359.

== Meshes ==

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. If there is no material we default to double-sidedness.

Each UV map is stored on the mesh in a separate primvar. Materials can
refer to these UV maps, but this is not yet exported by Blender. 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.

Face-varying mesh normals are written to USD. When the mesh has custom
loop normals those are written. Otherwise the poly flag `ME_SMOOTH` is
inspected to determine the normals.

The UV maps and mesh normals take up a significant amount of space, so
exporting them is optional. They're still enabled by default, though.
For comparison: a shot of Spring (03_035_A) is 1.2 GiB when exported
with UVs and normals, and 262 MiB without. We probably have room for
optimisation of written UVs and normals.

The mesh subdivision scheme isn't using the default value 'Catmull
Clark', but uses 'None', indicating we're exporting a polygonal mesh.
This is necessary for USD to understand our normals; otherwise the mesh
is always rendered smooth. In the future we may want to expose this
choice of subdivision scheme to the user, or auto-detect it when we
actually support exporting pre-subdivision meshes.

A possible optimisation could be to inspect whether all polygons are
smooth or flat, and mark the USD mesh as such. This can be added when
needed.

== Animation ==

Mesh and transform animation are now written when passing
`animation=True` to the export operator. There is no inspection of
whether an object is actually animated or not; USD can handle
deduplication of static values for us.

The administration of which timecode to use for the export is left to
the file-format-specific concrete subclasses of
`AbstractHierarchyIterator`; the abstract iterator itself doesn't know
anything about the passage of time. This will allow subclasses for the
frame-based USD format and time-based Alembic format.

== Support for simple preview materials ==

Very simple versions of the materials are now exported, using only the
viewport diffuse RGB, metallic, and roughness.

When there are multiple materials, the mesh faces are stored as geometry
subset and each material is assigned to the appropriate subset. If there
is only one material this is skipped.

The first material if any) is always applied to the mesh itself
(regardless of the existence of geometry subsets), because the Hydra
viewport doesn't support materials on subsets. See
https://github.com/PixarAnimationStudios/USD/issues/542 for more info.

Note that the geometry subsets are not yet time-sampled, so it may break
when an animated mesh changes topology.

Materials are exported as a flat list under a top-level '/_materials'
namespace. This inhibits instancing of the objects using those
materials, so this is subject to change.

== Hair ==

Only the parent strands are exported, and only with a constant colour.
No UV coordinates, no information about the normals.

== Camera ==

Only perspective cameras are supported for now.

== Particles ==

Particles are only written when they are alive, which means that they
are always visible (there is currently no code that deals with marking
them as invisible outside their lifespan).

Particle-system-instanced objects are exported by suffixing the object
name with the particle's persistent ID, giving each particle XForm a
unique name.

== Instancing/referencing ==

This exporter has experimental support for instancing/referencing.

Dupli-object meshes are now written to USD as references to the original
mesh. This is still very limited in correctness, as there are issues
referencing to materials from a referenced mesh.

I am still committing this, as it gives us a place to start when
continuing the quest for proper instancing in USD.

== Lights ==

USD does not directly support spot lights, so those aren't exported yet.
It's possible to add this in the future via the UsdLuxShapingAPI. The
units used for the light intensity are also still a bit of a mystery.

== Fluid vertex velocities ==

Currently only fluid simulations (not meshes in general) have explicit
vertex velocities. This is the most important case for exporting
velocities, 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.

== The Building Process ==

- USD is built as monolithic library, instead of 25 smaller libraries.
  We were linking all of them as 'whole archive' anyway, so this doesn't
  affect the final file size. It does, however, make life easier with
  respect to linking order, and handling upstream changes.
- The JSON files required by USD are installed into datafiles/usd; they
  are required on every platform. Set the `PXR_PATH_DEBUG` to any value
  to have the USD library print the paths it uses to find those files.
- USD is patched so that it finds the aforementioned JSON files in a path
  that we pass to it from Blender.
- USD is patched to have a `PXR_BUILD_USD_TOOLS` CMake option to disable
  building the tools in its `bin` directory. This is sent as a pull
  request at https://github.com/PixarAnimationStudios/USD/pull/1048

55 files changed:
CMakeLists.txt
build_files/build_environment/CMakeLists.txt
build_files/build_environment/cmake/harvest.cmake
build_files/build_environment/cmake/usd.cmake [new file with mode: 0644]
build_files/build_environment/cmake/versions.cmake
build_files/build_environment/patches/usd.diff [new file with mode: 0644]
build_files/cmake/Modules/FindUSD.cmake [new file with mode: 0644]
build_files/cmake/config/blender_full.cmake
build_files/cmake/config/blender_release.cmake
build_files/cmake/macros.cmake
build_files/cmake/platform/platform_apple.cmake
build_files/cmake/platform/platform_unix.cmake
build_files/cmake/platform/platform_win32.cmake
release/scripts/startup/bl_ui/space_topbar.py
release/scripts/startup/bl_ui/space_userpref.py
source/blender/CMakeLists.txt
source/blender/editors/io/CMakeLists.txt
source/blender/editors/io/io_ops.c
source/blender/editors/io/io_usd.c [new file with mode: 0644]
source/blender/editors/io/io_usd.h [new file with mode: 0644]
source/blender/editors/space_file/filelist.c
source/blender/editors/space_file/filesel.c
source/blender/makesdna/DNA_space_types.h
source/blender/makesdna/DNA_userdef_types.h
source/blender/makesrna/intern/rna_userdef.c
source/blender/python/intern/CMakeLists.txt
source/blender/python/intern/bpy_app_build_options.c
source/blender/usd/CMakeLists.txt [new file with mode: 0644]
source/blender/usd/intern/abstract_hierarchy_iterator.cc [new file with mode: 0644]
source/blender/usd/intern/abstract_hierarchy_iterator.h [new file with mode: 0644]
source/blender/usd/intern/usd_capi.cc [new file with mode: 0644]
source/blender/usd/intern/usd_exporter_context.h [new file with mode: 0644]
source/blender/usd/intern/usd_hierarchy_iterator.cc [new file with mode: 0644]
source/blender/usd/intern/usd_hierarchy_iterator.h [new file with mode: 0644]
source/blender/usd/intern/usd_writer_abstract.cc [new file with mode: 0644]
source/blender/usd/intern/usd_writer_abstract.h [new file with mode: 0644]
source/blender/usd/intern/usd_writer_camera.cc [new file with mode: 0644]
source/blender/usd/intern/usd_writer_camera.h [new file with mode: 0644]
source/blender/usd/intern/usd_writer_hair.cc [new file with mode: 0644]
source/blender/usd/intern/usd_writer_hair.h [new file with mode: 0644]
source/blender/usd/intern/usd_writer_light.cc [new file with mode: 0644]
source/blender/usd/intern/usd_writer_light.h [new file with mode: 0644]
source/blender/usd/intern/usd_writer_mesh.cc [new file with mode: 0644]
source/blender/usd/intern/usd_writer_mesh.h [new file with mode: 0644]
source/blender/usd/intern/usd_writer_transform.cc [new file with mode: 0644]
source/blender/usd/intern/usd_writer_transform.h [new file with mode: 0644]
source/blender/usd/usd.h [new file with mode: 0644]
source/blender/windowmanager/intern/wm_operator_props.c
source/creator/CMakeLists.txt
source/creator/creator.c
tests/gtests/CMakeLists.txt
tests/gtests/usd/CMakeLists.txt [new file with mode: 0644]
tests/gtests/usd/abstract_hierarchy_iterator_test.cc [new file with mode: 0644]
tests/gtests/usd/hierarchy_context_order_test.cc [new file with mode: 0644]
tests/gtests/usd/usd_stage_creation_test.cc [new file with mode: 0644]

index 372fa8fa784b279dee58129ff14871be230bf39e..e3e6f8c0c66f4a7f117e8bd636ab05f4f7a63b7d 100644 (file)
@@ -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)
index cdfa18ff4ff401de6b1298967d4da1ea30c3d167..fb32d2218b8f7c3d9050b98d8d41483acc6d889b 100644 (file)
@@ -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()
index 89eec7cf72fb447850cc01a7483d17eaa0797cc3..cc93db7de6483dee248cfa1ff08e482794931e0b 100644 (file)
@@ -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 (file)
index 0000000..c359439
--- /dev/null
@@ -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()
index 9cbf104e84206b9f7453cfc64c9a43b3be602aa8..2b08a74c1aa44b6943b475022e9d4d642cc7f324 100644 (file)
@@ -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 (file)
index 0000000..8afd970
--- /dev/null
@@ -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 (file)
index 0000000..3ebcbb1
--- /dev/null
@@ -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)
index fae15ea979b44ceb35033ea90062160aaeb304e3..2425354aa768a334516079270cb9a3226c123c4b 100644 (file)
@@ -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)
 
index 07d95a8411230ba416fe56268aa2c8ef076f481f..88285e07375e139bf677e37d38d085db503be37b 100644 (file)
@@ -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)
index 3944a8b3c56ce1aacf4c8c5633a6b84a2081b00f..3b6b4720a7c8cf7d13f20f1ed906c721b96f77e1 100644 (file)
@@ -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()
index 578ad143d59f7ab8d2276814e7d55377f97e7242..d99d7ec3c0ca62270af1650ee3ed159944e607fe 100644 (file)
@@ -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)
index c48780ebd6a13d5341b37661467f74b881f8ab56..d4a75e5e5c0aac90a1ce2c76ae06bb2d7ffeaa4a 100644 (file)
@@ -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)
index 9061e1fcf82574076cd9f6499965472cc4d10f4c..b1d1942598d6da3e1dbce62ca44ea187ece908b8 100644 (file)
@@ -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/*.*" )
index 2e2c5adb97043a43c4d19a999a15a7bee6f761bb..e69a28d69bf9707c515dbaeb9015a832b82c16ae 100644 (file)
@@ -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):
index bf39cbda3911ec038a6fafc3ef9468cdade3c8a6..68581ee2ad8400e45e1d6d19c80f95d029017fda 100644 (file)
@@ -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,
index 62923d18b7044a7ae721de528a3face7372d241a..68d7b6d9b2de456b7d933db98812a1a0277cfb21 100644 (file)
@@ -158,3 +158,6 @@ endif()
 if(WIN32)
   add_subdirectory(blendthumb)
 endif()
+if(WITH_USD)
+  add_subdirectory(usd)
+endif()
index 5a35b251d0c80ce919a353caec04e4392c71a6fd..5afe348158f0ea1e958c1f7ebfdba579fbbe65af 100644 (file)
@@ -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()
index e04fe4a20c0141997471f44f4964b53e8a01f086..acb511a414d3b20ae9a57d3a6bf26635828fd89d 100644 (file)
 #  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 (file)
index 0000000..524d8d5
--- /dev/null
@@ -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, &params, 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 (file)
index 0000000..4738e1c
--- /dev/null
@@ -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__ */
index 1ea7d81f9dae617ff0c8d241f597666b93e6423e..a567aeed8268ba84be96bc7e8985110a7e05c5ac 100644 (file)
@@ -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;
   }
index 99e4fc62980f9c9413a9d47c0221ef7d9fd6a81f..02fb98aa7d751af7c63b5b842acffbddc2db1471 100644 (file)
@@ -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(
index 2fb439c80741216370ce1a7d07828da35c1cd854..8261b9e678bb18d2e69b6ee48c234c49e5f39378 100644 (file)
@@ -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),
index c378f52d7ba8eafed498368655e4f1c18a8a896e..fb23b39a61619990848bfd4a118b9037b23316c5 100644 (file)
@@ -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) \
index 1267cfed3d877bb62090b543cb5ffc7d065ac3cd..f1aaa13fcf548c4e4e9b7d53a6b1f085e9c7e79e 100644 (file)
@@ -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)
index a5f71e9243825e8a7fd43dbfd2da6480406569ec..d5f0e2fb863ce5c770d76d898de0067e11f9c4f6 100644 (file)
@@ -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
index afb2f6b3636aa2d0a7d2c9b356e3a7210e5efbf9..ee6a3fd41d888d124151d0eedd9d539649063c46 100644 (file)
@@ -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 (file)
index 0000000..12d281f
--- /dev/null
@@ -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 (file)
index 0000000..3ad2b2c
--- /dev/null
@@ -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 (file)
index 0000000..db1e424
--- /dev/null
@@ -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 (file)
index 0000000..502f867
--- /dev/null
@@ -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 (file)
index 0000000..aaa0121
--- /dev/null
@@ -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 (file)
index 0000000..f53cba8
--- /dev/null
@@ -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 &params)
+    : 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 (file)
index 0000000..a937df8
--- /dev/null
@@ -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 &params_;
+
+ public:
+  USDHierarchyIterator(Depsgraph *depsgraph,
+                       pxr::UsdStageRefPtr stage,
+                       const USDExportParams &params);
+
+  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 (file)
index 0000000..4d0b436
--- /dev/null
@@ -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 (file)
index 0000000..cea4685
--- /dev/null
@@ -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 (file)
index 0000000..9b85d69
--- /dev/null
@@ -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 (file)
index 0000000..86b5ed4
--- /dev/null
@@ -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 (file)
index 0000000..d09922b
--- /dev/null
@@ -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 (file)
index 0000000..ccb5af7
--- /dev/null
@@ -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 (file)
index 0000000..e13e2c5
--- /dev/null
@@ -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 (file)
index 0000000..3813412
--- /dev/null
@@ -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 (file)
index 0000000..ca0171f
--- /dev/null
@@ -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 (file)
index 0000000..0f03c8d
--- /dev/null
@@ -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 (file)
index 0000000..321b516
--- /dev/null
@@ -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 (file)
index 0000000..fb59199
--- /dev/null
@@ -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 (file)
index 0000000..412a51b
--- /dev/null
@@ -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__ */
index 9aefb4f68cbe420434951f58c57de53d73e5c975..2a40fb138c096db66778296c222343708b428409 100644 (file)
@@ -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", "");
index 96ceac5c4d40fa5559ee05a5fba8c004f4a983c2..a7a816385ad40fca664b254066c7fdee083302bc 100644 (file)
@@ -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
index f4f5e3dcbdea4a8ae8cf0f3314e1c22c50e85807..32377da528413f7d8d87ded32c725be79508c12b 100644 (file)
@@ -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);
index 54a1ee411980d1a68d6d317e048f1dc0e3150e90..7da65bcc8b9809276b0e9eeb38571045ea686aec 100644 (file)
@@ -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 (file)
index 0000000..e276850
--- /dev/null
@@ -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 (file)
index 0000000..e87ef54
--- /dev/null
@@ -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 (file)
index 0000000..ce3b434
--- /dev/null
@@ -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 (file)
index 0000000..fcf1e93
--- /dev/null
@@ -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;
+}