Fix #117927: Limit rotation constraint flipping

The issue from the bug report mentions that the `Limit Rotation` constraint snaps
back to 0 when reaching 180 degrees with a min and max of 0 / 180.

The root cause of this goes a bit deeper though. Because the following also snaps back to 0.
* min max of 0 / 360
* angle of 185

The reason for this is that the clamping logic in the constraint was very simple.
It just took the matrix, decomposed it to euler and clamped the values directly.
However in that process, the euler angles are bound to a range of -180 / 180,
which means that any angle >= 180 would snap back to the min.

Pull Request: https://projects.blender.org/blender/blender/pulls/118502
This commit is contained in:
Christoph Lendenfeld 2024-04-04 16:30:51 +02:00 committed by Christoph Lendenfeld
parent 93cc55889c
commit ed2408400d
2 changed files with 73 additions and 27 deletions

@ -1659,6 +1659,64 @@ static bConstraintTypeInfo CTI_LOCLIMIT = {
/* -------- Limit Rotation --------- */
/**
* Wraps a number to be in [-PI, +PI].
*/
static inline float wrap_rad_angle(const float angle)
{
const float b = angle * (0.5 / M_PI) + 0.5;
return ((b - std::floor(b)) - 0.5) * (2.0 * M_PI);
}
/**
* Clamps an angle between min and max.
*
* All angles are in radians.
*
* This function treats angles as existing in a looping (cyclic) space, and is therefore
* specifically not equivalent to a simple `clamp(angle, min, max)`. `min` and `max` are treated as
* a directed range on the unit circle and `angle` is treated as a point on the unit circle.
* `angle` is then clamped to be within the directed range defined by `min` and `max`.
*/
static float clamp_angle(const float angle, const float min, const float max)
{
/* If the allowed range exceeds 360 degrees no clamping can occur. */
if ((max - min) >= (2 * M_PI)) {
return angle;
}
/* Invalid case, just return min. */
if (max <= min) {
return min;
}
/* Move min and max into a space where `angle == 0.0`, and wrap them to
* [-PI, +PI] in that space. This simplifies the cases below, as we can
* just use 0.0 in place of `angle` and know that everything is in
* [-PI, +PI]. */
const float min_wrapped = wrap_rad_angle(min - angle);
const float max_wrapped = wrap_rad_angle(max - angle);
/* If the range defined by `min`/`max` doesn't contain the boundary at
* PI/-PI. This is the simple case, because it means we can do a simple
* clamp. */
if (min_wrapped < max_wrapped) {
return angle + std::clamp(0.0f, min_wrapped, max_wrapped);
}
/* At this point we know that `min_wrapped` >= `max_wrapped`, meaning the boundary is crossed.
* With that we know that no clamping is needed in the following case. */
if (max_wrapped >= 0.0 || min_wrapped <= 0.0) {
return angle;
}
/* If zero is outside of the range, we clamp to the closest of `min_wrapped` or `max_wrapped`. */
if (std::fabs(max_wrapped) < std::fabs(min_wrapped)) {
return angle + max_wrapped;
}
return angle + min_wrapped;
}
static void rotlimit_evaluate(bConstraint *con, bConstraintOb *cob, ListBase * /*targets*/)
{
bRotLimitConstraint *data = static_cast<bRotLimitConstraint *>(con->data);
@ -1693,31 +1751,13 @@ static void rotlimit_evaluate(bConstraint *con, bConstraintOb *cob, ListBase * /
/* limiting of euler values... */
if (data->flag & LIMIT_XROT) {
if (eul[0] < data->xmin) {
eul[0] = data->xmin;
}
if (eul[0] > data->xmax) {
eul[0] = data->xmax;
}
eul[0] = clamp_angle(eul[0], data->xmin, data->xmax);
}
if (data->flag & LIMIT_YROT) {
if (eul[1] < data->ymin) {
eul[1] = data->ymin;
}
if (eul[1] > data->ymax) {
eul[1] = data->ymax;
}
eul[1] = clamp_angle(eul[1], data->ymin, data->ymax);
}
if (data->flag & LIMIT_ZROT) {
if (eul[2] < data->zmin) {
eul[2] = data->zmin;
}
if (eul[2] > data->zmax) {
eul[2] = data->zmax;
}
eul[2] = clamp_angle(eul[2], data->zmin, data->zmax);
}
loc_eulO_size_to_mat4(cob->matrix, loc, eul, size, rot_order);

@ -2639,37 +2639,43 @@ static void rna_def_constraint_rotation_limit(BlenderRNA *brna)
prop = RNA_def_property(srna, "min_x", PROP_FLOAT, PROP_ANGLE);
RNA_def_property_float_sdna(prop, nullptr, "xmin");
RNA_def_property_range(prop, -1000.0, 1000.0f);
RNA_def_property_ui_text(prop, "Minimum X", "Lowest X value to allow");
RNA_def_property_ui_range(prop, -2 * M_PI, 2 * M_PI, 10.0f, 1.0f);
RNA_def_property_ui_text(prop, "Minimum X", "Lower X angle bound");
RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update");
prop = RNA_def_property(srna, "min_y", PROP_FLOAT, PROP_ANGLE);
RNA_def_property_float_sdna(prop, nullptr, "ymin");
RNA_def_property_range(prop, -1000.0, 1000.0f);
RNA_def_property_ui_text(prop, "Minimum Y", "Lowest Y value to allow");
RNA_def_property_ui_range(prop, -2 * M_PI, 2 * M_PI, 10.0f, 1.0f);
RNA_def_property_ui_text(prop, "Minimum Y", "Lower Y angle bound");
RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update");
prop = RNA_def_property(srna, "min_z", PROP_FLOAT, PROP_ANGLE);
RNA_def_property_float_sdna(prop, nullptr, "zmin");
RNA_def_property_range(prop, -1000.0, 1000.0f);
RNA_def_property_ui_text(prop, "Minimum Z", "Lowest Z value to allow");
RNA_def_property_ui_range(prop, -2 * M_PI, 2 * M_PI, 10.0f, 1.0f);
RNA_def_property_ui_text(prop, "Minimum Z", "Lower Z angle bound");
RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update");
prop = RNA_def_property(srna, "max_x", PROP_FLOAT, PROP_ANGLE);
RNA_def_property_float_sdna(prop, nullptr, "xmax");
RNA_def_property_range(prop, -1000.0, 1000.0f);
RNA_def_property_ui_text(prop, "Maximum X", "Highest X value to allow");
RNA_def_property_ui_range(prop, -2 * M_PI, 2 * M_PI, 10.0f, 1.0f);
RNA_def_property_ui_text(prop, "Maximum X", "Upper X angle bound");
RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update");
prop = RNA_def_property(srna, "max_y", PROP_FLOAT, PROP_ANGLE);
RNA_def_property_float_sdna(prop, nullptr, "ymax");
RNA_def_property_range(prop, -1000.0, 1000.0f);
RNA_def_property_ui_text(prop, "Maximum Y", "Highest Y value to allow");
RNA_def_property_ui_range(prop, -2 * M_PI, 2 * M_PI, 10.0f, 1.0f);
RNA_def_property_ui_text(prop, "Maximum Y", "Upper Y angle bound");
RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update");
prop = RNA_def_property(srna, "max_z", PROP_FLOAT, PROP_ANGLE);
RNA_def_property_float_sdna(prop, nullptr, "zmax");
RNA_def_property_range(prop, -1000.0, 1000.0f);
RNA_def_property_ui_text(prop, "Maximum Z", "Highest Z value to allow");
RNA_def_property_ui_range(prop, -2 * M_PI, 2 * M_PI, 10.0f, 1.0f);
RNA_def_property_ui_text(prop, "Maximum Z", "Upper Z angle bound");
RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update");
prop = RNA_def_property(srna, "euler_order", PROP_ENUM, PROP_NONE);