@bitefoundry/retrieviewer 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ASSET-PROVENANCE.md +6 -0
- package/ASSETS-LICENSE.md +2 -1
- package/ATTRIBUTION.md +6 -1
- package/COMFORT-ART.md +65 -0
- package/DESKTOP-PLATFORMS.md +6 -1
- package/PLUGIN.md +13 -10
- package/PREREQUISITES.md +95 -0
- package/PUPPY-MEMORY.md +102 -1
- package/README.ko.md +110 -0
- package/README.md +117 -59
- package/RELEASING.md +23 -12
- package/THIRD-PARTY-NOTICES.md +5 -3
- package/braille.mjs +35 -9
- package/examples/live.mjs +2 -2
- package/integrations/linux-overlay/overlay.py +2 -2
- package/integrations/macos-overlay/overlay.jxa +8 -6
- package/integrations/opencode/runtime.mjs +4 -4
- package/integrations/opencode/tui.tsx +1 -1
- package/integrations/windows-overlay/Overlay.cs +4 -4
- package/integrations/windows-overlay/protocol.mjs +11 -7
- package/package.json +34 -13
- package/puppy-comfort-sprites.mjs +25 -0
- package/puppy-companion-comfort-q5.bin.gz +0 -0
- package/puppy-companion-comfort.json +77 -0
- package/puppy-companion-life.mjs +49 -16
- package/puppy-companion-objects.mjs +12 -0
- package/puppy-frame-predictive.mjs +15 -9
- package/puppy-frame-store.mjs +2 -2
- package/puppy-paper-world.mjs +14 -0
- package/puppy-paper.mjs +65 -14
- package/retrieviewer-plugin.d.mts +1 -1
- package/retrieviewer-plugin.mjs +1 -1
- package/retrieviewer.d.mts +10 -4
- package/retrieviewer.mjs +20 -14
- package/locomotion-core.mjs +0 -21
- package/locomotion-puppy.animal +0 -0
- package/vendor/procedural-animals/package.json +0 -40
- package/vendor/procedural-animals/src/animal.js +0 -146
- package/vendor/procedural-animals/src/bake.js +0 -51
- package/vendor/procedural-animals/src/build.js +0 -81
- package/vendor/procedural-animals/src/core/build/coatKit.js +0 -121
- package/vendor/procedural-animals/src/core/build/featherCards.js +0 -189
- package/vendor/procedural-animals/src/core/build/finEdges.js +0 -85
- package/vendor/procedural-animals/src/core/build/furSlope.js +0 -115
- package/vendor/procedural-animals/src/core/build/mesh.js +0 -67
- package/vendor/procedural-animals/src/core/build/pipeline.js +0 -272
- package/vendor/procedural-animals/src/core/build/seams.js +0 -183
- package/vendor/procedural-animals/src/core/build/warp.js +0 -112
- package/vendor/procedural-animals/src/core/build/weights.js +0 -393
- package/vendor/procedural-animals/src/core/math/vec.js +0 -81
- package/vendor/procedural-animals/src/core/motion/actions.js +0 -800
- package/vendor/procedural-animals/src/core/motion/bird.js +0 -3063
- package/vendor/procedural-animals/src/core/motion/birdActions.js +0 -1059
- package/vendor/procedural-animals/src/core/motion/common.js +0 -158
- package/vendor/procedural-animals/src/core/motion/index.js +0 -22
- package/vendor/procedural-animals/src/core/motion/quadruped.js +0 -2875
- package/vendor/procedural-animals/src/core/motion/snake.js +0 -1594
- package/vendor/procedural-animals/src/core/motion/spider.js +0 -1810
- package/vendor/procedural-animals/src/core/motion/swimmer.js +0 -1047
- package/vendor/procedural-animals/src/core/motion/swimmerActions.js +0 -613
- package/vendor/procedural-animals/src/core/render/animalObject.js +0 -262
- package/vendor/procedural-animals/src/core/render/coatMaterial.js +0 -1301
- package/vendor/procedural-animals/src/core/render/dqs.js +0 -159
- package/vendor/procedural-animals/src/core/render/eyes.js +0 -676
- package/vendor/procedural-animals/src/core/render/membranes.js +0 -77
- package/vendor/procedural-animals/src/core/render/skeleton.js +0 -41
- package/vendor/procedural-animals/src/core/rig/bird.js +0 -327
- package/vendor/procedural-animals/src/core/rig/quadruped.js +0 -76
- package/vendor/procedural-animals/src/core/rig/rig.js +0 -79
- package/vendor/procedural-animals/src/core/rig/snake.js +0 -76
- package/vendor/procedural-animals/src/core/rig/spider.js +0 -110
- package/vendor/procedural-animals/src/core/rig/swimmer.js +0 -181
- package/vendor/procedural-animals/src/core/sdf/eyeSocket.js +0 -53
- package/vendor/procedural-animals/src/core/sdf/mesher.js +0 -238
- package/vendor/procedural-animals/src/core/sdf/sdf.js +0 -263
- package/vendor/procedural-animals/src/index.d.ts +0 -98
- package/vendor/procedural-animals/src/index.js +0 -29
- package/vendor/procedural-animals/src/registry.js +0 -48
- package/vendor/procedural-animals/src/species/bear/behaviour.js +0 -463
- package/vendor/procedural-animals/src/species/bear/claws.js +0 -147
- package/vendor/procedural-animals/src/species/bear/coat.js +0 -726
- package/vendor/procedural-animals/src/species/bear/index.js +0 -139
- package/vendor/procedural-animals/src/species/bear/motion.js +0 -112
- package/vendor/procedural-animals/src/species/bear/regions.js +0 -78
- package/vendor/procedural-animals/src/species/bear/rig.js +0 -112
- package/vendor/procedural-animals/src/species/bear/sculpt.js +0 -272
- package/vendor/procedural-animals/src/species/boar/behaviour.js +0 -335
- package/vendor/procedural-animals/src/species/boar/coat.js +0 -372
- package/vendor/procedural-animals/src/species/boar/index.js +0 -110
- package/vendor/procedural-animals/src/species/boar/motion.js +0 -85
- package/vendor/procedural-animals/src/species/boar/regions.js +0 -88
- package/vendor/procedural-animals/src/species/boar/rig.js +0 -111
- package/vendor/procedural-animals/src/species/boar/sculpt.js +0 -285
- package/vendor/procedural-animals/src/species/cat/behaviour.js +0 -559
- package/vendor/procedural-animals/src/species/cat/coat.js +0 -841
- package/vendor/procedural-animals/src/species/cat/index.js +0 -211
- package/vendor/procedural-animals/src/species/cat/motion.js +0 -101
- package/vendor/procedural-animals/src/species/cat/regions.js +0 -82
- package/vendor/procedural-animals/src/species/cat/rig.js +0 -100
- package/vendor/procedural-animals/src/species/cat/sculpt.js +0 -266
- package/vendor/procedural-animals/src/species/cat/whiskers.js +0 -110
- package/vendor/procedural-animals/src/species/cheetah/coat.js +0 -427
- package/vendor/procedural-animals/src/species/cheetah/index.js +0 -51
- package/vendor/procedural-animals/src/species/cheetah/joints.js +0 -3
- package/vendor/procedural-animals/src/species/cheetah/motion.js +0 -77
- package/vendor/procedural-animals/src/species/cheetah/regions.js +0 -95
- package/vendor/procedural-animals/src/species/cheetah/rig.js +0 -66
- package/vendor/procedural-animals/src/species/cheetah/sculpt.js +0 -158
- package/vendor/procedural-animals/src/species/chicken/coat.js +0 -313
- package/vendor/procedural-animals/src/species/chicken/coverts.js +0 -320
- package/vendor/procedural-animals/src/species/chicken/index.js +0 -110
- package/vendor/procedural-animals/src/species/chicken/motion.js +0 -177
- package/vendor/procedural-animals/src/species/chicken/regions.js +0 -66
- package/vendor/procedural-animals/src/species/chicken/rig.js +0 -144
- package/vendor/procedural-animals/src/species/chicken/sculpt.js +0 -389
- package/vendor/procedural-animals/src/species/chicken/wingLook.js +0 -66
- package/vendor/procedural-animals/src/species/chicken/wingStack.js +0 -124
- package/vendor/procedural-animals/src/species/cow/behaviour.js +0 -521
- package/vendor/procedural-animals/src/species/cow/coat.js +0 -465
- package/vendor/procedural-animals/src/species/cow/index.js +0 -118
- package/vendor/procedural-animals/src/species/cow/motion.js +0 -81
- package/vendor/procedural-animals/src/species/cow/regions.js +0 -98
- package/vendor/procedural-animals/src/species/cow/rig.js +0 -116
- package/vendor/procedural-animals/src/species/cow/sculpt.js +0 -339
- package/vendor/procedural-animals/src/species/crow/coat.js +0 -145
- package/vendor/procedural-animals/src/species/crow/index.js +0 -70
- package/vendor/procedural-animals/src/species/crow/joints.js +0 -3
- package/vendor/procedural-animals/src/species/crow/motion.js +0 -101
- package/vendor/procedural-animals/src/species/crow/regions.js +0 -50
- package/vendor/procedural-animals/src/species/crow/rig.js +0 -83
- package/vendor/procedural-animals/src/species/crow/sculpt.js +0 -118
- package/vendor/procedural-animals/src/species/deer/behaviour.js +0 -513
- package/vendor/procedural-animals/src/species/deer/coat.js +0 -391
- package/vendor/procedural-animals/src/species/deer/index.js +0 -106
- package/vendor/procedural-animals/src/species/deer/motion.js +0 -97
- package/vendor/procedural-animals/src/species/deer/regions.js +0 -60
- package/vendor/procedural-animals/src/species/deer/rig.js +0 -124
- package/vendor/procedural-animals/src/species/deer/sculpt.js +0 -297
- package/vendor/procedural-animals/src/species/dog/behaviour.js +0 -521
- package/vendor/procedural-animals/src/species/dog/coat.js +0 -495
- package/vendor/procedural-animals/src/species/dog/index.js +0 -144
- package/vendor/procedural-animals/src/species/dog/motion.js +0 -118
- package/vendor/procedural-animals/src/species/dog/regions.js +0 -61
- package/vendor/procedural-animals/src/species/dog/rig.js +0 -129
- package/vendor/procedural-animals/src/species/dog/sculpt.js +0 -246
- package/vendor/procedural-animals/src/species/eagle/coat.js +0 -250
- package/vendor/procedural-animals/src/species/eagle/index.js +0 -76
- package/vendor/procedural-animals/src/species/eagle/joints.js +0 -3
- package/vendor/procedural-animals/src/species/eagle/motion.js +0 -137
- package/vendor/procedural-animals/src/species/eagle/regions.js +0 -57
- package/vendor/procedural-animals/src/species/eagle/rig.js +0 -107
- package/vendor/procedural-animals/src/species/eagle/sculpt.js +0 -137
- package/vendor/procedural-animals/src/species/fish/coat.js +0 -271
- package/vendor/procedural-animals/src/species/fish/index.js +0 -88
- package/vendor/procedural-animals/src/species/fish/motion.js +0 -92
- package/vendor/procedural-animals/src/species/fish/regions.js +0 -27
- package/vendor/procedural-animals/src/species/fish/rig.js +0 -143
- package/vendor/procedural-animals/src/species/fish/sculpt.js +0 -214
- package/vendor/procedural-animals/src/species/fish/variants.js +0 -126
- package/vendor/procedural-animals/src/species/fox/behaviour.js +0 -314
- package/vendor/procedural-animals/src/species/fox/coat.js +0 -444
- package/vendor/procedural-animals/src/species/fox/index.js +0 -120
- package/vendor/procedural-animals/src/species/fox/motion.js +0 -81
- package/vendor/procedural-animals/src/species/fox/regions.js +0 -59
- package/vendor/procedural-animals/src/species/fox/rig.js +0 -91
- package/vendor/procedural-animals/src/species/fox/sculpt.js +0 -202
- package/vendor/procedural-animals/src/species/frog/behaviour.js +0 -590
- package/vendor/procedural-animals/src/species/frog/coat.js +0 -305
- package/vendor/procedural-animals/src/species/frog/index.js +0 -97
- package/vendor/procedural-animals/src/species/frog/motion.js +0 -69
- package/vendor/procedural-animals/src/species/frog/regions.js +0 -46
- package/vendor/procedural-animals/src/species/frog/rig.js +0 -156
- package/vendor/procedural-animals/src/species/frog/sculpt.js +0 -240
- package/vendor/procedural-animals/src/species/goat/behaviour.js +0 -509
- package/vendor/procedural-animals/src/species/goat/coat.js +0 -590
- package/vendor/procedural-animals/src/species/goat/index.js +0 -149
- package/vendor/procedural-animals/src/species/goat/motion.js +0 -95
- package/vendor/procedural-animals/src/species/goat/regions.js +0 -98
- package/vendor/procedural-animals/src/species/goat/rig.js +0 -181
- package/vendor/procedural-animals/src/species/goat/sculpt.js +0 -547
- package/vendor/procedural-animals/src/species/horse/coat.js +0 -361
- package/vendor/procedural-animals/src/species/horse/coatSeam.js +0 -122
- package/vendor/procedural-animals/src/species/horse/index.js +0 -80
- package/vendor/procedural-animals/src/species/horse/motion.js +0 -245
- package/vendor/procedural-animals/src/species/horse/regions.js +0 -60
- package/vendor/procedural-animals/src/species/horse/rig.js +0 -117
- package/vendor/procedural-animals/src/species/horse/sculpt.js +0 -316
- package/vendor/procedural-animals/src/species/horse/seamWeights.js +0 -272
- package/vendor/procedural-animals/src/species/lion/behaviour.js +0 -509
- package/vendor/procedural-animals/src/species/lion/coat.js +0 -1016
- package/vendor/procedural-animals/src/species/lion/index.js +0 -111
- package/vendor/procedural-animals/src/species/lion/motion.js +0 -87
- package/vendor/procedural-animals/src/species/lion/regions.js +0 -106
- package/vendor/procedural-animals/src/species/lion/rig.js +0 -110
- package/vendor/procedural-animals/src/species/lion/sculpt.js +0 -384
- package/vendor/procedural-animals/src/species/lion/whiskers.js +0 -132
- package/vendor/procedural-animals/src/species/pig/behaviour.js +0 -575
- package/vendor/procedural-animals/src/species/pig/coat.js +0 -730
- package/vendor/procedural-animals/src/species/pig/ears.js +0 -242
- package/vendor/procedural-animals/src/species/pig/index.js +0 -161
- package/vendor/procedural-animals/src/species/pig/motion.js +0 -105
- package/vendor/procedural-animals/src/species/pig/regions.js +0 -100
- package/vendor/procedural-animals/src/species/pig/rig.js +0 -152
- package/vendor/procedural-animals/src/species/pig/sculpt.js +0 -382
- package/vendor/procedural-animals/src/species/rabbit/behaviour.js +0 -605
- package/vendor/procedural-animals/src/species/rabbit/coat.js +0 -285
- package/vendor/procedural-animals/src/species/rabbit/index.js +0 -85
- package/vendor/procedural-animals/src/species/rabbit/motion.js +0 -85
- package/vendor/procedural-animals/src/species/rabbit/regions.js +0 -48
- package/vendor/procedural-animals/src/species/rabbit/rig.js +0 -99
- package/vendor/procedural-animals/src/species/rabbit/sculpt.js +0 -148
- package/vendor/procedural-animals/src/species/rat/behaviour.js +0 -539
- package/vendor/procedural-animals/src/species/rat/coat.js +0 -341
- package/vendor/procedural-animals/src/species/rat/index.js +0 -111
- package/vendor/procedural-animals/src/species/rat/motion.js +0 -90
- package/vendor/procedural-animals/src/species/rat/regions.js +0 -92
- package/vendor/procedural-animals/src/species/rat/rig.js +0 -112
- package/vendor/procedural-animals/src/species/rat/sculpt.js +0 -163
- package/vendor/procedural-animals/src/species/rat/whiskers.js +0 -119
- package/vendor/procedural-animals/src/species/shark/coat.js +0 -383
- package/vendor/procedural-animals/src/species/shark/index.js +0 -104
- package/vendor/procedural-animals/src/species/shark/motion.js +0 -69
- package/vendor/procedural-animals/src/species/shark/regions.js +0 -34
- package/vendor/procedural-animals/src/species/shark/rig.js +0 -194
- package/vendor/procedural-animals/src/species/shark/sculpt.js +0 -343
- package/vendor/procedural-animals/src/species/shark/variants.js +0 -105
- package/vendor/procedural-animals/src/species/sheep/behaviour.js +0 -522
- package/vendor/procedural-animals/src/species/sheep/coat.js +0 -529
- package/vendor/procedural-animals/src/species/sheep/index.js +0 -152
- package/vendor/procedural-animals/src/species/sheep/motion.js +0 -86
- package/vendor/procedural-animals/src/species/sheep/neck.js +0 -41
- package/vendor/procedural-animals/src/species/sheep/regions.js +0 -62
- package/vendor/procedural-animals/src/species/sheep/rig.js +0 -163
- package/vendor/procedural-animals/src/species/sheep/sculpt.js +0 -542
- package/vendor/procedural-animals/src/species/snake/coat.js +0 -315
- package/vendor/procedural-animals/src/species/snake/index.js +0 -73
- package/vendor/procedural-animals/src/species/snake/motion.js +0 -51
- package/vendor/procedural-animals/src/species/snake/regions.js +0 -51
- package/vendor/procedural-animals/src/species/snake/rig.js +0 -108
- package/vendor/procedural-animals/src/species/snake/sculpt.js +0 -122
- package/vendor/procedural-animals/src/species/spider/coat.js +0 -279
- package/vendor/procedural-animals/src/species/spider/index.js +0 -78
- package/vendor/procedural-animals/src/species/spider/motion.js +0 -72
- package/vendor/procedural-animals/src/species/spider/regions.js +0 -87
- package/vendor/procedural-animals/src/species/spider/rig.js +0 -118
- package/vendor/procedural-animals/src/species/spider/sculpt.js +0 -182
- package/vendor/procedural-animals/src/species/wolf/behaviour.js +0 -160
- package/vendor/procedural-animals/src/species/wolf/coat.js +0 -749
- package/vendor/procedural-animals/src/species/wolf/index.js +0 -107
- package/vendor/procedural-animals/src/species/wolf/motion.js +0 -80
- package/vendor/procedural-animals/src/species/wolf/regions.js +0 -57
- package/vendor/procedural-animals/src/species/wolf/rig.js +0 -87
- package/vendor/procedural-animals/src/species/wolf/sculpt.js +0 -339
- package/vendor/procedural-animals/src/worker.js +0 -29
|
@@ -1,3063 +0,0 @@
|
|
|
1
|
-
// Bird motion engine: bipedal walking and hopping, flapping and gliding flight in 3D, take-off,
|
|
2
|
-
// landing (on the ground or gripping a perch), and every standard action, for the standard bird
|
|
3
|
-
// skeleton of core/rig/bird.js (crow, chicken, eagle, ...).
|
|
4
|
-
//
|
|
5
|
-
// Ground: a two-leg gait engine (phase-offset table blended by speed: alternating walks and runs,
|
|
6
|
-
// two-footed hops with a real flight phase) plants the feet with terrain-aware swing paths; the leg
|
|
7
|
-
// IK keeps the femur near its rest angle inside the body and solves tibiotarsus + tarsometatarsus to
|
|
8
|
-
// the planted foot (the ankle points back), rolling the foot onto the toes at push-off; toes curl
|
|
9
|
-
// in swing and spread before touchdown. The body bobs from the gait phase (walk: highest at mid
|
|
10
|
-
// stance; hop: a ballistic arc between two compressions), the head holds still in space and thrusts
|
|
11
|
-
// forward once per step (head bob), the tail bobs and sways.
|
|
12
|
-
// Air: airspeed, heading (banked, coordinated turns) and climb rate follow the input with the
|
|
13
|
-
// species' limits; a power demand (slow flight, climb, acceleration) drives stroke amplitude and
|
|
14
|
-
// frequency between cruise and take-off beats, and fades them to a glide when descending; the body
|
|
15
|
-
// pitches with the flight path and bobs with each downstroke; wings follow a stroke cycle (elevation,
|
|
16
|
-
// sweep, pronation, flexed upstroke) whose feather fan follows the wrist extension; the tail fans and
|
|
17
|
-
// steers; the terrain is never closer than the clearance unless landing. A species may opt into a sleek
|
|
18
|
-
// cruise-flight carriage (flight.shape, below: off by default): the head carried forward and low on an
|
|
19
|
-
// extended neck, the neck, torso and thigh plumage sleeked, the tail fanned; eased in once airborne and
|
|
20
|
-
// out for the flare, after touchdown and in a fall.
|
|
21
|
-
// Transitions: take-off (crouch, leg jump, first downstroke at toe-off, steep climb with fast deep
|
|
22
|
-
// beats, legs tucked back), landing (a guided path to the touch point, flare: body pitched up, wings
|
|
23
|
-
// raised and swept forward for braking beats, tail fanned and depressed, legs thrown forward; the
|
|
24
|
-
// feet plant where they touch, the body absorbs, wings stay up briefly then fold), perching (land on
|
|
25
|
-
// a target point and grip it: toes wrap the branch).
|
|
26
|
-
// Neck: the bind neck is fitted with a cubic Bezier (end tangents from the joints); every pose bends
|
|
27
|
-
// that curve from the chest to the head (arc length solved from the chord: a short chord folds the S
|
|
28
|
-
// and telescopes the neck a little), so the rest pose reproduces the bind neck exactly; part of the
|
|
29
|
-
// gaze turns the whole neck. The head is gaze stabilised, moves in quick saccades toward what it
|
|
30
|
-
// looks at and cocks to look with one eye.
|
|
31
|
-
// Perches: play('perch', { target, axis?, radius? }) or createAnimal({ perches: [points] }) - the bird
|
|
32
|
-
// flies there (flying out and back round when the target is inside its turning circle), settles
|
|
33
|
-
// facing across the branch and grips it. LOD 2 (crowd): feathers and toes ride rigidly on their
|
|
34
|
-
// carrier bones between full solves (every 3rd frame), cheaper neck solve.
|
|
35
|
-
//
|
|
36
|
-
// Lessons kept from the cheetah: continuous solves (every target is a continuous function of state;
|
|
37
|
-
// nothing jumps between frames), soft IK limits, motion from phase (not from events), springs for
|
|
38
|
-
// secondary motion, zero-dt safety, no allocations in the per-frame paths.
|
|
39
|
-
//
|
|
40
|
-
// ------------------------------------------------------------------------------------------------
|
|
41
|
-
// species.motion schema (lengths in metres and speeds in m/s at the reference size; the engine scales
|
|
42
|
-
// them by params.size (lengths) and sqrt(size) (speeds, frequencies) - dynamic similarity)
|
|
43
|
-
// ------------------------------------------------------------------------------------------------
|
|
44
|
-
// gears { walk, hop|run, fly } named speeds (the fastest ground gear < ground.maxSpeed; `fly`
|
|
45
|
-
// is the cruise airspeed). move() above ground.maxSpeed (or climb >= 0.5 m/s) takes off.
|
|
46
|
-
// maxSpeed top airspeed
|
|
47
|
-
// ground { maxSpeed, accel: [slow, fast], decel, turnRate (rad/s) }
|
|
48
|
-
// gaits (req.) [{ name, v, f (stride Hz), D (duty), off: [L, R] touchdown phases ([0, 0.5] walk/run,
|
|
49
|
-
// [0, 0] hop), lift (swing height), bob (walk bounce), hop (hop flight height), sway
|
|
50
|
-
// (lateral), pitch (body pitch, deg, - = nose down), headBob (0..1), wings (0..1 half
|
|
51
|
-
// open for balance), tail (0..1 tail bob) }] sorted by v
|
|
52
|
-
// feet { spread: [hallux, inner, middle, outer] extra yaw (deg), curl (swing toe curl, deg),
|
|
53
|
-
// grip (0..1), pad (MTP height above the ground, m), hock (least ankle height above the
|
|
54
|
-
// ground, m, default 0.004: the radius of the intertarsal joint's skin), sole (least MTP
|
|
55
|
-
// height of a free foot (dead, in the air), m, default 0.8 pad), toeClear (m: toe joints
|
|
56
|
-
// kept this far above the terrain, default 0.0012), sitPad (m: MTP lift when sitting on the
|
|
57
|
-
// tarsi, default 0.004) }
|
|
58
|
-
// body { tilt (standing body axis above horizontal, deg), belly: { y, hh, hw } (belly section:
|
|
59
|
-
// centre height, half height, half width, bind) }
|
|
60
|
-
// head { bob (head-bob strength), yaw, pitchUp, pitchDown (look limits, rad), saccade (0..1),
|
|
61
|
-
// cock (head-cock roll when looking with one eye, rad), jawRest (deg: mandible angle
|
|
62
|
-
// that closes the bill at rest, - closes), radius (skull radius, m), bobHold (share of
|
|
63
|
-
// each step the walking head holds still in space, default 0.5) }
|
|
64
|
-
// tail { pitch (rest, deg), walkBob (deg), sway (deg), flick (0..1), fanRest (0..1), flight (share
|
|
65
|
-
// of the tail's rest angle above the body axis given up in flight, default 1), soarFan (0..1
|
|
66
|
-
// extra fan while gliding) }
|
|
67
|
-
// wing { lengths: [humerus, ulna, hand], bind, fold, glide: wing configurations (deg: elev,
|
|
68
|
-
// pitch, alpha, elbow, wrist, bend, twist; see core/rig/bird.js), flap: { freq, amp,
|
|
69
|
-
// mid, down, upFlex, upWrist, sweep, twist } cruise stroke, power: {...} take-off /
|
|
70
|
-
// braking stroke, glideShare (0..1 share of level flight spent gliding), soar (0..1),
|
|
71
|
-
// tilt (deg: the wing root frame follows a body axis pitched this much nose-up from the
|
|
72
|
-
// bind horizontal; upright birds set it to body.tilt so glide chord and stroke plane are
|
|
73
|
-
// level in flight), fingerLift (deg: emarginated primaries bend up while gliding),
|
|
74
|
-
// groundLift (deg: part-open wings on the ground are raised so long wing tips clear it),
|
|
75
|
-
// skin (m: wrist / wing-tip clearance above the terrain, default 0.006) }
|
|
76
|
-
// feathers { primaries, secondaries, rectrices, coverts } (see resolveFeathers in core/rig/bird.js)
|
|
77
|
-
// flight { minSpeed, cruise, maxSpeed, climb: [up, down] (m/s), bankMax (deg), turnRate,
|
|
78
|
-
// clearance (m above the terrain), glideRatio, sustained (false: lands after maxTime),
|
|
79
|
-
// maxTime (s), legTuck { back, down, side } (feet folded back along the body axis in
|
|
80
|
-
// flight, fractions of the leg length; default { back: 0.82, down: 0.04, side: 0.55 }),
|
|
81
|
-
// shape (optional, off by default: the crow and the chicken fly in their standing shape) }
|
|
82
|
-
// flight.shape { reach, drop, neck, body, back, legs, fan }: a long-necked, sleek cruise-flight carriage
|
|
83
|
-
// (raptors), eased in once airborne and out for the landing flare, after touchdown and in
|
|
84
|
-
// a fall (a critically damped weight, ~0.5 s):
|
|
85
|
-
// reach, drop shares of the neck's arc length: the head is carried forward along the
|
|
86
|
-
// levelled body axis (reach) and lowered toward the belly side (drop) on an
|
|
87
|
-
// extended neck, from its resting place on the chest (without them the head
|
|
88
|
-
// keeps its bind offset from the chest in flight, so an upright standing
|
|
89
|
-
// carriage holds it above the body line)
|
|
90
|
-
// neck 0..1: the neck's cross-section thins by this share (sleeked feathers)
|
|
91
|
-
// body [width, length, depth]: torso scale in flight (sleeked contour plumage), in
|
|
92
|
-
// the chest's and the pelvis's own bone frames (lateral, along the bone,
|
|
93
|
-
// across it), applied with the breathing / fluff scale; default [1, 1, 1]
|
|
94
|
-
// back m: the torso's flight scale is centred this far dorsal of the synsacrum (on
|
|
95
|
-
// the back): a depth < 1 lifts the belly toward the back instead of thinning
|
|
96
|
-
// the body about its middle; default 0
|
|
97
|
-
// legs 0..1: the thigh and trouser feathers sleek in flight: the femur's skin is
|
|
98
|
-
// drawn in toward the hip and the tibiotarsus's thinned about the bone by this
|
|
99
|
-
// share (an upright bird's thighs otherwise hang under the levelled belly)
|
|
100
|
-
// fan 0..1: the tail is fanned at least this much in flight
|
|
101
|
-
// takeoff { crouch (s), jump (m/s), angle (deg climb), beats, open (0..1: wings opened and raised
|
|
102
|
-
// during the crouch) }
|
|
103
|
-
// individual optional (params) => { ...top-level keys } per-individual overrides (sex, age)
|
|
104
|
-
// landing { approach (m/s), flare (s), touch (touchdown speed, m/s) }
|
|
105
|
-
// variants { <params.variant>: { <key>: {...} } } per-variant overrides, merged one level deep
|
|
106
|
-
// actions (full list in core/motion/birdActions.js)
|
|
107
|
-
// { attack: { style: 'peck' | 'lunge' | 'talons', mantle }, eat: { style: 'peck' | 'tear', reach, billPitch, bodyPitch, drop (peck pose),
|
|
108
|
-
// rate }, drink: {}, jump: { height, distance }, sleep: { style: 'tuck' | 'sink' } }
|
|
109
|
-
// ------------------------------------------------------------------------------------------------
|
|
110
|
-
import * as THREE from 'three';
|
|
111
|
-
import {
|
|
112
|
-
TAU, DEG, clamp, lerp, smooth, wrap01, d01, angDiff, smin, Spring, tv, tvc, tq, tqc, tqa,
|
|
113
|
-
UP, X1, Z1, ONE, prng, frameQuat, writeFrameC, mulAffine,
|
|
114
|
-
} from './common.js';
|
|
115
|
-
import { wingFK, makeWingFrames, featherFrame, rectrixFrame, makeFeatherFrame, resolveFeathers, extension, v3 } from '../rig/bird.js';
|
|
116
|
-
import { BirdActions } from './birdActions.js';
|
|
117
|
-
import { vaneWidth } from '../build/featherCards.js';
|
|
118
|
-
|
|
119
|
-
const G = 9.81;
|
|
120
|
-
const LEGS = ['L', 'R'];
|
|
121
|
-
const isDeathPosture = (p) => p.name === 'death';
|
|
122
|
-
const _m4 = new THREE.Matrix4(), _m4b = new THREE.Matrix4(), _m4c = new THREE.Matrix4(), _qD = new THREE.Quaternion();
|
|
123
|
-
const _sp = new THREE.Vector3(), _sq = new THREE.Quaternion(), _ss = new THREE.Vector3(), _st = new THREE.Vector3();
|
|
124
|
-
const NECK_GAZE_YAW = 0.35, NECK_GAZE_PITCH = 0.25; // share of the gaze turned by the whole neck
|
|
125
|
-
const ELEV_CAP = 60 * Math.PI / 180; // wing elevation above the horizontal: soft cap (see poseWings)
|
|
126
|
-
const DEFAULT_TUCK = { back: 0.82, down: 0.04, side: 0.55 }; // flight.legTuck default (see poseLegs)
|
|
127
|
-
const TOES = [1, 2, 3, 4];
|
|
128
|
-
const WING_KEYS = ['elev', 'pitch', 'alpha', 'elbow', 'wrist', 'bend', 'twist'];
|
|
129
|
-
const expTo = (x, target, dt, tau) => x + (target - x) * (1 - Math.exp(-dt / tau));
|
|
130
|
-
const smoother = (x) => { const t = clamp(x, 0, 1); return t * t * t * (t * (t * 6 - 15) + 10); };
|
|
131
|
-
|
|
132
|
-
// Ground support of the body, measured on the built skin (not a hand-entered section): the extreme
|
|
133
|
-
// vertices, in ~190 directions that can face the ground, of the skin dominated by the torso (pelvis,
|
|
134
|
-
// chest) and the thighs (femurs): the points of the skin's convex hull that a lying, sitting, rolled or
|
|
135
|
-
// fluffed body rests on. They keep their skin weights and are skinned like the renderer does (dual
|
|
136
|
-
// quaternions with the bind-space scale pre-pass) while the body rests. Cached per build data (a
|
|
137
|
-
// flock shares one build).
|
|
138
|
-
const SUPPORT_CACHE = new WeakMap();
|
|
139
|
-
function groundSupport(data, bi) {
|
|
140
|
-
if (SUPPORT_CACHE.has(data)) return SUPPORT_CACHE.get(data);
|
|
141
|
-
let out = null;
|
|
142
|
-
const pos = data.pos, SI = data.skinIndex, SW = data.skinWeight, coat = data.coat;
|
|
143
|
-
if (pos && SI && SW && bi.pelvis !== undefined && bi.chest !== undefined) {
|
|
144
|
-
const groups = [bi.pelvis, bi.chest, bi.femurL ?? -1, bi.femurR ?? -1];
|
|
145
|
-
const lists = groups.map(() => []);
|
|
146
|
-
for (let v = 0; v < data.nV; v++) {
|
|
147
|
-
if (coat && coat[v * 4 + 3] < 0.5) continue; // hidden half of a cross-fade band
|
|
148
|
-
const g = groups.indexOf(SI[v * 4]);
|
|
149
|
-
if (g >= 0) lists[g].push(v);
|
|
150
|
-
}
|
|
151
|
-
// directions (Fibonacci sphere) that can face the ground in any pose (a body rolled onto its side
|
|
152
|
-
// still has its dorsal half up)
|
|
153
|
-
const dirs = [];
|
|
154
|
-
const ND = 256;
|
|
155
|
-
for (let i = 0; i < ND; i++) {
|
|
156
|
-
const y = 1 - (2 * i + 1) / ND, r = Math.sqrt(1 - y * y), a = i * 2.399963229728653;
|
|
157
|
-
if (y < 0.4) dirs.push([Math.cos(a) * r, y, Math.sin(a) * r]);
|
|
158
|
-
}
|
|
159
|
-
const set = new Set();
|
|
160
|
-
let minY = Infinity;
|
|
161
|
-
for (let g = 0; g < 4; g++) {
|
|
162
|
-
for (const d of dirs) {
|
|
163
|
-
let best = -Infinity, bv = -1;
|
|
164
|
-
for (const v of lists[g]) { const s = pos[v * 3] * d[0] + pos[v * 3 + 1] * d[1] + pos[v * 3 + 2] * d[2]; if (s > best) { best = s; bv = v; } }
|
|
165
|
-
if (bv >= 0) set.add(bv);
|
|
166
|
-
}
|
|
167
|
-
if (g < 2) for (const v of lists[g]) minY = Math.min(minY, pos[v * 3 + 1]);
|
|
168
|
-
}
|
|
169
|
-
const verts = [...set];
|
|
170
|
-
if (verts.length) {
|
|
171
|
-
const n = verts.length, P = new Float64Array(n * 3), I = new Uint16Array(n * 4), W = new Float32Array(n * 4);
|
|
172
|
-
const bones = new Set();
|
|
173
|
-
verts.forEach((v, k) => {
|
|
174
|
-
for (let c = 0; c < 3; c++) P[k * 3 + c] = pos[v * 3 + c];
|
|
175
|
-
for (let c = 0; c < 4; c++) { I[k * 4 + c] = SI[v * 4 + c]; W[k * 4 + c] = SW[v * 4 + c]; if (SW[v * 4 + c] > 0) bones.add(SI[v * 4 + c]); }
|
|
176
|
-
});
|
|
177
|
-
out = { n, P, I, W, bones: Int32Array.from(bones), minY };
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
SUPPORT_CACHE.set(data, out);
|
|
181
|
-
return out;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const DEFAULTS = {
|
|
185
|
-
ground: { maxSpeed: 2, accel: [2.5, 4], decel: 5, turnRate: 4 },
|
|
186
|
-
feet: { spread: [0, 0, 0, 0], curl: 70, grip: 1, pad: 0.005 },
|
|
187
|
-
body: { tilt: 30 },
|
|
188
|
-
head: { bob: 0.6, yaw: 2.4, pitchUp: 0.9, pitchDown: 1.2, saccade: 1, cock: 0.3 },
|
|
189
|
-
tail: { pitch: 0, walkBob: 5, sway: 4, flick: 1, fanRest: 0.05 },
|
|
190
|
-
flight: { minSpeed: 5, cruise: 10, maxSpeed: 14, climb: [3, -5], bankMax: 55, turnRate: 2.2, clearance: 1.5, glideRatio: 7, sustained: true, maxTime: 6 },
|
|
191
|
-
takeoff: { crouch: 0.16, jump: 2.2, angle: 35, beats: 5 },
|
|
192
|
-
landing: { approach: 7, flare: 0.55, touch: 1.0 },
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
// Wing skin support: per wing bone (humerus, ulna, hand; L and R), the extreme vertices of its skin in
|
|
196
|
-
// ~40 directions, in the bone's bind-local frame, and the farthest one from the bone (m). Cached per
|
|
197
|
-
// build data.
|
|
198
|
-
const WING_SUP_CACHE = new WeakMap();
|
|
199
|
-
function wingSupport(data, bi, inverses) {
|
|
200
|
-
if (WING_SUP_CACHE.has(data)) return WING_SUP_CACHE.get(data);
|
|
201
|
-
let out = null;
|
|
202
|
-
const pos = data.pos, SI = data.skinIndex, SW = data.skinWeight, coat = data.coat;
|
|
203
|
-
const names = [['humerusL', 'ulnaL', 'handL'], ['humerusR', 'ulnaR', 'handR']];
|
|
204
|
-
if (pos && SI && names.every((n) => n.every((b) => bi[b] !== undefined))) {
|
|
205
|
-
const dirs = [];
|
|
206
|
-
for (let i = 0; i < 40; i++) { const y = 1 - (2 * i + 1) / 40, r = Math.sqrt(1 - y * y), a = i * 2.399963229728653; dirs.push([Math.cos(a) * r, y, Math.sin(a) * r]); }
|
|
207
|
-
out = names.map((side) => side.map((b) => {
|
|
208
|
-
const ib = bi[b], inv = inverses[ib].elements, list = [];
|
|
209
|
-
for (let v = 0; v < data.nV; v++) if (SI[v * 4] === ib && SW[v * 4] >= 0.5 && !(coat && coat[v * 4 + 3] < 0.5)) list.push(v);
|
|
210
|
-
const set = new Set();
|
|
211
|
-
for (const d of dirs) {
|
|
212
|
-
let best = -Infinity, bv = -1;
|
|
213
|
-
for (const v of list) { const sc = pos[v * 3] * d[0] + pos[v * 3 + 1] * d[1] + pos[v * 3 + 2] * d[2]; if (sc > best) { best = sc; bv = v; } }
|
|
214
|
-
if (bv >= 0) set.add(bv);
|
|
215
|
-
}
|
|
216
|
-
const pts = [];
|
|
217
|
-
let rMax = 0;
|
|
218
|
-
for (const v of set) {
|
|
219
|
-
const x = pos[v * 3], y = pos[v * 3 + 1], z = pos[v * 3 + 2];
|
|
220
|
-
const lx = inv[0] * x + inv[4] * y + inv[8] * z + inv[12], ly = inv[1] * x + inv[5] * y + inv[9] * z + inv[13], lz = inv[2] * x + inv[6] * y + inv[10] * z + inv[14];
|
|
221
|
-
pts.push(lx, ly, lz);
|
|
222
|
-
rMax = Math.max(rMax, Math.hypot(lx, lz)); // (bone frame: +Y along the bone)
|
|
223
|
-
}
|
|
224
|
-
// bounding sphere of the points (bone frame): centre of the box, farthest point from it
|
|
225
|
-
const c = [0, 1, 2].map((a) => { let lo = Infinity, hi = -Infinity; for (let k = a; k < pts.length; k += 3) { lo = Math.min(lo, pts[k]); hi = Math.max(hi, pts[k]); } return 0.5 * (lo + hi); });
|
|
226
|
-
let R = 0;
|
|
227
|
-
for (let k = 0; k < pts.length; k += 3) R = Math.max(R, Math.hypot(pts[k] - c[0], pts[k + 1] - c[1], pts[k + 2] - c[2]));
|
|
228
|
-
return { ib, pts: Float64Array.from(pts), rMax, c, R };
|
|
229
|
-
}));
|
|
230
|
-
}
|
|
231
|
-
WING_SUP_CACHE.set(data, out);
|
|
232
|
-
return out;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Head skin support: the skin that can meet the ground when the head is low (pecking, a bill wiped on
|
|
236
|
-
// the ground, a dead bird's head on its side): the extreme vertices, in 64 directions, of the skin the
|
|
237
|
-
// head and the lower mandible move (head + jaw weight >= 0.3), taken in the bind pose and with the head
|
|
238
|
-
// bent against the neck (pitched down and up, rolled, turned): a blended vertex (throat, chin) is not
|
|
239
|
-
// on the bind hull, but the neck's share of its weight drags it below the bill when the head bends.
|
|
240
|
-
// They keep their skin weights and are skinned like the renderer (dual quaternions). A bill wiped on
|
|
241
|
-
// the ground put a hen's wattles 6 mm and a crow's throat 9 mm into it: the fixed bill / chin points
|
|
242
|
-
// did not see them. Cached per build data.
|
|
243
|
-
const HEAD_SUP_CACHE = new WeakMap();
|
|
244
|
-
function headSupport(data, bi) {
|
|
245
|
-
if (HEAD_SUP_CACHE.has(data)) return HEAD_SUP_CACHE.get(data);
|
|
246
|
-
let out = null;
|
|
247
|
-
const pos = data.pos, SI = data.skinIndex, SW = data.skinWeight, coat = data.coat, J = data.joints;
|
|
248
|
-
const ih = bi.head, ij = bi.jaw ?? -1;
|
|
249
|
-
if (pos && SI && SW && ih !== undefined && J.occiput) {
|
|
250
|
-
const list = [], wl = [];
|
|
251
|
-
for (let v = 0; v < data.nV; v++) {
|
|
252
|
-
if (coat && coat[v * 4 + 3] < 0.5) continue;
|
|
253
|
-
let w = 0;
|
|
254
|
-
for (let c = 0; c < 4; c++) { const b = SI[v * 4 + c]; if (b === ih || b === ij) w += SW[v * 4 + c]; }
|
|
255
|
-
if (w >= 0.3) { list.push(v); wl.push(w); }
|
|
256
|
-
}
|
|
257
|
-
const O = J.occiput;
|
|
258
|
-
// configurations of the head (and jaw, rigidly) against the rest: rotations about the occiput
|
|
259
|
-
// (x: pitch, + bill down; z: roll; y: turn), blended with the identity by the head's weight (DQS)
|
|
260
|
-
const confs = [[0, 0, 0], [0.7, 0, 0], [1.4, 0, 0], [-0.7, 0, 0], [0.7, 0, 0.9], [0.7, 0, -0.9], [0.5, 0.9, 0], [0.5, -0.9, 0]];
|
|
261
|
-
const q = new THREE.Quaternion(), e = new THREE.Euler(), o = new THREE.Vector3(), t = new THREE.Vector3(), pp = new THREE.Vector3();
|
|
262
|
-
const qi = new THREE.Quaternion(), qb = new THREE.Quaternion();
|
|
263
|
-
const Xs = new Float64Array(list.length * 3);
|
|
264
|
-
const set = new Set();
|
|
265
|
-
for (const [ax, ay, az] of confs) {
|
|
266
|
-
q.setFromEuler(e.set(ax, ay, az, 'YXZ'));
|
|
267
|
-
// translation of the rotation about O: t = O - q O; dual part d = 0.5 (t, 0) q
|
|
268
|
-
o.set(O[0], O[1], O[2]); t.copy(o).applyQuaternion(q).negate().add(o);
|
|
269
|
-
const dq = [0.5 * (t.x * q.w + t.y * q.z - t.z * q.y), 0.5 * (t.y * q.w + t.z * q.x - t.x * q.z), 0.5 * (t.z * q.w + t.x * q.y - t.y * q.x), -0.5 * (t.x * q.x + t.y * q.y + t.z * q.z)];
|
|
270
|
-
for (let k = 0; k < list.length; k++) {
|
|
271
|
-
const v = list[k], w = wl[k], u = 1 - w;
|
|
272
|
-
// blend (w q + u 1, w dq), normalised; point = rot(p) + translation
|
|
273
|
-
qb.set(w * q.x, w * q.y, w * q.z, w * q.w + u);
|
|
274
|
-
const L = Math.hypot(qb.x, qb.y, qb.z, qb.w);
|
|
275
|
-
const rx = qb.x / L, ry = qb.y / L, rz = qb.z / L, rw = qb.w / L, dx = w * dq[0] / L, dy = w * dq[1] / L, dz = w * dq[2] / L, dw = w * dq[3] / L;
|
|
276
|
-
const tx = 2 * (rw * dx - dw * rx + (ry * dz - rz * dy)), ty = 2 * (rw * dy - dw * ry + (rz * dx - rx * dz)), tz = 2 * (rw * dz - dw * rz + (rx * dy - ry * dx));
|
|
277
|
-
pp.set(pos[v * 3], pos[v * 3 + 1], pos[v * 3 + 2]).applyQuaternion(qi.set(rx, ry, rz, rw));
|
|
278
|
-
Xs[k * 3] = pp.x + tx; Xs[k * 3 + 1] = pp.y + ty; Xs[k * 3 + 2] = pp.z + tz;
|
|
279
|
-
}
|
|
280
|
-
for (let i = 0; i < 64; i++) {
|
|
281
|
-
const y = 1 - (2 * i + 1) / 64, r = Math.sqrt(1 - y * y), a = i * 2.399963229728653, dx = Math.cos(a) * r, dz = Math.sin(a) * r;
|
|
282
|
-
let best = -Infinity, bk = -1;
|
|
283
|
-
for (let k = 0; k < list.length; k++) { const sc = Xs[k * 3] * dx + Xs[k * 3 + 1] * y + Xs[k * 3 + 2] * dz; if (sc > best) { best = sc; bk = k; } }
|
|
284
|
-
if (bk >= 0) set.add(bk);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
const ks = [...set];
|
|
288
|
-
if (ks.length) {
|
|
289
|
-
const n = ks.length, P = new Float64Array(n * 3), I = new Uint16Array(n * 4), W = new Float32Array(n * 4), wH = new Float32Array(n);
|
|
290
|
-
const bones = new Set();
|
|
291
|
-
let rMax = 0;
|
|
292
|
-
ks.forEach((kk, k) => {
|
|
293
|
-
const v = list[kk];
|
|
294
|
-
for (let c = 0; c < 3; c++) P[k * 3 + c] = pos[v * 3 + c];
|
|
295
|
-
for (let c = 0; c < 4; c++) { I[k * 4 + c] = SI[v * 4 + c]; W[k * 4 + c] = SW[v * 4 + c]; if (SW[v * 4 + c] > 0) bones.add(SI[v * 4 + c]); }
|
|
296
|
-
wH[k] = wl[kk];
|
|
297
|
-
rMax = Math.max(rMax, Math.hypot(pos[v * 3] - O[0], pos[v * 3 + 1] - O[1], pos[v * 3 + 2] - O[2]));
|
|
298
|
-
});
|
|
299
|
-
bones.add(ih); if (ij >= 0) bones.add(ij);
|
|
300
|
-
out = { n, P, I, W, wH, bones: Int32Array.from(bones), rMax };
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
HEAD_SUP_CACHE.set(data, out);
|
|
304
|
-
return out;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Preening spots: the skin on each side of the breast in front of the wing, measured on the mesh (the
|
|
308
|
-
// visible chest skin farthest out in that direction, within the neck's comfortable reach), so the bill
|
|
309
|
-
// tip goes to the feathers - a fixed body-frame point put a hen's head inside its breast. Bind world
|
|
310
|
-
// positions and outward normals, [left, right]; cached per build data.
|
|
311
|
-
const PREEN_CACHE = new WeakMap();
|
|
312
|
-
function preenSpots(data, bi, reach) {
|
|
313
|
-
if (PREEN_CACHE.has(data)) return PREEN_CACHE.get(data);
|
|
314
|
-
let out = null;
|
|
315
|
-
const pos = data.pos, nrm = data.nrm, SI = data.skinIndex, SW = data.skinWeight, coat = data.coat, J = data.joints;
|
|
316
|
-
if (pos && nrm && SI && bi.chest !== undefined && J.neckBase && J.synsacrum) {
|
|
317
|
-
const nb = J.neckBase, sy = J.synsacrum;
|
|
318
|
-
const c = [0, 1, 2].map((k) => sy[k] + (nb[k] - sy[k]) * 0.55);
|
|
319
|
-
out = [1, -1].map((side) => {
|
|
320
|
-
const d = [side * 0.8, -0.15, 0.58], dl = Math.hypot(...d);
|
|
321
|
-
let best = -Infinity, bv = -1;
|
|
322
|
-
for (let v = 0; v < data.nV; v++) {
|
|
323
|
-
if (SI[v * 4] !== bi.chest || SW[v * 4] < 0.5 || (coat && coat[v * 4 + 3] < 0.5)) continue;
|
|
324
|
-
const x = pos[v * 3], y = pos[v * 3 + 1], z = pos[v * 3 + 2];
|
|
325
|
-
if (Math.hypot(x - nb[0], y - nb[1], z - nb[2]) > reach) continue;
|
|
326
|
-
const sc = ((x - c[0]) * d[0] + (y - c[1]) * d[1] + (z - c[2]) * d[2]) / dl;
|
|
327
|
-
if (sc > best) { best = sc; bv = v; }
|
|
328
|
-
}
|
|
329
|
-
return bv < 0 ? null : { p: [pos[bv * 3], pos[bv * 3 + 1], pos[bv * 3 + 2]], n: [nrm[bv * 3], nrm[bv * 3 + 1], nrm[bv * 3 + 2]] };
|
|
330
|
-
});
|
|
331
|
-
if (!out[0] || !out[1]) out = null;
|
|
332
|
-
}
|
|
333
|
-
PREEN_CACHE.set(data, out);
|
|
334
|
-
return out;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// ------------------------------------------------------------------------------------------------
|
|
338
|
-
function resolveConfig(spec0, data) {
|
|
339
|
-
// per-variant overrides (species.motion.variants[params.variant]), merged one level deep
|
|
340
|
-
const vo = spec0.variants?.[data.params?.variant];
|
|
341
|
-
let spec = vo ? { ...spec0 } : spec0;
|
|
342
|
-
if (vo) for (const [k, v] of Object.entries(vo)) spec[k] = v && typeof v === 'object' && !Array.isArray(v) && spec0[k] && typeof spec0[k] === 'object' ? { ...spec0[k], ...v } : v;
|
|
343
|
-
// per-individual overrides: species.motion.individual(params) returns top-level keys that replace
|
|
344
|
-
// the species' (e.g. a rooster's attack style, a chick's wing and feathers)
|
|
345
|
-
if (typeof spec.individual === 'function') spec = { ...spec, ...(spec.individual(data.params || {}) || {}) };
|
|
346
|
-
const J = {};
|
|
347
|
-
for (const [k, v] of Object.entries(data.joints)) J[k] = new THREE.Vector3(v[0], v[1], v[2]);
|
|
348
|
-
const s = data.params?.size || 1, sq = Math.sqrt(s);
|
|
349
|
-
const merge = (a, b) => ({ ...a, ...(b || {}) });
|
|
350
|
-
const cfg = {
|
|
351
|
-
J, s, sq,
|
|
352
|
-
maxSpeed: (spec.maxSpeed ?? 14) * sq,
|
|
353
|
-
ground: merge(DEFAULTS.ground, spec.ground),
|
|
354
|
-
feet: merge(DEFAULTS.feet, spec.feet),
|
|
355
|
-
head: merge(DEFAULTS.head, spec.head),
|
|
356
|
-
tail: merge(DEFAULTS.tail, spec.tail),
|
|
357
|
-
flight: merge(DEFAULTS.flight, spec.flight),
|
|
358
|
-
takeoff: merge(DEFAULTS.takeoff, spec.takeoff),
|
|
359
|
-
landing: merge(DEFAULTS.landing, spec.landing),
|
|
360
|
-
actions: spec.actions || {},
|
|
361
|
-
gaits: (spec.gaits || []).map((g) => ({ f: 2, D: 0.65, off: [0, 0.5], lift: 0.02, bob: 0.004, hop: 0, sway: 0, pitch: 0, headBob: 0, wings: 0, tail: 0, ...g })),
|
|
362
|
-
};
|
|
363
|
-
if (!cfg.gaits.length) throw new Error('procedural-animals bird: species.motion.gaits is required');
|
|
364
|
-
cfg.ground.maxSpeed *= sq; cfg.ground.turnRate /= sq;
|
|
365
|
-
cfg.gears = spec.gears ? Object.fromEntries(Object.entries(spec.gears).map(([k, v]) => [k, v * sq])) : { walk: 0.5 * sq, fly: 10 * sq };
|
|
366
|
-
const fl = cfg.flight;
|
|
367
|
-
fl.minSpeed *= sq; fl.cruise *= sq; fl.maxSpeed = Math.min(fl.maxSpeed * sq, cfg.maxSpeed);
|
|
368
|
-
fl.climbUp = fl.climb[0] * sq; fl.climbDown = fl.climb[1] * sq; fl.clearance *= s;
|
|
369
|
-
fl.bankMax *= DEG;
|
|
370
|
-
cfg.takeoff.jump *= sq; cfg.takeoff.angle *= DEG;
|
|
371
|
-
cfg.landing.approach *= sq; cfg.landing.touch *= sq;
|
|
372
|
-
cfg.feet.pad = (cfg.feet.pad ?? J.mtpL.y) * s;
|
|
373
|
-
if (cfg.feet.sole !== undefined) cfg.feet.sole *= s;
|
|
374
|
-
cfg.tilt = (spec.body?.tilt ?? 30) * DEG;
|
|
375
|
-
// flight.shape (opt-in): the standing body axis and its ventral normal (bind frame) carry the head
|
|
376
|
-
// forward and down in flight
|
|
377
|
-
if (spec.flight?.shape) {
|
|
378
|
-
const fs = spec.flight.shape;
|
|
379
|
-
cfg.shape = {
|
|
380
|
-
reach: fs.reach || 0, drop: fs.drop || 0, neck: clamp(fs.neck || 0, 0, 0.9), fan: clamp(fs.fan || 0, 0, 1), back: fs.back || 0, legs: clamp(fs.legs || 0, 0, 0.9),
|
|
381
|
-
body: fs.body ? [fs.body[0] ?? 1, fs.body[1] ?? 1, fs.body[2] ?? 1] : null,
|
|
382
|
-
ax: new THREE.Vector3(0, Math.sin(cfg.tilt), Math.cos(cfg.tilt)), vent: new THREE.Vector3(0, -Math.cos(cfg.tilt), Math.sin(cfg.tilt)),
|
|
383
|
-
};
|
|
384
|
-
} else cfg.shape = null;
|
|
385
|
-
// ---- body pivot (between the hips) and belly section
|
|
386
|
-
cfg.pivot = J.hipL.clone().add(J.hipR).multiplyScalar(0.5);
|
|
387
|
-
cfg.pivotY = cfg.pivot.y;
|
|
388
|
-
const bb = spec.body?.belly || {};
|
|
389
|
-
cfg.belly = {
|
|
390
|
-
c: new THREE.Vector3(0, (bb.y ?? cfg.pivotY) * s, J.synsacrum.z),
|
|
391
|
-
hh: (bb.hh ?? 0.35 * cfg.pivotY) * s, hw: (bb.hw ?? Math.abs(J.hipL.x) * 1.6) * s,
|
|
392
|
-
};
|
|
393
|
-
// ---- legs
|
|
394
|
-
cfg.legs = LEGS.map((S, idx) => {
|
|
395
|
-
const side = S === 'L' ? 1 : -1;
|
|
396
|
-
const hip = J['hip' + S], knee = J['knee' + S], ankle = J['ankle' + S], mtp = J['mtp' + S];
|
|
397
|
-
const L = { S, side, idx, key: 'leg' + S };
|
|
398
|
-
L.L1 = hip.distanceTo(knee); L.L2 = knee.distanceTo(ankle); L.L3 = ankle.distanceTo(mtp);
|
|
399
|
-
L.len = L.L1 + L.L2 + L.L3;
|
|
400
|
-
L.hip = hip.clone().sub(cfg.pivot); // body-local
|
|
401
|
-
L.femur0 = knee.clone().sub(hip).normalize(); // body-local rest femur direction
|
|
402
|
-
L.cx = Math.abs(mtp.x); L.cz = mtp.z - cfg.pivot.z; // neutral foot point (ground, heading frame)
|
|
403
|
-
L.pad = mtp.y;
|
|
404
|
-
// bind tarsus lean (ankle behind the MTP)
|
|
405
|
-
L.tarsus0 = ankle.clone().sub(mtp).normalize();
|
|
406
|
-
// toes in the foot frame (x lateral (own side positive), y up, z forward), per segment
|
|
407
|
-
L.toes = TOES.map((j) => {
|
|
408
|
-
const m = J[`t${j}m${S}`], t = J[`t${j}t${S}`];
|
|
409
|
-
const a = m.clone().sub(mtp), b = t.clone().sub(m);
|
|
410
|
-
a.x *= side; b.x *= side;
|
|
411
|
-
// (eA / eB: bind elevation of each segment in the foot frame, + up: a curl lowers every toe, the
|
|
412
|
-
// hallux included, from there)
|
|
413
|
-
const la = a.length(), lb = b.length();
|
|
414
|
-
return { j, a, b, la, lb, back: j === 1, eA: Math.asin(clamp(a.y / Math.max(la, 1e-9), -1, 1)), eB: Math.asin(clamp(b.y / Math.max(lb, 1e-9), -1, 1)) };
|
|
415
|
-
});
|
|
416
|
-
L.toeLen = L.toes[2].la + L.toes[2].lb;
|
|
417
|
-
L.n = {
|
|
418
|
-
femur: 'femur' + S, tibia: 'tibia' + S, tarsus: 'tarsus' + S,
|
|
419
|
-
toeA: TOES.map((j) => `toe${j}a${S}`), toeB: TOES.map((j) => `toe${j}b${S}`),
|
|
420
|
-
};
|
|
421
|
-
return L;
|
|
422
|
-
});
|
|
423
|
-
cfg.legLen = cfg.legs[0].len;
|
|
424
|
-
cfg.footLen = cfg.legs[0].toeLen;
|
|
425
|
-
// ---- neck & head
|
|
426
|
-
cfg.neckN = 0;
|
|
427
|
-
while (data.bones.some((b) => b.name === 'neck' + cfg.neckN)) cfg.neckN++;
|
|
428
|
-
const nj = (i) => (i === 0 ? 'neckBase' : i === cfg.neckN ? 'occiput' : 'neck' + i);
|
|
429
|
-
cfg.neckJ = Array.from({ length: cfg.neckN + 1 }, (_, i) => J[nj(i)]);
|
|
430
|
-
cfg.neckL = Array.from({ length: cfg.neckN }, (_, i) => cfg.neckJ[i].distanceTo(cfg.neckJ[i + 1]));
|
|
431
|
-
cfg.neckLen = cfg.neckL.reduce((a, b) => a + b, 0);
|
|
432
|
-
cfg.neckBaseLocal = J.neckBase.clone().sub(cfg.pivot);
|
|
433
|
-
cfg.neckSeg0 = cfg.neckJ[1].clone().sub(cfg.neckJ[0]).normalize(); // first segment at bind (body-local)
|
|
434
|
-
cfg.occLocal = J.occiput.clone().sub(cfg.pivot);
|
|
435
|
-
fitNeckCurve(cfg);
|
|
436
|
-
cfg.billLocal = J.bill.clone().sub(J.occiput);
|
|
437
|
-
cfg.mouthLocal = J.jawTip ? J.jawTip.clone().lerp(J.bill, 0.5).sub(J.occiput) : cfg.billLocal.clone();
|
|
438
|
-
cfg.hasJaw = data.bones.some((b) => b.name === 'jaw');
|
|
439
|
-
cfg.jawRest = (cfg.head.jawRest ?? 0) * DEG; // closed-bill rest angle of the mandible (deg, - closes)
|
|
440
|
-
// (head.radius is metres at the reference size, as the schema says: scaled - a chick carried a hen's skull)
|
|
441
|
-
cfg.headR = spec.head?.radius !== undefined ? spec.head.radius * s : 0.2 * J.occiput.distanceTo(J.bill);
|
|
442
|
-
cfg.chinLocal = J.jawHinge ? J.jawHinge.clone().lerp(J.jawTip || J.bill, 0.3).sub(J.occiput).add(new THREE.Vector3(0, -cfg.headR * 0.6, 0)) : new THREE.Vector3(0, -cfg.headR, 0);
|
|
443
|
-
cfg.headH = cfg.occLocal.y + cfg.pivotY; // occiput height standing
|
|
444
|
-
// ---- tail
|
|
445
|
-
cfg.tailLocal = J.tailBase.clone().sub(cfg.pivot);
|
|
446
|
-
cfg.tailLen = J.tailBase.distanceTo(J.tailTip);
|
|
447
|
-
cfg.tailDir0 = J.tailTip.clone().sub(J.tailBase).normalize();
|
|
448
|
-
cfg.tailRel = Math.atan2(cfg.tailDir0.y, -cfg.tailDir0.z) + (spec.body?.tilt ?? 30) * DEG; // tail above the body axis (rad)
|
|
449
|
-
// ---- wings
|
|
450
|
-
const w = spec.wing || {};
|
|
451
|
-
const conf = (a) => { const o = {}; for (const k of WING_KEYS) o[k] = (a?.[k] || 0) * DEG; o.uTwist = (a?.uTwist || 0) * DEG; o.tilt = (w.tilt || 0) * DEG; return o; };
|
|
452
|
-
cfg.wing = {
|
|
453
|
-
L: { h: J.shoulderL.distanceTo(J.elbowL), u: J.elbowL.distanceTo(J.wristL), m: J.wristL.distanceTo(J.handTipL) },
|
|
454
|
-
bind: conf(w.bind), fold: conf(w.fold), glide: conf(w.glide || w.bind),
|
|
455
|
-
flap: { freq: 4, amp: 100, mid: 5, down: 0.55, upFlex: 30, upWrist: 40, sweep: 10, twist: 12, ...(w.flap || {}) },
|
|
456
|
-
power: { freq: 6, amp: 140, mid: 12, down: 0.52, upFlex: 50, upWrist: 65, sweep: 20, twist: 18, ...(w.power || w.flap || {}) },
|
|
457
|
-
glideShare: w.glideShare ?? 0.15, soar: w.soar ?? 0, tilt: (w.tilt || 0) * DEG, fingerLift: (w.fingerLift || 0) * DEG, groundLift: (w.groundLift || 0) * DEG, skin: w.skin ?? 0.006,
|
|
458
|
-
shoulder: LEGS.map((S) => J['shoulder' + S].clone().sub(cfg.pivot)),
|
|
459
|
-
};
|
|
460
|
-
for (const k of ['flap', 'power']) { const f = cfg.wing[k]; f.freq /= sq; f.amp *= DEG; f.mid *= DEG; f.upFlex *= DEG; f.upWrist *= DEG; f.sweep *= DEG; f.twist *= DEG; }
|
|
461
|
-
// ---- feathers (lengths from the final joints)
|
|
462
|
-
const F = resolveFeathers(spec.feathers || {});
|
|
463
|
-
const flen = (n) => J[n + 'rL'] && J[n + 'tL'] ? J[n + 'rL'].distanceTo(J[n + 'tL']) : 0;
|
|
464
|
-
cfg.pri = F.primaries.filter((f) => data.bones.some((b) => b.name === `pri${f.i + 1}L`)).map((f) => ({ ...f, len: flen(`pri${f.i + 1}`), sc: s }));
|
|
465
|
-
cfg.sec = F.secondaries.filter((f) => data.bones.some((b) => b.name === `sec${f.i + 1}L`)).map((f) => ({ ...f, len: flen(`sec${f.i + 1}`), sc: s }));
|
|
466
|
-
cfg.rec = F.rectrices.filter((f) => data.bones.some((b) => b.name === `rec${f.i + 1}L`)).map((f) => ({ ...f, len: flen(`rec${f.i + 1}`), sc: s }));
|
|
467
|
-
for (const f of [...cfg.pri, ...cfg.sec]) { f.u0 = f.u; f.lift *= s; f.back *= s; }
|
|
468
|
-
for (const f of cfg.rec) { f.x *= s; f.lift *= s; }
|
|
469
|
-
// widest half vane of each feather card with its covert (core/build/featherCards.js: coverts up to
|
|
470
|
-
// 1.2x wider), for the engine's vane-edge ground check
|
|
471
|
-
// the card's shape for the engine's ground checks (core/build/featherCards.js): the rachis curves
|
|
472
|
-
// toward the inner vane by curve L t^2 and droops toward -normal by droop L t^2 (m at the tip); the
|
|
473
|
-
// vane half-widths [t, outer, inner] at the checked points (near the root the covert's width too)
|
|
474
|
-
for (const f of [...cfg.pri, ...cfg.sec, ...cfg.rec]) {
|
|
475
|
-
f.vane = (f.width || 0) * s * Math.max(f.outer ?? 0.4, 1 - (f.outer ?? 0.4)) * 1.2;
|
|
476
|
-
f.droopM = (f.droop || 0) * f.len;
|
|
477
|
-
f.curveM = (f.curve || 0) * f.len;
|
|
478
|
-
const vp = [];
|
|
479
|
-
for (const t of [0.22, 0.55, 0.8, 0.92, 1]) {
|
|
480
|
-
if (t <= 0.55) { vp.push(t, f.vane, f.vane); continue; }
|
|
481
|
-
const [wo, wi] = f.width ? vaneWidth({ outer: f.outer ?? 0.4, tipRound: f.tipRound ?? 0.4, emarg: f.emarg || 0 }, t, f.width * s) : [0, 0];
|
|
482
|
-
vp.push(t, wo * 1.05, wi * 1.05);
|
|
483
|
-
}
|
|
484
|
-
f.vp = Float64Array.from(vp);
|
|
485
|
-
}
|
|
486
|
-
cfg.wingExt = {
|
|
487
|
-
fW: cfg.wing.fold.wrist, oW: cfg.wing.glide.wrist, fE: cfg.wing.fold.elbow, oE: cfg.wing.glide.elbow,
|
|
488
|
-
};
|
|
489
|
-
cfg.span = 2 * (cfg.wing.L.h + cfg.wing.L.u + cfg.wing.L.m + (cfg.pri.length ? Math.max(...cfg.pri.map((f) => f.len)) : 0)) + 2 * Math.abs(J.shoulderL.x);
|
|
490
|
-
// roll response in flight (rad/s of a critically damped roll): a crow (0.9 m span) rolls into a turn
|
|
491
|
-
// in ~0.3 s, an eagle (2 m) in ~0.5 s (roll inertia grows with the span; dynamic similarity)
|
|
492
|
-
cfg.rollOmega = 6.5 * Math.sqrt(0.9 / Math.max(0.1, cfg.span));
|
|
493
|
-
// share of the tail's flight alignment a tail without flight feathers gives up: none for a chick's
|
|
494
|
-
// stub (pin feathers 5 % of its leg: swung down 56 deg in flight, it sheared the rump's skin), all for
|
|
495
|
-
// a tail of real rectrices (>= 25 % of the leg)
|
|
496
|
-
cfg.tailFlightK = smooth(0.05, 0.25, (cfg.rec.length ? Math.max(...cfg.rec.map((f) => f.len)) : 0) / Math.max(1e-6, cfg.legLen));
|
|
497
|
-
return cfg;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Bind neck as a cubic Bezier (the shape the engine bends): end tangents from the joints (quadratic
|
|
501
|
-
// estimate), tangent length fitted to the bind joints, and the curve's arc length. At rest the engine's
|
|
502
|
-
// curve then reproduces the bind neck (no distortion), and every pose bends the same curve.
|
|
503
|
-
function bezierArc(A, t0, B, t1, k, pts, S) {
|
|
504
|
-
const M = pts.length - 1;
|
|
505
|
-
let L = 0;
|
|
506
|
-
for (let i = 0; i <= M; i++) {
|
|
507
|
-
const u = i / M, a = 1 - u, b0 = a * a * a, b1 = 3 * a * a * u, b2 = 3 * a * u * u, b3 = u * u * u;
|
|
508
|
-
pts[i].set(
|
|
509
|
-
b0 * A.x + b1 * (A.x + t0.x * k) + b2 * (B.x - t1.x * k) + b3 * B.x,
|
|
510
|
-
b0 * A.y + b1 * (A.y + t0.y * k) + b2 * (B.y - t1.y * k) + b3 * B.y,
|
|
511
|
-
b0 * A.z + b1 * (A.z + t0.z * k) + b2 * (B.z - t1.z * k) + b3 * B.z);
|
|
512
|
-
if (i) L += pts[i].distanceTo(pts[i - 1]);
|
|
513
|
-
S[i] = L;
|
|
514
|
-
}
|
|
515
|
-
return L;
|
|
516
|
-
}
|
|
517
|
-
function pointAtArc(pts, S, s, out) {
|
|
518
|
-
let j = 1;
|
|
519
|
-
while (j < pts.length - 1 && S[j] < s) j++;
|
|
520
|
-
const f = clamp((s - S[j - 1]) / Math.max(1e-9, S[j] - S[j - 1]), 0, 1);
|
|
521
|
-
return out.copy(pts[j - 1]).lerp(pts[j], f);
|
|
522
|
-
}
|
|
523
|
-
function fitNeckCurve(cfg) {
|
|
524
|
-
const Jn = cfg.neckJ, N = cfg.neckN, A = Jn[0], B = Jn[N];
|
|
525
|
-
const est = (p0, p1, p2, sgn) => p1.clone().multiplyScalar(4).sub(p0.clone().multiplyScalar(3)).sub(p2).multiplyScalar(sgn).normalize();
|
|
526
|
-
cfg.neckT0 = N >= 2 ? est(A, Jn[1], Jn[2], 1) : B.clone().sub(A).normalize(); // body-local
|
|
527
|
-
cfg.neckT1 = N >= 2 ? est(B, Jn[N - 1], Jn[N - 2], -1) : B.clone().sub(A).normalize(); // head-local (bind head frame = identity)
|
|
528
|
-
const d0 = A.distanceTo(B);
|
|
529
|
-
const pts = Array.from({ length: 65 }, () => new THREE.Vector3()), S = new Float64Array(65), q = new THREE.Vector3();
|
|
530
|
-
let best = Infinity, bk = 0.4 * d0, barc = cfg.neckLen;
|
|
531
|
-
for (let i = 1; i <= 60; i++) {
|
|
532
|
-
const k = d0 * i / 50;
|
|
533
|
-
const arc = bezierArc(A, cfg.neckT0, B, cfg.neckT1, k, pts, S);
|
|
534
|
-
let e = 0;
|
|
535
|
-
for (let j = 1; j < N; j++) e += pointAtArc(pts, S, (arc * j) / N, q).distanceToSquared(Jn[j]);
|
|
536
|
-
if (e < best) { best = e; bk = k; barc = arc; }
|
|
537
|
-
}
|
|
538
|
-
cfg.neckChord0 = d0;
|
|
539
|
-
cfg.neckArc = barc; // arc length of the bind curve
|
|
540
|
-
cfg.neckK0 = bk / d0;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// ------------------------------------------------------------------------------------------------
|
|
544
|
-
export function createBirdMotion(ctx) { return new BirdMotion(ctx); }
|
|
545
|
-
|
|
546
|
-
export class BirdMotion {
|
|
547
|
-
constructor({ species, data, skeleton, ground, water, perches, position, heading = 0, emit }) {
|
|
548
|
-
this.species = species;
|
|
549
|
-
this.cfg = resolveConfig(species.motion || {}, data);
|
|
550
|
-
const cfg = this.cfg;
|
|
551
|
-
this.sk = skeleton;
|
|
552
|
-
this.ground = ground || (() => 0);
|
|
553
|
-
this.water = water || null;
|
|
554
|
-
this.perches = (perches || []).map((p) => (p.isVector3 ? p.clone() : new THREE.Vector3(p.x, p.y, p.z)));
|
|
555
|
-
this.emit = emit || (() => {});
|
|
556
|
-
this.rand = prng((data.seed || 1) * 7919 + 29);
|
|
557
|
-
this.J = cfg.J;
|
|
558
|
-
// ---- bones
|
|
559
|
-
this.bi = {};
|
|
560
|
-
this.bindQ = [];
|
|
561
|
-
for (let i = 0; i < data.bones.length; i++) {
|
|
562
|
-
this.bi[data.bones[i].name] = i;
|
|
563
|
-
this.bindQ.push(new THREE.Quaternion().setFromRotationMatrix(skeleton.bind[i]));
|
|
564
|
-
}
|
|
565
|
-
this.C = data.bones.map(() => new THREE.Quaternion()); // frame -> bone offsets (see setBoneC)
|
|
566
|
-
this._initFrames(data);
|
|
567
|
-
// wing skin support (see wingSupport)
|
|
568
|
-
this._wingSup = wingSupport(data, this.bi, skeleton.inverses || skeleton.bind.map((m) => m.clone().invert()));
|
|
569
|
-
// head skin support (see headSupport)
|
|
570
|
-
this._headSup = headSupport(data, this.bi);
|
|
571
|
-
// preening spots on the breast skin (see preenSpots): body-local (relative to the pivot, bind)
|
|
572
|
-
{
|
|
573
|
-
const ps = preenSpots(data, this.bi, 0.85 * (cfg.neckArc + cfg.billLocal.length()));
|
|
574
|
-
this._preen = ps ? ps.map((o) => ({ p: new THREE.Vector3(...o.p).sub(cfg.pivot), n: new THREE.Vector3(...o.n).normalize() })) : null;
|
|
575
|
-
}
|
|
576
|
-
// body support on the ground, from the skin (see groundSupport): per-bone skinning data of the bones
|
|
577
|
-
// its vertices use (bind rotation, translation and axes, as core/render/dqs.js)
|
|
578
|
-
this.sup = groundSupport(data, this.bi);
|
|
579
|
-
if (this.sup || this._headSup) {
|
|
580
|
-
this._supD = new Float64Array(data.bones.length * 20);
|
|
581
|
-
this._supBind = [];
|
|
582
|
-
const p = new THREE.Vector3(), q = new THREE.Quaternion(), sc = new THREE.Vector3();
|
|
583
|
-
for (const set of [this.sup, this._headSup]) {
|
|
584
|
-
if (!set) continue;
|
|
585
|
-
for (const i of set.bones) {
|
|
586
|
-
if (this._supBind[i]) continue;
|
|
587
|
-
skeleton.bind[i].decompose(p, q, sc);
|
|
588
|
-
this._supBind[i] = { qi: q.clone().invert(), t: p.clone(), R: new THREE.Matrix3().setFromMatrix4(new THREE.Matrix4().makeRotationFromQuaternion(q)) };
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
this._supPrev = new THREE.Matrix4(); this._supPrevOk = false;
|
|
592
|
-
}
|
|
593
|
-
// numeric bone indices for the per-frame writers (-1 = absent)
|
|
594
|
-
const ix = (n) => (this.bi[n] ?? -1);
|
|
595
|
-
this.ix = {
|
|
596
|
-
neck: Array.from({ length: cfg.neckN }, (_, i) => ix('neck' + i)),
|
|
597
|
-
wing: LEGS.map((S) => ({ humerus: ix('humerus' + S), ulna: ix('ulna' + S), hand: ix('hand' + S) })),
|
|
598
|
-
pri: LEGS.map((S) => cfg.pri.map((f) => ix(`pri${f.i + 1}${S}`))),
|
|
599
|
-
sec: LEGS.map((S) => cfg.sec.map((f) => ix(`sec${f.i + 1}${S}`))),
|
|
600
|
-
rec: LEGS.map((S) => cfg.rec.map((f) => ix(`rec${f.i + 1}${S}`))),
|
|
601
|
-
toes: cfg.legs.map((L) => ({ tarsus: ix(L.n.tarsus), toes: [...L.n.toeA, ...L.n.toeB].map(ix) })),
|
|
602
|
-
tail: ix('tail'), pelvis: ix('pelvis'), chest: ix('chest'), head: ix('head'), jaw: ix('jaw'),
|
|
603
|
-
};
|
|
604
|
-
// crowd LOD: feathers follow their carrier bone rigidly between full solves (every 3rd frame)
|
|
605
|
-
this.rel = data.bones.map(() => new THREE.Matrix4());
|
|
606
|
-
this.lodPhase = (data.seed || 0) % 3;
|
|
607
|
-
// ---- public contract
|
|
608
|
-
this.input = { speed: 0, heading, climb: 0, look: null, follow: null, target: null, targetSpeed: 1, crouch: 0, gait: null };
|
|
609
|
-
this.pos = new THREE.Vector3();
|
|
610
|
-
this.velocity = new THREE.Vector3();
|
|
611
|
-
this.heading = heading;
|
|
612
|
-
this.speed = 0;
|
|
613
|
-
this.state = {
|
|
614
|
-
position: this.pos, heading, speed: 0, velocity: this.velocity, gait: 'stand', grounded: true,
|
|
615
|
-
action: null, posture: 'stand', eyelid: 0, lookTarget: null, mode: 'ground', altitude: 0,
|
|
616
|
-
stats: { gait: 'stand', freq: 0, duty: 1, flapFreq: 0 },
|
|
617
|
-
legs: LEGS.map((S) => ({ key: 'leg' + S, stance: true, contact: new THREE.Vector3() })),
|
|
618
|
-
};
|
|
619
|
-
this.headPose = { position: new THREE.Vector3(), quaternion: new THREE.Quaternion() };
|
|
620
|
-
this.debug = { targets: Array.from({ length: 6 }, () => new THREE.Vector3()) };
|
|
621
|
-
this.gaits = [...new Set(cfg.gaits.map((g) => g.name)), 'fly'];
|
|
622
|
-
this.gears = cfg.gears;
|
|
623
|
-
this.lod = 0; this.frame = 0; this.time = 0;
|
|
624
|
-
// ---- dynamic state
|
|
625
|
-
this.mode = 'ground'; // ground | takeoff | air | landing | perch
|
|
626
|
-
this.yawRate = 0;
|
|
627
|
-
this.phase = 0;
|
|
628
|
-
this.gait = { name: 'walk', f: 0, D: 0.65, off: [0, 0.5], lift: 0, bob: 0, hop: 0, sway: 0, pitch: 0, headBob: 0, wings: 0, tail: 0, hopW: 0 };
|
|
629
|
-
this.body = new THREE.Vector3(); // world pivot
|
|
630
|
-
this.bodyV = new THREE.Vector3();
|
|
631
|
-
this.vy = 0;
|
|
632
|
-
this.hY = new Spring(0); this.pitchS = new Spring(0); this.bankS = new Spring(0); this.swayS = new Spring(0); this.hopS = new Spring(0);
|
|
633
|
-
this.turnBankS = new Spring(0); this.turnBank = 0; this._flying = false; // flight: bank of the turn (+ = left)
|
|
634
|
-
this.qB = new THREE.Quaternion(); this.qH = new THREE.Quaternion();
|
|
635
|
-
this.F = new THREE.Vector3(); this.Lf = new THREE.Vector3();
|
|
636
|
-
this.lastVel = new THREE.Vector3(); this.accel = new THREE.Vector3();
|
|
637
|
-
this.push = new THREE.Vector3();
|
|
638
|
-
// flight
|
|
639
|
-
this.fl = {
|
|
640
|
-
psi: 0, amp: 0, ampS: new Spring(0), freq: 0, power: 0.5, glide: 0, powerS: new Spring(0.5), glideT: 3, gliding: 0, glideS: new Spring(0),
|
|
641
|
-
open: new Spring(0), time: 0, alt: 0, forceGlide: 0, forceFlap: 0, flapBeats: 0, beatCount: 0, lastPsi: 0,
|
|
642
|
-
};
|
|
643
|
-
this.to = null; // takeoff state
|
|
644
|
-
this.ld = null; // landing state
|
|
645
|
-
this.perch = null; // { point, axis (branch direction), radius }
|
|
646
|
-
this.airLeg = new Spring(0); // legs: 0 ground IK .. 1 air targets
|
|
647
|
-
this.shapeW = 0; // flight.shape weight (0 ground .. 1 cruise flight; see pose)
|
|
648
|
-
this.reachLeg = 0; // air leg target: 0 tucked .. 1 thrown forward (landing)
|
|
649
|
-
// legs
|
|
650
|
-
this.legs = cfg.legs.map((L) => ({
|
|
651
|
-
def: L, state: 'stance', plant: new THREE.Vector3(), plantN: new THREE.Vector3(0, 1, 0), yaw: heading,
|
|
652
|
-
liftLocal: new THREE.Vector3(), landLocal: new THREE.Vector3(), land: new THREE.Vector3(), contact: new THREE.Vector3(),
|
|
653
|
-
swingT: 0, swingDur: 0.2, stanceTime: 0, prevPhase: 0, windowUsed: false, u: 0, w: 0, liftH: 0.02, mode: 'gait',
|
|
654
|
-
idleOff: new THREE.Vector3(), roll: new Spring(0), curl: new Spring(0), spread: new Spring(1),
|
|
655
|
-
mtp: new THREE.Vector3(), ankle: new THREE.Vector3(), knee: new THREE.Vector3(), hip: new THREE.Vector3(),
|
|
656
|
-
airT: new THREE.Vector3(), frozen: new THREE.Vector3(), frozenOn: false, lastTarget: new THREE.Vector3(),
|
|
657
|
-
yawS: new Spring(heading),
|
|
658
|
-
}));
|
|
659
|
-
// wings
|
|
660
|
-
this.wf = [makeWingFrames(), makeWingFrames()];
|
|
661
|
-
this.wp = [0, 1].map(() => Object.fromEntries(WING_KEYS.map((k) => [k, 0])));
|
|
662
|
-
this.wingS = [0, 1].map(() => Object.fromEntries(WING_KEYS.map((k) => [k, new Spring(0)])));
|
|
663
|
-
this.wingInit = false;
|
|
664
|
-
this.ff = makeFeatherFrame();
|
|
665
|
-
this._fw = { P0: v3(), d: v3(), n: v3(), lat: v3() };
|
|
666
|
-
// tail
|
|
667
|
-
this.tailS = { pitch: new Spring(0), yaw: new Spring(0), fan: new Spring(cfg.tail.fanRest), twist: new Spring(0) };
|
|
668
|
-
// head / neck
|
|
669
|
-
this.headS = { x: new Spring(0), y: new Spring(0), z: new Spring(0), init: false };
|
|
670
|
-
this.gaze = { yaw: 0, pitch: 0, roll: 0, tyaw: 0, tpitch: 0, troll: 0, hold: 0 };
|
|
671
|
-
this.gazeS = { yaw: new Spring(0), pitch: new Spring(0), roll: new Spring(0) };
|
|
672
|
-
this.bobRef = new THREE.Vector3(); this.bobInit = false;
|
|
673
|
-
this.jaw = new Spring(cfg.jawRest || 0);
|
|
674
|
-
this.breath = 0;
|
|
675
|
-
this.lookTarget = new THREE.Vector3();
|
|
676
|
-
this._neck = { P: Array.from({ length: 4 }, () => new THREE.Vector3()), pts: Array.from({ length: 17 }, () => new THREE.Vector3()), s: new Float64Array(17), J: Array.from({ length: cfg.neckN + 1 }, () => new THREE.Vector3()) };
|
|
677
|
-
// ---- actions
|
|
678
|
-
this.P = null;
|
|
679
|
-
this.actionsLayer = new BirdActions(this);
|
|
680
|
-
this.P = this.actionsLayer.P;
|
|
681
|
-
this.actions = this.actionsLayer.names;
|
|
682
|
-
this.attachmentPoints = this._attachments(data);
|
|
683
|
-
this.reset(position || new THREE.Vector3(), heading);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// bind frames -> constant offsets C (bone world quaternion = frameQuat(dir, ref) * C)
|
|
687
|
-
_initFrames(data) {
|
|
688
|
-
const cfg = this.cfg, J = this.J;
|
|
689
|
-
const d = new THREE.Vector3(), r = new THREE.Vector3(), q = new THREE.Quaternion();
|
|
690
|
-
const setC = (name, dir, ref) => {
|
|
691
|
-
const i = this.bi[name];
|
|
692
|
-
if (i === undefined) return;
|
|
693
|
-
frameQuat(q, dir, ref);
|
|
694
|
-
this.C[i].copy(q).invert().multiply(this.bindQ[i]);
|
|
695
|
-
};
|
|
696
|
-
// everything defaults to the lateral X reference at bind (C = identity up to rounding)
|
|
697
|
-
data.bones.forEach((b) => {
|
|
698
|
-
d.subVectors(J[b.tailJ], J[b.headJ]);
|
|
699
|
-
setC(b.name, d, X1);
|
|
700
|
-
});
|
|
701
|
-
// wings and feathers: dorsal normals from the bind configuration
|
|
702
|
-
for (let k = 0; k < 2; k++) {
|
|
703
|
-
const S = LEGS[k], side = k === 0 ? 1 : -1, fr = this.wf ? this.wf[k] : makeWingFrames();
|
|
704
|
-
wingFK(fr, side, cfg.wing.L, cfg.wing.bind);
|
|
705
|
-
setC('humerus' + S, d.set(fr.dH.x, fr.dH.y, fr.dH.z), r.set(fr.nH.x, fr.nH.y, fr.nH.z));
|
|
706
|
-
setC('ulna' + S, d.set(fr.dU.x, fr.dU.y, fr.dU.z), r.set(fr.nU.x, fr.nU.y, fr.nU.z));
|
|
707
|
-
setC('hand' + S, d.set(fr.dM.x, fr.dM.y, fr.dM.z), r.set(fr.nM.x, fr.nM.y, fr.nM.z));
|
|
708
|
-
const eH = extension(cfg.wing.bind.wrist, cfg.wingExt.fW, cfg.wingExt.oW);
|
|
709
|
-
const eU = extension(cfg.wing.bind.elbow, cfg.wingExt.fE, cfg.wingExt.oE);
|
|
710
|
-
const P0 = v3(fr.wrist.x, fr.wrist.y, fr.wrist.z), ff = makeFeatherFrame();
|
|
711
|
-
for (const f of cfg.pri) {
|
|
712
|
-
featherFrame(ff, P0, fr.dM, fr.nM, cfg.wing.L.m, side, f, f.fold + (f.spread - f.fold) * eH, false);
|
|
713
|
-
const nm = `pri${f.i + 1}${S}`;
|
|
714
|
-
d.subVectors(J[`pri${f.i + 1}t${S}`], J[`pri${f.i + 1}r${S}`]);
|
|
715
|
-
setC(nm, d, r.set(ff.normal.x, ff.normal.y, ff.normal.z));
|
|
716
|
-
}
|
|
717
|
-
for (const f of cfg.sec) {
|
|
718
|
-
featherFrame(ff, P0, fr.dU, fr.nU, cfg.wing.L.u, side, f, f.fold + (f.spread - f.fold) * eU, true);
|
|
719
|
-
const nm = `sec${f.i + 1}${S}`;
|
|
720
|
-
d.subVectors(J[`sec${f.i + 1}t${S}`], J[`sec${f.i + 1}r${S}`]);
|
|
721
|
-
setC(nm, d, r.set(ff.normal.x, ff.normal.y, ff.normal.z));
|
|
722
|
-
}
|
|
723
|
-
// rectrices: bind tail frame (dorsal = up)
|
|
724
|
-
const td = J.tailTip.clone().sub(J.tailBase).normalize();
|
|
725
|
-
const tn = new THREE.Vector3().crossVectors(td, X1).normalize();
|
|
726
|
-
if (tn.y < 0) tn.negate();
|
|
727
|
-
for (const f of cfg.rec) {
|
|
728
|
-
rectrixFrame(ff, v3(0, 0, 0), td, tn, X1, side, f, f.fold + (f.fan - f.fold) * 0.2);
|
|
729
|
-
const nm = `rec${f.i + 1}${S}`;
|
|
730
|
-
d.subVectors(J[`rec${f.i + 1}t${S}`], J[`rec${f.i + 1}r${S}`]);
|
|
731
|
-
setC(nm, d, r.set(ff.normal.x, ff.normal.y, ff.normal.z));
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
_attachments(data) {
|
|
737
|
-
const J = this.J, cfg = this.cfg;
|
|
738
|
-
const at = (bone, p) => {
|
|
739
|
-
const i = this.bi[bone];
|
|
740
|
-
if (i === undefined) return null;
|
|
741
|
-
const inv = this.sk.inverses ? this.sk.inverses[i] : this.sk.bind[i].clone().invert();
|
|
742
|
-
return { bone, local: new THREE.Matrix4().makeTranslation(p.x, p.y, p.z).premultiply(inv) };
|
|
743
|
-
};
|
|
744
|
-
let headP = J.occiput.clone().lerp(J.bill, 0.3);
|
|
745
|
-
if (data.eyes?.length) { headP = new THREE.Vector3(); for (const e of data.eyes) headP.add(new THREE.Vector3(...e.c)); headP.multiplyScalar(1 / data.eyes.length); }
|
|
746
|
-
const back = J.synsacrum.clone().lerp(J.neckBase, 0.4);
|
|
747
|
-
back.y += cfg.belly.hh * 0.5;
|
|
748
|
-
const out = { mouth: at('head', J.occiput.clone().add(cfg.mouthLocal)), head: at('head', headP), back: at('chest', back), footL: at('toe3aL', J.mtpL), footR: at('toe3aR', J.mtpR) };
|
|
749
|
-
for (const k of Object.keys(out)) if (!out[k]) delete out[k];
|
|
750
|
-
return out;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// ----------------------------------------------------------------- helpers
|
|
754
|
-
forward(h, out) { return out.set(Math.sin(h), 0, Math.cos(h)); }
|
|
755
|
-
left(h, out) { return out.set(Math.cos(h), 0, -Math.sin(h)); }
|
|
756
|
-
bodyToWorld(lx, ly, lz, h, p, out) {
|
|
757
|
-
const sh = Math.sin(h), ch = Math.cos(h);
|
|
758
|
-
return out.set(p.x + ch * lx + sh * lz, p.y + ly, p.z - sh * lx + ch * lz);
|
|
759
|
-
}
|
|
760
|
-
terrainH(x, z) { const h = this.ground(x, z); return Number.isFinite(h) ? h : 0; }
|
|
761
|
-
terrainN(x, z, out) {
|
|
762
|
-
const e = 0.06 * this.cfg.s + 0.02, h0 = this.terrainH(x, z);
|
|
763
|
-
return out.set(h0 - this.terrainH(x + e, z), e, h0 - this.terrainH(x, z + e)).normalize();
|
|
764
|
-
}
|
|
765
|
-
// body-local point (relative to the pivot, bind orientation) -> world
|
|
766
|
-
B(p, out) { return out.copy(p).applyQuaternion(this.qB).add(this.body); }
|
|
767
|
-
Bv(p, out) { return out.copy(p).applyQuaternion(this.qB); }
|
|
768
|
-
setBoneC(name, head, dir, ref, scale) {
|
|
769
|
-
// (the frame's matrix times the bone's constant offset, written directly: the quaternion round
|
|
770
|
-
// trips of frameQuat * C -> compose were a large share of the crowd tier's cost)
|
|
771
|
-
if (!this.Cm) {
|
|
772
|
-
const m = new THREE.Matrix4();
|
|
773
|
-
this.Cm = this.C.map((c) => { const e = m.makeRotationFromQuaternion(c).elements; return new Float64Array([e[0], e[1], e[2], e[4], e[5], e[6], e[8], e[9], e[10]]); });
|
|
774
|
-
}
|
|
775
|
-
const i = typeof name === 'number' ? name : this.bi[name];
|
|
776
|
-
if (i === undefined || i < 0) return;
|
|
777
|
-
writeFrameC(this.sk.bones[i].matrixWorld, head, dir, ref, this.Cm[i], scale || null);
|
|
778
|
-
}
|
|
779
|
-
// children keep their transform relative to a carrier bone (crowd LOD, between full solves)
|
|
780
|
-
store(parent, kids) {
|
|
781
|
-
if (parent < 0) return;
|
|
782
|
-
const inv = _m4.copy(this.sk.bones[parent].matrixWorld).invert();
|
|
783
|
-
for (const k of kids) if (k >= 0) mulAffine(this.rel[k], inv, this.sk.bones[k].matrixWorld);
|
|
784
|
-
}
|
|
785
|
-
follow(parent, kids) {
|
|
786
|
-
if (parent < 0) return;
|
|
787
|
-
const M = this.sk.bones[parent].matrixWorld;
|
|
788
|
-
for (const k of kids) if (k >= 0) mulAffine(this.sk.bones[k].matrixWorld, M, this.rel[k]);
|
|
789
|
-
}
|
|
790
|
-
setBoneQ(name, head, q, scale) {
|
|
791
|
-
const i = typeof name === 'number' ? name : this.bi[name];
|
|
792
|
-
if (i === undefined || i < 0) return;
|
|
793
|
-
this.sk.bones[i].matrixWorld.compose(head, tqc(q).multiply(this.bindQ[i]), scale || ONE);
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
reset(p, heading = this.heading) {
|
|
797
|
-
const cfg = this.cfg;
|
|
798
|
-
this.heading = heading; this.input.heading = heading;
|
|
799
|
-
this.pos.set(p.x, this.terrainH(p.x, p.z), p.z);
|
|
800
|
-
this.speed = 0; this.yawRate = 0; this.vy = 0;
|
|
801
|
-
this.velocity.set(0, 0, 0); this.lastVel.set(0, 0, 0);
|
|
802
|
-
this.mode = 'ground';
|
|
803
|
-
this.gaitAt(0);
|
|
804
|
-
for (const leg of this.legs) {
|
|
805
|
-
this.neutralContact(leg.def, 0, 0, leg.plant, leg);
|
|
806
|
-
leg.contact.copy(leg.plant);
|
|
807
|
-
leg.state = 'stance';
|
|
808
|
-
this.terrainN(leg.plant.x, leg.plant.z, leg.plantN);
|
|
809
|
-
leg.yaw = heading; leg.yawS.reset(heading);
|
|
810
|
-
}
|
|
811
|
-
this.body.set(this.pos.x, this.pos.y + cfg.pivotY, this.pos.z);
|
|
812
|
-
this.hY.reset(this.pos.y + cfg.pivotY);
|
|
813
|
-
this.pitchS.reset(0); this.bankS.reset(0);
|
|
814
|
-
this.qH.setFromAxisAngle(UP, heading);
|
|
815
|
-
this.qB.copy(this.qH);
|
|
816
|
-
this.headS.init = false; this.bobInit = false; this.wingInit = false;
|
|
817
|
-
this.airLeg.reset(0);
|
|
818
|
-
if (this.shapeS) this.shapeS.reset(0);
|
|
819
|
-
this.shapeW = 0;
|
|
820
|
-
this.landOff = null; this._touchTime = undefined;
|
|
821
|
-
this.pivot = null; this._standTurn = false;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
// ----------------------------------------------------------------- public API
|
|
825
|
-
play(name, opts) { return this.actionsLayer.play(name, opts || {}); }
|
|
826
|
-
stop(name) { this.actionsLayer.stop(name); }
|
|
827
|
-
setLod(level) { this.lod = clamp(level | 0, 0, 2); return this; }
|
|
828
|
-
dispose() { this.actionsLayer.clear(); }
|
|
829
|
-
get airborne() { return this.mode === 'air' || this.mode === 'landing' || (this.mode === 'takeoff' && this.to && this.to.air); }
|
|
830
|
-
|
|
831
|
-
update(dt) {
|
|
832
|
-
if (!(dt > 1e-4) || !Number.isFinite(dt)) dt = 1e-4;
|
|
833
|
-
dt = Math.min(dt, 1 / 20);
|
|
834
|
-
this._dt = dt;
|
|
835
|
-
this.frame++;
|
|
836
|
-
this.time += dt;
|
|
837
|
-
// crowd LOD: the pose is solved every other frame (with the summed dt); in between the whole
|
|
838
|
-
// skeleton moves rigidly with the root. Feathers and toes are re-solved on every 3rd solve.
|
|
839
|
-
const skip = this.lod === 2 && ((this.frame + this.lodPhase) & 1) === 1 && this._rootInit;
|
|
840
|
-
if (!skip) { this._poseN = (this._poseN || 0) + 1; this.fullFrame = this.lod < 2 || (this._poseN + this.lodPhase) % 3 === 0; }
|
|
841
|
-
this.drive(dt);
|
|
842
|
-
this.actionsLayer.update(dt);
|
|
843
|
-
const mode = this.mode;
|
|
844
|
-
if (mode === 'ground' || mode === 'perch' || (mode === 'takeoff' && !this.to.air)) this.stepGround(dt);
|
|
845
|
-
if (mode === 'takeoff') this.stepTakeoff(dt);
|
|
846
|
-
else if (mode === 'air') this.stepAir(dt);
|
|
847
|
-
else if (mode === 'landing') this.stepLanding(dt);
|
|
848
|
-
if (!this.airborne) { this._flying = false; this.turnBank = this.turnBankS.step(0, this.cfg.rollOmega, dt); }
|
|
849
|
-
this.accel.copy(this.velocity).sub(this.lastVel).divideScalar(dt);
|
|
850
|
-
this.lastVel.copy(this.velocity);
|
|
851
|
-
this._pdt = (this._pdt || 0) + dt;
|
|
852
|
-
this.rigidCarry = null; // (set by carryRigid: the render object then carries the skinning data too)
|
|
853
|
-
if (skip) this.carryRigid();
|
|
854
|
-
else { this.pose(this._pdt); this._pdt = 0; this.storeRoot(); }
|
|
855
|
-
this.publish();
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// root frame (ground point or flying body + heading) for the crowd LOD's rigid in-between frames
|
|
859
|
-
rootFrame(out) {
|
|
860
|
-
const r = this.airborne ? this.body : this.pos;
|
|
861
|
-
return out.compose(r, this.qH.setFromAxisAngle(UP, this.heading), ONE);
|
|
862
|
-
}
|
|
863
|
-
storeRoot() {
|
|
864
|
-
if (this.lod !== 2) { this._rootInit = false; return; }
|
|
865
|
-
this._rootM = this.rootFrame(this._rootM || new THREE.Matrix4());
|
|
866
|
-
this._rootInit = true;
|
|
867
|
-
}
|
|
868
|
-
carryRigid() {
|
|
869
|
-
const now = this.rootFrame(_m4b);
|
|
870
|
-
const D = mulAffine(this._carryD || (this._carryD = new THREE.Matrix4()), now, _m4.copy(this._rootM).invert());
|
|
871
|
-
for (const b of this.sk.bones) mulAffine(b.matrixWorld, D, b.matrixWorld);
|
|
872
|
-
if (this._supPrevOk) mulAffine(this._supPrev, D, this._supPrev);
|
|
873
|
-
this.rigidCarry = D;
|
|
874
|
-
this.headPose.position.applyMatrix4(D);
|
|
875
|
-
this.headPose.quaternion.premultiply(_qD.setFromRotationMatrix(D));
|
|
876
|
-
this._rootM.copy(now);
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// ----------------------------------------------------------------- intent: input -> mode changes
|
|
880
|
-
drive(dt) {
|
|
881
|
-
const inp = this.input, cfg = this.cfg, fl = cfg.flight;
|
|
882
|
-
let want = Math.max(0, inp.speed || 0);
|
|
883
|
-
this.wantHeading = inp.heading ?? this.heading;
|
|
884
|
-
this.wantClimb = clamp(inp.climb || 0, fl.climbDown, fl.climbUp);
|
|
885
|
-
const gaitAir = inp.gait === 'fly' || inp.gait === 'glide';
|
|
886
|
-
// (a gait name the ground table does not know - e.g. a tool's 'sprint' at the top speed of a
|
|
887
|
-
// burst flyer - forces nothing: the speed decides between walking and flying)
|
|
888
|
-
const gaitGround = !!inp.gait && !gaitAir && (this._groundGaits || (this._groundGaits = new Set(cfg.gaits.map((g) => g.name)))).has(inp.gait);
|
|
889
|
-
if (inp.target && !inp.follow) {
|
|
890
|
-
const dx = inp.target.x - this.pos.x, dz = inp.target.z - this.pos.z, d = Math.hypot(dx, dz);
|
|
891
|
-
const stopR = 0.3 * cfg.legLen + 0.02 * this.speed;
|
|
892
|
-
if (d < stopR && this.mode === 'ground') { inp.target = null; inp.speed = 0; want = 0; this.emit('arrive', {}); }
|
|
893
|
-
else {
|
|
894
|
-
this.wantHeading = inp.heading = Math.atan2(dx, dz);
|
|
895
|
-
const brake = Math.sqrt(Math.max(0, 2 * cfg.ground.decel * 0.7 * (d - stopR)));
|
|
896
|
-
want = Math.min(inp.targetSpeed || 1, brake + 0.2);
|
|
897
|
-
// far targets are flown to (and landed at)
|
|
898
|
-
if (this.mode === 'ground' && (inp.targetSpeed || 0) > cfg.ground.maxSpeed * 1.05) want = inp.targetSpeed;
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
this.wantSpeed = want;
|
|
902
|
-
const P = this.P;
|
|
903
|
-
if (inp.follow) { this.wantSpeed = 0; return; }
|
|
904
|
-
// postures stand up first
|
|
905
|
-
if (this.actionsLayer.holdsStill()) {
|
|
906
|
-
if ((want > 0.05 || this.wantClimb > 0.5) && this.mode !== 'air') this.actionsLayer.requestStand();
|
|
907
|
-
this.wantSpeed = 0; this.wantHeading = this.heading;
|
|
908
|
-
return;
|
|
909
|
-
}
|
|
910
|
-
if (this.mode === 'ground' || this.mode === 'perch') {
|
|
911
|
-
const fly = !gaitGround && (want > cfg.ground.maxSpeed * 1.02 || this.wantClimb >= 0.5 || gaitAir);
|
|
912
|
-
if (fly && !P.busyGround) this.startTakeoff();
|
|
913
|
-
else if (this.mode === 'perch' && want > 0.05) this.startTakeoff(); // hop down / fly off
|
|
914
|
-
} else if (this.mode === 'air') {
|
|
915
|
-
// after an explicit take-off with no steering yet: circle (loiter) at a safe height
|
|
916
|
-
if (this.actionsLayer.loiter && want < 0.05) {
|
|
917
|
-
this.wantSpeed = fl.cruise * 0.85;
|
|
918
|
-
this.wantHeading = this.heading + 0.45;
|
|
919
|
-
this.wantClimb = clamp((fl.clearance * 2.5 - this.altitude()) * 0.8, fl.climbDown, fl.climbUp);
|
|
920
|
-
}
|
|
921
|
-
// slow request, ground gait, or a non-sustained flyer out of time: land ahead
|
|
922
|
-
const tired = !fl.sustained && this.fl.time > (fl.maxTime ?? 5);
|
|
923
|
-
if (!this.actionsLayer.keepsFlying() && ((want < fl.minSpeed * 0.6 && this.wantClimb <= 0.2) || gaitGround || tired)) this.startLanding(null, 'auto');
|
|
924
|
-
}
|
|
925
|
-
// standing still: turn toward a look target behind
|
|
926
|
-
if ((this.mode === 'ground') && this.wantSpeed < 0.05) {
|
|
927
|
-
const lt = inp.look;
|
|
928
|
-
if (lt) {
|
|
929
|
-
const w = Math.atan2(lt.x - this.pos.x, lt.z - this.pos.z);
|
|
930
|
-
const off = angDiff(this.heading, w);
|
|
931
|
-
if (Math.abs(off) > cfg.head.yaw * 0.8) this._turnTo = w;
|
|
932
|
-
else if (this._turnTo !== undefined && Math.abs(off) < 0.4) this._turnTo = undefined;
|
|
933
|
-
if (this._turnTo !== undefined) { this.wantHeading = this._turnTo; inp.heading = this._turnTo; }
|
|
934
|
-
} else this._turnTo = undefined;
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// ----------------------------------------------------------------- ground locomotion
|
|
939
|
-
gaitAt(vn) {
|
|
940
|
-
const Gt = this.cfg.gaits, g = this.gait;
|
|
941
|
-
let i = 0;
|
|
942
|
-
while (i < Gt.length - 2 && vn > Gt[i + 1].v) i++;
|
|
943
|
-
const a = Gt[i], b = Gt[Math.min(i + 1, Gt.length - 1)];
|
|
944
|
-
const t = a === b ? 0 : smooth(0, 1, (vn - a.v) / (b.v - a.v));
|
|
945
|
-
g.name = t < 0.5 ? a.name : b.name;
|
|
946
|
-
g.f = lerp(a.f, b.f, t); g.D = lerp(a.D, b.D, t);
|
|
947
|
-
g.lift = lerp(a.lift, b.lift, t); g.bob = lerp(a.bob, b.bob, t); g.hop = lerp(a.hop, b.hop, t);
|
|
948
|
-
g.sway = lerp(a.sway, b.sway, t); g.pitch = lerp(a.pitch, b.pitch, t) * DEG; g.headBob = lerp(a.headBob, b.headBob, t);
|
|
949
|
-
g.wings = lerp(a.wings, b.wings, t); g.tail = lerp(a.tail, b.tail, t);
|
|
950
|
-
for (let j = 0; j < 2; j++) g.off[j] = wrap01(a.off[j] + d01(a.off[j], b.off[j]) * t);
|
|
951
|
-
if (vn > b.v) g.f = b.f + (vn - b.v) * 0.3;
|
|
952
|
-
const cfg = this.cfg, s = cfg.s;
|
|
953
|
-
g.f /= cfg.sq;
|
|
954
|
-
g.lift *= s; g.bob *= s; g.hop *= s; g.sway *= s;
|
|
955
|
-
// hop weight: both feet land together
|
|
956
|
-
g.hopW = 1 - clamp(Math.abs(d01(g.off[0], g.off[1])) / 0.5, 0, 1);
|
|
957
|
-
return g;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
neutralContact(L, lead, tAhead, out, leg) {
|
|
961
|
-
// (a pivot step lands under the heading the step turns the body to)
|
|
962
|
-
const hp = this.pivot ? this.pivot.h0 + this.pivot.dh : this.heading + this.yawRate * tAhead;
|
|
963
|
-
const hm = this.pivot ? hp : this.heading + this.yawRate * tAhead * 0.5;
|
|
964
|
-
const d = this.speed * tAhead;
|
|
965
|
-
const bx = this.pos.x + Math.sin(hm) * d, bz = this.pos.z + Math.cos(hm) * d;
|
|
966
|
-
let lx = L.cx * L.side, lz = L.cz + lead;
|
|
967
|
-
if (leg) { lx += leg.idleOff.x; lz += leg.idleOff.z; }
|
|
968
|
-
const sh = Math.sin(hp), ch = Math.cos(hp);
|
|
969
|
-
out.set(bx + ch * lx + sh * lz, 0, bz - sh * lx + ch * lz);
|
|
970
|
-
out.y = this.terrainH(out.x, out.z);
|
|
971
|
-
return out;
|
|
972
|
-
}
|
|
973
|
-
worldToBody(w, out) {
|
|
974
|
-
const dx = w.x - this.pos.x, dz = w.z - this.pos.z;
|
|
975
|
-
const sh = Math.sin(this.heading), ch = Math.cos(this.heading);
|
|
976
|
-
return out.set(dx * ch - dz * sh, w.y, dx * sh + dz * ch);
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
stepGround(dt) {
|
|
980
|
-
const cfg = this.cfg, inp = this.input, P = this.P, gr = cfg.ground;
|
|
981
|
-
const F = this.forward(this.heading, this.F);
|
|
982
|
-
if (inp.follow && this.mode === 'ground') {
|
|
983
|
-
const fv = inp.follow.velocity;
|
|
984
|
-
const vx = fv ? fv.x : 0, vz = fv ? fv.z : 0, sp = Math.hypot(vx, vz);
|
|
985
|
-
this.speed = expTo(this.speed, Math.min(sp, gr.maxSpeed * 1.5), dt, 0.1);
|
|
986
|
-
let hT = inp.follow.heading;
|
|
987
|
-
if (hT === null || hT === undefined) hT = sp > 0.2 * cfg.sq ? Math.atan2(vx, vz) : this.heading;
|
|
988
|
-
const wMax = gr.turnRate * 2;
|
|
989
|
-
const wT = clamp(angDiff(this.heading, hT) * 6, -wMax, wMax);
|
|
990
|
-
this.yawRate += clamp(wT - this.yawRate, -30 * dt, 30 * dt);
|
|
991
|
-
this.heading += this.yawRate * dt;
|
|
992
|
-
this.pos.x = inp.follow.position.x; this.pos.z = inp.follow.position.z;
|
|
993
|
-
this.forward(this.heading, F);
|
|
994
|
-
this.velocity.set(vx, 0, vz);
|
|
995
|
-
} else if (this.mode === 'ground') {
|
|
996
|
-
const vT = this.wantSpeed > gr.maxSpeed ? gr.maxSpeed : this.wantSpeed * P.speedScale;
|
|
997
|
-
const vn = this.speed / cfg.sq;
|
|
998
|
-
const acc = vT > this.speed ? lerp(gr.accel[0], gr.accel[1], smooth(0.5, 1.5, vn)) : gr.decel;
|
|
999
|
-
this.speed += clamp(vT - this.speed, -acc * dt, acc * dt);
|
|
1000
|
-
// standing: the bird turns on the spot in steps (stepLegs: the body turns while a foot swings)
|
|
1001
|
-
this._standTurn = vT < 0.05 * cfg.sq && this.speed < 0.08 * cfg.sq && P.legGait > 0.5 && P.restep > 0.5;
|
|
1002
|
-
if (this.pivot) {
|
|
1003
|
-
const pv = this.pivot;
|
|
1004
|
-
pv.t += dt;
|
|
1005
|
-
const u = clamp(pv.t / pv.dur, 0, 1), h = pv.h0 + pv.dh * smoother(u);
|
|
1006
|
-
this.yawRate = (h - this.heading) / dt;
|
|
1007
|
-
this.heading = h;
|
|
1008
|
-
if (u >= 1) { this.pivot = null; this.yawRate = 0; }
|
|
1009
|
-
} else if (this._standTurn) {
|
|
1010
|
-
this.yawRate += clamp(-this.yawRate, -12 * dt, 12 * dt);
|
|
1011
|
-
this.heading += this.yawRate * dt;
|
|
1012
|
-
} else {
|
|
1013
|
-
// turn rate: the species' limit (less when slow), what the legs give (a walking or running bird
|
|
1014
|
-
// changes its heading by ~35 deg at most per step: two steps per stride, one per hop) and the
|
|
1015
|
-
// lateral acceleration a bird can hold leaning into the turn (~0.5 g); the rate builds up over
|
|
1016
|
-
// ~0.2 s. (The body used to swing round at the species' full rate whatever the gait: a walking
|
|
1017
|
-
// hen turned 90 deg in half a second, a running one at 0.7 g, the stance feet twisting 70 deg.)
|
|
1018
|
-
const g = this.gait, steps = Math.max(1.5, (g.hopW > 0.5 ? 1 : 2) * (g.f || 0));
|
|
1019
|
-
const wMax = Math.min(gr.turnRate * lerp(0.5, 1, smooth(0.1, 0.8, vn)), 0.6 * steps, 0.5 * G / Math.max(0.1, this.speed)) * P.turnScale;
|
|
1020
|
-
const wT = clamp(angDiff(this.heading, this.wantHeading) * 4, -wMax, wMax);
|
|
1021
|
-
this.yawRate += clamp(wT - this.yawRate, -10 * dt / cfg.sq, 10 * dt / cfg.sq);
|
|
1022
|
-
this.heading += this.yawRate * dt;
|
|
1023
|
-
}
|
|
1024
|
-
this.forward(this.heading, F);
|
|
1025
|
-
this.velocity.copy(F).multiplyScalar(this.speed);
|
|
1026
|
-
this.pos.addScaledVector(F, this.speed * dt);
|
|
1027
|
-
if (this.push.lengthSq() > 1e-8) {
|
|
1028
|
-
this.pos.addScaledVector(this.push, dt);
|
|
1029
|
-
this.velocity.add(this.push);
|
|
1030
|
-
this.push.multiplyScalar(Math.exp(-dt * 7));
|
|
1031
|
-
}
|
|
1032
|
-
} else {
|
|
1033
|
-
// perched / taking off: no locomotion
|
|
1034
|
-
this.speed = expTo(this.speed, 0, dt, 0.1);
|
|
1035
|
-
this.yawRate = expTo(this.yawRate, 0, dt, 0.1);
|
|
1036
|
-
this.velocity.set(0, 0, 0);
|
|
1037
|
-
}
|
|
1038
|
-
if (this.heading > Math.PI * 64 || this.heading < -Math.PI * 64) this.heading = angDiff(0, this.heading);
|
|
1039
|
-
if (this.mode !== 'perch') this.pos.y = this.terrainH(this.pos.x, this.pos.z);
|
|
1040
|
-
this.stepLegs(dt);
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
stepLegs(dt) {
|
|
1044
|
-
const cfg = this.cfg, P = this.P;
|
|
1045
|
-
const v = this.speed, vn = v / cfg.sq;
|
|
1046
|
-
// (turning on the spot is stepped by the pivot below, not by the gait cycle)
|
|
1047
|
-
const vEff = this._standTurn || this.pivot ? vn : Math.max(vn, Math.abs(this.yawRate) * 0.12);
|
|
1048
|
-
const g = this.gaitAt(vEff);
|
|
1049
|
-
// a foot lands turned toward the heading the body will have at mid-stance (a turning body then
|
|
1050
|
-
// twists over its planted foot by half as much, both ways, instead of all of it one way)
|
|
1051
|
-
this._yawAhead = this.pivot ? 0 : clamp(0.5 * g.D / Math.max(g.f, 0.5), 0.05, 0.25);
|
|
1052
|
-
const moving = vEff > 0.04 && this.mode === 'ground';
|
|
1053
|
-
if (moving) this.phase = wrap01(this.phase + g.f * dt);
|
|
1054
|
-
const st = this.state.stats;
|
|
1055
|
-
st.gait = moving ? g.name : 'stand'; st.freq = moving ? g.f : 0; st.duty = g.D;
|
|
1056
|
-
const F = this.F;
|
|
1057
|
-
const legGait = P.legGait;
|
|
1058
|
-
// dead (limp legs) and sliding on (a bird killed in flight): the feet go where the body takes
|
|
1059
|
-
// them; their plants follow, so standing up starts from where the feet really are
|
|
1060
|
-
// (only while the legs are fully free: the ground targets are then unused, so moving them is invisible)
|
|
1061
|
-
if (P.legFree >= 0.995 && this.mode === 'ground' && this.speed > 0.01 * cfg.sq && this.actionsLayer.postures.some(isDeathPosture)) {
|
|
1062
|
-
for (const leg of this.legs) {
|
|
1063
|
-
if (!leg.mtpReal) continue;
|
|
1064
|
-
leg.plant.set(leg.mtpReal.x, 0, leg.mtpReal.z);
|
|
1065
|
-
leg.plant.y = this.terrainH(leg.plant.x, leg.plant.z);
|
|
1066
|
-
leg.state = 'stance'; leg.stanceTime = 0;
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
let swinging = 0;
|
|
1070
|
-
for (const leg of this.legs) if (leg.state === 'swing') swinging++;
|
|
1071
|
-
// turning on the spot: a foot lifts, the body turns while it swings (the other foot is the pivot)
|
|
1072
|
-
// and it lands under the new heading; after a moment on both feet the other foot follows - birds
|
|
1073
|
-
// turn round in small steps (the body used to spin at a constant rate over its planted feet)
|
|
1074
|
-
// The inside foot opens the turn; each step turns the body by 45 deg at most (3-4 steps to turn round).
|
|
1075
|
-
if (this._standTurn && !this.pivot && swinging === 0 && this.mode === 'ground' && legGait > 0.5 && !this.input.follow) {
|
|
1076
|
-
const err = angDiff(this.heading, this.wantHeading);
|
|
1077
|
-
if (Math.abs(err) > 0.1 && Math.abs(this.yawRate) < 0.3 && this.legs.every((l) => l.state === 'stance' && l.stanceTime > 0.05 * cfg.sq)) {
|
|
1078
|
-
const inside = this.legs[err > 0 ? 0 : 1], outside = this.legs[err > 0 ? 1 : 0];
|
|
1079
|
-
const tw = (lg) => Math.abs(angDiff(lg.yaw, this.heading));
|
|
1080
|
-
const leg = tw(outside) > tw(inside) + 0.15 ? outside : inside;
|
|
1081
|
-
const dur = 0.22 * Math.sqrt(cfg.legLen / 0.22);
|
|
1082
|
-
this.pivot = { h0: this.heading, dh: clamp(err, -0.8, 0.8), t: 0, dur };
|
|
1083
|
-
this.startSwing(leg, dur, g, true);
|
|
1084
|
-
swinging++;
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
for (const leg of this.legs) {
|
|
1088
|
-
const L = leg.def;
|
|
1089
|
-
const p = wrap01(this.phase - g.off[L.idx]);
|
|
1090
|
-
if (p < leg.prevPhase - 0.5) leg.windowUsed = false;
|
|
1091
|
-
if (this.mode === 'perch' || this.mode === 'takeoff') {
|
|
1092
|
-
leg.state = 'stance'; leg.u = 0.3;
|
|
1093
|
-
} else if (leg.state === 'stance') {
|
|
1094
|
-
leg.stanceTime += dt;
|
|
1095
|
-
leg.u = clamp(p / g.D, 0, 1);
|
|
1096
|
-
if (moving && legGait > 0.5) {
|
|
1097
|
-
leg.mode = 'gait';
|
|
1098
|
-
const jw = this.bodyToWorld(L.cx * L.side, 0, L.cz, this.heading, this.pos, tv());
|
|
1099
|
-
const behind = (leg.plant.x - jw.x) * F.x + (leg.plant.z - jw.z) * F.z;
|
|
1100
|
-
const over = behind < -0.62 * cfg.legLen && p > g.D * 0.3;
|
|
1101
|
-
if (((p >= g.D && leg.stanceTime > 0.3 * g.D / g.f) || over) && !leg.windowUsed) {
|
|
1102
|
-
leg.windowUsed = true;
|
|
1103
|
-
const remain = Math.max((1 - p) / g.f, 0.45 * (1 - g.D) / g.f);
|
|
1104
|
-
this.startSwing(leg, remain, g, false);
|
|
1105
|
-
}
|
|
1106
|
-
} else if (legGait > 0.5) {
|
|
1107
|
-
leg.u = lerp(leg.u, 0.3, 1 - Math.exp(-dt * 6));
|
|
1108
|
-
const nC = this.neutralContact(L, 0, 0, tv(), leg);
|
|
1109
|
-
const err = nC.distanceTo(leg.plant) + Math.abs(angDiff(leg.yaw, this.heading)) * 0.25 * cfg.legLen;
|
|
1110
|
-
if (err > 0.13 * cfg.legLen && swinging === 0 && leg.stanceTime > 0.22 && P.restep > 0.5) {
|
|
1111
|
-
this.startSwing(leg, 0.2 * cfg.sq, g, true);
|
|
1112
|
-
swinging++;
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
} else {
|
|
1116
|
-
leg.swingT += dt / leg.swingDur;
|
|
1117
|
-
leg.w = clamp(leg.swingT, 0, 1);
|
|
1118
|
-
const remain = (1 - leg.w) * leg.swingDur;
|
|
1119
|
-
// (continuous in the speed: no jump of the landing point when the bird stops)
|
|
1120
|
-
const stanceDur = g.D / Math.max(g.f, 1e-3);
|
|
1121
|
-
const lead = Math.min(v * stanceDur * 0.5, 0.45 * cfg.legLen);
|
|
1122
|
-
this.neutralContact(L, lead, remain, leg.land, leg);
|
|
1123
|
-
leg.landLocal.set(L.cx * L.side + leg.idleOff.x, 0, L.cz + lead + leg.idleOff.z);
|
|
1124
|
-
if (leg.swingT >= 1) {
|
|
1125
|
-
leg.state = 'stance'; leg.windowUsed = false;
|
|
1126
|
-
leg.plant.copy(leg.land);
|
|
1127
|
-
this.terrainN(leg.plant.x, leg.plant.z, leg.plantN);
|
|
1128
|
-
leg.yaw = this.heading + this.yawRate * this._yawAhead;
|
|
1129
|
-
leg.stanceTime = 0; leg.u = 0;
|
|
1130
|
-
if (legGait > 0.3) this.emit('footstep', { foot: L.S, position: leg.plant.clone(), speed: v, strength: clamp(0.3 + vn / 3, 0, 1) * (leg.mode === 'idle' ? 0.5 : 1) });
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
leg.prevPhase = p;
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
startSwing(leg, dur, g, idle) {
|
|
1138
|
-
this.worldToBody(leg.plant, leg.liftLocal);
|
|
1139
|
-
leg.state = 'swing';
|
|
1140
|
-
leg.mode = idle ? 'idle' : 'gait';
|
|
1141
|
-
leg.swingT = 0; leg.w = 0;
|
|
1142
|
-
leg.swingDur = Math.max(0.07, dur);
|
|
1143
|
-
leg.liftH = idle ? 0.018 * this.cfg.s + 0.25 * this.cfg.footLen : g.lift;
|
|
1144
|
-
leg.liftYaw = leg.yaw;
|
|
1145
|
-
leg.land.copy(leg.plant);
|
|
1146
|
-
leg.landLocal.copy(leg.liftLocal);
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
// current ground target of a leg (MTP ground point + foot yaw), swing paths in body space
|
|
1150
|
-
groundFoot(leg, out) {
|
|
1151
|
-
if (leg.state === 'stance') return out.copy(leg.plant);
|
|
1152
|
-
const w = leg.w, a = leg.liftLocal, b = leg.landLocal;
|
|
1153
|
-
const s = smooth(0, 1, w * 1.05 - 0.02);
|
|
1154
|
-
this.bodyToWorld(lerp(a.x, b.x, s), 0, lerp(a.z, b.z, s), this.heading, this.pos, out);
|
|
1155
|
-
const gh = this.terrainH(out.x, out.z);
|
|
1156
|
-
const bump = Math.pow(Math.sin(Math.PI * Math.pow(w, 0.9)), 1.2);
|
|
1157
|
-
const y0 = lerp(a.y, gh, smooth(0, 0.6, w));
|
|
1158
|
-
out.y = Math.max(y0 + leg.liftH * bump, gh + 0.002 * bump);
|
|
1159
|
-
return out;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// ----------------------------------------------------------------- take-off
|
|
1163
|
-
startTakeoff(opts = {}) {
|
|
1164
|
-
if (this.mode === 'takeoff' || this.mode === 'air' || this.mode === 'landing') return;
|
|
1165
|
-
const cfg = this.cfg, t = cfg.takeoff;
|
|
1166
|
-
const fromPerch = this.mode === 'perch';
|
|
1167
|
-
this.pivot = null;
|
|
1168
|
-
this.to = {
|
|
1169
|
-
t: 0, crouch: t.crouch * cfg.sq * (opts.quick ? 0.6 : 1), air: false, fromPerch,
|
|
1170
|
-
vy: opts.vy ?? t.jump * (fromPerch ? 0.35 : 1), vh: opts.vh ?? Math.max(this.speed, t.jump * Math.cos(t.angle) * 0.9),
|
|
1171
|
-
hopOnly: !!opts.hopOnly, target: opts.target || null, then: opts.then || null, flapLift: opts.flapLift ?? true, reach: 0,
|
|
1172
|
-
push: 0.12 * cfg.sq, pushing: false, y0: 0,
|
|
1173
|
-
};
|
|
1174
|
-
if (fromPerch) this.perchLeave = this.perch;
|
|
1175
|
-
this.mode = 'takeoff';
|
|
1176
|
-
this.emit('actionStart', { name: 'takeoff' });
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
stepTakeoff(dt) {
|
|
1180
|
-
const T = this.to, cfg = this.cfg;
|
|
1181
|
-
T.t += dt;
|
|
1182
|
-
if (!T.air) {
|
|
1183
|
-
// crouch, then push off: the body rises on extending legs until the feet leave the ground
|
|
1184
|
-
if (T.t >= T.crouch && !T.pushing) { T.pushing = true; T.y0 = this.body.y; }
|
|
1185
|
-
if (T.pushing) { const tau = clamp((T.t - T.crouch) / T.push, 0, 1); this.pos.addScaledVector(this.F, T.vh * (tau - Math.sin(TAU * tau) / TAU) * dt); }
|
|
1186
|
-
if (T.t >= T.crouch + T.push) {
|
|
1187
|
-
T.air = true;
|
|
1188
|
-
T.pushing = false;
|
|
1189
|
-
T.tAir = 0;
|
|
1190
|
-
this.bodyV.set(0, T.vy, 0).addScaledVector(this.F, T.vh);
|
|
1191
|
-
for (const leg of this.legs) { leg.frozen.copy(leg.mtp); leg.frozenOn = true; }
|
|
1192
|
-
if (T.hopOnly) {
|
|
1193
|
-
// (the hop's flight time - up with the wing beats carrying 20 % of the weight, down 45 % - and
|
|
1194
|
-
// the spots the feet will land on, under the body there: the stance plants glide there)
|
|
1195
|
-
const up = T.flapLift ? 0.8 : 1, dn = T.flapLift ? 0.55 : 1;
|
|
1196
|
-
const h = (T.vy * T.vy) / (2 * G * up);
|
|
1197
|
-
T.tFlight = Math.max(0.05, T.vy / (G * up) + Math.sqrt((2 * h) / (G * dn)));
|
|
1198
|
-
const sp0 = this.speed; this.speed = T.vh;
|
|
1199
|
-
for (const leg of this.legs) {
|
|
1200
|
-
leg.hop0 = (leg.hop0 || new THREE.Vector3()).copy(leg.plant);
|
|
1201
|
-
this.neutralContact(leg.def, 0, T.tFlight, leg.hop1 || (leg.hop1 = new THREE.Vector3()), leg);
|
|
1202
|
-
}
|
|
1203
|
-
this.speed = sp0;
|
|
1204
|
-
}
|
|
1205
|
-
this.vy = T.vy;
|
|
1206
|
-
this.speed = T.vh;
|
|
1207
|
-
this.state.grounded = false;
|
|
1208
|
-
this.perch = null;
|
|
1209
|
-
this.fl.time = 0;
|
|
1210
|
-
this.emit('takeoff', {});
|
|
1211
|
-
} else return;
|
|
1212
|
-
}
|
|
1213
|
-
T.tAir += dt;
|
|
1214
|
-
if (T.hopOnly) { this.stepHop(dt, T); return; }
|
|
1215
|
-
this.stepAir(dt, T);
|
|
1216
|
-
const done = T.hopOnly ? false : T.tAir > 0.35 && (this.altitude() > cfg.flight.clearance * 0.7 || T.tAir > 2.5);
|
|
1217
|
-
if (done) {
|
|
1218
|
-
this.mode = 'air';
|
|
1219
|
-
this.to = null;
|
|
1220
|
-
this.emit('actionEnd', { name: 'takeoff' });
|
|
1221
|
-
this.actionsLayer.onAirborne();
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
// ballistic hop (jump): gravity eased by a few wing beats; lands where the feet meet the ground
|
|
1226
|
-
stepHop(dt, T) {
|
|
1227
|
-
const cfg = this.cfg, F = this.F;
|
|
1228
|
-
// wing beats carry part of the weight, more so on the way down (braking the landing)
|
|
1229
|
-
const lift = T.flapLift ? (this.bodyV.y < 0 ? 0.55 : 0.8) : 1;
|
|
1230
|
-
this.bodyV.y -= G * lift * dt;
|
|
1231
|
-
let hDead = Infinity;
|
|
1232
|
-
if (T.dead) {
|
|
1233
|
-
// a dead bird's fall ends in a crumple, not a one-sample stop: over the last decimetres the
|
|
1234
|
-
// impact is absorbed (feathers, folding legs and wings) with a ~60 ms time constant, measured
|
|
1235
|
-
// from whichever reaches the ground first, the body or the (limp) feet
|
|
1236
|
-
// (the dead body comes to rest at the posture's ground height - the drop clamps at sitting -
|
|
1237
|
-
// reached a little above it: the rolled body's breast lies lower than in the settled pose)
|
|
1238
|
-
const sitH = this.sitHeight();
|
|
1239
|
-
hDead = this.body.y - this.terrainH(this.body.x, this.body.z) - sitH - 0.15 * (cfg.pivotY - sitH);
|
|
1240
|
-
let h = hDead;
|
|
1241
|
-
for (const leg of this.legs) if (leg.mtpReal) h = Math.min(h, leg.mtpReal.y - cfg.feet.pad - this.terrainH(leg.mtpReal.x, leg.mtpReal.z));
|
|
1242
|
-
const vMin = -(0.2 * cfg.sq + Math.max(0, h) / 0.08);
|
|
1243
|
-
if (this.bodyV.y < vMin) this.bodyV.y = vMin;
|
|
1244
|
-
}
|
|
1245
|
-
// the ground plants of the planted legs through the hop (a leg whose air weight is not yet full -
|
|
1246
|
-
// a short hop - reaches toward its plant: left at the take-off spot, it dragged the feet back, so
|
|
1247
|
-
// they touched down late with the body below sitting height, and it snapped up):
|
|
1248
|
-
// - a hop: the plants glide on a fixed path from the take-off spot to the predicted landing spot
|
|
1249
|
-
// (fed from the feet's own positions frame by frame instead, the toes of a running jump jittered)
|
|
1250
|
-
// - a dead fall: under the limp feet (the touchdown plants each foot where it already is)
|
|
1251
|
-
// A leg caught mid-swing keeps its body-relative swing path and its state.
|
|
1252
|
-
if (T.tAir > 0.02) {
|
|
1253
|
-
for (const leg of this.legs) {
|
|
1254
|
-
if (leg.state !== 'stance') continue;
|
|
1255
|
-
if (T.dead) { if (leg.mtpReal) { leg.plant.set(leg.mtpReal.x, 0, leg.mtpReal.z); leg.plant.y = this.terrainH(leg.plant.x, leg.plant.z); } }
|
|
1256
|
-
else if (leg.hop0 && T.tFlight) {
|
|
1257
|
-
const u = smoother(T.tAir / T.tFlight);
|
|
1258
|
-
leg.plant.lerpVectors(leg.hop0, leg.hop1, u);
|
|
1259
|
-
leg.plant.y = this.terrainH(leg.plant.x, leg.plant.z);
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
const vh = Math.hypot(this.bodyV.x, this.bodyV.z);
|
|
1264
|
-
this.body.addScaledVector(this.bodyV, dt);
|
|
1265
|
-
this.speed = vh; this.vy = this.bodyV.y;
|
|
1266
|
-
this.yawRate = expTo(this.yawRate, 0, dt, 0.1);
|
|
1267
|
-
this.velocity.copy(this.bodyV);
|
|
1268
|
-
this.pos.set(this.body.x, this.body.y - cfg.pivotY, this.body.z);
|
|
1269
|
-
this.forward(this.heading, F);
|
|
1270
|
-
// (a dead bird does not reach for the ground: its legs stay limp)
|
|
1271
|
-
T.reach = T.dead ? 0 : smooth(0.1, -0.6, this.vy) * smooth(0.12, 0.3, T.tAir);
|
|
1272
|
-
const L = cfg.legs[0];
|
|
1273
|
-
// touchdown when the (last posed) feet reach the ground within this frame's fall
|
|
1274
|
-
let feetY = this.body.y - L.len * 0.86 * lerp(0.6, 1, T.reach) - cfg.feet.pad, g0 = this.terrainH(this.body.x, this.body.z);
|
|
1275
|
-
if (this.legs[0].mtpReal && this.legs[1].mtpReal) {
|
|
1276
|
-
feetY = Infinity;
|
|
1277
|
-
for (const leg of this.legs) {
|
|
1278
|
-
const m = leg.mtpReal, gh = this.terrainH(m.x, m.z);
|
|
1279
|
-
feetY = Math.min(feetY, m.y - cfg.feet.pad - gh + this.vy * dt);
|
|
1280
|
-
}
|
|
1281
|
-
g0 = 0;
|
|
1282
|
-
}
|
|
1283
|
-
if (T.tAir > 0.12 && this.vy <= 0 && (feetY <= g0 || hDead <= 0 || T.touch)) {
|
|
1284
|
-
this.to = null;
|
|
1285
|
-
this.mode = 'air';
|
|
1286
|
-
this.ld = null;
|
|
1287
|
-
this.touchDown();
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
// perch description from a target point (the top of the branch where the feet grip)
|
|
1292
|
-
makePerch(target, o = {}) {
|
|
1293
|
-
const cfg = this.cfg;
|
|
1294
|
-
const point = target.isVector3 ? target.clone() : new THREE.Vector3(target.x, target.y, target.z);
|
|
1295
|
-
const r = o.radius ?? 0.02 * cfg.s + 0.006;
|
|
1296
|
-
// branch axis: given, else across the approach direction
|
|
1297
|
-
let axis;
|
|
1298
|
-
if (o.axis) axis = new THREE.Vector3().copy(o.axis).setY(0).normalize();
|
|
1299
|
-
else { const dx = point.x - this.pos.x, dz = point.z - this.pos.z; const l = Math.hypot(dx, dz) || 1; axis = new THREE.Vector3(dz / l, 0, -dx / l); }
|
|
1300
|
-
// toes wrap the branch: flexion so the toe arcs follow its circumference
|
|
1301
|
-
const curl = clamp((cfg.footLen * 0.5) / (r + 0.004 * cfg.s), 0.2, 1.4);
|
|
1302
|
-
return { point, top: point.y, axis, radius: r, curl };
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
// dead in the air: fall ballistically (no wing lift) and collapse where it hits the ground
|
|
1306
|
-
fall() {
|
|
1307
|
-
// (the limp body's pose while falling is relative to its attitude now: flight level, or standing)
|
|
1308
|
-
this._fallPitch0 = this.pitchS.x; this._fallBank0 = this.bankS.x;
|
|
1309
|
-
this.mode = 'takeoff';
|
|
1310
|
-
this.ld = null;
|
|
1311
|
-
// (tAir 0.3: the legs stay in their air pose, no blend back toward the ground from flight)
|
|
1312
|
-
this.to = { t: 1, crouch: 0, air: true, tAir: 0.3, hopOnly: true, flapLift: false, reach: 0, dead: true };
|
|
1313
|
-
this.bodyV.copy(this.velocity);
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
altitude() { return this.body.y - this.cfg.pivotY - this.terrainH(this.body.x, this.body.z); }
|
|
1317
|
-
|
|
1318
|
-
// ----------------------------------------------------------------- flight
|
|
1319
|
-
stepAir(dt, takeoff = null) {
|
|
1320
|
-
const cfg = this.cfg, fl = cfg.flight, F = this.F, inp = this.input, P = this.P;
|
|
1321
|
-
const air = this.fl;
|
|
1322
|
-
air.time += dt;
|
|
1323
|
-
if (inp.follow) {
|
|
1324
|
-
// an external body carries the bird: animate for its velocity, keep it off the terrain
|
|
1325
|
-
const fv = inp.follow.velocity, fp = inp.follow.position;
|
|
1326
|
-
const sp = Math.hypot(fv.x, fv.z);
|
|
1327
|
-
this.speed = expTo(this.speed, sp, dt, 0.1);
|
|
1328
|
-
const hT = sp > 0.3 ? Math.atan2(fv.x, fv.z) : this.heading;
|
|
1329
|
-
const wT = clamp(angDiff(this.heading, hT) * 5, -fl.turnRate * 3, fl.turnRate * 3);
|
|
1330
|
-
this.yawRate += clamp(wT - this.yawRate, -20 * dt, 20 * dt);
|
|
1331
|
-
this.heading += this.yawRate * dt;
|
|
1332
|
-
// (the body banks for the turn the carrier makes)
|
|
1333
|
-
this.turnBank = this.turnBankS.step(Math.atan(Math.max(this.speed, 1) * this.yawRate / G), cfg.rollOmega, dt);
|
|
1334
|
-
this._flying = true;
|
|
1335
|
-
this.forward(this.heading, F);
|
|
1336
|
-
const gy = this.terrainH(fp.x, fp.z);
|
|
1337
|
-
const y = Math.max(fp.y, gy) + cfg.pivotY + Math.max(0, fl.clearance * 0.5);
|
|
1338
|
-
this.vy = (y - this.body.y) / dt * 0.2;
|
|
1339
|
-
this.body.set(fp.x, expTo(this.body.y, y, dt, 0.25), fp.z);
|
|
1340
|
-
this.velocity.set(fv.x, this.vy, fv.z);
|
|
1341
|
-
this.pos.set(this.body.x, this.body.y - cfg.pivotY, this.body.z);
|
|
1342
|
-
return;
|
|
1343
|
-
}
|
|
1344
|
-
// take-off weight: 1 right after toe-off, easing to 0 (continuous across the switch to 'air')
|
|
1345
|
-
const tw = takeoff ? 1 - smooth(0.35, 1.3, takeoff.tAir) : Math.max(0, (this.toW || 0) - dt / 0.6);
|
|
1346
|
-
this.toW = tw;
|
|
1347
|
-
// --- airspeed
|
|
1348
|
-
let vT = clamp(Math.max(this.wantSpeed, fl.minSpeed), fl.minSpeed, fl.maxSpeed);
|
|
1349
|
-
vT = lerp(vT, Math.max(fl.minSpeed, Math.min(vT, fl.cruise)), tw);
|
|
1350
|
-
const drag = 0.9 + 0.012 * this.speed * this.speed / cfg.s;
|
|
1351
|
-
const accMax = lerp(2.5, 6, air.power) * (1 + 0.4 * tw);
|
|
1352
|
-
this.speed += clamp(vT - this.speed, -drag * dt * 2.2, accMax * dt);
|
|
1353
|
-
if (takeoff && takeoff.tAir < 0.3) this.speed = Math.max(this.speed, takeoff.vh);
|
|
1354
|
-
// --- heading: banked, coordinated turns. The bird rolls first and the tilted lift turns it: the
|
|
1355
|
-
// bank follows the one a turn at the wanted rate needs (critically damped roll, faster for small
|
|
1356
|
-
// wings), and the yaw rate is what that bank gives at this airspeed, g tan(bank) / v. (The yaw rate
|
|
1357
|
-
// used to lead and the body roll to follow it through a slow spring, with the wrong sign: the bird
|
|
1358
|
-
// swung out of its turns and stayed tilted after them.)
|
|
1359
|
-
const vEff = Math.max(this.speed, fl.minSpeed * 0.6);
|
|
1360
|
-
const wMax = Math.min(fl.turnRate, G * Math.tan(fl.bankMax) / vEff);
|
|
1361
|
-
// (the roll-out is anticipated: the heading still changes while the bird rolls level)
|
|
1362
|
-
const wT = clamp((angDiff(this.heading, this.wantHeading) - this.yawRate * 1.7 / cfg.rollOmega) * 1.8, -wMax, wMax);
|
|
1363
|
-
if (!this._flying) { this.turnBankS.reset(Math.atan(vEff * this.yawRate / G)); this._flying = true; }
|
|
1364
|
-
this.turnBank = this.turnBankS.step(Math.atan(vEff * wT / G), cfg.rollOmega, dt);
|
|
1365
|
-
this.yawRate = G * Math.tan(this.turnBank) / vEff;
|
|
1366
|
-
this.heading += this.yawRate * dt;
|
|
1367
|
-
if (this.heading > Math.PI * 64 || this.heading < -Math.PI * 64) this.heading = angDiff(0, this.heading);
|
|
1368
|
-
this.forward(this.heading, F);
|
|
1369
|
-
// --- climb: input, terrain clearance (look ahead), take-off climb
|
|
1370
|
-
const alt = this.altitude();
|
|
1371
|
-
let vyT = this.wantClimb;
|
|
1372
|
-
vyT = lerp(vyT, Math.max(vyT, Math.tan(cfg.takeoff.angle) * Math.max(this.speed, 2)), tw);
|
|
1373
|
-
const aheadG = this.terrainH(this.body.x + F.x * this.speed * 0.6, this.body.z + F.z * this.speed * 0.6);
|
|
1374
|
-
const altAhead = this.body.y - cfg.pivotY - aheadG;
|
|
1375
|
-
const minAlt = Math.min(alt, altAhead);
|
|
1376
|
-
const clr = this._clearance ?? fl.clearance;
|
|
1377
|
-
if (minAlt < clr) vyT = Math.max(vyT, (clr - minAlt) * 1.8);
|
|
1378
|
-
if (this.fl.forceGlide > 0.5 && !takeoff) vyT = Math.min(vyT, -this.speed / fl.glideRatio);
|
|
1379
|
-
vyT = clamp(vyT, fl.climbDown, fl.climbUp * (1 + 0.4 * tw));
|
|
1380
|
-
this.vy += clamp(vyT - this.vy, -6 * dt, 6 * dt);
|
|
1381
|
-
// --- integrate
|
|
1382
|
-
this.body.addScaledVector(F, this.speed * dt);
|
|
1383
|
-
this.body.y += this.vy * dt;
|
|
1384
|
-
if (this.push.lengthSq() > 1e-8) { this.body.addScaledVector(this.push, dt); this.push.multiplyScalar(Math.exp(-dt * 5)); }
|
|
1385
|
-
// hard floor: never below the terrain (legs tucked)
|
|
1386
|
-
const floor = this.terrainH(this.body.x, this.body.z) + cfg.pivotY * 0.75;
|
|
1387
|
-
if (this.body.y < floor) { this.body.y = floor; this.vy = Math.max(this.vy, 0); }
|
|
1388
|
-
this.velocity.copy(F).multiplyScalar(this.speed); this.velocity.y = this.vy;
|
|
1389
|
-
this.pos.set(this.body.x, this.body.y - cfg.pivotY, this.body.z);
|
|
1390
|
-
// --- power demand: slow flight, climbing, accelerating; gliding when descending
|
|
1391
|
-
const vr = this.speed / fl.cruise;
|
|
1392
|
-
let pw = 0.5 + 0.35 * Math.max(0, 1 - vr) * 2 + 0.2 * Math.max(0, vr - 1.15);
|
|
1393
|
-
pw += 0.4 * Math.max(0, this.vy) / fl.climbUp + 0.2 * Math.max(0, vT - this.speed) / 3;
|
|
1394
|
-
pw = Math.max(pw, lerp(0, 1, tw));
|
|
1395
|
-
pw = clamp(pw, 0, 1);
|
|
1396
|
-
air.power = air.powerS.step(pw, 6, dt);
|
|
1397
|
-
// intermittent gliding (flap-glide), and gliding when descending
|
|
1398
|
-
const w = cfg.wing;
|
|
1399
|
-
let glideWant = this.vy < -0.35 * this.speed / fl.glideRatio * 2 && !takeoff ? 1 : 0;
|
|
1400
|
-
if (!takeoff && w.glideShare > 0 && this.vy > -0.5 && pw < 0.72 + 0.2 * (w.soar || 0)) {
|
|
1401
|
-
air.glideT -= dt;
|
|
1402
|
-
if (air.glideT <= 0) {
|
|
1403
|
-
air.gliding = air.gliding ? 0 : 1;
|
|
1404
|
-
const share = Math.min(0.95, w.glideShare + 0.5 * (w.soar || 0)); // soarers: long glides, few beats
|
|
1405
|
-
air.glideT = air.gliding ? (0.6 + this.rand() * 1.4) * (0.5 + share * 3) : (2 + this.rand() * 4) * (1.2 - share);
|
|
1406
|
-
}
|
|
1407
|
-
glideWant = Math.max(glideWant, air.gliding);
|
|
1408
|
-
}
|
|
1409
|
-
if (this.fl.forceGlide > 0.5 && !takeoff) glideWant = 1;
|
|
1410
|
-
if (this.fl.forceFlap > 0.5) glideWant = 0;
|
|
1411
|
-
air.glide = air.glideS.step(glideWant, 4, dt);
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
// ----------------------------------------------------------------- landing
|
|
1415
|
-
// target: world point where the feet touch (ground or perch top); null = ahead on the ground
|
|
1416
|
-
startLanding(target, why, perch = null) {
|
|
1417
|
-
const cfg = this.cfg;
|
|
1418
|
-
if (this.mode === 'ground' || this.mode === 'perch') return false;
|
|
1419
|
-
if (this.mode === 'takeoff') { this.mode = 'air'; this.to = null; }
|
|
1420
|
-
const F = this.forward(this.heading, tv());
|
|
1421
|
-
const tgt = new THREE.Vector3();
|
|
1422
|
-
if (target) tgt.copy(target);
|
|
1423
|
-
else {
|
|
1424
|
-
const d = Math.max(3 * cfg.legLen, Math.min(this.speed * 1.2 + this.altitude() * 1.5, 12 * cfg.s + this.speed));
|
|
1425
|
-
tgt.set(this.body.x + F.x * d, 0, this.body.z + F.z * d);
|
|
1426
|
-
tgt.y = this.terrainH(tgt.x, tgt.z);
|
|
1427
|
-
}
|
|
1428
|
-
const dx = tgt.x - this.body.x, dz = tgt.z - this.body.z;
|
|
1429
|
-
const dist = Math.hypot(dx, dz);
|
|
1430
|
-
const dy = tgt.y + cfg.pivotY - this.body.y;
|
|
1431
|
-
this.ld = {
|
|
1432
|
-
target: tgt, perch, why, t: 0, phase: 'approach', p0: this.body.clone(), v0: this.velocity.clone(),
|
|
1433
|
-
T: 0, dist, dy, flare: 0, touched: false, reach: 0,
|
|
1434
|
-
};
|
|
1435
|
-
this.mode = 'landing';
|
|
1436
|
-
this.emit('actionStart', { name: perch ? 'perch' : 'land' });
|
|
1437
|
-
return true;
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
stepLanding(dt) {
|
|
1441
|
-
const cfg = this.cfg, fl = cfg.flight, L = this.ld, air = this.fl;
|
|
1442
|
-
L.t += dt;
|
|
1443
|
-
air.time += dt;
|
|
1444
|
-
const tgt = L.target;
|
|
1445
|
-
// an automatic landing (slowed down, a burst flyer out of breath) still follows the steering: its
|
|
1446
|
-
// touch point stays ahead along the wanted heading, as far as it is (fixed where the landing began,
|
|
1447
|
-
// a hen tired of flying ignored a 60 deg turn to the ground)
|
|
1448
|
-
if (L.phase === 'approach' && L.why === 'auto' && !L.perch && !this.input.follow) {
|
|
1449
|
-
const h = this.input.heading ?? this.heading, d = Math.hypot(tgt.x - this.body.x, tgt.z - this.body.z);
|
|
1450
|
-
tgt.set(this.body.x + Math.sin(h) * d, 0, this.body.z + Math.cos(h) * d);
|
|
1451
|
-
tgt.y = this.terrainH(tgt.x, tgt.z);
|
|
1452
|
-
}
|
|
1453
|
-
// aim: the body pivot above the touch point, standing height (a little higher for a perch grip)
|
|
1454
|
-
const aimY = tgt.y + cfg.pivotY * (L.perch ? 0.92 : 1.0);
|
|
1455
|
-
const dx = tgt.x - this.body.x, dz = tgt.z - this.body.z;
|
|
1456
|
-
const dist = Math.hypot(dx, dz);
|
|
1457
|
-
const hDes = Math.atan2(dx, dz);
|
|
1458
|
-
const vApp = Math.max(cfg.landing.approach, fl.minSpeed * 1.1);
|
|
1459
|
-
if (L.phase === 'approach') {
|
|
1460
|
-
// fly toward the target on a descending path; start the final (guided) segment when close
|
|
1461
|
-
// a target inside the turning circle: fly out straight, then come back round (no endless orbit)
|
|
1462
|
-
const R = this.speed * this.speed / (G * Math.tan(fl.bankMax)) + 0.5 * cfg.legLen;
|
|
1463
|
-
const err0 = Math.abs(angDiff(this.heading, hDes));
|
|
1464
|
-
if (!L.extend && err0 > 1.2 && dist < 2.2 * R) L.extend = true;
|
|
1465
|
-
if (L.extend && dist > 3.2 * R) L.extend = false;
|
|
1466
|
-
this.wantHeading = L.extend ? this.heading : hDes;
|
|
1467
|
-
const wantV = clamp(dist * 0.9, vApp, Math.max(vApp, Math.min(this.speed, fl.cruise)));
|
|
1468
|
-
this.wantSpeed = wantV;
|
|
1469
|
-
const hdgErr = Math.abs(angDiff(this.heading, hDes));
|
|
1470
|
-
const tFinal = Math.max(0.9, 2.2 * dist / Math.max(1, this.speed + cfg.landing.touch));
|
|
1471
|
-
const dh = aimY - this.body.y;
|
|
1472
|
-
// descend along a glide slope toward the aim point
|
|
1473
|
-
const slope = dist > 0.1 ? dh / dist : 0;
|
|
1474
|
-
this.wantClimb = clamp(slope * this.speed * 1.2, fl.climbDown, fl.climbUp);
|
|
1475
|
-
// terrain clearance gives way to the approach (the target may be low, a perch below it)
|
|
1476
|
-
this._clearance = Math.min(fl.clearance, Math.max(tgt.y - this.terrainH(tgt.x, tgt.z), 0) + 0.25 * dist);
|
|
1477
|
-
this.stepAirLanding(dt);
|
|
1478
|
-
this._clearance = null;
|
|
1479
|
-
if ((dist < Math.max(this.speed * 1.15, 2.5 * cfg.legLen) && hdgErr < 0.6) || (dist < 1.2 * cfg.legLen) || L.t > 12) {
|
|
1480
|
-
// final segment: cubic Hermite from the current state to the touch point
|
|
1481
|
-
L.phase = 'final';
|
|
1482
|
-
L.p0.copy(this.body);
|
|
1483
|
-
L.v0.copy(this.velocity);
|
|
1484
|
-
const vTouch = cfg.landing.touch;
|
|
1485
|
-
L.T = clamp((2 * dist) / Math.max(0.5, this.speed + vTouch), 0.55, 2.6);
|
|
1486
|
-
if (tFinal < L.T) L.T = Math.max(0.55, tFinal);
|
|
1487
|
-
L.tf = 0;
|
|
1488
|
-
L.p1 = new THREE.Vector3(tgt.x, aimY, tgt.z);
|
|
1489
|
-
const h = dist > 1e-3 ? hDes : this.heading;
|
|
1490
|
-
L.v1 = new THREE.Vector3(Math.sin(h) * vTouch * 0.5, -vTouch * 0.25, Math.cos(h) * vTouch * 0.5);
|
|
1491
|
-
L.h1 = h;
|
|
1492
|
-
// a perch: settle facing across the branch (the side nearer the approach)
|
|
1493
|
-
if (L.perch) {
|
|
1494
|
-
const a = L.perch.axis, hn = Math.atan2(a.z, -a.x);
|
|
1495
|
-
L.h1 = Math.abs(angDiff(h, hn)) < Math.PI / 2 ? hn : hn + Math.PI;
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
return;
|
|
1499
|
-
}
|
|
1500
|
-
// final: follow the Hermite curve exactly (arrives at T); flare in the last part
|
|
1501
|
-
L.tf += dt;
|
|
1502
|
-
const T = L.T, u = clamp(L.tf / T, 0, 1);
|
|
1503
|
-
const h00 = 2 * u * u * u - 3 * u * u + 1, h10 = u * u * u - 2 * u * u + u, h01 = -2 * u * u * u + 3 * u * u, h11 = u * u * u - u * u;
|
|
1504
|
-
const p0 = L.p0, v0 = L.v0, p1 = L.p1, v1 = L.v1;
|
|
1505
|
-
const nx = h00 * p0.x + h10 * T * v0.x + h01 * p1.x + h11 * T * v1.x;
|
|
1506
|
-
const ny = h00 * p0.y + h10 * T * v0.y + h01 * p1.y + h11 * T * v1.y;
|
|
1507
|
-
const nz = h00 * p0.z + h10 * T * v0.z + h01 * p1.z + h11 * T * v1.z;
|
|
1508
|
-
// keep clear of the terrain along the way (except at the very end)
|
|
1509
|
-
const clearY = this.terrainH(nx, nz) + cfg.pivotY * lerp(1.2, 0.8, smooth(0.7, 1, u));
|
|
1510
|
-
const vx = (nx - this.body.x) / dt, vz = (nz - this.body.z) / dt;
|
|
1511
|
-
this.body.set(nx, Math.max(ny, u < 0.95 ? clearY : ny), nz);
|
|
1512
|
-
const sp = Math.hypot(vx, vz);
|
|
1513
|
-
this.velocity.set(vx, 0, vz);
|
|
1514
|
-
// derivative of the curve for the vertical speed and the heading
|
|
1515
|
-
const d00 = 6 * u * u - 6 * u, d10 = 3 * u * u - 4 * u + 1, d01 = -6 * u * u + 6 * u, d11 = 3 * u * u - 2 * u;
|
|
1516
|
-
this.vy = (d00 * p0.y + d10 * T * v0.y + d01 * p1.y + d11 * T * v1.y) / T;
|
|
1517
|
-
this.velocity.y = this.vy;
|
|
1518
|
-
this.speed = sp;
|
|
1519
|
-
if (sp > 0.3 || L.perch) {
|
|
1520
|
-
let hT = sp > 0.3 ? Math.atan2(vx, vz) : this.heading;
|
|
1521
|
-
if (L.perch) hT = this.heading + angDiff(this.heading, hT) * (1 - smooth(0.4, 0.95, u)) + angDiff(this.heading, L.h1) * smooth(0.4, 0.95, u);
|
|
1522
|
-
this.yawRate = clamp(angDiff(this.heading, hT) / Math.max(dt, 1e-3), -6, 6) * 0.3;
|
|
1523
|
-
this.heading += this.yawRate * dt;
|
|
1524
|
-
} else this.yawRate *= 0.9;
|
|
1525
|
-
// (banked for the turn the path makes)
|
|
1526
|
-
this.turnBank = this.turnBankS.step(Math.atan(Math.max(sp, 1) * this.yawRate / G) * (1 - smooth(0.5, 0.9, u)), cfg.rollOmega, dt);
|
|
1527
|
-
this.forward(this.heading, this.F);
|
|
1528
|
-
this.pos.set(this.body.x, this.body.y - cfg.pivotY, this.body.z);
|
|
1529
|
-
// flare / braking power
|
|
1530
|
-
L.flare = smooth(0.35, 0.8, u);
|
|
1531
|
-
L.reach = smooth(0.3, 0.75, u);
|
|
1532
|
-
L.feetW = smooth(0.72, 1, u);
|
|
1533
|
-
air.power = air.powerS.step(lerp(0.7, 1, L.flare), 6, dt);
|
|
1534
|
-
air.glide = air.glideS.step(u < 0.3 && L.dist > 3 ? 0.6 : 0, 4, dt);
|
|
1535
|
-
if (u >= 1) this.touchDown();
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
// approach phase: normal flight dynamics toward the landing target
|
|
1539
|
-
stepAirLanding(dt) {
|
|
1540
|
-
const keep = this.mode;
|
|
1541
|
-
this.mode = 'air';
|
|
1542
|
-
this.stepAir(dt);
|
|
1543
|
-
this.mode = keep;
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
// touch point of a foot for the current landing (ground beside the target, or the perch top)
|
|
1547
|
-
landPlant(leg, out) {
|
|
1548
|
-
const L = this.ld, Ld = leg.def, tgt = L.target, perch = L.perch;
|
|
1549
|
-
if (perch) {
|
|
1550
|
-
const a = perch.axis;
|
|
1551
|
-
return out.copy(tgt).addScaledVector(a, Ld.side * Ld.cx * (a.x * Math.cos(this.heading) - a.z * Math.sin(this.heading) >= 0 ? 1 : -1));
|
|
1552
|
-
}
|
|
1553
|
-
// neutral stance point relative to the touch target (the landing target is the body pivot's ground point)
|
|
1554
|
-
this.bodyToWorld(Ld.cx * Ld.side, 0, Ld.cz, this.heading, tgt, out);
|
|
1555
|
-
out.y = this.terrainH(out.x, out.z);
|
|
1556
|
-
return out;
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
touchDown() {
|
|
1560
|
-
const cfg = this.cfg, L = this.ld;
|
|
1561
|
-
const perch = L ? L.perch : null;
|
|
1562
|
-
this.mode = perch ? 'perch' : 'ground';
|
|
1563
|
-
this.perch = perch;
|
|
1564
|
-
const tgt = L ? L.target : this.pos;
|
|
1565
|
-
this.pos.set(tgt.x, perch ? tgt.y : this.terrainH(tgt.x, tgt.z), tgt.z);
|
|
1566
|
-
// feet plant where they are (projected on the ground / the perch)
|
|
1567
|
-
for (const leg of this.legs) {
|
|
1568
|
-
const Ld = leg.def;
|
|
1569
|
-
if (L) this.landPlant(leg, leg.plant);
|
|
1570
|
-
else {
|
|
1571
|
-
// hop: feet plant where they touch (the idle re-step brings them under the body afterwards)
|
|
1572
|
-
const mr = leg.mtpReal || leg.mtp;
|
|
1573
|
-
leg.plant.set(mr.x, 0, mr.z);
|
|
1574
|
-
leg.plant.y = this.terrainH(leg.plant.x, leg.plant.z);
|
|
1575
|
-
}
|
|
1576
|
-
this.terrainN(leg.plant.x, leg.plant.z, leg.plantN);
|
|
1577
|
-
if (perch) leg.plantN.set(0, 1, 0);
|
|
1578
|
-
leg.state = 'stance'; leg.stanceTime = 0; leg.u = 0; leg.windowUsed = false; leg.prevPhase = 0;
|
|
1579
|
-
leg.yaw = this.heading; leg.yawS.reset(this.heading);
|
|
1580
|
-
leg.frozenOn = false;
|
|
1581
|
-
this.emit('footstep', { foot: Ld.S, position: leg.plant.clone(), speed: this.speed, strength: 0.8 });
|
|
1582
|
-
}
|
|
1583
|
-
// the feet are now held by the plants (no blend from the body-relative air pose, which moves with the body)
|
|
1584
|
-
// (a dead bird's limp legs ease over: their air pose is where the feet are, and the death pose frees
|
|
1585
|
-
// them; a switch pulled the sitting ankle down in one frame when the fall was short)
|
|
1586
|
-
const deadTouch = this.actionsLayer.dead();
|
|
1587
|
-
if (!deadTouch) this.airLeg.reset(0);
|
|
1588
|
-
// body height continues from where it is (absorbs the impact)
|
|
1589
|
-
this.hY.x = this.body.y; this.hY.v = this.vy;
|
|
1590
|
-
// horizontal momentum is absorbed by the legs (body sways forward over the feet)
|
|
1591
|
-
const vh = Math.min(1, 1.2 * this.cfg.sq / Math.max(1e-3, Math.hypot(this.velocity.x, this.velocity.z)));
|
|
1592
|
-
this.landOff = { x: 0, z: 0, vx: this.velocity.x * vh, vz: this.velocity.z * vh };
|
|
1593
|
-
this._touchFrame = this.frame;
|
|
1594
|
-
this._touchTime = this.time;
|
|
1595
|
-
// gait cycle restarts with both feet down (stance start)
|
|
1596
|
-
this.phase = this.gait.off[0];
|
|
1597
|
-
this.input.speed = this.input.speed > cfg.ground.maxSpeed ? 0 : this.input.speed;
|
|
1598
|
-
// a hop while moving carries on at the requested ground speed (no stop and restart)
|
|
1599
|
-
const vHor = Math.hypot(this.velocity.x, this.velocity.z);
|
|
1600
|
-
// (a dead bird hitting the ground keeps part of its momentum: it slides / tumbles to a stop)
|
|
1601
|
-
const dead = this.actionsLayer.dead();
|
|
1602
|
-
const carry = dead ? 0.4 * vHor : !L && this.input.speed > 0.05 ? Math.min(vHor, this.input.speed) : 0;
|
|
1603
|
-
this.speed = carry; this.yawRate = 0; this.vy = 0;
|
|
1604
|
-
if (carry > 0) {
|
|
1605
|
-
this.velocity.set(Math.sin(this.heading) * carry, 0, Math.cos(this.heading) * carry);
|
|
1606
|
-
// only the velocity difference is absorbed by the legs
|
|
1607
|
-
this.landOff.vx -= this.velocity.x * vh; this.landOff.vz -= this.velocity.z * vh;
|
|
1608
|
-
} else this.velocity.set(0, 0, 0);
|
|
1609
|
-
this.input.heading = this.heading;
|
|
1610
|
-
this.state.grounded = true;
|
|
1611
|
-
this.fl.landT = this.time;
|
|
1612
|
-
this.emit('land', { position: this.pos.clone(), speed: 0 });
|
|
1613
|
-
this.emit('actionEnd', { name: perch ? 'perch' : 'land' });
|
|
1614
|
-
this.ld = null;
|
|
1615
|
-
this.actionsLayer.onLand(perch);
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
// ----------------------------------------------------------------- pose
|
|
1619
|
-
pose(dt) {
|
|
1620
|
-
const cfg = this.cfg, P = this.P, s = cfg.s;
|
|
1621
|
-
const air = this.mode === 'air' || this.mode === 'landing' || (this.mode === 'takeoff' && this.to.air);
|
|
1622
|
-
const hop = this.mode === 'takeoff' && this.to.hopOnly;
|
|
1623
|
-
// landing flare, eased out after touchdown (wings stay up briefly, then fold)
|
|
1624
|
-
this.flareS = this.flareS || new Spring(0);
|
|
1625
|
-
this.flare = clamp(this.flareS.step(this.ld ? this.ld.flare || 0 : 0, this.ld ? 30 : 7, dt), 0, 1);
|
|
1626
|
-
const fly = air && !hop;
|
|
1627
|
-
// flight.shape weight: 0 on the ground (and in the flare, a fall), 1 in flight; critically damped
|
|
1628
|
-
if (cfg.shape) {
|
|
1629
|
-
this.shapeS = this.shapeS || new Spring(0);
|
|
1630
|
-
this.shapeW = clamp(this.shapeS.step(fly ? 1 - this.flare : 0, 8, dt), 0, 1);
|
|
1631
|
-
} else this.shapeW = 0;
|
|
1632
|
-
const h = this.heading;
|
|
1633
|
-
this.forward(h, this.F); this.left(h, this.Lf);
|
|
1634
|
-
this.qH.setFromAxisAngle(UP, h);
|
|
1635
|
-
const g = this.gait;
|
|
1636
|
-
const vn = this.speed / cfg.sq;
|
|
1637
|
-
const t = this.time;
|
|
1638
|
-
// breathing (fast in birds)
|
|
1639
|
-
this.breath += dt * TAU * lerp(1.1, 2.5, clamp(this.fl.power * (air ? 1 : 0), 0, 1)) * P.breathRate;
|
|
1640
|
-
// ---------------- body height, pitch, bank
|
|
1641
|
-
let pitchT = 0, bankT = 0, yT;
|
|
1642
|
-
const gw = P.gaitW;
|
|
1643
|
-
if (!air) {
|
|
1644
|
-
// standing height: pivot above the ground (or the perch), posture drops, gait bob / hop arc
|
|
1645
|
-
// (turning on the spot steps without the gait's bob: the pivot's yaw comes in pulses)
|
|
1646
|
-
const yawMv = this._standTurn || this.pivot ? 0 : Math.abs(this.yawRate);
|
|
1647
|
-
const moving = this.mode === 'ground' && (vn > 0.04 || yawMv > 0.3);
|
|
1648
|
-
const mv = moving ? smooth(0.02, 0.3, vn + yawMv * 0.1) : 0;
|
|
1649
|
-
let bob = 0, hopBob = 0;
|
|
1650
|
-
if (mv > 0) {
|
|
1651
|
-
// walk: highest at mid-stance (vaulting); hop: ballistic arc between two compressions
|
|
1652
|
-
let lw = 0;
|
|
1653
|
-
for (const leg of this.legs) {
|
|
1654
|
-
const pl = wrap01(this.phase - g.off[leg.def.idx]);
|
|
1655
|
-
if (pl < g.D) lw = Math.max(lw, Math.sin(Math.PI * pl / g.D));
|
|
1656
|
-
}
|
|
1657
|
-
const walkBob = g.bob * (lw - 0.75);
|
|
1658
|
-
const ph = wrap01(this.phase - g.off[0]);
|
|
1659
|
-
let hop = 0;
|
|
1660
|
-
if (g.hopW > 0.01 && g.hop > 0) {
|
|
1661
|
-
const Ts = g.D / g.f, Tf = (1 - g.D) / g.f;
|
|
1662
|
-
const A = g.hop * 4 * Ts / (Math.PI * Math.max(Tf, 1e-3));
|
|
1663
|
-
if (ph < g.D) hop = -Math.min(A, 0.12 * cfg.legLen) * Math.sin(Math.PI * ph / g.D);
|
|
1664
|
-
else { const u = (ph - g.D) / (1 - g.D); hop = g.hop * 4 * u * (1 - u); }
|
|
1665
|
-
}
|
|
1666
|
-
// (the hop arc is exact - added after the height filter so it does not lag the feet)
|
|
1667
|
-
bob = walkBob * (1 - g.hopW) * mv * gw;
|
|
1668
|
-
hopBob = hop * g.hopW * mv * gw;
|
|
1669
|
-
}
|
|
1670
|
-
const base = this.mode === 'perch' && this.perch ? this.perch.top : this.terrainH(this.body.x, this.body.z);
|
|
1671
|
-
const ground = this.mode === 'perch' ? base : Math.max(this.terrainH(this.legs[0].plant.x, this.legs[0].plant.z), this.terrainH(this.legs[1].plant.x, this.legs[1].plant.z), this.terrainH(this.pos.x, this.pos.z)) * 0.5 + this.terrainH(this.pos.x, this.pos.z) * 0.5;
|
|
1672
|
-
const drop = clamp(P.drop + (this.input.crouch || 0) * 0.25, -0.2, 1) * (cfg.pivotY - this.sitHeight());
|
|
1673
|
-
yT = ground + cfg.pivotY - drop + bob + P.lift * s;
|
|
1674
|
-
// the legs must reach their planted feet
|
|
1675
|
-
if (this.mode !== 'takeoff') {
|
|
1676
|
-
for (const leg of this.legs) {
|
|
1677
|
-
if (leg.state !== 'stance' || P.legGait < 0.3) continue;
|
|
1678
|
-
const L = leg.def;
|
|
1679
|
-
const hipOff = L.hip.y;
|
|
1680
|
-
const dxz = Math.hypot(this.body.x - leg.plant.x, this.body.z - leg.plant.z);
|
|
1681
|
-
const reach = (L.L1 + L.L2 + L.L3) * 0.93;
|
|
1682
|
-
const maxY = leg.plant.y + cfg.feet.pad + Math.sqrt(Math.max(0.05 * reach * reach, reach * reach - dxz * dxz)) - hipOff;
|
|
1683
|
-
yT = lerp(yT, smin(yT, maxY, 0.01 * s), 1 - this.airLeg.x);
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
// take-off crouch & push (continuous: crouch, then the body is carried by the jump)
|
|
1687
|
-
if (this.mode === 'takeoff' && !this.to.air) {
|
|
1688
|
-
const c = smooth(0, this.to.crouch * 0.7, this.to.t);
|
|
1689
|
-
yT -= c * 0.28 * (cfg.pivotY - this.sitHeight());
|
|
1690
|
-
}
|
|
1691
|
-
let om = (this.mode === 'ground' && vn > 0.04 ? 30 : 16) * P.bodyOmega;
|
|
1692
|
-
// touchdown: the legs take the impact progressively (no one-frame velocity step)
|
|
1693
|
-
if (this._touchTime !== undefined) om *= lerp(0.6, 1, smooth(0, 0.15, this.time - this._touchTime));
|
|
1694
|
-
// (on the touchdown frame the air step already moved the body: do not integrate twice)
|
|
1695
|
-
if (this._touchFrame === this.frame) { this.hY.x = this.body.y; } else this.body.y = this.hY.step(yT, om, dt) + this.hopS.step(hopBob, 60, dt);
|
|
1696
|
-
// a hard landing never crouches lower than sitting on the tarsi (unless a posture drops the body)
|
|
1697
|
-
// (faded in with the posture drop, not switched on at drop 0.5: a body still lying low as a
|
|
1698
|
-
// posture fades out - standing up after death - was lifted by up to 2 cm in one frame)
|
|
1699
|
-
const kFloor = 1 - smooth(0.3, 0.5, P.drop);
|
|
1700
|
-
if (kFloor > 0 && this.mode === 'ground') {
|
|
1701
|
-
const fl = ground + this.sitHeight() * 1.03, kf = 0.01 * s;
|
|
1702
|
-
if (this.body.y < fl + kf) {
|
|
1703
|
-
const d = fl + kf - this.body.y;
|
|
1704
|
-
const ny = this.body.y + (d >= 2 * kf ? d - kf : d * d / (4 * kf));
|
|
1705
|
-
this.body.y = lerp(this.body.y, ny, kFloor);
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
// take-off push: the legs extend and accelerate the body to the jump velocity (continuous)
|
|
1709
|
-
if (this.mode === 'takeoff' && this.to.pushing) {
|
|
1710
|
-
const T = this.to, tau = clamp((T.t - T.crouch) / T.push, 0, 1);
|
|
1711
|
-
// sin^2 acceleration profile: velocity 0 -> vy with no acceleration steps
|
|
1712
|
-
this.body.y = T.y0 + T.vy * T.push * (0.5 * tau * tau + (Math.cos(TAU * tau) - 1) / (4 * Math.PI * Math.PI));
|
|
1713
|
-
this.hY.x = this.body.y; this.hY.v = T.vy * (tau - Math.sin(TAU * tau) / TAU);
|
|
1714
|
-
}
|
|
1715
|
-
this.body.x = this.pos.x; this.body.z = this.pos.z;
|
|
1716
|
-
// pitch: gait (+ = nose up), posture, perched; bank into turns, hop sway
|
|
1717
|
-
pitchT = g.pitch * mv * gw + P.pitch;
|
|
1718
|
-
bankT = clamp(-this.speed * this.yawRate / G, -0.3, 0.3) * gw + P.roll;
|
|
1719
|
-
const sway = g.sway * Math.sin(TAU * (this.phase - g.off[0])) * mv * (1 - g.hopW) * gw;
|
|
1720
|
-
this.swayS.step(sway, 20, dt);
|
|
1721
|
-
} else {
|
|
1722
|
-
// flight: nose along the flight path (relative to the standing tilt), angle of attack at low
|
|
1723
|
-
// speed, flare; bank from the turn
|
|
1724
|
-
this.hY.x = this.body.y; this.hY.v = this.vy;
|
|
1725
|
-
const fl = cfg.flight;
|
|
1726
|
-
const gamma = Math.atan2(this.vy, Math.max(this.speed, 0.5));
|
|
1727
|
-
const aoa = 0.25 * clamp(1 - this.speed / fl.cruise, 0, 1) + 0.04;
|
|
1728
|
-
const flare = this.flare;
|
|
1729
|
-
const toPitch = 0.4 * (this.toW || 0);
|
|
1730
|
-
pitchT = -cfg.tilt + gamma * 0.7 + aoa + flare * (cfg.tilt + 0.55) + toPitch + P.pitch * 0.5;
|
|
1731
|
-
// (into the turn: a left turn (+ yaw rate) rolls the body's top to its left, as on the ground)
|
|
1732
|
-
bankT = clamp(-(this.turnBank || 0), -cfg.flight.bankMax, cfg.flight.bankMax) + P.roll * 0.4;
|
|
1733
|
-
// a hop (jump) keeps the standing carriage, leaning slightly into the jump; a bird falling dead
|
|
1734
|
-
// tumbles from the attitude it had (the death pose's pitch and roll are relative to it)
|
|
1735
|
-
if (hop) {
|
|
1736
|
-
if (this.to.dead) { pitchT = (this._fallPitch0 ?? 0) + P.pitch; bankT = (this._fallBank0 ?? 0) + P.roll; }
|
|
1737
|
-
else { pitchT = P.pitch - 0.12 + 0.2 * clamp(this.vy, -1, 1) * 0.3; bankT = P.roll; }
|
|
1738
|
-
}
|
|
1739
|
-
this.swayS.step(0, 10, dt);
|
|
1740
|
-
}
|
|
1741
|
-
// (spring rates ease between air and ground: a rate step is an acceleration step)
|
|
1742
|
-
this._pom = expTo(this._pom ?? 14, air ? 7 : 14 * P.bodyOmega, dt, 0.12);
|
|
1743
|
-
// (in the air the roll is already smooth - the turn bank is a spring - so the body follows it closely)
|
|
1744
|
-
this._bom = expTo(this._bom ?? 12, air ? 16 : 12, dt, 0.12);
|
|
1745
|
-
const pitch = this.pitchS.step(pitchT, this._pom, dt);
|
|
1746
|
-
const bank = this.bankS.step(bankT, this._bom, dt);
|
|
1747
|
-
// stroke-driven body bob in flight
|
|
1748
|
-
let flapBob = 0;
|
|
1749
|
-
flapBob = -Math.sin(TAU * this.fl.psi) * this.fl.amp * 0.004 * s * (1 - this.fl.glide);
|
|
1750
|
-
// (the bank rolls the body about its own long axis - the standing carriage's axis, level in flight
|
|
1751
|
-
// and lying: about the bind frame's forward axis, 30-46 deg off an upright bird's body axis, it skewed
|
|
1752
|
-
// the body off its path in banked turns - 26 deg for an eagle at 45 deg of bank - and a bird lying
|
|
1753
|
-
// dead on its side lay head-down with the tail up; about the horizontal, a falling body pitched nose
|
|
1754
|
-
// down cartwheeled onto a wing instead of tumbling about its axis)
|
|
1755
|
-
// (P.rollAxisK: a limp body falling from the sky tumbles about the bind frame's forward axis, as it
|
|
1756
|
-
// did before, eased back to the body axis after the impact)
|
|
1757
|
-
const RA = this._rollAx || (this._rollAx = new THREE.Vector3(0, Math.sin(cfg.tilt), Math.cos(cfg.tilt)));
|
|
1758
|
-
const ra = P.rollAxisK > 0 ? tv().copy(RA).lerp(Z1, P.rollAxisK).normalize() : RA;
|
|
1759
|
-
this.qB.copy(this.qH).multiply(tqa(X1, -pitch)).multiply(tqa(ra, bank));
|
|
1760
|
-
const origin = tv().copy(this.body).addScaledVector(this.Lf, this.swayS.x + P.side * s).addScaledVector(this.F, P.fwd * s);
|
|
1761
|
-
origin.y += flapBob;
|
|
1762
|
-
// landing momentum: the body carries on over the planted feet and settles back (critically damped)
|
|
1763
|
-
const lo = this.landOff;
|
|
1764
|
-
if (lo && (lo.x * lo.x + lo.z * lo.z + lo.vx * lo.vx + lo.vz * lo.vz) > 1e-10) {
|
|
1765
|
-
if (air) { lo.x = lo.z = lo.vx = lo.vz = 0; } else if (this._touchFrame !== this.frame) {
|
|
1766
|
-
const w = 13, e = Math.exp(-w * dt);
|
|
1767
|
-
for (const k of ['x', 'z']) {
|
|
1768
|
-
const vk = 'v' + k, x = lo[k], v = lo[vk], c = v + w * x;
|
|
1769
|
-
lo[k] = (x + c * dt) * e; lo[vk] = (v - w * c * dt) * e;
|
|
1770
|
-
}
|
|
1771
|
-
origin.x += lo.x; origin.z += lo.z;
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
this._origin = origin;
|
|
1775
|
-
// ---------------- torso
|
|
1776
|
-
const bodyO = this._bodyO || (this._bodyO = new THREE.Vector3());
|
|
1777
|
-
bodyO.copy(origin);
|
|
1778
|
-
const bs = 1 + Math.sin(this.breath) * 0.012 * P.breathAmp;
|
|
1779
|
-
const fluff = 1 + P.fluff * 0.08;
|
|
1780
|
-
const qBody = this.qB;
|
|
1781
|
-
// (a persistent vector, not a ring temporary: the leg, wing, tail and head solves below can use more
|
|
1782
|
-
// temporaries than the ring holds in a busy frame - feathers lifted off the ground while a bird
|
|
1783
|
-
// stands up after dying in flight - and the restored body position came back as garbage: a one-frame
|
|
1784
|
-
// jump of everything carried by it, the head target first)
|
|
1785
|
-
const saveBody = (this._saveBody || (this._saveBody = new THREE.Vector3())).copy(this.body);
|
|
1786
|
-
this.body.copy(origin);
|
|
1787
|
-
const syn = this.B(tv().copy(this.J.synsacrum).sub(cfg.pivot), tv());
|
|
1788
|
-
const qChest = tqc(qBody).multiply(tqa(X1, -P.chestPitch));
|
|
1789
|
-
// (the torso scales: kept so a lift off the ground re-writes the bones with the same ones)
|
|
1790
|
-
const sP = (this._sclP || (this._sclP = new THREE.Vector3())).set(fluff * bs, fluff, fluff);
|
|
1791
|
-
const sC = (this._sclC || (this._sclC = new THREE.Vector3())).set(fluff * bs, fluff * (1 + (bs - 1) * 0.5), fluff);
|
|
1792
|
-
// (flight.shape.body: sleeked plumage in flight, eased with the flight weight; scaled about a centre
|
|
1793
|
-
// `back` dorsal of the synsacrum: the bone heads move by Q (I - K) c, c the centre in the bone's frame)
|
|
1794
|
-
const shP = this._shP || (this._shP = new THREE.Vector3()), shC = this._shC || (this._shC = new THREE.Vector3());
|
|
1795
|
-
shP.set(0, 0, 0); shC.set(0, 0, 0);
|
|
1796
|
-
if (this.shapeW > 0 && cfg.shape.body) {
|
|
1797
|
-
const w = this.shapeW, sb = cfg.shape.body;
|
|
1798
|
-
const kx = lerp(1, sb[0], w), ky = lerp(1, sb[1], w), kz = lerp(1, sb[2], w);
|
|
1799
|
-
sP.x *= kx; sP.y *= ky; sP.z *= kz; sC.x *= kx; sC.y *= ky; sC.z *= kz;
|
|
1800
|
-
if (cfg.shape.back) {
|
|
1801
|
-
const iP = this.ix.pelvis, iC = this.ix.chest;
|
|
1802
|
-
for (const [sh, i, q] of [[shP, iP, qBody], [shC, iC, qChest]]) {
|
|
1803
|
-
const c = tv().copy(cfg.shape.vent).multiplyScalar(-cfg.shape.back * s).applyQuaternion(tqc(this.bindQ[i]).invert());
|
|
1804
|
-
sh.set((1 - kx) * c.x, (1 - ky) * c.y, (1 - kz) * c.z).applyQuaternion(tqc(q).multiply(this.bindQ[i]));
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
this.setBoneQ('pelvis', tv().addVectors(syn, shP), tqc(qBody), sP);
|
|
1809
|
-
this.setBoneQ('chest', tv().addVectors(syn, shC), qChest, sC);
|
|
1810
|
-
this.qChest = qChest;
|
|
1811
|
-
// ---------------- ground under the body: keep the belly and wings off the terrain
|
|
1812
|
-
// (and while a dead bird falls onto it: the belly meets the ground continuously)
|
|
1813
|
-
const deadFall = !!(this.to && this.to.dead);
|
|
1814
|
-
this._resting = false;
|
|
1815
|
-
if (!air || deadFall) this.restOnGround(dt);
|
|
1816
|
-
// ---------------- legs, wings, tail, neck & head
|
|
1817
|
-
this.poseLegs(dt, air);
|
|
1818
|
-
this.poseWings(dt, fly);
|
|
1819
|
-
this.poseTail(dt, fly);
|
|
1820
|
-
this.poseHead(dt, fly);
|
|
1821
|
-
// (the body frame the bones were written in: the ground support carries last frame's thighs with it)
|
|
1822
|
-
if (this.sup) { this._supPrev.compose(this.body, this.qB, ONE); this._supPrevOk = true; }
|
|
1823
|
-
this.body.copy(saveBody);
|
|
1824
|
-
// a dead bird whose belly has reached the ground is held there: the fall ends (see stepHop)
|
|
1825
|
-
if (deadFall && this._restPen > 0) { this.body.y += this._restPen; this.bodyV.y = Math.max(this.bodyV.y, 0); this.to.touch = true; }
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
sitHeight() {
|
|
1829
|
-
// pivot height when sitting on the tarsi: the lowest point of the torso skin (measured on the
|
|
1830
|
-
// mesh; the species' belly section when there is none) just above the ground, or the folded legs
|
|
1831
|
-
if (this._sitH !== undefined) return this._sitH;
|
|
1832
|
-
const cfg = this.cfg, L = cfg.legs[0];
|
|
1833
|
-
const belly = this.sup ? cfg.pivotY - this.sup.minY + 0.002 * cfg.s : cfg.belly.hh * 1.05 + (cfg.pivotY - cfg.belly.c.y) + 0.003 * cfg.s;
|
|
1834
|
-
this._sitH = Math.max(belly, 0.012 * cfg.s + 0.55 * (L.L1 + L.L2) * 0.8 + 0.003 * cfg.s);
|
|
1835
|
-
return this._sitH;
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
// lift the body (this frame) where its skin would enter the terrain: the torso (pelvis and chest,
|
|
1839
|
-
// with the fluff / breathing scale about the synsacrum) and the thighs (femur skin, posed as in the
|
|
1840
|
-
// last frame relative to the body: the femur stays near its rest angle, so it moves with the body)
|
|
1841
|
-
restOnGround() {
|
|
1842
|
-
const cfg = this.cfg, P = this.P;
|
|
1843
|
-
// (also just after a touchdown, when the landing crouch may press the belly down)
|
|
1844
|
-
// (skipped while nothing presses the body down, but only once the belly is clear: switching the
|
|
1845
|
-
// lift off while it still held the body up dropped it in one frame as a posture faded out)
|
|
1846
|
-
if (P.groundW < 0.01 && P.drop < 0.3 && !(this.time - (this._touchTime ?? -9) < 0.8) && !(this._restPen > 0)) { this._resting = false; return; }
|
|
1847
|
-
this._resting = true;
|
|
1848
|
-
const q = this.qB;
|
|
1849
|
-
let pen = 0;
|
|
1850
|
-
const sup = this.sup;
|
|
1851
|
-
if (sup) {
|
|
1852
|
-
const s = cfg.s, b = this.body;
|
|
1853
|
-
// candidates against the terrain's tangent plane under the body, the deepest few checked exactly
|
|
1854
|
-
const h0 = this.terrainH(b.x, b.z), n = this.terrainN(b.x, b.z, tv());
|
|
1855
|
-
const gx = n.x / n.y, gz = n.z / n.y;
|
|
1856
|
-
const K = 4, cand = this._supCand || (this._supCand = new Float64Array(K * 4));
|
|
1857
|
-
let nc = 0;
|
|
1858
|
-
const consider = (x, y, z) => {
|
|
1859
|
-
const pp = h0 - gx * (x - b.x) - gz * (z - b.z) - y;
|
|
1860
|
-
if (nc < K) { cand[nc * 4] = pp; cand[nc * 4 + 1] = x; cand[nc * 4 + 2] = y; cand[nc * 4 + 3] = z; nc++; return; }
|
|
1861
|
-
let mi = 0;
|
|
1862
|
-
for (let k = 1; k < K; k++) if (cand[k * 4] < cand[mi * 4]) mi = k;
|
|
1863
|
-
if (pp > cand[mi * 4]) { cand[mi * 4] = pp; cand[mi * 4 + 1] = x; cand[mi * 4 + 2] = y; cand[mi * 4 + 3] = z; }
|
|
1864
|
-
};
|
|
1865
|
-
// skinning data of the bones the support uses: the torso as written this frame, the others (thighs
|
|
1866
|
-
// and their blends) as posed last frame, carried by the body's motion since then
|
|
1867
|
-
const delta = this._supPrevOk ? _m4b.compose(b, q, ONE).multiply(_m4.copy(this._supPrev).invert()) : null;
|
|
1868
|
-
for (const i of sup.bones) {
|
|
1869
|
-
const M = this.sk.bones[i].matrixWorld;
|
|
1870
|
-
this._supBone(i, delta && i !== this.ix.pelvis && i !== this.ix.chest ? _m4c.multiplyMatrices(delta, M) : M);
|
|
1871
|
-
}
|
|
1872
|
-
const o3 = this._supOut || (this._supOut = new Float64Array(3));
|
|
1873
|
-
for (let k = 0; k < sup.n; k++) { this._supSkin(k, o3); consider(o3[0], o3[1], o3[2]); }
|
|
1874
|
-
const clr = 0.0008 * s;
|
|
1875
|
-
for (let k = 0; k < nc; k++) pen = Math.max(pen, this.terrainH(cand[k * 4 + 1], cand[k * 4 + 3]) + clr - cand[k * 4 + 2]);
|
|
1876
|
-
} else {
|
|
1877
|
-
for (let k = -1; k <= 1; k++) {
|
|
1878
|
-
const c = tv(0, cfg.belly.c.y - cfg.pivotY, cfg.belly.c.z - cfg.pivot.z + k * cfg.belly.hh * 0.9).applyQuaternion(q).add(this.body);
|
|
1879
|
-
const dl = tv(0, -1, 0).applyQuaternion(tqc(q).invert());
|
|
1880
|
-
const su = Math.sqrt((cfg.belly.hh * dl.y) ** 2 + (cfg.belly.hw * dl.x) ** 2 + (cfg.belly.hh * 1.3 * dl.z) ** 2);
|
|
1881
|
-
const gy = this.terrainH(c.x, c.z);
|
|
1882
|
-
pen = Math.max(pen, gy + 0.002 * cfg.s - (c.y - su * (k === 0 ? 1 : 0.8)));
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
this._restPen = pen;
|
|
1886
|
-
if (pen > 0) {
|
|
1887
|
-
this.body.y += pen;
|
|
1888
|
-
this._origin.y += pen;
|
|
1889
|
-
// re-write the torso bones at the lifted height (same rotations and scales)
|
|
1890
|
-
const syn = this.B(tv().copy(this.J.synsacrum).sub(cfg.pivot), tv());
|
|
1891
|
-
this.setBoneQ('pelvis', tv().addVectors(syn, this._shP), tqc(q), this._sclP);
|
|
1892
|
-
this.setBoneQ('chest', tv().addVectors(syn, this._shC), this.qChest, this._sclC);
|
|
1893
|
-
// the height spring carries the support's lift (and stops sinking); only the lift: the body height
|
|
1894
|
-
// also holds the sitting floor's soft lift, re-applied every frame - copying the whole height into
|
|
1895
|
-
// the spring doubled it (a hen standing up after a fall jumped 5 mm in one frame)
|
|
1896
|
-
this.hY.x += pen;
|
|
1897
|
-
if (this.hY.v < 0) this.hY.v = 0;
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
// dual quaternion + bind-space scale of bone i posed at M (as tools/lib/skin.mjs / core/render/dqs.js)
|
|
1902
|
-
_supBone(i, M) {
|
|
1903
|
-
const d = this._supD, o = i * 20, B = this._supBind[i];
|
|
1904
|
-
M.decompose(_sp, _sq, _ss);
|
|
1905
|
-
_sq.multiply(B.qi);
|
|
1906
|
-
_st.copy(B.t).applyQuaternion(_sq).negate().add(_sp);
|
|
1907
|
-
const qx = _sq.x, qy = _sq.y, qz = _sq.z, qw = _sq.w, tx = _st.x, ty = _st.y, tz = _st.z;
|
|
1908
|
-
d[o] = qx; d[o + 1] = qy; d[o + 2] = qz; d[o + 3] = qw;
|
|
1909
|
-
d[o + 4] = 0.5 * (qw * tx + ty * qz - tz * qy);
|
|
1910
|
-
d[o + 5] = 0.5 * (qw * ty + tz * qx - tx * qz);
|
|
1911
|
-
d[o + 6] = 0.5 * (qw * tz + tx * qy - ty * qx);
|
|
1912
|
-
d[o + 7] = -0.5 * (tx * qx + ty * qy + tz * qz);
|
|
1913
|
-
if (Math.abs(_ss.x - 1) + Math.abs(_ss.y - 1) + Math.abs(_ss.z - 1) < 1e-5) {
|
|
1914
|
-
d[o + 8] = 1; d[o + 9] = 0; d[o + 10] = 0; d[o + 11] = 0; d[o + 12] = 0; d[o + 13] = 1; d[o + 14] = 0; d[o + 15] = 0; d[o + 16] = 0; d[o + 17] = 0; d[o + 18] = 1; d[o + 19] = 0;
|
|
1915
|
-
} else {
|
|
1916
|
-
const R = B.R.elements, sx = _ss.x, sy = _ss.y, sz = _ss.z;
|
|
1917
|
-
// A = R S R^T (column-major R), then the offset that keeps the bind head fixed
|
|
1918
|
-
for (let r = 0; r < 3; r++) {
|
|
1919
|
-
const a0 = R[r] * sx * R[0] + R[r + 3] * sy * R[3] + R[r + 6] * sz * R[6];
|
|
1920
|
-
const a1 = R[r] * sx * R[1] + R[r + 3] * sy * R[4] + R[r + 6] * sz * R[7];
|
|
1921
|
-
const a2 = R[r] * sx * R[2] + R[r + 3] * sy * R[5] + R[r + 6] * sz * R[8];
|
|
1922
|
-
const bt = B.t;
|
|
1923
|
-
d[o + 8 + r * 4] = a0; d[o + 9 + r * 4] = a1; d[o + 10 + r * 4] = a2;
|
|
1924
|
-
d[o + 11 + r * 4] = (r === 0 ? bt.x : r === 1 ? bt.y : bt.z) - (a0 * bt.x + a1 * bt.y + a2 * bt.z);
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
// the same for a bone posed with world rotation q (bone frame incl. its bind rotation) and head p, unscaled
|
|
1929
|
-
_supBoneQ(i, q, p) {
|
|
1930
|
-
const d = this._supD, o = i * 20, B = this._supBind[i];
|
|
1931
|
-
_sq.copy(q).multiply(B.qi);
|
|
1932
|
-
_st.copy(B.t).applyQuaternion(_sq).negate().add(p);
|
|
1933
|
-
const qx = _sq.x, qy = _sq.y, qz = _sq.z, qw = _sq.w, tx = _st.x, ty = _st.y, tz = _st.z;
|
|
1934
|
-
d[o] = qx; d[o + 1] = qy; d[o + 2] = qz; d[o + 3] = qw;
|
|
1935
|
-
d[o + 4] = 0.5 * (qw * tx + ty * qz - tz * qy);
|
|
1936
|
-
d[o + 5] = 0.5 * (qw * ty + tz * qx - tx * qz);
|
|
1937
|
-
d[o + 6] = 0.5 * (qw * tz + tx * qy - ty * qx);
|
|
1938
|
-
d[o + 7] = -0.5 * (tx * qx + ty * qy + tz * qz);
|
|
1939
|
-
d[o + 8] = 1; d[o + 9] = 0; d[o + 10] = 0; d[o + 11] = 0; d[o + 12] = 0; d[o + 13] = 1; d[o + 14] = 0; d[o + 15] = 0; d[o + 16] = 0; d[o + 17] = 0; d[o + 18] = 1; d[o + 19] = 0;
|
|
1940
|
-
}
|
|
1941
|
-
// skinned position of support vertex k (bones set by _supBone)
|
|
1942
|
-
_supSkin(k, out, sup = this.sup) {
|
|
1943
|
-
const D = this._supD, P = sup.P, I = sup.I, W = sup.W;
|
|
1944
|
-
const px = P[k * 3], py = P[k * 3 + 1], pz = P[k * 3 + 2];
|
|
1945
|
-
let sx = 0, sy = 0, sz = 0, r0 = 0, r1 = 0, r2 = 0, r3 = 0, d0 = 0, d1 = 0, d2 = 0, d3 = 0, ws = 0;
|
|
1946
|
-
const b0 = I[k * 4] * 20;
|
|
1947
|
-
const a0 = D[b0], a1 = D[b0 + 1], a2 = D[b0 + 2], a3 = D[b0 + 3];
|
|
1948
|
-
for (let j = 0; j < 4; j++) {
|
|
1949
|
-
const w = W[k * 4 + j];
|
|
1950
|
-
if (!(w > 0)) continue;
|
|
1951
|
-
const o = I[k * 4 + j] * 20;
|
|
1952
|
-
ws += w;
|
|
1953
|
-
sx += w * (D[o + 8] * px + D[o + 9] * py + D[o + 10] * pz + D[o + 11]);
|
|
1954
|
-
sy += w * (D[o + 12] * px + D[o + 13] * py + D[o + 14] * pz + D[o + 15]);
|
|
1955
|
-
sz += w * (D[o + 16] * px + D[o + 17] * py + D[o + 18] * pz + D[o + 19]);
|
|
1956
|
-
const sg = D[o] * a0 + D[o + 1] * a1 + D[o + 2] * a2 + D[o + 3] * a3 < 0 ? -w : w;
|
|
1957
|
-
r0 += sg * D[o]; r1 += sg * D[o + 1]; r2 += sg * D[o + 2]; r3 += sg * D[o + 3];
|
|
1958
|
-
d0 += sg * D[o + 4]; d1 += sg * D[o + 5]; d2 += sg * D[o + 6]; d3 += sg * D[o + 7];
|
|
1959
|
-
}
|
|
1960
|
-
ws = Math.max(ws, 1e-5);
|
|
1961
|
-
sx /= ws; sy /= ws; sz /= ws;
|
|
1962
|
-
const L = Math.max(Math.hypot(r0, r1, r2, r3), 1e-6);
|
|
1963
|
-
const qx = r0 / L, qy = r1 / L, qz = r2 / L, qw = r3 / L, dx = d0 / L, dy = d1 / L, dz = d2 / L, dw = d3 / L;
|
|
1964
|
-
const tx = 2 * (qw * dx - dw * qx + (qy * dz - qz * dy));
|
|
1965
|
-
const ty = 2 * (qw * dy - dw * qy + (qz * dx - qx * dz));
|
|
1966
|
-
const tz = 2 * (qw * dz - dw * qz + (qx * dy - qy * dx));
|
|
1967
|
-
const cx = qy * sz - qz * sy + qw * sx, cy = qz * sx - qx * sz + qw * sy, cz = qx * sy - qy * sx + qw * sz;
|
|
1968
|
-
out[0] = sx + 2 * (qy * cz - qz * cy) + tx;
|
|
1969
|
-
out[1] = sy + 2 * (qz * cx - qx * cz) + ty;
|
|
1970
|
-
out[2] = sz + 2 * (qx * cy - qy * cx) + tz;
|
|
1971
|
-
return out;
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
// ----------------------------------------------------------------- legs
|
|
1975
|
-
poseLegs(dt, air) {
|
|
1976
|
-
const cfg = this.cfg, P = this.P, s = cfg.s;
|
|
1977
|
-
// air weight of the legs: tucked in flight, thrown forward for landing
|
|
1978
|
-
let airT = air ? 1 : 0;
|
|
1979
|
-
if (this.mode === 'takeoff' && this.to.air) airT = smooth(0.02, 0.28, this.to.tAir);
|
|
1980
|
-
// (legFree: the legs let go of the ground and hang in the body frame - dead, talon strike)
|
|
1981
|
-
const aw0 = Math.max(this.airLeg.step(airT, air ? 12 : 30, dt), P.legFree);
|
|
1982
|
-
// landing reach: held after touchdown while the legs blend back to the ground
|
|
1983
|
-
if (this.ld) this._reach = this.ld.reach || 0;
|
|
1984
|
-
else if (this.to && this.to.hopOnly) this._reach = this.to.reach || 0;
|
|
1985
|
-
else if (air) this._reach = 0;
|
|
1986
|
-
else if (this.airLeg.x < 0.01) this._reach = 0;
|
|
1987
|
-
const reach = Math.max(this._reach || 0, P.legReach * P.legFree);
|
|
1988
|
-
const bodyF = this.Bv(tv(0, 0, 1), tv()), bodyX = this.Bv(tv(1, 0, 0), tv()), bodyUp = this.Bv(tv(0, 1, 0), tv());
|
|
1989
|
-
for (let i = 0; i < 2; i++) {
|
|
1990
|
-
const leg = this.legs[i], L = leg.def;
|
|
1991
|
-
const hip = this.B(L.hip, leg.hip);
|
|
1992
|
-
// ---- target MTP (world) and foot frame
|
|
1993
|
-
const M = tv();
|
|
1994
|
-
const footUp = tv(0, 1, 0);
|
|
1995
|
-
let footYaw = leg.yaw;
|
|
1996
|
-
let groundY = null;
|
|
1997
|
-
// (legUp: this leg alone drawn up into the belly feathers, the bird standing on the other - a
|
|
1998
|
-
// raptor asleep; it leaves the ground like a free leg)
|
|
1999
|
-
const up = P.legUp[i], aw = Math.max(aw0, up);
|
|
2000
|
-
// (the ground target is dropped once the air weight is full: the blend reaches 1 exactly there,
|
|
2001
|
-
// so a far-behind planted foot cannot pop the leg when it is dropped)
|
|
2002
|
-
const awM = Math.min(1, aw / 0.995);
|
|
2003
|
-
if (awM < 1) {
|
|
2004
|
-
const c = this.groundFoot(leg, tv());
|
|
2005
|
-
leg.contact.copy(c);
|
|
2006
|
-
// posture: feet move toward sit / lie targets (tarsus under the body)
|
|
2007
|
-
M.copy(c);
|
|
2008
|
-
groundY = c.y;
|
|
2009
|
-
if (leg.state === 'stance') footUp.copy(leg.plantN);
|
|
2010
|
-
else footUp.copy(UP);
|
|
2011
|
-
// (to the vertical of the air pose as the leg leaves the ground: the terrain normal switched in
|
|
2012
|
-
// when the air weight fell below full, a dead chick's toes jumped 3 mm on the slope)
|
|
2013
|
-
if (aw > 0.5) footUp.lerp(UP, smooth(0.5, 0.995, aw)).normalize();
|
|
2014
|
-
// (toward the yaw the foot is planted with at touchdown: no twist step in turns)
|
|
2015
|
-
if (leg.state === 'swing') footYaw = lerp(leg.liftYaw ?? this.heading, this.heading + this.yawRate * (this._yawAhead ?? 0.05), smooth(0, 1, leg.w));
|
|
2016
|
-
M.addScaledVector(footUp, cfg.feet.pad);
|
|
2017
|
-
// sitting on the tarsi: the (thick) intertarsal pad rests on the ground, the MTP a little higher
|
|
2018
|
-
if (P.sit > 0.001) M.y += P.sit * (cfg.feet.sitPad ?? 0.004) * s;
|
|
2019
|
-
// scratching (fowl feeding): the foot reaches forward, rakes back along the ground and returns
|
|
2020
|
-
// to its print; C1 in the rake phase u (sin^2 lifts, smoothstep strokes)
|
|
2021
|
-
const sw = P.scratchW[i];
|
|
2022
|
-
if (sw > 1e-4 && leg.state === 'stance') {
|
|
2023
|
-
const u = P.scratchU[i];
|
|
2024
|
-
const a = smooth(0, 0.22, u), b = smooth(0.22, 0.72, u), c = smooth(0.72, 1, u);
|
|
2025
|
-
const f = (0.28 * a - 0.7 * b + 0.42 * c) * cfg.legLen * sw;
|
|
2026
|
-
const lift = (u < 0.22 ? Math.sin(Math.PI * u / 0.22) ** 2 * 0.13 : u > 0.72 ? Math.sin(Math.PI * (u - 0.72) / 0.28) ** 2 * 0.1 : 0) * cfg.legLen * sw;
|
|
2027
|
-
M.addScaledVector(this.F, f); M.y += lift;
|
|
2028
|
-
leg.scratchLift = lift / (0.13 * cfg.legLen);
|
|
2029
|
-
} else leg.scratchLift = 0;
|
|
2030
|
-
}
|
|
2031
|
-
if (aw > 0.001) {
|
|
2032
|
-
// air target in the body frame: folded back along the body axis, the feet under the tail
|
|
2033
|
-
// coverts (flight.legTuck { back, down, side }, fractions of the leg length; every bird tucks by
|
|
2034
|
-
// default - the old default, half a leg straight down in the standing frame, left an eagle's feet
|
|
2035
|
-
// hanging under its belly in cruise), or thrown forward to land
|
|
2036
|
-
const lt = cfg.flight.legTuck || DEFAULT_TUCK;
|
|
2037
|
-
const ct = Math.cos(cfg.tilt), st = Math.sin(cfg.tilt);
|
|
2038
|
-
const tuckLocal = tv(L.cx * L.side * (lt.side ?? 0.7), L.hip.y - L.len * (lt.back * st + (lt.down ?? 0) * ct), L.hip.z - L.len * (lt.back * ct - (lt.down ?? 0) * st));
|
|
2039
|
-
const reachLocal = tv(L.cx * L.side * 1.1, L.hip.y - L.len * 0.86, L.hip.z + L.len * 0.18);
|
|
2040
|
-
tuckLocal.lerp(reachLocal, reach);
|
|
2041
|
-
// drawn up standing: the foot folded under the belly, a little in from the hip
|
|
2042
|
-
if (up > 0) tuckLocal.lerp(tv(L.cx * L.side * 0.55, L.hip.y - L.len * 0.4, L.hip.z + L.len * 0.04), up);
|
|
2043
|
-
const A = this.B(tuckLocal, tv());
|
|
2044
|
-
// take-off: the feet leave from their planted spots and move to the air pose quickly
|
|
2045
|
-
if (leg.frozenOn) A.lerp(leg.frozen, 1 - smooth(0, 0.16, this.to && this.to.air ? this.to.tAir : 1));
|
|
2046
|
-
// landing: the feet arrive on their touch points exactly as the body does
|
|
2047
|
-
if (this.ld && this.ld.phase === 'final' && this.ld.feetW > 0) {
|
|
2048
|
-
const lp = this.landPlant(leg, tv());
|
|
2049
|
-
lp.y += cfg.feet.pad;
|
|
2050
|
-
A.lerp(lp, this.ld.feetW);
|
|
2051
|
-
}
|
|
2052
|
-
M.lerp(A, awM);
|
|
2053
|
-
// (the foot turns to the heading as the leg leaves the ground, continuously: a switch at half air
|
|
2054
|
-
// weight snapped every toe by the turn made since the foot was planted - a chick taking off in a
|
|
2055
|
-
// turn, 95 rad/s*)
|
|
2056
|
-
footYaw += angDiff(footYaw, this.heading) * smooth(0.25, 0.75, aw);
|
|
2057
|
-
}
|
|
2058
|
-
// perch: feet on the branch top
|
|
2059
|
-
// keep the MTP above the terrain
|
|
2060
|
-
const gH = this.terrainH(M.x, M.z) + lerp(cfg.feet.pad * 0.8, cfg.feet.sole ?? cfg.feet.pad * 0.8, aw);
|
|
2061
|
-
if (M.y < gH) M.y = gH;
|
|
2062
|
-
leg.mtp.copy(M);
|
|
2063
|
-
// ---- toe roll (heel lift) late in stance and toe curl in swing / flight
|
|
2064
|
-
// toe targets on the ground (roll at toe-off, curl in swing) and in the air (curled when tucked,
|
|
2065
|
-
// open when reaching), blended continuously by the air weight
|
|
2066
|
-
let rollT = 0, curlT = 0, spreadT = 1;
|
|
2067
|
-
if (leg.state === 'stance') rollT = leg.mode === 'gait' ? 30 * DEG * smooth(0.7, 1, leg.u) * P.legGait : 0;
|
|
2068
|
-
else {
|
|
2069
|
-
curlT = cfg.feet.curl * DEG * Math.sin(Math.PI * clamp(leg.w * 1.5, 0, 1));
|
|
2070
|
-
spreadT = 1 - 0.5 * Math.sin(Math.PI * leg.w);
|
|
2071
|
-
}
|
|
2072
|
-
const ka = smooth(0.25, 0.75, aw);
|
|
2073
|
-
leg.ka = ka;
|
|
2074
|
-
if (ka > 0) {
|
|
2075
|
-
rollT *= 1 - ka;
|
|
2076
|
-
curlT = lerp(curlT, lerp(cfg.feet.curl * DEG * 1.1, 0, reach), ka);
|
|
2077
|
-
spreadT = lerp(spreadT, lerp(0.35, 1.25, reach), ka);
|
|
2078
|
-
}
|
|
2079
|
-
if (this.mode === 'perch' && this.perch) { curlT = this.perch.curl; spreadT = 1; rollT = 0; }
|
|
2080
|
-
curlT += P.toeCurl;
|
|
2081
|
-
// scratching: toes close as the foot lifts, spread and rake flat along the ground
|
|
2082
|
-
if (leg.scratchLift > 0) { curlT += cfg.feet.curl * DEG * 0.6 * clamp(leg.scratchLift, 0, 1); rollT = 0; }
|
|
2083
|
-
let roll = leg.roll.step(rollT, 25, dt), curl = leg.curl.step(curlT, aw < 0.5 && leg.state === 'swing' ? 60 : 30, dt);
|
|
2084
|
-
let spread = leg.spread.step(spreadT, 18, dt);
|
|
2085
|
-
// a ground swing ends with the foot flat: the toe roll of the last push-off, the swing curl and the
|
|
2086
|
-
// closed toe spread are eased out exactly by touchdown (the springs lagged: toes still uncurling and the foot still
|
|
2087
|
-
// rolled in the first stance frames moved its ground contact by ~1 % S)
|
|
2088
|
-
if (leg.state === 'swing' && aw < 0.5 && this.mode === 'ground') {
|
|
2089
|
-
// (on the swing's progress at the next frame: a short swing - a chick's walk is 5 frames - ends
|
|
2090
|
-
// before its own w reaches 0.97, and the toes landed curled, uncurling through the stance)
|
|
2091
|
-
const wN = Math.min(1, leg.w + (this._dt || 1 / 60) / Math.max(1e-3, leg.swingDur));
|
|
2092
|
-
const k = 1 - smooth(0.72, 0.97, wN) * (1 - aw * 2);
|
|
2093
|
-
const cBase = P.toeCurl + (this.mode === 'perch' && this.perch ? this.perch.curl : 0);
|
|
2094
|
-
roll *= k; curl = cBase + (curl - cBase) * k; spread = 1 + (spread - 1) * k;
|
|
2095
|
-
leg.roll.x = roll; leg.roll.v *= k; leg.curl.x = curl; leg.curl.v *= k; leg.spread.x = spread; leg.spread.v *= k;
|
|
2096
|
-
}
|
|
2097
|
-
// rolling onto the toes raises the MTP about the toe joints
|
|
2098
|
-
const la = L.toes[2].la;
|
|
2099
|
-
if (roll > 1e-4) M.addScaledVector(footUp, la * Math.sin(roll));
|
|
2100
|
-
// ---- leg IK: femur near rest, 2-bone tibia + tarsus to the MTP (ankle points back)
|
|
2101
|
-
const ankle = this.solveLeg(leg, hip, M, bodyF, bodyX, bodyUp);
|
|
2102
|
-
// sitting: tarsus lies forward along the ground, ankle on the ground behind the foot
|
|
2103
|
-
// (only a foot on the ground folds so: one caught mid-swing by a collapse - death on the move -
|
|
2104
|
-
// lands first; the weight follows the swing's lift profile, so it is continuous at both ends)
|
|
2105
|
-
const onGround = leg.state === 'stance' ? 1 : 1 - Math.pow(Math.sin(Math.PI * Math.pow(clamp(leg.w, 0, 1), 0.9)), 1.2);
|
|
2106
|
-
let sitW = P.sit * (1 - smooth(0, 0.6, aw)) * onGround;
|
|
2107
|
-
if (sitW > 0.001) {
|
|
2108
|
-
const fwd = tv(Math.sin(footYaw), 0, Math.cos(footYaw));
|
|
2109
|
-
const ab = tv().copy(M).addScaledVector(fwd, -L.L3 * 0.94);
|
|
2110
|
-
ab.y = (groundY ?? this.terrainH(ab.x, ab.z)) + 0.0085 * s + L.L3 * 0.12;
|
|
2111
|
-
// (only a foot under the body can be sat on: one left behind by a body still sliding on - death
|
|
2112
|
-
// on the move - puts the folded ankle out of the leg's reach; the fold fades out there instead
|
|
2113
|
-
// of dragging the ankle round the reach limit)
|
|
2114
|
-
sitW *= 1 - smooth(0.92, 1.12, ab.distanceTo(hip) / ((L.L1 + L.L2) * 0.995));
|
|
2115
|
-
ankle.lerp(ab, sitW);
|
|
2116
|
-
const d = tv().subVectors(ankle, hip);
|
|
2117
|
-
const dl = d.length(), mx = (L.L1 + L.L2) * 0.995;
|
|
2118
|
-
if (dl > mx) ankle.copy(hip).addScaledVector(d, mx / dl);
|
|
2119
|
-
}
|
|
2120
|
-
// ankle never in the ground
|
|
2121
|
-
const agy = this.terrainH(ankle.x, ankle.z) + (cfg.feet.hock ?? 0.004) * s;
|
|
2122
|
-
if (ankle.y < agy) ankle.y = agy;
|
|
2123
|
-
// the tarsus keeps its length: an ankle pushed closer to the MTP target than the tarsus is long
|
|
2124
|
-
// (the body collapsing onto a foot: death on the move, a crouch over a swinging foot) would put
|
|
2125
|
-
// the MTP past its target, into the ground. Then the ankle moves out along the tarsus to its
|
|
2126
|
-
// length (above the ground), and the MTP lands on the target
|
|
2127
|
-
{
|
|
2128
|
-
const d = tv().subVectors(ankle, M), dl = d.length();
|
|
2129
|
-
if (dl < L.L3 * 0.999 && dl > 1e-6) {
|
|
2130
|
-
const mt = tv().copy(ankle).addScaledVector(d, -L.L3 / dl);
|
|
2131
|
-
// (weighted by the depth of the MTP, full at the ground: continuous, no threshold)
|
|
2132
|
-
const wFix = smooth(0, 1, (this.terrainH(mt.x, mt.z) + cfg.feet.pad - mt.y) / cfg.feet.pad);
|
|
2133
|
-
if (wFix > 0) {
|
|
2134
|
-
const a0 = tv().copy(ankle);
|
|
2135
|
-
ankle.copy(M).addScaledVector(d, L.L3 / dl);
|
|
2136
|
-
const ag = this.terrainH(ankle.x, ankle.z) + (cfg.feet.hock ?? 0.004) * s;
|
|
2137
|
-
if (ankle.y < ag) {
|
|
2138
|
-
const dy = Math.min(ag - M.y, L.L3 * 0.999), hx = ankle.x - M.x, hz = ankle.z - M.z, hl = Math.hypot(hx, hz) || 1;
|
|
2139
|
-
const hor = Math.sqrt(L.L3 * L.L3 - dy * dy);
|
|
2140
|
-
ankle.set(M.x + (hx / hl) * hor, M.y + dy, M.z + (hz / hl) * hor);
|
|
2141
|
-
}
|
|
2142
|
-
// (within the hip's reach)
|
|
2143
|
-
const dh = tv().subVectors(ankle, hip), dhl = dh.length(), mxh = (L.L1 + L.L2) * 0.995;
|
|
2144
|
-
if (dhl > mxh) ankle.copy(hip).addScaledVector(dh, mxh / dhl);
|
|
2145
|
-
ankle.lerp(a0, 1 - wFix);
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
|
-
// femur + tibia: 2-bone hip -> knee -> ankle, pole = heuristic knee
|
|
2150
|
-
const knee = this.twoBone(hip, ankle, L.L1, L.L2, leg.knee, leg.kneePole, leg);
|
|
2151
|
-
// tarsus: ankle -> MTP (length kept)
|
|
2152
|
-
const tdir = tv().subVectors(M, ankle);
|
|
2153
|
-
const tl = tdir.length();
|
|
2154
|
-
if (tl > 1e-6) tdir.multiplyScalar(1 / tl); else tdir.copy(bodyUp).negate();
|
|
2155
|
-
const mtp = tv().copy(ankle).addScaledVector(tdir, L.L3);
|
|
2156
|
-
(leg.mtpReal || (leg.mtpReal = new THREE.Vector3())).copy(mtp);
|
|
2157
|
-
// planted: in stance and the foot actually on its target (not still arriving / out of reach). This
|
|
2158
|
-
// is the real ground contact (state.legs[].stance). A gait stance counts as soon as the foot is
|
|
2159
|
-
// on its target (requiring the toe curl to be at rest too left most walking and running stances
|
|
2160
|
-
// unreported - a chicken's run had none - and so unmeasured); standing, toes that an action
|
|
2161
|
-
// curls or opens (a strike, a clench) are not a plant until they are still
|
|
2162
|
-
const walking = this.mode === 'ground' && this.speed > 0.05 * cfg.sq;
|
|
2163
|
-
// (a leg that starts to let go of the ground - its air weight rising, dead or striking - is no
|
|
2164
|
-
// longer a plant: its target blends toward the air pose, a leg length away)
|
|
2165
|
-
leg.planted = leg.state === 'stance' && aw < 1e-3 && mtp.distanceToSquared(M) < (0.0025 * s) ** 2 && (walking || Math.abs(leg.curl.v) < 0.25);
|
|
2166
|
-
leg.ankle.copy(ankle);
|
|
2167
|
-
// (flight.shape.legs: the thigh and trouser feathers sleeked in flight - the femur's skin drawn in
|
|
2168
|
-
// toward the hip, the tibiotarsus's thinned about the bone)
|
|
2169
|
-
const kL = this.shapeW > 0 && cfg.shape.legs ? 1 - cfg.shape.legs * this.shapeW : 1;
|
|
2170
|
-
this.setBoneC(L.n.femur, hip, tv().subVectors(knee, hip), bodyX, kL < 1 ? tv(kL, kL, kL) : null);
|
|
2171
|
-
this.setBoneC(L.n.tibia, knee, tv().subVectors(ankle, knee), bodyX, kL < 1 ? tv(kL, 1, kL) : null);
|
|
2172
|
-
this.setBoneC(L.n.tarsus, ankle, tdir, bodyX);
|
|
2173
|
-
// ---- toes in the foot frame
|
|
2174
|
-
const yaw = footYaw;
|
|
2175
|
-
const fz = tv(Math.sin(yaw), 0, Math.cos(yaw));
|
|
2176
|
-
fz.addScaledVector(footUp, -fz.dot(footUp)).normalize();
|
|
2177
|
-
const fx = tv().crossVectors(footUp, fz).normalize(); // left of the foot
|
|
2178
|
-
// crowd LOD: between full solves the toes ride on the tarsus
|
|
2179
|
-
const ti = this.ix.toes[i];
|
|
2180
|
-
if (this.fullFrame) {
|
|
2181
|
-
this.poseToes(leg, mtp, fx, footUp, fz, roll, curl, spread);
|
|
2182
|
-
if (this.lod === 2) this.store(ti.tarsus, ti.toes);
|
|
2183
|
-
} else this.follow(ti.tarsus, ti.toes);
|
|
2184
|
-
leg.contact.copy(M).addScaledVector(footUp, -cfg.feet.pad);
|
|
2185
|
-
}
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
// femur heuristic + 2-bone knee -> ankle -> M (ankle bends backward). Returns the ankle (temp).
|
|
2189
|
-
solveLeg(leg, hip, M, bodyF, bodyX, bodyUp) {
|
|
2190
|
-
const L = leg.def, cfg = this.cfg;
|
|
2191
|
-
// femur: rest direction in the body frame, swung a little with the foot's fore-aft offset
|
|
2192
|
-
const rel = tv().subVectors(M, hip);
|
|
2193
|
-
const fa = rel.dot(bodyF) / cfg.legLen - (L.cz - L.hip.z) / cfg.legLen;
|
|
2194
|
-
const fDir = this.Bv(L.femur0, tv());
|
|
2195
|
-
fDir.applyAxisAngle(bodyX, -Math.tanh(fa * 0.9 / 0.7) * 0.7);
|
|
2196
|
-
const knee0 = tv().copy(hip).addScaledVector(fDir, L.L1);
|
|
2197
|
-
leg.kneePole = leg.kneePole || new THREE.Vector3();
|
|
2198
|
-
leg.kneePole.copy(knee0);
|
|
2199
|
-
// 2-bone knee0 -> ankle -> M
|
|
2200
|
-
const d = tv().subVectors(M, knee0);
|
|
2201
|
-
let dist = d.length();
|
|
2202
|
-
const maxD = (L.L2 + L.L3) * 0.995, soft = (L.L2 + L.L3) * 0.9, minD = Math.abs(L.L2 - L.L3) + 0.01 * cfg.s;
|
|
2203
|
-
if (dist > soft) dist = soft + (maxD - soft) * (1 - Math.exp(-(dist - soft) / (maxD - soft)));
|
|
2204
|
-
dist = Math.max(dist, minD);
|
|
2205
|
-
const e1 = d.normalize();
|
|
2206
|
-
const pole = tv().copy(bodyF).negate().addScaledVector(bodyUp, 0.6); // ankle behind (and up when the foot trails)
|
|
2207
|
-
const e2 = tv().copy(pole).addScaledVector(e1, -pole.dot(e1));
|
|
2208
|
-
if (e2.lengthSq() < 1e-8) e2.copy(bodyUp);
|
|
2209
|
-
e2.normalize();
|
|
2210
|
-
const c = clamp((L.L2 * L.L2 + dist * dist - L.L3 * L.L3) / (2 * L.L2 * dist), -1, 1);
|
|
2211
|
-
return tv().copy(knee0).addScaledVector(e1, c * L.L2).addScaledVector(e2, Math.sqrt(1 - c * c) * L.L2);
|
|
2212
|
-
}
|
|
2213
|
-
|
|
2214
|
-
// 2-bone A -> joint -> B with lengths l1, l2 in the plane of the pole point
|
|
2215
|
-
twoBone(A, B, l1, l2, out, polePt, leg) {
|
|
2216
|
-
const d = tv().subVectors(B, A);
|
|
2217
|
-
let dist = d.length();
|
|
2218
|
-
const mx = (l1 + l2) * 0.999, mn = Math.abs(l1 - l2) + 1e-4;
|
|
2219
|
-
dist = clamp(dist, mn, mx);
|
|
2220
|
-
const e1 = d.normalize();
|
|
2221
|
-
// pole: toward the heuristic joint, biased forward so a straight limb never flips its plane
|
|
2222
|
-
const e2 = tv().subVectors(polePt, A).normalize().addScaledVector(this.Bv(tv(0, 0, 1), tv()), 0.7);
|
|
2223
|
-
e2.addScaledVector(e1, -e2.dot(e1));
|
|
2224
|
-
if (e2.lengthSq() < 1e-10) e2.copy(this.F);
|
|
2225
|
-
e2.normalize();
|
|
2226
|
-
const c = clamp((l1 * l1 + dist * dist - l2 * l2) / (2 * l1 * dist), -1, 1);
|
|
2227
|
-
out.copy(A).addScaledVector(e1, c * l1).addScaledVector(e2, Math.sqrt(1 - c * c) * l1);
|
|
2228
|
-
// B is re-derived by the caller from the solved joint (lengths kept)
|
|
2229
|
-
const dB = tv().subVectors(B, out).normalize();
|
|
2230
|
-
B.copy(out).addScaledVector(dB, l2);
|
|
2231
|
-
return out;
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
poseToes(leg, mtp, fx, fy, fz, roll, curl, spread) {
|
|
2235
|
-
const L = leg.def, S = L.side, P = this.P;
|
|
2236
|
-
const s = this.cfg.s;
|
|
2237
|
-
// clearance of the MTP above the ground: curled toes must not reach into it
|
|
2238
|
-
const gm = this.terrainH(mtp.x, mtp.z);
|
|
2239
|
-
const clear = mtp.y - gm - 0.0015 * s;
|
|
2240
|
-
for (let k = 0; k < 4; k++) {
|
|
2241
|
-
const T = L.toes[k];
|
|
2242
|
-
const la = T.a, lb = T.b;
|
|
2243
|
-
// bind directions in the foot frame, spread about the foot axis
|
|
2244
|
-
const dA = tv().copy(fx).multiplyScalar(la.x * spread * S).addScaledVector(fy, la.y).addScaledVector(fz, la.z).normalize();
|
|
2245
|
-
const dB = tv().copy(fx).multiplyScalar(lb.x * spread * S).addScaledVector(fy, lb.y).addScaledVector(fz, lb.z).normalize();
|
|
2246
|
-
// flexion toward the sole (down): toe a takes part of the curl plus the heel-lift roll (so
|
|
2247
|
-
// its mid joint stays on the ground), toe b the rest of the curl (flat when only rolled)
|
|
2248
|
-
let thA = T.back ? curl * 0.5 - roll * 0.5 : curl * 0.55 + roll;
|
|
2249
|
-
let thB = T.back ? curl * 0.9 : curl * 1.0;
|
|
2250
|
-
// no segment flexes past ~83 deg below the horizontal from its bind slope (soft cap): a death
|
|
2251
|
-
// clench on top of the flight tuck curled the toes 140 deg, past where the drop model below is
|
|
2252
|
-
// monotonic - the room limit then jumped as the feet came down (100 rad/s*); a joint cannot flex
|
|
2253
|
-
// that far anyway
|
|
2254
|
-
thA = smin(thA, T.eA + 1.45, 0.15);
|
|
2255
|
-
thB = smin(thB, T.eB + 1.45 - thA + (T.back ? 0 : roll), 0.15);
|
|
2256
|
-
// limit the curl to the room under the foot (continuous): tip drop = la sin(a) + lb sin(a + b)
|
|
2257
|
-
const base = T.back ? -roll * 0.5 : roll;
|
|
2258
|
-
// (angles capped at 90 deg: the drop is then monotonic in the curl, so the limit is continuous;
|
|
2259
|
-
// measured from each segment's bind slope - a raised MTP pad has its toes sloping down already,
|
|
2260
|
-
// and a clenched toe then dropped further than modelled, into the ground under a dead bird)
|
|
2261
|
-
const drop = (ka) => T.la * Math.sin(clamp(base + (thA - base) * ka - T.eA, 0, 1.5708)) + T.lb * Math.sin(clamp(base + (thA - base) * ka + thB * ka - (T.back ? 0 : roll) - T.eB, 0, 1.5708));
|
|
2262
|
-
// (the limit is rate-limited: it can release abruptly when the foot rises fast)
|
|
2263
|
-
let lo = 1;
|
|
2264
|
-
if (drop(1) > clear) {
|
|
2265
|
-
lo = 0; let hi = 1;
|
|
2266
|
-
for (let it = 0; it < 6; it++) { const mid = 0.5 * (lo + hi); if (drop(mid) > clear) hi = mid; else lo = mid; }
|
|
2267
|
-
}
|
|
2268
|
-
const ls = leg.toeLo || (leg.toeLo = [1, 2, 3, 4].map(() => new Spring(1)));
|
|
2269
|
-
// tightening is immediate (no penetration), releasing follows a spring
|
|
2270
|
-
lo = lo < ls[k].x ? (ls[k].reset(lo), lo) : ls[k].step(lo, 14, this._dt || 1 / 60);
|
|
2271
|
-
if (lo < 0.9999) { thA = base + (thA - base) * lo; thB *= lo; }
|
|
2272
|
-
// flexion axis: across the toe; a toe pointing along the foot's up axis falls back smoothly on the
|
|
2273
|
-
// foot's lateral axis (signed as for a level toe) so the axis never flips
|
|
2274
|
-
const lat = tv().crossVectors(dA, fy);
|
|
2275
|
-
const fl = T.back ? 1 : -1;
|
|
2276
|
-
lat.addScaledVector(fx, fl * Math.max(0, 0.35 - lat.length())).normalize();
|
|
2277
|
-
dA.applyAxisAngle(lat, -thA);
|
|
2278
|
-
const m = tv().copy(mtp).addScaledVector(dA, T.la);
|
|
2279
|
-
const latB = tv().crossVectors(dB, fy);
|
|
2280
|
-
latB.addScaledVector(fx, fl * Math.max(0, 0.35 - latB.length())).normalize();
|
|
2281
|
-
dB.applyAxisAngle(latB, -(thA + thB - (T.back ? 0 : roll)));
|
|
2282
|
-
// conform to uneven ground: raise the toe segments whose ends would enter the terrain
|
|
2283
|
-
const cl = (this.cfg.feet.toeClear ?? 0.0012) * s;
|
|
2284
|
-
const gmid = this.terrainH(m.x, m.z) + cl;
|
|
2285
|
-
if (m.y < gmid) {
|
|
2286
|
-
const up = Math.asin(clamp((gmid - m.y) / T.la, 0, 1));
|
|
2287
|
-
dA.applyAxisAngle(lat, up); m.copy(mtp).addScaledVector(dA, T.la);
|
|
2288
|
-
dB.applyAxisAngle(latB, up);
|
|
2289
|
-
}
|
|
2290
|
-
const tx = m.x + dB.x * T.lb, tz = m.z + dB.z * T.lb, ty = m.y + dB.y * T.lb;
|
|
2291
|
-
const gtip = this.terrainH(tx, tz) + cl;
|
|
2292
|
-
if (ty < gtip) dB.applyAxisAngle(latB, Math.asin(clamp((gtip - ty) / T.lb, 0, 1)));
|
|
2293
|
-
this.setBoneC(L.n.toeA[k], mtp, dA, fx);
|
|
2294
|
-
this.setBoneC(L.n.toeB[k], m, dB, fx);
|
|
2295
|
-
}
|
|
2296
|
-
}
|
|
2297
|
-
|
|
2298
|
-
// ----------------------------------------------------------------- wings & feathers
|
|
2299
|
-
poseWings(dt, air) {
|
|
2300
|
-
const cfg = this.cfg, P = this.P, W = cfg.wing, fl = this.fl;
|
|
2301
|
-
// ---- flap cycle
|
|
2302
|
-
let amp = 0, freqK = 0;
|
|
2303
|
-
const flap = W.flap, pw = W.power;
|
|
2304
|
-
if (air) {
|
|
2305
|
-
const pk = smooth(0.5, 1, fl.power);
|
|
2306
|
-
freqK = lerp(flap.freq, pw.freq, pk);
|
|
2307
|
-
amp = lerp(flap.amp * lerp(0.7, 1, smooth(0.2, 0.5, fl.power)), pw.amp, pk) * (1 - fl.glide);
|
|
2308
|
-
}
|
|
2309
|
-
// ground flapping (jump, attack, hit, balance)
|
|
2310
|
-
// ground flapping (jump, attack, hit, display), eased in and out
|
|
2311
|
-
this.flapS = this.flapS || new Spring(0);
|
|
2312
|
-
const gf = clamp(this.flapS.step(P.flap, 9, dt), 0, 1);
|
|
2313
|
-
if (gf > 0.001) {
|
|
2314
|
-
freqK = lerp(freqK, pw.freq * 1.05, gf);
|
|
2315
|
-
amp = lerp(amp, pw.amp * P.flapAmp, gf);
|
|
2316
|
-
}
|
|
2317
|
-
fl.amp = fl.ampS.step(amp, 10, dt);
|
|
2318
|
-
fl.freq = freqK || fl.freq;
|
|
2319
|
-
const prevPsi = fl.psi;
|
|
2320
|
-
if (fl.amp > 0.01 || fl.freq > 0) fl.psi = wrap01(fl.psi + fl.freq * dt);
|
|
2321
|
-
if (fl.psi < prevPsi && fl.amp > 0.05) { fl.beatCount++; if (air || P.flap > 0.3) this.emit('wingbeat', { amp: fl.amp }); }
|
|
2322
|
-
this.state.stats.flapFreq = fl.amp > 0.05 ? fl.freq : 0;
|
|
2323
|
-
// stroke blend between cruise and power parameters
|
|
2324
|
-
this.kS = this.kS || new Spring(0);
|
|
2325
|
-
const k = clamp(this.kS.step(clamp(smooth(0.5, 1, fl.power) * (air ? 1 : 0) + gf * 0.8, 0, 1), 8, dt), 0, 1);
|
|
2326
|
-
const D = lerp(flap.down, pw.down, k), ps = fl.psi;
|
|
2327
|
-
let e, up = 0, dn = 0;
|
|
2328
|
-
// (sin^2 shaping keeps the flexion / sweep / twist terms C1 across the stroke reversals)
|
|
2329
|
-
if (ps < D) { const u = ps / D; e = Math.cos(Math.PI * u); dn = Math.sin(Math.PI * u) ** 2; }
|
|
2330
|
-
else { const u = (ps - D) / (1 - D); e = -Math.cos(Math.PI * u); up = Math.sin(Math.PI * u) ** 2; }
|
|
2331
|
-
const ampN = fl.amp / Math.max(1e-3, lerp(flap.amp, pw.amp, k));
|
|
2332
|
-
const mid = lerp(flap.mid, pw.mid, k) + this.flare * 15 * DEG;
|
|
2333
|
-
// ---- openness: folded on the ground, open in the air / for ground actions
|
|
2334
|
-
let openT = air ? 1 : clamp(this.gait.wings * P.gaitW * smooth(0.3, 1.5, this.speed / cfg.sq), 0, 1);
|
|
2335
|
-
openT = Math.max(openT, P.wingOpen);
|
|
2336
|
-
// take-off: wings opened and raised during the crouch, ready for the first downstroke (takeoff.open)
|
|
2337
|
-
let preT = 0;
|
|
2338
|
-
const TO = this.to, toOpen = cfg.takeoff.open || 0;
|
|
2339
|
-
if (toOpen > 0 && this.mode === 'takeoff' && TO && !TO.hopOnly) {
|
|
2340
|
-
preT = TO.air ? toOpen * (1 - smooth(0, 0.25, TO.tAir)) : toOpen * smooth(0, Math.max(1e-3, 0.6 * (TO.crouch + TO.push)), TO.t);
|
|
2341
|
-
openT = Math.max(openT, preT);
|
|
2342
|
-
}
|
|
2343
|
-
const flareW = this.flare;
|
|
2344
|
-
const open = fl.open.step(openT, air ? 9 : preT > 0 ? 10 : 7, dt);
|
|
2345
|
-
// after landing: wings stay up briefly then fold
|
|
2346
|
-
// (groundLift only while the body is upright: a bird lying on its side must not lift a wing into the ground)
|
|
2347
|
-
const upright = W.groundLift ? smooth(0.5, 0.85, tv(0, 1, 0).applyQuaternion(this.qChest).y) : 0;
|
|
2348
|
-
for (let sI = 0; sI < 2; sI++) {
|
|
2349
|
-
const side = sI === 0 ? 1 : -1, wp = this.wp[sI], S = this.wingS[sI];
|
|
2350
|
-
const fold = W.fold, gl = W.glide;
|
|
2351
|
-
const fx = (key) => lerp(fold[key], gl[key], open);
|
|
2352
|
-
let elev = fx('elev'), pitch = fx('pitch'), alpha = fx('alpha'), elbow = fx('elbow'), wrist = fx('wrist'), bend = fx('bend'), twist = fx('twist');
|
|
2353
|
-
// stroke around the open pose
|
|
2354
|
-
if (fl.amp > 1e-3) {
|
|
2355
|
-
const flexU = lerp(flap.upFlex, pw.upFlex, k) * ampN, flexW = lerp(flap.upWrist, pw.upWrist, k) * ampN;
|
|
2356
|
-
const sweep = lerp(flap.sweep, pw.sweep, k) * ampN, tw = lerp(flap.twist, pw.twist, k) * ampN;
|
|
2357
|
-
elev += (mid + e * fl.amp * 0.5) * open;
|
|
2358
|
-
elbow += flexU * up; wrist += flexW * up;
|
|
2359
|
-
alpha += (-sweep * dn + sweep * 0.7 * up) * open;
|
|
2360
|
-
pitch += (-tw * 0.35 * dn + tw * 0.5 * up);
|
|
2361
|
-
twist += tw * (dn - 0.7 * up);
|
|
2362
|
-
bend += (-8 * dn + 10 * up) * DEG * ampN;
|
|
2363
|
-
} else if (air) {
|
|
2364
|
-
// glide: slight dihedral breathing and gust response
|
|
2365
|
-
elev += (Math.sin(this.time * 1.7 + sI) * 1.5 + Math.sin(this.time * 4.1) * 0.6) * DEG;
|
|
2366
|
-
}
|
|
2367
|
-
// flare: wings raised, swept forward, high angle of attack
|
|
2368
|
-
if (flareW > 0) {
|
|
2369
|
-
alpha += -18 * DEG * flareW; pitch += 25 * DEG * flareW; elev += 8 * DEG * flareW;
|
|
2370
|
-
}
|
|
2371
|
-
// banking: the inner wing slightly lower, outer higher (roll control)
|
|
2372
|
-
if (air) elev += side * this.bankS.x * 0.1;
|
|
2373
|
-
// posture / action offsets
|
|
2374
|
-
elev += P.wingElev + P.wingElevS[sI];
|
|
2375
|
-
// long wings part-open on the ground are carried raised so the tips clear the ground (wing.groundLift)
|
|
2376
|
-
if (!air && W.groundLift) elev += W.groundLift * 4 * open * (1 - open) * (1 - this.flare) * upright;
|
|
2377
|
-
if (preT > 0) elev += 40 * DEG * preT;
|
|
2378
|
-
alpha += P.wingAlpha;
|
|
2379
|
-
pitch += P.wingPitch;
|
|
2380
|
-
elbow = clamp(elbow - P.wingExtend * 60 * DEG, 5 * DEG, 178 * DEG);
|
|
2381
|
-
wrist = clamp(wrist - P.wingExtend * 60 * DEG + P.wingFoldExtra, 3 * DEG, 178 * DEG);
|
|
2382
|
-
elev += P.wingDroop * -20 * DEG * (1 - open * 0.5);
|
|
2383
|
-
// the stroke's top, the landing flare and the take-off lift add up to more than the shoulder can
|
|
2384
|
-
// raise the wing (the full power-stroke upstroke on top of a flare lifted it to 77 deg, the skin of
|
|
2385
|
-
// the shoulder stretched past 3x): soft cap at ~60 deg above the horizontal (C1, no kink)
|
|
2386
|
-
{ const a = ELEV_CAP - 18 * DEG; if (elev > a) elev = a + 18 * DEG * Math.tanh((elev - a) / (18 * DEG)); }
|
|
2387
|
-
// springs for the non-flapping part (the stroke itself is exact)
|
|
2388
|
-
if (!this.wingInit) { S.elev.reset(elev); S.pitch.reset(pitch); S.alpha.reset(alpha); S.elbow.reset(elbow); S.wrist.reset(wrist); S.bend.reset(bend); S.twist.reset(twist); }
|
|
2389
|
-
const om = lerp(P.wingOmega * 18, 60, smooth(0, 0.4, fl.amp));
|
|
2390
|
-
wp.elev = S.elev.step(elev, om, dt); wp.pitch = S.pitch.step(pitch, om, dt); wp.alpha = S.alpha.step(alpha, om, dt);
|
|
2391
|
-
wp.elbow = S.elbow.step(elbow, om, dt); wp.wrist = S.wrist.step(wrist, om, dt); wp.bend = S.bend.step(bend, om, dt); wp.twist = S.twist.step(twist, om, dt);
|
|
2392
|
-
wp.uTwist = wp.twist * 0.3; wp.tilt = W.tilt;
|
|
2393
|
-
this.solveWing(sI, side, wp, dn, up, ampN);
|
|
2394
|
-
}
|
|
2395
|
-
this.wingInit = true;
|
|
2396
|
-
}
|
|
2397
|
-
|
|
2398
|
-
solveWing(sI, side, wp, dn, up, ampN) {
|
|
2399
|
-
const cfg = this.cfg, W = cfg.wing, S = LEGS[sI], P = this.P;
|
|
2400
|
-
const fr = this.wf[sI];
|
|
2401
|
-
wingFK(fr, side, W.L, wp);
|
|
2402
|
-
// body-local -> world
|
|
2403
|
-
const q = this.qChest;
|
|
2404
|
-
const sh = tv().copy(W.shoulder[sI]).applyQuaternion(q).add(this.body);
|
|
2405
|
-
const toW = (v, out) => out.set(v.x, v.y, v.z).applyQuaternion(q);
|
|
2406
|
-
const dH = toW(fr.dH, tv()), nH = toW(fr.nH, tv()), dU = toW(fr.dU, tv()), nU = toW(fr.nU, tv()), dM = toW(fr.dM, tv()), nM = toW(fr.nM, tv());
|
|
2407
|
-
const elbow = tv().copy(sh).addScaledVector(dH, W.L.h);
|
|
2408
|
-
const r = W.skin * cfg.s;
|
|
2409
|
-
// the elbow too (a wing folded under a dead bird falling onto it: the forearm met the ground
|
|
2410
|
-
// first): the upper arm swings up about the shoulder just enough, with the same smooth ramp
|
|
2411
|
-
{
|
|
2412
|
-
const ge = this.terrainH(elbow.x, elbow.z) + r, pe = ge - elbow.y, ke = 0.012 * cfg.s;
|
|
2413
|
-
if (pe > -ke) {
|
|
2414
|
-
elbow.y += pe >= ke ? pe : (pe + ke) * (pe + ke) / (4 * ke);
|
|
2415
|
-
dH.subVectors(elbow, sh).normalize();
|
|
2416
|
-
elbow.copy(sh).addScaledVector(dH, W.L.h);
|
|
2417
|
-
}
|
|
2418
|
-
}
|
|
2419
|
-
const wrist = tv().copy(elbow).addScaledVector(dU, W.L.u);
|
|
2420
|
-
const tip = tv().copy(wrist).addScaledVector(dM, W.L.m);
|
|
2421
|
-
// keep the wrist and the hand tip above the terrain (lying, dead): lift the whole wing tip
|
|
2422
|
-
const gw = this.terrainH(wrist.x, wrist.z) + r, gt = this.terrainH(tip.x, tip.z) + r;
|
|
2423
|
-
const pen = Math.max(gw - wrist.y, gt - tip.y), kr = 0.012 * cfg.s;
|
|
2424
|
-
if (pen > -kr) {
|
|
2425
|
-
// smooth ramp (continuous in the penetration: no kink when the wing starts touching)
|
|
2426
|
-
const ramp = (x) => (x >= kr ? x : x > -kr ? (x + kr) * (x + kr) / (4 * kr) : 0);
|
|
2427
|
-
const lift = ramp(pen);
|
|
2428
|
-
// (the wrist follows 70 % of the tip's lift, but never stays below its own clearance: a wing
|
|
2429
|
-
// folded under a falling dead bird pressed its forearm into the ground)
|
|
2430
|
-
wrist.y += Math.max(lift * 0.7, ramp(gw - wrist.y)); tip.y += lift;
|
|
2431
|
-
dU.subVectors(wrist, elbow).normalize(); dM.subVectors(tip, wrist).normalize();
|
|
2432
|
-
}
|
|
2433
|
-
// the wing's skin too (measured on the mesh, see wingSupport): a half-open wing under a body rolling
|
|
2434
|
-
// onto it - a bird falling dead lands with its wings raised - put the forearm's trailing skin 4-6
|
|
2435
|
-
// cm into the ground with every joint clear. When a wing is near the ground, it swings up about the
|
|
2436
|
-
// shoulder, in the vertical plane through its deepest skin point, just enough (soft onset)
|
|
2437
|
-
const ws = this._wingSup ? this._wingSup[sI] : null;
|
|
2438
|
-
if (ws) {
|
|
2439
|
-
// (cheap gate: every skin point lies within reach + R of the shoulder; a lift still releasing
|
|
2440
|
-
// continues outside it)
|
|
2441
|
-
const reach = W.L.h + W.L.u + W.L.m + Math.max(ws[0].R, ws[1].R, ws[2].R);
|
|
2442
|
-
const gS = this.terrainH(sh.x, sh.z);
|
|
2443
|
-
const S = this._wsS || (this._wsS = [new Spring(0), new Spring(0)]);
|
|
2444
|
-
if (sh.y - reach < Math.max(gS, gw - r, gt - r) + 0.05 * cfg.s || S[sI].x > 1e-5) this.wingSkinLift(ws, sh, elbow, wrist, tip, dH, nH, dU, nU, dM, nM, gS, sI);
|
|
2445
|
-
}
|
|
2446
|
-
const wi = this.ix.wing[sI];
|
|
2447
|
-
this.setBoneC(wi.humerus, sh, dH, nH);
|
|
2448
|
-
this.setBoneC(wi.ulna, elbow, dU, nU);
|
|
2449
|
-
this.setBoneC(wi.hand, wrist, dM, nM);
|
|
2450
|
-
// crowd LOD: between full solves the feathers ride rigidly on the hand / forearm
|
|
2451
|
-
if (!this.fullFrame) { this.follow(wi.hand, this.ix.pri[sI]); this.follow(wi.ulna, this.ix.sec[sI]); return; }
|
|
2452
|
-
// ---- feathers: fan follows the wrist / elbow extension; primaries separate ("fingers") and
|
|
2453
|
-
// bend up under load on the downstroke
|
|
2454
|
-
const E = cfg.wingExt;
|
|
2455
|
-
const eH = clamp(extension(wp.wrist, E.fW, E.oW) + P.fanExtra, 0, 1.25), eU = clamp(extension(wp.elbow, E.fE, E.oE) + P.fanExtra * 0.5, 0, 1.2);
|
|
2456
|
-
const ff = this.ff, fw = this._fw;
|
|
2457
|
-
fw.P0.x = wrist.x; fw.P0.y = wrist.y; fw.P0.z = wrist.z;
|
|
2458
|
-
const air = this.mode !== 'ground' && this.mode !== 'perch';
|
|
2459
|
-
// (feather load eases in / out with the air weight: no bend step at take-off / touchdown)
|
|
2460
|
-
const aw = this._featherAirW = expTo(this._featherAirW ?? 0, air ? 1 : 0, sI === 0 ? (this._dt || 1 / 60) : 0, 0.08);
|
|
2461
|
-
const load = dn * ampN * aw;
|
|
2462
|
-
const sk = this.lod === 2 && (this.frame & 1) === 1;
|
|
2463
|
-
for (const f of cfg.pri) {
|
|
2464
|
-
const a = f.fold + (f.spread - f.fold) * eH;
|
|
2465
|
-
const bend0 = f.bend;
|
|
2466
|
-
f.bend = bend0 + (f.emarg > 0 ? load * 10 * DEG * f.emarg : load * 4 * DEG) - up * 3 * DEG * ampN + (f.emarg > 0 ? W.fingerLift * f.emarg * aw * this.fl.glide * eH : 0);
|
|
2467
|
-
featherFrame(ff, fw.P0, dM, nM, W.L.m, side, f, a, false);
|
|
2468
|
-
f.bend = bend0;
|
|
2469
|
-
this.featherBone(this.ix.pri[sI][f.i], ff, f.len, sk, 0, side, f);
|
|
2470
|
-
}
|
|
2471
|
-
for (const f of cfg.sec) {
|
|
2472
|
-
const a = f.fold + (f.spread - f.fold) * eU;
|
|
2473
|
-
featherFrame(ff, fw.P0, dU, nU, W.L.u, side, f, a, true);
|
|
2474
|
-
this.featherBone(this.ix.sec[sI][f.i], ff, f.len, sk, 0, side, f);
|
|
2475
|
-
}
|
|
2476
|
-
if (this.lod === 2) { this.store(wi.hand, this.ix.pri[sI]); this.store(wi.ulna, this.ix.sec[sI]); }
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
// (see solveWing) the wing's skin against the terrain: every skin point that comes within kr of the
|
|
2480
|
-
// ground asks for the rotation of the whole wing about the shoulder, in the vertical plane through
|
|
2481
|
-
// it, that lifts it clear (soft onset); the wing turns by the largest of them about the axis blended
|
|
2482
|
-
// from them by how much each needs (continuous, as the feather vanes: taking the single deepest point
|
|
2483
|
-
// swapped the axis between points lying equally deep - a wing folding off the ground popped its
|
|
2484
|
-
// feathers at 250 rad/s*). A bone whose skin's bounding sphere is clear of the ground is skipped
|
|
2485
|
-
// (its points could not ask for anything)
|
|
2486
|
-
wingSkinLift(ws, sh, elbow, wrist, tip, dH, nH, dU, nU, dM, nM, gS, sI) {
|
|
2487
|
-
const cfg = this.cfg, s = cfg.s, kr = 0.008 * s, clr = 0.002 * s;
|
|
2488
|
-
if (!this.Cm) this.setBoneC(-1);
|
|
2489
|
-
const n = this.terrainN(sh.x, sh.z, tv()), gx = n.x / n.y, gz = n.z / n.y, grad = Math.hypot(gx, gz);
|
|
2490
|
-
const heads = [sh, elbow, wrist], ds = [dH, dU, dM], ns = [nH, nU, nM];
|
|
2491
|
-
const axis = tv(0, 0, 0);
|
|
2492
|
-
let th = 0;
|
|
2493
|
-
for (let b = 0; b < 3; b++) {
|
|
2494
|
-
const wb = ws[b];
|
|
2495
|
-
const e = writeFrameC(_m4c, heads[b], ds[b], ns[b], this.Cm[wb.ib], null).elements, P = wb.pts;
|
|
2496
|
-
// (the terrain may bend away from the shoulder's tangent plane: a margin that grows with the distance)
|
|
2497
|
-
const cx = e[0] * wb.c[0] + e[4] * wb.c[1] + e[8] * wb.c[2] + e[12], cy = e[1] * wb.c[0] + e[5] * wb.c[1] + e[9] * wb.c[2] + e[13], cz = e[2] * wb.c[0] + e[6] * wb.c[1] + e[10] * wb.c[2] + e[14];
|
|
2498
|
-
const dc = Math.hypot(cx - sh.x, cz - sh.z);
|
|
2499
|
-
const curv = 0.01 * s + 0.1 * (dc + wb.R);
|
|
2500
|
-
if (cy - wb.R - (gS - gx * (cx - sh.x) - gz * (cz - sh.z)) - grad * wb.R > kr + clr + curv) continue;
|
|
2501
|
-
for (let k = 0, n3 = P.length / 3; k < n3; k++) {
|
|
2502
|
-
const x = P[k * 3], y = P[k * 3 + 1], z = P[k * 3 + 2];
|
|
2503
|
-
const wx = e[0] * x + e[4] * y + e[8] * z + e[12], wy = e[1] * x + e[5] * y + e[9] * z + e[13], wz = e[2] * x + e[6] * y + e[10] * z + e[14];
|
|
2504
|
-
if (gS - gx * (wx - sh.x) - gz * (wz - sh.z) + clr - wy <= -(kr + curv)) continue;
|
|
2505
|
-
const pe = this.terrainH(wx, wz) + clr - wy;
|
|
2506
|
-
if (pe <= -kr) continue;
|
|
2507
|
-
const lift = pe >= kr ? pe : (pe + kr) * (pe + kr) / (4 * kr);
|
|
2508
|
-
const bx = wx - sh.x, by = wy - sh.y, bz = wz - sh.z;
|
|
2509
|
-
const rv = Math.hypot(bx, by, bz), h = Math.hypot(bx, bz);
|
|
2510
|
-
if (rv < 1e-6 || h < 1e-6) continue;
|
|
2511
|
-
const a = Math.asin(clamp((by + lift) / rv, -1, 1)) - Math.asin(clamp(by / rv, -1, 1));
|
|
2512
|
-
if (!(a > 0)) continue;
|
|
2513
|
-
th = Math.max(th, a);
|
|
2514
|
-
axis.x -= (bz / h) * lift; axis.z += (bx / h) * lift;
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
// the lift tightens at once (no skin in the ground) and releases through a spring about the last
|
|
2518
|
-
// axis: a wing taking the impact of a falling body was flung up and dropped back within a few
|
|
2519
|
-
// samples as the body's descent stopped (a V in its angle: the primaries jittered 6 % S)
|
|
2520
|
-
const S = this._wsS[sI], AX = this._wsAx || (this._wsAx = [new THREE.Vector3(), new THREE.Vector3()]);
|
|
2521
|
-
th = Math.min(th, 1.2);
|
|
2522
|
-
if (th > 1e-6 && axis.lengthSq() > 1e-16) AX[sI].copy(axis.normalize());
|
|
2523
|
-
if (th >= S.x) S.reset(th); else S.step(th, 22, this._dt || 1 / 60);
|
|
2524
|
-
th = Math.max(0, S.x);
|
|
2525
|
-
if (!(th > 1e-6) || AX[sI].lengthSq() < 0.5) return;
|
|
2526
|
-
for (const v of [dH, nH, dU, nU, dM, nM]) v.applyAxisAngle(AX[sI], th);
|
|
2527
|
-
// (the joints turn with the wing, as they are: the wrist may sit off the ulna's length where the
|
|
2528
|
-
// clearance above lifted it - rebuilding it from the lengths snapped it back 5 cm as the lift ended)
|
|
2529
|
-
const d = tv();
|
|
2530
|
-
for (const p of [elbow, wrist, tip]) { d.subVectors(p, sh).applyAxisAngle(AX[sI], th); p.copy(sh).add(d); }
|
|
2531
|
-
}
|
|
2532
|
-
|
|
2533
|
-
// A feather bent on an arc (a rooster's sickle, core/build/featherCards.js `arc`) sweeps down behind
|
|
2534
|
-
// its root: swing it up about the bird's lateral axis just enough that no point along the arc is
|
|
2535
|
-
// below the terrain. The angle is a soft maximum over points along the arc, so it is continuous.
|
|
2536
|
-
liftArcFeather(root, ax, nn, len, arc, side, clr) {
|
|
2537
|
-
const s = this.cfg.s, F = this.F, Lf = this.Lf, kr = 0.012 * s;
|
|
2538
|
-
const tvv = tv().crossVectors(nn, ax).multiplyScalar(side), R = len / arc, q = tv();
|
|
2539
|
-
let th = 0;
|
|
2540
|
-
for (let k = 2; k <= 12; k++) {
|
|
2541
|
-
const an = (arc * k) / 12;
|
|
2542
|
-
q.copy(ax).multiplyScalar(R * Math.sin(an)).addScaledVector(tvv, R * (1 - Math.cos(an)));
|
|
2543
|
-
const pen = this.terrainH(root.x + q.x, root.z + q.z) + clr - root.y - q.y;
|
|
2544
|
-
if (pen <= -kr) continue;
|
|
2545
|
-
// soft (quadratic) onset of the lift: the rotation that lifts a point hanging below the root
|
|
2546
|
-
// grows like the square root of the lift, so a linear onset would pop
|
|
2547
|
-
const lift = pen >= kr ? pen : (pen + kr) * (pen + kr) / (4 * kr);
|
|
2548
|
-
const b = -(q.x * F.x + q.z * F.z), rho = Math.hypot(b, q.y);
|
|
2549
|
-
if (rho < 1e-4) continue;
|
|
2550
|
-
// elevation behind the root; points below or in front of the root are faded out (swinging the
|
|
2551
|
-
// feather back cannot lift them without first lowering them: no continuous solution there)
|
|
2552
|
-
const wf = smooth(0.05, 0.4, b / rho);
|
|
2553
|
-
if (wf > 0) th = Math.max(th, (Math.asin(clamp((q.y + lift) / rho, -1, 1)) - Math.atan2(q.y, b)) * wf);
|
|
2554
|
-
}
|
|
2555
|
-
if (th > 1e-5) { th = Math.min(th, 2.4); ax.applyAxisAngle(Lf, th); nn.applyAxisAngle(Lf, th); }
|
|
2556
|
-
}
|
|
2557
|
-
|
|
2558
|
-
// A feather's vane edges: the tip and the root alone were kept clear, but a feather lying on the
|
|
2559
|
-
// ground or rising from it (a folded wing under a body that rolls back up) dipped a vane edge into it
|
|
2560
|
-
// (an eagle's secondaries are 7 cm wide). Both edges near the root, at mid-length and near the tip
|
|
2561
|
-
// (the width a covert card reaches) are lifted by pitching the feather up about its root, about the
|
|
2562
|
-
// axis the tip clamp uses (horizontal, across the rachis; a steep feather blends in its own lateral
|
|
2563
|
-
// axis): it lifts both edges at once. Each point's angle is solved on its circle about that axis; the
|
|
2564
|
-
// feather turns by the largest (soft onset). (Each point's own most direct axis cancelled out between
|
|
2565
|
-
// the two edges near the root and swung with small imbalances: a hen's primaries jittered.)
|
|
2566
|
-
vaneLift(root, ax, nn, len, f, side, clr) {
|
|
2567
|
-
const s = this.cfg.s, kr = 0.006 * s;
|
|
2568
|
-
const T = tv().crossVectors(nn, ax), tl = T.length();
|
|
2569
|
-
if (tl < 1e-6) return;
|
|
2570
|
-
T.multiplyScalar(side / tl); // (the inner vane's side, as the card)
|
|
2571
|
-
const Np = tv().crossVectors(ax, T).multiplyScalar(side); // (the normal square to the rachis)
|
|
2572
|
-
const k = tv().crossVectors(ax, UP), kl = k.length();
|
|
2573
|
-
if (kl < 0.35) { const lu = tv().crossVectors(T, ax).y; k.addScaledVector(T, lu * (1 - kl / 0.35)); }
|
|
2574
|
-
if (k.lengthSq() < 1e-10) return;
|
|
2575
|
-
k.normalize();
|
|
2576
|
-
const d = tv(), kd = tv(), vp = f.vp, droop = f.droopM, curve = f.curveM;
|
|
2577
|
-
let th = 0;
|
|
2578
|
-
for (let i = 0, nP = vp.length / 3 * 2; i < nP; i++) {
|
|
2579
|
-
// (the card's shape: the rachis curves toward the inner vane and droops toward -normal, the
|
|
2580
|
-
// convex vane's edges sit lower still)
|
|
2581
|
-
const j = (i >> 1) * 3, t = vp[j], w = i & 1 ? vp[j + 2] : -vp[j + 1];
|
|
2582
|
-
d.copy(ax).multiplyScalar(len * t).addScaledVector(T, curve * t * t + w).addScaledVector(Np, -(droop * t * t + 0.12 * Math.abs(w)));
|
|
2583
|
-
const pe = this.terrainH(root.x + d.x, root.z + d.z) + clr - root.y - d.y;
|
|
2584
|
-
if (pe <= -kr) continue;
|
|
2585
|
-
const lift = pe >= kr ? pe : (pe + kr) * (pe + kr) / (4 * kr);
|
|
2586
|
-
// height of the point turned by x about k (through the root): c + A cos x + B sin x
|
|
2587
|
-
const A = d.y - d.dot(k) * k.y, B = kd.crossVectors(k, d).y, R = Math.hypot(A, B);
|
|
2588
|
-
if (R < 1e-6) continue;
|
|
2589
|
-
// (a point the turn lowers first, or barely lifts, is faded out: continuous)
|
|
2590
|
-
const wf = smooth(0, 0.25, B / R);
|
|
2591
|
-
if (!(wf > 0)) continue;
|
|
2592
|
-
const phi = Math.atan2(B, A);
|
|
2593
|
-
const x = phi - Math.acos(clamp((A + lift) / R, -1, 1));
|
|
2594
|
-
if (x > 0) th = Math.max(th, x * wf);
|
|
2595
|
-
}
|
|
2596
|
-
if (th > 1e-6) { th = Math.min(th, 1.2); ax.applyAxisAngle(k, th); nn.applyAxisAngle(k, th); }
|
|
2597
|
-
}
|
|
2598
|
-
|
|
2599
|
-
// writes a feather bone, lifting it off the terrain (rotating about its root) if needed
|
|
2600
|
-
// (wide: the widest half vane, m, for the vane-edge check; 0 = the rachis only)
|
|
2601
|
-
featherBone(name, ff, len, skipClamp, arc = 0, side = 1, f = null) {
|
|
2602
|
-
const r = ff.root, a = ff.axis, n = ff.normal;
|
|
2603
|
-
const root = tv(r.x, r.y, r.z), ax = tv(a.x, a.y, a.z), nn = tv(n.x, n.y, n.z);
|
|
2604
|
-
if (!skipClamp) {
|
|
2605
|
-
const s = this.cfg.s;
|
|
2606
|
-
const clr = 0.004 * s;
|
|
2607
|
-
const wide = f ? f.vane : 0, droop = f ? f.droopM : 0, curve = f ? f.curveM : 0;
|
|
2608
|
-
if (arc > 1e-3) this.liftArcFeather(root, ax, nn, len, arc, side, clr);
|
|
2609
|
-
else {
|
|
2610
|
-
// (the tip as the card draws it: curved toward the inner vane and drooped toward -normal)
|
|
2611
|
-
const Ti = tv().crossVectors(nn, ax), til = Ti.length();
|
|
2612
|
-
if (til > 1e-6) Ti.multiplyScalar(side / til);
|
|
2613
|
-
const Np = tv().crossVectors(ax, Ti).multiplyScalar(side);
|
|
2614
|
-
// (and the straight tip, the bone's end: whichever needs more lift - a feather upside down has
|
|
2615
|
-
// its drooped card tip above the straight one)
|
|
2616
|
-
let tipX = root.x + ax.x * len + Ti.x * curve - Np.x * droop, tipZ = root.z + ax.z * len + Ti.z * curve - Np.z * droop, tipY = root.y + ax.y * len + Ti.y * curve - Np.y * droop;
|
|
2617
|
-
let gT = this.terrainH(tipX, tipZ) + clr;
|
|
2618
|
-
if (curve > 0 || droop > 0) {
|
|
2619
|
-
const sx = root.x + ax.x * len, sz = root.z + ax.z * len, sy = root.y + ax.y * len, gs = this.terrainH(sx, sz) + clr;
|
|
2620
|
-
if (gs - sy > gT - tipY) { tipX = sx; tipZ = sz; tipY = sy; gT = gs; }
|
|
2621
|
-
}
|
|
2622
|
-
if (tipY < gT + 0.02 * s) {
|
|
2623
|
-
// smooth lift: rotate the axis toward up just enough (continuous in the penetration)
|
|
2624
|
-
const need = gT - tipY;
|
|
2625
|
-
const pen = smin(-need, 0, 0.012 * s); // <= 0, smooth near zero
|
|
2626
|
-
if (pen < 0) {
|
|
2627
|
-
const dy = -pen / len;
|
|
2628
|
-
const k = tv().crossVectors(ax, UP);
|
|
2629
|
-
// a steep feather: blend in its own lateral axis, weighted by how much a rotation about it
|
|
2630
|
-
// lifts the tip (latc x ax).y. The product latc * lift does not depend on latc's sign, so
|
|
2631
|
-
// the axis is continuous (flipping latc by the sign of the lift popped primaries of a wing
|
|
2632
|
-
// folded under a bird lying on its side as that sign crossed zero)
|
|
2633
|
-
const kl = k.length();
|
|
2634
|
-
if (kl < 0.35) {
|
|
2635
|
-
const latc = tv().crossVectors(nn, ax);
|
|
2636
|
-
const lift = tv().crossVectors(latc, ax).y;
|
|
2637
|
-
k.addScaledVector(latc, lift * (1 - kl / 0.35));
|
|
2638
|
-
}
|
|
2639
|
-
if (k.lengthSq() > 1e-8) {
|
|
2640
|
-
k.normalize();
|
|
2641
|
-
const cur = Math.asin(clamp(ax.y, -1, 1));
|
|
2642
|
-
const want = Math.asin(clamp(ax.y + dy, -1, 1));
|
|
2643
|
-
const ang = want - cur;
|
|
2644
|
-
ax.applyAxisAngle(k, ang); nn.applyAxisAngle(k, ang);
|
|
2645
|
-
// flatten the vane onto the ground when it lies there: the smaller turn about the rachis
|
|
2646
|
-
// that brings the vane level (either face up), faded out for a vane standing on its edge,
|
|
2647
|
-
// where both turns are 90 deg (the old one flipped from +72 to -72 deg there). The amount
|
|
2648
|
-
// grows over a fifth of the feather's length of penetration (over 2 cm, a wing lifting off
|
|
2649
|
-
// the ground at impact speed unrolled an eagle's secondaries 50 deg in 13 ms: 88 rad/s*)
|
|
2650
|
-
const lie = clamp(-pen / (0.2 * len), 0, 1);
|
|
2651
|
-
const fade = smooth(0.05, 0.3, Math.abs(nn.y));
|
|
2652
|
-
if (lie > 0 && fade > 0) {
|
|
2653
|
-
const side2 = tv().crossVectors(nn, ax);
|
|
2654
|
-
const tilt = Math.atan(side2.y / nn.y) * lie * 0.8 * fade;
|
|
2655
|
-
nn.applyAxisAngle(ax, -tilt);
|
|
2656
|
-
}
|
|
2657
|
-
}
|
|
2658
|
-
}
|
|
2659
|
-
}
|
|
2660
|
-
// the vane edges, when the feather is near the ground at all
|
|
2661
|
-
if (wide > 0 && Math.min(root.y, root.y + ax.y * len) - 1.2 * wide - droop - curve < Math.max(gT, this.terrainH(root.x, root.z) + clr) + 0.012 * s) this.vaneLift(root, ax, nn, len, f, side, clr);
|
|
2662
|
-
}
|
|
2663
|
-
// root above the ground too
|
|
2664
|
-
const gR = this.terrainH(root.x, root.z) + clr;
|
|
2665
|
-
if (root.y < gR) root.y = gR;
|
|
2666
|
-
}
|
|
2667
|
-
this.setBoneC(name, root, ax, nn);
|
|
2668
|
-
}
|
|
2669
|
-
|
|
2670
|
-
// ----------------------------------------------------------------- tail
|
|
2671
|
-
poseTail(dt, air) {
|
|
2672
|
-
const cfg = this.cfg, T = cfg.tail, P = this.P, s = cfg.s, g = this.gait;
|
|
2673
|
-
const vn = this.speed / cfg.sq;
|
|
2674
|
-
let pitchT = P.tailPitch, yawT = P.tailYaw, fanT = T.fanRest + P.tailFan, twistT = 0;
|
|
2675
|
-
if (!air) {
|
|
2676
|
-
const mv = smooth(0.03, 0.3, vn) * P.gaitW;
|
|
2677
|
-
const ph = TAU * (this.phase - g.off[0]);
|
|
2678
|
-
pitchT += T.walkBob * DEG * g.tail * mv * Math.sin(2 * ph - 0.6) * (1 - g.hopW) + g.hopW * mv * 12 * DEG * Math.sin(ph + 0.8);
|
|
2679
|
-
yawT += T.sway * DEG * mv * Math.sin(ph - 0.5) * (1 - g.hopW);
|
|
2680
|
-
yawT += clamp(-this.yawRate * 0.15, -0.3, 0.3);
|
|
2681
|
-
// counter-balance while hopping fast / braking
|
|
2682
|
-
pitchT += clamp(-this.accel.dot(this.F) * 0.03, -0.25, 0.25);
|
|
2683
|
-
} else {
|
|
2684
|
-
// flight: closed at cruise, fanned when slow / braking / climbing, depressed in the flare,
|
|
2685
|
-
// twisted and yawed into turns
|
|
2686
|
-
const fl = cfg.flight;
|
|
2687
|
-
const slow = clamp(1 - (this.speed - fl.minSpeed) / Math.max(1, fl.cruise - fl.minSpeed), 0, 1);
|
|
2688
|
-
fanT = Math.max(fanT, 0.1 + 0.6 * slow + 0.3 * smooth(0.6, 1, this.fl.power) + (T.soarFan || 0) * this.fl.glide);
|
|
2689
|
-
const flare = this.flare;
|
|
2690
|
-
fanT = lerp(fanT, 1, flare);
|
|
2691
|
-
pitchT += -0.1 * slow - 0.12 * flare + 0.05 * Math.sin(TAU * this.fl.psi) * (1 - this.fl.glide) * this.fl.amp;
|
|
2692
|
-
twistT = clamp(this.yawRate * 0.25, -0.5, 0.5);
|
|
2693
|
-
yawT += clamp(-this.yawRate * 0.12, -0.25, 0.25);
|
|
2694
|
-
// in flight the tail continues the (level) body axis (tail.flight < 1: long display tails, e.g.
|
|
2695
|
-
// a rooster's sickles, stay partly raised)
|
|
2696
|
-
pitchT -= cfg.tailRel * (T.flight ?? 1) * cfg.tailFlightK;
|
|
2697
|
-
}
|
|
2698
|
-
// flight.shape.fan: the tail fanned at least this much in flight
|
|
2699
|
-
if (this.shapeW > 0) fanT = Math.max(fanT, cfg.shape.fan * this.shapeW);
|
|
2700
|
-
const S = this.tailS;
|
|
2701
|
-
const om = air ? 10 : 16;
|
|
2702
|
-
const tp = S.pitch.step(pitchT, om, dt), ty = S.yaw.step(yawT, om, dt), tf = clamp(S.fan.step(fanT, 12, dt), 0, 1.1), tt = S.twist.step(twistT, 8, dt);
|
|
2703
|
-
// pygostyle frame: bind direction in the body, pitched / yawed about the tail base
|
|
2704
|
-
const base = this.B(cfg.tailLocal, tv());
|
|
2705
|
-
const d = this.Bv(cfg.tailDir0, tv());
|
|
2706
|
-
const lat = this.Bv(tv(1, 0, 0), tv());
|
|
2707
|
-
d.applyAxisAngle(lat, tp);
|
|
2708
|
-
const up = tv().crossVectors(d, lat).normalize();
|
|
2709
|
-
if (up.dot(this.Bv(tv(0, 1, 0), tv())) < 0) up.negate();
|
|
2710
|
-
d.applyAxisAngle(up, ty);
|
|
2711
|
-
lat.applyAxisAngle(up, ty);
|
|
2712
|
-
// never curl under the body: keep the tail pointing backward (at most ~70 deg below horizontal)
|
|
2713
|
-
const back = tv().copy(this.F).negate();
|
|
2714
|
-
const cur = Math.atan2(-d.y, d.dot(back));
|
|
2715
|
-
if (cur > 55 * DEG) {
|
|
2716
|
-
const want = 55 * DEG + 15 * DEG * Math.tanh((cur - 55 * DEG) / (15 * DEG));
|
|
2717
|
-
// rotate about the lateral axis (down -> back), well defined even for a vertical tail
|
|
2718
|
-
d.applyAxisAngle(lat, cur - want); up.applyAxisAngle(lat, cur - want);
|
|
2719
|
-
}
|
|
2720
|
-
// twist (roll about the tail axis)
|
|
2721
|
-
lat.applyAxisAngle(d, tt); up.applyAxisAngle(d, tt);
|
|
2722
|
-
// keep the tail tip (feathers) off the ground: raise the pitch just enough
|
|
2723
|
-
const maxLen = cfg.rec.length ? cfg.rec[0].len : 0;
|
|
2724
|
-
const tipP = tv().copy(base).addScaledVector(d, cfg.tailLen + maxLen);
|
|
2725
|
-
const gT = this.terrainH(tipP.x, tipP.z) + 0.006 * s;
|
|
2726
|
-
const need = gT - tipP.y;
|
|
2727
|
-
const kr = 0.01 * s;
|
|
2728
|
-
if (need > -kr) {
|
|
2729
|
-
// smooth ramp: 0 below -kr, need above +kr
|
|
2730
|
-
const lift = need >= kr ? need : (need + kr) * (need + kr) / (4 * kr);
|
|
2731
|
-
const ang = Math.asin(clamp(lift / (cfg.tailLen + maxLen), 0, 1));
|
|
2732
|
-
d.applyAxisAngle(lat, ang); up.applyAxisAngle(lat, ang);
|
|
2733
|
-
}
|
|
2734
|
-
this.setBoneC(this.ix.tail, base, d, lat);
|
|
2735
|
-
if (!this.fullFrame) { for (let sI = 0; sI < 2; sI++) this.follow(this.ix.tail, this.ix.rec[sI]); return; }
|
|
2736
|
-
// rectrices
|
|
2737
|
-
const tipJ = tv().copy(base).addScaledVector(d, cfg.tailLen);
|
|
2738
|
-
const fw = this._fw, ff = this.ff;
|
|
2739
|
-
fw.P0.x = tipJ.x; fw.P0.y = tipJ.y; fw.P0.z = tipJ.z;
|
|
2740
|
-
const sk = this.lod === 2 && (this.frame & 1) === 1;
|
|
2741
|
-
for (let sI = 0; sI < 2; sI++) {
|
|
2742
|
-
const side = sI === 0 ? 1 : -1, SS = LEGS[sI];
|
|
2743
|
-
for (const f of cfg.rec) {
|
|
2744
|
-
const a = f.fold + (f.fan - f.fold) * tf;
|
|
2745
|
-
rectrixFrame(ff, fw.P0, d, up, lat, side, f, a);
|
|
2746
|
-
this.featherBone(this.ix.rec[sI][f.i], ff, f.len, sk, f.arc, side, f);
|
|
2747
|
-
}
|
|
2748
|
-
if (this.lod === 2) this.store(this.ix.tail, this.ix.rec[sI]);
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
2751
|
-
|
|
2752
|
-
// ----------------------------------------------------------------- neck & head
|
|
2753
|
-
poseHead(dt, air) {
|
|
2754
|
-
const cfg = this.cfg, P = this.P, s = cfg.s, g = this.gait, inp = this.input;
|
|
2755
|
-
const qC = this.qChest;
|
|
2756
|
-
const base = this.B(cfg.neckBaseLocal, tv());
|
|
2757
|
-
// ---- head position target: carried by the chest (rest pose), extended in flight
|
|
2758
|
-
const occRest = tv().copy(cfg.occLocal).sub(cfg.neckBaseLocal).applyQuaternion(qC).add(base);
|
|
2759
|
-
const fwd = this.F;
|
|
2760
|
-
const ext = air ? 1 : P.neckExtend;
|
|
2761
|
-
// level the head carriage when the body tilts (the neck compensates body pitch)
|
|
2762
|
-
const bodyPitch = this.pitchS.x;
|
|
2763
|
-
const target = tv().copy(occRest);
|
|
2764
|
-
const lvl = tv().copy(cfg.occLocal).sub(cfg.neckBaseLocal).applyQuaternion(this.qH).add(base);
|
|
2765
|
-
target.lerp(lvl, 0.5 * clamp(-bodyPitch, 0, 1.2) / 1.2);
|
|
2766
|
-
if (air) {
|
|
2767
|
-
// flight: head forward of the chest, in line with the body
|
|
2768
|
-
// (the neck keeps close to its resting shape on the chest - pitched forward with the body -
|
|
2769
|
-
// pushed a little further forward, the head levelled by the atlas joint)
|
|
2770
|
-
target.copy(occRest).addScaledVector(fwd, cfg.neckLen * 0.04).addScaledVector(UP, -this.vy * 0.002);
|
|
2771
|
-
}
|
|
2772
|
-
// flight.shape: the head carried forward along the (levelled) body axis and lowered on an extended
|
|
2773
|
-
// neck, eased with the flight weight
|
|
2774
|
-
if (this.shapeW > 0) {
|
|
2775
|
-
const sh = cfg.shape, w = this.shapeW * cfg.neckArc;
|
|
2776
|
-
target.addScaledVector(tv().copy(sh.ax).applyQuaternion(qC), sh.reach * w).addScaledVector(tv().copy(sh.vent).applyQuaternion(qC), sh.drop * w);
|
|
2777
|
-
}
|
|
2778
|
-
target.addScaledVector(fwd, (P.headFwd + P.neckReach) * s).addScaledVector(UP, P.headUp * s).addScaledVector(this.Lf, P.headSide * s);
|
|
2779
|
-
// ---- head bob (walking): hold still in space, then thrust forward once per step
|
|
2780
|
-
const moving = this.mode === 'ground' && this.speed > 0.03 * cfg.sq && !air;
|
|
2781
|
-
const bobW = cfg.head.bob * g.headBob * smooth(0.03, 0.2, this.speed / cfg.sq) * P.gaitW * (moving ? 1 : 0) * (1 - P.headW);
|
|
2782
|
-
let bobOff = 0;
|
|
2783
|
-
if (bobW > 1e-3) {
|
|
2784
|
-
const stepP = wrap01(2 * (this.phase - g.off[0]) - 0.15);
|
|
2785
|
-
const hold = cfg.head.bobHold ?? 0.5;
|
|
2786
|
-
const stepLen = this.speed / Math.max(2 * g.f, 0.5);
|
|
2787
|
-
const q = clamp((stepP - hold) / (1 - hold), 0, 1);
|
|
2788
|
-
bobOff = stepLen * (smoother(q) - stepP + hold * 0.5) * bobW * 0.75;
|
|
2789
|
-
}
|
|
2790
|
-
target.addScaledVector(fwd, bobOff);
|
|
2791
|
-
// the neck turns with the gaze: part of the head's yaw / pitch swings the head position about the
|
|
2792
|
-
// neck base, so the rotation is spread along the neck instead of all at the skull
|
|
2793
|
-
{
|
|
2794
|
-
const gy = this.gazeS.yaw.x * NECK_GAZE_YAW, gp = this.gazeS.pitch.x * NECK_GAZE_PITCH;
|
|
2795
|
-
if (Math.abs(gy) + Math.abs(gp) > 1e-4) {
|
|
2796
|
-
const off = tv().subVectors(target, base);
|
|
2797
|
-
if (gp) off.applyAxisAngle(this.Lf, gp);
|
|
2798
|
-
if (gy) off.applyAxisAngle(UP, gy);
|
|
2799
|
-
target.copy(base).add(off);
|
|
2800
|
-
}
|
|
2801
|
-
}
|
|
2802
|
-
// posture target (eat, drink, preen, sleep...)
|
|
2803
|
-
// (blended about the neck base: direction and distance separately, so the head swings on an arc
|
|
2804
|
-
// instead of passing through the neck)
|
|
2805
|
-
if (P.headW > 0.001) {
|
|
2806
|
-
const a = tv().subVectors(target, base), b = tv().subVectors(P.headPos, base);
|
|
2807
|
-
const la = a.length(), lb = b.length();
|
|
2808
|
-
if (la > 1e-6 && lb > 1e-6 && P.headW < 0.999) {
|
|
2809
|
-
a.multiplyScalar(1 / la); b.multiplyScalar(1 / lb);
|
|
2810
|
-
const q = tq().setFromUnitVectors(a, b);
|
|
2811
|
-
a.applyQuaternion(tq().identity().slerp(q, P.headW));
|
|
2812
|
-
target.copy(base).addScaledVector(a, lerp(la, lb, P.headW));
|
|
2813
|
-
} else target.lerp(P.headPos, P.headW);
|
|
2814
|
-
}
|
|
2815
|
-
// ---- smoothing (not for the bob component, which must be exact)
|
|
2816
|
-
const hs = this.headS;
|
|
2817
|
-
if (!hs.init) { hs.x.reset(target.x); hs.y.reset(target.y); hs.z.reset(target.z); hs.init = true; }
|
|
2818
|
-
const om = (air ? 14 : 22) * P.headOmega;
|
|
2819
|
-
// filter relative to the body so locomotion does not lag
|
|
2820
|
-
const rx = hs.x.step(target.x - this.body.x, om, dt) + this.body.x;
|
|
2821
|
-
const ry = hs.y.step(target.y - this.body.y, om, dt) + this.body.y;
|
|
2822
|
-
const rz = hs.z.step(target.z - this.body.z, om, dt) + this.body.z;
|
|
2823
|
-
const occT = tv(rx, ry, rz);
|
|
2824
|
-
// ---- head orientation: gaze (saccades toward the look target), cocking, posture overrides
|
|
2825
|
-
const gz = this.gaze, lk = inp.look || P.look;
|
|
2826
|
-
let yawT = 0, pitchT = 0, rollT = 0;
|
|
2827
|
-
if (lk && P.lookW > 0.01) {
|
|
2828
|
-
const to = tv().subVectors(lk, occT);
|
|
2829
|
-
const want = Math.atan2(to.x, to.z);
|
|
2830
|
-
const hd = cfg.head;
|
|
2831
|
-
// a target (almost) straight below or at the head gives no stable direction: fade the look out
|
|
2832
|
-
// (and one behind, past the head's yaw range, would flip sides at +-180 deg)
|
|
2833
|
-
const ad = angDiff(this.heading, want);
|
|
2834
|
-
const lw = P.lookW * smooth(0.25, 0.6, Math.hypot(to.x, to.z) / Math.max(1e-4, cfg.neckLen)) * (1 - smooth(hd.yaw, hd.yaw + 0.7, Math.abs(ad)));
|
|
2835
|
-
yawT = clamp(ad, -hd.yaw, hd.yaw) * lw;
|
|
2836
|
-
pitchT = clamp(-Math.atan2(to.y, Math.hypot(to.x, to.z)), -hd.pitchUp, hd.pitchDown) * lw;
|
|
2837
|
-
rollT = P.cock * cfg.head.cock * P.lookW;
|
|
2838
|
-
this.lookTarget.copy(lk); this.state.lookTarget = this.lookTarget;
|
|
2839
|
-
} else this.state.lookTarget = null;
|
|
2840
|
-
// the head leads a turn: it turns toward the wanted heading at once (a saccade, with part of the
|
|
2841
|
-
// neck) and the body comes round under it (it used to trail the body's yaw rate, swung round last)
|
|
2842
|
-
const lead = inp.follow ? 0 : angDiff(this.heading, this.wantHeading ?? this.heading);
|
|
2843
|
-
yawT = clamp(yawT + clamp(lead * (air ? 0.4 : 0.75), -1.1, 1.1) * (1 - P.headW), -cfg.head.yaw, cfg.head.yaw) + clamp(this.yawRate * 0.06, -0.2, 0.2) + P.headYaw;
|
|
2844
|
-
// in flight the head follows part of the body's pitch away from level flight (flare, climb)
|
|
2845
|
-
pitchT += P.headPitch - (air ? (bodyPitch + cfg.tilt) * 0.5 : 0);
|
|
2846
|
-
rollT += P.headRoll;
|
|
2847
|
-
if (air) rollT += this.bankS.x * 0.2; // the head stays nearly level in banked turns (gaze stabilised)
|
|
2848
|
-
// saccades: the gaze jumps (fast) when the target moved far enough, and holds otherwise
|
|
2849
|
-
gz.hold -= dt;
|
|
2850
|
-
const sacc = cfg.head.saccade * (1 - P.headW * 0.7);
|
|
2851
|
-
const err = Math.abs(yawT - gz.tyaw) + Math.abs(pitchT - gz.tpitch) + Math.abs(rollT - gz.troll);
|
|
2852
|
-
if (sacc < 0.01 || err > 0.12 || gz.hold <= 0) { gz.tyaw = yawT; gz.tpitch = pitchT; gz.troll = rollT; gz.hold = 0.25 + this.rand() * 0.4; }
|
|
2853
|
-
const yT2 = lerp(yawT, gz.tyaw, sacc), pT2 = lerp(pitchT, gz.tpitch, sacc), rT2 = lerp(rollT, gz.troll, sacc);
|
|
2854
|
-
const gomg = lerp(9, 26, sacc) * P.headOmega;
|
|
2855
|
-
const yaw = this.gazeS.yaw.step(yT2, gomg, dt), hp = this.gazeS.pitch.step(pT2, gomg, dt), hr = this.gazeS.roll.step(rT2, gomg, dt);
|
|
2856
|
-
// head frame: heading + yaw, pitch (+ = bill down), roll; blended with the chest when limp
|
|
2857
|
-
const qHead = tq().setFromAxisAngle(UP, this.heading + yaw).multiply(tqa(X1, hp)).multiply(tqa(Z1, hr));
|
|
2858
|
-
if (P.headLimp > 0) qHead.slerp(tqc(qC).multiply(tqa(X1, 0.3)), P.headLimp);
|
|
2859
|
-
// ---- neck: Bezier S-curve with the neck's length from the chest to the head
|
|
2860
|
-
let occ = this.solveNeck(base, occT, qHead, qC);
|
|
2861
|
-
// the skull never enters the ground: raise the head target and solve again
|
|
2862
|
-
const rH = cfg.headR;
|
|
2863
|
-
// (the highest terrain under the skull's footprint: on a slope the downhill side of the head)
|
|
2864
|
-
let gO = this.terrainH(occ.x, occ.z);
|
|
2865
|
-
for (let k = 0; k < 4; k++) gO = Math.max(gO, this.terrainH(occ.x + (k === 0 ? rH : k === 1 ? -rH : 0), occ.z + (k === 2 ? rH : k === 3 ? -rH : 0)));
|
|
2866
|
-
gO += rH;
|
|
2867
|
-
if (occ.y < gO) { occT.y += gO - occ.y; occ = this.solveNeck(base, occT, qHead, qC); }
|
|
2868
|
-
// nor does the rest of the head's skin: swing the head up about the occiput just enough. The points
|
|
2869
|
-
// are the head's skin support (measured on the mesh, see headSupport: bill, mandible, wattles, comb,
|
|
2870
|
-
// throat, crown) or, without it, the bill tip, mouth, chin and both jaw corners. Each point is
|
|
2871
|
-
// lifted in the vertical plane through it (the most direct lift, whatever the head's roll), with a
|
|
2872
|
-
// soft onset, one after the other (each rotation is continuous, so their product is): the old pitch
|
|
2873
|
-
// about the head's own lateral axis had two roots and jumped between them when that axis stood near
|
|
2874
|
-
// vertical (a dead bird's head on its side)
|
|
2875
|
-
const hsup = this._headSup, rr = hsup ? hsup.rMax * 1.15 : 0;
|
|
2876
|
-
// (the head high above the ground - most frames - is told by the skull footprint's terrain already
|
|
2877
|
-
// sampled, with room for a slope over the head's size; only near it is the terrain sampled wider)
|
|
2878
|
-
const nearHead = !hsup || (occ.y - rr < gO - rH + 0.5 * rr + 0.01 * s &&
|
|
2879
|
-
occ.y - rr < Math.max(gO - rH, this.terrainH(occ.x + rr, occ.z), this.terrainH(occ.x - rr, occ.z), this.terrainH(occ.x, occ.z + rr), this.terrainH(occ.x, occ.z - rr)) + 0.01 * s);
|
|
2880
|
-
if (nearHead) {
|
|
2881
|
-
const kr = 0.003 * s, clr = hsup ? 0.001 * s : 0.0015 * s, slack = kr + 0.004 * s + 0.12 * rr;
|
|
2882
|
-
const n0 = this.terrainN(occ.x, occ.z, tv()), gx = n0.x / n0.y, gz = n0.z / n0.y, h0 = this.terrainH(occ.x, occ.z);
|
|
2883
|
-
const v = tv(), o3 = this._hsOut || (this._hsOut = new Float64Array(3));
|
|
2884
|
-
const ih = this.ix.head, ij = cfg.hasJaw ? this.ix.jaw : -1;
|
|
2885
|
-
const qJ = hsup && ij >= 0 ? tq() : null, hinge = qJ ? tv() : null;
|
|
2886
|
-
// (the other bones the head's skin blends with - the neck, as solved this frame - then the head
|
|
2887
|
-
// and the mandible as they are posed now)
|
|
2888
|
-
if (hsup) for (const i of hsup.bones) if (i !== ih && i !== ij) this._supBone(i, this.sk.bones[i].matrixWorld);
|
|
2889
|
-
const setHead = () => {
|
|
2890
|
-
this._supBoneQ(ih, tqc(qHead).multiply(this.bindQ[ih]), occ);
|
|
2891
|
-
if (qJ) {
|
|
2892
|
-
hinge.copy(this.J.jawHinge).sub(this.J.occiput).applyQuaternion(qHead).add(occ);
|
|
2893
|
-
qJ.copy(qHead).multiply(tqa(X1, this.jaw.x)).multiply(this.bindQ[ij]);
|
|
2894
|
-
this._supBoneQ(ij, qJ, hinge);
|
|
2895
|
-
}
|
|
2896
|
-
};
|
|
2897
|
-
const nP = hsup ? hsup.n : 5;
|
|
2898
|
-
for (let pass = 0; pass < 2; pass++) {
|
|
2899
|
-
if (hsup) setHead();
|
|
2900
|
-
for (let k = 0; k < nP; k++) {
|
|
2901
|
-
let wH = 1;
|
|
2902
|
-
if (hsup) {
|
|
2903
|
-
this._supSkin(k, o3, hsup);
|
|
2904
|
-
v.set(o3[0] - occ.x, o3[1] - occ.y, o3[2] - occ.z);
|
|
2905
|
-
// (terrain tangent plane prefilter; the exact test for the points near it)
|
|
2906
|
-
if (h0 - gx * v.x - gz * v.z + clr - occ.y - v.y <= -slack) continue;
|
|
2907
|
-
// (a vertex the neck shares moves with the head by its head weight only)
|
|
2908
|
-
wH = Math.max(0.3, hsup.wH[k]);
|
|
2909
|
-
} else {
|
|
2910
|
-
v.copy(k === 0 ? cfg.billLocal : k === 1 ? cfg.mouthLocal : cfg.chinLocal);
|
|
2911
|
-
if (k >= 3) v.x += (k === 3 ? 1 : -1) * cfg.headR * 0.8;
|
|
2912
|
-
v.applyQuaternion(qHead);
|
|
2913
|
-
}
|
|
2914
|
-
const pen = this.terrainH(occ.x + v.x, occ.z + v.z) + clr - occ.y - v.y;
|
|
2915
|
-
if (pen <= -kr) continue;
|
|
2916
|
-
const lift = (pen >= kr ? pen : (pen + kr) * (pen + kr) / (4 * kr)) / wH;
|
|
2917
|
-
const r = v.length(), h = Math.hypot(v.x, v.z);
|
|
2918
|
-
if (r < 1e-6) continue;
|
|
2919
|
-
const th = Math.asin(clamp((v.y + lift) / r, -1, 1)) - Math.asin(clamp(v.y / r, -1, 1));
|
|
2920
|
-
if (th <= 1e-7) continue;
|
|
2921
|
-
// axis: horizontal, across the point's direction; a point (almost) straight below the occiput
|
|
2922
|
-
// falls back on the head's lateral axis (continuous: the blend weight grows as h vanishes)
|
|
2923
|
-
const ax = tv(-v.z, 0, v.x);
|
|
2924
|
-
const lw = Math.max(0, 0.25 * r - h);
|
|
2925
|
-
if (lw > 0) { const hl = tv(1, 0, 0).applyQuaternion(qHead); hl.y = 0; ax.addScaledVector(hl, -lw * 4); } // (nose up: about the head's right)
|
|
2926
|
-
if (ax.lengthSq() < 1e-12) continue;
|
|
2927
|
-
ax.normalize();
|
|
2928
|
-
qHead.premultiply(tqa(ax, Math.min(th, 1.2)));
|
|
2929
|
-
if (hsup) setHead();
|
|
2930
|
-
}
|
|
2931
|
-
}
|
|
2932
|
-
}
|
|
2933
|
-
this.setBoneQ('head', occ, qHead);
|
|
2934
|
-
this.headPose.position.copy(occ);
|
|
2935
|
-
this.headPose.quaternion.copy(qHead);
|
|
2936
|
-
// jaw (lower mandible): peck, caw, pant
|
|
2937
|
-
const ja = this.jaw.step(P.jaw + (cfg.jawRest || 0) + (air ? 0.02 * this.fl.power : 0), 30, dt);
|
|
2938
|
-
if (cfg.hasJaw) {
|
|
2939
|
-
const hinge = tv().copy(this.J.jawHinge).sub(this.J.occiput).applyQuaternion(qHead).add(occ);
|
|
2940
|
-
this.setBoneQ('jaw', hinge, tqc(qHead).multiply(tqa(X1, ja)));
|
|
2941
|
-
}
|
|
2942
|
-
}
|
|
2943
|
-
|
|
2944
|
-
// neck chain from `base` to (near) `occT`; returns the actual occiput position (temp)
|
|
2945
|
-
solveNeck(base, occT, qHead, qC) {
|
|
2946
|
-
const cfg = this.cfg, N = cfg.neckN, nk = this._neck;
|
|
2947
|
-
const Ltot = cfg.neckArc;
|
|
2948
|
-
// clamp the target distance softly to what the neck can reach
|
|
2949
|
-
const dvec = tv().subVectors(occT, base);
|
|
2950
|
-
let dist = dvec.length();
|
|
2951
|
-
const maxD = Math.max(cfg.neckChord0 * 1.08, Ltot * 0.94), minD = Ltot * 0.5;
|
|
2952
|
-
if (dist > maxD * 0.92) { const soft = maxD * 0.92; dist = soft + (maxD - soft) * (1 - Math.exp(-(dist - soft) / (maxD - soft))); }
|
|
2953
|
-
dist = Math.max(dist, minD);
|
|
2954
|
-
dvec.normalize();
|
|
2955
|
-
const end = tv().copy(base).addScaledVector(dvec, dist);
|
|
2956
|
-
const t0 = tv().copy(cfg.neckT0).applyQuaternion(qC).normalize();
|
|
2957
|
-
const t1 = tv().copy(cfg.neckT1).applyQuaternion(qHead).normalize();
|
|
2958
|
-
// a head tilted far back / down must not tie the neck in a loop: keep both tangents within
|
|
2959
|
-
// ~80 deg of the chord
|
|
2960
|
-
for (const t of [t0, t1]) {
|
|
2961
|
-
const c = t.dot(dvec);
|
|
2962
|
-
if (c < 0.2) t.addScaledVector(dvec, 0.2 - c).normalize();
|
|
2963
|
-
}
|
|
2964
|
-
// the neck telescopes: a short chord is taken up partly by folding (S-curve) and partly by the
|
|
2965
|
-
// (feathered) neck shortening, so the segments never kink against each other
|
|
2966
|
-
// (at the bind chord the arc is the bind arc: the rest pose reproduces the bind neck)
|
|
2967
|
-
const Ld = Ltot * clamp(0.62 + 0.38 * dist / cfg.neckChord0, 0.74, 1);
|
|
2968
|
-
// find tangent length k so the curve's arc length equals Ld (bisection)
|
|
2969
|
-
const P = nk.P, pts = nk.pts, S = nk.s, M = pts.length - 1;
|
|
2970
|
-
const arc = (kk) => {
|
|
2971
|
-
P[0].copy(base); P[1].copy(base).addScaledVector(t0, kk); P[2].copy(end).addScaledVector(t1, -kk); P[3].copy(end);
|
|
2972
|
-
let L = 0;
|
|
2973
|
-
for (let i = 0; i <= M; i++) {
|
|
2974
|
-
const u = i / M, a = 1 - u;
|
|
2975
|
-
const b0 = a * a * a, b1 = 3 * a * a * u, b2 = 3 * a * u * u, b3 = u * u * u;
|
|
2976
|
-
pts[i].set(
|
|
2977
|
-
b0 * P[0].x + b1 * P[1].x + b2 * P[2].x + b3 * P[3].x,
|
|
2978
|
-
b0 * P[0].y + b1 * P[1].y + b2 * P[2].y + b3 * P[3].y,
|
|
2979
|
-
b0 * P[0].z + b1 * P[1].z + b2 * P[2].z + b3 * P[3].z);
|
|
2980
|
-
if (i) L += pts[i].distanceTo(pts[i - 1]);
|
|
2981
|
-
S[i] = L;
|
|
2982
|
-
}
|
|
2983
|
-
return L;
|
|
2984
|
-
};
|
|
2985
|
-
// arc length grows monotonically with k: bisection (a continuous function of the targets)
|
|
2986
|
-
// (a short chord - the head drawn in close to the neck base: preening the breast, tearing food under
|
|
2987
|
-
// the feet, sleeping - is taken up by the neck telescoping, not by a loop: the tangents are capped
|
|
2988
|
-
// at 0.6 of the chord (at least the bind neck's own), where longer ones folded the curve into a cusp
|
|
2989
|
-
// - an eagle's neck bent 121 deg at one joint, its segment squashed to 37 %)
|
|
2990
|
-
let lo = 0, hi = Math.min(Ltot * 1.6, Math.max(0.6, 1.05 * cfg.neckK0) * dist);
|
|
2991
|
-
if (arc(hi) <= Ld) lo = hi;
|
|
2992
|
-
else {
|
|
2993
|
-
const iters = this.lod === 2 ? 6 : 9;
|
|
2994
|
-
for (let it = 0; it < iters; it++) {
|
|
2995
|
-
const mid = 0.5 * (lo + hi);
|
|
2996
|
-
if (arc(mid) > Ld) hi = mid; else lo = mid;
|
|
2997
|
-
}
|
|
2998
|
-
}
|
|
2999
|
-
arc(0.5 * (lo + hi));
|
|
3000
|
-
// joints at equal fractions of the arc (scaled to the segment lengths)
|
|
3001
|
-
const Jn = nk.J;
|
|
3002
|
-
Jn[0].copy(base);
|
|
3003
|
-
const Ltot2 = S[M];
|
|
3004
|
-
let acc = 0, j = 1;
|
|
3005
|
-
for (let i = 0; i < N; i++) {
|
|
3006
|
-
acc = (Ltot2 * (i + 1)) / N;
|
|
3007
|
-
while (j < M && S[j] < acc) j++;
|
|
3008
|
-
const f = (acc - S[j - 1]) / Math.max(1e-9, S[j] - S[j - 1]);
|
|
3009
|
-
Jn[i + 1].copy(pts[j - 1]).lerp(pts[j], clamp(f, 0, 1));
|
|
3010
|
-
}
|
|
3011
|
-
// bones with twist distributed from the chest to the head
|
|
3012
|
-
// (parallel transport of the chest's lateral axis along the chain, then the remaining twist to
|
|
3013
|
-
// the head spread evenly: no degenerate or flipping frames whatever the head does)
|
|
3014
|
-
const dirs = nk.dirs || (nk.dirs = Array.from({ length: N }, () => new THREE.Vector3()));
|
|
3015
|
-
const lats = nk.lats || (nk.lats = Array.from({ length: N }, () => new THREE.Vector3()));
|
|
3016
|
-
const scl = nk.scl || (nk.scl = new Float64Array(N));
|
|
3017
|
-
let dp = tv().copy(cfg.neckSeg0).applyQuaternion(qC).normalize();
|
|
3018
|
-
const lp = tv(1, 0, 0).applyQuaternion(qC);
|
|
3019
|
-
const qr = tq();
|
|
3020
|
-
for (let i = 0; i < N; i++) {
|
|
3021
|
-
const d = dirs[i].subVectors(Jn[i + 1], Jn[i]);
|
|
3022
|
-
const len = d.length();
|
|
3023
|
-
if (len > 1e-9) d.multiplyScalar(1 / len); else d.copy(dp);
|
|
3024
|
-
scl[i] = len / cfg.neckL[i];
|
|
3025
|
-
qr.setFromUnitVectors(dp, d);
|
|
3026
|
-
lp.applyQuaternion(qr);
|
|
3027
|
-
lp.addScaledVector(d, -lp.dot(d)).normalize();
|
|
3028
|
-
lats[i].copy(lp);
|
|
3029
|
-
dp = d;
|
|
3030
|
-
}
|
|
3031
|
-
const dl = dirs[N - 1];
|
|
3032
|
-
const hl = tv(1, 0, 0).applyQuaternion(qHead);
|
|
3033
|
-
hl.addScaledVector(dl, -hl.dot(dl));
|
|
3034
|
-
let phi = 0;
|
|
3035
|
-
if (hl.lengthSq() > 1e-8) { hl.normalize(); phi = Math.atan2(tv().crossVectors(lats[N - 1], hl).dot(dl), clamp(lats[N - 1].dot(hl), -1, 1)); }
|
|
3036
|
-
for (let i = 0; i < N; i++) {
|
|
3037
|
-
const lat = lats[i].applyAxisAngle(dirs[i], phi * (i + 1) / (N + 1));
|
|
3038
|
-
const sc = scl[i], th = Math.pow(sc, -0.35) * (this.shapeW > 0 ? 1 - cfg.shape.neck * this.shapeW : 1);
|
|
3039
|
-
this.setBoneC(this.ix.neck[i], Jn[i], dirs[i], lat, tv(th, sc, th));
|
|
3040
|
-
}
|
|
3041
|
-
return Jn[N];
|
|
3042
|
-
}
|
|
3043
|
-
|
|
3044
|
-
// ----------------------------------------------------------------- state
|
|
3045
|
-
publish() {
|
|
3046
|
-
const st = this.state, P = this.P;
|
|
3047
|
-
st.heading = this.heading;
|
|
3048
|
-
st.speed = this.speed;
|
|
3049
|
-
st.mode = this.mode;
|
|
3050
|
-
const air = this.mode === 'air' || this.mode === 'landing' || (this.mode === 'takeoff' && this.to && this.to.air);
|
|
3051
|
-
st.grounded = !air;
|
|
3052
|
-
st.gait = air ? (this.fl.glide > 0.6 ? 'glide' : 'fly') : st.stats.gait;
|
|
3053
|
-
st.altitude = air ? Math.max(0, this.altitude()) : 0;
|
|
3054
|
-
st.eyelid = P.eyelid;
|
|
3055
|
-
st.action = this.actionsLayer.current();
|
|
3056
|
-
st.posture = this.actionsLayer.posture;
|
|
3057
|
-
for (let i = 0; i < 2; i++) {
|
|
3058
|
-
const leg = this.legs[i], o = st.legs[i];
|
|
3059
|
-
o.stance = this.mode === 'ground' && leg.state === 'stance' && !!leg.planted && P.legGait > 0.5 && P.sit < 0.05 && this.airLeg.x < 0.01 && P.scratchW[i] < 0.02;
|
|
3060
|
-
o.contact.copy(leg.contact);
|
|
3061
|
-
}
|
|
3062
|
-
}
|
|
3063
|
-
}
|