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:
+11
-15
@@ -36,7 +36,7 @@ use winit::window::{CursorGrabMode, Window, WindowId};
|
||||
|
||||
use crate::engine::player;
|
||||
use crate::engine::tga::Image;
|
||||
use crate::engine::{assets, gltf, map, model, obj, tga};
|
||||
use crate::engine::{assets, gltf, map, model, tga};
|
||||
use crate::session::{FrameInput, Mode, Session};
|
||||
use camera::Camera;
|
||||
use gpu::Gpu;
|
||||
@@ -64,7 +64,7 @@ pub fn run(mut session: Session) {
|
||||
// auf die GPU macht später `Gpu` — Decode (CPU) und Upload (GPU) bleiben
|
||||
// getrennt.
|
||||
let world = map::load(&assets::path("assets/maps/test.map"));
|
||||
// Blender-Modelle: alle .glb/.obj unter assets/maps/props/. Autorinnen-
|
||||
// Blender-Modelle: alle .glb unter assets/maps/props/. Autorinnen-
|
||||
// Content — ein kaputtes File wird gemeldet und übersprungen, nie fatal.
|
||||
let models = load_models(&assets::path("assets/maps/props"));
|
||||
|
||||
@@ -81,9 +81,9 @@ pub fn run(mut session: Session) {
|
||||
for (_, m) in &models { mesh.append(props::build(m, &tex_names)); }
|
||||
report_map(&world, &tex_names, &mesh);
|
||||
|
||||
// Sim-Welt (Collision-Brushes + Spawn, dann col_*-Proxies der Modelle) in
|
||||
// die Session einspielen — die Simulation gehört der Session, nicht dem
|
||||
// Render-Frontend.
|
||||
// Sim-Welt (Collision-Brushes + Spawn, dann Collider/Trigger der
|
||||
// Modelle) in die Session einspielen — die Simulation gehört der
|
||||
// Session, nicht dem Render-Frontend.
|
||||
session.load_world(&world);
|
||||
for (_, m) in &models { session.load_props(m); }
|
||||
|
||||
@@ -134,25 +134,21 @@ pub fn run(mut session: Session) {
|
||||
event_loop.run_app(&mut app).expect("winit: run");
|
||||
}
|
||||
|
||||
/// Alle Blender-Modelle (`.glb` und `.obj`) eines Verzeichnisses laden
|
||||
/// (sortiert → deterministische Reihenfolge). Fehlende Verzeichnisse sind
|
||||
/// kein Fehler (kein Props-Ordner = keine Props); kaputte Dateien werden
|
||||
/// gemeldet und übersprungen.
|
||||
/// Alle Blender-Modelle (`.glb`) eines Verzeichnisses laden (sortiert →
|
||||
/// deterministische Reihenfolge). Fehlende Verzeichnisse sind kein Fehler
|
||||
/// (kein Props-Ordner = keine Props); kaputte Dateien werden gemeldet und
|
||||
/// übersprungen.
|
||||
fn load_models(dir: &str) -> Vec<(String, model::Model)> {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else { return Vec::new(); };
|
||||
let mut paths: Vec<std::path::PathBuf> = entries.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| matches!(p.extension().and_then(|s| s.to_str()), Some("obj" | "glb")))
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("glb"))
|
||||
.collect();
|
||||
paths.sort();
|
||||
let mut models = Vec::new();
|
||||
for p in paths {
|
||||
let path = p.to_string_lossy();
|
||||
let loaded = match p.extension().and_then(|s| s.to_str()) {
|
||||
Some("glb") => gltf::load(&path),
|
||||
_ => obj::load(&path),
|
||||
};
|
||||
match loaded {
|
||||
match gltf::load(&path) {
|
||||
Ok(m) => {
|
||||
let name = p.file_stem().map_or_else(String::new, |s| s.to_string_lossy().into_owned());
|
||||
let tris: usize = m.objects.iter().map(|o| o.tris.len()).sum();
|
||||
|
||||
+36
-31
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user