Curve2D/Curve3D: exact linear interpolation

While calculating interpolated points, intervals between two baked
points has been assummed to be `baked_interval`. The assumption could
cause significant error in some extreme cases (for example #7088).

To improve accuracy, `baked_dist_cache` is introduced, which stores
distance from starting point for each baked points. `interpolate_baked`
now returns exact linear-interpolated position along baked points.
This commit is contained in:
Jihyun Yu 2021-08-21 16:57:59 +09:00
parent e599f1bdf0
commit 8a6fc54ccd
3 changed files with 109 additions and 27 deletions

View file

@ -662,19 +662,27 @@ void Curve2D::_bake() const {
if (points.size() == 0) {
baked_point_cache.resize(0);
baked_dist_cache.resize(0);
return;
}
if (points.size() == 1) {
baked_point_cache.resize(1);
baked_point_cache.set(0, points[0].pos);
baked_dist_cache.resize(1);
baked_dist_cache.set(0, 0.0);
return;
}
Vector2 pos = points[0].pos;
float dist = 0.0;
List<Vector2> pointlist;
List<float> distlist;
pointlist.push_back(pos); //start always from origin
distlist.push_back(0.0);
for (int i = 0; i < points.size() - 1; i++) {
float step = 0.1; // at least 10 substeps ought to be enough?
@ -712,7 +720,10 @@ void Curve2D::_bake() const {
pos = npp;
p = mid;
dist += d;
pointlist.push_back(pos);
distlist.push_back(dist);
} else {
p = np;
}
@ -722,16 +733,20 @@ void Curve2D::_bake() const {
Vector2 lastpos = points[points.size() - 1].pos;
float rem = pos.distance_to(lastpos);
baked_max_ofs = (pointlist.size() - 1) * bake_interval + rem;
dist += rem;
baked_max_ofs = dist;
pointlist.push_back(lastpos);
distlist.push_back(dist);
baked_point_cache.resize(pointlist.size());
Vector2 *w = baked_point_cache.ptrw();
int idx = 0;
baked_dist_cache.resize(distlist.size());
for (const Vector2 &E : pointlist) {
w[idx] = E;
idx++;
Vector2 *w = baked_point_cache.ptrw();
float *wd = baked_dist_cache.ptrw();
for (int i = 0; i < pointlist.size(); i++) {
w[i] = pointlist[i];
wd[i] = distlist[i];
}
}
@ -766,18 +781,25 @@ Vector2 Curve2D::interpolate_baked(float p_offset, bool p_cubic) const {
return r[bpc - 1];
}
int idx = Math::floor((double)p_offset / (double)bake_interval);
float frac = Math::fmod(p_offset, (float)bake_interval);
if (idx >= bpc - 1) {
return r[bpc - 1];
} else if (idx == bpc - 2) {
if (frac > 0) {
frac /= Math::fmod(baked_max_ofs, bake_interval);
}
int start = 0, end = bpc, idx = (end + start) / 2;
// binary search to find baked points
while (start < idx) {
float offset = baked_dist_cache[idx];
if (p_offset <= offset) {
end = idx;
} else {
frac /= bake_interval;
start = idx;
}
idx = (end + start) / 2;
}
float offset_begin = baked_dist_cache[idx];
float offset_end = baked_dist_cache[idx + 1];
float idx_interval = offset_end - offset_begin;
ERR_FAIL_COND_V_MSG(p_offset < offset_begin || p_offset > offset_end, Vector2(), "failed to find baked segment");
float frac = (p_offset - offset_begin) / idx_interval;
if (p_cubic) {
Vector2 pre = idx > 0 ? r[idx - 1] : r[idx];
@ -1145,6 +1167,7 @@ void Curve3D::_bake() const {
baked_point_cache.resize(0);
baked_tilt_cache.resize(0);
baked_up_vector_cache.resize(0);
baked_dist_cache.resize(0);
return;
}
@ -1153,6 +1176,8 @@ void Curve3D::_bake() const {
baked_point_cache.set(0, points[0].pos);
baked_tilt_cache.resize(1);
baked_tilt_cache.set(0, points[0].tilt);
baked_dist_cache.resize(1);
baked_dist_cache.set(0, 0.0);
if (up_vector_enabled) {
baked_up_vector_cache.resize(1);
@ -1165,8 +1190,12 @@ void Curve3D::_bake() const {
}
Vector3 pos = points[0].pos;
float dist = 0.0;
List<Plane> pointlist;
List<float> distlist;
pointlist.push_back(Plane(pos, points[0].tilt));
distlist.push_back(0.0);
for (int i = 0; i < points.size() - 1; i++) {
float step = 0.1; // at least 10 substeps ought to be enough?
@ -1207,7 +1236,10 @@ void Curve3D::_bake() const {
Plane post;
post.normal = pos;
post.d = Math::lerp(points[i].tilt, points[i + 1].tilt, mid);
dist += d;
pointlist.push_back(post);
distlist.push_back(dist);
} else {
p = np;
}
@ -1218,8 +1250,10 @@ void Curve3D::_bake() const {
float lastilt = points[points.size() - 1].tilt;
float rem = pos.distance_to(lastpos);
baked_max_ofs = (pointlist.size() - 1) * bake_interval + rem;
dist += rem;
baked_max_ofs = dist;
pointlist.push_back(Plane(lastpos, lastilt));
distlist.push_back(dist);
baked_point_cache.resize(pointlist.size());
Vector3 *w = baked_point_cache.ptrw();
@ -1231,6 +1265,9 @@ void Curve3D::_bake() const {
baked_up_vector_cache.resize(up_vector_enabled ? pointlist.size() : 0);
Vector3 *up_write = baked_up_vector_cache.ptrw();
baked_dist_cache.resize(pointlist.size());
float *wd = baked_dist_cache.ptrw();
Vector3 sideways;
Vector3 up;
Vector3 forward;
@ -1242,6 +1279,7 @@ void Curve3D::_bake() const {
for (const Plane &E : pointlist) {
w[idx] = E.normal;
wt[idx] = E.d;
wd[idx] = distlist[idx];
if (!up_vector_enabled) {
idx++;
@ -1308,18 +1346,25 @@ Vector3 Curve3D::interpolate_baked(float p_offset, bool p_cubic) const {
return r[bpc - 1];
}
int idx = Math::floor((double)p_offset / (double)bake_interval);
float frac = Math::fmod(p_offset, bake_interval);
if (idx >= bpc - 1) {
return r[bpc - 1];
} else if (idx == bpc - 2) {
if (frac > 0) {
frac /= Math::fmod(baked_max_ofs, bake_interval);
}
int start = 0, end = bpc, idx = (end + start) / 2;
// binary search to find baked points
while (start < idx) {
float offset = baked_dist_cache[idx];
if (p_offset <= offset) {
end = idx;
} else {
frac /= bake_interval;
start = idx;
}
idx = (end + start) / 2;
}
float offset_begin = baked_dist_cache[idx];
float offset_end = baked_dist_cache[idx + 1];
float idx_interval = offset_end - offset_begin;
ERR_FAIL_COND_V_MSG(p_offset < offset_begin || p_offset > offset_end, Vector3(), "failed to find baked segment");
float frac = (p_offset - offset_begin) / idx_interval;
if (p_cubic) {
Vector3 pre = idx > 0 ? r[idx - 1] : r[idx];

View file

@ -161,6 +161,7 @@ class Curve2D : public Resource {
mutable bool baked_cache_dirty = false;
mutable PackedVector2Array baked_point_cache;
mutable PackedFloat32Array baked_dist_cache;
mutable float baked_max_ofs = 0.0;
void _bake() const;
@ -224,6 +225,7 @@ class Curve3D : public Resource {
mutable PackedVector3Array baked_point_cache;
mutable Vector<real_t> baked_tilt_cache;
mutable PackedVector3Array baked_up_vector_cache;
mutable PackedFloat32Array baked_dist_cache;
mutable float baked_max_ofs = 0.0;
void _bake() const;

View file

@ -216,6 +216,41 @@ TEST_CASE("[Curve] Custom curve with linear tangents") {
Math::is_equal_approx(curve->interpolate_baked(0.7), (real_t)0.8),
"Custom free curve should return the expected baked value at offset 0.7 after removing point at invalid index 10.");
}
TEST_CASE("[Curve2D] Linear sampling should return exact value") {
Ref<Curve2D> curve = memnew(Curve2D);
int len = 2048;
curve->add_point(Vector2(0, 0));
curve->add_point(Vector2((float)len, 0));
float baked_length = curve->get_baked_length();
CHECK((float)len == baked_length);
for (int i = 0; i < len; i++) {
float expected = (float)i;
Vector2 pos = curve->interpolate_baked(expected);
CHECK_MESSAGE(pos.x == expected, "interpolate_baked should return exact value");
}
}
TEST_CASE("[Curve3D] Linear sampling should return exact value") {
Ref<Curve3D> curve = memnew(Curve3D);
int len = 2048;
curve->add_point(Vector3(0, 0, 0));
curve->add_point(Vector3((float)len, 0, 0));
float baked_length = curve->get_baked_length();
CHECK((float)len == baked_length);
for (int i = 0; i < len; i++) {
float expected = (float)i;
Vector3 pos = curve->interpolate_baked(expected);
CHECK_MESSAGE(pos.x == expected, "interpolate_baked should return exact value");
}
}
} // namespace TestCurve
#endif // TEST_CURVE_H