⚡ Programmable rendering pipeline implementation, full Chinese annotation, helping beginners learn rendering principles.
mkdir build
cmake -S . -B ./build
cmake --build ./build --config ReleaseNote that copy the res folder to the path where the executable file is located.
Just find an example file starting with sample_ and directly compile it with gcc single file:
gcc -O2 sample_07_specular.cpp -o sample_07_specular -lstdc++ It seems that I need to add -std=c++17 to my Mac. I shouldn't use anything 17, but I'm not sure if there is no environment. You may need to add a -lm to display and declare the link math library.
run:
./sample_07_specular Then get an image file output.bmp :
The model of this project uses the open source model in tinyrender.
It mainly uses a ShaderContext structure, which is used to pass parameters between VS->PS, and it is full of various types of variations.
// 着色器上下文,由 VS 设置,再由渲染器按像素逐点插值后,供 PS 读取
struct ShaderContext {
std::map< int , float > varying_float; // 浮点数 varying 列表
std::map< int , Vec2f> varying_vec2f; // 二维矢量 varying 列表
std::map< int , Vec3f> varying_vec3f; // 三维矢量 varying 列表
std::map< int , Vec4f> varying_vec4f; // 四维矢量 varying 列表
}; The outer layer needs to provide the function pointer to the renderer VS, and call the three vertices of the triangle in sequence when the renderer's DrawPrimitive function is initialized:
// 顶点着色器:因为是 C++ 编写,无需传递 attribute,传个 0-2 的顶点序号
// 着色器函数直接在外层根据序号读取响应数据即可,最后需要返回一个坐标 pos
// 各项 varying 设置到 output 里,由渲染器插值后传递给 PS
typedef std::function<Vec4f( int index, ShaderContext &output)> VertexShader; Each time the call is called, the renderer will pass the numbers 0 , 1 , index 2 of the three vertices to the VS program in turn to facilitate reading of vertex data from the outside.
The renderer calls the pixel shader for every point that needs to be filled in the triangle:
// 像素着色器:输入 ShaderContext,需要返回 Vec4f 类型的颜色
// 三角形内每个点的 input 具体值会根据前面三个顶点的 output 插值得到
typedef std::function<Vec4f(ShaderContext &input)> PixelShader;The color returned by the pixel shading program will be drawn to the corresponding position of the Frame Buffer.
Calling the following interface can draw a triangle:
bool RenderHelp::DrawPrimitive ()This function is the core of the renderer. First, call VS to initialize the vertex in turn, obtain the vertex coordinates, and then perform homogeneous space cropping, and obtain the screen coordinates of the triangle after normalization.
Then, two layers of for loop iterate over each point of the triangle connecting rectangle on the screen, and then determine that within the triangle range, call the VS program to calculate what color the point is.
Now you want to write a triangle drawing of D3D 12. You can't handle it without a thousand lines, but now we only need the following lines:
# include " RenderHelp.h "
int main ( void )
{
// 初始化渲染器和帧缓存大小
RenderHelp rh ( 800 , 600 );
const int VARYING_COLOR = 0 ; // 定义一个 varying 的 key
// 顶点数据,由 VS 读取,如有多个三角形,可每次更新 vs_input 再绘制
struct { Vec4f pos; Vec4f color; } vs_input[ 3 ] = {
{ { 0.0 , 0.7 , 0.90 , 1 }, { 1 , 0 , 0 , 1 } },
{ { - 0.6 , - 0.2 , 0.01 , 1 }, { 0 , 1 , 0 , 1 } },
{ { + 0.6 , - 0.2 , 0.01 , 1 }, { 0 , 0 , 1 , 1 } },
};
// 顶点着色器,初始化 varying 并返回坐标,
// 参数 index 是渲染器传入的顶点序号,范围 [0, 2] 用于读取顶点数据
rh. SetVertexShader ([&] ( int index , ShaderContext& output) -> Vec4f {
output. varying_vec4f [VARYING_COLOR] = vs_input[ index ]. color ;
return vs_input[ index ]. pos ; // 直接返回坐标
});
// 像素着色器,返回颜色
rh. SetPixelShader ([&] (ShaderContext& input) -> Vec4f {
return input. varying_vec4f [VARYING_COLOR];
});
// 渲染并保存
rh. DrawPrimitive ();
rh. SaveFile ( " output.bmp " );
return 0 ;
}Running results:
| file name | illustrate |
|---|---|
| RenderHelp.h | The renderer implementation file, when used include it is enough |
| Model.h | Loading the model |
| sample_01_triangle.cpp | Example of drawing triangles |
| sample_02_texture.cpp | How to use textures, how to set the camera matrix, etc. |
| sample_03_box.cpp | How to draw a box |
| sample_04_gouraud.cpp | Simple Galaude coloring of the box |
| sample_05_model.cpp | How to load and draw a model |
| sample_06_normal.cpp | Enhance model details with normal map |
| sample_07_specular.cpp | Draw Highlights |
More than ten years ago, I wrote a soft renderer tutorial mini3d, which clearly explained the core principles of software renderers. This is the implementation method of standard soft renderers, mainly based on Edge Walking and scanning line algorithms.
The implementation method of this project is modeled on the GPU's Edge Equation implementation method. The implementation method represented by mini3d is actually relatively complex, but it is very fast and suitable for real-time CPU rendering. The implementation method of this project simulates GPU is relatively simple and intuitive, but the calculation is very large and not suitable for real-time CPU, but it is suitable for rough parallel processing of GPU.
There are many tutorials for implementing programmable rendering pipelines on the Internet, but many of them have problems. For example, the screen coordinates are the point at the upper left corner of the pixel grid. In fact, the point in the center of the pixel grid should be taken, otherwise the edge of the triangle will feel like it will jump when the model moves; for example, how to deal with the edges of the triangle, I basically haven't seen a few correct processing; for example, when the texture sampling, the integer coordinate conversion should be rounded, otherwise the position of several vertices of the texture is not stable enough, and there will be signs of slightly moving; some software renderers are not even able to see the texture with correct perspective, and are still using imitation texture mapping. . . .
There are many very detailed aspects of the renderer implementation. If you don’t notice it, the rendering results are actually inaccurate. This project uses a standard model and draws a point well, so it is not considered a misunderstanding of the coordinates.
Another one is readability. In order to deliberately reduce the amount of code, some projects have cut a lot of details to deal with it. Many operations are a bunch of matrices, without even a source or description. This is very puzzling for beginners. You don’t even know the name of a formula or concept, and you can’t search it.
The main file RenderHelp.h of this project has more than one thousand lines in total, one-third of which are Chinese annotations. I have expanded all the complex operations and do not just sacrifice readability to save code size. Some calculations can actually be extracted to the outer layer, which makes the performance faster, but for readability, I still wrote it in a position related to it, so that reading comprehension is easier.
The basic principles, I have explained in the following article:
When reading, the code is basically a few tool libraries in front of it, which can be read from the last 200 lines. I wrote the source of each formula. After basically half an hour deriving it, you can not only understand what the principle of the renderer is, but also have an additional tool that is convenient for debugging shader and verifying ideas at any time.
If you don’t understand the code, you can ask a question in the issue. This will also be helpful to later people after answering it. PR enhancement functions are welcome to supplement various advanced rendering effects.