From 5dfbfc7ae99a9cd0dffbc72b76587c05b620eb35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:19:14 +0200 Subject: [PATCH] feat(avlivebody): lights, material and visibility --- .../Sources/AVLiveBody/MeshEntity.swift | 13 +++++- .../Sources/AVLiveBody/SceneController.swift | 42 +++++++++++++++++++ .../Sources/AVLiveBody/SkeletonEntity.swift | 2 + .../Sources/AVLiveBody/VideoQuad.swift | 18 +++++++- 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/avlivebody-mac/Sources/AVLiveBody/MeshEntity.swift b/avlivebody-mac/Sources/AVLiveBody/MeshEntity.swift index 8ee8029..ab358fe 100644 --- a/avlivebody-mac/Sources/AVLiveBody/MeshEntity.swift +++ b/avlivebody-mac/Sources/AVLiveBody/MeshEntity.swift @@ -13,7 +13,7 @@ final class MeshEntity { private static let vertexCount = 10475 private let faces: [UInt32] private var pools: [Int: ModelEntity] = [:] - private let material = SimpleMaterial( + private var material = SimpleMaterial( color: NSColor(white: 0.8, alpha: 1.0), roughness: 0.5, isMetallic: false) @@ -41,6 +41,17 @@ final class MeshEntity { } } + func setVisible(_ visible: Bool) { root.isEnabled = visible } + + func setMaterial(metallic: Bool, roughness: Double) { + material = SimpleMaterial( + color: NSColor(white: 0.8, alpha: 1.0), + roughness: .float(Float(roughness)), isMetallic: metallic) + for entity in pools.values { + entity.model?.materials = [material] + } + } + private func buildMesh(_ verts: [SIMD3]) -> MeshResource? { guard verts.count == Self.vertexCount, !faces.isEmpty else { diff --git a/avlivebody-mac/Sources/AVLiveBody/SceneController.swift b/avlivebody-mac/Sources/AVLiveBody/SceneController.swift index b4254a6..b5ebf90 100644 --- a/avlivebody-mac/Sources/AVLiveBody/SceneController.swift +++ b/avlivebody-mac/Sources/AVLiveBody/SceneController.swift @@ -16,6 +16,10 @@ final class SceneController { 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? @@ -33,6 +37,25 @@ final class SceneController { arView.environment.background = .color(.black) 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(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(-2, 1, -2)) + fillA.addChild(fillLight) + arView.scene.addAnchor(fillA) + + rimLight.light.intensity = 2000 + let rimA = AnchorEntity(world: SIMD3(0, 1, -5)) + rimA.addChild(rimLight) + arView.scene.addAnchor(rimA) + camera.camera.fieldOfViewInDegrees = 55 cameraAnchor.addChild(camera) arView.scene.addAnchor(cameraAnchor) @@ -65,6 +88,25 @@ final class SceneController { mesh?.update(persons) } + 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() { diff --git a/avlivebody-mac/Sources/AVLiveBody/SkeletonEntity.swift b/avlivebody-mac/Sources/AVLiveBody/SkeletonEntity.swift index 748bbb2..bb3e9b6 100644 --- a/avlivebody-mac/Sources/AVLiveBody/SkeletonEntity.swift +++ b/avlivebody-mac/Sources/AVLiveBody/SkeletonEntity.swift @@ -18,6 +18,8 @@ final class SkeletonEntity { private let material = SimpleMaterial( color: NSColor.systemYellow, roughness: 0.6, isMetallic: false) + func setVisible(_ visible: Bool) { root.isEnabled = visible } + func update(_ skeletons: [Int: SkeletonPayload]) { // Drop pools for pids no longer present. for pid in pools.keys where skeletons[pid] == nil { diff --git a/avlivebody-mac/Sources/AVLiveBody/VideoQuad.swift b/avlivebody-mac/Sources/AVLiveBody/VideoQuad.swift index 1b3f233..d3c13cf 100644 --- a/avlivebody-mac/Sources/AVLiveBody/VideoQuad.swift +++ b/avlivebody-mac/Sources/AVLiveBody/VideoQuad.swift @@ -1,3 +1,4 @@ +import AppKit import CoreImage import CoreVideo import Foundation @@ -10,6 +11,7 @@ final class VideoQuad { let entity = ModelEntity() private let ciContext = CIContext() + private var opacity: Double = 1.0 /// Plane is 1.6 m wide, 16:9; positioned 2 m behind the body. private static let width: Float = 1.6 private static let height: Float = 0.9 @@ -26,6 +28,17 @@ final class VideoQuad { SIMD3(0, 0, Self.zBack) } + func setVisible(_ visible: Bool) { entity.isEnabled = visible } + + func setOpacity(_ value: Double) { + opacity = value + // Re-tint current material; `update(_:)` will also use `opacity`. + if var mat = entity.model?.materials.first as? UnlitMaterial { + mat.color = .init(tint: NSColor(white: 1, alpha: CGFloat(value))) + entity.model?.materials = [mat] + } + } + /// Replace the plane's texture from a decoded camera frame. func update(_ pixelBuffer: CVPixelBuffer) { let ci = CIImage(cvPixelBuffer: pixelBuffer) @@ -39,8 +52,9 @@ final class VideoQuad { return } var material = UnlitMaterial() - material.color = .init(tint: .white, - texture: .init(texture)) + material.color = .init( + tint: NSColor(white: 1, alpha: CGFloat(opacity)), + texture: .init(texture)) entity.model?.materials = [material] } }