idk shitload of stuff
This commit is contained in:
+234
-34
@@ -1,41 +1,88 @@
|
||||
//! Fenster-Frontend: winit-Loop um den wgpu-Renderer.
|
||||
//!
|
||||
//! Stand: Schritt 3 des Renderer-Plans (notes/renderer-plan.md) —
|
||||
//! internes 320×240-Target, PS1-Szenen-Pass (Pixel-Snap, affine
|
||||
//! Interpolation, RGB555+Bayer) am Testwürfel, Letterbox-Blit.
|
||||
//! Als Nächstes: Flycam (Schritt 4), dann OBJ-Szene (Schritt 5).
|
||||
//! Stand: Schritt 4 des Renderer-Plans (notes/renderer-plan.md) — Flycam
|
||||
//! über dem PS1-Szenen-Pass. WASD bewegt, Maus blickt (Klick fängt den
|
||||
//! Cursor, Escape gibt ihn frei bzw. beendet), Space/Shift hoch/runter.
|
||||
//! Als Nächstes: OBJ-Szene (Schritt 5).
|
||||
//!
|
||||
//! Steuert dieselbe [`Session`] wie die CLI: das Fenster *besitzt* sie,
|
||||
//! Terminal-Befehle kommen über einen Channel von einem stdin-Thread und
|
||||
//! werden pro Loop-Durchlauf eingespielt. Im Dialog-Modus pausiert die
|
||||
//! Welt (die Sim-Uhr läuft nicht weiter) — so sieht das Fenster nie einen
|
||||
//! Welt (Sim-Uhr und Kamera stehen) — so sieht das Fenster nie einen
|
||||
//! anderen Zustand als die Konsole.
|
||||
//!
|
||||
//! Schicht-Regel wie `cli`: Geschwister von `engine`/`session`, konsumiert
|
||||
//! deren Schnittstellen — nie umgekehrt.
|
||||
|
||||
mod brush;
|
||||
mod camera;
|
||||
mod font;
|
||||
mod gpu;
|
||||
mod math;
|
||||
mod scene;
|
||||
mod sprite;
|
||||
mod ui;
|
||||
|
||||
use std::sync::mpsc::{self, Receiver};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::event::{ElementState, KeyEvent, WindowEvent};
|
||||
use winit::event::{DeviceEvent, DeviceId, ElementState, KeyEvent, MouseButton, WindowEvent};
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||
use winit::keyboard::{KeyCode, PhysicalKey};
|
||||
use winit::window::{Window, WindowId};
|
||||
use winit::window::{CursorGrabMode, Window, WindowId};
|
||||
|
||||
use crate::engine::tga::Image;
|
||||
use crate::engine::{assets, map, tga};
|
||||
use crate::session::{Mode, Session};
|
||||
use camera::Camera;
|
||||
use gpu::Gpu;
|
||||
use scene::Mesh;
|
||||
|
||||
/// Radiant pro Maus-Pixel.
|
||||
const MOUSE_SENS: f32 = 0.0025;
|
||||
/// Welt-Einheiten pro Sekunde.
|
||||
const MOVE_SPEED: f32 = 4.0;
|
||||
|
||||
pub fn run(mut session: Session) {
|
||||
// Init-Signal feuern, bevor das Fenster steht (kann bereits einen
|
||||
// Dialog öffnen — dann startet die Welt eben pausiert).
|
||||
for line in session.start() { println!("{line}"); }
|
||||
|
||||
// CPU-Assets dekodieren (Pull über die engine-Decoder). Das Hochladen
|
||||
// auf die GPU macht später `Gpu` — Decode (CPU) und Upload (GPU) bleiben
|
||||
// getrennt.
|
||||
let world = map::load(&assets::path("assets/maps/test.map"));
|
||||
let tex_names = brush::texture_names(&world);
|
||||
let images: Vec<Image> = tex_names.iter()
|
||||
.map(|n| tga::load(&assets::path(&format!("assets/textures/{n}.tga"))))
|
||||
.collect();
|
||||
let dims: Vec<(u32, u32)> = images.iter().map(|i| (i.width, i.height)).collect();
|
||||
let mesh = brush::build(&world, &tex_names, &dims);
|
||||
report_map(&world, &tex_names, &mesh);
|
||||
|
||||
// UI-Texturen, Reihenfolge = die Index-Konstanten in ui (WHITE, FONT_EGA,
|
||||
// FONT_CGA, CURSORS, ORN). Decode (CPU) bleibt in run(); Fonts, Cursor und
|
||||
// Ornament sind weiß-auf-schwarz und bekommen ihre Alpha-Maske aus der
|
||||
// Luminanz.
|
||||
let load_keyed = |p: &str| ui::key_luminance(&tga::load(&assets::path(p)));
|
||||
// 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 fonts = font::Fonts {
|
||||
ega: font::Font::ega(ui::FONT_EGA, &ega_img),
|
||||
cga: font::Font::cga(ui::FONT_CGA, &cga_img),
|
||||
};
|
||||
let ui_textures: Vec<Image> = vec![
|
||||
ui::white_pixel(),
|
||||
ega_img,
|
||||
cga_img,
|
||||
load_keyed("assets/textures/ui/cursors.tga"),
|
||||
load_keyed("assets/textures/ui/panel_ornaments.tga"),
|
||||
];
|
||||
|
||||
// stdin auf einem eigenen Thread: er darf blockieren, der Main-Thread
|
||||
// (winit) nicht. Es queren nur Strings die Thread-Grenze, kein Zustand
|
||||
// — der bleibt allein auf dem Main-Thread.
|
||||
@@ -58,25 +105,101 @@ pub fn run(mut session: Session) {
|
||||
// Poll statt Wait: wir rendern kontinuierlich (Spiel, kein Editor) und
|
||||
// pollen nebenbei den Befehls-Channel.
|
||||
event_loop.set_control_flow(ControlFlow::Poll);
|
||||
let mut app = App::new(session, rx);
|
||||
let mut app = App::new(session, rx, mesh, images, ui_textures, fonts);
|
||||
event_loop.run_app(&mut app).expect("winit: run");
|
||||
}
|
||||
|
||||
/// Kurzer Lade-Report: Map-Inhalt und resultierende Geometriegröße.
|
||||
fn report_map(world: &map::Map, tex_names: &[String], mesh: &Mesh) {
|
||||
let brushes: usize = world.entities.iter().map(|e| e.brushes.len()).sum();
|
||||
let faces: usize = world.entities.iter()
|
||||
.flat_map(|e| &e.brushes)
|
||||
.map(|b| b.faces.len())
|
||||
.sum();
|
||||
println!(
|
||||
"[map] {} Entities, {} Brushes, {} Faces, Texturen {:?} → {} Vertices, {} Dreiecke",
|
||||
world.entities.len(), brushes, faces, tex_names,
|
||||
mesh.verts.len(), mesh.indices.len() / 3,
|
||||
);
|
||||
}
|
||||
|
||||
/// Aktueller Eingabezustand. Tasten als gehaltene Flags (nicht Events),
|
||||
/// damit die Bewegung pro Frame mit dt skaliert; Maus-Delta wird zwischen
|
||||
/// den Frames akkumuliert und nach dem Anwenden genullt.
|
||||
#[derive(Default)]
|
||||
struct Input {
|
||||
fwd: bool,
|
||||
back: bool,
|
||||
left: bool,
|
||||
right: bool,
|
||||
up: bool,
|
||||
down: bool,
|
||||
mouse_dx: f32,
|
||||
mouse_dy: f32,
|
||||
/// Cursor gefangen → Maus-Look aktiv.
|
||||
grabbed: bool,
|
||||
}
|
||||
|
||||
struct App {
|
||||
window: Option<Arc<Window>>,
|
||||
gpu: Option<Gpu>,
|
||||
session: Session,
|
||||
/// Befehlszeilen vom stdin-Thread.
|
||||
rx: Receiver<String>,
|
||||
/// Sim-Zeit (Sekunden), läuft nur im `Free`-Modus weiter → Welt-Pause
|
||||
/// im Dialog. Treibt aktuell die Orbit-Kamera (bis Flycam, Schritt 4).
|
||||
sim_t: f32,
|
||||
camera: Camera,
|
||||
input: Input,
|
||||
last: Option<Instant>,
|
||||
/// Letzte Mausposition in Fenster-Pixeln (für den UI-Cursor).
|
||||
cursor_win: [f32; 2],
|
||||
/// CPU-seitige Welt-Geometrie + Texturen, in `resumed` einmal auf die
|
||||
/// GPU geladen.
|
||||
mesh: Mesh,
|
||||
images: Vec<Image>,
|
||||
ui_textures: Vec<Image>,
|
||||
fonts: font::Fonts,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new(session: Session, rx: Receiver<String>) -> Self {
|
||||
Self { window: None, gpu: None, session, rx, sim_t: 0.0, last: None }
|
||||
fn new(
|
||||
session: Session,
|
||||
rx: Receiver<String>,
|
||||
mesh: Mesh,
|
||||
images: Vec<Image>,
|
||||
ui_textures: Vec<Image>,
|
||||
fonts: font::Fonts,
|
||||
) -> Self {
|
||||
Self {
|
||||
window: None,
|
||||
gpu: None,
|
||||
session,
|
||||
rx,
|
||||
// Etwas zurück und erhöht, Blick Richtung Welt-Ursprung.
|
||||
camera: Camera::new([0.0, 1.0, 4.0]),
|
||||
input: Input::default(),
|
||||
last: None,
|
||||
cursor_win: [0.0, 0.0],
|
||||
mesh,
|
||||
images,
|
||||
ui_textures,
|
||||
fonts,
|
||||
}
|
||||
}
|
||||
|
||||
// Der OS-Cursor bleibt immer versteckt (in `resumed` einmal gesetzt) —
|
||||
// wir zeichnen stets ein eigenes Sprite. `set_grab` schaltet nur die
|
||||
// Greif-Art: Locked (Flycam, relativer Blick) ↔ None (freie Maus, UI).
|
||||
fn set_grab(&mut self, grab: bool) {
|
||||
let Some(window) = &self.window else { return; };
|
||||
if grab {
|
||||
// Locked (Cursor fixiert) ist am angenehmsten, wird aber nicht
|
||||
// überall unterstützt → auf Confined zurückfallen.
|
||||
let ok = window.set_cursor_grab(CursorGrabMode::Locked)
|
||||
.or_else(|_| window.set_cursor_grab(CursorGrabMode::Confined))
|
||||
.is_ok();
|
||||
if ok { self.input.grabbed = true; }
|
||||
} else {
|
||||
let _ = window.set_cursor_grab(CursorGrabMode::None);
|
||||
self.input.grabbed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,14 +210,33 @@ impl ApplicationHandler for App {
|
||||
if self.window.is_some() { return; }
|
||||
let attrs = Window::default_attributes()
|
||||
.with_title("wds")
|
||||
// 3× interne Auflösung als Startgröße; frei resizebar.
|
||||
// 2× interne Auflösung als Startgröße; frei resizebar.
|
||||
.with_inner_size(winit::dpi::LogicalSize::new(
|
||||
gpu::INTERNAL_W * 3, gpu::INTERNAL_H * 3));
|
||||
gpu::INTERNAL_W * 2, gpu::INTERNAL_H * 2));
|
||||
let window = Arc::new(event_loop.create_window(attrs).expect("winit: Fenster"));
|
||||
self.gpu = Some(Gpu::new(window.clone(), event_loop.owned_display_handle()));
|
||||
// OS-Cursor dauerhaft aus — das Overlay zeichnet immer einen eigenen.
|
||||
window.set_cursor_visible(false);
|
||||
self.gpu = Some(Gpu::new(
|
||||
window.clone(),
|
||||
event_loop.owned_display_handle(),
|
||||
&self.mesh,
|
||||
&self.images,
|
||||
&self.ui_textures,
|
||||
));
|
||||
self.window = Some(window);
|
||||
}
|
||||
|
||||
/// Rohe Maus-Bewegung (unabhängig von Cursor-Position) — nur sinnvoll,
|
||||
/// solange der Cursor gefangen ist.
|
||||
fn device_event(&mut self, _el: &ActiveEventLoop, _id: DeviceId, event: DeviceEvent) {
|
||||
if let DeviceEvent::MouseMotion { delta } = event {
|
||||
if self.input.grabbed {
|
||||
self.input.mouse_dx += delta.0 as f32;
|
||||
self.input.mouse_dy += delta.1 as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Läuft einmal pro Loop-Durchlauf, nachdem die Events abgearbeitet
|
||||
/// sind: Konsolen-Befehle einspielen und den nächsten Frame anfordern.
|
||||
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
|
||||
@@ -109,29 +251,87 @@ impl ApplicationHandler for App {
|
||||
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
|
||||
match event {
|
||||
WindowEvent::CloseRequested => event_loop.exit(),
|
||||
WindowEvent::KeyboardInput {
|
||||
event: KeyEvent {
|
||||
physical_key: PhysicalKey::Code(KeyCode::Escape),
|
||||
state: ElementState::Pressed, ..
|
||||
}, ..
|
||||
} => event_loop.exit(),
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
self.cursor_win = [position.x as f32, position.y as f32];
|
||||
}
|
||||
WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left, .. } => {
|
||||
// Nur im freien Modus greift der Klick die Maus (Flycam). Im
|
||||
// UI-/Dialog-Modus bleibt die Maus frei (für Klicks aufs UI).
|
||||
if matches!(self.session.mode, Mode::Free) { self.set_grab(true); }
|
||||
}
|
||||
WindowEvent::KeyboardInput { event: key, .. } => self.on_key(event_loop, key),
|
||||
WindowEvent::Resized(size) => {
|
||||
if let Some(gpu) = &mut self.gpu {
|
||||
gpu.resize(size.width, size.height);
|
||||
}
|
||||
}
|
||||
WindowEvent::RedrawRequested => {
|
||||
// dt messen; im Dialog die Sim-Uhr anhalten → Welt pausiert,
|
||||
// während die Konsole den Dialog treibt.
|
||||
let now = Instant::now();
|
||||
let dt = self.last.replace(now)
|
||||
.map_or(0.0, |prev| (now - prev).as_secs_f32());
|
||||
if matches!(self.session.mode, Mode::Free) {
|
||||
self.sim_t += dt;
|
||||
}
|
||||
if let Some(gpu) = &mut self.gpu { gpu.frame(self.sim_t); }
|
||||
}
|
||||
WindowEvent::RedrawRequested => self.redraw(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn on_key(&mut self, event_loop: &ActiveEventLoop, key: KeyEvent) {
|
||||
let PhysicalKey::Code(code) = key.physical_key else { return; };
|
||||
let down = key.state == ElementState::Pressed;
|
||||
|
||||
// Escape: erst den Cursor freigeben, sonst beenden.
|
||||
if code == KeyCode::Escape && down {
|
||||
if self.input.grabbed { self.set_grab(false); } else { event_loop.exit(); }
|
||||
return;
|
||||
}
|
||||
|
||||
match code {
|
||||
KeyCode::KeyW => self.input.fwd = down,
|
||||
KeyCode::KeyS => self.input.back = down,
|
||||
KeyCode::KeyA => self.input.left = down,
|
||||
KeyCode::KeyD => self.input.right = down,
|
||||
KeyCode::Space => self.input.up = down,
|
||||
KeyCode::ShiftLeft => self.input.down = down,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn redraw(&mut self) {
|
||||
// dt messen; im Dialog Kamera und Sim anhalten → Welt pausiert,
|
||||
// während die Konsole den Dialog treibt.
|
||||
let now = Instant::now();
|
||||
let dt = self.last.replace(now).map_or(0.0, |prev| (now - prev).as_secs_f32());
|
||||
|
||||
if matches!(self.session.mode, Mode::Free) {
|
||||
self.camera.apply_mouse(self.input.mouse_dx, self.input.mouse_dy, MOUSE_SENS);
|
||||
let fwd = axis(self.input.fwd, self.input.back);
|
||||
let right = axis(self.input.right, self.input.left);
|
||||
let up = axis(self.input.up, self.input.down);
|
||||
self.camera.translate(fwd, right, up, MOVE_SPEED * dt);
|
||||
} else if self.input.grabbed {
|
||||
// Nicht-Free (Dialog/Menü) → Maus freigeben, damit das UI klickbar
|
||||
// ist und der Maus-Cursor statt des HUD-Pointers erscheint.
|
||||
self.set_grab(false);
|
||||
}
|
||||
// Delta immer verwerfen, auch im Dialog — sonst springt der Blick
|
||||
// beim Fortsetzen um die aufgestaute Bewegung.
|
||||
self.input.mouse_dx = 0.0;
|
||||
self.input.mouse_dy = 0.0;
|
||||
|
||||
let view = self.camera.view();
|
||||
// Mausposition über die Letterbox in interne Pixel mappen.
|
||||
let cursor = ui::Cursor {
|
||||
pos: self.gpu.as_ref()
|
||||
.map(|g| g.map_cursor(self.cursor_win))
|
||||
.unwrap_or([0.0, 0.0]),
|
||||
grabbed: self.input.grabbed,
|
||||
};
|
||||
// Overlay state-driven bauen (Stage 3: Demo-Panel + Cursor).
|
||||
let overlay = ui::layout(
|
||||
[gpu::INTERNAL_W as f32, gpu::INTERNAL_H as f32], &self.fonts, &cursor,
|
||||
);
|
||||
if let Some(gpu) = &mut self.gpu { gpu.frame(&view, &overlay); }
|
||||
}
|
||||
}
|
||||
|
||||
/// Zwei gegensätzliche Tasten zu einer Achse in {-1, 0, 1}.
|
||||
fn axis(positive: bool, negative: bool) -> f32 {
|
||||
(positive as i32 - negative as i32) as f32
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user