author | Piotr Bialecki <bialpio@chromium.org> |
Sat, 14 Mar 2020 11:28:25 +0000 | |
changeset 518838 | 917205af08c0d042325797003bc9051e96aa8159 |
parent 518837 | ea93b29b3730ecb9638b270bfd4348028bde8b96 |
child 518839 | 5508fdda7dd9399556c7f503b25e34d9bdc121fc |
push id | 37217 |
push user | ccoroiu@mozilla.com |
push date | Sun, 15 Mar 2020 21:37:59 +0000 |
treeherder | mozilla-central@f9fc9427476e [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | testonly |
bugs | 1621802, 22200, 2015182, 749867 |
milestone | 76.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/testing/web-platform/tests/resources/chromium/webxr-test-math-helper.js +++ b/testing/web-platform/tests/resources/chromium/webxr-test-math-helper.js @@ -214,9 +214,30 @@ class XRMathHelper { invDet * det3x3(m00, m02, m03, m10, m12, m13, m20, m22, m23), invDet * det3x3(m00, m01, m03, m10, m11, m13, m20, m21, m23), invDet * det3x3(m00, m01, m02, m10, m11, m12, m20, m21, m22), ]; // Actual inverse is `1/det * transposed(comatrix)`: return XRMathHelper.transpose(result2); } + + static mul4x4(m1, m2) { + if (m1 == null || m2 == null) { + return null; + } + + const result = Array(16); + + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + result[4 * col + row] = 0; + for(let i = 0; i < 4; i++) { + result[4 * col + row] += m1[4 * i + row] * m2[4 * col + i]; + } + } + } + + return result; + } } + +XRMathHelper.EPSILON = 0.001;
--- a/testing/web-platform/tests/resources/chromium/webxr-test.js +++ b/testing/web-platform/tests/resources/chromium/webxr-test.js @@ -854,17 +854,17 @@ class MockRuntime { const edge_AB = sub(point_B, point_A); const edge_AC = sub(point_C, point_A); const normal = normalize(cross(edge_AB, edge_AC)); const numerator = dot(sub(point_A, origin), normal); const denominator = dot(direction, normal); - if (Math.abs(denominator) < 0.0001) { + if (Math.abs(denominator) < XRMathHelper.EPSILON) { // Planes are nearly parallel - there's either infinitely many intersection points or 0. // Both cases signify a "no hit" for us. return null; } else { // Single intersection point between the infinite plane and the line (*not* ray). // Need to calculate the hit test matrix taking into account the face vertices. const distance = numerator / denominator; if (distance < 0) { @@ -873,23 +873,23 @@ class MockRuntime { } else { const intersection_point = add(origin, mul(distance, direction)); // Since we are treating the face as a solid, flip the normal so that its // half-space will contain the ray origin. const y_axis = denominator > 0 ? neg(normal) : normal; let z_axis = null; const cos_direction_and_y_axis = dot(direction, y_axis); - if (Math.abs(cos_direction_and_y_axis) > 0.9999) { + if (Math.abs(cos_direction_and_y_axis) > (1 - XRMathHelper.EPSILON)) { // Ray and the hit test normal are co-linear - try using the 'up' or 'right' vector's projection on the face plane as the Z axis. // Note: this edge case is currently not covered by the spec. const up = {x: 0.0, y: 1.0, z: 0.0, w: 0.0}; - const right = {x:1.0, y: 0.0, z: 0.0, w: 0.0}; + const right = {x: 1.0, y: 0.0, z: 0.0, w: 0.0}; - z_axis = Math.abs(dot(up, y_axis)) > 0.9999 + z_axis = Math.abs(dot(up, y_axis)) > (1 - XRMathHelper.EPSILON) ? sub(up, mul(dot(right, y_axis), y_axis)) // `up is also co-linear with hit test normal, use `right` : sub(up, mul(dot(up, y_axis), y_axis)); // `up` is not co-linear with hit test normal, use it } else { // Project the ray direction onto the plane, negate it and use as a Z axis. z_axis = neg(sub(direction, mul(cos_direction_and_y_axis, y_axis))); // Z should point towards the ray origin, not away. } const x_axis = normalize(cross(y_axis, z_axis)); @@ -927,57 +927,83 @@ class MockRuntime { hitResult.hitMatrix.matrix[14] = intersection_point.z; hitResult.hitMatrix.matrix[15] = 1; return hitResult; } } } + _getMojoFromInputSource(mojo_from_viewer, input_source) { + if(input_source.target_ray_mode_ === 'gaze') { // XRTargetRayMode::GAZING + // If the pointer origin is gaze, then the result is + // just mojo_from_viewer. + return mojo_from_viewer; + } else if(input_source.target_ray_mode_ === 'tracked-pointer') { // XRTargetRayMode:::POINTING + // If the pointer origin is tracked-pointer, the result is just + // mojo_from_input*input_from_pointer. + return XRMathHelper.mul4x4( + input_source.mojo_from_input_.matrix, + input_source.input_from_pointer_.matrix); + } else if(input_source.target_ray_mode_ === 'screen') { // XRTargetRayMode::TAPPING + // If the pointer origin is screen, the input_from_pointer is + // equivalent to viewer_from_pointer and the result is + // mojo_from_viewer*viewer_from_pointer. + return XRMathHelper.mul4x4( + mojo_from_viewer, + input_source.input_from_pointer_.matrix); + } else { + return null + } + } + _getMojoFromNativeOrigin(nativeOriginInformation) { const identity = function() { return [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]; }; + const transform = { + position: [ + this.pose_.position.x, + this.pose_.position.y, + this.pose_.position.z], + orientation: [ + this.pose_.orientation.x, + this.pose_.orientation.y, + this.pose_.orientation.z, + this.pose_.orientation.w], + }; + + const mojo_from_viewer = getMatrixFromTransform(transform) + if (nativeOriginInformation.$tag == device.mojom.XRNativeOriginInformation.Tags.inputSourceId) { if (!this.input_sources_.has(nativeOriginInformation.inputSourceId)) { return null; } else { const inputSource = this.input_sources_.get(nativeOriginInformation.inputSourceId); - return inputSource.mojo_from_input_.matrix; + return this._getMojoFromInputSource(mojo_from_viewer, inputSource); } } else if (nativeOriginInformation.$tag == device.mojom.XRNativeOriginInformation.Tags.referenceSpaceCategory) { switch (nativeOriginInformation.referenceSpaceCategory) { case device.mojom.XRReferenceSpaceCategory.LOCAL: return identity(); case device.mojom.XRReferenceSpaceCategory.LOCAL_FLOOR: if (this.stageParameters_ == null || this.stageParameters_.standingTransform == null) { console.warn("Standing transform not available."); return null; } // this.stageParameters_.standingTransform = floor_from_mojo aka native_origin_from_mojo return XRMathHelper.inverse(this.stageParameters_.standingTransform.matrix); case device.mojom.XRReferenceSpaceCategory.VIEWER: - const transform = { - position: [ - this.pose_.position.x, - this.pose_.position.y, - this.pose_.position.z], - orientation: [ - this.pose_.orientation.x, - this.pose_.orientation.y, - this.pose_.orientation.z, - this.pose_.orientation.w], - }; - return getMatrixFromTransform(transform); // this.pose_ = mojo_from_viewer + return mojo_from_viewer; case device.mojom.XRReferenceSpaceCategory.BOUNDED_FLOOR: return null; case device.mojom.XRReferenceSpaceCategory.UNBOUNDED: return null; default: throw new TypeError("Unrecognized XRReferenceSpaceCategory!"); } } else { @@ -994,16 +1020,21 @@ class MockXRSessionMetricsRecorder { } class MockXRInputSource { constructor(fakeInputSourceInit, id, pairedDevice) { this.source_id_ = id; this.pairedDevice_ = pairedDevice; this.handedness_ = fakeInputSourceInit.handedness; this.target_ray_mode_ = fakeInputSourceInit.targetRayMode; + + if(fakeInputSourceInit.pointerOrigin == null) { + throw new TypeError("FakeXRInputSourceInit.pointerOrigin is required."); + } + this.setPointerOrigin(fakeInputSourceInit.pointerOrigin); this.setProfiles(fakeInputSourceInit.profiles); this.primary_input_pressed_ = false; if (fakeInputSourceInit.selectionStarted != null) { this.primary_input_pressed_ = fakeInputSourceInit.selectionStarted; }
new file mode 100644 --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_inputSources.https.html @@ -0,0 +1,171 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> +<canvas /> + +<script> + +// 1m above world origin. +const VIEWER_ORIGIN_TRANSFORM = { + position: [0, 1, 0], + orientation: [0, 0, 0, 1], +}; + +// 0.25m above world origin. +const FLOOR_ORIGIN_TRANSFORM = { + position: [0, -0.25, 0], + orientation: [0, 0, 0, 1], +}; + +const SCREEN_POINTER_TRANSFORM = { + position: [0, 0, 0], // middle of the screen + orientation: [0, 0, 0, 1] // forward-facing +}; + +const screen_controller_init = { + handedness: "none", + targetRayMode: "screen", + pointerOrigin: SCREEN_POINTER_TRANSFORM, // aka input_from_pointer + profiles: ["generic-touchscreen",] +}; + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + floorOrigin: FLOOR_ORIGIN_TRANSFORM, // aka floor_from_mojo + viewerOrigin: VIEWER_ORIGIN_TRANSFORM, // aka mojo_from_viewer + supportedFeatures: ALL_FEATURES, + world: createFakeWorld(5.0, 2.0, 5.0), // see webxr_test_constants_fake_world.js for details +}; + +// Generates a test function given the parameters for the hit test. +// |ray| - ray that will be used to subscribe to hit test. +// |expectedPoses| - array of expected pose objects. The poses should be expressed in local space. +// Null entries in the array mean that the given entry will not be validated. +// |inputFromPointer| - input from pointer transform that will be used as the input source's +// inputFromPointer (aka pointer origin) in subsequent rAF. +// |nextFrameExpectedPoses| - array of expected pose objects. The poses should be expressed in local space. +// Null entries in the array mean that the given entry will not be validated. +let testFunctionGenerator = function(ray, expectedPoses, inputFromPointer, nextFrameExpectedPoses) { + const testFunction = function(session, fakeDeviceController, t) { + return session.requestReferenceSpace('local').then((localRefSpace) => new Promise((resolve, reject) => { + + const input_source_controller = fakeDeviceController.simulateInputSourceConnection(screen_controller_init); + + session.requestAnimationFrame((time, frame) => { + t.step(() => { + assert_equals(session.inputSources.length, 1); + }); + + const input_source = session.inputSources[0]; + const hitTestOptionsInit = { + space: input_source.targetRaySpace, + offsetRay: ray, + }; + + session.requestHitTestSource(hitTestOptionsInit).then((hitTestSource) => { + t.step(() => { + assert_not_equals(hitTestSource, null); + }); + + // We got a hit test source, now get the results in subsequent rAFcb: + session.requestAnimationFrame((time, frame) => { + const results = frame.getHitTestResults(hitTestSource); + + t.step(() => { + assert_equals(results.length, expectedPoses.length); + for(const [index, expectedPose] of expectedPoses.entries()) { + const pose = results[index].getPose(localRefSpace); + assert_true(pose != null, "Each hit test result should have a pose in local space"); + if(expectedPose != null) { + assert_transform_approx_equals(pose.transform, expectedPose, FLOAT_EPSILON, "before-move-pose: "); + } + } + }); + + input_source_controller.setPointerOrigin(inputFromPointer, false); + + session.requestAnimationFrame((time, frame) => { + const results = frame.getHitTestResults(hitTestSource); + + t.step(() => { + assert_equals(results.length, nextFrameExpectedPoses.length); + for(const [index, expectedPose] of nextFrameExpectedPoses.entries()) { + const pose = results[index].getPose(localRefSpace); + assert_true(pose != null, "Each hit test result should have a pose in local space"); + if(expectedPose != null) { + assert_transform_approx_equals(pose.transform, expectedPose, FLOAT_EPSILON, "after-move-pose: "); + } + } + }); + + resolve(); + }); + }); + }); + }); + })); + }; + + return testFunction; +}; + + +// Pose of the first expected hit test result - straight ahead of the input source, viewer-facing. +const pose_1 = { + position: {x: 0.0, y: 1.0, z: -2.5, w: 1.0}, + orientation: {x: 0.0, y: -0.707, z: -0.707, w: 0.0}, + // Hit test API will set Y axis to the surface normal at the intersection point, + // Z axis towards the ray origin and X axis to cross product of Y axis & Z axis. + // If the surface normal and Z axis would be parallel, the hit test API + // will attempt to use `up` vector ([0, 1, 0]) as the Z axis, and if it so happens that Z axis + // and the surface normal would still be parallel, it will use the `right` vector ([1, 0, 0]) as the Z axis. + // In this particular case, `up` vector will work so the resulting pose.orientation + // becomes a rotation around [0, 1, 1] vector by 180 degrees. +}; + +xr_session_promise_test("Ensures subscription to hit test works with an XRSpace from input source - no move", + testFunctionGenerator(new XRRay(), [pose_1], SCREEN_POINTER_TRANSFORM, [pose_1]), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +const moved_pointer_transform_1 = { + position: [0, 0, 0], // middle of the screen + orientation: [ 0.707, 0, 0, 0.707 ] // 90 degrees around X axis = facing up +}; + +xr_session_promise_test("Ensures subscription to hit test works with an XRSpace from input source - after move - no results", + testFunctionGenerator(new XRRay(), [pose_1], moved_pointer_transform_1, []), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +const pose_2 = { + position: {x: -1.443, y: 1.0, z: -2.5, w: 1.0}, + // Intersection point will be on the same height as the viewer, on the front + // wall. Distance from the front wall to viewer is 2.5m, and we are rotating + // to the left, so X coordinate of the intersection point will be negative + // & equal to -2.5 * tan(30 deg) ~= 1.443m. + orientation: {x: 0.5, y: 0.5, z: 0.5, w: 0.5 }, + // See comment for pose_1.orientation for details. + // In this case, the hit test pose will have Y axis facing towards world's + // positive Z axis ([0,0,1]), Z axis to the right ([1,0,0]) and X axis + // towards world's Y axis ([0,1,0]). + // This is equivalent to the rotation around [1, 1, 1] vector by 120 degrees. +}; + +const moved_pointer_transform_2 = { + position: [0, 0, 0], // middle of the screen + orientation: [ 0, 0.2588, 0, 0.9659 ] // 30 degrees around Y axis = to the left, + // creating 30-60-90 triangle with the front wall +}; + +xr_session_promise_test("Ensures subscription to hit test works with an XRSpace from input source - after move - 1 result", + testFunctionGenerator(new XRRay(), [pose_1], moved_pointer_transform_2, [pose_2]), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +</script>
--- a/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_refSpaces.https.html +++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_refSpaces.https.html @@ -59,17 +59,17 @@ let testFunctionGenerator = function(ray const requestAnimationFrameCallback = function(time, frame) { const hitTestResults = frame.getHitTestResults(hitTestSource); t.step(() => { assert_equals(hitTestResults.length, expectedPoses.length, "Results length should match expected results length"); for(const [index, expectedPose] of expectedPoses.entries()) { const pose = hitTestResults[index].getPose(localRefSpace); - assert_true(pose != null, "Each hit test result should have a pose in viewer space"); + assert_true(pose != null, "Each hit test result should have a pose in local space"); if(expectedPose != null) { assert_transform_approx_equals(pose.transform, expectedPose); } } }); resolve(); };