Flipping sprites around X & Y axes (with perspective)


#1

Does anyone have a suggestion for how best to flip/rotate sprites around X & Y axes (with perspective)?

This is an example of the card-turning effect I’m trying to achieve using CSS (see demo): http://css3.bradshawenterprises.com/flip/

I’m assuming that this will require some form of 3D transform, but not sure how to go about it using Haxe/OpenFl.

Thanks in advance

Mark


#2

Hi Mark.

If you’re targeting Flash, it’s pretty simple. All you need to do is applying the rotationY property to a DisplayObjectContainer, e.g. a Sprite with a Bitmap child.

Things are a bit different though if you want to deploy to HTML5 because rotationX, rotationY and rotationZ aren’t implemented for this target,
Luckily with recent improvements to perspectiveProjection() and drawTriangles() there’s a way to do it with HTML5 too. It’s just a bit more complicated.
I wrote a quick sample for you. Please note that it assumes there’s a quadratic image called img.jpg in your assets/img folder. Furthermore it needs OpenFL 8.0.1 - I think previous versions didn’t calculate the perspective correctly.

package;

import openfl.display.Sprite;
import openfl.display.BitmapData;
import openfl.display.Bitmap;
import openfl.display.Graphics;
import openfl.display.TriangleCulling;
import openfl.geom.*;
import openfl.Vector;
import openfl.events.Event;
import openfl.Assets;

class Main extends Sprite 
{ 
    private var mat:Matrix3D = new Matrix3D();
    private var vertices:Vector<Float>=Vector.ofArray([-20, -20, 0, 20, -20, 0, 20, 20, 0, -20, 20, 0.0]);
    private var uvt:Vector<Float>=Vector.ofArray([0.0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0]);
    private var indices:Vector<Int>=Vector.ofArray([0, 1, 3, 1, 2, 3]);     
    private var projectedPoints:Vector<Float>;
    private var plane:Sprite = new Sprite();
    private var pProjection:PerspectiveProjection = new PerspectiveProjection();
    private var proj:Matrix3D;
    private var texture:BitmapData;
    private var angle:Int = 0;
	
    public function new() 
    {
        super();
        projectedPoints = new Vector<Float>(Math.ceil(vertices.length / 3 * 2));
        addChild(plane);
        plane.x = stage.stageWidth / 2;
        plane.y = stage.stageHeight / 2;
        pProjection.fieldOfView = 45;
        proj = pProjection.toMatrix3D();    
	texture = Assets.getBitmapData("img/img.jpg");
	addEventListener(Event.ENTER_FRAME, loop);
    }

    private function loop(e:Event):Void
    {
        mat.identity();
        mat.appendRotation(angle, Vector3D.Y_AXIS);
        mat.appendTranslation(0, 0, 100);
        mat.append(proj);    
        projectedPoints = new Vector<Float>();
        Utils3D.projectVectors(mat, vertices, projectedPoints, uvt);
        plane.graphics.clear();
        plane.graphics.beginBitmapFill(texture, null, false, false);
        plane.graphics.drawTriangles(projectedPoints, indices, uvt, TriangleCulling.NONE);
        plane.graphics.endFill(); 
	angle++;
    }    
}

#3

Thanks for this obscure,

The code is impressive and the 3D card flipping effect works well (after I updated to OpenFl 8.0.1 as you suggested). I’m trying to modify the code to have different images on each side of the ‘cards’…

However, as the image is turning, there is quite a lot of flickering and shifting pixelation.

It also turns quite slowly compared to the CSS example above (I increased my frame rate from 30 to 60 fps in the project.xml but that didn’t seem to have any effect), and the speed makes the pixelation more noticeable.

Do you think that there is any way this could be speeded up?
Would there be a way to apply easing in & out to this?

Thanks again

Mark


#4

Hey Mark.

In my sample code above, the speed of the rotation is controlled by the variable angle. To speed things up, you could simply increment it in bigger steps than just by one each frame. If the rotation looks jerky, try a release instead of a debug build.

Well, I was a little bored so here’s a more sophisticated example - including different images for both sides of the card.

package;

import openfl.display.Sprite;
import openfl.display.BitmapData;
import openfl.display.Bitmap;
import openfl.display.Graphics;
import openfl.display.TriangleCulling;
import openfl.geom.*;
import openfl.Vector;
import openfl.events.Event;
import openfl.Assets;

import openfl.display.FPS;
import motion.*;
import motion.easing.*;

class Main extends Sprite 
{ 
    private var mat:Matrix3D = new Matrix3D();
    private var vertices:Vector<Float>=Vector.ofArray([-20, -20, 0, 20, -20, 0, 20, 20, 0, -20, 20, 0.0]);
    private var uvt:Vector<Float>=Vector.ofArray([0.0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0]);
    private var indices:Vector<Int>=Vector.ofArray([0, 1, 3, 1, 2, 3]);     
    private var projectedPoints:Vector<Float>;
    private var plane:Sprite = new Sprite();
    private var pProjection:PerspectiveProjection = new PerspectiveProjection();
    private var proj:Matrix3D;
    private var textureA:BitmapData;
    private var textureB:BitmapData;    
    private var angle:Int = 0;
    
    public function new() 
    {
        super();
        projectedPoints = new Vector<Float>(Math.ceil(vertices.length / 3 * 2));
        addChild(plane);
        plane.x = stage.stageWidth / 2;
        plane.y = stage.stageHeight / 2;
        pProjection.fieldOfView = 45;
        proj = pProjection.toMatrix3D();    
        textureA = Assets.getBitmapData("img/imgA.jpg");
        textureB = Assets.getBitmapData("img/imgB.jpg");
        var fps:FPS = new FPS(10,10,0xff0000);
        addChild(fps );
        flip(3);
    }
    
    private function flip(state:Int):Void
    {
        switch(state)
        {
        case 0:
            Actuate.update (spin, 1, [90, textureB], [180, textureB]).ease(Quad.easeOut).onComplete(flip, [1]);    
        case 1:
            Actuate.update (spin, 1, [180, textureB], [270, textureB]).ease(Quad.easeIn).onComplete(flip, [2]);    
        case 2:
            Actuate.update (spin, 1, [270, textureA], [360, textureA]).ease(Quad.easeOut).onComplete(flip, [3]);    
        case 3:
            Actuate.update (spin, 1, [0, textureA], [90, textureA]).ease(Quad.easeIn).onComplete(flip,[0]);
        }
    }
    
    private function spin(degrees:Int,texture:BitmapData):Void
    {
        mat.identity();
        mat.appendRotation(degrees, Vector3D.Y_AXIS);
        mat.appendTranslation(0, 0, 100);
        mat.append(proj);    
        projectedPoints = new Vector<Float>();
        Utils3D.projectVectors(mat, vertices, projectedPoints, uvt);
        plane.graphics.clear();
        plane.graphics.beginBitmapFill(texture, null, false, false);
        plane.graphics.drawTriangles(projectedPoints, indices, uvt, TriangleCulling.NONE);
        plane.graphics.endFill(); 
    }    
}

#5

Thank you very much obscure,

That works really smoothly (and FPS = around 60 with HTML5 release build).

Not sure I understand all the workings of the code though (that’s pretty smart stuff), but I’m looking into it…

Best

Mark