Browse Source

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.
Jihyun Yu 4 years ago
parent
commit
8a6fc54ccd
3 changed files with 108 additions and 26 deletions
  1. 71 26
      scene/resources/curve.cpp
  2. 2 0
      scene/resources/curve.h
  3. 35 0
      tests/test_curve.h

+ 71 - 26
scene/resources/curve.cpp

@@ -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());
+	baked_dist_cache.resize(distlist.size());
+
 	Vector2 *w = baked_point_cache.ptrw();
-	int idx = 0;
+	float *wd = baked_dist_cache.ptrw();
 
-	for (const Vector2 &E : pointlist) {
-		w[idx] = E;
-		idx++;
+	for (int i = 0; i < pointlist.size(); i++) {
+		w[i] = pointlist[i];
+		wd[i] = distlist[i];
 	}
 }
 
@@ -766,19 +781,26 @@ 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 {
+			start = idx;
 		}
-	} else {
-		frac /= bake_interval;
+		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];
 		Vector2 post = (idx < (bpc - 2)) ? r[idx + 2] : r[idx + 1];
@@ -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,19 +1346,26 @@ 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 {
+			start = idx;
 		}
-	} else {
-		frac /= bake_interval;
+		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];
 		Vector3 post = (idx < (bpc - 2)) ? r[idx + 2] : r[idx + 1];

+ 2 - 0
scene/resources/curve.h

@@ -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;

+ 35 - 0
tests/test_curve.h

@@ -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