Mesh: Add "free" custom normals

Add a "dumb vector" storage option for custom normals, with the
"custom_normal" attribute. Adjust the mesh normals caching to
provide this attribute if it's available, and add a geometry node to
store custom normals.

## Free Normals
They're called "free" in the sense that they're just direction vectors
in the object's local space, rather than the existing "smooth corner
fan space" storage. They're also "free" in that they make further
normals calculation very inexpensive, since we just use the custom
normals instead. That's a big improvement from the existing custom
normals storage, which usually significantly decreases
viewport performance. For example, in a simple test file just storing
the vertex normals on a UV sphere, using free normals gives 25 times
better playback performance and 10% lower memory usage.

Free normals are adjusted when applying a transformation to the entire
mesh or when realizing instances, but in general they're not updated for
vertex deformations.

## Set Mesh Normal Node
The new geometry node allows storing free custom normals as well as
the existing corner fan space normals. When free normals are chosen,
free normals can be stored on vertices, faces, or face corners. Using
the face corner domain is necessary to bake existing mixed sharp and
smooth edges into the custom normal vectors.

The node also has a mode for storing edge and mesh sharpness, meant
as a "soft" replacement to the "Set Shade Smooth" node that's a bit
more convenient.

## Normal Input Node
The normal node outputs free custom normals mixed to whatever domain is
requested. A "true normal" output that ignores custom normals and
sharpness is added as well.

Across Blender, custom normals are generally accessed via face and
vertex normals, when "true normals" are not requested explicitly.
In many cases that means they are mixed from the face corner domain.

## Future Work
1. There are many places where propagation of free normals could be
   improved. They should probably be normalized after mixing, and it
   may be useful to not just use 0 vectors for new elements. To keep
   the scope of this change smaller, that sort of thing generally isn't
   handled here. Searching `CD_NORMAL` gives a hint of where better
   propagation could be useful.
2. Free normals are displayed properly in edit mode, but the existing
   custom normal editing operators don't work with free normals yet.
   This will hopefully be fairly straightforward since custom normals
   are usually converted to `float3` for editing anyway. Edit mode
   changes aren't included here because they're unnecessary for the
   procedural custom normals use cases.
3. Most importers can probably switch to using free normals instead,
   or at least provide an option for it. That will give a significant
   import performance improvement, and an improvement of Blender's
   FPS for imported scenes too.

Pull Request: https://projects.blender.org/blender/blender/pulls/132583
This commit is contained in:
Hans Goudey
2025-04-04 19:16:51 +02:00
committed by Hans Goudey
parent 6727675757
commit d3f84449ad
24 changed files with 825 additions and 107 deletions

View File

@@ -451,6 +451,7 @@ class NODE_MT_geometry_node_GEO_MESH_WRITE(Menu):
layout = self.layout
if context.space_data.geometry_nodes_type == 'TOOL':
node_add_menu.add_node_type(layout, "GeometryNodeToolSetFaceSet")
node_add_menu.add_node_type(layout, "GeometryNodeSetMeshNormal")
node_add_menu.add_node_type(layout, "GeometryNodeSetShadeSmooth")
node_add_menu.draw_assets_for_catalog(layout, "Mesh/Write")

View File

@@ -379,14 +379,18 @@ VArray<float3> curve_normals_varray(const CurvesGeometry &curves, AttrDomain dom
VArray<float3> mesh_normals_varray(const Mesh &mesh,
const IndexMask &mask,
AttrDomain domain,
bool no_corner_normals = false);
bool no_corner_normals = false,
bool true_normals = false);
class NormalFieldInput : public GeometryFieldInput {
bool legacy_corner_normals_ = false;
bool true_normals_ = false;
public:
NormalFieldInput(const bool legacy_corner_normals = false)
: GeometryFieldInput(CPPType::get<float3>()), legacy_corner_normals_(legacy_corner_normals)
NormalFieldInput(const bool legacy_corner_normals = false, const bool true_normals = false)
: GeometryFieldInput(CPPType::get<float3>()),
legacy_corner_normals_(legacy_corner_normals),
true_normals_(true_normals)
{
category_ = Category::Generated;
}

View File

@@ -10,6 +10,7 @@
#include <memory>
#include <mutex>
#include <variant>
#include "BLI_array.hh"
#include "BLI_bit_vector.hh"
@@ -19,6 +20,7 @@
#include "BLI_math_vector_types.hh"
#include "BLI_shared_cache.hh"
#include "BLI_vector.hh"
#include "BLI_virtual_array_fwd.hh"
#include "DNA_customdata_types.h"
@@ -95,6 +97,18 @@ struct LooseEdgeCache : public LooseGeomCache {};
*/
struct LooseVertCache : public LooseGeomCache {};
/** Similar to #VArraySpan but with the ability to be resized and updated. */
class NormalsCache {
std::variant<Vector<float3>, Span<float3>> data_;
public:
MutableSpan<float3> ensure_vector_size(const int size);
Span<float3> get_span() const;
void store_varray(const VArray<float3> &data);
void store_span(Span<float3> data);
void store_vector(Vector<float3> &&data);
};
struct TrianglesCache {
SharedCache<Array<int3>> data;
bool frozen = false;
@@ -203,11 +217,13 @@ struct MeshRuntime {
SubsurfRuntimeData *subsurf_runtime_data = nullptr;
/** Lazily computed vertex normals (#Mesh::vert_normals()). */
SharedCache<Vector<float3>> vert_normals_cache;
SharedCache<NormalsCache> vert_normals_cache;
SharedCache<Vector<float3>> vert_normals_true_cache;
/** Lazily computed face normals (#Mesh::face_normals()). */
SharedCache<Vector<float3>> face_normals_cache;
SharedCache<NormalsCache> face_normals_cache;
SharedCache<Vector<float3>> face_normals_true_cache;
/** Lazily computed face corner normals (#Mesh::corner_normals()). */
SharedCache<Vector<float3>> corner_normals_cache;
SharedCache<NormalsCache> corner_normals_cache;
/**
* Cache of offsets for vert to face/corner maps. The same offsets array is used to group

View File

@@ -388,7 +388,7 @@ static void data_transfer_dtdata_type_postprocess(Mesh *me_dst,
me_dst->corner_verts(),
me_dst->corner_edges(),
me_dst->vert_normals(),
me_dst->face_normals(),
me_dst->face_normals_true(),
sharp_faces,
sharp_edges.span,
{loop_nors_dst, me_dst->corners_num},

View File

@@ -123,21 +123,24 @@ void MeshComponent::count_memory(MemoryCounter &memory) const
VArray<float3> mesh_normals_varray(const Mesh &mesh,
const IndexMask &mask,
const AttrDomain domain,
const bool no_corner_normals)
const bool no_corner_normals,
const bool true_normals)
{
switch (domain) {
case AttrDomain::Face: {
return VArray<float3>::ForSpan(mesh.face_normals());
return VArray<float3>::ForSpan(true_normals ? mesh.face_normals_true() :
mesh.face_normals());
}
case AttrDomain::Point: {
return VArray<float3>::ForSpan(mesh.vert_normals());
return VArray<float3>::ForSpan(true_normals ? mesh.vert_normals_true() :
mesh.vert_normals());
}
case AttrDomain::Edge: {
/* In this case, start with vertex normals and convert to the edge domain, since the
* conversion from edges to vertices is very simple. Use "manual" domain interpolation
* instead of the GeometryComponent API to avoid calculating unnecessary values and to
* allow normalizing the result more simply. */
Span<float3> vert_normals = mesh.vert_normals();
Span<float3> vert_normals = true_normals ? mesh.vert_normals_true() : mesh.vert_normals();
const Span<int2> edges = mesh.edges();
Array<float3> edge_normals(mask.min_array_size());
mask.foreach_index([&](const int i) {
@@ -149,9 +152,11 @@ VArray<float3> mesh_normals_varray(const Mesh &mesh,
return VArray<float3>::ForContainer(std::move(edge_normals));
}
case AttrDomain::Corner: {
if (no_corner_normals) {
if (no_corner_normals || true_normals) {
return mesh.attributes().adapt_domain(
VArray<float3>::ForSpan(mesh.face_normals()), AttrDomain::Face, AttrDomain::Corner);
VArray<float3>::ForSpan(true_normals ? mesh.face_normals_true() : mesh.face_normals()),
AttrDomain::Face,
AttrDomain::Corner);
}
return VArray<float3>::ForSpan(mesh.corner_normals());
}

View File

@@ -718,7 +718,8 @@ GVArray NormalFieldInput::get_varray_for_context(const GeometryFieldContext &con
const IndexMask &mask) const
{
if (const Mesh *mesh = context.mesh()) {
return mesh_normals_varray(*mesh, mask, context.domain(), legacy_corner_normals_);
return mesh_normals_varray(
*mesh, mask, context.domain(), legacy_corner_normals_, true_normals_);
}
if (const CurvesGeometry *curves = context.curves_or_strokes()) {
return curve_normals_varray(*curves, context.domain());
@@ -728,17 +729,21 @@ GVArray NormalFieldInput::get_varray_for_context(const GeometryFieldContext &con
std::string NormalFieldInput::socket_inspection_name() const
{
return TIP_("Normal");
return true_normals_ ? TIP_("True Normal") : TIP_("Normal");
}
uint64_t NormalFieldInput::hash() const
{
return 213980475983;
return get_default_hash(2980541, legacy_corner_normals_, true_normals_);
}
bool NormalFieldInput::is_equal_to(const fn::FieldNode &other) const
{
return dynamic_cast<const NormalFieldInput *>(&other) != nullptr;
if (const NormalFieldInput *other_typed = dynamic_cast<const NormalFieldInput *>(&other)) {
return legacy_corner_normals_ == other_typed->legacy_corner_normals_ &&
true_normals_ == other_typed->true_normals_;
}
return false;
}
static std::optional<StringRefNull> try_get_field_direct_attribute_id(const fn::GField &any_field)

View File

@@ -141,7 +141,9 @@ static void mesh_copy_data(Main *bmain,
* Caches will be "un-shared" as necessary later on. */
mesh_dst->runtime->bounds_cache = mesh_src->runtime->bounds_cache;
mesh_dst->runtime->vert_normals_cache = mesh_src->runtime->vert_normals_cache;
mesh_dst->runtime->vert_normals_true_cache = mesh_src->runtime->vert_normals_true_cache;
mesh_dst->runtime->face_normals_cache = mesh_src->runtime->face_normals_cache;
mesh_dst->runtime->face_normals_true_cache = mesh_src->runtime->face_normals_true_cache;
mesh_dst->runtime->corner_normals_cache = mesh_src->runtime->corner_normals_cache;
mesh_dst->runtime->loose_verts_cache = mesh_src->runtime->loose_verts_cache;
mesh_dst->runtime->verts_no_face_cache = mesh_src->runtime->verts_no_face_cache;
@@ -1418,6 +1420,16 @@ static void translate_positions(MutableSpan<float3> positions, const float3 &tra
});
}
static void transform_normals(MutableSpan<float3> normals, const float4x4 &matrix)
{
const float3x3 normal_transform = math::transpose(math::invert(float3x3(matrix)));
threading::parallel_for(normals.index_range(), 1024, [&](const IndexRange range) {
for (float3 &normal : normals.slice(range)) {
normal = normal_transform * normal;
}
});
}
void mesh_translate(Mesh &mesh, const float3 &translation, const bool do_shape_keys)
{
if (math::is_zero(translation)) {
@@ -1455,6 +1467,16 @@ void mesh_transform(Mesh &mesh, const float4x4 &transform, bool do_shape_keys)
transform_positions(MutableSpan(static_cast<float3 *>(kb->data), kb->totelem), transform);
}
}
MutableAttributeAccessor attributes = mesh.attributes_for_write();
if (const std::optional<AttributeMetaData> meta_data = attributes.lookup_meta_data(
"custom_normal"))
{
if (meta_data->data_type == CD_PROP_FLOAT3) {
bke::SpanAttributeWriter normals = attributes.lookup_for_write_span<float3>("custom_normal");
transform_normals(normals.span, transform);
normals.finish();
}
}
mesh.tag_positions_changed();
}

View File

@@ -413,7 +413,7 @@ Mesh *BKE_mesh_mirror_apply_mirror_on_axis_for_modifier(MirrorModifierData *mmd,
result_corner_verts,
result_corner_edges,
result->corner_to_face_map(),
result->face_normals(),
result->face_normals_true(),
sharp_edges,
sharp_faces,
clnors,

View File

@@ -48,15 +48,55 @@ namespace blender::bke {
void mesh_vert_normals_assign(Mesh &mesh, Span<float3> vert_normals)
{
mesh.runtime->vert_normals_cache.ensure([&](Vector<float3> &r_data) { r_data = vert_normals; });
mesh.runtime->vert_normals_true_cache.ensure(
[&](Vector<float3> &r_data) { r_data = vert_normals; });
}
void mesh_vert_normals_assign(Mesh &mesh, Vector<float3> vert_normals)
{
mesh.runtime->vert_normals_cache.ensure(
mesh.runtime->vert_normals_true_cache.ensure(
[&](Vector<float3> &r_data) { r_data = std::move(vert_normals); });
}
MutableSpan<float3> NormalsCache::ensure_vector_size(const int size)
{
if (auto *vector = std::get_if<Vector<float3>>(&data_)) {
vector->resize(size);
}
else {
data_ = Vector<float3>(size);
}
return std::get<Vector<float3>>(data_).as_mutable_span();
}
Span<float3> NormalsCache::get_span() const
{
if (const auto *vector = std::get_if<Vector<float3>>(&data_)) {
return vector->as_span();
}
return std::get<Span<float3>>(data_);
}
void NormalsCache::store_varray(const VArray<float3> &data)
{
if (data.is_span()) {
data_ = data.get_internal_span();
}
else {
data.materialize(this->ensure_vector_size(data.size()));
}
}
void NormalsCache::store_span(const Span<float3> data)
{
data_ = data;
}
void NormalsCache::store_vector(Vector<float3> &&data)
{
data_ = std::move(data);
}
} // namespace blender::bke
bool BKE_mesh_vert_normals_are_dirty(const Mesh *mesh)
@@ -187,6 +227,70 @@ void normals_calc_verts(const Span<float3> vert_positions,
/** \} */
static void mix_normals_corner_to_vert(const Span<float3> vert_positions,
const OffsetIndices<int> faces,
const Span<int> corner_verts,
const GroupedSpan<int> vert_to_face_map,
const Span<float3> corner_normals,
MutableSpan<float3> vert_normals)
{
const Span<float3> positions = vert_positions;
threading::parallel_for(positions.index_range(), 1024, [&](const IndexRange range) {
for (const int vert : range) {
const Span<int> vert_faces = vert_to_face_map[vert];
if (vert_faces.is_empty()) {
vert_normals[vert] = math::normalize(positions[vert]);
continue;
}
float3 vert_normal(0);
for (const int face : vert_faces) {
const int corner = mesh::face_find_corner_from_vert(faces[face], corner_verts, vert);
const int2 adjacent_verts{corner_verts[mesh::face_corner_prev(faces[face], corner)],
corner_verts[mesh::face_corner_next(faces[face], corner)]};
const float3 dir_prev = math::normalize(positions[adjacent_verts[0]] - positions[vert]);
const float3 dir_next = math::normalize(positions[adjacent_verts[1]] - positions[vert]);
const float factor = math::safe_acos_approx(math::dot(dir_prev, dir_next));
vert_normal += corner_normals[corner] * factor;
}
vert_normals[vert] = math::normalize(vert_normal);
}
});
}
static void mix_normals_vert_to_face(const OffsetIndices<int> faces,
const Span<int> corner_verts,
const Span<float3> vert_normals,
MutableSpan<float3> face_normals)
{
threading::parallel_for(faces.index_range(), 1024, [&](const IndexRange range) {
for (const int face : range) {
float3 sum(0);
for (const int vert : corner_verts.slice(faces[face])) {
sum += vert_normals[vert];
}
face_normals[face] = math::normalize(sum);
}
});
}
static void mix_normals_corner_to_face(const OffsetIndices<int> faces,
const Span<float3> corner_normals,
MutableSpan<float3> face_normals)
{
threading::parallel_for(faces.index_range(), 1024, [&](const IndexRange range) {
for (const int face : range) {
const Span<float3> face_corner_normals = corner_normals.slice(faces[face]);
const float3 sum = std::accumulate(
face_corner_normals.begin(), face_corner_normals.end(), float3(0));
face_normals[face] = math::normalize(sum);
}
});
}
} // namespace blender::bke::mesh
/* -------------------------------------------------------------------- */
@@ -197,19 +301,16 @@ blender::bke::MeshNormalDomain Mesh::normals_domain(const bool support_sharp_fac
{
using namespace blender;
using namespace blender::bke;
if (this->faces_num == 0) {
return MeshNormalDomain::Point;
}
const bke::AttributeAccessor attributes = this->attributes();
if (const std::optional<AttributeMetaData> custom = attributes.lookup_meta_data("custom_normal"))
{
switch (custom->domain) {
case AttrDomain::Point:
return MeshNormalDomain::Point;
case AttrDomain::Edge:
case AttrDomain::Face:
/* Not supported yet. */
break;
case AttrDomain::Face:
return MeshNormalDomain::Face;
case AttrDomain::Corner:
return MeshNormalDomain::Corner;
default:
@@ -245,78 +346,157 @@ blender::Span<blender::float3> Mesh::vert_normals() const
{
using namespace blender;
using namespace blender::bke;
if (this->runtime->vert_normals_cache.is_cached()) {
return this->runtime->vert_normals_cache.data();
}
const Span<float3> positions = this->vert_positions();
const OffsetIndices faces = this->faces();
const Span<int> corner_verts = this->corner_verts();
const Span<float3> face_normals = this->face_normals();
const GroupedSpan<int> vert_to_face = this->vert_to_face_map();
this->runtime->vert_normals_cache.ensure([&](Vector<float3> &r_data) {
r_data.reinitialize(positions.size());
mesh::normals_calc_verts(positions, faces, corner_verts, vert_to_face, face_normals, r_data);
this->runtime->vert_normals_cache.ensure([&](NormalsCache &r_data) {
if (const GAttributeReader custom = this->attributes().lookup("custom_normal")) {
if (custom.varray.type().is<float3>()) {
if (custom.domain == AttrDomain::Point) {
r_data.store_varray(custom.varray.typed<float3>());
return;
}
if (custom.domain == AttrDomain::Face) {
mesh::normals_calc_verts(this->vert_positions(),
this->faces(),
this->corner_verts(),
this->vert_to_face_map(),
VArraySpan<float3>(custom.varray.typed<float3>()),
r_data.ensure_vector_size(this->verts_num));
return;
}
if (custom.domain == AttrDomain::Corner) {
mesh::mix_normals_corner_to_vert(this->vert_positions(),
this->faces(),
this->corner_verts(),
this->vert_to_face_map(),
VArraySpan<float3>(custom.varray.typed<float3>()),
r_data.ensure_vector_size(this->verts_num));
return;
}
}
else if (custom.varray.type().is<short2>() && custom.domain == AttrDomain::Corner) {
mesh::mix_normals_corner_to_vert(this->vert_positions(),
this->faces(),
this->corner_verts(),
this->vert_to_face_map(),
this->corner_normals(),
r_data.ensure_vector_size(this->verts_num));
return;
}
}
r_data.store_span(this->vert_normals_true());
});
return this->runtime->vert_normals_cache.data();
return this->runtime->vert_normals_cache.data().get_span();
}
blender::Span<blender::float3> Mesh::vert_normals_true() const
{
using namespace blender;
using namespace blender::bke;
this->runtime->vert_normals_true_cache.ensure([&](Vector<float3> &r_data) {
r_data.reinitialize(this->verts_num);
mesh::normals_calc_verts(this->vert_positions(),
this->faces(),
this->corner_verts(),
this->vert_to_face_map(),
this->face_normals(),
r_data);
});
return this->runtime->vert_normals_true_cache.data();
}
blender::Span<blender::float3> Mesh::face_normals() const
{
using namespace blender;
this->runtime->face_normals_cache.ensure([&](Vector<float3> &r_data) {
const Span<float3> positions = this->vert_positions();
const OffsetIndices faces = this->faces();
const Span<int> corner_verts = this->corner_verts();
r_data.reinitialize(faces.size());
bke::mesh::normals_calc_faces(positions, faces, corner_verts, r_data);
using namespace blender::bke;
this->runtime->face_normals_cache.ensure([&](NormalsCache &r_data) {
if (const GAttributeReader custom = this->attributes().lookup("custom_normal")) {
if (custom.varray.type().is<float3>()) {
if (custom.domain == AttrDomain::Face) {
r_data.store_varray(custom.varray.typed<float3>());
return;
}
if (custom.domain == AttrDomain::Point) {
mesh::mix_normals_vert_to_face(this->faces(),
this->corner_verts(),
VArraySpan<float3>(custom.varray.typed<float3>()),
r_data.ensure_vector_size(this->faces_num));
return;
}
if (custom.domain == AttrDomain::Corner) {
mesh::mix_normals_corner_to_face(this->faces(),
VArraySpan<float3>(custom.varray.typed<float3>()),
r_data.ensure_vector_size(this->faces_num));
return;
}
}
else if (custom.varray.type().is<short2>() && custom.domain == AttrDomain::Corner) {
mesh::mix_normals_corner_to_face(
this->faces(), this->corner_normals(), r_data.ensure_vector_size(this->faces_num));
return;
}
}
r_data.store_span(this->face_normals_true());
});
return this->runtime->face_normals_cache.data();
return this->runtime->face_normals_cache.data().get_span();
}
blender::Span<blender::float3> Mesh::face_normals_true() const
{
using namespace blender;
using namespace blender::bke;
this->runtime->face_normals_true_cache.ensure([&](Vector<float3> &r_data) {
r_data.reinitialize(this->faces_num);
mesh::normals_calc_faces(this->vert_positions(), this->faces(), this->corner_verts(), r_data);
});
return this->runtime->face_normals_true_cache.data();
}
blender::Span<blender::float3> Mesh::corner_normals() const
{
using namespace blender;
using namespace blender::bke;
this->runtime->corner_normals_cache.ensure([&](Vector<float3> &r_data) {
r_data.reinitialize(this->corners_num);
this->runtime->corner_normals_cache.ensure([&](NormalsCache &r_data) {
const OffsetIndices<int> faces = this->faces();
switch (this->normals_domain()) {
case MeshNormalDomain::Point: {
array_utils::gather(this->vert_normals(), this->corner_verts(), r_data.as_mutable_span());
MutableSpan<float3> data = r_data.ensure_vector_size(this->corners_num);
array_utils::gather(this->vert_normals(), this->corner_verts(), data);
break;
}
case MeshNormalDomain::Face: {
MutableSpan<float3> data = r_data.ensure_vector_size(this->corners_num);
const Span<float3> face_normals = this->face_normals();
threading::parallel_for(faces.index_range(), 1024, [&](const IndexRange range) {
for (const int i : range) {
r_data.as_mutable_span().slice(faces[i]).fill(face_normals[i]);
}
});
array_utils::gather_to_groups(faces, faces.index_range(), face_normals, data);
break;
}
case MeshNormalDomain::Corner: {
const AttributeAccessor attributes = this->attributes();
const GAttributeReader custom = attributes.lookup("custom_normal");
if (custom && custom.varray.type().is<float3>()) {
if (custom.domain == bke::AttrDomain::Corner) {
r_data.store_varray(custom.varray.typed<float3>());
}
return;
}
MutableSpan<float3> data = r_data.ensure_vector_size(this->corners_num);
const VArraySpan sharp_edges = *attributes.lookup<bool>("sharp_edge", AttrDomain::Edge);
const VArraySpan sharp_faces = *attributes.lookup<bool>("sharp_face", AttrDomain::Face);
const VArraySpan custom_normals = *attributes.lookup<short2>("custom_normal",
AttrDomain::Corner);
mesh::normals_calc_corners(this->vert_positions(),
this->edges(),
this->faces(),
this->corner_verts(),
this->corner_edges(),
this->corner_to_face_map(),
this->face_normals(),
this->face_normals_true(),
sharp_edges,
sharp_faces,
custom_normals,
VArraySpan<short2>(custom.varray.typed<short2>()),
nullptr,
r_data);
break;
data);
}
}
});
return this->runtime->corner_normals_cache.data();
return this->runtime->corner_normals_cache.data().get_span();
}
void BKE_lnor_spacearr_init(MLoopNorSpaceArray *lnors_spacearr,
@@ -1548,8 +1728,8 @@ static void mesh_set_custom_normals(Mesh &mesh,
mesh.faces(),
mesh.corner_verts(),
mesh.corner_edges(),
mesh.vert_normals(),
mesh.face_normals(),
mesh.vert_normals_true(),
mesh.face_normals_true(),
sharp_faces,
use_vertices,
r_custom_nors,

View File

@@ -321,7 +321,9 @@ void BKE_mesh_runtime_clear_geometry(Mesh *mesh)
mesh->runtime->vert_to_corner_map_cache.tag_dirty();
mesh->runtime->corner_to_face_map_cache.tag_dirty();
mesh->runtime->vert_normals_cache.tag_dirty();
mesh->runtime->vert_normals_true_cache.tag_dirty();
mesh->runtime->face_normals_cache.tag_dirty();
mesh->runtime->face_normals_true_cache.tag_dirty();
mesh->runtime->corner_normals_cache.tag_dirty();
mesh->runtime->loose_edges_cache.tag_dirty();
mesh->runtime->loose_verts_cache.tag_dirty();
@@ -340,6 +342,7 @@ void Mesh::tag_edges_split()
/* Triangulation didn't change because vertex positions and loop vertex indices didn't change. */
free_bvh_caches(*this->runtime);
this->runtime->vert_normals_cache.tag_dirty();
this->runtime->corner_normals_cache.tag_dirty();
this->runtime->subdiv_ccg.reset();
this->runtime->vert_to_face_offset_cache.tag_dirty();
this->runtime->vert_to_face_map_cache.tag_dirty();
@@ -366,11 +369,15 @@ void Mesh::tag_edges_split()
void Mesh::tag_sharpness_changed()
{
this->runtime->vert_normals_cache.tag_dirty();
this->runtime->face_normals_cache.tag_dirty();
this->runtime->corner_normals_cache.tag_dirty();
}
void Mesh::tag_custom_normals_changed()
{
this->runtime->vert_normals_cache.tag_dirty();
this->runtime->face_normals_cache.tag_dirty();
this->runtime->corner_normals_cache.tag_dirty();
}
@@ -378,6 +385,8 @@ void Mesh::tag_face_winding_changed()
{
this->runtime->vert_normals_cache.tag_dirty();
this->runtime->face_normals_cache.tag_dirty();
this->runtime->vert_normals_true_cache.tag_dirty();
this->runtime->face_normals_true_cache.tag_dirty();
this->runtime->corner_normals_cache.tag_dirty();
this->runtime->vert_to_corner_map_cache.tag_dirty();
this->runtime->shrinkwrap_boundary_cache.tag_dirty();
@@ -387,6 +396,8 @@ void Mesh::tag_positions_changed()
{
this->runtime->vert_normals_cache.tag_dirty();
this->runtime->face_normals_cache.tag_dirty();
this->runtime->vert_normals_true_cache.tag_dirty();
this->runtime->face_normals_true_cache.tag_dirty();
this->runtime->corner_normals_cache.tag_dirty();
this->runtime->shrinkwrap_boundary_cache.tag_dirty();
this->tag_positions_changed_no_normals();

View File

@@ -817,11 +817,11 @@ static const SharedCache<Vector<float3>> &vert_normals_cache_eval(const Object &
if (object_orig.mode & (OB_MODE_VERTEX_PAINT | OB_MODE_WEIGHT_PAINT)) {
if (const Mesh *mesh_eval = BKE_object_get_evaluated_mesh_no_subsurf(&object_eval)) {
if (mesh_topology_count_matches(*mesh_eval, mesh_orig)) {
return mesh_eval->runtime->vert_normals_cache;
return mesh_eval->runtime->vert_normals_true_cache;
}
}
if (const Mesh *mesh_eval = BKE_object_get_mesh_deform_eval(&object_eval)) {
return mesh_eval->runtime->vert_normals_cache;
return mesh_eval->runtime->vert_normals_true_cache;
}
}
@@ -830,7 +830,7 @@ static const SharedCache<Vector<float3>> &vert_normals_cache_eval(const Object &
return ss.vert_normals_deform;
}
return mesh_orig.runtime->vert_normals_cache;
return mesh_orig.runtime->vert_normals_true_cache;
}
static SharedCache<Vector<float3>> &vert_normals_cache_eval_for_write(Object &object_orig,
Object &object_eval)
@@ -848,11 +848,11 @@ static const SharedCache<Vector<float3>> &face_normals_cache_eval(const Object &
if (object_orig.mode & (OB_MODE_VERTEX_PAINT | OB_MODE_WEIGHT_PAINT)) {
if (const Mesh *mesh_eval = BKE_object_get_evaluated_mesh_no_subsurf(&object_eval)) {
if (mesh_topology_count_matches(*mesh_eval, mesh_orig)) {
return mesh_eval->runtime->face_normals_cache;
return mesh_eval->runtime->face_normals_true_cache;
}
}
if (const Mesh *mesh_eval = BKE_object_get_mesh_deform_eval(&object_eval)) {
return mesh_eval->runtime->face_normals_cache;
return mesh_eval->runtime->face_normals_true_cache;
}
}
@@ -861,7 +861,7 @@ static const SharedCache<Vector<float3>> &face_normals_cache_eval(const Object &
return ss.face_normals_deform;
}
return mesh_orig.runtime->face_normals_cache;
return mesh_orig.runtime->face_normals_true_cache;
}
static SharedCache<Vector<float3>> &face_normals_cache_eval_for_write(Object &object_orig,
Object &object_eval)

View File

@@ -268,6 +268,7 @@ class GVArraySpan : public GSpan {
public:
GVArraySpan();
GVArraySpan(GVArray varray);
template<typename T> GVArraySpan(VArray<T> varray) : GVArraySpan(GVArray(varray)) {}
GVArraySpan(GVArraySpan &&other);
~GVArraySpan();
GVArraySpan &operator=(GVArraySpan &&other);

View File

@@ -451,6 +451,15 @@ static bke::MeshNormalDomain bmesh_normals_domain(BMesh *bm)
return bke::MeshNormalDomain::Point;
}
if (CustomData_has_layer_named(&bm->vdata, CD_PROP_FLOAT3, "custom_normal")) {
return bke::MeshNormalDomain::Point;
}
if (CustomData_has_layer_named(&bm->pdata, CD_PROP_FLOAT3, "custom_normal")) {
return bke::MeshNormalDomain::Face;
}
if (CustomData_has_layer_named(&bm->ldata, CD_PROP_FLOAT3, "custom_normal")) {
return bke::MeshNormalDomain::Corner;
}
if (CustomData_has_layer_named(&bm->ldata, CD_PROP_INT16_2D, "custom_normal")) {
return bke::MeshNormalDomain::Corner;
}
@@ -486,6 +495,13 @@ void mesh_render_data_update_corner_normals(MeshRenderData &mr)
mr.corner_normals = mr.mesh->corner_normals();
}
else {
if (mr.bm_free_normal_offset_vert != -1 || mr.bm_free_normal_offset_face != -1 ||
mr.bm_free_normal_offset_corner != -1)
{
/* If there are free custom normals they should be used directly. */
mr.bm_loop_normals = {};
return;
}
mr.bm_loop_normals.reinitialize(mr.corners_num);
const int clnors_offset = CustomData_get_offset_named(
&mr.bm->ldata, CD_PROP_INT16_2D, "custom_normal");
@@ -549,11 +565,22 @@ MeshRenderData mesh_render_data_create(Object &object,
/* If there is no distinct cage, hide unmapped edges that can't be selected. */
mr.hide_unmapped_edges = !do_final || &mesh == eval_cage;
mr.bm_free_normal_offset_vert = CustomData_get_offset_named(
&mr.bm->vdata, CD_PROP_FLOAT3, "custom_normal");
mr.bm_free_normal_offset_face = CustomData_get_offset_named(
&mr.bm->pdata, CD_PROP_FLOAT3, "custom_normal");
mr.bm_free_normal_offset_corner = CustomData_get_offset_named(
&mr.bm->ldata, CD_PROP_FLOAT3, "custom_normal");
if (bke::EditMeshData *emd = mr.edit_data) {
if (!emd->vert_positions.is_empty()) {
mr.bm_vert_coords = mr.edit_data->vert_positions;
mr.bm_vert_normals = BKE_editmesh_cache_ensure_vert_normals(*mr.edit_bmesh, *emd);
mr.bm_face_normals = BKE_editmesh_cache_ensure_face_normals(*mr.edit_bmesh, *emd);
if (mr.bm_free_normal_offset_vert == -1) {
mr.bm_vert_normals = BKE_editmesh_cache_ensure_vert_normals(*mr.edit_bmesh, *emd);
}
if (mr.bm_free_normal_offset_face == -1) {
mr.bm_face_normals = BKE_editmesh_cache_ensure_face_normals(*mr.edit_bmesh, *emd);
}
}
}

View File

@@ -75,6 +75,9 @@ struct MeshRenderData {
Span<float3> bm_vert_normals;
Span<float3> bm_face_normals;
Array<float3> bm_loop_normals;
int bm_free_normal_offset_vert = -1;
int bm_free_normal_offset_face = -1;
int bm_free_normal_offset_corner = -1;
const int *orig_index_vert;
const int *orig_index_edge;
@@ -157,6 +160,9 @@ BLI_INLINE const float *bm_vert_co_get(const MeshRenderData &mr, const BMVert *e
BLI_INLINE const float *bm_vert_no_get(const MeshRenderData &mr, const BMVert *eve)
{
if (mr.bm_free_normal_offset_vert != -1) {
return BM_ELEM_CD_GET_FLOAT_P(eve, mr.bm_free_normal_offset_vert);
}
if (!mr.bm_vert_normals.is_empty()) {
return mr.bm_vert_normals[BM_elem_index_get(eve)];
}
@@ -165,6 +171,9 @@ BLI_INLINE const float *bm_vert_no_get(const MeshRenderData &mr, const BMVert *e
BLI_INLINE const float *bm_face_no_get(const MeshRenderData &mr, const BMFace *efa)
{
if (mr.bm_free_normal_offset_face != -1) {
return BM_ELEM_CD_GET_FLOAT_P(efa, mr.bm_free_normal_offset_face);
}
if (!mr.bm_face_normals.is_empty()) {
return mr.bm_face_normals[BM_elem_index_get(efa)];
}

View File

@@ -139,7 +139,21 @@ template<typename GPUType>
static void extract_vert_normals_bm(const MeshRenderData &mr, MutableSpan<GPUType> normals)
{
const BMesh &bm = *mr.bm;
if (!mr.bm_vert_normals.is_empty()) {
if (mr.bm_free_normal_offset_vert != -1) {
threading::parallel_for(IndexRange(bm.totface), 2048, [&](const IndexRange range) {
for (const int face_index : range) {
const BMFace &face = *BM_face_at_index(&const_cast<BMesh &>(bm), face_index);
const BMLoop *loop = BM_FACE_FIRST_LOOP(&face);
const IndexRange face_range(BM_elem_index_get(loop), face.len);
for (const int corner : face_range) {
normals[corner] = gpu::convert_normal<GPUType>(
BM_ELEM_CD_GET_FLOAT_P(loop->v, mr.bm_free_normal_offset_vert));
loop = loop->next;
}
}
});
}
else if (!mr.bm_vert_normals.is_empty()) {
Array<GPUType> vert_normals_converted(mr.bm_vert_normals.size());
gpu::convert_normals(mr.bm_vert_normals, vert_normals_converted.as_mutable_span());
threading::parallel_for(IndexRange(bm.totface), 2048, [&](const IndexRange range) {
@@ -173,7 +187,18 @@ template<typename GPUType>
static void extract_face_normals_bm(const MeshRenderData &mr, MutableSpan<GPUType> normals)
{
const BMesh &bm = *mr.bm;
if (!mr.bm_face_normals.is_empty()) {
if (mr.bm_free_normal_offset_face != -1) {
threading::parallel_for(IndexRange(bm.totface), 2048, [&](const IndexRange range) {
for (const int face_index : range) {
const BMFace &face = *BM_face_at_index(&const_cast<BMesh &>(bm), face_index);
const IndexRange face_range(BM_elem_index_get(BM_FACE_FIRST_LOOP(&face)), face.len);
normals.slice(face_range)
.fill(gpu::convert_normal<GPUType>(
BM_ELEM_CD_GET_FLOAT_P(&face, mr.bm_free_normal_offset_face)));
}
});
}
else if (!mr.bm_face_normals.is_empty()) {
threading::parallel_for(IndexRange(bm.totface), 2048, [&](const IndexRange range) {
for (const int face_index : range) {
const BMFace &face = *BM_face_at_index(&const_cast<BMesh &>(bm), face_index);
@@ -222,6 +247,20 @@ static void extract_normals_bm(const MeshRenderData &mr, MutableSpan<GPUType> no
else if (mr.normals_domain == bke::MeshNormalDomain::Point) {
extract_vert_normals_bm(mr, normals);
}
else if (mr.bm_free_normal_offset_corner != -1) {
threading::parallel_for(IndexRange(bm.totface), 2048, [&](const IndexRange range) {
for (const int face_index : range) {
const BMFace &face = *BM_face_at_index(&const_cast<BMesh &>(bm), face_index);
const BMLoop *loop = BM_FACE_FIRST_LOOP(&face);
const IndexRange face_range(BM_elem_index_get(loop), face.len);
for (const int corner : face_range) {
normals[corner] = gpu::convert_normal<GPUType>(
BM_ELEM_CD_GET_FLOAT_P(loop, mr.bm_free_normal_offset_corner));
loop = loop->next;
}
}
});
}
else if (!mr.bm_loop_normals.is_empty()) {
gpu::convert_normals(mr.bm_loop_normals, normals);
}

View File

@@ -12,6 +12,7 @@
#include "BLI_math_matrix.hh"
#include "BLI_noise.hh"
#include "BKE_attribute.hh"
#include "BKE_curves.hh"
#include "BKE_customdata.hh"
#include "BKE_geometry_nodes_gizmos_transforms.hh"
@@ -107,6 +108,8 @@ struct MeshRealizeInfo {
/** Vertex ids stored on the mesh. If there are no ids, this #Span is empty. */
Span<int> stored_vertex_ids;
VArray<int> material_indices;
/** Custom normals are rotated based on each instance's transformation. */
GVArraySpan custom_normal;
};
struct RealizeMeshTask {
@@ -209,6 +212,81 @@ struct AllPointCloudsInfo {
bool create_radius_attribute = false;
};
static bke::AttrDomain normal_domain_to_domain(bke::MeshNormalDomain domain)
{
switch (domain) {
case bke::MeshNormalDomain::Point:
return bke::AttrDomain::Point;
case bke::MeshNormalDomain::Face:
return bke::AttrDomain::Face;
case bke::MeshNormalDomain::Corner:
return bke::AttrDomain::Corner;
}
BLI_assert_unreachable();
return bke::AttrDomain::Point;
}
constexpr bke::AttributeMetaData CORNER_FAN_META_DATA{bke::AttrDomain::Corner, CD_PROP_INT16_2D};
/** Tracks the storage format for the resulting mesh based on the combination of input meshes. */
struct MeshNormalInfo {
enum class Output : int8_t { None, CornerFan, Free };
Output result_type = Output::None;
std::optional<bke::AttrDomain> result_domain;
void add_no_custom_normals(const bke::MeshNormalDomain domain)
{
if (result_type == Output::None) {
return;
}
this->add_free_normals(normal_domain_to_domain(domain));
}
void add_corner_fan_normals()
{
this->result_domain = bke::AttrDomain::Corner;
if (this->result_type == Output::None) {
this->result_type = Output::CornerFan;
}
}
void add_free_normals(const bke::AttrDomain domain)
{
if (this->result_domain) {
/* Any combination of point/face domains puts the result normals on the corner domain. */
if (this->result_domain != domain) {
this->result_domain = bke::AttrDomain::Corner;
}
}
else {
this->result_domain = domain;
}
this->result_type = Output::Free;
}
void add_mesh(const Mesh &mesh)
{
const bke::AttributeAccessor attributes = mesh.attributes();
const std::optional<bke::AttributeMetaData> custom_normal = attributes.lookup_meta_data(
"custom_normal");
if (!custom_normal) {
this->add_no_custom_normals(mesh.normals_domain());
return;
}
if (custom_normal->data_type == CD_PROP_FLOAT3) {
if (custom_normal->domain == bke::AttrDomain::Edge) {
/* Skip invalid storage on the edge domain.*/
this->add_no_custom_normals(mesh.normals_domain());
return;
}
this->add_free_normals(custom_normal->domain);
}
else if (*custom_normal == CORNER_FAN_META_DATA) {
this->add_corner_fan_normals();
}
}
};
struct AllMeshesInfo {
/** Ordering of all attributes that are propagated to the output mesh generically. */
OrderedAttributes attributes;
@@ -220,6 +298,7 @@ struct AllMeshesInfo {
VectorSet<Material *> materials;
bool create_id_attribute = false;
bool create_material_index_attribute = false;
MeshNormalInfo custom_normal_info;
/** True if we know that there are no loose edges in any of the input meshes. */
bool no_loose_edges_hint = false;
@@ -1301,6 +1380,7 @@ static OrderedAttributes gather_generic_mesh_attributes_to_propagate(
attributes_to_propagate.remove(".edge_verts");
attributes_to_propagate.remove(".corner_vert");
attributes_to_propagate.remove(".corner_edge");
attributes_to_propagate.remove("custom_normal");
r_create_id = attributes_to_propagate.pop_try("id").has_value();
r_create_material_index = attributes_to_propagate.pop_try("material_index").has_value();
OrderedAttributes ordered_attributes;
@@ -1351,6 +1431,11 @@ static AllMeshesInfo preprocess_meshes(const bke::GeometrySet &geometry_set,
}
}
}
for (const Mesh *mesh : info.order) {
info.custom_normal_info.add_mesh(*mesh);
}
info.create_material_index_attribute |= info.materials.size() > 1;
info.realize_info.reinitialize(info.order.size());
for (const int mesh_index : info.realize_info.index_range()) {
@@ -1396,6 +1481,35 @@ static AllMeshesInfo preprocess_meshes(const bke::GeometrySet &geometry_set,
}
mesh_info.material_indices = *attributes.lookup_or_default<int>(
"material_index", bke::AttrDomain::Face, 0);
switch (info.custom_normal_info.result_type) {
case MeshNormalInfo::Output::None: {
break;
}
case MeshNormalInfo::Output::CornerFan: {
if (attributes.lookup_meta_data("custom_normal") == CORNER_FAN_META_DATA) {
mesh_info.custom_normal = *attributes.lookup<short2>("custom_normal",
bke::AttrDomain::Corner);
}
break;
}
case MeshNormalInfo::Output::Free: {
switch (*info.custom_normal_info.result_domain) {
case bke::AttrDomain::Point:
mesh_info.custom_normal = VArray<float3>::ForSpan(mesh->vert_normals());
break;
case bke::AttrDomain::Face:
mesh_info.custom_normal = VArray<float3>::ForSpan(mesh->face_normals());
break;
case bke::AttrDomain::Corner:
mesh_info.custom_normal = VArray<float3>::ForSpan(mesh->corner_normals());
break;
default:
BLI_assert_unreachable();
}
break;
}
}
}
info.no_loose_edges_hint = std::all_of(
@@ -1424,7 +1538,8 @@ static void execute_realize_mesh_task(const RealizeInstancesOptions &options,
MutableSpan<int> all_dst_corner_verts,
MutableSpan<int> all_dst_corner_edges,
MutableSpan<int> all_dst_vertex_ids,
MutableSpan<int> all_dst_material_indices)
MutableSpan<int> all_dst_material_indices,
GSpanAttributeWriter &all_dst_custom_normals)
{
const MeshRealizeInfo &mesh_info = *task.mesh_info;
const Mesh &mesh = *mesh_info.mesh;
@@ -1504,26 +1619,46 @@ static void execute_realize_mesh_task(const RealizeInstancesOptions &options,
all_dst_vertex_ids.slice(task.start_indices.vertex, mesh.verts_num));
}
copy_generic_attributes_to_result(
mesh_info.attributes,
task.attribute_fallbacks,
ordered_attributes,
[&](const bke::AttrDomain domain) {
switch (domain) {
case bke::AttrDomain::Point:
return dst_vert_range;
case bke::AttrDomain::Edge:
return dst_edge_range;
case bke::AttrDomain::Face:
return dst_face_range;
case bke::AttrDomain::Corner:
return dst_loop_range;
default:
BLI_assert_unreachable();
return IndexRange();
}
},
dst_attribute_writers);
const auto domain_to_range = [&](const bke::AttrDomain domain) {
switch (domain) {
case bke::AttrDomain::Point:
return dst_vert_range;
case bke::AttrDomain::Edge:
return dst_edge_range;
case bke::AttrDomain::Face:
return dst_face_range;
case bke::AttrDomain::Corner:
return dst_loop_range;
default:
BLI_assert_unreachable();
return IndexRange();
}
};
if (all_dst_custom_normals) {
if (all_dst_custom_normals.span.type().is<short2>()) {
if (mesh_info.custom_normal.is_empty()) {
all_dst_custom_normals.span.typed<short2>().slice(dst_loop_range).fill(short2(0));
}
else {
all_dst_custom_normals.span.typed<short2>()
.slice(dst_loop_range)
.copy_from(mesh_info.custom_normal.typed<short2>());
}
}
else {
const IndexRange dst_range = domain_to_range(all_dst_custom_normals.domain);
copy_transformed_normals(mesh_info.custom_normal.typed<float3>(),
task.transform,
all_dst_custom_normals.span.typed<float3>().slice(dst_range));
}
}
copy_generic_attributes_to_result(mesh_info.attributes,
task.attribute_fallbacks,
ordered_attributes,
domain_to_range,
dst_attribute_writers);
}
static void copy_vertex_group_names(Mesh &dst_mesh,
@@ -1568,8 +1703,7 @@ static void execute_realize_mesh_tasks(const RealizeInstancesOptions &options,
const RealizeMeshTask &task = tasks.first();
Mesh *new_mesh = BKE_mesh_copy_for_eval(*task.mesh_info->mesh);
if (!skip_transform(task.transform)) {
transform_positions(task.transform, new_mesh->vert_positions_for_write());
new_mesh->tag_positions_changed();
bke::mesh_transform(*new_mesh, task.transform, false);
}
add_instance_attributes_to_single_geometry(
ordered_attributes, task.attribute_fallbacks, new_mesh->attributes_for_write());
@@ -1623,6 +1757,24 @@ static void execute_realize_mesh_tasks(const RealizeInstancesOptions &options,
"material_index", bke::AttrDomain::Face);
}
GSpanAttributeWriter custom_normals;
switch (all_meshes_info.custom_normal_info.result_type) {
case MeshNormalInfo::Output::None: {
break;
}
case MeshNormalInfo::Output::CornerFan: {
custom_normals = dst_attributes.lookup_or_add_for_write_only_span(
"custom_normal", bke::AttrDomain::Corner, CD_PROP_INT16_2D);
break;
}
case MeshNormalInfo::Output::Free: {
const bke::AttrDomain domain = *all_meshes_info.custom_normal_info.result_domain;
custom_normals = dst_attributes.lookup_or_add_for_write_only_span(
"custom_normal", domain, CD_PROP_FLOAT3);
break;
}
}
/* Prepare generic output attributes. */
Vector<GSpanAttributeWriter> dst_attribute_writers;
for (const int attribute_index : ordered_attributes.index_range()) {
@@ -1662,7 +1814,8 @@ static void execute_realize_mesh_tasks(const RealizeInstancesOptions &options,
dst_corner_verts,
dst_corner_edges,
vertex_ids.span,
material_indices.span);
material_indices.span,
custom_normals);
}
});
@@ -1672,6 +1825,7 @@ static void execute_realize_mesh_tasks(const RealizeInstancesOptions &options,
}
vertex_ids.finish();
material_indices.finish();
custom_normals.finish();
if (all_meshes_info.no_loose_edges_hint) {
dst_mesh->tag_loose_edges_none();

View File

@@ -388,17 +388,21 @@ typedef struct Mesh {
* Normal direction of faces, defined by positions and the winding direction of face corners.
*/
blender::Span<blender::float3> face_normals() const;
blender::Span<blender::float3> face_normals_true() const;
/**
* Normal direction of vertices, defined as the weighted average of face normals
* surrounding each vertex and the normalized position for loose vertices.
*/
blender::Span<blender::float3> vert_normals() const;
blender::Span<blender::float3> vert_normals_true() const;
/**
* Normal direction at each face corner. Defined by a combination of face normals, vertex
* normals, the `sharp_edge` and `sharp_face` attributes, and potentially by custom normals.
*
* \note Because of the large memory requirements of storing normals per face corner, prefer
* using #face_normals() or #vert_normals() when possible (see #normals_domain()).
* using #face_normals() or #vert_normals() when possible (see #normals_domain()). For this
* reason, the "true" face corner normals aren't cached, since they're just the same as the
* corresponding face normals.
*/
blender::Span<blender::float3> corner_normals() const;

View File

@@ -196,6 +196,8 @@ static void rna_Mesh_update(Mesh *mesh,
mesh->runtime->vert_normals_cache.tag_dirty();
mesh->runtime->face_normals_cache.tag_dirty();
mesh->runtime->corner_normals_cache.tag_dirty();
mesh->runtime->vert_normals_true_cache.tag_dirty();
mesh->runtime->face_normals_true_cache.tag_dirty();
DEG_id_tag_update(&mesh->id, 0);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, mesh);

View File

@@ -12870,6 +12870,7 @@ static void rna_def_nodes(BlenderRNA *brna)
define("GeometryNode", "GeometryNodeSetInstanceTransform");
define("GeometryNode", "GeometryNodeSetMaterial");
define("GeometryNode", "GeometryNodeSetMaterialIndex");
define("GeometryNode", "GeometryNodeSetMeshNormal");
define("GeometryNode", "GeometryNodeSetPointRadius");
define("GeometryNode", "GeometryNodeSetPosition");
define("GeometryNode", "GeometryNodeSetShadeSmooth");

View File

@@ -314,7 +314,7 @@ static void normalEditModifier_do_radial(NormalEditModifierData *enmd,
}
if (do_facenors_fix) {
faces_check_flip(*mesh, nos, mesh->face_normals());
faces_check_flip(*mesh, nos, mesh->face_normals_true());
}
const bke::AttributeAccessor attributes = mesh->attributes();
const VArraySpan sharp_faces = *attributes.lookup<bool>("sharp_face", bke::AttrDomain::Face);
@@ -323,8 +323,8 @@ static void normalEditModifier_do_radial(NormalEditModifierData *enmd,
faces,
corner_verts,
corner_edges,
mesh->vert_normals(),
mesh->face_normals(),
mesh->vert_normals_true(),
mesh->face_normals_true(),
sharp_faces,
sharp_edges,
nos,
@@ -419,7 +419,7 @@ static void normalEditModifier_do_directional(NormalEditModifierData *enmd,
}
if (do_facenors_fix) {
faces_check_flip(*mesh, nos, mesh->face_normals());
faces_check_flip(*mesh, nos, mesh->face_normals_true());
}
const bke::AttributeAccessor attributes = mesh->attributes();
const VArraySpan sharp_faces = *attributes.lookup<bool>("sharp_face", bke::AttrDomain::Face);
@@ -428,8 +428,8 @@ static void normalEditModifier_do_directional(NormalEditModifierData *enmd,
faces,
corner_verts,
corner_edges,
mesh->vert_normals(),
mesh->face_normals(),
mesh->vert_normals_true(),
mesh->face_normals_true(),
sharp_faces,
sharp_edges,
nos,
@@ -512,7 +512,7 @@ static Mesh *normalEditModifier_do(NormalEditModifierData *enmd,
corner_verts,
corner_edges,
result->corner_to_face_map(),
result->face_normals(),
result->face_normals_true(),
sharp_edges.span,
sharp_faces,
custom_nors_dst.span,

View File

@@ -519,7 +519,7 @@ static Mesh *modify_mesh(ModifierData *md, const ModifierEvalContext *ctx, Mesh
wn_data.clnors = clnors.span;
wn_data.faces = faces;
wn_data.face_normals = mesh->face_normals();
wn_data.face_normals = mesh->face_normals_true();
wn_data.sharp_faces = *attributes.lookup<bool>("sharp_face", bke::AttrDomain::Face);
wn_data.face_strength = static_cast<const int *>(CustomData_get_layer_named(
&result->face_data, CD_PROP_INT32, MOD_WEIGHTEDNORMALS_FACEWEIGHT_CDLAYER_ID));

View File

@@ -199,6 +199,7 @@ set(SRC
nodes/node_geo_set_instance_transform.cc
nodes/node_geo_set_material.cc
nodes/node_geo_set_material_index.cc
nodes/node_geo_set_mesh_normal.cc
nodes/node_geo_set_point_radius.cc
nodes/node_geo_set_position.cc
nodes/node_geo_set_shade_smooth.cc

View File

@@ -11,6 +11,10 @@ namespace blender::nodes::node_geo_input_normal_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_output<decl::Vector>("Normal").field_source();
b.add_output<decl::Vector>("True Normal")
.field_source()
.description(
"For meshes, outputs normals without custom normal attributes taken into account");
}
static void node_layout_ex(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr)
@@ -21,8 +25,15 @@ static void node_layout_ex(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr)
static void node_geo_exec(GeoNodeExecParams params)
{
const bool legacy_corner_normals = bool(params.node().custom1);
Field<float3> normal_field{std::make_shared<bke::NormalFieldInput>(legacy_corner_normals)};
params.set_output("Normal", std::move(normal_field));
if (params.output_is_required("Normal")) {
params.set_output(
"Normal",
Field<float3>{std::make_shared<bke::NormalFieldInput>(legacy_corner_normals, false)});
}
if (params.output_is_required("True Normal")) {
params.set_output("True Normal",
Field<float3>{std::make_shared<bke::NormalFieldInput>(false, true)});
}
}
static void node_register()

View File

@@ -0,0 +1,225 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_mesh.hh"
#include "UI_interface.hh"
#include "UI_resources.hh"
#include "NOD_rna_define.hh"
#include "RNA_enum_types.hh"
#include "node_geometry_util.hh"
namespace blender::nodes::node_geo_set_mesh_normal_cc {
enum class Mode {
Sharpness = 0,
Free = 1,
CornerFanSpace = 2,
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.use_custom_socket_order();
b.allow_any_socket_order();
b.add_default_layout();
b.add_input<decl::Geometry>("Mesh").supported_type(GeometryComponent::Type::Mesh);
b.add_output<decl::Geometry>("Mesh").propagate_all().align_with_previous();
if (const bNode *node = b.node_or_null()) {
switch (Mode(node->custom1)) {
case Mode::Sharpness:
b.add_input<decl::Bool>("Remove Custom").default_value(true);
b.add_input<decl::Bool>("Edge Sharpness").supports_field();
b.add_input<decl::Bool>("Face Sharpness").supports_field();
break;
case Mode::Free:
case Mode::CornerFanSpace:
b.add_input<decl::Vector>("Custom Normal")
.subtype(PROP_XYZ)
.implicit_field(nodes::implicit_field_inputs::normal)
.hide_value();
break;
}
}
}
static void node_layout(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr)
{
const bNode &node = *static_cast<const bNode *>(ptr->data);
uiItemR(layout, ptr, "mode", UI_ITEM_NONE, "", ICON_NONE);
if (Mode(node.custom1) == Mode::Free) {
uiItemR(layout, ptr, "domain", UI_ITEM_NONE, "", ICON_NONE);
}
}
static void node_init(bNodeTree * /*tree*/, bNode *node)
{
node->custom1 = int16_t(Mode::Sharpness);
node->custom2 = int16_t(bke::AttrDomain::Point);
}
static void node_geo_exec(GeoNodeExecParams params)
{
const bNode &node = params.node();
const Mode mode = static_cast<Mode>(node.custom1);
GeometrySet geometry_set = params.extract_input<GeometrySet>("Mesh");
bool add_sharpness_and_corner_fan_info = false;
switch (mode) {
case Mode::Sharpness: {
const bool remove_custom = params.extract_input<bool>("Remove Custom");
const fn::Field sharp_edge = params.extract_input<fn::Field<bool>>("Edge Sharpness");
const fn::Field sharp_face = params.extract_input<fn::Field<bool>>("Face Sharpness");
geometry_set.modify_geometry_sets([&](GeometrySet &geometry_set) {
if (Mesh *mesh = geometry_set.get_mesh_for_write()) {
/* Evaluate both fields before storing the result to avoid one attribute change
* potentially affecting the other field evaluation. */
const bke::MeshFieldContext edge_context(*mesh, bke::AttrDomain::Edge);
const bke::MeshFieldContext face_context(*mesh, bke::AttrDomain::Face);
fn::FieldEvaluator edge_evaluator(edge_context, mesh->edges_num);
fn::FieldEvaluator face_evaluator(face_context, mesh->faces_num);
edge_evaluator.add(sharp_edge);
face_evaluator.add(sharp_face);
edge_evaluator.evaluate();
face_evaluator.evaluate();
const IndexMask edge_values = edge_evaluator.get_evaluated_as_mask(0);
const IndexMask face_values = face_evaluator.get_evaluated_as_mask(0);
bke::MutableAttributeAccessor attributes = mesh->attributes_for_write();
if (edge_values.is_empty()) {
attributes.remove("sharp_edge");
}
else {
bke::SpanAttributeWriter attr = attributes.lookup_or_add_for_write_only_span<bool>(
"sharp_edge", bke::AttrDomain::Edge);
edge_values.to_bools(attr.span);
attr.finish();
}
if (face_values.is_empty()) {
attributes.remove("sharp_face");
}
else {
bke::SpanAttributeWriter attr = attributes.lookup_or_add_for_write_only_span<bool>(
"sharp_face", bke::AttrDomain::Face);
face_values.to_bools(attr.span);
attr.finish();
}
if (remove_custom) {
attributes.remove("custom_normal");
}
else {
if (const std::optional<bke::AttributeMetaData> meta_data =
attributes.lookup_meta_data("custom_normal"))
{
if (meta_data->domain == bke::AttrDomain::Corner &&
meta_data->data_type == CD_PROP_INT16_2D)
{
add_sharpness_and_corner_fan_info = true;
}
}
}
}
});
break;
}
case Mode::Free: {
const fn::Field custom_normal = params.extract_input<fn::Field<float3>>("Custom Normal");
geometry_set.modify_geometry_sets([&](GeometrySet &geometry_set) {
if (Mesh *mesh = geometry_set.get_mesh_for_write()) {
const bke::AttrDomain domain = bke::AttrDomain(node.custom2);
bke::try_capture_field_on_geometry(mesh->attributes_for_write(),
bke::MeshFieldContext(*mesh, domain),
"custom_normal",
domain,
fn::make_constant_field(true),
custom_normal);
}
});
break;
}
case Mode::CornerFanSpace: {
const fn::Field custom_normal = params.extract_input<fn::Field<float3>>("Custom Normal");
geometry_set.modify_geometry_sets([&](GeometrySet &geometry_set) {
if (Mesh *mesh = geometry_set.get_mesh_for_write()) {
const bke::MeshFieldContext context(*mesh, bke::AttrDomain::Corner);
fn::FieldEvaluator evaluator(context, mesh->corners_num);
Array<float3> corner_normals(mesh->corners_num);
evaluator.add_with_destination<float3>(custom_normal, corner_normals);
evaluator.evaluate();
bke::mesh_set_custom_normals(*mesh, corner_normals);
}
});
break;
}
}
if (add_sharpness_and_corner_fan_info) {
params.error_message_add(NodeWarningType::Info,
"Adjusting sharpness with \"Tangent Space\" custom normals "
"may lead to unexpected results");
}
params.set_output("Mesh", std::move(geometry_set));
}
static void node_rna(StructRNA *srna)
{
static const EnumPropertyItem mode_items[] = {
{int(Mode::Sharpness),
"SHARPNESS",
0,
"Sharpness",
"Store the sharpness of each face or edge. Similar to the \"Shade Smooth\" and \"Shade "
"Flat\" operators."},
{int(Mode::Free),
"FREE",
0,
"Free",
"Store custom normals as simple vectors in the local space of the mesh. Values are not "
"necessarily updated automatically later on as the mesh is deformed."},
{int(Mode::CornerFanSpace),
"TANGENT_SPACE",
0,
"Tangent Space",
"Store normals in a deformation dependent custom transformation space. This method is "
"slower, but can be better when subsequent operations change the mesh without handling "
"normals specifically."},
{0, nullptr, 0, nullptr, nullptr},
};
RNA_def_node_enum(srna,
"mode",
"Mode",
"Storage mode for custom normal data",
mode_items,
NOD_inline_enum_accessors(custom1));
RNA_def_node_enum(srna,
"domain",
"Domain",
"Attribute domain to store free custom normals",
rna_enum_attribute_domain_only_mesh_no_edge_items,
NOD_inline_enum_accessors(custom2));
}
static void node_register()
{
static blender::bke::bNodeType ntype;
geo_node_type_base(&ntype, "GeometryNodeSetMeshNormal");
ntype.ui_name = "Set Mesh Normal";
ntype.ui_description = "Store a normal vector for each mesh element";
ntype.nclass = NODE_CLASS_GEOMETRY;
ntype.declare = node_declare;
ntype.geometry_node_execute = node_geo_exec;
ntype.initfunc = node_init;
ntype.draw_buttons = node_layout;
blender::bke::node_register_type(ntype);
node_rna(ntype.rna_ext.srna);
}
NOD_REGISTER_NODE(node_register)
} // namespace blender::nodes::node_geo_set_mesh_normal_cc