Zum Inhalt

native/macos/KuroNativeSaver/Core/CameraFly.swift

// CameraFly — reusable "banked weaving flight": multi-frequency lateral + vertical
// sway with roll/yaw that bank INTO the turn, plus occasional eased maneuvers
// (hard-bank / dive / climb). Designed to be CONTINUOUS (C1): the bank is derived
// only from the smooth weave velocity, and maneuvers use a sin²(πp) envelope whose
// value AND slope are zero at both ends — so events never snap the roll (no "hard
// cuts"). Uses a per-instance LOCAL clock so every scene starts centered + LEVEL
// and eases the bank in. bankScale exposes the user's bank-strength slider.

import simd

struct CameraFly {
    var latAmp: Float = 10
    var vertAmp: Float = 2.4
    var vertBase: Float = 6.5
    var pitchBase: Float = -0.2
    var bankGain: Float = 2.2
    var yawGain: Float = 0.8
    var bankScale: Float = 1        // user bank-strength multiplier

    private var rng: LCG
    private var t0: Double = -1      // local clock origin (first update)
    private var nextEventT: Double
    private var event: (kind: Int, start: Double, dur: Double, dir: Float)?

    init(seed: Int32, latAmp: Float = 10, vertAmp: Float = 2.4, vertBase: Float = 6.5,
         pitchBase: Float = -0.2, bankScale: Float = 1) {
        rng = LCG(seed: seed)
        nextEventT = 6 + Double(rng.nextF()) * 8
        self.latAmp = latAmp; self.vertAmp = vertAmp; self.vertBase = vertBase
        self.pitchBase = pitchBase; self.bankScale = bankScale
    }

    struct Sample { var x: Float; var y: Float; var pitch: Float; var yaw: Float; var roll: Float }

    private func smooth01(_ u: Float) -> Float { let c = max(0, min(1, u)); return c * c * (3 - 2 * c) }

    mutating func update(t: Double, forwardSpeed: Float) -> Sample {
        if t0 < 0 { t0 = t }
        let lt = t - t0                 // local time → scene starts centered + level
        let tf = Float(lt)

        // continuous weave (position + analytic velocity, both C∞)
        var x = sin(tf * 0.11) * latAmp + sin(tf * 0.043) * latAmp * 0.5
        let xv = 0.11 * latAmp * cos(tf * 0.11) + 0.043 * latAmp * 0.5 * cos(tf * 0.043)
        var y = vertBase + sin(tf * 0.17) * vertAmp + sin(tf * 0.063) * vertAmp * 0.5
        var pitch = pitchBase + sin(tf * 0.13) * 0.045
        var extraRoll: Float = 0

        // occasional eased maneuver (sin²(πp): zero value AND zero slope at both ends)
        if event == nil, lt > nextEventT {
            let kind = Int(rng.nextF() * 3)             // 0 hard-bank · 1 dive · 2 climb
            let dir: Float = rng.nextF() < 0.5 ? 1 : -1
            event = (kind, lt, Double(2.8 + rng.nextF() * 2.4), dir)
            nextEventT = lt + Double(12 + rng.nextF() * 16)
        }
        if let e = event {
            let p = Float((lt - e.start) / e.dur)
            if p >= 1 { event = nil }
            else {
                let s = sin(p * .pi); let env = s * s   // C1
                switch e.kind {
                case 0: x += e.dir * latAmp * 0.5 * env; extraRoll += e.dir * 0.3 * env   // lean direct (no vel-bank → no snap)
                case 1: y -= 3.2 * env; pitch -= 0.13 * env
                default: y += 4.0 * env; pitch += 0.10 * env
                }
            }
        }

        let turn = max(-0.4, min(0.4, -xv / max(1, forwardSpeed)))
        let ramp = smooth01(tf / 3.5)                   // start LEVEL, ease the bank in
        let bank = bankScale * ramp
        return Sample(x: x, y: y, pitch: pitch, yaw: turn * yawGain * bank, roll: (turn * bankGain + extraRoll) * bank)
    }
}