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];
});
self.program = [YDWShaderHelper programWithShaderName:@"normal"];
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];
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"];
}
- (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;
}
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;
}
- (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);
}
只需要在两个点之间,按照一定的密度进行插值,就可以绘制出连续的轨迹。但是如果绘制结果是折线,那么并不平滑。
/**
通过二次贝塞尔曲线的三个关键点,计算点序列
@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];
}
// 绘制绘画的结果
- (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实现手写“绘画板”