Created
June 7, 2026 11:21
-
-
Save SMUsamaShah/44c608a869e78c3ed2376cecb19ecaef to your computer and use it in GitHub Desktop.
Sebastian League Software Rasterization video transcript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| https://www.youtube.com/watch?v=yyJ-hdISgnw | |
| The transcript for this *Coding Adventure* video by *Sebastian Lague* is available in the provided video metadata section. Below is the full transcript for your reference: | |
| 0:00 [Music] | |
| 0:01 Hello everyone and welcome to another | |
| 0:03 episode of coding adventures. In the | |
| 0:06 past, we've played around a bit with | |
| 0:08 rate tracing, which more and more games | |
| 0:10 lately are starting to make use of, | |
| 0:11 especially for things like shadows and | |
| 0:13 reflections. But the vast majority of 3D | |
| 0:15 graphics is still done with a different | |
| 0:17 approach known as | |
| 0:19 rasterization. So, a simple little | |
| 0:21 software rasterizer is what I'd like to | |
| 0:23 try building today. Nothing terribly | |
| 0:25 fast or fancy, just some humble 3D | |
| 0:28 graphics rendered from scratch on the | |
| 0:31 CPU. So, I've started out with this | |
| 0:33 structure here holding three | |
| 0:35 floatingoint values, quite creatively | |
| 0:36 called float 3, which will represent the | |
| 0:39 red, green, and blue channels of our | |
| 0:41 colors. These have been mapped to X, Y, | |
| 0:43 and Z variables, by the way. So, we can | |
| 0:45 use it more generally as a 3D vector for | |
| 0:47 positions and so on. But for now, let's | |
| 0:50 just create an image by defining a grid | |
| 0:51 of these float 3es and then loop over | |
| 0:54 every cell or pixel in that grid to | |
| 0:57 assign a color. For instance, here I've | |
| 0:59 just calculated how far along each axis | |
| 1:01 of the image we are as values between 0 | |
| 1:03 and one and then used those as the | |
| 1:05 intensities of red and | |
| 1:08 green. This would probably look very | |
| 1:10 cool already if only we could see it. | |
| 1:12 So, let's write our artwork out to an | |
| 1:14 image file. And to do that, I've been | |
| 1:16 looking into this bit map format, which | |
| 1:18 at least on Windows is the simplest I've | |
| 1:20 come across that can be opened without | |
| 1:22 any special software. We basically just | |
| 1:24 need to write a little header | |
| 1:26 specifying, for example, the width and | |
| 1:27 height. And that we're not bothering | |
| 1:29 with compression today. Thank you. And | |
| 1:31 after all that, we loop over our image | |
| 1:33 and write each color channel into the | |
| 1:34 file as a single | |
| 1:37 bite. Okay, let's give it a well. And | |
| 1:39 our file has just popped into existence | |
| 1:41 over here. So, let's open that up. And | |
| 1:44 it sure looks like a beautiful bunch of | |
| 1:46 bites, but we don't really want it | |
| 1:47 interpreted as text, of course, but | |
| 1:49 rather as a | |
| 1:51 bump. That's looking very promising, | |
| 1:54 although rather less red than I was | |
| 1:56 anticipating. Hang on, let me scrutinize | |
| 1:58 this example I was following. Uh, red | |
| 2:01 pixel | |
| 2:02 00255. And here's a blue pixel 255 0. | |
| 2:07 So, it's just backwards. All righty | |
| 2:09 then. Let's switch that up quickly. And | |
| 2:12 now we have our stunning image. All that | |
| 2:16 remains is to draw some 3D graphics on | |
| 2:18 it. For that, we're going to need some | |
| 2:21 triangles. So over here, I've defined | |
| 2:23 three two-dimensional points just | |
| 2:25 somewhere between the width and height | |
| 2:26 of the image. And then said that if the | |
| 2:28 current pixel is inside the triangle, it | |
| 2:31 should be colored in blue. This point in | |
| 2:34 triangle function is looking ominously | |
| 2:35 red though, which means we're going to | |
| 2:37 need to get mathematical for a moment. | |
| 2:39 So here are three points for us to | |
| 2:41 ponder and we can draw little arrows | |
| 2:43 between them showing the direction of | |
| 2:44 the segments from A to B to C and back | |
| 2:47 to | |
| 2:48 A. It'll be helpful I think to consider | |
| 2:51 each of these as having a left side and | |
| 2:53 a right side but might get easily | |
| 2:55 confused. So let's tilt our head | |
| 2:57 slightly so that this segment here is | |
| 2:58 standing straight up and then mark left | |
| 3:00 and right with little arrows. Then | |
| 3:03 tilting our heads a bit further so that | |
| 3:05 the next segment standing straight, we | |
| 3:06 can again label the left and right. | |
| 3:09 Okay, one last twist. The side's left, | |
| 3:12 the side's right. And then having spun | |
| 3:13 our heads right round, we're back to | |
| 3:15 where we | |
| 3:16 started. Now we want to know if a point | |
| 3:19 is inside this triangle. So let's | |
| 3:20 introduce a point P over here. And we | |
| 3:23 can see that it's currently on the right | |
| 3:24 side of two of the segments and on the | |
| 3:26 left side of the other. And if we | |
| 3:28 observe as it creeps along the | |
| 3:30 outskirts, we can see how at all times | |
| 3:32 it's on a different side of one of the | |
| 3:35 segments. It's only of course if point P | |
| 3:37 finally gathers the courage to dot | |
| 3:39 inside that the segments now have it | |
| 3:42 completely surrounded. Meaning the point | |
| 3:44 is on the same side of all of them. So | |
| 3:46 that's a condition we want to test for. | |
| 3:48 We just need to solve the sub problem of | |
| 3:50 which side of a segment is the point | |
| 3:52 actually on. So let's picture an arrow | |
| 3:54 going from point A to our point P. And | |
| 3:57 then using the ever helpful dot product, | |
| 3:59 we can calculate whether these two | |
| 4:01 arrows are pointing more in the same | |
| 4:02 direction where the dot product is | |
| 4:04 positive or more so in opposite | |
| 4:06 directions where the dot product is | |
| 4:08 negative. So we've divided space up into | |
| 4:11 above and below, which is kind of the | |
| 4:14 opposite of what we wanted. But to fix | |
| 4:16 that, we can simply rotate one of the | |
| 4:18 vectors by 90°, transforming above and | |
| 4:21 below into the left and right. we've | |
| 4:23 been looking | |
| 4:27 for. All right, so quickly turning these | |
| 4:29 concepts into code, here's our function | |
| 4:31 for the dotproduct of two vectors, which | |
| 4:33 multiplies together their lengths and | |
| 4:35 the cosine of the angle between them. | |
| 4:37 Although the math simplifies wonderfully | |
| 4:39 to just be the product of their x axis | |
| 4:41 plus the product of their y- axis. Then | |
| 4:44 rotating a 2D vector by 90° is nice and | |
| 4:46 easy. We just swap x and y around and | |
| 4:49 make either one of them negative for | |
| 4:50 clockwise or counterclockwise. Building | |
| 4:53 on those two, we then have our point on | |
| 4:55 right side of line function which just | |
| 4:57 calculates the two vectors we were | |
| 4:58 looking at a moment ago with one of them | |
| 5:00 rotated by 90° and then tests whether | |
| 5:03 that dotproduct is positive. At long | |
| 5:06 last, that brings us to our actual point | |
| 5:08 and triangle test, which checks which | |
| 5:10 side the point is on of all three edges | |
| 5:12 and makes sure that they are all the | |
| 5:14 same. So, we can run the code now and | |
| 5:16 admire the triangle we've created. Uh, I | |
| 5:20 think I messed up the maths. Oh, | |
| 5:22 actually, it was the code that got me. I | |
| 5:24 forgot how programming works for a | |
| 5:25 moment there. That is not how you test | |
| 5:27 if three things are | |
| 5:28 equal. Okay, let's run it again. And | |
| 5:31 this time, we get | |
| 5:33 perfection. At least for this one | |
| 5:35 specific case. So, to test a wider | |
| 5:38 variety, I've quickly set up a whole | |
| 5:39 bunch of randomly initialized triangles. | |
| 5:42 They also have random velocities and | |
| 5:44 colors. And I've set them to just bounce | |
| 5:46 around the screen for fun. Then in our | |
| 5:48 image loop, we loop over each set of | |
| 5:50 three points, run our triangle test on | |
| 5:52 those, and of course color in the pixel | |
| 5:54 if it passes. Now we just need to wait | |
| 5:57 for this to | |
| 5:59 [Music] | |
| 6:05 render. And finally, it is done. It | |
| 6:09 looks like I forgot to clear the | |
| 6:10 background each frame, though, so it's a | |
| 6:12 bit of a mess. Well, I guess I'll render | |
| 6:15 it again, | |
| 6:16 [Music] | |
| 6:19 then. All right, taking a look at some | |
| 6:21 of these frames, we can spot a few | |
| 6:22 artifacts, like these very thin triangle | |
| 6:24 slivers, where there's just not enough | |
| 6:26 pixels in the image to properly resolve | |
| 6:28 it. But what I'm more worried about, | |
| 6:30 jumping forwards a little, is that the | |
| 6:32 background is suddenly completely | |
| 6:33 colored in. | |
| 6:36 Okay, after a little debugging, I | |
| 6:37 discovered that one of the triangles | |
| 6:39 must have somehow collapsed into a | |
| 6:41 single point, which causes our test to | |
| 6:43 malfunction and say that everywhere is | |
| 6:45 inside of that triangle. We could fix | |
| 6:47 this in different ways, but I thought | |
| 6:49 I'd try simply moving that loop we're | |
| 6:51 doing over all the triangles up to the | |
| 6:53 top of the rendering code. The point | |
| 6:54 being that we can first calculate the | |
| 6:57 bounding box of the current triangle and | |
| 6:59 then limit the scope of the rendering to | |
| 7:01 only the pixels that lie within it. That | |
| 7:04 is both an optimization. We can see the | |
| 7:06 frames flying past | |
| 7:08 now. And it also solves our problem | |
| 7:10 because if a triangle were to collapse | |
| 7:12 into a single point again, then we would | |
| 7:14 have a 1x one pixel bonding box, meaning | |
| 7:17 it would be drawn as a single pixel, | |
| 7:18 which I think is | |
| 7:21 reasonable. Anyway, I think this is | |
| 7:23 looking good. So, enough dawling in two | |
| 7:25 dimensions. Let's boldly go where many | |
| 7:28 have gone before and try to draw a cube. | |
| 7:31 First though, we'll need to pass the | |
| 7:32 data in this 3D object file, which is | |
| 7:34 happily quite self-explanatory. We have | |
| 7:36 vertex positions denoted by V, then some | |
| 7:39 vertx normals and texture coordinates, | |
| 7:40 which we can worry about later, followed | |
| 7:43 by a mysterious S, the significance of | |
| 7:45 which I've not yet ascertained, before | |
| 7:47 finally the face indices. So, I believe | |
| 7:50 this means that we take vertx positions | |
| 7:52 number 1, 5, 7, and three, and that will | |
| 7:55 be our first face. Although four points | |
| 7:58 is famously not a triangle. So, we'll | |
| 8:01 have to do that conversion | |
| 8:03 ourselves. And here's what I've come up | |
| 8:05 with so far. We just go line by line | |
| 8:06 through the file, reading in all the | |
| 8:08 points and adding them to a list. Then, | |
| 8:11 once we get to the faces, we just read | |
| 8:12 in each point index and add the | |
| 8:15 corresponding point to this list of | |
| 8:16 triangle points. If there are more than | |
| 8:19 three points in a face, then we can just | |
| 8:21 make a triangle fan like this. So, no | |
| 8:24 matter how many points there are, we | |
| 8:26 just take the first three as our first | |
| 8:27 triangle. And then when we add in the | |
| 8:29 fourth point, we'll first append another | |
| 8:31 copy of the first point in the face and | |
| 8:34 the last point from the previous | |
| 8:35 triangle. And so on until the whole | |
| 8:38 thing is filled. This is assuming that | |
| 8:40 the faces have the decency to be convex. | |
| 8:43 If we wanted to handle concavity, then | |
| 8:44 we'd need something slightly more | |
| 8:46 sophisticated like the ear clipping | |
| 8:48 algorithm. But anyway, here's the setup. | |
| 8:51 Now, we load in our cube data, generate | |
| 8:53 some random colors for the triangles, | |
| 8:55 and store that in this model class. | |
| 8:58 Additionally, we create a render target | |
| 9:00 to draw to, which is nothing fancy at | |
| 9:02 all. It just holds an array of colors | |
| 9:04 exactly like the image we made before | |
| 9:06 and gives convenient access to its width | |
| 9:08 and height. The model class is even less | |
| 9:12 interesting. Now, this data can then | |
| 9:15 just be passed to the same rendering | |
| 9:16 function we had for our triangle soup | |
| 9:18 earlier, although it is panicking at the | |
| 9:20 moment because the triangle points are | |
| 9:22 now three-dimensional and it has no idea | |
| 9:24 what to do with that. So, let's help it | |
| 9:26 out by first passing the points through | |
| 9:28 a function that converts them into the | |
| 9:30 familiar 2D pixel coordinates, which we | |
| 9:32 can refer to as screen | |
| 9:35 space. Now, if we imagine our vertices | |
| 9:37 being somewhere out in the world, which | |
| 9:39 might be measured in meters or whatever, | |
| 9:41 we'll need to define how many meters fit | |
| 9:43 into our image. I've chosen five for no | |
| 9:46 particular reason, so that we can then | |
| 9:48 figure out a conversion factor between | |
| 9:50 meters and pixels. If we then fishly | |
| 9:53 flatten three dimensions into two by | |
| 9:56 just pretending that Z doesn't exist, we | |
| 9:58 can convert that to pixel units and | |
| 10:00 finally add it onto the screen center. | |
| 10:03 Just that a point at zero in the world | |
| 10:04 maps nicely to the center of the screen. | |
| 10:08 Okay, let's run this quickly and at last | |
| 10:11 we can behold our beautiful box. Well, a | |
| 10:14 single side of it anyway. So, let's | |
| 10:16 maybe get it spinning around so that we | |
| 10:17 can appreciate it from every angle. | |
| 10:20 Consider then the X, Y, and Z axes of | |
| 10:23 our world, pointing perhaps rightwards, | |
| 10:25 upwards, and forwards. If we want our | |
| 10:27 cube to spin around the Y-axis, for | |
| 10:29 instance, we'll need to create a new set | |
| 10:31 of vectors that describe what rightwards | |
| 10:34 and forwards specifically mean to our | |
| 10:36 cube or whatever object we're rotating. | |
| 10:39 I find it helpful to think of these | |
| 10:41 vectors as little points just moving | |
| 10:43 around a unit circle. And for any given | |
| 10:45 angle, we can describe them using S and | |
| 10:48 cosine like this. | |
| 10:50 So imagine now that our model has one of | |
| 10:52 its vertices over here for example. We | |
| 10:56 can apply the rotation to that vertex by | |
| 10:58 simply taking its xcoordinate 1.8 in | |
| 11:01 this case and going that many times | |
| 11:03 along the model's own x-axis vector | |
| 11:05 which I'll refer to as ihat by the way | |
| 11:08 just to differentiate it from the | |
| 11:09 unchanging x-axis of the | |
| 11:12 world. Okay. Next, we take the | |
| 11:14 y-coordinate, so zero, and move that | |
| 11:17 many times along jhat. And finally, the | |
| 11:20 zcoordinate, 0.5, and move that many | |
| 11:23 times along khat. And here's where the | |
| 11:26 vertex has ended up. So, if we now | |
| 11:29 continue increasing the angle, we can | |
| 11:31 see how that point of course rotates | |
| 11:33 along with its basis vectors. | |
| 11:37 Okay, I've made this little transform | |
| 11:39 class now which holds a variable for our | |
| 11:41 angle around the y-axis called the yaw. | |
| 11:44 And it has a function for applying that | |
| 11:46 yaw and whatever else we want to add | |
| 11:48 later, which we can think of as moving a | |
| 11:50 point from its local space within the | |
| 11:53 object to its true location in the | |
| 11:55 world. This works as we've just | |
| 11:57 discussed by calculating a set of basis | |
| 11:59 vectors using s and cosine and then | |
| 12:02 moving each coordinate of the given | |
| 12:04 local point along the corresponding | |
| 12:06 vector which is done over here. Then | |
| 12:09 I've simply added this transform to the | |
| 12:11 model class and set the y to decrease a | |
| 12:13 little every frame to test it | |
| 12:16 out. So why is nothing | |
| 12:19 happening? Oh right, you have to | |
| 12:21 actually call functions for them to do | |
| 12:23 something. So when we're transforming | |
| 12:24 each vertex into screen space, we'll now | |
| 12:27 first want to transform that local | |
| 12:29 vertex point into world space by | |
| 12:31 applying the rotation with our little | |
| 12:33 function. And then we can convert that | |
| 12:35 into screen space like before. All | |
| 12:37 right, let's run it again. And this time | |
| 12:40 it is actually doing something. And it's | |
| 12:41 looking great. Uh, never mind, I spoke | |
| 12:44 too soon. Okay, clearly the fast side of | |
| 12:47 the cube is rendering in front as it | |
| 12:50 rotates around since we're just drawing | |
| 12:51 each triangle on top of the last which | |
| 12:54 somewhat spoils the effect. So let's | |
| 12:57 revisit our triangle mass for a moment. | |
| 12:59 And here we can see again our point P on | |
| 13:01 the right side of all of the segments. | |
| 13:04 But if we rotate this triangle around, | |
| 13:07 suddenly it appears that the point is on | |
| 13:08 the left side of all of them. So | |
| 13:11 assuming that the triangles in our | |
| 13:12 models are all wound in a consistent | |
| 13:14 order, be that clockwise or | |
| 13:16 counterclockwise, we can tweak our code | |
| 13:18 to cull this back face by only | |
| 13:21 registering the point as inside when | |
| 13:23 it's on one specific side of all of the | |
| 13:26 segments. Okay, so now with only the | |
| 13:28 front faces showing, first of all, it | |
| 13:30 renders a bit faster because it's not | |
| 13:32 wasting time filling in those back faces | |
| 13:34 unnecessarily. But more importantly, | |
| 13:36 it's now looking correct from every | |
| 13:38 angle. Well, we can't actually see the | |
| 13:40 top and bottom yet, I suppose. So, let's | |
| 13:42 quickly add in a second axis of | |
| 13:44 rotation. For that, we could just | |
| 13:46 calculate how the up and forwards | |
| 13:48 vectors rotate around the red rightwards | |
| 13:51 vector. Let's call that pitch. And | |
| 13:54 obviously, that's again just simple | |
| 13:55 signs and cosiness to get us our I, J, | |
| 13:58 and K hat for this new rotation. What I | |
| 14:01 found confusing at first though is what | |
| 14:03 if we've already applied some Y? How do | |
| 14:06 we then make the pitch now rotate around | |
| 14:08 this new orientation of the rightwards | |
| 14:10 factor? But in fact, we've already | |
| 14:12 figured this out because we wrote this | |
| 14:14 function here that takes in a set of | |
| 14:16 basis vectors describing what we want | |
| 14:18 right and up and forwards to mean and | |
| 14:20 then transforms some input vector along | |
| 14:22 those | |
| 14:23 axes. So far that input vector has just | |
| 14:26 been our vertex positions. But if we now | |
| 14:28 calculate the vectors for pitch, simply | |
| 14:31 ignoring the yaw entirely for the | |
| 14:32 moment, we can then afterwards take each | |
| 14:35 pitch vector and transform it along the | |
| 14:38 vectors. This will give us a new set of | |
| 14:41 vectors describing the effect of first | |
| 14:43 rotating around the vertical axis and | |
| 14:46 then rotating around wherever the | |
| 14:48 horizontal axis ended up after | |
| 14:50 that. So trying this out now, I'll first | |
| 14:53 apply some yaw and then apply some | |
| 14:55 pitch. Hang on. Okay, I got a little | |
| 14:59 mixed up with the minus | |
| 15:01 signs. While we're back here though, I | |
| 15:03 quickly want to mention that we could, | |
| 15:04 of course, work through these | |
| 15:05 calculations and simplify things quite a | |
| 15:08 lot or even just use some neater | |
| 15:10 notation like each of these could be the | |
| 15:11 columns of one matrix. Each of these | |
| 15:14 could be the columns of another matrix | |
| 15:16 and then this here is just multiplying | |
| 15:18 the two matrices together. But I find it | |
| 15:20 helpful to write everything out like | |
| 15:22 this to understand what's going on. And | |
| 15:24 then we can always tidy up and optimize | |
| 15:26 later. Anyway, let's see if this is | |
| 15:28 actually working at last. So, I'll do | |
| 15:30 some yawing and some pitching. And | |
| 15:32 that's looking pretty good. One thing | |
| 15:35 that is lacking at the moment, though, | |
| 15:37 is a sense of perspective, which is | |
| 15:39 where things far away look small, while | |
| 15:41 things nearby look really big. You may | |
| 15:44 have noticed this before. So, when we're | |
| 15:46 calculating the number of pixels per | |
| 15:48 world unit, that value should actually | |
| 15:50 vary based on how far away the object | |
| 15:52 is. since if it's further away, it'll | |
| 15:54 obviously cover fewer pixels on the | |
| 15:55 screen. So to achieve that, we can try | |
| 15:58 simply dividing by the Z component of | |
| 16:00 the current | |
| 16:01 point. Then I've also quickly added a | |
| 16:04 position vector to the transform class, | |
| 16:06 by the way, so we can move our cube | |
| 16:07 around. And this just needs to be added | |
| 16:09 on once we've applied the rotation to a | |
| 16:11 vertex, so that all the models vertices | |
| 16:13 will be centered around that position in | |
| 16:16 the | |
| 16:17 world. Okay, let's give it a go. And we | |
| 16:19 can see that to start with things are a | |
| 16:21 little funky. If we rotate the cube | |
| 16:23 around, since it's centered at zero, | |
| 16:25 meaning that some points will have | |
| 16:26 negative Z values that we're then | |
| 16:28 dividing by. But if we move the cube a | |
| 16:30 little ways forwards to prevent that, we | |
| 16:33 can rotate it around and see that it is | |
| 16:35 indeed in glorious | |
| 16:38 perspective. The perspective is maybe a | |
| 16:40 touch on the extreme side though. So | |
| 16:42 let's think about how we can control it. | |
| 16:47 Say this point here represents our sort | |
| 16:49 of imaginary camera through which we're | |
| 16:51 viewing the world looking forwards along | |
| 16:53 the Z-axis. What we want is to be able | |
| 16:56 to define its field of view, which is to | |
| 16:58 say how much of the world it can see at | |
| 17:00 once. Like here we have a 20° view | |
| 17:02 angle, which is fairly narrow. So we're | |
| 17:04 zoomed in on just a small section of the | |
| 17:06 world. Whereas 120, for example, would | |
| 17:09 be pretty wide. Let's go for something | |
| 17:11 in between. Then I want to bring our | |
| 17:14 cube into the scene and let's divide | |
| 17:16 each of its vertices by their own | |
| 17:18 z-coordinate like we effectively just | |
| 17:20 did in the code which gives us these | |
| 17:22 projected points over here or | |
| 17:24 unsurprisingly one unit away from the | |
| 17:26 camera on the z-axis since z over z is | |
| 17:29 one. Now we could think of this plane | |
| 17:32 that our points have landed on as the | |
| 17:34 screen or image that we're rendering to. | |
| 17:36 And we can see that if one of the | |
| 17:38 vertices of our cube goes outside of the | |
| 17:40 field of view, its projected point has | |
| 17:43 now gone off the top of the screen and | |
| 17:45 would of course no longer be visible. | |
| 17:47 But if we just increase the field of | |
| 17:48 view a little bit, now it's back on the | |
| 17:50 screen and of course visible once again. | |
| 17:54 So the world space height of this screen | |
| 17:56 here is obviously dependent on the field | |
| 17:59 of view. and we need to calculate it so | |
| 18:01 that we can replace this arbitrary | |
| 18:03 constant in our code with the more | |
| 18:05 intuitively adjustable field of view | |
| 18:07 value with a touch of trig. This is not | |
| 18:10 too tricky because we have a right angle | |
| 18:12 triangle here with an angle we know | |
| 18:13 that's just half the field of view and a | |
| 18:16 side length of one and this is our | |
| 18:18 unknown half of the screen height. So, | |
| 18:21 it's an opposite over adjacent kind of | |
| 18:23 situation, meaning we can plug into the | |
| 18:25 code that the screen's world height is | |
| 18:27 equal to the tangent of the field of | |
| 18:29 view over two * | |
| 18:31 2. Running this now with a vertical | |
| 18:34 field of view of 60°, I think the | |
| 18:36 perspective is looking a lot more | |
| 18:38 natural. | |
| 18:40 To play with the effect of our fancy new | |
| 18:42 field of view a little, I've quickly | |
| 18:44 hacked in some temporary code which | |
| 18:45 constantly recalculates the field of | |
| 18:47 view based on the cube's current ZV | |
| 18:50 valueue in order to try keep it the same | |
| 18:52 size on the screen. And with that | |
| 18:54 active, if we now move the cube closer | |
| 18:56 to the screen, we can see the | |
| 18:58 perspective getting more intense as the | |
| 19:00 field of view widens to keep it the same | |
| 19:02 size. And if we move the cube further | |
| 19:04 away instead, now it begins to look | |
| 19:06 flatter and flatter as the field of view | |
| 19:08 narrows to zoom in on it. In fact, it | |
| 19:11 pretty much looks like what we had | |
| 19:12 originally, which is known as an | |
| 19:14 orthographic | |
| 19:15 projection. Let's put things back into | |
| 19:17 perspective, | |
| 19:21 though. Now, cubes are great. I love | |
| 19:24 cubes, but let's try rendering something | |
| 19:26 a little more sophisticated, such as | |
| 19:28 Blender's little built-in monkey. | |
| 19:30 Exporting her gives us this rather long | |
| 19:32 list of numbers, but we can simply feed | |
| 19:35 them to our paser, of course. And Bob's | |
| 19:37 your monkeyy's uncle. Out pops Suzanne. | |
| 19:40 Somewhat distressingly though, her ears | |
| 19:42 and brow ridges are rendering right | |
| 19:43 through her head. So, our trick of not | |
| 19:46 drawing the back faces of triangles | |
| 19:48 obviously falls short with this more | |
| 19:50 complex geometry. | |
| 19:52 We'll want to keep the coloring as an | |
| 19:53 optimization, but for a more robust | |
| 19:55 solution, we're going to need to track | |
| 19:57 how far away each pixel is that we draw | |
| 19:59 so that we don't overwrite things nearby | |
| 20:02 with things further away. To that end, | |
| 20:05 I've tweaked the world to screen | |
| 20:06 function once again to return a float | |
| 20:09 three. Now, with the first two | |
| 20:10 dimensions still just being the pixel | |
| 20:12 coordinates, but the third telling us | |
| 20:14 the depth of the pixel, which is just | |
| 20:16 how far away it is on the Z-axis. | |
| 20:19 Problematically though, this tells us | |
| 20:21 only the depths at the three corners of | |
| 20:23 the triangle. Well, we need to know it | |
| 20:24 for any pixel inside, which means, lucky | |
| 20:27 us, it's time for some more triangle | |
| 20:32 maths. Now, obviously, if the pixel | |
| 20:35 we're drawing is all the way over at | |
| 20:36 point A, for instance, then the depth | |
| 20:38 would just be the depth at A. But if the | |
| 20:41 pixel is halfway between A and B, let's | |
| 20:43 say, we'd need to blend between those | |
| 20:46 two depth values. More generally, what | |
| 20:49 we're looking for is a set of three | |
| 20:51 weights telling us for any location what | |
| 20:54 proportion of depths A, B, and C would | |
| 20:57 need to be mixed together. So for the | |
| 20:59 weight of depth C, for example, we'd | |
| 21:01 need some quantity that gets bigger and | |
| 21:03 bigger as the pixel approaches point C, | |
| 21:05 but smaller and smaller all the way to | |
| 21:07 zero as it approaches either A or B. | |
| 21:11 Well, something that fits those criteria | |
| 21:13 would simply be the area of this | |
| 21:15 triangle here from A to B to P. Clearly, | |
| 21:18 that gets bigger and bigger as the pixel | |
| 21:20 goes towards C, covering finally the | |
| 21:22 entire original triangle, but is reduced | |
| 21:25 all the way to zero if the pixel goes to | |
| 21:27 either other corner. So, that's perfect | |
| 21:30 and it's probably no great mystery. | |
| 21:31 Then, what we'll be using is the waiting | |
| 21:33 factors for the other two corners. For | |
| 21:35 corner A, it would just be the area of | |
| 21:37 this little triangle. and for corner B | |
| 21:39 the area of this other little | |
| 21:41 triangle. So having arrived at this | |
| 21:43 concept the specific math problem we | |
| 21:45 actually need to solve is just given any | |
| 21:48 three points what is the area of that | |
| 21:50 triangle as a starting point we can see | |
| 21:53 that two copies of the same triangle can | |
| 21:55 be joined together to form a | |
| 21:57 parallelogram which like a regular old | |
| 21:59 rectangle has an area of base time | |
| 22:02 height. So our triangle must have an | |
| 22:04 area that's exactly half of that. | |
| 22:07 Putting on our trig goggles again, we | |
| 22:09 can spot that this height value is equal | |
| 22:11 to the length of this segment here | |
| 22:14 multiplied by the cosine of the angle | |
| 22:16 between it and the direction | |
| 22:18 perpendicular to the base. So let's | |
| 22:20 picture rotating the base by 90°. And | |
| 22:23 now we can just take the dotproduct of | |
| 22:25 this perpendicular base vector with the | |
| 22:28 other vector here. Since remember that | |
| 22:30 this has the effect of multiplying | |
| 22:32 together their lengths and the cosine of | |
| 22:34 the angle between them, giving us the | |
| 22:36 base times height that we're looking | |
| 22:38 for. Typing that up into code, we get | |
| 22:40 this, which is, by the way, a signed | |
| 22:42 area, meaning it ends up being positive | |
| 22:44 or negative depending on the point being | |
| 22:46 given in a clockwise or counterclockwise | |
| 22:48 order. You might have noticed actually | |
| 22:50 that this is pretty much identical to | |
| 22:52 the code from earlier for figuring out | |
| 22:54 if a point is on the right side of a | |
| 22:56 line segment. We can see now how that's | |
| 22:58 just calculating the area of the | |
| 23:00 triangle formed by the segment in the | |
| 23:02 point or parallelogram really since we | |
| 23:04 don't divide by two. And then it's | |
| 23:06 testing if that's wound in a clockwise | |
| 23:09 direction. I wasn't thinking in terms of | |
| 23:11 area when we wrote this though. So I | |
| 23:13 didn't connect the dots until now that | |
| 23:14 we've just implemented the same thing | |
| 23:16 again. Well, all right. Let's delete | |
| 23:19 this function then I guess. And of | |
| 23:20 course our point and triangle function | |
| 23:22 is complaining bitterly, but I'm going | |
| 23:24 to rewrite that quickly. And here's the | |
| 23:27 new version. It calculates the signed | |
| 23:29 areas of those inner triangles and | |
| 23:31 figures out whether the point is inside | |
| 23:33 the triangle by checking that they're | |
| 23:34 all clockwise. So, exactly the same as | |
| 23:37 before. Those areas can then also be | |
| 23:39 repurposed as the waiting factors we've | |
| 23:41 just been talking about. Although, one | |
| 23:43 extra detail is that we're dividing each | |
| 23:45 by the sum of all three just so that | |
| 23:48 they're neatly normalized to add up to | |
| 23:51 one. All right. To put this to use, I've | |
| 23:53 added a grid of floats to the render | |
| 23:55 target as a sort of special image purely | |
| 23:57 for keeping track of the depths. So, its | |
| 23:59 values will be initialized to infinity | |
| 24:01 each frame. And then when we're | |
| 24:03 rendering, we can first calculate the | |
| 24:05 depth of the current pixel by | |
| 24:07 multiplying the depth at each corner | |
| 24:08 with its associated weight and adding | |
| 24:10 those all together, which is yet another | |
| 24:13 dot product. We can then compare that to | |
| 24:15 the value currently in the depth buffer. | |
| 24:17 And if it's further away than whatever's | |
| 24:19 been drawn here so far, we just skip it. | |
| 24:22 But otherwise, we draw in the pixel's | |
| 24:24 new color and depth value. All right, | |
| 24:27 with that done, let's observe the | |
| 24:28 monkey. And she is looking wonderfully | |
| 24:31 solid. Just for fun, let's also render | |
| 24:34 out the depth map quickly to see what | |
| 24:35 that's looking | |
| 24:38 like. Okay, with this implemented, we | |
| 24:41 should now be able to correctly render | |
| 24:43 multiple objects. So, I've quickly added | |
| 24:45 our little cube back in, and it's | |
| 24:47 looking pretty good, except for this | |
| 24:49 sliver along the bottom here slicing | |
| 24:51 through Suzanne. I'm pretty sure this is | |
| 24:54 the bottom triangles of the cube | |
| 24:56 misbehaving due to having zero area when | |
| 24:58 projected onto the screen. So, I guess | |
| 25:00 we can fix that by just changing all | |
| 25:02 these area checks here to be strictly | |
| 25:04 greater than zero. And now the line has | |
| 25:08 disappeared. Unfortunately, so too has | |
| 25:10 the middle of our monkeykey's face. | |
| 25:13 Well, I didn't really think that | |
| 25:14 through. What we really care about is | |
| 25:16 the total area not being zero since that | |
| 25:19 messes up our weights when we do the | |
| 25:21 division. So, let's tell it to ignore | |
| 25:24 the triangle if it has zero area like | |
| 25:26 so. And now that seems to be working | |
| 25:29 just | |
| 25:30 fine. Okay, I've quickly thrown in a | |
| 25:33 handful more cubes to make this simple | |
| 25:34 little test scene here. And I'm longing | |
| 25:36 to just step inside and have a look | |
| 25:38 around. | |
| 25:40 So, writing our results out as image | |
| 25:42 files has been a nice minimal way to get | |
| 25:44 started, but I'd like to now switch to | |
| 25:47 real-time rendering instead. And I'm | |
| 25:49 just going to go with the easiest | |
| 25:50 approach I know, which is a lovely | |
| 25:52 little library called Rayb. This has | |
| 25:55 loads of great features that we'll be | |
| 25:57 completely ignoring today because all we | |
| 25:59 want to do is open a window and shove | |
| 26:01 our image onto it. I've also taken this | |
| 26:04 opportunity to clean up the code a bit | |
| 26:06 by the way. basically separating it into | |
| 26:08 a scene which is responsible for loading | |
| 26:10 in models and moving them around however | |
| 26:12 we want each frame and then the | |
| 26:14 rasterizer itself which takes in the | |
| 26:16 data from the scene just a list of | |
| 26:18 models and actually renders it. So | |
| 26:21 running this | |
| 26:25 now we can see our little window pop up | |
| 26:28 and render in real time. Very exciting. | |
| 26:31 The whole point though is to be able to | |
| 26:32 move around in here. So I've created now | |
| 26:35 this camera class which holds the field | |
| 26:37 of view and more relevant to our purpose | |
| 26:39 a transform for the camera. This means | |
| 26:42 that when we're updating the test scene, | |
| 26:44 we can just listen to some keyboard | |
| 26:46 input to control the camera's position. | |
| 26:49 For this to have any effect though, we | |
| 26:51 need to take the world point of each | |
| 26:52 vertex and convert it now into view | |
| 26:55 space, which is like if we imagine | |
| 26:57 moving the camera back to zero while | |
| 27:00 dragging the rest of the world along | |
| 27:02 with it. To do that, I've made this | |
| 27:04 little function here, which simply | |
| 27:06 subtracts the transform's position from | |
| 27:08 the given point. It should also reverse | |
| 27:10 any rotations stored in the transform, | |
| 27:12 but one thing at a time. So, let's | |
| 27:15 launch the game to try it out. And I'll | |
| 27:17 start by moving the camera along to the | |
| 27:19 left, and we can see how the world | |
| 27:20 appears to move rightwards in response. | |
| 27:23 And then, let's go back to the middle | |
| 27:25 and head forwards. | |
| 27:27 Things unfortunately seem to be falling | |
| 27:29 apart a bit now, but I'm pretty sure | |
| 27:31 what's happening is that points are | |
| 27:33 ending up with negative Z values as they | |
| 27:35 go behind the camera, which as we | |
| 27:37 noticed earlier causes all sorts of | |
| 27:39 nonsense when we do the perspective | |
| 27:41 division. So, let's try simply checking | |
| 27:43 before attempting to render a triangle | |
| 27:45 if any of the depths are subzero. And if | |
| 27:48 so, just skip that triangle. And running | |
| 27:51 this now, we get a flawless result. | |
| 27:55 Still, it's better than having the | |
| 27:56 triangles blow up all over the screen. | |
| 27:58 Though, we will of course have to come | |
| 27:59 up with a more sophisticated fix at some | |
| 28:01 point. But for now, I just really want | |
| 28:04 to be able to look around with the | |
| 28:05 mouse. So, I've added some mouse input | |
| 28:08 handling here to just update the pitch | |
| 28:10 in your of the camera and also tweak the | |
| 28:12 movement so that pressing forwards, for | |
| 28:14 instance, moves along the camera's own | |
| 28:16 forward factor. | |
| 28:18 We will need to head back to this little | |
| 28:20 function here now to not only undo | |
| 28:22 translation but also reverse rotation so | |
| 28:25 the whole world will rotate opposite to | |
| 28:27 however we rotate the camera. Let's | |
| 28:30 maybe copy over the local to world code | |
| 28:32 and try to do everything backwards. So | |
| 28:34 rather than first applying the rotation | |
| 28:36 and then the translation, we'll first | |
| 28:38 undo the translation and then undo the | |
| 28:41 rotation by calculating a set of inverse | |
| 28:45 basis vectors. I think we can just | |
| 28:47 calculate these the same way as the | |
| 28:49 regular basis vectors except with pitch | |
| 28:51 and your being negative. So it goes in | |
| 28:54 the opposite | |
| 28:55 direction. All right, trying this out. | |
| 28:58 It almost works, but our view is swaying | |
| 29:01 somewhat sickeningly from side to side | |
| 29:03 like we're out at sea. So to reverse the | |
| 29:06 rotation, we can't just negate pitch and | |
| 29:08 yaw. We also need to apply them in | |
| 29:11 reverse order. Like when we rotate | |
| 29:13 something, we're first applying the yaw | |
| 29:15 and then applying the pitch. So to undo | |
| 29:17 that, we'd obviously need to now first | |
| 29:19 apply the negative pitch and only then | |
| 29:21 the negative | |
| 29:23 yaw. In the code, this just means using | |
| 29:25 the pitched axes here to transform each | |
| 29:28 of the yord axes rather than the other | |
| 29:31 way around. Or in terms of matrices, as | |
| 29:33 I mentioned earlier, this would just be | |
| 29:35 swapping the order in which we multiply | |
| 29:38 them. Anyway, we can now look around | |
| 29:41 without wobbling. Truly a great day for | |
| 29:43 land | |
| 29:45 lovers. Now, I think this solution we've | |
| 29:47 come up with is nice in terms of | |
| 29:49 conceptual clarity. We're literally | |
| 29:51 reversing our calculations. But there is | |
| 29:53 a much more elegant approach I've come | |
| 29:55 across in the past. The way this goes is | |
| 29:58 to just calculate the regular basis | |
| 30:00 vectors and then swap things around so | |
| 30:03 that Ihat is equal to the first | |
| 30:05 component of each of them. Jhat is equal | |
| 30:07 to the second component of each. And you | |
| 30:10 guessed it, Khat is equal to the last | |
| 30:12 component of each of | |
| 30:14 them. And would you look at that? It | |
| 30:16 doesn't work at | |
| 30:19 [Music] | |
| 30:22 all. Okay, silly mistake. If we modify I | |
| 30:26 hatch here, then of course that modified | |
| 30:28 version gets used in the next | |
| 30:29 calculation and so on, which is not | |
| 30:31 actually what I was going for. So I'll | |
| 30:33 just make new variables for each of | |
| 30:34 these to fix that. | |
| 30:37 In matrix terms, in case you care, we're | |
| 30:39 simply swapping around the rows and | |
| 30:41 columns here to give us the transpose of | |
| 30:43 the original matrix, which turns out to | |
| 30:45 be a shortcut for inverting the matrix | |
| 30:48 in the special case of pure rotations, | |
| 30:51 where our three basis vectors are | |
| 30:52 unscaled and at right angles to one | |
| 30:55 another. Anyway, more importantly, we | |
| 30:57 can see it's working identically to our | |
| 30:59 original solution, just a lot more clean | |
| 31:02 code-wise, of course. | |
| 31:05 Now, this disappearing floor is bugging | |
| 31:07 me quite a bit, but I'm not quite sure | |
| 31:09 how I want to tackle it yet. So, let's | |
| 31:11 simply sweep it under the rug for the | |
| 31:12 moment by subdividing the model several | |
| 31:14 times. Meaning that when we look around, | |
| 31:16 the offending triangles are typically | |
| 31:19 out of sight when they get | |
| 31:21 called. I have noticed another issue, | |
| 31:23 though, with objects that are close | |
| 31:25 together, such as this dragon and the | |
| 31:27 floor. We can see how what gets drawn on | |
| 31:29 top is shifting ever so slightly as we | |
| 31:31 look around. So clearly there's | |
| 31:32 something slightly suspicious going on | |
| 31:34 in our depth | |
| 31:36 calculations. Let's have a look then at | |
| 31:38 this ruler, the middle of which is over | |
| 31:41 here, halfway, believe it or not, | |
| 31:42 between the top and the | |
| 31:44 bottom. However, if we change our point | |
| 31:47 of view a little, we can see that the | |
| 31:49 midpoint over here is in fact no longer | |
| 31:51 halfway between top and bottom. At least | |
| 31:53 not if we're thinking of those as just | |
| 31:55 points on our two-dimensional screen. | |
| 31:57 And that's exactly the trouble with our | |
| 31:59 current implementation. We're projecting | |
| 32:01 the three-dimensional triangle points | |
| 32:03 into two dimensions by dividing by the | |
| 32:05 depth at each point. And then we use | |
| 32:08 those 2D points to derive the weights | |
| 32:10 for interpolating between the depth | |
| 32:12 values. So seeing as the two-dimensional | |
| 32:14 points are squished closer and closer to | |
| 32:16 zero the larger the depth happens to be, | |
| 32:19 we need to apply the same squishing | |
| 32:21 effect to the depths themselves before | |
| 32:23 we can sensibly blend between them in | |
| 32:25 two dimensions. And so let's take one | |
| 32:28 over each of the depth values here to do | |
| 32:31 that. It doesn't actually matter if it's | |
| 32:33 one or some other constant because once | |
| 32:35 we have our squishified depth value for | |
| 32:37 the current pixel, we'll simply reverse | |
| 32:40 that operation to get back the actual | |
| 32:42 depth value. All right, trying this out | |
| 32:45 in our test scene, we can see that the | |
| 32:47 dragon is no longer bobbing about, but | |
| 32:49 instead has its feet planted firmly on | |
| 32:51 the floor. | |
| 32:53 That's very nice. But I am getting a | |
| 32:55 little sick of staring at these randomly | |
| 32:57 colored triangles. So let's see if we | |
| 32:59 can perhaps apply some textures to our | |
| 33:02 models. Earlier on, we noticed these | |
| 33:05 texture coordinates in our OBJ files to | |
| 33:07 help with that. And the idea is just | |
| 33:09 that for every three-dimensional point | |
| 33:11 in a face, there should be a | |
| 33:13 corresponding two-dimensional coordinate | |
| 33:15 between 0 and 1 describing where that | |
| 33:17 point lies on an image or texture. So, | |
| 33:21 I've tweaked the loader quickly to read | |
| 33:23 those coordinates in and then set up the | |
| 33:25 rasterizer to now interpolate them as | |
| 33:27 well inside of each triangle. We will | |
| 33:30 need to do some perspective correction | |
| 33:32 for these two, but I'm just curious | |
| 33:34 first to see what it looks like without | |
| 33:36 it. Then, I've also set up a very crude | |
| 33:39 kind of concept of a shader since we | |
| 33:41 might want different objects rendered in | |
| 33:43 different ways like with textures or | |
| 33:45 without for instance. So, here's a tiny | |
| 33:48 texture shader which simply returns the | |
| 33:50 color of its assigned texture at the | |
| 33:52 given coordinate. And to help with that | |
| 33:55 is this new texture object holding a | |
| 33:57 familiar grid of colors which can just | |
| 33:59 be loaded in from a file of course. And | |
| 34:01 it allows that image to be conveniently | |
| 34:04 sampled. In this case, just converting | |
| 34:06 the 0ero to one coordinates to actual | |
| 34:08 indices for a very simple nearest | |
| 34:11 neighbor sampling. Let's see how that | |
| 34:13 looks. All right, not too bad. But we do | |
| 34:16 indeed have some strange distortions | |
| 34:18 taking place for the exact same reasons | |
| 34:20 we saw before with the depths. So let's | |
| 34:23 fix that quickly. And it's just the same | |
| 34:24 idea again. We need to divide each | |
| 34:26 texture coordinate by the depth at its | |
| 34:28 vertx position so that they get squished | |
| 34:31 just like the points themselves. Meaning | |
| 34:33 once again that we can interpolate | |
| 34:35 between them sensibly in two dimensions. | |
| 34:38 And then afterwards we want to undo the | |
| 34:40 squishing to get the actual coordinate. | |
| 34:42 And so we just multiply by the depth of | |
| 34:44 the current pixel. All right, giving | |
| 34:46 that a go now. It's looking great. So | |
| 34:49 let's slap on a slightly snazzier | |
| 34:50 texture to | |
| 34:52 celebrate. We should probably test this | |
| 34:54 on something slightly more complex than | |
| 34:56 a cube, though. And that is looking | |
| 34:59 pretty tasty. It is also looking quite | |
| 35:02 flat, however, since we haven't yet | |
| 35:03 invited light to the party. So to fix | |
| 35:06 that, I'm going to start by quickly | |
| 35:08 loading in the normal vectors that are | |
| 35:09 also stored in our UBJ files, which just | |
| 35:12 tell us what direction the surface is | |
| 35:14 facing at each vertex of a | |
| 35:17 model. We'll then want to interpolate | |
| 35:20 these across each triangle, like we did | |
| 35:21 with the texture coordinates, because | |
| 35:23 even though triangles themselves are | |
| 35:25 flat, allowing the normal to vary across | |
| 35:27 the surface, lets us fake a | |
| 35:29 lowresolution mesh looking a lot | |
| 35:31 smoother than it really is. | |
| 35:34 So this normal vector is then passed to | |
| 35:36 each shader as an additional piece of | |
| 35:38 information. And I've made a little lit | |
| 35:40 shader now for us to do some simple | |
| 35:41 lighting in. To begin with, we should | |
| 35:44 technically actually normalize the | |
| 35:45 normal to ensure it has a length of one | |
| 35:48 like any respectable direction because | |
| 35:50 it can become a little disheveled during | |
| 35:52 the | |
| 35:53 interpolation. This is done by | |
| 35:55 calculating the length of the vector via | |
| 35:57 Pythagoras and then dividing each of its | |
| 35:59 components by that length. So each | |
| 36:01 component is guaranteed to now be | |
| 36:03 somewhere between negative and positive | |
| 36:05 one. So we can display it nicely as a | |
| 36:08 color just to check that everything's | |
| 36:09 working properly by adding one and then | |
| 36:12 having the result which makes each | |
| 36:14 component be between zero and one | |
| 36:16 instead. Here's what that looks like for | |
| 36:18 flat shaded normals, which is where the | |
| 36:20 normals of each triangle do all point in | |
| 36:22 the same direction. And here's what it | |
| 36:24 looks like with smooth normals, where | |
| 36:26 they're allowed to point in different | |
| 36:28 directions across the triangle. This is | |
| 36:30 all looking perfectly normal to me. So | |
| 36:32 let's move ahead with our simple | |
| 36:34 lighting. And for that we should take a | |
| 36:36 moment to contemplate this little beam | |
| 36:38 of light which happens to be hitting | |
| 36:40 some surface over here. Now when the | |
| 36:43 surface is facing the light source it of | |
| 36:45 course receives the maximum amount of | |
| 36:47 light whereas if it tilts away it | |
| 36:49 obviously receives less and less of it. | |
| 36:52 But how much less exactly is what we | |
| 36:54 need to know. So let's say this surface | |
| 36:57 has a width of one for simplicity. in | |
| 36:59 which case the fraction of light it | |
| 37:01 receives for a particular angle is just | |
| 37:04 equal to the length of this bottom side | |
| 37:06 here of this triangle. Trigonometry then | |
| 37:09 once again tells us that the length we | |
| 37:11 want is equal to the cosine of this | |
| 37:13 angle here divided by the length of this | |
| 37:16 side which we defined as just being one. | |
| 37:19 So all we actually need is the cosine of | |
| 37:21 that angle which we can get perhaps | |
| 37:23 you've heard from the dot product. So if | |
| 37:26 we take the unit vector pointing along | |
| 37:28 the surface and dot it with the unit | |
| 37:31 vector pointing along here, we'll have | |
| 37:33 our answer. Or to make these vectors a | |
| 37:36 little more intuitive, we can imagine | |
| 37:37 rotating both of them by 90°, giving us | |
| 37:40 the normal vector of the surface and the | |
| 37:42 direction pointing towards the light | |
| 37:45 source. Therefore, in our shader, I've | |
| 37:47 added a setting for the direction | |
| 37:49 towards the light and dotted that with | |
| 37:51 the surface normal to calculate the | |
| 37:53 proportion of light hitting the surface. | |
| 37:54 As we saw, this will be negative if the | |
| 37:57 light is pointing away from the surface. | |
| 37:59 So, it's worth just clamping it to zero | |
| 38:01 since negative light doesn't make much | |
| 38:03 sense. But we can then output that as | |
| 38:05 the color of the pixel. And just like | |
| 38:07 that, our dragon is | |
| 38:09 lit. This lighting can look a little | |
| 38:12 harsh though since in reality light | |
| 38:14 bounces around and illuminates darker | |
| 38:16 regions as we saw in the ray tracing | |
| 38:18 episode. Of course, doing this sort of | |
| 38:20 thing in a rasterized renderer is not | |
| 38:23 easy. So for now we can just crudely | |
| 38:25 fudge things if we want. For instance, | |
| 38:27 by remapping the dot product from | |
| 38:29 negative to positive one to be between | |
| 38:31 zero and one instead. And that basically | |
| 38:34 just brightens things up a | |
| 38:36 bunch. Now if we just smush our lit and | |
| 38:39 texture shaders together into a lit | |
| 38:41 texture shader, we can see that the cake | |
| 38:44 from before is looking tastier than | |
| 38:45 ever. I'm happy to announce, by the way, | |
| 38:47 that there'll be free slices at the end | |
| 38:49 for everyone who likes and subscribes | |
| 38:51 right now. First though, I want to set | |
| 38:54 up another simple test quickly. And I | |
| 38:56 think it'd be fun to bring in this | |
| 38:58 little fella that I made about 10 years | |
| 38:59 ago. Obviously, we don't have support | |
| 39:02 for animations yet. That's a whole other | |
| 39:04 can of worms, I'd imagine. So, I've just | |
| 39:06 placed him in a fixed pose of wonder and | |
| 39:09 astonishment. Here's what the model | |
| 39:11 looks like unwrapped, by the way. And it | |
| 39:13 just has a super simple texture map with | |
| 39:15 some blocks of color for the skin and | |
| 39:18 clothes. I also figured he'd appreciate | |
| 39:21 a companion on his adventures. And who | |
| 39:23 better, I reckon, this remarkably | |
| 39:25 ravenous fox from my old ecosystem | |
| 39:30 experiment. So, I've set the two of them | |
| 39:32 up in a little scene here along with our | |
| 39:34 trusty test dragon and a random tree or | |
| 39:38 two. These are all being rendered with | |
| 39:40 our wondrous new lit texture shader. And | |
| 39:42 I also quickly added a scale setting to | |
| 39:45 the transform script because I must have | |
| 39:47 modeled the fox with slightly different | |
| 39:49 units in mind. | |
| 39:52 That just means in the code that we have | |
| 39:53 to multiply each basis vector by the | |
| 39:56 corresponding scale factor so that the | |
| 39:58 vertices are moved to a greater or | |
| 40:00 lesser extent along those directions. | |
| 40:02 And for completeness in our inverse | |
| 40:04 function, we should also then undo that | |
| 40:07 scaling. All right, let's see how it's | |
| 40:09 looking with the scale applied. So here | |
| 40:11 we have boy and fox together looking on | |
| 40:13 in awe at the green dragon in front of | |
| 40:15 them. | |
| 40:18 I think it's really cool that we can | |
| 40:20 render little scenes like this now. | |
| 40:21 Although obviously there is still so | |
| 40:23 much to be done like shadows and | |
| 40:25 optimizations, fixing the flickering | |
| 40:27 floor and so forth. There's no chance | |
| 40:30 we'll get to everything today, but I | |
| 40:32 would at least like to have a go at this | |
| 40:34 floor problem. So remember the trouble | |
| 40:36 we ran into is that when a vertex of a | |
| 40:38 triangle goes behind the camera, in | |
| 40:40 other words, it has a depth of zero or | |
| 40:42 less, we can't divide by that depth | |
| 40:45 anymore without causing a mess. A sneaky | |
| 40:48 idea we could try though is simply | |
| 40:50 clamping the depth value so that it's | |
| 40:52 forced to be above zero. That might not | |
| 40:55 seem like it should work, and indeed it | |
| 40:57 absolutely does | |
| 40:59 not. Speaking of absolutely though, my | |
| 41:02 next brain wave was to take the absolute | |
| 41:04 value instead. That way, the depth will | |
| 41:06 always be positive without us having to | |
| 41:08 cut it off anywhere. This went about as | |
| 41:12 well as you might | |
| 41:14 expect. What we're looking at here is | |
| 41:16 actually a slight variation of that idea | |
| 41:18 using the magnitude of the view position | |
| 41:20 as the depth rather than just the | |
| 41:22 absolute Z. The result is pretty | |
| 41:24 similar, but this one has a nice funky | |
| 41:26 sort of spherizing effect. | |
| 41:29 Anyway, these desperate attempts are | |
| 41:31 clearly not going to cut it. Instead, | |
| 41:33 what we're going to have to cut is the | |
| 41:35 triangles | |
| 41:36 themselves. So, when a vertex crosses | |
| 41:39 over the boundary into the forbidden | |
| 41:40 lands, we'll just lop off the tip, which | |
| 41:43 leaves this little quadrilateral here | |
| 41:45 that we'll then have to turn into a pair | |
| 41:47 of triangles on the | |
| 41:49 fly. Another situation that could occur | |
| 41:52 is that two of the vertices could cross | |
| 41:54 the boundary. And in that case, we only | |
| 41:56 have to form a single new triangle. or | |
| 41:59 obviously if all three are beyond the | |
| 42:00 boundary then it's a lost cause and | |
| 42:02 we'll abandon it | |
| 42:04 entirely. So each model now holds a list | |
| 42:07 of rasterization points. I don't really | |
| 42:09 know what to call them but it's just the | |
| 42:11 screen position of each vertex along | |
| 42:13 with the corresponding texture | |
| 42:14 coordinates and so on. Then each frame | |
| 42:17 we run through all the triangles in the | |
| 42:18 model and calculate their viewace | |
| 42:20 positions so that we can test which of | |
| 42:22 their depths are below our clipping | |
| 42:24 threshold. After that, we can count up | |
| 42:27 how many vertices will need to be sliced | |
| 42:29 off. And if that number is zero, then we | |
| 42:31 simply add the three points to the list | |
| 42:33 without any | |
| 42:34 modifications. However, if one of the | |
| 42:36 vertices does need to be cut, in that | |
| 42:38 case, we'll calculate where along each | |
| 42:40 of the connecting edges the depth cutff | |
| 42:43 occurs. First as a fraction between 0 | |
| 42:45 and one, and then using a linear | |
| 42:47 interpolation to get the points | |
| 42:48 themselves, which we can use to create | |
| 42:50 our two new triangles. By the way, we're | |
| 42:53 passing the fraction values in here so | |
| 42:55 that when we add in the texture | |
| 42:57 coordinates and normals for those new | |
| 42:59 points, we can just do some more | |
| 43:00 interpolating to figure out what their | |
| 43:02 values should | |
| 43:03 be. Now, slicing off two vertices | |
| 43:06 instead of one is just more of the same | |
| 43:08 sort of code. So, let's head straight to | |
| 43:09 the results here. By the way, we're back | |
| 43:12 to using just two triangles for the | |
| 43:13 floor. None of that subdivision trickery | |
| 43:15 from before. And as you can see, it's | |
| 43:18 working | |
| 43:19 beautifully. All right. Fine. My first | |
| 43:21 attempt was a total disaster, but by | |
| 43:23 about the 10th attempt, I finally had it | |
| 43:25 working nicely. The colors here is just | |
| 43:27 some debugging I was doing for the | |
| 43:29 different triangle split cases. So, | |
| 43:31 green and magenta are where it splits | |
| 43:33 into two triangles, and yellow is when | |
| 43:35 just a single new triangle is | |
| 43:39 formed. Anyway, I'll remove those debug | |
| 43:41 colors quickly so that we can verify | |
| 43:43 that the texture coordinates are working | |
| 43:45 properly as well. And that's looking | |
| 43:47 good to me. | |
| 43:50 All right, we've come a long way. So, | |
| 43:52 let's do something fun to celebrate. And | |
| 43:54 by fun, I'm referring, of course, to | |
| 43:56 procedural terrain generation. So, this | |
| 43:59 code I've been working on here just | |
| 44:00 calculates an elevation value given some | |
| 44:02 point in the world, which it does using | |
| 44:04 layers of simplex noise with increasing | |
| 44:06 frequency and decreasing amplitude. | |
| 44:10 Then this function here generates a grid | |
| 44:12 of points jiggled slightly for some | |
| 44:13 variation and with elevations calculated | |
| 44:16 in a shocking twist by our calculate | |
| 44:18 elevation function. Finally, we have | |
| 44:21 this function here for actually | |
| 44:22 assembling the mesh triangles from that | |
| 44:25 grid. By the way, I've stored the | |
| 44:27 average elevation of each triangle in | |
| 44:29 the texture coordinates, which obviously | |
| 44:31 wouldn't be helpful for actually | |
| 44:32 sampling a texture, but we can get | |
| 44:34 creative and store whatever arbitrary | |
| 44:36 data we want in there. So, here's what | |
| 44:38 our mountainous mesh is looking like at | |
| 44:40 the moment. Not too bad, I'd say, but a | |
| 44:42 bit bland in the color department. So, | |
| 44:45 let's liven things up with a special new | |
| 44:47 terrain shader. In here, we've got a few | |
| 44:49 floats defining the elevation range of | |
| 44:52 these color bands. And then for every | |
| 44:54 pixel, we do a simple loop to search for | |
| 44:56 the appropriate color based on that | |
| 44:58 triangle's height from the texture | |
| 45:00 coordinates. And now that is looking | |
| 45:02 much more lively. | |
| 45:04 It is a rather small chunk of terrain | |
| 45:06 though, so I've also quickly been | |
| 45:07 setting up a simple system to generate | |
| 45:10 new chunks as we move around, making the | |
| 45:12 terrain effectively infinite. With that | |
| 45:15 in place, we can now fly around to our | |
| 45:16 heart's content. At approximately five | |
| 45:19 frames per second, that is, which my | |
| 45:21 heart is not particularly content with. | |
| 45:24 So, I've been doing some quick | |
| 45:25 optimizations. Like for instance, | |
| 45:27 whenever a transform has its rotation | |
| 45:29 modified, it now stores the resulting | |
| 45:31 basis vectors rather than having to | |
| 45:33 recomputee those over and over for every | |
| 45:35 vertex. I also tried to optimize the | |
| 45:38 triangle test, which did speed things up | |
| 45:40 a bunch and even came with a funky free | |
| 45:42 side effect. Following that huge | |
| 45:44 success, I left some aggressive | |
| 45:46 suggestions to the compiler in several | |
| 45:48 spots and flattened our color and depth | |
| 45:50 buffers into one-dimensional arrays, | |
| 45:53 which tend to be a bit faster to work | |
| 45:54 with. | |
| 45:56 I also made this giant array of objects | |
| 45:58 for helping with some tremendously crude | |
| 46:00 parallelism that I have planned in our | |
| 46:02 rendering loop. I'll need to think about | |
| 46:04 how to do this better in the future, but | |
| 46:06 for now, when it comes time to write to | |
| 46:08 a pixel, we place a lock on its | |
| 46:10 corresponding object so that any other | |
| 46:12 threads trying to access it at the same | |
| 46:14 time will be forced to wait | |
| 46:16 that. And now our little scene | |
| 46:18 containing a few hundred,000 triangles | |
| 46:20 is running wonderfully smoothly at about | |
| 46:23 90 frames pers. Of course, this is on a | |
| 46:26 960x 540 window, which is rather | |
| 46:29 minuscule, so we should calibrate our | |
| 46:31 enthusiasm accordingly. Anyway, what | |
| 46:33 this scene is definitely missing is a | |
| 46:35 little plane to zoom about in. So, I've | |
| 46:37 gone ahead and modeled one here, but it | |
| 46:39 itself is missing a pilot and passenger | |
| 46:42 at the moment, so I'll need to fix that. | |
| 46:44 And of course, they can hardly go | |
| 46:45 anywhere if the propeller isn't spinning | |
| 46:47 around on the front. So, I've quickly | |
| 46:49 gone and added the last missing axis of | |
| 46:51 rotation, a roll around the Z-axis, | |
| 46:54 which will allow the propeller to spin | |
| 46:55 and the plane to tilt as it turns. Then, | |
| 46:58 we'll want the pilot and passenger and | |
| 47:00 propeller to all be parented to the | |
| 47:02 plane so that they automatically follow | |
| 47:04 its every movement. For that, we can | |
| 47:07 give the transform an optional reference | |
| 47:09 to a parent transform. And when it comes | |
| 47:11 time to actually transform a point, it | |
| 47:13 can first apply its own properties as | |
| 47:15 before, but then pass the point along to | |
| 47:18 its parent, if it has one, to apply its | |
| 47:21 own properties as well, and so on | |
| 47:23 recursively to the grandparents and | |
| 47:25 great-grandparents until the end of the | |
| 47:27 line. So, let's try now saying that the | |
| 47:30 pilot's parent is the plane and see how | |
| 47:32 that goes. | |
| 47:33 [Music] | |
| 47:41 Okay, it's a little oddlooking, but | |
| 47:43 fundamentally seems to be working. And | |
| 47:45 with a few slight adjustments, we've got | |
| 47:48 something now that's closer to what I | |
| 47:50 had envvisaged. The propeller is even | |
| 47:52 spinning around. And since it's parented | |
| 47:54 to the plane as well, we can see how it | |
| 47:56 inherits that rotation as it turns. | |
| 47:59 Truly remarkable technology. | |
| 48:03 All right, there's just two tiny things | |
| 48:04 I want to do before we call it a day | |
| 48:06 here. Firstly, in the terrain shader, if | |
| 48:09 we take in the depth of the current | |
| 48:10 pixel here, that can then be used to | |
| 48:12 blend the terrain towards the sky color | |
| 48:14 in the distance for a bit of an | |
| 48:16 atmospheric effect. And secondly, I | |
| 48:19 think it'd be cute to have some little | |
| 48:20 clouds floating around. So, I've just | |
| 48:22 set up a system to spawn some blobby | |
| 48:24 meshes in the vicinity of the player. | |
| 48:26 And these scale up and down over the | |
| 48:28 course of their lifetime and once | |
| 48:30 they've disappeared are just recycled in | |
| 48:32 some other | |
| 48:33 location. All right, I think that that | |
| 48:35 is all. So hold on to your hats because | |
| 48:37 it's finally time to fly away. | |
| 48:40 [Music] | |
| 48:50 I'm quite happy with the progress we've | |
| 48:51 made here, but I just want to take a | |
| 48:53 minute to think about the main things | |
| 48:54 I'd still like to do if we return to | |
| 48:56 this in the future. I think support for | |
| 48:58 transparency would be nice to have. So, | |
| 49:00 we could make these clouds slightly | |
| 49:01 see-through, for instance, and shadows | |
| 49:03 for sure would be great to have as well. | |
| 49:06 These things are fairly expensive, | |
| 49:07 though, so putting some proper thought | |
| 49:09 into optimizations would definitely be | |
| 49:11 wise. Also, I've just noticed that the | |
| 49:14 lighting on the plane is not actually | |
| 49:16 changing as it turns since it somehow | |
| 49:18 slipped my mind to transform the normal | |
| 49:20 vectors into world space. So, that's at | |
| 49:22 least one bug that needs to be fixed. | |
| 49:24 And knowing me, there's probably many | |
| 49:26 more. Then there are other odds and ends | |
| 49:28 like it would be nice and neat to | |
| 49:29 represent our transformations and the | |
| 49:31 camera projection with matrices. Maybe | |
| 49:34 add support for a vertex stage in the | |
| 49:36 shaders. and textures certainly need | |
| 49:38 some attention to with more sampling | |
| 49:40 options and my mapping and so on. | |
| 49:42 There's certainly plenty to keep on | |
| 49:44 occupied. But that is going to be | |
| 49:46 everything for today. So, thank you for | |
| 49:48 watching. Let me know if you have any | |
| 49:50 suggestions for the future. And until | |
| 49:52 next time, cheers. | |
| 49:58 [Music] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment