1. 程式人生 > 程式設計 >在iOS中使用OpenGL ES實現繪畫板的方法

在iOS中使用OpenGL ES實現繪畫板的方法

今天我們使用 OpenGL ES 來實現一個繪畫板,主要介紹在 OpenGL ES 中繪製平滑曲線的實現方案。

首先看一下最終效果:

在iOS中使用OpenGL ES實現繪畫板的方法

在 iOS 中,有很多種方式可以實現一個繪畫板,比如我的另外一個專案 MFPaintView 就是基於 CoreGraphics 實現的。

然而,使用 OpenGL ES 來實現可以獲得更多的靈活性,比如我們可以自定義筆觸的形狀,這是其他實現方式做不到的。

我們知道,OpenGL ES 中只有 點、直線、三角形 這三種圖元。因此, 怎麼在 OpenGL ES 中繪製曲線 ,是我們第一個要解決的問題,也是最複雜的問題。

我們會使用比較大的篇幅來講解這個問題。至於繪畫板的其他功能實現,並不是說不重要,只是說其他的繪畫板實現方式,也會有類似的邏輯,所以這部分會放在最後再簡單介紹一下。

一、怎麼繪製曲線

在 OpenGL ES 中繪製曲線的方式,就是 將曲線拆分成點序列來繪製

因為要繪製點,所以我們採取的是 點圖元 。即我們要把頂點資料當成 來繪製,並且每個點都要繪製出筆觸的紋理。關鍵步驟如下:

指定圖元型別:

glDrawArrays(GL_POINTS,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 系統觸控事件的派發頻率有限,我們最終得到的只能是稀疏的點。如下圖所示,每個觸控點之間的間隔會比較大。

在iOS中使用OpenGL ES實現繪畫板的方法

二、怎麼繪製密集的點

很容易想到,只需要在兩個點之間,按照一定的密度進行插值,就可以繪製出連續的軌跡。

在iOS中使用OpenGL ES實現繪畫板的方法

但是很明顯,我們的繪製結果是折線,並不平滑。

三、怎麼使曲線變平滑

解決點連線不平滑的問題,一般是使用貝塞爾曲線。這種方案在 MFPaintView 中也得到了很好的應用。

具體的做法是使用 兩個頂點間的中點一個頂點 ,來構造一條貝塞爾曲線。如下圖,圖中的 3 個 紅點 被用來構造一條貝塞爾曲線。

在iOS中使用OpenGL ES實現繪畫板的方法

於是,我們的問題就變成了 怎麼在 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 ,就可以得到一個點序列。

在iOS中使用OpenGL ES實現繪畫板的方法

先將 n 取一個比較小的值,這樣比較容易看出存在的問題。我們發現, 點序列的間隔並不均勻 。原因有兩個:

  • 不同貝塞爾曲線的長度不一樣,使用同一個 n 值,算出來的點的疏密程度肯定不同。
  • 由於貝塞爾曲線隨著 t 增長,曲線長度的增長並不是線性的。按照我們上面的演算法,最終會得到的結果是 兩頭比較稀疏,中間比較密集

四、怎麼生成均勻的點序列

貝塞爾曲線生成均勻的點序列,涉及到了一個經典的「貝塞爾曲線勻速運動」問題。

這個問題的推導和計算比較複雜。如果你有興趣,可以閱讀一下文末的兩篇文章。由於我還不能完全領悟,就不在這裡誤導大家了。

簡單來說,就是我們通過一系列的騷操作,封裝了一個方法,只需要傳入貝塞爾曲線的 3 個關鍵點和筆觸尺寸,就可以獲取均勻的點序列。

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

下面我們固定貝塞爾曲線的 起始點控制點 ,只移動 終止點 ,來驗證一下這個方法是否可靠。

在iOS中使用OpenGL ES實現繪畫板的方法

可以看到,在移動過程中,點和點的距離基本是保持一致的,並且是均勻的。通過這個「神奇」的方法,我們終於畫出了平滑且均勻的曲線。

在iOS中使用OpenGL ES實現繪畫板的方法

五、繪畫板功能實現

終於講完了最麻煩的部分,接下來簡單介紹一下繪畫板基本功能的實現。

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 繪畫板內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!