📜 ⬆️ ⬇️

A short course in computer graphics: we write a simplified OpenGL do it yourself, article 4c of 6

Content of the main course




Code enhancement






The official translation (with a bit of polishing) is available here.



')

New rasterizer and perspective distortion correction


The topic of today's conversation is the correction of interpolation distortions, look at the difference in texturing on the floor:



I deliberately removed from the render all that relates to lighting, normals and other things, leaving only the texture. Thank you MrShoor , I was lazy and did not make this correction, but in the end I was confused due to his kick. With the old version of the rasterizer it was a chore, with the new one it is quite simple.

Therefore, we will start with how the new rasterizer works, and for this we need to be able to work with barycentric coordinates.

Finding the barycentric coordinates of a point in a two-dimensional triangle


Given a 2D triangle ABC, point P, all in Cartesian coordinates. Our task is to find the barycentric coordinates of the point P with respect to the triangle ABC. This is a triple of numbers (1-uv, u, v), with which we can find the point P:



This means that if we place weights (1-uv, u, v) at the corresponding vertices of the triangle, then the center of mass of the system will be at point P. Exactly the same can be rewritten, saying that point P will have coordinates (u, v ) in the frame (A, AB , AC ):



So, given the vectors AB, AC, AP, we need to find two real u, v, which answer the following equation:



This is a vector equation, which is equivalent to a system of two ordinary equations.



The system of two linear equations with two unknowns, the whole matter is very easily solved. I am lazy, honestly do not want to display a decision, let's decide as follows. Let's rewrite our system in matrix form:



This means that we are looking for a vector (u, v, 1) that is simultaneously orthogonal to two given vectors (ABx, ACx, PAx) and (ABy, ACy, PAy). Already understand what I'm getting at? That's right, we just multiply vectorially (ABx, ACx, PAx) x (ABy, ACy, PAy) and divide by the resulting third component.

This is a small hint: in 2D, the intersection of two straight lines (and this is exactly what we have just found) is considered to be a single vector product. By the way, finding the equation of a line passing through two given points is considered exactly the same!

New rasterizer


So, let's program a new version of the rasterizer, in which we simply find the describing rectangle, and go through all its pixels. For each pixel count barycentric coordinates. If there is at least one negative coordinate - a pixel outside the triangle, we fold it. To make it easier, I will provide a freestanding program that simply draws a two-dimensional triangle:

Hidden text
#include <vector> #include <iostream> #include "geometry.h" #include "tgaimage.h" const int width = 200; const int height = 200; Vec3f barycentric(Vec2i *pts, Vec2i P) { Vec3f u = cross(Vec3f(pts[2][0]-pts[0][0], pts[1][0]-pts[0][0], pts[0][0]-P[0]), Vec3f(pts[2][1]-pts[0][1], pts[1][1]-pts[0][1], pts[0][1]-P[1])); if (std::abs(u[2])<1) return Vec3f(-1,1,1); // triangle is degenerate, in this case return smth with negative coordinates return Vec3f(1.f-(u.x+uy)/uz, uy/uz, ux/uz); } void triangle(Vec2i *pts, TGAImage &image, TGAColor color) { Vec2i bboxmin(image.get_width()-1, image.get_height()-1); Vec2i bboxmax(0, 0); Vec2i clamp(image.get_width()-1, image.get_height()-1); for (int i=0; i<3; i++) { for (int j=0; j<2; j++) { bboxmin[j] = std::max(0, std::min(bboxmin[j], pts[i][j])); bboxmax[j] = std::min(clamp[j], std::max(bboxmax[j], pts[i][j])); } } Vec2i P; for (Px=bboxmin.x; Px<=bboxmax.x; P.x++) { for (Py=bboxmin.y; Py<=bboxmax.y; P.y++) { Vec3f bc_screen = barycentric(pts, P); if (bc_screen.x<0 || bc_screen.y<0 || bc_screen.z<0) continue; image.set(Px, Py, color); } } } int main(int argc, char** argv) { TGAImage frame(200, 200, TGAImage::RGB); Vec2i pts[3] = {Vec2i(10,10), Vec2i(100, 30), Vec2i(190, 160)}; triangle(pts, frame, TGAColor(255, 0, 0)); frame.flip_vertically(); // to place the origin in the bottom left corner of the image frame.write_tga_file("framebuffer.tga"); return 0; } 



The barycentric function counts the coordinates of the point P in this triangle; we just discussed this in the previous paragraph. Let's see how the triangle function works. First of all, it considers the describing rectangle. It is given by its two corners - lower left and upper right. We go through all points of the triangle and find the smallest and largest coordinates. In addition to this, I also found the intersection of the describing rectangle with the screen rectangle, so as not to waste time, if our drawing triangle goes beyond the screen.

Congratulations, we learned how to draw a triangle.



Perspective display correction


How do we use this rasterizer in the render? It would seem that replacing the line image.set (Px, Py, color) with a call to a fragment shader, and that's the end of it. Unfortunately, this is not entirely true.

Here is the code that does just that. The result of his work on the title picture on the left. But his correction , which will give us the correct render. The change is strictly one: I transferred to the shader not the barycentric coordinates bc_screen, but the barycentric coordinates bc_clip. Phew Let's figure it out.

The problem is that in the long run we at some point divided by the last homogeneous coordinate, breaking the linearity of our pipeline. Therefore, we do not have the right to use the barycentric coordinates of our pixel to interpolate attributes in the original space (be it texture coordinates or just depth).

Let's set the task as follows. We know that a certain point P belonging to a triangle ABC, after a perspective division, turns into a point P 'according to the following law:



We know the barycentric coordinates of the point P 'relative to the triangle A'B'C' (these are the transformed vertices of the triangle ABC):



So, knowing the coordinates of the triangle A'B'C 'and the barycentric coordinates of the point P' relative to it, we need to find the barycentric coordinates of the point P relative to the triangle ABC:



So, we write down the coordinates of the point P ':



Multiply everything by rP.z + 1:



We got the expression P = [ABC] * [incomprehensible vector]. But this is the definition of barycentric coordinates! Remained a little. What do we know and what is unknown to us in the definition of this vector? Alpha-beta-gamma-all-stroke we know. rA.z + 1, rB.z + 1, rC.z + 1 are known to us, these are the coordinates of the triangle transferred to the rasterizer. Only one thing left = rP.z + 1. That is, the z coordinate of the point P. And with its help we determine the point P. Is this not a closed circle? Fortunately, no.

Let's use the fact that in (normalized) barycentric coordinates, the sum of the coordinates gives one, that is, alpha + beta + gamma = 1:



Now we know everything, we can convert barycentric coordinates in screen space into barycentric coordinates in global space. Coordinate transformation is non-linear, but this is exactly what normal linear interpolation can do. And that is why, transferring the adjusted coordinates to the fragment shader, we get a strikingly different pattern on the texture in a checkered pattern.

So, to find, for example, texture coordinates, we need (scalar) to multiply (uv0 uv1 uv2) by (alpha beta gamma). Or (z0 z1 z2) on (alpha beta gamma). Or (vn0 vn1 vn2) to (alpha beta gamma). In general, everything that we need to interpolate!

On good data, this correction is not very necessary.


This picture is one title picture minus the other.



The head is completely gone, which indicates a weak error introduced by incorrect interpolation.

As a (optional) course bonus, we have to figure out how to count the tangent space, to use tangent-space textures of normals, make glowing surfaces, look at what ambient occlusion is.

Source: https://habr.com/ru/post/249467/


All Articles