TinyRenderer Note 1: Z-buffer and Texture Interpolation

Original link: https://www.kirito41dd.cn/tinyrenderer-note-1/

logo.png

At the end of the last article, the model under lighting was rendered:

But this model looks a little weird, especially the mouth part. Because when rendering, the triangles are drawn one by one according to the vertex information read from the model, but the occlusion relationship of the triangles is not considered. If we draw the triangle for the face first, and then the triangle for the back of the head, the resulting graphic will be weird as above, because we’re not dealing with depth information.

Y-buffer

The rendering model is to map 3D to 2D. Let’s first consider how to map 2D to 1D. The goal is to map several line segments in 2d space to the x-axis.

If we look from the top down in a one-dimensional perspective, we should see a colored line segment. Write a function to draw this line segment:

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
 pub fn resterize < I : GenericImage > (      mut a : glm :: IVec2 ,      mut b : glm :: IVec2 ,      image : & mut I ,      color : I :: Pixel ,  ) {      if ( a . x - b . x ). abs () < ( a . y - b . y ). abs () {          swap ( & mut a . x , & mut a . y );          swap ( & mut b . x , & mut b . y );      }      for x in a . x .. = b . x {          for i in 0 .. image . height () {              image . put_pixel ( x as u32 , i , color );          }      }  }    {      draw :: resterize ( glm :: ivec2 ( 330 , 463 ), glm :: ivec2 ( 594 , 200 ), & mut image , BLUE );      draw :: resterize ( glm :: ivec2 ( 120 , 434 ), glm :: ivec2 ( 444 , 400 ), & mut image , GREEN );      draw :: resterize ( glm :: ivec2 ( 20 , 34 ), glm :: ivec2 ( 744 , 400 ), & mut image , RED );  }  

The length of the drawn line segment is as follows:

Obviously not right, only red is seen, because we are the last red line segment drawn, and the red line segment is the longest, so it directly covers the other two.

In order to draw the line segment correctly, we need to know the depth information of each pixel. In this example, a pixel with a larger y coordinate cannot be covered by a smaller pixel.

Introduce y-buffer:

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
 pub fn resterize < I : GenericImage > (      mut a : glm :: IVec2 ,      mut b : glm :: IVec2 ,      image : & mut I ,      ybuffer : & mut [ i32 ],      color : I :: Pixel ,  ) {      if ( a . x - b . x ). abs () < ( a . y - b . y ). abs () {          swap ( & mut a . x , & mut a . y );          swap ( & mut b . x , & mut b . y );      }      for x in a . x .. = b . x {          let t = ( x - a . x ) as f32 / ( b . x - a . x ) as f32 ;          let y = a . y as f32 * ( 1. - t ) + b . y as f32 * t ;            if ybuffer [ x as usize ] < y as i32 {              ybuffer [ x as usize ] = y as i32 ;              for i in 0 .. image . height () {                  image . put_pixel ( x as u32 , i , color );              }          }      }  }    {      let mut ybuffer = vec ! [ 0 ; 800 ];      draw :: resterize ( glm :: ivec2 ( 330 , 463 ), glm :: ivec2 ( 594 , 200 ), & mut image , ybuffer , BLUE );      draw :: resterize ( glm :: ivec2 ( 120 , 434 ), glm :: ivec2 ( 444 , 400 ), & mut image , ybuffer , GREEN );      draw :: resterize ( glm :: ivec2 ( 20 , 34 ), glm :: ivec2 ( 744 , 400 ), & mut image , ybuffer , RED );  }  

The correct line segment colors are now obtained:

See the detailed code here f23d87e4e34700767da9c6754d2b0aa6b37fe58f

Z-buffer

Going back to 3d, in order to map to a 2d screen, the zbuffer needs two dimensions:

 1
 let zbuffer = vec ! [ 0 ; width * height ];  

Although the required array is two-dimensional, it can be represented by a one-dimensional array with a simple transformation:

 1 2 3 4
 let idx = x + y * width ;    let x = idx % width ;  let y = idx / wdith ;  

The only difference from ybuffer is how the z value is calculated. The previous y value is calculated like this:

 1
 let y = p0 . y * ( 1 - t ) + p1 . y * t  

The above formula can be regarded as the dot product of two vectors: $(y_1, y_2) * (1-t, t)$, $(1-t, t)$ is actually the barycentric coordinate of the point x about the line segment p0p1.

So for the z value, you can use the z coordinates and barycentric coordinates of the three vertices of the triangle to calculate: $(z_1,z_2,z_3)*(1-uv, u, v)$

Introduce zbuffer in model rendering code:

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
 pub fn triangle < I : GenericImage > (      //...    zbuffer : & mut [ f32 ],  ) {      //...    for px in bboxmin . x as i32 .. = bboxmax . x as i32 {          for py in bboxmin . y as i32 .. = bboxmax . y as i32 {              let bc_screen = barycentric ( t0 , t1 , t2 , glm :: vec3 ( px as f32 , py as f32 , 0. ));                if bc_screen . x < 0. || bc_screen . y < 0. || bc_screen . z < 0. {                  continue ;              }              // 计算z值            let pz = glm :: dot ( glm :: vec3 ( t0 . z , t1 . z , t2 . z ), bc_screen );              let idx = px + py * image . width () as i32 ;              if zbuffer [ idx as usize ] <= pz {                  zbuffer [ idx as usize ] = pz ;                  image . put_pixel ( px as u32 , py as u32 , color );              }          }      }  }    {      let mut zbuffer = vec ! [ f32 :: MIN ; ( image . width () * image . height ()) as usize ]; // 注意一定初始化为最小值    //...    for arr in model . indices . chunks ( 3 ) {          //...        draw :: triangle ( sa , sb , sc , & mut image ,              Rgba ([( 255. * intensity ) as u8 , ( 255. * intensity ) as u8 ,( 255. * intensity ) as u8 , 255 ]),              & mut zbuffer ,          );      }  }  

This will render correctly, and the effect is as follows:

See the detailed code here 7bd6b1b50f602336a3fbc8d345399c5f9872a19d

texture

The previously rendered model has no skin, and uses white intensity to represent lighting. Next, texture interpolation is performed to calculate what color each pixel should be through texture coordinates.

In the model file, some lines are in this format: vt uv 0.0 which gives a texture (vertex) coordinate.

These lines fx/x/xx/x/xx/x/x describe a face, and the x in the middle of each group of data (press/split) is the texture coordinate number of the vertex of the triangle.

Interpolate the triangle based on the texture coordinates, multiply by the width-height of the texture image, and you’ll get the color to render.

The author calculates the texture coordinates by scanning lines. I tried to use the barycentric coordinates to calculate them. The zbuffer gave me inspiration. I didn’t expect that it was possible to experience the magic of barycentric coordinates.

First of all, you can download the texture image provided by the author from here . Read the picture first:

 1 2
 let mut diffus = image :: open ( "obj/african_head/african_head_diffuse.tga" ). unwrap (). to_rgba8 ();  let diffuse = flip_vertical_in_place ( & mut diffus );  

Since we flipped the y-axis earlier, we also need to flip the texture image.

Next write a new function to draw the triangle. In the zbuffer section above, we used the barycentric coordinates to calculate the z value. Now we have the texture coordinates of the three vertices of the triangle. We can also use the barycentric coordinates to calculate the texture coordinates of the current position:

$$
\begin{aligned}
x &= (x_1, x_2, x_3) * barycentric \\
y &= (y_1, y_2, y_3) * barycentric
\end{aligned}
$$

After calculating the coordinates according to the above formula, zoom in to the texture image ratio:

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
 pub fn triangle_with_texture < I : GenericImage < Pixel = Rgba < u8 >>> (      a : glm :: Vec3 , // 模型顶点    b : glm :: Vec3 ,      c : glm :: Vec3 ,      ta : glm :: Vec3 , // 纹理坐标顶点    tb : glm :: Vec3 ,      tc : glm :: Vec3 ,      image : & mut I ,      intensity : f32 ,      zbuffer : & mut [ f32 ],      diffuse : & I , // 纹理图片) {      //...    for px in bboxmin . x as i32 .. = bboxmax . x as i32 {          for py in bboxmin . y as i32 .. = bboxmax . y as i32 {              let bc_screen = barycentric ( a , b , c , glm :: vec3 ( px as f32 , py as f32 , 0. ));                if bc_screen . x < 0. || bc_screen . y < 0. || bc_screen . z < 0. {                  continue ;              }              // 计算z值            let pz = glm :: dot ( glm :: vec3 ( a . z , b . z , c . z ), bc_screen );                // 计算纹理坐标            let tx = glm :: dot ( glm :: vec3 ( ta . x , tb . x , tc . x ), bc_screen ) * diffuse . width () as f32 ;              let ty = glm :: dot ( glm :: vec3 ( ta . y , tb . y , tc . y ), bc_screen ) * diffuse . height () as f32 ;              let idx = px + py * image . width () as i32 ;              let pi : Rgba < u8 > = diffuse . get_pixel ( tx as u32 , ty as u32 ); // 获取像素            if zbuffer [ idx as usize ] <= pz {                  zbuffer [ idx as usize ] = pz ;                  image . put_pixel (                      px as u32 ,                      py as u32 ,                      Rgba ([                          ( pi . 0 [ 0 ] as f32 * intensity ) as u8 ,                          ( pi . 0 [ 1 ] as f32 * intensity ) as u8 ,                          ( pi . 0 [ 2 ] as f32 * intensity ) as u8 ,                          255 ,                      ]),                  );              }          }      }  }  

The effect is as follows:

See the detailed code here dd09711edb4fa1dcb129ccce64959a0fec749f42

This article is reprinted from: https://www.kirito41dd.cn/tinyrenderer-note-1/
This site is for inclusion only, and the copyright belongs to the original author.

Leave a Comment