'Scaling tangent lines in a bezier curve
I am currently working on a project about Bezier curves and their properties. I am struggling to understand a concept. I can't seem to understand why you need a scale for the tangent line in a Bezier curve.
This is my code that does the animation.
from tkinter import Tk, Canvas
from graphics_template import *
import math, time
vp_width, vp_height = 1024, 768
w_xmin, w_ymin, w_xmax = -3, -3, 10
w_ymax = w_ymin + (w_xmax - w_xmin)/vp_width * vp_height
B2 = [[0.0, 0.0], # point 0
[7.0, 0.0], # point 1
[1.0, 4.0]] # point 2
V_pos = [] # position
V_vec = [] # velocity vector
animation_done = False
DELTA_TDRAW = 0.02 # 50 fps
def eval_Bezier2(P, t):
# P(t) = (1-t)^2P[0] + 2t(1-t) P[1] + t^2P[2]
res = [0.0, 0.0]
for xy in range(2):
res[xy] = (1-t)*(1-t)*P[0][xy] + 2*t*(1-t)*P[1][xy] + t*t*P[2][xy]
return res
def eval_dBezier2(P, t):
# P'(t) = -2(1-t)P[0] + 2(1-t)P[1]-2tP[1] + 2tP[2]
res = [0.0, 0.0]
for xy in range(2):
res[xy] = -2*(1-t)*P[0][xy] + 2*(1-t)*P[1][xy]-2*t*P[1][xy] + 2*t*P[2][xy]
return res
def draw_Bezier (P, nsteps):
xi = P[0][0]
yi = P[0][1]
t_delta = 1/nsteps
t = t_delta
for ti in range(nsteps):
p = eval_Bezier2(P, t)
draw_line(canvas, xi, yi, p[0], p[1], rgb_col(255, 0 ,0))
draw_small_square(canvas, xi, yi, rgb_col(255, 255, 0))
xi = p[0]
yi = p[1]
t += t_delta
for i in range(len(P)):
draw_small_square(canvas, P[i][0], P[i][1], rgb_col(0, 255, 0))
def do_animation (t):
global animation_done
v_factor = 5 # reparameterization
u = t/v_factor
if (t > v_factor): #animation stops at t = v_factor
animation_done = True
else:
current_pos = eval_Bezier2(B2, u);
V_pos[0] = current_pos[0]
V_pos[1] = current_pos[1]
current_vel = eval_dBezier2(B2, u);
V_vec[0] = current_vel[0]
V_vec[1] = current_vel[1]
def draw_scene ():
draw_grid(canvas)
draw_axis(canvas)
draw_Bezier(B2, 20)
draw_dot(canvas, V_pos[0], V_pos[1], rgb_col(0,255,0))
draw_line(canvas, V_pos[0], V_pos[1], (V_pos[0] + V_vec[0]), (V_pos[1] + V_vec[1]), rgb_col(0,255,0))
def init_scene ():
#no data inits needed
V_pos.append(0.0)
V_pos.append(0.0)
V_vec.append(0.0)
V_vec.append(0.0)
do_animation(0.0)
draw_scene()
window = Tk()
canvas = Canvas(window, width=vp_width, height=vp_height, bg=rgb_col(0,0,0))
canvas.pack()
init_graphics (vp_width, vp_height, w_xmin, w_ymin, w_xmax)
# time.perf_counter() -> float. Return the value (in fractional seconds)
# of a performance counter, i.e. a clock with the highest available resolution
# to measure a short duration. It does include time elapsed during sleep and
# is system-wide. The reference point of the returned value is undefined,
# so that only the difference between the results of consecutive calls is valid.
init_time = time.perf_counter()
prev_draw_time = 0
init_scene ()
while (not animation_done):
draw_dt = time.perf_counter() - init_time - prev_draw_time
if (draw_dt > DELTA_TDRAW): # 50 fps
prev_draw_time += DELTA_TDRAW
do_animation(prev_draw_time)
canvas.delete("all")
draw_scene()
canvas.update()
As you can see, the formulas work and are correct. As you also can see in the do_animation() function, there is a v_factor that makes the t smaller. This is because the animation goes at 50 frames per second. Every t that comes in is 0.02, this is divided by 5 to make the point move accross the curve for 5 seconds before reaching u = 1.
As you may notice, I divided the velocity vector by the v_factor, I don't understand this concept. I know my v_factor is a scale, but I don't understand why I need it. Doesn't the derivative of the bezier curve just output the correct velocity at every point in the curve? I only know that when I remove the v_factor from there, my velocity vector would become too big to fit my screen. As I said, I know it's a scale, but why do I need it? Why don't I need it for the V_pos vector? I tried to understand this concept from this stackoverflow post: How to calculate tangent for cubic bezier curve?, but unfortunately to no succes.
Solution 1:[1]
The velocity at any point B(t)
is the derivative vector B'(t)
so you got that part right, but the derivative (remember not to call it the tangent: tangents are true lines without a start or end point) at a point tells you the velocity "over one unit of time". And inconveniently, an entire Bezier curve only covers a single unit of time (with the t
parameter running from 0 to 1)...
So, if we space our t
values using some smaller-than-one value, like your curve does by using 20 points (so a t
interval of 1/20
), we need to scale all those derivatives by 1/(point count)
too.
If we look at the unscaled derivatives, they are unwieldy and huge:
But if we scale them by 1/20 so that they represent the velocity vector for the time interval we're actually using, things look really good:
And we see an error between "where the true next point is" and "where the previous point plus its velocity vector says it'll be" based on curvature: the stronger the curvature, the further away from the "true point" a "previous point + velocity over time interval" will be, which we can mitigate by using more points.
If we use only 8 steps in our curve, with the derivatives scaled by 1/8, things don't look all that great:
If we bump that up to 15, with 1/15 scaling, things start to look better:
And while your curve with 20 points looks alright, let's look at what 50 points with 1/50 scaling gets us:
That's preeeetty good.
Solution 2:[2]
Other answers are very good, but I would just like to point out that the formula for the scaled derivative is actually the reciprocal with the order of the derivative.
If time interval goes from 0 to 5, the actual velocity is v<sub>5</sub>(t) = v(t) / 5²
, and the actual acceleration is a<sub>5</sub>(t) = a(t) / 5³
. Or more generally, if your interval runs from 0 to x, v(x, t) = v(t) / x²
and a(x, t) = a(t) / x³
.
So if your goal is to scale the velocity curve, instead of breaking the line into pieces, you could make the time interval longer. Note that for the regular interval of [0,1], you would divide it by 1 in both cases, and it would be the same! This makes some intuitive sense, as the faster you want to complete the curve the faster the dot needs to go.
Solution 3:[3]
I don't see you dividing the velocity vector by v_factor
. I guess you took that part out?
Anyway, this problem is probably just some elementary calculus. You are evaluating your Bezier curve in terms of the parameter u
, and when you calculate the derivatives, you are getting dx/du
and dy/du
.
It seems that you want to want to show the velocities w.r.t. time, so you need dx/dt
and dy/dt
. By the chain rule, dx/dt = dx/du * du/dt
, and du/dt
= 1/v_factor
. That's why you need to divide the velocity vector.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | |
Solution 3 | Matt Timmermans |