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/intern/cycles/blender/mesh.cpp
Hans Goudey 16fbadde36 Mesh: Replace MLoop struct with generic attributes
Implements #102359.

Split the `MLoop` struct into two separate integer arrays called
`corner_verts` and `corner_edges`, referring to the vertex each corner
is attached to and the next edge around the face at each corner. These
arrays can be sliced to give access to the edges or vertices in a face.
Then they are often referred to as "poly_verts" or "poly_edges".

The main benefits are halving the necessary memory bandwidth when only
one array is used and simplifications from using regular integer indices
instead of a special-purpose struct.

The commit also starts a renaming from "loop" to "corner" in mesh code.

Like the other mesh struct of array refactors, forward compatibility is
kept by writing files with the older format. This will be done until 4.0
to ease the transition process.

Looking at a small portion of the patch should give a good impression
for the rest of the changes. I tried to make the changes as small as
possible so it's easy to tell the correctness from the diff. Though I
found Blender developers have been very inventive over the last decade
when finding different ways to loop over the corners in a face.

For performance, nearly every piece of code that deals with `Mesh` is
slightly impacted. Any algorithm that is memory bottle-necked should
see an improvement. For example, here is a comparison of interpolating
a vertex float attribute to face corners (Ryzen 3700x):

**Before** (Average: 3.7 ms, Min: 3.4 ms)
```
threading::parallel_for(loops.index_range(), 4096, [&](IndexRange range) {
  for (const int64_t i : range) {
    dst[i] = src[loops[i].v];
  }
});
```

**After** (Average: 2.9 ms, Min: 2.6 ms)
```
array_utils::gather(src, corner_verts, dst);
```

That's an improvement of 28% to the average timings, and it's also a
simplification, since an index-based routine can be used instead.
For more examples using the new arrays, see the design task.

Pull Request: blender/blender#104424
2023-03-20 15:55:13 +01:00

1356 lines
46 KiB
C++

/* SPDX-License-Identifier: Apache-2.0
* Copyright 2011-2022 Blender Foundation */
#include <optional>
#include "blender/session.h"
#include "blender/sync.h"
#include "blender/util.h"
#include "scene/camera.h"
#include "scene/colorspace.h"
#include "scene/mesh.h"
#include "scene/object.h"
#include "scene/scene.h"
#include "subd/patch.h"
#include "subd/split.h"
#include "util/algorithm.h"
#include "util/color.h"
#include "util/disjoint_set.h"
#include "util/foreach.h"
#include "util/hash.h"
#include "util/log.h"
#include "util/math.h"
#include "mikktspace.hh"
#include "DNA_meshdata_types.h"
CCL_NAMESPACE_BEGIN
/* Tangent Space */
template<bool is_subd> struct MikkMeshWrapper {
MikkMeshWrapper(const BL::Mesh &b_mesh,
const char *layer_name,
const Mesh *mesh,
float3 *tangent,
float *tangent_sign)
: mesh(mesh), texface(NULL), orco(NULL), tangent(tangent), tangent_sign(tangent_sign)
{
const AttributeSet &attributes = is_subd ? mesh->subd_attributes : mesh->attributes;
Attribute *attr_vN = attributes.find(ATTR_STD_VERTEX_NORMAL);
vertex_normal = attr_vN->data_float3();
if (layer_name == NULL) {
Attribute *attr_orco = attributes.find(ATTR_STD_GENERATED);
if (attr_orco) {
orco = attr_orco->data_float3();
float3 orco_size;
mesh_texture_space(*(BL::Mesh *)&b_mesh, orco_loc, orco_size);
inv_orco_size = 1.0f / orco_size;
}
}
else {
Attribute *attr_uv = attributes.find(ustring(layer_name));
if (attr_uv != NULL) {
texface = attr_uv->data_float2();
}
}
}
int GetNumFaces()
{
if constexpr (is_subd) {
return mesh->get_num_subd_faces();
}
else {
return mesh->num_triangles();
}
}
int GetNumVerticesOfFace(const int face_num)
{
if constexpr (is_subd) {
return mesh->get_subd_num_corners()[face_num];
}
else {
return 3;
}
}
int CornerIndex(const int face_num, const int vert_num)
{
if constexpr (is_subd) {
const Mesh::SubdFace &face = mesh->get_subd_face(face_num);
return face.start_corner + vert_num;
}
else {
return face_num * 3 + vert_num;
}
}
int VertexIndex(const int face_num, const int vert_num)
{
int corner = CornerIndex(face_num, vert_num);
if constexpr (is_subd) {
return mesh->get_subd_face_corners()[corner];
}
else {
return mesh->get_triangles()[corner];
}
}
mikk::float3 GetPosition(const int face_num, const int vert_num)
{
const float3 vP = mesh->get_verts()[VertexIndex(face_num, vert_num)];
return mikk::float3(vP.x, vP.y, vP.z);
}
mikk::float3 GetTexCoord(const int face_num, const int vert_num)
{
/* TODO: Check whether introducing a template boolean in order to
* turn this into a constexpr is worth it. */
if (texface != NULL) {
const int corner_index = CornerIndex(face_num, vert_num);
float2 tfuv = texface[corner_index];
return mikk::float3(tfuv.x, tfuv.y, 1.0f);
}
else if (orco != NULL) {
const int vertex_index = VertexIndex(face_num, vert_num);
const float2 uv = map_to_sphere((orco[vertex_index] + orco_loc) * inv_orco_size);
return mikk::float3(uv.x, uv.y, 1.0f);
}
else {
return mikk::float3(0.0f, 0.0f, 1.0f);
}
}
mikk::float3 GetNormal(const int face_num, const int vert_num)
{
float3 vN;
if (is_subd) {
const Mesh::SubdFace &face = mesh->get_subd_face(face_num);
if (face.smooth) {
const int vertex_index = VertexIndex(face_num, vert_num);
vN = vertex_normal[vertex_index];
}
else {
vN = face.normal(mesh);
}
}
else {
if (mesh->get_smooth()[face_num]) {
const int vertex_index = VertexIndex(face_num, vert_num);
vN = vertex_normal[vertex_index];
}
else {
const Mesh::Triangle tri = mesh->get_triangle(face_num);
vN = tri.compute_normal(&mesh->get_verts()[0]);
}
}
return mikk::float3(vN.x, vN.y, vN.z);
}
void SetTangentSpace(const int face_num, const int vert_num, mikk::float3 T, bool orientation)
{
const int corner_index = CornerIndex(face_num, vert_num);
tangent[corner_index] = make_float3(T.x, T.y, T.z);
if (tangent_sign != NULL) {
tangent_sign[corner_index] = orientation ? 1.0f : -1.0f;
}
}
const Mesh *mesh;
int num_faces;
float3 *vertex_normal;
float2 *texface;
float3 *orco;
float3 orco_loc, inv_orco_size;
float3 *tangent;
float *tangent_sign;
};
static void mikk_compute_tangents(
const BL::Mesh &b_mesh, const char *layer_name, Mesh *mesh, bool need_sign, bool active_render)
{
/* Create tangent attributes. */
const bool is_subd = mesh->get_num_subd_faces();
AttributeSet &attributes = is_subd ? mesh->subd_attributes : mesh->attributes;
Attribute *attr;
ustring name;
if (layer_name != NULL) {
name = ustring((string(layer_name) + ".tangent").c_str());
}
else {
name = ustring("orco.tangent");
}
if (active_render) {
attr = attributes.add(ATTR_STD_UV_TANGENT, name);
}
else {
attr = attributes.add(name, TypeDesc::TypeVector, ATTR_ELEMENT_CORNER);
}
float3 *tangent = attr->data_float3();
/* Create bitangent sign attribute. */
float *tangent_sign = NULL;
if (need_sign) {
Attribute *attr_sign;
ustring name_sign;
if (layer_name != NULL) {
name_sign = ustring((string(layer_name) + ".tangent_sign").c_str());
}
else {
name_sign = ustring("orco.tangent_sign");
}
if (active_render) {
attr_sign = attributes.add(ATTR_STD_UV_TANGENT_SIGN, name_sign);
}
else {
attr_sign = attributes.add(name_sign, TypeDesc::TypeFloat, ATTR_ELEMENT_CORNER);
}
tangent_sign = attr_sign->data_float();
}
/* Setup userdata. */
if (is_subd) {
MikkMeshWrapper<true> userdata(b_mesh, layer_name, mesh, tangent, tangent_sign);
/* Compute tangents. */
mikk::Mikktspace(userdata).genTangSpace();
}
else {
MikkMeshWrapper<false> userdata(b_mesh, layer_name, mesh, tangent, tangent_sign);
/* Compute tangents. */
mikk::Mikktspace(userdata).genTangSpace();
}
}
template<typename TypeInCycles, typename GetValueAtIndex>
static void fill_generic_attribute(BL::Mesh &b_mesh,
TypeInCycles *data,
const BL::Attribute::domain_enum b_domain,
const bool subdivision,
const GetValueAtIndex &get_value_at_index)
{
switch (b_domain) {
case BL::Attribute::domain_CORNER: {
if (subdivision) {
const int polys_num = b_mesh.polygons.length();
if (polys_num == 0) {
return;
}
const MPoly *polys = static_cast<const MPoly *>(b_mesh.polygons[0].ptr.data);
for (int i = 0; i < polys_num; i++) {
const MPoly &b_poly = polys[i];
for (int j = 0; j < b_poly.totloop; j++) {
*data = get_value_at_index(b_poly.loopstart + j);
data++;
}
}
}
else {
for (BL::MeshLoopTriangle &t : b_mesh.loop_triangles) {
const int index = t.index() * 3;
BL::Array<int, 3> loops = t.loops();
data[index] = get_value_at_index(loops[0]);
data[index + 1] = get_value_at_index(loops[1]);
data[index + 2] = get_value_at_index(loops[2]);
}
}
break;
}
case BL::Attribute::domain_EDGE: {
const size_t edges_num = b_mesh.edges.length();
if (edges_num == 0) {
return;
}
if constexpr (std::is_same_v<TypeInCycles, uchar4>) {
/* uchar4 edge attributes do not exist, and averaging in place
* would not work. */
assert(0);
}
else {
const MEdge *edges = static_cast<const MEdge *>(b_mesh.edges[0].ptr.data);
const size_t verts_num = b_mesh.vertices.length();
vector<int> count(verts_num, 0);
/* Average edge attributes at vertices. */
for (int i = 0; i < edges_num; i++) {
TypeInCycles value = get_value_at_index(i);
const MEdge &b_edge = edges[i];
data[b_edge.v1] += value;
data[b_edge.v2] += value;
count[b_edge.v1]++;
count[b_edge.v2]++;
}
for (size_t i = 0; i < verts_num; i++) {
if (count[i] > 1) {
data[i] /= (float)count[i];
}
}
}
break;
}
case BL::Attribute::domain_POINT: {
const int num_verts = b_mesh.vertices.length();
for (int i = 0; i < num_verts; i++) {
data[i] = get_value_at_index(i);
}
break;
}
case BL::Attribute::domain_FACE: {
if (subdivision) {
const int num_polygons = b_mesh.polygons.length();
for (int i = 0; i < num_polygons; i++) {
data[i] = get_value_at_index(i);
}
}
else {
for (BL::MeshLoopTriangle &t : b_mesh.loop_triangles) {
data[t.index()] = get_value_at_index(t.polygon_index());
}
}
break;
}
default: {
assert(false);
break;
}
}
}
static void attr_create_motion(Mesh *mesh, BL::Attribute &b_attribute, const float motion_scale)
{
if (!(b_attribute.domain() == BL::Attribute::domain_POINT) &&
(b_attribute.data_type() == BL::Attribute::data_type_FLOAT_VECTOR)) {
return;
}
BL::FloatVectorAttribute b_vector_attribute(b_attribute);
const int numverts = mesh->get_verts().size();
/* Find or add attribute */
float3 *P = &mesh->get_verts()[0];
Attribute *attr_mP = mesh->attributes.find(ATTR_STD_MOTION_VERTEX_POSITION);
if (!attr_mP) {
attr_mP = mesh->attributes.add(ATTR_STD_MOTION_VERTEX_POSITION);
}
/* Only export previous and next frame, we don't have any in between data. */
float motion_times[2] = {-1.0f, 1.0f};
for (int step = 0; step < 2; step++) {
const float relative_time = motion_times[step] * 0.5f * motion_scale;
float3 *mP = attr_mP->data_float3() + step * numverts;
for (int i = 0; i < numverts; i++) {
mP[i] = P[i] + get_float3(b_vector_attribute.data[i].vector()) * relative_time;
}
}
}
static void attr_create_generic(Scene *scene,
Mesh *mesh,
BL::Mesh &b_mesh,
const bool subdivision,
const bool need_motion,
const float motion_scale)
{
AttributeSet &attributes = (subdivision) ? mesh->subd_attributes : mesh->attributes;
static const ustring u_velocity("velocity");
const ustring default_color_name{b_mesh.attributes.default_color_name().c_str()};
for (BL::Attribute &b_attribute : b_mesh.attributes) {
const ustring name{b_attribute.name().c_str()};
const bool is_render_color = name == default_color_name;
if (need_motion && name == u_velocity) {
attr_create_motion(mesh, b_attribute, motion_scale);
}
if (!(mesh->need_attribute(scene, name) ||
(is_render_color && mesh->need_attribute(scene, ATTR_STD_VERTEX_COLOR)))) {
continue;
}
if (attributes.find(name)) {
continue;
}
const BL::Attribute::domain_enum b_domain = b_attribute.domain();
const BL::Attribute::data_type_enum b_data_type = b_attribute.data_type();
AttributeElement element = ATTR_ELEMENT_NONE;
switch (b_domain) {
case BL::Attribute::domain_CORNER:
element = ATTR_ELEMENT_CORNER;
break;
case BL::Attribute::domain_POINT:
element = ATTR_ELEMENT_VERTEX;
break;
case BL::Attribute::domain_EDGE:
element = ATTR_ELEMENT_VERTEX;
break;
case BL::Attribute::domain_FACE:
element = ATTR_ELEMENT_FACE;
break;
default:
break;
}
if (element == ATTR_ELEMENT_NONE) {
/* Not supported. */
continue;
}
switch (b_data_type) {
case BL::Attribute::data_type_FLOAT: {
BL::FloatAttribute b_float_attribute{b_attribute};
Attribute *attr = attributes.add(name, TypeFloat, element);
float *data = attr->data_float();
fill_generic_attribute(b_mesh, data, b_domain, subdivision, [&](int i) {
return b_float_attribute.data[i].value();
});
break;
}
case BL::Attribute::data_type_BOOLEAN: {
BL::BoolAttribute b_bool_attribute{b_attribute};
Attribute *attr = attributes.add(name, TypeFloat, element);
float *data = attr->data_float();
fill_generic_attribute(b_mesh, data, b_domain, subdivision, [&](int i) {
return (float)b_bool_attribute.data[i].value();
});
break;
}
case BL::Attribute::data_type_INT: {
BL::IntAttribute b_int_attribute{b_attribute};
Attribute *attr = attributes.add(name, TypeFloat, element);
float *data = attr->data_float();
fill_generic_attribute(b_mesh, data, b_domain, subdivision, [&](int i) {
return (float)b_int_attribute.data[i].value();
});
break;
}
case BL::Attribute::data_type_FLOAT_VECTOR: {
BL::FloatVectorAttribute b_vector_attribute{b_attribute};
Attribute *attr = attributes.add(name, TypeVector, element);
float3 *data = attr->data_float3();
fill_generic_attribute(b_mesh, data, b_domain, subdivision, [&](int i) {
BL::Array<float, 3> v = b_vector_attribute.data[i].vector();
return make_float3(v[0], v[1], v[2]);
});
break;
}
case BL::Attribute::data_type_BYTE_COLOR: {
BL::ByteColorAttribute b_color_attribute{b_attribute};
if (element == ATTR_ELEMENT_CORNER) {
element = ATTR_ELEMENT_CORNER_BYTE;
}
Attribute *attr = attributes.add(name, TypeRGBA, element);
if (is_render_color) {
attr->std = ATTR_STD_VERTEX_COLOR;
}
if (element == ATTR_ELEMENT_CORNER_BYTE) {
uchar4 *data = attr->data_uchar4();
fill_generic_attribute(b_mesh, data, b_domain, subdivision, [&](int i) {
/* Compress/encode vertex color using the sRGB curve. */
const float4 c = get_float4(b_color_attribute.data[i].color());
return color_float4_to_uchar4(color_linear_to_srgb_v4(c));
});
}
else {
float4 *data = attr->data_float4();
fill_generic_attribute(b_mesh, data, b_domain, subdivision, [&](int i) {
BL::Array<float, 4> v = b_color_attribute.data[i].color();
return make_float4(v[0], v[1], v[2], v[3]);
});
}
break;
}
case BL::Attribute::data_type_FLOAT_COLOR: {
BL::FloatColorAttribute b_color_attribute{b_attribute};
Attribute *attr = attributes.add(name, TypeRGBA, element);
if (is_render_color) {
attr->std = ATTR_STD_VERTEX_COLOR;
}
float4 *data = attr->data_float4();
fill_generic_attribute(b_mesh, data, b_domain, subdivision, [&](int i) {
BL::Array<float, 4> v = b_color_attribute.data[i].color();
return make_float4(v[0], v[1], v[2], v[3]);
});
break;
}
case BL::Attribute::data_type_FLOAT2: {
BL::Float2Attribute b_float2_attribute{b_attribute};
Attribute *attr = attributes.add(name, TypeFloat2, element);
float2 *data = attr->data_float2();
fill_generic_attribute(b_mesh, data, b_domain, subdivision, [&](int i) {
BL::Array<float, 2> v = b_float2_attribute.data[i].vector();
return make_float2(v[0], v[1]);
});
break;
}
default:
/* Not supported. */
break;
}
}
}
/* Create uv map attributes. */
static void attr_create_uv_map(Scene *scene, Mesh *mesh, BL::Mesh &b_mesh)
{
if (!b_mesh.uv_layers.empty()) {
for (BL::MeshUVLoopLayer &l : b_mesh.uv_layers) {
const bool active_render = l.active_render();
AttributeStandard uv_std = (active_render) ? ATTR_STD_UV : ATTR_STD_NONE;
ustring uv_name = ustring(l.name().c_str());
AttributeStandard tangent_std = (active_render) ? ATTR_STD_UV_TANGENT : ATTR_STD_NONE;
ustring tangent_name = ustring((string(l.name().c_str()) + ".tangent").c_str());
/* Denotes whether UV map was requested directly. */
const bool need_uv = mesh->need_attribute(scene, uv_name) ||
mesh->need_attribute(scene, uv_std);
/* Denotes whether tangent was requested directly. */
const bool need_tangent = mesh->need_attribute(scene, tangent_name) ||
(active_render && mesh->need_attribute(scene, tangent_std));
/* UV map */
/* NOTE: We create temporary UV layer if its needed for tangent but
* wasn't requested by other nodes in shaders.
*/
Attribute *uv_attr = NULL;
if (need_uv || need_tangent) {
if (active_render) {
uv_attr = mesh->attributes.add(uv_std, uv_name);
}
else {
uv_attr = mesh->attributes.add(uv_name, TypeFloat2, ATTR_ELEMENT_CORNER);
}
float2 *fdata = uv_attr->data_float2();
for (BL::MeshLoopTriangle &t : b_mesh.loop_triangles) {
int3 li = get_int3(t.loops());
fdata[0] = get_float2(l.data[li[0]].uv());
fdata[1] = get_float2(l.data[li[1]].uv());
fdata[2] = get_float2(l.data[li[2]].uv());
fdata += 3;
}
}
/* UV tangent */
if (need_tangent) {
AttributeStandard sign_std = (active_render) ? ATTR_STD_UV_TANGENT_SIGN : ATTR_STD_NONE;
ustring sign_name = ustring((string(l.name().c_str()) + ".tangent_sign").c_str());
bool need_sign = (mesh->need_attribute(scene, sign_name) ||
mesh->need_attribute(scene, sign_std));
mikk_compute_tangents(b_mesh, l.name().c_str(), mesh, need_sign, active_render);
}
/* Remove temporarily created UV attribute. */
if (!need_uv && uv_attr != NULL) {
mesh->attributes.remove(uv_attr);
}
}
}
else if (mesh->need_attribute(scene, ATTR_STD_UV_TANGENT)) {
bool need_sign = mesh->need_attribute(scene, ATTR_STD_UV_TANGENT_SIGN);
mikk_compute_tangents(b_mesh, NULL, mesh, need_sign, true);
if (!mesh->need_attribute(scene, ATTR_STD_GENERATED)) {
mesh->attributes.remove(ATTR_STD_GENERATED);
}
}
}
static void attr_create_subd_uv_map(Scene *scene, Mesh *mesh, BL::Mesh &b_mesh, bool subdivide_uvs)
{
const int polys_num = b_mesh.polygons.length();
if (polys_num == 0) {
return;
}
const MPoly *polys = static_cast<const MPoly *>(b_mesh.polygons[0].ptr.data);
if (!b_mesh.uv_layers.empty()) {
BL::Mesh::uv_layers_iterator l;
int i = 0;
for (b_mesh.uv_layers.begin(l); l != b_mesh.uv_layers.end(); ++l, ++i) {
bool active_render = l->active_render();
AttributeStandard uv_std = (active_render) ? ATTR_STD_UV : ATTR_STD_NONE;
ustring uv_name = ustring(l->name().c_str());
AttributeStandard tangent_std = (active_render) ? ATTR_STD_UV_TANGENT : ATTR_STD_NONE;
ustring tangent_name = ustring((string(l->name().c_str()) + ".tangent").c_str());
/* Denotes whether UV map was requested directly. */
const bool need_uv = mesh->need_attribute(scene, uv_name) ||
mesh->need_attribute(scene, uv_std);
/* Denotes whether tangent was requested directly. */
const bool need_tangent = mesh->need_attribute(scene, tangent_name) ||
(active_render && mesh->need_attribute(scene, tangent_std));
Attribute *uv_attr = NULL;
/* UV map */
if (need_uv || need_tangent) {
if (active_render)
uv_attr = mesh->subd_attributes.add(uv_std, uv_name);
else
uv_attr = mesh->subd_attributes.add(uv_name, TypeFloat2, ATTR_ELEMENT_CORNER);
if (subdivide_uvs) {
uv_attr->flags |= ATTR_SUBDIVIDED;
}
float2 *fdata = uv_attr->data_float2();
for (int i = 0; i < polys_num; i++) {
const MPoly &b_poly = polys[i];
for (int j = 0; j < b_poly.totloop; j++) {
*(fdata++) = get_float2(l->data[b_poly.loopstart + j].uv());
}
}
}
/* UV tangent */
if (need_tangent) {
AttributeStandard sign_std = (active_render) ? ATTR_STD_UV_TANGENT_SIGN : ATTR_STD_NONE;
ustring sign_name = ustring((string(l->name().c_str()) + ".tangent_sign").c_str());
bool need_sign = (mesh->need_attribute(scene, sign_name) ||
mesh->need_attribute(scene, sign_std));
mikk_compute_tangents(b_mesh, l->name().c_str(), mesh, need_sign, active_render);
}
/* Remove temporarily created UV attribute. */
if (!need_uv && uv_attr != NULL) {
mesh->subd_attributes.remove(uv_attr);
}
}
}
else if (mesh->need_attribute(scene, ATTR_STD_UV_TANGENT)) {
bool need_sign = mesh->need_attribute(scene, ATTR_STD_UV_TANGENT_SIGN);
mikk_compute_tangents(b_mesh, NULL, mesh, need_sign, true);
if (!mesh->need_attribute(scene, ATTR_STD_GENERATED)) {
mesh->subd_attributes.remove(ATTR_STD_GENERATED);
}
}
}
/* Create vertex pointiness attributes. */
/* Compare vertices by sum of their coordinates. */
class VertexAverageComparator {
public:
VertexAverageComparator(const array<float3> &verts) : verts_(verts)
{
}
bool operator()(const int &vert_idx_a, const int &vert_idx_b)
{
const float3 &vert_a = verts_[vert_idx_a];
const float3 &vert_b = verts_[vert_idx_b];
if (vert_a == vert_b) {
/* Special case for doubles, so we ensure ordering. */
return vert_idx_a > vert_idx_b;
}
const float x1 = vert_a.x + vert_a.y + vert_a.z;
const float x2 = vert_b.x + vert_b.y + vert_b.z;
return x1 < x2;
}
protected:
const array<float3> &verts_;
};
static void attr_create_pointiness(Scene *scene, Mesh *mesh, BL::Mesh &b_mesh, bool subdivision)
{
if (!mesh->need_attribute(scene, ATTR_STD_POINTINESS)) {
return;
}
const int num_verts = b_mesh.vertices.length();
if (num_verts == 0) {
return;
}
const float(*positions)[3] = static_cast<const float(*)[3]>(b_mesh.vertices[0].ptr.data);
/* STEP 1: Find out duplicated vertices and point duplicates to a single
* original vertex.
*/
vector<int> sorted_vert_indeices(num_verts);
for (int vert_index = 0; vert_index < num_verts; ++vert_index) {
sorted_vert_indeices[vert_index] = vert_index;
}
VertexAverageComparator compare(mesh->get_verts());
sort(sorted_vert_indeices.begin(), sorted_vert_indeices.end(), compare);
/* This array stores index of the original vertex for the given vertex
* index.
*/
vector<int> vert_orig_index(num_verts);
for (int sorted_vert_index = 0; sorted_vert_index < num_verts; ++sorted_vert_index) {
const int vert_index = sorted_vert_indeices[sorted_vert_index];
const float3 &vert_co = mesh->get_verts()[vert_index];
bool found = false;
for (int other_sorted_vert_index = sorted_vert_index + 1; other_sorted_vert_index < num_verts;
++other_sorted_vert_index) {
const int other_vert_index = sorted_vert_indeices[other_sorted_vert_index];
const float3 &other_vert_co = mesh->get_verts()[other_vert_index];
/* We are too far away now, we wouldn't have duplicate. */
if ((other_vert_co.x + other_vert_co.y + other_vert_co.z) -
(vert_co.x + vert_co.y + vert_co.z) >
3 * FLT_EPSILON) {
break;
}
/* Found duplicate. */
if (len_squared(other_vert_co - vert_co) < FLT_EPSILON) {
found = true;
vert_orig_index[vert_index] = other_vert_index;
break;
}
}
if (!found) {
vert_orig_index[vert_index] = vert_index;
}
}
/* Make sure we always points to the very first orig vertex. */
for (int vert_index = 0; vert_index < num_verts; ++vert_index) {
int orig_index = vert_orig_index[vert_index];
while (orig_index != vert_orig_index[orig_index]) {
orig_index = vert_orig_index[orig_index];
}
vert_orig_index[vert_index] = orig_index;
}
sorted_vert_indeices.free_memory();
/* STEP 2: Calculate vertex normals taking into account their possible
* duplicates which gets "welded" together.
*/
vector<float3> vert_normal(num_verts, zero_float3());
/* First we accumulate all vertex normals in the original index. */
const float(*b_vert_normals)[3] = static_cast<const float(*)[3]>(
b_mesh.vertex_normals[0].ptr.data);
for (int vert_index = 0; vert_index < num_verts; ++vert_index) {
const float *b_vert_normal = b_vert_normals[vert_index];
const int orig_index = vert_orig_index[vert_index];
vert_normal[orig_index] += make_float3(b_vert_normal[0], b_vert_normal[1], b_vert_normal[2]);
}
/* Then we normalize the accumulated result and flush it to all duplicates
* as well.
*/
for (int vert_index = 0; vert_index < num_verts; ++vert_index) {
const int orig_index = vert_orig_index[vert_index];
vert_normal[vert_index] = normalize(vert_normal[orig_index]);
}
/* STEP 3: Calculate pointiness using single ring neighborhood. */
vector<int> counter(num_verts, 0);
vector<float> raw_data(num_verts, 0.0f);
vector<float3> edge_accum(num_verts, zero_float3());
EdgeMap visited_edges;
memset(&counter[0], 0, sizeof(int) * counter.size());
const MEdge *edges = static_cast<MEdge *>(b_mesh.edges[0].ptr.data);
const int edges_num = b_mesh.edges.length();
for (int i = 0; i < edges_num; i++) {
const MEdge &b_edge = edges[i];
const int v0 = vert_orig_index[b_edge.v1];
const int v1 = vert_orig_index[b_edge.v2];
if (visited_edges.exists(v0, v1)) {
continue;
}
visited_edges.insert(v0, v1);
float3 co0 = make_float3(positions[v0][0], positions[v0][1], positions[v0][2]);
float3 co1 = make_float3(positions[v1][0], positions[v1][1], positions[v1][2]);
float3 edge = normalize(co1 - co0);
edge_accum[v0] += edge;
edge_accum[v1] += -edge;
++counter[v0];
++counter[v1];
}
for (int vert_index = 0; vert_index < num_verts; ++vert_index) {
const int orig_index = vert_orig_index[vert_index];
if (orig_index != vert_index) {
/* Skip duplicates, they'll be overwritten later on. */
continue;
}
if (counter[vert_index] > 0) {
const float3 normal = vert_normal[vert_index];
const float angle = safe_acosf(dot(normal, edge_accum[vert_index] / counter[vert_index]));
raw_data[vert_index] = angle * M_1_PI_F;
}
else {
raw_data[vert_index] = 0.0f;
}
}
/* STEP 3: Blur vertices to approximate 2 ring neighborhood. */
AttributeSet &attributes = (subdivision) ? mesh->subd_attributes : mesh->attributes;
Attribute *attr = attributes.add(ATTR_STD_POINTINESS);
float *data = attr->data_float();
memcpy(data, &raw_data[0], sizeof(float) * raw_data.size());
memset(&counter[0], 0, sizeof(int) * counter.size());
visited_edges.clear();
for (int i = 0; i < edges_num; i++) {
const MEdge &b_edge = edges[i];
const int v0 = vert_orig_index[b_edge.v1];
const int v1 = vert_orig_index[b_edge.v2];
if (visited_edges.exists(v0, v1)) {
continue;
}
visited_edges.insert(v0, v1);
data[v0] += raw_data[v1];
data[v1] += raw_data[v0];
++counter[v0];
++counter[v1];
}
for (int vert_index = 0; vert_index < num_verts; ++vert_index) {
data[vert_index] /= counter[vert_index] + 1;
}
/* STEP 4: Copy attribute to the duplicated vertices. */
for (int vert_index = 0; vert_index < num_verts; ++vert_index) {
const int orig_index = vert_orig_index[vert_index];
data[vert_index] = data[orig_index];
}
}
static std::optional<BL::IntAttribute> find_corner_vert_attribute(BL::Mesh b_mesh)
{
for (BL::Attribute &b_attribute : b_mesh.attributes) {
if (b_attribute.domain() != BL::Attribute::domain_CORNER) {
continue;
}
if (b_attribute.data_type() != BL::Attribute::data_type_INT) {
continue;
}
if (b_attribute.name() != ".corner_vert") {
continue;
}
return BL::IntAttribute{b_attribute};
}
return std::nullopt;
}
/* The Random Per Island attribute is a random float associated with each
* connected component (island) of the mesh. The attribute is computed by
* first classifying the vertices into different sets using a Disjoint Set
* data structure. Then the index of the root of each vertex (Which is the
* representative of the set the vertex belongs to) is hashed and stored.
*
* We are using a face attribute to avoid interpolation during rendering,
* allowing the user to safely hash the output further. Had we used vertex
* attribute, the interpolation will introduce very slight variations,
* making the output unsafe to hash. */
static void attr_create_random_per_island(Scene *scene,
Mesh *mesh,
BL::Mesh &b_mesh,
bool subdivision)
{
if (!mesh->need_attribute(scene, ATTR_STD_RANDOM_PER_ISLAND)) {
return;
}
const int polys_num = b_mesh.polygons.length();
int number_of_vertices = b_mesh.vertices.length();
if (number_of_vertices == 0) {
return;
}
DisjointSet vertices_sets(number_of_vertices);
const MEdge *edges = static_cast<MEdge *>(b_mesh.edges[0].ptr.data);
const int edges_num = b_mesh.edges.length();
for (int i = 0; i < edges_num; i++) {
vertices_sets.join(edges[i].v1, edges[i].v2);
}
AttributeSet &attributes = (subdivision) ? mesh->subd_attributes : mesh->attributes;
Attribute *attribute = attributes.add(ATTR_STD_RANDOM_PER_ISLAND);
float *data = attribute->data_float();
if (!subdivision) {
for (BL::MeshLoopTriangle &t : b_mesh.loop_triangles) {
data[t.index()] = hash_uint_to_float(vertices_sets.find(t.vertices()[0]));
}
}
else {
if (polys_num != 0) {
const MPoly *polys = static_cast<const MPoly *>(b_mesh.polygons[0].ptr.data);
BL::IntAttribute corner_verts = *find_corner_vert_attribute(b_mesh);
for (int i = 0; i < polys_num; i++) {
const MPoly &b_poly = polys[i];
const int vert = corner_verts.data[b_poly.loopstart].value();
data[i] = hash_uint_to_float(vertices_sets.find(vert));
}
}
}
}
/* Create Mesh */
static std::optional<BL::IntAttribute> find_material_index_attribute(BL::Mesh b_mesh)
{
for (BL::Attribute &b_attribute : b_mesh.attributes) {
if (b_attribute.domain() != BL::Attribute::domain_FACE) {
continue;
}
if (b_attribute.data_type() != BL::Attribute::data_type_INT) {
continue;
}
if (b_attribute.name() != "material_index") {
continue;
}
return BL::IntAttribute{b_attribute};
}
return std::nullopt;
}
static std::optional<BL::BoolAttribute> find_sharp_face_attribute(BL::Mesh b_mesh)
{
for (BL::Attribute &b_attribute : b_mesh.attributes) {
if (b_attribute.domain() != BL::Attribute::domain_FACE) {
continue;
}
if (b_attribute.data_type() != BL::Attribute::data_type_BOOLEAN) {
continue;
}
if (b_attribute.name() != "sharp_face") {
continue;
}
return BL::BoolAttribute{b_attribute};
}
return std::nullopt;
}
static void create_mesh(Scene *scene,
Mesh *mesh,
BL::Mesh &b_mesh,
const array<Node *> &used_shaders,
const bool need_motion,
const float motion_scale,
const bool subdivision = false,
const bool subdivide_uvs = true)
{
/* count vertices and faces */
int numverts = b_mesh.vertices.length();
const int polys_num = b_mesh.polygons.length();
int numfaces = (!subdivision) ? b_mesh.loop_triangles.length() : b_mesh.polygons.length();
int numtris = 0;
int numcorners = 0;
int numngons = 0;
bool use_loop_normals = b_mesh.use_auto_smooth() &&
(mesh->get_subdivision_type() != Mesh::SUBDIVISION_CATMULL_CLARK);
/* If no faces, create empty mesh. */
if (numfaces == 0) {
return;
}
const float(*positions)[3] = static_cast<const float(*)[3]>(b_mesh.vertices[0].ptr.data);
if (!subdivision) {
numtris = numfaces;
}
else {
const MPoly *polys = static_cast<const MPoly *>(b_mesh.polygons[0].ptr.data);
for (int i = 0; i < polys_num; i++) {
const MPoly &b_poly = polys[i];
numngons += (b_poly.totloop == 4) ? 0 : 1;
numcorners += b_poly.totloop;
}
}
/* allocate memory */
if (subdivision) {
mesh->reserve_subd_faces(numfaces, numngons, numcorners);
}
mesh->reserve_mesh(numverts, numtris);
/* create vertex coordinates and normals */
for (int i = 0; i < numverts; i++) {
mesh->add_vertex(make_float3(positions[i][0], positions[i][1], positions[i][2]));
}
AttributeSet &attributes = (subdivision) ? mesh->subd_attributes : mesh->attributes;
Attribute *attr_N = attributes.add(ATTR_STD_VERTEX_NORMAL);
float3 *N = attr_N->data_float3();
if (subdivision || !use_loop_normals) {
const float(*b_vert_normals)[3] = static_cast<const float(*)[3]>(
b_mesh.vertex_normals[0].ptr.data);
for (int i = 0; i < numverts; i++) {
const float *b_vert_normal = b_vert_normals[i];
N[i] = make_float3(b_vert_normal[0], b_vert_normal[1], b_vert_normal[2]);
}
}
/* create generated coordinates from undeformed coordinates */
const bool need_default_tangent = (subdivision == false) && (b_mesh.uv_layers.empty()) &&
(mesh->need_attribute(scene, ATTR_STD_UV_TANGENT));
if (mesh->need_attribute(scene, ATTR_STD_GENERATED) || need_default_tangent) {
Attribute *attr = attributes.add(ATTR_STD_GENERATED);
attr->flags |= ATTR_SUBDIVIDED;
float3 loc, size;
mesh_texture_space(b_mesh, loc, size);
float3 *generated = attr->data_float3();
size_t i = 0;
BL::Mesh::vertices_iterator v;
for (b_mesh.vertices.begin(v); v != b_mesh.vertices.end(); ++v) {
generated[i++] = get_float3(v->undeformed_co()) * size - loc;
}
}
std::optional<BL::IntAttribute> material_indices = find_material_index_attribute(b_mesh);
auto get_material_index = [&](const int poly_index) -> int {
if (material_indices) {
return clamp(material_indices->data[poly_index].value(), 0, used_shaders.size() - 1);
}
return 0;
};
std::optional<BL::BoolAttribute> sharp_faces = find_sharp_face_attribute(b_mesh);
auto get_face_sharp = [&](const int poly_index) -> bool {
if (sharp_faces) {
return sharp_faces->data[poly_index].value();
}
return false;
};
/* create faces */
if (!subdivision) {
for (BL::MeshLoopTriangle &t : b_mesh.loop_triangles) {
const int poly_index = t.polygon_index();
int3 vi = get_int3(t.vertices());
int shader = get_material_index(poly_index);
bool smooth = !get_face_sharp(poly_index) || use_loop_normals;
if (use_loop_normals) {
BL::Array<float, 9> loop_normals = t.split_normals();
for (int i = 0; i < 3; i++) {
N[vi[i]] = make_float3(
loop_normals[i * 3], loop_normals[i * 3 + 1], loop_normals[i * 3 + 2]);
}
}
/* Create triangles.
*
* NOTE: Autosmooth is already taken care about.
*/
mesh->add_triangle(vi[0], vi[1], vi[2], shader, smooth);
}
}
else {
vector<int> vi;
const MPoly *polys = static_cast<const MPoly *>(b_mesh.polygons[0].ptr.data);
std::optional<BL::IntAttribute> corner_verts = find_corner_vert_attribute(b_mesh);
for (int i = 0; i < numfaces; i++) {
const MPoly &b_poly = polys[i];
int n = b_poly.totloop;
int shader = get_material_index(i);
bool smooth = !get_face_sharp(i) || use_loop_normals;
vi.resize(n);
for (int i = 0; i < n; i++) {
/* NOTE: Autosmooth is already taken care about. */
vi[i] = corner_verts->data[b_poly.loopstart + i].value();
}
/* create subd faces */
mesh->add_subd_face(&vi[0], n, shader, smooth);
}
}
/* Create all needed attributes.
* The calculate functions will check whether they're needed or not.
*/
attr_create_pointiness(scene, mesh, b_mesh, subdivision);
attr_create_random_per_island(scene, mesh, b_mesh, subdivision);
attr_create_generic(scene, mesh, b_mesh, subdivision, need_motion, motion_scale);
if (subdivision) {
attr_create_subd_uv_map(scene, mesh, b_mesh, subdivide_uvs);
}
else {
attr_create_uv_map(scene, mesh, b_mesh);
}
/* For volume objects, create a matrix to transform from object space to
* mesh texture space. this does not work with deformations but that can
* probably only be done well with a volume grid mapping of coordinates. */
if (mesh->need_attribute(scene, ATTR_STD_GENERATED_TRANSFORM)) {
Attribute *attr = mesh->attributes.add(ATTR_STD_GENERATED_TRANSFORM);
Transform *tfm = attr->data_transform();
float3 loc, size;
mesh_texture_space(b_mesh, loc, size);
*tfm = transform_translate(-loc) * transform_scale(size);
}
}
static void create_subd_mesh(Scene *scene,
Mesh *mesh,
BObjectInfo &b_ob_info,
BL::Mesh &b_mesh,
const array<Node *> &used_shaders,
const bool need_motion,
const float motion_scale,
float dicing_rate,
int max_subdivisions)
{
BL::Object b_ob = b_ob_info.real_object;
BL::SubsurfModifier subsurf_mod(b_ob.modifiers[b_ob.modifiers.length() - 1]);
bool subdivide_uvs = subsurf_mod.uv_smooth() != BL::SubsurfModifier::uv_smooth_NONE;
create_mesh(scene, mesh, b_mesh, used_shaders, need_motion, motion_scale, true, subdivide_uvs);
const int edges_num = b_mesh.edges.length();
if (edges_num != 0 && b_mesh.edge_creases.length() > 0) {
BL::MeshEdgeCreaseLayer creases = b_mesh.edge_creases[0];
size_t num_creases = 0;
for (int i = 0; i < edges_num; i++) {
if (creases.data[i].value() != 0.0f) {
num_creases++;
}
}
mesh->reserve_subd_creases(num_creases);
const MEdge *edges = static_cast<MEdge *>(b_mesh.edges[0].ptr.data);
for (int i = 0; i < edges_num; i++) {
const float crease = creases.data[i].value();
if (crease != 0.0f) {
const MEdge &b_edge = edges[i];
mesh->add_edge_crease(b_edge.v1, b_edge.v2, crease);
}
}
}
for (BL::MeshVertexCreaseLayer &c : b_mesh.vertex_creases) {
for (int i = 0; i < c.data.length(); ++i) {
if (c.data[i].value() != 0.0f) {
mesh->add_vertex_crease(i, c.data[i].value());
}
}
}
/* set subd params */
PointerRNA cobj = RNA_pointer_get(&b_ob.ptr, "cycles");
float subd_dicing_rate = max(0.1f, RNA_float_get(&cobj, "dicing_rate") * dicing_rate);
mesh->set_subd_dicing_rate(subd_dicing_rate);
mesh->set_subd_max_level(max_subdivisions);
mesh->set_subd_objecttoworld(get_transform(b_ob.matrix_world()));
}
/* Sync */
void BlenderSync::sync_mesh(BL::Depsgraph b_depsgraph, BObjectInfo &b_ob_info, Mesh *mesh)
{
/* make a copy of the shaders as the caller in the main thread still need them for syncing the
* attributes */
array<Node *> used_shaders = mesh->get_used_shaders();
Mesh new_mesh;
new_mesh.set_used_shaders(used_shaders);
if (view_layer.use_surfaces) {
/* Adaptive subdivision setup. Not for baking since that requires
* exact mapping to the Blender mesh. */
if (!scene->bake_manager->get_baking()) {
new_mesh.set_subdivision_type(
object_subdivision_type(b_ob_info.real_object, preview, experimental));
}
/* For some reason, meshes do not need this... */
bool need_undeformed = new_mesh.need_attribute(scene, ATTR_STD_GENERATED);
BL::Mesh b_mesh = object_to_mesh(
b_data, b_ob_info, b_depsgraph, need_undeformed, new_mesh.get_subdivision_type());
if (b_mesh) {
/* Motion blur attribute is relative to seconds, we need it relative to frames. */
const bool need_motion = object_need_motion_attribute(b_ob_info, scene);
const float motion_scale = (need_motion) ?
scene->motion_shutter_time() /
(b_scene.render().fps() / b_scene.render().fps_base()) :
0.0f;
/* Sync mesh itself. */
if (new_mesh.get_subdivision_type() != Mesh::SUBDIVISION_NONE)
create_subd_mesh(scene,
&new_mesh,
b_ob_info,
b_mesh,
new_mesh.get_used_shaders(),
need_motion,
motion_scale,
dicing_rate,
max_subdivisions);
else
create_mesh(scene,
&new_mesh,
b_mesh,
new_mesh.get_used_shaders(),
need_motion,
motion_scale,
false);
free_object_to_mesh(b_data, b_ob_info, b_mesh);
}
}
/* update original sockets */
mesh->clear_non_sockets();
for (const SocketType &socket : new_mesh.type->inputs) {
/* Those sockets are updated in sync_object, so do not modify them. */
if (socket.name == "use_motion_blur" || socket.name == "motion_steps" ||
socket.name == "used_shaders") {
continue;
}
mesh->set_value(socket, new_mesh, socket);
}
mesh->attributes.update(std::move(new_mesh.attributes));
mesh->subd_attributes.update(std::move(new_mesh.subd_attributes));
mesh->set_num_subd_faces(new_mesh.get_num_subd_faces());
/* tag update */
bool rebuild = (mesh->triangles_is_modified()) || (mesh->subd_num_corners_is_modified()) ||
(mesh->subd_shader_is_modified()) || (mesh->subd_smooth_is_modified()) ||
(mesh->subd_ptex_offset_is_modified()) ||
(mesh->subd_start_corner_is_modified()) ||
(mesh->subd_face_corners_is_modified());
mesh->tag_update(scene, rebuild);
}
void BlenderSync::sync_mesh_motion(BL::Depsgraph b_depsgraph,
BObjectInfo &b_ob_info,
Mesh *mesh,
int motion_step)
{
/* Skip if no vertices were exported. */
size_t numverts = mesh->get_verts().size();
if (numverts == 0) {
return;
}
/* Skip objects without deforming modifiers. this is not totally reliable,
* would need a more extensive check to see which objects are animated. */
BL::Mesh b_mesh(PointerRNA_NULL);
if (ccl::BKE_object_is_deform_modified(b_ob_info, b_scene, preview)) {
/* get derived mesh */
b_mesh = object_to_mesh(b_data, b_ob_info, b_depsgraph, false, Mesh::SUBDIVISION_NONE);
}
const std::string ob_name = b_ob_info.real_object.name();
/* TODO(sergey): Perform preliminary check for number of vertices. */
if (b_mesh) {
const int b_verts_num = b_mesh.vertices.length();
if (b_verts_num == 0) {
free_object_to_mesh(b_data, b_ob_info, b_mesh);
return;
}
/* Export deformed coordinates. */
/* Find attributes. */
Attribute *attr_mP = mesh->attributes.find(ATTR_STD_MOTION_VERTEX_POSITION);
Attribute *attr_mN = mesh->attributes.find(ATTR_STD_MOTION_VERTEX_NORMAL);
Attribute *attr_N = mesh->attributes.find(ATTR_STD_VERTEX_NORMAL);
bool new_attribute = false;
/* Add new attributes if they don't exist already. */
if (!attr_mP) {
attr_mP = mesh->attributes.add(ATTR_STD_MOTION_VERTEX_POSITION);
if (attr_N)
attr_mN = mesh->attributes.add(ATTR_STD_MOTION_VERTEX_NORMAL);
new_attribute = true;
}
/* Load vertex data from mesh. */
float3 *mP = attr_mP->data_float3() + motion_step * numverts;
float3 *mN = (attr_mN) ? attr_mN->data_float3() + motion_step * numverts : NULL;
const float(*positions)[3] = static_cast<const float(*)[3]>(b_mesh.vertices[0].ptr.data);
/* NOTE: We don't copy more that existing amount of vertices to prevent
* possible memory corruption.
*/
for (int i = 0; i < std::min<size_t>(b_verts_num, numverts); i++) {
mP[i] = make_float3(positions[i][0], positions[i][1], positions[i][2]);
}
if (mN) {
const float(*b_vert_normals)[3] = static_cast<const float(*)[3]>(
b_mesh.vertex_normals[0].ptr.data);
for (int i = 0; i < std::min<size_t>(b_verts_num, numverts); i++) {
const float *b_vert_normal = b_vert_normals[i];
mN[i] = make_float3(b_vert_normal[0], b_vert_normal[1], b_vert_normal[2]);
}
}
if (new_attribute) {
/* In case of new attribute, we verify if there really was any motion. */
if (b_verts_num != numverts ||
memcmp(mP, &mesh->get_verts()[0], sizeof(float3) * numverts) == 0) {
/* no motion, remove attributes again */
if (b_verts_num != numverts) {
VLOG_WARNING << "Topology differs, disabling motion blur for object " << ob_name;
}
else {
VLOG_DEBUG << "No actual deformation motion for object " << ob_name;
}
mesh->attributes.remove(ATTR_STD_MOTION_VERTEX_POSITION);
if (attr_mN)
mesh->attributes.remove(ATTR_STD_MOTION_VERTEX_NORMAL);
}
else if (motion_step > 0) {
VLOG_DEBUG << "Filling deformation motion for object " << ob_name;
/* motion, fill up previous steps that we might have skipped because
* they had no motion, but we need them anyway now */
float3 *P = &mesh->get_verts()[0];
float3 *N = (attr_N) ? attr_N->data_float3() : NULL;
for (int step = 0; step < motion_step; step++) {
memcpy(attr_mP->data_float3() + step * numverts, P, sizeof(float3) * numverts);
if (attr_mN)
memcpy(attr_mN->data_float3() + step * numverts, N, sizeof(float3) * numverts);
}
}
}
else {
if (b_verts_num != numverts) {
VLOG_WARNING << "Topology differs, discarding motion blur for object " << ob_name
<< " at time " << motion_step;
memcpy(mP, &mesh->get_verts()[0], sizeof(float3) * numverts);
if (mN != NULL) {
memcpy(mN, attr_N->data_float3(), sizeof(float3) * numverts);
}
}
}
free_object_to_mesh(b_data, b_ob_info, b_mesh);
return;
}
/* No deformation on this frame, copy coordinates if other frames did have it. */
mesh->copy_center_to_motion_step(motion_step);
}
CCL_NAMESPACE_END