Close

The shear revolution

A project log for HTML5 Retro Game Engine(s)

A Side project where I try and build various scriptable retro game engines in the browser.

timescaleTimescale 04/11/2020 at 16:340 Comments

While it might look like I’m easily distracted and roam from project to project without actually ever finishing any of them, you might have a point but there is some method to my madness. The main objective is to create a toolkit of functions that are easy to implement as building blocks for any sort of retro game. Simple functions like drawing lines or boxes to assets or the screen and functions that can do a whole lot more like scaling, animating “sprites” and other effects.

These function have to be simple, fast and modular. If I build a effect in the adventure engine, it must also be usable in Shovelization and visa versa. Some game paradigms lend themselves better for certain functions than others and that is the reason why I have forked the project into several other game/app types. I now have the Adventure engine, the platform, the top down strategy platform, several audio and input experiments and an asteroids/city command type of environment.

The asteroids platform named “ARRRtype” (Because I imagined something with space pirates) is based on a fire effect with multiple draw layers. I put a random spaceship in there and quickly made it “fly” and rotate. It was a natural evolution for this branche to have the ability to rotate graphics.

The first rotation however wasn’t what I needed. I did a simple source to destination transformation that looked like this.

x = Math.round((Math.cos(rotate) * (tileX - centerx)) - (Math.sin(rotate) * (tileY - centery)) + centerx);
y = Math.round((Math.sin(rotate) * (tileX - centerx)) + (Math.cos(rotate) * (tileY - centery)) + centery);

 For every pixel, this calculation has to be made and what you get is a moire mess.. I knew this, but I thought I’d write something better later. I didn’t think it would be that difficult. I understand the principle of using “destination to source” transformation which always results in a pixel to be plotted. I use this type of transformation in a pinch-punch and swirl effect that I have in the same toolbox.

Quickly I realised that for big graphics or many graphics at once, this was quite a costly operation. Then I stumbled on the notion of rotating by shearing the image. While it took me some time to wrap my head around it and adapt this method for full 360 rotation, it ended up pretty fast and versatile. You can read about the principle here : https://www.ocf.berkeley.edu/~fricke/projects/israel/paeth/rotation_by_shearing.html

But the essence is that by shearing the image 3 times, you can rotate a bitmap with all pixels intact. At the beginning you need to calculate a couple of values and then use these in the drawing function to calculate the displacement of each pixel. This is the way I implemented the Alpha en Beta shear values:

aShear = -Math.tan(rad/2).toFixed(2)
bShear = Math.sin(rad).toFixed(2)

And then for all pixels, the following cascade of transformation actually gives the new location.

destX = tileX + Math.floor(aShear * tileY);
destY = tileY + Math.floor(bShear * destX);
destX = destX + Math.floor(aShear * destY);

While on a per-pixel level, basically these are the operations that are done in order to rotate the original bitmap.

Now there are some issues I found with this method. I could not do a full 360 with this method. -90 and 90 degrees (or -100 and 100 rad) worked fine, but out of those boundaries, the image would distort and completely shatter at the top end. This issue was easily resolved by simply mirroring the X and Y axis while doing the same rotation for the lower angles. I’m not sure how neat my solution here was, but for now it works.

Then there was the problem of rotating around the center. The simple rotation method or a dest-to-source method automatically rotates around the center, which is what you’d want in many cases. The shear method however rotates around the top right corner of the image. To make it rotate around another point, you need to find the offset between the pivot point you want and the axis around which the shear method rotates. This is where I like this method more than others because it is trivial to assign a pivot point. All you need to do is use the same cascade calculation on the pivot point and calculate the difference. This could be the center, but in reality it could be any point in or outside the graphic. With a bit of code to account for the image flip, the rotation is flawless. 

 

Top left is the ship rotated with the shear method. The bottom right rotation is the old method with the source to destination method. It is like a piece of Swiss cheese.

Here is the entire function :

    function drawSprite(x,y,deg,tileName){

        var tileID = assetPointer[tileName],
            centerX = Math.round(sceneGraphics[tileID].width / 2),
            centerY = Math.round(sceneGraphics[tileID].height / 2);

        // needed for full 360 rotation with shear transforms.    
        if (deg > 270){ 
            deg -= 360; 
        }     
        if (deg < 90){
            var rad = deg2rad(deg)
        } else {
            var rad = -deg2rad(180 - deg)
        }

        var destX,
            destY,
            aShear = -Math.tan(rad/2).toFixed(2),
            bShear =  Math.sin(rad).toFixed(2),
            curPixel,
            destScreenPixel;

        // shear transform just the center
        var xDisplace = centerX + Math.floor(aShear * centerY),
            yDisplace = centerY + Math.floor(bShear * xDisplace),
            xDisplace = xDisplace + Math.floor(aShear * yDisplace),
            xDiff = centerX - xDisplace,// get the xoffset difference
            yDiff = centerY - yDisplace;// get the yoffset difference

        // Draw tile at position on board
        for(tileX = 0; tileX < sceneGraphics[tileID].width; tileX++){
            for(tileY = 0; tileY < sceneGraphics[tileID].height; tileY++){
                // the 3 shear transformation for the rotation
                destX = tileX + Math.floor(aShear * tileY);
                destY = tileY + Math.floor(bShear * destX);
                destX = destX + Math.floor(aShear * destY);        

                // Include the offset for the pivot point.

                if (deg >= 90 && deg <= 270){
                    // mirror the shear rotation on both axis for full rotation & set pivot point.
                    destX = (destX * -1) - xDiff;
                    destY = (destY * -1) - yDiff; 
                } else {
                    // set pivot point for the other half
                    destX += Math.floor(xDiff - sceneGraphics[tileID].width);
                    destY += Math.floor(yDiff - sceneGraphics[tileID].height)-2; 
                }
            
                curPixel = (tileY *  sceneGraphics[tileID].width + tileX) * 4;
                destScreenPixel = ((destY + y - 1) * 320 + (destX + x - 1)) * 4;

                if (sceneGraphics[tileID].data[curPixel + 3] > 0){
                    imgdata.data[destScreenPixel + 0] = sceneGraphics[tileID].data[curPixel + 0]; 
                    imgdata.data[destScreenPixel + 1] = sceneGraphics[tileID].data[curPixel + 1]; 
                    imgdata.data[destScreenPixel + 2] = sceneGraphics[tileID].data[curPixel + 2];
                }
            }    
        }
    }
        

And a short demo

Discussions