mirror of
https://github.com/blender/blender
synced 2026-09-29 04:37:17 +03:00
Geometry Nodes: experimental hair physics using XPBD
This adds new **experimental** built-in nodes and a couple of node group assets
which allow simulating hair curves and are fundamental building blocks for more
physics systems in the future.
At the highest level, there is a new "Hair Dynamics" asset which can be used as
modifier or as a node. It takes in hair curves with their corresponding surface
geometry and animates/simulates them based on the surface movement and other
effectors.
This is a fairly large node group which sets up a simulation world bundle that
ends up being passed to the new built-in "XPBD Solver" node. This node actually
updates the position, rotation, velocity and angular velocity attributes based
on the passed in constraints and forces. Note that this node generally needs
some setup like pre-evaluated fields and should generally be used as part of a
node group.
### World Bundle
The constraints as well as the simulated geometry are passed to the solver node
in form of a "world bundle". This bundle declaratively describes what the
physics system looks like.
It is a potentially potentially deeply nested bundle which contains so called
"typed bundles". The typed bundles correspond to specific constraint types based
on their type. Besides typed bundles, it also contains geometries which are
simulated.
Due to their generic nature (just a bundle), world bundles can be composed from
other bundles. For example, it is common to combine multiple
effectors/constraints into one bundle which is then added to the world bundle at
once. Objects can also "export" bundles on the geometry like force fields or
other constraints which can then be passed into the simulation on another
object.
### Constraint Types
The following constraint types are currently supported in the XPBD Solver node:
* Mesh colliders: Handles general collisions with closed(!) meshes.
* Infinite plane colliders: Can handle e.g. an infinite ground plane
efficiently.
* Damping: General damping of linear and angular velocities to increase
stability.
* Pin Position: Explicitly pin the position of individual points. Used for hair
root points.
* Pin Rotation: Explicitly pin the rotation of individual points/segments. Used
for hair root points.
* Rod Stretch/Shear: Attempts to maintain a specific rest length of a curve
segment (stretch) and ensures that the rotation of the segment aligns with
point positions (shear).
* Rod Bend/Twist: Attempts to maintain a specific rotation between neighboring
segments.
* Edge Length: Constrains the length of mesh edges to a given rest length.
* Cross Edge Length: A cheap bending constraint that works by adding a distance
constraint of two vertices opposite an edge.
Most of these constraints also have a compliance value. It indicates how much
the constraint is allowed to be broken. A value of zero means that the contraint
should be perfectly maintained. Sometimes this cannot be achieved due to too few
substeps or constraint steps, or because the way the system is setup makes it
impossible. Since this is a very non-linear value, it is generally exposed as a
"softness" value between 0 and 1 (although technically there is no upper limit).
It's also named e.g. "Bendiness" or "Stretchiness" depending on the context.
### Other New Nodes
Besides the solver node, there are two additional new built-in nodes. These are
required to build the higher level assets.
* Transfer Attributes: Allows copying all or a subset of attributes from one
geometry to another using custom ids on each domain for the mapping.
Importantly, this is even capable of transferring anonymous attributes.
* Tag Filter: A simple node that takes a list of tags and a filter string and
checks if the filter matches the tag list. Currently, only a comma separated
list is allowed as filter but a slightly more complex syntax may be supported
in the future. The goal is to be able to reuse the same syntax across
different tagging systems in Blender.
### Field Pre-evaluation
The XPBD Solver node does not evaluate any fields itself currently. Instead it
relies on other nodes to pre-evaluate these fields and to write them in specific
named attributes for easy consumption.
Besides simplifying the solver, this appears to be necessary to cleanly support
constraints which are interpolated across the substeps like pinning constraints.
For those, the solver not only needs the current pin position, but also the pin
position from the previous time step. Doing this attribute handling entirely
outside of the solver node allows it to be used more ways.
Two different kinds of pre-evaluations are necessary:
* Hard-coded attribute names: The solver reads from (and sometimes writes to)
these hard-coded attribute names directly. This includes `position`,
`velocity`, `rotation`, `angular_velocity`, `external_force`,
`external_torque`, `mass`, `moment_of_inertia`, `static_friction`,
`dynamic_friction`, `radius`.
* Effector specific attributes: Some effectors (like the Pin Position
constraint) needs additional attributes on the geometries it affects. In this
specific case, it's necessary to know for the current and previous frame which
points are pinned and where (and that per constraint when there are multiple).
* The used naming convention for these attributes is:
`sim:prop:{effector_path}:{property_name}` (e.g. `sim:prop:Hair/Pin
Position:selection`). The effector path is the bundle path starting at the
root bundle.
* The data from the previous frame is stored with a very similar naming
convention: `sim::prop_prev:{effector_path}:{property_name}`.
* Note that outside of building fully custom simulation systems, the user is
never expected to deal with these attributes manually. This is entirely
handled in existing node groups.
### Todos before release
Before this is released, a few things need to be done:
* New node group assets still need to be finalized, especially with respect to
the surface geometry and forces.
* The already existing hair assets need to be updated too.
* The operator to set up hair needs to be updated.
Co-authored-by: Lukas Tönne <lukas@blender.org>
Co-authored-by: Hans Goudey <hans@blender.org>
Pull Request: https://projects.blender.org/blender/blender/pulls/154435
This commit is contained in:
parent
eae8e8569d
commit
73be1e5090
48 changed files with 5966 additions and 6 deletions
|
|
@ -55,5 +55,6 @@ a01301f0-5ae0-4627-91a4-b7fde91e5c5a:Instances:Instances
|
|||
b9d488d7-441e-4f1a-905a-64ab84c31706:Mesh:Mesh
|
||||
b6bb38bb-bfe1-4a12-b2bc-fc87d060864f:Mesh/Read:Mesh-Read
|
||||
9e98eca8-e987-44d3-88a1-6633d0a8ad82:Normals:Normals
|
||||
df62a3e8-fc21-457b-9415-89f89af431ac:Simulation:Simulation
|
||||
b8945cf7-3045-4675-aae3-0b0eba853415:Utilities:Utilities
|
||||
8c0273f0-3645-449f-9d95-c4f17fb910db:Utilities/Vector:Utilities-Vector
|
||||
|
|
|
|||
3
assets/nodes/hair_dynamics_assets.blend
Normal file
3
assets/nodes/hair_dynamics_assets.blend
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a6a966de7bd0f5b8c60c974c80fb4695ae2d11b8c42d4919dc600f4e00333033
|
||||
size 432717
|
||||
|
|
@ -12,7 +12,7 @@ from bpy.app.translations import (
|
|||
class NODE_MT_gn_attribute_base(node_add_menu.NodeMenu):
|
||||
bl_label = "Attribute"
|
||||
|
||||
def draw(self, _context):
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
self.node_operator(layout, "GeometryNodeAttributeStatistic")
|
||||
self.node_operator(layout, "GeometryNodeAttributeDomainSize")
|
||||
|
|
@ -23,6 +23,8 @@ class NODE_MT_gn_attribute_base(node_add_menu.NodeMenu):
|
|||
self.node_operator(layout, "GeometryNodeRemoveAttribute")
|
||||
self.node_operator(layout, "GeometryNodeRenameAttribute")
|
||||
self.node_operator(layout, "GeometryNodeStoreNamedAttribute", search_weight=1.0)
|
||||
if context.preferences.experimental.use_geometry_nodes_hair_dynamics:
|
||||
self.node_operator(layout, "GeometryNodeTransferAttributes")
|
||||
|
||||
self.draw_assets_for_catalog(layout, self.bl_label)
|
||||
|
||||
|
|
@ -644,9 +646,12 @@ class NODE_MT_gn_point_base(node_add_menu.NodeMenu):
|
|||
class NODE_MT_gn_simulation_base(node_add_menu.NodeMenu):
|
||||
bl_label = "Simulation"
|
||||
|
||||
def draw(self, _context):
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
self.simulation_zone(layout, label="Simulation")
|
||||
layout.separator()
|
||||
if context.preferences.experimental.use_geometry_nodes_hair_dynamics:
|
||||
self.node_operator(layout, "GeometryNodeXPBDSolver")
|
||||
|
||||
self.draw_assets_for_catalog(layout, self.bl_label)
|
||||
|
||||
|
|
@ -672,6 +677,8 @@ class NODE_MT_gn_utilities_text_base(node_add_menu.NodeMenu):
|
|||
self.node_operator(layout, "FunctionNodeValueToString")
|
||||
layout.separator()
|
||||
self.node_operator(layout, "FunctionNodeInputSpecialCharacters")
|
||||
if context.preferences.experimental.use_geometry_nodes_hair_dynamics:
|
||||
self.node_operator(layout, "GeometryNodeTagFilter")
|
||||
|
||||
self.draw_assets_for_catalog(layout, self.menu_path)
|
||||
|
||||
|
|
|
|||
|
|
@ -107,15 +107,21 @@ StringRefNull essentials_directory_path()
|
|||
return path;
|
||||
}
|
||||
|
||||
bool skip_experimental_asset_catalog(const UUID & /*catalog_id*/)
|
||||
bool skip_experimental_asset_catalog(const UUID &catalog_id)
|
||||
{
|
||||
/* Return false when the catalog_id should be rejected based on experimental features:
|
||||
/* Return true when the catalog_id should be rejected based on experimental features:
|
||||
*
|
||||
* const UUID UUID_my_feature_catalog_id("11111111-2222-3333-4444-555555555555");
|
||||
* if (!U.experimental.use_my_feature && catalog_id == UUID_my_feature_catalog_id) {
|
||||
* return true;
|
||||
* }
|
||||
*/
|
||||
|
||||
/* Enable catalog for hair dynamics only if the feature is enabled. */
|
||||
const UUID UUID_hair_dynamics("df62a3e8-fc21-457b-9415-89f89af431ac");
|
||||
if (!U.experimental.use_geometry_nodes_hair_dynamics && catalog_id == UUID_hair_dynamics) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
62
source/blender/blenlib/BLI_virtual_array_range_spans.hh
Normal file
62
source/blender/blenlib/BLI_virtual_array_range_spans.hh
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_resource_scope.hh"
|
||||
#include "BLI_virtual_array.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
/**
|
||||
* This allows efficiently accessing a single-value virtual array as span, assuming that only ever
|
||||
* a partial span is necessary. Specifically, this is more efficient than materializing the full
|
||||
* single-value #VArray into a large array.
|
||||
*/
|
||||
template<typename T> class VArrayRangeSpans {
|
||||
private:
|
||||
int64_t max_span_size_ = 0;
|
||||
std::optional<Span<T>> full_span_;
|
||||
std::optional<Span<T>> chunk_span_;
|
||||
|
||||
public:
|
||||
VArrayRangeSpans() = default;
|
||||
|
||||
VArrayRangeSpans(ResourceScope &scope, const VArray<T> &varray, const int max_range_size)
|
||||
: max_span_size_(max_range_size)
|
||||
{
|
||||
if (varray.is_span()) {
|
||||
/* This copy is generally very cheap. */
|
||||
if (varray.common_info().may_have_ownership) {
|
||||
const VArray<T> &owned_varray = scope.construct<VArray<T>>(varray);
|
||||
full_span_ = owned_varray.get_internal_span();
|
||||
}
|
||||
else {
|
||||
full_span_ = varray.get_internal_span();
|
||||
}
|
||||
}
|
||||
else if (const std::optional<T> single_value = varray.get_if_single()) {
|
||||
chunk_span_ = scope.allocator().construct_array<T>(max_range_size, *single_value);
|
||||
}
|
||||
else {
|
||||
MutableSpan<T> full_span = scope.allocator().allocate_array<T>(varray.size());
|
||||
varray.materialize_to_uninitialized(full_span);
|
||||
full_span_ = full_span;
|
||||
scope.add_destruct_call([full_span]() { destruct_n(full_span.data(), full_span.size()); });
|
||||
}
|
||||
}
|
||||
|
||||
Span<T> get_span_for_range(const IndexRange range) const
|
||||
{
|
||||
BLI_assert(full_span_ || chunk_span_);
|
||||
const int range_size = range.size();
|
||||
BLI_assert(range_size <= max_span_size_);
|
||||
if (full_span_) {
|
||||
return full_span_->slice(range);
|
||||
}
|
||||
return chunk_span_->take_front(range_size);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender
|
||||
|
|
@ -409,6 +409,7 @@ set(SRC
|
|||
BLI_vector_set_slots.hh
|
||||
BLI_virtual_array.hh
|
||||
BLI_virtual_array_fwd.hh
|
||||
BLI_virtual_array_range_spans.hh
|
||||
BLI_virtual_vector_array.hh
|
||||
BLI_voxel.h
|
||||
BLI_winstuff.h
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
#include "BLI_vector.hh"
|
||||
#include "BLI_vector_set.hh"
|
||||
#include "BLI_virtual_array.hh"
|
||||
#include "BLI_virtual_array_range_spans.hh"
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
#include "BLI_strict_flags.h" /* IWYU pragma: keep. Keep last. */
|
||||
|
|
@ -293,6 +295,67 @@ TEST_F(VirtualArrayTest, EmptySpanWrapper)
|
|||
}
|
||||
}
|
||||
|
||||
TEST_F(VirtualArrayTest, SingleValueRangeSpans)
|
||||
{
|
||||
const VArray<int> varray = VArray<int>::from_single(42, 10);
|
||||
ResourceScope scope;
|
||||
const VArrayRangeSpans<int> spans(scope, varray, 3);
|
||||
|
||||
const Span<int> span1 = spans.get_span_for_range(IndexRange::from_begin_size(0, 3));
|
||||
const Span<int> span2 = spans.get_span_for_range(IndexRange::from_begin_size(7, 3));
|
||||
const Span<int> span3 = spans.get_span_for_range(IndexRange::from_begin_size(5, 1));
|
||||
EXPECT_EQ_SPAN(span1, {42, 42, 42});
|
||||
EXPECT_EQ(span1.data(), span2.data());
|
||||
EXPECT_EQ(span1.data(), span3.data());
|
||||
}
|
||||
|
||||
TEST_F(VirtualArrayTest, SpanRangeSpans)
|
||||
{
|
||||
const Array<int> array = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
|
||||
const VArray<int> varray = VArray<int>::from_span(array);
|
||||
ResourceScope scope;
|
||||
const VArrayRangeSpans<int> spans(scope, varray, 3);
|
||||
|
||||
const Span<int> span1 = spans.get_span_for_range(IndexRange::from_begin_size(0, 3));
|
||||
EXPECT_EQ(span1.data(), array.data());
|
||||
|
||||
const Span<int> span2 = spans.get_span_for_range(IndexRange::from_begin_size(7, 3));
|
||||
EXPECT_EQ(span2.data(), &array[7]);
|
||||
|
||||
const Span<int> span3 = spans.get_span_for_range(IndexRange::from_begin_size(5, 1));
|
||||
EXPECT_EQ(span3.data(), &array[5]);
|
||||
}
|
||||
|
||||
TEST_F(VirtualArrayTest, ArrayRangeSpans)
|
||||
{
|
||||
const VArray<int> varray = VArray<int>::from_container(Array<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
|
||||
ResourceScope scope;
|
||||
const VArrayRangeSpans<int> spans(scope, varray, 3);
|
||||
|
||||
const Span<int> span1 = spans.get_span_for_range(IndexRange::from_begin_size(0, 3));
|
||||
const Span<int> span2 = spans.get_span_for_range(IndexRange::from_begin_size(7, 3));
|
||||
const Span<int> span3 = spans.get_span_for_range(IndexRange::from_begin_size(5, 1));
|
||||
|
||||
EXPECT_EQ(span1.data() + 7, span2.data());
|
||||
EXPECT_EQ(span1.data() + 5, span3.data());
|
||||
}
|
||||
|
||||
TEST_F(VirtualArrayTest, FunctionRangeSpans)
|
||||
{
|
||||
const VArray<int> varray = VArray<int>::from_std_func(10, [](const int64_t i) { return i; });
|
||||
ResourceScope scope;
|
||||
const VArrayRangeSpans<int> spans(scope, varray, 3);
|
||||
|
||||
const Span<int> span1 = spans.get_span_for_range(IndexRange::from_begin_size(0, 3));
|
||||
EXPECT_EQ_SPAN(span1, {0, 1, 2});
|
||||
|
||||
const Span<int> span2 = spans.get_span_for_range(IndexRange::from_begin_size(7, 3));
|
||||
EXPECT_EQ_SPAN(span2, {7, 8, 9});
|
||||
|
||||
const Span<int> span3 = spans.get_span_for_range(IndexRange::from_begin_size(5, 1));
|
||||
EXPECT_EQ_SPAN(span3, {5});
|
||||
}
|
||||
|
||||
TEST(generic_virtual_array, FromFunc)
|
||||
{
|
||||
GVArray gvarray = GVArray::from_func(
|
||||
|
|
|
|||
|
|
@ -2345,8 +2345,9 @@ static wmOperatorStatus object_curves_empty_hair_add_exec(bContext *C, wmOperato
|
|||
curves_id->surface_uv_map = BLI_strdupn(uv_name.data(), uv_name.size());
|
||||
}
|
||||
|
||||
/* Add deformation modifier. */
|
||||
ed::curves::ensure_surface_deformation_node_exists(*C, *curves_ob);
|
||||
if (!U.experimental.use_geometry_nodes_hair_dynamics) {
|
||||
ed::curves::ensure_surface_deformation_node_exists(*C, *curves_ob);
|
||||
}
|
||||
|
||||
/* Make sure the surface object has a rest position attribute which is necessary for
|
||||
* deformations. */
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
set(INC
|
||||
PUBLIC .
|
||||
PUBLIC xpbd
|
||||
../makesrna
|
||||
../../../intern/eigen
|
||||
)
|
||||
|
|
@ -57,6 +58,8 @@ set(SRC
|
|||
intern/uv_parametrizer.cc
|
||||
intern/volume_grid_resample.cc
|
||||
|
||||
xpbd/intern/xpbd_constraint_coloring.cc
|
||||
|
||||
GEO_add_curves_on_mesh.hh
|
||||
GEO_curve_constraints.hh
|
||||
GEO_curves_remove_and_split.hh
|
||||
|
|
@ -100,6 +103,28 @@ set(SRC
|
|||
GEO_uv_pack.hh
|
||||
GEO_uv_parametrizer.hh
|
||||
GEO_volume_grid_resample.hh
|
||||
|
||||
xpbd/GEO_xpbd_constraint_align_rotations.hh
|
||||
xpbd/GEO_xpbd_constraint_collision_edge.hh
|
||||
xpbd/GEO_xpbd_constraint_collision_face.hh
|
||||
xpbd/GEO_xpbd_constraint_coloring.hh
|
||||
xpbd/GEO_xpbd_constraint_damping_angular.hh
|
||||
xpbd/GEO_xpbd_constraint_damping_linear.hh
|
||||
xpbd/GEO_xpbd_constraint_distance.hh
|
||||
xpbd/GEO_xpbd_constraint_friction_edge.hh
|
||||
xpbd/GEO_xpbd_constraint_friction_face.hh
|
||||
xpbd/GEO_xpbd_constraint_math.hh
|
||||
xpbd/GEO_xpbd_constraint_pin_position.hh
|
||||
xpbd/GEO_xpbd_constraint_pin_rotation.hh
|
||||
xpbd/GEO_xpbd_constraint_rod_bend_twist.hh
|
||||
xpbd/GEO_xpbd_constraint_rod_stretch_shear.hh
|
||||
xpbd/GEO_xpbd_constraint_set.hh
|
||||
xpbd/GEO_xpbd_constraint_set_params.hh
|
||||
xpbd/GEO_xpbd_constraint_set_templated.hh
|
||||
xpbd/GEO_xpbd_geometry_ref.hh
|
||||
xpbd/GEO_xpbd_updater_gauss_seidel.hh
|
||||
xpbd/GEO_xpbd_updater_velocity.hh
|
||||
|
||||
intern/mesh_boolean_intern.hh
|
||||
intern/mesh_boolean_manifold.hh
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_math_quaternion.hh"
|
||||
#include "BLI_math_vector.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
struct AlignRotationsConstraintResult {
|
||||
float4 delta_lambda;
|
||||
math::Quaternion offset0 = math::Quaternion(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
math::Quaternion offset1 = math::Quaternion(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
};
|
||||
|
||||
inline AlignRotationsConstraintResult evaluate_align_rotations_constraint(
|
||||
const math::Quaternion &r0,
|
||||
const math::Quaternion &r1,
|
||||
const float3 &inertia0,
|
||||
const float3 &inertia1,
|
||||
const math::Quaternion &rest_rotation,
|
||||
const float compliance_term,
|
||||
const float4 &lambda_prev)
|
||||
{
|
||||
const float inv_lumped_inertia0 = math::safe_rcp(0.5f * (inertia0.x + inertia0.y + inertia0.z));
|
||||
const float inv_lumped_inertia1 = math::safe_rcp(0.5f * (inertia1.x + inertia1.y + inertia1.z));
|
||||
if (inv_lumped_inertia0 == 0.0f && inv_lumped_inertia1 == 0.0f) {
|
||||
/* Everything is pinned, so the constraint can't do anything. */
|
||||
return {};
|
||||
}
|
||||
|
||||
const float4 rest_rot_f = float4(rest_rotation);
|
||||
|
||||
/* Note In "Position and Orientation Based Cosserat Rods" (Kugelstadt, Schoemer) the W
|
||||
* component of the Darboux vector is ignored. In "Sag-Free Initialization for Strand-Based
|
||||
* Hybrid Hair Simulation" (Hsu et al.) it is included to improve stability in cases where the
|
||||
* hair is bent at nearly 180 degrees. */
|
||||
const math::Quaternion &rot_diff = math::invert_normalized(r0) * r1;
|
||||
const float4 rot_diff_f = float4(rot_diff);
|
||||
|
||||
const float4 residual_neg = rot_diff_f - rest_rot_f;
|
||||
const float4 residual_pos = rot_diff_f + rest_rot_f;
|
||||
const float4 residual = math::length_squared(residual_neg) < math::length_squared(residual_pos) ?
|
||||
residual_neg :
|
||||
residual_pos;
|
||||
|
||||
const float4 delta_lambda = (-residual - compliance_term * lambda_prev) /
|
||||
(inv_lumped_inertia0 + inv_lumped_inertia1 + compliance_term);
|
||||
|
||||
const math::Quaternion offset0 = r1 * math::conjugate(
|
||||
math::Quaternion(delta_lambda * inv_lumped_inertia0));
|
||||
const math::Quaternion offset1 = r0 * math::Quaternion(delta_lambda * inv_lumped_inertia1);
|
||||
|
||||
return {delta_lambda, offset0, offset1};
|
||||
}
|
||||
|
||||
} // namespace blender::xpbd
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "GEO_xpbd_constraint_coloring_utils.hh"
|
||||
#include "GEO_xpbd_constraint_math.hh"
|
||||
#include "GEO_xpbd_constraint_set_templated.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
/* Constraint implementation for static and dynamic friction is based on
|
||||
* "Detailed Rigid Body Simulation with Extended Position Based Dynamics",
|
||||
* Mueller, Macklin, et al., 2020 */
|
||||
class CollisionEdgeConstraintSet : public TemplatedConstraintSet<CollisionEdgeConstraintSet> {
|
||||
private:
|
||||
int geo_i_;
|
||||
Span<int2> point_pairs_;
|
||||
Span<float2> point_radii_;
|
||||
Span<float3> contact_points_on_edge_;
|
||||
Span<float3> contact_points_motion_;
|
||||
/* Direction of the collider edges. */
|
||||
Span<float3> edge_directions_;
|
||||
/* Normal vectors in the plane of the adjacent face.
|
||||
* The edge normal is cross(edge_direction, face_normal). */
|
||||
Span<float3> edge_normals_;
|
||||
/* Margin of the collider surface. */
|
||||
Span<float> edge_margins_;
|
||||
Span<float> compliance_terms_;
|
||||
Span<float> static_frictions_;
|
||||
Span<float> dynamic_frictions_;
|
||||
MutableSpan<bool> active_states_;
|
||||
MutableSpan<float> point_mix_factors_;
|
||||
MutableSpan<float> lambdas_normal_;
|
||||
|
||||
public:
|
||||
static constexpr StringRefNull debug_name = "Collision Plane";
|
||||
|
||||
CollisionEdgeConstraintSet(const int geo_i,
|
||||
const Span<int2> point_pairs,
|
||||
const Span<float2> point_radii,
|
||||
const Span<float3> contact_points_on_edge,
|
||||
const Span<float3> contact_points_motion,
|
||||
const Span<float3> edge_directions,
|
||||
const Span<float3> edge_normals,
|
||||
const Span<float> edge_margins,
|
||||
const Span<float> compliance_terms,
|
||||
const Span<float> static_frictions,
|
||||
const Span<float> dynamic_frictions,
|
||||
MutableSpan<bool> active_states,
|
||||
MutableSpan<float> point_mix_factors,
|
||||
MutableSpan<float> lambdas_normal)
|
||||
: TemplatedConstraintSet<CollisionEdgeConstraintSet>(point_pairs.size(), {geo_i}),
|
||||
geo_i_(geo_i),
|
||||
point_pairs_(point_pairs),
|
||||
point_radii_(point_radii),
|
||||
contact_points_on_edge_(contact_points_on_edge),
|
||||
contact_points_motion_(contact_points_motion),
|
||||
edge_directions_(edge_directions),
|
||||
edge_normals_(edge_normals),
|
||||
edge_margins_(edge_margins),
|
||||
compliance_terms_(compliance_terms),
|
||||
static_frictions_(static_frictions),
|
||||
dynamic_frictions_(dynamic_frictions),
|
||||
active_states_(active_states),
|
||||
point_mix_factors_(point_mix_factors),
|
||||
lambdas_normal_(lambdas_normal)
|
||||
{
|
||||
}
|
||||
|
||||
void reset_force(const int constraint_i) const
|
||||
{
|
||||
active_states_[constraint_i] = false;
|
||||
lambdas_normal_[constraint_i] = 0.0f;
|
||||
}
|
||||
|
||||
template<typename UpdaterT>
|
||||
void solve_single(const ConstraintSetParams ¶ms,
|
||||
UpdaterT &updater,
|
||||
const int constraint_i) const
|
||||
{
|
||||
const int2 &point_pair = point_pairs_[constraint_i];
|
||||
const float3 &pos0 = params.position(geo_i_, point_pair[0]);
|
||||
const float3 &pos1 = params.position(geo_i_, point_pair[1]);
|
||||
const float3 &edge_pos = contact_points_on_edge_[constraint_i];
|
||||
const float3 &edge_dir = edge_directions_[constraint_i];
|
||||
const float3 &edge_nor = edge_normals_[constraint_i];
|
||||
const float margin = edge_margins_[constraint_i];
|
||||
const float compliance_term = compliance_terms_[constraint_i];
|
||||
const float inv_m0 = params.inv_mass(geo_i_, point_pair[0]);
|
||||
const float inv_m1 = params.inv_mass(geo_i_, point_pair[1]);
|
||||
const float radius0 = point_radii_[constraint_i][0];
|
||||
const float radius1 = point_radii_[constraint_i][1];
|
||||
BLI_assert(math::is_unit(edge_dir));
|
||||
BLI_assert(math::is_unit(edge_nor));
|
||||
bool &is_active = active_states_[constraint_i];
|
||||
float &point_mix_factor = point_mix_factors_[constraint_i];
|
||||
|
||||
if (inv_m0 <= 0.0f && inv_m1 <= 0.0f) {
|
||||
/* Points with infinite mass are pinned and don't collide dynamically. */
|
||||
is_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const SegmentClosestToRay closest = closest_on_segment_to_ray(
|
||||
pos0, pos1, edge_pos, edge_dir, true);
|
||||
const float segment_factor = closest.segment_lambda;
|
||||
point_mix_factor = segment_factor;
|
||||
|
||||
/* Effective weight/mass */
|
||||
const float weight0 = (1.0f - segment_factor) * inv_m0;
|
||||
const float weight1 = segment_factor * inv_m1;
|
||||
if (weight0 <= 0.0f && weight1 <= 0.0f) {
|
||||
/* Could happen if the segment_factor is exactly 0 or 1. */
|
||||
is_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const float3 closest_on_segment = math::interpolate(pos0, pos1, segment_factor);
|
||||
const float3 closest_on_ray = edge_pos + closest.ray_lambda * edge_dir;
|
||||
/* Curve surface is ambiguous: The closest point on the center line isn't necessarily the
|
||||
* contact point of the implicit curve surface as defined by the curve radius. A prospective
|
||||
* surface point is the intersection of connecting line between the closest points on the
|
||||
* segment and the edge.
|
||||
* If the curve is "inside" the collider (intersects the half-plane under the edge) then use
|
||||
* the curve surface point on the opposite side is used as the contact. */
|
||||
float3 seg_nor = closest_on_ray - closest_on_segment;
|
||||
const std::optional<PlaneIntersection> intersection = intersect_plane(
|
||||
pos0, pos1, edge_pos, edge_dir, edge_nor);
|
||||
if (intersection && math::dot(intersection->position - edge_pos, edge_nor) < 0.0f) {
|
||||
/* Reflect on segment direction. This produces the correct normal also in case the closest
|
||||
* segment point is clamped and the normal isn't perpendicular to the edge direction. */
|
||||
seg_nor = 2.0f * edge_dir * math::dot(edge_dir, seg_nor) - seg_nor;
|
||||
}
|
||||
float seg_dist;
|
||||
seg_nor = math::normalize_and_get_length(seg_nor, seg_dist);
|
||||
|
||||
const float radius = math::interpolate(radius0, radius1, segment_factor);
|
||||
const float3 distance = (closest_on_segment + radius * seg_nor) -
|
||||
(closest_on_ray + margin * edge_nor);
|
||||
const float residual = -math::dot(distance, seg_nor);
|
||||
if (residual >= 0.0f) {
|
||||
is_active = false;
|
||||
return;
|
||||
}
|
||||
const float3 gradient = -seg_nor;
|
||||
|
||||
/* Positional correction for penetration. */
|
||||
float3 offset0 = float3(0.0f);
|
||||
float3 offset1 = float3(0.0f);
|
||||
float &lambda_normal = lambdas_normal_[constraint_i];
|
||||
const float delta_lambda_normal = -residual / (weight0 + weight1 + compliance_term);
|
||||
offset0 += delta_lambda_normal * weight0 * gradient;
|
||||
offset1 += delta_lambda_normal * weight1 * gradient;
|
||||
lambda_normal += delta_lambda_normal;
|
||||
|
||||
/* Apply static friction as a direct positional update. */
|
||||
const float3 &prev_pos0 = params.prev_position(geo_i_, point_pair[0]);
|
||||
const float3 &prev_pos1 = params.prev_position(geo_i_, point_pair[1]);
|
||||
const float3 &collider_velocity = contact_points_motion_[constraint_i];
|
||||
const float3 velocity = math::interpolate(pos0 - prev_pos0, pos1 - prev_pos1, segment_factor) -
|
||||
collider_velocity;
|
||||
const float3 velocity_tangent = velocity - math::dot(velocity, gradient) * gradient;
|
||||
const float lambda_tangent_sq = math::length_squared(velocity_tangent /
|
||||
(weight0 + weight1 + compliance_term));
|
||||
const bool is_static = lambda_tangent_sq <
|
||||
math::square(static_frictions_[constraint_i] * lambda_normal);
|
||||
if (is_static) {
|
||||
offset0 -= velocity_tangent * weight0 / (weight0 + weight1 + compliance_term);
|
||||
offset1 -= velocity_tangent * weight1 / (weight0 + weight1 + compliance_term);
|
||||
}
|
||||
|
||||
is_active = true;
|
||||
updater.update_position(geo_i_, point_pair[0], offset0);
|
||||
updater.update_position(geo_i_, point_pair[1], offset1);
|
||||
}
|
||||
|
||||
ConstraintColoring color_constraints(IndexMaskMemory &memory) const override
|
||||
{
|
||||
return color_constraints__binary(point_pairs_, memory);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender::xpbd
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "GEO_xpbd_constraint_coloring_utils.hh"
|
||||
#include "GEO_xpbd_constraint_set_templated.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
/* Constraint implementation for static and dynamic friction is based on
|
||||
* "Detailed Rigid Body Simulation with Extended Position Based Dynamics",
|
||||
* Mueller, Macklin, et al., 2020 */
|
||||
class CollisionFaceConstraintSet : public TemplatedConstraintSet<CollisionFaceConstraintSet> {
|
||||
private:
|
||||
int geo_i_;
|
||||
Span<int> points_;
|
||||
Span<float> point_radii_;
|
||||
Span<float3> contact_points_on_face_;
|
||||
Span<float3> contact_points_motion_;
|
||||
Span<float3> face_normals_;
|
||||
Span<float> face_margins_;
|
||||
Span<float> compliance_terms_;
|
||||
Span<float> static_frictions_;
|
||||
Span<float> dynamic_frictions_;
|
||||
MutableSpan<bool> active_states_;
|
||||
MutableSpan<float> lambdas_normal_;
|
||||
|
||||
public:
|
||||
static constexpr StringRefNull debug_name = "Collision Plane";
|
||||
|
||||
CollisionFaceConstraintSet(const int geo_i,
|
||||
const Span<int> points,
|
||||
const Span<float> point_radii,
|
||||
const Span<float3> contact_points_on_face,
|
||||
const Span<float3> contact_points_motion,
|
||||
const Span<float3> face_normals,
|
||||
const Span<float> face_margins,
|
||||
const Span<float> compliance_terms,
|
||||
const Span<float> static_frictions,
|
||||
const Span<float> dynamic_frictions,
|
||||
MutableSpan<bool> active_states,
|
||||
MutableSpan<float> lambdas_normal)
|
||||
: TemplatedConstraintSet<CollisionFaceConstraintSet>(points.size(), {geo_i}),
|
||||
geo_i_(geo_i),
|
||||
points_(points),
|
||||
point_radii_(point_radii),
|
||||
contact_points_on_face_(contact_points_on_face),
|
||||
contact_points_motion_(contact_points_motion),
|
||||
face_normals_(face_normals),
|
||||
face_margins_(face_margins),
|
||||
compliance_terms_(compliance_terms),
|
||||
static_frictions_(static_frictions),
|
||||
dynamic_frictions_(dynamic_frictions),
|
||||
active_states_(active_states),
|
||||
lambdas_normal_(lambdas_normal)
|
||||
{
|
||||
}
|
||||
|
||||
void reset_force(const int constraint_i) const
|
||||
{
|
||||
active_states_[constraint_i] = false;
|
||||
lambdas_normal_[constraint_i] = 0.0f;
|
||||
}
|
||||
|
||||
template<typename UpdaterT>
|
||||
void solve_single(const ConstraintSetParams ¶ms,
|
||||
UpdaterT &updater,
|
||||
const int constraint_i) const
|
||||
{
|
||||
const int point_i = points_[constraint_i];
|
||||
const float radius = point_radii_[constraint_i];
|
||||
const float3 &pos = params.position(geo_i_, point_i);
|
||||
const float3 &face_pos = contact_points_on_face_[constraint_i];
|
||||
const float3 &face_nor = face_normals_[constraint_i];
|
||||
const float margin = face_margins_[constraint_i];
|
||||
const float compliance_term = compliance_terms_[constraint_i];
|
||||
const float inv_m = params.inv_mass(geo_i_, point_i);
|
||||
bool &is_active = active_states_[constraint_i];
|
||||
|
||||
if (inv_m <= 0.0f) {
|
||||
/* Points with infinite mass are pinned and don't collide dynamically. */
|
||||
is_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const float3 diff = pos - face_pos;
|
||||
const float normal_distance = math::dot(diff, face_nor) - margin - radius;
|
||||
is_active = normal_distance < 0.0f;
|
||||
if (!is_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Positional correction for penetration. */
|
||||
float3 offset = float3(0.0f);
|
||||
float &lambda_normal = lambdas_normal_[constraint_i];
|
||||
const float delta_lambda_normal = -normal_distance / (inv_m + compliance_term);
|
||||
offset += delta_lambda_normal * inv_m * face_nor;
|
||||
lambda_normal += delta_lambda_normal;
|
||||
|
||||
/* Apply static friction as a direct positional update. */
|
||||
const float3 &prev_pos = params.prev_position(geo_i_, point_i);
|
||||
const float3 &collider_velocity = contact_points_motion_[constraint_i];
|
||||
const float3 velocity = (pos - prev_pos) - collider_velocity;
|
||||
const float3 velocity_tangent = velocity - math::dot(velocity, face_nor) * face_nor;
|
||||
const float lambda_tangent_sq = math::length_squared(velocity_tangent /
|
||||
(inv_m + compliance_term));
|
||||
const bool is_static = lambda_tangent_sq <
|
||||
math::square(static_frictions_[constraint_i] * lambda_normal);
|
||||
if (is_static) {
|
||||
offset -= velocity_tangent * inv_m / (inv_m + compliance_term);
|
||||
}
|
||||
|
||||
updater.update_position(geo_i_, point_i, offset);
|
||||
}
|
||||
|
||||
ConstraintColoring color_constraints(IndexMaskMemory &memory) const override
|
||||
{
|
||||
return color_constraints__unary(points_, memory);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender::xpbd
|
||||
15
source/blender/geometry/xpbd/GEO_xpbd_constraint_coloring.hh
Normal file
15
source/blender/geometry/xpbd/GEO_xpbd_constraint_coloring.hh
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_index_mask.hh"
|
||||
namespace blender::xpbd {
|
||||
|
||||
struct ConstraintColoring {
|
||||
/** Indices within the same #IndexMask are independent and can be evaluated in parallel. */
|
||||
Vector<IndexMask> colors;
|
||||
};
|
||||
|
||||
} // namespace blender::xpbd
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_index_mask.hh"
|
||||
#include "BLI_math_vector_types.hh"
|
||||
|
||||
#include "GEO_xpbd_constraint_coloring.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
ConstraintColoring color_constraints__unary(const Span<int> affected_points,
|
||||
IndexMaskMemory &memory);
|
||||
|
||||
ConstraintColoring color_constraints__binary(const Span<int2> affected_points,
|
||||
IndexMaskMemory &memory);
|
||||
|
||||
ConstraintColoring color_constraints__n_ary(const GroupedSpan<int> affected_points,
|
||||
IndexMaskMemory &memory);
|
||||
|
||||
ConstraintColoring color_constraints__all_independent(const int constraints_num);
|
||||
|
||||
} // namespace blender::xpbd
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "GEO_xpbd_constraint_set_templated.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
class AngularDampingConstraintSet
|
||||
: public TemplatedVelocityConstraintSet<AngularDampingConstraintSet> {
|
||||
private:
|
||||
int geo_i_;
|
||||
IndexRange points_;
|
||||
/** Indexed by constraint index. */
|
||||
Span<float> angular_dampings_;
|
||||
MutableSpan<float> lambdas_;
|
||||
|
||||
public:
|
||||
static constexpr StringRefNull debug_name = "Angular Damping";
|
||||
|
||||
AngularDampingConstraintSet(const int geo_i,
|
||||
const IndexRange points,
|
||||
const Span<float> angular_dampings,
|
||||
MutableSpan<float> lambdas)
|
||||
: TemplatedVelocityConstraintSet<AngularDampingConstraintSet>(points.size(), {geo_i}),
|
||||
geo_i_(geo_i),
|
||||
points_(points),
|
||||
angular_dampings_(angular_dampings),
|
||||
lambdas_(lambdas)
|
||||
{
|
||||
}
|
||||
|
||||
void reset_force(const int constraint_i) const
|
||||
{
|
||||
lambdas_[constraint_i] = 0.0f;
|
||||
}
|
||||
|
||||
template<typename UpdaterT>
|
||||
void solve_single(const ConstraintSetParams ¶ms,
|
||||
UpdaterT &updater,
|
||||
const int constraint_i) const
|
||||
{
|
||||
const int point_i = points_[constraint_i];
|
||||
const float3 &angular_velocity = params.angular_velocity(geo_i_, point_i);
|
||||
const float damping = angular_dampings_[constraint_i];
|
||||
const float damping_factor = damping * params.delta_time;
|
||||
float residual;
|
||||
const float3 gradient = math::normalize_and_get_length(angular_velocity, residual);
|
||||
const float delta_lambda = -residual * damping_factor - lambdas_[constraint_i];
|
||||
const float3 offset = gradient * delta_lambda;
|
||||
lambdas_[constraint_i] += delta_lambda;
|
||||
updater.update_angular_velocity(geo_i_, point_i, offset);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender::xpbd
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "GEO_xpbd_constraint_set_templated.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
class LinearDampingConstraintSet
|
||||
: public TemplatedVelocityConstraintSet<LinearDampingConstraintSet> {
|
||||
private:
|
||||
int geo_i_;
|
||||
IndexRange points_;
|
||||
/** Indexed by constraint index. */
|
||||
Span<float> linear_dampings_;
|
||||
MutableSpan<float> lambdas_;
|
||||
|
||||
public:
|
||||
static constexpr StringRefNull debug_name = "Linear Damping";
|
||||
|
||||
LinearDampingConstraintSet(const int geo_i,
|
||||
const IndexRange points,
|
||||
const Span<float> linear_dampings,
|
||||
MutableSpan<float> lambdas)
|
||||
: TemplatedVelocityConstraintSet<LinearDampingConstraintSet>(points.size(), {geo_i}),
|
||||
geo_i_(geo_i),
|
||||
points_(points),
|
||||
linear_dampings_(linear_dampings),
|
||||
lambdas_(lambdas)
|
||||
{
|
||||
}
|
||||
|
||||
void reset_force(const int constraint_i) const
|
||||
{
|
||||
lambdas_[constraint_i] = 0.0f;
|
||||
}
|
||||
|
||||
template<typename UpdaterT>
|
||||
void solve_single(const ConstraintSetParams ¶ms,
|
||||
UpdaterT &updater,
|
||||
const int constraint_i) const
|
||||
{
|
||||
const int point_i = points_[constraint_i];
|
||||
const float3 &velocity = params.velocity(geo_i_, point_i);
|
||||
const float damping = linear_dampings_[constraint_i];
|
||||
const float damping_factor = std::clamp(params.delta_time * damping, 0.0f, 1.0f);
|
||||
float residual;
|
||||
const float3 gradient = math::normalize_and_get_length(velocity, residual);
|
||||
const float delta_lambda = -residual * damping_factor - lambdas_[constraint_i];
|
||||
const float3 offset = gradient * delta_lambda;
|
||||
lambdas_[constraint_i] += delta_lambda;
|
||||
updater.update_velocity(geo_i_, point_i, offset);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender::xpbd
|
||||
100
source/blender/geometry/xpbd/GEO_xpbd_constraint_distance.hh
Normal file
100
source/blender/geometry/xpbd/GEO_xpbd_constraint_distance.hh
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "GEO_xpbd_constraint_coloring_utils.hh"
|
||||
#include "GEO_xpbd_constraint_set_templated.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
struct DistanceConstraintResult {
|
||||
float delta_lambda = 0.0f;
|
||||
float3 offset0 = float3(0.0f);
|
||||
float3 offset1 = float3(0.0f);
|
||||
};
|
||||
|
||||
inline DistanceConstraintResult evaluate_distance_constraint(const float3 &p0,
|
||||
const float3 &p1,
|
||||
const float inv_m0,
|
||||
const float inv_m1,
|
||||
const float rest_distance,
|
||||
const float compliance_term,
|
||||
const float lambda_prev)
|
||||
{
|
||||
if (inv_m0 == 0.0f && inv_m1 == 0.0f) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const float3 p_diff = p1 - p0;
|
||||
float length;
|
||||
const float3 normalized_dir = math::normalize_and_get_length(p_diff, length);
|
||||
const float length_diff = length - rest_distance;
|
||||
const float delta_lambda = (-length_diff - compliance_term * lambda_prev) /
|
||||
(inv_m0 + inv_m1 + compliance_term);
|
||||
|
||||
const float3 offset0 = -delta_lambda * inv_m0 * normalized_dir;
|
||||
const float3 offset1 = delta_lambda * inv_m1 * normalized_dir;
|
||||
|
||||
return {delta_lambda, offset0, offset1};
|
||||
}
|
||||
|
||||
class DistanceConstraintSet : public TemplatedConstraintSet<DistanceConstraintSet> {
|
||||
private:
|
||||
/** Indexed by constraint index. */
|
||||
Span<int2> point_pairs_;
|
||||
Span<float> distances_;
|
||||
Span<float> compliances_;
|
||||
MutableSpan<float> lambdas_;
|
||||
|
||||
public:
|
||||
static constexpr StringRefNull debug_name = "Distance Constraint";
|
||||
|
||||
DistanceConstraintSet(const int geo_i,
|
||||
const Span<int2> point_pairs,
|
||||
const Span<float> distances,
|
||||
const Span<float> compliances,
|
||||
MutableSpan<float> lambdas)
|
||||
: TemplatedConstraintSet<DistanceConstraintSet>(point_pairs.size(), {geo_i}),
|
||||
point_pairs_(point_pairs),
|
||||
distances_(distances),
|
||||
compliances_(compliances),
|
||||
lambdas_(lambdas)
|
||||
{
|
||||
}
|
||||
|
||||
void reset_force(const int constraint_i) const
|
||||
{
|
||||
lambdas_[constraint_i] = 0.0f;
|
||||
}
|
||||
|
||||
template<typename UpdaterT>
|
||||
void solve_single(const ConstraintSetParams ¶ms,
|
||||
UpdaterT &updater,
|
||||
const int constraint_i) const
|
||||
{
|
||||
const int geo_i = affected_geo_indices_[0];
|
||||
const int2 &point_pair = point_pairs_[constraint_i];
|
||||
const int point_i0 = point_pair[0];
|
||||
const int point_i1 = point_pair[1];
|
||||
const DistanceConstraintResult result = evaluate_distance_constraint(
|
||||
params.position(geo_i, point_i0),
|
||||
params.position(geo_i, point_i1),
|
||||
params.inv_mass(geo_i, point_i0),
|
||||
params.inv_mass(geo_i, point_i1),
|
||||
distances_[constraint_i],
|
||||
compliances_[constraint_i] * params.compliance_term_factor,
|
||||
lambdas_[constraint_i]);
|
||||
lambdas_[constraint_i] += result.delta_lambda;
|
||||
updater.update_position(geo_i, point_i0, result.offset0);
|
||||
updater.update_position(geo_i, point_i1, result.offset1);
|
||||
}
|
||||
|
||||
ConstraintColoring color_constraints(IndexMaskMemory &memory) const override
|
||||
{
|
||||
return color_constraints__binary(point_pairs_, memory);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender::xpbd
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "GEO_xpbd_constraint_math.hh"
|
||||
#include "GEO_xpbd_constraint_set_templated.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
class FrictionEdgeConstraintSet
|
||||
: public TemplatedVelocityConstraintSet<FrictionEdgeConstraintSet> {
|
||||
private:
|
||||
int geo_i_;
|
||||
/* Constraint index for each point. */
|
||||
Span<int2> point_pairs_;
|
||||
Span<float3> separating_axes_;
|
||||
Span<float3> contact_velocities_;
|
||||
/* Constraint multiplier lambda for the normal displacement divided by time step. */
|
||||
Span<float> dynamic_friction_terms_;
|
||||
Span<float> lambdas_normal_;
|
||||
Span<float> point_mix_factors_;
|
||||
MutableSpan<float> lambdas_;
|
||||
|
||||
public:
|
||||
static constexpr StringRefNull debug_name = "Friction";
|
||||
|
||||
FrictionEdgeConstraintSet(const int geo_i,
|
||||
const Span<int2> point_pairs,
|
||||
const Span<float3> separating_axes,
|
||||
const Span<float3> contact_velocities,
|
||||
const Span<float> dynamic_friction_terms,
|
||||
const Span<float> lambdas_normal,
|
||||
const Span<float> point_mix_factors,
|
||||
MutableSpan<float> lambdas)
|
||||
: TemplatedVelocityConstraintSet<FrictionEdgeConstraintSet>(point_pairs.size(), {geo_i}),
|
||||
geo_i_(geo_i),
|
||||
point_pairs_(point_pairs),
|
||||
separating_axes_(separating_axes),
|
||||
contact_velocities_(contact_velocities),
|
||||
dynamic_friction_terms_(dynamic_friction_terms),
|
||||
lambdas_normal_(lambdas_normal),
|
||||
point_mix_factors_(point_mix_factors),
|
||||
lambdas_(lambdas)
|
||||
{
|
||||
}
|
||||
|
||||
void reset_force(const int constraint_i) const
|
||||
{
|
||||
lambdas_[constraint_i] = 0.0f;
|
||||
}
|
||||
|
||||
template<typename UpdaterT>
|
||||
void solve_single(const ConstraintSetParams ¶ms,
|
||||
UpdaterT &updater,
|
||||
const int constraint_i) const
|
||||
{
|
||||
const int2 point_pair = point_pairs_[constraint_i];
|
||||
const float segment_factor = point_mix_factors_[constraint_i];
|
||||
|
||||
/* Effective weight/mass */
|
||||
const float inv_m0 = params.inv_mass(geo_i_, point_pair[0]);
|
||||
const float inv_m1 = params.inv_mass(geo_i_, point_pair[1]);
|
||||
const float weight0 = (1.0f - segment_factor) * inv_m0;
|
||||
const float weight1 = segment_factor * inv_m1;
|
||||
/* Should be inactive if both are zero. */
|
||||
BLI_assert(weight0 > 0.0f || weight1 > 0.0f);
|
||||
|
||||
const float dynamic_friction = dynamic_friction_terms_[constraint_i] *
|
||||
params.dynamic_friction_factor;
|
||||
const float lambda_normal = lambdas_normal_[constraint_i];
|
||||
const float3 &axis = separating_axes_[constraint_i];
|
||||
const float3 &contact_velocity = contact_velocities_[constraint_i];
|
||||
const float3 &velocity = math::interpolate(params.velocity(geo_i_, point_pair[0]),
|
||||
params.velocity(geo_i_, point_pair[1]),
|
||||
segment_factor) -
|
||||
contact_velocity;
|
||||
const float3 velocity_tangent = velocity - math::dot(velocity, axis) * axis;
|
||||
float residual;
|
||||
const float3 gradient = math::normalize_and_get_length(velocity_tangent, residual);
|
||||
const float delta_lambda = std::min(dynamic_friction * lambda_normal,
|
||||
residual / (weight0 + weight1));
|
||||
|
||||
lambdas_[constraint_i] += delta_lambda;
|
||||
updater.update_velocity(geo_i_, point_pair[0], -weight0 * gradient * delta_lambda);
|
||||
updater.update_velocity(geo_i_, point_pair[1], -weight1 * gradient * delta_lambda);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender::xpbd
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "GEO_xpbd_constraint_set_templated.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
class FrictionFaceConstraintSet
|
||||
: public TemplatedVelocityConstraintSet<FrictionFaceConstraintSet> {
|
||||
private:
|
||||
int geo_i_;
|
||||
/* Constraint index for each point. */
|
||||
Span<int> points_;
|
||||
Span<float3> separating_axes_;
|
||||
Span<float3> contact_velocities_;
|
||||
/* Constraint multiplier lambda for the normal displacement divided by time step. */
|
||||
Span<float> dynamic_friction_terms_;
|
||||
Span<float> lambdas_normal_;
|
||||
MutableSpan<float> lambdas_;
|
||||
|
||||
public:
|
||||
static constexpr StringRefNull debug_name = "Friction";
|
||||
|
||||
FrictionFaceConstraintSet(const int geo_i,
|
||||
const Span<int> points,
|
||||
const Span<float3> separating_axes,
|
||||
const Span<float3> contact_velocities,
|
||||
const Span<float> dynamic_friction_terms,
|
||||
const Span<float> lambdas_normal,
|
||||
MutableSpan<float> lambdas)
|
||||
: TemplatedVelocityConstraintSet<FrictionFaceConstraintSet>(points.size(), {geo_i}),
|
||||
geo_i_(geo_i),
|
||||
points_(points),
|
||||
separating_axes_(separating_axes),
|
||||
contact_velocities_(contact_velocities),
|
||||
dynamic_friction_terms_(dynamic_friction_terms),
|
||||
lambdas_normal_(lambdas_normal),
|
||||
lambdas_(lambdas)
|
||||
{
|
||||
}
|
||||
|
||||
void reset_force(const int constraint_i) const
|
||||
{
|
||||
lambdas_[constraint_i] = 0.0f;
|
||||
}
|
||||
|
||||
template<typename UpdaterT>
|
||||
void solve_single(const ConstraintSetParams ¶ms,
|
||||
UpdaterT &updater,
|
||||
const int constraint_i) const
|
||||
{
|
||||
const int point_i = points_[constraint_i];
|
||||
const float inv_m = params.inv_mass(geo_i_, point_i);
|
||||
/* Should be inactive if weight is zero. */
|
||||
BLI_assert(inv_m > 0.0f);
|
||||
|
||||
const float dynamic_friction = dynamic_friction_terms_[constraint_i] *
|
||||
params.dynamic_friction_factor;
|
||||
const float lambda_normal = lambdas_normal_[constraint_i];
|
||||
const float3 &axis = separating_axes_[constraint_i];
|
||||
const float3 &contact_velocity = contact_velocities_[constraint_i];
|
||||
const float3 &velocity = params.velocity(geo_i_, point_i) - contact_velocity;
|
||||
const float3 velocity_tangent = velocity - math::dot(velocity, axis) * axis;
|
||||
float residual;
|
||||
const float3 gradient = math::normalize_and_get_length(velocity_tangent, residual);
|
||||
/* Note: lambda_normal already includes the 1/inv_m weighting factor. */
|
||||
const float delta_lambda = std::min(dynamic_friction * lambda_normal, residual / inv_m);
|
||||
|
||||
lambdas_[constraint_i] += delta_lambda;
|
||||
updater.update_velocity(geo_i_, point_i, -gradient * inv_m * delta_lambda);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender::xpbd
|
||||
104
source/blender/geometry/xpbd/GEO_xpbd_constraint_math.hh
Normal file
104
source/blender/geometry/xpbd/GEO_xpbd_constraint_math.hh
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_math_vector.hh"
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
struct PlaneIntersection {
|
||||
/* Intersection point. */
|
||||
float3 position;
|
||||
/* Position of the intersection relative to segment points. */
|
||||
float segment_lambda;
|
||||
};
|
||||
|
||||
/**
|
||||
* Test intersection of a line segment with a plane defined by two tangent vectors.
|
||||
* \param pos0 First point of the line segment.
|
||||
* \param pos1 Second point of the line segment.
|
||||
* \param origin Point on the half-plane edge.
|
||||
* \param edge Edge of the half-plane.
|
||||
* \param normal Direction away from the half-plane.
|
||||
* \return Intersection if the line segment intersects the plane.
|
||||
*/
|
||||
inline std::optional<PlaneIntersection> intersect_plane(const float3 &pos0,
|
||||
const float3 &pos1,
|
||||
const float3 &origin,
|
||||
const float3 &edge,
|
||||
const float3 &normal)
|
||||
{
|
||||
BLI_assert(math::is_unit(edge));
|
||||
BLI_assert(math::is_unit(normal));
|
||||
|
||||
const float3 tangent = math::cross(edge, normal);
|
||||
BLI_assert(math::is_unit(tangent));
|
||||
|
||||
const float len = math::dot(tangent, pos0 - pos1);
|
||||
if (math::abs(len) < 1e-9) {
|
||||
/* Segment is parallel to the plane or too short. */
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const float dist0 = math::dot(tangent, pos0 - origin);
|
||||
const float lambda = dist0 / len;
|
||||
if (lambda < 0.0f || lambda > 1.0f) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const float3 position = math::interpolate(pos0, pos1, lambda);
|
||||
return PlaneIntersection{position, lambda};
|
||||
}
|
||||
|
||||
struct SegmentClosestToRay {
|
||||
/* Position relative to segment points. */
|
||||
float segment_lambda;
|
||||
/* Distance of the closest point along the ray, unclamped. */
|
||||
float ray_lambda;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find closest point of a segment to a ray.
|
||||
* \param pos0 First point of the line segment.
|
||||
* \param pos1 Second point of the line segment.
|
||||
* \param ray_pos Origin of the ray.
|
||||
* \param ray_dir Direction of the ray.
|
||||
* \return Closest point on the segment or null if the closest point is outside the segment.
|
||||
*/
|
||||
inline SegmentClosestToRay closest_on_segment_to_ray(const float3 &pos0,
|
||||
const float3 &pos1,
|
||||
const float3 &ray_pos,
|
||||
const float3 &ray_dir,
|
||||
const bool clamp)
|
||||
{
|
||||
BLI_assert(math::is_unit(ray_dir));
|
||||
|
||||
const float3 segment = pos1 - pos0;
|
||||
const float3 dist0 = pos0 - ray_pos;
|
||||
const float a = math::dot(segment, dist0);
|
||||
const float b = math::dot(ray_dir, dist0);
|
||||
const float c = math::dot(segment, ray_dir);
|
||||
|
||||
const float len_segment_sq = math::length_squared(segment);
|
||||
if (UNLIKELY(len_segment_sq <= 1e-9)) {
|
||||
return SegmentClosestToRay{0.0f, b};
|
||||
}
|
||||
|
||||
float segment_lambda;
|
||||
if (clamp) {
|
||||
segment_lambda = math::clamp(math::safe_divide(c * b - a, len_segment_sq - c * c), 0.0f, 1.0f);
|
||||
}
|
||||
else {
|
||||
segment_lambda = math::safe_divide(c * b - a, len_segment_sq - c * c);
|
||||
}
|
||||
|
||||
const float ray_lambda = c * segment_lambda + b;
|
||||
|
||||
return SegmentClosestToRay{segment_lambda, ray_lambda};
|
||||
}
|
||||
|
||||
} // namespace blender::xpbd
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "GEO_xpbd_constraint_coloring_utils.hh"
|
||||
#include "GEO_xpbd_constraint_distance.hh"
|
||||
#include "GEO_xpbd_constraint_set_templated.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
class PinPositionConstraintSet : public TemplatedConstraintSet<PinPositionConstraintSet> {
|
||||
private:
|
||||
/** Indexed by constraint index. */
|
||||
Span<int> point_indices_;
|
||||
Span<float3> pin_positions_;
|
||||
Span<float> compliances_;
|
||||
MutableSpan<float> lambdas_;
|
||||
|
||||
public:
|
||||
static constexpr StringRefNull debug_name = "Pinned Position";
|
||||
|
||||
PinPositionConstraintSet(const int geo_i,
|
||||
const Span<int> point_indices,
|
||||
const Span<float3> pin_positions,
|
||||
const Span<float> compliances,
|
||||
const MutableSpan<float> lambdas)
|
||||
: TemplatedConstraintSet<PinPositionConstraintSet>(point_indices.size(), {geo_i}),
|
||||
point_indices_(point_indices),
|
||||
pin_positions_(pin_positions),
|
||||
compliances_(compliances),
|
||||
lambdas_(lambdas)
|
||||
{
|
||||
}
|
||||
|
||||
void reset_force(const int constraint_i) const
|
||||
{
|
||||
lambdas_[constraint_i] = 0.0f;
|
||||
}
|
||||
|
||||
template<typename UpdaterT>
|
||||
void solve_single(const ConstraintSetParams ¶ms,
|
||||
UpdaterT &updater,
|
||||
const int constraint_i) const
|
||||
{
|
||||
const int geo_i = affected_geo_indices_[0];
|
||||
const int point_i = point_indices_[constraint_i];
|
||||
const DistanceConstraintResult result = evaluate_distance_constraint(
|
||||
params.position(geo_i, point_i),
|
||||
pin_positions_[constraint_i],
|
||||
params.inv_mass(geo_i, point_i),
|
||||
0.0f,
|
||||
0.0f,
|
||||
compliances_[constraint_i] * params.compliance_term_factor,
|
||||
lambdas_[constraint_i]);
|
||||
lambdas_[constraint_i] += result.delta_lambda;
|
||||
updater.update_position(geo_i, point_i, result.offset0);
|
||||
}
|
||||
|
||||
ConstraintColoring color_constraints(IndexMaskMemory &memory) const override
|
||||
{
|
||||
return color_constraints__unary(point_indices_, memory);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender::xpbd
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "GEO_xpbd_constraint_align_rotations.hh"
|
||||
#include "GEO_xpbd_constraint_coloring_utils.hh"
|
||||
#include "GEO_xpbd_constraint_set_templated.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
class PinRotationConstraintSet : public TemplatedConstraintSet<PinRotationConstraintSet> {
|
||||
private:
|
||||
/** Indexed by constraint index. */
|
||||
Span<float> compliances_;
|
||||
Span<int> point_indices_;
|
||||
Span<math::Quaternion> pin_rotations_;
|
||||
MutableSpan<float4> lambdas_;
|
||||
|
||||
public:
|
||||
static constexpr StringRefNull debug_name = "Pin Rotation";
|
||||
|
||||
PinRotationConstraintSet(const int geo_i,
|
||||
const Span<int> point_indices,
|
||||
const Span<math::Quaternion> pin_rotations,
|
||||
const Span<float> compliances,
|
||||
MutableSpan<float4> lambdas)
|
||||
: TemplatedConstraintSet<PinRotationConstraintSet>(point_indices.size(), {geo_i}),
|
||||
compliances_(compliances),
|
||||
point_indices_(point_indices),
|
||||
pin_rotations_(pin_rotations),
|
||||
lambdas_(lambdas)
|
||||
{
|
||||
}
|
||||
|
||||
void reset_force(const int constraint_i) const
|
||||
{
|
||||
lambdas_[constraint_i] = float4(0.0f);
|
||||
}
|
||||
|
||||
template<typename UpdaterT>
|
||||
void solve_single(const ConstraintSetParams ¶ms,
|
||||
UpdaterT &updater,
|
||||
const int constraint_i) const
|
||||
{
|
||||
const int geo_i = affected_geo_indices_[0];
|
||||
const int point_i = point_indices_[constraint_i];
|
||||
const AlignRotationsConstraintResult result = evaluate_align_rotations_constraint(
|
||||
params.rotation(geo_i, point_i),
|
||||
pin_rotations_[constraint_i],
|
||||
params.moment_of_inertia(geo_i, point_i),
|
||||
float3(std::numeric_limits<float>::infinity()),
|
||||
math::Quaternion::identity(),
|
||||
compliances_[constraint_i] * params.compliance_term_factor,
|
||||
lambdas_[constraint_i]);
|
||||
lambdas_[constraint_i] += result.delta_lambda;
|
||||
updater.update_rotation(geo_i, point_i, result.offset0);
|
||||
}
|
||||
|
||||
ConstraintColoring color_constraints(IndexMaskMemory &memory) const override
|
||||
{
|
||||
return color_constraints__unary(point_indices_, memory);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender::xpbd
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "GEO_xpbd_constraint_align_rotations.hh"
|
||||
#include "GEO_xpbd_constraint_coloring_utils.hh"
|
||||
#include "GEO_xpbd_constraint_set_templated.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
/** Aligns rotations of two consecutive rods based on a rest rotation. */
|
||||
class RodBendAndTwistConstraintSet : public TemplatedConstraintSet<RodBendAndTwistConstraintSet> {
|
||||
private:
|
||||
/** Curves that are effected by this constraint set. Each curve is seen as one constraint. */
|
||||
IndexRange curves_range_;
|
||||
OffsetIndices<int> points_by_curve_;
|
||||
|
||||
/** Indexed by point index. */
|
||||
Span<math::Quaternion> rest_rotations_;
|
||||
MutableSpan<float4> lambdas_;
|
||||
|
||||
/** Indexed by `point_i - first_point_i_in_constraint_set`. */
|
||||
Span<float> compliances_;
|
||||
|
||||
public:
|
||||
static constexpr StringRefNull debug_name = "Rod Bend and Twist";
|
||||
|
||||
RodBendAndTwistConstraintSet(const int geo_i,
|
||||
const IndexRange curves_range,
|
||||
const OffsetIndices<int> points_by_curve,
|
||||
const Span<math::Quaternion> rest_rotations,
|
||||
const Span<float> compliances,
|
||||
MutableSpan<float4> lambdas)
|
||||
: TemplatedConstraintSet<RodBendAndTwistConstraintSet>(curves_range.size(), {geo_i}),
|
||||
curves_range_(curves_range),
|
||||
points_by_curve_(points_by_curve),
|
||||
rest_rotations_(rest_rotations),
|
||||
lambdas_(lambdas),
|
||||
compliances_(compliances)
|
||||
{
|
||||
}
|
||||
|
||||
void reset_force(const int constraint_i) const
|
||||
{
|
||||
const int curve_i = curves_range_[constraint_i];
|
||||
const IndexRange points = points_by_curve_[curve_i];
|
||||
lambdas_.slice(points).fill(float4(0.0f));
|
||||
}
|
||||
|
||||
template<typename UpdaterT>
|
||||
void solve_single(const ConstraintSetParams ¶ms,
|
||||
UpdaterT &updater,
|
||||
const int constraint_i) const
|
||||
{
|
||||
const int curve_i = curves_range_[constraint_i];
|
||||
const IndexRange points = points_by_curve_[curve_i];
|
||||
const int geo_i = affected_geo_indices_[0];
|
||||
const int first_point_i_in_constraint_set = points_by_curve_[curves_range_.first()].first();
|
||||
|
||||
/* Could implement bilateral interleaving ordering for better stability. */
|
||||
/* Note that the last segment does not have this constraint, because the rotation of the last
|
||||
* point in the rod is meaningless.*/
|
||||
for (const int point_i0 : points.drop_back(2)) {
|
||||
const int point_i1 = point_i0 + 1;
|
||||
const float compliance = compliances_[point_i0 - first_point_i_in_constraint_set];
|
||||
const AlignRotationsConstraintResult result = evaluate_align_rotations_constraint(
|
||||
params.rotation(geo_i, point_i0),
|
||||
params.rotation(geo_i, point_i1),
|
||||
params.moment_of_inertia(geo_i, point_i0),
|
||||
params.moment_of_inertia(geo_i, point_i1),
|
||||
rest_rotations_[point_i0],
|
||||
compliance * params.compliance_term_factor,
|
||||
lambdas_[point_i0]);
|
||||
lambdas_[point_i0] += result.delta_lambda;
|
||||
updater.update_rotation(geo_i, point_i0, result.offset0);
|
||||
updater.update_rotation(geo_i, point_i1, result.offset1);
|
||||
}
|
||||
}
|
||||
|
||||
ConstraintColoring color_constraints(IndexMaskMemory & /*memory*/) const override
|
||||
{
|
||||
return color_constraints__all_independent(constraints_num_);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender::xpbd
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_math_base.h"
|
||||
|
||||
#include "GEO_xpbd_constraint_coloring_utils.hh"
|
||||
#include "GEO_xpbd_constraint_set_templated.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
struct RodStretchAndShearConstraintResult {
|
||||
float3 delta_lambda_pos;
|
||||
float3 delta_lambda_rot;
|
||||
float3 offset0 = float3(0.0f);
|
||||
float3 offset1 = float3(0.0f);
|
||||
math::Quaternion offset_rot = math::Quaternion(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
};
|
||||
|
||||
inline RodStretchAndShearConstraintResult evaluate_rod_stretch_and_shear_constraint(
|
||||
const float3 &p0,
|
||||
const float3 &p1,
|
||||
const math::Quaternion &rot,
|
||||
const float inv_m0,
|
||||
const float inv_m1,
|
||||
const float3 &inertia,
|
||||
const float rest_length,
|
||||
const float compliance_term,
|
||||
const float3 &lambda_pos_prev,
|
||||
const float3 &lambda_rot_prev)
|
||||
{
|
||||
/* Lumped weight for the rotation influence. The higher the inertia, the lower the change of
|
||||
* the rotation should be compared to the change in point positions. */
|
||||
const float inv_lumped_inertia = math::safe_rcp(0.5f * (inertia.x + inertia.y + inertia.z));
|
||||
|
||||
if (inv_m0 == 0.0f && inv_m1 == 0.0f && inv_lumped_inertia == 0.0f) {
|
||||
/* Everything is pinned, so the constraint can't do anything. */
|
||||
return {};
|
||||
}
|
||||
|
||||
/* TODO The positional and rotational parts use different residuals to avoid errors when the
|
||||
* current segment length deviates too much from the rest length. The rotational offset uses
|
||||
* the residual as the angle of rotation which becomes larger with stretching. To avoid
|
||||
* instabilities the rotation residual is computed relative to the current length.
|
||||
* This should be cleaned up and optimized if possible. */
|
||||
|
||||
/* Current non-normalized tangent of the rod. */
|
||||
const float3 p_diff = p1 - p0;
|
||||
const float p_len = math::length(p_diff);
|
||||
/* Expected non-normalized tangent of the rod based on the rotation. */
|
||||
const float3 forward_rest = math::transform_point(rot, float3(0.0f, 0.0f, rest_length));
|
||||
const float3 forward = math::transform_point(rot, float3(0.0f, 0.0f, p_len));
|
||||
/* How much the rod is stretched and sheared. */
|
||||
const float3 residual_pos = p_diff - forward_rest;
|
||||
const float3 residual_rot = p_diff - forward;
|
||||
|
||||
/* Based on "Position and Orientation Based Cosserat Rods" (Kugelstadt, Schömer, 2016). */
|
||||
const float weight_sum = inv_m0 + inv_m1 + 4.0f * inv_lumped_inertia * pow2f(rest_length);
|
||||
const float weight_sum_rot = inv_m0 + inv_m1 + 4.0f * inv_lumped_inertia * pow2f(p_len);
|
||||
const float3 delta_lambda_pos = (-residual_pos - compliance_term * lambda_pos_prev) /
|
||||
(weight_sum + compliance_term);
|
||||
const float3 delta_lambda_rot = (-residual_rot - compliance_term * lambda_rot_prev) /
|
||||
(weight_sum_rot + compliance_term);
|
||||
|
||||
RodStretchAndShearConstraintResult result;
|
||||
result.delta_lambda_pos = delta_lambda_pos;
|
||||
result.delta_lambda_rot = delta_lambda_rot;
|
||||
result.offset0 = -delta_lambda_pos * inv_m0;
|
||||
result.offset1 = delta_lambda_pos * inv_m1;
|
||||
result.offset_rot = math::Quaternion(0.0f, -delta_lambda_rot * inv_lumped_inertia * p_len) *
|
||||
rot * math::Quaternion(0, 0, 0, -1);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Considers a single rod at a time. Tries to enforce that the rotation of the rod is aligned with
|
||||
* the actual tangent of the rod. If it is misaligned, it moves the start position, end position
|
||||
* and rotation of the frame. At the same time, it enforces a certain length.
|
||||
*/
|
||||
class RodStretchAndShearConstraintSet
|
||||
: public TemplatedConstraintSet<RodStretchAndShearConstraintSet> {
|
||||
private:
|
||||
/** Curves that are effected by this constraint set. Each curve is seen as one constraint. */
|
||||
IndexRange curves_range_;
|
||||
OffsetIndices<int> points_by_curve_;
|
||||
|
||||
/** Indexed by segment-end point index. */
|
||||
Span<float> rest_lengths_;
|
||||
MutableSpan<float3> lambdas_pos_;
|
||||
MutableSpan<float3> lambdas_rot_;
|
||||
|
||||
/** Indexed by `point_i - first_point_i_in_constraint_set`. */
|
||||
Span<float> compliances_;
|
||||
|
||||
public:
|
||||
static constexpr StringRefNull debug_name = "Rod Stretch and Shear";
|
||||
|
||||
RodStretchAndShearConstraintSet(const int geo_i,
|
||||
const IndexRange curves_range,
|
||||
const OffsetIndices<int> points_by_curve,
|
||||
const Span<float> rest_lengths,
|
||||
const Span<float> compliances,
|
||||
MutableSpan<float3> lambdas_pos,
|
||||
MutableSpan<float3> lambdas_rot)
|
||||
: TemplatedConstraintSet<RodStretchAndShearConstraintSet>(curves_range.size(), {geo_i}),
|
||||
curves_range_(curves_range),
|
||||
points_by_curve_(points_by_curve),
|
||||
rest_lengths_(rest_lengths),
|
||||
lambdas_pos_(lambdas_pos),
|
||||
lambdas_rot_(lambdas_rot),
|
||||
compliances_(compliances)
|
||||
{
|
||||
}
|
||||
|
||||
void reset_force(const int constraint_i) const
|
||||
{
|
||||
const int curve_i = curves_range_[constraint_i];
|
||||
const IndexRange points = points_by_curve_[curve_i];
|
||||
lambdas_pos_.slice(points).fill(float3(0.0f));
|
||||
lambdas_rot_.slice(points).fill(float3(0.0f));
|
||||
}
|
||||
|
||||
template<typename UpdaterT>
|
||||
void solve_single(const ConstraintSetParams ¶ms,
|
||||
UpdaterT &updater,
|
||||
const int constraint_i) const
|
||||
{
|
||||
const int curve_i = curves_range_[constraint_i];
|
||||
const IndexRange points = points_by_curve_[curve_i];
|
||||
const int geo_i = affected_geo_indices_[0];
|
||||
const int first_point_i_in_constraint_set = points_by_curve_[curves_range_.first()].first();
|
||||
|
||||
/* Could try implementing bilateral interleaving ordering for better stability. */
|
||||
for (const int point_i0 : points.drop_back(1)) {
|
||||
const int point_i1 = point_i0 + 1;
|
||||
const float compliance = compliances_[point_i0 - first_point_i_in_constraint_set];
|
||||
const RodStretchAndShearConstraintResult result = evaluate_rod_stretch_and_shear_constraint(
|
||||
params.position(geo_i, point_i0),
|
||||
params.position(geo_i, point_i1),
|
||||
params.rotation(geo_i, point_i0),
|
||||
params.inv_mass(geo_i, point_i0),
|
||||
params.inv_mass(geo_i, point_i1),
|
||||
params.moment_of_inertia(geo_i, point_i0),
|
||||
rest_lengths_[point_i1],
|
||||
compliance * params.compliance_term_factor,
|
||||
lambdas_pos_[point_i1],
|
||||
lambdas_rot_[point_i1]);
|
||||
lambdas_pos_[point_i1] += result.delta_lambda_pos;
|
||||
lambdas_rot_[point_i1] += result.delta_lambda_rot;
|
||||
updater.update_position(geo_i, point_i0, result.offset0);
|
||||
updater.update_position(geo_i, point_i1, result.offset1);
|
||||
updater.update_rotation(geo_i, point_i0, result.offset_rot);
|
||||
}
|
||||
}
|
||||
|
||||
ConstraintColoring color_constraints(IndexMaskMemory & /*memory*/) const override
|
||||
{
|
||||
return color_constraints__all_independent(constraints_num_);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender::xpbd
|
||||
77
source/blender/geometry/xpbd/GEO_xpbd_constraint_set.hh
Normal file
77
source/blender/geometry/xpbd/GEO_xpbd_constraint_set.hh
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_index_mask.hh"
|
||||
#include "BLI_string_ref.hh"
|
||||
#include "BLI_vector.hh"
|
||||
|
||||
#include "GEO_xpbd_constraint_coloring.hh"
|
||||
#include "GEO_xpbd_constraint_set_params.hh"
|
||||
#include "GEO_xpbd_updater_gauss_seidel.hh"
|
||||
#include "GEO_xpbd_updater_velocity.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
/**
|
||||
* Base class for constraint evaluators. It evaluate a batch of constraints and writes back the
|
||||
* results using a passed in "updater".
|
||||
*
|
||||
* Use #TemplatedConstraintSet to instantiate the constraint evaluation for each updater
|
||||
* automatically. This avoids having to implement separate Jacobian and Gauss Seidel code paths for
|
||||
* such constraints.
|
||||
*/
|
||||
class ConstraintSet {
|
||||
protected:
|
||||
int constraints_num_;
|
||||
Vector<int> affected_geo_indices_;
|
||||
|
||||
public:
|
||||
ConstraintSet(int constraints_num, Vector<int> affected_geo_indices)
|
||||
: constraints_num_(constraints_num), affected_geo_indices_(std::move(affected_geo_indices))
|
||||
{
|
||||
}
|
||||
|
||||
virtual ~ConstraintSet() = default;
|
||||
|
||||
virtual StringRefNull debug_name() const = 0;
|
||||
virtual void solve_sequential(const ConstraintSetParams ¶ms,
|
||||
GaussSeidelUpdater &updater,
|
||||
const IndexMask &mask) = 0;
|
||||
virtual void reset_forces() = 0;
|
||||
virtual ConstraintColoring color_constraints(IndexMaskMemory &memory) const = 0;
|
||||
|
||||
void solve_sequential_all(const ConstraintSetParams ¶ms, GaussSeidelUpdater &updater)
|
||||
{
|
||||
this->solve_sequential(params, updater, IndexMask(constraints_num_));
|
||||
}
|
||||
|
||||
Span<int> get_affected_geo_indices() const
|
||||
{
|
||||
return affected_geo_indices_;
|
||||
}
|
||||
};
|
||||
|
||||
class VelocityConstraintSet {
|
||||
protected:
|
||||
Vector<int> affected_geo_indices_;
|
||||
|
||||
public:
|
||||
VelocityConstraintSet(Vector<int> affected_geo_indices)
|
||||
: affected_geo_indices_(std::move(affected_geo_indices))
|
||||
{
|
||||
}
|
||||
virtual ~VelocityConstraintSet() = default;
|
||||
|
||||
virtual void reset_forces() = 0;
|
||||
virtual void solve_sequential(const ConstraintSetParams ¶ms, VelocityUpdater &updater) = 0;
|
||||
|
||||
Span<int> get_affected_geo_indices() const
|
||||
{
|
||||
return affected_geo_indices_;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender::xpbd
|
||||
166
source/blender/geometry/xpbd/GEO_xpbd_constraint_set_params.hh
Normal file
166
source/blender/geometry/xpbd/GEO_xpbd_constraint_set_params.hh
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "GEO_xpbd_geometry_ref.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
/** Provides access to the input data that should be considered by a constraint. */
|
||||
class ConstraintSetParams {
|
||||
private:
|
||||
Span<GeometryRef> geometry_refs_;
|
||||
|
||||
public:
|
||||
float delta_time;
|
||||
float compliance_term_factor;
|
||||
float dynamic_friction_factor;
|
||||
|
||||
ConstraintSetParams(Span<GeometryRef> geometry_refs, float delta_time);
|
||||
|
||||
Span<GeometryRef> geometry_refs() const;
|
||||
|
||||
Span<float3> positions(int geo_i) const;
|
||||
const float3 &position(int geo_i, int point_i) const;
|
||||
|
||||
Span<math::Quaternion> rotations(int geo_i) const;
|
||||
const math::Quaternion &rotation(int geo_i, int point_i) const;
|
||||
|
||||
Span<float3> prev_positions(int geo_i) const;
|
||||
const float3 &prev_position(int geo_i, int point_i) const;
|
||||
|
||||
Span<math::Quaternion> prev_rotations(int geo_i) const;
|
||||
const math::Quaternion &prev_rotation(int geo_i, int point_i) const;
|
||||
|
||||
Span<float3> velocities(int geo_i) const;
|
||||
const float3 &velocity(int geo_i, int point_i) const;
|
||||
|
||||
Span<float3> angular_velocities(int geo_i) const;
|
||||
const float3 &angular_velocity(int geo_i, int point_i) const;
|
||||
|
||||
Span<float> inv_masses(int geo_i) const;
|
||||
float inv_mass(int geo_i, int point_i) const;
|
||||
|
||||
Span<float3> moments_of_inertia(int geo_i) const;
|
||||
float3 moment_of_inertia(int geo_i, int point_i) const;
|
||||
|
||||
Span<float3> inv_moments_of_inertia(int geo_i) const;
|
||||
float3 inv_moment_of_inertia(int geo_i, int point_i) const;
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Inline Functions
|
||||
* \{ */
|
||||
|
||||
inline ConstraintSetParams::ConstraintSetParams(Span<GeometryRef> geometry_refs,
|
||||
const float delta_time)
|
||||
: geometry_refs_(geometry_refs),
|
||||
delta_time(delta_time),
|
||||
compliance_term_factor(math::safe_rcp(delta_time * delta_time)),
|
||||
dynamic_friction_factor(math::safe_rcp(delta_time))
|
||||
{
|
||||
}
|
||||
|
||||
inline Span<GeometryRef> ConstraintSetParams::geometry_refs() const
|
||||
{
|
||||
return geometry_refs_;
|
||||
}
|
||||
|
||||
inline const float3 &ConstraintSetParams::position(const int geo_i, const int point_i) const
|
||||
{
|
||||
return geometry_refs_[geo_i].positions[point_i];
|
||||
}
|
||||
|
||||
inline const math::Quaternion &ConstraintSetParams::rotation(const int geo_i,
|
||||
const int point_i) const
|
||||
{
|
||||
return geometry_refs_[geo_i].rotations[point_i];
|
||||
}
|
||||
|
||||
inline const float3 &ConstraintSetParams::prev_position(const int geo_i, const int point_i) const
|
||||
{
|
||||
return geometry_refs_[geo_i].prev_positions[point_i];
|
||||
}
|
||||
|
||||
inline const math::Quaternion &ConstraintSetParams::prev_rotation(const int geo_i,
|
||||
const int point_i) const
|
||||
{
|
||||
return geometry_refs_[geo_i].prev_rotations[point_i];
|
||||
}
|
||||
|
||||
inline const float3 &ConstraintSetParams::velocity(const int geo_i, const int point_i) const
|
||||
{
|
||||
return geometry_refs_[geo_i].velocities[point_i];
|
||||
}
|
||||
|
||||
inline const float3 &ConstraintSetParams::angular_velocity(const int geo_i,
|
||||
const int point_i) const
|
||||
{
|
||||
return geometry_refs_[geo_i].angular_velocities[point_i];
|
||||
}
|
||||
|
||||
inline Span<float3> ConstraintSetParams::positions(const int geo_i) const
|
||||
{
|
||||
return geometry_refs_[geo_i].positions;
|
||||
}
|
||||
|
||||
inline Span<math::Quaternion> ConstraintSetParams::rotations(const int geo_i) const
|
||||
{
|
||||
return geometry_refs_[geo_i].rotations;
|
||||
}
|
||||
|
||||
inline Span<float3> ConstraintSetParams::prev_positions(const int geo_i) const
|
||||
{
|
||||
return geometry_refs_[geo_i].prev_positions;
|
||||
}
|
||||
|
||||
inline Span<math::Quaternion> ConstraintSetParams::prev_rotations(const int geo_i) const
|
||||
{
|
||||
return geometry_refs_[geo_i].prev_rotations;
|
||||
}
|
||||
|
||||
inline Span<float3> ConstraintSetParams::velocities(const int geo_i) const
|
||||
{
|
||||
return geometry_refs_[geo_i].velocities;
|
||||
}
|
||||
|
||||
inline Span<float3> ConstraintSetParams::angular_velocities(const int geo_i) const
|
||||
{
|
||||
return geometry_refs_[geo_i].angular_velocities;
|
||||
}
|
||||
|
||||
inline float ConstraintSetParams::inv_mass(const int geo_i, const int point_i) const
|
||||
{
|
||||
return geometry_refs_[geo_i].inv_masses[point_i];
|
||||
}
|
||||
|
||||
inline Span<float> ConstraintSetParams::inv_masses(const int geo_i) const
|
||||
{
|
||||
return geometry_refs_[geo_i].inv_masses;
|
||||
}
|
||||
|
||||
inline float3 ConstraintSetParams::moment_of_inertia(const int geo_i, const int point_i) const
|
||||
{
|
||||
return geometry_refs_[geo_i].moments_of_inertia[point_i];
|
||||
}
|
||||
|
||||
inline Span<float3> ConstraintSetParams::moments_of_inertia(const int geo_i) const
|
||||
{
|
||||
return geometry_refs_[geo_i].moments_of_inertia;
|
||||
}
|
||||
|
||||
inline float3 ConstraintSetParams::inv_moment_of_inertia(const int geo_i, const int point_i) const
|
||||
{
|
||||
return geometry_refs_[geo_i].inv_moments_of_inertia[point_i];
|
||||
}
|
||||
|
||||
inline Span<float3> ConstraintSetParams::inv_moments_of_inertia(const int geo_i) const
|
||||
{
|
||||
return geometry_refs_[geo_i].inv_moments_of_inertia;
|
||||
}
|
||||
|
||||
/** \} */
|
||||
|
||||
} // namespace blender::xpbd
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "GEO_xpbd_constraint_set.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
/**
|
||||
* Utility to implement a constraint evaluator that automatically works with multiple updaters like
|
||||
* #GaussSeidelUpdater.
|
||||
*
|
||||
* Child classes have to implement the templated #solve_single method.
|
||||
*/
|
||||
template<typename Child> class TemplatedConstraintSet : public ConstraintSet {
|
||||
public:
|
||||
TemplatedConstraintSet(const int constraints_num, Vector<int> affected_geo_indices)
|
||||
: ConstraintSet(constraints_num, std::move(affected_geo_indices))
|
||||
{
|
||||
}
|
||||
|
||||
void reset_forces() override
|
||||
{
|
||||
const Child &self = static_cast<const Child &>(*this);
|
||||
for (const int constraint_i : IndexRange(constraints_num_)) {
|
||||
self.reset_force(constraint_i);
|
||||
}
|
||||
}
|
||||
|
||||
void solve_sequential(const ConstraintSetParams ¶ms,
|
||||
GaussSeidelUpdater &updater,
|
||||
const IndexMask &mask) override
|
||||
{
|
||||
const Child &self = static_cast<const Child &>(*this);
|
||||
mask.foreach_index(
|
||||
[&](const int64_t constraint_i) { self.solve_single(params, updater, constraint_i); });
|
||||
}
|
||||
|
||||
StringRefNull debug_name() const final
|
||||
{
|
||||
return Child::debug_name;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename Child> class TemplatedVelocityConstraintSet : public VelocityConstraintSet {
|
||||
protected:
|
||||
const int constraint_num_;
|
||||
|
||||
public:
|
||||
TemplatedVelocityConstraintSet(int constraints_num, Vector<int> affected_geo_indices)
|
||||
: VelocityConstraintSet(std::move(affected_geo_indices)), constraint_num_(constraints_num)
|
||||
{
|
||||
}
|
||||
|
||||
void reset_forces() override
|
||||
{
|
||||
Child &self = static_cast<Child &>(*this);
|
||||
for (const int constraint_i : IndexRange(constraint_num_)) {
|
||||
self.reset_force(constraint_i);
|
||||
}
|
||||
}
|
||||
|
||||
void solve_sequential(const ConstraintSetParams ¶ms, VelocityUpdater &updater) override
|
||||
{
|
||||
Child &self = static_cast<Child &>(*this);
|
||||
for (const int constraint_i : IndexRange(constraint_num_)) {
|
||||
self.solve_single(params, updater, constraint_i);
|
||||
}
|
||||
}
|
||||
|
||||
StringRef debug_name() const
|
||||
{
|
||||
return Child::debug_name;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender::xpbd
|
||||
43
source/blender/geometry/xpbd/GEO_xpbd_geometry_ref.hh
Normal file
43
source/blender/geometry/xpbd/GEO_xpbd_geometry_ref.hh
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_math_quaternion_types.hh"
|
||||
#include "BLI_math_vector_types.hh"
|
||||
#include "BLI_span.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
/**
|
||||
* References to the data of a geometry that is being simulated.
|
||||
*/
|
||||
struct GeometryRef {
|
||||
/** The position of each point. */
|
||||
MutableSpan<float3> positions;
|
||||
/** The linear velocity of each point. */
|
||||
MutableSpan<float3> velocities;
|
||||
/** Positions before time integration, at the beginning of the current substep. */
|
||||
Span<float3> prev_positions;
|
||||
/** Inverse mass of each point. */
|
||||
Span<float> inv_masses;
|
||||
|
||||
/** Optional rotation data. */
|
||||
MutableSpan<math::Quaternion> rotations;
|
||||
/** Optional angular_velocity data. */
|
||||
MutableSpan<float3> angular_velocities;
|
||||
/** Rotations before time integration. */
|
||||
Span<math::Quaternion> prev_rotations;
|
||||
/** Optional moment of inertia of each point. */
|
||||
Span<float3> moments_of_inertia;
|
||||
/** Inverse of the above. */
|
||||
Span<float3> inv_moments_of_inertia;
|
||||
|
||||
uint64_t size() const
|
||||
{
|
||||
return this->positions.size();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender::xpbd
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_math_quaternion.hh"
|
||||
|
||||
#include "GEO_xpbd_geometry_ref.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
inline math::Quaternion apply_rotation_offset(math::Quaternion rotation, float4 offset)
|
||||
{
|
||||
return math::normalize(math::Quaternion(float4(rotation) + offset));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updater that writes the changes directly to the simulated points.
|
||||
*/
|
||||
class GaussSeidelUpdater {
|
||||
private:
|
||||
Span<GeometryRef> geometry_refs_;
|
||||
|
||||
public:
|
||||
GaussSeidelUpdater(Span<GeometryRef> geometry_refs) : geometry_refs_(geometry_refs) {}
|
||||
|
||||
void update_position(const int geo_i, const int point_i, const float3 &offset)
|
||||
{
|
||||
geometry_refs_[geo_i].positions[point_i] += offset;
|
||||
}
|
||||
void update_rotation(const int geo_i, const int point_i, const math::Quaternion &offset)
|
||||
{
|
||||
math::Quaternion &rotation = geometry_refs_[geo_i].rotations[point_i];
|
||||
rotation = apply_rotation_offset(rotation, float4(offset));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender::xpbd
|
||||
32
source/blender/geometry/xpbd/GEO_xpbd_updater_velocity.hh
Normal file
32
source/blender/geometry/xpbd/GEO_xpbd_updater_velocity.hh
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "GEO_xpbd_geometry_ref.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
/**
|
||||
* Updater that writes the changes directly to the simulated points.
|
||||
*/
|
||||
class VelocityUpdater {
|
||||
private:
|
||||
Span<GeometryRef> geometry_refs_;
|
||||
|
||||
public:
|
||||
VelocityUpdater(const Span<GeometryRef> geometry_refs) : geometry_refs_(geometry_refs) {}
|
||||
|
||||
void update_velocity(const int geo_i, const int point_i, const float3 &offset)
|
||||
{
|
||||
geometry_refs_[geo_i].velocities[point_i] += offset;
|
||||
}
|
||||
|
||||
void update_angular_velocity(const int geo_i, const int point_i, const float3 &offset)
|
||||
{
|
||||
geometry_refs_[geo_i].angular_velocities[point_i] += offset;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender::xpbd
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "BLI_multi_value_map.hh"
|
||||
|
||||
#include "GEO_xpbd_constraint_coloring_utils.hh"
|
||||
|
||||
namespace blender::xpbd {
|
||||
|
||||
template<typename PointID, typename GetConstraintPointIdsFn>
|
||||
inline int color_constraints(GetConstraintPointIdsFn &&get_constraint_point_ids_fn,
|
||||
MutableSpan<int> r_colors)
|
||||
{
|
||||
const int constraints_num = r_colors.size();
|
||||
MultiValueMap<PointID, int> constraints_by_point;
|
||||
for (const int constraint_i : IndexRange(constraints_num)) {
|
||||
for (const PointID &point_id : get_constraint_point_ids_fn(constraint_i)) {
|
||||
constraints_by_point.add(point_id, constraint_i);
|
||||
}
|
||||
}
|
||||
int colors_num = 0;
|
||||
for (const int constraint_i : IndexRange(constraints_num)) {
|
||||
Vector<int> used_colors;
|
||||
for (const PointID &point_id : get_constraint_point_ids_fn(constraint_i)) {
|
||||
for (const int other_constraint_i : constraints_by_point.lookup(point_id)) {
|
||||
if (other_constraint_i >= constraint_i) {
|
||||
continue;
|
||||
}
|
||||
used_colors.append_non_duplicates(r_colors[other_constraint_i]);
|
||||
}
|
||||
}
|
||||
int best_color = 0;
|
||||
while (used_colors.contains(best_color)) {
|
||||
best_color++;
|
||||
}
|
||||
r_colors[constraint_i] = best_color;
|
||||
colors_num = std::max(colors_num, best_color + 1);
|
||||
}
|
||||
return colors_num;
|
||||
}
|
||||
|
||||
template<typename PointID, typename GetConstraintPointIdsFn>
|
||||
inline ConstraintColoring generic_constraint_coloring(
|
||||
GetConstraintPointIdsFn &&get_constraint_points_fn,
|
||||
const int constraints_num,
|
||||
IndexMaskMemory &memory)
|
||||
{
|
||||
if (constraints_num == 0) {
|
||||
return {};
|
||||
}
|
||||
Array<int> colors(constraints_num);
|
||||
const int colors_num = color_constraints<PointID>(get_constraint_points_fn, colors);
|
||||
Array<Vector<int>> color_indices(colors_num);
|
||||
for (const int constraint_i : IndexRange(constraints_num)) {
|
||||
color_indices[colors[constraint_i]].append(constraint_i);
|
||||
}
|
||||
ConstraintColoring coloring;
|
||||
for (const int color_i : IndexRange(colors_num)) {
|
||||
const IndexMask mask = IndexMask::from_indices<int>(color_indices[color_i], memory);
|
||||
coloring.colors.append(mask);
|
||||
}
|
||||
return coloring;
|
||||
}
|
||||
|
||||
ConstraintColoring color_constraints__unary(const Span<int> affected_points,
|
||||
IndexMaskMemory &memory)
|
||||
{
|
||||
return generic_constraint_coloring<int>(
|
||||
[&](const int constraint_i) { return Span<int>(&affected_points[constraint_i], 1); },
|
||||
affected_points.size(),
|
||||
memory);
|
||||
}
|
||||
|
||||
ConstraintColoring color_constraints__binary(const Span<int2> affected_points,
|
||||
IndexMaskMemory &memory)
|
||||
{
|
||||
return generic_constraint_coloring<int>(
|
||||
[&](const int constraint_i) { return Span<int>(&affected_points[constraint_i][0], 2); },
|
||||
affected_points.size(),
|
||||
memory);
|
||||
}
|
||||
|
||||
ConstraintColoring color_constraints__n_ary(const GroupedSpan<int> affected_points,
|
||||
IndexMaskMemory &memory)
|
||||
{
|
||||
return generic_constraint_coloring<int>(
|
||||
[&](const int constraint_i) { return affected_points[constraint_i]; },
|
||||
affected_points.size(),
|
||||
memory);
|
||||
}
|
||||
|
||||
ConstraintColoring color_constraints__all_independent(const int constraints_num)
|
||||
{
|
||||
return ConstraintColoring{{IndexMask(constraints_num)}};
|
||||
}
|
||||
|
||||
} // namespace blender::xpbd
|
||||
|
|
@ -10463,6 +10463,7 @@ static void rna_def_nodes(BlenderRNA *brna)
|
|||
define("FunctionNode", "FunctionNodeValueToString");
|
||||
|
||||
define("GeometryNode", "GeometryNodeAccumulateField");
|
||||
define("GeometryNode", "GeometryNodeApplySimulatedData");
|
||||
define("GeometryNode", "GeometryNodeAttributeDomainSize");
|
||||
define("GeometryNode", "GeometryNodeAttributeStatistic");
|
||||
define("GeometryNode", "GeometryNodeBake", rna_def_geo_bake);
|
||||
|
|
@ -10697,6 +10698,7 @@ static void rna_def_nodes(BlenderRNA *brna)
|
|||
define("GeometryNode", "GeometryNodeSubdivideMesh");
|
||||
define("GeometryNode", "GeometryNodeSubdivisionSurface");
|
||||
define("GeometryNode", "GeometryNodeSwitch");
|
||||
define("GeometryNode", "GeometryNodeTagFilter");
|
||||
define("GeometryNode", "GeometryNodeTool3DCursor");
|
||||
define("GeometryNode", "GeometryNodeToolActiveElement");
|
||||
define("GeometryNode", "GeometryNodeToolFaceSet");
|
||||
|
|
@ -10706,6 +10708,7 @@ static void rna_def_nodes(BlenderRNA *brna)
|
|||
define("GeometryNode", "GeometryNodeToolSetSelection");
|
||||
define("GeometryNode", "GeometryNodeTransform");
|
||||
define("GeometryNode", "GeometryNodeTranslateInstances");
|
||||
define("GeometryNode", "GeometryNodeTransferAttributes");
|
||||
define("GeometryNode", "GeometryNodeTriangulate");
|
||||
define("GeometryNode", "GeometryNodeTrimCurve");
|
||||
define("GeometryNode", "GeometryNodeUVPackIslands");
|
||||
|
|
@ -10717,6 +10720,7 @@ static void rna_def_nodes(BlenderRNA *brna)
|
|||
define("GeometryNode", "GeometryNodeVolumeCube");
|
||||
define("GeometryNode", "GeometryNodeVolumeToMesh");
|
||||
define("GeometryNode", "GeometryNodeWarning");
|
||||
define("GeometryNode", "GeometryNodeXPBDSolver");
|
||||
|
||||
|
||||
/* Node group types are currently defined for each tree type individually. */
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ set(SRC
|
|||
intern/geometry_nodes_lazy_function.cc
|
||||
intern/geometry_nodes_list.cc
|
||||
intern/geometry_nodes_repeat_zone.cc
|
||||
intern/geometry_nodes_physics_bundles.cc
|
||||
intern/geometry_nodes_srna.cc
|
||||
intern/geometry_nodes_warning.cc
|
||||
intern/inverse_eval.cc
|
||||
|
|
@ -139,6 +140,7 @@ set(SRC
|
|||
NOD_geometry_nodes_lazy_function.hh
|
||||
NOD_geometry_nodes_list.hh
|
||||
NOD_geometry_nodes_list_fwd.hh
|
||||
NOD_geometry_nodes_physics_bundles.hh
|
||||
NOD_geometry_nodes_srna.hh
|
||||
NOD_geometry_nodes_values.hh
|
||||
NOD_geometry_nodes_warning.hh
|
||||
|
|
|
|||
73
source/blender/nodes/NOD_geometry_nodes_physics_bundles.hh
Normal file
73
source/blender/nodes/NOD_geometry_nodes_physics_bundles.hh
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_string_ref.hh"
|
||||
|
||||
#include "NOD_bundle_type_fwd.hh"
|
||||
|
||||
namespace blender::nodes::physics_bundles {
|
||||
|
||||
class ColliderBundle {
|
||||
public:
|
||||
static constexpr StringRefNull name = "Blender.Collider";
|
||||
static const FlatBundleTypePtr &get_bundle_type();
|
||||
};
|
||||
|
||||
class InfinitePlaneColliderBundle {
|
||||
public:
|
||||
static constexpr StringRefNull name = "Blender.InfinitePlaneCollider";
|
||||
static const FlatBundleTypePtr &get_bundle_type();
|
||||
};
|
||||
|
||||
class CollisionContactsBundle {
|
||||
public:
|
||||
static constexpr StringRefNull name = "Blender.CollisionContacts";
|
||||
static const FlatBundleTypePtr &get_bundle_type();
|
||||
};
|
||||
|
||||
class DampingBundle {
|
||||
public:
|
||||
static constexpr StringRefNull name = "Blender.Damping";
|
||||
static const FlatBundleTypePtr &get_bundle_type();
|
||||
};
|
||||
|
||||
class PinPositionBundle {
|
||||
public:
|
||||
static constexpr StringRefNull name = "Blender.PinPositionConstraint";
|
||||
static const FlatBundleTypePtr &get_bundle_type();
|
||||
};
|
||||
|
||||
class PinRotationBundle {
|
||||
public:
|
||||
static constexpr StringRefNull name = "Blender.PinRotationConstraint";
|
||||
static const FlatBundleTypePtr &get_bundle_type();
|
||||
};
|
||||
|
||||
class RodStretchShearBundle {
|
||||
public:
|
||||
static constexpr StringRefNull name = "Blender.RodStretchShearConstraint";
|
||||
static const FlatBundleTypePtr &get_bundle_type();
|
||||
};
|
||||
|
||||
class RodBendTwistBundle {
|
||||
public:
|
||||
static constexpr StringRefNull name = "Blender.RodBendTwistConstraint";
|
||||
static const FlatBundleTypePtr &get_bundle_type();
|
||||
};
|
||||
|
||||
class EdgeLengthConstraintBundle {
|
||||
public:
|
||||
static constexpr StringRefNull name = "Blender.EdgeLengthConstraint";
|
||||
static const FlatBundleTypePtr &get_bundle_type();
|
||||
};
|
||||
|
||||
class CrossEdgeLengthConstraintBundle {
|
||||
public:
|
||||
static constexpr StringRefNull name = "Blender.CrossEdgeLengthConstraint";
|
||||
static const FlatBundleTypePtr &get_bundle_type();
|
||||
};
|
||||
|
||||
} // namespace blender::nodes::physics_bundles
|
||||
|
|
@ -273,6 +273,7 @@ set(SRC
|
|||
nodes/node_geo_string_to_curves.cc
|
||||
nodes/node_geo_subdivision_surface.cc
|
||||
nodes/node_geo_switch.cc
|
||||
nodes/node_geo_tag_filter.cc
|
||||
nodes/node_geo_tool_3d_cursor.cc
|
||||
nodes/node_geo_tool_active_element.cc
|
||||
nodes/node_geo_tool_face_set.cc
|
||||
|
|
@ -281,6 +282,7 @@ set(SRC
|
|||
nodes/node_geo_tool_set_selection.cc
|
||||
nodes/node_geo_transform_geometry.cc
|
||||
nodes/node_geo_translate_instances.cc
|
||||
nodes/node_geo_transfer_attributes.cc
|
||||
nodes/node_geo_triangulate.cc
|
||||
nodes/node_geo_uv_pack_islands.cc
|
||||
nodes/node_geo_uv_tangent.cc
|
||||
|
|
@ -290,6 +292,7 @@ set(SRC
|
|||
nodes/node_geo_volume_cube.cc
|
||||
nodes/node_geo_volume_to_mesh.cc
|
||||
nodes/node_geo_warning.cc
|
||||
nodes/node_geo_xpbd_solver.cc
|
||||
|
||||
include/NOD_geo_bake.hh
|
||||
include/NOD_geo_bundle.hh
|
||||
|
|
@ -302,6 +305,7 @@ set(SRC
|
|||
include/NOD_geo_menu_switch.hh
|
||||
include/NOD_geo_repeat.hh
|
||||
include/NOD_geo_simulation.hh
|
||||
include/NOD_geo_tag_filter.hh
|
||||
include/NOD_geo_viewer.hh
|
||||
|
||||
node_geometry_tree.cc
|
||||
|
|
|
|||
14
source/blender/nodes/geometry/include/NOD_geo_tag_filter.hh
Normal file
14
source/blender/nodes/geometry/include/NOD_geo_tag_filter.hh
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_set.hh"
|
||||
#include "BLI_string_ref.hh"
|
||||
|
||||
namespace blender::nodes {
|
||||
|
||||
bool tag_filter_matches(const StringRef tag_filter, const Set<std::string> &tags);
|
||||
|
||||
} // namespace blender::nodes
|
||||
|
|
@ -418,6 +418,10 @@ static void node_declare(NodeDeclarationBuilder &b)
|
|||
input_decl.supports_field().structure_type(StructureType::Dynamic);
|
||||
output_decl.dependent_field({input_decl.index()});
|
||||
}
|
||||
if (socket_type == SOCK_BUNDLE) {
|
||||
dynamic_cast<decl::BundleBuilder &>(output_decl)
|
||||
.pass_through_input_index(input_decl.index());
|
||||
}
|
||||
}
|
||||
b.add_input<decl::Extend>(""_ustr, "__extend__"_ustr).structure_type(StructureType::Dynamic);
|
||||
b.add_output<decl::Extend>(""_ustr, "__extend__"_ustr)
|
||||
|
|
@ -749,6 +753,10 @@ static void node_declare(NodeDeclarationBuilder &b)
|
|||
input_decl.supports_field().structure_type(StructureType::Dynamic);
|
||||
output_decl.dependent_field({input_decl.index()});
|
||||
}
|
||||
if (socket_type == SOCK_BUNDLE) {
|
||||
dynamic_cast<decl::BundleBuilder &>(output_decl)
|
||||
.pass_through_input_index(input_decl.index());
|
||||
}
|
||||
}
|
||||
b.add_input<decl::Extend>(""_ustr, "__extend__"_ustr).structure_type(StructureType::Dynamic);
|
||||
b.add_output<decl::Extend>(""_ustr, "__extend__"_ustr)
|
||||
|
|
|
|||
80
source/blender/nodes/geometry/nodes/node_geo_tag_filter.cc
Normal file
80
source/blender/nodes/geometry/nodes/node_geo_tag_filter.cc
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "NOD_geo_tag_filter.hh"
|
||||
#include "NOD_geometry_nodes_list.hh"
|
||||
|
||||
#include "node_geometry_util.hh"
|
||||
|
||||
namespace blender::nodes::node_geo_tag_filter_cc {
|
||||
|
||||
static void node_declare(NodeDeclarationBuilder &b)
|
||||
{
|
||||
b.add_input<decl::String>("Tag Filter"_ustr).optional_label();
|
||||
b.add_input<decl::String>("Tags"_ustr).structure_type(StructureType::List);
|
||||
b.add_output<decl::Bool>("Match"_ustr);
|
||||
}
|
||||
|
||||
static void node_geo_exec(GeoNodeExecParams params)
|
||||
{
|
||||
SocketValueVariant tags_variant = params.extract_input<SocketValueVariant>("Tags"_ustr);
|
||||
const std::string tag_filter = params.extract_input<std::string>("Tag Filter"_ustr);
|
||||
|
||||
Set<std::string> tags;
|
||||
if (tags_variant.is_list()) {
|
||||
const GListPtr tags_list_ptr = tags_variant.extract<GListPtr>();
|
||||
if (tags_list_ptr) {
|
||||
const GList &list = *tags_list_ptr;
|
||||
if (list.cpp_type().is<std::string>()) {
|
||||
list.typed<std::string>().foreach([&](const std::string &tag) { tags.add(tag); });
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool match = tag_filter_matches(tag_filter, tags);
|
||||
params.set_output("Match"_ustr, match);
|
||||
}
|
||||
|
||||
static void node_register()
|
||||
{
|
||||
static bke::bNodeType ntype;
|
||||
geo_node_type_base(&ntype, "GeometryNodeTagFilter"_ustr);
|
||||
ntype.ui_name = "Tag Filter";
|
||||
ntype.ui_description = "Check if a filter string matches a list of tags";
|
||||
ntype.nclass = NODE_CLASS_CONVERTER;
|
||||
ntype.declare = node_declare;
|
||||
ntype.geometry_node_execute = node_geo_exec;
|
||||
bke::node_register_type(ntype);
|
||||
}
|
||||
NOD_REGISTER_NODE(node_register)
|
||||
|
||||
} // namespace blender::nodes::node_geo_tag_filter_cc
|
||||
|
||||
namespace blender::nodes {
|
||||
|
||||
bool tag_filter_matches(const StringRef tag_filter, const Set<std::string> &tags)
|
||||
{
|
||||
if (tag_filter.is_empty()) {
|
||||
/* An empty filter matches everything. */
|
||||
return true;
|
||||
}
|
||||
StringRef remaining = tag_filter;
|
||||
while (!remaining.is_empty()) {
|
||||
const int sep = remaining.find(',');
|
||||
if (sep == -1) {
|
||||
const StringRef tag = remaining.trim();
|
||||
if (tags.contains_as(tag)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const StringRef tag = remaining.substr(0, sep).trim();
|
||||
if (tags.contains_as(tag)) {
|
||||
return true;
|
||||
}
|
||||
remaining = remaining.substr(sep + 1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace blender::nodes
|
||||
|
|
@ -0,0 +1,369 @@
|
|||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "BKE_curves.hh"
|
||||
#include "BKE_grease_pencil.hh"
|
||||
#include "BKE_instances.hh"
|
||||
|
||||
#include "DNA_curves_types.h"
|
||||
#include "DNA_grease_pencil_types.h"
|
||||
#include "DNA_mesh_types.h"
|
||||
#include "DNA_pointcloud_types.h"
|
||||
|
||||
#include "NOD_geometry_nodes_list.hh"
|
||||
|
||||
#include "node_geometry_util.hh"
|
||||
|
||||
namespace blender::nodes::node_geo_transfer_attributes_cc {
|
||||
|
||||
static void node_declare(NodeDeclarationBuilder &b)
|
||||
{
|
||||
b.use_custom_socket_order();
|
||||
b.allow_any_socket_order();
|
||||
|
||||
b.add_input<decl::Geometry>("Target"_ustr);
|
||||
b.add_output<decl::Geometry>("Target"_ustr).align_with_previous().propagate_all();
|
||||
b.add_output<decl::Bool>("Success"_ustr);
|
||||
{
|
||||
auto &p = b.add_panel("Target IDs"_ustr).default_closed(true);
|
||||
Vector<BaseSocketDeclarationBuilder *> sockets;
|
||||
sockets.append(&p.add_input<decl::Int>("Target Point ID"_ustr));
|
||||
sockets.append(&p.add_input<decl::Int>("Target Edge ID"_ustr));
|
||||
sockets.append(&p.add_input<decl::Int>("Target Face ID"_ustr));
|
||||
sockets.append(&p.add_input<decl::Int>("Target Corner ID"_ustr));
|
||||
sockets.append(&p.add_input<decl::Int>("Target Curve ID"_ustr));
|
||||
sockets.append(&p.add_input<decl::Int>("Target Instance ID"_ustr));
|
||||
|
||||
for (BaseSocketDeclarationBuilder *socket : sockets) {
|
||||
socket->implicit_field(NODE_DEFAULT_INPUT_INDEX_FIELD);
|
||||
socket->structure_type(StructureType::Field);
|
||||
}
|
||||
}
|
||||
b.add_input<decl::Geometry>("Source"_ustr);
|
||||
{
|
||||
auto &p = b.add_panel("Source IDs"_ustr).default_closed(true);
|
||||
Vector<BaseSocketDeclarationBuilder *> sockets;
|
||||
sockets.append(&p.add_input<decl::Int>("Source Point ID"_ustr));
|
||||
sockets.append(&p.add_input<decl::Int>("Source Edge ID"_ustr));
|
||||
sockets.append(&p.add_input<decl::Int>("Source Face ID"_ustr));
|
||||
sockets.append(&p.add_input<decl::Int>("Source Corner ID"_ustr));
|
||||
sockets.append(&p.add_input<decl::Int>("Source Curve ID"_ustr));
|
||||
sockets.append(&p.add_input<decl::Int>("Source Instance ID"_ustr));
|
||||
|
||||
for (BaseSocketDeclarationBuilder *socket : sockets) {
|
||||
socket->implicit_field(NODE_DEFAULT_INPUT_INDEX_FIELD);
|
||||
socket->structure_type(StructureType::Field);
|
||||
}
|
||||
}
|
||||
|
||||
b.add_input<decl::String>("Names"_ustr)
|
||||
.optional_label()
|
||||
.structure_type(StructureType::List)
|
||||
.description(
|
||||
"List of attribute names (not) to transfer. A wildcard (*) at the end is allowed");
|
||||
b.add_input<decl::Bool>("Ignore Names"_ustr).default_value(false);
|
||||
}
|
||||
|
||||
static bool name_matches_any_pattern(const VectorSet<std::string> &patterns, const StringRef name)
|
||||
{
|
||||
if (patterns.contains_as(name)) {
|
||||
return true;
|
||||
}
|
||||
for (const StringRef pattern : patterns) {
|
||||
// TODO: Support wildcards similar to Remove Attribute node.
|
||||
if (pattern.endswith("*")) {
|
||||
if (name.startswith(pattern.drop_known_suffix("*"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool should_transfer(const VectorSet<std::string> &patterns,
|
||||
const StringRef name,
|
||||
const bool ignore_names)
|
||||
{
|
||||
if (ELEM(name, ".corner_vert", ".corner_edge", ".edge_verts")) {
|
||||
return false;
|
||||
}
|
||||
const bool matches = name_matches_any_pattern(patterns, name);
|
||||
if (ignore_names) {
|
||||
return !matches;
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
static bool transfer_attributes(
|
||||
const VectorSet<std::string> &patterns,
|
||||
const bool ignore_names,
|
||||
const bke::AttributeAccessor &src_attributes,
|
||||
bke::MutableAttributeAccessor &dst_attributes,
|
||||
const Map<bke::AttrDomain, Field<int>> &src_id_fields,
|
||||
const Map<bke::AttrDomain, Field<int>> &dst_id_fields,
|
||||
FunctionRef<fn::FieldContext &(ResourceScope &scope, const bke::AttrDomain domain)>
|
||||
create_src_context,
|
||||
FunctionRef<fn::FieldContext &(ResourceScope &scope, const bke::AttrDomain domain)>
|
||||
create_dst_context)
|
||||
{
|
||||
struct AttrItem {
|
||||
StringRef name;
|
||||
AttrDomain domain;
|
||||
bke::AttrType type;
|
||||
};
|
||||
struct IDs {
|
||||
bool transfer_by_index = false;
|
||||
Array<int> src_by_dst_index;
|
||||
IndexMask dst_mask;
|
||||
};
|
||||
Map<bke::AttrDomain, IDs> ids_by_domain;
|
||||
Vector<AttrItem> items;
|
||||
src_attributes.foreach_attribute([&](const bke::AttributeIter &iter) {
|
||||
if (should_transfer(patterns, iter.name, ignore_names)) {
|
||||
items.append({iter.name, iter.domain, iter.data_type});
|
||||
ids_by_domain.lookup_or_add_default(iter.domain);
|
||||
}
|
||||
});
|
||||
|
||||
ResourceScope scope;
|
||||
for (const auto &[domain, ids] : ids_by_domain.items()) {
|
||||
const Field<int> &src_id_field = src_id_fields.lookup(domain);
|
||||
const Field<int> &dst_id_field = dst_id_fields.lookup(domain);
|
||||
if (src_id_field.get_input_if<fn::IndexFieldInput>() &&
|
||||
dst_id_field.get_input_if<fn::IndexFieldInput>())
|
||||
{
|
||||
ids.transfer_by_index = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const int src_size = src_attributes.domain_size(domain);
|
||||
const int dst_size = dst_attributes.domain_size(domain);
|
||||
|
||||
fn::FieldContext &src_field_context = create_src_context(scope, domain);
|
||||
fn::FieldEvaluator src_evaluator(src_field_context, src_size);
|
||||
src_evaluator.add(src_id_field);
|
||||
src_evaluator.evaluate();
|
||||
const VArraySpan<int> src_ids = src_evaluator.get_evaluated<int>(0);
|
||||
|
||||
fn::FieldContext &dst_field_context = create_dst_context(scope, domain);
|
||||
fn::FieldEvaluator dst_evaluator(dst_field_context, dst_size);
|
||||
dst_evaluator.add(dst_id_field);
|
||||
dst_evaluator.evaluate();
|
||||
const VArraySpan<int> dst_ids = dst_evaluator.get_evaluated<int>(0);
|
||||
|
||||
Map<int, int> src_index_by_id;
|
||||
for (const int i : IndexRange(src_size)) {
|
||||
const int id = src_ids[i];
|
||||
src_index_by_id.add(id, i);
|
||||
}
|
||||
ids.src_by_dst_index.reinitialize(dst_size);
|
||||
threading::parallel_for(IndexRange(dst_size), 2048, [&](const IndexRange range) {
|
||||
for (const int dst_i : range) {
|
||||
const int dst_id = dst_ids[dst_i];
|
||||
const int src_i = src_index_by_id.lookup_default(dst_id, -1);
|
||||
ids.src_by_dst_index[dst_i] = src_i;
|
||||
}
|
||||
});
|
||||
ids.dst_mask = array_utils::indices_non_negative(
|
||||
IndexMask(dst_size), ids.src_by_dst_index, scope.allocator());
|
||||
}
|
||||
|
||||
bool any_transferred = false;
|
||||
for (const AttrItem &item : items) {
|
||||
const bke::GAttributeReader src_attr = src_attributes.lookup(item.name);
|
||||
const CommonVArrayInfo info = src_attr.varray.common_info();
|
||||
const IDs &ids = ids_by_domain.lookup(item.domain);
|
||||
if (info.type == CommonVArrayInfo::Type::Single) {
|
||||
if (ids.dst_mask.size() == dst_attributes.domain_size(item.domain)) {
|
||||
if (dst_attributes.add(item.name,
|
||||
item.domain,
|
||||
item.type,
|
||||
bke::AttributeInitValue(GPointer{
|
||||
bke::attribute_type_to_cpp_type(item.type), info.data})))
|
||||
{
|
||||
any_transferred = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (info.type == CommonVArrayInfo::Type::Span) {
|
||||
if (ids.transfer_by_index) {
|
||||
if (ids.dst_mask.size() == dst_attributes.domain_size(item.domain)) {
|
||||
if (src_attr.sharing_info) {
|
||||
if (dst_attributes.add(item.name,
|
||||
item.domain,
|
||||
item.type,
|
||||
bke::AttributeInitShared(info.data, *src_attr.sharing_info)))
|
||||
{
|
||||
any_transferred = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bke::GSpanAttributeWriter dst_attr;
|
||||
if (ids.dst_mask.size() == dst_attributes.domain_size(item.domain)) {
|
||||
dst_attr = dst_attributes.lookup_or_add_for_write_span(item.name, item.domain, item.type);
|
||||
}
|
||||
else {
|
||||
dst_attr = dst_attributes.lookup_or_add_for_write_only_span(
|
||||
item.name, item.domain, item.type);
|
||||
}
|
||||
if (!dst_attr) {
|
||||
continue;
|
||||
}
|
||||
const int src_size = src_attr.varray.size();
|
||||
const int dst_size = dst_attr.span.size();
|
||||
|
||||
if (ids.transfer_by_index) {
|
||||
const int copy_num = std::min(src_size, dst_size);
|
||||
const IndexRange slice(copy_num);
|
||||
array_utils::copy(src_attr.varray.slice(slice), dst_attr.span.slice(slice));
|
||||
}
|
||||
else {
|
||||
bke::attribute_math::gather(*src_attr, ids.src_by_dst_index, ids.dst_mask, dst_attr.span);
|
||||
}
|
||||
dst_attr.finish();
|
||||
any_transferred = true;
|
||||
}
|
||||
|
||||
return any_transferred;
|
||||
}
|
||||
|
||||
static void node_geo_exec(GeoNodeExecParams params)
|
||||
{
|
||||
GeometrySet dst_geo = params.extract_input<GeometrySet>("Target"_ustr);
|
||||
GeometrySet src_geo = params.extract_input<GeometrySet>("Source"_ustr);
|
||||
const GListPtr attribute_patterns_list = params.extract_input<GListPtr>("Names"_ustr);
|
||||
const bool ignore_names = params.extract_input<bool>("Ignore Names"_ustr);
|
||||
|
||||
Map<bke::AttrDomain, Field<int>> dst_id_fields = {
|
||||
{AttrDomain::Point, params.extract_input<Field<int>>("Target Point ID"_ustr)},
|
||||
{AttrDomain::Edge, params.extract_input<Field<int>>("Target Edge ID"_ustr)},
|
||||
{AttrDomain::Face, params.extract_input<Field<int>>("Target Face ID"_ustr)},
|
||||
{AttrDomain::Corner, params.extract_input<Field<int>>("Target Corner ID"_ustr)},
|
||||
{AttrDomain::Curve, params.extract_input<Field<int>>("Target Curve ID"_ustr)},
|
||||
{AttrDomain::Instance, params.extract_input<Field<int>>("Target Instance ID"_ustr)},
|
||||
};
|
||||
|
||||
Map<bke::AttrDomain, Field<int>> src_id_fields{
|
||||
{AttrDomain::Point, params.extract_input<Field<int>>("Source Point ID"_ustr)},
|
||||
{AttrDomain::Edge, params.extract_input<Field<int>>("Source Edge ID"_ustr)},
|
||||
{AttrDomain::Face, params.extract_input<Field<int>>("Source Face ID"_ustr)},
|
||||
{AttrDomain::Corner, params.extract_input<Field<int>>("Source Corner ID"_ustr)},
|
||||
{AttrDomain::Curve, params.extract_input<Field<int>>("Source Curve ID"_ustr)},
|
||||
{AttrDomain::Instance, params.extract_input<Field<int>>("Source Instance ID"_ustr)},
|
||||
};
|
||||
|
||||
VectorSet<std::string> patterns;
|
||||
if (attribute_patterns_list) {
|
||||
if (attribute_patterns_list->cpp_type().is<std::string>()) {
|
||||
const VArray<std::string> values = attribute_patterns_list->typed<std::string>().varray();
|
||||
for (const int i : values.index_range()) {
|
||||
patterns.add(values[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (patterns.is_empty()) {
|
||||
params.set_output("Target"_ustr, std::move(dst_geo));
|
||||
params.set_output("Success"_ustr, true);
|
||||
return;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
for (const bke::GeometryComponent::Type type : {bke::GeometryComponent::Type::Mesh,
|
||||
bke::GeometryComponent::Type::PointCloud,
|
||||
bke::GeometryComponent::Type::Curve,
|
||||
bke::GeometryComponent::Type::Instance})
|
||||
{
|
||||
if (!dst_geo.has(type)) {
|
||||
continue;
|
||||
}
|
||||
if (!src_geo.has(type)) {
|
||||
continue;
|
||||
}
|
||||
const GeometryComponent &src_component = *src_geo.get_component(type);
|
||||
GeometryComponent &dst_component = dst_geo.get_component_for_write(type);
|
||||
const bke::AttributeAccessor src_attributes = *src_component.attributes();
|
||||
bke::MutableAttributeAccessor dst_attributes = *dst_component.attributes_for_write();
|
||||
success = success |
|
||||
transfer_attributes(
|
||||
patterns,
|
||||
ignore_names,
|
||||
src_attributes,
|
||||
dst_attributes,
|
||||
src_id_fields,
|
||||
dst_id_fields,
|
||||
[&](ResourceScope &scope, const bke::AttrDomain domain) -> fn::FieldContext & {
|
||||
return scope.construct<bke::GeometryFieldContext>(src_component, domain);
|
||||
},
|
||||
[&](ResourceScope &scope, const bke::AttrDomain domain) -> fn::FieldContext & {
|
||||
return scope.construct<bke::GeometryFieldContext>(dst_component, domain);
|
||||
});
|
||||
}
|
||||
|
||||
if (src_geo.has_grease_pencil() && dst_geo.has_grease_pencil()) {
|
||||
using namespace bke::greasepencil;
|
||||
const GreasePencil &src_grease_pencil = *src_geo.get_grease_pencil();
|
||||
GreasePencil &dst_grease_pencil = *dst_geo.get_grease_pencil_for_write();
|
||||
const int src_layer_num = src_grease_pencil.layers().size();
|
||||
const int dst_layer_num = dst_grease_pencil.layers().size();
|
||||
/* Could also support custom mapping of src to dst layers. */
|
||||
const int common_layer_num = std::min(src_layer_num, dst_layer_num);
|
||||
for (const int layer_i : IndexRange(common_layer_num)) {
|
||||
const Layer &src_layer = src_grease_pencil.layer(layer_i);
|
||||
const Drawing *src_drawing = src_grease_pencil.get_eval_drawing(src_layer);
|
||||
if (!src_drawing) {
|
||||
continue;
|
||||
}
|
||||
Layer &dst_layer = dst_grease_pencil.layer(layer_i);
|
||||
Drawing *dst_drawing = dst_grease_pencil.get_eval_drawing(dst_layer);
|
||||
if (!dst_drawing) {
|
||||
continue;
|
||||
}
|
||||
const bke::CurvesGeometry &src_curves = src_drawing->strokes();
|
||||
bke::CurvesGeometry &dst_curves = dst_drawing->strokes_for_write();
|
||||
const bke::AttributeAccessor src_attributes = src_curves.attributes();
|
||||
bke::MutableAttributeAccessor dst_attributes = dst_curves.attributes_for_write();
|
||||
success = success |
|
||||
transfer_attributes(
|
||||
patterns,
|
||||
ignore_names,
|
||||
src_attributes,
|
||||
dst_attributes,
|
||||
src_id_fields,
|
||||
dst_id_fields,
|
||||
[&](ResourceScope &scope, const bke::AttrDomain domain) -> fn::FieldContext & {
|
||||
return scope.construct<bke::GreasePencilLayerFieldContext>(
|
||||
src_grease_pencil, domain, layer_i);
|
||||
},
|
||||
[&](ResourceScope &scope, const bke::AttrDomain domain) -> fn::FieldContext & {
|
||||
return scope.construct<bke::GreasePencilLayerFieldContext>(
|
||||
dst_grease_pencil, domain, layer_i);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
params.set_output("Target"_ustr, std::move(dst_geo));
|
||||
params.set_output("Success"_ustr, success);
|
||||
}
|
||||
|
||||
static void node_register()
|
||||
{
|
||||
static bke::bNodeType ntype;
|
||||
|
||||
geo_node_type_base(&ntype, "GeometryNodeTransferAttributes"_ustr);
|
||||
ntype.ui_name = "Transfer Attributes";
|
||||
ntype.ui_description = "Copy attributes from one geometry to another";
|
||||
ntype.nclass = NODE_CLASS_GEOMETRY;
|
||||
ntype.geometry_node_execute = node_geo_exec;
|
||||
ntype.declare = node_declare;
|
||||
bke::node_register_type(ntype);
|
||||
}
|
||||
NOD_REGISTER_NODE(node_register)
|
||||
|
||||
} // namespace blender::nodes::node_geo_transfer_attributes_cc
|
||||
3224
source/blender/nodes/geometry/nodes/node_geo_xpbd_solver.cc
Normal file
3224
source/blender/nodes/geometry/nodes/node_geo_xpbd_solver.cc
Normal file
File diff suppressed because it is too large
Load diff
170
source/blender/nodes/intern/geometry_nodes_physics_bundles.cc
Normal file
170
source/blender/nodes/intern/geometry_nodes_physics_bundles.cc
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "NOD_bundle_type.hh"
|
||||
#include "NOD_geometry_nodes_physics_bundles.hh"
|
||||
#include "NOD_socket_declarations.hh"
|
||||
#include "NOD_socket_declarations_geometry.hh"
|
||||
|
||||
namespace blender::nodes::physics_bundles {
|
||||
|
||||
static void add_filter(FlatBundleTypeBuilder &b)
|
||||
{
|
||||
b.add<decl::String>("filter"_ustr);
|
||||
b.add<decl::Bool>("filter_local"_ustr).default_value(false);
|
||||
}
|
||||
|
||||
const FlatBundleTypePtr &ColliderBundle::get_bundle_type()
|
||||
{
|
||||
static const FlatBundleTypePtr bundle_type = []() {
|
||||
FlatBundleTypeBuilder b(ColliderBundle::name);
|
||||
add_filter(b);
|
||||
b.add<decl::Geometry>("geometry"_ustr);
|
||||
b.add<decl::Float>("margin"_ustr).min(0.0f);
|
||||
b.add<decl::Float>("friction"_ustr).min(0.0f);
|
||||
b.add<decl::Float>("compliance"_ustr).min(0.0f);
|
||||
b.add<decl::Bool>("deforming"_ustr).default_value(false);
|
||||
b.add<decl::Bool>("use_edge_contacts"_ustr).default_value(false);
|
||||
const FlatBundleTypePtr bundle_type = b.build();
|
||||
BundleTypeRegistry::register_type(bundle_type);
|
||||
return bundle_type;
|
||||
}();
|
||||
return bundle_type;
|
||||
}
|
||||
|
||||
const FlatBundleTypePtr &DampingBundle::get_bundle_type()
|
||||
{
|
||||
static const FlatBundleTypePtr bundle_type = []() {
|
||||
FlatBundleTypeBuilder b(DampingBundle::name);
|
||||
add_filter(b);
|
||||
b.add<decl::Float>("linear_damping"_ustr).min(0.0f);
|
||||
b.add<decl::Float>("angular_damping"_ustr).min(0.0f);
|
||||
const FlatBundleTypePtr bundle_type = b.build();
|
||||
BundleTypeRegistry::register_type(bundle_type);
|
||||
return bundle_type;
|
||||
}();
|
||||
return bundle_type;
|
||||
}
|
||||
|
||||
const FlatBundleTypePtr &PinPositionBundle::get_bundle_type()
|
||||
{
|
||||
static const FlatBundleTypePtr bundle_type = []() {
|
||||
FlatBundleTypeBuilder b(PinPositionBundle::name);
|
||||
add_filter(b);
|
||||
b.add<decl::Bool>("selection"_ustr).default_value(true).supports_field();
|
||||
b.add<decl::Vector>("position"_ustr).supports_field();
|
||||
b.add<decl::Float>("compliance"_ustr).min(0.0f).supports_field();
|
||||
b.add<decl::String>("was_pinned_attribute"_ustr);
|
||||
b.add<decl::String>("previous_pin_position_attribute"_ustr);
|
||||
b.add<decl::String>("lambda_attribute"_ustr);
|
||||
const FlatBundleTypePtr bundle_type = b.build();
|
||||
BundleTypeRegistry::register_type(bundle_type);
|
||||
return bundle_type;
|
||||
}();
|
||||
return bundle_type;
|
||||
}
|
||||
|
||||
const FlatBundleTypePtr &PinRotationBundle::get_bundle_type()
|
||||
{
|
||||
static const FlatBundleTypePtr bundle_type = []() {
|
||||
FlatBundleTypeBuilder b(PinRotationBundle::name);
|
||||
add_filter(b);
|
||||
b.add<decl::Bool>("selection"_ustr).default_value(true).supports_field();
|
||||
b.add<decl::Rotation>("rotation"_ustr).supports_field();
|
||||
b.add<decl::Float>("compliance"_ustr).min(0.0f).supports_field();
|
||||
b.add<decl::String>("was_pinned_attribute"_ustr);
|
||||
b.add<decl::String>("previous_pin_rotation_attribute"_ustr);
|
||||
const FlatBundleTypePtr bundle_type = b.build();
|
||||
BundleTypeRegistry::register_type(bundle_type);
|
||||
return bundle_type;
|
||||
}();
|
||||
return bundle_type;
|
||||
}
|
||||
|
||||
const FlatBundleTypePtr &InfinitePlaneColliderBundle::get_bundle_type()
|
||||
{
|
||||
static const FlatBundleTypePtr bundle_type = []() {
|
||||
FlatBundleTypeBuilder b(InfinitePlaneColliderBundle::name);
|
||||
add_filter(b);
|
||||
b.add<decl::Vector>("position"_ustr);
|
||||
b.add<decl::Vector>("normal"_ustr).default_value(float3(0.0f, 0.0f, 1.0f));
|
||||
b.add<decl::Float>("friction"_ustr).default_value(0.5f).min(0.0f);
|
||||
const FlatBundleTypePtr bundle_type = b.build();
|
||||
BundleTypeRegistry::register_type(bundle_type);
|
||||
return bundle_type;
|
||||
}();
|
||||
return bundle_type;
|
||||
}
|
||||
|
||||
const FlatBundleTypePtr &CollisionContactsBundle::get_bundle_type()
|
||||
{
|
||||
static const FlatBundleTypePtr bundle_type = []() {
|
||||
FlatBundleTypeBuilder b(CollisionContactsBundle::name);
|
||||
add_filter(b);
|
||||
const FlatBundleTypePtr bundle_type = b.build();
|
||||
BundleTypeRegistry::register_type(bundle_type);
|
||||
return bundle_type;
|
||||
}();
|
||||
return bundle_type;
|
||||
}
|
||||
|
||||
const FlatBundleTypePtr &RodStretchShearBundle::get_bundle_type()
|
||||
{
|
||||
static const FlatBundleTypePtr bundle_type = []() {
|
||||
FlatBundleTypeBuilder b(RodStretchShearBundle::name);
|
||||
add_filter(b);
|
||||
b.add<decl::Float>("rest_length"_ustr).min(0.0f).supports_field();
|
||||
b.add<decl::Float>("compliance"_ustr).default_value(1e-4f).min(0.0f);
|
||||
b.add<decl::String>("lambda_position_attribute"_ustr);
|
||||
b.add<decl::String>("lambda_rotation_attribute"_ustr);
|
||||
const FlatBundleTypePtr bundle_type = b.build();
|
||||
BundleTypeRegistry::register_type(bundle_type);
|
||||
return bundle_type;
|
||||
}();
|
||||
return bundle_type;
|
||||
}
|
||||
|
||||
const FlatBundleTypePtr &RodBendTwistBundle::get_bundle_type()
|
||||
{
|
||||
static const FlatBundleTypePtr bundle_type = []() {
|
||||
FlatBundleTypeBuilder b(RodBendTwistBundle::name);
|
||||
add_filter(b);
|
||||
b.add<decl::Rotation>("rest_bend_rotation"_ustr).supports_field();
|
||||
b.add<decl::Float>("compliance"_ustr).default_value(1e-4f).min(0.0f);
|
||||
const FlatBundleTypePtr bundle_type = b.build();
|
||||
BundleTypeRegistry::register_type(bundle_type);
|
||||
return bundle_type;
|
||||
}();
|
||||
return bundle_type;
|
||||
}
|
||||
|
||||
const FlatBundleTypePtr &EdgeLengthConstraintBundle::get_bundle_type()
|
||||
{
|
||||
static const FlatBundleTypePtr bundle_type = []() {
|
||||
FlatBundleTypeBuilder b(EdgeLengthConstraintBundle::name);
|
||||
add_filter(b);
|
||||
b.add<decl::Float>("rest_length"_ustr).min(0.0f).supports_field();
|
||||
b.add<decl::Float>("compliance"_ustr).default_value(1e-4f).min(0.0f);
|
||||
const FlatBundleTypePtr bundle_type = b.build();
|
||||
BundleTypeRegistry::register_type(bundle_type);
|
||||
return bundle_type;
|
||||
}();
|
||||
return bundle_type;
|
||||
}
|
||||
|
||||
const FlatBundleTypePtr &CrossEdgeLengthConstraintBundle::get_bundle_type()
|
||||
{
|
||||
static const FlatBundleTypePtr bundle_type = []() {
|
||||
FlatBundleTypeBuilder b(CrossEdgeLengthConstraintBundle::name);
|
||||
add_filter(b);
|
||||
b.add<decl::Vector>("rest_position"_ustr).supports_field();
|
||||
b.add<decl::Float>("compliance"_ustr).default_value(1e-4f).min(0.0f);
|
||||
const FlatBundleTypePtr bundle_type = b.build();
|
||||
BundleTypeRegistry::register_type(bundle_type);
|
||||
return bundle_type;
|
||||
}();
|
||||
return bundle_type;
|
||||
}
|
||||
|
||||
} // namespace blender::nodes::physics_bundles
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# This is an Asset Catalog Definition file for Blender.
|
||||
#
|
||||
# Empty lines and lines starting with `#` will be ignored.
|
||||
# The first non-ignored line should be the version indicator.
|
||||
# Other lines are of the format "UUID:catalog/path/for/assets:simple catalog name"
|
||||
|
||||
VERSION 1
|
||||
|
||||
59f793c7-ac31-4161-909f-e7a0631e8ce2:Debug:Debug
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:88947307e8ad6291f117cbca0fa33ff841f327d4f636a9795bbf5aba17c5bcb9
|
||||
size 3981189
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e9cf40cb2797edf924fa65fa1e9f5133f2d21017917ee1b0651532207ab71b12
|
||||
size 189897
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:525436d3dfa88a9cb71a9beef99a5ad08c682c440ec1ddd2d5da4e63500fd954
|
||||
size 231665
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a1dfaa61d0585e45d92767b8e1c6500d3f087e112414ac688bda1d3f3e692baa
|
||||
size 793228
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1fb47bc4f1c3205f9e5d05d2e83a646ee403ca927fe61164497a932a4bc379f0
|
||||
size 227440
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e97bbffe11cf5380392259499b375656fea8554eb25601c8b08fccf81a276fa8
|
||||
size 306669
|
||||
Loading…
Add table
Add a link
Reference in a new issue