以下为博客正文:

二次贝塞尔曲线通常以如下方式构建,给定二维平面上的固定点 P0, P1, P2,用 B(t) 表示该条曲线 

二次 Bezier 曲线匀速运动的实现【C++】【修正版】_ico

用一个动画来演示,可以更加清楚的表明这条曲线的构建过程 

二次 Bezier 曲线匀速运动的实现【C++】【修正版】_贝塞尔曲线_02

如果 t 变量本身是线性变化的话,这条贝塞尔曲线的生成过程是并不是匀速的,通常都是两头快中间慢。 



二次 Bezier 曲线匀速运动的实现【C++】【修正版】_ico_03

可以看出中间的点较为密集,而两边则较为稀疏

如何想要得到匀速的贝塞尔曲线运动呢?比如我们在某款游戏中设计了一条贝塞尔曲线的路径,如何实现玩家匀速在这条路径上运动呢? 

思考这个算法颇费了一番脑筋,其间还得到数学牛人 Charlesgao 的帮助,非常感谢他(比较糗的是,我问问题的时候就把其中的一个公式搞错了,见笑了-_-!)。 


首先需要求得 B(t) 相对于 t 的速度(梯度)公式 s(t) 




二次 Bezier 曲线匀速运动的实现【C++】【修正版】_贝塞尔曲线_04

分别在 x,y 方向上计算一阶导



为了简化公式,我们令 二次 Bezier 曲线匀速运动的实现【C++】【修正版】_贝塞尔曲线_05

其中 A, B, C 是根据 P0, P1, P2 计算出的常数   

二次 Bezier 曲线匀速运动的实现【C++】【修正版】_贝塞尔曲线_06 , 二次 Bezier 曲线匀速运动的实现【C++】【修正版】_#include_07

二次 Bezier 曲线匀速运动的实现【C++】【修正版】_ico_08

根据这个公式,可将贝塞尔曲线的长度公式,由 二次 Bezier 曲线匀速运动的实现【C++】【修正版】_贝塞尔曲线_09 转化为以下形式


二次 Bezier 曲线匀速运动的实现【C++】【修正版】_贝塞尔曲线_10



二次 Bezier 曲线匀速运动的实现【C++】【修正版】_ico_11 就是能够使 L 实现匀速运动的自变量,那么此时曲线长度应该满足线性增长,即 L(二次 Bezier 曲线匀速运动的实现【C++】【修正版】_ico_11) = L(1.0) * t,(代码中 L(1.0) 表示总长度,t 表示当前的曲线片段索引占曲线分割总数的比例,其实这里用 t 字母不太合适,容易和之前的积分上限混淆)即 

二次 Bezier 曲线匀速运动的实现【C++】【修正版】_bezier curve_13

由于 L(t) 函数非常复杂,直接求逆函数的表达式几乎不可能,还好我们可以知道它的导数为 s(t),在实际使用中,可以使用(数值分析中的)牛顿切线法 —— 二次 Bezier 曲线匀速运动的实现【C++】【修正版】_bezier curve_14 求出近似解 。

因为 L(1.0) * t 其实是常数,求导为 0,所以 二次 Bezier 曲线匀速运动的实现【C++】【修正版】_#include_15

则其迭代算法可以表达为 

二次 Bezier 曲线匀速运动的实现【C++】【修正版】_#include_16

我写了一个测试程序用于验证该算法,运算结果如下,可以看到,这条曲线已经是以匀速方式生成的了 ^_^ 


二次 Bezier 曲线匀速运动的实现【C++】【修正版】_贝塞尔曲线_17

 修正过的完整 Win32 代码:

// UniformVelocityBezier.cpp : 定义应用程序的入口点。
//
#include "stdafx.h"
#include "UniformVelocityBezier.h"
#include <stdio.h>
#include <math.h>
#include <windows.h>
#define MAX_LOADSTRING 100
// 全局变量:
HINSTANCE hInst; // 当前实例
TCHAR szTitle[MAX_LOADSTRING]; // 标题栏文本
TCHAR szWindowClass[MAX_LOADSTRING]; // 主窗口类名
// 此代码模块中包含的函数的前向声明:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
//-------------------------------------------------------------------------------------
//三个控制点
POINT P0 = { 50, 50 }, P1 = { 500, 600 }, P2 = { 800, 200 };
int ax = P0.x - 2 * P1.x + P2.x;
int ay = P0.y - 2 * P1.y + P2.y;
int bx = 2 * P1.x - 2 * P0.x;
int by = 2 * P1.y - 2 * P0.y;
double A = 4 * (ax * ax + ay *ay);
double B = 4 * (ax * bx + ay *by);
double C = bx * bx + by * by;
//曲线总长度
double total_length = 0.0;
//曲线分割的份数
const int STEP = 70;
//用于保存绘制点数据的数组
POINT pixels[STEP];
//-------------------------------------------------------------------------------------
//速度函数
/*
s(t_) = Sqrt[A*t*t+B*t+C]
*/
double s(double t)
{
return sqrt(A * t * t + B * t + C);
}
//-------------------------------------------------------------------------------------
//长度函数
/*
L(t) = Integrate[s[t], t]
L(t_) = ((2*Sqrt[A]*(2*A*t*Sqrt[C + t*(B + A*t)] + B*(-Sqrt[C] + Sqrt[C + t*(B + A*t)])) +
(B^2 - 4*A*C) (Log[B + 2*Sqrt[A]*Sqrt[C]] - Log[B + 2*A*t + 2 Sqrt[A]*Sqrt[C + t*(B + A*t)]])) /
(8* A^(3/2)));
*/
double L(double t)
{
double temp1 = sqrt(C + t * (B + A * t));
double temp2 = (2 * A * t * temp1 + B * (temp1 - sqrt(C)));
double temp3 = log(B + 2 * sqrt(A) * sqrt(C));
double temp4 = log(B + 2 * A * t + 2 * sqrt(A) * temp1);
double temp5 = 2 * sqrt(A) * temp2;
double temp6 = (B * B - 4 * A * C) * (temp3 - temp4);
return (temp5 + temp6) / (8 * pow(A, 1.5));
}
//-------------------------------------------------------------------------------------
//长度函数反函数,使用牛顿切线法求解
/*
X(n+1) = Xn - F(Xn)/F'(Xn)
*/
double InvertL(double t, double l)
{
double t1 = t, t2;
do
{
t2 = t1 - (L(t1) - l) / s(t1);
if (abs(t1 - t2) < 0.000001) // 如果几乎不再变化,即收敛
break;
t1 = t2;
}
while (true);
return t2;
}
int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPTSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
//注册窗口类
WNDCLASSEX wcex;
ZeroMemory(&wcex, sizeof(WNDCLASSEX));
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = (WNDPROC)WndProc;
wcex.hInstance = hInstance;
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszClassName = L"BezierClass";
RegisterClassEx(&wcex);
//创建窗口
HWND hWnd = CreateWindow(L"BezierClass", L"BezierDemo", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
//计算总长度
total_length = L(1);
//清空绘制点数据
ZeroMemory(&pixels, sizeof(pixels));
//设定定时刷新计时器
SetTimer(hWnd, 101, 10, 0);
//消息循环
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int) msg.wParam;
}
//
// 函数: MyRegisterClass()
//
// 目的: 注册窗口类。
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_UNIFORMVELOCITYBEZIER));
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = MAKEINTRESOURCE(IDC_UNIFORMVELOCITYBEZIER);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
return RegisterClassEx(&wcex);
}
//
// 函数: InitInstance(HINSTANCE, int)
//
// 目的: 保存实例句柄并创建主窗口
//
// 注释:
//
// 在此函数中,我们在全局变量中保存实例句柄并
// 创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
hInst = hInstance; // 将实例句柄存储在全局变量中
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
//
// 函数: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// 目的: 处理主窗口的消息。
//
// WM_COMMAND - 处理应用程序菜单
// WM_PAINT - 绘制主窗口
// WM_DESTROY - 发送退出消息并返回
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_TIMER:
{
static int nIndex = 0;
if (nIndex >= 0 && nIndex < STEP)
{
double t = (double)nIndex / STEP;
//如果按照线形增长,此时对应的曲线长度
double l = t * total_length;
//根据 L 函数的反函数,求得 l 对应的 t 值
t = InvertL(t, l);
//根据贝塞尔曲线函数,求得取得此时的x,y坐标
double x = (1 - t) * (1 - t) * P0.x + 2 * (1 - t) * t * P1.x + t * t * P2.x;
double y = (1 - t) * (1 - t) * P0.y + 2 * (1 - t) * t * P1.y + t * t * P2.y;
//取整
pixels[nIndex].x = (int)(x + 0.5);
pixels[nIndex].y = (int)(y + 0.5);
nIndex++;
InvalidateRect(hWnd, 0, 0);
}
else
{
KillTimer(hWnd, 101);
}
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
::MoveToEx(hdc, P0.x, P0.y, 0);
// 绘制直线连接起点、终点
LineTo(hdc, P1.x, P1.y);
LineTo(hdc, P2.x, P2.y);
// 绘制分段的各个点
for (int i = 0; i < STEP; i++)
{
const POINT &pt = pixels[i];
if (pt.x == 0 && pt.y == 0)
break;
// 画个 X (叉)
::MoveToEx(hdc, pt.x - 2, pt.y, 0);
::LineTo(hdc, pt.x + 2, pt.y);
::MoveToEx(hdc, pt.x, pt.y - 2, 0);
::LineTo(hdc, pt.x, pt.y + 2);
}
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
// “关于”框的消息处理程序。
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
return (INT_PTR)TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}