aboutsummaryrefslogtreecommitdiff
path: root/crates/render-mission-demo
diff options
context:
space:
mode:
Diffstat (limited to 'crates/render-mission-demo')
-rw-r--r--crates/render-mission-demo/Cargo.toml33
-rw-r--r--crates/render-mission-demo/src/lib.rs881
-rw-r--r--crates/render-mission-demo/src/main.rs924
3 files changed, 0 insertions, 1838 deletions
diff --git a/crates/render-mission-demo/Cargo.toml b/crates/render-mission-demo/Cargo.toml
deleted file mode 100644
index d658212..0000000
--- a/crates/render-mission-demo/Cargo.toml
+++ /dev/null
@@ -1,33 +0,0 @@
-[package]
-name = "render-mission-demo"
-version = "0.1.0"
-edition = "2021"
-
-[features]
-default = []
-demo = ["dep:sdl2", "dep:glow"]
-
-[dependencies]
-encoding_rs = "0.8"
-glow = { version = "0.16", optional = true }
-nres = { path = "../nres" }
-render-core = { path = "../render-core" }
-render-demo = { path = "../render-demo" }
-tma = { path = "../tma" }
-terrain-core = { path = "../terrain-core" }
-texm = { path = "../texm" }
-unitdat = { path = "../unitdat" }
-
-[dev-dependencies]
-common = { path = "../common" }
-
-[target.'cfg(target_os = "macos")'.dependencies]
-sdl2 = { version = "0.37", optional = true, default-features = false, features = ["use-pkgconfig"] }
-
-[target.'cfg(not(target_os = "macos"))'.dependencies]
-sdl2 = { version = "0.37", optional = true, default-features = false, features = ["bundled", "static-link"] }
-
-[[bin]]
-name = "parkan-render-mission-demo"
-path = "src/main.rs"
-required-features = ["demo"]
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, &registry, 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
-}