import SwiftUI import RealityKit import ARKit import HandVector struct TargetPracticeView: View { @State var jointPositions: [HandSkeleton.JointName: SIMD3<Float>] = [:] @State var targets: [Target] = [] @State var laserBeamEntity: ModelEntity? let defaultRayLength: Float = 10 // 10 meters long ray default ray length when no target is hit @State var gunEntity: ModelEntity? // used to set scale and sphereRadius of target let targetScale: Float = 0.25 let numberOfTargets: Int = 3 // Adjust this to change the number of targets var latestHandTracking: HandVectorManager = .init(left: nil, right: nil) let fingerGunReadyHandInfo = String.gunReadyPosition.toModel(HVHandJsonModel.self)!.convertToHVHandInfo() let fingerGunFiredTriggerHandInfo = String.gunTriggerFiredPosition.toModel(HVHandJsonModel.self)!.convertToHVHandInfo() let thresholdForFingerGunDetection: Float = 0.95 let thresholdForTriggerFingerDetection: Float = 0.9 @State private var isShowingGun: Bool = false @State private var isReadyToFire: Bool = false @State private var didFire: Bool = false @State private var lastFingerGunDetectionTime: Date = Date() let fingerGunDebounceInterval: TimeInterval = 0.125 // 125 milliseconds let onTargetValidityDuration: TimeInterval = 0.1 // 100 milliseconds @State var gunshotAudioResource: AudioResource? static let defaultLaserOpacity: Float = 0.25 @State private var laserOpacity: Float = defaultLaserOpacity let laserFlashDuration: TimeInterval = 0.25 // Duration of the flash effect @State private var isInCooldown: Bool = false var body: some View { RealityView { content in let skybox = createSkybox() content.add(skybox) gunshotAudioResource = try! await loadGunShotAudioResource() let gunEntity = await createGun() content.add(gunEntity) self.gunEntity = gunEntity let laserBeam = createLaserBeam() laserBeamEntity = laserBeam content.add(laserBeam) let targets = try! await createTargets() targets.forEach({ content.add($0.entity) }) self.targets = targets } .task { await setupHandTracking() } } 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 { await updateForHandAnchor(handAnchor) } } } catch { print("Error setting up hand tracking: \(error)") } } } // MARK: creation entities extension TargetPracticeView { func createGun() async -> ModelEntity { let entity = try! await loadGunEntity() await removeEntityLighting(entity) return entity } func createSkybox() -> ModelEntity { let radius: Float = 1E3 let skyboxMeshResource = MeshResource.generateSphere(radius: radius) let entity = ModelEntity(mesh: skyboxMeshResource, materials: [UnlitMaterial(color: .white)]) entity.scale *= .init(x: -1, y: 1, z: 1) entity.transform.translation += SIMD3<Float>(0.0, 200.0, 0.0) return entity } func createTargets() async throws -> [Target] { let targetEntity = try await loadTargetEntity() await removeEntityLighting(targetEntity) var targets = [Target]() for _ in 0..<numberOfTargets { let clonedTarget = targetEntity.clone(recursive: true) clonedTarget.scale = [targetScale, targetScale, targetScale] clonedTarget.position = randomPosition() targets.append(Target(entity: clonedTarget)) } return targets } func removeEntityLighting(_ entity: ModelEntity) async { let material = entity.model?.materials.first as! PhysicallyBasedMaterial let baseColorTexture = material.baseColor.texture!.resource let originalTexture = try! copyTextureResourceToLowLevelTexture(from: baseColorTexture) let newMaterial = await UnlitMaterial(texture: try! .init(from: originalTexture)) entity.model?.materials = [newMaterial] } func copyTextureResourceToLowLevelTexture(from textureResource: TextureResource) throws -> LowLevelTexture { var descriptor = LowLevelTexture.Descriptor() descriptor.textureType = .type2D descriptor.pixelFormat = .rgba16Float // Use a high precision format descriptor.width = textureResource.width descriptor.height = textureResource.height descriptor.mipmapLevelCount = 1 descriptor.textureUsage = [.shaderRead, .shaderWrite] let texture = try LowLevelTexture(descriptor: descriptor) try textureResource.copy(to: texture.read()) return texture } // TODO: make this an array of cylinders create a glow effect func createLaserBeam() -> ModelEntity { let cylinderMesh = MeshResource.generateCylinder(height: 1, radius: 0.002) let material = UnlitMaterial(color: .red) let entity = ModelEntity(mesh: cylinderMesh, materials: [material]) entity.components.set(OpacityComponent(opacity: 0.0)) return entity } func randomPosition() -> SIMD3<Float> { SIMD3<Float>( Float.random(in: -1.0...1.0), Float.random(in: 0.25...1.75), Float.random(in: -5.0...(-2.0)) ) } } // MARK: hand updates extension TargetPracticeView { func updateForHandAnchor(_ handAnchor: HandAnchor) async { updateJointPositions(for: handAnchor) updateForHandPosition() await checkForHandGestures(for: handAnchor) } 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 wristPosition = jointPositions[.wrist], let palmPosition = calculatePalmPosition() else { return } var rayDirection = simd_normalize(palmPosition - wristPosition) rayDirection.y -= .pi * 0.05 let closestHitTarget = updateTargets(rayStart: wristPosition, rayDirection: rayDirection) let rayLength: Float let rayEnd: SIMD3<Float> if let hitTarget = closestHitTarget { rayLength = simd_distance(wristPosition, hitTarget.entity.position) rayEnd = wristPosition + rayDirection * rayLength } else { rayLength = defaultRayLength rayEnd = wristPosition + rayDirection * rayLength } updateLaser(from: wristPosition, to: rayEnd) updateGun(from: wristPosition, to: rayEnd) didFire = false } func updateTargets(rayStart: SIMD3<Float>, rayDirection: SIMD3<Float>) -> Target? { var closestHit: (target: Target, distance: Float, position: SIMD3<Float>)? = nil for index in targets.indices { if let intersectionPoint = rayIntersectsSphere(rayStart: rayStart, rayDirection: rayDirection, sphereCenter: targets[index].entity.position, sphereRadius: targetScale) { targets[index].lastOnTargetTime = Date() let distance = simd_distance(rayStart, intersectionPoint) if closestHit == nil || distance < closestHit!.distance { closestHit = (targets[index], distance, intersectionPoint) } } else { targets[index].lastOnTargetTime = nil } if didFire { let isRecentlyOnTarget = isRecentlyOnTarget(for: targets[index]) targets[index].lastOnTargetTime = nil if isRecentlyOnTarget && !targets[index].isHit { // Hit detected, move the sphere targets[index].entity.position = randomPosition() targets[index].isHit = true } } } // Reset didFire after processing all targets if didFire { didFire = false // Reset all targets' hit status for the next round for index in targets.indices { targets[index].isHit = false } } return closestHit?.target } private func rayIntersectsSphere(rayStart: SIMD3<Float>, rayDirection: SIMD3<Float>, sphereCenter: SIMD3<Float>, sphereRadius: Float) -> SIMD3<Float>? { let originToCenter = rayStart - sphereCenter let a = simd_dot(rayDirection, rayDirection) let b = 2.0 * simd_dot(originToCenter, rayDirection) let c = simd_dot(originToCenter, originToCenter) - sphereRadius * sphereRadius let discriminant = b * b - 4 * a * c if discriminant < 0 { return nil // No intersection } let t = (-b - sqrt(discriminant)) / (2 * a) if t < 0 { return nil // Intersection behind the ray start } let intersectionPoint = rayStart + t * rayDirection return intersectionPoint } func isRecentlyOnTarget(for target: Target) -> Bool { guard let lastOnTargetTime = target.lastOnTargetTime else { return false } return Date().timeIntervalSince(lastOnTargetTime) <= onTargetValidityDuration } func updateLaser(from start: SIMD3<Float>, to end: SIMD3<Float>) { guard let entity = laserBeamEntity else { return } let direction = end - start let distance = simd_length(direction) let midpoint = (start + end) / 2 entity.position = midpoint entity.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)) entity.transform.rotation = simd_quaternion(rotationMatrix) // Update the opacity entity.components.set(OpacityComponent(opacity: laserOpacity)) } func flashLaserBeam() { laserOpacity = 1.0 isInCooldown = true // Schedule a task to reset opacity after the flash duration Task { try? await Task.sleep(nanoseconds: UInt64(laserFlashDuration * 1_000_000_000)) laserOpacity = TargetPracticeView.defaultLaserOpacity isInCooldown = false } } func updateGun(from start: SIMD3<Float>, to end: SIMD3<Float>) { guard let entity = gunEntity else { return } let direction = end - start entity.position = start entity.scale = [0.1, 0.1, 0.1] let yAxis = simd_normalize(direction) let xAxis = simd_normalize(simd_cross([0, 1, 0], yAxis)) let zAxis = simd_cross(xAxis, yAxis) // fix how the source is not oriented forward let rotationZ = simd_quatf(angle: .pi*0.5, axis: [0, 0, 1]) let rotationX = simd_quatf(angle: -.pi*0.5, axis: [1, 0, 0]) let rotationMatrix = simd_float3x3(columns: (xAxis, yAxis, zAxis)) entity.transform.rotation = simd_quaternion(rotationMatrix) * rotationZ * rotationX } func calculatePalmPosition() -> SIMD3<Float>? { guard let middleFingerBase = jointPositions[.middleFingerMetacarpal], let ringFingerBase = jointPositions[.ringFingerMetacarpal], let indexFingerBase = jointPositions[.indexFingerMetacarpal] else { return nil } // Calculate the average position of these three joints to estimate the palm center return (middleFingerBase + ringFingerBase + indexFingerBase) / 3 } func checkForHandGestures(for handAnchor: HandAnchor) async { let handInfo = latestHandTracking.generateHandInfo(from: handAnchor) if let handInfo { await latestHandTracking.updateHandSkeletonEntity(from: handInfo) } let averageAndEachRightScores = latestHandTracking.rightHandVector?.averageAndEachSimilarities(of: .fiveFingers, to: fingerGunReadyHandInfo!) let average = averageAndEachRightScores?.0 if thresholdForFingerGunDetection < average! { lastFingerGunDetectionTime = Date() if !isReadyToFire { print("got the finger gun with average: \(average!)") isReadyToFire = true } if !isShowingGun { guard let debugCylinder = laserBeamEntity else { return } debugCylinder.components.set(OpacityComponent(opacity: 1.0)) isShowingGun = true } } else { // Debounced task Task { try await Task.sleep(nanoseconds: UInt64(fingerGunDebounceInterval * 1_000_000_000)) if Date().timeIntervalSince(lastFingerGunDetectionTime) >= fingerGunDebounceInterval { if isShowingGun { print("got no finger gun with average: \(average!)") guard let debugCylinder = laserBeamEntity else { return } debugCylinder.components.set(OpacityComponent(opacity: 0.0)) isShowingGun = false isReadyToFire = false } } } } let triggerScores = latestHandTracking.rightHandVector?.averageAndEachSimilarities(of: .fiveFingers, to: fingerGunFiredTriggerHandInfo!) let triggerAverage = triggerScores?.0 if thresholdForTriggerFingerDetection < triggerAverage! { if isReadyToFire && !isInCooldown { print("got trigger with average: \(triggerAverage!)") isReadyToFire = false didFire = true if let gunshotAudioResource { gunEntity?.playAudio(gunshotAudioResource) } flashLaserBeam() } } } } // MARK: loading entities extension TargetPracticeView { func loadGunEntity(url: URL = URL(string: "https://matt54.github.io/Resources/laser_gun.usdz")!) async throws -> ModelEntity { let (downloadedURL, _) = try await URLSession.shared.download(from: url) let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let destinationURL = documentsDirectory.appendingPathComponent("downloadedLaserGunModel.usdz") if FileManager.default.fileExists(atPath: destinationURL.path) { try FileManager.default.removeItem(at: destinationURL) } try FileManager.default.moveItem(at: downloadedURL, to: destinationURL) let entity = try await ModelEntity.init(contentsOf: destinationURL) return entity } func loadTargetEntity(url: URL = URL(string: "https://matt54.github.io/Resources/target.usdz")!) async throws -> ModelEntity { let (downloadedURL, _) = try await URLSession.shared.download(from: url) let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let destinationURL = documentsDirectory.appendingPathComponent("downloadedTargetModel.usdz") if FileManager.default.fileExists(atPath: destinationURL.path) { try FileManager.default.removeItem(at: destinationURL) } try FileManager.default.moveItem(at: downloadedURL, to: destinationURL) let entity = try await ModelEntity.init(contentsOf: destinationURL) let rotationX = simd_quatf(angle: .pi, axis: [0, 1, 0]) entity.transform.rotation = rotationX return entity } func loadGunShotAudioResource(url: URL = URL(string: "https://matt54.github.io/Resources/laser_gun_shot.wav")!) async throws -> AudioFileResource { let (downloadedURL, _) = try await URLSession.shared.download(from: url) let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let destinationURL = documentsDirectory.appendingPathComponent("downloadedSound.wav") if FileManager.default.fileExists(atPath: destinationURL.path) { try FileManager.default.removeItem(at: destinationURL) } try FileManager.default.moveItem(at: downloadedURL, to: destinationURL) let audioResource = try AudioFileResource.load(contentsOf: destinationURL) return audioResource } } struct Target { let entity: ModelEntity var isHit: Bool = false var lastOnTargetTime: Date? } // TODO: make pull request with HandVector library to make this extension public extension String { func toModel<T>(_ type: T.Type, using encoding: String.Encoding = .utf8) -> T? where T : Decodable { guard let data = self.data(using: encoding) else { return nil } do { return try JSONDecoder().decode(T.self, from: data) } catch { print(error) } return nil } } extension String { static let gunReadyPosition: String = """ {"transform":[[0.03681829,-0.35651627,0.9335633,0],[0.9981043,-0.03298368,-0.051959727,0],[0.049316857,0.9337067,0.35462606,0],[0.119505,1.058816,-0.3033591,1]],"name":"right","joints":[{"transform":[[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]],"name":"wrist","isTracked":true},{"isTracked":true,"transform":[[0.7483124,0.08903434,-0.6573445,0],[0.6232275,0.24503177,0.7426623,0],[0.22719273,-0.96541846,0.12787166,0],[-0.023037225,-0.011565208,0.019217536,1]],"name":"thumbKnuckle"},{"isTracked":true,"transform":[[0.9145838,0.03962274,-0.40245014,0],[0.3839136,0.22763534,0.8948699,0],[0.1270691,-0.9729398,0.19297989,0],[-0.06262271,-0.016338525,0.053467937,0.9999999]],"name":"thumbIntermediateBase"},{"transform":[[0.55769247,-0.09324432,-0.8247935,0],[0.82048357,0.21233602,0.5307732,0],[0.12564181,-0.97273785,0.19492361,0],[-0.09256029,-0.017590031,0.06606347,0.9999999]],"name":"thumbIntermediateTip","isTracked":true},{"isTracked":true,"transform":[[0.5576926,-0.09324434,-0.8247935,0],[0.8204833,0.21233588,0.5307733,0],[0.12564164,-0.9727376,0.1949236,0],[-0.10933509,-0.014713061,0.09040629,0.9999999]],"name":"thumbTip"},{"isTracked":true,"name":"indexFingerMetacarpal","transform":[[0.9873512,0.0007703505,-0.15854767,0],[-0.00018403005,0.99999326,0.0037125493,0],[0.15854947,-0.003636434,0.9873445,0],[-0.02504237,8.650124e-05,0.016333118,1]]},{"name":"indexFingerKnuckle","isTracked":true,"transform":[[0.9271389,0.1894835,0.3232792,0],[-0.20793396,0.9778683,0.023180438,0],[-0.31173214,-0.08871223,0.94601953,0],[-0.09699588,0.00033357737,0.02648428,1]]},{"isTracked":true,"transform":[[0.9415803,0.10521732,0.3199318,0],[-0.12780455,0.9905191,0.050381172,0],[-0.31159747,-0.08832667,0.94610006,0],[-0.13916928,-0.008303657,0.011948585,0.99999994]],"name":"indexFingerIntermediateBase"},{"transform":[[0.94819283,0.034945816,0.31576785,0],[-0.060861282,0.9955039,0.072583534,0],[-0.31181157,-0.08804125,0.9460563,0],[-0.16357674,-0.010998578,0.0036838902,0.99999994]],"isTracked":true,"name":"indexFingerIntermediateTip"},{"transform":[[0.94819283,0.0349457,0.315768,0],[-0.060861256,0.9955037,0.07258351,0],[-0.3118117,-0.08804125,0.9460561,0],[-0.18628134,-0.011772217,-0.0039144484,0.99999994]],"name":"indexFingerTip","isTracked":true},{"isTracked":true,"transform":[[0.9999436,0.00029446578,0.010613574,0],[-0.00033532665,0.9999925,0.0038510533,0],[-0.0106123155,-0.0038544233,0.99993616,0],[-0.027172834,0.0001347065,0.0036730468,1]],"name":"middleFingerMetacarpal"},{"isTracked":true,"name":"middleFingerKnuckle","transform":[[0.31107935,0.9494624,0.0418443,0],[-0.94966394,0.312256,-0.025200296,0],[-0.036992837,-0.031898756,0.99880624,0],[-0.095856436,0.00042155385,0.0019043083,0.99999994]]},{"transform":[[-0.97067386,0.23868601,-0.028664839,0],[-0.23642187,-0.9694015,-0.06607428,0],[-0.04355868,-0.057359572,0.99740297,0],[-0.10820865,-0.04728632,0.0028083322,0.9999999]],"name":"middleFingerIntermediateBase","isTracked":true},{"isTracked":true,"transform":[[-0.98711973,-0.1526101,-0.048010733,0],[0.1551362,-0.98640466,-0.05421107,0],[-0.039084814,-0.06096107,0.9973746,0],[-0.07775817,-0.05483128,0.004591465,0.9999999]],"name":"middleFingerIntermediateTip"},{"name":"middleFingerTip","isTracked":true,"transform":[[-0.98711985,-0.15260991,-0.04801078,0],[0.15513599,-0.9864048,-0.054211125,0],[-0.039084826,-0.06096117,0.99737483,0],[-0.055462185,-0.051387776,0.005703509,0.9999999]]},{"isTracked":true,"transform":[[0.9876838,0.00039916535,0.15646292,0],[-0.001013631,0.999992,0.0038470975,0],[-0.15646014,-0.00395833,0.98767626,0],[-0.02746223,-0.0013832152,-0.00899744,1]],"name":"ringFingerMetacarpal"},{"transform":[[-0.0012533537,0.9747956,-0.22309683,0],[-0.9960826,-0.020943103,-0.08591284,0],[-0.088419706,0.22211517,0.9710031,0],[-0.094665445,-0.0011133702,-0.020322772,0.99999994]],"isTracked":true,"name":"ringFingerKnuckle"},{"name":"ringFingerIntermediateBase","isTracked":true,"transform":[[-0.9931481,0.042646114,-0.10880357,0],[-0.0667791,-0.97115594,0.22890316,0],[-0.09590343,0.23460054,0.96734947,0],[-0.09184222,-0.04280009,-0.008270023,0.9999999]]},{"isTracked":true,"name":"ringFingerIntermediateTip","transform":[[-0.87231886,-0.48775947,0.033920035,0],[0.47993666,-0.84094685,0.24993832,0],[-0.093384825,0.23430538,0.9676676,0],[-0.06364327,-0.044113144,-0.004825471,0.9999999]]},{"transform":[[-0.8723191,-0.4877596,0.03391996,0],[0.4799366,-0.84094703,0.24993841,0],[-0.093384914,0.23430541,0.96766764,0],[-0.044957124,-0.033260565,-0.00605893,0.9999999]],"isTracked":true,"name":"ringFingerTip"},{"transform":[[0.9754438,0.0007679999,0.22024716,0],[-0.0016250212,0.9999917,0.003710361,0],[-0.22024249,-0.003977161,0.9754369,0],[-0.026689336,-0.0029354095,-0.023558915,1]],"isTracked":true,"name":"littleFingerMetacarpal"},{"name":"littleFingerKnuckle","transform":[[-0.021861674,0.9597742,-0.2799214,0],[-0.9730536,-0.08471092,-0.21445592,0],[-0.2295416,0.26769006,0.9357632,0],[-0.08487275,-0.0027343482,-0.037119243,1]],"isTracked":true},{"name":"littleFingerIntermediateBase","transform":[[-0.95487165,0.1191173,-0.2720872,0],[-0.18119034,-0.9594734,0.21582665,0],[-0.23535168,0.2553863,0.9377566,0],[-0.082020074,-0.035506573,-0.025537848,1]],"isTracked":true},{"name":"littleFingerIntermediateTip","transform":[[-0.8006587,-0.59837437,-0.029901683,0],[0.5545437,-0.75905365,0.34105653,0],[-0.22677635,0.25648803,0.93956715,0],[-0.06307931,-0.037934672,-0.01993567,1]],"isTracked":true},{"name":"littleFingerTip","isTracked":true,"transform":[[-0.8006587,-0.59837437,-0.029901683,0],[0.55454355,-0.7590536,0.3410568,0],[-0.22677657,0.2564883,0.93956715,0],[-0.04816789,-0.026222985,-0.020020342,1]]},{"isTracked":true,"transform":[[-0.83367807,-0.33506683,0.4389886,0],[-0.36958972,0.92916626,0.007321467,0],[-0.41034657,-0.15614182,-0.8984629,0],[-2.9802322e-08,1.4901161e-08,-2.9802322e-08,1]],"name":"forearmWrist"},{"name":"forearmArm","transform":[[-0.833678,-0.3350668,0.4389886,0],[-0.36958975,0.92916626,0.0073214276,0],[-0.41034657,-0.15614179,-0.89846283,0],[0.22288692,0.083471484,-0.107541226,1]],"isTracked":true}],"chirality":"right"} """ static var gunTriggerFiredPosition: String = """ {"chirality":"right","joints":[{"transform":[[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]],"name":"wrist","isTracked":true},{"transform":[[0.7483334,0.09519935,-0.65645605,0],[0.6199202,0.2517302,0.7431899,0],[0.23600104,-0.96310407,0.12936214,0],[-0.023353577,-0.011348635,0.018963456,1.0000001]],"name":"thumbKnuckle","isTracked":true},{"isTracked":true,"transform":[[0.9556311,0.05204724,-0.28993207,0],[0.27259618,0.21673742,0.93739885,0],[0.11162812,-0.9748416,0.19293313,0],[-0.06494015,-0.016365109,0.053815972,1.0000001]],"name":"thumbIntermediateBase"},{"transform":[[0.65147614,-0.07337183,-0.7551131,0],[0.7500864,0.21158442,0.6265804,0],[0.11379688,-0.97460204,0.19287737,0],[-0.09750525,-0.017905606,0.06285423,1]],"isTracked":true,"name":"thumbIntermediateTip"},{"transform":[[0.65147597,-0.07337182,-0.755113,0],[0.7500863,0.21158457,0.6265802,0],[0.11379693,-0.97460186,0.19287738,0],[-0.11828706,-0.015484761,0.0855253,1]],"name":"thumbTip","isTracked":true},{"isTracked":true,"name":"indexFingerMetacarpal","transform":[[0.9904148,0.00047922484,-0.1381248,0],[-0.0015663946,0.9999686,-0.0077618966,0],[0.13811672,0.007903845,0.9903845,0],[-0.025574356,0.00016841292,0.016237155,1.0000001]]},{"transform":[[0.33106402,0.9313371,0.15168403,0],[-0.9435455,0.32487956,0.064618684,0],[0.010902685,-0.16451362,0.9863149,0],[-0.10142365,0.00072960556,0.025060492,1.0000001]],"isTracked":true,"name":"indexFingerKnuckle"},{"name":"indexFingerIntermediateBase","isTracked":true,"transform":[[-0.9999372,-0.0050846054,0.010002013,0],[0.003429331,-0.98726726,-0.1590336,0],[0.0106832525,-0.15898922,0.9872226,0],[-0.11412289,-0.04431664,0.019907355,1.0000001]]},{"name":"indexFingerIntermediateTip","isTracked":true,"transform":[[-0.55583847,-0.8214681,-0.12741268,0],[0.8312102,-0.5470858,-0.09893063,0],[0.011562692,-0.16089611,0.98690385,0],[-0.08689164,-0.044329368,0.020021155,1.0000001]]},{"name":"indexFingerTip","isTracked":true,"transform":[[-0.55583835,-0.8214681,-0.1274127,0],[0.8312101,-0.54708576,-0.09893059,0],[0.011562661,-0.16089608,0.9869037,0],[-0.07507946,-0.0250963,0.022131085,1.0000001]]},{"isTracked":true,"name":"middleFingerMetacarpal","transform":[[0.99994224,5.4265183e-05,0.010770501,0],[2.7246339e-05,0.9999715,-0.007565906,0],[-0.010770587,0.0075657517,0.99991375,0],[-0.027691185,0.00021959841,0.0036745965,1.0000001]]},{"transform":[[0.2625057,0.96121,0.08465515,0],[-0.9639889,0.26511344,-0.020991897,0],[-0.042620845,-0.07609611,0.9961894,0],[-0.10007346,0.0007865727,0.0017653409,1.0000001]],"name":"middleFingerKnuckle","isTracked":true},{"isTracked":true,"transform":[[-0.9960801,-0.06880847,-0.055586327,0],[0.07441876,-0.9915644,-0.10612293,0],[-0.047815263,-0.10984362,0.9927982,0],[-0.109669164,-0.0483307,4.4017594e-05,1]],"name":"middleFingerIntermediateBase"},{"name":"middleFingerIntermediateTip","transform":[[-0.34592554,-0.9304006,-0.121205255,0],[0.937399,-0.34825101,-0.0021236737,0],[-0.040233966,-0.114352286,0.9926253,0],[-0.07765334,-0.04632087,0.0020706353,1]],"isTracked":true},{"name":"middleFingerTip","transform":[[-0.3459255,-0.93040043,-0.12120523,0],[0.9373989,-0.34825101,-0.0021236467,0],[-0.04023399,-0.11435226,0.9926252,0],[-0.071053825,-0.024442425,0.0037397146,1]],"isTracked":true},{"transform":[[0.9901111,-0.000524097,0.14028518,0],[0.0015186571,0.9999746,-0.0069826003,0],[-0.14027794,0.007126593,0.99008673,0],[-0.027912617,-0.00127545,-0.008930057,1.0000001]],"isTracked":true,"name":"ringFingerMetacarpal"},{"transform":[[0.11645274,0.99289507,-0.024456479,0],[-0.9910486,0.11454721,-0.0685683,0],[-0.0652797,0.032222517,0.99734664,0],[-0.09785095,-0.0007026344,-0.019427152,1.0000001]],"name":"ringFingerKnuckle","isTracked":true},{"isTracked":true,"name":"ringFingerIntermediateBase","transform":[[-0.997761,-0.016860921,-0.06472351,0],[0.012973086,-0.9981128,0.060025338,0],[-0.06561342,0.059051238,0.9960965,0],[-0.099433765,-0.044465277,-0.015910923,1.0000001]]},{"transform":[[-0.2544789,-0.96663296,0.029352397,0],[0.96455187,-0.2515047,0.079906285,0],[-0.06985777,0.04864639,0.9963702,0],[-0.069589265,-0.044131514,-0.013678611,1.0000001]],"name":"ringFingerIntermediateTip","isTracked":true},{"isTracked":true,"transform":[[-0.2544788,-0.9666329,0.02935244,0],[0.964552,-0.25150472,0.07990631,0],[-0.06985778,0.048646417,0.9963702,0],[-0.0654154,-0.022222081,-0.015528647,1]],"name":"ringFingerTip"},{"transform":[[0.97497225,-0.000826969,0.22232538,0],[0.0023143436,0.9999767,-0.006429921,0],[-0.2223149,0.006783522,0.97495145,0],[-0.027045697,-0.0027898103,-0.023430854,1.0000001]],"name":"littleFingerMetacarpal","isTracked":true},{"transform":[[0.06825185,0.9954417,-0.066615984,0],[-0.9734078,0.05180799,-0.22314477,0],[-0.21867634,0.08007455,0.97250664,0],[-0.08645508,-0.00229989,-0.037184328,1.0000001]],"name":"littleFingerKnuckle","isTracked":true},{"isTracked":true,"transform":[[-0.9729524,0.06563075,-0.22148769,0],[-0.08437353,-0.99351335,0.07624091,0],[-0.21504717,0.09286644,0.9721785,0],[-0.086084165,-0.037450034,-0.032782976,1.0000002]],"name":"littleFingerIntermediateBase"},{"isTracked":true,"name":"littleFingerIntermediateTip","transform":[[-0.31905454,-0.94770515,0.007732848,0],[0.92150366,-0.30830652,0.23617493,0],[-0.22144006,0.08247853,0.97168005,0],[-0.065962076,-0.038880963,-0.028106306,1.0000002]]},{"isTracked":true,"transform":[[-0.31905454,-0.94770527,0.007732892,0],[0.9215038,-0.30830655,0.23617496,0],[-0.22144006,0.08247855,0.97168016,0],[-0.06090485,-0.019953838,-0.02932877,1.0000004]],"name":"littleFingerTip"},{"isTracked":true,"name":"forearmWrist","transform":[[-0.91424805,-0.29512966,0.2775777,0],[-0.30960014,0.9508265,-0.008769782,0],[-0.26133996,-0.09395588,-0.96066326,0],[2.0861626e-07,-1.6391277e-07,2.0861626e-07,1.0000001]]},{"transform":[[-0.91424805,-0.29512966,0.2775777,0],[-0.30960017,0.9508265,-0.008769772,0],[-0.26134,-0.09395589,-0.9606634,0],[0.25306943,0.070762575,-0.0623462,1]],"name":"forearmArm","isTracked":true}],"name":"right","transform":[[-0.09891344,-0.16087997,0.98200476,0],[0.9935917,-0.07020852,0.08857849,0],[0.054694623,0.9844735,0.16679356,0],[0.11687718,1.0611293,-0.43109167,0.99999994]]} """ }