@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,1594 @@
|
|
|
1
|
+
// Snake motion engine: path-following lateral undulation over uneven terrain, a curvature-space pose
|
|
2
|
+
// layer for everything that leaves the path (raised forebody, strike S-coil, flinch, writhing), a
|
|
3
|
+
// scripted-path coil, head / jaw / tongue / fang / rattle control, and the snake action set.
|
|
4
|
+
//
|
|
5
|
+
// Locomotion. A virtual leader (the occiput, joint v0) moves along a serpentine path: a centre point
|
|
6
|
+
// C travels at the commanded speed and heading, and the leader swings about it,
|
|
7
|
+
// leader = C + left(heading) * A * sin(phi), phi += 2 pi dx / lambda
|
|
8
|
+
// with amplitude A and wavelength lambda from the gait table (springs in the travelled distance, so
|
|
9
|
+
// the path is C1 whatever the speed does; A starts from 0 when a snake sets off, so the new path
|
|
10
|
+
// leaves exactly along its neck). The leader lays a trail on the terrain (a ring buffer of ground
|
|
11
|
+
// points parameterised by arc length); every spine joint sits on the trail at its arc distance behind
|
|
12
|
+
// the leader (non-uniform Catmull-Rom interpolation), lifted along the ground normal by its axis
|
|
13
|
+
// height. So the body follows the head's path exactly (no lateral slip), hugs the ground and cannot
|
|
14
|
+
// float or sink. Turns are rate limited (min radius, and so the leader never moves backwards).
|
|
15
|
+
// state.position is C (the smooth centre of the wave, the point games follow / steer).
|
|
16
|
+
//
|
|
17
|
+
// Pose layer. Actions write, per joint, a lift above the ground, a yaw offset per segment and a roll.
|
|
18
|
+
// The body is then re-integrated segment by segment from the head in the horizontal plane (exact
|
|
19
|
+
// segment lengths: a lifted segment gets shorter horizontally) and translated so that the joints the
|
|
20
|
+
// actions leave alone stay exactly on the trail (weighted fit). Heights always come from the terrain:
|
|
21
|
+
// y = ground + axis height (+ lift), so lifted parts never go below it. A coil is not a pose but a
|
|
22
|
+
// scripted path: the leader crawls an inward Archimedean spiral (curvature integrated along arc
|
|
23
|
+
// length) and the body follows it into a flat coil. When a posture that changed the body shape ends
|
|
24
|
+
// ('stand'), the current shape is baked into the trail, so the snake crawls off along its own body.
|
|
25
|
+
// Self-contact: a part of the body that crosses over another rides on top of it (anterior over
|
|
26
|
+
// posterior), with a smooth ramp along the chain.
|
|
27
|
+
//
|
|
28
|
+
// Lessons kept from the cheetah: continuous solves (nothing jumps between frames; weights and poses
|
|
29
|
+
// are smooth functions of time), motion from phase (the wave is a function of travelled distance),
|
|
30
|
+
// springs for secondary motion (head, jaw), zero-dt safety, no allocations in hot paths.
|
|
31
|
+
//
|
|
32
|
+
// Bones (core/rig/snake.js): head (v0 -> nose), jaw, tongue, tongueTip, fang, spine0..spineN-1
|
|
33
|
+
// (v0 -> vN). Joint vi's bind height is its axis height above the belly.
|
|
34
|
+
//
|
|
35
|
+
// ------------------------------------------------------------------------------------------------
|
|
36
|
+
// species.motion schema (reference size; lengths x total length TL where marked)
|
|
37
|
+
// ------------------------------------------------------------------------------------------------
|
|
38
|
+
// maxSpeed m/s, accel m/s^2, decel m/s^2, turnRate rad/s, minTurnRadius (x TL)
|
|
39
|
+
// gears { name: speed } named speeds (crawl, slither, dash ...)
|
|
40
|
+
// gaits [{ name, v, wave (x TL), amp (x TL), headLift (m), stab (0..1 head yaw stabilisation) }]
|
|
41
|
+
// sorted by v; rows are blended by speed
|
|
42
|
+
// body { neck: x TL (part raised to look around), forebody: x TL (part used by strikes) }
|
|
43
|
+
// tongue { rate: flicks per s at idle, length: extension (x head length), flicks: [min, max] tip
|
|
44
|
+
// oscillations per flick, freq: tip oscillation Hz }
|
|
45
|
+
// breath { rate Hz, roll rad }
|
|
46
|
+
// actions { strike: { reach (x TL), speed (m/s), gape (deg), coilHeight (x TL), rise (x TL) },
|
|
47
|
+
// coil: { pitch (ring spacing / body width), speed (m/s) }, sit: { raise (x TL) },
|
|
48
|
+
// eat: { swallow (s) }, drink: { pumps } }
|
|
49
|
+
// variants { <params.variant>: { speedK, ampK, strike: {...}, rattle: bool } } per-variant overrides
|
|
50
|
+
// ------------------------------------------------------------------------------------------------
|
|
51
|
+
// Contract extras: state.contacts = [{ key: 'v<i>', contact, position }] (ground contact of every
|
|
52
|
+
// spine joint; contact = lying on the ground), motion.chain (spine bone indices, head -> tail),
|
|
53
|
+
// state.stats.freq (undulation frequency), state.posture, events attackHit / actionStart /
|
|
54
|
+
// actionEnd / death / arrive. Actions: idle, stand, sit (alert coil, head raised), lie (stretch out),
|
|
55
|
+
// sleep (resting coil), coil, strike = attack, jump (a forward lunge), eat (strike + swallow), drink,
|
|
56
|
+
// hit (flinch and whip away), death (writhe, go limp, roll half belly-up).
|
|
57
|
+
import * as THREE from 'three';
|
|
58
|
+
import { TAU, clamp, lerp, smooth, angDiff, Spring, prng, writeFrame } from './common.js';
|
|
59
|
+
|
|
60
|
+
const DEFAULTS = {
|
|
61
|
+
maxSpeed: 0.8, accel: 0.9, decel: 1.6, turnRate: 2.2, minTurnRadius: 0.1,
|
|
62
|
+
body: { neck: 0.12, forebody: 0.3 },
|
|
63
|
+
tongue: { rate: 0.6, length: 0.6, flicks: [3, 6], freq: 7.5 },
|
|
64
|
+
breath: { rate: 0.28, roll: 0.03 },
|
|
65
|
+
};
|
|
66
|
+
const STRIKE = { reach: 0.33, speed: 2.8, gape: 75, coilHeight: 0.07, rise: 0.12 };
|
|
67
|
+
const POSTURES = ['sit', 'lie', 'sleep', 'death', 'coil'];
|
|
68
|
+
const COILED = { sit: 1, sleep: 1, coil: 1 };
|
|
69
|
+
|
|
70
|
+
const _m1 = new THREE.Matrix4(), _m2 = new THREE.Matrix4(), _m3 = new THREE.Matrix4();
|
|
71
|
+
const _q = new THREE.Quaternion(), _q2 = new THREE.Quaternion();
|
|
72
|
+
const _v = new THREE.Vector3(), _v2 = new THREE.Vector3(), _v3 = new THREE.Vector3(), _v4 = new THREE.Vector3();
|
|
73
|
+
const h2 = (a, b) => Math.sqrt(a * a + b * b);
|
|
74
|
+
const h3 = (a, b, c) => Math.sqrt(a * a + b * b + c * c);
|
|
75
|
+
const _X = new THREE.Vector3(1, 0, 0), _Y = new THREE.Vector3(0, 1, 0), _Z = new THREE.Vector3(0, 0, 1);
|
|
76
|
+
|
|
77
|
+
// ------------------------------------------------------------------------------------------------
|
|
78
|
+
// Trail: ground points by arc length (ring buffer), newest point = the leader
|
|
79
|
+
// ------------------------------------------------------------------------------------------------
|
|
80
|
+
class Trail {
|
|
81
|
+
constructor(cap) {
|
|
82
|
+
this.cap = cap;
|
|
83
|
+
this.x = new Float64Array(cap); this.y = new Float64Array(cap); this.z = new Float64Array(cap); this.s = new Float64Array(cap);
|
|
84
|
+
this.o = 0; this.n = 0; this.ver = 0; this.hit = 0;
|
|
85
|
+
}
|
|
86
|
+
clear() { this.o = 0; this.n = 0; this.ver++; }
|
|
87
|
+
// linear storage: logical k lives at o + k; when the end is reached the newest half moves to the front
|
|
88
|
+
idx(k) { return this.o + k; }
|
|
89
|
+
push(x, y, z, s) {
|
|
90
|
+
this.ver++;
|
|
91
|
+
if (this.o + this.n === this.cap) {
|
|
92
|
+
const keep = this.n >> 1, from = this.o + this.n - keep;
|
|
93
|
+
this.x.copyWithin(0, from, from + keep); this.y.copyWithin(0, from, from + keep);
|
|
94
|
+
this.z.copyWithin(0, from, from + keep); this.s.copyWithin(0, from, from + keep);
|
|
95
|
+
this.o = 0; this.n = keep;
|
|
96
|
+
}
|
|
97
|
+
const i = this.o + this.n;
|
|
98
|
+
this.x[i] = x; this.y[i] = y; this.z[i] = z; this.s[i] = s;
|
|
99
|
+
this.n++;
|
|
100
|
+
}
|
|
101
|
+
get lastS() { return this.s[this.o + this.n - 1]; }
|
|
102
|
+
// leader moved: update the provisional newest point, commit it once it is `spacing` from the one before
|
|
103
|
+
lead(x, y, z, spacing) {
|
|
104
|
+
const a = this.o + this.n - 1, b = a - 1;
|
|
105
|
+
const ax = x - this.x[a], ay = y - this.y[a], az = z - this.z[a];
|
|
106
|
+
if (ax * ax + ay * ay + az * az < 1e-18) return;
|
|
107
|
+
const bx = x - this.x[b], by = y - this.y[b], bz = z - this.z[b];
|
|
108
|
+
const db = Math.sqrt(bx * bx + by * by + bz * bz);
|
|
109
|
+
if (db >= spacing) this.push(x, y, z, this.s[a] + Math.sqrt(ax * ax + ay * ay + az * az));
|
|
110
|
+
else { this.x[a] = x; this.y[a] = y; this.z[a] = z; this.s[a] = this.s[b] + db; this.ver++; }
|
|
111
|
+
}
|
|
112
|
+
// position at arc s (Hermite with finite-difference tangents: C1 for any spacing); beyond the
|
|
113
|
+
// oldest point the trail is extended straight. `hint`: a nearby logical segment (walk from it).
|
|
114
|
+
sample(s, out, hint = -1) {
|
|
115
|
+
const n = this.n, o = this.o, X = this.x, Y = this.y, Z = this.z, S = this.s;
|
|
116
|
+
this.hit = 0;
|
|
117
|
+
if (s <= S[o]) {
|
|
118
|
+
const i0 = o, i1 = o + 1, k = (s - S[i0]) / (S[i1] - S[i0] || 1e-9);
|
|
119
|
+
out[0] = X[i0] + (X[i1] - X[i0]) * k; out[1] = Y[i0] + (Y[i1] - Y[i0]) * k; out[2] = Z[i0] + (Z[i1] - Z[i0]) * k;
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
const iN = o + n - 1;
|
|
123
|
+
if (s >= S[iN]) { out[0] = X[iN]; out[1] = Y[iN]; out[2] = Z[iN]; this.hit = n - 2; return out; }
|
|
124
|
+
let lo;
|
|
125
|
+
if (hint >= 0 && hint < n - 1) {
|
|
126
|
+
lo = hint;
|
|
127
|
+
while (lo > 0 && S[o + lo] > s) lo--;
|
|
128
|
+
while (lo < n - 2 && S[o + lo + 1] <= s) lo++;
|
|
129
|
+
} else {
|
|
130
|
+
lo = 0; let hi = n - 1;
|
|
131
|
+
while (hi - lo > 1) { const md = (lo + hi) >> 1; if (S[o + md] <= s) lo = md; else hi = md; }
|
|
132
|
+
}
|
|
133
|
+
this.hit = lo;
|
|
134
|
+
const a = o + lo, b = a + 1;
|
|
135
|
+
const h = S[b] - S[a] || 1e-9, u = (s - S[a]) / h;
|
|
136
|
+
// tangents per unit arc at a and b from their neighbours
|
|
137
|
+
const a0 = lo > 0 ? a - 1 : a, b1 = lo + 2 < n ? b + 1 : b;
|
|
138
|
+
const da = S[b] - S[a0] || 1e-9, db = S[b1] - S[a] || 1e-9;
|
|
139
|
+
const u2 = u * u, u3 = u2 * u;
|
|
140
|
+
const h00 = 2 * u3 - 3 * u2 + 1, h10 = ((u3 - 2 * u2 + u) * h) / da, h01 = -2 * u3 + 3 * u2, h11 = ((u3 - u2) * h) / db;
|
|
141
|
+
out[0] = h00 * X[a] + h10 * (X[b] - X[a0]) + h01 * X[b] + h11 * (X[b1] - X[a]);
|
|
142
|
+
out[1] = h00 * Y[a] + h10 * (Y[b] - Y[a0]) + h01 * Y[b] + h11 * (Y[b1] - Y[a]);
|
|
143
|
+
out[2] = h00 * Z[a] + h10 * (Z[b] - Z[a0]) + h01 * Z[b] + h11 * (Z[b1] - Z[a]);
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const S3 = new Float64Array(3);
|
|
148
|
+
|
|
149
|
+
// ------------------------------------------------------------------------------------------------
|
|
150
|
+
export function createSnakeMotion(ctx) { return new SnakeMotion(ctx); }
|
|
151
|
+
|
|
152
|
+
export class SnakeMotion {
|
|
153
|
+
constructor({ species, data, skeleton, ground, water, position, heading = 0, emit }) {
|
|
154
|
+
this.species = species;
|
|
155
|
+
this.sk = skeleton;
|
|
156
|
+
this.ground = ground || (() => 0);
|
|
157
|
+
this.water = water || null;
|
|
158
|
+
this.emit = emit || (() => {});
|
|
159
|
+
this.rand = prng((data.seed || 1) * 7919 + 29);
|
|
160
|
+
this.seed = data.seed || 1;
|
|
161
|
+
const params = data.params || {};
|
|
162
|
+
const spec = species.motion || {};
|
|
163
|
+
const vk = (spec.variants && spec.variants[params.variant]) || {};
|
|
164
|
+
this.viper = params.variant === 'rattlesnake';
|
|
165
|
+
// ---- skeleton
|
|
166
|
+
const J = {};
|
|
167
|
+
for (const [k, v] of Object.entries(data.joints)) J[k] = new THREE.Vector3(v[0], v[1], v[2]);
|
|
168
|
+
this.J = J;
|
|
169
|
+
let N = 0;
|
|
170
|
+
while (J['v' + (N + 1)]) N++;
|
|
171
|
+
this.N = N;
|
|
172
|
+
const bi = {};
|
|
173
|
+
data.bones.forEach((b, i) => { bi[b.name] = i; });
|
|
174
|
+
this.bi = bi;
|
|
175
|
+
this.spine = Array.from({ length: N }, (_, i) => skeleton.bones[bi['spine' + i]]);
|
|
176
|
+
this.chain = Array.from({ length: N }, (_, i) => bi['spine' + i]);
|
|
177
|
+
this.bHead = skeleton.bones[bi.head]; this.bJaw = skeleton.bones[bi.jaw];
|
|
178
|
+
this.bTongue = skeleton.bones[bi.tongue]; this.bTip = skeleton.bones[bi.tongueTip]; this.bFang = skeleton.bones[bi.fang];
|
|
179
|
+
const bind = (n) => skeleton.bind[bi[n]];
|
|
180
|
+
this.bindHead = bind('head'); this.bindJaw = bind('jaw'); this.bindTongue = bind('tongue'); this.bindTip = bind('tongueTip'); this.bindFang = bind('fang');
|
|
181
|
+
// ---- dimensions
|
|
182
|
+
const s = params.size || 1;
|
|
183
|
+
this.s = s;
|
|
184
|
+
this.sq = Math.sqrt(s);
|
|
185
|
+
this.len = new Float64Array(N); this.sig = new Float64Array(N + 1); this.r = new Float64Array(N + 1); this.hw = new Float64Array(N + 1);
|
|
186
|
+
const flat = spec.flat || (params.variant === 'rattlesnake' ? 0.64 : 0.74);
|
|
187
|
+
for (let i = 0; i < N; i++) { this.len[i] = J['v' + i].distanceTo(J['v' + (i + 1)]); this.sig[i + 1] = this.sig[i] + this.len[i]; }
|
|
188
|
+
for (let i = 0; i <= N; i++) { this.r[i] = J['v' + i].y; this.hw[i] = this.r[i] / flat; }
|
|
189
|
+
this.bodyLen = this.sig[N];
|
|
190
|
+
// support of each body cross-section toward the ground as a function of roll, measured on the
|
|
191
|
+
// bind mesh (vertices skinned mostly to the bone that starts at the joint): the body can lie on its
|
|
192
|
+
// side or back on any terrain without sinking in or floating
|
|
193
|
+
{
|
|
194
|
+
const NA = (this.supN = 48), tab = (this.supTab = new Float32Array((N + 1) * NA)), idx = new Int16Array(data.bones.length).fill(-1);
|
|
195
|
+
for (let i = 0; i < N; i++) idx[bi['spine' + i]] = i;
|
|
196
|
+
for (let v = 0; v < data.nV; v++) {
|
|
197
|
+
if (data.skinWeight[v * 4] < 0.4) continue;
|
|
198
|
+
const i = idx[data.skinIndex[v * 4]];
|
|
199
|
+
if (i < 0) continue;
|
|
200
|
+
const z = data.pos[v * 3 + 2];
|
|
201
|
+
// only the body's own section (not a rattle or other rigid ornament beyond the joints)
|
|
202
|
+
if (z > J['v' + i].z + 0.3 * this.len[i] || z < J['v' + (i + 1)].z - 0.3 * this.len[i]) continue;
|
|
203
|
+
const x = data.pos[v * 3], y = data.pos[v * 3 + 1] - J['v' + i].y;
|
|
204
|
+
for (let k = 0; k < NA; k++) {
|
|
205
|
+
const ro = -Math.PI + (TAU * k) / NA;
|
|
206
|
+
const d = -(x * Math.sin(ro) + y * Math.cos(ro));
|
|
207
|
+
if (d > tab[i * NA + k]) tab[i * NA + k] = d;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (let k = 0; k < NA; k++) tab[N * NA + k] = tab[(N - 1) * NA + k];
|
|
211
|
+
// sections with no data (none expected): the analytic loaf
|
|
212
|
+
for (let i = 0; i <= N; i++) if (tab[i * NA + (NA >> 1)] <= 0) for (let k = 0; k < NA; k++) tab[i * NA + k] = J['v' + i].y;
|
|
213
|
+
}
|
|
214
|
+
this.headLen = J.nose.distanceTo(J.v0);
|
|
215
|
+
this.TL = this.bodyLen + this.headLen;
|
|
216
|
+
let hwMax = 0; for (let i = 0; i <= N; i++) hwMax = Math.max(hwMax, this.hw[i]);
|
|
217
|
+
this.hwMax = hwMax;
|
|
218
|
+
this.restCurl = spec.restCurl ?? 1;
|
|
219
|
+
// ---- config
|
|
220
|
+
const m = (a, b) => ({ ...a, ...(b || {}) });
|
|
221
|
+
const speedK = (vk.speedK ?? 1) * this.sq;
|
|
222
|
+
this.cfg = {
|
|
223
|
+
maxSpeed: vk.maxSpeed !== undefined ? vk.maxSpeed * this.sq : (spec.maxSpeed ?? DEFAULTS.maxSpeed) * speedK,
|
|
224
|
+
accel: (spec.accel ?? DEFAULTS.accel) * this.sq, decel: (spec.decel ?? DEFAULTS.decel) * this.sq,
|
|
225
|
+
turnRate: (spec.turnRate ?? DEFAULTS.turnRate) / this.sq,
|
|
226
|
+
minR: (spec.minTurnRadius ?? DEFAULTS.minTurnRadius) * this.TL,
|
|
227
|
+
body: m(DEFAULTS.body, spec.body), tongue: m(m(DEFAULTS.tongue, spec.tongue), vk.tongue), breath: m(DEFAULTS.breath, spec.breath),
|
|
228
|
+
actions: spec.actions || {},
|
|
229
|
+
strike: m(m(STRIKE, spec.actions?.strike), vk.strike),
|
|
230
|
+
rattle: !!vk.rattle,
|
|
231
|
+
ampK: vk.ampK ?? 1,
|
|
232
|
+
};
|
|
233
|
+
// a variant may bring its own gait table / gears (reference speeds, not scaled by speedK)
|
|
234
|
+
const G = vk.gaits || (spec.gaits && spec.gaits.length ? spec.gaits : [{ name: 'slither', v: 0, wave: 0.42, amp: 0.07, headLift: 0.008, stab: 0.6 }]);
|
|
235
|
+
const gK = vk.gaits ? this.sq : speedK, aK = vk.gaits ? 1 : this.cfg.ampK;
|
|
236
|
+
this.gaitRows = G.map((g) => [g.v * gK, g.wave, g.amp * aK, g.headLift * s, g.stab ?? 0.6]);
|
|
237
|
+
this.gaitNames = G.map((g) => g.name);
|
|
238
|
+
this.gaits = [...new Set(this.gaitNames)];
|
|
239
|
+
this.gears = vk.gears ? Object.fromEntries(Object.entries(vk.gears).map(([k, v]) => [k, v * this.sq])) : spec.gears ? Object.fromEntries(Object.entries(spec.gears).map(([k, v]) => [k, v * speedK])) : { slither: 0.25 * speedK };
|
|
240
|
+
this.cfg.tailCarry = (vk.tailCarry ?? 0) * s;
|
|
241
|
+
this._row = [0, 0, 0, 0];
|
|
242
|
+
// ---- bind-space head landmarks (for ground clearance and attachments)
|
|
243
|
+
this.v0b = J.v0.clone();
|
|
244
|
+
this.noseB = J.nose.clone();
|
|
245
|
+
this.chinB = J.jawTip.clone(); this.chinB.y = 0.0005 * s;
|
|
246
|
+
this.hingeB = J.jawHinge.clone();
|
|
247
|
+
// outer corner of the lower jaw (bind), for ground clearance when the head rolls
|
|
248
|
+
this.jawCorner = new THREE.Vector3(0, 0.0005 * s, J.jawHinge.z);
|
|
249
|
+
this.jawMid = new THREE.Vector3(0, 0.0005 * s, lerp(J.jawHinge.z, J.jawTip.z, 0.55));
|
|
250
|
+
{ const jb = bi.jaw; let mx = 0; for (let v = 0; v < data.nV; v++) if (data.skinIndex[v * 4] === jb && data.skinWeight[v * 4] > 0.5 && Math.abs(data.pos[v * 3]) > mx) { mx = Math.abs(data.pos[v * 3]); this.jawCorner.z = data.pos[v * 3 + 2]; } this.jawCorner.x = mx; this.jawMid.x = 0.5 * mx; }
|
|
251
|
+
this.tongueBaseB = J.tongueBase.clone(); this.tongueForkB = J.tongueFork.clone();
|
|
252
|
+
this.tongueDirB = J.tongueFork.clone().sub(J.tongueBase).normalize();
|
|
253
|
+
this.fangBaseB = J.fangBase.clone();
|
|
254
|
+
this.tipLen = J.tongueTip.distanceTo(J.tongueFork);
|
|
255
|
+
// ---- public contract
|
|
256
|
+
this.input = { speed: 0, heading, climb: 0, look: null, follow: null, target: null, targetSpeed: 0.3, crouch: 0, gait: null };
|
|
257
|
+
this.pos = new THREE.Vector3();
|
|
258
|
+
this.velocity = new THREE.Vector3();
|
|
259
|
+
this.heading = heading;
|
|
260
|
+
this.speed = 0;
|
|
261
|
+
this.yawRate = 0;
|
|
262
|
+
this.time = 0;
|
|
263
|
+
this.lod = 0;
|
|
264
|
+
this.frame = 0; this._acc = 0; this._posed = false;
|
|
265
|
+
this.lookTarget = new THREE.Vector3();
|
|
266
|
+
this.state = {
|
|
267
|
+
position: this.pos, heading, speed: 0, velocity: this.velocity, gait: 'stand', grounded: true,
|
|
268
|
+
action: null, posture: 'stand', eyelid: 0, lookTarget: null,
|
|
269
|
+
stats: { gait: 'stand', freq: 0, duty: 1 },
|
|
270
|
+
legs: [],
|
|
271
|
+
contacts: Array.from({ length: N + 1 }, (_, i) => ({ key: 'v' + i, contact: true, position: new THREE.Vector3() })),
|
|
272
|
+
};
|
|
273
|
+
this.headPose = { position: new THREE.Vector3(), quaternion: new THREE.Quaternion() };
|
|
274
|
+
this.debug = { targets: Array.from({ length: 6 }, () => new THREE.Vector3()) };
|
|
275
|
+
this.actions = ['idle', 'stand', 'sit', 'lie', 'sleep', 'coil', 'eat', 'drink', 'attack', 'strike', 'jump', 'hit', 'death'];
|
|
276
|
+
// ---- locomotion state
|
|
277
|
+
this.trail = new Trail(2048);
|
|
278
|
+
this.C = new THREE.Vector3();
|
|
279
|
+
this.leader = new THREE.Vector3();
|
|
280
|
+
this.x = 0; this.phi = 0;
|
|
281
|
+
this.A = new Spring(0); this.lam = new Spring(0.42 * this.TL);
|
|
282
|
+
this.reseed = true;
|
|
283
|
+
this.script = null; // scripted leader path (coil)
|
|
284
|
+
this.follow = { init: false, p: new THREE.Vector3() };
|
|
285
|
+
this.pursuit = { on: false, psi: 0, dx: 0, dz: 0, follow: false };
|
|
286
|
+
// ---- per-joint arrays
|
|
287
|
+
const F = (n) => new Float64Array(n);
|
|
288
|
+
this.gx = F(N + 1); this.gy = F(N + 1); this.gz = F(N + 1); // base ground points (trail)
|
|
289
|
+
this.nx = F(N + 1); this.ny = F(N + 1); this.nz = F(N + 1); // ground normals
|
|
290
|
+
this.px = F(N + 1); this.py = F(N + 1); this.pz = F(N + 1); // final axis points
|
|
291
|
+
this.hx = F(N + 1); this.hz = F(N + 1);
|
|
292
|
+
this.byaw = F(N); this.byawU = F(N); this.blh = F(N); this.bch = F(N); this.bsin = F(N); // base segment yaw, horizontal length, chord, elevation sine
|
|
293
|
+
this.lift = F(N + 1); this.dyaw = F(N); this.roll = F(N + 1); this.affect = F(N + 1); this.stack = F(N + 1); this.stackTmp = F(N + 1);
|
|
294
|
+
this.grid = { head: new Int16Array(256), next: new Int16Array(N + 1) };
|
|
295
|
+
this.lat = Array.from({ length: N }, () => new THREE.Vector3());
|
|
296
|
+
this.contact = new Uint8Array(N + 1);
|
|
297
|
+
// ---- head & secondary springs
|
|
298
|
+
this.hYaw = new Spring(0); this.hPitch = new Spring(0); this.hRoll = new Spring(0);
|
|
299
|
+
this.gape = new Spring(0); this.fang = new Spring(0);
|
|
300
|
+
this.tongue = { t: 0, dur: 0, next: 1 + this.rand() * 2, osc: 4, ext: new Spring(0), tipS: new Spring(0), pitchS: new Spring(0), pitch: 0, tip: 0, extD: 0 };
|
|
301
|
+
this.qHead = new THREE.Quaternion();
|
|
302
|
+
this.Tm = new THREE.Matrix4(); // head-rigid transform: bind space -> world
|
|
303
|
+
this._headInit = false;
|
|
304
|
+
// ---- actions
|
|
305
|
+
this.P = makePose(N);
|
|
306
|
+
this.layer = new SnakeActions(this);
|
|
307
|
+
this.attachmentPoints = this._attachments(data);
|
|
308
|
+
this.reset(position || new THREE.Vector3(), heading);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
_attachments(data) {
|
|
312
|
+
const J = this.J;
|
|
313
|
+
const at = (bone, p) => {
|
|
314
|
+
const i = data.bones.findIndex((b) => b.name === bone);
|
|
315
|
+
if (i < 0) return null;
|
|
316
|
+
const inv = this.sk.inverses ? this.sk.inverses[i] : this.sk.bind[i].clone().invert();
|
|
317
|
+
return { bone, local: new THREE.Matrix4().makeTranslation(p.x, p.y, p.z).premultiply(inv) };
|
|
318
|
+
};
|
|
319
|
+
let headP = J.v0.clone().lerp(J.nose, 0.55);
|
|
320
|
+
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); }
|
|
321
|
+
const mid = this.N >> 1;
|
|
322
|
+
const back = J['v' + mid].clone(); back.y += this.hw[mid];
|
|
323
|
+
const out = { mouth: at('head', J.nose.clone().lerp(J.jawTip, 0.5)), head: at('head', headP), back: at('spine' + mid, back) };
|
|
324
|
+
for (const k of Object.keys(out)) if (!out[k]) delete out[k];
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ----------------------------------------------------------------- terrain
|
|
329
|
+
terrainH(x, z) { const h = this.ground(x, z); return Number.isFinite(h) ? h : 0; }
|
|
330
|
+
terrainN(x, z, out) {
|
|
331
|
+
const e = 0.05 * this.s, h0 = this.terrainH(x, z);
|
|
332
|
+
out[0] = h0 - this.terrainH(x + e, z); out[1] = e; out[2] = h0 - this.terrainH(x, z + e);
|
|
333
|
+
const l = h3(out[0], out[1], out[2]);
|
|
334
|
+
out[0] /= l; out[1] /= l; out[2] /= l;
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Lay the body straight behind p along `heading` (bind layout). */
|
|
339
|
+
reset(p, heading = this.heading) {
|
|
340
|
+
const N = this.N, tr = this.trail;
|
|
341
|
+
tr.clear();
|
|
342
|
+
// rest in a loose S behind the head (curvature integrated backwards along the body)
|
|
343
|
+
const extra = 0.25 * this.TL;
|
|
344
|
+
const total = this.bodyLen + extra;
|
|
345
|
+
const step = 0.01 * this.s;
|
|
346
|
+
const nPts = Math.ceil(total / step) + 1;
|
|
347
|
+
const X = new Float64Array(nPts), Z = new Float64Array(nPts);
|
|
348
|
+
let x = p.x, z = p.z, psi = heading;
|
|
349
|
+
const rr = prng(Math.imul(this.seed || 1, 2654435761) ^ 0x5bd1e995), ph = rr() * TAU;
|
|
350
|
+
const lam = (0.42 + 0.16 * rr()) * this.TL, amp = (0.3 + 0.45 * rr()) * this.restCurl;
|
|
351
|
+
for (let k = 0; k < nPts; k++) {
|
|
352
|
+
X[k] = x; Z[k] = z;
|
|
353
|
+
const d = k * step;
|
|
354
|
+
const a = heading + amp * (Math.sin((TAU * d) / lam + ph) - Math.sin(ph)) * smooth(0.02 * this.TL, 0.15 * this.TL, d) * (1 - smooth(this.bodyLen, total, d));
|
|
355
|
+
psi = a;
|
|
356
|
+
x -= Math.sin(psi) * step; z -= Math.cos(psi) * step;
|
|
357
|
+
}
|
|
358
|
+
let s = 0, lx = 0, ly = 0, lz = 0;
|
|
359
|
+
for (let k = nPts - 1; k >= 0; k--) {
|
|
360
|
+
const y = this.terrainH(X[k], Z[k]);
|
|
361
|
+
if (k < nPts - 1) s += h3(X[k] - lx, y - ly, Z[k] - lz);
|
|
362
|
+
tr.push(X[k], y, Z[k], s);
|
|
363
|
+
lx = X[k]; ly = y; lz = Z[k];
|
|
364
|
+
}
|
|
365
|
+
this.heading = heading; this.input.heading = heading;
|
|
366
|
+
this.speed = 0; this.yawRate = 0; this.velocity.set(0, 0, 0);
|
|
367
|
+
this.C.set(p.x, this.terrainH(p.x, p.z), p.z);
|
|
368
|
+
this.leader.copy(this.C);
|
|
369
|
+
this.pos.copy(this.C);
|
|
370
|
+
this.x = 0; this.phi = 0; this.A.reset(0); this.lam.reset(0.42 * this.TL);
|
|
371
|
+
this.reseed = true;
|
|
372
|
+
this.script = null;
|
|
373
|
+
this._headInit = false;
|
|
374
|
+
this.follow.init = false;
|
|
375
|
+
this.pursuit.on = false;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ----------------------------------------------------------------- public API
|
|
379
|
+
play(name, opts) { return this.layer.play(name, opts || {}); }
|
|
380
|
+
stop(name) { this.layer.stop(name); }
|
|
381
|
+
setLod(level) { this.lod = clamp(level | 0, 0, 2); return this; }
|
|
382
|
+
dispose() { this.layer.clear(); }
|
|
383
|
+
|
|
384
|
+
update(dt) {
|
|
385
|
+
if (!(dt > 1e-4) || !Number.isFinite(dt)) dt = 1e-4;
|
|
386
|
+
dt = Math.min(dt, 1 / 20);
|
|
387
|
+
this.time += dt;
|
|
388
|
+
this.drive(dt);
|
|
389
|
+
this.locomote(dt);
|
|
390
|
+
// crowd LOD: the pose is solved at half rate (the simulation still runs every frame)
|
|
391
|
+
this.frame = (this.frame + 1) | 0;
|
|
392
|
+
if (this.lod < 2 || (this.frame & 1) === 0 || !this._posed) {
|
|
393
|
+
this.basePose();
|
|
394
|
+
this.layer.update(dt + this._acc);
|
|
395
|
+
this.solveBody();
|
|
396
|
+
this.solveHead(this._acc + dt);
|
|
397
|
+
this._acc = 0;
|
|
398
|
+
this._posed = true;
|
|
399
|
+
} else { this._acc += dt; }
|
|
400
|
+
this.publish();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ----------------------------------------------------------------- steering
|
|
404
|
+
drive(dt) {
|
|
405
|
+
const inp = this.input, cfg = this.cfg;
|
|
406
|
+
this.wantSpeed = clamp(inp.speed || 0, 0, cfg.maxSpeed);
|
|
407
|
+
this.wantHeading = inp.heading ?? this.heading;
|
|
408
|
+
if (inp.target && !inp.follow) {
|
|
409
|
+
const dx = inp.target.x - this.pos.x, dz = inp.target.z - this.pos.z;
|
|
410
|
+
const d = h2(dx, dz);
|
|
411
|
+
const stopR = 0.08 * this.TL + 0.1 * this.speed;
|
|
412
|
+
if (d < stopR) { inp.target = null; inp.speed = 0; this.wantSpeed = 0; this.emit('arrive', {}); }
|
|
413
|
+
else {
|
|
414
|
+
const want = Math.atan2(dx, dz);
|
|
415
|
+
inp.heading = want; this.wantHeading = want;
|
|
416
|
+
const brake = Math.sqrt(Math.max(0, 2 * cfg.decel * 0.7 * (d - stopR)));
|
|
417
|
+
this.wantSpeed = Math.min(inp.targetSpeed || 0.3, cfg.maxSpeed, brake + 0.03);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (this.layer.holdsStill()) {
|
|
421
|
+
if ((this.wantSpeed > 0.01 || inp.follow) && !this.layer.dead()) this.layer.requestStand();
|
|
422
|
+
this.wantSpeed = 0;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
gaitAt(v) {
|
|
427
|
+
const R = this.gaitRows;
|
|
428
|
+
let i = 0;
|
|
429
|
+
while (i < R.length - 2 && v > R[i + 1][0]) i++;
|
|
430
|
+
const a = R[i], b = R[Math.min(i + 1, R.length - 1)];
|
|
431
|
+
const t = a === b ? 0 : smooth(0, 1, (v - a[0]) / (b[0] - a[0]));
|
|
432
|
+
const o = this._row;
|
|
433
|
+
for (let k = 0; k < 4; k++) o[k] = lerp(a[k + 1], b[k + 1], t);
|
|
434
|
+
this.gaitName = t < 0.5 ? this.gaitNames[i] : this.gaitNames[Math.min(i + 1, R.length - 1)];
|
|
435
|
+
return o;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ----------------------------------------------------------------- locomotion
|
|
439
|
+
locomote(dt) {
|
|
440
|
+
const inp = this.input, cfg = this.cfg, P = this.P;
|
|
441
|
+
const tr = this.trail;
|
|
442
|
+
const spacing = 0.005 * this.s;
|
|
443
|
+
if (this.script) { this.runScript(dt, spacing); return; }
|
|
444
|
+
const following = !!inp.follow && !this.layer.holdsStill();
|
|
445
|
+
let v, dx;
|
|
446
|
+
if (following) {
|
|
447
|
+
const f = inp.follow, fv = f.velocity;
|
|
448
|
+
const vx = fv ? fv.x : 0, vz = fv ? fv.z : 0;
|
|
449
|
+
const sp = h2(vx, vz);
|
|
450
|
+
const fp = this.follow.p;
|
|
451
|
+
if (!this.follow.init) { fp.set(f.position.x, 0, f.position.z); this.follow.init = true; this.reseed = false; this.A.reset(0); }
|
|
452
|
+
// alpha-beta filter: the given velocity carries it, the position pulls gently (physics jitter)
|
|
453
|
+
const ox = fp.x, oz = fp.z;
|
|
454
|
+
fp.x += vx * dt; fp.z += vz * dt;
|
|
455
|
+
const k = 1 - Math.exp(-dt / 0.06);
|
|
456
|
+
fp.x += (f.position.x - fp.x) * k; fp.z += (f.position.z - fp.z) * k;
|
|
457
|
+
v = sp;
|
|
458
|
+
let hT = f.heading ?? (sp > 0.02 * this.sq ? Math.atan2(vx, vz) : this.heading);
|
|
459
|
+
this.turn(hT, v, dt);
|
|
460
|
+
const Fx = Math.sin(this.heading), Fz = Math.cos(this.heading);
|
|
461
|
+
dx = Math.max(0, (fp.x - ox) * Fx + (fp.z - oz) * Fz);
|
|
462
|
+
this.C.x = fp.x; this.C.z = fp.z;
|
|
463
|
+
this.speed = v;
|
|
464
|
+
this.velocity.set(vx, 0, vz);
|
|
465
|
+
} else {
|
|
466
|
+
this.follow.init = false;
|
|
467
|
+
const vT = this.wantSpeed * P.speedScale;
|
|
468
|
+
const acc = vT > this.speed ? cfg.accel : cfg.decel;
|
|
469
|
+
this.speed += clamp(vT - this.speed, -acc * dt, acc * dt);
|
|
470
|
+
if (this.speed < 1e-4 && vT <= 0) this.speed = 0;
|
|
471
|
+
v = this.speed;
|
|
472
|
+
if (v > 0 && this.reseed) this.startMoving();
|
|
473
|
+
this.turn(this.wantHeading, v, dt);
|
|
474
|
+
dx = v * dt;
|
|
475
|
+
const Fx = Math.sin(this.heading), Fz = Math.cos(this.heading);
|
|
476
|
+
this.C.x += Fx * dx; this.C.z += Fz * dx;
|
|
477
|
+
this.velocity.set(Fx * v, 0, Fz * v);
|
|
478
|
+
}
|
|
479
|
+
this.C.y = this.terrainH(this.C.x, this.C.z);
|
|
480
|
+
this.pos.copy(this.C);
|
|
481
|
+
const row = this.gaitAt(v / this.sq);
|
|
482
|
+
this.gait = row;
|
|
483
|
+
if (dx > 0 || following) {
|
|
484
|
+
const lamT = row[0] * this.TL, ampT = row[1] * this.TL;
|
|
485
|
+
if (dx > 0) {
|
|
486
|
+
const wd = TAU / Math.max(0.05, this.lam.x);
|
|
487
|
+
this.A.step(ampT, 1.6 * wd, dx);
|
|
488
|
+
this.lam.step(lamT, 1.6 * wd, dx);
|
|
489
|
+
this.phi += (TAU * dx) / this.lam.x;
|
|
490
|
+
this.x += dx;
|
|
491
|
+
}
|
|
492
|
+
const y = this.A.x * Math.sin(this.phi);
|
|
493
|
+
const Lx = Math.cos(this.heading), Lz = -Math.sin(this.heading);
|
|
494
|
+
const Dx = this.C.x + Lx * y, Dz = this.C.z + Lz * y;
|
|
495
|
+
// The head tracks its ideal place D on the wave with a curvature-bounded steering law (heading
|
|
496
|
+
// relaxes toward the ideal path's tangent over a short distance, plus lateral and along-track
|
|
497
|
+
// corrections), so the laid path is smooth and never tighter than the body can bend, whatever
|
|
498
|
+
// the input does (sharp turns, a misaligned follow start, physics jitter).
|
|
499
|
+
const pu = this.pursuit;
|
|
500
|
+
if (!pu.on) { pu.on = true; pu.psi = this.neckYaw(); pu.k = 0; pu.dx = Dx; pu.dz = Dz; }
|
|
501
|
+
const tdx = Dx - pu.dx, tdz = Dz - pu.dz;
|
|
502
|
+
pu.dx = Dx; pu.dz = Dz;
|
|
503
|
+
const dD = h2(tdx, tdz);
|
|
504
|
+
const psiD = dD > 1e-7 ? Math.atan2(tdx, tdz) : pu.psi;
|
|
505
|
+
const ex = Dx - this.leader.x, ez = Dz - this.leader.z;
|
|
506
|
+
const Fx = Math.sin(pu.psi), Fz = Math.cos(pu.psi);
|
|
507
|
+
const lat = ex * Fz - ez * Fx, along = ex * Fx + ez * Fz;
|
|
508
|
+
const TL = this.TL;
|
|
509
|
+
const psiT = psiD + clamp(lat / (0.05 * TL), -0.7, 0.7);
|
|
510
|
+
const ds = Math.max(0, dD * clamp(Math.cos(angDiff(pu.psi, psiD)), 0.25, 1) + clamp(along, -0.03 * TL, 0.06 * TL) * 0.12);
|
|
511
|
+
const kMax = 1 / (0.036 * TL), kRate = kMax / (0.035 * TL);
|
|
512
|
+
const nSub = Math.max(1, Math.ceil(ds / (0.004 * this.s)));
|
|
513
|
+
let lx = this.leader.x, lz = this.leader.z;
|
|
514
|
+
for (let k = 0; k < nSub; k++) {
|
|
515
|
+
const h = ds / nSub;
|
|
516
|
+
// curvature command with a smooth saturation, and a bounded rate of change along the path
|
|
517
|
+
const kc = kMax * Math.tanh(angDiff(pu.psi, psiT) / (0.03 * TL * kMax));
|
|
518
|
+
pu.k += clamp(kc - pu.k, -kRate * h, kRate * h);
|
|
519
|
+
pu.psi += pu.k * h;
|
|
520
|
+
lx += Math.sin(pu.psi) * h; lz += Math.cos(pu.psi) * h;
|
|
521
|
+
}
|
|
522
|
+
this.leader.set(lx, this.terrainH(lx, lz), lz);
|
|
523
|
+
if (!following && pu.follow) { pu.follow = false; }
|
|
524
|
+
pu.follow = following;
|
|
525
|
+
tr.lead(this.leader.x, this.leader.y, this.leader.z, spacing);
|
|
526
|
+
}
|
|
527
|
+
this.freq = v > 1e-3 ? v / this.lam.x : 0;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// limited yaw rate: never tighter than the minimum radius, and slow enough that the leader keeps
|
|
531
|
+
// moving forward along its path (the wave offset swings with the heading)
|
|
532
|
+
turn(hT, v, dt) {
|
|
533
|
+
const cfg = this.cfg;
|
|
534
|
+
const wMax = Math.min(cfg.turnRate, v / cfg.minR, (0.35 * v) / Math.max(1e-3, Math.abs(this.A.x)));
|
|
535
|
+
const wT = clamp(angDiff(this.heading, hT) * 2.2, -wMax, wMax);
|
|
536
|
+
this.yawRate += clamp(wT - this.yawRate, -4 * dt, 4 * dt);
|
|
537
|
+
this.yawRate = clamp(this.yawRate, -wMax, wMax);
|
|
538
|
+
this.heading += this.yawRate * dt;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
neckYaw() {
|
|
542
|
+
const tr = this.trail, sl = tr.lastS;
|
|
543
|
+
tr.sample(sl, S3); const ax = S3[0], az = S3[2];
|
|
544
|
+
tr.sample(sl - 0.04 * this.TL, S3);
|
|
545
|
+
return Math.atan2(ax - S3[0], az - S3[2]);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// a new path leaves exactly along the neck: centre on the leader, heading along the neck, no wave yet
|
|
549
|
+
startMoving() {
|
|
550
|
+
const tr = this.trail, sl = tr.lastS;
|
|
551
|
+
tr.sample(sl, S3); const ax = S3[0], ay = S3[1], az = S3[2];
|
|
552
|
+
tr.sample(sl - 0.04 * this.TL, S3);
|
|
553
|
+
this.heading = Math.atan2(ax - S3[0], az - S3[2]);
|
|
554
|
+
this.yawRate = 0;
|
|
555
|
+
this.C.set(ax, ay, az);
|
|
556
|
+
this.leader.set(ax, ay, az);
|
|
557
|
+
this.A.reset(0);
|
|
558
|
+
this.phi = 0;
|
|
559
|
+
this.reseed = false;
|
|
560
|
+
this.pursuit.on = false;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// scripted leader path (coil): curvature integrated along arc length
|
|
564
|
+
startScript(sc) {
|
|
565
|
+
const tr = this.trail, sl = tr.lastS;
|
|
566
|
+
tr.sample(sl, S3); const ax = S3[0], az = S3[2];
|
|
567
|
+
tr.sample(sl - 0.03 * this.TL, S3); const bx = S3[0], bz = S3[2];
|
|
568
|
+
tr.sample(sl - 0.06 * this.TL, S3); const cx = S3[0], cz = S3[2];
|
|
569
|
+
const y1 = Math.atan2(ax - bx, az - bz), y0 = Math.atan2(bx - cx, bz - cz);
|
|
570
|
+
sc.psi = y1;
|
|
571
|
+
sc.k0 = clamp(angDiff(y0, y1) / (0.03 * this.TL), -30, 30);
|
|
572
|
+
sc.u = 0;
|
|
573
|
+
sc.x = ax; sc.z = az;
|
|
574
|
+
this.script = sc;
|
|
575
|
+
this.speed = 0; this.yawRate = 0;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
runScript(dt, spacing) {
|
|
579
|
+
const sc = this.script;
|
|
580
|
+
const rem = sc.total - sc.u;
|
|
581
|
+
const v = sc.speed * (0.25 + 0.75 * smooth(0, 0.05 * this.TL, sc.u)) * Math.max(0.12, smooth(0, 0.08 * this.TL, rem));
|
|
582
|
+
const du = Math.min(rem, v * dt);
|
|
583
|
+
const n = Math.max(1, Math.ceil(du / (0.004 * this.s)));
|
|
584
|
+
for (let k = 0; k < n; k++) {
|
|
585
|
+
const h = du / n;
|
|
586
|
+
const kap = lerp(sc.k0, sc.kappa(sc.u + h * 0.5), smooth(0, 0.07 * this.TL, sc.u));
|
|
587
|
+
sc.psi += kap * h;
|
|
588
|
+
sc.x += Math.sin(sc.psi) * h; sc.z += Math.cos(sc.psi) * h;
|
|
589
|
+
sc.u += h;
|
|
590
|
+
}
|
|
591
|
+
this.leader.set(sc.x, this.terrainH(sc.x, sc.z), sc.z);
|
|
592
|
+
// the head leads: the reported position follows it while the snake coils up
|
|
593
|
+
this.C.copy(this.leader); this.pos.copy(this.C);
|
|
594
|
+
this.trail.lead(this.leader.x, this.leader.y, this.leader.z, spacing);
|
|
595
|
+
this.velocity.set(0, 0, 0);
|
|
596
|
+
this.freq = 0;
|
|
597
|
+
if (sc.u >= sc.total - 1e-6) { this.script = null; sc.done = true; this.reseed = true; this.pursuit.on = false; }
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ----------------------------------------------------------------- base pose: joints on the trail
|
|
601
|
+
basePose() {
|
|
602
|
+
const N = this.N, tr = this.trail, sl = tr.lastS, lod = this.lod;
|
|
603
|
+
// nothing moved (the trail is unchanged and the terrain is static): the base pose stands
|
|
604
|
+
if (tr.ver === this._baseVer && lod === this._baseLod) return;
|
|
605
|
+
this._baseVer = tr.ver; this._baseLod = lod;
|
|
606
|
+
let hint = tr.n - 2;
|
|
607
|
+
const gx = this.gx, gy = this.gy, gz = this.gz, sig = this.sig;
|
|
608
|
+
// crowd LOD: every other joint from the trail, the ones between at the chord midpoint
|
|
609
|
+
const stepI = lod >= 2 ? 2 : 1;
|
|
610
|
+
for (let i = 0; i <= N; i += stepI) {
|
|
611
|
+
tr.sample(sl - sig[i], S3, hint);
|
|
612
|
+
hint = tr.hit;
|
|
613
|
+
gx[i] = S3[0]; gz[i] = S3[2];
|
|
614
|
+
gy[i] = this.terrainH(S3[0], S3[2]);
|
|
615
|
+
if (lod >= 2) { this.nx[i] = 0; this.ny[i] = 1; this.nz[i] = 0; }
|
|
616
|
+
else { this.terrainN(S3[0], S3[2], S3); this.nx[i] = S3[0]; this.ny[i] = S3[1]; this.nz[i] = S3[2]; }
|
|
617
|
+
if (stepI === 2 && i === N - 1) i--; // make sure the tail tip is sampled
|
|
618
|
+
}
|
|
619
|
+
if (stepI === 2) {
|
|
620
|
+
for (let i = 1; i < N; i += 2) {
|
|
621
|
+
if (i + 1 > N) break;
|
|
622
|
+
const t = (sig[i] - sig[i - 1]) / (sig[i + 1] - sig[i - 1]);
|
|
623
|
+
gx[i] = gx[i - 1] + (gx[i + 1] - gx[i - 1]) * t; gz[i] = gz[i - 1] + (gz[i + 1] - gz[i - 1]) * t;
|
|
624
|
+
gy[i] = this.terrainH(gx[i], gz[i]);
|
|
625
|
+
this.nx[i] = 0; this.ny[i] = 1; this.nz[i] = 0;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
for (let i = 0; i < N; i++) {
|
|
629
|
+
const dx = this.gx[i] - this.gx[i + 1], dz = this.gz[i] - this.gz[i + 1], dy = this.gy[i] - this.gy[i + 1];
|
|
630
|
+
const lh = h2(dx, dz);
|
|
631
|
+
this.byaw[i] = lh > 1e-9 ? Math.atan2(dx, dz) : i > 0 ? this.byaw[i - 1] : this.heading;
|
|
632
|
+
this.blh[i] = lh;
|
|
633
|
+
const c = h2(lh, dy);
|
|
634
|
+
this.bch[i] = c;
|
|
635
|
+
this.bsin[i] = c > 1e-9 ? dy / c : 0;
|
|
636
|
+
// yaw unwrapped along the chain (a coiled body turns through more than pi)
|
|
637
|
+
this.byawU[i] = i === 0 ? this.byaw[0] : this.byawU[i - 1] + angDiff(this.byaw[i - 1], this.byaw[i]);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ----------------------------------------------------------------- pose integration and frames
|
|
642
|
+
solveBody() {
|
|
643
|
+
const N = this.N, P = this.P;
|
|
644
|
+
const lift = P.lift, dyaw = P.dyaw, roll = P.roll;
|
|
645
|
+
let any = false;
|
|
646
|
+
for (let i = 0; i < N; i++) if (Math.abs(dyaw[i]) > 1e-7) { any = true; break; }
|
|
647
|
+
if (!any) for (let i = 0; i <= N; i++) if (lift[i] > 1e-7) { any = true; break; }
|
|
648
|
+
const hx = this.hx, hz = this.hz;
|
|
649
|
+
if (any) {
|
|
650
|
+
hx[0] = this.gx[0]; hz[0] = this.gz[0];
|
|
651
|
+
for (let i = 0; i < N; i++) {
|
|
652
|
+
const c = this.bch[i];
|
|
653
|
+
const sn = clamp(this.bsin[i] + (lift[i] - lift[i + 1]) / Math.max(c, 1e-9), -0.995, 0.995);
|
|
654
|
+
const lh = c * Math.sqrt(1 - sn * sn);
|
|
655
|
+
const yw = this.byaw[i] + dyaw[i];
|
|
656
|
+
hx[i + 1] = hx[i] - Math.sin(yw) * lh;
|
|
657
|
+
hz[i + 1] = hz[i] - Math.cos(yw) * lh;
|
|
658
|
+
}
|
|
659
|
+
// keep the joints the actions leave alone where they were
|
|
660
|
+
let sw = 0, tx = 0, tz = 0;
|
|
661
|
+
for (let i = 0; i <= N; i++) {
|
|
662
|
+
const w = Math.max(1e-3, 1 - clamp(this.affect[i], 0, 1));
|
|
663
|
+
sw += w; tx += w * (this.gx[i] - hx[i]); tz += w * (this.gz[i] - hz[i]);
|
|
664
|
+
}
|
|
665
|
+
tx /= sw; tz /= sw;
|
|
666
|
+
for (let i = 0; i <= N; i++) { hx[i] += tx; hz[i] += tz; }
|
|
667
|
+
} else {
|
|
668
|
+
for (let i = 0; i <= N; i++) { hx[i] = this.gx[i]; hz[i] = this.gz[i]; }
|
|
669
|
+
}
|
|
670
|
+
// heights from the terrain, axis offset along the ground normal (grows toward the half width
|
|
671
|
+
// when the body is rolled onto its back)
|
|
672
|
+
const lod = this.lod;
|
|
673
|
+
for (let i = 0; i <= N; i++) {
|
|
674
|
+
const x = hx[i], z = hz[i];
|
|
675
|
+
let gy, nx, ny, nz;
|
|
676
|
+
if (any) {
|
|
677
|
+
gy = this.terrainH(x, z);
|
|
678
|
+
if (lod >= 2) { nx = 0; ny = 1; nz = 0; } else { this.terrainN(x, z, S3); nx = S3[0]; ny = S3[1]; nz = S3[2]; }
|
|
679
|
+
} else { gy = this.gy[i]; nx = this.nx[i]; ny = this.ny[i]; nz = this.nz[i]; }
|
|
680
|
+
const ro = roll[i];
|
|
681
|
+
// support of the cross-section toward the ground when rolled: flat belly (axis height r) when
|
|
682
|
+
// upright, the half width on its side, the round back (~half width) when belly-up
|
|
683
|
+
const rr = ro === 0 ? this.r[i] : this.support(i, ro);
|
|
684
|
+
this.px[i] = x + nx * rr; this.pz[i] = z + nz * rr;
|
|
685
|
+
this.py[i] = gy + ny * rr + lift[i];
|
|
686
|
+
this.stack[i] = 0;
|
|
687
|
+
}
|
|
688
|
+
if (lod < 2 && P.stackW > 0) this.selfContact();
|
|
689
|
+
for (let i = 0; i <= N; i++) this.py[i] += this.stack[i];
|
|
690
|
+
// on the ground: this joint and its neighbours are not lifted (a joint next to a raised one is
|
|
691
|
+
// the start of the rise)
|
|
692
|
+
const tol = 0.0015 * this.s;
|
|
693
|
+
for (let i = 0; i <= N; i++) {
|
|
694
|
+
const a = lift[i] + this.stack[i], b = i > 0 ? lift[i - 1] + this.stack[i - 1] : 0, c = i < N ? lift[i + 1] + this.stack[i + 1] : 0;
|
|
695
|
+
this.contact[i] = a < tol && b < tol && c < tol && !(this.affect[i] > 0.05 && lift[i] > 1e-5) ? 1 : 0;
|
|
696
|
+
}
|
|
697
|
+
// frames: lateral axis from the ground normal (belly down), transported where a segment stands
|
|
698
|
+
// up steeply, then rolled
|
|
699
|
+
const up = S3;
|
|
700
|
+
let plx = 0, ply = 0, plz = 0, havePrev = false;
|
|
701
|
+
for (let i = N - 1; i >= 0; i--) {
|
|
702
|
+
const fx = this.px[i] - this.px[i + 1], fy = this.py[i] - this.py[i + 1], fz = this.pz[i] - this.pz[i + 1];
|
|
703
|
+
const fl = h3(fx, fy, fz) || 1;
|
|
704
|
+
const ux = fx / fl, uy = fy / fl, uz = fz / fl;
|
|
705
|
+
const gnx = (this.nx[i] + this.nx[i + 1]) * 0.5, gny = (this.ny[i] + this.ny[i + 1]) * 0.5, gnz = (this.nz[i] + this.nz[i + 1]) * 0.5;
|
|
706
|
+
// left = up x forward
|
|
707
|
+
let lx = gny * uz - gnz * uy, ly = gnz * ux - gnx * uz, lz = gnx * uy - gny * ux;
|
|
708
|
+
let ll = h3(lx, ly, lz);
|
|
709
|
+
if (havePrev) {
|
|
710
|
+
const d = plx * ux + ply * uy + plz * uz;
|
|
711
|
+
let tx = plx - ux * d, ty = ply - uy * d, tz = plz - uz * d;
|
|
712
|
+
const tl = h3(tx, ty, tz) || 1;
|
|
713
|
+
tx /= tl; ty /= tl; tz /= tl;
|
|
714
|
+
const w = smooth(0.25, 0.6, ll);
|
|
715
|
+
if (ll > 1e-9) { lx /= ll; ly /= ll; lz /= ll; }
|
|
716
|
+
lx = lerp(tx, lx, w); ly = lerp(ty, ly, w); lz = lerp(tz, lz, w);
|
|
717
|
+
ll = h3(lx, ly, lz);
|
|
718
|
+
}
|
|
719
|
+
if (ll < 1e-9) { lx = Math.cos(this.heading); ly = 0; lz = -Math.sin(this.heading); ll = 1; }
|
|
720
|
+
lx /= ll; ly /= ll; lz /= ll;
|
|
721
|
+
plx = lx; ply = ly; plz = lz; havePrev = true;
|
|
722
|
+
// roll about the bone (forward axis)
|
|
723
|
+
const ro = (roll[i] + roll[i + 1]) * 0.5;
|
|
724
|
+
const L = this.lat[i];
|
|
725
|
+
if (Math.abs(ro) > 1e-6) {
|
|
726
|
+
const c = Math.cos(ro), s = Math.sin(ro);
|
|
727
|
+
// rotate lat about u by ro (Rodrigues; lat is perpendicular to u)
|
|
728
|
+
const cxv = uy * lz - uz * ly, cyv = uz * lx - ux * lz, czv = ux * ly - uy * lx;
|
|
729
|
+
L.set(lx * c + cxv * s, ly * c + cyv * s, lz * c + czv * s);
|
|
730
|
+
} else L.set(lx, ly, lz);
|
|
731
|
+
}
|
|
732
|
+
for (let i = 0; i < N; i++) {
|
|
733
|
+
_v.set(this.px[i], this.py[i], this.pz[i]);
|
|
734
|
+
_v2.set(this.px[i + 1] - this.px[i], this.py[i + 1] - this.py[i], this.pz[i + 1] - this.pz[i]);
|
|
735
|
+
writeFrame(this.spine[i].matrixWorld, _v, _v2, this.lat[i]);
|
|
736
|
+
}
|
|
737
|
+
void up;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// cross-section support toward the ground for roll ro (table, linear in angle)
|
|
741
|
+
support(i, ro) {
|
|
742
|
+
const NA = this.supN;
|
|
743
|
+
let a = ((ro + Math.PI) / TAU) * NA;
|
|
744
|
+
a -= Math.floor(a / NA) * NA;
|
|
745
|
+
const k0 = Math.floor(a), k1 = (k0 + 1) % NA, t = a - k0;
|
|
746
|
+
const T = this.supTab, o = i * NA;
|
|
747
|
+
return T[o + k0] + (T[o + k1] - T[o + k0]) * t;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// a part of the body lying across another rides on top of it (anterior over posterior)
|
|
751
|
+
selfContact() {
|
|
752
|
+
const N = this.N, px = this.px, pz = this.pz, py = this.py, st = this.stack, hw = this.hw, sig = this.sig;
|
|
753
|
+
// spatial hash of the joints already processed (posterior ones), cell >= the widest contact
|
|
754
|
+
const G = this.grid, cs = 2 * this.hwMax, inv = 1 / cs;
|
|
755
|
+
G.head.fill(-1);
|
|
756
|
+
let anyHit = false;
|
|
757
|
+
const gap = 0.02 * this.TL;
|
|
758
|
+
for (let i = N; i >= 0; i--) {
|
|
759
|
+
let need = 0;
|
|
760
|
+
const cx = Math.floor(px[i] * inv), cz = Math.floor(pz[i] * inv);
|
|
761
|
+
for (let ox = -1; ox <= 1; ox++) for (let oz = -1; oz <= 1; oz++) {
|
|
762
|
+
for (let j = G.head[((cx + ox) * 73856093 ^ (cz + oz) * 19349663) & 255]; j >= 0; j = G.next[j]) {
|
|
763
|
+
if (sig[j] - sig[i] < 2.5 * (hw[i] + hw[j]) + gap) continue;
|
|
764
|
+
const D = (hw[i] + hw[j]) * 0.92;
|
|
765
|
+
const dx = px[i] - px[j], dz = pz[i] - pz[j];
|
|
766
|
+
const d2 = dx * dx + dz * dz;
|
|
767
|
+
if (d2 >= D * D) continue;
|
|
768
|
+
const q = 1 - d2 / (D * D);
|
|
769
|
+
const h = (py[j] + st[j] + (this.r[i] + hw[j]) * q * (2 - q) - py[i]) * 1.3;
|
|
770
|
+
if (h > need) need = h;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
st[i] = need;
|
|
774
|
+
if (need > 0) anyHit = true;
|
|
775
|
+
const k = (cx * 73856093 ^ cz * 19349663) & 255;
|
|
776
|
+
G.next[i] = G.head[k]; G.head[k] = i;
|
|
777
|
+
}
|
|
778
|
+
if (!anyHit) return;
|
|
779
|
+
// ramps along the chain so the climbing part forms an arch, not a step: max-plus dilation, then
|
|
780
|
+
// a light smoothing (the requirement was padded for it)
|
|
781
|
+
const slope = 0.2;
|
|
782
|
+
for (let i = 1; i <= N; i++) st[i] = Math.max(st[i], st[i - 1] - slope * this.len[i - 1]);
|
|
783
|
+
for (let i = N - 1; i >= 0; i--) st[i] = Math.max(st[i], st[i + 1] - slope * this.len[i]);
|
|
784
|
+
const tmp = this.stackTmp;
|
|
785
|
+
for (let it = 0; it < 3; it++) {
|
|
786
|
+
for (let i = 0; i <= N; i++) tmp[i] = 0.25 * st[Math.max(0, i - 1)] + 0.5 * st[i] + 0.25 * st[Math.min(N, i + 1)];
|
|
787
|
+
for (let i = 0; i <= N; i++) st[i] = tmp[i];
|
|
788
|
+
}
|
|
789
|
+
const k = this.P.stackW;
|
|
790
|
+
for (let i = 0; i <= N; i++) st[i] *= k;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// ----------------------------------------------------------------- head, jaw, tongue, fangs
|
|
794
|
+
solveHead(dt) {
|
|
795
|
+
const P = this.P, inp = this.input;
|
|
796
|
+
const px = this.px, py = this.py, pz = this.pz;
|
|
797
|
+
const s = this.s;
|
|
798
|
+
// neck direction (segment 0) and base head orientation
|
|
799
|
+
const fx = px[0] - px[1], fy = py[0] - py[1], fz = pz[0] - pz[1];
|
|
800
|
+
const fh = h2(fx, fz);
|
|
801
|
+
const segYaw = fh > 1e-9 ? Math.atan2(fx, fz) : this.heading;
|
|
802
|
+
const segEl = Math.atan2(fy, Math.max(fh, 1e-9));
|
|
803
|
+
const moving = smooth(0.005, 0.05, this.speed / this.sq) * (this.script ? 0 : 1);
|
|
804
|
+
const stab = (this.gait ? this.gait[3] : 0.6) * moving * P.stabW;
|
|
805
|
+
let yawT = segYaw + clamp(angDiff(segYaw, this.heading), -0.8, 0.8) * stab;
|
|
806
|
+
// look target: turn the head the rest of the way
|
|
807
|
+
const look = inp.look || P.look;
|
|
808
|
+
if (look && P.lookW > 0.01) {
|
|
809
|
+
const want = Math.atan2(look.x - px[0], look.z - pz[0]);
|
|
810
|
+
yawT += clamp(angDiff(yawT, want), -0.7, 0.7) * P.lookW;
|
|
811
|
+
this.lookTarget.copy(look);
|
|
812
|
+
this.state.lookTarget = this.lookTarget;
|
|
813
|
+
} else this.state.lookTarget = null;
|
|
814
|
+
yawT += P.headYaw;
|
|
815
|
+
// elevation: level with the ground under the head when lying, level in space when raised
|
|
816
|
+
const cyaw = Math.cos(yawT), syaw = Math.sin(yawT);
|
|
817
|
+
const g0 = this.terrainH(px[0], pz[0]);
|
|
818
|
+
const gN = this.terrainH(px[0] + syaw * this.headLen, pz[0] + cyaw * this.headLen);
|
|
819
|
+
const groundEl = Math.atan2(gN - g0, this.headLen);
|
|
820
|
+
const raised = smooth(0.003 * s, 0.02 * s, py[0] - g0 - this.r[0]);
|
|
821
|
+
let elT = lerp(groundEl, lerp(segEl, 0, 0.75), raised) + P.headPitch;
|
|
822
|
+
if (look && P.lookW > 0.01) {
|
|
823
|
+
const dy = look.y - (py[0] + 0.3 * this.headLen), dd = h2(look.x - px[0], look.z - pz[0]);
|
|
824
|
+
elT += clamp(Math.atan2(dy, dd) * 0.6, -0.4, 0.5) * P.lookW * raised;
|
|
825
|
+
}
|
|
826
|
+
if (!this._headInit) { this.hYaw.reset(0); this.hPitch.reset(elT); this.hRoll.reset(0); this._headInit = true; }
|
|
827
|
+
// yaw spring works on the offset from the neck direction (no wrap-around)
|
|
828
|
+
const yawOff = this.hYaw.step(angDiff(segYaw, yawT), P.headOmega, dt);
|
|
829
|
+
const yaw = segYaw + yawOff;
|
|
830
|
+
let el = this.hPitch.step(elT, P.headOmega, dt);
|
|
831
|
+
const rollT = (P.roll[0] + P.roll[1]) * 0.5 + P.headRoll;
|
|
832
|
+
const rl = this.hRoll.step(rollT, P.headOmega, dt);
|
|
833
|
+
// head rotation (bind space faces +Z)
|
|
834
|
+
const q = this.qHead;
|
|
835
|
+
q.setFromAxisAngle(_Y, yaw);
|
|
836
|
+
_q.setFromAxisAngle(_X, -el); q.multiply(_q);
|
|
837
|
+
_q.setFromAxisAngle(_Z, rl); q.multiply(_q);
|
|
838
|
+
const ga = this.gape.step(P.gape, P.gapeOmega, dt);
|
|
839
|
+
// snout and chin (with the jaw's gape) never enter the ground: pitch up about v0 just enough
|
|
840
|
+
for (let k = 0; k < 4; k++) {
|
|
841
|
+
const b = k === 0 ? this.noseB : k === 1 ? this.chinB : this.jawMid;
|
|
842
|
+
_v.copy(b);
|
|
843
|
+
if (k === 3) _v.x = -_v.x;
|
|
844
|
+
if (k >= 1) { rotAbout(_m1, this.hingeB, _X, ga); _v.applyMatrix4(_m1); }
|
|
845
|
+
_v.sub(this.v0b).applyQuaternion(q);
|
|
846
|
+
const wy = py[0] + _v.y, floor = this.terrainH(px[0] + _v.x, pz[0] + _v.z) + (k === 0 ? 0.001 * s : 0.0003 * s);
|
|
847
|
+
if (wy < floor) {
|
|
848
|
+
const hl = h2(_v.x, _v.z) || 1e-6;
|
|
849
|
+
const add = Math.atan2(floor - wy, hl);
|
|
850
|
+
el += add;
|
|
851
|
+
q.setFromAxisAngle(_Y, yaw); _q.setFromAxisAngle(_X, -el); q.multiply(_q); _q.setFromAxisAngle(_Z, rl); q.multiply(_q);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (el > this.hPitch.x) { this.hPitch.x = el; this.hPitch.v = Math.max(0, this.hPitch.v); }
|
|
855
|
+
// a rolled head (limp, dead) must not push the corners of its open jaw into the ground
|
|
856
|
+
let rr = rl;
|
|
857
|
+
for (let it = 0; it < 5 && Math.abs(rr) > 1e-3; it++) {
|
|
858
|
+
let pen = false;
|
|
859
|
+
for (let k = 0; k < 2 && !pen; k++) {
|
|
860
|
+
_v.copy(this.jawCorner).setX(k ? -this.jawCorner.x : this.jawCorner.x);
|
|
861
|
+
rotAbout(_m1, this.hingeB, _X, ga); _v.applyMatrix4(_m1);
|
|
862
|
+
_v.sub(this.v0b).applyQuaternion(q);
|
|
863
|
+
if (py[0] + _v.y < this.terrainH(px[0] + _v.x, pz[0] + _v.z)) pen = true;
|
|
864
|
+
}
|
|
865
|
+
if (!pen) break;
|
|
866
|
+
rr *= 0.6;
|
|
867
|
+
q.setFromAxisAngle(_Y, yaw); _q.setFromAxisAngle(_X, -el); q.multiply(_q); _q.setFromAxisAngle(_Z, rr); q.multiply(_q);
|
|
868
|
+
}
|
|
869
|
+
// head-rigid transform T = translate(v0 world) * R * translate(-v0 bind)
|
|
870
|
+
const T = this.Tm;
|
|
871
|
+
T.makeRotationFromQuaternion(q);
|
|
872
|
+
_v.copy(this.v0b).applyQuaternion(q);
|
|
873
|
+
T.setPosition(px[0] - _v.x, py[0] - _v.y, pz[0] - _v.z);
|
|
874
|
+
this.bHead.matrixWorld.multiplyMatrices(T, this.bindHead);
|
|
875
|
+
this.headPose.position.set(px[0], py[0], pz[0]);
|
|
876
|
+
this.headPose.quaternion.copy(q);
|
|
877
|
+
// jaw: gape about the quadrate hinge (bind X axis)
|
|
878
|
+
rotAbout(_m1, this.hingeB, _X, ga);
|
|
879
|
+
_m2.multiplyMatrices(T, _m1);
|
|
880
|
+
this.bJaw.matrixWorld.multiplyMatrices(_m2, this.bindJaw);
|
|
881
|
+
// fangs swing forward from the palate as the mouth opens (vipers)
|
|
882
|
+
const fe = this.fang.step(this.viper ? smooth(0.35, 0.9, ga / Math.max(0.3, this.cfg.strike.gape * Math.PI / 180)) * 1.75 + P.fang : 0, 18, dt);
|
|
883
|
+
rotAbout(_m1, this.fangBaseB, _X, -fe);
|
|
884
|
+
_m3.multiplyMatrices(T, _m1);
|
|
885
|
+
this.bFang.matrixWorld.multiplyMatrices(_m3, this.bindFang);
|
|
886
|
+
// tongue: slides out of its sheath (with the jaw), dips, the fork tips oscillate
|
|
887
|
+
this.solveTongue(dt, ga);
|
|
888
|
+
const tg = this.tongue;
|
|
889
|
+
_m3.makeTranslation(this.tongueDirB.x * tg.extD, this.tongueDirB.y * tg.extD, this.tongueDirB.z * tg.extD);
|
|
890
|
+
rotAbout(_m1, this.tongueBaseB, _X, tg.pitch);
|
|
891
|
+
_m3.multiply(_m1);
|
|
892
|
+
_m2.multiply(_m3); // T * jaw * tongue
|
|
893
|
+
this.bTongue.matrixWorld.multiplyMatrices(_m2, this.bindTongue);
|
|
894
|
+
rotAbout(_m1, this.tongueForkB, _X, tg.tip);
|
|
895
|
+
_m2.multiply(_m1);
|
|
896
|
+
this.bTip.matrixWorld.multiplyMatrices(_m2, this.bindTip);
|
|
897
|
+
// the flicking tips may brush the ground but never go through it: lift the whole tongue
|
|
898
|
+
const e = this.bTip.matrixWorld.elements, Lt = this.tipLen;
|
|
899
|
+
const tx = e[12] + e[4] * Lt, ty = e[13] + e[5] * Lt, tz = e[14] + e[6] * Lt;
|
|
900
|
+
const lo = Math.min(ty, e[13]) - this.terrainH(tx, tz) - 0.0008 * this.s;
|
|
901
|
+
// the lift is kept (it only relaxes slowly), so the tongue never snaps back into the ground
|
|
902
|
+
const need = lo < 0 && tg.extD > 1e-5 ? -lo : 0;
|
|
903
|
+
tg.lift = Math.max(need, (tg.lift || 0) * Math.exp(-dt * 6));
|
|
904
|
+
if (tg.lift > 0) {
|
|
905
|
+
this.bTongue.matrixWorld.elements[13] += tg.lift;
|
|
906
|
+
e[13] += tg.lift;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
solveTongue(dt, gape) {
|
|
911
|
+
const tg = this.tongue, P = this.P, cfg = this.cfg.tongue;
|
|
912
|
+
const allow = P.tongue * (gape < 0.25 ? 1 : 0);
|
|
913
|
+
tg.next -= dt * (0.4 + 0.6 * P.tongueRate);
|
|
914
|
+
if (tg.dur <= 0 && tg.next <= 0 && allow > 0.5) {
|
|
915
|
+
tg.dur = 0.35 + this.rand() * 0.45;
|
|
916
|
+
tg.t = 0;
|
|
917
|
+
tg.osc = cfg.flicks[0] + this.rand() * (cfg.flicks[1] - cfg.flicks[0]);
|
|
918
|
+
tg.next = (1 / Math.max(0.05, cfg.rate)) * (0.5 + this.rand());
|
|
919
|
+
}
|
|
920
|
+
let ext = 0, tip = 0, pitch = 0;
|
|
921
|
+
const amp = cfg.amp ?? 1; // a longer tongue whips its tips farther for the same angle
|
|
922
|
+
if (tg.dur > 0) {
|
|
923
|
+
tg.t += dt;
|
|
924
|
+
const t = tg.t, d = tg.dur;
|
|
925
|
+
ext = smooth(0, 0.09, t) * (1 - smooth(d - 0.09, d, t));
|
|
926
|
+
// tips oscillate at the species' flick frequency, ramped in and out over the flick
|
|
927
|
+
const env = Math.sin(clamp((t - 0.05) / (d - 0.1), 0, 1) * Math.PI);
|
|
928
|
+
const osc = Math.sin(TAU * cfg.freq * Math.max(0, t - 0.05)) * env;
|
|
929
|
+
tip = 0.45 * amp * osc * ext;
|
|
930
|
+
pitch = (0.16 + 0.18 * amp * osc) * ext;
|
|
931
|
+
if (t >= d) tg.dur = 0;
|
|
932
|
+
if (allow < 0.5 && t < d - 0.09) tg.t = Math.max(t, d - 0.09); // cut short: retract now
|
|
933
|
+
}
|
|
934
|
+
ext = Math.max(ext, P.tongueOut);
|
|
935
|
+
// springs keep the tongue's own motion smooth whatever starts or cuts a flick
|
|
936
|
+
const w = 55;
|
|
937
|
+
tg.ext.step(ext, w, dt);
|
|
938
|
+
tg.tipS.step(tip, w, dt);
|
|
939
|
+
tg.pitchS.step(pitch + 0.25 * P.tongueOut + 0.08 * (amp < 1 ? 1 : 0) * ext, w, dt);
|
|
940
|
+
const e = clamp(tg.ext.x, 0, 1);
|
|
941
|
+
tg.extD = e * cfg.length * this.headLen;
|
|
942
|
+
tg.tip = tg.tipS.x;
|
|
943
|
+
tg.pitch = tg.pitchS.x;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// ----------------------------------------------------------------- state
|
|
947
|
+
publish() {
|
|
948
|
+
const st = this.state, N = this.N;
|
|
949
|
+
st.heading = this.heading;
|
|
950
|
+
st.speed = this.speed;
|
|
951
|
+
const moving = this.speed > 0.004 * this.sq || this.script;
|
|
952
|
+
st.gait = moving ? (this.script ? 'slither' : this.gaitName) : 'stand';
|
|
953
|
+
st.stats.gait = st.gait; st.stats.freq = this.freq || 0; st.stats.duty = 1;
|
|
954
|
+
st.action = this.layer.current();
|
|
955
|
+
st.posture = this.layer.posture;
|
|
956
|
+
for (let i = 0; i <= N; i++) {
|
|
957
|
+
const c = st.contacts[i];
|
|
958
|
+
c.contact = !!this.contact[i];
|
|
959
|
+
c.position.set(this.hx[i], this.terrainH(this.hx[i], this.hz[i]), this.hz[i]);
|
|
960
|
+
}
|
|
961
|
+
const dbg = this.debug.targets;
|
|
962
|
+
dbg[0].copy(this.leader); dbg[1].copy(this.C);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// rotation by angle a about an axis through point c (bind space) -> out
|
|
967
|
+
function rotAbout(out, c, axis, a) {
|
|
968
|
+
_q2.setFromAxisAngle(axis, a);
|
|
969
|
+
out.makeRotationFromQuaternion(_q2);
|
|
970
|
+
_v4.copy(c).applyQuaternion(_q2);
|
|
971
|
+
out.setPosition(c.x - _v4.x, c.y - _v4.y, c.z - _v4.z);
|
|
972
|
+
return out;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// ------------------------------------------------------------------------------------------------
|
|
976
|
+
// Pose parameters (filled by the action layer every frame)
|
|
977
|
+
// ------------------------------------------------------------------------------------------------
|
|
978
|
+
function makePose(N) {
|
|
979
|
+
return {
|
|
980
|
+
lift: new Float64Array(N + 1), dyaw: new Float64Array(N), roll: new Float64Array(N + 1),
|
|
981
|
+
speedScale: 1, headYaw: 0, headPitch: 0, headRoll: 0, headOmega: 9, stabW: 1,
|
|
982
|
+
gape: 0, gapeOmega: 14, fang: 0, tongue: 1, tongueRate: 1, tongueOut: 0,
|
|
983
|
+
look: null, lookW: 1, stackW: 1,
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
function resetPose(P, affect) {
|
|
987
|
+
P.lift.fill(0); P.dyaw.fill(0); P.roll.fill(0); affect.fill(0);
|
|
988
|
+
P.speedScale = 1; P.headYaw = 0; P.headPitch = 0; P.headRoll = 0; P.headOmega = 9; P.stabW = 1;
|
|
989
|
+
P.gape = 0; P.gapeOmega = 14; P.fang = 0; P.tongue = 1; P.tongueRate = 1; P.tongueOut = 0;
|
|
990
|
+
P.look = null; P.lookW = 1; P.stackW = 1;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// ------------------------------------------------------------------------------------------------
|
|
994
|
+
// Actions
|
|
995
|
+
// ------------------------------------------------------------------------------------------------
|
|
996
|
+
// Postures (sit, lie, sleep, coil, death) hold until 'stand' or a move request. Coil-family postures
|
|
997
|
+
// (coil = resting coil, sleep = coil + stillness, sit = alert coil with the forebody raised in an S)
|
|
998
|
+
// crawl into a spiral first. One-shots (strike / attack, jump, eat, drink, hit) blend over whatever
|
|
999
|
+
// runs and end by themselves. play() returns a Promise (postures: resolved once in the pose).
|
|
1000
|
+
class SnakeActions {
|
|
1001
|
+
constructor(e) {
|
|
1002
|
+
this.e = e;
|
|
1003
|
+
this.postures = []; // { name, t, w, target, shapeW, reached, resolveIn, side, ... }
|
|
1004
|
+
this.oneshots = [];
|
|
1005
|
+
this.posture = 'stand';
|
|
1006
|
+
this.standReq = null;
|
|
1007
|
+
this.idle = { w: 0, t: 0, lookT: 2, look: new THREE.Vector3(), has: false, raise: 0, raiseT: 3, raiseS: new Spring(0), yawS: new Spring(0), bend: 0 };
|
|
1008
|
+
this.lookS = { raise: new Spring(0), bend: new Spring(0), w: new Spring(0) };
|
|
1009
|
+
this.coiled = false;
|
|
1010
|
+
this._t = new THREE.Vector3();
|
|
1011
|
+
}
|
|
1012
|
+
holdsStill() {
|
|
1013
|
+
if (this.postures.length) return true;
|
|
1014
|
+
for (let i = 0; i < this.oneshots.length; i++) { const a = this.oneshots[i]; if (a.still && !a.stopping) return true; }
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
dead() { for (let i = 0; i < this.postures.length; i++) { const p = this.postures[i]; if (p.name === 'death' && p.target > 0) return true; } return false; }
|
|
1018
|
+
// automatic stand-up (asked to move): never revives a dead snake
|
|
1019
|
+
requestStand() { if (!this.standReq && !this.dead()) this.play('stand'); }
|
|
1020
|
+
// arbitration: a new one-shot interrupts the running
|
|
1021
|
+
// ones (fade out, the promise resolves 'interrupted'); a posture ends them (except a hit); stop()
|
|
1022
|
+
// ends everything but death; dead: only hit is taken. Promises resolve 'done' | 'interrupted' |
|
|
1023
|
+
// 'stopped' | 'refused'.
|
|
1024
|
+
end(a, reason) { if (a.stopping) return; a.stopping = true; a.reason = reason; }
|
|
1025
|
+
current() {
|
|
1026
|
+
for (let i = 0; i < this.oneshots.length; i++) if (!this.oneshots[i].stopping) return this.oneshots[i].name;
|
|
1027
|
+
for (let i = 0; i < this.postures.length; i++) if (this.postures[i].target > 0) return this.postures[i].name;
|
|
1028
|
+
return this.standReq ? 'stand' : null;
|
|
1029
|
+
}
|
|
1030
|
+
clear() {
|
|
1031
|
+
for (const a of [...this.postures, ...this.oneshots]) { a.resolveIn?.('stopped'); a.resolve?.('stopped'); }
|
|
1032
|
+
this.postures.length = 0; this.oneshots.length = 0;
|
|
1033
|
+
if (this.standReq) { this.standReq.resolve('stopped'); this.standReq = null; }
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
play(name, opts = {}) {
|
|
1037
|
+
const e = this.e;
|
|
1038
|
+
if (name === 'idle') name = 'stand';
|
|
1039
|
+
if (name === 'attack') name = 'strike';
|
|
1040
|
+
if (name === 'stand') {
|
|
1041
|
+
if (!this.postures.some((p) => p.target > 0) && !this.postures.length) return Promise.resolve('done');
|
|
1042
|
+
for (const p of this.postures) if (p.target > 0) { p.target = 0; p.resolveIn?.('interrupted'); }
|
|
1043
|
+
e.emit('actionStart', { name: 'stand' });
|
|
1044
|
+
if (this.standReq) return this.standReq.promise;
|
|
1045
|
+
let resolve;
|
|
1046
|
+
const promise = new Promise((r) => { resolve = r; });
|
|
1047
|
+
this.standReq = { promise, resolve };
|
|
1048
|
+
return promise;
|
|
1049
|
+
}
|
|
1050
|
+
if (POSTURES.includes(name)) {
|
|
1051
|
+
const cur = this.postures.find((p) => p.target > 0);
|
|
1052
|
+
if (cur && cur.name === 'death' && name !== 'death') return Promise.resolve('refused');
|
|
1053
|
+
if (cur && cur.name === name) return cur.promiseIn;
|
|
1054
|
+
for (const p of this.postures) if (p.target > 0) { p.target = 0; p.resolveIn?.('interrupted'); }
|
|
1055
|
+
if (this.standReq) { this.standReq.resolve('interrupted'); this.standReq = null; }
|
|
1056
|
+
for (const a of this.oneshots) if (a.name !== 'hit') this.end(a, 'interrupted');
|
|
1057
|
+
let side = opts.side ?? (e.rand() < 0.5 ? -1 : 1);
|
|
1058
|
+
if (opts.direction && name === 'death') {
|
|
1059
|
+
const d = opts.direction;
|
|
1060
|
+
side = d.x * Math.cos(e.heading) - d.z * Math.sin(e.heading) > 0 ? 1 : -1;
|
|
1061
|
+
}
|
|
1062
|
+
const inst = { name, t: 0, w: 0, target: 1, side, opts, reached: false, baked: false, phase0: e.rand() * TAU };
|
|
1063
|
+
inst.promiseIn = new Promise((r) => { inst.resolveIn = r; });
|
|
1064
|
+
if (COILED[name]) {
|
|
1065
|
+
// already lying in a coil (the body winds through more than ~300 degrees): stay put
|
|
1066
|
+
const wound = Math.abs(e.byawU[0] - e.byawU[e.N - 1]) > 1.7 * Math.PI;
|
|
1067
|
+
if (!this.coiled && !wound) this.startCoil(inst);
|
|
1068
|
+
else { inst.coilDone = true; this.coiled = true; }
|
|
1069
|
+
}
|
|
1070
|
+
this.postures.push(inst);
|
|
1071
|
+
e.emit('actionStart', { name });
|
|
1072
|
+
if (name === 'death') e.emit('death', { position: e.pos.clone() });
|
|
1073
|
+
return inst.promiseIn;
|
|
1074
|
+
}
|
|
1075
|
+
const def = ONESHOTS[name];
|
|
1076
|
+
if (!def) return Promise.reject(new Error(`procedural-animals: unknown action "${name}"`));
|
|
1077
|
+
if (this.dead() && name !== 'hit') return Promise.resolve('refused');
|
|
1078
|
+
if (this.postures.some((p) => p.target > 0) && (name === 'jump' || name === 'strike') && !this.postures.some((p) => p.name === 'sit' && p.target > 0)) {
|
|
1079
|
+
return this.play('stand').then((r) => (r === 'refused' ? r : this.play(name, opts)));
|
|
1080
|
+
}
|
|
1081
|
+
// a new one-shot interrupts the running ones (they fade out, their promises resolve)
|
|
1082
|
+
for (const a of this.oneshots) this.end(a, 'interrupted');
|
|
1083
|
+
const inst = { name, t: 0, w: 0, dur: 1, stopping: false, opts, fade: 0.15, still: false, side: e.rand() < 0.5 ? -1 : 1 };
|
|
1084
|
+
def.start(inst, this);
|
|
1085
|
+
inst.promise = new Promise((r) => { inst.resolve = r; });
|
|
1086
|
+
this.oneshots.push(inst);
|
|
1087
|
+
e.emit('actionStart', { name });
|
|
1088
|
+
return inst.promise;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
stop(name) {
|
|
1092
|
+
if (name === 'attack') name = 'strike';
|
|
1093
|
+
for (const a of this.oneshots) if (!name || a.name === name) this.end(a, 'stopped');
|
|
1094
|
+
if (!name || POSTURES.includes(name)) if (this.postures.some((p) => p.target > 0 && (name ? p.name === name : p.name !== 'death'))) this.play('stand');
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
startCoil(inst) {
|
|
1098
|
+
const e = this.e, tune = e.cfg.actions.coil || {};
|
|
1099
|
+
// a real resting coil is not a machined spiral: the ring spacing is a little tighter than the body
|
|
1100
|
+
// (inner turns ride up onto the outer ones), the curvature wanders (seeded), the centre is small
|
|
1101
|
+
// and the whole body, tail tip included, ends up in the coil
|
|
1102
|
+
const pitch = 2 * e.hwMax * (tune.pitch ?? 0.88);
|
|
1103
|
+
const rin = Math.max(0.022 * e.TL, 1.8 * e.hw[0] + 0.008 * e.s);
|
|
1104
|
+
const total = e.bodyLen * 1.08;
|
|
1105
|
+
const dir = inst.side;
|
|
1106
|
+
const f1 = 0.16 * e.TL * (0.8 + 0.4 * e.rand()), f2 = 0.07 * e.TL * (0.8 + 0.4 * e.rand()), p1 = e.rand() * TAU, p2 = e.rand() * TAU;
|
|
1107
|
+
const sc = {
|
|
1108
|
+
total, speed: (tune.speed ?? 0.34) * e.sq,
|
|
1109
|
+
kappa: (u) => (dir * (1 + 0.13 * Math.sin((TAU * u) / f1 + p1) + 0.06 * Math.sin((TAU * u) / f2 + p2))) / Math.sqrt(rin * rin + (pitch * Math.max(0, total - u)) / Math.PI),
|
|
1110
|
+
done: false,
|
|
1111
|
+
};
|
|
1112
|
+
e.startScript(sc);
|
|
1113
|
+
inst.script = sc;
|
|
1114
|
+
this.coiled = true;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
update(dt) {
|
|
1118
|
+
const e = this.e, P = e.P, N = e.N;
|
|
1119
|
+
resetPose(P, e.affect);
|
|
1120
|
+
// ---- postures
|
|
1121
|
+
for (const p of this.postures) {
|
|
1122
|
+
p.t += dt;
|
|
1123
|
+
const coilWait = p.script && !p.script.done;
|
|
1124
|
+
if (p.target > 0) p.w = Math.min(1, p.w + dt / (p.name === 'death' ? 0.2 : 0.8));
|
|
1125
|
+
else p.w = Math.max(0, p.w - dt / (p.name === 'death' ? 1.0 : 0.7));
|
|
1126
|
+
if (!p.reached && p.target > 0 && p.w >= 1 && !coilWait && p.t > (p.name === 'death' ? 1.8 : p.name === 'lie' ? 1.6 : 0.6)) {
|
|
1127
|
+
p.reached = true; p.resolveIn('done'); e.emit('actionEnd', { name: p.name });
|
|
1128
|
+
}
|
|
1129
|
+
// leaving the posture: once lifts and rolls are gone, bake the body shape into the trail
|
|
1130
|
+
if (p.target === 0 && p.w <= 0 && !p.baked) {
|
|
1131
|
+
if (p.script && !p.script.done) { e.script = null; e.reseed = true; }
|
|
1132
|
+
p.baked = true;
|
|
1133
|
+
p.bakeNow = true;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
// any posture that changed the shape (death, lie) is baked when it ends
|
|
1137
|
+
let bake = false;
|
|
1138
|
+
for (let i = 0; i < this.postures.length; i++) { const p = this.postures[i]; if (p.bakeNow && (p.name === 'death' || p.name === 'lie')) bake = true; }
|
|
1139
|
+
if (bake) this.coiled = false; // the body shape changed (death, lie)
|
|
1140
|
+
compact(this.postures, notBaked);
|
|
1141
|
+
if (bake) this.bake();
|
|
1142
|
+
let active = null;
|
|
1143
|
+
for (let i = 0; i < this.postures.length; i++) if (this.postures[i].target > 0) { active = this.postures[i]; break; }
|
|
1144
|
+
this.posture = active ? (active.name === 'death' ? 'dead' : active.name) : this.postures.length ? 'standing-up' : 'stand';
|
|
1145
|
+
if (this.standReq && !this.postures.length) { this.standReq.resolve('done'); this.standReq = null; e.emit('actionEnd', { name: 'stand' }); }
|
|
1146
|
+
if (!active && e.speed > 0.01) this.coiled = false;
|
|
1147
|
+
|
|
1148
|
+
// ---- idle and look-at
|
|
1149
|
+
const still = e.speed < 0.01 * e.sq && !e.input.follow && !e.script;
|
|
1150
|
+
let busy = false;
|
|
1151
|
+
for (let i = 0; i < this.oneshots.length; i++) if (!this.oneshots[i].stopping) busy = true;
|
|
1152
|
+
const idleT = still && !this.postures.length && !busy ? 1 : 0;
|
|
1153
|
+
const id = this.idle;
|
|
1154
|
+
id.w += clamp(idleT - id.w, -dt * 1.5, dt * 0.6);
|
|
1155
|
+
this.applyIdle(dt, id.w);
|
|
1156
|
+
this.applyLook(dt);
|
|
1157
|
+
// rattlesnakes carry the rattle just off the ground while they travel
|
|
1158
|
+
if (e.cfg.tailCarry > 0) {
|
|
1159
|
+
this.carry = (this.carry || 0) + (smooth(0.003, 0.03, e.speed / e.sq) * (this.postures.length ? 0 : 1) - (this.carry || 0)) * (1 - Math.exp(-dt * 3));
|
|
1160
|
+
if (this.carry > 1e-3) {
|
|
1161
|
+
const Lt = 0.07 * e.TL;
|
|
1162
|
+
for (let i = e.N; i >= 0 && e.bodyLen - e.sig[i] < Lt; i--) {
|
|
1163
|
+
const q = 1 - (e.bodyLen - e.sig[i]) / Lt;
|
|
1164
|
+
P.lift[i] += this.carry * e.cfg.tailCarry * q * q;
|
|
1165
|
+
e.affect[i] += this.carry * q;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
// ---- postures
|
|
1170
|
+
for (const p of this.postures) {
|
|
1171
|
+
const w = p.w * p.w * (3 - 2 * p.w);
|
|
1172
|
+
if (w <= 0) continue;
|
|
1173
|
+
POSES[p.name](P, w, p, this, dt);
|
|
1174
|
+
}
|
|
1175
|
+
// ---- one-shots
|
|
1176
|
+
const wantsToMove = e.wantSpeed > 0.01 || !!e.input.follow;
|
|
1177
|
+
for (const a of this.oneshots) {
|
|
1178
|
+
if (wantsToMove && (a.name === 'eat' || a.name === 'drink')) this.end(a, 'interrupted');
|
|
1179
|
+
a.t += dt;
|
|
1180
|
+
const def = ONESHOTS[a.name];
|
|
1181
|
+
if (!a.stopping && def.update(a, dt, this)) this.end(a, 'done');
|
|
1182
|
+
if (a.stopping) a.w = Math.max(0, a.w - dt / a.fade);
|
|
1183
|
+
else a.w = Math.min(1, a.w + dt / a.fade);
|
|
1184
|
+
const w = a.w * a.w * (3 - 2 * a.w);
|
|
1185
|
+
if (w > 0) def.apply(P, w, a, this);
|
|
1186
|
+
}
|
|
1187
|
+
for (const a of this.oneshots) if (a.stopping && a.w <= 0) { a.resolve(a.reason || 'done'); e.emit('actionEnd', { name: a.name, reason: a.reason || 'done' }); }
|
|
1188
|
+
compact(this.oneshots, stillRunning);
|
|
1189
|
+
// the neck never kinks: cap the per-segment yaw change of the posed body
|
|
1190
|
+
void N;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// the current body shape becomes the trail (the snake then crawls off along its own body)
|
|
1194
|
+
bake() {
|
|
1195
|
+
const e = this.e, N = e.N, tr = e.trail;
|
|
1196
|
+
// current ground points, tail -> head, parameterised by the rest arc lengths
|
|
1197
|
+
const sl = tr.lastS;
|
|
1198
|
+
const tail = tr.lastS - e.sig[N];
|
|
1199
|
+
// keep the trail behind the tail: extend straight from the posed tail
|
|
1200
|
+
const tx = e.hx[N] - e.hx[N - 1], tz = e.hz[N] - e.hz[N - 1], tl = h2(tx, tz) || 1;
|
|
1201
|
+
tr.clear();
|
|
1202
|
+
const ext = 0.2 * e.TL;
|
|
1203
|
+
for (let k = 4; k >= 1; k--) {
|
|
1204
|
+
const d = (ext * k) / 4, x = e.hx[N] + (tx / tl) * d, z = e.hz[N] + (tz / tl) * d;
|
|
1205
|
+
tr.push(x, e.terrainH(x, z), z, tail - d);
|
|
1206
|
+
}
|
|
1207
|
+
for (let i = N; i >= 0; i--) tr.push(e.hx[i], e.terrainH(e.hx[i], e.hz[i]), e.hz[i], sl - e.sig[i]);
|
|
1208
|
+
e.reseed = true;
|
|
1209
|
+
e.leader.set(e.hx[0], e.terrainH(e.hx[0], e.hz[0]), e.hz[0]);
|
|
1210
|
+
e.C.copy(e.leader); e.pos.copy(e.C);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
applyIdle(dt, w) {
|
|
1214
|
+
const e = this.e, P = e.P, id = this.idle, t = e.time, s = e.s;
|
|
1215
|
+
if (w <= 0.001) { id.has = false; id.raiseS.step(0, 3, dt); id.yawS.step(0, 3, dt); return; }
|
|
1216
|
+
id.t += dt;
|
|
1217
|
+
// gentle head sway and breathing (a slow roll of the trunk)
|
|
1218
|
+
P.headYaw += w * (0.05 * Math.sin(t * 0.7) + 0.03 * Math.sin(t * 1.9 + 1));
|
|
1219
|
+
P.headPitch += w * 0.03 * Math.sin(t * 0.43 + 2);
|
|
1220
|
+
const br = e.cfg.breath;
|
|
1221
|
+
const b = Math.sin(t * TAU * br.rate) * br.roll * w;
|
|
1222
|
+
for (let i = 0; i <= e.N; i++) { const q = e.sig[i] / e.bodyLen; P.roll[i] += b * smooth(0.1, 0.3, q) * (1 - smooth(0.6, 0.85, q)); }
|
|
1223
|
+
// now and then: raise the head and look about
|
|
1224
|
+
id.raiseT -= dt;
|
|
1225
|
+
if (id.raiseT <= 0) {
|
|
1226
|
+
id.raiseT = 2.5 + e.rand() * 4;
|
|
1227
|
+
id.raise = e.rand() < 0.45 ? 0.02 + e.rand() * 0.035 : 0;
|
|
1228
|
+
id.bend = (e.rand() - 0.5) * 1.1;
|
|
1229
|
+
}
|
|
1230
|
+
const rz = id.raiseS.step(id.raise * w, 1.6, dt), bd = id.yawS.step(id.bend * (id.raise > 0 ? 1 : 0.3) * w, 1.4, dt);
|
|
1231
|
+
if (!e.input.look) this.raiseNeck(P, rz * e.TL, bd, e.cfg.body.neck * e.TL * 1.4, 1);
|
|
1232
|
+
void s;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// raise the forebody to height h (m) over arc length Ln, and bend the neck by yaw b (rad)
|
|
1236
|
+
raiseNeck(P, h, b, Ln, w) {
|
|
1237
|
+
const e = this.e, N = e.N;
|
|
1238
|
+
Ln = Math.max(Ln, 2.3 * Math.abs(h));
|
|
1239
|
+
for (let i = 0; i <= N; i++) {
|
|
1240
|
+
const sg = e.sig[i];
|
|
1241
|
+
if (sg > Ln) break;
|
|
1242
|
+
const q = sg / Ln; // 0 head .. 1 base of the raised part
|
|
1243
|
+
// the neck rises straight up to the head and flattens into the ground at its base
|
|
1244
|
+
const f = Math.pow(1 - q, 1.7);
|
|
1245
|
+
P.lift[i] += w * h * f;
|
|
1246
|
+
if (i < N) {
|
|
1247
|
+
const qm = (sg + e.len[i] * 0.5) / Ln;
|
|
1248
|
+
P.dyaw[i] += w * b * (1 - smooth(0, 1, qm));
|
|
1249
|
+
}
|
|
1250
|
+
e.affect[i] += w * f;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
applyLook(dt) {
|
|
1255
|
+
const e = this.e, P = e.P, L = this.lookS;
|
|
1256
|
+
const look = e.input.look;
|
|
1257
|
+
let posed = false;
|
|
1258
|
+
for (let i = 0; i < this.postures.length; i++) { const p = this.postures[i]; if (p.target > 0 && (p.name === 'death' || p.name === 'sleep')) posed = true; }
|
|
1259
|
+
for (let i = 0; i < this.oneshots.length; i++) { const a = this.oneshots[i]; if (!a.stopping && (a.name === 'strike' || a.name === 'eat' || a.name === 'jump')) posed = true; }
|
|
1260
|
+
let raise = 0, bend = 0, wT = 0;
|
|
1261
|
+
if (look && !posed) {
|
|
1262
|
+
wT = 1;
|
|
1263
|
+
const hx = e.hx[0], hz = e.hz[0];
|
|
1264
|
+
const to = Math.atan2(look.x - hx, look.z - hz);
|
|
1265
|
+
bend = clamp(angDiff(e.byaw[0], to), -1.5, 1.5) * (e.speed > 0.02 ? 0.3 : 1);
|
|
1266
|
+
const d = h2(look.x - hx, look.z - hz);
|
|
1267
|
+
const dy = look.y - e.terrainH(hx, hz);
|
|
1268
|
+
raise = clamp(0.03 * e.TL + Math.max(0, dy) * 0.25 + 0.04 * e.TL * smooth(3, 0.5, d / e.TL), 0.02 * e.TL, 0.14 * e.TL) * (e.speed > 0.02 ? 0.3 : 1);
|
|
1269
|
+
}
|
|
1270
|
+
const w = L.w.step(wT, 3, dt);
|
|
1271
|
+
const r = L.raise.step(raise, 2.5, dt), b = L.bend.step(bend, 2.5, dt);
|
|
1272
|
+
if (w > 0.001) this.raiseNeck(P, r, b, e.cfg.body.neck * e.TL * (1.2 + Math.abs(b) * 0.6), w);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// ---------------------------------------------------------------- forebody shapes (strike family)
|
|
1277
|
+
// S-coil in the horizontal plane, raised at the front; extension e (0..1) straightens it toward the
|
|
1278
|
+
// target. Writes yaw offsets and lifts with weight w.
|
|
1279
|
+
function strikeShape(P, a, L, w, e, beta, hS, aimYaw, aimLift, curl) {
|
|
1280
|
+
const E = L.e, N = E.N;
|
|
1281
|
+
const Ls = a.Ls, ia = a.ia;
|
|
1282
|
+
const baseA = E.byawU[Math.min(N - 1, ia)];
|
|
1283
|
+
for (let i = 0; i < N && E.sig[i] <= Ls; i++) {
|
|
1284
|
+
const q = (E.sig[i] + E.len[i] * 0.5) / Ls; // 0 head .. 1 anchor
|
|
1285
|
+
const envS = Math.sin(Math.PI * Math.min(1, q * 1.05));
|
|
1286
|
+
// S-coil yaw about the aim direction (two bends), aim ramp near the anchor
|
|
1287
|
+
const aimRamp = 1 - smooth(0.55, 1, q);
|
|
1288
|
+
const ySx = aimYaw * aimRamp + beta * Math.sin(TAU * q * curl) * envS;
|
|
1289
|
+
const yE = aimYaw * (1 - smooth(0.55, 1, q));
|
|
1290
|
+
const yaw = baseA + lerp(ySx, yE, e);
|
|
1291
|
+
// hand over to the body's own curvature toward the anchor (no curvature step there)
|
|
1292
|
+
P.dyaw[i] += w * (yaw - E.byawU[i]) * (1 - smooth(0.5, 1, q));
|
|
1293
|
+
}
|
|
1294
|
+
for (let i = 0; i <= N && E.sig[i] <= Ls; i++) {
|
|
1295
|
+
const q = E.sig[i] / Ls;
|
|
1296
|
+
const lS = hS * Math.pow(1 - smooth(0, 1, q), 1.2);
|
|
1297
|
+
const lE = aimLift * (1 - smooth(0.05, 1, q));
|
|
1298
|
+
P.lift[i] += w * lerp(lS, lE, e);
|
|
1299
|
+
E.affect[i] += w * (1 - smooth(0.85, 1, q));
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function setupStrike(a, L, target, reachK) {
|
|
1304
|
+
const E = L.e, N = E.N;
|
|
1305
|
+
const t = target || L._t.set(E.hx[0] + Math.sin(E.byaw[0]) * 0.3 * E.TL, E.terrainH(E.hx[0], E.hz[0]) + 0.02 * E.TL, E.hz[0] + Math.cos(E.byaw[0]) * 0.3 * E.TL);
|
|
1306
|
+
a.target = new THREE.Vector3().copy(t);
|
|
1307
|
+
const maxL = reachK * E.TL;
|
|
1308
|
+
// forebody length: enough to reach the target from the anchor, within limits
|
|
1309
|
+
let Ls = maxL;
|
|
1310
|
+
for (let it = 0; it < 3; it++) {
|
|
1311
|
+
let ia = 0; while (ia < N && E.sig[ia] < Ls) ia++;
|
|
1312
|
+
const d = h2(a.target.x - E.hx[ia], a.target.z - E.hz[ia]);
|
|
1313
|
+
Ls = clamp(d * 1.04, 0.14 * E.TL, maxL);
|
|
1314
|
+
}
|
|
1315
|
+
let ia = 0; while (ia < N && E.sig[ia] < Ls) ia++;
|
|
1316
|
+
a.Ls = E.sig[ia]; a.ia = ia;
|
|
1317
|
+
const ax = E.hx[ia], az = E.hz[ia];
|
|
1318
|
+
const to = Math.atan2(a.target.x - ax, a.target.z - az);
|
|
1319
|
+
a.aimYaw = clamp(angDiff(E.byaw[Math.min(N - 1, ia)], to), -1.2, 1.2);
|
|
1320
|
+
const dh = h2(a.target.x - ax, a.target.z - az);
|
|
1321
|
+
const gy = E.terrainH(a.target.x, a.target.z);
|
|
1322
|
+
a.aimLift = clamp(a.target.y - gy - E.r[0], 0, 0.6 * a.Ls) * smooth(0, 1, a.Ls / Math.max(dh, 1e-3));
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
const ONESHOTS = {
|
|
1326
|
+
// S-coil, strike at the target with the jaw wide open (attackHit), retract, relax
|
|
1327
|
+
strike: {
|
|
1328
|
+
start(a, L) {
|
|
1329
|
+
const E = L.e, st = E.cfg.strike;
|
|
1330
|
+
setupStrike(a, L, a.opts.target, st.reach);
|
|
1331
|
+
a.still = true;
|
|
1332
|
+
a.T = { cock: 0.5, hold: 0.62, hit: 0.62 + clamp(a.Ls / (st.speed * E.sq * 0.9), 0.14, 0.22) };
|
|
1333
|
+
a.T.stay = a.T.hit + 0.14; a.T.back = a.T.stay + 0.32; a.T.end = a.T.back + 0.6;
|
|
1334
|
+
a.dur = a.T.end;
|
|
1335
|
+
a.fade = 0.2;
|
|
1336
|
+
a.hitDone = false;
|
|
1337
|
+
},
|
|
1338
|
+
update(a, dt, L) {
|
|
1339
|
+
const E = L.e, T = a.T;
|
|
1340
|
+
if (!a.hitDone && a.t >= T.hit) {
|
|
1341
|
+
a.hitDone = true;
|
|
1342
|
+
_v.set(0, 0, 0);
|
|
1343
|
+
const m = E.attachmentPoints.mouth;
|
|
1344
|
+
const pos = new THREE.Vector3();
|
|
1345
|
+
if (m) pos.setFromMatrixPosition(_m1.multiplyMatrices(E.bHead.matrixWorld, m.local)); else pos.set(E.px[0], E.py[0], E.pz[0]);
|
|
1346
|
+
const dir = new THREE.Vector3(0, 0, 1).applyQuaternion(E.qHead);
|
|
1347
|
+
E.emit('attackHit', { position: pos, direction: dir, style: 'strike', target: a.target.clone() });
|
|
1348
|
+
}
|
|
1349
|
+
return a.t >= a.dur;
|
|
1350
|
+
},
|
|
1351
|
+
apply(P, w, a, L) {
|
|
1352
|
+
const E = L.e, T = a.T, t = a.t, st = E.cfg.strike;
|
|
1353
|
+
const cock = smooth(0, T.cock, t) * (1 - smooth(T.back, T.end, t));
|
|
1354
|
+
const ext = smootherstep(T.hold, T.hit, t) * (1 - smootherstep(T.stay, T.back, t));
|
|
1355
|
+
const hS = st.coilHeight * E.TL;
|
|
1356
|
+
strikeShape(P, a, L, w * cock, ext, 1.25, hS, a.aimYaw, a.aimLift, 1);
|
|
1357
|
+
P.speedScale *= 1 - w;
|
|
1358
|
+
const openT = smooth(0.45, 0.85, ext);
|
|
1359
|
+
P.gape += w * openT * st.gape * Math.PI / 180;
|
|
1360
|
+
P.gapeOmega = Math.max(P.gapeOmega, 40);
|
|
1361
|
+
P.headPitch += w * (0.12 * cock * (1 - ext) + 0.25 * openT);
|
|
1362
|
+
P.headOmega = Math.max(P.headOmega, lerp(9, 40, w * ext));
|
|
1363
|
+
P.stabW *= 1 - w;
|
|
1364
|
+
P.tongue *= 1 - w * cock;
|
|
1365
|
+
P.lookW *= 1 - w;
|
|
1366
|
+
P.stackW *= 1 - w;
|
|
1367
|
+
},
|
|
1368
|
+
},
|
|
1369
|
+
// a forward lunge (snakes do not jump): a flat, closed-mouth strike straight ahead
|
|
1370
|
+
jump: {
|
|
1371
|
+
start(a, L) {
|
|
1372
|
+
const E = L.e;
|
|
1373
|
+
const h = E.byaw[0];
|
|
1374
|
+
a.opts.target = new THREE.Vector3(E.hx[0] + Math.sin(h) * 0.22 * E.TL, E.terrainH(E.hx[0], E.hz[0]) + 0.03 * E.TL, E.hz[0] + Math.cos(h) * 0.22 * E.TL);
|
|
1375
|
+
setupStrike(a, L, a.opts.target, 0.26);
|
|
1376
|
+
a.still = true;
|
|
1377
|
+
a.T = { cock: 0.4, hold: 0.45, hit: 0.7, stay: 0.8, back: 1.15, end: 1.6 };
|
|
1378
|
+
a.dur = a.T.end; a.fade = 0.2;
|
|
1379
|
+
},
|
|
1380
|
+
update(a) { return a.t >= a.dur; },
|
|
1381
|
+
apply(P, w, a, L) {
|
|
1382
|
+
const E = L.e, T = a.T, t = a.t;
|
|
1383
|
+
const cock = smooth(0, T.cock, t) * (1 - smooth(T.back, T.end, t));
|
|
1384
|
+
const ext = smootherstep(T.hold, T.hit, t) * (1 - smootherstep(T.stay, T.back, t));
|
|
1385
|
+
strikeShape(P, a, L, w * cock, ext, 0.5, 0.03 * E.TL, a.aimYaw, a.aimLift, 1);
|
|
1386
|
+
P.speedScale *= 1 - w;
|
|
1387
|
+
P.headPitch += w * 0.05 * cock;
|
|
1388
|
+
P.headOmega = Math.max(P.headOmega, lerp(9, 30, w * ext));
|
|
1389
|
+
P.stabW *= 1 - w; P.lookW *= 1 - w; P.tongue *= 1 - w * cock; P.stackW *= 1 - w;
|
|
1390
|
+
},
|
|
1391
|
+
},
|
|
1392
|
+
// strike at prey on the ground ahead, then swallow: jaws walk left and right, the neck works
|
|
1393
|
+
eat: {
|
|
1394
|
+
start(a, L) {
|
|
1395
|
+
const E = L.e, h = E.byaw[0];
|
|
1396
|
+
a.opts.target = a.opts.target || new THREE.Vector3(E.hx[0] + Math.sin(h) * 0.12 * E.TL, 0, E.hz[0] + Math.cos(h) * 0.12 * E.TL);
|
|
1397
|
+
a.opts.target.y = E.terrainH(a.opts.target.x, a.opts.target.z) + 0.005 * E.s;
|
|
1398
|
+
setupStrike(a, L, a.opts.target, 0.22);
|
|
1399
|
+
a.still = true;
|
|
1400
|
+
const sw = E.cfg.actions.eat?.swallow ?? 3.2;
|
|
1401
|
+
a.T = { cock: 0.35, hold: 0.4, hit: 0.62, stay: 0.9, back: 1.3, swallow: 1.3 + sw, yawn: 1.3 + sw + 0.7 };
|
|
1402
|
+
a.dur = a.T.yawn + 0.3; a.fade = 0.25;
|
|
1403
|
+
},
|
|
1404
|
+
update(a) { return a.t >= a.dur; },
|
|
1405
|
+
apply(P, w, a, L) {
|
|
1406
|
+
const E = L.e, T = a.T, t = a.t;
|
|
1407
|
+
const cock = smooth(0, T.cock, t) * (1 - smooth(T.stay, T.back, t));
|
|
1408
|
+
const ext = smootherstep(T.hold, T.hit, t) * (1 - smootherstep(T.stay, T.back, t));
|
|
1409
|
+
strikeShape(P, a, L, w * cock, ext, 0.3, 0.025 * E.TL, a.aimYaw, 0, 1);
|
|
1410
|
+
P.gape += w * smooth(0.3, 0.8, ext) * 0.7;
|
|
1411
|
+
P.gapeOmega = Math.max(P.gapeOmega, 25);
|
|
1412
|
+
// swallowing: alternate jaw walks, a pulse down the neck
|
|
1413
|
+
const sw = smooth(T.back - 0.2, T.back + 0.3, t) * (1 - smooth(T.swallow - 0.3, T.swallow, t));
|
|
1414
|
+
const ph = (t - T.back) * TAU * 0.7;
|
|
1415
|
+
P.headYaw += w * sw * 0.14 * Math.sin(ph);
|
|
1416
|
+
P.headRoll += w * sw * 0.1 * Math.sin(ph);
|
|
1417
|
+
P.gape += w * sw * (0.25 + 0.15 * Math.sin(ph * 2));
|
|
1418
|
+
P.headPitch += w * sw * 0.06 * Math.sin(ph + 1);
|
|
1419
|
+
const pulse = ((t - T.back) * 0.12 * E.TL) % (0.2 * E.TL);
|
|
1420
|
+
for (let i = 0; i <= E.N && E.sig[i] < 0.25 * E.TL; i++) {
|
|
1421
|
+
const d = (E.sig[i] - pulse) / (0.03 * E.TL);
|
|
1422
|
+
P.lift[i] += w * sw * 0.004 * E.s * Math.exp(-d * d);
|
|
1423
|
+
}
|
|
1424
|
+
// yawn to reset the jaws
|
|
1425
|
+
const yw = smooth(T.swallow, T.swallow + 0.3, t) * (1 - smooth(T.yawn - 0.3, T.yawn, t));
|
|
1426
|
+
P.gape += w * yw * 1.1;
|
|
1427
|
+
P.headPitch += w * yw * 0.25;
|
|
1428
|
+
P.speedScale *= 1 - w; P.stabW *= 1 - w; P.lookW *= 1 - w; P.tongue *= 1 - w; void yw;
|
|
1429
|
+
},
|
|
1430
|
+
},
|
|
1431
|
+
// head down, snout to the ground / water surface, buccal pumping
|
|
1432
|
+
drink: {
|
|
1433
|
+
start(a, L) { a.still = true; a.dur = 1.0 + (L.e.cfg.actions.drink?.pumps ?? 4) * 0.8; a.fade = 0.4; },
|
|
1434
|
+
update(a) { return a.t >= a.dur; },
|
|
1435
|
+
apply(P, w, a, L) {
|
|
1436
|
+
const t = a.t;
|
|
1437
|
+
const down = smooth(0, 0.6, t);
|
|
1438
|
+
P.headPitch -= w * down * 0.35;
|
|
1439
|
+
const pump = Math.max(0, Math.sin((t - 0.6) * TAU * 1.25)) * smooth(0.6, 0.9, t);
|
|
1440
|
+
P.gape += w * down * (0.03 + 0.06 * pump);
|
|
1441
|
+
P.gapeOmega = Math.max(P.gapeOmega, 20);
|
|
1442
|
+
P.tongue *= 1 - w * down;
|
|
1443
|
+
P.speedScale *= 1 - w; P.lookW *= 1 - w; P.stabW *= 1 - w;
|
|
1444
|
+
},
|
|
1445
|
+
},
|
|
1446
|
+
// flinch into a curve away from the blow, head withdrawn, tail whipping, then settle back
|
|
1447
|
+
hit: {
|
|
1448
|
+
start(a, L) {
|
|
1449
|
+
const E = L.e;
|
|
1450
|
+
const d = a.opts.direction;
|
|
1451
|
+
if (d) a.side = d.x * Math.cos(E.heading) - d.z * Math.sin(E.heading) > 0 ? 1 : -1;
|
|
1452
|
+
a.dur = 1.4; a.fade = 0.2;
|
|
1453
|
+
},
|
|
1454
|
+
update(a) { return a.t >= a.dur; },
|
|
1455
|
+
apply(P, w, a, L) {
|
|
1456
|
+
const E = L.e, t = a.t, N = E.N;
|
|
1457
|
+
const k = smootherstep(0, 0.3, t) * (1 - smootherstep(0.45, 1.35, t));
|
|
1458
|
+
const ov = 0;
|
|
1459
|
+
for (let i = 0; i < N; i++) {
|
|
1460
|
+
const q = (E.sig[i] + E.len[i] * 0.5) / E.bodyLen;
|
|
1461
|
+
// C-bend: forebody swings with the blow, the tail whips the other way
|
|
1462
|
+
const prof = 1.0 * (1 - smooth(0, 0.45, q)) - 0.55 * smooth(0.55, 1, q);
|
|
1463
|
+
P.dyaw[i] += w * a.side * (k * 0.75 + 0.2 * ov) * prof;
|
|
1464
|
+
}
|
|
1465
|
+
for (let i = 0; i <= N; i++) E.affect[i] += w * k;
|
|
1466
|
+
L.raiseNeck(P, 0.035 * E.TL * k, -a.side * 0.3 * k, 0.12 * E.TL, w);
|
|
1467
|
+
P.headYaw += w * a.side * 0.35 * k;
|
|
1468
|
+
P.gape += w * k * (E.viper ? 0.45 : 0.2);
|
|
1469
|
+
P.speedScale *= 1 - w * k;
|
|
1470
|
+
P.lookW *= 1 - w * k;
|
|
1471
|
+
P.stackW *= 1 - w * k;
|
|
1472
|
+
if (E.cfg.rattle) rattle(P, E, w * smooth(0, 0.3, t), t);
|
|
1473
|
+
},
|
|
1474
|
+
},
|
|
1475
|
+
};
|
|
1476
|
+
|
|
1477
|
+
const notBaked = (p) => !p.baked;
|
|
1478
|
+
const stillRunning = (a) => !(a.stopping && a.w <= 0);
|
|
1479
|
+
function compact(arr, keep) {
|
|
1480
|
+
let j = 0;
|
|
1481
|
+
for (let i = 0; i < arr.length; i++) if (keep(arr[i])) arr[j++] = arr[i];
|
|
1482
|
+
arr.length = j;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function smootherstep(a, b, x) {
|
|
1486
|
+
const t = clamp((x - a) / (b - a), 0, 1);
|
|
1487
|
+
return t * t * t * (t * (t * 6 - 15) + 10);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// tail tip raised and buzzing (rattlesnake)
|
|
1491
|
+
function rattle(P, E, w, t) {
|
|
1492
|
+
if (w <= 0) return;
|
|
1493
|
+
const N = E.N, Lt = 0.11 * E.TL;
|
|
1494
|
+
const buzz = Math.sin(t * TAU * 27);
|
|
1495
|
+
for (let i = N; i >= 0 && E.bodyLen - E.sig[i] < Lt; i--) {
|
|
1496
|
+
const q = 1 - (E.bodyLen - E.sig[i]) / Lt; // 0 .. 1 at the tip
|
|
1497
|
+
P.lift[i] += w * 0.036 * E.TL * q * q;
|
|
1498
|
+
E.affect[i] += w * q;
|
|
1499
|
+
if (i < N) P.dyaw[i] += w * 0.03 * buzz * smooth(0.5, 1, q);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// ---------------------------------------------------------------- posture poses
|
|
1504
|
+
const POSES = {
|
|
1505
|
+
coil(P, w, p, L) {
|
|
1506
|
+
const E = L.e;
|
|
1507
|
+
P.speedScale *= 1 - w;
|
|
1508
|
+
P.tongueRate *= 1 - 0.4 * w;
|
|
1509
|
+
// once coiled, the neck swings out over the inner ring and the head rests on top of the coils
|
|
1510
|
+
const done = p.coilDone || (p.script && p.script.done);
|
|
1511
|
+
if (done) {
|
|
1512
|
+
if (p.restT === undefined) p.restT = p.t;
|
|
1513
|
+
const k = w * smooth(0, 1.2, p.t - p.restT);
|
|
1514
|
+
L.raiseNeck(P, 1.5 * E.hwMax * k, -p.side * 0.75 * k, 0.14 * E.TL, 1);
|
|
1515
|
+
P.headPitch += k * 0.06;
|
|
1516
|
+
}
|
|
1517
|
+
},
|
|
1518
|
+
sleep(P, w, p, L) {
|
|
1519
|
+
POSES.coil(P, w, p, L);
|
|
1520
|
+
const c = w * smooth(0.5, 2.5, p.t);
|
|
1521
|
+
P.tongue *= 1 - c; P.lookW *= 1 - c; P.stabW *= 1 - c;
|
|
1522
|
+
P.headPitch += c * 0.04;
|
|
1523
|
+
P.headOmega = Math.min(P.headOmega, 4);
|
|
1524
|
+
},
|
|
1525
|
+
// alert / defensive coil: the forebody rises in an S facing the look target (rattlesnakes rattle)
|
|
1526
|
+
sit(P, w, p, L) {
|
|
1527
|
+
const E = L.e;
|
|
1528
|
+
POSES.coil(P, w, p, L);
|
|
1529
|
+
const up = w * (p.script ? smooth(0.6, 1, p.script.u / p.script.total) : 1);
|
|
1530
|
+
if (up <= 0) return;
|
|
1531
|
+
if (!p.S) p.S = { Ls: 0, ia: 0, aimYaw: 0, aimLift: 0 };
|
|
1532
|
+
const S = p.S;
|
|
1533
|
+
const Lf = (E.cfg.actions.sit?.raise ?? 0.1) * E.TL * 2.6;
|
|
1534
|
+
let ia = 0; while (ia < E.N && E.sig[ia] < Lf) ia++;
|
|
1535
|
+
S.Ls = E.sig[ia]; S.ia = ia; S.aimYaw = 0; S.aimLift = 0;
|
|
1536
|
+
const look = E.input.look;
|
|
1537
|
+
if (look) S.aimYaw = clamp(angDiff(E.byaw[Math.min(E.N - 1, ia)], Math.atan2(look.x - E.hx[ia], look.z - E.hz[ia])), -1.2, 1.2) * 0.6;
|
|
1538
|
+
strikeShape(P, S, L, up, 0, 0.9, (E.cfg.actions.sit?.raise ?? 0.1) * E.TL, S.aimYaw, 0, 1);
|
|
1539
|
+
P.stackW *= 1 - up;
|
|
1540
|
+
P.tongueRate += up * 1.5;
|
|
1541
|
+
if (E.cfg.rattle) rattle(P, E, up * smooth(0.5, 1.5, p.t), E.time);
|
|
1542
|
+
},
|
|
1543
|
+
// stretched out: the wave relaxes toward the body's mean line, head down
|
|
1544
|
+
lie(P, w, p, L) {
|
|
1545
|
+
const E = L.e, N = E.N;
|
|
1546
|
+
P.speedScale *= 1 - w;
|
|
1547
|
+
const k = w * smooth(0, 1.4, p.t) * 0.8;
|
|
1548
|
+
const win = 0.18 * E.TL;
|
|
1549
|
+
// mean line: yaw averaged over a window along the body (unwrapped relative to the local yaw)
|
|
1550
|
+
for (let i = 0; i < N; i++) {
|
|
1551
|
+
let sum = 0, n = 0;
|
|
1552
|
+
for (let j = 0; j < N; j++) {
|
|
1553
|
+
if (Math.abs(E.sig[j] - E.sig[i]) > win) continue;
|
|
1554
|
+
sum += angDiff(E.byaw[i], E.byaw[j]); n++;
|
|
1555
|
+
}
|
|
1556
|
+
P.dyaw[i] += k * (sum / Math.max(1, n));
|
|
1557
|
+
E.affect[i] += k;
|
|
1558
|
+
}
|
|
1559
|
+
E.affect[N] += k;
|
|
1560
|
+
P.headPitch -= w * 0.02;
|
|
1561
|
+
P.tongueRate *= 1 - 0.5 * w;
|
|
1562
|
+
},
|
|
1563
|
+
// writhe, go limp and roll half onto the back; stays down until 'stand'
|
|
1564
|
+
death(P, w, p, L) {
|
|
1565
|
+
const E = L.e, N = E.N, t = p.t;
|
|
1566
|
+
P.speedScale = 0;
|
|
1567
|
+
const writhe = 0.55 * smooth(0, 0.12, t) * Math.exp(-t / 0.55);
|
|
1568
|
+
const slack = 0.32 * smooth(0.2, 1.4, t);
|
|
1569
|
+
const rollK = smooth(0.35, 1.3, t);
|
|
1570
|
+
const lam = 0.36 * E.TL;
|
|
1571
|
+
for (let i = 0; i < N; i++) {
|
|
1572
|
+
const sg = E.sig[i] + E.len[i] * 0.5;
|
|
1573
|
+
const q = sg / E.bodyLen;
|
|
1574
|
+
const env = smooth(0, 0.08, q) * (1 - 0.75 * smooth(0.78, 1, q));
|
|
1575
|
+
const wv = writhe * Math.sin(TAU * (sg / lam - 1.7 * t) + p.phase0) + slack * Math.sin(TAU * sg / (0.55 * E.TL) + p.phase0 * 1.7) * 0.8;
|
|
1576
|
+
P.dyaw[i] += w * wv * env;
|
|
1577
|
+
}
|
|
1578
|
+
for (let i = 0; i <= N; i++) {
|
|
1579
|
+
const q = E.sig[i] / E.bodyLen;
|
|
1580
|
+
E.affect[i] += w;
|
|
1581
|
+
// only parts turn belly-up: neck upright, mid-body over ~160 deg, a second partial twist behind
|
|
1582
|
+
const c1 = 0.4 + 0.1 * Math.sin(p.phase0), c2 = 0.76 + 0.06 * Math.cos(p.phase0 * 1.3);
|
|
1583
|
+
const prof = 2.8 * Math.exp(-(((q - c1) / 0.15) ** 2)) + 1.05 * Math.exp(-(((q - c2) / 0.09) ** 2));
|
|
1584
|
+
P.roll[i] += w * p.side * rollK * prof;
|
|
1585
|
+
}
|
|
1586
|
+
P.gape += w * 0.2 * smooth(0.4, 1.2, t);
|
|
1587
|
+
P.gapeOmega = 6;
|
|
1588
|
+
P.tongueOut += w * 0.45 * smooth(0.8, 1.6, t);
|
|
1589
|
+
P.tongue = 0;
|
|
1590
|
+
P.headRoll += w * p.side * 0.6 * rollK;
|
|
1591
|
+
P.headOmega = lerp(P.headOmega, 5, w);
|
|
1592
|
+
P.lookW = 0; P.stabW = 0; P.stackW *= 1 - w;
|
|
1593
|
+
},
|
|
1594
|
+
};
|