メッシュレンダラへの責任分離 < 点を描くところから始めるRust製ソフトウェアラスタライザ
A. mainのコードにworldの管理に関する責任が紛れ込んでたり
B. Actorに四面体の挙動特有の動作が入り込んでいる
C. Cameraにレンダリングのコードががっつり入ってしまっている
D. Shaderに拡張性がなく、シグネチャがシェーダーごとに変わってしまう。
動機
コードの責任がぐっちゃぐちゃ!!
A. mainのコードにworldの管理に関する責任が紛れ込んでたり
mainのコードはオブジェクトの操作だけに留められないかappbird.icon
code:main.rs
let mut world = vec![];
let mut to_be_destroyed = Vec::<usize>::new();
code:main.rs
for (idx, actor) in world.iter_mut().enumerate() {
actor.update(dt);
if actor.is_terminated() { to_be_destroyed.push(idx); }
}
while let Some(idx) = to_be_destroyed.pop() {
world.swap_remove(idx);
}
actorの追加や削除・updateの呼び出しや管理はWorldクラスを作ってまとめてしまいたい。
deltatimeの管理は誰がする?
Canvasかな それってCanvasだね
フレームに関することだしなという
B. Actorに四面体の挙動特有の動作が入り込んでいる
もっといろんなActorを取り扱いたいときに、これでは不便...。
---> コンポーネントシステムの導入を図る
物理的挙動をさせるのはTransformに、self.scaleあたりや寿命の設定はtetrahedronオブジェクト専用の性質としたい。 Transformは物理的挙動を担当しないので、正確には別に新たにRigidbodyコンポーネントを作る必要があるとは思うが...appbird.icon
まぁそれは区別が必要になった時にやる。
code:world/actor.rs
pub fn update(&mut self, dt:f64) -> () {
// transformが持つべきデータ
self.position += &self.velocity * dt;
self.velocity += &self.acc * dt;
self.theta += &self.omega * dt;
self.omega += &self.angacc * dt;
// actorが持つべき情報
let t = self.created_at.elapsed().as_secs_f64();
// tetrahedronが持つべき情報
let scale_t = f64::min(t / 0.5, 1.);
self.scale = Vec4::newvec(1., 1., 1.) * 1.1 * ease::out_back(scale_t);
if t > 3.0 { self.terminate(); }
}
C. Cameraにレンダリングのコードががっつり入ってしまっている
オブジェクトごとにレンダリング手法が変わる可能性があるappbird.icon
三角形のラスタライズコードはカメラが担当するべきなのか...?
---> MeshRendererクラスを作って責任の分割を行うことで、万が一それがが新しく追加された仕様に対応できなくても、新たなMeshRendererを作ることで元のレンダラーは残しつつも対応できる
interpolated: points, color, uv, ...
関数の引数として渡したい
uniform: texture, far, near, fog_far, background ...
メンバー変数でもいいんじゃないか?
code:world/camera.rs
impl Camera{
...
pub fn draw_triangle(
&mut self,
canvas:&mut Canvas,
) {
let points = points.map(|p| p.0);
let bound_x = ClosedInterval::between(0,(canvas.width-1) as i32);
...
}
}
D. Shaderに拡張性がなく、シグネチャがシェーダーごとに変わってしまう。
今後いろんな種類のシェーダーを描いていくことになることを思うと、ちょっと整理したい。
関数の引数の数が多くなりすぎる!
code:world/camera.rs
pub fn draw_triangle(
&mut self
...
) {
...
let color = &color0*w0 + &color1*w1 + &color2*w2; let color = fragment::fog_fshader(
p, depth,
&color,
&canvas.background_color,
self.far, self.near,
40.0
);
...
}
code:shader/fragment.rs
pub fn fog_fshader(
_point:Point2,
depth:f64,
color:&Vec4,
background_color:&Vec4,
far: f64, near: f64,
fog_far:f64
) -> Vec4 {
let depth = (2.*far*near) / (far+near - (far-near)*depth);
let depth = (depth - near)/(fog_far - near);
let depth = f64::clamp(depth, 0.0, 1.0);
//let depth = depth*depth;
return background_color*depth + color*(1. - depth);
}
というわけでまずは問題Aに取り組もう
A. mainのコードにworldの管理に関する責任が紛れ込んでいる
Worldクラスを作るappbird.icon
持つべきはspawn, update, delete_objみたいな関数かな
code:world/world.rs
struct World {
actors:Vec<Actor>
}
impl World {
pub fn spawn(&mut self, actor: Actor) {
self.actors.push(actor);
}
pub fn update(&mut self, deltatime:f64) {
let mut terminated = Vec::<usize>::new();
for (idx, actor) in self.actors.iter_mut().enumerate() {
actor.update(deltatime);
if actor.is_terminated() { terminated.push(idx); }
}
self.sweep_obj(terminated);
}
fn sweep_obj(&mut self, mut terminated:Vec::<usize>) {
while let Some(idx) = terminated.pop() {
self.actors.swap_remove(idx);
}
}
}
Canvasにdeltatime関連の実装を移す。
ストップウォッチをCanvas側の管理にする
そして、new時に開始することにして
passed_time(), deltatime()をそれぞれとれるように
deltatimeの更新はupdate時に行う。
code:canvas/base.rs
pub struct Canvas {
...
from_start:Stopwatch,
from_prev_frame:Stopwatch,
}
impl Canvas {
pub fn new(width:usize, height:usize) -> minifb::Result<Canvas>{
...
let mut from_start = Stopwatch::new();
let from_prev_frame = Stopwatch::new();
from_start.start();
...
}
...
pub fn passed_time(&self) -> f64 {
self.from_start.elapsed_as_sec()
}
pub fn deltatime(&self) -> f64 {
self.from_prev_frame.elapsed_as_sec()
}
pub fn update(&mut self) -> minifb::Result<bool> {
self.window.update_with_buffer(&self.color_buffer, self.width, self.height)?;
self.from_prev_frame.reset(); // inserted
self.from_prev_frame.start(); // inserted
for i in 0 .. self.width * self.height {
...
}
Ok(self.window.is_open() && !self.window.is_key_down(Key::Escape))
}
...
}
B. Actorに四面体の挙動特有の動作が入り込んでいる
まず、Transformを作る
座標変換関連を管理することを想定
update関数を呼んでもらうことで、position, thetaを更新していくことを想定する
モデル変換行列もこいつが構成することができるので、そうしてもらう。
code:actor.rs
pub struct Transform {
pub position:Vec4,
pub velocity:Vec4,
pub acc:Vec4,
pub theta:Vec4,
pub omega:Vec4,
pub angacc:Vec4,
pub scale: Vec4,
}
impl Transform {
pub fn new() -> Self {
let zerovec = Vec4::newvec(0., 0., 0.);
Transform {
position: Vec4::newpoint(0., 0., 0.),
velocity: zerovec.clone(),
acc: zerovec.clone(),
theta: Vec4::newvec(2.*PI, 0., 0.),
omega: zerovec.clone(),
angacc: zerovec.clone(),
scale: Vec4::newvec(1., 1., 1.)
}
}
pub fn update(&mut self, dt:f64) {
self.position += &self.velocity * dt;
self.velocity += &self.acc * dt;
self.theta += &self.omega * dt;
self.omega += &self.angacc * dt;
self.acc = Vec4::zero();
self.angacc = Vec4::zero();
}
pub fn model_conversion(&self) -> Mat4x4 {
let t = self.theta.norm3d();
Mat4x4::translate(&self.position)
* Mat4x4::rotation(&(&self.theta / t), t)
* Mat4x4::scale(&self.scale)
}
}
そのうえで、Transformに移したものをActor側では削る
code:world/actor.rs
pub fn new(
mesh:Mesh
) -> Actor {
Actor {
mesh,
transform: Transform::new(),
created_at: Instant::now(),
terminated: false
}
}
pub fn update(&mut self, dt:f64) -> () {
self.transform.update(dt);
let t = self.created_at.elapsed().as_secs_f64();
let scale_t = f64::min(t / 0.5, 1.);
self.transform.scale = Vec4::newvec(1., 1., 1.) * 1.1 * ease::out_back(scale_t);
if t > 3.0 { self.terminate(); }
}
C. Cameraにレンダリングのコードががっつり入ってしまっている
MeshRendererを作り、CameraとCodeを移すようにする
Meshを定義する
今まで、三角ポリゴンのVecで定義していたが、使用する頂点と色属性のデータを格納したうえで、三角形を構成するのにどの頂点をどういう順番で使うかを明示するようにした。
code:rs
pub struct Mesh {
pub vertices: Vec<Vec4Model>,
pub colors: Vec<Vec4>,
}
これにより、一枚以上のポリゴンで構成されていた時に、メモリが必要以上に喰われることがなくなった
ポリゴンの三角形が辺を共有していた時に、その頂点を二度記録する必要があった
VBOがvertices, colorsに相当していて、v_idxインデックスバッファ(IBO)に対応 これを用いて、頂点シェーダ周りのコードを修正してみる。
code:world/mesh_renderer.rs
pub struct MeshRenderer {
culling: bool
}
まず、これを用いてrenderメソッドを作ってみる
描画対象となるMesh、Camera、そしてCanvasを受け取るようにする
PV(投影ビュー行列)はCameraから計算はできるが、Actor毎に同じものを計算させるのは無駄なので避ける。同じものを使わせる。
modelはモデル変換行列で、これはTransformからとれる
つまり、利用する側となる描画呼び出しコードはこんな感じになる。今回はWorldにまかせることにする。
正直引数はもっと減らせる気がするが、今はできるだけ早く結果を見ることを目的にするので、後から減らすことを考える
code:world/world.rs
pub fn draw(&self, camera:&Camera, canvas:&mut Canvas) {
let p = camera.perspective_conversion();
let v = camera.view_conversion();
let pv = p*v;
for actor in &self.actors {
actor.mesh_renderer.render(
&actor.mesh, camera, canvas,
&pv,
&actor.transform.model_conversion()
);
}
}
したがって、renderのコードは次のようになる。
変換処理はVBOに相当するverticesを逐次処理していけばOK
code:world/mesh_renderer.rs
impl MeshRenderer {
pub fn new() -> Self {
MeshRenderer { culling: false }
}
pub fn render(
&self,
mesh:&Mesh,
camera:&Camera,
canvas:&mut Canvas,
pv:&Mat4x4,
model:&Mat4x4
) {
let pvm = pv * model;
let converted_vertex =
mesh.vertices.iter()
.map(|v| { vertex::default_vshader(&pvm, v).into_screen(&canvas.size()) })
.collect();
self.rasterize( // 未実装
mesh,
&converted_vertex,
camera,
canvas
);
}
}
そしてself.rasterizeも実装していく。
meshや変換かけられた頂点群の対応を取ること以外は基本的にすべて同じコードなので、一部省略。
code:rs
fn rasterize(
&self,
mesh:&Mesh,
converted_vertex:&Vec<Vec4Screen>,
camera:&Camera,
canvas:&mut Canvas
) {
for idx in &mesh.v_idx {
let points:Vec<&Vec4> = idx.iter().map(|d| { &converted_vertex*d.0 } ).collect(); let colors:Vec<&Vec4> = idx.iter().map(|d| { &mesh.colors*d } ).collect(); // y基準でソート
let bound_x = ClosedInterval::between(0,(canvas.width-1) as i32);
let bound_y = ClosedInterval::between(0,(canvas.height-1) as i32);
let bound_z = ClosedInterval::between(-1.,1.);
...
for y in &y_segment.and(&bound_y) {
for x in &x_segment.and(&bound_x) {
...
canvas.draw_pixel_with_depth(&p, &depth, &color);
}
}
}
}
以上で、メッシュレンダラーが定義でき、cameraから責任を分離することができた。
カメラのレンダー部分のコードは消しておく
code:camera.rs
pub struct Camera {
pub near:f64,
pub far:f64,
pub width:f64,
pub height:f64,
pub position:Vec4,
pub up:Vec4,
pub look:Vec4,
}
impl Camera {
pub fn new(aspect: f64) -> Camera {
...
}
pub fn perspective_conversion(&self) -> Mat4x4 {
...
}
pub fn view_conversion(&self) -> Mat4x4 {
...
}
}
最後に、tetrahedronをMeshの定義に合わせる
ロジックが結構変わり、ちょいとややこしいが本質的には変わっていない。
今までPolygonを複数枚まとめていたものを返していたのを、Mesh一つ返すようにしているので注意
code:rs
pub fn tetrahedron(vert_color:Vec4; 4) -> Mesh { let vectors = [
...
];
...
];
let mut vertices: Vec<Vec4Model> = vec![];
let mut colors:Vec<Vec4> = vec![];
let mut v_idx_list: Vec<usize; 3> = vec![]; for (idx, index_array) in indecies.into_iter().enumerate() {
let tri_vertices = vec![
];
let g = (&(tri_vertices0 + tri_vertices1) + tri_vertices2) / 3.; for i in 0..3 {
let v = &((tri_verticesi - &g) * 0.95) + &g; vertices.push(Vec4Model(v));
colors.push(vert_color[index_arrayi].clone()); }
};
Mesh {
vertices,
colors,
v_idx: v_idx_list
}
}
最後にmainをちょっと変える。
code:main.rs
while canvas.update()? {
...
if t > tetra_interval * (count_tetra as f64) {
...
actor1.transform.velocity = Vec4::newvec(1.2*f64::cos(theta_fire), 1.2*f64::sin(theta_fire), 0.75).normalized3d() * 17.5;
actor1.transform.acc = Vec4::newvec(0., 0., -12.);
actor1.transform.theta = Vec4::choice_in_sphere(&mut rng) * PI;
actor1.transform.omega = Vec4::choice_in_sphere(&mut rng) * rng.random_range(0. .. 4.*PI);
...
}
world.update(dt);
world.draw(camera, canvas);
}
D. はやらない
調べてみた感じ
Bevyだと型のリフレクション等を用いており、実装がかなり大変そう 今のままでも、MeshRendererの中に責任を閉じ込めているので、なんとかはなるappbird.icon
個人開発では抽象化は予期してやるものというより、必要になってからやるものと思うので、今回は先にTexture MappingとShadingを優先してやることに。
ただ、今後やるとすれば、Enumを用いてフラグメントシェーダーのパラメータを渡せるようにするとか、そういう感じになるのかな~appbird.icon
動け!
なんか....動きがやたら遅いですね...?
でもfpsが下がってるわけじゃなく、ただ単に時が遅くなったように感じる
fpsが下がってると、deltatimeが大きくなるはずなので
ということはdeltatimeの算出がなんか変
https://gyazo.com/889c3cfdcae78a271c331392d3b1b79f
いや、deltatimeは変ではないな...appbird.icon
となるとworldの更新がなんか変?appbird.icon
いや、そうでもない....
となると、mainのコードか?
あぁここだ...。updateでdeltatimeのリセットが挟まってるのだから、ここでdeltatimeとっても意味がないんじゃ...。
今のdeltatimeは前フレームリセットからの経過時間って実装になってるけど、前フレームと今フレームの時間の経過具合にしないといけないappbird.icon
code:main.rs
while canvas.update()? {
let t = canvas.passed_time();
let dt = canvas.deltatime();
...
if t > tetra_interval * (count_tetra as f64) {
...
}
world.update(dt);
world.draw(camera, canvas);
}
というわけでCanvasのコードをこんな感じに書き換え
code:base.rs
pub fn deltatime(&self) -> f64 {
self.deltatime
}
pub fn update(&mut self) -> minifb::Result<bool> {
self.deltatime = self.from_prev_frame.elapsed_as_sec();
self.from_prev_frame.reset();
self.from_prev_frame.start();
self.window.update_with_buffer(&self.color_buffer, self.width, self.height)?;
for i in 0 .. self.width * self.height {
self.color_bufferi = encode_color(&self.background_color); }
Ok(self.window.is_open() && !self.window.is_key_down(Key::Escape))
}
元気に飛ぶようになった!
https://gyazo.com/bc65d39f020b43de28ea6ecc236bcb7f
B. についてさらに考えたい
今のコードも含んでしまっている
いま、TransformとMeshRendererを分離できているので、組み合わせて実装することは非常に簡単appbird.icon
ということは...appbird.icon
update(self, dt:f64) -> ()
transform(&self) -> &Transform
Q. これはmutでなくてもよいのか?appbird.icon
今後衝突判定とかやりだすなら必要そう...?appbird.icon
別のオブジェクトがほかのオブジェクトを移動させる場合とかは必要だろうなとは思う
World以上が必要なければ
render(&self) -> ()
is_terminated(&self) -> bool
の実装を要求すればよい。
code:actor.rs
pub trait Actor {
fn update(&mut self, dt:f64) -> ();
fn transform(&self) -> &Transform;
fn render(&self, camera:&Camera, canvas:&mut Canvas, pv:&Mat4x4) -> ();
fn is_terminated(&self) -> bool;
}
terminatedの実装はなんならupdateに役割を渡してもよいかもしれないが、なんかコードがややこしくなりそうなので今回はやめとくappbird.icon
戻り値で判別できるようにする
というわけでActorはtraitの実装だけにしておく。
これに対して、tetrahedronのモデルを次のように書き換えておく。
適宜、ファイルを分離しておく。
mesh.rs, transform.rsなどに
code:world/tetrahedron.rs
pub fn tetrahedron_mesh(vert_color:Vec4; 4) -> Mesh { let vectors = [
Vec4::newpoint(0., 1., 0.),
Vec4::newpoint(f64::cos(0.), -1./3., f64::sin(0.)),
Vec4::newpoint(f64::cos(2.*PI/3.), -1./3., f64::sin(2.*PI/3.)),
Vec4::newpoint(f64::cos(4.*PI/3.), -1./3., f64::sin(4.*PI/3.)),
];
....
}
pub struct TetrahedronActor {
pub mesh:Mesh,
pub transform:Transform,
pub mesh_renderer: MeshRenderer,
created_at: Instant,
terminated:bool,
}
impl TetrahedronActor {
pub fn new(
mesh:Mesh
) -> Self {
Self {
mesh,
transform: Transform::new(),
mesh_renderer: MeshRenderer::new(),
created_at: Instant::now(),
terminated: false
}
}
}
impl Actor for TetrahedronActor {
fn update(&mut self, dt:f64) -> () {
self.transform.update(dt);
let t = self.created_at.elapsed().as_secs_f64();
let scale_t = f64::min(t / 0.5, 1.);
self.transform.scale = Vec4::newvec(1., 1., 1.) * 1.1 * ease::out_back(scale_t);
if t > 3.0 { self.terminated = true; }
}
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());
}
fn is_terminated(&self) -> bool {
self.terminated
}
}
それに合わせてworldのdrawも書き換え
code:world/world.rs
pub struct World<T> where T:Actor {
actors:Vec<T>
}
impl<T> World<T> where T:Actor {
pub fn draw(&self, camera:&Camera, canvas:&mut Canvas) {
let p = camera.perspective_conversion();
let v = camera.view_conversion();
let pv = p*v;
for actor in &self.actors {
actor.render(camera, canvas, &pv);
}
}
}
いまこのWorldには一種類のワールドしか追加できないが、まあここは後で考えるappbird.icon
理想的には、使えるだけのゲームオブジェクトの型をすべて列挙したWorldが欲しいねappbird.icon
衝突判定とかを将来的に視野に入れるなら、そのオブジェクト間の判定を書けるようにしたい
動いた!!
1:00までやるわよappbird.icon