| /* |
| * Copyright (C) 2020 Igalia S.L. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "WebXRFrame.h" |
| |
| #if ENABLE(WEBXR) |
| |
| #include "WebXRBoundedReferenceSpace.h" |
| #include "WebXRJointPose.h" |
| #include "WebXRJointSpace.h" |
| #include "WebXRReferenceSpace.h" |
| #include "WebXRSession.h" |
| #include "WebXRViewerPose.h" |
| #include <wtf/IsoMallocInlines.h> |
| |
| namespace WebCore { |
| |
| WTF_MAKE_ISO_ALLOCATED_IMPL(WebXRFrame); |
| |
| Ref<WebXRFrame> WebXRFrame::create(WebXRSession& session, IsAnimationFrame isAnimationFrame) |
| { |
| return adoptRef(*new WebXRFrame(session, isAnimationFrame)); |
| } |
| |
| WebXRFrame::WebXRFrame(WebXRSession& session, IsAnimationFrame isAnimationFrame) |
| : m_isAnimationFrame(isAnimationFrame == IsAnimationFrame::Yes) |
| , m_session(session) |
| { |
| } |
| |
| WebXRFrame::~WebXRFrame() = default; |
| |
| bool WebXRFrame::isOutsideNativeBoundsOfBoundedReferenceSpace(const WebXRSpace& space, const WebXRSpace&) const |
| { |
| if (!is<WebXRBoundedReferenceSpace>(space)) |
| return false; |
| |
| // FIXME: return true whenever the distance from the bounded geometry of |
| // |space| to the native origin of |other| space is greater than 1m |
| // (suggested by specs). |
| |
| return false; |
| } |
| |
| bool WebXRFrame::isLocalReferenceSpace(const WebXRSpace& space) const |
| { |
| if (!is<WebXRReferenceSpace>(space)) |
| return false; |
| |
| auto type = downcast<WebXRReferenceSpace>(space).type(); |
| if (type == XRReferenceSpaceType::Local || type == XRReferenceSpaceType::LocalFloor) |
| return true; |
| |
| return false; |
| } |
| |
| // https://immersive-web.github.io/webxr/#poses-must-be-limited |
| bool WebXRFrame::mustPosesBeLimited(const WebXRSpace& space, const WebXRSpace& baseSpace) const |
| { |
| if (isOutsideNativeBoundsOfBoundedReferenceSpace(space, baseSpace) |
| || isOutsideNativeBoundsOfBoundedReferenceSpace(baseSpace, space)) |
| return true; |
| |
| if (isLocalReferenceSpace(space) || isLocalReferenceSpace(baseSpace)) { |
| // FIXME: If the distance between native origins of spaces is greater |
| // than 15m (suggested by specs) return true. |
| } |
| |
| return false; |
| } |
| |
| struct WebXRFrame::PopulatedPose { |
| TransformationMatrix transform; |
| bool emulatedPosition { false }; |
| }; |
| |
| // https://immersive-web.github.io/webxr/#populate-the-pose |
| ExceptionOr<std::optional<WebXRFrame::PopulatedPose>> WebXRFrame::populatePose(const Document& document, const WebXRSpace& space, const WebXRSpace& baseSpace) |
| { |
| // 1. If frame’s active boolean is false, throw an InvalidStateError and abort these steps. |
| if (!m_active) |
| return Exception { InvalidStateError }; |
| |
| // 2. Let session be frame’s session object. |
| // 3. If space’s session does not equal session, throw an InvalidStateError and abort these steps. |
| if (space.session() != m_session.ptr()) |
| return Exception { InvalidStateError }; |
| |
| // 4. If baseSpace’s session does not equal session, throw an InvalidStateError and abort these steps. |
| if (baseSpace.session() != m_session.ptr()) |
| return Exception { InvalidStateError }; |
| |
| // 5. Check if poses may be reported and, if not, throw a SecurityError and abort these steps. |
| if (!m_session->posesCanBeReported(document)) |
| return Exception { SecurityError }; |
| |
| // 6. Let limit be the result of whether poses must be limited between space and baseSpace. |
| // 7. Let transform be pose’s transform. |
| // 8. Query the XR device's tracking system for space’s pose relative to baseSpace at the frame’s time. |
| |
| if (m_isAnimationFrame && !m_session->frameData().isTrackingValid) { |
| // FIXME: check if space’s pose relative to baseSpace has been determined in the past. |
| // Anyway this emulation is usually provided by the system in the pose (e.g. OpenXR) |
| // so we shouldn't hit this path in most XRPlatform ports. |
| return { std::nullopt }; |
| } |
| |
| auto baseTransform = baseSpace.effectiveOrigin(); |
| if (!baseTransform) |
| return Exception { InvalidStateError }; |
| |
| if (!baseTransform.value().isInvertible()) |
| return { std::nullopt }; |
| |
| auto effectiveOrigin = space.effectiveOrigin(); |
| // A space's effectiveOrigin can be null, such as a joint pose from a hand that has |
| // other missing joint poses. |
| if (!effectiveOrigin) |
| return { std::nullopt }; |
| |
| auto transform = *baseTransform.value().inverse() * effectiveOrigin.value(); |
| |
| auto isPositionEmulated = space.isPositionEmulated(); |
| if (!isPositionEmulated) |
| return Exception { InvalidStateError }; |
| |
| auto baseSpaceIsPositionEmulated = baseSpace.isPositionEmulated(); |
| if (!baseSpaceIsPositionEmulated) |
| return Exception { InvalidStateError }; |
| |
| bool emulatedPosition = isPositionEmulated.value() || baseSpaceIsPositionEmulated.value(); |
| |
| bool limit = mustPosesBeLimited(space, baseSpace); |
| if (limit) { |
| // FIXME: apply pose limits logic |
| // https://immersive-web.github.io/webxr/#poses-must-be-limited |
| } |
| |
| return { PopulatedPose { transform, emulatedPosition } }; |
| } |
| |
| // https://immersive-web.github.io/webxr/#dom-xrframe-getviewerpose |
| ExceptionOr<RefPtr<WebXRViewerPose>> WebXRFrame::getViewerPose(const Document& document, const WebXRReferenceSpace& referenceSpace) |
| { |
| // 1. Let frame be this. |
| // 2. Let session be frame’s session object. |
| // 3. If frame’s animationFrame boolean is false, throw an InvalidStateError and abort these steps. |
| if (!m_isAnimationFrame) |
| return Exception { InvalidStateError }; |
| |
| // 4. Let pose be a new XRViewerPose object in the relevant realm of session. |
| // 5. Populate the pose of session’s viewer reference space in referenceSpace at the time represented by frame into pose. |
| auto populatePoseResult = populatePose(document, m_session->viewerReferenceSpace(), referenceSpace); |
| if (populatePoseResult.hasException()) |
| return populatePoseResult.releaseException(); |
| |
| // 6. If pose is null return null. |
| auto populateValue = populatePoseResult.releaseReturnValue(); |
| if (!populateValue) |
| return nullptr; |
| |
| RefPtr<WebXRViewerPose> pose = WebXRViewerPose::create(WebXRRigidTransform::create(populateValue->transform), populateValue->emulatedPosition); |
| |
| // 7. Let xrviews be an empty list. |
| Vector<Ref<WebXRView>> xrViews; |
| // 8. For each active view view in the list of views on session, perform the following steps: |
| const auto& frameData = m_session->frameData(); |
| for (auto& view : m_session->views()) { |
| auto index = xrViews.size(); |
| if (!view.active || frameData.views.size() <= index) |
| continue; |
| |
| // 8.1 Let xrview be a new XRView object in the relevant realm of session. |
| // 8.2 Initialize xrview’s underlying view to view. |
| // 8.3 Initialize xrview’s eye to view’s eye. |
| // 8.4 Initialize xrview’s frame time to frame’s time. |
| // 8.5 Initialize xrview’s session to session. |
| // 8.6. Let offset be an new XRRigidTransform object equal to the view offset of view in the relevant realm of session. |
| // 8.7. Set xrview’s transform property to the result of multiplying the XRViewerPose's transform by the offset transform in the relevant realm of session |
| auto offset = matrixFromPose(frameData.views[index].offset); |
| auto transform = WebXRRigidTransform::create(pose->transform().rawTransform() * offset); |
| |
| // Set projection matrix for each view |
| std::array<float, 16> projection = WTF::switchOn(frameData.views[index].projection, [&](const PlatformXR::Device::FrameData::Fov& fov) { |
| double near = m_session->renderState().depthNear(); |
| double far = m_session->renderState().depthFar(); |
| return TransformationMatrix::fromProjection(fov.up, fov.down, fov.left, fov.right, near, far).toColumnMajorFloatArray(); |
| }, [&](const std::array<float, 16>& matrix) { |
| return matrix; |
| }, [&](const std::nullptr_t&) { |
| // Use aspect projection for inline sessions |
| double fov = m_session->renderState().inlineVerticalFieldOfView().value_or(piOverTwoDouble); |
| float aspect = 1; |
| auto layer = m_session->renderState().baseLayer(); |
| if (layer) |
| aspect = static_cast<double>(layer->framebufferWidth()) / static_cast<double>(layer->framebufferHeight()); |
| double near = m_session->renderState().depthNear(); |
| double far = m_session->renderState().depthFar(); |
| return TransformationMatrix::fromProjection(fov, aspect, near, far).toColumnMajorFloatArray(); |
| }); |
| |
| auto xrView = WebXRView::create(Ref { *this }, view.eye, WTFMove(transform), Float32Array::create(projection.data(), projection.size())); |
| xrView->setViewportModifiable(m_session->supportsViewportScaling()); |
| |
| // 8.8. Append xrview to xrviews |
| xrViews.append(WTFMove(xrView)); |
| } |
| |
| // 9. Set pose’s views to xrviews |
| pose->setViews(WTFMove(xrViews)); |
| |
| // 10. Return pose. |
| return pose; |
| } |
| |
| ExceptionOr<RefPtr<WebXRPose>> WebXRFrame::getPose(const Document& document, const WebXRSpace& space, const WebXRSpace& baseSpace) |
| { |
| // 1. Let frame be this. |
| // 2. Let pose be a new XRPose object in the relevant realm of frame. |
| // 3. Populate the pose of space in baseSpace at the time represented by frame into pose. |
| |
| auto populatePoseResult = populatePose(document, space, baseSpace); |
| if (populatePoseResult.hasException()) |
| return populatePoseResult.releaseException(); |
| |
| auto populateValue = populatePoseResult.releaseReturnValue(); |
| if (!populateValue) |
| return nullptr; |
| |
| // 4. Return pose. |
| return RefPtr<WebXRPose>(WebXRPose::create(WebXRRigidTransform::create(populateValue->transform), populateValue->emulatedPosition)); |
| } |
| |
| TransformationMatrix WebXRFrame::matrixFromPose(const PlatformXR::Device::FrameData::Pose& pose) |
| { |
| TransformationMatrix matrix; |
| matrix.translate3d(pose.position.x(), pose.position.y(), pose.position.z()); |
| matrix.multiply(TransformationMatrix::fromQuaternion(pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w)); |
| return matrix; |
| } |
| |
| #if ENABLE(WEBXR_HANDS) |
| |
| // https://immersive-web.github.io/webxr-hand-input/#dom-xrframe-getjointpose |
| ExceptionOr<RefPtr<WebXRJointPose>> WebXRFrame::getJointPose(const Document& document, const WebXRJointSpace& jointSpace, const WebXRSpace& baseSpace) |
| { |
| auto populatePoseResult = populatePose(document, jointSpace, baseSpace); |
| if (populatePoseResult.hasException()) |
| return populatePoseResult.releaseException(); |
| |
| auto populateValue = populatePoseResult.releaseReturnValue(); |
| if (!populateValue) |
| return nullptr; |
| |
| return RefPtr<WebXRJointPose>(WebXRJointPose::create(WebXRRigidTransform::create(populateValue->transform), populateValue->emulatedPosition, jointSpace.radius())); |
| } |
| |
| // https://immersive-web.github.io/webxr-hand-input/#dom-xrframe-filljointradii |
| ExceptionOr<bool> WebXRFrame::fillJointRadii(const Vector<RefPtr<WebXRJointSpace>>& jointSpaces, Float32Array& radii) |
| { |
| // If frame’s active boolean is false, throw an InvalidStateError and abort these steps. |
| if (!m_active) |
| return Exception { InvalidStateError, "Frame is not active"_s }; |
| |
| // For each joint in the jointSpaces: |
| // If joint’s session is different from session, throw an InvalidStateError and abort these steps. |
| for (const auto& jointSpace : jointSpaces) { |
| if (!jointSpace || jointSpace->session() != m_session.ptr()) |
| return Exception { InvalidStateError, "Joint space's session does not match frame's session"_s }; |
| } |
| |
| // If the length of jointSpaces is larger than the number of elements in radii, throw a TypeError and abort these steps. |
| if (jointSpaces.size() > radii.length()) |
| return Exception { TypeError, "Unexpected length of radii array"_s }; |
| |
| // Let allValid be true. |
| bool allValid = true; |
| |
| // For each joint in the jointSpaces: |
| // 1. Set the float value of radii at offset as follows: |
| // If the user agent can determine the poses of all the joints belonging to the joint’s hand: |
| // Set the float value of radii at offset to that radius. |
| // Otherwise |
| // Set the float value of radii at offset to NaN. |
| // Set allValid to false. |
| // 2. Increase offset by 1. |
| for (size_t i = 0; i < jointSpaces.size(); ++i) { |
| if (jointSpaces[i]->handHasMissingPoses()) { |
| radii.set(i, std::numeric_limits<float>::quiet_NaN()); |
| allValid = false; |
| } else |
| radii.set(i, jointSpaces[i]->radius()); |
| } |
| |
| return allValid; |
| } |
| |
| // https://immersive-web.github.io/webxr-hand-input/#dom-xrframe-fillposes |
| ExceptionOr<bool> WebXRFrame::fillPoses(const Document& document, const Vector<RefPtr<WebXRSpace>>& spaces, const WebXRSpace& baseSpace, Float32Array& transforms) |
| { |
| // If frame’s active boolean is false, throw an InvalidStateError and abort these steps. |
| if (!m_active) |
| return Exception { InvalidStateError, "Frame is not active"_s }; |
| |
| // For each space in the spaces sequence: |
| // If space’s session is different from session, throw an InvalidStateError and abort these steps. |
| for (const auto& space : spaces) { |
| if (!space || space->session() != m_session.ptr()) |
| return Exception { InvalidStateError, "Space's session does not match frame's session"_s }; |
| } |
| |
| // If baseSpace’s session is different from session, throw an InvalidStateError and abort these steps. |
| if (baseSpace.session() != m_session.ptr()) |
| return Exception { InvalidStateError, "Base space's session does not match frame's session"_s }; |
| |
| // If the length of spaces multiplied by 16 is larger than the number of elements in transforms, |
| // throw a TypeError and abort these steps. |
| const size_t numberOfFloatsPerTransform = 16; |
| if (spaces.size() * numberOfFloatsPerTransform > transforms.length()) |
| return Exception { TypeError, "Unexpected length of transforms array"_s }; |
| |
| // Check if poses may be reported and, if not, throw a SecurityError and abort these steps. |
| if (!m_session->posesCanBeReported(document)) |
| return Exception { SecurityError, "Poses cannot be reported"_s }; |
| |
| // Let allValid be true. |
| bool allValid = true; |
| |
| // For each space in the spaces sequence: |
| for (size_t spaceIndex = 0; spaceIndex < spaces.size(); ++spaceIndex) { |
| // 1. Populate the pose of space in baseSpace at the time represented by frame into pose. |
| auto populatePoseResult = populatePose(document, *(spaces[spaceIndex]), baseSpace); |
| if (populatePoseResult.hasException()) |
| return populatePoseResult.releaseException(); |
| |
| // 2. If pose is null, perform the following steps: |
| // 3. Set 16 consecutive elements of the transforms array starting at offset to NaN. |
| // 4. Set allValid to false. |
| auto populateValue = populatePoseResult.releaseReturnValue(); |
| if (!populateValue) { |
| for (size_t transformIndex = 0; transformIndex < numberOfFloatsPerTransform; ++transformIndex) |
| transforms.set(spaceIndex * numberOfFloatsPerTransform + transformIndex, std::numeric_limits<float>::quiet_NaN()); |
| allValid = false; |
| } else { |
| // 5. If pose is not null, copy all elements from pose’s matrix member to the transforms array starting at offset. |
| // 6. Increase offset by 16. |
| auto matrix = populateValue->transform.toColumnMajorFloatArray(); |
| for (size_t transformIndex = 0; transformIndex < numberOfFloatsPerTransform; ++transformIndex) |
| transforms.set(spaceIndex * numberOfFloatsPerTransform + transformIndex, matrix[transformIndex]); |
| } |
| } |
| |
| return allValid; |
| } |
| |
| #endif |
| |
| } // namespace WebCore |
| |
| #endif // ENABLE(WEBXR) |