Curve evaluation: keys, tangents, loop types and the XNA reference

CNA snapshot 009d40f5  ·  Deep Dives › Framework core  ·  source links pinned to 009d40f5

✓

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:

  • Add inserts 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 within MathHelper::MachineEpsilonFloat of the old one, and otherwise removes it and re-adds it, so the key's index can change.
  • The non-const indexer and getItemProperty return a mutable CurveKey&: editing a key's value or tangents changes the curve immediately. Clone() copies every key.
  • Index errors (indexer, RemoveAt, CopyTo) throw std::out_of_range; XNA throws ArgumentOutOfRangeException.

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 typeCNAXNA 4.0
Constantthe first (or last) key's valuesame
Linear, before the rangefirst.Value − first.TangentIn · (first.Position − position)same
Linear, after the rangelast.Value + first.TangentOut · (position − last.Position)last.Value + last.TangentOut · (position − last.Position)
Cycleevaluates at position − k · spansame mapping
CycleOffsetas Cycle, plus k · (last.Value − first.Value), so each repetition continues from where the last endedsame
Oscillateas Cycle for even k; for odd k the position is mirrored (last.Position − position + first.Position + k · span), giving a ping-pongsame 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.

TypeTangentInTangentOut
Flat00
Linearv − v0v1 − 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.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Tests and validation
Test architecture