@bitefoundry/retrieviewer 0.0.3 → 0.0.5

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.
Files changed (263) hide show
  1. package/ASSET-PROVENANCE.md +11 -0
  2. package/ASSETS-LICENSE.md +2 -1
  3. package/ATTRIBUTION.md +6 -1
  4. package/COMFORT-ART.md +95 -0
  5. package/DESKTOP-PLATFORMS.md +6 -1
  6. package/INSTALLING.md +4 -0
  7. package/PLUGIN.md +13 -10
  8. package/PREREQUISITES.md +95 -0
  9. package/PUPPY-MEMORY.md +102 -1
  10. package/README.ko.md +116 -0
  11. package/README.md +121 -59
  12. package/RELEASING.md +33 -13
  13. package/THIRD-PARTY-NOTICES.md +5 -3
  14. package/braille.mjs +35 -9
  15. package/examples/live.mjs +2 -2
  16. package/integrations/linux-overlay/overlay.py +8 -2
  17. package/integrations/macos-overlay/overlay.jxa +9 -7
  18. package/integrations/opencode/runtime.mjs +4 -4
  19. package/integrations/opencode/tui.tsx +1 -1
  20. package/integrations/windows-overlay/Overlay.cs +7 -6
  21. package/integrations/windows-overlay/desktop.mjs +2 -1
  22. package/integrations/windows-overlay/protocol.mjs +13 -7
  23. package/integrations/windows-overlay/worker.mjs +1 -1
  24. package/package.json +34 -13
  25. package/puppy-comfort-sprites.mjs +33 -0
  26. package/puppy-companion-comfort-q5.bin.gz +0 -0
  27. package/puppy-companion-comfort.json +140 -0
  28. package/puppy-companion-life.mjs +58 -18
  29. package/puppy-companion-objects.mjs +12 -0
  30. package/puppy-companion-sprites.mjs +4 -2
  31. package/puppy-frame-predictive.mjs +15 -9
  32. package/puppy-frame-store.mjs +2 -2
  33. package/puppy-paper-actions.mjs +4 -2
  34. package/puppy-paper-behaviors.mjs +1 -1
  35. package/puppy-paper-contact.mjs +4 -2
  36. package/puppy-paper-seat.mjs +4 -2
  37. package/puppy-paper-world.mjs +14 -0
  38. package/puppy-paper.mjs +79 -21
  39. package/retrieviewer-cli.mjs +8 -6
  40. package/retrieviewer-plugin.d.mts +1 -1
  41. package/retrieviewer-plugin.mjs +1 -1
  42. package/retrieviewer.d.mts +12 -4
  43. package/retrieviewer.mjs +20 -14
  44. package/locomotion-core.mjs +0 -21
  45. package/locomotion-puppy.animal +0 -0
  46. package/vendor/procedural-animals/package.json +0 -40
  47. package/vendor/procedural-animals/src/animal.js +0 -146
  48. package/vendor/procedural-animals/src/bake.js +0 -51
  49. package/vendor/procedural-animals/src/build.js +0 -81
  50. package/vendor/procedural-animals/src/core/build/coatKit.js +0 -121
  51. package/vendor/procedural-animals/src/core/build/featherCards.js +0 -189
  52. package/vendor/procedural-animals/src/core/build/finEdges.js +0 -85
  53. package/vendor/procedural-animals/src/core/build/furSlope.js +0 -115
  54. package/vendor/procedural-animals/src/core/build/mesh.js +0 -67
  55. package/vendor/procedural-animals/src/core/build/pipeline.js +0 -272
  56. package/vendor/procedural-animals/src/core/build/seams.js +0 -183
  57. package/vendor/procedural-animals/src/core/build/warp.js +0 -112
  58. package/vendor/procedural-animals/src/core/build/weights.js +0 -393
  59. package/vendor/procedural-animals/src/core/math/vec.js +0 -81
  60. package/vendor/procedural-animals/src/core/motion/actions.js +0 -800
  61. package/vendor/procedural-animals/src/core/motion/bird.js +0 -3063
  62. package/vendor/procedural-animals/src/core/motion/birdActions.js +0 -1059
  63. package/vendor/procedural-animals/src/core/motion/common.js +0 -158
  64. package/vendor/procedural-animals/src/core/motion/index.js +0 -22
  65. package/vendor/procedural-animals/src/core/motion/quadruped.js +0 -2875
  66. package/vendor/procedural-animals/src/core/motion/snake.js +0 -1594
  67. package/vendor/procedural-animals/src/core/motion/spider.js +0 -1810
  68. package/vendor/procedural-animals/src/core/motion/swimmer.js +0 -1047
  69. package/vendor/procedural-animals/src/core/motion/swimmerActions.js +0 -613
  70. package/vendor/procedural-animals/src/core/render/animalObject.js +0 -262
  71. package/vendor/procedural-animals/src/core/render/coatMaterial.js +0 -1301
  72. package/vendor/procedural-animals/src/core/render/dqs.js +0 -159
  73. package/vendor/procedural-animals/src/core/render/eyes.js +0 -676
  74. package/vendor/procedural-animals/src/core/render/membranes.js +0 -77
  75. package/vendor/procedural-animals/src/core/render/skeleton.js +0 -41
  76. package/vendor/procedural-animals/src/core/rig/bird.js +0 -327
  77. package/vendor/procedural-animals/src/core/rig/quadruped.js +0 -76
  78. package/vendor/procedural-animals/src/core/rig/rig.js +0 -79
  79. package/vendor/procedural-animals/src/core/rig/snake.js +0 -76
  80. package/vendor/procedural-animals/src/core/rig/spider.js +0 -110
  81. package/vendor/procedural-animals/src/core/rig/swimmer.js +0 -181
  82. package/vendor/procedural-animals/src/core/sdf/eyeSocket.js +0 -53
  83. package/vendor/procedural-animals/src/core/sdf/mesher.js +0 -238
  84. package/vendor/procedural-animals/src/core/sdf/sdf.js +0 -263
  85. package/vendor/procedural-animals/src/index.d.ts +0 -98
  86. package/vendor/procedural-animals/src/index.js +0 -29
  87. package/vendor/procedural-animals/src/registry.js +0 -48
  88. package/vendor/procedural-animals/src/species/bear/behaviour.js +0 -463
  89. package/vendor/procedural-animals/src/species/bear/claws.js +0 -147
  90. package/vendor/procedural-animals/src/species/bear/coat.js +0 -726
  91. package/vendor/procedural-animals/src/species/bear/index.js +0 -139
  92. package/vendor/procedural-animals/src/species/bear/motion.js +0 -112
  93. package/vendor/procedural-animals/src/species/bear/regions.js +0 -78
  94. package/vendor/procedural-animals/src/species/bear/rig.js +0 -112
  95. package/vendor/procedural-animals/src/species/bear/sculpt.js +0 -272
  96. package/vendor/procedural-animals/src/species/boar/behaviour.js +0 -335
  97. package/vendor/procedural-animals/src/species/boar/coat.js +0 -372
  98. package/vendor/procedural-animals/src/species/boar/index.js +0 -110
  99. package/vendor/procedural-animals/src/species/boar/motion.js +0 -85
  100. package/vendor/procedural-animals/src/species/boar/regions.js +0 -88
  101. package/vendor/procedural-animals/src/species/boar/rig.js +0 -111
  102. package/vendor/procedural-animals/src/species/boar/sculpt.js +0 -285
  103. package/vendor/procedural-animals/src/species/cat/behaviour.js +0 -559
  104. package/vendor/procedural-animals/src/species/cat/coat.js +0 -841
  105. package/vendor/procedural-animals/src/species/cat/index.js +0 -211
  106. package/vendor/procedural-animals/src/species/cat/motion.js +0 -101
  107. package/vendor/procedural-animals/src/species/cat/regions.js +0 -82
  108. package/vendor/procedural-animals/src/species/cat/rig.js +0 -100
  109. package/vendor/procedural-animals/src/species/cat/sculpt.js +0 -266
  110. package/vendor/procedural-animals/src/species/cat/whiskers.js +0 -110
  111. package/vendor/procedural-animals/src/species/cheetah/coat.js +0 -427
  112. package/vendor/procedural-animals/src/species/cheetah/index.js +0 -51
  113. package/vendor/procedural-animals/src/species/cheetah/joints.js +0 -3
  114. package/vendor/procedural-animals/src/species/cheetah/motion.js +0 -77
  115. package/vendor/procedural-animals/src/species/cheetah/regions.js +0 -95
  116. package/vendor/procedural-animals/src/species/cheetah/rig.js +0 -66
  117. package/vendor/procedural-animals/src/species/cheetah/sculpt.js +0 -158
  118. package/vendor/procedural-animals/src/species/chicken/coat.js +0 -313
  119. package/vendor/procedural-animals/src/species/chicken/coverts.js +0 -320
  120. package/vendor/procedural-animals/src/species/chicken/index.js +0 -110
  121. package/vendor/procedural-animals/src/species/chicken/motion.js +0 -177
  122. package/vendor/procedural-animals/src/species/chicken/regions.js +0 -66
  123. package/vendor/procedural-animals/src/species/chicken/rig.js +0 -144
  124. package/vendor/procedural-animals/src/species/chicken/sculpt.js +0 -389
  125. package/vendor/procedural-animals/src/species/chicken/wingLook.js +0 -66
  126. package/vendor/procedural-animals/src/species/chicken/wingStack.js +0 -124
  127. package/vendor/procedural-animals/src/species/cow/behaviour.js +0 -521
  128. package/vendor/procedural-animals/src/species/cow/coat.js +0 -465
  129. package/vendor/procedural-animals/src/species/cow/index.js +0 -118
  130. package/vendor/procedural-animals/src/species/cow/motion.js +0 -81
  131. package/vendor/procedural-animals/src/species/cow/regions.js +0 -98
  132. package/vendor/procedural-animals/src/species/cow/rig.js +0 -116
  133. package/vendor/procedural-animals/src/species/cow/sculpt.js +0 -339
  134. package/vendor/procedural-animals/src/species/crow/coat.js +0 -145
  135. package/vendor/procedural-animals/src/species/crow/index.js +0 -70
  136. package/vendor/procedural-animals/src/species/crow/joints.js +0 -3
  137. package/vendor/procedural-animals/src/species/crow/motion.js +0 -101
  138. package/vendor/procedural-animals/src/species/crow/regions.js +0 -50
  139. package/vendor/procedural-animals/src/species/crow/rig.js +0 -83
  140. package/vendor/procedural-animals/src/species/crow/sculpt.js +0 -118
  141. package/vendor/procedural-animals/src/species/deer/behaviour.js +0 -513
  142. package/vendor/procedural-animals/src/species/deer/coat.js +0 -391
  143. package/vendor/procedural-animals/src/species/deer/index.js +0 -106
  144. package/vendor/procedural-animals/src/species/deer/motion.js +0 -97
  145. package/vendor/procedural-animals/src/species/deer/regions.js +0 -60
  146. package/vendor/procedural-animals/src/species/deer/rig.js +0 -124
  147. package/vendor/procedural-animals/src/species/deer/sculpt.js +0 -297
  148. package/vendor/procedural-animals/src/species/dog/behaviour.js +0 -521
  149. package/vendor/procedural-animals/src/species/dog/coat.js +0 -495
  150. package/vendor/procedural-animals/src/species/dog/index.js +0 -144
  151. package/vendor/procedural-animals/src/species/dog/motion.js +0 -118
  152. package/vendor/procedural-animals/src/species/dog/regions.js +0 -61
  153. package/vendor/procedural-animals/src/species/dog/rig.js +0 -129
  154. package/vendor/procedural-animals/src/species/dog/sculpt.js +0 -246
  155. package/vendor/procedural-animals/src/species/eagle/coat.js +0 -250
  156. package/vendor/procedural-animals/src/species/eagle/index.js +0 -76
  157. package/vendor/procedural-animals/src/species/eagle/joints.js +0 -3
  158. package/vendor/procedural-animals/src/species/eagle/motion.js +0 -137
  159. package/vendor/procedural-animals/src/species/eagle/regions.js +0 -57
  160. package/vendor/procedural-animals/src/species/eagle/rig.js +0 -107
  161. package/vendor/procedural-animals/src/species/eagle/sculpt.js +0 -137
  162. package/vendor/procedural-animals/src/species/fish/coat.js +0 -271
  163. package/vendor/procedural-animals/src/species/fish/index.js +0 -88
  164. package/vendor/procedural-animals/src/species/fish/motion.js +0 -92
  165. package/vendor/procedural-animals/src/species/fish/regions.js +0 -27
  166. package/vendor/procedural-animals/src/species/fish/rig.js +0 -143
  167. package/vendor/procedural-animals/src/species/fish/sculpt.js +0 -214
  168. package/vendor/procedural-animals/src/species/fish/variants.js +0 -126
  169. package/vendor/procedural-animals/src/species/fox/behaviour.js +0 -314
  170. package/vendor/procedural-animals/src/species/fox/coat.js +0 -444
  171. package/vendor/procedural-animals/src/species/fox/index.js +0 -120
  172. package/vendor/procedural-animals/src/species/fox/motion.js +0 -81
  173. package/vendor/procedural-animals/src/species/fox/regions.js +0 -59
  174. package/vendor/procedural-animals/src/species/fox/rig.js +0 -91
  175. package/vendor/procedural-animals/src/species/fox/sculpt.js +0 -202
  176. package/vendor/procedural-animals/src/species/frog/behaviour.js +0 -590
  177. package/vendor/procedural-animals/src/species/frog/coat.js +0 -305
  178. package/vendor/procedural-animals/src/species/frog/index.js +0 -97
  179. package/vendor/procedural-animals/src/species/frog/motion.js +0 -69
  180. package/vendor/procedural-animals/src/species/frog/regions.js +0 -46
  181. package/vendor/procedural-animals/src/species/frog/rig.js +0 -156
  182. package/vendor/procedural-animals/src/species/frog/sculpt.js +0 -240
  183. package/vendor/procedural-animals/src/species/goat/behaviour.js +0 -509
  184. package/vendor/procedural-animals/src/species/goat/coat.js +0 -590
  185. package/vendor/procedural-animals/src/species/goat/index.js +0 -149
  186. package/vendor/procedural-animals/src/species/goat/motion.js +0 -95
  187. package/vendor/procedural-animals/src/species/goat/regions.js +0 -98
  188. package/vendor/procedural-animals/src/species/goat/rig.js +0 -181
  189. package/vendor/procedural-animals/src/species/goat/sculpt.js +0 -547
  190. package/vendor/procedural-animals/src/species/horse/coat.js +0 -361
  191. package/vendor/procedural-animals/src/species/horse/coatSeam.js +0 -122
  192. package/vendor/procedural-animals/src/species/horse/index.js +0 -80
  193. package/vendor/procedural-animals/src/species/horse/motion.js +0 -245
  194. package/vendor/procedural-animals/src/species/horse/regions.js +0 -60
  195. package/vendor/procedural-animals/src/species/horse/rig.js +0 -117
  196. package/vendor/procedural-animals/src/species/horse/sculpt.js +0 -316
  197. package/vendor/procedural-animals/src/species/horse/seamWeights.js +0 -272
  198. package/vendor/procedural-animals/src/species/lion/behaviour.js +0 -509
  199. package/vendor/procedural-animals/src/species/lion/coat.js +0 -1016
  200. package/vendor/procedural-animals/src/species/lion/index.js +0 -111
  201. package/vendor/procedural-animals/src/species/lion/motion.js +0 -87
  202. package/vendor/procedural-animals/src/species/lion/regions.js +0 -106
  203. package/vendor/procedural-animals/src/species/lion/rig.js +0 -110
  204. package/vendor/procedural-animals/src/species/lion/sculpt.js +0 -384
  205. package/vendor/procedural-animals/src/species/lion/whiskers.js +0 -132
  206. package/vendor/procedural-animals/src/species/pig/behaviour.js +0 -575
  207. package/vendor/procedural-animals/src/species/pig/coat.js +0 -730
  208. package/vendor/procedural-animals/src/species/pig/ears.js +0 -242
  209. package/vendor/procedural-animals/src/species/pig/index.js +0 -161
  210. package/vendor/procedural-animals/src/species/pig/motion.js +0 -105
  211. package/vendor/procedural-animals/src/species/pig/regions.js +0 -100
  212. package/vendor/procedural-animals/src/species/pig/rig.js +0 -152
  213. package/vendor/procedural-animals/src/species/pig/sculpt.js +0 -382
  214. package/vendor/procedural-animals/src/species/rabbit/behaviour.js +0 -605
  215. package/vendor/procedural-animals/src/species/rabbit/coat.js +0 -285
  216. package/vendor/procedural-animals/src/species/rabbit/index.js +0 -85
  217. package/vendor/procedural-animals/src/species/rabbit/motion.js +0 -85
  218. package/vendor/procedural-animals/src/species/rabbit/regions.js +0 -48
  219. package/vendor/procedural-animals/src/species/rabbit/rig.js +0 -99
  220. package/vendor/procedural-animals/src/species/rabbit/sculpt.js +0 -148
  221. package/vendor/procedural-animals/src/species/rat/behaviour.js +0 -539
  222. package/vendor/procedural-animals/src/species/rat/coat.js +0 -341
  223. package/vendor/procedural-animals/src/species/rat/index.js +0 -111
  224. package/vendor/procedural-animals/src/species/rat/motion.js +0 -90
  225. package/vendor/procedural-animals/src/species/rat/regions.js +0 -92
  226. package/vendor/procedural-animals/src/species/rat/rig.js +0 -112
  227. package/vendor/procedural-animals/src/species/rat/sculpt.js +0 -163
  228. package/vendor/procedural-animals/src/species/rat/whiskers.js +0 -119
  229. package/vendor/procedural-animals/src/species/shark/coat.js +0 -383
  230. package/vendor/procedural-animals/src/species/shark/index.js +0 -104
  231. package/vendor/procedural-animals/src/species/shark/motion.js +0 -69
  232. package/vendor/procedural-animals/src/species/shark/regions.js +0 -34
  233. package/vendor/procedural-animals/src/species/shark/rig.js +0 -194
  234. package/vendor/procedural-animals/src/species/shark/sculpt.js +0 -343
  235. package/vendor/procedural-animals/src/species/shark/variants.js +0 -105
  236. package/vendor/procedural-animals/src/species/sheep/behaviour.js +0 -522
  237. package/vendor/procedural-animals/src/species/sheep/coat.js +0 -529
  238. package/vendor/procedural-animals/src/species/sheep/index.js +0 -152
  239. package/vendor/procedural-animals/src/species/sheep/motion.js +0 -86
  240. package/vendor/procedural-animals/src/species/sheep/neck.js +0 -41
  241. package/vendor/procedural-animals/src/species/sheep/regions.js +0 -62
  242. package/vendor/procedural-animals/src/species/sheep/rig.js +0 -163
  243. package/vendor/procedural-animals/src/species/sheep/sculpt.js +0 -542
  244. package/vendor/procedural-animals/src/species/snake/coat.js +0 -315
  245. package/vendor/procedural-animals/src/species/snake/index.js +0 -73
  246. package/vendor/procedural-animals/src/species/snake/motion.js +0 -51
  247. package/vendor/procedural-animals/src/species/snake/regions.js +0 -51
  248. package/vendor/procedural-animals/src/species/snake/rig.js +0 -108
  249. package/vendor/procedural-animals/src/species/snake/sculpt.js +0 -122
  250. package/vendor/procedural-animals/src/species/spider/coat.js +0 -279
  251. package/vendor/procedural-animals/src/species/spider/index.js +0 -78
  252. package/vendor/procedural-animals/src/species/spider/motion.js +0 -72
  253. package/vendor/procedural-animals/src/species/spider/regions.js +0 -87
  254. package/vendor/procedural-animals/src/species/spider/rig.js +0 -118
  255. package/vendor/procedural-animals/src/species/spider/sculpt.js +0 -182
  256. package/vendor/procedural-animals/src/species/wolf/behaviour.js +0 -160
  257. package/vendor/procedural-animals/src/species/wolf/coat.js +0 -749
  258. package/vendor/procedural-animals/src/species/wolf/index.js +0 -107
  259. package/vendor/procedural-animals/src/species/wolf/motion.js +0 -80
  260. package/vendor/procedural-animals/src/species/wolf/regions.js +0 -57
  261. package/vendor/procedural-animals/src/species/wolf/rig.js +0 -87
  262. package/vendor/procedural-animals/src/species/wolf/sculpt.js +0 -339
  263. 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
- }