diff options
Diffstat (limited to 'crates/render-demo')
| -rw-r--r-- | crates/render-demo/Cargo.toml | 31 | ||||
| -rw-r--r-- | crates/render-demo/README.md | 84 | ||||
| -rw-r--r-- | crates/render-demo/build.rs | 4 | ||||
| -rw-r--r-- | crates/render-demo/src/lib.rs | 591 | ||||
| -rw-r--r-- | crates/render-demo/src/main.rs | 997 |
5 files changed, 0 insertions, 1707 deletions
diff --git a/crates/render-demo/Cargo.toml b/crates/render-demo/Cargo.toml deleted file mode 100644 index a2161bb..0000000 --- a/crates/render-demo/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "render-demo" -version = "0.1.0" -edition = "2021" - -[features] -default = [] -demo = ["dep:sdl2", "dep:glow", "dep:image"] - -[dependencies] -encoding_rs = "0.8" -msh-core = { path = "../msh-core" } -nres = { path = "../nres" } -render-core = { path = "../render-core" } -texm = { path = "../texm" } -glow = { version = "0.17", optional = true } -image = { version = "0.25", optional = true, default-features = false, features = ["png"] } - -[dev-dependencies] -common = { path = "../common" } - -[target.'cfg(target_os = "macos")'.dependencies] -sdl2 = { version = "0.38", optional = true, default-features = false, features = ["use-pkgconfig"] } - -[target.'cfg(not(target_os = "macos"))'.dependencies] -sdl2 = { version = "0.38", optional = true, default-features = false, features = ["bundled", "static-link"] } - -[[bin]] -name = "parkan-render-demo" -path = "src/main.rs" -required-features = ["demo"] diff --git a/crates/render-demo/README.md b/crates/render-demo/README.md deleted file mode 100644 index e9d5950..0000000 --- a/crates/render-demo/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# render-demo - -Тестовый рендерер Parkan-моделей на Rust (`SDL2 + OpenGL`: GLES2 с fallback на Core 3.3). - -## Назначение - -- Проверить, что `nres + msh-core + render-core` дают рабочий draw-path на реальных ассетах. -- Проверить текстурный path `WEAR -> MAT0 -> Texm` на реальных ассетах. -- Служить минимальным reference-приложением. - -## Запуск - -```bash -cargo run -p render-demo --features demo -- \ - --archive "testdata/Parkan - Iron Strategy/animals.rlb" \ - --model "A_L_01.msh" \ - --lod 0 \ - --group 0 -``` - -### macOS prerequisites - -Для macOS `render-demo` ожидает системный SDL2 через `pkg-config`: - -```bash -brew install sdl2 pkg-config -``` - -После этого запускайте той же командой `cargo run ... --features demo`. - -Параметры: - -- `--archive` (обязательный): NRes-архив с `.msh` entry. -- `--model` (опционально): имя модели; если не задано, берётся первая `.msh`. -- `--lod` (опционально, default `0`). -- `--group` (опционально, default `0`). -- `--width`, `--height` (опционально, default `1280x720`). -- `--angle` (опционально): фиксированный угол поворота вокруг Y (в радианах). -- `--spin-rate` (опционально, default `0.35`): скорость вращения в интерактивном режиме. -- В интерактивном режиме FPS выводится в заголовок окна и в stdout (обновление примерно каждые 0.5 сек). -- `--texture <name>`: явное имя `Texm` (override авто-резолва). -- `--texture-archive <path>`: путь к архиву текстур (по умолчанию `textures.lib` рядом с `--archive`). -- `--material-archive <path>`: путь к `material.lib` (по умолчанию соседний `material.lib`). -- `--wear <name.wea>`: имя wear-entry внутри модельного архива (по умолчанию `<model_stem>.wea`). -- `--no-texture`: отключить текстуры и рендерить однотонным цветом. - -## Авто-резолв текстуры - -Если не передан `--texture`, демо пытается взять текстуру из игровых данных: - -1. `model.msh -> model.wea` (первый wear-материал), -2. `material.lib` (`MAT0`) по имени материала с fallback `DEFAULT`, -3. первая непустая `textureName` фаза материала, -4. загрузка `Texm` из `textures.lib` (или `lightmap.lib` как fallback). - -## Детерминированный снимок кадра - -Для parity-проверок используется headless-сценарий с фиксированными параметрами: - -```bash -cargo run -p render-demo --features demo -- \ - --archive "testdata/Parkan - Iron Strategy/animals.rlb" \ - --model "A_L_01.msh" \ - --lod 0 \ - --group 0 \ - --width 1280 \ - --height 720 \ - --angle 0.0 \ - --capture "target/render-parity/current/animals_a_l_01.png" -``` - -Явный выбор текстуры: - -```bash -cargo run -p render-demo --features demo -- \ - --archive "testdata/Parkan - Iron Strategy/animals.rlb" \ - --model "A_L_01.msh" \ - --texture "PG09.0" -``` - -## Ограничения - -- Используется только базовая texture-фаза (без полной material/fx анимации). -- Вывод через `glDrawElements(GL_TRIANGLES)` с index-buffer (позиции+UV). diff --git a/crates/render-demo/build.rs b/crates/render-demo/build.rs deleted file mode 100644 index 126d1d7..0000000 --- a/crates/render-demo/build.rs +++ /dev/null @@ -1,4 +0,0 @@ -fn main() { - #[cfg(windows)] - println!("cargo:rustc-link-lib=advapi32"); -} diff --git a/crates/render-demo/src/lib.rs b/crates/render-demo/src/lib.rs deleted file mode 100644 index 9555151..0000000 --- a/crates/render-demo/src/lib.rs +++ /dev/null @@ -1,591 +0,0 @@ -use encoding_rs::WINDOWS_1251; -use msh_core::{parse_model_payload, Model}; -use nres::{Archive, EntryRef}; -use std::fmt; -use std::path::{Path, PathBuf}; -use texm::{decode_mip_rgba8, parse_texm}; - -const WEAR_KIND: u32 = 0x5241_4557; -const MAT0_KIND: u32 = 0x3054_414D; - -#[derive(Debug)] -pub enum Error { - Nres(nres::error::Error), - Msh(msh_core::error::Error), - Texm(texm::error::Error), - Io(std::io::Error), - NoMshEntries, - ModelNotFound(String), - NoTexmEntries, - TextureNotFound(String), - MaterialNotFound(String), - WearNotFound(String), - InvalidWear(String), - InvalidMaterial(String), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Nres(err) => write!(f, "{err}"), - Self::Msh(err) => write!(f, "{err}"), - Self::Texm(err) => write!(f, "{err}"), - Self::Io(err) => write!(f, "{err}"), - Self::NoMshEntries => write!(f, "archive does not contain .msh entries"), - Self::ModelNotFound(name) => write!(f, "model not found: {name}"), - Self::NoTexmEntries => write!(f, "archive does not contain Texm entries"), - Self::TextureNotFound(name) => write!(f, "texture not found: {name}"), - Self::MaterialNotFound(name) => write!(f, "material not found: {name}"), - Self::WearNotFound(name) => write!(f, "wear entry not found: {name}"), - Self::InvalidWear(reason) => write!(f, "invalid WEAR payload: {reason}"), - Self::InvalidMaterial(reason) => write!(f, "invalid MAT0 payload: {reason}"), - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::Nres(err) => Some(err), - Self::Msh(err) => Some(err), - Self::Texm(err) => Some(err), - Self::Io(err) => Some(err), - _ => None, - } - } -} - -impl From<nres::error::Error> for Error { - fn from(value: nres::error::Error) -> Self { - Self::Nres(value) - } -} - -impl From<msh_core::error::Error> for Error { - fn from(value: msh_core::error::Error) -> Self { - Self::Msh(value) - } -} - -impl From<texm::error::Error> for Error { - fn from(value: texm::error::Error) -> Self { - Self::Texm(value) - } -} - -impl From<std::io::Error> for Error { - fn from(value: std::io::Error) -> Self { - Self::Io(value) - } -} - -pub type Result<T> = core::result::Result<T, Error>; - -#[derive(Clone, Debug)] -pub struct LoadedModel { - pub name: String, - pub model: Model, -} - -#[derive(Clone, Debug)] -pub struct LoadedTexture { - pub name: String, - pub width: u32, - pub height: u32, - pub rgba8: Vec<u8>, -} - -pub fn load_model_with_name_from_archive( - path: &Path, - model_name: Option<&str>, -) -> Result<LoadedModel> { - let archive = Archive::open_path(path)?; - let mut msh_entries = Vec::new(); - for entry in archive.entries() { - if entry.meta.name.to_ascii_lowercase().ends_with(".msh") { - msh_entries.push((entry.id, entry.meta.name.clone())); - } - } - if msh_entries.is_empty() { - return Err(Error::NoMshEntries); - } - - let target_id = if let Some(name) = model_name { - msh_entries - .iter() - .find(|(_, n)| n.eq_ignore_ascii_case(name)) - .map(|(id, _)| *id) - .ok_or_else(|| Error::ModelNotFound(name.to_string()))? - } else { - msh_entries[0].0 - }; - - let target_name = archive - .get(target_id) - .map(|entry| entry.meta.name.clone()) - .unwrap_or_else(|| String::from("<unknown>")); - let payload = archive.read(target_id)?; - Ok(LoadedModel { - name: target_name, - model: parse_model_payload(payload.as_slice())?, - }) -} - -pub fn load_model_from_archive(path: &Path, model_name: Option<&str>) -> Result<Model> { - Ok(load_model_with_name_from_archive(path, model_name)?.model) -} - -pub fn load_texture_from_archive(path: &Path, texture_name: Option<&str>) -> Result<LoadedTexture> { - let archive = Archive::open_path(path)?; - if let Some(name) = texture_name { - return load_texture_from_archive_by_name(&archive, name); - } - - let mut texm_entries = archive - .entries() - .filter(|entry| entry.meta.kind == texm::TEXM_MAGIC) - .collect::<Vec<_>>(); - if texm_entries.is_empty() { - return Err(Error::NoTexmEntries); - } - texm_entries.sort_by(|a, b| { - a.meta - .name - .to_ascii_lowercase() - .cmp(&b.meta.name.to_ascii_lowercase()) - }); - let first = texm_entries[0]; - decode_texture_entry(&archive, first) -} - -pub fn resolve_texture_for_model( - model_archive_path: &Path, - model_entry_name: &str, - texture_name_override: Option<&str>, - textures_archive_override: Option<&Path>, - material_archive_override: Option<&Path>, - wear_entry_override: Option<&str>, -) -> Result<Option<LoadedTexture>> { - if let Some(name) = texture_name_override { - return load_texture_by_name_from_candidate_archives( - name, - candidate_texture_archives(model_archive_path, textures_archive_override), - ) - .map(Some); - } - - let wear_entry_name = if let Some(name) = wear_entry_override { - name.to_string() - } else { - derive_wear_entry_name(model_entry_name).ok_or_else(|| { - Error::WearNotFound(format!( - "cannot derive WEAR name from model '{model_entry_name}'" - )) - })? - }; - - let model_archive = Archive::open_path(model_archive_path)?; - let wear_materials = parse_wear_material_names( - read_entry_by_name_kind(&model_archive, &wear_entry_name, WEAR_KIND)? - .0 - .as_slice(), - )?; - let Some(primary_material) = wear_materials.first() else { - return Ok(None); - }; - - let material_path = if let Some(path) = material_archive_override { - path.to_path_buf() - } else { - sibling_archive_path(model_archive_path, "material.lib") - .ok_or_else(|| Error::MaterialNotFound(String::from("material.lib")))? - }; - let material_archive = Archive::open_path(&material_path)?; - let material_entry = find_material_entry_with_fallback(&material_archive, primary_material)?; - let material_payload = material_archive.read(material_entry.id)?.into_owned(); - let texture_name = - parse_primary_texture_name_from_mat0(&material_payload, material_entry.meta.attr2)?; - let Some(texture_name) = texture_name else { - return Ok(None); - }; - - let texture = load_texture_by_name_from_candidate_archives( - &texture_name, - candidate_texture_archives(model_archive_path, textures_archive_override), - )?; - Ok(Some(texture)) -} - -fn load_texture_by_name_from_candidate_archives( - texture_name: &str, - archives: Vec<PathBuf>, -) -> Result<LoadedTexture> { - let mut last_not_found = None; - for archive_path in archives { - if !archive_path.is_file() { - continue; - } - let archive = Archive::open_path(&archive_path)?; - match load_texture_from_archive_by_name(&archive, texture_name) { - Ok(texture) => return Ok(texture), - Err(Error::TextureNotFound(name)) => { - last_not_found = Some(name); - } - Err(other) => return Err(other), - } - } - - Err(Error::TextureNotFound( - last_not_found.unwrap_or_else(|| texture_name.to_string()), - )) -} - -fn candidate_texture_archives( - model_archive_path: &Path, - textures_archive_override: Option<&Path>, -) -> Vec<PathBuf> { - if let Some(path) = textures_archive_override { - return vec![path.to_path_buf()]; - } - - let mut out = Vec::new(); - if let Some(path) = sibling_archive_path(model_archive_path, "textures.lib") { - out.push(path); - } - if let Some(path) = sibling_archive_path(model_archive_path, "lightmap.lib") { - out.push(path); - } - out -} - -fn sibling_archive_path(model_archive_path: &Path, name: &str) -> Option<PathBuf> { - let parent = model_archive_path.parent()?; - Some(parent.join(name)) -} - -fn derive_wear_entry_name(model_entry_name: &str) -> Option<String> { - let stem = model_entry_name.rsplit_once('.').map(|(left, _)| left)?; - Some(format!("{stem}.wea")) -} - -fn read_entry_by_name_kind( - archive: &Archive, - name: &str, - expected_kind: u32, -) -> Result<(Vec<u8>, String)> { - let Some(id) = archive.find(name) else { - return Err(Error::WearNotFound(name.to_string())); - }; - let Some(entry) = archive.get(id) else { - return Err(Error::WearNotFound(name.to_string())); - }; - if entry.meta.kind != expected_kind { - return Err(Error::WearNotFound(name.to_string())); - } - let payload = archive.read(id)?.into_owned(); - Ok((payload, entry.meta.name.clone())) -} - -fn find_material_entry_with_fallback<'a>( - archive: &'a Archive, - requested_name: &str, -) -> Result<EntryRef<'a>> { - if let Some(id) = archive.find(requested_name) { - if let Some(entry) = archive.get(id) { - if entry.meta.kind == MAT0_KIND { - return Ok(entry); - } - } - } - - if let Some(id) = archive.find("DEFAULT") { - if let Some(entry) = archive.get(id) { - if entry.meta.kind == MAT0_KIND { - return Ok(entry); - } - } - } - - let Some(entry) = archive.entries().find(|entry| entry.meta.kind == MAT0_KIND) else { - return Err(Error::MaterialNotFound(requested_name.to_string())); - }; - Ok(entry) -} - -fn parse_wear_material_names(payload: &[u8]) -> Result<Vec<String>> { - let text = decode_cp1251(payload).replace('\r', ""); - let mut lines = text.lines(); - let Some(first) = lines.next() else { - return Err(Error::InvalidWear(String::from("WEAR payload is empty"))); - }; - let count = first - .trim() - .parse::<usize>() - .map_err(|_| Error::InvalidWear(format!("invalid wearCount line: '{first}'")))?; - if count == 0 { - return Err(Error::InvalidWear(String::from("wearCount must be > 0"))); - } - - let mut materials = Vec::with_capacity(count); - for idx in 0..count { - let Some(line) = lines.next() else { - return Err(Error::InvalidWear(format!( - "missing material line {idx} of {count}" - ))); - }; - let mut parts = line.split_whitespace(); - let _legacy = parts - .next() - .ok_or_else(|| Error::InvalidWear(format!("invalid material line {idx}: '{line}'")))?; - let name = parts - .next() - .ok_or_else(|| Error::InvalidWear(format!("invalid material line {idx}: '{line}'")))?; - materials.push(name.to_string()); - } - - Ok(materials) -} - -fn parse_primary_texture_name_from_mat0(payload: &[u8], attr2: u32) -> Result<Option<String>> { - if payload.len() < 4 { - return Err(Error::InvalidMaterial(String::from( - "MAT0 payload is too small for header", - ))); - } - let phase_count = u16::from_le_bytes([payload[0], payload[1]]) as usize; - if phase_count == 0 { - return Ok(None); - } - - let mut offset = 4usize; - if attr2 >= 2 { - offset = offset - .checked_add(2) - .ok_or_else(|| Error::InvalidMaterial(String::from("MAT0 offset overflow")))?; - } - if attr2 >= 3 { - offset = offset - .checked_add(4) - .ok_or_else(|| Error::InvalidMaterial(String::from("MAT0 offset overflow")))?; - } - if attr2 >= 4 { - offset = offset - .checked_add(4) - .ok_or_else(|| Error::InvalidMaterial(String::from("MAT0 offset overflow")))?; - } - - for phase in 0..phase_count { - let phase_off = offset - .checked_add(phase.checked_mul(34).ok_or_else(|| { - Error::InvalidMaterial(String::from("MAT0 phase offset overflow")) - })?) - .ok_or_else(|| Error::InvalidMaterial(String::from("MAT0 phase offset overflow")))?; - let phase_end = phase_off - .checked_add(34) - .ok_or_else(|| Error::InvalidMaterial(String::from("MAT0 phase offset overflow")))?; - let Some(rec) = payload.get(phase_off..phase_end) else { - return Err(Error::InvalidMaterial(format!( - "MAT0 phase {phase} is out of bounds" - ))); - }; - let name_raw = &rec[18..34]; - let name_end = name_raw - .iter() - .position(|&b| b == 0) - .unwrap_or(name_raw.len()); - let name = decode_cp1251(&name_raw[..name_end]).trim().to_string(); - if !name.is_empty() { - return Ok(Some(name)); - } - } - - Ok(None) -} - -fn decode_cp1251(bytes: &[u8]) -> String { - let (decoded, _, _) = WINDOWS_1251.decode(bytes); - decoded.into_owned() -} - -fn load_texture_from_archive_by_name(archive: &Archive, name: &str) -> Result<LoadedTexture> { - let Some(id) = archive.find(name) else { - return Err(Error::TextureNotFound(name.to_string())); - }; - let Some(entry) = archive.get(id) else { - return Err(Error::TextureNotFound(name.to_string())); - }; - if entry.meta.kind != texm::TEXM_MAGIC { - return Err(Error::TextureNotFound(name.to_string())); - } - decode_texture_entry(archive, entry) -} - -fn decode_texture_entry(archive: &Archive, entry: EntryRef<'_>) -> Result<LoadedTexture> { - let payload = archive.read(entry.id)?.into_owned(); - let parsed = parse_texm(&payload)?; - let decoded = decode_mip_rgba8(&parsed, &payload, 0)?; - Ok(LoadedTexture { - name: entry.meta.name.clone(), - width: decoded.width, - height: decoded.height, - rgba8: decoded.rgba8, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use common::collect_files_recursive; - use std::fs; - use std::path::{Path, PathBuf}; - - fn archive_with_msh() -> Option<PathBuf> { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("testdata"); - let mut files = Vec::new(); - collect_files_recursive(&root, &mut files); - files.sort(); - for path in files { - let Ok(bytes) = fs::read(&path) else { - continue; - }; - if bytes.get(0..4) != Some(b"NRes") { - continue; - } - let Ok(archive) = Archive::open_path(&path) else { - continue; - }; - if archive - .entries() - .any(|entry| entry.meta.name.to_ascii_lowercase().ends_with(".msh")) - { - return Some(path); - } - } - None - } - - fn game_root() -> Option<PathBuf> { - let path = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("testdata") - .join("Parkan - Iron Strategy"); - if path.is_dir() { - Some(path) - } else { - None - } - } - - #[test] - fn load_model_from_real_archive() { - let Some(path) = archive_with_msh() else { - eprintln!("skipping load_model_from_real_archive: no .msh archives in testdata"); - return; - }; - let model = load_model_from_archive(&path, None) - .unwrap_or_else(|err| panic!("failed to load model from {}: {err:?}", path.display())); - assert!(model.node_count > 0); - assert!(!model.positions.is_empty()); - assert!(!model.indices.is_empty()); - } - - #[test] - fn resolve_texture_for_real_model_via_wear_and_material() { - let Some(root) = game_root() else { - eprintln!( - "skipping resolve_texture_for_real_model_via_wear_and_material: no game root" - ); - return; - }; - let archive = root.join("animals.rlb"); - if !archive.is_file() { - eprintln!("skipping resolve_texture_for_real_model_via_wear_and_material: missing animals.rlb"); - return; - } - - let loaded = load_model_with_name_from_archive(&archive, Some("A_L_01.msh")) - .unwrap_or_else(|err| { - panic!( - "failed to load model A_L_01.msh from {}: {err:?}", - archive.display() - ) - }); - let texture = resolve_texture_for_model(&archive, &loaded.name, None, None, None, None) - .unwrap_or_else(|err| panic!("failed to resolve texture for {}: {err:?}", loaded.name)) - .expect("texture must be resolved for A_L_01.msh"); - assert!(texture.width > 0 && texture.height > 0); - assert_eq!( - texture.rgba8.len(), - usize::try_from(texture.width) - .ok() - .and_then(|w| usize::try_from(texture.height).ok().map(|h| w * h * 4)) - .unwrap_or(0) - ); - } - - #[test] - fn load_first_texture_from_real_archive() { - let Some(root) = game_root() else { - eprintln!("skipping load_first_texture_from_real_archive: no game root"); - return; - }; - let archive = root.join("textures.lib"); - if !archive.is_file() { - eprintln!("skipping load_first_texture_from_real_archive: missing textures.lib"); - return; - } - let texture = load_texture_from_archive(&archive, None).unwrap_or_else(|err| { - panic!( - "failed to load first texture from {}: {err:?}", - archive.display() - ) - }); - assert!(texture.width > 0 && texture.height > 0); - assert!(!texture.rgba8.is_empty()); - } - - #[test] - fn parse_wear_material_names_parses_counted_lines() { - let payload = b"2\r\n0 MAT_A\r\n1 MAT_B\r\n"; - let materials = - parse_wear_material_names(payload).expect("failed to parse valid WEAR payload"); - assert_eq!(materials, vec!["MAT_A".to_string(), "MAT_B".to_string()]); - } - - #[test] - fn parse_wear_material_names_rejects_invalid_payload() { - let payload = b"2\n0 ONLY_ONE\n"; - assert!(matches!( - parse_wear_material_names(payload), - Err(Error::InvalidWear(_)) - )); - } - - #[test] - fn parse_primary_texture_name_from_mat0_respects_attr2_layout() { - let mut payload = vec![0u8; 4 + 10 + 34]; - payload[0..2].copy_from_slice(&1u16.to_le_bytes()); // phase_count - // attr2=4 adds 10 bytes before phase table - let name = b"TEX_MAIN"; - payload[4 + 10 + 18..4 + 10 + 18 + name.len()].copy_from_slice(name); - - let parsed = parse_primary_texture_name_from_mat0(&payload, 4) - .expect("failed to parse MAT0 payload with attr2=4"); - assert_eq!(parsed, Some("TEX_MAIN".to_string())); - } - - #[test] - fn parse_primary_texture_name_from_mat0_decodes_cp1251_bytes() { - let mut payload = vec![0u8; 4 + 34]; - payload[0..2].copy_from_slice(&1u16.to_le_bytes()); // phase_count - payload[4 + 18] = 0xC0; // 'А' in CP1251 - - let parsed = - parse_primary_texture_name_from_mat0(&payload, 0).expect("failed to parse MAT0"); - assert_eq!(parsed, Some("А".to_string())); - } -} diff --git a/crates/render-demo/src/main.rs b/crates/render-demo/src/main.rs deleted file mode 100644 index 61f6bfa..0000000 --- a/crates/render-demo/src/main.rs +++ /dev/null @@ -1,997 +0,0 @@ -use glow::HasContext as _; -use render_core::{build_render_mesh, compute_bounds_for_mesh}; -use render_demo::{load_model_with_name_from_archive, resolve_texture_for_model, LoadedTexture}; -use std::io::Write as _; -use std::path::{Path, PathBuf}; -use std::time::{Duration, Instant}; - -struct Args { - archive: PathBuf, - model: Option<String>, - lod: usize, - group: usize, - width: u32, - height: u32, - fov_deg: f32, - capture: Option<PathBuf>, - angle: Option<f32>, - spin_rate: f32, - texture: Option<String>, - texture_archive: Option<PathBuf>, - material_archive: Option<PathBuf>, - wear: Option<String>, - no_texture: bool, -} - -struct GpuTexture { - handle: glow::NativeTexture, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -enum GlBackend { - Gles2, - Core33, -} - -fn parse_args() -> Result<Args, String> { - let mut archive = None; - let mut model = None; - let mut lod = 0usize; - let mut group = 0usize; - let mut width = 1280u32; - let mut height = 720u32; - let mut fov_deg = 60.0f32; - let mut capture = None; - let mut angle = None; - let mut spin_rate = 0.35f32; - let mut texture = None; - let mut texture_archive = None; - let mut material_archive = None; - let mut wear = None; - let mut no_texture = false; - - let mut it = std::env::args().skip(1); - while let Some(arg) = it.next() { - match arg.as_str() { - "--archive" => { - let value = it - .next() - .ok_or_else(|| String::from("missing value for --archive"))?; - archive = Some(PathBuf::from(value)); - } - "--model" => { - let value = it - .next() - .ok_or_else(|| String::from("missing value for --model"))?; - model = Some(value); - } - "--lod" => { - let value = it - .next() - .ok_or_else(|| String::from("missing value for --lod"))?; - lod = value - .parse::<usize>() - .map_err(|_| String::from("invalid --lod value"))?; - } - "--group" => { - let value = it - .next() - .ok_or_else(|| String::from("missing value for --group"))?; - group = value - .parse::<usize>() - .map_err(|_| String::from("invalid --group value"))?; - } - "--width" => { - let value = it - .next() - .ok_or_else(|| String::from("missing value for --width"))?; - width = value - .parse::<u32>() - .map_err(|_| String::from("invalid --width value"))?; - if width == 0 { - return Err(String::from("--width must be > 0")); - } - } - "--height" => { - let value = it - .next() - .ok_or_else(|| String::from("missing value for --height"))?; - height = value - .parse::<u32>() - .map_err(|_| String::from("invalid --height value"))?; - if height == 0 { - return Err(String::from("--height must be > 0")); - } - } - "--fov" => { - let value = it - .next() - .ok_or_else(|| String::from("missing value for --fov"))?; - fov_deg = value - .parse::<f32>() - .map_err(|_| String::from("invalid --fov value"))?; - if !(1.0..=179.0).contains(&fov_deg) { - return Err(String::from("--fov must be in range [1, 179]")); - } - } - "--capture" => { - let value = it - .next() - .ok_or_else(|| String::from("missing value for --capture"))?; - capture = Some(PathBuf::from(value)); - } - "--angle" => { - let value = it - .next() - .ok_or_else(|| String::from("missing value for --angle"))?; - angle = Some( - value - .parse::<f32>() - .map_err(|_| String::from("invalid --angle value"))?, - ); - } - "--spin-rate" => { - let value = it - .next() - .ok_or_else(|| String::from("missing value for --spin-rate"))?; - spin_rate = value - .parse::<f32>() - .map_err(|_| String::from("invalid --spin-rate value"))?; - } - "--texture" => { - let value = it - .next() - .ok_or_else(|| String::from("missing value for --texture"))?; - texture = Some(value); - } - "--texture-archive" => { - let value = it - .next() - .ok_or_else(|| String::from("missing value for --texture-archive"))?; - texture_archive = Some(PathBuf::from(value)); - } - "--material-archive" => { - let value = it - .next() - .ok_or_else(|| String::from("missing value for --material-archive"))?; - material_archive = Some(PathBuf::from(value)); - } - "--wear" => { - let value = it - .next() - .ok_or_else(|| String::from("missing value for --wear"))?; - wear = Some(value); - } - "--no-texture" => { - no_texture = true; - } - "--help" | "-h" => { - print_help(); - std::process::exit(0); - } - other => { - return Err(format!("unknown argument: {other}")); - } - } - } - - let archive = archive.ok_or_else(|| String::from("missing required --archive"))?; - Ok(Args { - archive, - model, - lod, - group, - width, - height, - fov_deg, - capture, - angle, - spin_rate, - texture, - texture_archive, - material_archive, - wear, - no_texture, - }) -} - -fn print_help() { - eprintln!( - "parkan-render-demo --archive <path> [--model <name.msh>] [--lod N] [--group N] [--width W] [--height H] [--fov DEG]" - ); - eprintln!(" [--capture <out.png>] [--angle RAD] [--spin-rate RAD_PER_SEC]"); - eprintln!(" [--texture <name>] [--texture-archive <path>] [--material-archive <path>] [--wear <name.wea>] [--no-texture]"); -} - -fn main() { - let args = match parse_args() { - Ok(v) => v, - Err(err) => { - eprintln!("{err}"); - print_help(); - std::process::exit(2); - } - }; - - if let Err(err) = run(args) { - eprintln!("{err}"); - std::process::exit(1); - } -} - -fn run(args: Args) -> Result<(), String> { - let loaded_model = load_model_with_name_from_archive(&args.archive, args.model.as_deref()) - .map_err(|err| { - format!( - "failed to load model from archive {}: {err}", - args.archive.display() - ) - })?; - let mesh = build_render_mesh(&loaded_model.model, args.lod, args.group); - if mesh.indices.is_empty() { - return Err(format!( - "model has no renderable triangles for lod={} group={}", - args.lod, args.group - )); - } - if mesh.index_overflow { - eprintln!( - "warning: mesh exceeds u16 index space and may be partially rendered on GLES2 targets" - ); - } - let Some((bounds_min, bounds_max)) = compute_bounds_for_mesh(&mesh.vertices) else { - return Err(String::from("failed to compute mesh bounds")); - }; - - let resolved_texture = resolve_texture(&args, &loaded_model.name)?; - if let Some(tex) = resolved_texture.as_ref() { - println!( - "resolved texture '{}' ({}x{})", - tex.name, tex.width, tex.height - ); - } else { - println!("texture path disabled or unresolved; rendering with fallback color"); - } - - let center = [ - 0.5 * (bounds_min[0] + bounds_max[0]), - 0.5 * (bounds_min[1] + bounds_max[1]), - 0.5 * (bounds_min[2] + bounds_max[2]), - ]; - let extent = [ - bounds_max[0] - bounds_min[0], - bounds_max[1] - bounds_min[1], - bounds_max[2] - bounds_min[2], - ]; - let radius = - (extent[0] * extent[0] + extent[1] * extent[1] + extent[2] * extent[2]).sqrt() * 0.5; - let camera_distance = (radius * 2.5).max(2.0); - - let sdl = sdl2::init().map_err(|err| format!("failed to init SDL2: {err}"))?; - let video = sdl - .video() - .map_err(|err| format!("failed to init SDL2 video: {err}"))?; - - let (mut window, _gl_ctx, gl_backend) = create_window_and_context(&video, &args)?; - let _ = if args.capture.is_some() { - video.gl_set_swap_interval(0) - } else { - video.gl_set_swap_interval(1) - }; - - let mut vertex_data = Vec::with_capacity(mesh.vertices.len() * 5); - for vertex in &mesh.vertices { - vertex_data.push(vertex.position[0]); - vertex_data.push(vertex.position[1]); - vertex_data.push(vertex.position[2]); - vertex_data.push(vertex.uv0[0]); - vertex_data.push(vertex.uv0[1]); - } - let vertex_bytes = f32_slice_to_ne_bytes(&vertex_data); - let index_bytes = u16_slice_to_ne_bytes(&mesh.indices); - - let gl = unsafe { - glow::Context::from_loader_function(|name| video.gl_get_proc_address(name) as *const _) - }; - - let program = unsafe { create_program(&gl, gl_backend)? }; - let u_mvp = unsafe { gl.get_uniform_location(program, "u_mvp") }; - let u_use_tex = unsafe { gl.get_uniform_location(program, "u_use_tex") }; - let u_tex = unsafe { gl.get_uniform_location(program, "u_tex") }; - let a_pos = unsafe { gl.get_attrib_location(program, "a_pos") } - .ok_or_else(|| String::from("shader attribute a_pos is missing"))?; - let a_uv = unsafe { gl.get_attrib_location(program, "a_uv") } - .ok_or_else(|| String::from("shader attribute a_uv is missing"))?; - - let vbo = unsafe { gl.create_buffer().map_err(|e| e.to_string())? }; - let ebo = unsafe { gl.create_buffer().map_err(|e| e.to_string())? }; - unsafe { - gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo)); - gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, &vertex_bytes, glow::STATIC_DRAW); - gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(ebo)); - gl.buffer_data_u8_slice(glow::ELEMENT_ARRAY_BUFFER, &index_bytes, glow::STATIC_DRAW); - gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None); - gl.bind_buffer(glow::ARRAY_BUFFER, None); - } - let vao = unsafe { create_vertex_layout_if_needed(&gl, gl_backend, vbo, ebo, a_pos, a_uv)? }; - - let gpu_texture = if let Some(texture) = resolved_texture.as_ref() { - Some(unsafe { create_texture(&gl, texture)? }) - } else { - None - }; - - let result = if let Some(capture_path) = args.capture.as_ref() { - run_capture( - &gl, - program, - u_mvp.as_ref(), - u_use_tex.as_ref(), - u_tex.as_ref(), - a_pos, - a_uv, - vbo, - ebo, - vao, - gpu_texture.as_ref(), - mesh.indices.len(), - &args, - center, - camera_distance, - capture_path, - ) - } else { - run_interactive( - &sdl, - &mut window, - &gl, - program, - u_mvp.as_ref(), - u_use_tex.as_ref(), - u_tex.as_ref(), - a_pos, - a_uv, - vbo, - ebo, - vao, - gpu_texture.as_ref(), - mesh.indices.len(), - &args, - center, - camera_distance, - ) - }; - - unsafe { - if let Some(texture) = gpu_texture { - gl.delete_texture(texture.handle); - } - if let Some(vao) = vao { - gl.delete_vertex_array(vao); - } - gl.delete_buffer(ebo); - gl.delete_buffer(vbo); - gl.delete_program(program); - } - - result -} - -fn create_window_and_context( - video: &sdl2::VideoSubsystem, - args: &Args, -) -> Result<(sdl2::video::Window, sdl2::video::GLContext, GlBackend), String> { - let candidates = [ - (GlBackend::Gles2, sdl2::video::GLProfile::GLES, 2, 0), - (GlBackend::Core33, sdl2::video::GLProfile::Core, 3, 3), - ]; - let mut errors = Vec::new(); - - for (backend, profile, major, minor) in candidates { - { - let gl_attr = video.gl_attr(); - gl_attr.set_context_profile(profile); - gl_attr.set_context_version(major, minor); - gl_attr.set_depth_size(24); - gl_attr.set_double_buffer(true); - } - - let mut window_builder = video.window( - "Parkan Render Demo (SDL2 + OpenGL)", - args.width, - args.height, - ); - window_builder.opengl(); - if args.capture.is_some() { - window_builder.hidden(); - } else { - window_builder.resizable(); - } - - let window = match window_builder.build() { - Ok(window) => window, - Err(err) => { - errors.push(format!( - "{profile:?} {major}.{minor}: window build failed ({err})" - )); - continue; - } - }; - - let gl_ctx = match window.gl_create_context() { - Ok(ctx) => ctx, - Err(err) => { - errors.push(format!( - "{profile:?} {major}.{minor}: context create failed ({err})" - )); - continue; - } - }; - - if let Err(err) = window.gl_make_current(&gl_ctx) { - errors.push(format!( - "{profile:?} {major}.{minor}: make current failed ({err})" - )); - continue; - } - - return Ok((window, gl_ctx, backend)); - } - - Err(format!( - "failed to create OpenGL context. Attempts: {}", - errors.join(" | ") - )) -} - -unsafe fn create_vertex_layout_if_needed( - gl: &glow::Context, - backend: GlBackend, - vbo: glow::NativeBuffer, - ebo: glow::NativeBuffer, - a_pos: u32, - a_uv: u32, -) -> Result<Option<glow::NativeVertexArray>, String> { - if backend != GlBackend::Core33 { - return Ok(None); - } - - let vao = gl.create_vertex_array().map_err(|e| e.to_string())?; - gl.bind_vertex_array(Some(vao)); - gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo)); - gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(ebo)); - gl.enable_vertex_attrib_array(a_pos); - gl.vertex_attrib_pointer_f32(a_pos, 3, glow::FLOAT, false, 20, 0); - gl.enable_vertex_attrib_array(a_uv); - gl.vertex_attrib_pointer_f32(a_uv, 2, glow::FLOAT, false, 20, 12); - gl.bind_vertex_array(None); - Ok(Some(vao)) -} - -fn resolve_texture(args: &Args, model_name: &str) -> Result<Option<LoadedTexture>, String> { - if args.no_texture { - return Ok(None); - } - - match resolve_texture_for_model( - &args.archive, - model_name, - args.texture.as_deref(), - args.texture_archive.as_deref(), - args.material_archive.as_deref(), - args.wear.as_deref(), - ) { - Ok(texture) => Ok(texture), - Err(err) => { - if args.texture.is_some() - || args.texture_archive.is_some() - || args.material_archive.is_some() - || args.wear.is_some() - { - Err(format!("failed to resolve texture: {err}")) - } else { - eprintln!("warning: auto texture resolve failed ({err}), fallback to solid color"); - Ok(None) - } - } - } -} - -unsafe fn create_texture( - gl: &glow::Context, - texture: &LoadedTexture, -) -> Result<GpuTexture, String> { - let handle = gl.create_texture().map_err(|e| e.to_string())?; - gl.bind_texture(glow::TEXTURE_2D, Some(handle)); - gl.tex_parameter_i32( - glow::TEXTURE_2D, - glow::TEXTURE_MIN_FILTER, - glow::LINEAR as i32, - ); - gl.tex_parameter_i32( - glow::TEXTURE_2D, - glow::TEXTURE_MAG_FILTER, - glow::LINEAR as i32, - ); - gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::REPEAT as i32); - gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::REPEAT as i32); - gl.pixel_store_i32(glow::UNPACK_ALIGNMENT, 1); - gl.tex_image_2d( - glow::TEXTURE_2D, - 0, - glow::RGBA as i32, - texture.width.min(i32::MAX as u32) as i32, - texture.height.min(i32::MAX as u32) as i32, - 0, - glow::RGBA, - glow::UNSIGNED_BYTE, - glow::PixelUnpackData::Slice(Some(texture.rgba8.as_slice())), - ); - gl.bind_texture(glow::TEXTURE_2D, None); - Ok(GpuTexture { handle }) -} - -#[allow(clippy::too_many_arguments)] -fn run_capture( - gl: &glow::Context, - program: glow::NativeProgram, - u_mvp: Option<&glow::NativeUniformLocation>, - u_use_tex: Option<&glow::NativeUniformLocation>, - u_tex: Option<&glow::NativeUniformLocation>, - a_pos: u32, - a_uv: u32, - vbo: glow::NativeBuffer, - ebo: glow::NativeBuffer, - vao: Option<glow::NativeVertexArray>, - texture: Option<&GpuTexture>, - index_count: usize, - args: &Args, - center: [f32; 3], - camera_distance: f32, - capture_path: &Path, -) -> Result<(), String> { - let angle = args.angle.unwrap_or(0.0); - let mvp = compute_mvp( - args.width, - args.height, - args.fov_deg, - center, - camera_distance, - angle, - ); - unsafe { - draw_frame( - gl, - program, - u_mvp, - u_use_tex, - u_tex, - a_pos, - a_uv, - vbo, - ebo, - vao, - texture, - index_count, - args.width, - args.height, - &mvp, - ); - } - let mut rgba = unsafe { read_pixels_rgba(gl, args.width, args.height)? }; - flip_image_y_rgba(&mut rgba, args.width as usize, args.height as usize); - save_png(capture_path, args.width, args.height, rgba)?; - println!("captured frame to {}", capture_path.display()); - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -fn run_interactive( - sdl: &sdl2::Sdl, - window: &mut sdl2::video::Window, - gl: &glow::Context, - program: glow::NativeProgram, - u_mvp: Option<&glow::NativeUniformLocation>, - u_use_tex: Option<&glow::NativeUniformLocation>, - u_tex: Option<&glow::NativeUniformLocation>, - a_pos: u32, - a_uv: u32, - vbo: glow::NativeBuffer, - ebo: glow::NativeBuffer, - vao: Option<glow::NativeVertexArray>, - texture: Option<&GpuTexture>, - index_count: usize, - args: &Args, - center: [f32; 3], - camera_distance: f32, -) -> Result<(), String> { - let mut events = sdl - .event_pump() - .map_err(|err| format!("failed to get SDL event pump: {err}"))?; - let start = Instant::now(); - let mut fps_window_start = Instant::now(); - let mut fps_frames: u32 = 0; - let mut fps_printed = false; - let base_title = "Parkan Render Demo (SDL2 + OpenGL)"; - - 'main_loop: loop { - for event in events.poll_iter() { - match event { - sdl2::event::Event::Quit { .. } => break 'main_loop, - sdl2::event::Event::KeyDown { - keycode: Some(sdl2::keyboard::Keycode::Escape), - .. - } => break 'main_loop, - _ => {} - } - } - - let (w, h) = window.size(); - let angle = args - .angle - .unwrap_or(start.elapsed().as_secs_f32() * args.spin_rate); - let mvp = compute_mvp(w, h, args.fov_deg, center, camera_distance, angle); - - unsafe { - draw_frame( - gl, - program, - u_mvp, - u_use_tex, - u_tex, - a_pos, - a_uv, - vbo, - ebo, - vao, - texture, - index_count, - w, - h, - &mvp, - ); - } - window.gl_swap_window(); - - fps_frames = fps_frames.saturating_add(1); - let elapsed = fps_window_start.elapsed(); - if elapsed >= Duration::from_millis(500) { - let fps = fps_frames as f32 / elapsed.as_secs_f32().max(0.000_1); - let frame_time_ms = 1000.0 / fps.max(0.000_1); - let _ = window.set_title(&format!( - "{base_title} | FPS: {fps:.1} ({frame_time_ms:.2} ms)" - )); - print!("\rFPS: {fps:.1} ({frame_time_ms:.2} ms)"); - let _ = std::io::stdout().flush(); - fps_printed = true; - fps_frames = 0; - fps_window_start = Instant::now(); - } - } - - if fps_printed { - println!(); - } - - Ok(()) -} - -fn compute_mvp( - width: u32, - height: u32, - fov_deg: f32, - center: [f32; 3], - camera_distance: f32, - angle_rad: f32, -) -> [f32; 16] { - let aspect = (width as f32 / (height.max(1) as f32)).max(0.01); - let proj = mat4_perspective(fov_deg.to_radians(), aspect, 0.01, camera_distance * 10.0); - let view = mat4_translation(0.0, 0.0, -camera_distance); - let center_shift = mat4_translation(-center[0], -center[1], -center[2]); - let rot = mat4_rotation_y(angle_rad); - let model_m = mat4_mul(&rot, ¢er_shift); - let vp = mat4_mul(&view, &model_m); - mat4_mul(&proj, &vp) -} - -#[allow(clippy::too_many_arguments)] -unsafe fn draw_frame( - gl: &glow::Context, - program: glow::NativeProgram, - u_mvp: Option<&glow::NativeUniformLocation>, - u_use_tex: Option<&glow::NativeUniformLocation>, - u_tex: Option<&glow::NativeUniformLocation>, - a_pos: u32, - a_uv: u32, - vbo: glow::NativeBuffer, - ebo: glow::NativeBuffer, - vao: Option<glow::NativeVertexArray>, - texture: Option<&GpuTexture>, - index_count: usize, - width: u32, - height: u32, - mvp: &[f32; 16], -) { - gl.viewport( - 0, - 0, - width.min(i32::MAX as u32) as i32, - height.min(i32::MAX as u32) as i32, - ); - gl.enable(glow::DEPTH_TEST); - gl.clear_color(0.06, 0.08, 0.12, 1.0); - gl.clear(glow::COLOR_BUFFER_BIT | glow::DEPTH_BUFFER_BIT); - - gl.use_program(Some(program)); - gl.uniform_matrix_4_f32_slice(u_mvp, false, mvp); - - let texture_enabled = texture.is_some(); - gl.uniform_1_f32(u_use_tex, if texture_enabled { 1.0 } else { 0.0 }); - if let Some(tex) = texture { - gl.active_texture(glow::TEXTURE0); - gl.bind_texture(glow::TEXTURE_2D, Some(tex.handle)); - gl.uniform_1_i32(u_tex, 0); - } else { - gl.bind_texture(glow::TEXTURE_2D, None); - } - - if let Some(vao) = vao { - gl.bind_vertex_array(Some(vao)); - gl.draw_elements( - glow::TRIANGLES, - index_count.min(i32::MAX as usize) as i32, - glow::UNSIGNED_SHORT, - 0, - ); - gl.bind_vertex_array(None); - } else { - gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo)); - gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(ebo)); - gl.enable_vertex_attrib_array(a_pos); - gl.vertex_attrib_pointer_f32(a_pos, 3, glow::FLOAT, false, 20, 0); - gl.enable_vertex_attrib_array(a_uv); - gl.vertex_attrib_pointer_f32(a_uv, 2, glow::FLOAT, false, 20, 12); - gl.draw_elements( - glow::TRIANGLES, - index_count.min(i32::MAX as usize) as i32, - glow::UNSIGNED_SHORT, - 0, - ); - gl.disable_vertex_attrib_array(a_uv); - gl.disable_vertex_attrib_array(a_pos); - gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None); - gl.bind_buffer(glow::ARRAY_BUFFER, None); - } - gl.bind_texture(glow::TEXTURE_2D, None); - gl.use_program(None); -} - -unsafe fn read_pixels_rgba(gl: &glow::Context, width: u32, height: u32) -> Result<Vec<u8>, String> { - let pixel_count = usize::try_from(width) - .ok() - .and_then(|w| usize::try_from(height).ok().map(|h| w.saturating_mul(h))) - .ok_or_else(|| String::from("frame dimensions are too large"))?; - let mut pixels = vec![0u8; pixel_count.saturating_mul(4)]; - gl.read_pixels( - 0, - 0, - width.min(i32::MAX as u32) as i32, - height.min(i32::MAX as u32) as i32, - glow::RGBA, - glow::UNSIGNED_BYTE, - glow::PixelPackData::Slice(Some(pixels.as_mut_slice())), - ); - Ok(pixels) -} - -fn flip_image_y_rgba(rgba: &mut [u8], width: usize, height: usize) { - let stride = width.saturating_mul(4); - if stride == 0 { - return; - } - for y in 0..(height / 2) { - let top = y * stride; - let bottom = (height - 1 - y) * stride; - for i in 0..stride { - rgba.swap(top + i, bottom + i); - } - } -} - -fn save_png(path: &Path, width: u32, height: u32, rgba: Vec<u8>) -> Result<(), String> { - if let Some(parent) = path.parent() { - if !parent.as_os_str().is_empty() { - std::fs::create_dir_all(parent).map_err(|err| { - format!( - "failed to create output directory {}: {err}", - parent.display() - ) - })?; - } - } - let image = image::RgbaImage::from_raw(width, height, rgba) - .ok_or_else(|| String::from("failed to build image from framebuffer bytes"))?; - image - .save(path) - .map_err(|err| format!("failed to save PNG {}: {err}", path.display())) -} - -unsafe fn create_program( - gl: &glow::Context, - backend: GlBackend, -) -> Result<glow::NativeProgram, String> { - let (vs_src, fs_src) = match backend { - GlBackend::Gles2 => ( - r#" -attribute vec3 a_pos; -attribute vec2 a_uv; -uniform mat4 u_mvp; -varying vec2 v_uv; -void main() { - v_uv = a_uv; - gl_Position = u_mvp * vec4(a_pos, 1.0); -} -"#, - r#" -precision mediump float; -uniform sampler2D u_tex; -uniform float u_use_tex; -varying vec2 v_uv; -void main() { - vec4 base = vec4(0.85, 0.90, 1.00, 1.0); - vec4 texColor = texture2D(u_tex, v_uv); - gl_FragColor = mix(base, texColor, u_use_tex); -} -"#, - ), - GlBackend::Core33 => ( - r#"#version 330 core -in vec3 a_pos; -in vec2 a_uv; -uniform mat4 u_mvp; -out vec2 v_uv; -void main() { - v_uv = a_uv; - gl_Position = u_mvp * vec4(a_pos, 1.0); -} -"#, - r#"#version 330 core -uniform sampler2D u_tex; -uniform float u_use_tex; -in vec2 v_uv; -out vec4 fragColor; -void main() { - vec4 base = vec4(0.85, 0.90, 1.00, 1.0); - vec4 texColor = texture(u_tex, v_uv); - fragColor = mix(base, texColor, u_use_tex); -} -"#, - ), - }; - - let program = gl.create_program().map_err(|e| e.to_string())?; - let vs = gl - .create_shader(glow::VERTEX_SHADER) - .map_err(|e| e.to_string())?; - let fs = gl - .create_shader(glow::FRAGMENT_SHADER) - .map_err(|e| e.to_string())?; - - gl.shader_source(vs, vs_src); - gl.compile_shader(vs); - if !gl.get_shader_compile_status(vs) { - let log = gl.get_shader_info_log(vs); - gl.delete_shader(vs); - gl.delete_shader(fs); - gl.delete_program(program); - return Err(format!("vertex shader compile failed: {log}")); - } - - gl.shader_source(fs, fs_src); - gl.compile_shader(fs); - if !gl.get_shader_compile_status(fs) { - let log = gl.get_shader_info_log(fs); - gl.delete_shader(vs); - gl.delete_shader(fs); - gl.delete_program(program); - return Err(format!("fragment shader compile failed: {log}")); - } - - gl.attach_shader(program, vs); - gl.attach_shader(program, fs); - gl.link_program(program); - - gl.detach_shader(program, vs); - gl.detach_shader(program, fs); - gl.delete_shader(vs); - gl.delete_shader(fs); - - if !gl.get_program_link_status(program) { - let log = gl.get_program_info_log(program); - gl.delete_program(program); - return Err(format!("program link failed: {log}")); - } - - Ok(program) -} - -fn f32_slice_to_ne_bytes(slice: &[f32]) -> Vec<u8> { - let mut out = Vec::with_capacity(slice.len().saturating_mul(std::mem::size_of::<f32>())); - for &value in slice { - out.extend_from_slice(&value.to_ne_bytes()); - } - out -} - -fn u16_slice_to_ne_bytes(slice: &[u16]) -> Vec<u8> { - let mut out = Vec::with_capacity(slice.len().saturating_mul(std::mem::size_of::<u16>())); - for &value in slice { - out.extend_from_slice(&value.to_ne_bytes()); - } - out -} - -fn mat4_identity() -> [f32; 16] { - [ - 1.0, 0.0, 0.0, 0.0, // - 0.0, 1.0, 0.0, 0.0, // - 0.0, 0.0, 1.0, 0.0, // - 0.0, 0.0, 0.0, 1.0, // - ] -} - -fn mat4_translation(x: f32, y: f32, z: f32) -> [f32; 16] { - let mut m = mat4_identity(); - m[12] = x; - m[13] = y; - m[14] = z; - m -} - -fn mat4_rotation_y(rad: f32) -> [f32; 16] { - let c = rad.cos(); - let s = rad.sin(); - [ - c, 0.0, -s, 0.0, // - 0.0, 1.0, 0.0, 0.0, // - s, 0.0, c, 0.0, // - 0.0, 0.0, 0.0, 1.0, // - ] -} - -fn mat4_perspective(fovy: f32, aspect: f32, near: f32, far: f32) -> [f32; 16] { - let f = 1.0 / (0.5 * fovy).tan(); - let nf = 1.0 / (near - far); - [ - f / aspect, - 0.0, - 0.0, - 0.0, - 0.0, - f, - 0.0, - 0.0, - 0.0, - 0.0, - (far + near) * nf, - -1.0, - 0.0, - 0.0, - (2.0 * far * near) * nf, - 0.0, - ] -} - -fn mat4_mul(a: &[f32; 16], b: &[f32; 16]) -> [f32; 16] { - let mut out = [0.0f32; 16]; - for c in 0..4 { - for r in 0..4 { - let mut acc = 0.0f32; - for k in 0..4 { - acc += a[k * 4 + r] * b[c * 4 + k]; - } - out[c * 4 + r] = acc; - } - } - out -} |
