基于位置的增强现实体验 location-ar-experience

该技能用于设计基于地理位置的增强现实体验,整合GPS、计算机视觉和空间计算技术,实现实世界交互覆盖。关键词:增强现实、位置服务、GPS、计算机视觉、AR体验设计、移动AR开发。

移动开发 0 次安装 0 次浏览 更新于 3/7/2026

名称: 基于位置的增强现实体验 描述: 设计基于地理位置的增强现实体验,具有地理空间锚定、GPS集成和实世界交互覆盖。 许可证: MIT

基于位置的增强现实体验

此技能为设计锚定到实世界位置的增强现实体验提供指导,结合了GPS、计算机视觉和空间计算。

核心能力

  • 地理空间锚定: GPS、地理围栏、坐标系
  • 视觉定位: SLAM、图像识别、云锚点
  • 内容放置: 世界尺度AR、遮挡、持久性
  • 移动AR平台: ARKit、ARCore、WebXR

基于位置的AR基础知识

AR体验类型

┌─────────────────────────────────────────────────────────────────────┐
│                    基于位置的AR频谱                                  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  GPS-Only          混合              Vision-Based                 │
│  ┌─────────┐      ┌─────────┐         ┌─────────┐                  │
│  │ ~10m    │      │ ~1m     │         │ ~1cm    │                  │
│  │精度     │      │精度     │         │精度     │                  │
│  └─────────┘      └─────────┘         └─────────┘                  │
│      │                │                    │                        │
│      ▼                ▼                    ▼                        │
│  城市尺度            建筑尺度             房间尺度                  │
│  导航游戏            POI覆盖              精确安装                  │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

按体验类型的精度要求

体验类型 所需精度 定位方法
城市导航 5-15米 GPS
POI发现 3-5米 GPS + Wi-Fi
建筑入口 1-2米 GPS + 视觉
室内导航 0.5-1米 视觉 + 信标
物体放置 1-10厘米 纯视觉SLAM

地理空间系统

坐标系

from dataclasses import dataclass
import math

@dataclass
class GeoCoordinate:
    """WGS84坐标"""
    latitude: float   # -90 到 90
    longitude: float  # -180 到 180
    altitude: float = 0  # 海拔高度(米)

    def distance_to(self, other: 'GeoCoordinate') -> float:
        """Haversine距离(米)"""
        R = 6371000  # 地球半径(米)

        lat1, lat2 = math.radians(self.latitude), math.radians(other.latitude)
        dlat = math.radians(other.latitude - self.latitude)
        dlon = math.radians(other.longitude - self.longitude)

        a = (math.sin(dlat/2)**2 +
             math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2)
        c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))

        return R * c

    def bearing_to(self, other: 'GeoCoordinate') -> float:
        """方位角(度,0-360)"""
        lat1 = math.radians(self.latitude)
        lat2 = math.radians(other.latitude)
        dlon = math.radians(other.longitude - self.longitude)

        x = math.sin(dlon) * math.cos(lat2)
        y = (math.cos(lat1) * math.sin(lat2) -
             math.sin(lat1) * math.cos(lat2) * math.cos(dlon))

        bearing = math.degrees(math.atan2(x, y))
        return (bearing + 360) % 360


@dataclass
class ARWorldCoordinate:
    """本地AR坐标(从原点起的米)"""
    x: float  # 东
    y: float  # 上
    z: float  # 北


def geo_to_local(geo: GeoCoordinate, origin: GeoCoordinate) -> ARWorldCoordinate:
    """将地理坐标转换为本地AR坐标"""
    # 简化平面投影(适用于小区域)
    lat_scale = 111320  # 每度纬度米数
    lon_scale = 111320 * math.cos(math.radians(origin.latitude))

    x = (geo.longitude - origin.longitude) * lon_scale  # 东
    z = (geo.latitude - origin.latitude) * lat_scale    # 北
    y = geo.altitude - origin.altitude                   # 上

    return ARWorldCoordinate(x, y, z)

地理围栏

from enum import Enum
from typing import List, Callable

class GeofenceShape(Enum):
    CIRCLE = "circle"
    POLYGON = "polygon"

class Geofence:
    """定义AR内容的触发区域"""

    def __init__(self, id: str, center: GeoCoordinate, radius: float):
        self.id = id
        self.center = center
        self.radius = radius  # 米
        self.shape = GeofenceShape.CIRCLE
        self.on_enter: List[Callable] = []
        self.on_exit: List[Callable] = []
        self.on_dwell: List[Callable] = []
        self.dwell_time = 30  # 秒

    def contains(self, point: GeoCoordinate) -> bool:
        """检查点是否在地理围栏内"""
        return self.center.distance_to(point) <= self.radius

    def add_enter_handler(self, callback: Callable):
        self.on_enter.append(callback)


class GeofenceManager:
    """管理多个地理围栏"""

    def __init__(self):
        self.fences: dict[str, Geofence] = {}
        self.active_fences: set[str] = set()
        self.entry_times: dict[str, float] = {}

    def add_fence(self, fence: Geofence):
        self.fences[fence.id] = fence

    def update_position(self, position: GeoCoordinate, timestamp: float):
        """检查地理围栏并触发回调"""
        currently_inside = set()

        for fence_id, fence in self.fences.items():
            if fence.contains(position):
                currently_inside.add(fence_id)

                # 触发进入事件
                if fence_id not in self.active_fences:
                    self.entry_times[fence_id] = timestamp
                    for callback in fence.on_enter:
                        callback(fence, position)

                # 检查停留时间
                elif timestamp - self.entry_times[fence_id] >= fence.dwell_time:
                    for callback in fence.on_dwell:
                        callback(fence, position)

        # 触发退出事件
        for fence_id in self.active_fences - currently_inside:
            fence = self.fences[fence_id]
            for callback in fence.on_exit:
                callback(fence, position)
            del self.entry_times[fence_id]

        self.active_fences = currently_inside

视觉定位

ARCore Geospatial API 模式

// Android/Kotlin 使用 ARCore Geospatial API
class GeospatialManager(private val session: Session) {

    fun placeAnchorAtLocation(
        latitude: Double,
        longitude: Double,
        altitude: Double,
        heading: Float
    ): Anchor? {
        val earth = session.earth ?: return null

        // 检查跟踪质量
        if (earth.trackingState != TrackingState.TRACKING) {
            return null
        }

        // 检查水平精度
        val pose = earth.cameraGeospatialPose
        if (pose.horizontalAccuracy > 10) {  // 米
            return null  // 精度不足
        }

        // 创建地理空间锚点
        val quaternion = Quaternion.axisAngle(
            Vector3(0f, 1f, 0f),
            Math.toRadians(heading.toDouble()).toFloat()
        )

        return earth.createAnchor(
            latitude, longitude, altitude,
            quaternion.x, quaternion.y, quaternion.z, quaternion.w
        )
    }

    fun resolveTerrainAnchor(
        latitude: Double,
        longitude: Double,
        heading: Float,
        callback: (Anchor?) -> Unit
    ) {
        val earth = session.earth ?: return callback(null)

        // 地形锚点自动确定高度
        val future = earth.resolveAnchorOnTerrainAsync(
            latitude, longitude,
            0.0,  // 地形上高度
            /* quaternion */ 0f, 0f, 0f, 1f,
            { anchor, state ->
                when (state) {
                    Anchor.TerrainAnchorState.SUCCESS -> callback(anchor)
                    else -> callback(null)
                }
            }
        )
    }
}

云锚点用于持久性

// iOS/Swift 使用 ARKit Cloud Anchors
class CloudAnchorManager {
    var arView: ARView
    var anchorStore: [String: ARAnchor] = [:]

    func saveAnchor(_ anchor: ARAnchor, completion: @escaping (String?) -> Void) {
        // 上传锚点到云服务
        let anchorData = AnchorData(
            transform: anchor.transform,
            identifier: anchor.identifier.uuidString
        )

        CloudService.shared.uploadAnchor(anchorData) { cloudId in
            if let id = cloudId {
                self.anchorStore[id] = anchor
            }
            completion(cloudId)
        }
    }

    func resolveAnchor(cloudId: String, completion: @escaping (ARAnchor?) -> Void) {
        CloudService.shared.downloadAnchor(cloudId) { anchorData in
            guard let data = anchorData else {
                completion(nil)
                return
            }

            let anchor = ARAnchor(transform: data.transform)
            self.arView.session.add(anchor: anchor)
            completion(anchor)
        }
    }
}

内容管理

AR内容数据模型

from dataclasses import dataclass, field
from typing import Optional, List
from enum import Enum

class ContentType(Enum):
    MODEL_3D = "model_3d"
    IMAGE = "image"
    VIDEO = "video"
    AUDIO = "audio"
    TEXT = "text"
    INTERACTIVE = "interactive"

@dataclass
class ARContent:
    """锚定到位置的AR内容"""
    id: str
    content_type: ContentType
    location: GeoCoordinate

    # 资产引用
    asset_url: str
    thumbnail_url: Optional[str] = None

    # 空间属性
    scale: float = 1.0
    rotation_y: float = 0.0  # 方位角(度)
    offset_y: float = 0.0    # 地面高度偏移

    # 可见性规则
    min_distance: float = 0.0
    max_distance: float = 100.0
    visible_hours: Optional[tuple[int, int]] = None  # 开始、结束小时

    # 交互
    interactive: bool = False
    trigger_radius: float = 5.0

    # 元数据
    title: str = ""
    description: str = ""
    tags: List[str] = field(default_factory=list)


@dataclass
class ARExperience:
    """形成体验的AR内容集合"""
    id: str
    name: str
    description: str

    # 边界
    center: GeoCoordinate
    radius: float  # 米

    # 内容
    contents: List[ARContent] = field(default_factory=list)

    # 体验流程
    ordered: bool = False  # 顺序 vs 自由探索
    start_content_id: Optional[str] = None

    def get_visible_content(
        self,
        user_position: GeoCoordinate,
        current_hour: int
    ) -> List[ARContent]:
        """从用户位置过滤可见内容"""
        visible = []

        for content in self.contents:
            distance = user_position.distance_to(content.location)

            # 距离检查
            if distance < content.min_distance or distance > content.max_distance:
                continue

            # 时间检查
            if content.visible_hours:
                start, end = content.visible_hours
                if not (start <= current_hour < end):
                    continue

            visible.append(content)

        return visible

空间数据加载策略

class SpatialContentLoader:
    """高效加载用户附近内容"""

    def __init__(self, api_client):
        self.api = api_client
        self.cache: dict[str, ARContent] = {}
        self.loaded_tiles: set[str] = set()
        self.tile_size = 0.001  # 约111米在赤道

    def get_tile_key(self, coord: GeoCoordinate) -> str:
        """获取坐标的图块标识符"""
        lat_tile = int(coord.latitude / self.tile_size)
        lon_tile = int(coord.longitude / self.tile_size)
        return f"{lat_tile}:{lon_tile}"

    async def update_position(self, position: GeoCoordinate):
        """加载位置和周围图块的内容"""
        # 获取用户周围的3x3图块网格
        tiles_needed = set()
        for dlat in [-1, 0, 1]:
            for dlon in [-1, 0, 1]:
                adjusted = GeoCoordinate(
                    position.latitude + dlat * self.tile_size,
                    position.longitude + dlon * self.tile_size
                )
                tiles_needed.add(self.get_tile_key(adjusted))

        # 加载新图块
        new_tiles = tiles_needed - self.loaded_tiles
        for tile in new_tiles:
            content = await self.api.get_content_for_tile(tile)
            for item in content:
                self.cache[item.id] = item
            self.loaded_tiles.add(tile)

        # 卸载远距离图块(仅保留5x5网格)
        # ... 清理逻辑

    def get_nearby_content(
        self,
        position: GeoCoordinate,
        radius: float
    ) -> List[ARContent]:
        """获取缓存中半径内的内容"""
        return [
            content for content in self.cache.values()
            if position.distance_to(content.location) <= radius
        ]

用户体验模式

路径导航

class ARWayfinder:
    """引导用户到AR内容"""

    def __init__(self):
        self.current_target: Optional[ARContent] = None
        self.path: List[GeoCoordinate] = []

    def set_target(self, content: ARContent, user_position: GeoCoordinate):
        """设置导航目标"""
        self.current_target = content
        self.path = self._calculate_path(user_position, content.location)

    def get_direction_indicator(
        self,
        user_position: GeoCoordinate,
        user_heading: float
    ) -> dict:
        """获取AR方向指示器数据"""
        if not self.current_target:
            return None

        target_bearing = user_position.bearing_to(self.current_target.location)
        relative_bearing = (target_bearing - user_heading + 360) % 360

        distance = user_position.distance_to(self.current_target.location)

        return {
            'type': 'direction_arrow',
            'bearing': relative_bearing,  # 0 = 正前方
            'distance': distance,
            'distance_text': self._format_distance(distance),
            'in_view': -30 <= relative_bearing <= 30 or relative_bearing >= 330
        }

    def _format_distance(self, meters: float) -> str:
        if meters < 100:
            return f"{int(meters)}m"
        elif meters < 1000:
            return f"{int(meters/10)*10}m"
        else:
            return f"{meters/1000:.1f}km"

内容发现

用户接近内容:

100米外: [仅地图指示器]
            "附近有5个AR体验"

50米外:   [浮动标签出现]
            "历史建筑 - 50米"
                    ▼

20米外:   [标签增大,显示缩略图]
            ┌──────────────┐
            │   [图像]     │
            │ 历史         │
            │ 建筑         │
            │    20米 →    │
            └──────────────┘

5米外:    [完整AR内容触发]
            3D模型出现在位置
            信息面板可用

交互区域

class InteractionZone:
    """定义用户如何与AR内容交互"""

    def __init__(self, content: ARContent):
        self.content = content

        # 区域半径(米)
        self.awareness_radius = 100  # 在地图上显示
        self.preview_radius = 50     # 显示浮动标签
        self.activation_radius = 20  # 显示完整AR
        self.interaction_radius = 5  # 可交互

    def get_interaction_state(
        self,
        user_position: GeoCoordinate
    ) -> str:
        """确定当前交互状态"""
        distance = user_position.distance_to(self.content.location)

        if distance <= self.interaction_radius:
            return "interactive"
        elif distance <= self.activation_radius:
            return "active"
        elif distance <= self.preview_radius:
            return "preview"
        elif distance <= self.awareness_radius:
            return "aware"
        else:
            return "hidden"

性能优化

LOD(细节级别)

class LODManager:
    """基于距离管理内容细节"""

    LOD_LEVELS = {
        'full': {'max_distance': 10, 'quality': 'high'},
        'medium': {'max_distance': 30, 'quality': 'medium'},
        'low': {'max_distance': 100, 'quality': 'low'},
        'billboard': {'max_distance': float('inf'), 'quality': 'billboard'}
    }

    def get_lod_for_distance(self, distance: float) -> str:
        for level, config in self.LOD_LEVELS.items():
            if distance <= config['max_distance']:
                return level
        return 'billboard'

    def get_asset_url(self, content: ARContent, lod: str) -> str:
        """获取适合LOD级别的资产URL"""
        base_url = content.asset_url.rsplit('.', 1)[0]

        if lod == 'billboard':
            return content.thumbnail_url
        elif lod == 'low':
            return f"{base_url}_low.glb"
        elif lod == 'medium':
            return f"{base_url}_med.glb"
        else:
            return content.asset_url

电池和数据节省

class LocationOptimizer:
    """优化位置更新以节省电池寿命"""

    def __init__(self):
        self.high_accuracy_mode = False
        self.last_position: Optional[GeoCoordinate] = None
        self.movement_threshold = 5  # 米

    def should_update_ar(self, new_position: GeoCoordinate) -> bool:
        """确定是否需要更新AR场景"""
        if not self.last_position:
            self.last_position = new_position
            return True

        distance_moved = self.last_position.distance_to(new_position)

        if distance_moved > self.movement_threshold:
            self.last_position = new_position
            return True

        return False

    def get_location_config(self, near_content: bool) -> dict:
        """基于上下文获取GPS配置"""
        if near_content:
            return {
                'accuracy': 'high',
                'interval_ms': 1000,
                'distance_filter': 1
            }
        else:
            return {
                'accuracy': 'balanced',
                'interval_ms': 5000,
                'distance_filter': 10
            }

最佳实践

设计指南

  1. 尊重物理空间: 不要放置AR内容阻塞路径或位于危险位置
  2. 考虑照明: 户外AR需要处理强光和阴影
  3. 提供备用方案: 当AR不可行时显示2D地图
  4. 明确可用性: 用户应知道什么是可交互的
  5. 优雅降级: 适应不同的GPS精度

测试考虑

  • 用真实GPS测试,不只是模拟坐标
  • 在不同天气和光照条件下测试
  • 测试长时间会话的电池消耗
  • 测试网络连接差的情况

参考资料

  • references/arcore-geospatial.md - ARCore Geospatial API 指南
  • references/arkit-location.md - ARKit 位置锚定
  • references/coordinate-systems.md - 地理空间坐标转换