2D Sprite Affine Transformations

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hello in this video I thought we'd take a look as a fundamental component of all 2d engines the ability to transform your sprites but before we get started I'm just going to remind you about the one line code at code Jam 2018 if you haven't signed up check the link below it starts next week and that's when I'll be releasing the theme and the theme of it is going to be well of course you're gonna have to wait for that until next week anyway we're going to be looking at rotating sprites well not just rotating them but providing all sorts of different types of transformations to sprites so here I've got a surprise of a car and I can rotate it I can also change the scale of it in both axes to make it a bit bigger again so we don't lose it there we go and I can do a sheer transform as well she is are a bit weird I don't know how useful they are but they can be used for sort of wobbly effects but certainly the ability to rotate and position and scale sprites on a screen is really important and so in this video I want to introduce the code to implement an affine transform let's get started for this video I'm going to use the one line coder pixel game engine and I've already created the bare bones of what's required here I've overridden the pixel game engine class with a base class called sprite transforms and that will provide two functions on user create and on user update on user create is called once and on user update is called every single frame I'll create an instance of this derived class in my main function and the size of the pixel game engine window I'm creating is going to be 256 pixels by 240 where each pixel in game engine space is 4 by 4 pixels on my screen space since I started with the console game engine and now the pixel game engine the concept of a sprite has been quite fundamental so I've added a variable called SPR car it's a pointer to a type of sprite and in on user create I'm going to load a PNG file into that sprite object drawing the sprite to the screen is quite a routine operation firstly I'm going to clear this screen entirely to dark cyan in this case it makes a change from black makes the thumbnail a bit more interesting and I'm going to draw the sprite to location 0 0 which is the top left the single one line command draw sprite so that's it that's our program we're going to load the sprite and draw it to the screen something we've done many many times on this channel and there we go we can see the sprite of a yellow car with a white background on the dark cyan background of the pixel game engine window the car has a white background because we've not told the pixel game engine to care about alpha blending the transparency of the PNG file now PNG files they contain their own transparency information but we choose not to use it in most cases because actually blending things together is quite computationally intensive so you only want to do it when and where you need it so just before I draw the sprite I'm going to set the pixel mode to alpha Nuttall instruct the pixel game engine to actually acknowledge that the pixels in the PNG file of the sprite have transparency but it's important that once we've enabled it we should also disable it as well because here if we didn't set these back to normal the next time we call the clear screen function it'll try to take into account all of the alpha values that are there too which would slow things down and just to sanity check that that works there we go we've now got the car without the white border so far so good we can easily draw the sprite at any X Y location on the screen ie we can offset it or translate it anywhere we want and in fact in the pixel game engine we can go on further we can introduce scale the sprite - and this is very simply done and by integer scaling what I mean is we've provided an integer value as the fourth argument of the draw sprite function and it will modify as we can see now the car is twice as large so it is scaled in both axes by a factor of two change it to three and it's three times bigger and I can do integer scaling quite simply because the sprite shows its x and y axes with the x and y axis of the screen in the example I've just shown instead of drawing one pixel I'll draw three pixels instead easy stuff but this is not very applicable when you want to start rotating the sprites and we've done the rotation of a point many many times on this channel and so we should know these formulae off by heart now but if we wanted to transform point X we would take COS theta which is how much we want to rotate it by times X plus sine theta times y and for the y component it's very similar minus sine theta X plus cos theta Y and so this will take our original X&Y point rotate it by theta and produce a new X&Y point if you're interested in how this is derived I suggest you go and check out the code it yourself asteroids video I do a full derivation there alongside rotation we may also want translation the ability to move the sprite somewhere along the plane well we can see here that we've once we've rotated the point we can offset it quite easily by adding another component so let's say a translation in the X and a translation in the Y and depending on how you want to look at things you could also take the whole lot and multiply it by a scaling factor scale X or scale Y the order in which you do these things is of course important one of the things I've done a lot of this year is transformation matrices so here we've got the rotation matrix equivalent of the equations we've just seen and the scaling matrix we don't have one for translation and that's because we've not got enough dimensions here if you remember how we calculate matrices is quite a simple algorithm we go along each row of the element on the left hand side of the multiplication and multiply it by the column on the right hand side so this course gets multiplied by this X X cos theta and we add to that this sign multiplied by this y plus y sine theta then we do the same for the bottom row minus X sine theta plus y cos theta and I'll just close off my matrix these two equations are of course what we've just seen on the previous page but you'll notice we need a third additional term to implement the translation so let's just rewrite things a little bit I'm going to extend the rotation matrix to accommodate the additional term so it now becomes a three by three matrix and when we do our multiplication we're multiplying by X and by Y and by one in this case the third term would be one multiplied by zero or one multiplied by one in either way it's irrelevant in fact we'll do exactly the same from our scaling matrix zero zero one zero zero so now our scaling matrix is also a three by three matrix but now we have enough dimensions to create a translation matrix which is 1 0 TX 0 1t why because these are the terms that get added on remember and 0 0 1 just to make it a 3x3 and now that our three fundamental transforms are all 3x3 matrices of course we can combine them so we could scale something then rotate it then translate it then rotate it then scale it then translate it again then scale something else and we'll get a combined transform the result of this combined transform is itself a 3x3 matrix which means we can have a very complicated transformation represented with a little amount of data and these sorts of combined transforms are called affine transforms affine transforms have lots of interesting mathematical properties in their own right but I'm only interested in rotating and drawing sprites in different places and as with a lot of programming algorithms Wikipedia isn't a bad place to go yes it starts to talk about the affine transform from quite a mathematical perspective what I like about the article is about 2/3 of the way down we get to see it lists f-fine matrices as 3x3 matrices with an example of what they do so here we've got the identity matrix which does nothing it keeps the image on transformed we've got the reflection matrix well if we look at the reflection matrix we can actually see that that's the same as the scale matrix we're only affecting things on the leading diagonal in this case we're multiply x-value by minus 1 so we get to flip in the x-axis that's no different to scaling except we're using a minus sign in the front we've also got here we go the rotation 3 by 3 matrix very nice we've just seen that and we have an odd one here which is called shear which allows us to sort of stretch along one axis only and the thing to know about affine transforms is that parallel lines always remain parallel so let's get started with implementing our own matrix framework I'm going to remove what we've already got so far I'm going to create a basic struct called matrix 3x3 it's just a 3x3 two-dimensional array of floating-point numbers and I'm going to adopt the convention that I'll do things column and then row this is the same as x and y just as we did with the code itself 3d graphics series I'm going to add some utility functions to help me work with these matrices so the first function I'll add is a function that just simply sets the matrix that's passed into it to be an identity the leading diagonals are set to one and we know that we're going to be multiplying matrices a lot so I'm going to add a quick function just to do that it simply loops through the columns and the rows and multiplies them accordingly we also need a function to pass a point through our transformation matrix so this is going to take in an x and a y value and the matrix and produce a new x and a y value after that point has been transformed so you'll see here we've got the x coordinate x our matrix the y coordinate x matrix but we don't have the Z coordinate I'm going to assume it's one so we're just going to accumulate the matrix the humble draw sprite routine is no longer any good to us now we'll have to create our own to use the transformation matrix so I'm going to make an assumption that we'll have a final transformation I'll call that in that final and just to be on the safe side right now I'll just make that an identity matrix to perform the transformation I'm going to take a really naive approach and it's not going to be the approach will finish with but I think it shows an interesting concept on the journey I'm going to create two for loops that will allow me to iterate across each pixel contained within the sprite and I'm going to sample that pixel and store it in this value P here to additional floating point variables are going to represent my new X and new y-coordinate after I have transformed the X&Y coordinate from sprite space so this will put it into screen space and this is just a matter of calling the forward function we've just defined once we've got the new X and the new Y function we then draw the pixel using the standard pixel game engine draw function in the new location so we've taken the point within the sprite transformed it and we're drawing it to the transform location on the screen now because this is an identity matrix what we should see is no difference at all let's take a look and perfect what we see is the car aligned with the top left so let's add a translation matrix we want to move the sprite around on the screen well that's just a case of setting this value and this value in the rightmost column and instead of calling the identity function let's call the translate function and we'll pass into that a value 100 by 100 so this should offset the sprite from the top left somewhere towards the middle of the screen let's take a look and very nice it has them so our translation effect has worked quite nicely and we've used matrices to do this I'll go ahead and simply add in the three other types of affine transform matrix rotation scaling and shear let's try them out so let's say I wanted to draw the sprite half the size it should be well I'll apply scaling factors over not 0.5 to both axes let's take a look at that well the car seems smaller and quite nicely it's not got any awkward artifacts there to complain about good let's make it larger we'll make it twice as large so we'll have a scaling factor of 2 in both axes just take a look now hmm yes hmm we seem to have hit a bit of a snack here there's gaps throughout the image why is this well simply we've not got enough pixels as we've iterated through all of the pixels in the sprites we've created new locations for those pixels but the gaps in between have a look at filling these gaps in in a bit but let's try the other transforms first well scale was a bit unsuccessful so let's try rotate instead and we'll rotate by some arbitrary amount nor point to this is of course in radians well it simply rotated the image but there are those pesky blank pixels again nonetheless let's try some combined transformations as well I'm going to need some more matrices for these just some temporary ones at the moment we've just rotated the sprite around its top left coordinate let's rotate it around the middle so this means we'll need to translate the sprite before we do any rotation so let's create a translation matrix to do that well store that in matte a now I happen to know that the sprite is 200 pixels wide by 100 pixels high so if I translate it by minus 100 and minus 50 I'll move the zero point to the middle of the sprite I still want to do the rotation but I'm going to store that in matrix B as I want to combine these two matrices into my matrix final I'll call the matrix multiply function that we created earlier the order of operations here is quite important let's see if we've got it right well I'm looking at that and thinking no we didn't get it quite right let's reverse those around okay that's looking a little bit more sensible certainly the middle of the roof of the car is now top left and it's angled so we probably want to offset the car now to the middle of the screen this means we need another translation operation or we use matrix a now we're done with it and I will set the translation to be screen width divided by 2 and screen height divided by 2 make sure I put this matrix multiply in the right place first so we've done the translation and rotation and that's given us mat final then we're creating another matrix a going to need some more matrices here and I let's add in a matte C so instead of translating from to the final we'll go to Matt C first and now our final multiply will give us the final matrix we've got our new translation going in and we want to multiply that with what we had previously you know it complicated this and there we go we can see the car now offset to the middle nicely bang on in the middle and rotated by a certain amount let's just test this rotation I'll add in some keystrokes to handle some user presses to rotate the car we'll need a quick variable I'll call this one F rotate and I'll just initialize it to 0 and I'm going to be sensitive to these Z and X keys to increase or decrease that value as necessary I'll use Fe elapsed time so it's nice and smooth instead of this hard-coded value let's put in our rotate value we'll come back to this sort of matrix mess in a minute let's just see if this works so that looks like the car normally I'm going to start rotating it and it does rotate we still got these pesky Sion dots all over the place but it seems to be doing quite a nice and smooth rotation and certainly the performance is quite good - that's a pretty funky pattern the reason the matrix stuff gets a little bit complicated is matrix multiplication is not commutative ie the order matters and that's why we saw at the start I had to switch around the a and B values so I created two exclusive matrices want to translate and want to rotate and I've multiplied them in the correct order to give me a new matrix matrix C I don't want to take matrix C as my already existing matrix and multiply it by a new matrix which represents a translation to the middle of the screen what is it we've just done here well without any transformation we saw the sprite was drawn on the screen in the top left that's because the sprites top left was aligned with the screens top left the first translation operation translated the cast right so that the middle of the sprite was aligned with the top left of the screen ie we moved the origin of the sprite to its middle the origin being in the middle of the spice is quite useful for scaling and rotation because we'll always rotate around the origin and so in this case that's exactly what we did sprite doesn't quite fit on my screen here and once we've rotated the sprite we then translated it back to the middle of the screen so what can we do about these annoying gaps in our strengths in the sauce bite marks here in blue we're iterating across our pixels one by one this means our transformed image will only ever consist of the same number of pixels but the locations of these pixels may not align very nicely with the pixels on the screen and so we start to see the gaps in between we saw this especially when we were scaling I think that's a very visual example of this effect and so the problem was come around because of the forward way we're doing things I'm going to suggest doing things backwards so firstly we'll work out the area that our sprite is going to occupy once it's transformed I've some sort of bounding box we know that will cover every single pixel of the sprite within this region and instead of projecting the source sprite to the destination sprite I'm now going to iterate through the bounding box pixels which I know are now aligned with the screen and sample from the correct location in the image in effect what we want is the transform that takes our final sprite location rotation scale and shear and gives us the original sprite location rotation scale inch here we want the inverse of the transform we actually created to begin with now we know that this approach will leave no gaps because we're going to go across all of the pixels in that region but this does require a matrix inversion and as I've said in the past matrix inversion is a very complicated field and it's well beyond my skill to be able to explain nicely so I'm just going to take it off the shelf 3x3 matrix inversion function and manipulate it to our needs and so blam there we go and it looks horrific and indeed it is horrific but what I should point out is it's nothing more than multiplies and additions subtraction addition are pretty much the same thing so even though there's a lot to do it's actually quite simple for a CPU which is optimized for doing things like multiplications and additions it's also important to note that we're not going to need to do this for every pixel just once the transform is establish once we've got the correct transformation matrix or in this case the inverted transformation matrix then we just pass points through it using our forward function as normal so in many respects I tend to treat things like this as a non recurrent expenditure I'll also have I've made no attempt to optimize any of this at all I want to just keep it clear as I did with the 3d graphics engine so knowing that we're going to need an additional matrix let's quickly add it matrix final in and we'll calculate the inverse matrix right here invert that final mat final in now not change the drawing function just yet because we've got one last problem to solve I'll just quickly run this again and what it is is how do we work out where the bounding box of our sprite is well one assumption we can make is that the corners of our sprite are accurately projected in space and so if we pass through the coordinates of the corners of our sprite through our forward travelling matrix ie the non inverted one will get some screen coordinates which we can use to determine the bounding box the way that we were originally drawing the sprite is now inaccurate so I'm going to comment that out we don't need it anymore instead I want to work out where the bounding box of the transformed sprite is going to be and so to do this I'm going to pass through each corner of the existing sprite art forward through the transformation to work out where the bounding box is and so I've created four variables start X and start Y and end X and end Y they'll give us the four sides of our bounding rectangle I'm going to create two additional variables px and py you'll see why in a minute so taking the top left of the sprite which would be zero zero I can pass that through our forward function using our standard transformation matrix there's the zero zero and the result is going to be placed into px and py well to start with that's all the information I have so I'm going to set my starting X&Y and ending X&Y positions to be px and py and then going to send through in the forward direction the was it corner of our sprite so this is like the full bottom right location of the sprite the width and the height again this goes through the normal matrix now ultimately I want to traverse through the bounding box from the top left of the bounding box to the bottom right so I want to sort my starting X&Y positions which I do here so for my starting X is always going to be the minimum of the points that we've just calculated or the existing starting X location and likewise the ending X location is always going to be the maximum of my most recently calculated point or what I've already got the same happens for Y so now I've got the top left and bottom right of my bounding box you might think that would be enough but not when you've considered you've got rotations we actually need to do all four locations of the sprite so here is the bottom left and here is the top right so this algorithm will give me the bounding box of my transform sprite and I've needed the forward transformation matrix in order to do that now that I have the bounding box I can now traverse through each pixel of the bounding box and use the inverse transformation to sample the sprite at the correct location so all of the pixels in the bounding box are going to get selected I'm not going to have any gaps and the code looks very similar to what we had before I'll use my forward command but I'll use the inverted matrix to give me two new locations and I'll use these locations to sample the sprite at a particular point I've offset NX and NY because these are no longer integer by adding not 0.5 to them I make sure that I'm rounding to the correct pixel and so I guess the final thing to do is to draw the new pixel in the X&Y location on the screen so let's take a look well the spite looks good but we've got no gaps what we've not rotated anything yet so let's start to rotate and we can see nicely now that the bounding box is expanding and contracting as required that might be useful for other things but what we see is no gaps at all in fact it looks very nice I like the fact that it bounces it's very jolly I think the last thing I'm going to do is enable alpha blending again for the sprite as we're drawing it but I'll just take this bit of code and move it down here so we disabled when we're clearing the screen there we go and so there you have it we've got a sprite that has been transformed on the screen it's been translated it's been scaled it's been rotated know if we've looked at shears but I don't think we'll ever use them for anything that's quite a nice flexible routine for handling sprites in fact I like it so much that I wrapped it up in what has become the first pixel game engine extension I'll now quickly prepare the program we've just written to use the extension and this is a bit brutal we're going to delete pretty much everything we've created all I've left in is loading of a sprite handling of the rotation and the Alpha mode settings to use an extension for the pixel game engine after you've included OLC pixel game engine h you can just include the extension and that's all you'll need to do in this case it's graphics 2d the extension provides several interfaces one of which is OLC GFX 2d transform 2d so I'll create an instance of that and just as we've done manually will first to this transform we will translate it well then rotate it to our rotate variable and we'll translate it again to the middle of the screen you'll notice I'm not needing to deal with matrices or matrix multiplication that's all hidden behind the scenes and it's written in a way that optimizes the creation and manipulation of matrix data the final thing to do is to call another draw sprite routine however this one exists within the namespace of graphics 2d it's a special one because instead of taking coordinates it takes the transform object we've just created let's take a look as you can see it has exactly the same performance and behavior as the manual code but it's considerably more concise in fact that's the entire program quite like the idea of having extensions for the pixel game engine so I think I'll probably develop some more and the interface is written in such a way that you guys can also create them quite easily too I know some people on the discord have already been working on controller interfaces which i think is quite cool anyway I've got a plan for being able to rotate spiced give them we're approaching the end of the year it's usually about this time I start having a think about a big project to do in the run-up to Christmas so watch this space also don't forget to take part in the jam if you're interested in doing so the theme will be announced next Friday that's the 16th if you liked this video as usual big thumbs up please have a think about subscribing and I'll see you next time take care
Info
Channel: javidx9
Views: 25,879
Rating: undefined out of 5
Keywords: one lone coder, onelonecoder, learning, programming, tutorial, c++, beginner, olcconsolegameengine, command prompt, ascii, game, game engine, affine, affine transformation, rotation, sprites, scaling, translation, 2d graphics, drawing routines
Id: zxwLN2blwbQ
Channel Id: undefined
Length: 27min 59sec (1679 seconds)
Published: Sat Nov 10 2018
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.