Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
/* 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);
float residual_error_squared = 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 float error_squared = math::length_squared(residual + compliance_term * lambda_prev);
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, error_squared};
}
} // namespace blender::xpbd

View File

@@ -0,0 +1,191 @@
/* 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_;
/* Scale factor for residual error. */
Span<float> error_scales_;
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,
const Span<float> error_scales,
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),
error_scales_(error_scales),
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 &params,
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 error_squared = math::square(residual + compliance_term * lambda_normal);
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);
updater.add_residual_error(geo_i_, error_squared * error_scales_[constraint_i]);
}
ConstraintColoring color_constraints(LinearAllocator<> &memory) const override
{
return color_constraints__binary(point_pairs_, memory);
}
};
} // namespace blender::xpbd

View File

@@ -0,0 +1,130 @@
/* 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_;
/* Scale factor for residual error. */
Span<float> error_scales_;
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,
const Span<float> error_scales,
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),
error_scales_(error_scales),
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 &params,
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 error_squared = math::square(normal_distance + compliance_term * lambda_normal);
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);
updater.add_residual_error(geo_i_, error_squared * error_scales_[constraint_i]);
}
ConstraintColoring color_constraints(LinearAllocator<> &memory) const override
{
return color_constraints__unary(points_, memory);
}
};
} // namespace blender::xpbd

View 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

View File

@@ -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,
LinearAllocator<> &memory);
ConstraintColoring color_constraints__binary(const Span<int2> affected_points,
LinearAllocator<> &memory);
ConstraintColoring color_constraints__n_ary(const GroupedSpan<int> affected_points,
LinearAllocator<> &memory);
ConstraintColoring color_constraints__all_independent(const int constraints_num);
} // namespace blender::xpbd

View File

@@ -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 &params,
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

View File

@@ -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 &params,
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

View File

@@ -0,0 +1,107 @@
/* 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);
float residual_error_squared = 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 error_squared = math::square(length_diff + compliance_term * lambda_prev);
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, error_squared};
}
class DistanceConstraintSet : public TemplatedConstraintSet<DistanceConstraintSet> {
private:
/** Indexed by constraint index. */
Span<int2> point_pairs_;
Span<float> distances_;
Span<float> compliances_;
/* Scale factor for residual error. */
float error_scale_;
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,
const float error_scale,
MutableSpan<float> lambdas)
: TemplatedConstraintSet<DistanceConstraintSet>(point_pairs.size(), {geo_i}),
point_pairs_(point_pairs),
distances_(distances),
compliances_(compliances),
error_scale_(error_scale),
lambdas_(lambdas)
{
}
void reset_force(const int constraint_i) const
{
lambdas_[constraint_i] = 0.0f;
}
template<typename UpdaterT>
void solve_single(const ConstraintSetParams &params,
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);
updater.add_residual_error(geo_i, result.residual_error_squared * error_scale_);
}
ConstraintColoring color_constraints(LinearAllocator<> &memory) const override
{
return color_constraints__binary(point_pairs_, memory);
}
};
} // namespace blender::xpbd

View File

@@ -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 &params,
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

View File

@@ -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 &params,
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

View 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

View File

@@ -0,0 +1,72 @@
/* 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_;
/* Scale factor for residual error. */
float error_scale_;
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 float error_scale,
const MutableSpan<float> lambdas)
: TemplatedConstraintSet<PinPositionConstraintSet>(point_indices.size(), {geo_i}),
point_indices_(point_indices),
pin_positions_(pin_positions),
compliances_(compliances),
error_scale_(error_scale),
lambdas_(lambdas)
{
}
void reset_force(const int constraint_i) const
{
lambdas_[constraint_i] = 0.0f;
}
template<typename UpdaterT>
void solve_single(const ConstraintSetParams &params,
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);
updater.add_residual_error(geo_i, result.residual_error_squared * error_scale_);
}
ConstraintColoring color_constraints(LinearAllocator<> &memory) const override
{
return color_constraints__unary(point_indices_, memory);
}
};
} // namespace blender::xpbd

View File

@@ -0,0 +1,72 @@
/* 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_;
/* Scale factor for residual error. */
float error_scale_;
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,
const float error_scale,
MutableSpan<float4> lambdas)
: TemplatedConstraintSet<PinRotationConstraintSet>(point_indices.size(), {geo_i}),
compliances_(compliances),
point_indices_(point_indices),
pin_rotations_(pin_rotations),
error_scale_(error_scale),
lambdas_(lambdas)
{
}
void reset_force(const int constraint_i) const
{
lambdas_[constraint_i] = float4(0.0f);
}
template<typename UpdaterT>
void solve_single(const ConstraintSetParams &params,
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);
updater.add_residual_error(geo_i, result.residual_error_squared * error_scale_);
}
ConstraintColoring color_constraints(LinearAllocator<> &memory) const override
{
return color_constraints__unary(point_indices_, memory);
}
};
} // namespace blender::xpbd

View File

@@ -0,0 +1,95 @@
/* 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_;
/** Indexed by `point_i - first_point_i_in_constraint_set`. */
Span<float> compliances_;
/* Scale factor for residual error. */
float error_scale_;
MutableSpan<float4> lambdas_;
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,
const float error_scale,
MutableSpan<float4> lambdas)
: TemplatedConstraintSet<RodBendAndTwistConstraintSet>(curves_range.size(), {geo_i}),
curves_range_(curves_range),
points_by_curve_(points_by_curve),
rest_rotations_(rest_rotations),
compliances_(compliances),
error_scale_(error_scale),
lambdas_(lambdas)
{
}
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 &params,
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);
updater.add_residual_error(geo_i, result.residual_error_squared * error_scale_);
}
}
ConstraintColoring color_constraints(LinearAllocator<> & /*memory*/) const override
{
return color_constraints__all_independent(constraints_num_);
}
};
} // namespace blender::xpbd

View File

@@ -0,0 +1,175 @@
/* 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);
float residual_error_squared = 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;
const float error_squared = math::length_squared(residual_pos +
compliance_term * lambda_pos_prev);
/* 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);
result.residual_error_squared = error_squared;
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_;
/* Scale factor for residual error. */
float error_scale_;
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,
const float error_scale,
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),
error_scale_(error_scale)
{
}
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 &params,
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);
updater.add_residual_error(geo_i, result.residual_error_squared * error_scale_);
}
}
ConstraintColoring color_constraints(LinearAllocator<> & /*memory*/) const override
{
return color_constraints__all_independent(constraints_num_);
}
};
} // namespace blender::xpbd

View 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 &params,
GaussSeidelUpdater &updater,
const IndexMask &mask) = 0;
virtual void reset_forces() = 0;
virtual ConstraintColoring color_constraints(LinearAllocator<> &memory) const = 0;
void solve_sequential_all(const ConstraintSetParams &params, 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 &params, VelocityUpdater &updater) = 0;
Span<int> get_affected_geo_indices() const
{
return affected_geo_indices_;
}
};
} // namespace blender::xpbd

View 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

View File

@@ -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 &params,
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 &params, 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

View 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

View File

@@ -0,0 +1,60 @@
/* 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_;
float total_error_squared_;
int total_error_count_;
public:
GaussSeidelUpdater(Span<GeometryRef> geometry_refs)
: geometry_refs_(geometry_refs), total_error_squared_(0.0f), total_error_count_(0.0f)
{
}
float total_error_squared() const
{
return total_error_squared_;
}
int total_error_count() const
{
return total_error_count_;
}
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));
}
void add_residual_error(const int /*geo_i*/, const float error_squared)
{
/* Geometry index is ignored for now for simplicity. We could record a separate error for each
* geometry. */
total_error_squared_ += error_squared;
++total_error_count_;
}
};
} // namespace blender::xpbd

View 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

View File

@@ -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,
LinearAllocator<> &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,
LinearAllocator<> &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,
LinearAllocator<> &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,
LinearAllocator<> &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