Commit 82276ea1 authored by Piotr Bialecki's avatar Piotr Bialecki Committed by Commit Bot

WebXR - AR - anchor WPTs - basic anchor creation tests

Change-Id: I2f05f6eaa5ea7bcbaa27e0a8f4472affb42afa5e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2168768
Commit-Queue: Piotr Bialecki <bialpio@chromium.org>
Reviewed-by: default avatarAlexander Cooper <alcooper@chromium.org>
Cr-Commit-Position: refs/heads/master@{#773348}
parent 3a4e77ad
......@@ -87,6 +87,8 @@ const char kHitTestFeatureNotSupported[] =
const char kHitTestSubscriptionFailed[] = "Hit test subscription failed.";
const char kAnchorCreationFailed[] = "Anchor creation failed.";
const char kLightEstimationFeatureNotSupported[] =
"Light estimation feature is not supported.";
......@@ -914,6 +916,8 @@ void XRSession::OnSubscribeToHitTestForTransientInputResult(
void XRSession::OnCreateAnchorResult(ScriptPromiseResolver* resolver,
device::mojom::CreateAnchorResult result,
uint64_t id) {
DVLOG(2) << __func__ << ": result=" << result << ", id=" << id;
DCHECK(create_anchor_promises_.Contains(resolver));
create_anchor_promises_.erase(resolver);
......@@ -922,7 +926,8 @@ void XRSession::OnCreateAnchorResult(ScriptPromiseResolver* resolver,
// must contain newly created anchor data.
anchor_ids_to_pending_anchor_promises_.insert(id, resolver);
} else {
resolver->Reject();
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kOperationError, kAnchorCreationFailed));
}
}
......
......@@ -239,6 +239,52 @@ class XRMathHelper {
return result;
}
// Decomposes a matrix, with the assumption that the passed in matrix is
// a rigid transformation (i.e. position and rotation *only*!).
// The result is an object with `position` and `orientation` keys, which should
// be compatible with FakeXRRigidTransformInit.
// The implementation should match the behavior of gfx::Transform, but assumes
// that scale, skew & perspective are not present in the matrix so it could be
// simplified.
static decomposeRigidTransform(m) {
const m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3];
const m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7];
const m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11];
const m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15];
const position = [m30, m31, m32];
const orientation = [0, 0, 0, 0];
const trace = m00 + m11 + m22;
if (trace > 0) {
const S = Math.sqrt(trace + 1) * 2;
orientation[3] = 0.25 * S;
orientation[0] = (m12 - m21) / S;
orientation[1] = (m20 - m02) / S;
orientation[2] = (m01 - m10) / S;
} else if (m00 > m11 && m00 > m22) {
const S = Math.sqrt(1.0 + m00 - m11 - m22) * 2;
orientation[3] = (m12 - m21) / S;
orientation[0] = 0.25 * S;
orientation[1] = (m01 + m10) / S;
orientation[2] = (m20 + m02) / S;
} else if (m11 > m22) {
const S = Math.sqrt(1.0 + m11 - m00 - m22) * 2;
orientation[3] = (m20 - m02) / S;
orientation[0] = (m01 + m10) / S;
orientation[1] = 0.25 * S;
orientation[2] = (m12 + m21) / S;
} else {
const S = Math.sqrt(1.0 + m22 - m00 - m11) * 2;
orientation[3] = (m01 - m10) / S;
orientation[0] = (m20 + m02) / S;
orientation[1] = (m12 + m21) / S;
orientation[2] = 0.25 * S;
}
return { position, orientation };
}
static identity() {
return [
1, 0, 0, 0,
......
......@@ -201,9 +201,115 @@ class MockVRService {
}
}
class FakeXRAnchorController {
constructor() {
// Private properties.
this.device_ = null;
this.id_ = null;
this.dirty_ = true;
// Properties backing up public attributes / methods.
this.deleted_ = false;
this.paused_ = false;
this.anchorOrigin_ = XRMathHelper.identity();
}
get deleted() {
return this.deleted_;
}
pauseTracking() {
if(!this.paused_) {
this.paused_ = true;
this.dirty_ = true;
}
}
resumeTracking() {
if(this.paused_) {
this.paused_ = false;
this.dirty_ = true;
}
}
stopTracking() {
if(!this.deleted_) {
this.device_.deleteAnchorController(this.id_);
this.deleted_ = true;
this.dirty_ = true;
}
}
setAnchorOrigin(anchorOrigin) {
this.anchorOrigin_ = getMatrixFromTransform(anchorOrigin);
this.dirty_ = true;
}
// Internal implementation:
set id(value) {
this.id_ = value;
}
set device(value) {
this.device_ = value;
}
get dirty() {
return this.dirty_;
}
markProcessed() {
this.dirty_ = false;
}
getAnchorOrigin() {
return this.anchorOrigin_;
}
}
class FakeXRAnchorCreationEvent extends Event {
constructor(type, eventInitDict) {
super(type, eventInitDict);
this.success_ = false;
this.requestedAnchorOrigin_ = {};
this.isAttachedToEntity_ = false;
this.anchorController_ = new FakeXRAnchorController();
if(eventInitDict.requestedAnchorOrigin != null) {
this.requestedAnchorOrigin_ = eventInitDict.requestedAnchorOrigin;
}
if(eventInitDict.isAttachedToEntity != null) {
this.isAttachedToEntity_ = eventInitDict.isAttachedToEntity;
}
}
get requestedAnchorOrigin() {
return this.requestedAnchorOrigin_;
}
get isAttachedToEntity() {
return this.isAttachedToEntity_;
}
get success() {
return this.success_;
}
set success(value) {
this.success_ = value;
}
get anchorController() {
return this.anchorController_;
}
}
// Implements XRFrameDataProvider and XRPresentationProvider. Maintains a mock
// for XRPresentationProvider.
class MockRuntime {
// for XRPresentationProvider. Implements FakeXRDevice test API.
class MockRuntime extends EventTarget {
// Mapping from string feature names to the corresponding mojo types.
// This is exposed as a member for extensibility.
static featureToMojoMap = {
......@@ -215,6 +321,7 @@ class MockRuntime {
'hit-test': device.mojom.XRSessionFeature.HIT_TEST,
'dom-overlay': device.mojom.XRSessionFeature.DOM_OVERLAY,
'light-estimation': device.mojom.XRSessionFeature.LIGHT_ESTIMATION,
'anchors': device.mojom.XRSessionFeature.ANCHORS,
};
static sessionModeToMojoMap = {
......@@ -224,6 +331,8 @@ class MockRuntime {
};
constructor(fakeDeviceInit, service) {
super();
this.sessionClient_ = new device.mojom.XRSessionClientPtr();
this.presentation_provider_ = new MockXRPresentationProvider();
......@@ -248,6 +357,10 @@ class MockRuntime {
// ID of the next subscription to be assigned.
this.next_hit_test_id_ = 1;
this.anchor_controllers_ = new Map();
// ID of the next anchor to be assigned.
this.next_anchor_id_ = 1;
let supportedModes = [];
if (fakeDeviceInit.supportedModes) {
supportedModes = fakeDeviceInit.supportedModes.slice();
......@@ -571,6 +684,11 @@ class MockRuntime {
this.input_sources_.delete(source.source_id_);
}
// These methods are intended to be used by FakeXRAnchorController only.
deleteAnchorController(controllerId) {
this.anchor_controllers_.delete(controllerId);
}
// Extension point for non-standard modules.
_injectAdditionalFrameData(options, frameData) {
......@@ -623,6 +741,8 @@ class MockRuntime {
this._calculateHitTestResults(frameData);
this._calculateAnchorInformation(frameData);
this._injectAdditionalFrameData(options, frameData);
return Promise.resolve({
......@@ -662,25 +782,7 @@ class MockRuntime {
});
}
if (nativeOriginInformation.$tag == device.mojom.XRNativeOriginInformation.Tags.inputSourceId) {
if (!this.input_sources_.has(nativeOriginInformation.inputSourceId)) {
// Reject - unknown input source ID.
return Promise.resolve({
result : device.mojom.SubscribeToHitTestResult.FAILURE_GENERIC,
subscriptionId : 0
});
}
} else if (nativeOriginInformation.$tag == device.mojom.XRNativeOriginInformation.Tags.referenceSpaceCategory) {
// Bounded_floor & unbounded ref spaces are not yet supported for AR:
if (nativeOriginInformation.referenceSpaceCategory == device.mojom.XRReferenceSpaceCategory.UNBOUNDED
|| nativeOriginInformation.referenceSpaceCategory == device.mojom.XRReferenceSpaceCategory.BOUNDED_FLOOR) {
return Promise.resolve({
result : device.mojom.SubscribeToHitTestResult.FAILURE_GENERIC,
subscriptionId : 0
});
}
} else {
// Planes and anchors are not yet supported by the mock interface.
if (!this._nativeOriginKnown(nativeOriginInformation)) {
return Promise.resolve({
result : device.mojom.SubscribeToHitTestResult.FAILURE_GENERIC,
subscriptionId : 0
......@@ -716,6 +818,65 @@ class MockRuntime {
});
}
createAnchor(nativeOriginInformation, nativeOriginFromAnchor) {
return new Promise((resolve) => {
const mojoFromNativeOrigin = this._getMojoFromNativeOrigin(nativeOriginInformation);
if(mojoFromNativeOrigin == null) {
resolve({
result : device.mojom.CreateAnchorResult.FAILURE,
anchorId : 0
});
return;
}
const mojoFromAnchor = XRMathHelper.mul4x4(mojoFromNativeOrigin, nativeOriginFromAnchor);
const createAnchorEvent = new FakeXRAnchorCreationEvent("anchorcreate", {
requestedAnchorOrigin: mojoFromAnchor,
isAttachedToEntity: false,
});
this.dispatchEvent(createAnchorEvent);
if(createAnchorEvent.success) {
let anchor_controller = createAnchorEvent.anchorController;
const anchor_id = this.next_anchor_id_;
this.next_anchor_id_++;
// If the test allowed the anchor creation,
// store the anchor controller & return success.
this.anchor_controllers_.set(anchor_id, anchor_controller);
anchor_controller.device = this;
anchor_controller.id = anchor_id;
resolve({
result : device.mojom.CreateAnchorResult.SUCCESS,
anchorId : anchor_id
});
return;
}
resolve({
result : device.mojom.CreateAnchorResult.FAILURE,
anchorId : 0
});
});
}
createPlaneAnchor(planeFromAnchor, planeId) {
return new Promise((resolve) => {
// Not supported yet.
resolve({
result : device.mojom.CreateAnchorResult.FAILURE,
anchorId : 0
});
});
}
// Utility function
requestRuntimeSession(sessionOptions) {
return this.runtimeSupportsSession(sessionOptions).then((result) => {
......@@ -777,6 +938,61 @@ class MockRuntime {
});
}
// Private functions - utilities:
_nativeOriginKnown(nativeOriginInformation){
if (nativeOriginInformation.$tag == device.mojom.XRNativeOriginInformation.Tags.inputSourceId) {
if (!this.input_sources_.has(nativeOriginInformation.inputSourceId)) {
// Unknown input source.
return false;
}
return true;
} else if (nativeOriginInformation.$tag == device.mojom.XRNativeOriginInformation.Tags.referenceSpaceCategory) {
// Bounded_floor & unbounded ref spaces are not yet supported for AR:
if (nativeOriginInformation.referenceSpaceCategory == device.mojom.XRReferenceSpaceCategory.UNBOUNDED
|| nativeOriginInformation.referenceSpaceCategory == device.mojom.XRReferenceSpaceCategory.BOUNDED_FLOOR) {
return false;
}
return true;
} else {
// Planes and anchors are not yet supported by the mock interface.
return false;
}
}
// Private functions - anchors implementation:
// Modifies passed in frameData to add anchor information.
_calculateAnchorInformation(frameData) {
if (!this.supportedModes_.includes(device.mojom.XRSessionMode.kImmersiveAr)) {
return;
}
frameData.anchorsData = new device.mojom.XRAnchorsData();
frameData.anchorsData.allAnchorsIds = [];
frameData.anchorsData.updatedAnchorsData = [];
for(const [id, controller] of this.anchor_controllers_) {
frameData.anchorsData.allAnchorsIds.push(id);
// Send the entire anchor data over if there was a change since last GetFrameData().
if(controller.dirty) {
const anchorData = new device.mojom.XRAnchorData();
anchorData.id = id;
if(!controller.paused) {
anchorData.pose = XRMathHelper.decomposeRigidTransform(
controller.getAnchorOrigin());
}
controller.markProcessed();
frameData.anchorsData.updatedAnchorsData.push(anchorData);
}
}
}
// Private functions - hit test implementation:
// Modifies passed in frameData to add hit test results.
......
<!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],
};
const fakeDeviceInitParams = {
supportedModes: ["immersive-ar"],
views: VALID_VIEWS,
supportedFeatures: ALL_FEATURES,
viewerOrigin: VIEWER_ORIGIN_TRANSFORM,
};
// Creates a test method that leverages anchors API.
// |expectSucceeded| - true if the anchors creation request is expected to succeed, false otherwise
// |endSession| - true if the test case should call session.end() prior to creating an anchor
// |expectedError| - expected error name that should be returned in case expectSucceeded is false
const testFunctionGenerator = function(expectSucceeded, endSession, expectedError) {
const testFunction = function(session, fakeDeviceController, t) {
let debug = xr_debug.bind(this, 'testAnchorStates');
fakeDeviceController.addEventListener("anchorcreate", (anchorCreateEvent) => {
// All anchor creation requests that reach this stage should be marked as successful.
// If this test is expected to fail, the failure will happen earlier in the anchor
// creation process.
anchorCreateEvent.success = true;
});
let watcherDone = new Event("watcherdone");
let eventWatcher = new EventWatcher(t, session, ["watcherdone"]);
let eventPromise = eventWatcher.wait_for(["watcherdone"]);
session.requestReferenceSpace('local').then((localRefSpace) => {
const onFrame = function(time, frame) {
debug("rAF 1");
let setUpPromise = Promise.resolve();
if(endSession) {
debug("ending session");
setUpPromise = session.end();
}
setUpPromise.then(() => {
debug("creating anchor");
frame.createAnchor(new XRRigidTransform(), localRefSpace)
.then((anchor) => {
debug("anchor created");
t.step(() => {
assert_true(expectSucceeded,
"`createAnchor` succeeded when it was expected to fail");
});
session.dispatchEvent(watcherDone);
}).catch((error) => {
debug("anchor creation failed");
t.step(() => {
assert_false(expectSucceeded,
"`createAnchor` failed when it was expected to succeed, error: " + error);
assert_equals(error.name, expectedError,
"`createAnchor` failed with unexpected error name");
});
session.dispatchEvent(watcherDone);
});
// Anchor result will only take effect with frame data - schedule
// a frame after we requested anchor creation, otherwise the test will time out.
session.requestAnimationFrame(() => {
debug("rAF 2");
});
}); // setUpPromise.then(() => { ... })
}; // onFrame() { ... }
debug("requesting animation frame");
session.requestAnimationFrame(onFrame);
}); // session.requestReferenceSpace(...)
return eventPromise;
}; // testFunction
return testFunction;
};
xr_session_promise_test("Anchor creation succeeds if the feature was requested",
testFunctionGenerator(/*expectSucceeded=*/true, /*endSession=*/false),
fakeDeviceInitParams,
'immersive-ar', { 'requiredFeatures': ['anchors'] });
xr_session_promise_test("Anchor creation fails if the feature was not requested",
testFunctionGenerator(/*expectSucceeded=*/false, /*endSession=*/false, "NotSupportedError"),
fakeDeviceInitParams,
'immersive-ar', {});
xr_session_promise_test("Anchor creation fails if the feature was requested but the session already ended",
testFunctionGenerator(/*expectSucceeded=*/false, /*endSession=*/true, "InvalidStateError"),
fakeDeviceInitParams,
'immersive-ar', { 'requiredFeatures': ['anchors'] });
</script>
......@@ -124,6 +124,7 @@ const ALL_FEATURES = [
'hit-test',
'dom-overlay',
'light-estimation',
'anchors',
];
const TRACKED_IMMERSIVE_DEVICE = {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment