bevy_math

Module curve

Source
Available on crate feature curve only.
Expand description

The Curve trait, providing a domain-agnostic description of curves.

§Overview

At a high level, Curve is a trait that abstracts away the implementation details of curves, which comprise any kind of data parametrized by a single continuous variable. For example, that variable could represent time, in which case a curve would represent a value that changes over time, as in animation; on the other hand, it could represent something like displacement or distance, as in graphs, gradients, and curves in space.

The trait itself has two fundamental components: a curve must have a domain, which is a nonempty range of f32 values, and it must be able to be sampled on every one of those values, producing output of some fixed type.

A primary goal of the trait is to allow interfaces to simply accept impl Curve<T> as input rather than requiring for input curves to be defined in data in any particular way. This is supported by a number of interface methods which allow changing parametrizations, mapping output, and rasterization.

§Analogy with Iterator

The Curve API behaves, in many ways, like a continuous counterpart to Iterator. The analogy looks something like this with some of the common methods:

IteratorsCurves
mapmap
skip/step_byreparametrize
enumerategraph
chainchain
zipzip
revreverse
by_refby_ref

Of course, there are very important differences, as well. For instance, the continuous nature of curves means that many iterator methods make little sense in the context of curves, or at least require numerical techniques. For example, the analogue of sum would be an integral, approximated by something like Riemann summation.

Furthermore, the two also differ greatly in their orientation to borrowing and mutation: iterators are mutated by being iterated, and by contrast, all curve methods are immutable. More information on the implications of this can be found below.

§Defining curves

Curves may be defined in a number of ways. The following are common:

Among these, the first is the most versatile1: the domain and the sampling output are just specified directly in the construction. For this reason, function curves are a reliable go-to for simple one-off constructions and procedural uses, where flexibility is desirable. For example:

// A sinusoid:
let sine_curve = FunctionCurve::new(Interval::EVERYWHERE, f32::sin);

// A sawtooth wave:
let sawtooth_curve = FunctionCurve::new(Interval::EVERYWHERE, |t| t % 1.0);

// A helix:
let helix_curve = FunctionCurve::new(Interval::EVERYWHERE, |theta| vec3(theta.sin(), theta, theta.cos()));

Sample-interpolated curves commonly arises in both rasterization and in animation, and this library has support for producing them in both fashions. See below for more information about rasterization. Here is what an explicit sample-interpolated curve might look like:

// A list of angles that we want to traverse:
let angles = [
    0.0,
    -FRAC_PI_2,
    0.0,
    FRAC_PI_2,
    0.0
];

// Make each angle into a rotation by that angle:
let rotations = angles.map(|angle| Rot2::radians(angle));

// Interpolate these rotations with a `Rot2`-valued curve:
let rotation_curve = SampleAutoCurve::new(interval(0.0, 4.0).unwrap(), rotations).unwrap();

For more information on spline curves and easing curves, see their respective modules.

And, of course, you are also free to define curve types yourself, implementing the trait directly. For custom sample-interpolated curves, the cores submodule provides machinery to avoid having to reimplement interpolation logic yourself. In many other cases, implementing the trait directly is often quite straightforward:

struct ExponentialCurve {
    exponent: f32,
}

impl Curve<f32> for ExponentialCurve {
    fn domain(&self) -> Interval {
        Interval::EVERYWHERE
    }

    fn sample_unchecked(&self, t: f32) -> f32 {
        f32::exp(self.exponent * t)
    }

    // All other trait methods can be inferred from these.
}

§Transforming curves

The API provides a few key ways of transforming one curve into another. These are often useful when you would like to make use of an interface that requires a curve that bears some logical relationship to one that you already have access to, but with different requirements or expectations. For example, the output type of the curves may differ, or the domain may be expected to be different. The map and reparametrize methods can help address this.

As a simple example of the kind of thing that arises in practice, let’s imagine that we have a Curve<Vec2> that we want to use to describe the motion of some object over time, but the interface for animation expects a Curve<Vec3>, since the object will move in three dimensions:

// Our original curve, which may look something like this:
let ellipse_curve = FunctionCurve::new(
    interval(0.0, TAU).unwrap(),
    |t| vec2(t.cos(), t.sin() * 2.0)
);

// Use `map` to situate this in 3D as a Curve<Vec3>; in this case, it will be in the xy-plane:
let ellipse_motion_curve = ellipse_curve.map(|pos| pos.extend(0.0));

We might imagine further still that the interface expects the curve to have domain [0, 1]. The reparametrize methods can address this:

// Change the domain to `[0, 1]` instead of `[0, TAU]`:
let final_curve = ellipse_motion_curve.reparametrize_linear(Interval::UNIT).unwrap();

Of course, there are many other ways of using these methods. In general, map is used for transforming the output and using it to drive something else, while reparametrize preserves the curve’s shape but changes the speed and direction in which it is traversed. For instance:

// A line segment curve connecting two points in the plane:
let start = vec2(-1.0, 1.0);
let end = vec2(1.0, 1.0);
let segment = FunctionCurve::new(Interval::UNIT, |t| start.lerp(end, t));

// Let's make a curve that goes back and forth along this line segment forever.
//
// Start by stretching the line segment in parameter space so that it travels along its length
// from `-1` to `1` instead of `0` to `1`:
let stretched_segment = segment.reparametrize_linear(interval(-1.0, 1.0).unwrap()).unwrap();

// Now, the *output* of `f32::sin` in `[-1, 1]` corresponds to the *input* interval of
// `stretched_segment`; the sinusoid output is mapped to the input parameter and controls how
// far along the segment we are:
let back_and_forth_curve = stretched_segment.reparametrize(Interval::EVERYWHERE, f32::sin);

§Combining curves

Curves become more expressive when used together. For example, maybe you want to combine two curves end-to-end:

// A line segment connecting `(-1, 0)` to `(0, 0)`:
let line_curve = FunctionCurve::new(
    Interval::UNIT,
    |t| vec2(-1.0, 0.0).lerp(vec2(0.0, 0.0), t)
);

// A half-circle curve starting at `(0, 0)`:
let half_circle_curve = FunctionCurve::new(
    interval(0.0, PI).unwrap(),
    |t| vec2(t.cos() * -1.0 + 1.0, t.sin())
);

// A curve that traverses `line_curve` and then `half_circle_curve` over the interval
// from `0` to `PI + 1`:
let combined_curve = line_curve.chain(half_circle_curve).unwrap();

Or, instead, maybe you want to combine two curves the other way, producing a single curve that combines their output in a tuple:

// Some entity's position in 2D:
let position_curve = FunctionCurve::new(Interval::UNIT, |t| vec2(t.cos(), t.sin()));

// The same entity's orientation, described as a rotation. (In this case it will be spinning.)
let orientation_curve = FunctionCurve::new(Interval::UNIT, |t| Rot2::radians(5.0 * t));

// Both in one curve with `(Vec2, Rot2)` output:
let position_and_orientation = position_curve.zip(orientation_curve).unwrap();

See the documentation on chain and zip for more details on how these methods work.

§Resampling and rasterization

Sometimes, for reasons of portability, performance, or otherwise, it can be useful to ensure that curves of various provenance all actually share the same concrete type. This is the purpose of the resample family of functions: they allow a curve to be replaced by an approximate version of itself defined by interpolation over samples from the original curve.

In effect, this allows very different curves to be rasterized and treated uniformly. For example:

// A curve that is not easily transported because it relies on evaluating a function:
let interesting_curve = FunctionCurve::new(Interval::UNIT, |t| vec2(t * 3.0, t.exp()));

// A rasterized form of the preceding curve which is just a `SampleAutoCurve`. Inside, this
// just stores an `Interval` along with a buffer of sample data, so it's easy to serialize
// and deserialize:
let resampled_curve = interesting_curve.resample_auto(100).unwrap();

// The rasterized form can be seamlessly used as a curve itself:
let some_value = resampled_curve.sample(0.5).unwrap();

§Ownership and borrowing

It can be easy to get tripped up by how curves specifically interact with Rust’s ownership semantics. First of all, it’s worth noting that the API never uses &mut self — every method either takes ownership of the original curve or uses a shared reference.

Because of the methods that take ownership, it is useful to be aware of the following:

  • If curve is a curve, then &curve is also a curve with the same output. For convenience, &curve can be written as curve.by_ref() for use in method chaining.
  • However, &curve cannot outlive curve. In general, it is not 'static.

In other words, &curve can be used to perform temporary operations without consuming curve (for example, to effectively pass curve into an API which expects an impl Curve<T>), but it cannot be used in situations where persistence is necessary (e.g. when the curve itself must be stored for later use).

Here is a demonstration:

//`my_curve` is obtained somehow. It is a `Curve<(f32, f32)>`.
let my_curve = some_magic_constructor();

// Now, we want to sample a mapped version of `my_curve`.

// let samples: Vec<f32> = my_curve.map(|(x, y)| y).samples(50).unwrap().collect();
// ^ This would work, but it would also invalidate `my_curve`, since `map` takes ownership.

// Instead, we pass a borrowed version of `my_curve` to `map`. It lives long enough that we
// can extract samples:
let samples: Vec<f32> = my_curve.by_ref().map(|(x, y)| y).samples(50).unwrap().collect();

// This way, we retain the ability to use `my_curve` later:
let new_curve = my_curve.map(|(x,y)| x + y);

  1. In fact, universal as well, in some sense: if curve is any curve, then FunctionCurve::new (curve.domain(), |t| curve.sample_unchecked(t)) is an equivalent function curve. 

Re-exports§

Modules§

  • Adaptors used by the Curve API for transforming and combining curves together.
  • Core data structures to be used internally in Curve implementations, encapsulating storage and access patterns for reuse.
  • Module containing different easing functions to control the transition between two values and the EasingCurve struct to make use of them.
  • The Interval type for nonempty intervals used by the Curve trait.
  • Iterable curves, which sample in the form of an iterator in order to support Vec-like output whose length cannot be known statically.
  • Sample-interpolated curves constructed using the Curve API.

Enums§

  • An error indicating that an end-to-end composition couldn’t be performed because of malformed inputs.
  • An error indicating that a linear reparametrization couldn’t be performed because of malformed inputs.
  • An error indicating that a ping ponging of a curve couldn’t be performed because of malformed inputs.
  • An error indicating that a repetition of a curve couldn’t be performed because of malformed inputs.
  • An error indicating that a resampling operation could not be performed because of malformed inputs.
  • An error indicating that a reversion of a curve couldn’t be performed because of malformed inputs.

Traits§

  • A trait for a type that can represent values of type T parametrized over a fixed interval.