【转】从零开始学图形学:10分钟看懂贝塞尔曲线

转自:https://zhuanlan.zhihu.com/p/344934774

从零开始学图形学:10分钟看懂贝塞尔曲线

引入

在画画的时候,你可能会遇到画曲线的情况。比如你想画一个肥宅的大肚子轮廓,此时你随手一画,发现不好看,感觉太鼓了,于是你只能重新画,再画一遍,发现太小了,于是只能再重新画,如此反复许多次之后,你终于画对了。

作为一个天才小画家,你心里想,如果有一个小滑块,可以在保证曲线平滑的情况下,通过拉动滑块实现曲线形状的调节,那不就不用来回画了吗!

嘿,您别说,还真有,这个东西就叫做贝塞尔曲线(Bézier curve),有了这个,你便可以像这样调节曲线:

是不是很熟悉?没错!贝塞尔曲线广泛应用于各种绘图相关的软件中,甚至计算机中的字体设计就全靠贝塞尔曲线来控制。

接下来,我们详细讲一讲贝塞尔曲线的原理。

一个简单的例子

讲之前,我们先看一张图:

这里的 P0、P1、P2 分别称之为控制点,贝塞尔曲线的产生完全与这三个点位置相关。

这也就意味着,我们可以通过调节控制点的位置,进而调整整个曲线。

贝塞尔曲线是一个对强迫症极其友好的曲线,看这个动图就让人很舒适,而它的构造方法也一样让人很舒适。

最开始,对于绿色线段的两头 Q0 和 Q1,将其分别放在 P0 和 P1 的位置,此时让它们运动,要求:Q0 往 P1 方向,Q1 往 P2 方向,分别匀速运动,并且同时到达线段的另一头。

转化成数学公式,即为

 

在绿色线段上再取一个点 B ,如果 B 在绿色线段上的运动也满足上述的规律,那岂不是很爽!所以不妨再规定:

 

令上述等式等于 t,t 肯定是 [0,1] 的,其意义是点在它所处线段的位置。那么随着 t 的增大,Q0、Q1、B 的位置也就随之确定了!最终 B 的轨迹,便构成了贝塞尔曲线。

递归性质

仔细观察一下上述的构造过程,我们可以观察到:

首先,有三个控制点;

三个控制点形成两个线段,每个线段上有一个点在运动,于是得到两个点;

两个点形成一个线段,这个线段上有一个点在运动,于是得到一个点;

最后一个点的运动轨迹便构成了贝塞尔曲线!

我们发现,实际上是每轮都是 n 个点,形成 n-1 条线段,每个线段上有一个点在运动,那么就只关注这 n-1 个点,循环往复。最终只剩一个点时,它的轨迹便是结果。

那么,似乎最开始的控制点,也不一定是三个?如果是四个、五个,甚至更多呢?

当然有——

如此一来,你会发现贝塞尔曲线内的递归结构。实际上,上述介绍的分别是三阶、四阶、五阶的贝塞尔曲线,贝塞尔曲线可以由阶数递归定义。

公式

到了最讨厌的公式环节。

点在线段上,可以用  来表示。如果你把整个递归的每一步都按这个展开,那么最终可以得到如下公式:

 

分段贝塞尔曲线

虽然贝塞尔曲线的阶数可以很高,但是如果曲线的阶数过高,调整控制点对曲线的影响就比较小,调整起来相当麻烦。

 

于是,我们常常使用分段的贝塞尔曲线,保证每一小段不会太复杂。这样每次只用调小段,还可以做到只调局部不影响大局,那就相当舒服了。

分段带来的唯一问题是,曲线在段与段的交界处,如何保证平滑?

所谓平滑,其实就是一阶导数连续,也就是左右导数的极限相同。

对两侧的贝塞尔曲线求导,分别代入 t=0 和 t=1 (即贝塞尔曲线的开始和结束时间),让二者相等。此时能发现,当两侧控制点与分段交接点共线且形成的线段长度相等时,满足曲线平滑性质。

 

再分享一个网站,可以在线玩贝塞尔曲线:链接

实验

内容:

  1. 完成贝塞尔曲线的递归定义函数。
  2. 对曲线进行反走样(Anti-Aliasing, AA)。

解析

简单的不得了,直接带入点在线段上的公式即可。

cv::Point2f recursive_bezier(const std::vector<cv::Point2f> &control_points, float t) 
{
    // TODO: Implement de Casteljau's algorithm
    
    if(control_points.size() == 1) return control_points[0];

    std::vector<cv::Point2f> a;
    for(int i = 0;i+1 < control_points.size();i ++) {
        auto p = control_points[i] + t * (control_points[i+1] - control_points[i]);
        a.push_back(p);
    }

    return recursive_bezier(a, t);
}

对于 AA ,只需要在曲线附近做点插值就行,满足离曲线越近的像素的像素值越高,越远的越低,即可。这里随便写了点:

  1. For 2*2 grids,  for each  .
void bezier(const std::vector<cv::Point2f> &control_points, cv::Mat &window) 
{
    // TODO: Iterate through all t = 0 to t = 1 with small steps, and call de Casteljau's 
    // recursive Bezier algorithm.

    double delta = 0.001;
    for(double t = 0;t <= 1;t += delta) {
        auto point = recursive_bezier(control_points, t);
        int w = 1;
        for(int i = -w+1;i <= w;i ++) {
            for(int j = -w+1;j <= w;j ++) {
                int x = point.x + i, y = point.y + j;

                double dist = sqrt(pow(point.x-x,2) + pow(point.y-y,2));
                window.at<cv::Vec3b>(y, x)[1] = std::min(window.at<cv::Vec3b>(y, x)[1] + 255 * std::max(2-exp(dist),0.0), 255.0);

                // auto k = abs(((int)(point.x+1-i))-point.x) * abs(((int)(point.y+1-j))-point.y);
                // window.at<cv::Vec3b>(y, x)[1] = std::min(window.at<cv::Vec3b>(y, x)[1] + 255 * k, 255.0f);
            }
        }
    }

}

注:图源网络、games101课件

 

本文首发于知乎专栏图形图像与机器学习,以后会常更新,欢迎大家的关注与催更。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值