Skip to main content

gltf_viewer_lib/
viewer.rs

1use std::f32::consts::PI;
2use std::os::raw::c_void;
3use std::path::Path;
4use std::process;
5use std::time::Instant;
6
7use cgmath::{Deg, Point3};
8use collision::Aabb;
9use gl;
10use gltf;
11use glutin;
12use glutin::dpi::PhysicalSize;
13use glutin::ElementState::*;
14use glutin::{
15    Api, GlContext, GlProfile, GlRequest, MouseButton, MouseScrollDelta, VirtualKeyCode,
16    WindowEvent,
17};
18
19use image::DynamicImage;
20use log::{error, info, warn};
21
22use crate::controls::CameraMovement::*;
23use crate::controls::{NavState, OrbitControls};
24use crate::framebuffer::Framebuffer;
25use crate::importdata::ImportData;
26use crate::render::math::*;
27use crate::render::*;
28use crate::utils::{gl_check_error, print_context_info, print_elapsed, FrameTimer};
29
30// TODO!: complete and pass through draw calls? or get rid of multiple shaders?
31// How about state ordering anyway?
32// struct DrawState {
33//     current_shader: ShaderFlags,
34//     back_face_culling_enabled: bool
35// }
36
37#[derive(Copy, Clone)]
38pub struct CameraOptions {
39    pub index: i32,
40    pub position: Option<Vector3>,
41    pub target: Option<Vector3>,
42    pub fovy: Deg<f32>,
43    pub straight: bool,
44}
45
46pub struct GltfViewer {
47    size: PhysicalSize,
48    dpi_factor: f64,
49
50    orbit_controls: OrbitControls,
51    events_loop: Option<glutin::EventsLoop>,
52    gl_window: Option<glutin::GlWindow>,
53
54    // TODO!: get rid of scene?
55    root: Root,
56    scene: Scene,
57
58    delta_time: f64, // seconds
59    last_frame: Instant,
60
61    render_timer: FrameTimer,
62}
63
64/// Note about `headless` and `visible`: True headless rendering doesn't work on
65/// all operating systems, but an invisible window usually works
66impl GltfViewer {
67    pub fn new(
68        source: &str,
69        width: u32,
70        height: u32,
71        headless: bool,
72        visible: bool,
73        camera_options: CameraOptions,
74        scene_index: usize,
75    ) -> GltfViewer {
76        let gl_request = GlRequest::Specific(Api::OpenGl, (3, 3));
77        let gl_profile = GlProfile::Core;
78        let (events_loop, gl_window, dpi_factor, inner_size) = if headless {
79            let headless_context = glutin::HeadlessRendererBuilder::new(width, height)
80                // .with_gl(gl_request)
81                // .with_gl_profile(gl_profile)
82                .build()
83                .unwrap();
84            unsafe { headless_context.make_current().unwrap() }
85            gl::load_with(|symbol| headless_context.get_proc_address(symbol) as *const _);
86            let framebuffer = Framebuffer::new(width, height);
87            framebuffer.bind();
88            unsafe {
89                gl::Viewport(0, 0, width as i32, height as i32);
90            }
91
92            (
93                None,
94                None,
95                1.0,
96                PhysicalSize::new(width as f64, height as f64),
97            ) // TODO: real height (retina? (should be the same as PhysicalSize when headless?))
98        } else {
99            // glutin: initialize and configure
100            let events_loop = glutin::EventsLoop::new();
101            let window_size = glutin::dpi::LogicalSize::new(width as f64, height as f64);
102
103            // TODO?: hints for 4.1, core profile, forward compat
104            let window = glutin::WindowBuilder::new()
105                .with_title("gltf-viewer")
106                .with_dimensions(window_size)
107                .with_visibility(visible);
108
109            let context = glutin::ContextBuilder::new()
110                .with_gl(gl_request)
111                .with_gl_profile(gl_profile)
112                .with_vsync(true);
113            let gl_window = glutin::GlWindow::new(window, context, &events_loop).unwrap();
114
115            // Real dimensions might be much higher on High-DPI displays
116            let dpi_factor = gl_window.get_hidpi_factor();
117            let inner_size = gl_window.get_inner_size().unwrap().to_physical(dpi_factor);
118
119            unsafe {
120                gl_window.make_current().unwrap();
121            }
122
123            // gl: load all OpenGL function pointers
124            gl::load_with(|symbol| gl_window.get_proc_address(symbol) as *const _);
125
126            (Some(events_loop), Some(gl_window), dpi_factor, inner_size)
127        };
128
129        let mut orbit_controls = OrbitControls::new(Point3::new(0.0, 0.0, 2.0), inner_size);
130        orbit_controls.camera = Camera::default();
131        orbit_controls.camera.fovy = camera_options.fovy;
132        orbit_controls
133            .camera
134            .update_aspect_ratio(inner_size.width as f32 / inner_size.height as f32); // updates projection matrix
135
136        unsafe {
137            print_context_info();
138
139            gl::ClearColor(0.0, 1.0, 0.0, 1.0); // green for debugging
140            gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT);
141
142            if headless || !visible {
143                // transparent background for screenshots
144                gl::ClearColor(0.0, 0.0, 0.0, 0.0);
145            } else {
146                gl::ClearColor(0.1, 0.2, 0.3, 1.0);
147            }
148
149            gl::Enable(gl::DEPTH_TEST);
150
151            // TODO: keyboard switch?
152            // draw in wireframe
153            // gl::PolygonMode(gl::FRONT_AND_BACK, gl::LINE);
154        };
155
156        let (root, scene) = Self::load(source, scene_index);
157        let mut viewer = GltfViewer {
158            size: inner_size,
159            dpi_factor,
160
161            orbit_controls,
162
163            events_loop,
164            gl_window,
165
166            root,
167            scene,
168
169            delta_time: 0.0, // seconds
170            last_frame: Instant::now(),
171
172            render_timer: FrameTimer::new("rendering", 300),
173        };
174        unsafe {
175            gl_check_error!();
176        };
177
178        if camera_options.index != 0
179            && camera_options.index >= viewer.root.camera_nodes.len() as i32
180        {
181            error!(
182                "No camera with index {} found in glTF file (max: {})",
183                camera_options.index,
184                viewer.root.camera_nodes.len() as i32 - 1
185            );
186            process::exit(2)
187        }
188        if !viewer.root.camera_nodes.is_empty() && camera_options.index != -1 {
189            let cam_node = &viewer.root.get_camera_node(camera_options.index as usize);
190            let cam_node_info = format!("{} ({:?})", cam_node.index, cam_node.name);
191            let cam = cam_node.camera.as_ref().unwrap();
192            info!(
193                "Using camera {} on node {}",
194                cam.description(),
195                cam_node_info
196            );
197            viewer
198                .orbit_controls
199                .set_camera(cam, &cam_node.final_transform);
200
201            if camera_options.position.is_some() || camera_options.target.is_some() {
202                warn!("Ignoring --cam-pos / --cam-target since --cam-index is given.")
203            }
204        } else {
205            info!("Determining camera view from bounding box");
206            viewer.set_camera_from_bounds(camera_options.straight);
207
208            if let Some(p) = camera_options.position {
209                viewer.orbit_controls.position = Point3::from_vec(p)
210            }
211            if let Some(target) = camera_options.target {
212                viewer.orbit_controls.target = Point3::from_vec(target)
213            }
214        }
215
216        viewer
217    }
218
219    pub fn load(source: &str, scene_index: usize) -> (Root, Scene) {
220        let mut start_time = Instant::now();
221        // TODO!: http source
222        // let gltf =
223        if source.starts_with("http") {
224            panic!("not implemented: HTTP support temporarily removed.")
225            // let http_source = HttpSource::new(source);
226            // let import = gltf::Import::custom(http_source, Default::default());
227            // let gltf = import_gltf(import);
228            // println!(); // to end the "progress dots"
229            // gltf
230        }
231        //     else {
232        let (doc, buffers, images) = match gltf::import(source) {
233            Ok(tuple) => tuple,
234            Err(err) => {
235                error!("glTF import failed: {:?}", err);
236                if let gltf::Error::Io(_) = err {
237                    error!("Hint: Are the .bin file(s) referenced by the .gltf file available?")
238                }
239                process::exit(1)
240            }
241        };
242        let imp = ImportData {
243            doc,
244            buffers,
245            images,
246        };
247
248        print_elapsed("Imported glTF in ", start_time);
249        start_time = Instant::now();
250
251        // load first scene
252        if scene_index >= imp.doc.scenes().len() {
253            error!(
254                "Scene index too high - file has only {} scene(s)",
255                imp.doc.scenes().len()
256            );
257            process::exit(3)
258        }
259        let base_path = Path::new(source);
260        let mut root = Root::from_gltf(&imp, base_path);
261        let scene = Scene::from_gltf(&imp.doc.scenes().nth(scene_index).unwrap(), &mut root);
262        print_elapsed(
263            &format!(
264                "Loaded scene with {} nodes, {} meshes in ",
265                imp.doc.nodes().count(),
266                imp.doc.meshes().len()
267            ),
268            start_time,
269        );
270
271        (root, scene)
272    }
273
274    /// determine "nice" camera perspective from bounding box. Inspired by donmccurdy/three-gltf-viewer
275    fn set_camera_from_bounds(&mut self, straight: bool) {
276        let bounds = &self.scene.bounds;
277        let size = (bounds.max - bounds.min).magnitude();
278        let center = bounds.center();
279
280        // TODO: x,y addition optional
281        let cam_pos = if straight {
282            Point3::new(center.x, center.y, center.z + size * 0.75)
283        } else {
284            Point3::new(
285                center.x + size / 2.0,
286                center.y + size / 5.0,
287                center.z + size / 2.0,
288            )
289        };
290
291        self.orbit_controls.position = cam_pos;
292        self.orbit_controls.target = center;
293        self.orbit_controls.camera.znear = size / 100.0;
294        self.orbit_controls.camera.zfar = Some(size * 20.0);
295        self.orbit_controls.camera.update_projection_matrix();
296    }
297
298    pub fn start_render_loop(&mut self) {
299        loop {
300            // per-frame time logic
301            // NOTE: Deliberately ignoring the seconds of `elapsed()`
302            self.delta_time = f64::from(self.last_frame.elapsed().subsec_nanos()) / 1_000_000_000.0;
303            self.last_frame = Instant::now();
304
305            // events
306            let keep_running = process_events(
307                &mut self.events_loop.as_mut().unwrap(),
308                self.gl_window.as_mut().unwrap(),
309                &mut self.orbit_controls,
310                &mut self.dpi_factor,
311                &mut self.size,
312            );
313            if !keep_running {
314                unsafe {
315                    gl_check_error!();
316                } // final error check so errors don't go unnoticed
317                break;
318            }
319
320            self.orbit_controls.frame_update(self.delta_time); // keyboard navigation
321
322            self.draw();
323
324            self.gl_window.as_ref().unwrap().swap_buffers().unwrap();
325        }
326    }
327
328    // Returns whether to keep running
329    pub fn draw(&mut self) {
330        // render
331        unsafe {
332            self.render_timer.start();
333
334            gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT);
335
336            let cam_params = self.orbit_controls.camera_params();
337            self.scene.draw(&mut self.root, &cam_params);
338
339            self.render_timer.end();
340        }
341    }
342
343    pub fn screenshot(&mut self, filename: &str) {
344        self.draw();
345
346        let mut img = DynamicImage::new_rgba8(self.size.width as u32, self.size.height as u32);
347        unsafe {
348            let pixels = img.as_mut_rgba8().unwrap();
349            gl::PixelStorei(gl::PACK_ALIGNMENT, 1);
350            gl::ReadPixels(
351                0,
352                0,
353                self.size.width as i32,
354                self.size.height as i32,
355                gl::RGBA,
356                gl::UNSIGNED_BYTE,
357                pixels.as_mut_ptr() as *mut c_void,
358            );
359            gl_check_error!();
360        }
361
362        let img = img.flipv();
363        if let Err(err) = img.save(filename) {
364            error!("{}", err);
365        } else {
366            println!(
367                "Saved {}x{} screenshot to {}",
368                self.size.width, self.size.height, filename
369            );
370        }
371    }
372    pub fn multiscreenshot(&mut self, filename: &str, count: u32) {
373        let min_angle: f32 = 0.0;
374        let max_angle: f32 = 2.0 * PI;
375        let increment_angle: f32 = ((max_angle - min_angle) / (count as f32)) as f32;
376        let suffix_length = count.to_string().len();
377        for i in 1..=count {
378            self.orbit_controls.rotate_object(increment_angle);
379            let dot = filename.rfind('.').unwrap_or_else(|| filename.len());
380            let mut actual_name = filename.to_string();
381            actual_name.insert_str(
382                dot,
383                &format!("_{:0suffix_length$}", i, suffix_length = suffix_length),
384            );
385            self.screenshot(&actual_name[..]);
386        }
387    }
388
389    pub fn screenshot_save(&mut self) -> DynamicImage {
390        self.draw();
391
392        let mut img = DynamicImage::new_rgba8(self.size.width as u32, self.size.height as u32);
393        unsafe {
394            let pixels = img.as_mut_rgba8().unwrap();
395            gl::PixelStorei(gl::PACK_ALIGNMENT, 1);
396            gl::ReadPixels(
397                0,
398                0,
399                self.size.width as i32,
400                self.size.height as i32,
401                gl::RGBA,
402                gl::UNSIGNED_BYTE,
403                pixels.as_mut_ptr() as *mut c_void,
404            );
405            gl_check_error!();
406        }
407
408        let img = img.flipv();
409        img
410    }
411
412    pub fn multiscreenshot_save(&mut self, count: u32) -> Vec<DynamicImage> {
413        let min_angle: f32 = 0.0;
414        let max_angle: f32 = 2.0 * PI;
415        let increment_angle: f32 = ((max_angle - min_angle) / (count as f32)) as f32;
416        let mut images: Vec<DynamicImage> = vec![];
417        for _ in 1..=count {
418            self.orbit_controls.rotate_object(increment_angle);
419            let image = self.screenshot_save();
420            images.push(image);
421        }
422        images
423    }
424}
425
426#[allow(clippy::too_many_arguments)]
427fn process_events(
428    events_loop: &mut glutin::EventsLoop,
429    gl_window: &glutin::GlWindow,
430    mut orbit_controls: &mut OrbitControls,
431    dpi_factor: &mut f64,
432    size: &mut PhysicalSize,
433) -> bool {
434    let mut keep_running = true;
435    #[allow(clippy::single_match)]
436    events_loop.poll_events(|event| {
437        match event {
438            glutin::Event::WindowEvent { event, .. } => match event {
439                WindowEvent::CloseRequested => {
440                    keep_running = false;
441                }
442                WindowEvent::Destroyed => {
443                    // Log and exit?
444                    panic!("WindowEvent::Destroyed, unimplemented.");
445                }
446                WindowEvent::Resized(logical) => {
447                    let ph = logical.to_physical(*dpi_factor);
448                    gl_window.resize(ph);
449
450                    // This doesn't seem to be needed on macOS but linux X11, Wayland and Windows
451                    // do need it.
452                    unsafe {
453                        gl::Viewport(0, 0, ph.width as i32, ph.height as i32);
454                    }
455
456                    *size = ph;
457                    orbit_controls
458                        .camera
459                        .update_aspect_ratio((ph.width / ph.height) as f32);
460                    orbit_controls.screen_size = ph;
461                }
462                WindowEvent::HiDpiFactorChanged(f) => {
463                    *dpi_factor = f;
464                }
465                WindowEvent::DroppedFile(_path_buf) => {
466                    // TODO: drag file in
467                }
468                WindowEvent::MouseInput {
469                    button,
470                    state: Pressed,
471                    ..
472                } => match button {
473                    MouseButton::Left => {
474                        orbit_controls.state = NavState::Rotating;
475                    }
476                    MouseButton::Right => {
477                        orbit_controls.state = NavState::Panning;
478                    }
479                    _ => (),
480                },
481                WindowEvent::MouseInput {
482                    button,
483                    state: Released,
484                    ..
485                } => match (button, orbit_controls.state.clone()) {
486                    (MouseButton::Left, NavState::Rotating)
487                    | (MouseButton::Right, NavState::Panning) => {
488                        orbit_controls.state = NavState::None;
489                        orbit_controls.handle_mouse_up();
490                    }
491                    _ => (),
492                },
493                WindowEvent::CursorMoved { position, .. } => {
494                    let ph = position.to_physical(*dpi_factor);
495                    orbit_controls.handle_mouse_move(ph)
496                }
497                WindowEvent::MouseWheel {
498                    delta: MouseScrollDelta::PixelDelta(logical),
499                    ..
500                } => {
501                    let ph = logical.to_physical(*dpi_factor);
502                    orbit_controls.process_mouse_scroll(ph.y as f32);
503                }
504                WindowEvent::MouseWheel {
505                    delta: MouseScrollDelta::LineDelta(_rows, lines),
506                    ..
507                } => {
508                    orbit_controls.process_mouse_scroll(lines * 3.0);
509                }
510                WindowEvent::KeyboardInput { input, .. } => {
511                    keep_running = process_input(input, &mut orbit_controls);
512                }
513                _ => (),
514            },
515            _ => (),
516        }
517    });
518
519    keep_running
520}
521
522fn process_input(input: glutin::KeyboardInput, controls: &mut OrbitControls) -> bool {
523    let pressed = match input.state {
524        Pressed => true,
525        Released => false,
526    };
527    if let Some(code) = input.virtual_keycode {
528        match code {
529            VirtualKeyCode::Escape if pressed => return false,
530            VirtualKeyCode::W | VirtualKeyCode::Up => controls.process_keyboard(FORWARD, pressed),
531            VirtualKeyCode::S | VirtualKeyCode::Down => {
532                controls.process_keyboard(BACKWARD, pressed)
533            }
534            VirtualKeyCode::A | VirtualKeyCode::Left => controls.process_keyboard(LEFT, pressed),
535            VirtualKeyCode::D | VirtualKeyCode::Right => controls.process_keyboard(RIGHT, pressed),
536            _ => (),
537        }
538    }
539    true
540}