Hi, I heve a problem with an visionOS app and I couldn't find a solution. I have 3D carousel with cards and when I use the drag gesture and I drag to the left I want the carousel to rotate clockwise and when I drag to the right I want the carousel to rotate counter clockwise. My problem is when I rotate my body more than 90 degrees to the left or to the right the drag gesture changes it's value and the carousel rotates in the opposite direction? Do you know how can I maintain the right sense taking into account that the user can rotate his body?
I've tried to take the user orientation with device tracking and check if rotation on Y axis is greater than 90 degrees in both direction but It is a very small area bettween 70-110 degrees when it still rotates in the opposite direction. I think that's because the device traker doesn't update at the same rate as drag gesture or it doesn't have the same acurracy.
Have you considered a RotateGesture3D? I encourage you to use the standard gesture since that's what people expect. Transforming RealityKit entities using gestures is a sample code project that demonstrates how to manipulate virtual objects using the standard system drag, rotate, and scale gestures.
That said, here's code to implement "drag to rotate" in an immersive space (the code uses queryDeviceAnchor which is only available in an immersive space). The solution is to perform an axis rotation along the cross product of the vector between the camera and entity and the vector between drag positions.
Use ECS to implement a custom DragToRotate
component.
import RealityKit
import ARKit
struct DragToRotateComponent: Component {
var previousGesture: EntityTargetValue<DragGesture.Value>?
var currentGesture: EntityTargetValue<DragGesture.Value>?
}
struct DragToRotateSystem: System {
static let query = EntityQuery(where: .has(DragToRotateComponent.self))
private let arkitSession = ARKitSession()
private let worldTrackingProvider = WorldTrackingProvider()
public init(scene: RealityKit.Scene) {
setUpSession()
}
func setUpSession() {
Task {
do {
try await arkitSession.run([worldTrackingProvider])
} catch {
print("Error: \(error)")
}
}
}
public func update(context: SceneUpdateContext) {
let scene = context.scene
let entities = scene.performQuery(Self.query).filter({$0.components[DragToRotateComponent.self]?.previousGesture != nil && $0.components[DragToRotateComponent.self]?.currentGesture != nil})
guard let deviceAnchor = worldTrackingProvider.queryDeviceAnchor(atTimestamp: CACurrentMediaTime()) else { return }
let cameraPosition = simd_make_float3(deviceAnchor.originFromAnchorTransform.columns.3)
for entity in entities {
if let component = entity.components[DragToRotateComponent.self],
let previousGesture = component.previousGesture,
let currentGesture = component.currentGesture {
let previousLocation = previousGesture.convert(previousGesture.location3D, from: .global, to: .scene)
let currentLocation = currentGesture.convert(currentGesture.location3D, from: .global, to: .scene)
let dragDelta = currentLocation - previousLocation
let lengthOfDrag = distance(currentLocation, previousLocation)
let entityPosition = entity.position(relativeTo: nil)
let cameraToEntity = normalize(entityPosition - cameraPosition)
let axis = normalize(cross(normalize(dragDelta), cameraToEntity))
let angle:Float = lengthOfDrag * 2 * .pi
entity.transform.rotation = simd_quatf(angle: angle, axis: axis) * entity.transform.rotation
entity.components.set(DragToRotateComponent())
}
}
}
}
Register the component and system in your app's initializer.
init() {
DragToRotateSystem.registerSystem()
DragToRotateComponent.registerComponent()
}
Create a custom view modifier to enable drag to rotate.
extension View {
func enableDragToRotate() -> some View {
self.modifier(
DragToRotateModifier()
)
}
}
private struct DragToRotateModifier: ViewModifier {
func body(content: Content) -> some View {
let dragGesture = DragGesture(minimumDistance: 0.0)
.targetedToEntity(where: .has(DragToRotateComponent.self))
.onChanged { gesture in
let entity = gesture.entity
let component = entity.components[DragToRotateComponent.self]
let previousGesture = component?.currentGesture
let currentGesture = gesture
entity.components.set(DragToRotateComponent(previousGesture: previousGesture, currentGesture: currentGesture))
}
.onEnded {
gesture in
let entity = gesture.entity
entity.components.set(DragToRotateComponent())
}
content.gesture(dragGesture)
}
}
Use the modifier and custom component.
struct ImmersiveView: View {
var body: some View {
RealityView { content in
let entity = ModelEntity(mesh: .generateBox(size: 0.3), materials: [SimpleMaterial(color: .red, isMetallic: true)])
// Important, generate collision shapes and add an input target component.
entity.generateCollisionShapes(recursive: true)
entity.components.set(InputTargetComponent())
entity.components.set(DragToRotateComponent())
entity.position = [0, 1.2, -1]
content.add(entity)
}
.enableDragToRotate()
}
}