feat: Enhance model and texture loading with improved error handling and new features
Some checks failed
Test / Lint (push) Failing after 1m10s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped

- 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.
This commit is contained in:
2026-02-19 05:19:18 +04:00
parent 18d4c6cf9f
commit a281ffa32e
12 changed files with 985 additions and 28 deletions

View File

@@ -11,6 +11,7 @@ demo = ["dep:sdl2", "dep:glow", "dep:image"]
msh-core = { path = "../msh-core" }
nres = { path = "../nres" }
render-core = { path = "../render-core" }
texm = { path = "../texm" }
sdl2 = { version = "0.37", optional = true, default-features = false, features = ["bundled", "static-link"] }
glow = { version = "0.16", optional = true }
image = { version = "0.25", optional = true, default-features = false, features = ["png"] }

View File

@@ -5,6 +5,7 @@
## Назначение
- Проверить, что `nres + msh-core + render-core` дают рабочий draw-path на реальных ассетах.
- Проверить текстурный path `WEAR -> MAT0 -> Texm` на реальных ассетах.
- Служить минимальным reference-приложением.
## Запуск
@@ -26,6 +27,20 @@ cargo run -p render-demo --features demo -- \
- `--width`, `--height` (опционально, default `1280x720`).
- `--angle` (опционально): фиксированный угол поворота вокруг Y (в радианах).
- `--spin-rate` (опционально, default `0.35`): скорость вращения в интерактивном режиме.
- `--texture <name>`: явное имя `Texm` (override авто-резолва).
- `--texture-archive <path>`: путь к архиву текстур (по умолчанию `textures.lib` рядом с `--archive`).
- `--material-archive <path>`: путь к `material.lib` (по умолчанию соседний `material.lib`).
- `--wear <name.wea>`: имя wear-entry внутри модельного архива (по умолчанию `<model_stem>.wea`).
- `--no-texture`: отключить текстуры и рендерить однотонным цветом.
## Авто-резолв текстуры
Если не передан `--texture`, демо пытается взять текстуру из игровых данных:
1. `model.msh -> model.wea` (первый wear-материал),
2. `material.lib` (`MAT0`) по имени материала с fallback `DEFAULT`,
3. первая непустая `textureName` фаза материала,
4. загрузка `Texm` из `textures.lib` (или `lightmap.lib` как fallback).
## Детерминированный снимок кадра
@@ -43,7 +58,16 @@ cargo run -p render-demo --features demo -- \
--capture "target/render-parity/current/animals_a_l_01.png"
```
Явный выбор текстуры:
```bash
cargo run -p render-demo --features demo -- \
--archive "testdata/Parkan - Iron Strategy/animals.rlb" \
--model "A_L_01.msh" \
--texture "PG09.0"
```
## Ограничения
- Рендер только геометрии (без материалов/текстур/FX).
- Вывод через `glDrawArrays(GL_TRIANGLES)` из расширенного triangle-list.
- Используется только базовая texture-фаза (без полной material/fx анимации).
- Вывод через `glDrawArrays(GL_TRIANGLES)` из расширенного triangle-list (позиции+UV).

View File

@@ -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());
}
}

View File

@@ -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);
}
"#;