nanoMMO

Bloom and cel shading in C++ with raylib

Bloom from raylib's post-processing example, done the way games do it, with a frame of floats and lava that gives off light, then cel shading from raylib's example, FXAA, and settings for them.

Watch on YouTube

We add post-processing to our MMORPG's client with raylib, starting from its bloom example, which washes the whole world out. So we build bloom from what's brighter than white, have the lava and second wind give off light, and then add cel shading with flat bands and outlines, and FXAA to smooth the edges. This is episode 33 of a series about building an MMORPG from scratch in C++, with the focus on the engineering around the game.

The code

Each step on screen is a tag in the game's repository. The code at the end of the episode is ep033/end.

  1. 2:46ep033/step-1A frame of our ownBrowseChanges
  2. 6:55ep033/step-2A frame of our ownBrowseChanges
  3. 8:51ep033/step-3The example's bloomBrowseChanges
  4. 11:02ep033/step-4Brighter than whiteBrowseChanges
  5. 19:11ep033/step-5Light of its ownBrowseChanges
  6. 22:03ep033/step-6Cel shadingBrowseChanges
  7. 24:43ep033/step-7Cel shadingBrowseChanges
  8. 30:21ep033/step-8SettingsBrowseChanges
  9. 36:14ep033/step-9Smooth edgesBrowseChanges

Transcript

Intro

0:00 Hello and welcome back to episode thirty-three, where we're going to give the game bloom, the glow around what's bright, and cel shading, starting from raylib's examples. Let's begin with a quick look at where we're going to be by the end of the episode.

0:15 By the lava in the Ashlands, its light glows over the shore and around the trees. Second wind's glow blooms around alice too, and in the settings window, with the bloom off, the lava's glow goes, and back on, it's there again, and cel shading gives the world a cartoon's look.

0:31 On the website of the series you can find more information about the project. The code for the project is open source on GitHub, tagged at each step of each episode so that you can follow along.

Last time

0:43 Now, before we get to today's work, let's quickly go over what we did last time. The client opened raylib's audio device, which plays through miniaudio. The music streams from its file, and it played once we refilled its buffer each frame, which we found out when it made no sound. A jukebox fades it in as the player enters the world, rests between plays, and fades it out as they leave. The settings window got a volume, the music and a mute, with sliders that follow the decibels, and the client keeps them in a file, written next to the old one and renamed over it. And the track is Pavel Bekirov's, from Pixabay.

Today

1:22 So far, the client draws the world straight onto the screen, and the lava in the Ashlands is a flat orange, lit by the sun like the ground around it. Today, the lava and the spells are going to glow, with bloom. Bloom works on the whole picture, so first, the world goes into a frame of our own, which the screen gets from there. For the bloom, we'll start from the shader in raylib's example. The lava and second wind's glow are going to give off light of their own, so that they glow. Then cel shading, from another of raylib's examples, which gives a world a cartoon's flat colours and outlines. The settings window turns each of them on or off, and the client keeps that with the sound, and the world's edges get smoothed with FXAA, a shader that games use for anti-aliasing. All right, let's get started, with the render texture in raylib's post-processing example.

2:16 raylib's example for post-processing has a dozen effects, each a shader in its resources folder, with bloom among them. It makes a render texture, which is a texture the GPU can draw into, as big as its window. Then each frame, it draws the scene into it, between begin texture mode and end texture mode, and draws the texture onto the screen through the shader picked. It's drawn with a negative height, as this note says: a render texture has to be flipped, for OpenGL's coordinates.

A frame of our own

2:47 Our post-processing goes in a header of its own, postprocess dot h, which includes raylib, for its render texture. Post process is what the world goes through on its way to the screen, and its comment says how. The renderer draws the world into a frame of our own, a render texture, and the frame goes to the screen from there, so that shaders can work on the whole picture first. The GUI goes on top, as it is, since bloom is for the world's light. The constructor is the default for now, and like our other classes that hold things on the GPU, post process can't be copied. Begin and end bracket what's drawn into the frame, as their comment says, and draw puts the frame on the screen. The frame is raylib's render texture, which is a framebuffer with a texture for its colours and a buffer for its depth, and the empty braces start it at zero, before it's loaded. Postprocess dot c c includes raylib, and rlgl, raylib's layer over OpenGL, for a blend that raylib's own functions don't have. The destructor unloads the frame, and raylib skips one with no id, so a frame that wasn't made yet, as when the player quits at the login screen, is fine. Begin takes the frame's size from the screen's width and height, like the example does, and the comment says when that changes: it's as big as the screen, made again when the window changes size. When the size doesn't match, it unloads the old frame and loads a new one. Loading a render texture each frame would be slow, so it's made once and kept. Then begin texture mode points raylib's drawing at the frame, until end texture mode points it back at the screen. Draw puts the frame on the screen, and normally, raylib blends what it draws into what's there, by how see-through it is. But over the sea, the frame is about eighty-five percent opaque, as raylib blends the alpha like the colours: the sea's alpha times the sea's alpha, plus the sky's times what's left. So we give rlgl a blend that takes all of the frame and none of what was on the screen: one times the frame, plus zero times what's there. Those are OpenGL's blend factors, which rlgl hands on as they are. The sea and the glows are see-through, and leave the frame see-through where they are, and blending it in would let an older picture show through. Begin blend mode with custom uses the factors we gave, until end blend mode puts raylib's usual blending back. The frame's texture gets a short name, as we need its size twice. Its comment is the example's note, in our words: OpenGL keeps a texture's rows from the bottom up, so the frame is drawn with a negative height, which flips it the right way up. So draw texture rec puts it at the top left corner of the screen, from a rectangle as big as the texture, with its height negative. Postprocess dot c c joins the client's library in its CMake file, and main dot c c includes its header. The client makes the post-processing next to the renderer, after the window, so that it's destroyed before the window closes, as unloading the frame needs raylib's window open. Before drawing, a comment says what changes: the world goes into a frame of its own, which the screen gets from there. While we're in the world, the renderer draws between begin and end, into the frame. That happens before begin drawing, as raylib can draw into a texture outside of it, like the character screen's preview does. Then, inside begin drawing, the frame goes on the screen where the renderer drew before, with the GUI on top. And the README gets a section on post-processing, which starts with the frame.

6:21 Let's build it and log in, and the world comes to the screen through our frame. Up close, though, it's coarse: alice and the ground have edges in steps, in blocks, while her name over her is as sharp as before. This screen has two pixels to each point each way, like a Mac's Retina screen, and get screen width gives points, which is what we draw the GUI in. So our frame has a quarter of the screen's pixels, and each of them covers a block of two by two. The example's window isn't high-DPI, so there, points and pixels match.

6:56 raylib has two sizes for the screen: get screen width, in points, and get render width, in pixels, which is how big the framebuffer really is. So the frame takes the render width and height, and the comment says why: in pixels, as a high-DPI screen has more of them than points. It's made again when the window changes size. raylib draws on the screen in points, though, so draw texture rec would now put the frame on the screen at twice its size each way, mostly past the window's edges. The comment over the draw says what we do about it: we draw on the screen in points, so the frame's pixels are fitted to the screen's points. Draw texture pro takes a source rectangle and a destination one, so the source stays the whole texture, with its negative height. And the destination is the screen's size in points. The zeros after it are the rectangle's origin and its rotation, which we don't need. In window dot c c, the window asked for four times multisampling, which smooths the edges of what's drawn on the screen. A render texture has one sample to a pixel, so the world doesn't get it any more. The flag would cost four samples a pixel for the GUI, which ImGui smooths on its own, so it goes. On a high-DPI screen, a jagged edge is a pixel wide, and smoothing the world again would take an anti-aliasing pass of our own. And the README says how the frame is sized.

8:17 Now the world is as sharp as the screen, and up close, the edges of alice and of the ground are a pixel wide, as sharp as her name.

The example's bloom

8:26 The example's bloom is a fragment shader, a small program in GLSL, OpenGL's shading language, which the GPU runs for each pixel it draws. It has the frame's size written in, as eight hundred by four hundred and fifty, which is the example's window. For each pixel, it adds up the twenty-five pixels around it, two and a half pixels apart, which blurs the picture, and then it adds their average on top of the pixel.

8:52 Post process gets a constructor of its own, which is going to load the shader, and it keeps the shader, next to the frame. The shader goes in postprocess dot c c, as a raw string, like the world's shaders in the renderer. It's the example's, which we just read: each pixel gets the average of the twenty-five around it, spread two and a half pixels apart, added to it. Like the example's, it takes what raylib's vertex shader hands on: the texture's coordinates for this pixel, and a colour. Texture zero is the texture being drawn, our frame, and col diffuse is a colour raylib sets to white when it draws like this. The size comes in as a uniform, which is a value we set from C++, as our frame is the screen's size, not the example's window. Two and a half divided by the size is the distance between the samples in the texture's coordinates, which go from zero to one across it. The example works its range out as two, from its five samples, and we write the two into the loops, which add up five by five pixels around this one. The pixel gets their average on top of its own colour, tinted, as final colour. The constructor loads it with no vertex shader of ours, and the comment says why that's enough: raylib's own vertex shader passes the texture's coordinates on. The destructor unloads the shader after the frame, like the renderer unloads its own. Draw starts with the frame's texture, and puts its size into a vector of two floats. The comment over it says why: the example writes its frame's size into the shader, as eight hundred by four hundred and fifty, and ours is the screen's. Set shader value sends the size to the uniform, which get shader location finds by its name. Then the frame goes to the screen through the shader, between begin and end shader mode, inside our blend.

10:39 The whole world is washed out, about twice as bright, with the grass a light lime and the trees pale. The shader adds a blurred copy of the picture onto it, however dark each pixel is, so everything gets brighter, and the glow is a few pixels wide on our screen. Bloom is meant for the light around what's brighter than the rest, such as the sun or lava, so a bloom shader picks those out first.

Brighter than white

11:03 A pixel has to be past white for bloom, like the sun, a fire or lava would be, and raylib's render textures keep eight bits to a channel, which stop at white. So the frame is going to keep more than that, and the bloom is going to take what's past white, blur it widely and cheaply, and add it back. Postprocess dot h includes array, for a list of textures that has a fixed size. Draw isn't const any more, as it draws into textures of its own. The bloom works in halves of the frame, each half the size of the one before, as the comment says, and there are five of them. Resize is going to make the textures for a screen's size, and three shaders replace the example's, for what's bright, the spread and the combine, which we're going to write next. The halves are an array of five render textures, which start at zero, like the frame. In postprocess dot c c, algorithm comes in, for std max, as each half has to be at least a pixel. The first of the three raw strings picks out what's bright: what's past white, from none of a colour whose brightest channel is just at it, to all of one at twice as bright. Like the example's shader, it reads the texture's coordinates and texture zero, and writes a final colour. It needs no tint, so it leaves out raylib's fragment colour and col diffuse. It reads the pixel's colour, as red, green and blue, as the bloom has no use for how see-through it is. Then it takes the brightest of the three channels, so that a saturated orange counts as bright, where an average would let its blue pull it down. It keeps a share of the colour, from none of it at a brightest channel of one to all of it at two. Anything at white or below gives black, so it adds nothing to the bloom. The spread shader is for the halves, as they double back up. Its comment says why: it spreads a half as it doubles, over the nine of its pixels around each one, so that the light fades out smoothly, where doubling alone would leave blocks. A half drawn at twice its size, with a filter that blends the four pixels nearest to where it's read, would show its pixels as soft squares. It reads the texture's coordinates and texture zero, like the bright shader. It takes the size of a pixel of the half it reads, as a uniform. Then it reads the nine pixels around this one, three by three, with a weight of four in the middle, two on each side and one in each corner, which adds up to sixteen. That's a tent filter, so called for its shape. Dividing by sixteen keeps the light as bright as it was, spread out. The combine shader puts the frame on the screen with the bloom added on top, as its comment says. It reads the frame from texture zero, and the bloom from a second texture, which we bind from C++. The bloom is added times a strength, and the alpha is one, so that the frame comes out opaque, where the sea left it see-through. The strength is a quarter, a fraction, as the halves add up. Now the frame, which load frame makes like raylib's load render texture does, with half a float to each channel. A half float goes up to about sixty-five thousand, and the comment says what that changes: it can go past white, where raylib's eight bits stop at it. The format is raylib's name for sixteen bits for each of four channels, which OpenGL keeps as half floats. rlgl loads an empty framebuffer, and a texture in our format, with no pixels to start from, and one level, so no mipmaps, the smaller copies a texture can keep, as the halves are our own. The texture's fields are set by name, with designated initialisers, so that each value says what it is. Then a depth buffer, as a renderbuffer, so that what's near hides what's far, and it attaches the two to the framebuffer, as the colour and the depth. The frame's colours take twice the memory of eight bits: at two thousand five hundred and sixty by fourteen forty, about thirty megabytes. Draw over draws a texture over the whole of a target, with the flip we had. The target's size is in points for the screen and in pixels for a texture, so it comes in as a width and a height. Draw into does it into another render texture, between begin and end texture mode, at that texture's size, which is in pixels. Inside texture mode, raylib draws in the texture's pixels, so the two sizes we pass match what it expects. The constructor loads the three shaders, with raylib's vertex shader again, as each of them draws a texture over a rectangle. The destructor unloads the halves as well, after the frame, and then the three shaders. Begin calls resize when the screen's size changes, where it loaded the frame before, as there are six textures to make now. Resize unloads the old frame and loads one in our format, with that bilinear filter, which blends the four nearest pixels. Its comment says why that's handy: halving a texture with the bilinear filter averages each two by two block of its pixels, so the GPU does our averaging as it reads. Then the halves, in a loop that halves the width and the height each time around. Each half is half the size of the one before, at least a pixel, so on a screen of two thousand five hundred and sixty by fourteen forty, the smallest is eighty by forty-five. A half from an earlier size is unloaded first, like the frame. Each is in our format, so the light past white keeps its strength as it spreads, and each has the bilinear filter too, as each is read at half its size or at double. Together, the five halves come to about a third of the frame's memory. Then draw starts with the bright shader, which draws what's past white into the first half: what's bright, in a frame half the size. Then each half goes into the next, smaller one, so that the smallest half spreads its light the widest, for few pixels, as the comment says. These go through raylib's own shader, as the bilinear filter does the averaging. A pixel of the smallest covers thirty-two by thirty-two of the screen's. Then the halves are added back up, from the smallest, each spread as it doubles, and additive blending adds each one onto the half above it, which keeps its own light. Additive is raylib's blend mode that adds what's drawn to what's there, instead of covering it. They go through the spread shader, and the loop counts down, from the last half to the first. The spread shader needs the size of a pixel of the half it reads, in the texture's coordinates, so one over its width and one over its height, which set shader value sends before each draw. Each half is drawn into the one above, so the first half ends up with the light of all five, from a narrow glow to a wide one. End shader mode and end blend mode put raylib back as it was, and the bloom is in the first half. Then the frame goes to the screen, under the comment we had over the frame's draw, which has the bloom in it now. It says how: with the bloom on top, fitted to its points, as raylib draws on the screen in points. And it says why the blend we wrote for the frame goes: the sea and the glows leave the frame see-through where they are, and the combine shader writes it opaque, so that it covers what's on the screen. raylib's usual blending, at an alpha of one, takes all of the frame and none of the screen, as ours did. So the combine shader puts it on the screen, with raylib's usual blending. Begin shader mode makes it the shader raylib draws with, until end shader mode puts raylib's own back. It draws with the first half bound as its bloom texture, inside the shader mode, as raylib binds it for the next draw, on the texture unit after the frame's. The strength goes in as a uniform, and draw over puts the frame on the screen at the screen's size in points. And the README says how the bloom works, from what's past white to the halves added back up. Where it said that the frame replaces what's on the screen, it now says that the shader writes the frame opaque, as blending it in would let an older picture show through.

18:56 This time, I've put alice by the lava in the Ashlands, and the world looks as it would without a bloom. The lava stays under white, so there's no glow around it. The sun lights the lava like it lights the ground, and in the Ashlands' red light, its brightest channel stays under one.

Light of its own

19:12 Lava gives off light of its own, and so does a spell's glow, which a game calls emission, and that's what takes them past white. So a land's look gets the light its sea gives off, its emission, zero unless a land says otherwise. The Meadows' sea is water, so it leaves it out, and gets zero. The comment says how much one is: it's added to the sun's, where one is as bright as a face turned to a white sun. Then the Ashlands' lava gets a redder colour, as it was a yellow orange, which the Ashlands' red sunlight made orange. With light of its own, it would come out yellow, so its green and blue go down. And the lava, as its comment says, gives off one. The renderer keeps the sea's emission from the look, and the world's fragment shader takes it as a uniform, called emission, which is zero until we set it. The light that reaches a face, from the sun and the ambient light, gets the emission added, before it multiplies the face's colour. Second wind's glow gives off half as much as the lava, and its comment puts that in the sun's terms: half the light of a face turned to a white sun. Set emission sets the uniform for what's drawn next, and its comment says why: the light it gives off of its own takes it past white, so that it blooms. Set terrain is where the renderer gets a land's look, so it keeps the land's emission there, next to its atmosphere. The renderer sets it before it draws the sea. raylib draws a mesh as soon as we ask it to, so the uniform holds for that draw. The glows' emission is set before them, under their comment, which now says how they're drawn too: a glow is light, so it adds to what's behind it, where blending would cover it. So additive blending adds the glows to the player inside them, the way light does. After the glows, the emission goes back to zero, for the next frame's ground. In postprocess dot c c, the combine shader changes too. It takes the colour's brightest channel, like the bright shader does. The comment over it says why: the screen cuts each channel at white on its own, which turns a bright orange yellow, as its green goes past white as well. So a colour past white gets divided by its brightest channel, which scales it down until that channel is at white, and keeps its hue. Below white, the division is by one, so it changes nothing. And the README says what gives off light.

21:33 Now the lava glows, and its light spills over the shore and around the trees in front of it. Second wind's glow blooms for the second of its cast, with alice showing through it.

Cel shading

21:44 raylib's cel shading example works in two shaders, and the first one's fragment shader, cel dot f s, makes the bands. It takes a number of bands, and rounds each light's share down to one of them, so that the light steps from band to band, where it would fade smoothly.

22:04 raylib's example makes its bands in the fragment shader, which runs once a pixel, and our light comes from the vertex shader, which runs once a vertex, blended across each triangle. So our lighting moves into the fragment shader first. The vertex shader hands on the vertex's normal, turned like the model by the normal matrix, and the sun's uniforms leave it. The fragment shader gets it blended across the triangle, like the colour and the depth. The normal isn't normalised here, as blending it across a triangle shortens it, so the fragment shader normalises it. The skinned vertex shader hands its normal on too, turned by the bones as well. The fragment shader's new comment says why it gets the light: it's worked out for each pixel, from the normal the vertex shaders hand on, so that cel shading's bands follow the surface. The fragment shader takes the normal in, where it took the light, The sun, its light and the ambient light come in here now, with the number of bands. Sunlit is how much of the sun's light this pixel gets, from the normal and the sun's direction, as the vertex shader had it. Then the bands, and the comment over them says what they are: cel shading, like raylib's example, the sun's light in a few flat bands, as a cartoon's colours are, where no bands is smooth. It's the example's formula: the light times the bands, rounded down to a whole band, and divided back down, so that the top band is all of the light. The min keeps a face turned straight at the sun in the top band, as one times the bands would be a band too many. Then the light is what it was, the ambient light, the sun's share and the emission, times the colour. There are three bands, as its comment says: in shadow, half lit and lit. raylib's example starts at ten, and fewer bands make each one wider and plainer to see. The constructor sets them on both of the world's shaders, and the comment says why once is enough: a shader keeps a uniform's value until it's set again. And the README says what cel shading does.

24:02 Now the world's light comes in three flat shades, and up close, each of the NPCs' balls has a lit side, a half lit band and a shadow, like a cartoon's, and so does alice. The ground's faces were flat already, so each takes the band its light falls in.

24:17 The other half of the example's cel shading is an outline. It loads a second shader for an inverted hull, which, as its comment says, draws back faces extruded along normals. Each frame, it culls the front faces and first draws the model with that shader, a little bigger and in a dark colour, and then it draws the model as usual. The back of the bigger hull shows around the model's edges, and the model covers the rest of it.

24:44 Our outlines get a header of their own, outline dot h, which includes raylib. An outline is a pair of shaders, one for each kind of model. Its comment is the example's hull: a model is drawn a second time, a little bigger and inside out, in a dark colour, so that it shows around the model's edges. There's a shader for models without bones, and one for models with them, as our characters are posed on the GPU, and a shader that pushes their vertices out has to pose them first. Still is for models without bones, like the NPCs' balls and the weapons, and skinned for the characters and the monsters. Draw outline draws a model with one of them, and its comment says how: inside out, with its front faces culled, so that its back faces show, behind the model. Outline dot c c includes raylib, raymath for its matrices, rlgl for the culling, and vector. Draw outline is a function of its own, as the characters and the renderer both draw outlines. raylib draws a model with its materials, and each material has its shader. The comment over them says what we make: a copy of the model, with the outline's shader in its materials, so that raylib draws it posed, with its bones, as it does the model. raylib sends a model's bones to its shader when it draws the whole model, so drawing the meshes one by one would leave them out. So draw outline copies the materials into a vector, and gives each of them the outline's shader. The example swaps the shader in its model's material and back. Our model comes in const, with several materials, so draw outline copies them instead, and the model keeps its own. Then it makes the copy, which shares the model's meshes and its bones, with those materials, and the transform it's given on top of the model's own. rlgl culls the front faces while the copy is drawn, and puts the culling back to the back faces after it. Outline dot c c joins the client's library, and characters dot h includes outline dot h. Draw takes an outline, and its comment says when it draws one: when it's given one, as a null pointer means none. In characters dot c c, draw has posed the figure by now, and the matrix that places it moves up, from the weapon further down, so that the outline is drawn with it, with the skinned shader, before the figure. Drawing it here, right after the pose, means that a figure isn't posed twice. The weapon gets its outline too, with the still shader, as its transform is in the model already. The renderer includes outline dot h and keeps an outline. In renderer dot c c, the outline's shaders come after the world's. Its comment says what the vertex shader does: each vertex pushed out along its normal, in metres, so that the model is drawn a little bigger. Like the example's, it reads each vertex's position and its normal. It takes the model's matrix, the normal matrix, the view and the projection, which raylib sets for each draw, and the thickness. The vertex goes into the world with the model's matrix, and out along its normal, in the world, by the thickness. Then the view and the projection put it on the screen, and its depth is handed on, for the fog. The skinned one does that too, and its comment says it's posed like the skinned vertex shader. So it takes the bones' indices and weights for each vertex, and the matrices of up to a hundred and twenty-eight bones, which raylib sends for the model. Each vertex follows up to four bones, as the skinned vertex shader's do. These four lines are the skinned vertex shader's, so that the hull takes the figure's pose this frame, where unposed it would stand in the T of its bind pose. The bones' matrices are blended by their weights, which turn the normal, and move the vertex. The outline is pushed out after the pose, so that it follows an arm or a leg as it moves. The fragment shader's comment says what colour an outline is: nearly black, into the fog like the rest of the world. It takes the fog's uniforms, like the world's, and no light, as an outline is one colour all over. It fades into the fog like the world's fragment shader does, from a dark grey, like the example's. An outline is five centimetres thick, and its comment says why that's in metres: an outline is thinner the further away it is, like the rest of the world. The example pushes out in its model's units, before its matrix, and ours are scaled, the characters to their height and each ball to its radius, so we push after it, in metres. The fog's uniforms move into a function of their own, set fog, which set atmosphere calls first, as the outline's shaders take the fog but no light. The constructor loads the outline's two shaders, from the vertex shaders above, which share the fragment shader, and sets the thickness on both, once, as it doesn't change. The destructor unloads them with the world's shaders, and draw sets their fog each frame, from the land's atmosphere, as the preview does with its own. The renderer draws the NPCs' balls, so it draws their outlines too. A ball gets its outline before the ball, with the ball's own matrix, scaled to its radius and raised to rest on the ground. And the characters and the monsters get the outline, as draw's last argument. And the README says how the outlines are drawn.

30:04 Now the figures have outlines, and up close, alice, the skeletons and the NPCs' balls each have a dark edge, which is thinner the further away they are. Where the parts of a character's body meet, one part's hull shows through another now and then, as a dark sliver, as an inverted hull outlines each part on its own.

Settings

30:21 Bloom and cel shading cost GPU time and change the game's whole look, which is the player's taste, so they go into the settings. They get a header of their own, graphics dot h, like the sound's. Graphics settings is how the world looks on its way to the screen, as the player sets it in the settings window. Bloom is on by default, and cel shading is off, and its comment says why: the sun's light in bands, and outlines, like a cartoon, is a look that's the player's to choose, so it starts off. The equals operator is defaulted, like the sound's, so that main can compare them, to see when the settings change. Postprocess dot h includes the new header, and begin takes the settings now. Its comment says what draw does with them: it puts the frame on the screen as the settings say, with bloom if it's on. The bloom's passes move into a function of their own, and post process keeps the settings. In postprocess dot c c, begin keeps the settings it's given, for draw. The bloom's passes move into bloom, unchanged, which draw calls when it's on, and with the bloom off, the combine adds none of it, as the strength is zero. The renderer gets set cel shading, which turns its bands and its outlines on or off, and its comment says it starts off, like the setting. It keeps whether they're on, to draw the outlines with. In renderer dot c c, the constructor doesn't set the bands any more. Set cel shading does it now, when the setting changes, as the bands are a uniform of the world's two shaders, and returns straight away when it hasn't, as the client calls it each frame. So the bands are three with cel shading on, or none. The comment over them says why starting off needs no call: a shader keeps a uniform's value until it's set again, and with no bands, which a uniform starts at, the light is smooth. They go on both of the world's shaders. With cel shading on, an NPC's ball gets its outline. With it off, the hulls aren't drawn at all, so each figure is drawn once again, not twice. And so do the characters and the monsters, with a null pointer for none, as draw's last argument. The settings file gets the graphics, in settings dot proto. Graphics has the bloom and the cel shading, each optional, like the sound's fields, so that a file from before this episode keeps the defaults. Settings holds the graphics next to the sound, as field two. In settings dot h, the graphics header comes in. Settings is what the player sets, which the client keeps between runs, as its comment says: the sound and the graphics, with an equals operator, defaulted. Load settings gives back all of them, and save settings takes the player's. In settings dot c c, load settings starts from the defaults, called loaded, as settings is the name of protobuf's namespace for our file. The sound's fields come from a reference to the file's sound, which keeps each line short. Each volume still stays between nothing and full, under the comment from last time: a player can write anything into the file. The graphics come from a reference too, with the bloom, and the cel shading. They're true or false, so whatever the file has is fine as it is. Save settings calls what it takes player, as settings is protobuf's namespace here too, and writes the graphics next to the sound, the bloom and the cel shading. In menu dot h, the menu includes the settings' header, where it had the sound's, and build takes the player's settings, the sound and the graphics, as its comment says, and its sliders and mute edit their sound. Under the music's credit, there's a graphics section, with a separator and Font Awesome's display icon, like the sound's and the sky's. Then a checkbox for the bloom, and one for the cel shading. In main dot c c, the client loads the whole of the settings as it starts, where it had the sound before, and keeps a copy of what's in the file. Each frame, the renderer gets the cel shading, which costs a comparison when it hasn't changed, begin gets the graphics, the menu gets all of the settings, and after the menu, the client compares and saves all of them. The music's volume and the master volume read the sound's. And the README's section on post-processing says where the graphics settings are.

34:39 View, settings opens the window, with the graphics under the sound. With the bloom off, the lava's edges are hard again, and back on, its light spills over the shore. Ticking cel shading gives the Ashlands their bands, and alice her outline.

34:57 Now the settings' tests, which move to the whole of the settings, and the first two check the graphics. The first one keeps them between runs now. It builds settings that aren't the defaults, with designated initialisers for each part: the sound from before, and the graphics with the bloom off and the cel shading on. Each value is set away from its default, so a field that save or load dropped would come back as its default and fail the comparison. Saving them has to work, so that's an assert. Loading them back has to work too, and it prints the status when it doesn't, and they have to equal what it saved, all five fields at once, with the equals operators we defaulted. The second test starts with a missing file, which gives the defaults for all of the settings now, compared as a whole with a default settings struct. The file with the music alone loads the music and keeps the default volume, as before, and with no graphics in it, it gets their defaults, which is what a player upgrading from the last episode would have. The last test loads the volumes from before, now from the whole of the settings, and they come back as full and nothing, and the music that isn't a number keeps its default.

36:05 The settings' tests pass, with the graphics kept between runs and defaulted.

Smooth edges

36:14 Since the world went into a frame of its own, it has one sample to a pixel, so its edges and the outlines' are in steps a pixel wide, and we smooth them in a shader instead. FXAA, fast approximate anti-aliasing, by Timothy Lottes at NVIDIA, is a common one, as it's cheap. Anti-aliasing goes into the graphics settings too, as a setting of its own. It's on by default, and its comment says what it does: it smooths the world's edges, with FXAA. Post process gets combine, which draws the frame with the bloom on top at a width and a height, as its comment says. Combine takes a size, as the frame now goes either into the picture, in its pixels, or straight to the screen, in points. There's a shader for FXAA, and a picture, which is the frame with its bloom, as the screen would show it, for FXAA. In postprocess dot c c, the FXAA shader comes after the combine shader. Its comment says what it does: it finds which way an edge runs from the brightness around each pixel, and blends along it. This is the short form, which reads this pixel and the four around it, and four more along the edge, where the full one walks along the edge to find its end. It takes the size of one of the picture's pixels, like the spread shader, and luma, how bright each channel looks to us, as its comment says: green the most and blue the least. Brightness reads a pixel at an offset from this one, counted in pixels, and turns its colour into one brightness, with a dot product. Main reads the brightness of the four pixels diagonally around this one, north west, north east, south west and south east, and of this pixel, in the middle. It keeps the darkest and the brightest of the five, for later. Then it works out the direction the edge runs in. The comment over it says why it comes from differences: across an edge, the brightness changes, so the edge runs at right angles to the change. The first part is the bottom row against the top one, and the second the left column against the right, which turns the change a quarter of the way round, along the edge. Reduce keeps the direction from being divided by nearly nothing where its shorter side is close to zero, as in a flat part of the picture or along an edge that runs straight across. Scale stretches the direction, so that its shorter side is about a pixel, up to eight of the picture's pixels. Near is the average of two samples along the edge, a sixth of the way out on each side, ends adds up two more, half way out, and far averages all four. Reached is far's brightness, and the comment over it says why we check it: reaching further can cross into another edge, which shows as a brightness that isn't around the pixel. So when far is darker than the darkest or brighter than the brightest, the pixel takes near, and otherwise far, with an alpha of one. The constructor loads it, and the destructor unloads it with the picture. The picture is drawn into and then read from, like the frame, so it's a render texture too. Resize unloads the old picture, and the comment over it says why eight bits a channel are enough: it's past the bloom, and white is as bright as the screen goes. So it's raylib's own render texture, at the screen's size, with the bilinear filter, as the comment's last line says: FXAA reads it between its pixels. Draw smooths with the setting on, and the comment over it says why that needs the picture: FXAA smooths the picture as the screen would show it, so the frame and its bloom go into the picture first. FXAA reads nine points around each pixel, so combining inside it would add the bloom nine times a pixel, where a picture of its own does it once. So with FXAA, combine draws the frame and its bloom into the picture, at the picture's size in pixels, as it's a texture. The comment over the draw to the screen says both shaders write it opaque now, the combine and FXAA, so that it covers what's on the screen. Then FXAA draws the picture on the screen, with the size of one of its pixels, at the screen's size in points. The picture has as many pixels as the screen, so it fills the screen's pixels one for one, and FXAA's samples land where it expects them. Without FXAA, combine draws straight to the screen at its size in points. Combine is the end of draw from before, moved into a function of its own: the combine shader, the bloom texture bound, the strength, and the frame drawn at the size it's given. FXAA costs a pass over the screen's pixels, and the picture takes four bytes a pixel at the screen's size. The settings file gets anti-aliasing as field three, optional like the other two, so a file from before keeps it on. Load settings reads it when the file has it, and save settings writes it with the rest of the graphics. And the first test saves it off, away from its default, so a load that lost it would fail. In menu dot c c, anti-aliasing gets a checkbox under the cel shading's. And the README says how the edges are smoothed.

41:06 With the settings window open on the left, and the gate up close, the stones of the gate and the balls by it have smooth edges. Unticking anti-aliasing in the settings window takes their edges back to steps a pixel wide, and with it on, FXAA blends along them again.

Wrap-up

41:22 So, today the client drew the world into a frame of its own, sized in pixels, which we found out when it came out coarse on a high-DPI screen. The example's bloom washed the whole world out, as it blooms each pixel. So the bloom takes what's past white, in a frame of half floats, halves it five times and adds the halves back up. The lava and second wind's glow give off light of their own, which takes them past white, so they glow. Cel shading puts the sun's light in flat bands, worked out for each pixel, and outlines the figures with an inverted hull, posed on the GPU like they are. The settings window has the bloom, the cel shading and the anti-aliasing, kept with the sound, and FXAA smooths the edges that the frame's single sample left in steps.

Playing it

43:15 Let me know in the comments what you'd like to see next. And if you want to follow along with the series, subscribe to the channel. I hope you find this useful, and thanks for watching.