Collider via collide-Property + Quickhull, OBJ-Pfad entfernt

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>
This commit is contained in:
2026-07-16 19:14:42 +02:00
parent f7bfdb4a0b
commit 9db3929750
12 changed files with 371 additions and 2497 deletions
+36 -31
View File
@@ -1,18 +1,19 @@
//! Blender-Modell → Render-Geometrie.
//!
//! Gegenstück zu render::brush für den OBJ-Pfad: nimmt das neutrale
//! [`Model`] aus engine::obj und baut daraus ein [`Mesh`] (Vertices + nach
//! 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 `usemtl`-Materialname *ist* der Texturname — aufgelöst wie
//! 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: OBJ hat 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; reine
//! `col_*`-Proxies sind Sache von engine::collision.
//! 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};
@@ -21,7 +22,7 @@ use crate::render::scene::{Batch, Mesh, Vertex};
pub const FALLBACK_TEXTURE: &str = "placeholder";
/// Distinkte Texturnamen des Modells (sortiert), `""` bereits auf den
/// Fallback gemappt — dasselbe Kontrakt wie `brush::texture_names`.
/// 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())
@@ -69,43 +70,48 @@ pub fn build(model: &Model, tex_names: &[String]) -> Mesh {
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::obj;
use crate::engine::model::{Model, Object, Props};
const SAMPLE: &str = "\
o Ding
v 0 0 0
v 1 0 0
v 1 1 0
vt 0 0
vt 1 0
vt 1 1
usemtl carpet
f 1/1 2/2 3/3
o col_Proxy
v 5 0 0
v 6 0 0
v 6 1 0
f 4 5 6
";
/// 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 = obj::parse(SAMPLE);
let m = sample();
let names = texture_names(&m);
assert_eq!(names, vec!["carpet".to_string()]);
let mesh = build(&m, &names);
// Nur „Ding" (col_Proxy ist unsichtbar): 3 Vertices, 1 Dreieck.
// 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);
// vt (1,1) → V-Flip → uv (1,0).
// 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 m = obj::parse("o A\nv 0 0 0\nv 1 0 0\nv 1 1 0\nf 1 2 3\n");
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);
@@ -115,9 +121,8 @@ f 4 5 6
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 m = obj::parse(SAMPLE);
let names = vec!["aaa".to_string(), "carpet".to_string(), "zzz".to_string()];
let mesh = build(&m, &names);
let mesh = build(&sample(), &names);
assert_eq!(mesh.batches[0].texture, 1);
}
}