2026-02-19 04:46:23 +04:00
|
|
|
use glow::HasContext as _;
|
2026-02-19 05:19:18 +04:00
|
|
|
use render_core::{build_render_mesh, compute_bounds_for_mesh};
|
|
|
|
|
use render_demo::{load_model_with_name_from_archive, resolve_texture_for_model, LoadedTexture};
|
2026-02-19 05:02:26 +04:00
|
|
|
use std::path::{Path, PathBuf};
|
2026-02-19 04:46:23 +04:00
|
|
|
use std::time::Instant;
|
|
|
|
|
|
|
|
|
|
struct Args {
|
|
|
|
|
archive: PathBuf,
|
|
|
|
|
model: Option<String>,
|
|
|
|
|
lod: usize,
|
|
|
|
|
group: usize,
|
2026-02-19 05:02:26 +04:00
|
|
|
width: u32,
|
|
|
|
|
height: u32,
|
2026-02-19 09:46:23 +00:00
|
|
|
fov_deg: f32,
|
2026-02-19 05:02:26 +04:00
|
|
|
capture: Option<PathBuf>,
|
|
|
|
|
angle: Option<f32>,
|
|
|
|
|
spin_rate: f32,
|
2026-02-19 05:19:18 +04:00
|
|
|
texture: Option<String>,
|
|
|
|
|
texture_archive: Option<PathBuf>,
|
|
|
|
|
material_archive: Option<PathBuf>,
|
|
|
|
|
wear: Option<String>,
|
|
|
|
|
no_texture: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct GpuTexture {
|
|
|
|
|
handle: glow::NativeTexture,
|
2026-02-19 04:46:23 +04:00
|
|
|
}
|
|
|
|
|
|
2026-02-19 10:17:14 +00:00
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
|
|
|
|
enum GlBackend {
|
|
|
|
|
Gles2,
|
|
|
|
|
Core33,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
fn parse_args() -> Result<Args, String> {
|
|
|
|
|
let mut archive = None;
|
|
|
|
|
let mut model = None;
|
|
|
|
|
let mut lod = 0usize;
|
|
|
|
|
let mut group = 0usize;
|
2026-02-19 05:02:26 +04:00
|
|
|
let mut width = 1280u32;
|
|
|
|
|
let mut height = 720u32;
|
2026-02-19 09:46:23 +00:00
|
|
|
let mut fov_deg = 60.0f32;
|
2026-02-19 05:02:26 +04:00
|
|
|
let mut capture = None;
|
|
|
|
|
let mut angle = None;
|
|
|
|
|
let mut spin_rate = 0.35f32;
|
2026-02-19 05:19:18 +04:00
|
|
|
let mut texture = None;
|
|
|
|
|
let mut texture_archive = None;
|
|
|
|
|
let mut material_archive = None;
|
|
|
|
|
let mut wear = None;
|
|
|
|
|
let mut no_texture = false;
|
2026-02-19 04:46:23 +04:00
|
|
|
|
|
|
|
|
let mut it = std::env::args().skip(1);
|
|
|
|
|
while let Some(arg) = it.next() {
|
|
|
|
|
match arg.as_str() {
|
|
|
|
|
"--archive" => {
|
|
|
|
|
let value = it
|
|
|
|
|
.next()
|
|
|
|
|
.ok_or_else(|| String::from("missing value for --archive"))?;
|
|
|
|
|
archive = Some(PathBuf::from(value));
|
|
|
|
|
}
|
|
|
|
|
"--model" => {
|
|
|
|
|
let value = it
|
|
|
|
|
.next()
|
|
|
|
|
.ok_or_else(|| String::from("missing value for --model"))?;
|
|
|
|
|
model = Some(value);
|
|
|
|
|
}
|
|
|
|
|
"--lod" => {
|
|
|
|
|
let value = it
|
|
|
|
|
.next()
|
|
|
|
|
.ok_or_else(|| String::from("missing value for --lod"))?;
|
|
|
|
|
lod = value
|
|
|
|
|
.parse::<usize>()
|
|
|
|
|
.map_err(|_| String::from("invalid --lod value"))?;
|
|
|
|
|
}
|
|
|
|
|
"--group" => {
|
|
|
|
|
let value = it
|
|
|
|
|
.next()
|
|
|
|
|
.ok_or_else(|| String::from("missing value for --group"))?;
|
|
|
|
|
group = value
|
|
|
|
|
.parse::<usize>()
|
|
|
|
|
.map_err(|_| String::from("invalid --group value"))?;
|
|
|
|
|
}
|
2026-02-19 05:02:26 +04:00
|
|
|
"--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"));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-19 09:46:23 +00:00
|
|
|
"--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]"));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-19 05:02:26 +04:00
|
|
|
"--capture" => {
|
|
|
|
|
let value = it
|
|
|
|
|
.next()
|
|
|
|
|
.ok_or_else(|| String::from("missing value for --capture"))?;
|
|
|
|
|
capture = Some(PathBuf::from(value));
|
|
|
|
|
}
|
|
|
|
|
"--angle" => {
|
|
|
|
|
let value = it
|
|
|
|
|
.next()
|
|
|
|
|
.ok_or_else(|| String::from("missing value for --angle"))?;
|
|
|
|
|
angle = Some(
|
|
|
|
|
value
|
|
|
|
|
.parse::<f32>()
|
|
|
|
|
.map_err(|_| String::from("invalid --angle value"))?,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
"--spin-rate" => {
|
|
|
|
|
let value = it
|
|
|
|
|
.next()
|
|
|
|
|
.ok_or_else(|| String::from("missing value for --spin-rate"))?;
|
|
|
|
|
spin_rate = value
|
|
|
|
|
.parse::<f32>()
|
|
|
|
|
.map_err(|_| String::from("invalid --spin-rate value"))?;
|
|
|
|
|
}
|
2026-02-19 05:19:18 +04:00
|
|
|
"--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;
|
|
|
|
|
}
|
2026-02-19 04:46:23 +04:00
|
|
|
"--help" | "-h" => {
|
|
|
|
|
print_help();
|
|
|
|
|
std::process::exit(0);
|
|
|
|
|
}
|
|
|
|
|
other => {
|
|
|
|
|
return Err(format!("unknown argument: {other}"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let archive = archive.ok_or_else(|| String::from("missing required --archive"))?;
|
|
|
|
|
Ok(Args {
|
|
|
|
|
archive,
|
|
|
|
|
model,
|
|
|
|
|
lod,
|
|
|
|
|
group,
|
2026-02-19 05:02:26 +04:00
|
|
|
width,
|
|
|
|
|
height,
|
2026-02-19 09:46:23 +00:00
|
|
|
fov_deg,
|
2026-02-19 05:02:26 +04:00
|
|
|
capture,
|
|
|
|
|
angle,
|
|
|
|
|
spin_rate,
|
2026-02-19 05:19:18 +04:00
|
|
|
texture,
|
|
|
|
|
texture_archive,
|
|
|
|
|
material_archive,
|
|
|
|
|
wear,
|
|
|
|
|
no_texture,
|
2026-02-19 04:46:23 +04:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn print_help() {
|
2026-02-19 05:02:26 +04:00
|
|
|
eprintln!(
|
2026-02-19 09:46:23 +00:00
|
|
|
"parkan-render-demo --archive <path> [--model <name.msh>] [--lod N] [--group N] [--width W] [--height H] [--fov DEG]"
|
2026-02-19 05:02:26 +04:00
|
|
|
);
|
|
|
|
|
eprintln!(" [--capture <out.png>] [--angle RAD] [--spin-rate RAD_PER_SEC]");
|
2026-02-19 05:19:18 +04:00
|
|
|
eprintln!(" [--texture <name>] [--texture-archive <path>] [--material-archive <path>] [--wear <name.wea>] [--no-texture]");
|
2026-02-19 04:46:23 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
|
let args = match parse_args() {
|
|
|
|
|
Ok(v) => v,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
eprintln!("{err}");
|
|
|
|
|
print_help();
|
|
|
|
|
std::process::exit(2);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-19 05:02:26 +04:00
|
|
|
if let Err(err) = run(args) {
|
|
|
|
|
eprintln!("{err}");
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn run(args: Args) -> Result<(), String> {
|
2026-02-19 05:19:18 +04:00
|
|
|
let loaded_model = load_model_with_name_from_archive(&args.archive, args.model.as_deref())
|
|
|
|
|
.map_err(|err| {
|
|
|
|
|
format!(
|
2026-02-19 09:46:23 +00:00
|
|
|
"failed to load model from archive {}: {err}",
|
2026-02-19 05:19:18 +04:00
|
|
|
args.archive.display()
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
let mesh = build_render_mesh(&loaded_model.model, args.lod, args.group);
|
2026-02-19 10:09:18 +00:00
|
|
|
if mesh.indices.is_empty() {
|
2026-02-19 05:02:26 +04:00
|
|
|
return Err(format!(
|
2026-02-19 04:46:23 +04:00
|
|
|
"model has no renderable triangles for lod={} group={}",
|
|
|
|
|
args.lod, args.group
|
2026-02-19 05:02:26 +04:00
|
|
|
));
|
2026-02-19 04:46:23 +04:00
|
|
|
}
|
2026-02-19 10:09:18 +00:00
|
|
|
if mesh.index_overflow {
|
|
|
|
|
eprintln!(
|
|
|
|
|
"warning: mesh exceeds u16 index space and may be partially rendered on GLES2 targets"
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-02-19 05:19:18 +04:00
|
|
|
let Some((bounds_min, bounds_max)) = compute_bounds_for_mesh(&mesh.vertices) else {
|
2026-02-19 05:02:26 +04:00
|
|
|
return Err(String::from("failed to compute mesh bounds"));
|
2026-02-19 04:46:23 +04:00
|
|
|
};
|
|
|
|
|
|
2026-02-19 05:19:18 +04:00
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
let center = [
|
|
|
|
|
0.5 * (bounds_min[0] + bounds_max[0]),
|
|
|
|
|
0.5 * (bounds_min[1] + bounds_max[1]),
|
|
|
|
|
0.5 * (bounds_min[2] + bounds_max[2]),
|
|
|
|
|
];
|
|
|
|
|
let extent = [
|
|
|
|
|
bounds_max[0] - bounds_min[0],
|
|
|
|
|
bounds_max[1] - bounds_min[1],
|
|
|
|
|
bounds_max[2] - bounds_min[2],
|
|
|
|
|
];
|
|
|
|
|
let radius =
|
|
|
|
|
(extent[0] * extent[0] + extent[1] * extent[1] + extent[2] * extent[2]).sqrt() * 0.5;
|
|
|
|
|
let camera_distance = (radius * 2.5).max(2.0);
|
|
|
|
|
|
2026-02-19 05:02:26 +04:00
|
|
|
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}"))?;
|
2026-02-19 04:46:23 +04:00
|
|
|
|
2026-02-19 10:17:14 +00:00
|
|
|
let (window, _gl_ctx, gl_backend) = create_window_and_context(&video, &args)?;
|
2026-02-19 05:02:26 +04:00
|
|
|
let _ = if args.capture.is_some() {
|
|
|
|
|
video.gl_set_swap_interval(0)
|
|
|
|
|
} else {
|
|
|
|
|
video.gl_set_swap_interval(1)
|
|
|
|
|
};
|
2026-02-19 04:46:23 +04:00
|
|
|
|
2026-02-19 05:19:18 +04:00
|
|
|
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]);
|
2026-02-19 04:46:23 +04:00
|
|
|
}
|
2026-02-19 09:46:23 +00:00
|
|
|
let vertex_bytes = f32_slice_to_ne_bytes(&vertex_data);
|
2026-02-19 10:09:18 +00:00
|
|
|
let index_bytes = u16_slice_to_ne_bytes(&mesh.indices);
|
2026-02-19 04:46:23 +04:00
|
|
|
|
|
|
|
|
let gl = unsafe {
|
|
|
|
|
glow::Context::from_loader_function(|name| video.gl_get_proc_address(name) as *const _)
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-19 10:17:14 +00:00
|
|
|
let program = unsafe { create_program(&gl, gl_backend)? };
|
2026-02-19 04:46:23 +04:00
|
|
|
let u_mvp = unsafe { gl.get_uniform_location(program, "u_mvp") };
|
2026-02-19 05:19:18 +04:00
|
|
|
let u_use_tex = unsafe { gl.get_uniform_location(program, "u_use_tex") };
|
|
|
|
|
let u_tex = unsafe { gl.get_uniform_location(program, "u_tex") };
|
2026-02-19 05:02:26 +04:00
|
|
|
let a_pos = unsafe { gl.get_attrib_location(program, "a_pos") }
|
|
|
|
|
.ok_or_else(|| String::from("shader attribute a_pos is missing"))?;
|
2026-02-19 05:19:18 +04:00
|
|
|
let a_uv = unsafe { gl.get_attrib_location(program, "a_uv") }
|
|
|
|
|
.ok_or_else(|| String::from("shader attribute a_uv is missing"))?;
|
2026-02-19 04:46:23 +04:00
|
|
|
|
2026-02-19 05:02:26 +04:00
|
|
|
let vbo = unsafe { gl.create_buffer().map_err(|e| e.to_string())? };
|
2026-02-19 10:09:18 +00:00
|
|
|
let ebo = unsafe { gl.create_buffer().map_err(|e| e.to_string())? };
|
2026-02-19 04:46:23 +04:00
|
|
|
unsafe {
|
|
|
|
|
gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
|
2026-02-19 09:46:23 +00:00
|
|
|
gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, &vertex_bytes, glow::STATIC_DRAW);
|
2026-02-19 10:09:18 +00:00
|
|
|
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);
|
2026-02-19 04:46:23 +04:00
|
|
|
gl.bind_buffer(glow::ARRAY_BUFFER, None);
|
|
|
|
|
}
|
2026-02-19 10:17:14 +00:00
|
|
|
let vao = unsafe { create_vertex_layout_if_needed(&gl, gl_backend, vbo, ebo, a_pos, a_uv)? };
|
2026-02-19 04:46:23 +04:00
|
|
|
|
2026-02-19 05:19:18 +04:00
|
|
|
let gpu_texture = if let Some(texture) = resolved_texture.as_ref() {
|
|
|
|
|
Some(unsafe { create_texture(&gl, texture)? })
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-19 05:02:26 +04:00
|
|
|
let result = if let Some(capture_path) = args.capture.as_ref() {
|
|
|
|
|
run_capture(
|
|
|
|
|
&gl,
|
|
|
|
|
program,
|
|
|
|
|
u_mvp.as_ref(),
|
2026-02-19 05:19:18 +04:00
|
|
|
u_use_tex.as_ref(),
|
|
|
|
|
u_tex.as_ref(),
|
2026-02-19 05:02:26 +04:00
|
|
|
a_pos,
|
2026-02-19 05:19:18 +04:00
|
|
|
a_uv,
|
2026-02-19 05:02:26 +04:00
|
|
|
vbo,
|
2026-02-19 10:09:18 +00:00
|
|
|
ebo,
|
2026-02-19 10:17:14 +00:00
|
|
|
vao,
|
2026-02-19 05:19:18 +04:00
|
|
|
gpu_texture.as_ref(),
|
2026-02-19 10:09:18 +00:00
|
|
|
mesh.indices.len(),
|
2026-02-19 05:02:26 +04:00
|
|
|
&args,
|
|
|
|
|
center,
|
|
|
|
|
camera_distance,
|
|
|
|
|
capture_path,
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
run_interactive(
|
|
|
|
|
&sdl,
|
|
|
|
|
&window,
|
|
|
|
|
&gl,
|
|
|
|
|
program,
|
|
|
|
|
u_mvp.as_ref(),
|
2026-02-19 05:19:18 +04:00
|
|
|
u_use_tex.as_ref(),
|
|
|
|
|
u_tex.as_ref(),
|
2026-02-19 05:02:26 +04:00
|
|
|
a_pos,
|
2026-02-19 05:19:18 +04:00
|
|
|
a_uv,
|
2026-02-19 05:02:26 +04:00
|
|
|
vbo,
|
2026-02-19 10:09:18 +00:00
|
|
|
ebo,
|
2026-02-19 10:17:14 +00:00
|
|
|
vao,
|
2026-02-19 05:19:18 +04:00
|
|
|
gpu_texture.as_ref(),
|
2026-02-19 10:09:18 +00:00
|
|
|
mesh.indices.len(),
|
2026-02-19 05:02:26 +04:00
|
|
|
&args,
|
|
|
|
|
center,
|
|
|
|
|
camera_distance,
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
unsafe {
|
2026-02-19 05:19:18 +04:00
|
|
|
if let Some(texture) = gpu_texture {
|
|
|
|
|
gl.delete_texture(texture.handle);
|
|
|
|
|
}
|
2026-02-19 10:17:14 +00:00
|
|
|
if let Some(vao) = vao {
|
|
|
|
|
gl.delete_vertex_array(vao);
|
|
|
|
|
}
|
2026-02-19 10:09:18 +00:00
|
|
|
gl.delete_buffer(ebo);
|
2026-02-19 05:02:26 +04:00
|
|
|
gl.delete_buffer(vbo);
|
|
|
|
|
gl.delete_program(program);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 10:17:14 +00:00
|
|
|
fn create_window_and_context(
|
|
|
|
|
video: &sdl2::VideoSubsystem,
|
|
|
|
|
args: &Args,
|
|
|
|
|
) -> 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 Render Demo (SDL2 + OpenGL)",
|
|
|
|
|
args.width,
|
|
|
|
|
args.height,
|
|
|
|
|
);
|
|
|
|
|
window_builder.opengl();
|
|
|
|
|
if args.capture.is_some() {
|
|
|
|
|
window_builder.hidden();
|
|
|
|
|
} else {
|
|
|
|
|
window_builder.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_vertex_layout_if_needed(
|
|
|
|
|
gl: &glow::Context,
|
|
|
|
|
backend: GlBackend,
|
|
|
|
|
vbo: glow::NativeBuffer,
|
|
|
|
|
ebo: glow::NativeBuffer,
|
|
|
|
|
a_pos: u32,
|
|
|
|
|
a_uv: u32,
|
|
|
|
|
) -> Result<Option<glow::NativeVertexArray>, String> {
|
|
|
|
|
if backend != GlBackend::Core33 {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let vao = gl.create_vertex_array().map_err(|e| e.to_string())?;
|
|
|
|
|
gl.bind_vertex_array(Some(vao));
|
|
|
|
|
gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
|
|
|
|
|
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(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.bind_vertex_array(None);
|
|
|
|
|
Ok(Some(vao))
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 05:19:18 +04:00
|
|
|
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()
|
|
|
|
|
{
|
2026-02-19 09:46:23 +00:00
|
|
|
Err(format!("failed to resolve texture: {err}"))
|
2026-02-19 05:19:18 +04:00
|
|
|
} else {
|
2026-02-19 09:46:23 +00:00
|
|
|
eprintln!("warning: auto texture resolve failed ({err}), fallback to solid color");
|
2026-02-19 05:19:18 +04:00
|
|
|
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 })
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 05:02:26 +04:00
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
|
fn run_capture(
|
|
|
|
|
gl: &glow::Context,
|
|
|
|
|
program: glow::NativeProgram,
|
|
|
|
|
u_mvp: Option<&glow::NativeUniformLocation>,
|
2026-02-19 05:19:18 +04:00
|
|
|
u_use_tex: Option<&glow::NativeUniformLocation>,
|
|
|
|
|
u_tex: Option<&glow::NativeUniformLocation>,
|
2026-02-19 05:02:26 +04:00
|
|
|
a_pos: u32,
|
2026-02-19 05:19:18 +04:00
|
|
|
a_uv: u32,
|
2026-02-19 05:02:26 +04:00
|
|
|
vbo: glow::NativeBuffer,
|
2026-02-19 10:09:18 +00:00
|
|
|
ebo: glow::NativeBuffer,
|
2026-02-19 10:17:14 +00:00
|
|
|
vao: Option<glow::NativeVertexArray>,
|
2026-02-19 05:19:18 +04:00
|
|
|
texture: Option<&GpuTexture>,
|
2026-02-19 10:09:18 +00:00
|
|
|
index_count: usize,
|
2026-02-19 05:02:26 +04:00
|
|
|
args: &Args,
|
|
|
|
|
center: [f32; 3],
|
|
|
|
|
camera_distance: f32,
|
|
|
|
|
capture_path: &Path,
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
let angle = args.angle.unwrap_or(0.0);
|
2026-02-19 09:46:23 +00:00
|
|
|
let mvp = compute_mvp(
|
|
|
|
|
args.width,
|
|
|
|
|
args.height,
|
|
|
|
|
args.fov_deg,
|
|
|
|
|
center,
|
|
|
|
|
camera_distance,
|
|
|
|
|
angle,
|
|
|
|
|
);
|
2026-02-19 05:02:26 +04:00
|
|
|
unsafe {
|
|
|
|
|
draw_frame(
|
|
|
|
|
gl,
|
|
|
|
|
program,
|
|
|
|
|
u_mvp,
|
2026-02-19 05:19:18 +04:00
|
|
|
u_use_tex,
|
|
|
|
|
u_tex,
|
2026-02-19 05:02:26 +04:00
|
|
|
a_pos,
|
2026-02-19 05:19:18 +04:00
|
|
|
a_uv,
|
2026-02-19 05:02:26 +04:00
|
|
|
vbo,
|
2026-02-19 10:09:18 +00:00
|
|
|
ebo,
|
2026-02-19 10:17:14 +00:00
|
|
|
vao,
|
2026-02-19 05:19:18 +04:00
|
|
|
texture,
|
2026-02-19 10:09:18 +00:00
|
|
|
index_count,
|
2026-02-19 05:02:26 +04:00
|
|
|
args.width,
|
|
|
|
|
args.height,
|
|
|
|
|
&mvp,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let mut rgba = unsafe { read_pixels_rgba(gl, args.width, args.height)? };
|
|
|
|
|
flip_image_y_rgba(&mut rgba, args.width as usize, args.height as usize);
|
|
|
|
|
save_png(capture_path, args.width, args.height, rgba)?;
|
|
|
|
|
println!("captured frame to {}", capture_path.display());
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
|
fn run_interactive(
|
|
|
|
|
sdl: &sdl2::Sdl,
|
|
|
|
|
window: &sdl2::video::Window,
|
|
|
|
|
gl: &glow::Context,
|
|
|
|
|
program: glow::NativeProgram,
|
|
|
|
|
u_mvp: Option<&glow::NativeUniformLocation>,
|
2026-02-19 05:19:18 +04:00
|
|
|
u_use_tex: Option<&glow::NativeUniformLocation>,
|
|
|
|
|
u_tex: Option<&glow::NativeUniformLocation>,
|
2026-02-19 05:02:26 +04:00
|
|
|
a_pos: u32,
|
2026-02-19 05:19:18 +04:00
|
|
|
a_uv: u32,
|
2026-02-19 05:02:26 +04:00
|
|
|
vbo: glow::NativeBuffer,
|
2026-02-19 10:09:18 +00:00
|
|
|
ebo: glow::NativeBuffer,
|
2026-02-19 10:17:14 +00:00
|
|
|
vao: Option<glow::NativeVertexArray>,
|
2026-02-19 05:19:18 +04:00
|
|
|
texture: Option<&GpuTexture>,
|
2026-02-19 10:09:18 +00:00
|
|
|
index_count: usize,
|
2026-02-19 05:02:26 +04:00
|
|
|
args: &Args,
|
|
|
|
|
center: [f32; 3],
|
|
|
|
|
camera_distance: f32,
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
let mut events = sdl
|
|
|
|
|
.event_pump()
|
|
|
|
|
.map_err(|err| format!("failed to get SDL event pump: {err}"))?;
|
2026-02-19 04:46:23 +04:00
|
|
|
let start = Instant::now();
|
|
|
|
|
|
|
|
|
|
'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,
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let (w, h) = window.size();
|
2026-02-19 05:02:26 +04:00
|
|
|
let angle = args
|
|
|
|
|
.angle
|
|
|
|
|
.unwrap_or(start.elapsed().as_secs_f32() * args.spin_rate);
|
2026-02-19 09:46:23 +00:00
|
|
|
let mvp = compute_mvp(w, h, args.fov_deg, center, camera_distance, angle);
|
2026-02-19 04:46:23 +04:00
|
|
|
|
|
|
|
|
unsafe {
|
2026-02-19 05:19:18 +04:00
|
|
|
draw_frame(
|
|
|
|
|
gl,
|
|
|
|
|
program,
|
|
|
|
|
u_mvp,
|
|
|
|
|
u_use_tex,
|
|
|
|
|
u_tex,
|
|
|
|
|
a_pos,
|
|
|
|
|
a_uv,
|
|
|
|
|
vbo,
|
2026-02-19 10:09:18 +00:00
|
|
|
ebo,
|
2026-02-19 10:17:14 +00:00
|
|
|
vao,
|
2026-02-19 05:19:18 +04:00
|
|
|
texture,
|
2026-02-19 10:09:18 +00:00
|
|
|
index_count,
|
2026-02-19 05:19:18 +04:00
|
|
|
w,
|
|
|
|
|
h,
|
|
|
|
|
&mvp,
|
|
|
|
|
);
|
2026-02-19 04:46:23 +04:00
|
|
|
}
|
|
|
|
|
window.gl_swap_window();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 05:02:26 +04:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn compute_mvp(
|
|
|
|
|
width: u32,
|
|
|
|
|
height: u32,
|
2026-02-19 09:46:23 +00:00
|
|
|
fov_deg: f32,
|
2026-02-19 05:02:26 +04:00
|
|
|
center: [f32; 3],
|
|
|
|
|
camera_distance: f32,
|
|
|
|
|
angle_rad: f32,
|
|
|
|
|
) -> [f32; 16] {
|
|
|
|
|
let aspect = (width as f32 / (height.max(1) as f32)).max(0.01);
|
2026-02-19 09:46:23 +00:00
|
|
|
let proj = mat4_perspective(fov_deg.to_radians(), aspect, 0.01, camera_distance * 10.0);
|
2026-02-19 05:02:26 +04:00
|
|
|
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);
|
|
|
|
|
let model_m = mat4_mul(&rot, ¢er_shift);
|
|
|
|
|
let vp = mat4_mul(&view, &model_m);
|
|
|
|
|
mat4_mul(&proj, &vp)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
|
unsafe fn draw_frame(
|
|
|
|
|
gl: &glow::Context,
|
|
|
|
|
program: glow::NativeProgram,
|
|
|
|
|
u_mvp: Option<&glow::NativeUniformLocation>,
|
2026-02-19 05:19:18 +04:00
|
|
|
u_use_tex: Option<&glow::NativeUniformLocation>,
|
|
|
|
|
u_tex: Option<&glow::NativeUniformLocation>,
|
2026-02-19 05:02:26 +04:00
|
|
|
a_pos: u32,
|
2026-02-19 05:19:18 +04:00
|
|
|
a_uv: u32,
|
2026-02-19 05:02:26 +04:00
|
|
|
vbo: glow::NativeBuffer,
|
2026-02-19 10:09:18 +00:00
|
|
|
ebo: glow::NativeBuffer,
|
2026-02-19 10:17:14 +00:00
|
|
|
vao: Option<glow::NativeVertexArray>,
|
2026-02-19 05:19:18 +04:00
|
|
|
texture: Option<&GpuTexture>,
|
2026-02-19 10:09:18 +00:00
|
|
|
index_count: usize,
|
2026-02-19 05:02:26 +04:00
|
|
|
width: u32,
|
|
|
|
|
height: u32,
|
|
|
|
|
mvp: &[f32; 16],
|
|
|
|
|
) {
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
gl.use_program(Some(program));
|
|
|
|
|
gl.uniform_matrix_4_f32_slice(u_mvp, false, mvp);
|
|
|
|
|
|
2026-02-19 05:19:18 +04:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 10:17:14 +00:00
|
|
|
if let Some(vao) = vao {
|
|
|
|
|
gl.bind_vertex_array(Some(vao));
|
|
|
|
|
gl.draw_elements(
|
|
|
|
|
glow::TRIANGLES,
|
|
|
|
|
index_count.min(i32::MAX as usize) as i32,
|
|
|
|
|
glow::UNSIGNED_SHORT,
|
|
|
|
|
0,
|
|
|
|
|
);
|
|
|
|
|
gl.bind_vertex_array(None);
|
|
|
|
|
} else {
|
|
|
|
|
gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
|
|
|
|
|
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(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,
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-02-19 05:19:18 +04:00
|
|
|
gl.bind_texture(glow::TEXTURE_2D, None);
|
2026-02-19 05:02:26 +04:00
|
|
|
gl.use_program(None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unsafe fn read_pixels_rgba(gl: &glow::Context, width: u32, height: u32) -> Result<Vec<u8>, String> {
|
|
|
|
|
let pixel_count = usize::try_from(width)
|
|
|
|
|
.ok()
|
|
|
|
|
.and_then(|w| usize::try_from(height).ok().map(|h| w.saturating_mul(h)))
|
|
|
|
|
.ok_or_else(|| String::from("frame dimensions are too large"))?;
|
|
|
|
|
let mut pixels = vec![0u8; pixel_count.saturating_mul(4)];
|
|
|
|
|
gl.read_pixels(
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
width.min(i32::MAX as u32) as i32,
|
|
|
|
|
height.min(i32::MAX as u32) as i32,
|
|
|
|
|
glow::RGBA,
|
|
|
|
|
glow::UNSIGNED_BYTE,
|
|
|
|
|
glow::PixelPackData::Slice(Some(pixels.as_mut_slice())),
|
|
|
|
|
);
|
|
|
|
|
Ok(pixels)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn flip_image_y_rgba(rgba: &mut [u8], width: usize, height: usize) {
|
|
|
|
|
let stride = width.saturating_mul(4);
|
|
|
|
|
if stride == 0 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
for y in 0..(height / 2) {
|
|
|
|
|
let top = y * stride;
|
|
|
|
|
let bottom = (height - 1 - y) * stride;
|
|
|
|
|
for i in 0..stride {
|
|
|
|
|
rgba.swap(top + i, bottom + i);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn save_png(path: &Path, width: u32, height: u32, rgba: Vec<u8>) -> Result<(), String> {
|
|
|
|
|
if let Some(parent) = path.parent() {
|
|
|
|
|
if !parent.as_os_str().is_empty() {
|
|
|
|
|
std::fs::create_dir_all(parent).map_err(|err| {
|
|
|
|
|
format!(
|
|
|
|
|
"failed to create output directory {}: {err}",
|
|
|
|
|
parent.display()
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
}
|
2026-02-19 04:46:23 +04:00
|
|
|
}
|
2026-02-19 05:02:26 +04:00
|
|
|
let image = image::RgbaImage::from_raw(width, height, rgba)
|
|
|
|
|
.ok_or_else(|| String::from("failed to build image from framebuffer bytes"))?;
|
|
|
|
|
image
|
|
|
|
|
.save(path)
|
|
|
|
|
.map_err(|err| format!("failed to save PNG {}: {err}", path.display()))
|
2026-02-19 04:46:23 +04:00
|
|
|
}
|
|
|
|
|
|
2026-02-19 10:17:14 +00:00
|
|
|
unsafe fn create_program(
|
|
|
|
|
gl: &glow::Context,
|
|
|
|
|
backend: GlBackend,
|
|
|
|
|
) -> Result<glow::NativeProgram, String> {
|
|
|
|
|
let (vs_src, fs_src) = match backend {
|
|
|
|
|
GlBackend::Gles2 => (
|
|
|
|
|
r#"
|
2026-02-19 04:46:23 +04:00
|
|
|
attribute vec3 a_pos;
|
2026-02-19 05:19:18 +04:00
|
|
|
attribute vec2 a_uv;
|
2026-02-19 04:46:23 +04:00
|
|
|
uniform mat4 u_mvp;
|
2026-02-19 05:19:18 +04:00
|
|
|
varying vec2 v_uv;
|
2026-02-19 04:46:23 +04:00
|
|
|
void main() {
|
2026-02-19 05:19:18 +04:00
|
|
|
v_uv = a_uv;
|
2026-02-19 04:46:23 +04:00
|
|
|
gl_Position = u_mvp * vec4(a_pos, 1.0);
|
|
|
|
|
}
|
2026-02-19 10:17:14 +00:00
|
|
|
"#,
|
|
|
|
|
r#"
|
2026-02-19 04:46:23 +04:00
|
|
|
precision mediump float;
|
2026-02-19 05:19:18 +04:00
|
|
|
uniform sampler2D u_tex;
|
|
|
|
|
uniform float u_use_tex;
|
|
|
|
|
varying vec2 v_uv;
|
2026-02-19 04:46:23 +04:00
|
|
|
void main() {
|
2026-02-19 05:19:18 +04:00
|
|
|
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);
|
2026-02-19 04:46:23 +04:00
|
|
|
}
|
2026-02-19 10:17:14 +00:00
|
|
|
"#,
|
|
|
|
|
),
|
|
|
|
|
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.85, 0.90, 1.00, 1.0);
|
|
|
|
|
vec4 texColor = texture(u_tex, v_uv);
|
|
|
|
|
fragColor = mix(base, texColor, u_use_tex);
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
),
|
|
|
|
|
};
|
2026-02-19 04:46:23 +04:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 09:46:23 +00:00
|
|
|
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
|
2026-02-19 04:46:23 +04:00
|
|
|
}
|
|
|
|
|
|
2026-02-19 10:09:18 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
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_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_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
|
|
|
|
|
}
|