Suppress exporting this attribute as there's no conversion to USD that supports its data type (2d, 16-bit int) and because mesh normals (from the `corner_normals` API) are already exported. This causes a rather annoying UI Warning notification that the attribute isn't convertible during export. Pull Request: https://projects.blender.org/blender/blender/pulls/135271
902 lines
33 KiB
C++
902 lines
33 KiB
C++
/* SPDX-FileCopyrightText: 2019 Blender Authors
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
#include "usd_writer_mesh.hh"
|
|
|
|
#include "usd_armature_utils.hh"
|
|
#include "usd_attribute_utils.hh"
|
|
#include "usd_blend_shape_utils.hh"
|
|
#include "usd_skel_convert.hh"
|
|
#include "usd_utils.hh"
|
|
|
|
#include <pxr/usd/usdGeom/mesh.h>
|
|
#include <pxr/usd/usdGeom/primvarsAPI.h>
|
|
#include <pxr/usd/usdShade/material.h>
|
|
#include <pxr/usd/usdShade/materialBindingAPI.h>
|
|
#include <pxr/usd/usdSkel/bindingAPI.h>
|
|
|
|
#include "BLI_array_utils.hh"
|
|
#include "BLI_assert.h"
|
|
#include "BLI_math_vector_types.hh"
|
|
|
|
#include "BKE_anonymous_attribute_id.hh"
|
|
#include "BKE_attribute.hh"
|
|
#include "BKE_customdata.hh"
|
|
#include "BKE_lib_id.hh"
|
|
#include "BKE_material.hh"
|
|
#include "BKE_mesh.hh"
|
|
#include "BKE_mesh_wrapper.hh"
|
|
#include "BKE_object.hh"
|
|
#include "BKE_report.hh"
|
|
#include "BKE_subdiv.hh"
|
|
|
|
#include "bmesh.hh"
|
|
#include "bmesh_tools.hh"
|
|
|
|
#include "DEG_depsgraph.hh"
|
|
|
|
#include "DNA_key_types.h"
|
|
#include "DNA_material_types.h"
|
|
#include "DNA_modifier_types.h"
|
|
|
|
#include "CLG_log.h"
|
|
static CLG_LogRef LOG = {"io.usd"};
|
|
|
|
namespace blender::io::usd {
|
|
|
|
USDGenericMeshWriter::USDGenericMeshWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx)
|
|
{
|
|
}
|
|
|
|
bool USDGenericMeshWriter::is_supported(const HierarchyContext *context) const
|
|
{
|
|
if (usd_export_context_.export_params.visible_objects_only) {
|
|
return context->is_object_visible(usd_export_context_.export_params.evaluation_mode);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* Get the last subdiv modifier, regardless of enable/disable status */
|
|
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 = (ModifierData *)(obj->modifiers.last);
|
|
|
|
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;
|
|
}
|
|
|
|
void USDGenericMeshWriter::do_write(HierarchyContext &context)
|
|
{
|
|
Object *object_eval = context.object;
|
|
bool needsfree = false;
|
|
Mesh *mesh = get_export_mesh(object_eval, needsfree);
|
|
|
|
if (mesh == nullptr) {
|
|
return;
|
|
}
|
|
|
|
if (usd_export_context_.export_params.triangulate_meshes) {
|
|
const bool tag_only = false;
|
|
const int quad_method = usd_export_context_.export_params.quad_method;
|
|
const int ngon_method = usd_export_context_.export_params.ngon_method;
|
|
|
|
BMeshCreateParams bmesh_create_params{};
|
|
BMeshFromMeshParams bmesh_from_mesh_params{};
|
|
bmesh_from_mesh_params.calc_face_normal = true;
|
|
bmesh_from_mesh_params.calc_vert_normal = true;
|
|
BMesh *bm = BKE_mesh_to_bmesh_ex(mesh, &bmesh_create_params, &bmesh_from_mesh_params);
|
|
|
|
BM_mesh_triangulate(bm, quad_method, ngon_method, 4, tag_only, nullptr, nullptr, nullptr);
|
|
|
|
Mesh *triangulated_mesh = BKE_mesh_from_bmesh_for_eval_nomain(bm, nullptr, mesh);
|
|
BM_mesh_free(bm);
|
|
|
|
if (needsfree) {
|
|
free_export_mesh(mesh);
|
|
}
|
|
mesh = triangulated_mesh;
|
|
needsfree = true;
|
|
}
|
|
|
|
try {
|
|
/* Fetch the subdiv modifier, if one exists and it is the last modifier. */
|
|
const SubsurfModifierData *subsurfData = get_last_subdiv_modifier(
|
|
usd_export_context_.export_params.evaluation_mode, object_eval);
|
|
|
|
write_mesh(context, mesh, subsurfData);
|
|
|
|
auto prim = usd_export_context_.stage->GetPrimAtPath(usd_export_context_.usd_path);
|
|
if (prim.IsValid() && object_eval) {
|
|
prim.SetActive((object_eval->duplicator_visibility_flag & OB_DUPLI_FLAG_RENDER) != 0);
|
|
write_id_properties(prim, mesh->id, get_export_time_code());
|
|
}
|
|
|
|
if (needsfree) {
|
|
free_export_mesh(mesh);
|
|
}
|
|
}
|
|
catch (...) {
|
|
if (needsfree) {
|
|
free_export_mesh(mesh);
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
void USDGenericMeshWriter::write_custom_data(const Object *obj,
|
|
const Mesh *mesh,
|
|
const pxr::UsdGeomMesh &usd_mesh)
|
|
{
|
|
const bke::AttributeAccessor attributes = mesh->attributes();
|
|
|
|
const StringRef active_uvmap_name = CustomData_get_render_layer_name(&mesh->corner_data,
|
|
CD_PROP_FLOAT2);
|
|
|
|
attributes.foreach_attribute([&](const bke::AttributeIter &iter) {
|
|
/* Skip "internal" Blender properties and attributes processed elsewhere.
|
|
* Skip edge domain because USD doesn't have a good conversion for them. */
|
|
if (iter.name[0] == '.' || bke::attribute_name_is_anonymous(iter.name) ||
|
|
iter.domain == bke::AttrDomain::Edge ||
|
|
ELEM(iter.name, "position", "material_index", "velocity", "crease_vert", "custom_normal"))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ((usd_export_context_.export_params.export_armatures ||
|
|
usd_export_context_.export_params.export_shapekeys) &&
|
|
iter.name.rfind("skel:") == 0)
|
|
{
|
|
/* If we're exporting armatures or shape keys to UsdSkel, we skip any
|
|
* attributes that have names with the "skel:" namespace, to avoid possible
|
|
* conflicts. Such attribute might have been previously imported into Blender
|
|
* from USD, but can no longer be considered valid. */
|
|
return;
|
|
}
|
|
|
|
if (usd_export_context_.export_params.export_armatures &&
|
|
is_armature_modifier_bone_name(*obj, iter.name, usd_export_context_.depsgraph))
|
|
{
|
|
/* This attribute is likely a vertex group for the armature modifier,
|
|
* and it may conflict with skinning data that will be written to
|
|
* the USD mesh, so we skip it. Such vertex groups will instead be
|
|
* handled in #export_deform_verts(). */
|
|
return;
|
|
}
|
|
|
|
/* UV Data. */
|
|
if (iter.domain == bke::AttrDomain::Corner && iter.data_type == CD_PROP_FLOAT2) {
|
|
if (usd_export_context_.export_params.export_uvmaps) {
|
|
this->write_uv_data(usd_mesh, iter, active_uvmap_name);
|
|
}
|
|
}
|
|
|
|
else {
|
|
this->write_generic_data(mesh, usd_mesh, iter);
|
|
}
|
|
});
|
|
}
|
|
|
|
static std::optional<pxr::TfToken> convert_blender_domain_to_usd(
|
|
const bke::AttrDomain blender_domain)
|
|
{
|
|
switch (blender_domain) {
|
|
case bke::AttrDomain::Corner:
|
|
return pxr::UsdGeomTokens->faceVarying;
|
|
case bke::AttrDomain::Point:
|
|
return pxr::UsdGeomTokens->vertex;
|
|
case bke::AttrDomain::Face:
|
|
return pxr::UsdGeomTokens->uniform;
|
|
|
|
/* Notice: Edge types are not supported in USD! */
|
|
default:
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
void USDGenericMeshWriter::write_generic_data(const Mesh *mesh,
|
|
const pxr::UsdGeomMesh &usd_mesh,
|
|
const bke::AttributeIter &attr)
|
|
{
|
|
const pxr::TfToken pv_name(
|
|
make_safe_name(attr.name, usd_export_context_.export_params.allow_unicode));
|
|
const bool use_color3f_type = pv_name == usdtokens::displayColor;
|
|
const std::optional<pxr::TfToken> pv_interp = convert_blender_domain_to_usd(attr.domain);
|
|
const std::optional<pxr::SdfValueTypeName> pv_type = convert_blender_type_to_usd(
|
|
attr.data_type, use_color3f_type);
|
|
|
|
if (!pv_interp || !pv_type) {
|
|
BKE_reportf(reports(),
|
|
RPT_WARNING,
|
|
"Mesh '%s', Attribute '%s' (domain %d, type %d) cannot be converted to USD",
|
|
BKE_id_name(mesh->id),
|
|
attr.name.c_str(),
|
|
int8_t(attr.domain),
|
|
attr.data_type);
|
|
return;
|
|
}
|
|
|
|
const GVArray attribute = *attr.get();
|
|
if (attribute.is_empty()) {
|
|
return;
|
|
}
|
|
|
|
const pxr::UsdTimeCode timecode = get_export_time_code();
|
|
const pxr::UsdGeomPrimvarsAPI pv_api = pxr::UsdGeomPrimvarsAPI(usd_mesh);
|
|
|
|
pxr::UsdGeomPrimvar pv_attr = pv_api.CreatePrimvar(pv_name, *pv_type, *pv_interp);
|
|
|
|
copy_blender_attribute_to_primvar(
|
|
attribute, attr.data_type, timecode, pv_attr, usd_value_writer_);
|
|
}
|
|
|
|
void USDGenericMeshWriter::write_uv_data(const pxr::UsdGeomMesh &usd_mesh,
|
|
const bke::AttributeIter &attr,
|
|
const StringRef active_uvmap_name)
|
|
{
|
|
const VArray<float2> buffer = *attr.get<float2>(bke::AttrDomain::Corner);
|
|
if (buffer.is_empty()) {
|
|
return;
|
|
}
|
|
|
|
/* Optionally rename active UV map to "st", to follow USD conventions
|
|
* and better work with MaterialX shader nodes. */
|
|
const StringRef name = usd_export_context_.export_params.rename_uvmaps &&
|
|
active_uvmap_name == attr.name ?
|
|
"st" :
|
|
attr.name;
|
|
|
|
const pxr::UsdTimeCode timecode = get_export_time_code();
|
|
const pxr::TfToken pv_name(
|
|
make_safe_name(name, usd_export_context_.export_params.allow_unicode));
|
|
const pxr::UsdGeomPrimvarsAPI pv_api = pxr::UsdGeomPrimvarsAPI(usd_mesh);
|
|
|
|
pxr::UsdGeomPrimvar pv_uv = pv_api.CreatePrimvar(
|
|
pv_name, pxr::SdfValueTypeNames->TexCoord2fArray, pxr::UsdGeomTokens->faceVarying);
|
|
|
|
copy_blender_buffer_to_primvar<float2, pxr::GfVec2f>(buffer, timecode, pv_uv, usd_value_writer_);
|
|
}
|
|
|
|
void USDGenericMeshWriter::free_export_mesh(Mesh *mesh)
|
|
{
|
|
BKE_id_free(nullptr, mesh);
|
|
}
|
|
|
|
struct USDMeshData {
|
|
pxr::VtArray<pxr::GfVec3f> points;
|
|
pxr::VtIntArray face_vertex_counts;
|
|
pxr::VtIntArray face_indices;
|
|
MaterialFaceGroups 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 sharpness's for all edges making up the creases on a mesh. */
|
|
pxr::VtFloatArray crease_sharpnesses;
|
|
|
|
/* The lengths of this array specifies the number of sharp corners (or vertex crease) on the
|
|
* surface. Each value is the index of a vertex in the mesh's vertex list. */
|
|
pxr::VtIntArray corner_indices;
|
|
/* The per-vertex sharpnesses. The lengths of this array must match that of `corner_indices`. */
|
|
pxr::VtFloatArray corner_sharpnesses;
|
|
};
|
|
|
|
void USDGenericMeshWriter::write_mesh(HierarchyContext &context,
|
|
Mesh *mesh,
|
|
const SubsurfModifierData *subsurfData)
|
|
{
|
|
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);
|
|
write_visibility(context, timecode, usd_mesh);
|
|
|
|
USDMeshData usd_mesh_data;
|
|
/* Ensure data exists if currently in edit mode. */
|
|
BKE_mesh_wrapper_ensure_mdata(mesh);
|
|
get_geometry_data(mesh, usd_mesh_data);
|
|
|
|
pxr::UsdAttribute attr_points = usd_mesh.CreatePointsAttr(pxr::VtValue(), true);
|
|
pxr::UsdAttribute attr_face_vertex_counts = usd_mesh.CreateFaceVertexCountsAttr(pxr::VtValue(),
|
|
true);
|
|
pxr::UsdAttribute attr_face_vertex_indices = usd_mesh.CreateFaceVertexIndicesAttr(pxr::VtValue(),
|
|
true);
|
|
|
|
if (!attr_points.HasValue()) {
|
|
/* Provide the initial value as default. This makes USD write the value as constant if they
|
|
* don't change over time. */
|
|
attr_points.Set(usd_mesh_data.points, pxr::UsdTimeCode::Default());
|
|
attr_face_vertex_counts.Set(usd_mesh_data.face_vertex_counts, pxr::UsdTimeCode::Default());
|
|
attr_face_vertex_indices.Set(usd_mesh_data.face_indices, pxr::UsdTimeCode::Default());
|
|
}
|
|
|
|
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::VtValue(), true);
|
|
pxr::UsdAttribute attr_crease_indices = usd_mesh.CreateCreaseIndicesAttr(pxr::VtValue(), true);
|
|
pxr::UsdAttribute attr_crease_sharpness = usd_mesh.CreateCreaseSharpnessesAttr(pxr::VtValue(),
|
|
true);
|
|
|
|
if (!attr_crease_lengths.HasValue()) {
|
|
attr_crease_lengths.Set(usd_mesh_data.crease_lengths, pxr::UsdTimeCode::Default());
|
|
attr_crease_indices.Set(usd_mesh_data.crease_vertex_indices, pxr::UsdTimeCode::Default());
|
|
attr_crease_sharpness.Set(usd_mesh_data.crease_sharpnesses, pxr::UsdTimeCode::Default());
|
|
}
|
|
|
|
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_mesh_data.corner_indices.empty() &&
|
|
usd_mesh_data.corner_indices.size() == usd_mesh_data.corner_sharpnesses.size())
|
|
{
|
|
pxr::UsdAttribute attr_corner_indices = usd_mesh.CreateCornerIndicesAttr(pxr::VtValue(), true);
|
|
pxr::UsdAttribute attr_corner_sharpnesses = usd_mesh.CreateCornerSharpnessesAttr(
|
|
pxr::VtValue(), true);
|
|
|
|
if (!attr_corner_indices.HasValue()) {
|
|
attr_corner_indices.Set(usd_mesh_data.corner_indices, pxr::UsdTimeCode::Default());
|
|
attr_corner_sharpnesses.Set(usd_mesh_data.corner_sharpnesses, pxr::UsdTimeCode::Default());
|
|
}
|
|
|
|
usd_value_writer_.SetAttribute(
|
|
attr_corner_indices, pxr::VtValue(usd_mesh_data.corner_indices), timecode);
|
|
usd_value_writer_.SetAttribute(
|
|
attr_corner_sharpnesses, pxr::VtValue(usd_mesh_data.corner_sharpnesses), timecode);
|
|
}
|
|
|
|
write_custom_data(context.object, mesh, usd_mesh);
|
|
write_surface_velocity(mesh, usd_mesh);
|
|
|
|
const pxr::TfToken subdiv_scheme = get_subdiv_scheme(subsurfData);
|
|
|
|
/* Normals can be animated, so ensure these are written for each frame,
|
|
* unless a subdiv modifier is used, in which case normals are computed,
|
|
* not stored with the mesh. */
|
|
if (usd_export_context_.export_params.export_normals &&
|
|
subdiv_scheme == pxr::UsdGeomTokens->none)
|
|
{
|
|
write_normals(mesh, usd_mesh);
|
|
}
|
|
|
|
this->author_extent(usd_mesh, mesh->bounds_min_max(), timecode);
|
|
|
|
/* TODO(Sybren): figure out what happens when the face groups change. */
|
|
if (frame_has_been_written_) {
|
|
return;
|
|
}
|
|
|
|
/* The subdivision scheme is a uniform according to spec,
|
|
* so this value cannot be animated. */
|
|
write_subdiv(subdiv_scheme, usd_mesh, subsurfData);
|
|
|
|
if (usd_export_context_.export_params.export_materials) {
|
|
assign_materials(context, usd_mesh, usd_mesh_data.face_groups);
|
|
}
|
|
}
|
|
|
|
pxr::TfToken USDGenericMeshWriter::get_subdiv_scheme(const SubsurfModifierData *subsurfData)
|
|
{
|
|
/* Default to setting the subdivision scheme to None. */
|
|
pxr::TfToken subdiv_scheme = pxr::UsdGeomTokens->none;
|
|
|
|
if (subsurfData) {
|
|
if (subsurfData->subdivType == SUBSURF_TYPE_CATMULL_CLARK) {
|
|
if (usd_export_context_.export_params.export_subdiv == USD_SUBDIV_BEST_MATCH) {
|
|
/* If a subdivision modifier exists, and it uses Catmull-Clark, then apply Catmull-Clark
|
|
* SubD scheme. */
|
|
subdiv_scheme = pxr::UsdGeomTokens->catmullClark;
|
|
}
|
|
}
|
|
else {
|
|
/* "Simple" is currently the only other subdivision type provided by Blender, */
|
|
/* and we do not yet provide a corresponding representation for USD export. */
|
|
BKE_reportf(reports(),
|
|
RPT_WARNING,
|
|
"USD export: Simple subdivision not supported, exporting subdivided mesh");
|
|
}
|
|
}
|
|
|
|
return subdiv_scheme;
|
|
}
|
|
|
|
void USDGenericMeshWriter::write_subdiv(const pxr::TfToken &subdiv_scheme,
|
|
const pxr::UsdGeomMesh &usd_mesh,
|
|
const SubsurfModifierData *subsurfData)
|
|
{
|
|
usd_mesh.CreateSubdivisionSchemeAttr().Set(subdiv_scheme);
|
|
if (subdiv_scheme == pxr::UsdGeomTokens->catmullClark) {
|
|
/* For Catmull-Clark, also consider the various interpolation modes. */
|
|
/* For reference, see
|
|
* https://graphics.pixar.com/opensubdiv/docs/subdivision_surfaces.html#face-varying-interpolation-rules
|
|
*/
|
|
switch (subsurfData->uv_smooth) {
|
|
case SUBSURF_UV_SMOOTH_NONE:
|
|
usd_mesh.CreateFaceVaryingLinearInterpolationAttr().Set(pxr::UsdGeomTokens->all);
|
|
break;
|
|
case SUBSURF_UV_SMOOTH_PRESERVE_CORNERS:
|
|
usd_mesh.CreateFaceVaryingLinearInterpolationAttr().Set(pxr::UsdGeomTokens->cornersOnly);
|
|
break;
|
|
case SUBSURF_UV_SMOOTH_PRESERVE_CORNERS_AND_JUNCTIONS:
|
|
usd_mesh.CreateFaceVaryingLinearInterpolationAttr().Set(pxr::UsdGeomTokens->cornersPlus1);
|
|
break;
|
|
case SUBSURF_UV_SMOOTH_PRESERVE_CORNERS_JUNCTIONS_AND_CONCAVE:
|
|
usd_mesh.CreateFaceVaryingLinearInterpolationAttr().Set(pxr::UsdGeomTokens->cornersPlus2);
|
|
break;
|
|
case SUBSURF_UV_SMOOTH_PRESERVE_BOUNDARIES:
|
|
usd_mesh.CreateFaceVaryingLinearInterpolationAttr().Set(pxr::UsdGeomTokens->boundaries);
|
|
break;
|
|
case SUBSURF_UV_SMOOTH_ALL:
|
|
usd_mesh.CreateFaceVaryingLinearInterpolationAttr().Set(pxr::UsdGeomTokens->none);
|
|
break;
|
|
default:
|
|
BLI_assert_msg(0, "Unsupported UV smoothing mode.");
|
|
}
|
|
|
|
/* For reference, see
|
|
* https://graphics.pixar.com/opensubdiv/docs/subdivision_surfaces.html#boundary-interpolation-rules
|
|
*/
|
|
switch (subsurfData->boundary_smooth) {
|
|
case SUBSURF_BOUNDARY_SMOOTH_ALL:
|
|
usd_mesh.CreateInterpolateBoundaryAttr().Set(pxr::UsdGeomTokens->edgeOnly);
|
|
break;
|
|
case SUBSURF_BOUNDARY_SMOOTH_PRESERVE_CORNERS:
|
|
usd_mesh.CreateInterpolateBoundaryAttr().Set(pxr::UsdGeomTokens->edgeAndCorner);
|
|
break;
|
|
default:
|
|
BLI_assert_msg(0, "Unsupported boundary smoothing mode.");
|
|
}
|
|
}
|
|
}
|
|
|
|
static void get_positions(const Mesh *mesh, USDMeshData &usd_mesh_data)
|
|
{
|
|
const Span<pxr::GfVec3f> positions = mesh->vert_positions().cast<pxr::GfVec3f>();
|
|
usd_mesh_data.points = pxr::VtArray<pxr::GfVec3f>(positions.begin(), positions.end());
|
|
}
|
|
|
|
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. */
|
|
const bke::AttributeAccessor attributes = mesh->attributes();
|
|
const VArray<int> material_indices = *attributes.lookup_or_default<int>(
|
|
"material_index", bke::AttrDomain::Face, 0);
|
|
if (!material_indices.is_single() && mesh->totcol > 1) {
|
|
const VArraySpan<int> indices_span(material_indices);
|
|
for (const int i : indices_span.index_range()) {
|
|
usd_mesh_data.face_groups.lookup_or_add_default(indices_span[i]).push_back(i);
|
|
}
|
|
}
|
|
|
|
usd_mesh_data.face_vertex_counts.resize(mesh->faces_num);
|
|
const OffsetIndices faces = mesh->faces();
|
|
offset_indices::copy_group_sizes(
|
|
faces,
|
|
faces.index_range(),
|
|
MutableSpan(usd_mesh_data.face_vertex_counts.data(), mesh->faces_num));
|
|
|
|
const Span<int> corner_verts = mesh->corner_verts();
|
|
usd_mesh_data.face_indices = pxr::VtIntArray(corner_verts.begin(), corner_verts.end());
|
|
}
|
|
|
|
static void get_edge_creases(const Mesh *mesh, USDMeshData &usd_mesh_data)
|
|
{
|
|
const bke::AttributeAccessor attributes = mesh->attributes();
|
|
const bke::AttributeReader attribute = attributes.lookup<float>("crease_edge",
|
|
bke::AttrDomain::Edge);
|
|
if (!attribute) {
|
|
return;
|
|
}
|
|
const VArraySpan creases(*attribute);
|
|
const Span<int2> edges = mesh->edges();
|
|
for (const int i : edges.index_range()) {
|
|
const float crease = std::clamp(creases[i], 0.0f, 1.0f);
|
|
|
|
if (crease != 0.0f) {
|
|
usd_mesh_data.crease_vertex_indices.push_back(edges[i][0]);
|
|
usd_mesh_data.crease_vertex_indices.push_back(edges[i][1]);
|
|
usd_mesh_data.crease_lengths.push_back(2);
|
|
usd_mesh_data.crease_sharpnesses.push_back(bke::subdiv::crease_to_sharpness(crease));
|
|
}
|
|
}
|
|
}
|
|
|
|
static void get_vert_creases(const Mesh *mesh, USDMeshData &usd_mesh_data)
|
|
{
|
|
const bke::AttributeAccessor attributes = mesh->attributes();
|
|
const bke::AttributeReader attribute = attributes.lookup<float>("crease_vert",
|
|
bke::AttrDomain::Point);
|
|
if (!attribute) {
|
|
return;
|
|
}
|
|
const VArraySpan creases(*attribute);
|
|
for (const int i : creases.index_range()) {
|
|
const float crease = std::clamp(creases[i], 0.0f, 1.0f);
|
|
|
|
if (crease != 0.0f) {
|
|
usd_mesh_data.corner_indices.push_back(i);
|
|
usd_mesh_data.corner_sharpnesses.push_back(bke::subdiv::crease_to_sharpness(crease));
|
|
}
|
|
}
|
|
}
|
|
|
|
void USDGenericMeshWriter::get_geometry_data(const Mesh *mesh, USDMeshData &usd_mesh_data)
|
|
{
|
|
get_positions(mesh, usd_mesh_data);
|
|
get_loops_polys(mesh, usd_mesh_data);
|
|
get_edge_creases(mesh, usd_mesh_data);
|
|
get_vert_creases(mesh, usd_mesh_data);
|
|
}
|
|
|
|
void USDGenericMeshWriter::assign_materials(const HierarchyContext &context,
|
|
const 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;
|
|
auto mesh_prim = usd_mesh.GetPrim();
|
|
pxr::UsdShadeMaterialBindingAPI material_binding_api(mesh_prim);
|
|
for (int mat_num = 0; mat_num < context.object->totcol; mat_num++) {
|
|
Material *material = BKE_object_material_get(context.object, mat_num + 1);
|
|
if (material == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
pxr::UsdShadeMaterial usd_material = ensure_usd_material(context, material);
|
|
material_binding_api.Bind(usd_material);
|
|
|
|
/* 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) {
|
|
/* USD will require that prims with material bindings have the #MaterialBindingAPI applied
|
|
* schema. While Bind() above will create the binding attribute, Apply() needs to be called as
|
|
* well to add the #MaterialBindingAPI schema to the prim itself. */
|
|
pxr::UsdShadeMaterialBindingAPI::Apply(mesh_prim);
|
|
}
|
|
else {
|
|
/* 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::Item &face_group : usd_face_groups.items()) {
|
|
short material_number = face_group.key;
|
|
const pxr::VtIntArray &face_indices = face_group.value;
|
|
|
|
Material *material = BKE_object_material_get(context.object, material_number + 1);
|
|
if (material == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
pxr::UsdShadeMaterial usd_material = ensure_usd_material(context, material);
|
|
pxr::TfToken material_name = usd_material.GetPath().GetNameToken();
|
|
|
|
pxr::UsdGeomSubset usd_face_subset = material_binding_api.CreateMaterialBindSubset(
|
|
material_name, face_indices);
|
|
auto subset_prim = usd_face_subset.GetPrim();
|
|
auto subset_material_api = pxr::UsdShadeMaterialBindingAPI(subset_prim);
|
|
subset_material_api.Bind(usd_material);
|
|
/* Apply the #MaterialBindingAPI applied schema, as required by USD. */
|
|
pxr::UsdShadeMaterialBindingAPI::Apply(subset_prim);
|
|
}
|
|
}
|
|
|
|
void USDGenericMeshWriter::write_normals(const Mesh *mesh, pxr::UsdGeomMesh &usd_mesh)
|
|
{
|
|
pxr::UsdTimeCode timecode = get_export_time_code();
|
|
|
|
pxr::VtVec3fArray loop_normals;
|
|
loop_normals.resize(mesh->corners_num);
|
|
|
|
MutableSpan dst_normals(reinterpret_cast<float3 *>(loop_normals.data()), loop_normals.size());
|
|
|
|
switch (mesh->normals_domain()) {
|
|
case bke::MeshNormalDomain::Point: {
|
|
array_utils::gather(mesh->vert_normals(), mesh->corner_verts(), dst_normals);
|
|
break;
|
|
}
|
|
case bke::MeshNormalDomain::Face: {
|
|
const OffsetIndices faces = mesh->faces();
|
|
const Span<float3> face_normals = mesh->face_normals();
|
|
for (const int i : faces.index_range()) {
|
|
dst_normals.slice(faces[i]).fill(face_normals[i]);
|
|
}
|
|
break;
|
|
}
|
|
case bke::MeshNormalDomain::Corner: {
|
|
array_utils::copy(mesh->corner_normals(), dst_normals);
|
|
break;
|
|
}
|
|
}
|
|
|
|
pxr::UsdAttribute attr_normals = usd_mesh.CreateNormalsAttr(pxr::VtValue(), true);
|
|
if (!attr_normals.HasValue()) {
|
|
attr_normals.Set(loop_normals, pxr::UsdTimeCode::Default());
|
|
}
|
|
usd_value_writer_.SetAttribute(attr_normals, pxr::VtValue(loop_normals), timecode);
|
|
usd_mesh.SetNormalsInterpolation(pxr::UsdGeomTokens->faceVarying);
|
|
}
|
|
|
|
void USDGenericMeshWriter::write_surface_velocity(const Mesh *mesh,
|
|
const pxr::UsdGeomMesh &usd_mesh)
|
|
{
|
|
/* Export velocity attribute output by fluid sim, sequence cache modifier
|
|
* and geometry nodes. */
|
|
const VArraySpan velocity = *mesh->attributes().lookup<float3>("velocity",
|
|
blender::bke::AttrDomain::Point);
|
|
if (velocity.is_empty()) {
|
|
return;
|
|
}
|
|
|
|
/* Export per-vertex velocity vectors. */
|
|
Span<pxr::GfVec3f> data = velocity.cast<pxr::GfVec3f>();
|
|
pxr::VtVec3fArray usd_velocities;
|
|
usd_velocities.assign(data.begin(), data.end());
|
|
|
|
pxr::UsdTimeCode timecode = get_export_time_code();
|
|
pxr::UsdAttribute attr_vel = usd_mesh.CreateVelocitiesAttr(pxr::VtValue(), true);
|
|
if (!attr_vel.HasValue()) {
|
|
attr_vel.Set(usd_velocities, pxr::UsdTimeCode::Default());
|
|
}
|
|
|
|
usd_value_writer_.SetAttribute(attr_vel, usd_velocities, timecode);
|
|
}
|
|
|
|
USDMeshWriter::USDMeshWriter(const USDExporterContext &ctx)
|
|
: USDGenericMeshWriter(ctx), write_skinned_mesh_(false), write_blend_shapes_(false)
|
|
{
|
|
}
|
|
|
|
void USDMeshWriter::set_skel_export_flags(const HierarchyContext &context)
|
|
{
|
|
write_skinned_mesh_ = false;
|
|
write_blend_shapes_ = false;
|
|
|
|
const USDExportParams ¶ms = usd_export_context_.export_params;
|
|
|
|
/* We can write a skinned mesh if exporting armatures is enabled and the object has an armature
|
|
* modifier. */
|
|
write_skinned_mesh_ = params.export_armatures &&
|
|
can_export_skinned_mesh(*context.object, usd_export_context_.depsgraph);
|
|
|
|
/* We can write blend shapes if exporting shape keys is enabled and the object has shape keys. */
|
|
write_blend_shapes_ = params.export_shapekeys && is_mesh_with_shape_keys(context.object);
|
|
}
|
|
|
|
void USDMeshWriter::init_skinned_mesh(const HierarchyContext &context)
|
|
{
|
|
pxr::UsdStageRefPtr stage = usd_export_context_.stage;
|
|
|
|
pxr::UsdPrim mesh_prim = stage->GetPrimAtPath(usd_export_context_.usd_path);
|
|
|
|
if (!mesh_prim.IsValid()) {
|
|
CLOG_WARN(&LOG,
|
|
"%s: couldn't get valid mesh prim for mesh %s",
|
|
__func__,
|
|
usd_export_context_.usd_path.GetAsString().c_str());
|
|
return;
|
|
}
|
|
|
|
pxr::UsdSkelBindingAPI skel_api = pxr::UsdSkelBindingAPI::Apply(mesh_prim);
|
|
|
|
if (!skel_api) {
|
|
CLOG_WARN(&LOG,
|
|
"Couldn't apply UsdSkelBindingAPI to mesh prim %s",
|
|
usd_export_context_.usd_path.GetAsString().c_str());
|
|
return;
|
|
}
|
|
|
|
const Object *arm_obj = get_armature_modifier_obj(*context.object,
|
|
usd_export_context_.depsgraph);
|
|
|
|
if (!arm_obj) {
|
|
CLOG_WARN(&LOG,
|
|
"Couldn't get armature modifier object for skinned mesh %s",
|
|
usd_export_context_.usd_path.GetAsString().c_str());
|
|
return;
|
|
}
|
|
|
|
Vector<std::string> bone_names;
|
|
get_armature_bone_names(
|
|
arm_obj, usd_export_context_.export_params.only_deform_bones, bone_names);
|
|
|
|
if (bone_names.is_empty()) {
|
|
CLOG_WARN(&LOG,
|
|
"No armature bones for skinned mesh %s",
|
|
usd_export_context_.usd_path.GetAsString().c_str());
|
|
return;
|
|
}
|
|
|
|
bool needsfree = false;
|
|
Mesh *mesh = get_export_mesh(context.object, needsfree);
|
|
|
|
if (mesh == nullptr) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
export_deform_verts(mesh, skel_api, bone_names);
|
|
|
|
if (needsfree) {
|
|
free_export_mesh(mesh);
|
|
}
|
|
}
|
|
catch (...) {
|
|
if (needsfree) {
|
|
free_export_mesh(mesh);
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
void USDMeshWriter::init_blend_shapes(const HierarchyContext &context)
|
|
{
|
|
pxr::UsdStageRefPtr stage = usd_export_context_.stage;
|
|
|
|
pxr::UsdPrim mesh_prim = stage->GetPrimAtPath(usd_export_context_.usd_path);
|
|
|
|
if (!mesh_prim.IsValid()) {
|
|
CLOG_WARN(&LOG,
|
|
"Couldn't get valid mesh prim for mesh %s",
|
|
mesh_prim.GetPath().GetAsString().c_str());
|
|
return;
|
|
}
|
|
|
|
create_blend_shapes(this->usd_export_context_.stage,
|
|
context.object,
|
|
mesh_prim,
|
|
usd_export_context_.export_params.allow_unicode);
|
|
}
|
|
|
|
void USDMeshWriter::do_write(HierarchyContext &context)
|
|
{
|
|
set_skel_export_flags(context);
|
|
|
|
if (frame_has_been_written_ && (write_skinned_mesh_ || write_blend_shapes_)) {
|
|
/* When writing skinned meshes or blend shapes, we only write the rest mesh once,
|
|
* so we return early after the first frame has been written. However, we still
|
|
* update blend shape weights if needed. */
|
|
if (write_blend_shapes_) {
|
|
add_shape_key_weights_sample(context.object);
|
|
}
|
|
return;
|
|
}
|
|
|
|
USDGenericMeshWriter::do_write(context);
|
|
|
|
if (write_skinned_mesh_) {
|
|
init_skinned_mesh(context);
|
|
}
|
|
|
|
if (write_blend_shapes_) {
|
|
init_blend_shapes(context);
|
|
add_shape_key_weights_sample(context.object);
|
|
}
|
|
}
|
|
|
|
Mesh *USDMeshWriter::get_export_mesh(Object *object_eval, bool &r_needsfree)
|
|
{
|
|
if (write_blend_shapes_) {
|
|
r_needsfree = true;
|
|
/* We return the pre-modified mesh with the verts in the shape key
|
|
* basis positions. */
|
|
return get_shape_key_basis_mesh(object_eval);
|
|
}
|
|
|
|
if (write_skinned_mesh_) {
|
|
r_needsfree = false;
|
|
/* We must export the skinned mesh in its rest pose. We therefore
|
|
* return the pre-modified mesh, so that the armature modifier isn't
|
|
* applied. */
|
|
/* TODO: Store the "needs free" mesh in a separate variable. */
|
|
return const_cast<Mesh *>(BKE_object_get_pre_modified_mesh(object_eval));
|
|
}
|
|
|
|
/* Return the fully evaluated mesh. */
|
|
r_needsfree = false;
|
|
return BKE_object_get_evaluated_mesh(object_eval);
|
|
}
|
|
|
|
void USDMeshWriter::add_shape_key_weights_sample(const Object *obj)
|
|
{
|
|
if (!obj) {
|
|
return;
|
|
}
|
|
|
|
const Key *key = get_mesh_shape_key(obj);
|
|
if (!key) {
|
|
return;
|
|
}
|
|
|
|
pxr::UsdStageRefPtr stage = usd_export_context_.stage;
|
|
|
|
pxr::UsdPrim mesh_prim = stage->GetPrimAtPath(usd_export_context_.usd_path);
|
|
|
|
if (!mesh_prim.IsValid()) {
|
|
CLOG_WARN(&LOG,
|
|
"Couldn't get valid mesh prim for mesh %s",
|
|
usd_export_context_.usd_path.GetAsString().c_str());
|
|
return;
|
|
}
|
|
|
|
pxr::VtFloatArray weights = get_blendshape_weights(key);
|
|
pxr::UsdTimeCode timecode = get_export_time_code();
|
|
|
|
/* Save the weights samples to a temporary privar which will be copied to
|
|
* a skeleton animation later. */
|
|
pxr::UsdAttribute temp_weights_attr = pxr::UsdGeomPrimvarsAPI(mesh_prim).CreatePrimvar(
|
|
TempBlendShapeWeightsPrimvarName, pxr::SdfValueTypeNames->FloatArray);
|
|
|
|
if (!temp_weights_attr) {
|
|
CLOG_WARN(&LOG,
|
|
"Couldn't create primvar %s on prim %s",
|
|
TempBlendShapeWeightsPrimvarName.GetText(),
|
|
mesh_prim.GetPath().GetAsString().c_str());
|
|
return;
|
|
}
|
|
|
|
temp_weights_attr.Set(weights, timecode);
|
|
}
|
|
|
|
} // namespace blender::io::usd
|