This repository has been archived on 2023-10-09. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
blender-archive/source/blender/io/alembic/exporter/abc_writer_mesh.cc
Sybren A. Stüvel ee97add4c4 Alembic export: write custom properties
Write custom properties (aka ID properties) to Alembic, to the
`.userProperties` compound property.

Manifest Task: https://developer.blender.org/T50725

Scalar properties (so single-value/non-array properties) are written as
single-element array properties to Alembic. This is also what's done by
Houdini and Maya exporters, so it seems to be the standard way of doing
things. It also simplifies the implementation.

Two-dimensional arrays are flattened by concatenating all the numbers
into a single array. This is because ID properties have a limited type
system. This means that a 3x3 "matrix" could just as well be a list of
three 3D vectors.

Alembic has two container properties to store custom data:
- `.userProperties`, which is meant for properties that aren't
  necessarily understood by other software packages, and
- `.arbGeomParams`, which can contain the same kind of data as
  `.userProperties`, but can also specify that these vary per face of a
  mesh. This property is mostly intended for renderers.

Most industry packages write their custom data to `.arbGeomParams`.
However, given their goals I feel that `.userProperties` is the more
appropriate one for Blender's ID Properties.

The code is a bit more involved than I would have liked. An
`ABCAbstractWriter` has a `uniqueptr` to its `CustomPropertiesExporter`,
but the `CustomPropertiesExporter` also has a pointer back to its owning
`ABCAbstractWriter`. It's the latter pointer that I'm not too happy
with, but it has a reason. Getting the aforementioned `.userProperties`
from the Alembic library will automatically create it if it doesn't
exist already. If it's not used to actually add custom properties to, it
will crash the Alembic CLI tools (and maybe others too). This is what
the pointer back to the `ABCAbstractWriter` is used for: to get the
`.userProperties` at the last moment, when it's 100% sure at least one
custom property will be written.

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

Reviewed by: sergey, dbystedt
2020-09-14 12:49:27 +02:00

561 lines
17 KiB
C++

/*
* 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.
*/
/** \file
* \ingroup balembic
*/
#include "abc_writer_mesh.h"
#include "abc_hierarchy_iterator.h"
#include "intern/abc_axis_conversion.h"
#include "BLI_assert.h"
#include "BLI_math_vector.h"
#include "BKE_customdata.h"
#include "BKE_lib_id.h"
#include "BKE_material.h"
#include "BKE_mesh.h"
#include "BKE_modifier.h"
#include "BKE_object.h"
#include "bmesh.h"
#include "bmesh_tools.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"
#include "CLG_log.h"
static CLG_LogRef LOG = {"io.alembic"};
using Alembic::Abc::FloatArraySample;
using Alembic::Abc::Int32ArraySample;
using Alembic::Abc::OObject;
using Alembic::Abc::V2fArraySample;
using Alembic::Abc::V3fArraySample;
using Alembic::AbcGeom::kFacevaryingScope;
using Alembic::AbcGeom::OBoolProperty;
using Alembic::AbcGeom::OCompoundProperty;
using Alembic::AbcGeom::OFaceSet;
using Alembic::AbcGeom::OFaceSetSchema;
using Alembic::AbcGeom::ON3fGeomParam;
using Alembic::AbcGeom::OPolyMesh;
using Alembic::AbcGeom::OPolyMeshSchema;
using Alembic::AbcGeom::OSubD;
using Alembic::AbcGeom::OSubDSchema;
using Alembic::AbcGeom::OV2fGeomParam;
using Alembic::AbcGeom::UInt32ArraySample;
namespace blender::io::alembic {
/* NOTE: Alembic's polygon winding order is clockwise, to match with Renderman. */
static void get_vertices(struct Mesh *mesh, std::vector<Imath::V3f> &points);
static void get_topology(struct Mesh *mesh,
std::vector<int32_t> &poly_verts,
std::vector<int32_t> &loop_counts,
bool &r_has_flat_shaded_poly);
static void get_creases(struct Mesh *mesh,
std::vector<int32_t> &indices,
std::vector<int32_t> &lengths,
std::vector<float> &sharpnesses);
static void get_loop_normals(struct Mesh *mesh,
std::vector<Imath::V3f> &normals,
bool has_flat_shaded_poly);
ABCGenericMeshWriter::ABCGenericMeshWriter(const ABCWriterConstructorArgs &args)
: ABCAbstractWriter(args), is_subd_(false)
{
}
void ABCGenericMeshWriter::create_alembic_objects(const HierarchyContext *context)
{
if (!args_.export_params->apply_subdiv && export_as_subdivision_surface(context->object)) {
is_subd_ = args_.export_params->use_subdiv_schema;
}
if (is_subd_) {
CLOG_INFO(&LOG, 2, "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();
}
else {
CLOG_INFO(&LOG, 2, "exporting OPolyMesh %s", args_.abc_path.c_str());
abc_poly_mesh_ = OPolyMesh(args_.abc_parent, args_.abc_name, timesample_index_);
abc_poly_mesh_schema_ = abc_poly_mesh_.getSchema();
OCompoundProperty typeContainer = abc_poly_mesh_.getSchema().getUserProperties();
OBoolProperty type(typeContainer, "meshtype");
type.set(subsurf_modifier_ == nullptr);
}
Scene *scene_eval = DEG_get_evaluated_scene(args_.depsgraph);
liquid_sim_modifier_ = get_liquid_sim_modifier(scene_eval, context->object);
}
ABCGenericMeshWriter::~ABCGenericMeshWriter()
{
}
Alembic::Abc::OObject ABCGenericMeshWriter::get_alembic_object() const
{
if (is_subd_) {
return abc_subdiv_;
}
return abc_poly_mesh_;
}
Alembic::Abc::OCompoundProperty ABCGenericMeshWriter::abc_prop_for_custom_props()
{
if (is_subd_) {
return abc_schema_prop_for_custom_props(abc_subdiv_schema_);
}
return abc_schema_prop_for_custom_props(abc_poly_mesh_schema_);
}
bool ABCGenericMeshWriter::export_as_subdivision_surface(Object *ob_eval) const
{
ModifierData *md = static_cast<ModifierData *>(ob_eval->modifiers.last);
for (; md; md = md->prev) {
/* This modifier has been temporarily disabled by SubdivModifierDisabler,
* so this indicates this is to be exported as subdivision surface. */
if (md->type == eModifierType_Subsurf && (md->mode & eModifierMode_DisableTemporary)) {
return true;
}
}
return false;
}
ModifierData *ABCGenericMeshWriter::get_liquid_sim_modifier(Scene *scene, Object *ob)
{
ModifierData *md = BKE_modifiers_findby_type(ob, eModifierType_Fluidsim);
if (md && (BKE_modifier_is_enabled(scene, md, eModifierMode_Render))) {
FluidsimModifierData *fsmd = reinterpret_cast<FluidsimModifierData *>(md);
if (fsmd->fss && fsmd->fss->type == OB_FLUIDSIM_DOMAIN) {
return md;
}
}
return nullptr;
}
bool ABCGenericMeshWriter::is_supported(const HierarchyContext *context) const
{
if (args_.export_params->visible_objects_only) {
return context->is_object_visible(DAG_EVAL_RENDER);
}
return true;
}
void ABCGenericMeshWriter::do_write(HierarchyContext &context)
{
Object *object = context.object;
bool needsfree = false;
Mesh *mesh = get_export_mesh(object, needsfree);
if (mesh == nullptr) {
return;
}
if (args_.export_params->triangulate) {
const bool tag_only = false;
const int quad_method = args_.export_params->quad_method;
const int ngon_method = args_.export_params->ngon_method;
struct BMeshCreateParams bmcp = {false};
struct BMeshFromMeshParams bmfmp = {true, false, false, 0};
BMesh *bm = BKE_mesh_to_bmesh_ex(mesh, &bmcp, &bmfmp);
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;
}
m_custom_data_config.pack_uvs = args_.export_params->packuv;
m_custom_data_config.mpoly = mesh->mpoly;
m_custom_data_config.mloop = mesh->mloop;
m_custom_data_config.totpoly = mesh->totpoly;
m_custom_data_config.totloop = mesh->totloop;
m_custom_data_config.totvert = mesh->totvert;
try {
if (is_subd_) {
write_subd(context, mesh);
}
else {
write_mesh(context, mesh);
}
if (needsfree) {
free_export_mesh(mesh);
}
}
catch (...) {
if (needsfree) {
free_export_mesh(mesh);
}
throw;
}
}
void ABCGenericMeshWriter::free_export_mesh(Mesh *mesh)
{
BKE_id_free(nullptr, mesh);
}
void ABCGenericMeshWriter::write_mesh(HierarchyContext &context, Mesh *mesh)
{
std::vector<Imath::V3f> points, normals;
std::vector<int32_t> poly_verts, loop_counts;
std::vector<Imath::V3f> velocities;
bool has_flat_shaded_poly = false;
get_vertices(mesh, points);
get_topology(mesh, poly_verts, loop_counts, has_flat_shaded_poly);
if (!frame_has_been_written_ && args_.export_params->face_sets) {
write_face_sets(context.object, mesh, abc_poly_mesh_schema_);
}
OPolyMeshSchema::Sample mesh_sample = OPolyMeshSchema::Sample(
V3fArraySample(points), Int32ArraySample(poly_verts), Int32ArraySample(loop_counts));
UVSample uvs_and_indices;
if (!frame_has_been_written_ && args_.export_params->uvs) {
const char *name = get_uv_sample(uvs_and_indices, m_custom_data_config, &mesh->ldata);
if (!uvs_and_indices.indices.empty() && !uvs_and_indices.uvs.empty()) {
OV2fGeomParam::Sample uv_sample;
uv_sample.setVals(V2fArraySample(uvs_and_indices.uvs));
uv_sample.setIndices(UInt32ArraySample(uvs_and_indices.indices));
uv_sample.setScope(kFacevaryingScope);
abc_poly_mesh_schema_.setUVSourceName(name);
mesh_sample.setUVs(uv_sample);
}
write_custom_data(
abc_poly_mesh_schema_.getArbGeomParams(), m_custom_data_config, &mesh->ldata, CD_MLOOPUV);
}
if (args_.export_params->normals) {
get_loop_normals(mesh, normals, has_flat_shaded_poly);
ON3fGeomParam::Sample normals_sample;
if (!normals.empty()) {
normals_sample.setScope(kFacevaryingScope);
normals_sample.setVals(V3fArraySample(normals));
}
mesh_sample.setNormals(normals_sample);
}
if (liquid_sim_modifier_ != nullptr) {
get_velocities(mesh, velocities);
mesh_sample.setVelocities(V3fArraySample(velocities));
}
update_bounding_box(context.object);
mesh_sample.setSelfBounds(bounding_box_);
abc_poly_mesh_schema_.set(mesh_sample);
write_arb_geo_params(mesh);
}
void ABCGenericMeshWriter::write_subd(HierarchyContext &context, struct Mesh *mesh)
{
std::vector<float> crease_sharpness;
std::vector<Imath::V3f> points;
std::vector<int32_t> poly_verts, loop_counts;
std::vector<int32_t> crease_indices, crease_lengths;
bool has_flat_poly = false;
get_vertices(mesh, points);
get_topology(mesh, poly_verts, loop_counts, has_flat_poly);
get_creases(mesh, crease_indices, crease_lengths, crease_sharpness);
if (!frame_has_been_written_ && args_.export_params->face_sets) {
write_face_sets(context.object, mesh, abc_subdiv_schema_);
}
OSubDSchema::Sample subdiv_sample = OSubDSchema::Sample(
V3fArraySample(points), Int32ArraySample(poly_verts), Int32ArraySample(loop_counts));
UVSample sample;
if (!frame_has_been_written_ && args_.export_params->uvs) {
const char *name = get_uv_sample(sample, m_custom_data_config, &mesh->ldata);
if (!sample.indices.empty() && !sample.uvs.empty()) {
OV2fGeomParam::Sample uv_sample;
uv_sample.setVals(V2fArraySample(sample.uvs));
uv_sample.setIndices(UInt32ArraySample(sample.indices));
uv_sample.setScope(kFacevaryingScope);
abc_subdiv_schema_.setUVSourceName(name);
subdiv_sample.setUVs(uv_sample);
}
write_custom_data(
abc_subdiv_schema_.getArbGeomParams(), m_custom_data_config, &mesh->ldata, CD_MLOOPUV);
}
if (!crease_indices.empty()) {
subdiv_sample.setCreaseIndices(Int32ArraySample(crease_indices));
subdiv_sample.setCreaseLengths(Int32ArraySample(crease_lengths));
subdiv_sample.setCreaseSharpnesses(FloatArraySample(crease_sharpness));
}
update_bounding_box(context.object);
subdiv_sample.setSelfBounds(bounding_box_);
abc_subdiv_schema_.set(subdiv_sample);
write_arb_geo_params(mesh);
}
template<typename Schema>
void ABCGenericMeshWriter::write_face_sets(Object *object, struct Mesh *mesh, Schema &schema)
{
std::map<std::string, std::vector<int32_t>> geo_groups;
get_geo_groups(object, mesh, geo_groups);
std::map<std::string, std::vector<int32_t>>::iterator it;
for (it = geo_groups.begin(); it != geo_groups.end(); ++it) {
OFaceSet face_set = schema.createFaceSet(it->first);
OFaceSetSchema::Sample samp;
samp.setFaces(Int32ArraySample(it->second));
face_set.getSchema().set(samp);
}
}
void ABCGenericMeshWriter::write_arb_geo_params(struct Mesh *me)
{
if (liquid_sim_modifier_ != nullptr) {
/* We don't need anything more for liquid meshes. */
return;
}
if (frame_has_been_written_ || !args_.export_params->vcolors) {
return;
}
OCompoundProperty arb_geom_params;
if (is_subd_) {
arb_geom_params = abc_subdiv_.getSchema().getArbGeomParams();
}
else {
arb_geom_params = abc_poly_mesh_.getSchema().getArbGeomParams();
}
write_custom_data(arb_geom_params, m_custom_data_config, &me->ldata, CD_MLOOPCOL);
}
void ABCGenericMeshWriter::get_velocities(struct Mesh *mesh, std::vector<Imath::V3f> &vels)
{
const int totverts = mesh->totvert;
vels.clear();
vels.resize(totverts);
FluidsimModifierData *fmd = reinterpret_cast<FluidsimModifierData *>(liquid_sim_modifier_);
FluidsimSettings *fss = fmd->fss;
if (fss->meshVelocities) {
float *mesh_vels = reinterpret_cast<float *>(fss->meshVelocities);
for (int i = 0; i < totverts; i++) {
copy_yup_from_zup(vels[i].getValue(), mesh_vels);
mesh_vels += 3;
}
}
else {
std::fill(vels.begin(), vels.end(), Imath::V3f(0.0f));
}
}
void ABCGenericMeshWriter::get_geo_groups(Object *object,
struct Mesh *mesh,
std::map<std::string, std::vector<int32_t>> &geo_groups)
{
const int num_poly = mesh->totpoly;
MPoly *polygons = mesh->mpoly;
for (int i = 0; i < num_poly; i++) {
MPoly &current_poly = polygons[i];
short mnr = current_poly.mat_nr;
Material *mat = BKE_object_material_get(object, mnr + 1);
if (!mat) {
continue;
}
std::string name = args_.hierarchy_iterator->get_id_name(&mat->id);
if (geo_groups.find(name) == geo_groups.end()) {
std::vector<int32_t> faceArray;
geo_groups[name] = faceArray;
}
geo_groups[name].push_back(i);
}
if (geo_groups.empty()) {
Material *mat = BKE_object_material_get(object, 1);
std::string name = (mat) ? args_.hierarchy_iterator->get_id_name(&mat->id) : "default";
std::vector<int32_t> faceArray;
for (int i = 0, e = mesh->totface; i < e; i++) {
faceArray.push_back(i);
}
geo_groups[name] = faceArray;
}
}
/* NOTE: Alembic's polygon winding order is clockwise, to match with Renderman. */
static void get_vertices(struct Mesh *mesh, std::vector<Imath::V3f> &points)
{
points.clear();
points.resize(mesh->totvert);
MVert *verts = mesh->mvert;
for (int i = 0, e = mesh->totvert; i < e; i++) {
copy_yup_from_zup(points[i].getValue(), verts[i].co);
}
}
static void get_topology(struct Mesh *mesh,
std::vector<int32_t> &poly_verts,
std::vector<int32_t> &loop_counts,
bool &r_has_flat_shaded_poly)
{
const int num_poly = mesh->totpoly;
const int num_loops = mesh->totloop;
MLoop *mloop = mesh->mloop;
MPoly *mpoly = mesh->mpoly;
r_has_flat_shaded_poly = false;
poly_verts.clear();
loop_counts.clear();
poly_verts.reserve(num_loops);
loop_counts.reserve(num_poly);
/* NOTE: data needs to be written in the reverse order. */
for (int i = 0; i < num_poly; i++) {
MPoly &poly = mpoly[i];
loop_counts.push_back(poly.totloop);
r_has_flat_shaded_poly |= (poly.flag & ME_SMOOTH) == 0;
MLoop *loop = mloop + poly.loopstart + (poly.totloop - 1);
for (int j = 0; j < poly.totloop; j++, loop--) {
poly_verts.push_back(loop->v);
}
}
}
static void get_creases(struct Mesh *mesh,
std::vector<int32_t> &indices,
std::vector<int32_t> &lengths,
std::vector<float> &sharpnesses)
{
const float factor = 1.0f / 255.0f;
indices.clear();
lengths.clear();
sharpnesses.clear();
MEdge *edge = mesh->medge;
for (int i = 0, e = mesh->totedge; i < e; i++) {
const float sharpness = static_cast<float>(edge[i].crease) * factor;
if (sharpness != 0.0f) {
indices.push_back(edge[i].v1);
indices.push_back(edge[i].v2);
sharpnesses.push_back(sharpness);
}
}
lengths.resize(sharpnesses.size(), 2);
}
static void get_loop_normals(struct Mesh *mesh,
std::vector<Imath::V3f> &normals,
bool has_flat_shaded_poly)
{
normals.clear();
/* If all polygons are smooth shaded, and there are no custom normals, we don't need to export
* normals at all. This is also done by other software, see T71246. */
if (!has_flat_shaded_poly && !CustomData_has_layer(&mesh->ldata, CD_CUSTOMLOOPNORMAL) &&
(mesh->flag & ME_AUTOSMOOTH) == 0) {
return;
}
BKE_mesh_calc_normals_split(mesh);
const float(*lnors)[3] = static_cast<float(*)[3]>(CustomData_get_layer(&mesh->ldata, CD_NORMAL));
BLI_assert(lnors != nullptr || !"BKE_mesh_calc_normals_split() should have computed CD_NORMAL");
normals.resize(mesh->totloop);
/* NOTE: data needs to be written in the reverse order. */
int abc_index = 0;
MPoly *mp = mesh->mpoly;
for (int i = 0, e = mesh->totpoly; i < e; i++, mp++) {
for (int j = mp->totloop - 1; j >= 0; j--, abc_index++) {
int blender_index = mp->loopstart + j;
copy_yup_from_zup(normals[abc_index].getValue(), lnors[blender_index]);
}
}
}
ABCMeshWriter::ABCMeshWriter(const ABCWriterConstructorArgs &args) : ABCGenericMeshWriter(args)
{
}
Mesh *ABCMeshWriter::get_export_mesh(Object *object_eval, bool & /*r_needsfree*/)
{
return BKE_object_get_evaluated_mesh(object_eval);
}
} // namespace blender::io::alembic