Today we use OpenGL ES to achieve a drawing board, mainly introduced in OpenGL ES drawing smooth curve implementation scheme.

First, take a look at the final result:

In iOS, there are many ways to implement a drawing board, for example, my other project MFPaintView is based on CoreGraphics.

However, using OpenGL ES allows for more flexibility, such as customizing the shape of the stroke, which is not possible with other implementations.

We know that there are only three primitives in OpenGL ES: points, lines, and triangles. So, how to draw curves in OpenGL ES is the first problem that we’re going to solve, and it’s also the most complicated problem.

We’re going to talk about this in a lot of space. As for the realization of other functions of the drawing board, it is not to say that it is not important, but that other ways of realizing the drawing board will have similar logic, so this chapter will be briefly introduced at the end.

How to draw a curve

The way to draw a curve in OpenGL ES, is to break it up into sequences of points.

Because we want to draw points, we use point primitives. That is, we draw the vertex data as points, and each point is textured as a stroke. The key steps are as follows:

Specify primitive type:

glDrawArrays(GL_POINTS, 0.self.vertexCount);
Copy the code

Vertex shaders:

attribute vec4 Position;

uniform float Size;

void main (void) {
    gl_Position = Position;
    gl_PointSize = Size;
}
Copy the code

Fragment shader:

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

Copy the code

The key point here is the built-in variable gl_PointCoord, which allows us to get the normalized coordinates of the current pixel in the point pixel when we use the point pixel.

But the origin of this coordinate is in the upper left corner, which is the opposite of the texture coordinate in the vertical direction. So when you read the color from the texture, you need to do a y coordinate conversion.

Next, we use the UITouch to get the position of the touch points and then calculate normalized vertex coordinates.

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

However, due to the limited distribution frequency of touch events in iOS system, we can only get sparse points in the end. As shown in the figure below, the distance between each touch point will be large.

How to draw dense points

It is easy to imagine that a continuous trajectory could be plotted by interpolating between two points at a certain density.

But obviously, our result is a broken line, not a smooth one.

Three, how to make the curve smooth

Bessel curve is usually used to solve the problem of non-smooth point connection. This scheme is also well applied in MFPaintView.

The method is to construct a Bessel curve using the midpoint and one vertex between two vertices. The three red dots in the figure below are used to construct a Bessel curve.

So, our problem becomes how to draw bezier curves in OpenGL ES. It is equivalent to finding the sequence of points on the Bessel curve inversely when three key points are known.

We know that the equation of the Bezier curve is P = (1-t)^2 * P0 + 2 * t * (1-t) * P1 + t^2 * P2, where t is the only variable, and it ranges from 0 to 1.

So we can take linear values, n points for each Bezier curve (n is a certain constant). Just plug in 1 / n, 2 / n,… N over n, you get a sequence of points.

Let’s take n to a small value, so it’s easier to see the problem. We find that the sequence of points is not evenly spaced. There are two reasons:

  1. Different Bezier curves have different lengths. Use the samenThe density of the points must be different.
  2. Because the Bezier curve goes along withtThe increase, the increase in the length of the curve is not linear. So if we do the algorithm above, what we’re going to end up with is thisThe ends are sparse and the middle is dense

How to generate a uniform sequence of points

Bessel curve generates a uniform sequence of points, which involves a classical problem of “Bessel curve uniform motion”.

The derivation and calculation of this problem are complicated. If you are interested, you can read the following two articles. Since I don’t fully understand, I won’t mislead you here.

In simple terms, we encapsulated a method through a series of operations, just need to pass in three key points of bezier curve and stroke size, can obtain a uniform sequence of points.

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

Now let’s fix the starting point and control point of the Bezier curve and only move the ending point to verify whether this method is reliable.

It can be seen that in the process of movement, the distance between points is basically the same and uniform. Through this “magic” method, we finally drew a smooth and uniform curve.

Five, painting board function realization

Finally finished talking about the most troublesome part, the next simple introduction to the realization of the basic function of the painting board.

1, color mix

In previous examples, we used to call glClear(GL_COLOR_BUFFER_BIT) to clear the canvas before starting a render, because we didn’t want to keep the last render.

But for a drawing board, we’re constantly painting on the canvas, so we want to keep the last result. Therefore, no cleanup operation can be performed before drawing.

Also, since our brushes may be translucent, the newly drawn colors need to be blended with the existing colors on the canvas. So before you start drawing, you need to turn on the blending option.

glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
Copy the code

2, brush adjustment

The stroke has three properties that can be adjusted: color, size and shape. They are all essentially adjustments to point primitives, passing colors, sizes, and textures into shaders and applying them in the form of uniform variables.

3. Eraser

When GLPaintView is initialized, it needs to pass in a background color parameter. When the user switches to the eraser function, it simply switches the brush color to the background color, thus creating the eraser effect.

4. Undo redo

The undo redo function relies on two stacks. We define the data generated by the user’s finger from pressing down the screen to leaving the screen as an action object, which holds the normalized sequence of points and the properties of the points.

@interface MFPaintModel : NSObject

/// Brush size
@property (nonatomic.assign) CGFloat brushSize;
/// Brush color
@property (nonatomic.strong) UIColor *brushColor;
/// Brush mode
@property (nonatomic.assign) GLPaintViewBrushMode brushMode;
/// Brush texture image file name
@property (nonatomic.copy) NSString *brushImageName;
/ / / point sequence
@property (nonatomic.copy) NSArray<NSValue *> *points;

@end
Copy the code

The undo redo code implementation might look something like this:

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

Note that since the undo operation requires the canvas to be cleaned first, it needs to be redrawn each time. Redo operations can take advantage of the results of the last drawing, so only one step is required at a time.

The source code

Check out the full code on GitHub.

reference

  • IOS development -OpenGL ES Drawing application
  • Bezier – The realization of uniform Bezier curve motion
  • How to obtain the approximate relation between the length of bezier curve and t?

Use OpenGL ES for drawing boards on iOS