OpenGL 实践之贝塞尔曲线绘制
说到贝塞尔曲线,大家肯定都不陌生,网上有很多关于介绍和理解贝塞尔曲线的优秀文章和动态图。
以下两个是比较经典的动图了。
二阶贝塞尔曲线:

三阶贝塞尔曲线:

由于在工作中经常要和贝塞尔曲线打交道,所以简单说一下自己的理解:
现在假设我们要在坐标系中绘制一条直线,直线的方程很简单,就是 y=x ,很容易得到下图:

现在我们限制一下 x 的取值范围为 0~1 的闭区间,那么可以得出 y 的取值范围也是 0~1。
而在 0~1 的区间范围内,x 能取的数有多少个呢?答案当然是无数个了。

同理,y 的取值个数也是有无数个。每一个 x 都有唯一的 y 与之对应,一个 (x,y) 在坐标系上就是一个点。
所以最终得到的 0~1 区间的线段,实际上是由无数的点组成的。
那么这条线段有多长呢?长度是由 x 的取值范围来决定的,若 x 的取值为 0~2,那么线段就长了一倍。
另外,如果 x 的取值范围不是无数个,而是以 0.05 的间距从 0 到 1 之间递增,那么得到的就是一串点了。
由于 点 是一个理想状态下的描述,在数学上点是没有宽高、没有面积的。
但是,如果你在草稿纸上绘制一个点,不管你用到是铅笔、毛笔、水笔还是画笔,一个点总是要占面积的。
毛笔画一个点的面积可能需要铅笔画几十个点了。
在实际生活中,如果要以 0.05 的间距在第一幅坐标系图中画出 x 在 0~1 区间的一串点,最终结果就和直接画一条线段没啥差别了。
这就是现实和理想的差别了。理想一串点,现实一条线。
我们把这个逻辑放到手机屏幕上。
手机屏幕上的最小显示单位就是像素了,一个 1920 * 1080 的屏幕指的就是各方向上像素点的数量。
假如绘制一条和屏幕一样宽的线段,一个点最小就算一个像素,最多也就 1080 个点了。
点占的像素越多,那么实际绘制时需要的点的数量越少,这也算是潜在的优化项了。
说完直线,再回到贝塞尔曲线上。
曲线和直线都有一个共同点,它们都有各自特定的方程,只不过我们用的直线例子比较简单,既 y = x ,一眼看出计算结果。
直线方程 y = x,在数学上可以这么描述:y 是关于 x 的函数,既 y = F(x) ,其中 x 的取值决定了该直线的长度。
根据上面的理解,这个长度的直线实际又是由在 x 的取值范围内对应的无数个点组成的。
反观贝塞尔曲线方程以及对应的图形如下:
- 二阶贝塞尔曲线:


其中,P0 和 P2 是起始点,P1 是控制点。
- 三阶贝塞尔曲线


其中,P0 和 P3 是起始点,P1 和 P2 是控制点。
不难理解,假设我们要绘制一条曲线,肯定要有起始和结束点来指定曲线的范围曲线。
而控制点就是指定该曲线的弧度,或者说指定该曲线的弯曲走向,不同的控制点得出的曲线绘制结果是不一样的。
另外,可以观察到,无论是几阶贝塞尔曲线,都会有参数 t 以及 t 的取值范围限定。
t 在 0~1 范围的闭区间内,那么 t 的取值个数实际上就有无数个了,这时的 t 就可以理解成上面介绍直线中讲到的 x 。
这样一来,就可以把起始点、控制点当初固定参数,那么贝塞尔曲线计算公式就成了 B = F(t) ,B 是关于 t 的函数,而 t 的取值范围为 0~1 的闭区间。
也就是说贝塞尔曲线,选定了起始点和控制点,照样可以看成是 t 在 0~1 闭区间内对应的无数个点所组成的。
有了上面的阐述,在工(ban)程(zhuan)的角度上,就不难理解贝塞尔曲线到底怎么使用了。
Android 绘制贝塞尔曲线
Android 自带贝塞尔曲线绘制 API ,通过 Path 类的 quadTo 和 cubicTo 方法就可以完成绘制。
// 构建 path 路径,也就是选取
path.reset();
path.moveTo(p0x, p0y);
// 绘制二阶贝塞尔曲线
path.quadTo(p1x, p1y, p2x, p2y);
path.moveTo(p0x, p0y);
path.close();
// 最后的绘制操作
canvas.drawPath(path, paint);
这里的绘制实际上就是把贝塞尔曲线计算的方程式交给了 Android 系统内部去完成了,参数传递上只传递了起始点和控制点。
我们可以通过自己的代码来计算这个方程式从而对逻辑上获得更多控制权,也就是把曲线拆分成许多个点组成,如果点的尺寸比较大,甚至可以减少点的个数实现同样的效果,达到绘制优化的目的。
OpenGL 绘制
通过 OpenGL 可以实现我们上述的方案,把曲线拆分成多个点组成。这种方案要求我们在 CPU 上去计算贝塞尔曲线方程,根据 t 的每一个取值,计算出一个贝塞尔点,用 OpenGL 去绘制上这个点。
这个点的绘制可以采用 OpenGL 中画三角形 GL_TRIANGLES 的形式去绘制,这样就可以给点带上纹理效果,不过这里面的坑略多,起始点和控制点都是运行时动态可变的实现难度会大于固定不变的。
这里先介绍另一种方案,这种方案实现比较简单也能达到优化效果,我们可以把贝塞尔曲线的计算方程式交给 GPU, 在 OpenGL Shader 中去完成。
这样一来,我们只要给定起始点和控制点,中间计算贝塞尔曲线去填补点的过程就交给 Shader 去完成了。
另外,通过控制 t 的数量,我们可以控制贝塞尔点填补的疏密。
t 越大,填补的点越多,超过一定阈值后,不会对绘制效果有提升,反而影响性能。
t 越小,那么贝塞尔曲线就退化成一串点组成了。所以说 t 的取值范围也能对绘制起到优化作用。
绘制效果如下图所示:

以下就是实际的代码部分了,关于 OpenGL 的基础理论部分可以参考之前写过的文章和公众号,就不再阐述了。
在 Shader 中定义一个函数,实现贝塞尔方程:
vec2 fun(in vec2 p0, in vec2 p1, in vec2 p2, in vec2 p3, in float t){
float tt = (1.0 - t) * (1.0 -t);
return tt * (1.0 -t) *p0
+ 3.0 * t * tt * p1
+ 3.0 * t *t *(1.0 -t) *p2
+ t *t *t *p3;
}
该方程可以利用 Shader 中自带的函数优化一波:
vec2 fun2(in vec2 p0, in vec2 p1, in vec2 p2, in vec2 p3, in float t)
{
vec2 q0 = mix(p0, p1, t);
vec2 q1 = mix(p1, p2, t);
vec2 q2 = mix(p2, p3, t);
vec2 r0 = mix(q0, q1, t);
vec2 r1 = mix(q1, q2, t);
return mix(r0, r1, t);
}
接下来就是具体的顶点着色器 shader :
// 对应 t 数据的传递
attribute float aData;
// 对应起始点和结束点
uniform vec4 uStartEndData;
// 对应控制点
uniform vec4 uControlData;
// mvp 矩阵
uniform mat4 u_MVPMatrix;
void main() {
vec4 pos;
pos.w = 1.0;
// 取出起始点、结束点、控制点
vec2 p0 = uStartEndData.xy;
vec2 p3 = uStartEndData.zw;
vec2 p1 = uControlData.xy;
vec2 p2 = uControlData.zw;
// 取出 t 的值
float t = aData;
// 计算贝塞尔点的函数调用
vec2 point = fun2(p0, p1, p2, p3, t);
// 定义点的 x,y 坐标
pos.xy = point;
// 要绘制的位置
gl_Position = u_MVPMatrix * pos;
// 定义点的尺寸大小
gl_PointSize = 20.0;
}
代码中的 uStartEndData 对应起始点和结束点,uControlData 对应两个控制点。
这两个变量的数据传递通过 glUniform4f 方法就好了:
mStartEndHandle = glGetUniformLocation(mProgram, "uStartEndData");
mControlHandle = glGetUniformLocation(mProgram, "uControlData");
// 传递数据,作为固定值
glUniform4f(mStartEndHandle,
mStartEndPoints[0],
mStartEndPoints[1],
mStartEndPoints[2],
mStartEndPoints[3]);
glUniform4f(mControlHandle,
mControlPoints[0],
mControlPoints[1],
mControlPoints[2],
mControlPoints[3]);
另外重要的变量就是 aData 了,它对应的就是 t 在 0~1 闭区间的划分的数量。
private float[] genTData() {
float[] tData = new float[Const.NUM_POINTS];
for (int i = 0; i < tData.length; i ++) {
float t = (float) i / (float) tData.length;
tData[i] = t;
}
return tData;
}
以上函数就是把 t 在 0~1 闭区间分成 Const.NUM_POINTS 份,每一份的值都存在 tData 数组中,最后通过 glVertexAttribPointer 函数传递给 Shader 。
最后实际绘制时,我们采用 GL_POINTS 的形式绘制就好了。
GLES20.glDrawArrays(GLES20.GL_POINTS, 0, Const.NUM_POINTS );
以上就是 OpenGL 绘制贝塞尔曲线的小实践。
具体的代码部分可以参考我的项目:
在参考中,也有一个 OpenGL 绘制贝塞尔曲线的例子,不过他绘制的是贝塞尔曲线面,采用的是 GL_TRIANGLES 的形式,而且在 tData 数组的构造也有些不同,但是都大同小异了,看明白了本文的例子也不难理解参考的文章。
关于 OpenGL 相关的文章,可以参考我之前写过的公众号内容:
欢迎关注微信公众号,及时推送更多图形、图像、多媒体相关文章~~
参考
扫码关注,阅读更多精彩~~~

OpenGL 实践之贝塞尔曲线绘制的更多相关文章
- n阶贝塞尔曲线绘制(C/C#)
原文:n阶贝塞尔曲线绘制(C/C#) 贝塞尔是很经典的东西,轮子应该有很多的.求n阶贝塞尔曲线用到了 德卡斯特里奥算法(De Casteljau's Algorithm) 需要拷贝代码请直接使用本文最 ...
- iOS 使用贝塞尔曲线绘制路径
使用贝塞尔曲线绘制路径 大多数时候,我们在开发中使用的控件的边框是矩形,或者做一点圆角,是使得矩形的角看起来更加的圆滑. 但是如果我们想要一个不规则的图形怎么办?有人说,叫UI妹子做,不仅省事,还可以 ...
- 基于canvas二次贝塞尔曲线绘制鲜花
canvas中二次贝塞尔曲线参数说明: cp1x:控制点1横坐标 cp1y:控制点1纵坐标 x: 结束点1横坐标 y:结束点1纵坐标 cp2x:控制点2横坐标 cp2y:控制点2纵坐标 z:结束点2横 ...
- JavaScript+canvas 利用贝塞尔曲线绘制曲线
效果图: <body> <canvas id="test" width="800" height="300">< ...
- iOS:使用贝塞尔曲线绘制图表(折线图、柱状图、饼状图)
1.介绍: UIBezierPath :画贝塞尔曲线的path类 UIBezierPath定义 : 贝赛尔曲线的每一个顶点都有两个控制点,用于控制在该顶点两侧的曲线的弧度. 曲线的定义有四个点:起始点 ...
- 利用贝塞尔曲线绘制(UIBezierPath)自定义iOS动态速度表,可以自定义刻度,刻度值,进度条样式
GitHub的Demo下载地址 使用UIBezierPath画图步骤: 创建一个UIBezierPath对象 调用-moveToPoint:设置初始线段的起点 添加线或者曲线去定义一个或者多个子路径 ...
- Android中贝塞尔曲线的绘制方法
贝塞尔曲线,很多人可能不太了解,什么叫做贝塞尔曲线呢?这里先做一下简单介绍:贝塞尔曲线也可以叫做贝济埃曲线或者贝兹曲线,它由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋.一般的矢量图形软件常 ...
- Android 贝塞尔曲线解析
相信很多同学都知道"贝塞尔曲线"这个词,我们在很多地方都能经常看到.利用"贝塞尔曲线"可以做出很多好看的UI效果,本篇博客就让我们一起学习"贝塞尔曲线 ...
- OpenGL超级宝典笔记——贝塞尔曲线和曲面(转)
http://my.oschina.net/sweetdark/blog/183721 参数方程表现形式 在中学的时候,我们都学习过直线的参数方程:y = kx + b;其中k表示斜率,b表示截距(即 ...
随机推荐
- 奇点云 x 阿里云 | 联合发布综合体数字化转型与数据创新解决方案
2019年7月25日下午,在阿里云峰会上海站,奇点云入选阿里云首批联合解决方案合作伙伴,并联合发布了“综合体数字化转型与数据创新解决方案”,共同探索综合体的智能服务. 关于综合体的数字化转型,奇点云联 ...
- REVIT 卸载工具,完美彻底卸载清除干净revit各种残留注册表和文件
一些同学安装revit出错了,也有时候想重新安装revit的时候会出现这种本电脑windows系统已安装revit,你要是不留意直接安装,只会安装revit的附件,revit是不会安装上的.这种原因呢 ...
- MongoDB的图形化连接工具MongoDB VUE
MongoDB的图形化连接工具MongoDB VUE 类似mysql的navicat.
- WEB前端资源集(二)
在上一篇为大家整理出了一些资源网站,接下来给大家整理了一些开发中常用的工具. 开发工具篇 开发工具集 Sublime Text 3:SublimeText 3是一个代码编辑器,也是HTML和散文先进的 ...
- Hexo博客部署
前些天使用wordpress程序搭建了个人网站,但感觉太重比较适合个人空间,所以这次介绍Hexo搭建免费博客,先提供官网给大家英文版的请点击这里,中文版的请点击这里,在安装一个Git,再是github ...
- 分布式文件系统与HDFS
HDFS,它是一个虚拟文件系统,用于存储文件,通过目录树来定位文件:其次,它是分布式的,由很多服务器联合起来实现其功能,集群中的服务器有各自的角色. HDFS 的设计适合一次写入,多次读出的场景,且不 ...
- 转:zabbix 2.4.4 更换 logo
zabbix 2.4.4 更换 logo 想把 zabbix 的 logo 改为自己公司的. 把更改过程做一下记录 先找到修改文件的目录 zabbix 版本 2.4.4 操作系统 linux 目录定位 ...
- python语法生成器、迭代器、闭包、装饰器总结
1.生成器 生成器的创建方法: (1)通过列表生成式创建 可以通过将列表生成式的[]改成() eg: # 列表生成式 L = [ x*2 for x in range(5)] # L = [0, 2, ...
- 高阶函数---swift中的泛型介绍(一步步实现Map函数)
说明 本文内容均出自函数式 Swift一书, 此处整理仅仅是为了自己日后方便查看, 需要深入研究的话, 可以点进去购买, 支持原作者 本书由 王巍–新浪微博大神翻译 OneV's Den 喵神博客 接 ...
- 极验验证码破解之selenium
这一篇写完很久了,因为识别率一直很低,没办法拿出来见大家,所以一直隐藏着,今天终于可以拿出来见见阳光了. 哈喽,大家好,我是星星在线,我又来了,今天给大家带来的是极验验证码的selenium破解之法, ...