Bendy Bones: implement a new curve-aware vertex to segment mapping mode. #110758

Merged
Alexander Gavrilov merged 1 commits from angavrilov/blender:pr-bbone-curved-mapping into main 2023-09-26 14:17:29 +02:00
9 changed files with 301 additions and 7 deletions

View File

@ -140,6 +140,8 @@ class BONE_PT_curved(BoneButtonsPanel, Panel):
topcol = layout.column()
topcol.active = bone.bbone_segments > 1
topcol.prop(bone, "bbone_mapping_mode", text="Vertex Mapping")
col = topcol.column(align=True)
col.prop(bbone, "bbone_curveinx", text="Curve In X")
col.prop(bbone, "bbone_curveinz", text="Z")

View File

@ -81,6 +81,8 @@ typedef struct EditBone {
/** for envelope scaling */
float oldlength;
/** Mapping of vertices to segments. */
eBone_BBoneMappingMode bbone_mapping_mode;

Since this is purely runtime data anyway, just use the eBone_BBoneMappingMode type instead of char.

Since this is purely runtime data anyway, just use the `eBone_BBoneMappingMode` type instead of `char`.

What is the point of using an enum for a field that is a temporary storage of a char DNA field. It means nothing.

In other words: this declaration has the same type as the corresponding permanent DNA field.

What is the point of using an enum for a field that is a temporary storage of a char DNA field. It means nothing. In other words: this declaration has the same type as the corresponding permanent DNA field.

DNA not supporting enum types doesn't mean other code cannot use them. Use enum types whenever the data is of the enum type, unless there is a specific reason why this is not possible. This is not such a reason.

DNA not supporting enum types doesn't mean other code cannot use them. Use enum types whenever the data is of the enum type, unless there is a specific reason why this is not possible. This is not such a reason.
/** Type of next/prev bone handles */
char bbone_prev_type;
char bbone_next_type;

View File

@ -1048,6 +1048,7 @@ void BKE_pose_channel_free_bbone_cache(bPoseChannel_Runtime *runtime)
MEM_SAFE_FREE(runtime->bbone_pose_mats);
MEM_SAFE_FREE(runtime->bbone_deform_mats);
MEM_SAFE_FREE(runtime->bbone_dual_quats);
MEM_SAFE_FREE(runtime->bbone_segment_boundaries);
}
void BKE_pose_channel_free(bPoseChannel *pchan)

View File

@ -54,6 +54,7 @@
#include "DEG_depsgraph_query.hh"
#include "BIK_api.h"
#include "BLI_math_base_safe.h"
#include "BLO_read_write.hh"
@ -1509,7 +1510,9 @@ int BKE_pchan_bbone_spline_compute(BBoneSplineParameters *param,
return param->segments;

const

Document what joints actually means. Especially since segments actually means "number of segments" (and num_segments thus would be a better name), it gets confusing when joints doesn't mean "number of joints".

The value of joints is either determined by pchan->bone->bbone_mapping_mode == BBONE_MAPPING_CURVED or pchan->runtime->bbone_use_bsp_mapping. So what's the ground truth here? And why isn't that property used everywhere, given that each call has access to the pchan and thus also its runtime data?

There's another confusion in the naming. The bone property is called ..._segment_joints, but the property that determines whether it's allocated is called ..._bsp_mapping. I'm guessing that the "planes" in the "segment joints" form a space partition, but that's guesswork. It's better if related things are actually named such that that relation is clear. Or at the bare minimum, have either side's documentation mention the other.

`const` Document what `joints` actually means. Especially since `segments` actually means "number of segments" (and `num_segments` thus would be a better name), it gets confusing when `joints` doesn't mean "number of joints". The value of `joints` is either determined by `pchan->bone->bbone_mapping_mode == BBONE_MAPPING_CURVED` or `pchan->runtime->bbone_use_bsp_mapping`. So what's the ground truth here? And why isn't that property used everywhere, given that each call has access to the `pchan` and thus also its runtime data? There's another confusion in the naming. The bone property is called `..._segment_joints`, but the property that determines whether it's allocated is called `..._bsp_mapping`. I'm guessing that the "planes" in the "segment joints" form a space partition, but that's guesswork. It's better if related things are actually named such that that relation is clear. Or at the bare minimum, have either side's documentation mention the other.

The value of joints is either determined by pchan->bone->bbone_mapping_mode == BBONE_MAPPING_CURVED or pchan->runtime->bbone_use_bsp_mapping.

It is pretty simple: when computing the data, it is using the original mapping mode. However when copying it, it copies what is there. The same copy code also uses runtime.bbone_segments instead of bone->segments.

> The value of joints is either determined by `pchan->bone->bbone_mapping_mode == BBONE_MAPPING_CURVED` or `pchan->runtime->bbone_use_bsp_mapping`. It is pretty simple: when computing the data, it is using the original mapping mode. However when copying it, it copies what is there. The same copy code also uses `runtime.bbone_segments` instead of `bone->segments`.
}
static void allocate_bbone_cache(bPoseChannel *pchan, int segments)
static void allocate_bbone_cache(bPoseChannel *pchan,
const int segments,
const bool use_boundaries)
{
bPoseChannel_Runtime *runtime = &pchan->runtime;
@ -1526,6 +1529,69 @@ static void allocate_bbone_cache(bPoseChannel *pchan, int segments)
runtime->bbone_dual_quats = static_cast<DualQuat *>(MEM_malloc_arrayN(
1 + uint(segments), sizeof(DualQuat), "bPoseChannel_Runtime::bbone_dual_quats"));
}
/* If the segment count changed, the array was deallocated and nulled above. */
if (use_boundaries && !runtime->bbone_segment_boundaries) {
runtime->bbone_segment_boundaries = static_cast<bPoseChannel_BBoneSegmentBoundary *>(
MEM_malloc_arrayN(1 + uint(segments),
dr.sybren marked this conversation as resolved Outdated

Document what the returned bool means.

This actually computes the bbone_segment_joints. Sometimes they're called "segment joints", sometimes "joint planes", and sometimes "bsp_mapping".

Document what the returned `bool` means. This actually computes the `bbone_segment_joints`. Sometimes they're called "segment joints", sometimes "joint planes", and sometimes "bsp_mapping".
sizeof(bPoseChannel_BBoneSegmentBoundary),
"bPoseChannel_Runtime::bbone_segment_boundaries"));
}
else if (!use_boundaries) {
MEM_SAFE_FREE(runtime->bbone_segment_boundaries);
}
}
/** Computes the B-Bone segment boundary planes for the curved mapping. */
static void compute_bbone_segment_boundaries(bPoseChannel *pchan)
{
const Bone *bone = pchan->bone;
bPoseChannel_Runtime *runtime = &pchan->runtime;
const Mat4 *b_bone_rest = runtime->bbone_rest_mats;
bPoseChannel_BBoneSegmentBoundary *boundaries = runtime->bbone_segment_boundaries;
/* Convert joints to pose space. */
for (int i = 0; i <= bone->segments; i++) {
mul_v3_m4v3(boundaries[i].point, bone->arm_mat, b_bone_rest[i].mat[3]);
mul_v3_mat3_m4v3(boundaries[i].plane_normal, bone->arm_mat, b_bone_rest[i].mat[1]);
normalize_v3(boundaries[i].plane_normal);
}
angavrilov marked this conversation as resolved Outdated

It's definitely possible to make these conditions not hold with a b-bone. And this function just returns false in that case, which isn't checked or used in the calling code.

I tried disabling the last_start < first_end if clause (lines 1578-1580), and that results in a weird mapping when the conditions don't hold, but nothing really bad happens. And the straight mapping method also results in a similarly weird mapping in these cases, so that just seems par for the course.

So I think we can just remove these checks entirely, and just accept that if people make weird extreme b-bones, they're going end up with weird mappings.

It's definitely possible to make these conditions not hold with a b-bone. And this function just returns `false` in that case, which isn't checked or used in the calling code. I tried disabling the `last_start < first_end` if clause (lines 1578-1580), and that results in a weird mapping when the conditions don't hold, but nothing really bad happens. And the straight mapping method *also* results in a similarly weird mapping in these cases, so that just seems par for the course. So I think we can just remove these checks entirely, and just accept that if people make weird extreme b-bones, they're going end up with weird mappings.

The return value is not used, but failing the conditions still disables the curved mapping through side effects.

The return value is not used, but failing the conditions still disables the curved mapping through side effects.

Remove the parentheses, as the word "inverted" is not something that's optional here. It'll always do that.

Remove the parentheses, as the word "inverted" is not something that's optional here. It'll always do that.
/* Precompute coefficients for the mapping calculations. */
for (int i = 0; i <= bone->segments; i++) {
boundaries[i].plane_offset = dot_v3v3(boundaries[i].point, boundaries[i].plane_normal);
}
/* Precompute the inverted length of the curve. */
float arc_length = 0.0f;
for (int i = 0; i < bone->segments; i++) {
arc_length += len_v3v3(boundaries[i + 1].point, boundaries[i].point);
}
dr.sybren marked this conversation as resolved Outdated

"the" primary split -- I haven't seen anything about about "primary splits" or "secondary splits" or similar terms, so talking about the primary split gets confusing. What does it mean? What is it referring to?

"the" primary split -- I haven't seen anything about about "primary splits" or "secondary splits" or similar terms, so talking about *the* primary split gets confusing. What does it mean? What is it referring to?
runtime->bbone_arc_length_reciprocal = 1.0f / arc_length;
dr.sybren marked this conversation as resolved Outdated

If the points are on a curve, this is not the arclength of the bone, but the distance between its start & end point. Naming this something like straight_length and the length_sum to arc_length would make things clearer.

If the points are on a curve, this is not the arclength of the bone, but the distance between its start & end point. Naming this something like `straight_length` and the `length_sum` to `arc_length` would make things clearer.

What's the magic 0.222f?

What's the magic `0.222f`?
/* Precompute the BSP depth based widening coefficients.
* The actual space partitioning includes two extra virtual segments for the ends. */

What are layers here? And why is a width of 1 desirable?

What are layers here? And why is a width of 1 desirable?
const int bsp_depth = int(ceilf(log2f(bone->segments + 2)));
dr.sybren marked this conversation as resolved Outdated

What is the typical range of this scale factor? Is it in [0, 1] (i.e. the loop below diminishes the scale for every subsequent joint), ≥1 (i.e. the loop increases the scale for every subsequent joint), <0 (toggles back & forth between positive & negative)?

What is the typical range of this scale factor? Is it in `[0, 1]` (i.e. the loop below diminishes the scale for every subsequent joint), `≥1` (i.e. the loop increases the scale for every subsequent joint), `<0` (toggles back & forth between positive & negative)?
BLI_assert(bsp_depth <= bone->segments);
/* Maximum half-width of the smoothing band at the bsp tree root plane, in segments.
* The tuning coefficient was chosen by trial and error (see PR #110758). */
const float tuning_factor = 0.222f;
const float straight_length = len_v3v3(boundaries[0].point, boundaries[bone->segments].point);
const float max_depth_scale = bone->segments * (straight_length / arc_length) * tuning_factor;
/* Per tree layer scaling factor, aiming to reduce the radius to 1 segment at the leaf level.
* Since depth_scale is actually a reciprocal of the width, this factor is >= 1. */
const float scale_factor = powf(max_ff(max_depth_scale, 1.0f), 1.0f / (bsp_depth - 1));
boundaries[0].depth_scale = bone->segments / max_depth_scale;
for (int i = 1; i < bsp_depth; i++) {
boundaries[i].depth_scale = boundaries[i - 1].depth_scale * scale_factor;
}
}
void BKE_pchan_bbone_segments_cache_compute(bPoseChannel *pchan)
@ -1537,7 +1603,9 @@ void BKE_pchan_bbone_segments_cache_compute(bPoseChannel *pchan)
BLI_assert(segments > 1);
/* Allocate the cache if needed. */
allocate_bbone_cache(pchan, segments);
const bool use_curved_mapping = bone->bbone_mapping_mode == BBONE_MAPPING_CURVED;
allocate_bbone_cache(pchan, segments, use_curved_mapping);
/* Compute the shape. */
Mat4 *b_bone = runtime->bbone_pose_mats;

If the BSP mapping isn't going to get used, what's the use in allocating the "segment joints"? Shouldn't they either be allocated and used, or not allocated at all? Or if that's tricky, deallocated when it turns out they can't be used?

If the BSP mapping isn't going to get used, what's the use in allocating the "segment joints"? Shouldn't they either be allocated and used, or not allocated at all? Or if that's tricky, deallocated when it turns out they can't be used?

I still don't think runtime->bbone_use_bsp_mapping (now runtime->bbone_use_curved_mapping) is necessary (this is related to my other question about about the allocation). If the mapping isn't going to be used, then the data shouldn't be kept allocated and the pointer can be nullptr. This means that you can simply use runtime->bbone_segment_boundaries != nullptr instead of having a separate boolean that needs to be kept in sync.

I still don't think `runtime->bbone_use_bsp_mapping` (now `runtime->bbone_use_curved_mapping`) is necessary (this is related to my other question about about the allocation). If the mapping isn't going to be used, then the data shouldn't be kept allocated and the pointer can be `nullptr`. This means that you can simply use `runtime->bbone_segment_boundaries != nullptr` instead of having a separate boolean that needs to be kept in sync.
@ -1549,6 +1617,12 @@ void BKE_pchan_bbone_segments_cache_compute(bPoseChannel *pchan)
BKE_pchan_bbone_spline_setup(pchan, false, true, b_bone);
BKE_pchan_bbone_spline_setup(pchan, true, true, b_bone_rest);
/* Compute segment boundaries. */
if (runtime->bbone_segment_boundaries) {
BLI_assert(use_curved_mapping);
compute_bbone_segment_boundaries(pchan);
}
/* Compute deform matrices. */
/* first matrix is the inverse arm_mat, to bring points in local bone space
* for finding out which segment it belongs to */
@ -1589,7 +1663,9 @@ void BKE_pchan_bbone_segments_cache_copy(bPoseChannel *pchan, bPoseChannel *pcha
BKE_pose_channel_free_bbone_cache(&pchan->runtime);
}
else {
allocate_bbone_cache(pchan, segments);
const bool use_curved_mapping = runtime_from->bbone_segment_boundaries != nullptr;
allocate_bbone_cache(pchan, segments, use_curved_mapping);

If !use_bsp_mapping, shouldn't this reset some of the properties that are set below?

If `!use_bsp_mapping`, shouldn't this reset some of the properties that are set below?
memcpy(runtime->bbone_rest_mats, runtime_from->bbone_rest_mats, sizeof(Mat4) * (1 + segments));
memcpy(runtime->bbone_pose_mats, runtime_from->bbone_pose_mats, sizeof(Mat4) * (1 + segments));
@ -1599,6 +1675,17 @@ void BKE_pchan_bbone_segments_cache_copy(bPoseChannel *pchan, bPoseChannel *pcha
memcpy(runtime->bbone_dual_quats,
runtime_from->bbone_dual_quats,
sizeof(DualQuat) * (1 + segments));
if (use_curved_mapping) {
runtime->bbone_arc_length_reciprocal = runtime_from->bbone_arc_length_reciprocal;
memcpy(runtime->bbone_segment_boundaries,
runtime_from->bbone_segment_boundaries,
sizeof(bPoseChannel_BBoneSegmentBoundary) * (1 + segments));
}

Add an assert that runtime->bbone_segment_boundaries == nullptr in an else clause, so that it's locally clear that !use_curved_mapping implies that this pointer is nil.

Add an assert that `runtime->bbone_segment_boundaries == nullptr` in an `else` clause, so that it's locally clear that `!use_curved_mapping` implies that this pointer is nil.
else {
BLI_assert(runtime->bbone_segment_boundaries == nullptr);
dr.sybren marked this conversation as resolved Outdated

I think a better approach would be to just copy what's in the source, instead of reevaluating whether the curved mapping should be used. In other words, call allocate_bbone_cache(pchan, segments, false), then:

runtime->bbone_use_curved_mapping = runtime_from->use_curved_mapping;
runtime->bbone_arc_length_reciprocal = runtime_from->bbone_arc_length_reciprocal;
runtime->bbone_segment_boundaries = MEM_dupallocN(runtime_from->bbone_segment_boundaries);

MEM_dupallocN is NULL-safe, so it'll just do the right thing here.

I think a better approach would be to just copy what's in the source, instead of reevaluating whether the curved mapping should be used. In other words, call `allocate_bbone_cache(pchan, segments, false)`, then: ```cpp runtime->bbone_use_curved_mapping = runtime_from->use_curved_mapping; runtime->bbone_arc_length_reciprocal = runtime_from->bbone_arc_length_reciprocal; runtime->bbone_segment_boundaries = MEM_dupallocN(runtime_from->bbone_segment_boundaries); ``` `MEM_dupallocN` is NULL-safe, so it'll just do the right thing here.

This whole function is clearly structured as to avoid freeing and immediately re-allocating buffers. Otherwise it all can be changed to do free + dupalloc.

This whole function is clearly structured as to avoid freeing and immediately re-allocating buffers. Otherwise it all can be changed to do free + dupalloc.

It's not clearly structured that way. But I get your point.

It's not *clearly* structured that way. But I get your point.
}
}
}
@ -1627,10 +1714,11 @@ void BKE_pchan_bbone_deform_clamp_segment_index(const bPoseChannel *pchan,
*r_blend_next = blend;
}
void BKE_pchan_bbone_deform_segment_index(const bPoseChannel *pchan,
const float *co,
int *r_index,
float *r_blend_next)
/** Implementation of the Straight B-Bone segment mapping. */
static void find_bbone_segment_index_straight(const bPoseChannel *pchan,
const float *co,
int *r_index,

What is the joint tangent? Isn't this just the segment itself, expressed as a vector in some space or other?

What is the joint tangent? Isn't this just the segment itself, expressed as a vector in some space or other?

This question hasn't been answered yet.

This question hasn't been answered yet.
float *r_blend_next)
dr.sybren marked this conversation as resolved Outdated

Name the function after what it does, so something like bbone_segment_bsp_signed_distance.

Name the function after what it does, so something like `bbone_segment_bsp_signed_distance`.
{
const Mat4 *mats = pchan->runtime.bbone_deform_mats;
const float(*mat)[4] = mats[0].mat;
@ -1643,6 +1731,147 @@ void BKE_pchan_bbone_deform_segment_index(const bPoseChannel *pchan,
pchan, y / pchan->bone->length, r_index, r_blend_next);
}
/** Computes signed distance to the segment boundary BSP plane. */
inline float bbone_segment_bsp_signed_distance(const bPoseChannel_BBoneSegmentBoundary &boundary,
const float *co)
{
return dot_v3v3(co, boundary.plane_normal) - boundary.plane_offset;
angavrilov marked this conversation as resolved Outdated

Minor readability improvement: stack_idx -> boundary_idx_stack. stack_idx reads to me like it's a single index into a stack (like stack_ptr), rather than a stack of indices into another array. And including boundary in the name makes it clearer what the indices in the stack are indexing into.

Another possibility might be joint_idx_stack, since that's (I think?) equivalent here. Although that makes the relationship to the boundary_dist array a little less clear.

Minor readability improvement: `stack_idx` -> `boundary_idx_stack`. `stack_idx` reads to me like it's a single index into a stack (like `stack_ptr`), rather than a stack of indices into another array. And including `boundary` in the name makes it clearer what the indices in the stack are indexing into. Another possibility might be `joint_idx_stack`, since that's (I think?) equivalent here. Although that makes the relationship to the `boundary_dist` array a little less clear.

What is this stack?

What is this stack?
}
/** Implementation of the Curved B-Bone segment mapping. */
static void find_bbone_segment_index_curved(const bPoseChannel *pchan,
const float *co,
int *r_index,
float *r_blend_next)
{
const bPoseChannel_BBoneSegmentBoundary *boundaries = pchan->runtime.bbone_segment_boundaries;
const int segments = pchan->runtime.bbone_segments;
/* Saved signed distances from co to each checked boundary plane. */
float boundary_dist[MAX_BBONE_SUBDIV + 1];
/* Stack of BSP plane indices that were checked in the binary search. */
int boundary_idx_stack[MAX_BBONE_SUBDIV];
int stack_top = -1;
dr.sybren marked this conversation as resolved Outdated

Now that I understand better what this is, just use a blender::Vector<int> to store a variable-sized array.
If you want to keep things as they are, at least move the declaration of stack_ptr here as well so that the relevant variables are declared together. Possibly rename stack_ptr (because it's not a pointer) to something like boundary_idx_stack_top (or stack_top_index, or something else non-pointery).

Now that I understand better what this is, just use a `blender::Vector<int>` to store a variable-sized array. If you want to keep things as they are, at least move the declaration of `stack_ptr` here as well so that the relevant variables are declared together. Possibly rename `stack_ptr` (because it's not a pointer) to something like `boundary_idx_stack_top` (or `stack_top_index`, or something else non-pointery).
/* Perform a BSP binary search to narrow things down to one segment.
* Checked BSP planes are stored for the smoothing pass later. */
int start = -1, end = segments + 1, bias = 0;
while (end - start > 1) {
const int mid = (start + end + bias) / 2;
BLI_assert(start < mid && mid < end);
const float dist = bbone_segment_bsp_signed_distance(boundaries[mid], co);
boundary_idx_stack[++stack_top] = mid;
boundary_dist[mid] = dist;
if (dist < 0) {
end = mid;
/* Bias division of odd numbers toward the previous split. This should produce
* a slightly smoother and more symmetrical evolute boundary near the ends. */
bias = 1;
}
else {
start = mid;
bias = 0;
angavrilov marked this conversation as resolved Outdated

If this can all just be rolled into the main binary traversal further below, I think that would be cleaner.

If this can all just be rolled into the main binary traversal further below, I think that would be cleaner.

Well, bias cannot be rolled into it cleanly, and it ensures that the ends get the maximum number of bsp splits for symmetry and smoothness.

Well, `bias` cannot be rolled into it cleanly, and it ensures that the ends get the maximum number of bsp splits for symmetry and smoothness.

Biasing toward the previous rather than the first split also seems ok, so fixed.

Biasing toward the previous rather than the first split also seems ok, so fixed.

I'm having a little trouble following the math on this. If I understand correctly, the goal here is to compute a [0, 1] parameter that represents the [head, tail] position across the whole length of the bone. If that's the case, this code is easier for me to follow:

const float t = d1 / (d1 + d2);  /* Position between start and end. */
head_tail = (segment_size * start) + (segment_size * t);  /* Position between head and tail. */

Maybe that's just me, but it also takes one less multiply.

I'm having a little trouble following the math on this. If I understand correctly, the goal here is to compute a [0, 1] parameter that represents the [head, tail] position across the whole length of the bone. If that's the case, this code is easier for me to follow: ```c++ const float t = d1 / (d1 + d2); /* Position between start and end. */ head_tail = (segment_size * start) + (segment_size * t); /* Position between head and tail. */ ``` Maybe that's just me, but it also takes one less multiply.

I think I was thinking in terms of a 'weighted sum' here, hence the somewhat weird math expression.

I think I was thinking in terms of a 'weighted sum' here, hence the somewhat weird math expression.
}
}
angavrilov marked this conversation as resolved Outdated

We should include a reference to this PR in this comment, since the rationale behind this as well as the blend file where you worked out the formulas for this are both there. And that gives people a lot more to work from if this ever needs to be revisited in the future.

We should include a reference to this PR in this comment, since the rationale behind this as well as the blend file where you worked out the formulas for this are both there. And that gives people a lot more to work from if this ever needs to be revisited in the future.
/* Compute the mapping from the individual segment, or the curve ends. */
const float segment_size = 1.0f / segments;
float head_tail;
if (end <= 0) {
head_tail = 0;
}
else if (start >= segments) {
head_tail = 1;
}
else {
/* Linear interpolation between the innermost two planes. */
const float d1 = fabsf(boundary_dist[start]);
const float d2 = fabsf(boundary_dist[end]);
const float t = d1 / (d1 + d2);
dr.sybren marked this conversation as resolved Outdated

is "the original 3d point" the same as co?

is "the original 3d point" the same as `co`?
head_tail = segment_size * (start + t);
}
/* Smooth the mapping to suppress discontinuities by using BSP boundaries up the stack.
*
* This works basically by pulling the mapped position towards the boundary in order to
* reduce the gradient slope to the ideal value (the one you get for points directly on
* the curve), using heuristic blend strength falloff coefficients based on the distances
* to the boundary plane before and after mapping. See PR #110758 for more details, or
* https://wiki.blender.org/wiki/Source/Animation/B-Bone_Vertex_Mapping#Curved_Mapping */
const float segment_scale = pchan->runtime.bbone_arc_length_reciprocal;
for (int i = stack_top; i >= 0; --i) {
const int boundary_idx = boundary_idx_stack[i];
/* Boundary in the head-tail space. */
const float boundary_pos = boundary_idx * segment_size;
/* Distance of the original 3d point (co) from the boundary plane,
* mapped to the head-tail space using the ideal slope ratio. */
const float point_dist = boundary_dist[boundary_idx] * segment_scale;
const float point_dist_abs = fabsf(point_dist);
/* Distance of the current mapped position from the boundary in the head-tail space. */
const float mapped_dist = fabsf(head_tail - boundary_pos);
/* Only reduce the local gradient slope, don't increase it. This basically limits
* smoothing to the inside of the curve, leaving outside as is. */
const float slope_gap = mapped_dist - point_dist_abs;

Where do these constants come from (inclyuding the 3.0f above)? It's fine if they were just experimentally found. Just add some documentation that explains.

Where do these constants come from (inclyuding the `3.0f` above)? It's fine if they were just experimentally found. Just add some documentation that explains.

The refactor to using named constants is good, but it doesn't answer the question of where these values came from.

The refactor to using named constants is good, but it doesn't answer the question of where these values came from.

It comes from what looks reasonably good in the pictures.

It comes from what looks reasonably good in the pictures.
if (slope_gap <= 0) {
continue;
}
/* Only affect points close to the split line; the radius depends on the depth
* in the stack using precomputed coefficients. */
const float dist_coeff = 1.0f - point_dist_abs * boundaries[i].depth_scale;
if (dist_coeff <= 0) {
continue;
}
/* Asymptotically clamp the slope coefficient to 1. The tune coefficients here and
* below control the sharpness of the transition and were chosen by trial and error. */
const float slope_tune_coeff = 3.0f;
const float scaled_gap = slope_gap * slope_tune_coeff;
const float slope_coeff = scaled_gap / (scaled_gap + point_dist_abs);
/* Smooth the distance based coefficient around zero. */
const float dist_tune_coeff = 7.0f;
const float dist_coeff_smooth = dist_coeff * dist_coeff * (dist_tune_coeff + 1.0f) /
(dist_tune_coeff * dist_coeff + 1.0f);
/* Blend towards the point on the ideal slope. */
const float target_pos = boundary_pos + point_dist;
head_tail = interpf(target_pos, head_tail, slope_coeff * dist_coeff_smooth);
}
/* Calculate the indices of the 2 affecting b_bone segments. */
BKE_pchan_bbone_deform_clamp_segment_index(pchan, head_tail, r_index, r_blend_next);
}
void BKE_pchan_bbone_deform_segment_index(const bPoseChannel *pchan,
const float *co,
int *r_index,
float *r_blend_next)
{
if (pchan->runtime.bbone_segment_boundaries) {
find_bbone_segment_index_curved(pchan, co, r_index, r_blend_next);
}
else {
find_bbone_segment_index_straight(pchan, co, r_index, r_blend_next);
}
}
/** \} */
/* -------------------------------------------------------------------- */

View File

@ -1313,6 +1313,7 @@ static int armature_symmetrize_exec(bContext *C, wmOperator *op)
ebone->bbone_prev_type = ebone_iter->bbone_prev_type;
ebone->bbone_next_type = ebone_iter->bbone_next_type;
ebone->bbone_mapping_mode = ebone_iter->bbone_mapping_mode;
ebone->bbone_flag = ebone_iter->bbone_flag;
ebone->bbone_prev_flag = ebone_iter->bbone_prev_flag;
ebone->bbone_next_flag = ebone_iter->bbone_next_flag;

View File

@ -519,6 +519,7 @@ static EditBone *make_boneList_recursive(ListBase *edbo,
eBone->bbone_prev_type = curBone->bbone_prev_type;
eBone->bbone_next_type = curBone->bbone_next_type;
eBone->bbone_mapping_mode = eBone_BBoneMappingMode(curBone->bbone_mapping_mode);
eBone->bbone_flag = curBone->bbone_flag;
eBone->bbone_prev_flag = curBone->bbone_prev_flag;
eBone->bbone_next_flag = curBone->bbone_next_flag;
@ -733,6 +734,7 @@ void ED_armature_from_edit(Main *bmain, bArmature *arm)
newBone->bbone_prev_type = eBone->bbone_prev_type;
newBone->bbone_next_type = eBone->bbone_next_type;
newBone->bbone_mapping_mode = eBone->bbone_mapping_mode;
newBone->bbone_flag = eBone->bbone_flag;
newBone->bbone_prev_flag = eBone->bbone_prev_flag;
newBone->bbone_next_flag = eBone->bbone_next_flag;

View File

@ -180,6 +180,20 @@ typedef struct bPoseChannelDrawData {
struct DualQuat;
struct Mat4;
/* Describes a plane in pose space that delimits B-Bone segments. */
dr.sybren marked this conversation as resolved Outdated

Given that this is already a "bone" (often also represented as joints), add some docstring that explains what is a "segment joint". The code also refers to a plane; the relation of "segment joints" and "planes" should be documented as well.

Given that this is already a "bone" (often also represented as joints), add some docstring that explains what is a "segment joint". The code also refers to a plane; the relation of "segment joints" and "planes" should be documented as well.
typedef struct bPoseChannel_BBoneSegmentBoundary {
/* Boundary data in pose space. */
float point[3];
float plane_normal[3];
angavrilov marked this conversation as resolved Outdated

Now that the member names have been changed, also change this comment to match: "Dot product of point and plane_normal..."

Now that the member names have been changed, also change this comment to match: "Dot product of point and plane_normal..."
/* Dot product of point and plane_normal to speed up distance computation. */
angavrilov marked this conversation as resolved Outdated

Instead of tangent and plane_bias, maybe plane_normal and plane_offset? That feels more clear to me given how they're used.

Instead of `tangent` and `plane_bias`, maybe `plane_normal` and `plane_offset`? That feels more clear to me given how they're used.
float plane_offset;
/* Inverse width of the smoothing at this level in head-tail space.
angavrilov marked this conversation as resolved Outdated

Nit: instead of "Hack" use "Optimization".

Nit: instead of "Hack" use "Optimization".

👍 for documenting this optimisation.

:+1: for documenting this optimisation.
* Optimization: this value is actually indexed by bsp depth (0 to bsp_depth-1), not joint
* index. It's put here to avoid allocating a separate array by utilizing the padding space. */
float depth_scale;
} bPoseChannel_BBoneSegmentBoundary;
typedef struct bPoseChannel_Runtime {
SessionUUID session_uuid;
@ -189,6 +203,10 @@ typedef struct bPoseChannel_Runtime {
/* B-Bone shape data: copy of the segment count for validation. */
int bbone_segments;
/* Inverse of the total length of the segment polyline. */
float bbone_arc_length_reciprocal;
char _pad1[4];
angavrilov marked this conversation as resolved Outdated

Both bbone_primary_split and bbone_bsp_depth appear unnecessary to store precomputed here, based on looking at the rest of the code. bbone_primary_split can trivially be computed on the fly at the start of the binary search (and equivalent code will be run anyway at each depth for further splits). And bbone_bsp_depth only appears to actually be used within the same function that computes it, and is otherwise only used for an assert that I don't think really guards against anything meaningful.

Both `bbone_primary_split` and `bbone_bsp_depth` appear unnecessary to store precomputed here, based on looking at the rest of the code. `bbone_primary_split` can trivially be computed on the fly at the start of the binary search (and equivalent code will be run anyway at each depth for further splits). And `bbone_bsp_depth` only appears to actually be used within the same function that computes it, and is otherwise only used for an assert that I don't think really guards against anything meaningful.

bbone_primary_split can be adjusted by the above condition checks to ensure the conditions hold, even if somehow they don't hold for segments / 2.

`bbone_primary_split` can be adjusted by the above condition checks to ensure the conditions hold, even if somehow they don't hold for `segments / 2`.
/* Rest and posed matrices for segments. */
angavrilov marked this conversation as resolved Outdated

I think we can just call this bbone_total_length_inverse.

I think we can just call this `bbone_total_length_inverse`.
struct Mat4 *bbone_rest_mats;
struct Mat4 *bbone_pose_mats;
@ -196,6 +214,10 @@ typedef struct bPoseChannel_Runtime {
/* Delta from rest to pose in matrix and DualQuat form. */
struct Mat4 *bbone_deform_mats;
struct DualQuat *bbone_dual_quats;
/* Segment boundaries for curved mode. */
struct bPoseChannel_BBoneSegmentBoundary *bbone_segment_boundaries;
void *_pad;
} bPoseChannel_Runtime;
dr.sybren marked this conversation as resolved Outdated

If those are "segment boundaries", why is the struct name "joints", and not "boundaries"?

👍 for documenting that this is specific to curved mode.

If those are "segment boundaries", why is the struct name "joints", and not "boundaries"? :+1: for documenting that this is specific to curved mode.
/* ************************************************ */

View File

@ -122,6 +122,9 @@ typedef struct Bone {
int layer;
/** For B-bones. */
short segments;
/** Vertex to segment mapping mode. */
char bbone_mapping_mode;
char _pad2[7];
/** Type of next/prev bone handles. */
char bbone_prev_type;
@ -395,6 +398,12 @@ typedef enum eBone_BBoneHandleType {
BBONE_HANDLE_TANGENT = 3, /* Custom handle in tangent mode (use direction, not location). */
} eBone_BBoneHandleType;
/* bone->bbone_mapping_mode */
typedef enum eBone_BBoneMappingMode {
BBONE_MAPPING_STRAIGHT = 0, /* Default mode that ignores the rest pose curvature. */
BBONE_MAPPING_CURVED = 1, /* Mode that takes the rest pose curvature into account. */
} eBone_BBoneMappingMode;
/* bone->bbone_flag */
typedef enum eBone_BBoneFlag {
/** Add the parent Out roll to the In roll. */

View File

@ -1104,6 +1104,22 @@ static void rna_def_bone_common(StructRNA *srna, int editbone)
{0, nullptr, 0, nullptr, nullptr},
};
static const EnumPropertyItem prop_bbone_mapping_mode[] = {
{BBONE_MAPPING_STRAIGHT,
"STRAIGHT",
0,
"Straight",
"Fast mapping that is good for most situations, but ignores the rest pose "
"curvature of the B-Bone"},
{BBONE_MAPPING_CURVED,
"CURVED",
0,
"Curved",
"Slower mapping that gives better deformation for B-Bones that are sharply "
"curved in rest pose"},
{0, nullptr, 0, nullptr, nullptr},
};
static const EnumPropertyItem prop_inherit_scale_mode[] = {
{BONE_INHERIT_SCALE_FULL, "FULL", 0, "Full", "Inherit all effects of parent scaling"},
{BONE_INHERIT_SCALE_FIX_SHEAR,
@ -1292,6 +1308,16 @@ static void rna_def_bone_common(StructRNA *srna, int editbone)
RNA_def_property_ui_text(
prop, "B-Bone Segments", "Number of subdivisions of bone (for B-Bones only)");
prop = RNA_def_property(srna, "bbone_mapping_mode", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_sdna(prop, NULL, "bbone_mapping_mode");
RNA_def_property_enum_items(prop, prop_bbone_mapping_mode);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_ui_text(
prop,
"B-Bone Vertex Mapping Mode",
"Selects how the vertices are mapped to B-Bone segments based on their position");
RNA_def_property_update(prop, 0, "rna_Armature_update_data");
prop = RNA_def_property(srna, "bbone_x", PROP_FLOAT, PROP_NONE);
if (editbone) {
RNA_def_property_update(prop, 0, "rna_Armature_editbone_transform_update");