Skip to content

fix vector lerp edge case - #1032

Merged
bjornbytes merged 2 commits into
bjornbytes:stablefrom
DonaldHays:stable
Oct 29, 2026
Merged

bjornbytes merged 2 commits into
bjornbytes:stablefrom
DonaldHays:stable

Conversation

@DonaldHays

Copy link
Copy Markdown
Contributor

There are two common ways to implement lerp: a + (b - a) * t and a * (1 - t) + b * t.

LOVR actually uses both techniques: the vector type uses the former, while vec3_lerp in src/core/maf.h uses the latter.

Despite its extra multiplication, it's actually better to use the latter technique, because it guarantees that lerp(a, b, 1) == b. In the former technique, lerp(a, b, 1) may diverge from b in some cases, because floating point numbers are gonna be floating point numbers, and this can make a difference in things like interpolated procedural geometry vertex positions.

This pull request changes the vector type to use the a * (1 - t) + b * t form of lerp, and adds a test that failed on the old logic.

(The test also tosses in a handful of other tests for good measure, but only the final assertion failed before the change in lerp.)

@bjornbytes

Copy link
Copy Markdown
Owner

I like this investigation of different lerp implementations by zeux:

https://gist.github.com/zeux/ea70d4574a50cc819e5f02b6d7ebb1f2

The implementation in this PR (and in maf) adds the "exactness" property but loses "monotonicity" and "determinancy".

Luau went with t == 1. ? b : a + (b - a) * t as a compromise that preserves more of the desirable properties.

@bjornbytes

bjornbytes commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Although, the branch could be more expensive in Lua than it is in C (LuaJIT probably handles it well though).

@DonaldHays

Copy link
Copy Markdown
Contributor Author

Interesting!

I proposed this change after actually running into the issue in question, but I can look into those additional properties.

Skimming over the investigation, "monotonicity" does look important, but I'm not sure "determinancy" ("NaN only if t is infinite or a/b/t are NaN") is as important as a practical consideration. It feels like trying to lerp infinity or NaN is just kind of a "here be dragons" space.

@DonaldHays

Copy link
Copy Markdown
Contributor Author

Although, the branch could be more expensive in Lua than it is in C (LuaJIT probably handles it well though).

I'm already betting on LuaJIT optimizations a little by repeating (1 - t) three times. A common subexpression pass helps there.

@DonaldHays

Copy link
Copy Markdown
Contributor Author

Pushed a change to do t == 1 and b or a + (b - a) * t, with tests.

a + (b - a) * t fails the exactness test.
a * (1 - t) + b * t fails the monotonicity test.

I also included an assertion on the test itself (expect(monoT0 > monoT1).to.be.truthy()) because I'm scared about getting close to ulp on different architectures.

This Stack Overflow answer helped point me in the direction of how to work out a monotonicity test, though the particular test values (41808 and 3598) were found by searching random numbers until I found some that tripped the test.


I only set out to adjust vector.lerp, but would you like me to change vec3_lerp in maf while I'm at it?

@bjornbytes

Copy link
Copy Markdown
Owner

Thanks, looks good to me! I can take care of the maf changes

@bjornbytes
bjornbytes merged commit 9be6853 into bjornbytes:stable Oct 29, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants