ImmersiveSpace中用DragGesture跨垂直墙移动锚实体的问题
解决ImmersiveSpace中拖拽相框实体切换垂直墙的问题
原代码核心问题
- 初始锚点
AnchorEntity使用.once追踪模式,绑定到首次检测到的墙面后无法切换到其他墙面 - 直接修改锚点位置仅能在初始墙面的局部空间内移动,无法实现跨墙面的重新锚定
- 缺少拖拽过程中的墙面检测逻辑,无法识别新的垂直墙面并调整实体位置
修正后的实现代码
import SwiftUI import RealityKit import ARKit struct ImmersiveView: View { // 相框实体 @State private var frameEntity: ModelEntity? // 当前绑定的墙面锚点 @State private var currentWallAnchor: AnchorEntity? // 拖拽起始位置(世界空间) @State private var dragStartWorldPosition: SIMD3<Float>? // 拖拽起始时相框在墙面上的局部位置 @State private var dragStartLocalPosition: SIMD2<Float>? // 墙面边缘限制(避免放置在墙角) private let wallEdgeMargin: Float = 0.2 var body: some View { RealityView { content, attachments in // 加载相框模型 async let loadFrame = ModelEntity(named: "Photo") guard let frame = try? await loadFrame else { return } // 设置碰撞和输入目标 frame.generateCollisionShapes(recursive: true) frame.components.set(InputTargetComponent()) // 初始放置:找到用户前方的垂直墙面并放置相框 placeFrameOnInitialWall(frame: frame, content: content) frameEntity = frame content.add(frame) } .gesture( DragGesture() .onChanged { value in guard let frame = frameEntity else { return } // 首次触发拖拽时记录起始位置 if dragStartWorldPosition == nil { dragStartWorldPosition = frame.position // 获取当前墙面的局部位置(如果已绑定墙面) if let anchor = currentWallAnchor { dragStartLocalPosition = frame.position.convert(from: .world, to: anchor).xy } } // 执行射线检测,寻找垂直墙面 guard let hitResult = raycastForVerticalWall(from: value) else { return } // 处理墙面切换或同墙面移动 handleDragOnWall(hitResult: hitResult, frame: frame) } .onEnded { _ in // 重置拖拽状态 dragStartWorldPosition = nil dragStartLocalPosition = nil } ) } // 初始放置相框到用户前方墙面 private func placeFrameOnInitialWall(frame: ModelEntity, content: RealityViewContent) { // 从用户视角向前发射射线 guard let cameraTransform = ARView.cameraTransform else { return } let rayOrigin = cameraTransform.translation let rayDirection = cameraTransform.forwardVector let query = ARRaycastQuery(origin: rayOrigin, direction: rayDirection, allowing: .existingPlaneInfinite, alignment: .vertical) guard let hitResult = ARView.session.raycast(query).first else { return } // 创建墙面锚点 let wallAnchor = AnchorEntity(raycastResult: hitResult) currentWallAnchor = wallAnchor content.add(wallAnchor) // 将相框放置在墙面中心位置(避开边缘) let localPosition = SIMD3<Float>(0, 0, -0.01) // 稍微突出墙面 frame.setPosition(localPosition, relativeTo: wallAnchor) } // 射线检测垂直墙面 private func raycastForVerticalWall(from gestureValue: DragGesture.Value) -> ARRaycastResult? { // 将手势位置转换为ARView中的射线 guard let arView = ARView else { return nil } let screenPos = gestureValue.location let query = arView.raycastQuery(from: screenPos, allowing: .existingPlaneInfinite, alignment: .vertical) return arView.session.raycast(query)?.first } // 处理拖拽时的墙面交互 private func handleDragOnWall(hitResult: ARRaycastResult, frame: ModelEntity) { // 获取墙面锚点(复用已有锚点或创建新的) let wallAnchor: AnchorEntity if let currentAnchor = currentWallAnchor, currentAnchor.anchor is ARPlaneAnchor, (currentAnchor.anchor as! ARPlaneAnchor).identifier == hitResult.anchorIdentifier { // 同一块墙面,复用锚点 wallAnchor = currentAnchor } else { // 新墙面,创建新锚点 wallAnchor = AnchorEntity(raycastResult: hitResult) currentWallAnchor = wallAnchor // 添加到场景 if let content = frame.scene as? RealityViewContent { content.add(wallAnchor) } } // 计算墙面局部位置,避开边缘 let wallPlane = hitResult.anchor as! ARPlaneAnchor let hitLocalPos = hitResult.worldTransform.translation.convert(from: .world, to: wallAnchor) // 限制位置在墙面边缘内 let clampedX = clamp(hitLocalPos.x, -wallPlane.extent.x/2 + wallEdgeMargin, wallPlane.extent.x/2 - wallEdgeMargin) let clampedY = clamp(hitLocalPos.y, -wallPlane.extent.y/2 + wallEdgeMargin, wallPlane.extent.y/2 - wallEdgeMargin) // 如果是同墙面拖拽,保留Y轴的拖拽偏移 if let startLocalPos = dragStartLocalPosition, currentWallAnchor?.anchor?.identifier == hitResult.anchorIdentifier { let deltaY = hitLocalPos.y - startLocalPos.y let targetY = startLocalPos.y + deltaY frame.setPosition(SIMD3(clampedX, targetY, -0.01), relativeTo: wallAnchor) } else { // 新墙面直接放置在射线命中位置(已限制边缘) frame.setPosition(SIMD3(clampedX, clampedY, -0.01), relativeTo: wallAnchor) } } // 辅助函数:限制值在范围内 private func clamp<T: Comparable>(_ value: T, _ min: T, _ max: T) -> T { return Swift.max(min, Swift.min(value, max)) } // 获取当前ARView的便捷属性 private var ARView: ARView? { return (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.windows.first?.rootViewController?.view as? ARView } }
关键实现说明
- 动态墙面锚定:不再使用固定的
.once锚点,而是在拖拽过程中通过射线检测实时识别垂直墙面,创建或复用墙面锚点 - 同墙面拖拽:记录拖拽起始时的墙面局部位置,保持Y轴的拖拽偏移,实现平滑的同墙面移动
- 墙角限制:通过
wallEdgeMargin设置边缘距离,使用clamp函数将相框位置限制在墙面有效区域内,避免放置在墙角 - 初始放置逻辑:进入空间时自动检测用户前方的垂直墙面,将相框放置在中心位置
内容的提问来源于stack exchange,提问作者user2801974
相关产品推荐
相关产品推荐

