We know that puddings are easily deformed by external forces. And because the pudding is elastic, it shakes back and forth when it’s deformed. Today we’re going to use Shader to simulate the jiggling effect of pudding.

As usual, here’s the end result:

1. Location and shape

1. Control layer

At first, all we had was a static image. So the first step is to figure out where the pudding is in the picture.

Just to be clear: the position and shape of the pudding is determined by the user, and this interaction needs to be done at the UIKit layer. After the determination, the corresponding position and shape information needs to be passed to the Shader in preparation for the subsequent animation simulation.

Since the pudding can be oval or quasi-circular, it can’t be determined by simply using a center and radius. We need a more flexible form of control.

The final scheme is as follows: four vertices are used to control four Bessel curves. Bezier curves are drawn by taking the midpoint of each edge as the starting point and ending point, and the vertex as the control point. The four Bezier curves form a closed quasi-circle. As shown below:

While this control still isn’t enough to cover all shapes, it’s a lot more flexible than a circle.

In addition, you can see that there is a green dot in the center, which is also a dimension that allows the user to control, which represents the center of the pudding. It is mainly related to the simulated sloshing effect, which will be discussed later.

Thus, on the control layer, the user can determine the shape and center of the pudding by controlling the coordinates of five points.

2. Data transfer

From the previous step, we got the position and shape information. The next step is to give this information to the Shader, and the Shader can calculate and offset the points in the target area during the animation.

P = (1-t)^2 * P0 + 2 * t * (1-t) * P1 + t^2 * P2

Note: P0 is the starting point, P1 is the control point, and P2 is the end point. These three points are all known, and the only variable is T, whose value ranges from 0 to 1.

Since bezier curves have specific equations, we only need to pass the coordinates of key points (start points, stop points, control points) and then calculate the position relationship in the Shader.

Since UIKit coordinates are different from texture coordinates, there is a conversion process before passing, and the conversion code is as follows:

MFWobbleModel *wobbleModel = [[MFWobbleModel alloc] init];
wobbleModel.pointLT = CGPointMake(model.pointLT.x / width, 1 - (model.pointLT.y / height));
wobbleModel.pointRT = CGPointMake(model.pointRT.x / width, 1 - (model.pointRT.y / height));
wobbleModel.pointRB = CGPointMake(model.pointRB.x / width, 1 - (model.pointRB.y / height));
wobbleModel.pointLB = CGPointMake(model.pointLB.x / width, 1 - (model.pointLB.y / height));
wobbleModel.center = CGPointMake(model.center.x / width, 1 - (model.center.y / height));
Copy the code

Note: wobbleModel holds texture coordinates, model holds UIKit coordinates.

Passing is still the uniform variable method, which we covered in the previous article and won’t repeat here.

Now that we have bezier’s equations in the Shader, how do we determine the position of the points in relation to the four curves?

This is the first point of this article.

We know that in the fragment shader, the fragment shader code is executed once for each fragment. Therefore, the problem we are faced with is: given the texture coordinates of a point, how to judge whether the point is in the target region?

First, we divide the target area into four regions based on four Bezier curves and midpoints. Therefore, the above problem can be simplified as: given the texture coordinates of a point, how to determine whether the point is in the region formed by a single Bezier curve and the midpoint?

The specific steps are as follows:

  1. Connect the current point with the midpoint to get a straight line, and work out the equation of the line.
  2. Find the intersection of the line and the Bezier curve.
  3. If there is an intersection, determine whether the current point is between the intersection and the center point, if it is in the region, otherwise it is outside the region.

Through the above steps, it is possible to determine whether a point is within the range of a Bessel curve. If it’s not there, let’s just switch to another curve and continue. In this way, we can determine whether the point falls in the target region.

Now that I have the idea, the next step is to solve the specific steps.

We know that the general formula for the equation of a line is Ax + By + C = 0

Given two points P1(x1, y1) and P2(x2, y2) on the line, the corresponding parameter values can be calculated:

A = y2 - y1
B = x1 - x2
C = x2 * y1 - x1 * y2
Copy the code

The code is:

float getA(vec2 point1, vec2 point2) {
    return point2.y - point1.y;
}

float getB(vec2 point1, vec2 point2) {
    return point1.x - point2.x;
}

float getC(vec2 point1, vec2 point2) {
    return point2.x * point1.y - point1.x * point2.y;
}
Copy the code

A, B, and C can be regarded as known numbers.

We have already mentioned the equations of the Bezier curve, now we break them down into the equations of x and y respectively.

x = (1 - t)^2 * x0 + 2 * t * (1 - t) * x1 + t^2 * x2
y = (1 - t)^2 * y0 + 2 * t * (1 - t) * y1 + t^2 * y2
Copy the code

Substituting the above two equations into the general equation Ax + By + C = 0 will eliminate x and y, leaving only t as the unknown.

And then we solve this equation, and we get two solutions. As follows:

It’s a long string to write in code, but I won’t post the details here, but encapsulate them into two functions:

float getT1(vec2 point1, vec2 point2, vec2 point3, float a, float b, float c) {
    float t;  // t = ...
    return t;
}

float getT2(vec2 point1, vec2 point2, vec2 point3, float a, float b, float c) {
    float t;  // t = ...
    return t;
}
Copy the code

Of course, I didn’t figure this out myself. Here’s a tool site that can help us solve our equations very quickly. In the picture below, we input the equation after eliminating x and y, and it gives us two solutions:

Note: If you read the source code carefully, you will see that the implementations of getT1 and getT2 are not exactly the same as the ones shown above, but they are still equivalent after the transformation. I don’t want to go into too much detail here, just know that it’s an intermediate step in finding the intersection, and where it came from.

Therefore, we can use the above function to find the values of the two t, as long as t satisfies the range of 0 to 1, it means that there is an intersection between the line and bezier curve. And then you substitute t that satisfies this condition into the Bezier curve equation, and you can figure out the corresponding intersection coordinates. The code is as follows:

vec2 getPoint(vec2 point1, vec2 point2, vec2 point3, float t) {
    vec2 point = pow(1.0 - t, 2.0) * point1 + 2.0 * t * (1.0 - t) * point2 + pow(t, 2.0) * point3;
    return point;
}
Copy the code

After solving the intersection point, judge whether the current point is located between the intersection point and the midpoint, the code is as follows:

bool isPointInside(vec2 point, vec2 point1, vec2 point2) {
    vec2 tmp1 = point - point1;
    vec2 tmp2 = point - point2;
    return tmp1.x * tmp2.x <= 0.0 && tmp1.y * tmp2.y <= 0.0;
}
Copy the code

This returns true to indicate that the point is in the range and false to indicate that the point is outside the range.

Second, physical effect simulation

1. Position offset

In essence, the sloshing effect is achieved by varying the position of the points in the target area. And the displacement rule of each point determines the true degree of the final effect.

This is the second focus of this paper.

I thought there would be a formula for this physics-related phenomenon, and ALL I had to do was stick to it. How to find a circle, what also can not find, may be my search posture is wrong, that had to make up their own.

Note: the rules of displacement directly determine the final rendering effect, I only explain my rules and implementation here. If you are good at math, you can try to set up a three-dimensional coordinate system and map all the points in the target region to coordinates in space. This will give you a more accurate calculation of the different displacement effects of the central point on each point. In my case, just “passable.”

My displacement rule is as follows:

  1. The displacement depends only on the distance between the current point and the center point. The larger the distance, the smaller the displacement, and the displacement at the edge of the region is 0.
  2. The displacement decreases linearly with the increase of distance from the center point.

The first point should be easy to understand, but here’s a brief explanation of the “nonlinearity” of the second point.

To achieve the desired effect, the target region needs to be treated approximately as a semi-sphere. Our still image is a top view, with a semicircle acting as an approximate front view.

Here D represents the midpoint of the target region, E represents any point within the target region, and A represents the intersection point calculated by t mentioned above. The radius of the semicircle, AC, represents the distance from the intersection to the midpoint.

When point D moves to point F, point E moves to point G, and point A remains the same. From the top view, point D is moving HC and point E is moving IJ. So our ultimate goal is to find IJ in terms of HC.

We assume that at all points on AD, the arc length from A is proportional to the arc length before and after D moves. AG/AE = AF/AD.

So the solution steps of IJ are:

AF = acos(HC / AC) * AC
AE = acos(JC / AC) * AC
AD = (PI / 2) * AC
AG = AE * AF / AD
IJ = AC * (cos(AG / AC) - cos(AE / AC))
Copy the code

This corresponds to the code:

float centerOffsetAngle = acos(maxCenterDistance / maxDistance);
float currentAngle = acos(distanceToCenter / maxDistance);
float currentOffsetAngle = currentAngle * centerOffsetAngle / (PI / 2.0);
float currentOffset = maxDistance * (cos(currentOffsetAngle) - cos(currentAngle));
Copy the code

In simple terms, the point displacement currentOffset is calculated according to the distance from the point to the center.

2. Resistance simulation

Because the pudding is elastic, after it deforms, it accumulates elastic potential energy. So the closer you get to the edge, the greater the drag. So when you’re in the middle, you’re moving faster, and when you’re on the edge, you’re moving slower.

The Easeout easing function is used here to simulate this effect. Unfortunately, no off-the-shelf functions are available in GLSL.

Let’s look at the equation y = 2 * x – x ^ 2, which looks like this:

As you can see, when x goes from 0 to 1, y goes faster and then slower. We can use it as an easy out buffer.

3. Amplitude attenuation

According to the law of conservation of energy, the pudding’s kinetic and elastic potential energies decay due to the loss of energy each time it is shaken. In other words, each wobble will be less than the last.

This is done by multiplying the amplitude by a reduction factor at the end of each wobble period. In addition, when the amplitude is less than a certain threshold value, it is directly set to 0, indicating that the state is back to rest.

The actual code is as follows:

model.amplitude *= 0.7;
model.amplitude = model.amplitude < 0.1 ? 0 : model.amplitude;
Copy the code

Input event processing

With the above steps, we now have a complete wobble animation. The final step is to make the animation respond to user input events.

In this step, what we want to do is convert the input event into a unit direction vector, which we then pass to the Shader to represent the sloshing direction.

Two kinds of input events are handled here: screen touch and accelerometer.

1. Touch events

When your finger touches the screen, determine whether the touch point is within the range of the target area. If yes, the direction of the unit vector is determined according to the direction of the finger when the finger moves.

The key codes are as follows:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    
    CGPoint currentPoint = [[touches anyObject] locationInView:self];
    currentPoint = CGPointMake(currentPoint.x / self.bounds.size.width, 1 - (currentPoint.y / self.bounds.size.height)); / / normalization
    for (MFWobbleModel *model in self.wobbleModels) {
        if ([model containsPoint:currentPoint]) {
            self.currentTouchModel = model;
            self.startPoint = currentPoint;
            break; }}} - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesMoved:touches withEvent:event];
    
    if (self.currentTouchModel) {
        CGPoint currentPoint = [[touches anyObject] locationInView:self];
        currentPoint = CGPointMake(currentPoint.x / self.bounds.size.width, 1 - (currentPoint.y / self.bounds.size.height)); / / normalization
        CGFloat distance = sqrt(pow(self.startPoint.x - currentPoint.x, 2.0) + pow(self.startPoint.y - currentPoint.y, 2.0));
        CGPoint direction = CGPointMake((currentPoint.x - self.startPoint.x) / distance, ((currentPoint.y - self.startPoint.y) / distance));
        [self startAnimationWithModel:self.currentTouchModel direction:direction amplitude:1.0];
        
        self.currentTouchModel = nil; }}Copy the code

2. Accelerometer

The details of how the accelerometer is used are not expanded here. We just need to add a monitor, and when the phone shakes, we can get the change of acceleration value in the callback, so as to determine the direction.

The key codes are as follows:

self.motionManager.accelerometerUpdateInterval = 0.1;  // Check every 0.1 seconds
__weak typeof(self) weakSelf = self;
[self.motionManager startAccelerometerUpdatesToQueue:[[NSOperationQueue alloc] init] withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) {
    CMAcceleration acceleration = accelerometerData.acceleration;
    CGFloat sensitivity = sqrt(pow(acceleration.x, 2.0) + pow(acceleration.y, 2.0));
    if (sensitivity > 1.0) {
        CGPoint direction = CGPointMake(acceleration.x / sensitivity, acceleration.y / sensitivity);
        for (MFWobbleModel *model in weakSelf.wobbleModels) {
            // The current amplitude is less than a certain threshold to be affected
            if (model.amplitude < 0.3) {
                [weakSelf startAnimationWithModel:model direction:direction amplitude:1.0]; }}}}];Copy the code

And now we have a perfect pudding.

Finally, go through the whole process:

The source code

Check out the full code on GitHub.

reference

  • Bessel curve _ Baidu Encyclopedia
  • Recommend a math tool site
  • Small analysis of slow motion formula

For a better reading experience, visit Lyman’s Blog. GLSL and pudding Shaking Art