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:
parent
e599f1bdf0
commit
8a6fc54ccd
3 changed files with 109 additions and 27 deletions
|
@ -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];
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue