diff options
Diffstat (limited to 'crates/render-demo')
| -rw-r--r-- | crates/render-demo/Cargo.toml | 1 | ||||
| -rw-r--r-- | crates/render-demo/src/lib.rs | 85 | ||||
| -rw-r--r-- | crates/render-demo/src/main.rs | 53 |
3 files changed, 119 insertions, 20 deletions
diff --git a/crates/render-demo/Cargo.toml b/crates/render-demo/Cargo.toml index 286b48c..94c2e46 100644 --- a/crates/render-demo/Cargo.toml +++ b/crates/render-demo/Cargo.toml @@ -8,6 +8,7 @@ default = [] demo = ["dep:sdl2", "dep:glow", "dep:image"] [dependencies] +encoding_rs = "0.8" msh-core = { path = "../msh-core" } nres = { path = "../nres" } render-core = { path = "../render-core" } diff --git a/crates/render-demo/src/lib.rs b/crates/render-demo/src/lib.rs index c5c72b5..c82e055 100644 --- a/crates/render-demo/src/lib.rs +++ b/crates/render-demo/src/lib.rs @@ -1,5 +1,7 @@ +use encoding_rs::WINDOWS_1251; use msh_core::{parse_model_payload, Model}; use nres::{Archive, EntryRef}; +use std::fmt; use std::path::{Path, PathBuf}; use texm::{decode_mip_rgba8, parse_texm}; @@ -22,6 +24,37 @@ pub enum Error { InvalidMaterial(String), } +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Nres(err) => write!(f, "{err}"), + Self::Msh(err) => write!(f, "{err}"), + Self::Texm(err) => write!(f, "{err}"), + Self::Io(err) => write!(f, "{err}"), + Self::NoMshEntries => write!(f, "archive does not contain .msh entries"), + Self::ModelNotFound(name) => write!(f, "model not found: {name}"), + Self::NoTexmEntries => write!(f, "archive does not contain Texm entries"), + Self::TextureNotFound(name) => write!(f, "texture not found: {name}"), + Self::MaterialNotFound(name) => write!(f, "material not found: {name}"), + Self::WearNotFound(name) => write!(f, "wear entry not found: {name}"), + Self::InvalidWear(reason) => write!(f, "invalid WEAR payload: {reason}"), + Self::InvalidMaterial(reason) => write!(f, "invalid MAT0 payload: {reason}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Nres(err) => Some(err), + Self::Msh(err) => Some(err), + Self::Texm(err) => Some(err), + Self::Io(err) => Some(err), + _ => None, + } + } +} + impl From<nres::error::Error> for Error { fn from(value: nres::error::Error) -> Self { Self::Nres(value) @@ -280,7 +313,7 @@ fn find_material_entry_with_fallback<'a>( } fn parse_wear_material_names(payload: &[u8]) -> Result<Vec<String>> { - let text = String::from_utf8_lossy(payload).replace('\r', ""); + let text = decode_cp1251(payload).replace('\r', ""); let mut lines = text.lines(); let Some(first) = lines.next() else { return Err(Error::InvalidWear(String::from("WEAR payload is empty"))); @@ -360,9 +393,7 @@ fn parse_primary_texture_name_from_mat0(payload: &[u8], attr2: u32) -> Result<Op .iter() .position(|&b| b == 0) .unwrap_or(name_raw.len()); - let name = String::from_utf8_lossy(&name_raw[..name_end]) - .trim() - .to_string(); + let name = decode_cp1251(&name_raw[..name_end]).trim().to_string(); if !name.is_empty() { return Ok(Some(name)); } @@ -371,6 +402,11 @@ fn parse_primary_texture_name_from_mat0(payload: &[u8], attr2: u32) -> Result<Op Ok(None) } +fn decode_cp1251(bytes: &[u8]) -> String { + let (decoded, _, _) = WINDOWS_1251.decode(bytes); + decoded.into_owned() +} + fn load_texture_from_archive_by_name(archive: &Archive, name: &str) -> Result<LoadedTexture> { let Some(id) = archive.find(name) else { return Err(Error::TextureNotFound(name.to_string())); @@ -524,4 +560,45 @@ mod tests { assert!(texture.width > 0 && texture.height > 0); assert!(!texture.rgba8.is_empty()); } + + #[test] + fn parse_wear_material_names_parses_counted_lines() { + let payload = b"2\r\n0 MAT_A\r\n1 MAT_B\r\n"; + let materials = + parse_wear_material_names(payload).expect("failed to parse valid WEAR payload"); + assert_eq!(materials, vec!["MAT_A".to_string(), "MAT_B".to_string()]); + } + + #[test] + fn parse_wear_material_names_rejects_invalid_payload() { + let payload = b"2\n0 ONLY_ONE\n"; + assert!(matches!( + parse_wear_material_names(payload), + Err(Error::InvalidWear(_)) + )); + } + + #[test] + fn parse_primary_texture_name_from_mat0_respects_attr2_layout() { + let mut payload = vec![0u8; 4 + 10 + 34]; + payload[0..2].copy_from_slice(&1u16.to_le_bytes()); // phase_count + // attr2=4 adds 10 bytes before phase table + let name = b"TEX_MAIN"; + payload[4 + 10 + 18..4 + 10 + 18 + name.len()].copy_from_slice(name); + + let parsed = parse_primary_texture_name_from_mat0(&payload, 4) + .expect("failed to parse MAT0 payload with attr2=4"); + assert_eq!(parsed, Some("TEX_MAIN".to_string())); + } + + #[test] + fn parse_primary_texture_name_from_mat0_decodes_cp1251_bytes() { + let mut payload = vec![0u8; 4 + 34]; + payload[0..2].copy_from_slice(&1u16.to_le_bytes()); // phase_count + payload[4 + 18] = 0xC0; // 'А' in CP1251 + + let parsed = + parse_primary_texture_name_from_mat0(&payload, 0).expect("failed to parse MAT0"); + assert_eq!(parsed, Some("А".to_string())); + } } diff --git a/crates/render-demo/src/main.rs b/crates/render-demo/src/main.rs index bb826d5..8d309d1 100644 --- a/crates/render-demo/src/main.rs +++ b/crates/render-demo/src/main.rs @@ -11,6 +11,7 @@ struct Args { group: usize, width: u32, height: u32, + fov_deg: f32, capture: Option<PathBuf>, angle: Option<f32>, spin_rate: f32, @@ -32,6 +33,7 @@ fn parse_args() -> Result<Args, String> { let mut group = 0usize; let mut width = 1280u32; let mut height = 720u32; + let mut fov_deg = 60.0f32; let mut capture = None; let mut angle = None; let mut spin_rate = 0.35f32; @@ -94,6 +96,17 @@ fn parse_args() -> Result<Args, String> { return Err(String::from("--height must be > 0")); } } + "--fov" => { + let value = it + .next() + .ok_or_else(|| String::from("missing value for --fov"))?; + fov_deg = value + .parse::<f32>() + .map_err(|_| String::from("invalid --fov value"))?; + if !(1.0..=179.0).contains(&fov_deg) { + return Err(String::from("--fov must be in range [1, 179]")); + } + } "--capture" => { let value = it .next() @@ -163,6 +176,7 @@ fn parse_args() -> Result<Args, String> { group, width, height, + fov_deg, capture, angle, spin_rate, @@ -176,7 +190,7 @@ fn parse_args() -> Result<Args, String> { fn print_help() { eprintln!( - "parkan-render-demo --archive <path> [--model <name.msh>] [--lod N] [--group N] [--width W] [--height H]" + "parkan-render-demo --archive <path> [--model <name.msh>] [--lod N] [--group N] [--width W] [--height H] [--fov DEG]" ); eprintln!(" [--capture <out.png>] [--angle RAD] [--spin-rate RAD_PER_SEC]"); eprintln!(" [--texture <name>] [--texture-archive <path>] [--material-archive <path>] [--wear <name.wea>] [--no-texture]"); @@ -202,7 +216,7 @@ fn run(args: Args) -> Result<(), String> { let loaded_model = load_model_with_name_from_archive(&args.archive, args.model.as_deref()) .map_err(|err| { format!( - "failed to load model from archive {}: {err:?}", + "failed to load model from archive {}: {err}", args.archive.display() ) })?; @@ -289,6 +303,7 @@ fn run(args: Args) -> Result<(), String> { vertex_data.push(vertex.uv0[0]); vertex_data.push(vertex.uv0[1]); } + let vertex_bytes = f32_slice_to_ne_bytes(&vertex_data); let gl = unsafe { glow::Context::from_loader_function(|name| video.gl_get_proc_address(name) as *const _) @@ -306,11 +321,7 @@ fn run(args: Args) -> Result<(), String> { let vbo = unsafe { gl.create_buffer().map_err(|e| e.to_string())? }; unsafe { gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo)); - gl.buffer_data_u8_slice( - glow::ARRAY_BUFFER, - cast_slice_u8(&vertex_data), - glow::STATIC_DRAW, - ); + gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, &vertex_bytes, glow::STATIC_DRAW); gl.bind_buffer(glow::ARRAY_BUFFER, None); } @@ -388,11 +399,9 @@ fn resolve_texture(args: &Args, model_name: &str) -> Result<Option<LoadedTexture || args.material_archive.is_some() || args.wear.is_some() { - Err(format!("failed to resolve texture: {err:?}")) + Err(format!("failed to resolve texture: {err}")) } else { - eprintln!( - "warning: auto texture resolve failed ({err:?}), fallback to solid color" - ); + eprintln!("warning: auto texture resolve failed ({err}), fallback to solid color"); Ok(None) } } @@ -451,7 +460,14 @@ fn run_capture( capture_path: &Path, ) -> Result<(), String> { let angle = args.angle.unwrap_or(0.0); - let mvp = compute_mvp(args.width, args.height, center, camera_distance, angle); + let mvp = compute_mvp( + args.width, + args.height, + args.fov_deg, + center, + camera_distance, + angle, + ); unsafe { draw_frame( gl, @@ -515,7 +531,7 @@ fn run_interactive( let angle = args .angle .unwrap_or(start.elapsed().as_secs_f32() * args.spin_rate); - let mvp = compute_mvp(w, h, center, camera_distance, angle); + let mvp = compute_mvp(w, h, args.fov_deg, center, camera_distance, angle); unsafe { draw_frame( @@ -543,12 +559,13 @@ fn run_interactive( fn compute_mvp( width: u32, height: u32, + fov_deg: f32, center: [f32; 3], camera_distance: f32, angle_rad: f32, ) -> [f32; 16] { let aspect = (width as f32 / (height.max(1) as f32)).max(0.01); - let proj = mat4_perspective(60.0_f32.to_radians(), aspect, 0.01, camera_distance * 10.0); + let proj = mat4_perspective(fov_deg.to_radians(), aspect, 0.01, camera_distance * 10.0); let view = mat4_translation(0.0, 0.0, -camera_distance); let center_shift = mat4_translation(-center[0], -center[1], -center[2]); let rot = mat4_rotation_y(angle_rad); @@ -733,8 +750,12 @@ void main() { Ok(program) } -fn cast_slice_u8<T>(slice: &[T]) -> &[u8] { - unsafe { std::slice::from_raw_parts(slice.as_ptr() as *const u8, std::mem::size_of_val(slice)) } +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 mat4_identity() -> [f32; 16] { |
