Programming Pseudo 3D Planes aka MODE7 (C++)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hello I'm gonna take a quick break from the role-playing game series to have a look at programming pseudo three-dimensional planes and this was a technique made popular by the Super Nintendo and I'm going to use a Super Mario Kart as an example where an image is taken as the ground plane and it is rotated and scaled and translated around the cameras necessary to give the impression of a three-dimensional environment this was known as mode 7 which for the Super Nintendo was just really implementing an affine transform in hardware I'm not going to be using affine transforms directly because I want to play with some of the parameters and see what happens and so this is what we're going to be developing of course it's in the console now to those of a certain age this may look very familiar this is indeed the first track I believe from Super Mario Kart and for those expecting another episode of the role-playing game you can see it vaguely related we're using exactly this technology for the world map and what I think is really neat about this program is that is it there is barely anything to it at all so there should be a nice short quick video showing something interesting we might be able to use in other games and technologies so as we usually do on this channel let's take a quick look to just explore what it is we're going to do and try and understand some of the concepts and principles behind the algorithm I'm going to create a big OLC sprite and we know that these behave just like textures and that we can sample from them assuming that the top-left is 0 0 and the extremities are 1.0 and 1.0 and if you remember from the code at yourself first-person shooters this makes sampling the texture scale invariant I'm going to treat this big texture as if it were my world map or the mario kart track in this case in this map I'm going to have a position which represents where the camera is and I'm going to call this position world X and world y the camera will also have an angle associated with it we're in a 2d plane here so we don't need a three-dimensional camera and we'll assume that the angle is what direction is the camera looking at in this plane from this information I'm going to create a viewing frustum but I need a little bit more information first firstly I need a field of view and we'll call that theta and then need two more pieces of information I need to know what is the nearest location to the camera and which is the farthest location from the camera and these are going to be lines that straddle our viewing frustum let's draw these in and if we were to continue drawing our camera vector what we can see about these two lines is they are at right angles to where the camera is looking this gives me four coordinates appear here here and here and I'm going to use those coordinates to give me a region which I will be sampling to display to the user and this is my frustum once we've got the frustum we can now determine the mapping between what the player can see on the 2d plane versus what they can see on the screen now in the console window we know that the y axis goes in this direction and the x axis goes in this direction well that maps on to our frustum - so the farthest away y location is going to be towards the top of the command prompt window which means this becomes our y-axis and this becomes our x-axis and we'll notice that the x axis changes lengths depending on where it is in the y axis when we're far away we're looking at a lot more X than we are when were close to it this means we can take any XY coordinates in the command prompt window work out where it lies inside our frustum and that becomes the sampled point that we will display on the screen it would appear that there's quite a degree of interpolation happening and there is so I'm going to put some naming conventions in so we can follow the code this point up here is going to be known as far x1 and y1 this one down here is going to be known as near x1 and y1 this one is far x2 and y2 and this one is near x2 and y2 when we extract a pixel location from the console screen we will be normalizing it versus the screen width and screen height so we'll be getting in a value between 0 and 1 so let's say our pixels location X and Y is not 0.5 by not 0.5 that means we want to select somewhere in the middle of our first strip but how do we know where the middle is on a shape like this well we know that the y coordinate is going to be reasonably consistent for now those who are thinking ahead will come back to that later no point 5 will be somewhere halfway along the line between near and far and because we know the vector between far X 1 and near X 1 and the vector between far X 2 and near X 2 we can work out where those two points are and once you've got those two points we create a new line kind of goes through the middle so I'll just flag that in a normalized space this is halfway along and then of course because we've got this line we can do exactly the same to this new line to work out the X location which is halfway across this new line which we've drawn in already it's there and so by using this approach we can specify any coordinates between zero and one and work out where it lies within this shape let's just do it a second example this to make sure that let's assume that instead of not point five we've got not point two five by not point seven five so in this instance we would go three quarters of the way a longer line linearly not 0.75 there and it's about there on the same create a new line between those two points and it says we're at not point two five along the x-axis well we know that the middle is about thirst of the knot 0.25 must be about there so this becomes our sampling point now you may have noticed that I subtly hinted something's not quite right with this approach but we'll come back to that when we start writing some code and so let's get started with the code I've already created a subclass called one line code afaik mode seven and I've coated a main function which creates an instance of this class constructing the console to be 320 wide by 240 high and each character is going to be four by four pixels note that this is quite a high resolution and indeed may not work on certain screens if you're not using a 1080p screen it's not going to fit on that desktop so you'll have to use a smaller console size well let's start by adding the very basics I know I'm going to need a sprite that represents the ground later we'll show how to load the sprite but to begin with I'm going to create it algorithmically because I want it to be a strike of lines so we can see how does the 3d effect start to distort what the user can see and on the whole I know that my map is going to be square and both sides are going to be 1,024 so it's going to be like a one megapixel texture quite a big thing for the console game engine so in the on user create function I'm going to create it and here we go I've defined it as being a square sprite of size map size by map size I want to populate this map by hand so I'm going to create two nested for-loops to draw lines going from one side of the map to the other in both the X and y directions and I wanted to leave a gap of 32 pixels between each line for the opposing axis I want to draw in every single pixel so we skip by one pixel at a time now even though I've used the naming conventions x and y here I can actually book draw both the horizontal and vertical lines at the same time and that's because the map will be reflected along the XY axis it's fine and sprites have accessing methods so we can actually set the contents of the sprite manually so I'm going to set the color of that location to be magenta and I'm going to set the pixel type at that location to be the solid pixel in fact I'm going to do exactly the same for the neighboring pixels because this will give the line some thickness so this will be drawing lines in the y axis we can simply flip the coordinates to draw lines in the x axis so these two nested for-loops work to create a grid-like pattern of magenta lines in one axis and blue lines in the other everything else we want to worry about is in on user update I'm going to add some variables that represent where the player is in the world X Y and angle in radians I will also need some variables that represent the frustum I'm going to choose some numbers at random to begin with so the near plane is going to be naught point naught 1 and the far plane naught point 1 I've really no idea what these mean in the scale of the map that we're creating I'm also going to need a field of view variable but I'm going to store instead the field of view 1/2 variable so in this case the field of view would be pi divided by 2 but I'm awesome at Lee Harvey it already so it's pi divided by 4 and the reason for this is because I'm taking the angle of the center line from the player into the map and I'm going to subtract half the field of view and I'm going to add half of the field of view to give me the first two points so let's do this now we want to create our first set of points which was far x-one and y-one well if we assume that the player exists at world x we can create a unit vector using cosine and sine of the angle that the player is facing minus the field of view times the farplane so wherever the player is in the x-axis we're going to create a value between 0 and 1 here and we're multiplying that value by the farplane again I don't know where this exists in the space of our world it goes without saying that to create the B Y component is exactly the same except we use sine for the opposing Farpoint were plussing half the field of view instead of subtracting it and for the near points we're using the near plane value instead of the far plane value so this gives us for two-dimensional points that represent the corners of our frustum within the map when we're drawing the maps we only want them to occupy half of the screen not the full screen because this assumes that our vertical vanishing point is halfway up the screen so I'm going to work on a row by row basis within our console window going from 0 to half the screen height we'll need to normalize the screen height value to between 0 and 1 I'm going to call this sample depth and it's just simply the Y value divided by the total range of the Y value in this case knowing how far into the frustum we need to be we can create two points that represent the scan line going across the frustum so on the left hand side the x1 side I get the direction vector of the line of the side of the frustum I multiply it by my normalized value to create a sampling point and I offset it by the near plane and I get an end point for the scan line by doing exactly the same thing now we know where the scan line begins and ends in our first rim we can use the X component of the screen coordinate to work out where along that line we should sample the pixel and color so in exactly the same way I'm going to create a normalized point between zero and one sample width and I'm going to find the sample location by interpolating along that line once I have my sample coordinates I can go and extract them from the OLC sprite using the sample glyph and sample color functions now these functions will operate on a nearest neighbor principle which means we don't really care about the spatial scale in this instance the sampling algorithm will do what it should do and will never give us any holes or blanks once we know which symbol to draw and which color it is we should draw it but I'm going to draw offset by half the screen height in the y axis so this gives us a ground plane along the bottom of the screen let's take a look hmm well something's going on from that static image I can't really work out what's happening I need to add some user controls to move the player location around the map and I will add these outside of our rendering loop we've seen this many times before I'm just going to see if the left arrow key is held down and if it is I'm going to decrement the angle if the right arrow key is held down I'm going to increment the angle if the up arrow key is held down then I want to increment the X&Y coordinate along the unit vector created by the angle and I'm going to increment at this particular speed it's quite slow but at the moment slow is relative because I have no idea what the scale of our world is this will take a little bit of fiddling about and getting a feel for it and I can do the same for Reverse except this time although I put a minus sign in front of the Carson sign let's take a look well I'm turning in the world and it's looking pretty funky what I noticed though is that the world seems to end quite quickly let's try moving backwards well that was strange and moving forwards well it's just outright psychedelic it's pretty but useless the problem here is I've just taken a guess at what are near and far planes should be and our field of view so I'm going to make these controllable to similar code I'm just going to check for certain key presses and alter the parameter by incrementing it or decrementing it let's see how this works so without turning anything q-and-a changed the plains near parameter well that seems to stretch things out W and s change the plains far parameter well we can see there is change and I guess it kind of makes some sense the field of view is changed by Zedd and X and that's definitely affecting things in the x-axis let's rotate the player into the scene a little bit and we'll widen the field of view and we'll try changing the farplane a little bit change the near plane a little bit well that kind of looks like we're just looking top-down so rotation and sampling and things are working but something's not quite right if I orientate the camera so we get vertical and horizontal lines somewhere in our scene we can see that they behave kind of appropriately but it looks like our scene has its perspective firstly in the wrong direction and upside down so as the players moving forward here between the two magenta lines the blue lines are behaving somewhat sensibly and if I rotate 90 degrees so that we're now traveling along the blue lines the magenta lines behave somewhat sensibly so it is kind of working but it's clear that our perspective projection is well rubbish and surprise surprise this is our problem we've made a rather naive assumption that our interpolation along the frustum central axis away from the camera is linear and it isn't perspective assumes that we've got three components to an image x y and z where z is equal to the depth and we intuitively know that things that are further away move less and have less significance and so if we were to look at the relationship between depth and X&Y what we might be able to assume is that some new position X is equal to some position X divided by depth and the same goes for the y axis and this implies that as Z approaches infinity any changes to X become minuscule whereas if Zedd approached 0 the changes become very large indeed and in many ways as this is why the stars in the night sky don't appear to move as you move your head around yet to the trees and lampposts in front of them do but what's interesting here is that we're seeing that depth is certainly a 1-over depth relationship to X&Y I thought it might be useful to quickly explore why this might be the case I want you to imagine for a minute that the camera is the man's head and the man is looking forwards into the world what can the man actually see if we broke the world up into scanlines so here I've projected two rays from the players eye into the distance and if we were to attribute how much of this distant scanline can be seen by the player we might want to estimate it by drawing a little arrow between the two we can see it's quite small let's draw in some more lines and now if we look at the proportion of what can be seen for each scanline we might be able to identify a bit of a trend you and I'll very carefully try and draw this line in joining all of the points of these up look at that I'm getting somewhat better with the old drawing tablet we can see very clearly that we have indeed got a y equals 1 over X relationship here what this means in real terms is that we're not looking in the right place for our frustum instead of this line being broken up linearly we want to look at it in a slightly different relationship where those lines close to the player are close together but as we get further away from the player they get further apart and we can also see here that this equation is not quite correct we're starting with the scanline that's furthest away yet our sample depth variable assumes the nearest line so let's see what happens if we flip this round well it still looks a bit rubbish but we could assume that now at least things are moving in the right location as I move along now the perspective is corrected towards the player and the vanishing point lines disappear into the distance and we just play with the planes a little bit so there we have it I can rotate 90 degrees but there's still a great degree of curvature that's because we're still operating linearly and I can do this in one hit we know that the first line we're calculating is the farthest away but that requires our scan line to be the largest in width and yet the most minimum in detail what I'm going to do is not flip this round instead of multiplying I'm going to divide so starting with the farthest away scan line our sample depth is effectively zero yes there's a little bit of a divide by zero here but in this case let's assume that's infinity it means as far away as we can possibly see we can see everything but as we get closer towards the player we start to approach one which means the scan line becomes very small now let's take a look and what's the difference that is made already I can rotate the player and I'll move forwards into the map and we can see despite a few sampling issues we can walk up to the boundaries of the map let's just take a minute whilst it's working well to play with our parameters so the field of view o whitens and narrow is to fill the few as you might expect you'll have seen this affect in quite a lot of the first-person shooter games if we alter the near plane we can start to imply a degree of curvature to the map let's try and straighten that back up again there we go and if we alter the far plane it gives us the impression that the camera has from the ground and of course that's because that's the only way that equation can be solved if we're saying that the farplane has to be wider the player needs to be higher up and further away so this leads to some promising things we could have a jumping mechanic but one thing I have noticed is the world ends and this might be undesirable for emulating things like globes in role-playing games see where I'm going so because our sample positions are calculated in world space but our map is a finite texture between 0 & 1 I'm going to take the floating-point modulus of our sample coordinates so they should implement a wraparound functionality see we've not got a wrapper and we're currently at 0 0 so we don't have anything in the negative axis but we can see the map goes on indefinitely away from 0 0 so what we might want to do is place the player somewhere in the middle of the world first so they're nowhere near the negative edges of this map I'm gonna pick a number 1000 and that's nice so we can see that the player is now stood within the middle of the world I think it's time to say goodbye now to our hands generated map and use a pre-loaded texture so I'll comment that out and load the texture directly now I went online to have a look at Mario Kart maps and found a fantastic website which had them as PNG files and as some of you may know now from quite a few videos the fantastic King from the discord server has been creating lots of algorithms to turn PNG files into a console game engine format sprites and so I converted the PNG into a sprite ready to use in the game engine let's have a look and we can see well we've certainly got a map it looks a bit distorted and parallel lines aren't quite right but that's why we've got adjustable parameters so firstly it looks like I'm floating in the air so I'm going to adjust the Far viewpoint down a little bit so it brings me closer to the ground I'm going to bring in the near parameter to try and take out some of the curvature so I can put in lots of curvature it just makes it look like we're always going downhill I want to try and iron that out a little bit until it looks about right there we go that's not too bad the field of view seems reasonable any way we can make it wider and we can make it narrower I think in racing games you can change the field of view to give you different perceptions of speed so as I make the field of view wider I'll just try and zoom along in a particular direction so the field of view is getting very wide but if I bring it in and come a bit narrower yes I think it starts to look like we're speeding up maybe I don't know they need some playing with one thing I am curious to see is how does the mario kart map look if we don't do this perspective correction maybe it'll be clearer to see what's going on without the blue and magenta lines so we can see it's upside down again I mean if I just zoom out a bit so now I'm altering the farplane and we're getting these sort of inception like rolling that's simply because the it is upside down for a start but we're attributing perspective in the wrong place we're basically seeing the farthest away parts of the world in the most detail and it's very disorienting let's put that back one of the benefits of having calculated all of this maths for one texture is we can use exactly the same coordinates to sample a second texture for the sky so I'm going to add another sprite sprite sky in this instance I'm going to load that and here we're sampling the ground texture and drawing the sky I'm going to just use exactly the same coordinates and variables again to sample the sky texture we don't need to calculate it because it's in the same place well to the ground but this time I instead of drawing offset with the screen height I'm assuming I'm drawing from zero to the middle of the screen but I want to draw it upside down in this instance or else it'll look very strange let's take a look well there's our screen for some reason it's divided into quads that one looks to be like it's a close to zero sampling error somewhere to do with the sampling of the OLC sprite but now we can see the sky behaves in exactly the same way as the ground this is very good so if we increase the farplane it works for both of them so it makes the environment appear a lot more vast make it very big it takes a slight hit in framerate now but we can make the environment appear very big or very claustrophobic might be interesting to do some sort of first-person shooter with this if we alter the near plane well we'll start to get some weird curvature effects again I'm gonna have loads of fun playing with these parameters what I have noticed is a big ugly black line in the middle of the screen of the horizon and that's because of this infinity value that we talked about before we can see it is here in the ground map and it's also here in the sky map well I tell you what in this instance I'm going to cheat I'm just going to draw a cyan line at that location take a look and there we go and the nice thing is because this is just a 2d map we could store all sorts of additional information so for example these squares they could be solid and we wouldn't be able to move the player past them we might be able to interpret a secondary texture that contains track or not track information to give us different levels of grip this approach gives us quite a lot of possibilities for interesting games in the future anyway I hope this video has been of some use to you if you've enjoyed it please give me a big thumbs up thanks to King on the discord server for creating his PNG to sprite converter have a think about subscribing and I'll see you next time take care
Info
Channel: javidx9
Views: 77,131
Rating: undefined out of 5
Keywords: one lone coder, onelonecoder, learning, programming, tutorial, c++, beginner, olcconsolegameengine, command prompt, ascii, game, game engine, planes, mode7, graphics, pseudo 3d, super nintendo, snes, mario kart, affine transform, projection, perspective
Id: ybLZyY655iY
Channel Id: undefined
Length: 27min 16sec (1636 seconds)
Published: Sun Apr 15 2018
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.