Description
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?
- 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. - I would like it to be as fast as can be expected from creating a fixed size struct.
- I would like
CubicSegment::iter_positions
and other cool functions thatCubicCurve
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.