diff options
| author | Valentin Popov <valentin@popov.link> | 2026-02-19 04:19:18 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-02-19 04:19:18 +0300 |
| commit | a281ffa32ea615670d369503692f057b2dc60e6f (patch) | |
| tree | 321bcc1bfc489e2e7d0ff10dae620864ab08d054 /crates/render-demo/src | |
| parent | 18d4c6cf9fabc18282b29d103c8d30024f66e49b (diff) | |
| download | fparkan-a281ffa32ea615670d369503692f057b2dc60e6f.tar.xz fparkan-a281ffa32ea615670d369503692f057b2dc60e6f.zip | |
feat: Enhance model and texture loading with improved error handling and new features
- Introduced `LoadedModel` and `LoadedTexture` structs for better encapsulation of model and texture data.
- Added functions to load models and textures from archives, including support for resolving textures based on materials and wear entries.
- Implemented error handling for missing textures, materials, and wear entries.
- Updated the rendering pipeline to support texture loading and binding, including command-line arguments for texture customization.
- Enhanced the `texm` crate with new decoding capabilities for various pixel formats, including indexed textures.
- Added tests for texture decoding and loading to ensure reliability and correctness.
- Updated documentation to reflect changes in the material and texture resolution process.
Diffstat (limited to 'crates/render-demo/src')
| -rw-r--r-- | crates/render-demo/src/lib.rs | 422 | ||||
| -rw-r--r-- | crates/render-demo/src/main.rs | 235 |
2 files changed, 635 insertions, 22 deletions
diff --git a/crates/render-demo/src/lib.rs b/crates/render-demo/src/lib.rs index 4c73c09..c5c72b5 100644 --- a/crates/render-demo/src/lib.rs +++ b/crates/render-demo/src/lib.rs @@ -1,13 +1,25 @@ use msh_core::{parse_model_payload, Model}; -use nres::Archive; -use std::path::Path; +use nres::{Archive, EntryRef}; +use std::path::{Path, PathBuf}; +use texm::{decode_mip_rgba8, parse_texm}; + +const WEAR_KIND: u32 = 0x5241_4557; +const MAT0_KIND: u32 = 0x3054_414D; #[derive(Debug)] pub enum Error { Nres(nres::error::Error), Msh(msh_core::error::Error), + Texm(texm::error::Error), + Io(std::io::Error), NoMshEntries, ModelNotFound(String), + NoTexmEntries, + TextureNotFound(String), + MaterialNotFound(String), + WearNotFound(String), + InvalidWear(String), + InvalidMaterial(String), } impl From<nres::error::Error> for Error { @@ -22,9 +34,38 @@ impl From<msh_core::error::Error> for Error { } } +impl From<texm::error::Error> for Error { + fn from(value: texm::error::Error) -> Self { + Self::Texm(value) + } +} + +impl From<std::io::Error> for Error { + fn from(value: std::io::Error) -> Self { + Self::Io(value) + } +} + pub type Result<T> = core::result::Result<T, Error>; -pub fn load_model_from_archive(path: &Path, model_name: Option<&str>) -> Result<Model> { +#[derive(Clone, Debug)] +pub struct LoadedModel { + pub name: String, + pub model: Model, +} + +#[derive(Clone, Debug)] +pub struct LoadedTexture { + pub name: String, + pub width: u32, + pub height: u32, + pub rgba8: Vec<u8>, +} + +pub fn load_model_with_name_from_archive( + path: &Path, + model_name: Option<&str>, +) -> Result<LoadedModel> { let archive = Archive::open_path(path)?; let mut msh_entries = Vec::new(); for entry in archive.entries() { @@ -46,8 +87,313 @@ pub fn load_model_from_archive(path: &Path, model_name: Option<&str>) -> Result< msh_entries[0].0 }; + let target_name = archive + .get(target_id) + .map(|entry| entry.meta.name.clone()) + .unwrap_or_else(|| String::from("<unknown>")); let payload = archive.read(target_id)?; - Ok(parse_model_payload(payload.as_slice())?) + Ok(LoadedModel { + name: target_name, + model: parse_model_payload(payload.as_slice())?, + }) +} + +pub fn load_model_from_archive(path: &Path, model_name: Option<&str>) -> Result<Model> { + Ok(load_model_with_name_from_archive(path, model_name)?.model) +} + +pub fn load_texture_from_archive(path: &Path, texture_name: Option<&str>) -> Result<LoadedTexture> { + let archive = Archive::open_path(path)?; + if let Some(name) = texture_name { + return load_texture_from_archive_by_name(&archive, name); + } + + let mut texm_entries = archive + .entries() + .filter(|entry| entry.meta.kind == texm::TEXM_MAGIC) + .collect::<Vec<_>>(); + if texm_entries.is_empty() { + return Err(Error::NoTexmEntries); + } + texm_entries.sort_by(|a, b| { + a.meta + .name + .to_ascii_lowercase() + .cmp(&b.meta.name.to_ascii_lowercase()) + }); + let first = texm_entries[0]; + decode_texture_entry(&archive, first) +} + +pub fn resolve_texture_for_model( + model_archive_path: &Path, + model_entry_name: &str, + texture_name_override: Option<&str>, + textures_archive_override: Option<&Path>, + material_archive_override: Option<&Path>, + wear_entry_override: Option<&str>, +) -> Result<Option<LoadedTexture>> { + if let Some(name) = texture_name_override { + return load_texture_by_name_from_candidate_archives( + name, + candidate_texture_archives(model_archive_path, textures_archive_override), + ) + .map(Some); + } + + let wear_entry_name = if let Some(name) = wear_entry_override { + name.to_string() + } else { + derive_wear_entry_name(model_entry_name).ok_or_else(|| { + Error::WearNotFound(format!( + "cannot derive WEAR name from model '{model_entry_name}'" + )) + })? + }; + + let model_archive = Archive::open_path(model_archive_path)?; + let wear_materials = parse_wear_material_names( + read_entry_by_name_kind(&model_archive, &wear_entry_name, WEAR_KIND)? + .0 + .as_slice(), + )?; + let Some(primary_material) = wear_materials.first() else { + return Ok(None); + }; + + let material_path = if let Some(path) = material_archive_override { + path.to_path_buf() + } else { + sibling_archive_path(model_archive_path, "material.lib") + .ok_or_else(|| Error::MaterialNotFound(String::from("material.lib")))? + }; + let material_archive = Archive::open_path(&material_path)?; + let material_entry = find_material_entry_with_fallback(&material_archive, primary_material)?; + let material_payload = material_archive.read(material_entry.id)?.into_owned(); + let texture_name = + parse_primary_texture_name_from_mat0(&material_payload, material_entry.meta.attr2)?; + let Some(texture_name) = texture_name else { + return Ok(None); + }; + + let texture = load_texture_by_name_from_candidate_archives( + &texture_name, + candidate_texture_archives(model_archive_path, textures_archive_override), + )?; + Ok(Some(texture)) +} + +fn load_texture_by_name_from_candidate_archives( + texture_name: &str, + archives: Vec<PathBuf>, +) -> Result<LoadedTexture> { + let mut last_not_found = None; + for archive_path in archives { + if !archive_path.is_file() { + continue; + } + let archive = Archive::open_path(&archive_path)?; + match load_texture_from_archive_by_name(&archive, texture_name) { + Ok(texture) => return Ok(texture), + Err(Error::TextureNotFound(name)) => { + last_not_found = Some(name); + } + Err(other) => return Err(other), + } + } + + Err(Error::TextureNotFound( + last_not_found.unwrap_or_else(|| texture_name.to_string()), + )) +} + +fn candidate_texture_archives( + model_archive_path: &Path, + textures_archive_override: Option<&Path>, +) -> Vec<PathBuf> { + if let Some(path) = textures_archive_override { + return vec![path.to_path_buf()]; + } + + let mut out = Vec::new(); + if let Some(path) = sibling_archive_path(model_archive_path, "textures.lib") { + out.push(path); + } + if let Some(path) = sibling_archive_path(model_archive_path, "lightmap.lib") { + out.push(path); + } + out +} + +fn sibling_archive_path(model_archive_path: &Path, name: &str) -> Option<PathBuf> { + let parent = model_archive_path.parent()?; + Some(parent.join(name)) +} + +fn derive_wear_entry_name(model_entry_name: &str) -> Option<String> { + let stem = model_entry_name.rsplit_once('.').map(|(left, _)| left)?; + Some(format!("{stem}.wea")) +} + +fn read_entry_by_name_kind( + archive: &Archive, + name: &str, + expected_kind: u32, +) -> Result<(Vec<u8>, String)> { + let Some(id) = archive.find(name) else { + return Err(Error::WearNotFound(name.to_string())); + }; + let Some(entry) = archive.get(id) else { + return Err(Error::WearNotFound(name.to_string())); + }; + if entry.meta.kind != expected_kind { + return Err(Error::WearNotFound(name.to_string())); + } + let payload = archive.read(id)?.into_owned(); + Ok((payload, entry.meta.name.clone())) +} + +fn find_material_entry_with_fallback<'a>( + archive: &'a Archive, + requested_name: &str, +) -> Result<EntryRef<'a>> { + if let Some(id) = archive.find(requested_name) { + if let Some(entry) = archive.get(id) { + if entry.meta.kind == MAT0_KIND { + return Ok(entry); + } + } + } + + if let Some(id) = archive.find("DEFAULT") { + if let Some(entry) = archive.get(id) { + if entry.meta.kind == MAT0_KIND { + return Ok(entry); + } + } + } + + let Some(entry) = archive.entries().find(|entry| entry.meta.kind == MAT0_KIND) else { + return Err(Error::MaterialNotFound(requested_name.to_string())); + }; + Ok(entry) +} + +fn parse_wear_material_names(payload: &[u8]) -> Result<Vec<String>> { + let text = String::from_utf8_lossy(payload).replace('\r', ""); + let mut lines = text.lines(); + let Some(first) = lines.next() else { + return Err(Error::InvalidWear(String::from("WEAR payload is empty"))); + }; + let count = first + .trim() + .parse::<usize>() + .map_err(|_| Error::InvalidWear(format!("invalid wearCount line: '{first}'")))?; + if count == 0 { + return Err(Error::InvalidWear(String::from("wearCount must be > 0"))); + } + + let mut materials = Vec::with_capacity(count); + for idx in 0..count { + let Some(line) = lines.next() else { + return Err(Error::InvalidWear(format!( + "missing material line {idx} of {count}" + ))); + }; + let mut parts = line.split_whitespace(); + let _legacy = parts + .next() + .ok_or_else(|| Error::InvalidWear(format!("invalid material line {idx}: '{line}'")))?; + let name = parts + .next() + .ok_or_else(|| Error::InvalidWear(format!("invalid material line {idx}: '{line}'")))?; + materials.push(name.to_string()); + } + + Ok(materials) +} + +fn parse_primary_texture_name_from_mat0(payload: &[u8], attr2: u32) -> Result<Option<String>> { + if payload.len() < 4 { + return Err(Error::InvalidMaterial(String::from( + "MAT0 payload is too small for header", + ))); + } + let phase_count = u16::from_le_bytes([payload[0], payload[1]]) as usize; + if phase_count == 0 { + return Ok(None); + } + + let mut offset = 4usize; + if attr2 >= 2 { + offset = offset + .checked_add(2) + .ok_or_else(|| Error::InvalidMaterial(String::from("MAT0 offset overflow")))?; + } + if attr2 >= 3 { + offset = offset + .checked_add(4) + .ok_or_else(|| Error::InvalidMaterial(String::from("MAT0 offset overflow")))?; + } + if attr2 >= 4 { + offset = offset + .checked_add(4) + .ok_or_else(|| Error::InvalidMaterial(String::from("MAT0 offset overflow")))?; + } + + for phase in 0..phase_count { + let phase_off = offset + .checked_add(phase.checked_mul(34).ok_or_else(|| { + Error::InvalidMaterial(String::from("MAT0 phase offset overflow")) + })?) + .ok_or_else(|| Error::InvalidMaterial(String::from("MAT0 phase offset overflow")))?; + let phase_end = phase_off + .checked_add(34) + .ok_or_else(|| Error::InvalidMaterial(String::from("MAT0 phase offset overflow")))?; + let Some(rec) = payload.get(phase_off..phase_end) else { + return Err(Error::InvalidMaterial(format!( + "MAT0 phase {phase} is out of bounds" + ))); + }; + let name_raw = &rec[18..34]; + let name_end = name_raw + .iter() + .position(|&b| b == 0) + .unwrap_or(name_raw.len()); + let name = String::from_utf8_lossy(&name_raw[..name_end]) + .trim() + .to_string(); + if !name.is_empty() { + return Ok(Some(name)); + } + } + + Ok(None) +} + +fn load_texture_from_archive_by_name(archive: &Archive, name: &str) -> Result<LoadedTexture> { + let Some(id) = archive.find(name) else { + return Err(Error::TextureNotFound(name.to_string())); + }; + let Some(entry) = archive.get(id) else { + return Err(Error::TextureNotFound(name.to_string())); + }; + if entry.meta.kind != texm::TEXM_MAGIC { + return Err(Error::TextureNotFound(name.to_string())); + } + decode_texture_entry(archive, entry) +} + +fn decode_texture_entry(archive: &Archive, entry: EntryRef<'_>) -> Result<LoadedTexture> { + let payload = archive.read(entry.id)?.into_owned(); + let parsed = parse_texm(&payload)?; + let decoded = decode_mip_rgba8(&parsed, &payload, 0)?; + Ok(LoadedTexture { + name: entry.meta.name.clone(), + width: decoded.width, + height: decoded.height, + rgba8: decoded.rgba8, + }) } #[cfg(test)] @@ -98,6 +444,19 @@ mod tests { None } + fn game_root() -> Option<PathBuf> { + let path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("testdata") + .join("Parkan - Iron Strategy"); + if path.is_dir() { + Some(path) + } else { + None + } + } + #[test] fn load_model_from_real_archive() { let Some(path) = archive_with_msh() else { @@ -110,4 +469,59 @@ mod tests { assert!(!model.positions.is_empty()); assert!(!model.indices.is_empty()); } + + #[test] + fn resolve_texture_for_real_model_via_wear_and_material() { + let Some(root) = game_root() else { + eprintln!( + "skipping resolve_texture_for_real_model_via_wear_and_material: no game root" + ); + return; + }; + let archive = root.join("animals.rlb"); + if !archive.is_file() { + eprintln!("skipping resolve_texture_for_real_model_via_wear_and_material: missing animals.rlb"); + return; + } + + let loaded = load_model_with_name_from_archive(&archive, Some("A_L_01.msh")) + .unwrap_or_else(|err| { + panic!( + "failed to load model A_L_01.msh from {}: {err:?}", + archive.display() + ) + }); + let texture = resolve_texture_for_model(&archive, &loaded.name, None, None, None, None) + .unwrap_or_else(|err| panic!("failed to resolve texture for {}: {err:?}", loaded.name)) + .expect("texture must be resolved for A_L_01.msh"); + assert!(texture.width > 0 && texture.height > 0); + assert_eq!( + texture.rgba8.len(), + usize::try_from(texture.width) + .ok() + .and_then(|w| usize::try_from(texture.height).ok().map(|h| w * h * 4)) + .unwrap_or(0) + ); + } + + #[test] + fn load_first_texture_from_real_archive() { + let Some(root) = game_root() else { + eprintln!("skipping load_first_texture_from_real_archive: no game root"); + return; + }; + let archive = root.join("textures.lib"); + if !archive.is_file() { + eprintln!("skipping load_first_texture_from_real_archive: missing textures.lib"); + return; + } + let texture = load_texture_from_archive(&archive, None).unwrap_or_else(|err| { + panic!( + "failed to load first texture from {}: {err:?}", + archive.display() + ) + }); + assert!(texture.width > 0 && texture.height > 0); + assert!(!texture.rgba8.is_empty()); + } } diff --git a/crates/render-demo/src/main.rs b/crates/render-demo/src/main.rs index 5bb0a58..bb826d5 100644 --- a/crates/render-demo/src/main.rs +++ b/crates/render-demo/src/main.rs @@ -1,6 +1,6 @@ use glow::HasContext as _; -use render_core::{build_render_mesh, compute_bounds}; -use render_demo::load_model_from_archive; +use render_core::{build_render_mesh, compute_bounds_for_mesh}; +use render_demo::{load_model_with_name_from_archive, resolve_texture_for_model, LoadedTexture}; use std::path::{Path, PathBuf}; use std::time::Instant; @@ -14,6 +14,15 @@ struct Args { capture: Option<PathBuf>, angle: Option<f32>, spin_rate: f32, + texture: Option<String>, + texture_archive: Option<PathBuf>, + material_archive: Option<PathBuf>, + wear: Option<String>, + no_texture: bool, +} + +struct GpuTexture { + handle: glow::NativeTexture, } fn parse_args() -> Result<Args, String> { @@ -26,6 +35,11 @@ fn parse_args() -> Result<Args, String> { let mut capture = None; let mut angle = None; let mut spin_rate = 0.35f32; + let mut texture = None; + let mut texture_archive = None; + let mut material_archive = None; + let mut wear = None; + let mut no_texture = false; let mut it = std::env::args().skip(1); while let Some(arg) = it.next() { @@ -104,6 +118,33 @@ fn parse_args() -> Result<Args, String> { .parse::<f32>() .map_err(|_| String::from("invalid --spin-rate value"))?; } + "--texture" => { + let value = it + .next() + .ok_or_else(|| String::from("missing value for --texture"))?; + texture = Some(value); + } + "--texture-archive" => { + let value = it + .next() + .ok_or_else(|| String::from("missing value for --texture-archive"))?; + texture_archive = Some(PathBuf::from(value)); + } + "--material-archive" => { + let value = it + .next() + .ok_or_else(|| String::from("missing value for --material-archive"))?; + material_archive = Some(PathBuf::from(value)); + } + "--wear" => { + let value = it + .next() + .ok_or_else(|| String::from("missing value for --wear"))?; + wear = Some(value); + } + "--no-texture" => { + no_texture = true; + } "--help" | "-h" => { print_help(); std::process::exit(0); @@ -125,6 +166,11 @@ fn parse_args() -> Result<Args, String> { capture, angle, spin_rate, + texture, + texture_archive, + material_archive, + wear, + no_texture, }) } @@ -133,6 +179,7 @@ fn print_help() { "parkan-render-demo --archive <path> [--model <name.msh>] [--lod N] [--group N] [--width W] [--height H]" ); eprintln!(" [--capture <out.png>] [--angle RAD] [--spin-rate RAD_PER_SEC]"); + eprintln!(" [--texture <name>] [--texture-archive <path>] [--material-archive <path>] [--wear <name.wea>] [--no-texture]"); } fn main() { @@ -152,24 +199,34 @@ fn main() { } fn run(args: Args) -> Result<(), String> { - let model = load_model_from_archive(&args.archive, args.model.as_deref()).map_err(|err| { - format!( - "failed to load model from archive {}: {err:?}", - args.archive.display() - ) - })?; - - let mesh = build_render_mesh(&model, args.lod, args.group); + let loaded_model = load_model_with_name_from_archive(&args.archive, args.model.as_deref()) + .map_err(|err| { + format!( + "failed to load model from archive {}: {err:?}", + args.archive.display() + ) + })?; + let mesh = build_render_mesh(&loaded_model.model, args.lod, args.group); if mesh.vertices.is_empty() { return Err(format!( "model has no renderable triangles for lod={} group={}", args.lod, args.group )); } - let Some((bounds_min, bounds_max)) = compute_bounds(&mesh.vertices) else { + let Some((bounds_min, bounds_max)) = compute_bounds_for_mesh(&mesh.vertices) else { return Err(String::from("failed to compute mesh bounds")); }; + let resolved_texture = resolve_texture(&args, &loaded_model.name)?; + if let Some(tex) = resolved_texture.as_ref() { + println!( + "resolved texture '{}' ({}x{})", + tex.name, tex.width, tex.height + ); + } else { + println!("texture path disabled or unresolved; rendering with fallback color"); + } + let center = [ 0.5 * (bounds_min[0] + bounds_max[0]), 0.5 * (bounds_min[1] + bounds_max[1]), @@ -224,9 +281,13 @@ fn run(args: Args) -> Result<(), String> { video.gl_set_swap_interval(1) }; - let mut vertices_flat = Vec::with_capacity(mesh.vertices.len() * 3); - for pos in &mesh.vertices { - vertices_flat.extend_from_slice(pos); + let mut vertex_data = Vec::with_capacity(mesh.vertices.len() * 5); + for vertex in &mesh.vertices { + vertex_data.push(vertex.position[0]); + vertex_data.push(vertex.position[1]); + vertex_data.push(vertex.position[2]); + vertex_data.push(vertex.uv0[0]); + vertex_data.push(vertex.uv0[1]); } let gl = unsafe { @@ -235,27 +296,41 @@ fn run(args: Args) -> Result<(), String> { let program = unsafe { create_program(&gl)? }; let u_mvp = unsafe { gl.get_uniform_location(program, "u_mvp") }; + let u_use_tex = unsafe { gl.get_uniform_location(program, "u_use_tex") }; + let u_tex = unsafe { gl.get_uniform_location(program, "u_tex") }; let a_pos = unsafe { gl.get_attrib_location(program, "a_pos") } .ok_or_else(|| String::from("shader attribute a_pos is missing"))?; + let a_uv = unsafe { gl.get_attrib_location(program, "a_uv") } + .ok_or_else(|| String::from("shader attribute a_uv is missing"))?; let vbo = unsafe { gl.create_buffer().map_err(|e| e.to_string())? }; unsafe { gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo)); gl.buffer_data_u8_slice( glow::ARRAY_BUFFER, - cast_slice_u8(&vertices_flat), + cast_slice_u8(&vertex_data), glow::STATIC_DRAW, ); gl.bind_buffer(glow::ARRAY_BUFFER, None); } + let gpu_texture = if let Some(texture) = resolved_texture.as_ref() { + Some(unsafe { create_texture(&gl, texture)? }) + } else { + None + }; + let result = if let Some(capture_path) = args.capture.as_ref() { run_capture( &gl, program, u_mvp.as_ref(), + u_use_tex.as_ref(), + u_tex.as_ref(), a_pos, + a_uv, vbo, + gpu_texture.as_ref(), mesh.vertices.len(), &args, center, @@ -269,8 +344,12 @@ fn run(args: Args) -> Result<(), String> { &gl, program, u_mvp.as_ref(), + u_use_tex.as_ref(), + u_tex.as_ref(), a_pos, + a_uv, vbo, + gpu_texture.as_ref(), mesh.vertices.len(), &args, center, @@ -279,6 +358,9 @@ fn run(args: Args) -> Result<(), String> { }; unsafe { + if let Some(texture) = gpu_texture { + gl.delete_texture(texture.handle); + } gl.delete_buffer(vbo); gl.delete_program(program); } @@ -286,13 +368,82 @@ fn run(args: Args) -> Result<(), String> { result } +fn resolve_texture(args: &Args, model_name: &str) -> Result<Option<LoadedTexture>, String> { + if args.no_texture { + return Ok(None); + } + + match resolve_texture_for_model( + &args.archive, + model_name, + args.texture.as_deref(), + args.texture_archive.as_deref(), + args.material_archive.as_deref(), + args.wear.as_deref(), + ) { + Ok(texture) => Ok(texture), + Err(err) => { + if args.texture.is_some() + || args.texture_archive.is_some() + || args.material_archive.is_some() + || args.wear.is_some() + { + Err(format!("failed to resolve texture: {err:?}")) + } else { + eprintln!( + "warning: auto texture resolve failed ({err:?}), fallback to solid color" + ); + Ok(None) + } + } + } +} + +unsafe fn create_texture( + gl: &glow::Context, + texture: &LoadedTexture, +) -> Result<GpuTexture, String> { + let handle = gl.create_texture().map_err(|e| e.to_string())?; + gl.bind_texture(glow::TEXTURE_2D, Some(handle)); + gl.tex_parameter_i32( + glow::TEXTURE_2D, + glow::TEXTURE_MIN_FILTER, + glow::LINEAR as i32, + ); + gl.tex_parameter_i32( + glow::TEXTURE_2D, + glow::TEXTURE_MAG_FILTER, + glow::LINEAR as i32, + ); + gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::REPEAT as i32); + gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::REPEAT as i32); + gl.pixel_store_i32(glow::UNPACK_ALIGNMENT, 1); + gl.tex_image_2d( + glow::TEXTURE_2D, + 0, + glow::RGBA as i32, + texture.width.min(i32::MAX as u32) as i32, + texture.height.min(i32::MAX as u32) as i32, + 0, + glow::RGBA, + glow::UNSIGNED_BYTE, + glow::PixelUnpackData::Slice(Some(texture.rgba8.as_slice())), + ); + gl.bind_texture(glow::TEXTURE_2D, None); + Ok(GpuTexture { handle }) +} + #[allow(clippy::too_many_arguments)] fn run_capture( gl: &glow::Context, program: glow::NativeProgram, u_mvp: Option<&glow::NativeUniformLocation>, + u_use_tex: Option<&glow::NativeUniformLocation>, + u_tex: Option<&glow::NativeUniformLocation>, a_pos: u32, + a_uv: u32, vbo: glow::NativeBuffer, + texture: Option<&GpuTexture>, vertex_count: usize, args: &Args, center: [f32; 3], @@ -306,8 +457,12 @@ fn run_capture( gl, program, u_mvp, + u_use_tex, + u_tex, a_pos, + a_uv, vbo, + texture, vertex_count, args.width, args.height, @@ -328,8 +483,12 @@ fn run_interactive( gl: &glow::Context, program: glow::NativeProgram, u_mvp: Option<&glow::NativeUniformLocation>, + u_use_tex: Option<&glow::NativeUniformLocation>, + u_tex: Option<&glow::NativeUniformLocation>, a_pos: u32, + a_uv: u32, vbo: glow::NativeBuffer, + texture: Option<&GpuTexture>, vertex_count: usize, args: &Args, center: [f32; 3], @@ -359,7 +518,21 @@ fn run_interactive( let mvp = compute_mvp(w, h, center, camera_distance, angle); unsafe { - draw_frame(gl, program, u_mvp, a_pos, vbo, vertex_count, w, h, &mvp); + draw_frame( + gl, + program, + u_mvp, + u_use_tex, + u_tex, + a_pos, + a_uv, + vbo, + texture, + vertex_count, + w, + h, + &mvp, + ); } window.gl_swap_window(); } @@ -389,8 +562,12 @@ unsafe fn draw_frame( gl: &glow::Context, program: glow::NativeProgram, u_mvp: Option<&glow::NativeUniformLocation>, + u_use_tex: Option<&glow::NativeUniformLocation>, + u_tex: Option<&glow::NativeUniformLocation>, a_pos: u32, + a_uv: u32, vbo: glow::NativeBuffer, + texture: Option<&GpuTexture>, vertex_count: usize, width: u32, height: u32, @@ -409,16 +586,30 @@ unsafe fn draw_frame( gl.use_program(Some(program)); gl.uniform_matrix_4_f32_slice(u_mvp, false, mvp); + let texture_enabled = texture.is_some(); + gl.uniform_1_f32(u_use_tex, if texture_enabled { 1.0 } else { 0.0 }); + if let Some(tex) = texture { + gl.active_texture(glow::TEXTURE0); + gl.bind_texture(glow::TEXTURE_2D, Some(tex.handle)); + gl.uniform_1_i32(u_tex, 0); + } else { + gl.bind_texture(glow::TEXTURE_2D, None); + } + gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo)); gl.enable_vertex_attrib_array(a_pos); - gl.vertex_attrib_pointer_f32(a_pos, 3, glow::FLOAT, false, 12, 0); + 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_arrays( glow::TRIANGLES, 0, vertex_count.min(i32::MAX as usize) as i32, ); + gl.disable_vertex_attrib_array(a_uv); gl.disable_vertex_attrib_array(a_pos); gl.bind_buffer(glow::ARRAY_BUFFER, None); + gl.bind_texture(glow::TEXTURE_2D, None); gl.use_program(None); } @@ -475,16 +666,24 @@ fn save_png(path: &Path, width: u32, height: u32, rgba: Vec<u8>) -> Result<(), S unsafe fn create_program(gl: &glow::Context) -> Result<glow::NativeProgram, String> { let vs_src = 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); } "#; let fs_src = r#" precision mediump float; +uniform sampler2D u_tex; +uniform float u_use_tex; +varying vec2 v_uv; void main() { - gl_FragColor = vec4(0.85, 0.90, 1.00, 1.0); + vec4 base = vec4(0.85, 0.90, 1.00, 1.0); + vec4 texColor = texture2D(u_tex, v_uv); + gl_FragColor = mix(base, texColor, u_use_tex); } "#; |
