[go: up one dir, main page]

version update in Vision Pro

Hi, I'm developing an app for Vision Pro using Xcode, while updating the latest update, things that worked in my app suddenly didn't. in my app flow I'm tapping spheres to get their positions, from some reason I get an offset from where I tap to where a marker on that position is showing up. here's the part of code that does that, and a part that is responsible for an alignment that happens afterwards:

func loadMainScene(at position: SIMD3<Float>) async { guard let content = self.content else { return }

    do {
        let rootEntity = try await Entity(named: "surgery 16.09", in: realityKitContentBundle)
        rootEntity.scale = SIMD3<Float>(repeating: 0.5)
        rootEntity.generateCollisionShapes(recursive: true)
        self.modelRootEntity = rootEntity
        let bounds = rootEntity.visualBounds(relativeTo: nil)
        print("📏 Model bounds: center=\(bounds.center), extents=\(bounds.extents)")
        let pivotEntity = Entity()
        pivotEntity.addChild(rootEntity)
        self.pivotEntity = pivotEntity
        let modelAnchor = AnchorEntity(world: [1, 1.3, -0.8])
        modelAnchor.addChild(pivotEntity)
        content.add(modelAnchor)
        updateModelOpacity(0.5)
        self.modelAnchor = modelAnchor
        rootEntity.visit { entity in
            print("👀 Entity in model: \(entity.name)")
            if entity.name.lowercased().hasPrefix("focus") {
                entity.generateCollisionShapes(recursive: true)
                entity.components.set(InputTargetComponent())
                print("🎯 Made tappable: \(entity.name)")
            }
        }
        print("✅ Model loaded with collisions")
        guard let sphere = placementSphere else { return }
        let sphereWorldXform = sphere.transformMatrix(relativeTo: nil)
        var newXform = sphereWorldXform
        newXform.columns.3.y += 0.1   // move up by 20 cm
        let gridAnchor = AnchorEntity(world: newXform)
        self.gridAnchor = gridAnchor
        content.add(gridAnchor)
        let baseScene = try await Entity(named: "Scene", in: realityKitContentBundle)
        let gridSizeX = 18
        let gridSizeY = 10
        let gridSizeZ = 10
        let spacing: Float = 0.05
        let startX: Float = -Float(gridSizeX - 1) * spacing * 0.5 + 0.3
        let startY: Float = -Float(gridSizeY - 1) * spacing * 0.5 - 0.1
        let startZ: Float = -Float(gridSizeZ - 1) * spacing * 0.5
        for i in 0..<gridSizeX {
            for j in 0..<gridSizeY {
                for k in 0..<gridSizeZ {
                    if j < 2 || j > gridSizeY - 5 { continue } // remove 2 bottom, 4 top
                    let cell = baseScene.clone(recursive: true)
                    cell.name = "Sphere"
                    cell.scale = .one * 0.02
                    cell.position = [
                        startX + Float(i) * spacing,
                        startY + Float(j) * spacing,
                        startZ + Float(k) * spacing
                    ]
                    cell.generateCollisionShapes(recursive: true)
                    gridCells.append(cell)
                    gridAnchor.addChild(cell)
                }
            }
        }
        content.add(gridAnchor)
        print("✅ Grid added")
    } catch {
        print("❌ Failed to load: \(error)")
    }
}

private func handleModelOrGridTap(_ tappedEntity: Entity) { guard let modelRootEntity = modelRootEntity else { return }

    let localPosition = tappedEntity.position(relativeTo: modelRootEntity)
    let worldPosition = tappedEntity.position(relativeTo: nil)

    switch tapStep {
    case 0:
        modelPointA = localPosition
        modelAnchor?.addChild(createMarker(at: worldPosition, color: [1, 0, 0]))
        print("📍 Model point A: \(localPosition)")
        tapStep += 1
    case 1:
        modelPointB = localPosition
        modelAnchor?.addChild(createMarker(at: worldPosition, color: [1, 0.5, 0]))
        print("📍 Model point B: \(localPosition)")
        tapStep += 1
        
    case 2:
        targetPointA = worldPosition
        targetMarkerA = createMarker(at: worldPosition,color: [0, 1, 0])
        modelAnchor?.addChild(targetMarkerA!)
        print("✅ Target point A: \(worldPosition)")
        tapStep += 1
        
    case 3:
        targetPointB = worldPosition
        targetMarkerB = createMarker(at: worldPosition,color: [0, 0, 1])
        modelAnchor?.addChild(targetMarkerB!)
        print("✅ Target point B: \(worldPosition)")
        alignmentReady = true
        tapStep += 1

    default:
        print("⚠️ Unexpected tap on model helper at step \(tapStep)")
    }
}

func alignModel2Points() { guard let modelPointA = modelPointA, let modelPointB = modelPointB, let targetPointA = targetPointA, let targetPointB = targetPointB, let modelRootEntity = modelRootEntity, let pivotEntity = pivotEntity, let modelAnchor = modelAnchor else { print("❌ Missing data for alignment") return }

    let modelVec = modelPointB - modelPointA
    let targetVec = targetPointB - targetPointA

    let modelLength = length(modelVec)
    let targetLength = length(targetVec)
    let scale = targetLength / modelLength

    let modelDir = normalize(modelVec)
    let targetDir = normalize(targetVec)

    var axis = cross(modelDir, targetDir)
    let axisLength = length(axis)

    var rotation = simd_quatf()

    if axisLength < 1e-6 {
        if dot(modelDir, targetDir) > 0 {
            rotation = simd_quatf(angle: 0, axis: [0,1,0])
        } else {
            let up: SIMD3<Float> = [0,1,0]
            axis = cross(modelDir, up)
            if length(axis) < 1e-6 {
                axis = cross(modelDir, [1,0,0])
            }
            rotation = simd_quatf(angle: .pi, axis: normalize(axis))
        }
    } else {
        let dotProduct = dot(modelDir, targetDir)
        let clampedDot = max(-1.0, min(dotProduct, 1.0))
        let angle = acos(clampedDot)
        rotation = simd_quatf(angle: angle, axis: normalize(axis))
    }

    modelRootEntity.scale = .one * scale
    modelRootEntity.orientation = rotation

    let transformedPointA = rotation.act(modelPointA * scale)

    pivotEntity.position = -transformedPointA

    modelAnchor.position = targetPointA
    
    alignedModelPosition = modelAnchor.position

    print("✅ Aligned with scale \(scale), rotation \(rotation)")

Hi @ArcSurgeryLab

I'm not sure why updating would cause a shift in entities. It sounds like you are trying to display a grid of entities, when someone taps an entity, in the grid, you want to position content relative to the tapped entity (not the tap location). Is that correct? If so here's a snippet that does that. Try comparing it to your code to it as a means of debugging.

What does stand out is your use of a world AnchorEntity. Keep in mind, querying the position of a world AnchorEntity or its children will return a position that does not account for the position you passed to the world AnchorEntity's constructor (in this case [1, 1.3, -0.8]). For example, in the snippet below, modelAnchor.position(relativeTo: nil) will return [0, 0, 0] and not [0, 1.3, -0.8].

struct ImmersiveView: View {
    let modelAnchor = AnchorEntity(world: [0, 1.3, -0.8])

    var body: some View {
        RealityView { content in
            let gridCell1 = createGridCell()
            let gridCell2 = createGridCell()
            gridCell2.position.x = 0.5
            
            modelAnchor.addChild(gridCell1)
            modelAnchor.addChild(gridCell2)
            content.add(modelAnchor)
        }
        .gesture(TapGesture().targetedToAnyEntity().onEnded { gesture in
            let tappedEntity = gesture.entity
            let position = tappedEntity.position
            let marker = createMarker(at: position, color: .red)
            
            modelAnchor.addChild(marker)
        })
    }
    
    func createGridCell() -> Entity {
        let cell = ModelEntity(mesh: .generateBox(size: 0.1), materials: [SimpleMaterial(color: .blue, isMetallic: false)])
        cell.generateCollisionShapes(recursive: false)
        cell.components.set(InputTargetComponent())
        
        return cell
    }
    
    func createMarker(at position: SIMD3<Float>, color: UIColor) -> Entity {
        let marker = ModelEntity(mesh: .generateSphere(radius: 0.02), materials: [SimpleMaterial(color: color, isMetallic: false)])
        
         marker.position = position
        
        // Offset the marker on the y-axis so you can see it.
        marker.position.y += 0.2
        return marker
    }
}

Hi thanks for the reply! the problem I'm having is with the alignment of a model. I tap 2 times on a 3D model and then 2 times on a grid of spheres. after calculating the alignment the model should jump to it's new position on the grid of spheres and match the target points. I have an unknown visual offset after the alignment even though visually I can see the taps are in the correct place. I'll add a snippet of my alignment function and an image of the visual. the brute force offset you can see in the code was to try and move the alignment a meter to the left (which wasn't successful).

func alignModel2Points() { guard let modelPointA = modelPointA, let modelPointB = modelPointB, let targetPointA = targetPointA, let targetPointB = targetPointB, let modelRootEntity = modelRootEntity, let modelAnchor = modelAnchor else { print("❌ Missing data for alignment") return } let offset = SIMD3<Float>(-1, 0, 0) let modelPointA_shifted = modelPointA + offset let modelPointB_shifted = modelPointB + offset let targetPointA_shifted = targetPointA + offset let targetPointB_shifted = targetPointB + offset

    modelRootEntity.position = .zero
    modelRootEntity.orientation = simd_quatf()
    modelRootEntity.scale = SIMD3<Float>(repeating: 1.0)
    pivotEntity?.position = .zero

    // 1) compute scale from the two segments
    let modelVec = modelPointB_shifted - modelPointA_shifted
    let targetVec = targetPointB_shifted - targetPointA_shifted
    let modelLen = length(modelVec)
    let targetLen = length(targetVec)
    guard modelLen > 1e-6 else {
        print("❌ model points coincide")
        return
    }
    let s = targetLen / modelLen

    // 2) compute rotation that aligns modelVec -> targetVec
    let modelDir = normalize(modelVec)
    let targetDir = normalize(targetVec)
    let crossProd = cross(modelDir, targetDir)
    let crossLen = length(crossProd)

    var rot: simd_quatf
    if crossLen < 1e-6 {
        // parallel or opposite
        if dot(modelDir, targetDir) > 0 {
            rot = simd_quatf(angle: 0, axis: [0,1,0])
        } else {
            var tmp = cross(modelDir, [0,1,0])
            if length(tmp) < 1e-6 { tmp = cross(modelDir, [1,0,0]) }
            rot = simd_quatf(angle: .pi, axis: normalize(tmp))
        }
    } else {
        let dp = dot(modelDir, targetDir)
        let clamped = min(max(dp, -1.0), 1.0)
        let angle = acos(clamped)
        rot = simd_quatf(angle: angle, axis: normalize(crossProd))
    }

    // 3) Compute world-space translation for the anchor:
    //    We want: worldPoint = AnchorTransform * (rot * (s * modelPoint) + modelRootLocalPosition)
    //    Since modelRootEntity is identity at origin, simplified:
    //    translation = targetPointA - rot.act(modelPointA_shifted * s)
    let transformedModelA = rot.act(modelPointA_shifted * s)
    let translation = targetPointA - transformedModelA

    // 4) Build final Transform and set it on the modelAnchor (anchor is root — transform is world)
    var final = Transform()
    final.scale = SIMD3<Float>(repeating: s)
    final.rotation = rot
    final.translation = translation

    // Apply final transform to the anchor (this places the whole model in world space)
    modelAnchor.transform = final

    alignedModelPosition = modelAnchor.position
    alignmentDone = true

    // debug prints
    let modelA_world_after = modelRootEntity.convert(position: modelPointA_shifted, to: nil)
    let modelB_world_after = modelRootEntity.convert(position: modelPointB_shifted, to: nil)
    print("""
    ✅ ALIGN:
      scale s = \(s)
      rotation = \(rot)
      translation = \(translation)
      modelA_world_after = \(modelA_world_after)
      targetA = \(targetPointA)
      modelB_world_after = \(modelB_world_after)
      targetB = \(targetPointB)
      finalAnchor = \(modelAnchor.position)
    """)

    // cleanup

// removeAllMarkers() NotificationCenter.default.post(name: .alignmentDidComplete, object: nil) debugSpawnSpheres() printAlignmentOffsets() }

func printAlignmentOffsets() {
    guard let mr = modelRootEntity,
          let mA = modelPointA, let mB = modelPointB,
          let tA = targetPointA, let tB = targetPointB else { return }

    let mA_world = mr.convert(position: mA, to: nil)
    let mB_world = mr.convert(position: mB, to: nil)

    let offsetA = tA - mA_world
    let offsetB = tB - mB_world

    print("DEBUG OFFSETS:")
    print(" mA_world = \(mA_world)")
    print(" tA       = \(tA)")
    print(" offsetA  = \(offsetA)")
    print(" mB_world = \(mB_world)")
    print(" tB       = \(tB)")
    print(" offsetB  = \(offsetB)")
}

func debugSpawnSpheres() {
    guard let c = content, let a = modelRootEntity,
          let A = modelPointA, let B = modelPointB,
          let tA = targetPointA, let tB = targetPointB else { return }
    let s = [(a.convert(position: A, to: nil), UIColor.red),
             (a.convert(position: B, to: nil), UIColor.orange),
             (tA, UIColor.green),
             (tB, UIColor.blue)]
    for (pos, color) in s {
        let e = ModelEntity(mesh: MeshResource.generateSphere(radius: 0.01),
                            materials: [SimpleMaterial(color: color, isMetallic: false)])
        e.position = pos
        c.add(e)
        allMarkers.append(e)
    }
}
version update in Vision Pro
 
 
Q