Welt-Texturen aus der .glb statt aus assets/textures/

Der Loader zieht jetzt die baseColorTexture jedes Materials aus dem
BIN-Chunk und dekodiert sie (PNG, via png-Crate) → model::Texture mit
Pixeln. Die Namenskonvention "Materialname = Dateiname" und der
assets/textures/-Pfad für Welt-Texturen entfallen; assets/textures/
bleibt nur noch für UI/Fonts/Platzhalter.

- engine::image: neuer, geteilter RGBA8-Image-Typ (aus tga.rs gezogen)
  plus PNG-Decoder. tga.rs liefert weiter denselben Typ.
- gltf: material -> texture -> image -> bufferView auflösen, Bild aus
  BIN dekodieren; fehlt ein Glied, bleibt image None (Platzhalter).
  KHR_texture_transform (Blenders Mapping-Node) wird beim Laden in die
  UVs eingebacken.
- model::Model: materials: Vec<String> -> textures: Vec<Texture>.
- render: die Pixel je Schlüssel aus den Modellen sammeln (erstes
  Vorkommen gewinnt, Schlüssel ohne Bild -> Platzhalter).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0196AM4UyAJEXVtPvp5DdKr8
This commit is contained in:
irrlicht
2026-09-02 00:29:46 +02:00
co-authored by Claude Sonnet 5
parent 937fbd6ede
commit fc3494466e
19 changed files with 495 additions and 90 deletions
+24 -17
View File
@@ -29,8 +29,8 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::{CursorGrabMode, Window, WindowId};
use crate::engine::image::Image;
use crate::engine::player;
use crate::engine::tga::Image;
use crate::engine::{assets, gltf, model, tga, wav};
use crate::session::{FrameInput, Mode, Session};
use gpu::Gpu;
@@ -58,13 +58,26 @@ pub fn run(mut session: Session, maps_dir: &str) {
// `Gpu` — die beiden bleiben getrennt.
let models = load_models(maps_dir);
// Eine geteilte Texturliste über alle Modelle, damit ihre Meshes
// denselben Index-Raum benutzen und zu einem Buffer verschmelzen können.
// Eine geteilte Textur-Schlüsselliste über alle Modelle, damit ihre
// Meshes denselben Index-Raum benutzen und zu einem Buffer verschmelzen.
let mut tex_names: Vec<String> = Vec::new();
for (_, m) in &models { tex_names.extend(props::texture_names(m)); }
for (_, m) in &models { tex_names.extend(props::texture_keys(m)); }
tex_names.sort_unstable();
tex_names.dedup();
let images: Vec<Image> = tex_names.iter().map(|n| load_texture(n)).collect();
// Die Pixel liegen in den `.glb` selbst; gleicher Schlüssel = eine Textur
// (erstes Vorkommen gewinnt). Schlüssel ohne Bild treffen den Platzhalter
// (Projekt-Asset, darf hart scheitern).
let mut by_key: HashMap<&str, &Image> = HashMap::new();
for (_, m) in &models {
for t in &m.textures {
if let Some(img) = &t.image { by_key.entry(props::texture_key(t)).or_insert(img); }
}
}
let placeholder = placeholder();
let images: Vec<Image> = tex_names.iter()
.map(|k| by_key.get(k.as_str()).map_or_else(|| placeholder.clone(), |i| (*i).clone()))
.collect();
let mut mesh = Mesh::default();
for (_, m) in &models { mesh.append(props::build(m, &tex_names)); }
@@ -186,18 +199,12 @@ fn load_sound(name: &str) -> Option<audio::Sound> {
}
}
/// Welt-Textur laden. Autorinnen-Content: fehlt oder klemmt die Datei, wird
/// gemeldet und der Platzhalter genommen — der selbst ist Projekt-Asset und
/// darf hart scheitern.
fn load_texture(name: &str) -> Image {
match tga::load(&assets::path(&format!("assets/textures/{name}.tga"))) {
Ok(img) => img,
Err(e) => {
eprintln!("[tex] {e} — Platzhalter");
tga::load(&assets::path(&format!("assets/textures/{}.tga", props::FALLBACK_TEXTURE)))
.expect("Platzhalter-Textur (Projekt-Asset)")
}
}
/// Die Platzhalter-Textur für Schlüssel ohne eigenes Bild in der `.glb`.
/// Projekt-Asset: fehlt sie, ist die Installation kaputt und es darf scheitern.
fn placeholder() -> Image {
let name = props::FALLBACK_TEXTURE;
tga::load(&assets::path(&format!("assets/textures/{name}.tga")))
.expect("Platzhalter-Textur (Projekt-Asset)")
}
/// Aktueller Eingabezustand. Tasten als gehaltene Flags (nicht Events),