Curve evaluation: keys, tangents, loop types and the XNA reference
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Read at 009d40f5 and compared with the decompiled XNA 4.0 Curve; no XNA oracle covers Curve and nothing was run; worked values were derived by hand.
Curve maps a float position, usually time, to a float value through a sorted set of CurveKey control points, with a loop rule on each side of the key range. It drives camera paths, fades and tuning tables in XNA games, and curves arrive both from code and from content files. This page gives CNA's exact evaluation rules — segment search, the Hermite basis, step keys, the five loop types, tangent computation — and compares each with the genuine XNA 4.0 algorithm, because several of CNA's rules (ported from FNA) give different values for the same keys. It is for anyone authoring curves in code, loading them from content, or porting a game whose animation timing must match. The maintainer view is Math internals: Curve, CurveKey and CurveKeyCollection; the member list is Math types: curve types.
Keys and the key collection
A CurveKey holds Position, Value, TangentIn, TangentOut and Continuity (CurveContinuity::Smooth or Step). Its position has a getter only: the collection owns ordering, so a position changes only by replacing the key. Constructors take (position, value), (position, value, tangentIn, tangentOut) and the same plus a continuity; tangents default to 0 and continuity to Smooth.
CurveKeyCollection (CurveKeyCollection.cpp) keeps its keys sorted by position:
Addinserts before the first key with a strictly greater position, so a key at an existing position goes after the equal ones. Duplicates are accepted, exactly as in XNA, and nothing throws.setItemProperty(index, key)replaces the key in place when the new position is withinMathHelper::MachineEpsilonFloatof the old one, and otherwise removes it and re-adds it, so the key's index can change.- The non-const indexer and
getItemPropertyreturn a mutableCurveKey&: editing a key's value or tangents changes the curve immediately.Clone()copies every key. - Index errors (indexer,
RemoveAt,CopyTo) throwstd::out_of_range; XNA throwsArgumentOutOfRangeException.
Curve owns its collection by value, with PreLoop and PostLoop both defaulting to CurveLoopType::Constant; getIsConstantProperty() is true for zero or one key.
Evaluate inside the key range
Curve.cpp handles the trivial cases first: no keys gives 0 and one key gives that key's value. Inside the range, GetCurvePosition walks the keys to the first one whose position is at or after the query, takes the previous key as the segment start and evaluates the cubic Hermite polynomial
t = (position - prev.Position) / (next.Position - prev.Position)
v = (2t³ - 3t² + 1)·prev.Value + (t³ - 2t² + t)·prev.TangentOut
+ (3t² - 2t³)·next.Value + (t³ - t²)·next.TangentIn
Tangents are therefore in units of value per segment, not value per unit of position: the same slope needs a larger tangent on a longer segment. That is XNA's convention, and it is why ComputeTangent scales its smooth tangents by the neighbouring spans. XNA computes the same polynomial, but finds t in double and uses t = 0 when a segment is no wider than 1e-10; CNA computes t in float with no width guard (see degenerate curves).
Step continuity
When the segment's start key has CurveContinuity::Step, XNA holds the start key's value across the whole segment and switches to the next key's value only at t ≥ 1, that is at the next key itself. CNA compares the absolute query position with the constant 1.0 instead of the segment parameter: it returns the next key's value whenever position >= 1.0f and the start key's value otherwise. The two agree only for a segment from 0 to 1. With Step keys at positions 0 and 1 followed by a key at 2 (values 0, 5, 9), Evaluate(1.5f) returns 9 in CNA and 5 in XNA; with Step keys at 0 and 0.5, Evaluate(0.5f) returns the first key's value in CNA and the second's in XNA. The only Step test evaluates at 0.5 inside a 0-to-1 segment, where the two rules coincide. Until this changes, look up step values directly instead of evaluating Step curves whose segments do not span 0 to 1.
Outside the key range: the five loop types
Before the first key PreLoop applies, after the last key PostLoop. With span = last.Position − first.Position and an integer cycle count k = ⌊(position − first.Position) / span⌋ (computed by truncating the quotient, minus one for negative quotients):
| Loop type | CNA | XNA 4.0 |
|---|---|---|
Constant | the first (or last) key's value | same |
Linear, before the range | first.Value − first.TangentIn · (first.Position − position) | same |
Linear, after the range | last.Value + first.TangentOut · (position − last.Position) | last.Value + last.TangentOut · (position − last.Position) |
Cycle | evaluates at position − k · span | same mapping |
CycleOffset | as Cycle, plus k · (last.Value − first.Value), so each repetition continues from where the last ended | same |
Oscillate | as Cycle for even k; for odd k the position is mirrored (last.Position − position + first.Position + k · span), giving a ping-pong | same mapping |
The post-loop Linear line is the one that differs: for keys (0, 0) with TangentOut 2 and (1, 1) with zero tangents, Evaluate(2.0f) is 3 in CNA and 1 in XNA. The line is FNA's (whose own comment says "with a tangent of last point"), and CurveTest.EvaluatePostLoopLinear asserts the CNA value, so the test pins the difference. Where this matters, give the first key the same TangentOut as the last, or extrapolate outside the curve yourself.
ComputeTangent and ComputeTangents
ComputeTangents(type) calls ComputeTangent(i, type) for every key, and the two-argument forms set the incoming and outgoing tangent types separately. For key i with position p and value v, the neighbours (p0, v0) and (p1, v1) are the previous and next keys; the first and last keys use themselves where a neighbour is missing.
| Type | TangentIn | TangentOut |
|---|---|---|
Flat | 0 | 0 |
Linear | v − v0 | v1 − v |
Smooth (CNA) | (v1 − v0) · (p − p0)/(p1 − p0); 0 when |p1 − p0| < 2−24 | (v1 − v0) · (p1 − p)/(p1 − p0); 0 when |p1 − p0| < the smallest denormal (about 1.4e-45) |
Smooth (XNA 4.0) | (v1 − v0) · |p0 − p|/(p1 − p0); 0 when |v1 − v0| < 1.1920929e-7 | (v1 − v0) · |p1 − p|/(p1 − p0); 0 under the same value test |
For well-spaced keys with distinct values the formulas agree: keys at positions 0, 1 and 3 with values 0, 1 and 5 give the middle key TangentIn = 5/3 and TangentOut = 10/3 in both. They differ at the edges: CNA tests the key spacing, with two thresholds about 4·1037 apart, where XNA tests the value difference with one. Neighbours whose values differ by less than FLT_EPSILON get a tiny non-zero tangent in CNA and 0 in XNA, and a neighbour span between the two CNA thresholds zeroes the in-tangent while making the out-tangent huge. The only Smooth test uses three equal values, which cannot tell the formulas apart. Curves loaded from content keep their stored tangents and are affected only if the game recomputes them; where exact agreement matters, set tangents explicitly with setTangentInProperty and setTangentOutProperty.
Degenerate curves
Duplicate key positions are legal, but Evaluate divides by the segment width and, for the cyclic loop types, by the key span, with no guard. For keys (1, 3), (1, 4), (2, 5), Evaluate(1.0f) computes 0/0 and returns NaN where XNA returns 3. If all keys share one position and a Cycle, CycleOffset or Oscillate loop applies, the cycle count is an infinity converted to int, which is undefined behaviour in C++; XNA sets the inverse span to 0 below 1.4e-45 and returns finite values: the first key's value for a position before the range and the last key's value after it. A NaN query position falls through every comparison and returns 0 in CNA. Keep key positions distinct and give cyclic curves a positive span; treat a duplicate-position curve as an authored edge case and test it with the evaluator that will ship.
Example: a camera-height curve
This fragment was syntax-checked against the TARGET headers (same flags as the other math pages); the values in the comments were computed by hand from the formulas above, not by running it.
Curve height;
height.getKeysProperty().Add(CurveKey(0.0f, 10.0f));
height.getKeysProperty().Add(CurveKey(2.0f, 40.0f));
height.getKeysProperty().Add(CurveKey(4.0f, 10.0f));
height.ComputeTangents(CurveTangent::Smooth); // tangents: key0 out 30, key1 in 0 and out 0, key2 in -30
height.setPreLoopProperty(CurveLoopType::Constant);
height.setPostLoopProperty(CurveLoopType::Constant);
const float atOne = height.Evaluate(1.0f); // 28.75: t = 0.5 in the first segment
const float atFive = height.Evaluate(5.0f); // 10: past the last key, Constant
At t = 0.5 the basis weights are 0.5, 0.125, 0.5 and −0.125, so the value is 0.5·10 + 0.125·30 + 0.5·40 − 0.125·0 = 28.75. This curve has distinct positions and values, so XNA gives the same numbers.
Curves in content
Curves also arrive from files. The XNB loader registers Microsoft.Xna.Framework.Content.CurveReader (CurveContentTypeReader.cpp) and the build-time pipeline can write the same format; the CNJ text format carries a self-contained curve (loop types by name, keys with their tangents and continuity) through CnjCanonicalRead.cpp, and CNB stores the converted form. A successful load proves that the fields were recovered; it says nothing about evaluation, which is the code above. See Content pipeline and XNB for the loading ladder.
Evidence
Checked by reading Curve.cpp, CurveKey.cpp, CurveKeyCollection.cpp and their tests at 009d40f5 (CurveTests 28 definitions, CurveKeyCollectionTests 21, CurveKeyTests 17, plus the enum-identity files); nothing was run. XNA's algorithm (FindSegment, Hermite, CalcCycle, ComputeTangent) was read in the decompiled genuine Microsoft.Xna.Framework assembly, and FNA at b355124 to locate the origin of the ported lines. No XNA oracle covers Curve; the differences above are recorded on Known Issues.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Math types: curve types
- Architecture
- Content architecture
- Maintainer workflow
- Add a regression test: the math case
- Tests and validation
- Test architecture