| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "device/vr/android/arcore/arcore_impl.h" |
| |
| #include "base/android/jni_android.h" |
| #include "base/bind.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/span.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/numerics/checked_math.h" |
| #include "base/numerics/math_constants.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/trace_event/trace_event.h" |
| #include "base/types/pass_key.h" |
| #include "device/vr/android/arcore/arcore_math_utils.h" |
| #include "device/vr/android/arcore/arcore_plane_manager.h" |
| #include "device/vr/android/arcore/type_converters.h" |
| #include "device/vr/public/mojom/pose.h" |
| #include "device/vr/public/mojom/vr_service.mojom.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "third_party/skia/include/core/SkCanvas.h" |
| #include "third_party/skia/include/core/SkImage.h" |
| #include "third_party/skia/include/core/SkPaint.h" |
| #include "third_party/skia/include/core/SkPixmap.h" |
| #include "ui/display/display.h" |
| #include "ui/gfx/geometry/point3_f.h" |
| #include "ui/gfx/geometry/point_f.h" |
| #include "ui/gfx/geometry/transform.h" |
| #include "ui/gfx/geometry/transform_util.h" |
| |
| using base::android::JavaRef; |
| |
| namespace { |
| |
| // Anchor creation requests that are older than 3 seconds are considered |
| // outdated and should be failed. |
| constexpr base::TimeDelta kOutdatedAnchorCreationRequestThreshold = |
| base::Seconds(3); |
| |
| // Helper, returns new VRPosePtr with position and orientation set to match the |
| // position and orientation of passed in |pose|. |
| device::mojom::VRPosePtr GetMojomVRPoseFromArPose(const ArSession* session, |
| const ArPose* pose) { |
| device::mojom::VRPosePtr result = device::mojom::VRPose::New(); |
| std::tie(result->orientation, result->position) = |
| device::GetPositionAndOrientationFromArPose(session, pose); |
| |
| return result; |
| } |
| |
| // Helper, creates new ArPose* with position and orientation set to match the |
| // position and orientation of passed in |pose|. |
| |
| ArTrackableType GetArCoreEntityType( |
| device::mojom::EntityTypeForHitTest entity_type) { |
| switch (entity_type) { |
| case device::mojom::EntityTypeForHitTest::PLANE: |
| return AR_TRACKABLE_PLANE; |
| case device::mojom::EntityTypeForHitTest::POINT: |
| return AR_TRACKABLE_POINT; |
| } |
| } |
| |
| std::set<ArTrackableType> GetArCoreEntityTypes( |
| const std::vector<device::mojom::EntityTypeForHitTest>& entity_types) { |
| std::set<ArTrackableType> result; |
| |
| std::transform(entity_types.begin(), entity_types.end(), |
| std::inserter(result, result.end()), GetArCoreEntityType); |
| |
| return result; |
| } |
| |
| // Helper, computes mojo_from_input_source transform based on mojo_from_viever |
| // pose and input source state (containing input_from_pointer transform, which |
| // in case of input sources is equivalent to viewer_from_pointer). |
| // TODO(https://crbug.com/1043389): this currently assumes that the input source |
| // ray mode is "tapping", which is OK for input sources available for AR on |
| // Android, but is not true in the general case. This method should duplicate |
| // the logic found in XRTargetRaySpace::MojoFromNative(). |
| absl::optional<gfx::Transform> GetMojoFromInputSource( |
| const device::mojom::XRInputSourceStatePtr& input_source_state, |
| const gfx::Transform& mojo_from_viewer) { |
| if (!input_source_state->description || |
| !input_source_state->description->input_from_pointer) { |
| return absl::nullopt; |
| } |
| |
| gfx::Transform viewer_from_pointer = |
| *input_source_state->description->input_from_pointer; |
| |
| return mojo_from_viewer * viewer_from_pointer; |
| } |
| |
| void ReleaseArCoreCubemap(ArImageCubemap* cube_map) { |
| for (auto* image : *cube_map) { |
| ArImage_release(image); |
| } |
| |
| memset(cube_map, 0, sizeof(*cube_map)); |
| } |
| |
| // Helper, copies ARCore image to the passed in buffer, assuming that the caller |
| // allocated the buffer to fit all the data. |
| template <typename T> |
| void CopyArCoreImage(const ArSession* session, |
| const ArImage* image, |
| int32_t plane_index, |
| base::span<T> out_pixels, |
| uint32_t width, |
| uint32_t height) { |
| DVLOG(3) << __func__ << ": width=" << width << ", height=" << height |
| << ", out_pixels.size()=" << out_pixels.size(); |
| |
| DCHECK_GE(out_pixels.size(), width * height); |
| |
| int32_t src_row_stride = 0, src_pixel_stride = 0; |
| ArImage_getPlaneRowStride(session, image, plane_index, &src_row_stride); |
| ArImage_getPlanePixelStride(session, image, plane_index, &src_pixel_stride); |
| |
| // Naked pointer since ArImage_getPlaneData does not transfer ownership to us. |
| uint8_t const* src_buffer = nullptr; |
| int32_t src_buffer_length = 0; |
| ArImage_getPlaneData(session, image, plane_index, &src_buffer, |
| &src_buffer_length); |
| |
| // Fast path: Source and destination have the same layout |
| bool const fast_path = |
| static_cast<size_t>(src_row_stride) == width * sizeof(T); |
| TRACE_EVENT1("xr", "CopyArCoreImage: memcpy", "fastPath", fast_path); |
| |
| DVLOG(3) << __func__ << ": plane_index=" << plane_index |
| << ", src_buffer_length=" << src_buffer_length |
| << ", src_row_stride=" << src_row_stride |
| << ", src_pixel_stride=" << src_pixel_stride |
| << ", fast_path=" << fast_path << ", sizeof(T)=" << sizeof(T); |
| |
| // If they have the same layout, we can copy the entire buffer at once |
| if (fast_path) { |
| CHECK_EQ(out_pixels.size() * sizeof(T), |
| static_cast<size_t>(src_buffer_length)); |
| memcpy(out_pixels.data(), src_buffer, src_buffer_length); |
| return; |
| } |
| |
| CHECK_EQ(sizeof(T), static_cast<size_t>(src_pixel_stride)); |
| |
| // Slow path: copy pixel by pixel, row by row |
| for (uint32_t row = 0; row < height; ++row) { |
| auto* src = src_buffer + src_row_stride * row; |
| auto* dest = out_pixels.data() + width * row; |
| |
| // For each pixel |
| for (uint32_t x = 0; x < width; ++x) { |
| memcpy(dest, src, sizeof(T)); |
| |
| src += src_pixel_stride; |
| dest += 1; |
| } |
| } |
| } |
| |
| // Helper, copies ARCore image to the passed in vector, discovering the buffer |
| // size and resizing the vector first. |
| template <typename T> |
| void CopyArCoreImage(const ArSession* session, |
| const ArImage* image, |
| int32_t plane_index, |
| std::vector<T>* out_pixels, |
| uint32_t* out_width, |
| uint32_t* out_height) { |
| // Get source image information |
| int32_t width = 0, height = 0; |
| ArImage_getWidth(session, image, &width); |
| ArImage_getHeight(session, image, &height); |
| |
| *out_width = width; |
| *out_height = height; |
| |
| // Allocate memory for the output. |
| out_pixels->resize(width * height); |
| |
| CopyArCoreImage(session, image, plane_index, base::span<T>(*out_pixels), |
| width, height); |
| } |
| |
| device::mojom::XRLightProbePtr GetLightProbe( |
| ArSession* arcore_session, |
| ArLightEstimate* arcore_light_estimate) { |
| // ArCore hands out 9 sets of RGB spherical harmonics coefficients |
| // https://developers.google.com/ar/reference/c/group/light#arlightestimate_getenvironmentalhdrambientsphericalharmonics |
| constexpr size_t kNumShCoefficients = 9; |
| |
| auto light_probe = device::mojom::XRLightProbe::New(); |
| |
| light_probe->spherical_harmonics = device::mojom::XRSphericalHarmonics::New(); |
| light_probe->spherical_harmonics->coefficients = |
| std::vector<device::RgbTupleF32>(kNumShCoefficients, |
| device::RgbTupleF32{}); |
| |
| ArLightEstimate_getEnvironmentalHdrAmbientSphericalHarmonics( |
| arcore_session, arcore_light_estimate, |
| light_probe->spherical_harmonics->coefficients.data()->components); |
| |
| float main_light_direction[3] = {0}; |
| ArLightEstimate_getEnvironmentalHdrMainLightDirection( |
| arcore_session, arcore_light_estimate, main_light_direction); |
| light_probe->main_light_direction.set_x(main_light_direction[0]); |
| light_probe->main_light_direction.set_y(main_light_direction[1]); |
| light_probe->main_light_direction.set_z(main_light_direction[2]); |
| |
| ArLightEstimate_getEnvironmentalHdrMainLightIntensity( |
| arcore_session, arcore_light_estimate, |
| light_probe->main_light_intensity.components); |
| |
| return light_probe; |
| } |
| |
| device::mojom::XRReflectionProbePtr GetReflectionProbe( |
| ArSession* arcore_session, |
| ArLightEstimate* arcore_light_estimate) { |
| ArImageCubemap arcore_cube_map = {nullptr}; |
| ArLightEstimate_acquireEnvironmentalHdrCubemap( |
| arcore_session, arcore_light_estimate, arcore_cube_map); |
| |
| auto cube_map = device::mojom::XRCubeMap::New(); |
| std::vector<device::RgbaTupleF16>* const cube_map_faces[] = { |
| &cube_map->positive_x, &cube_map->negative_x, &cube_map->positive_y, |
| &cube_map->negative_y, &cube_map->positive_z, &cube_map->negative_z}; |
| |
| static_assert( |
| std::size(cube_map_faces) == std::size(arcore_cube_map), |
| "`ArImageCubemap` and `device::mojom::XRCubeMap` are expected to " |
| "have the same number of faces (6)."); |
| |
| static_assert(device::mojom::XRCubeMap::kNumComponentsPerPixel == 4, |
| "`device::mojom::XRCubeMap::kNumComponentsPerPixel` is " |
| "expected to be 4 (RGBA)`, as that's the format ArCore uses."); |
| |
| for (size_t i = 0; i < std::size(arcore_cube_map); ++i) { |
| auto* arcore_cube_map_face = arcore_cube_map[i]; |
| if (!arcore_cube_map_face) { |
| DVLOG(1) << "`ArLightEstimate_acquireEnvironmentalHdrCubemap` failed to " |
| "return all faces"; |
| ReleaseArCoreCubemap(&arcore_cube_map); |
| return nullptr; |
| } |
| |
| auto* cube_map_face = cube_map_faces[i]; |
| |
| // Make sure we only have a single image plane |
| int32_t num_planes = 0; |
| ArImage_getNumberOfPlanes(arcore_session, arcore_cube_map_face, |
| &num_planes); |
| if (num_planes != 1) { |
| DVLOG(1) << "ArCore cube map face " << i |
| << " does not have exactly 1 plane."; |
| ReleaseArCoreCubemap(&arcore_cube_map); |
| return nullptr; |
| } |
| |
| // Make sure the format for the image is in RGBA16F |
| ArImageFormat format = AR_IMAGE_FORMAT_INVALID; |
| ArImage_getFormat(arcore_session, arcore_cube_map_face, &format); |
| if (format != AR_IMAGE_FORMAT_RGBA_FP16) { |
| DVLOG(1) << "ArCore cube map face " << i |
| << " not in expected image format."; |
| ReleaseArCoreCubemap(&arcore_cube_map); |
| return nullptr; |
| } |
| |
| // Copy the cubemap |
| uint32_t face_width = 0, face_height = 0; |
| CopyArCoreImage(arcore_session, arcore_cube_map_face, 0, cube_map_face, |
| &face_width, &face_height); |
| |
| // Make sure the cube map is square |
| if (face_width != face_height) { |
| DVLOG(1) << "ArCore cube map contains non-square image."; |
| ReleaseArCoreCubemap(&arcore_cube_map); |
| return nullptr; |
| } |
| |
| // Make sure all faces have the same dimensions |
| if (i == 0) { |
| cube_map->width_and_height = face_width; |
| } else if (face_width != cube_map->width_and_height || |
| face_height != cube_map->width_and_height) { |
| DVLOG(1) << "ArCore cube map faces not all of the same dimensions."; |
| ReleaseArCoreCubemap(&arcore_cube_map); |
| return nullptr; |
| } |
| } |
| |
| ReleaseArCoreCubemap(&arcore_cube_map); |
| |
| auto reflection_probe = device::mojom::XRReflectionProbe::New(); |
| reflection_probe->cube_map = std::move(cube_map); |
| return reflection_probe; |
| } |
| |
| constexpr float kDefaultFloorHeightEstimation = 1.2; |
| |
| } // namespace |
| |
| namespace device { |
| |
| ArCoreImpl::ArCoreImpl() |
| : gl_thread_task_runner_(base::ThreadTaskRunnerHandle::Get()) {} |
| |
| ArCoreImpl::~ArCoreImpl() { |
| for (auto& create_anchor : create_anchor_requests_) { |
| create_anchor.TakeCallback().Run(mojom::CreateAnchorResult::FAILURE, 0); |
| } |
| |
| for (auto& create_anchor : create_plane_attached_anchor_requests_) { |
| create_anchor.TakeCallback().Run(mojom::CreateAnchorResult::FAILURE, 0); |
| } |
| } |
| |
| absl::optional<ArCore::InitializeResult> ArCoreImpl::Initialize( |
| base::android::ScopedJavaLocalRef<jobject> context, |
| const std::unordered_set<device::mojom::XRSessionFeature>& |
| required_features, |
| const std::unordered_set<device::mojom::XRSessionFeature>& |
| optional_features, |
| const std::vector<device::mojom::XRTrackedImagePtr>& tracked_images, |
| absl::optional<ArCore::DepthSensingConfiguration> depth_sensing_config) { |
| DCHECK(IsOnGlThread()); |
| DCHECK(!arcore_session_.is_valid()); |
| |
| // TODO(https://crbug.com/837944): Notify error earlier if this will fail. |
| |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| if (!env) { |
| DLOG(ERROR) << "Unable to get JNIEnv for ArCore"; |
| return absl::nullopt; |
| } |
| |
| // Use a local scoped ArSession for the next steps, we want the |
| // arcore_session_ member to remain null until we complete successful |
| // initialization. |
| internal::ScopedArCoreObject<ArSession*> session; |
| |
| ArStatus status = ArSession_create( |
| env, context.obj(), |
| internal::ScopedArCoreObject<ArSession*>::Receiver(session).get()); |
| if (status != AR_SUCCESS) { |
| DLOG(ERROR) << "ArSession_create failed: " << status; |
| return absl::nullopt; |
| } |
| |
| // Set incognito mode for ARCore session - this is done unconditionally as we |
| // always want to limit the amount of logging done by ARCore. |
| ArSession_enableIncognitoMode_private(session.get()); |
| DVLOG(1) << __func__ << ": ARCore incognito mode enabled"; |
| |
| absl::optional<std::unordered_set<device::mojom::XRSessionFeature>> |
| maybe_enabled_features = |
| ConfigureFeatures(session.get(), required_features, optional_features, |
| tracked_images, depth_sensing_config); |
| |
| if (!maybe_enabled_features) { |
| DLOG(ERROR) << "Failed to configure session features"; |
| return absl::nullopt; |
| } |
| |
| if (!ConfigureCamera(session.get())) { |
| DLOG(ERROR) << "Failed to configure session camera"; |
| return absl::nullopt; |
| } |
| |
| internal::ScopedArCoreObject<ArFrame*> frame; |
| ArFrame_create(session.get(), |
| internal::ScopedArCoreObject<ArFrame*>::Receiver(frame).get()); |
| if (!frame.is_valid()) { |
| DLOG(ERROR) << "ArFrame_create failed"; |
| return absl::nullopt; |
| } |
| |
| if (base::Contains(*maybe_enabled_features, |
| device::mojom::XRSessionFeature::LIGHT_ESTIMATION)) { |
| internal::ScopedArCoreObject<ArLightEstimate*> light_estimate; |
| ArLightEstimate_create( |
| session.get(), |
| internal::ScopedArCoreObject<ArLightEstimate*>::Receiver(light_estimate) |
| .get()); |
| if (!light_estimate.is_valid()) { |
| DVLOG(1) << "ArLightEstimate_create failed"; |
| return absl::nullopt; |
| } |
| arcore_light_estimate_ = std::move(light_estimate); |
| } |
| |
| // Success, we now have a valid session and a valid frame. |
| arcore_frame_ = std::move(frame); |
| arcore_session_ = std::move(session); |
| |
| if (base::Contains(*maybe_enabled_features, |
| device::mojom::XRSessionFeature::ANCHORS)) { |
| anchor_manager_ = std::make_unique<ArCoreAnchorManager>( |
| base::PassKey<ArCoreImpl>(), arcore_session_.get()); |
| } |
| if (base::Contains(*maybe_enabled_features, |
| device::mojom::XRSessionFeature::PLANE_DETECTION)) { |
| plane_manager_ = std::make_unique<ArCorePlaneManager>( |
| base::PassKey<ArCoreImpl>(), arcore_session_.get()); |
| } |
| |
| return ArCore::InitializeResult(*maybe_enabled_features, |
| depth_configuration_); |
| } |
| |
| absl::optional<std::unordered_set<device::mojom::XRSessionFeature>> |
| ArCoreImpl::ConfigureFeatures( |
| ArSession* ar_session, |
| const std::unordered_set<device::mojom::XRSessionFeature>& |
| required_features, |
| const std::unordered_set<device::mojom::XRSessionFeature>& |
| optional_features, |
| const std::vector<device::mojom::XRTrackedImagePtr>& tracked_images, |
| const absl::optional<ArCore::DepthSensingConfiguration>& |
| depth_sensing_config) { |
| // Let's assume we will be able to configure a session with all features - |
| // this will be adjusted if it turns out we can only create a session w/o some |
| // optional features. Currently, only depth sensing is not supported across |
| // all the ARCore-capable devices. |
| std::unordered_set<device::mojom::XRSessionFeature> enabled_features; |
| enabled_features.insert(required_features.begin(), required_features.end()); |
| enabled_features.insert(optional_features.begin(), optional_features.end()); |
| |
| internal::ScopedArCoreObject<ArConfig*> arcore_config; |
| ArConfig_create( |
| ar_session, |
| internal::ScopedArCoreObject<ArConfig*>::Receiver(arcore_config).get()); |
| if (!arcore_config.is_valid()) { |
| DLOG(ERROR) << __func__ << ": ArConfig_create failed"; |
| return absl::nullopt; |
| } |
| |
| const bool light_estimation_requested = |
| base::Contains(required_features, |
| device::mojom::XRSessionFeature::LIGHT_ESTIMATION) || |
| base::Contains(optional_features, |
| device::mojom::XRSessionFeature::LIGHT_ESTIMATION); |
| |
| if (light_estimation_requested) { |
| // Enable lighting estimation with spherical harmonics |
| ArConfig_setLightEstimationMode(ar_session, arcore_config.get(), |
| AR_LIGHT_ESTIMATION_MODE_ENVIRONMENTAL_HDR); |
| } |
| |
| const bool image_tracking_requested = |
| base::Contains(required_features, |
| device::mojom::XRSessionFeature::IMAGE_TRACKING) || |
| base::Contains(optional_features, |
| device::mojom::XRSessionFeature::IMAGE_TRACKING); |
| |
| if (image_tracking_requested) { |
| internal::ScopedArCoreObject<ArAugmentedImageDatabase*> image_db; |
| ArAugmentedImageDatabase_create( |
| ar_session, |
| internal::ScopedArCoreObject<ArAugmentedImageDatabase*>::Receiver( |
| image_db) |
| .get()); |
| if (!image_db.is_valid()) { |
| DLOG(ERROR) << "ArAugmentedImageDatabase creation failed"; |
| return absl::nullopt; |
| } |
| |
| // Populate the image tracking database and set up data structures, |
| // this doesn't modify the ArConfig or session yet. |
| BuildImageDatabase(ar_session, image_db.get(), tracked_images); |
| |
| if (!tracked_image_arcore_id_to_index_.empty()) { |
| // Image tracking with a non-empty image DB adds a few frames of |
| // synchronization delay internally in ARCore, has a high CPU cost, and |
| // reconfigures its graphics pipeline. Only activate it if we got images. |
| // (Apparently an empty image db is equivalent, but that seems fragile.) |
| ArConfig_setAugmentedImageDatabase(ar_session, arcore_config.get(), |
| image_db.get()); |
| // Switch to autofocus mode when tracking images. The default fixed focus |
| // mode has trouble tracking close images since they end up blurry. |
| ArConfig_setFocusMode(ar_session, arcore_config.get(), |
| AR_FOCUS_MODE_AUTO); |
| } |
| } |
| |
| const bool depth_api_optional = |
| base::Contains(optional_features, device::mojom::XRSessionFeature::DEPTH); |
| const bool depth_api_required = |
| base::Contains(required_features, device::mojom::XRSessionFeature::DEPTH); |
| const bool depth_api_requested = depth_api_required || depth_api_optional; |
| |
| const bool depth_api_configuration_successful = |
| depth_api_requested && ConfigureDepthSensing(depth_sensing_config); |
| |
| if (depth_api_configuration_successful) { |
| // Don't try to set the depth mode if we know we won't be able to support |
| // the desired usage and data format. |
| ArConfig_setDepthMode(ar_session, arcore_config.get(), |
| AR_DEPTH_MODE_AUTOMATIC); |
| } |
| |
| ArStatus status = ArSession_configure(ar_session, arcore_config.get()); |
| if (status != AR_SUCCESS && depth_api_requested && |
| depth_api_configuration_successful && !depth_api_required) { |
| // Configuring an ARCore session failed for some reason, and we know depth |
| // API was requested but is not required to be enabled. |
| // Depth API may not be available on some ARCore-capable devices - since it |
| // was requested optionally, let's try to request the session w/o it. |
| // Currently, Depth API is the only feature that is not supported across the |
| // board, so we speculatively assume that it is the reason why the session |
| // creation failed. |
| |
| DLOG(WARNING) << __func__ |
| << ": Depth API was optionally requested and the session " |
| "creation failed, re-trying with depth API disabled"; |
| |
| enabled_features.erase(device::mojom::XRSessionFeature::DEPTH); |
| |
| ArConfig_setDepthMode(ar_session, arcore_config.get(), |
| AR_DEPTH_MODE_DISABLED); |
| |
| status = ArSession_configure(ar_session, arcore_config.get()); |
| } |
| |
| if (status != AR_SUCCESS) { |
| DLOG(ERROR) << __func__ << ": ArSession_configure failed: " << status; |
| return absl::nullopt; |
| } |
| |
| return enabled_features; |
| } |
| |
| bool ArCoreImpl::ConfigureDepthSensing( |
| const absl::optional<ArCore::DepthSensingConfiguration>& |
| depth_sensing_config) { |
| if (!depth_sensing_config) { |
| return false; |
| } |
| |
| if (!base::Contains(depth_sensing_config->depth_usage_preference, |
| device::mojom::XRDepthUsage::kCPUOptimized)) { |
| return false; |
| } |
| |
| if (!base::Contains(depth_sensing_config->depth_data_format_preference, |
| device::mojom::XRDepthDataFormat::kLuminanceAlpha)) { |
| return false; |
| } |
| |
| depth_configuration_ = device::mojom::XRDepthConfig( |
| device::mojom::XRDepthUsage::kCPUOptimized, |
| device::mojom::XRDepthDataFormat::kLuminanceAlpha); |
| |
| return true; |
| } |
| |
| bool ArCoreImpl::ConfigureCamera(ArSession* ar_session) { |
| internal::ScopedArCoreObject<ArCameraConfigFilter*> camera_config_filter; |
| ArCameraConfigFilter_create( |
| ar_session, internal::ScopedArCoreObject<ArCameraConfigFilter*>::Receiver( |
| camera_config_filter) |
| .get()); |
| if (!camera_config_filter.is_valid()) { |
| DLOG(ERROR) << "ArCameraConfigFilter_create failed"; |
| return false; |
| } |
| |
| // We only want to work at 30fps for now. |
| ArCameraConfigFilter_setTargetFps(ar_session, camera_config_filter.get(), |
| AR_CAMERA_CONFIG_TARGET_FPS_30); |
| // We do not care if depth sensor is available or not for now. |
| // The default depth sensor usage of the newly created filter is not |
| // documented, so let's set the filter explicitly to accept both cameras with |
| // and without depth sensors. |
| ArCameraConfigFilter_setDepthSensorUsage( |
| ar_session, camera_config_filter.get(), |
| AR_CAMERA_CONFIG_DEPTH_SENSOR_USAGE_REQUIRE_AND_USE | |
| AR_CAMERA_CONFIG_DEPTH_SENSOR_USAGE_DO_NOT_USE); |
| |
| internal::ScopedArCoreObject<ArCameraConfigList*> camera_config_list; |
| ArCameraConfigList_create( |
| ar_session, internal::ScopedArCoreObject<ArCameraConfigList*>::Receiver( |
| camera_config_list) |
| .get()); |
| |
| if (!camera_config_list.is_valid()) { |
| DLOG(ERROR) << "ArCameraConfigList_create failed"; |
| return false; |
| } |
| |
| ArSession_getSupportedCameraConfigsWithFilter( |
| ar_session, camera_config_filter.get(), camera_config_list.get()); |
| if (!camera_config_list.is_valid()) { |
| DLOG(ERROR) << "ArSession_getSupportedCameraConfigsWithFilter failed"; |
| return false; |
| } |
| |
| int32_t available_configs_count; |
| ArCameraConfigList_getSize(ar_session, camera_config_list.get(), |
| &available_configs_count); |
| |
| DVLOG(2) << __func__ << ": ARCore reported " << available_configs_count |
| << " available configurations"; |
| |
| std::vector<internal::ScopedArCoreObject<ArCameraConfig*>> available_configs; |
| available_configs.reserve(available_configs_count); |
| for (int32_t i = 0; i < available_configs_count; ++i) { |
| internal::ScopedArCoreObject<ArCameraConfig*> camera_config; |
| ArCameraConfig_create( |
| ar_session, |
| internal::ScopedArCoreObject<ArCameraConfig*>::Receiver(camera_config) |
| .get()); |
| |
| if (!camera_config.is_valid()) { |
| DVLOG(1) << __func__ |
| << ": ArCameraConfig_create failed for camera config at index " |
| << i; |
| continue; |
| } |
| |
| ArCameraConfigList_getItem(ar_session, camera_config_list.get(), i, |
| camera_config.get()); |
| |
| ArCameraConfigFacingDirection facing_direction; |
| ArCameraConfig_getFacingDirection(ar_session, camera_config.get(), |
| &facing_direction); |
| |
| if (facing_direction != AR_CAMERA_CONFIG_FACING_DIRECTION_BACK) { |
| DVLOG(2) |
| << __func__ |
| << ": camera config does not refer to back-facing camera, ignoring"; |
| continue; |
| } |
| |
| #if DCHECK_IS_ON() |
| { |
| int32_t tex_width, tex_height; |
| ArCameraConfig_getTextureDimensions(ar_session, camera_config.get(), |
| &tex_width, &tex_height); |
| |
| int32_t img_width, img_height; |
| ArCameraConfig_getImageDimensions(ar_session, camera_config.get(), |
| &img_width, &img_height); |
| |
| uint32_t depth_sensor_usage; |
| ArCameraConfig_getDepthSensorUsage(ar_session, camera_config.get(), |
| &depth_sensor_usage); |
| |
| int32_t min_fps, max_fps; |
| ArCameraConfig_getFpsRange(ar_session, camera_config.get(), &min_fps, |
| &max_fps); |
| |
| DVLOG(3) << __func__ |
| << ": matching camera config found, texture dimensions=" |
| << tex_width << "x" << tex_height |
| << ", image dimensions= " << img_width << "x" << img_height |
| << ", depth sensor usage=" << depth_sensor_usage |
| << ", min_fps=" << min_fps << ", max_fps=" << max_fps; |
| } |
| #endif |
| |
| available_configs.push_back(std::move(camera_config)); |
| } |
| |
| if (available_configs.empty()) { |
| DLOG(ERROR) << "No matching configs found"; |
| return false; |
| } |
| |
| auto best_config = std::max_element( |
| available_configs.begin(), available_configs.end(), |
| [ar_session](const internal::ScopedArCoreObject<ArCameraConfig*>& lhs, |
| const internal::ScopedArCoreObject<ArCameraConfig*>& rhs) { |
| // true means that lhs is less than rhs |
| |
| // We'll prefer the configs with higher total resolution (GPU first, |
| // then CPU), everything else does not matter for us now, but we will |
| // weakly prefer the cameras that support depth sensor (it will be used |
| // as a tie-breaker). |
| |
| { |
| int32_t lhs_tex_width, lhs_tex_height; |
| int32_t rhs_tex_width, rhs_tex_height; |
| |
| ArCameraConfig_getTextureDimensions(ar_session, lhs.get(), |
| &lhs_tex_width, &lhs_tex_height); |
| ArCameraConfig_getTextureDimensions(ar_session, rhs.get(), |
| &rhs_tex_width, &rhs_tex_height); |
| |
| if (lhs_tex_width * lhs_tex_height != |
| rhs_tex_width * rhs_tex_height) { |
| return lhs_tex_width * lhs_tex_height < |
| rhs_tex_width * rhs_tex_height; |
| } |
| } |
| |
| { |
| int32_t lhs_img_width, lhs_img_height; |
| int32_t rhs_img_width, rhs_img_height; |
| |
| ArCameraConfig_getImageDimensions(ar_session, lhs.get(), |
| &lhs_img_width, &lhs_img_height); |
| ArCameraConfig_getImageDimensions(ar_session, rhs.get(), |
| &rhs_img_width, &rhs_img_height); |
| |
| if (lhs_img_width * lhs_img_height != |
| rhs_img_width * rhs_img_height) { |
| return lhs_img_width * lhs_img_height < |
| rhs_img_width * rhs_img_height; |
| } |
| } |
| |
| { |
| uint32_t lhs_depth_sensor_usage; |
| uint32_t rhs_depth_sensor_usage; |
| |
| ArCameraConfig_getDepthSensorUsage(ar_session, lhs.get(), |
| &lhs_depth_sensor_usage); |
| ArCameraConfig_getDepthSensorUsage(ar_session, rhs.get(), |
| &rhs_depth_sensor_usage); |
| |
| bool lhs_has_depth = |
| lhs_depth_sensor_usage & |
| AR_CAMERA_CONFIG_DEPTH_SENSOR_USAGE_REQUIRE_AND_USE; |
| bool rhs_has_depth = |
| rhs_depth_sensor_usage & |
| AR_CAMERA_CONFIG_DEPTH_SENSOR_USAGE_REQUIRE_AND_USE; |
| |
| return lhs_has_depth < rhs_has_depth; |
| } |
| }); |
| |
| int32_t fps_min, fps_max; |
| ArCameraConfig_getFpsRange(ar_session, best_config->get(), &fps_min, |
| &fps_max); |
| target_framerate_range_ = {static_cast<float>(fps_min), |
| static_cast<float>(fps_max)}; |
| |
| #if DCHECK_IS_ON() |
| { |
| int32_t tex_width, tex_height; |
| ArCameraConfig_getTextureDimensions(ar_session, best_config->get(), |
| &tex_width, &tex_height); |
| |
| int32_t img_width, img_height; |
| ArCameraConfig_getImageDimensions(ar_session, best_config->get(), |
| &img_width, &img_height); |
| |
| uint32_t depth_sensor_usage; |
| ArCameraConfig_getDepthSensorUsage(ar_session, best_config->get(), |
| &depth_sensor_usage); |
| DVLOG(3) << __func__ |
| << ": selected camera config with texture dimensions=" << tex_width |
| << "x" << tex_height << ", image dimensions=" << img_width << "x" |
| << img_height << ", depth sensor usage=" << depth_sensor_usage |
| << ", min_fps=" << target_framerate_range_.min |
| << ", max_fps=" << target_framerate_range_.max; |
| } |
| #endif |
| |
| ArStatus status = ArSession_setCameraConfig(ar_session, best_config->get()); |
| if (status != AR_SUCCESS) { |
| DLOG(ERROR) << "ArSession_setCameraConfig failed: " << status; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| ArCore::MinMaxRange ArCoreImpl::GetTargetFramerateRange() { |
| return target_framerate_range_; |
| } |
| |
| void ArCoreImpl::BuildImageDatabase( |
| const ArSession* session, |
| ArAugmentedImageDatabase* image_db, |
| const std::vector<device::mojom::XRTrackedImagePtr>& tracked_images) { |
| for (std::size_t index = 0; index < tracked_images.size(); ++index) { |
| const device::mojom::XRTrackedImage* image = tracked_images[index].get(); |
| gfx::Size size = image->size_in_pixels; |
| |
| // Use Skia to convert the image to grayscale. |
| const SkBitmap& src_bitmap = image->bitmap; |
| SkBitmap canvas_bitmap; |
| canvas_bitmap.allocPixelsFlags( |
| SkImageInfo::Make(size.width(), size.height(), kGray_8_SkColorType, |
| kOpaque_SkAlphaType), |
| SkBitmap::kZeroPixels_AllocFlag); |
| SkCanvas gray_canvas(canvas_bitmap); |
| sk_sp<SkImage> src_image = SkImage::MakeFromBitmap(src_bitmap); |
| gray_canvas.drawImage(src_image, 0, 0); |
| SkPixmap gray_pixmap; |
| if (!gray_canvas.peekPixels(&gray_pixmap)) { |
| DLOG(WARNING) << __func__ << ": failed to access grayscale bitmap"; |
| image_trackable_scores_.push_back(false); |
| continue; |
| } |
| |
| const SkPixmap& pixmap = gray_pixmap; |
| float width_in_meters = image->width_in_meters; |
| DVLOG(3) << __func__ << " tracked image index=" << index |
| << " size=" << pixmap.width() << "x" << pixmap.height() |
| << " width_in_meters=" << width_in_meters; |
| int32_t arcore_id = -1; |
| std::string id_name = base::NumberToString(index); |
| ArStatus status = ArAugmentedImageDatabase_addImageWithPhysicalSize( |
| session, image_db, id_name.c_str(), pixmap.addr8(), pixmap.width(), |
| pixmap.height(), pixmap.rowBytesAsPixels(), width_in_meters, |
| &arcore_id); |
| if (status != AR_SUCCESS) { |
| DVLOG(2) << __func__ << ": add image failed"; |
| image_trackable_scores_.push_back(false); |
| continue; |
| } |
| |
| // ARCore uses internal IDs for images, these only include the trackable |
| // images. The tracking results need to refer to the original image index |
| // corresponding to its position in the input tracked_images array. |
| tracked_image_arcore_id_to_index_[arcore_id] = index; |
| DVLOG(2) << __func__ << ": added image, index=" << index |
| << " arcore_id=" << arcore_id; |
| image_trackable_scores_.push_back(true); |
| } |
| } |
| |
| void ArCoreImpl::SetCameraTexture(uint32_t camera_texture_id) { |
| DCHECK(IsOnGlThread()); |
| DCHECK(arcore_session_.is_valid()); |
| ArSession_setCameraTextureName(arcore_session_.get(), camera_texture_id); |
| } |
| |
| void ArCoreImpl::SetDisplayGeometry( |
| const gfx::Size& frame_size, |
| display::Display::Rotation display_rotation) { |
| DCHECK(IsOnGlThread()); |
| DCHECK(arcore_session_.is_valid()); |
| // Display::Rotation is the same as Android's rotation and is compatible with |
| // what ArCore is expecting. |
| ArSession_setDisplayGeometry(arcore_session_.get(), display_rotation, |
| frame_size.width(), frame_size.height()); |
| } |
| |
| std::vector<float> ArCoreImpl::TransformDisplayUvCoords( |
| const base::span<const float> uvs) const { |
| DCHECK(IsOnGlThread()); |
| DCHECK(arcore_session_.is_valid()); |
| DCHECK(arcore_frame_.is_valid()); |
| |
| size_t num_elements = uvs.size(); |
| DCHECK(num_elements % 2 == 0); |
| DCHECK_GE(num_elements, 6u); |
| |
| std::vector<float> uvs_out(num_elements); |
| ArFrame_transformCoordinates2d( |
| arcore_session_.get(), arcore_frame_.get(), |
| AR_COORDINATES_2D_VIEW_NORMALIZED, num_elements / 2, &uvs[0], |
| AR_COORDINATES_2D_TEXTURE_NORMALIZED, &uvs_out[0]); |
| |
| DVLOG(3) << __func__ << ": transformed uvs=[ " << uvs_out[0] << " , " |
| << uvs_out[1] << " , " << uvs_out[2] << " , " << uvs_out[3] << " , " |
| << uvs_out[4] << " , " << uvs_out[5] << " ]"; |
| |
| return uvs_out; |
| } |
| |
| gfx::Size ArCoreImpl::GetUncroppedCameraImageSize() const { |
| return uncropped_camera_image_size_; |
| } |
| |
| mojom::VRPosePtr ArCoreImpl::Update(bool* camera_updated) { |
| DVLOG(3) << __func__; |
| |
| TRACE_EVENT0("gpu", "ArCoreImpl Update"); |
| |
| DCHECK(IsOnGlThread()); |
| DCHECK(arcore_session_.is_valid()); |
| DCHECK(arcore_frame_.is_valid()); |
| DCHECK(camera_updated); |
| |
| TRACE_EVENT_BEGIN0("gpu", "ArCore Update"); |
| ArStatus status = |
| ArSession_update(arcore_session_.get(), arcore_frame_.get()); |
| TRACE_EVENT_END0("gpu", "ArCore Update"); |
| |
| if (status != AR_SUCCESS) { |
| DLOG(ERROR) << "ArSession_update failed: " << status; |
| *camera_updated = false; |
| return nullptr; |
| } |
| |
| if (plane_manager_) { |
| TRACE_EVENT0("gpu", "ArCorePlaneManager Update"); |
| plane_manager_->Update(arcore_frame_.get()); |
| } |
| |
| if (anchor_manager_) { |
| TRACE_EVENT0("gpu", "ArCoreAnchorManager Update"); |
| anchor_manager_->Update(arcore_frame_.get()); |
| } |
| |
| // If we get here, assume we have a valid camera image, but we don't know yet |
| // if tracking is working. |
| *camera_updated = true; |
| internal::ScopedArCoreObject<ArCamera*> arcore_camera; |
| ArFrame_acquireCamera( |
| arcore_session_.get(), arcore_frame_.get(), |
| internal::ScopedArCoreObject<ArCamera*>::Receiver(arcore_camera).get()); |
| if (!arcore_camera.is_valid()) { |
| DLOG(ERROR) << "ArFrame_acquireCamera failed!"; |
| return nullptr; |
| } |
| |
| // Get the camera image dimensions via ARCore's intrinsics methods. We don't |
| // currently use the raw focal length and principal point that's exported by |
| // ARCore. Instead, WebXR uses the projection matrix provided by |
| // ARCore. Currently, it seems that ARCore simply uses a centered virtual |
| // camera, ignoring the low-level principal point which may be offset from |
| // center by a few pixels. This is unlikely to make a noticeable difference in |
| // practice. |
| internal::ScopedArCoreObject<ArCameraIntrinsics*> intrinsics; |
| ArCameraIntrinsics_create( |
| arcore_session_.get(), |
| internal::ScopedArCoreObject<ArCameraIntrinsics*>::Receiver(intrinsics) |
| .get()); |
| ArCamera_getTextureIntrinsics(arcore_session_.get(), arcore_camera.get(), |
| intrinsics.get()); |
| int32_t intrinsics_width, intrinsics_height; |
| ArCameraIntrinsics_getImageDimensions(arcore_session_.get(), intrinsics.get(), |
| &intrinsics_width, &intrinsics_height); |
| DVLOG(3) << __func__ << ": intrinsics_width=" << intrinsics_width |
| << " intrinsics_height=" << intrinsics_height; |
| uncropped_camera_image_size_ = {intrinsics_width, intrinsics_height}; |
| |
| ArTrackingState tracking_state; |
| ArCamera_getTrackingState(arcore_session_.get(), arcore_camera.get(), |
| &tracking_state); |
| if (tracking_state != AR_TRACKING_STATE_TRACKING) { |
| DVLOG(1) << "Tracking state is not AR_TRACKING_STATE_TRACKING: " |
| << tracking_state; |
| return nullptr; |
| } |
| |
| internal::ScopedArCoreObject<ArPose*> arcore_pose; |
| ArPose_create( |
| arcore_session_.get(), nullptr, |
| internal::ScopedArCoreObject<ArPose*>::Receiver(arcore_pose).get()); |
| if (!arcore_pose.is_valid()) { |
| DLOG(ERROR) << "ArPose_create failed!"; |
| return nullptr; |
| } |
| |
| ArCamera_getDisplayOrientedPose(arcore_session_.get(), arcore_camera.get(), |
| arcore_pose.get()); |
| |
| auto mojo_from_viewer = |
| GetMojomVRPoseFromArPose(arcore_session_.get(), arcore_pose.get()); |
| |
| return mojo_from_viewer; |
| } |
| |
| mojom::XRTrackedImagesDataPtr ArCoreImpl::GetTrackedImages() { |
| std::vector<mojom::XRTrackedImageDataPtr> images_data; |
| |
| internal::ScopedArCoreObject<ArTrackableList*> updated_images; |
| ArTrackableList_create( |
| arcore_session_.get(), |
| internal::ScopedArCoreObject<ArTrackableList*>::Receiver(updated_images) |
| .get()); |
| ArFrame_getUpdatedTrackables(arcore_session_.get(), arcore_frame_.get(), |
| AR_TRACKABLE_AUGMENTED_IMAGE, |
| updated_images.get()); |
| |
| int32_t images_count = 0; |
| ArTrackableList_getSize(arcore_session_.get(), updated_images.get(), |
| &images_count); |
| DVLOG(2) << __func__ << ": trackable images count=" << images_count; |
| |
| for (int32_t i = 0; i < images_count; ++i) { |
| internal::ScopedArCoreObject<ArTrackable*> trackable; |
| ArTrackableList_acquireItem( |
| arcore_session_.get(), updated_images.get(), i, |
| internal::ScopedArCoreObject<ArTrackable*>::Receiver(trackable).get()); |
| ArTrackingState tracking_state; |
| ArTrackable_getTrackingState(arcore_session_.get(), trackable.get(), |
| &tracking_state); |
| ArAugmentedImage* image = ArAsAugmentedImage(trackable.get()); |
| |
| // Get the original image index from ARCore's internal ID for use in the |
| // returned results. |
| int32_t arcore_id; |
| ArAugmentedImage_getIndex(arcore_session_.get(), image, &arcore_id); |
| uint64_t index = tracked_image_arcore_id_to_index_[arcore_id]; |
| DVLOG(3) << __func__ << ": #" << i << " tracked image index=" << index |
| << " arcore_id=" << arcore_id << " state=" << tracking_state; |
| |
| if (tracking_state == AR_TRACKING_STATE_TRACKING) { |
| internal::ScopedArCoreObject<ArPose*> arcore_pose; |
| ArPose_create( |
| arcore_session_.get(), nullptr, |
| internal::ScopedArCoreObject<ArPose*>::Receiver(arcore_pose).get()); |
| if (!arcore_pose.is_valid()) { |
| DLOG(ERROR) << "ArPose_create failed!"; |
| continue; |
| } |
| ArAugmentedImage_getCenterPose(arcore_session_.get(), image, |
| arcore_pose.get()); |
| float pose_raw[7]; |
| ArPose_getPoseRaw(arcore_session_.get(), arcore_pose.get(), &pose_raw[0]); |
| DVLOG(3) << __func__ << ": tracked image pose_raw pos=(" << pose_raw[4] |
| << ", " << pose_raw[5] << ", " << pose_raw[6] << ")"; |
| |
| device::Pose device_pose = |
| GetPoseFromArPose(arcore_session_.get(), arcore_pose.get()); |
| |
| ArAugmentedImageTrackingMethod tracking_method; |
| ArAugmentedImage_getTrackingMethod(arcore_session_.get(), image, |
| &tracking_method); |
| bool actively_tracked = |
| tracking_method == AR_AUGMENTED_IMAGE_TRACKING_METHOD_FULL_TRACKING; |
| |
| float width_in_meters; |
| ArAugmentedImage_getExtentX(arcore_session_.get(), image, |
| &width_in_meters); |
| |
| images_data.push_back(mojom::XRTrackedImageData::New( |
| index, device_pose, actively_tracked, width_in_meters)); |
| } |
| } |
| |
| // Include information about each image's trackability status in the first |
| // returned result list. |
| if (!image_trackable_scores_sent_) { |
| auto ret = mojom::XRTrackedImagesData::New( |
| std::move(images_data), std::move(image_trackable_scores_)); |
| image_trackable_scores_sent_ = true; |
| image_trackable_scores_.clear(); |
| return ret; |
| } else { |
| return mojom::XRTrackedImagesData::New(std::move(images_data), |
| absl::nullopt); |
| } |
| } |
| |
| base::TimeDelta ArCoreImpl::GetFrameTimestamp() { |
| DCHECK(arcore_session_.is_valid()); |
| DCHECK(arcore_frame_.is_valid()); |
| int64_t out_timestamp_ns; |
| ArFrame_getTimestamp(arcore_session_.get(), arcore_frame_.get(), |
| &out_timestamp_ns); |
| return base::Nanoseconds(out_timestamp_ns); |
| } |
| |
| mojom::XRPlaneDetectionDataPtr ArCoreImpl::GetDetectedPlanesData() { |
| DVLOG(2) << __func__; |
| |
| TRACE_EVENT0("gpu", __func__); |
| |
| // ArCoreGl::ProcessFrame only calls this method if the feature is enabled. |
| DCHECK(plane_manager_); |
| |
| return plane_manager_->GetDetectedPlanesData(); |
| } |
| |
| mojom::XRAnchorsDataPtr ArCoreImpl::GetAnchorsData() { |
| DVLOG(2) << __func__; |
| |
| TRACE_EVENT0("gpu", __func__); |
| |
| // ArCoreGl::ProcessFrame only calls this method if the feature is enabled. |
| DCHECK(anchor_manager_); |
| |
| return anchor_manager_->GetAnchorsData(); |
| } |
| |
| mojom::XRLightEstimationDataPtr ArCoreImpl::GetLightEstimationData() { |
| TRACE_EVENT0("gpu", __func__); |
| |
| // ArCoreGl::ProcessFrame only calls this method if the feature is enabled. |
| DCHECK(arcore_light_estimate_.get()); |
| |
| ArFrame_getLightEstimate(arcore_session_.get(), arcore_frame_.get(), |
| arcore_light_estimate_.get()); |
| |
| ArLightEstimateState light_estimate_state = AR_LIGHT_ESTIMATE_STATE_NOT_VALID; |
| ArLightEstimate_getState(arcore_session_.get(), arcore_light_estimate_.get(), |
| &light_estimate_state); |
| |
| // The light estimate state is not guaranteed to be valid initially |
| if (light_estimate_state != AR_LIGHT_ESTIMATE_STATE_VALID) { |
| DVLOG(2) << "ArCore light estimation state invalid."; |
| return nullptr; |
| } |
| |
| auto light_estimation_data = mojom::XRLightEstimationData::New(); |
| light_estimation_data->light_probe = |
| GetLightProbe(arcore_session_.get(), arcore_light_estimate_.get()); |
| if (!light_estimation_data->light_probe) { |
| DVLOG(1) << "Failed to generate light probe."; |
| return nullptr; |
| } |
| light_estimation_data->reflection_probe = |
| GetReflectionProbe(arcore_session_.get(), arcore_light_estimate_.get()); |
| if (!light_estimation_data->reflection_probe) { |
| DVLOG(1) << "Failed to generate reflection probe."; |
| return nullptr; |
| } |
| |
| return light_estimation_data; |
| } |
| |
| void ArCoreImpl::Pause() { |
| DVLOG(2) << __func__; |
| |
| DCHECK(IsOnGlThread()); |
| DCHECK(arcore_session_.is_valid()); |
| |
| ArStatus status = ArSession_pause(arcore_session_.get()); |
| DLOG_IF(ERROR, status != AR_SUCCESS) |
| << "ArSession_pause failed: status = " << status; |
| } |
| |
| void ArCoreImpl::Resume() { |
| DVLOG(2) << __func__; |
| |
| DCHECK(IsOnGlThread()); |
| DCHECK(arcore_session_.is_valid()); |
| |
| ArStatus status = ArSession_resume(arcore_session_.get()); |
| DLOG_IF(ERROR, status != AR_SUCCESS) |
| << "ArSession_resume failed: status = " << status; |
| } |
| |
| gfx::Transform ArCoreImpl::GetProjectionMatrix(float near, float far) { |
| DCHECK(IsOnGlThread()); |
| DCHECK(arcore_session_.is_valid()); |
| DCHECK(arcore_frame_.is_valid()); |
| |
| internal::ScopedArCoreObject<ArCamera*> arcore_camera; |
| ArFrame_acquireCamera( |
| arcore_session_.get(), arcore_frame_.get(), |
| internal::ScopedArCoreObject<ArCamera*>::Receiver(arcore_camera).get()); |
| DCHECK(arcore_camera.is_valid()) |
| << "ArFrame_acquireCamera failed despite documentation saying it cannot"; |
| |
| // ArCore's projection matrix is 16 floats in column-major order. |
| float matrix_4x4[16]; |
| ArCamera_getProjectionMatrix(arcore_session_.get(), arcore_camera.get(), near, |
| far, matrix_4x4); |
| gfx::Transform result; |
| result.matrix().setColMajorf(matrix_4x4); |
| return result; |
| } |
| |
| float ArCoreImpl::GetEstimatedFloorHeight() { |
| return kDefaultFloorHeightEstimation; |
| } |
| |
| absl::optional<uint64_t> ArCoreImpl::SubscribeToHitTest( |
| mojom::XRNativeOriginInformationPtr native_origin_information, |
| const std::vector<mojom::EntityTypeForHitTest>& entity_types, |
| mojom::XRRayPtr ray) { |
| // First, check if we recognize the type of the native origin. |
| switch (native_origin_information->which()) { |
| case mojom::XRNativeOriginInformation::Tag::INPUT_SOURCE_SPACE_INFO: |
| // Input sources are verified in the higher layer as ArCoreImpl does |
| // not carry input source state. |
| break; |
| case mojom::XRNativeOriginInformation::Tag::REFERENCE_SPACE_TYPE: |
| // Reference spaces are implicitly recognized and don't carry an ID. |
| break; |
| case mojom::XRNativeOriginInformation::Tag::PLANE_ID: |
| // Validate that we know which plane's space the hit test is interested in |
| // tracking. |
| if (!plane_manager_ || !plane_manager_->PlaneExists(PlaneId( |
| native_origin_information->get_plane_id()))) { |
| return absl::nullopt; |
| } |
| break; |
| case mojom::XRNativeOriginInformation::Tag::HAND_JOINT_SPACE_INFO: |
| // Unsupported by ARCore: |
| return absl::nullopt; |
| case mojom::XRNativeOriginInformation::Tag::IMAGE_INDEX: |
| // TODO(https://crbug.com/1143575): Add hit test support for tracked |
| // images. |
| return absl::nullopt; |
| case mojom::XRNativeOriginInformation::Tag::ANCHOR_ID: |
| // Validate that we know which anchor's space the hit test is interested |
| // in tracking. |
| if (!anchor_manager_ || |
| !anchor_manager_->AnchorExists( |
| AnchorId(native_origin_information->get_anchor_id()))) { |
| return absl::nullopt; |
| } |
| break; |
| } |
| |
| auto subscription_id = CreateHitTestSubscriptionId(); |
| |
| hit_test_subscription_id_to_data_.emplace( |
| subscription_id, |
| HitTestSubscriptionData{std::move(native_origin_information), |
| entity_types, std::move(ray)}); |
| |
| return subscription_id.GetUnsafeValue(); |
| } |
| |
| absl::optional<uint64_t> ArCoreImpl::SubscribeToHitTestForTransientInput( |
| const std::string& profile_name, |
| const std::vector<mojom::EntityTypeForHitTest>& entity_types, |
| mojom::XRRayPtr ray) { |
| auto subscription_id = CreateHitTestSubscriptionId(); |
| |
| hit_test_subscription_id_to_transient_hit_test_data_.emplace( |
| subscription_id, TransientInputHitTestSubscriptionData{ |
| profile_name, entity_types, std::move(ray)}); |
| |
| return subscription_id.GetUnsafeValue(); |
| } |
| |
| mojom::XRHitTestSubscriptionResultsDataPtr |
| ArCoreImpl::GetHitTestSubscriptionResults( |
| const gfx::Transform& mojo_from_viewer, |
| const std::vector<mojom::XRInputSourceStatePtr>& input_state) { |
| mojom::XRHitTestSubscriptionResultsDataPtr result = |
| mojom::XRHitTestSubscriptionResultsData::New(); |
| |
| DVLOG(3) << __func__ |
| << ": calculating hit test subscription results, " |
| "hit_test_subscription_id_to_data_.size()=" |
| << hit_test_subscription_id_to_data_.size(); |
| |
| for (auto& subscription_id_and_data : hit_test_subscription_id_to_data_) { |
| // First, check if we can find the current transformation for a ray. If not, |
| // skip processing this subscription. |
| auto maybe_mojo_from_native_origin = GetMojoFromNativeOrigin( |
| *subscription_id_and_data.second.native_origin_information, |
| mojo_from_viewer, input_state); |
| |
| if (!maybe_mojo_from_native_origin) { |
| continue; |
| } |
| |
| // Since we have a transform, let's use it to obtain hit test results. |
| result->results.push_back(GetHitTestSubscriptionResult( |
| HitTestSubscriptionId(subscription_id_and_data.first), |
| *subscription_id_and_data.second.ray, |
| subscription_id_and_data.second.entity_types, |
| *maybe_mojo_from_native_origin)); |
| } |
| |
| DVLOG(3) |
| << __func__ |
| << ": calculating hit test subscription results for transient input, " |
| "hit_test_subscription_id_to_transient_hit_test_data_.size()=" |
| << hit_test_subscription_id_to_transient_hit_test_data_.size(); |
| |
| for (const auto& subscription_id_and_data : |
| hit_test_subscription_id_to_transient_hit_test_data_) { |
| auto input_source_ids_and_transforms = |
| GetMojoFromInputSources(subscription_id_and_data.second.profile_name, |
| mojo_from_viewer, input_state); |
| |
| result->transient_input_results.push_back( |
| GetTransientHitTestSubscriptionResult( |
| HitTestSubscriptionId(subscription_id_and_data.first), |
| *subscription_id_and_data.second.ray, |
| subscription_id_and_data.second.entity_types, |
| input_source_ids_and_transforms)); |
| } |
| |
| return result; |
| } |
| |
| device::mojom::XRHitTestSubscriptionResultDataPtr |
| ArCoreImpl::GetHitTestSubscriptionResult( |
| HitTestSubscriptionId id, |
| const mojom::XRRay& native_origin_ray, |
| const std::vector<mojom::EntityTypeForHitTest>& entity_types, |
| const gfx::Transform& mojo_from_native_origin) { |
| DVLOG(3) << __func__ << ": id=" << id; |
| |
| // Transform the ray according to the latest transform based on the XRSpace |
| // used in hit test subscription. |
| |
| gfx::Point3F origin = native_origin_ray.origin; |
| mojo_from_native_origin.TransformPoint(&origin); |
| |
| gfx::Vector3dF direction = native_origin_ray.direction; |
| mojo_from_native_origin.TransformVector(&direction); |
| |
| std::vector<mojom::XRHitResultPtr> hit_results; |
| if (!RequestHitTest(origin, direction, entity_types, &hit_results)) { |
| hit_results.clear(); // On failure, clear partial results. |
| } |
| |
| return mojom::XRHitTestSubscriptionResultData::New(id.GetUnsafeValue(), |
| std::move(hit_results)); |
| } |
| |
| device::mojom::XRHitTestTransientInputSubscriptionResultDataPtr |
| ArCoreImpl::GetTransientHitTestSubscriptionResult( |
| HitTestSubscriptionId id, |
| const mojom::XRRay& input_source_ray, |
| const std::vector<mojom::EntityTypeForHitTest>& entity_types, |
| const std::vector<std::pair<uint32_t, gfx::Transform>>& |
| input_source_ids_and_mojo_from_input_sources) { |
| auto result = |
| device::mojom::XRHitTestTransientInputSubscriptionResultData::New(); |
| |
| result->subscription_id = id.GetUnsafeValue(); |
| |
| for (const auto& input_source_id_and_mojo_from_input_source : |
| input_source_ids_and_mojo_from_input_sources) { |
| gfx::Point3F origin = input_source_ray.origin; |
| input_source_id_and_mojo_from_input_source.second.TransformPoint(&origin); |
| |
| gfx::Vector3dF direction = input_source_ray.direction; |
| input_source_id_and_mojo_from_input_source.second.TransformVector( |
| &direction); |
| |
| std::vector<mojom::XRHitResultPtr> hit_results; |
| if (!RequestHitTest(origin, direction, entity_types, &hit_results)) { |
| hit_results.clear(); // On failure, clear partial results. |
| } |
| |
| result->input_source_id_to_hit_test_results.insert( |
| {input_source_id_and_mojo_from_input_source.first, |
| std::move(hit_results)}); |
| } |
| |
| return result; |
| } |
| |
| std::vector<std::pair<uint32_t, gfx::Transform>> |
| ArCoreImpl::GetMojoFromInputSources( |
| const std::string& profile_name, |
| const gfx::Transform& mojo_from_viewer, |
| const std::vector<mojom::XRInputSourceStatePtr>& input_state) { |
| std::vector<std::pair<uint32_t, gfx::Transform>> result; |
| |
| for (const auto& input_source_state : input_state) { |
| if (input_source_state && input_source_state->description) { |
| if (base::Contains(input_source_state->description->profiles, |
| profile_name)) { |
| // Input source represented by input_state matches the profile, find |
| // the transform and grab input source id. |
| absl::optional<gfx::Transform> maybe_mojo_from_input_source = |
| GetMojoFromInputSource(input_source_state, mojo_from_viewer); |
| |
| if (!maybe_mojo_from_input_source) |
| continue; |
| |
| result.push_back( |
| {input_source_state->source_id, *maybe_mojo_from_input_source}); |
| } |
| } |
| } |
| |
| return result; |
| } |
| |
| absl::optional<gfx::Transform> ArCoreImpl::GetMojoFromReferenceSpace( |
| device::mojom::XRReferenceSpaceType type, |
| const gfx::Transform& mojo_from_viewer) { |
| DVLOG(3) << __func__ << ": type=" << type; |
| |
| switch (type) { |
| case device::mojom::XRReferenceSpaceType::kLocal: |
| return gfx::Transform{}; |
| case device::mojom::XRReferenceSpaceType::kLocalFloor: { |
| auto result = gfx::Transform{}; |
| result.Translate3d(0, -GetEstimatedFloorHeight(), 0); |
| return result; |
| } |
| case device::mojom::XRReferenceSpaceType::kViewer: |
| return mojo_from_viewer; |
| case device::mojom::XRReferenceSpaceType::kBoundedFloor: |
| return absl::nullopt; |
| case device::mojom::XRReferenceSpaceType::kUnbounded: |
| return absl::nullopt; |
| } |
| } |
| |
| bool ArCoreImpl::NativeOriginExists( |
| const mojom::XRNativeOriginInformation& native_origin_information, |
| const std::vector<mojom::XRInputSourceStatePtr>& input_state) { |
| switch (native_origin_information.which()) { |
| case mojom::XRNativeOriginInformation::Tag::INPUT_SOURCE_SPACE_INFO: { |
| mojom::XRInputSourceSpaceInfo* input_source_space_info = |
| native_origin_information.get_input_source_space_info().get(); |
| |
| // ARCore only supports input sources that have "TAPPING" target ray mode, |
| // those input sources do not have grip space so the native origin is |
| // guaranteed not to exist: |
| if (input_source_space_info->input_source_space_type == |
| mojom::XRInputSourceSpaceType::kGrip) { |
| return false; |
| } |
| |
| // Linear search should be fine for ARCore device as it only has one input |
| // source (for now). |
| for (auto& input_source_state : input_state) { |
| if (input_source_state->source_id == |
| input_source_space_info->input_source_id) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| case mojom::XRNativeOriginInformation::Tag::REFERENCE_SPACE_TYPE: |
| // All reference spaces are known to ARCore. |
| return true; |
| |
| case mojom::XRNativeOriginInformation::Tag::PLANE_ID: |
| return plane_manager_ ? plane_manager_->PlaneExists(PlaneId( |
| native_origin_information.get_plane_id())) |
| : false; |
| case mojom::XRNativeOriginInformation::Tag::ANCHOR_ID: |
| return anchor_manager_ ? anchor_manager_->AnchorExists(AnchorId( |
| native_origin_information.get_anchor_id())) |
| : false; |
| case mojom::XRNativeOriginInformation::Tag::HAND_JOINT_SPACE_INFO: |
| return false; |
| case mojom::XRNativeOriginInformation::Tag::IMAGE_INDEX: |
| // TODO(https://crbug.com/1143575): Needed for anchor creation relaitve to |
| // tracked images. |
| return false; |
| } |
| } |
| |
| absl::optional<gfx::Transform> ArCoreImpl::GetMojoFromNativeOrigin( |
| const mojom::XRNativeOriginInformation& native_origin_information, |
| const gfx::Transform& mojo_from_viewer, |
| const std::vector<mojom::XRInputSourceStatePtr>& input_state) { |
| switch (native_origin_information.which()) { |
| case mojom::XRNativeOriginInformation::Tag::INPUT_SOURCE_SPACE_INFO: { |
| mojom::XRInputSourceSpaceInfo* input_source_space_info = |
| native_origin_information.get_input_source_space_info().get(); |
| |
| // ARCore only supports input sources that have "TAPPING" target ray mode, |
| // those input sources do not have grip space so the native origin is |
| // guaranteed not to be localizable: |
| if (input_source_space_info->input_source_space_type == |
| mojom::XRInputSourceSpaceType::kGrip) { |
| return absl::nullopt; |
| } |
| |
| // Linear search should be fine for ARCore device as it only has one input |
| // source (for now). |
| for (auto& input_source_state : input_state) { |
| if (input_source_state->source_id == |
| input_source_space_info->input_source_id) { |
| return GetMojoFromInputSource(input_source_state, mojo_from_viewer); |
| } |
| } |
| |
| return absl::nullopt; |
| } |
| case mojom::XRNativeOriginInformation::Tag::REFERENCE_SPACE_TYPE: |
| return GetMojoFromReferenceSpace( |
| native_origin_information.get_reference_space_type(), |
| mojo_from_viewer); |
| case mojom::XRNativeOriginInformation::Tag::PLANE_ID: |
| return plane_manager_ ? plane_manager_->GetMojoFromPlane(PlaneId( |
| native_origin_information.get_plane_id())) |
| : absl::nullopt; |
| case mojom::XRNativeOriginInformation::Tag::ANCHOR_ID: |
| return anchor_manager_ ? anchor_manager_->GetMojoFromAnchor(AnchorId( |
| native_origin_information.get_anchor_id())) |
| : absl::nullopt; |
| case mojom::XRNativeOriginInformation::Tag::HAND_JOINT_SPACE_INFO: |
| return absl::nullopt; |
| |
| case mojom::XRNativeOriginInformation::Tag::IMAGE_INDEX: |
| // TODO(https://crbug.com/1143575): Needed for hit test and anchors |
| // support for tracked images. |
| return absl::nullopt; |
| } |
| } |
| |
| void ArCoreImpl::UnsubscribeFromHitTest(uint64_t subscription_id) { |
| DVLOG(2) << __func__ << ": subscription_id=" << subscription_id; |
| |
| // Hit test subscription ID space is the same for transient and non-transient |
| // hit test sources, so we can attempt to remove it from both collections (it |
| // will succeed only for one of them anyway). |
| |
| hit_test_subscription_id_to_data_.erase( |
| HitTestSubscriptionId(subscription_id)); |
| hit_test_subscription_id_to_transient_hit_test_data_.erase( |
| HitTestSubscriptionId(subscription_id)); |
| } |
| |
| HitTestSubscriptionId ArCoreImpl::CreateHitTestSubscriptionId() { |
| CHECK(next_id_ != std::numeric_limits<uint64_t>::max()) |
| << "preventing ID overflow"; |
| |
| uint64_t current_id = next_id_++; |
| |
| return HitTestSubscriptionId(current_id); |
| } |
| |
| bool ArCoreImpl::RequestHitTest( |
| const mojom::XRRayPtr& ray, |
| std::vector<mojom::XRHitResultPtr>* hit_results) { |
| DCHECK(ray); |
| return RequestHitTest(ray->origin, ray->direction, |
| {mojom::EntityTypeForHitTest::PLANE}, |
| hit_results); // "Plane" to maintain current behavior |
| // of async hit test. |
| } |
| |
| bool ArCoreImpl::RequestHitTest( |
| const gfx::Point3F& origin, |
| const gfx::Vector3dF& direction, |
| const std::vector<mojom::EntityTypeForHitTest>& entity_types, |
| std::vector<mojom::XRHitResultPtr>* hit_results) { |
| DVLOG(2) << __func__ << ": origin=" << origin.ToString() |
| << ", direction=" << direction.ToString(); |
| |
| DCHECK(hit_results); |
| DCHECK(IsOnGlThread()); |
| DCHECK(arcore_session_.is_valid()); |
| DCHECK(arcore_frame_.is_valid()); |
| |
| auto arcore_entity_types = GetArCoreEntityTypes(entity_types); |
| |
| // ArCore returns hit-results in sorted order, thus providing the guarantee |
| // of sorted results promised by the WebXR spec for requestHitTest(). |
| std::array<float, 3> origin_array = {origin.x(), origin.y(), origin.z()}; |
| std::array<float, 3> direction_array = {direction.x(), direction.y(), |
| direction.z()}; |
| |
| internal::ScopedArCoreObject<ArHitResultList*> arcore_hit_result_list; |
| ArHitResultList_create( |
| arcore_session_.get(), |
| internal::ScopedArCoreObject<ArHitResultList*>::Receiver( |
| arcore_hit_result_list) |
| .get()); |
| if (!arcore_hit_result_list.is_valid()) { |
| DLOG(ERROR) << "ArHitResultList_create failed!"; |
| return false; |
| } |
| |
| ArFrame_hitTestRay(arcore_session_.get(), arcore_frame_.get(), |
| origin_array.data(), direction_array.data(), |
| arcore_hit_result_list.get()); |
| |
| int arcore_hit_result_list_size = 0; |
| ArHitResultList_getSize(arcore_session_.get(), arcore_hit_result_list.get(), |
| &arcore_hit_result_list_size); |
| DVLOG(2) << __func__ |
| << ": arcore_hit_result_list_size=" << arcore_hit_result_list_size; |
| |
| // Go through the list in reverse order so the first hit we encounter is the |
| // furthest. |
| // We will accept the furthest hit and then for the rest require that the hit |
| // be within the actual polygon detected by ArCore. This heuristic allows us |
| // to get better results on floors w/o overestimating the size of tables etc. |
| // See https://crbug.com/872855. |
| for (int i = arcore_hit_result_list_size - 1; i >= 0; i--) { |
| internal::ScopedArCoreObject<ArHitResult*> arcore_hit; |
| |
| ArHitResult_create( |
| arcore_session_.get(), |
| internal::ScopedArCoreObject<ArHitResult*>::Receiver(arcore_hit).get()); |
| |
| if (!arcore_hit.is_valid()) { |
| DLOG(ERROR) << "ArHitResult_create failed!"; |
| return false; |
| } |
| |
| ArHitResultList_getItem(arcore_session_.get(), arcore_hit_result_list.get(), |
| i, arcore_hit.get()); |
| |
| internal::ScopedArCoreObject<ArTrackable*> ar_trackable; |
| |
| ArHitResult_acquireTrackable( |
| arcore_session_.get(), arcore_hit.get(), |
| internal::ScopedArCoreObject<ArTrackable*>::Receiver(ar_trackable) |
| .get()); |
| ArTrackableType ar_trackable_type = AR_TRACKABLE_NOT_VALID; |
| ArTrackable_getType(arcore_session_.get(), ar_trackable.get(), |
| &ar_trackable_type); |
| |
| // Only consider trackables listed in arcore_entity_types. |
| if (!base::Contains(arcore_entity_types, ar_trackable_type)) { |
| DVLOG(2) << __func__ |
| << ": hit a trackable that is not in entity types set, ignoring " |
| "it. ar_trackable_type=" |
| << ar_trackable_type; |
| continue; |
| } |
| |
| internal::ScopedArCoreObject<ArPose*> arcore_pose; |
| ArPose_create( |
| arcore_session_.get(), nullptr, |
| internal::ScopedArCoreObject<ArPose*>::Receiver(arcore_pose).get()); |
| if (!arcore_pose.is_valid()) { |
| DLOG(ERROR) << "ArPose_create failed!"; |
| return false; |
| } |
| |
| ArHitResult_getHitPose(arcore_session_.get(), arcore_hit.get(), |
| arcore_pose.get()); |
| |
| // After the first (furthest) hit, for planes, only return hits that are |
| // within the actual detected polygon and not just within than the larger |
| // plane. |
| uint64_t plane_id = 0; |
| if (ar_trackable_type == AR_TRACKABLE_PLANE) { |
| ArPlane* ar_plane = ArAsPlane(ar_trackable.get()); |
| |
| if (!hit_results->empty()) { |
| int32_t in_polygon = 0; |
| ArPlane_isPoseInPolygon(arcore_session_.get(), ar_plane, |
| arcore_pose.get(), &in_polygon); |
| if (!in_polygon) { |
| DVLOG(2) << __func__ |
| << ": hit a trackable that is not within detected polygon, " |
| "ignoring it"; |
| continue; |
| } |
| } |
| |
| absl::optional<PlaneId> maybe_plane_id = |
| plane_manager_ ? plane_manager_->GetPlaneId(ar_plane) : absl::nullopt; |
| if (maybe_plane_id) { |
| plane_id = maybe_plane_id->GetUnsafeValue(); |
| } |
| } |
| |
| mojom::XRHitResultPtr mojo_hit = mojom::XRHitResult::New(); |
| |
| mojo_hit->plane_id = plane_id; |
| |
| { |
| std::array<float, 7> raw_pose; |
| ArPose_getPoseRaw(arcore_session_.get(), arcore_pose.get(), |
| raw_pose.data()); |
| |
| gfx::Quaternion orientation(raw_pose[0], raw_pose[1], raw_pose[2], |
| raw_pose[3]); |
| gfx::Point3F position(raw_pose[4], raw_pose[5], raw_pose[6]); |
| |
| mojo_hit->mojo_from_result = device::Pose(position, orientation); |
| |
| DVLOG(3) << __func__ |
| << ": adding hit test result, position=" << position.ToString() |
| << ", orientation=" << orientation.ToString() |
| << ", plane_id=" << plane_id << " (0 means no plane)"; |
| } |
| |
| // Insert new results at head to preserver order from ArCore |
| hit_results->insert(hit_results->begin(), std::move(mojo_hit)); |
| } |
| |
| DVLOG(2) << __func__ << ": hit_results->size()=" << hit_results->size(); |
| return true; |
| } |
| |
| void ArCoreImpl::CreateAnchor( |
| const mojom::XRNativeOriginInformation& native_origin_information, |
| const device::Pose& native_origin_from_anchor, |
| CreateAnchorCallback callback) { |
| DVLOG(2) << __func__ << ": native_origin_information.which()=" |
| << static_cast<uint32_t>(native_origin_information.which()) |
| << ", native_origin_from_anchor.position()=" |
| << native_origin_from_anchor.position().ToString() |
| << ", native_origin_from_anchor.orientation()=" |
| << native_origin_from_anchor.orientation().ToString(); |
| |
| create_anchor_requests_.emplace_back(native_origin_information, |
| native_origin_from_anchor.ToTransform(), |
| std::move(callback)); |
| } |
| |
| void ArCoreImpl::CreatePlaneAttachedAnchor( |
| const mojom::XRNativeOriginInformation& native_origin_information, |
| const device::Pose& native_origin_from_anchor, |
| uint64_t plane_id, |
| CreateAnchorCallback callback) { |
| DVLOG(2) << __func__ << ": native_origin_information.which()=" |
| << static_cast<uint32_t>(native_origin_information.which()) |
| << ", plane_id=" << plane_id |
| << ", native_origin_from_anchor.position()=" |
| << native_origin_from_anchor.position().ToString() |
| << ", native_origin_from_anchor.orientation()=" |
| << native_origin_from_anchor.orientation().ToString(); |
| |
| create_plane_attached_anchor_requests_.emplace_back( |
| native_origin_information, native_origin_from_anchor.ToTransform(), |
| plane_id, std::move(callback)); |
| } |
| |
| void ArCoreImpl::ProcessAnchorCreationRequests( |
| const gfx::Transform& mojo_from_viewer, |
| const std::vector<mojom::XRInputSourceStatePtr>& input_state, |
| const base::TimeTicks& frame_time) { |
| // This is only called from ArCoreGl::ProcessFrame if the feature is enabled. |
| DCHECK(anchor_manager_); |
| |
| DVLOG(2) << __func__ << ": Processing free-floating anchor creation requests"; |
| ProcessAnchorCreationRequestsHelper( |
| mojo_from_viewer, input_state, &create_anchor_requests_, frame_time, |
| [this](const CreateAnchorRequest& create_anchor_request, |
| const gfx::Point3F& position, const gfx::Quaternion& orientation) { |
| return anchor_manager_->CreateAnchor( |
| device::mojom::Pose(orientation, position)); |
| }); |
| |
| // Plane detection and anchors are separate features, we can't assume that |
| // plane detection is enabled. If not, just skip this step. |
| if (!plane_manager_) |
| return; |
| |
| DVLOG(2) << __func__ |
| << ": Processing plane-attached anchor creation requests"; |
| ProcessAnchorCreationRequestsHelper( |
| mojo_from_viewer, input_state, &create_plane_attached_anchor_requests_, |
| frame_time, |
| [this](const CreatePlaneAttachedAnchorRequest& create_anchor_request, |
| const gfx::Point3F& position, const gfx::Quaternion& orientation) { |
| PlaneId plane_id = PlaneId(create_anchor_request.GetPlaneId()); |
| return anchor_manager_->CreateAnchor( |
| plane_manager_.get(), device::mojom::Pose(orientation, position), |
| plane_id); |
| }); |
| } |
| |
| template <typename T, typename FunctionType> |
| void ArCoreImpl::ProcessAnchorCreationRequestsHelper( |
| const gfx::Transform& mojo_from_viewer, |
| const std::vector<mojom::XRInputSourceStatePtr>& input_state, |
| std::vector<T>* anchor_creation_requests, |
| const base::TimeTicks& frame_time, |
| FunctionType&& create_anchor_function) { |
| DCHECK(anchor_creation_requests); |
| |
| DVLOG(3) << __func__ << ": pre-call anchor_creation_requests->size()=" |
| << anchor_creation_requests->size(); |
| |
| // If we are unable to create an anchor because position of the native origin |
| // is unknown, keep deferring it. On the other hand, if the anchor creation |
| // failed in ARCore SDK, notify blink - we are ensuring that anchor creation |
| // requests are processed when ARCore is in correct state so any failures |
| // coming from ARCore SDK are real failures we won't be able to recover from. |
| std::vector<T> postponed_requests; |
| for (auto& create_anchor : *anchor_creation_requests) { |
| auto anchor_creation_age = frame_time - create_anchor.GetRequestStartTime(); |
| |
| if (anchor_creation_age > kOutdatedAnchorCreationRequestThreshold) { |
| DVLOG(3) |
| << __func__ |
| << ": failing outdated anchor creation request, anchor_creation_age=" |
| << anchor_creation_age; |
| create_anchor.TakeCallback().Run( |
| device::mojom::CreateAnchorResult::FAILURE, 0); |
| continue; |
| } |
| |
| mojom::XRNativeOriginInformation native_origin_information = |
| create_anchor.GetNativeOriginInformation(); |
| |
| if (!NativeOriginExists(native_origin_information, input_state)) { |
| DVLOG(3) << __func__ |
| << ": failing anchor creation request, native origin does not " |
| "exist"; |
| create_anchor.TakeCallback().Run( |
| device::mojom::CreateAnchorResult::FAILURE, 0); |
| continue; |
| } |
| |
| absl::optional<gfx::Transform> maybe_mojo_from_native_origin = |
| GetMojoFromNativeOrigin(native_origin_information, mojo_from_viewer, |
| input_state); |
| |
| if (!maybe_mojo_from_native_origin) { |
| // We don't know where the native origin currently is (but we know it is |
| // still tracked), so let's postpone the request & try again later. |
| DVLOG(3) << __func__ |
| << ": postponing anchor creation request, native origin is not " |
| "currently localizable"; |
| postponed_requests.emplace_back(std::move(create_anchor)); |
| continue; |
| } |
| |
| absl::optional<device::Pose> mojo_from_anchor = |
| device::Pose::Create(*maybe_mojo_from_native_origin * |
| create_anchor.GetNativeOriginFromAnchor()); |
| |
| if (!mojo_from_anchor) { |
| // Fail the call now, failure to decompose is unlikely to resolve itself. |
| DVLOG(3) |
| << __func__ |
| << ": failing anchor creation request, unable to decompose a matrix"; |
| create_anchor.TakeCallback().Run( |
| device::mojom::CreateAnchorResult::FAILURE, 0); |
| continue; |
| } |
| |
| absl::optional<AnchorId> maybe_anchor_id = std::forward<FunctionType>( |
| create_anchor_function)(create_anchor, mojo_from_anchor->position(), |
| mojo_from_anchor->orientation()); |
| |
| if (!maybe_anchor_id) { |
| // Fail the call now, failure to create anchor in ARCore SDK is unlikely |
| // to resolve itself. |
| DVLOG(3) << __func__ |
| << ": failing anchor creation request, anchor creation " |
| "function did not return an anchor id"; |
| create_anchor.TakeCallback().Run( |
| device::mojom::CreateAnchorResult::FAILURE, 0); |
| continue; |
| } |
| |
| DVLOG(3) << __func__ << ": anchor creation request succeeded, time taken: " |
| << anchor_creation_age; |
| create_anchor.TakeCallback().Run(device::mojom::CreateAnchorResult::SUCCESS, |
| maybe_anchor_id->GetUnsafeValue()); |
| } |
| |
| // Return the postponed requests - all other requests should have their |
| // status already reported to blink at this point: |
| anchor_creation_requests->swap(postponed_requests); |
| DVLOG(3) << __func__ << ": post-call anchor_creation_requests->size()=" |
| << anchor_creation_requests->size(); |
| } |
| |
| void ArCoreImpl::DetachAnchor(uint64_t anchor_id) { |
| DCHECK(anchor_manager_); |
| anchor_manager_->DetachAnchor(AnchorId(anchor_id)); |
| } |
| |
| mojom::XRDepthDataPtr ArCoreImpl::GetDepthData() { |
| DVLOG(3) << __func__; |
| |
| internal::ScopedArCoreObject<ArImage*> ar_image; |
| ArStatus status = ArFrame_acquireDepthImage( |
| arcore_session_.get(), arcore_frame_.get(), |
| internal::ScopedArCoreObject<ArImage*>::Receiver(ar_image).get()); |
| |
| if (status != AR_SUCCESS) { |
| DVLOG(2) << __func__ |
| << ": ArFrame_acquireDepthImage failed, status=" << status; |
| return nullptr; |
| } |
| |
| int64_t timestamp_ns; |
| ArImage_getTimestamp(arcore_session_.get(), ar_image.get(), ×tamp_ns); |
| base::TimeDelta time_delta = base::Nanoseconds(timestamp_ns); |
| DVLOG(3) << __func__ << ": depth image time_delta=" << time_delta; |
| |
| // The image returned from ArFrame_acquireDepthImage() is documented to have |
| // a single 16-bit plane at index 0. The ArImage format is documented to be |
| // AR_IMAGE_FORMAT_DEPTH16 (equivalent to ImageFormat.DEPTH16). There should |
| // be no need to validate this in non-debug builds. |
| // https://developers.google.com/ar/reference/c/group/ar-frame#arframe_acquiredepthimage |
| // https://developer.android.com/reference/android/graphics/ImageFormat#DEPTH16 |
| |
| ArImageFormat image_format; |
| ArImage_getFormat(arcore_session_.get(), ar_image.get(), &image_format); |
| |
| CHECK_EQ(image_format, AR_IMAGE_FORMAT_DEPTH16) |
| << "Depth image format must be AR_IMAGE_FORMAT_DEPTH16, found: " |
| << image_format; |
| |
| int32_t num_planes; |
| ArImage_getNumberOfPlanes(arcore_session_.get(), ar_image.get(), &num_planes); |
| |
| CHECK_EQ(num_planes, 1) << "Depth image must have 1 plane, found: " |
| << num_planes; |
| |
| if (time_delta > previous_depth_data_time_) { |
| // The depth data is more recent than what was previously returned, we need |
| // to send the latest information back: |
| mojom::XRDepthDataUpdatedPtr result = mojom::XRDepthDataUpdated::New(); |
| |
| result->time_delta = time_delta; |
| |
| int32_t width = 0, height = 0; |
| ArImage_getWidth(arcore_session_.get(), ar_image.get(), &width); |
| ArImage_getHeight(arcore_session_.get(), ar_image.get(), &height); |
| |
| DVLOG(3) << __func__ << ": depth image dimensions=" << width << "x" |
| << height; |
| |
| // Depth image is defined as a width by height array of 2-byte elements: |
| auto checked_buffer_size = base::CheckMul<size_t>(2, width, height); |
| |
| size_t buffer_size; |
| if (!checked_buffer_size.AssignIfValid(&buffer_size)) { |
| DVLOG(2) << __func__ |
| << ": overflow in 2 * width * height expression, returning null " |
| "depth data"; |
| return nullptr; |
| } |
| |
| // Log a histogram w/ the number of entries in the depth buffer to make sure |
| // we have a way of measuring the impact of the decision to suppress |
| // too-high-resolution depth buffers. Assuming various common aspect ratios |
| // & fixing the width to 160 pixels, the total number of pixels varies from |
| // ~6000 to ~20000, and w/ the threshold below set to 43200 pixels, the |
| // custom count from 5000 to 55000 with bucket size of 1000 should give us |
| // sufficient granularity of data. |
| UMA_HISTOGRAM_CUSTOM_COUNTS("XR.ARCore.DepthBufferSizeInPixels", |
| buffer_size / 2, 5000, 55000, 50); |
| |
| TRACE_COUNTER2(TRACE_DISABLED_BY_DEFAULT("xr.debug"), |
| "Depth buffer resolution (in pixels)", "width", width, |
| "height", height); |
| |
| if (buffer_size / 2 > 240 * 180) { |
| // ARCore should report depth data buffers w/ resolution in the ballpark |
| // of 160x120. If the number of data entries is higher than 240 * 180 |
| // (=43200), we should not return it. The threshold was picked by |
| // multiplying each expected dimension (160x120) by 1.5. Note that this |
| // translates to 2.25 increase in allowed number of pixels compared to |
| // the currently expected resolution. |
| return nullptr; |
| } |
| |
| mojo_base::BigBuffer pixels(buffer_size); |
| |
| // Interpret BigBuffer's data as a width by height array of uint16_t's and |
| // copy image data into it: |
| CopyArCoreImage( |
| arcore_session_.get(), ar_image.get(), 0, |
| base::span<uint16_t>(reinterpret_cast<uint16_t*>(pixels.data()), |
| pixels.size() / 2), |
| width, height); |
| |
| result->pixel_data = std::move(pixels); |
| // Transform needed to consume the data: |
| result->norm_texture_from_norm_view = GetDepthUvFromScreenUvTransform(); |
| result->size = gfx::Size(width, height); |
| result->raw_value_to_meters = |
| 1.0 / 1000.0; // DepthInMillimeters * 1/1000 = DepthInMeters |
| |
| DVLOG(3) << __func__ << ": norm_texture_from_norm_view=\n" |
| << result->norm_texture_from_norm_view.ToString(); |
| |
| previous_depth_data_time_ = time_delta; |
| |
| return mojom::XRDepthData::NewUpdatedDepthData(std::move(result)); |
| } |
| |
| // We don't have more recent data than what was already returned, inform the |
| // caller that previously returned data is still valid: |
| return mojom::XRDepthData::NewDataStillValid( |
| mojom::XRDepthDataStillValid::New()); |
| } |
| |
| bool ArCoreImpl::IsOnGlThread() const { |
| return gl_thread_task_runner_->BelongsToCurrentThread(); |
| } |
| |
| std::unique_ptr<ArCore> ArCoreImplFactory::Create() { |
| return std::make_unique<ArCoreImpl>(); |
| } |
| |
| CreateAnchorRequest::CreateAnchorRequest( |
| const mojom::XRNativeOriginInformation& native_origin_information, |
| const gfx::Transform& native_origin_from_anchor, |
| ArCore::CreateAnchorCallback callback) |
| : native_origin_information_(native_origin_information), |
| native_origin_from_anchor_(native_origin_from_anchor), |
| request_start_time_(base::TimeTicks::Now()), |
| callback_(std::move(callback)) {} |
| CreateAnchorRequest::CreateAnchorRequest(CreateAnchorRequest&& other) = default; |
| CreateAnchorRequest::~CreateAnchorRequest() = default; |
| |
| mojom::XRNativeOriginInformation |
| CreateAnchorRequest::GetNativeOriginInformation() const { |
| return native_origin_information_; |
| } |
| |
| gfx::Transform CreateAnchorRequest::GetNativeOriginFromAnchor() const { |
| return native_origin_from_anchor_; |
| } |
| |
| base::TimeTicks CreateAnchorRequest::GetRequestStartTime() const { |
| return request_start_time_; |
| } |
| |
| ArCore::CreateAnchorCallback CreateAnchorRequest::TakeCallback() { |
| return std::move(callback_); |
| } |
| |
| CreatePlaneAttachedAnchorRequest::CreatePlaneAttachedAnchorRequest( |
| const mojom::XRNativeOriginInformation& native_origin_information, |
| const gfx::Transform& native_origin_from_anchor, |
| uint64_t plane_id, |
| ArCore::CreateAnchorCallback callback) |
| : native_origin_information_(native_origin_information), |
| native_origin_from_anchor_(native_origin_from_anchor), |
| plane_id_(plane_id), |
| request_start_time_(base::TimeTicks::Now()), |
| callback_(std::move(callback)) {} |
| CreatePlaneAttachedAnchorRequest::CreatePlaneAttachedAnchorRequest( |
| CreatePlaneAttachedAnchorRequest&& other) = default; |
| CreatePlaneAttachedAnchorRequest::~CreatePlaneAttachedAnchorRequest() = default; |
| |
| mojom::XRNativeOriginInformation |
| CreatePlaneAttachedAnchorRequest::GetNativeOriginInformation() const { |
| return native_origin_information_; |
| } |
| |
| uint64_t CreatePlaneAttachedAnchorRequest::GetPlaneId() const { |
| return plane_id_; |
| } |
| |
| gfx::Transform CreatePlaneAttachedAnchorRequest::GetNativeOriginFromAnchor() |
| const { |
| return native_origin_from_anchor_; |
| } |
| |
| base::TimeTicks CreatePlaneAttachedAnchorRequest::GetRequestStartTime() const { |
| return request_start_time_; |
| } |
| |
| ArCore::CreateAnchorCallback CreatePlaneAttachedAnchorRequest::TakeCallback() { |
| return std::move(callback_); |
| } |
| |
| } // namespace device |