use std::f32::consts::PI;
use std::os::raw::c_void;
use std::path::Path;
use std::process;
use std::time::Instant;
use cgmath::{ Deg, Point3 };
use collision::Aabb;
use gl;
use gltf;
use glutin;
use glutin::{
Api,
MouseScrollDelta,
MouseButton,
GlContext,
GlRequest,
GlProfile,
VirtualKeyCode,
WindowEvent,
};
use glutin::dpi::PhysicalSize;
use glutin::ElementState::*;
use image::{DynamicImage};
use crate::controls::{OrbitControls, NavState};
use crate::controls::CameraMovement::*;
use crate::framebuffer::Framebuffer;
use crate::importdata::ImportData;
use crate::render::*;
use crate::render::math::*;
use crate::utils::{print_elapsed, FrameTimer, gl_check_error, print_context_info};
#[derive(Copy, Clone)]
pub struct CameraOptions {
pub index: i32,
pub position: Option<Vector3>,
pub target: Option<Vector3>,
pub fovy: Deg<f32>,
pub straight: bool,
}
pub struct GltfViewer {
size: PhysicalSize,
dpi_factor: f64,
orbit_controls: OrbitControls,
events_loop: Option<glutin::EventsLoop>,
gl_window: Option<glutin::GlWindow>,
root: Root,
scene: Scene,
delta_time: f64, last_frame: Instant,
render_timer: FrameTimer,
}
impl GltfViewer {
pub fn new(
source: &str,
width: u32,
height: u32,
headless: bool,
visible: bool,
camera_options: CameraOptions,
scene_index: usize,
) -> GltfViewer {
let gl_request = GlRequest::Specific(Api::OpenGl, (3, 3));
let gl_profile = GlProfile::Core;
let (events_loop, gl_window, dpi_factor, inner_size) =
if headless {
let headless_context = glutin::HeadlessRendererBuilder::new(width, height)
.build()
.unwrap();
unsafe { headless_context.make_current().unwrap() }
gl::load_with(|symbol| headless_context.get_proc_address(symbol) as *const _);
let framebuffer = Framebuffer::new(width, height);
framebuffer.bind();
unsafe { gl::Viewport(0, 0, width as i32, height as i32); }
(None, None, 1.0, PhysicalSize::new(width as f64, height as f64)) }
else {
let events_loop = glutin::EventsLoop::new();
let window_size = glutin::dpi::LogicalSize::new(width as f64, height as f64);
let window = glutin::WindowBuilder::new()
.with_title("gltf-viewer")
.with_dimensions(window_size)
.with_visibility(visible);
let context = glutin::ContextBuilder::new()
.with_gl(gl_request)
.with_gl_profile(gl_profile)
.with_vsync(true);
let gl_window = glutin::GlWindow::new(window, context, &events_loop).unwrap();
let dpi_factor = gl_window.get_hidpi_factor();
let inner_size = gl_window.get_inner_size().unwrap().to_physical(dpi_factor);
unsafe { gl_window.make_current().unwrap(); }
gl::load_with(|symbol| gl_window.get_proc_address(symbol) as *const _);
(Some(events_loop), Some(gl_window), dpi_factor, inner_size)
};
let mut orbit_controls = OrbitControls::new(
Point3::new(0.0, 0.0, 2.0),
inner_size);
orbit_controls.camera = Camera::default();
orbit_controls.camera.fovy = camera_options.fovy;
orbit_controls.camera.update_aspect_ratio(inner_size.width as f32 / inner_size.height as f32);
unsafe {
print_context_info();
gl::ClearColor(0.0, 1.0, 0.0, 1.0); gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT);
if headless || !visible {
gl::ClearColor(0.0, 0.0, 0.0, 0.0);
}
else {
gl::ClearColor(0.1, 0.2, 0.3, 1.0);
}
gl::Enable(gl::DEPTH_TEST);
};
let (root, scene) = Self::load(source, scene_index);
let mut viewer = GltfViewer {
size: inner_size,
dpi_factor,
orbit_controls,
events_loop,
gl_window,
root,
scene,
delta_time: 0.0, last_frame: Instant::now(),
render_timer: FrameTimer::new("rendering", 300),
};
unsafe { gl_check_error!(); };
if camera_options.index != 0 && camera_options.index >= viewer.root.camera_nodes.len() as i32 {
error!("No camera with index {} found in glTF file (max: {})",
camera_options.index, viewer.root.camera_nodes.len() as i32 - 1);
process::exit(2)
}
if !viewer.root.camera_nodes.is_empty() && camera_options.index != -1 {
let cam_node = &viewer.root.get_camera_node(camera_options.index as usize);
let cam_node_info = format!("{} ({:?})", cam_node.index, cam_node.name);
let cam = cam_node.camera.as_ref().unwrap();
info!("Using camera {} on node {}", cam.description(), cam_node_info);
viewer.orbit_controls.set_camera(cam, &cam_node.final_transform);
if camera_options.position.is_some() || camera_options.target.is_some() {
warn!("Ignoring --cam-pos / --cam-target since --cam-index is given.")
}
} else {
info!("Determining camera view from bounding box");
viewer.set_camera_from_bounds(camera_options.straight);
if let Some(p) = camera_options.position {
viewer.orbit_controls.position = Point3::from_vec(p)
}
if let Some(target) = camera_options.target {
viewer.orbit_controls.target = Point3::from_vec(target)
}
}
viewer
}
pub fn load(source: &str, scene_index: usize) -> (Root, Scene) {
let mut start_time = Instant::now();
if source.starts_with("http") {
panic!("not implemented: HTTP support temporarily removed.")
}
let (doc, buffers, images) = match gltf::import(source) {
Ok(tuple) => tuple,
Err(err) => {
error!("glTF import failed: {:?}", err);
if let gltf::Error::Io(_) = err {
error!("Hint: Are the .bin file(s) referenced by the .gltf file available?")
}
process::exit(1)
},
};
let imp = ImportData { doc, buffers, images };
print_elapsed("Imported glTF in ", start_time);
start_time = Instant::now();
if scene_index >= imp.doc.scenes().len() {
error!("Scene index too high - file has only {} scene(s)", imp.doc.scenes().len());
process::exit(3)
}
let base_path = Path::new(source);
let mut root = Root::from_gltf(&imp, base_path);
let scene = Scene::from_gltf(&imp.doc.scenes().nth(scene_index).unwrap(), &mut root);
print_elapsed(&format!("Loaded scene with {} nodes, {} meshes in ",
imp.doc.nodes().count(), imp.doc.meshes().len()), start_time);
(root, scene)
}
fn set_camera_from_bounds(&mut self, straight: bool) {
let bounds = &self.scene.bounds;
let size = (bounds.max - bounds.min).magnitude();
let center = bounds.center();
let cam_pos = if straight {
Point3::new(
center.x,
center.y,
center.z + size * 0.75,
)
} else {
Point3::new(
center.x + size / 2.0,
center.y + size / 5.0,
center.z + size / 2.0,
)
};
self.orbit_controls.position = cam_pos;
self.orbit_controls.target = center;
self.orbit_controls.camera.znear = size / 100.0;
self.orbit_controls.camera.zfar = Some(size * 20.0);
self.orbit_controls.camera.update_projection_matrix();
}
pub fn start_render_loop(&mut self) {
loop {
self.delta_time = f64::from(self.last_frame.elapsed().subsec_nanos()) / 1_000_000_000.0;
self.last_frame = Instant::now();
let keep_running = process_events(
&mut self.events_loop.as_mut().unwrap(),
self.gl_window.as_mut().unwrap(),
&mut self.orbit_controls,
&mut self.dpi_factor,
&mut self.size);
if !keep_running {
unsafe { gl_check_error!(); } break
}
self.orbit_controls.frame_update(self.delta_time);
self.draw();
self.gl_window.as_ref().unwrap().swap_buffers().unwrap();
}
}
pub fn draw(&mut self) {
unsafe {
self.render_timer.start();
gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT);
let cam_params = self.orbit_controls.camera_params();
self.scene.draw(&mut self.root, &cam_params);
self.render_timer.end();
}
}
pub fn screenshot(&mut self, filename: &str) {
self.draw();
let mut img = DynamicImage::new_rgba8(self.size.width as u32, self.size.height as u32);
unsafe {
let pixels = img.as_mut_rgba8().unwrap();
gl::PixelStorei(gl::PACK_ALIGNMENT, 1);
gl::ReadPixels(0, 0, self.size.width as i32, self.size.height as i32, gl::RGBA,
gl::UNSIGNED_BYTE, pixels.as_mut_ptr() as *mut c_void);
gl_check_error!();
}
let img = img.flipv();
if let Err(err) = img.save(filename) {
error!("{}", err);
}
else {
println!("Saved {}x{} screenshot to {}", self.size.width, self.size.height, filename);
}
}
pub fn multiscreenshot(&mut self, filename: &str, count: u32) {
let min_angle : f32 = 0.0 ;
let max_angle : f32 = 2.0 * PI ;
let increment_angle : f32 = ((max_angle - min_angle)/(count as f32)) as f32;
for i in 1..=count {
self.orbit_controls.rotate_object(increment_angle);
let dot = filename.rfind('.').unwrap_or_else(|| filename.len());
let mut actual_name = filename.to_string();
actual_name.insert_str(dot, &format!("_{}", i));
self.screenshot(&actual_name[..]);
}
}
}
#[allow(clippy::too_many_arguments)]
fn process_events(
events_loop: &mut glutin::EventsLoop,
gl_window: &glutin::GlWindow,
mut orbit_controls: &mut OrbitControls,
dpi_factor: &mut f64,
size: &mut PhysicalSize) -> bool
{
let mut keep_running = true;
#[allow(clippy::single_match)]
events_loop.poll_events(|event| {
match event {
glutin::Event::WindowEvent{ event, .. } => match event {
WindowEvent::CloseRequested => {
keep_running = false;
},
WindowEvent::Destroyed => {
panic!("WindowEvent::Destroyed, unimplemented.");
},
WindowEvent::Resized(logical) => {
let ph = logical.to_physical(*dpi_factor);
gl_window.resize(ph);
unsafe { gl::Viewport(0, 0, ph.width as i32, ph.height as i32); }
*size = ph;
orbit_controls.camera.update_aspect_ratio((ph.width / ph.height) as f32);
orbit_controls.screen_size = ph;
},
WindowEvent::HiDpiFactorChanged(f) => {
*dpi_factor = f;
},
WindowEvent::DroppedFile(_path_buf) => {
}
WindowEvent::MouseInput { button, state: Pressed, ..} => {
match button {
MouseButton::Left => {
orbit_controls.state = NavState::Rotating;
},
MouseButton::Right => {
orbit_controls.state = NavState::Panning;
},
_ => ()
}
},
WindowEvent::MouseInput { button, state: Released, ..} => {
match (button, orbit_controls.state.clone()) {
(MouseButton::Left, NavState::Rotating) | (MouseButton::Right, NavState::Panning) => {
orbit_controls.state = NavState::None;
orbit_controls.handle_mouse_up();
},
_ => ()
}
}
WindowEvent::CursorMoved { position, .. } => {
let ph = position.to_physical(*dpi_factor);
orbit_controls.handle_mouse_move(ph)
},
WindowEvent::MouseWheel { delta: MouseScrollDelta::PixelDelta(logical), .. } => {
let ph = logical.to_physical(*dpi_factor);
orbit_controls.process_mouse_scroll(ph.y as f32);
}
WindowEvent::MouseWheel { delta: MouseScrollDelta::LineDelta(_rows, lines), .. } => {
orbit_controls.process_mouse_scroll(lines * 3.0);
}
WindowEvent::KeyboardInput { input, .. } => {
keep_running = process_input(input, &mut orbit_controls);
}
_ => ()
},
_ => ()
}
});
keep_running
}
fn process_input(input: glutin::KeyboardInput, controls: &mut OrbitControls) -> bool {
let pressed = match input.state {
Pressed => true,
Released => false
};
if let Some(code) = input.virtual_keycode {
match code {
VirtualKeyCode::Escape if pressed => return false,
VirtualKeyCode::W | VirtualKeyCode::Up => controls.process_keyboard(FORWARD, pressed),
VirtualKeyCode::S | VirtualKeyCode::Down => controls.process_keyboard(BACKWARD, pressed),
VirtualKeyCode::A | VirtualKeyCode::Left => controls.process_keyboard(LEFT, pressed),
VirtualKeyCode::D | VirtualKeyCode::Right => controls.process_keyboard(RIGHT, pressed),
_ => ()
}
}
true
}