Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save SMUsamaShah/44c608a869e78c3ed2376cecb19ecaef to your computer and use it in GitHub Desktop.

Select an option

Save SMUsamaShah/44c608a869e78c3ed2376cecb19ecaef to your computer and use it in GitHub Desktop.
Sebastian League Software Rasterization video transcript
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