上一篇结尾渲染出了光照下的模型:
但是这个模型看起来有些奇怪,尤其是嘴巴的部分。因为渲染时候仅仅是按照从模型中读取的顶点信息,将三角形一个个的画了出来,但是并没有考虑三角形的遮挡关系。如果我们先画出了面部的三角形,然后又画了脑后勺的三角形,那最终展示出来的图形就会像上面一样很奇怪,原因是我们没有处理深度信息。
渲染模型是将3维映射到2维,先来考虑将2维映射到1维该怎么做。目标是把几条2d空间的线段,映射到x轴上。
如果我们以一维的视角从上往下看,看到的应该是一条彩色的线段,写一个函数来绘制这个线段:
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);
}
|
绘制出来的线段长这样:
明显不太对,只看到了红色,因为我们是最后画的红色线段,红色线段又最长,所以直接覆盖了另外两条。
为了正确画出线段,我们需要知道每个像素的深度信息。在这个例子中,y坐标更大的像素不能被更小的像素覆盖。
引入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);
}
|
现在得到了正确的线段颜色:
详细代码见这里f23d87e4e34700767da9c6754d2b0aa6b37fe58f
回到3d,为了能映射到2d屏幕上,zbuffer需要两个维度:
1
|
let zbuffer = vec![0; width*height];
|
虽然需要的数组是二维的,但可以用一维数组来表示,只需要简单的变换:
1
2
3
4
|
let idx = x + y*width;
let x = idx % width;
let y = idx / wdith;
|
与ybuffer唯一的不同是如何计算z值,前面y值是这样计算的:
1
|
let y = p0.y*(1-t) + p1.y*t
|
上面的式子可以看作两个向量的点积: $(y_1, y_2) * (1-t, t)$,$(1-t, t)$ 其实是点x关于线段p0p1的重心坐标。
所以对于z值,可以用三角形三个顶点z坐标和重心坐标计算: $(z_1,z_2,z_3)*(1-u-v, u, v)$
在模型渲染的代码中引入zbuffer:
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,
);
}
}
|
这样就能正确渲染了,效果如下:
详细代码见这里7bd6b1b50f602336a3fbc8d345399c5f9872a19d
前面渲染的模型没有皮肤,都是用白色的强度来表示光照,接下来要进行纹理插值,通过纹理坐标算出每个像素应该是什么颜色。
在模型文件里,有些行是这样的格式: vt u v 0.0
它给出了一个纹理(顶点)坐标。
这些行f x/x/x x/x/x x/x/x
描述了一个面,每组数据(按/分割)中间的x,就是三角形该顶点的纹理坐标编号。
根据纹理坐标对三角形进行插值,乘以纹理图像的宽度-高度,就会得到要渲染的颜色。
作者是通过扫描线的方式计算纹理坐标的,我尝试了一下用重心坐标来计算,zbuffer那里给了我启发,没想到真的可以,体会下重心坐标的神奇。
首先从这里可以下载作者提供的纹理图片。先读取图片:
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);
|
因为我们之前翻转了y轴,所以也需要将纹理图片翻转一下。
接下来写一个新的函数来画三角形。上面zbuffer部分,我们用重心坐标算出了z值,现在我们有了三角形三个顶点的纹理坐标,同样可以用重心坐标
算出当前位置的纹理坐标:
$$
\begin{aligned}
x &= (x_1, x_2, x_3) * barycentric \\
y &= (y_1, y_2, y_3) * barycentric
\end{aligned}
$$
根据上面式子算出坐标后,放大到纹理图片比例即可:
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,
]),
);
}
}
}
}
|
效果如下:
详细代码见这里dd09711edb4fa1dcb129ccce64959a0fec749f42