設定繪畫板
一、設定畫板視圖
- 設定繪畫板位置、大小、背景
CGFloat ratio = self.view.frame.size.height / self.view.frame.size.width;
CGFloat width = 1500;
CGSize textureSize = CGSizeMake(width, width * ratio);
UIImage *image = [UIImage imageNamed:@"paper.jpg"];
self.paintView = [[YDWPaintView alloc] initWithFrame:self.view.bounds
textureSize:textureSize
backgroundImage:image];
self.paintView.delegate = self;
[self.view addSubview:self.paintView];
- 繪畫板的初始化
// 手指按住螢屏,到離開,產生的所有的點
@property (nonatomic, strong) NSMutableArray *pointsPreDraw;
// 操作的堆疊
@property (nonatomic, strong) YDWPaintStack *operationStack;
// 撤銷的操作的堆疊
@property (nonatomic, strong) YDWPaintStack *undoOperationStack;
self.operationStack = [[YDWPaintStack alloc] init];
self.undoOperationStack = [[YDWPaintStack alloc] init];
self.pointsPreDraw = [[NSMutableArray alloc] init];
self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:self.context];
self.vertices = malloc(sizeof(Vertex) * 4);
self.vertices[0] = (Vertex){{-1, 1, 0}, {0, 1}};
self.vertices[1] = (Vertex){{-1, -1, 0}, {0, 0}};
self.vertices[2] = (Vertex){{1, 1, 0}, {1, 1}};
self.vertices[3] = (Vertex){{1, -1, 0}, {1, 0}};
[self setupGLLayer];
[self genProgram];
[self genBuffers];
[self bindRenderLayer:self.glLayer];
// 沒有指定紋理尺寸,設定默認值
if (CGSizeEqualToSize(self.textureSize, CGSizeZero)) {
self.textureSize = CGSizeMake(self.drawableWidth, self.drawableHeight);
}
self.paintTexture = [[YDWPaintTexture alloc] initWithContext:self.context
size:self.textureSize
backgroundColor:self.backgroundColor
backgroundImage:self.backgroundImage];
[self bindTexture];
self.brushSize = kDefaultBrushSize;
self.brushColor = [UIColor blackColor];
self.brushMode = GLPaintViewBrushModePaint;
dispatch_async(dispatch_get_main_queue(), ^{
[self clear];
});
- 創建 program
self.program = [YDWShaderHelper programWithShaderName:@"normal"];
- 創建 buffer
glGenFramebuffers(1, &_frameBuffer);
glGenRenderbuffers(1, &_renderBuffer);
glGenBuffers(1, &_vertexBuffer);
- 創建輸出層
CAEAGLLayer *layer = [[CAEAGLLayer alloc] init];
layer.frame = self.bounds;
layer.contentsScale = [[UIScreen mainScreen] scale];
self.glLayer = layer;
[self.layer addSublayer:self.glLayer];
- 系結影像要輸出的 layer
glBindRenderbuffer(GL_RENDERBUFFER, self.renderBuffer);
[self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];
glBindFramebuffer(GL_FRAMEBUFFER, self.frameBuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_RENDERBUFFER,
self.renderBuffer);
- 系結要繪制的紋理
glUseProgram(self.program);
GLuint textureSlot = glGetUniformLocation(self.program, "Texture");
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, self.paintTexture.textureID);
glUniform1i(textureSlot, 1);
- 繪制資料到螢屏
[self.paintTexture drawPoints:points];
[self display];
- 繪制
glDisable(GL_BLEND);
glViewport(0, 0, [self drawableWidth], [self drawableHeight]); // 繪制前先切換 Viewport
glBindFramebuffer(GL_FRAMEBUFFER, self.frameBuffer);
glUseProgram(self.program);
GLuint positionSlot = glGetAttribLocation(self.program, "Position");
GLuint textureCoordsSlot = glGetAttribLocation(self.program, "TextureCoords");
glBindBuffer(GL_ARRAY_BUFFER, self.vertexBuffer);
GLsizeiptr bufferSizeBytes = sizeof(Vertex) * 4;
glBufferData(GL_ARRAY_BUFFER, bufferSizeBytes, self.vertices, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(positionSlot);
glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), NULL + offsetof(Vertex, positionCoord));
glEnableVertexAttribArray(textureCoordsSlot);
glVertexAttribPointer(textureCoordsSlot, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), NULL + offsetof(Vertex, textureCoord));
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
[self.context presentRenderbuffer:GL_RENDERBUFFER];
- 獲取渲染快取寬度和獲取渲染快取高度
// 獲取渲染快取寬度
- (GLint)drawableWidth {
GLint backingWidth;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth);
return backingWidth;
}
// 獲取渲染快取高度
- (GLint)drawableHeight {
GLint backingHeight;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &backingHeight);
return backingHeight;
}
二、 設定畫板紋理
- 創建著色器程式
// 創建 brushProgram
- (void)genBrushProgram {
self.brushProgram = [YDWShaderHelper programWithShaderName:@"brush"];
}
// 創建 normalProgram
- (void)genNormalProgram {
self.normalProgram = [YDWShaderHelper programWithShaderName:@"normal"];
}
- 創建快取和創建 buffer
- (void)genBuffers {
glGenFramebuffers(1, &_frameBuffer);
glGenRenderbuffers(1, &_renderBuffer);
glGenBuffers(1, &_vertexBuffer);
}
// 初始化繪制紋理的頂點快取
- (void)setupNormalVertexBuffer {
float vertices[] = {
-1.0f, -1.0f, 0.0f, 0.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
1.0f, -1.0f, 0.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
};
glGenBuffers(1, &_normalVertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, _normalVertexBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
}
- 創建紋理
// 創建目標紋理
- (void)genTargetTexture {
glGenTextures(1, &_textureID);
glBindTexture(GL_TEXTURE_2D, _textureID);
glBindFramebuffer(GL_FRAMEBUFFER, self.frameBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, [self drawableWidth], [self drawableHeight], 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _textureID, 0);
glBindTexture(GL_TEXTURE_2D, 0);
}
// 創建繪畫紋理
- (void)genPaintTexture {
glGenTextures(1, &_paintTextureID);
glBindTexture(GL_TEXTURE_2D, _paintTextureID);
glBindFramebuffer(GL_FRAMEBUFFER, self.frameBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, [self drawableWidth], [self drawableHeight], 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _paintTextureID, 0);
glBindTexture(GL_TEXTURE_2D, 0);
}
// 創建背景紋理
- (void)genBackgroundTexture {
if (self.backgroundImage) {
self.backgroundTextureID = [YDWShaderHelper createTextureWithImage:self.backgroundImage];
}
}
- 初始化筆觸紋理
- (void)setBrushTextureWithImageName:(NSString *)imageName
isFastMode:(BOOL)isFastMode {
if (imageName.length == 0) {
return;
}
if (isFastMode) {
NSMutableArray *textureIDs = [self.brushTextureCache valueForKey:imageName];
if (!textureIDs) {
return;
}
self.brushTextureID = (GLuint)[[textureIDs firstObject] intValue];
} else {
// 加載紋理
UIImage *image = [UIImage imageNamed:imageName];
self.brushTextureID = [YDWShaderHelper createTextureWithImage:image];
// 添加快取
NSMutableArray *textureIDs = [self.brushTextureCache valueForKey:imageName];
if (!textureIDs) {
textureIDs = [[NSMutableArray alloc] init];
}
[textureIDs addObject:@(self.brushTextureID)];
[self.brushTextureCache setValue:textureIDs forKey:imageName];
}
}
繪制曲線
OpenGL ES 中只有 點、直線、三角形這三種圖元,因此,怎么在 OpenGL ES 中繪制曲線,是我們第一個要解決的問題,也是最復雜的問題,
一、繪制曲線連接的點
在 OpenGL ES 中繪制曲線的方式,就是將曲線拆分成點序列來繪制,
因為要繪制點,所以采取的是點圖元 ,即要把頂點資料當成點來繪制,并且每個點都要繪制出筆觸的紋理,關鍵步驟如下:
- 指定圖元型別
// 繪制前切換 Viewport
glViewport(0, 0, [self drawableWidth], [self drawableHeight]);
glBindFramebuffer(GL_FRAMEBUFFER, self.frameBuffer);
// 系結到繪畫紋理
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self.paintTextureID, 0);
glUseProgram(self.brushProgram);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, self.brushTextureID);
glUniform1i(glGetUniformLocation(self.brushProgram, "Texture"), 0);
GLuint positionSlot = glGetAttribLocation(self.brushProgram, "Position");
glBindBuffer(GL_ARRAY_BUFFER, self.vertexBuffer);
GLsizeiptr bufferSizeBytes = sizeof(Vertex) * self.vertexCount;
glBufferData(GL_ARRAY_BUFFER, bufferSizeBytes, self.vertices, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(positionSlot);
glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), NULL + offsetof(Vertex, positionCoord));
// 指定圖元型別
glDrawArrays(GL_POINTS, 0, self.vertexCount);
- 頂點著色器
attribute vec4 Position;
uniform float Size;
void main (void) {
gl_Position = Position;
gl_PointSize = Size;
}
- 片元著色器
關鍵在于 gl_PointCoord 這個內置變數,當使用點圖元的時候,可以通過這個變數獲取到 當前像素在點圖元中的歸一化坐標,但是這個坐標的原點是在左上角,這和紋理坐標在豎直方向上是相反的,所以從紋理讀取顏色的時候,要做一個 y 坐標的轉換,
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;
}
- 通過 UITouch 來獲取觸摸點的位置,然后算出歸一化的頂點坐標,
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesMoved:touches withEvent:event];
[self addPointWithTouches:touches];
}
- (void)addPointWithTouches:(NSSet<UITouch *> *)touches {
UITouch *currentTouch = [touches anyObject];
CGPoint previousPoint = [currentTouch previousLocationInView:self];
CGPoint currentPoint = [currentTouch locationInView:self];
// 起始點和當前的點重合,不需要繪制
if (CGPointEqualToPoint(self.fromPoint, currentPoint)) {
return;
}
CGPoint from = self.fromPoint;
CGPoint to = middlePoint(previousPoint, currentPoint);
CGPoint control = previousPoint;
NSArray <NSValue *>*points = [YDWBezierCurvesTool pointsWithFrom:from
to:to
control:control
pointSize:self.brushSize];
if (points.count == 0) {
return;
}
// 去除第一個點,避免與上次繪制的最后一個點重復
NSMutableArray *mutPoints = [points mutableCopy];
[mutPoints removeObjectAtIndex:0];
points = [self verticesWithPoints:[mutPoints copy]];
[self.pointsPreDraw addObjectsFromArray:points];
[self drawPointsToScreen:points];
self.fromPoint = to;
}
// UIKit 坐標點,轉化為頂點坐標
- (NSArray <NSValue *>*)verticesWithPoints:(NSArray <NSValue *>*)points {
NSMutableArray *mutArr = [[NSMutableArray alloc] init];
for (int i = 0; i < points.count; ++i) {
[mutArr addObject:@([self vertexWithPoint:points[i].CGPointValue])];
}
return [mutArr copy];
}
// 歸一化頂點坐標
- (CGPoint)vertexWithPoint:(CGPoint)point {
float x = (point.x / self.frame.size.width) * 2 - 1;
float y = 1 - (point.y / self.frame.size.height) * 2;
return CGPointMake(x, y);
}
- 由于 iOS 系統觸摸事件的派發頻率有限,最終得到的只能是稀疏的點,如下圖所示,每個觸摸點之間的間隔會比較大,

二、繪制密集的點
只需要在兩個點之間,按照一定的密度進行插值,就可以繪制出連續的軌跡,但是如果繪制結果是折線,那么并不平滑,

三、使曲線變平滑
- 解決點連接不平滑的問題,一般是使用貝塞爾曲線,具體的做法是使用兩個頂點間的中點和 一個頂點 ,來構造一條貝塞爾曲線,如下圖,圖中的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 增長,曲線長度的增長并不是線性的,按照上面的演算法,最侄訓得到的結果是 兩頭比較稀疏,中間比較密集 ,
四、生成均勻的點序列
- 貝塞爾曲線生成均勻的點序列,涉及到了一個經典的“貝塞爾曲線勻速運動”問題,
- 在YDWBezierCurvesTool封裝一個方法,只需要傳入貝塞爾曲線的 3 個關鍵點和筆觸尺寸,就可以獲取均勻的點序列,
/**
通過二次貝塞爾曲線的三個關鍵點,計算點序列
@param from 起始點
@param to 終止點
@param control 控制點
@param pointSize 畫筆尺寸,用于計算生成點的數量
@return 點序列
*/
+ (NSArray<NSValue *> *)pointsWithFrom:(CGPoint)from
to:(CGPoint)to
control:(CGPoint)control
pointSize:(CGFloat)pointSize {
CGPoint P0 = from;
// 如果 control 是 from 和 to 的中點,則將 control 設定為和 from 重合
CGPoint P1 = isCenter(control, from, to) ? from : control;
CGPoint P2 = to;
float ax = P0.x - 2 * P1.x + P2.x;
float ay = P0.y - 2 * P1.y + P2.y;
float bx = 2 * P1.x - 2 * P0.x;
float by = 2 * P1.y - 2 * P0.y;
float A = 4 * (ax * ax + ay * ay);
float B = 4 * (ax * bx + ay * by);
float C = bx * bx + by * by;
// 整條曲線的長度
float totalLength = [self lengthWithT:1 A:A B:B C:C];
// 用點的尺寸計算出,單位長度需要多少個點
float pointsPerLength = 5.0 / pointSize;
// 曲線應該生成的點數
int count = MAX(1, ceilf(pointsPerLength * totalLength));
NSMutableArray *mutArr = [[NSMutableArray alloc] init];
for(int i = 0; i <= count; ++i) {
float t = i * 1.0f / count;
float length = t*totalLength;
t = [self tWithT:t length:length A:A B:B C:C];
// 根據 t 求出坐標
float x = (1-t)*(1-t)*P0.x +2*(1-t)*t*P1.x + t*t*P2.x;
float y = (1-t)*(1-t)*P0.y +2*(1-t)*t*P1.y + t*t*P2.y;
[mutArr addObject:[NSValue valueWithCGPoint:CGPointMake(x, y)]];
}
return [mutArr copy];
}
- 固定貝塞爾曲線的起始點和控制點,只移動終止點,來驗證一下這個方法是否可靠,

- 可以看到,在移動程序中,點和點的距離基本是保持一致的,并且是均勻的,通過這個方法,終于畫出了平滑且均勻的曲線,

繪畫板功能實作
一、顏色混合
- 之前的 OpenGL ES 在開始一次渲染之前,都會呼叫 glClear(GL_COLOR_BUFFER_BIT) 來清除畫布,因為不希望保留上次的渲染結果,
- 對于一個繪畫板來說,要不斷地往畫布上畫東西,所以是希望保留上次結果的,因此,在繪制之前不能執行清除的操作,
- 由于畫筆可能是半透明的,所以新繪制的顏色需要和畫布上已經存在的顏色進行混合,因此在繪制開始之前,需要開啟混合選項,
// 繪制繪畫的結果
- (void)renderPaint {
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glViewport(0, 0, [self drawableWidth], [self drawableHeight]);
glBindFramebuffer(GL_FRAMEBUFFER, self.frameBuffer);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self.textureID, 0);
glBindBuffer(GL_ARRAY_BUFFER, self.normalVertexBuffer);
glUseProgram(self.normalProgram);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, self.paintTextureID);
glUniform1i(glGetUniformLocation(self.normalProgram, "Texture"), 0);
GLuint positionSlot = glGetAttribLocation(self.normalProgram, "Position");
GLuint textureSlot = glGetAttribLocation(self.normalProgram, "TextureCoords");
glEnableVertexAttribArray(positionSlot);
glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(textureSlot);
glVertexAttribPointer(textureSlot, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3* sizeof(float)));
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
二、筆觸調整
筆觸有 3 個屬性可以調整:顏色、尺寸、形狀,它們本質上都是對點圖元的調整,通過 uniform 變數的形式,將顏色、尺寸、紋理傳入著色器并應用,
- (void)setBrushTextureUseFastModeIfCanWithImageName:(NSString *)imageName {
NSMutableArray *textureIDs = [self.brushTextureCache valueForKey:imageName];
[self setBrushTextureWithImageName:imageName isFastMode:textureIDs != nil];
}
- (void)setBrushTextureWithImageName:(NSString *)imageName
isFastMode:(BOOL)isFastMode {
if (imageName.length == 0) {
return;
}
if (isFastMode) {
NSMutableArray *textureIDs = [self.brushTextureCache valueForKey:imageName];
if (!textureIDs) {
return;
}
self.brushTextureID = (GLuint)[[textureIDs firstObject] intValue];
} else {
// 加載紋理
UIImage *image = [UIImage imageNamed:imageName];
self.brushTextureID = [YDWShaderHelper createTextureWithImage:image];
// 添加快取
NSMutableArray *textureIDs = [self.brushTextureCache valueForKey:imageName];
if (!textureIDs) {
textureIDs = [[NSMutableArray alloc] init];
}
[textureIDs addObject:@(self.brushTextureID)];
[self.brushTextureCache setValue:textureIDs forKey:imageName];
}
}
三、 橡皮擦
YDWPaintView在初始化的時候,需要傳入一個背景色引數,當用戶切換到橡皮擦功能的時候,內部只是單純地將畫筆的顏色切換成背景色,于是就產生了橡皮擦的效果,
- (void)setBrushMode:(GLPaintViewBrushMode)brushMode {
_brushMode = brushMode;
self.paintTexture.brushMode = (GLPaintTextureBrushMode)brushMode;
}
四、撤銷重做
撤銷重做功能需要依賴兩個堆疊來實作,我們把用戶的手指從按下螢屏到離開螢屏這一程序中產生的資料,定義為一個操作物件,這個操作物件保存了歸一化后的點序列,以及點的屬性,
/// 筆刷尺寸
@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;
- 撤銷重做的實作邏輯
- (void)undo {
if ([self.operationStack isEmpty]) {
return;
}
YDWPaintModel *model = self.operationStack.topModel;
[self.operationStack popModel];
[self.undoOperationStack pushModel:model];
[self reDraw];
}
- (void)redo {
if ([self.undoOperationStack isEmpty]) {
return;
}
YDWPaintModel *model = self.undoOperationStack.topModel;
[self.undoOperationStack popModel];
[self.operationStack pushModel:model];
[self drawModel:model];
}
- 由于撤銷操作需要先清除畫布,所以每次都需要重繪,而重做操作可以利用上次繪制的結果,所以每次只需要繪制一個步驟即可,
效果展示

完整示例
iOS之OpenGL ES實作手寫“繪畫板”
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/2814.html
標籤:其他
上一篇:JZ51 構建乘積陣列
下一篇:2020-09-08
