今天咱們使用 OpenGL ES 來實現一個繪畫板,主要介紹在 OpenGL ES 中繪製平滑曲線的實現方案。html
首先看一下最終效果:ios
在 iOS 中,有不少種方式能夠實現一個繪畫板,好比個人另一個項目 MFPaintView 就是基於 CoreGraphics 實現的。git
然而,使用 OpenGL ES 來實現能夠得到更多的靈活性,好比咱們能夠自定義筆觸的形狀,這是其餘實現方式作不到的。github
咱們知道,OpenGL ES 中只有 點、直線、三角形 這三種圖元。所以,怎麼在 OpenGL ES 中繪製曲線,是咱們第一個要解決的問題,也是最複雜的問題。算法
咱們會使用比較大的篇幅來說解這個問題。至於繪畫板的其餘功能實現,並非說不重要,只是說其餘的繪畫板實現方式,也會有相似的邏輯,因此這部分會放在最後再簡單介紹一下。markdown
在 OpenGL ES 中繪製曲線的方式,就是 將曲線拆分紅點序列來繪製 。oop
由於要繪製點,因此咱們採起的是 點圖元 。即咱們要把頂點數據當成 點 來繪製,而且每一個點都要繪製出筆觸的紋理。關鍵步驟以下:atom
指定圖元類型:spa
glDrawArrays(GL_POINTS, 0, self.vertexCount); 複製代碼
頂點着色器:code
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 * P2
, t
是惟一的變量,其取值範圍是 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; 複製代碼
下面咱們固定貝塞爾曲線的 起始點 和 控制點,只移動 終止點,來驗證一下這個方法是否可靠。
能夠看到,在移動過程當中,點和點的距離基本是保持一致的,而且是均勻的。經過這個「神奇」的方法,咱們終於畫出了平滑且均勻的曲線。
終於講完了最麻煩的部分,接下來簡單介紹一下繪畫板基本功能的實現。
一、顏色混合
在以往的例子中,咱們在開始一次渲染以前,都會調用 glClear(GL_COLOR_BUFFER_BIT)
來清除畫布,由於咱們不但願保留上次的渲染結果。
可是對於一個繪畫板來講,咱們要不斷地往畫布上畫東西,因此是但願保留上次結果的。所以,在繪製以前不能執行清除的操做。
另外,因爲咱們的畫筆多是半透明的,因此新繪製的顏色須要和畫布上已經存在的顏色進行混合。所以在繪製開始以前,須要開啓混合選項。
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
複製代碼
二、筆觸調整
筆觸有 3 個屬性能夠調整:顏色、尺寸、形狀。它們本質上都是對點圖元的調整,經過 uniform
變量的形式,將顏色、尺寸、紋理傳入着色器並應用。
三、橡皮擦
GLPaintView
在初始化的時候,須要傳入一個背景色參數,當用戶切換到橡皮擦功能的時候,內部只是單純地將畫筆的顏色切換成背景色,因而就產生了橡皮擦的效果。
四、撤銷重作
撤銷重作功能須要依賴兩個棧來實現。咱們把用戶的手指從 按下屏幕到離開屏幕 這一過程當中產生的數據,定義爲一個操做對象,這個操做對象保存了歸一化後的點序列,以及點的屬性。
@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 上查看完整代碼。
獲取更佳的閱讀體驗,請訪問原文地址【Lyman's Blog】在 iOS 中使用 OpenGL ES 實現繪畫板