[CG Notes] Drawing Primitives: Triangle

The learning material is a Github project Tiny renderer or how OpenGL works: software rendering in 500 lines of code

This article corresponds to part of the second lesson of the original tutorial

The original tutorial focuses on ideas, and the main content is based on derivation, so here is still to record ideas and make comments for the code

Zhihu also gave a Chinese translation: [Building a raster renderer from scratch] 0. Introduction

Wireframe drawing and area filling of triangles

In the code given in the tutorial, geometry.ha line should be added at the reference #include <ostream>, otherwise an error will be reported

The most basic method is of course to borrow the line drawing function that has been implemented line, and use the three vertices in pairs in turn. The usage method is similar to the following:

// defination
void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
    
     
    line(t0, t1, image, color); 
    line(t1, t2, image, color); 
    line(t2, t0, image, color); 
}

// ...

// using in code
Vec2i t0[3] = {
    
    Vec2i(10, 70),   Vec2i(50, 160),  Vec2i(70, 80)}; 
Vec2i t1[3] = {
    
    Vec2i(180, 50),  Vec2i(150, 1),   Vec2i(70, 180)}; 
Vec2i t2[3] = {
    
    Vec2i(180, 150), Vec2i(120, 160), Vec2i(130, 180)}; 
triangle(t0[0], t0[1], t0[2], image, red); 
triangle(t1[0], t1[1], t1[2], image, white); 
triangle(t2[0], t2[1], t2[2], image, green);

A good method of drawing triangles should have the following characteristics:

  • Should be simple and efficient
  • Symmetrically, the image should not depend on the order of vertices passed to the draw function

The method given by the author is:

  1. Sort the vertices that make up the triangle by Y coordinate
  2. Rasterize both left and right sides of a triangle
  3. Fill the area between the left and right with a horizontal line

This is similar to the polygon area fillscan line method

insert image description here

The author means that a triangularsideDivided into left and right parts, one part is the connection line from the bottom vertex of the y-axis to the top vertex (the red part has the largest span in the y direction), and the rest constitute another part, which has two line segments, obviously It can't be drawn in one go

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
    
     
    // sort the vertices, t0, t1, t2 lower−to−upper (bubblesort yay!) 
    if (t0.y>t1.y) std::swap(t0, t1); 
    if (t0.y>t2.y) std::swap(t0, t2); 
    if (t1.y>t2.y) std::swap(t1, t2); 
    int total_height = t2.y-t0.y; 
    for (int y=t0.y; y<=t1.y; y++) {
    
     
        int segment_height = t1.y-t0.y+1; 
        float alpha = (float)(y-t0.y)/total_height; 
        float beta  = (float)(y-t0.y)/segment_height; // be careful with divisions by zero 
        Vec2i A = t0 + (t2-t0)*alpha; 
        Vec2i B = t0 + (t1-t0)*beta; 
        if (A.x>B.x) std::swap(A, B); 
        for (int j=A.x; j<=B.x; j++) {
    
     
            image.set(j, y, color); // attention, due to int casts t0.y+i != A.y 
        } 
    } 
    for (int y=t1.y; y<=t2.y; y++) {
    
     
        int segment_height =  t2.y-t1.y+1; 
        float alpha = (float)(y-t0.y)/total_height; 
        float beta  = (float)(y-t1.y)/segment_height; // be careful with divisions by zero 
        Vec2i A = t0 + (t2-t0)*alpha; 
        Vec2i B = t1 + (t2-t1)*beta; 
        if (A.x>B.x) std::swap(A, B); 
        for (int j=A.x; j<=B.x; j++) {
    
     
            image.set(j, y, color); // attention, due to int casts t0.y+i != A.y 
        } 
    } 
}

insert image description here
According to the code above, two for loops draw the upper and lower half of a triangle respectively. Since graphics are composed of pixels, here we need toby rowDraw (the third of the three points mentioned by the author above), as shown in the picture below (I made this picture in PS, and it has anti-aliasing, but the drawing here should not have anti-aliasing). Each time of the outer for loop, a line is drawn, and the memory for loop once is to draw one of the pixels of this line.

insert image description here

AJust take out a for loop to see, in which the outer loop first calculates the left and right endpoints of this line (that is, the sum in the code B), and the calculation of the left and right endpoints depends aplhaon betathe two variables of sum, and these two variables, the above Take the half part as an example, which is the difference between the y coordinate of the current drawn horizontal line (red) and the bottom (the y coordinate of t0), which accounts for the proportion of the length of the entire triangle in the y direction. This can be used to infer the vector in the figure below (that is, the origin points to AA point vector), that is A = t0 + (t2-t0)*alpha, the B vector is the same. Knowing the vector, you naturally know where point A is and where point B is.

According to the proportion (equal triangle), the line connecting points A and B itself is also horizontal to the coordinate axis, so the X coordinates of the two points can be used as both ends, and pixels are filled between them

float alpha = (float)(y-t0.y)/total_height; 
float beta  = (float)(y-t0.y)/segment_height; // be careful with divisions by zero 

insert image description here

And this code has two problems, one is that there are too many repeated codes, and the other is that it cannot be drawn in some cases (it is said in the comment that segment_heightit may be 0, and the divisor cannot be used at this time), so it is optimized:

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
    
     
    if (t0.y==t1.y && t0.y==t2.y) return; // I dont care about degenerate triangles 
    // sort the vertices, t0, t1, t2 lower−to−upper (bubblesort yay!) 
    if (t0.y>t1.y) std::swap(t0, t1); 
    if (t0.y>t2.y) std::swap(t0, t2); 
    if (t1.y>t2.y) std::swap(t1, t2); 
    int total_height = t2.y-t0.y; 
    for (int i=0; i<total_height; i++) {
    
     
        bool second_half = i>t1.y-t0.y || t1.y==t0.y; 
        int segment_height = second_half ? t2.y-t1.y : t1.y-t0.y; 
        float alpha = (float)i/total_height; 
        float beta  = (float)(i-(second_half ? t1.y-t0.y : 0))/segment_height; // be careful: with above conditions no division by zero here 
        Vec2i A = t0 + (t2-t0)*alpha; 
        Vec2i B = second_half ? t1 + (t2-t1)*beta : t0 + (t1-t0)*beta; 
        if (A.x>B.x) std::swap(A, B); 
        for (int j=A.x; j<=B.x; j++) {
    
     
            image.set(j, t0.y+i, color); // attention, due to int casts t0.y+i != A.y 
        } 
    } 
}

Observing the above code, you can find that there are

If segment_heightit is 0, one side of the triangle itself is parallel to the direction of the scan line, that is, the direction of the X axis. At this time, it is equivalent to drawing the triangle in the above example.the second half(so-called second_half).

Therefore, the variable is set in the code second_half, and there are two situations for drawing the lower part. One is that the lower part is indeed drawn (ie i>t1.y-t0.y), and the second is that the bottom edge mentioned above is drawn parallel to the direction of the scan line (ie t1.y==t0.y), also as Draw the lower half.

Use second_halfas a flag variable to selectively change segment_heightand betato realize all operations in one for.

This is how you draw a 2D triangle

Digression: barycentric coordinate system

Here's another way to draw a triangle

The author then mentioned something called barycentric coordinates . Let me just take a picture from Wikipedia and show it:

insert image description here

Supplementary link: Computer Graphics III (Supplement): Detailed explanation of barycentric coordinates and their functions

It is not difficult to find that the coordinate system has 3 dimensions, one dimension at the vertex must be 1, and the others are 0. The center of gravity is (1/3, 1/3, 1/3). So this is a new way of thinking: check whether a certain coordinate is within a triangle in the barycentric coordinate system .

Very simple, if the three components of this coordinate have negative values, it means that it is outside the triangle

Tips: If a point is inside a triangle, its three components in the barycentric coordinate system of the triangle are all non-negative numbers

So we need a barycentric()function that calculates the coordinates of a point P in a given triangle.

As for triangle()the function, it computes the bounding box. Defining a bounding box requires knowing the bottom left and top right corners. To find these positions, we iterate over all vertices of the triangle and find the min/max coordinates. We do a point-by-point check within the bounding box to see if it is inside the triangle.

#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 = 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]);
    /* `pts` and `P` has integer value as coordinates
       so `abs(u[2])` < 1 means `u[2]` is 0, that means
       triangle is degenerate, in this case return something with negative coordinates */
    if (std::abs(u.z)<1) return Vec3f(-1,1,1);
    return Vec3f(1.f-(u.x+u.y)/u.z, u.y/u.z, u.x/u.z); 
} 
 
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++) {
    
     
        bboxmin.x = std::max(0, std::min(bboxmin.x, pts[i].x));
	bboxmin.y = std::max(0, std::min(bboxmin.y, pts[i].y));

	bboxmax.x = std::min(clamp.x, std::max(bboxmax.x, pts[i].x));
	bboxmax.y = std::min(clamp.y, std::max(bboxmax.y, pts[i].y));
    } 
    Vec2i P; 
    for (P.x=bboxmin.x; P.x<=bboxmax.x; P.x++) {
    
     
        for (P.y=bboxmin.y; P.y<=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(P.x, P.y, 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; 
}

After reading the code, you have to understand Vec3f barycentric(Vec2i*, Vec2i)the principle of this function. The key point is the following line of code:

Vec3f u = 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]);

It can be seen that trianglethis function is called in the function. The idea of ​​calling is to Pcall each point in the bounding box in turn, Vec3f bc_screen = barycentric(pts, P); check the coordinate points in the returned barycentric coordinate system bc_screen, and do nothing if a certain component is less than 0. If they are not less than 0, it is considered to be within the triangle, and the specified operation (drawing here) is performed.

What needs to be known is ptswhat is called from mainthe function. You can know that it is the coordinates of the three points of the triangle. The type is Vec2i[3](I know it may not be strict in C++), that is, each point is a Vec2itype variable.

It can be guessed that this code converts the Cartesian coordinate system into a barycentric coordinate system, butRealize ( x , y ) (x,y)(x,y ) to( a , b , c ) (\alpha, \beta, \gamma)( a ,b ,γ ) conversionWhat is the formula for ? It can be found from the above linked article and the following link: barycentric coordinate system

I directly took a screenshot of the Chinese version of Zhihu linked above:

insert image description here
The last formula here is to be solved, actually ( u , v , 1 ) (u,v,1)(u,v,1) ( A B → x , A C → x , P A → x ) {( \overrightarrow{AB}_x , \overrightarrow{AC}_x , \overrightarrow{PA}_x) } (AB x,AC x,PA x) ( A B → y , A C → y , P A → y ) {( \overrightarrow{AB}_y , \overrightarrow{AC}_y , \overrightarrow{PA}_y) } (AB y,AC y,PA y) These two vectors are orthogonal (the dot product is zero). The meaning here is to take out( AB → , AC → , PA → ) {( \overrightarrow{AB} , \overrightarrow{AC} , \overrightarrow{PA}) }(AB ,AC ,PA ) ofxxxyyy component to operate, becauseAB → \overrightarrow{AB}AB is still a vector with xxxyyy two dimensions, this is actually a2 × 3 2 \times32×3 matrix.

Orthogonal definition: for vector α \alphaαβ \betaβ,有( α , β ) = α T β = 0 (\alpha,\beta)=\alpha^T\beta=0( a ,b )=aTβ=0

For the dot product α ⸳ β \alpha ⸳ \betaThe geometric meaning of α β isα \alphaαβ \betaprojection on β , a dot product of zero meansverticalOr orthogonal. If you forget the dot product, you can read: the concept and geometric meaning of vector dot product and cross product

Functional constant α ⸳ β = ∣ α ∣ ∣ β ∣ cos θ \alpha ⸳ \beta=|\alpha||\beta|cos\thetaαβ=α∣∣βcosθ

Then the vector ( u , v , 1 ) (u,v,1) perpendicular to both vectors(u,v,1 ) You can use the cross product to get the direction of the variable, and then modify the length. The nature of the cross product is that for two vectors in different directionsα \alphaαβ \betaβ,叉乘α × β \alpha \times \betaa×The result obtained by β is a vector, and at the same time it is perpendicular to α \alphaαβ \betaβ these two vectors.

In short, the principle is understood, but the question is why Vec3fthe constructor accepts so many parameters, I don't want to understand.

Vec3fIn addition, the operation in the code ^is actually a cross product, which can be defined in the given header file:

inline Vec3<t> operator ^(const Vec3<t> &v) const {
    
     return Vec3<t>(y*v.z-z*v.y, z*v.x-x*v.z, x*v.y-y*v.x); }

Guess you like

Origin blog.csdn.net/qq_39377889/article/details/128276939