diff options
| author | Valentin Popov <valentin@popov.link> | 2026-06-22 12:12:27 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-06-22 12:13:32 +0300 |
| commit | d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448 (patch) | |
| tree | a0bd35c3940be62a5b5de1acc2366af377ffd181 /crates/render-mission-demo/src | |
| parent | 7416fdc7e9a48837fff5056e6dc8d0774e90964b (diff) | |
| download | fparkan-d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448.tar.xz fparkan-d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448.zip | |
feat: implement FParkan architecture foundation
Add the modular fparkan workspace, domain crates, adapters, apps, xtask policy/CI, acceptance evidence, and licensed corpus gates for the macOS-focused roadmap foundation.
Diffstat (limited to 'crates/render-mission-demo/src')
| -rw-r--r-- | crates/render-mission-demo/src/lib.rs | 881 | ||||
| -rw-r--r-- | crates/render-mission-demo/src/main.rs | 924 |
2 files changed, 0 insertions, 1805 deletions
diff --git a/crates/render-mission-demo/src/lib.rs b/crates/render-mission-demo/src/lib.rs deleted file mode 100644 index 9732f39..0000000 --- a/crates/render-mission-demo/src/lib.rs +++ /dev/null @@ -1,881 +0,0 @@ -use encoding_rs::WINDOWS_1251; -use nres::Archive; -use render_core::{build_render_mesh, RenderMesh}; -use render_demo::{load_model_with_name_from_archive, resolve_texture_for_model, LoadedTexture}; -use std::collections::HashMap; -use std::fmt; -use std::fs; -use std::path::{Path, PathBuf}; -use terrain_core::TerrainMesh; -use tma::MissionFile; - -const MAT0_KIND: u32 = 0x3054_414D; -const MESH_KIND: u32 = 0x4853_454D; -const OBJECT_REF_STRIDE: usize = 64; -const OBJECT_REF_ARCHIVE_BYTES: usize = 32; - -pub type Result<T> = core::result::Result<T, Error>; - -#[derive(Debug)] -pub enum Error { - Io(std::io::Error), - Mission(tma::Error), - Terrain(terrain_core::Error), - UnitDat(unitdat::Error), - RenderDemo(render_demo::Error), - Nres(nres::error::Error), - Texm(texm::error::Error), - InvalidMapPath(String), - GameRootNotFound(PathBuf), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Io(err) => write!(f, "{err}"), - Self::Mission(err) => write!(f, "{err}"), - Self::Terrain(err) => write!(f, "{err}"), - Self::UnitDat(err) => write!(f, "{err}"), - Self::RenderDemo(err) => write!(f, "{err}"), - Self::Nres(err) => write!(f, "{err}"), - Self::Texm(err) => write!(f, "{err}"), - Self::InvalidMapPath(path) => write!(f, "invalid mission map path: {path}"), - Self::GameRootNotFound(path) => { - write!( - f, - "failed to detect game root from mission path {}", - path.display() - ) - } - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::Io(err) => Some(err), - Self::Mission(err) => Some(err), - Self::Terrain(err) => Some(err), - Self::UnitDat(err) => Some(err), - Self::RenderDemo(err) => Some(err), - Self::Nres(err) => Some(err), - Self::Texm(err) => Some(err), - Self::InvalidMapPath(_) | Self::GameRootNotFound(_) => None, - } - } -} - -impl From<std::io::Error> for Error { - fn from(value: std::io::Error) -> Self { - Self::Io(value) - } -} - -impl From<tma::Error> for Error { - fn from(value: tma::Error) -> Self { - Self::Mission(value) - } -} - -impl From<terrain_core::Error> for Error { - fn from(value: terrain_core::Error) -> Self { - Self::Terrain(value) - } -} - -impl From<unitdat::Error> for Error { - fn from(value: unitdat::Error) -> Self { - Self::UnitDat(value) - } -} - -impl From<render_demo::Error> for Error { - fn from(value: render_demo::Error) -> Self { - Self::RenderDemo(value) - } -} - -impl From<nres::error::Error> for Error { - fn from(value: nres::error::Error) -> Self { - Self::Nres(value) - } -} - -impl From<texm::error::Error> for Error { - fn from(value: texm::error::Error) -> Self { - Self::Texm(value) - } -} - -#[derive(Copy, Clone, Debug)] -pub struct LoadOptions { - pub load_model_textures: bool, - pub load_terrain_texture: bool, -} - -impl Default for LoadOptions { - fn default() -> Self { - Self { - load_model_textures: true, - load_terrain_texture: true, - } - } -} - -#[derive(Clone, Debug)] -pub struct MissionScene { - pub game_root: PathBuf, - pub mission_path: PathBuf, - pub mission: MissionFile, - pub map_folder_rel: PathBuf, - pub land_msh_path: PathBuf, - pub terrain: TerrainMesh, - pub terrain_texture: Option<LoadedTexture>, - pub models: Vec<SceneModel>, - pub skipped_objects: usize, -} - -#[derive(Clone, Debug)] -pub struct SceneModel { - pub archive_path: PathBuf, - pub model_name: String, - pub mesh: RenderMesh, - pub texture: Option<LoadedTexture>, - pub instances: Vec<ModelInstance>, -} - -#[derive(Copy, Clone, Debug)] -pub struct ModelInstance { - pub position: [f32; 3], - pub yaw_rad: f32, - pub scale: [f32; 3], -} - -#[derive(Clone, Debug)] -struct ObjectPrototype { - archive_path: PathBuf, - model_name: String, -} - -#[derive(Clone, Debug)] -struct ObjectRef { - archive_name: String, - resource_name: String, -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -struct ModelKey { - archive_path: PathBuf, - model_name: String, -} - -pub fn detect_game_root_from_mission_path(mission_path: &Path) -> Option<PathBuf> { - let mut cursor = mission_path.parent(); - while let Some(dir) = cursor { - if dir.join("DATA").is_dir() && dir.join("objects.rlb").is_file() { - return Some(dir.to_path_buf()); - } - cursor = dir.parent(); - } - None -} - -pub fn load_scene( - game_root: impl AsRef<Path>, - mission_path: impl AsRef<Path>, -) -> Result<MissionScene> { - load_scene_with_options(game_root, mission_path, LoadOptions::default()) -} - -pub fn load_scene_with_options( - game_root: impl AsRef<Path>, - mission_path: impl AsRef<Path>, - options: LoadOptions, -) -> Result<MissionScene> { - let game_root = game_root.as_ref().to_path_buf(); - let mission_path = mission_path.as_ref().to_path_buf(); - - let mission = tma::parse_path(&mission_path)?; - let map_folder_rel = map_folder_from_footer(&mission.footer.map_path)?; - let land_msh_path = game_root.join(&map_folder_rel).join("Land.msh"); - let terrain = terrain_core::load_land_mesh(&land_msh_path)?; - let terrain_texture = if options.load_terrain_texture { - resolve_terrain_texture(&game_root, &map_folder_rel)? - } else { - None - }; - - let mut grouped_instances: HashMap<ModelKey, Vec<ModelInstance>> = HashMap::new(); - let mut prototype_cache: HashMap<String, Option<ObjectPrototype>> = HashMap::new(); - let mut skipped = 0usize; - - for object in &mission.objects { - let cache_key = object.resource_name.to_ascii_lowercase(); - let proto = if let Some(cached) = prototype_cache.get(&cache_key) { - cached.clone() - } else { - let resolved = resolve_object_prototype(&game_root, object)?; - prototype_cache.insert(cache_key, resolved.clone()); - resolved - }; - - let Some(proto) = proto else { - skipped += 1; - continue; - }; - - let instance = ModelInstance { - position: object.position, - yaw_rad: object.orientation[2], - scale: normalize_scale(object.scale), - }; - - grouped_instances - .entry(ModelKey { - archive_path: proto.archive_path, - model_name: proto.model_name, - }) - .or_default() - .push(instance); - } - - let mut models = Vec::new(); - for (key, instances) in grouped_instances { - let loaded = - match load_model_with_name_from_archive(&key.archive_path, Some(&key.model_name)) { - Ok(v) => v, - Err(_) => { - skipped += instances.len(); - continue; - } - }; - - let mesh = build_render_mesh(&loaded.model, 0, 0); - if mesh.indices.is_empty() { - skipped += instances.len(); - continue; - } - - let texture = if options.load_model_textures { - resolve_texture_for_model(&key.archive_path, &loaded.name, None, None, None, None) - .ok() - .flatten() - } else { - None - }; - - models.push(SceneModel { - archive_path: key.archive_path, - model_name: loaded.name, - mesh, - texture, - instances, - }); - } - - models.sort_by(|a, b| a.model_name.cmp(&b.model_name)); - - Ok(MissionScene { - game_root, - mission_path, - mission, - map_folder_rel, - land_msh_path, - terrain, - terrain_texture, - models, - skipped_objects: skipped, - }) -} - -pub fn compute_scene_bounds(scene: &MissionScene) -> Option<([f32; 3], [f32; 3])> { - let mut min_v = [f32::INFINITY; 3]; - let mut max_v = [f32::NEG_INFINITY; 3]; - let mut any = false; - - for pos in &scene.terrain.positions { - merge_bounds(&mut min_v, &mut max_v, *pos); - any = true; - } - - for model in &scene.models { - for instance in &model.instances { - merge_bounds(&mut min_v, &mut max_v, instance.position); - any = true; - } - } - - any.then_some((min_v, max_v)) -} - -fn merge_bounds(min_v: &mut [f32; 3], max_v: &mut [f32; 3], p: [f32; 3]) { - for i in 0..3 { - if p[i] < min_v[i] { - min_v[i] = p[i]; - } - if p[i] > max_v[i] { - max_v[i] = p[i]; - } - } -} - -fn normalize_scale(scale: [f32; 3]) -> [f32; 3] { - let mut out = scale; - for item in &mut out { - if !item.is_finite() || item.abs() < 0.000_1 { - *item = 1.0; - } - } - out -} - -fn map_folder_from_footer(map_path: &str) -> Result<PathBuf> { - let mut parts = split_relative_path(map_path); - if parts.len() < 2 { - return Err(Error::InvalidMapPath(map_path.to_string())); - } - parts.pop(); // remove 'land' - - let mut out = PathBuf::new(); - for part in parts { - out.push(part); - } - Ok(out) -} - -fn resolve_object_prototype( - game_root: &Path, - object: &tma::MissionObject, -) -> Result<Option<ObjectPrototype>> { - if object.resource_name.to_ascii_lowercase().ends_with(".dat") { - let dat_path = game_root.join(pathbuf_from_rel(&object.resource_name)); - if !dat_path.is_file() { - return Ok(None); - } - - let parsed = unitdat::parse_path(&dat_path)?; - let archive_path = game_root.join(pathbuf_from_rel(&parsed.archive_name)); - if !archive_path.is_file() { - return Ok(None); - } - return resolve_archive_model(game_root, &archive_path, &parsed.model_key); - } - - let archive_path = game_root.join("objects.rlb"); - if !archive_path.is_file() { - return Ok(None); - } - resolve_archive_model(game_root, &archive_path, &object.resource_name) -} - -fn resolve_archive_model( - game_root: &Path, - archive_path: &Path, - model_key: &str, -) -> Result<Option<ObjectPrototype>> { - if !archive_path.is_file() { - return Ok(None); - } - - if is_objects_registry_archive(archive_path) { - if let Some(proto) = resolve_objects_registry_model(game_root, archive_path, model_key)? { - return Ok(Some(proto)); - } - } - - let model_name = ensure_msh_suffix(model_key); - if !archive_has_mesh_entry(archive_path, &model_name)? { - return Ok(None); - } - - Ok(Some(ObjectPrototype { - archive_path: archive_path.to_path_buf(), - model_name: model_name.to_ascii_lowercase(), - })) -} - -fn is_objects_registry_archive(archive_path: &Path) -> bool { - archive_path - .file_name() - .and_then(|name| name.to_str()) - .is_some_and(|name| name.eq_ignore_ascii_case("objects.rlb")) -} - -fn resolve_objects_registry_model( - game_root: &Path, - registry_archive_path: &Path, - object_key: &str, -) -> Result<Option<ObjectPrototype>> { - let archive = Archive::open_path(registry_archive_path)?; - let Some(entry_id) = find_registry_entry_id(&archive, object_key) else { - return Ok(None); - }; - - let payload = archive.read(entry_id)?.into_owned(); - let refs = parse_object_refs(&payload); - if refs.is_empty() { - return Ok(None); - } - - for item in refs - .iter() - .filter(|item| has_extension(&item.resource_name, "msh")) - { - if let Some(proto) = resolve_object_ref_model(game_root, item, &item.resource_name)? { - return Ok(Some(proto)); - } - } - - for item in refs - .iter() - .filter(|item| has_extension(&item.resource_name, "bas")) - { - let Some(stem) = Path::new(&item.resource_name) - .file_stem() - .and_then(|stem| stem.to_str()) - else { - continue; - }; - if stem.is_empty() { - continue; - } - let candidate = format!("{stem}.msh"); - if let Some(proto) = resolve_object_ref_model(game_root, item, &candidate)? { - return Ok(Some(proto)); - } - } - - Ok(None) -} - -fn find_registry_entry_id(archive: &Archive, object_key: &str) -> Option<nres::EntryId> { - mesh_name_candidates(object_key) - .into_iter() - .find_map(|candidate| archive.find(&candidate)) -} - -fn resolve_object_ref_model( - game_root: &Path, - item: &ObjectRef, - model_name: &str, -) -> Result<Option<ObjectPrototype>> { - let archive_path = game_root.join(pathbuf_from_rel(&item.archive_name)); - if !archive_path.is_file() { - return Ok(None); - } - if !archive_has_mesh_entry(&archive_path, model_name)? { - return Ok(None); - } - - Ok(Some(ObjectPrototype { - archive_path, - model_name: model_name.to_ascii_lowercase(), - })) -} - -fn parse_object_refs(payload: &[u8]) -> Vec<ObjectRef> { - if !payload.len().is_multiple_of(OBJECT_REF_STRIDE) { - return Vec::new(); - } - - let mut refs = Vec::with_capacity(payload.len() / OBJECT_REF_STRIDE); - for chunk in payload.chunks_exact(OBJECT_REF_STRIDE) { - let archive_name = decode_cp1251_cstr(&chunk[..OBJECT_REF_ARCHIVE_BYTES]); - let resource_name = decode_cp1251_cstr(&chunk[OBJECT_REF_ARCHIVE_BYTES..]); - if archive_name.is_empty() || resource_name.is_empty() { - continue; - } - refs.push(ObjectRef { - archive_name, - resource_name, - }); - } - refs -} - -fn archive_has_mesh_entry(archive_path: &Path, requested_name: &str) -> Result<bool> { - let archive = Archive::open_path(archive_path)?; - Ok(find_mesh_entry_id(&archive, requested_name).is_some()) -} - -fn find_mesh_entry_id(archive: &Archive, requested_name: &str) -> Option<nres::EntryId> { - for candidate in mesh_name_candidates(requested_name) { - let Some(id) = archive.find(&candidate) else { - continue; - }; - let Some(entry) = archive.get(id) else { - continue; - }; - if entry.meta.kind == MESH_KIND || has_extension(&entry.meta.name, "msh") { - return Some(id); - } - } - None -} - -fn mesh_name_candidates(name: &str) -> Vec<String> { - let mut out = Vec::new(); - let trimmed = name.trim(); - if trimmed.is_empty() { - return out; - } - - push_unique_string(&mut out, trimmed.to_string()); - if let Some(stem) = trimmed - .strip_suffix(".msh") - .or_else(|| trimmed.strip_suffix(".MSH")) - { - if !stem.is_empty() { - push_unique_string(&mut out, stem.to_string()); - } - } else { - push_unique_string(&mut out, format!("{trimmed}.msh")); - } - - out -} - -fn push_unique_string(items: &mut Vec<String>, value: String) { - if !items.iter().any(|item| item.eq_ignore_ascii_case(&value)) { - items.push(value); - } -} - -fn ensure_msh_suffix(name: &str) -> String { - let trimmed = name.trim(); - if trimmed.to_ascii_lowercase().ends_with(".msh") { - trimmed.to_string() - } else { - format!("{trimmed}.msh") - } -} - -fn has_extension(name: &str, ext: &str) -> bool { - Path::new(name) - .extension() - .and_then(|value| value.to_str()) - .is_some_and(|value| value.eq_ignore_ascii_case(ext)) -} - -fn resolve_terrain_texture( - game_root: &Path, - map_folder_rel: &Path, -) -> Result<Option<LoadedTexture>> { - let material_archive_path = game_root.join("material.lib"); - let texture_archive_path = game_root.join("textures.lib"); - if !material_archive_path.is_file() || !texture_archive_path.is_file() { - return Ok(None); - } - - for wear_name in ["Land1.wea", "Land2.wea"] { - let wear_path = game_root.join(map_folder_rel).join(wear_name); - if !wear_path.is_file() { - continue; - } - let wear_payload = fs::read(&wear_path)?; - let Some(material_name) = parse_primary_material_from_wear(&wear_payload) else { - continue; - }; - let Some(texture_name) = - resolve_texture_name_from_material_archive(&material_archive_path, &material_name)? - else { - continue; - }; - if let Some(texture) = load_texm_by_name(&texture_archive_path, &texture_name)? { - return Ok(Some(texture)); - } - } - - Ok(None) -} - -fn parse_primary_material_from_wear(bytes: &[u8]) -> Option<String> { - let text = decode_cp1251(bytes).replace('\r', ""); - let mut lines = text.lines(); - let count = lines.next()?.trim().parse::<usize>().ok()?; - if count == 0 { - return None; - } - - for line in lines.take(count) { - let mut parts = line.split_whitespace(); - let _legacy = parts.next()?; - let name = parts.next()?; - if !name.is_empty() { - return Some(name.to_string()); - } - } - None -} - -fn resolve_texture_name_from_material_archive( - archive_path: &Path, - material_name: &str, -) -> Result<Option<String>> { - let archive = Archive::open_path(archive_path)?; - - let entry = if let Some(id) = archive.find(material_name) { - archive - .get(id) - .filter(|entry| entry.meta.kind == MAT0_KIND) - .or_else(|| { - archive - .find("DEFAULT") - .and_then(|id| archive.get(id)) - .filter(|entry| entry.meta.kind == MAT0_KIND) - }) - } else { - archive - .find("DEFAULT") - .and_then(|id| archive.get(id)) - .filter(|entry| entry.meta.kind == MAT0_KIND) - } - .or_else(|| archive.entries().find(|entry| entry.meta.kind == MAT0_KIND)); - - let Some(entry) = entry else { - return Ok(None); - }; - - let payload = archive.read(entry.id)?.into_owned(); - parse_primary_texture_name_from_mat0(&payload, entry.meta.attr2) -} - -fn parse_primary_texture_name_from_mat0(payload: &[u8], attr2: u32) -> Result<Option<String>> { - if payload.len() < 4 { - return Ok(None); - } - - 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.saturating_add(2); - } - if attr2 >= 3 { - offset = offset.saturating_add(4); - } - if attr2 >= 4 { - offset = offset.saturating_add(4); - } - - for phase in 0..phase_count { - let phase_off = offset.saturating_add(phase.saturating_mul(34)); - let Some(rec) = payload.get(phase_off..phase_off + 34) else { - break; - }; - let name_raw = &rec[18..34]; - let end = name_raw - .iter() - .position(|&b| b == 0) - .unwrap_or(name_raw.len()); - let name = decode_cp1251(&name_raw[..end]).trim().to_string(); - if !name.is_empty() { - return Ok(Some(name)); - } - } - - Ok(None) -} - -fn load_texm_by_name(archive_path: &Path, texture_name: &str) -> Result<Option<LoadedTexture>> { - let archive = Archive::open_path(archive_path)?; - let Some(id) = archive.find(texture_name) else { - return Ok(None); - }; - let Some(entry) = archive.get(id) else { - return Ok(None); - }; - if entry.meta.kind != texm::TEXM_MAGIC { - return Ok(None); - } - - let payload = archive.read(id)?.into_owned(); - let parsed = texm::parse_texm(&payload)?; - let decoded = texm::decode_mip_rgba8(&parsed, &payload, 0)?; - - Ok(Some(LoadedTexture { - name: entry.meta.name.clone(), - width: decoded.width, - height: decoded.height, - rgba8: decoded.rgba8, - })) -} - -fn split_relative_path(path: &str) -> Vec<&str> { - path.split(['\\', '/']) - .filter(|part| !part.is_empty()) - .collect() -} - -fn pathbuf_from_rel(path: &str) -> PathBuf { - let mut out = PathBuf::new(); - for part in split_relative_path(path) { - out.push(part); - } - out -} - -fn decode_cp1251_cstr(bytes: &[u8]) -> String { - let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len()); - let (decoded, _, _) = WINDOWS_1251.decode(&bytes[..end]); - decoded.trim().to_string() -} - -fn decode_cp1251(bytes: &[u8]) -> String { - let (decoded, _, _) = WINDOWS_1251.decode(bytes); - decoded.into_owned() -} - -#[cfg(test)] -mod tests { - use super::*; - use std::path::Path; - - fn game_root() -> Option<PathBuf> { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("testdata") - .join("Parkan - Iron Strategy"); - root.is_dir().then_some(root) - } - - #[test] - fn detects_game_root_from_mission_path() { - let Some(root) = game_root() else { - eprintln!("skipping: game root missing"); - return; - }; - - let mission = root - .join("MISSIONS") - .join("CAMPAIGN") - .join("CAMPAIGN.00") - .join("Mission.01") - .join("data.tma"); - if !mission.is_file() { - eprintln!("skipping missing mission sample"); - return; - } - - let detected = detect_game_root_from_mission_path(&mission) - .expect("failed to detect game root from mission path"); - assert_eq!(detected, root); - } - - #[test] - fn loads_scene_cpu_without_textures() { - let Some(root) = game_root() else { - eprintln!("skipping: game root missing"); - return; - }; - - let mission = root - .join("MISSIONS") - .join("CAMPAIGN") - .join("CAMPAIGN.00") - .join("Mission.01") - .join("data.tma"); - if !mission.is_file() { - eprintln!("skipping missing mission sample"); - return; - } - - let scene = load_scene_with_options( - &root, - &mission, - LoadOptions { - load_model_textures: false, - load_terrain_texture: false, - }, - ) - .unwrap_or_else(|err| panic!("failed to load scene {}: {err}", mission.display())); - - assert!(!scene.terrain.positions.is_empty()); - assert!(!scene.terrain.faces.is_empty()); - assert!(!scene.models.is_empty()); - - let instance_count = scene - .models - .iter() - .map(|model| model.instances.len()) - .sum::<usize>(); - assert!(instance_count >= 10); - - let bounds = compute_scene_bounds(&scene).expect("scene bounds should exist"); - assert!(bounds.0[0] <= bounds.1[0]); - assert!(bounds.0[1] <= bounds.1[1]); - assert!(bounds.0[2] <= bounds.1[2]); - } - - #[test] - fn loads_scene_with_textures() { - let Some(root) = game_root() else { - eprintln!("skipping: game root missing"); - return; - }; - - let mission = root - .join("MISSIONS") - .join("CAMPAIGN") - .join("CAMPAIGN.00") - .join("Mission.01") - .join("data.tma"); - if !mission.is_file() { - eprintln!("skipping missing mission sample"); - return; - } - - let scene = load_scene_with_options(&root, &mission, LoadOptions::default()) - .unwrap_or_else(|err| panic!("failed to load textured scene {}: {err}", mission.display())); - - assert!(!scene.models.is_empty()); - let textured_models = scene.models.iter().filter(|model| model.texture.is_some()).count(); - assert!(textured_models > 0, "no model textures resolved"); - assert!(scene.terrain_texture.is_some(), "terrain texture was not resolved"); - } - - #[test] - fn resolves_objects_registry_models() { - let Some(root) = game_root() else { - eprintln!("skipping: game root missing"); - return; - }; - - let registry = root.join("objects.rlb"); - if !registry.is_file() { - eprintln!("skipping missing objects.rlb"); - return; - } - - let cases = [ - ("r_h_01", "bases.rlb", "r_h_01.msh"), - ("s_tree_04", "static.rlb", "s_tree_0_04.msh"), - ("fr_m_brige", "fortif.rlb", "fr_m_brige.msh"), - ]; - - for (key, archive_name, model_name) in cases { - let proto = resolve_objects_registry_model(&root, ®istry, key) - .unwrap_or_else(|err| panic!("failed to resolve '{key}' from objects.rlb: {err}")) - .unwrap_or_else(|| panic!("missing model resolution for '{key}'")); - - let got_archive = proto - .archive_path - .file_name() - .and_then(|name| name.to_str()) - .map(|name| name.to_ascii_lowercase()) - .unwrap_or_default(); - assert_eq!(got_archive, archive_name.to_ascii_lowercase()); - assert!( - proto.model_name.eq_ignore_ascii_case(model_name), - "unexpected model for key '{key}': got '{}', expected '{}'", - proto.model_name, - model_name - ); - } - } -} diff --git a/crates/render-mission-demo/src/main.rs b/crates/render-mission-demo/src/main.rs deleted file mode 100644 index 01b6e06..0000000 --- a/crates/render-mission-demo/src/main.rs +++ /dev/null @@ -1,924 +0,0 @@ -use glow::HasContext as _; -use render_mission_demo::{ - compute_scene_bounds, detect_game_root_from_mission_path, load_scene_with_options, LoadOptions, - MissionScene, ModelInstance, -}; -use std::io::Write as _; -use std::path::PathBuf; -use std::time::{Duration, Instant}; - -struct Args { - mission: PathBuf, - game_root: Option<PathBuf>, - width: u32, - height: u32, - fov_deg: f32, - no_model_texture: bool, - no_terrain_texture: bool, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -enum GlBackend { - Gles2, - Core33, -} - -struct GpuTexture { - handle: glow::NativeTexture, -} - -struct GpuRenderable { - vbo: glow::NativeBuffer, - ebo: glow::NativeBuffer, - index_count: usize, - texture: Option<GpuTexture>, -} - -struct ModelRenderable { - gpu: GpuRenderable, - instances: Vec<ModelInstance>, -} - -#[derive(Copy, Clone, Debug)] -struct Camera { - position: [f32; 3], - yaw: f32, - pitch: f32, - move_speed: f32, - mouse_sensitivity: f32, -} - -fn parse_args() -> Result<Args, String> { - let mut mission = None; - let mut game_root = None; - let mut width = 1600u32; - let mut height = 900u32; - let mut fov_deg = 60.0f32; - let mut no_model_texture = false; - let mut no_terrain_texture = false; - - let mut it = std::env::args().skip(1); - while let Some(arg) = it.next() { - match arg.as_str() { - "--mission" => { - let value = it - .next() - .ok_or_else(|| String::from("missing value for --mission"))?; - mission = Some(PathBuf::from(value)); - } - "--game-root" => { - let value = it - .next() - .ok_or_else(|| String::from("missing value for --game-root"))?; - game_root = Some(PathBuf::from(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]")); - } - } - "--no-model-texture" => { - no_model_texture = true; - } - "--no-terrain-texture" => { - no_terrain_texture = true; - } - "--help" | "-h" => { - print_help(); - std::process::exit(0); - } - other => { - return Err(format!("unknown argument: {other}")); - } - } - } - - let mission = mission.ok_or_else(|| String::from("missing required --mission"))?; - Ok(Args { - mission, - game_root, - width, - height, - fov_deg, - no_model_texture, - no_terrain_texture, - }) -} - -fn print_help() { - eprintln!("parkan-render-mission-demo --mission <path/to/data.tma> [--game-root <path>] [--width W] [--height H] [--fov DEG]"); - eprintln!(" [--no-model-texture] [--no-terrain-texture]"); - eprintln!("controls: arrows/WASD move, PageUp/PageDown vertical move, Right Mouse drag look, Shift speed-up, Esc exit"); -} - -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 game_root = if let Some(path) = args.game_root.clone() { - path - } else { - detect_game_root_from_mission_path(&args.mission).ok_or_else(|| { - format!( - "failed to detect game root from mission path {} (use --game-root)", - args.mission.display() - ) - })? - }; - - let scene = load_scene_with_options( - &game_root, - &args.mission, - LoadOptions { - load_model_textures: !args.no_model_texture, - load_terrain_texture: !args.no_terrain_texture, - }, - ) - .map_err(|err| format!("failed to load mission scene: {err}"))?; - - let terrain_mesh = terrain_core::build_render_mesh(&scene.terrain) - .map_err(|err| format!("failed to build terrain render mesh: {err}"))?; - - let instance_count = scene - .models - .iter() - .map(|model| model.instances.len()) - .sum::<usize>(); - println!( - "mission loaded: map='{}', terrain_vertices={}, terrain_faces={}, models={}, instances={}, skipped={}", - scene.mission.footer.map_path, - scene.terrain.positions.len(), - scene.terrain.faces.len(), - scene.models.len(), - instance_count, - scene.skipped_objects - ); - - 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.width, args.height)?; - let _ = video.gl_set_swap_interval(1); - - 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 terrain_gpu = - unsafe { upload_terrain_renderable(&gl, &terrain_mesh, scene.terrain_texture.as_ref())? }; - - let mut model_gpus = Vec::new(); - for model in &scene.models { - let renderable = unsafe { upload_model_renderable(&gl, model)? }; - model_gpus.push(renderable); - } - - let (scene_center, scene_radius) = initial_scene_sphere(&scene); - let mut camera = Camera { - position: [ - scene_center[0], - scene_center[1] + scene_radius * 0.6, - scene_center[2] + scene_radius * 1.4, - ], - yaw: std::f32::consts::PI, - pitch: -0.28, - move_speed: (scene_radius * 0.55).max(60.0), - mouse_sensitivity: 0.005, - }; - - let mut events = sdl - .event_pump() - .map_err(|err| format!("failed to get SDL event pump: {err}"))?; - let mut last = Instant::now(); - let mut fps_window_start = Instant::now(); - let mut fps_frames = 0u32; - let mut fps_printed = false; - let mut mouse_look = false; - - '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, - sdl2::event::Event::MouseButtonDown { - mouse_btn: sdl2::mouse::MouseButton::Right, - .. - } => { - mouse_look = true; - sdl.mouse().set_relative_mouse_mode(true); - } - sdl2::event::Event::MouseButtonUp { - mouse_btn: sdl2::mouse::MouseButton::Right, - .. - } => { - mouse_look = false; - sdl.mouse().set_relative_mouse_mode(false); - } - sdl2::event::Event::MouseMotion { xrel, yrel, .. } if mouse_look => { - camera.yaw += xrel as f32 * camera.mouse_sensitivity; - camera.pitch -= yrel as f32 * camera.mouse_sensitivity; - camera.pitch = camera.pitch.clamp(-1.54, 1.54); - } - _ => {} - } - } - - let now = Instant::now(); - let dt = (now - last).as_secs_f32().clamp(0.0, 0.05); - last = now; - - update_camera(&events, &mut camera, dt); - - let (w, h) = window.size(); - let proj = mat4_perspective( - args.fov_deg.to_radians(), - (w as f32 / h.max(1) as f32).max(0.01), - 0.1, - (scene_radius * 25.0).max(5000.0), - ); - let forward = camera_forward(camera.yaw, camera.pitch); - let view = mat4_look_at( - camera.position, - [ - camera.position[0] + forward[0], - camera.position[1] + forward[1], - camera.position[2] + forward[2], - ], - [0.0, 1.0, 0.0], - ); - - unsafe { - draw_frame_begin(&gl, w, h); - - let terrain_mvp = mat4_mul(&proj, &view); - draw_gpu_renderable( - &gl, - program, - u_mvp.as_ref(), - u_use_tex.as_ref(), - u_tex.as_ref(), - a_pos, - a_uv, - &terrain_gpu, - &terrain_mvp, - ); - - for model in &model_gpus { - for instance in &model.instances { - let model_m = model_matrix(instance.position, instance.yaw_rad, instance.scale); - let view_model = mat4_mul(&view, &model_m); - let mvp = mat4_mul(&proj, &view_model); - draw_gpu_renderable( - &gl, - program, - u_mvp.as_ref(), - u_use_tex.as_ref(), - u_tex.as_ref(), - a_pos, - a_uv, - &model.gpu, - &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!( - "Parkan Mission Demo | FPS: {fps:.1} ({frame_time_ms:.2} ms) | objects: {instance_count}" - )); - 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!(); - } - - unsafe { - cleanup_renderable(&gl, terrain_gpu); - for model in model_gpus { - cleanup_renderable(&gl, model.gpu); - } - gl.delete_program(program); - } - - Ok(()) -} - -fn initial_scene_sphere(scene: &MissionScene) -> ([f32; 3], f32) { - if let Some((min_v, max_v)) = compute_scene_bounds(scene) { - let center = [ - 0.5 * (min_v[0] + max_v[0]), - 0.5 * (min_v[1] + max_v[1]), - 0.5 * (min_v[2] + max_v[2]), - ]; - let extent = [ - max_v[0] - min_v[0], - max_v[1] - min_v[1], - max_v[2] - min_v[2], - ]; - let radius = ((extent[0] * extent[0]) + (extent[1] * extent[1]) + (extent[2] * extent[2])) - .sqrt() - .max(10.0) - * 0.5; - return (center, radius); - } - ([0.0, 0.0, 0.0], 100.0) -} - -fn update_camera(events: &sdl2::EventPump, camera: &mut Camera, dt: f32) { - use sdl2::keyboard::Scancode; - - let keys = events.keyboard_state(); - let mut move_dir = [0.0f32, 0.0f32, 0.0f32]; - - let forward = camera_forward(camera.yaw, camera.pitch); - let right = normalize3(cross3(forward, [0.0, 1.0, 0.0])); - - if keys.is_scancode_pressed(Scancode::Up) || keys.is_scancode_pressed(Scancode::W) { - move_dir[0] += forward[0]; - move_dir[1] += forward[1]; - move_dir[2] += forward[2]; - } - if keys.is_scancode_pressed(Scancode::Down) || keys.is_scancode_pressed(Scancode::S) { - move_dir[0] -= forward[0]; - move_dir[1] -= forward[1]; - move_dir[2] -= forward[2]; - } - if keys.is_scancode_pressed(Scancode::Left) || keys.is_scancode_pressed(Scancode::A) { - move_dir[0] -= right[0]; - move_dir[1] -= right[1]; - move_dir[2] -= right[2]; - } - if keys.is_scancode_pressed(Scancode::Right) || keys.is_scancode_pressed(Scancode::D) { - move_dir[0] += right[0]; - move_dir[1] += right[1]; - move_dir[2] += right[2]; - } - if keys.is_scancode_pressed(Scancode::PageUp) || keys.is_scancode_pressed(Scancode::E) { - move_dir[1] += 1.0; - } - if keys.is_scancode_pressed(Scancode::PageDown) || keys.is_scancode_pressed(Scancode::Q) { - move_dir[1] -= 1.0; - } - - let shift = - keys.is_scancode_pressed(Scancode::LShift) || keys.is_scancode_pressed(Scancode::RShift); - let speed_mul = if shift { 3.0 } else { 1.0 }; - - let norm = normalize3(move_dir); - camera.position[0] += norm[0] * camera.move_speed * speed_mul * dt; - camera.position[1] += norm[1] * camera.move_speed * speed_mul * dt; - camera.position[2] += norm[2] * camera.move_speed * speed_mul * dt; -} - -unsafe fn upload_model_renderable( - gl: &glow::Context, - model: &render_mission_demo::SceneModel, -) -> Result<ModelRenderable, String> { - let mut vertex_data = Vec::with_capacity(model.mesh.vertices.len() * 5); - for vertex in &model.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 gpu = upload_gpu_renderable( - gl, - &vertex_data, - &model.mesh.indices, - model.texture.as_ref(), - )?; - - Ok(ModelRenderable { - gpu, - instances: model.instances.clone(), - }) -} - -unsafe fn upload_terrain_renderable( - gl: &glow::Context, - mesh: &terrain_core::TerrainRenderMesh, - texture: Option<&render_demo::LoadedTexture>, -) -> Result<GpuRenderable, String> { - 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]); - } - - upload_gpu_renderable(gl, &vertex_data, &mesh.indices, texture) -} - -unsafe fn upload_gpu_renderable( - gl: &glow::Context, - vertices: &[f32], - indices: &[u16], - texture: Option<&render_demo::LoadedTexture>, -) -> Result<GpuRenderable, String> { - let vbo = gl.create_buffer().map_err(|e| e.to_string())?; - let ebo = gl.create_buffer().map_err(|e| e.to_string())?; - - let vertex_bytes = f32_slice_to_ne_bytes(vertices); - let index_bytes = u16_slice_to_ne_bytes(indices); - - 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 gpu_texture = if let Some(texture) = texture { - Some(create_texture(gl, texture)?) - } else { - None - }; - - Ok(GpuRenderable { - vbo, - ebo, - index_count: indices.len(), - texture: gpu_texture, - }) -} - -unsafe fn cleanup_renderable(gl: &glow::Context, renderable: GpuRenderable) { - if let Some(tex) = renderable.texture { - gl.delete_texture(tex.handle); - } - gl.delete_buffer(renderable.ebo); - gl.delete_buffer(renderable.vbo); -} - -unsafe fn draw_frame_begin(gl: &glow::Context, width: u32, height: u32) { - 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); -} - -unsafe fn draw_gpu_renderable( - 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, - renderable: &GpuRenderable, - mvp: &[f32; 16], -) { - gl.use_program(Some(program)); - gl.uniform_matrix_4_f32_slice(u_mvp, false, mvp); - - let texture_enabled = renderable.texture.is_some(); - gl.uniform_1_f32(u_use_tex, if texture_enabled { 1.0 } else { 0.0 }); - - if let Some(tex) = &renderable.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); - } - - gl.bind_buffer(glow::ARRAY_BUFFER, Some(renderable.vbo)); - gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(renderable.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, - renderable.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); -} - -fn create_window_and_context( - video: &sdl2::VideoSubsystem, - width: u32, - height: u32, -) -> 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 Mission Demo", width, height); - window_builder.opengl().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_texture( - gl: &glow::Context, - texture: &render_demo::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 }) -} - -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.82, 0.87, 0.95, 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.82, 0.87, 0.95, 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 model_matrix(position: [f32; 3], yaw: f32, scale: [f32; 3]) -> [f32; 16] { - let translation = mat4_translation(position[0], position[1], position[2]); - let rotation = mat4_rotation_y(yaw); - let scaling = mat4_scale(scale[0], scale[1], scale[2]); - let tr = mat4_mul(&translation, &rotation); - mat4_mul(&tr, &scaling) -} - -fn camera_forward(yaw: f32, pitch: f32) -> [f32; 3] { - let cp = pitch.cos(); - normalize3([yaw.sin() * cp, pitch.sin(), yaw.cos() * cp]) -} - -fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] { - [ - a[1] * b[2] - a[2] * b[1], - a[2] * b[0] - a[0] * b[2], - a[0] * b[1] - a[1] * b[0], - ] -} - -fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 { - a[0] * b[0] + a[1] * b[1] + a[2] * b[2] -} - -fn normalize3(v: [f32; 3]) -> [f32; 3] { - let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt(); - if len <= 1e-6 { - [0.0, 0.0, 0.0] - } else { - [v[0] / len, v[1] / len, v[2] / len] - } -} - -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_scale(x: f32, y: f32, z: f32) -> [f32; 16] { - [ - x, 0.0, 0.0, 0.0, // - 0.0, y, 0.0, 0.0, // - 0.0, 0.0, z, 0.0, // - 0.0, 0.0, 0.0, 1.0, // - ] -} - -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_look_at(eye: [f32; 3], target: [f32; 3], up: [f32; 3]) -> [f32; 16] { - let f = normalize3([target[0] - eye[0], target[1] - eye[1], target[2] - eye[2]]); - let s = normalize3(cross3(f, up)); - let u = cross3(s, f); - - [ - s[0], - u[0], - -f[0], - 0.0, - s[1], - u[1], - -f[1], - 0.0, - s[2], - u[2], - -f[2], - 0.0, - -dot3(s, eye), - -dot3(u, eye), - dot3(f, eye), - 1.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 -} - -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 -} |
