diff --git a/examples/beat-saber-clone/assets/beat_saber.glb b/examples/beat-saber-clone/assets/beat_saber.glb index 8f0e6553..5d931c6a 100644 --- a/examples/beat-saber-clone/assets/beat_saber.glb +++ b/examples/beat-saber-clone/assets/beat_saber.glb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6bba7a8d7e33efedee59191c5cda8a5513cb7b58885dbf51df2e3901bb60054 -size 73504 +oid sha256:7ef784fb740e2583eceacfb885ac17b58fb47c6f209b3d962b321026d71ede37 +size 89840 diff --git a/examples/beat-saber-clone/src/lib.rs b/examples/beat-saber-clone/src/lib.rs index 892285be..546ad938 100644 --- a/examples/beat-saber-clone/src/lib.rs +++ b/examples/beat-saber-clone/src/lib.rs @@ -2,27 +2,47 @@ mod components; pub mod resources; mod systems; -use crate::components::{Colour, Saber}; -use crate::resources::GameState; -use crate::systems::cube_spawner::{create_cubes, cube_spawner_system}; -use crate::systems::game_system; +use crate::{ + components::{Colour, Saber}, + resources::GameState, + systems::{ + cube_spawner::{create_cubes, cube_spawner_system}, + game_system, + sabers::{add_saber_physics, sabers_system}, + update_ui_system, + }, +}; use hotham_debug_server::DebugServer; +use rapier3d::prelude::{ColliderBuilder, InteractionGroups}; use std::collections::HashMap; -use hotham::gltf_loader::add_model_to_world; -use hotham::legion::{Resources, Schedule, World}; -use hotham::resources::{PhysicsContext, RenderContext, XrContext}; -use hotham::schedule_functions::{begin_frame, end_frame, physics_step, sync_debug_server}; -use hotham::systems::rendering::rendering_system; -use hotham::systems::{ - collision_system, update_parent_transform_matrix_system, update_rigid_body_transforms_system, - update_transform_matrix_system, +use hotham::{ + components::{ + hand::Handedness, + panel::{create_panel, PanelButton}, + Collider, Pointer, + }, + gltf_loader::{self, add_model_to_world}, + legion::{Resources, Schedule, World}, + resources::{ + physics_context::PANEL_COLLISION_GROUP, GuiContext, HapticContext, PhysicsContext, + RenderContext, XrContext, + }, + schedule_functions::{ + apply_haptic_feedback, begin_frame, begin_pbr_renderpass, end_frame, end_pbr_renderpass, + physics_step, sync_debug_server, + }, + systems::{ + collision_system, draw_gui_system, pointers_system, rendering_system, + update_parent_transform_matrix_system, update_rigid_body_transforms_system, + update_transform_matrix_system, + }, + util::entity_to_u64, + App, HothamResult, }; -use hotham::{gltf_loader, App, HothamResult}; -use nalgebra::{Matrix4, Vector3}; +use nalgebra::{vector, Matrix4, Vector3}; use serde::{Deserialize, Serialize}; -use systems::sabers::{add_saber_physics, sabers_system}; #[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))] pub fn main() { @@ -45,6 +65,7 @@ struct DebugInfo { pub fn real_main() -> HothamResult<()> { let (xr_context, vulkan_context) = XrContext::new()?; let render_context = RenderContext::new(&vulkan_context, &xr_context)?; + let gui_context = GuiContext::new(&vulkan_context); let mut physics_context = PhysicsContext::default(); let mut world = World::default(); let glb_bufs: Vec<&[u8]> = vec![include_bytes!("../assets/beat_saber.glb")]; @@ -53,6 +74,7 @@ pub fn real_main() -> HothamResult<()> { &vulkan_context, &render_context.descriptor_set_layouts, )?; + let haptic_context = HapticContext::default(); // Add Environment add_environment_models(&models, &mut world, &vulkan_context, &render_context); @@ -77,31 +99,75 @@ pub fn real_main() -> HothamResult<()> { &mut physics_context, ); - // Add Blue Saber - add_saber( + // // Add Blue Saber + // add_saber( + // Colour::Blue, + // &models, + // &mut world, + // &vulkan_context, + // &render_context, + // &mut physics_context, + // ); + + // Add pointer + add_pointer( Colour::Blue, &models, &mut world, &vulkan_context, &render_context, - &mut physics_context, ); + // Add a panel + let mut panel_components = create_panel( + "Not clicked!", + 800, + 800, + &vulkan_context, + &render_context, + &gui_context, + vec![PanelButton::new("Test")], + ); + let t = &mut panel_components.3.translation; + t[1] = 1.5; + t[2] = -2.; + + let panel_entity = world.push(panel_components); + let collider = ColliderBuilder::cuboid(0.5, 0.5, 0.) + .sensor(true) + .collision_groups(InteractionGroups::new( + PANEL_COLLISION_GROUP, + PANEL_COLLISION_GROUP, + )) + .translation(vector![0.0, 1.5, -2.]) + .user_data(entity_to_u64(panel_entity).into()) + .build(); + let handle = physics_context.colliders.insert(collider); + let collider = Collider { + collisions_this_frame: Vec::new(), + handle, + }; + let mut panel_entry = world.entry(panel_entity).unwrap(); + panel_entry.add_component(collider); + let debug_server = DebugServer::new(); let mut resources = Resources::default(); resources.insert(xr_context); resources.insert(vulkan_context); + resources.insert(gui_context); resources.insert(physics_context); resources.insert(debug_server); resources.insert(render_context); resources.insert(models); resources.insert(0 as usize); resources.insert(GameState::default()); + resources.insert(haptic_context); let schedule = Schedule::builder() .add_thread_local_fn(begin_frame) .add_system(sabers_system()) + .add_system(pointers_system()) .add_thread_local_fn(physics_step) .add_system(collision_system()) .add_system(cube_spawner_system(SPAWN_RATE)) @@ -109,9 +175,14 @@ pub fn real_main() -> HothamResult<()> { .add_system(update_transform_matrix_system()) .add_system(update_parent_transform_matrix_system()) .add_system(game_system()) + .add_system(update_ui_system()) + .add_system(draw_gui_system()) + .add_thread_local_fn(begin_pbr_renderpass) .add_system(rendering_system()) - .add_thread_local_fn(sync_debug_server) + .add_thread_local_fn(end_pbr_renderpass) + .add_thread_local_fn(apply_haptic_feedback) .add_thread_local_fn(end_frame) + .add_thread_local_fn(sync_debug_server) .build(); let mut app = App::new(world, resources, schedule)?; @@ -119,6 +190,36 @@ pub fn real_main() -> HothamResult<()> { Ok(()) } +fn add_pointer( + colour: Colour, + models: &HashMap, + world: &mut World, + vulkan_context: &hotham::resources::vulkan_context::VulkanContext, + render_context: &RenderContext, +) { + let (handedness, model_name) = match colour { + Colour::Red => (Handedness::Left, "Red Pointer"), + Colour::Blue => (Handedness::Right, "Blue Pointer"), + }; + let pointer = add_model_to_world( + model_name, + models, + world, + None, + vulkan_context, + &render_context.descriptor_set_layouts, + ) + .unwrap(); + { + let mut pointer_entry = world.entry(pointer).unwrap(); + pointer_entry.add_component(Pointer { + handedness, + trigger_value: 0., + }); + pointer_entry.add_component(colour); + } +} + fn add_saber( colour: Colour, models: &HashMap, diff --git a/examples/beat-saber-clone/src/systems/game.rs b/examples/beat-saber-clone/src/systems/game.rs index ca28e856..9cdaf223 100644 --- a/examples/beat-saber-clone/src/systems/game.rs +++ b/examples/beat-saber-clone/src/systems/game.rs @@ -3,7 +3,7 @@ use legion::{system, systems::CommandBuffer, world::SubWorld, Entity, EntityStor use crate::{ components::{Colour, Cube}, - resources::{game_state, GameState}, + resources::GameState, }; #[system(for_each)] @@ -63,8 +63,6 @@ pub fn game( #[cfg(test)] mod tests { use hotham::resources::PhysicsContext; - use hotham::schedule_functions::physics_step; - use hotham::systems::collision_system; use legion::Schedule; use legion::{Resources, World}; use nalgebra::{vector, UnitQuaternion}; diff --git a/examples/beat-saber-clone/src/systems/mod.rs b/examples/beat-saber-clone/src/systems/mod.rs index 5f9f7451..4c600ae2 100644 --- a/examples/beat-saber-clone/src/systems/mod.rs +++ b/examples/beat-saber-clone/src/systems/mod.rs @@ -1,5 +1,7 @@ pub mod cube_spawner; pub mod game; pub mod sabers; +pub mod update_ui; pub use game::game_system; pub use sabers::sabers_system; +pub use update_ui::update_ui_system; diff --git a/examples/beat-saber-clone/src/systems/sabers.rs b/examples/beat-saber-clone/src/systems/sabers.rs index 590d63fc..aaba9182 100644 --- a/examples/beat-saber-clone/src/systems/sabers.rs +++ b/examples/beat-saber-clone/src/systems/sabers.rs @@ -56,7 +56,7 @@ pub fn sabers( .unwrap(); let mut position = posef_to_isometry(pose); - apply_offset(&mut position); + apply_grip_offset(&mut position); rigid_body.set_next_kinematic_position(position); } @@ -77,7 +77,7 @@ pub fn add_saber_physics(world: &mut World, physics_context: &mut PhysicsContext saber_entry.add_component(rigid_body); } -fn apply_offset(position: &mut Isometry3) { +pub fn apply_grip_offset(position: &mut Isometry3) { let updated_rotation = position.rotation.quaternion() * ROTATION_OFFSET; let updated_translation = position.translation.vector - vector!(POSITION_OFFSET[0], POSITION_OFFSET[1], POSITION_OFFSET[2]); @@ -139,7 +139,7 @@ mod tests { )); let t = Translation3::new(0.2, 1.4, 2.); let mut position = Isometry3::from_parts(t, q1); - apply_offset(&mut position); + apply_grip_offset(&mut position); let expected_rotation = Quaternion::new( -0.5493369162990798, diff --git a/examples/beat-saber-clone/src/systems/update_ui.rs b/examples/beat-saber-clone/src/systems/update_ui.rs new file mode 100644 index 00000000..182338f0 --- /dev/null +++ b/examples/beat-saber-clone/src/systems/update_ui.rs @@ -0,0 +1,11 @@ +use hotham::components::Panel; +use legion::system; + +use crate::resources::GameState; + +#[system(for_each)] +pub fn update_ui(panel: &mut Panel, #[resource] game_state: &GameState) { + let score = game_state.current_score; + let new_string = format!("Current score: {}", score); + panel.text = new_string; +} diff --git a/hotham-simulator/src/simulator.rs b/hotham-simulator/src/simulator.rs index 3c8d243b..d0c366dc 100644 --- a/hotham-simulator/src/simulator.rs +++ b/hotham-simulator/src/simulator.rs @@ -758,6 +758,7 @@ pub unsafe extern "system" fn attach_action_sets( Result::SUCCESS } +// TODO: Handle aim pose. pub unsafe extern "system" fn create_action_space( _session: Session, create_info: *const ActionSpaceCreateInfo, @@ -1066,6 +1067,7 @@ unsafe fn build_swapchain(state: &mut MutexGuard) -> vk::SwapchainKHR { .with_drag_and_drop(false) .build(&event_loop) .unwrap(); + println!("WINDOW SCALE FACTOR, {:?}", window.scale_factor()); println!("[HOTHAM_SIMULATOR] ..done."); let extent = vk::Extent2D { height: VIEWPORT_HEIGHT, diff --git a/hotham.code-workspace b/hotham.code-workspace index 28796f9a..e09f070d 100644 --- a/hotham.code-workspace +++ b/hotham.code-workspace @@ -1,19 +1,10 @@ { "folders": [ { - "path": "hotham" + "path": "." }, { - "path": "hotham-debug-frontend" - }, - { - "path": "hotham-simulator" - }, - { - "path": "examples\\beat-saber-clone" - }, - { - "path": "hotham-debug-server" + "path": "..\\egui-winit-ash-integration" } ], "settings": { @@ -26,7 +17,9 @@ "hotham", "hotham-debug-server", "hotham-simulator", - "beat-saber-clone" + "beat-saber-clone", + "test_assets" ], + "jest.rootPath": "hotham_debug_frontend", } } \ No newline at end of file diff --git a/hotham/Cargo.toml b/hotham/Cargo.toml index 0e004723..9d470ef1 100644 --- a/hotham/Cargo.toml +++ b/hotham/Cargo.toml @@ -14,6 +14,7 @@ ash = "0.33.2" console = "0.14" crossbeam = "0.8.1" ctrlc = {version = "3", features = ["termination"]} +egui = "0.15" gltf = {version = "0.16", features = ["KHR_materials_pbrSpecularGlossiness"]} hotham-debug-server = {path = "../hotham-debug-server"} image = "0.23" @@ -25,7 +26,8 @@ mint = "0.5.6" nalgebra = {features = ["convert-mint", "serde-serialize"], version = "0.29.0"} openxr = {features = ["loaded", "mint"], version = "0.15.4"} rand = "0.8" -rapier3d = "0.11" +rapier3d = "0.11.1" +renderdoc = "0.10" schemars = "0.8" serde = {version = "1.0", features = ["derive"]} serde_json = "1.0" diff --git a/hotham/capture_capture.rdc b/hotham/capture_capture.rdc new file mode 100644 index 00000000..b3efe6fa Binary files /dev/null and b/hotham/capture_capture.rdc differ diff --git a/hotham/shaders/gui.frag.spv b/hotham/shaders/gui.frag.spv new file mode 100644 index 00000000..594c9142 Binary files /dev/null and b/hotham/shaders/gui.frag.spv differ diff --git a/hotham/shaders/gui.vert.spv b/hotham/shaders/gui.vert.spv new file mode 100644 index 00000000..fe6942aa Binary files /dev/null and b/hotham/shaders/gui.vert.spv differ diff --git a/hotham/shaders/model.frag b/hotham/shaders/model.frag deleted file mode 100644 index 67ebe1f9..00000000 --- a/hotham/shaders/model.frag +++ /dev/null @@ -1,32 +0,0 @@ -#version 450 -#extension GL_ARB_separate_shader_objects : enable -#extension GL_EXT_multiview : require - -layout(set = 1, binding = 1) uniform sampler2D textureSampler; -layout(set = 1, binding = 2) uniform sampler2D normalSampler; - -layout(location = 0) in vec2 inTextureCoordinates; -layout(location = 1) in vec3 inNormal; -layout(location = 2) in vec3 inViewVec; -layout(location = 3) in vec3 inLightVec; -layout(location = 4) in vec4 inTangent; - -layout(location = 0) out vec4 outFragColor; - -void main() { - vec4 color = texture(textureSampler, inTextureCoordinates); - - vec3 N = normalize(inNormal); - vec3 T = normalize(inTangent.xyz); - vec3 B = cross(inNormal, inTangent.xyz) * inTangent.w; - mat3 TBN = mat3(T, B, N); - N = TBN * normalize(texture(normalSampler, inTextureCoordinates).xyz * 2.0 - vec3(1.0)); - - const float ambient = 0.05; - vec3 L = normalize(inLightVec); - vec3 V = normalize(inViewVec); - vec3 R = reflect(-L, N); - vec3 diffuse = max(dot(N, L), ambient).rrr; - float specular = pow(max(dot(R, V), 0.0), 32.0); - outFragColor = vec4(diffuse * color.rgb + specular, color.a); -} \ No newline at end of file diff --git a/hotham/shaders/model.frag.spv b/hotham/shaders/model.frag.spv deleted file mode 100644 index c7c6be79..00000000 Binary files a/hotham/shaders/model.frag.spv and /dev/null differ diff --git a/hotham/shaders/model.vert b/hotham/shaders/model.vert deleted file mode 100644 index 12a26e1f..00000000 --- a/hotham/shaders/model.vert +++ /dev/null @@ -1,53 +0,0 @@ -#version 450 -#extension GL_ARB_separate_shader_objects : enable -#extension GL_EXT_multiview : enable - -// Scene Uniform Buffer -layout(set = 0, binding = 0) uniform UniformBufferObject { - mat4 view[2]; - mat4 projection[2]; - float deltaTime; - vec4 lightPos; -} ubo; - -// Skin SSBO -layout(std430, set = 1, binding = 0) readonly buffer JointMatrices { - mat4 jointMatrices[]; -}; - -layout(push_constant) uniform PushConsts { - mat4 model; -} pushConsts; - - -layout(location = 0) in vec3 inPosition; -layout(location = 1) in vec2 inTextureCoordinates; -layout(location = 2) in vec3 inNormal; -layout(location = 3) in vec4 inTangent; -layout(location = 4) in vec4 inJointIndices; -layout(location = 5) in vec4 inJointWeights; - -layout(location = 0) out vec2 outTextureCoordinates; -layout(location = 1) out vec3 outNormal; -layout(location = 2) out vec3 outViewVec; -layout(location = 3) out vec3 outLightVec; -layout(location = 4) out vec4 outTangent; - -void main() { - mat4 skinMat = - inJointWeights.x * jointMatrices[int(inJointIndices.x)] + - inJointWeights.y * jointMatrices[int(inJointIndices.y)] + - inJointWeights.z * jointMatrices[int(inJointIndices.z)] + - inJointWeights.w * jointMatrices[int(inJointIndices.w)]; - - gl_Position = ubo.projection[gl_ViewIndex] * ubo.view[gl_ViewIndex] * pushConsts.model * skinMat * vec4(inPosition, 1.0); - - outNormal = normalize(transpose(inverse(mat3(ubo.view[gl_ViewIndex] * pushConsts.model * skinMat))) * inNormal); - outTextureCoordinates = inTextureCoordinates; - - vec4 pos = ubo.view[gl_ViewIndex] * vec4(inPosition, 1.0); - vec3 lightPos = mat3(ubo.view[gl_ViewIndex]) * ubo.lightPos.xyz; - outLightVec = lightPos- pos.xyz; - outViewVec = -pos.xyz; - outTangent = inTangent; -} \ No newline at end of file diff --git a/hotham/shaders/model.vert.spv b/hotham/shaders/model.vert.spv deleted file mode 100644 index 45f73887..00000000 Binary files a/hotham/shaders/model.vert.spv and /dev/null differ diff --git a/hotham/shaders/pbr.frag.spv b/hotham/shaders/pbr.frag.spv index 072e962d..eceaa1cf 100644 Binary files a/hotham/shaders/pbr.frag.spv and b/hotham/shaders/pbr.frag.spv differ diff --git a/hotham/shaders/pbr.vert.spv b/hotham/shaders/pbr.vert.spv index 7c4d2aa0..994890ca 100644 Binary files a/hotham/shaders/pbr.vert.spv and b/hotham/shaders/pbr.vert.spv differ diff --git a/hotham/src/components/material.rs b/hotham/src/components/material.rs index e42444de..2139b564 100644 --- a/hotham/src/components/material.rs +++ b/hotham/src/components/material.rs @@ -8,20 +8,20 @@ use crate::{resources::VulkanContext, texture::Texture}; #[repr(C)] #[derive(Debug, Default, Clone, PartialEq)] pub struct Material { - base_colour_factor: Vector4, - emmissive_factor: Vector4, - diffuse_factor: Vector4, - specular_factor: Vector4, - workflow: f32, - base_color_texture_set: i32, - metallic_roughness_texture_set: i32, - normal_texture_set: i32, - occlusion_texture_set: i32, - emissive_texture_set: i32, - metallic_factor: f32, - roughness_factor: f32, - alpha_mask: f32, - alpha_mask_cutoff: f32, + pub base_colour_factor: Vector4, + pub emmissive_factor: Vector4, + pub diffuse_factor: Vector4, + pub specular_factor: Vector4, + pub workflow: f32, + pub base_color_texture_set: i32, + pub metallic_roughness_texture_set: i32, + pub normal_texture_set: i32, + pub occlusion_texture_set: i32, + pub emissive_texture_set: i32, + pub metallic_factor: f32, + pub roughness_factor: f32, + pub alpha_mask: f32, + pub alpha_mask_cutoff: f32, } impl Material { diff --git a/hotham/src/components/mod.rs b/hotham/src/components/mod.rs index b840eed4..cd3e8503 100644 --- a/hotham/src/components/mod.rs +++ b/hotham/src/components/mod.rs @@ -6,7 +6,9 @@ pub mod info; pub mod joint; pub mod material; pub mod mesh; +pub mod panel; pub mod parent; +pub mod pointer; pub mod primitive; pub mod rigid_body; pub mod root; @@ -23,7 +25,9 @@ pub use info::Info; pub use joint::Joint; pub use material::Material; pub use mesh::Mesh; +pub use panel::Panel; pub use parent::Parent; +pub use pointer::Pointer; pub use primitive::Primitive; pub use rigid_body::RigidBody; pub use root::Root; diff --git a/hotham/src/components/panel.rs b/hotham/src/components/panel.rs new file mode 100644 index 00000000..3e26784c --- /dev/null +++ b/hotham/src/components/panel.rs @@ -0,0 +1,295 @@ +use ash::vk::{self}; +use egui::emath::vec2; +use egui::epaint::Vertex as EguiVertex; +use egui::{CtxRef, Pos2}; +use itertools::izip; +use nalgebra::{vector, Vector4}; + +const BUFFER_SIZE: usize = 1024; + +use crate::buffer::Buffer; +use crate::components::mesh::MeshUBO; +use crate::components::{Material, Mesh, Primitive}; +use crate::resources::gui_context::SCALE_FACTOR; +use crate::resources::GuiContext; +use crate::{ + resources::{RenderContext, VulkanContext}, + texture::Texture, +}; +use crate::{Vertex, COLOR_FORMAT}; + +use super::{Transform, TransformMatrix, Visible}; +#[derive(Clone)] +pub struct Panel { + pub text: String, + pub extent: vk::Extent2D, + pub framebuffer: vk::Framebuffer, + pub vertex_buffer: Buffer, + pub index_buffer: Buffer, + pub egui_context: CtxRef, + pub raw_input: egui::RawInput, + pub input: Option, + pub buttons: Vec, +} + +#[derive(Debug, Clone)] +pub struct PanelInput { + pub cursor_location: Pos2, + pub trigger_value: f32, +} + +#[derive(Debug, Clone)] +pub struct PanelButton { + pub text: String, + pub clicked_this_frame: bool, +} + +impl PanelButton { + pub fn new(text: &str) -> Self { + PanelButton { + text: text.to_string(), + clicked_this_frame: false, + } + } +} + +pub fn create_panel( + text: &str, + width: u32, + height: u32, + vulkan_context: &VulkanContext, + render_context: &RenderContext, + gui_context: &GuiContext, + buttons: Vec, +) -> (Panel, Mesh, Texture, Transform, TransformMatrix, Visible) { + let extent = vk::Extent2D { width, height }; + let output_image = vulkan_context + .create_image( + COLOR_FORMAT, + &extent, + vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::SAMPLED, + 1, + 1, + ) + .unwrap(); + let sampler = vulkan_context + .create_texture_sampler(vk::SamplerAddressMode::REPEAT, 1) + .unwrap(); + let descriptor = vk::DescriptorImageInfo::builder() + .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) + .image_view(output_image.view) + .sampler(sampler) + .build(); + let output_texture = Texture { + image: output_image, + sampler, + descriptor, + }; + let mesh = create_mesh(&output_texture, &vulkan_context, &render_context, &extent); + + let egui_context = CtxRef::default(); + let raw_input = egui::RawInput { + screen_rect: Some(egui::Rect::from_min_size( + Default::default(), + vec2(extent.width as f32, extent.height as f32) / SCALE_FACTOR, + )), + pixels_per_point: Some(SCALE_FACTOR), + time: Some(0.0), + ..Default::default() + }; + + let framebuffer = unsafe { + let attachments = &[output_texture.image.view]; + vulkan_context + .device + .create_framebuffer( + &vk::FramebufferCreateInfo::builder() + .render_pass(gui_context.render_pass) + .attachments(attachments) + .width(extent.width) + .height(extent.height) + .layers(1), + None, + ) + .expect("Failed to create framebuffer.") + }; + + let (vertex_buffer, index_buffer) = create_mesh_buffers(vulkan_context); + + ( + Panel { + text: text.to_string(), + extent, + framebuffer, + vertex_buffer, + index_buffer, + egui_context, + raw_input, + input: Default::default(), + buttons, + }, + mesh, + output_texture, + Default::default(), + Default::default(), + Visible {}, + ) +} + +fn create_mesh( + output_texture: &Texture, + vulkan_context: &VulkanContext, + render_context: &RenderContext, + extent: &vk::Extent2D, +) -> Mesh { + let (material, descriptor_set) = + get_material(&output_texture, &vulkan_context, &render_context); + let (half_width, half_height) = get_panel_dimensions(&extent); + + let positions = [ + vector![-half_width, half_height, 0.], // v0 + vector![half_width, -half_height, 0.], // v1 + vector![half_width, half_height, 0.], // v2 + vector![-half_width, -half_height, 0.], // v3 + ]; + let tex_coords_0 = [ + vector![0., 0.], // v0 + vector![1., 1.], // v1 + vector![1., 0.], // v2 + vector![0., 1.], // v3 + ]; + let vertices: Vec = izip!(positions, tex_coords_0) + .into_iter() + .map(|(p, t)| Vertex { + position: p, + texture_coords_0: t, + ..Default::default() + }) + .collect(); + + let vertex_buffer = Buffer::new( + vulkan_context, + &vertices, + vk::BufferUsageFlags::VERTEX_BUFFER, + ) + .unwrap(); + + let index_buffer = Buffer::new( + vulkan_context, + &[0, 1, 2, 0, 3, 1], + vk::BufferUsageFlags::INDEX_BUFFER, + ) + .unwrap(); + + let primitive = Primitive { + index_buffer, + vertex_buffer, + indicies_count: 6, + material, + texture_descriptor_set: descriptor_set, + }; + + // Create descriptor sets + let descriptor_sets = vulkan_context + .create_mesh_descriptor_sets(render_context.descriptor_set_layouts.mesh_layout, "GUI") + .unwrap(); + let descriptor_sets = [descriptor_sets[0]]; + + let mesh_ubo = MeshUBO::default(); + let ubo_buffer = Buffer::new( + vulkan_context, + &[mesh_ubo], + vk::BufferUsageFlags::UNIFORM_BUFFER, + ) + .unwrap(); + vulkan_context.update_buffer_descriptor_set( + &ubo_buffer, + descriptor_sets[0], + 0, + vk::DescriptorType::UNIFORM_BUFFER, + ); + + Mesh { + descriptor_sets, + ubo_buffer, + ubo_data: mesh_ubo, + primitives: vec![primitive], + } +} + +pub fn get_panel_dimensions(extent: &vk::Extent2D) -> (f32, f32) { + let (width, height) = (extent.width as f32, extent.height as f32); + let (half_width, half_height) = if height > width { + let half_width = (width / height) * 0.5; + (half_width, 0.5) + } else { + let half_height = (height / width) * 0.5; + (0.5, half_height) + }; + (half_width, half_height) +} + +fn get_material( + output_texture: &Texture, + vulkan_context: &VulkanContext, + render_context: &RenderContext, +) -> (Material, vk::DescriptorSet) { + let empty_texture = Texture::empty(&vulkan_context).unwrap(); + // Descriptor set + let descriptor_set = vulkan_context + .create_textures_descriptor_sets( + render_context.descriptor_set_layouts.textures_layout, + "GUI Texture", + &output_texture, + &empty_texture, + &empty_texture, + &empty_texture, + &empty_texture, + ) + .unwrap()[0]; + + let material = Material { + base_colour_factor: vector![1., 1., 1., 1.], + emmissive_factor: Vector4::zeros(), + diffuse_factor: Vector4::zeros(), + specular_factor: Vector4::zeros(), + workflow: 2., + base_color_texture_set: 0, + metallic_roughness_texture_set: -1, + normal_texture_set: -1, + occlusion_texture_set: -1, + emissive_texture_set: -1, + metallic_factor: 0., + roughness_factor: 0., + alpha_mask: 0., + alpha_mask_cutoff: 1., + }; + + (material, descriptor_set) +} + +fn create_mesh_buffers(vulkan_context: &VulkanContext) -> (Buffer, Buffer) { + println!("[HOTHAM_DRAW_GUI] Creating mesh buffers.."); + let vertices = (0..BUFFER_SIZE) + .map(|_| Default::default()) + .collect::>(); + let empty_index_buffer = [0; BUFFER_SIZE * 2]; + + let vertex_buffer = Buffer::new( + &vulkan_context, + &vertices, + vk::BufferUsageFlags::VERTEX_BUFFER, + ) + .expect("Unable to create font index buffer"); + + let index_buffer = Buffer::new( + &vulkan_context, + &empty_index_buffer, + vk::BufferUsageFlags::INDEX_BUFFER, + ) + .expect("Unable to create font index buffer"); + + println!("[HOTHAM_DRAW_GUI] ..done!"); + + (vertex_buffer, index_buffer) +} diff --git a/hotham/src/components/pointer.rs b/hotham/src/components/pointer.rs new file mode 100644 index 00000000..593b7e18 --- /dev/null +++ b/hotham/src/components/pointer.rs @@ -0,0 +1,6 @@ +use super::hand::Handedness; + +pub struct Pointer { + pub handedness: Handedness, + pub trigger_value: f32, +} diff --git a/hotham/src/resources/gui_context.rs b/hotham/src/resources/gui_context.rs new file mode 100644 index 00000000..f47527dc --- /dev/null +++ b/hotham/src/resources/gui_context.rs @@ -0,0 +1,521 @@ +use ash::vk::{self, Handle}; +pub const SCALE_FACTOR: f32 = 3.; + +use crate::{ + components::{panel::PanelInput, Panel}, + resources::render_context::{create_push_constant, CLEAR_VALUES}, + texture::Texture, + COLOR_FORMAT, +}; + +use super::{render_context::create_shader, RenderContext, VulkanContext}; + +#[derive(Debug, Clone)] +pub struct GuiContext { + pub render_pass: vk::RenderPass, + pub pipeline: vk::Pipeline, + pub pipeline_layout: vk::PipelineLayout, + pub font_texture_descriptor_sets: Vec, + pub font_texture_version: u64, + pub hovered_this_frame: bool, + pub hovered_last_frame: bool, +} + +impl GuiContext { + pub fn new(vulkan_context: &VulkanContext) -> Self { + let device = &vulkan_context.device; + + // Descriptor sets, etc + let descriptor_set_layout = unsafe { + device + .create_descriptor_set_layout( + &vk::DescriptorSetLayoutCreateInfo::builder().bindings(&[ + vk::DescriptorSetLayoutBinding::builder() + .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) + .descriptor_count(1) + .binding(0) + .stage_flags(vk::ShaderStageFlags::FRAGMENT) + .build(), + ]), + None, + ) + .expect("Failed to create descriptor set layout.") + }; + let font_texture_descriptor_sets = unsafe { + device.allocate_descriptor_sets( + &vk::DescriptorSetAllocateInfo::builder() + .descriptor_pool(vulkan_context.descriptor_pool) + .set_layouts(&[descriptor_set_layout]), + ) + } + .expect("Failed to create descriptor sets."); + + // Create PipelineLayout + let pipeline_layout = unsafe { + device.create_pipeline_layout( + &vk::PipelineLayoutCreateInfo::builder() + .set_layouts(&[descriptor_set_layout]) + .push_constant_ranges(&[vk::PushConstantRange::builder() + .stage_flags(vk::ShaderStageFlags::VERTEX) + .offset(0) + .size(std::mem::size_of::() as u32 * 2) // screen size + .build()]), + None, + ) + } + .expect("Failed to create pipeline layout."); + + vulkan_context + .set_debug_name( + vk::ObjectType::PIPELINE_LAYOUT, + pipeline_layout.as_raw(), + "GUI Pipeline Layout", + ) + .unwrap(); + + // Create render pass + let render_pass = unsafe { + device.create_render_pass( + &vk::RenderPassCreateInfo::builder() + .attachments(&[vk::AttachmentDescription::builder() + .format(COLOR_FORMAT) + .samples(vk::SampleCountFlags::TYPE_1) + .load_op(vk::AttachmentLoadOp::CLEAR) + .store_op(vk::AttachmentStoreOp::STORE) + .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE) + .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE) + .initial_layout(vk::ImageLayout::UNDEFINED) + .final_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) + .build()]) + .subpasses(&[vk::SubpassDescription::builder() + .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS) + .color_attachments(&[vk::AttachmentReference::builder() + .attachment(0) + .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .build()]) + .build()]) + .dependencies(&[vk::SubpassDependency::builder() + .src_subpass(0) + .dst_subpass(vk::SUBPASS_EXTERNAL) + .src_access_mask( + vk::AccessFlags::MEMORY_READ | vk::AccessFlags::MEMORY_WRITE, + ) + .dst_access_mask( + vk::AccessFlags::MEMORY_READ | vk::AccessFlags::MEMORY_WRITE, + ) + .src_stage_mask(vk::PipelineStageFlags::ALL_GRAPHICS) + .dst_stage_mask(vk::PipelineStageFlags::ALL_GRAPHICS) + .build()]), + None, + ) + } + .expect("Failed to create render pass."); + + // Create Pipeline + let pipeline = { + let bindings = [vk::VertexInputBindingDescription::builder() + .binding(0) + .input_rate(vk::VertexInputRate::VERTEX) + .stride( + 4 * std::mem::size_of::() as u32 + 4 * std::mem::size_of::() as u32, + ) + .build()]; + + let attributes = [ + // position + vk::VertexInputAttributeDescription::builder() + .binding(0) + .offset(0) + .location(0) + .format(vk::Format::R32G32_SFLOAT) + .build(), + // uv + vk::VertexInputAttributeDescription::builder() + .binding(0) + .offset(8) + .location(1) + .format(vk::Format::R32G32_SFLOAT) + .build(), + // color + vk::VertexInputAttributeDescription::builder() + .binding(0) + .offset(16) + .location(2) + .format(vk::Format::R8G8B8A8_UNORM) + .build(), + ]; + + // Vertex shader stage + let (vertex_shader, vertex_stage) = create_shader( + include_bytes!("../../shaders/gui.vert.spv"), + vk::ShaderStageFlags::VERTEX, + &vulkan_context, + ) + .expect("Unable to create vertex shader"); + + // Fragment shader stage + let (fragment_shader, fragment_stage) = create_shader( + include_bytes!("../../shaders/gui.frag.spv"), + vk::ShaderStageFlags::FRAGMENT, + &vulkan_context, + ) + .expect("Unable to create fragment shader"); + + let pipeline_shader_stages = [vertex_stage, fragment_stage]; + + let input_assembly_info = vk::PipelineInputAssemblyStateCreateInfo::builder() + .topology(vk::PrimitiveTopology::TRIANGLE_LIST); + let viewport_info = vk::PipelineViewportStateCreateInfo::builder() + .viewport_count(1) + .scissor_count(1); + let rasterization_info = vk::PipelineRasterizationStateCreateInfo::builder() + .depth_clamp_enable(false) + .rasterizer_discard_enable(false) + .polygon_mode(vk::PolygonMode::FILL) + .cull_mode(vk::CullModeFlags::NONE) + .front_face(vk::FrontFace::COUNTER_CLOCKWISE) + .depth_bias_enable(false) + .line_width(1.0); + let stencil_op = vk::StencilOpState::builder() + .fail_op(vk::StencilOp::KEEP) + .pass_op(vk::StencilOp::KEEP) + .compare_op(vk::CompareOp::ALWAYS) + .build(); + let depth_stencil_info = vk::PipelineDepthStencilStateCreateInfo::builder() + .depth_test_enable(false) + .depth_write_enable(false) + .depth_compare_op(vk::CompareOp::ALWAYS) + .depth_bounds_test_enable(false) + .stencil_test_enable(false) + .front(stencil_op) + .back(stencil_op); + let color_blend_attachments = [vk::PipelineColorBlendAttachmentState::builder() + .color_write_mask( + vk::ColorComponentFlags::R + | vk::ColorComponentFlags::G + | vk::ColorComponentFlags::B + | vk::ColorComponentFlags::A, + ) + .blend_enable(true) + .src_color_blend_factor(vk::BlendFactor::ONE) + .dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA) + .build()]; + let color_blend_info = vk::PipelineColorBlendStateCreateInfo::builder() + .attachments(&color_blend_attachments); + let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR]; + let dynamic_state_info = + vk::PipelineDynamicStateCreateInfo::builder().dynamic_states(&dynamic_states); + let vertex_input_state = vk::PipelineVertexInputStateCreateInfo::builder() + .vertex_attribute_descriptions(&attributes) + .vertex_binding_descriptions(&bindings); + let multisample_info = vk::PipelineMultisampleStateCreateInfo::builder() + .rasterization_samples(vk::SampleCountFlags::TYPE_1); + + let pipeline_create_info = [vk::GraphicsPipelineCreateInfo::builder() + .stages(&pipeline_shader_stages) + .vertex_input_state(&vertex_input_state) + .input_assembly_state(&input_assembly_info) + .viewport_state(&viewport_info) + .rasterization_state(&rasterization_info) + .multisample_state(&multisample_info) + .depth_stencil_state(&depth_stencil_info) + .color_blend_state(&color_blend_info) + .dynamic_state(&dynamic_state_info) + .layout(pipeline_layout) + .render_pass(render_pass) + .subpass(0) + .build()]; + + let pipeline = unsafe { + device.create_graphics_pipelines( + vk::PipelineCache::null(), + &pipeline_create_info, + None, + ) + } + .expect("Failed to create graphics pipeline.")[0]; + unsafe { + device.destroy_shader_module(vertex_shader, None); + device.destroy_shader_module(fragment_shader, None); + } + vulkan_context + .set_debug_name(vk::ObjectType::PIPELINE, pipeline.as_raw(), "GUI Pipeline") + .unwrap(); + pipeline + }; + + Self { + render_pass, + pipeline, + pipeline_layout, + font_texture_descriptor_sets, + font_texture_version: 0, + hovered_last_frame: false, + hovered_this_frame: false, + } + } + + pub fn paint_gui( + &mut self, + vulkan_context: &VulkanContext, + render_context: &RenderContext, + current_swapchain_image_index: usize, + panel: &mut Panel, + ) { + let device = &vulkan_context.device; + let frame = &render_context.frames[current_swapchain_image_index]; + let command_buffer = frame.command_buffer; + let framebuffer = panel.framebuffer; + let extent = panel.extent; + let (raw_input, panel_input) = handle_panel_input(panel); + + let text = panel.text.clone(); + let mut updated_buttons = panel.buttons.clone(); + let egui_context = &mut panel.egui_context; + + egui_context.begin_frame(raw_input); + let inner_layout = egui::Layout::from_main_dir_and_cross_align( + egui::Direction::TopDown, + egui::Align::Center, + ); + + // GUI Layout + egui::CentralPanel::default().show(&egui_context, |ui| { + ui.with_layout(inner_layout, |ui| { + for button in &mut updated_buttons { + let response = ui.button(&button.text); + + if response.hovered() { + self.hovered_this_frame = true; + } + + if response.clicked() { + button.clicked_this_frame = true; + } + } + + ui.label(&text); + + if let Some(panel_input) = panel_input { + let position = ui + .painter() + .round_pos_to_pixels(panel_input.cursor_location); + let cursor_colour = if panel_input.trigger_value > 0.9 { + egui::Color32::LIGHT_BLUE + } else { + egui::Color32::WHITE + }; + ui.painter().circle_filled(position, 4.00, cursor_colour); + } + ui.allocate_space(ui.available_size()) + }) + }); + + let (_output, shapes) = egui_context.end_frame(); + + let texture = &egui_context.fonts().texture(); + if texture.version != self.font_texture_version { + let _font_texture = update_font_texture( + &vulkan_context, + texture, + self.font_texture_descriptor_sets[0], + ); + self.font_texture_version = texture.version; + } + + let clipped_meshes = egui_context.tessellate(shapes); + panel.buttons = updated_buttons; + let vertex_buffer = &panel.vertex_buffer; + let index_buffer = &panel.index_buffer; + + // begin render pass + unsafe { + device.cmd_begin_render_pass( + command_buffer, + &vk::RenderPassBeginInfo::builder() + .render_pass(self.render_pass) + .framebuffer(framebuffer) + .clear_values(&[CLEAR_VALUES[0]]) + .render_area(vk::Rect2D::builder().extent(extent).build()), + vk::SubpassContents::INLINE, + ); + device.cmd_bind_pipeline( + command_buffer, + vk::PipelineBindPoint::GRAPHICS, + self.pipeline, + ); + device.cmd_bind_vertex_buffers(command_buffer, 0, &[vertex_buffer.handle], &[0]); + device.cmd_bind_index_buffer( + command_buffer, + index_buffer.handle, + 0, + vk::IndexType::UINT32, + ); + device.cmd_set_viewport( + command_buffer, + 0, + &[vk::Viewport::builder() + .x(0.0) + .y(0.0) + .width(extent.width as f32) + .height(extent.height as f32) + .min_depth(0.0) + .max_depth(1.0) + .build()], + ); + + // Set push contants + let width_points = extent.width as f32 / SCALE_FACTOR; + let height_points = extent.height as f32 / SCALE_FACTOR; + device.cmd_push_constants( + command_buffer, + self.pipeline_layout, + vk::ShaderStageFlags::VERTEX, + 0, + create_push_constant(&width_points), + ); + device.cmd_push_constants( + command_buffer, + self.pipeline_layout, + vk::ShaderStageFlags::VERTEX, + std::mem::size_of_val(&width_points) as u32, + create_push_constant(&height_points), + ); + + // Bind descriptor sets + device.cmd_bind_descriptor_sets( + command_buffer, + vk::PipelineBindPoint::GRAPHICS, + self.pipeline_layout, + 0, + &self.font_texture_descriptor_sets, + &[], + ); + } + + for egui::ClippedMesh(rect, mesh) in &clipped_meshes { + // Update vertex buffer + vertex_buffer + .update(&vulkan_context, &mesh.vertices) + .unwrap(); + + // Update index buffer + index_buffer.update(&vulkan_context, &mesh.indices).unwrap(); + + // record draw commands + unsafe { + let width = extent.width as f32; + let height = extent.height as f32; + + let min = rect.min; + let min = egui::Pos2 { + x: min.x * SCALE_FACTOR, + y: min.y * SCALE_FACTOR, + }; + let min = egui::Pos2 { + x: f32::clamp(min.x, 0.0, width), + y: f32::clamp(min.y, 0.0, height), + }; + let max = rect.max; + let max = egui::Pos2 { + x: max.x * SCALE_FACTOR, + y: max.y * SCALE_FACTOR, + }; + let max = egui::Pos2 { + x: f32::clamp(max.x, min.x, width), + y: f32::clamp(max.y, min.y, height), + }; + device.cmd_set_scissor( + command_buffer, + 0, + &[vk::Rect2D::builder() + .offset( + vk::Offset2D::builder() + .x(min.x.round() as i32) + .y(min.y.round() as i32) + .build(), + ) + .extent( + vk::Extent2D::builder() + .width((max.x.round() - min.x) as u32) + .height((max.y.round() - min.y) as u32) + .build(), + ) + .build()], + ); + + device.cmd_draw_indexed(command_buffer, mesh.indices.len() as u32, 1, 0, 0, 0); + } + } + + unsafe { + device.cmd_end_render_pass(command_buffer); + } + } +} + +fn handle_panel_input(panel: &mut Panel) -> (egui::RawInput, Option) { + let mut raw_input = panel.raw_input.clone(); + let panel_input = panel.input.take(); + if let Some(input) = &panel_input { + let pos = input.cursor_location; + raw_input.events.push(egui::Event::PointerMoved(pos)); + if input.trigger_value >= 0. { + raw_input.events.push(egui::Event::PointerButton { + pos, + button: egui::PointerButton::Primary, + pressed: input.trigger_value > 0.9, + modifiers: Default::default(), + }); + } + } else { + raw_input.events.push(egui::Event::PointerGone); + } + + println!("[HOTHAM_GUI] RawInput.events: {:?}", raw_input.events); + + return (raw_input, panel_input); +} + +fn update_font_texture( + vulkan_context: &VulkanContext, + texture: &egui::Texture, + descriptor_set: vk::DescriptorSet, +) -> Texture { + unsafe { + vulkan_context + .device + .device_wait_idle() + .expect("Failed to wait device idle"); + } + + let image_buf = texture + .pixels + .iter() + .flat_map(|&r| vec![r, r, r, r]) + .collect::>(); + + let texture = Texture::new( + "Font texture", + vulkan_context, + &image_buf, + texture.width as u32, + texture.height as u32, + COLOR_FORMAT, + ) + .expect("Unable to create font texture"); + + unsafe { + vulkan_context.device.update_descriptor_sets( + &[vk::WriteDescriptorSet::builder() + .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) + .dst_set(descriptor_set) + .image_info(&[texture.descriptor]) + .dst_binding(0) + .build()], + &[], + ); + } + + texture +} diff --git a/hotham/src/resources/haptic_context.rs b/hotham/src/resources/haptic_context.rs new file mode 100644 index 00000000..9c83d306 --- /dev/null +++ b/hotham/src/resources/haptic_context.rs @@ -0,0 +1,12 @@ +#[derive(Clone, Debug, Default)] +pub struct HapticContext { + pub amplitude_this_frame: f32, +} + +impl HapticContext { + pub fn request_haptic_feedback(&mut self, amplitude: f32) { + if amplitude > self.amplitude_this_frame { + self.amplitude_this_frame = amplitude; + } + } +} diff --git a/hotham/src/resources/mod.rs b/hotham/src/resources/mod.rs index a2a9570b..7d9f2911 100644 --- a/hotham/src/resources/mod.rs +++ b/hotham/src/resources/mod.rs @@ -1,8 +1,12 @@ +pub mod gui_context; +pub mod haptic_context; pub mod physics_context; pub mod render_context; pub mod vulkan_context; pub mod xr_context; +pub use gui_context::GuiContext; +pub use haptic_context::HapticContext; pub use physics_context::PhysicsContext; pub use render_context::RenderContext; pub(crate) use vulkan_context::VulkanContext; diff --git a/hotham/src/resources/physics_context.rs b/hotham/src/resources/physics_context.rs index 433b3252..6185a01e 100644 --- a/hotham/src/resources/physics_context.rs +++ b/hotham/src/resources/physics_context.rs @@ -6,6 +6,9 @@ use rapier3d::prelude::*; use crate::components::{Collider as ColliderComponent, RigidBody as RigidBodyComponent}; use crate::util::entity_to_u64; +pub const DEFAULT_COLLISION_GROUP: u32 = 0b01; +pub const PANEL_COLLISION_GROUP: u32 = 0b10; + pub struct PhysicsContext { pub physics_pipeline: PhysicsPipeline, pub gravity: Matrix3x1, @@ -68,6 +71,9 @@ impl PhysicsContext { &(), &self.event_handler, ); + + self.query_pipeline + .update(&self.island_manager, &self.rigid_bodies, &self.colliders); } pub fn add_rigid_body_and_collider( @@ -79,6 +85,12 @@ impl PhysicsContext { collider.user_data = entity_to_u64(entity) as _; let rigid_body_handle = self.rigid_bodies.insert(rigid_body); + // TODO: Users may wish to pass in their own interaction groups. + collider.set_collision_groups(InteractionGroups::new( + DEFAULT_COLLISION_GROUP, + DEFAULT_COLLISION_GROUP, + )); + let a_collider_handle = self.colliders .insert_with_parent(collider, rigid_body_handle, &mut self.rigid_bodies); diff --git a/hotham/src/resources/render_context.rs b/hotham/src/resources/render_context.rs index c4f5d9c5..0daa4040 100644 --- a/hotham/src/resources/render_context.rs +++ b/hotham/src/resources/render_context.rs @@ -1,6 +1,6 @@ use std::{ffi::CStr, io::Cursor, mem::size_of, time::Instant}; -static CLEAR_VALUES: [vk::ClearValue; 2] = [ +pub static CLEAR_VALUES: [vk::ClearValue; 2] = [ vk::ClearValue { color: vk::ClearColorValue { float32: [0.0, 0.0, 0.0, 1.0], @@ -281,14 +281,36 @@ impl RenderContext { Ok(()) } - pub(crate) fn begin_render_pass( + pub(crate) fn begin_frame( &self, vulkan_context: &VulkanContext, - available_swapchain_image_index: usize, + swapchain_image_index: usize, + ) { + // Get the values we need to start the frame.. + let device = &vulkan_context.device; + let frame = &self.frames[swapchain_image_index]; + let command_buffer = frame.command_buffer; + + // Begin recording the command buffer. + unsafe { + device + .begin_command_buffer( + command_buffer, + &vk::CommandBufferBeginInfo::builder() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT), + ) + .unwrap(); + } + } + + pub(crate) fn begin_pbr_render_pass( + &self, + vulkan_context: &VulkanContext, + swapchain_image_index: usize, ) { // Get the values we need to start a renderpass let device = &vulkan_context.device; - let frame = &self.frames[available_swapchain_image_index]; + let frame = &self.frames[swapchain_image_index]; let command_buffer = frame.command_buffer; let framebuffer = frame.framebuffer; @@ -300,13 +322,6 @@ impl RenderContext { .clear_values(&CLEAR_VALUES); unsafe { - device - .begin_command_buffer( - command_buffer, - &vk::CommandBufferBeginInfo::builder() - .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT), - ) - .unwrap(); device.cmd_begin_render_pass( command_buffer, &render_pass_begin_info, @@ -328,20 +343,32 @@ impl RenderContext { } } - pub(crate) fn end_render_pass( + pub(crate) fn end_pbr_render_pass( &mut self, vulkan_context: &VulkanContext, - available_swapchain_image_index: usize, + swapchain_image_index: usize, + ) { + let device = &vulkan_context.device; + let frame = &self.frames[swapchain_image_index]; + let command_buffer = frame.command_buffer; + unsafe { + device.cmd_end_render_pass(command_buffer); + } + } + + pub(crate) fn end_frame( + &mut self, + vulkan_context: &VulkanContext, + swapchain_image_index: usize, ) { // Get the values we need to end the renderpass let device = &vulkan_context.device; - let frame = &self.frames[available_swapchain_image_index]; + let frame = &self.frames[swapchain_image_index]; let command_buffer = frame.command_buffer; let graphics_queue = vulkan_context.graphics_queue; // End the render pass and submit. unsafe { - device.cmd_end_render_pass(command_buffer); device.end_command_buffer(command_buffer).unwrap(); let fence = frame.fence; let submit_info = vk::SubmitInfo::builder() @@ -808,7 +835,7 @@ fn create_pipeline( Ok(primary_pipeline) } -fn create_shader( +pub fn create_shader( shader_code: &[u8], stage: vk::ShaderStageFlags, vulkan_context: &VulkanContext, diff --git a/hotham/src/resources/vulkan_context.rs b/hotham/src/resources/vulkan_context.rs index 54345504..c3c38571 100644 --- a/hotham/src/resources/vulkan_context.rs +++ b/hotham/src/resources/vulkan_context.rs @@ -641,7 +641,7 @@ impl VulkanContext { } } - fn create_texture_sampler( + pub fn create_texture_sampler( &self, address_mode: vk::SamplerAddressMode, mip_count: u32, diff --git a/hotham/src/resources/xr_context.rs b/hotham/src/resources/xr_context.rs index 062c5631..38215817 100644 --- a/hotham/src/resources/xr_context.rs +++ b/hotham/src/resources/xr_context.rs @@ -5,8 +5,8 @@ use openxr::{ SessionState, Space, Swapchain, Vulkan, }; use xr::{ - vulkan::SessionCreateInfo, Duration, FrameState, ReferenceSpaceType, SwapchainCreateFlags, - SwapchainCreateInfo, SwapchainUsageFlags, Time, View, + vulkan::SessionCreateInfo, Duration, FrameState, Haptic, ReferenceSpaceType, + SwapchainCreateFlags, SwapchainCreateInfo, SwapchainUsageFlags, Time, View, }; use crate::{resources::VulkanContext, BLEND_MODE, COLOR_FORMAT, VIEW_COUNT, VIEW_TYPE}; @@ -20,10 +20,14 @@ pub struct XrContext { pub action_set: ActionSet, pub pose_action: Action, pub grab_action: Action, + pub trigger_action: Action, + pub haptic_feedback_action: Action, pub left_hand_space: Space, pub left_hand_subaction_path: Path, + pub left_pointer_space: Space, pub right_hand_space: Space, pub right_hand_subaction_path: Path, + pub right_pointer_space: Space, pub swapchain_resolution: vk::Extent2D, pub frame_waiter: FrameWaiter, pub frame_stream: FrameStream, @@ -59,16 +63,32 @@ impl XrContext { let left_hand_pose_path = instance .string_to_path("/user/hand/left/input/grip/pose") .unwrap(); + let left_pointer_path = instance + .string_to_path("/user/hand/left/input/aim/pose") + .unwrap(); let right_hand_pose_path = instance .string_to_path("/user/hand/right/input/grip/pose") .unwrap(); + let right_pointer_path = instance + .string_to_path("/user/hand/right/input/aim/pose") + .unwrap(); let left_hand_grip_squeeze_path = instance .string_to_path("/user/hand/left/input/squeeze/value") .unwrap(); + let left_hand_grip_trigger_path = instance + .string_to_path("/user/hand/left/input/trigger/value") + .unwrap(); + let right_hand_grip_squeeze_path = instance .string_to_path("/user/hand/right/input/squeeze/value") .unwrap(); + let right_hand_grip_trigger_path = instance + .string_to_path("/user/hand/right/input/trigger/value") + .unwrap(); + let right_hand_haptic_feedback_path = instance + .string_to_path("/user/hand/right/output/haptic") + .unwrap(); let pose_action = action_set.create_action::( "hand_pose", @@ -76,12 +96,30 @@ impl XrContext { &[left_hand_subaction_path, right_hand_subaction_path], )?; + let aim_action = action_set.create_action::( + "pointer_pose", + "Pointer Pose", + &[left_hand_subaction_path, right_hand_subaction_path], + )?; + + let trigger_action = action_set.create_action::( + "trigger_pulled", + "Pull Trigger", + &[left_hand_subaction_path, right_hand_subaction_path], + )?; + let grab_action = action_set.create_action::( "grab_object", "Grab Object", &[left_hand_subaction_path, right_hand_subaction_path], )?; + let haptic_feedback_action = action_set.create_action::( + "haptic_feedback", + "Haptic Feedback", + &[left_hand_subaction_path, right_hand_subaction_path], + )?; + // Bind our actions to input devices using the given profile instance.suggest_interaction_profile_bindings( instance @@ -90,18 +128,29 @@ impl XrContext { &[ xr::Binding::new(&pose_action, left_hand_pose_path), xr::Binding::new(&pose_action, right_hand_pose_path), + xr::Binding::new(&aim_action, left_pointer_path), + xr::Binding::new(&aim_action, right_pointer_path), xr::Binding::new(&grab_action, left_hand_grip_squeeze_path), xr::Binding::new(&grab_action, right_hand_grip_squeeze_path), + xr::Binding::new(&trigger_action, left_hand_grip_trigger_path), + xr::Binding::new(&trigger_action, right_hand_grip_trigger_path), + xr::Binding::new(&grab_action, right_hand_grip_squeeze_path), + xr::Binding::new(&haptic_feedback_action, right_hand_haptic_feedback_path), ], )?; let left_hand_space = pose_action.create_space(session.clone(), left_hand_subaction_path, Posef::IDENTITY)?; + let left_pointer_space = + aim_action.create_space(session.clone(), left_hand_subaction_path, Posef::IDENTITY)?; + let right_hand_space = pose_action.create_space( session.clone(), right_hand_subaction_path, Posef::IDENTITY, )?; + let right_pointer_space = + aim_action.create_space(session.clone(), left_hand_subaction_path, Posef::IDENTITY)?; let frame_state = FrameState { predicted_display_time: Time::from_nanos(0), @@ -119,10 +168,14 @@ impl XrContext { reference_space, action_set, pose_action, + trigger_action, grab_action, + haptic_feedback_action, left_hand_space, + left_pointer_space, left_hand_subaction_path, right_hand_space, + right_pointer_space, right_hand_subaction_path, swapchain_resolution, frame_waiter, diff --git a/hotham/src/schedule_functions/apply_haptic_feedback.rs b/hotham/src/schedule_functions/apply_haptic_feedback.rs new file mode 100644 index 00000000..fb635ddb --- /dev/null +++ b/hotham/src/schedule_functions/apply_haptic_feedback.rs @@ -0,0 +1,38 @@ +use legion::{Resources, World}; +use openxr::{Duration, HapticVibration}; + +use crate::resources::{HapticContext, XrContext}; + +pub fn apply_haptic_feedback(_: &mut World, resources: &mut Resources) { + let mut haptic_context = resources + .get_mut::() + .expect("Unable to get HapticContext"); + + if haptic_context.amplitude_this_frame == 0. { + return; + } + + let xr_context = resources + .get_mut::() + .expect("Unable to get XrContext"); + + let duration = Duration::from_nanos(1e+7 as _); + let frequency = 180.; + + let event = HapticVibration::new() + .amplitude(haptic_context.amplitude_this_frame) + .frequency(frequency) + .duration(duration); + + xr_context + .haptic_feedback_action + .apply_feedback( + &xr_context.session, + xr_context.right_hand_subaction_path, + &event, + ) + .expect("Unable to apply haptic feedback!"); + + // Reset the value + haptic_context.amplitude_this_frame = 0.; +} diff --git a/hotham/src/schedule_functions/begin_frame.rs b/hotham/src/schedule_functions/begin_frame.rs index 0cda41a5..7ff8c7c8 100644 --- a/hotham/src/schedule_functions/begin_frame.rs +++ b/hotham/src/schedule_functions/begin_frame.rs @@ -2,14 +2,15 @@ use legion::{Resources, World}; use openxr::ActiveActionSet; use crate::{ - resources::xr_context::XrContext, resources::RenderContext, resources::VulkanContext, VIEW_TYPE, + resources::{xr_context::XrContext, RenderContext, VulkanContext}, + VIEW_TYPE, }; pub fn begin_frame(_world: &mut World, resources: &mut Resources) { // Get resources let mut xr_context = resources.get_mut::().unwrap(); - let mut render_context = resources.get_mut::().unwrap(); let mut current_swapchain_image_index = resources.get_mut::().unwrap(); + let render_context = resources.get_mut::().unwrap(); let vulkan_context = resources.get::().unwrap(); let active_action_set = ActiveActionSet::new(&xr_context.action_set); @@ -32,17 +33,9 @@ pub fn begin_frame(_world: &mut World, resources: &mut Resources) { &xr_context.reference_space, ) .unwrap(); - - // Update uniform buffers - // TODO: We should do this ourselves. - render_context - .update_scene_data(&views, &vulkan_context) - .unwrap(); xr_context.views = views; - // Begin the renderpass. - render_context.begin_render_pass(&vulkan_context, available_swapchain_image_index); - // ..and we're off! + render_context.begin_frame(&vulkan_context, available_swapchain_image_index); } #[cfg(test)] diff --git a/hotham/src/schedule_functions/begin_pbr_renderpass.rs b/hotham/src/schedule_functions/begin_pbr_renderpass.rs new file mode 100644 index 00000000..9724447b --- /dev/null +++ b/hotham/src/schedule_functions/begin_pbr_renderpass.rs @@ -0,0 +1,21 @@ +use crate::{resources::xr_context::XrContext, resources::RenderContext, resources::VulkanContext}; +use legion::{Resources, World}; +pub fn begin_pbr_renderpass(_world: &mut World, resources: &mut Resources) { + // Get resources + let xr_context = resources.get_mut::().unwrap(); + let mut render_context = resources.get_mut::().unwrap(); + let current_swapchain_image_index = resources.get_mut::().unwrap(); + let vulkan_context = resources.get::().unwrap(); + + // Get views from OpenXR + let views = &xr_context.views; + + // Update uniform buffers + render_context + .update_scene_data(&views, &vulkan_context) + .unwrap(); + + // Begin the renderpass. + render_context.begin_pbr_render_pass(&vulkan_context, *current_swapchain_image_index); + // ..and we're off! +} diff --git a/hotham/src/schedule_functions/end_frame.rs b/hotham/src/schedule_functions/end_frame.rs index 66823587..a3fad3af 100644 --- a/hotham/src/schedule_functions/end_frame.rs +++ b/hotham/src/schedule_functions/end_frame.rs @@ -4,10 +4,11 @@ use legion::{Resources, World}; pub fn end_frame(_world: &mut World, resources: &mut Resources) { // Get resources let mut xr_context = resources.get_mut::().unwrap(); - let vulkan_context = resources.get::().unwrap(); let mut render_context = resources.get_mut::().unwrap(); - let swapchain_image_index = resources.get::().unwrap(); - render_context.end_render_pass(&vulkan_context, *swapchain_image_index); + let current_swapchain_image_index = resources.get_mut::().unwrap(); + let vulkan_context = resources.get::().unwrap(); + + render_context.end_frame(&vulkan_context, *current_swapchain_image_index); xr_context.end_frame().unwrap(); } diff --git a/hotham/src/schedule_functions/end_pbr_renderpass.rs b/hotham/src/schedule_functions/end_pbr_renderpass.rs new file mode 100644 index 00000000..d922e303 --- /dev/null +++ b/hotham/src/schedule_functions/end_pbr_renderpass.rs @@ -0,0 +1,9 @@ +use crate::{resources::RenderContext, resources::VulkanContext}; +use legion::{Resources, World}; +pub fn end_pbr_renderpass(_world: &mut World, resources: &mut Resources) { + // Get resources + let mut render_context = resources.get_mut::().unwrap(); + let current_swapchain_image_index = resources.get_mut::().unwrap(); + let vulkan_context = resources.get::().unwrap(); + render_context.end_pbr_render_pass(&vulkan_context, *current_swapchain_image_index); +} diff --git a/hotham/src/schedule_functions/mod.rs b/hotham/src/schedule_functions/mod.rs index 20d61b77..6cdfaa5b 100644 --- a/hotham/src/schedule_functions/mod.rs +++ b/hotham/src/schedule_functions/mod.rs @@ -1,9 +1,15 @@ +pub mod apply_haptic_feedback; pub mod begin_frame; +pub mod begin_pbr_renderpass; pub mod end_frame; +pub mod end_pbr_renderpass; pub mod physics_step; pub mod sync_debug_server; +pub use apply_haptic_feedback::apply_haptic_feedback; pub use begin_frame::begin_frame; +pub use begin_pbr_renderpass::begin_pbr_renderpass; pub use end_frame::end_frame; +pub use end_pbr_renderpass::end_pbr_renderpass; pub use physics_step::physics_step; pub use sync_debug_server::sync_debug_server; diff --git a/hotham/src/shaders/gui.frag b/hotham/src/shaders/gui.frag new file mode 100644 index 00000000..a300479c --- /dev/null +++ b/hotham/src/shaders/gui.frag @@ -0,0 +1,13 @@ +#version 450 + +layout(location = 0) in vec4 inColor; +layout(location = 1) in vec2 inUV; + +layout(location = 0) out vec4 outColor; + +layout(binding = 0, set = 0) uniform sampler2D font_texture; + +void main() { + outColor = inColor * texture(font_texture, inUV); + // outColor = vec4(1.0, 1.0, 1.0, 1.0); +} diff --git a/hotham/src/shaders/gui.vert b/hotham/src/shaders/gui.vert new file mode 100644 index 00000000..97af8ea8 --- /dev/null +++ b/hotham/src/shaders/gui.vert @@ -0,0 +1,27 @@ +// Adapted from https://github.com/MatchaChoco010/egui-winit-ash-integration/blob/main/src/shaders/src/vert.vert +#version 450 + +layout(location = 0) in vec2 inPos; +layout(location = 1) in vec2 inUV; +layout(location = 2) in vec4 inColor; + +layout(location = 0) out vec4 outColor; +layout(location = 1) out vec2 outUV; + +layout(push_constant) uniform PushConstants { vec2 screen_size; } +pushConstants; + +vec3 srgb_to_linear(vec3 srgb) { + bvec3 cutoff = lessThan(srgb, vec3(0.04045)); + vec3 lower = srgb / vec3(12.92); + vec3 higher = pow((srgb + vec3(0.055)) / vec3(1.055), vec3(2.4)); + return mix(higher, lower, cutoff); +} + +void main() { + gl_Position = + vec4(2.0 * inPos.x / pushConstants.screen_size.x - 1.0, + 2.0 * inPos.y / pushConstants.screen_size.y - 1.0, 0.0, 1.0); + outColor = vec4(srgb_to_linear(inColor.rgb), inColor.a); + outUV = inUV; +} diff --git a/hotham/src/shaders/pbr.frag b/hotham/src/shaders/pbr.frag index 75b234ee..c65c7d7d 100644 --- a/hotham/src/shaders/pbr.frag +++ b/hotham/src/shaders/pbr.frag @@ -80,6 +80,7 @@ const float c_MinRoughness = 0.04; const float PBR_WORKFLOW_METALLIC_ROUGHNESS = 0.0; const float PBR_WORKFLOW_SPECULAR_GLOSINESS = 1.0f; +const float PBR_WORKFLOW_UNLIT = 2.0f; #define MANUAL_SRGB 1 vec3 Uncharted2Tonemap(vec3 color) @@ -354,6 +355,11 @@ void main() outColor = vec4(color, baseColor.a); + if (material.workflow == PBR_WORKFLOW_UNLIT) { + outColor = (texture(colorMap, material.baseColorTextureSet == 0 ? inUV0 : inUV1) * material.baseColorFactor); + outColor.a = 1; + } + // vec3 N = normalize(inNormal); // vec3 L = normalize(inLightVec); // vec3 V = normalize(inViewVec); diff --git a/hotham/src/shaders/pbr.frag.spv b/hotham/src/shaders/pbr.frag.spv deleted file mode 100644 index 24da8d0c..00000000 Binary files a/hotham/src/shaders/pbr.frag.spv and /dev/null differ diff --git a/hotham/src/shaders/pbr.vert b/hotham/src/shaders/pbr.vert index 0a648f81..e14d4b83 100644 --- a/hotham/src/shaders/pbr.vert +++ b/hotham/src/shaders/pbr.vert @@ -53,6 +53,11 @@ void main() locPos = node.matrix * vec4(inPos, 1.0); outNormal = normalize(transpose(inverse(mat3(node.matrix))) * inNormal); } + + if (length(inNormal) == 0.0) { + outNormal = inNormal; + } + outWorldPos = locPos.xyz / locPos.w; outUV0 = inUV0; outUV1 = inUV1; diff --git a/hotham/src/shaders/pbr.vert.spv b/hotham/src/shaders/pbr.vert.spv deleted file mode 100644 index 7c4d2aa0..00000000 Binary files a/hotham/src/shaders/pbr.vert.spv and /dev/null differ diff --git a/hotham/src/systems/draw_gui.rs b/hotham/src/systems/draw_gui.rs new file mode 100644 index 00000000..2ce61f78 --- /dev/null +++ b/hotham/src/systems/draw_gui.rs @@ -0,0 +1,340 @@ +use legion::world::SubWorld; +use legion::{system, IntoQuery}; + +use crate::components::Panel; +use crate::resources::{GuiContext, HapticContext}; +use crate::resources::{RenderContext, VulkanContext}; + +#[system] +#[write_component(Panel)] +pub fn draw_gui( + world: &mut SubWorld, + #[resource] vulkan_context: &VulkanContext, + #[resource] swapchain_image_index: &usize, + #[resource] render_context: &RenderContext, + #[resource] gui_context: &mut GuiContext, + #[resource] haptic_context: &mut HapticContext, +) { + // Reset hovered_this_frame + gui_context.hovered_this_frame = false; + + // Draw each panel + let mut query = <&mut Panel>::query(); + query.for_each_mut(world, |panel| { + // Reset the button state + for button in &mut panel.buttons { + button.clicked_this_frame = false; + } + + gui_context.paint_gui( + &vulkan_context, + &render_context, + *swapchain_image_index, + panel, + ); + }); + + // Did we hover over a button in this frame? If so request haptic feedback. + if !gui_context.hovered_last_frame && gui_context.hovered_this_frame { + haptic_context.request_haptic_feedback(1.); + } + + // Stash the value for the next frame. + gui_context.hovered_last_frame = gui_context.hovered_this_frame; +} + +#[cfg(test)] +mod tests { + use std::process::Command; + + use ash::vk::{self, Handle}; + use egui::Pos2; + use image::{jpeg::JpegEncoder, DynamicImage, RgbaImage}; + use legion::{IntoQuery, Resources, Schedule, World}; + use nalgebra::UnitQuaternion; + use openxr::{Fovf, Quaternionf, Vector3f}; + use renderdoc::RenderDoc; + + use crate::{ + buffer::Buffer, + components::{ + panel::{create_panel, PanelButton, PanelInput}, + Panel, + }, + gltf_loader, + image::Image, + resources::{ + gui_context::SCALE_FACTOR, GuiContext, HapticContext, RenderContext, VulkanContext, + }, + scene_data::SceneParams, + swapchain::Swapchain, + systems::{ + rendering_system, update_parent_transform_matrix_system, update_transform_matrix_system, + }, + util::get_from_device_memory, + COLOR_FORMAT, + }; + + use super::draw_gui_system; + + #[test] + pub fn test_draw_gui() { + let resolution = vk::Extent2D { + height: 800, + width: 800, + }; + let (mut world, mut resources, image, mut schedule) = setup(resolution.clone()); + + let mut renderdoc: RenderDoc = RenderDoc::new().unwrap(); + + // Begin. Use renderdoc in headless mode for debugging. + renderdoc.start_frame_capture(std::ptr::null(), std::ptr::null()); + schedule.execute(&mut world, &mut resources); + + // Assert that haptic feedback has been requested. + assert_eq!(get_haptic_amplitude(&mut resources), 1.0); + + // Assert the button WAS NOT clicked this frame + assert!(!button_was_clicked(&mut world)); + + // Release the trigger slightly + change_panel_trigger_value(&mut world); + schedule.execute(&mut world, &mut resources); + + // Assert that NO haptic feedback has been requested. + assert_eq!(get_haptic_amplitude(&mut resources), 0.); + + // Assert the button WAS clicked this frame + assert!(button_was_clicked(&mut world)); + + // Move the cursor off the panel and release the trigger entirely + move_cursor_off_panel(&mut world); + schedule.execute(&mut world, &mut resources); + + // Assert the button WAS NOT clicked this frame + assert!(!button_was_clicked(&mut world)); + + // Assert that NO haptic feedback has been requested. + assert_eq!(get_haptic_amplitude(&mut resources), 0.); + + renderdoc.end_frame_capture(std::ptr::null(), std::ptr::null()); + + // Get the image off the GPU + let vulkan_context = resources.get::().unwrap(); + write_image_to_disk(&vulkan_context, image, resolution); + + if !renderdoc.is_target_control_connected() { + let _ = Command::new("explorer.exe") + .args(["..\\test_assets\\render_gui.jpg"]) + .output() + .unwrap(); + } + } + + fn button_was_clicked(world: &mut World) -> bool { + let mut query = <&mut Panel>::query(); + let panel = query.iter_mut(world).next().unwrap(); + return panel.buttons[0].clicked_this_frame; + } + + fn get_haptic_amplitude(resources: &mut Resources) -> f32 { + let haptic_context = resources.get::().unwrap(); + return haptic_context.amplitude_this_frame; + } + + fn change_panel_trigger_value(world: &mut World) { + let mut query = <&mut Panel>::query(); + let panel = query.iter_mut(world).next().unwrap(); + panel.input = Some(PanelInput { + cursor_location: Pos2::new(0.5 * (800. / SCALE_FACTOR), 0.05 * (800. / SCALE_FACTOR)), + trigger_value: 0.2, + }); + } + + fn move_cursor_off_panel(world: &mut World) { + let mut query = <&mut Panel>::query(); + let panel = query.iter_mut(world).next().unwrap(); + panel.input = Some(PanelInput { + cursor_location: Pos2::new(0., 0.), + trigger_value: 0.0, + }); + } + + fn write_image_to_disk(vulkan_context: &VulkanContext, image: Image, resolution: vk::Extent2D) { + let size = (resolution.height * resolution.width * 4) as usize; + let image_data = vec![0; size]; + let buffer = Buffer::new( + &vulkan_context, + &image_data, + vk::BufferUsageFlags::TRANSFER_DST, + ) + .unwrap(); + vulkan_context.transition_image_layout( + image.handle, + vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL, + vk::ImageLayout::TRANSFER_SRC_OPTIMAL, + 1, + 1, + ); + vulkan_context.copy_image_to_buffer( + &image, + vk::ImageLayout::TRANSFER_SRC_OPTIMAL, + buffer.handle, + ); + let image_bytes = unsafe { get_from_device_memory(&vulkan_context, &buffer) }.to_vec(); + let image_from_vulkan = DynamicImage::ImageRgba8( + RgbaImage::from_raw(resolution.width, resolution.height, image_bytes).unwrap(), + ); + let output_path = "../test_assets/render_gui.jpg"; + { + let output_path = std::path::Path::new(&output_path); + let mut file = std::fs::File::create(output_path).unwrap(); + let mut jpeg_encoder = JpegEncoder::new(&mut file); + jpeg_encoder.encode_image(&image_from_vulkan).unwrap(); + } + } + + pub fn setup(resolution: vk::Extent2D) -> (World, Resources, Image, Schedule) { + let vulkan_context = VulkanContext::testing().unwrap(); + // Create an image with vulkan_context + let image = vulkan_context + .create_image( + COLOR_FORMAT, + &resolution, + vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::TRANSFER_SRC, + 2, + 1, + ) + .unwrap(); + vulkan_context + .set_debug_name(vk::ObjectType::IMAGE, image.handle.as_raw(), "Screenshot") + .unwrap(); + + let swapchain = Swapchain { + images: vec![image.handle], + resolution, + }; + + let render_context = + RenderContext::new_from_swapchain(&vulkan_context, &swapchain).unwrap(); + let gui_context = GuiContext::new(&vulkan_context); + + let gltf_data: Vec<&[u8]> = vec![include_bytes!( + "../../../test_assets/ferris-the-crab/source/ferris.glb" + )]; + let mut models = gltf_loader::load_models_from_glb( + &gltf_data, + &vulkan_context, + &render_context.descriptor_set_layouts, + ) + .unwrap(); + let (_, mut world) = models.drain().next().unwrap(); + + let mut panel_components = create_panel( + "Hello..", + 800, + 800, + &vulkan_context, + &render_context, + &gui_context, + vec![PanelButton::new("Click me!")], + ); + panel_components.0.input = Some(PanelInput { + cursor_location: Pos2::new(0.5 * (800. / SCALE_FACTOR), 0.05 * (800. / SCALE_FACTOR)), + trigger_value: 1., + }); + panel_components.3.translation[0] = -1.0; + world.push(panel_components); + + let haptic_context = HapticContext::default(); + + let mut resources = Resources::default(); + resources.insert(vulkan_context); + resources.insert(render_context); + resources.insert(gui_context); + resources.insert(0 as usize); + resources.insert(haptic_context); + + let schedule = build_schedule(); + + (world, resources, image, schedule) + } + + fn build_schedule() -> Schedule { + Schedule::builder() + .add_thread_local_fn(|_, resources| { + // let rotation: mint::Quaternion = + // UnitQuaternion::from_euler_angles(0., 45_f32.to_radians(), 0.).into(); + // let rotation: mint::Quaternion = UnitQuaternion::from_euler_angles( + // -10_f32.to_radians(), + // 10_f32.to_radians(), + // 0., + // ) + // .into(); + let rotation: mint::Quaternion = + UnitQuaternion::from_euler_angles(0., 0., 0.).into(); + let position = Vector3f { + x: -1.0, + y: 0.0, + z: 1.0, + }; + + let view = openxr::View { + pose: openxr::Posef { + orientation: Quaternionf::from(rotation), + position, + }, + fov: Fovf { + angle_up: 45.0_f32.to_radians(), + angle_down: -45.0_f32.to_radians(), + angle_left: -45.0_f32.to_radians(), + angle_right: 45.0_f32.to_radians(), + }, + }; + let views = vec![view.clone(), view]; + let vulkan_context = resources.get::().unwrap(); + let mut render_context = resources.get_mut::().unwrap(); + + render_context + .update_scene_data(&views, &vulkan_context) + .unwrap(); + render_context + .scene_params_buffer + .update( + &vulkan_context, + &[SceneParams { + // debug_view_inputs: 1., + ..Default::default() + }], + ) + .unwrap(); + + render_context.begin_frame(&vulkan_context, 0); + }) + .add_thread_local_fn(|_, resources| { + // Reset the haptic context each frame - do this instead of having to create an OpenXR context etc. + let mut haptic_context = resources.get_mut::().unwrap(); + haptic_context.amplitude_this_frame = 0.; + }) + .add_system(draw_gui_system()) + .add_thread_local_fn(|_, resources| { + let vulkan_context = resources.get::().unwrap(); + let render_context = resources.get_mut::().unwrap(); + render_context.begin_pbr_render_pass(&vulkan_context, 0); + }) + .add_system(update_transform_matrix_system()) + .add_system(update_parent_transform_matrix_system()) + .add_system(rendering_system()) + .add_thread_local_fn(|_, resources| { + let vulkan_context = resources.get::().unwrap(); + let mut render_context = resources.get_mut::().unwrap(); + render_context.end_pbr_render_pass(&vulkan_context, 0); + }) + .add_thread_local_fn(|_, resources| { + let vulkan_context = resources.get::().unwrap(); + let mut render_context = resources.get_mut::().unwrap(); + render_context.end_frame(&vulkan_context, 0); + }) + .build() + } +} diff --git a/hotham/src/systems/mod.rs b/hotham/src/systems/mod.rs index 5717363d..df7c9754 100644 --- a/hotham/src/systems/mod.rs +++ b/hotham/src/systems/mod.rs @@ -1,7 +1,9 @@ pub mod animation; pub mod collision; +pub mod draw_gui; pub mod grabbing; pub mod hands; +pub mod pointers; pub mod rendering; pub mod skinning; pub mod update_parent_transform_matrix; @@ -10,8 +12,10 @@ pub mod update_transform_matrix; pub use animation::animation_system; pub use collision::collision_system; +pub use draw_gui::draw_gui_system; pub use grabbing::grabbing_system; pub use hands::hands_system; +pub use pointers::pointers_system; pub use rendering::rendering_system; pub use skinning::skinning_system; pub use update_parent_transform_matrix::update_parent_transform_matrix_system; diff --git a/hotham/src/systems/pointers.rs b/hotham/src/systems/pointers.rs new file mode 100644 index 00000000..78bc3241 --- /dev/null +++ b/hotham/src/systems/pointers.rs @@ -0,0 +1,347 @@ +use ash::vk; +use egui::Pos2; +use legion::{system, world::SubWorld, IntoQuery}; +use nalgebra::{ + point, vector, Isometry3, Orthographic3, Point3, Quaternion, Translation3, UnitQuaternion, +}; +use rapier3d::{ + math::Point, + prelude::{InteractionGroups, Ray}, +}; + +const POSITION_OFFSET: [f32; 3] = [0., 0.071173, -0.066082]; +const ROTATION_OFFSET: Quaternion = Quaternion::new( + -0.5581498959847122, + 0.8274912503663805, + 0.03413791007514528, + -0.05061153302400824, +); + +use crate::{ + components::{ + hand::Handedness, + panel::{get_panel_dimensions, PanelInput}, + Panel, Pointer, Transform, + }, + resources::{gui_context::SCALE_FACTOR, PhysicsContext, XrContext}, + util::{posef_to_isometry, u64_to_entity}, +}; + +#[system(for_each)] +#[write_component(Panel)] +pub fn pointers( + pointer: &mut Pointer, + transform: &mut Transform, + world: &mut SubWorld, + #[resource] xr_context: &XrContext, + #[resource] physics_context: &mut PhysicsContext, +) { + // Get our the space and path of the pointer. + let time = xr_context.frame_state.predicted_display_time; + let (space, path) = match pointer.handedness { + Handedness::Left => ( + &xr_context.left_hand_space, + xr_context.left_hand_subaction_path, + ), + Handedness::Right => ( + &xr_context.right_hand_space, + xr_context.right_hand_subaction_path, + ), + }; + + // Locate the pointer in the space. + let pose = space + .locate(&xr_context.reference_space, time) + .unwrap() + .pose; + + // apply transform + let mut position = posef_to_isometry(pose); + apply_grip_offset(&mut position); + + transform.translation = position.translation.vector; + transform.rotation = position.rotation; + + // get trigger value + let trigger_value = + openxr::ActionInput::get(&xr_context.trigger_action, &xr_context.session, path) + .unwrap() + .current_state; + pointer.trigger_value = trigger_value; + + let ray_direction = transform.rotation.transform_vector(&vector![0., 1.0, 0.]); + + // Sweet baby ray + let ray = Ray::new(Point::from(transform.translation), ray_direction); + let max_toi = 40.0; + let solid = true; + let groups = InteractionGroups::new(0b10, 0b10); + let filter = None; + + if let Some((handle, toi)) = physics_context.query_pipeline.cast_ray( + &physics_context.colliders, + &ray, + max_toi, + solid, + groups, + filter, + ) { + // The first collider hit has the handle `handle` and it hit after + // the ray travelled a distance equal to `ray.dir * toi`. + let hit_point = ray.point_at(toi); // Same as: `ray.origin + ray.dir * toi` + let hit_collider = physics_context.colliders.get(handle).unwrap(); + let entity = u64_to_entity(hit_collider.user_data as u64); + let mut query = <&mut Panel>::query(); + let panel = query + .get_mut(world, entity) + .expect(&format!("Unable to find entity {:?} in world", entity)); + let panel_extent = &panel.extent; + let panel_transform = hit_collider.position(); + let cursor_location = + get_cursor_location_for_panel(&hit_point, panel_transform, panel_extent); + panel.input = Some(PanelInput { + cursor_location, + trigger_value, + }); + } +} + +pub fn apply_grip_offset(position: &mut Isometry3) { + let updated_rotation = position.rotation.quaternion() * ROTATION_OFFSET; + let updated_translation = position.translation.vector + - vector!(POSITION_OFFSET[0], POSITION_OFFSET[1], POSITION_OFFSET[2]); + position.rotation = UnitQuaternion::from_quaternion(updated_rotation); + position.translation = Translation3::from(updated_translation); +} + +fn get_cursor_location_for_panel( + hit_point: &Point3, + panel_transform: &Isometry3, + panel_extent: &vk::Extent2D, +) -> Pos2 { + let projected_hit_point = ray_to_panel_space(hit_point, panel_transform, panel_extent); + let transformed_hit_point = panel_transform + .rotation + .transform_point(&projected_hit_point); + + // Adjust the point such that 0,0 is the panel's top left + let x = (transformed_hit_point.x + 1.) * 0.5; + let y = ((transformed_hit_point.y * -1.) * 0.5) + 0.5; + + // Convert to screen coordinates + let x_points = x * panel_extent.width as f32 / SCALE_FACTOR; + let y_points = y * panel_extent.height as f32 / SCALE_FACTOR; + + return Pos2::new(x_points, y_points); +} + +fn ray_to_panel_space( + hit_point: &Point3, + panel_transform: &Isometry3, + panel_extent: &vk::Extent2D, +) -> Point3 { + // Translate the extents of the panel into world space, using the panel's translation. + let (extent_x, extent_y) = get_panel_dimensions(&panel_extent); + let translated_extents = panel_transform * point![extent_x, extent_y, 0.]; + + // Now build an orthographic matrix to project from world space into the panel's screen space + let left = translated_extents.x - 1.; + let right = translated_extents.x; + let bottom = translated_extents.y - 1.; + let top = translated_extents.y; + let panel_projection = Orthographic3::new(left, right, bottom, top, 0., 1.); + + // Project the ray's hit point into panel space + return panel_projection.project_point(hit_point); +} + +#[cfg(test)] +mod tests { + use std::marker::PhantomData; + + use approx::assert_relative_eq; + use ash::vk; + use legion::{IntoQuery, Resources, Schedule, World}; + use nalgebra::vector; + use rapier3d::prelude::ColliderBuilder; + + use crate::{ + buffer::Buffer, + components::{Collider, Panel, Transform}, + resources::{ + physics_context::{DEFAULT_COLLISION_GROUP, PANEL_COLLISION_GROUP}, + XrContext, + }, + schedule_functions::physics_step, + util::entity_to_u64, + }; + + use super::*; + #[test] + pub fn test_pointers_system() { + let (xr_context, _) = XrContext::new().unwrap(); + let mut physics_context = PhysicsContext::default(); + let mut world = World::default(); + let mut resources = Resources::default(); + + let panel = Panel { + text: "Test Panel".to_string(), + extent: vk::Extent2D { + width: 300, + height: 300, + }, + framebuffer: vk::Framebuffer::null(), + vertex_buffer: empty_buffer(), + index_buffer: empty_buffer(), + egui_context: Default::default(), + raw_input: Default::default(), + input: Default::default(), + buttons: Vec::new(), + }; + let panel_entity = world.push((panel,)); + + // Place the panel *directly above* where the pointer will be located. + let collider = ColliderBuilder::cuboid(0.5, 0.5, 0.0) + .sensor(true) + .collision_groups(InteractionGroups::new( + PANEL_COLLISION_GROUP, + PANEL_COLLISION_GROUP, + )) + .translation(vector![-0.2, 2., -0.433918]) + .rotation(vector![(3. * std::f32::consts::PI) * 0.5, 0., 0.]) + .user_data(entity_to_u64(panel_entity).into()) + .build(); + let handle = physics_context.colliders.insert(collider); + let collider = Collider { + collisions_this_frame: Vec::new(), + handle, + }; + let mut panel_entry = world.entry(panel_entity).unwrap(); + panel_entry.add_component(collider); + + // Add a decoy collider to ensure we're using collision groups correctly. + let collider = ColliderBuilder::cuboid(0.1, 0.1, 0.1) + .sensor(true) + .collision_groups(InteractionGroups::new( + DEFAULT_COLLISION_GROUP, + DEFAULT_COLLISION_GROUP, + )) + .translation(vector![-0.2, 1.5, -0.433918]) + .rotation(vector![(3. * std::f32::consts::PI) * 0.5, 0., 0.]) + .build(); + let handle = physics_context.colliders.insert(collider); + let collider = Collider { + collisions_this_frame: Vec::new(), + handle, + }; + world.push((collider,)); + + resources.insert(xr_context); + resources.insert(physics_context); + let pointer_entity = world.push(( + Pointer { + handedness: Handedness::Left, + trigger_value: 0.0, + }, + Transform::default(), + )); + + let mut schedule = Schedule::builder() + .add_thread_local_fn(physics_step) + .add_system(pointers_system()) + .build(); + + schedule.execute(&mut world, &mut resources); + let mut query = <&Transform>::query(); + let transform = query.get(&world, pointer_entity).unwrap(); + + // Assert that the pointer has moved + assert_relative_eq!(transform.translation, vector![-0.2, 1.328827, -0.433918]); + + let mut query = <&Panel>::query(); + let panel = query.get(&world, panel_entity).unwrap(); + let input = panel.input.clone().unwrap(); + assert_relative_eq!(input.cursor_location.x, 50.); + assert_relative_eq!(input.cursor_location.y, 29.491043); + assert_eq!(input.trigger_value, 0.); + } + + #[test] + pub fn test_get_cursor_location_for_panel() { + let panel_transform = Isometry3::new(nalgebra::zero(), nalgebra::zero()); + let panel_extent = vk::Extent2D { + width: 100 * SCALE_FACTOR as u32, + height: 100 * SCALE_FACTOR as u32, + }; + + // Trivial example. Panel and hit point at origin: + let result = + get_cursor_location_for_panel(&point![0., 0., 0.], &panel_transform, &panel_extent); + assert_relative_eq!(result.x, 50.); + assert_relative_eq!(result.y, 50.); + + // hit top left + let result = + get_cursor_location_for_panel(&point![-0.5, 0.5, 0.], &panel_transform, &panel_extent); + assert_relative_eq!(result.x, 0.); + assert_relative_eq!(result.y, 0.); + + // hit top right + let result = + get_cursor_location_for_panel(&point![0.5, 0.5, 0.], &panel_transform, &panel_extent); + assert_relative_eq!(result.x, 100.); + assert_relative_eq!(result.y, 0.); + + // hit bottom right + let result = + get_cursor_location_for_panel(&point![0.5, -0.5, 0.], &panel_transform, &panel_extent); + assert_relative_eq!(result.x, 100.); + assert_relative_eq!(result.y, 100.); + + // hit bottom left + let result = + get_cursor_location_for_panel(&point![-0.5, -0.5, 0.], &panel_transform, &panel_extent); + assert_relative_eq!(result.x, 0.); + assert_relative_eq!(result.y, 100.); + } + + #[test] + pub fn test_ray_to_panel_space() { + let panel_transform = Isometry3::new(nalgebra::zero(), nalgebra::zero()); + let panel_extent = vk::Extent2D { + width: 100, + height: 100, + }; + + let result = ray_to_panel_space(&point![0., 0., 0.], &panel_transform, &panel_extent); + assert_relative_eq!(result, point![0.0, 0.0, -1.0]); + + // hit top left + let result = ray_to_panel_space(&point![-0.5, 0.5, 0.], &panel_transform, &panel_extent); + assert_relative_eq!(result, point![-1.0, 1.0, -1.0]); + + // hit top right + let result = ray_to_panel_space(&point![0.5, 0.5, 0.], &panel_transform, &panel_extent); + assert_relative_eq!(result, point![1.0, 1.0, -1.0]); + + // hit bottom right + let result = ray_to_panel_space(&point![0.5, -0.5, 0.], &panel_transform, &panel_extent); + assert_relative_eq!(result, point![1.0, -1.0, -1.0]); + + // hit bottom left + let result = ray_to_panel_space(&point![-0.5, -0.5, 0.], &panel_transform, &panel_extent); + assert_relative_eq!(result, point![-1.0, -1.0, -1.0]); + } + + fn empty_buffer() -> Buffer { + let vertex_buffer = Buffer { + handle: vk::Buffer::null(), + device_memory: vk::DeviceMemory::null(), + _phantom: PhantomData, + size: 0, + device_memory_size: 0, + usage: vk::BufferUsageFlags::empty(), + }; + vertex_buffer + } +} diff --git a/hotham/src/systems/rendering.rs b/hotham/src/systems/rendering.rs index 42a9cbd3..d1e7e964 100644 --- a/hotham/src/systems/rendering.rs +++ b/hotham/src/systems/rendering.rs @@ -75,6 +75,8 @@ pub fn rendering( #[cfg(test)] mod tests { + use std::{collections::hash_map::DefaultHasher, hash::Hasher}; + use super::*; use ash::vk::Handle; use image::{jpeg::JpegEncoder, DynamicImage, RgbaImage}; @@ -186,6 +188,11 @@ mod tests { name: &str, debug_view_equation: f32, ) { + let mut resources = Resources::default(); + resources.insert(vulkan_context.clone()); + resources.insert(render_context.clone()); + resources.insert(0 as usize); + let mut schedule = Schedule::builder() .add_thread_local_fn(move |_, resources| { // SPONZA @@ -234,7 +241,9 @@ mod tests { }], ) .unwrap(); - render_context.begin_render_pass(&vulkan_context, 0); + + render_context.begin_frame(&vulkan_context, 0); + render_context.begin_pbr_render_pass(&vulkan_context, 0); }) .add_system(update_transform_matrix_system()) .add_system(update_parent_transform_matrix_system()) @@ -242,13 +251,10 @@ mod tests { .add_thread_local_fn(|_, resources| { let vulkan_context = resources.get::().unwrap(); let mut render_context = resources.get_mut::().unwrap(); - render_context.end_render_pass(&vulkan_context, 0); + render_context.end_pbr_render_pass(&vulkan_context, 0); + render_context.end_frame(&vulkan_context, 0); }) .build(); - let mut resources = Resources::default(); - resources.insert(vulkan_context.clone()); - resources.insert(render_context.clone()); - resources.insert(0 as usize); schedule.execute(world, &mut resources); let size = (resolution.height * resolution.width * 4) as usize; let vulkan_context = resources.get::().unwrap(); @@ -276,10 +282,26 @@ mod tests { RgbaImage::from_raw(resolution.width, resolution.height, image_bytes).unwrap(), ); - let path = format!("../test_assets/render_{}.jpeg", name); - let path = std::path::Path::new(&path); - let mut file = std::fs::File::create(path).unwrap(); - let mut jpeg_encoder = JpegEncoder::new(&mut file); - jpeg_encoder.encode_image(&image_from_vulkan).unwrap(); + let output_path = format!("../test_assets/render_{}.jpg", name); + { + let output_path = std::path::Path::new(&output_path); + let mut file = std::fs::File::create(output_path).unwrap(); + let mut jpeg_encoder = JpegEncoder::new(&mut file); + jpeg_encoder.encode_image(&image_from_vulkan).unwrap(); + } + + // Compare the render with a "known good" copy. + let output_hash = hash_file(&output_path); + let known_good_path = format!("../test_assets/render_{}_known_good.jpg", name); + let known_good_hash = hash_file(&known_good_path); + + assert_eq!(output_hash, known_good_hash); + } + + fn hash_file(file_path: &str) -> u64 { + let mut hasher = DefaultHasher::new(); + let bytes = std::fs::read(&file_path).unwrap(); + bytes.iter().for_each(|b| hasher.write_u8(*b)); + return hasher.finish(); } } diff --git a/hotham/src/texture.rs b/hotham/src/texture.rs index 10592a21..21d09862 100644 --- a/hotham/src/texture.rs +++ b/hotham/src/texture.rs @@ -77,20 +77,35 @@ impl Texture { .unwrap(), ) } - gltf::image::Source::View { .. } => { + // TODO: Fix this + gltf::image::Source::View { mime_type, .. } => { let index = texture.source().index(); let image = &images[index]; - let pixels = add_alpha_channel(&image); - Texture::new( - texture_name, - &vulkan_context, - &pixels, - image.width, - image.height, - TEXTURE_FORMAT, - ) - .map_err(|e| eprintln!("Failed to load texture {} - {:?}", index, e)) - .ok() + let texture = if mime_type == "image/jpeg" { + let pixels = add_alpha_channel(&image); + let texture = Texture::new( + texture_name, + &vulkan_context, + &pixels, + image.width, + image.height, + TEXTURE_FORMAT, + ); + texture + } else { + Texture::new( + texture_name, + &vulkan_context, + &image.pixels, + image.width, + image.height, + TEXTURE_FORMAT, + ) + }; + + texture + .map_err(|e| eprintln!("Failed to load texture {} - {:?}", index, e)) + .ok() } } } diff --git a/hotham/src/vertex.rs b/hotham/src/vertex.rs index ff5c68be..962d38e2 100644 --- a/hotham/src/vertex.rs +++ b/hotham/src/vertex.rs @@ -2,7 +2,7 @@ use ash::vk; use nalgebra::{Vector2, Vector3, Vector4}; #[repr(C)] -#[derive(Clone, Debug, Copy, PartialEq)] +#[derive(Clone, Debug, Copy, PartialEq, Default)] pub struct Vertex { pub position: Vector3, pub normal: Vector3, diff --git a/test_assets/ferris-the-crab/source/ferris.glb b/test_assets/ferris-the-crab/source/ferris.glb new file mode 100644 index 00000000..eb259158 --- /dev/null +++ b/test_assets/ferris-the-crab/source/ferris.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba3a61f50ac3d5c17a63b817f7ea65d687f45445e3dd492f3d0bb75a846534bf +size 221636 diff --git a/test_assets/ferris-the-crab/textures/ferris_eyes_0.png b/test_assets/ferris-the-crab/textures/ferris_eyes_0.png new file mode 100644 index 00000000..2a7a06cb Binary files /dev/null and b/test_assets/ferris-the-crab/textures/ferris_eyes_0.png differ diff --git a/test_assets/render_D.jpeg b/test_assets/render_D.jpeg deleted file mode 100644 index 889dd19c..00000000 Binary files a/test_assets/render_D.jpeg and /dev/null differ diff --git a/test_assets/render_D.jpg b/test_assets/render_D.jpg index 889dd19c..053b6470 100644 Binary files a/test_assets/render_D.jpg and b/test_assets/render_D.jpg differ diff --git a/test_assets/render_D_known_good.jpg b/test_assets/render_D_known_good.jpg new file mode 100644 index 00000000..053b6470 Binary files /dev/null and b/test_assets/render_D_known_good.jpg differ diff --git a/test_assets/render_Diffuse.jpeg b/test_assets/render_Diffuse.jpeg deleted file mode 100644 index aa8fd4f1..00000000 Binary files a/test_assets/render_Diffuse.jpeg and /dev/null differ diff --git a/test_assets/render_Diffuse.jpg b/test_assets/render_Diffuse.jpg index aa8fd4f1..6279b941 100644 Binary files a/test_assets/render_Diffuse.jpg and b/test_assets/render_Diffuse.jpg differ diff --git a/test_assets/render_Diffuse_known_good.jpg b/test_assets/render_Diffuse_known_good.jpg new file mode 100644 index 00000000..6279b941 Binary files /dev/null and b/test_assets/render_Diffuse_known_good.jpg differ diff --git a/test_assets/render_F.jpeg b/test_assets/render_F.jpeg deleted file mode 100644 index 6d768c6e..00000000 Binary files a/test_assets/render_F.jpeg and /dev/null differ diff --git a/test_assets/render_F.jpg b/test_assets/render_F.jpg index 6d768c6e..5bf3f059 100644 Binary files a/test_assets/render_F.jpg and b/test_assets/render_F.jpg differ diff --git a/test_assets/render_F_known_good.jpg b/test_assets/render_F_known_good.jpg new file mode 100644 index 00000000..5bf3f059 Binary files /dev/null and b/test_assets/render_F_known_good.jpg differ diff --git a/test_assets/render_G.jpeg b/test_assets/render_G.jpeg deleted file mode 100644 index e3dda157..00000000 Binary files a/test_assets/render_G.jpeg and /dev/null differ diff --git a/test_assets/render_G.jpg b/test_assets/render_G.jpg index e3dda157..59b25f2b 100644 Binary files a/test_assets/render_G.jpg and b/test_assets/render_G.jpg differ diff --git a/test_assets/render_G_known_good.jpg b/test_assets/render_G_known_good.jpg new file mode 100644 index 00000000..59b25f2b Binary files /dev/null and b/test_assets/render_G_known_good.jpg differ diff --git a/test_assets/render_Normal.jpeg b/test_assets/render_Normal.jpeg deleted file mode 100644 index 3a8462ff..00000000 Binary files a/test_assets/render_Normal.jpeg and /dev/null differ diff --git a/test_assets/render_Normal.jpg b/test_assets/render_Normal.jpg index 3a8462ff..ee1ab20e 100644 Binary files a/test_assets/render_Normal.jpg and b/test_assets/render_Normal.jpg differ diff --git a/test_assets/render_Normal_known_good.jpg b/test_assets/render_Normal_known_good.jpg new file mode 100644 index 00000000..ee1ab20e Binary files /dev/null and b/test_assets/render_Normal_known_good.jpg differ diff --git a/test_assets/render_Specular.jpg b/test_assets/render_Specular.jpg index 9f3cd108..8c8857c0 100644 Binary files a/test_assets/render_Specular.jpg and b/test_assets/render_Specular.jpg differ diff --git a/test_assets/render_Specular.jpeg b/test_assets/render_Specular_known_good.jpg similarity index 53% rename from test_assets/render_Specular.jpeg rename to test_assets/render_Specular_known_good.jpg index 9f3cd108..8c8857c0 100644 Binary files a/test_assets/render_Specular.jpeg and b/test_assets/render_Specular_known_good.jpg differ diff --git a/test_assets/render_gui.jpg b/test_assets/render_gui.jpg new file mode 100644 index 00000000..3df51b15 Binary files /dev/null and b/test_assets/render_gui.jpg differ