Content of the main course
Code enhancement
The official translation (with a bit of polishing) is available here.
')
Shadow mapping
Well, our short course is coming to an end, the task for today is to learn how to draw shadows (attention, rendering of half-shadows is a separate topic):

As always, the code is available
on githubUntil now, we have been able to shade convex objects due to normals on the surface, but for non-convex objects our renders gave an incorrect result, why is the demon's right (for us) left shoulder lit? Why is there no horn shadow on my left cheek? Disorder.

The idea is very simple: we will render in two passes. If we render the picture for the first time, putting the camera in the place of the light source, then we will know exactly which places are lit. And then in the second pass we will use the result of the work of the first pass. There are almost no difficulties. Let's write this shader:
Hidden textstruct DepthShader : public IShader { mat<3,3,float> varying_tri; DepthShader() : varying_tri() {} virtual Vec4f vertex(int iface, int nthvert) { Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert)); // read the vertex from .obj file gl_Vertex = Viewport*Projection*ModelView*gl_Vertex; // transform it to screen coordinates varying_tri.set_col(nthvert, proj<3>(gl_Vertex/gl_Vertex[3])); return gl_Vertex; } virtual bool fragment(Vec3f bar, TGAColor &color) { Vec3f p = varying_tri*bar; color = TGAColor(255, 255, 255)*(pz/depth); return false; } };
This shader simply draws the contents of the z-buffer in the frame buffer. I call this shader from main ():
Hidden text { // rendering the shadow buffer TGAImage depth(width, height, TGAImage::RGB); lookat(light_dir, center, up); viewport(width/8, height/8, width*3/4, height*3/4); projection(0); DepthShader depthshader; Vec4f screen_coords[3]; for (int i=0; i<model->nfaces(); i++) { for (int j=0; j<3; j++) { screen_coords[j] = depthshader.vertex(i, j); } triangle(screen_coords, depthshader, depth, shadowbuffer); } depth.flip_vertically(); // to place the origin in the bottom left corner of the image depth.write_tga_file("depth.tga"); } Matrix M = Viewport*Projection*ModelView;
I put the camera in the place of the light source (lookat (light_dir, center, up);) and render it. The z-buffer of this rendering pass is saved by the shadowbuffer pointer. Note that with the very last line I save the transition matrix from the coordinates of the object to the screen coordinates.
Here is the result of this shader, the first pass of rendering is finished.
I do the second pass with another shader:
Hidden text struct Shader : public IShader { mat<4,4,float> uniform_M; // Projection*ModelView mat<4,4,float> uniform_MIT; // (Projection*ModelView).invert_transpose() mat<4,4,float> uniform_Mshadow; // transform framebuffer screen coordinates to shadowbuffer screen coordinates mat<2,3,float> varying_uv; // triangle uv coordinates, written by the vertex shader, read by the fragment shader mat<3,3,float> varying_tri; // triangle coordinates before Viewport transform, written by VS, read by FS Shader(Matrix M, Matrix MIT, Matrix MS) : uniform_M(M), uniform_MIT(MIT), uniform_Mshadow(MS), varying_uv(), varying_tri() {} virtual Vec4f vertex(int iface, int nthvert) { varying_uv.set_col(nthvert, model->uv(iface, nthvert)); Vec4f gl_Vertex = Viewport*Projection*ModelView*embed<4>(model->vert(iface, nthvert)); varying_tri.set_col(nthvert, proj<3>(gl_Vertex/gl_Vertex[3])); return gl_Vertex; } virtual bool fragment(Vec3f bar, TGAColor &color) { Vec4f sb_p = uniform_Mshadow*embed<4>(varying_tri*bar); // corresponding point in the shadow buffer sb_p = sb_p/sb_p[3]; int idx = int(sb_p[0]) + int(sb_p[1])*width; // index in the shadowbuffer array float shadow = .3+.7*(shadowbuffer[idx]<sb_p[2]); Vec2f uv = varying_uv*bar; // interpolate uv for the current pixel Vec3f n = proj<3>(uniform_MIT*embed<4>(model->normal(uv))).normalize(); // normal Vec3f l = proj<3>(uniform_M *embed<4>(light_dir )).normalize(); // light vector Vec3f r = (n*(n*l*2.f) - l).normalize(); // reflected light float spec = pow(std::max(rz, 0.0f), model->specular(uv)); float diff = std::max(0.f, n*l); TGAColor c = model->diffuse(uv); for (int i=0; i<3; i++) color[i] = std::min<float>(20 + c[i]*shadow*(1.2*diff + .6*spec), 255); return false; } };
This is practically one-to-one shader from the end of the previous article, with one exception:
I declared a constant matrix that does not change during operation of either the vertex or fragment shaders mat <4.4, float> uniform_Mshadow.
This matrix will allow me to turn the screen coordinates of the current shader into the screen coordinates of the already rendered shadow buffer!
About how we consider it in the next paragraph. Let's see how we use it, let's pay attention to these four lines of the shader:
Vec4f sb_p = uniform_Mshadow*embed<4>(varying_tri*bar); // corresponding point in the shadow buffer sb_p = sb_p/sb_p[3]; int idx = int(sb_p[0]) + int(sb_p[1])*width; // index in the shadowbuffer array float shadow = .3+.7*(shadowbuffer[idx]<sb_p[2]);
varying_tri * bar gives me the screen coordinates of the current snippet that we draw. We submerge them in homogeneous coordinates, transform with our magic matrix uniform_Mshadow and ta-dam, we know the xyz coordinates in the shadow shader space that we used for the first pass. Now, in order to understand whether this point is lit or not, it is enough for us to compare its z-coordinate and the value of the z-buffer from the first pass!
What does the second shader call in main () look like? Everything is pretty standard:
Matrix M = Viewport*Projection*ModelView; { // rendering the frame buffer TGAImage frame(width, height, TGAImage::RGB); lookat(eye, center, up); viewport(width/8, height/8, width*3/4, height*3/4); projection(-1.f/(eye-center).norm()); Shader shader(ModelView, (Projection*ModelView).invert_transpose(), M*(Viewport*Projection*ModelView).invert()); Vec4f screen_coords[3]; for (int i=0; i<model->nfaces(); i++) { for (int j=0; j<3; j++) { screen_coords[j] = shader.vertex(i, j); } triangle(screen_coords, shader, frame, zbuffer); } frame.flip_vertically(); // to place the origin in the bottom left corner of the image frame.write_tga_file("framebuffer.tga"); }
I remind you that the matrix M is a matrix for converting object coordinates into screen coordinates of the shadow buffer. We put the camera in the place where it should be, set up the viewport and the perspective projection parameters, and declare the second-pass rendering shader.
We know that Viewport * Projection * ModelView is a matrix for converting object coordinates into screen coordinates of the second shader. But we need to know the transformation matrix of the screen of the second shader to the screen of the first shader. It's simple: (Viewport * Projection * ModelView) .invert () converts the screen of the second shader to object coordinates, and then multiply simply by M, getting the final transformation matrix as M * (Viewport * Projection * ModelView) .invert ().
Everything would be fine if it were not a trifle: nineteen in half, it seems, is not divided. Here is the result of our two-pass render:
What is it? This artifact is known as the
fight for z . If a pixel should be lit, then its z-coordinate should be in the z-buffer of the shadow shader. Or should it be the neighboring pixel z-value? In general, the resolution of our z-buffer is not enough to give a picture without artifacts. We will deal with this problem by brute force:
float shadow = .3+.7*(shadowbuffer[idx]<sb_p[2]+43.34);
I shift one z-buffer relative to another by some constant, which is enough for this artifact to disappear. Yes, it creates new artifacts (which ones?), But significantly less noticeable to the eye. Everything, the result of our program is visible in the title picture.
Congratulations, our short course has come to an end. We wrote from scratch a very good, I think, analogue of OpenGL.
Did you sign up to volunteer?
As a bonus to the short course, next time I will show how to count the tangent basis to our surface (to use the textures defined in tangent space) and at the same time we write a simple shader that can work with luminous objects (see the crystal in the head of diablo):

Samuel Sharit very kindly provided us with this model, of course, it can be used without his special permission only within the framework of this training course, as well as the head model of a Negro made by Vidar Rapp.