USD: Add support for animated volumes
Some checks reported errors
buildbot/vdev-code-daily-lint Build done.
buildbot/vdev-code-daily-darwin-arm64 Build done.
buildbot/vdev-code-daily-linux-x86_64 Build done.
buildbot/vdev-code-daily-darwin-x86_64 Build done.
buildbot/vdev-code-daily-windows-amd64 Build done.
buildbot/vdev-code-daily-coordinator Build done.
Some checks reported errors
buildbot/vdev-code-daily-lint Build done.
buildbot/vdev-code-daily-darwin-arm64 Build done.
buildbot/vdev-code-daily-linux-x86_64 Build done.
buildbot/vdev-code-daily-darwin-x86_64 Build done.
buildbot/vdev-code-daily-windows-amd64 Build done.
buildbot/vdev-code-daily-coordinator Build done.
The existing Volume export, which already supports VDB file sequences and static volumes created inside Blender, is now extended to handle dynamically created and modified volumes. This allows scenarios where a Volume Displace modifier is placed over-top an existing VDB sequence or when Geometry Nodes is used to create animated volumes procedurally. Detection of what counts as animation is simplistic and mimics what has been used for Meshes. Essentially if there are any modifiers on the volume we assume that the volume is "varying" in some way. This can lead to situations where new volume files are written unnecessarily. Volume import was also adjusted to correctly set the sequence "offset" value. This is required to properly handle the case when a VDB sequence begins animating at a different frame than what's implied by the file name. For example, a VDB file sequence with file names containing 14-19 but the user wants to animate on frames 8-13 instead. Tests are added which cover: - Animated VDB file sequences - Animated Mesh To Volume where the mesh has been animated - Animated Volume Displacement where displacement settings are animated - Animated Volumes created with a Geometry Nodes simulation ---- New test data has been checked in: `tests/data/usd/usd_volumes.blend` and files inside `tests/data/usd/volume-data/` Pull Request: #128907
This commit is contained in:
parent
08a9c8b786
commit
391612c725
@ -4,6 +4,7 @@
|
||||
|
||||
#include "usd_reader_volume.hh"
|
||||
|
||||
#include "BLI_path_utils.hh"
|
||||
#include "BLI_string.h"
|
||||
|
||||
#include "BKE_object.hh"
|
||||
@ -53,23 +54,25 @@ void USDVolumeReader::read_object_data(Main *bmain, const double motionSampleTim
|
||||
pxr::SdfAssetPath fp;
|
||||
filepathAttr.Get(&fp, motionSampleTime);
|
||||
|
||||
const std::string filepath = fp.GetResolvedPath();
|
||||
STRNCPY(volume->filepath, filepath.c_str());
|
||||
|
||||
if (filepathAttr.ValueMightBeTimeVarying()) {
|
||||
std::vector<double> filePathTimes;
|
||||
filepathAttr.GetTimeSamples(&filePathTimes);
|
||||
|
||||
if (!filePathTimes.empty()) {
|
||||
int start = int(filePathTimes.front());
|
||||
int end = int(filePathTimes.back());
|
||||
const int start = int(filePathTimes.front());
|
||||
const int end = int(filePathTimes.back());
|
||||
const int offset = BLI_path_sequence_decode(
|
||||
volume->filepath, nullptr, 0, nullptr, 0, nullptr);
|
||||
|
||||
volume->is_sequence = char(true);
|
||||
volume->frame_start = start;
|
||||
volume->frame_duration = (end - start) + 1;
|
||||
volume->frame_offset = offset - 1;
|
||||
}
|
||||
}
|
||||
|
||||
std::string filepath = fp.GetResolvedPath();
|
||||
|
||||
STRNCPY(volume->filepath, filepath.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,10 +6,14 @@
|
||||
#include "usd_hierarchy_iterator.hh"
|
||||
#include "usd_utils.hh"
|
||||
|
||||
#include <pxr/base/gf/vec3f.h>
|
||||
#include <pxr/base/tf/pathUtils.h>
|
||||
#include <pxr/base/vt/array.h>
|
||||
#include <pxr/base/vt/value.h>
|
||||
#include <pxr/usd/usdVol/openVDBAsset.h>
|
||||
#include <pxr/usd/usdVol/volume.h>
|
||||
|
||||
#include "DNA_scene_types.h"
|
||||
#include "DNA_volume_types.h"
|
||||
#include "DNA_windowmanager_types.h"
|
||||
|
||||
@ -23,14 +27,34 @@
|
||||
#include "BLI_path_utils.hh"
|
||||
#include "BLI_string.h"
|
||||
|
||||
#include "DEG_depsgraph_query.hh"
|
||||
|
||||
namespace blender::io::usd {
|
||||
|
||||
static bool has_varying_modifiers(const Object *ob)
|
||||
{
|
||||
/* These modifiers may vary the Volume either over time or by deformation/transformation. */
|
||||
ModifierData *md = static_cast<ModifierData *>(ob->modifiers.first);
|
||||
while (md) {
|
||||
if (ELEM(md->type,
|
||||
eModifierType_Nodes,
|
||||
eModifierType_VolumeDisplace,
|
||||
eModifierType_MeshToVolume))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
md = md->next;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
USDVolumeWriter::USDVolumeWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx) {}
|
||||
|
||||
bool USDVolumeWriter::check_is_animated(const HierarchyContext &context) const
|
||||
{
|
||||
const Volume *volume = static_cast<Volume *>(context.object->data);
|
||||
return volume->is_sequence;
|
||||
return volume->is_sequence || has_varying_modifiers(context.object);
|
||||
}
|
||||
|
||||
void USDVolumeWriter::do_write(HierarchyContext &context)
|
||||
@ -45,7 +69,8 @@ void USDVolumeWriter::do_write(HierarchyContext &context)
|
||||
return;
|
||||
}
|
||||
|
||||
auto vdb_file_path = resolve_vdb_file(volume);
|
||||
const bool has_modifiers = has_varying_modifiers(context.object);
|
||||
auto vdb_file_path = resolve_vdb_file(volume, has_modifiers);
|
||||
if (!vdb_file_path.has_value()) {
|
||||
BKE_reportf(reports(),
|
||||
RPT_WARNING,
|
||||
@ -78,27 +103,49 @@ void USDVolumeWriter::do_write(HierarchyContext &context)
|
||||
usd_export_context_.export_params.allow_unicode);
|
||||
const pxr::SdfPath grid_path = volume_path.AppendPath(pxr::SdfPath(grid_id));
|
||||
pxr::UsdVolOpenVDBAsset usd_grid = pxr::UsdVolOpenVDBAsset::Define(stage, grid_path);
|
||||
usd_grid.GetFieldNameAttr().Set(pxr::TfToken(grid_name), timecode);
|
||||
usd_grid.GetFilePathAttr().Set(pxr::SdfAssetPath(*vdb_file_path), timecode);
|
||||
|
||||
pxr::TfToken grid_name_token = pxr::TfToken(grid_name);
|
||||
pxr::SdfAssetPath asset_path = pxr::SdfAssetPath(*vdb_file_path);
|
||||
pxr::UsdAttribute attr_field = usd_grid.CreateFieldNameAttr(pxr::VtValue(), true);
|
||||
pxr::UsdAttribute attr_file = usd_grid.CreateFilePathAttr(pxr::VtValue(), true);
|
||||
if (!attr_field.HasValue()) {
|
||||
attr_field.Set(grid_name_token, pxr::UsdTimeCode::Default());
|
||||
}
|
||||
if (!attr_file.HasValue()) {
|
||||
attr_file.Set(asset_path, pxr::UsdTimeCode::Default());
|
||||
}
|
||||
|
||||
usd_value_writer_.SetAttribute(attr_field, grid_name_token, timecode);
|
||||
usd_value_writer_.SetAttribute(attr_file, asset_path, timecode);
|
||||
|
||||
usd_volume.CreateFieldRelationship(pxr::TfToken(grid_id), grid_path);
|
||||
}
|
||||
|
||||
if (const std::optional<Bounds<float3>> bounds = BKE_volume_min_max(volume)) {
|
||||
const pxr::VtArray<pxr::GfVec3f> volume_extent = {pxr::GfVec3f(&bounds->min.x),
|
||||
pxr::GfVec3f(&bounds->max.x)};
|
||||
usd_volume.GetExtentAttr().Set(volume_extent, timecode);
|
||||
pxr::VtArray<pxr::GfVec3f> volume_extent = {pxr::GfVec3f(&bounds->min.x),
|
||||
pxr::GfVec3f(&bounds->max.x)};
|
||||
|
||||
pxr::UsdAttribute attr_extent = usd_volume.CreateExtentAttr(pxr::VtValue(), true);
|
||||
if (!attr_extent.HasValue()) {
|
||||
attr_extent.Set(volume_extent, pxr::UsdTimeCode::Default());
|
||||
}
|
||||
|
||||
usd_value_writer_.SetAttribute(attr_extent, volume_extent, timecode);
|
||||
}
|
||||
|
||||
BKE_volume_unload(volume);
|
||||
}
|
||||
|
||||
std::optional<std::string> USDVolumeWriter::resolve_vdb_file(const Volume *volume) const
|
||||
std::optional<std::string> USDVolumeWriter::resolve_vdb_file(const Volume *volume,
|
||||
bool has_modifiers) const
|
||||
{
|
||||
std::optional<std::string> vdb_file_path;
|
||||
if (volume->filepath[0] == '\0') {
|
||||
/* Entering this section should mean that Volume object contains OpenVDB data that is not
|
||||
* obtained from external `.vdb` file but rather generated inside of Blender (i.e. by 'Mesh to
|
||||
* Volume' modifier). Try to save this data to a `.vdb` file. */
|
||||
|
||||
const bool needs_vdb_save = volume->filepath[0] == '\0' || has_modifiers;
|
||||
if (needs_vdb_save) {
|
||||
/* Entering this section means that the Volume object contains OpenVDB data that is not
|
||||
* obtained soley from external `.vdb` files but is generated or modified inside of Blender.
|
||||
* Write this data as a new `.vdb` files. */
|
||||
|
||||
vdb_file_path = construct_vdb_file_path(volume);
|
||||
if (!BKE_volume_save(
|
||||
@ -144,13 +191,15 @@ std::optional<std::string> USDVolumeWriter::construct_vdb_file_path(const Volume
|
||||
BLI_strncat(vdb_directory_path, vdb_directory_name, sizeof(vdb_directory_path));
|
||||
BLI_dir_create_recursive(vdb_directory_path);
|
||||
|
||||
const Scene *scene = DEG_get_input_scene(usd_export_context_.depsgraph);
|
||||
const int max_frame_digits = std::max(2, integer_digits_i(abs(scene->r.efra)));
|
||||
|
||||
char vdb_file_name[FILE_MAXFILE];
|
||||
STRNCPY(vdb_file_name, volume->id.name + 2);
|
||||
const pxr::UsdTimeCode timecode = get_export_time_code();
|
||||
if (!timecode.IsDefault()) {
|
||||
const int frame = int(timecode.GetValue());
|
||||
const int num_frame_digits = frame == 0 ? 1 : integer_digits_i(abs(frame));
|
||||
BLI_path_frame(vdb_file_name, sizeof(vdb_file_name), frame, num_frame_digits);
|
||||
BLI_path_frame(vdb_file_name, sizeof(vdb_file_name), frame, max_frame_digits);
|
||||
}
|
||||
BLI_strncat(vdb_file_name, ".vdb", sizeof(vdb_file_name));
|
||||
|
||||
|
@ -27,7 +27,7 @@ class USDVolumeWriter : public USDAbstractWriter {
|
||||
* mean that `resolve_vdb_file` method will try to export volume data to a new `.vdb` file.
|
||||
* If successful, this method returns absolute file path to the resolved `.vdb` file, if not,
|
||||
* returns `std::nullopt`. */
|
||||
std::optional<std::string> resolve_vdb_file(const Volume *volume) const;
|
||||
std::optional<std::string> resolve_vdb_file(const Volume *volume, bool has_modifiers) const;
|
||||
|
||||
std::optional<std::string> construct_vdb_file_path(const Volume *volume) const;
|
||||
std::optional<std::string> construct_vdb_relative_file_path(
|
||||
|
@ -8,7 +8,7 @@ import pprint
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pxr import Gf, Sdf, Usd, UsdGeom, UsdShade, UsdSkel, UsdUtils
|
||||
from pxr import Gf, Sdf, Usd, UsdGeom, UsdShade, UsdSkel, UsdUtils, UsdVol
|
||||
|
||||
import bpy
|
||||
|
||||
@ -605,6 +605,61 @@ class USDExportTest(AbstractUSDTest):
|
||||
weight_samples = anim.GetBlendShapeWeightsAttr().GetTimeSamples()
|
||||
self.assertEqual(weight_samples, [1.0, 2.0, 3.0, 4.0, 5.0])
|
||||
|
||||
def test_export_volumes(self):
|
||||
"""Test various combinations of volume export including with all supported volume modifiers."""
|
||||
|
||||
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_volumes.blend"))
|
||||
# Ensure the simulation zone data is baked for all relevant frames...
|
||||
for frame in range(4, 15):
|
||||
bpy.context.scene.frame_set(frame)
|
||||
bpy.context.scene.frame_set(4)
|
||||
|
||||
export_path = self.tempdir / "usd_volumes.usda"
|
||||
self.export_and_validate(filepath=str(export_path), export_animation=True, evaluation_mode="RENDER")
|
||||
|
||||
stage = Usd.Stage.Open(str(export_path))
|
||||
|
||||
# Validate that we see some form of time varyability across the Volume prim's extents and
|
||||
# file paths. The data should be sparse so it should only be written on the frames which
|
||||
# change.
|
||||
|
||||
# File sequence
|
||||
vol_fileseq = UsdVol.Volume(stage.GetPrimAtPath("/root/vol_filesequence/vol_filesequence"))
|
||||
density = UsdVol.OpenVDBAsset(stage.GetPrimAtPath("/root/vol_filesequence/vol_filesequence/density_noise"))
|
||||
flame = UsdVol.OpenVDBAsset(stage.GetPrimAtPath("/root/vol_filesequence/vol_filesequence/flame_noise"))
|
||||
self.assertEqual(vol_fileseq.GetExtentAttr().GetTimeSamples(), [10.0, 11.0])
|
||||
self.assertEqual(density.GetFieldNameAttr().GetTimeSamples(), [])
|
||||
self.assertEqual(density.GetFilePathAttr().GetTimeSamples(), [8.0, 9.0, 10.0, 11.0, 12.0, 13.0])
|
||||
self.assertEqual(flame.GetFieldNameAttr().GetTimeSamples(), [])
|
||||
self.assertEqual(flame.GetFilePathAttr().GetTimeSamples(), [8.0, 9.0, 10.0, 11.0, 12.0, 13.0])
|
||||
|
||||
# Mesh To Volume
|
||||
vol_mesh2vol = UsdVol.Volume(stage.GetPrimAtPath("/root/vol_mesh2vol/vol_mesh2vol"))
|
||||
density = UsdVol.OpenVDBAsset(stage.GetPrimAtPath("/root/vol_mesh2vol/vol_mesh2vol/density"))
|
||||
self.assertEqual(vol_mesh2vol.GetExtentAttr().GetTimeSamples(),
|
||||
[6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0])
|
||||
self.assertEqual(density.GetFieldNameAttr().GetTimeSamples(), [])
|
||||
self.assertEqual(density.GetFilePathAttr().GetTimeSamples(),
|
||||
[4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0])
|
||||
|
||||
# Volume Displace
|
||||
vol_displace = UsdVol.Volume(stage.GetPrimAtPath("/root/vol_displace/vol_displace"))
|
||||
unnamed = UsdVol.OpenVDBAsset(stage.GetPrimAtPath("/root/vol_displace/vol_displace/_"))
|
||||
self.assertEqual(vol_displace.GetExtentAttr().GetTimeSamples(),
|
||||
[5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0])
|
||||
self.assertEqual(unnamed.GetFieldNameAttr().GetTimeSamples(), [])
|
||||
self.assertEqual(unnamed.GetFilePathAttr().GetTimeSamples(),
|
||||
[4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0])
|
||||
|
||||
# Geometry Node simulation
|
||||
vol_sim = UsdVol.Volume(stage.GetPrimAtPath("/root/vol_sim/Volume"))
|
||||
density = UsdVol.OpenVDBAsset(stage.GetPrimAtPath("/root/vol_sim/Volume/density"))
|
||||
self.assertEqual(vol_sim.GetExtentAttr().GetTimeSamples(),
|
||||
[4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0])
|
||||
self.assertEqual(density.GetFieldNameAttr().GetTimeSamples(), [])
|
||||
self.assertEqual(density.GetFilePathAttr().GetTimeSamples(),
|
||||
[4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0])
|
||||
|
||||
def test_export_xform_ops(self):
|
||||
"""Test exporting different xform operation modes."""
|
||||
|
||||
|
@ -442,6 +442,56 @@ class USDImportTest(AbstractUSDTest):
|
||||
self.assertAlmostEqual(ob_arm2_side_a.matrix_world.to_euler('XYZ').z, 1.5708, 5)
|
||||
self.assertAlmostEqual(ob_arm2_side_b.matrix_world.to_euler('XYZ').z, 1.5708, 5)
|
||||
|
||||
def test_import_volumes(self):
|
||||
"""Validate volume import."""
|
||||
|
||||
# Use the existing volume test file to create the USD file
|
||||
# for import. It is validated as part of the bl_usd_export test.
|
||||
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_volumes.blend"))
|
||||
# Ensure the simulation zone data is baked for all relevant frames...
|
||||
for frame in range(4, 15):
|
||||
bpy.context.scene.frame_set(frame)
|
||||
bpy.context.scene.frame_set(4)
|
||||
|
||||
testfile = str(self.tempdir / "usd_volumes.usda")
|
||||
res = bpy.ops.wm.usd_export(filepath=testfile, export_animation=True, evaluation_mode="RENDER")
|
||||
self.assertEqual({'FINISHED'}, res, f"Unable to export to {testfile}")
|
||||
|
||||
# Reload the empty file and import back in
|
||||
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
|
||||
res = bpy.ops.wm.usd_import(filepath=testfile)
|
||||
self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {testfile}")
|
||||
|
||||
# Validate that all volumes are properly configured.
|
||||
vol_displace = bpy.data.objects["vol_displace"]
|
||||
vol_filesequence = bpy.data.objects["vol_filesequence"]
|
||||
vol_mesh2vol = bpy.data.objects["vol_mesh2vol"]
|
||||
vol_sim = bpy.data.objects["Volume"]
|
||||
|
||||
def check_sequence(ob, frames, start, offset):
|
||||
self.assertTrue(ob.data.is_sequence)
|
||||
self.assertEqual(ob.data.frame_duration, frames)
|
||||
self.assertEqual(ob.data.frame_start, start)
|
||||
self.assertEqual(ob.data.frame_offset, offset)
|
||||
|
||||
check_sequence(vol_displace, 11, 4, 3)
|
||||
check_sequence(vol_filesequence, 6, 8, 13)
|
||||
check_sequence(vol_mesh2vol, 11, 4, 3)
|
||||
check_sequence(vol_sim, 11, 4, 3)
|
||||
|
||||
# Validate that their object dimensions are changing by spot checking 2 interesting frames
|
||||
bpy.context.scene.frame_set(8)
|
||||
dim_displace = vol_displace.dimensions.copy()
|
||||
dim_filesequence = vol_filesequence.dimensions.copy()
|
||||
dim_mesh2vol = vol_mesh2vol.dimensions.copy()
|
||||
dim_sim = vol_sim.dimensions.copy()
|
||||
|
||||
bpy.context.scene.frame_set(12)
|
||||
self.assertTrue(vol_displace.dimensions != dim_displace)
|
||||
self.assertTrue(vol_filesequence.dimensions != dim_filesequence)
|
||||
self.assertTrue(vol_mesh2vol.dimensions != dim_mesh2vol)
|
||||
self.assertTrue(vol_sim.dimensions != dim_sim)
|
||||
|
||||
def test_import_usd_blend_shapes(self):
|
||||
"""Test importing USD blend shapes with animated weights."""
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user