在 iOS 中使用 OpenGL ES 實(shí)現(xiàn)繪畫板

今天我們使用 OpenGL ES 來(lái)實(shí)現(xiàn)一個(gè)繪畫板吐根,主要介紹在 OpenGL ES 中繪制平滑曲線的實(shí)現(xiàn)方案。

首先看一下最終效果:

在 iOS 中局义,有很多種方式可以實(shí)現(xiàn)一個(gè)繪畫板喜爷,比如我的另外一個(gè)項(xiàng)目 MFPaintView 就是基于 CoreGraphics 實(shí)現(xiàn)的檩帐。

然而另萤,使用 OpenGL ES 來(lái)實(shí)現(xiàn)可以獲得更多的靈活性,比如我們可以自定義筆觸的形狀泛源,這是其他實(shí)現(xiàn)方式做不到的目养。

我們知道,OpenGL ES 中只有 點(diǎn)幻梯、直線努释、三角形 這三種圖元。因此煞躬,怎么在 OpenGL ES 中繪制曲線逸邦,是我們第一個(gè)要解決的問(wèn)題,也是最復(fù)雜的問(wèn)題雷客。

我們會(huì)使用比較大的篇幅來(lái)講解這個(gè)問(wèn)題桥狡。至于繪畫板的其他功能實(shí)現(xiàn),并不是說(shuō)不重要部逮,只是說(shuō)其他的繪畫板實(shí)現(xiàn)方式嫂易,也會(huì)有類似的邏輯,所以這部分會(huì)放在最后再簡(jiǎn)單介紹一下蜈漓。

一、怎么繪制曲線

在 OpenGL ES 中繪制曲線的方式享完,就是 將曲線拆分成點(diǎn)序列來(lái)繪制 有额。

因?yàn)橐L制點(diǎn),所以我們采取的是 點(diǎn)圖元 茴迁。即我們要把頂點(diǎn)數(shù)據(jù)當(dāng)成 點(diǎn) 來(lái)繪制萤衰,并且每個(gè)點(diǎn)都要繪制出筆觸的紋理。關(guān)鍵步驟如下:

指定圖元類型:

glDrawArrays(GL_POINTS, 0, self.vertexCount);

頂點(diǎn)著色器:

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;
}

這里的關(guān)鍵點(diǎn)在于 gl_PointCoord 這個(gè)內(nèi)置變量倦卖,當(dāng)我們使用點(diǎn)圖元的時(shí)候椿争,可以通過(guò)這個(gè)變量獲取到 當(dāng)前像素在點(diǎn)圖元中的歸一化坐標(biāo) 秦踪。

但是這個(gè)坐標(biāo)的原點(diǎn)是在左上角,這和紋理坐標(biāo)在豎直方向上是相反的柠逞。所以從紋理讀取顏色的時(shí)候景馁,要做一個(gè) y 坐標(biāo)的轉(zhuǎn)換。

接下來(lái)裁僧,我們通過(guò) UITouch 來(lái)獲取觸摸點(diǎn)的位置聊疲,然后算出歸一化的頂點(diǎn)坐標(biāo)沪悲。

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

但是由于 iOS 系統(tǒng)觸摸事件的派發(fā)頻率有限,我們最終得到的只能是稀疏的點(diǎn)贡珊。如下圖所示,每個(gè)觸摸點(diǎn)之間的間隔會(huì)比較大爱致。

二寒随、怎么繪制密集的點(diǎn)

很容易想到妻往,只需要在兩個(gè)點(diǎn)之間,按照一定的密度進(jìn)行插值纫普,就可以繪制出連續(xù)的軌跡好渠。

但是很明顯,我們的繪制結(jié)果是折線悦昵,并不平滑晌畅。

三、怎么使曲線變平滑

解決點(diǎn)連接不平滑的問(wèn)題棋凳,一般是使用貝塞爾曲線连躏。這種方案在 MFPaintView 中也得到了很好的應(yīng)用。

具體的做法是使用 兩個(gè)頂點(diǎn)間的中點(diǎn)一個(gè)頂點(diǎn) 拍棕,來(lái)構(gòu)造一條貝塞爾曲線勺良。如下圖尚困,圖中的 3 個(gè) 紅點(diǎn) 被用來(lái)構(gòu)造一條貝塞爾曲線。

于是谬泌,我們的問(wèn)題就變成了 怎么在 OpenGL ES 中繪制貝塞爾曲線 。相當(dāng)于已知貝塞爾曲線的 3 個(gè)關(guān)鍵點(diǎn)陪蜻,反向來(lái)求曲線上的點(diǎn)序列潮峦。

我們知道貝塞爾曲線的方程是 P = (1 - t)^2 * P0 + 2 * t * (1 - t) * P1 + t^2 * P2t 是唯一的變量嘱腥,其取值范圍是 0 ~ 1 拘悦。

所以我們可以采取線性取值的方式,每一條貝塞爾曲線取 n 個(gè)點(diǎn)(n 是個(gè)確定的常量)分苇。只要依次往方程中代入 1 / n 屁桑、 2 / n 、 ... n / n 靖秩,就可以得到一個(gè)點(diǎn)序列竖瘾。

先將 n 取一個(gè)比較小的值捕传,這樣比較容易看出存在的問(wèn)題。我們發(fā)現(xiàn)职辅,點(diǎn)序列的間隔并不均勻聂示。原因有兩個(gè):

  1. 不同貝塞爾曲線的長(zhǎng)度不一樣,使用同一個(gè) n 值,算出來(lái)的點(diǎn)的疏密程度肯定不同。
  2. 由于貝塞爾曲線隨著 t 增長(zhǎng)气筋,曲線長(zhǎng)度的增長(zhǎng)并不是線性的旋圆。按照我們上面的算法灵巧,最終會(huì)得到的結(jié)果是 兩頭比較稀疏,中間比較密集 刻肄。

四敏弃、怎么生成均勻的點(diǎn)序列

貝塞爾曲線生成均勻的點(diǎn)序列,涉及到了一個(gè)經(jīng)典的「貝塞爾曲線勻速運(yùn)動(dòng)」問(wèn)題绿饵。

這個(gè)問(wèn)題的推導(dǎo)和計(jì)算比較復(fù)雜瓶颠。如果你有興趣,可以閱讀一下文末的兩篇文章吸祟。由于我還不能完全領(lǐng)悟廓啊,就不在這里誤導(dǎo)大家了。

簡(jiǎn)單來(lái)說(shuō)炒瘟,就是我們通過(guò)一系列的騷操作第步,封裝了一個(gè)方法,只需要傳入貝塞爾曲線的 3 個(gè)關(guān)鍵點(diǎn)和筆觸尺寸廓推,就可以獲取均勻的點(diǎn)序列翩隧。

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

下面我們固定貝塞爾曲線的 起始點(diǎn)控制點(diǎn),只移動(dòng) 終止點(diǎn)雷酪,來(lái)驗(yàn)證一下這個(gè)方法是否可靠涝婉。

可以看到墩弯,在移動(dòng)過(guò)程中,點(diǎn)和點(diǎn)的距離基本是保持一致的渔工,并且是均勻的涨缚。通過(guò)這個(gè)「神奇」的方法,我們終于畫出了平滑且均勻的曲線脓魏。

五茂翔、繪畫板功能實(shí)現(xiàn)

終于講完了最麻煩的部分,接下來(lái)簡(jiǎn)單介紹一下繪畫板基本功能的實(shí)現(xiàn)惭嚣。

1悔政、顏色混合

在以往的例子中,我們?cè)陂_始一次渲染之前槽地,都會(huì)調(diào)用 glClear(GL_COLOR_BUFFER_BIT) 來(lái)清除畫布芦瘾,因?yàn)槲覀儾幌MA羯洗蔚匿秩窘Y(jié)果近弟。

但是對(duì)于一個(gè)繪畫板來(lái)說(shuō),我們要不斷地往畫布上畫東西窗宦,所以是希望保留上次結(jié)果的。因此沐扳,在繪制之前不能執(zhí)行清除的操作句占。

另外躯嫉,由于我們的畫筆可能是半透明的,所以新繪制的顏色需要和畫布上已經(jīng)存在的顏色進(jìn)行混合擂啥。因此在繪制開始之前帆阳,需要開啟混合選項(xiàng)。

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

2山宾、筆觸調(diào)整

筆觸有 3 個(gè)屬性可以調(diào)整:顏色资锰、尺寸阶祭、形狀。它們本質(zhì)上都是對(duì)點(diǎn)圖元的調(diào)整鞭盟,通過(guò) uniform 變量的形式瑰剃,將顏色、尺寸鹃两、紋理傳入著色器并應(yīng)用舀凛。

3、橡皮擦

GLPaintView 在初始化的時(shí)候馋记,需要傳入一個(gè)背景色參數(shù),當(dāng)用戶切換到橡皮擦功能的時(shí)候宽堆,內(nèi)部只是單純地將畫筆的顏色切換成背景色茸习,于是就產(chǎn)生了橡皮擦的效果号胚。

4、撤銷重做

撤銷重做功能需要依賴兩個(gè)棧來(lái)實(shí)現(xiàn)箱亿。我們把用戶的手指從 按下屏幕到離開屏幕 這一過(guò)程中產(chǎn)生的數(shù)據(jù)弃秆,定義為一個(gè)操作對(duì)象,這個(gè)操作對(duì)象保存了歸一化后的點(diǎn)序列脑豹,以及點(diǎn)的屬性衡查。

@interface MFPaintModel : NSObject

/// 筆刷尺寸
@property (nonatomic, assign) CGFloat brushSize;
/// 筆刷顏色
@property (nonatomic, strong) UIColor *brushColor;
/// 筆刷模式
@property (nonatomic, assign) GLPaintViewBrushMode brushMode;
/// 筆觸紋理圖片文件名
@property (nonatomic, copy) NSString *brushImageName;
/// 點(diǎn)序列
@property (nonatomic, copy) NSArray<NSValue *> *points;

@end

撤銷重做的代碼實(shí)現(xiàn)大概像這樣子:

- (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];
}

需要注意的是峡捡,由于 撤銷操作 需要先清除畫布们拙,所以每次都需要重繪。而 重做操作 可以利用上次繪制的結(jié)果械拍,所以每次只需要繪制一個(gè)步驟即可装盯。

源碼

請(qǐng)到 GitHub 上查看完整代碼。

參考

獲取更佳的閱讀體驗(yàn)账磺,請(qǐng)?jiān)L問(wèn)原文地址【Lyman's Blog】在 iOS 中使用 OpenGL ES 實(shí)現(xiàn)繪畫板

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市碧聪,隨后出現(xiàn)的幾起案子液茎,更是在濱河造成了極大的恐慌,老刑警劉巖哼凯,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異班缎,居然都是意外死亡她渴,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門沉唠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)满葛,“玉大人罢屈,你說(shuō)我怎么就攤上這事〕” “怎么了曼月?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵哑芹,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我蹦魔,道長(zhǎng)勿决,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮咆繁,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘银觅。我一直安慰自己坏为,他們只是感情好匀伏,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布够颠。 她就那樣靜靜地躺著,像睡著了一般蛉抓。 火紅的嫁衣襯著肌膚如雪剃诅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天惩系,我揣著相機(jī)與錄音如筛,去河邊找鬼杨刨。 笑死,一個(gè)胖子當(dāng)著我的面吹牛芥颈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播纠屋,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼售担,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼署辉!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起哭尝,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤材鹦,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后侠姑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體箩做,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡邦邦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年燃辖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了黔龟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡巍棱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蛋欣,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布厚宰,位于F島的核電站,受9級(jí)特大地震影響杠袱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜窝稿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一楣富、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧讹躯,春花似錦菩彬、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)秉馏。三九已至耙旦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間萝究,已是汗流浹背免都。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留帆竹,地道東北人绕娘。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像栽连,于是被迫代替她去往敵國(guó)和親险领。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354