ar_hittest_subscription_inputSources.https.html (7524B)
1 <!DOCTYPE html> 2 <script src="/resources/testharness.js"></script> 3 <script src="/resources/testharnessreport.js"></script> 4 <script src="../resources/webxr_util.js"></script> 5 <script src="../resources/webxr_math_utils.js"></script> 6 <script src="../resources/webxr_test_asserts.js"></script> 7 <script src="../resources/webxr_test_constants.js"></script> 8 <script src="../resources/webxr_test_constants_fake_world.js"></script> 9 10 <script> 11 12 // 1m above world origin. 13 const VIEWER_ORIGIN_TRANSFORM = { 14 position: [0, 1, 0], 15 orientation: [0, 0, 0, 1], 16 }; 17 18 // 0.25m above world origin. 19 const FLOOR_ORIGIN_TRANSFORM = { 20 position: [0, -0.25, 0], 21 orientation: [0, 0, 0, 1], 22 }; 23 24 // Start the screen pointer at the same place as the viewer, so it's essentially 25 // coming straight forward from the middle of the screen. 26 const SCREEN_POINTER_TRANSFORM = VIEWER_ORIGIN_TRANSFORM; 27 28 const screen_controller_init = { 29 handedness: "none", 30 targetRayMode: "screen", 31 pointerOrigin: SCREEN_POINTER_TRANSFORM, // aka mojo_from_pointer 32 profiles: ["generic-touchscreen",] 33 }; 34 35 const fakeDeviceInitParams = { 36 supportedModes: ["immersive-ar"], 37 views: VALID_VIEWS, 38 floorOrigin: FLOOR_ORIGIN_TRANSFORM, // aka floor_from_mojo 39 viewerOrigin: VIEWER_ORIGIN_TRANSFORM, // aka mojo_from_viewer 40 supportedFeatures: ALL_FEATURES, 41 world: createFakeWorld(5.0, 2.0, 5.0), // see webxr_test_constants_fake_world.js for details 42 }; 43 44 // Generates a test function given the parameters for the hit test. 45 // |ray| - ray that will be used to subscribe to hit test. 46 // |expectedPoses| - array of expected pose objects. The poses should be expressed in local space. 47 // Null entries in the array mean that the given entry will not be validated. 48 // |inputFromPointer| - input from pointer transform that will be used as the input source's 49 // inputFromPointer (aka pointer origin) in subsequent rAF. 50 // |nextFrameExpectedPoses| - array of expected pose objects. The poses should be expressed in local space. 51 // Null entries in the array mean that the given entry will not be validated. 52 let testFunctionGenerator = function(ray, expectedPoses, inputFromPointer, nextFrameExpectedPoses) { 53 const testFunction = function(session, fakeDeviceController, t) { 54 return session.requestReferenceSpace('local').then((localRefSpace) => new Promise((resolve, reject) => { 55 56 const input_source_controller = fakeDeviceController.simulateInputSourceConnection(screen_controller_init); 57 58 requestSkipAnimationFrame(session, (time, frame) => { 59 t.step(() => { 60 assert_equals(session.inputSources.length, 1); 61 }); 62 63 const input_source = session.inputSources[0]; 64 const hitTestOptionsInit = { 65 space: input_source.targetRaySpace, 66 offsetRay: ray, 67 }; 68 69 session.requestHitTestSource(hitTestOptionsInit).then((hitTestSource) => { 70 t.step(() => { 71 assert_not_equals(hitTestSource, null); 72 }); 73 74 // We got a hit test source, now get the results in subsequent rAFcb: 75 session.requestAnimationFrame((time, frame) => { 76 const results = frame.getHitTestResults(hitTestSource); 77 78 t.step(() => { 79 assert_equals(results.length, expectedPoses.length); 80 for(const [index, expectedPose] of expectedPoses.entries()) { 81 const pose = results[index].getPose(localRefSpace); 82 assert_true(pose != null, "Each hit test result should have a pose in local space"); 83 if(expectedPose != null) { 84 assert_transform_approx_equals(pose.transform, expectedPose, FLOAT_EPSILON, "before-move-pose: "); 85 } 86 } 87 }); 88 89 input_source_controller.setPointerOrigin(inputFromPointer, false); 90 91 session.requestAnimationFrame((time, frame) => { 92 const results = frame.getHitTestResults(hitTestSource); 93 94 t.step(() => { 95 assert_equals(results.length, nextFrameExpectedPoses.length); 96 for(const [index, expectedPose] of nextFrameExpectedPoses.entries()) { 97 const pose = results[index].getPose(localRefSpace); 98 assert_true(pose != null, "Each hit test result should have a pose in local space"); 99 if(expectedPose != null) { 100 assert_transform_approx_equals(pose.transform, expectedPose, FLOAT_EPSILON, "after-move-pose: "); 101 } 102 } 103 }); 104 105 resolve(); 106 }); 107 }); 108 }); 109 }); 110 })); 111 }; 112 113 return testFunction; 114 }; 115 116 117 // Pose of the first expected hit test result - straight ahead of the input source, viewer-facing. 118 const pose_1 = { 119 position: {x: 0.0, y: 1.0, z: -2.5, w: 1.0}, 120 orientation: {x: 0.0, y: -0.707, z: -0.707, w: 0.0}, 121 // Hit test API will set Y axis to the surface normal at the intersection point, 122 // Z axis towards the ray origin and X axis to cross product of Y axis & Z axis. 123 // If the surface normal and Z axis would be parallel, the hit test API 124 // will attempt to use `up` vector ([0, 1, 0]) as the Z axis, and if it so happens that Z axis 125 // and the surface normal would still be parallel, it will use the `right` vector ([1, 0, 0]) as the Z axis. 126 // In this particular case, `up` vector will work so the resulting pose.orientation 127 // becomes a rotation around [0, 1, 1] vector by 180 degrees. 128 }; 129 130 xr_session_promise_test("Ensures subscription to hit test works with an XRSpace from input source - no move", 131 testFunctionGenerator(new XRRay(), [pose_1], SCREEN_POINTER_TRANSFORM, [pose_1]), 132 fakeDeviceInitParams, 133 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); 134 135 const moved_pointer_transform_1 = { 136 position: [0, 1, 0], 137 orientation: [ 0.707, 0, 0, 0.707 ] // 90 degrees around X axis = facing up 138 }; 139 140 xr_session_promise_test("Ensures subscription to hit test works with an XRSpace from input source - after move - no results", 141 testFunctionGenerator(new XRRay(), [pose_1], moved_pointer_transform_1, []), 142 fakeDeviceInitParams, 143 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); 144 145 const pose_2 = { 146 position: {x: -1.443, y: 1.0, z: -2.5, w: 1.0}, 147 // Intersection point will be on the same height as the viewer, on the front 148 // wall. Distance from the front wall to viewer is 2.5m, and we are rotating 149 // to the left, so X coordinate of the intersection point will be negative 150 // & equal to -2.5 * tan(30 deg) ~= 1.443m. 151 orientation: {x: 0.5, y: 0.5, z: 0.5, w: 0.5 }, 152 // See comment for pose_1.orientation for details. 153 // In this case, the hit test pose will have Y axis facing towards world's 154 // positive Z axis ([0,0,1]), Z axis to the right ([1,0,0]) and X axis 155 // towards world's Y axis ([0,1,0]). 156 // This is equivalent to the rotation around [1, 1, 1] vector by 120 degrees. 157 }; 158 159 const moved_pointer_transform_2 = { 160 position: [0, 1, 0], 161 orientation: [ 0, 0.2588, 0, 0.9659 ] // 30 degrees around Y axis = to the left, 162 // creating 30-60-90 triangle with the front wall 163 }; 164 165 xr_session_promise_test("Ensures subscription to hit test works with an XRSpace from input source - after move - 1 result", 166 testFunctionGenerator(new XRRay(), [pose_1], moved_pointer_transform_2, [pose_2]), 167 fakeDeviceInitParams, 168 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); 169 170 </script>