iBooks翻页原理-圆锥变形

1. http://wdnuon.blogspot.com/2010/05/implementing-ibooks-page-curling-using.html

One of iPad's most talked about apps is Apple's own iBooks e-reader. Perhaps its most eye-catching but completely superfluous feature is the beautiful, dynamic page curling effect that follows your finger naturally as you drag to turn pages. Unlike cheap implementations using simple masks and gradients, iBook's page curling is very realistic, with content that bleeds through and deforms accurately along the curve of the page. Shortly after the iPad announcement Steven Troughton-Smith discovered the page curl effect hiding inside a private API but it offers few cues to the actual implementation.

This article aims to demonstrate one technique that Apple may have used to implement their page curling behavior but doesn't assert that they did. Instead of a 3D approach through OpenGL it's quite possible that they may have simply used Core Image and some fancy math to implement everything entirely in 2D, something hinted at by the fact their page flattens out for large deformations rather than exhibiting a uniform curvature. Whether or not this is the same algorithm Apple uses, it's a simple, elegant solution that works well from all viewing angles, not just straight on.

Background

I was particularly intrigued when I first saw iBooks' dynamic page curling because several years ago I developed a system for kiosk-based virtual books. Using a touchscreen, users can browse a rare book by swiping with a finger while the priceless original remains protected in a climate-controlled case. During the initial stages of the project I experimented with various methods of handling the page turning. High visual quality followed by performance were the top criteria. Unfortunately realtime 3D didn't cut it on the hardware and software available at the time. While it would have been nice to have had dynamic page curling, in the end we opted to use pre-rendered bitmaps of each page turn for the sake of image quality. Needless to say, it was a lengthy and expensive process but yielded the best looking results by far.

Fast forward a few years into the future, and graphics hardware was advancing faster than computers in general. A new, high-performance 3D development tool called Unity had just been released and I was quite blown away by its capabilities. On a whim I decided to revisit dynamic page curling to see if then current technology could deliver acceptable results. I was pleasantly surprised with what I could accomplish in less than a day, and you can view the proof-of-concept online (installs Unity plugin if needed, no restart required), or as a stand-alone Mac application (11.8 MB). The interface isn't pretty, but it gets the job done.

The math behind page curling

Many people have tackled the job of modeling a page curl dynamically in 3D. Some early attempts treated a page as a soft body, utilizing a mass-springs system to calculate all the internal and external forces applied to each point in a mesh. While this gives realistic results for supple, cloth-like objects, paper is by nature stiff and rigid and normally bends in a predictable manner. In addition, mass-springs models are very computationally expensive and may exhibit compression or stretching, effects unwanted when modeling stiff paper. By thinking of a page curl as a geometric transformation, we can greatly simplify our calculations.

It turns out some clever scientists at Xerox PARC did in fact do this and released a research paper in 2004. [1] Their brilliantly simple solution consists of thinking of a page curl as an imaginary cone of changing dimensions, around which the page wraps itself as the cone rolls across the surface of the book. It's probably easier to visualize once you've read the paper. Go ahead, it's only a single page PDF. Now grab a nearby book and curl a page yourself. Neat, isn't it?

The math presented in the research paper may seem daunting at first but it's actually just basic algebra and trigonometry. I won't attempt to explain it any better than the authors because they already do a pretty good job and I suck at math anyway. What's important is that they have come up with a set of functions that map any arbitrary 2D point P(x, y) on a flat page to its corresponding 3D point T(x, y, z) in space given cone parameters θ and A. Go review the math before reading on, if you haven't already.



θ represents the cone angle and can range from {0…π/2} (90° for those who prefer to think in degrees over radians). The smaller the value, the skinner the cone; at zero, the cone degenerates into a ray and makes the page virtually disappear. At the other extreme, the cone is infinitely wide, which maps to a perfectly flat page with no curl whatsoever.

A represents the cone apex. A value of zero puts the apex at the origin while larger and smaller values translate the apex farther away along the y axis (up and down the spine of the book). Very large values of A (approaching infinity) combined with small values of θ produce an extremely elongated cone, or essentially a cylinder. As θ becomes smaller, the page begins to wind in upon itself, which may be an interesting way of implementing a scroll-like effect.

The third and final parameter ρ represents the rotation of the page around the y axis, or the spine of the book. As mentioned in the paper, a complete page turn comprises both a deformation and rotation component. While we could combine both processes into one step, for illustrative purposes it's easier to keep them separate and apply ρ separately as a basic rotation transform.

Ok, enough with the math, let's see some…

Examples



If you don't have Xcode installed, take a look at this Grapher example. Grapher is a wonderful yet little-known graphing application lurking in the Applications/Utilities folder. This example includes only the page deformation equations. Rotation is omitted because Grapher does not support 4x4 matrices, which are needed for 3D affine transformations. (It's possible to implement through matrix expansion but I don't want to clutter up the math any more than necessary.)

The equations are taken pretty much straight from the PARC research paper, though I had to substitute d for r and h for θ since Grapher reserves r and θ for drawing in polar coordinates. Click on the little triangle next to t to play the page curl animation. The effect is very basic since θ and A are defined here as linear functions of t.

You can adjust the values of θ and A manually but it's a little tricky since Grapher allows only one parameter to be animated at a time. To do this, select t at the top of the equation list then click on the square stop button above the slider. Select either of the next two equations for θ or A and delete the t variable. Press return to confirm the edit then select Equation—>Animate Parameter from the menu. Click on the settings button to adjust the parameters to your liking. Go ahead and play around with it for a while before coming back here.

If you didn't already, you may want to check out my original proof-of-concept Unity demo. I've recreated a similar version for iPad using OpenGL ES 1.1 ( 1.3 MB Xcode project). After compiling and running the app, you should see a page continually flipping from right to left, with sliders that adjust for the current parameter values. Stop the animation by clicking on the Animate switch, or grabbing any one of the sliders (if you can!).



The Time slider automatically calculates appropriate values of rho, theta, and A for time t to generate a decent looking page flip animation. This is acceptable for an automatic, non-interactive page flip, say for when the user taps or swipes the page to quickly go to the next one. However, to generate a page curl that dynamically tracks the user's finger requires calculating appropriate values of rho, theta, and A from the touch location as a 2D coordinate. That requires a bit more complicated math and isn't included yet, but will hopefully be addressed in a future article.

While the animation is stopped, drag the rho, theta, and A sliders to see how they affect the geometry of the curl.

Code

The real meat of the code is in the CCPage class, specifically the -deform method which performs all the magic in a scant few lines of code, in fact just six lines for the actual deformation process! The method takes no arguments because rho, theta, and A are properties of CCPage. Just set them before calling -deform. Take a close look and feel free to step through it in the debugger. There's plenty of inline documentation to guide you through.

  
  
  
  
- ( void)deform
{
  Vertex2f   vi;   // Current input vertex
  Vertex3f   v1;   // First stage of the deformation
  Vertex3f *vo;   // Pointer to the finished vertex
CGFloat R, r, beta;
  for ( ushort ii = 0; ii < numVertices_; ii++)
  {
    // Get the current input vertex.
    vi    = inputMesh_[ii];                       
    // Radius of the circle circumscribed by vertex (vi.x, vi.y) around A on the x-y plane
    R     = sqrt(vi. x * vi. x + pow(vi. y - A, 2)); 
    // Now get the radius of the cone cross section intersected by our vertex in 3D space.
    r     = R * sin( theta);                       
    // Angle subtended by arc |ST| on the cone cross section.
    beta  = asin(vi. x / R) / sin( theta);       
   
    // *** MAGIC!!! ***
    v1. x  = r * sin(beta);
    v1. y  = R + A - r * ( 1 - cos(beta)) * sin( theta); 
    v1. z  = r * ( 1 - cos(beta)) * cos( theta);
    // Apply a basic rotation transform around the y axis to rotate the curled page.
  // These two steps could be combined through simple substitution, but are left
    // separate to keep the math simple for debugging and illustrative purposes.
    vo    = & outputMesh_[ii];
    vo-> x = (v1. x * cos( rho) - v1. z * sin( rho));
    vo-> y =  v1. y;
    vo-> z = (v1. x * sin( rho) + v1. z * cos( rho));
  }  
}
This algorithm simply iterates through each vertex in our input mesh, deforming it according to properties rho, theta, and A, and puts the result in our output mesh, which is fed to our OpenGL renderer for display. Since each vertex is completely independent of any other, this algorithm is a prime candidate for parallelization, such as with blocks or OpenGL ES 2.0 vertex shaders.

The rest of the project is mostly OpenGL and application infrastructure hacked together from Xcode templates and demos so don't look to it as a shining example of good application design, but feel free to poke around if you want.

Notes

The code is intentionally verbose for illustrative purposes and far from optimized. The intermediate variables may be handy when stepping through with the debugger. Look forward to future articles where I revisit parts of the code for cleanup, optimization, and adding additional features such as lighting and shadows, triangle strips, blocks, and OpenGL 2.0 support. (Triangle strip support added and enabled with USE_TRIANGLE_STRIPS in CCCommon.h.)

Turning a page in real life usually does not form perfect conical shapes as in this algorithm. They'll typically be elliptical cones or some other irregular shape, sometimes with two curves if the fingers are pinching the corner of the page. For most intents and purposes though this algorithm gives results that are Good Enough, with an almost trivial implementation.

Part of the reason this writeup took a while is that I abandoned my original algorithm that modeled a page as a solid 3D cuboid. This approach could account for the general but rare case where the thickness should be visibly noticeable, say when flipping a large chunk of pages at once in a telephone directory, but for most practical purposes this was highly unlikely. Therefore I decided to rewrite the code to model a page as two independent coplanar meshes to represent the front and back faces. This greatly simplifies the math and texture mapping but can only handle pages of zero thickness (which is perfectly fine for the vast majority of cases).

I'm still relatively new to OpenGL and Objective-C in general, so I welcome any tips, suggestions, or corrections. Thanks for taking the time to read my first blog post. Hope you enjoyed it!


2: http://blog.flirble.org/2010/10/08/the-anatomy-of-a-page-curl/

As I was developing the ABC of the Sea App, which is an interactive picture-book based on a proof-of-concept Tara (my wife) made many years ago and did not publish, one of the early and obvious items on the checklist of cool things it should do is have a nice curling transition when moving between pages. Initially I expected this to be easy – Apples Apps do this, iBooks on the iPad especially, so it’s built in, right?

Wrong. Well, mostly wrong. I quickly discovered that the only documented curling transition for a UIView goes up/down, not left/right. It’s the one that Maps uses. Some Googling then revealed that an undocumented transition did perform left/right curling. However, being undocumented means it’s not viable for an App Store submission. It may later acquire an official constant and become kosher, but not yet.

My quest continued. I came across a number of implementations but most of them were lame approximations of a proper page curl. The most promising one was by CodeFlakes that promised very good page-corner-tracking using touch events, so a user could drag the corner around, but it didn’t attempt to curl – it used shadows and gradients to fake the effect. When I queried this they said nobody had asked for real curling.

Page deformation

Then I came across this blog post by W. Dana Nuon: Implementing iBooks page curling using a conical deformation algorithm which was pretty close to what I wanted, except it used OpenGL – as yet an unknown quantity to me. I downloaded the code, examined it, tried to ignore the mathematics involved and eventually decided to use the concepts from it in my code. This took a while and I ended up with acceptable results, at least to be going on with. I parked the issue since I had working code and I wanted to focus on other aspects of the project.

Eventually I came back to it after I was working on some code to pre-render pages that touched the OpenGL code. As I dug in to it my confidence grew and then I finally decided to brave the math since I wanted to develop a touch-tracking mechanism, to drag the page around.

Cone Parameters © PARC

The basis of the conical deformation came from a research paper published by Xerox PARC on the topic where they used a cone with parameters that varied over the course of the transition as an approximation of a page turn. The model assumes a cone, apex-down, with a vertex running from that apex on the y-axis (that is, x=0). The y coordinate of the apex is a variable, A. The other cone parameter is the angle, θ, between the surface of the cone and a vertex that runs from the apex through the center of the cone. They then add a third parameter, ρ, which is the rotation of the entire page about the y-axis. The PDF from PARC linked above has a reasonably descriptive diagram showing this arrangement which I have shown here. Nuon then empirically determined a heuristic based on time t that varied these parameters to produce a reasonable animation.

The original algorithm for mapping a point onto the surface of a cone, from Nuon’s sample code and taken directly from the mechanism described by PARC, looked like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    R     = sqrt (vi.x * vi.x + pow (vi.y - A, 2.0f ) ); // Radius of the circle
                                                    //circumscribed by vertex
                                                    //(vi.x, vi.y) around A on the
                                                    //x-y plane.
    r     = R * sin (theta );                           // From R, calculate the
                                                    //radius of the cone cross
                                                    //section intersected by our
                                                    //vertex in 3D space.
    beta = asin (vi.x / R ) / sin (theta );             // Angle SCT, the angle of the
                                                    //cone cross section subtended
                                                    //by the arc |ST|.

    v1.x = r * sin (beta );
    v1.y = R + A - r * (1.0f - cos (beta ) ) * sin (theta ); // *** MAGIC!!! ***
    v1.z = r * (1.0f - cos (beta ) ) * cos (theta );
 

The short version of what this does is assume that the distance from the apex of the cone to the 2D location of a point on the flat page is the same as the distance from the apex to where that point would be when mapped onto the cone. Some simple geometry gives you a short cut to then work out the angle around the cone that this point ends up.

When I started to dissect this algorithm, ostensibly to invert the equations, I came across some significant hurdles. The most significant one was that the cone deformation, as implemented, had a serious limitation of where it would position the corner of the page in the view-port. There were two major elements to this limitation:

  1. The entire page was mapped to a position on the cone. There were no flat parts. To get the corner of the page into some locations, you ended up either having the page curl back in on itself slightly or you ended up with a large z-component which looked odd.
  2. The parameters necessary to achieve some corner locations required extreme values and these were hard to develop a heuristic for.

After messing around with a sheet of paper for 30 minutes and after examining some sketches of a page curl I decided I could overcome most of these limitations if I could introduce flat-parts into the deformation. In effect, I wanted to add an x-component to the position of the apex of the cone and limit how far around the cone a page would curl. It was time to dust off the trigonometry that I had not used in twenty years.

Moving the apex

Using my imagination, I called the new parameter B (meaning that {x,y} of the apex would be {B,A} in the code) and once I realised it was a small matter of offsetting in the right places and conditionally transforming a point, ended up with this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    R = sqrt ( pow (vi.x - B, 2.0f ) + pow (vi.y - A, 2.0f ) ); // Radius of the circle
                                                          //circumscribed by vertex
                                                          //(vi.x, vi.y) around A
                                                          //on the x-y plane.
    r = R * sin (theta );                                   // From R, calculate the
                                                          //radius of the cone
                                                          //cross section
                                                          //intersected by our
                                                          //vertex in 3D space.
    if (vi.x < B ) beta = 0.0f;
    else beta = asin ( (vi.x - B ) / R ) / sin (theta );       // Angle SCT, the angle
                                                          //of the cone cross
                                                          //section subtended by
                                                          //the arc |ST|.

    if (vi.x < B ) v1.x = vi.x;
    else v1.x = B + r * sin (beta );

    if (vi.x < B ) v1.y = vi.y;
    else v1.y = R + A - r * (1.0f - cos (beta ) ) * sin (theta );

    v1.z = r * (1.0f - cos (beta ) ) * cos (theta );
 

The effect of this code is to provide a flat page prior to the cone, which starts at x=B. Experimentally I found that the useful range of θ was about 18 to 30 degrees. I then wrote some code to map an arbitrary {x,y} point to the parameters for the curl and, though it was better than for the stock deformation, it was not perfect:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    CGFloat h = sqrt ( (x * x ) + (y * y ) );
    if (h > 1.0f ) {                     // eep - map to somewhere we might actually
                                        //be able to get to
        CGFloat e = (h - 1.0f );
        x -= e;
        y -= e;
    }
    x *= width;
    y *= height;

    // Attempts to set the deform parameters to something that would put
    // the corner of the page in the given position.

    CGFloat aRho = 0, aA = -15.0f, aB = 0.0f, aTheta = M_PI / 2;

    CGFloat wx = sqrt ( (x * x ) + (y * y ) );
    wx *= 0.7;
    wx *= x;
    wx += 0.3f;

    aTheta = DEGREES_TO_RADIANS (funcLinear (y, 30, 18 ) );

    // Initial part of turn is done mostly by B
    aA   = -0.06f;
    aB   = wx;
    aRho = 0.0f;

    rho   = aRho;
    theta = aTheta;
    A     = aA;
    B     = aB;
 

In particular, though the corner does move with the touch location, it’s not directly under it. I decided this was a matter of refinement but, more pressing, there was still a significant limitation to where it could place the corner of the page – it needed to flatten the page after the cone in order to reach more places.

This proved to be rather tricky. In order to do this, you need to transform the points relative to the last position they would have had on the cone. All of the math discovered this location in terms of the distance from the apex, which is a function of the original 2D position. Not something that can be easily inferred from an arbitrary point.

Would a tube work?

Instead of going down a path of nasty trigonometry, I decided to attack it from a different angle. Instead of a cone, why not a cylinder whose point of rotation was freely moveable? The geometry involved would be much simpler – and it should be pretty easy to reverse, too. I went and found a kitchen paper-towel tube and a sheet of paper and convinced myself it was a reasonable model.

Cylinders are pretty easy objects to draw in 3D – it’s just a 2D circle, in our case with radius C, that you transform into 3D and then extrude. A bit trickier is using this simple model and wrapping a page around it – this is because the page doesn’t simply follow the cylinder around in a circle, it’s an elipse and for a given plane in the 2D page, one complete revolution around the tube does not end up at the same point it started – just wrap a page around a tube to see. The page travels up the tube when it’s at an angle.

Initially I thought that the angle formed by the path leading into the cylinder and the path leading out was 2θ but that ended up being too simplistic, not least because I inadvertently changed the meaning of θ to be the angle between the x-axis and the vertex that ran along the face of the cylinder at z=0. What I ended up with was .

The code to perform this deformation ended up reasonably clean, if not as simple as the conical one looked:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#define CYL_RX(C, b)              (C * sin(b))
#define CYL_DY(C, d)              (C * (d > 2.0f ? 2.0f : d))
#define ROT_X(xin, yin, angle)    ((xin * cos(angle)) - (yin * sin(angle)))
#define ROT_Y(xin, yin, angle)    ((xin * sin(angle)) + (yin * cos(angle)))

    // theta is angle of cylinder in x,y plane
    // A,B are y,x of "bottom" of cylinder
    // C is radius of cylinder

    // Take Y and work out the corresponding X for the start of the cylinder
    sx = B + ( (vi.y - A ) / tan (theta ) );

    // amount of flat x-travel around cylinder
    cx = (C * M_PI ) * sin (theta );

    // How far round the cylinder we are
    tx = vi.x - sx;
    if (tx < 0.0f ) tx = 0.0f;
    else if (tx > cx ) tx = cx;

    // Excess of x after the cylinder. -ve if not finished
    xx = vi.x - (cx + sx );

    if (vi.x < sx ) {
        // Flat before cylinder
        v1.x = vi.x;
        v1.y = vi.y;
        v1.z = 0.0f;
    }
    else if (xx > = 0.0f ) {
        // Flat after cylinder
        beta = M_PI;
        dy   = tan (M_PI_2 - (theta * 1.0f ) );

        // Construct a vertical cylinder
        v0.x = CYL_RX (C, beta );
        v0.y = CYL_DY (C, dy );

        // Rotate...
        v1.x = ROT_X (v0.x, v0.y, theta - M_PI_2 );
        v1.y = ROT_Y (v0.x, v0.y, theta - M_PI_2 );

        // Extend by the excess
        v0.x = xx;
        v0.y = 0;

        // Rotate...
        v1.x += ROT_X (v0.x, v0.y, theta * 2.0f );
        v1.y += ROT_Y (v0.x, v0.y, theta * 2.0f );

        // Translate into place
        v1.x += sx;
        v1.y += vi.y;

        v1.z = C * 2.0f;
    }
    else {
        // Curl around cylinder
        beta = (tx / cx ) * M_PI;
        // Lateral travel along cylinder
        dy = tan (M_PI / 2.0f - (theta * 1.0f ) ) * (tx / cx );

        // Construct a vertical cylinder
        v0.x = CYL_RX (C, beta );
        v0.y = CYL_DY (C, dy );

        // Rotate...
        v1.x = ROT_X (v0.x, v0.y, theta - M_PI_2 );
        v1.y = ROT_Y (v0.x, v0.y, theta - M_PI_2 );

        // Translate into place
        v1.x += sx;
        v1.y += vi.y;

        v1.z = C * (1.0f - cos (beta ) );
    }
 

Now this is not perfect either, in particular, for values of approaching there is a small tendancy for the page to curve below the y-axis a little. This I hope to remedy later. It’s also straightforward to curl from a different corner, or even an edge, thanks to the complete flexibility of the rotation point of the cylinder. Significantly, however, the logic to map an {x,y} location to deformation parameters proved, if not straight forward then very achievable.

But can you touch it?

By looking at a sketch of three phases of the cylindrical deformation it became clear that there was a single equation that could involve all the parameters to hand. I called the distance along the bottom edge of the page that was after the cylinder h, the distance along that edge that was wrapped around the cylinder c and the distance along the bottom edge, on the x-axis, that runs from where the page leaves the x-axis back to the x-coordinate of the corner of the page being dragged Δ.

Page Curl Phases


With this, it was apparent that , that and that . With substitution, you end up with:

and you know what? It works. Calculating θ was simple pythagoras and I ended up with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    A   = 0.0f;
    rho = 0.0f;
    C   = 0.02f;

    // How much is lost in the curve?
    CGFloat dc = (C * M_PI * 0.5f );

    // Funky!
    CGFloat dx =
        (y * y - x * x +
    (2.0f - 2.0f *
    dc ) * x - dc * dc + 2.0f * dc - 1 ) / (2.0f * x + 2.0f * dc - 2.0f );

    // Limits on the range of B
    CGFloat mx = dx - dc;
    if ( (x + mx ) < = 0.0f ) {
        dx = - (x + dc );
        mx = -x;
    }
    B = (x + mx ) * width;

    // And since we now have a right triangle, the angle is...
    if (dx == 0.0f ) theta = M_PI_4;
    else {
        theta = atan (y / fabs (dx ) );
        theta = (M_PI - theta ) / 2.0f;
        if (dx < 0 ) theta = M_PI_4 - (theta - M_PI_4 );
    }
 


And there we have it. There are further refinements to make – for θ approaching  our OpenGL vertices have a problem coping and the edge of the curl looks ragged. There’s the problem noted above with the curl dipping below the x-axis. And for larger values of c the touch tracking code loses accuracy. But these can all be worked on.

你可能感兴趣的:(Algorithm,Math,xcode,Parameters,transition,distance)