Files
L'électron rare b1e53da2bb
CI build oscope-of / build-check (push) Has been cancelled
fix(avlivebody): track hand face depth to pelvis
Drive HandFaceSkeleton.setDepth() from the body pelvis z each frame
(negated ARKit→RealityKit) so hand/face markers follow body depth
instead of rendering at the fixed default plane depth 0.
2026-06-26 14:23:15 +02:00

173 lines
5.3 KiB
Swift

import AppKit
import Foundation
import CoreVideo
import RealityKit
import simd
import AVLiveWire
/// Owns the single RealityKit scene: the video quad, the body root,
/// and an orbital camera. The app calls `updateVideo/updateSkeleton/
/// updateMesh` from the main queue.
@MainActor
final class SceneController {
let arView = ARView(frame: .zero)
private let cameraAnchor = AnchorEntity(world: .zero)
private let camera = PerspectiveCamera()
private let worldAnchor = AnchorEntity(world: .zero)
private let keyLight = DirectionalLight()
private let fillLight = DirectionalLight()
private let rimLight = DirectionalLight()
private(set) var videoQuad: VideoQuad?
private(set) var skeleton: SkeletonEntity?
private(set) var mesh: MeshEntity?
private(set) var handFace: HandFaceSkeleton?
/// Orbital camera state.
private var orbitYaw: Float = 0
private var orbitPitch: Float = 0
private var orbitRadius: Float = 3.0
private var didSetUp = false
func setUp() {
guard !didSetUp else { return }
didSetUp = true
arView.environment.background = .color(.clear)
arView.layer?.isOpaque = false
arView.layer?.backgroundColor = NSColor.clear.cgColor
arView.scene.addAnchor(worldAnchor)
keyLight.light.intensity = 4000
keyLight.orientation = simd_quatf(angle: .pi / 6,
axis: SIMD3(1, 0, 0))
let keyA = AnchorEntity(world: SIMD3<Float>(1, 2, -1))
keyA.addChild(keyLight)
arView.scene.addAnchor(keyA)
fillLight.light.intensity = 1500
fillLight.light.color = NSColor(red: 0.7, green: 0.8,
blue: 1.0, alpha: 1.0)
let fillA = AnchorEntity(world: SIMD3<Float>(-2, 1, -2))
fillA.addChild(fillLight)
arView.scene.addAnchor(fillA)
rimLight.light.intensity = 2000
let rimA = AnchorEntity(world: SIMD3<Float>(0, 1, -5))
rimA.addChild(rimLight)
arView.scene.addAnchor(rimA)
camera.camera.fieldOfViewInDegrees = 55
cameraAnchor.addChild(camera)
arView.scene.addAnchor(cameraAnchor)
applyCamera()
let q = VideoQuad()
worldAnchor.addChild(q.entity)
videoQuad = q
let s = SkeletonEntity()
worldAnchor.addChild(s.root)
skeleton = s
let m = MeshEntity()
worldAnchor.addChild(m.root)
mesh = m
let hf = HandFaceSkeleton()
worldAnchor.addChild(hf.root)
handFace = hf
installOrbitGestures()
}
func updateVideo(_ pixelBuffer: CVPixelBuffer) {
videoQuad?.update(pixelBuffer)
}
func updateSkeleton(_ skeletons: [Int: SkeletonPayload]) {
skeleton?.update(skeletons)
}
func updateMesh(_ persons: [MultiHMRPerson]) {
mesh?.update(persons)
}
func updateHandFace(hands: HandsPayload?, face: FacePayload?,
depth: Float) {
handFace?.setDepth(depth)
handFace?.update(hands: hands, face: face)
}
func setSkeletonVisible(_ v: Bool) { skeleton?.setVisible(v) }
func setMeshVisible(_ v: Bool) { mesh?.setVisible(v) }
func setVideoVisible(_ v: Bool) { videoQuad?.setVisible(v) }
func setVideoOpacity(_ v: Double) { videoQuad?.setOpacity(v) }
func updateMeshMaterial(metallic: Bool, roughness: Double) {
mesh?.setMaterial(metallic: metallic, roughness: roughness)
}
func setLightIntensities(key: Double, fill: Double, rim: Double) {
keyLight.light.intensity = Float(key)
fillLight.light.intensity = Float(fill)
rimLight.light.intensity = Float(rim)
}
func setFieldOfView(_ deg: Double) {
camera.camera.fieldOfViewInDegrees = Float(deg)
}
// MARK: - Orbital camera
private func applyCamera() {
let cy = cos(orbitYaw), sy = sin(orbitYaw)
let cp = cos(orbitPitch), sp = sin(orbitPitch)
let pos = SIMD3<Float>(orbitRadius * cp * sy,
orbitRadius * sp,
orbitRadius * cp * cy)
cameraAnchor.transform.translation = pos
camera.look(at: .zero, from: pos, relativeTo: nil)
}
private func installOrbitGestures() {
let pan = NSPanGestureRecognizer(
target: OrbitTarget.shared, action: #selector(
OrbitTarget.handlePan(_:)))
OrbitTarget.shared.controller = self
arView.addGestureRecognizer(pan)
}
fileprivate func orbit(dx: Float, dy: Float) {
orbitYaw += dx * 0.01
orbitPitch = max(-1.4, min(1.4, orbitPitch + dy * 0.01))
applyCamera()
}
}
/// Bridges the AppKit pan gesture to `SceneController.orbit`.
final class OrbitTarget: NSObject {
static let shared = OrbitTarget()
weak var controller: SceneController?
private var last: CGPoint = .zero
@objc func handlePan(_ g: NSPanGestureRecognizer) {
switch g.state {
case .began:
last = g.translation(in: g.view)
case .changed:
let p = g.translation(in: g.view)
let dx = Float(p.x - last.x)
let dy = Float(p.y - last.y)
last = p
MainActor.assumeIsolated {
self.controller?.orbit(dx: dx, dy: -dy)
}
default:
break
}
}
}