0

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.

Picture of what I get as the velocity vector.

Mike 'Pomax' Kamermans
  • 49,297
  • 16
  • 112
  • 153
Badr
  • 3
  • 4
  • Give (part of) https://pomax.github.io/bezierinfo a read? – Mike 'Pomax' Kamermans May 02 '22 at 16:31
  • @Mike'Pomax'Kamermans I already did that, can't find anything about scales and why you need to divide the derivative by a certain factor to make it right. Do you maybe got any other suggestions? – Badr May 02 '22 at 16:37
  • Bezier curves are non-linear curves: if you travel fixed intervals of `t`, you are moving along the curve at completely different speeds at each step. Scaling for the tangent, or rather, the derivative (because the tangent is just a line, not a vector), is a cheap approximate way to compensate for that. Especially because getting the derivative for a Bezier curve is [really easy](https://pomax.github.io/bezierinfo/#derivatives). – Mike 'Pomax' Kamermans May 02 '22 at 17:29
  • @Mike'Pomax'Kamermans I don't quite understand what you mean by moving along the curve at completely different speeds at each step. Could you maybe use you're explanation to explain why my derivative is very high if I remove the scaling? I think I would get that. – Badr May 02 '22 at 17:49
  • Literally just try it: render 20 points along your curve (`for (i=0; i<=20; i++) { t = i/20; draw point at t }`) and look at how the points are spaced: that spacing is not uniform. Not even remotely. So instead of using `t` values directly, you correct them based on their derivative vector (which tells you "how far from the previous value" a point is. A large derivative means we're traveling along the curve at high speed, a small derivative tells us we're moving at low speed, so by scaling our `t` values using the derivative at that `t` value we can shift our points to look a little better) – Mike 'Pomax' Kamermans May 02 '22 at 18:35
  • Of course, that does of course require scaling based on the velocity, and right now your code doesn't seem to do that, it just scales everything by 5? (also note that your derivative is a bit strange: for a quadratic curve `P0 * (1-t)² + P1 * 2 * (1-t) * t + P2 * t²` the derivative is `2 * (P1-P0) * (1-t) + 2 * (P2-P1)*t`) – Mike 'Pomax' Kamermans May 02 '22 at 18:42
  • @Mike'Pomax'Kamermans I think I don't understand. The do_animation() function gets a t value. This t value is 0.02 everytime, because I want the animation to go 50 frames per second. So the animation does something 50 times a second. I can't use this t value in eval bezier function, because it has to big of a value. The point must move from 0(P0) to 1(P1), so 0.02 must be divided by 5, to get u, which is 0.004. This u is passed to the bezier function which gives x and y of that point. The derivative in this code is correct, this code works. But I don't understand why it works. Why divide by 5? – Badr May 02 '22 at 19:47
  • @Mike'Pomax'Kamermans As for you're comment on the derivative of the curve, this is the same. When you use some algebraic tricks you get the same as you're stating. – Badr May 02 '22 at 19:48
  • @Mike'Pomax'Kamermans If I remove the / v_factor from the code for the derivative of x and y it gives too big of a values. This is the thing I don't understand. So why are those lines correct, but if I leave current_vel as it is, without dividing the x and y by 5, it becomes incorrect? – Badr May 02 '22 at 19:53
  • But why is the `t` value the same value every time? Bezier curve draws from 0 to 1, so if you call `do_animation(t)`, you want successive calls to it to pass a `t` values that uses that Bezier interval. Where is the code that actually calls `do_animation`? – Mike 'Pomax' Kamermans May 02 '22 at 20:41
  • @Mike'Pomax'Kamermans I'm sorry I said that wrong. t increments with 0.02 everytime. It goes on like that for 5 seconds – Badr May 02 '22 at 21:55
  • How many times per second? Because if it's 10, then no scaling would be required at all. (at least, not to generate valid points over the entire `[0,1]` interval – Mike 'Pomax' Kamermans May 02 '22 at 22:05
  • 50 times. It's 50 frames per second, so the loop calls the do_animation() 50 times a second. – Badr May 02 '22 at 22:19
  • Then the question becomes: why not just pass the correct `t` value? If it's running 50fps over 5 seconds, you almost certainly want to not be tied to frame rates: record the start time, and then run `do_animation(t = (now - start) / total_seconds)` (or possibly x1000 for millis, or x1000000 for nanos). That way even if you change the framerate, or your system can't quite service one frame every 1/50th of a second, things still just work. – Mike 'Pomax' Kamermans May 02 '22 at 22:32
  • Yes, it would be better that way. But I still don't understand why I need to divide the x and y derivative by 5. Why do I get too big of values when I remove the / 5? The point moves through the curve from t=0 to t=1. Thats the first part where current_pos is retrieved from the eval_Bezier, but when I want the current_vel, I have to divide it by the v_factor to get the right velocities. Why is that? – Badr May 02 '22 at 22:39
  • @Mike'Pomax'Kamermans – Badr May 02 '22 at 22:57
  • You don't: the velocity at some position B(t) is the derivative B'(t). The problem is what you then _do_ with that value: it's the instantaneous velocity, so using it for a rather large time interval of 0.02, rather than something much smaller like 0.001 or even 0.0005, is going to yield a (anywhere between "somewhat" and "incredibly") wrong results. Scaling by 5 just happens to "look reasonable" with your current choice in interval. – Mike 'Pomax' Kamermans May 02 '22 at 23:05
  • @Mike'Pomax'Kamermans So you're saying the way I calculate the derivative is wrong? The derivative of B(t) is B'(t), that is what I do in my program. I pass the u to both the functions, and get for B(u), B''(u). This means my velocities are wrongly calculated, what can it be that is wrong? Could you look at my eval_dBezier() function, i put the readable formula in comments at beginning. – Badr May 02 '22 at 23:07
  • no, I'm saying "you're computing position and velocity at `t` values that don't need scaling" unless you're trying to hardcode your fps in there so that you're pushing the correct `t` value into your `eval_Bezier2`. That should not be the responsibility of `do_animation`, but of whatever's _calling_ `do_animation`, and that code is not in your post, so there's nothing to comment on there other than "probably a good idea to show that part too". I would recommend just "as a sanity check" to update your derivative function to use `res[xy] = (1-t)*(P[1][xy] - P[0][xy]) + t*(P[2][xy] - P[1][xy])` – Mike 'Pomax' Kamermans May 02 '22 at 23:12
  • @Mike'Pomax'Kamermans I'm sorry, I put the whole code in my post. It should you give a better idea of the funtions. At the end I put a picture of my simulation as a screenshot. As you can see, the velocity vector is too big for that point. (I know this because the correct velocity vector is shown in the answers of this assignment) I want to thank you very much for you're time and you're responses. I really appreciate it. – Badr May 02 '22 at 23:20
  • The derivative in your image is entirely correct, but the unscaled derivative represents the velocity vector "over one unit of time", whereas we're moving at intervals that are much smaller than one unit of time. So to see the velocity vector that applies to a series of t values that are spaced 1/20 apart we need to scale the derivative vectors by 1/20. I've added an answer now that we've drilled down to the problem =) – Mike 'Pomax' Kamermans May 03 '22 at 18:27

3 Answers3

2

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:

Quadratic Bezier curve (0,0),(7,0),(1,4) sampled over 20 points, with derivative vectors

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:

the same curve with scaled derivative vectors

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:

the same curve, sampled over 8 points

If we bump that up to 15, with 1/15 scaling, things start to look better:

the same curve, sampled over 15 points

And while your curve with 20 points looks alright, let's look at what 50 points with 1/50 scaling gets us:

the same curve, sampled over 50 points

That's preeeetty good.

Mike 'Pomax' Kamermans
  • 49,297
  • 16
  • 112
  • 153
  • I get it now. Thank you very much. You really helped me understand these concepts by showing me these examples. I can see now what the derivative is and how it needs to be scaled as the Bezier curve is an entire unit of time. For this assignment I had to go through the curve in 5 seconds and output the derivative vector, this means that I divide t by 5 and scale the derivative appropriately, now I understand that the points won't match as I'm printing the curve with 20 steps. You just went beyond my assignment and showed me a ton of new things, thanks for that :D – Badr May 03 '22 at 23:27
1

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.

0

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.

Matt Timmermans
  • 53,709
  • 3
  • 46
  • 87
  • Thank you for answering. My u value is 5 times smaller than my t. Shouldnt my dx/du be a very small derivative, as I evaluate the slope on a very very small du? What happens now is that my dx/du and dy/du are really big values. Take t = 0 for example, my dx/du on that point is 14 and my dy/du is 0, how can that be? But if I take the dx/dt, the value becomes smaller? – Badr May 03 '22 at 12:41
  • du is in the *denominator*, so a smaller du makes dx/du bigger. – Matt Timmermans May 03 '22 at 12:58
  • I see, I see. I get it now. I just don't quite understand the chain rule that you're using, i know the chain rule, but the use in this example I don't quite understand. Could you maybe write down the step in between. How are you going from dx/dt to dx/du * du/dt – Badr May 03 '22 at 13:11
  • https://www.khanacademy.org/math/ap-calculus-ab/ab-differentiation-2-new/ab-3-1a/v/chain-rule-introduction – Matt Timmermans May 03 '22 at 13:15
  • Thank you very much for you're help. I see now that t = 1/5t, when you take the derivative of that for the chain rule, you have to multiply you're derivative with 1/5 to get the good scaling :D Thank you!!! – Badr May 03 '22 at 23:22