rend3_scene_viewer_example/
lib.rs

1use glam::{DVec2, Mat3A, Mat4, UVec2, Vec3, Vec3A};
2use instant::Instant;
3use pico_args::Arguments;
4use rend3::{
5    types::{
6        Backend, Camera, CameraProjection, DirectionalLight, DirectionalLightHandle, SampleCount, Texture,
7        TextureFormat,
8    },
9    util::typedefs::FastHashMap,
10    Renderer, RendererProfile,
11};
12use rend3_framework::{lock, AssetPath, Mutex};
13use rend3_gltf::GltfSceneInstance;
14use rend3_routine::{base::BaseRenderGraph, pbr::NormalTextureYDirection, skybox::SkyboxRoutine};
15use std::{collections::HashMap, future::Future, hash::BuildHasher, path::Path, sync::Arc, time::Duration};
16use wgpu_profiler::GpuTimerScopeResult;
17use winit::{
18    event::{DeviceEvent, ElementState, Event, KeyboardInput, MouseButton, WindowEvent},
19    window::{Fullscreen, WindowBuilder},
20};
21
22mod platform;
23
24async fn load_skybox_image(loader: &rend3_framework::AssetLoader, data: &mut Vec<u8>, path: &str) {
25    let decoded = image::load_from_memory(
26        &loader
27            .get_asset(AssetPath::Internal(path))
28            .await
29            .unwrap_or_else(|e| panic!("Error {}: {}", path, e)),
30    )
31    .unwrap()
32    .into_rgba8();
33
34    data.extend_from_slice(decoded.as_raw());
35}
36
37async fn load_skybox(
38    renderer: &Renderer,
39    loader: &rend3_framework::AssetLoader,
40    skybox_routine: &Mutex<SkyboxRoutine>,
41) -> Result<(), Box<dyn std::error::Error>> {
42    let mut data = Vec::new();
43    load_skybox_image(loader, &mut data, "skybox/right.jpg").await;
44    load_skybox_image(loader, &mut data, "skybox/left.jpg").await;
45    load_skybox_image(loader, &mut data, "skybox/top.jpg").await;
46    load_skybox_image(loader, &mut data, "skybox/bottom.jpg").await;
47    load_skybox_image(loader, &mut data, "skybox/front.jpg").await;
48    load_skybox_image(loader, &mut data, "skybox/back.jpg").await;
49
50    let handle = renderer.add_texture_cube(Texture {
51        format: TextureFormat::Rgba8UnormSrgb,
52        size: UVec2::new(2048, 2048),
53        data,
54        label: Some("background".into()),
55        mip_count: rend3::types::MipmapCount::ONE,
56        mip_source: rend3::types::MipmapSource::Uploaded,
57    });
58    lock(skybox_routine).set_background_texture(Some(handle));
59    Ok(())
60}
61
62async fn load_gltf(
63    renderer: &Renderer,
64    loader: &rend3_framework::AssetLoader,
65    settings: &rend3_gltf::GltfLoadSettings,
66    location: AssetPath<'_>,
67) -> Option<(rend3_gltf::LoadedGltfScene, GltfSceneInstance)> {
68    // profiling::scope!("loading gltf");
69    let gltf_start = Instant::now();
70    let is_default_scene = matches!(location, AssetPath::Internal(_));
71    let path = loader.get_asset_path(location);
72    let path = Path::new(&*path);
73    let parent = path.parent().unwrap();
74
75    let parent_str = parent.to_string_lossy();
76    let path_str = path.as_os_str().to_string_lossy();
77    log::info!("Reading gltf file: {}", path_str);
78    let gltf_data_result = loader.get_asset(AssetPath::External(&path_str)).await;
79
80    let gltf_data = match gltf_data_result {
81        Ok(d) => d,
82        Err(_) if is_default_scene => {
83            let suffix = if cfg!(target_os = "windows") { ".exe" } else { "" };
84
85            indoc::eprintdoc!("
86                *** WARNING ***
87
88                It appears you are running scene-viewer with no file to display.
89                
90                The default scene is no longer bundled into the repository. If you are running on git, use the following commands
91                to download and unzip it into the right place. If you're running it through not-git, pass a custom folder to the -C argument
92                to tar, then run scene-viewer path/to/scene.gltf.
93                
94                curl{0} https://cdn.cwfitz.com/scenes/rend3-default-scene.tar -o ./examples/scene-viewer/resources/rend3-default-scene.tar
95                tar{0} xf ./examples/scene-viewer/resources/rend3-default-scene.tar -C ./examples/scene-viewer/resources
96
97                ***************
98            ", suffix);
99
100            return None;
101        }
102        e => e.unwrap(),
103    };
104
105    let gltf_elapsed = gltf_start.elapsed();
106    let resources_start = Instant::now();
107    let (scene, instance) = rend3_gltf::load_gltf(renderer, &gltf_data, settings, |uri| async {
108        log::info!("Loading resource {}", uri);
109        let uri = uri;
110        let full_uri = parent_str.clone() + "/" + uri.as_str();
111        loader.get_asset(AssetPath::External(&full_uri)).await
112    })
113    .await
114    .unwrap();
115
116    log::info!(
117        "Loaded gltf in {:.3?}, resources loaded in {:.3?}",
118        gltf_elapsed,
119        resources_start.elapsed()
120    );
121    Some((scene, instance))
122}
123
124fn button_pressed<Hash: BuildHasher>(map: &HashMap<u32, bool, Hash>, key: u32) -> bool {
125    map.get(&key).map_or(false, |b| *b)
126}
127
128fn extract_backend(value: &str) -> Result<Backend, &'static str> {
129    Ok(match value.to_lowercase().as_str() {
130        "vulkan" | "vk" => Backend::Vulkan,
131        "dx12" | "12" => Backend::Dx12,
132        "dx11" | "11" => Backend::Dx11,
133        "metal" | "mtl" => Backend::Metal,
134        "opengl" | "gl" => Backend::Gl,
135        _ => return Err("unknown backend"),
136    })
137}
138
139fn extract_mode(value: &str) -> Result<rend3::RendererProfile, &'static str> {
140    Ok(match value.to_lowercase().as_str() {
141        "legacy" | "c" | "cpu" => rend3::RendererProfile::CpuDriven,
142        "modern" | "g" | "gpu" => rend3::RendererProfile::GpuDriven,
143        _ => return Err("unknown rendermode"),
144    })
145}
146
147fn extract_msaa(value: &str) -> Result<SampleCount, &'static str> {
148    Ok(match value {
149        "1" => SampleCount::One,
150        "4" => SampleCount::Four,
151        _ => return Err("invalid msaa count"),
152    })
153}
154
155fn extract_vec3(value: &str) -> Result<Vec3, &'static str> {
156    let mut res = [0.0_f32, 0.0, 0.0];
157    let split: Vec<_> = value.split(',').enumerate().collect();
158
159    if split.len() != 3 {
160        return Err("Directional lights are defined with 3 values");
161    }
162
163    for (idx, inner) in split {
164        let inner = inner.trim();
165
166        res[idx] = inner.parse().map_err(|_| "Cannot parse direction number")?;
167    }
168    Ok(Vec3::from(res))
169}
170
171fn option_arg<T>(result: Result<Option<T>, pico_args::Error>) -> Option<T> {
172    match result {
173        Ok(o) => o,
174        Err(pico_args::Error::Utf8ArgumentParsingFailed { value, cause }) => {
175            eprintln!("{}: '{}'\n\n{}", cause, value, HELP);
176            std::process::exit(1);
177        }
178        Err(pico_args::Error::OptionWithoutAValue(value)) => {
179            eprintln!("{} flag needs an argument", value);
180            std::process::exit(1);
181        }
182        Err(e) => {
183            eprintln!("{:?}", e);
184            std::process::exit(1);
185        }
186    }
187}
188
189#[cfg(not(target_arch = "wasm32"))]
190pub fn spawn<Fut>(fut: Fut)
191where
192    Fut: Future + Send + 'static,
193    Fut::Output: Send + 'static,
194{
195    std::thread::spawn(|| pollster::block_on(fut));
196}
197
198#[cfg(target_arch = "wasm32")]
199pub fn spawn<Fut>(fut: Fut)
200where
201    Fut: Future + 'static,
202    Fut::Output: 'static,
203{
204    wasm_bindgen_futures::spawn_local(async move {
205        fut.await;
206    });
207}
208
209const HELP: &str = "\
210scene-viewer
211
212gltf and glb scene viewer powered by the rend3 rendering library.
213
214usage: scene-viewer --options ./path/to/gltf/file.gltf
215
216Meta:
217  --help            This menu.
218
219Rendering:
220  -b --backend                 Choose backend to run on ('vk', 'dx12', 'dx11', 'metal', 'gl').
221  -d --device                  Choose device to run on (case insensitive device substring).
222  -p --profile                 Choose rendering profile to use ('cpu', 'gpu').
223  --msaa <level>               Level of antialiasing (either 1 or 4). Default 1.
224
225Windowing:
226  --absolute-mouse             Interpret the relative mouse coordinates as absolute. Useful when using things like VNC.
227  --fullscreen                 Open the window in borderless fullscreen.
228
229Assets:
230  --normal-y-down                        Interpret all normals as having the DirectX convention of Y down. Defaults to Y up.
231  --directional-light <x,y,z>            Create a directional light pointing towards the given coordinates.
232  --directional-light-intensity <value>  All lights created by the above flag have this intensity. Defaults to 4.
233  --gltf-disable-directional-lights      Disable all directional lights in the gltf
234  --ambient <value>                      Set the value of the minimum ambient light. This will be treated as white light of this intensity. Defaults to 0.1.
235  --scale <scale>                        Scale all objects loaded by this factor. Defaults to 1.0.
236  --shadow-distance <value>              Distance from the camera there will be directional shadows. Lower values means higher quality shadows. Defaults to 100.
237
238Controls:
239  --walk <speed>               Walk speed (speed without holding shift) in units/second (typically meters). Default 10.
240  --run  <speed>               Run speed (speed while holding shift) in units/second (typically meters). Default 50.
241";
242
243struct SceneViewer {
244    absolute_mouse: bool,
245    desired_backend: Option<Backend>,
246    desired_device_name: Option<String>,
247    desired_profile: Option<RendererProfile>,
248    file_to_load: Option<String>,
249    walk_speed: f32,
250    run_speed: f32,
251    gltf_settings: rend3_gltf::GltfLoadSettings,
252    directional_light_direction: Option<Vec3>,
253    directional_light_intensity: f32,
254    directional_light: Option<DirectionalLightHandle>,
255    ambient_light_level: f32,
256    samples: SampleCount,
257
258    fullscreen: bool,
259
260    scancode_status: FastHashMap<u32, bool>,
261    camera_pitch: f32,
262    camera_yaw: f32,
263    camera_location: Vec3A,
264    previous_profiling_stats: Option<Vec<GpuTimerScopeResult>>,
265    timestamp_last_second: Instant,
266    timestamp_last_frame: Instant,
267    frame_times: histogram::Histogram,
268    last_mouse_delta: Option<DVec2>,
269
270    grabber: Option<rend3_framework::Grabber>,
271}
272impl SceneViewer {
273    pub fn new() -> Self {
274        let mut args = Arguments::from_vec(std::env::args_os().skip(1).collect());
275
276        // Meta
277        let help = args.contains(["-h", "--help"]);
278
279        // Rendering
280        let desired_backend = option_arg(args.opt_value_from_fn(["-b", "--backend"], extract_backend));
281        let desired_device_name: Option<String> =
282            option_arg(args.opt_value_from_str(["-d", "--device"])).map(|s: String| s.to_lowercase());
283        let desired_mode = option_arg(args.opt_value_from_fn(["-p", "--profile"], extract_mode));
284        let samples = option_arg(args.opt_value_from_fn("--msaa", extract_msaa)).unwrap_or(SampleCount::One);
285
286        // Windowing
287        let absolute_mouse: bool = args.contains("--absolute-mouse");
288        let fullscreen = args.contains("--fullscreen");
289
290        // Assets
291        let normal_direction = match args.contains("--normal-y-down") {
292            true => NormalTextureYDirection::Down,
293            false => NormalTextureYDirection::Up,
294        };
295        let directional_light_direction = option_arg(args.opt_value_from_fn("--directional-light", extract_vec3));
296        let directional_light_intensity: f32 =
297            option_arg(args.opt_value_from_str("--directional-light-intensity")).unwrap_or(4.0);
298        let ambient_light_level: f32 = option_arg(args.opt_value_from_str("--ambient")).unwrap_or(0.10);
299        let scale: Option<f32> = option_arg(args.opt_value_from_str("--scale"));
300        let shadow_distance: Option<f32> = option_arg(args.opt_value_from_str("--shadow-distance"));
301        let gltf_disable_directional_light: bool = args.contains("--gltf-disable-directional-lights");
302
303        // Controls
304        let walk_speed = args.value_from_str("--walk").unwrap_or(10.0_f32);
305        let run_speed = args.value_from_str("--run").unwrap_or(50.0_f32);
306
307        // Free args
308        let file_to_load: Option<String> = args.free_from_str().ok();
309
310        let remaining = args.finish();
311
312        if !remaining.is_empty() {
313            eprint!("Unknown arguments:");
314            for flag in remaining {
315                eprint!(" '{}'", flag.to_string_lossy());
316            }
317            eprintln!("\n");
318
319            eprintln!("{}", HELP);
320            std::process::exit(1);
321        }
322
323        if help {
324            eprintln!("{}", HELP);
325            std::process::exit(1);
326        }
327
328        let mut gltf_settings = rend3_gltf::GltfLoadSettings {
329            normal_direction,
330            enable_directional: !gltf_disable_directional_light,
331            ..Default::default()
332        };
333        if let Some(scale) = scale {
334            gltf_settings.scale = scale
335        }
336        if let Some(shadow_distance) = shadow_distance {
337            gltf_settings.directional_light_shadow_distance = shadow_distance;
338        }
339
340        Self {
341            absolute_mouse,
342            desired_backend,
343            desired_device_name,
344            desired_profile: desired_mode,
345            file_to_load,
346            walk_speed,
347            run_speed,
348            gltf_settings,
349            directional_light_direction,
350            directional_light_intensity,
351            directional_light: None,
352            ambient_light_level,
353            samples,
354
355            fullscreen,
356
357            scancode_status: FastHashMap::default(),
358            camera_pitch: -std::f32::consts::FRAC_PI_8,
359            camera_yaw: std::f32::consts::FRAC_PI_4,
360            camera_location: Vec3A::new(3.0, 3.0, 3.0),
361            previous_profiling_stats: None,
362            timestamp_last_second: Instant::now(),
363            timestamp_last_frame: Instant::now(),
364            frame_times: histogram::Histogram::new(),
365            last_mouse_delta: None,
366
367            grabber: None,
368        }
369    }
370}
371impl rend3_framework::App for SceneViewer {
372    const HANDEDNESS: rend3::types::Handedness = rend3::types::Handedness::Right;
373
374    fn create_iad<'a>(
375        &'a mut self,
376    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<rend3::InstanceAdapterDevice>> + 'a>> {
377        Box::pin(async move {
378            Ok(rend3::create_iad(
379                self.desired_backend,
380                self.desired_device_name.clone(),
381                self.desired_profile,
382                None,
383            )
384            .await?)
385        })
386    }
387
388    fn sample_count(&self) -> SampleCount {
389        self.samples
390    }
391
392    fn scale_factor(&self) -> f32 {
393        // Android has very low memory bandwidth, so lets run internal buffers at half
394        // res by default
395        cfg_if::cfg_if! {
396            if #[cfg(target_os = "android")] {
397                0.5
398            } else {
399                1.0
400            }
401        }
402    }
403
404    fn setup<'a>(
405        &'a mut self,
406        window: &'a winit::window::Window,
407        renderer: &'a Arc<Renderer>,
408        routines: &'a Arc<rend3_framework::DefaultRoutines>,
409        _surface_format: rend3::types::TextureFormat,
410    ) {
411        self.grabber = Some(rend3_framework::Grabber::new(window));
412
413        if let Some(direction) = self.directional_light_direction {
414            self.directional_light = Some(renderer.add_directional_light(DirectionalLight {
415                color: Vec3::splat(1.0),
416                intensity: self.directional_light_intensity,
417                direction,
418                distance: self.gltf_settings.directional_light_shadow_distance,
419            }));
420        }
421
422        let gltf_settings = self.gltf_settings;
423        let file_to_load = self.file_to_load.take();
424        let renderer = Arc::clone(renderer);
425        let routines = Arc::clone(routines);
426        spawn(async move {
427            let loader = rend3_framework::AssetLoader::new_local(
428                concat!(env!("CARGO_MANIFEST_DIR"), "/resources/"),
429                "",
430                "http://localhost:8000/resources/",
431            );
432            if let Err(e) = load_skybox(&renderer, &loader, &routines.skybox).await {
433                println!("Failed to load skybox {}", e)
434            };
435            Box::leak(Box::new(
436                load_gltf(
437                    &renderer,
438                    &loader,
439                    &gltf_settings,
440                    file_to_load
441                        .as_deref()
442                        .map_or_else(|| AssetPath::Internal("default-scene/scene.gltf"), AssetPath::External),
443                )
444                .await,
445            ));
446        });
447    }
448
449    fn handle_event(
450        &mut self,
451        window: &winit::window::Window,
452        renderer: &Arc<rend3::Renderer>,
453        routines: &Arc<rend3_framework::DefaultRoutines>,
454        base_rendergraph: &BaseRenderGraph,
455        surface: Option<&Arc<rend3::types::Surface>>,
456        resolution: UVec2,
457        event: rend3_framework::Event<'_, ()>,
458        control_flow: impl FnOnce(winit::event_loop::ControlFlow),
459    ) {
460        match event {
461            Event::MainEventsCleared => {
462                profiling::scope!("MainEventsCleared");
463                let now = Instant::now();
464
465                let delta_time = now - self.timestamp_last_frame;
466                self.frame_times.increment(delta_time.as_micros() as u64).unwrap();
467
468                let elapsed_since_second = now - self.timestamp_last_second;
469                if elapsed_since_second > Duration::from_secs(1) {
470                    let count = self.frame_times.entries();
471                    println!(
472                        "{:0>5} frames over {:0>5.2}s. \
473                        Min: {:0>5.2}ms; \
474                        Average: {:0>5.2}ms; \
475                        95%: {:0>5.2}ms; \
476                        99%: {:0>5.2}ms; \
477                        Max: {:0>5.2}ms; \
478                        StdDev: {:0>5.2}ms",
479                        count,
480                        elapsed_since_second.as_secs_f32(),
481                        self.frame_times.minimum().unwrap() as f32 / 1_000.0,
482                        self.frame_times.mean().unwrap() as f32 / 1_000.0,
483                        self.frame_times.percentile(95.0).unwrap() as f32 / 1_000.0,
484                        self.frame_times.percentile(99.0).unwrap() as f32 / 1_000.0,
485                        self.frame_times.maximum().unwrap() as f32 / 1_000.0,
486                        self.frame_times.stddev().unwrap() as f32 / 1_000.0,
487                    );
488                    self.timestamp_last_second = now;
489                    self.frame_times.clear();
490                }
491
492                self.timestamp_last_frame = now;
493
494                let rotation =
495                    Mat3A::from_euler(glam::EulerRot::XYZ, -self.camera_pitch, -self.camera_yaw, 0.0).transpose();
496                let forward = -rotation.z_axis;
497                let up = rotation.y_axis;
498                let side = -rotation.x_axis;
499                let velocity = if button_pressed(&self.scancode_status, platform::Scancodes::SHIFT) {
500                    self.run_speed
501                } else {
502                    self.walk_speed
503                };
504                if button_pressed(&self.scancode_status, platform::Scancodes::W) {
505                    self.camera_location += forward * velocity * delta_time.as_secs_f32();
506                }
507                if button_pressed(&self.scancode_status, platform::Scancodes::S) {
508                    self.camera_location -= forward * velocity * delta_time.as_secs_f32();
509                }
510                if button_pressed(&self.scancode_status, platform::Scancodes::A) {
511                    self.camera_location += side * velocity * delta_time.as_secs_f32();
512                }
513                if button_pressed(&self.scancode_status, platform::Scancodes::D) {
514                    self.camera_location -= side * velocity * delta_time.as_secs_f32();
515                }
516                if button_pressed(&self.scancode_status, platform::Scancodes::Q) {
517                    self.camera_location += up * velocity * delta_time.as_secs_f32();
518                }
519                if button_pressed(&self.scancode_status, platform::Scancodes::Z) {
520                    self.camera_location -= up * velocity * delta_time.as_secs_f32();
521                }
522
523                if button_pressed(&self.scancode_status, platform::Scancodes::ESCAPE) {
524                    self.grabber.as_mut().unwrap().request_ungrab(window);
525                }
526
527                if button_pressed(&self.scancode_status, platform::Scancodes::P) {
528                    // write out gpu side performance info into a trace readable by chrome://tracing
529                    if let Some(ref stats) = self.previous_profiling_stats {
530                        println!("Outputing gpu timing chrome trace to profile.json");
531                        wgpu_profiler::chrometrace::write_chrometrace(Path::new("profile.json"), stats).unwrap();
532                    } else {
533                        println!("No gpu timing trace available, either timestamp queries are unsupported or not enough frames have elapsed yet!");
534                    }
535                }
536
537                window.request_redraw()
538            }
539            Event::RedrawRequested(_) => {
540                let view = Mat4::from_euler(glam::EulerRot::XYZ, -self.camera_pitch, -self.camera_yaw, 0.0);
541                let view = view * Mat4::from_translation((-self.camera_location).into());
542
543                renderer.set_camera_data(Camera {
544                    projection: CameraProjection::Perspective { vfov: 60.0, near: 0.1 },
545                    view,
546                });
547
548                // Get a frame
549                let frame = rend3::util::output::OutputFrame::Surface {
550                    surface: Arc::clone(surface.unwrap()),
551                };
552                // Lock all the routines
553                let pbr_routine = lock(&routines.pbr);
554                let mut skybox_routine = lock(&routines.skybox);
555                let tonemapping_routine = lock(&routines.tonemapping);
556
557                // Ready up the renderer
558                let (cmd_bufs, ready) = renderer.ready();
559                // Ready up the routines
560                skybox_routine.ready(renderer);
561
562                // Build a rendergraph
563                let mut graph = rend3::graph::RenderGraph::new();
564
565                // Add the default rendergraph
566                base_rendergraph.add_to_graph(
567                    &mut graph,
568                    &ready,
569                    &pbr_routine,
570                    Some(&skybox_routine),
571                    &tonemapping_routine,
572                    resolution,
573                    self.samples,
574                    Vec3::splat(self.ambient_light_level).extend(1.0),
575                );
576
577                // Dispatch a render using the built up rendergraph!
578                self.previous_profiling_stats = graph.execute(renderer, frame, cmd_bufs, &ready);
579                // mark the end of the frame for tracy/other profilers
580                profiling::finish_frame!();
581            }
582            Event::WindowEvent {
583                event: WindowEvent::Focused(focus),
584                ..
585            } => {
586                if !focus {
587                    self.grabber.as_mut().unwrap().request_ungrab(window);
588                }
589            }
590            Event::WindowEvent {
591                event:
592                    WindowEvent::KeyboardInput {
593                        input: KeyboardInput { scancode, state, .. },
594                        ..
595                    },
596                ..
597            } => {
598                log::info!("WE scancode {:x}", scancode);
599                self.scancode_status.insert(
600                    scancode,
601                    match state {
602                        ElementState::Pressed => true,
603                        ElementState::Released => false,
604                    },
605                );
606            }
607            Event::WindowEvent {
608                event:
609                    WindowEvent::MouseInput {
610                        button: MouseButton::Left,
611                        state: ElementState::Pressed,
612                        ..
613                    },
614                ..
615            } => {
616                let grabber = self.grabber.as_mut().unwrap();
617
618                if !grabber.grabbed() {
619                    grabber.request_grab(window);
620                }
621            }
622            Event::DeviceEvent {
623                event:
624                    DeviceEvent::MouseMotion {
625                        delta: (delta_x, delta_y),
626                        ..
627                    },
628                ..
629            } => {
630                if !self.grabber.as_ref().unwrap().grabbed() {
631                    return;
632                }
633
634                const TAU: f32 = std::f32::consts::PI * 2.0;
635
636                let mouse_delta = if self.absolute_mouse {
637                    let prev = self.last_mouse_delta.replace(DVec2::new(delta_x, delta_y));
638                    if let Some(prev) = prev {
639                        (DVec2::new(delta_x, delta_y) - prev) / 4.0
640                    } else {
641                        return;
642                    }
643                } else {
644                    DVec2::new(delta_x, delta_y)
645                };
646
647                self.camera_yaw -= (mouse_delta.x / 1000.0) as f32;
648                self.camera_pitch -= (mouse_delta.y / 1000.0) as f32;
649                if self.camera_yaw < 0.0 {
650                    self.camera_yaw += TAU;
651                } else if self.camera_yaw >= TAU {
652                    self.camera_yaw -= TAU;
653                }
654                self.camera_pitch = self
655                    .camera_pitch
656                    .max(-std::f32::consts::FRAC_PI_2 + 0.0001)
657                    .min(std::f32::consts::FRAC_PI_2 - 0.0001);
658            }
659            Event::WindowEvent {
660                event: WindowEvent::CloseRequested,
661                ..
662            } => {
663                control_flow(winit::event_loop::ControlFlow::Exit);
664            }
665            _ => {}
666        }
667    }
668}
669
670#[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on", logger(level = "debug")))]
671pub fn main() {
672    let app = SceneViewer::new();
673
674    let mut builder = WindowBuilder::new().with_title("scene-viewer").with_maximized(true);
675    if app.fullscreen {
676        builder = builder.with_fullscreen(Some(Fullscreen::Borderless(None)));
677    }
678
679    rend3_framework::start(app, builder);
680}