├── .github └── workflows │ └── build.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── TODO.md ├── assets ├── env │ └── equi.hdr ├── fonts │ └── mplus-1p-regular.ttf └── models │ └── cesium_man_with_light.glb ├── config.yml ├── crates ├── libs │ ├── environment │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── brdf.rs │ │ │ ├── cubemap.rs │ │ │ ├── irradiance.rs │ │ │ ├── lib.rs │ │ │ └── pre_filtered.rs │ ├── math │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── aabb.rs │ │ │ └── lib.rs │ ├── model │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── animation.rs │ │ │ ├── error.rs │ │ │ ├── lib.rs │ │ │ ├── light.rs │ │ │ ├── material.rs │ │ │ ├── mesh.rs │ │ │ ├── metadata.rs │ │ │ ├── mikktspace.rs │ │ │ ├── node.rs │ │ │ ├── skin.rs │ │ │ ├── texture.rs │ │ │ └── vertex.rs │ ├── util │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ └── vulkan │ │ ├── Cargo.toml │ │ └── src │ │ ├── buffer.rs │ │ ├── context │ │ ├── mod.rs │ │ └── shared.rs │ │ ├── debug.rs │ │ ├── descriptor.rs │ │ ├── image.rs │ │ ├── lib.rs │ │ ├── msaa.rs │ │ ├── pipeline.rs │ │ ├── shader.rs │ │ ├── swapchain.rs │ │ ├── texture.rs │ │ ├── util.rs │ │ └── vertex.rs └── viewer │ ├── Cargo.toml │ ├── build.rs │ ├── shaders │ ├── blur.frag │ ├── blur.frag.spv │ ├── brdf_lookup.frag │ ├── brdf_lookup.frag.spv │ ├── cubemap.vert │ ├── cubemap.vert.spv │ ├── downsample.frag │ ├── downsample.frag.spv │ ├── final.frag │ ├── final.frag.spv │ ├── fullscreen.vert │ ├── fullscreen.vert.spv │ ├── gbuffer.frag │ ├── gbuffer.frag.spv │ ├── gbuffer.vert │ ├── gbuffer.vert.spv │ ├── irradiance.frag │ ├── irradiance.frag.spv │ ├── libs │ │ ├── camera.glsl │ │ └── material.glsl │ ├── model.frag │ ├── model.frag.spv │ ├── model.vert │ ├── model.vert.spv │ ├── pre_filtered.frag │ ├── pre_filtered.frag.spv │ ├── skybox.frag │ ├── skybox.frag.spv │ ├── skybox.vert │ ├── skybox.vert.spv │ ├── spherical.frag │ ├── spherical.frag.spv │ ├── ssao.frag │ ├── ssao.frag.spv │ ├── ssao.vert │ ├── ssao.vert.spv │ ├── upsample.frag │ └── upsample.frag.spv │ └── src │ ├── camera.rs │ ├── config.rs │ ├── controls.rs │ ├── error.rs │ ├── gui.rs │ ├── loader.rs │ ├── main.rs │ ├── renderer │ ├── attachments.rs │ ├── fullscreen.rs │ ├── mod.rs │ ├── model │ │ ├── gbufferpass.rs │ │ ├── lightpass.rs │ │ ├── mod.rs │ │ └── uniform.rs │ ├── postprocess │ │ ├── bloom.rs │ │ ├── blurpass.rs │ │ ├── finalpass.rs │ │ └── mod.rs │ ├── skybox.rs │ └── ssao.rs │ └── viewer.rs ├── images ├── cesium.gif ├── clearcoat_wicker.png ├── corset.png ├── env.png ├── flight_helmet.png ├── helmet_indoor.png ├── helmet_night.png ├── helmet_sand.png ├── helmet_woods.png ├── junkrat.png └── mg08.png └── scripts ├── debug.ps1 ├── debug.sh ├── run.ps1 └── run.sh /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Cross-platform build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | # Build the project on linux, windows and macos 7 | build: 8 | name: Build on ${{ matrix.os }} 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: ["ubuntu-latest", "windows-latest", "macos-latest"] 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Install toolchain 16 | uses: actions-rs/toolchain@v1 17 | with: 18 | toolchain: stable 19 | override: true 20 | - name: Build 21 | uses: actions-rs/cargo@v1 22 | env: 23 | SKIP_SHADER_COMPILATION: true 24 | with: 25 | command: build 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | /.vscode 4 | *.bat 5 | /.idea 6 | Cargo.lock 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/viewer", 4 | "crates/libs/*" 5 | ] 6 | resolver = "2" 7 | 8 | [workspace.package] 9 | version = "0.1.0" 10 | edition = "2021" 11 | authors = ["Adrien Bennadji "] 12 | 13 | [workspace.dependencies] 14 | vulkan = { path = "crates/libs/vulkan" } 15 | model = { path = "crates/libs/model" } 16 | math = { path = "crates/libs/math" } 17 | util = { path = "crates/libs/util" } 18 | environment = { path = "crates/libs/environment" } 19 | 20 | log = "0.4" 21 | env_logger = "0.11" 22 | serde = { version = "1.0", features = ["derive"] } 23 | serde_yaml = "0.9" 24 | clap = {version = "4.5", features = ["derive"] } 25 | cgmath = "0.18" 26 | rand = "0.9" 27 | lerp = "0.5" 28 | byteorder = "1.4" 29 | mikktspace = "0.3" 30 | image = "0.25" 31 | ash = "0.38" 32 | ash-window = "0.13" 33 | raw-window-handle = "0.6" 34 | winit = "0.30" 35 | gltf = "1.3" 36 | egui = "0.31" 37 | egui-winit = "0.31" 38 | egui-ash-renderer = { version = "0.8", features = ["dynamic-rendering"] } 39 | 40 | [patch.crates-io.gltf] 41 | git = "https://github.com/adrien-ben/gltf" 42 | branch = "missing_extensions" 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gltf-viewer-rs 2 | 3 | This project is a [glTF 2.0][0] viewer written in Rust. Rendering is done using the [Vulkan API][1] 4 | using [Ash][2]. It runs on Window, Linux and MacOS. 5 | 6 | ![Screenshot](images/helmet_woods.png "Screenshot") 7 | 8 | ## Features 9 | 10 | - [x] Mesh vertices, normals, colors and two uv channels 11 | - [x] Tangents generation 12 | - [x] Material 13 | - [x] Base color factor and texture 14 | - [x] Metallic/Roughness factors and textures 15 | - [x] Emissive factor and texture 16 | - [x] Ambient occlusion 17 | - [x] Normal maps 18 | - [x] Alpha 19 | - [x] Opaque 20 | - [x] Mask 21 | - [x] Blend 22 | - [x] Double sided surfaces 23 | - [x] IBL 24 | - [x] Animations 25 | - [x] Node animation 26 | - [x] Skinning 27 | - [x] Interpolation 28 | - [x] Step 29 | - [x] Linear 30 | - [x] Cubicspline 31 | - [ ] Extensions 32 | - [x] KHR_lights_punctual 33 | - [x] KHR_materials_unlit 34 | - [x] KHR_materials_pbrSpecularGlossiness 35 | - [x] KHR_materials_emissive_strength 36 | - [x] KHR_materials_clearcoat 37 | - [x] KHR_texture_transform 38 | - [x] KHR_materials_ior 39 | - [ ] KHR_draco_mesh_compression 40 | - [x] Camera controls 41 | - [x] Orbital 42 | - [x] First Person 43 | - [x] Drag and drop 44 | - [x] Background loading 45 | - [ ] Post processing 46 | - [x] Gamma correction 47 | - [x] Tone mapping 48 | - [x] Ambient occlusion 49 | - [x] Bloom 50 | - [ ] Depth of field 51 | - [x] egui integration 52 | - [x] Model description 53 | - [x] Animation controller 54 | - [x] Camera details 55 | - [x] Renderer settings 56 | - [x] HDR display 57 | - [x] Allow enabling HDR (if supported by the device) 58 | - [x] Add HDR specific tonemapping 59 | 60 | ## Requirements 61 | 62 | - Vulkan 1.0 63 | - VK_KHR_dynamic_rendering 64 | 65 | > You can check your Vulkan feature set using Sascha Willems' [Vulkan Caps Viewer][6] 66 | 67 | ## Controls 68 | 69 | - 'H' to toggle UI 70 | 71 | Orbital camera: 72 | - Left click and move to rotate camera around origin 73 | - Right click and move to move camera 74 | - Mouse wheel to un/zoom 75 | 76 | FPS camera: 77 | - WASD to move 78 | - Left Ctrl to go down 79 | - Space to go up 80 | - Left click to rotate 81 | 82 | ## Build it 83 | 84 | ```sh 85 | cargo build 86 | ``` 87 | 88 | By default, building the project will trigger shader compilation for all shaders in `./assets/shaders`. 89 | You can either skip this step altogether by setting the environnement variable `SKIP_SHADER_COMPILATION` 90 | to `true`, or you can change the default by setting `SHADERS_DIR`. Compiled shaders will be generated at 91 | the same location as the shader source, with the same name suffixed by `.spv`. 92 | 93 | > To compile the shaders you'll need to have `glslangValidator` on your PATH. 94 | 95 | ## Run it 96 | 97 | Just type the following command and drag and drop and gltf/glb file in the window. 98 | 99 | ```sh 100 | RUST_LOG=gltf_viewer_rs=warn cargo run 101 | ``` 102 | 103 | You can provide a yaml configuration file with `--config` (or `-c`). Check [this example file](./config.yml). And you can specify a file to load at startup 104 | with `--file` (or `-f`) 105 | 106 | ```sh 107 | RUST_LOG=gltf_viewer_rs=warn,vulkan=warn cargo run -- --config config.yml --file C:\\dev\\assets\\glTF-Sample-Models\\2.0\\Triangle\\glTF\\Triangle.gltf 108 | ``` 109 | 110 | ### Validation layers 111 | 112 | You can set up validation layers by running the following commands. 113 | 114 | ```sh 115 | export VK_LAYER_PATH=$VULKAN_SDK/Bin 116 | export VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation 117 | 118 | # Then running the app with the --debug (-d) flag. 119 | RUST_LOG=gltf_viewer_rs=warn cargo run -- --debug 120 | ``` 121 | 122 | > You can also use the run.sh and debug.sh scripts 123 | 124 | ## Known issues 125 | 126 | On linux, enabling v-sync causes the system to freeze with some hardware configuration (tested on a laptop running Pop_os 19.04, with a GTX 1050Ti). It seems to be an issue related with PRIME sync. 127 | 128 | ## Credits 129 | 130 | Most of the shader code for BRDF lookup and pre-filtered map generation come from the excellent [Sasha Willems' vulkan samples repository][3]. 131 | 132 | Cubemap faces have been generated using [matheowis' HDRi to cubemap tool][4]. 133 | 134 | HDRi textures have been downloaded from [hdriheaven][5]. 135 | 136 | SSAO tutorial from John Chapman's [blog][7]. 137 | 138 | Bloom tutorial on [Learn OpenGL][8] 139 | 140 | [0]: https://github.com/KhronosGroup/glTF 141 | [1]: https://www.khronos.org/vulkan/ 142 | [2]: https://github.com/MaikKlein/ash 143 | [3]: https://github.com/SaschaWillems/Vulkan 144 | [4]: https://github.com/matheowis/HDRI-to-CubeMap 145 | [5]: https://hdrihaven.com/ 146 | [6]: https://vulkan.gpuinfo.org/download.php 147 | [7]: http://john-chapman-graphics.blogspot.com/2013/01/ssao-tutorial.html 148 | [8]: https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom 149 | 150 | ## Screenshots 151 | 152 | ![Cesium](images/cesium.gif "Cesium") 153 | 154 | |![Woods](images/helmet_woods.png "Woods")|![Sand](images/helmet_sand.png "Sand")|![Night](images/helmet_night.png "Night")| 155 | |---|---|---| 156 | |![Indoor](images/helmet_indoor.png "Indoor")|![Env](images/env.png "Env")|![Corset](images/corset.png "Corset")| 157 | |![MG08](images/mg08.png "MG08")|![Flight](images/flight_helmet.png "Flight Helmet")|![Junkrat](images/junkrat.png "Junkrat")| 158 | |![Wicker](images/clearcoat_wicker.png "Wicker")||| 159 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - improvement: Rework pbr to include ior factor 2 | - improvement: Keep adding extension support 3 | - improvement: anisotropic filtering 4 | - improvement: shadows mapping 5 | -------------------------------------------------------------------------------- /assets/env/equi.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/assets/env/equi.hdr -------------------------------------------------------------------------------- /assets/fonts/mplus-1p-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/assets/fonts/mplus-1p-regular.ttf -------------------------------------------------------------------------------- /assets/models/cesium_man_with_light.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/assets/models/cesium_man_with_light.glb -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | resolution: 2 | width: 1024 3 | height: 768 4 | 5 | fullscreen: false 6 | 7 | # Enable vertical synchronization (if supported). Optional(default = false) 8 | vsync: true 9 | 10 | # 64, 32, 16, 8, 4, 2, 1. Optional(default = 1) 11 | # If the requested value is not supported it will drop until one is found. 12 | # MSAA is VERY memory intensive ! 13 | msaa: 1 14 | 15 | # Environment configuration. Optional(default = "assets/env/equi.hdr"@1024) 16 | env: 17 | # Path to an HDRi file to use as environment map. 18 | path: assets/env/equi.hdr 19 | 20 | # Resolution of the skybox. Optional(default = 1024) 21 | resolution: 1024 22 | -------------------------------------------------------------------------------- /crates/libs/environment/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "environment" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | 7 | [dependencies] 8 | vulkan.workspace = true 9 | util.workspace = true 10 | math.workspace = true 11 | 12 | log.workspace = true 13 | -------------------------------------------------------------------------------- /crates/libs/environment/src/brdf.rs: -------------------------------------------------------------------------------- 1 | use super::{create_env_pipeline, EnvPipelineParameters}; 2 | use std::mem::size_of; 3 | use std::sync::Arc; 4 | use std::time::Instant; 5 | use vulkan::ash::vk::{self, RenderingAttachmentInfo, RenderingInfo}; 6 | use vulkan::{create_device_local_buffer_with_data, Buffer, Context, Texture, Vertex}; 7 | 8 | #[repr(C)] 9 | #[derive(Clone, Copy)] 10 | #[allow(dead_code)] 11 | struct QuadVertex { 12 | position: [f32; 2], 13 | coords: [f32; 2], 14 | } 15 | 16 | impl Vertex for QuadVertex { 17 | fn get_bindings_descriptions() -> Vec { 18 | vec![vk::VertexInputBindingDescription { 19 | binding: 0, 20 | stride: size_of::() as _, 21 | input_rate: vk::VertexInputRate::VERTEX, 22 | }] 23 | } 24 | 25 | fn get_attributes_descriptions() -> Vec { 26 | vec![ 27 | vk::VertexInputAttributeDescription { 28 | location: 0, 29 | binding: 0, 30 | format: vk::Format::R32G32_SFLOAT, 31 | offset: 0, 32 | }, 33 | vk::VertexInputAttributeDescription { 34 | location: 1, 35 | binding: 0, 36 | format: vk::Format::R32G32_SFLOAT, 37 | offset: 8, 38 | }, 39 | ] 40 | } 41 | } 42 | 43 | struct QuadModel { 44 | vertices: Buffer, 45 | indices: Buffer, 46 | } 47 | 48 | impl QuadModel { 49 | fn new(context: &Arc) -> Self { 50 | let indices: [u32; 6] = [0, 1, 2, 2, 3, 0]; 51 | let indices = create_device_local_buffer_with_data::( 52 | context, 53 | vk::BufferUsageFlags::INDEX_BUFFER, 54 | &indices, 55 | ); 56 | let vertices: [f32; 16] = [ 57 | -1.0, -1.0, 0.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, -1.0, 1.0, 0.0, 0.0, 58 | ]; 59 | let vertices = create_device_local_buffer_with_data::( 60 | context, 61 | vk::BufferUsageFlags::VERTEX_BUFFER, 62 | &vertices, 63 | ); 64 | 65 | Self { vertices, indices } 66 | } 67 | } 68 | 69 | pub(crate) fn create_brdf_lookup(context: &Arc, size: u32) -> Texture { 70 | log::info!("Creating brdf lookup"); 71 | let start = Instant::now(); 72 | 73 | let device = context.device(); 74 | 75 | let quad_model = QuadModel::new(context); 76 | 77 | let (pipeline_layout, pipeline) = { 78 | let layout = { 79 | let layout_info = vk::PipelineLayoutCreateInfo::default(); 80 | 81 | unsafe { device.create_pipeline_layout(&layout_info, None).unwrap() } 82 | }; 83 | 84 | let pipeline = { 85 | let viewports = [vk::Viewport { 86 | x: 0.0, 87 | y: 0.0, 88 | width: size as _, 89 | height: size as _, 90 | min_depth: 0.0, 91 | max_depth: 1.0, 92 | }]; 93 | 94 | let scissors = [vk::Rect2D { 95 | offset: vk::Offset2D { x: 0, y: 0 }, 96 | extent: vk::Extent2D { 97 | width: size, 98 | height: size, 99 | }, 100 | }]; 101 | let viewport_info = vk::PipelineViewportStateCreateInfo::default() 102 | .viewports(&viewports) 103 | .scissors(&scissors); 104 | 105 | let rasterizer_info = vk::PipelineRasterizationStateCreateInfo::default() 106 | .depth_clamp_enable(false) 107 | .rasterizer_discard_enable(false) 108 | .polygon_mode(vk::PolygonMode::FILL) 109 | .line_width(1.0) 110 | .cull_mode(vk::CullModeFlags::BACK) 111 | .front_face(vk::FrontFace::CLOCKWISE) 112 | .depth_bias_enable(false) 113 | .depth_bias_constant_factor(0.0) 114 | .depth_bias_clamp(0.0) 115 | .depth_bias_slope_factor(0.0); 116 | 117 | create_env_pipeline::( 118 | context, 119 | EnvPipelineParameters { 120 | vertex_shader_name: "fullscreen", 121 | fragment_shader_name: "brdf_lookup", 122 | viewport_info: &viewport_info, 123 | rasterizer_info: &rasterizer_info, 124 | dynamic_state_info: None, 125 | layout, 126 | format: vk::Format::R16G16_SFLOAT, 127 | }, 128 | ) 129 | }; 130 | 131 | (layout, pipeline) 132 | }; 133 | 134 | let lookup = Texture::create_renderable_texture(context, size, size, vk::Format::R16G16_SFLOAT); 135 | 136 | // Render 137 | context.execute_one_time_commands(|buffer| { 138 | { 139 | let attachment_info = RenderingAttachmentInfo::default() 140 | .clear_value(vk::ClearValue { 141 | color: vk::ClearColorValue { 142 | float32: [1.0, 0.0, 0.0, 1.0], 143 | }, 144 | }) 145 | .image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) 146 | .image_view(lookup.view) 147 | .load_op(vk::AttachmentLoadOp::CLEAR) 148 | .store_op(vk::AttachmentStoreOp::STORE); 149 | 150 | let rendering_info = RenderingInfo::default() 151 | .color_attachments(std::slice::from_ref(&attachment_info)) 152 | .layer_count(1) 153 | .render_area(vk::Rect2D { 154 | offset: vk::Offset2D { x: 0, y: 0 }, 155 | extent: vk::Extent2D { 156 | width: size, 157 | height: size, 158 | }, 159 | }); 160 | 161 | unsafe { 162 | context 163 | .dynamic_rendering() 164 | .cmd_begin_rendering(buffer, &rendering_info) 165 | }; 166 | 167 | unsafe { device.cmd_bind_pipeline(buffer, vk::PipelineBindPoint::GRAPHICS, pipeline) }; 168 | 169 | unsafe { 170 | device.cmd_bind_vertex_buffers(buffer, 0, &[quad_model.vertices.buffer], &[0]); 171 | } 172 | 173 | unsafe { 174 | device.cmd_bind_index_buffer( 175 | buffer, 176 | quad_model.indices.buffer, 177 | 0, 178 | vk::IndexType::UINT32, 179 | ); 180 | } 181 | 182 | // Draw quad 183 | unsafe { device.cmd_draw_indexed(buffer, 6, 1, 0, 0, 0) }; 184 | 185 | // End render pass 186 | unsafe { context.dynamic_rendering().cmd_end_rendering(buffer) } 187 | } 188 | }); 189 | 190 | lookup.image.transition_image_layout( 191 | vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL, 192 | vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, 193 | ); 194 | 195 | // Cleanup 196 | unsafe { 197 | device.destroy_pipeline(pipeline, None); 198 | device.destroy_pipeline_layout(pipeline_layout, None); 199 | } 200 | 201 | let time = start.elapsed().as_millis(); 202 | log::info!("Finished creating brdf lookup. Took {} ms", time); 203 | 204 | lookup 205 | } 206 | -------------------------------------------------------------------------------- /crates/libs/environment/src/cubemap.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | create_descriptors, create_env_pipeline, get_view_matrices, EnvPipelineParameters, SkyboxModel, 3 | SkyboxVertex, 4 | }; 5 | use cgmath::{Deg, Matrix4}; 6 | use math::*; 7 | use std::mem::size_of; 8 | use std::path::Path; 9 | use std::sync::Arc; 10 | use std::time::Instant; 11 | use util::*; 12 | use vulkan::ash::vk::{self, RenderingAttachmentInfo, RenderingInfo}; 13 | use vulkan::{Context, SamplerParameters, Texture}; 14 | 15 | pub(crate) fn create_skybox_cubemap>( 16 | context: &Arc, 17 | path: P, 18 | size: u32, 19 | ) -> Texture { 20 | log::info!("Creating cubemap from equirectangular texture"); 21 | let start = Instant::now(); 22 | let device = context.device(); 23 | let (w, h, data) = load_hdr_image(path); 24 | let mip_levels = (size as f32).log2().floor() as u32 + 1; 25 | 26 | let cubemap_format = vk::Format::R16G16B16A16_SFLOAT; 27 | 28 | let sampler_parameters = SamplerParameters { 29 | anisotropy_enabled: true, 30 | max_anisotropy: 16.0, 31 | ..Default::default() 32 | }; 33 | let texture = Texture::from_rgba_32(context, w, h, true, &data, Some(sampler_parameters)); 34 | let cubemap = Texture::create_renderable_cubemap(context, size, mip_levels, cubemap_format); 35 | 36 | let skybox_model = SkyboxModel::new(context); 37 | 38 | let descriptors = create_descriptors(context, &texture); 39 | 40 | let (pipeline_layout, pipeline) = { 41 | let layout = { 42 | let layouts = [descriptors.layout()]; 43 | let push_constant_range = [vk::PushConstantRange { 44 | stage_flags: vk::ShaderStageFlags::VERTEX, 45 | offset: 0, 46 | size: size_of::>() as _, 47 | }]; 48 | let layout_info = vk::PipelineLayoutCreateInfo::default() 49 | .set_layouts(&layouts) 50 | .push_constant_ranges(&push_constant_range); 51 | 52 | unsafe { device.create_pipeline_layout(&layout_info, None).unwrap() } 53 | }; 54 | 55 | let pipeline = { 56 | let viewport = vk::Viewport { 57 | x: 0.0, 58 | y: 0.0, 59 | width: size as _, 60 | height: size as _, 61 | min_depth: 0.0, 62 | max_depth: 1.0, 63 | }; 64 | 65 | let viewports = [viewport]; 66 | let scissor = vk::Rect2D { 67 | offset: vk::Offset2D { x: 0, y: 0 }, 68 | extent: vk::Extent2D { 69 | width: size, 70 | height: size, 71 | }, 72 | }; 73 | let scissors = [scissor]; 74 | let viewport_info = vk::PipelineViewportStateCreateInfo::default() 75 | .viewports(&viewports) 76 | .scissors(&scissors); 77 | 78 | let rasterizer_info = vk::PipelineRasterizationStateCreateInfo::default() 79 | .depth_clamp_enable(false) 80 | .rasterizer_discard_enable(false) 81 | .polygon_mode(vk::PolygonMode::FILL) 82 | .line_width(1.0) 83 | .cull_mode(vk::CullModeFlags::FRONT) 84 | .front_face(vk::FrontFace::COUNTER_CLOCKWISE) 85 | .depth_bias_enable(false) 86 | .depth_bias_constant_factor(0.0) 87 | .depth_bias_clamp(0.0) 88 | .depth_bias_slope_factor(0.0); 89 | 90 | create_env_pipeline::( 91 | context, 92 | EnvPipelineParameters { 93 | vertex_shader_name: "cubemap", 94 | fragment_shader_name: "spherical", 95 | viewport_info: &viewport_info, 96 | rasterizer_info: &rasterizer_info, 97 | dynamic_state_info: None, 98 | layout, 99 | format: cubemap_format, 100 | }, 101 | ) 102 | }; 103 | 104 | (layout, pipeline) 105 | }; 106 | 107 | let views = (0..6) 108 | .map(|i| { 109 | let create_info = vk::ImageViewCreateInfo::default() 110 | .image(cubemap.image.image) 111 | .view_type(vk::ImageViewType::TYPE_2D) 112 | .format(cubemap_format) 113 | .subresource_range(vk::ImageSubresourceRange { 114 | aspect_mask: vk::ImageAspectFlags::COLOR, 115 | base_mip_level: 0, 116 | level_count: 1, 117 | base_array_layer: i, 118 | layer_count: 1, 119 | }); 120 | 121 | unsafe { device.create_image_view(&create_info, None).unwrap() } 122 | }) 123 | .collect::>(); 124 | 125 | let view_matrices = get_view_matrices(); 126 | 127 | let proj = perspective(Deg(90.0), 1.0, 0.1, 10.0); 128 | 129 | // Render 130 | context.execute_one_time_commands(|buffer| { 131 | { 132 | for face in 0..6 { 133 | let attachment_info = RenderingAttachmentInfo::default() 134 | .clear_value(vk::ClearValue { 135 | color: vk::ClearColorValue { 136 | float32: [0.0, 0.0, 0.0, 1.0], 137 | }, 138 | }) 139 | .image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) 140 | .image_view(views[face]) 141 | .load_op(vk::AttachmentLoadOp::CLEAR) 142 | .store_op(vk::AttachmentStoreOp::STORE); 143 | 144 | let rendering_info = RenderingInfo::default() 145 | .color_attachments(std::slice::from_ref(&attachment_info)) 146 | .layer_count(1) 147 | .render_area(vk::Rect2D { 148 | offset: vk::Offset2D { x: 0, y: 0 }, 149 | extent: vk::Extent2D { 150 | width: size, 151 | height: size, 152 | }, 153 | }); 154 | 155 | unsafe { 156 | context 157 | .dynamic_rendering() 158 | .cmd_begin_rendering(buffer, &rendering_info) 159 | }; 160 | 161 | unsafe { 162 | device.cmd_bind_pipeline(buffer, vk::PipelineBindPoint::GRAPHICS, pipeline) 163 | }; 164 | 165 | unsafe { 166 | device.cmd_bind_descriptor_sets( 167 | buffer, 168 | vk::PipelineBindPoint::GRAPHICS, 169 | pipeline_layout, 170 | 0, 171 | &descriptors.sets()[0..=0], 172 | &[], 173 | ) 174 | }; 175 | 176 | let view = view_matrices[face]; 177 | let view_proj = proj * view; 178 | unsafe { 179 | let push = any_as_u8_slice(&view_proj); 180 | device.cmd_push_constants( 181 | buffer, 182 | pipeline_layout, 183 | vk::ShaderStageFlags::VERTEX, 184 | 0, 185 | push, 186 | ); 187 | }; 188 | 189 | unsafe { 190 | device.cmd_bind_vertex_buffers( 191 | buffer, 192 | 0, 193 | &[skybox_model.vertices().buffer], 194 | &[0], 195 | ); 196 | } 197 | 198 | unsafe { 199 | device.cmd_bind_index_buffer( 200 | buffer, 201 | skybox_model.indices().buffer, 202 | 0, 203 | vk::IndexType::UINT32, 204 | ); 205 | } 206 | 207 | // Draw skybox 208 | unsafe { device.cmd_draw_indexed(buffer, 36, 1, 0, 0, 0) }; 209 | 210 | // End render pass 211 | unsafe { context.dynamic_rendering().cmd_end_rendering(buffer) }; 212 | } 213 | } 214 | }); 215 | 216 | cubemap.image.transition_image_layout( 217 | vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL, 218 | vk::ImageLayout::TRANSFER_DST_OPTIMAL, 219 | ); 220 | 221 | cubemap.image.generate_mipmaps(vk::Extent2D { 222 | width: size, 223 | height: size, 224 | }); 225 | 226 | // Cleanup 227 | unsafe { 228 | views 229 | .iter() 230 | .for_each(|v| device.destroy_image_view(*v, None)); 231 | device.destroy_pipeline(pipeline, None); 232 | device.destroy_pipeline_layout(pipeline_layout, None); 233 | } 234 | 235 | let time = start.elapsed().as_millis(); 236 | log::info!( 237 | "Finished creating cubemap from equirectangular texture. Took {} ms", 238 | time 239 | ); 240 | 241 | cubemap 242 | } 243 | -------------------------------------------------------------------------------- /crates/libs/environment/src/irradiance.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | create_descriptors, create_env_pipeline, get_view_matrices, EnvPipelineParameters, SkyboxModel, 3 | SkyboxVertex, 4 | }; 5 | use cgmath::{Deg, Matrix4}; 6 | use math::*; 7 | use std::mem::size_of; 8 | use std::sync::Arc; 9 | use std::time::Instant; 10 | use util::*; 11 | use vulkan::ash::vk::{self, RenderingAttachmentInfo, RenderingInfo}; 12 | use vulkan::{Context, Texture}; 13 | 14 | pub(crate) fn create_irradiance_map( 15 | context: &Arc, 16 | cubemap: &Texture, 17 | size: u32, 18 | ) -> Texture { 19 | log::info!("Creating irradiance map"); 20 | let start = Instant::now(); 21 | 22 | let device = context.device(); 23 | 24 | let skybox_model = SkyboxModel::new(context); 25 | 26 | let descriptors = create_descriptors(context, cubemap); 27 | 28 | let (pipeline_layout, pipeline) = { 29 | let layout = { 30 | let layouts = [descriptors.layout()]; 31 | let push_constant_range = [vk::PushConstantRange { 32 | stage_flags: vk::ShaderStageFlags::VERTEX, 33 | offset: 0, 34 | size: size_of::>() as _, 35 | }]; 36 | let layout_info = vk::PipelineLayoutCreateInfo::default() 37 | .set_layouts(&layouts) 38 | .push_constant_ranges(&push_constant_range); 39 | 40 | unsafe { device.create_pipeline_layout(&layout_info, None).unwrap() } 41 | }; 42 | 43 | let pipeline = { 44 | let viewport = vk::Viewport { 45 | x: 0.0, 46 | y: 0.0, 47 | width: size as _, 48 | height: size as _, 49 | min_depth: 0.0, 50 | max_depth: 1.0, 51 | }; 52 | 53 | let viewports = [viewport]; 54 | let scissor = vk::Rect2D { 55 | offset: vk::Offset2D { x: 0, y: 0 }, 56 | extent: vk::Extent2D { 57 | width: size, 58 | height: size, 59 | }, 60 | }; 61 | let scissors = [scissor]; 62 | let viewport_info = vk::PipelineViewportStateCreateInfo::default() 63 | .viewports(&viewports) 64 | .scissors(&scissors); 65 | 66 | let rasterizer_info = vk::PipelineRasterizationStateCreateInfo::default() 67 | .depth_clamp_enable(false) 68 | .rasterizer_discard_enable(false) 69 | .polygon_mode(vk::PolygonMode::FILL) 70 | .line_width(1.0) 71 | .cull_mode(vk::CullModeFlags::FRONT) 72 | .front_face(vk::FrontFace::COUNTER_CLOCKWISE) 73 | .depth_bias_enable(false) 74 | .depth_bias_constant_factor(0.0) 75 | .depth_bias_clamp(0.0) 76 | .depth_bias_slope_factor(0.0); 77 | 78 | create_env_pipeline::( 79 | context, 80 | EnvPipelineParameters { 81 | vertex_shader_name: "cubemap", 82 | fragment_shader_name: "irradiance", 83 | viewport_info: &viewport_info, 84 | rasterizer_info: &rasterizer_info, 85 | dynamic_state_info: None, 86 | layout, 87 | format: vk::Format::R32G32B32A32_SFLOAT, 88 | }, 89 | ) 90 | }; 91 | 92 | (layout, pipeline) 93 | }; 94 | 95 | // create cubemap 96 | let irradiance_map = 97 | Texture::create_renderable_cubemap(context, size, 1, vk::Format::R32G32B32A32_SFLOAT); 98 | 99 | let views = (0..6) 100 | .map(|i| { 101 | let create_info = vk::ImageViewCreateInfo::default() 102 | .image(irradiance_map.image.image) 103 | .view_type(vk::ImageViewType::TYPE_2D) 104 | .format(vk::Format::R32G32B32A32_SFLOAT) 105 | .subresource_range(vk::ImageSubresourceRange { 106 | aspect_mask: vk::ImageAspectFlags::COLOR, 107 | base_mip_level: 0, 108 | level_count: 1, 109 | base_array_layer: i, 110 | layer_count: 1, 111 | }); 112 | 113 | unsafe { device.create_image_view(&create_info, None).unwrap() } 114 | }) 115 | .collect::>(); 116 | 117 | let view_matrices = get_view_matrices(); 118 | 119 | let proj = perspective(Deg(90.0), 1.0, 0.1, 10.0); 120 | 121 | // Render 122 | context.execute_one_time_commands(|buffer| { 123 | { 124 | for face in 0..6 { 125 | let attachment_info = RenderingAttachmentInfo::default() 126 | .clear_value(vk::ClearValue { 127 | color: vk::ClearColorValue { 128 | float32: [0.0, 0.0, 0.0, 1.0], 129 | }, 130 | }) 131 | .image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) 132 | .image_view(views[face]) 133 | .load_op(vk::AttachmentLoadOp::CLEAR) 134 | .store_op(vk::AttachmentStoreOp::STORE); 135 | 136 | let rendering_info = RenderingInfo::default() 137 | .color_attachments(std::slice::from_ref(&attachment_info)) 138 | .layer_count(1) 139 | .render_area(vk::Rect2D { 140 | offset: vk::Offset2D { x: 0, y: 0 }, 141 | extent: vk::Extent2D { 142 | width: size, 143 | height: size, 144 | }, 145 | }); 146 | 147 | unsafe { 148 | context 149 | .dynamic_rendering() 150 | .cmd_begin_rendering(buffer, &rendering_info) 151 | }; 152 | 153 | unsafe { 154 | device.cmd_bind_pipeline(buffer, vk::PipelineBindPoint::GRAPHICS, pipeline) 155 | }; 156 | 157 | unsafe { 158 | device.cmd_bind_descriptor_sets( 159 | buffer, 160 | vk::PipelineBindPoint::GRAPHICS, 161 | pipeline_layout, 162 | 0, 163 | &descriptors.sets()[0..=0], 164 | &[], 165 | ) 166 | }; 167 | 168 | let view = view_matrices[face]; 169 | let view_proj = proj * view; 170 | unsafe { 171 | let push = any_as_u8_slice(&view_proj); 172 | device.cmd_push_constants( 173 | buffer, 174 | pipeline_layout, 175 | vk::ShaderStageFlags::VERTEX, 176 | 0, 177 | push, 178 | ); 179 | }; 180 | 181 | unsafe { 182 | device.cmd_bind_vertex_buffers( 183 | buffer, 184 | 0, 185 | &[skybox_model.vertices().buffer], 186 | &[0], 187 | ); 188 | } 189 | 190 | unsafe { 191 | device.cmd_bind_index_buffer( 192 | buffer, 193 | skybox_model.indices().buffer, 194 | 0, 195 | vk::IndexType::UINT32, 196 | ); 197 | } 198 | 199 | // Draw skybox 200 | unsafe { device.cmd_draw_indexed(buffer, 36, 1, 0, 0, 0) }; 201 | 202 | // End render pass 203 | unsafe { context.dynamic_rendering().cmd_end_rendering(buffer) }; 204 | } 205 | } 206 | }); 207 | 208 | irradiance_map.image.transition_image_layout( 209 | vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL, 210 | vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, 211 | ); 212 | 213 | // Cleanup 214 | unsafe { 215 | views 216 | .iter() 217 | .for_each(|v| device.destroy_image_view(*v, None)); 218 | device.destroy_pipeline(pipeline, None); 219 | device.destroy_pipeline_layout(pipeline_layout, None); 220 | } 221 | 222 | let time = start.elapsed().as_millis(); 223 | log::info!("Finished creating irradiance map. Took {} ms", time); 224 | 225 | irradiance_map 226 | } 227 | -------------------------------------------------------------------------------- /crates/libs/math/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "math" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | 7 | [dependencies] 8 | log.workspace = true 9 | cgmath.workspace = true 10 | rand.workspace = true 11 | lerp.workspace = true 12 | -------------------------------------------------------------------------------- /crates/libs/math/src/aabb.rs: -------------------------------------------------------------------------------- 1 | use super::{partial_max, partial_min}; 2 | use cgmath::{BaseFloat, Matrix4, Vector3, Vector4}; 3 | use std::ops::Mul; 4 | 5 | /// Axis aligned bounding box. 6 | #[derive(Copy, Clone, Debug)] 7 | pub struct Aabb { 8 | min: Vector3, 9 | max: Vector3, 10 | } 11 | 12 | impl Aabb { 13 | /// Create a new AABB. 14 | pub fn new(min: Vector3, max: Vector3) -> Self { 15 | Aabb { min, max } 16 | } 17 | } 18 | 19 | impl Aabb { 20 | /// Compute the union of several AABBs. 21 | pub fn union(aabbs: &[Aabb]) -> Option { 22 | if aabbs.is_empty() { 23 | None 24 | } else if aabbs.len() == 1 { 25 | Some(aabbs[0]) 26 | } else { 27 | let min_x = partial_min(aabbs.iter().map(|aabb| aabb.min.x)).unwrap(); 28 | let min_y = partial_min(aabbs.iter().map(|aabb| aabb.min.y)).unwrap(); 29 | let min_z = partial_min(aabbs.iter().map(|aabb| aabb.min.z)).unwrap(); 30 | let min = Vector3::new(min_x, min_y, min_z); 31 | 32 | let max_x = partial_max(aabbs.iter().map(|aabb| aabb.max.x)).unwrap(); 33 | let max_y = partial_max(aabbs.iter().map(|aabb| aabb.max.y)).unwrap(); 34 | let max_z = partial_max(aabbs.iter().map(|aabb| aabb.max.z)).unwrap(); 35 | let max = Vector3::new(max_x, max_y, max_z); 36 | 37 | Some(Aabb::new(min, max)) 38 | } 39 | } 40 | 41 | /// Get the size of the larger side of the AABB. 42 | pub fn get_larger_side_size(&self) -> S { 43 | let size = self.max - self.min; 44 | let x = size.x.abs(); 45 | let y = size.y.abs(); 46 | let z = size.z.abs(); 47 | 48 | if x > y && x > z { 49 | x 50 | } else if y > z { 51 | y 52 | } else { 53 | z 54 | } 55 | } 56 | 57 | /// Get the center of the AABB. 58 | pub fn get_center(&self) -> Vector3 { 59 | let two = S::one() + S::one(); 60 | self.min + (self.max - self.min) / two 61 | } 62 | } 63 | 64 | /// Transform the AABB by multiplying it with a Matrix4. 65 | impl Mul> for Aabb { 66 | type Output = Aabb; 67 | 68 | fn mul(self, rhs: Matrix4) -> Self::Output { 69 | let min = self.min; 70 | let min = rhs * Vector4::new(min.x, min.y, min.z, S::one()); 71 | 72 | let max = self.max; 73 | let max = rhs * Vector4::new(max.x, max.y, max.z, S::one()); 74 | 75 | Aabb::new(min.truncate(), max.truncate()) 76 | } 77 | } 78 | 79 | /// Scale the AABB by multiplying it by a BaseFloat 80 | impl Mul for Aabb { 81 | type Output = Aabb; 82 | 83 | fn mul(self, rhs: S) -> Self::Output { 84 | Aabb::new(self.min * rhs, self.max * rhs) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /crates/libs/math/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod aabb; 2 | 3 | pub use aabb::*; 4 | pub use cgmath; 5 | pub use lerp; 6 | pub use rand; 7 | 8 | use cgmath::prelude::*; 9 | use cgmath::{BaseFloat, Matrix4, Quaternion, Rad}; 10 | use std::cmp::Ordering; 11 | 12 | /// Perspective matrix that is suitable for Vulkan. 13 | /// 14 | /// It inverts the projected y-axis. And set the depth range to 0..1 15 | /// instead of -1..1. Mind the vertex winding order though. 16 | #[rustfmt::skip] 17 | pub fn perspective(fovy: F, aspect: S, near: S, far: S) -> Matrix4 18 | where 19 | S: BaseFloat, 20 | F: Into>, 21 | { 22 | let two = S::one() + S::one(); 23 | let f = Rad::cot(fovy.into() / two); 24 | 25 | let c0r0 = f / aspect; 26 | let c0r1 = S::zero(); 27 | let c0r2 = S::zero(); 28 | let c0r3 = S::zero(); 29 | 30 | let c1r0 = S::zero(); 31 | let c1r1 = -f; 32 | let c1r2 = S::zero(); 33 | let c1r3 = S::zero(); 34 | 35 | let c2r0 = S::zero(); 36 | let c2r1 = S::zero(); 37 | let c2r2 = -far / (far - near); 38 | let c2r3 = -S::one(); 39 | 40 | let c3r0 = S::zero(); 41 | let c3r1 = S::zero(); 42 | let c3r2 = -(far * near) / (far - near); 43 | let c3r3 = S::zero(); 44 | 45 | Matrix4::new( 46 | c0r0, c0r1, c0r2, c0r3, 47 | c1r0, c1r1, c1r2, c1r3, 48 | c2r0, c2r1, c2r2, c2r3, 49 | c3r0, c3r1, c3r2, c3r3, 50 | ) 51 | } 52 | 53 | /// Clamp `value` between `min` and `max`. 54 | pub fn clamp(value: T, min: T, max: T) -> T { 55 | let value = if value > max { max } else { value }; 56 | if value < min { 57 | min 58 | } else { 59 | value 60 | } 61 | } 62 | 63 | /// Returns the min value of two PartialOrd values. 64 | pub fn min(v1: S, v2: S) -> S { 65 | match v1.partial_cmp(&v2) { 66 | Some(Ordering::Less) => v1, 67 | _ => v2, 68 | } 69 | } 70 | 71 | /// Returns the max value of two PartialOrd values. 72 | pub fn max(v1: S, v2: S) -> S { 73 | match v1.partial_cmp(&v2) { 74 | Some(Ordering::Greater) => v1, 75 | _ => v2, 76 | } 77 | } 78 | 79 | /// Return the partial minimum from an Iterator of PartialOrd if it exists. 80 | pub fn partial_min(iter: I) -> Option 81 | where 82 | S: PartialOrd, 83 | I: Iterator, 84 | { 85 | iter.min_by(|v1, v2| v1.partial_cmp(v2).unwrap_or(Ordering::Equal)) 86 | } 87 | 88 | /// Return the partial maximum from an Iterator of PartialOrd if it exists. 89 | pub fn partial_max(iter: I) -> Option 90 | where 91 | S: PartialOrd, 92 | I: Iterator, 93 | { 94 | iter.max_by(|v1, v2| v1.partial_cmp(v2).unwrap_or(Ordering::Equal)) 95 | } 96 | 97 | /// slerp from cgmath is bugged. 98 | /// 99 | /// This algorithm is suggested in the cgmath issue about slerp 100 | /// https://github.com/rustgd/cgmath/issues/300 101 | pub fn slerp(left: Quaternion, right: Quaternion, amount: f32) -> Quaternion { 102 | let num2; 103 | let num3; 104 | let num = amount; 105 | let mut num4 = (((left.v.x * right.v.x) + (left.v.y * right.v.y)) + (left.v.z * right.v.z)) 106 | + (left.s * right.s); 107 | let mut flag = false; 108 | if num4 < 0.0 { 109 | flag = true; 110 | num4 = -num4; 111 | } 112 | if num4 > 0.999_999 { 113 | num3 = 1.0 - num; 114 | num2 = if flag { -num } else { num }; 115 | } else { 116 | let num5 = num4.acos(); 117 | let num6 = 1.0 / num5.sin(); 118 | num3 = ((1.0 - num) * num5).sin() * num6; 119 | num2 = if flag { 120 | -(num * num5).sin() * num6 121 | } else { 122 | (num * num5).sin() * num6 123 | }; 124 | } 125 | Quaternion::new( 126 | (num3 * left.s) + (num2 * right.s), 127 | (num3 * left.v.x) + (num2 * right.v.x), 128 | (num3 * left.v.y) + (num2 * right.v.y), 129 | (num3 * left.v.z) + (num2 * right.v.z), 130 | ) 131 | } 132 | -------------------------------------------------------------------------------- /crates/libs/model/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "model" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | 7 | [dependencies] 8 | vulkan.workspace = true 9 | math.workspace = true 10 | 11 | log.workspace = true 12 | byteorder.workspace = true 13 | mikktspace.workspace = true 14 | 15 | [dependencies.gltf] 16 | workspace = true 17 | features = [ 18 | "KHR_lights_punctual", 19 | "KHR_materials_unlit", 20 | "KHR_materials_pbrSpecularGlossiness", 21 | "KHR_materials_emissive_strength", 22 | "KHR_materials_clearcoat", 23 | "KHR_texture_transform", 24 | "KHR_materials_ior", 25 | ] 26 | -------------------------------------------------------------------------------- /crates/libs/model/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt}; 2 | 3 | #[derive(Debug)] 4 | pub struct ModelLoadingError { 5 | message: String, 6 | } 7 | 8 | impl ModelLoadingError { 9 | pub fn new>(message: S) -> Self { 10 | Self { 11 | message: message.into(), 12 | } 13 | } 14 | } 15 | 16 | impl fmt::Display for ModelLoadingError { 17 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 18 | write!(f, "Failed to load model. Cause: {}", self.message) 19 | } 20 | } 21 | 22 | impl Error for ModelLoadingError {} 23 | -------------------------------------------------------------------------------- /crates/libs/model/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod animation; 2 | mod error; 3 | mod light; 4 | mod material; 5 | mod mesh; 6 | pub mod metadata; 7 | mod mikktspace; 8 | mod node; 9 | mod skin; 10 | mod texture; 11 | mod vertex; 12 | 13 | use self::mikktspace::generate_tangents; 14 | pub use self::{ 15 | animation::*, error::*, light::*, material::*, mesh::*, node::*, skin::*, texture::*, vertex::*, 16 | }; 17 | use cgmath::Matrix4; 18 | use math::*; 19 | use metadata::Metadata; 20 | use std::{error::Error, path::Path, result::Result, sync::Arc}; 21 | use vulkan::ash::vk; 22 | use vulkan::{Buffer, Context, PreLoadedResource}; 23 | 24 | pub struct ModelStagingResources { 25 | _staged_vertices: Buffer, 26 | _staged_indices: Option, 27 | _staged_textures: Vec, 28 | } 29 | 30 | pub struct Model { 31 | metadata: Metadata, 32 | meshes: Vec, 33 | nodes: Nodes, 34 | global_transform: Matrix4, 35 | animations: Option, 36 | skins: Vec, 37 | textures: Textures, 38 | materials: Vec, 39 | lights: Vec, 40 | } 41 | 42 | impl Model { 43 | pub fn create_from_file>( 44 | context: Arc, 45 | command_buffer: vk::CommandBuffer, 46 | path: P, 47 | ) -> Result, Box> { 48 | log::debug!("Importing gltf file"); 49 | let (document, buffers, images) = gltf::import(&path)?; 50 | 51 | let metadata = Metadata::new(path, &document); 52 | 53 | log::debug!("Creating the model"); 54 | if document.scenes().len() == 0 { 55 | return Err(Box::new(ModelLoadingError::new("There is no scene"))); 56 | } 57 | 58 | let meshes = create_meshes_from_gltf(&context, command_buffer, &document, &buffers); 59 | if meshes.is_none() { 60 | return Err(Box::new(ModelLoadingError::new( 61 | "Could not find any renderable primitives", 62 | ))); 63 | } 64 | 65 | let Meshes { 66 | meshes, 67 | vertices: staged_vertices, 68 | indices: staged_indices, 69 | } = meshes.unwrap(); 70 | 71 | let scene = document 72 | .default_scene() 73 | .unwrap_or_else(|| document.scenes().next().unwrap()); 74 | 75 | let animations = load_animations(document.animations(), &buffers); 76 | 77 | let mut skins = create_skins_from_gltf(document.skins(), &buffers); 78 | 79 | let mut nodes = Nodes::from_gltf_nodes(document.nodes(), &scene); 80 | 81 | let global_transform = { 82 | let aabb = compute_aabb(&nodes, &meshes); 83 | let transform = compute_unit_cube_at_origin_transform(aabb); 84 | nodes.transform(Some(transform)); 85 | nodes 86 | .get_skins_transform() 87 | .iter() 88 | .for_each(|(index, transform)| { 89 | let skin = &mut skins[*index]; 90 | skin.compute_joints_matrices(*transform, nodes.nodes()); 91 | }); 92 | transform 93 | }; 94 | 95 | let (textures, staged_textures) = texture::create_textures_from_gltf( 96 | &context, 97 | command_buffer, 98 | document.textures(), 99 | document.materials(), 100 | &images, 101 | ); 102 | 103 | let materials = create_materials_from_gltf(&document); 104 | 105 | let lights = create_lights_from_gltf(&document); 106 | 107 | let model = Model { 108 | metadata, 109 | meshes, 110 | nodes, 111 | global_transform, 112 | animations, 113 | skins, 114 | textures, 115 | materials, 116 | lights, 117 | }; 118 | 119 | let model_staging_res = ModelStagingResources { 120 | _staged_vertices: staged_vertices, 121 | _staged_indices: staged_indices, 122 | _staged_textures: staged_textures, 123 | }; 124 | 125 | Ok(PreLoadedResource::new( 126 | context, 127 | command_buffer, 128 | model, 129 | model_staging_res, 130 | )) 131 | } 132 | } 133 | 134 | impl Model { 135 | pub fn update(&mut self, delta_time: f32) -> bool { 136 | let updated = if let Some(animations) = self.animations.as_mut() { 137 | animations.update(&mut self.nodes, delta_time) 138 | } else { 139 | false 140 | }; 141 | 142 | if updated { 143 | self.nodes.transform(Some(self.global_transform)); 144 | self.nodes 145 | .get_skins_transform() 146 | .iter() 147 | .for_each(|(index, transform)| { 148 | let skin = &mut self.skins[*index]; 149 | skin.compute_joints_matrices(*transform, self.nodes.nodes()); 150 | }); 151 | } 152 | 153 | updated 154 | } 155 | } 156 | 157 | /// Animations methods 158 | impl Model { 159 | pub fn get_animation_playback_state(&self) -> Option { 160 | self.animations 161 | .as_ref() 162 | .map(Animations::get_playback_state) 163 | .copied() 164 | } 165 | 166 | pub fn set_current_animation(&mut self, animation_index: usize) { 167 | if let Some(animations) = self.animations.as_mut() { 168 | animations.set_current(animation_index); 169 | } 170 | } 171 | 172 | pub fn set_animation_playback_mode(&mut self, playback_mode: PlaybackMode) { 173 | if let Some(animations) = self.animations.as_mut() { 174 | animations.set_playback_mode(playback_mode); 175 | } 176 | } 177 | 178 | pub fn toggle_animation(&mut self) { 179 | if let Some(animations) = self.animations.as_mut() { 180 | animations.toggle(); 181 | } 182 | } 183 | 184 | pub fn stop_animation(&mut self) { 185 | if let Some(animations) = self.animations.as_mut() { 186 | animations.stop(); 187 | } 188 | } 189 | 190 | pub fn reset_animation(&mut self) { 191 | if let Some(animations) = self.animations.as_mut() { 192 | animations.reset(); 193 | } 194 | } 195 | } 196 | 197 | /// Getters 198 | impl Model { 199 | pub fn metadata(&self) -> &Metadata { 200 | &self.metadata 201 | } 202 | 203 | pub fn meshes(&self) -> &[Mesh] { 204 | &self.meshes 205 | } 206 | 207 | pub fn mesh(&self, index: usize) -> &Mesh { 208 | &self.meshes[index] 209 | } 210 | 211 | pub fn primitive_count(&self) -> usize { 212 | self.meshes.iter().map(Mesh::primitive_count).sum() 213 | } 214 | 215 | pub fn skins(&self) -> &[Skin] { 216 | &self.skins 217 | } 218 | 219 | pub fn nodes(&self) -> &Nodes { 220 | &self.nodes 221 | } 222 | 223 | pub fn textures(&self) -> &[Texture] { 224 | &self.textures.textures 225 | } 226 | 227 | pub fn materials(&self) -> &[Material] { 228 | &self.materials 229 | } 230 | 231 | pub fn lights(&self) -> &[Light] { 232 | &self.lights 233 | } 234 | } 235 | 236 | fn compute_aabb(nodes: &Nodes, meshes: &[Mesh]) -> Aabb { 237 | let aabbs = nodes 238 | .nodes() 239 | .iter() 240 | .filter(|n| n.mesh_index().is_some()) 241 | .map(|n| { 242 | let mesh = &meshes[n.mesh_index().unwrap()]; 243 | mesh.aabb() * n.transform() 244 | }) 245 | .collect::>(); 246 | Aabb::union(&aabbs).unwrap() 247 | } 248 | 249 | fn compute_unit_cube_at_origin_transform(aabb: Aabb) -> Matrix4 { 250 | let larger_side = aabb.get_larger_side_size(); 251 | let scale_factor = (1.0_f32 / larger_side) * 10.0; 252 | 253 | let aabb = aabb * scale_factor; 254 | let center = aabb.get_center(); 255 | 256 | let translation = Matrix4::from_translation(-center); 257 | let scale = Matrix4::from_scale(scale_factor); 258 | translation * scale 259 | } 260 | -------------------------------------------------------------------------------- /crates/libs/model/src/light.rs: -------------------------------------------------------------------------------- 1 | use gltf::iter::Lights; 2 | use gltf::khr_lights_punctual::{Kind, Light as GltfLight}; 3 | use gltf::Document; 4 | 5 | #[derive(Copy, Clone, Debug)] 6 | pub struct Light { 7 | color: [f32; 3], 8 | intensity: f32, 9 | range: Option, 10 | light_type: Type, 11 | } 12 | 13 | impl Light { 14 | pub fn color(&self) -> [f32; 3] { 15 | self.color 16 | } 17 | 18 | pub fn intensity(&self) -> f32 { 19 | self.intensity 20 | } 21 | 22 | pub fn range(&self) -> Option { 23 | self.range 24 | } 25 | 26 | pub fn light_type(&self) -> Type { 27 | self.light_type 28 | } 29 | } 30 | 31 | #[derive(Copy, Clone, PartialEq, Debug)] 32 | pub enum Type { 33 | Directional, 34 | Point, 35 | Spot { 36 | inner_cone_angle: f32, 37 | outer_cone_angle: f32, 38 | }, 39 | } 40 | 41 | pub(crate) fn create_lights_from_gltf(document: &Document) -> Vec { 42 | document.lights().map_or(vec![], map_gltf_lights) 43 | } 44 | 45 | fn map_gltf_lights(lights: Lights) -> Vec { 46 | lights.map(map_gltf_light).collect() 47 | } 48 | 49 | fn map_gltf_light(light: GltfLight) -> Light { 50 | let color = light.color(); 51 | let intensity = light.intensity(); 52 | let range = light.range(); 53 | let light_type = map_gltf_light_type(light.kind()); 54 | 55 | Light { 56 | color, 57 | intensity, 58 | range, 59 | light_type, 60 | } 61 | } 62 | 63 | fn map_gltf_light_type(kind: Kind) -> Type { 64 | match kind { 65 | Kind::Directional => Type::Directional, 66 | Kind::Point => Type::Point, 67 | Kind::Spot { 68 | inner_cone_angle, 69 | outer_cone_angle, 70 | } => Type::Spot { 71 | inner_cone_angle, 72 | outer_cone_angle, 73 | }, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /crates/libs/model/src/mikktspace.rs: -------------------------------------------------------------------------------- 1 | use super::vertex::ModelVertex; 2 | 3 | const VERTEX_PER_FACE: usize = 3; 4 | 5 | type Face = [u32; 3]; 6 | 7 | struct Mesh<'a> { 8 | faces: Vec, 9 | vertices: &'a mut [ModelVertex], 10 | } 11 | 12 | impl<'a> Mesh<'a> { 13 | fn get_vertex(&self, face: usize, vert: usize) -> ModelVertex { 14 | let face = self.faces[face]; 15 | self.vertices[face[vert] as usize] 16 | } 17 | 18 | fn get_vertex_mut(&mut self, face: usize, vert: usize) -> &mut ModelVertex { 19 | let face = self.faces[face]; 20 | &mut self.vertices[face[vert] as usize] 21 | } 22 | } 23 | 24 | impl<'a> mikktspace::Geometry for Mesh<'a> { 25 | fn num_faces(&self) -> usize { 26 | self.faces.len() 27 | } 28 | 29 | fn num_vertices_of_face(&self, _face: usize) -> usize { 30 | VERTEX_PER_FACE 31 | } 32 | 33 | fn position(&self, face: usize, vert: usize) -> [f32; 3] { 34 | self.get_vertex(face, vert).position 35 | } 36 | 37 | fn normal(&self, face: usize, vert: usize) -> [f32; 3] { 38 | self.get_vertex(face, vert).normal 39 | } 40 | 41 | fn tex_coord(&self, face: usize, vert: usize) -> [f32; 2] { 42 | self.get_vertex(face, vert).tex_coords_0 43 | } 44 | 45 | fn set_tangent( 46 | &mut self, 47 | tangent: [f32; 3], 48 | _bi_tangent: [f32; 3], 49 | _f_mag_s: f32, 50 | _f_mag_t: f32, 51 | bi_tangent_preserves_orientation: bool, 52 | face: usize, 53 | vert: usize, 54 | ) { 55 | let sign = if bi_tangent_preserves_orientation { 56 | -1.0 57 | } else { 58 | 1.0 59 | }; 60 | let vertex = self.get_vertex_mut(face, vert); 61 | vertex.tangent = [tangent[0], tangent[1], tangent[2], sign]; 62 | } 63 | } 64 | 65 | pub fn generate_tangents(indices: Option<&[u32]>, vertices: &mut [ModelVertex]) { 66 | log::info!("Generating tangents"); 67 | 68 | let index_count = indices.map_or(0, |indices| indices.len()); 69 | if !can_generate_inputs(index_count, vertices.len()) { 70 | log::warn!("Tangents won't be generated"); 71 | return; 72 | } 73 | 74 | let faces = if let Some(indices) = indices { 75 | (0..index_count) 76 | .step_by(VERTEX_PER_FACE) 77 | .map(|i| [indices[i], indices[i + 1], indices[i + 2]]) 78 | .collect::>() 79 | } else { 80 | let vertex_count = vertices.len() as u32; 81 | (0..vertex_count) 82 | .step_by(VERTEX_PER_FACE) 83 | .map(|i| [i, i + 1, i + 2]) 84 | .collect::>() 85 | }; 86 | 87 | let mut mesh = Mesh { faces, vertices }; 88 | 89 | mikktspace::generate_tangents(&mut mesh); 90 | } 91 | 92 | fn can_generate_inputs(index_count: usize, vertex_count: usize) -> bool { 93 | if vertex_count == 0 { 94 | log::warn!("A primitive must have at least 1 vertex in order to generate tangents"); 95 | return false; 96 | } 97 | 98 | if index_count > 0 && index_count % VERTEX_PER_FACE != 0 { 99 | log::warn!("The number of indices for a given primitive mush be a multiple of 3"); 100 | return false; 101 | } 102 | 103 | if index_count == 0 && vertex_count % VERTEX_PER_FACE != 0 { 104 | log::warn!( 105 | "The number of vertices for a given primitive without indices mush be a multiple of 3" 106 | ); 107 | return false; 108 | } 109 | 110 | true 111 | } 112 | -------------------------------------------------------------------------------- /crates/libs/model/src/node.rs: -------------------------------------------------------------------------------- 1 | use gltf::{iter::Nodes as GltfNodes, scene::Transform, Scene}; 2 | use math::cgmath::{Matrix4, Quaternion, Vector3}; 3 | 4 | #[derive(Clone, Debug)] 5 | pub struct Nodes { 6 | nodes: Vec, 7 | depth_first_taversal_indices: Vec<(usize, Option)>, 8 | } 9 | 10 | impl Nodes { 11 | pub fn from_gltf_nodes(gltf_nodes: GltfNodes, scene: &Scene) -> Nodes { 12 | let roots_indices = scene.nodes().map(|n| n.index()).collect::>(); 13 | let node_count = gltf_nodes.len(); 14 | let mut nodes = Vec::with_capacity(node_count); 15 | for node in gltf_nodes { 16 | let node_index = node.index(); 17 | let local_transform = node.transform(); 18 | let global_transform_matrix = compute_transform_matrix(&local_transform); 19 | let mesh_index = node.mesh().map(|m| m.index()); 20 | let skin_index = node.skin().map(|s| s.index()); 21 | let light_index = node.light().map(|l| l.index()); 22 | let children_indices = node.children().map(|c| c.index()).collect::>(); 23 | let node = Node { 24 | local_transform, 25 | global_transform_matrix, 26 | mesh_index, 27 | skin_index, 28 | light_index, 29 | children_indices, 30 | }; 31 | nodes.insert(node_index, node); 32 | } 33 | 34 | let mut nodes = Nodes::new(nodes, roots_indices); 35 | // Derive the global transform 36 | nodes.transform(None); 37 | nodes 38 | } 39 | 40 | fn new(nodes: Vec, roots_indices: Vec) -> Self { 41 | let depth_first_taversal_indices = build_graph_run_indices(&roots_indices, &nodes); 42 | Self { 43 | nodes, 44 | depth_first_taversal_indices, 45 | } 46 | } 47 | } 48 | 49 | impl Nodes { 50 | pub fn transform(&mut self, global_transform: Option>) { 51 | for (index, parent_index) in &self.depth_first_taversal_indices { 52 | let parent_transform = parent_index 53 | .map(|id| { 54 | let parent = &self.nodes[id]; 55 | parent.global_transform_matrix 56 | }) 57 | .or(global_transform); 58 | 59 | if let Some(matrix) = parent_transform { 60 | let node = &mut self.nodes[*index]; 61 | node.apply_transform(matrix); 62 | } 63 | } 64 | } 65 | 66 | pub fn get_skins_transform(&self) -> Vec<(usize, Matrix4)> { 67 | self.nodes 68 | .iter() 69 | .filter(|n| n.skin_index.is_some()) 70 | .map(|n| (n.skin_index.unwrap(), n.transform())) 71 | .collect::>() 72 | } 73 | } 74 | 75 | fn build_graph_run_indices(roots_indices: &[usize], nodes: &[Node]) -> Vec<(usize, Option)> { 76 | let mut indices = Vec::new(); 77 | for root_index in roots_indices { 78 | build_graph_run_indices_rec(nodes, *root_index, None, &mut indices); 79 | } 80 | indices 81 | } 82 | 83 | fn build_graph_run_indices_rec( 84 | nodes: &[Node], 85 | node_index: usize, 86 | parent_index: Option, 87 | indices: &mut Vec<(usize, Option)>, 88 | ) { 89 | indices.push((node_index, parent_index)); 90 | for child_index in &nodes[node_index].children_indices { 91 | build_graph_run_indices_rec(nodes, *child_index, Some(node_index), indices); 92 | } 93 | } 94 | 95 | impl Nodes { 96 | pub fn nodes(&self) -> &[Node] { 97 | &self.nodes 98 | } 99 | 100 | pub fn nodes_mut(&mut self) -> &mut [Node] { 101 | &mut self.nodes 102 | } 103 | } 104 | 105 | #[derive(Clone, Debug)] 106 | pub struct Node { 107 | local_transform: Transform, 108 | global_transform_matrix: Matrix4, 109 | mesh_index: Option, 110 | skin_index: Option, 111 | light_index: Option, 112 | children_indices: Vec, 113 | } 114 | 115 | impl Node { 116 | fn apply_transform(&mut self, transform: Matrix4) { 117 | let new_tranform = transform * compute_transform_matrix(&self.local_transform); 118 | self.global_transform_matrix = new_tranform; 119 | } 120 | } 121 | 122 | impl Node { 123 | pub fn transform(&self) -> Matrix4 { 124 | self.global_transform_matrix 125 | } 126 | 127 | pub fn mesh_index(&self) -> Option { 128 | self.mesh_index 129 | } 130 | 131 | pub fn skin_index(&self) -> Option { 132 | self.skin_index 133 | } 134 | 135 | pub fn light_index(&self) -> Option { 136 | self.light_index 137 | } 138 | 139 | pub fn set_translation(&mut self, translation: Vector3) { 140 | if let Transform::Decomposed { 141 | rotation, scale, .. 142 | } = self.local_transform 143 | { 144 | self.local_transform = Transform::Decomposed { 145 | translation: [translation.x, translation.y, translation.z], 146 | rotation, 147 | scale, 148 | } 149 | } 150 | } 151 | 152 | pub fn set_rotation(&mut self, rotation: Quaternion) { 153 | if let Transform::Decomposed { 154 | translation, scale, .. 155 | } = self.local_transform 156 | { 157 | self.local_transform = Transform::Decomposed { 158 | translation, 159 | rotation: [rotation.v.x, rotation.v.y, rotation.v.z, rotation.s], 160 | scale, 161 | } 162 | } 163 | } 164 | 165 | pub fn set_scale(&mut self, scale: Vector3) { 166 | if let Transform::Decomposed { 167 | translation, 168 | rotation, 169 | .. 170 | } = self.local_transform 171 | { 172 | self.local_transform = Transform::Decomposed { 173 | translation, 174 | rotation, 175 | scale: [scale.x, scale.y, scale.z], 176 | } 177 | } 178 | } 179 | } 180 | 181 | fn compute_transform_matrix(transform: &Transform) -> Matrix4 { 182 | match transform { 183 | Transform::Matrix { matrix } => Matrix4::from(*matrix), 184 | Transform::Decomposed { 185 | translation, 186 | rotation: [xr, yr, zr, wr], 187 | scale: [xs, ys, zs], 188 | } => { 189 | let translation = Matrix4::from_translation(Vector3::from(*translation)); 190 | let rotation = Matrix4::from(Quaternion::new(*wr, *xr, *yr, *zr)); 191 | let scale = Matrix4::from_nonuniform_scale(*xs, *ys, *zs); 192 | translation * rotation * scale 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /crates/libs/model/src/skin.rs: -------------------------------------------------------------------------------- 1 | use super::node::Node; 2 | use gltf::{buffer::Data, iter::Skins as GltfSkins, Skin as GltfSkin}; 3 | use math::cgmath::{Matrix4, SquareMatrix}; 4 | 5 | // Must be kept in sync with the value in model.vert 6 | pub const MAX_JOINTS_PER_MESH: usize = 512; 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct Skin { 10 | joints: Vec, 11 | } 12 | 13 | impl Skin { 14 | /// Compute the joints matrices from the nodes matrices. 15 | pub fn compute_joints_matrices(&mut self, transform: Matrix4, nodes: &[Node]) { 16 | self.joints 17 | .iter_mut() 18 | .for_each(|j| j.compute_matrix(transform, nodes)); 19 | } 20 | } 21 | 22 | impl Skin { 23 | pub fn joints(&self) -> &[Joint] { 24 | &self.joints 25 | } 26 | } 27 | 28 | #[derive(Copy, Clone, Debug)] 29 | pub struct Joint { 30 | matrix: Matrix4, 31 | inverse_bind_matrix: Matrix4, 32 | node_id: usize, 33 | } 34 | 35 | impl Joint { 36 | fn new(inverse_bind_matrix: Matrix4, node_id: usize) -> Self { 37 | Joint { 38 | matrix: Matrix4::identity(), 39 | inverse_bind_matrix, 40 | node_id, 41 | } 42 | } 43 | 44 | fn compute_matrix(&mut self, transform: Matrix4, nodes: &[Node]) { 45 | let global_transform_inverse = transform 46 | .invert() 47 | .expect("Transform matrix should be invertible"); 48 | let node_transform = nodes[self.node_id].transform(); 49 | 50 | self.matrix = global_transform_inverse * node_transform * self.inverse_bind_matrix; 51 | } 52 | } 53 | 54 | impl Joint { 55 | pub fn matrix(&self) -> Matrix4 { 56 | self.matrix 57 | } 58 | } 59 | 60 | pub fn create_skins_from_gltf(gltf_skins: GltfSkins, data: &[Data]) -> Vec { 61 | gltf_skins.map(|s| map_skin(&s, data)).collect::>() 62 | } 63 | 64 | fn map_skin(gltf_skin: &GltfSkin, data: &[Data]) -> Skin { 65 | let joint_count = gltf_skin.joints().count(); 66 | if joint_count > MAX_JOINTS_PER_MESH { 67 | log::warn!( 68 | "Skin {} has more than {} joints ({}). Mesh might not display properly", 69 | gltf_skin.index(), 70 | MAX_JOINTS_PER_MESH, 71 | joint_count 72 | ); 73 | } 74 | 75 | let inverse_bind_matrices = map_inverse_bind_matrices(gltf_skin, data); 76 | let node_ids = map_node_ids(gltf_skin); 77 | 78 | let joints = inverse_bind_matrices 79 | .iter() 80 | .zip(node_ids) 81 | .map(|(matrix, node_id)| Joint::new(*matrix, node_id)) 82 | .collect::>(); 83 | 84 | Skin { joints } 85 | } 86 | 87 | fn map_inverse_bind_matrices(gltf_skin: &GltfSkin, data: &[Data]) -> Vec> { 88 | let iter = gltf_skin 89 | .reader(|buffer| Some(&data[buffer.index()])) 90 | .read_inverse_bind_matrices() 91 | .expect("IBM reader not found for skin"); 92 | iter.map(Matrix4::from).collect::>() 93 | } 94 | 95 | fn map_node_ids(gltf_skin: &GltfSkin) -> Vec { 96 | gltf_skin 97 | .joints() 98 | .map(|node| node.index()) 99 | .collect::>() 100 | } 101 | -------------------------------------------------------------------------------- /crates/libs/model/src/texture.rs: -------------------------------------------------------------------------------- 1 | use gltf::image::{Data, Format}; 2 | use gltf::iter::{Materials, Textures as GltfTextures}; 3 | use gltf::json::texture::{MagFilter, MinFilter, WrappingMode}; 4 | use gltf::texture::Sampler; 5 | use std::collections::HashSet; 6 | use std::sync::Arc; 7 | use vulkan::ash::vk; 8 | use vulkan::{Buffer, Context, Image, Texture as VulkanTexture}; 9 | 10 | pub(crate) struct Textures { 11 | _images: Vec, 12 | pub textures: Vec, 13 | } 14 | 15 | pub struct Texture { 16 | context: Arc, 17 | view: vk::ImageView, 18 | sampler: vk::Sampler, 19 | } 20 | 21 | impl Texture { 22 | pub fn get_view(&self) -> vk::ImageView { 23 | self.view 24 | } 25 | 26 | pub fn get_sampler(&self) -> vk::Sampler { 27 | self.sampler 28 | } 29 | } 30 | 31 | impl Drop for Texture { 32 | fn drop(&mut self) { 33 | unsafe { 34 | self.context.device().destroy_sampler(self.sampler, None); 35 | } 36 | } 37 | } 38 | 39 | /// Create 40 | pub(crate) fn create_textures_from_gltf( 41 | context: &Arc, 42 | command_buffer: vk::CommandBuffer, 43 | textures: GltfTextures, 44 | materials: Materials, 45 | images: &[Data], 46 | ) -> (Textures, Vec) { 47 | let srgb_image_indices = { 48 | let mut indices = HashSet::new(); 49 | 50 | for m in materials { 51 | if let Some(t) = m.pbr_metallic_roughness().base_color_texture() { 52 | indices.insert(t.texture().source().index()); 53 | } 54 | 55 | if let Some(pbr_specular_glossiness) = m.pbr_specular_glossiness() { 56 | if let Some(t) = pbr_specular_glossiness.diffuse_texture() { 57 | indices.insert(t.texture().source().index()); 58 | } 59 | 60 | if let Some(t) = pbr_specular_glossiness.specular_glossiness_texture() { 61 | indices.insert(t.texture().source().index()); 62 | } 63 | } 64 | 65 | if let Some(t) = m.emissive_texture() { 66 | indices.insert(t.texture().source().index()); 67 | } 68 | } 69 | 70 | indices 71 | }; 72 | 73 | let (images, buffers) = images 74 | .iter() 75 | .enumerate() 76 | .map(|(index, image)| { 77 | let pixels = build_rgba_buffer(image); 78 | let is_srgb = srgb_image_indices.contains(&index); 79 | VulkanTexture::cmd_from_rgba( 80 | context, 81 | command_buffer, 82 | image.width, 83 | image.height, 84 | &pixels, 85 | !is_srgb, 86 | ) 87 | }) 88 | .unzip::<_, _, Vec<_>, _>(); 89 | 90 | let textures = textures 91 | .map(|t| { 92 | let context = Arc::clone(context); 93 | let image = &images[t.source().index()]; 94 | let view = image.view; 95 | let sampler = map_sampler(&context, &image.image, &t.sampler()); 96 | Texture { 97 | context, 98 | view, 99 | sampler, 100 | } 101 | }) 102 | .collect(); 103 | 104 | ( 105 | Textures { 106 | _images: images, 107 | textures, 108 | }, 109 | buffers, 110 | ) 111 | } 112 | 113 | fn build_rgba_buffer(image: &Data) -> Vec { 114 | let mut buffer = Vec::new(); 115 | let size = image.width * image.height; 116 | for index in 0..size { 117 | let rgba = get_next_rgba(&image.pixels, image.format, index as usize); 118 | buffer.extend_from_slice(&rgba); 119 | } 120 | buffer 121 | } 122 | 123 | fn get_next_rgba(pixels: &[u8], format: Format, index: usize) -> [u8; 4] { 124 | use Format::*; 125 | match format { 126 | // actually luma8 127 | R8 => [pixels[index], pixels[index], pixels[index], u8::MAX], 128 | // actually luma8 with alpha 129 | R8G8 => [ 130 | pixels[index * 2], 131 | pixels[index * 2], 132 | pixels[index * 2], 133 | pixels[index * 2 + 1], 134 | ], 135 | R8G8B8 => [ 136 | pixels[index * 3], 137 | pixels[index * 3 + 1], 138 | pixels[index * 3 + 2], 139 | u8::MAX, 140 | ], 141 | R8G8B8A8 => [ 142 | pixels[index * 4], 143 | pixels[index * 4 + 1], 144 | pixels[index * 4 + 2], 145 | pixels[index * 4 + 3], 146 | ], 147 | R16 | R16G16 | R16G16B16 | R16G16B16A16 | R32G32B32FLOAT | R32G32B32A32FLOAT => { 148 | panic!("16/32 bits colors are not supported for model textures") 149 | } 150 | } 151 | } 152 | 153 | fn map_sampler(context: &Arc, image: &Image, sampler: &Sampler) -> vk::Sampler { 154 | let min_filter = sampler.min_filter().unwrap_or(MinFilter::Linear); 155 | let mag_filter = sampler.mag_filter().unwrap_or(MagFilter::Linear); 156 | let has_mipmaps = has_mipmaps(min_filter); 157 | let max_lod = if has_mipmaps { 158 | image.get_mip_levels() as f32 159 | } else { 160 | 0.25 161 | }; 162 | 163 | let sampler_info = vk::SamplerCreateInfo::default() 164 | .mag_filter(map_mag_filter(mag_filter)) 165 | .min_filter(map_min_filter(min_filter)) 166 | .address_mode_u(map_wrap_mode(sampler.wrap_s())) 167 | .address_mode_v(map_wrap_mode(sampler.wrap_t())) 168 | .address_mode_w(vk::SamplerAddressMode::REPEAT) 169 | .anisotropy_enable(has_mipmaps) 170 | .max_anisotropy(16.0) 171 | .border_color(vk::BorderColor::INT_OPAQUE_BLACK) 172 | .unnormalized_coordinates(false) 173 | .compare_enable(false) 174 | .compare_op(vk::CompareOp::ALWAYS) 175 | .mipmap_mode(map_mipmap_filter(min_filter)) 176 | .mip_lod_bias(0.0) 177 | .min_lod(0.0) 178 | .max_lod(max_lod); 179 | 180 | unsafe { 181 | context 182 | .device() 183 | .create_sampler(&sampler_info, None) 184 | .expect("Failed to create sampler") 185 | } 186 | } 187 | 188 | fn has_mipmaps(filter: MinFilter) -> bool { 189 | filter != MinFilter::Linear && filter != MinFilter::Nearest 190 | } 191 | 192 | fn map_wrap_mode(wrap_mode: WrappingMode) -> vk::SamplerAddressMode { 193 | match wrap_mode { 194 | WrappingMode::ClampToEdge => vk::SamplerAddressMode::CLAMP_TO_EDGE, 195 | WrappingMode::MirroredRepeat => vk::SamplerAddressMode::MIRRORED_REPEAT, 196 | WrappingMode::Repeat => vk::SamplerAddressMode::REPEAT, 197 | } 198 | } 199 | 200 | fn map_min_filter(min_filter: MinFilter) -> vk::Filter { 201 | match min_filter { 202 | MinFilter::Nearest => vk::Filter::NEAREST, 203 | MinFilter::Linear => vk::Filter::LINEAR, 204 | MinFilter::NearestMipmapNearest => vk::Filter::NEAREST, 205 | MinFilter::LinearMipmapNearest => vk::Filter::LINEAR, 206 | MinFilter::NearestMipmapLinear => vk::Filter::NEAREST, 207 | MinFilter::LinearMipmapLinear => vk::Filter::LINEAR, 208 | } 209 | } 210 | 211 | fn map_mag_filter(mag_filter: MagFilter) -> vk::Filter { 212 | match mag_filter { 213 | MagFilter::Nearest => vk::Filter::NEAREST, 214 | MagFilter::Linear => vk::Filter::LINEAR, 215 | } 216 | } 217 | 218 | fn map_mipmap_filter(min_filter: MinFilter) -> vk::SamplerMipmapMode { 219 | match min_filter { 220 | MinFilter::Nearest => vk::SamplerMipmapMode::NEAREST, 221 | MinFilter::Linear => vk::SamplerMipmapMode::NEAREST, 222 | MinFilter::NearestMipmapNearest => vk::SamplerMipmapMode::NEAREST, 223 | MinFilter::LinearMipmapNearest => vk::SamplerMipmapMode::NEAREST, 224 | MinFilter::NearestMipmapLinear => vk::SamplerMipmapMode::LINEAR, 225 | MinFilter::LinearMipmapLinear => vk::SamplerMipmapMode::LINEAR, 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /crates/libs/model/src/vertex.rs: -------------------------------------------------------------------------------- 1 | use ash::vk; 2 | use std::{mem::size_of, sync::Arc}; 3 | use vulkan::*; 4 | 5 | const POSITION_LOCATION: u32 = 0; 6 | const NORMAL_LOCATION: u32 = 1; 7 | const TEX_COORDS_0_LOCATION: u32 = 2; 8 | const TEX_COORDS_1_LOCATION: u32 = 3; 9 | const TANGENT_LOCATION: u32 = 4; 10 | const WEIGHTS_LOCATION: u32 = 5; 11 | const JOINTS_LOCATION: u32 = 6; 12 | const COLOR_LOCATION: u32 = 7; 13 | 14 | const POSITION_OFFSET: u32 = 0; 15 | const NORMAL_OFFSET: u32 = 12; 16 | const TEX_COORDS_0_OFFSET: u32 = 24; 17 | const TEX_COORDS_1_OFFSET: u32 = 32; 18 | const TANGENT_OFFSET: u32 = 40; 19 | const WEIGHTS_OFFSET: u32 = 56; 20 | const JOINTS_OFFSET: u32 = 72; 21 | const COLOR_OFFSET: u32 = 88; 22 | 23 | #[repr(C)] 24 | #[derive(Clone, Copy, Debug)] 25 | pub struct ModelVertex { 26 | pub position: [f32; 3], 27 | pub normal: [f32; 3], 28 | pub tex_coords_0: [f32; 2], 29 | pub tex_coords_1: [f32; 2], 30 | pub tangent: [f32; 4], 31 | pub weights: [f32; 4], 32 | pub joints: [u32; 4], 33 | pub colors: [f32; 4], 34 | } 35 | 36 | impl Vertex for ModelVertex { 37 | fn get_bindings_descriptions() -> Vec { 38 | vec![vk::VertexInputBindingDescription { 39 | binding: 0, 40 | stride: size_of::() as _, 41 | input_rate: vk::VertexInputRate::VERTEX, 42 | }] 43 | } 44 | 45 | fn get_attributes_descriptions() -> Vec { 46 | vec![ 47 | vk::VertexInputAttributeDescription { 48 | location: POSITION_LOCATION, 49 | binding: 0, 50 | format: vk::Format::R32G32B32_SFLOAT, 51 | offset: POSITION_OFFSET, 52 | }, 53 | vk::VertexInputAttributeDescription { 54 | location: NORMAL_LOCATION, 55 | binding: 0, 56 | format: vk::Format::R32G32B32_SFLOAT, 57 | offset: NORMAL_OFFSET, 58 | }, 59 | vk::VertexInputAttributeDescription { 60 | location: TEX_COORDS_0_LOCATION, 61 | binding: 0, 62 | format: vk::Format::R32G32_SFLOAT, 63 | offset: TEX_COORDS_0_OFFSET, 64 | }, 65 | vk::VertexInputAttributeDescription { 66 | location: TEX_COORDS_1_LOCATION, 67 | binding: 0, 68 | format: vk::Format::R32G32_SFLOAT, 69 | offset: TEX_COORDS_1_OFFSET, 70 | }, 71 | vk::VertexInputAttributeDescription { 72 | location: TANGENT_LOCATION, 73 | binding: 0, 74 | format: vk::Format::R32G32B32A32_SFLOAT, 75 | offset: TANGENT_OFFSET, 76 | }, 77 | vk::VertexInputAttributeDescription { 78 | location: WEIGHTS_LOCATION, 79 | binding: 0, 80 | format: vk::Format::R32G32B32A32_SFLOAT, 81 | offset: WEIGHTS_OFFSET, 82 | }, 83 | vk::VertexInputAttributeDescription { 84 | location: JOINTS_LOCATION, 85 | binding: 0, 86 | format: vk::Format::R32G32B32A32_UINT, 87 | offset: JOINTS_OFFSET, 88 | }, 89 | vk::VertexInputAttributeDescription { 90 | location: COLOR_LOCATION, 91 | binding: 0, 92 | format: vk::Format::R32G32B32A32_SFLOAT, 93 | offset: COLOR_OFFSET, 94 | }, 95 | ] 96 | } 97 | } 98 | 99 | pub struct VertexBuffer { 100 | buffer: Arc, 101 | offset: vk::DeviceSize, 102 | element_count: u32, 103 | } 104 | 105 | impl VertexBuffer { 106 | pub fn new(buffer: Arc, offset: vk::DeviceSize, element_count: u32) -> Self { 107 | Self { 108 | buffer, 109 | offset, 110 | element_count, 111 | } 112 | } 113 | } 114 | 115 | impl VertexBuffer { 116 | pub fn buffer(&self) -> &Buffer { 117 | &self.buffer 118 | } 119 | 120 | pub fn offset(&self) -> vk::DeviceSize { 121 | self.offset 122 | } 123 | 124 | pub fn element_count(&self) -> u32 { 125 | self.element_count 126 | } 127 | } 128 | 129 | pub struct IndexBuffer { 130 | buffer: Arc, 131 | offset: vk::DeviceSize, 132 | element_count: u32, 133 | index_type: vk::IndexType, 134 | } 135 | 136 | impl IndexBuffer { 137 | pub fn new(buffer: Arc, offset: vk::DeviceSize, element_count: u32) -> Self { 138 | Self { 139 | buffer, 140 | offset, 141 | element_count, 142 | index_type: vk::IndexType::UINT32, 143 | } 144 | } 145 | } 146 | 147 | impl IndexBuffer { 148 | pub fn buffer(&self) -> &Buffer { 149 | &self.buffer 150 | } 151 | 152 | pub fn offset(&self) -> vk::DeviceSize { 153 | self.offset 154 | } 155 | 156 | pub fn element_count(&self) -> u32 { 157 | self.element_count 158 | } 159 | 160 | pub fn index_type(&self) -> vk::IndexType { 161 | self.index_type 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /crates/libs/util/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "util" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | 7 | [dependencies] 8 | log.workspace = true 9 | image.workspace = true 10 | -------------------------------------------------------------------------------- /crates/libs/util/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | /// Return a `&[u8]` for any sized object passed in. 4 | pub unsafe fn any_as_u8_slice(any: &T) -> &[u8] { 5 | let ptr = (any as *const T) as *const u8; 6 | std::slice::from_raw_parts(ptr, std::mem::size_of::()) 7 | } 8 | 9 | pub fn load_hdr_image>(path: P) -> (u32, u32, Vec) { 10 | let img = image::open(path).unwrap(); 11 | let w = img.width(); 12 | let h = img.height(); 13 | let data = img.into_rgba32f().into_raw(); 14 | 15 | (w, h, data) 16 | } 17 | -------------------------------------------------------------------------------- /crates/libs/vulkan/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vulkan" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | 7 | [dependencies] 8 | log.workspace = true 9 | ash.workspace = true 10 | ash-window.workspace = true 11 | raw-window-handle.workspace = true 12 | winit.workspace = true 13 | -------------------------------------------------------------------------------- /crates/libs/vulkan/src/buffer.rs: -------------------------------------------------------------------------------- 1 | use super::{context::*, util::*}; 2 | use ash::vk; 3 | use std::{ 4 | ffi::c_void, 5 | marker::{Send, Sync}, 6 | mem::size_of_val, 7 | sync::Arc, 8 | }; 9 | 10 | /// Wrapper over a raw pointer to make it moveable and accessible from other threads 11 | struct MemoryMapPointer(*mut c_void); 12 | unsafe impl Send for MemoryMapPointer {} 13 | unsafe impl Sync for MemoryMapPointer {} 14 | 15 | pub struct Buffer { 16 | context: Arc, 17 | pub buffer: vk::Buffer, 18 | pub memory: vk::DeviceMemory, 19 | pub size: vk::DeviceSize, 20 | mapped_pointer: Option, 21 | } 22 | 23 | impl Buffer { 24 | fn new( 25 | context: Arc, 26 | buffer: vk::Buffer, 27 | memory: vk::DeviceMemory, 28 | size: vk::DeviceSize, 29 | ) -> Self { 30 | Self { 31 | context, 32 | buffer, 33 | memory, 34 | size, 35 | mapped_pointer: None, 36 | } 37 | } 38 | 39 | /// Create a buffer and allocate its memory. 40 | /// 41 | /// # Returns 42 | /// 43 | /// The buffer, its memory and the actual size in bytes of the 44 | /// allocated memory since in may differ from the requested size. 45 | pub fn create( 46 | context: Arc, 47 | size: vk::DeviceSize, 48 | usage: vk::BufferUsageFlags, 49 | mem_properties: vk::MemoryPropertyFlags, 50 | ) -> Self { 51 | let device = context.device(); 52 | let buffer = { 53 | let buffer_info = vk::BufferCreateInfo::default() 54 | .size(size) 55 | .usage(usage) 56 | .sharing_mode(vk::SharingMode::EXCLUSIVE); 57 | unsafe { 58 | device 59 | .create_buffer(&buffer_info, None) 60 | .expect("Failed to create buffer") 61 | } 62 | }; 63 | 64 | let mem_requirements = unsafe { device.get_buffer_memory_requirements(buffer) }; 65 | let memory = { 66 | let mem_type = find_memory_type( 67 | mem_requirements, 68 | context.get_mem_properties(), 69 | mem_properties, 70 | ); 71 | 72 | let alloc_info = vk::MemoryAllocateInfo::default() 73 | .allocation_size(mem_requirements.size) 74 | .memory_type_index(mem_type); 75 | unsafe { 76 | device 77 | .allocate_memory(&alloc_info, None) 78 | .expect("Failed to allocate memory") 79 | } 80 | }; 81 | 82 | unsafe { 83 | device 84 | .bind_buffer_memory(buffer, memory, 0) 85 | .expect("Failed to bind buffer memory") 86 | }; 87 | 88 | Buffer::new(context, buffer, memory, size) 89 | } 90 | } 91 | 92 | impl Buffer { 93 | /// Register the commands to copy the `size` first bytes of `src` this buffer. 94 | pub fn cmd_copy(&self, command_buffer: vk::CommandBuffer, src: &Buffer, size: vk::DeviceSize) { 95 | let region = vk::BufferCopy { 96 | src_offset: 0, 97 | dst_offset: 0, 98 | size, 99 | }; 100 | let regions = [region]; 101 | 102 | unsafe { 103 | self.context 104 | .device() 105 | .cmd_copy_buffer(command_buffer, src.buffer, self.buffer, ®ions) 106 | }; 107 | } 108 | 109 | /// Map the buffer memory and return the mapped pointer. 110 | /// 111 | /// If the memory is already mapped it just returns the pointer. 112 | pub fn map_memory(&mut self) -> *mut c_void { 113 | if let Some(ptr) = &self.mapped_pointer { 114 | ptr.0 115 | } else { 116 | unsafe { 117 | let ptr = self 118 | .context 119 | .device() 120 | .map_memory(self.memory, 0, self.size, vk::MemoryMapFlags::empty()) 121 | .expect("Failed to map memory"); 122 | self.mapped_pointer = Some(MemoryMapPointer(ptr)); 123 | ptr 124 | } 125 | } 126 | } 127 | 128 | /// Unmap the buffer memory. 129 | /// 130 | /// Does nothing if memory is not mapped. 131 | pub fn unmap_memory(&mut self) { 132 | if self.mapped_pointer.take().is_some() { 133 | unsafe { 134 | self.context.device().unmap_memory(self.memory); 135 | } 136 | } 137 | } 138 | } 139 | 140 | impl Drop for Buffer { 141 | fn drop(&mut self) { 142 | unsafe { 143 | self.unmap_memory(); 144 | self.context.device().destroy_buffer(self.buffer, None); 145 | self.context.device().free_memory(self.memory, None); 146 | } 147 | } 148 | } 149 | 150 | /// Create a buffer and it's gpu memory and fill it. 151 | /// 152 | /// This function internally creates an host visible staging buffer and 153 | /// a device local buffer. The data is first copied from the cpu to the 154 | /// staging buffer. Then we copy the data from the staging buffer to the 155 | /// final buffer using a one-time command buffer. 156 | pub fn create_device_local_buffer_with_data( 157 | context: &Arc, 158 | usage: vk::BufferUsageFlags, 159 | data: &[T], 160 | ) -> Buffer { 161 | let (buffer, _) = context.execute_one_time_commands(|command_buffer| { 162 | cmd_create_device_local_buffer_with_data::(context, command_buffer, usage, data) 163 | }); 164 | buffer 165 | } 166 | 167 | pub fn cmd_create_device_local_buffer_with_data( 168 | context: &Arc, 169 | command_buffer: vk::CommandBuffer, 170 | usage: vk::BufferUsageFlags, 171 | data: &[T], 172 | ) -> (Buffer, Buffer) { 173 | let size = size_of_val(data) as vk::DeviceSize; 174 | let staging_buffer = 175 | create_host_visible_buffer(context, vk::BufferUsageFlags::TRANSFER_SRC, data); 176 | let buffer = Buffer::create( 177 | Arc::clone(context), 178 | size, 179 | vk::BufferUsageFlags::TRANSFER_DST | usage, 180 | vk::MemoryPropertyFlags::DEVICE_LOCAL, 181 | ); 182 | 183 | buffer.cmd_copy(command_buffer, &staging_buffer, staging_buffer.size); 184 | 185 | (buffer, staging_buffer) 186 | } 187 | 188 | pub fn create_host_visible_buffer( 189 | context: &Arc, 190 | usage: vk::BufferUsageFlags, 191 | data: &[T], 192 | ) -> Buffer { 193 | let size = size_of_val(data) as vk::DeviceSize; 194 | let mut buffer = Buffer::create( 195 | Arc::clone(context), 196 | size, 197 | usage, 198 | vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, 199 | ); 200 | 201 | unsafe { 202 | let data_ptr = buffer.map_memory(); 203 | mem_copy(data_ptr, data); 204 | }; 205 | 206 | buffer 207 | } 208 | -------------------------------------------------------------------------------- /crates/libs/vulkan/src/context/mod.rs: -------------------------------------------------------------------------------- 1 | mod shared; 2 | 3 | pub use self::shared::HDR_SURFACE_FORMAT; 4 | 5 | use self::shared::*; 6 | use crate::MsaaSamples; 7 | use ash::{ 8 | khr::{dynamic_rendering, surface, synchronization2}, 9 | vk, Device, Instance, 10 | }; 11 | use std::sync::Arc; 12 | use winit::window::Window; 13 | 14 | pub struct Context { 15 | shared_context: Arc, 16 | general_command_pool: vk::CommandPool, 17 | transient_command_pool: vk::CommandPool, 18 | } 19 | 20 | impl Context { 21 | pub fn new(window: &Window, enable_debug: bool) -> Self { 22 | let shared_context = Arc::new(SharedContext::new(window, enable_debug)); 23 | let general_command_pool = create_command_pool( 24 | shared_context.device(), 25 | shared_context.queue_families_indices, 26 | vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER, 27 | ); 28 | let transient_command_pool = create_command_pool( 29 | shared_context.device(), 30 | shared_context.queue_families_indices, 31 | vk::CommandPoolCreateFlags::TRANSIENT, 32 | ); 33 | 34 | Self { 35 | shared_context, 36 | general_command_pool, 37 | transient_command_pool, 38 | } 39 | } 40 | 41 | pub fn new_thread(&self) -> Self { 42 | let shared_context = Arc::clone(&self.shared_context); 43 | let general_command_pool = create_command_pool( 44 | shared_context.device(), 45 | shared_context.queue_families_indices, 46 | vk::CommandPoolCreateFlags::empty(), 47 | ); 48 | let transient_command_pool = create_command_pool( 49 | shared_context.device(), 50 | shared_context.queue_families_indices, 51 | vk::CommandPoolCreateFlags::TRANSIENT, 52 | ); 53 | 54 | Self { 55 | shared_context, 56 | general_command_pool, 57 | transient_command_pool, 58 | } 59 | } 60 | } 61 | 62 | fn create_command_pool( 63 | device: &Device, 64 | queue_families_indices: QueueFamiliesIndices, 65 | create_flags: vk::CommandPoolCreateFlags, 66 | ) -> vk::CommandPool { 67 | let command_pool_info = vk::CommandPoolCreateInfo::default() 68 | .queue_family_index(queue_families_indices.graphics_index) 69 | .flags(create_flags); 70 | 71 | unsafe { 72 | device 73 | .create_command_pool(&command_pool_info, None) 74 | .expect("Failed to create command pool") 75 | } 76 | } 77 | 78 | impl Context { 79 | pub fn instance(&self) -> &Instance { 80 | self.shared_context.instance() 81 | } 82 | 83 | pub fn surface(&self) -> &surface::Instance { 84 | self.shared_context.surface() 85 | } 86 | 87 | pub fn surface_khr(&self) -> vk::SurfaceKHR { 88 | self.shared_context.surface_khr() 89 | } 90 | 91 | pub fn physical_device(&self) -> vk::PhysicalDevice { 92 | self.shared_context.physical_device() 93 | } 94 | 95 | pub fn device(&self) -> &Device { 96 | self.shared_context.device() 97 | } 98 | 99 | pub fn queue_families_indices(&self) -> QueueFamiliesIndices { 100 | self.shared_context.queue_families_indices() 101 | } 102 | 103 | pub fn graphics_compute_queue(&self) -> vk::Queue { 104 | self.shared_context.graphics_compute_queue() 105 | } 106 | 107 | pub fn present_queue(&self) -> vk::Queue { 108 | self.shared_context.present_queue() 109 | } 110 | 111 | pub fn dynamic_rendering(&self) -> &dynamic_rendering::Device { 112 | self.shared_context.dynamic_rendering() 113 | } 114 | 115 | pub fn synchronization2(&self) -> &synchronization2::Device { 116 | self.shared_context.synchronization2() 117 | } 118 | 119 | pub fn has_hdr_support(&self) -> bool { 120 | self.shared_context.has_hdr_support() 121 | } 122 | 123 | pub fn general_command_pool(&self) -> vk::CommandPool { 124 | self.general_command_pool 125 | } 126 | 127 | pub fn transient_command_pool(&self) -> vk::CommandPool { 128 | self.transient_command_pool 129 | } 130 | } 131 | 132 | impl Context { 133 | pub fn get_mem_properties(&self) -> vk::PhysicalDeviceMemoryProperties { 134 | self.shared_context.get_mem_properties() 135 | } 136 | 137 | /// Find the first compatible format from `candidates`. 138 | pub fn find_supported_format( 139 | &self, 140 | candidates: &[vk::Format], 141 | tiling: vk::ImageTiling, 142 | features: vk::FormatFeatureFlags, 143 | ) -> Option { 144 | self.shared_context 145 | .find_supported_format(candidates, tiling, features) 146 | } 147 | 148 | /// Return the preferred sample count or the maximim supported below preferred. 149 | pub fn get_max_usable_sample_count(&self, preferred: MsaaSamples) -> vk::SampleCountFlags { 150 | self.shared_context.get_max_usable_sample_count(preferred) 151 | } 152 | 153 | pub fn get_ubo_alignment(&self) -> u32 { 154 | self.shared_context.get_ubo_alignment::() 155 | } 156 | 157 | /// Create a one time use command buffer and pass it to `executor`. 158 | pub fn execute_one_time_commands R>( 159 | &self, 160 | executor: F, 161 | ) -> R { 162 | self.shared_context 163 | .execute_one_time_commands(self.transient_command_pool, executor) 164 | } 165 | 166 | pub fn graphics_queue_wait_idle(&self) { 167 | self.shared_context.graphics_queue_wait_idle() 168 | } 169 | } 170 | 171 | impl Drop for Context { 172 | fn drop(&mut self) { 173 | let device = self.shared_context.device(); 174 | unsafe { 175 | device.destroy_command_pool(self.transient_command_pool, None); 176 | device.destroy_command_pool(self.general_command_pool, None); 177 | } 178 | } 179 | } 180 | 181 | /// Find a memory type in `mem_properties` that is suitable 182 | /// for `requirements` and supports `required_properties`. 183 | /// 184 | /// # Returns 185 | /// 186 | /// The index of the memory type from `mem_properties`. 187 | pub fn find_memory_type( 188 | requirements: vk::MemoryRequirements, 189 | mem_properties: vk::PhysicalDeviceMemoryProperties, 190 | required_properties: vk::MemoryPropertyFlags, 191 | ) -> u32 { 192 | for i in 0..mem_properties.memory_type_count { 193 | if requirements.memory_type_bits & (1 << i) != 0 194 | && mem_properties.memory_types[i as usize] 195 | .property_flags 196 | .contains(required_properties) 197 | { 198 | return i; 199 | } 200 | } 201 | panic!("Failed to find suitable memory type.") 202 | } 203 | -------------------------------------------------------------------------------- /crates/libs/vulkan/src/debug.rs: -------------------------------------------------------------------------------- 1 | use ash::{ext::debug_utils, vk, Entry, Instance}; 2 | use std::{ffi::CStr, os::raw::c_void}; 3 | 4 | unsafe extern "system" fn vulkan_debug_callback( 5 | flag: vk::DebugUtilsMessageSeverityFlagsEXT, 6 | typ: vk::DebugUtilsMessageTypeFlagsEXT, 7 | p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT, 8 | _: *mut c_void, 9 | ) -> vk::Bool32 { 10 | use vk::DebugUtilsMessageSeverityFlagsEXT as Flag; 11 | 12 | let message = CStr::from_ptr((*p_callback_data).p_message); 13 | match flag { 14 | Flag::VERBOSE => log::debug!("{:?} - {:?}", typ, message), 15 | Flag::INFO => log::info!("{:?} - {:?}", typ, message), 16 | Flag::WARNING => log::warn!("{:?} - {:?}", typ, message), 17 | _ => log::error!("{:?} - {:?}", typ, message), 18 | } 19 | vk::FALSE 20 | } 21 | 22 | /// Setup the debug message if validation layers are enabled. 23 | pub fn setup_debug_messenger( 24 | entry: &Entry, 25 | instance: &Instance, 26 | ) -> (debug_utils::Instance, vk::DebugUtilsMessengerEXT) { 27 | use vk::DebugUtilsMessageSeverityFlagsEXT as Severity; 28 | use vk::DebugUtilsMessageTypeFlagsEXT as MsgType; 29 | 30 | let create_info = vk::DebugUtilsMessengerCreateInfoEXT::default() 31 | .flags(vk::DebugUtilsMessengerCreateFlagsEXT::empty()) 32 | .message_severity(Severity::VERBOSE | Severity::INFO | Severity::WARNING | Severity::ERROR) 33 | .message_type(MsgType::GENERAL | MsgType::VALIDATION | MsgType::PERFORMANCE) 34 | .pfn_user_callback(Some(vulkan_debug_callback)); 35 | let debug_utils = debug_utils::Instance::new(entry, instance); 36 | let debug_utils_messenger = unsafe { 37 | debug_utils 38 | .create_debug_utils_messenger(&create_info, None) 39 | .expect("Failed to create debug report callback") 40 | }; 41 | (debug_utils, debug_utils_messenger) 42 | } 43 | -------------------------------------------------------------------------------- /crates/libs/vulkan/src/descriptor.rs: -------------------------------------------------------------------------------- 1 | use super::context::Context; 2 | use ash::vk; 3 | use std::sync::Arc; 4 | 5 | pub struct Descriptors { 6 | context: Arc, 7 | layout: vk::DescriptorSetLayout, 8 | pool: vk::DescriptorPool, 9 | sets: Vec, 10 | } 11 | 12 | impl Descriptors { 13 | pub fn new( 14 | context: Arc, 15 | layout: vk::DescriptorSetLayout, 16 | pool: vk::DescriptorPool, 17 | sets: Vec, 18 | ) -> Self { 19 | Self { 20 | context, 21 | layout, 22 | pool, 23 | sets, 24 | } 25 | } 26 | } 27 | 28 | impl Descriptors { 29 | pub fn layout(&self) -> vk::DescriptorSetLayout { 30 | self.layout 31 | } 32 | 33 | pub fn pool(&self) -> vk::DescriptorPool { 34 | self.pool 35 | } 36 | 37 | pub fn sets(&self) -> &[vk::DescriptorSet] { 38 | &self.sets 39 | } 40 | 41 | pub fn set_sets(&mut self, sets: Vec) { 42 | self.sets = sets; 43 | } 44 | } 45 | 46 | impl Drop for Descriptors { 47 | fn drop(&mut self) { 48 | let device = self.context.device(); 49 | unsafe { 50 | device.destroy_descriptor_pool(self.pool, None); 51 | device.destroy_descriptor_set_layout(self.layout, None); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crates/libs/vulkan/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod buffer; 2 | mod context; 3 | mod debug; 4 | mod descriptor; 5 | mod image; 6 | mod msaa; 7 | mod pipeline; 8 | mod shader; 9 | mod swapchain; 10 | mod texture; 11 | mod util; 12 | mod vertex; 13 | 14 | pub use self::{ 15 | buffer::*, context::*, debug::*, descriptor::*, image::*, msaa::*, pipeline::*, shader::*, 16 | swapchain::*, texture::*, util::*, vertex::*, 17 | }; 18 | 19 | pub use ash; 20 | use ash::vk; 21 | use std::sync::Arc; 22 | pub use winit; 23 | 24 | /// Hold a partially loaded resource. 25 | /// 26 | /// The main usecase is to create resource that don't 27 | /// required submitting work through a queue and to bake 28 | /// commands for actions that do required the use of a queue. 29 | /// It means you can pre-load data on a seperate thread and 30 | /// finish loading on the main thread. 31 | /// 32 | /// The struct also holds the temporary data that is required 33 | /// when submitting work to the queue (a staging buffer for example). 34 | /// 35 | /// To finish loading you need to call the [finish] method. This will 36 | /// submit the command buffer through the main queue then free the command 37 | /// buffer. The temporary data is also dropped at this point. 38 | pub struct PreLoadedResource { 39 | context: Arc, 40 | command_buffer: vk::CommandBuffer, 41 | resource: Option, 42 | tmp_data: Option, 43 | } 44 | 45 | impl PreLoadedResource { 46 | pub fn new( 47 | context: Arc, 48 | command_buffer: vk::CommandBuffer, 49 | resource: R, 50 | tmp_data: T, 51 | ) -> Self { 52 | Self { 53 | context, 54 | command_buffer, 55 | resource: Some(resource), 56 | tmp_data: Some(tmp_data), 57 | } 58 | } 59 | } 60 | 61 | impl PreLoadedResource { 62 | /// Finish loading the resource. 63 | /// 64 | /// Submit the command buffer to the main queue and free it afterwards. 65 | /// Temporary data is dropped. 66 | /// 67 | /// # Returns 68 | /// 69 | /// The loaded resource. 70 | pub fn finish(&mut self) -> R { 71 | assert!( 72 | self.resource.is_some(), 73 | "Resource loading was already finished" 74 | ); 75 | 76 | self.execute_commands(); 77 | self.free_command_buffer(); 78 | self.tmp_data.take(); 79 | 80 | self.resource.take().unwrap() 81 | } 82 | 83 | fn execute_commands(&self) { 84 | self.context 85 | .execute_one_time_commands(|primary_command_buffer| unsafe { 86 | let secondary_command_buffer = [self.command_buffer]; 87 | self.context 88 | .device() 89 | .cmd_execute_commands(primary_command_buffer, &secondary_command_buffer); 90 | }); 91 | } 92 | 93 | fn free_command_buffer(&self) { 94 | let secondary_command_buffer = [self.command_buffer]; 95 | unsafe { 96 | self.context.device().free_command_buffers( 97 | self.context.general_command_pool(), 98 | &secondary_command_buffer, 99 | ) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /crates/libs/vulkan/src/msaa.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone, Debug, PartialOrd, PartialEq, Eq, Default)] 2 | pub enum MsaaSamples { 3 | #[default] 4 | S1, 5 | S2, 6 | S4, 7 | S8, 8 | S16, 9 | S32, 10 | S64, 11 | } 12 | -------------------------------------------------------------------------------- /crates/libs/vulkan/src/pipeline.rs: -------------------------------------------------------------------------------- 1 | use super::{Context, ShaderModule, Vertex}; 2 | use ash::vk; 3 | use std::{ffi::CString, sync::Arc}; 4 | 5 | #[derive(Copy, Clone)] 6 | pub struct PipelineParameters<'a> { 7 | pub vertex_shader_params: ShaderParameters<'a>, 8 | pub fragment_shader_params: ShaderParameters<'a>, 9 | pub multisampling_info: &'a vk::PipelineMultisampleStateCreateInfo<'a>, 10 | pub viewport_info: &'a vk::PipelineViewportStateCreateInfo<'a>, 11 | pub rasterizer_info: &'a vk::PipelineRasterizationStateCreateInfo<'a>, 12 | pub dynamic_state_info: Option<&'a vk::PipelineDynamicStateCreateInfo<'a>>, 13 | pub depth_stencil_info: Option<&'a vk::PipelineDepthStencilStateCreateInfo<'a>>, 14 | pub color_blend_attachments: &'a [vk::PipelineColorBlendAttachmentState], 15 | pub color_attachment_formats: &'a [vk::Format], 16 | pub depth_attachment_format: Option, 17 | pub layout: vk::PipelineLayout, 18 | pub parent: Option, 19 | pub allow_derivatives: bool, 20 | } 21 | 22 | pub fn create_pipeline( 23 | context: &Arc, 24 | params: PipelineParameters, 25 | ) -> vk::Pipeline { 26 | let entry_point_name = CString::new("main").unwrap(); 27 | 28 | let (_vertex_shader_module, vertex_shader_state_info) = create_shader_stage_info( 29 | context, 30 | &entry_point_name, 31 | vk::ShaderStageFlags::VERTEX, 32 | params.vertex_shader_params, 33 | ); 34 | 35 | let (_fragment_shader_module, fragment_shader_state_info) = create_shader_stage_info( 36 | context, 37 | &entry_point_name, 38 | vk::ShaderStageFlags::FRAGMENT, 39 | params.fragment_shader_params, 40 | ); 41 | 42 | let shader_states_infos = [vertex_shader_state_info, fragment_shader_state_info]; 43 | 44 | let bindings_descs = V::get_bindings_descriptions(); 45 | let attributes_descs = V::get_attributes_descriptions(); 46 | let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::default() 47 | .vertex_binding_descriptions(&bindings_descs) 48 | .vertex_attribute_descriptions(&attributes_descs); 49 | 50 | let input_assembly_info = vk::PipelineInputAssemblyStateCreateInfo::default() 51 | .topology(vk::PrimitiveTopology::TRIANGLE_LIST) 52 | .primitive_restart_enable(false); 53 | 54 | let color_blending_info = vk::PipelineColorBlendStateCreateInfo::default() 55 | .logic_op_enable(false) 56 | .logic_op(vk::LogicOp::COPY) 57 | .attachments(params.color_blend_attachments) 58 | .blend_constants([0.0, 0.0, 0.0, 0.0]); 59 | 60 | let mut dynamic_rendering = vk::PipelineRenderingCreateInfo::default() 61 | .color_attachment_formats(params.color_attachment_formats) 62 | .depth_attachment_format(params.depth_attachment_format.unwrap_or_default()); 63 | 64 | let mut pipeline_info = vk::GraphicsPipelineCreateInfo::default() 65 | .stages(&shader_states_infos) 66 | .vertex_input_state(&vertex_input_info) 67 | .input_assembly_state(&input_assembly_info) 68 | .viewport_state(params.viewport_info) 69 | .rasterization_state(params.rasterizer_info) 70 | .multisample_state(params.multisampling_info) 71 | .color_blend_state(&color_blending_info) 72 | .layout(params.layout) 73 | .push_next(&mut dynamic_rendering); 74 | 75 | if let Some(depth_stencil_info) = params.depth_stencil_info { 76 | pipeline_info = pipeline_info.depth_stencil_state(depth_stencil_info) 77 | } 78 | 79 | if let Some(dynamic_state_info) = params.dynamic_state_info { 80 | pipeline_info = pipeline_info.dynamic_state(dynamic_state_info); 81 | } 82 | 83 | if let Some(parent) = params.parent { 84 | pipeline_info = pipeline_info.base_pipeline_handle(parent); 85 | } 86 | 87 | if params.allow_derivatives { 88 | pipeline_info = pipeline_info.flags(vk::PipelineCreateFlags::ALLOW_DERIVATIVES); 89 | } 90 | 91 | let pipeline_infos = [pipeline_info]; 92 | 93 | unsafe { 94 | context 95 | .device() 96 | .create_graphics_pipelines(vk::PipelineCache::null(), &pipeline_infos, None) 97 | .expect("Failed to create graphics pipeline")[0] 98 | } 99 | } 100 | 101 | fn create_shader_stage_info<'a>( 102 | context: &Arc, 103 | entry_point_name: &'a CString, 104 | stage: vk::ShaderStageFlags, 105 | params: ShaderParameters<'a>, 106 | ) -> (ShaderModule, vk::PipelineShaderStageCreateInfo<'a>) { 107 | let extension = get_shader_file_extension(stage); 108 | let shader_path = format!("crates/viewer/shaders/{}.{}.spv", params.name, extension); 109 | let module = ShaderModule::new(Arc::clone(context), shader_path); 110 | 111 | let mut stage_info = vk::PipelineShaderStageCreateInfo::default() 112 | .stage(stage) 113 | .module(module.module()) 114 | .name(entry_point_name); 115 | if let Some(specialization) = params.specialization { 116 | stage_info = stage_info.specialization_info(specialization); 117 | } 118 | 119 | (module, stage_info) 120 | } 121 | 122 | fn get_shader_file_extension(stage: vk::ShaderStageFlags) -> &'static str { 123 | match stage { 124 | vk::ShaderStageFlags::VERTEX => "vert", 125 | vk::ShaderStageFlags::FRAGMENT => "frag", 126 | _ => panic!("Unsupported shader stage"), 127 | } 128 | } 129 | 130 | #[derive(Copy, Clone, Debug)] 131 | pub struct ShaderParameters<'a> { 132 | name: &'a str, 133 | specialization: Option<&'a vk::SpecializationInfo<'a>>, 134 | } 135 | 136 | impl<'a> ShaderParameters<'a> { 137 | pub fn new(name: &'a str) -> Self { 138 | Self { 139 | name, 140 | specialization: None, 141 | } 142 | } 143 | 144 | pub fn specialized(name: &'static str, specialization: &'a vk::SpecializationInfo) -> Self { 145 | Self { 146 | name, 147 | specialization: Some(specialization), 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /crates/libs/vulkan/src/shader.rs: -------------------------------------------------------------------------------- 1 | use super::Context; 2 | use ash::{vk, Device}; 3 | use std::{path::Path, sync::Arc}; 4 | 5 | pub struct ShaderModule { 6 | context: Arc, 7 | module: vk::ShaderModule, 8 | } 9 | 10 | impl ShaderModule { 11 | pub fn new>(context: Arc, path: P) -> Self { 12 | let source = read_shader_from_file(path); 13 | let module = create_shader_module(context.device(), &source); 14 | Self { context, module } 15 | } 16 | } 17 | 18 | impl ShaderModule { 19 | pub fn module(&self) -> vk::ShaderModule { 20 | self.module 21 | } 22 | } 23 | 24 | impl Drop for ShaderModule { 25 | fn drop(&mut self) { 26 | let device = self.context.device(); 27 | unsafe { device.destroy_shader_module(self.module, None) }; 28 | } 29 | } 30 | 31 | fn read_shader_from_file>(path: P) -> Vec { 32 | log::debug!("Loading shader file {}", path.as_ref().to_str().unwrap()); 33 | let mut file = std::fs::File::open(path).expect("Failed to open shader file"); 34 | ash::util::read_spv(&mut file).expect("Failed to read shader source") 35 | } 36 | 37 | fn create_shader_module(device: &Device, code: &[u32]) -> vk::ShaderModule { 38 | let create_info = vk::ShaderModuleCreateInfo::default().code(code); 39 | unsafe { 40 | device 41 | .create_shader_module(&create_info, None) 42 | .expect("Failed to create shader module") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /crates/libs/vulkan/src/util.rs: -------------------------------------------------------------------------------- 1 | use ash::{util::Align, vk::DeviceSize}; 2 | use std::{ffi::c_void, mem::size_of}; 3 | 4 | /// Utility function that copy the content of a slice at the position of a given pointer. 5 | pub unsafe fn mem_copy(ptr: *mut c_void, data: &[T]) { 6 | let elem_size = size_of::() as DeviceSize; 7 | let size = data.len() as DeviceSize * elem_size; 8 | let mut align = Align::new(ptr, elem_size, size); 9 | align.copy_from_slice(data); 10 | } 11 | 12 | /// Utility function that copy the content of a slice at the position of a given pointer and pad elements to respect the requested alignment. 13 | pub unsafe fn mem_copy_aligned(ptr: *mut c_void, alignment: DeviceSize, data: &[T]) { 14 | let size = data.len() as DeviceSize * alignment; 15 | let mut align = Align::new(ptr, alignment, size); 16 | align.copy_from_slice(data); 17 | } 18 | -------------------------------------------------------------------------------- /crates/libs/vulkan/src/vertex.rs: -------------------------------------------------------------------------------- 1 | use ash::vk::{VertexInputAttributeDescription, VertexInputBindingDescription}; 2 | 3 | pub trait Vertex { 4 | fn get_bindings_descriptions() -> Vec; 5 | fn get_attributes_descriptions() -> Vec; 6 | } 7 | 8 | impl Vertex for () { 9 | fn get_bindings_descriptions() -> Vec { 10 | vec![] 11 | } 12 | 13 | fn get_attributes_descriptions() -> Vec { 14 | vec![] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/viewer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gltf-viewer-rs" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | build = "build.rs" 7 | 8 | [dependencies] 9 | vulkan.workspace = true 10 | model.workspace = true 11 | math.workspace = true 12 | util.workspace = true 13 | environment.workspace = true 14 | 15 | log.workspace = true 16 | env_logger.workspace = true 17 | serde.workspace = true 18 | serde_yaml.workspace = true 19 | clap.workspace = true 20 | egui.workspace = true 21 | egui-winit.workspace = true 22 | egui-ash-renderer.workspace = true 23 | -------------------------------------------------------------------------------- /crates/viewer/build.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env::var, 3 | fs, 4 | io::Result, 5 | path::{Path, PathBuf}, 6 | process::{Command, Output}, 7 | }; 8 | 9 | fn main() { 10 | if !should_skip_shader_compilation() { 11 | compile_shaders(); 12 | } 13 | } 14 | 15 | fn should_skip_shader_compilation() -> bool { 16 | var("SKIP_SHADER_COMPILATION") 17 | .map(|var| var.parse::().unwrap_or(false)) 18 | .unwrap_or(false) 19 | } 20 | 21 | fn compile_shaders() { 22 | println!("Compiling shaders"); 23 | 24 | let shader_dir_path = get_shader_source_dir_path(); 25 | 26 | fs::read_dir(shader_dir_path.clone()) 27 | .unwrap() 28 | .map(Result::unwrap) 29 | .filter(|dir| dir.file_type().unwrap().is_file()) 30 | .filter(|dir| dir.path().extension().is_some()) 31 | .filter(|dir| { 32 | !matches!( 33 | dir.path().extension().unwrap().to_str(), 34 | Some("spv") | Some("h") 35 | ) 36 | }) 37 | .for_each(|dir| { 38 | let path = dir.path(); 39 | let name = path.file_name().unwrap().to_str().unwrap(); 40 | let output_name = format!("{}.spv", &name); 41 | println!("Found file {:?}.\nCompiling...", path.as_os_str()); 42 | 43 | let result = Command::new("glslangValidator") 44 | .current_dir(&shader_dir_path) 45 | .arg("-V") 46 | .arg(&path) 47 | .arg("-o") 48 | .arg(output_name) 49 | .output(); 50 | 51 | handle_program_result(result); 52 | }) 53 | } 54 | 55 | fn get_shader_source_dir_path() -> PathBuf { 56 | var("SHADERS_DIR") 57 | .map(PathBuf::from) 58 | .unwrap_or_else(|_| get_default_shader_source_dir_path()) 59 | } 60 | 61 | fn get_default_shader_source_dir_path() -> PathBuf { 62 | let path = get_root_path().join("shaders"); 63 | println!("Shader source directory: {:?}", path.as_os_str()); 64 | path 65 | } 66 | 67 | fn get_root_path() -> &'static Path { 68 | Path::new(env!("CARGO_MANIFEST_DIR")) 69 | } 70 | 71 | fn handle_program_result(result: Result) { 72 | match result { 73 | Ok(output) => { 74 | if output.status.success() { 75 | println!("Shader compilation succedeed."); 76 | print!( 77 | "stdout: {}", 78 | String::from_utf8(output.stdout) 79 | .unwrap_or_else(|_| "Failed to print program stdout".to_string()) 80 | ); 81 | } else { 82 | eprintln!("Shader compilation failed. Status: {}", output.status); 83 | eprint!( 84 | "stdout: {}", 85 | String::from_utf8(output.stdout) 86 | .unwrap_or_else(|_| "Failed to print program stdout".to_string()) 87 | ); 88 | eprint!( 89 | "stderr: {}", 90 | String::from_utf8(output.stderr) 91 | .unwrap_or_else(|_| "Failed to print program stderr".to_string()) 92 | ); 93 | panic!("Shader compilation failed. Status: {}", output.status); 94 | } 95 | } 96 | Err(error) => { 97 | panic!("Failed to compile shader. Cause: {}", error); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /crates/viewer/shaders/blur.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout(location = 0) in vec2 oCoords; 4 | 5 | layout(binding = 0) uniform sampler2D inputImage; 6 | 7 | layout(location = 0) out vec4 finalColor; 8 | 9 | void main() { 10 | const int blurRange = 2; 11 | int n = 0; 12 | vec2 texelSize = 1.0 / vec2(textureSize(inputImage, 0)); 13 | float result = 0.0; 14 | for (int x = -blurRange; x < blurRange; x++) { 15 | for (int y = -blurRange; y < blurRange; y++) { 16 | vec2 offset = vec2(float(x), float(y)) * texelSize; 17 | result += texture(inputImage, oCoords + offset).r; 18 | n++; 19 | } 20 | } 21 | finalColor = vec4(vec3(result / float(n)), 1.0); 22 | } 23 | -------------------------------------------------------------------------------- /crates/viewer/shaders/blur.frag.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/crates/viewer/shaders/blur.frag.spv -------------------------------------------------------------------------------- /crates/viewer/shaders/brdf_lookup.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | const float PI = 3.14159; 4 | // TODO: use specialization 5 | const uint NUM_SAMPLES = 1024u; 6 | 7 | layout(location = 0) in vec2 oCoords; 8 | 9 | layout(location = 0) out vec4 outColor; 10 | 11 | // Based omn http://byteblacksmith.com/improvements-to-the-canonical-one-liner-glsl-rand-for-opengl-es-2-0/ 12 | float random(vec2 co) { 13 | float a = 12.9898; 14 | float b = 78.233; 15 | float c = 43758.5453; 16 | float dt= dot(co.xy ,vec2(a,b)); 17 | float sn= mod(dt,3.14); 18 | return fract(sin(sn) * c); 19 | } 20 | 21 | vec2 hammersley2d(uint i, uint N) { 22 | // Radical inverse based on http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html 23 | uint bits = (i << 16u) | (i >> 16u); 24 | bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); 25 | bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); 26 | bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); 27 | bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); 28 | float rdi = float(bits) * 2.3283064365386963e-10; 29 | return vec2(float(i) /float(N), rdi); 30 | } 31 | 32 | // Based on http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_slides.pdf 33 | vec3 importanceSample_GGX(vec2 Xi, float roughness, vec3 normal) { 34 | // Maps a 2D point to a hemisphere with spread based on roughness 35 | float alpha = roughness * roughness; 36 | float phi = 2.0 * PI * Xi.x + random(normal.xz) * 0.1; 37 | float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (alpha*alpha - 1.0) * Xi.y)); 38 | float sinTheta = sqrt(1.0 - cosTheta * cosTheta); 39 | vec3 H = vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta); 40 | 41 | // Tangent space 42 | vec3 up = abs(normal.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); 43 | vec3 tangentX = normalize(cross(up, normal)); 44 | vec3 tangentY = normalize(cross(normal, tangentX)); 45 | 46 | // Convert to world Space 47 | return normalize(tangentX * H.x + tangentY * H.y + normal * H.z); 48 | } 49 | 50 | // Geometric Shadowing function 51 | float G_SchlicksmithGGX(float dotNL, float dotNV, float roughness) { 52 | float k = (roughness * roughness) / 2.0; 53 | float GL = dotNL / (dotNL * (1.0 - k) + k); 54 | float GV = dotNV / (dotNV * (1.0 - k) + k); 55 | return GL * GV; 56 | } 57 | 58 | vec2 BRDF(float NoV, float roughness) { 59 | // Normal always points along z-axis for the 2D lookup 60 | const vec3 N = vec3(0.0, 0.0, 1.0); 61 | vec3 V = vec3(sqrt(1.0 - NoV*NoV), 0.0, NoV); 62 | 63 | vec2 LUT = vec2(0.0); 64 | for(uint i = 0u; i < NUM_SAMPLES; i++) { 65 | vec2 Xi = hammersley2d(i, NUM_SAMPLES); 66 | vec3 H = importanceSample_GGX(Xi, roughness, N); 67 | vec3 L = 2.0 * dot(V, H) * H - V; 68 | 69 | float dotNL = max(dot(N, L), 0.0); 70 | float dotNV = max(dot(N, V), 0.0); 71 | float dotVH = max(dot(V, H), 0.0); 72 | float dotNH = max(dot(H, N), 0.0); 73 | 74 | if (dotNL > 0.0) { 75 | float G = G_SchlicksmithGGX(dotNL, dotNV, roughness); 76 | float G_Vis = (G * dotVH) / (dotNH * dotNV); 77 | float Fc = pow(1.0 - dotVH, 5.0); 78 | LUT += vec2((1.0 - Fc) * G_Vis, Fc * G_Vis); 79 | } 80 | } 81 | return LUT / float(NUM_SAMPLES); 82 | } 83 | 84 | void main() { 85 | outColor = vec4(BRDF(oCoords.s, 1.0 - oCoords.t), 0.0, 1.0); 86 | } -------------------------------------------------------------------------------- /crates/viewer/shaders/brdf_lookup.frag.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/crates/viewer/shaders/brdf_lookup.frag.spv -------------------------------------------------------------------------------- /crates/viewer/shaders/cubemap.vert: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout(location = 0) in vec3 vPositions; 4 | 5 | layout(push_constant) uniform Camera { 6 | layout(offset = 0) mat4 viewProj; 7 | } camera; 8 | 9 | 10 | layout(location = 0) out vec3 oPositions; 11 | 12 | void main() { 13 | oPositions = vPositions; 14 | gl_Position = camera.viewProj * vec4(vPositions, 1.0); 15 | // TODO: why do I need to do that ? (and cull the front face) 16 | gl_Position.x *= -1; 17 | } 18 | -------------------------------------------------------------------------------- /crates/viewer/shaders/cubemap.vert.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/crates/viewer/shaders/cubemap.vert.spv -------------------------------------------------------------------------------- /crates/viewer/shaders/downsample.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout(location = 0) in vec2 texCoord; 4 | 5 | layout(binding = 0) uniform sampler2D srcTexture; 6 | 7 | layout(push_constant) uniform Constants { 8 | vec2 srcResolution; 9 | } c; 10 | 11 | layout(location = 0) out vec3 downsample; 12 | 13 | // Shader taken from https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom 14 | void main() { 15 | 16 | vec2 srcTexelSize = 1.0 / c.srcResolution; 17 | float x = srcTexelSize.x; 18 | float y = srcTexelSize.y; 19 | 20 | // Take 13 samples around current texel: 21 | // a - b - c 22 | // - j - k - 23 | // d - e - f 24 | // - l - m - 25 | // g - h - i 26 | // === ('e' is the current texel) === 27 | vec3 a = texture(srcTexture, vec2(texCoord.x - 2*x, texCoord.y + 2*y)).rgb; 28 | vec3 b = texture(srcTexture, vec2(texCoord.x, texCoord.y + 2*y)).rgb; 29 | vec3 c = texture(srcTexture, vec2(texCoord.x + 2*x, texCoord.y + 2*y)).rgb; 30 | 31 | vec3 d = texture(srcTexture, vec2(texCoord.x - 2*x, texCoord.y)).rgb; 32 | vec3 e = texture(srcTexture, vec2(texCoord.x, texCoord.y)).rgb; 33 | vec3 f = texture(srcTexture, vec2(texCoord.x + 2*x, texCoord.y)).rgb; 34 | 35 | vec3 g = texture(srcTexture, vec2(texCoord.x - 2*x, texCoord.y - 2*y)).rgb; 36 | vec3 h = texture(srcTexture, vec2(texCoord.x, texCoord.y - 2*y)).rgb; 37 | vec3 i = texture(srcTexture, vec2(texCoord.x + 2*x, texCoord.y - 2*y)).rgb; 38 | 39 | vec3 j = texture(srcTexture, vec2(texCoord.x - x, texCoord.y + y)).rgb; 40 | vec3 k = texture(srcTexture, vec2(texCoord.x + x, texCoord.y + y)).rgb; 41 | vec3 l = texture(srcTexture, vec2(texCoord.x - x, texCoord.y - y)).rgb; 42 | vec3 m = texture(srcTexture, vec2(texCoord.x + x, texCoord.y - y)).rgb; 43 | 44 | // Apply weighted distribution: 45 | // 0.5 + 0.125 + 0.125 + 0.125 + 0.125 = 1 46 | // a,b,d,e * 0.125 47 | // b,c,e,f * 0.125 48 | // d,e,g,h * 0.125 49 | // e,f,h,i * 0.125 50 | // j,k,l,m * 0.5 51 | // This shows 5 square areas that are being sampled. But some of them overlap, 52 | // so to have an energy preserving downsample we need to make some adjustments. 53 | // The weights are the distributed, so that the sum of j,k,l,m (e.g.) 54 | // contribute 0.5 to the final color output. The code below is written 55 | // to effectively yield this sum. We get: 56 | // 0.125*5 + 0.03125*4 + 0.0625*4 = 1 57 | downsample = e*0.125; 58 | downsample += (a+c+g+i)*0.03125; 59 | downsample += (b+d+f+h)*0.0625; 60 | downsample += (j+k+l+m)*0.125; 61 | } 62 | -------------------------------------------------------------------------------- /crates/viewer/shaders/downsample.frag.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/crates/viewer/shaders/downsample.frag.spv -------------------------------------------------------------------------------- /crates/viewer/shaders/final.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout(location = 0) in vec2 oCoords; 4 | 5 | layout(binding = 0) uniform sampler2D inputImage; 6 | layout(binding = 1) uniform sampler2D bloomImage; 7 | 8 | layout(location = 0) out vec4 finalColor; 9 | 10 | layout(constant_id = 0) const uint TONE_MAP_MODE = 0; 11 | const uint TONE_MAP_MODE_DEFAULT = 0; 12 | const uint TONE_MAP_MODE_UNCHARTED = 1; 13 | const uint TONE_MAP_MODE_HEJL_RICHARD = 2; 14 | const uint TONE_MAP_MODE_ACES = 3; 15 | const uint TONE_MAP_MODE_ACESREC2020 = 4; 16 | 17 | layout(push_constant) uniform Constants { 18 | float bloomStrength; 19 | } c; 20 | 21 | const float GAMMA = 2.2; 22 | 23 | vec3 SRGBtoLINEAR(vec3 color) { 24 | return pow(color, vec3(GAMMA)); 25 | } 26 | 27 | // Uncharted 2 tone map 28 | // see: http://filmicworlds.com/blog/filmic-tonemapping-operators/ 29 | vec3 toneMapUncharted2Impl(vec3 color) { 30 | const float A = 0.15; 31 | const float B = 0.50; 32 | const float C = 0.10; 33 | const float D = 0.20; 34 | const float E = 0.02; 35 | const float F = 0.30; 36 | return ((color*(A*color+C*B)+D*E)/(color*(A*color+B)+D*F))-E/F; 37 | } 38 | 39 | vec3 toneMapUncharted(vec3 color) { 40 | const float W = 11.2; 41 | color = toneMapUncharted2Impl(color * 2.0); 42 | vec3 whiteScale = 1.0 / toneMapUncharted2Impl(vec3(W)); 43 | return color * whiteScale; 44 | } 45 | 46 | // Hejl Richard tone map 47 | // see: http://filmicworlds.com/blog/filmic-tonemapping-operators/ 48 | vec3 toneMapHejlRichard(vec3 color) { 49 | color = max(vec3(0.0), color - vec3(0.004)); 50 | return SRGBtoLINEAR((color*(6.2*color+.5))/(color*(6.2*color+1.7)+0.06)); 51 | } 52 | 53 | // ACES tone map 54 | // see: https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/ 55 | vec3 toneMapACES(vec3 color) { 56 | const float A = 2.51; 57 | const float B = 0.03; 58 | const float C = 2.43; 59 | const float D = 0.59; 60 | const float E = 0.14; 61 | return clamp((color * (A * color + B)) / (color * (C * color + D) + E), 0.0, 1.0); 62 | } 63 | 64 | // ACES approximation for HDR 65 | // https://knarkowicz.wordpress.com/2016/08/31/hdr-display-first-steps/ 66 | vec3 toneMapACESRec2020(vec3 x) { 67 | float a = 15.8f; 68 | float b = 2.12f; 69 | float c = 1.2f; 70 | float d = 5.92f; 71 | float e = 1.9f; 72 | return ( x * ( a * x + b ) ) / ( x * ( c * x + d ) + e ); 73 | } 74 | 75 | vec3 defaultToneMap(vec3 color) { 76 | color = color/(color + 1.0); 77 | return color; 78 | } 79 | 80 | void main() { 81 | vec3 color = texture(inputImage, oCoords).rgb; 82 | vec3 bloom = texture(bloomImage, oCoords).rgb; 83 | vec3 bloomed = mix(color, bloom, c.bloomStrength); 84 | 85 | if (TONE_MAP_MODE == TONE_MAP_MODE_DEFAULT) { 86 | color = defaultToneMap(bloomed); 87 | } else if (TONE_MAP_MODE == TONE_MAP_MODE_UNCHARTED) { 88 | color = toneMapUncharted(bloomed); 89 | } else if (TONE_MAP_MODE == TONE_MAP_MODE_HEJL_RICHARD) { 90 | color = toneMapHejlRichard(bloomed); 91 | } else if (TONE_MAP_MODE == TONE_MAP_MODE_ACES) { 92 | color = toneMapACES(bloomed); 93 | } else if(TONE_MAP_MODE == TONE_MAP_MODE_ACESREC2020) { 94 | color = toneMapACESRec2020(bloomed); 95 | } else { 96 | color = bloomed; 97 | } 98 | 99 | finalColor = vec4(color, 1.0); 100 | } 101 | -------------------------------------------------------------------------------- /crates/viewer/shaders/final.frag.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/crates/viewer/shaders/final.frag.spv -------------------------------------------------------------------------------- /crates/viewer/shaders/fullscreen.vert: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout(location = 0) in vec2 vPos; 4 | layout(location = 1) in vec2 vCoords; 5 | 6 | layout(location = 0) out vec2 oCoords; 7 | 8 | void main() { 9 | oCoords = vCoords; 10 | gl_Position = vec4(vPos.x, vPos.y, 0.0, 1.0); 11 | } 12 | -------------------------------------------------------------------------------- /crates/viewer/shaders/fullscreen.vert.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/crates/viewer/shaders/fullscreen.vert.spv -------------------------------------------------------------------------------- /crates/viewer/shaders/gbuffer.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | const uint NO_TEXTURE_ID = 255; 4 | const uint ALPHA_MODE_MASK = 1; 5 | const float ALPHA_CUTOFF_BIAS = 0.0000001; 6 | 7 | // -- Inputs -- 8 | layout(location = 0) in vec3 oViewSpaceNormal; 9 | layout(location = 1) in vec2 oTexcoords0; 10 | layout(location = 2) in vec2 oTexcoords1; 11 | layout(location = 3) in float oAlpha; 12 | 13 | // -- Push constants 14 | layout(push_constant) uniform MaterialUniform { 15 | float alpha; 16 | uint colorTextureChannel; 17 | uint alphaMode; 18 | float alphaCutoff; 19 | } material; 20 | 21 | // -- Samplers -- 22 | layout(binding = 3, set = 1) uniform sampler2D colorSampler; 23 | 24 | // -- Output -- 25 | layout(location = 0) out vec4 outNormals; 26 | 27 | vec2 getUV(uint texChannel) { 28 | if (texChannel == 0) { 29 | return oTexcoords0; 30 | } 31 | return oTexcoords1; 32 | } 33 | 34 | float getAlpha(uint textureChannel) { 35 | float alpha = material.alpha; 36 | if(textureChannel != NO_TEXTURE_ID) { 37 | vec2 uv = getUV(textureChannel); 38 | float sampledAlpha = texture(colorSampler, uv).a; 39 | alpha *= sampledAlpha; 40 | } 41 | return alpha * oAlpha; 42 | } 43 | 44 | bool isMasked(float alpha) { 45 | return material.alphaMode == ALPHA_MODE_MASK && alpha + ALPHA_CUTOFF_BIAS < material.alphaCutoff; 46 | } 47 | 48 | void main() { 49 | float alpha = getAlpha(material.colorTextureChannel); 50 | if (isMasked(alpha)) { 51 | discard; 52 | } 53 | 54 | outNormals = vec4(normalize(oViewSpaceNormal), 0.0); 55 | } 56 | -------------------------------------------------------------------------------- /crates/viewer/shaders/gbuffer.frag.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/crates/viewer/shaders/gbuffer.frag.spv -------------------------------------------------------------------------------- /crates/viewer/shaders/gbuffer.vert: -------------------------------------------------------------------------------- 1 | #version 450 2 | #extension GL_GOOGLE_include_directive : require 3 | 4 | #include "libs/camera.glsl" 5 | 6 | layout(location = 0) in vec3 vPositions; 7 | layout(location = 1) in vec3 vNormals; 8 | layout(location = 2) in vec2 vTexcoords0; 9 | layout(location = 3) in vec2 vTexcoords1; 10 | layout(location = 4) in vec4 vTangents; 11 | layout(location = 5) in vec4 vWeights; 12 | layout(location = 6) in uvec4 vJoints; 13 | layout(location = 7) in vec4 vColors; 14 | 15 | layout(binding = 0, set = 0) uniform Frame { 16 | Camera camera; 17 | }; 18 | 19 | layout(binding = 1, set = 0) uniform TransformUBO { 20 | mat4 matrix; 21 | } transform; 22 | 23 | layout(binding = 2, set = 0) uniform SkinUBO { 24 | mat4 jointMatrices[512]; 25 | } skin; 26 | 27 | layout(location = 0) out vec3 oViewSpaceNormal; 28 | layout(location = 1) out vec2 oTexcoords0; 29 | layout(location = 2) out vec2 oTexcoords1; 30 | layout(location = 3) out float oAlpha; 31 | 32 | void main() { 33 | mat4 world = transform.matrix; 34 | if (vWeights != vec4(0.0)) { 35 | world *= vWeights.x * skin.jointMatrices[vJoints.x] 36 | + vWeights.y * skin.jointMatrices[vJoints.y] 37 | + vWeights.z * skin.jointMatrices[vJoints.z] 38 | + vWeights.w * skin.jointMatrices[vJoints.w]; 39 | } 40 | 41 | oViewSpaceNormal = normalize((camera.view * world * vec4(vNormals, 0.0)).xyz); 42 | oTexcoords0 = vTexcoords0; 43 | oTexcoords1 = vTexcoords1; 44 | oAlpha = vColors.a; 45 | 46 | gl_Position = camera.proj * camera.view * world * vec4(vPositions, 1.0); 47 | } 48 | -------------------------------------------------------------------------------- /crates/viewer/shaders/gbuffer.vert.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/crates/viewer/shaders/gbuffer.vert.spv -------------------------------------------------------------------------------- /crates/viewer/shaders/irradiance.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | const float PI = 3.14159; 4 | 5 | layout(location = 0) in vec3 oPositions; 6 | 7 | layout(binding = 0) uniform samplerCube cubemapSampler; 8 | 9 | layout(location = 0) out vec4 outColor; 10 | 11 | void main() { 12 | vec3 irradiance = vec3(0.0); 13 | 14 | vec3 normal = normalize(oPositions); 15 | vec3 up = vec3(0.0, 1.0, 0.0); 16 | vec3 right = cross(normal, up); 17 | up = cross(normal, right); 18 | 19 | float step = 0.025; 20 | int sampleCount = 0; 21 | 22 | for(float phi = 0.0; phi < 2.0 * PI; phi += step) { 23 | for(float theta = 0.0; theta < 0.5 * PI; theta += step) { 24 | // spherical coordinates (phi, theta) to cartesian (in tangent space) 25 | vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta)); 26 | // tangent space to world 27 | vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * normal; 28 | 29 | irradiance += texture(cubemapSampler, sampleVec).rgb * cos(theta) * sin(theta); 30 | sampleCount++; 31 | } 32 | } 33 | irradiance = PI * irradiance * (1.0/ float(sampleCount)); 34 | outColor = vec4(irradiance, 1.0); 35 | } 36 | -------------------------------------------------------------------------------- /crates/viewer/shaders/irradiance.frag.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/crates/viewer/shaders/irradiance.frag.spv -------------------------------------------------------------------------------- /crates/viewer/shaders/libs/camera.glsl: -------------------------------------------------------------------------------- 1 | struct Camera { 2 | mat4 view; 3 | mat4 proj; 4 | mat4 invertedProj; 5 | vec4 eye; 6 | float zNear; 7 | float zFar; 8 | }; 9 | -------------------------------------------------------------------------------- /crates/viewer/shaders/libs/material.glsl: -------------------------------------------------------------------------------- 1 | const uint NO_TEXTURE_ID = 255; 2 | 3 | const uint ALPHA_MODE_MASK = 1; 4 | const uint ALPHA_MODE_BLEND = 2; 5 | const float ALPHA_CUTOFF_BIAS = 0.0000001; 6 | 7 | const uint METALLIC_ROUGHNESS_WORKFLOW = 0; 8 | 9 | struct Material { 10 | vec4 color; 11 | vec3 emissiveFactor; 12 | // - roughness for metallic/roughness workflows 13 | // - glossiness for specular/glossiness workflows 14 | float roughnessGlossiness; 15 | // Contains the metallic (or specular) factor. 16 | // - metallic: r (for metallic/roughness workflows) 17 | // - specular: rgb (for specular/glossiness workflows) 18 | vec3 metallicSpecular; 19 | float occlusion; 20 | float alphaCutoff; 21 | float clearcoatFactor; 22 | float clearcoatRoughness; 23 | uint colorTextureChannel; 24 | uint materialTextureChannel; 25 | uint emissiveTextureChannel; 26 | uint normalsTextureChannel; 27 | uint occlusionTextureChannel; 28 | uint clearcoatFactorTextureChannel; 29 | uint clearcoatRoughnessTextureChannel; 30 | uint clearcoatNormalsTextureChannel; 31 | uint alphaMode; 32 | bool isUnlit; 33 | uint workflow; 34 | float ior; 35 | mat4 colorTextureTransform; 36 | mat4 materialTextureTransform; 37 | mat4 emissiveTextureTransform; 38 | mat4 normalsTextureTransform; 39 | mat4 occlusionTextureTransform; 40 | mat4 clearcoatFactorTextureTransform; 41 | mat4 clearcoatRoughnessTextureTransform; 42 | mat4 clearcoatNormalsTextureTransform; 43 | }; 44 | -------------------------------------------------------------------------------- /crates/viewer/shaders/model.frag.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/crates/viewer/shaders/model.frag.spv -------------------------------------------------------------------------------- /crates/viewer/shaders/model.vert: -------------------------------------------------------------------------------- 1 | #version 450 2 | #extension GL_GOOGLE_include_directive : require 3 | 4 | #include "libs/camera.glsl" 5 | 6 | layout(location = 0) in vec3 vPositions; 7 | layout(location = 1) in vec3 vNormals; 8 | layout(location = 2) in vec2 vTexcoords0; 9 | layout(location = 3) in vec2 vTexcoords1; 10 | layout(location = 4) in vec4 vTangents; 11 | layout(location = 5) in vec4 vWeights; 12 | layout(location = 6) in uvec4 vJoints; 13 | layout(location = 7) in vec4 vColors; 14 | 15 | layout(binding = 0, set = 0) uniform Frame { 16 | Camera camera; 17 | }; 18 | 19 | layout(binding = 3, set = 0) uniform TransformUBO { 20 | mat4 matrix; 21 | } transform; 22 | 23 | layout(binding = 4, set = 0) uniform SkinUBO { 24 | mat4 jointMatrices[512]; 25 | } skin; 26 | 27 | layout(location = 0) out vec3 oNormals; 28 | layout(location = 1) out vec2 oTexcoords0; 29 | layout(location = 2) out vec2 oTexcoords1; 30 | layout(location = 3) out vec3 oPositions; 31 | layout(location = 4) out vec4 oColors; 32 | layout(location = 5) out mat3 oTBN; 33 | 34 | void main() { 35 | mat4 world = transform.matrix; 36 | if (vWeights != vec4(0.0)) { 37 | world *= vWeights.x * skin.jointMatrices[vJoints.x] 38 | + vWeights.y * skin.jointMatrices[vJoints.y] 39 | + vWeights.z * skin.jointMatrices[vJoints.z] 40 | + vWeights.w * skin.jointMatrices[vJoints.w]; 41 | } 42 | 43 | vec3 normal = normalize((world * vec4(vNormals, 0.0)).xyz); 44 | vec3 tangent = normalize((world * vec4(vTangents.xyz, 0.0)).xyz); 45 | tangent = normalize(tangent - dot(tangent, normal)*normal); 46 | vec3 bitangent = cross(normal, tangent) * vTangents.w; 47 | 48 | oNormals = normal; 49 | oTexcoords0 = vTexcoords0; 50 | oTexcoords1 = vTexcoords1; 51 | oPositions = (world * vec4(vPositions, 1.0)).xyz; 52 | oTBN = mat3(tangent, bitangent, normal); 53 | oColors = vColors; 54 | gl_Position = camera.proj * camera.view * world * vec4(vPositions, 1.0); 55 | } 56 | -------------------------------------------------------------------------------- /crates/viewer/shaders/model.vert.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/crates/viewer/shaders/model.vert.spv -------------------------------------------------------------------------------- /crates/viewer/shaders/pre_filtered.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | const float PI = 3.14159; 4 | 5 | layout(location = 0) in vec3 oPositions; 6 | 7 | layout(binding = 0) uniform samplerCube cubemapSampler; 8 | 9 | layout(push_constant) uniform Roughness { 10 | layout(offset = 64) float value; 11 | } roughness; 12 | 13 | layout(location = 0) out vec4 outColor; 14 | 15 | // Based omn http://byteblacksmith.com/improvements-to-the-canonical-one-liner-glsl-rand-for-opengl-es-2-0/ 16 | float random(vec2 co) { 17 | float a = 12.9898; 18 | float b = 78.233; 19 | float c = 43758.5453; 20 | float dt= dot(co.xy ,vec2(a,b)); 21 | float sn= mod(dt,3.14); 22 | return fract(sin(sn) * c); 23 | } 24 | 25 | vec2 hammersley2d(uint i, uint N) { 26 | // Radical inverse based on http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html 27 | uint bits = (i << 16u) | (i >> 16u); 28 | bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); 29 | bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); 30 | bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); 31 | bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); 32 | float rdi = float(bits) * 2.3283064365386963e-10; 33 | return vec2(float(i) /float(N), rdi); 34 | } 35 | 36 | // Based on http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_slides.pdf 37 | vec3 importanceSample_GGX(vec2 Xi, float roughness, vec3 normal) { 38 | // Maps a 2D point to a hemisphere with spread based on roughness 39 | float alpha = roughness * roughness; 40 | float phi = 2.0 * PI * Xi.x + random(normal.xz) * 0.1; 41 | float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (alpha*alpha - 1.0) * Xi.y)); 42 | float sinTheta = sqrt(1.0 - cosTheta * cosTheta); 43 | vec3 H = vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta); 44 | 45 | // Tangent space 46 | vec3 up = abs(normal.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); 47 | vec3 tangentX = normalize(cross(up, normal)); 48 | vec3 tangentY = normalize(cross(normal, tangentX)); 49 | 50 | // Convert to world Space 51 | return normalize(tangentX * H.x + tangentY * H.y + normal * H.z); 52 | } 53 | 54 | // Normal Distribution function 55 | float D_GGX(float dotNH, float roughness) { 56 | float alpha = roughness * roughness; 57 | float alpha2 = alpha * alpha; 58 | float denom = dotNH * dotNH * (alpha2 - 1.0) + 1.0; 59 | return (alpha2)/(PI * denom*denom); 60 | } 61 | 62 | vec3 prefilterEnvMap(vec3 R, float roughness) { 63 | vec3 N = R; 64 | vec3 V = R; 65 | vec3 color = vec3(0.0); 66 | float totalWeight = 0.0; 67 | float envMapDim = float(textureSize(cubemapSampler, 0).s); 68 | for(uint i = 0u; i < 32; i++) { 69 | vec2 Xi = hammersley2d(i, 32); 70 | vec3 H = importanceSample_GGX(Xi, roughness, N); 71 | vec3 L = 2.0 * dot(V, H) * H - V; 72 | float dotNL = clamp(dot(N, L), 0.0, 1.0); 73 | if(dotNL > 0.0) { 74 | // Filtering based on https://placeholderart.wordpress.com/2015/07/28/implementation-notes-runtime-environment-map-filtering-for-image-based-lighting/ 75 | 76 | float dotNH = clamp(dot(N, H), 0.0, 1.0); 77 | float dotVH = clamp(dot(V, H), 0.0, 1.0); 78 | 79 | // Probability Distribution Function 80 | float pdf = D_GGX(dotNH, roughness) * dotNH / (4.0 * dotVH) + 0.0001; 81 | // Slid angle of current smple 82 | float omegaS = 1.0 / (float(32) * pdf); 83 | // Solid angle of 1 pixel across all cube faces 84 | float omegaP = 4.0 * PI / (6.0 * envMapDim * envMapDim); 85 | // Biased (+1.0) mip level for better result 86 | float mipLevel = roughness == 0.0 ? 0.0 : max(0.5 * log2(omegaS / omegaP) + 1.0, 0.0f); 87 | color += textureLod(cubemapSampler, L, mipLevel).rgb * dotNL; 88 | totalWeight += dotNL; 89 | 90 | } 91 | } 92 | return (color / totalWeight); 93 | } 94 | 95 | void main() { 96 | vec3 N = normalize(oPositions); 97 | outColor = vec4(prefilterEnvMap(N, roughness.value), 1.0); 98 | } -------------------------------------------------------------------------------- /crates/viewer/shaders/pre_filtered.frag.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/crates/viewer/shaders/pre_filtered.frag.spv -------------------------------------------------------------------------------- /crates/viewer/shaders/skybox.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout(location = 0) in vec3 oPositions; 4 | 5 | layout(binding = 1) uniform samplerCube cubemapSampler; 6 | 7 | layout(location = 0) out vec4 outColor; 8 | 9 | void main() { 10 | vec3 color = texture(cubemapSampler, oPositions).rgb; 11 | 12 | outColor = vec4(color, 1.0); 13 | } 14 | -------------------------------------------------------------------------------- /crates/viewer/shaders/skybox.frag.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/crates/viewer/shaders/skybox.frag.spv -------------------------------------------------------------------------------- /crates/viewer/shaders/skybox.vert: -------------------------------------------------------------------------------- 1 | #version 450 2 | #extension GL_GOOGLE_include_directive : require 3 | 4 | #include "libs/camera.glsl" 5 | 6 | layout(location = 0) in vec3 vPositions; 7 | 8 | layout(binding = 0) uniform Frame { 9 | Camera camera; 10 | }; 11 | 12 | layout(location = 0) out vec3 oPositions; 13 | 14 | mat4 getViewAtOrigin() { 15 | mat4 view = mat4(camera.view); 16 | view[3][0] = 0; 17 | view[3][1] = 0; 18 | view[3][2] = 0; 19 | return view; 20 | } 21 | 22 | void main() { 23 | oPositions = vPositions; 24 | 25 | mat4 view = getViewAtOrigin(); 26 | 27 | gl_Position = camera.proj * view * vec4(vPositions, 1.0); 28 | } 29 | -------------------------------------------------------------------------------- /crates/viewer/shaders/skybox.vert.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/crates/viewer/shaders/skybox.vert.spv -------------------------------------------------------------------------------- /crates/viewer/shaders/spherical.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout(location = 0) in vec3 oPositions; 4 | 5 | layout(binding = 0) uniform sampler2D textureSampler; 6 | 7 | layout(location = 0) out vec4 outColor; 8 | 9 | vec2 invATan = vec2(0.1591, 0.3183); 10 | vec2 sampleShericalMap(vec3 position) { 11 | return 0.5 + (vec2(atan(position.z, position.x), asin(-position.y)) * invATan); 12 | } 13 | 14 | void main() { 15 | vec2 uv = sampleShericalMap(normalize(oPositions)); 16 | vec3 color = texture(textureSampler, uv).rgb; 17 | outColor = vec4(color, 1.0); 18 | } -------------------------------------------------------------------------------- /crates/viewer/shaders/spherical.frag.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/crates/viewer/shaders/spherical.frag.spv -------------------------------------------------------------------------------- /crates/viewer/shaders/ssao.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | #extension GL_GOOGLE_include_directive : require 3 | 4 | #include "libs/camera.glsl" 5 | 6 | layout (constant_id = 0) const uint SSAO_KERNEL_SIZE = 32; 7 | 8 | layout(push_constant) uniform Config { 9 | float ssaoRadius; 10 | float ssaoStrength; 11 | } config; 12 | 13 | layout(location = 0) in vec2 oCoords; 14 | layout(location = 1) in vec3 oViewRay; 15 | 16 | layout(binding = 0, set = 0) uniform sampler2D normalsSampler; 17 | layout(binding = 1, set = 0) uniform sampler2D depthSampler; 18 | layout(binding = 2, set = 0) uniform sampler2D noiseSampler; 19 | 20 | layout(binding = 3, set = 1) uniform SSAOKernel { 21 | vec4 samples[SSAO_KERNEL_SIZE]; 22 | } ssaoKernel; 23 | 24 | layout(binding = 4, set = 2) uniform Frame { 25 | Camera camera; 26 | }; 27 | 28 | layout(location = 0) out float finalColor; 29 | 30 | float linearDepth(vec2 uv) { 31 | float near = camera.zNear; 32 | float far = camera.zFar; 33 | float depth = texture(depthSampler, uv).r; 34 | return (near * far) / (far + depth * (near - far)); 35 | } 36 | 37 | void main() { 38 | // View-space position 39 | vec3 position = oViewRay * linearDepth(oCoords); 40 | 41 | // View-space normal 42 | vec3 normal = normalize(texture(normalsSampler, oCoords).xyz); 43 | 44 | // View space random vector 45 | ivec2 ssaoSize = textureSize(depthSampler, 0); 46 | ivec2 noiseSize = textureSize(noiseSampler, 0); 47 | vec2 noiseScale = vec2(float(ssaoSize.x) / float(noiseSize.x), float(ssaoSize.y) / float(noiseSize.y)); 48 | vec3 randomVec = texture(noiseSampler, oCoords * noiseScale).xyz; 49 | 50 | // View space TBN matrix 51 | vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal)); 52 | vec3 bitangent = cross(normal, tangent); 53 | mat3 tbn = mat3(tangent, bitangent, normal); 54 | 55 | // Occlusion computation 56 | float occlusion = 0.0; 57 | const float bias = 0.01f; 58 | for (int i = 0; i < SSAO_KERNEL_SIZE; i++) { 59 | // get sample position: 60 | vec3 kSample = tbn * ssaoKernel.samples[i].xyz; 61 | kSample = kSample * config.ssaoRadius + position; 62 | 63 | // project sample position: 64 | vec4 offset = vec4(kSample, 1.0); 65 | offset = camera.proj * offset; 66 | offset.xy /= offset.w; 67 | offset.xy = offset.xy * 0.5 + 0.5; 68 | 69 | float depth = -linearDepth(offset.xy); 70 | 71 | // range check & accumulate: 72 | float rangeCheck = smoothstep(0.0f, 1.0f, config.ssaoRadius / abs(depth - position.z)); 73 | occlusion += (depth >= kSample.z + bias ? 1.0f : 0.0f) * rangeCheck; 74 | } 75 | occlusion = 1.0 - (occlusion / float(SSAO_KERNEL_SIZE)); 76 | 77 | finalColor = pow(occlusion, config.ssaoStrength); 78 | } 79 | -------------------------------------------------------------------------------- /crates/viewer/shaders/ssao.frag.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/crates/viewer/shaders/ssao.frag.spv -------------------------------------------------------------------------------- /crates/viewer/shaders/ssao.vert: -------------------------------------------------------------------------------- 1 | #version 450 2 | #extension GL_GOOGLE_include_directive : require 3 | 4 | #include "libs/camera.glsl" 5 | 6 | layout(location = 0) in vec2 vPos; 7 | layout(location = 1) in vec2 vCoords; 8 | 9 | layout(binding = 4, set = 2) uniform Frame { 10 | Camera camera; 11 | }; 12 | 13 | layout(location = 0) out vec2 oCoords; 14 | layout(location = 1) out vec3 oViewRay; 15 | 16 | void main() { 17 | oCoords = vCoords; 18 | oViewRay = (camera.invertedProj * vec4(vPos.x, vPos.y, 0.0, 1.0)).xyz; 19 | gl_Position = vec4(vPos.x, vPos.y, 0.0, 1.0); 20 | } 21 | -------------------------------------------------------------------------------- /crates/viewer/shaders/ssao.vert.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/crates/viewer/shaders/ssao.vert.spv -------------------------------------------------------------------------------- /crates/viewer/shaders/upsample.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout(location = 0) in vec2 texCoord; 4 | 5 | layout(binding = 0) uniform sampler2D srcTexture; 6 | 7 | layout(push_constant) uniform Constants { 8 | float filterRadius; 9 | } c; 10 | 11 | layout(location = 0) out vec3 upsample; 12 | 13 | // Shader taken from https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom 14 | void main() { 15 | // The filter kernel is applied with a radius, specified in texture 16 | // coordinates, so that the radius will vary across mip resolutions. 17 | float x = c.filterRadius; 18 | float y = c.filterRadius; 19 | 20 | // Take 9 samples around current texel: 21 | // a - b - c 22 | // d - e - f 23 | // g - h - i 24 | // === ('e' is the current texel) === 25 | vec3 a = texture(srcTexture, vec2(texCoord.x - x, texCoord.y + y)).rgb; 26 | vec3 b = texture(srcTexture, vec2(texCoord.x, texCoord.y + y)).rgb; 27 | vec3 c = texture(srcTexture, vec2(texCoord.x + x, texCoord.y + y)).rgb; 28 | 29 | vec3 d = texture(srcTexture, vec2(texCoord.x - x, texCoord.y)).rgb; 30 | vec3 e = texture(srcTexture, vec2(texCoord.x, texCoord.y)).rgb; 31 | vec3 f = texture(srcTexture, vec2(texCoord.x + x, texCoord.y)).rgb; 32 | 33 | vec3 g = texture(srcTexture, vec2(texCoord.x - x, texCoord.y - y)).rgb; 34 | vec3 h = texture(srcTexture, vec2(texCoord.x, texCoord.y - y)).rgb; 35 | vec3 i = texture(srcTexture, vec2(texCoord.x + x, texCoord.y - y)).rgb; 36 | 37 | // Apply weighted distribution, by using a 3x3 tent filter: 38 | // 1 | 1 2 1 | 39 | // -- * | 2 4 2 | 40 | // 16 | 1 2 1 | 41 | upsample = e*4.0; 42 | upsample += (b+d+f+h)*2.0; 43 | upsample += (a+c+g+i); 44 | upsample *= 1.0 / 16.0; 45 | } 46 | -------------------------------------------------------------------------------- /crates/viewer/shaders/upsample.frag.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/crates/viewer/shaders/upsample.frag.spv -------------------------------------------------------------------------------- /crates/viewer/src/camera.rs: -------------------------------------------------------------------------------- 1 | use crate::controls::*; 2 | use math::cgmath::{Deg, InnerSpace, Matrix3, Matrix4, Point3, Rad, Vector3, Zero}; 3 | use math::clamp; 4 | 5 | const MIN_ORBITAL_CAMERA_DISTANCE: f32 = 0.5; 6 | const TARGET_MOVEMENT_SPEED: f32 = 0.003; 7 | const ROTATION_SPEED_DEG: f32 = 0.4; 8 | pub const DEFAULT_FPS_MOVE_SPEED: f32 = 6.0; 9 | 10 | pub const DEFAULT_FOV: f32 = 45.0; 11 | pub const DEFAULT_Z_NEAR: f32 = 0.01; 12 | pub const DEFAULT_Z_FAR: f32 = 100.0; 13 | 14 | #[derive(Debug, Clone, Copy)] 15 | 16 | pub struct Camera { 17 | mode: Mode, 18 | pub fov: Deg, 19 | pub z_near: f32, 20 | pub z_far: f32, 21 | } 22 | 23 | impl Default for Camera { 24 | fn default() -> Self { 25 | Self { 26 | mode: Default::default(), 27 | fov: Deg(DEFAULT_FOV), 28 | z_near: DEFAULT_Z_NEAR, 29 | z_far: DEFAULT_Z_FAR, 30 | } 31 | } 32 | } 33 | 34 | #[derive(Debug, Clone, Copy)] 35 | enum Mode { 36 | Orbital(Orbital), 37 | Fps(Fps), 38 | } 39 | 40 | impl Default for Mode { 41 | fn default() -> Self { 42 | Self::Orbital(Default::default()) 43 | } 44 | } 45 | 46 | impl Camera { 47 | pub fn update(&mut self, input: &InputState, delta_time_secs: f32) { 48 | match &mut self.mode { 49 | Mode::Orbital(c) => c.update(input, delta_time_secs), 50 | Mode::Fps(c) => c.update(input, delta_time_secs), 51 | } 52 | } 53 | 54 | pub fn position(&self) -> Point3 { 55 | match self.mode { 56 | Mode::Orbital(c) => c.position(), 57 | Mode::Fps(c) => c.position(), 58 | } 59 | } 60 | 61 | pub fn target(&self) -> Point3 { 62 | match self.mode { 63 | Mode::Orbital(c) => c.target(), 64 | Mode::Fps(c) => c.target(), 65 | } 66 | } 67 | 68 | pub fn to_orbital(self) -> Self { 69 | let mode = match self.mode { 70 | Mode::Orbital(_) => self.mode, 71 | Mode::Fps(c) => Mode::Orbital(c.into()), 72 | }; 73 | Self { mode, ..self } 74 | } 75 | 76 | pub fn to_fps(self) -> Self { 77 | let mode = match self.mode { 78 | Mode::Fps(_) => self.mode, 79 | Mode::Orbital(c) => Mode::Fps(c.into()), 80 | }; 81 | Self { mode, ..self } 82 | } 83 | 84 | pub fn set_move_speed(&mut self, move_speed: f32) { 85 | if let Mode::Fps(c) = &mut self.mode { 86 | c.move_speed = move_speed; 87 | } 88 | } 89 | } 90 | 91 | #[derive(Debug, Clone, Copy)] 92 | struct Orbital { 93 | theta: f32, 94 | phi: f32, 95 | r: f32, 96 | target: Point3, 97 | } 98 | 99 | impl Default for Orbital { 100 | fn default() -> Self { 101 | Self { 102 | theta: 0.0_f32.to_radians(), 103 | phi: 90.0_f32.to_radians(), 104 | r: 10.0, 105 | target: Point3::new(0.0, 0.0, 0.0), 106 | } 107 | } 108 | } 109 | 110 | impl From for Orbital { 111 | fn from(fps: Fps) -> Self { 112 | let Point3 { x, y, z } = fps.position; 113 | let xx = x * x; 114 | let yy = y * y; 115 | let zz = z * z; 116 | 117 | let r = (xx + yy + zz).sqrt(); 118 | let theta = x.signum() * (z / (f32::EPSILON + (zz + xx).sqrt())).acos(); 119 | let phi = (y / (r + f32::EPSILON)).acos(); 120 | 121 | Self { 122 | r, 123 | theta, 124 | phi, 125 | target: Point3::new(0.0, 0.0, 0.0), 126 | } 127 | } 128 | } 129 | 130 | impl Orbital { 131 | fn update(&mut self, input: &InputState, _: f32) { 132 | // Rotation 133 | if input.is_left_clicked() { 134 | let delta = input.cursor_delta(); 135 | let theta = delta[0] * ROTATION_SPEED_DEG.to_radians(); 136 | let phi = delta[1] * ROTATION_SPEED_DEG.to_radians(); 137 | self.rotate(theta, phi); 138 | } 139 | 140 | // Target move 141 | if input.is_right_clicked() { 142 | let position = self.position(); 143 | let forward = (self.target - position).normalize(); 144 | let up = Vector3::unit_y(); 145 | let right = up.cross(forward).normalize(); 146 | let up = forward.cross(right.normalize()); 147 | 148 | let delta = input.cursor_delta(); 149 | if delta[0] != 0.0 { 150 | self.target += right * delta[0] * self.r * TARGET_MOVEMENT_SPEED; 151 | } 152 | if delta[1] != 0.0 { 153 | self.target += up * delta[1] * self.r * TARGET_MOVEMENT_SPEED; 154 | } 155 | } 156 | 157 | // Zoom 158 | self.forward(input.wheel_delta() * self.r * 0.2); 159 | } 160 | 161 | fn rotate(&mut self, theta: f32, phi: f32) { 162 | self.theta += theta; 163 | let phi = self.phi + phi; 164 | self.phi = clamp(phi, 10.0_f32.to_radians(), 170.0_f32.to_radians()); 165 | } 166 | 167 | fn forward(&mut self, r: f32) { 168 | if (self.r - r).abs() > MIN_ORBITAL_CAMERA_DISTANCE { 169 | self.r -= r; 170 | } 171 | } 172 | 173 | fn position(&self) -> Point3 { 174 | Point3::new( 175 | self.target[0] + self.r * self.phi.sin() * self.theta.sin(), 176 | self.target[1] + self.r * self.phi.cos(), 177 | self.target[2] + self.r * self.phi.sin() * self.theta.cos(), 178 | ) 179 | } 180 | 181 | fn target(&self) -> Point3 { 182 | self.target 183 | } 184 | } 185 | 186 | #[derive(Debug, Clone, Copy)] 187 | struct Fps { 188 | position: Point3, 189 | direction: Vector3, 190 | move_speed: f32, 191 | } 192 | 193 | impl Default for Fps { 194 | fn default() -> Self { 195 | Self { 196 | position: Point3::new(0.0, 0.0, 10.0), 197 | direction: -Vector3::unit_z(), 198 | move_speed: DEFAULT_FPS_MOVE_SPEED, 199 | } 200 | } 201 | } 202 | 203 | impl From for Fps { 204 | fn from(orbital: Orbital) -> Self { 205 | let position = orbital.position(); 206 | let target = orbital.target(); 207 | let direction = (target - position).normalize(); 208 | Self { 209 | position, 210 | direction, 211 | move_speed: DEFAULT_FPS_MOVE_SPEED, 212 | } 213 | } 214 | } 215 | 216 | impl Fps { 217 | fn update(&mut self, input: &InputState, delta_time_secs: f32) { 218 | let forward = self.direction.normalize(); 219 | let up = Vector3::unit_y(); 220 | let right = up.cross(forward).normalize(); 221 | let up = forward.cross(right.normalize()); 222 | 223 | // compute movement 224 | let mut move_dir = Vector3::zero(); 225 | if input.is_forward_pressed() { 226 | move_dir += forward; 227 | } 228 | if input.is_backward_pressed() { 229 | move_dir -= forward; 230 | } 231 | if input.is_left_pressed() { 232 | move_dir += right; 233 | } 234 | if input.is_right_pressed() { 235 | move_dir -= right; 236 | } 237 | if input.is_up_pressed() { 238 | move_dir += up; 239 | } 240 | if input.is_down_pressed() { 241 | move_dir -= up; 242 | } 243 | 244 | if !move_dir.is_zero() { 245 | move_dir = move_dir.normalize() * delta_time_secs * self.move_speed; 246 | } 247 | 248 | self.position += move_dir; 249 | 250 | // compute rotation 251 | if input.is_left_clicked() { 252 | let delta = input.cursor_delta(); 253 | 254 | let rot_speed = delta_time_secs * ROTATION_SPEED_DEG; 255 | let rot_y = Matrix3::::from_angle_y(Rad(-delta[0] * rot_speed)); 256 | let rot_x = Matrix3::::from_axis_angle(right, Rad(delta[1] * rot_speed)); 257 | 258 | self.direction = (rot_x * rot_y * forward).normalize(); 259 | } 260 | } 261 | 262 | fn position(&self) -> Point3 { 263 | self.position 264 | } 265 | 266 | fn target(&self) -> Point3 { 267 | self.position + self.direction.normalize() 268 | } 269 | } 270 | 271 | #[derive(Clone, Copy)] 272 | #[allow(dead_code)] 273 | pub struct CameraUBO { 274 | view: Matrix4, 275 | proj: Matrix4, 276 | inverted_proj: Matrix4, 277 | eye: Point3, 278 | padding: f32, 279 | z_near: f32, 280 | z_far: f32, 281 | } 282 | 283 | impl CameraUBO { 284 | pub fn new( 285 | view: Matrix4, 286 | proj: Matrix4, 287 | inverted_proj: Matrix4, 288 | eye: Point3, 289 | z_near: f32, 290 | z_far: f32, 291 | ) -> Self { 292 | Self { 293 | view, 294 | proj, 295 | inverted_proj, 296 | eye, 297 | padding: 0.0, 298 | z_near, 299 | z_far, 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /crates/viewer/src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use serde::de::Unexpected; 3 | use serde::Deserialize; 4 | use std::fs::File; 5 | use std::path::Path; 6 | use vulkan::MsaaSamples; 7 | 8 | #[derive(Deserialize, Clone)] 9 | pub struct Config { 10 | resolution: Resolution, 11 | #[serde(default)] 12 | fullscreen: bool, 13 | vsync: Option, 14 | #[serde(deserialize_with = "deserialize_msaa")] 15 | msaa: MsaaSamples, 16 | #[serde(default)] 17 | env: Environment, 18 | } 19 | 20 | fn deserialize_msaa<'de, D>(deserializer: D) -> Result 21 | where 22 | D: serde::de::Deserializer<'de>, 23 | { 24 | let samples: u64 = serde::de::Deserialize::deserialize(deserializer)?; 25 | 26 | match samples { 27 | 1 => Ok(MsaaSamples::S1), 28 | 2 => Ok(MsaaSamples::S2), 29 | 4 => Ok(MsaaSamples::S4), 30 | 8 => Ok(MsaaSamples::S8), 31 | 16 => Ok(MsaaSamples::S16), 32 | 32 => Ok(MsaaSamples::S32), 33 | 64 => Ok(MsaaSamples::S64), 34 | _ => Err(serde::de::Error::invalid_value( 35 | Unexpected::Unsigned(samples), 36 | &"msaa should be one of 1, 2, 4, 8, 16, 32 or 64", 37 | )), 38 | } 39 | } 40 | 41 | impl Config { 42 | pub fn resolution(&self) -> Resolution { 43 | self.resolution 44 | } 45 | 46 | pub fn fullscreen(&self) -> bool { 47 | self.fullscreen 48 | } 49 | 50 | pub fn vsync(&self) -> bool { 51 | self.vsync.unwrap_or(false) 52 | } 53 | 54 | pub fn msaa(&self) -> MsaaSamples { 55 | self.msaa 56 | } 57 | 58 | pub fn env(&self) -> &Environment { 59 | &self.env 60 | } 61 | } 62 | 63 | impl Default for Config { 64 | fn default() -> Self { 65 | Config { 66 | resolution: Default::default(), 67 | fullscreen: false, 68 | vsync: Some(false), 69 | msaa: MsaaSamples::S1, 70 | env: Default::default(), 71 | } 72 | } 73 | } 74 | 75 | #[derive(Deserialize, Copy, Clone)] 76 | pub struct Resolution { 77 | width: u32, 78 | height: u32, 79 | } 80 | 81 | impl Resolution { 82 | pub fn width(self) -> u32 { 83 | self.width 84 | } 85 | 86 | pub fn height(self) -> u32 { 87 | self.height 88 | } 89 | } 90 | 91 | impl Default for Resolution { 92 | fn default() -> Self { 93 | Resolution { 94 | width: 120, 95 | height: 120, 96 | } 97 | } 98 | } 99 | 100 | #[derive(Deserialize, Clone)] 101 | pub struct Environment { 102 | path: String, 103 | resolution: Option, 104 | } 105 | 106 | impl Environment { 107 | const SKYBOX_DEFAULT_PATH: &'static str = "assets/env/equi.hdr"; 108 | const SKYBOX_DEFAULT_RESOLUTION: u32 = 1024; 109 | 110 | pub fn path(&self) -> &String { 111 | &self.path 112 | } 113 | 114 | pub fn resolution(&self) -> u32 { 115 | self.resolution.unwrap_or(Self::SKYBOX_DEFAULT_RESOLUTION) 116 | } 117 | } 118 | 119 | impl Default for Environment { 120 | fn default() -> Self { 121 | Self { 122 | path: String::from(Self::SKYBOX_DEFAULT_PATH), 123 | resolution: None, 124 | } 125 | } 126 | } 127 | 128 | pub fn load_config>(path: P) -> Result { 129 | let config_file = File::open(path) 130 | .map_err(|e| AppError::ConfigLoadError(format!("Failed to load file: {}", e)))?; 131 | serde_yaml::from_reader(config_file) 132 | .map_err(|e| AppError::ConfigLoadError(format!("Failed to deserialize config: {}", e))) 133 | } 134 | -------------------------------------------------------------------------------- /crates/viewer/src/controls.rs: -------------------------------------------------------------------------------- 1 | use vulkan::winit::{ 2 | event::{DeviceEvent, ElementState, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent}, 3 | keyboard::{KeyCode, PhysicalKey}, 4 | }; 5 | 6 | #[derive(Copy, Clone, Debug)] 7 | pub struct InputState { 8 | is_forward_pressed: bool, 9 | is_backward_pressed: bool, 10 | is_left_pressed: bool, 11 | is_right_pressed: bool, 12 | is_up_pressed: bool, 13 | is_down_pressed: bool, 14 | is_left_clicked: bool, 15 | is_right_clicked: bool, 16 | cursor_delta: [f32; 2], 17 | wheel_delta: f32, 18 | } 19 | 20 | impl InputState { 21 | pub fn reset(self) -> Self { 22 | Self { 23 | cursor_delta: [0.0, 0.0], 24 | wheel_delta: 0.0, 25 | ..self 26 | } 27 | } 28 | 29 | pub fn handle_window_event(self, event: &WindowEvent) -> Self { 30 | let mut is_forward_pressed = None; 31 | let mut is_backward_pressed = None; 32 | let mut is_left_pressed = None; 33 | let mut is_right_pressed = None; 34 | let mut is_up_pressed = None; 35 | let mut is_down_pressed = None; 36 | let mut is_left_clicked = None; 37 | let mut is_right_clicked = None; 38 | let mut wheel_delta = self.wheel_delta; 39 | 40 | match event { 41 | WindowEvent::MouseInput { button, state, .. } => { 42 | let clicked = matches!(state, ElementState::Pressed); 43 | match button { 44 | MouseButton::Left => is_left_clicked = Some(clicked), 45 | MouseButton::Right => is_right_clicked = Some(clicked), 46 | _ => {} 47 | }; 48 | } 49 | WindowEvent::MouseWheel { 50 | delta: MouseScrollDelta::LineDelta(_, v_lines), 51 | .. 52 | } => { 53 | wheel_delta += v_lines; 54 | } 55 | WindowEvent::KeyboardInput { 56 | event: 57 | KeyEvent { 58 | physical_key: PhysicalKey::Code(scancode), 59 | state, 60 | .. 61 | }, 62 | .. 63 | } => { 64 | let pressed = matches!(state, ElementState::Pressed); 65 | match scancode { 66 | KeyCode::KeyW => is_forward_pressed = Some(pressed), 67 | KeyCode::KeyS => is_backward_pressed = Some(pressed), 68 | KeyCode::KeyA => is_left_pressed = Some(pressed), 69 | KeyCode::KeyD => is_right_pressed = Some(pressed), 70 | KeyCode::Space => is_up_pressed = Some(pressed), 71 | KeyCode::ControlLeft => is_down_pressed = Some(pressed), 72 | _ => {} 73 | }; 74 | } 75 | _ => {} 76 | } 77 | 78 | Self { 79 | is_forward_pressed: is_forward_pressed.unwrap_or(self.is_forward_pressed), 80 | is_backward_pressed: is_backward_pressed.unwrap_or(self.is_backward_pressed), 81 | is_left_pressed: is_left_pressed.unwrap_or(self.is_left_pressed), 82 | is_right_pressed: is_right_pressed.unwrap_or(self.is_right_pressed), 83 | is_up_pressed: is_up_pressed.unwrap_or(self.is_up_pressed), 84 | is_down_pressed: is_down_pressed.unwrap_or(self.is_down_pressed), 85 | is_left_clicked: is_left_clicked.unwrap_or(self.is_left_clicked), 86 | is_right_clicked: is_right_clicked.unwrap_or(self.is_right_clicked), 87 | wheel_delta, 88 | ..self 89 | } 90 | } 91 | 92 | pub fn handle_device_event(self, event: &DeviceEvent) -> Self { 93 | let mut cursor_delta = self.cursor_delta; 94 | 95 | if let DeviceEvent::MouseMotion { delta: (x, y) } = event { 96 | cursor_delta[0] += *x as f32; 97 | cursor_delta[1] += *y as f32; 98 | } 99 | 100 | Self { 101 | cursor_delta, 102 | ..self 103 | } 104 | } 105 | } 106 | 107 | impl InputState { 108 | pub fn is_forward_pressed(&self) -> bool { 109 | self.is_forward_pressed 110 | } 111 | 112 | pub fn is_backward_pressed(&self) -> bool { 113 | self.is_backward_pressed 114 | } 115 | 116 | pub fn is_left_pressed(&self) -> bool { 117 | self.is_left_pressed 118 | } 119 | 120 | pub fn is_right_pressed(&self) -> bool { 121 | self.is_right_pressed 122 | } 123 | 124 | pub fn is_up_pressed(&self) -> bool { 125 | self.is_up_pressed 126 | } 127 | 128 | pub fn is_down_pressed(&self) -> bool { 129 | self.is_down_pressed 130 | } 131 | 132 | pub fn is_left_clicked(&self) -> bool { 133 | self.is_left_clicked 134 | } 135 | 136 | pub fn is_right_clicked(&self) -> bool { 137 | self.is_right_clicked 138 | } 139 | 140 | pub fn cursor_delta(&self) -> [f32; 2] { 141 | self.cursor_delta 142 | } 143 | 144 | pub fn wheel_delta(&self) -> f32 { 145 | self.wheel_delta 146 | } 147 | } 148 | 149 | impl Default for InputState { 150 | fn default() -> Self { 151 | Self { 152 | is_forward_pressed: false, 153 | is_backward_pressed: false, 154 | is_left_pressed: false, 155 | is_right_pressed: false, 156 | is_up_pressed: false, 157 | is_down_pressed: false, 158 | is_left_clicked: false, 159 | is_right_clicked: false, 160 | cursor_delta: [0.0, 0.0], 161 | wheel_delta: 0.0, 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /crates/viewer/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt}; 2 | 3 | #[derive(Debug)] 4 | pub enum AppError { 5 | ConfigLoadError(String), 6 | } 7 | 8 | impl fmt::Display for AppError { 9 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 10 | match self { 11 | AppError::ConfigLoadError(message) => { 12 | write!(f, "Failed to load app configuration: {}", message) 13 | } 14 | } 15 | } 16 | } 17 | 18 | impl Error for AppError {} 19 | -------------------------------------------------------------------------------- /crates/viewer/src/loader.rs: -------------------------------------------------------------------------------- 1 | //! Non-blocking model loader 2 | //! 3 | //! The loader starts a worker thread that will wait for load messages. 4 | //! Once a message is received the thread will load the model and send the 5 | //! loaded model through another channel. 6 | //! 7 | //! When dropping the loader, a stop message is sent to the thread so it can 8 | //! stop listening for load events. Then we wait for the thread to terminate. 9 | //! 10 | //! Users have to call `load` to load a new model and `get_model` to retrieve 11 | //! the loaded model. 12 | 13 | use model::{Model, ModelStagingResources}; 14 | 15 | use std::error::Error; 16 | use std::path::{Path, PathBuf}; 17 | use std::sync::mpsc; 18 | use std::sync::mpsc::{Receiver, Sender}; 19 | use std::sync::Arc; 20 | use std::thread; 21 | use std::thread::JoinHandle; 22 | use vulkan::ash::vk; 23 | use vulkan::{Context, PreLoadedResource}; 24 | 25 | enum Message { 26 | Load(PathBuf), 27 | Stop, 28 | } 29 | 30 | pub struct Loader { 31 | message_sender: Sender, 32 | model_receiver: Receiver>, 33 | thread_handle: Option>, 34 | } 35 | 36 | impl Loader { 37 | pub fn new(context: Arc) -> Self { 38 | let (message_sender, message_receiver) = mpsc::channel(); 39 | let (model_sender, model_receiver) = mpsc::channel(); 40 | 41 | let thread_handle = Some(thread::spawn(move || { 42 | log::info!("Starting loader"); 43 | loop { 44 | let message = message_receiver.recv().expect("Failed to receive a path"); 45 | match message { 46 | Message::Load(path) => { 47 | log::info!("Start loading {}", path.as_path().display()); 48 | let pre_loaded_model = pre_load_model(&context, path.as_path()); 49 | 50 | match pre_loaded_model { 51 | Ok(pre_loaded_model) => { 52 | log::info!("Finish loading {}", path.as_path().display()); 53 | model_sender.send(pre_loaded_model).unwrap(); 54 | } 55 | Err(error) => { 56 | log::error!( 57 | "Failed to load {}. Cause: {}", 58 | path.as_path().display(), 59 | error 60 | ); 61 | } 62 | } 63 | } 64 | Message::Stop => break, 65 | } 66 | } 67 | log::info!("Stopping loader"); 68 | })); 69 | 70 | Self { 71 | message_sender, 72 | model_receiver, 73 | thread_handle, 74 | } 75 | } 76 | 77 | /// Start loading a new model in the background. 78 | /// 79 | /// Call `get_model` to retrieve the loaded model. 80 | pub fn load(&self, path: PathBuf) { 81 | self.message_sender 82 | .send(Message::Load(path)) 83 | .expect("Failed to send load message to loader"); 84 | } 85 | 86 | /// Get the last loaded model. 87 | /// 88 | /// If no model is ready, then `None` is returned. 89 | pub fn get_model(&self) -> Option { 90 | match self.model_receiver.try_recv() { 91 | Ok(mut pre_loaded_model) => Some(pre_loaded_model.finish()), 92 | _ => None, 93 | } 94 | } 95 | } 96 | 97 | fn pre_load_model>( 98 | context: &Arc, 99 | path: P, 100 | ) -> Result, Box> { 101 | let device = context.device(); 102 | 103 | // Create command buffer 104 | let command_buffer = { 105 | let allocate_info = vk::CommandBufferAllocateInfo::default() 106 | .command_pool(context.general_command_pool()) 107 | .level(vk::CommandBufferLevel::SECONDARY) 108 | .command_buffer_count(1); 109 | 110 | unsafe { device.allocate_command_buffers(&allocate_info).unwrap()[0] } 111 | }; 112 | 113 | // Begin recording command buffer 114 | { 115 | let inheritance_info = vk::CommandBufferInheritanceInfo::default(); 116 | let command_buffer_begin_info = vk::CommandBufferBeginInfo::default() 117 | .inheritance_info(&inheritance_info) 118 | .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); 119 | unsafe { 120 | device 121 | .begin_command_buffer(command_buffer, &command_buffer_begin_info) 122 | .unwrap() 123 | }; 124 | } 125 | 126 | // Load model data and prepare command buffer 127 | let model = Model::create_from_file(Arc::clone(context), command_buffer, path); 128 | 129 | // End recording command buffer 130 | unsafe { device.end_command_buffer(command_buffer).unwrap() }; 131 | 132 | model 133 | } 134 | 135 | impl Drop for Loader { 136 | fn drop(&mut self) { 137 | self.message_sender 138 | .send(Message::Stop) 139 | .expect("Failed to send stop message to loader thread"); 140 | if let Some(handle) = self.thread_handle.take() { 141 | handle 142 | .join() 143 | .expect("Failed to wait for loader thread termination"); 144 | } 145 | log::info!("Loader dropped"); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /crates/viewer/src/main.rs: -------------------------------------------------------------------------------- 1 | mod camera; 2 | mod config; 3 | mod controls; 4 | mod error; 5 | mod gui; 6 | mod loader; 7 | mod renderer; 8 | mod viewer; 9 | 10 | use crate::{camera::*, config::Config, controls::*, loader::*, renderer::*}; 11 | use clap::Parser; 12 | use std::{error::Error, path::PathBuf}; 13 | use viewer::Viewer; 14 | use vulkan::*; 15 | use winit::{ 16 | application::ApplicationHandler, 17 | dpi::PhysicalSize, 18 | event::{DeviceEvent, DeviceId, StartCause, WindowEvent}, 19 | event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, 20 | window::{Fullscreen, Window, WindowId}, 21 | }; 22 | 23 | const TITLE: &str = "Gltf Viewer"; 24 | 25 | fn main() -> Result<(), Box> { 26 | env_logger::init(); 27 | log::info!("Welcome to gltf-viewer-rs"); 28 | 29 | let event_loop = EventLoop::new().unwrap(); 30 | event_loop.set_control_flow(ControlFlow::Poll); 31 | let mut app = App::new()?; 32 | event_loop.run_app(&mut app)?; 33 | 34 | Ok(()) 35 | } 36 | 37 | struct App { 38 | config: Config, 39 | enable_debug: bool, 40 | model_path: Option, 41 | window: Option, 42 | viewer: Option, 43 | } 44 | 45 | impl App { 46 | fn new() -> Result> { 47 | let cli = Cli::parse(); 48 | 49 | let config = cli 50 | .config 51 | .as_ref() 52 | .map(config::load_config) 53 | .transpose()? 54 | .unwrap_or_default(); 55 | let enable_debug = cli.debug; 56 | let model_path = cli.file; 57 | 58 | Ok(Self { 59 | config, 60 | enable_debug, 61 | model_path, 62 | window: None, 63 | viewer: None, 64 | }) 65 | } 66 | } 67 | 68 | impl ApplicationHandler for App { 69 | fn resumed(&mut self, event_loop: &ActiveEventLoop) { 70 | let window = event_loop 71 | .create_window( 72 | Window::default_attributes() 73 | .with_title(TITLE) 74 | .with_inner_size(PhysicalSize::new( 75 | self.config.resolution().width(), 76 | self.config.resolution().height(), 77 | )) 78 | .with_fullscreen( 79 | self.config 80 | .fullscreen() 81 | .then_some(Fullscreen::Borderless(None)), 82 | ), 83 | ) 84 | .expect("Failed to create window"); 85 | 86 | self.viewer = Some(Viewer::new( 87 | self.config.clone(), 88 | &window, 89 | self.enable_debug, 90 | self.model_path.take(), 91 | )); 92 | self.window = Some(window); 93 | } 94 | 95 | fn new_events(&mut self, _: &ActiveEventLoop, _: StartCause) { 96 | if let Some(viewer) = self.viewer.as_mut() { 97 | viewer.new_frame(); 98 | } 99 | } 100 | 101 | fn about_to_wait(&mut self, _: &ActiveEventLoop) { 102 | self.viewer 103 | .as_mut() 104 | .unwrap() 105 | .end_frame(self.window.as_ref().unwrap()); 106 | } 107 | 108 | fn window_event(&mut self, event_loop: &ActiveEventLoop, _: WindowId, event: WindowEvent) { 109 | if let WindowEvent::CloseRequested = event { 110 | event_loop.exit(); 111 | } 112 | 113 | self.viewer 114 | .as_mut() 115 | .unwrap() 116 | .handle_window_event(self.window.as_ref().unwrap(), &event); 117 | } 118 | 119 | fn device_event(&mut self, _: &ActiveEventLoop, _: DeviceId, event: DeviceEvent) { 120 | self.viewer.as_mut().unwrap().handle_device_event(&event); 121 | } 122 | 123 | fn exiting(&mut self, _: &ActiveEventLoop) { 124 | self.viewer.as_mut().unwrap().on_exit(); 125 | } 126 | } 127 | 128 | #[derive(Parser)] 129 | #[command(name = "GLTF Viewer")] 130 | #[command(version = "1.0")] 131 | #[command(about = "Viewer for GLTF 2.0 files.", long_about = None)] 132 | struct Cli { 133 | #[arg(short, long, value_name = "FILE")] 134 | config: Option, 135 | #[arg(short, long, value_name = "FILE")] 136 | file: Option, 137 | #[arg(short, long)] 138 | debug: bool, 139 | } 140 | -------------------------------------------------------------------------------- /crates/viewer/src/renderer/fullscreen.rs: -------------------------------------------------------------------------------- 1 | use std::mem::size_of; 2 | use std::sync::Arc; 3 | use vulkan::{ash::vk, create_device_local_buffer_with_data, Buffer, Context, Vertex}; 4 | 5 | use super::{create_renderer_pipeline, RendererPipelineParameters}; 6 | 7 | #[repr(C)] 8 | #[derive(Clone, Copy)] 9 | #[allow(dead_code)] 10 | pub struct QuadVertex { 11 | position: [f32; 2], 12 | coords: [f32; 2], 13 | } 14 | 15 | impl Vertex for QuadVertex { 16 | fn get_bindings_descriptions() -> Vec { 17 | vec![vk::VertexInputBindingDescription { 18 | binding: 0, 19 | stride: size_of::() as _, 20 | input_rate: vk::VertexInputRate::VERTEX, 21 | }] 22 | } 23 | 24 | fn get_attributes_descriptions() -> Vec { 25 | vec![ 26 | vk::VertexInputAttributeDescription { 27 | location: 0, 28 | binding: 0, 29 | format: vk::Format::R32G32_SFLOAT, 30 | offset: 0, 31 | }, 32 | vk::VertexInputAttributeDescription { 33 | location: 1, 34 | binding: 0, 35 | format: vk::Format::R32G32_SFLOAT, 36 | offset: 8, 37 | }, 38 | ] 39 | } 40 | } 41 | 42 | pub struct QuadModel { 43 | pub vertices: Buffer, 44 | pub indices: Buffer, 45 | } 46 | 47 | impl QuadModel { 48 | pub fn new(context: &Arc) -> Self { 49 | let indices: [u16; 6] = [0, 1, 2, 2, 3, 0]; 50 | let indices = create_device_local_buffer_with_data::( 51 | context, 52 | vk::BufferUsageFlags::INDEX_BUFFER, 53 | &indices, 54 | ); 55 | let vertices: [f32; 16] = [ 56 | -1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 0.0, -1.0, -1.0, 0.0, 0.0, 57 | ]; 58 | let vertices = create_device_local_buffer_with_data::( 59 | context, 60 | vk::BufferUsageFlags::VERTEX_BUFFER, 61 | &vertices, 62 | ); 63 | 64 | Self { vertices, indices } 65 | } 66 | } 67 | 68 | pub fn create_fullscreen_pipeline( 69 | context: &Arc, 70 | output_format: vk::Format, 71 | layout: vk::PipelineLayout, 72 | fragment_shader_name: &'static str, 73 | fragment_shader_specialization: Option<&vk::SpecializationInfo>, 74 | ) -> vk::Pipeline { 75 | let depth_stencil_info = vk::PipelineDepthStencilStateCreateInfo::default() 76 | .depth_test_enable(false) 77 | .depth_write_enable(false) 78 | .depth_compare_op(vk::CompareOp::LESS_OR_EQUAL) 79 | .depth_bounds_test_enable(false) 80 | .min_depth_bounds(0.0) 81 | .max_depth_bounds(1.0) 82 | .stencil_test_enable(false) 83 | .front(Default::default()) 84 | .back(Default::default()); 85 | 86 | let color_blend_attachments = [vk::PipelineColorBlendAttachmentState::default() 87 | .color_write_mask( 88 | vk::ColorComponentFlags::R 89 | | vk::ColorComponentFlags::G 90 | | vk::ColorComponentFlags::B 91 | | vk::ColorComponentFlags::A, 92 | ) 93 | .blend_enable(false) 94 | .src_color_blend_factor(vk::BlendFactor::ONE) 95 | .dst_color_blend_factor(vk::BlendFactor::ZERO) 96 | .color_blend_op(vk::BlendOp::ADD) 97 | .src_alpha_blend_factor(vk::BlendFactor::ONE) 98 | .dst_alpha_blend_factor(vk::BlendFactor::ZERO) 99 | .alpha_blend_op(vk::BlendOp::ADD)]; 100 | 101 | create_renderer_pipeline::( 102 | context, 103 | RendererPipelineParameters { 104 | vertex_shader_name: "fullscreen", 105 | fragment_shader_name, 106 | vertex_shader_specialization: None, 107 | fragment_shader_specialization, 108 | msaa_samples: vk::SampleCountFlags::TYPE_1, 109 | color_attachment_formats: &[output_format], 110 | depth_attachment_format: None, 111 | layout, 112 | depth_stencil_info: &depth_stencil_info, 113 | color_blend_attachments: &color_blend_attachments, 114 | enable_face_culling: true, 115 | parent: None, 116 | }, 117 | ) 118 | } 119 | -------------------------------------------------------------------------------- /crates/viewer/src/renderer/model/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod gbufferpass; 2 | pub mod lightpass; 3 | 4 | mod uniform; 5 | 6 | use gbufferpass::GBufferPass; 7 | use lightpass::LightPass; 8 | use math::cgmath::Matrix4; 9 | use model::Material; 10 | use model::Model; 11 | use model::MAX_JOINTS_PER_MESH; 12 | use std::cell::RefCell; 13 | use std::rc::Weak; 14 | use std::sync::Arc; 15 | use uniform::*; 16 | use vulkan::ash::vk; 17 | use vulkan::{mem_copy, mem_copy_aligned, Buffer, Context}; 18 | 19 | type JointsBuffer = [Matrix4; MAX_JOINTS_PER_MESH]; 20 | 21 | pub struct ModelData { 22 | context: Arc, 23 | model: Weak>, 24 | transform_ubos: Vec, 25 | skin_ubos: Vec, 26 | skin_matrices: Vec>, 27 | materials_ubo: Buffer, 28 | light_ubos: Vec, 29 | } 30 | 31 | pub struct ModelRenderer { 32 | pub data: ModelData, 33 | pub gbuffer_pass: GBufferPass, 34 | pub light_pass: LightPass, 35 | } 36 | 37 | impl ModelData { 38 | pub fn create(context: Arc, model: Weak>, image_count: u32) -> Self { 39 | let model_rc = model 40 | .upgrade() 41 | .expect("Cannot create model renderer because model was dropped"); 42 | 43 | let transform_ubos = create_transform_ubos(&context, &model_rc.borrow(), image_count); 44 | let (skin_ubos, skin_matrices) = 45 | create_skin_ubos(&context, &model_rc.borrow(), image_count); 46 | let materials_ubo = create_materials_ubo(&context, &model_rc.borrow()); 47 | let light_ubos = create_lights_ubos(&context, image_count); 48 | 49 | Self { 50 | context, 51 | model, 52 | transform_ubos, 53 | skin_ubos, 54 | skin_matrices, 55 | materials_ubo, 56 | light_ubos, 57 | } 58 | } 59 | 60 | pub fn update_buffers(&mut self, frame_index: usize) { 61 | let model = &self 62 | .model 63 | .upgrade() 64 | .expect("Cannot update buffers because model was dropped"); 65 | let model = model.borrow(); 66 | 67 | // Update transform buffers 68 | { 69 | let mesh_nodes = model 70 | .nodes() 71 | .nodes() 72 | .iter() 73 | .filter(|n| n.mesh_index().is_some()); 74 | 75 | let transforms = mesh_nodes.map(|n| n.transform()).collect::>(); 76 | 77 | let elem_size = &self.context.get_ubo_alignment::>(); 78 | let buffer = &mut self.transform_ubos[frame_index]; 79 | unsafe { 80 | let data_ptr = buffer.map_memory(); 81 | mem_copy_aligned(data_ptr, u64::from(*elem_size), &transforms); 82 | } 83 | } 84 | 85 | // Update skin buffers 86 | { 87 | let skins = model.skins(); 88 | let skin_matrices = &mut self.skin_matrices[frame_index]; 89 | 90 | for (index, skin) in skins.iter().enumerate() { 91 | let matrices = &mut skin_matrices[index]; 92 | for (index, joint) in skin.joints().iter().take(MAX_JOINTS_PER_MESH).enumerate() { 93 | let joint_matrix = joint.matrix(); 94 | matrices[index] = joint_matrix; 95 | } 96 | } 97 | 98 | let elem_size = self.context.get_ubo_alignment::(); 99 | let buffer = &mut self.skin_ubos[frame_index]; 100 | unsafe { 101 | let data_ptr = buffer.map_memory(); 102 | mem_copy_aligned(data_ptr, u64::from(elem_size), skin_matrices); 103 | } 104 | } 105 | 106 | // Update light buffers 107 | { 108 | let mut lights_ubo = LightsUBO::default(); 109 | 110 | for (i, ln) in model 111 | .nodes() 112 | .nodes() 113 | .iter() 114 | .filter(|n| n.light_index().is_some()) 115 | .map(|n| (n.transform(), n.light_index().unwrap())) 116 | .map(|(t, i)| (t, model.lights()[i]).into()) 117 | .enumerate() 118 | .take(MAX_LIGHT_COUNT) 119 | { 120 | lights_ubo.count += 1; 121 | lights_ubo.lights[i] = ln; 122 | } 123 | 124 | let buffer = &mut self.light_ubos[frame_index]; 125 | let data_ptr = buffer.map_memory(); 126 | unsafe { mem_copy(data_ptr, &[lights_ubo]) }; 127 | } 128 | 129 | // Update materials buffer 130 | { 131 | let mut ubos: Vec = vec![Material::default().into()]; 132 | model 133 | .materials() 134 | .iter() 135 | .copied() 136 | .map(|m| m.into()) 137 | .for_each(|m| ubos.push(m)); 138 | 139 | let elem_size = self.context.get_ubo_alignment::() as vk::DeviceSize; 140 | let buffer = &mut self.materials_ubo; 141 | unsafe { 142 | let data_ptr = buffer.map_memory(); 143 | mem_copy_aligned(data_ptr, elem_size, &ubos); 144 | } 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /crates/viewer/src/renderer/postprocess/blurpass.rs: -------------------------------------------------------------------------------- 1 | use crate::renderer::attachments::Attachments; 2 | use crate::renderer::fullscreen::*; 3 | use std::sync::Arc; 4 | use vulkan::ash::vk::{RenderingAttachmentInfo, RenderingInfo}; 5 | use vulkan::ash::{vk, Device}; 6 | use vulkan::{Context, Descriptors, Texture}; 7 | 8 | const BLUR_OUTPUT_FORMAT: vk::Format = vk::Format::R8_UNORM; 9 | 10 | /// Blur pass 11 | pub struct BlurPass { 12 | context: Arc, 13 | descriptors: Descriptors, 14 | pipeline_layout: vk::PipelineLayout, 15 | pipeline: vk::Pipeline, 16 | } 17 | 18 | impl BlurPass { 19 | pub fn create(context: Arc, input_image: &Texture) -> Self { 20 | let descriptors = create_descriptors(&context, input_image); 21 | let pipeline_layout = create_pipeline_layout(context.device(), descriptors.layout()); 22 | let pipeline = create_pipeline(&context, pipeline_layout); 23 | 24 | BlurPass { 25 | context, 26 | descriptors, 27 | pipeline_layout, 28 | pipeline, 29 | } 30 | } 31 | } 32 | 33 | impl BlurPass { 34 | pub fn set_input_image(&mut self, input_image: &Texture) { 35 | self.descriptors 36 | .sets() 37 | .iter() 38 | .for_each(|s| update_descriptor_set(&self.context, *s, input_image)); 39 | } 40 | 41 | pub fn cmd_draw( 42 | &self, 43 | command_buffer: vk::CommandBuffer, 44 | attachments: &Attachments, 45 | quad_model: &QuadModel, 46 | ) { 47 | let device = self.context.device(); 48 | 49 | let extent = vk::Extent2D { 50 | width: attachments.ssao_blur.image.extent.width, 51 | height: attachments.ssao_blur.image.extent.height, 52 | }; 53 | 54 | unsafe { 55 | self.context.device().cmd_set_viewport( 56 | command_buffer, 57 | 0, 58 | &[vk::Viewport { 59 | width: extent.width as _, 60 | height: extent.height as _, 61 | max_depth: 1.0, 62 | ..Default::default() 63 | }], 64 | ); 65 | self.context.device().cmd_set_scissor( 66 | command_buffer, 67 | 0, 68 | &[vk::Rect2D { 69 | extent, 70 | ..Default::default() 71 | }], 72 | ) 73 | } 74 | 75 | { 76 | let attachment_info = RenderingAttachmentInfo::default() 77 | .clear_value(vk::ClearValue { 78 | color: vk::ClearColorValue { 79 | float32: [0.0, 0.0, 0.0, 1.0], 80 | }, 81 | }) 82 | .image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) 83 | .image_view(attachments.ssao_blur.view) 84 | .load_op(vk::AttachmentLoadOp::CLEAR) 85 | .store_op(vk::AttachmentStoreOp::STORE); 86 | 87 | let rendering_info = RenderingInfo::default() 88 | .color_attachments(std::slice::from_ref(&attachment_info)) 89 | .layer_count(1) 90 | .render_area(vk::Rect2D { 91 | extent, 92 | ..Default::default() 93 | }); 94 | 95 | unsafe { 96 | self.context 97 | .dynamic_rendering() 98 | .cmd_begin_rendering(command_buffer, &rendering_info) 99 | }; 100 | } 101 | 102 | // Bind pipeline 103 | unsafe { 104 | device.cmd_bind_pipeline( 105 | command_buffer, 106 | vk::PipelineBindPoint::GRAPHICS, 107 | self.pipeline, 108 | ) 109 | }; 110 | 111 | // Bind buffers 112 | unsafe { 113 | device.cmd_bind_vertex_buffers(command_buffer, 0, &[quad_model.vertices.buffer], &[0]); 114 | device.cmd_bind_index_buffer( 115 | command_buffer, 116 | quad_model.indices.buffer, 117 | 0, 118 | vk::IndexType::UINT16, 119 | ); 120 | } 121 | 122 | // Bind descriptor sets 123 | unsafe { 124 | device.cmd_bind_descriptor_sets( 125 | command_buffer, 126 | vk::PipelineBindPoint::GRAPHICS, 127 | self.pipeline_layout, 128 | 0, 129 | self.descriptors.sets(), 130 | &[], 131 | ) 132 | }; 133 | 134 | // Draw 135 | unsafe { device.cmd_draw_indexed(command_buffer, 6, 1, 0, 0, 1) }; 136 | 137 | unsafe { 138 | self.context 139 | .dynamic_rendering() 140 | .cmd_end_rendering(command_buffer) 141 | }; 142 | } 143 | } 144 | 145 | impl Drop for BlurPass { 146 | fn drop(&mut self) { 147 | let device = self.context.device(); 148 | unsafe { 149 | device.destroy_pipeline(self.pipeline, None); 150 | device.destroy_pipeline_layout(self.pipeline_layout, None); 151 | } 152 | } 153 | } 154 | 155 | fn create_descriptors(context: &Arc, input_image: &Texture) -> Descriptors { 156 | let layout = create_descriptor_set_layout(context.device()); 157 | let pool = create_descriptor_pool(context.device()); 158 | let sets = create_descriptor_sets(context, pool, layout, input_image); 159 | Descriptors::new(Arc::clone(context), layout, pool, sets) 160 | } 161 | 162 | fn create_descriptor_set_layout(device: &Device) -> vk::DescriptorSetLayout { 163 | let bindings = [vk::DescriptorSetLayoutBinding::default() 164 | .binding(0) 165 | .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) 166 | .descriptor_count(1) 167 | .stage_flags(vk::ShaderStageFlags::FRAGMENT)]; 168 | 169 | let layout_info = vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings); 170 | 171 | unsafe { 172 | device 173 | .create_descriptor_set_layout(&layout_info, None) 174 | .unwrap() 175 | } 176 | } 177 | fn create_descriptor_pool(device: &Device) -> vk::DescriptorPool { 178 | let descriptor_count = 1; 179 | let pool_sizes = [vk::DescriptorPoolSize { 180 | ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER, 181 | descriptor_count, 182 | }]; 183 | 184 | let create_info = vk::DescriptorPoolCreateInfo::default() 185 | .pool_sizes(&pool_sizes) 186 | .max_sets(descriptor_count) 187 | .flags(vk::DescriptorPoolCreateFlags::FREE_DESCRIPTOR_SET); 188 | 189 | unsafe { device.create_descriptor_pool(&create_info, None).unwrap() } 190 | } 191 | 192 | fn create_descriptor_sets( 193 | context: &Arc, 194 | pool: vk::DescriptorPool, 195 | layout: vk::DescriptorSetLayout, 196 | input_image: &Texture, 197 | ) -> Vec { 198 | let layouts = [layout]; 199 | let allocate_info = vk::DescriptorSetAllocateInfo::default() 200 | .descriptor_pool(pool) 201 | .set_layouts(&layouts); 202 | let sets = unsafe { 203 | context 204 | .device() 205 | .allocate_descriptor_sets(&allocate_info) 206 | .unwrap() 207 | }; 208 | 209 | update_descriptor_set(context, sets[0], input_image); 210 | 211 | sets 212 | } 213 | 214 | fn update_descriptor_set(context: &Arc, set: vk::DescriptorSet, input_image: &Texture) { 215 | let input_image_info = [vk::DescriptorImageInfo::default() 216 | .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) 217 | .image_view(input_image.view) 218 | .sampler( 219 | input_image 220 | .sampler 221 | .expect("Post process input image must have a sampler"), 222 | )]; 223 | 224 | let descriptor_writes = [vk::WriteDescriptorSet::default() 225 | .dst_set(set) 226 | .dst_binding(0) 227 | .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) 228 | .image_info(&input_image_info)]; 229 | 230 | unsafe { 231 | context 232 | .device() 233 | .update_descriptor_sets(&descriptor_writes, &[]) 234 | } 235 | } 236 | 237 | fn create_pipeline_layout( 238 | device: &Device, 239 | descriptor_set_layout: vk::DescriptorSetLayout, 240 | ) -> vk::PipelineLayout { 241 | let layouts = [descriptor_set_layout]; 242 | let layout_info = vk::PipelineLayoutCreateInfo::default().set_layouts(&layouts); 243 | unsafe { device.create_pipeline_layout(&layout_info, None).unwrap() } 244 | } 245 | 246 | fn create_pipeline(context: &Arc, layout: vk::PipelineLayout) -> vk::Pipeline { 247 | create_fullscreen_pipeline(context, BLUR_OUTPUT_FORMAT, layout, "blur", None) 248 | } 249 | -------------------------------------------------------------------------------- /crates/viewer/src/renderer/postprocess/mod.rs: -------------------------------------------------------------------------------- 1 | mod bloom; 2 | mod blurpass; 3 | mod finalpass; 4 | 5 | pub use self::{bloom::*, blurpass::*, finalpass::*}; 6 | -------------------------------------------------------------------------------- /crates/viewer/src/renderer/skybox.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | attachments::SCENE_COLOR_FORMAT, create_renderer_pipeline, RendererPipelineParameters, 3 | }; 4 | use ash::{vk, Device}; 5 | use environment::*; 6 | use std::sync::Arc; 7 | use vulkan::*; 8 | 9 | pub struct SkyboxRenderer { 10 | context: Arc, 11 | model: SkyboxModel, 12 | descriptors: Descriptors, 13 | pipeline_layout: vk::PipelineLayout, 14 | pipeline: vk::Pipeline, 15 | } 16 | 17 | impl SkyboxRenderer { 18 | pub fn create( 19 | context: Arc, 20 | camera_buffers: &[Buffer], 21 | environment: &Environment, 22 | msaa_samples: vk::SampleCountFlags, 23 | depth_format: vk::Format, 24 | ) -> Self { 25 | let model = SkyboxModel::new(&context); 26 | let descriptors = create_descriptors(&context, camera_buffers, environment); 27 | let pipeline_layout = create_pipeline_layout(context.device(), descriptors.layout()); 28 | let pipeline = 29 | create_skybox_pipeline(&context, msaa_samples, depth_format, pipeline_layout); 30 | 31 | Self { 32 | context, 33 | model, 34 | descriptors, 35 | pipeline_layout, 36 | pipeline, 37 | } 38 | } 39 | } 40 | 41 | impl SkyboxRenderer { 42 | pub fn cmd_draw(&self, command_buffer: vk::CommandBuffer, frame_index: usize) { 43 | let device = self.context.device(); 44 | // Bind skybox pipeline 45 | unsafe { 46 | device.cmd_bind_pipeline( 47 | command_buffer, 48 | vk::PipelineBindPoint::GRAPHICS, 49 | self.pipeline, 50 | ) 51 | }; 52 | 53 | // Bind skybox descriptor sets 54 | unsafe { 55 | device.cmd_bind_descriptor_sets( 56 | command_buffer, 57 | vk::PipelineBindPoint::GRAPHICS, 58 | self.pipeline_layout, 59 | 0, 60 | &self.descriptors.sets()[frame_index..=frame_index], 61 | &[], 62 | ) 63 | }; 64 | 65 | unsafe { 66 | device.cmd_bind_vertex_buffers( 67 | command_buffer, 68 | 0, 69 | &[self.model.vertices().buffer], 70 | &[0], 71 | ); 72 | } 73 | 74 | unsafe { 75 | device.cmd_bind_index_buffer( 76 | command_buffer, 77 | self.model.indices().buffer, 78 | 0, 79 | vk::IndexType::UINT32, 80 | ); 81 | } 82 | 83 | // Draw skybox 84 | unsafe { device.cmd_draw_indexed(command_buffer, 36, 1, 0, 0, 0) }; 85 | } 86 | } 87 | 88 | impl Drop for SkyboxRenderer { 89 | fn drop(&mut self) { 90 | let device = self.context.device(); 91 | unsafe { 92 | device.destroy_pipeline(self.pipeline, None); 93 | device.destroy_pipeline_layout(self.pipeline_layout, None); 94 | } 95 | } 96 | } 97 | 98 | fn create_descriptors( 99 | context: &Arc, 100 | uniform_buffers: &[Buffer], 101 | environment: &Environment, 102 | ) -> Descriptors { 103 | let layout = create_descriptor_set_layout(context.device()); 104 | let pool = create_descriptor_pool(context.device(), uniform_buffers.len() as _); 105 | let sets = create_descriptor_sets(context, pool, layout, uniform_buffers, environment.skybox()); 106 | Descriptors::new(Arc::clone(context), layout, pool, sets) 107 | } 108 | 109 | fn create_descriptor_set_layout(device: &Device) -> vk::DescriptorSetLayout { 110 | let bindings = [ 111 | vk::DescriptorSetLayoutBinding::default() 112 | .binding(0) 113 | .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER) 114 | .descriptor_count(1) 115 | .stage_flags(vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT), 116 | vk::DescriptorSetLayoutBinding::default() 117 | .binding(1) 118 | .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) 119 | .descriptor_count(1) 120 | .stage_flags(vk::ShaderStageFlags::FRAGMENT), 121 | ]; 122 | 123 | let layout_info = vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings); 124 | 125 | unsafe { 126 | device 127 | .create_descriptor_set_layout(&layout_info, None) 128 | .unwrap() 129 | } 130 | } 131 | fn create_descriptor_pool(device: &Device, descriptor_count: u32) -> vk::DescriptorPool { 132 | let pool_sizes = [ 133 | vk::DescriptorPoolSize { 134 | ty: vk::DescriptorType::UNIFORM_BUFFER, 135 | descriptor_count, 136 | }, 137 | vk::DescriptorPoolSize { 138 | ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER, 139 | descriptor_count, 140 | }, 141 | ]; 142 | 143 | let create_info = vk::DescriptorPoolCreateInfo::default() 144 | .pool_sizes(&pool_sizes) 145 | .max_sets(descriptor_count); 146 | 147 | unsafe { device.create_descriptor_pool(&create_info, None).unwrap() } 148 | } 149 | 150 | fn create_descriptor_sets( 151 | context: &Arc, 152 | pool: vk::DescriptorPool, 153 | layout: vk::DescriptorSetLayout, 154 | buffers: &[Buffer], 155 | cubemap: &Texture, 156 | ) -> Vec { 157 | let layouts = (0..buffers.len()).map(|_| layout).collect::>(); 158 | 159 | let allocate_info = vk::DescriptorSetAllocateInfo::default() 160 | .descriptor_pool(pool) 161 | .set_layouts(&layouts); 162 | let sets = unsafe { 163 | context 164 | .device() 165 | .allocate_descriptor_sets(&allocate_info) 166 | .unwrap() 167 | }; 168 | 169 | sets.iter().zip(buffers.iter()).for_each(|(set, buffer)| { 170 | let buffer_info = [vk::DescriptorBufferInfo::default() 171 | .buffer(buffer.buffer) 172 | .offset(0) 173 | .range(vk::WHOLE_SIZE)]; 174 | 175 | let cubemap_info = [vk::DescriptorImageInfo::default() 176 | .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) 177 | .image_view(cubemap.view) 178 | .sampler(cubemap.sampler.unwrap())]; 179 | 180 | let descriptor_writes = [ 181 | vk::WriteDescriptorSet::default() 182 | .dst_set(*set) 183 | .dst_binding(0) 184 | .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER) 185 | .buffer_info(&buffer_info), 186 | vk::WriteDescriptorSet::default() 187 | .dst_set(*set) 188 | .dst_binding(1) 189 | .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) 190 | .image_info(&cubemap_info), 191 | ]; 192 | 193 | unsafe { 194 | context 195 | .device() 196 | .update_descriptor_sets(&descriptor_writes, &[]) 197 | } 198 | }); 199 | 200 | sets 201 | } 202 | 203 | fn create_pipeline_layout( 204 | device: &Device, 205 | descriptor_set_layout: vk::DescriptorSetLayout, 206 | ) -> vk::PipelineLayout { 207 | let layouts = [descriptor_set_layout]; 208 | let layout_info = vk::PipelineLayoutCreateInfo::default().set_layouts(&layouts); 209 | unsafe { device.create_pipeline_layout(&layout_info, None).unwrap() } 210 | } 211 | 212 | fn create_skybox_pipeline( 213 | context: &Arc, 214 | msaa_samples: vk::SampleCountFlags, 215 | depth_format: vk::Format, 216 | layout: vk::PipelineLayout, 217 | ) -> vk::Pipeline { 218 | let depth_stencil_info = vk::PipelineDepthStencilStateCreateInfo::default() 219 | .depth_test_enable(false) 220 | .depth_write_enable(false) 221 | .depth_compare_op(vk::CompareOp::LESS_OR_EQUAL) 222 | .depth_bounds_test_enable(false) 223 | .min_depth_bounds(0.0) 224 | .max_depth_bounds(1.0) 225 | .stencil_test_enable(false) 226 | .front(Default::default()) 227 | .back(Default::default()); 228 | 229 | let color_blend_attachments = [vk::PipelineColorBlendAttachmentState::default() 230 | .color_write_mask( 231 | vk::ColorComponentFlags::R 232 | | vk::ColorComponentFlags::G 233 | | vk::ColorComponentFlags::B 234 | | vk::ColorComponentFlags::A, 235 | ) 236 | .blend_enable(false) 237 | .src_color_blend_factor(vk::BlendFactor::ONE) 238 | .dst_color_blend_factor(vk::BlendFactor::ZERO) 239 | .color_blend_op(vk::BlendOp::ADD) 240 | .src_alpha_blend_factor(vk::BlendFactor::ONE) 241 | .dst_alpha_blend_factor(vk::BlendFactor::ZERO) 242 | .alpha_blend_op(vk::BlendOp::ADD)]; 243 | 244 | create_renderer_pipeline::( 245 | context, 246 | RendererPipelineParameters { 247 | vertex_shader_name: "skybox", 248 | fragment_shader_name: "skybox", 249 | vertex_shader_specialization: None, 250 | fragment_shader_specialization: None, 251 | msaa_samples, 252 | color_attachment_formats: &[SCENE_COLOR_FORMAT], 253 | depth_attachment_format: Some(depth_format), 254 | layout, 255 | depth_stencil_info: &depth_stencil_info, 256 | color_blend_attachments: &color_blend_attachments, 257 | enable_face_culling: true, 258 | parent: None, 259 | }, 260 | ) 261 | } 262 | -------------------------------------------------------------------------------- /crates/viewer/src/viewer.rs: -------------------------------------------------------------------------------- 1 | use environment::Environment; 2 | use model::{Model, PlaybackMode}; 3 | use std::{cell::RefCell, path::PathBuf, rc::Rc, sync::Arc, time::Instant}; 4 | use vulkan::{ 5 | winit::{ 6 | dpi::PhysicalSize, 7 | event::{DeviceEvent, ElementState, KeyEvent, WindowEvent}, 8 | keyboard::Key, 9 | window::Window, 10 | }, 11 | Context, 12 | }; 13 | 14 | use crate::{ 15 | config::Config, 16 | gui::{self, Gui}, 17 | Camera, InputState, Loader, RenderError, Renderer, RendererSettings, 18 | }; 19 | 20 | pub struct Viewer { 21 | config: Config, 22 | context: Arc, 23 | renderer_settings: RendererSettings, 24 | gui: Gui, 25 | enable_ui: bool, 26 | renderer: Renderer, 27 | model: Option>>, 28 | loader: Loader, 29 | camera: Camera, 30 | input_state: InputState, 31 | time: Instant, 32 | dirty_swapchain: bool, 33 | } 34 | 35 | impl Viewer { 36 | pub fn new( 37 | config: Config, 38 | window: &Window, 39 | enable_debug: bool, 40 | model_path: Option, 41 | ) -> Self { 42 | let context = Arc::new(Context::new(window, enable_debug)); 43 | 44 | let renderer_settings = RendererSettings::new(&context); 45 | 46 | let environment = 47 | Environment::new(&context, config.env().path(), config.env().resolution()); 48 | let gui = Gui::new(window, renderer_settings); 49 | let enable_ui = true; 50 | let renderer = Renderer::create( 51 | Arc::clone(&context), 52 | &config, 53 | renderer_settings, 54 | environment, 55 | ); 56 | 57 | let model: Option>> = None; 58 | let loader = Loader::new(Arc::new(context.new_thread())); 59 | if let Some(p) = model_path { 60 | loader.load(p); 61 | } 62 | 63 | let camera = Camera::default(); 64 | let input_state = InputState::default(); 65 | let time = Instant::now(); 66 | let dirty_swapchain = false; 67 | 68 | Self { 69 | config, 70 | context, 71 | renderer_settings, 72 | gui, 73 | enable_ui, 74 | renderer, 75 | model, 76 | loader, 77 | camera, 78 | input_state, 79 | time, 80 | dirty_swapchain, 81 | } 82 | } 83 | 84 | pub fn new_frame(&mut self) { 85 | self.input_state = self.input_state.reset(); 86 | } 87 | 88 | pub fn handle_window_event(&mut self, window: &Window, event: &WindowEvent) { 89 | self.input_state = self.input_state.handle_window_event(event); 90 | self.gui.handle_event(window, event); 91 | match event { 92 | // Dropped file 93 | WindowEvent::DroppedFile(path) => { 94 | log::debug!("File dropped: {:?}", path); 95 | self.loader.load(path.clone()); 96 | } 97 | // Resizing 98 | WindowEvent::Resized(_) => { 99 | self.dirty_swapchain = true; 100 | } 101 | // Key events 102 | WindowEvent::KeyboardInput { 103 | event: 104 | KeyEvent { 105 | logical_key: Key::Character(c), 106 | state: ElementState::Pressed, 107 | .. 108 | }, 109 | .. 110 | } => { 111 | if c == "h" { 112 | self.enable_ui = !self.enable_ui; 113 | } 114 | } 115 | _ => (), 116 | } 117 | } 118 | 119 | pub fn handle_device_event(&mut self, event: &DeviceEvent) { 120 | self.input_state = self.input_state.handle_device_event(event); 121 | } 122 | 123 | pub fn end_frame(&mut self, window: &Window) { 124 | let new_time = Instant::now(); 125 | let delta_s = (new_time - self.time).as_secs_f32(); 126 | self.time = new_time; 127 | 128 | // Load new model 129 | if let Some(loaded_model) = self.loader.get_model() { 130 | self.gui.set_model_metadata(loaded_model.metadata().clone()); 131 | self.model.take(); 132 | 133 | self.context.graphics_queue_wait_idle(); 134 | let loaded_model = Rc::new(RefCell::new(loaded_model)); 135 | self.renderer.set_model(&loaded_model); 136 | self.model = Some(loaded_model); 137 | } 138 | 139 | // Update model 140 | if let Some(model) = self.model.as_ref() { 141 | let mut model = model.borrow_mut(); 142 | 143 | if self.gui.should_toggle_animation() { 144 | model.toggle_animation(); 145 | } else if self.gui.should_stop_animation() { 146 | model.stop_animation(); 147 | } else if self.gui.should_reset_animation() { 148 | model.reset_animation(); 149 | } else { 150 | let playback_mode = if self.gui.is_infinite_animation_checked() { 151 | PlaybackMode::Loop 152 | } else { 153 | PlaybackMode::Once 154 | }; 155 | 156 | model.set_animation_playback_mode(playback_mode); 157 | model.set_current_animation(self.gui.get_selected_animation()); 158 | } 159 | self.gui 160 | .set_animation_playback_state(model.get_animation_playback_state()); 161 | 162 | let delta_s = delta_s * self.gui.get_animation_speed(); 163 | model.update(delta_s); 164 | } 165 | 166 | // Update camera 167 | { 168 | if self.gui.should_reset_camera() { 169 | self.camera = Default::default(); 170 | } 171 | 172 | self.camera = match self.gui.camera_mode() { 173 | gui::CameraMode::Orbital => self.camera.to_orbital(), 174 | gui::CameraMode::Fps => self.camera.to_fps(), 175 | }; 176 | 177 | self.camera.fov = self.gui.camera_fov(); 178 | self.camera.z_near = self.gui.camera_z_near(); 179 | self.camera.z_far = self.gui.camera_z_far(); 180 | self.camera.set_move_speed(self.gui.camera_move_speed()); 181 | 182 | if !self.gui.is_hovered() { 183 | self.camera.update(&self.input_state, delta_s); 184 | self.gui.set_camera(Some(self.camera)); 185 | } 186 | } 187 | 188 | // Check if settings changed 189 | if let Some(new_renderer_settings) = self.gui.get_new_renderer_settings() { 190 | // recreate swapchain if hdr was toggled 191 | if self.renderer_settings.hdr_enabled != new_renderer_settings.hdr_enabled { 192 | self.renderer.recreate_swapchain( 193 | window.inner_size().into(), 194 | self.config.vsync(), 195 | new_renderer_settings.hdr_enabled.unwrap_or_default(), 196 | ); 197 | self.dirty_swapchain = false; 198 | } 199 | 200 | // Update renderer 201 | self.renderer.update_settings(new_renderer_settings); 202 | 203 | self.renderer_settings = new_renderer_settings; 204 | } 205 | 206 | // If swapchain must be recreated wait for windows to not be minimized anymore 207 | if self.dirty_swapchain { 208 | let PhysicalSize { width, height } = window.inner_size(); 209 | if width > 0 && height > 0 { 210 | self.renderer.recreate_swapchain( 211 | window.inner_size().into(), 212 | self.config.vsync(), 213 | self.renderer_settings.hdr_enabled.unwrap_or_default(), 214 | ); 215 | } else { 216 | return; 217 | } 218 | } 219 | 220 | let gui = self.enable_ui.then_some(&mut self.gui); 221 | self.dirty_swapchain = matches!( 222 | self.renderer.render(window, self.camera, gui), 223 | Err(RenderError::DirtySwapchain) 224 | ); 225 | } 226 | 227 | pub fn on_exit(&mut self) { 228 | self.renderer.wait_idle_gpu(); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /images/cesium.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/images/cesium.gif -------------------------------------------------------------------------------- /images/clearcoat_wicker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/images/clearcoat_wicker.png -------------------------------------------------------------------------------- /images/corset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/images/corset.png -------------------------------------------------------------------------------- /images/env.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/images/env.png -------------------------------------------------------------------------------- /images/flight_helmet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/images/flight_helmet.png -------------------------------------------------------------------------------- /images/helmet_indoor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/images/helmet_indoor.png -------------------------------------------------------------------------------- /images/helmet_night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/images/helmet_night.png -------------------------------------------------------------------------------- /images/helmet_sand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/images/helmet_sand.png -------------------------------------------------------------------------------- /images/helmet_woods.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/images/helmet_woods.png -------------------------------------------------------------------------------- /images/junkrat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/images/junkrat.png -------------------------------------------------------------------------------- /images/mg08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien-ben/gltf-viewer-rs/b13e429eec868b25ed2dd1a73ae520f691ab2416/images/mg08.png -------------------------------------------------------------------------------- /scripts/debug.ps1: -------------------------------------------------------------------------------- 1 | $env:VK_LAYER_PATH = "$env:VULKAN_SDK\Bin" 2 | $env:VK_INSTANCE_LAYERS = "VK_LAYER_KHRONOS_validation" 3 | $env:RUST_LOG="DEBUG" 4 | $env:RUST_BACKTRACE=1 5 | 6 | cargo run --release -- -c config.yml -f assets/models/cesium_man_with_light.glb -d 7 | 8 | $env:VK_LAYER_PATH = "" 9 | $env:VK_INSTANCE_LAYERS = "" 10 | $env:RUST_LOG="" 11 | $env:RUST_BACKTRACE="" -------------------------------------------------------------------------------- /scripts/debug.sh: -------------------------------------------------------------------------------- 1 | export VK_LAYER_PATH=$VULKAN_SDK/Bin 2 | export VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation 3 | 4 | RUST_BACKTRACE=1 RUST_LOG=debug cargo run --release -- -c config.yml -f assets/models/cesium_man_with_light.glb -d 5 | 6 | export VK_LAYER_PATH= 7 | export VK_INSTANCE_LAYERS= -------------------------------------------------------------------------------- /scripts/run.ps1: -------------------------------------------------------------------------------- 1 | $env:RUST_LOG="INFO" 2 | 3 | cargo run --release -- -c config.yml -f assets/models/cesium_man_with_light.glb 4 | 5 | $env:RUST_LOG="" -------------------------------------------------------------------------------- /scripts/run.sh: -------------------------------------------------------------------------------- 1 | RUST_LOG=info cargo run --release -- -c config.yml -f assets/models/cesium_man_with_light.glb --------------------------------------------------------------------------------