ar_hittest_subscription_transientInputSources.https.html (7994B)
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 transient 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 let debug = xr_debug.bind(this, 'testFunction'); 55 return session.requestReferenceSpace('local').then((localRefSpace) => new Promise((resolve, reject) => { 56 57 const input_source_controller = fakeDeviceController.simulateInputSourceConnection(screen_controller_init); 58 59 requestSkipAnimationFrame(session, (time, frame) => { 60 debug('rAF 1'); 61 t.step(() => { 62 assert_equals(session.inputSources.length, 1); 63 }); 64 65 const hitTestOptionsInit = { 66 profile: "generic-touchscreen", 67 offsetRay: ray, 68 }; 69 70 session.requestHitTestSourceForTransientInput(hitTestOptionsInit) 71 .then((hitTestSource) => { 72 t.step(() => { 73 assert_not_equals(hitTestSource, null); 74 }); 75 76 // We got a hit test source, now get the results in subsequent rAFcb: 77 session.requestAnimationFrame((time, frame) => { 78 debug('rAF 2'); 79 const results = frame.getHitTestResultsForTransientInput(hitTestSource); 80 81 t.step(() => { 82 assert_true(results != null, "Transient input hit tests should not be null"); 83 assert_equals(results.length, 1, "There should be exactly one group of transient hit test results!"); 84 assert_equals(results[0].results.length, expectedPoses.length); 85 for(const [index, expectedPose] of expectedPoses.entries()) { 86 const pose = results[0].results[index].getPose(localRefSpace); 87 assert_true(pose != null, "Each hit test result should have a pose in local space"); 88 if(expectedPose != null) { 89 assert_transform_approx_equals(pose.transform, expectedPose, FLOAT_EPSILON, "before-move-pose: "); 90 } 91 } 92 }); 93 94 input_source_controller.setPointerOrigin(inputFromPointer, false); 95 96 session.requestAnimationFrame((time, frame) => { 97 debug('rAF 3'); 98 const results = frame.getHitTestResultsForTransientInput(hitTestSource); 99 100 t.step(() => { 101 assert_equals(results[0].results.length, nextFrameExpectedPoses.length); 102 for(const [index, expectedPose] of nextFrameExpectedPoses.entries()) { 103 const pose = results[0].results[index].getPose(localRefSpace); 104 assert_true(pose != null, "Each hit test result should have a pose in local space"); 105 if(expectedPose != null) { 106 assert_transform_approx_equals(pose.transform, expectedPose, FLOAT_EPSILON, "after-move-pose: "); 107 } 108 } 109 }); 110 111 debug('resolving'); 112 resolve(); 113 }); 114 }); 115 }); 116 }); 117 })); 118 }; 119 120 return testFunction; 121 }; 122 123 124 // Pose of the first expected hit test result - straight ahead of the input source, viewer-facing. 125 const pose_1 = { 126 position: {x: 0.0, y: 1.0, z: -2.5, w: 1.0}, 127 orientation: {x: 0.0, y: -0.707, z: -0.707, w: 0.0}, 128 // Hit test API will set Y axis to the surface normal at the intersection point, 129 // Z axis towards the ray origin and X axis to cross product of Y axis & Z axis. 130 // If the surface normal and Z axis would be parallel, the hit test API 131 // will attempt to use `up` vector ([0, 1, 0]) as the Z axis, and if it so happens that Z axis 132 // and the surface normal would still be parallel, it will use the `right` vector ([1, 0, 0]) as the Z axis. 133 // In this particular case, `up` vector will work so the resulting pose.orientation 134 // becomes a rotation around [0, 1, 1] vector by 180 degrees. 135 }; 136 137 xr_session_promise_test("Ensures subscription to transient hit test works with an XRSpace from input source - no move", 138 testFunctionGenerator(new XRRay(), [pose_1], SCREEN_POINTER_TRANSFORM, [pose_1]), 139 fakeDeviceInitParams, 140 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); 141 142 const moved_pointer_transform_1 = { 143 position: [0, 1, 0], 144 orientation: [ 0.707, 0, 0, 0.707 ] // 90 degrees around X axis = facing up 145 }; 146 147 xr_session_promise_test("Ensures subscription to transient hit test works with an XRSpace from input source - after move - no results", 148 testFunctionGenerator(new XRRay(), [pose_1], moved_pointer_transform_1, []), 149 fakeDeviceInitParams, 150 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); 151 152 const pose_2 = { 153 position: {x: -1.443, y: 1.0, z: -2.5, w: 1.0}, 154 // Intersection point will be on the same height as the viewer, on the front 155 // wall. Distance from the front wall to viewer is 2.5m, and we are rotating 156 // to the left, so X coordinate of the intersection point will be negative 157 // & equal to -2.5 * tan(30 deg) ~= 1.443m. 158 orientation: {x: 0.5, y: 0.5, z: 0.5, w: 0.5 }, 159 // See comment for pose_1.orientation for details. 160 // In this case, the hit test pose will have Y axis facing towards world's 161 // positive Z axis ([0,0,1]), Z axis to the right ([1,0,0]) and X axis 162 // towards world's Y axis ([0,1,0]). 163 // This is equivalent to the rotation around [1, 1, 1] vector by 120 degrees. 164 }; 165 166 const moved_pointer_transform_2 = { 167 position: [0, 1, 0], 168 orientation: [ 0, 0.2588, 0, 0.9659 ] // 30 degrees around Y axis = to the left, 169 // creating 30-60-90 triangle with the front wall 170 }; 171 172 xr_session_promise_test("Ensures subscription to transient hit test works with an XRSpace from input source - after move - 1 result", 173 testFunctionGenerator(new XRRay(), [pose_1], moved_pointer_transform_2, [pose_2]), 174 fakeDeviceInitParams, 175 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); 176 177 </script>