名称: 基于位置的增强现实体验 描述: 设计基于地理位置的增强现实体验,具有地理空间锚定、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
}
最佳实践
设计指南
- 尊重物理空间: 不要放置AR内容阻塞路径或位于危险位置
- 考虑照明: 户外AR需要处理强光和阴影
- 提供备用方案: 当AR不可行时显示2D地图
- 明确可用性: 用户应知道什么是可交互的
- 优雅降级: 适应不同的GPS精度
测试考虑
- 用真实GPS测试,不只是模拟坐标
- 在不同天气和光照条件下测试
- 测试长时间会话的电池消耗
- 测试网络连接差的情况
参考资料
references/arcore-geospatial.md- ARCore Geospatial API 指南references/arkit-location.md- ARKit 位置锚定references/coordinate-systems.md- 地理空间坐标转换