Perspective distortion using OpenFL

We’re in the year 2018 and most of you are aware that Adobe’s Flash isn’t as popular as it used to be. In fact it has been superseded by HTML5. While there are many frameworks out there we could use to deliver HTML5 content, there’s a perfect choice for former AS3 coders called OpenFL.
I’m not getting into detail how to get started with OpenFL though. One of it’s drawbacks has always been missing functionality if you use it to deploy to anything other than Flash.
It’s maturing constantly though and the latest version, which as of writing is 8.0.2, brings a long awaited feature for the HTML5 target: the drawTriangles() method of the openfl.display.Graphics class. It has been included for some years yet but didn’t work properly.
In this blog post we’re utilizing drawTriangles() in conjunction with perspectiveProjection() to mimic the behaviour of Flash’s rotationX and rotationY properties.
In case you’ve never heard of these, here’s an excellent introduction over at Kirupa: https://www.kirupa.com/developer/as3/intro_3d_as3_pg1.htm

I want to make sure I don’t loose your attention yet, so let’s take a look at what we’ll achieve first. Move your mouse or your finger over the image to rotate it:

Did you notice? It actually isn’t Flash.

Prerequisites:

  • A texteditor or a full-blowing IDE like FlashDevelop for writing the Haxe code
  • Openfl version 8.0.0
  • Lime version 6.3.0
  • Haxe 3.4.3 or above

Let’s start by creating a new class called Sprite3D, which should be in the same folder as Main.hx.

I assume you’re familiar with the basics so I’ll just point out the important stuff.
As you can see our freshly created class accepts an input parameter of type DisplayObject. This can be a Sprite containing a Bitmap or a Sprite made up of vector art for example. In any case, this DisplayObject will be drawn onto a BitmapData, otherwise we wouldn’t be able to use it as a fill in our call to drawTriangles().
Add this two lines right after the call to super();

This creates a new BitmapData instance the size of our input DisplayObject. Math.ceil() makes sure we round to the next biggest integer in case the width and/or height respectively aren’t integers. To be able to rotate around the center of our DisplayObject, we need to move the coordinates of the corner points in such way that the center is at x=0 and y=0 of a cartesian coordinate system. This is simply done by shifting it horizontally by half it’s width to the left and vertically up by half it’s height. In 3D world these points are the vertices. A vertex is made up of x, y and z coordinates. The following array holds the vertices for the four corner points of our DisplayObject, starting clockwise from the top-left. Let’s add this line:

coordinatesPP

There’s more to consider when working in three dimensions. Maybe you already wondered why the main function we’re talking about here is called drawTRIANGLES. Yeah, in 3D everything is made up of triangles. To be able to use our rectangular texture, we need to divide it into two equal triangles, what simply is done by imaging a diagonal going from vertex 1 to vertex 3, like so:

didiveTriangles

How the hell do we tell it that there are actually two triangles? In fact, we already did! Scroll back and take a look at line 27 – we defined which vertices make up the triangles there:
0, 1, 3 the first triangle
1, 2, 3 the second triangle

Now that our rectangle knows it’s actually made up of two triangles, how does it know which part of it belongs to which part of the original DisplayObject? This is done by UV mapping, a method for texturing objects, where U is the horizontal and V the vertical position. Without a texture, we would just see two rotating triangles – what’s rather boring. In our case the texture is the BitmapData instance we created initially.
Let’s take a look at our first triangle once more. Plain speaking vertex 0 is at the top-left, vertex 1 is at the top-right and vertex 3 at the bottom-left. You won’t belive but you can transfer this knowledge to UV mapping, we just need to put ‘top-left’, ‘top-right’ and ‘bottom-left’ differently. What about percentages? Let’s try it:
top-left: 0% width / 0% height
top-right: 100% width / 0% height
bottom-left: 0%width / 100% height

That’s it! Instead of percentages we’re using a decimal number ranging from 0 to 1 though.
With this in mind, the UV data for the vertices of our first triangle would be 0, 0, 1, 0, 0, 1.
Unfortunately we’re not quite there yet. drawTriangles() expects a third value for each vertex: T. It represents the scale factor for the associated vertex. As you know, objects further away appear smaller. This is controlled by T. Luckily we don’t have to worry about it’s value as it will be filled by a nifty helper function Utils3D.projectVectors() as we finally render stuff to the screen. For the moment we just need to set it to 0. The complete UVT data for the four vertices that make up our two triangles is: 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0
If you scroll back once again, you can see we already defined the UVT data at line 26.

Let’s add five more lines and our constructor function is ready.

Beside initializing our PerspectiveProjection we’re also adding a Sprite called plane. This Sprite serves as a canvas we’re drawing our distorted texture onto.

Now it’s time to write the render function and getters & setters for the rotX and rotY properties, which control the rotation on the corresponding axis.
It’s pretty straight-forward. Each call to render() we reset a Matrix3D, apply the rotX and rotY values, get the 3D coordinates of our vertices using Utils3D.projectVectors() and finally draw it onto our canvas Sprite with the help of drawTriangles().

Let’s add the getters & setters and our Sprite3D class is ready!

As you can see, each time we set a Sprite3D’s rotX or rotY property, render() gets called. This makes sure we can see the result of changing these properties anyway.

The bulk of the work is done! It’s time for the fun part – let’s write our Main class which utilizes our new Sprite3D class!

Wow – we’re done!

Leave a Reply

Your email address will not be published. Required fields are marked *