feat: add terrain-core, tma, and unitdat crates with parsing functionality
- Introduced `terrain-core` crate for loading and processing terrain mesh data. - Added `tma` crate for parsing mission files, including footer and object records. - Created `unitdat` crate for reading unit data files with validation of structure. - Implemented error handling and tests for all new crates. - Documented object registry format and rendering pipeline in specifications.
This commit is contained in:
@@ -0,0 +1,924 @@
|
||||
use glow::HasContext as _;
|
||||
use render_mission_demo::{
|
||||
compute_scene_bounds, detect_game_root_from_mission_path, load_scene_with_options, LoadOptions,
|
||||
MissionScene, ModelInstance,
|
||||
};
|
||||
use std::io::Write as _;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
struct Args {
|
||||
mission: PathBuf,
|
||||
game_root: Option<PathBuf>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
fov_deg: f32,
|
||||
no_model_texture: bool,
|
||||
no_terrain_texture: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
enum GlBackend {
|
||||
Gles2,
|
||||
Core33,
|
||||
}
|
||||
|
||||
struct GpuTexture {
|
||||
handle: glow::NativeTexture,
|
||||
}
|
||||
|
||||
struct GpuRenderable {
|
||||
vbo: glow::NativeBuffer,
|
||||
ebo: glow::NativeBuffer,
|
||||
index_count: usize,
|
||||
texture: Option<GpuTexture>,
|
||||
}
|
||||
|
||||
struct ModelRenderable {
|
||||
gpu: GpuRenderable,
|
||||
instances: Vec<ModelInstance>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
struct Camera {
|
||||
position: [f32; 3],
|
||||
yaw: f32,
|
||||
pitch: f32,
|
||||
move_speed: f32,
|
||||
mouse_sensitivity: f32,
|
||||
}
|
||||
|
||||
fn parse_args() -> Result<Args, String> {
|
||||
let mut mission = None;
|
||||
let mut game_root = None;
|
||||
let mut width = 1600u32;
|
||||
let mut height = 900u32;
|
||||
let mut fov_deg = 60.0f32;
|
||||
let mut no_model_texture = false;
|
||||
let mut no_terrain_texture = false;
|
||||
|
||||
let mut it = std::env::args().skip(1);
|
||||
while let Some(arg) = it.next() {
|
||||
match arg.as_str() {
|
||||
"--mission" => {
|
||||
let value = it
|
||||
.next()
|
||||
.ok_or_else(|| String::from("missing value for --mission"))?;
|
||||
mission = Some(PathBuf::from(value));
|
||||
}
|
||||
"--game-root" => {
|
||||
let value = it
|
||||
.next()
|
||||
.ok_or_else(|| String::from("missing value for --game-root"))?;
|
||||
game_root = Some(PathBuf::from(value));
|
||||
}
|
||||
"--width" => {
|
||||
let value = it
|
||||
.next()
|
||||
.ok_or_else(|| String::from("missing value for --width"))?;
|
||||
width = value
|
||||
.parse::<u32>()
|
||||
.map_err(|_| String::from("invalid --width value"))?;
|
||||
if width == 0 {
|
||||
return Err(String::from("--width must be > 0"));
|
||||
}
|
||||
}
|
||||
"--height" => {
|
||||
let value = it
|
||||
.next()
|
||||
.ok_or_else(|| String::from("missing value for --height"))?;
|
||||
height = value
|
||||
.parse::<u32>()
|
||||
.map_err(|_| String::from("invalid --height value"))?;
|
||||
if height == 0 {
|
||||
return Err(String::from("--height must be > 0"));
|
||||
}
|
||||
}
|
||||
"--fov" => {
|
||||
let value = it
|
||||
.next()
|
||||
.ok_or_else(|| String::from("missing value for --fov"))?;
|
||||
fov_deg = value
|
||||
.parse::<f32>()
|
||||
.map_err(|_| String::from("invalid --fov value"))?;
|
||||
if !(1.0..=179.0).contains(&fov_deg) {
|
||||
return Err(String::from("--fov must be in range [1, 179]"));
|
||||
}
|
||||
}
|
||||
"--no-model-texture" => {
|
||||
no_model_texture = true;
|
||||
}
|
||||
"--no-terrain-texture" => {
|
||||
no_terrain_texture = true;
|
||||
}
|
||||
"--help" | "-h" => {
|
||||
print_help();
|
||||
std::process::exit(0);
|
||||
}
|
||||
other => {
|
||||
return Err(format!("unknown argument: {other}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mission = mission.ok_or_else(|| String::from("missing required --mission"))?;
|
||||
Ok(Args {
|
||||
mission,
|
||||
game_root,
|
||||
width,
|
||||
height,
|
||||
fov_deg,
|
||||
no_model_texture,
|
||||
no_terrain_texture,
|
||||
})
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
eprintln!("parkan-render-mission-demo --mission <path/to/data.tma> [--game-root <path>] [--width W] [--height H] [--fov DEG]");
|
||||
eprintln!(" [--no-model-texture] [--no-terrain-texture]");
|
||||
eprintln!("controls: arrows/WASD move, PageUp/PageDown vertical move, Right Mouse drag look, Shift speed-up, Esc exit");
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = match parse_args() {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
print_help();
|
||||
std::process::exit(2);
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = run(args) {
|
||||
eprintln!("{err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run(args: Args) -> Result<(), String> {
|
||||
let game_root = if let Some(path) = args.game_root.clone() {
|
||||
path
|
||||
} else {
|
||||
detect_game_root_from_mission_path(&args.mission).ok_or_else(|| {
|
||||
format!(
|
||||
"failed to detect game root from mission path {} (use --game-root)",
|
||||
args.mission.display()
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
let scene = load_scene_with_options(
|
||||
&game_root,
|
||||
&args.mission,
|
||||
LoadOptions {
|
||||
load_model_textures: !args.no_model_texture,
|
||||
load_terrain_texture: !args.no_terrain_texture,
|
||||
},
|
||||
)
|
||||
.map_err(|err| format!("failed to load mission scene: {err}"))?;
|
||||
|
||||
let terrain_mesh = terrain_core::build_render_mesh(&scene.terrain)
|
||||
.map_err(|err| format!("failed to build terrain render mesh: {err}"))?;
|
||||
|
||||
let instance_count = scene
|
||||
.models
|
||||
.iter()
|
||||
.map(|model| model.instances.len())
|
||||
.sum::<usize>();
|
||||
println!(
|
||||
"mission loaded: map='{}', terrain_vertices={}, terrain_faces={}, models={}, instances={}, skipped={}",
|
||||
scene.mission.footer.map_path,
|
||||
scene.terrain.positions.len(),
|
||||
scene.terrain.faces.len(),
|
||||
scene.models.len(),
|
||||
instance_count,
|
||||
scene.skipped_objects
|
||||
);
|
||||
|
||||
let sdl = sdl2::init().map_err(|err| format!("failed to init SDL2: {err}"))?;
|
||||
let video = sdl
|
||||
.video()
|
||||
.map_err(|err| format!("failed to init SDL2 video: {err}"))?;
|
||||
|
||||
let (mut window, _gl_ctx, gl_backend) =
|
||||
create_window_and_context(&video, args.width, args.height)?;
|
||||
let _ = video.gl_set_swap_interval(1);
|
||||
|
||||
let gl = unsafe {
|
||||
glow::Context::from_loader_function(|name| video.gl_get_proc_address(name) as *const _)
|
||||
};
|
||||
|
||||
let program = unsafe { create_program(&gl, gl_backend)? };
|
||||
let u_mvp = unsafe { gl.get_uniform_location(program, "u_mvp") };
|
||||
let u_use_tex = unsafe { gl.get_uniform_location(program, "u_use_tex") };
|
||||
let u_tex = unsafe { gl.get_uniform_location(program, "u_tex") };
|
||||
let a_pos = unsafe { gl.get_attrib_location(program, "a_pos") }
|
||||
.ok_or_else(|| String::from("shader attribute a_pos is missing"))?;
|
||||
let a_uv = unsafe { gl.get_attrib_location(program, "a_uv") }
|
||||
.ok_or_else(|| String::from("shader attribute a_uv is missing"))?;
|
||||
|
||||
let terrain_gpu =
|
||||
unsafe { upload_terrain_renderable(&gl, &terrain_mesh, scene.terrain_texture.as_ref())? };
|
||||
|
||||
let mut model_gpus = Vec::new();
|
||||
for model in &scene.models {
|
||||
let renderable = unsafe { upload_model_renderable(&gl, model)? };
|
||||
model_gpus.push(renderable);
|
||||
}
|
||||
|
||||
let (scene_center, scene_radius) = initial_scene_sphere(&scene);
|
||||
let mut camera = Camera {
|
||||
position: [
|
||||
scene_center[0],
|
||||
scene_center[1] + scene_radius * 0.6,
|
||||
scene_center[2] + scene_radius * 1.4,
|
||||
],
|
||||
yaw: std::f32::consts::PI,
|
||||
pitch: -0.28,
|
||||
move_speed: (scene_radius * 0.55).max(60.0),
|
||||
mouse_sensitivity: 0.005,
|
||||
};
|
||||
|
||||
let mut events = sdl
|
||||
.event_pump()
|
||||
.map_err(|err| format!("failed to get SDL event pump: {err}"))?;
|
||||
let mut last = Instant::now();
|
||||
let mut fps_window_start = Instant::now();
|
||||
let mut fps_frames = 0u32;
|
||||
let mut fps_printed = false;
|
||||
let mut mouse_look = false;
|
||||
|
||||
'main_loop: loop {
|
||||
for event in events.poll_iter() {
|
||||
match event {
|
||||
sdl2::event::Event::Quit { .. } => break 'main_loop,
|
||||
sdl2::event::Event::KeyDown {
|
||||
keycode: Some(sdl2::keyboard::Keycode::Escape),
|
||||
..
|
||||
} => break 'main_loop,
|
||||
sdl2::event::Event::MouseButtonDown {
|
||||
mouse_btn: sdl2::mouse::MouseButton::Right,
|
||||
..
|
||||
} => {
|
||||
mouse_look = true;
|
||||
sdl.mouse().set_relative_mouse_mode(true);
|
||||
}
|
||||
sdl2::event::Event::MouseButtonUp {
|
||||
mouse_btn: sdl2::mouse::MouseButton::Right,
|
||||
..
|
||||
} => {
|
||||
mouse_look = false;
|
||||
sdl.mouse().set_relative_mouse_mode(false);
|
||||
}
|
||||
sdl2::event::Event::MouseMotion { xrel, yrel, .. } if mouse_look => {
|
||||
camera.yaw += xrel as f32 * camera.mouse_sensitivity;
|
||||
camera.pitch -= yrel as f32 * camera.mouse_sensitivity;
|
||||
camera.pitch = camera.pitch.clamp(-1.54, 1.54);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
let dt = (now - last).as_secs_f32().clamp(0.0, 0.05);
|
||||
last = now;
|
||||
|
||||
update_camera(&events, &mut camera, dt);
|
||||
|
||||
let (w, h) = window.size();
|
||||
let proj = mat4_perspective(
|
||||
args.fov_deg.to_radians(),
|
||||
(w as f32 / h.max(1) as f32).max(0.01),
|
||||
0.1,
|
||||
(scene_radius * 25.0).max(5000.0),
|
||||
);
|
||||
let forward = camera_forward(camera.yaw, camera.pitch);
|
||||
let view = mat4_look_at(
|
||||
camera.position,
|
||||
[
|
||||
camera.position[0] + forward[0],
|
||||
camera.position[1] + forward[1],
|
||||
camera.position[2] + forward[2],
|
||||
],
|
||||
[0.0, 1.0, 0.0],
|
||||
);
|
||||
|
||||
unsafe {
|
||||
draw_frame_begin(&gl, w, h);
|
||||
|
||||
let terrain_mvp = mat4_mul(&proj, &view);
|
||||
draw_gpu_renderable(
|
||||
&gl,
|
||||
program,
|
||||
u_mvp.as_ref(),
|
||||
u_use_tex.as_ref(),
|
||||
u_tex.as_ref(),
|
||||
a_pos,
|
||||
a_uv,
|
||||
&terrain_gpu,
|
||||
&terrain_mvp,
|
||||
);
|
||||
|
||||
for model in &model_gpus {
|
||||
for instance in &model.instances {
|
||||
let model_m = model_matrix(instance.position, instance.yaw_rad, instance.scale);
|
||||
let view_model = mat4_mul(&view, &model_m);
|
||||
let mvp = mat4_mul(&proj, &view_model);
|
||||
draw_gpu_renderable(
|
||||
&gl,
|
||||
program,
|
||||
u_mvp.as_ref(),
|
||||
u_use_tex.as_ref(),
|
||||
u_tex.as_ref(),
|
||||
a_pos,
|
||||
a_uv,
|
||||
&model.gpu,
|
||||
&mvp,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.gl_swap_window();
|
||||
|
||||
fps_frames = fps_frames.saturating_add(1);
|
||||
let elapsed = fps_window_start.elapsed();
|
||||
if elapsed >= Duration::from_millis(500) {
|
||||
let fps = fps_frames as f32 / elapsed.as_secs_f32().max(0.000_1);
|
||||
let frame_time_ms = 1000.0 / fps.max(0.000_1);
|
||||
let _ = window.set_title(&format!(
|
||||
"Parkan Mission Demo | FPS: {fps:.1} ({frame_time_ms:.2} ms) | objects: {instance_count}"
|
||||
));
|
||||
print!("\rFPS: {fps:.1} ({frame_time_ms:.2} ms)");
|
||||
let _ = std::io::stdout().flush();
|
||||
fps_printed = true;
|
||||
fps_frames = 0;
|
||||
fps_window_start = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
if fps_printed {
|
||||
println!();
|
||||
}
|
||||
|
||||
unsafe {
|
||||
cleanup_renderable(&gl, terrain_gpu);
|
||||
for model in model_gpus {
|
||||
cleanup_renderable(&gl, model.gpu);
|
||||
}
|
||||
gl.delete_program(program);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn initial_scene_sphere(scene: &MissionScene) -> ([f32; 3], f32) {
|
||||
if let Some((min_v, max_v)) = compute_scene_bounds(scene) {
|
||||
let center = [
|
||||
0.5 * (min_v[0] + max_v[0]),
|
||||
0.5 * (min_v[1] + max_v[1]),
|
||||
0.5 * (min_v[2] + max_v[2]),
|
||||
];
|
||||
let extent = [
|
||||
max_v[0] - min_v[0],
|
||||
max_v[1] - min_v[1],
|
||||
max_v[2] - min_v[2],
|
||||
];
|
||||
let radius = ((extent[0] * extent[0]) + (extent[1] * extent[1]) + (extent[2] * extent[2]))
|
||||
.sqrt()
|
||||
.max(10.0)
|
||||
* 0.5;
|
||||
return (center, radius);
|
||||
}
|
||||
([0.0, 0.0, 0.0], 100.0)
|
||||
}
|
||||
|
||||
fn update_camera(events: &sdl2::EventPump, camera: &mut Camera, dt: f32) {
|
||||
use sdl2::keyboard::Scancode;
|
||||
|
||||
let keys = events.keyboard_state();
|
||||
let mut move_dir = [0.0f32, 0.0f32, 0.0f32];
|
||||
|
||||
let forward = camera_forward(camera.yaw, camera.pitch);
|
||||
let right = normalize3(cross3(forward, [0.0, 1.0, 0.0]));
|
||||
|
||||
if keys.is_scancode_pressed(Scancode::Up) || keys.is_scancode_pressed(Scancode::W) {
|
||||
move_dir[0] += forward[0];
|
||||
move_dir[1] += forward[1];
|
||||
move_dir[2] += forward[2];
|
||||
}
|
||||
if keys.is_scancode_pressed(Scancode::Down) || keys.is_scancode_pressed(Scancode::S) {
|
||||
move_dir[0] -= forward[0];
|
||||
move_dir[1] -= forward[1];
|
||||
move_dir[2] -= forward[2];
|
||||
}
|
||||
if keys.is_scancode_pressed(Scancode::Left) || keys.is_scancode_pressed(Scancode::A) {
|
||||
move_dir[0] -= right[0];
|
||||
move_dir[1] -= right[1];
|
||||
move_dir[2] -= right[2];
|
||||
}
|
||||
if keys.is_scancode_pressed(Scancode::Right) || keys.is_scancode_pressed(Scancode::D) {
|
||||
move_dir[0] += right[0];
|
||||
move_dir[1] += right[1];
|
||||
move_dir[2] += right[2];
|
||||
}
|
||||
if keys.is_scancode_pressed(Scancode::PageUp) || keys.is_scancode_pressed(Scancode::E) {
|
||||
move_dir[1] += 1.0;
|
||||
}
|
||||
if keys.is_scancode_pressed(Scancode::PageDown) || keys.is_scancode_pressed(Scancode::Q) {
|
||||
move_dir[1] -= 1.0;
|
||||
}
|
||||
|
||||
let shift =
|
||||
keys.is_scancode_pressed(Scancode::LShift) || keys.is_scancode_pressed(Scancode::RShift);
|
||||
let speed_mul = if shift { 3.0 } else { 1.0 };
|
||||
|
||||
let norm = normalize3(move_dir);
|
||||
camera.position[0] += norm[0] * camera.move_speed * speed_mul * dt;
|
||||
camera.position[1] += norm[1] * camera.move_speed * speed_mul * dt;
|
||||
camera.position[2] += norm[2] * camera.move_speed * speed_mul * dt;
|
||||
}
|
||||
|
||||
unsafe fn upload_model_renderable(
|
||||
gl: &glow::Context,
|
||||
model: &render_mission_demo::SceneModel,
|
||||
) -> Result<ModelRenderable, String> {
|
||||
let mut vertex_data = Vec::with_capacity(model.mesh.vertices.len() * 5);
|
||||
for vertex in &model.mesh.vertices {
|
||||
vertex_data.push(vertex.position[0]);
|
||||
vertex_data.push(vertex.position[1]);
|
||||
vertex_data.push(vertex.position[2]);
|
||||
vertex_data.push(vertex.uv0[0]);
|
||||
vertex_data.push(vertex.uv0[1]);
|
||||
}
|
||||
|
||||
let gpu = upload_gpu_renderable(
|
||||
gl,
|
||||
&vertex_data,
|
||||
&model.mesh.indices,
|
||||
model.texture.as_ref(),
|
||||
)?;
|
||||
|
||||
Ok(ModelRenderable {
|
||||
gpu,
|
||||
instances: model.instances.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
unsafe fn upload_terrain_renderable(
|
||||
gl: &glow::Context,
|
||||
mesh: &terrain_core::TerrainRenderMesh,
|
||||
texture: Option<&render_demo::LoadedTexture>,
|
||||
) -> Result<GpuRenderable, String> {
|
||||
let mut vertex_data = Vec::with_capacity(mesh.vertices.len() * 5);
|
||||
for vertex in &mesh.vertices {
|
||||
vertex_data.push(vertex.position[0]);
|
||||
vertex_data.push(vertex.position[1]);
|
||||
vertex_data.push(vertex.position[2]);
|
||||
vertex_data.push(vertex.uv0[0]);
|
||||
vertex_data.push(vertex.uv0[1]);
|
||||
}
|
||||
|
||||
upload_gpu_renderable(gl, &vertex_data, &mesh.indices, texture)
|
||||
}
|
||||
|
||||
unsafe fn upload_gpu_renderable(
|
||||
gl: &glow::Context,
|
||||
vertices: &[f32],
|
||||
indices: &[u16],
|
||||
texture: Option<&render_demo::LoadedTexture>,
|
||||
) -> Result<GpuRenderable, String> {
|
||||
let vbo = gl.create_buffer().map_err(|e| e.to_string())?;
|
||||
let ebo = gl.create_buffer().map_err(|e| e.to_string())?;
|
||||
|
||||
let vertex_bytes = f32_slice_to_ne_bytes(vertices);
|
||||
let index_bytes = u16_slice_to_ne_bytes(indices);
|
||||
|
||||
gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
|
||||
gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, &vertex_bytes, glow::STATIC_DRAW);
|
||||
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(ebo));
|
||||
gl.buffer_data_u8_slice(glow::ELEMENT_ARRAY_BUFFER, &index_bytes, glow::STATIC_DRAW);
|
||||
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None);
|
||||
gl.bind_buffer(glow::ARRAY_BUFFER, None);
|
||||
|
||||
let gpu_texture = if let Some(texture) = texture {
|
||||
Some(create_texture(gl, texture)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(GpuRenderable {
|
||||
vbo,
|
||||
ebo,
|
||||
index_count: indices.len(),
|
||||
texture: gpu_texture,
|
||||
})
|
||||
}
|
||||
|
||||
unsafe fn cleanup_renderable(gl: &glow::Context, renderable: GpuRenderable) {
|
||||
if let Some(tex) = renderable.texture {
|
||||
gl.delete_texture(tex.handle);
|
||||
}
|
||||
gl.delete_buffer(renderable.ebo);
|
||||
gl.delete_buffer(renderable.vbo);
|
||||
}
|
||||
|
||||
unsafe fn draw_frame_begin(gl: &glow::Context, width: u32, height: u32) {
|
||||
gl.viewport(
|
||||
0,
|
||||
0,
|
||||
width.min(i32::MAX as u32) as i32,
|
||||
height.min(i32::MAX as u32) as i32,
|
||||
);
|
||||
gl.enable(glow::DEPTH_TEST);
|
||||
gl.clear_color(0.06, 0.08, 0.12, 1.0);
|
||||
gl.clear(glow::COLOR_BUFFER_BIT | glow::DEPTH_BUFFER_BIT);
|
||||
}
|
||||
|
||||
unsafe fn draw_gpu_renderable(
|
||||
gl: &glow::Context,
|
||||
program: glow::NativeProgram,
|
||||
u_mvp: Option<&glow::NativeUniformLocation>,
|
||||
u_use_tex: Option<&glow::NativeUniformLocation>,
|
||||
u_tex: Option<&glow::NativeUniformLocation>,
|
||||
a_pos: u32,
|
||||
a_uv: u32,
|
||||
renderable: &GpuRenderable,
|
||||
mvp: &[f32; 16],
|
||||
) {
|
||||
gl.use_program(Some(program));
|
||||
gl.uniform_matrix_4_f32_slice(u_mvp, false, mvp);
|
||||
|
||||
let texture_enabled = renderable.texture.is_some();
|
||||
gl.uniform_1_f32(u_use_tex, if texture_enabled { 1.0 } else { 0.0 });
|
||||
|
||||
if let Some(tex) = &renderable.texture {
|
||||
gl.active_texture(glow::TEXTURE0);
|
||||
gl.bind_texture(glow::TEXTURE_2D, Some(tex.handle));
|
||||
gl.uniform_1_i32(u_tex, 0);
|
||||
} else {
|
||||
gl.bind_texture(glow::TEXTURE_2D, None);
|
||||
}
|
||||
|
||||
gl.bind_buffer(glow::ARRAY_BUFFER, Some(renderable.vbo));
|
||||
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(renderable.ebo));
|
||||
gl.enable_vertex_attrib_array(a_pos);
|
||||
gl.vertex_attrib_pointer_f32(a_pos, 3, glow::FLOAT, false, 20, 0);
|
||||
gl.enable_vertex_attrib_array(a_uv);
|
||||
gl.vertex_attrib_pointer_f32(a_uv, 2, glow::FLOAT, false, 20, 12);
|
||||
|
||||
gl.draw_elements(
|
||||
glow::TRIANGLES,
|
||||
renderable.index_count.min(i32::MAX as usize) as i32,
|
||||
glow::UNSIGNED_SHORT,
|
||||
0,
|
||||
);
|
||||
|
||||
gl.disable_vertex_attrib_array(a_uv);
|
||||
gl.disable_vertex_attrib_array(a_pos);
|
||||
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None);
|
||||
gl.bind_buffer(glow::ARRAY_BUFFER, None);
|
||||
gl.bind_texture(glow::TEXTURE_2D, None);
|
||||
gl.use_program(None);
|
||||
}
|
||||
|
||||
fn create_window_and_context(
|
||||
video: &sdl2::VideoSubsystem,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<(sdl2::video::Window, sdl2::video::GLContext, GlBackend), String> {
|
||||
let candidates = [
|
||||
(GlBackend::Gles2, sdl2::video::GLProfile::GLES, 2, 0),
|
||||
(GlBackend::Core33, sdl2::video::GLProfile::Core, 3, 3),
|
||||
];
|
||||
let mut errors = Vec::new();
|
||||
|
||||
for (backend, profile, major, minor) in candidates {
|
||||
{
|
||||
let gl_attr = video.gl_attr();
|
||||
gl_attr.set_context_profile(profile);
|
||||
gl_attr.set_context_version(major, minor);
|
||||
gl_attr.set_depth_size(24);
|
||||
gl_attr.set_double_buffer(true);
|
||||
}
|
||||
|
||||
let mut window_builder = video.window("Parkan Mission Demo", width, height);
|
||||
window_builder.opengl().resizable();
|
||||
|
||||
let window = match window_builder.build() {
|
||||
Ok(window) => window,
|
||||
Err(err) => {
|
||||
errors.push(format!(
|
||||
"{profile:?} {major}.{minor}: window build failed ({err})"
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let gl_ctx = match window.gl_create_context() {
|
||||
Ok(ctx) => ctx,
|
||||
Err(err) => {
|
||||
errors.push(format!(
|
||||
"{profile:?} {major}.{minor}: context create failed ({err})"
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = window.gl_make_current(&gl_ctx) {
|
||||
errors.push(format!(
|
||||
"{profile:?} {major}.{minor}: make current failed ({err})"
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
return Ok((window, gl_ctx, backend));
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"failed to create OpenGL context. Attempts: {}",
|
||||
errors.join(" | ")
|
||||
))
|
||||
}
|
||||
|
||||
unsafe fn create_texture(
|
||||
gl: &glow::Context,
|
||||
texture: &render_demo::LoadedTexture,
|
||||
) -> Result<GpuTexture, String> {
|
||||
let handle = gl.create_texture().map_err(|e| e.to_string())?;
|
||||
gl.bind_texture(glow::TEXTURE_2D, Some(handle));
|
||||
gl.tex_parameter_i32(
|
||||
glow::TEXTURE_2D,
|
||||
glow::TEXTURE_MIN_FILTER,
|
||||
glow::LINEAR as i32,
|
||||
);
|
||||
gl.tex_parameter_i32(
|
||||
glow::TEXTURE_2D,
|
||||
glow::TEXTURE_MAG_FILTER,
|
||||
glow::LINEAR as i32,
|
||||
);
|
||||
gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::REPEAT as i32);
|
||||
gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::REPEAT as i32);
|
||||
gl.pixel_store_i32(glow::UNPACK_ALIGNMENT, 1);
|
||||
gl.tex_image_2d(
|
||||
glow::TEXTURE_2D,
|
||||
0,
|
||||
glow::RGBA as i32,
|
||||
texture.width.min(i32::MAX as u32) as i32,
|
||||
texture.height.min(i32::MAX as u32) as i32,
|
||||
0,
|
||||
glow::RGBA,
|
||||
glow::UNSIGNED_BYTE,
|
||||
glow::PixelUnpackData::Slice(Some(texture.rgba8.as_slice())),
|
||||
);
|
||||
gl.bind_texture(glow::TEXTURE_2D, None);
|
||||
Ok(GpuTexture { handle })
|
||||
}
|
||||
|
||||
unsafe fn create_program(
|
||||
gl: &glow::Context,
|
||||
backend: GlBackend,
|
||||
) -> Result<glow::NativeProgram, String> {
|
||||
let (vs_src, fs_src) = match backend {
|
||||
GlBackend::Gles2 => (
|
||||
r#"
|
||||
attribute vec3 a_pos;
|
||||
attribute vec2 a_uv;
|
||||
uniform mat4 u_mvp;
|
||||
varying vec2 v_uv;
|
||||
void main() {
|
||||
v_uv = a_uv;
|
||||
gl_Position = u_mvp * vec4(a_pos, 1.0);
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
precision mediump float;
|
||||
uniform sampler2D u_tex;
|
||||
uniform float u_use_tex;
|
||||
varying vec2 v_uv;
|
||||
void main() {
|
||||
vec4 base = vec4(0.82, 0.87, 0.95, 1.0);
|
||||
vec4 texColor = texture2D(u_tex, v_uv);
|
||||
gl_FragColor = mix(base, texColor, u_use_tex);
|
||||
}
|
||||
"#,
|
||||
),
|
||||
GlBackend::Core33 => (
|
||||
r#"#version 330 core
|
||||
in vec3 a_pos;
|
||||
in vec2 a_uv;
|
||||
uniform mat4 u_mvp;
|
||||
out vec2 v_uv;
|
||||
void main() {
|
||||
v_uv = a_uv;
|
||||
gl_Position = u_mvp * vec4(a_pos, 1.0);
|
||||
}
|
||||
"#,
|
||||
r#"#version 330 core
|
||||
uniform sampler2D u_tex;
|
||||
uniform float u_use_tex;
|
||||
in vec2 v_uv;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 base = vec4(0.82, 0.87, 0.95, 1.0);
|
||||
vec4 texColor = texture(u_tex, v_uv);
|
||||
fragColor = mix(base, texColor, u_use_tex);
|
||||
}
|
||||
"#,
|
||||
),
|
||||
};
|
||||
|
||||
let program = gl.create_program().map_err(|e| e.to_string())?;
|
||||
let vs = gl
|
||||
.create_shader(glow::VERTEX_SHADER)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let fs = gl
|
||||
.create_shader(glow::FRAGMENT_SHADER)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
gl.shader_source(vs, vs_src);
|
||||
gl.compile_shader(vs);
|
||||
if !gl.get_shader_compile_status(vs) {
|
||||
let log = gl.get_shader_info_log(vs);
|
||||
gl.delete_shader(vs);
|
||||
gl.delete_shader(fs);
|
||||
gl.delete_program(program);
|
||||
return Err(format!("vertex shader compile failed: {log}"));
|
||||
}
|
||||
|
||||
gl.shader_source(fs, fs_src);
|
||||
gl.compile_shader(fs);
|
||||
if !gl.get_shader_compile_status(fs) {
|
||||
let log = gl.get_shader_info_log(fs);
|
||||
gl.delete_shader(vs);
|
||||
gl.delete_shader(fs);
|
||||
gl.delete_program(program);
|
||||
return Err(format!("fragment shader compile failed: {log}"));
|
||||
}
|
||||
|
||||
gl.attach_shader(program, vs);
|
||||
gl.attach_shader(program, fs);
|
||||
gl.link_program(program);
|
||||
|
||||
gl.detach_shader(program, vs);
|
||||
gl.detach_shader(program, fs);
|
||||
gl.delete_shader(vs);
|
||||
gl.delete_shader(fs);
|
||||
|
||||
if !gl.get_program_link_status(program) {
|
||||
let log = gl.get_program_info_log(program);
|
||||
gl.delete_program(program);
|
||||
return Err(format!("program link failed: {log}"));
|
||||
}
|
||||
|
||||
Ok(program)
|
||||
}
|
||||
|
||||
fn model_matrix(position: [f32; 3], yaw: f32, scale: [f32; 3]) -> [f32; 16] {
|
||||
let translation = mat4_translation(position[0], position[1], position[2]);
|
||||
let rotation = mat4_rotation_y(yaw);
|
||||
let scaling = mat4_scale(scale[0], scale[1], scale[2]);
|
||||
let tr = mat4_mul(&translation, &rotation);
|
||||
mat4_mul(&tr, &scaling)
|
||||
}
|
||||
|
||||
fn camera_forward(yaw: f32, pitch: f32) -> [f32; 3] {
|
||||
let cp = pitch.cos();
|
||||
normalize3([yaw.sin() * cp, pitch.sin(), yaw.cos() * cp])
|
||||
}
|
||||
|
||||
fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
|
||||
[
|
||||
a[1] * b[2] - a[2] * b[1],
|
||||
a[2] * b[0] - a[0] * b[2],
|
||||
a[0] * b[1] - a[1] * b[0],
|
||||
]
|
||||
}
|
||||
|
||||
fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
|
||||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
}
|
||||
|
||||
fn normalize3(v: [f32; 3]) -> [f32; 3] {
|
||||
let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
|
||||
if len <= 1e-6 {
|
||||
[0.0, 0.0, 0.0]
|
||||
} else {
|
||||
[v[0] / len, v[1] / len, v[2] / len]
|
||||
}
|
||||
}
|
||||
|
||||
fn mat4_identity() -> [f32; 16] {
|
||||
[
|
||||
1.0, 0.0, 0.0, 0.0, //
|
||||
0.0, 1.0, 0.0, 0.0, //
|
||||
0.0, 0.0, 1.0, 0.0, //
|
||||
0.0, 0.0, 0.0, 1.0, //
|
||||
]
|
||||
}
|
||||
|
||||
fn mat4_translation(x: f32, y: f32, z: f32) -> [f32; 16] {
|
||||
let mut m = mat4_identity();
|
||||
m[12] = x;
|
||||
m[13] = y;
|
||||
m[14] = z;
|
||||
m
|
||||
}
|
||||
|
||||
fn mat4_scale(x: f32, y: f32, z: f32) -> [f32; 16] {
|
||||
[
|
||||
x, 0.0, 0.0, 0.0, //
|
||||
0.0, y, 0.0, 0.0, //
|
||||
0.0, 0.0, z, 0.0, //
|
||||
0.0, 0.0, 0.0, 1.0, //
|
||||
]
|
||||
}
|
||||
|
||||
fn mat4_rotation_y(rad: f32) -> [f32; 16] {
|
||||
let c = rad.cos();
|
||||
let s = rad.sin();
|
||||
[
|
||||
c, 0.0, -s, 0.0, //
|
||||
0.0, 1.0, 0.0, 0.0, //
|
||||
s, 0.0, c, 0.0, //
|
||||
0.0, 0.0, 0.0, 1.0, //
|
||||
]
|
||||
}
|
||||
|
||||
fn mat4_perspective(fovy: f32, aspect: f32, near: f32, far: f32) -> [f32; 16] {
|
||||
let f = 1.0 / (0.5 * fovy).tan();
|
||||
let nf = 1.0 / (near - far);
|
||||
[
|
||||
f / aspect,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
f,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
(far + near) * nf,
|
||||
-1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
(2.0 * far * near) * nf,
|
||||
0.0,
|
||||
]
|
||||
}
|
||||
|
||||
fn mat4_look_at(eye: [f32; 3], target: [f32; 3], up: [f32; 3]) -> [f32; 16] {
|
||||
let f = normalize3([target[0] - eye[0], target[1] - eye[1], target[2] - eye[2]]);
|
||||
let s = normalize3(cross3(f, up));
|
||||
let u = cross3(s, f);
|
||||
|
||||
[
|
||||
s[0],
|
||||
u[0],
|
||||
-f[0],
|
||||
0.0,
|
||||
s[1],
|
||||
u[1],
|
||||
-f[1],
|
||||
0.0,
|
||||
s[2],
|
||||
u[2],
|
||||
-f[2],
|
||||
0.0,
|
||||
-dot3(s, eye),
|
||||
-dot3(u, eye),
|
||||
dot3(f, eye),
|
||||
1.0,
|
||||
]
|
||||
}
|
||||
|
||||
fn mat4_mul(a: &[f32; 16], b: &[f32; 16]) -> [f32; 16] {
|
||||
let mut out = [0.0f32; 16];
|
||||
for c in 0..4 {
|
||||
for r in 0..4 {
|
||||
let mut acc = 0.0f32;
|
||||
for k in 0..4 {
|
||||
acc += a[k * 4 + r] * b[c * 4 + k];
|
||||
}
|
||||
out[c * 4 + r] = acc;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn f32_slice_to_ne_bytes(slice: &[f32]) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(slice.len().saturating_mul(std::mem::size_of::<f32>()));
|
||||
for &value in slice {
|
||||
out.extend_from_slice(&value.to_ne_bytes());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn u16_slice_to_ne_bytes(slice: &[u16]) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(slice.len().saturating_mul(std::mem::size_of::<u16>()));
|
||||
for &value in slice {
|
||||
out.extend_from_slice(&value.to_ne_bytes());
|
||||
}
|
||||
out
|
||||
}
|
||||
Reference in New Issue
Block a user