read.cash is a platform where you could earn money (total earned by users so far: $ 767,060.24).
You could get tips for writing articles and comments, which are paid in Bitcoin Cash (BCH) cryptocurrency,
which can be spent on the Internet or converted to your local money.
This article assumes that you already have some prior knowledge of the C++ language, specifically the C++11 variant, albeit some C++20 features might also be used. I will seek to produce readable code at the expense of performance.
This software renderer is written in C++, specifically the C++20 variant, albeit we'll be mostly sticking to C++11 features. Discounting any libraries that import textures, or header-only libraries like GLM, the only external library we'll be relying on is SDL2. This article assumes you already know how to add dynamic linking libraries to projects.
So upon up your favourite C++ IDE, and start a new project. I'll be using QtCreator. What greets us, is obviously Hello World....
Hello, world!Hello, world!
#include <iostream>
using namespace std;
int main()
{
cout << "Hello world!" < endl;
return 0;
}
So, what we need to do is to start working. Now, you may feel tempted to write C-style code, where we simply do this:
#include <iostream>
using namespace std;
static const int WIDTH = 640;
static const int HEIGHT = 480;
int main()
{
SDL_Init(SDL_INIT_VIDEO);
SDL_Window* win = SDL_CreateWindow("Software Renderer Tutorial",0,0,WIDTH,HEIGHT,0);
bool isInterrupted=false;
do {
SDL_Event ev;
while(SDL_PollEvent(&ev)) {
switch(ev.type) {
case SDL_QUIT: isInterrupted = true; break;
default: break;
}
}
} while(!isInterrupted);
return 0;
}
But this will lead to difficult-to-read code on the long run. Sure, it's a lot of up-front investment to do, but I recommend already starting with wrapping SDL constructs within C++ classes from the very start, and encapsulating everything.
We applied RIIA onto the SDL_Window, which meant that we no longer have to manually create it or destroy it. We also split the initial main loop into three clearly separate functions, which allow us to cleanly reimplement them when needed.
Effectively, we have a run() function that contains our loop, which keeps running processEvent(), updateLogic() and render() in that particular order until some event causes the program to quit.
As you can see, this code so far doesn't do much so far. It creates a window, but doesn't even fill it with anything. So, before we proceed, I'm going to refactor just three things.
AppSystem is now an abstract class, with three pure virtual functions. All three of them get called within "run".AppSystem is now an abstract class, with three pure virtual functions. All three of them get called within "run".
class AppSystem
{
public:
typedef std::unique_ptr<SDL_Window,decltype(&SDL_DestroyWindow)> uWindow;
private:
AppSystem(const AppSystem& cpy) = delete; // Disable copy constructor
AppSystem& operator=(const AppSystem& cpy) = delete; // Disable copy assignment operator
protected:
uWindow window;
virtual void processEvent(const SDL_Event& ev, bool& causesExit) = 0;
virtual void updateLogic() = 0;
virtual void render() = 0;
public:
// Regular constructors
virtual ~AppSystem() = default;
AppSystem(const char *title, int offsetX, int offsetY, int width, int height, Uint32 flags);
AppSystem(const std::string& title, int offsetX, int offsetY, int width, int height, Uint32 flags);
void run();
};
I'm fully aware that inheritance and the usage of virtual functions are highly frowned upon these days in the programming community, but this will aid code readability.
So, now we're going to create a new class that subclasses AppSystem. I'll call it SoftwareRendererSystem.
#include <iostream>
#include "SoftwareRendererSystem.hpp"
using namespace std;
static const int WIDTH = 640;
static const int HEIGHT = 480;
int main()
{
SDL_Init(SDL_INIT_VIDEO);
SoftwareRendererSystem app(WIDTH,HEIGHT);
app.run();
return 0;
}
And with this, we are finally producing something...
But I think I owe you all an explanation on what just happened, or rather, what is all this new code. What does SDL_TEXTUREACCESS_STREAMING event mean?!
Well, first of all, the SDL_Renderer takes care of presenting our texture on the screen. The SDL_Texture is, as its name says, a texture, the one that we'll be putting onto the screen, which makes it actually a framebuffer. We use SDL_RenderCopy() to ensure that the renderer will present the texture, then SDL_RenderPresent() to put it onto the screen. Just like with the Window, we are applying RIIA onto the Renderer and Texture. SDL_TEXTUREACCESS_STREAMINGsimply means that we'll be modifying the texture very often - like, uh, every single frame?
However, this black is obviously rather boring, so we'll need to find a way to put colour onto the screen. We'll need another texture - one we can access directly - to store pixel data we are currently modifying, and then we can run SDL_UpdateTexture() to update our framebuffer from it, effectively creating a form of double-buffering.
At this point, you might be pulling out your own hair, at the amount of work we have to do perform just to put some stinking pixels onto the screen, but all the up-front investment will pay off in the end.
Now, once again, I'm fully aware that I am making a million anti-OOP programmers scream in agony, as I am declaring an abstract class with pure virtual functions with the full intent of having other classes inherit it, but trust me, it'll improve code readability and re-usability. If performance was the main concern, I'd be probably relying on templates.
With this trusty little template, we can easily wrap around any pixel format, as long as we feed it a struct that contains the fillKernel and fromKernel functions, both taking a reference to a glm::vec4. So, let's implement one.
Okay, so now we can have textures of 8-bit RGBA type, that we can directly modify. I'm not going to get into details on how to manipulate pixels bitwise, there are already a million articles on that - if it's highly requested of me, I might get into dithering though.
So anyway... What now?
Well, we put it into our renderer!
Okay, so, explanation time. What does this code do now?
The first thing that warrants explanation is the SDL_UpdateTexture() part. It copies the raw bytes out of our render buffer, and copies them into the framebuffer. So basically, we have a double-buffering of sorts going on.
The loop above it is a a quick demonstration, where we basically draw a gradient that looks sorta like...
Be patient, my child. We just took the first great step towards implementing software-rendering, manipulating pixels that are going to appear on the screen.
Still, if you are THAT impatient, you get a sneak peak from the next episode: