You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ImmersiveSpace中用DragGesture跨垂直墙移动锚实体的问题

解决ImmersiveSpace中拖拽相框实体切换垂直墙的问题

原代码核心问题

  1. 初始锚点AnchorEntity使用.once追踪模式,绑定到首次检测到的墙面后无法切换到其他墙面
  2. 直接修改锚点位置仅能在初始墙面的局部空间内移动,无法实现跨墙面的重新锚定
  3. 缺少拖拽过程中的墙面检测逻辑,无法识别新的垂直墙面并调整实体位置

修正后的实现代码

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
    }
}

关键实现说明

  1. 动态墙面锚定:不再使用固定的.once锚点,而是在拖拽过程中通过射线检测实时识别垂直墙面,创建或复用墙面锚点
  2. 同墙面拖拽:记录拖拽起始时的墙面局部位置,保持Y轴的拖拽偏移,实现平滑的同墙面移动
  3. 墙角限制:通过wallEdgeMargin设置边缘距离,使用clamp函数将相框位置限制在墙面有效区域内,避免放置在墙角
  4. 初始放置逻辑:进入空间时自动检测用户前方的垂直墙面,将相框放置在中心位置

内容的提问来源于stack exchange,提问作者user2801974

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 16:39:51