@bitefoundry/retrieviewer 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ASSET-PROVENANCE.md +43 -0
- package/ASSETS-LICENSE.md +58 -0
- package/ATTRIBUTION.md +54 -0
- package/EMBEDDING.md +96 -0
- package/INSTALLING.md +126 -0
- package/LICENSE +16 -0
- package/LICENSE-CODE +21 -0
- package/PLUGIN.md +110 -0
- package/PUPPY-MEMORY.md +57 -0
- package/PUPPY-Q5-COMPARISON.md +73 -0
- package/README.md +86 -2
- package/RELEASING.md +70 -0
- package/THIRD-PARTY-NOTICES.md +17 -0
- package/TRADEMARKS.md +21 -0
- package/WINDOWS-COMPANION.md +113 -0
- package/braille.mjs +45 -0
- package/examples/bridge.py +62 -0
- package/examples/embed.mjs +15 -0
- package/examples/live.mjs +40 -0
- package/examples/plugin.mjs +21 -0
- package/integrations/opencode/runtime.mjs +195 -0
- package/integrations/opencode/tui.tsx +59 -0
- package/integrations/windows-overlay/Overlay.cs +259 -0
- package/integrations/windows-overlay/desktop.mjs +87 -0
- package/integrations/windows-overlay/protocol.mjs +39 -0
- package/integrations/windows-overlay/worker.mjs +60 -0
- package/locomotion-core.mjs +21 -0
- package/locomotion-puppy.animal +0 -0
- package/package.json +131 -4
- package/pet-q5-manifest.json +118 -0
- package/puppy-clean.mjs +51 -0
- package/puppy-companion-interactions-v3-q5.bin.gz +0 -0
- package/puppy-companion-interactions-v3.json +1 -0
- package/puppy-companion-life.mjs +182 -0
- package/puppy-companion-objects.mjs +30 -0
- package/puppy-companion-sprites.mjs +14 -0
- package/puppy-continuous.mjs +57 -0
- package/puppy-face.mjs +46 -0
- package/puppy-frame-predictive.mjs +147 -0
- package/puppy-frame-store.mjs +108 -0
- package/puppy-mouth-anchor.json +1 -0
- package/puppy-mouth-anchor.mjs +33 -0
- package/puppy-paper-actions-v9-q5.bin.gz +0 -0
- package/puppy-paper-actions-v9.json +1 -0
- package/puppy-paper-actions.mjs +48 -0
- package/puppy-paper-approach-q5.bin.gz +0 -0
- package/puppy-paper-approach.json +1 -0
- package/puppy-paper-area.mjs +15 -0
- package/puppy-paper-arrival-seat-q5.bin.gz +0 -0
- package/puppy-paper-arrival-seat.json +1 -0
- package/puppy-paper-behaviors-q5.bin.gz +0 -0
- package/puppy-paper-behaviors.json +1 -0
- package/puppy-paper-behaviors.mjs +8 -0
- package/puppy-paper-contact.mjs +103 -0
- package/puppy-paper-director.mjs +21 -0
- package/puppy-paper-paw-transport.mjs +76 -0
- package/puppy-paper-rear-gallop-q5.bin.gz +0 -0
- package/puppy-paper-rear-gallop.json +1 -0
- package/puppy-paper-seat-q5.bin.gz +0 -0
- package/puppy-paper-seat.json +14 -0
- package/puppy-paper-seat.mjs +13 -0
- package/puppy-paper-trick-timing.mjs +58 -0
- package/puppy-paper-world.mjs +108 -0
- package/puppy-paper.mjs +271 -0
- package/puppy-stable-meta.json +1606 -0
- package/puppy-stable.mjs +72 -0
- package/puppy-stride-meta.json +343 -0
- package/puppy-stride-tweens-q5.bin.gz +0 -0
- package/puppy-zoomies.mjs +27 -0
- package/puppy-zoomies30.mjs +12 -0
- package/retrieviewer-cli.mjs +94 -0
- package/retrieviewer-config.mjs +49 -0
- package/retrieviewer-install.mjs +157 -0
- package/retrieviewer-plugin.d.mts +44 -0
- package/retrieviewer-plugin.mjs +97 -0
- package/retrieviewer-stdio.mjs +38 -0
- package/retrieviewer.d.mts +85 -0
- package/retrieviewer.mjs +142 -0
- package/vendor/procedural-animals/LICENSE +21 -0
- package/vendor/procedural-animals/package.json +40 -0
- package/vendor/procedural-animals/src/animal.js +146 -0
- package/vendor/procedural-animals/src/bake.js +51 -0
- package/vendor/procedural-animals/src/build.js +81 -0
- package/vendor/procedural-animals/src/core/build/coatKit.js +121 -0
- package/vendor/procedural-animals/src/core/build/featherCards.js +189 -0
- package/vendor/procedural-animals/src/core/build/finEdges.js +85 -0
- package/vendor/procedural-animals/src/core/build/furSlope.js +115 -0
- package/vendor/procedural-animals/src/core/build/mesh.js +67 -0
- package/vendor/procedural-animals/src/core/build/pipeline.js +272 -0
- package/vendor/procedural-animals/src/core/build/seams.js +183 -0
- package/vendor/procedural-animals/src/core/build/warp.js +112 -0
- package/vendor/procedural-animals/src/core/build/weights.js +393 -0
- package/vendor/procedural-animals/src/core/math/vec.js +81 -0
- package/vendor/procedural-animals/src/core/motion/actions.js +800 -0
- package/vendor/procedural-animals/src/core/motion/bird.js +3063 -0
- package/vendor/procedural-animals/src/core/motion/birdActions.js +1059 -0
- package/vendor/procedural-animals/src/core/motion/common.js +158 -0
- package/vendor/procedural-animals/src/core/motion/index.js +22 -0
- package/vendor/procedural-animals/src/core/motion/quadruped.js +2875 -0
- package/vendor/procedural-animals/src/core/motion/snake.js +1594 -0
- package/vendor/procedural-animals/src/core/motion/spider.js +1810 -0
- package/vendor/procedural-animals/src/core/motion/swimmer.js +1047 -0
- package/vendor/procedural-animals/src/core/motion/swimmerActions.js +613 -0
- package/vendor/procedural-animals/src/core/render/animalObject.js +262 -0
- package/vendor/procedural-animals/src/core/render/coatMaterial.js +1301 -0
- package/vendor/procedural-animals/src/core/render/dqs.js +159 -0
- package/vendor/procedural-animals/src/core/render/eyes.js +676 -0
- package/vendor/procedural-animals/src/core/render/membranes.js +77 -0
- package/vendor/procedural-animals/src/core/render/skeleton.js +41 -0
- package/vendor/procedural-animals/src/core/rig/bird.js +327 -0
- package/vendor/procedural-animals/src/core/rig/quadruped.js +76 -0
- package/vendor/procedural-animals/src/core/rig/rig.js +79 -0
- package/vendor/procedural-animals/src/core/rig/snake.js +76 -0
- package/vendor/procedural-animals/src/core/rig/spider.js +110 -0
- package/vendor/procedural-animals/src/core/rig/swimmer.js +181 -0
- package/vendor/procedural-animals/src/core/sdf/eyeSocket.js +53 -0
- package/vendor/procedural-animals/src/core/sdf/mesher.js +238 -0
- package/vendor/procedural-animals/src/core/sdf/sdf.js +263 -0
- package/vendor/procedural-animals/src/index.d.ts +98 -0
- package/vendor/procedural-animals/src/index.js +29 -0
- package/vendor/procedural-animals/src/registry.js +48 -0
- package/vendor/procedural-animals/src/species/bear/behaviour.js +463 -0
- package/vendor/procedural-animals/src/species/bear/claws.js +147 -0
- package/vendor/procedural-animals/src/species/bear/coat.js +726 -0
- package/vendor/procedural-animals/src/species/bear/index.js +139 -0
- package/vendor/procedural-animals/src/species/bear/motion.js +112 -0
- package/vendor/procedural-animals/src/species/bear/regions.js +78 -0
- package/vendor/procedural-animals/src/species/bear/rig.js +112 -0
- package/vendor/procedural-animals/src/species/bear/sculpt.js +272 -0
- package/vendor/procedural-animals/src/species/boar/behaviour.js +335 -0
- package/vendor/procedural-animals/src/species/boar/coat.js +372 -0
- package/vendor/procedural-animals/src/species/boar/index.js +110 -0
- package/vendor/procedural-animals/src/species/boar/motion.js +85 -0
- package/vendor/procedural-animals/src/species/boar/regions.js +88 -0
- package/vendor/procedural-animals/src/species/boar/rig.js +111 -0
- package/vendor/procedural-animals/src/species/boar/sculpt.js +285 -0
- package/vendor/procedural-animals/src/species/cat/behaviour.js +559 -0
- package/vendor/procedural-animals/src/species/cat/coat.js +841 -0
- package/vendor/procedural-animals/src/species/cat/index.js +211 -0
- package/vendor/procedural-animals/src/species/cat/motion.js +101 -0
- package/vendor/procedural-animals/src/species/cat/regions.js +82 -0
- package/vendor/procedural-animals/src/species/cat/rig.js +100 -0
- package/vendor/procedural-animals/src/species/cat/sculpt.js +266 -0
- package/vendor/procedural-animals/src/species/cat/whiskers.js +110 -0
- package/vendor/procedural-animals/src/species/cheetah/coat.js +427 -0
- package/vendor/procedural-animals/src/species/cheetah/index.js +51 -0
- package/vendor/procedural-animals/src/species/cheetah/joints.js +3 -0
- package/vendor/procedural-animals/src/species/cheetah/motion.js +77 -0
- package/vendor/procedural-animals/src/species/cheetah/regions.js +95 -0
- package/vendor/procedural-animals/src/species/cheetah/rig.js +66 -0
- package/vendor/procedural-animals/src/species/cheetah/sculpt.js +158 -0
- package/vendor/procedural-animals/src/species/chicken/coat.js +313 -0
- package/vendor/procedural-animals/src/species/chicken/coverts.js +320 -0
- package/vendor/procedural-animals/src/species/chicken/index.js +110 -0
- package/vendor/procedural-animals/src/species/chicken/motion.js +177 -0
- package/vendor/procedural-animals/src/species/chicken/regions.js +66 -0
- package/vendor/procedural-animals/src/species/chicken/rig.js +144 -0
- package/vendor/procedural-animals/src/species/chicken/sculpt.js +389 -0
- package/vendor/procedural-animals/src/species/chicken/wingLook.js +66 -0
- package/vendor/procedural-animals/src/species/chicken/wingStack.js +124 -0
- package/vendor/procedural-animals/src/species/cow/behaviour.js +521 -0
- package/vendor/procedural-animals/src/species/cow/coat.js +465 -0
- package/vendor/procedural-animals/src/species/cow/index.js +118 -0
- package/vendor/procedural-animals/src/species/cow/motion.js +81 -0
- package/vendor/procedural-animals/src/species/cow/regions.js +98 -0
- package/vendor/procedural-animals/src/species/cow/rig.js +116 -0
- package/vendor/procedural-animals/src/species/cow/sculpt.js +339 -0
- package/vendor/procedural-animals/src/species/crow/coat.js +145 -0
- package/vendor/procedural-animals/src/species/crow/index.js +70 -0
- package/vendor/procedural-animals/src/species/crow/joints.js +3 -0
- package/vendor/procedural-animals/src/species/crow/motion.js +101 -0
- package/vendor/procedural-animals/src/species/crow/regions.js +50 -0
- package/vendor/procedural-animals/src/species/crow/rig.js +83 -0
- package/vendor/procedural-animals/src/species/crow/sculpt.js +118 -0
- package/vendor/procedural-animals/src/species/deer/behaviour.js +513 -0
- package/vendor/procedural-animals/src/species/deer/coat.js +391 -0
- package/vendor/procedural-animals/src/species/deer/index.js +106 -0
- package/vendor/procedural-animals/src/species/deer/motion.js +97 -0
- package/vendor/procedural-animals/src/species/deer/regions.js +60 -0
- package/vendor/procedural-animals/src/species/deer/rig.js +124 -0
- package/vendor/procedural-animals/src/species/deer/sculpt.js +297 -0
- package/vendor/procedural-animals/src/species/dog/behaviour.js +521 -0
- package/vendor/procedural-animals/src/species/dog/coat.js +495 -0
- package/vendor/procedural-animals/src/species/dog/index.js +144 -0
- package/vendor/procedural-animals/src/species/dog/motion.js +118 -0
- package/vendor/procedural-animals/src/species/dog/regions.js +61 -0
- package/vendor/procedural-animals/src/species/dog/rig.js +129 -0
- package/vendor/procedural-animals/src/species/dog/sculpt.js +246 -0
- package/vendor/procedural-animals/src/species/eagle/coat.js +250 -0
- package/vendor/procedural-animals/src/species/eagle/index.js +76 -0
- package/vendor/procedural-animals/src/species/eagle/joints.js +3 -0
- package/vendor/procedural-animals/src/species/eagle/motion.js +137 -0
- package/vendor/procedural-animals/src/species/eagle/regions.js +57 -0
- package/vendor/procedural-animals/src/species/eagle/rig.js +107 -0
- package/vendor/procedural-animals/src/species/eagle/sculpt.js +137 -0
- package/vendor/procedural-animals/src/species/fish/coat.js +271 -0
- package/vendor/procedural-animals/src/species/fish/index.js +88 -0
- package/vendor/procedural-animals/src/species/fish/motion.js +92 -0
- package/vendor/procedural-animals/src/species/fish/regions.js +27 -0
- package/vendor/procedural-animals/src/species/fish/rig.js +143 -0
- package/vendor/procedural-animals/src/species/fish/sculpt.js +214 -0
- package/vendor/procedural-animals/src/species/fish/variants.js +126 -0
- package/vendor/procedural-animals/src/species/fox/behaviour.js +314 -0
- package/vendor/procedural-animals/src/species/fox/coat.js +444 -0
- package/vendor/procedural-animals/src/species/fox/index.js +120 -0
- package/vendor/procedural-animals/src/species/fox/motion.js +81 -0
- package/vendor/procedural-animals/src/species/fox/regions.js +59 -0
- package/vendor/procedural-animals/src/species/fox/rig.js +91 -0
- package/vendor/procedural-animals/src/species/fox/sculpt.js +202 -0
- package/vendor/procedural-animals/src/species/frog/behaviour.js +590 -0
- package/vendor/procedural-animals/src/species/frog/coat.js +305 -0
- package/vendor/procedural-animals/src/species/frog/index.js +97 -0
- package/vendor/procedural-animals/src/species/frog/motion.js +69 -0
- package/vendor/procedural-animals/src/species/frog/regions.js +46 -0
- package/vendor/procedural-animals/src/species/frog/rig.js +156 -0
- package/vendor/procedural-animals/src/species/frog/sculpt.js +240 -0
- package/vendor/procedural-animals/src/species/goat/behaviour.js +509 -0
- package/vendor/procedural-animals/src/species/goat/coat.js +590 -0
- package/vendor/procedural-animals/src/species/goat/index.js +149 -0
- package/vendor/procedural-animals/src/species/goat/motion.js +95 -0
- package/vendor/procedural-animals/src/species/goat/regions.js +98 -0
- package/vendor/procedural-animals/src/species/goat/rig.js +181 -0
- package/vendor/procedural-animals/src/species/goat/sculpt.js +547 -0
- package/vendor/procedural-animals/src/species/horse/coat.js +361 -0
- package/vendor/procedural-animals/src/species/horse/coatSeam.js +122 -0
- package/vendor/procedural-animals/src/species/horse/index.js +80 -0
- package/vendor/procedural-animals/src/species/horse/motion.js +245 -0
- package/vendor/procedural-animals/src/species/horse/regions.js +60 -0
- package/vendor/procedural-animals/src/species/horse/rig.js +117 -0
- package/vendor/procedural-animals/src/species/horse/sculpt.js +316 -0
- package/vendor/procedural-animals/src/species/horse/seamWeights.js +272 -0
- package/vendor/procedural-animals/src/species/lion/behaviour.js +509 -0
- package/vendor/procedural-animals/src/species/lion/coat.js +1016 -0
- package/vendor/procedural-animals/src/species/lion/index.js +111 -0
- package/vendor/procedural-animals/src/species/lion/motion.js +87 -0
- package/vendor/procedural-animals/src/species/lion/regions.js +106 -0
- package/vendor/procedural-animals/src/species/lion/rig.js +110 -0
- package/vendor/procedural-animals/src/species/lion/sculpt.js +384 -0
- package/vendor/procedural-animals/src/species/lion/whiskers.js +132 -0
- package/vendor/procedural-animals/src/species/pig/behaviour.js +575 -0
- package/vendor/procedural-animals/src/species/pig/coat.js +730 -0
- package/vendor/procedural-animals/src/species/pig/ears.js +242 -0
- package/vendor/procedural-animals/src/species/pig/index.js +161 -0
- package/vendor/procedural-animals/src/species/pig/motion.js +105 -0
- package/vendor/procedural-animals/src/species/pig/regions.js +100 -0
- package/vendor/procedural-animals/src/species/pig/rig.js +152 -0
- package/vendor/procedural-animals/src/species/pig/sculpt.js +382 -0
- package/vendor/procedural-animals/src/species/rabbit/behaviour.js +605 -0
- package/vendor/procedural-animals/src/species/rabbit/coat.js +285 -0
- package/vendor/procedural-animals/src/species/rabbit/index.js +85 -0
- package/vendor/procedural-animals/src/species/rabbit/motion.js +85 -0
- package/vendor/procedural-animals/src/species/rabbit/regions.js +48 -0
- package/vendor/procedural-animals/src/species/rabbit/rig.js +99 -0
- package/vendor/procedural-animals/src/species/rabbit/sculpt.js +148 -0
- package/vendor/procedural-animals/src/species/rat/behaviour.js +539 -0
- package/vendor/procedural-animals/src/species/rat/coat.js +341 -0
- package/vendor/procedural-animals/src/species/rat/index.js +111 -0
- package/vendor/procedural-animals/src/species/rat/motion.js +90 -0
- package/vendor/procedural-animals/src/species/rat/regions.js +92 -0
- package/vendor/procedural-animals/src/species/rat/rig.js +112 -0
- package/vendor/procedural-animals/src/species/rat/sculpt.js +163 -0
- package/vendor/procedural-animals/src/species/rat/whiskers.js +119 -0
- package/vendor/procedural-animals/src/species/shark/coat.js +383 -0
- package/vendor/procedural-animals/src/species/shark/index.js +104 -0
- package/vendor/procedural-animals/src/species/shark/motion.js +69 -0
- package/vendor/procedural-animals/src/species/shark/regions.js +34 -0
- package/vendor/procedural-animals/src/species/shark/rig.js +194 -0
- package/vendor/procedural-animals/src/species/shark/sculpt.js +343 -0
- package/vendor/procedural-animals/src/species/shark/variants.js +105 -0
- package/vendor/procedural-animals/src/species/sheep/behaviour.js +522 -0
- package/vendor/procedural-animals/src/species/sheep/coat.js +529 -0
- package/vendor/procedural-animals/src/species/sheep/index.js +152 -0
- package/vendor/procedural-animals/src/species/sheep/motion.js +86 -0
- package/vendor/procedural-animals/src/species/sheep/neck.js +41 -0
- package/vendor/procedural-animals/src/species/sheep/regions.js +62 -0
- package/vendor/procedural-animals/src/species/sheep/rig.js +163 -0
- package/vendor/procedural-animals/src/species/sheep/sculpt.js +542 -0
- package/vendor/procedural-animals/src/species/snake/coat.js +315 -0
- package/vendor/procedural-animals/src/species/snake/index.js +73 -0
- package/vendor/procedural-animals/src/species/snake/motion.js +51 -0
- package/vendor/procedural-animals/src/species/snake/regions.js +51 -0
- package/vendor/procedural-animals/src/species/snake/rig.js +108 -0
- package/vendor/procedural-animals/src/species/snake/sculpt.js +122 -0
- package/vendor/procedural-animals/src/species/spider/coat.js +279 -0
- package/vendor/procedural-animals/src/species/spider/index.js +78 -0
- package/vendor/procedural-animals/src/species/spider/motion.js +72 -0
- package/vendor/procedural-animals/src/species/spider/regions.js +87 -0
- package/vendor/procedural-animals/src/species/spider/rig.js +118 -0
- package/vendor/procedural-animals/src/species/spider/sculpt.js +182 -0
- package/vendor/procedural-animals/src/species/wolf/behaviour.js +160 -0
- package/vendor/procedural-animals/src/species/wolf/coat.js +749 -0
- package/vendor/procedural-animals/src/species/wolf/index.js +107 -0
- package/vendor/procedural-animals/src/species/wolf/motion.js +80 -0
- package/vendor/procedural-animals/src/species/wolf/regions.js +57 -0
- package/vendor/procedural-animals/src/species/wolf/rig.js +87 -0
- package/vendor/procedural-animals/src/species/wolf/sculpt.js +339 -0
- package/vendor/procedural-animals/src/worker.js +29 -0
|
@@ -0,0 +1,1047 @@
|
|
|
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;
|