缩放贝塞尔曲线中的切线

Scaling tangent lines in a bezier curve

我目前正在从事一个关于贝塞尔曲线及其属性的项目。我正在努力理解一个概念。我似乎无法理解为什么您需要对贝塞尔曲线中的切线进行缩放。

这是我制作动画的代码。

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()

如您所见,公式有效且正确。正如您在 do_animation() 函数中也可以看到的那样,有一个 v_factor 可以使 t 变小。这是因为动画以每秒 50 帧的速度运行。每个进来的 t 都是 0.02,除以 5 使点在到达 u = 1 之前穿过曲线移动 5 秒。

你可能注意到了,我把速度矢量除以v_factor,我不明白这个概念。我知道我的 v_factor 是一个秤,但我不明白为什么我需要它。贝塞尔曲线的导数不只是在曲线中的每个点输出正确的速度吗?我只知道当我从那里删除 v_factor 时,我的速度矢量会变得太大而无法适应我的屏幕。正如我所说,我知道这是一个秤,但我为什么需要它?为什么 V_pos 向量不需要它?我试图从这个 Whosebug post 中理解这个概念:How to calculate tangent for cubic bezier curve?,但不幸的是没有成功。

我没看到你将速度矢量除以 v_factor。我猜你把那部分去掉了?

无论如何,这个问题可能只是一些初等微积分。您正在根据参数 u 评估贝塞尔曲线,当您计算导数时,您将得到 dx/dudy/du.

您似乎想要显示速度 w.r.t。时间,所以你需要 dx/dtdy/dt。根据链式法则,dx/dt = dx/du * du/dtdu/dt = 1/v_factor。这就是为什么你需要划分速度矢量。

任何一点的速度 B(t) 是导数矢量 B'(t) 所以你做对了那部分,但是导数(记住不要称它为切线:切线是没有起点的真实直线或终点)在一个点告诉你“在一个时间单位内”的速度。并且不方便的是,整个贝塞尔曲线仅覆盖一个时间单位(使用t参数运行从0到1)...

所以,如果我们 space 我们的 t 值使用一些 smaller-than-one 值,就像你的曲线使用 20 个点(所以 t 间隔 1/20),我们也需要将所有这些导数缩放 1/(point count)

如果我们看一下未缩放的导数,它们既笨重又庞大:

但是如果我们将它们按 1/20 缩放,以便它们代表我们实际使用的时间间隔的速度矢量,看起来 非常好:

并且我们看到基于曲率的“真正的下一个点在哪里”和“前一个点加上它的速度矢量表明它将在哪里”之间的错误:曲率越大,离“真实点”将是“前一个点 + 时间间隔内的速度”,我们可以通过使用更多点来缓解这种情况。

如果我们在曲线中只使用 8 步,导数按 1/8 缩放,事情看起来就不会那么好:

如果我们将其提高到 15,缩放比例为 1/15,情况就会开始好转:

虽然你的 20 点曲线看起来不错,但让我们看看 50 点和 1/50 缩放得到的结果:

太好了。

其他答案都很好,但我想指出的是,缩放导数的公式实际上是导数阶数的倒数。

如果时间间隔从0到5,则实际速度为v<sub>5</sub>(t) = v(t) / 5²,实际加速度为a<sub>5</sub>(t) = a(t) / 5³。或者更一般地说,如果您的间隔从 0 到 x,v(x, t) = v(t) / x²a(x, t) = a(t) / x³.

因此,如果您的目标是缩放速度曲线,而不是将线分成几段,则可以延长时间间隔。请注意,对于 [0,1] 的规则间隔,在这两种情况下都将其除以 1,结果是一样的!这有一些直观的意义,因为你想越快完成曲线,点就需要越快。