9db3929750
col_*-Namenspräfix raus: Rollen steuern nur noch Custom Properties (collide=1 sichtbar+Collider, "proxy" unsichtbar, signal wie gehabt). Collider sind die konvexe Hülle der Vertices — konkave Meshes erlaubt. glb ist der einzige Modell-Pfad; suzanne.obj → suzanne.glb. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
129 lines
4.8 KiB
Rust
129 lines
4.8 KiB
Rust
//! 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<String> {
|
|
let mut names: Vec<String> = 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<u32>> = 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);
|
|
}
|
|
}
|