View the figure from a slight elevation and refine the leg-machine rollers
The default camera pitches down 10 degrees, so the floor reads as a plane (drawn as a rectangle) and near/far contacts straddle it. Elevation is pure presentation - IK pins solve in the flat authored view and the posed body tilts, the same pattern as the orbit, so authored canvas targets never go out of reach. The leg-extension roller moves up onto the shin above the ankle and the leg-curl roller tucks under the heel. Fixtures and reference test values regenerated for the pitched camera. Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
This commit is contained in:
@@ -279,9 +279,12 @@ enum MotionSolver {
|
||||
return [attach, knee, ankleJoint, toe]
|
||||
}
|
||||
|
||||
/// FK a normalized frame into view space through the camera yaw.
|
||||
static func pose(_ nf: NormalizedFrame, prof: SkeletonProfile, cam: Double) -> FigurePose {
|
||||
let fRoot = chain(Mat3.rotY(-cam), Mat3.rotY(nf.yaw), Mat3.rotZ(-nf.pitch), Mat3.rotX(nf.roll))
|
||||
/// FK a normalized frame into view space through the camera yaw; `camPitch`
|
||||
/// tilts the viewpoint down from slightly above (the scene rotates about the root).
|
||||
static func pose(_ nf: NormalizedFrame, prof: SkeletonProfile, cam: Double,
|
||||
camPitch: Double = 0) -> FigurePose {
|
||||
let fRoot = chain(Mat3.rotX(camPitch), Mat3.rotY(-cam),
|
||||
Mat3.rotY(nf.yaw), Mat3.rotZ(-nf.pitch), Mat3.rotX(nf.roll))
|
||||
let origin = Vec3(0, 0, 0)
|
||||
let s1 = nf.spine[0], s2 = nf.spine[1]
|
||||
let f1 = chain(fRoot, Mat3.rotZ(-s1.flexion), Mat3.rotX(s1.lateral), Mat3.rotY(-s1.rotation))
|
||||
@@ -397,8 +400,15 @@ enum MotionSolver {
|
||||
/// restored afterward), the depth-sorted draw order (head last), the spine curve,
|
||||
/// and the foreshortened gaze nose tick (hidden when the face points at/away from
|
||||
/// the camera).
|
||||
static func frameGeometry(_ nf: NormalizedFrame, prof: SkeletonProfile, cam: Double) -> (NormalizedFrame, FigureGeometry) {
|
||||
let p0 = pose(nf, prof: prof, cam: cam)
|
||||
/// The standard slightly-elevated viewpoint: the camera pitches down a touch so
|
||||
/// the floor reads as a plane. Kept 1:1 with the reference renderer's CAMERA_PITCH.
|
||||
static let defaultPitch: Double = 10
|
||||
/// Floor plane half-depth; its projected height is `floorHalfDepth · sin(pitch)`.
|
||||
static let floorHalfDepth: Double = 30
|
||||
|
||||
static func frameGeometry(_ nf: NormalizedFrame, prof: SkeletonProfile, cam: Double,
|
||||
pitch: Double = MotionSolver.defaultPitch) -> (NormalizedFrame, FigureGeometry) {
|
||||
let p0 = pose(nf, prof: prof, cam: cam, camPitch: pitch)
|
||||
var shade: [FigureLimb: Shade] = [:]
|
||||
for (right, left) in pairs {
|
||||
guard let rp = p0.limbs[right], let lp = p0.limbs[left] else { continue }
|
||||
@@ -421,8 +431,14 @@ enum MotionSolver {
|
||||
work.pins[limb.pinKey] = CGPoint(x: pin.x - off.x, y: pin.y - off.y)
|
||||
}
|
||||
}
|
||||
// Pins are canvas targets in the authored, unpitched view: solve IK flat,
|
||||
// then tilt the *posed* body - the camera elevation is pure presentation,
|
||||
// so contacts straddle the floor plane instead of pins going out of reach.
|
||||
var (resolved, p) = resolve(work, prof: prof, cam: cam)
|
||||
resolved.pins = nf.pins // keep authored pins; only angles resolved
|
||||
if pitch != 0 {
|
||||
p = pose(resolved, prof: prof, cam: cam, camPitch: pitch)
|
||||
}
|
||||
|
||||
let anchor = nf.rootPos
|
||||
func scr(_ v: Vec3, _ limbOffset: CGPoint = .zero) -> CGPoint {
|
||||
@@ -476,18 +492,21 @@ struct MotionTimeline {
|
||||
let resolved: [NormalizedFrame]
|
||||
let profile: SkeletonProfile
|
||||
let cam: Double
|
||||
let pitch: Double
|
||||
let duration: Double
|
||||
|
||||
init?(motion: ExerciseMotion, profile: SkeletonProfile) {
|
||||
let cam = motion.camera?.yaw ?? 0
|
||||
let pitch = motion.camera?.pitch ?? MotionSolver.defaultPitch
|
||||
let norms = motion.frames.map { MotionSolver.normalize($0) }
|
||||
guard !norms.isEmpty else { return nil }
|
||||
let resolved = norms.map { MotionSolver.frameGeometry($0, prof: profile, cam: cam).0 }
|
||||
let resolved = norms.map { MotionSolver.frameGeometry($0, prof: profile, cam: cam, pitch: pitch).0 }
|
||||
let duration = resolved.reduce(0) { $0 + $1.hold + $1.tween }
|
||||
guard duration > 0 else { return nil }
|
||||
self.resolved = resolved
|
||||
self.profile = profile
|
||||
self.cam = cam
|
||||
self.pitch = pitch
|
||||
self.duration = duration
|
||||
}
|
||||
|
||||
@@ -516,10 +535,10 @@ struct MotionTimeline {
|
||||
func geometry(at time: Double, yawOffset: Double = 0) -> FigureGeometry {
|
||||
let frame = frame(at: time)
|
||||
guard yawOffset != 0 else {
|
||||
return MotionSolver.frameGeometry(frame, prof: profile, cam: cam).1
|
||||
return MotionSolver.frameGeometry(frame, prof: profile, cam: cam, pitch: pitch).1
|
||||
}
|
||||
var posed = MotionSolver.frameGeometry(frame, prof: profile, cam: cam).0
|
||||
var posed = MotionSolver.frameGeometry(frame, prof: profile, cam: cam, pitch: pitch).0
|
||||
posed.pins = [:]
|
||||
return MotionSolver.frameGeometry(posed, prof: profile, cam: cam + yawOffset).1
|
||||
return MotionSolver.frameGeometry(posed, prof: profile, cam: cam + yawOffset, pitch: pitch).1
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user