diff --git a/scripts/startup/bl_ui/node_add_menu_geometry.py b/scripts/startup/bl_ui/node_add_menu_geometry.py index fd7f96ae64c..e95be44047e 100644 --- a/scripts/startup/bl_ui/node_add_menu_geometry.py +++ b/scripts/startup/bl_ui/node_add_menu_geometry.py @@ -846,6 +846,7 @@ class NODE_MT_gn_mesh_uv_base(node_add_menu.NodeMenu): def draw(self, _context): layout = self.layout self.node_operator(layout, "GeometryNodeUVPackIslands") + self.node_operator(layout, "GeometryNodeUVTangent") self.node_operator(layout, "GeometryNodeUVUnwrap") self.draw_assets_for_catalog(layout, self.menu_path) diff --git a/source/blender/makesrna/intern/rna_nodetree.cc b/source/blender/makesrna/intern/rna_nodetree.cc index a50f628e8de..78f7b9b4571 100644 --- a/source/blender/makesrna/intern/rna_nodetree.cc +++ b/source/blender/makesrna/intern/rna_nodetree.cc @@ -10224,6 +10224,7 @@ static void rna_def_nodes(BlenderRNA *brna) define("GeometryNode", "GeometryNodeTriangulate"); define("GeometryNode", "GeometryNodeTrimCurve"); define("GeometryNode", "GeometryNodeUVPackIslands"); + define("GeometryNode", "GeometryNodeUVTangent"); define("GeometryNode", "GeometryNodeUVUnwrap"); define("GeometryNode", "GeometryNodeVertexOfCorner"); define("GeometryNode", "GeometryNodeViewer", rna_def_geo_viewer); diff --git a/source/blender/nodes/geometry/CMakeLists.txt b/source/blender/nodes/geometry/CMakeLists.txt index 66c1d94ab32..4b455791989 100644 --- a/source/blender/nodes/geometry/CMakeLists.txt +++ b/source/blender/nodes/geometry/CMakeLists.txt @@ -242,6 +242,7 @@ set(SRC nodes/node_geo_translate_instances.cc nodes/node_geo_triangulate.cc nodes/node_geo_uv_pack_islands.cc + nodes/node_geo_uv_tangent.cc nodes/node_geo_uv_unwrap.cc nodes/node_geo_viewer.cc nodes/node_geo_viewport_transform.cc diff --git a/source/blender/nodes/geometry/nodes/node_geo_uv_tangent.cc b/source/blender/nodes/geometry/nodes/node_geo_uv_tangent.cc new file mode 100644 index 00000000000..089ccd7b053 --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_uv_tangent.cc @@ -0,0 +1,247 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_mesh.hh" +#include "BKE_mesh_tangent.hh" + +#include "BLI_math_vector.hh" + +#include "node_geometry_util.hh" + +namespace blender::nodes::node_geo_uv_tangent_cc { + +enum class Method { + Exact = 0, + Fast = 1, +}; + +static EnumPropertyItem method_items[] = { + {int(Method::Exact), + "EXACT", + 0, + "Exact", + "Calculation using the MikkTSpace library, consistent with tangents used elsewhere in " + "Blender"}, + {int(Method::Fast), + "FAST", + 0, + "Fast", + "Significantly faster method that approximates tangents interpolated across face corners " + "with matching UVs. For a value actually tangential to the surface, use the cross product " + "with the normal."}, + {0, nullptr, 0, nullptr, nullptr}, +}; + +static void node_declare(NodeDeclarationBuilder &b) +{ + b.add_input("Method").static_items(method_items); + b.add_input("UV").dimensions(2).subtype(PROP_XYZ).supports_field(); + b.add_output("Tangent").field_source_reference_all(); +} + +static float3 compute_triangle_tangent(const float3 &p1, + const float3 &p2, + const float3 &p3, + const float2 &uv1, + const float2 &uv2, + const float2 &uv3) +{ + const float x1 = p2.x - p1.x; + const float x2 = p3.x - p1.x; + const float y1 = p2.y - p1.y; + const float y2 = p3.y - p1.y; + const float z1 = p2.z - p1.z; + const float z2 = p3.z - p1.z; + const float s1 = uv2.x - uv1.x; + const float s2 = uv3.x - uv1.x; + const float t1 = uv2.y - uv1.y; + const float t2 = uv3.y - uv1.y; + const float r = 1.0f / (s1 * t2 - s2 * t1); + const float3 tangent((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r); + return tangent; +} + +static void calc_uv_tangents_simple(const Span positions, + const Span corner_verts, + const Span corner_tris, + const GroupedSpan vert_to_corners_map, + const Span uvs, + MutableSpan r_corner_tangents) +{ + BLI_assert(r_corner_tangents.size() == corner_verts.size()); + + /* Compute a tangent vector for each triangle. */ + threading::parallel_for(corner_tris.index_range(), 256, [&](const IndexRange range) { + for (const int tri_i : range) { + const int3 &tri = corner_tris[tri_i]; + const float3 tangent = compute_triangle_tangent(positions[corner_verts[tri[0]]], + positions[corner_verts[tri[1]]], + positions[corner_verts[tri[2]]], + uvs[tri[0]].xy(), + uvs[tri[1]].xy(), + uvs[tri[2]].xy()); + /* Writing the result separately for every triangle simplifies the next loop. */ + r_corner_tangents[tri[0]] = tangent; + r_corner_tangents[tri[1]] = tangent; + r_corner_tangents[tri[2]] = tangent; + } + }); + + /* Mix the tangent vectors in vertices where multiple corners share the same uv. */ + threading::parallel_for(positions.index_range(), 512, [&](const IndexRange range) { + struct SharedCorners { + float2 uv; + Vector corners; + float3 tangent_sum = float3(0.0f); + }; + Vector shared_corners; + for (const int vert : range) { + const Span corners = vert_to_corners_map[vert]; + + shared_corners.clear(); + for (const int corner : corners) { + const float2 uv = uvs[corner].xy(); + /* This is only the non-interpolated tangent right now. */ + const float3 &tri_tangent = r_corner_tangents[corner]; + bool found = false; + for (SharedCorners &shared_corner : shared_corners) { + if (math::distance_manhattan(uv, shared_corner.uv) < 0.00001f) { + shared_corner.corners.append(corner); + shared_corner.tangent_sum += tri_tangent; + found = true; + break; + } + } + if (!found) { + shared_corners.append({uv, {corner}, tri_tangent}); + } + } + for (const SharedCorners &shared_corner : shared_corners) { + const float3 tangent = math::normalize(shared_corner.tangent_sum); + for (const int corner : shared_corner.corners) { + r_corner_tangents[corner] = tangent; + } + } + } + }); +} + +class TangentFieldInput final : public bke::MeshFieldInput { + private: + Method method_; + Field uv_field_; + + public: + TangentFieldInput(const Method method, Field uv) + : bke::MeshFieldInput(CPPType::get(), "Tangent Field"), + method_(method), + uv_field_(std::move(uv)) + { + category_ = Category::Generated; + } + + GVArray get_varray_for_context(const Mesh &mesh, + const AttrDomain domain, + const IndexMask & /*mask*/) const override + { + const bke::AttributeAccessor attributes = mesh.attributes(); + + const bke::MeshFieldContext corner_context{mesh, AttrDomain::Corner}; + FieldEvaluator evaluator{corner_context, mesh.corners_num}; + evaluator.add(uv_field_); + evaluator.evaluate(); + const VArraySpan uvs = evaluator.get_evaluated(0); + + Array corner_tangents(mesh.corners_num); + switch (method_) { + case Method::Fast: { + calc_uv_tangents_simple(mesh.vert_positions(), + mesh.corner_verts(), + mesh.corner_tris(), + mesh.vert_to_corner_map(), + uvs, + corner_tangents); + break; + } + case Method::Exact: { + const VArraySpan sharp_faces = *attributes.lookup("sharp_face", + bke::AttrDomain::Face); + Array uvs_float2(uvs.size()); + threading::parallel_for(corner_tangents.index_range(), 4096, [&](const IndexRange range) { + for (const int64_t corner : range) { + uvs_float2[corner] = uvs[corner].xy(); + } + }); + Array> mikk_tangents = bke::mesh::calc_uv_tangents(mesh.vert_positions(), + mesh.faces(), + mesh.corner_verts(), + mesh.corner_tris(), + mesh.corner_tri_faces(), + sharp_faces, + mesh.vert_normals(), + mesh.face_normals(), + mesh.corner_normals(), + {uvs_float2}); + threading::parallel_for(corner_tangents.index_range(), 4096, [&](const IndexRange range) { + for (const int64_t corner : range) { + corner_tangents[corner] = mikk_tangents[0][corner].xyz(); + } + }); + break; + } + } + + return attributes.adapt_domain(VArray::from_container(std::move(corner_tangents)), + bke::AttrDomain::Corner, + domain); + } + + void for_each_field_input_recursive(FunctionRef fn) const override + { + uv_field_.node().for_each_field_input_recursive(fn); + } + + bool is_equal_to(const FieldNode &other) const override + { + if (const TangentFieldInput *other_endpoint = dynamic_cast(&other)) + { + return method_ == other_endpoint->method_ && uv_field_ == other_endpoint->uv_field_; + } + return false; + } + + uint64_t hash() const override + { + return get_default_hash(method_, uv_field_); + } + + std::optional preferred_domain(const Mesh & /*mesh*/) const override + { + return AttrDomain::Corner; + } +}; + +static void node_geo_exec(GeoNodeExecParams params) +{ + const Method method = params.extract_input("Method"); + Field uv_field = params.extract_input>("UV"); + params.set_output("Tangent", + Field(std::make_shared(method, uv_field))); +} + +static void node_register() +{ + static blender::bke::bNodeType ntype; + + geo_node_type_base(&ntype, "GeometryNodeUVTangent"); + ntype.ui_name = "UV Tangent"; + ntype.ui_description = "Generate tangent directions based on a UV map"; + ntype.nclass = NODE_CLASS_INPUT; + ntype.declare = node_declare; + ntype.geometry_node_execute = node_geo_exec; + blender::bke::node_register_type(ntype); +} +NOD_REGISTER_NODE(node_register) + +} // namespace blender::nodes::node_geo_uv_tangent_cc diff --git a/tests/files/modeling/geometry_nodes/mesh/uv_tangent.blend b/tests/files/modeling/geometry_nodes/mesh/uv_tangent.blend new file mode 100644 index 00000000000..41606b5915a --- /dev/null +++ b/tests/files/modeling/geometry_nodes/mesh/uv_tangent.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a66a2d01ffbf851ff162c59956226ba9209abe9d8417335b85151e868b0e3e8d +size 999714