diff --git a/assets/textures/fonts/oldschool-cga-8x8.png b/assets/textures/fonts/oldschool-cga-8x8.png new file mode 100644 index 0000000..0ebc4f0 Binary files /dev/null and b/assets/textures/fonts/oldschool-cga-8x8.png differ diff --git a/assets/textures/fonts/oldschool-cga-8x8.tga b/assets/textures/fonts/oldschool-cga-8x8.tga deleted file mode 100644 index d342b01..0000000 Binary files a/assets/textures/fonts/oldschool-cga-8x8.tga and /dev/null differ diff --git a/assets/textures/fonts/oldschool-ega-8x14.png b/assets/textures/fonts/oldschool-ega-8x14.png new file mode 100644 index 0000000..eff3394 Binary files /dev/null and b/assets/textures/fonts/oldschool-ega-8x14.png differ diff --git a/assets/textures/fonts/oldschool-ega-8x14.tga b/assets/textures/fonts/oldschool-ega-8x14.tga deleted file mode 100644 index b66fd00..0000000 Binary files a/assets/textures/fonts/oldschool-ega-8x14.tga and /dev/null differ diff --git a/assets/textures/placeholder.png b/assets/textures/placeholder.png new file mode 100644 index 0000000..bfd4be3 Binary files /dev/null and b/assets/textures/placeholder.png differ diff --git a/assets/textures/placeholder.tga b/assets/textures/placeholder.tga deleted file mode 100644 index a9e2a1a..0000000 Binary files a/assets/textures/placeholder.tga and /dev/null differ diff --git a/assets/textures/ui/cursors.png b/assets/textures/ui/cursors.png new file mode 100644 index 0000000..132d274 Binary files /dev/null and b/assets/textures/ui/cursors.png differ diff --git a/assets/textures/ui/cursors.tga b/assets/textures/ui/cursors.tga deleted file mode 100644 index ceeb5dc..0000000 Binary files a/assets/textures/ui/cursors.tga and /dev/null differ diff --git a/assets/textures/ui/panel_ornaments.png b/assets/textures/ui/panel_ornaments.png new file mode 100644 index 0000000..192b5a2 Binary files /dev/null and b/assets/textures/ui/panel_ornaments.png differ diff --git a/assets/textures/ui/panel_ornaments.tga b/assets/textures/ui/panel_ornaments.tga deleted file mode 100644 index a8fa494..0000000 Binary files a/assets/textures/ui/panel_ornaments.tga and /dev/null differ diff --git a/notes/artist.md b/notes/artist.md index e11c487..9bb776f 100644 --- a/notes/artist.md +++ b/notes/artist.md @@ -10,13 +10,14 @@ Start in der Konsole gemeldet (`[model]`, `[audio]`, `[tex]`, …). - **Format:** glTF Binary (`.glb`) nach `assets/maps/props/` — alle Dateien dort werden beim Start geladen. -- **Beim Export anhaken:** „Include → Custom Properties" (sonst kommen - keine der Properties unten an!). +- **Beim Export anhaken:** „Include → Custom Properties" (sonst fehlen die + Properties unten) **und** „Include → Textures" (sonst fehlen die Texturen). - **Maßstab:** 1 Blender-Meter = 1 Engine-Meter. Augenhöhe 1,6 m, Türen/Durchgänge ≥ 2 m hoch und ≥ 0,8 m breit denken. -- **Material-Name = Textur-Name:** Material `holz` → `assets/textures/holz.tga`. - Fehlt die Textur (oder das Material), erscheint die Platzhalter-Textur. -- **Texturen:** TGA, unkomprimiert oder RLE, 24/32 bit. +- **Texturen:** Bild im Material als Base Color zuweisen — wird beim Export + in die `.glb` eingebettet (PNG), kein `assets/textures/` mehr. Fehlt das + Bild, erscheint die Platzhalter-Textur. Kacheln/Skalieren über einen + Mapping-Node (`KHR_texture_transform` wird beim Laden in die UVs gebacken). ### Custom Properties auf Mesh-Objekten diff --git a/src/engine/image.rs b/src/engine/image.rs index fdc6755..eb31c2e 100644 --- a/src/engine/image.rs +++ b/src/engine/image.rs @@ -4,8 +4,8 @@ //! UV-Konvention) selbst. //! //! [`decode`] liest die Bilder, die Blender beim glTF-Export in die `.glb` -//! packt (PNG). TGA-Dateien (UI, Fonts) laufen über [`super::tga`] und liefern -//! denselben `Image`-Typ. +//! packt; [`load`] dieselbe Sorte Bild von der Platte (UI, Fonts, +//! Platzhalter). Beides PNG — ein Decoder für alles. #[derive(Clone)] pub struct Image { @@ -15,6 +15,12 @@ pub struct Image { pub rgba: Vec, } +/// Eine PNG-Datei von der Platte laden. +pub fn load(path: &str) -> Result { + let bytes = std::fs::read(path).map_err(|e| format!("image load {path}: {e}"))?; + decode(&bytes, "").map_err(|e| format!("image {path}: {e}")) +} + /// Ein eingebettetes glTF-Bild dekodieren. `mime` ist der glTF-`mimeType` /// (darf leer sein — dann entscheidet die Datei-Signatur). Unterstützt wird /// nur PNG; JPEG/WebP werden gemeldet, nicht dekodiert. @@ -94,4 +100,19 @@ mod tests { assert!(decode(b"\xff\xd8\xff\xe0junk", "image/jpeg").is_err()); assert!(decode(b"whatever", "").is_err()); } + + #[test] + fn loads_the_real_project_assets() { + for (path, w, h) in [ + ("assets/textures/placeholder.png", 256, 256), + ("assets/textures/fonts/oldschool-ega-8x14.png", 256, 112), + ("assets/textures/fonts/oldschool-cga-8x8.png", 256, 64), + ("assets/textures/ui/cursors.png", 32, 32), + ("assets/textures/ui/panel_ornaments.png", 32, 32), + ] { + let img = load(path).unwrap_or_else(|e| panic!("{path}: {e}")); + assert_eq!((img.width, img.height), (w, h), "{path}"); + assert_eq!(img.rgba.len(), (w * h * 4) as usize, "{path}"); + } + } } diff --git a/src/engine/mod.rs b/src/engine/mod.rs index eee0a99..3f47ffd 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -14,11 +14,12 @@ //! so wie story_ctrl Tags zurückgibt, statt selbst `signals::dispatch` zu //! rufen. //! -//! `gltf`, `tga`, `image` und `wav` sind reine Decoder (Bytes → owned Daten); +//! `gltf`, `image` und `wav` sind reine Decoder (Bytes → owned Daten); //! `gltf` ist DER 3D-Pfad und produziert das `model::Model`, den Vertrag //! zwischen Loader und seinen Konsumenten (collision, trigger, audio, -//! render::props) — samt der Welt-Texturen, die als PNG in der `.glb` liegen -//! (dekodiert über `image`, das den RGBA8-`Image`-Typ auch für `tga` stellt). +//! render::props) — samt der Welt-Texturen, die als PNG in der `.glb` liegen. +//! `image` dekodiert PNG (eingebettet wie von der Platte) und stellt den +//! RGBA8-`Image`-Typ für alle. //! `audio` ist die Emitter-Logik zum Frontend-Treiber render::audio, `player` //! die First-Person-Physik auf der Welt aus `collision`. Post-Effekte kennt //! der Kern nur als `game::Action::SetPost` — die Kette lebt in render::post. @@ -35,6 +36,5 @@ pub mod model; pub mod player; pub mod signals; pub mod story_ctrl; -pub mod tga; pub mod trigger; pub mod wav; diff --git a/src/engine/model.rs b/src/engine/model.rs index ec70c28..49d4802 100644 --- a/src/engine/model.rs +++ b/src/engine/model.rs @@ -61,7 +61,7 @@ pub struct Object { /// vom Loader eingebacken), Maßstab 1:1 (Blender-Meter = Engine-Unit). pub verts: Vec<[f32; 3]>, /// UVs parallel zu `verts`, V-Ursprung oben links — glTF- und zugleich - /// `tga::Image`-Konvention, direkt sampelbar ohne Flip. + /// `image::Image`-Konvention, direkt sampelbar ohne Flip. pub uvs: Vec<[f32; 2]>, /// Normalen parallel zu `verts`, normiert, Welt-Raum. Fehlen sie im /// Modell, rechnet der Loader sie aus den Flächen. diff --git a/src/engine/tga.rs b/src/engine/tga.rs deleted file mode 100644 index 081e54a..0000000 --- a/src/engine/tga.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! TGA-Decoder → fertig dekodiertes RGBA8-[`Image`] (der Typ lebt in -//! [`super::image`], geteilt mit dem PNG-Pfad). -//! -//! Neutral: kennt weder GPU noch UI noch Fonts. Liefert *immer* RGBA8, -//! Ursprung oben-links, vollständig konvertiert — der Aufrufer macht keine -//! Nachbearbeitung, nur das Sampling (und damit die UV-Konvention) gehört ihm. -//! Zuständig für UI- und Font-Assets; Welt-Texturen kommen als PNG aus der -//! `.glb` (siehe [`super::gltf`]). -//! -//! Unterstützt True-Color unkomprimiert (Typ 2) und RLE (Typ 10) mit 24 -//! oder 32 bpp, sowie Graustufen (Typ 3/11, 8 bpp → zu RGBA expandiert) für -//! Font-Maps. Paletten-TGAs gibt es nicht mehr (wds ist 15-bit True-Color). - -use crate::engine::image::Image; - -pub fn load(path: &str) -> Result { - let bytes = std::fs::read(path).map_err(|e| format!("tga load {path}: {e}"))?; - decode(&bytes).map_err(|e| format!("tga {path}: {e}")) -} - -pub fn decode(d: &[u8]) -> Result { - if d.len() < 18 { return Err("Header zu kurz".into()); } - let id_len = d[0] as usize; - let cmap_type = d[1]; - let image_type = d[2]; - let width = u16::from_le_bytes([d[12], d[13]]) as u32; - let height = u16::from_le_bytes([d[14], d[15]]) as u32; - let depth = d[16]; - let descriptor = d[17]; - - if cmap_type != 0 { - return Err("Color-Map-TGAs nicht unterstützt (True-Color only)".into()); - } - // Pixeldaten beginnen nach Header + ID-Feld (Color-Map ist leer). - let off = 18 + id_len; - - let channels = match (image_type, depth) { - (2 | 10, 24) => 3, - (2 | 10, 32) => 4, - (3 | 11, 8) => 1, - (t, b) => return Err(format!("nicht unterstützt: Typ {t}, {b} bpp")), - }; - let n = (width as usize) * (height as usize); - if n == 0 { return Err("Nullgröße".into()); } - - // Rohpixel (channels Bytes je Pixel), bei RLE entpackt. - let raw_len = n * channels; - let raw: Vec = match image_type { - 2 | 3 => d.get(off..off + raw_len).ok_or("Pixeldaten zu kurz")?.to_vec(), - 10 | 11 => decode_rle(d.get(off..).ok_or("RLE-Daten fehlen")?, raw_len, channels)?, - _ => unreachable!(), - }; - - // → RGBA8. True-Color liegt im TGA als BGR(A) vor; Graustufen replizieren. - let mut rgba = vec![0u8; n * 4]; - for i in 0..n { - let (r, g, b, a) = match channels { - 1 => { let v = raw[i]; (v, v, v, 255) } - 3 => (raw[i * 3 + 2], raw[i * 3 + 1], raw[i * 3], 255), - 4 => (raw[i * 4 + 2], raw[i * 4 + 1], raw[i * 4], raw[i * 4 + 3]), - _ => unreachable!(), - }; - let o = i * 4; - rgba[o] = r; rgba[o + 1] = g; rgba[o + 2] = b; rgba[o + 3] = a; - } - - // Ursprung oben-links erzwingen. Descriptor-Bit 5 gesetzt = top-origin. - if (descriptor >> 5) & 1 == 0 { - flip_vertical(&mut rgba, width as usize, height as usize); - } - Ok(Image { width, height, rgba }) -} - -/// TGA-RLE: Pakete operieren auf ganzen Pixeln (`ch` Bytes). -fn decode_rle(src: &[u8], raw_len: usize, ch: usize) -> Result, String> { - let mut out = Vec::with_capacity(raw_len); - let mut si = 0; - while out.len() < raw_len { - let header = *src.get(si).ok_or("RLE: Quelle zu kurz")?; - si += 1; - let count = (header & 0x7f) as usize + 1; - if header & 0x80 != 0 { - // RLE-Paket: ein Pixel count-mal. - let px = src.get(si..si + ch).ok_or("RLE: Pixel zu kurz")?; - si += ch; - for _ in 0..count { out.extend_from_slice(px); } - } else { - // Raw-Paket: count Pixel am Stück. - let bytes = count * ch; - let chunk = src.get(si..si + bytes).ok_or("RLE: Chunk zu kurz")?; - si += bytes; - out.extend_from_slice(chunk); - } - } - out.truncate(raw_len); - Ok(out) -} - -fn flip_vertical(rgba: &mut [u8], w: usize, h: usize) { - let row = w * 4; - for y in 0..h / 2 { - let (a, b) = (y * row, (h - 1 - y) * row); - for x in 0..row { rgba.swap(a + x, b + x); } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn bgr_to_rgba_uncompressed() { - // Minimal-TGA: Typ 2, 2×1, 24 bpp, bottom-origin (descriptor 0). - // Zwei Pixel BGR (10,20,30) und (40,50,60). - let mut d = vec![0u8; 18]; - d[2] = 2; d[12] = 2; d[14] = 1; d[16] = 24; - d.extend_from_slice(&[10, 20, 30, 40, 50, 60]); - let img = decode(&d).unwrap(); - assert_eq!((img.width, img.height), (2, 1)); - assert_eq!(&img.rgba[0..4], &[30, 20, 10, 255]); - assert_eq!(&img.rgba[4..8], &[60, 50, 40, 255]); - } - - #[test] - fn rle_truecolor_expands() { - // Typ 10, 4×1, 24 bpp: ein RLE-Paket (4× BGR 1,2,3). - let mut d = vec![0u8; 18]; - d[2] = 10; d[12] = 4; d[14] = 1; d[16] = 24; - d.push(0x80 | 3); // RLE, count = 4 - d.extend_from_slice(&[1, 2, 3]); // ein Pixel - let img = decode(&d).unwrap(); - assert_eq!(img.rgba.len(), 4 * 4); - assert!(img.rgba.chunks(4).all(|p| p == [3, 2, 1, 255])); - } - - #[test] - fn decodes_real_placeholder() { - let bytes = std::fs::read("assets/textures/placeholder.tga").unwrap(); - let img = decode(&bytes).unwrap(); - assert_eq!((img.width, img.height), (256, 256)); - assert_eq!(img.rgba.len(), 256 * 256 * 4); - assert!(img.rgba.chunks(4).all(|p| p[3] == 255)); // 24bpp → Alpha 255 - } -} diff --git a/src/render/font.rs b/src/render/font.rs index 1da1a58..b455cef 100644 --- a/src/render/font.rs +++ b/src/render/font.rs @@ -47,11 +47,11 @@ pub(crate) struct Fonts { } impl Font { - /// EGA 8×14 (`assets/textures/fonts/oldschool-ega-8x14.tga`, 256×112). + /// EGA 8×14 (`assets/textures/fonts/oldschool-ega-8x14.png`, 256×112). pub(crate) fn ega(tex: usize, atlas: &Image) -> Font { Font::measure(8.0, 14.0, 256.0, 112.0, tex, atlas) } - /// CGA 8×8 (`assets/textures/fonts/oldschool-cga-8x8.tga`, 256×64). + /// CGA 8×8 (`assets/textures/fonts/oldschool-cga-8x8.png`, 256×64). pub(crate) fn cga(tex: usize, atlas: &Image) -> Font { Font::measure(8.0, 8.0, 256.0, 64.0, tex, atlas) } diff --git a/src/render/mod.rs b/src/render/mod.rs index b66cca6..7f3d5cd 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -29,9 +29,9 @@ 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::image::{self, Image}; use crate::engine::player; -use crate::engine::{assets, gltf, model, tga, wav}; +use crate::engine::{assets, gltf, model, wav}; use crate::session::{FrameInput, Mode, Session}; use gpu::Gpu; use math::Mat4; @@ -106,12 +106,12 @@ pub fn run(mut session: Session, maps_dir: &str) { // Autorinnen-Content sind sie Projekt-Assets: fehlen sie, ist die // Installation kaputt und es darf scheitern. let load_keyed = |p: &str| { - ui::key_luminance(&tga::load(&assets::path(p)).expect("UI-Textur (Projekt-Asset)")) + ui::key_luminance(&image::load(&assets::path(p)).expect("UI-Textur (Projekt-Asset)")) }; // Font-Atlanten zuerst keyen, daraus die Glyph-Breiten messen (Font), // dann dieselben Bilder in die GPU-Texturliste übernehmen. - let ega_img = load_keyed("assets/textures/fonts/oldschool-ega-8x14.tga"); - let cga_img = load_keyed("assets/textures/fonts/oldschool-cga-8x8.tga"); + let ega_img = load_keyed("assets/textures/fonts/oldschool-ega-8x14.png"); + let cga_img = load_keyed("assets/textures/fonts/oldschool-cga-8x8.png"); let fonts = font::Fonts { ega: font::Font::ega(ui::FONT_EGA, &ega_img), cga: font::Font::cga(ui::FONT_CGA, &cga_img), @@ -120,8 +120,8 @@ pub fn run(mut session: Session, maps_dir: &str) { ui::white_pixel(), ega_img, cga_img, - load_keyed("assets/textures/ui/cursors.tga"), - load_keyed("assets/textures/ui/panel_ornaments.tga"), + load_keyed("assets/textures/ui/cursors.png"), + load_keyed("assets/textures/ui/panel_ornaments.png"), ]; // stdin auf einem eigenen Thread: er darf blockieren, der Main-Thread @@ -203,7 +203,7 @@ fn load_sound(name: &str) -> Option { /// 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"))) + image::load(&assets::path(&format!("assets/textures/{name}.png"))) .expect("Platzhalter-Textur (Projekt-Asset)") } diff --git a/src/render/props.rs b/src/render/props.rs index 6a0f2fc..38d0ad4 100644 --- a/src/render/props.rs +++ b/src/render/props.rs @@ -19,7 +19,7 @@ use crate::engine::model::{Model, Texture}; use crate::render::scene::{Batch, Mesh, Vertex}; /// Schlüssel für Texturen ohne eigenes Bild; der Aufrufer lädt die Pixel -/// dazu (`assets/textures/placeholder.tga`). +/// dazu (`assets/textures/placeholder.png`). pub const FALLBACK_TEXTURE: &str = "placeholder"; /// Distinkte Textur-Schlüssel des Modells (sortiert), bildlose bereits auf