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 3b43bd86; unless a sentence says otherwise, nothing here was built or executed. Compared with the decompiled XNA 4.0 Curve; no XNA oracle suite covers Curve and nothing was run; worked values were derived by hand or taken from CNA's test expectations.
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. At TARGET evaluation follows XNA's segment, step and loop rules; smooth tangent computation and cyclic loops over a zero-width key range still differ. 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 equals the old one exactly, and otherwise removes it and re-adds it, so the key's index can change. The new key is copied before the removal, so it may itself be a reference to another element of the same collection (CurveKeyCollectionTest.ReplacementMayAliasAnotherElement). Since the 0.1.0 release the comparison is exact; the release replaced in place withinMathHelper::MachineEpsilonFloat, which could leave a nearby key out of order.- 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,
setItemProperty,RemoveAt, a negativeCopyToindex) throwSystem::ArgumentOutOfRangeExceptionand aCopyTodestination that is too small throwsSystem::ArgumentException, the types XNA throws.
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. As XNA does, CNA computes t in double and rounds it once, and uses t = 0 when a segment is no wider than 1e-10, so a zero-width segment selects its first key (see degenerate curves).
Step continuity
When the segment's start key has CurveContinuity::Step, CNA, like XNA, holds the start key's value while the segment parameter t is below 1 and switches to the next key's value at t ≥ 1, that is at the next key itself; only the start key's continuity matters. With Step keys at positions 0 and 1 followed by a key at 2 (values 0, 5, 9), Evaluate(1.5f) returns 5; with Step keys at 0 and 0.5, Evaluate(0.5f) returns the second key's value. CurveTest.StepUsesNormalizedSegmentPosition pins shifted and non-unit spans. Since the 0.1.0 release this is fixed: the release compared the absolute query position with 1.0 instead of t, so those two calls returned 9 and the first key's value.
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 + last.TangentOut · (position − last.Position) | same |
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 |
For keys (0, 0) with TangentOut 2 and (1, 1) with TangentOut 3, a post-loop Linear curve gives Evaluate(2.0f) = 1 + 3 · 1 = 4 and Evaluate(1.25f) = 1.75; CurveTest.EvaluatePostLoopLinear asserts both. Since the 0.1.0 release the post-loop line uses the last key's outgoing tangent, as XNA does; the release used the first key's (FNA's line), so code that copied the last tangent onto the first key to work around it can drop that copy.
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 (CNA-BUG-028). 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. Inside the key range a segment no wider than 1e-10 gets t = 0 and evaluates to its first key, as in XNA: for keys (1, 3), (1, 4), (2, 5), Evaluate(1.0f) returns 3, and a Step or Smooth segment from 0 to 1e-11 returns its first key's value even at 1e-11 (CurveTest.DuplicateAndTinySegmentsMatchXnaSelection; the 0.1.0 release divided by zero there and returned NaN). The cyclic loop types still divide by the key span with no guard: 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 (CNA-BUG-065). A NaN query position follows XNA: an empty curve returns 0 and a one-key curve that key's value; otherwise the segment search runs to the last key, so a Smooth last segment propagates NaN and a Step one returns the last key's value (CurveTest.NaNPositionMatchesXnaForEmptySingleSmoothAndStep). 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 headers at c1c316b9 (same flags as the other math pages); the curve declarations it uses are unchanged at TARGET. 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 3b43bd86 (CurveTests 31 definitions, CurveKeyCollectionTests 25, CurveKeyTests 18, 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 suite covers Curve; several CurveTests expectations cite CNA's own comparisons with the genuine runtime (its CNA-DEMOS issue numbers), not re-run here. The two remaining differences 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