mirror of
https://github.com/blender/blender
synced 2026-09-29 04:37:17 +03:00
Alembic: import and export of subdivision parameters
This adds support to import and to export subdivision parameters from subsurface modifiers in Alembic. As Alembic is using the OpenSubDiv 2.0 API to define the parameters, we need to convert to and from the OpenSubDiv 3.0 API which Blender uses. Since Alembic does not define proper enumerations for these values, this adds relevant enumerations whose names derive from the USD Alembic exporter to avoid using magic values. On top of that we also export and import the subdivision levels for viewport and render so those values get preserved when reimporting in Blender. For import, this also automatically adds a subdivision surface modifier to subdivision meshes. Pull Request: https://projects.blender.org/blender/blender/pulls/160260
This commit is contained in:
parent
9cb019e4be
commit
f2a1c2848a
9 changed files with 271 additions and 3 deletions
|
|
@ -9,6 +9,7 @@
|
|||
#include "abc_writer_mesh.h"
|
||||
#include "abc_hierarchy_iterator.h"
|
||||
#include "intern/abc_axis_conversion.h"
|
||||
#include "intern/abc_util.h"
|
||||
|
||||
#include "BKE_attribute.h"
|
||||
#include "BKE_attribute.hh"
|
||||
|
|
@ -45,6 +46,7 @@ using Alembic::AbcGeom::OBoolProperty;
|
|||
using Alembic::AbcGeom::OCompoundProperty;
|
||||
using Alembic::AbcGeom::OFaceSet;
|
||||
using Alembic::AbcGeom::OFaceSetSchema;
|
||||
using Alembic::AbcGeom::OInt32Property;
|
||||
using Alembic::AbcGeom::ON3fGeomParam;
|
||||
using Alembic::AbcGeom::OPolyMesh;
|
||||
using Alembic::AbcGeom::OPolyMeshSchema;
|
||||
|
|
@ -70,6 +72,44 @@ static void get_vert_creases(Mesh *mesh,
|
|||
std::vector<float> &sharpnesses);
|
||||
static void get_loop_normals(const Mesh *mesh, std::vector<Imath::V3f> &normals);
|
||||
|
||||
/* Get the last subdiv modifier ignoring subsequent particle systems modifiers, regardless of
|
||||
* enable/disable status.
|
||||
* TODO(kevindietrich) : deduplicate this with USD, but USD does not ignore particle systems. */
|
||||
static const SubsurfModifierData *get_last_subdiv_modifier(eEvaluationMode eval_mode, Object *obj)
|
||||
{
|
||||
BLI_assert(obj);
|
||||
|
||||
/* Return the subdiv modifier if it is the last modifier and has
|
||||
* the required mode enabled. */
|
||||
|
||||
ModifierData *md = static_cast<ModifierData *>(obj->modifiers.last);
|
||||
|
||||
while (md != nullptr) {
|
||||
if (md->type != eModifierType_ParticleSystem) {
|
||||
break;
|
||||
}
|
||||
md = md->prev;
|
||||
}
|
||||
|
||||
if (!md) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* Determine if the modifier is enabled for the current evaluation mode. */
|
||||
ModifierMode mod_mode = (eval_mode == DAG_EVAL_RENDER) ? eModifierMode_Render :
|
||||
eModifierMode_Realtime;
|
||||
|
||||
if ((md->mode & mod_mode) != mod_mode) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (md->type == eModifierType_Subsurf) {
|
||||
return reinterpret_cast<SubsurfModifierData *>(md);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ABCGenericMeshWriter::ABCGenericMeshWriter(const ABCWriterConstructorArgs &args)
|
||||
: ABCAbstractWriter(args), is_subd_(false)
|
||||
{
|
||||
|
|
@ -85,6 +125,12 @@ void ABCGenericMeshWriter::create_alembic_objects(const HierarchyContext *contex
|
|||
CLOG_DEBUG(&LOG, "exporting OSubD %s", args_.abc_path.c_str());
|
||||
abc_subdiv_ = OSubD(args_.abc_parent, args_.abc_name, timesample_index_);
|
||||
abc_subdiv_schema_ = abc_subdiv_.getSchema();
|
||||
|
||||
abc_custom_data_container_ = abc_subdiv_schema_.getUserProperties();
|
||||
abc_subdiv_render_levels_ = OInt32Property(
|
||||
abc_custom_data_container_, "subdivRenderLevels", timesample_index_);
|
||||
abc_subdiv_viewport_levels_ = OInt32Property(
|
||||
abc_custom_data_container_, "subdivViewportLevels", timesample_index_);
|
||||
}
|
||||
else {
|
||||
CLOG_DEBUG(&LOG, "exporting OPolyMesh %s", args_.abc_path.c_str());
|
||||
|
|
@ -319,6 +365,53 @@ void ABCGenericMeshWriter::write_subd(HierarchyContext &context, Mesh *mesh)
|
|||
subdiv_sample.setCornerSharpnesses(FloatArraySample(vert_crease_sharpness));
|
||||
}
|
||||
|
||||
const SubsurfModifierData *subsurf_data = get_last_subdiv_modifier(
|
||||
args_.export_params->evaluation_mode, context.object);
|
||||
|
||||
if (subsurf_data) {
|
||||
AbcFaceVaryingInterpolateBoundary fvar_interpolate_boundary =
|
||||
AbcFaceVaryingInterpolateBoundary::ALL;
|
||||
AbcInterpolateBoundary interpolate_boundary = AbcInterpolateBoundary::NONE;
|
||||
int propagate_corners = 0;
|
||||
|
||||
/* Confusingly, ALL is NONE and NONE is ALL. */
|
||||
switch (subsurf_data->uv_smooth) {
|
||||
case SUBSURF_UV_SMOOTH_NONE:
|
||||
fvar_interpolate_boundary = AbcFaceVaryingInterpolateBoundary::ALL;
|
||||
break;
|
||||
case SUBSURF_UV_SMOOTH_PRESERVE_CORNERS:
|
||||
case SUBSURF_UV_SMOOTH_PRESERVE_CORNERS_AND_JUNCTIONS:
|
||||
fvar_interpolate_boundary = AbcFaceVaryingInterpolateBoundary::EDGE_AND_CORNERS;
|
||||
break;
|
||||
case SUBSURF_UV_SMOOTH_PRESERVE_CORNERS_JUNCTIONS_AND_CONCAVE:
|
||||
fvar_interpolate_boundary = AbcFaceVaryingInterpolateBoundary::EDGE_AND_CORNERS;
|
||||
propagate_corners = 1;
|
||||
break;
|
||||
case SUBSURF_UV_SMOOTH_PRESERVE_BOUNDARIES:
|
||||
fvar_interpolate_boundary = AbcFaceVaryingInterpolateBoundary::BOUNDARIES;
|
||||
break;
|
||||
case SUBSURF_UV_SMOOTH_ALL:
|
||||
fvar_interpolate_boundary = AbcFaceVaryingInterpolateBoundary::NONE;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (subsurf_data->boundary_smooth) {
|
||||
case SUBSURF_BOUNDARY_SMOOTH_PRESERVE_CORNERS:
|
||||
interpolate_boundary = AbcInterpolateBoundary::EDGE_AND_CORNERS;
|
||||
break;
|
||||
case SUBSURF_BOUNDARY_SMOOTH_ALL:
|
||||
interpolate_boundary = AbcInterpolateBoundary::EDGE_ONLY;
|
||||
break;
|
||||
}
|
||||
|
||||
subdiv_sample.setFaceVaryingInterpolateBoundary(int(fvar_interpolate_boundary));
|
||||
subdiv_sample.setFaceVaryingPropagateCorners(propagate_corners);
|
||||
subdiv_sample.setInterpolateBoundary(int(interpolate_boundary));
|
||||
|
||||
abc_subdiv_viewport_levels_.set(subsurf_data->levels);
|
||||
abc_subdiv_render_levels_.set(subsurf_data->renderLevels);
|
||||
}
|
||||
|
||||
update_bounding_box(context.object);
|
||||
subdiv_sample.setSelfBounds(bounding_box_);
|
||||
abc_subdiv_schema_.set(subdiv_sample);
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ class ABCGenericMeshWriter : public ABCAbstractWriter {
|
|||
Alembic::AbcGeom::OSubD abc_subdiv_;
|
||||
Alembic::AbcGeom::OSubDSchema abc_subdiv_schema_;
|
||||
|
||||
Alembic::AbcGeom::OCompoundProperty abc_custom_data_container_;
|
||||
Alembic::AbcGeom::OInt32Property abc_subdiv_render_levels_;
|
||||
Alembic::AbcGeom::OInt32Property abc_subdiv_viewport_levels_;
|
||||
|
||||
/* Determines whether a poly mesh or a subdivision surface is exported.
|
||||
* The value is set by an export option but only true if there is a subdivision modifier on the
|
||||
* exported object. */
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
#include "BKE_main.hh"
|
||||
#include "BKE_material.hh"
|
||||
#include "BKE_mesh.hh"
|
||||
#include "BKE_modifier.hh"
|
||||
#include "BKE_object.hh"
|
||||
#include "BKE_subdiv.hh"
|
||||
|
||||
|
|
@ -50,6 +51,7 @@ using Alembic::AbcGeom::IC3fGeomParam;
|
|||
using Alembic::AbcGeom::IC4fGeomParam;
|
||||
using Alembic::AbcGeom::IFaceSet;
|
||||
using Alembic::AbcGeom::IFaceSetSchema;
|
||||
using Alembic::AbcGeom::IInt32Property;
|
||||
using Alembic::AbcGeom::IN3fGeomParam;
|
||||
using Alembic::AbcGeom::IObject;
|
||||
using Alembic::AbcGeom::IPolyMesh;
|
||||
|
|
@ -1147,6 +1149,8 @@ void AbcSubDReader::readObjectData(Main *bmain, const Alembic::Abc::ISampleSelec
|
|||
if (m_settings->always_add_cache_reader || has_animations(m_schema, m_settings)) {
|
||||
addCacheModifier();
|
||||
}
|
||||
|
||||
add_subdiv_modifier();
|
||||
}
|
||||
|
||||
bool AbcSubDReader::topology_changed(const Mesh *existing_mesh, const ISampleSelector &sample_sel)
|
||||
|
|
@ -1227,5 +1231,85 @@ void AbcSubDReader::read_geometry(bke::GeometrySet &geometry_set,
|
|||
geometry_set.replace_mesh(new_mesh);
|
||||
}
|
||||
|
||||
void AbcSubDReader::add_subdiv_modifier()
|
||||
{
|
||||
ModifierData *md = BKE_modifier_new(eModifierType_Subsurf);
|
||||
BLI_addtail(&m_object->modifiers, md);
|
||||
BKE_modifiers_persistent_uid_init(*m_object, *md);
|
||||
|
||||
SubsurfModifierData *subdiv_data = reinterpret_cast<SubsurfModifierData *>(md);
|
||||
|
||||
try {
|
||||
/* We only read the first sample as it is unlikely that those value are animated. */
|
||||
ISampleSelector sample_selector = ISampleSelector(Alembic::Abc::index_t(0));
|
||||
ISubDSchema::Sample sample = m_schema.getValue(sample_selector);
|
||||
|
||||
const AbcFaceVaryingInterpolateBoundary fvar_interpolate_boundary =
|
||||
AbcFaceVaryingInterpolateBoundary(sample.getFaceVaryingInterpolateBoundary());
|
||||
|
||||
const AbcInterpolateBoundary interpolate_boundary = AbcInterpolateBoundary(
|
||||
sample.getInterpolateBoundary());
|
||||
|
||||
/* FaceVaryingPropagateCorners should have a value of either 0 (false) or 1 (true). Consider
|
||||
* only values that are greater than 0 as true to allow for bad values, but discard negative
|
||||
* values for the potential set of true values are the value used by Alembic to tell that the
|
||||
* property has not been initialized is negative. */
|
||||
const bool propagate_corners = sample.getFaceVaryingPropagateCorners() > 0;
|
||||
|
||||
/* Confusingly, ALL is NONE and NONE is ALL. */
|
||||
switch (fvar_interpolate_boundary) {
|
||||
case AbcFaceVaryingInterpolateBoundary::ALL:
|
||||
subdiv_data->uv_smooth = SUBSURF_UV_SMOOTH_NONE;
|
||||
break;
|
||||
case AbcFaceVaryingInterpolateBoundary::EDGE_AND_CORNERS: {
|
||||
if (propagate_corners) {
|
||||
subdiv_data->uv_smooth = SUBSURF_UV_SMOOTH_PRESERVE_CORNERS_JUNCTIONS_AND_CONCAVE;
|
||||
}
|
||||
else {
|
||||
subdiv_data->uv_smooth = SUBSURF_UV_SMOOTH_PRESERVE_CORNERS;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AbcFaceVaryingInterpolateBoundary::NONE:
|
||||
subdiv_data->uv_smooth = SUBSURF_UV_SMOOTH_ALL;
|
||||
break;
|
||||
case AbcFaceVaryingInterpolateBoundary::BOUNDARIES:
|
||||
subdiv_data->uv_smooth = SUBSURF_UV_SMOOTH_PRESERVE_BOUNDARIES;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (interpolate_boundary) {
|
||||
case AbcInterpolateBoundary::NONE:
|
||||
/* Blender has no concept for this. */
|
||||
break;
|
||||
case AbcInterpolateBoundary::EDGE_AND_CORNERS:
|
||||
subdiv_data->boundary_smooth = SUBSURF_BOUNDARY_SMOOTH_PRESERVE_CORNERS;
|
||||
break;
|
||||
case AbcInterpolateBoundary::EDGE_ONLY:
|
||||
subdiv_data->boundary_smooth = SUBSURF_BOUNDARY_SMOOTH_ALL;
|
||||
break;
|
||||
}
|
||||
|
||||
ICompoundProperty user_properties = m_schema.getUserProperties();
|
||||
|
||||
if (user_properties.valid() && user_properties.getPropertyHeader("subdivRenderLevels") &&
|
||||
user_properties.getPropertyHeader("subdivViewportLevels"))
|
||||
{
|
||||
IInt32Property render_levels(user_properties, "subdivRenderLevels");
|
||||
IInt32Property viewport_levels(user_properties, "subdivViewportLevels");
|
||||
|
||||
subdiv_data->renderLevels = short(render_levels.getValue(sample_selector));
|
||||
subdiv_data->levels = short(viewport_levels.getValue(sample_selector));
|
||||
}
|
||||
}
|
||||
catch (Alembic::Util::Exception &ex) {
|
||||
CLOG_WARN(&LOG,
|
||||
"Error reading subdivision parameter for '%s/%s' : %s",
|
||||
m_iobject.getFullName().c_str(),
|
||||
m_schema.getName().c_str(),
|
||||
ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace io::alembic
|
||||
} // namespace blender
|
||||
|
|
|
|||
|
|
@ -80,6 +80,8 @@ class AbcSubDReader final : public AbcObjectReader {
|
|||
const Alembic::Abc::ISampleSelector &sample_sel,
|
||||
const AbcReadGeometryParams &read_params,
|
||||
const char **r_err_str);
|
||||
|
||||
void add_subdiv_modifier();
|
||||
};
|
||||
|
||||
void read_mverts(Mesh &mesh,
|
||||
|
|
|
|||
|
|
@ -80,5 +80,31 @@ std::optional<SampleInterpolationSettings> get_sample_interpolation_settings(
|
|||
|
||||
AbcObjectReader *create_reader(const AbcReaderConstructorArgs &args);
|
||||
|
||||
/* Alembic does not have specific enumerations for FaceVaryingInterpolateBoundary and
|
||||
* InterpolateBoundary, rather it uses the values from OpenSubDiv 2.x and RenderMan
|
||||
* directly respectively.
|
||||
*
|
||||
* The identifiers used in the following enumerations are based on their USD equivalent
|
||||
* as used in USD's Alembic writer.
|
||||
*/
|
||||
|
||||
/* See https://opensubdiv.org/docs/compatibility.html for how to convert values from
|
||||
* OpenSubDiv 2.x to 3.x. */
|
||||
enum class AbcFaceVaryingInterpolateBoundary : int {
|
||||
ALL = 0,
|
||||
EDGE_AND_CORNERS = 1,
|
||||
NONE = 2,
|
||||
BOUNDARIES = 3,
|
||||
};
|
||||
|
||||
/* See the RenderMan documentation for those values:
|
||||
* https://rmanwiki-27.pixar.com/space/REN27/542213016/Subdivision+Surfaces#Boundary-Interpolation
|
||||
*/
|
||||
enum class AbcInterpolateBoundary : int {
|
||||
NONE = 0,
|
||||
EDGE_AND_CORNERS = 1,
|
||||
EDGE_ONLY = 2,
|
||||
};
|
||||
|
||||
} // namespace io::alembic
|
||||
} // namespace blender
|
||||
|
|
|
|||
|
|
@ -6,4 +6,6 @@
|
|||
- pos 0.000, 0.000, 0.000
|
||||
- rot 0.000, 0.000, 0.000 (XYZ)
|
||||
- scl 1.000, 1.000, 1.000
|
||||
- 1 modifiers
|
||||
- SUBSURF 'Subdivision' levels:1/2 type:CATMULL_CLARK crease:True
|
||||
|
||||
|
|
|
|||
|
|
@ -26,4 +26,6 @@
|
|||
- pos 0.000, 0.000, 0.000
|
||||
- rot 0.000, 0.000, 0.000 (XYZ)
|
||||
- scl 1.000, 1.000, 1.000
|
||||
- 1 modifiers
|
||||
- SUBSURF 'Subdivision' levels:1/2 type:CATMULL_CLARK crease:True
|
||||
|
||||
|
|
|
|||
3
tests/files/alembic/mesh_subd_params.blend
Normal file
3
tests/files/alembic/mesh_subd_params.blend
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7854b8e39041b7fa750061c02b78d2579b30fa07ac282ae65f989e23b564d030
|
||||
size 105460
|
||||
|
|
@ -305,7 +305,7 @@ class CameraExportImportTest(unittest.TestCase):
|
|||
|
||||
# A MeshSequenceCache modifier should be present on every imported object
|
||||
for blender_mesh in [blender_mesh1, blender_mesh2]:
|
||||
self.assertTrue(len(blender_mesh.modifiers) == 1 and blender_mesh.modifiers[0].type ==
|
||||
self.assertTrue(len(blender_mesh.modifiers) >= 1 and blender_mesh.modifiers[0].type ==
|
||||
'MESH_SEQUENCE_CACHE', f"{blender_mesh.name} has incorrect modifiers")
|
||||
|
||||
# Conversion from USD to Blender convention
|
||||
|
|
@ -320,8 +320,10 @@ class CameraExportImportTest(unittest.TestCase):
|
|||
blender_mesh2_eval = bpy.data.objects["mesh_vert_crease"].evaluated_get(depsgraph)
|
||||
|
||||
# The file was written using a simple formula for each frame's crease value
|
||||
expected_edge_creases = [round(frame / 24.0, 3)] * 12
|
||||
expected_vert_creases = [round(frame / 24.0, 3)] * 4
|
||||
expected_edge_creases = [round(frame / 24.0, 3)] * 48
|
||||
# Creases are not propagated to edges that do not come from a coarse edge
|
||||
expected_edge_creases.extend([0.0] * 144)
|
||||
expected_vert_creases = [round(frame / 24.0, 3)] * 25
|
||||
|
||||
# Check crease values
|
||||
blender_crease_data = [round(d.value, 3) for d in blender_mesh1_eval.data.attributes["crease_edge"].data]
|
||||
|
|
@ -336,6 +338,56 @@ class CameraExportImportTest(unittest.TestCase):
|
|||
expected_vert_creases,
|
||||
f"Frame {frame}: {blender_mesh2_eval.name} crease values do not match")
|
||||
|
||||
def test_mesh_subd_params(self):
|
||||
"""Test export of subdivision parameters."""
|
||||
|
||||
abc_path = str(self.tempdir / "mesh_subd_params.abc")
|
||||
|
||||
# Export
|
||||
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "mesh_subd_params.blend"))
|
||||
self.assertIn('FINISHED', bpy.ops.wm.alembic_export(
|
||||
filepath=abc_path,
|
||||
subdiv_schema=True
|
||||
))
|
||||
|
||||
# Re-import what we just exported into an empty file.
|
||||
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "empty.blend"))
|
||||
bpy.ops.wm.alembic_import(
|
||||
filepath=abc_path,
|
||||
as_background_job=False)
|
||||
|
||||
uv_smooth_options = [
|
||||
'NONE',
|
||||
'PRESERVE_CORNERS',
|
||||
# NOTE: in Blender this should be PRESERVE_CORNERS_AND_JUNCTIONS, however
|
||||
# Alembic does not have this concept.
|
||||
'PRESERVE_CORNERS',
|
||||
'PRESERVE_CORNERS_JUNCTIONS_AND_CONCAVE',
|
||||
'PRESERVE_BOUNDARIES',
|
||||
'SMOOTH_ALL'
|
||||
]
|
||||
|
||||
boundary_smooth_options = [
|
||||
'PRESERVE_CORNERS',
|
||||
'ALL'
|
||||
]
|
||||
|
||||
for uv_smooth_index in range(len(uv_smooth_options)):
|
||||
uv_smooth = uv_smooth_options[uv_smooth_index]
|
||||
|
||||
for boundary_smooth_index in range(len(boundary_smooth_options)):
|
||||
boundary_smooth = boundary_smooth_options[boundary_smooth_index]
|
||||
|
||||
obj_name = 'obj{}{}'.format(uv_smooth_index, boundary_smooth_index)
|
||||
|
||||
obj = bpy.data.objects[obj_name]
|
||||
modifier = obj.modifiers['Subdivision']
|
||||
|
||||
self.assertEqual(modifier.uv_smooth, uv_smooth)
|
||||
self.assertEqual(modifier.boundary_smooth, boundary_smooth)
|
||||
self.assertEqual(modifier.levels, 1 + uv_smooth_index)
|
||||
self.assertEqual(modifier.render_levels, 2 + uv_smooth_index)
|
||||
|
||||
def do_export_import_test(self, *, flatten: bool):
|
||||
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "camera_transforms.blend"))
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue