OpenGL 实践之贝塞尔曲线绘制

简介: 说到贝塞尔曲线,大家肯定都不陌生,网上有很多关于介绍和理解贝塞尔曲线的优秀文章和动态图。

说到贝塞尔曲线,大家肯定都不陌生,网上有很多关于介绍和理解贝塞尔曲线的优秀文章和动态图。

以下两个是比较经典的动图了。

二阶贝塞尔曲线:

640.gif

三阶贝塞尔曲线:

640 (1).gif

由于在工作中经常要和贝塞尔曲线打交道,所以简单说一下自己的理解:

现在假设我们要在坐标系中绘制一条直线,直线的方程很简单,就是 y=x ,很容易得到下图:

image.png

现在我们限制一下 x 的取值范围为 0~1 的闭区间,那么可以得出 y 的取值范围也是 0~1。

而在 0~1 的区间范围内,x 能取的数有多少个呢?答案当然是无数个了。

image.png

同理,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 是控制点。

image.png

image.png

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

image.png
image.png

不难理解,假设我们要绘制一条曲线,肯定要有起始和结束点来指定曲线的范围曲线。

而控制点就是指定该曲线的弧度,或者说指定该曲线的弯曲走向,不同的控制点得出的曲线绘制结果是不一样的。

另外,可以观察到,无论是几阶贝塞尔曲线,都会有参数 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 方法就可以完成绘制。

1  // 构建 path 路径,也就是选取
 2  path.reset();
 3  path.moveTo(p0x, p0y);
 4  // 绘制二阶贝塞尔曲线
 5  path.quadTo(p1x, p1y, p2x, p2y);
 6  path.moveTo(p0x, p0y);
 7  path.close();
 8
 9  // 最后的绘制操作
10  canvas.drawPath(path, paint);

这里的绘制实际上就是把贝塞尔曲线计算的方程式交给了 Android 系统内部去完成了,参数传递上只传递了起始点和控制点。

我们可以通过自己的代码来计算这个方程式从而对逻辑上获得更多控制权,也就是把曲线拆分成许多个点组成,如果点的尺寸比较大,甚至可以减少点的个数实现同样的效果,达到绘制优化的目的。

OpenGL 绘制

通过 OpenGL 可以实现我们上述的方案,把曲线拆分成多个点组成。这种方案要求我们在 CPU 上去计算贝塞尔曲线方程,根据 t 的每一个取值,计算出一个贝塞尔点,用 OpenGL 去绘制上这个点。

这个点的绘制可以采用 OpenGL 中画三角形 GL_TRIANGLES 的形式去绘制,这样就可以给点带上纹理效果,不过这里面的坑略多,起始点和控制点都是运行时动态可变的实现难度会大于固定不变的。

这里先介绍另一种方案,这种方案实现比较简单也能达到优化效果,我们可以把贝塞尔曲线的计算方程式交给 GPU, 在 OpenGL Shader 中去完成。

这样一来,我们只要给定起始点和控制点,中间计算贝塞尔曲线去填补点的过程就交给 Shader 去完成了。

另外,通过控制 t 的数量,我们可以控制贝塞尔点填补的疏密。

t 越大,填补的点越多,超过一定阈值后,不会对绘制效果有提升,反而影响性能。

t 越小,那么贝塞尔曲线就退化成一串点组成了。所以说 t 的取值范围也能对绘制起到优化作用。

绘制效果如下图所示:

640 (2).gif

以下就是实际的代码部分了,关于 OpenGL 的基础理论部分可以参考之前写过的文章和公众号,就不再阐述了。

在 Shader 中定义一个函数,实现贝塞尔方程:

1vec2 fun(in vec2 p0, in vec2 p1, in vec2 p2, in vec2 p3, in float t){
2    float tt = (1.0 - t) * (1.0 -t);
3    return tt * (1.0 -t) *p0 
4            + 3.0 * t * tt * p1 
5            + 3.0 * t *t *(1.0 -t) *p2 
6            + t *t *t *p3;
7}

该方程可以利用 Shader 中自带的函数优化一波:

1vec2 fun2(in vec2 p0, in vec2 p1, in vec2 p2, in vec2 p3, in float t)
2{
3    vec2 q0 = mix(p0, p1, t);
4    vec2 q1 = mix(p1, p2, t);
5    vec2 q2 = mix(p2, p3, t);
6    vec2 r0 = mix(q0, q1, t);
7    vec2 r1 = mix(q1, q2, t);
8    return mix(r0, r1, t);
9}

接下来就是具体的顶点着色器 shader :

1// 对应 t 数据的传递
 2attribute float aData;
 3// 对应起始点和结束点
 4uniform vec4 uStartEndData;
 5// 对应控制点
 6uniform vec4 uControlData;
 7// mvp 矩阵
 8uniform mat4 u_MVPMatrix;
 9
10void main() {
11    vec4 pos;
12    pos.w = 1.0;
13    // 取出起始点、结束点、控制点
14    vec2 p0 = uStartEndData.xy;
15    vec2 p3 = uStartEndData.zw;
16    vec2 p1 = uControlData.xy;
17    vec2 p2 = uControlData.zw;
18    // 取出 t 的值
19    float t = aData;
20    // 计算贝塞尔点的函数调用
21    vec2 point = fun2(p0, p1, p2, p3, t);
22    // 定义点的 x,y 坐标
23    pos.xy = point;
24    // 要绘制的位置
25    gl_Position = u_MVPMatrix * pos;
26    // 定义点的尺寸大小
27    gl_PointSize = 20.0;
28}

代码中的 uStartEndData 对应起始点和结束点,uControlData 对应两个控制点。

这两个变量的数据传递通过 glUniform4f 方法就好了:

1    mStartEndHandle = glGetUniformLocation(mProgram, "uStartEndData");
 2    mControlHandle = glGetUniformLocation(mProgram, "uControlData");
 3    // 传递数据,作为固定值
 4    glUniform4f(mStartEndHandle,
 5            mStartEndPoints[0],
 6            mStartEndPoints[1],
 7            mStartEndPoints[2],
 8            mStartEndPoints[3]);
 9    glUniform4f(mControlHandle,
10            mControlPoints[0],
11            mControlPoints[1],
12            mControlPoints[2],
13            mControlPoints[3]);

另外重要的变量就是 aData 了,它对应的就是 t 在 0~1 闭区间的划分的数量。

1    private float[] genTData() {
2        float[] tData = new float[Const.NUM_POINTS];
3        for (int i = 0; i < tData.length; i ++) {
4            float t = (float) i / (float) tData.length;
5            tData[i] = t;
6        }
7        return tData;
8    }

以上函数就是把 t 在 0~1 闭区间分成 Const.NUM_POINTS 份,每一份的值都存在 tData 数组中,最后通过 glVertexAttribPointer 函数传递给 Shader 。

最后实际绘制时,我们采用 GL_POINTS 的形式绘制就好了。

1        GLES20.glDrawArrays(GLES20.GL_POINTS, 0, Const.NUM_POINTS );

以上就是 OpenGL 绘制贝塞尔曲线的小实践。

具体的代码部分可以参考我的项目:

https://github.com/glumes/AndroidOpenGLTutorial

在参考中,也有一个 OpenGL 绘制贝塞尔曲线的例子,不过他绘制的是贝塞尔曲线面,采用的是 GL_TRIANGLES 的形式,而且在 tData 数组的构造也有些不同,但是都大同小异了,看明白了本文的例子也不难理解参考的文章。

OpenGL 系列文章

「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。

阿里云社区.png

相关文章
|
SQL Oracle 关系型数据库
FastAPI数据库系列(一) MySQL数据库操作 一、简介
FastAPI中你可以使用任何关系型数据库,可以通过SQLAlchemy将其轻松的适应于任何的数据库,比如: PostgreSQL MySQL SQLite Oracle Microsoft SQL Server ...
|
10月前
|
API Apache
OPIK:一个开源的自动提示词优化框架
本文介绍如何用OPIK的MetaPromptOptimizer实现自动提示词优化,通过几轮迭代将大模型在复杂推理任务上的准确率从34%提升至97%。详解环境搭建、代码实现及优缺点,展示如何让LLM自我改进提示词,大幅提升效率与性能,推动提示工程迈向自动化。
1128 2
|
机器学习/深度学习 数据可视化 数据挖掘
香烟品牌识别和规格识别设计思路
基于YOLOv8实现香烟品牌与规格(条装/单盒装)识别,采用“品牌+规格”组合为60类的复合类别方案,结合充足标注数据(每类300-500张)、数据增强与反例优化,进行端到端联合训练,提升模型在复杂场景下的检测与分类精度。
|
数据采集 人工智能 自然语言处理
Riona-AI-Agent:自媒体 AI 代理!自动点赞、评论、个性化内容生成和发布等交互任务
Riona-AI-Agent 是一款基于 Node.js 和 TypeScript 的 AI 自动化工具,支持 Instagram、Twitter 等平台的自动化交互,生成高质量内容,提升社交媒体管理效率。
1925 13
Riona-AI-Agent:自媒体 AI 代理!自动点赞、评论、个性化内容生成和发布等交互任务
|
人工智能 Java API
MCP客户端调用看这一篇就够了(Java版)
本文详细介绍了MCP(Model Context Protocol)客户端的开发方法,包括在没有MCP时的痛点、MCP的作用以及如何通过Spring-AI框架和原生SDK调用MCP服务。文章首先分析了MCP协议的必要性,接着分别讲解了Spring-AI框架和自研SDK的使用方式,涵盖配置LLM接口、工具注入、动态封装工具等步骤,并提供了代码示例。此外,还记录了开发过程中遇到的问题及解决办法,如版本冲突、服务连接超时等。最后,文章探讨了框架与原生SDK的选择,认为框架适合快速构建应用,而原生SDK更适合平台级开发,强调了两者结合使用的价值。
15079 33
MCP客户端调用看这一篇就够了(Java版)
|
安全 Linux 开发者
⭐⭐⭐⭐⭐Linux C/C++ 进程崩溃诊断以及有效数据收集:解锁代码问题快速定位与修复的方法
⭐⭐⭐⭐⭐Linux C/C++ 进程崩溃诊断以及有效数据收集:解锁代码问题快速定位与修复的方法
1436 1
|
存储 SQL 数据库
虚拟化数据恢复—Vmware虚拟机误还原快照的数据恢复案例
虚拟化数据恢复环境: 一台虚拟机从物理机迁移到ESXI虚拟化平台,迁移完成后做了一个快照。虚拟机上运行了一个SQL Server数据库,记录了数年的数据。 ESXI虚拟化平台上有数十台虚拟机,EXSI虚拟化平台连接了一台EVA存储,所有的虚拟机都存放在EVA存储上。 虚拟化故障: 工组人员误操作将数年前迁移完成后做的快照还原了,也就意味着虚拟机状态还原到数年前,近几年数据都被删除了。 还原快照相当于删除数据,意味着部分存储空间会被释放。为了不让这部分释放的空间被重用,需要将连接到这台存储的所有虚拟机都关掉,需要将不能长时间宕机的虚拟机迁移到别的EXSI虚拟化平台上。
714 50
|
监控 安全 API
拥抱开源:下一代API管理工具Kong的崛起
【10月更文挑战第27天】在微服务架构和API经济的推动下,API管理成为软件开发的关键环节。Kong作为开源的API管理平台,凭借其灵活性和强大功能,受到开发者的青睐。本文探讨了Kong的核心特性、使用技巧及其在企业中的应用,帮助读者更好地理解和利用这一工具。

热门文章

最新文章