diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0a328a5d..81b851d9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -45,7 +45,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: -- -D warnings + args: --tests -- -D warnings - name: Run cargo clippy (android) uses: actions-rs/cargo@v1 with: diff --git a/examples/custom-rendering/src/lib.rs b/examples/custom-rendering/src/lib.rs index 313bd165..13613434 100644 --- a/examples/custom-rendering/src/lib.rs +++ b/examples/custom-rendering/src/lib.rs @@ -1,6 +1,7 @@ mod custom_render_context; mod custom_rendering; mod hologram; +mod surface_solver; use custom_render_context::{create_quadrics_pipeline, CustomRenderContext}; use custom_rendering::custom_rendering_system; @@ -10,8 +11,8 @@ use hotham::{ components::{ hand::Handedness, physics::SharedShape, Collider, Grabbable, LocalTransform, Mesh, }, - glam::{Mat4, Quat, Vec3}, - hecs::World, + glam::{Mat4, Quat}, + hecs::{Entity, World}, systems::{ animation_system, debug::debug_system, grabbing_system, hands::add_hand, hands_system, physics_system, skinning::skinning_system, update_global_transform_system, @@ -20,6 +21,7 @@ use hotham::{ xr, Engine, HothamResult, TickData, }; use hotham_examples::navigation::{navigation_system, State}; +use surface_solver::{surface_solver_system, ControlPoints, HologramBackside}; #[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))] pub fn main() { @@ -62,6 +64,7 @@ fn tick( physics_system(engine); animation_system(engine); navigation_system(engine, state); + surface_solver_system(engine); update_global_transform_system(engine); skinning_system(engine); debug_system(engine); @@ -123,87 +126,179 @@ fn init(engine: &mut Engine) -> Result<(), hotham::HothamError> { let vulkan_context = &mut engine.vulkan_context; let world = &mut engine.world; - let mut glb_buffers: Vec<&[u8]> = vec![ + let glb_buffers: Vec<&[u8]> = vec![ include_bytes!("../../../test_assets/left_hand.glb"), include_bytes!("../../../test_assets/right_hand.glb"), - include_bytes!("../../../test_assets/sphere.glb"), + include_bytes!("../../../test_assets/hologram_templates.glb"), ]; - #[cfg(target_os = "android")] - glb_buffers.push(include_bytes!( - "../../../test_assets/damaged_helmet_squished.glb" - )); - - #[cfg(not(target_os = "android"))] - glb_buffers.push(include_bytes!("../../../test_assets/damaged_helmet.glb")); - let models = asset_importer::load_models_from_glb(&glb_buffers, vulkan_context, render_context)?; - add_helmet(&models, world, [-1., 1.4, -1.].into()); - add_helmet(&models, world, [1., 1.4, -1.].into()); add_hand(&models, Handedness::Left, world); add_hand(&models, Handedness::Right, world); + let uv1_from_local = Mat4::from_diagonal([1.0, 1.0, 1.0, 0.1].into()); + let uv2_from_local = Mat4::from_cols_array(&[ + 1.0, 0.0, 0.0, 0.0, // + 0.0, 0.0, 1.0, 0.0, // + 0.0, 1.0, 0.0, 0.0, // + 0.0, 0.0, 0.0, 0.1, // + ]); + let uv3_from_local = Mat4::from_diagonal([0.0, 1.0, 0.0, 0.1].into()); add_quadric( &models, + "Sphere", world, - &LocalTransform { - translation: [1.0, 1.4, -1.5].into(), - rotation: Quat::IDENTITY, - scale: [0.5, 0.5, 0.5].into(), - }, + &make_transform(-1.0, 1.4, -1.5, 0.5), 0.5, HologramData { + // Sphere, x² + y² + z² = 1 surface_q_in_local: Mat4::from_diagonal([1.0, 1.0, 1.0, -1.0].into()), bounds_q_in_local: Mat4::from_diagonal([0.0, 0.0, 0.0, 0.0].into()), - uv_from_local: Mat4::IDENTITY, + uv_from_local: uv1_from_local, }, ); add_quadric( &models, + "Cylinder", world, - &LocalTransform { - translation: [-1.0, 1.4, -1.5].into(), - rotation: Quat::IDENTITY, - scale: [0.5, 0.5, 0.5].into(), + &make_transform(0.0, 1.4, -1.5, 0.5), + 0.5_f32.sqrt(), + HologramData { + // Cylinder, x² + z² = 1 + surface_q_in_local: Mat4::from_diagonal([1.0, 0.0, 1.0, -1.0].into()), + bounds_q_in_local: Mat4::from_diagonal([0.0, 1.0, 0.0, -1.0].into()), + uv_from_local: uv1_from_local, }, - 0.5, + ); + add_quadric( + &models, + "Cylinder", + world, + &make_transform(1.0, 1.4, -1.5, 0.5), + 0.5_f32.sqrt(), + HologramData { + // Hyperboloid of one sheet, x² - y² + z² = c + surface_q_in_local: Mat4::from_diagonal([1.0, -1.0 + 0.1, 1.0, -0.1].into()), + bounds_q_in_local: Mat4::from_diagonal([0.0, 1.0, 0.0, -1.0].into()), + uv_from_local: uv2_from_local, + }, + ); + add_quadric( + &models, + "Cylinder", + world, + &make_transform(2.0, 1.4, -1.5, 0.5), + 0.5_f32.sqrt(), + HologramData { + // Double cones, x² - y² + z² = 0 + surface_q_in_local: Mat4::from_diagonal([1.0, -1.0, 1.0, 0.0].into()), + bounds_q_in_local: Mat4::from_diagonal([0.0, 1.0, 0.0, -1.0].into()), + uv_from_local: uv2_from_local, + }, + ); + add_quadric( + &models, + "Cylinder", + world, + &make_transform(3.0, 1.4, -1.5, 0.5), + 0.5_f32.sqrt(), + HologramData { + // Hyperboloid of two sheets, x² - y² + z² = -c + surface_q_in_local: Mat4::from_diagonal([1.0, -1.0 - 0.1, 1.0, 0.1].into()), + bounds_q_in_local: Mat4::from_diagonal([0.0, 1.0, 0.0, -1.0].into()), + uv_from_local: uv2_from_local, + }, + ); + add_quadric( + &models, + "Cylinder", + world, + &make_transform(4.0, 1.4, -1.5, 0.5), + 0.5_f32.sqrt(), HologramData { - surface_q_in_local: Mat4::from_diagonal([1.0, 1.0, 0.0, -1.0].into()), - bounds_q_in_local: Mat4::from_diagonal([0.0, 0.0, 1.0, -1.0].into()), - uv_from_local: Mat4::IDENTITY, + // Hyperbolic paraboloid - "saddle", cy = x² + z² + surface_q_in_local: Mat4::from_cols( + [1.0, 0.0, 0.0, 0.0].into(), + [0.0, 0.0, 0.0, 1.0].into(), + [0.0, 0.0, -1.0, 0.0].into(), + [0.0, 1.0, 0.0, 0.0].into(), + ), + bounds_q_in_local: Mat4::from_diagonal([1.0, 0.0, 1.0, -1.0].into()), + uv_from_local: uv2_from_local, }, ); - Ok(()) -} + // Quadric surfaces with varying number of control points + let target_initial_hologram_data = HologramData { + surface_q_in_local: Mat4::from_diagonal([1.0, 1.0, 1.0, -1.0].into()), + bounds_q_in_local: Mat4::from_diagonal([1.0, 1.0, 1.0, -1.0].into()), + uv_from_local: uv2_from_local, + }; + let t_from_local = Mat4::from_translation([0.0, -1.0, 0.0].into()); + let t2_from_local = Mat4::from_translation([0.0, -0.5, 0.0].into()); + let control_point_hologram_data = HologramData { + surface_q_in_local: t_from_local.transpose() + * Mat4::from_diagonal([1.0, -1.0, 1.0, 0.0].into()) + * t_from_local, + bounds_q_in_local: t2_from_local.transpose() + * Mat4::from_diagonal([0.0, 1.0, 0.0, -0.25].into()) + * t2_from_local, + uv_from_local: uv3_from_local, + }; + for n in 1..=9 { + let target = add_quadric( + &models, + "Sphere", + world, + &make_transform(-2.0 + n as f32, 1.4, 1.5, 0.5), + 0.5, + target_initial_hologram_data, + ); -fn add_helmet( - models: &std::collections::HashMap, - world: &mut World, - translation: Vec3, -) { - let helmet = add_model_to_world("Damaged Helmet", models, world, None) - .expect("Could not find Damaged Helmet"); + let entities = (0..n) + .map(|i| { + add_quadric( + &models, + "Cylinder", + world, + &make_transform( + -2.0 + n as f32 + 0.1 * (i & 1) as f32, + 1.4, + 1.5 + 0.1 * (i >> 1) as f32, + 0.05, + ), + 0.05, + control_point_hologram_data, + ) + }) + .collect(); - { - let mut local_transform = world.get::<&mut LocalTransform>(helmet).unwrap(); - local_transform.translation = translation; - local_transform.scale = [0.5, 0.5, 0.5].into(); + let control_points = ControlPoints { entities }; + world.insert_one(target, control_points).unwrap(); + world.remove_one::(target).unwrap(); } - let collider = Collider::new(SharedShape::ball(0.35)); + Ok(()) +} - world.insert(helmet, (collider, Grabbable {})).unwrap(); +fn make_transform(x: f32, y: f32, z: f32, scale: f32) -> LocalTransform { + LocalTransform { + translation: [x, y, z].into(), + rotation: Quat::IDENTITY, + scale: [scale, scale, scale].into(), + } } fn add_quadric( models: &std::collections::HashMap, + model_name: &str, world: &mut World, local_transform: &LocalTransform, ball_radius: f32, hologram_data: HologramData, -) { - let entity = add_model_to_world("Sphere", models, world, None).expect("Could not find Sphere"); +) -> Entity { + let entity = add_model_to_world(model_name, models, world, None) + .unwrap_or_else(|| panic!("Could not find {}", model_name)); *world.get::<&mut LocalTransform>(entity).unwrap() = *local_transform; let collider = Collider::new(SharedShape::ball(ball_radius)); let hologram_component = Hologram { @@ -214,4 +309,20 @@ fn add_quadric( .insert(entity, (collider, Grabbable {}, hologram_component)) .unwrap(); world.remove_one::(entity).unwrap(); + + // Add second entity for the back surface + let second_entity = add_model_to_world(model_name, models, world, Some(entity)) + .unwrap_or_else(|| panic!("Could not find {}", model_name)); + // Negate Q to flip the surface normal + let mut hologram_component = hologram_component; + hologram_component.hologram_data.surface_q_in_local *= -1.0; + world + .insert( + second_entity, + (hologram_component, HologramBackside { entity }), + ) + .unwrap(); + world.remove_one::(second_entity).unwrap(); + + entity } diff --git a/examples/custom-rendering/src/shaders/quadric.frag b/examples/custom-rendering/src/shaders/quadric.frag index 64a1e025..5317eadf 100644 --- a/examples/custom-rendering/src/shaders/quadric.frag +++ b/examples/custom-rendering/src/shaders/quadric.frag @@ -26,6 +26,18 @@ const vec2 offsetSample1 = vec2(0.875 - 0.5, 0.375 - 0.5); const vec2 offsetSample2 = vec2(0.125 - 0.5, 0.625 - 0.5); const vec2 offsetSample3 = vec2(0.625 - 0.5, 0.875 - 0.5); +// Based on https://iquilezles.org/articles/filterableprocedurals/ +float filteredGrid( in vec2 p, in vec2 dpdx, in vec2 dpdy ) +{ + const float N = 10.0; + vec2 w = max(abs(dpdx), abs(dpdy)) + 0.001; + vec2 a = p + 0.5*w; + vec2 b = p - 0.5*w; + vec2 i = (floor(a)+min(fract(a)*N,1.0)- + floor(b)-min(fract(b)*N,1.0))/(N*w); + return (1.0-i.x)*(1.0-i.y); +} + void main() { // Start by setting the output color to a familiar "error" magenta. outColor = ERROR_MAGENTA; @@ -34,18 +46,57 @@ void main() { QuadricData d = quadricDataBuffer.data[inInstanceIndex]; // Find ray-quadric intersection, if any - vec4 rayOrigin = inRayOrigin / inRayOrigin.w; - vec4 rayDir = vec4(normalize(rayOrigin.xyz - sceneData.cameraPosition[gl_ViewIndex].xyz), 0.0); + vec4 rayOrigin = sceneData.cameraPosition[gl_ViewIndex]; + vec4 rayDir = vec4(inRayOrigin.xyz / inRayOrigin.w - sceneData.cameraPosition[gl_ViewIndex].xyz, 0.0); + + // A point p on the ray is + // p = rayOrigin + rayDir*t + // The point is on the surface of the quadric Q when + // pᵀ*Q*p = 0 + // These can be combined and manipulated to give a quadratic equation in standard form: + // (rayOrigin + rayDir*t)ᵀ*Q*(rayOrigin + rayDir*t) = 0 + // (rayOriginᵀ + rayDirᵀ*t)*(Q*rayOrigin + Q*rayDir*t) = 0 + // (rayOriginᵀ*(Q*rayOrigin + Q*rayDir*t) + rayDirᵀ*t*(Q*rayOrigin + Q*rayDir*t) = 0 + // rayOriginᵀ*Q*rayOrigin + rayOriginᵀ*Q*rayDir*t + rayDirᵀ*t*Q*rayOrigin + rayDirᵀ*t*Q*rayDir*t = 0 + // rayOriginᵀ*Q*rayOrigin + 2*rayOriginᵀ*Q*rayDir*t + rayDirᵀ*Q*rayDir*t*t = 0 + + // The quadratic formula based on ax² + bx + c = 0 has the solutions + // x = (-b ± √(b² - 4ac)) / 2a + // but we can simplify it by basing it on ax² + 2bx + c = 0. + // x = (-2b ± √(4b² - 4ac)) / 2a + // x = (-2b ± 2√(b² - ac)) / 2a + // x = (-b ± √(b² - ac)) / a + // We are only interested in the solution for when the surface is facing us. + // The ray direction and surface normal should be more than 90 degrees apart + // rayDir ⬤ n < 0 + // The surface normal is proportional to the gradient of the quadric. + // n ~= Q₃*p + // rayDirᵀ*Q₃*p < 0 + // rayDirᵀ*Q₃*(rayOrigin + rayDir*t) < 0 + // rayDirᵀ*Q₃*rayOrigin + rayDirᵀ*Q₃*rayDir*t < 0 + // b + a*t < 0 + // a*t < -b + // This allows us to pick a single solution ("±" becomes "-") + // x = (-b ± √(b² - ac)) / a + // a*x = -b ± √(b² - ac) + // a*t = -b - √(b² - ac), because a*t < -b and √(b² - ac) > 0 + // t = (-b - √(b² - ac)) / a + // What if a = 0? + // The "Citardauq Formula" works even when a = 0. + // It can be derived by expanding the fraction with the "conjugate" of the numerator. + // t = (-b - √(b² - ac)) / a * (-b + √(b² - ac)) / (-b + √(b² - ac)) + // t = c / (-b + √(b² - ac)) + // This is better but can still get division by zero if ac = 0 and b > 0. + // We will also have catastrophic cancellation if b > 0 and |ac| << b² + // b = rayOriginᵀ*Q*rayDir vec4 surfaceQTimesRayOrigin = d.surfaceQ * rayOrigin; vec4 surfaceQTimesRayDir = d.surfaceQ * rayDir; float a = dot(rayDir, surfaceQTimesRayDir); - float b = dot(rayOrigin, surfaceQTimesRayDir); + float b = dot(rayDir, surfaceQTimesRayOrigin); float c = dot(rayOrigin, surfaceQTimesRayOrigin); - // Discriminant from quadratic formula is - // b^2 - 4ac - // but we are able to simplify it by substituting b with b/2. + float discriminant = b * b - a * c; vec2 gradientOfDiscriminant = vec2(dFdx(discriminant), dFdy(discriminant)); gl_SampleMask[0] = int( @@ -55,24 +106,32 @@ void main() { step(0.0, discriminant + dot(offsetSample3, gradientOfDiscriminant)) * 8); // Pick the solution that is facing us - float t = -(b + sqrt(max(0.0, discriminant))) / a; + float t = c / (-b + sqrt(max(0.0, discriminant))); + // hitPoint.w = 1 because rayOrigin.w = 1 and rayDir.w = 0. + vec4 hitPoint = rayOrigin + rayDir * t; + + // Compute gradient along the surface (orthogonal to surface normal). + // Clamp them to avoid flying pixels due to overshooting. + vec3 ddx_hitPoint = clamp(dFdx(hitPoint.xyz), -1.0, 1.0); + vec3 ddy_hitPoint = clamp(dFdy(hitPoint.xyz), -1.0, 1.0); - if (t < -0.0001) { - t = 0.0; - gl_SampleMask[0] = 0; + // Discarding is postponed until here to make sure the derivatives above are valid. + if (t < 1.0) { + discard; } - // hitPoint.w = 1 because rayOrigin.w = 1 and rayDir.w = 0. - vec4 hitPoint = rayOrigin + rayDir * t; - float boundsValue = 0.0001 - dot(hitPoint, d.boundsQ * hitPoint); - vec2 gradientOfBoundsValue = vec2(dFdx(boundsValue), dFdy(boundsValue)); + vec4 boundsQTimesHitPoint = d.boundsQ * hitPoint; + float boundsValue = dot(hitPoint, boundsQTimesHitPoint); + vec2 gradientOfBoundsValue = vec2( + dot(ddx_hitPoint, boundsQTimesHitPoint.xyz), + dot(ddy_hitPoint, boundsQTimesHitPoint.xyz)); gl_SampleMask[0] &= int( - step(0.0, boundsValue + dot(offsetSample0, gradientOfBoundsValue)) + - step(0.0, boundsValue + dot(offsetSample1, gradientOfBoundsValue)) * 2 + - step(0.0, boundsValue + dot(offsetSample2, gradientOfBoundsValue)) * 4 + - step(0.0, boundsValue + dot(offsetSample3, gradientOfBoundsValue)) * 8); + step(boundsValue + dot(offsetSample0, gradientOfBoundsValue), 0.0) + + step(boundsValue + dot(offsetSample1, gradientOfBoundsValue), 0.0) * 2 + + step(boundsValue + dot(offsetSample2, gradientOfBoundsValue), 0.0) * 4 + + step(boundsValue + dot(offsetSample3, gradientOfBoundsValue), 0.0) * 8); - // Discarding is postponed until here to make sure the derivatives above are valid. + // Discard if all samples have been masked out. if (gl_SampleMask[0] == 0) { discard; } @@ -85,11 +144,14 @@ void main() { pos = hitPoint.xyz; v = normalize(sceneData.cameraPosition[gl_ViewIndex].xyz - pos); - // Compute normal from gradient of surface quadric + // Compute normal from gradient of surface quadric. n = normalize((d.surfaceQ * hitPoint).xyz); vec4 uv4 = d.uvFromGos * hitPoint; uv = uv4.xy / uv4.w; + mat3x2 uvFromGos23 = mat3x2(d.uvFromGos[0].xy, d.uvFromGos[1].xy, d.uvFromGos[2].xy); + vec2 ddx_uv = uvFromGos23 * ddx_hitPoint / uv4.w; // TODO: Handle derivative of w. + vec2 ddy_uv = uvFromGos23 * ddy_hitPoint / uv4.w; // Unpack the material parameters materialFlags = material.flagsAndBaseTextureID & 0xFFFF; @@ -97,6 +159,7 @@ void main() { // Determine the base color f16vec3 baseColor = V16(unpackUnorm4x8(material.packedBaseColor)); + baseColor.rgb *= V16(filteredGrid(uv, ddx_uv, ddy_uv)); if ((materialFlags & MATERIAL_FLAG_HAS_BASE_COLOR_TEXTURE) != 0) { baseColor *= V16(texture(textures[baseTextureID], uv)); diff --git a/examples/custom-rendering/src/surface_solver.rs b/examples/custom-rendering/src/surface_solver.rs new file mode 100644 index 00000000..2b5bb9f9 --- /dev/null +++ b/examples/custom-rendering/src/surface_solver.rs @@ -0,0 +1,91 @@ +use hotham::{ + components::LocalTransform, + glam::{Mat4, Vec3}, + hecs::{Entity, World}, + na::{ArrayStorage, Matrix, U1, U10, U3}, + util::na_vector_from_glam, + Engine, +}; + +use crate::hologram::Hologram; + +type Matrix10x10 = Matrix>; +type Matrix10x3 = Matrix>; +type Vector10 = Matrix>; +type RowVector10 = Matrix>; + +pub struct ControlPoints { + pub entities: Vec, +} + +pub struct HologramBackside { + pub entity: Entity, +} + +pub fn surface_solver_system(engine: &mut Engine) { + let world = &mut engine.world; + surface_solver_system_inner(world); +} + +fn surface_solver_system_inner(world: &mut World) { + for (_, (hologram, control_points, local_transform)) in world + .query::<(&mut Hologram, &mut ControlPoints, &mut LocalTransform)>() + .iter() + { + let local_from_global = local_transform.to_affine().inverse(); + + #[allow(non_snake_case)] + let mut AtA: Matrix10x10 = Default::default(); + #[allow(non_snake_case)] + let mut BtB: Matrix10x10 = Default::default(); + #[allow(non_snake_case)] + let mut BtN: Vector10 = Default::default(); + + for e in &control_points.entities { + let t = world.get::<&LocalTransform>(*e).unwrap(); + let global_from_control = t.to_affine(); + let local_from_control = local_from_global * global_from_control; + let point_in_local = local_from_control.transform_point3(Vec3::ZERO); + let arrow_in_local = local_from_control.transform_vector3(Vec3::Y); + let p = na_vector_from_glam(point_in_local); + let d = na_vector_from_glam(arrow_in_local); + + let (x, y, z) = (p.x, p.y, p.z); + let a_row: RowVector10 = + [x * x, y * y, z * z, x * y, x * z, y * z, x, y, z, 1.0].into(); + AtA += a_row.transpose() * a_row; + let b_rows_t = Matrix10x3::from_columns(&[ + [2.0 * x, 0.0, 0.0, y, z, 0.0, 1.0, 0.0, 0.0, 0.].into(), + [0.0, 2.0 * y, 0.0, x, 0.0, z, 0.0, 1.0, 0.0, 0.].into(), + [0.0, 0.0, 2.0 * z, 0.0, x, y, 0.0, 0.0, 1.0, 0.].into(), + ]); + BtB += b_rows_t * b_rows_t.transpose(); + BtN += b_rows_t * d; + } + // let eigen_decomposition = AtA.symmetric_eigen(); + // eigen_decomposition + let eps = 1.0e-6; + let svd = AtA.svd(false, true); + let rank = svd.rank(eps).min(9); + let v_t = svd.v_t.unwrap(); + let nullspace = v_t.rows(rank, 10 - rank); + let svd_subspace = (nullspace * BtB * nullspace.transpose()).svd(true, true); + let solution_in_subspace = svd_subspace.pseudo_inverse(eps).unwrap() * nullspace * BtN; + let q = nullspace.transpose() * solution_in_subspace; + hologram.hologram_data.surface_q_in_local = Mat4::from_cols_array_2d(&[ + [2.0 * q[0], q[3], q[4], q[6]], + [q[3], 2.0 * q[1], q[5], q[7]], + [q[4], q[5], 2.0 * q[2], q[8]], + [q[6], q[7], q[8], 2.0 * q[9]], + ]); + } + + for (_, (target, source)) in world.query::<(&mut Hologram, &HologramBackside)>().iter() { + target.hologram_data.surface_q_in_local = world + .get::<&Hologram>(source.entity) + .unwrap() + .hologram_data + .surface_q_in_local + * -1.0; + } +} diff --git a/examples/shared/src/navigation.rs b/examples/shared/src/navigation.rs index ecaf2480..a1246fbf 100644 --- a/examples/shared/src/navigation.rs +++ b/examples/shared/src/navigation.rs @@ -1,5 +1,5 @@ use hotham::{ - components::{Collider, Hand, LocalTransform}, + components::{hand::Handedness, Hand, LocalTransform}, contexts::InputContext, glam::{Affine3A, Vec3}, hecs::{self, World}, @@ -28,16 +28,16 @@ fn navigation_system_inner( stage_entity: hecs::Entity, state: &mut State, ) { - // First, check to see if either of the hands have collided with anything. - let hands_have_collisions = world - .query::<&Collider>() - .with::<&Hand>() - .iter() - .any(|(_, collider)| !collider.collisions_this_frame.is_empty()); - - // If they have, then just return. - if hands_have_collisions { - return; + let mut left_hand_is_holding_something = false; + let mut right_hand_is_holding_something = false; + // First, check to see if either of the hands are holding anything. + for (_, hand) in world.query::<&Hand>().iter() { + if hand.grabbed_entity.is_some() { + match hand.handedness { + Handedness::Left => left_hand_is_holding_something = true, + Handedness::Right => right_hand_is_holding_something = true, + } + } } // Get the stage transform. @@ -49,21 +49,24 @@ fn navigation_system_inner( let stage_from_right_grip = input_context.right.stage_from_grip(); // Update grip states. - if input_context.left.grip_button_just_pressed() { + if input_context.left.grip_button_just_pressed() && !left_hand_is_holding_something { state.global_from_left_grip = Some(global_from_stage * stage_from_left_grip); } - if input_context.right.grip_button_just_pressed() { + if input_context.right.grip_button_just_pressed() && !right_hand_is_holding_something { state.global_from_right_grip = Some(global_from_stage * stage_from_right_grip); } - if input_context.right.grip_button() && input_context.left.grip_button_just_released() { + if input_context.right.grip_button() + && input_context.left.grip_button_just_released() + && !right_hand_is_holding_something + { // Handle when going from two grips to one state.global_from_right_grip = Some(global_from_stage * stage_from_right_grip); } - if !input_context.left.grip_button() { + if !input_context.left.grip_button() || left_hand_is_holding_something { state.global_from_left_grip = None; state.scale = None; } - if !input_context.right.grip_button() { + if !input_context.right.grip_button() || right_hand_is_holding_something { state.global_from_right_grip = None; } diff --git a/hotham-asset-client/src/client.rs b/hotham-asset-client/src/client.rs index 9ae6ed42..9030b28d 100644 --- a/hotham-asset-client/src/client.rs +++ b/hotham-asset-client/src/client.rs @@ -180,7 +180,7 @@ mod tests { #[test] fn it_works() -> Result<(), Box> { let (sender, mut receiver) = tokio::sync::mpsc::channel(100); - let files: Vec = vec![ + let files: Vec = [ "Many", "Different", "Files", diff --git a/hotham/src/components/hand.rs b/hotham/src/components/hand.rs index 6a876265..2c9bc2ff 100644 --- a/hotham/src/components/hand.rs +++ b/hotham/src/components/hand.rs @@ -1,3 +1,4 @@ +use glam::Affine3A; use hecs::Entity; /// A component that represents the "side" or "handedness" that an entity is on @@ -10,6 +11,12 @@ pub enum Handedness { Right, } +#[derive(Clone)] +pub struct GrabbedEntity { + pub entity: Entity, + pub grip_from_local: Affine3A, +} + /// A component that's added to an entity to represent a "hand" presence. /// Used to give the player a feeling of immersion by allowing them to grab objects in the world /// Requires `hands_system` @@ -17,10 +24,12 @@ pub enum Handedness { pub struct Hand { /// How much has this hand been gripped? pub grip_value: f32, + /// Did the grip button go from not pressed to pressed this frame? + pub grip_button_just_pressed: bool, /// Which side is this hand on? pub handedness: Handedness, /// Have we grabbed something? - pub grabbed_entity: Option, + pub grabbed_entity: Option, } impl Hand { @@ -28,6 +37,7 @@ impl Hand { pub fn left() -> Hand { Hand { grip_value: 0.0, + grip_button_just_pressed: false, handedness: Handedness::Left, grabbed_entity: None, } @@ -37,6 +47,7 @@ impl Hand { pub fn right() -> Hand { Hand { grip_value: 0.0, + grip_button_just_pressed: false, handedness: Handedness::Right, grabbed_entity: None, } diff --git a/hotham/src/systems/audio.rs b/hotham/src/systems/audio.rs index c28dd207..a2e7b6d8 100644 --- a/hotham/src/systems/audio.rs +++ b/hotham/src/systems/audio.rs @@ -167,6 +167,7 @@ mod tests { } } + #[allow(clippy::too_many_arguments)] fn tick( xr_context: &mut XrContext, audio_entity: Entity, @@ -218,9 +219,8 @@ mod tests { tell_me_that_i_cant: MusicTrack, ) { let mut source = world.get::<&mut SoundEmitter>(entity).unwrap(); - match source.current_state() { - SoundState::Stopped => source.play(), - _ => {} + if let SoundState::Stopped = source.current_state() { + source.play() } if start.elapsed().as_secs() >= 2 && audio_context.current_music_track != Some(right_here) { diff --git a/hotham/src/systems/grabbing.rs b/hotham/src/systems/grabbing.rs index 2cbcf97c..a71693c5 100644 --- a/hotham/src/systems/grabbing.rs +++ b/hotham/src/systems/grabbing.rs @@ -1,8 +1,10 @@ +use glam::Vec3; use hecs::World; use crate::{ components::{ - physics::BodyType, Collider, Grabbable, Grabbed, Hand, Parent, Released, RigidBody, + hand::GrabbedEntity, physics::BodyType, Collider, Grabbable, Grabbed, Hand, LocalTransform, + Parent, Released, RigidBody, }, Engine, }; @@ -30,52 +32,69 @@ fn grabbing_system_inner(world: &mut World) { let mut command_buffer = hecs::CommandBuffer::new(); - for (_, (hand, collider)) in world.query::<(&mut Hand, &Collider)>().iter() { + for (_, (hand, collider, local_transform)) in world + .query::<(&mut Hand, &Collider, &LocalTransform)>() + .iter() + { // Check to see if we are currently gripping if hand.grip_value > 0.1 { - // If we already have a grabbed entity, no need to do anything. - if hand.grabbed_entity.is_some() { - return; - }; + // Only grip when button is just pressed + if !hand.grip_button_just_pressed { + continue; + } + let global_from_grip = local_transform.to_affine(); + let grip_origin_in_global = global_from_grip.transform_point3(Vec3::ZERO); // Check to see if we are colliding with an entity + // Pick the entity closest to the grip origin + let mut closest_length_squared = f32::INFINITY; + let mut closest_grippable = None; for collided_entity in collider.collisions_this_frame.iter() { if world.get::<&Grabbable>(*collided_entity).is_ok() { - // If what we're grabbing has a rigid-body, set its body type to kinematic position based so it can be updated with the hand - if let Ok(mut rigid_body) = world.get::<&mut RigidBody>(*collided_entity) { - rigid_body.body_type = BodyType::KinematicPositionBased; - } - - // If the item we're grabbing has a parent, remove it - if world.entity(*collided_entity).unwrap().has::() { - println!( - "Removing parent from grabbed entity: {:?}", - *collided_entity - ); - command_buffer.remove_one::(*collided_entity); + let global_from_local = world + .get::<&LocalTransform>(*collided_entity) + .unwrap() + .to_affine(); + let local_origin_in_global = global_from_local.transform_point3(Vec3::ZERO); + let length_squared = + (local_origin_in_global - grip_origin_in_global).length_squared(); + if length_squared < closest_length_squared { + closest_length_squared = length_squared; + closest_grippable = Some(collided_entity); } - - // Add a "Grabbed" marker trait for other systems to read - command_buffer.insert_one(*collided_entity, Grabbed); - - // Store a reference to the grabbed entity - hand.grabbed_entity.replace(*collided_entity); - - break; } } + if let Some(entity) = closest_grippable { + // If the item we're grabbing has a parent, remove it + if world.entity(*entity).unwrap().has::() { + println!("Removing parent from grabbed entity: {:?}", *entity); + command_buffer.remove_one::(*entity); + } + + // Add a "Grabbed" marker trait for other systems to read + command_buffer.insert_one(*entity, Grabbed); + + // Store a reference to the grabbed entity + let global_from_local = world.get::<&LocalTransform>(*entity).unwrap().to_affine(); + let grip_from_local = global_from_grip.inverse() * global_from_local; + let grabbed_entity = GrabbedEntity { + entity: *entity, + grip_from_local, + }; + hand.grabbed_entity.replace(grabbed_entity); + } } else { // If we are not gripping, but we have a grabbed entity, release it if let Some(grabbed_entity) = hand.grabbed_entity.take() { // If what we're grabbing has a rigid-body, set it back to dynamic. // TODO: This is a bug. We could have grabbed a rigid-body that was originally kinematic! - if let Ok(mut rigid_body) = world.get::<&mut RigidBody>(grabbed_entity) { + if let Ok(mut rigid_body) = world.get::<&mut RigidBody>(grabbed_entity.entity) { rigid_body.body_type = BodyType::Dynamic; } // Add a marker trait for other systems to know that this item has at some point been grabbed - command_buffer.remove_one::(grabbed_entity); - command_buffer.insert_one(grabbed_entity, Released); + command_buffer.remove_one::(grabbed_entity.entity); + command_buffer.insert_one(grabbed_entity.entity, Released); } } } @@ -101,6 +120,7 @@ mod tests { node_id: 0, }, Grabbable {}, + LocalTransform::default(), )); world .insert(grabbed_entity, (grabbed_collider, grabbed_rigid_body)) @@ -110,6 +130,7 @@ mod tests { let hand = Hand { handedness: Handedness::Left, grip_value: 1.0, + grip_button_just_pressed: true, grabbed_entity: None, }; @@ -119,13 +140,17 @@ mod tests { ..Default::default() }; - let hand_entity = world.spawn((hand, collider)); + // A local transform is needed to determine the relative transform. + let local_transform = LocalTransform::default(); + + let hand_entity = world.spawn((hand, collider, local_transform)); tick(&mut world); let mut hand = world.get::<&mut Hand>(hand_entity).unwrap(); - assert_eq!(hand.grabbed_entity.unwrap(), grabbed_entity); + assert_eq!(hand.grabbed_entity.as_ref().unwrap().entity, grabbed_entity); hand.grip_value = 0.0; + hand.grip_button_just_pressed = false; drop(hand); tick(&mut world); @@ -135,6 +160,7 @@ mod tests { // Make sure hand can't grip colliders *without* a Grabbable component hand.grip_value = 1.0; + hand.grip_button_just_pressed = true; drop(hand); world.remove::<(Grabbable,)>(grabbed_entity).unwrap(); diff --git a/hotham/src/systems/hands.rs b/hotham/src/systems/hands.rs index 51bf07a4..ac8c6287 100644 --- a/hotham/src/systems/hands.rs +++ b/hotham/src/systems/hands.rs @@ -1,7 +1,9 @@ use crate::{ asset_importer::add_model_to_world, components::{ - global_transform::GlobalTransform, hand::Handedness, local_transform::LocalTransform, + global_transform::GlobalTransform, + hand::{GrabbedEntity, Handedness}, + local_transform::LocalTransform, stage, AnimationController, Collider, Grabbed, Hand, }, contexts::{physics_context::HAND_COLLISION_GROUP, InputContext}, @@ -32,42 +34,49 @@ pub fn hands_system_inner(world: &mut World, input_context: &InputContext) { .iter() { // Get the position of the hand in stage space. - let (stage_from_grip, grip_value) = match hand.handedness { + let (stage_from_grip, grip_value, grip_button_just_pressed) = match hand.handedness { Handedness::Left => ( input_context.left.stage_from_grip(), input_context.left.grip_analog(), + input_context.left.grip_button_just_pressed(), ), Handedness::Right => ( input_context.right.stage_from_grip(), input_context.right.grip_analog(), + input_context.right.grip_button_just_pressed(), ), }; // Get global transform - let global_from_local = global_from_stage * stage_from_grip; + let global_from_grip = global_from_stage * stage_from_grip; // Apply transform - local_transform.update_from_affine(&global_from_local); - global_transform.0 = global_from_local; + local_transform.update_from_affine(&global_from_grip); + global_transform.0 = global_from_grip; // If we've grabbed something, update its transform, being careful to preserve its scale. - if let Some(grabbed_entity) = hand.grabbed_entity { + if let Some(GrabbedEntity { + entity, + grip_from_local, + }) = hand.grabbed_entity + { // We first need to check if some other system has decided that this item should no longer be grabbed. - if !world.entity(grabbed_entity).unwrap().has::() { + if !world.entity(entity).unwrap().has::() { hand.grabbed_entity = None; } else { // OK. We are sure that this entity exists, and is being grabbed. - let mut local_transform = world.get::<&mut LocalTransform>(grabbed_entity).unwrap(); + let global_from_local = global_from_grip * grip_from_local; + let mut local_transform = world.get::<&mut LocalTransform>(entity).unwrap(); local_transform.update_rotation_translation_from_affine(&global_from_local); - let mut global_transform = - world.get::<&mut GlobalTransform>(grabbed_entity).unwrap(); + let mut global_transform = world.get::<&mut GlobalTransform>(entity).unwrap(); *global_transform = (*local_transform).into(); } } // Apply grip value to hand hand.grip_value = grip_value; + hand.grip_button_just_pressed = grip_button_just_pressed; // Apply to AnimationController animation_controller.blend_amount = grip_value; @@ -139,7 +148,7 @@ mod tests { let (mut world, input_context) = setup(); let expected_scale = Vec3::X * 1000.; - let grabbed_entity = world.spawn(( + let entity = world.spawn(( Grabbed, RigidBody::default(), LocalTransform { @@ -148,11 +157,15 @@ mod tests { }, GlobalTransform::default(), )); + let grabbed_entity = GrabbedEntity { + entity, + grip_from_local: Default::default(), + }; add_hand_to_world(&mut world, Some(grabbed_entity)); tick(&mut world, &input_context); - let local_transform = world.get::<&mut LocalTransform>(grabbed_entity).unwrap(); + let local_transform = world.get::<&mut LocalTransform>(entity).unwrap(); assert_relative_eq!(local_transform.translation, [-0.2, 1.4, -0.5].into()); // Make sure that scale gets preserved @@ -163,16 +176,20 @@ mod tests { pub fn test_ungrabbed_object_do_not_move() { let (mut world, input_context) = setup(); - let grabbed_entity = world.spawn(( + let entity = world.spawn(( RigidBody::default(), LocalTransform::default(), GlobalTransform::default(), )); + let grabbed_entity = GrabbedEntity { + entity, + grip_from_local: Default::default(), + }; add_hand_to_world(&mut world, Some(grabbed_entity)); tick(&mut world, &input_context); - let local_transform = world.get::<&mut LocalTransform>(grabbed_entity).unwrap(); + let local_transform = world.get::<&mut LocalTransform>(entity).unwrap(); assert_relative_eq!(local_transform.translation, Default::default()); assert_relative_eq!(local_transform.scale, Vec3::ONE); assert_relative_eq!(local_transform.rotation, Default::default()); @@ -189,7 +206,7 @@ mod tests { hands_system_inner(world, input_context); } - fn add_hand_to_world(world: &mut World, grabbed_entity: Option) -> Entity { + fn add_hand_to_world(world: &mut World, grabbed_entity: Option) -> Entity { let animation_controller = AnimationController { blend_amount: 100.0, // bogus value ..Default::default() diff --git a/hotham/src/systems/rendering.rs b/hotham/src/systems/rendering.rs index 0d5fc0c4..15a5fa6f 100644 --- a/hotham/src/systems/rendering.rs +++ b/hotham/src/systems/rendering.rs @@ -532,6 +532,7 @@ mod tests { assert!(errors.is_empty(), "{errors:#?}"); } + #[allow(clippy::too_many_arguments)] fn render_object_with_debug_data( vulkan_context: &VulkanContext, render_context: &mut RenderContext, diff --git a/test_assets/hologram_templates.glb b/test_assets/hologram_templates.glb new file mode 100644 index 00000000..f5393521 --- /dev/null +++ b/test_assets/hologram_templates.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b1766b1019fc57d9da019328b8ea9929b03082c4aae6dc216dbed4fe6cf2f12 +size 14236 diff --git a/test_assets/left_hand.glb b/test_assets/left_hand.glb index 5af8b121..a2cb46b7 100644 --- a/test_assets/left_hand.glb +++ b/test_assets/left_hand.glb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:477731f7b79019f6ea563034ac339d3edc13dbd2dd4a11c273d7edbf658f056e -size 95252 +oid sha256:33e57bcf816fe8b70250ca54b603f390e36b630ee0afe543ca66ca60f1344b3b +size 95132 diff --git a/test_assets/right_hand.glb b/test_assets/right_hand.glb index 3c282eca..149ac71e 100644 --- a/test_assets/right_hand.glb +++ b/test_assets/right_hand.glb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71403c859d7daccbd24241f624c1d2c365bd4ec9ee382decc91df87f27ee8ddf -size 95444 +oid sha256:92f636788d02bf908f96b04182aa9072f2d7f7718410dcc5b8928fd6e2b4e49b +size 95332