フルコードはgithubにあります。
めっちゃ間が空きました。コードだけ先に書いて文章を書くのを後回しにするとサボってしまう。いかん。
Zバッファの実装
複数のポリゴンを扱うときにどちらが前面かを扱うためにZバッファ法を導入する。
まずzbuffer.rsモジュールを作成する。構造体は幅と高さの情報と、そのサイズ分のピクセルの配列をもつ。データの初期値は無限遠。インデックス経由で配列にアクセスする。分かりやすく2次元配列にできないものかは後で考える。
pub struct ZBuffer {
pub width: u32,
pub height: u32,
pub data: Vec<f64>,
}
impl ZBuffer {
pub fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
data: vec![f64::INFINITY; (width * height) as usize],
}
}
#[inline]
pub fn index(&self, x: u32, y: u32) -> usize {
(y * self.width + x) as usize
}
}
ポリゴンの頂点に、Z深度フィールドを追加。
--- a/src/triangle.rs
+++ b/src/triangle.rs
@@ -3,6 +3,7 @@ use crate::color::Color;
#[derive(Copy, Clone)]
pub struct Vertex {
pub pos: Vec2,
+ pub z: f64,
pub color: Color,
}
polygon_fill()で、ピクセル描画時にZ深度をチェックして、手前だった時だけピクセルを更新する。これで手前のポリゴンが見えるようになる。
pub fn polygon_fill(
fb: &mut ImageBuffer<image::Rgb<u8>, Vec<u8>>,
transform: &Transform,
+ zbuffer: &mut ZBuffer,
triangle: &Triangle,
) {
let min_x = triangle.v0.pos.x.min(triangle.v1.pos.x.min(triangle.v2.pos.x)) as i32;
let max_x = triangle.v0.pos.x.max(triangle.v1.pos.x.max(triangle.v2.pos.x)) as i32;
let min_y = triangle.v0.pos.y.min(triangle.v1.pos.y.min(triangle.v2.pos.y)) as i32;
@@ -60,7 +66,13 @@ pub fn polygon_fill(fb: &mut ImageBuffer<image::Rgb<u8>, Vec<u8>>, transform: &T
]);
if let Some((sx, sy)) = transform.to_screen(i, j) {
- fb.put_pixel(sx, sy, rgb);
+ let idx = zbuffer.index(sx, sy);
+ let z: f64 = w0 * triangle.v0.z + w1 * triangle.v1.z + w2 * triangle.v2.z;
+
+ if z < zbuffer.data[idx] {
+ zbuffer.data[idx] = z;
+ fb.put_pixel(sx, sy, rgb);
+ }
}
}
}
境界値計算の改善
スクリーンを切り出すときに、四隅の座標を浮動小数点から整数に変換するが、単純にキャストせずきちんと切り上げ(.floor())、切り捨て(.ceil())処理することで、境界値を正確に算出した。
見た目では違いが分からんけど。
- let min_x = triangle.v0.pos.x.min(triangle.v1.pos.x.min(triangle.v2.pos.x)) as i32;
- let max_x = triangle.v0.pos.x.max(triangle.v1.pos.x.max(triangle.v2.pos.x)) as i32;
- let min_y = triangle.v0.pos.y.min(triangle.v1.pos.y.min(triangle.v2.pos.y)) as i32;
- let max_y = triangle.v0.pos.y.max(triangle.v1.pos.y.max(triangle.v2.pos.y)) as i32;
+ let (min_x, max_x, min_y, max_y) = triangle.bounding_box();
impl Triangle {
pub fn bounding_box(&self) -> (i32, i32, i32, i32) {
let v0 = self.v0.pos;
let v1 = self.v1.pos;
let v2 = self.v2.pos;
let min_x = v0.x.min(v1.x.min(v2.x)).floor();
let max_x = v0.x.max(v1.x.max(v2.x)).ceil();
let min_y = v0.y.min(v1.y.min(v2.y)).floor();
let max_y = v0.y.max(v1.y.max(v2.y)).ceil();
(min_x as i32, max_x as i32, min_y as i32, max_y as i32)
}
}
3Dへの道
今まで2次元座標を扱っていたが、vec3.rsを作って3次元に拡張する。zはとりあえず0に固定するので、動作的には変更無し。
use crate::triangle::Pixel;
use std::fmt;
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub};
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Vec3 {
pub x: f64,
pub y: f64,
pub z: f64,
}
pub type Point3 = Vec3;
impl Vec3 {
pub fn new(x: f64, y: f64, z: f64) -> Self {
Self { x, y, z }
}
impl From<Pixel> for Vec3 {
fn from(item: Pixel) -> Self {
Vec3 {
// Convert to center pixel by adding 0.5
x: item.x as f64 + 0.5,
y: item.y as f64 + 0.5,
z: 0.0,
}
}
}
ゴールは3Dのポリゴンを扱うことですが、先に2.5D化する。2.5DとはZ深度を使ってポリゴンを描くという意味。
下記のように二次元のまま各頂点のZ深度を変えて、二つの三角形が交わるようにする。
let tri = Triangle {
v0: Vertex {
pos: Vec3 {
x: 129.7,
y: -26.4,
z: 0.0,
},
z: 1.0,
color: Color::new(0.0, 0.0, 1.0),
},
v1: Vertex {
pos: Vec3 {
x: 1327.3,
y: 480.1,
z: 0.0,
},
z: 1.0,
color: Color::new(0.0, 1.0, 0.0),
},
v2: Vertex {
pos: Vec3 {
x: 124.0,
y: 841.0,
z: 0.0,
},
z: 1.0,
color: Color::new(0.5, 0.0, 0.0),
},
};
polygon_fill(&mut imgbuf, &transform, &mut zbuf, &tri);
let tri = Triangle {
v0: Vertex {
pos: Vec3 {
x: 1412.7,
y: -26.4,
z: 0.0,
},
z: 1.5,
color: Color::new(0.0, 0.0, 0.5),
},
v1: Vertex {
pos: Vec3 {
x: -55.3,
y: 280.1,
z: 0.0,
},
z: 0.1,
color: Color::new(1.0, 1.0, 0.0),
},
v2: Vertex {
pos: Vec3 {
x: 1033.0,
y: 841.0,
z: 0.0,
},
z: 1.3,
color: Color::new(1.0, 0.0, 0.0),
},
};
polygon_fill(&mut imgbuf, &transform, &mut zbuf, &tri);
出力された画像。
続く。
