b1e53da2bb
CI build oscope-of / build-check (push) Has been cancelled
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.
173 lines
5.3 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|