カラー頂点属性を持ったモデルのロード < rusterizer-from-dots
https://gyazo.com/1eade565fe9711c8a798502f244ea151
目次
gltfの基本的な使い方とテスト
gltfのデータをVertexArrayObjectに変換する カラー頂点属性を持ったモデルのレンダリング
bugfix: y軸方向に空いたポリゴンのスリット
bugfix: cullingをonにすると正しく描画されなくなる
bugfix: glTFとBlenderの座標系における前方向の違い
まずgltfの基本的な使い方とテスト
AC.icon
今回はMeshのデータを読みたいので、Meshのドキュメントを漁ってExampleを見てみよう。
こうかな いったんテストしてみよう。
code:mesh/mesh_loader.rs
use std::path::Path;
use gltf::Gltf;
use crate::{mesh::VertexArrayObject, shader::texture_pipeline, util::Throwable};
fn load_vao(path:&Path)
-> Throwable<()> {
//-> Throwable<VertexArrayObject<texture_pipeline::Attribute>> {
let model = Gltf::open(path)?;
for mesh in model.meshes() {
println!("Mesh #{}", mesh.index()); for primitive in mesh.primitives() {
println!("- primitive #{}", primitive.index()); for (semantic, _) in primitive.attributes() {
println!("-- {semantic:?}");
}
}
}
Ok(())
}
まずBlenderでトーラスでも作りますappbird.icon
カラー属性つけたいな
カラー属性どうやっていじるんや
https://youtu.be/hqBJtM8y064?si=Uud5Vk8dxWFz1OtY
https://gyazo.com/d162cbb503089d4f2f00a5b040d29164
いかにもマリルイで出てきそうなオブジェが出来てしまった...appbird.icon
まず、既存のtetrahedronのデモと、mesh_loadのデモをsrc/binに写しておく
まっさらになったmainのコードにglTFのロードのコードを組みこんでいく!
期待するのは、ファイルの中身の属性値を全部出力することだけどいけるかなappbird.icon
code:main.rs
use std::path::Path;
use renderer_core::mesh::mesh_loader::load_vao;
use renderer_core::world::camera::Camera;
use renderer_core::canvas::Canvas;
use renderer_core::util::Throwable;
fn main() -> Throwable<()> {
let w: usize = 640;
let h: usize = 480;
let aspect = (h as f64) / (w as f64);
let mut _canvas = Canvas::new(w, h)?;
let mut _camera = Camera::new(aspect);
load_vao(&Path::new("./resource/test_model.glb"))?;
Ok(())
}
code:stdout
-- Positions
-- Normals
-- Colors(0)
-- TexCoords(0)
おおお!!!appbird.icon
いけてるわねappbird.icon
しかし、Colorsの後の(0)はなんだ...?appbird.icon
あぁ、0番目の色指定ってことか
まぁともかく、Colorsから始まるものを取り出せばいいのかな
いや、そんな安全じゃないことやる必要あるか
Exampleを続けて読むと、readerがあった!
code:rs
let (model, buffers, _) = gltf::import(path)?;
:
if let Some(iter) = reader.read_positions() {
println!("vertex position");
for vertex_position in iter.take(5) {
println!("{vertex_position:?}");
}
}
なるほど、これで頂点情報、カラー情報、インデックスバッファ情報全部取ってみるかappbird.icon
code:stdout
vertex position
vertex color
vertex index
1
50
55
1
55
7
5
53
59
5
見た感じ、vertex indexは本当に三角形ポリゴンの頂点添字列が3つ並びで1次元配列に沿って並んでるっぽいなappbird.icon
ほなそんな感じにロードしてみるかappbird.icon
gltfのデータをVertexArrayObjectに変換するappbird.icon code:rs
let positions =
reader.read_positions().expect("The model data doesn't have position data.")
.map(|point| Vec4::newpoint(point0 as f64, point1 as f64, point2 as f64)) .collect::<Vec<_>>();
let colors =
reader.read_colors(0).expect("The model data doesn't have color data.")
.into_rgb_f32()
.map(|color| Vec4::newvec(color0 as f64, color1 as f64, color2 as f64)) .collect::<Vec<_>>();
ん...?Vec<usize>をVec<[usize; 3]>にするのどうすりゃいいんだ...?appbird.icon
単純なforで書く場合にはindexで3ずつ送りにするほかはないのかな
そういうメソッドはないのだろうかappbird.icon
chunks_exactがあるでGPT.icon
なるほど
2025-10-15
code:rs
let idcs =
reader.read_indices()
.expect("The model data doesn't have indices data.")
.into_u32()
.collect::<Vec<_>>()
.chunks_exact(3)
.map(|idx| [idx0, idx1, idx2]); あとはprimitiveが出てくるたびにこいつを入れていけばいいねappbird.icon
上手く書けば関数型言語っぽくきれいにまとまるもんだね
code:rs
let points =
reader.read_positions().expect("The model data doesn't have position data.")
.map(|point| Vec4::newpoint(point0 as f64, point1 as f64, point2 as f64)) .map(|p| Vec4Model(p))
.collect::<Vec<_>>();
let colors =
reader.read_colors(0).expect("The model data doesn't have color data.")
.into_rgb_f32()
.map(|color| Vec4::newvec(color0 as f64, color1 as f64, color2 as f64)) .collect::<Vec<_>>();
assert_eq!(points.len(), colors.len());
let attribute =
points.into_iter()
.zip(colors)
.map(|(point, color)| color_pipeline::Attribute{ point, color })
.collect::<Vec<_>>();
let idx =
reader.read_indices()
.expect("The model data doesn't have color data.")
.into_u32()
.map(|u| u as usize)
.collect::<Vec<_>>()
.chunks_exact(3)
.map(|idx| [idx0, idx1, idx2]) .collect::<Vec<_>>();
vao_seq.push(VAOColor { attribute, idx });
カラー頂点属性を持ったモデルのレンダリング
というわけで、VAOだけ受け取ってあとは何もしない新しいActorとしてGltfTestActorを作って...
mainのコードをこうする!
code:rs
fn main() -> Throwable<()> {
let w: usize = 640;
let h: usize = 480;
let aspect = (h as f64) / (w as f64);
let mut canvas = Canvas::new(w, h)?;
let mut camera = Camera::new(aspect);
let mut world = World::<GltfTestActor>::new();
let vao_seq = load_vao(&Path::new("./resource/test_model.glb"))?;
vao_seq.into_iter()
.map(|vao| GltfTestActor::new(vao))
.for_each(|actor| world.spawn(actor));
while canvas.update()? {
let t = canvas.passed_time();
let theta = 2.*PI/5. * t;
let r = 3.;
camera.position = Vec4::newpoint(r* f64::cos(theta), r*f64::sin(theta), 1.);
camera.look = -camera.position.normalized3d();
camera.up = Vec4::newvec(0., 0., 1.);
world.update(canvas.deltatime());
world.draw(&camera, &mut canvas);
}
Ok(())
}
https://gyazo.com/404ebfc40e264c32c3a10bb11f8fee13
おおっ...!ちゃんと読み取れてる!!
ただ問題がいくつか...appbird.icon
1. なんか妙に穴が空いているように見える。
ポリゴンとポリゴンの合間が見える....appbird.icon
ポリゴンとポリゴンの合間も塗るようにしたい
一回確認してみるか
2. cullingをonにすると正しく描画されなくなる
モデル側の問題か?
---> そうだったとして、それをどう確かめる?appbird.icon
全部のポリゴンに対して面積を表示させる...?
一番目、二番目、三番目のそれぞれのポリゴンに着色するのもありかも。
というか、tetrahedronのデモでも表示が変になるならそれはプログラムが変ってことだ
それともrendererの問題か?
https://gyazo.com/1f5ab652a2125ab8f3674d3b401d6d9c
2025-10-16
bugfix: y軸方向に空いたポリゴンのスリット
なんか妙に穴が空いているように見える。
不等号の取り扱いに関する問題かと思ったがどうやら違うらしい
code:component/mesh_renderer.rs
let u_intv = ClosedInterval::between(0., 1.);
if !w.iter().all(|e| u_intv.includes(*e)) { continue; }
code:util/interval.rs
pub fn includes(&self, x:T) -> bool {
self.min <= x && x <= self.max
}
浮動小数点に関する誤差の話かと思い1e-6だけ広げてみたけどこれまたうーんって感じ。
これなんで三角対角線の部分はあんまり穴が空いてないんだ?
グリッドの四角辺の部分だけ穴が空いている...
アイデアがない...。
本来頂点としては共有してるはずなので、穴が空くハズはないんだけどな~~
https://gyazo.com/9e147e1b48df2ab66bca8009ea52c3e9
...穴がxy軸に平行に空いている?
これ、そもそも三角形を包むbounding boxを求める際の、x_segmentとかy_segmentとかの話ではなかろうか
いやでもrangeとかandとかor演算とかに誤りはないな...
code:mesh_renderer.rs
fn calc_bounding_box(canvas: &mut Canvas, points: &&Vec4; 3) -> (ClosedInterval, ClosedInterval, ClosedInterval<f64>) { 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.);
let x_segment = ClosedInterval::range(points.iter().map(|p| p.x() as i32));
let y_segment = ClosedInterval::range(points.iter().map(|p| p.y() as i32));
let z_segment = ClosedInterval::range(points.iter().map(|p| p.z()));
let x_segment = x_segment.and(&bound_x);
let y_segment = y_segment.and(&bound_y);
let z_segment = z_segment.and(&bound_z);
(x_segment, y_segment, z_segment)
}
待てよ?これIteratorとして実装していたよね?
もしかして、ClosedIntervalにもかかわらずendが含まれてないように実装したんじゃないか?
code:rs
impl Iterator for ClosedIntervalIter {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
let current = self.current;
self.current = current + 1;
if self.end > current {
Some(current)
} else {
None
}
}
}
...あのさぁ....
code:rs
if self.end > current {
code:rs
if self.end >= current {
はい。
よっしゃ動いたぞ動いた動いた
滑らかですね
https://gyazo.com/958fdc728cb925fcceb6101294afb79c
bugfix: cullingをonにすると正しく描画されなくなる
code:mesh_renderer.rs
// y基準でソート
let (x_segment, y_segment, z_segment) = calc_bounding_box(canvas, &points);
if z_segment.is_empty() { return; }
// Barycentric座標
let area_abc = area(&points0, &points1, &points2); // culling
if self.culling && area_abc < 0. { return; }
cullingが関わってくるのはこの部分だけ...。
符号付和が上手く計算できていなさそうだなぁ
ほとんどの場合においてみんな裏側になっているらしい、なんでだろうね
というわけで、少しばかし道具立てしてきて、符号付和の計算が毎フレーム何回行われるかを集積するようにしてみた。
シングルトンを用いて、いつでもどこでも簡単に呼び出せるやつを作っておいた。 Rustでのシングルトンなので、マルチスレッドから触られると困るので、毎回lock, unwrapする必要はある。
まぁでもデバッグ用なのでとりあえずはヨシかな。
これでどれぐらいのポリゴンが毎フレーム表を向いていると判定されているかを測れる。
code:mesh_renderer.rs
// culling
TRIANGLE_COUNTER.lock().unwrap().count(area_abc >= 0.);
if self.culling && area_abc < 0. { return; }
if area_abc.abs() < 1e-6 { return; }
let inv_abc = 1./area_abc;
code:.rs
while canvas.update()? {
:
{
let mut logger_on_loop = LOGGER_ON_LOOP.lock().unwrap();
let mut triangle_counter = TRIANGLE_COUNTER.lock().unwrap();
logger_on_loop.println(&triangle_counter.describe())?;
logger_on_loop.flush()?;
triangle_counter.reset();
}
}
するとこうなった。
culling off
code:rs
TRIANGLE_COUNTER: 600 / 1152
おおよそ600あたりを前後する。
ふむ、どうやら表裏の判定はできているようだ
culling on
code:rs
TRIANGLE_COUNTER: 1 / 1
TRIANGLE_COUNTER: 3 / 3
TRIANGLE_COUNTER: 5 / 5
:
...だいぶ奇々怪々な動きしてますね...
つまり本来描画されるべきだったポリゴンすらこの前のコードの段階で削られていることがわかる
z_segmentがemptyになっていることが原因かと見たが、どうにもそうではないらしい。
varyingやpolygonsが問題ではないっぽいことはわかった
どちらも元の数通り1152個のポリゴンを列挙すること自体はできていた。。
...じゃあどこで実行回数が減ってるんだ!?
code:rs
TRIANGLE_COUNTER.lock().unwrap().count( area_abc >= 0.);
if self.culling && area_abc < 0. { return; }
if area_abc.abs() < 1e-6 { return; }
let inv_abc = 1./area_abc;
ん...???
code:rs
if self.culling && area_abc < 0. { **return;** }
if area_abc.abs() < 1e-6 { **return;** }
んんんんんんん????????
return;で帰っちゃループが終わっちゃうじゃんかよ!!!
bugfix: glTFとBlenderの座標系における前方向の違い
cullingできた... でき.......あれ...?
https://gyazo.com/7ac3f65e5dd9c984e9408317c5d60c32
なんか....逆じゃないか?
裏側が見えてるような気がするんだが...appbird.icon
code:rs
if self.culling && area_abc > 0. { continue; }
https://gyazo.com/d29bab014c7244f1d1515d5cfadaba56
うーーーん....?areaの計算が間違えている?
頂点が反時計回りだと正なんだっけ?
はいGPT.icon
じゃああってるな....
....glTFの座標系は右手?左手?appbird.icon
📦 glTF 仕様の座標系GPT.icon
右手系 (Right-handed)
上方向 (Up): +Y
前方向 (Forward): -Z
...もう一つ質問いいかな。Blenderでの座標系は?appbird.icon
🧭 Blender の内部座標系GPT.icon
右手系 (Right-handed)
上方向 (Up): +Z
前方向 (Forward): -Y
→ つまり、Blender内で「前を向くオブジェクト」は -Y 方向を向いています。
自作レンダラーなどで glTF の頂点座標をそのまま読み込む場合、glTF の座標系(+Y up, -Z forward)に合わせて処理しないと、モデルが「寝ている」「裏返っている」ように見えます。GPT.icon
あぁぁぁぁ~~~~~~appbird.icon
それだ。。。 なんかモデルの向きがBlenderで見たものとは違うな、とは思ったが......appbird.icon
添え字を入れ替えるか
code:mesh/mesh_loader.rs
let points =
reader.read_positions().expect("The model data doesn't have position data.")
.map(|point| Vec4::newpoint(point0 as f64, point2 as f64, point1 as f64)) .map(|p| Vec4Model(p))
.collect::<Vec<_>>();
code:rs
if self.culling && area_abc < 0. { continue; }
https://gyazo.com/1eade565fe9711c8a798502f244ea151
き......きたぁぁぁぁあぁぁぁぁあぁぁぁああああ!!!!!!!!!!!!!!!!!