Blender-Import-Pipeline: glTF/OBJ, col_*-Proxies, Trigger
Blender wird das Haupt-3D-Tool: zwei Loader (engine/gltf.rs für .glb als Hauptpfad, engine/obj.rs aus irl3d eingebettet) liefern ein neutrales engine/model.rs::Model. Loader sind reine Dekoder; das Frontend zieht sie und schiebt das Ergebnis per Session::load_props in den State, der Renderer baut sein Mesh (render/props.rs, verschmolzen mit der Brush-Welt über eine geteilte Texturliste). glTF trägt Blender-Custom-Properties (node.extras → Object::props) und Empties (Nodes ohne Mesh) als Entity-Kanal. Namenskonvention: `col_*` = unsichtbarer Collision-Proxy, aus dem collision::add_model konvexe Ebenen-Sets baut (konvex-only, daher kein Default-Collider auf Sichtgeometrie). Trigger (engine/trigger.rs) hängen an der Custom Property `signal`: mit Collider = Point-and-Click-Ziel (Session::pick raycastet aus der Bildmitte, Treffer läuft durch den `use <name>`-Trichter), ohne Collider = unsichtbare Betretens-Zone, die einmal je Eintritt feuert. Gefeuert wird nur über Session::exec — der Eingabe-Trichter bleibt der einzige Weg in den State. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
//! 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
|
||||
//! 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
|
||||
//! 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.
|
||||
|
||||
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 — dasselbe 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::obj;
|
||||
|
||||
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
|
||||
";
|
||||
|
||||
#[test]
|
||||
fn builds_only_visible_objects_with_v_flip() {
|
||||
let m = obj::parse(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.
|
||||
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).
|
||||
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");
|
||||
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 m = obj::parse(SAMPLE);
|
||||
let names = vec!["aaa".to_string(), "carpet".to_string(), "zzz".to_string()];
|
||||
let mesh = build(&m, &names);
|
||||
assert_eq!(mesh.batches[0].texture, 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user