Skip to content

CubicSegment lacks functionality of CubicCurve #17642

Closed
@hocop

Description

@hocop

What problem does this solve or what need does it fill?

CubicCurve, which is a Vec<CubicSegment>, can be used in many ways, e.g. creating roads and paths.

However, in many cases CubicSegment is very useful by itself. For example, I want to check if my entity will not collide with anything, while following a curved path. But the check is still local enough, so I don't need the whole CubicCurve

Performance considerations.

CubicCurve contains a Vec, which is heap allocated. It is expensive, if spawned every frame for many entities.

CubicSegment, on the other hand, is fixed size, which is ideal for such scenario.

However, the implementation of CubicSegment::new_bezier includes creating a CubicCurve of size 1, only to take the first element later. It is still an allocation, and in this case, an unnecessary one.

What solution would you like?

  1. I want be able to create CubicSegment from 4 control points, not just 2. Right now it seems like it is just intended for animation easing purposes.
  2. I would like it to be as fast as can be expected from creating a fixed size struct.
  3. I would like CubicSegment::iter_positions and other cool functions that CubicCurve has

What alternative(s) have you considered?

I implemented my own hacky trait to have this functionality:

pub trait CubicSegment4 {
    fn new_bezier_4(p1: impl Into<Vec2>, p2: impl Into<Vec2>, p3: impl Into<Vec2>, p4: impl Into<Vec2>) -> Self;

    fn iter_positions(self, subdivisions: usize) -> impl Iterator<Item=Vec2>;
}

impl CubicSegment4 for CubicSegment<Vec2> {
    fn new_bezier_4(p1: impl Into<Vec2>, p2: impl Into<Vec2>, p3: impl Into<Vec2>, p4: impl Into<Vec2>) -> Self {
        // A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin.
        // <https://xiaoxingchen.github.io/2020/03/02/bspline_in_so3/general_matrix_representation_for_bsplines.pdf>
        // See section 4.2 and equation 11.
        let char_matrix = [
            [1., 0., 0., 0.],
            [-3., 3., 0., 0.],
            [3., -6., 3., 0.],
            [-1., 3., -3., 1.],
        ];
        let p: [Vec2; 4] = [p1.into(), p2.into(), p3.into(), p4.into()];
        coefficients(p, char_matrix)
    }

    fn iter_positions(self, subdivisions: usize) -> impl Iterator<Item=Vec2> {
        let step = 1.0 / subdivisions as f32;
        (0..=subdivisions).map(move |i| i as f32 * step)
        .map(move |t| self.position(t))
    }
}

fn coefficients<P: VectorSpace>(p: [P; 4], char_matrix: [[f32; 4]; 4]) -> CubicSegment<P> {
    let [c0, c1, c2, c3] = char_matrix;
    // These are the polynomial coefficients, computed by multiplying the characteristic
    // matrix by the point matrix.
    let coeff = [
        p[0] * c0[0] + p[1] * c0[1] + p[2] * c0[2] + p[3] * c0[3],
        p[0] * c1[0] + p[1] * c1[1] + p[2] * c1[2] + p[3] * c1[3],
        p[0] * c2[0] + p[1] * c2[1] + p[2] * c2[2] + p[3] * c2[3],
        p[0] * c3[0] + p[1] * c3[1] + p[2] * c3[2] + p[3] * c3[3],
    ];
    CubicSegment::<P> { coeff }
}

It does the thing without memory allocations. But has some duplicated code.

Example usage:

pub fn get_curve(
    pos_1: Vec2,
    pos_2: Vec2,
    anchor: Option<Vec2>
) -> CubicSegment<Vec2> {
    if let Some(anchor) = anchor {
        // Curve with anchor point
        CubicSegment::new_bezier_4(
            pos_1,
            pos_1.lerp(anchor, 0.66),
            pos_2.lerp(anchor, 0.66),
            pos_2
        )
    } else {
        // Straight line
        CubicSegment::new_bezier_4(
            pos_1,
            pos_1.lerp(pos_2, 0.33),
            pos_1.lerp(pos_2, 0.66),
            pos_2
        )
    }
}

Additional context

Looking at code, looks like CubicSegment is being sort of ignored in favor of the CubicCurve. I think many functions like the new_bezier and new_bezier_4 should really be implemented on the base structure.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-MathFundamental domain-agnostic mathematical operationsC-UsabilityA targeted quality-of-life change that makes Bevy easier to useD-ModestA "normal" level of difficulty; suitable for simple features or challenging fixesS-Ready-For-ImplementationThis issue is ready for an implementation PR. Go for it!X-UncontroversialThis work is generally agreed upon

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions