File size: 3,168 Bytes
d9bb6b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/**
 * A placeholder wing-flap frequency controller — same spirit as
 * tripodGait.ts driving real leg joints with a procedural placeholder gait:
 * this drives the real wing-hinge roll joints (rigMetadata.ts) from
 * flyBrain.ts's motor output, not real indirect-flight-muscle dynamics.
 *
 * Each side is independent, fed by that side's own real DNp01 neuron
 * (flyBrain.ts's motorOutput) rather than one averaged signal for both.
 *
 * Two things this is deliberately NOT:
 * - Not a constant idle buzz. A first draft kept a small always-on
 *   frequency/amplitude floor so the wings never looked "dead." Instead,
 *   the raw per-frame motor output is low-pass filtered into a smoothed
 *   drive, and both frequency and amplitude scale directly off of it with
 *   no floor — at genuine rest, drive decays to ~0 and the wing actually
 *   stops, rather than trembling forever.
 * - Not a symmetric oscillation about the folded rest pose. The wing's own
 *   roll axis is such that a NEGATIVE angle raises it and a POSITIVE angle
 *   swings it toward/into the body (verified live while fixing the "wing
 *   moves the wrong direction" bug) — a plain sin() wave would drive the
 *   body-ward half of every cycle straight into the mesh. A first pass
 *   scaled that half down by a fraction, which still clipped visibly at
 *   high drive; the positive half is now hard-clamped to 0 instead — the
 *   downstroke bottoms out at the neutral folded pose (never past it, at
 *   any drive level) and only the raise (negative) half is ever nonzero.
 */

export interface WingSideState {
  phase: number;
  smoothedDrive: number;
}

export interface WingControllerState {
  left: WingSideState;
  right: WingSideState;
}

export interface WingFlapOutput {
  left: number;
  right: number;
}

const LOW_PASS_TAU_S = 0.15; // smooths raw per-frame brightness spikes into a continuous drive
// Below this, frequency snaps to exactly 0 -- genuinely still, not just faint. Exported so
// flyBuzz.ts can treat "silent" identically for the audible buzz and the visible wing.
export const DRIVE_THRESHOLD = 0.05;
const MAX_HZ = 8; // frequency at full smoothed drive; visible on-screen, NOT the real ~200Hz wingbeat
const MAX_AMPLITUDE_RAD = 1.4; // matches FlyInstances' MAX_WING_LIFT_RAD, already live-verified

export function createWingControllerState(): WingControllerState {
  return {
    left: { phase: 0, smoothedDrive: 0 },
    right: { phase: 0, smoothedDrive: 0 },
  };
}

function stepSide(state: WingSideState, rawDrive: number, dt: number): number {
  state.smoothedDrive += (rawDrive - state.smoothedDrive) * (1 - Math.exp(-dt / LOW_PASS_TAU_S));

  const freq = state.smoothedDrive > DRIVE_THRESHOLD ? state.smoothedDrive * MAX_HZ : 0;
  state.phase += freq * Math.PI * 2 * dt;

  const angle = Math.sin(state.phase) * state.smoothedDrive * MAX_AMPLITUDE_RAD;
  return Math.min(angle, 0);
}

export function stepWingController(
  state: WingControllerState,
  leftDrive: number,
  rightDrive: number,
  dt: number,
): WingFlapOutput {
  return {
    left: stepSide(state.left, leftDrive, dt),
    right: stepSide(state.right, rightDrive, dt),
  };
}