图形渲染(4)贝赛尔曲线

1,290 阅读3分钟

欢迎关注公众号:sumsmile /专注图像处理的移动开发老兵~~

这篇文章讲述贝塞尔曲线的原理及代码实现,非常简单轻松的一篇。

背景

几何建模与仿真是图形学里很重要的模块,实际的应用场景中,模型的外观往往不规则,不能由简单的点、线、面生成。

贝塞尔曲线就是简单而实用的几何模型。 【掘金上不能放视频,请在“sumsmile”公众号内预览】图形渲染(4)贝赛尔曲线

贝塞尔曲线于1962年,由法国工程师皮埃尔·贝兹(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。维基百科-贝赛尔曲线

可见工业推动科学文明

贝塞尔曲线原理

曲线原理推导

原理非常简单,跟着我的思路一步步推导。

  1. 按比例t在直线P0 P1上可以取一个点

  1. 在P0P1和P1P2上分别按比例t取两个点,连成一条线(绿色),绿线上再按比例t取一个点(黑色),之后不断更改t的取值[0~1],黑点连续移动形成的红色线条就是二次贝赛尔曲线。

你应该有感觉了,这是个典型的递归算法。

明白了二次贝塞尔曲线,再感受更高次的演化。

  • 3次贝赛尔曲线

  • 4次贝赛尔曲线

  • 5次贝赛尔曲线

数学公式推导

上文提到,贝塞尔曲线由多个点控制。依次按比例t取线段上的点,取得的点组成线条再继续取点,最后只剩下一个点,即贝赛尔曲线的轨迹。

以二次曲线为例:

看起来就是对b0 b1 b2的坐标按权重取值。

如果是多次曲线呢?其实每个点的系数符合二次多项式的规则。

假设有n个点,那么由下面这个代数式决定曲线的轨迹,不详细讲了,耐心看看就明白了。

当然了,计算机程序不用推导这么复杂的代数,用迭代的方式计算即可。

贝塞尔曲线代码实现

实现代码、

基于openCV,屏幕上选择4个点,生成一条贝赛尔曲线。

代码有详细的注释,对照注释看代码。详细代码附录在文末。

/**
 * 贝塞尔曲线递归算法,计算每个t对一个的曲线轨迹点
 * 
 * control_points:控制贝塞尔曲线的点
 * t:插值比例/权重
 */
cv::Point2f recursive_bezier(const std::vector<cv::Point2f> &control_points, float t) 
{

    if (control_points.size() == 1) {
        return control_points[0];
    }

    std::vector<cv::Point2f> lerp_points;
    for (size_t i = 1; i < control_points.size(); i++)
    {
        lerp_points.push_back(lerp_v2f(control_points[i - 1], control_points[i], t));
    }
    
    return recursive_bezier(lerp_points, t);

}

/**
 * 触发贝塞尔曲线插值算法,获取所有曲线点
 * 
 * control_points:控制贝塞尔曲线的点
 * window:绘制曲线/点的窗口
 */
void bezier(const std::vector<cv::Point2f> &control_points, cv::Mat &window) 
{

    // 每次迭代的幅度为0.001,设计足够小的步幅,使曲线更连贯,否则可能会有断点
    for (double i = 0.0; i < 1.0; i+=0.001)
    {
        // 每一次for循环迭代,都计算出曲线上的一个轨迹点
        auto point = recursive_bezier(control_points, i);
        // 点的颜色为绿色,注意通道为BGR
        window.at<cv::Vec3b>(point.y, point.x)[1] = 255;
    }
    
}

实现效果

完整代码及工程

完整工程:

github.com/summer-go/g…

main.cpp完整代码:

#include <chrono>
#include <iostream>
#include <opencv2/opencv.hpp>

std::vector<cv::Point2f> control_points;

int pointsize = 4;

void mouse_handler(int event, int x, int y, int flags, void *userdata) 
{
    if (event == cv::EVENT_LBUTTONDOWN && control_points.size() < pointsize) 
    {
        std::cout << "Left button of the mouse is clicked - position (" << x << ", "
        << y << ")" << '\n';
        control_points.emplace_back(x, y);
    }     
}

void naive_bezier(const std::vector<cv::Point2f> &points, cv::Mat &window) 
{
    auto &p_0 = points[0];
    auto &p_1 = points[1];
    auto &p_2 = points[2];
    auto &p_3 = points[3];

    for (double t = 0.0; t <= 1.0; t += 0.001) 
    {
        auto point = std::pow(1 - t, 3) * p_0 + 3 * t * std::pow(1 - t, 2) * p_1 +
                 3 * std::pow(t, 2) * (1 - t) * p_2 + std::pow(t, 3) * p_3;

        window.at<cv::Vec3b>(point.y, point.x)[2] = 255;
    }
}

cv::Point2f lerp_v2f(const cv::Point2f& a, const cv::Point2f& b, float t)
{
    return a + (b - a) * t;
}


/**
 * 贝塞尔曲线递归算法,计算每个t对一个的曲线轨迹点
 * 
 * control_points:控制贝塞尔曲线的点
 * t:插值比例/权重
 */
cv::Point2f recursive_bezier(const std::vector<cv::Point2f> &control_points, float t) 
{

    if (control_points.size() == 1) {
        return control_points[0];
    }

    std::vector<cv::Point2f> lerp_points;
    for (size_t i = 1; i < control_points.size(); i++)
    {
        lerp_points.push_back(lerp_v2f(control_points[i - 1], control_points[i], t));
    }
    
    return recursive_bezier(lerp_points, t);

}

/**
 * 触发贝塞尔曲线插值算法,获取所有曲线点
 * 
 * control_points:控制贝塞尔曲线的点
 * window:绘制曲线/点的窗口
 */
void bezier(const std::vector<cv::Point2f> &control_points, cv::Mat &window) 
{

    // 每次迭代的幅度为0.001,设计足够小的步幅,使曲线更连贯,否则可能会有断点
    for (double i = 0.0; i < 1.0; i+=0.001)
    {
        // 每一次for循环迭代,都计算出曲线上的一个轨迹点
        auto point = recursive_bezier(control_points, i);
        // 点的颜色为绿色,注意通道为BGR
        window.at<cv::Vec3b>(point.y, point.x)[1] = 255;
    }
    
}

int main() 
{
    cv::Mat window = cv::Mat(700, 700, CV_8UC3, cv::Scalar(0));
    cv::cvtColor(window, window, cv::COLOR_BGR2RGB);
    cv::namedWindow("Bezier Curve", cv::WINDOW_AUTOSIZE);

    cv::setMouseCallback("Bezier Curve", mouse_handler, nullptr);

    int key = -1;
    while (key != 27) 
    {
        for (auto &point : control_points) 
        {
            cv::circle(window, point, 3, {255, 255, 255}, 3);
        }

        if (control_points.size() == pointsize) 
        {
            naive_bezier(control_points, window);
            bezier(control_points, window);

            cv::imshow("Bezier Curve", window);
            cv::imwrite("my_bezier_curve.png", window);
            key = cv::waitKey(0);

            return 0;
        }

        cv::imshow("Bezier Curve", window);
        key = cv::waitKey(20);
    }

return 0;
}

欢迎关注公众号:sumsmile /专注图像处理的移动开发老兵~~