@bitefoundry/retrieviewer 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ASSET-PROVENANCE.md +6 -0
- package/ASSETS-LICENSE.md +2 -1
- package/ATTRIBUTION.md +6 -1
- package/COMFORT-ART.md +65 -0
- package/DESKTOP-PLATFORMS.md +6 -1
- package/PLUGIN.md +13 -10
- package/PREREQUISITES.md +95 -0
- package/PUPPY-MEMORY.md +102 -1
- package/README.ko.md +110 -0
- package/README.md +117 -59
- package/RELEASING.md +23 -12
- package/THIRD-PARTY-NOTICES.md +5 -3
- package/braille.mjs +35 -9
- package/examples/live.mjs +2 -2
- package/integrations/linux-overlay/overlay.py +2 -2
- package/integrations/macos-overlay/overlay.jxa +8 -6
- package/integrations/opencode/runtime.mjs +4 -4
- package/integrations/opencode/tui.tsx +1 -1
- package/integrations/windows-overlay/Overlay.cs +4 -4
- package/integrations/windows-overlay/protocol.mjs +11 -7
- package/package.json +34 -13
- package/puppy-comfort-sprites.mjs +25 -0
- package/puppy-companion-comfort-q5.bin.gz +0 -0
- package/puppy-companion-comfort.json +77 -0
- package/puppy-companion-life.mjs +49 -16
- package/puppy-companion-objects.mjs +12 -0
- package/puppy-frame-predictive.mjs +15 -9
- package/puppy-frame-store.mjs +2 -2
- package/puppy-paper-world.mjs +14 -0
- package/puppy-paper.mjs +65 -14
- package/retrieviewer-plugin.d.mts +1 -1
- package/retrieviewer-plugin.mjs +1 -1
- package/retrieviewer.d.mts +10 -4
- package/retrieviewer.mjs +20 -14
- package/locomotion-core.mjs +0 -21
- package/locomotion-puppy.animal +0 -0
- package/vendor/procedural-animals/package.json +0 -40
- package/vendor/procedural-animals/src/animal.js +0 -146
- package/vendor/procedural-animals/src/bake.js +0 -51
- package/vendor/procedural-animals/src/build.js +0 -81
- package/vendor/procedural-animals/src/core/build/coatKit.js +0 -121
- package/vendor/procedural-animals/src/core/build/featherCards.js +0 -189
- package/vendor/procedural-animals/src/core/build/finEdges.js +0 -85
- package/vendor/procedural-animals/src/core/build/furSlope.js +0 -115
- package/vendor/procedural-animals/src/core/build/mesh.js +0 -67
- package/vendor/procedural-animals/src/core/build/pipeline.js +0 -272
- package/vendor/procedural-animals/src/core/build/seams.js +0 -183
- package/vendor/procedural-animals/src/core/build/warp.js +0 -112
- package/vendor/procedural-animals/src/core/build/weights.js +0 -393
- package/vendor/procedural-animals/src/core/math/vec.js +0 -81
- package/vendor/procedural-animals/src/core/motion/actions.js +0 -800
- package/vendor/procedural-animals/src/core/motion/bird.js +0 -3063
- package/vendor/procedural-animals/src/core/motion/birdActions.js +0 -1059
- package/vendor/procedural-animals/src/core/motion/common.js +0 -158
- package/vendor/procedural-animals/src/core/motion/index.js +0 -22
- package/vendor/procedural-animals/src/core/motion/quadruped.js +0 -2875
- package/vendor/procedural-animals/src/core/motion/snake.js +0 -1594
- package/vendor/procedural-animals/src/core/motion/spider.js +0 -1810
- package/vendor/procedural-animals/src/core/motion/swimmer.js +0 -1047
- package/vendor/procedural-animals/src/core/motion/swimmerActions.js +0 -613
- package/vendor/procedural-animals/src/core/render/animalObject.js +0 -262
- package/vendor/procedural-animals/src/core/render/coatMaterial.js +0 -1301
- package/vendor/procedural-animals/src/core/render/dqs.js +0 -159
- package/vendor/procedural-animals/src/core/render/eyes.js +0 -676
- package/vendor/procedural-animals/src/core/render/membranes.js +0 -77
- package/vendor/procedural-animals/src/core/render/skeleton.js +0 -41
- package/vendor/procedural-animals/src/core/rig/bird.js +0 -327
- package/vendor/procedural-animals/src/core/rig/quadruped.js +0 -76
- package/vendor/procedural-animals/src/core/rig/rig.js +0 -79
- package/vendor/procedural-animals/src/core/rig/snake.js +0 -76
- package/vendor/procedural-animals/src/core/rig/spider.js +0 -110
- package/vendor/procedural-animals/src/core/rig/swimmer.js +0 -181
- package/vendor/procedural-animals/src/core/sdf/eyeSocket.js +0 -53
- package/vendor/procedural-animals/src/core/sdf/mesher.js +0 -238
- package/vendor/procedural-animals/src/core/sdf/sdf.js +0 -263
- package/vendor/procedural-animals/src/index.d.ts +0 -98
- package/vendor/procedural-animals/src/index.js +0 -29
- package/vendor/procedural-animals/src/registry.js +0 -48
- package/vendor/procedural-animals/src/species/bear/behaviour.js +0 -463
- package/vendor/procedural-animals/src/species/bear/claws.js +0 -147
- package/vendor/procedural-animals/src/species/bear/coat.js +0 -726
- package/vendor/procedural-animals/src/species/bear/index.js +0 -139
- package/vendor/procedural-animals/src/species/bear/motion.js +0 -112
- package/vendor/procedural-animals/src/species/bear/regions.js +0 -78
- package/vendor/procedural-animals/src/species/bear/rig.js +0 -112
- package/vendor/procedural-animals/src/species/bear/sculpt.js +0 -272
- package/vendor/procedural-animals/src/species/boar/behaviour.js +0 -335
- package/vendor/procedural-animals/src/species/boar/coat.js +0 -372
- package/vendor/procedural-animals/src/species/boar/index.js +0 -110
- package/vendor/procedural-animals/src/species/boar/motion.js +0 -85
- package/vendor/procedural-animals/src/species/boar/regions.js +0 -88
- package/vendor/procedural-animals/src/species/boar/rig.js +0 -111
- package/vendor/procedural-animals/src/species/boar/sculpt.js +0 -285
- package/vendor/procedural-animals/src/species/cat/behaviour.js +0 -559
- package/vendor/procedural-animals/src/species/cat/coat.js +0 -841
- package/vendor/procedural-animals/src/species/cat/index.js +0 -211
- package/vendor/procedural-animals/src/species/cat/motion.js +0 -101
- package/vendor/procedural-animals/src/species/cat/regions.js +0 -82
- package/vendor/procedural-animals/src/species/cat/rig.js +0 -100
- package/vendor/procedural-animals/src/species/cat/sculpt.js +0 -266
- package/vendor/procedural-animals/src/species/cat/whiskers.js +0 -110
- package/vendor/procedural-animals/src/species/cheetah/coat.js +0 -427
- package/vendor/procedural-animals/src/species/cheetah/index.js +0 -51
- package/vendor/procedural-animals/src/species/cheetah/joints.js +0 -3
- package/vendor/procedural-animals/src/species/cheetah/motion.js +0 -77
- package/vendor/procedural-animals/src/species/cheetah/regions.js +0 -95
- package/vendor/procedural-animals/src/species/cheetah/rig.js +0 -66
- package/vendor/procedural-animals/src/species/cheetah/sculpt.js +0 -158
- package/vendor/procedural-animals/src/species/chicken/coat.js +0 -313
- package/vendor/procedural-animals/src/species/chicken/coverts.js +0 -320
- package/vendor/procedural-animals/src/species/chicken/index.js +0 -110
- package/vendor/procedural-animals/src/species/chicken/motion.js +0 -177
- package/vendor/procedural-animals/src/species/chicken/regions.js +0 -66
- package/vendor/procedural-animals/src/species/chicken/rig.js +0 -144
- package/vendor/procedural-animals/src/species/chicken/sculpt.js +0 -389
- package/vendor/procedural-animals/src/species/chicken/wingLook.js +0 -66
- package/vendor/procedural-animals/src/species/chicken/wingStack.js +0 -124
- package/vendor/procedural-animals/src/species/cow/behaviour.js +0 -521
- package/vendor/procedural-animals/src/species/cow/coat.js +0 -465
- package/vendor/procedural-animals/src/species/cow/index.js +0 -118
- package/vendor/procedural-animals/src/species/cow/motion.js +0 -81
- package/vendor/procedural-animals/src/species/cow/regions.js +0 -98
- package/vendor/procedural-animals/src/species/cow/rig.js +0 -116
- package/vendor/procedural-animals/src/species/cow/sculpt.js +0 -339
- package/vendor/procedural-animals/src/species/crow/coat.js +0 -145
- package/vendor/procedural-animals/src/species/crow/index.js +0 -70
- package/vendor/procedural-animals/src/species/crow/joints.js +0 -3
- package/vendor/procedural-animals/src/species/crow/motion.js +0 -101
- package/vendor/procedural-animals/src/species/crow/regions.js +0 -50
- package/vendor/procedural-animals/src/species/crow/rig.js +0 -83
- package/vendor/procedural-animals/src/species/crow/sculpt.js +0 -118
- package/vendor/procedural-animals/src/species/deer/behaviour.js +0 -513
- package/vendor/procedural-animals/src/species/deer/coat.js +0 -391
- package/vendor/procedural-animals/src/species/deer/index.js +0 -106
- package/vendor/procedural-animals/src/species/deer/motion.js +0 -97
- package/vendor/procedural-animals/src/species/deer/regions.js +0 -60
- package/vendor/procedural-animals/src/species/deer/rig.js +0 -124
- package/vendor/procedural-animals/src/species/deer/sculpt.js +0 -297
- package/vendor/procedural-animals/src/species/dog/behaviour.js +0 -521
- package/vendor/procedural-animals/src/species/dog/coat.js +0 -495
- package/vendor/procedural-animals/src/species/dog/index.js +0 -144
- package/vendor/procedural-animals/src/species/dog/motion.js +0 -118
- package/vendor/procedural-animals/src/species/dog/regions.js +0 -61
- package/vendor/procedural-animals/src/species/dog/rig.js +0 -129
- package/vendor/procedural-animals/src/species/dog/sculpt.js +0 -246
- package/vendor/procedural-animals/src/species/eagle/coat.js +0 -250
- package/vendor/procedural-animals/src/species/eagle/index.js +0 -76
- package/vendor/procedural-animals/src/species/eagle/joints.js +0 -3
- package/vendor/procedural-animals/src/species/eagle/motion.js +0 -137
- package/vendor/procedural-animals/src/species/eagle/regions.js +0 -57
- package/vendor/procedural-animals/src/species/eagle/rig.js +0 -107
- package/vendor/procedural-animals/src/species/eagle/sculpt.js +0 -137
- package/vendor/procedural-animals/src/species/fish/coat.js +0 -271
- package/vendor/procedural-animals/src/species/fish/index.js +0 -88
- package/vendor/procedural-animals/src/species/fish/motion.js +0 -92
- package/vendor/procedural-animals/src/species/fish/regions.js +0 -27
- package/vendor/procedural-animals/src/species/fish/rig.js +0 -143
- package/vendor/procedural-animals/src/species/fish/sculpt.js +0 -214
- package/vendor/procedural-animals/src/species/fish/variants.js +0 -126
- package/vendor/procedural-animals/src/species/fox/behaviour.js +0 -314
- package/vendor/procedural-animals/src/species/fox/coat.js +0 -444
- package/vendor/procedural-animals/src/species/fox/index.js +0 -120
- package/vendor/procedural-animals/src/species/fox/motion.js +0 -81
- package/vendor/procedural-animals/src/species/fox/regions.js +0 -59
- package/vendor/procedural-animals/src/species/fox/rig.js +0 -91
- package/vendor/procedural-animals/src/species/fox/sculpt.js +0 -202
- package/vendor/procedural-animals/src/species/frog/behaviour.js +0 -590
- package/vendor/procedural-animals/src/species/frog/coat.js +0 -305
- package/vendor/procedural-animals/src/species/frog/index.js +0 -97
- package/vendor/procedural-animals/src/species/frog/motion.js +0 -69
- package/vendor/procedural-animals/src/species/frog/regions.js +0 -46
- package/vendor/procedural-animals/src/species/frog/rig.js +0 -156
- package/vendor/procedural-animals/src/species/frog/sculpt.js +0 -240
- package/vendor/procedural-animals/src/species/goat/behaviour.js +0 -509
- package/vendor/procedural-animals/src/species/goat/coat.js +0 -590
- package/vendor/procedural-animals/src/species/goat/index.js +0 -149
- package/vendor/procedural-animals/src/species/goat/motion.js +0 -95
- package/vendor/procedural-animals/src/species/goat/regions.js +0 -98
- package/vendor/procedural-animals/src/species/goat/rig.js +0 -181
- package/vendor/procedural-animals/src/species/goat/sculpt.js +0 -547
- package/vendor/procedural-animals/src/species/horse/coat.js +0 -361
- package/vendor/procedural-animals/src/species/horse/coatSeam.js +0 -122
- package/vendor/procedural-animals/src/species/horse/index.js +0 -80
- package/vendor/procedural-animals/src/species/horse/motion.js +0 -245
- package/vendor/procedural-animals/src/species/horse/regions.js +0 -60
- package/vendor/procedural-animals/src/species/horse/rig.js +0 -117
- package/vendor/procedural-animals/src/species/horse/sculpt.js +0 -316
- package/vendor/procedural-animals/src/species/horse/seamWeights.js +0 -272
- package/vendor/procedural-animals/src/species/lion/behaviour.js +0 -509
- package/vendor/procedural-animals/src/species/lion/coat.js +0 -1016
- package/vendor/procedural-animals/src/species/lion/index.js +0 -111
- package/vendor/procedural-animals/src/species/lion/motion.js +0 -87
- package/vendor/procedural-animals/src/species/lion/regions.js +0 -106
- package/vendor/procedural-animals/src/species/lion/rig.js +0 -110
- package/vendor/procedural-animals/src/species/lion/sculpt.js +0 -384
- package/vendor/procedural-animals/src/species/lion/whiskers.js +0 -132
- package/vendor/procedural-animals/src/species/pig/behaviour.js +0 -575
- package/vendor/procedural-animals/src/species/pig/coat.js +0 -730
- package/vendor/procedural-animals/src/species/pig/ears.js +0 -242
- package/vendor/procedural-animals/src/species/pig/index.js +0 -161
- package/vendor/procedural-animals/src/species/pig/motion.js +0 -105
- package/vendor/procedural-animals/src/species/pig/regions.js +0 -100
- package/vendor/procedural-animals/src/species/pig/rig.js +0 -152
- package/vendor/procedural-animals/src/species/pig/sculpt.js +0 -382
- package/vendor/procedural-animals/src/species/rabbit/behaviour.js +0 -605
- package/vendor/procedural-animals/src/species/rabbit/coat.js +0 -285
- package/vendor/procedural-animals/src/species/rabbit/index.js +0 -85
- package/vendor/procedural-animals/src/species/rabbit/motion.js +0 -85
- package/vendor/procedural-animals/src/species/rabbit/regions.js +0 -48
- package/vendor/procedural-animals/src/species/rabbit/rig.js +0 -99
- package/vendor/procedural-animals/src/species/rabbit/sculpt.js +0 -148
- package/vendor/procedural-animals/src/species/rat/behaviour.js +0 -539
- package/vendor/procedural-animals/src/species/rat/coat.js +0 -341
- package/vendor/procedural-animals/src/species/rat/index.js +0 -111
- package/vendor/procedural-animals/src/species/rat/motion.js +0 -90
- package/vendor/procedural-animals/src/species/rat/regions.js +0 -92
- package/vendor/procedural-animals/src/species/rat/rig.js +0 -112
- package/vendor/procedural-animals/src/species/rat/sculpt.js +0 -163
- package/vendor/procedural-animals/src/species/rat/whiskers.js +0 -119
- package/vendor/procedural-animals/src/species/shark/coat.js +0 -383
- package/vendor/procedural-animals/src/species/shark/index.js +0 -104
- package/vendor/procedural-animals/src/species/shark/motion.js +0 -69
- package/vendor/procedural-animals/src/species/shark/regions.js +0 -34
- package/vendor/procedural-animals/src/species/shark/rig.js +0 -194
- package/vendor/procedural-animals/src/species/shark/sculpt.js +0 -343
- package/vendor/procedural-animals/src/species/shark/variants.js +0 -105
- package/vendor/procedural-animals/src/species/sheep/behaviour.js +0 -522
- package/vendor/procedural-animals/src/species/sheep/coat.js +0 -529
- package/vendor/procedural-animals/src/species/sheep/index.js +0 -152
- package/vendor/procedural-animals/src/species/sheep/motion.js +0 -86
- package/vendor/procedural-animals/src/species/sheep/neck.js +0 -41
- package/vendor/procedural-animals/src/species/sheep/regions.js +0 -62
- package/vendor/procedural-animals/src/species/sheep/rig.js +0 -163
- package/vendor/procedural-animals/src/species/sheep/sculpt.js +0 -542
- package/vendor/procedural-animals/src/species/snake/coat.js +0 -315
- package/vendor/procedural-animals/src/species/snake/index.js +0 -73
- package/vendor/procedural-animals/src/species/snake/motion.js +0 -51
- package/vendor/procedural-animals/src/species/snake/regions.js +0 -51
- package/vendor/procedural-animals/src/species/snake/rig.js +0 -108
- package/vendor/procedural-animals/src/species/snake/sculpt.js +0 -122
- package/vendor/procedural-animals/src/species/spider/coat.js +0 -279
- package/vendor/procedural-animals/src/species/spider/index.js +0 -78
- package/vendor/procedural-animals/src/species/spider/motion.js +0 -72
- package/vendor/procedural-animals/src/species/spider/regions.js +0 -87
- package/vendor/procedural-animals/src/species/spider/rig.js +0 -118
- package/vendor/procedural-animals/src/species/spider/sculpt.js +0 -182
- package/vendor/procedural-animals/src/species/wolf/behaviour.js +0 -160
- package/vendor/procedural-animals/src/species/wolf/coat.js +0 -749
- package/vendor/procedural-animals/src/species/wolf/index.js +0 -107
- package/vendor/procedural-animals/src/species/wolf/motion.js +0 -80
- package/vendor/procedural-animals/src/species/wolf/regions.js +0 -57
- package/vendor/procedural-animals/src/species/wolf/rig.js +0 -87
- package/vendor/procedural-animals/src/species/wolf/sculpt.js +0 -339
- package/vendor/procedural-animals/src/worker.js +0 -29
|
@@ -1,1810 +0,0 @@
|
|
|
1
|
-
// Spider motion engine: eight legs of seven segments, pedipalps, chelicerae with fangs and a hanging
|
|
2
|
-
// abdomen, on uneven ground. Drives the bones of core/rig/spider.js.
|
|
3
|
-
//
|
|
4
|
-
// Locomotion: a phase-offset gait table (alternating tetrapod: L1 R2 L3 R4 against R1 L2 R3 L4, with
|
|
5
|
-
// the metachronal lag inside each tetrapod) blended by speed. Stance claws stay planted where they
|
|
6
|
-
// touched down; swing claws travel on a "ballistic" path (short speed ramps, straight cruise, a
|
|
7
|
-
// rounded apex) whose height clears the terrain sampled along the way, so legs step over bumps.
|
|
8
|
-
// Standing still, legs that end up away from their neutral foothold re-step one tetrapod at a time;
|
|
9
|
-
// turning on the spot runs the gait from the yaw rate. The body height, pitch and roll follow a
|
|
10
|
-
// least-squares plane through the terrain under the eight neutral footholds (springs), and the
|
|
11
|
-
// sternum / abdomen never touch a bump. The abdomen hangs from the pedicel on a spring (lags in turns
|
|
12
|
-
// and speed changes) and breathes (bone scale).
|
|
13
|
-
//
|
|
14
|
-
// Leg solver (body frame, per frame, continuous): the tarsus lies along the leg's azimuth at its
|
|
15
|
-
// bind slope (steeper while the "heel" lifts), which gives the metatarsus-tarsus joint; the coxa
|
|
16
|
-
// yaws part of the way toward it and the trochanter the rest; in the vertical leg plane the
|
|
17
|
-
// metatarsus takes a preferred slope that flattens as the leg stretches, and femur + (patella +
|
|
18
|
-
// tibia, a rigid pair) solve as a two-link chain, knee up, with a soft reach limit. Every bone is
|
|
19
|
-
// written as a rotation from its bind frame: body rotation x yaw about the body's up axis x pitch
|
|
20
|
-
// about the bind leg-plane normal, so the bone roll never flips. Action poses (raised threat legs,
|
|
21
|
-
// held prey, the death curl, the in-air tuck) are blended in the same joint-angle space.
|
|
22
|
-
//
|
|
23
|
-
// Actions: idle (breathing, leg and palp twitches, looking around), jump (a pounce: crouch, hydraulic
|
|
24
|
-
// push-off, front legs raised to grab, landing), sit / lie / sleep (crouch low with the legs drawn
|
|
25
|
-
// in), eat (chelicerae working, palps manipulating, prey held by legs I), drink (mouthparts down),
|
|
26
|
-
// attack (threat pose: front legs raised, fangs out; then a lunge and attackHit), hit (flinch and
|
|
27
|
-
// scuttle back), death (hydraulic leg collapse: the legs curl under the body and it settles on its
|
|
28
|
-
// back or side on whatever ground is there), stand.
|
|
29
|
-
//
|
|
30
|
-
// Lessons from the cheetah engine kept here: continuous solves (no state jumps between frames),
|
|
31
|
-
// soft reach limits, motion from phase not events, springs for secondary motion, zero-dt safety,
|
|
32
|
-
// no allocations in the hot path.
|
|
33
|
-
//
|
|
34
|
-
// ------------------------------------------------------------------------------------------------
|
|
35
|
-
// species.motion schema (all lengths in metres at the reference size; per-variant overrides in
|
|
36
|
-
// `variants: { name: { ...same fields } }`, chosen by params.variant)
|
|
37
|
-
// ------------------------------------------------------------------------------------------------
|
|
38
|
-
// maxSpeed m/s, accel / decel m/s^2, turnRate rad/s, latAccelMax m/s^2 (cornering: wider arcs at speed)
|
|
39
|
-
// gears { name: speed } named speeds (walk, run, sprint)
|
|
40
|
-
// gaits (req.) [{ name, v, f, D, off: [L1 L2 L3 L4 R1 R2 R3 R4], lift, bob, sway }] sorted by v.
|
|
41
|
-
// v: speed of the row, f: stride Hz, D: duty factor, off: touchdown phase per leg, lift:
|
|
42
|
-
// swing height (m), bob: body bounce (m, twice per stride), sway: body yaw sway (rad).
|
|
43
|
-
// stance { reach: [I, II, III, IV] neutral claw distance from the coxa as a fraction of the
|
|
44
|
-
// bind pose, height: stance height factor, width: lateral spread factor }
|
|
45
|
-
// feet { heelLift: tarsus heel lift at push-off (deg) }
|
|
46
|
-
// abdomen { stiffness: spring rate (1/s), lag: how far it swings in accelerations (rad per
|
|
47
|
-
// m/s^2 x s), droop: pitch it sags by when relaxed (rad) }
|
|
48
|
-
// breath { rate Hz, amp: abdomen scale amplitude }
|
|
49
|
-
// chelicerae { fang: 'labidognath' (fangs close sideways: wolf spiders) | 'orthognath' (fangs
|
|
50
|
-
// strike down: tarantulas), fangOpen: rad, spread: rad, pitch: rad }
|
|
51
|
-
// palps { twitch 0..1, tap 0..1 } idle { twitch, raiseFront (rad), look (rad) }
|
|
52
|
-
// look { yaw, pitch }: how far the body turns / rears toward a look target (rad)
|
|
53
|
-
// actions { jump: { height, distance, crouch (s) }, attack: { rear (rad), lunge (m),
|
|
54
|
-
// threat (s) }, sleep: { tuck 0..1 } }
|
|
55
|
-
// ------------------------------------------------------------------------------------------------
|
|
56
|
-
import * as THREE from 'three';
|
|
57
|
-
import { TAU, clamp, lerp, smooth, smin, wrap01, d01, angDiff, Spring, prng, envelope, tv } from './common.js';
|
|
58
|
-
import { LEG_KEYS, legBones, legJoints, palpBones, palpJoints } from '../rig/spider.js';
|
|
59
|
-
|
|
60
|
-
const DEG = Math.PI / 180;
|
|
61
|
-
// swing shaping (exported for tuning tools)
|
|
62
|
-
export const TUNE = { bf: 1.15, bp: 0.25, bm: 0.2, ramp: 0.22, path: 'joint' };
|
|
63
|
-
const G_ACC = 9.81;
|
|
64
|
-
const PUSH = 0.07; // jump push-off duration (s)
|
|
65
|
-
const COAST = 1 / 50; // constant-velocity moments between push, flight and landing (s)
|
|
66
|
-
const Y_AXIS = Object.freeze(new THREE.Vector3(0, 1, 0));
|
|
67
|
-
const X_AXIS = Object.freeze(new THREE.Vector3(1, 0, 0));
|
|
68
|
-
const Z_AXIS = Object.freeze(new THREE.Vector3(0, 0, 1));
|
|
69
|
-
const ONE = Object.freeze(new THREE.Vector3(1, 1, 1));
|
|
70
|
-
const SET_A = [1, 0, 1, 0, 0, 1, 0, 1]; // tetrapod A: L1 R2 L3 R4
|
|
71
|
-
// action leg poses: bends (deg) of patella, tibia, metatarsus, tarsus relative to the previous segment
|
|
72
|
-
const P_AIR_F = [-40, -25, -30, -20], P_AIR_H = [-15, -12, -10, -8];
|
|
73
|
-
const P_RAISE = [-18, -12, -14, -6], P_HOLD = [-78, -30, -52, -28], P_CURL = [-120, -65, -55, -45], P_TUCK = [-105, -35, -115, -40];
|
|
74
|
-
const STAG = [null, null, null, [0.2, 0.85], [0.1, 0.7], [0, 0.55], [0, 0.45]];
|
|
75
|
-
const softMin = (x, lim, w) => (x < lim - w ? x : lim - w * Math.exp((lim - w - x) / w)); // smooth clamp from above
|
|
76
|
-
const expTo = (x, target, dt, tau) => x + (target - x) * (1 - Math.exp(-dt / tau));
|
|
77
|
-
|
|
78
|
-
// ------------------------------------------------------------------------------------------------
|
|
79
|
-
// configuration
|
|
80
|
-
function mergeSpec(base, over) {
|
|
81
|
-
if (!over) return base;
|
|
82
|
-
const out = { ...base };
|
|
83
|
-
for (const [k, v] of Object.entries(over)) {
|
|
84
|
-
if (v && typeof v === 'object' && !Array.isArray(v) && base[k] && typeof base[k] === 'object' && !Array.isArray(base[k])) out[k] = mergeSpec(base[k], v);
|
|
85
|
-
else out[k] = v;
|
|
86
|
-
}
|
|
87
|
-
return out;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function resolveConfig(species, data, skeleton) {
|
|
91
|
-
const base = species.motion || {};
|
|
92
|
-
const variant = data.params?.variant;
|
|
93
|
-
const spec = mergeSpec(base, base.variants && variant ? base.variants[variant] : null);
|
|
94
|
-
const J = {};
|
|
95
|
-
for (const [k, v] of Object.entries(data.joints)) J[k] = new THREE.Vector3(v[0], v[1], v[2]);
|
|
96
|
-
const s = data.params?.size || 1, sq = Math.sqrt(s);
|
|
97
|
-
const boneIndex = Object.fromEntries(data.bones.map((b, i) => [b.name, i]));
|
|
98
|
-
const bindQ = skeleton.bind.map((m) => new THREE.Quaternion().setFromRotationMatrix(m));
|
|
99
|
-
// body root: centre of the coxa bases (sternum level), on the midline
|
|
100
|
-
const root = new THREE.Vector3();
|
|
101
|
-
for (const key of LEG_KEYS) root.add(J['coxa' + key[1] + key[0]]);
|
|
102
|
-
root.multiplyScalar(1 / 8); root.x = 0;
|
|
103
|
-
const loc = (v) => v.clone().sub(root);
|
|
104
|
-
const cfg = {
|
|
105
|
-
spec, s, sq, J, root, boneIndex, bindQ,
|
|
106
|
-
rootH: root.y,
|
|
107
|
-
maxSpeed: (spec.maxSpeed ?? 0.3) * sq,
|
|
108
|
-
accel: (spec.accel ?? 2) * sq, decel: (spec.decel ?? 3) * sq,
|
|
109
|
-
turnRate: (spec.turnRate ?? 4) / sq,
|
|
110
|
-
latAccelMax: (spec.latAccelMax ?? 0.4) * sq,
|
|
111
|
-
gears: Object.fromEntries(Object.entries(spec.gears || { walk: 0.05, run: 0.15 }).map(([k, v]) => [k, v * sq])),
|
|
112
|
-
gaits: (spec.gaits || []).map((g) => ({ lift: 0.003, bob: 0, sway: 0, ...g })),
|
|
113
|
-
stance: { reach: [0.9, 0.93, 0.93, 0.9], height: 1, width: 1, ...(spec.stance || {}) },
|
|
114
|
-
heelLift: (spec.feet?.heelLift ?? 12) * DEG,
|
|
115
|
-
abd: { stiffness: 16, lag: 0.12, droop: 0.05, ...(spec.abdomen || {}) },
|
|
116
|
-
breath: { rate: 0.5, amp: 0.015, ...(spec.breath || {}) },
|
|
117
|
-
chel: { fang: 'labidognath', fangOpen: 1.2, spread: 0.3, pitch: 0.45, ...(spec.chelicerae || {}) },
|
|
118
|
-
palps: { twitch: 1, tap: 0.5, ...(spec.palps || {}) },
|
|
119
|
-
idle: { twitch: 1, raiseFront: 0.1, look: 0.3, ...(spec.idle || {}) },
|
|
120
|
-
look: { yaw: 0.45, pitch: 0.3, ...(spec.look || {}) },
|
|
121
|
-
actions: spec.actions || {},
|
|
122
|
-
};
|
|
123
|
-
if (!cfg.gaits.length) throw new Error('procedural-animals spider: species.motion.gaits is required');
|
|
124
|
-
// ---- legs
|
|
125
|
-
cfg.legs = LEG_KEYS.map((key, idx) => {
|
|
126
|
-
const S = key[0], i = +key[1], side = S === 'L' ? 1 : -1;
|
|
127
|
-
const jn = legJoints(i, S), bn = legBones(i, S);
|
|
128
|
-
const P = jn.map((n) => loc(J[n]));
|
|
129
|
-
const len = [], az = [], el = [];
|
|
130
|
-
for (let k = 0; k < 7; k++) {
|
|
131
|
-
const d = P[k + 1].clone().sub(P[k]);
|
|
132
|
-
len.push(d.length());
|
|
133
|
-
d.normalize();
|
|
134
|
-
az.push(Math.atan2(d.x * side, d.z));
|
|
135
|
-
el.push(Math.asin(clamp(d.y, -1, 1)));
|
|
136
|
-
}
|
|
137
|
-
const phi0 = az[2];
|
|
138
|
-
const u0 = new THREE.Vector3(Math.sin(phi0) * side, 0, Math.cos(phi0));
|
|
139
|
-
const n0 = new THREE.Vector3().crossVectors(u0, Y_AXIS).normalize();
|
|
140
|
-
// patella + tibia as one rigid link from the knee to the tibia end
|
|
141
|
-
const pt = P[5].clone().sub(P[3]);
|
|
142
|
-
const ptH = pt.x * u0.x + pt.z * u0.z;
|
|
143
|
-
const ept0 = Math.atan2(pt.y, ptH);
|
|
144
|
-
const L = {
|
|
145
|
-
key, idx, i, S, side, front: i <= 2,
|
|
146
|
-
bones: bn.map((b) => cfg.boneIndex[b]), bindQ: bn.map((b) => bindQ[cfg.boneIndex[b]]),
|
|
147
|
-
P0: P, len, az0: az, el0: el, phi0, u0, n0,
|
|
148
|
-
Lpt: Math.hypot(ptH, pt.y), dp: el[3] - ept0, dt: el[4] - ept0,
|
|
149
|
-
total: len.reduce((a, b) => a + b, 0),
|
|
150
|
-
tipY: J[jn[7]].y, // claw tip height above the ground at bind
|
|
151
|
-
set: SET_A[idx],
|
|
152
|
-
};
|
|
153
|
-
L.reach2 = L.len[2] + L.Lpt;
|
|
154
|
-
const F0 = P[2], A0 = P[6];
|
|
155
|
-
L.D0 = Math.hypot((A0.x - F0.x) * u0.x + (A0.z - F0.z) * u0.z, A0.y - F0.y);
|
|
156
|
-
// neutral foothold (body frame, horizontal): the bind claw pulled toward the coxa
|
|
157
|
-
const rk = cfg.stance.reach[i - 1] ?? 0.9;
|
|
158
|
-
L.neutral = new THREE.Vector3(P[0].x + (P[7].x - P[0].x) * rk * cfg.stance.width, 0, P[0].z + (P[7].z - P[0].z) * rk);
|
|
159
|
-
// workspace: horizontal distance of the claw from the coxa base (too close folds the leg over
|
|
160
|
-
// itself, too far cannot be reached)
|
|
161
|
-
L.rN = Math.hypot(L.neutral.x - P[0].x, L.neutral.z - P[0].z);
|
|
162
|
-
L.rMin = 0.64 * L.rN;
|
|
163
|
-
L.rMax = Math.min(1.18 * L.rN, 0.95 * (L.total - len[0] - len[1]));
|
|
164
|
-
L.azN = Math.atan2((L.neutral.x - P[0].x) * side, L.neutral.z - P[0].z);
|
|
165
|
-
L.azMax = 0.7;
|
|
166
|
-
return L;
|
|
167
|
-
});
|
|
168
|
-
cfg.legLen = cfg.legs.reduce((a, L) => a + L.total, 0) / 8;
|
|
169
|
-
// least-squares plane through the neutral footholds: rows of (X^T X)^-1 X^T for y = a + b x + c z
|
|
170
|
-
{
|
|
171
|
-
const X = cfg.legs.map((L) => [1, L.neutral.x, L.neutral.z]);
|
|
172
|
-
const A = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
|
|
173
|
-
for (const r of X) for (let a = 0; a < 3; a++) for (let b = 0; b < 3; b++) A[a][b] += r[a] * r[b];
|
|
174
|
-
const inv = new THREE.Matrix3().set(...A[0], ...A[1], ...A[2]).invert().elements; // column-major
|
|
175
|
-
const Ai = (a, b) => inv[b * 3 + a];
|
|
176
|
-
cfg.fitW = [0, 1, 2].map((a) => X.map((r) => Ai(a, 0) * r[0] + Ai(a, 1) * r[1] + Ai(a, 2) * r[2]));
|
|
177
|
-
}
|
|
178
|
-
cfg.turnR = cfg.legs.reduce((a, L) => a + Math.hypot(L.neutral.x, L.neutral.z), 0) / 8;
|
|
179
|
-
// ---- palps
|
|
180
|
-
cfg.palpDefs = ['L', 'R'].map((S) => {
|
|
181
|
-
const side = S === 'L' ? 1 : -1, jn = palpJoints(S), bn = palpBones(S);
|
|
182
|
-
const P = jn.map((n) => loc(J[n]));
|
|
183
|
-
const len = [], az = [], el = [];
|
|
184
|
-
for (let k = 0; k < 6; k++) {
|
|
185
|
-
const d = P[k + 1].clone().sub(P[k]);
|
|
186
|
-
len.push(d.length()); d.normalize();
|
|
187
|
-
az.push(Math.atan2(d.x * side, d.z)); el.push(Math.asin(clamp(d.y, -1, 1)));
|
|
188
|
-
}
|
|
189
|
-
const phi0 = az[2];
|
|
190
|
-
const u0 = new THREE.Vector3(Math.sin(phi0) * side, 0, Math.cos(phi0));
|
|
191
|
-
return { S, side, bones: bn.map((b) => cfg.boneIndex[b]), bindQ: bn.map((b) => bindQ[cfg.boneIndex[b]]), P0: P, len, az0: az, el0: el, phi0, n0: new THREE.Vector3().crossVectors(u0, Y_AXIS).normalize(), tipR: 0.0004 * s };
|
|
192
|
-
});
|
|
193
|
-
// ---- chelicerae and fangs
|
|
194
|
-
cfg.chels = ['L', 'R'].map((S) => {
|
|
195
|
-
const side = S === 'L' ? 1 : -1;
|
|
196
|
-
const base = loc(J['cheBase' + S]), tip = loc(J['cheTip' + S]), ft = loc(J['fangTip' + S]);
|
|
197
|
-
const fangVec = ft.clone().sub(tip);
|
|
198
|
-
// fang hinge: labidognath fangs swing sideways (about the forward axis), orthognath fangs swing
|
|
199
|
-
// down and forward (about the lateral axis); the sign is chosen so that opening moves the tip down
|
|
200
|
-
const axis = cfg.chel.fang === 'orthognath' ? new THREE.Vector3(1, 0, 0) : new THREE.Vector3(0, 0, 1);
|
|
201
|
-
const test = fangVec.clone().applyAxisAngle(axis, 0.2);
|
|
202
|
-
if (test.y > fangVec.y) axis.negate();
|
|
203
|
-
return { S, side, base, tip, fangVec, fangAxis: axis, bc: cfg.boneIndex['chelicera' + S], bf: cfg.boneIndex['fang' + S] };
|
|
204
|
-
});
|
|
205
|
-
cfg.ax = {};
|
|
206
|
-
for (const n of ['front', 'ceph', 'pedA', 'pedB', 'abdEnd', 'spinTip']) cfg.ax[n] = loc(J[n]);
|
|
207
|
-
cfg.bone = {};
|
|
208
|
-
for (const n of ['prosoma', 'head', 'pedicel', 'abdomen', 'spinnerets']) cfg.bone[n] = cfg.boneIndex[n] ?? -1;
|
|
209
|
-
// support points for ground contact of the body: extreme vertices of the bind mesh in 26
|
|
210
|
-
// directions, for the parts rigid with the prosoma and with the abdomen
|
|
211
|
-
const abdC = cfg.ax.pedB.clone().lerp(cfg.ax.abdEnd, 0.5);
|
|
212
|
-
cfg.abdC = abdC; cfg.abdL = cfg.ax.pedB.distanceTo(cfg.ax.abdEnd) * 0.5;
|
|
213
|
-
cfg.abdR = cfg.abdL * 0.62;
|
|
214
|
-
{
|
|
215
|
-
const dirs = [];
|
|
216
|
-
for (let x = -1; x <= 1; x++) for (let y = -1; y <= 1; y++) for (let z = -1; z <= 1; z++) if (x || y || z) dirs.push(new THREE.Vector3(x, y, z).normalize());
|
|
217
|
-
const grp = data.bones.map((b) => (b.name === 'prosoma' || b.name === 'head' ? 0 : b.name === 'abdomen' || b.name === 'spinnerets' ? 1 : -1));
|
|
218
|
-
const best = [dirs.map(() => ({ d: -1e9, p: null })), dirs.map(() => ({ d: -1e9, p: null }))];
|
|
219
|
-
const pos = data.pos, si = data.skinIndex, sw = data.skinWeight;
|
|
220
|
-
const c0 = [cfg.ax.pedA.clone().lerp(cfg.ax.front, 0.5), abdC];
|
|
221
|
-
for (let v = 0; v < data.nV; v++) {
|
|
222
|
-
if (sw[v * 4] < 0.9) continue;
|
|
223
|
-
const gI = grp[si[v * 4]];
|
|
224
|
-
if (gI < 0) continue;
|
|
225
|
-
const x = pos[v * 3] - root.x, y = pos[v * 3 + 1] - root.y, z = pos[v * 3 + 2] - root.z;
|
|
226
|
-
const c = c0[gI];
|
|
227
|
-
for (let k = 0; k < dirs.length; k++) {
|
|
228
|
-
const dd = (x - c.x) * dirs[k].x + (y - c.y) * dirs[k].y + (z - c.z) * dirs[k].z;
|
|
229
|
-
if (dd > best[gI][k].d) best[gI][k] = { d: dd, p: [x, y, z] };
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
cfg.supPro = best[0].filter((b) => b.p).map((b) => new THREE.Vector3(...b.p));
|
|
233
|
-
cfg.supAbd = best[1].filter((b) => b.p).map((b) => new THREE.Vector3(...b.p).sub(cfg.ax.pedB));
|
|
234
|
-
// the abdomen's lowest bind point (for resting it on the ground)
|
|
235
|
-
let lo = null;
|
|
236
|
-
for (const p of cfg.supAbd) if (!lo || p.y < lo.y) lo = p;
|
|
237
|
-
cfg.abdLow = lo || new THREE.Vector3(0, -cfg.abdR, -cfg.abdL);
|
|
238
|
-
}
|
|
239
|
-
cfg.bodyLen = cfg.ax.front.distanceTo(cfg.ax.spinTip);
|
|
240
|
-
// ---- death curl, per leg: every joint flexed ventrally so the leg folds under the sternum and no
|
|
241
|
-
// joint sticks out beyond the carapace top or the body's flanks (a dead spider on its back or side
|
|
242
|
-
// rests on its body, never propped on its knees). Solved once here in joint space (femur
|
|
243
|
-
// elevation + four flexions) by coordinate descent.
|
|
244
|
-
{
|
|
245
|
-
let yTop = -1e9, yBot = 1e9, halfW = 0;
|
|
246
|
-
for (const p of cfg.supPro) { yTop = Math.max(yTop, p.y); yBot = Math.min(yBot, p.y); halfW = Math.max(halfW, Math.abs(p.x)); }
|
|
247
|
-
for (const p of cfg.supAbd) halfW = Math.max(halfW, Math.abs(p.x + cfg.ax.pedB.x));
|
|
248
|
-
cfg.bodyTop = yTop; cfg.bodyHalfW = halfW;
|
|
249
|
-
// two curls per leg: 'back' (a loose fist over the sternum: on its back, or the upper legs on its
|
|
250
|
-
// side) and 'side' (the legs underneath when it lies on its side: folded straight down so no
|
|
251
|
-
// joint reaches beyond the flank it rests on)
|
|
252
|
-
const solve = (L, mode) => {
|
|
253
|
-
const m = 1.6 * L.tipY, s = L.side, side = mode === 'side', up = mode === 'up';
|
|
254
|
-
const out = [Math.sin(L.phi0) * s, Math.cos(L.phi0)];
|
|
255
|
-
const tgt = side ? [s * 0.35 * halfW, yBot - 0.15 * L.total, L.P0[0].z * 0.7]
|
|
256
|
-
: up ? [L.P0[0].x + out[0] * 0.28 * L.total, yBot + L.tipY, L.P0[0].z + out[1] * 0.28 * L.total]
|
|
257
|
-
: [s * 0.12 * halfW, yBot - 0.1 * L.total, L.P0[0].z * 0.5];
|
|
258
|
-
const deep = yBot - 0.3 * L.total;
|
|
259
|
-
const jp = [];
|
|
260
|
-
const fk = (x) => {
|
|
261
|
-
// x = [femur, b1, b2, b3, b4, coxa] (rad); leg plane at phi0
|
|
262
|
-
let px = L.P0[0].x, py = L.P0[0].y, pz = L.P0[0].z;
|
|
263
|
-
const segs = [[L.az0[0], x[5]], [L.phi0, x[5]]];
|
|
264
|
-
let e = x[0];
|
|
265
|
-
segs.push([L.phi0, e]);
|
|
266
|
-
for (let k = 1; k < 5; k++) { e += x[k]; segs.push([L.phi0, e]); }
|
|
267
|
-
jp.length = 0;
|
|
268
|
-
for (let k = 0; k < 7; k++) {
|
|
269
|
-
const [a, el] = segs[k], ce = Math.cos(el);
|
|
270
|
-
px += L.len[k] * Math.sin(a) * ce * s; py += L.len[k] * Math.sin(el); pz += L.len[k] * Math.cos(a) * ce;
|
|
271
|
-
jp.push(px, py, pz);
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
const cost = (x) => {
|
|
275
|
-
fk(x);
|
|
276
|
-
const n = jp.length;
|
|
277
|
-
let c = ((jp[n - 3] - tgt[0]) ** 2 + (jp[n - 2] - tgt[1]) ** 2 + (jp[n - 1] - tgt[2]) ** 2) / (L.total * L.total);
|
|
278
|
-
for (let k = 0; k < 7; k++) {
|
|
279
|
-
const x_ = jp[k * 3], y_ = jp[k * 3 + 1];
|
|
280
|
-
const over = up ? Math.max(0, yBot + m - y_) : Math.max(0, y_ - (yTop - m)) + (side ? Math.max(0, Math.abs(x_) - (halfW - m)) : Math.max(0, deep - y_));
|
|
281
|
-
c += 400 * (over / L.total) ** 2;
|
|
282
|
-
}
|
|
283
|
-
// even flexions (a curl, not one sharp fold), within anatomical limits
|
|
284
|
-
const mean = (x[1] + x[2] + x[3] + x[4]) / 4;
|
|
285
|
-
for (let k = 1; k < 5; k++) c += 0.02 * (x[k] - mean) ** 2 + 50 * Math.max(0, x[k] + 0.1) ** 2 + 50 * Math.max(0, -x[k] - 2.6) ** 2;
|
|
286
|
-
c += 50 * Math.max(0, x[0] - (up ? 1.3 : 0.2)) ** 2 + 50 * Math.max(0, -x[0] - 1.6) ** 2 + (side ? 0 : up ? 0.05 * (x[0] - 0.8) ** 2 : 0.05 * (x[0] + 0.8) ** 2);
|
|
287
|
-
// the coxa and trochanter may swing down (ventrally) in their sockets, not up
|
|
288
|
-
c += 50 * Math.max(0, x[5] - L.el0[0]) ** 2 + 50 * Math.max(0, -x[5] - 1.4) ** 2 + 0.05 * (x[5] - L.el0[0]) ** 2;
|
|
289
|
-
return c;
|
|
290
|
-
};
|
|
291
|
-
const x = up ? [0.8, -1.6, -0.6, -0.6, -0.4, L.el0[0]] : [-1.0, -1.4, -0.8, -0.8, -0.6, L.el0[0]];
|
|
292
|
-
let best = cost(x), step = 0.4;
|
|
293
|
-
for (let it = 0; it < 400 && step > 1e-3; it++) {
|
|
294
|
-
let improved = false;
|
|
295
|
-
for (let k = 0; k < 6; k++) for (const d of [step, -step]) {
|
|
296
|
-
x[k] += d;
|
|
297
|
-
const c = cost(x);
|
|
298
|
-
if (c < best) { best = c; improved = true; } else x[k] -= d;
|
|
299
|
-
}
|
|
300
|
-
if (!improved) step *= 0.5;
|
|
301
|
-
}
|
|
302
|
-
return { F: x[0], C: x[5], B: [x[1], x[2], x[3], x[4]] };
|
|
303
|
-
};
|
|
304
|
-
for (const L of cfg.legs) { L.curlBack = solve(L, 'back'); L.curlSide = solve(L, 'side'); L.curlUp = solve(L, 'up'); }
|
|
305
|
-
}
|
|
306
|
-
// mouth: between the chelicera tips
|
|
307
|
-
cfg.mouth = cfg.chels[0].tip.clone().add(cfg.chels[1].tip).multiplyScalar(0.5);
|
|
308
|
-
return cfg;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// ------------------------------------------------------------------------------------------------
|
|
312
|
-
// swing path profiles (see the header): horizontal progress with short linear speed ramps; vertical
|
|
313
|
-
// hump: constant climb, a rounded apex, constant descent
|
|
314
|
-
function progress(u, a) {
|
|
315
|
-
const vm = 1 / (1 - a);
|
|
316
|
-
if (u <= 0) return 0;
|
|
317
|
-
if (u >= 1) return 1;
|
|
318
|
-
if (u < a) return (vm * u * u) / (2 * a);
|
|
319
|
-
if (u > 1 - a) { const r = 1 - u; return 1 - (vm * r * r) / (2 * a); }
|
|
320
|
-
return vm * (u - a / 2);
|
|
321
|
-
}
|
|
322
|
-
function hump(u) {
|
|
323
|
-
// vertical speed +c on [0, u1], easing linearly to -c2 over [u1, u2], -c2 on [u2, 1]; c2 makes the
|
|
324
|
-
// path end at 0; normalised to a peak of 1
|
|
325
|
-
if (u <= 0 || u >= 1) return 0;
|
|
326
|
-
const u1 = 0.3, u2 = 0.6, T = u2 - u1;
|
|
327
|
-
const c2 = (u1 + 0.5 * T) / (0.5 * T + (1 - u2));
|
|
328
|
-
const peak = u1 + (0.5 * T) / (1 + c2);
|
|
329
|
-
let y;
|
|
330
|
-
if (u < u1) y = u;
|
|
331
|
-
else if (u < u2) { const t = u - u1; y = u1 + t - ((1 + c2) * t * t) / (2 * T); }
|
|
332
|
-
else y = c2 * (1 - u);
|
|
333
|
-
return y / peak;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// ------------------------------------------------------------------------------------------------
|
|
337
|
-
// actions
|
|
338
|
-
const POSTURES = new Set(['sit', 'lie', 'sleep', 'death']);
|
|
339
|
-
const ONESHOTS = new Set(['jump', 'attack', 'hit', 'eat', 'drink']);
|
|
340
|
-
export const SPIDER_ACTIONS = ['idle', 'jump', 'sit', 'lie', 'sleep', 'eat', 'drink', 'attack', 'hit', 'death', 'stand'];
|
|
341
|
-
|
|
342
|
-
function createParams() {
|
|
343
|
-
return {
|
|
344
|
-
gaitW: 1, speedScale: 1, crouch: 0, drop: 0, pitch: 0, roll: 0, fwd: 0, side: 0, yaw: 0,
|
|
345
|
-
legIn: 0, drawIn: 0, tuck: 0, curl2: 0, raise: new Float32Array(8), hold: new Float32Array(8), curl: 0, air: 0, lean: 0,
|
|
346
|
-
palpRaise: 0, palpReach: 0, palpWork: 0, palpDown: 0, palpFold: 0,
|
|
347
|
-
chelOpen: 0, chelWork: 0, fangOpen: 0, abdPitch: 0, abdDown: 0, breathRate: 1, breathAmp: 1,
|
|
348
|
-
flip: 0, reviving: 0, pullIn: 0, flipSide: 1, flipAngle: Math.PI, dead: 0, lookW: 1, look: null, twitch: 1,
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
function resetParams(P) {
|
|
352
|
-
P.gaitW = 1; P.speedScale = 1; P.crouch = P.drop = P.pitch = P.roll = P.fwd = P.side = P.yaw = 0;
|
|
353
|
-
P.legIn = 0; P.drawIn = 0; P.tuck = 0; P.curl2 = 0; P.raise.fill(0); P.hold.fill(0); P.curl = P.tuck = P.air = P.lean = 0;
|
|
354
|
-
P.palpRaise = P.palpReach = P.palpWork = P.palpDown = P.palpFold = 0;
|
|
355
|
-
P.chelOpen = P.chelWork = P.fangOpen = P.abdPitch = P.abdDown = 0; P.breathRate = 1; P.breathAmp = 1;
|
|
356
|
-
P.flip = 0; P.reviving = 0; P.pullIn = 0; P.dead = 0; P.lookW = 1; P.look = null; P.twitch = 1;
|
|
357
|
-
}
|
|
358
|
-
const mx = (P, k, v, w) => { P[k] += (v - P[k]) * w; };
|
|
359
|
-
const mxA = (arr, i, v, w) => { arr[i] += (v - arr[i]) * w; };
|
|
360
|
-
|
|
361
|
-
const POSTURE_DEFS = {
|
|
362
|
-
// body lowered, abdomen resting on the ground, legs a little drawn in
|
|
363
|
-
sit: {
|
|
364
|
-
fadeIn: 0.7, fadeOut: 0.6,
|
|
365
|
-
apply(P, w) { mx(P, 'gaitW', 0, w); mx(P, 'crouch', 0.5, w); mx(P, 'legIn', 0.25, w); mx(P, 'abdDown', 1, w); mx(P, 'palpDown', 0.4, w); mx(P, 'pitch', 0.14, w); },
|
|
366
|
-
},
|
|
367
|
-
// flat on the ground, legs drawn in
|
|
368
|
-
lie: {
|
|
369
|
-
fadeIn: 0.9, fadeOut: 0.8,
|
|
370
|
-
apply(P, w) { mx(P, 'gaitW', 0, w); mx(P, 'crouch', 0.8, w); mx(P, 'legIn', 0.5, w); mx(P, 'abdDown', 1, w); mx(P, 'palpDown', 0.8, w); mx(P, 'breathRate', 0.8, w); },
|
|
371
|
-
},
|
|
372
|
-
// lie with the legs pulled in tight, palps folded, slow breathing
|
|
373
|
-
sleep: {
|
|
374
|
-
fadeIn: 1.1, fadeOut: 1.0,
|
|
375
|
-
apply(P, w, inst, e) {
|
|
376
|
-
POSTURE_DEFS.lie.apply(P, w);
|
|
377
|
-
const c = smooth(0.4, 2.0, inst.t) * w;
|
|
378
|
-
const tuck = e.cfg.actions.sleep?.tuck ?? 0.8;
|
|
379
|
-
mx(P, 'crouch', 0.92, c); mx(P, 'legIn', 0.5 + 0.35 * tuck, c); mx(P, 'palpFold', 1, c);
|
|
380
|
-
// the claws are drawn in along the ground, the front legs folded against the chelicerae
|
|
381
|
-
mx(P, 'drawIn', tuck, c); mx(P, 'pullIn', tuck, c);
|
|
382
|
-
mx(P, 'breathRate', 0.45, c); mx(P, 'breathAmp', 1.3, c); mx(P, 'twitch', 0.1, c); mx(P, 'lookW', 0, c);
|
|
383
|
-
for (let i = 0; i < 8; i++) if (e.legs[i].def.i === 1) mxA(P.hold, i, 0.3 * tuck, c);
|
|
384
|
-
},
|
|
385
|
-
},
|
|
386
|
-
// hydraulic collapse: the legs curl under the body, it drops, then rolls onto its back or side
|
|
387
|
-
death: {
|
|
388
|
-
fadeIn: 0.2, fadeOut: 3.0,
|
|
389
|
-
apply(P, w, inst) {
|
|
390
|
-
const t = inst.t;
|
|
391
|
-
mx(P, 'gaitW', 0, w); mx(P, 'dead', 1, w);
|
|
392
|
-
// the legs lose pressure: the body sinks and the claws are drawn in, then the legs fold up
|
|
393
|
-
// (reviving runs the other way round: it rolls back first, then uncurls, then stands up)
|
|
394
|
-
mx(P, 'drawIn', smooth(0.05, 0.6, t), smooth(0, 0.35, w));
|
|
395
|
-
// (no dorsal tuck any more: the legs flex ventrally at once, the body hunches on them and tips over)
|
|
396
|
-
// on its back the legs contract further, ventrally (over the sternum)
|
|
397
|
-
mx(P, 'curl2', smooth(0.3, 1.3, t), smooth(0.3, 0.8, w));
|
|
398
|
-
mx(P, 'crouch', smooth(0, 0.35, t), smooth(0, 0.45, w));
|
|
399
|
-
const f = smooth(1.0, 2.0, t);
|
|
400
|
-
const settle = t > 2.0 ? Math.exp(-(t - 2.0) * 5) * Math.sin((t - 2.0) * 14) * 0.05 : 0;
|
|
401
|
-
mx(P, 'flip', clamp(f - settle, 0, 1.05), smooth(0.6, 1, w));
|
|
402
|
-
P.flipSide = inst.side; P.flipAngle = inst.flipAngle;
|
|
403
|
-
if (inst.target < 0.5) P.reviving = 1;
|
|
404
|
-
mx(P, 'palpFold', 1, w); mx(P, 'fangOpen', 0.3, w); mx(P, 'breathAmp', 0, w); mx(P, 'breathRate', 0, w);
|
|
405
|
-
mx(P, 'twitch', 0, w); mx(P, 'lookW', 0, w); mx(P, 'abdDown', 0, w);
|
|
406
|
-
},
|
|
407
|
-
},
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
const ONESHOT_DEFS = {
|
|
411
|
-
// pounce: crouch, push off with legs III-IV, fly with the front legs raised to grab, land
|
|
412
|
-
jump: {
|
|
413
|
-
start(inst, e) {
|
|
414
|
-
const cfg = e.cfg, o = cfg.actions.jump || {};
|
|
415
|
-
inst.tc = (o.crouch ?? 0.22) * cfg.sq;
|
|
416
|
-
const h = (o.height ?? 0.014) * cfg.s;
|
|
417
|
-
// apex h above the take-off height, counting the push-off and coast rise (vy (PUSH / 2 + COAST))
|
|
418
|
-
const k = PUSH / 2 + COAST;
|
|
419
|
-
inst.vy = G_ACC * (-k + Math.sqrt(k * k + (2 * h) / G_ACC));
|
|
420
|
-
const T = (2 * inst.vy) / G_ACC + PUSH * 0.5 + 2 * COAST;
|
|
421
|
-
let dist = (o.distance ?? 0.05) * cfg.s;
|
|
422
|
-
if (inst.opts.target) { const t = inst.opts.target; dist = clamp(Math.hypot(t.x - e.pos.x, t.z - e.pos.z), 0.3 * dist, dist * 2); }
|
|
423
|
-
inst.vh = Math.max(e.speed, dist / T);
|
|
424
|
-
inst.phase = 'crouch'; inst.dur = Infinity;
|
|
425
|
-
},
|
|
426
|
-
update(inst, dt, e) {
|
|
427
|
-
if (inst.phase === 'crouch' && inst.t >= inst.tc) { inst.phase = 'air'; inst.tAir = inst.t; e.takeOff(inst.vy, inst.vh); }
|
|
428
|
-
if (inst.phase === 'air' && inst.t - inst.tAir > 3) e.touchDown();
|
|
429
|
-
// pose weights are states with finite rates (the flight of a small spider lasts ~0.1 s)
|
|
430
|
-
inst.aw = clamp((inst.aw || 0) + (inst.phase === 'air' ? dt / 0.1 : inst.phase === 'land' ? -dt / 0.25 : 0), 0, 1);
|
|
431
|
-
return inst.phase === 'land' && inst.t - inst.tLand > 0.45;
|
|
432
|
-
},
|
|
433
|
-
onLand(inst) { inst.phase = 'land'; inst.tLand = inst.t; },
|
|
434
|
-
apply(P, w, inst, e) {
|
|
435
|
-
const aw = (inst.aw || 0) * w;
|
|
436
|
-
const pre = inst.phase === 'crouch' ? smooth(0, inst.tc, inst.t) : inst.phase === 'air' ? 1 : 1 - smooth(0, 0.3, inst.t - inst.tLand);
|
|
437
|
-
const land = inst.phase === 'land' ? envelope(inst.t - inst.tLand, 0.45, 0.06, 0.35) : 0;
|
|
438
|
-
const cr = inst.phase === 'crouch' ? smooth(0, inst.tc, inst.t) : inst.phase === 'air' ? 1 - smooth(0, PUSH + 0.03, inst.t - inst.tAir) : 0;
|
|
439
|
-
mx(P, 'crouch', 0.55, cr * w); mx(P, 'crouch', 0.4, land * w); mx(P, 'pitch', 0.1, pre * w * (1 - aw));
|
|
440
|
-
mx(P, 'air', 1, aw);
|
|
441
|
-
// landing contacts are not gait contacts (the claws settle while the body brakes)
|
|
442
|
-
if (inst.phase === 'land') mx(P, 'gaitW', 0, (1 - smooth(0.3, 0.45, inst.t - inst.tLand)) * w);
|
|
443
|
-
mx(P, 'palpRaise', 1, Math.max(smooth(0, 1, aw), 0.5 * pre) * w); mx(P, 'fangOpen', 1, Math.max(aw, 0.4 * pre) * w); mx(P, 'chelOpen', 0.8, aw);
|
|
444
|
-
for (let i = 0; i < 8; i++) {
|
|
445
|
-
const li = e.legs[i].def.i;
|
|
446
|
-
// front legs lift already in the crouch and reach up and forward to grab; after landing they
|
|
447
|
-
// come down onto the prey
|
|
448
|
-
if (li === 1) { mxA(P.raise, i, 1, Math.max(aw, 0.35 * pre) * w); mxA(P.hold, i, 0.6, land * (1 - aw) * w); }
|
|
449
|
-
else if (li === 2) mxA(P.raise, i, 0.6, aw);
|
|
450
|
-
}
|
|
451
|
-
},
|
|
452
|
-
},
|
|
453
|
-
// threat display (front legs raised, fangs out, body reared), then a lunge with a fang strike
|
|
454
|
-
attack: {
|
|
455
|
-
start(inst, e) {
|
|
456
|
-
const o = e.cfg.actions.attack || {};
|
|
457
|
-
inst.threat = (o.threat ?? 0.7) * (inst.opts.duration ? inst.opts.duration / 1.7 : 1);
|
|
458
|
-
inst.dur = inst.threat + 1.0;
|
|
459
|
-
inst.hit = false;
|
|
460
|
-
if (inst.opts.target && !e.input.follow) {
|
|
461
|
-
const t = inst.opts.target;
|
|
462
|
-
const want = Math.atan2(t.x - e.pos.x, t.z - e.pos.z);
|
|
463
|
-
if (Math.abs(angDiff(e.heading, want)) > 0.2) e._turnTo = want;
|
|
464
|
-
}
|
|
465
|
-
},
|
|
466
|
-
update(inst, dt, e) {
|
|
467
|
-
const tl = inst.t - inst.threat;
|
|
468
|
-
if (!inst.hit && tl >= 0.16) {
|
|
469
|
-
inst.hit = true;
|
|
470
|
-
const cfg = e.cfg;
|
|
471
|
-
const p = e.localToWorld(cfg.mouth.x, cfg.mouth.y - 0.002 * cfg.s, cfg.mouth.z + 0.003 * cfg.s, new THREE.Vector3());
|
|
472
|
-
e.emit('attackHit', { position: p, direction: e.forward(e.heading, new THREE.Vector3()), style: 'bite' });
|
|
473
|
-
}
|
|
474
|
-
return inst.t >= inst.dur;
|
|
475
|
-
},
|
|
476
|
-
apply(P, w, inst, e) {
|
|
477
|
-
const o = e.cfg.actions.attack || {}, cfg = e.cfg;
|
|
478
|
-
const t = inst.t, T = inst.threat, tl = t - T;
|
|
479
|
-
const rear = smooth(0, Math.min(0.35, T), t) * (1 - smooth(0.05, 0.2, tl) * 0.8) * (1 - smooth(0.45, 0.95, tl));
|
|
480
|
-
const lunge = smooth(0, 0.14, tl) * (1 - smooth(0.4, 0.9, tl));
|
|
481
|
-
const still = 1 - 0.75 * smooth(0, 0.3 * cfg.maxSpeed, e.speed); // on the run it rears less
|
|
482
|
-
mx(P, 'pitch', ((o.rear ?? 0.32) * rear - 0.12 * lunge) * still, w);
|
|
483
|
-
mx(P, 'crouch', 0.15 * rear + 0.25 * lunge, w);
|
|
484
|
-
// the lunge is a body thrust over planted claws (smaller on the run: the gait carries it)
|
|
485
|
-
mx(P, 'fwd', (o.lunge ?? 0.012) * cfg.s * lunge * (1 - smooth(0, 0.3 * cfg.maxSpeed, e.speed)), w);
|
|
486
|
-
mx(P, 'fangOpen', smooth(0.1, 0.4, t) * (1 - smooth(0.3, 0.6, tl) * 0.7) * (1 - smooth(0.7, 0.95, tl)), w);
|
|
487
|
-
mx(P, 'chelOpen', 0.8 * rear + 0.3 * lunge, w);
|
|
488
|
-
mx(P, 'palpRaise', 0.9 * rear, w);
|
|
489
|
-
mx(P, 'abdPitch', -0.15 * rear, w);
|
|
490
|
-
const strike = lunge * (1 - smooth(0.2, 0.5, tl));
|
|
491
|
-
for (let i = 0; i < 8; i++) {
|
|
492
|
-
const li = e.legs[i].def.i;
|
|
493
|
-
if (li === 1) { mxA(P.raise, i, rear, w); mxA(P.hold, i, strike, w); }
|
|
494
|
-
else if (li === 2) mxA(P.raise, i, (o.raise2 ?? 0.45) * rear, w);
|
|
495
|
-
}
|
|
496
|
-
},
|
|
497
|
-
},
|
|
498
|
-
// flinch, then scuttle back
|
|
499
|
-
hit: {
|
|
500
|
-
start(inst, e) {
|
|
501
|
-
const cfg = e.cfg;
|
|
502
|
-
inst.dur = 0.9;
|
|
503
|
-
const d = inst.opts.direction;
|
|
504
|
-
const dir = tv();
|
|
505
|
-
if (d && d.lengthSq() > 1e-8) dir.set(d.x, 0, d.z).normalize(); else e.forward(e.heading, dir).negate();
|
|
506
|
-
// scuttle away from the blow, mostly backwards
|
|
507
|
-
const back = e.forward(e.heading, tv()).negate();
|
|
508
|
-
dir.lerp(back, 0.5).normalize();
|
|
509
|
-
e.push.addScaledVector(dir, 5.5 * cfg.bodyLen / Math.max(0.5, cfg.sq));
|
|
510
|
-
},
|
|
511
|
-
update(inst) { return inst.t >= inst.dur; },
|
|
512
|
-
apply(P, w, inst, e) {
|
|
513
|
-
const t = inst.t;
|
|
514
|
-
const fl = envelope(t, 0.9, 0.05, 0.55);
|
|
515
|
-
const jerk = smooth(0, 0.12, t) * (1 - smooth(0.15, 0.45, t));
|
|
516
|
-
mx(P, 'crouch', 0.3 * fl, w); mx(P, 'pitch', 0.18 * jerk, w); mx(P, 'fwd', -0.06 * e.cfg.bodyLen * jerk, w);
|
|
517
|
-
mx(P, 'palpRaise', 0.6 * fl, w); mx(P, 'abdPitch', 0.12 * jerk, w);
|
|
518
|
-
for (let i = 0; i < 8; i++) if (e.legs[i].def.i === 1) mxA(P.raise, i, 0.45 * jerk, w);
|
|
519
|
-
},
|
|
520
|
-
},
|
|
521
|
-
// prey held under the chelicerae by legs I and the palps; chelicerae working
|
|
522
|
-
eat: {
|
|
523
|
-
start(inst) { inst.dur = inst.opts.duration ?? (inst.opts.loop ? Infinity : 4); },
|
|
524
|
-
update(inst) { return inst.t >= inst.dur; },
|
|
525
|
-
apply(P, w, inst, e) {
|
|
526
|
-
const a = smooth(0, 0.5, inst.t) * w;
|
|
527
|
-
mx(P, 'crouch', 0.3, a); mx(P, 'pitch', -0.06, a);
|
|
528
|
-
mx(P, 'palpWork', 1, a); mx(P, 'chelWork', 1, a); mx(P, 'fangOpen', 0.35, a); mx(P, 'palpReach', 0.3, a);
|
|
529
|
-
for (let i = 0; i < 8; i++) if (e.legs[i].def.i === 1) mxA(P.hold, i, 0.7, a);
|
|
530
|
-
},
|
|
531
|
-
},
|
|
532
|
-
// mouthparts down to the ground / water
|
|
533
|
-
drink: {
|
|
534
|
-
start(inst) { inst.dur = inst.opts.duration ?? (inst.opts.loop ? Infinity : 3); },
|
|
535
|
-
update(inst) { return inst.t >= inst.dur; },
|
|
536
|
-
apply(P, w, inst) {
|
|
537
|
-
const a = smooth(0, 0.6, inst.t) * w;
|
|
538
|
-
mx(P, 'crouch', 0.35, a); mx(P, 'pitch', -0.2, a); mx(P, 'palpRaise', -0.2, a); mx(P, 'palpReach', 0.5, a);
|
|
539
|
-
mx(P, 'chelWork', 0.25, a); mx(P, 'chelOpen', 0.2, a); mx(P, 'abdPitch', 0.05, a);
|
|
540
|
-
},
|
|
541
|
-
},
|
|
542
|
-
};
|
|
543
|
-
|
|
544
|
-
class SpiderActions {
|
|
545
|
-
constructor(engine) {
|
|
546
|
-
this.e = engine;
|
|
547
|
-
this.names = SPIDER_ACTIONS.slice();
|
|
548
|
-
this.postures = [];
|
|
549
|
-
this.oneshots = [];
|
|
550
|
-
this.posture = 'stand';
|
|
551
|
-
this.standReq = null;
|
|
552
|
-
this.idle = { w: 0, lookT: 1, look: new THREE.Vector3(), hasLook: false, twitchT: 2, palpT: 1 };
|
|
553
|
-
}
|
|
554
|
-
holdsStill() { for (const p of this.postures) if (p.target > 0 || p.w > 0.02) return true; return false; }
|
|
555
|
-
// automatic stand-up (asked to move): never revives a dead spider
|
|
556
|
-
requestStand() { if (!this.standReq && !this.dead()) this.play('stand'); }
|
|
557
|
-
// arbitration: a new one-shot interrupts the running
|
|
558
|
-
// ones (fade out, 'interrupted'); a jump in the air is committed (only a hit layers over it); a
|
|
559
|
-
// posture ends the one-shots (except a hit); stop() ends everything but death and never cuts a
|
|
560
|
-
// jump in the air; dead: only hit is taken. Promises resolve 'done' | 'interrupted' | 'stopped' |
|
|
561
|
-
// 'refused'.
|
|
562
|
-
committed(a) { return !a.stopping && a.name === 'jump' && this.e.air.active; }
|
|
563
|
-
end(a, reason) { if (a.stopping) return; a.stopping = true; a.reason = reason; }
|
|
564
|
-
current() {
|
|
565
|
-
for (const a of this.oneshots) if (!a.stopping) return a.name;
|
|
566
|
-
for (const p of this.postures) if (p.target > 0) return p.name;
|
|
567
|
-
return this.standReq ? 'stand' : null;
|
|
568
|
-
}
|
|
569
|
-
clear() { for (const a of [...this.postures, ...this.oneshots]) { a.resolve?.('stopped'); a.resolveIn?.('stopped'); } this.postures.length = 0; this.oneshots.length = 0; }
|
|
570
|
-
dead() { for (const p of this.postures) if (p.name === 'death' && p.target > 0) return true; return false; }
|
|
571
|
-
|
|
572
|
-
play(name, opts = {}) {
|
|
573
|
-
const e = this.e;
|
|
574
|
-
if (name === 'idle') name = 'stand';
|
|
575
|
-
if (name === 'stand') {
|
|
576
|
-
if (!this.postures.length) return Promise.resolve('done');
|
|
577
|
-
for (const p of this.postures) if (p.target > 0) { p.target = 0; p.resolveIn?.('interrupted'); }
|
|
578
|
-
e.emit('actionStart', { name: 'stand' });
|
|
579
|
-
if (this.standReq) return this.standReq.promise;
|
|
580
|
-
let resolve;
|
|
581
|
-
const promise = new Promise((r) => { resolve = r; });
|
|
582
|
-
this.standReq = { promise, resolve };
|
|
583
|
-
return promise;
|
|
584
|
-
}
|
|
585
|
-
if (POSTURES.has(name)) {
|
|
586
|
-
const cur = this.postures.find((p) => p.target > 0);
|
|
587
|
-
if (cur && cur.name === 'death' && name !== 'death') return Promise.resolve('refused');
|
|
588
|
-
if (cur && cur.name === name) return cur.promiseIn;
|
|
589
|
-
if (name !== 'death' && this.oneshots.some((a) => this.committed(a))) return Promise.resolve('refused');
|
|
590
|
-
for (const p of this.postures) if (p.target > 0) { p.target = 0; p.resolveIn?.('interrupted'); }
|
|
591
|
-
if (this.standReq) { this.standReq.resolve('interrupted'); this.standReq = null; }
|
|
592
|
-
for (const a of this.oneshots) if (a.name !== 'hit' && !this.committed(a)) this.end(a, 'interrupted');
|
|
593
|
-
const def = POSTURE_DEFS[name];
|
|
594
|
-
let side = opts.side ?? (e.rand() < 0.5 ? -1 : 1);
|
|
595
|
-
if (opts.direction && name === 'death') { const d = opts.direction; side = (d.x * Math.cos(e.heading) - d.z * Math.sin(e.heading)) > 0 ? 1 : -1; }
|
|
596
|
-
const inst = { name, t: 0, w: 0, target: 1, fadeIn: def.fadeIn / (opts.speed || 1), fadeOut: def.fadeOut, side, opts, reached: false };
|
|
597
|
-
// death: on its back most of the time, sometimes on its side
|
|
598
|
-
inst.flipAngle = opts.onSide ?? (e.rand() < 0.35) ? Math.PI * 0.52 : Math.PI * (0.9 + 0.08 * e.rand());
|
|
599
|
-
const prev = this.postures.find((p) => p.name === 'lie');
|
|
600
|
-
if (prev && name === 'sleep') { inst.w = prev.w; prev.w = 0; this.postures = this.postures.filter((p) => p !== prev); }
|
|
601
|
-
inst.promiseIn = new Promise((r) => { inst.resolveIn = r; });
|
|
602
|
-
this.postures.push(inst);
|
|
603
|
-
e.emit('actionStart', { name });
|
|
604
|
-
if (name === 'death') e.emit('death', { position: e.pos.clone() });
|
|
605
|
-
return inst.promiseIn;
|
|
606
|
-
}
|
|
607
|
-
if (!ONESHOTS.has(name)) return Promise.reject(new Error(`procedural-animals: unknown action "${name}"`));
|
|
608
|
-
if (this.dead() && name !== 'hit') return Promise.resolve('refused');
|
|
609
|
-
if (this.holdsStill() && (name === 'jump' || name === 'attack')) return this.play('stand').then((r) => (r === 'refused' ? r : this.play(name, opts)));
|
|
610
|
-
// arbitration: a new one-shot interrupts the running ones; in the air only a hit is taken
|
|
611
|
-
if (e.air.active || this.oneshots.some((a) => this.committed(a))) {
|
|
612
|
-
if (name !== 'hit') return Promise.resolve('refused');
|
|
613
|
-
for (const a of this.oneshots) if (a.name === 'hit') this.end(a, 'interrupted');
|
|
614
|
-
} else for (const a of this.oneshots) this.end(a, 'interrupted');
|
|
615
|
-
const inst = { name, t: 0, w: 0, dur: 1, stopping: false, opts, fade: name === 'hit' ? 0.06 : 0.15, speed: opts.speed || 1 };
|
|
616
|
-
ONESHOT_DEFS[name].start(inst, e);
|
|
617
|
-
inst.promise = new Promise((r) => { inst.resolve = r; });
|
|
618
|
-
this.oneshots.push(inst);
|
|
619
|
-
e.emit('actionStart', { name });
|
|
620
|
-
return inst.promise;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
stop(name) {
|
|
624
|
-
for (const a of this.oneshots) if ((!name || a.name === name) && !this.committed(a)) this.end(a, 'stopped');
|
|
625
|
-
if (!name || POSTURES.has(name)) if (this.postures.some((p) => p.target > 0 && (name ? p.name === name : p.name !== 'death'))) this.play('stand');
|
|
626
|
-
}
|
|
627
|
-
onLand() { for (const a of this.oneshots) if (ONESHOT_DEFS[a.name].onLand) ONESHOT_DEFS[a.name].onLand(a, this.e); }
|
|
628
|
-
|
|
629
|
-
update(dt) {
|
|
630
|
-
const e = this.e, P = e.P;
|
|
631
|
-
resetParams(P);
|
|
632
|
-
for (const p of this.postures) {
|
|
633
|
-
p.t += dt;
|
|
634
|
-
const rate = p.target > p.w ? 1 / p.fadeIn : 1 / p.fadeOut;
|
|
635
|
-
p.w = clamp(p.w + clamp(p.target - p.w, -rate * dt, rate * dt), 0, 1);
|
|
636
|
-
if (!p.reached && p.target > 0 && p.w >= 1 && (p.name !== 'death' || p.t > 2.0) && (p.name !== 'sleep' || p.t > 2.0)) { p.reached = true; p.resolveIn('done'); e.emit('actionEnd', { name: p.name }); }
|
|
637
|
-
}
|
|
638
|
-
for (let i = this.postures.length - 1; i >= 0; i--) { const p = this.postures[i]; if (!(p.target > 0 || p.w > 0)) this.postures.splice(i, 1); }
|
|
639
|
-
let active = null;
|
|
640
|
-
for (const p of this.postures) if (p.target > 0) active = p;
|
|
641
|
-
this.posture = active ? (active.name === 'death' ? 'dead' : active.name) : this.postures.length ? 'standing-up' : 'stand';
|
|
642
|
-
if (this.standReq && !this.postures.length) { this.standReq.resolve('done'); this.standReq = null; e.emit('actionEnd', { name: 'stand' }); }
|
|
643
|
-
// idle
|
|
644
|
-
const still = e.speed < 0.08 * e.cfg.maxSpeed && !e.input.follow && e.push.lengthSq() < 1e-6 && e.pushV.lengthSq() < 1e-6;
|
|
645
|
-
let busy = false;
|
|
646
|
-
for (const a of this.oneshots) if (!a.stopping && a.name !== 'hit') busy = true;
|
|
647
|
-
const idleT = still && !this.postures.length && !busy ? 1 : 0;
|
|
648
|
-
const id = this.idle;
|
|
649
|
-
id.w += clamp(idleT - id.w, -dt * 2, dt * 0.8);
|
|
650
|
-
this.applyIdle(P, id.w, dt);
|
|
651
|
-
for (const p of this.postures) { const w = p.w * p.w * (3 - 2 * p.w); POSTURE_DEFS[p.name].apply(P, w, p, e); }
|
|
652
|
-
const wantsToMove = e.wantSpeed > 0.02 * e.cfg.maxSpeed || !!e.input.follow;
|
|
653
|
-
for (const a of this.oneshots) {
|
|
654
|
-
if (wantsToMove && (a.name === 'eat' || a.name === 'drink')) this.end(a, 'interrupted');
|
|
655
|
-
a.t += dt * a.speed;
|
|
656
|
-
if (!a.stopping && ONESHOT_DEFS[a.name].update(a, dt * a.speed, e)) this.end(a, 'done');
|
|
657
|
-
const fadeOut = a.name === 'jump' && a.reason === 'done' ? 0.05 : a.name === 'attack' ? 0.25 : a.fade;
|
|
658
|
-
if (a.stopping) a.w = Math.max(0, a.w - dt / fadeOut); else a.w = Math.min(1, a.w + dt / a.fade);
|
|
659
|
-
const w = a.w * a.w * (3 - 2 * a.w);
|
|
660
|
-
if (w > 0) ONESHOT_DEFS[a.name].apply(P, w, a, e);
|
|
661
|
-
}
|
|
662
|
-
for (let i = this.oneshots.length - 1; i >= 0; i--) {
|
|
663
|
-
const a = this.oneshots[i];
|
|
664
|
-
if (a.stopping && a.w <= 0) { this.oneshots.splice(i, 1); a.resolve(a.reason || 'done'); e.emit('actionEnd', { name: a.name, reason: a.reason || 'done' }); }
|
|
665
|
-
}
|
|
666
|
-
if (e.air.active) { let air = false; for (const a of this.oneshots) if (a.name === 'jump') air = true; if (!air) e.touchDown(); }
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
applyIdle(P, w, dt) {
|
|
670
|
-
const e = this.e, id = this.idle, cfg = e.cfg;
|
|
671
|
-
if (w <= 0.001) { id.hasLook = false; return; }
|
|
672
|
-
// sit-and-wait: front legs a little raised now and then
|
|
673
|
-
const rf = cfg.idle.raiseFront * (0.5 + 0.5 * Math.sin(e.time * 0.21 + 1.3));
|
|
674
|
-
for (let i = 0; i < 8; i++) if (e.legs[i].def.i === 1) mxA(P.raise, i, rf, w);
|
|
675
|
-
// leg twitch: one leg lifts a touch and sets down again
|
|
676
|
-
id.twitchT -= dt;
|
|
677
|
-
if (id.twitchT <= 0 && w > 0.9) {
|
|
678
|
-
id.twitchT = (2.5 + e.rand() * 5) / Math.max(0.05, cfg.idle.twitch);
|
|
679
|
-
e.twitchLeg(Math.floor(e.rand() * 8));
|
|
680
|
-
}
|
|
681
|
-
// look around
|
|
682
|
-
if (!e.input.look) {
|
|
683
|
-
id.lookT -= dt;
|
|
684
|
-
if (id.lookT <= 0) {
|
|
685
|
-
id.lookT = 1.5 + e.rand() * 3;
|
|
686
|
-
if (e.rand() < 0.35) id.hasLook = false;
|
|
687
|
-
else {
|
|
688
|
-
const a = e.heading + (e.rand() - 0.5) * 2 * cfg.idle.look;
|
|
689
|
-
const d = 10 * cfg.bodyLen;
|
|
690
|
-
const px = e.pos.x + Math.sin(a) * d, pz = e.pos.z + Math.cos(a) * d;
|
|
691
|
-
id.look.set(px, e.terrainH(px, pz) + (e.rand() * 3) * cfg.bodyLen, pz);
|
|
692
|
-
id.hasLook = true;
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
if (id.hasLook) { P.look = id.look; P.lookW = w; }
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// ------------------------------------------------------------------------------------------------
|
|
701
|
-
export function createSpiderMotion(ctx) { return new SpiderMotion(ctx); }
|
|
702
|
-
|
|
703
|
-
export class SpiderMotion {
|
|
704
|
-
constructor({ species, data, skeleton, ground, water, position, heading = 0, emit }) {
|
|
705
|
-
this.species = species;
|
|
706
|
-
this.sk = skeleton;
|
|
707
|
-
this.cfg = resolveConfig(species, data, skeleton);
|
|
708
|
-
const cfg = this.cfg;
|
|
709
|
-
this.ground = ground || (() => 0);
|
|
710
|
-
this.water = water || null;
|
|
711
|
-
this.emit = emit || (() => {});
|
|
712
|
-
this.rand = prng((data.seed || 1) * 7919 + 29);
|
|
713
|
-
this.input = { speed: 0, heading, climb: 0, look: null, follow: null, target: null, targetSpeed: cfg.gears.run || 0.1, crouch: 0, gait: null };
|
|
714
|
-
this.pos = new THREE.Vector3();
|
|
715
|
-
this.velocity = new THREE.Vector3();
|
|
716
|
-
this.heading = heading; this.speed = 0; this.yawRate = 0; this.phase = 0; this.time = 0; this.lod = 0; this.frame = 0;
|
|
717
|
-
this.wantSpeed = 0; this.wantHeading = heading;
|
|
718
|
-
this.push = new THREE.Vector3(); this.pushV = new THREE.Vector3();
|
|
719
|
-
this.moveDir = new THREE.Vector3(0, 0, 1);
|
|
720
|
-
this.air = { active: false, t: 0, vy: 0, vh: 0, y0: 0, T: 0, dir: new THREE.Vector3() };
|
|
721
|
-
this.state = {
|
|
722
|
-
position: this.pos, heading, speed: 0, velocity: this.velocity, gait: 'stand', grounded: true,
|
|
723
|
-
action: null, posture: 'stand', eyelid: 0, lookTarget: null,
|
|
724
|
-
stats: { gait: 'stand', freq: 0, duty: 1 },
|
|
725
|
-
legs: LEG_KEYS.map((key) => ({ key, stance: true, contact: new THREE.Vector3() })),
|
|
726
|
-
};
|
|
727
|
-
this.headPose = { position: new THREE.Vector3(), quaternion: new THREE.Quaternion() };
|
|
728
|
-
this.debug = { targets: Array.from({ length: 8 }, () => new THREE.Vector3()) };
|
|
729
|
-
this.gaits = [...new Set(cfg.gaits.map((g) => g.name))];
|
|
730
|
-
this.gears = cfg.gears;
|
|
731
|
-
this.gait = { name: 'walk', f: 1, D: 0.7, lift: 0.003, bob: 0, sway: 0, off: new Float64Array(8) };
|
|
732
|
-
// springs
|
|
733
|
-
this.hS = new Spring(0); this.pitchS = new Spring(0); this.rollS = new Spring(0);
|
|
734
|
-
this.lookYawS = new Spring(0); this.lookPitchS = new Spring(0);
|
|
735
|
-
this.abdPitchS = new Spring(0); this.abdYawS = new Spring(0);
|
|
736
|
-
this.supportS = new Spring(0); this.speedS = new Spring(0);
|
|
737
|
-
this.breath = 0; this._lift = 0; this.brakeA = 0;
|
|
738
|
-
this.lastVel = new THREE.Vector3(); this.accelV = new THREE.Vector3();
|
|
739
|
-
// legs
|
|
740
|
-
this.legs = cfg.legs.map((L) => ({
|
|
741
|
-
def: L, state: 'stance', mode: 'gait',
|
|
742
|
-
plant: new THREE.Vector3(), lift: new THREE.Vector3(), land: new THREE.Vector3(), landBase: new THREE.Vector3(), tip: new THREE.Vector3(),
|
|
743
|
-
swingT: 0, swingDur: 0.1, liftH: 0.002, hMax: 0, stanceTime: 0, prevPhase: 0, windowUsed: false, heel: 0,
|
|
744
|
-
az: new Float64Array(7), el: new Float64Array(7), jp: Array.from({ length: 8 }, () => new THREE.Vector3()),
|
|
745
|
-
airFrom: new THREE.Vector3(), airRoot: new THREE.Vector3(), airT: 1, carry0: 0, swingInit: false, liftAz: new Float64Array(7), liftEl: new Float64Array(7), rawAz: new Float64Array(7), rawEl: new Float64Array(7), ikEl: new Float64Array(7), ikAz: new Float64Array(7),
|
|
746
|
-
}));
|
|
747
|
-
this.palpState = cfg.palpDefs.map(() => ({ twitch: new Float64Array(6), tgt: new Float64Array(6), twT: 0.5, az: new Float64Array(6), el: new Float64Array(6), jp: Array.from({ length: 7 }, () => new THREE.Vector3()) }));
|
|
748
|
-
// frames (preallocated)
|
|
749
|
-
this.qB = new THREE.Quaternion(); this.qBinv = new THREE.Quaternion(); this.upB = new THREE.Vector3(0, 1, 0); this.root = new THREE.Vector3();
|
|
750
|
-
this.qAbd = new THREE.Quaternion(); this.qPed = new THREE.Quaternion();
|
|
751
|
-
this._e = new THREE.Euler(); this._q = new THREE.Quaternion(); this._q2 = new THREE.Quaternion(); this._q3 = new THREE.Quaternion();
|
|
752
|
-
this._qc1 = new THREE.Quaternion(); this._qc2 = new THREE.Quaternion(); this._qc3 = new THREE.Quaternion();
|
|
753
|
-
this._scale = new THREE.Vector3(1, 1, 1); this._v = new THREE.Vector3(); this._w = new THREE.Vector3();
|
|
754
|
-
this.lookTarget = new THREE.Vector3();
|
|
755
|
-
this.P = createParams();
|
|
756
|
-
this.actionsLayer = new SpiderActions(this);
|
|
757
|
-
this.actions = this.actionsLayer.names;
|
|
758
|
-
this.attachmentPoints = this._attachments(data);
|
|
759
|
-
this.reset(position || new THREE.Vector3(), heading);
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
_attachments(data) {
|
|
763
|
-
const cfg = this.cfg, J = cfg.J;
|
|
764
|
-
const at = (bone, p) => {
|
|
765
|
-
const i = data.bones.findIndex((b) => b.name === bone);
|
|
766
|
-
if (i < 0) return null;
|
|
767
|
-
const inv = this.sk.inverses ? this.sk.inverses[i] : this.sk.bind[i].clone().invert();
|
|
768
|
-
return { bone, local: new THREE.Matrix4().makeTranslation(p.x, p.y, p.z).premultiply(inv) };
|
|
769
|
-
};
|
|
770
|
-
const mouth = cfg.mouth.clone().add(cfg.root);
|
|
771
|
-
let headP = J.ceph.clone().lerp(J.front, 0.6);
|
|
772
|
-
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); }
|
|
773
|
-
this._eyeLocal = headP.clone().sub(cfg.root);
|
|
774
|
-
const back = cfg.abdC.clone().add(cfg.root); back.y += cfg.abdR * 0.9;
|
|
775
|
-
const out = { mouth: at('head', mouth), head: at('head', headP), back: at('abdomen', back) };
|
|
776
|
-
for (const k of Object.keys(out)) if (!out[k]) delete out[k];
|
|
777
|
-
return out;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// ----------------------------------------------------------------- frames
|
|
781
|
-
forward(h, out) { return out.set(Math.sin(h), 0, Math.cos(h)); }
|
|
782
|
-
left(h, out) { return out.set(Math.cos(h), 0, -Math.sin(h)); }
|
|
783
|
-
terrainH(x, z) { const h = this.ground(x, z); return Number.isFinite(h) ? h : 0; }
|
|
784
|
-
localToWorld(x, y, z, out) { return out.set(x, y, z).applyQuaternion(this.qB).add(this.root); }
|
|
785
|
-
// heading frame (no pitch / roll) about the ground point p
|
|
786
|
-
headingToWorld(lx, lz, h, px, pz, out) {
|
|
787
|
-
const sh = Math.sin(h), ch = Math.cos(h);
|
|
788
|
-
return out.set(px + ch * lx + sh * lz, 0, pz - sh * lx + ch * lz);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
reset(p, heading = this.heading) {
|
|
792
|
-
this.pos.set(p.x, this.terrainH(p.x, p.z), p.z);
|
|
793
|
-
this.heading = heading; this.input.heading = heading; this.wantHeading = heading;
|
|
794
|
-
this.speed = 0; this.yawRate = 0; this.velocity.set(0, 0, 0); this.lastVel.set(0, 0, 0);
|
|
795
|
-
this.gaitAt(0);
|
|
796
|
-
for (const leg of this.legs) {
|
|
797
|
-
this.neutral(leg, 0, 0, 0, leg.plant);
|
|
798
|
-
leg.state = 'stance'; leg.stanceTime = 1;
|
|
799
|
-
}
|
|
800
|
-
this.hS.reset(this.cfg.rootH); this.pitchS.reset(0); this.rollS.reset(0);
|
|
801
|
-
this._fitTerrain();
|
|
802
|
-
this.hS.reset(this._fit.h + this.cfg.rootH * this.cfg.stance.height);
|
|
803
|
-
this.pitchS.reset(this._fit.pitch); this.rollS.reset(this._fit.roll);
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// ----------------------------------------------------------------- gait table
|
|
807
|
-
gaitAt(vn) {
|
|
808
|
-
const T = this.cfg.gaits, g = this.gait;
|
|
809
|
-
let i = 0;
|
|
810
|
-
while (i < T.length - 2 && vn > T[i + 1].v) i++;
|
|
811
|
-
const a = T[i], b = T[Math.min(i + 1, T.length - 1)];
|
|
812
|
-
const t = a === b ? 0 : smooth(0, 1, (vn - a.v) / (b.v - a.v));
|
|
813
|
-
g.name = t < 0.5 ? a.name : b.name;
|
|
814
|
-
g.f = lerp(a.f, b.f, t); g.D = lerp(a.D, b.D, t);
|
|
815
|
-
g.lift = lerp(a.lift, b.lift, t); g.bob = lerp(a.bob, b.bob, t); g.sway = lerp(a.sway, b.sway, t);
|
|
816
|
-
for (let j = 0; j < 8; j++) g.off[j] = wrap01(a.off[j] + d01(a.off[j], b.off[j]) * t);
|
|
817
|
-
if (vn > b.v) g.f = b.f * Math.min(1.3, vn / Math.max(b.v, 1e-6));
|
|
818
|
-
const cfg = this.cfg;
|
|
819
|
-
g.f /= cfg.sq;
|
|
820
|
-
g.lift *= cfg.s; g.bob *= cfg.s;
|
|
821
|
-
return g;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
// neutral claw position (world, on the terrain + tip height) tAhead seconds from now, plus a lead
|
|
825
|
-
// along the velocity (half a stance sweep) and the yaw of the turn
|
|
826
|
-
neutral(leg, tAhead, lead, yawLead, out) {
|
|
827
|
-
const L = leg.def, P = this.P;
|
|
828
|
-
const hp = this.heading + clamp(this.yawRate * tAhead + yawLead, -0.45, 0.45);
|
|
829
|
-
const bx = this.pos.x + this.velocity.x * tAhead, bz = this.pos.z + this.velocity.z * tAhead;
|
|
830
|
-
const k = 1 - 0.22 * P.legIn * (L.i === 1 || L.i === 4 ? 1.2 : 1);
|
|
831
|
-
this.headingToWorld(L.neutral.x * k, L.neutral.z * k, hp, bx, bz, out);
|
|
832
|
-
if (lead) { const vl = Math.hypot(this.velocity.x, this.velocity.z); if (vl > 1e-6) { out.x += (this.velocity.x / vl) * lead; out.z += (this.velocity.z / vl) * lead; } }
|
|
833
|
-
out.y = this.terrainH(out.x, out.z) + L.tipY;
|
|
834
|
-
return out;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// ----------------------------------------------------------------- public API
|
|
838
|
-
play(name, opts) { return this.actionsLayer.play(name, opts || {}); }
|
|
839
|
-
stop(name) { this.actionsLayer.stop(name); }
|
|
840
|
-
setLod(level) { this.lod = clamp(level | 0, 0, 2); return this; }
|
|
841
|
-
dispose() { this.actionsLayer.clear(); }
|
|
842
|
-
|
|
843
|
-
update(dt) {
|
|
844
|
-
if (!(dt > 1e-4) || !Number.isFinite(dt)) dt = 1e-4;
|
|
845
|
-
this.frame++;
|
|
846
|
-
// LOD 2 (crowds): the whole rig runs at half rate (a spider in a crowd is a few pixels tall)
|
|
847
|
-
if (this.lod === 2 && !this.input.follow) {
|
|
848
|
-
this._lodDt = (this._lodDt || 0) + dt;
|
|
849
|
-
if ((this.frame & 1) === 1 && this._lodDt < 1 / 24) return;
|
|
850
|
-
dt = this._lodDt; this._lodDt = 0;
|
|
851
|
-
}
|
|
852
|
-
dt = Math.min(dt, 1 / 20);
|
|
853
|
-
this.drive(dt);
|
|
854
|
-
this.actionsLayer.update(dt);
|
|
855
|
-
const sub = dt > 1 / 70 && this.lod === 0 ? 2 : 1;
|
|
856
|
-
for (let i = 0; i < sub; i++) this.step(dt / sub);
|
|
857
|
-
this.pose(dt);
|
|
858
|
-
this.publish();
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
// steering input -> wanted speed / heading
|
|
862
|
-
drive(dt) {
|
|
863
|
-
const inp = this.input, cfg = this.cfg;
|
|
864
|
-
this.wantSpeed = clamp(inp.speed || 0, 0, cfg.maxSpeed);
|
|
865
|
-
this.wantHeading = inp.heading ?? this.heading;
|
|
866
|
-
if (inp.target && !inp.follow) {
|
|
867
|
-
const dx = inp.target.x - this.pos.x, dz = inp.target.z - this.pos.z;
|
|
868
|
-
const d = Math.hypot(dx, dz);
|
|
869
|
-
const stopR = 0.4 * cfg.bodyLen + 0.05 * this.speed;
|
|
870
|
-
if (d < stopR) { inp.target = null; inp.speed = 0; this.wantSpeed = 0; this.emit('arrive', {}); }
|
|
871
|
-
else {
|
|
872
|
-
const want = Math.atan2(dx, dz);
|
|
873
|
-
inp.heading = want; this.wantHeading = want;
|
|
874
|
-
const ang = Math.abs(angDiff(this.heading, want));
|
|
875
|
-
const brake = Math.sqrt(Math.max(0, 2 * cfg.decel * 0.7 * (d - stopR)));
|
|
876
|
-
this.wantSpeed = Math.min(inp.targetSpeed || cfg.gears.run, cfg.maxSpeed, brake + 0.01 * cfg.sq, ang > 1.0 ? cfg.gears.walk || 0.05 : 1e9);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
if (this.actionsLayer.holdsStill()) {
|
|
880
|
-
if (this.wantSpeed > 0.02 * cfg.maxSpeed && !inp.follow) this.actionsLayer.requestStand();
|
|
881
|
-
this.wantSpeed = 0; this.wantHeading = this.heading; this._turnTo = undefined;
|
|
882
|
-
} else if (!inp.follow && this.wantSpeed < 0.02 * cfg.maxSpeed) {
|
|
883
|
-
// turn on the spot toward a look target outside the body's twist range (or an attack target)
|
|
884
|
-
const lt = inp.look;
|
|
885
|
-
if (lt) {
|
|
886
|
-
const want = Math.atan2(lt.x - this.pos.x, lt.z - this.pos.z);
|
|
887
|
-
const off = angDiff(this.heading, want);
|
|
888
|
-
if (Math.abs(off) > cfg.look.yaw * 1.1) this._turnTo = want;
|
|
889
|
-
else if (this._turnTo !== undefined && Math.abs(angDiff(this.heading, this._turnTo)) < 0.15) this._turnTo = undefined;
|
|
890
|
-
} else if (this._turnTo !== undefined && Math.abs(angDiff(this.heading, this._turnTo)) < 0.08) this._turnTo = undefined;
|
|
891
|
-
if (this._turnTo !== undefined) { this.wantHeading = this._turnTo; inp.heading = this._turnTo; }
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// ----------------------------------------------------------------- simulation step
|
|
896
|
-
step(dt) {
|
|
897
|
-
this.time += dt;
|
|
898
|
-
this.landT = (this.landT ?? 9) + dt;
|
|
899
|
-
const cfg = this.cfg, inp = this.input, P = this.P, air = this.air;
|
|
900
|
-
const F = this.forward(this.heading, tv());
|
|
901
|
-
if (inp.follow && !air.active) {
|
|
902
|
-
const fv = inp.follow.velocity;
|
|
903
|
-
const vx = fv ? fv.x : 0, vz = fv ? fv.z : 0;
|
|
904
|
-
const sp = Math.hypot(vx, vz);
|
|
905
|
-
this.speed = Math.max(0, this.speedS.step(sp, 12, dt));
|
|
906
|
-
let hT = inp.follow.heading;
|
|
907
|
-
if (hT === null || hT === undefined) hT = sp > 0.1 * cfg.maxSpeed ? Math.atan2(vx, vz) : this.heading;
|
|
908
|
-
const wMax = cfg.turnRate * 1.5;
|
|
909
|
-
const wT = clamp(angDiff(this.heading, hT) * 6, -wMax, wMax);
|
|
910
|
-
this.yawRate += clamp(wT - this.yawRate, -40 * dt, 40 * dt);
|
|
911
|
-
this.heading += this.yawRate * dt;
|
|
912
|
-
this.pos.x = inp.follow.position.x; this.pos.z = inp.follow.position.z;
|
|
913
|
-
this.velocity.set(vx, 0, vz);
|
|
914
|
-
} else {
|
|
915
|
-
let vT = this.wantSpeed * P.speedScale * P.gaitW;
|
|
916
|
-
if (air.active) vT = this.speed;
|
|
917
|
-
const acc = vT > this.speed ? cfg.accel : this.landT < COAST ? 0 : this.landT < 0.2 ? Math.max(cfg.decel, this.landV / 0.1) : cfg.decel;
|
|
918
|
-
this.speed += clamp(vT - this.speed, -acc * dt, acc * dt);
|
|
919
|
-
if (air.active) this.speed = lerp(air.v0, air.vh, Math.min(1, air.t / PUSH));
|
|
920
|
-
const vn = this.speed / cfg.maxSpeed;
|
|
921
|
-
const wMax = air.active ? 0 : Math.min(cfg.turnRate * lerp(0.7, 1, smooth(0, 0.5, vn)), cfg.latAccelMax / Math.max(this.speed, 1e-3)) * (P.gaitW > 0.5 ? 1 : 0);
|
|
922
|
-
const wT = clamp(angDiff(this.heading, this.wantHeading) * 5, -wMax, wMax);
|
|
923
|
-
this.yawRate += clamp(wT - this.yawRate, -30 * dt, 30 * dt);
|
|
924
|
-
this.heading += this.yawRate * dt;
|
|
925
|
-
this.forward(this.heading, F);
|
|
926
|
-
this.velocity.copy(air.active ? air.dir : F).multiplyScalar(this.speed);
|
|
927
|
-
if (this.push.lengthSq() > 1e-12 || this.pushV.lengthSq() > 1e-12) {
|
|
928
|
-
// a shove: the velocity it adds rises over a few frames and decays (no velocity step)
|
|
929
|
-
this.pushV.lerp(this.push, 1 - Math.exp(-dt / 0.05));
|
|
930
|
-
this.velocity.add(this.pushV);
|
|
931
|
-
this.push.multiplyScalar(Math.exp(-dt * 7));
|
|
932
|
-
if (this.push.lengthSq() < 1e-10) this.push.set(0, 0, 0);
|
|
933
|
-
if (this.pushV.lengthSq() < 1e-10 && this.push.lengthSq() === 0) this.pushV.set(0, 0, 0);
|
|
934
|
-
}
|
|
935
|
-
this.pos.x += this.velocity.x * dt; this.pos.z += this.velocity.z * dt;
|
|
936
|
-
}
|
|
937
|
-
if (this.heading > Math.PI * 64 || this.heading < -Math.PI * 64) this.heading = angDiff(0, this.heading);
|
|
938
|
-
this.pos.y = this.terrainH(this.pos.x, this.pos.z);
|
|
939
|
-
this.accelV.copy(this.velocity).sub(this.lastVel).divideScalar(dt);
|
|
940
|
-
this.lastVel.copy(this.velocity);
|
|
941
|
-
|
|
942
|
-
// jump flight
|
|
943
|
-
if (air.active) {
|
|
944
|
-
air.t += dt;
|
|
945
|
-
// push-off: the legs extend over PUSH seconds (body accelerating, claws still planted), then
|
|
946
|
-
// the flight is ballistic
|
|
947
|
-
// (the vertical acceleration never flips sign from one frame to the next: push, a coast
|
|
948
|
-
// frame, free fall, and at touchdown another coast frame before the legs brake the fall)
|
|
949
|
-
const t = air.t, Tp = PUSH, Tc = PUSH + COAST;
|
|
950
|
-
if (t < Tp) this.airY = air.y0 + (air.vy * t * t) / (2 * Tp);
|
|
951
|
-
else if (t < Tc) this.airY = air.y0 + air.vy * Tp * 0.5 + air.vy * (t - Tp);
|
|
952
|
-
else { const u = t - Tc; this.airY = air.y0 + air.vy * (Tp * 0.5 + COAST) + air.vy * u - 0.5 * G_ACC * u * u; }
|
|
953
|
-
if (!air.legsOff && t >= Tp * 0.0) {
|
|
954
|
-
air.legsOff = true;
|
|
955
|
-
for (const leg of this.legs) if (leg.state === 'stance') { leg.carry0 = 0; leg.state = 'air'; this.fkTip(leg, leg.rawAz, leg.rawEl, leg.airFrom); leg.airRoot.copy(this.root); leg.airT = 0; leg.liftAz.set(leg.rawAz); leg.liftEl.set(leg.rawEl); }
|
|
956
|
-
}
|
|
957
|
-
// the legs reach the ground well before the body comes down: it is braked on them
|
|
958
|
-
const yLand = this.pos.y + air.h0 + 4.2 * cfg.rootH;
|
|
959
|
-
if (air.t > PUSH + COAST + 0.02 && air.vy - G_ACC * (air.t - PUSH - COAST) < 0 && this.airY <= yLand) this.touchDown();
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
// gait
|
|
963
|
-
const vPlanar = Math.hypot(this.velocity.x, this.velocity.z);
|
|
964
|
-
const vEff = Math.max(vPlanar, Math.abs(this.yawRate) * cfg.turnR * 0.8);
|
|
965
|
-
const g = this.gaitAt(vEff / cfg.sq);
|
|
966
|
-
const moving = vEff > 0.012 * cfg.maxSpeed && P.gaitW > 0.5 && !air.active;
|
|
967
|
-
if (moving) this.phase = wrap01(this.phase + g.f * dt);
|
|
968
|
-
const st = this.state.stats;
|
|
969
|
-
st.gait = moving ? g.name : 'stand'; st.freq = moving ? g.f : 0; st.duty = g.D;
|
|
970
|
-
this._moving = moving;
|
|
971
|
-
let swinging = 0, swingSet = -1;
|
|
972
|
-
for (const leg of this.legs) if (leg.state === 'swing') { swinging++; swingSet = leg.def.set; }
|
|
973
|
-
const tStance = g.D / g.f;
|
|
974
|
-
for (let li = 0; li < 8; li++) {
|
|
975
|
-
const leg = this.legs[li], L = leg.def;
|
|
976
|
-
const p = wrap01(this.phase - g.off[li]);
|
|
977
|
-
if (p < leg.prevPhase - 0.5) leg.windowUsed = false;
|
|
978
|
-
if (air.active) {
|
|
979
|
-
leg.prevPhase = p;
|
|
980
|
-
continue;
|
|
981
|
-
}
|
|
982
|
-
if (leg.state === 'stance') {
|
|
983
|
-
leg.stanceTime += dt;
|
|
984
|
-
if (moving) {
|
|
985
|
-
// lift-off: at the end of the stance window, or early if the claw trails too far
|
|
986
|
-
const nb = this.neutral(leg, 0, 0, 0, tv());
|
|
987
|
-
const fx = leg.plant.x - nb.x, fz = leg.plant.z - nb.z;
|
|
988
|
-
const trail = -(fx * this.velocity.x + fz * this.velocity.z) / Math.max(vPlanar, 1e-6);
|
|
989
|
-
const off = Math.hypot(fx, fz);
|
|
990
|
-
const rr = this.coxaDist(leg, leg.plant);
|
|
991
|
-
const caz = Math.abs(this.coxaAz(leg, leg.plant));
|
|
992
|
-
// early lift-off when the claw drifts out of comfortable range (after a minimum stance), at
|
|
993
|
-
// once when it is about to leave the reachable workspace
|
|
994
|
-
const soft = (vPlanar > 1e-4 && trail > 0.32 * L.total) || off > 0.42 * L.total || rr < L.rMin * 0.92 || rr > L.rMax || caz > L.azMax * 1.15;
|
|
995
|
-
const hard = rr < L.rMin * 0.8 || rr > L.rMax * 1.06 || caz > L.azMax * 1.4;
|
|
996
|
-
const drag = (leg.reachErr || 0) > 0.004 * L.total; // the claw can no longer be held: step at once
|
|
997
|
-
// (while shoved, early lifts are capped at four legs so it scrambles on a tetrapod)
|
|
998
|
-
// (just after a landing the claws are wherever they came down: they re-step one tetrapod at a
|
|
999
|
-
// time instead of all together)
|
|
1000
|
-
const settle = this.landT < 0.8 && !((swinging === 0 || swingSet === L.set) && swinging < 4);
|
|
1001
|
-
const over = drag || hard || (!settle && soft && leg.stanceTime > 0.4 * tStance && (swinging < 4 || this.pushV.lengthSq() < 1e-8));
|
|
1002
|
-
if (((p >= g.D && leg.stanceTime > 0.3 * tStance) || over) && !leg.windowUsed) {
|
|
1003
|
-
leg.windowUsed = true;
|
|
1004
|
-
const remain = Math.max((1 - p) / g.f, 0.45 * (1 - g.D) / g.f);
|
|
1005
|
-
this.startSwing(leg, remain, g.lift, 'gait');
|
|
1006
|
-
swinging++; swingSet = L.set;
|
|
1007
|
-
}
|
|
1008
|
-
} else if (P.gaitW > 0.5 || P.dead < 0.5) {
|
|
1009
|
-
// standing: re-step feet that are far from their neutral foothold, one tetrapod at a time
|
|
1010
|
-
const nb = this.neutral(leg, 0, 0, 0, tv());
|
|
1011
|
-
const err = Math.hypot(nb.x - leg.plant.x, nb.z - leg.plant.z);
|
|
1012
|
-
const thr = (P.gaitW > 0.5 ? 0.075 : 0.05) * L.total;
|
|
1013
|
-
if (err > thr && leg.stanceTime > 0.12 && this.wantSpeed < 0.02 * cfg.maxSpeed && P.curl < 0.05 && P.dead < 0.02 && P.drawIn < 0.02 && P.raise[li] + P.hold[li] < 0.05 && (swinging === 0 || swingSet === L.set) && swinging < 4) {
|
|
1014
|
-
this.startSwing(leg, (0.12 + 0.5 * err / L.total) * cfg.sq, 0.06 * L.total * (P.gaitW > 0.5 ? 1 : 0.6), 'step');
|
|
1015
|
-
swinging++; swingSet = L.set;
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
} else if (leg.state === 'swing') {
|
|
1019
|
-
leg.swingT += dt / leg.swingDur;
|
|
1020
|
-
const remain = Math.max(0, (1 - leg.swingT) * leg.swingDur);
|
|
1021
|
-
if (leg.mode === 'gait' || leg.mode === 'step') {
|
|
1022
|
-
const half = moving ? Math.min(vPlanar * tStance * 0.5, 0.3 * L.total) : 0;
|
|
1023
|
-
const yawLead = moving ? this.yawRate * tStance * 0.5 : 0;
|
|
1024
|
-
// the landing spot is predicted once, at lift-off (a swing lasts a few frames; a target that
|
|
1025
|
-
// moved under a fast claw would make it jerk); the next step corrects any change of course
|
|
1026
|
-
if (!leg.targetSet) {
|
|
1027
|
-
leg.targetSet = true;
|
|
1028
|
-
this.neutral(leg, remain, half, yawLead, leg.landBase);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
// the landing spot stays inside the workspace of where the body will actually be (the clamp
|
|
1032
|
-
// moves continuously with the body, so the claw is never yanked)
|
|
1033
|
-
if (leg.mode !== 'land') {
|
|
1034
|
-
leg.land.copy(leg.landBase);
|
|
1035
|
-
this.clampWorkspace(leg, leg.land, remain);
|
|
1036
|
-
this._swingClear(leg);
|
|
1037
|
-
}
|
|
1038
|
-
if (leg.swingT >= 1) {
|
|
1039
|
-
leg.state = 'stance'; leg.windowUsed = leg.mode === 'gait' ? leg.windowUsed : false;
|
|
1040
|
-
leg.plant.copy(leg.land); leg.stanceTime = 0;
|
|
1041
|
-
if (leg.mode === 'gait') leg.windowUsed = false;
|
|
1042
|
-
if (leg.mode === 'gait' && P.gaitW > 0.5) this.emit('footstep', { foot: L.key, position: leg.plant.clone(), speed: this.speed, strength: clamp(0.15 + this.speed / cfg.maxSpeed * 0.5, 0, 1) });
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
leg.prevPhase = p;
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
// horizontal distance of a world point from the leg's coxa base (current body, heading frame)
|
|
1050
|
-
coxaDist(leg, w) {
|
|
1051
|
-
const L = leg.def, h = this.heading, sh = Math.sin(h), ch = Math.cos(h);
|
|
1052
|
-
const dx = w.x - this.pos.x, dz = w.z - this.pos.z;
|
|
1053
|
-
const lx = dx * ch - dz * sh, lz = dx * sh + dz * ch;
|
|
1054
|
-
return Math.hypot(lx - L.P0[0].x, lz - L.P0[0].z);
|
|
1055
|
-
}
|
|
1056
|
-
// azimuth of a world point about the coxa base relative to the leg's neutral direction
|
|
1057
|
-
coxaAz(leg, w) {
|
|
1058
|
-
const L = leg.def, h = this.heading, sh = Math.sin(h), ch = Math.cos(h);
|
|
1059
|
-
const dx = w.x - this.pos.x, dz = w.z - this.pos.z;
|
|
1060
|
-
const lx = dx * ch - dz * sh - L.P0[0].x, lz = dx * sh + dz * ch - L.P0[0].z;
|
|
1061
|
-
return angDiff(L.azN, Math.atan2(lx * L.side, lz));
|
|
1062
|
-
}
|
|
1063
|
-
// keep a landing target inside the leg's workspace (radially about the coxa base)
|
|
1064
|
-
clampWorkspace(leg, w, tAhead = 0) {
|
|
1065
|
-
// relative to where the body will be at touchdown
|
|
1066
|
-
const L = leg.def, h = this.heading + clamp(this.yawRate * tAhead, -0.45, 0.45), sh = Math.sin(h), ch = Math.cos(h);
|
|
1067
|
-
const px = this.pos.x + this.velocity.x * tAhead, pz = this.pos.z + this.velocity.z * tAhead;
|
|
1068
|
-
const dx = w.x - px, dz = w.z - pz;
|
|
1069
|
-
let lx = dx * ch - dz * sh - L.P0[0].x, lz = dx * sh + dz * ch - L.P0[0].z;
|
|
1070
|
-
const r = Math.hypot(lx, lz) || 1e-9;
|
|
1071
|
-
const rc = clamp(r, L.rMin * 1.04, L.rMax * 0.96);
|
|
1072
|
-
// and within +-L.azMax of the neutral direction
|
|
1073
|
-
const a = Math.atan2(lx * L.side, lz), da = angDiff(L.azN, a);
|
|
1074
|
-
const ac = L.azN + clamp(da, -L.azMax, L.azMax);
|
|
1075
|
-
if (rc === r && ac === L.azN + da) return w;
|
|
1076
|
-
lx = Math.sin(ac) * rc * L.side + L.P0[0].x; lz = Math.cos(ac) * rc + L.P0[0].z;
|
|
1077
|
-
w.x = px + ch * lx + sh * lz; w.z = pz - sh * lx + ch * lz;
|
|
1078
|
-
w.y = this.terrainH(w.x, w.z) + L.tipY;
|
|
1079
|
-
return w;
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
// swing clearance: sample the terrain along the path so the claw steps over bumps
|
|
1083
|
-
_swingClear(leg) {
|
|
1084
|
-
let m = -1e9;
|
|
1085
|
-
for (let k = 1; k <= 3; k++) {
|
|
1086
|
-
const u = k / 4;
|
|
1087
|
-
const x = lerp(leg.lift.x, leg.land.x, u), z = lerp(leg.lift.z, leg.land.z, u);
|
|
1088
|
-
const base = lerp(leg.lift.y, leg.land.y, u);
|
|
1089
|
-
m = Math.max(m, this.terrainH(x, z) + leg.def.tipY - base);
|
|
1090
|
-
}
|
|
1091
|
-
leg.hMax = Math.max(0, m);
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
startSwing(leg, dur, lift, mode) {
|
|
1095
|
-
leg.lift.copy(leg.plant);
|
|
1096
|
-
leg.land.copy(leg.plant); leg.landBase.copy(leg.plant);
|
|
1097
|
-
leg.state = 'swing'; leg.mode = mode;
|
|
1098
|
-
leg.swingT = 0; leg.swingDur = Math.max(0.05, dur);
|
|
1099
|
-
leg.liftH = lift;
|
|
1100
|
-
leg.swingInit = true;
|
|
1101
|
-
leg.targetSet = mode === 'twitch';
|
|
1102
|
-
this._swingClear(leg);
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
// an idle twitch: lift the claw a little and set it down again (almost) in place
|
|
1106
|
-
twitchLeg(i) {
|
|
1107
|
-
const leg = this.legs[i];
|
|
1108
|
-
if (leg.state !== 'stance' || this._moving || this.P.gaitW < 0.5) return;
|
|
1109
|
-
this.startSwing(leg, 0.22 * this.cfg.sq, 0.05 * leg.def.total, 'twitch');
|
|
1110
|
-
const nb = this.neutral(leg, 0, 0, 0, tv());
|
|
1111
|
-
leg.land.lerp(nb, 0.5); leg.land.y = this.terrainH(leg.land.x, leg.land.z) + leg.def.tipY;
|
|
1112
|
-
leg.landBase.copy(leg.land);
|
|
1113
|
-
this._swingClear(leg);
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
takeOff(vy, vh) {
|
|
1117
|
-
const a = this.air, cfg = this.cfg;
|
|
1118
|
-
a.active = true; a.t = 0; a.vy = vy; a.vh = vh; a.v0 = this.speed;
|
|
1119
|
-
this.forward(this.heading, a.dir);
|
|
1120
|
-
a.y0 = this.root.y;
|
|
1121
|
-
a.h0 = this.root.y - this.pos.y;
|
|
1122
|
-
a.T = (2 * vy) / G_ACC;
|
|
1123
|
-
this.airY = a.y0;
|
|
1124
|
-
a.legsOff = false;
|
|
1125
|
-
// legs already in the air (mid-swing) follow the body at once; planted ones push off first
|
|
1126
|
-
for (const leg of this.legs) if (leg.state !== 'stance' || leg.def.i <= 2) { leg.carry0 = leg.state === 'stance' ? 0 : 1; leg.state = 'air'; this.fkTip(leg, leg.rawAz, leg.rawEl, leg.airFrom); leg.airRoot.copy(this.root); leg.airT = 0; leg.liftAz.set(leg.rawAz); leg.liftEl.set(leg.rawEl); }
|
|
1127
|
-
this.state.grounded = false;
|
|
1128
|
-
this.emit('takeoff', {});
|
|
1129
|
-
void cfg;
|
|
1130
|
-
}
|
|
1131
|
-
touchDown() {
|
|
1132
|
-
const a = this.air;
|
|
1133
|
-
if (!a.active) return;
|
|
1134
|
-
a.active = false;
|
|
1135
|
-
this.state.grounded = true;
|
|
1136
|
-
// every claw comes down near its neutral foothold, the legs reaching out from the tuck
|
|
1137
|
-
// (clamped into the workspace around where the body stops: a coast frame, then the brake)
|
|
1138
|
-
const tStop = COAST + Math.min(this.speed / (2 * this.cfg.decel), 0.05);
|
|
1139
|
-
for (const leg of this.legs) {
|
|
1140
|
-
// (the legs reach out to spread footholds as they land: a landing on legs tucked under the
|
|
1141
|
-
// body would leave it standing on stilts)
|
|
1142
|
-
const nb = this.neutral(leg, tStop, 0, 0, tv());
|
|
1143
|
-
leg.plant.set(lerp(leg.tip.x, nb.x, 0.85), 0, lerp(leg.tip.z, nb.z, 0.85));
|
|
1144
|
-
leg.plant.y = this.terrainH(leg.plant.x, leg.plant.z) + leg.def.tipY;
|
|
1145
|
-
this.clampWorkspace(leg, leg.plant, tStop);
|
|
1146
|
-
leg.lift.copy(leg.tip);
|
|
1147
|
-
leg.land.copy(leg.plant);
|
|
1148
|
-
leg.state = 'swing'; leg.mode = 'land'; leg.swingInit = true; leg.swingT = 0; leg.swingDur = 0.13 * this.cfg.sq; leg.liftH = 0; leg.hMax = 0;
|
|
1149
|
-
}
|
|
1150
|
-
this.landV = this.speed;
|
|
1151
|
-
// the body keeps its falling speed into the landing spring (legs absorb the impact)
|
|
1152
|
-
this.hS.x = this.airY; this.hS.v = Math.min(0, a.vy - G_ACC * Math.max(0, a.t - PUSH - COAST));
|
|
1153
|
-
{
|
|
1154
|
-
const v = -this.hS.v, rest = this.pos.y + this.cfg.rootH * 0.78, d = Math.max(1e-4, this.airY - v * COAST - rest);
|
|
1155
|
-
this.brakeA = (v * v) / (2 * d);
|
|
1156
|
-
this.landFrames = 0;
|
|
1157
|
-
}
|
|
1158
|
-
this.actionsLayer.onLand();
|
|
1159
|
-
this.landT = 0;
|
|
1160
|
-
this.emit('land', {});
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
_fitTerrain() {
|
|
1164
|
-
const cfg = this.cfg, W = cfg.fitW, h = this.heading;
|
|
1165
|
-
let a = 0, b = 0, c = 0;
|
|
1166
|
-
const tmp = tv();
|
|
1167
|
-
for (let i = 0; i < 8; i++) {
|
|
1168
|
-
const L = cfg.legs[i];
|
|
1169
|
-
this.headingToWorld(L.neutral.x, L.neutral.z, h, this.pos.x, this.pos.z, tmp);
|
|
1170
|
-
const y = this.terrainH(tmp.x, tmp.z);
|
|
1171
|
-
a += W[0][i] * y; b += W[1][i] * y; c += W[2][i] * y;
|
|
1172
|
-
}
|
|
1173
|
-
this._fit = this._fit || { h: 0, pitch: 0, roll: 0 };
|
|
1174
|
-
this._fit.h = a; this._fit.pitch = Math.atan(c) * 0.85; this._fit.roll = Math.atan(b) * 0.85;
|
|
1175
|
-
return this._fit;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
// ----------------------------------------------------------------- pose
|
|
1179
|
-
pose(dt) {
|
|
1180
|
-
const cfg = this.cfg, P = this.P, g = this.gait, t = this.time, air = this.air;
|
|
1181
|
-
const fit = this._fitTerrain();
|
|
1182
|
-
const vn = this.speed / cfg.maxSpeed;
|
|
1183
|
-
// look: body twist and rearing toward the look target
|
|
1184
|
-
const look = this.input.look || P.look;
|
|
1185
|
-
let lyT = 0, lpT = 0;
|
|
1186
|
-
if (look && P.lookW > 0.01 && P.dead < 0.5) {
|
|
1187
|
-
const dx = look.x - this.pos.x, dz = look.z - this.pos.z;
|
|
1188
|
-
const rel = angDiff(this.heading, Math.atan2(dx, dz));
|
|
1189
|
-
lyT = clamp(rel, -cfg.look.yaw, cfg.look.yaw) * P.lookW * (1 - smooth(0.2, 0.6, vn) * 0.8);
|
|
1190
|
-
const dy = look.y - (this.pos.y + cfg.rootH * 1.5);
|
|
1191
|
-
lpT = clamp(Math.atan2(dy, Math.hypot(dx, dz) + 1e-6), 0, cfg.look.pitch) * P.lookW * (1 - smooth(0.1, 0.4, vn));
|
|
1192
|
-
this.lookTarget.copy(look); this.state.lookTarget = this.lookTarget;
|
|
1193
|
-
} else this.state.lookTarget = null;
|
|
1194
|
-
const lookYaw = this.lookYawS.step(lyT, 7, dt), lookPitch = this.lookPitchS.step(lpT, 6, dt);
|
|
1195
|
-
// raise the front legs a little when rearing to look up
|
|
1196
|
-
if (lookPitch > 0.01) for (let i = 0; i < 8; i++) if (this.legs[i].def.i === 1) P.raise[i] = Math.max(P.raise[i], 0.8 * lookPitch / cfg.look.pitch);
|
|
1197
|
-
// body height, pitch and roll from the terrain plane, the posture and the gait
|
|
1198
|
-
const moving = this._moving ? smooth(0, 0.15, vn) : 0;
|
|
1199
|
-
const bob = g.bob * moving * Math.cos(TAU * 2 * this.phase);
|
|
1200
|
-
const sway = g.sway * moving * Math.sin(TAU * this.phase);
|
|
1201
|
-
const dropK = 1 - 0.55 * P.crouch;
|
|
1202
|
-
const hT = fit.h + cfg.rootH * cfg.stance.height * dropK - P.drop + bob;
|
|
1203
|
-
// a stiffer spring for a moment after landing (the legs absorb the fall)
|
|
1204
|
-
let h;
|
|
1205
|
-
if (this.brakeA > 0 && this.hS.v < 0) {
|
|
1206
|
-
// landing: a coast frame, then constant braking on the legs down to the stance height
|
|
1207
|
-
// frame 0: the landing height itself; frame 1: coasting; then braking
|
|
1208
|
-
const lf = this.landFrames++;
|
|
1209
|
-
if (lf >= 2) this.hS.v = Math.min(0, this.hS.v + this.brakeA * dt);
|
|
1210
|
-
if (lf >= 1) this.hS.x += this.hS.v * dt;
|
|
1211
|
-
h = this.hS.x;
|
|
1212
|
-
} else { this.brakeA = 0; h = this.hS.step(hT, 18, dt); }
|
|
1213
|
-
if (air.active) { h = this.airY; this.hS.x = h; this.hS.v = 0; }
|
|
1214
|
-
const pitch = this.pitchS.step(fit.pitch * (1 - P.dead) + P.pitch + lookPitch, 14, dt);
|
|
1215
|
-
const roll = this.rollS.step(fit.roll * (1 - P.dead), 14, dt);
|
|
1216
|
-
// body rotation: heading (+ look twist + sway), pitch (nose up), roll, then the death flip about
|
|
1217
|
-
// the long axis
|
|
1218
|
-
this._e.set(-pitch, this.heading + lookYaw + sway + P.yaw, roll + P.roll, 'YXZ');
|
|
1219
|
-
this.qB.setFromEuler(this._e);
|
|
1220
|
-
if (P.flip > 1e-4) this.qB.multiply(this._q.setFromAxisAngle(Z_AXIS, P.flip * P.flipAngle * P.flipSide));
|
|
1221
|
-
// lunge / side offsets in the heading frame
|
|
1222
|
-
const F = this.forward(this.heading, tv()), Lf = this.left(this.heading, tv());
|
|
1223
|
-
this.root.set(this.pos.x, h, this.pos.z).addScaledVector(F, P.fwd).addScaledVector(Lf, P.side);
|
|
1224
|
-
this.qBinv.copy(this.qB).invert();
|
|
1225
|
-
this.upB.set(0, 1, 0).applyQuaternion(this.qBinv); // world up in the body frame
|
|
1226
|
-
|
|
1227
|
-
// abdomen: hangs from the pedicel on a spring; lags in accelerations and turns; rests on the
|
|
1228
|
-
// ground in low postures; breathes
|
|
1229
|
-
const aLocal = this._v.copy(this.accelV).applyQuaternion(this.qBinv);
|
|
1230
|
-
const A = cfg.abd;
|
|
1231
|
-
const pT = -A.droop * (1 - moving) + P.abdPitch - clamp(aLocal.z * A.lag * 0.02, -0.2, 0.2) + clamp(aLocal.y * 0.002, -0.1, 0.1);
|
|
1232
|
-
const yT = clamp(-aLocal.x * A.lag * 0.02, -0.25, 0.25) - this.yawRate * 0.04 * A.lag * 10;
|
|
1233
|
-
let ap = this.abdPitchS.step(pT, A.stiffness, dt);
|
|
1234
|
-
const ay = this.abdYawS.step(clamp(yT, -0.3, 0.3), A.stiffness * 0.8, dt);
|
|
1235
|
-
const tmp = tv();
|
|
1236
|
-
const pb = cfg.ax.pedB, lo = cfg.abdLow;
|
|
1237
|
-
const abdLowAt = (a) => {
|
|
1238
|
-
// world position of the abdomen's lowest bind point for abdomen pitch a (no yaw)
|
|
1239
|
-
const c = Math.cos(a), sn = Math.sin(a);
|
|
1240
|
-
return this.localToWorld(pb.x + lo.x, pb.y + lo.y * c - lo.z * sn, pb.z + lo.y * sn + lo.z * c, tmp);
|
|
1241
|
-
};
|
|
1242
|
-
// (faded out continuously in death, when the body itself lies on the ground)
|
|
1243
|
-
const wAl = 1 - smooth(0.25, 0.6, P.dead);
|
|
1244
|
-
if (wAl > 0) {
|
|
1245
|
-
// keep the abdomen off the ground (or rest it on the ground in low postures)
|
|
1246
|
-
const ap0 = ap;
|
|
1247
|
-
abdLowAt(ap);
|
|
1248
|
-
const gap = tmp.y - this.terrainH(tmp.x, tmp.z) - 0.00025 * cfg.s;
|
|
1249
|
-
const L = Math.hypot(lo.y, lo.z);
|
|
1250
|
-
if (P.abdDown > 0.01 && gap > 0) ap -= P.abdDown * clamp(gap / L, 0, 0.35);
|
|
1251
|
-
if (gap < 0) ap += Math.min(0.6, (-gap / L) * 1.1);
|
|
1252
|
-
// every other extreme point too (rearing, the rear end is the lowest)
|
|
1253
|
-
const c = Math.cos(ap), sn = Math.sin(ap);
|
|
1254
|
-
let fix = 0;
|
|
1255
|
-
for (const p of cfg.supAbd) {
|
|
1256
|
-
if (p === lo || p.z > 0) continue;
|
|
1257
|
-
this.localToWorld(pb.x + p.x, pb.y + p.y * c - p.z * sn, pb.z + p.y * sn + p.z * c, tmp);
|
|
1258
|
-
const g = tmp.y - this.terrainH(tmp.x, tmp.z) - 0.00025 * cfg.s;
|
|
1259
|
-
if (g < 0) fix = Math.max(fix, Math.min(0.6, (-g / Math.max(1e-6, Math.hypot(p.y, p.z))) * 1.1));
|
|
1260
|
-
}
|
|
1261
|
-
ap += fix;
|
|
1262
|
-
ap = ap0 + (ap - ap0) * wAl;
|
|
1263
|
-
}
|
|
1264
|
-
this._e.set(ap, ay, 0, 'YXZ');
|
|
1265
|
-
this.qAbd.setFromEuler(this._e);
|
|
1266
|
-
this.qPed.identity().slerp(this.qAbd, 0.5);
|
|
1267
|
-
// body support: nothing of the body goes into the ground (bumps under the sternum or abdomen,
|
|
1268
|
-
// a dead spider lying on its back or side); a spring keeps it continuous
|
|
1269
|
-
let lift = 0;
|
|
1270
|
-
for (const p of cfg.supPro) {
|
|
1271
|
-
this.localToWorld(p.x, p.y, p.z, tmp);
|
|
1272
|
-
const pen = this.terrainH(tmp.x, tmp.z) + 0.0001 * cfg.s - tmp.y;
|
|
1273
|
-
if (pen > lift) lift = pen;
|
|
1274
|
-
}
|
|
1275
|
-
{
|
|
1276
|
-
const qa = this._q2.copy(this.qB).multiply(this.qAbd);
|
|
1277
|
-
const ped = this._w.subVectors(pb, cfg.ax.pedA).applyQuaternion(this._q.copy(this.qB).multiply(this.qPed));
|
|
1278
|
-
const pA = this.localToWorld(cfg.ax.pedA.x, cfg.ax.pedA.y, cfg.ax.pedA.z, tv()).add(ped);
|
|
1279
|
-
for (const p of cfg.supAbd) {
|
|
1280
|
-
tmp.copy(p).applyQuaternion(qa).add(pA);
|
|
1281
|
-
const pen = this.terrainH(tmp.x, tmp.z) + 0.0001 * cfg.s - tmp.y;
|
|
1282
|
-
if (pen > lift) lift = pen;
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
// the leg roots (trochanters, femur bases) are as rigid with the body as the carapace: a deep crouch
|
|
1286
|
-
// or a steep rear must not push them into the ground either
|
|
1287
|
-
for (const leg of this.legs) for (let k = 1; k <= 2; k++) {
|
|
1288
|
-
const q = leg.jp[k];
|
|
1289
|
-
const pen = this.terrainH(q.x, q.z) + 2.2 * leg.def.tipY - (q.y - this._lift);
|
|
1290
|
-
if (pen > lift) lift = pen;
|
|
1291
|
-
}
|
|
1292
|
-
const wLeg = P.dead * Math.max(smooth(0, 0.08, P.flip), P.curl2); // dying, it rests on its curling legs
|
|
1293
|
-
if (wLeg > 1e-3) {
|
|
1294
|
-
// rolling over, a dead spider rests on its curled legs as well (folded tight before it rolls);
|
|
1295
|
-
// the joints were placed last frame including last frame's lift, so compare without it
|
|
1296
|
-
lift = Math.max(lift, this._legSupport(wLeg, this._lift));
|
|
1297
|
-
}
|
|
1298
|
-
const sup = this.supportS.step(lift, 30, dt);
|
|
1299
|
-
// smooth maximum of the spring and the raw depth (never far below the ground, no kinks)
|
|
1300
|
-
this._lift = -smin(-sup, -lift, 0.0003 * cfg.s);
|
|
1301
|
-
this.root.y += this._lift;
|
|
1302
|
-
this.breath += dt * TAU * cfg.breath.rate * P.breathRate;
|
|
1303
|
-
const br = Math.sin(this.breath) * cfg.breath.amp * P.breathAmp;
|
|
1304
|
-
|
|
1305
|
-
this.writeAxial(br);
|
|
1306
|
-
this.solveLegs(dt);
|
|
1307
|
-
if (wLeg > 1e-3) {
|
|
1308
|
-
// the legs of a dying spider move fast (curling, rolling over): the support above used last
|
|
1309
|
-
// frame's joints, so check this frame's too and lift the whole body clear if any went under
|
|
1310
|
-
const extra = this._legSupport(wLeg, 0);
|
|
1311
|
-
if (extra > 1e-6) {
|
|
1312
|
-
this.root.y += extra; this._lift += extra;
|
|
1313
|
-
this.writeAxial(br);
|
|
1314
|
-
this.solveLegs(0);
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
this.solvePalps(dt);
|
|
1318
|
-
this.solveChelicerae(dt);
|
|
1319
|
-
// head pose
|
|
1320
|
-
const hb = this.sk.bones[cfg.bone.head];
|
|
1321
|
-
if (hb) {
|
|
1322
|
-
this.localToWorld(this._eyeLocal.x, this._eyeLocal.y, this._eyeLocal.z, this.headPose.position);
|
|
1323
|
-
this.headPose.quaternion.copy(this.qB);
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
// how far the body must rise so no leg joint is in the ground (joints nearer the knee carry a wider
|
|
1328
|
-
// margin: the knuckles are thick): a soft maximum over the joints, so the support does not jerk as
|
|
1329
|
-
// the lowest joint changes from one leg to another while it rolls
|
|
1330
|
-
_legSupport(w, lifted) {
|
|
1331
|
-
const k = 0.00006 * this.cfg.s;
|
|
1332
|
-
let mx = -Infinity;
|
|
1333
|
-
const pens = this._pens || (this._pens = new Float64Array(48));
|
|
1334
|
-
let n = 0;
|
|
1335
|
-
for (const leg of this.legs) for (let j = 2; j < 8; j++) {
|
|
1336
|
-
const q = leg.jp[j];
|
|
1337
|
-
const p = this.terrainH(q.x, q.z) + (j === 7 ? 1.6 : j === 2 ? 2.2 : 5) * leg.def.tipY - (q.y - lifted);
|
|
1338
|
-
pens[n++] = p; if (p > mx) mx = p;
|
|
1339
|
-
}
|
|
1340
|
-
if (mx < -8 * k) return 0;
|
|
1341
|
-
let sum = 0;
|
|
1342
|
-
for (let i = 0; i < n; i++) sum += Math.exp((pens[i] - mx) / k);
|
|
1343
|
-
// softplus of the soft maximum: 0 when every joint is clear, the depth when one is under
|
|
1344
|
-
const sm = mx + k * Math.log(sum);
|
|
1345
|
-
return w * (sm > 8 * k ? sm : k * Math.log1p(Math.exp(sm / k)));
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
setBone(i, pos, q, scale) {
|
|
1349
|
-
if (i < 0) return;
|
|
1350
|
-
const b = this.sk.bones[i];
|
|
1351
|
-
b.matrixWorld.compose(pos, this._q3.copy(q).multiply(this.cfg.bindQ[i]), scale || ONE);
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
writeAxial(br) {
|
|
1355
|
-
const cfg = this.cfg, ax = cfg.ax, B = cfg.bone;
|
|
1356
|
-
const p = tv();
|
|
1357
|
-
this.localToWorld(ax.pedA.x, ax.pedA.y, ax.pedA.z, p);
|
|
1358
|
-
this.setBone(B.prosoma, p, this.qB);
|
|
1359
|
-
this.localToWorld(ax.ceph.x, ax.ceph.y, ax.ceph.z, p);
|
|
1360
|
-
this.setBone(B.head, p, this.qB);
|
|
1361
|
-
// pedicel and abdomen hang from pedA
|
|
1362
|
-
const qP = this._q2.copy(this.qB).multiply(this.qPed);
|
|
1363
|
-
this.localToWorld(ax.pedA.x, ax.pedA.y, ax.pedA.z, p);
|
|
1364
|
-
this.setBone(B.pedicel, p, qP);
|
|
1365
|
-
const pedVec = tv().subVectors(ax.pedB, ax.pedA).applyQuaternion(qP);
|
|
1366
|
-
const abdHead = tv().copy(p).add(pedVec);
|
|
1367
|
-
const qA = tv(); void qA;
|
|
1368
|
-
const qAw = this._q2.copy(this.qB).multiply(this.qAbd);
|
|
1369
|
-
this._scale.set(1 + br, 1 + br * 0.4, 1 + br);
|
|
1370
|
-
this.setBone(B.abdomen, abdHead, qAw, this._scale);
|
|
1371
|
-
if (B.spinnerets >= 0) {
|
|
1372
|
-
const sv = tv().subVectors(ax.abdEnd, ax.pedB).multiplyScalar(1 + br * 0.4).applyQuaternion(qAw);
|
|
1373
|
-
const sp = tv().copy(abdHead).add(sv);
|
|
1374
|
-
// spinnerets twitch a little
|
|
1375
|
-
this._e.set(0.05 * Math.sin(this.time * 1.7) * this.P.twitch, 0.06 * Math.sin(this.time * 0.9 + 1) * this.P.twitch, 0, 'YXZ');
|
|
1376
|
-
this.setBone(B.spinnerets, sp, this._q.copy(qAw).multiply(this._q3.setFromEuler(this._e)));
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
// ---------------------------------------------------------------- legs
|
|
1381
|
-
solveLegs(dt) {
|
|
1382
|
-
const P = this.P, cfg = this.cfg, air = this.air;
|
|
1383
|
-
const tipL = tv();
|
|
1384
|
-
for (let li = 0; li < 8; li++) {
|
|
1385
|
-
const leg = this.legs[li], L = leg.def;
|
|
1386
|
-
// world claw target
|
|
1387
|
-
const T = tv();
|
|
1388
|
-
let heel = 0;
|
|
1389
|
-
if (leg.state === 'stance') {
|
|
1390
|
-
this.postureTarget(leg, T.copy(leg.plant));
|
|
1391
|
-
if (this._moving) heel = smooth(0.5, 1.0, leg.stanceTime * this.gait.f / Math.max(this.gait.D, 0.05));
|
|
1392
|
-
} else if (leg.state === 'swing') {
|
|
1393
|
-
// swing in joint space: from the lift-off angles to the angles that reach the (moving) landing
|
|
1394
|
-
// target, with short speed ramps, plus a femur lift that clears the terrain on the way. Each
|
|
1395
|
-
// joint then sweeps a smooth arc instead of folding and unfolding to follow a straight path.
|
|
1396
|
-
const u = clamp(leg.swingT, 0, 1);
|
|
1397
|
-
const s = progress(u, TUNE.ramp);
|
|
1398
|
-
if (leg.swingInit) { leg.liftAz.set(leg.rawAz); leg.liftEl.set(leg.rawEl); leg.liftHeel = leg.heel; leg.swingInit = false; }
|
|
1399
|
-
// the landing pose is solved in the body frame predicted for the moment of touchdown (in the
|
|
1400
|
-
// current frame a claw landing ahead would look further away than it will be)
|
|
1401
|
-
const rem = Math.max(0, (1 - u) * leg.swingDur);
|
|
1402
|
-
const qp = this._q2.setFromAxisAngle(Y_AXIS, clamp(this.yawRate * rem, -0.45, 0.45)).multiply(this.qB).invert();
|
|
1403
|
-
this.postureTarget(leg, tipL.copy(leg.land));
|
|
1404
|
-
tipL.x -= this.root.x + this.velocity.x * rem; tipL.y -= this.root.y; tipL.z -= this.root.z + this.velocity.z * rem;
|
|
1405
|
-
tipL.applyQuaternion(qp);
|
|
1406
|
-
const az = leg.az, el = leg.el;
|
|
1407
|
-
let reach = Math.max(0.3 * L.total, Math.hypot(tipL.x - L.P0[2].x, tipL.z - L.P0[2].z));
|
|
1408
|
-
if (TUNE.path === 'polar') {
|
|
1409
|
-
// claw path in polar coordinates about the coxa base (radius and azimuth interpolate, so a
|
|
1410
|
-
// radial leg neither overshoots nor cuts across), height with the clearance hump
|
|
1411
|
-
const B = L.P0[0], sd = L.side;
|
|
1412
|
-
const lL = tv().copy(leg.lift).sub(this.root).applyQuaternion(this.qBinv);
|
|
1413
|
-
const r0 = Math.hypot(lL.x - B.x, lL.z - B.z), r1 = Math.hypot(tipL.x - B.x, tipL.z - B.z);
|
|
1414
|
-
const a0 = Math.atan2((lL.x - B.x) * sd, lL.z - B.z), a1 = Math.atan2((tipL.x - B.x) * sd, tipL.z - B.z);
|
|
1415
|
-
const r = lerp(r0, r1, s), a = a0 + angDiff(a0, a1) * s;
|
|
1416
|
-
const y = lerp(lL.y, tipL.y, s) + (leg.liftH + leg.hMax) * hump(u);
|
|
1417
|
-
tipL.set(B.x + Math.sin(a) * r * sd, y, B.z + Math.cos(a) * r);
|
|
1418
|
-
this.solveLeg(leg, tipL, leg.liftHeel * (1 - s) * cfg.heelLift);
|
|
1419
|
-
} else {
|
|
1420
|
-
this.solveLeg(leg, tipL, 0);
|
|
1421
|
-
if (TUNE.debug) leg._landEl = Array.from(el);
|
|
1422
|
-
for (let k = 0; k < 7; k++) { az[k] = leg.liftAz[k] + angDiff(leg.liftAz[k], az[k]) * s; el[k] = leg.liftEl[k] + angDiff(leg.liftEl[k], el[k]) * s; }
|
|
1423
|
-
const b = ((leg.liftH + leg.hMax) * hump(u)) / reach;
|
|
1424
|
-
el[2] += b * TUNE.bf; el[3] -= b * TUNE.bp; el[5] -= b * TUNE.bm;
|
|
1425
|
-
}
|
|
1426
|
-
this._unwrapIK(leg, li);
|
|
1427
|
-
leg.rawAz.set(az); leg.rawEl.set(el);
|
|
1428
|
-
T.lerpVectors(leg.lift, leg.land, s);
|
|
1429
|
-
const wPose = this.blendLegPose(leg, li);
|
|
1430
|
-
this.writeLeg(leg);
|
|
1431
|
-
if (wPose > 1e-3 && (P.dead < 0.5 || P.flip < 0.3)) this.groundGuard(leg, (P.dead < 0.5 ? 1 : 1 - smooth(0.02, 0.3, P.flip)) * smooth(0, 0.05, wPose));
|
|
1432
|
-
// a joint-space swing can dip toward the ground between lift-off and touchdown: lift the femur
|
|
1433
|
-
// by just enough to keep the claw and the tarsus base clear (continuous in the depth)
|
|
1434
|
-
{
|
|
1435
|
-
// (faded in and out at the ends of the swing, where the claw is on the ground anyway)
|
|
1436
|
-
const r = smooth(0, 0.05, u) * (1 - smooth(0.95, 1, u));
|
|
1437
|
-
// (the tibia end first: the femur lifts it; then the claw and tarsus: the metatarsus)
|
|
1438
|
-
const q5 = leg.jp[5], p5 = this.terrainH(q5.x, q5.z) + L.tipY * 2 - q5.y;
|
|
1439
|
-
if (p5 > 0 && r > 0) { const de = r * Math.min(0.6, (p5 * 1.2) / (L.len[2] + L.Lpt)); el[2] += de; leg.rawEl[2] += de; this.writeLeg(leg); }
|
|
1440
|
-
let pen = 0;
|
|
1441
|
-
for (let k = 6; k <= 7; k++) { const q = leg.jp[k]; pen = Math.max(pen, this.terrainH(q.x, q.z) + L.tipY * (k === 7 ? 1 : 1.5) - q.y); }
|
|
1442
|
-
if (pen > 0 && r > 0) { const de = r * Math.min(0.8, (pen * 1.3) / (L.len[5] * Math.max(0.3, Math.cos(el[5])))); el[5] += de; leg.rawEl[5] += de; this.writeLeg(leg); }
|
|
1443
|
-
}
|
|
1444
|
-
leg.ikEl.set(leg.el); leg.ikAz.set(leg.az);
|
|
1445
|
-
if (this.debug.targets[li]) this.debug.targets[li].copy(T);
|
|
1446
|
-
continue;
|
|
1447
|
-
} else {
|
|
1448
|
-
// in the air: the claw leaves the ground picking up the body's motion gradually, then settles
|
|
1449
|
-
// on its place relative to the body (the action pose does the rest)
|
|
1450
|
-
const nb = tv().copy(L.P0[7]).applyQuaternion(this.qB).add(this.root);
|
|
1451
|
-
leg.airT = Math.min(1, leg.airT + dt / 0.25);
|
|
1452
|
-
const carry = Math.min(1, leg.carry0 + leg.airT * 2.5);
|
|
1453
|
-
T.copy(leg.airFrom).addScaledVector(tv().subVectors(this.root, leg.airRoot), carry);
|
|
1454
|
-
T.lerp(nb, smooth(0.2, 1, leg.airT));
|
|
1455
|
-
}
|
|
1456
|
-
// the heel eases (a leg that has stood for a while must not snap to a lifted heel)
|
|
1457
|
-
leg.heel += (heel - leg.heel) * (1 - Math.exp(-dt / 0.045));
|
|
1458
|
-
heel = leg.heel;
|
|
1459
|
-
// body frame
|
|
1460
|
-
tipL.copy(T).sub(this.root).applyQuaternion(this.qBinv);
|
|
1461
|
-
this.solveLeg(leg, tipL, heel * cfg.heelLift);
|
|
1462
|
-
if (leg.state === 'air' && leg.airT < 0.6) {
|
|
1463
|
-
// leaving the ground (possibly mid-swing, whose joint-space pose is not the IK one): the
|
|
1464
|
-
// joints ease from their take-off angles into the solved ones
|
|
1465
|
-
const w = smooth(0, 0.6, leg.airT), az = leg.az, el = leg.el;
|
|
1466
|
-
for (let k = 0; k < 7; k++) { az[k] = leg.liftAz[k] + angDiff(leg.liftAz[k], az[k]) * w; el[k] = leg.liftEl[k] + (el[k] - leg.liftEl[k]) * w; }
|
|
1467
|
-
}
|
|
1468
|
-
this._unwrapIK(leg, li);
|
|
1469
|
-
leg.rawAz.set(leg.az); leg.rawEl.set(leg.el);
|
|
1470
|
-
// action poses blended in joint-angle space
|
|
1471
|
-
const wPose = this.blendLegPose(leg, li);
|
|
1472
|
-
this.writeLeg(leg);
|
|
1473
|
-
// a planted claw the leg can no longer reach (body shoved or lunging past it) must step
|
|
1474
|
-
leg.reachErr = leg.state === 'stance' && wPose < 1e-3 ? Math.hypot(leg.tip.x - T.x, leg.tip.z - T.z) : 0;
|
|
1475
|
-
if (wPose > 1e-3 && (P.dead < 0.5 || P.flip < 0.3)) this.groundGuard(leg, (P.dead < 0.5 ? 1 : 1 - smooth(0.02, 0.3, P.flip)) * smooth(0, 0.05, wPose));
|
|
1476
|
-
leg.ikEl.set(leg.el); leg.ikAz.set(leg.az); // the displayed angles: next frame's IK is unwrapped against them
|
|
1477
|
-
if (this.debug.targets[li]) this.debug.targets[li].copy(T);
|
|
1478
|
-
}
|
|
1479
|
-
void air; void P;
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
solveLeg(leg, T, heel) {
|
|
1483
|
-
const L = leg.def, az = leg.az, el = leg.el, s = L.side;
|
|
1484
|
-
const B = L.P0[0];
|
|
1485
|
-
// tarsus along the leg azimuth at its bind slope (steeper while the heel lifts)
|
|
1486
|
-
// azimuths of points very close to the coxa are ill-defined: fall back to the neutral one
|
|
1487
|
-
const rS = L.rMin;
|
|
1488
|
-
const azG = (dx, dz, r0) => {
|
|
1489
|
-
const a = Math.atan2(dx * s, dz), d = Math.hypot(dx, dz);
|
|
1490
|
-
return L.phi0 + angDiff(L.phi0, a) * smooth(0.3 * r0, 0.75 * r0, d);
|
|
1491
|
-
};
|
|
1492
|
-
const phT = azG(T.x - B.x, T.z - B.z, rS);
|
|
1493
|
-
// the tarsus keeps its slope relative to the ground, not the body (a planted tarsus does not roll
|
|
1494
|
-
// on its pad when the body pitches or rolls)
|
|
1495
|
-
const u = this.upB, tilt = Math.asin(clamp(u.x * Math.sin(phT) * s + u.z * Math.cos(phT), -1, 1));
|
|
1496
|
-
const eta = L.el0[6] - heel - clamp(tilt, -0.7, 0.7) * (1 - this.P.dead);
|
|
1497
|
-
const cT = Math.cos(eta);
|
|
1498
|
-
const Ax = T.x - L.len[6] * Math.sin(phT) * cT * s, Ay = T.y - L.len[6] * Math.sin(eta), Az = T.z - L.len[6] * Math.cos(phT) * cT;
|
|
1499
|
-
// coxa yaws part of the way, the trochanter the rest
|
|
1500
|
-
const phA = azG(Ax - B.x, Az - B.z, rS);
|
|
1501
|
-
const dph = angDiff(L.phi0, phA);
|
|
1502
|
-
const phC = L.az0[0] + clamp(0.55 * dph, -0.6, 0.6);
|
|
1503
|
-
const eC = L.el0[0];
|
|
1504
|
-
let x = B.x + L.len[0] * Math.sin(phC) * Math.cos(eC) * s, y = B.y + L.len[0] * Math.sin(eC), z = B.z + L.len[0] * Math.cos(phC) * Math.cos(eC);
|
|
1505
|
-
const phTr = azG(Ax - x, Az - z, rS * 0.7);
|
|
1506
|
-
const eTr = L.el0[1];
|
|
1507
|
-
x += L.len[1] * Math.sin(phTr) * Math.cos(eTr) * s; y += L.len[1] * Math.sin(eTr); z += L.len[1] * Math.cos(phTr) * Math.cos(eTr);
|
|
1508
|
-
// vertical leg plane from the femur base to the metatarsus-tarsus joint
|
|
1509
|
-
const phP = azG(Ax - x, Az - z, rS * 0.6);
|
|
1510
|
-
const ux = Math.sin(phP) * s, uz = Math.cos(phP);
|
|
1511
|
-
const h = (Ax - x) * ux + (Az - z) * uz, v = Ay - y;
|
|
1512
|
-
const Lf = L.len[2], Lpt = L.Lpt, Lm = L.len[5];
|
|
1513
|
-
const d1 = (Lf + Lpt) * 0.97;
|
|
1514
|
-
// metatarsus: preferred slope, flattening as the leg stretches
|
|
1515
|
-
const D = Math.hypot(h, v);
|
|
1516
|
-
let em = L.el0[5] + clamp(1.1 * (D - L.D0) / L.D0, -0.6, 0.9);
|
|
1517
|
-
em = Math.min(em, -0.05);
|
|
1518
|
-
let mh = h - Lm * Math.cos(em), mv = v - Lm * Math.sin(em);
|
|
1519
|
-
let dm = Math.hypot(mh, mv);
|
|
1520
|
-
{
|
|
1521
|
-
// too far for femur + patella/tibia with the preferred slope: turn the metatarsus toward the
|
|
1522
|
-
// femur base (its straightest, longest reach), smoothly with the reach excess
|
|
1523
|
-
const w = smooth(0.84 * d1, 1.0 * d1, dm);
|
|
1524
|
-
if (w > 0) {
|
|
1525
|
-
em = em + angDiff(em, Math.atan2(v, h)) * w;
|
|
1526
|
-
mh = h - Lm * Math.cos(em); mv = v - Lm * Math.sin(em);
|
|
1527
|
-
dm = Math.hypot(mh, mv);
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
// two-link femur + (patella, tibia), knee up, soft reach limit
|
|
1531
|
-
const dr = clamp(softMin(dm, d1, 0.04 * d1), Math.abs(Lf - Lpt) * 1.02 + 1e-7, d1);
|
|
1532
|
-
const al = Math.atan2(mv, mh);
|
|
1533
|
-
const cb = clamp((Lf * Lf + dr * dr - Lpt * Lpt) / (2 * Lf * dr), -1, 1);
|
|
1534
|
-
const ef = al + Math.acos(cb);
|
|
1535
|
-
const kh = Lf * Math.cos(ef), kv = Lf * Math.sin(ef);
|
|
1536
|
-
const mhr = mh * dr / Math.max(dm, 1e-9), mvr = mv * dr / Math.max(dm, 1e-9);
|
|
1537
|
-
const ept = Math.atan2(mvr - kv, mhr - kh);
|
|
1538
|
-
az[0] = phC; el[0] = eC;
|
|
1539
|
-
az[1] = phTr; el[1] = eTr;
|
|
1540
|
-
az[2] = az[3] = az[4] = az[5] = phP;
|
|
1541
|
-
el[2] = ef; el[3] = ept + L.dp; el[4] = ept + L.dt; el[5] = em;
|
|
1542
|
-
az[6] = phT; el[6] = eta;
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
// Action poses in joint-angle space. Relative bends (deg) from the femur outward:
|
|
1546
|
-
// raise (threat / grab): femur up, leg nearly straight, pointing up and forward
|
|
1547
|
-
// hold (prey, sleep tuck): femur up, knee sharply flexed, tip toward the mouth
|
|
1548
|
-
// curl (death): femur down, every joint flexed: the leg folds under the body
|
|
1549
|
-
// air (jump): front legs by raise, the hind legs trailing, all a little flexed
|
|
1550
|
-
blendLegPose(leg, li) {
|
|
1551
|
-
const P = this.P, L = leg.def, az = leg.az, el = leg.el;
|
|
1552
|
-
// the in-air pose takes a leg only once it has left the ground (pushing legs stay planted)
|
|
1553
|
-
const r = P.raise[li], hd = P.hold[li], c = P.curl, a = P.air > 0 ? P.air * (leg.state === 'stance' && this.air.active ? 0 : smooth(0, 0.35, leg.airT)) : 0;
|
|
1554
|
-
if (r + hd + c + a + P.curl2 < 1e-4) return 0;
|
|
1555
|
-
const front = L.i === 1 ? 1 : L.i === 2 ? 0.7 : 0;
|
|
1556
|
-
if (a > 0) this.poseBlend(leg, a * (1 - front * 0.7), L.i >= 3 ? 10 : 30, L.i >= 3 ? P_AIR_H : P_AIR_F, L.i >= 3 ? 8 : -6, false);
|
|
1557
|
-
if (r > 0) this.poseBlend(leg, r, 62 + 8 * front, P_RAISE, -10 * front, false);
|
|
1558
|
-
if (hd > 0) this.poseBlend(leg, hd, 42, P_HOLD, -22 * front - 6, false);
|
|
1559
|
-
// death: every joint flexes ventrally, distal joints first (the hydraulic collapse), and the legs
|
|
1560
|
-
// gather under the sternum (over it once it lies on its back)
|
|
1561
|
-
if (c > 0) this.poseBlend(leg, c, 75, P_TUCK, -10, false);
|
|
1562
|
-
if (P.curl2 > 1e-4) {
|
|
1563
|
-
// upright, the legs half-flex with the knees up (the dying spider sinks, legs drawn in); as the
|
|
1564
|
-
// body rolls over the femurs swing ventrally and every joint flexes fully, so the corpse rests
|
|
1565
|
-
// on its back or side inside the cage of its legs. Once curl2 is 1 no angle depends on the IK.
|
|
1566
|
-
const cu = P.flipAngle < 2 && L.side === -P.flipSide ? L.curlSide : L.curlBack;
|
|
1567
|
-
const down = L.side === -P.flipSide, roll = P.flip * P.flipAngle;
|
|
1568
|
-
// (the legs it rolls onto fold ventrally first, the upper ones as they come off the ground)
|
|
1569
|
-
const w = P.curl2, fl = down ? smooth(0, 0.3 * Math.PI, roll) : smooth(0.3 * Math.PI, 0.6 * Math.PI, roll), cp = L.curlUp;
|
|
1570
|
-
if (P.flip > 1e-4 || (P.reviving && this._lift > 0.4 * this.cfg.rootH)) {
|
|
1571
|
-
// rolled over, the claws' stance targets mean nothing: the IK is replaced by the upright
|
|
1572
|
-
// curl as the base the pose blends from (the IK comes back only once it is upright)
|
|
1573
|
-
const g = Math.max(smooth(0, 0.1, P.flip), P.reviving * smooth(0.4 * this.cfg.rootH, 1.2 * this.cfg.rootH, this._lift));
|
|
1574
|
-
el[0] += (cp.C - el[0]) * g; el[1] += (cp.C - el[1]) * g;
|
|
1575
|
-
let e0 = cp.F;
|
|
1576
|
-
el[2] += (e0 - el[2]) * g;
|
|
1577
|
-
for (let k = 3; k < 7; k++) { e0 += cp.B[k - 3]; el[k] += (e0 - el[k]) * g; }
|
|
1578
|
-
az[0] += (L.az0[0] - az[0]) * g;
|
|
1579
|
-
for (let k = 1; k < 7; k++) az[k] += (L.phi0 - az[k]) * g;
|
|
1580
|
-
}
|
|
1581
|
-
const cT = lerp(cp.C, cu.C, fl);
|
|
1582
|
-
el[0] += (cT - el[0]) * w; el[1] += (cT - el[1]) * w;
|
|
1583
|
-
el[2] += (lerp(cp.F, cu.F, fl) - el[2]) * w;
|
|
1584
|
-
let e = el[2];
|
|
1585
|
-
for (let k = 3; k < 7; k++) { e += lerp(cp.B[k - 3], cu.B[k - 3], fl); el[k] += (e - el[k]) * w; }
|
|
1586
|
-
az[0] += (L.az0[0] - az[0]) * w;
|
|
1587
|
-
for (let k = 1; k < 7; k++) az[k] += (L.phi0 - az[k]) * w;
|
|
1588
|
-
}
|
|
1589
|
-
return Math.min(1, r + hd + c + a + P.curl2);
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
// posture changes of a claw target (world): drawn in toward the coxa along the ground (a collapsing
|
|
1593
|
-
// spider), lifted while the legs curl or uncurl (so the blend never sweeps through the ground),
|
|
1594
|
-
// tucked under the sternum; the same for planted claws and for landing targets
|
|
1595
|
-
postureTarget(leg, T) {
|
|
1596
|
-
const P = this.P, L = leg.def;
|
|
1597
|
-
if (P.drawIn > 1e-3) {
|
|
1598
|
-
// (sleep pulls them in further than death: the living legs keep their pose)
|
|
1599
|
-
const cb = tv().copy(L.P0[0]).applyQuaternion(this.qB).add(this.root);
|
|
1600
|
-
const k = 0.3 * P.drawIn + 0.2 * P.pullIn;
|
|
1601
|
-
T.x += (cb.x - T.x) * k; T.z += (cb.z - T.z) * k;
|
|
1602
|
-
T.y = this.terrainH(T.x, T.z) + L.tipY;
|
|
1603
|
-
}
|
|
1604
|
-
if (P.dead > 1e-3 && P.curl > 1e-3) T.y += 0.28 * L.total * smooth(0, 0.6, P.curl);
|
|
1605
|
-
if (P.tuck > 1e-3) {
|
|
1606
|
-
const B = L.P0[0];
|
|
1607
|
-
T.lerp(this.localToWorld(B.x * 0.35, B.y - 0.25 * L.len[2], B.z * 0.6, tv()), P.tuck);
|
|
1608
|
-
}
|
|
1609
|
-
return T;
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
// The IK's elevations are defined modulo a turn. Pose blends interpolate them linearly toward fixed
|
|
1613
|
-
// targets, so while any pose is blended in they are taken on the turn nearest to what was shown
|
|
1614
|
-
// last frame (a leg released from a pose takes the short way back, never a full spin); with no pose
|
|
1615
|
-
// active they are brought back to (-pi, pi], which changes nothing visible.
|
|
1616
|
-
_unwrapIK(leg, li) {
|
|
1617
|
-
const P = this.P, el = leg.el, az = leg.az, pv = leg.ikEl, pa = leg.ikAz;
|
|
1618
|
-
const posed = P.raise[li] + P.hold[li] + P.curl + P.air + P.curl2 > 1e-5;
|
|
1619
|
-
for (let k = 0; k < 7; k++) {
|
|
1620
|
-
el[k] = posed ? pv[k] + angDiff(pv[k], el[k]) : angDiff(0, el[k]);
|
|
1621
|
-
az[k] = posed ? pa[k] + angDiff(pa[k], az[k]) : angDiff(0, az[k]);
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
// blend one pose into the solved joint angles: femur elevation (deg) and the bends of the following
|
|
1626
|
-
// joints relative to it; staggered = distal joints move first
|
|
1627
|
-
poseBlend(leg, w, femur, bends, dAz, staggered) {
|
|
1628
|
-
if (w < 1e-4) return;
|
|
1629
|
-
const L = leg.def, az = leg.az, el = leg.el;
|
|
1630
|
-
let e = femur * DEG;
|
|
1631
|
-
el[2] += (e - el[2]) * (staggered ? smooth(0.35, 1, w) : w);
|
|
1632
|
-
for (let k = 3; k < 7; k++) {
|
|
1633
|
-
e += bends[k - 3] * DEG;
|
|
1634
|
-
const wk = staggered ? smooth(STAG[k][0], STAG[k][1], w) : w;
|
|
1635
|
-
el[k] += (e - el[k]) * wk;
|
|
1636
|
-
}
|
|
1637
|
-
const tAz = L.phi0 + dAz * DEG;
|
|
1638
|
-
az[0] += (L.az0[0] - az[0]) * w;
|
|
1639
|
-
for (let k = 1; k < 7; k++) az[k] += (tAz - az[k]) * w;
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
// A posed leg (raised, holding, curled, tucked) must not pass through the ground: rotate the chain
|
|
1643
|
-
// from the femur outward about its base by just enough to lift the deepest joint clear (the
|
|
1644
|
-
// direction that raises that joint, found numerically, so it also works upside down).
|
|
1645
|
-
groundGuard(leg, strength = 1) {
|
|
1646
|
-
const L = leg.def, el = leg.el, jp = leg.jp, m = L.tipY * 1.1, k2 = (0.1 * L.total) ** 2;
|
|
1647
|
-
const pen = this._gpen || (this._gpen = new Float64Array(8)), y0 = this._gy0 || (this._gy0 = new Float64Array(8));
|
|
1648
|
-
for (let it = 0; it < 3; it++) {
|
|
1649
|
-
let any = false;
|
|
1650
|
-
for (let k = 3; k < 8; k++) { const q = jp[k]; pen[k] = Math.max(0, this.terrainH(q.x, q.z) + (k === 7 ? m : 1.8 * m) - q.y); y0[k] = q.y; if (pen[k] > 0) any = true; }
|
|
1651
|
-
if (!any) return;
|
|
1652
|
-
const e = 0.05;
|
|
1653
|
-
for (let k = 2; k < 7; k++) el[k] += e;
|
|
1654
|
-
this.writeLeg(leg);
|
|
1655
|
-
// least-squares step over every joint in the ground (each enters with zero depth: continuous)
|
|
1656
|
-
let num = 0, den = k2;
|
|
1657
|
-
for (let k = 3; k < 8; k++) { const sl = (jp[k].y - y0[k]) / e; num += pen[k] * sl; den += pen[k] > 0 ? sl * sl : 0; }
|
|
1658
|
-
const dd = clamp((1.05 * num) / den, -0.6, 0.6) * strength - e;
|
|
1659
|
-
for (let k = 2; k < 7; k++) el[k] += dd;
|
|
1660
|
-
this.writeLeg(leg);
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
// claw tip (world) of a leg for joint angles az / el (no bones written)
|
|
1668
|
-
fkTip(leg, az, el, out) {
|
|
1669
|
-
const L = leg.def, s = L.side;
|
|
1670
|
-
let x = L.P0[0].x, y = L.P0[0].y, z = L.P0[0].z;
|
|
1671
|
-
for (let k = 0; k < 7; k++) {
|
|
1672
|
-
const ce = Math.cos(el[k]);
|
|
1673
|
-
x += L.len[k] * Math.sin(az[k]) * ce * s; y += L.len[k] * Math.sin(el[k]); z += L.len[k] * Math.cos(az[k]) * ce;
|
|
1674
|
-
}
|
|
1675
|
-
return this.localToWorld(x, y, z, out);
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
writeLeg(leg) {
|
|
1679
|
-
const L = leg.def, az = leg.az, el = leg.el, s = L.side, jp = leg.jp;
|
|
1680
|
-
const q = this._q, qy = this._q2;
|
|
1681
|
-
const p = tv().copy(L.P0[0]);
|
|
1682
|
-
this.localToWorld(p.x, p.y, p.z, jp[0]);
|
|
1683
|
-
for (let k = 0; k < 7; k++) {
|
|
1684
|
-
// rotation from bind: body x yaw about body up x pitch about the bind leg-plane normal
|
|
1685
|
-
qy.setFromAxisAngle(Y_AXIS, s * angDiff(L.az0[k], az[k]));
|
|
1686
|
-
q.setFromAxisAngle(L.n0, el[k] - L.el0[k]);
|
|
1687
|
-
qy.multiply(q);
|
|
1688
|
-
q.copy(this.qB).multiply(qy);
|
|
1689
|
-
this.setBone(L.bones[k], jp[k], q);
|
|
1690
|
-
const ce = Math.cos(el[k]);
|
|
1691
|
-
p.x += L.len[k] * Math.sin(az[k]) * ce * s; p.y += L.len[k] * Math.sin(el[k]); p.z += L.len[k] * Math.cos(az[k]) * ce;
|
|
1692
|
-
this.localToWorld(p.x, p.y, p.z, jp[k + 1]);
|
|
1693
|
-
}
|
|
1694
|
-
leg.tip.copy(jp[7]);
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
// ---------------------------------------------------------------- palps
|
|
1698
|
-
solvePalps(dt) {
|
|
1699
|
-
const cfg = this.cfg, P = this.P, t = this.time;
|
|
1700
|
-
// (eased: the gait can switch on and off for a frame, e.g. at a landing)
|
|
1701
|
-
this._palpMove = expTo(this._palpMove || 0, this._moving ? 1 : 0, dt, 0.12);
|
|
1702
|
-
const moving = this._palpMove;
|
|
1703
|
-
for (let k = 0; k < 2; k++) {
|
|
1704
|
-
const D = cfg.palpDefs[k], S = this.palpState[k], az = S.az, el = S.el, s = D.side;
|
|
1705
|
-
// idle twitches: random small targets, eased
|
|
1706
|
-
S.twT -= dt;
|
|
1707
|
-
if (S.twT <= 0) {
|
|
1708
|
-
S.twT = 0.4 + this.rand() * 2.2;
|
|
1709
|
-
const amp = 0.25 * cfg.palps.twitch * P.twitch * (1 - moving * 0.6);
|
|
1710
|
-
for (let j = 0; j < 6; j++) S.tgt[j] = (this.rand() - 0.5) * amp;
|
|
1711
|
-
}
|
|
1712
|
-
const kT = 1 - Math.exp(-dt / 0.07), kD = 1 - Math.exp(-dt / 0.6);
|
|
1713
|
-
const work = P.palpWork * Math.sin(t * TAU * 2.2 + k * Math.PI);
|
|
1714
|
-
const gaitSwing = moving * cfg.palps.tap * 0.25 * Math.sin(TAU * (this.phase + k * 0.5));
|
|
1715
|
-
for (let j = 0; j < 6; j++) {
|
|
1716
|
-
S.twitch[j] += (S.tgt[j] - S.twitch[j]) * kT;
|
|
1717
|
-
S.tgt[j] -= S.tgt[j] * kD;
|
|
1718
|
-
az[j] = D.az0[j] - s * 0; el[j] = D.el0[j];
|
|
1719
|
-
}
|
|
1720
|
-
const raise = P.palpRaise, reach = P.palpReach, down = P.palpDown, fold = P.palpFold;
|
|
1721
|
-
const ftw = S.twitch[2] + S.twitch[3];
|
|
1722
|
-
el[2] += 0.85 * raise + 0.2 * reach - 0.35 * down + 0.3 * work + gaitSwing + ftw * 0.6 + 0.5 * fold;
|
|
1723
|
-
el[3] += -0.3 * raise - 0.4 * down - 0.2 * work - 0.9 * fold + S.twitch[4] * 0.5;
|
|
1724
|
-
el[4] += -0.2 * raise + 0.35 * reach - 0.25 * work - 0.7 * fold;
|
|
1725
|
-
el[5] += -0.4 * raise + 0.45 * reach + 0.35 * work - 0.4 * fold + S.twitch[5] * 0.5;
|
|
1726
|
-
for (let j = 1; j < 6; j++) az[j] += (-0.12 * raise - 0.05 * work + 0.1 * S.twitch[1]) ;
|
|
1727
|
-
this.writePalp(D, S);
|
|
1728
|
-
// keep the palp tip above the ground: lift the femur (continuous in the penetration depth)
|
|
1729
|
-
const tip = S.jp[6];
|
|
1730
|
-
let pen = this._palpPen(D, S);
|
|
1731
|
-
for (let it = 0; it < 3 && pen > 0; it++) {
|
|
1732
|
-
// lift the palp femur (and flatten the tarsus) until the tip clears
|
|
1733
|
-
const reachL = D.len[2] + D.len[3] + D.len[4] + D.len[5];
|
|
1734
|
-
el[2] += Math.min(0.9, Math.asin(Math.min(1, pen / reachL)) * 1.4);
|
|
1735
|
-
el[5] += Math.min(0.6, pen / D.len[5]);
|
|
1736
|
-
this.writePalp(D, S);
|
|
1737
|
-
pen = this._palpPen(D, S);
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
// deepest ground penetration of the palp tip and of the patella, tibia and tarsus (ends and middles)
|
|
1742
|
-
_palpPen(D, S) {
|
|
1743
|
-
const tip = S.jp[6];
|
|
1744
|
-
let m = this.terrainH(tip.x, tip.z) + D.tipR - tip.y;
|
|
1745
|
-
for (let j = 3; j <= 5; j++) {
|
|
1746
|
-
const q = S.jp[j], q2 = S.jp[j + 1];
|
|
1747
|
-
m = Math.max(m, this.terrainH(q.x, q.z) + D.tipR * 2.4 - q.y);
|
|
1748
|
-
const mx_ = (q.x + q2.x) * 0.5, my = (q.y + q2.y) * 0.5, mz = (q.z + q2.z) * 0.5;
|
|
1749
|
-
m = Math.max(m, this.terrainH(mx_, mz) + D.tipR * 2.2 - my);
|
|
1750
|
-
}
|
|
1751
|
-
return m;
|
|
1752
|
-
}
|
|
1753
|
-
writePalp(D, S) {
|
|
1754
|
-
const az = S.az, el = S.el, s = D.side, jp = S.jp;
|
|
1755
|
-
const q = this._q, qy = this._q2;
|
|
1756
|
-
const p = tv().copy(D.P0[0]);
|
|
1757
|
-
this.localToWorld(p.x, p.y, p.z, jp[0]);
|
|
1758
|
-
for (let k = 0; k < 6; k++) {
|
|
1759
|
-
qy.setFromAxisAngle(Y_AXIS, s * angDiff(D.az0[k], az[k]));
|
|
1760
|
-
q.setFromAxisAngle(D.n0, el[k] - D.el0[k]);
|
|
1761
|
-
qy.multiply(q);
|
|
1762
|
-
q.copy(this.qB).multiply(qy);
|
|
1763
|
-
this.setBone(D.bones[k], jp[k], q);
|
|
1764
|
-
const ce = Math.cos(el[k]);
|
|
1765
|
-
p.x += D.len[k] * Math.sin(az[k]) * ce * s; p.y += D.len[k] * Math.sin(el[k]); p.z += D.len[k] * Math.cos(az[k]) * ce;
|
|
1766
|
-
this.localToWorld(p.x, p.y, p.z, jp[k + 1]);
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
// ---------------------------------------------------------------- chelicerae and fangs
|
|
1771
|
-
solveChelicerae() {
|
|
1772
|
-
const cfg = this.cfg, P = this.P, C = cfg.chel, t = this.time;
|
|
1773
|
-
const q = this._qc1, qw = this._qc2, qf = this._qc3;
|
|
1774
|
-
for (let k = 0; k < 2; k++) {
|
|
1775
|
-
const ch = cfg.chels[k], s = ch.side;
|
|
1776
|
-
const work = P.chelWork * Math.sin(t * TAU * 2.6 + k * Math.PI);
|
|
1777
|
-
const pitch = C.pitch * (0.35 * P.chelOpen + 0.25 * work) - 0.1 * P.dead;
|
|
1778
|
-
const spread = C.spread * (P.chelOpen + 0.3 * Math.max(0, work)) * s;
|
|
1779
|
-
// chelicera: pitch about the lateral axis (tips forward), spread about the forward axis
|
|
1780
|
-
q.setFromAxisAngle(X_AXIS, -pitch).premultiply(qf.setFromAxisAngle(Z_AXIS, spread));
|
|
1781
|
-
qw.copy(this.qB).multiply(q);
|
|
1782
|
-
const base = this.localToWorld(ch.base.x, ch.base.y, ch.base.z, tv());
|
|
1783
|
-
this.setBone(ch.bc, base, qw);
|
|
1784
|
-
// fang: unfolds about its hinge
|
|
1785
|
-
const tipLocal = tv().subVectors(ch.tip, ch.base).applyQuaternion(q).add(ch.base);
|
|
1786
|
-
const tipW = this.localToWorld(tipLocal.x, tipLocal.y, tipLocal.z, tv());
|
|
1787
|
-
const open = clamp(P.fangOpen + 0.15 * Math.max(0, work) * P.chelWork, 0, 1.1) * C.fangOpen;
|
|
1788
|
-
qw.multiply(qf.setFromAxisAngle(ch.fangAxis, open));
|
|
1789
|
-
this.setBone(ch.bf, tipW, qw);
|
|
1790
|
-
}
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
// ----------------------------------------------------------------- state
|
|
1794
|
-
publish() {
|
|
1795
|
-
const st = this.state, P = this.P;
|
|
1796
|
-
st.heading = this.heading;
|
|
1797
|
-
st.speed = Math.hypot(this.velocity.x, this.velocity.z);
|
|
1798
|
-
st.gait = this.air.active ? 'jump' : st.stats.gait;
|
|
1799
|
-
st.grounded = !this.air.active;
|
|
1800
|
-
st.eyelid = 0;
|
|
1801
|
-
st.action = this.actionsLayer.current();
|
|
1802
|
-
st.posture = this.actionsLayer.posture;
|
|
1803
|
-
for (let i = 0; i < 8; i++) {
|
|
1804
|
-
const leg = this.legs[i], o = st.legs[i];
|
|
1805
|
-
o.stance = leg.state === 'stance' && !this.air.active && P.gaitW > 0.5 && P.raise[i] + P.hold[i] + P.curl + P.curl2 + P.air + P.drawIn + P.tuck + P.dead < 0.004;
|
|
1806
|
-
if (leg.state === 'stance') o.contact.copy(leg.plant); else o.contact.copy(leg.tip);
|
|
1807
|
-
o.contact.y -= leg.def.tipY;
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
}
|