//! Blender-Modell → Render-Geometrie. //! //! Gegenstück zu render::brush für den Modell-Pfad: nimmt das neutrale //! [`Model`] aus engine::gltf und baut daraus ein [`Mesh`] (Vertices + nach //! Textur gruppierte Index-Batches), das mit der Brush-Geometrie zu einem //! Buffer verschmilzt (siehe `Mesh::append`). //! //! Texturen: der Blender-Materialname *ist* der Texturname — aufgelöst wie //! bei Brushes zu `assets/textures/{name}.tga` (macht der Aufrufer). Faces //! ohne Material (`""`) laufen unter [`FALLBACK_TEXTURE`]. //! //! UVs: das Modell trägt den V-Ursprung unten links, unsere `tga::Image`s //! oben links — V wird hier gespiegelt (Achsen-Konvention ist Sache des //! Konsumenten, siehe engine::tga). Nur `visible`-Objekte werden //! ausgegeben; Collider-/Zonen-Volumen sind Sache von engine::collision //! und engine::trigger. use crate::engine::model::Model; use crate::render::scene::{Batch, Mesh, Vertex}; /// Texturname für Faces ohne `usemtl` (Material `""`). pub const FALLBACK_TEXTURE: &str = "placeholder"; /// Distinkte Texturnamen des Modells (sortiert), `""` bereits auf den /// Fallback gemappt — derselbe Kontrakt wie `brush::texture_names`. pub fn texture_names(model: &Model) -> Vec { let mut names: Vec = model.materials.iter() .map(|m| resolve(m).to_string()) .collect(); names.sort_unstable(); names.dedup(); names } fn resolve(material: &str) -> &str { if material.is_empty() { FALLBACK_TEXTURE } else { material } } /// Alle sichtbaren Objekte zu einem Mesh. `tex_names` gibt (wie bei /// `brush::build`) die Textur-Indizes vor — darf eine Obermenge sein, so /// teilen Brush- und Modell-Geometrie eine Texturliste. pub fn build(model: &Model, tex_names: &[String]) -> Mesh { let mut verts = Vec::new(); let mut per_tex: Vec> = vec![Vec::new(); tex_names.len()]; for o in &model.objects { if !o.visible { continue; } let base = verts.len() as u32; for (p, uv) in o.verts.iter().zip(&o.uvs) { verts.push(Vertex { pos: *p, uv: [uv[0], 1.0 - uv[1]] }); // V-Flip } for (t, &m) in o.tris.iter().zip(&o.tri_mats) { let name = resolve(&model.materials[m]); let Some(ti) = tex_names.iter().position(|n| n == name) else { continue; }; per_tex[ti].extend(t.map(|i| base + i as u32)); } } // Pro-Textur-Indexlisten zu einem Buffer + Batches verflachen (wie brush). let mut indices = Vec::new(); let mut batches = Vec::new(); for (ti, list) in per_tex.into_iter().enumerate() { if list.is_empty() { continue; } batches.push(Batch { texture: ti, start: indices.len() as u32, count: list.len() as u32 }); indices.extend_from_slice(&list); } Mesh { verts, indices, batches } } #[cfg(test)] mod tests { use super::*; use crate::engine::model::{Model, Object, Props}; /// Ein Dreieck mit UVs; `visible` steuert, ob es gerendert werden darf. fn tri(name: &str, visible: bool) -> Object { Object { name: name.into(), verts: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]], uvs: vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]], tris: vec![[0, 1, 2]], tri_mats: vec![0], visible, collider: !visible, props: Props::new(), } } fn sample() -> Model { Model { objects: vec![tri("Ding", true), tri("Proxy", false)], empties: Vec::new(), materials: vec!["carpet".to_string()], } } #[test] fn builds_only_visible_objects_with_v_flip() { let m = sample(); let names = texture_names(&m); assert_eq!(names, vec!["carpet".to_string()]); let mesh = build(&m, &names); // Nur „Ding" (der Proxy ist unsichtbar): 3 Vertices, 1 Dreieck. assert_eq!(mesh.verts.len(), 3); assert_eq!(mesh.indices, vec![0, 1, 2]); assert_eq!(mesh.batches.len(), 1); // uv (1,1) → V-Flip → (1,0). assert_eq!(mesh.verts[2].uv, [1.0, 0.0]); } #[test] fn missing_material_maps_to_fallback() { let mut m = sample(); m.materials = vec![String::new()]; // Faces ohne Material assert_eq!(texture_names(&m), vec![FALLBACK_TEXTURE.to_string()]); let mesh = build(&m, &texture_names(&m)); assert_eq!(mesh.batches.len(), 1); } #[test] fn shares_texture_index_space_with_superset_list() { // Obermengen-Liste (wie beim Merge mit Brush-Texturen): der Batch // zeigt auf den richtigen Index in der geteilten Liste. let names = vec!["aaa".to_string(), "carpet".to_string(), "zzz".to_string()]; let mesh = build(&sample(), &names); assert_eq!(mesh.batches[0].texture, 1); } }