@bitefoundry/retrieviewer 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/ASSET-PROVENANCE.md +6 -0
  2. package/ASSETS-LICENSE.md +2 -1
  3. package/ATTRIBUTION.md +6 -1
  4. package/COMFORT-ART.md +65 -0
  5. package/DESKTOP-PLATFORMS.md +75 -0
  6. package/INSTALLING.md +8 -5
  7. package/PLUGIN.md +13 -10
  8. package/PREREQUISITES.md +95 -0
  9. package/PUPPY-MEMORY.md +102 -1
  10. package/README.ko.md +110 -0
  11. package/README.md +119 -58
  12. package/RELEASING.md +27 -14
  13. package/THIRD-PARTY-NOTICES.md +5 -3
  14. package/braille.mjs +35 -9
  15. package/examples/live.mjs +2 -2
  16. package/integrations/desktop/platform.mjs +27 -0
  17. package/integrations/linux-overlay/README.md +83 -0
  18. package/integrations/linux-overlay/overlay.py +1094 -0
  19. package/integrations/macos-overlay/README.md +83 -0
  20. package/integrations/macos-overlay/overlay.jxa +639 -0
  21. package/integrations/opencode/runtime.mjs +4 -4
  22. package/integrations/opencode/tui.tsx +1 -1
  23. package/integrations/windows-overlay/Overlay.cs +4 -4
  24. package/integrations/windows-overlay/desktop.mjs +40 -8
  25. package/integrations/windows-overlay/protocol.mjs +11 -7
  26. package/integrations/windows-overlay/worker.mjs +13 -7
  27. package/package.json +41 -14
  28. package/puppy-comfort-sprites.mjs +25 -0
  29. package/puppy-companion-comfort-q5.bin.gz +0 -0
  30. package/puppy-companion-comfort.json +77 -0
  31. package/puppy-companion-life.mjs +49 -16
  32. package/puppy-companion-objects.mjs +12 -0
  33. package/puppy-frame-predictive.mjs +15 -9
  34. package/puppy-frame-store.mjs +2 -2
  35. package/puppy-paper-world.mjs +14 -0
  36. package/puppy-paper.mjs +65 -14
  37. package/retrieviewer-cli.mjs +10 -3
  38. package/retrieviewer-plugin.d.mts +1 -1
  39. package/retrieviewer-plugin.mjs +1 -1
  40. package/retrieviewer.d.mts +10 -4
  41. package/retrieviewer.mjs +20 -14
  42. package/locomotion-core.mjs +0 -21
  43. package/locomotion-puppy.animal +0 -0
  44. package/vendor/procedural-animals/package.json +0 -40
  45. package/vendor/procedural-animals/src/animal.js +0 -146
  46. package/vendor/procedural-animals/src/bake.js +0 -51
  47. package/vendor/procedural-animals/src/build.js +0 -81
  48. package/vendor/procedural-animals/src/core/build/coatKit.js +0 -121
  49. package/vendor/procedural-animals/src/core/build/featherCards.js +0 -189
  50. package/vendor/procedural-animals/src/core/build/finEdges.js +0 -85
  51. package/vendor/procedural-animals/src/core/build/furSlope.js +0 -115
  52. package/vendor/procedural-animals/src/core/build/mesh.js +0 -67
  53. package/vendor/procedural-animals/src/core/build/pipeline.js +0 -272
  54. package/vendor/procedural-animals/src/core/build/seams.js +0 -183
  55. package/vendor/procedural-animals/src/core/build/warp.js +0 -112
  56. package/vendor/procedural-animals/src/core/build/weights.js +0 -393
  57. package/vendor/procedural-animals/src/core/math/vec.js +0 -81
  58. package/vendor/procedural-animals/src/core/motion/actions.js +0 -800
  59. package/vendor/procedural-animals/src/core/motion/bird.js +0 -3063
  60. package/vendor/procedural-animals/src/core/motion/birdActions.js +0 -1059
  61. package/vendor/procedural-animals/src/core/motion/common.js +0 -158
  62. package/vendor/procedural-animals/src/core/motion/index.js +0 -22
  63. package/vendor/procedural-animals/src/core/motion/quadruped.js +0 -2875
  64. package/vendor/procedural-animals/src/core/motion/snake.js +0 -1594
  65. package/vendor/procedural-animals/src/core/motion/spider.js +0 -1810
  66. package/vendor/procedural-animals/src/core/motion/swimmer.js +0 -1047
  67. package/vendor/procedural-animals/src/core/motion/swimmerActions.js +0 -613
  68. package/vendor/procedural-animals/src/core/render/animalObject.js +0 -262
  69. package/vendor/procedural-animals/src/core/render/coatMaterial.js +0 -1301
  70. package/vendor/procedural-animals/src/core/render/dqs.js +0 -159
  71. package/vendor/procedural-animals/src/core/render/eyes.js +0 -676
  72. package/vendor/procedural-animals/src/core/render/membranes.js +0 -77
  73. package/vendor/procedural-animals/src/core/render/skeleton.js +0 -41
  74. package/vendor/procedural-animals/src/core/rig/bird.js +0 -327
  75. package/vendor/procedural-animals/src/core/rig/quadruped.js +0 -76
  76. package/vendor/procedural-animals/src/core/rig/rig.js +0 -79
  77. package/vendor/procedural-animals/src/core/rig/snake.js +0 -76
  78. package/vendor/procedural-animals/src/core/rig/spider.js +0 -110
  79. package/vendor/procedural-animals/src/core/rig/swimmer.js +0 -181
  80. package/vendor/procedural-animals/src/core/sdf/eyeSocket.js +0 -53
  81. package/vendor/procedural-animals/src/core/sdf/mesher.js +0 -238
  82. package/vendor/procedural-animals/src/core/sdf/sdf.js +0 -263
  83. package/vendor/procedural-animals/src/index.d.ts +0 -98
  84. package/vendor/procedural-animals/src/index.js +0 -29
  85. package/vendor/procedural-animals/src/registry.js +0 -48
  86. package/vendor/procedural-animals/src/species/bear/behaviour.js +0 -463
  87. package/vendor/procedural-animals/src/species/bear/claws.js +0 -147
  88. package/vendor/procedural-animals/src/species/bear/coat.js +0 -726
  89. package/vendor/procedural-animals/src/species/bear/index.js +0 -139
  90. package/vendor/procedural-animals/src/species/bear/motion.js +0 -112
  91. package/vendor/procedural-animals/src/species/bear/regions.js +0 -78
  92. package/vendor/procedural-animals/src/species/bear/rig.js +0 -112
  93. package/vendor/procedural-animals/src/species/bear/sculpt.js +0 -272
  94. package/vendor/procedural-animals/src/species/boar/behaviour.js +0 -335
  95. package/vendor/procedural-animals/src/species/boar/coat.js +0 -372
  96. package/vendor/procedural-animals/src/species/boar/index.js +0 -110
  97. package/vendor/procedural-animals/src/species/boar/motion.js +0 -85
  98. package/vendor/procedural-animals/src/species/boar/regions.js +0 -88
  99. package/vendor/procedural-animals/src/species/boar/rig.js +0 -111
  100. package/vendor/procedural-animals/src/species/boar/sculpt.js +0 -285
  101. package/vendor/procedural-animals/src/species/cat/behaviour.js +0 -559
  102. package/vendor/procedural-animals/src/species/cat/coat.js +0 -841
  103. package/vendor/procedural-animals/src/species/cat/index.js +0 -211
  104. package/vendor/procedural-animals/src/species/cat/motion.js +0 -101
  105. package/vendor/procedural-animals/src/species/cat/regions.js +0 -82
  106. package/vendor/procedural-animals/src/species/cat/rig.js +0 -100
  107. package/vendor/procedural-animals/src/species/cat/sculpt.js +0 -266
  108. package/vendor/procedural-animals/src/species/cat/whiskers.js +0 -110
  109. package/vendor/procedural-animals/src/species/cheetah/coat.js +0 -427
  110. package/vendor/procedural-animals/src/species/cheetah/index.js +0 -51
  111. package/vendor/procedural-animals/src/species/cheetah/joints.js +0 -3
  112. package/vendor/procedural-animals/src/species/cheetah/motion.js +0 -77
  113. package/vendor/procedural-animals/src/species/cheetah/regions.js +0 -95
  114. package/vendor/procedural-animals/src/species/cheetah/rig.js +0 -66
  115. package/vendor/procedural-animals/src/species/cheetah/sculpt.js +0 -158
  116. package/vendor/procedural-animals/src/species/chicken/coat.js +0 -313
  117. package/vendor/procedural-animals/src/species/chicken/coverts.js +0 -320
  118. package/vendor/procedural-animals/src/species/chicken/index.js +0 -110
  119. package/vendor/procedural-animals/src/species/chicken/motion.js +0 -177
  120. package/vendor/procedural-animals/src/species/chicken/regions.js +0 -66
  121. package/vendor/procedural-animals/src/species/chicken/rig.js +0 -144
  122. package/vendor/procedural-animals/src/species/chicken/sculpt.js +0 -389
  123. package/vendor/procedural-animals/src/species/chicken/wingLook.js +0 -66
  124. package/vendor/procedural-animals/src/species/chicken/wingStack.js +0 -124
  125. package/vendor/procedural-animals/src/species/cow/behaviour.js +0 -521
  126. package/vendor/procedural-animals/src/species/cow/coat.js +0 -465
  127. package/vendor/procedural-animals/src/species/cow/index.js +0 -118
  128. package/vendor/procedural-animals/src/species/cow/motion.js +0 -81
  129. package/vendor/procedural-animals/src/species/cow/regions.js +0 -98
  130. package/vendor/procedural-animals/src/species/cow/rig.js +0 -116
  131. package/vendor/procedural-animals/src/species/cow/sculpt.js +0 -339
  132. package/vendor/procedural-animals/src/species/crow/coat.js +0 -145
  133. package/vendor/procedural-animals/src/species/crow/index.js +0 -70
  134. package/vendor/procedural-animals/src/species/crow/joints.js +0 -3
  135. package/vendor/procedural-animals/src/species/crow/motion.js +0 -101
  136. package/vendor/procedural-animals/src/species/crow/regions.js +0 -50
  137. package/vendor/procedural-animals/src/species/crow/rig.js +0 -83
  138. package/vendor/procedural-animals/src/species/crow/sculpt.js +0 -118
  139. package/vendor/procedural-animals/src/species/deer/behaviour.js +0 -513
  140. package/vendor/procedural-animals/src/species/deer/coat.js +0 -391
  141. package/vendor/procedural-animals/src/species/deer/index.js +0 -106
  142. package/vendor/procedural-animals/src/species/deer/motion.js +0 -97
  143. package/vendor/procedural-animals/src/species/deer/regions.js +0 -60
  144. package/vendor/procedural-animals/src/species/deer/rig.js +0 -124
  145. package/vendor/procedural-animals/src/species/deer/sculpt.js +0 -297
  146. package/vendor/procedural-animals/src/species/dog/behaviour.js +0 -521
  147. package/vendor/procedural-animals/src/species/dog/coat.js +0 -495
  148. package/vendor/procedural-animals/src/species/dog/index.js +0 -144
  149. package/vendor/procedural-animals/src/species/dog/motion.js +0 -118
  150. package/vendor/procedural-animals/src/species/dog/regions.js +0 -61
  151. package/vendor/procedural-animals/src/species/dog/rig.js +0 -129
  152. package/vendor/procedural-animals/src/species/dog/sculpt.js +0 -246
  153. package/vendor/procedural-animals/src/species/eagle/coat.js +0 -250
  154. package/vendor/procedural-animals/src/species/eagle/index.js +0 -76
  155. package/vendor/procedural-animals/src/species/eagle/joints.js +0 -3
  156. package/vendor/procedural-animals/src/species/eagle/motion.js +0 -137
  157. package/vendor/procedural-animals/src/species/eagle/regions.js +0 -57
  158. package/vendor/procedural-animals/src/species/eagle/rig.js +0 -107
  159. package/vendor/procedural-animals/src/species/eagle/sculpt.js +0 -137
  160. package/vendor/procedural-animals/src/species/fish/coat.js +0 -271
  161. package/vendor/procedural-animals/src/species/fish/index.js +0 -88
  162. package/vendor/procedural-animals/src/species/fish/motion.js +0 -92
  163. package/vendor/procedural-animals/src/species/fish/regions.js +0 -27
  164. package/vendor/procedural-animals/src/species/fish/rig.js +0 -143
  165. package/vendor/procedural-animals/src/species/fish/sculpt.js +0 -214
  166. package/vendor/procedural-animals/src/species/fish/variants.js +0 -126
  167. package/vendor/procedural-animals/src/species/fox/behaviour.js +0 -314
  168. package/vendor/procedural-animals/src/species/fox/coat.js +0 -444
  169. package/vendor/procedural-animals/src/species/fox/index.js +0 -120
  170. package/vendor/procedural-animals/src/species/fox/motion.js +0 -81
  171. package/vendor/procedural-animals/src/species/fox/regions.js +0 -59
  172. package/vendor/procedural-animals/src/species/fox/rig.js +0 -91
  173. package/vendor/procedural-animals/src/species/fox/sculpt.js +0 -202
  174. package/vendor/procedural-animals/src/species/frog/behaviour.js +0 -590
  175. package/vendor/procedural-animals/src/species/frog/coat.js +0 -305
  176. package/vendor/procedural-animals/src/species/frog/index.js +0 -97
  177. package/vendor/procedural-animals/src/species/frog/motion.js +0 -69
  178. package/vendor/procedural-animals/src/species/frog/regions.js +0 -46
  179. package/vendor/procedural-animals/src/species/frog/rig.js +0 -156
  180. package/vendor/procedural-animals/src/species/frog/sculpt.js +0 -240
  181. package/vendor/procedural-animals/src/species/goat/behaviour.js +0 -509
  182. package/vendor/procedural-animals/src/species/goat/coat.js +0 -590
  183. package/vendor/procedural-animals/src/species/goat/index.js +0 -149
  184. package/vendor/procedural-animals/src/species/goat/motion.js +0 -95
  185. package/vendor/procedural-animals/src/species/goat/regions.js +0 -98
  186. package/vendor/procedural-animals/src/species/goat/rig.js +0 -181
  187. package/vendor/procedural-animals/src/species/goat/sculpt.js +0 -547
  188. package/vendor/procedural-animals/src/species/horse/coat.js +0 -361
  189. package/vendor/procedural-animals/src/species/horse/coatSeam.js +0 -122
  190. package/vendor/procedural-animals/src/species/horse/index.js +0 -80
  191. package/vendor/procedural-animals/src/species/horse/motion.js +0 -245
  192. package/vendor/procedural-animals/src/species/horse/regions.js +0 -60
  193. package/vendor/procedural-animals/src/species/horse/rig.js +0 -117
  194. package/vendor/procedural-animals/src/species/horse/sculpt.js +0 -316
  195. package/vendor/procedural-animals/src/species/horse/seamWeights.js +0 -272
  196. package/vendor/procedural-animals/src/species/lion/behaviour.js +0 -509
  197. package/vendor/procedural-animals/src/species/lion/coat.js +0 -1016
  198. package/vendor/procedural-animals/src/species/lion/index.js +0 -111
  199. package/vendor/procedural-animals/src/species/lion/motion.js +0 -87
  200. package/vendor/procedural-animals/src/species/lion/regions.js +0 -106
  201. package/vendor/procedural-animals/src/species/lion/rig.js +0 -110
  202. package/vendor/procedural-animals/src/species/lion/sculpt.js +0 -384
  203. package/vendor/procedural-animals/src/species/lion/whiskers.js +0 -132
  204. package/vendor/procedural-animals/src/species/pig/behaviour.js +0 -575
  205. package/vendor/procedural-animals/src/species/pig/coat.js +0 -730
  206. package/vendor/procedural-animals/src/species/pig/ears.js +0 -242
  207. package/vendor/procedural-animals/src/species/pig/index.js +0 -161
  208. package/vendor/procedural-animals/src/species/pig/motion.js +0 -105
  209. package/vendor/procedural-animals/src/species/pig/regions.js +0 -100
  210. package/vendor/procedural-animals/src/species/pig/rig.js +0 -152
  211. package/vendor/procedural-animals/src/species/pig/sculpt.js +0 -382
  212. package/vendor/procedural-animals/src/species/rabbit/behaviour.js +0 -605
  213. package/vendor/procedural-animals/src/species/rabbit/coat.js +0 -285
  214. package/vendor/procedural-animals/src/species/rabbit/index.js +0 -85
  215. package/vendor/procedural-animals/src/species/rabbit/motion.js +0 -85
  216. package/vendor/procedural-animals/src/species/rabbit/regions.js +0 -48
  217. package/vendor/procedural-animals/src/species/rabbit/rig.js +0 -99
  218. package/vendor/procedural-animals/src/species/rabbit/sculpt.js +0 -148
  219. package/vendor/procedural-animals/src/species/rat/behaviour.js +0 -539
  220. package/vendor/procedural-animals/src/species/rat/coat.js +0 -341
  221. package/vendor/procedural-animals/src/species/rat/index.js +0 -111
  222. package/vendor/procedural-animals/src/species/rat/motion.js +0 -90
  223. package/vendor/procedural-animals/src/species/rat/regions.js +0 -92
  224. package/vendor/procedural-animals/src/species/rat/rig.js +0 -112
  225. package/vendor/procedural-animals/src/species/rat/sculpt.js +0 -163
  226. package/vendor/procedural-animals/src/species/rat/whiskers.js +0 -119
  227. package/vendor/procedural-animals/src/species/shark/coat.js +0 -383
  228. package/vendor/procedural-animals/src/species/shark/index.js +0 -104
  229. package/vendor/procedural-animals/src/species/shark/motion.js +0 -69
  230. package/vendor/procedural-animals/src/species/shark/regions.js +0 -34
  231. package/vendor/procedural-animals/src/species/shark/rig.js +0 -194
  232. package/vendor/procedural-animals/src/species/shark/sculpt.js +0 -343
  233. package/vendor/procedural-animals/src/species/shark/variants.js +0 -105
  234. package/vendor/procedural-animals/src/species/sheep/behaviour.js +0 -522
  235. package/vendor/procedural-animals/src/species/sheep/coat.js +0 -529
  236. package/vendor/procedural-animals/src/species/sheep/index.js +0 -152
  237. package/vendor/procedural-animals/src/species/sheep/motion.js +0 -86
  238. package/vendor/procedural-animals/src/species/sheep/neck.js +0 -41
  239. package/vendor/procedural-animals/src/species/sheep/regions.js +0 -62
  240. package/vendor/procedural-animals/src/species/sheep/rig.js +0 -163
  241. package/vendor/procedural-animals/src/species/sheep/sculpt.js +0 -542
  242. package/vendor/procedural-animals/src/species/snake/coat.js +0 -315
  243. package/vendor/procedural-animals/src/species/snake/index.js +0 -73
  244. package/vendor/procedural-animals/src/species/snake/motion.js +0 -51
  245. package/vendor/procedural-animals/src/species/snake/regions.js +0 -51
  246. package/vendor/procedural-animals/src/species/snake/rig.js +0 -108
  247. package/vendor/procedural-animals/src/species/snake/sculpt.js +0 -122
  248. package/vendor/procedural-animals/src/species/spider/coat.js +0 -279
  249. package/vendor/procedural-animals/src/species/spider/index.js +0 -78
  250. package/vendor/procedural-animals/src/species/spider/motion.js +0 -72
  251. package/vendor/procedural-animals/src/species/spider/regions.js +0 -87
  252. package/vendor/procedural-animals/src/species/spider/rig.js +0 -118
  253. package/vendor/procedural-animals/src/species/spider/sculpt.js +0 -182
  254. package/vendor/procedural-animals/src/species/wolf/behaviour.js +0 -160
  255. package/vendor/procedural-animals/src/species/wolf/coat.js +0 -749
  256. package/vendor/procedural-animals/src/species/wolf/index.js +0 -107
  257. package/vendor/procedural-animals/src/species/wolf/motion.js +0 -80
  258. package/vendor/procedural-animals/src/species/wolf/regions.js +0 -57
  259. package/vendor/procedural-animals/src/species/wolf/rig.js +0 -87
  260. package/vendor/procedural-animals/src/species/wolf/sculpt.js +0 -339
  261. package/vendor/procedural-animals/src/worker.js +0 -29
@@ -1,1047 +0,0 @@
1
- // Swimmer motion engine (fish, sharks): a travelling body wave on a spine chain, steering by body
2
- // bends, paired fins that scull and fold, gill covers and mouth that pump, 3D swimming inside the
3
- // water volume between the bed `ground(x, z)` and the surface `water(x, z)`.
4
- //
5
- // Model. The spine chain (snout -> spine0 .. spineN -> caudal tip, core/rig/swimmer.js) is rebuilt
6
- // every frame from segment angles, so segment lengths never change (no stretch):
7
- // * body wave: lateral displacement y(u, t) = A(u) sin(2 pi (phi - u / lambda)), u = arc length /
8
- // body length; the phase phi integrates the tail-beat frequency (motion from phase, continuous
9
- // when the frequency changes); A(u) = amp/2 * L * envelope(u) with envelope exp(k (u - 1))
10
- // (carangiform) or u^p (thunniform: stiff front, all motion in the peduncle and tail);
11
- // * steering: the body bends into an arc (C-bend) proportional to the yaw rate and inversely to
12
- // the speed (it follows its own path), distributed along the flexible part of the body;
13
- // * recoil: the mass-weighted mean orientation of the body is the heading and the mass-weighted
14
- // centre is state.position, so the head yaws against the tail beat as in real fish;
15
- // * vertical plane: rest pitch of every segment from the bind pose (an up-turned heterocercal
16
- // shark tail just works), plus action arches (leap, death droop), plus the path pitch.
17
- // Fins: pectorals scull at low speed (flap + row + feather from a fin phase) and fold flat against
18
- // the body as the speed rises; they flare to brake and on the inside of turns; pelvic fins spread when
19
- // slow; gill covers flare with each breath while the mouth opens in counter-phase (buccal pump).
20
- // The water column: the fish keeps a clearance above the bed and below the surface (soft steering)
21
- // and a hard skin-extent clamp (bone-local skin extents measured from the mesh at start) so the skin
22
- // never cuts the bed or breaks the surface unless it is leaping. Out of the water it flies
23
- // ballistically and splashes back ('takeoff' / 'land' events).
24
- // Shores (water(x, z) null or shallower than the body: a lake in a landscape): the fish turns away from
25
- // water too shallow ahead and never swims into it; a leap is aimed short enough to come down in deep
26
- // water; a body that still comes down on dry ground (or is placed there) is beached: it flops and
27
- // slides back toward the nearest water ('beach' event, state.beached, gait 'flop') and swims on there.
28
- // Lessons from the cheetah: continuous solves, soft limits, springs for secondary motion, zero-dt
29
- // safety, no allocations in the hot path.
30
- //
31
- // ------------------------------------------------------------------------------------------------
32
- // species.motion schema (all optional; speeds in body lengths per second, BL/s; L = snout -> caudal
33
- // tip along the chain of this individual; `variants: { key: { ...overrides } }` merges per
34
- // params.variant, e.g. fish trout / goldfish, shark white / blacktip)
35
- // ------------------------------------------------------------------------------------------------
36
- // speeds { name: BL/s } named speeds (default { scull: 0.4, cruise: 1.5, sprint: 4 }); the engine
37
- // exposes them in m/s for this individual (animal.gears)
38
- // maxSpeed BL/s top sustained speed from move(); burstSpeed BL/s peak of burst / attack / hit
39
- // minSpeed BL/s: obligate ram ventilators (sharks) never drop below it: they patrol in slow circles
40
- // patrolRadius BL: radius of that patrol circle (default 0 = the tightest turn)
41
- // accel, decel BL/s^2 (decel is also the drag of a gliding fish)
42
- // turnRate rad/s yaw rate at rest; turnRadius: minimum turning radius at speed (BL)
43
- // wave rows [v BL/s, f Hz, amp (tail-tip peak-to-peak / L), lambda (L), pect 0..1 (pectoral
44
- // sculling activity), pf Hz (pectoral beat), fold 0..1 (pectorals folded)], sorted by v.
45
- // From Bainbridge (1958): f = (U/L + 1) / 0.75 above ~2 BL/s, amp ~0.2 L.
46
- // envelope { exp: k } (a = e^(k (u-1)), carangiform, default k = 3.5) or { pow: p } (thunniform,
47
- // p ~ 2.5); head: 0..1 extra head yaw (added to recoil)
48
- // flex [u0, u1]: where the body can bend for steering (fraction of L), default [0.15, 0.95]
49
- // caudalStiff 0..1: how much the caudal fin stays a flat plate continuing the peduncle (default 0.6)
50
- // bend max total C-bend of a turn (rad), cBend: max C-start bend (rad)
51
- // bank max roll in turns (rad); bankGain: roll per unit of lateral acceleration (default 0.12;
52
- // sharks bank into turns ~1-1.4); pitchMax: max climb pitch (rad); hover: vertical hover speed (BL/s)
53
- // strandRoll rad: the roll of a body stranded on dry ground (default 1.3: a bony fish lies on its side;
54
- // a broad shark ~0.25, on its belly)
55
- // clearance distance kept from the bed and the surface (BL); cruiseDepth: preferred height above
56
- // the bed when spawned (BL)
57
- // pectoral { rest: [abduct, row, feather] deg, flap, row, feather: sculling amplitudes (deg),
58
- // fold: deg against the body at speed, flare: deg when braking, alternate: 0..1
59
- // (0 = both fins in phase, 1 = alternating, hovering goldfish), stiff: 0..1 (sharks:
60
- // hydrofoils that do not scull) }
61
- // pelvic { rest: deg abduct, spread: deg when slow, fold: deg at speed }
62
- // breath { rate Hz, operculum deg flare, mouth 0..1 buccal gape, ram: BL/s above which the
63
- // pumping fades (ram ventilation), slits: true for sharks (no gill covers) }
64
- // jaw { gape deg (max mouth opening), protrude (BL, upper jaw protrusion), protrudeDir: [x, y, z]
65
- // bind-space direction of the protrusion (default [0, -0.3, 1]; sharks forward and down) }
66
- // actions { burst: { turn deg, speed BL/s }, jump: { height BL }, eat: { style 'suction' |
67
- // 'bottom', lunge BL }, drink: { reach BL (surface gulp if the surface is closer) },
68
- // attack: { lunge BL, headLift rad (snout lift on the bite), shake (head shakes after the
69
- // bite), eye: 'roll' | 'nictitate' (eye protection on the bite) }, eat: { style 'tear' too
70
- // (bite, head lift, shakes: sharks) }, jump: { bite: bool (breach with a bite), roll rad
71
- // (rotation in the air) }, sleep: { style 'bottom' | 'hover' | 'side' }, lie: { style },
72
- // death: { float: bool, roll rad (final roll, default pi/2 = on its side) } }
73
- // state extras: eyeRoll 0..1 (white shark: the eye rolls back on the bite), nictitate 0..1 (membrane)
74
- // ------------------------------------------------------------------------------------------------
75
- import * as THREE from 'three';
76
- import { TAU, clamp, lerp, smooth, angDiff, Spring, writeFrame, prng, tv } from './common.js';
77
- import { SwimmerActions, createSwimParams } from './swimmerActions.js';
78
-
79
- const DEFAULTS = {
80
- speeds: { scull: 0.4, cruise: 1.5, sprint: 4 },
81
- maxSpeed: 6, burstSpeed: 10, minSpeed: 0, patrolRadius: 0, accel: 12, decel: 5, turnRate: 3.2, turnRadius: 0.35,
82
- wave: [
83
- [0, 0.8, 0.03, 1.0, 1.0, 1.5, 0],
84
- [0.5, 1.9, 0.1, 0.95, 0.6, 1.8, 0.3],
85
- [1.5, 3.3, 0.17, 0.9, 0.15, 2.0, 0.75],
86
- [3, 5.3, 0.2, 0.85, 0, 2, 1],
87
- [6, 9.3, 0.2, 0.8, 0, 2, 1],
88
- [10, 14.7, 0.21, 0.75, 0, 2, 1],
89
- ],
90
- envelope: { exp: 3.5 }, flex: [0.15, 0.97], bend: 1.4, cBend: 2.6, bank: 0.35, bankGain: 0.12, pitchMax: 0.9, hover: 0.35,
91
- clearance: 0.12, cruiseDepth: 0.6,
92
- pectoral: { rest: [0, 0, 0], flap: 22, row: 18, feather: 25, fold: 26, flare: 55, alternate: 0.3, stiff: 0 },
93
- pelvic: { rest: 0, spread: 10, fold: 14 },
94
- breath: { rate: 1.2, operculum: 9, mouth: 0.18, ram: 2.5, slits: false },
95
- jaw: { gape: 28, protrude: 0.02 },
96
- actions: {},
97
- };
98
-
99
- const _m = new THREE.Matrix4(), _m2 = new THREE.Matrix4(), _q = new THREE.Quaternion(), _q2 = new THREE.Quaternion();
100
- const _v1 = new THREE.Vector3(), _v2 = new THREE.Vector3(), _v3 = new THREE.Vector3();
101
-
102
- function isObj(x) { return x && typeof x === 'object' && !Array.isArray(x); }
103
- function merge(a, b) {
104
- if (!isObj(b)) return b === undefined ? a : b;
105
- const o = { ...(isObj(a) ? a : {}) };
106
- for (const [k, v] of Object.entries(b)) o[k] = isObj(v) && isObj(o[k]) ? merge(o[k], v) : v;
107
- return o;
108
- }
109
-
110
- function resolveConfig(species, data) {
111
- const base = species.motion || {};
112
- const variant = data.params?.variant;
113
- let spec = merge(DEFAULTS, { ...base, variants: undefined });
114
- if (variant && base.variants && base.variants[variant]) spec = merge(spec, base.variants[variant]);
115
- // named speeds replace the defaults (a species' gears are its own, e.g. a shark has no 'scull')
116
- spec.speeds = (variant && base.variants?.[variant]?.speeds) || base.speeds || DEFAULTS.speeds;
117
- const J = {};
118
- for (const [k, v] of Object.entries(data.joints)) J[k] = new THREE.Vector3(v[0], v[1], v[2]);
119
- const names = data.bones.map((b) => b.name);
120
- const spine = names.filter((n) => /^spine\d+$/.test(n)).sort((a, b) => +a.slice(5) - +b.slice(5));
121
- const caudal = names.filter((n) => /^caudal\d+$/.test(n)).sort((a, b) => +a.slice(6) - +b.slice(6));
122
- if (!spine.length || !J.snout) throw new Error('procedural-animals swimmer: the rig needs snout, spine0..N (core/rig/swimmer.js)');
123
- const chainBones = [...spine, ...caudal];
124
- const bIndex = Object.fromEntries(names.map((n, i) => [n, i]));
125
- const chainJ = chainBones.map((n) => J[data.bones[bIndex[n]].headJ]);
126
- chainJ.push(J[data.bones[bIndex[chainBones[chainBones.length - 1]]].tailJ]);
127
- const n = chainBones.length;
128
- const segLen = new Float64Array(n), restPitch = new Float64Array(n), restYaw = new Float64Array(n);
129
- for (let i = 0; i < n; i++) {
130
- const d = _v1.copy(chainJ[i + 1]).sub(chainJ[i]);
131
- segLen[i] = d.length();
132
- restPitch[i] = Math.atan2(d.y, Math.hypot(d.x, d.z));
133
- restYaw[i] = Math.atan2(d.x, -d.z);
134
- }
135
- const hd = _v1.copy(J.snout).sub(chainJ[0]);
136
- const headLen = hd.length(), headPitch = Math.atan2(hd.y, Math.hypot(hd.x, hd.z));
137
- // arc positions (from the snout) and body length
138
- const sJ = new Float64Array(n + 1);
139
- sJ[0] = headLen;
140
- for (let i = 0; i < n; i++) sJ[i + 1] = sJ[i] + segLen[i];
141
- const L = sJ[n];
142
- const cfg = {
143
- caudalStart: spine.length,
144
- spec, J, L, n, chainBones, chainJ, segLen, restPitch, restYaw, headLen, headPitch, sJ, bIndex,
145
- uJ: Float64Array.from(sJ, (s) => s / L),
146
- gears: Object.fromEntries(Object.entries(spec.speeds).map(([k, v]) => [k, v * L])),
147
- maxSpeed: spec.maxSpeed * L, burstSpeed: spec.burstSpeed * L, minSpeed: spec.minSpeed * L,
148
- accel: spec.accel * L, decel: spec.decel * L,
149
- hover: spec.hover * L, clearance: spec.clearance * L,
150
- actions: spec.actions || {},
151
- };
152
- // amplitude envelope and bend distribution per joint
153
- const env = spec.envelope || {};
154
- const envAt = (u) => (env.pow ? Math.pow(Math.max(0, u), env.pow) : Math.exp((env.exp ?? 3.5) * (u - 1)));
155
- cfg.envJ = Float64Array.from(cfg.uJ, envAt);
156
- const [f0, f1] = spec.flex || [0.15, 0.97];
157
- const bw = new Float64Array(n);
158
- let bs = 0;
159
- for (let i = 0; i < n; i++) {
160
- const u = (cfg.uJ[i] + cfg.uJ[i + 1]) / 2;
161
- bw[i] = smooth(f0 - 0.1, f0 + 0.1, u) * (1 - smooth(f1 - 0.05, f1 + 0.08, u)) * (0.5 + 0.5 * Math.sqrt(envAt(u)));
162
- bs += bw[i];
163
- }
164
- for (let i = 0; i < n; i++) bw[i] /= bs || 1;
165
- cfg.bendW = bw;
166
- return cfg;
167
- }
168
-
169
- export function createSwimmerMotion(ctx) { return new SwimmerMotion(ctx); }
170
-
171
- export class SwimmerMotion {
172
- constructor({ species, data, skeleton, ground, water, position, heading = 0, emit }) {
173
- this.species = species;
174
- this.cfg = resolveConfig(species, data);
175
- const cfg = this.cfg;
176
- this.sk = skeleton;
177
- this.data = data;
178
- this.ground = ground || (() => 0);
179
- this.water = typeof water === 'function' ? water : null;
180
- this.emit = emit || (() => {});
181
- this.rand = prng((data.seed || 1) * 7919 + 29);
182
- this.bone = {};
183
- for (let i = 0; i < data.bones.length; i++) this.bone[data.bones[i].name] = skeleton.bones[i];
184
- this.chainBoneObjs = cfg.chainBones.map((nm) => this.bone[nm]);
185
- this.isChain = new Uint8Array(data.bones.length);
186
- for (const nm of [...cfg.chainBones, 'head']) this.isChain[cfg.bIndex[nm]] = 1;
187
- // ---- public contract
188
- this.input = { speed: 0, heading, climb: 0, look: null, follow: null, target: null, targetSpeed: cfg.gears.cruise ?? cfg.L, crouch: 0, gait: null };
189
- this.pos = new THREE.Vector3();
190
- this.velocity = new THREE.Vector3();
191
- this.heading = heading;
192
- this.speed = 0;
193
- this.lookTarget = new THREE.Vector3();
194
- this.state = {
195
- position: this.pos, heading, speed: 0, velocity: this.velocity, gait: 'stand', grounded: true,
196
- action: null, posture: 'stand', eyelid: 0, eyeRoll: 0, nictitate: 0, lookTarget: null, inWater: true, depth: 0, pitch: 0, roll: 0,
197
- stats: { gait: 'stand', freq: 0, duty: 0 },
198
- legs: [],
199
- };
200
- this.headPose = { position: new THREE.Vector3(), quaternion: new THREE.Quaternion() };
201
- this.debug = { targets: Array.from({ length: 6 }, () => new THREE.Vector3()) };
202
- this.gears = cfg.gears;
203
- this.gaits = Object.keys(cfg.gears);
204
- this.lod = 0;
205
- this.frame = 0;
206
- this.time = 0;
207
- // ---- dynamic state
208
- this.pitch = 0; this.roll = 0; this.yawRate = 0; this.accS = 0;
209
- this.pitchS = new Spring(0); this.rollS = new Spring(0); this.yawS = new Spring(0);
210
- this.bendS = new Spring(0); this.archS = new Spring(0);
211
- this.ampS = new Spring(0); this.freqS = new Spring(0.8); this.lamS = new Spring(1);
212
- this.pectS = new Spring(1); this.pfS = new Spring(1.5); this.foldS = new Spring(0); this.flareS = new Spring(0);
213
- this.pelS = new Spring(0);
214
- this.gapeS = new Spring(0); this.protS = new Spring(0); this.opS = new Spring(0);
215
- this.lookYawS = new Spring(0); this.lookPitchS = new Spring(0);
216
- this.headLiftS = new Spring(0); this.eyeRollS = new Spring(0); this.nictS = new Spring(0);
217
- this.turnSideS = new Spring(0);
218
- this.phase = this.rand(); this.pPhase = this.rand(); this.bPhase = this.rand();
219
- this.vy = 0; // vertical velocity (m/s) beyond the path pitch: hovering, sinking, ballistic flight
220
- this.vh = 0; // horizontal speed in flight
221
- this.air = false;
222
- this.beached = false; // out of the water on dry ground: flopping back toward the nearest water
223
- this.surfaceY = Infinity; // last known water surface (null callbacks keep the last value)
224
- this.lift = 0; // hard clamp correction carried to the next frame
225
- this.P = createSwimParams();
226
- // ---- preallocated pose buffers
227
- const n = cfg.n;
228
- this.th = new Float64Array(n); this.ph = new Float64Array(n); this.yw = new Float64Array(n + 1);
229
- this.Q = Array.from({ length: n + 2 }, () => new THREE.Vector3()); // Q[0] = spine0 ... Q[n] = tail tip, Q[n+1] = snout (local)
230
- this.W = Array.from({ length: n + 2 }, () => new THREE.Vector3()); // world
231
- this.D = Array.from({ length: n + 1 }, () => new THREE.Vector3()); // world bone directions (last = head)
232
- this.X = Array.from({ length: n + 1 }, () => new THREE.Vector3()); // world lateral axes
233
- this.basis = { F: new THREE.Vector3(), U: new THREE.Vector3(), Lf: new THREE.Vector3() };
234
- this.gh = new Float64Array(3); // ground samples (snout, middle, tail)
235
- // ---- mass weights, skin extents, children
236
- this._measure(data);
237
- this._children(data);
238
- this.attachmentPoints = this._attachments(data);
239
- this.actionsLayer = new SwimmerActions(this);
240
- this.actions = this.actionsLayer.names;
241
- this.reset(position || new THREE.Vector3(), heading);
242
- }
243
-
244
- // skin extents of every bone in its own bind frame (from the mesh), and a mass per chain segment
245
- _measure(data) {
246
- const cfg = this.cfg, sk = this.sk, nB = data.bones.length;
247
- const ext = new Float64Array(nB * 6);
248
- for (let b = 0; b < nB; b++) { ext[b * 6] = ext[b * 6 + 2] = ext[b * 6 + 4] = Infinity; ext[b * 6 + 1] = ext[b * 6 + 3] = ext[b * 6 + 5] = -Infinity; }
249
- const massA = new Float64Array(nB * 4); // body-only lateral / dorsoventral extents (no fins)
250
- for (let b = 0; b < nB; b++) { massA[b * 4] = massA[b * 4 + 2] = Infinity; massA[b * 4 + 1] = massA[b * 4 + 3] = -Infinity; }
251
- const inv = sk.inverses || sk.bind.map((m) => m.clone().invert());
252
- const p = new THREE.Vector3();
253
- for (let v = 0; v < data.nV; v++) {
254
- if (data.coat[v * 4 + 3] < 0.5) continue;
255
- let bb = data.skinIndex[v * 4], bw = data.skinWeight[v * 4];
256
- for (let k = 1; k < 4; k++) if (data.skinWeight[v * 4 + k] > bw) { bw = data.skinWeight[v * 4 + k]; bb = data.skinIndex[v * 4 + k]; }
257
- p.set(data.pos[v * 3], data.pos[v * 3 + 1], data.pos[v * 3 + 2]).applyMatrix4(inv[bb]);
258
- const o = bb * 6;
259
- if (p.x < ext[o]) ext[o] = p.x; if (p.x > ext[o + 1]) ext[o + 1] = p.x;
260
- if (p.y < ext[o + 2]) ext[o + 2] = p.y; if (p.y > ext[o + 3]) ext[o + 3] = p.y;
261
- if (p.z < ext[o + 4]) ext[o + 4] = p.z; if (p.z > ext[o + 5]) ext[o + 5] = p.z;
262
- if (Math.round(data.tint[v * 4 + 3]) !== 11) {
263
- const q = bb * 4;
264
- if (p.x < massA[q]) massA[q] = p.x; if (p.x > massA[q + 1]) massA[q + 1] = p.x;
265
- if (p.z < massA[q + 2]) massA[q + 2] = p.z; if (p.z > massA[q + 3]) massA[q + 3] = p.z;
266
- }
267
- }
268
- // the bone itself (head .. tail joint) is part of the body: fin tips are joints too
269
- for (let b = 0; b < nB; b++) {
270
- const bl = data.bones[b], h = data.joints[bl.headJ], t = data.joints[bl.tailJ];
271
- const len = Math.hypot(t[0] - h[0], t[1] - h[1], t[2] - h[2]);
272
- const o = b * 6;
273
- if (!Number.isFinite(ext[o])) { ext[o] = ext[o + 1] = ext[o + 4] = ext[o + 5] = 0; ext[o + 2] = ext[o + 3] = 0; }
274
- ext[o] = Math.min(ext[o], 0); ext[o + 1] = Math.max(ext[o + 1], 0);
275
- ext[o + 2] = Math.min(ext[o + 2], 0); ext[o + 3] = Math.max(ext[o + 3], len);
276
- ext[o + 4] = Math.min(ext[o + 4], 0); ext[o + 5] = Math.max(ext[o + 5], 0);
277
- }
278
- // ellipse / box extents: centre + half size per bone, local X (lateral), Y (along), Z (third)
279
- this.ext = new Float64Array(nB * 6);
280
- for (let b = 0; b < nB; b++) {
281
- const o = b * 6;
282
- this.ext[o] = (ext[o] + ext[o + 1]) / 2; this.ext[o + 1] = (ext[o + 1] - ext[o]) / 2;
283
- this.ext[o + 2] = (ext[o + 2] + ext[o + 3]) / 2; this.ext[o + 3] = (ext[o + 3] - ext[o + 2]) / 2;
284
- this.ext[o + 4] = (ext[o + 4] + ext[o + 5]) / 2; this.ext[o + 5] = (ext[o + 5] - ext[o + 4]) / 2;
285
- }
286
- // mass per chain segment (+ head as the last entry): cross-section area x length
287
- const n = cfg.n;
288
- this.mass = new Float64Array(n + 1);
289
- const area = (b) => {
290
- const q = b * 4;
291
- const hx = Number.isFinite(massA[q]) ? (massA[q + 1] - massA[q]) / 2 : 0, hz = Number.isFinite(massA[q + 2]) ? (massA[q + 3] - massA[q + 2]) / 2 : 0;
292
- return Math.max(1e-8, hx * hz);
293
- };
294
- let tot = 0;
295
- for (let i = 0; i < n; i++) { this.mass[i] = area(cfg.bIndex[cfg.chainBones[i]]) * cfg.segLen[i]; tot += this.mass[i]; }
296
- this.mass[n] = area(cfg.bIndex.head) * cfg.headLen * 1.3; tot += this.mass[n];
297
- for (let i = 0; i <= n; i++) this.mass[i] /= tot;
298
- // extents of the whole bind body below / above the centre (for spawning and clearance)
299
- let lo = Infinity, hi = -Infinity;
300
- for (let v = 0; v < data.nV; v++) { const y = data.pos[v * 3 + 1]; if (y < lo) lo = y; if (y > hi) hi = y; }
301
- this.bindLow = lo; this.bindHigh = hi;
302
- this.nB = nB;
303
- }
304
-
305
- // children of the chain: jaw, upper jaw, gill covers, paired fins (bind-space rotation axes)
306
- _children(data) {
307
- const J = this.cfg.J, sk = this.sk, bi = this.cfg.bIndex;
308
- const V = (a) => (J[a] ? J[a].clone() : null);
309
- const mk = (name, pivotJ, axes) => {
310
- const i = bi[name];
311
- if (i === undefined) return null;
312
- const b = data.bones[i];
313
- const pi = bi[b.parent];
314
- const base = J[pivotJ || b.headJ];
315
- const A = new THREE.Matrix4().copy(sk.inverses ? sk.inverses[pi] : sk.bind[pi].clone().invert()).multiply(new THREE.Matrix4().makeTranslation(base.x, base.y, base.z));
316
- const C0 = sk.bind[i].clone().setPosition(0, 0, 0);
317
- return { name, i, pi, bone: sk.bones[i], parent: sk.bones[pi], A, C0, axes, q: new THREE.Quaternion(), t: new THREE.Vector3(), M: new THREE.Matrix4() };
318
- };
319
- const axis = (a, b) => a && b ? new THREE.Vector3().copy(a).cross(b).normalize() : null;
320
- this.kids = [];
321
- // lower jaw: rotates down about the hinge
322
- if (J.jawHinge && J.jawTip && bi.jaw !== undefined) {
323
- const j = V('jawTip').sub(J.jawHinge).normalize();
324
- this.kids.push(this.jaw = mk('jaw', 'jawHinge', { open: axis(j, new THREE.Vector3(0, -1, 0)) }));
325
- }
326
- if (bi.upperJaw !== undefined) this.kids.push(this.upperJaw = mk('upperJaw', null, {}));
327
- this.opercula = [];
328
- this.pecs = [];
329
- this.pels = [];
330
- for (const [S, s] of [['L', 1], ['R', -1]]) {
331
- const out = new THREE.Vector3(s, 0, 0);
332
- if (bi['operculum' + S] !== undefined) {
333
- const e = V('opEdge' + S).sub(J['opHinge' + S]).normalize();
334
- const k = mk('operculum' + S, null, { flare: axis(e, out) });
335
- this.kids.push(k); this.opercula.push(k);
336
- }
337
- if (bi['pectoral' + S] !== undefined) {
338
- const d = V('pecTip' + S).sub(J['pecBase' + S]).normalize();
339
- const k = mk('pectoral' + S, null, { ab: axis(d, out), row: axis(d, new THREE.Vector3(0, 0, 1)), long: d.clone(), s });
340
- this.kids.push(k); this.pecs.push(k);
341
- }
342
- if (bi['pelvic' + S] !== undefined) {
343
- const d = V('pelTip' + S).sub(J['pelBase' + S]).normalize();
344
- const k = mk('pelvic' + S, null, { ab: axis(d, out), long: d.clone(), s });
345
- this.kids.push(k); this.pels.push(k);
346
- }
347
- }
348
- this.kids = this.kids.filter(Boolean);
349
- // children of the head must be posed after it; others after their spine bone (chain first)
350
- }
351
-
352
- _attachments(data) {
353
- const J = this.cfg.J, sk = this.sk, bi = this.cfg.bIndex;
354
- const at = (bone, p) => {
355
- const i = bi[bone];
356
- if (i === undefined || !p) return null;
357
- const inv = sk.inverses ? sk.inverses[i] : sk.bind[i].clone().invert();
358
- return { bone, local: new THREE.Matrix4().makeTranslation(p.x, p.y, p.z).premultiply(inv) };
359
- };
360
- let headP = J.snout.clone().lerp(this.cfg.chainJ[0], 0.5);
361
- 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); }
362
- const k = Math.min(this.cfg.n - 1, Math.round(this.cfg.n * 0.3));
363
- const back = this.cfg.chainJ[k].clone();
364
- back.y = this.bindHigh;
365
- const out = { mouth: at('head', J.snout), head: at('head', headP), back: at(this.cfg.chainBones[k], back) };
366
- for (const kk of Object.keys(out)) if (!out[kk]) delete out[kk];
367
- this._headLocal = at('head', headP)?.local || new THREE.Matrix4();
368
- return out;
369
- }
370
-
371
- // ----------------------------------------------------------------- environment
372
- groundAt(x, z) { const h = this.ground(x, z); return Number.isFinite(h) ? h : 0; }
373
- surfaceAt(x, z) {
374
- if (!this.water) return Infinity;
375
- const w = this.water(x, z);
376
- if (w !== null && w !== undefined && Number.isFinite(w)) this.surfaceY = w;
377
- return this.surfaceY;
378
- }
379
- // depth of the water column at (x, z): 0 where water(x, z) gives no surface (dry land); Infinity
380
- // without a water callback
381
- waterDepth(x, z) {
382
- if (!this.water) return Infinity;
383
- const w = this.water(x, z);
384
- if (w === null || w === undefined || !Number.isFinite(w)) return 0;
385
- return w - this.groundAt(x, z);
386
- }
387
- // the water a body of this size can swim in: deeper than its own height (centre to belly + back)
388
- swimDepth() { return 0.8 * (this.cBelow + this.cAbove) + this.cfg.clearance; }
389
- // heading (rad) toward water at least `need` deep: the candidate nearest to `h0` among 24 directions
390
- // at distance r (null when none)
391
- wetHeading(h0, r, need) {
392
- for (let k = 0; k <= 12; k++) {
393
- for (const sgn of k === 0 || k === 12 ? [1] : [1, -1]) {
394
- const h = h0 + sgn * k * (Math.PI / 12);
395
- if (this.waterDepth(this.pos.x + Math.sin(h) * r, this.pos.z + Math.cos(h) * r) >= need) return h;
396
- }
397
- }
398
- return null;
399
- }
400
- // the nearest water at least `need` deep: rings of 16 directions out to `rMax` (null when none)
401
- nearestWater(need, rMax) {
402
- const L = this.cfg.L;
403
- for (let r = 0.25 * L; r <= rMax; r *= 1.3) {
404
- let best = null, bd = -1;
405
- for (let k = 0; k < 16; k++) {
406
- const h = (k / 16) * Math.PI * 2;
407
- const d = this.waterDepth(this.pos.x + Math.sin(h) * r, this.pos.z + Math.cos(h) * r);
408
- if (d >= need && d > bd) { bd = d; best = h; }
409
- }
410
- if (best !== null) return { heading: best, dist: r };
411
- }
412
- return null;
413
- }
414
-
415
- // allowed range of the centre height at (x, z): bed + below-extent + clearance .. surface - above-extent - clearance
416
- depthRange(x, z, out) {
417
- const c = this.cfg, cl = c.clearance;
418
- const g = this.groundAt(x, z), s = this.surfaceAt(x, z);
419
- const below = this.cBelow, above = this.cAbove;
420
- out.lo = g + below + cl;
421
- out.hi = s - above - cl * 0.8;
422
- if (out.hi < out.lo) out.hi = out.lo = Math.max(g + below + cl * 0.2, Math.min(out.lo, s - above));
423
- return out;
424
- }
425
-
426
- reset(p, heading = this.heading) {
427
- const cfg = this.cfg;
428
- this.heading = heading; this.input.heading = heading;
429
- this.speed = 0; this.velocity.set(0, 0, 0); this.vy = 0; this.yawRate = 0;
430
- // centre of mass of the bind pose relative to the bind root, and the body's vertical extents
431
- this.poseLocal(0);
432
- let cy = 0;
433
- for (let i = 0; i < cfg.n; i++) cy += this.mass[i] * (cfg.chainJ[i].y + cfg.chainJ[i + 1].y) / 2;
434
- cy += this.mass[cfg.n] * (cfg.chainJ[0].y + cfg.J.snout.y) / 2;
435
- this.cBelow = cy - this.bindLow; this.cAbove = this.bindHigh - cy;
436
- this.pos.copy(p);
437
- const r = this.depthRange(p.x, p.z, {});
438
- const want = this.groundAt(p.x, p.z) + this.cBelow + cfg.spec.cruiseDepth * cfg.L;
439
- if (!(p.y > r.lo && p.y < r.hi)) this.pos.y = clamp(Math.min(want, r.hi), r.lo, Math.max(r.lo, r.hi));
440
- this.wantY = this.pos.y;
441
- this.pitchS.reset(0); this.rollS.reset(0); this.yawS.reset(0); this.bendS.reset(0); this.archS.reset(0);
442
- this.lift = 0; this.air = false;
443
- this.beached = !!this.water && this.waterDepth(p.x, p.z) < 0.5 * (this.cBelow + this.cAbove);
444
- if (this.beached) { this.pos.y = this.groundAt(p.x, p.z) + this.cBelow; this._strandSide = 1; }
445
- this._init = true;
446
- }
447
-
448
- // ----------------------------------------------------------------- public API
449
- play(name, opts) { return this.actionsLayer.play(name, opts || {}); }
450
- stop(name) { this.actionsLayer.stop(name); }
451
- setLod(level) { this.lod = clamp(level | 0, 0, 2); return this; }
452
- dispose() { this.actionsLayer.clear(); }
453
-
454
- update(dt) {
455
- if (!(dt > 1e-4) || !Number.isFinite(dt)) dt = 1e-4;
456
- dt = Math.min(dt, 1 / 20);
457
- this.frame++;
458
- this.time += dt;
459
- this.drive(dt);
460
- this.actionsLayer.update(dt);
461
- this.step(dt);
462
- this.pose(dt);
463
- this.publish();
464
- }
465
-
466
- // steering input -> wanted speed, heading and height
467
- drive(dt) {
468
- const inp = this.input, cfg = this.cfg, L = cfg.L;
469
- this.wantSpeed = clamp(inp.speed || 0, 0, cfg.maxSpeed);
470
- this.wantHeading = inp.heading ?? this.heading;
471
- this.wantPitch = clamp(inp.climb || 0, -1, 1) * cfg.spec.pitchMax;
472
- this.wantVy = clamp(inp.climb || 0, -1, 1) * cfg.hover;
473
- this.targetY = null;
474
- if (inp.target && !inp.follow) {
475
- const t = inp.target;
476
- const dx = t.x - this.pos.x, dz = t.z - this.pos.z, dy = t.y - this.pos.y;
477
- const d = Math.hypot(dx, dz, dy);
478
- const stopR = 0.4 * L + 0.05 * this.speed;
479
- if (d < stopR) { inp.target = null; inp.speed = 0; this.wantSpeed = 0; this.emit('arrive', {}); }
480
- else {
481
- const want = Math.atan2(dx, dz);
482
- inp.heading = want;
483
- this.wantHeading = want;
484
- this.wantPitch = clamp(Math.atan2(dy, Math.hypot(dx, dz)), -cfg.spec.pitchMax, cfg.spec.pitchMax);
485
- this.targetY = t.y;
486
- const brake = Math.sqrt(Math.max(0, 2 * cfg.decel * 0.7 * (d - stopR)));
487
- this.wantSpeed = Math.min(inp.targetSpeed || cfg.gears.cruise || L, cfg.maxSpeed, brake + 0.1 * L);
488
- }
489
- }
490
- if (this.actionsLayer.holdsStill()) {
491
- if (this.wantSpeed > 0.05 * L && !inp.follow) this.actionsLayer.requestStand();
492
- this.wantSpeed = 0; this.wantPitch = 0; this.wantVy = 0;
493
- this.wantHeading = this.heading;
494
- } else if (!inp.follow && this.wantSpeed < 0.05 * L) {
495
- // turn toward a look target behind
496
- const lt = inp.look;
497
- if (lt) {
498
- const want = Math.atan2(lt.x - this.pos.x, lt.z - this.pos.z);
499
- const off = angDiff(this.heading, want);
500
- if (Math.abs(off) > 0.7) this._turnTo = want;
501
- else if (this._turnTo !== undefined && Math.abs(off) < 0.2) this._turnTo = undefined;
502
- if (this._turnTo !== undefined) { this.wantHeading = this._turnTo; inp.heading = this._turnTo; }
503
- } else this._turnTo = undefined;
504
- }
505
- // obligate ram ventilators keep swimming: slow circles when asked to stop
506
- if (cfg.minSpeed > 0 && this.wantSpeed < cfg.minSpeed && !inp.follow && this.P.alive > 0.5) {
507
- this.wantSpeed = cfg.minSpeed;
508
- // circle at patrolRadius (the heading controller turns at 5 x the heading error)
509
- if (!inp.target) this.wantHeading = this.heading + (cfg.spec.patrolRadius > 0 ? Math.max(this.speed, cfg.minSpeed) / (cfg.spec.patrolRadius * L * 5) : 0.25);
510
- }
511
- this.avoidShore();
512
- }
513
-
514
- // shores: water too shallow for the body ahead (or dry land) turns the fish toward deep water and,
515
- // close in, slows it down so it can turn inside its turning radius. Only where water(x, z) ends or
516
- // the bed rises into the body's height (an open water column never triggers it).
517
- avoidShore() {
518
- if (!this.water || this.air || this.beached || this.input.follow) return;
519
- const L = this.cfg.L, need = this.swimDepth();
520
- const v = Math.max(this.speed, this.wantSpeed);
521
- const h = this.heading;
522
- const near = 0.6 * L + v * 0.6, far = 1.0 * L + v * 1.5;
523
- const dNear = this.waterDepth(this.pos.x + Math.sin(h) * near, this.pos.z + Math.cos(h) * near);
524
- const dFar = this.waterDepth(this.pos.x + Math.sin(h) * far, this.pos.z + Math.cos(h) * far);
525
- // also where the fish is asked to go
526
- const hw = this.wantHeading;
527
- const dWant = this.waterDepth(this.pos.x + Math.sin(hw) * far, this.pos.z + Math.cos(hw) * far);
528
- this.shore = 0;
529
- if (dFar >= need && dNear >= need && dWant >= need) return;
530
- const okWant = dWant >= need && this.waterDepth(this.pos.x + Math.sin(hw) * near, this.pos.z + Math.cos(hw) * near) >= need;
531
- const hNew = okWant ? hw : this.wetHeading(hw, far, need) ?? this.wetHeading(hw, near, need) ?? this.nearestWater(need, 6 * L)?.heading;
532
- if (hNew === null || hNew === undefined) return;
533
- this.wantHeading = hNew;
534
- this.input.heading = hNew;
535
- // close to the shore and still heading at it: brake to turn tighter
536
- const urgency = dNear < need ? 1 : dFar < need ? 0.5 : 0;
537
- this.shore = urgency;
538
- if (urgency > 0) this.wantSpeed = Math.min(this.wantSpeed, Math.max(this.cfg.minSpeed || 0, (urgency > 0.9 ? 0.4 : 0.9) * L));
539
- }
540
-
541
- // ----------------------------------------------------------------- simulation
542
- step(dt) {
543
- const cfg = this.cfg, inp = this.input, P = this.P, L = cfg.L, sp = cfg.spec;
544
- const rng = this._rng || (this._rng = {});
545
- // ---- follow: an external body places the fish
546
- if (inp.follow) {
547
- const fv = inp.follow.velocity, fp = inp.follow.position;
548
- const v3 = Math.hypot(fv.x, fv.y, fv.z);
549
- this.speed = v3;
550
- if (Math.hypot(fv.x, fv.z) > 0.02 * L) this.wantHeading = inp.follow.heading ?? Math.atan2(fv.x, fv.z);
551
- else if (inp.follow.heading !== null && inp.follow.heading !== undefined) this.wantHeading = inp.follow.heading;
552
- const err = angDiff(this.heading, this.wantHeading);
553
- const yr = clamp(err * 8, -sp.turnRate * 2, sp.turnRate * 2);
554
- this.yawRate = this.yawS.step(yr, 18, dt);
555
- this.heading += this.yawRate * dt;
556
- this.pitch = this.pitchS.step(v3 > 0.05 * L ? clamp(Math.atan2(fv.y, Math.hypot(fv.x, fv.z)), -sp.pitchMax, sp.pitchMax) : 0, 6, dt);
557
- // velocity feed-forward + a stiff pull toward the followed point (smooths a jittery body)
558
- const r = this.depthRange(fp.x, fp.z, rng);
559
- const ty = fp.y > r.lo && fp.y < r.hi ? fp.y : clamp(this.pos.y, r.lo, Math.max(r.lo, r.hi));
560
- const k = 1 - Math.exp(-dt * 25);
561
- this.pos.x += fv.x * dt; this.pos.z += fv.z * dt; this.pos.y += fv.y * dt;
562
- this.pos.x += (fp.x - this.pos.x) * k; this.pos.z += (fp.z - this.pos.z) * k; this.pos.y += (ty - this.pos.y) * (1 - Math.exp(-dt * 6));
563
- this.velocity.copy(fv);
564
- this.air = false;
565
- this.advancePhases(dt);
566
- return;
567
- }
568
- if (this.beached) { this.stepBeached(dt); return; }
569
- // ---- speed (with a smoothed acceleration: no jerk pops)
570
- const want = lerp(this.wantSpeed, P.speed, P.speedW);
571
- if (!this.air) {
572
- const dv = want - this.speed;
573
- const aCmd = clamp(dv * 6, -cfg.decel * (1 + 2 * P.brake), cfg.accel * (1 + P.accelK));
574
- this.accS += (aCmd - this.accS) * (1 - Math.exp(-dt * 14));
575
- this.speed = Math.max(0, this.speed + this.accS * dt);
576
- if (this.speed < 1e-4 && want <= 0) { this.speed = 0; if (this.accS < 0) this.accS = 0; }
577
- }
578
- // ---- heading: wanted yaw rate from the heading error, limited by the turning radius at speed
579
- let wantH = this.wantHeading;
580
- if (P.headingW > 0) wantH = this.wantHeading + angDiff(this.wantHeading, P.heading) * P.headingW;
581
- const err = angDiff(this.heading, wantH);
582
- const vmin = 0.6 * L;
583
- const yMax = Math.min(sp.turnRate * (1 + P.turnK), (Math.max(this.speed, vmin) / (sp.turnRadius * L)));
584
- const yr = clamp(err * 5, -yMax, yMax) + P.yawRate;
585
- this.yawRate = this.yawS.step(yr, 12 * (1 + P.turnK), dt);
586
- if (!this.air) this.heading += this.yawRate * dt;
587
- // ---- pitch and height: climb input, targets, clearance to the bed and the surface
588
- const r = this.depthRange(this.pos.x, this.pos.z, rng);
589
- let wantP = this.wantPitch;
590
- // look ahead: the bed rising in front makes the fish climb
591
- const la = L * 0.8 + this.speed * 0.5;
592
- const fx = Math.sin(this.heading), fz = Math.cos(this.heading);
593
- const r2 = this.depthRange(this.pos.x + fx * la, this.pos.z + fz * la, this._r2 || (this._r2 = {}));
594
- const lo = Math.max(r.lo, r2.lo), hi = Math.min(r.hi, r2.hi) < lo ? lo : Math.min(r.hi, r2.hi);
595
- if (this.targetY !== null) this.wantY = clamp(this.targetY, lo, hi);
596
- else if (Math.abs(this.wantVy) > 1e-4 || Math.abs(this.wantPitch) > 1e-3) this.wantY = this.pos.y;
597
- this.wantY = clamp(this.wantY ?? this.pos.y, lo, hi);
598
- // keep the height: pitch toward the wanted height at speed, hover up / down when slow
599
- const dyW = this.wantY - this.pos.y;
600
- const fast = smooth(0.15 * L, 0.8 * L, this.speed);
601
- if (Math.abs(this.wantPitch) < 1e-3) wantP = clamp(Math.atan2(dyW * 2, Math.max(L, this.speed * 1.2)), -0.5, 0.5) * fast;
602
- wantP = lerp(wantP, P.pitch, P.pitchW);
603
- // near the bed / surface limits, only allow pitching away from them
604
- if (this.pos.y <= lo + 0.02 * L) wantP = Math.max(wantP, 0);
605
- if (this.pos.y >= hi - 0.02 * L && !P.breach) wantP = Math.min(wantP, 0);
606
- this.pitch = this.air ? this.pitch : this.pitchS.step(wantP, 5 * (1 + P.pitchK), dt);
607
- // ---- velocity
608
- const cp = Math.cos(this.pitch), spn = Math.sin(this.pitch);
609
- const surf = this.surfaceAt(this.pos.x, this.pos.z);
610
- if (this.air) {
611
- // ballistic: the horizontal speed is kept, gravity bends the path, the body follows it
612
- this.vy -= 9.81 * dt;
613
- this.pitch = this.pitchS.step(Math.atan2(this.vy, Math.max(0.1 * L, this.vh)), 7, dt);
614
- } else {
615
- // hover: slow vertical swimming toward the wanted height (climb input when slow); settle / sink
616
- const hoverV = clamp(this.wantVy + dyW * 1.5 * (1 - fast), -cfg.hover, cfg.hover) * (1 - fast);
617
- const vyT = lerp(hoverV, P.vy, P.vyW);
618
- this.vy += (vyT - this.vy) * (1 - Math.exp(-dt * 4));
619
- }
620
- const hs = this.air ? this.vh : this.speed * cp;
621
- this.velocity.set(fx * hs, this.air ? this.vy : this.speed * spn + this.vy, fz * hs);
622
- this.velocity.x += P.push.x; this.velocity.z += P.push.z; this.velocity.y += P.push.y;
623
- const ox = this.pos.x, oz = this.pos.z;
624
- this.pos.addScaledVector(this.velocity, dt);
625
- // ---- shores: never swim into water shallower than the body (the move is undone; a push or an
626
- // action may still drive the fish there, it then stops at the edge)
627
- if (!this.air && this.water) {
628
- const need = this.swimDepth();
629
- const dN = this.waterDepth(this.pos.x, this.pos.z);
630
- if (dN < need && dN < this.waterDepth(ox, oz)) {
631
- this.pos.x = ox; this.pos.z = oz;
632
- this.speed *= Math.exp(-dt * 6);
633
- }
634
- }
635
- // ---- in / out of the water
636
- const top = this.pos.y + this.cAbove * 0.2;
637
- const gNow = this.groundAt(this.pos.x, this.pos.z);
638
- const wetNow = this.waterDepth(this.pos.x, this.pos.z);
639
- if (!this.air && top > surf && (P.breach || this.vy + this.speed * spn > 0.8 * L) && this.water) {
640
- this.air = true;
641
- this.vy = this.speed * spn + this.vy;
642
- this.vh = this.speed * cp;
643
- this.aimLeap(surf);
644
- this.emit('takeoff', { position: this.pos.clone(), speed: this.speed });
645
- } else if (this.air && this.pos.y - this.cBelow < gNow + 0.02 * L && wetNow < 0.5 * (this.cBelow + this.cAbove)) {
646
- // came down on dry ground (or in water too shallow to swim): beached
647
- this.air = false;
648
- this.beached = true;
649
- this.pos.y = Math.max(this.pos.y, gNow + this.cBelow);
650
- this.emit('beach', { position: this.pos.clone(), speed: Math.hypot(this.vy, this.vh) });
651
- this.speed = 0; this.vy = 0; this.vh = 0; this.accS = 0;
652
- this.actionsLayer.onLand?.();
653
- this._toWater = null; this._toWaterT = 0; this._strandSide = this.rand() < 0.5 ? -1 : 1;
654
- this.advancePhases(dt);
655
- return;
656
- } else if (this.air && this.pos.y < surf - this.cBelow * 0.3 && wetNow > 0) {
657
- this.air = false;
658
- this.emit('land', { position: this.pos.clone(), speed: Math.abs(this.vy), splash: true });
659
- this.actionsLayer.onLand?.();
660
- // continue along the current pitch with the same velocity; the water drag slows it (actions brake)
661
- const c2 = Math.max(0.3, Math.cos(this.pitch));
662
- this.speed = this.vh / c2;
663
- this.vy = this.vy - this.speed * Math.sin(this.pitch);
664
- this.accS = 0;
665
- }
666
- if (!this.air) {
667
- // soft limits: the controller should never get here, but a sinking corpse or a strong push can
668
- const rr = this.depthRange(this.pos.x, this.pos.z, rng);
669
- const loY = P.groundW > 0 ? lerp(rr.lo, rr.lo - cfg.clearance, P.groundW) : rr.lo;
670
- if (this.pos.y < loY) { this.pos.y += (loY - this.pos.y) * (1 - Math.exp(-dt * 10)); this.vy = Math.max(this.vy, 0); }
671
- if (this.pos.y > rr.hi && !P.breach && !P.surfaceOK) { this.pos.y += (rr.hi - this.pos.y) * (1 - Math.exp(-dt * 10)); this.vy = Math.min(this.vy, 0); }
672
- }
673
- // ---- bank into turns (+ action roll)
674
- const bank = clamp(-this.yawRate * Math.max(this.speed, 0.3 * L) / L * (sp.bankGain ?? 0.12), -sp.bank, sp.bank);
675
- this.roll = this.rollS.step(lerp(bank, P.roll, P.rollW), 4 * (1 + P.rollK), dt);
676
- this.advancePhases(dt);
677
- }
678
-
679
- // a leap out of the water comes down in water deep enough: the horizontal speed is cut so the
680
- // landing point (and half a body length past it) stays over deep water along the heading
681
- aimLeap(surf) {
682
- const L = this.cfg.L, g = 9.81, need = this.swimDepth();
683
- const yLand = surf - 0.3 * this.cBelow;
684
- const tF = (this.vy + Math.sqrt(Math.max(0, this.vy * this.vy + 2 * g * Math.max(0, this.pos.y - yLand)))) / g;
685
- if (!(tF > 1e-3) || !(this.vh > 0)) return;
686
- const fx = Math.sin(this.heading), fz = Math.cos(this.heading);
687
- const D = this.vh * tF, step = 0.2 * L;
688
- let ok = 0;
689
- for (let d = 0; d <= D + 1e-6; d += step) {
690
- if (this.waterDepth(this.pos.x + fx * (d + 0.5 * L), this.pos.z + fz * (d + 0.5 * L)) < need) break;
691
- ok = d;
692
- }
693
- if (ok < D) this.vh = Math.max(0, ok) / tF;
694
- }
695
-
696
- // beached: the body lies on the ground and flops (strong, slow tail beats, a rocking roll) while it
697
- // slides toward the nearest water; deep enough there, it swims on
698
- stepBeached(dt) {
699
- const cfg = this.cfg, L = cfg.L;
700
- const need = 0.6 * (this.cBelow + this.cAbove);
701
- this._toWaterT = (this._toWaterT || 0) - dt;
702
- if (!this._toWater || this._toWaterT <= 0) {
703
- this._toWater = this.nearestWater(need, 30 * L);
704
- this._toWaterT = 0.5;
705
- }
706
- const to = this._toWater;
707
- const alive = this.P.alive > 0.5;
708
- const flop = alive ? clamp(0.3 * L, 0.3, 1.2) : 0;
709
- if (to && alive) {
710
- const err = angDiff(this.heading, to.heading);
711
- this.yawRate = this.yawS.step(clamp(err * 2, -0.9, 0.9), 8, dt);
712
- this.heading += this.yawRate * dt;
713
- // slides toward the water (not only along the body: it flips and rolls its way there)
714
- this.pos.x += Math.sin(to.heading) * flop * dt;
715
- this.pos.z += Math.cos(to.heading) * flop * dt;
716
- } else this.yawRate = this.yawS.step(0, 8, dt);
717
- this.speed = flop;
718
- this.wantSpeed = 0;
719
- // lying on the ground (the skin clamp keeps it on the bed), level, rocking with the beats
720
- const g = this.groundAt(this.pos.x, this.pos.z);
721
- this.vy -= 9.81 * dt;
722
- this.pos.y += this.vy * dt;
723
- if (this.pos.y < g + this.cBelow) { this.pos.y = g + this.cBelow; this.vy = 0; }
724
- this.pitch = this.pitchS.step(0, 5, dt);
725
- const side = (cfg.spec.strandRoll ?? 1.3) * this._strandSide;
726
- this.roll = this.rollS.step(side + (alive ? 0.3 * Math.sin(this.phase * Math.PI * 2) : 0), 6, dt);
727
- this.velocity.set(to && alive ? Math.sin(to.heading) * flop : 0, 0, to && alive ? Math.cos(to.heading) * flop : 0);
728
- // back in the water
729
- if (this.waterDepth(this.pos.x, this.pos.z) >= need) {
730
- this.beached = false;
731
- this.speed = flop; this.vy = 0; this.accS = 0;
732
- this.wantY = this.pos.y;
733
- this.emit('land', { position: this.pos.clone(), speed: flop, splash: true });
734
- }
735
- this.advancePhases(dt);
736
- }
737
-
738
- advancePhases(dt) {
739
- const cfg = this.cfg, P = this.P, L = cfg.L, sp = cfg.spec;
740
- const vbl = this.speed / L;
741
- const row = this._row || (this._row = new Float64Array(6));
742
- tableAt6(sp.wave, vbl, row);
743
- const fl = this.beached && P.alive > 0.5 ? 1 : 0;
744
- const f = row[0] * P.freqK + P.freqAdd + fl * 1.2;
745
- this.freqS.step(f, 8, dt);
746
- this.ampS.step(fl ? Math.max(row[1] * 2.2, 0.45) : row[1] * P.ampK * (this.air ? 0.6 : 1), 6, dt);
747
- this.lamS.step(row[2], 4, dt);
748
- this.pectS.step(row[3] * P.pectK, 5, dt);
749
- this.pfS.step(row[4], 4, dt);
750
- this.foldS.step(clamp(row[5] + P.fold, 0, 1), 6, dt);
751
- this.phase += Math.max(0, this.freqS.x) * dt;
752
- if (this.phase > 1e4) this.phase -= 1e4;
753
- this.pPhase += this.pfS.x * dt * (0.4 + 0.6 * this.pectS.x);
754
- const bRate = sp.breath.rate * P.breathRate * (1 + P.stress * 0.9);
755
- this.bPhase += bRate * dt;
756
- this.stressDecay(dt);
757
- }
758
- stressDecay(dt) { this.P.stress *= 1; void dt; }
759
-
760
- // ----------------------------------------------------------------- pose
761
- // Local chain: segment angles -> joint positions (spine0 at the origin; body frame x = left, y = up,
762
- // z = forward) -> centred on the mass centre.
763
- poseLocal(dt) {
764
- const cfg = this.cfg, n = cfg.n, P = this.P, L = cfg.L, sp = cfg.spec;
765
- const th = this.th, ph = this.ph, yw = this.yw, Q = this.Q, m = this.mass;
766
- const amp = dt > 0 ? this.ampS.x : 0, lam = Math.max(0.3, this.lamS.x);
767
- const phi = this.phase;
768
- // lateral wave displacement at each joint (0 = spine0 ... n = tail tip)
769
- for (let j = 0; j <= n; j++) {
770
- const u = cfg.uJ[j];
771
- yw[j] = 0.5 * amp * L * cfg.envJ[j] * Math.sin(TAU * (phi - u / lam));
772
- }
773
- // the caudal fin is a fairly stiff plate hinged at the peduncle: past the caudal base the wave
774
- // gives way to the peduncle's own slope (a thunniform tail is fully stiff)
775
- const jb = cfg.caudalStart, cs = sp.caudalStiff ?? 0.6;
776
- if (jb > 0 && jb < n && cs > 0) {
777
- const slope = (yw[jb] - yw[jb - 1]) / cfg.segLen[jb - 1];
778
- for (let j = jb + 1; j <= n; j++) yw[j] += (yw[jb] + slope * (cfg.sJ[j] - cfg.sJ[jb]) - yw[j]) * cs;
779
- }
780
- // steering bend (+ action C-bend) distributed over the flexible body, and the action arch
781
- const bend = dt > 0 ? this.bendS.x : 0, arch = dt > 0 ? this.archS.x : 0;
782
- let cum = 0, mean = 0;
783
- for (let i = 0; i < n; i++) {
784
- const dy = (yw[i + 1] - yw[i]) / cfg.segLen[i];
785
- th[i] = Math.asin(clamp(dy, -0.95, 0.95)) + cfg.restYaw[i];
786
- cum += bend * cfg.bendW[i];
787
- th[i] += cum - bend * cfg.bendW[i] * 0.5;
788
- ph[i] = cfg.restPitch[i] - arch * cfg.bendW[i] * n * (cfg.uJ[i] - 0.45) * 1.2 + P.droop * cfg.bendW[i] * n * Math.max(0, cfg.uJ[i] - 0.5);
789
- mean += th[i] * m[i];
790
- }
791
- // head: continues the first trunk segment, turned by the look yaw
792
- const headTh0 = th[0] - (P.headWave * (yw[1] - yw[0])) / cfg.segLen[0];
793
- mean += headTh0 * m[n];
794
- for (let i = 0; i < n; i++) th[i] -= mean;
795
- this.headTh = headTh0 - mean + (dt > 0 ? this.lookYawS.x + P.headShake : 0);
796
- this.headPh = cfg.headPitch + (dt > 0 ? this.lookPitchS.x + this.headLiftS.x : 0) + arch * 0.15;
797
- // integrate joint positions (backward from spine0)
798
- Q[0].set(0, 0, 0);
799
- for (let i = 0; i < n; i++) {
800
- const c = Math.cos(ph[i]);
801
- Q[i + 1].set(Q[i].x + cfg.segLen[i] * Math.sin(th[i]) * c, Q[i].y + cfg.segLen[i] * Math.sin(ph[i]), Q[i].z - cfg.segLen[i] * Math.cos(th[i]) * c);
802
- }
803
- const hc = Math.cos(this.headPh);
804
- Q[n + 1].set(-cfg.headLen * Math.sin(this.headTh) * hc, cfg.headLen * Math.sin(this.headPh), cfg.headLen * Math.cos(this.headTh) * hc);
805
- // mass centre
806
- let cx = 0, cy = 0, cz = 0;
807
- for (let i = 0; i < n; i++) { cx += m[i] * (Q[i].x + Q[i + 1].x) * 0.5; cy += m[i] * (Q[i].y + Q[i + 1].y) * 0.5; cz += m[i] * (Q[i].z + Q[i + 1].z) * 0.5; }
808
- cx += m[n] * Q[n + 1].x * 0.5; cy += m[n] * Q[n + 1].y * 0.5; cz += m[n] * Q[n + 1].z * 0.5;
809
- for (let j = 0; j <= n + 1; j++) Q[j].set(Q[j].x - cx, Q[j].y - cy, Q[j].z - cz);
810
- this.bindCom = this.bindCom || new THREE.Vector3(cfg.chainJ[0].x + cx, cfg.chainJ[0].y + cy, cfg.chainJ[0].z + cz);
811
- void sp;
812
- }
813
-
814
- pose(dt) {
815
- const cfg = this.cfg, n = cfg.n, P = this.P, sp = cfg.spec, L = cfg.L;
816
- // ---- secondary springs
817
- const turnBend = clamp(this.yawRate / Math.max(this.speed, 0.5 * L) * L * 0.75, -sp.bend, sp.bend);
818
- this.bendS.step(turnBend * (1 - P.bendHold) + P.bend, 14 * (1 + P.bendK), dt);
819
- this.archS.step(P.arch, 7, dt);
820
- // look: the head turns only with the body; a little yaw from a slight bend, pitch from the body
821
- const lt = this.input.look || P.look;
822
- let lyaw = 0, lpit = 0;
823
- if (lt && P.lookW > 0) {
824
- const dx = lt.x - this.pos.x, dz = lt.z - this.pos.z, dy = lt.y - this.pos.y;
825
- lyaw = clamp(angDiff(this.heading, Math.atan2(dx, dz)), -0.6, 0.6) * 0.22 * P.lookW;
826
- lpit = clamp(Math.atan2(dy, Math.hypot(dx, dz)), -0.5, 0.5) * 0.15 * P.lookW;
827
- this.lookTarget.copy(lt);
828
- this.state.lookTarget = this.lookTarget;
829
- } else this.state.lookTarget = null;
830
- this.lookYawS.step(lyaw, 5, dt); this.lookPitchS.step(lpit, 5, dt);
831
- this.headLiftS.step(P.headLift, 14, dt);
832
- this.eyeRollS.step(P.eyeRoll, 22, dt); this.nictS.step(P.nictitate, 22, dt);
833
- this.poseLocal(dt);
834
- // ---- world basis: heading, pitch, roll
835
- const B = this.basis, h = this.heading, p = this.pitch, r = this.roll;
836
- const ch = Math.cos(h), shh = Math.sin(h), cp = Math.cos(p), spp = Math.sin(p);
837
- const F = B.F.set(shh * cp, spp, ch * cp);
838
- const L0 = _v1.set(ch, 0, -shh);
839
- const U0 = _v2.crossVectors(F, L0);
840
- const cr = Math.cos(r), sr = Math.sin(r);
841
- B.Lf.copy(L0).multiplyScalar(cr).addScaledVector(U0, sr);
842
- B.U.copy(U0).multiplyScalar(cr).addScaledVector(L0, -sr);
843
- const Q = this.Q, W = this.W, o = this.pos;
844
- for (let j = 0; j <= n + 1; j++) {
845
- const q = Q[j];
846
- W[j].set(o.x + q.x * B.Lf.x + q.y * B.U.x + q.z * F.x, o.y + q.x * B.Lf.y + q.y * B.U.y + q.z * F.y, o.z + q.x * B.Lf.z + q.y * B.U.z + q.z * F.z);
847
- }
848
- // lateral axes of the segments (body-local (cos th, 0, sin th)) and of the head
849
- for (let i = 0; i < n; i++) {
850
- const c = Math.cos(this.th[i]), s = Math.sin(this.th[i]);
851
- this.X[i].set(c * B.Lf.x + s * F.x, c * B.Lf.y + s * F.y, c * B.Lf.z + s * F.z);
852
- }
853
- {
854
- const c = Math.cos(this.headTh), s = Math.sin(this.headTh);
855
- this.X[n].set(c * B.Lf.x + s * F.x, c * B.Lf.y + s * F.y, c * B.Lf.z + s * F.z);
856
- }
857
- // ---- bed contact (lying, dead): conform the chain to the uneven bed
858
- if (P.conform > 0.001) this.conform(P.conform);
859
- // ---- write the chain
860
- for (let i = 0; i < n; i++) {
861
- this.D[i].copy(W[i + 1]).sub(W[i]);
862
- writeFrame(this.chainBoneObjs[i].matrixWorld, W[i], this.D[i], this.X[i]);
863
- }
864
- this.D[n].copy(W[n + 1]).sub(W[0]);
865
- writeFrame(this.bone.head.matrixWorld, W[0], this.D[n], this.X[n]);
866
- // ---- children: jaw, gill covers, fins
867
- this.poseChildren(dt);
868
- // ---- hard clamp: skin extents against the bed and the surface
869
- this.clampToWater();
870
- // ---- head pose (eyes, attachments)
871
- _m.multiplyMatrices(this.bone.head.matrixWorld, this._headLocal);
872
- this.headPose.position.setFromMatrixPosition(_m);
873
- this.headPose.quaternion.setFromRotationMatrix(this.bone.head.matrixWorld);
874
- const dbg = this.debug.targets;
875
- dbg[0].copy(W[n]); dbg[1].copy(W[n + 1]); dbg[2].copy(this.pos);
876
- if (this.state.lookTarget) dbg[3].copy(this.state.lookTarget);
877
- }
878
-
879
- conform(w) {
880
- const cfg = this.cfg, n = cfg.n, W = this.W;
881
- const dl = this._dl || (this._dl = new Float64Array(n + 2)), d2 = this._d2 || (this._d2 = new Float64Array(n + 2));
882
- // the body's lowest skin under each joint, from the bone extents in the current orientation
883
- for (let j = 0; j <= n + 1; j++) {
884
- const bi = j === n + 1 ? cfg.bIndex.head : cfg.bIndex[cfg.chainBones[Math.min(n - 1, j)]];
885
- const e = this.ext, oo = bi * 6;
886
- const X = j === n + 1 ? this.X[n] : this.X[Math.min(n - 1, j)];
887
- // local Z of the bone frame ~ X x Y; use the body up rotated with the roll
888
- const Z = _v3.crossVectors(X, j === n + 1 ? _v1.copy(W[n + 1]).sub(W[0]).normalize() : _v1.copy(W[Math.min(n, j + 1)]).sub(W[Math.min(n - 1, j)]).normalize());
889
- const below = Math.sqrt((e[oo + 1] * X.y) ** 2 + (e[oo + 5] * Z.y) ** 2) - (e[oo] * X.y + e[oo + 4] * Z.y);
890
- const g = this.groundAt(W[j].x, W[j].z);
891
- dl[j] = g + below + 0.004 * cfg.L - W[j].y;
892
- }
893
- // smooth the corrections along the chain (snout = n + 1 sits before spine0): no kinks
894
- for (let it = 0; it < 3; it++) {
895
- for (let j = 0; j <= n; j++) {
896
- const prev = j === 0 ? dl[n + 1] : dl[j - 1], next = j === n ? dl[n] : dl[j + 1];
897
- d2[j] = 0.25 * prev + 0.5 * dl[j] + 0.25 * next;
898
- }
899
- d2[n + 1] = 0.5 * dl[n + 1] + 0.5 * dl[0];
900
- for (let j = 0; j <= n + 1; j++) dl[j] = d2[j];
901
- }
902
- for (let j = 0; j <= n + 1; j++) W[j].y += dl[j] * w;
903
- }
904
-
905
- poseChildren(dt) {
906
- const cfg = this.cfg, P = this.P, sp = cfg.spec, L = cfg.L;
907
- const vbl = this.speed / L;
908
- // breathing: gill covers out, mouth open in counter-phase; fades with ram ventilation
909
- const ram = 1 - smooth(sp.breath.ram * 0.5, sp.breath.ram * 1.2, vbl) * 0.8;
910
- const b = Math.sin(TAU * this.bPhase);
911
- const bAmp = P.breathAmp * ram * (1 + P.stress * 0.8);
912
- const opT = (0.5 + 0.5 * b) * bAmp + P.opFlare;
913
- this.opS.step(opT, 18, dt);
914
- const gapeT = clamp((0.5 - 0.5 * b) * sp.breath.mouth * bAmp + P.gape, 0, 1.2);
915
- this.gapeS.step(gapeT, P.gapeFast ? 45 : 16, dt);
916
- this.protS.step(P.protrude, 30, dt);
917
- const D2R = Math.PI / 180;
918
- for (const k of this.kids) {
919
- const q = k.q.identity();
920
- k.t.set(0, 0, 0);
921
- if (k === this.jaw) {
922
- q.setFromAxisAngle(k.axes.open, this.gapeS.x * sp.jaw.gape * D2R);
923
- } else if (k === this.upperJaw) {
924
- const pd = sp.jaw.protrudeDir;
925
- if (pd) k.t.set(pd[0], pd[1], pd[2]); else k.t.set(0, -0.3, 1);
926
- k.t.normalize().multiplyScalar(this.protS.x * sp.jaw.protrude * L);
927
- } else if (k.axes.flare) {
928
- q.setFromAxisAngle(k.axes.flare, this.opS.x * sp.breath.operculum * D2R);
929
- } else if (k.axes.row) {
930
- // pectoral: sculling (flap + row + feather) when slow, folded at speed, flared to brake
931
- const pc = sp.pectoral, s = k.axes.s;
932
- const alt = s > 0 ? 0 : pc.alternate * 0.5;
933
- const ph = TAU * (this.pPhase + alt);
934
- const act = this.pectS.x * (1 - pc.stiff) * P.pectAmp;
935
- const fold = this.foldS.x;
936
- const turnIn = clamp(this.yawRate * s * 0.5, -1, 1); // inside fin of a turn flares
937
- const flare = clamp(this.flareS.step(Math.max(P.flare, P.brake * 0.9, turnIn * 0.5 * (1 - fold)), 10, dt), 0, 1.2);
938
- const ab = (pc.rest[0] * (1 - fold) - pc.fold * fold + pc.flap * act * Math.sin(ph) + (pc.flare - pc.rest[0]) * flare + P.pectAb) * D2R;
939
- const row = (pc.rest[1] + pc.row * act * Math.sin(ph + 1.3) + 20 * flare) * D2R;
940
- const fe = (pc.rest[2] + pc.feather * act * Math.cos(ph) + 30 * flare) * D2R;
941
- q.setFromAxisAngle(k.axes.ab, ab);
942
- q.multiply(_q.setFromAxisAngle(k.axes.row, row));
943
- q.multiply(_q.setFromAxisAngle(k.axes.long, fe * s));
944
- } else if (k.axes.ab) {
945
- // pelvic: spread when slow and braking, folded at speed
946
- const pv = sp.pelvic;
947
- const fold = this.foldS.x;
948
- if (k.axes.s > 0) this.pelS.step(20 * P.flare + P.pelvicAb, 8, dt);
949
- const ab = (pv.rest + pv.spread * (1 - fold) - pv.fold * fold + this.pelS.x + 3 * Math.sin(TAU * this.pPhase * 0.5) * this.pectS.x) * D2R;
950
- q.setFromAxisAngle(k.axes.ab, ab);
951
- }
952
- // M = parentWorld * A * T(t) * R * C0
953
- _m.makeRotationFromQuaternion(q);
954
- _m.multiply(k.C0);
955
- _m.setPosition(_m.elements[12] + k.t.x, _m.elements[13] + k.t.y, _m.elements[14] + k.t.z);
956
- k.bone.matrixWorld.multiplyMatrices(k.parent.matrixWorld, k.A).multiply(_m);
957
- }
958
- }
959
-
960
- // skin extents of every bone against the bed (and the surface when swimming)
961
- clampToWater() {
962
- const cfg = this.cfg, n = cfg.n, W = this.W, sk = this.sk, e = this.ext;
963
- if (this.lod >= 2 && (this.frame & 1)) { if (this.liftCarry) this.shiftAll(this.liftCarry); return; }
964
- // ground under the snout, the middle and the tail; interpolated along the body
965
- const gS = this.groundAt(W[n + 1].x, W[n + 1].z), gT = this.groundAt(W[n].x, W[n].z), gM = this.groundAt(this.pos.x, this.pos.z);
966
- const surf = this.air || this.beached ? Infinity : this.surfaceAt(this.pos.x, this.pos.z);
967
- const fx = W[n + 1].x - W[n].x, fz = W[n + 1].z - W[n].z;
968
- const fl2 = fx * fx + fz * fz || 1e-9;
969
- let need = -Infinity, over = -Infinity;
970
- const nb = this.nB;
971
- const margin = 0.002 * cfg.L;
972
- for (let b = 0; b < nb; b++) {
973
- const me = sk.bones[b].matrixWorld.elements, oo = b * 6;
974
- if (e[oo + 1] === 0 && e[oo + 5] === 0) continue;
975
- // centre of the skin box in world, and the vertical half extent (elliptic in X/Z, box along Y)
976
- const cx = e[oo], cy = e[oo + 2], cz = e[oo + 4];
977
- const wy = me[13] + cx * me[1] + cy * me[5] + cz * me[9];
978
- // chain bones: elliptic cross-section; fins and head parts: box (flat fans reach their corners)
979
- const half = this.isChain[b] ? Math.sqrt((e[oo + 1] * me[1]) ** 2 + (e[oo + 5] * me[9]) ** 2) + Math.abs(e[oo + 3] * me[5])
980
- : Math.abs(e[oo + 1] * me[1]) + Math.abs(e[oo + 3] * me[5]) + Math.abs(e[oo + 5] * me[9]);
981
- const wx = me[12] + cx * me[0] + cy * me[4] + cz * me[8], wz = me[14] + cx * me[2] + cy * me[6] + cz * me[10];
982
- const t = clamp(((wx - W[n].x) * fx + (wz - W[n].z) * fz) / fl2, 0, 1);
983
- // along the body the bed is interpolated from three samples; fins and head parts stick out
984
- // sideways (on a slope that matters when lying on the side), so they sample their own spot
985
- const g = !this.isChain[b] && this.lod === 0 ? this.groundAt(wx, wz) : t < 0.5 ? lerp(gT, gM, t * 2) : lerp(gM, gS, t * 2 - 1);
986
- const d = g + margin - (wy - half);
987
- if (d > need) need = d;
988
- const u = wy + half - (surf - margin);
989
- if (u > over) over = u;
990
- }
991
- let shift = 0;
992
- if (need > 0) shift = need;
993
- else if (over > 0 && !this.P.breach && !this.P.surfaceOK) shift = -Math.min(over, -need);
994
- this.liftCarry = shift;
995
- if (shift !== 0) {
996
- this.shiftAll(shift);
997
- this.pos.y += shift;
998
- if (shift > 0) this.vy = Math.max(this.vy, 0); else this.vy = Math.min(this.vy, 0);
999
- }
1000
- }
1001
- shiftAll(dy) {
1002
- for (const b of this.sk.bones) b.matrixWorld.elements[13] += dy;
1003
- for (const w of this.W) w.y += dy;
1004
- }
1005
-
1006
- // ----------------------------------------------------------------- state
1007
- publish() {
1008
- const st = this.state, cfg = this.cfg, L = cfg.L;
1009
- st.heading = this.heading;
1010
- st.speed = this.speed;
1011
- st.pitch = this.pitch; st.roll = this.roll;
1012
- const vbl = this.speed / L;
1013
- let g = 'stand';
1014
- if (vbl > 0.08) {
1015
- const G = cfg.gears;
1016
- const names = this.gaits;
1017
- g = names[0];
1018
- for (let i = 1; i < names.length; i++) if (this.speed > (G[names[i - 1]] + G[names[i]]) / 2) g = names[i];
1019
- }
1020
- st.stats.gait = g;
1021
- st.stats.freq = Math.max(0.05, this.freqS.x);
1022
- st.gait = this.air ? 'jump' : this.beached ? 'flop' : g;
1023
- st.grounded = !this.air;
1024
- st.inWater = !this.air && !this.beached;
1025
- st.beached = this.beached;
1026
- const s = this.surfaceY;
1027
- st.depth = Number.isFinite(s) ? s - this.pos.y : Infinity;
1028
- st.breaching = this.air || this.P.breach > 0.01 || this.P.surfaceOK > 0.01;
1029
- st.eyelid = 0;
1030
- st.eyeRoll = clamp(this.eyeRollS.x, 0, 1);
1031
- st.nictitate = clamp(this.nictS.x, 0, 1);
1032
- st.action = this.actionsLayer.current();
1033
- st.posture = this.actionsLayer.posture;
1034
- }
1035
- }
1036
-
1037
- // row lookup: rows [v, a, b, c, d, e, f] -> out[0..5], smoothstep blend
1038
- function tableAt6(rows, v, out) {
1039
- let i = 0;
1040
- while (i < rows.length - 2 && v > rows[i + 1][0]) i++;
1041
- const a = rows[i], b = rows[Math.min(i + 1, rows.length - 1)];
1042
- const t = b === a ? 0 : clamp((v - a[0]) / (b[0] - a[0]), 0, 1);
1043
- const s = t * t * (3 - 2 * t);
1044
- for (let k = 0; k < 6; k++) out[k] = lerp(a[k + 1], b[k + 1], k === 0 ? t : s);
1045
- return out;
1046
- }
1047
- void tv; void _v3; void _m2; void _q2;