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,128 @@
|
||||
//! Neutrales 3D-Modell — das gemeinsame Ergebnis der Blender-Loader
|
||||
//! (engine::obj, engine::gltf). Konsumenten (render::props, collision)
|
||||
//! kennen nur diese Struktur; welcher Parser sie erzeugt hat, ist ihnen
|
||||
//! egal — so bleibt ein Format-Wechsel ein lokaler Loader-Tausch.
|
||||
//!
|
||||
//! Naming-Konvention (Objekt-Präfix, case-insensitiv):
|
||||
//! `col_*` → unsichtbar, collidierbar (Collision-Proxy, konvex!)
|
||||
//! sonst → nur sichtbar (`vis_*` sagt dasselbe explizit)
|
||||
//!
|
||||
//! Anders als irl3d („ohne Präfix = beides"): dessen Physik kollidierte per
|
||||
//! Dreieck, unsere per konvexem Ebenen-Schnitt — konkave Sichtgeometrie als
|
||||
//! Default-Collider ergäbe hier nur ein sinnloses Hüll-Volumen. Kollision
|
||||
//! ist deshalb immer ein bewusst gebauter `col_*`-Proxy.
|
||||
//!
|
||||
//! Objektnamen sind zugleich die künftigen Interact-Keys: Blender-Suffixe
|
||||
//! (`Thing.001`) strippt `signals::signal_key` beim Dispatch, nicht hier —
|
||||
//! die Loader bleiben frei von Signal-Wissen.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Blender-Custom-Properties eines Objekts/Empties (glTF-`extras`),
|
||||
/// Werte zu Strings vereinheitlicht. OBJ kann keine tragen → leer.
|
||||
pub type Props = HashMap<String, String>;
|
||||
|
||||
pub struct Model {
|
||||
pub objects: Vec<Object>,
|
||||
/// Nodes ohne Mesh (Blender-Empties) — der Entity-Kanal: Marker für
|
||||
/// Spawn-Punkte, Trigger o.Ä., Bedeutung geben künftige Konsumenten.
|
||||
pub empties: Vec<Empty>,
|
||||
/// Distinkte Materialnamen in Auftrittsreihenfolge; `Object::tri_mats`
|
||||
/// indiziert hierhin. `""` steht für „Faces ohne Material" — der
|
||||
/// Konsument wählt dafür seinen Fallback (z.B. Platzhalter-Textur).
|
||||
pub materials: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct Object {
|
||||
pub name: String,
|
||||
/// Positionen in Engine-Koords (Y-up, Blick −Z), Welt-Raum (Transforms
|
||||
/// vom Loader eingebacken), Maßstab 1:1 (Blender-Meter = Engine-Unit).
|
||||
pub verts: Vec<[f32; 3]>,
|
||||
/// UVs parallel zu `verts`, V-Ursprung unten links (OBJ-/glTF-Rohform;
|
||||
/// den Flip auf Bild-Konvention macht der Render-Konsument).
|
||||
pub uvs: Vec<[f32; 2]>,
|
||||
pub tris: Vec<[usize; 3]>,
|
||||
/// Material-Index je Dreieck, parallel zu `tris` (→ `Model::materials`).
|
||||
pub tri_mats: Vec<usize>,
|
||||
pub visible: bool,
|
||||
pub collider: bool,
|
||||
pub props: Props,
|
||||
}
|
||||
|
||||
pub struct Empty {
|
||||
pub name: String,
|
||||
/// Welt-Position (Engine-Koords).
|
||||
pub pos: [f32; 3],
|
||||
pub props: Props,
|
||||
}
|
||||
|
||||
/// Präfix-Klassifikation (case-insensitiv): `col_` = nur Collider, alles
|
||||
/// andere nur sichtbar (siehe Modul-Doc, Abweichung von irl3d).
|
||||
pub fn classify(name: &str) -> (bool, bool) {
|
||||
let is_col = name.get(..4).is_some_and(|p| p.eq_ignore_ascii_case("col_"));
|
||||
(!is_col, is_col)
|
||||
}
|
||||
|
||||
/// Custom-Property-Schlüssel, der ein Objekt zum Trigger macht (Wert =
|
||||
/// Signal-Name für `signals::dispatch`). Die Collider-Frage entscheidet die
|
||||
/// Semantik — anfassbar = klickbar, durchlaufbar = Betretens-Zone:
|
||||
/// `col_*` + `signal` → solide **und** klickbar (Point-and-Click)
|
||||
/// sonst + `signal` → unsichtbare Trigger-Zone, feuert beim Betreten
|
||||
/// (einmal je Eintritt; erneut erst nach Verlassen)
|
||||
pub const SIGNAL_PROP: &str = "signal";
|
||||
|
||||
/// Die `signal`-Regel anwenden (nach `classify`, wenn `props` bekannt sind):
|
||||
/// eine Betretens-Zone ist ein reines Trigger-Volumen und wird nie gerendert.
|
||||
pub fn apply_signal_rule(o: &mut Object) {
|
||||
if !o.collider && o.props.contains_key(SIGNAL_PROP) {
|
||||
o.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Test-Helfer: Einheitswürfel-Objekt bei `min..min+1` (Quads CCW von außen,
|
||||
/// Fan-trianguliert wie die Loader) mit optionaler `signal`-Property —
|
||||
/// geteilt von trigger-/session-Tests.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_cube(name: &str, min: [f32; 3], signal: Option<&str>) -> Object {
|
||||
let v = |dx: f32, dy: f32, dz: f32| [min[0] + dx, min[1] + dy, min[2] + dz];
|
||||
let verts = vec![
|
||||
v(0.0, 0.0, 0.0), v(1.0, 0.0, 0.0), v(1.0, 1.0, 0.0), v(0.0, 1.0, 0.0),
|
||||
v(0.0, 0.0, 1.0), v(1.0, 0.0, 1.0), v(1.0, 1.0, 1.0), v(0.0, 1.0, 1.0),
|
||||
];
|
||||
let quads = [
|
||||
[4, 5, 6, 7], [0, 3, 2, 1], [1, 2, 6, 5],
|
||||
[0, 4, 7, 3], [3, 7, 6, 2], [0, 1, 5, 4],
|
||||
];
|
||||
let mut tris = Vec::new();
|
||||
for q in quads {
|
||||
tris.push([q[0], q[1], q[2]]);
|
||||
tris.push([q[0], q[2], q[3]]);
|
||||
}
|
||||
let n = tris.len();
|
||||
let (visible, collider) = classify(name);
|
||||
let mut props = Props::new();
|
||||
if let Some(s) = signal { props.insert(SIGNAL_PROP.into(), s.into()); }
|
||||
let mut o = Object {
|
||||
name: name.into(),
|
||||
uvs: vec![[0.0, 0.0]; verts.len()],
|
||||
verts, tris,
|
||||
tri_mats: vec![0; n],
|
||||
visible, collider, props,
|
||||
};
|
||||
apply_signal_rule(&mut o);
|
||||
o
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn classify_is_prefix_and_case_insensitive() {
|
||||
assert_eq!(classify("col_Wand"), (false, true));
|
||||
assert_eq!(classify("COL_Wand"), (false, true));
|
||||
assert_eq!(classify("vis_Deko"), (true, false));
|
||||
assert_eq!(classify("Suzanne"), (true, false));
|
||||
assert_eq!(classify(""), (true, false));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user