Created
September 14, 2024 00:41
-
-
Save Matt54/5faee4bbbb935c334ebf8df7b44db28d to your computer and use it in GitHub Desktop.
RealityView with raycasting from the index finger to a sphere, showing a color change on hit
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
import RealityKit | |
import ARKit | |
struct HandTrackingRaycastView: View { | |
@State var jointPositions: [HandSkeleton.JointName: SIMD3<Float>] = [:] | |
@State var sphereEntity: ModelEntity? | |
@State var debugCylinderEntity: ModelEntity? | |
let sphereRadius: Float = 0.1 | |
var body: some View { | |
RealityView { content in | |
let sphere = createSphere() | |
content.add(sphere) | |
let cylinder = createCylinder() | |
sphereEntity = sphere | |
debugCylinderEntity = cylinder | |
content.add(cylinder) | |
} | |
.task { | |
await setupHandTracking() | |
} | |
} | |
func createSphere() -> ModelEntity { | |
let sphere = ModelEntity(mesh: .generateSphere(radius: sphereRadius), | |
materials: [SimpleMaterial(color: .blue, isMetallic: false)]) | |
sphere.collision = CollisionComponent(shapes: [.generateSphere(radius: sphereRadius)]) | |
sphere.position = SIMD3<Float>(0.0, 1.3, -2.0) | |
sphere.position.z = -2.0 | |
return sphere | |
} | |
func createCylinder() -> ModelEntity { | |
let cylinderMesh = MeshResource.generateCylinder(height: 1, radius: 0.002) | |
let material = SimpleMaterial(color: .red, isMetallic: false) | |
let entity = ModelEntity(mesh: cylinderMesh, materials: [material]) | |
return entity | |
} | |
private func setupHandTracking() async { | |
let session = ARKitSession() | |
let handTracking = HandTrackingProvider() | |
do { | |
try await session.run([handTracking]) | |
for await update in handTracking.anchorUpdates { | |
let handAnchor = update.anchor | |
if handAnchor.chirality == .right { | |
updateJointPositions(for: handAnchor) | |
updateForHandPosition() | |
} | |
} | |
} catch { | |
print("Error setting up hand tracking: \(error)") | |
} | |
} | |
func updateJointPositions(for handAnchor: HandAnchor) { | |
jointPositions = Dictionary(uniqueKeysWithValues: | |
HandSkeleton.JointName.allCases.compactMap { jointName in | |
guard let joint = handAnchor.handSkeleton?.joint(jointName) else { return nil } | |
let worldPosition = handAnchor.originFromAnchorTransform * joint.anchorFromJointTransform.columns.3 | |
return (jointName, SIMD3<Float>(worldPosition.x, worldPosition.y, worldPosition.z)) | |
} | |
) | |
} | |
func updateForHandPosition() { | |
guard let indexTipPosition = jointPositions[.indexFingerTip], | |
let indexPIPPosition = jointPositions[.indexFingerIntermediateBase] else { | |
return | |
} | |
let rayDirection = simd_normalize(indexTipPosition - indexPIPPosition) | |
let rayLength: Float = 10 // 10 meters long ray | |
let rayEnd = indexPIPPosition + rayDirection * rayLength | |
updateSphere(rayStart: indexPIPPosition, rayDirection: rayDirection) | |
updateCylinder(rayStart: indexPIPPosition, rayEnd: rayEnd) | |
} | |
func updateSphere(rayStart: SIMD3<Float>, rayDirection: SIMD3<Float>) { | |
guard let sphere = sphereEntity else { return } | |
let intersection = rayIntersectsSphere(rayStart: rayStart, | |
rayDirection: rayDirection, | |
sphereCenter: sphere.position, | |
sphereRadius: sphereRadius) | |
if intersection { | |
sphere.model?.materials = [SimpleMaterial(color: .green, isMetallic: false)] | |
} else { | |
sphere.model?.materials = [SimpleMaterial(color: .blue, isMetallic: false)] | |
} | |
} | |
func updateCylinder(rayStart: SIMD3<Float>, rayEnd: SIMD3<Float>) { | |
guard let debugCylinder = debugCylinderEntity else { return } | |
positionAndOrientCylinder(debugCylinder, from: rayStart, to: rayEnd) | |
} | |
private func rayIntersectsSphere(rayStart: SIMD3<Float>, rayDirection: SIMD3<Float>, sphereCenter: SIMD3<Float>, sphereRadius: Float) -> Bool { | |
let originToCenter = rayStart - sphereCenter | |
let directionMagnitudeSquared = simd_dot(rayDirection, rayDirection) | |
let originToCenterProjection = 2.0 * simd_dot(originToCenter, rayDirection) | |
let perpDistanceSquared = simd_dot(originToCenter, originToCenter) - sphereRadius * sphereRadius | |
let discriminant = originToCenterProjection * originToCenterProjection - 4 * directionMagnitudeSquared * perpDistanceSquared | |
return discriminant > 0 | |
} | |
private func positionAndOrientCylinder(_ cylinder: ModelEntity, from start: SIMD3<Float>, to end: SIMD3<Float>) { | |
let direction = end - start | |
let distance = simd_length(direction) | |
let midpoint = (start + end) / 2 | |
cylinder.position = midpoint | |
cylinder.scale = [1, distance, 1] | |
let yAxis = simd_normalize(direction) | |
let xAxis = simd_normalize(simd_cross([0, 1, 0], yAxis)) | |
let zAxis = simd_cross(xAxis, yAxis) | |
let rotationMatrix = simd_float3x3(columns: (xAxis, yAxis, zAxis)) | |
cylinder.transform.rotation = simd_quaternion(rotationMatrix) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment