テクスチャマッピング < 点を描くところから始めるRust製ソフトウェアラスタライザ
テクスチャマッピング < 点を描くところから始めるRust製ソフトウェアラスタライザ
https://gyazo.com/8127c3b9c06b682db08ff70fb196ab35
やること
1. テクスチャのマッピング
TexturedMeshの定義
板ポリのメッシュ
TexturedMeshに対応したMeshRenderer
TextureMeshRendererの実装
texture_fshaderの実装
ワールドの実装
2. リファクタリング
バーテックスシェーダー・フラグメントシェーダーで構成されたレンダリングパイプライン < 点を描くところから始めるRust製ソフトウェアラスタライザ
そもそも何にどういうテクスチャを貼る...?appbird.icon
長方形に
このテクスチャを貼ることを
目的としてみる。
https://youtu.be/yyJ-hdISgnw?si=AVwRha67A_k_68Wa
とりあえず、まずは雑にやる
まずuv座標が必要なので、colorsの代わりにUV座標を拾ってくるようにする。
面倒なのでこいつもvec4で管理する
理想はVec2とその加乗算を定義することだが...
まあそれは後からできるから...
今回UV座標を一つ一つ決めていくのは面倒なので、板ポリの上にマッピングできるかを考えていく。
というわけで、板ポリのActorを定義しよう。
今回色情報は属性として必要ではないので、代わりにuvを載せたい。 ---> TexturedMesh
code:world/mesh.rs
#derive(Debug, Clone)
pub struct TexturedMesh {
pub vertices: Vec<Vec4Model>,
pub uv: Vec<Vec4>,
pub v_idx: Vec<usize; 3>
}
板ポリのメッシュを定義する。
code:plane.rs
pub fn plane_mesh() -> TexturedMesh {
let vectors = vec![
Vec4::newpoint(0., -1., -1.),
Vec4::newpoint(0., -1., 1.),
Vec4::newpoint(0., 1., 1.),
Vec4::newpoint(0., 1., -1.),
];
let indecies: Vec<usize; 3> = vec![
0, 1, 2,
2, 3, 0,
];
let uv: Vec<Vec4> = vec![
Vec4::newpoint(0., 0., 0.),
Vec4::newpoint(0., 1., 0.),
Vec4::newpoint(1., 1., 0.),
Vec4::newpoint(1., 0., 0.),
];
TexturedMesh {
vertices: vectors.into_iter().map(|e| { Vec4Model(e) }).collect(),
uv,
v_idx: indecies
}
}
そしてtetrahedronのActorの実装を持ってきて、書き換えていく。
TexturedMeshに対応したMeshRendererが欲しい。
code:world/plane.rs
#derive(Clone)
pub struct PlaneActor {
pub mesh:TexturedMesh,
pub transform:Transform,
pub mesh_renderer: MeshRenderer,
}
impl PlaneActor {
pub fn new() -> Self {
let white = Vec4::new(1., 1., 1., 1.);
Self {
mesh: plane_mesh(0,1,2,3.map(|_| { white.clone() })),
transform: Transform::new(),
mesh_renderer: MeshRenderer::new()
}
}
}
impl Actor for PlaneActor {
fn update(&mut self, dt:f64) -> () {
self.transform.update(dt);
}
fn transform(&self) -> &Transform {
&self.transform
}
fn render(&self, camera:&Camera, canvas:&mut Canvas, pv:&Mat4x4) -> () {
self.mesh_renderer.render(&self.mesh, camera, canvas, pv, &self.transform.model_conversion()); // error!
}
fn is_terminated(&self) -> bool {
false
}
}
ん??ちょっと待て、正方形を写したはずなのになんか縦横比違うくない...?
https://gyazo.com/3cfa8d61200e6319996de5461164a915
よく考えたら乾板のサイズがWxHと設定されたうえで、ウィンドウのアスペクトH/Wとも合わせてるのに
(-1.0, 1.0)サイズになってるのを、ウィンドウサイズに引き伸ばしてあげなきゃいけないな
code:vec4.rs
impl Vec4Project {
pub fn into_screen(&self, size:&Point2) -> Vec4Screen {
let scale_y = size.y as f64 / 2.;
let scale_x = size.x as f64 / 2.;
let v = self.0.scaled_xy(&scale_x, &(-&scale_y)) + size.to_vec4() / 2.;
Vec4Screen(v)
}
}
uvを渡されてサンプリングするテクスチャがほしい ---> TextureMeshRendererの実装をする
MeshRendererを変えたくはないので、コピーして新しくTextureMeshRendererを作る
これはDRYの原則に反するが、抽象化の方針を見出すためにいったんこうする。
code:world/textured_mesh_renderer.rs
use image::{ImageReader, Rgba32FImage};
#derive(Clone)
pub struct TextureMeshRenderer {
texture: Rgba32FImage,
culling: bool
}
さらに話を簡単化するために、内部でどんなテクスチャ画像を使うかも決めてしまう
あとでカスタマイズできるようにする。
code:rs
impl TextureMeshRenderer {
pub fn new() -> Self {
let texture = ImageReader::open("resource/texture.jpg").unwrap().decode().unwrap();
let texture = texture.to_rgba32f();
Self { texture, culling: false }
}
基本的にそれ以外は同じだが、MeshをTexturedMeshに置き換えるのを忘れずに...
code:rs
pub fn render(
&self,
mesh:&TexturedMesh, // changed!
_camera:&Camera,
canvas:&mut Canvas,
pv:&Mat4x4,
model:&Mat4x4
) {
...
}
rasterizeも同じだが、uv座標を出すように置き換える。
code:rs
fn rasterize(
&self,
mesh:&TexturedMesh,
converted_vertex:&Vec<Vec4Screen>,
canvas:&mut Canvas
) {
for idx in &mesh.v_idx {
let points:Vec<&Vec4> = idx.iter().map(|d| { &converted_vertex*d.0 } ).collect();
let uvs:Vec<&Vec4> = idx.iter().map(|d| { &mesh.uv*d } ).collect();
...
for y in &y_segment.and(&bound_y) {
for x in &x_segment.and(&bound_x) {
...
let p = p.to_point2();
let depth = w0*z0 + w1*z1 + w2*z2;
let uv = uvs0*w0 + uvs1*w1 + uvs2*w2; // changed
let color = fragment::texture_fshader(p, &uv, &self.texture);
canvas.draw_pixel_with_depth(&p, &depth, &color);
}
}
}
}
さらに、textureに対応したフラグメントシェーダーtexture_fshaderも実装してしまう。
code:shader/fragment.rs
pub fn texture_fshader(
_point:Point2,
uv:&Vec4,
texture:&Rgba32FImage
) -> Vec4 {
let (w, h) = texture.dimensions();
let (u, v) = (uv.x() * w as f64, uv.y() * h as f64);
let (u, v) = (u as u32, v as u32);
let pixel = texture.get_pixel_checked(u, v);
if let Some(c) = pixel {
Vec4::new(c0 as f64, c1 as f64, c2 as f64, c3 as f64)
} else {
Vec4::new(0., 0., 0., 0.)
}
}
最後にワールドを整える!
code:main.rs
pub fn plane_world(camera:&mut Camera, canvas:&mut Canvas) -> Throwable<()> {
let mut world = World::new();
let r = 4.0;
let actor = PlaneActor::new();
world.spawn(actor);
while canvas.update()? {
let t = canvas.passed_time();
let dt = canvas.deltatime();
let camera_theta = 2.*PI * ease::outin_quart(t / 4. % 1.);
camera.position = Vec4::newpoint( r*f64::cos(camera_theta), r*f64::sin(camera_theta), 2. * (f64::cos(camera_theta) - 0.5)) ;
camera.look = (-&camera.position).normalized3d();
camera.up = Vec4::newvec(0., 0., 1.);
world.update(dt);
world.draw(camera, canvas);
}
Ok(())
}
fn main() -> Throwable<()> {
...
plane_world(&mut camera, &mut canvas)?;
Ok(())
}
以上の編集によって示されたのが次の板ポリである。
ffmpegを使うときに、ノイズを低減しつつmp4をgif化したい
https://gyazo.com/df015327e6318bb0bdded8939a85931b
なんか...歪んでる?appbird.icon
code:texture_mesh_renderer.rs
let uv = uvs0*w0 + uvs1*w1 + uvs2*w2;
uvの補間が画面画素上での補間になっている点に注意
正確には、3Dポリゴンの空間の上で行わないといけない
これをするには、奥行depthまで含めてテクスチャをサンプルする必要がある。
ある画面上の点$ p' = (x', y')のBarycentric座標が$ (w_1', w_2', w_3')だったとき、
$ p' = w_1'p_1' + w_2'p_2' + w_3'p_3'
三次元上点$ p = (x,y,z)のBarycentric座標は$ (w_1 z/z_1, w_2 z/z_2, w_3 z/z_3)と表せるのだった。
c.f. depth-bufferingとカリング < rusterizer-from-dots
$ w' = w z'/z
したがって、このBarycentric座標を利用すれば題意のものを計算できる
このz値は透視深度から変換することでも得られるが、今回は手っ取り早くz値をrenderから渡してもらうことにする。
code:world/textured_mesh_renderer.rs
pub fn render(
&self,
mesh:&TexturedMesh,
_camera:&Camera,
canvas:&mut Canvas,
pv:&Mat4x4,
model:&Mat4x4
) {
let pvm = pv * model;
let converted_vertex:Vec<Vec4Project> =
mesh.vertices.iter()
.map(|v| { vertex::default_vshader(&pvm, v) })
.collect();
let w =
converted_vertex.iter().map(|v| { v.w() }).collect();
let converted_vertex =
converted_vertex.into_iter().map(|v| {v.into_screen(&canvas.size())}).collect();
self.rasterize(
mesh,
&converted_vertex,
&w,
canvas
);
}
code:rs
fn rasterize(
&self,
mesh:&TexturedMesh,
converted_vertex:&Vec<Vec4Screen>,
z_values:&Vec<f64>,
canvas:&mut Canvas
) {
for idx in &mesh.v_idx {
let points:Vec<&Vec4> = idx.iter().map(|d| { &converted_vertex*d.0 } ).collect();
let uvs:Vec<&Vec4> = idx.iter().map(|d| { &mesh.uv*d } ).collect();
let z_value:Vec<f64> = idx.iter().map(|d| { z_values*d }).collect();
...
for y in &y_segment.and(&bound_y) {
for x in &x_segment.and(&bound_x) {
...
let z = 1./(w0/z_value0 + w1/z_value1 + w2/z_value2);
let uv = uvs0*w0/z_value0 + uvs1*w1/z_value1 + uvs2*w2/z_value2;
let uv = uv * z;
let color = fragment::texture_fshader(p, &uv, &self.texture);
canvas.draw_pixel_with_depth(&p, &depth, &color);
}
}
}
}
うん...?描画結果が変わらない...
よく観察すると、Vec4Projectがwで除算された結果になってる...appbird.icon
code:world/textured_mesh_renderer.rs
let converted_vertex:Vec<Vec4Project> =
mesh.vertices.iter()
.map(|v| { vertex::default_vshader(&pvm, v) })
.collect();
println!("{converted_vertex:?}");
code:rs
[Vec4Project(Vec4 { e: 0.17758107315882818, -0.2504460617501806, 0.9586020625571247, 1.0 }), Vec4Project(Vec4 { e: 0.14160994905994392, 0.12381946080830496, 0.9673932280787986, 1.0 }), Vec4Project(Vec4 { e: -0.1557722137898125, 0.2196886007039331, 0.963932040347779, 1.0 }), Vec4Project(Vec4 { e: -0.2004325265320655, -0.1752521452651702, 0.9530172799280721, 1.0 })]
Vec4Projectの結果を見返してみると、あぁ....
これnewしてる地点でwで除算しちゃってるな
pubしないようにしないと
code:util/vec4.rs
pub struct Vec4Project(pub Vec4);
impl Vec4Project {
pub fn new(v:Vec4) -> Self {
if v.w() < 1e-6 { Self(v*1e6) } else { Self(&v/v.w()) }
}
wの情報が欲しいので、こんな感じに中身のVec4は隠ぺいしておいて、Vec4に直すメソッドで正規化するか
code:rs
pub struct Vec4Project(Vec4);
impl Vec4Project {
pub fn new(v:Vec4) -> Self {
Self(v)
}
...
pub fn to_vec4(&self) -> Vec4 {
let v = &self.0;
if v.w() < 1e-6 { v*1e6 } else { v/v.w() }
}
}
となると、スクリーン座標に持ってくinto_screenの実装もちょっと修正
code:util/vec4.rs
pub fn into_screen(&self, size:&Point2) -> Vec4Screen {
let scale_x = size.x as f64 / 2.;
let scale_y = size.y as f64 / 2.;
let v = self.to_vec4().scaled_xy(&scale_x, &(-&scale_y)) + size.to_vec4() / 2.; // self.0となっていたのを修正
Vec4Screen(v)
}
たぶんヨシ!!appbird.icon
https://gyazo.com/8127c3b9c06b682db08ff70fb196ab35
バーテックスシェーダー・フラグメントシェーダーで構成されたレンダリングパイプライン < 点を描くところから始めるRust製ソフトウェアラスタライザ