在iOS中使用OpenGL ES实现绘画板的方法

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

首先看一下最终效果:

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

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

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

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

一、怎么绘制曲线

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

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

指定图元类型:

glDrawArrays(GL_POINTS, 0, self.vertexCount);

顶点着色器:

attribute vec4 Position;

uniform float Size;

void main (void) {
  gl_Position = Position;
  gl_PointSize = Size;
}

片段着色器:

precision highp float;

uniform float R;
uniform float G;
uniform float B;
uniform float A;

uniform sampler2D Texture;

void main (void) {
  vec4 mask = texture2D(Texture, vec2(gl_PointCoord.x, 1.0 - gl_PointCoord.y));
  gl_FragColor = A * vec4(R, G, B, 1.0) * mask;
}

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

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

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

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
  [super touchesMoved:touches withEvent:event];

  [self addPointWithTouches:touches];
}

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

二、怎么绘制密集的点

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

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

三、怎么使曲线变平滑

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

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

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

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

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

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

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

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

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

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

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

+ (NSArray <NSValue *>*)pointsWithFrom:(CGPoint)from
                  to:(CGPoint)to
                control:(CGPoint)control
               pointSize:(CGFloat)pointSize;

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

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

五、绘画板功能实现

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

1、颜色混合

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

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

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

glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

2、笔触调整

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

3、橡皮擦

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

4、撤销重做

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

@interface MFPaintModel : NSObject

/// 笔刷尺寸
@property (nonatomic, assign) CGFloat brushSize;
/// 笔刷颜色
@property (nonatomic, strong) UIColor *brushColor;
/// 笔刷模式
@property (nonatomic, assign) GLPaintViewBrushMode brushMode;
/// 笔触纹理图片文件名
@property (nonatomic, copy) NSString *brushImageName;
/// 点序列
@property (nonatomic, copy) NSArray<NSValue *> *points;

@end

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

- (void)undo {
  if ([self.operationStack isEmpty]) {
    return;
  }
  MFPaintModel *model = self.operationStack.topModel;
  [self.operationStack popModel];
  [self.undoOperationStack pushModel:model];

  [self reDraw];
}

- (void)redo {
  if ([self.undoOperationStack isEmpty]) {
    return;
  }
  MFPaintModel *model = self.undoOperationStack.topModel;
  [self.undoOperationStack popModel];
  [self.operationStack pushModel:model];

  [self drawModel:model];
}

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

源码

请到 GitHub上查看完整代码。

到此这篇关于在iOS中使用OpenGL ES实现绘画板的方法的文章就介绍到这了,更多相关iOS 绘画板内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 分享一个iOS下实现基本绘画板功能的简单方法

    代码部分 TouchView.h 复制代码 代码如下: #import <UIKit/UIKit.h>    @interface TouchView : UIView  {      NSMutableArray *points;      NSArray *points_all;      CGContextRef context;      UIColor *paint_clr;  }  @property (strong,nonatomic) NSMutableArray *point

  • iOS简单画板开发案例分享

    最近在学习Quartz2D,学习了一个简单画板的实现,现在把实现过程记录一下. 主要用到的点就是画线,截屏,绘制图片,选择图片,以及保存所有绘制的线. 首先在storyboard上布局好控件,设置约束等等,最后的效果是这样: 自定义画板DrawView,使用时可能是从xib中加载,也可能是手动创建,所以创建对象的方法需要实现两个: #import <UIKit/UIKit.h> @interface DrawView : UIView /** 线宽 */ @property (nonatomi

  • iOS给图片添加滤镜&使用openGLES动态渲染图片详解及实例

    iOS给图片添加滤镜&使用openGLES动态渲染图片 给图片增加滤镜有这两种方式: CoreImage / openGLES 下面先说明如何使用CoreImage给图片添加滤镜, 主要为以下步骤: #1.导入CIImage格式的原始图片 #2.创建CIFilter滤镜 #3.用CIContext将滤镜中的图片渲染出来 #4.导出渲染后的图片 参考代码: //导入CIImage CIImage *ciImage = [[CIImage alloc] initWithImage:[UIImage

  • IOS实现的简单画板功能

    效果图 设计要求 1.画笔能设置大小.颜色 2.有清屏.撤销.橡皮擦.导入照片功能 3.能将绘好的画面保存到相册 实现思路 1.画笔的实现,我们可以通过监听用户的 平移手势 中创建 UIBezierPath 来实现线条的绘制 2.撤销功能,我们先来看下撤销功能,我们会想到用一个数组队列将用户的每一次的笔画都加入到数组中,然后撤销的时候只需要将最后添加进去的笔画pop掉,重新绘制就可以了 3.清屏功能就简单了,只需要将上面说到的那个数组清空重新绘制下就可以了 4.导入照片功能,可以用系统的 UII

  • 在iOS中使用OpenGL ES实现绘画板的方法

    今天我们使用 OpenGL ES 来实现一个绘画板,主要介绍在 OpenGL ES 中绘制平滑曲线的实现方案. 首先看一下最终效果: 在 iOS 中,有很多种方式可以实现一个绘画板,比如我的另外一个项目 MFPaintView 就是基于 CoreGraphics 实现的. 然而,使用 OpenGL ES 来实现可以获得更多的灵活性,比如我们可以自定义笔触的形状,这是其他实现方式做不到的. 我们知道,OpenGL ES 中只有 点.直线.三角形 这三种图元.因此, 怎么在 OpenGL ES 中绘

  • IOS中Json解析实例方法详解(四种方法)

    作为一种轻量级的数据交换格式,json正在逐步取代xml,成为网络数据的通用格式. 有的json代码格式比较混乱,可以使用此"http://www.bejson.com/"网站来进行JSON格式化校验(点击打开链接).此网站不仅可以检测Json代码中的错误,而且可以以视图形式显示json中的数据内容,很是方便. 从IOS5开始,APPLE提供了对json的原生支持(NSJSONSerialization),但是为了兼容以前的iOS版本,可以使用第三方库来解析Json. 本文将介绍Tou

  • iOS中从网络获取数据的几种方法的比较

    IOS中获取网络数据一般有三种:1.NSURLCondition(已过时) 2.NSURLSession  3.三方库AFNetWorking NSURLSession 是苹果对NSULRCondition的替代品,NSURLSession比NSURLCondition多了 1.可配置的数据信息NSURLSessionConfiguration,NSURLSessionConfiguration使你可以设置你要请求的数据,通常的设置如缓存,也可以使用默认的配置信息defaultCongurati

  • iOS中的地理位置的获取及plist设置方法

    1.在前台的时候获取地理位置信息 ios 8/9 在info.plist中配置NSLocationWhenInUseUsageDescription的值,否则上面的方法无效 调用.requestWhenInUseAuthorization()获取前台获取地理位置权限 调用.startUpdatingLocation() 代码示例 class ViewController: UIViewController { lazy var locateM : CLLocationManager = { le

  • iOS中Label实现显示不同颜色与字体的方法

    前言 iOS中Label是我们经常遇到的一个控件,我们大家应该都会简单的使用它,像下面这个代码,就能简单的创建一个label // 1.创建 CGRectrect =CGRectMake(100,100,100,100); UILabel* label = [[UILabelalloc]initWithFrame:rect]; 引言 然而我们在开发中,经常会遇到一行字,但是显示不同颜色和字体的情况,话不多说,直接上代码. 1.显示不同颜色,有两种方式 (1)通过 range 来设置 NSMuta

  • iOS中修改UITextField占位符字体颜色的方法总结

    前言 最近学了UITextField控件, 感觉在里面设置占位符非常好, 给用户提示信息, 于是就在想占位符的字体和颜色能不能改变呢?下面是小编的一些简单的实现,有需要的朋友们可以参考. 修改UITextField的占位符文字颜色主要有三个方法: 1.使用attributedPlaceholder属性 @property(nullable, nonatomic,copy) NSAttributedString *attributedPlaceholder NS_AVAILABLE_IOS(6_0

  • iOS中UIAlertController设置自定义标题与内容的方法

    前言 相信大家都知道,UIAlertController的标题和内容都是黑色的(对UIAlertController不了解的朋友可以参考这篇文章),但是在很多场景下都需要修改他们的颜色,比如在输入错误时把提示信息变为红色,或者自定义标题的颜色,可是在公开的API接口中好像并没有对应的方法,那么我们应该怎么做呢?下面话不多说了,来一起看看详细的介绍: 第三方控件 第一种方法当然就是使用第三方的Alert控件了,现在Github上有着众多的Alert控件(如SCLAlertView等),相信有很多都

  • iOS中设置view圆角化的四种方法示例

    前言 在最近进行项目性能优化的过程中,遇到view圆角优化的问题,有一些粗略的看法,现总结一下.分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 设置圆角目前知道的有四种方法: 1.通过shapeLayer设置 2.通过view的layer设置 3.通过BezierPath设置 4.通过贴图的方式设置 1.shapeLayer的实现 通过bezizerpath设置一个路径,加到目标视图的layer上.代码如下: // 创建一个view UIView *showView = [[U

  • iOS中设置网络超时时间+模拟的方法详解

    设置方法如下: 在封装的网络请求类里面如下设置 AFWEBAPI_REQUEST_TIMEOUT 这个参数为超时时间 #define AFWEBAPI_REQUEST_TIMEOUT 20 #pragma mark - 单例 & 构造函数 + (instancetype)sharedTools { static WXNetworkTools *instance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ i

  • iOS中监听UITextField值改变事件的方法实例

    前言 在实际情况中我们有时候在界面输入时候需要让用户输入的数据与模型同步,那么可能我们就需要监听UITextField值改变事件,然后在响应的方法中将新的值同步到模型中.这次我们主要提出三种方案,其中第一种方案是不一定有效的,后两种方案则是比较可靠的方案. 一.实现UITextFieldDelegate协议. 这种方式实际上是由系统空间回调协议中的方式,并且通过查阅文档我们可以发现有以下相关接口是相关的. - (BOOL)textFieldShouldBeginEditing:(UITextFi

随机推荐