Created
September 14, 2024 14:28
-
-
Save Matt54/7581778f0a3f4f0da5bdcdf85db074f2 to your computer and use it in GitHub Desktop.
RealityView where users practice target shooting by detecting finger gun gestures and using ray casting to hit virtual targets
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 | |
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]]} | |
""" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment