2

TinyRenderer笔记1:Z-buffer和纹理插值

 1 year ago
source link: https://www.kirito41dd.cn/tinyrenderer-note-1/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

上一篇结尾渲染出了光照下的模型:

shadow-1.png

但是这个模型看起来有些奇怪,尤其是嘴巴的部分。因为渲染时候仅仅是按照从模型中读取的顶点信息,将三角形一个个的画了出来,但是并没有考虑三角形的遮挡关系。如果我们先画出了面部的三角形,然后又画了脑后勺的三角形,那最终展示出来的图形就会像上面一样很奇怪,原因是我们没有处理深度信息。

Y-buffer

渲染模型是将3维映射到2维,先来考虑将2维映射到1维该怎么做。目标是把几条2d空间的线段,映射到x轴上。

1-1.png

如果我们以一维的视角从上往下看,看到的应该是一条彩色的线段,写一个函数来绘制这个线段:

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);
}

绘制出来的线段长这样:

1-2.png

明显不太对,只看到了红色,因为我们是最后画的红色线段,红色线段又最长,所以直接覆盖了另外两条。

为了正确画出线段,我们需要知道每个像素的深度信息。在这个例子中,y坐标更大的像素不能被更小的像素覆盖。

引入y-buffer:

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);
}

现在得到了正确的线段颜色:

1-3.png

详细代码见这里f23d87e4e34700767da9c6754d2b0aa6b37fe58f

Z-buffer

回到3d,为了能映射到2d屏幕上,zbuffer需要两个维度:

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

虽然需要的数组是二维的,但可以用一维数组来表示,只需要简单的变换:

let idx = x + y*width;

let x = idx % width;
let y = idx / wdith;

与ybuffer唯一的不同是如何计算z值,前面y值是这样计算的:

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

上面的式子可以看作两个向量的点积: (y1,y2)∗(1−t,t)(y_1, y_2) * (1-t, t)(y1​,y2​)∗(1−t,t),(1−t,t)(1-t, t)(1−t,t) 其实是点x关于线段p0p1的重心坐标。

所以对于z值,可以用三角形三个顶点z坐标和重心坐标计算: (z1,z2,z3)∗(1−u−v,u,v)(z_1,z_2,z_3)*(1-u-v, u, v)(z1​,z2​,z3​)∗(1−u−v,u,v)

在模型渲染的代码中引入zbuffer:

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,
        );
    }
}

这样就能正确渲染了,效果如下:

1-4.png

详细代码见这里7bd6b1b50f602336a3fbc8d345399c5f9872a19d

前面渲染的模型没有皮肤,都是用白色的强度来表示光照,接下来要进行纹理插值,通过纹理坐标算出每个像素应该是什么颜色。

在模型文件里,有些行是这样的格式: vt u v 0.0 它给出了一个纹理(顶点)坐标。

这些行f x/x/x x/x/x x/x/x 描述了一个面,每组数据(按/分割)中间的x,就是三角形该顶点的纹理坐标编号。

根据纹理坐标对三角形进行插值,乘以纹理图像的宽度-高度,就会得到要渲染的颜色。

作者是通过扫描线的方式计算纹理坐标的,我尝试了一下用重心坐标来计算,zbuffer那里给了我启发,没想到真的可以,体会下重心坐标的神奇。

首先从这里可以下载作者提供的纹理图片。先读取图片:

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

因为我们之前翻转了y轴,所以也需要将纹理图片翻转一下。

接下来写一个新的函数来画三角形。上面zbuffer部分,我们用重心坐标算出了z值,现在我们有了三角形三个顶点的纹理坐标,同样可以用重心坐标 算出当前位置的纹理坐标:

x=(x1,x2,x3)∗barycentricy=(y1,y2,y3)∗barycentric \begin{aligned} x &= (x_1, x_2, x_3) * barycentric \\ y &= (y_1, y_2, y_3) * barycentric \end{aligned} xy​=(x1​,x2​,x3​)∗barycentric=(y1​,y2​,y3​)∗barycentric​

根据上面式子算出坐标后,放大到纹理图片比例即可:

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,
                    ]),
                );
            }
        }
    }
}

效果如下:

1-5.png

详细代码见这里dd09711edb4fa1dcb129ccce64959a0fec749f42


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK