当前位置: 首页 > news >正文

ios 平滑移动view_在 iOS 中使用 OpenGL ES 实现绘画板

0f7bc514e8d50a31f9c31e7156b9d172.png

作者:lyman来源:http://www.lymanli.com/2020/01/04/ios-opengles-paint/

今天我们使用 OpenGL ES 来实现一个绘画板,主要介绍在 OpenGL ES 中绘制平滑曲线的实现方案。

首先看一下最终效果:

6cf278cd1b1ed60ab21720c270953309.gif

在 iOS 中,有很多种方式可以实现一个绘画板,比如我的另外一个项目 MFPaintView 就是基于 CoreGraphics 实现的。

然而,使用 OpenGL ES 来实现可以获得更多的灵活性,比如我们可以自定义笔触的形状,这是其他实现方式做不到的。

我们知道,OpenGL ES 中只有 点、直线、三角形 这三种图元。因此,怎么在 OpenGL ES 中绘制曲线,是我们第一个要解决的问题,也是最复杂的问题。

我们会使用比较大的篇幅来讲解这个问题。至于绘画板的其他功能实现,并不是说不重要,只是说其他的绘画板实现方式,也会有类似的逻辑,所以这部分会放在最后再简单介绍一下。

一、怎么绘制曲线

在 OpenGL ES 中绘制曲线的方式,就是 将曲线拆分成点序列来绘制

因为要绘制点,所以我们采取的是 点图元 。即我们要把顶点数据当成 来绘制,并且每个点都要绘制出笔触的纹理。关键步骤如下:

指定图元类型:

1glDrawArrays(GL_POINTS, 0, self.vertexCount);2复制代码

顶点着色器:

1attribute vec4 Position;23uniform float Size;45void main (void) {6    gl_Position = Position;7    gl_PointSize = Size;8}9复制代码

片段着色器:

 1precision highp float; 2 3uniform float R; 4uniform float G; 5uniform float B; 6uniform float A; 7 8uniform sampler2D Texture; 910void main (void) {11    vec4 mask = texture2D(Texture, vec2(gl_PointCoord.x, 1.0 - gl_PointCoord.y));12    gl_FragColor = A * vec4(R, G, B, 1.0) * mask;13}1415复制代码

这里的关键点在于 gl_PointCoord 这个内置变量,当我们使用点图元的时候,可以通过这个变量获取到 当前像素在点图元中的归一化坐标

但是这个坐标的原点是在左上角,这和纹理坐标在竖直方向上是相反的。所以从纹理读取颜色的时候,要做一个 y 坐标的转换。

接下来,我们通过 UITouch 来获取触摸点的位置,然后算出归一化的顶点坐标。

1- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {2    [super touchesMoved:touches withEvent:event];34    [self addPointWithTouches:touches];5}6复制代码

但是由于 iOS 系统触摸事件的派发频率有限,我们最终得到的只能是稀疏的点。如下图所示,每个触摸点之间的间隔会比较大。

1011ed02b1407a13ebb1d44cb5abfeaa.png

img

二、怎么绘制密集的点

很容易想到,只需要在两个点之间,按照一定的密度进行插值,就可以绘制出连续的轨迹。

a7c8fa8a8a5e59a5e482f65598f72469.png

img

但是很明显,我们的绘制结果是折线,并不平滑。

三、怎么使曲线变平滑

解决点连接不平滑的问题,一般是使用贝塞尔曲线。这种方案在 MFPaintView 中也得到了很好的应用。

具体的做法是使用 两个顶点间的中点一个顶点 ,来构造一条贝塞尔曲线。如下图,图中的 3 个 红点 被用来构造一条贝塞尔曲线。

672e563c98e6dd7a0589d5785c2a4e0d.png

img

于是,我们的问题就变成了 怎么在 OpenGL ES 中绘制贝塞尔曲线 。相当于已知贝塞尔曲线的 3 个关键点,反向来求曲线上的点序列。

我们知道贝塞尔曲线的方程是 P = (1 - t)^2 * P0 + 2 * t * (1 - t) * P1 + t^2 * P2, t 是唯一的变量,其取值范围是 0 ~ 1 。

所以我们可以采取线性取值的方式,每一条贝塞尔曲线取 n 个点(n 是个确定的常量)。只要依次往方程中代入 1 / n 、 2 / n 、 ... n / n ,就可以得到一个点序列。

fd5f4ac0539d4b485367b84aeea93d1e.png

img

先将 n 取一个比较小的值,这样比较容易看出存在的问题。我们发现,点序列的间隔并不均匀。原因有两个:

  1. 不同贝塞尔曲线的长度不一样,使用同一个 n 值,算出来的点的疏密程度肯定不同。
  2. 由于贝塞尔曲线随着 t 增长,曲线长度的增长并不是线性的。按照我们上面的算法,最终会得到的结果是 两头比较稀疏,中间比较密集

四、怎么生成均匀的点序列

贝塞尔曲线生成均匀的点序列,涉及到了一个经典的「贝塞尔曲线匀速运动」问题。

这个问题的推导和计算比较复杂。如果你有兴趣,可以阅读一下文末的两篇文章。由于我还不能完全领悟,就不在这里误导大家了。

简单来说,就是我们通过一系列的骚操作,封装了一个方法,只需要传入贝塞尔曲线的 3 个关键点和笔触尺寸,就可以获取均匀的点序列。

1+ (NSArray *)pointsWithFrom:(CGPoint)from2                                    to:(CGPoint)to3                               control:(CGPoint)control4                             pointSize:(CGFloat)pointSize;5复制代码

下面我们固定贝塞尔曲线的 起始点控制点,只移动 终止点,来验证一下这个方法是否可靠。

cdfb5970835386c2767ba0b07253479a.gif

img

可以看到,在移动过程中,点和点的距离基本是保持一致的,并且是均匀的。通过这个「神奇」的方法,我们终于画出了平滑且均匀的曲线。

360abcf7c936a46377c83937defd2444.png

img

五、绘画板功能实现

终于讲完了最麻烦的部分,接下来简单介绍一下绘画板基本功能的实现。

1、颜色混合

在以往的例子中,我们在开始一次渲染之前,都会调用 glClear(GL_COLOR_BUFFER_BIT) 来清除画布,因为我们不希望保留上次的渲染结果。

但是对于一个绘画板来说,我们要不断地往画布上画东西,所以是希望保留上次结果的。因此,在绘制之前不能执行清除的操作。

另外,由于我们的画笔可能是半透明的,所以新绘制的颜色需要和画布上已经存在的颜色进行混合。因此在绘制开始之前,需要开启混合选项。

1glEnable(GL_BLEND);2glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);3复制代码

2、笔触调整

笔触有 3 个属性可以调整:颜色、尺寸、形状。它们本质上都是对点图元的调整,通过 uniform 变量的形式,将颜色、尺寸、纹理传入着色器并应用。

3、橡皮擦

GLPaintView 在初始化的时候,需要传入一个背景色参数,当用户切换到橡皮擦功能的时候,内部只是单纯地将画笔的颜色切换成背景色,于是就产生了橡皮擦的效果。

4、撤销重做

撤销重做功能需要依赖两个栈来实现。我们把用户的手指从 按下屏幕到离开屏幕 这一过程中产生的数据,定义为一个操作对象,这个操作对象保存了归一化后的点序列,以及点的属性。

 1@interface MFPaintModel : NSObject 2 3/// 笔刷尺寸 4@property (nonatomic, assign) CGFloat brushSize; 5/// 笔刷颜色 6@property (nonatomic, strong) UIColor *brushColor; 7/// 笔刷模式 8@property (nonatomic, assign) GLPaintViewBrushMode brushMode; 9/// 笔触纹理图片文件名10@property (nonatomic, copy) NSString *brushImageName;11/// 点序列12@property (nonatomic, copy) NSArray *points;1314@end15复制代码

撤销重做的代码实现大概像这样子:

 1- (void)undo { 2    if ([self.operationStack isEmpty]) { 3        return; 4    } 5    MFPaintModel *model = self.operationStack.topModel; 6    [self.operationStack popModel]; 7    [self.undoOperationStack pushModel:model]; 8 9    [self reDraw];10}1112- (void)redo {13    if ([self.undoOperationStack isEmpty]) {14        return;15    }16    MFPaintModel *model = self.undoOperationStack.topModel;17    [self.undoOperationStack popModel];18    [self.operationStack pushModel:model];1920    [self drawModel:model];21}22复制代码

需要注意的是,由于 撤销操作 需要先清除画布,所以每次都需要重绘。而 重做操作 可以利用上次绘制的结果,所以每次只需要绘制一个步骤即可。

源码

请到 GitHub 上查看完整代码。

https://github.com/lmf12/GLPaintView

7b8dbb8e1c51b8d0a338618869298110.png

相关文章:

  • 怎么把照片上传到画板_摄影技巧:全黑背景的照片怎么出?怎么拍出高大上的照片...
  • 八皇后时间复杂度 回溯_LeetCode--回溯法心得
  • 运维平台_舜通云-智能光伏运维平台
  • 查询子串_Entity Framework Core Like 查询揭秘
  • 开关电源中的磁性元件书籍_超详细!开关电源电路方案选择指南!
  • 单位和流明_流明 | 你值得这世间所有美好
  • 什么是多态python_Python的多态是什么
  • python谁发明的1003python谁发明的_PAT乙级1003-Python
  • python用户输入10个_2019-07-18 python练习:编写一个程序,要求用户输入10个整数,然后输出其中最大的奇数,如果用户没有输入奇数,则输出一个消息进行说明。...
  • python能为我们做什么读后感作文_《与运气竞争》读书笔记 坚韧不拔|静水流深|读书|写作|博雅|数据分析|Python|商业|独立·独特·自立门户 kebook...
  • 三因素方差分析_菜鸟也爱数据分析之SPSS篇——多因素方差分析
  • aimesh r6400 开_适合家用的路由器有哪些?
  • 调用赋码远程服务异常_用REST方式访问wcf服务,post时老报“远程服务器返回异常: (400) 异常的请求”...
  • mysql交互操作过程中使用的语言是_使用mySQL与数据库进行交互(一)
  • mysql 从如何重新同步_如何重置(重新同步)MySQL主从复制
  • ----------
  • [PHP内核探索]PHP中的哈希表
  • 「译」Node.js Streams 基础
  • 【每日笔记】【Go学习笔记】2019-01-10 codis proxy处理流程
  • 【跃迁之路】【477天】刻意练习系列236(2018.05.28)
  • CSS盒模型深入
  • ES学习笔记(12)--Symbol
  • Git学习与使用心得(1)—— 初始化
  • Making An Indicator With Pure CSS
  • MYSQL 的 IF 函数
  • PHP 程序员也能做的 Java 开发 30分钟使用 netty 轻松打造一个高性能 websocket 服务...
  • Spark学习笔记之相关记录
  • Zepto.js源码学习之二
  • 官方新出的 Kotlin 扩展库 KTX,到底帮你干了什么?
  • 使用Tinker来调试Laravel应用程序的数据以及使用Tinker一些总结
  • #define,static,const,三种常量的区别
  • #stm32驱动外设模块总结w5500模块
  • #预处理和函数的对比以及条件编译
  • #中国IT界的第一本漂流日记 传递IT正能量# 【分享得“IT漂友”勋章】
  • (1)安装hadoop之虚拟机准备(配置IP与主机名)
  • (分布式缓存)Redis哨兵
  • (删)Java线程同步实现一:synchronzied和wait()/notify()
  • (四) Graphivz 颜色选择
  • (转)Linq学习笔记
  • (转)项目管理杂谈-我所期望的新人
  • (转载)虚幻引擎3--【UnrealScript教程】章节一:20.location和rotation
  • (轉)JSON.stringify 语法实例讲解
  • *Algs4-1.5.25随机网格的倍率测试-(未读懂题)
  • .bat批处理(二):%0 %1——给批处理脚本传递参数
  • .Net Winform开发笔记(一)
  • .net 开发怎么实现前后端分离_前后端分离:分离式开发和一体式发布
  • .NET/ASP.NETMVC 深入剖析 Model元数据、HtmlHelper、自定义模板、模板的装饰者模式(二)...
  • .Net+SQL Server企业应用性能优化笔记4——精确查找瓶颈
  • .Net开发笔记(二十)创建一个需要授权的第三方组件
  • .NET使用存储过程实现对数据库的增删改查
  • @RequestBody详解:用于获取请求体中的Json格式参数
  • @requestBody写与不写的情况
  • [ CTF ] WriteUp- 2022年第三届“网鼎杯”网络安全大赛(朱雀组)
  • [ACTF2020 新生赛]Upload 1
  • [ASP.NET MVC]如何定制Numeric属性/字段验证消息