USD IO: Move to the new Mesh Attributes API for Colors #105347

Merged
Sybren A. Stüvel merged 11 commits from CharlesWardlaw/blender:feature/usd_colors into main 2023-04-14 11:05:38 +02:00
5 changed files with 257 additions and 70 deletions

View File

@ -326,7 +326,7 @@ void WM_OT_usd_export(struct wmOperatorType *ot)
"overwrite_textures",
false,
"Overwrite Textures",
"Allow overwriting existing texture files when exporting textures");
"Overwrite existing files when exporting textures");
RNA_def_boolean(ot->srna,
"relative_paths",
@ -612,7 +612,7 @@ void WM_OT_usd_import(struct wmOperatorType *ot)
RNA_def_boolean(ot->srna, "read_mesh_uvs", true, "UV Coordinates", "Read mesh UV coordinates");
RNA_def_boolean(
ot->srna, "read_mesh_colors", false, "Color Attributes", "Read mesh color attributes");
ot->srna, "read_mesh_colors", true, "Color Attributes", "Read mesh color attributes");
RNA_def_string(ot->srna,
"prim_path_mask",

View File

@ -25,6 +25,9 @@
#include "DNA_meshdata_types.h"
#include "DNA_modifier_types.h"
#include "DNA_object_types.h"
#include "DNA_windowmanager_types.h"
#include "WM_api.h"
#include "MEM_guardedalloc.h"
@ -408,102 +411,191 @@ void USDMeshReader::read_uvs(Mesh *mesh, const double motionSampleTime, const bo
}
}
void USDMeshReader::read_colors(Mesh *mesh, const double motionSampleTime)
void USDMeshReader::read_color_data_all_primvars(Mesh *mesh, const double motionSampleTime)
{
if (!(mesh && mesh_prim_ && mesh->totloop > 0)) {
return;
}
/* Early out if we read the display color before and if this attribute isn't animated. */
if (primvar_varying_map_.find(usdtokens::displayColor) != primvar_varying_map_.end() &&
!primvar_varying_map_.at(usdtokens::displayColor)) {
pxr::UsdGeomPrimvarsAPI pv_api = pxr::UsdGeomPrimvarsAPI(mesh_prim_);
std::vector<pxr::UsdGeomPrimvar> primvars = pv_api.GetPrimvarsWithValues();
pxr::TfToken active_color_name;
/* Convert color primvars to custom layer data. */
for (pxr::UsdGeomPrimvar &pv : primvars) {
if (!pv.HasValue()) {
continue;
}
pxr::SdfValueTypeName type = pv.GetTypeName();
if (!ELEM(type,
pxr::SdfValueTypeNames->Color3hArray,
pxr::SdfValueTypeNames->Color3fArray,
pxr::SdfValueTypeNames->Color3dArray)) {
continue;
}
pxr::TfToken name = pv.GetPrimvarName();
/* Set the active color name to 'displayColor', if a color primvar
* with this name exists. Otherwise, use the name of the first
* color primvar we find for the active color. */
if (active_color_name.IsEmpty() || name == usdtokens::displayColor) {
active_color_name = name;
}
/* Skip if we read this primvar before and it isn't animated. */
const std::map<const pxr::TfToken, bool>::const_iterator is_animated_iter =
primvar_varying_map_.find(name);
if (is_animated_iter != primvar_varying_map_.end() && !is_animated_iter->second) {
continue;
}
read_color_data_primvar(mesh, pv, motionSampleTime);
}
if (!active_color_name.IsEmpty()) {
BKE_id_attributes_default_color_set(&mesh->id, active_color_name.GetText());
BKE_id_attributes_active_color_set(&mesh->id, active_color_name.GetText());
}
}
void USDMeshReader::read_color_data_primvar(Mesh *mesh,
const pxr::UsdGeomPrimvar &color_primvar,
const double motionSampleTime)
{
if (!(mesh && color_primvar && color_primvar.HasValue())) {
return;
}
pxr::UsdGeomPrimvar color_primvar = mesh_prim_.GetDisplayColorPrimvar();
if (!color_primvar.HasValue()) {
return;
}
pxr::TfToken interp = color_primvar.GetInterpolation();
if (interp == pxr::UsdGeomTokens->varying) {
std::cerr << "WARNING: Unsupported varying interpolation for display colors\n" << std::endl;
return;
}
if (primvar_varying_map_.find(usdtokens::displayColor) == primvar_varying_map_.end()) {
if (primvar_varying_map_.find(color_primvar.GetPrimvarName()) == primvar_varying_map_.end()) {
bool might_be_time_varying = color_primvar.ValueMightBeTimeVarying();
primvar_varying_map_.insert(std::make_pair(usdtokens::displayColor, might_be_time_varying));
primvar_varying_map_.insert(
std::make_pair(color_primvar.GetPrimvarName(), might_be_time_varying));
if (might_be_time_varying) {
is_time_varying_ = true;
}
}
pxr::VtArray<pxr::GfVec3f> display_colors;
pxr::VtArray<pxr::GfVec3f> usd_colors;
if (!color_primvar.ComputeFlattened(&display_colors, motionSampleTime)) {
std::cerr << "WARNING: Couldn't compute display colors\n" << std::endl;
if (!color_primvar.ComputeFlattened(&usd_colors, motionSampleTime)) {
WM_reportf(RPT_WARNING,
"USD Import: couldn't compute values for color attribute '%s'",
color_primvar.GetName().GetText());
return;
}
if ((interp == pxr::UsdGeomTokens->faceVarying && display_colors.size() != mesh->totloop) ||
(interp == pxr::UsdGeomTokens->vertex && display_colors.size() != mesh->totvert) ||
(interp == pxr::UsdGeomTokens->constant && display_colors.size() != 1) ||
(interp == pxr::UsdGeomTokens->uniform && display_colors.size() != mesh->totpoly)) {
std::cerr << "WARNING: display colors count mismatch\n" << std::endl;
pxr::TfToken interp = color_primvar.GetInterpolation();
if ((interp == pxr::UsdGeomTokens->faceVarying && usd_colors.size() != mesh->totloop) ||
(interp == pxr::UsdGeomTokens->varying && usd_colors.size() != mesh->totloop) ||

For a future patch (because I feel it's more important to land this patch & improve upon it than to keep delaying further), I think it might be better to structure this something like:

int expected_size(const pxr::TfToken interp, const Mesh *mesh) {
  switch (interp) {
  case pxr::UsdGeomTokens->faceVarying:
  case pxr::UsdGeomTokens->varying:
    return mesh->totloop;
  case pxr::UsdGeomTokens->vertex:
    return mesh->totvert;
  case pxr::UsdGeomTokens->constant:
    return 1;
  case pxr::UsdGeomTokens->uniform:
    return mesh->totpoly;
  default:
    return 0;
  }
}

...

const int expect = expected_size(interp, mesh)
if (expect && usd_colors.size() != expected_size(interp, mesh)) { ... }

(update: also see my comment below about a way to take this even further)

For a future patch (because I feel it's more important to land this patch & improve upon it than to keep delaying further), I think it might be better to structure this something like: ```cpp int expected_size(const pxr::TfToken interp, const Mesh *mesh) { switch (interp) { case pxr::UsdGeomTokens->faceVarying: case pxr::UsdGeomTokens->varying: return mesh->totloop; case pxr::UsdGeomTokens->vertex: return mesh->totvert; case pxr::UsdGeomTokens->constant: return 1; case pxr::UsdGeomTokens->uniform: return mesh->totpoly; default: return 0; } } ... const int expect = expected_size(interp, mesh) if (expect && usd_colors.size() != expected_size(interp, mesh)) { ... } ``` (update: also see my comment below about a way to take this even further)
(interp == pxr::UsdGeomTokens->vertex && usd_colors.size() != mesh->totvert) ||
(interp == pxr::UsdGeomTokens->constant && usd_colors.size() != 1) ||
(interp == pxr::UsdGeomTokens->uniform && usd_colors.size() != mesh->totpoly)) {
WM_reportf(RPT_WARNING,
"USD Import: color attribute value '%s' count inconsistent with interpolation type",
color_primvar.GetName().GetText());
return;
}
void *cd_ptr = add_customdata_cb(mesh, "displayColor", CD_PROP_BYTE_COLOR);
const StringRef color_primvar_name(color_primvar.GetBaseName().GetString());
bke::MutableAttributeAccessor attributes = mesh->attributes_for_write();
if (!cd_ptr) {
std::cerr << "WARNING: Couldn't add displayColor custom data.\n";
eAttrDomain color_domain = ATTR_DOMAIN_POINT;
if (ELEM(interp,
pxr::UsdGeomTokens->varying,
pxr::UsdGeomTokens->faceVarying,
pxr::UsdGeomTokens->uniform)) {
color_domain = ATTR_DOMAIN_CORNER;
}
bke::SpanAttributeWriter<ColorGeometry4f> color_data;
color_data = attributes.lookup_or_add_for_write_only_span<ColorGeometry4f>(color_primvar_name,
color_domain);
if (!color_data) {
WM_reportf(RPT_WARNING,
"USD Import: couldn't add color attribute '%s'",
color_primvar.GetBaseName().GetText());
return;
}
MLoopCol *colors = static_cast<MLoopCol *>(cd_ptr);
const OffsetIndices polys = mesh->polys();
const Span<int> corner_verts = mesh->corner_verts();
for (const int i : polys.index_range()) {
const IndexRange poly = polys[i];
for (int j = 0; j < poly.size(); ++j) {
int loop_index = poly[j];
/* Default for constant varying interpolation. */
int usd_index = 0;
if (interp == pxr::UsdGeomTokens->vertex) {
usd_index = corner_verts[loop_index];
if (ELEM(interp, pxr::UsdGeomTokens->constant, pxr::UsdGeomTokens->uniform)) {
/* For situations where there's only a single item, flood fill the object. */

For the code below, a refactor would be in order. The handling of the interpolation type is now interleaved through various conditionals, and that makes things hard to follow / extend.

Instead of my suggestion above, it could be nice to have a strategy pattern, where each interpolation type extends an abstract base class. The selection of the concrete class would be done with a switch like above, and then that class can handle the 'how many entries are expected?' queries as well as handling the actual filling of the mesh data.

For the code below, a refactor would be in order. The handling of the interpolation type is now interleaved through various conditionals, and that makes things hard to follow / extend. Instead of my suggestion above, it could be nice to have a strategy pattern, where each interpolation type extends an abstract base class. The selection of the concrete class would be done with a `switch` like above, and then that class can handle the 'how many entries are expected?' queries as well as handling the actual filling of the mesh data.
color_data.span.fill(
ColorGeometry4f(usd_colors[0][0], usd_colors[0][1], usd_colors[0][2], 1.0f));
}
else {
/* Check for situations that allow for a straight-forward copy by index. */
if ((ELEM(interp, pxr::UsdGeomTokens->vertex)) ||
(color_domain == ATTR_DOMAIN_CORNER && !is_left_handed_)) {
for (int i = 0; i < usd_colors.size(); i++) {
ColorGeometry4f color = ColorGeometry4f(
usd_colors[i][0], usd_colors[i][1], usd_colors[i][2], 1.0f);
color_data.span[i] = color;
}
else if (interp == pxr::UsdGeomTokens->faceVarying) {
usd_index = poly.start();
if (is_left_handed_) {
usd_index += poly.size() - 1 - j;
}
else {
usd_index += j;
}
/* Special case: expand uniform color into corner color.
* Uniforms in USD come through as single colors, face-varying. Since Blender does not
* support this particular combination for paintable color attributes, we convert the type
* here to make sure that the user gets the same visual result.
* */
else if (ELEM(interp, pxr::UsdGeomTokens->uniform)) {
for (int i = 0; i < usd_colors.size(); i++) {
const ColorGeometry4f color = ColorGeometry4f(
usd_colors[i][0], usd_colors[i][1], usd_colors[i][2], 1.0f);
color_data.span[i * 4] = color;
color_data.span[i * 4 + 1] = color;
color_data.span[i * 4 + 2] = color;
color_data.span[i * 4 + 3] = color;
}
}
else {
const OffsetIndices polys = mesh->polys();
const Span<int> corner_verts = mesh->corner_verts();
for (const int i : polys.index_range()) {
const IndexRange &poly = polys[i];
for (int j = 0; j < poly.size(); ++j) {
int loop_index = poly[j];
/* Default for constant varying interpolation. */
int usd_index = 0;
if (interp == pxr::UsdGeomTokens->vertex) {
usd_index = corner_verts[loop_index];
}
else if (interp == pxr::UsdGeomTokens->faceVarying) {
usd_index = poly.start();
if (is_left_handed_) {
usd_index += poly.size() - 1 - j;
}
else {
usd_index += j;
}
}
else if (interp == pxr::UsdGeomTokens->uniform) {
/* Uniform varying uses the poly index. */
usd_index = i;
}
if (usd_index >= usd_colors.size()) {
continue;
}
ColorGeometry4f color = ColorGeometry4f(
usd_colors[usd_index][0], usd_colors[usd_index][1], usd_colors[usd_index][2], 1.0f);
color_data.span[usd_index] = color;
}
}
else if (interp == pxr::UsdGeomTokens->uniform) {
/* Uniform varying uses the poly index. */
usd_index = i;
}
if (usd_index >= display_colors.size()) {
continue;
}
colors[loop_index].r = unit_float_to_uchar_clamp(display_colors[usd_index][0]);
colors[loop_index].g = unit_float_to_uchar_clamp(display_colors[usd_index][1]);
colors[loop_index].b = unit_float_to_uchar_clamp(display_colors[usd_index][2]);
colors[loop_index].a = unit_float_to_uchar_clamp(1.0);
}
}
BKE_id_attributes_active_color_set(&mesh->id, "displayColor");
color_data.finish();
}
void USDMeshReader::read_vertex_creases(Mesh *mesh, const double motionSampleTime)
@ -672,9 +764,19 @@ void USDMeshReader::read_mesh_sample(ImportSettings *settings,
read_uvs(mesh, motionSampleTime, new_mesh);
}
/* Custom Data layers. */
read_custom_data(settings, mesh, motionSampleTime);
}
void USDMeshReader::read_custom_data(const ImportSettings *settings,
Mesh *mesh,
const double motionSampleTime)
{
if ((settings->read_flag & MOD_MESHSEQ_READ_COLOR) != 0) {
read_colors(mesh, motionSampleTime);
read_color_data_all_primvars(mesh, motionSampleTime);
}
/* TODO: Generic readers for custom data layers not listed above. */
}
void USDMeshReader::assign_facesets_to_material_indices(double motionSampleTime,

View File

@ -66,13 +66,20 @@ class USDMeshReader : public USDGeomReader {
void read_mpolys(Mesh *mesh);
void read_uvs(Mesh *mesh, double motionSampleTime, bool load_uvs = false);
void read_colors(Mesh *mesh, double motionSampleTime);
void read_vertex_creases(Mesh *mesh, double motionSampleTime);
void read_mesh_sample(ImportSettings *settings,
Mesh *mesh,
double motionSampleTime,
bool new_mesh);
void read_custom_data(const ImportSettings *settings,
Mesh *mesh,
double motionSampleTime);
void read_color_data_all_primvars(Mesh *mesh, const double motionSampleTime);
CharlesWardlaw marked this conversation as resolved Outdated

Now there is ..._data and ..._data_all. It's not immediately clear from the naming how these relate. read_color_data_primvar and read_color_data_all_primvars would be better, but if you feel that's getting too long, documentation of what they do would already help a lot.

Now there is `..._data` and `..._data_all`. It's not immediately clear from the naming how these relate. `read_color_data_primvar` and `read_color_data_all_primvars` would be better, but if you feel that's getting too long, documentation of what they do would already help a lot.
void read_color_data_primvar(Mesh *mesh, const pxr::UsdGeomPrimvar &color_primvar,
const double motionSampleTime);
};
} // namespace blender::io::usd

View File

@ -14,7 +14,6 @@
#include "BLI_math_vector_types.hh"
#include "BKE_attribute.h"
#include "BKE_attribute.hh"
#include "BKE_customdata.h"
#include "BKE_lib_id.h"
#include "BKE_material.h"
@ -32,6 +31,8 @@
#include "DNA_object_fluidsim_types.h"
#include "DNA_particle_types.h"
#include "WM_api.h"
#include <iostream>
namespace blender::io::usd {
@ -73,6 +74,72 @@ void USDGenericMeshWriter::do_write(HierarchyContext &context)
}
}
void USDGenericMeshWriter::write_custom_data(const Mesh *mesh, pxr::UsdGeomMesh usd_mesh)
{
const bke::AttributeAccessor attributes = mesh->attributes();
attributes.for_all(
[&](const bke::AttributeIDRef &attribute_id, const bke::AttributeMetaData &meta_data) {
/* Color data. */
if (ELEM(meta_data.domain, ATTR_DOMAIN_CORNER, ATTR_DOMAIN_POINT) &&
ELEM(meta_data.data_type, CD_PROP_BYTE_COLOR, CD_PROP_COLOR)) {
write_color_data(mesh, usd_mesh, attribute_id, meta_data);
}
return true;
});
}
void USDGenericMeshWriter::write_color_data(const Mesh *mesh,
pxr::UsdGeomMesh usd_mesh,
const bke::AttributeIDRef &attribute_id,
const bke::AttributeMetaData &meta_data)
{
pxr::UsdTimeCode timecode = get_export_time_code();
const std::string name = attribute_id.name();
pxr::TfToken primvar_name(pxr::TfMakeValidIdentifier(name));
const pxr::UsdGeomPrimvarsAPI pvApi = pxr::UsdGeomPrimvarsAPI(usd_mesh);
/* Varying type depends on original domain. */
const pxr::TfToken prim_varying = meta_data.domain == ATTR_DOMAIN_CORNER ?
pxr::UsdGeomTokens->faceVarying :
pxr::UsdGeomTokens->vertex;
pxr::UsdGeomPrimvar colors_pv = pvApi.CreatePrimvar(
primvar_name, pxr::SdfValueTypeNames->Color3fArray, prim_varying);
const VArray<ColorGeometry4f> attribute = mesh->attributes().lookup_or_default<ColorGeometry4f>(
attribute_id, meta_data.domain, {0.0f, 0.0f, 0.0f, 1.0f});
pxr::VtArray<pxr::GfVec3f> colors_data;
/* TODO: Thread the copy, like the obj exporter. */
switch (meta_data.domain) {
case ATTR_DOMAIN_CORNER:
for (size_t loop_idx = 0; loop_idx < mesh->totloop; loop_idx++) {
const ColorGeometry4f color = attribute.get(loop_idx);
colors_data.push_back(pxr::GfVec3f(color.r, color.g, color.b));
}
break;
case ATTR_DOMAIN_POINT:
for (const int point_index : attribute.index_range()) {
const ColorGeometry4f color = attribute.get(point_index);
colors_data.push_back(pxr::GfVec3f(color.r, color.g, color.b));
}
break;
default:
BLI_assert_msg(0, "Invalid domain for mesh color data.");
return;
}
colors_pv.Set(colors_data, timecode);
const pxr::UsdAttribute &prim_colors_attr = colors_pv.GetAttr();
usd_value_writer_.SetAttribute(prim_colors_attr, pxr::VtValue(colors_data), timecode);
}
void USDGenericMeshWriter::free_export_mesh(Mesh *mesh)
{
BKE_id_free(nullptr, mesh);
@ -233,6 +300,9 @@ void USDGenericMeshWriter::write_mesh(HierarchyContext &context, Mesh *mesh)
if (usd_export_context_.export_params.export_uvmaps) {
write_uv_maps(mesh, usd_mesh);
}
write_custom_data(mesh, usd_mesh);
if (usd_export_context_.export_params.export_normals) {
write_normals(mesh, usd_mesh);
}

View File

@ -4,6 +4,8 @@
#include "usd_writer_abstract.h"
#include "BKE_attribute.hh"
#include <pxr/usd/usdGeom/mesh.h>
namespace blender::io::usd {
@ -34,6 +36,12 @@ class USDGenericMeshWriter : public USDAbstractWriter {
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(const Mesh *mesh, pxr::UsdGeomMesh usd_mesh);
void write_custom_data(const Mesh *mesh, pxr::UsdGeomMesh usd_mesh);
void write_color_data(const Mesh *mesh,
CharlesWardlaw marked this conversation as resolved Outdated

At the moment write_custom_data() only writes color data, meaning that this function is strongly related to write_color_data(). Remove the empty line between the two to show this relation more visually.

At the moment `write_custom_data()` only writes color data, meaning that this function is strongly related to `write_color_data()`. Remove the empty line between the two to show this relation more visually.
pxr::UsdGeomMesh usd_mesh,
const bke::AttributeIDRef &attribute_id,
const bke::AttributeMetaData &meta_data);
};
class USDMeshWriter : public USDGenericMeshWriter {