Body step 2.1: climbing, mantling, crawling, legs, playground, 144 Hz
- Step rule + lower-body collider: no walking onto tables, steps <= 0.35 m - Structural hand strength: two-hand mantle/pull-up works, one hand can't - Arms aim from the physical shoulder horizontally, keep True Body pose vertically - Crawl pose, crawl carry, 35 deg slope limit, ground feed-forward ignores play-space corrections (fixes a runaway feedback loop) - Grab static world (climbing), procedural visual legs - Playground sandbox; menu strength slider, spectator (old drag) and turn toggles - Physics 144 Hz with interpolation; legacy chase-body spikes pin 90 Hz - RagdollSpike: 27 phases incl. mantle, pull-up, crawl, ledge drop, ramps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d83273dd2f
commit
2aff2b61da
26 files changed
+1243
-70
No files matched your search
@@ -123,7 +123,9 @@ device only; the editor has an XR simulator for dev. Trackers/action map: see `x
|
|||||||
ruin immersion and are hard to aim at distance; they are the fallback, not the default.
|
ruin immersion and are hard to aim at distance; they are the fallback, not the default.
|
||||||
- **Hand tracking**: cheap spike early, full support later. Interaction logic must not assume
|
- **Hand tracking**: cheap spike early, full support later. Interaction logic must not assume
|
||||||
controllers — controller tip and fingertip feed the same cursor concept.
|
controllers — controller tip and fingertip feed the same cursor concept.
|
||||||
- **Locomotion** (implemented, NEEDS FIXING per owner 2026-10-01 — two-hand turn not
|
- **Locomotion** (SUPERSEDED 2026-10-02 by the ragdoll player's stick locomotion in `PlayerRig`; the grab-drag
|
||||||
|
mode below survives only as the menu's Spectator mode; teleport markers still work and carry the body.
|
||||||
|
Historical notes: implemented, NEEDS FIXING per owner 2026-10-01 — two-hand turn not
|
||||||
2-anchor based, two-hand simultaneous move weird, teleport + lasers broken/experimental;
|
2-anchor based, two-hand simultaneous move weird, teleport + lasers broken/experimental;
|
||||||
fix-or-replace rides on the body-physics track decision, owner candidate replacement =
|
fix-or-replace rides on the body-physics track decision, owner candidate replacement =
|
||||||
physics thumbstick movement):
|
physics thumbstick movement):
|
||||||
@@ -297,8 +299,14 @@ device only; the editor has an XR simulator for dev. Trackers/action map: see `x
|
|||||||
discrete origin jumps carry the body). It REPLACED PhysicsHands/PhysicsTorso/PhysicsArms/BodyRig in
|
discrete origin jumps carry the body). It REPLACED PhysicsHands/PhysicsTorso/PhysicsArms/BodyRig in
|
||||||
`main.tscn` (legacy files kept for their spikes). Record + measured rules:
|
`main.tscn` (legacy files kept for their spikes). Record + measured rules:
|
||||||
`research/ragdoll-player-step2.md` — **never put a light (< ~1 kg) body between two strong motors** (Box3D
|
`research/ragdoll-player-step2.md` — **never put a light (< ~1 kg) body between two strong motors** (Box3D
|
||||||
one-iteration substeps oscillate). Regression `scenes/ragdoll_spike.tscn`. Next: procedural legs, 144 Hz,
|
one-iteration substeps oscillate). Regression `scenes/ragdoll_spike.tscn`. **Step 2.1 (owner playtest fixes):**
|
||||||
climbing. Owner scope: thumbstick locomotion only, no jump, avatar scaling and NPCs later.
|
step rule + lower-body collider (no walking onto tables), structural arm strength (two-hand mantles/pull-ups,
|
||||||
|
one hand can't), crawl pose, slope limit 35°, climbing on any static body (group `no_grab` excluded = the floor),
|
||||||
|
`ProceduralLegs`, `Playground.cs` sandbox, menu strength slider / spectator (old drag movement, no body) / snap-
|
||||||
|
smooth turn, **physics 144 Hz with physics interpolation** (`Main.PhysicsRate`; XROrigin3D, TrueBody, PlayerRig
|
||||||
|
are interpolation-OFF because they move per frame — anything new moved in `_Process` must be excluded too).
|
||||||
|
Rule learned: play-space corrections must never feed ground control's feed-forward (runaway feedback).
|
||||||
|
Owner scope: thumbstick locomotion only, no jump, avatar scaling and NPCs later.
|
||||||
10. **Body physics Stage 2 — PHYSICAL ARMS, implemented 2026-10-02, awaiting owner playtest.**
|
10. **Body physics Stage 2 — PHYSICAL ARMS, implemented 2026-10-02, awaiting owner playtest.**
|
||||||
`PhysicsArms.cs`: per side an upper-arm + forearm RigidBody3D (2.0 / 1.2 kg, layer 5) that
|
`PhysicsArms.cs`: per side an upper-arm + forearm RigidBody3D (2.0 / 1.2 kg, layer 5) that
|
||||||
PD-chase their analytic 2-bone IK pose, solved each physics tick from the PHYSICAL chest
|
PD-chase their analytic 2-bone IK pose, solved each physics tick from the PHYSICAL chest
|
||||||
|
|||||||
+24
-13
@@ -92,7 +92,22 @@ TERMINOLOGY (owner-corrected — use precisely):
|
|||||||
collision exceptions), so grabbing works unchanged. Hand ghosts at > 10 cm lag. Rebuilds on recalibration. Full
|
collision exceptions), so grabbing works unchanged. Hand ghosts at > 10 cm lag. Rebuilds on recalibration. Full
|
||||||
design + measured findings: `research/ragdoll-player-step2.md` (incl. the light-link oscillation rule).
|
design + measured findings: `research/ragdoll-player-step2.md` (incl. the light-link oscillation rule).
|
||||||
Regression `scenes/ragdoll_spike.tscn` "RAGDOLL: overall PASS" (stand, reach, swings, stick + room-scale wall,
|
Regression `scenes/ragdoll_spike.tscn` "RAGDOLL: overall PASS" (stand, reach, swings, stick + room-scale wall,
|
||||||
bar blocked/crouch under, lean over table, crate push, 12 kg close vs far, teleport + snap turn, random soak).
|
bar blocked/crouch under, lean over table, crate push, 12 kg close vs far, teleport + snap turn, random soak,
|
||||||
|
knee-high box + table block walking, 0.25 m step up, two-hand mantle onto 0.9 m vs one hand fails, pull-up on a
|
||||||
|
grabbed bar two vs one hand, crawl through a 0.6 m tunnel, walk off a 1 m ledge, 18° ramp up / 40° ramp stops).
|
||||||
|
**Step 2.1 (owner playtest fixes, same day)** — see `research/ragdoll-player-step2.md` "Step 2.1": step rule
|
||||||
|
(MaxStepHeight 0.35 + back-edge ray) and a lower-body `Legs` collider (blocks knee-high things, tucks when
|
||||||
|
crawling/mantling, shape DISABLED not layer-cleared), STRUCTURAL hand strength (40 → 400 N with the axial share of
|
||||||
|
the error), arm aim from the physical shoulder horizontally / True Body pose vertically, crawl pose in
|
||||||
|
`TrueBodySolver` (trunk tips to 83° keeping the hips ≥ 0.15 m), crawl carry, MaxSlope 35°, `Climbing` (hand on
|
||||||
|
static or arms lifting) = the only state that drags the view vertically, ground feed-forward that ignores play-space
|
||||||
|
corrections (a runaway at 7 m/s otherwise), `ProceduralLegs` visuals, static-world grabs (climbing), menu strength
|
||||||
|
slider / spectator / snap-smooth toggles, 144 Hz + physics interpolation (XR origin, TrueBody, PlayerRig excluded).
|
||||||
|
- **Playground.cs** — the body sandbox around the table (east climbing/mantling, west physics toys, south
|
||||||
|
crawl/duck/door); dynamic pieces are "prop"s reset by the menu's Reset props. `tools/playground_shots.tscn` renders
|
||||||
|
overview PNGs (`godot-mono --path . tools/playground_shots.tscn -- <dir>`).
|
||||||
|
- **AnchorMoveMode.cs** is now SPECTATOR-only (`AnchorMoveMode.Enabled`, menu → Player → Spectator), where the
|
||||||
|
ragdoll is cleared; leaving rebuilds it at the current pose.
|
||||||
- **LEGACY (not in main.tscn since step 2; kept for their spikes): PhysicsHands / PhysicsTorso / PhysicsArms / BodyRig.**
|
- **LEGACY (not in main.tscn since step 2; kept for their spikes): PhysicsHands / PhysicsTorso / PhysicsArms / BodyRig.**
|
||||||
- **PhysicsHands.cs** — physical palm bodies chasing the controller poses: 0.4 kg boxes (flat slab,
|
- **PhysicsHands.cs** — physical palm bodies chasing the controller poses: 0.4 kg boxes (flat slab,
|
||||||
thumb-side face), gravity off, CCD, **layer 4 (player hands)/mask 1|4** — NOT colliding with the
|
thumb-side face), gravity off, CCD, **layer 4 (player hands)/mask 1|4** — NOT colliding with the
|
||||||
@@ -372,11 +387,10 @@ Floor StaticBody mask 1|3|4|5 (collision_mask 29).
|
|||||||
|
|
||||||
## 5. Known issues / open questions
|
## 5. Known issues / open questions
|
||||||
|
|
||||||
- **Ragdoll player (step 2) — unvalidated in-headset.** Known gaps (details in
|
- **Ragdoll player (step 2.1) — owner playtested step 2 ("feels amazing for the most part"); 2.1 fixes are
|
||||||
`research/ragdoll-player-step2.md`): no legs yet; shoulder has no cone limit; grabs are props only (no
|
unvalidated in-headset.** Remaining gaps: shoulder has no cone limit (fork addition); strength values are
|
||||||
climbing on static geometry); strength values are human-scale guesses; physics still at the headset rate
|
human-scale guesses (menu slider); stick-crawling is a comfort concession (Nimso pulls with hands only); legs are
|
||||||
(144 Hz not switched); legacy anchor-grab movement now fights gravity (the body falls, the view follows), so
|
visual only. Legacy spikes (arm/grab/body/collapse) pin 90 Hz because the chase bodies were tuned there.
|
||||||
it only pulls you along the ground; teleport markers still work (the body is carried).
|
|
||||||
|
|
||||||
- **Body Stage 1 chest collapse — ROOT-CAUSED + FIXED (2026-10-01), awaiting owner re-playtest.**
|
- **Body Stage 1 chest collapse — ROOT-CAUSED + FIXED (2026-10-01), awaiting owner re-playtest.**
|
||||||
The first in-headset session NaN'd the chest ("head detaching from the body"). Cause was
|
The first in-headset session NaN'd the chest ("head detaching from the body"). Cause was
|
||||||
@@ -522,13 +536,10 @@ Floor StaticBody mask 1|3|4|5 (collision_mask 29).
|
|||||||
|
|
||||||
## 7. Next steps (in order)
|
## 7. Next steps (in order)
|
||||||
|
|
||||||
0. **Owner playtest of body step 2 (the ragdoll player)** — stand, reach, push props, grab/hold (strength is now
|
0. **Owner playtest of body step 2.1 + the playground** — mantle the ledges (both hands on top, press down, lean
|
||||||
human-scale: heavy things only close to the body), walk with the left stick into walls/the table, snap turn
|
in), climb the wall holds / pull-up bar / monkey bars (grip = grab static), crawl the tunnel, walk ramps/stairs,
|
||||||
with the right, crouch. Read the log after ("PlayerRagdoll: ... snapped" = a safety reset). Tuning knobs:
|
planks/seesaw/swing board, strength slider, spectator mode, smooth turn. Read the log after. Next body work
|
||||||
`PlayerRagdoll` strength/gain constants, `PlayerRig.MoveSpeed/SmoothTurn`. Step 1 (calibration, True Body)
|
after feedback: shoulder cone limit (fork), Nimso flight-phase gait, NPC on the same ragdoll.
|
||||||
was validated by the owner ("seems normal"); `TrueBody.ShowDebug` is off now that the ragdoll is the visual.
|
|
||||||
Next body steps: procedural legs, 144 Hz (needs physics interpolation + origin moves in physics ticks),
|
|
||||||
climbing (weld to static), one global strength scalar.
|
|
||||||
1. **Owner re-playtest of the body (chest fix + Stage 2 arms)** — the Stage-1 collapse is
|
1. **Owner re-playtest of the body (chest fix + Stage 2 arms)** — the Stage-1 collapse is
|
||||||
fixed (`collapse_spike` 49/49); feel is unvalidated in-headset. Read the log after.
|
fixed (`collapse_spike` 49/49); feel is unvalidated in-headset. Read the log after.
|
||||||
2. **Locomotion decision** — owner report 2026-10-01: two-hand anchor turning broken (not
|
2. **Locomotion decision** — owner report 2026-10-01: two-hand anchor turning broken (not
|
||||||
|
|||||||
+2
-1
@@ -26,7 +26,8 @@ project/assembly_name="VR Messs"
|
|||||||
|
|
||||||
[physics]
|
[physics]
|
||||||
|
|
||||||
common/physics_ticks_per_second=90
|
common/physics_ticks_per_second=144
|
||||||
|
common/physics_interpolation=true
|
||||||
common/max_physics_steps_per_frame=14
|
common/max_physics_steps_per_frame=14
|
||||||
3d/physics_engine="Box3D Physics"
|
3d/physics_engine="Box3D Physics"
|
||||||
|
|
||||||
|
|||||||
@@ -63,3 +63,32 @@ body), `scripts/PlayerRig.cs` (XR driver + stick locomotion), regression `scenes
|
|||||||
`AnchorMoveMode`/`Locomotion` move the XR origin in `_Process`, which would jitter under interpolation.
|
`AnchorMoveMode`/`Locomotion` move the XR origin in `_Process`, which would jitter under interpolation.
|
||||||
- **Anchor-grab / teleport legacy locomotion** still drive the origin. Teleports carry the body. Grab-flying upward
|
- **Anchor-grab / teleport legacy locomotion** still drive the origin. Teleports carry the body. Grab-flying upward
|
||||||
now fights gravity (the body falls and the view follows it), so it effectively only pulls you along the ground.
|
now fights gravity (the body falls and the view follows it), so it effectively only pulls you along the ground.
|
||||||
|
|
||||||
|
## Step 2.1 — owner playtest fixes + sandbox (2026-10-02)
|
||||||
|
|
||||||
|
Owner playtest of step 2: "feels amazing for the most part". It found three problems:
|
||||||
|
- You could walk onto the table: the body detached for a second and wobbled before settling.
|
||||||
|
- You couldn't climb onto the table by pushing yourself up.
|
||||||
|
- You couldn't crawl under the table.
|
||||||
|
|
||||||
|
Changes and the measured reasons:
|
||||||
|
|
||||||
|
| Change | Why (RagdollSpike evidence) |
|
||||||
|
|---|---|
|
||||||
|
| **Step rule**: ground more than `MaxStepHeight` 0.35 m above the feet isn't stood on unless the hips got up there: airborne/landing, legs tucked, or hips > 60% of hip height above it, and a second ray 12 cm behind the hips also finds it | The single pelvis ray treated a 0.725 m table under the hips as a "step" → walking onto the table |
|
||||||
|
| **Lower-body collider** (`Legs`, 6 kg box from the hips down to a step above the feet, DRIVE from the pelvis). It slides up when crouching, and its shape is **disabled** (not just layer-cleared) and tucked into the pelvis when crawling or when the arms lift the body (mantle) | Knee-high obstacles and tables now block; steps ≤ 0.35 m pass under it. Box3D kept the old contact when only the collision layer changed, which jammed a crawl against a tunnel roof |
|
||||||
|
| **Structural hand strength**: guide cap = lerp(40 N, 400 N, axial share of the hand error), with a 3 cm deadband | Force along the arm (pressing down on a ledge, hanging, carrying at your side) is bone-strong; across the arm it stays weak. Measured: two-hand mantle onto 0.9 m ✓, one hand ✗ (5 cm). Pull-up on a grabbed bar: two hands lift the pelvis 0.69 m, one hand 0.24 m. 12 kg at arm's length still sags (6.6 cm vs 0.9 cm close) |
|
||||||
|
| **Arm aim from the physical shoulder**, horizontal only: the arm reaches for the real hand when the body is held away horizontally (ledge, lean-over), but keeps its True Body pose vertically | Purely camera-relative arms left the hands 26 cm short when the body was held back at a ledge. Purely physical arms fold to wherever a sagging chest is and stop being pillars, so the crawl collapsed (eye 0.14 m vs 0.42). The split fixes both |
|
||||||
|
| **Ground feed-forward from the GOAL** (pose + ground offset) | When the floor follows up onto a ledge, pose and offset move opposite; differentiating the pose alone launched the body 1 m up |
|
||||||
|
| **Crawl pose** (`TrueBodySolver`): when the hips would go below 0.15 m, the trunk tips toward ~83°, hips kick back; ragdoll hips take the full lean in a crawl | Crouch lean capped at 43° put the hips through the floor, so the head could never get low |
|
||||||
|
| **Crawl carry** 300 N + torso friction 0.3 | With the torso resting on the floor, μ·weight exceeded the support-scaled carry, so stick crawling stalled (Nimso has no stick crawl at all; ours is a comfort concession) |
|
||||||
|
| **Braced air control** 150 N, **AirSlack** 0.05 m | While mantling/hanging, the hips drift after the head; airborne, the view rides the body tightly |
|
||||||
|
| **Climbing grabs**: `PropGrabber` welds the hand to any `StaticBody3D` under the palm (5 cm sphere) except group `no_grab` (the floor) | Pull-ups, ledges, climbing-wall holds, monkey bars |
|
||||||
|
| **Procedural legs** (`ProceduralLegs.cs`): visual only. Planted feet, alternate stepping on 20 cm / 34° error, stride leads velocity by 0.25 s, 0.2–0.32 s steps; airborne they dangle, crawling they trail | Nimso: feet don't hold the body up |
|
||||||
|
| **144 Hz physics + physics interpolation**. `XROrigin3D`, `TrueBody` and `PlayerRig` (ghosts) are excluded; the ragdoll is explicitly interpolated. Legacy chase-body spikes pin 90 Hz | Owner's earlier choice; the old origin-moving code runs per frame, so its subtree must not interpolate |
|
||||||
|
| **Strength slider** (0.25–3×, scales every muscle + hand guides), **Spectator** toggle (no body, the old grab-drag flight), **Turning** snap/smooth toggle | Owner: drag movement "doesn't fit this anymore", keep it as a spectator mode |
|
||||||
|
| **Playground** (`Playground.cs`): mantling ledges 0.3–1.5 m, a 1 m platform with stairs, an 18° ramp and a 40° ramp, a climbing wall, a pull-up bar, monkey bars, a plank bridge, loose planks, a seesaw, a hanging swing board, a crate pyramid, a 25 kg box, barrels, a balance beam, a 0.6 m crawl tunnel, a duck bar, a doorway. "Reset props" resets it | Owner: a Nimso-sandbox-like level |
|
||||||
|
|
||||||
|
Ground steeper than `MaxSlopeDegrees` 35° gives no support, so the 40° ramp can't be walked up; the 18° one can.
|
||||||
|
Still open: the shoulder cone limit (needs a fork addition). `tools/playground_shots.tscn` renders overview PNGs for
|
||||||
|
layout checks.
|
||||||
+11
-4
@@ -1,4 +1,4 @@
|
|||||||
[gd_scene load_steps=20 format=3]
|
[gd_scene load_steps=21 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://scripts/Main.cs" id="1_main"]
|
[ext_resource type="Script" path="res://scripts/Main.cs" id="1_main"]
|
||||||
[ext_resource type="PackedScene" path="res://scenes/teleport_marker.tscn" id="2_marker"]
|
[ext_resource type="PackedScene" path="res://scenes/teleport_marker.tscn" id="2_marker"]
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
[ext_resource type="Script" path="res://scripts/Calibration.cs" id="12_calibration"]
|
[ext_resource type="Script" path="res://scripts/Calibration.cs" id="12_calibration"]
|
||||||
[ext_resource type="Script" path="res://scripts/TrueBody.cs" id="13_truebody"]
|
[ext_resource type="Script" path="res://scripts/TrueBody.cs" id="13_truebody"]
|
||||||
[ext_resource type="Script" path="res://scripts/PlayerRig.cs" id="14_playerrig"]
|
[ext_resource type="Script" path="res://scripts/PlayerRig.cs" id="14_playerrig"]
|
||||||
|
[ext_resource type="Script" path="res://scripts/Playground.cs" id="15_playground"]
|
||||||
|
|
||||||
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_sky"]
|
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_sky"]
|
||||||
sky_horizon_color = Color(0.66, 0.72, 0.81, 1)
|
sky_horizon_color = Color(0.66, 0.72, 0.81, 1)
|
||||||
@@ -27,10 +28,10 @@ albedo_color = Color(0.35, 0.35, 0.38, 1)
|
|||||||
|
|
||||||
[sub_resource type="PlaneMesh" id="PlaneMesh_floor"]
|
[sub_resource type="PlaneMesh" id="PlaneMesh_floor"]
|
||||||
material = SubResource("Material_floor")
|
material = SubResource("Material_floor")
|
||||||
size = Vector2(10, 10)
|
size = Vector2(40, 40)
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape_floor"]
|
[sub_resource type="BoxShape3D" id="BoxShape_floor"]
|
||||||
size = Vector3(10, 0.1, 10)
|
size = Vector3(40, 0.1, 40)
|
||||||
|
|
||||||
[sub_resource type="SphereMesh" id="SphereMesh_hand"]
|
[sub_resource type="SphereMesh" id="SphereMesh_hand"]
|
||||||
radius = 0.04
|
radius = 0.04
|
||||||
@@ -55,7 +56,7 @@ directional_shadow_max_distance = 10.0
|
|||||||
shadow_bias = 0.04
|
shadow_bias = 0.04
|
||||||
shadow_normal_bias = 0.5
|
shadow_normal_bias = 0.5
|
||||||
|
|
||||||
[node name="Floor" type="StaticBody3D" parent="."]
|
[node name="Floor" type="StaticBody3D" parent="." groups=["no_grab"]]
|
||||||
collision_mask = 29
|
collision_mask = 29
|
||||||
|
|
||||||
[node name="FloorShape" type="CollisionShape3D" parent="Floor"]
|
[node name="FloorShape" type="CollisionShape3D" parent="Floor"]
|
||||||
@@ -66,6 +67,7 @@ shape = SubResource("BoxShape_floor")
|
|||||||
mesh = SubResource("PlaneMesh_floor")
|
mesh = SubResource("PlaneMesh_floor")
|
||||||
|
|
||||||
[node name="XROrigin3D" type="XROrigin3D" parent="."]
|
[node name="XROrigin3D" type="XROrigin3D" parent="."]
|
||||||
|
physics_interpolation_mode = 2
|
||||||
|
|
||||||
[node name="XRCamera3D" type="XRCamera3D" parent="XROrigin3D"]
|
[node name="XRCamera3D" type="XRCamera3D" parent="XROrigin3D"]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.7, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.7, 0)
|
||||||
@@ -106,6 +108,9 @@ script = ExtResource("3_locomotion")
|
|||||||
[node name="AnchorMoveMode" type="Node" parent="."]
|
[node name="AnchorMoveMode" type="Node" parent="."]
|
||||||
script = ExtResource("5_anchormove")
|
script = ExtResource("5_anchormove")
|
||||||
|
|
||||||
|
[node name="Playground" type="Node3D" parent="."]
|
||||||
|
script = ExtResource("15_playground")
|
||||||
|
|
||||||
[node name="PropShop" type="Node3D" parent="."]
|
[node name="PropShop" type="Node3D" parent="."]
|
||||||
script = ExtResource("6_propshop")
|
script = ExtResource("6_propshop")
|
||||||
|
|
||||||
@@ -116,9 +121,11 @@ script = ExtResource("7_propgrabber")
|
|||||||
script = ExtResource("12_calibration")
|
script = ExtResource("12_calibration")
|
||||||
|
|
||||||
[node name="TrueBody" type="Node3D" parent="."]
|
[node name="TrueBody" type="Node3D" parent="."]
|
||||||
|
physics_interpolation_mode = 2
|
||||||
script = ExtResource("13_truebody")
|
script = ExtResource("13_truebody")
|
||||||
|
|
||||||
[node name="PlayerRig" type="Node" parent="."]
|
[node name="PlayerRig" type="Node" parent="."]
|
||||||
|
physics_interpolation_mode = 2
|
||||||
script = ExtResource("14_playerrig")
|
script = ExtResource("14_playerrig")
|
||||||
|
|
||||||
[node name="FadeLayer" type="CanvasLayer" parent="."]
|
[node name="FadeLayer" type="CanvasLayer" parent="."]
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
using Godot;
|
using Godot;
|
||||||
|
|
||||||
|
// The old grab-the-air movement. Since the ragdoll player (body step 2) it is only the SPECTATOR
|
||||||
|
// mode (menu → Player → Spectator), where there is no body; off by default. Known issues kept as-is
|
||||||
|
// (owner, 2026-10-02): two-hand turning isn't 2-anchor based, simultaneous two-hand moves act weird.
|
||||||
public partial class AnchorMoveMode : Node
|
public partial class AnchorMoveMode : Node
|
||||||
{
|
{
|
||||||
|
public static bool Enabled;
|
||||||
|
|
||||||
private const float GripThreshold = 0.6f;
|
private const float GripThreshold = 0.6f;
|
||||||
|
|
||||||
private XROrigin3D _origin;
|
private XROrigin3D _origin;
|
||||||
@@ -25,6 +30,13 @@ public partial class AnchorMoveMode : Node
|
|||||||
|
|
||||||
public override void _Process(double delta)
|
public override void _Process(double delta)
|
||||||
{
|
{
|
||||||
|
if (!Enabled)
|
||||||
|
{
|
||||||
|
_wasTwoHand = false;
|
||||||
|
_leftPrev = _left.Position;
|
||||||
|
_rightPrev = _right.Position;
|
||||||
|
return;
|
||||||
|
}
|
||||||
bool leftGrip = _left.GetFloat("grip") > GripThreshold && !PropGrabber.IsGrabbing(_left);
|
bool leftGrip = _left.GetFloat("grip") > GripThreshold && !PropGrabber.IsGrabbing(_left);
|
||||||
bool rightGrip = _right.GetFloat("grip") > GripThreshold && !PropGrabber.IsGrabbing(_right);
|
bool rightGrip = _right.GetFloat("grip") > GripThreshold && !PropGrabber.IsGrabbing(_right);
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ public partial class ArmSpike : Node3D
|
|||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
|
// Legacy chase bodies (PhysicsHands/Torso/Arms) were tuned at 90 Hz; the project runs 144 Hz
|
||||||
|
// since body step 2, so this regression pins their design rate.
|
||||||
|
Engine.PhysicsTicksPerSecond = 90;
|
||||||
StaticBody3D floor = new() { CollisionMask = 0b11101 };
|
StaticBody3D floor = new() { CollisionMask = 0b11101 };
|
||||||
floor.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(20f, 0.1f, 20f) }, Position = new Vector3(0f, -0.05f, 0f) });
|
floor.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(20f, 0.1f, 20f) }, Position = new Vector3(0f, -0.05f, 0f) });
|
||||||
AddChild(floor);
|
AddChild(floor);
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ public partial class BodySpike : Node3D
|
|||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
|
// Legacy chase bodies (PhysicsHands/Torso/Arms) were tuned at 90 Hz; the project runs 144 Hz
|
||||||
|
// since body step 2, so this regression pins their design rate.
|
||||||
|
Engine.PhysicsTicksPerSecond = 90;
|
||||||
BuildWorld();
|
BuildWorld();
|
||||||
_head = new Node3D { Position = new Vector3(0f, 1.6f, 0f) };
|
_head = new Node3D { Position = new Vector3(0f, 1.6f, 0f) };
|
||||||
AddChild(_head);
|
AddChild(_head);
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ public partial class CollapseSpike : Node3D
|
|||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
|
// Legacy chase bodies (PhysicsHands/Torso/Arms) were tuned at 90 Hz; the project runs 144 Hz
|
||||||
|
// since body step 2, so this regression pins their design rate.
|
||||||
|
Engine.PhysicsTicksPerSecond = 90;
|
||||||
// A real profile is measured standing once (T-pose) and does not shrink when seated:
|
// A real profile is measured standing once (T-pose) and does not shrink when seated:
|
||||||
// every scenario, seated or not, uses the same standing 1.65 m-eye profile.
|
// every scenario, seated or not, uses the same standing 1.65 m-eye profile.
|
||||||
BodyProfile.Current = BodyProfile.Estimate(1.65f);
|
BodyProfile.Current = BodyProfile.Estimate(1.65f);
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ public partial class GrabSpike : Node3D
|
|||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
|
// Legacy chase bodies (PhysicsHands/Torso/Arms) were tuned at 90 Hz; the project runs 144 Hz
|
||||||
|
// since body step 2, so this regression pins their design rate.
|
||||||
|
Engine.PhysicsTicksPerSecond = 90;
|
||||||
StaticBody3D floor = new() { Position = new Vector3(0, -0.5f, 0) };
|
StaticBody3D floor = new() { Position = new Vector3(0, -0.5f, 0) };
|
||||||
floor.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(20, 1, 20) } });
|
floor.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(20, 1, 20) } });
|
||||||
AddChild(floor);
|
AddChild(floor);
|
||||||
|
|||||||
+6
-1
@@ -14,6 +14,8 @@ public partial class Main : Node3D
|
|||||||
[Export]
|
[Export]
|
||||||
public int MaximumRefreshRate { get; set; } = 90;
|
public int MaximumRefreshRate { get; set; } = 90;
|
||||||
|
|
||||||
|
public const int PhysicsRate = 144;
|
||||||
|
|
||||||
private OpenXRInterface _xrInterface;
|
private OpenXRInterface _xrInterface;
|
||||||
private bool _xrIsFocused;
|
private bool _xrIsFocused;
|
||||||
|
|
||||||
@@ -70,7 +72,10 @@ public partial class Main : Node3D
|
|||||||
currentRefreshRate = newRate;
|
currentRefreshRate = newRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
Engine.PhysicsTicksPerSecond = (int)currentRefreshRate;
|
// Physics runs at a fixed PhysicsRate (the ragdoll's joints like small steps), decoupled from
|
||||||
|
// the display; physics interpolation smooths rendering (XR origin subtree excluded — the
|
||||||
|
// runtime and locomotion place it every frame).
|
||||||
|
Engine.PhysicsTicksPerSecond = PhysicsRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSessionVisible()
|
private void OnSessionVisible()
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ public partial class MainMenu : PanelContainer
|
|||||||
public event Action PingRequested;
|
public event Action PingRequested;
|
||||||
public event Action CloseRequested;
|
public event Action CloseRequested;
|
||||||
public event Action Clicked;
|
public event Action Clicked;
|
||||||
|
public event Action<bool> SpectatorToggled;
|
||||||
|
public event Action<bool> SmoothTurnToggled;
|
||||||
|
public event Action<float> StrengthChanged;
|
||||||
|
|
||||||
public TabContainer Tabs { get; private set; }
|
public TabContainer Tabs { get; private set; }
|
||||||
public Button ReturnToSpawnButton { get; private set; }
|
public Button ReturnToSpawnButton { get; private set; }
|
||||||
@@ -27,6 +30,9 @@ public partial class MainMenu : PanelContainer
|
|||||||
public Button CloseButton { get; private set; }
|
public Button CloseButton { get; private set; }
|
||||||
public Button HeaderCloseButton { get; private set; }
|
public Button HeaderCloseButton { get; private set; }
|
||||||
public HSlider ClickVolumeSlider { get; private set; }
|
public HSlider ClickVolumeSlider { get; private set; }
|
||||||
|
public HSlider StrengthSlider { get; private set; }
|
||||||
|
public Button SpectatorButton { get; private set; }
|
||||||
|
public Button TurnModeButton { get; private set; }
|
||||||
public ScrollContainer LogScroll { get; private set; }
|
public ScrollContainer LogScroll { get; private set; }
|
||||||
|
|
||||||
private Label _fpsLabel;
|
private Label _fpsLabel;
|
||||||
@@ -116,6 +122,39 @@ public partial class MainMenu : PanelContainer
|
|||||||
_calibrationStatus.AddThemeColorOverride("font_color", new Color(0.6f, 0.6f, 0.65f));
|
_calibrationStatus.AddThemeColorOverride("font_color", new Color(0.6f, 0.6f, 0.65f));
|
||||||
calibRow.AddChild(_calibrationStatus);
|
calibRow.AddChild(_calibrationStatus);
|
||||||
player.AddChild(calibRow);
|
player.AddChild(calibRow);
|
||||||
|
|
||||||
|
// Nimso's single strength value: every muscle and the hand guides scale with it.
|
||||||
|
HBoxContainer strengthRow = new();
|
||||||
|
strengthRow.AddThemeConstantOverride("separation", 20);
|
||||||
|
Label strengthLabel = new() { Text = "Strength 1.00×", VerticalAlignment = VerticalAlignment.Center, CustomMinimumSize = new Vector2(220, 0) };
|
||||||
|
strengthRow.AddChild(strengthLabel);
|
||||||
|
StrengthSlider = new HSlider
|
||||||
|
{
|
||||||
|
MinValue = 0.25,
|
||||||
|
MaxValue = 3,
|
||||||
|
Step = 0.05,
|
||||||
|
Value = 1,
|
||||||
|
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||||
|
SizeFlagsVertical = SizeFlags.ShrinkCenter,
|
||||||
|
CustomMinimumSize = new Vector2(0, 48),
|
||||||
|
FocusMode = FocusModeEnum.None,
|
||||||
|
};
|
||||||
|
StrengthSlider.ValueChanged += v =>
|
||||||
|
{
|
||||||
|
strengthLabel.Text = $"Strength {v:0.00}×";
|
||||||
|
StrengthChanged?.Invoke((float)v);
|
||||||
|
};
|
||||||
|
strengthRow.AddChild(StrengthSlider);
|
||||||
|
player.AddChild(strengthRow);
|
||||||
|
|
||||||
|
HBoxContainer modeRow = new();
|
||||||
|
modeRow.AddThemeConstantOverride("separation", 16);
|
||||||
|
TurnModeButton = MakeToggle("Turning: snap", "Turning: smooth", on => SmoothTurnToggled?.Invoke(on));
|
||||||
|
modeRow.AddChild(TurnModeButton);
|
||||||
|
// Spectator: no body; fly around by grabbing the air (the old drag movement).
|
||||||
|
SpectatorButton = MakeToggle("Spectator: off", "Spectator: on (grab-fly)", on => SpectatorToggled?.Invoke(on));
|
||||||
|
modeRow.AddChild(SpectatorButton);
|
||||||
|
player.AddChild(modeRow);
|
||||||
Tabs.AddChild(player);
|
Tabs.AddChild(player);
|
||||||
|
|
||||||
// World
|
// World
|
||||||
@@ -180,6 +219,25 @@ public partial class MainMenu : PanelContainer
|
|||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Button MakeToggle(string offText, string onText, Action<bool> changed)
|
||||||
|
{
|
||||||
|
Button button = new()
|
||||||
|
{
|
||||||
|
Text = offText,
|
||||||
|
ToggleMode = true,
|
||||||
|
CustomMinimumSize = new Vector2(0, ButtonHeight),
|
||||||
|
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||||
|
FocusMode = FocusModeEnum.None,
|
||||||
|
};
|
||||||
|
button.Toggled += on =>
|
||||||
|
{
|
||||||
|
button.Text = on ? onText : offText;
|
||||||
|
Clicked?.Invoke();
|
||||||
|
Callable.From(() => changed(on)).CallDeferred();
|
||||||
|
};
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
private Button MakeButton(string text, Action action)
|
private Button MakeButton(string text, Action action)
|
||||||
{
|
{
|
||||||
Button button = new()
|
Button button = new()
|
||||||
|
|||||||
@@ -302,6 +302,20 @@ public partial class MenuSpike : Node3D
|
|||||||
await TouchPress(1, Center(_menu.RecalibrateButton));
|
await TouchPress(1, Center(_menu.RecalibrateButton));
|
||||||
Check("touch: Recalibrate called once", stub.Calls == 1, $"(calls={stub.Calls})");
|
Check("touch: Recalibrate called once", stub.Calls == 1, $"(calls={stub.Calls})");
|
||||||
|
|
||||||
|
// 7b) Player-page toggles: spectator and smooth turn fire with the new state.
|
||||||
|
bool? spectator = null, smooth = null;
|
||||||
|
_menu.SpectatorToggled += on => spectator = on;
|
||||||
|
_menu.SmoothTurnToggled += on => smooth = on;
|
||||||
|
await TouchPress(1, TabCenter(0));
|
||||||
|
await TouchPress(1, Center(_menu.SpectatorButton));
|
||||||
|
await TouchPress(1, Center(_menu.TurnModeButton));
|
||||||
|
await Frame(3);
|
||||||
|
Check("touch: spectator toggled on", spectator == true, $"({spectator})");
|
||||||
|
Check("touch: smooth turn toggled on", smooth == true, $"({smooth})");
|
||||||
|
await TouchPress(1, Center(_menu.SpectatorButton));
|
||||||
|
await Frame(3);
|
||||||
|
Check("touch: spectator toggled back off", spectator == false, $"({spectator})");
|
||||||
|
|
||||||
// 8) Header Close via touch.
|
// 8) Header Close via touch.
|
||||||
await TouchPress(1, Center(_menu.HeaderCloseButton));
|
await TouchPress(1, Center(_menu.HeaderCloseButton));
|
||||||
Check("touch: Close fired once", _close == 1, $"(count={_close})");
|
Check("touch: Close fired once", _close == 1, $"(count={_close})");
|
||||||
|
|||||||
+245
-21
@@ -33,6 +33,14 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
// Hand guide (chest→hand drive): pulls the hand onto the controller; capped so the arm,
|
// Hand guide (chest→hand drive): pulls the hand onto the controller; capped so the arm,
|
||||||
// not the guide, carries real loads.
|
// not the guide, carries real loads.
|
||||||
public const float HandGuideKp = 40f, HandGuideForce = 40f, HandGuideMaxSpeed = 10f;
|
public const float HandGuideKp = 40f, HandGuideForce = 40f, HandGuideMaxSpeed = 10f;
|
||||||
|
// STRUCTURAL strength: force along the arm (straight arm pressing on a ledge, hanging from a
|
||||||
|
// bar, carrying a bucket at your side) goes through bone and is strong; force across the arm
|
||||||
|
// needs muscle torque and stays at HandGuideForce. Cap = lerp(base, structural, alignment).
|
||||||
|
// Two arms ≈ 1.2 × body weight → two hands mantle/pull up, one can't (Nimso).
|
||||||
|
public const float HandStructuralForce = 400f;
|
||||||
|
public const float StructuralDeadband = 0.03f;
|
||||||
|
// Nimso's one global strength value: scales every muscle and the hand guides (menu slider).
|
||||||
|
public static float Strength = 1f;
|
||||||
public const float HandGuideAngularKp = 40f, HandGuideTorque = 25f;
|
public const float HandGuideAngularKp = 40f, HandGuideTorque = 25f;
|
||||||
// Ground control (world-space support on the pelvis, only while grounded).
|
// Ground control (world-space support on the pelvis, only while grounded).
|
||||||
public const float GroundFrequency = 4f; // Hz
|
public const float GroundFrequency = 4f; // Hz
|
||||||
@@ -40,6 +48,18 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
public const float LegStrength = 2f; // max support, × body weight
|
public const float LegStrength = 2f; // max support, × body weight
|
||||||
public const float GroundFriction = 1f; // horizontal force ≤ this × support
|
public const float GroundFriction = 1f; // horizontal force ≤ this × support
|
||||||
public const float GroundProbeExtra = 0.3f; // ground counts while within hip height + this
|
public const float GroundProbeExtra = 0.3f; // ground counts while within hip height + this
|
||||||
|
public const float MaxStepHeight = 0.35f;
|
||||||
|
public const float MaxSlopeDegrees = 35f; // steeper ground gives no support (Nimso: "slopes beyond an angle can't be run up") // higher ground under the hips isn't stood on unless the body got up there
|
||||||
|
public const float BracedAirControl = 150f; // N of horizontal hip carry while airborne but braced on the hands (mantling)
|
||||||
|
public const float CrawlCarry = 300f; // N: the torso lies on the floor in a crawl; knees/hands would push (not simulated)
|
||||||
|
public const float TorsoFriction = 0.3f; // clothing on floor: lets a crawl slide
|
||||||
|
// Lower-body collider: a box from the hips down to MaxStepHeight above the feet, so knee-high
|
||||||
|
// obstacles block walking (no walking over tables) while lower steps pass under it. It slides
|
||||||
|
// up into the torso when crouching and switches off when crawling (Nimso: legs don't snag).
|
||||||
|
public const float LegsMass = 6f;
|
||||||
|
public const float LegsKp = 30f, LegsForce = 3000f, LegsTorque = 400f;
|
||||||
|
public const float LegsOffAbove = 0.25f; // collider disabled once slid this far above the hips
|
||||||
|
public const float MantleLift = 0.08f; // braced + hips this far above the support goal = being lifted (legs tuck)
|
||||||
// Balance: an implicit torque-capped motor from a world anchor to the pelvis (explicit torques
|
// Balance: an implicit torque-capped motor from a world anchor to the pelvis (explicit torques
|
||||||
// let the arms' reaction torques set the whole trunk swinging).
|
// let the arms' reaction torques set the whole trunk swinging).
|
||||||
public const float BalanceKp = 10f;
|
public const float BalanceKp = 10f;
|
||||||
@@ -48,6 +68,8 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
// Play-space reconciliation.
|
// Play-space reconciliation.
|
||||||
public const float CameraRadius = 0.03f; // sphere swept from the head's eye to the camera
|
public const float CameraRadius = 0.03f; // sphere swept from the head's eye to the camera
|
||||||
public const float FreeSlack = 0.2f; // camera may lead an unobstructed (lagging/held) head by this much
|
public const float FreeSlack = 0.2f; // camera may lead an unobstructed (lagging/held) head by this much
|
||||||
|
public const float AirSlack = 0.05f; // climbing/hanging (braced): the view rides the body tightly
|
||||||
|
public const float FallSlack = 0.25f; // free airborne: brief ground loss (sliding off a steep slope) mustn't sink the view
|
||||||
public const float GroundFollowRate = 8f;
|
public const float GroundFollowRate = 8f;
|
||||||
public const float ResetDistance = 1.5f; // pelvis this far from its target → snap the whole body to the pose
|
public const float ResetDistance = 1.5f; // pelvis this far from its target → snap the whole body to the pose
|
||||||
// Geometry.
|
// Geometry.
|
||||||
@@ -92,8 +114,9 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
public HingeJoint3D Elbow;
|
public HingeJoint3D Elbow;
|
||||||
public Drive Shoulder, Guide;
|
public Drive Shoulder, Guide;
|
||||||
public float UpperLen, ForeLen;
|
public float UpperLen, ForeLen;
|
||||||
public RigidBody3D Held;
|
|
||||||
public float PrevFlexTarget = -1f;
|
public float PrevFlexTarget = -1f;
|
||||||
|
public float GuideCap = HandGuideForce;
|
||||||
|
public PhysicsBody3D Held;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RigidBody3D Pelvis { get; private set; }
|
public RigidBody3D Pelvis { get; private set; }
|
||||||
@@ -101,7 +124,11 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
public RigidBody3D Head { get; private set; }
|
public RigidBody3D Head { get; private set; }
|
||||||
public bool Built { get; private set; }
|
public bool Built { get; private set; }
|
||||||
public bool Grounded { get; private set; }
|
public bool Grounded { get; private set; }
|
||||||
public float GroundY { get; private set; }
|
public float GroundY => _footY;
|
||||||
|
public bool Braced { get; private set; } // a hand touches or holds static world
|
||||||
|
// Really climbing: a hand holds static world, or the arms are lifting the body (mantle).
|
||||||
|
public bool Climbing => Built && (_arms[0].Held is StaticBody3D || _arms[1].Held is StaticBody3D || _lifted);
|
||||||
|
public RigidBody3D Legs { get; private set; }
|
||||||
public Vector3 Eye => Head.GlobalTransform * _eyeLocal;
|
public Vector3 Eye => Head.GlobalTransform * _eyeLocal;
|
||||||
public BodyProfile Profile { get; private set; }
|
public BodyProfile Profile { get; private set; }
|
||||||
public int Snaps { get; private set; } // safety resets (NaN / too far) — should stay 0
|
public int Snaps { get; private set; } // safety resets (NaN / too far) — should stay 0
|
||||||
@@ -115,6 +142,14 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
private Vector3 _pelvisCenterLocal, _chestCenterLocal, _headCenterLocal; // from each body's proximal joint, in its basis
|
private Vector3 _pelvisCenterLocal, _chestCenterLocal, _headCenterLocal; // from each body's proximal joint, in its basis
|
||||||
private readonly Dictionary<RigidBody3D, Transform3D> _targets = new();
|
private readonly Dictionary<RigidBody3D, Transform3D> _targets = new();
|
||||||
private readonly Vector3[] _girdle = new Vector3[2]; // True Body shoulder offset, chest frame
|
private readonly Vector3[] _girdle = new Vector3[2]; // True Body shoulder offset, chest frame
|
||||||
|
private float _footY;
|
||||||
|
private float _supportGoalY;
|
||||||
|
private bool _lifted;
|
||||||
|
private float _legsHeight;
|
||||||
|
private Drive _legsDrive;
|
||||||
|
private bool _legsSolid = true;
|
||||||
|
private ProceduralLegs _legVisuals;
|
||||||
|
private float _appliedStrength = 1f;
|
||||||
private Vector3 _pelvisTargetPrev;
|
private Vector3 _pelvisTargetPrev;
|
||||||
private bool _havePelvisPrev;
|
private bool _havePelvisPrev;
|
||||||
|
|
||||||
@@ -157,14 +192,21 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
_eyeLocal -= headCenterFromPivot; // eye relative to the head body's centre
|
_eyeLocal -= headCenterFromPivot; // eye relative to the head body's centre
|
||||||
|
|
||||||
Color skin = new(0.85f, 0.72f, 0.6f), shirt = new(0.3f, 0.45f, 0.7f), pants = new(0.25f, 0.27f, 0.32f);
|
Color skin = new(0.85f, 0.72f, 0.6f), shirt = new(0.3f, 0.45f, 0.7f), pants = new(0.25f, 0.27f, 0.32f);
|
||||||
Pelvis = MakeBody("Pelvis", new BoxShape3D { Size = pelvisSize }, (0.142f + 0.139f + 0.322f) * m, BodyLayer, WorldMask, pants,
|
Pelvis = MakeBody("Pelvis", new BoxShape3D { Size = pelvisSize }, (0.142f + 0.139f + 0.322f) * m - LegsMass, BodyLayer, WorldMask, pants,
|
||||||
c * new Transform3D(Basis.Identity, hip + _pelvisCenterLocal));
|
c * new Transform3D(Basis.Identity, hip + _pelvisCenterLocal));
|
||||||
|
float standingHip = pose.PelvisTarget.Y - (pose.Eye.Y - p.EyeHeight);
|
||||||
|
_legsHeight = Mathf.Max(0.2f, standingHip - MaxStepHeight);
|
||||||
|
Legs = MakeBody("Legs", new BoxShape3D { Size = new Vector3(0.28f * s, _legsHeight, 0.2f * s) }, LegsMass, BodyLayer, WorldMask, pants,
|
||||||
|
c * new Transform3D(Basis.Identity, hip - new Vector3(0f, _legsHeight * 0.5f, 0f)), visible: false);
|
||||||
|
_footY = pose.Eye.Y - p.EyeHeight;
|
||||||
Chest = MakeBody("Chest", new BoxShape3D { Size = chestSize }, 0.216f * m, BodyLayer, WorldMask, shirt,
|
Chest = MakeBody("Chest", new BoxShape3D { Size = chestSize }, 0.216f * m, BodyLayer, WorldMask, shirt,
|
||||||
c * new Transform3D(Basis.Identity, lumbar + _chestCenterLocal));
|
c * new Transform3D(Basis.Identity, lumbar + _chestCenterLocal));
|
||||||
Head = MakeBody("Head", new BoxShape3D { Size = HeadSize * s }, 0.081f * m, BodyLayer, WorldMask, skin,
|
Head = MakeBody("Head", new BoxShape3D { Size = HeadSize * s }, 0.081f * m, BodyLayer, WorldMask, skin,
|
||||||
c * new Transform3D(Basis.Identity, neckPivot + headCenterFromPivot));
|
c * new Transform3D(Basis.Identity, neckPivot + headCenterFromPivot));
|
||||||
// The neck: a box hanging under the head, same body.
|
// The neck: a box hanging under the head, same body.
|
||||||
Vector3 neckCenter = (neckBase + neckPivot) * 0.5f - (neckPivot + headCenterFromPivot);
|
Vector3 neckCenter = (neckBase + neckPivot) * 0.5f - (neckPivot + headCenterFromPivot);
|
||||||
|
foreach (RigidBody3D torso in new[] { Pelvis, Chest, Head, Legs })
|
||||||
|
torso.PhysicsMaterialOverride = new PhysicsMaterial { Friction = TorsoFriction };
|
||||||
Head.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(0.09f * s, _neckLen, 0.09f * s) }, Position = neckCenter });
|
Head.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(0.09f * s, _neckLen, 0.09f * s) }, Position = neckCenter });
|
||||||
MeshInstance3D neckMesh = new() { Mesh = new BoxMesh { Size = new Vector3(0.09f * s, _neckLen, 0.09f * s) }, Position = neckCenter };
|
MeshInstance3D neckMesh = new() { Mesh = new BoxMesh { Size = new Vector3(0.09f * s, _neckLen, 0.09f * s) }, Position = neckCenter };
|
||||||
neckMesh.SetSurfaceOverrideMaterial(0, new StandardMaterial3D { AlbedoColor = skin, Roughness = 0.6f });
|
neckMesh.SetSurfaceOverrideMaterial(0, new StandardMaterial3D { AlbedoColor = skin, Roughness = 0.6f });
|
||||||
@@ -175,6 +217,10 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
StaticBody3D anchor = new() { Name = "BalanceAnchor", CollisionLayer = 0, CollisionMask = 0 };
|
StaticBody3D anchor = new() { Name = "BalanceAnchor", CollisionLayer = 0, CollisionMask = 0 };
|
||||||
AddChild(anchor);
|
AddChild(anchor);
|
||||||
_balance = MakeDrive(anchor, Pelvis, Pelvis.GlobalTransform, 0f, BalanceTorque);
|
_balance = MakeDrive(anchor, Pelvis, Pelvis.GlobalTransform, 0f, BalanceTorque);
|
||||||
|
_legsDrive = MakeDrive(Pelvis, Legs, c * new Transform3D(Basis.Identity, hip), LegsForce, LegsTorque);
|
||||||
|
_legVisuals = new ProceduralLegs { Name = "LegVisuals" };
|
||||||
|
AddChild(_legVisuals);
|
||||||
|
_legVisuals.Setup(Pelvis, -_pelvisCenterLocal, p.Stature, pants, new Color(0.15f, 0.15f, 0.17f));
|
||||||
_balanceCap = BalanceTorque;
|
_balanceCap = BalanceTorque;
|
||||||
|
|
||||||
_spine = MakeBall(Pelvis, Chest, c * lumbar, c.Basis * TwistAlong(Vector3.Up), Mathf.DegToRad(60f), Mathf.DegToRad(35f), SpineKp, SpineTorque);
|
_spine = MakeBall(Pelvis, Chest, c * lumbar, c.Basis * TwistAlong(Vector3.Up), Mathf.DegToRad(60f), Mathf.DegToRad(35f), SpineKp, SpineTorque);
|
||||||
@@ -217,6 +263,9 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
|
|
||||||
Built = true;
|
Built = true;
|
||||||
_havePelvisPrev = false;
|
_havePelvisPrev = false;
|
||||||
|
_appliedStrength = 1f;
|
||||||
|
_legsSolid = true;
|
||||||
|
ApplyStrength();
|
||||||
SnapToPose(pose, left, right);
|
SnapToPose(pose, left, right);
|
||||||
GD.Print($"PlayerRagdoll: built for stature {p.Stature:F2} m (arm {p.UpperArm:F3}+{p.Forearm:F3} m, {_bodies.Count} bodies)");
|
GD.Print($"PlayerRagdoll: built for stature {p.Stature:F2} m (arm {p.UpperArm:F3}+{p.Forearm:F3} m, {_bodies.Count} bodies)");
|
||||||
}
|
}
|
||||||
@@ -249,11 +298,11 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
return new Basis(x, boneDir, x.Cross(boneDir));
|
return new Basis(x, boneDir, x.Cross(boneDir));
|
||||||
}
|
}
|
||||||
|
|
||||||
private RigidBody3D MakeBody(string name, Shape3D shape, float mass, uint layer, uint mask, Color color, Transform3D at)
|
private RigidBody3D MakeBody(string name, Shape3D shape, float mass, uint layer, uint mask, Color color, Transform3D at, bool visible = true)
|
||||||
{
|
{
|
||||||
RigidBody3D body = new() { Name = name, Mass = mass, CollisionLayer = layer, CollisionMask = mask, Transform = at };
|
RigidBody3D body = new() { Name = name, Mass = mass, CollisionLayer = layer, CollisionMask = mask, Transform = at, ContactMonitor = true, MaxContactsReported = 4 };
|
||||||
body.AddChild(new CollisionShape3D { Shape = shape });
|
body.AddChild(new CollisionShape3D { Shape = shape });
|
||||||
if (shape is BoxShape3D box && layer != 0)
|
if (shape is BoxShape3D box && layer != 0 && visible)
|
||||||
{
|
{
|
||||||
MeshInstance3D mesh = new() { Mesh = new BoxMesh { Size = box.Size } };
|
MeshInstance3D mesh = new() { Mesh = new BoxMesh { Size = box.Size } };
|
||||||
mesh.SetSurfaceOverrideMaterial(0, new StandardMaterial3D { AlbedoColor = color, Roughness = 0.6f });
|
mesh.SetSurfaceOverrideMaterial(0, new StandardMaterial3D { AlbedoColor = color, Roughness = 0.6f });
|
||||||
@@ -337,14 +386,17 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
// World transform every body should have in the True Body pose. The trunk is rebuilt top-down
|
// World transform every body should have in the True Body pose. The trunk is rebuilt top-down
|
||||||
// through the physical chain (upper chest → lumbar → hips) so a leaning chest doesn't drag
|
// through the physical chain (upper chest → lumbar → hips) so a leaning chest doesn't drag
|
||||||
// the head off the camera; the hips take part of the crouch lean (hip hinge).
|
// the head off the camera; the hips take part of the crouch lean (hip hinge).
|
||||||
private void ComputeTargets(TrueBodySolver s, Transform3D left, Transform3D right)
|
private void ComputeTargets(TrueBodySolver s, Transform3D left, Transform3D right, bool physicalArms = false, float dt = 0f)
|
||||||
{
|
{
|
||||||
Basis chest = s.ChestBasis;
|
Basis chest = s.ChestBasis;
|
||||||
Basis pelvis = s.YawBasis * new Basis(Vector3.Right, -s.Lean * PelvisLeanShare);
|
// Hips take half of a crouch lean, all of it in a crawl (keeps them off the floor).
|
||||||
|
float share = Mathf.Lerp(PelvisLeanShare, 1f, Mathf.Clamp((s.Lean - TrueBodySolver.ChestLeanMax) / 0.5f, 0f, 1f));
|
||||||
|
Basis pelvis = s.YawBasis * new Basis(Vector3.Right, -s.Lean * share);
|
||||||
Vector3 lumbar = s.UpperChest - chest.Y * (_trunk - _lumbar);
|
Vector3 lumbar = s.UpperChest - chest.Y * (_trunk - _lumbar);
|
||||||
Vector3 hip = lumbar - pelvis.Y * _lumbar;
|
Vector3 hip = lumbar - pelvis.Y * _lumbar;
|
||||||
_targets[Pelvis] = new Transform3D(pelvis, hip + pelvis * _pelvisCenterLocal);
|
_targets[Pelvis] = new Transform3D(pelvis, hip + pelvis * _pelvisCenterLocal);
|
||||||
_targets[Chest] = new Transform3D(chest, lumbar + chest * _chestCenterLocal);
|
_targets[Chest] = new Transform3D(chest, lumbar + chest * _chestCenterLocal);
|
||||||
|
_targets[Legs] = new Transform3D(s.YawBasis, hip - Vector3.Up * (_legsHeight * 0.5f)); // snap pose only; LegsDrive steps it
|
||||||
// Head + neck pivot at the neck base: orient like the camera, tilted by the small extra
|
// Head + neck pivot at the neck base: orient like the camera, tilted by the small extra
|
||||||
// rotation that puts the body's eye on the camera (the True Body bends the neck at two points).
|
// rotation that puts the body's eye on the camera (the True Body bends the neck at two points).
|
||||||
Vector3 eyeDir = s.HeadBasis * _neckBaseToEye;
|
Vector3 eyeDir = s.HeadBasis * _neckBaseToEye;
|
||||||
@@ -368,9 +420,33 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
_targets[a.Upper] = new Transform3D(BoneBasis(up, hinge), sh + up * (a.UpperLen * 0.5f));
|
_targets[a.Upper] = new Transform3D(BoneBasis(up, hinge), sh + up * (a.UpperLen * 0.5f));
|
||||||
_targets[a.Fore] = new Transform3D(BoneBasis(fore, hinge), el + fore * (a.ForeLen * 0.5f));
|
_targets[a.Fore] = new Transform3D(BoneBasis(fore, hinge), el + fore * (a.ForeLen * 0.5f));
|
||||||
_targets[a.Hand] = (side == 0 ? left : right).Orthonormalized();
|
_targets[a.Hand] = (side == 0 ? left : right).Orthonormalized();
|
||||||
|
|
||||||
|
if (physicalArms)
|
||||||
|
{
|
||||||
|
// Re-aim the arm from the PHYSICAL shoulder. Horizontally the body may be held away from
|
||||||
|
// the camera (pressed against a ledge, leaning over a table): the arm reaches for the
|
||||||
|
// real hand. Vertically a sagging body keeps its arm pose (the aim point moves with
|
||||||
|
// the shoulder), so straight real arms stay straight virtual arms — pillars that hold
|
||||||
|
// you up when crawling or press you up a ledge (the hand guide adds the rest).
|
||||||
|
Transform3D chestPhys = Chest.GlobalTransform.Orthonormalized();
|
||||||
|
Vector3 shPhys = chestPhys * (a.Shoulder.AnchorA + _girdle[side]);
|
||||||
|
Transform3D ctrl = _targets[a.Hand];
|
||||||
|
Vector3 aim = ctrl * new Vector3(0f, 0f, WristOffset) + Vector3.Up * (shPhys.Y - sh.Y);
|
||||||
|
ArmSolver solver = _armSolvers[side];
|
||||||
|
solver.Solve(shPhys, aim, chestPhys.Basis, chestPhys.Basis.X * (side == 0 ? -1f : 1f),
|
||||||
|
ctrl.Basis, a.UpperLen, a.ForeLen, dt);
|
||||||
|
Vector3 d = solver.Wrist - shPhys;
|
||||||
|
Vector3 h = d.LengthSquared() > 1e-8f ? solver.Pole.Cross(d.Normalized()) : chestPhys.Basis.X;
|
||||||
|
h = h.LengthSquared() > 1e-8f ? h.Normalized() : chestPhys.Basis.X;
|
||||||
|
Vector3 u = (solver.Elbow - shPhys).Normalized(), f = (solver.Wrist - solver.Elbow).Normalized();
|
||||||
|
_targets[a.Upper] = new Transform3D(BoneBasis(u, h), shPhys + u * (a.UpperLen * 0.5f));
|
||||||
|
_targets[a.Fore] = new Transform3D(BoneBasis(f, h), solver.Elbow + f * (a.ForeLen * 0.5f));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly ArmSolver[] _armSolvers = { new(), new() };
|
||||||
|
|
||||||
public Transform3D Target(RigidBody3D body) => _targets.TryGetValue(body, out Transform3D t) ? t : body.GlobalTransform;
|
public Transform3D Target(RigidBody3D body) => _targets.TryGetValue(body, out Transform3D t) ? t : body.GlobalTransform;
|
||||||
|
|
||||||
// ---------------------------------------------------------------- per tick
|
// ---------------------------------------------------------------- per tick
|
||||||
@@ -379,7 +455,7 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
{
|
{
|
||||||
if (!Built)
|
if (!Built)
|
||||||
return;
|
return;
|
||||||
ComputeTargets(s, left, right);
|
ComputeTargets(s, left, right, physicalArms: true, dt);
|
||||||
foreach (RigidBody3D b in _bodies)
|
foreach (RigidBody3D b in _bodies)
|
||||||
{
|
{
|
||||||
if (BodyMath.ResetIfNonFinite(b, Target(b), "PlayerRagdoll"))
|
if (BodyMath.ResetIfNonFinite(b, Target(b), "PlayerRagdoll"))
|
||||||
@@ -401,14 +477,20 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
|
|
||||||
Muscle(_spine, _targets[Pelvis].Basis, _targets[Chest].Basis, dt);
|
Muscle(_spine, _targets[Pelvis].Basis, _targets[Chest].Basis, dt);
|
||||||
Muscle(_neck, _targets[Chest].Basis, _targets[Head].Basis, dt);
|
Muscle(_neck, _targets[Chest].Basis, _targets[Head].Basis, dt);
|
||||||
|
if (Strength != _appliedStrength)
|
||||||
|
ApplyStrength();
|
||||||
|
Braced = false;
|
||||||
for (int side = 0; side < 2; side++)
|
for (int side = 0; side < 2; side++)
|
||||||
{
|
{
|
||||||
Arm a = _arms[side];
|
Arm a = _arms[side];
|
||||||
ShoulderDrive(a, side, dt);
|
ShoulderDrive(a, side, dt);
|
||||||
ElbowMuscle(a, dt);
|
ElbowMuscle(a, dt);
|
||||||
HandGuide(a, _targets[a.Hand], side == 0 ? leftVelocity : rightVelocity);
|
HandGuide(a, _targets[a.Hand], side == 0 ? leftVelocity : rightVelocity);
|
||||||
|
Braced |= IsBraced(a);
|
||||||
}
|
}
|
||||||
GroundControl(pelvisTarget, floorY, dt);
|
GroundControl(pelvisTarget, floorY, dt);
|
||||||
|
LegsDrive(s);
|
||||||
|
_legVisuals.Tick(_footY, Grounded, !_legsSolid, s.YawBasis, dt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Muscle = PD on the joint's relative rotation toward the relative rotation of the two TARGET
|
// Muscle = PD on the joint's relative rotation toward the relative rotation of the two TARGET
|
||||||
@@ -461,6 +543,72 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
a.Elbow.SetParam(HingeJoint3D.Param.MotorTargetVelocity, HingeSign * w);
|
a.Elbow.SetParam(HingeJoint3D.Param.MotorTargetVelocity, HingeSign * w);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ApplyStrength()
|
||||||
|
{
|
||||||
|
_appliedStrength = Strength;
|
||||||
|
float k = Mathf.Max(0.05f, Strength);
|
||||||
|
SetAngularCap(_spine.Joint, SpineTorque * k);
|
||||||
|
SetAngularCap(_neck.Joint, NeckTorque * k);
|
||||||
|
foreach (Arm a in _arms)
|
||||||
|
{
|
||||||
|
SetAngularCap(a.Shoulder.Joint, ShoulderTorque * k);
|
||||||
|
SetAngularCap(a.Guide.Joint, HandGuideTorque * k);
|
||||||
|
a.Elbow.SetParam(HingeJoint3D.Param.MotorMaxImpulse, ElbowTorque * k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetAngularCap(Generic6DofJoint3D j, float cap)
|
||||||
|
{
|
||||||
|
j.SetParamX(Generic6DofJoint3D.Param.AngularMotorForceLimit, cap);
|
||||||
|
j.SetParamY(Generic6DofJoint3D.Param.AngularMotorForceLimit, cap);
|
||||||
|
j.SetParamZ(Generic6DofJoint3D.Param.AngularMotorForceLimit, cap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetLinearCap(Generic6DofJoint3D j, float cap)
|
||||||
|
{
|
||||||
|
j.SetParamX(Generic6DofJoint3D.Param.LinearMotorForceLimit, cap);
|
||||||
|
j.SetParamY(Generic6DofJoint3D.Param.LinearMotorForceLimit, cap);
|
||||||
|
j.SetParamZ(Generic6DofJoint3D.Param.LinearMotorForceLimit, cap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A hand pressing on or holding static world (a ledge, a wall, a grabbed bar).
|
||||||
|
private static bool IsBraced(Arm a)
|
||||||
|
{
|
||||||
|
if (a.Held is StaticBody3D)
|
||||||
|
return true;
|
||||||
|
foreach (Node3D n in a.Hand.GetCollidingBodies())
|
||||||
|
if (n is StaticBody3D)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The lower-body collider hangs from the actual hips, its bottom never below a step above the
|
||||||
|
// feet; once it would have to slide far up into the torso (deep crouch, crawl) it stops colliding.
|
||||||
|
private void LegsDrive(TrueBodySolver s)
|
||||||
|
{
|
||||||
|
Transform3D pelvis = Pelvis.GlobalTransform;
|
||||||
|
Vector3 hip = pelvis * (-_pelvisCenterLocal);
|
||||||
|
float minCenter = _footY + MaxStepHeight + _legsHeight * 0.5f;
|
||||||
|
float center = Mathf.Max(hip.Y - _legsHeight * 0.5f, minCenter);
|
||||||
|
// Tucked: deep crouch/crawl, or being lifted by the arms (mantling — the knee comes up).
|
||||||
|
_lifted = Braced && Pelvis.GlobalPosition.Y > _supportGoalY + MantleLift;
|
||||||
|
bool solid = center + _legsHeight * 0.5f < hip.Y + LegsOffAbove && !_lifted;
|
||||||
|
if (solid != _legsSolid)
|
||||||
|
{
|
||||||
|
_legsSolid = solid;
|
||||||
|
// The shape itself is disabled: Box3D keeps existing contacts when only the layer changes.
|
||||||
|
Legs.GetChild<CollisionShape3D>(0).SetDeferred(CollisionShape3D.PropertyName.Disabled, !solid);
|
||||||
|
}
|
||||||
|
if (!solid)
|
||||||
|
center = hip.Y; // tucked into the pelvis, out of the way
|
||||||
|
Vector3 desired = new(hip.X, center, hip.Z);
|
||||||
|
Quaternion frameA = Rot(Pelvis) * _legsDrive.FrameA;
|
||||||
|
SetLinearVelocity(_legsDrive.Joint, frameA.Inverse() * ((desired - Legs.GlobalPosition) * LegsKp));
|
||||||
|
Quaternion target = (Rot(Pelvis) * _legsDrive.FrameA).Inverse() * (s.YawBasis.GetRotationQuaternion() * _legsDrive.FrameB);
|
||||||
|
Quaternion current = frameA.Inverse() * (Rot(Legs) * _legsDrive.FrameB);
|
||||||
|
SetAngularVelocity(_legsDrive.Joint, (BodyMath.RotationError(target, current) * LegsKp).LimitLength(MuscleMaxSpin));
|
||||||
|
}
|
||||||
|
|
||||||
// Shoulder = one drive: its linear motor keeps the humeral head on the chest's shoulder point
|
// Shoulder = one drive: its linear motor keeps the humeral head on the chest's shoulder point
|
||||||
// shifted by the True Body girdle offset (shrug/protraction), its angular motor is the
|
// shifted by the True Body girdle offset (shrug/protraction), its angular motor is the
|
||||||
// shoulder muscle (same PD + feed-forward as Muscle, parent = chest).
|
// shoulder muscle (same PD + feed-forward as Muscle, parent = chest).
|
||||||
@@ -474,7 +622,8 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
SetLinearVelocity(d.Joint, frameA.Inverse() * (err * GirdleKp));
|
SetLinearVelocity(d.Joint, frameA.Inverse() * (err * GirdleKp));
|
||||||
|
|
||||||
Quaternion current = frameA.Inverse() * (Rot(a.Upper) * d.FrameB);
|
Quaternion current = frameA.Inverse() * (Rot(a.Upper) * d.FrameB);
|
||||||
Quaternion target = (_targets[Chest].Basis.Orthonormalized().GetRotationQuaternion() * d.FrameA).Inverse()
|
// Arm targets are solved from the physical chest, so the physical chest is the parent frame.
|
||||||
|
Quaternion target = (chest.Basis.GetRotationQuaternion() * d.FrameA).Inverse()
|
||||||
* (_targets[a.Upper].Basis.Orthonormalized().GetRotationQuaternion() * d.FrameB);
|
* (_targets[a.Upper].Basis.Orthonormalized().GetRotationQuaternion() * d.FrameB);
|
||||||
Vector3 ff = d.HasPrev && dt > 0f ? (BodyMath.RotationError(target, d.PrevTarget) / dt).LimitLength(MuscleMaxSpin) : Vector3.Zero;
|
Vector3 ff = d.HasPrev && dt > 0f ? (BodyMath.RotationError(target, d.PrevTarget) / dt).LimitLength(MuscleMaxSpin) : Vector3.Zero;
|
||||||
d.PrevTarget = target;
|
d.PrevTarget = target;
|
||||||
@@ -488,12 +637,24 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
Transform3D chest = Chest.GlobalTransform;
|
Transform3D chest = Chest.GlobalTransform;
|
||||||
Vector3 anchor = chest * a.Guide.AnchorA;
|
Vector3 anchor = chest * a.Guide.AnchorA;
|
||||||
Vector3 anchorVelocity = Chest.LinearVelocity + Chest.AngularVelocity.Cross(anchor - Chest.GlobalPosition);
|
Vector3 anchorVelocity = Chest.LinearVelocity + Chest.AngularVelocity.Cross(anchor - Chest.GlobalPosition);
|
||||||
Vector3 desired = (targetVelocity + (target.Origin - a.Hand.GlobalPosition) * HandGuideKp).LimitLength(HandGuideMaxSpeed);
|
Vector3 err = target.Origin - a.Hand.GlobalPosition;
|
||||||
|
Vector3 desired = (targetVelocity + err * HandGuideKp).LimitLength(HandGuideMaxSpeed);
|
||||||
Quaternion frameA = Rot(Chest) * a.Guide.FrameA;
|
Quaternion frameA = Rot(Chest) * a.Guide.FrameA;
|
||||||
|
Vector3 shoulder = a.Upper.GlobalTransform * a.Shoulder.AnchorB;
|
||||||
|
Vector3 axis = a.Hand.GlobalPosition - shoulder;
|
||||||
|
// Axial share of the error, with a 3 cm floor so a nearly-reached target's noisy error
|
||||||
|
// direction can't flip a held load into "structural".
|
||||||
|
float align = axis.LengthSquared() > 1e-8f ? Mathf.Abs(err.Dot(axis.Normalized())) / (err.Length() + StructuralDeadband) : 0f;
|
||||||
|
float cap = Mathf.Lerp(HandGuideForce, HandStructuralForce, align) * Mathf.Max(0.05f, Strength);
|
||||||
|
if (Mathf.Abs(cap - a.GuideCap) > 1f)
|
||||||
|
{
|
||||||
|
a.GuideCap = cap;
|
||||||
|
SetLinearCap(a.Guide.Joint, cap);
|
||||||
|
}
|
||||||
SetLinearVelocity(a.Guide.Joint, frameA.Inverse() * (desired - anchorVelocity));
|
SetLinearVelocity(a.Guide.Joint, frameA.Inverse() * (desired - anchorVelocity));
|
||||||
// Angular: hand rotation onto the controller rotation, as a relative spin vs the chest.
|
// Angular: hand rotation onto the controller rotation, as a relative spin vs the chest.
|
||||||
Vector3 err = BodyMath.RotationError(target.Basis.GetRotationQuaternion(), Rot(a.Hand));
|
Vector3 rotErr = BodyMath.RotationError(target.Basis.GetRotationQuaternion(), Rot(a.Hand));
|
||||||
Vector3 wWorld = (err * HandGuideAngularKp).LimitLength(MuscleMaxSpin * 2f) - Chest.AngularVelocity;
|
Vector3 wWorld = (rotErr * HandGuideAngularKp).LimitLength(MuscleMaxSpin * 2f) - Chest.AngularVelocity;
|
||||||
SetAngularVelocity(a.Guide.Joint, frameA.Inverse() * wWorld);
|
SetAngularVelocity(a.Guide.Joint, frameA.Inverse() * wWorld);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,20 +667,42 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
float hipAboveFloor = target.Origin.Y - floorY;
|
float hipAboveFloor = target.Origin.Y - floorY;
|
||||||
var query = PhysicsRayQueryParameters3D.Create(pos, pos + Vector3.Down * (hipAboveFloor + GroundProbeExtra + 0.5f), WorldMask);
|
var query = PhysicsRayQueryParameters3D.Create(pos, pos + Vector3.Down * (hipAboveFloor + GroundProbeExtra + 0.5f), WorldMask);
|
||||||
Godot.Collections.Dictionary hit = GetWorld3D().DirectSpaceState.IntersectRay(query);
|
Godot.Collections.Dictionary hit = GetWorld3D().DirectSpaceState.IntersectRay(query);
|
||||||
|
bool wasGrounded = Grounded;
|
||||||
Grounded = false;
|
Grounded = false;
|
||||||
if (hit.Count > 0)
|
if (hit.Count > 0)
|
||||||
{
|
{
|
||||||
Vector3 point = (Vector3)hit["position"];
|
Vector3 point = (Vector3)hit["position"];
|
||||||
Vector3 normal = (Vector3)hit["normal"];
|
Vector3 normal = (Vector3)hit["normal"];
|
||||||
if (pos.Y - point.Y < hipAboveFloor + GroundProbeExtra && normal.Y > 0.6f)
|
bool inReach = pos.Y - point.Y < hipAboveFloor + GroundProbeExtra;
|
||||||
|
bool steep = normal.Y <= Mathf.Cos(Mathf.DegToRad(MaxSlopeDegrees));
|
||||||
|
if (inReach && steep && wasGrounded && point.Y <= _footY + MaxStepHeight)
|
||||||
{
|
{
|
||||||
|
// The foot of a too-steep slope: keep standing on the ground we're on (the legs
|
||||||
|
// collider stops us); dropping support here would collapse the body.
|
||||||
Grounded = true;
|
Grounded = true;
|
||||||
GroundY = point.Y;
|
}
|
||||||
|
else if (inReach && !steep)
|
||||||
|
{
|
||||||
|
// Step rule: ground more than a step above the feet is only stood on once the
|
||||||
|
// hips are already well above it (climbed/mantled/landed there) — otherwise it's an
|
||||||
|
// obstacle the legs collider holds us back from, and the feet stay where they were.
|
||||||
|
// Higher ground climbed onto must be under the hips' back edge too (not just a
|
||||||
|
// pelvis centre hanging over the lip).
|
||||||
|
bool clear = point.Y <= _footY + MaxStepHeight || GroundBehindAt(pos, point.Y);
|
||||||
|
bool reachable = point.Y <= _footY + MaxStepHeight
|
||||||
|
|| ((!wasGrounded || !_legsSolid || pos.Y - point.Y > 0.6f * hipAboveFloor) && clear);
|
||||||
|
Grounded = true;
|
||||||
|
if (reachable)
|
||||||
|
_footY = point.Y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector3 targetVelocity = _havePelvisPrev && dt > 0f ? (target.Origin - _pelvisTargetPrev) / dt : Vector3.Zero;
|
// Feed-forward from the GOAL (pose + ground offset): when the play-space floor follows the
|
||||||
_pelvisTargetPrev = target.Origin;
|
// ground, the pose target and the ground offset move by the same amount in opposite
|
||||||
|
// directions — differentiating the pose alone would launch the body.
|
||||||
|
Vector3 goalNow = target.Origin + Vector3.Up * (_footY - floorY);
|
||||||
|
Vector3 targetVelocity = _havePelvisPrev && dt > 0f ? (goalNow - _pelvisTargetPrev) / dt : Vector3.Zero;
|
||||||
|
_pelvisTargetPrev = goalNow;
|
||||||
_havePelvisPrev = true;
|
_havePelvisPrev = true;
|
||||||
if (targetVelocity.Length() > 10f)
|
if (targetVelocity.Length() > 10f)
|
||||||
targetVelocity = Vector3.Zero;
|
targetVelocity = Vector3.Zero;
|
||||||
@@ -529,13 +712,25 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
{
|
{
|
||||||
// Pose heights are relative to the tracking floor; stand on the ground actually under us.
|
// Pose heights are relative to the tracking floor; stand on the ground actually under us.
|
||||||
Vector3 goal = target.Origin + Vector3.Up * (GroundY - floorY);
|
Vector3 goal = target.Origin + Vector3.Up * (GroundY - floorY);
|
||||||
|
_supportGoalY = goal.Y;
|
||||||
float w = Mathf.Tau * GroundFrequency;
|
float w = Mathf.Tau * GroundFrequency;
|
||||||
Vector3 accel = (goal - pos) * (w * w) + (targetVelocity - Pelvis.LinearVelocity) * (2f * GroundDamping * w);
|
Vector3 accel = (goal - pos) * (w * w) + (targetVelocity - Pelvis.LinearVelocity) * (2f * GroundDamping * w);
|
||||||
Vector3 force = accel * BodyMass;
|
Vector3 force = accel * BodyMass;
|
||||||
float support = Mathf.Clamp(force.Y + weight, 0f, LegStrength * weight);
|
float support = Mathf.Clamp(force.Y + weight, 0f, LegStrength * weight);
|
||||||
Vector3 horizontal = new Vector3(force.X, 0f, force.Z).LimitLength(GroundFriction * support);
|
float carry = Mathf.Max(GroundFriction * support, _legsSolid ? 0f : CrawlCarry);
|
||||||
|
Vector3 horizontal = new Vector3(force.X, 0f, force.Z).LimitLength(carry);
|
||||||
Pelvis.ApplyCentralForce(horizontal + Vector3.Up * support);
|
Pelvis.ApplyCentralForce(horizontal + Vector3.Up * support);
|
||||||
}
|
}
|
||||||
|
else if (Braced)
|
||||||
|
{
|
||||||
|
// Hanging/mantling on the hands: the hips still drift after the head a little, so you can
|
||||||
|
// shift your weight over a ledge you're pressing yourself up onto.
|
||||||
|
Vector3 lead = target.Origin - pos;
|
||||||
|
lead.Y = 0f;
|
||||||
|
Vector3 v = Pelvis.LinearVelocity;
|
||||||
|
v.Y = 0f;
|
||||||
|
Pelvis.ApplyCentralForce(((lead * 20f - v * 8f) * BodyMass).LimitLength(BracedAirControl));
|
||||||
|
}
|
||||||
|
|
||||||
Vector3 rotErr = BodyMath.RotationError(target.Basis.GetRotationQuaternion(), Rot(Pelvis));
|
Vector3 rotErr = BodyMath.RotationError(target.Basis.GetRotationQuaternion(), Rot(Pelvis));
|
||||||
Vector3 spin = (rotErr * BalanceKp).LimitLength(MuscleMaxSpin);
|
Vector3 spin = (rotErr * BalanceKp).LimitLength(MuscleMaxSpin);
|
||||||
@@ -576,20 +771,44 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
float[] fractions = GetWorld3D().DirectSpaceState.CastMotion(_cameraQuery);
|
float[] fractions = GetWorld3D().DirectSpaceState.CastMotion(_cameraQuery);
|
||||||
float safe = fractions.Length > 0 ? fractions[0] : 1f;
|
float safe = fractions.Length > 0 ? fractions[0] : 1f;
|
||||||
if (safe < 1f)
|
if (safe < 1f)
|
||||||
|
{
|
||||||
correction = -lead * (1f - safe);
|
correction = -lead * (1f - safe);
|
||||||
else if (lead.Length() > FreeSlack)
|
// Only a climbing body drags the view vertically through geometry; otherwise a
|
||||||
correction = -lead * (1f - FreeSlack / lead.Length());
|
// camera-to-eye line grazing a slope would sink the view (and with it the pose).
|
||||||
|
if (!Climbing)
|
||||||
|
correction.Y = 0f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float slack = Grounded ? FreeSlack : Climbing ? AirSlack : FallSlack;
|
||||||
|
if (lead.Length() > slack)
|
||||||
|
correction = -lead * (1f - slack / lead.Length());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (Grounded)
|
if (Grounded)
|
||||||
{
|
{
|
||||||
correction.Y = 0f;
|
correction.Y = 0f;
|
||||||
correction.Y += (GroundY - floorY) * (1f - Mathf.Exp(-dt * GroundFollowRate));
|
correction.Y += (GroundY - floorY) * (1f - Mathf.Exp(-dt * GroundFollowRate));
|
||||||
}
|
}
|
||||||
|
// The pose target moves with the play space: a correction is not intended motion, so it
|
||||||
|
// must not show up in ground control's feed-forward (that loop ran away at 7 m/s).
|
||||||
|
_pelvisTargetPrev += correction;
|
||||||
return correction;
|
return correction;
|
||||||
}
|
}
|
||||||
|
|
||||||
private PhysicsShapeQueryParameters3D _cameraQuery;
|
private PhysicsShapeQueryParameters3D _cameraQuery;
|
||||||
|
|
||||||
|
// Second ground ray 12 cm behind the hips (toward the body's back): ground at least `groundY`.
|
||||||
|
private bool GroundBehindAt(Vector3 pelvisPos, float groundY)
|
||||||
|
{
|
||||||
|
Vector3 back = Pelvis.GlobalTransform.Basis.Z;
|
||||||
|
back.Y = 0f;
|
||||||
|
back = back.LengthSquared() > 1e-6f ? back.Normalized() * 0.12f : Vector3.Zero;
|
||||||
|
var query = PhysicsRayQueryParameters3D.Create(pelvisPos + back, pelvisPos + back + Vector3.Down * 3f, WorldMask);
|
||||||
|
Godot.Collections.Dictionary hit = GetWorld3D().DirectSpaceState.IntersectRay(query);
|
||||||
|
return hit.Count > 0 && ((Vector3)hit["position"]).Y > groundY - 0.05f;
|
||||||
|
}
|
||||||
|
|
||||||
public void TeleportBy(Transform3D delta)
|
public void TeleportBy(Transform3D delta)
|
||||||
{
|
{
|
||||||
if (!Built)
|
if (!Built)
|
||||||
@@ -601,6 +820,9 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
b.AngularVelocity = delta.Basis * b.AngularVelocity;
|
b.AngularVelocity = delta.Basis * b.AngularVelocity;
|
||||||
}
|
}
|
||||||
_pelvisTargetPrev = delta * _pelvisTargetPrev;
|
_pelvisTargetPrev = delta * _pelvisTargetPrev;
|
||||||
|
_footY += delta.Origin.Y;
|
||||||
|
foreach (RigidBody3D b in _bodies)
|
||||||
|
b.ResetPhysicsInterpolation();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SnapToPose(TrueBodySolver s, Transform3D left, Transform3D right)
|
public void SnapToPose(TrueBodySolver s, Transform3D left, Transform3D right)
|
||||||
@@ -620,11 +842,13 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
a.Hand.GlobalTransform = new Transform3D(hb, s.Wrist[side] - hb * new Vector3(0f, 0f, WristOffset));
|
a.Hand.GlobalTransform = new Transform3D(hb, s.Wrist[side] - hb * new Vector3(0f, 0f, WristOffset));
|
||||||
}
|
}
|
||||||
_havePelvisPrev = false;
|
_havePelvisPrev = false;
|
||||||
|
foreach (RigidBody3D b in _bodies)
|
||||||
|
b.ResetPhysicsInterpolation();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collision exceptions between a held prop and that arm's segments (its hand is excluded by
|
// Collision exceptions between a held prop and that arm's segments (its hand is excluded by
|
||||||
// the grab weld itself) — a forearm wrapped around a held object must not fight it.
|
// the grab weld itself) — a forearm wrapped around a held object must not fight it.
|
||||||
public void SetHeld(int side, RigidBody3D prop)
|
public void SetHeld(int side, PhysicsBody3D prop)
|
||||||
{
|
{
|
||||||
if (!Built)
|
if (!Built)
|
||||||
return;
|
return;
|
||||||
|
|||||||
+23
-3
@@ -38,6 +38,21 @@ public partial class PlayerRig : Node
|
|||||||
private bool _rebuild = true;
|
private bool _rebuild = true;
|
||||||
|
|
||||||
public PlayerRagdoll Ragdoll => _ragdoll;
|
public PlayerRagdoll Ragdoll => _ragdoll;
|
||||||
|
public static bool Spectator { get; private set; }
|
||||||
|
|
||||||
|
// Spectator = no body (fly with the old grab-drag movement); leaving it rebuilds the body at
|
||||||
|
// the current pose.
|
||||||
|
public static void SetSpectator(bool on)
|
||||||
|
{
|
||||||
|
Spectator = on;
|
||||||
|
AnchorMoveMode.Enabled = on;
|
||||||
|
if (_instance == null)
|
||||||
|
return;
|
||||||
|
if (on)
|
||||||
|
_instance._ragdoll.Clear();
|
||||||
|
_instance._rebuild = true;
|
||||||
|
GD.Print($"PlayerRig: spectator {(on ? "on" : "off")}");
|
||||||
|
}
|
||||||
|
|
||||||
public static RigidBody3D GetHandBody(XRController3D controller)
|
public static RigidBody3D GetHandBody(XRController3D controller)
|
||||||
{
|
{
|
||||||
@@ -46,7 +61,7 @@ public partial class PlayerRig : Node
|
|||||||
return _instance._ragdoll.Hand(controller == _instance._left ? 0 : 1);
|
return _instance._ragdoll.Hand(controller == _instance._left ? 0 : 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SetHeld(XRController3D controller, RigidBody3D prop)
|
public static void SetHeld(XRController3D controller, PhysicsBody3D prop)
|
||||||
{
|
{
|
||||||
if (_instance != null)
|
if (_instance != null)
|
||||||
_instance._ragdoll.SetHeld(controller == _instance._left ? 0 : 1, prop);
|
_instance._ragdoll.SetHeld(controller == _instance._left ? 0 : 1, prop);
|
||||||
@@ -65,7 +80,9 @@ public partial class PlayerRig : Node
|
|||||||
_trueBody.ExternallySolved = true;
|
_trueBody.ExternallySolved = true;
|
||||||
TrueBody.ShowDebug = false;
|
TrueBody.ShowDebug = false;
|
||||||
}
|
}
|
||||||
_ragdoll = new PlayerRagdoll { Name = "Ragdoll" };
|
// PlayerRig itself is excluded from physics interpolation (ghosts follow controllers per
|
||||||
|
// frame); the body is simulated, so it interpolates.
|
||||||
|
_ragdoll = new PlayerRagdoll { Name = "Ragdoll", PhysicsInterpolationMode = PhysicsInterpolationModeEnum.On };
|
||||||
AddChild(_ragdoll);
|
AddChild(_ragdoll);
|
||||||
BodyProfile.Changed += _ => _rebuild = true;
|
BodyProfile.Changed += _ => _rebuild = true;
|
||||||
for (int i = 0; i < 2; i++)
|
for (int i = 0; i < 2; i++)
|
||||||
@@ -93,8 +110,11 @@ public partial class PlayerRig : Node
|
|||||||
{
|
{
|
||||||
float dt = (float)delta;
|
float dt = (float)delta;
|
||||||
BodyProfile profile = BodyProfile.Current;
|
BodyProfile profile = BodyProfile.Current;
|
||||||
if (profile == null)
|
if (profile == null || Spectator)
|
||||||
|
{
|
||||||
|
_haveLastOrigin = false;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Someone else moved the play space discretely: bring the body along.
|
// 1. Someone else moved the play space discretely: bring the body along.
|
||||||
Transform3D origin = _origin.GlobalTransform;
|
Transform3D origin = _origin.GlobalTransform;
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
using Godot;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
// The body-physics sandbox (Nimso-sandbox style), built in code around the spawn/table area:
|
||||||
|
// EAST — mantling ledges (0.3 … 1.5 m), a 1 m platform with stairs, a walkable 18° ramp and a
|
||||||
|
// too-steep 40° one, a climbing wall with holds onto a 2.4 m top, a pull-up bar, monkey bars.
|
||||||
|
// WEST — physics toys: a plank bridge on two blocks (walk on it / knock it off), loose planks,
|
||||||
|
// a seesaw, a hanging swing board (grab it and hang), a crate pyramid, a heavy box,
|
||||||
|
// barrels, a balance beam.
|
||||||
|
// SOUTH — a crawl tunnel (0.6 m clearance), a duck-under bar (1.2 m) and a doorway.
|
||||||
|
// Static pieces are world (layer 1) and grabbable for climbing (PropGrabber grabs any static body
|
||||||
|
// not in "no_grab"); dynamic pieces are "prop"s (grab/push) and come back with Reset().
|
||||||
|
public partial class Playground : Node3D
|
||||||
|
{
|
||||||
|
private readonly List<(RigidBody3D Body, Transform3D Start)> _dynamic = new();
|
||||||
|
private const uint WorldLayer = 1, WorldMask = 0b11101, PropMask = 0b11101;
|
||||||
|
|
||||||
|
private static readonly Color Concrete = new(0.62f, 0.62f, 0.6f), Wood = new(0.72f, 0.55f, 0.33f),
|
||||||
|
DarkWood = new(0.5f, 0.36f, 0.2f), Metal = new(0.45f, 0.47f, 0.52f), Hold = new(0.9f, 0.45f, 0.15f),
|
||||||
|
Blue = new(0.3f, 0.45f, 0.75f), Green = new(0.35f, 0.6f, 0.35f), Red = new(0.7f, 0.25f, 0.2f);
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
BuildEast();
|
||||||
|
BuildWest();
|
||||||
|
BuildSouth();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
foreach (var (body, start) in _dynamic)
|
||||||
|
{
|
||||||
|
if (!IsInstanceValid(body))
|
||||||
|
continue;
|
||||||
|
body.GlobalTransform = start;
|
||||||
|
body.LinearVelocity = Vector3.Zero;
|
||||||
|
body.AngularVelocity = Vector3.Zero;
|
||||||
|
body.ResetPhysicsInterpolation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- builders
|
||||||
|
|
||||||
|
private StaticBody3D Block(string name, Vector3 size, Vector3 center, Color color, Basis? basis = null)
|
||||||
|
{
|
||||||
|
StaticBody3D body = new() { Name = name, CollisionLayer = WorldLayer, CollisionMask = WorldMask, Transform = new Transform3D(basis ?? Basis.Identity, center) };
|
||||||
|
body.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = size } });
|
||||||
|
body.AddChild(Mesh(new BoxMesh { Size = size }, color));
|
||||||
|
AddChild(body);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private StaticBody3D Bar(string name, Vector3 from, Vector3 to, float radius, Color color)
|
||||||
|
{
|
||||||
|
Vector3 axis = to - from;
|
||||||
|
float len = axis.Length();
|
||||||
|
Vector3 y = axis / len;
|
||||||
|
Vector3 x = Mathf.Abs(y.Y) < 0.99f ? Vector3.Up.Cross(y).Normalized() : Vector3.Right;
|
||||||
|
StaticBody3D body = new() { Name = name, CollisionLayer = WorldLayer, CollisionMask = WorldMask, Transform = new Transform3D(new Basis(x, y, x.Cross(y)), (from + to) * 0.5f) };
|
||||||
|
body.AddChild(new CollisionShape3D { Shape = new CylinderShape3D { Radius = radius, Height = len } });
|
||||||
|
body.AddChild(Mesh(new CylinderMesh { TopRadius = radius, BottomRadius = radius, Height = len, RadialSegments = 12 }, color));
|
||||||
|
AddChild(body);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RigidBody3D Prop(string name, Shape3D shape, Mesh mesh, float mass, Vector3 center, Color color, Basis? basis = null, float friction = 0.8f)
|
||||||
|
{
|
||||||
|
RigidBody3D body = new()
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Mass = mass,
|
||||||
|
Transform = new Transform3D(basis ?? Basis.Identity, center),
|
||||||
|
CollisionMask = PropMask,
|
||||||
|
ContactMonitor = true,
|
||||||
|
MaxContactsReported = 4,
|
||||||
|
PhysicsMaterialOverride = new PhysicsMaterial { Friction = friction, Bounce = 0.05f },
|
||||||
|
};
|
||||||
|
body.AddToGroup("prop");
|
||||||
|
body.AddChild(new CollisionShape3D { Shape = shape });
|
||||||
|
body.AddChild(Mesh(mesh, color));
|
||||||
|
AddChild(body);
|
||||||
|
_dynamic.Add((body, body.GlobalTransform));
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RigidBody3D BoxProp(string name, Vector3 size, float mass, Vector3 center, Color color, Basis? basis = null) =>
|
||||||
|
Prop(name, new BoxShape3D { Size = size }, new BoxMesh { Size = size }, mass, center, color, basis);
|
||||||
|
|
||||||
|
private static MeshInstance3D Mesh(Mesh mesh, Color color)
|
||||||
|
{
|
||||||
|
MeshInstance3D m = new() { Mesh = mesh };
|
||||||
|
m.SetSurfaceOverrideMaterial(0, new StandardMaterial3D { AlbedoColor = color, Roughness = 0.75f });
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Sign(string text, Vector3 at)
|
||||||
|
{
|
||||||
|
AddChild(new Label3D
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
FontSize = 96,
|
||||||
|
PixelSize = 0.002f,
|
||||||
|
OutlineSize = 12,
|
||||||
|
Billboard = BaseMaterial3D.BillboardModeEnum.FixedY,
|
||||||
|
Position = at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- zones
|
||||||
|
|
||||||
|
private void BuildEast()
|
||||||
|
{
|
||||||
|
// Mantling ledges: walk up to one, put both hands on it and press yourself up.
|
||||||
|
float[] heights = { 0.3f, 0.6f, 0.9f, 1.2f, 1.5f };
|
||||||
|
for (int i = 0; i < heights.Length; i++)
|
||||||
|
{
|
||||||
|
float h = heights[i];
|
||||||
|
Block($"Ledge{h:0.0}", new Vector3(1.2f, h, 1.2f), new Vector3(4.5f + i * 1.9f, h * 0.5f, -8f), Concrete);
|
||||||
|
Sign($"{h:0.0} m", new Vector3(4.5f + i * 1.9f, h + 0.35f, -7.3f));
|
||||||
|
}
|
||||||
|
Sign("Mantle: both hands on the ledge, push down", new Vector3(8.3f, 2.4f, -8f));
|
||||||
|
|
||||||
|
// Platform 1 m high with stairs (0.17 m risers) on the south side and ramps on the north.
|
||||||
|
Block("Platform", new Vector3(3f, 1f, 3f), new Vector3(8f, 0.5f, -1.5f), Concrete);
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
float top = 0.17f * (i + 1);
|
||||||
|
Block($"Stair{i}", new Vector3(1.2f, top, 0.3f), new Vector3(8f, top * 0.5f, 0.15f + (4 - i) * 0.3f), Concrete.Lightened(0.1f));
|
||||||
|
}
|
||||||
|
float walkable = Mathf.DegToRad(18f);
|
||||||
|
float rampLen = 1f / Mathf.Sin(walkable);
|
||||||
|
// Negative X rotation lifts the slab's +Z end, which touches the platform's north edge.
|
||||||
|
Block("RampWalkable", new Vector3(1.2f, 0.1f, rampLen), new Vector3(7f, 0.5f, -3f - Mathf.Cos(walkable) * rampLen * 0.5f),
|
||||||
|
Green, new Basis(Vector3.Right, -walkable));
|
||||||
|
float steep = Mathf.DegToRad(40f);
|
||||||
|
float steepLen = 1f / Mathf.Sin(steep);
|
||||||
|
Block("RampSteep", new Vector3(1.2f, 0.1f, steepLen), new Vector3(9f, 0.5f, -3f - Mathf.Cos(steep) * steepLen * 0.5f),
|
||||||
|
Red, new Basis(Vector3.Right, -steep));
|
||||||
|
Sign("18° ramp", new Vector3(7f, 1.6f, -4.5f));
|
||||||
|
Sign("40° (too steep)", new Vector3(9f, 1.6f, -4.5f));
|
||||||
|
|
||||||
|
// Climbing wall: holds on the west face, top at 2.4 m to climb onto.
|
||||||
|
Block("ClimbWall", new Vector3(1.5f, 2.4f, 3f), new Vector3(12.25f, 1.2f, -1.5f), Concrete.Darkened(0.15f));
|
||||||
|
RandomNumberGenerator rng = new() { Seed = 5 };
|
||||||
|
for (float y = 0.4f; y < 2.35f; y += 0.35f)
|
||||||
|
{
|
||||||
|
for (int k = 0; k < 4; k++)
|
||||||
|
{
|
||||||
|
float z = -2.7f + k * 0.8f + rng.RandfRange(-0.15f, 0.15f);
|
||||||
|
Block("Hold", new Vector3(0.08f, 0.06f, 0.22f), new Vector3(11.46f, y + rng.RandfRange(-0.06f, 0.06f), z), Hold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Sign("Climbing wall: grip the holds", new Vector3(11f, 3f, -1.5f));
|
||||||
|
|
||||||
|
// Pull-up bar and monkey bars.
|
||||||
|
Bar("PostA", new Vector3(5f, 0f, 2.5f), new Vector3(5f, 2.25f, 2.5f), 0.05f, Metal);
|
||||||
|
Bar("PostB", new Vector3(6.4f, 0f, 2.5f), new Vector3(6.4f, 2.25f, 2.5f), 0.05f, Metal);
|
||||||
|
Bar("PullUpBar", new Vector3(5f, 2.1f, 2.5f), new Vector3(6.4f, 2.1f, 2.5f), 0.025f, Metal);
|
||||||
|
Sign("Pull-up bar", new Vector3(5.7f, 2.6f, 2.5f));
|
||||||
|
Bar("MonkeyRailL", new Vector3(5f, 2.25f, 4.5f), new Vector3(10f, 2.25f, 4.5f), 0.04f, Metal);
|
||||||
|
Bar("MonkeyRailR", new Vector3(5f, 2.25f, 5.1f), new Vector3(10f, 2.25f, 5.1f), 0.04f, Metal);
|
||||||
|
foreach (float x in new[] { 5f, 10f })
|
||||||
|
foreach (float z in new[] { 4.5f, 5.1f })
|
||||||
|
Bar("MonkeyPost", new Vector3(x, 0f, z), new Vector3(x, 2.25f, z), 0.05f, Metal);
|
||||||
|
for (float x = 5.4f; x < 9.8f; x += 0.4f)
|
||||||
|
Bar("Rung", new Vector3(x, 2.2f, 4.5f), new Vector3(x, 2.2f, 5.1f), 0.022f, Metal);
|
||||||
|
Sign("Monkey bars", new Vector3(7.5f, 2.8f, 4.8f));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildWest()
|
||||||
|
{
|
||||||
|
// Plank bridge: two blocks with a loose 9 kg plank across — walk on it, or knock it off.
|
||||||
|
Block("BridgeBlockA", new Vector3(1f, 0.8f, 1f), new Vector3(-4.5f, 0.4f, -5f), Concrete);
|
||||||
|
Block("BridgeBlockB", new Vector3(1f, 0.8f, 1f), new Vector3(-8f, 0.4f, -5f), Concrete);
|
||||||
|
BoxProp("BridgePlank", new Vector3(3.4f, 0.06f, 0.4f), 9f, new Vector3(-6.25f, 0.83f, -5f), Wood);
|
||||||
|
Sign("Plank bridge", new Vector3(-6.25f, 1.6f, -5f));
|
||||||
|
|
||||||
|
// Loose planks to pick up, carry, lean, swing (light enough to wield, long enough to lever).
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
BoxProp($"Plank{i}", new Vector3(2f, 0.04f, 0.2f), 4f, new Vector3(-5f, 0.02f + i * 0.045f, -2.6f + i * 0.05f), Wood.Lightened(0.05f * i),
|
||||||
|
new Basis(Vector3.Up, 0.1f * i));
|
||||||
|
|
||||||
|
// Seesaw: a plank hinged on a pivot block.
|
||||||
|
Block("SeesawPivot", new Vector3(0.3f, 0.45f, 0.5f), new Vector3(-8.5f, 0.225f, -1.5f), Metal);
|
||||||
|
RigidBody3D seesaw = BoxProp("Seesaw", new Vector3(3.2f, 0.06f, 0.45f), 12f, new Vector3(-8.5f, 0.48f, -1.5f), DarkWood);
|
||||||
|
HingeJoint3D pivot = new()
|
||||||
|
{
|
||||||
|
Transform = new Transform3D(new Basis(Vector3.Right, Vector3.Up, Vector3.Back), new Vector3(-8.5f, 0.48f, -1.5f)),
|
||||||
|
NodeA = GetPathTo(GetNode("SeesawPivot")),
|
||||||
|
NodeB = GetPathTo(seesaw),
|
||||||
|
};
|
||||||
|
AddChild(pivot);
|
||||||
|
Sign("Seesaw", new Vector3(-8.5f, 1.3f, -1.5f));
|
||||||
|
|
||||||
|
// Swing board: a 10 kg board hinged at its top edge from a 2.4 m beam — grab it and hang.
|
||||||
|
Bar("SwingPostA", new Vector3(-5f, 0f, 1.5f), new Vector3(-5f, 2.45f, 1.5f), 0.05f, Metal);
|
||||||
|
Bar("SwingPostB", new Vector3(-7f, 0f, 1.5f), new Vector3(-7f, 2.45f, 1.5f), 0.05f, Metal);
|
||||||
|
StaticBody3D beam = Bar("SwingBeam", new Vector3(-5f, 2.4f, 1.5f), new Vector3(-7f, 2.4f, 1.5f), 0.05f, Metal);
|
||||||
|
RigidBody3D board = BoxProp("SwingBoard", new Vector3(1.2f, 1.2f, 0.05f), 10f, new Vector3(-6f, 1.75f, 1.5f), Wood);
|
||||||
|
HingeJoint3D swing = new()
|
||||||
|
{
|
||||||
|
Transform = new Transform3D(new Basis(Vector3.Back, Vector3.Up, Vector3.Left), new Vector3(-6f, 2.35f, 1.5f)),
|
||||||
|
NodeA = GetPathTo(beam),
|
||||||
|
NodeB = GetPathTo(board),
|
||||||
|
};
|
||||||
|
AddChild(swing);
|
||||||
|
Sign("Hanging board", new Vector3(-6f, 2.9f, 1.5f));
|
||||||
|
|
||||||
|
// Crate pyramid (4 kg each), a heavy 25 kg box, barrels.
|
||||||
|
Vector3 basePos = new(-9.5f, 0f, 2f);
|
||||||
|
int n = 0;
|
||||||
|
for (int row = 0; row < 3; row++)
|
||||||
|
for (int col = 0; col < 3 - row; col++)
|
||||||
|
BoxProp($"Crate{n++}", Vector3.One * 0.4f, 4f, basePos + new Vector3((col + row * 0.5f) * 0.42f, 0.2f + row * 0.401f, 0f), Wood.Darkened(0.1f));
|
||||||
|
BoxProp("HeavyBox", Vector3.One * 0.7f, 25f, new Vector3(-9f, 0.35f, 4f), Blue);
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
Prop($"Barrel{i}", new CylinderShape3D { Radius = 0.28f, Height = 0.85f }, new CylinderMesh { TopRadius = 0.28f, BottomRadius = 0.28f, Height = 0.85f }, 12f,
|
||||||
|
new Vector3(-7.5f + i * 0.7f, 0.425f, 4.5f), Red);
|
||||||
|
|
||||||
|
// Balance beam.
|
||||||
|
Block("BalanceBeam", new Vector3(0.12f, 0.3f, 4f), new Vector3(-11f, 0.15f, -2f), DarkWood);
|
||||||
|
Sign("Balance beam", new Vector3(-11f, 1.1f, -2f));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildSouth()
|
||||||
|
{
|
||||||
|
// Crawl tunnel: 0.6 m clearance, 3 m long.
|
||||||
|
Block("TunnelRoof", new Vector3(1.4f, 0.1f, 3f), new Vector3(-2.5f, 0.65f, 7f), Concrete);
|
||||||
|
Block("TunnelWallL", new Vector3(0.1f, 0.6f, 3f), new Vector3(-3.15f, 0.3f, 7f), Concrete);
|
||||||
|
Block("TunnelWallR", new Vector3(0.1f, 0.6f, 3f), new Vector3(-1.85f, 0.3f, 7f), Concrete);
|
||||||
|
Sign("Crawl tunnel (0.6 m)", new Vector3(-2.5f, 1.2f, 5.3f));
|
||||||
|
|
||||||
|
// Duck-under bar at 1.2 m and a doorway (1.9 × 0.8 m).
|
||||||
|
Bar("DuckPostA", new Vector3(1f, 0f, 7f), new Vector3(1f, 1.3f, 7f), 0.05f, Metal);
|
||||||
|
Bar("DuckPostB", new Vector3(2.6f, 0f, 7f), new Vector3(2.6f, 1.3f, 7f), 0.05f, Metal);
|
||||||
|
Bar("DuckBar", new Vector3(1f, 1.25f, 7f), new Vector3(2.6f, 1.25f, 7f), 0.03f, Metal);
|
||||||
|
Sign("Duck under (1.2 m)", new Vector3(1.8f, 1.8f, 7f));
|
||||||
|
Block("DoorL", new Vector3(0.2f, 2.1f, 0.3f), new Vector3(4.1f, 1.05f, 7f), Wood);
|
||||||
|
Block("DoorR", new Vector3(0.2f, 2.1f, 0.3f), new Vector3(5.1f, 1.05f, 7f), Wood);
|
||||||
|
Block("DoorTop", new Vector3(1.2f, 0.2f, 0.3f), new Vector3(4.6f, 2.0f, 7f), Wood);
|
||||||
|
Sign("Doorway", new Vector3(4.6f, 2.5f, 7f));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dg665wra8wjhg
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
using Godot;
|
||||||
|
|
||||||
|
// Visual procedural legs for the ragdoll player (Nimso: "a very basic procedural leg system", feet
|
||||||
|
// "don't actually hold the player up" — ground control does). Feet stay planted on the ground and
|
||||||
|
// step, alternating, when the hips move or turn away from them; the stride leads with the body's
|
||||||
|
// velocity. Airborne the feet hang under the hips; low (crawling) they trail behind. Knees bend
|
||||||
|
// forward via a 2-bone solve. Physics-tick state (PlayerRagdoll.Step calls Tick), drawn in
|
||||||
|
// _Process from the interpolated pelvis. No collision — the lower-body collider is separate.
|
||||||
|
public partial class ProceduralLegs : Node3D
|
||||||
|
{
|
||||||
|
public const float ThighRatio = 0.245f, ShinRatio = 0.246f, AnkleRatio = 0.039f; // Winter, × stature
|
||||||
|
public const float HipHalfWidth = 0.09f;
|
||||||
|
public const float StanceHalfWidth = 0.11f;
|
||||||
|
public const float StepTrigger = 0.2f; // m from the ideal spot
|
||||||
|
public const float StepTurnTrigger = 0.6f; // rad
|
||||||
|
public const float StepLead = 0.25f; // s of velocity the new foothold leads by
|
||||||
|
public const float StepTimeSlow = 0.32f, StepTimeFast = 0.2f;
|
||||||
|
public const float StepHeight = 0.09f;
|
||||||
|
public const float SettleTime = 0.4f; // standing still this long → the trailing foot squares up
|
||||||
|
|
||||||
|
private struct Foot
|
||||||
|
{
|
||||||
|
public Vector3 Planted, From, To;
|
||||||
|
public float Yaw, FromYaw, ToYaw;
|
||||||
|
public float T; // step progress 0..1 (1 = planted)
|
||||||
|
public Vector3 Current;
|
||||||
|
public float CurrentYaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Foot[] _feet = new Foot[2];
|
||||||
|
private readonly MeshInstance3D[] _thigh = new MeshInstance3D[2], _shin = new MeshInstance3D[2], _foot = new MeshInstance3D[2];
|
||||||
|
private RigidBody3D _pelvis;
|
||||||
|
private Vector3 _hipLocal; // pelvis-body-local hip centre
|
||||||
|
private float _thighLen, _shinLen, _ankle, _scale;
|
||||||
|
private bool _initialized;
|
||||||
|
private int _lastStepped;
|
||||||
|
private float _stillTime;
|
||||||
|
|
||||||
|
public void Setup(RigidBody3D pelvis, Vector3 hipLocal, float stature, Color pants, Color shoes)
|
||||||
|
{
|
||||||
|
_pelvis = pelvis;
|
||||||
|
_hipLocal = hipLocal;
|
||||||
|
_thighLen = ThighRatio * stature;
|
||||||
|
_shinLen = ShinRatio * stature;
|
||||||
|
_ankle = AnkleRatio * stature;
|
||||||
|
_scale = stature / 1.75f;
|
||||||
|
PhysicsInterpolationMode = PhysicsInterpolationModeEnum.Off; // meshes are placed every frame
|
||||||
|
TopLevel = true;
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
{
|
||||||
|
_thigh[i] = MakeMesh(new Vector3(0.13f * _scale, 1f, 0.14f * _scale), pants);
|
||||||
|
_shin[i] = MakeMesh(new Vector3(0.1f * _scale, 1f, 0.11f * _scale), pants);
|
||||||
|
_foot[i] = MakeMesh(new Vector3(0.1f * _scale, 0.07f * _scale, 0.25f * _scale), shoes);
|
||||||
|
}
|
||||||
|
_initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MeshInstance3D MakeMesh(Vector3 size, Color color)
|
||||||
|
{
|
||||||
|
MeshInstance3D m = new() { Mesh = new BoxMesh { Size = size } };
|
||||||
|
m.SetSurfaceOverrideMaterial(0, new StandardMaterial3D { AlbedoColor = color, Roughness = 0.7f });
|
||||||
|
AddChild(m);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector3 Hip(Transform3D pelvis, int side) =>
|
||||||
|
pelvis * (_hipLocal + new Vector3(side == 0 ? -HipHalfWidth * _scale : HipHalfWidth * _scale, 0f, 0f));
|
||||||
|
|
||||||
|
private static float YawOf(Basis b)
|
||||||
|
{
|
||||||
|
Vector3 f = -b.Z;
|
||||||
|
return Mathf.Atan2(-f.X, -f.Z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// groundY = where the feet stand (ground control's foot level); crawl = legs collider off.
|
||||||
|
public void Tick(float groundY, bool grounded, bool crawl, Basis yawBasis, float dt)
|
||||||
|
{
|
||||||
|
if (_pelvis == null)
|
||||||
|
return;
|
||||||
|
Transform3D pelvis = _pelvis.GlobalTransform;
|
||||||
|
Vector3 velocity = _pelvis.LinearVelocity;
|
||||||
|
velocity.Y = 0f;
|
||||||
|
float yaw = YawOf(yawBasis);
|
||||||
|
Vector3 back = yawBasis.Z;
|
||||||
|
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
{
|
||||||
|
Vector3 hip = Hip(pelvis, i);
|
||||||
|
Vector3 side = yawBasis.X * (i == 0 ? -1f : 1f);
|
||||||
|
Vector3 ideal = new Vector3(hip.X, groundY, hip.Z) + side * (StanceHalfWidth - HipHalfWidth) * _scale;
|
||||||
|
if (crawl)
|
||||||
|
ideal += back * (_thighLen + _shinLen) * 0.8f;
|
||||||
|
if (!_initialized)
|
||||||
|
{
|
||||||
|
_feet[i] = new Foot { Planted = ideal, Current = ideal, To = ideal, Yaw = yaw, CurrentYaw = yaw, ToYaw = yaw, T = 1f };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref Foot f = ref _feet[i];
|
||||||
|
if (!grounded)
|
||||||
|
{
|
||||||
|
// Hanging: feet dangle under the hips, a little forward, and replant on landing.
|
||||||
|
Vector3 hang = hip + Vector3.Down * (_thighLen + _shinLen) * 0.92f - back * 0.08f;
|
||||||
|
f.Current = f.Current.Lerp(hang, 1f - Mathf.Exp(-dt * 10f));
|
||||||
|
f.CurrentYaw = Mathf.LerpAngle(f.CurrentYaw, yaw, 1f - Mathf.Exp(-dt * 10f));
|
||||||
|
f.Planted = f.Current;
|
||||||
|
f.T = 1f;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f.T < 1f)
|
||||||
|
{
|
||||||
|
float speed = velocity.Length();
|
||||||
|
float duration = Mathf.Lerp(StepTimeSlow, StepTimeFast, Mathf.Clamp(speed / 2.5f, 0f, 1f));
|
||||||
|
f.T = Mathf.Min(1f, f.T + dt / duration);
|
||||||
|
f.To = new Vector3(f.To.X, groundY, f.To.Z);
|
||||||
|
float k = Mathf.SmoothStep(0f, 1f, f.T);
|
||||||
|
f.Current = f.From.Lerp(f.To, k) + Vector3.Up * (Mathf.Sin(Mathf.Pi * f.T) * (StepHeight + 0.03f * speed));
|
||||||
|
f.CurrentYaw = Mathf.LerpAngle(f.FromYaw, f.ToYaw, k);
|
||||||
|
if (f.T >= 1f)
|
||||||
|
{
|
||||||
|
f.Planted = f.To;
|
||||||
|
f.Yaw = f.ToYaw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
f.Planted = new Vector3(f.Planted.X, Mathf.MoveToward(f.Planted.Y, groundY, dt * 2f), f.Planted.Z);
|
||||||
|
f.Current = f.Planted;
|
||||||
|
f.CurrentYaw = f.Yaw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_initialized)
|
||||||
|
{
|
||||||
|
_initialized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!grounded)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// One foot at a time: step the foot that is farthest from where it should be.
|
||||||
|
if (_feet[0].T >= 1f && _feet[1].T >= 1f)
|
||||||
|
{
|
||||||
|
float speed = velocity.Length();
|
||||||
|
_stillTime = speed < 0.05f ? _stillTime + dt : 0f;
|
||||||
|
int pick = -1;
|
||||||
|
float worst = 0f;
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
{
|
||||||
|
Vector3 hip = Hip(pelvis, i);
|
||||||
|
Vector3 side = yawBasis.X * (i == 0 ? -1f : 1f);
|
||||||
|
Vector3 ideal = new Vector3(hip.X, groundY, hip.Z) + side * (StanceHalfWidth - HipHalfWidth) * _scale;
|
||||||
|
if (crawl)
|
||||||
|
ideal += back * (_thighLen + _shinLen) * 0.8f;
|
||||||
|
float off = new Vector2(_feet[i].Planted.X - ideal.X, _feet[i].Planted.Z - ideal.Z).Length();
|
||||||
|
float turn = Mathf.Abs(Mathf.AngleDifference(_feet[i].Yaw, yaw));
|
||||||
|
bool need = off > StepTrigger * _scale || turn > StepTurnTrigger || (_stillTime > SettleTime && off > 0.06f);
|
||||||
|
float score = off + turn * 0.2f + (i == _lastStepped ? -0.01f : 0f);
|
||||||
|
if (need && score > worst)
|
||||||
|
{
|
||||||
|
worst = score;
|
||||||
|
pick = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pick >= 0)
|
||||||
|
{
|
||||||
|
Vector3 hip = Hip(pelvis, pick);
|
||||||
|
Vector3 side = yawBasis.X * (pick == 0 ? -1f : 1f);
|
||||||
|
Vector3 to = new Vector3(hip.X, groundY, hip.Z) + side * (StanceHalfWidth - HipHalfWidth) * _scale + velocity * StepLead;
|
||||||
|
if (crawl)
|
||||||
|
to += back * (_thighLen + _shinLen) * 0.8f;
|
||||||
|
ref Foot f = ref _feet[pick];
|
||||||
|
f.From = f.Planted;
|
||||||
|
f.FromYaw = f.Yaw;
|
||||||
|
f.To = to;
|
||||||
|
f.ToYaw = yaw;
|
||||||
|
f.T = 0f;
|
||||||
|
_lastStepped = pick;
|
||||||
|
_stillTime = 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Process(double delta)
|
||||||
|
{
|
||||||
|
if (_pelvis == null || !_initialized || !IsInstanceValid(_pelvis))
|
||||||
|
return;
|
||||||
|
Transform3D pelvis = _pelvis.GetGlobalTransformInterpolated();
|
||||||
|
Vector3 forward = -pelvis.Basis.Z;
|
||||||
|
forward.Y = 0f;
|
||||||
|
forward = forward.LengthSquared() > 1e-6f ? forward.Normalized() : Vector3.Forward;
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
{
|
||||||
|
Vector3 hip = Hip(pelvis, i);
|
||||||
|
Vector3 ankle = _feet[i].Current + Vector3.Up * _ankle;
|
||||||
|
// 2-bone solve, knee bending forward.
|
||||||
|
Vector3 d = ankle - hip;
|
||||||
|
float reach = _thighLen + _shinLen;
|
||||||
|
float len = Mathf.Clamp(d.Length(), 0.05f, reach * 0.999f);
|
||||||
|
Vector3 dir = d.LengthSquared() > 1e-8f ? d.Normalized() : Vector3.Down;
|
||||||
|
ankle = hip + dir * len;
|
||||||
|
float along = (_thighLen * _thighLen - _shinLen * _shinLen + len * len) / (2f * len);
|
||||||
|
float radius = Mathf.Sqrt(Mathf.Max(_thighLen * _thighLen - along * along, 0f));
|
||||||
|
Vector3 pole = forward - dir * forward.Dot(dir);
|
||||||
|
pole = pole.LengthSquared() > 1e-6f ? pole.Normalized() : forward;
|
||||||
|
Vector3 knee = hip + dir * along + pole * radius;
|
||||||
|
Place(_thigh[i], hip, knee);
|
||||||
|
Place(_shin[i], knee, ankle);
|
||||||
|
Basis footBasis = new Basis(Vector3.Up, _feet[i].CurrentYaw);
|
||||||
|
_foot[i].GlobalTransform = new Transform3D(footBasis, _feet[i].Current + Vector3.Up * (0.035f * _scale) + footBasis * new Vector3(0f, 0f, -0.06f * _scale));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Place(MeshInstance3D mesh, Vector3 from, Vector3 to)
|
||||||
|
{
|
||||||
|
Vector3 seg = to - from;
|
||||||
|
float len = seg.Length();
|
||||||
|
if (len < 1e-4f)
|
||||||
|
return;
|
||||||
|
Vector3 y = seg / len;
|
||||||
|
Vector3 x = Mathf.Abs(y.Y) < 0.99f ? Vector3.Up.Cross(y).Normalized() : Vector3.Right;
|
||||||
|
Vector3 z = x.Cross(y);
|
||||||
|
// Box mesh is 1 m tall along Y: stretch it to the segment.
|
||||||
|
mesh.GlobalTransform = new Transform3D(new Basis(x, y * len, z), (from + to) * 0.5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bwbp1b87b5qxx
|
||||||
+36
-13
@@ -11,6 +11,8 @@ public partial class PropGrabber : Node
|
|||||||
private const float BreakTime = 0.25f;
|
private const float BreakTime = 0.25f;
|
||||||
private const float BreakDistanceBoth = 0.4f;
|
private const float BreakDistanceBoth = 0.4f;
|
||||||
private const float GripRipDistance = 0.8f;
|
private const float GripRipDistance = 0.8f;
|
||||||
|
private const float WorldGrabRadius = 0.05f; // palm sphere for grabbing static world (ledges, bars, holds)
|
||||||
|
public const string NoGrabGroup = "no_grab"; // static bodies that can't be grabbed (the floor)
|
||||||
|
|
||||||
private static PropGrabber _instance;
|
private static PropGrabber _instance;
|
||||||
|
|
||||||
@@ -18,7 +20,7 @@ public partial class PropGrabber : Node
|
|||||||
private XRController3D _right;
|
private XRController3D _right;
|
||||||
private PhysicsArms _arms;
|
private PhysicsArms _arms;
|
||||||
|
|
||||||
private readonly RigidBody3D[] _held = new RigidBody3D[2];
|
private readonly PhysicsBody3D[] _held = new PhysicsBody3D[2]; // a prop, or static world (climbing)
|
||||||
private readonly ConeTwistJoint3D[] _joints = new ConeTwistJoint3D[2];
|
private readonly ConeTwistJoint3D[] _joints = new ConeTwistJoint3D[2];
|
||||||
private readonly Vector3[] _localAnchor = new Vector3[2];
|
private readonly Vector3[] _localAnchor = new Vector3[2];
|
||||||
private readonly uint[] _grabSeq = new uint[2];
|
private readonly uint[] _grabSeq = new uint[2];
|
||||||
@@ -59,7 +61,7 @@ public partial class PropGrabber : Node
|
|||||||
|
|
||||||
if (grip && !_gripPrev[hand] && _held[hand] == null)
|
if (grip && !_gripPrev[hand] && _held[hand] == null)
|
||||||
{
|
{
|
||||||
RigidBody3D prop = FindProp(controller, hand);
|
PhysicsBody3D prop = FindProp(controller, hand);
|
||||||
RigidBody3D handBody = PhysicsHands.GetBody(controller);
|
RigidBody3D handBody = PhysicsHands.GetBody(controller);
|
||||||
if (prop != null && handBody != null)
|
if (prop != null && handBody != null)
|
||||||
{
|
{
|
||||||
@@ -68,26 +70,27 @@ public partial class PropGrabber : Node
|
|||||||
Vector3 pivot = PalmPoint(handBody, hand);
|
Vector3 pivot = PalmPoint(handBody, hand);
|
||||||
_joints[hand] = MakeWeld(handBody, prop, pivot, RigidWeldBias);
|
_joints[hand] = MakeWeld(handBody, prop, pivot, RigidWeldBias);
|
||||||
_localAnchor[hand] = prop.GlobalTransform.AffineInverse() * pivot;
|
_localAnchor[hand] = prop.GlobalTransform.AffineInverse() * pivot;
|
||||||
_arms?.SetHeld(hand, prop);
|
_arms?.SetHeld(hand, prop as RigidBody3D);
|
||||||
PlayerRig.SetHeld(controller, prop);
|
PlayerRig.SetHeld(controller, prop);
|
||||||
GD.Print($"PropGrabber: grabbed '{prop.Name}' ({prop.Mass:0.###} kg, {(hand == 0 ? "left" : "right")}{(_held[other] == prop ? ", two-handed" : "")})");
|
string what = prop is RigidBody3D rb ? $"{rb.Mass:0.###} kg" : "static world";
|
||||||
|
GD.Print($"PropGrabber: grabbed '{prop.Name}' ({what}, {(hand == 0 ? "left" : "right")}{(_held[other] == prop ? ", two-handed" : "")})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!grip && _held[hand] != null)
|
if (!grip && _held[hand] != null)
|
||||||
{
|
{
|
||||||
RigidBody3D prop = _held[hand];
|
PhysicsBody3D prop = _held[hand];
|
||||||
_held[hand] = null;
|
_held[hand] = null;
|
||||||
DestroyJoint(hand);
|
DestroyJoint(hand);
|
||||||
if (IsInstanceValid(prop))
|
if (prop is RigidBody3D released && IsInstanceValid(released))
|
||||||
GD.Print($"PropGrabber: released '{prop.Name}' at {prop.LinearVelocity.Length():F2} m/s");
|
GD.Print($"PropGrabber: released '{released.Name}' at {released.LinearVelocity.Length():F2} m/s");
|
||||||
}
|
}
|
||||||
_gripPrev[hand] = grip;
|
_gripPrev[hand] = grip;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HoldUpdate(XRController3D controller, int hand, float delta)
|
private void HoldUpdate(XRController3D controller, int hand, float delta)
|
||||||
{
|
{
|
||||||
RigidBody3D body = _held[hand];
|
PhysicsBody3D body = _held[hand];
|
||||||
if (body == null)
|
if (body == null)
|
||||||
return;
|
return;
|
||||||
if (!IsInstanceValid(body))
|
if (!IsInstanceValid(body))
|
||||||
@@ -148,7 +151,7 @@ public partial class PropGrabber : Node
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int PrimaryHand(RigidBody3D body)
|
private int PrimaryHand(PhysicsBody3D body)
|
||||||
{
|
{
|
||||||
int primary = -1;
|
int primary = -1;
|
||||||
for (int hand = 0; hand < 2; hand++)
|
for (int hand = 0; hand < 2; hand++)
|
||||||
@@ -159,7 +162,7 @@ public partial class PropGrabber : Node
|
|||||||
return primary;
|
return primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ConeTwistJoint3D MakeWeld(RigidBody3D handBody, RigidBody3D prop, Vector3 pivot, float bias)
|
private ConeTwistJoint3D MakeWeld(RigidBody3D handBody, PhysicsBody3D prop, Vector3 pivot, float bias)
|
||||||
{
|
{
|
||||||
ConeTwistJoint3D weld = new()
|
ConeTwistJoint3D weld = new()
|
||||||
{
|
{
|
||||||
@@ -175,7 +178,7 @@ public partial class PropGrabber : Node
|
|||||||
return weld;
|
return weld;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BreakGrip(int hand, RigidBody3D body, string reason)
|
private void BreakGrip(int hand, PhysicsBody3D body, string reason)
|
||||||
{
|
{
|
||||||
if (_held[hand] != body)
|
if (_held[hand] != body)
|
||||||
return;
|
return;
|
||||||
@@ -199,7 +202,7 @@ public partial class PropGrabber : Node
|
|||||||
|
|
||||||
private static float PalmOffset(int hand) => hand == 0 ? PalmGrabOffset : -PalmGrabOffset;
|
private static float PalmOffset(int hand) => hand == 0 ? PalmGrabOffset : -PalmGrabOffset;
|
||||||
|
|
||||||
private RigidBody3D FindProp(XRController3D controller, int hand)
|
private PhysicsBody3D FindProp(XRController3D controller, int hand)
|
||||||
{
|
{
|
||||||
RigidBody3D handBody = PhysicsHands.GetBody(controller);
|
RigidBody3D handBody = PhysicsHands.GetBody(controller);
|
||||||
if (handBody == null)
|
if (handBody == null)
|
||||||
@@ -208,7 +211,27 @@ public partial class PropGrabber : Node
|
|||||||
RigidBody3D nearest = NearestProp(palm, GrabRadius);
|
RigidBody3D nearest = NearestProp(palm, GrabRadius);
|
||||||
if (nearest != null)
|
if (nearest != null)
|
||||||
return nearest;
|
return nearest;
|
||||||
return NearestProp(handBody.GlobalPosition, CenterGrabRadius);
|
nearest = NearestProp(handBody.GlobalPosition, CenterGrabRadius);
|
||||||
|
if (nearest != null)
|
||||||
|
return nearest;
|
||||||
|
return WorldUnderPalm(palm, handBody.GetWorld3D());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Climbing: any static body touching the palm (except the "no_grab" group) can be held.
|
||||||
|
private static StaticBody3D WorldUnderPalm(Vector3 palm, World3D world)
|
||||||
|
{
|
||||||
|
PhysicsShapeQueryParameters3D query = new()
|
||||||
|
{
|
||||||
|
Shape = new SphereShape3D { Radius = WorldGrabRadius },
|
||||||
|
Transform = new Transform3D(Basis.Identity, palm),
|
||||||
|
CollisionMask = 1,
|
||||||
|
};
|
||||||
|
foreach (Godot.Collections.Dictionary hit in world.DirectSpaceState.IntersectShape(query, 8))
|
||||||
|
{
|
||||||
|
if (hit["collider"].AsGodotObject() is StaticBody3D body && !body.IsInGroup(NoGrabGroup))
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private RigidBody3D NearestProp(Vector3 point, float radius)
|
private RigidBody3D NearestProp(Vector3 point, float radius)
|
||||||
|
|||||||
@@ -57,6 +57,17 @@ public partial class RagdollSpike : Node3D
|
|||||||
_phases.Add(("12 kg held at arm's length sags more", () => SetupWeight(close: false), _ => { }, 3f, () => FinishWeight(close: false)));
|
_phases.Add(("12 kg held at arm's length sags more", () => SetupWeight(close: false), _ => { }, 3f, () => FinishWeight(close: false)));
|
||||||
_phases.Add(("teleport + snap turn carry the body", Rest, TickTeleport, 2f, FinishTeleport));
|
_phases.Add(("teleport + snap turn carry the body", Rest, TickTeleport, 2f, FinishTeleport));
|
||||||
_phases.Add(("random motion soak", Rest, TickRandom, 8f, FinishSoak));
|
_phases.Add(("random motion soak", Rest, TickRandom, 8f, FinishSoak));
|
||||||
|
_phases.Add(("a knee-high box blocks walking (no stepping onto it)", () => SetupObstacle(0.6f), _ => _stickVelocity = new Vector3(0f, 0f, -1.5f), 3f, FinishBlockedLow));
|
||||||
|
_phases.Add(("a table can't be walked over", SetupTableObstacle, _ => _stickVelocity = new Vector3(0f, 0f, -1.5f), 3f, FinishBlockedLow));
|
||||||
|
_phases.Add(("walks up a 0.25 m step", () => SetupObstacle(0.25f, 8f), _ => _stickVelocity = new Vector3(0f, 0f, -1.2f), 3f, FinishStep));
|
||||||
|
_phases.Add(("two-handed mantle onto a 0.9 m ledge", () => SetupObstacle(0.9f), t => TickMantle(both: true), 6f, () => FinishMantle(true)));
|
||||||
|
_phases.Add(("one hand can't mantle", () => SetupObstacle(0.9f), t => TickMantle(both: false), 6f, () => FinishMantle(false)));
|
||||||
|
_phases.Add(("two-handed pull-up on a grabbed bar", () => SetupBarHang(), t => TickPullUp(both: true), 3.5f, () => FinishPullUp(true)));
|
||||||
|
_phases.Add(("one-handed pull-up is weaker", () => SetupBarHang(), t => TickPullUp(both: false), 3.5f, () => FinishPullUp(false)));
|
||||||
|
_phases.Add(("crawls through a 0.6 m tunnel", SetupTunnel, TickCrawl, 7f, FinishCrawl));
|
||||||
|
_phases.Add(("walks off a 1 m ledge and lands", SetupLedgeTop, _ => _stickVelocity = _t > 0.5f && _t < 2f ? new Vector3(0f, 0f, -1.2f) : Vector3.Zero, 4f, FinishDrop));
|
||||||
|
_phases.Add(("walks up an 18° ramp", () => SetupRamp(18f), _ => _stickVelocity = new Vector3(0f, 0f, -1.2f), 4f, () => FinishRamp(true)));
|
||||||
|
_phases.Add(("can't walk up a 40° ramp", () => SetupRamp(40f), _ => _stickVelocity = new Vector3(0f, 0f, -1.2f), 4f, () => FinishRamp(false)));
|
||||||
Next();
|
Next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,6 +459,198 @@ public partial class RagdollSpike : Node3D
|
|||||||
return _finite && NewSnaps == 0 && _maxEyeErr < 0.05f && _maxHandErr < 0.06f;
|
return _finite && NewSnaps == 0 && _maxEyeErr < 0.05f && _maxHandErr < 0.06f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- obstacles, mantling, climbing, crawling
|
||||||
|
|
||||||
|
private const float ObstacleFront = -0.55f;
|
||||||
|
|
||||||
|
// A box from ObstacleFront 2 m deep (wide), `height` tall.
|
||||||
|
private void SetupObstacle(float height, float depth = 2f)
|
||||||
|
{
|
||||||
|
Rest();
|
||||||
|
_stage.AddChild(MakeStatic(new Vector3(3f, height, depth), new Vector3(0f, height * 0.5f, ObstacleFront - depth * 0.5f), 29));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupTableObstacle()
|
||||||
|
{
|
||||||
|
Rest();
|
||||||
|
_stage.AddChild(MakeStatic(new Vector3(1.4f, 0.05f, 0.8f), new Vector3(0f, 0.725f, ObstacleFront - 0.4f), 29));
|
||||||
|
foreach (float x in new[] { -0.65f, 0.65f })
|
||||||
|
foreach (float z in new[] { ObstacleFront - 0.05f, ObstacleFront - 0.75f })
|
||||||
|
_stage.AddChild(MakeStatic(new Vector3(0.06f, 0.7f, 0.06f), new Vector3(x, 0.35f, z), 29));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool FinishBlockedLow()
|
||||||
|
{
|
||||||
|
GD.Print($"RAGDOLL: obstacle front z {ObstacleFront}: pelvis z {_ragdoll.Pelvis.GlobalPosition.Z:F3}, feet y {_ragdoll.GroundY:F3}, origin y {_origin.Origin.Y:F3}, camera min z {_minCamZ:F3}, snaps {NewSnaps}");
|
||||||
|
return _finite && NewSnaps == 0 && _ragdoll.GroundY < 0.1f && _origin.Origin.Y < 0.1f && _ragdoll.Pelvis.GlobalPosition.Z > ObstacleFront - 0.3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool FinishStep()
|
||||||
|
{
|
||||||
|
GD.Print($"RAGDOLL: step: feet y {_ragdoll.GroundY:F3}, origin y {_origin.Origin.Y:F3}, pelvis z {_ragdoll.Pelvis.GlobalPosition.Z:F3}, snaps {NewSnaps}");
|
||||||
|
return _finite && NewSnaps == 0 && Mathf.Abs(_ragdoll.GroundY - 0.25f) < 0.03f && _origin.Origin.Y > 0.2f && _ragdoll.Pelvis.GlobalPosition.Z < ObstacleFront - 0.3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float _maxPelvisRise, _lastRise;
|
||||||
|
|
||||||
|
// Put both hands on the ledge, press down (controllers sink as the real arms straighten), then
|
||||||
|
// walk forward onto it.
|
||||||
|
// Walk up to the ledge (the legs collider stops you), hands on top just ahead, press down.
|
||||||
|
private void TickMantle(bool both)
|
||||||
|
{
|
||||||
|
float t = _t;
|
||||||
|
_stickVelocity = t < 1f ? new Vector3(0f, 0f, -0.8f) : t > 3.5f ? new Vector3(0f, 0f, -0.8f) : Vector3.Zero;
|
||||||
|
float down = Mathf.Clamp((t - 1.6f) / 1.5f, 0f, 1f);
|
||||||
|
float y = t < 1.6f ? Mathf.Lerp(1.0f, 0.95f, Mathf.Clamp(t - 1f, 0f, 0.6f) / 0.6f) : Mathf.Lerp(0.95f, 0.35f, down);
|
||||||
|
// Hands just past the ledge edge (aimed in the room, like a real player looking at it).
|
||||||
|
float z = t < 1f ? -0.4f : ObstacleFront - 0.1f - _origin.Origin.Z;
|
||||||
|
Vector3 l = new(-0.22f, y, z), r = new(0.22f, y, z);
|
||||||
|
_rightLocal = new Transform3D(Aim(Vector3.Down + Vector3.Forward * 0.3f), r);
|
||||||
|
_leftLocal = both ? new Transform3D(Aim(Vector3.Down + Vector3.Forward * 0.3f), l) : new Transform3D(Basis.Identity, new Vector3(-0.2f, 1.0f, -0.3f));
|
||||||
|
if (t > 3.6f)
|
||||||
|
{
|
||||||
|
// Up and walking on: hands come back to the body like a real player's.
|
||||||
|
_leftLocal = new Transform3D(Basis.Identity, new Vector3(-0.2f, 1.0f, -0.3f));
|
||||||
|
_rightLocal = new Transform3D(Basis.Identity, new Vector3(0.2f, 1.0f, -0.3f));
|
||||||
|
}
|
||||||
|
if (_ragdoll.Built && !_needBuild)
|
||||||
|
_maxPelvisRise = Mathf.Max(_maxPelvisRise, _ragdoll.Pelvis.GlobalPosition.Y - 0.99f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool FinishMantle(bool both)
|
||||||
|
{
|
||||||
|
GD.Print($"RAGDOLL: mantle ({(both ? "two hands" : "one hand")}): pelvis rose up to {_maxPelvisRise:F3} m, feet y {_ragdoll.GroundY:F3}, origin y {_origin.Origin.Y:F3}, snaps {NewSnaps}");
|
||||||
|
bool onTop = _ragdoll.GroundY > 0.85f && _origin.Origin.Y > 0.8f;
|
||||||
|
_lastRise = _maxPelvisRise;
|
||||||
|
_maxPelvisRise = 0f;
|
||||||
|
return _finite && NewSnaps == 0 && (both ? onTop : !onTop && _lastRise < 0.25f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private StaticBody3D _bar;
|
||||||
|
private const float BarY = 1.95f, BarHangZ = -0.15f;
|
||||||
|
private float _barPelvisStart;
|
||||||
|
private bool _barGrabbed;
|
||||||
|
|
||||||
|
private void SetupBarHang()
|
||||||
|
{
|
||||||
|
Rest();
|
||||||
|
_bar = new StaticBody3D { Position = new Vector3(0f, BarY, BarHangZ), CollisionMask = 29 };
|
||||||
|
_bar.AddChild(new CollisionShape3D { Shape = new CylinderShape3D { Radius = 0.025f, Height = 2f }, RotationDegrees = new Vector3(0f, 0f, 90f) });
|
||||||
|
_stage.AddChild(_bar);
|
||||||
|
_barGrabbed = false;
|
||||||
|
_maxPelvisRise = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reach up to the bar, grip it (the PropGrabber weld), then pull the controllers down.
|
||||||
|
private void TickPullUp(bool both)
|
||||||
|
{
|
||||||
|
float t = _t;
|
||||||
|
float pull = Mathf.Clamp((t - 0.8f) / 1.5f, 0f, 1f);
|
||||||
|
float y = Mathf.Lerp(BarY - 0.03f, BarY - 0.75f, pull);
|
||||||
|
Basis grip = Aim(Vector3.Up + Vector3.Forward * 0.2f);
|
||||||
|
_rightLocal = new Transform3D(grip, new Vector3(0.22f, y, BarHangZ + 0.03f));
|
||||||
|
_leftLocal = both ? new Transform3D(grip, new Vector3(-0.22f, y, BarHangZ + 0.03f)) : new Transform3D(Basis.Identity, new Vector3(-0.2f, 1.0f, -0.3f));
|
||||||
|
if (!_barGrabbed && t > 0.6f && _ragdoll.Built)
|
||||||
|
{
|
||||||
|
_barGrabbed = true;
|
||||||
|
_barPelvisStart = _ragdoll.Pelvis.GlobalPosition.Y;
|
||||||
|
for (int side = both ? 0 : 1; side < 2; side++)
|
||||||
|
{
|
||||||
|
RigidBody3D hand = _ragdoll.Hand(side);
|
||||||
|
ConeTwistJoint3D weld = new() { Position = hand.GlobalPosition, NodeA = hand.GetPath(), NodeB = _bar.GetPath(), SwingSpan = 0f, TwistSpan = 0f, Bias = 0f };
|
||||||
|
_stage.AddChild(weld);
|
||||||
|
_ragdoll.SetHeld(side, _bar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_barGrabbed)
|
||||||
|
_maxPelvisRise = Mathf.Max(_maxPelvisRise, _ragdoll.Pelvis.GlobalPosition.Y - _barPelvisStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float _twoHandRise;
|
||||||
|
|
||||||
|
private bool FinishPullUp(bool both)
|
||||||
|
{
|
||||||
|
GD.Print($"RAGDOLL: pull-up ({(both ? "two hands" : "one hand")}): pelvis rose {_maxPelvisRise:F3} m, braced {_ragdoll.Braced}, snaps {NewSnaps}");
|
||||||
|
_ragdoll.SetHeld(0, null);
|
||||||
|
_ragdoll.SetHeld(1, null);
|
||||||
|
if (both)
|
||||||
|
{
|
||||||
|
_twoHandRise = _maxPelvisRise;
|
||||||
|
return _finite && NewSnaps == 0 && _maxPelvisRise > 0.25f;
|
||||||
|
}
|
||||||
|
return _finite && NewSnaps == 0 && _maxPelvisRise < _twoHandRise * 0.6f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private const float TunnelStart = -1.0f, TunnelEnd = -3.5f;
|
||||||
|
private float _minCamY;
|
||||||
|
|
||||||
|
private void SetupTunnel()
|
||||||
|
{
|
||||||
|
Rest();
|
||||||
|
float len = TunnelStart - TunnelEnd;
|
||||||
|
float mid = (TunnelStart + TunnelEnd) * 0.5f;
|
||||||
|
_stage.AddChild(MakeStatic(new Vector3(1.4f, 0.1f, len), new Vector3(0f, 0.65f, mid), 29));
|
||||||
|
_stage.AddChild(MakeStatic(new Vector3(0.1f, 0.6f, len), new Vector3(-0.65f, 0.3f, mid), 29));
|
||||||
|
_stage.AddChild(MakeStatic(new Vector3(0.1f, 0.6f, len), new Vector3(0.65f, 0.3f, mid), 29));
|
||||||
|
_minCamY = float.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get down on hands and knees (eyes ~0.4 m, looking ahead), then stick-crawl forward.
|
||||||
|
private void TickCrawl(float dt)
|
||||||
|
{
|
||||||
|
float k = Mathf.SmoothStep(0f, 1f, Mathf.Min(_t / 2f, 1f));
|
||||||
|
_headLocal = new Transform3D(new Basis(Vector3.Right, -0.35f * k), new Vector3(0f, Mathf.Lerp(EyeHeight, 0.42f, k), Mathf.Lerp(0f, -0.25f, k)));
|
||||||
|
_leftLocal = new Transform3D(Aim(Vector3.Forward + Vector3.Down), new Vector3(-0.2f, Mathf.Lerp(1.0f, 0.12f, k), Mathf.Lerp(-0.3f, -0.45f, k)));
|
||||||
|
_rightLocal = new Transform3D(Aim(Vector3.Forward + Vector3.Down), new Vector3(0.2f, Mathf.Lerp(1.0f, 0.12f, k), Mathf.Lerp(-0.3f, -0.45f, k)));
|
||||||
|
_stickVelocity = _t > 2.3f ? new Vector3(0f, 0f, -0.8f) : Vector3.Zero;
|
||||||
|
_measureFrom = float.MaxValue;
|
||||||
|
if (_t > 2.3f)
|
||||||
|
_minCamY = Mathf.Min(_minCamY, Camera.Origin.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool FinishCrawl()
|
||||||
|
{
|
||||||
|
GD.Print($"RAGDOLL: crawl: camera min z {_minCamZ:F3} (tunnel {TunnelStart}..{TunnelEnd}), eye y {_ragdoll.Eye.Y:F2}, pelvis y {_ragdoll.Pelvis.GlobalPosition.Y:F2}, origin y {_origin.Origin.Y:F3}, snaps {NewSnaps}");
|
||||||
|
return _finite && NewSnaps == 0 && _minCamZ < TunnelStart - 1.5f && Mathf.Abs(_origin.Origin.Y) < 0.1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Start standing on a 1 m platform whose edge is 0.5 m ahead.
|
||||||
|
private void SetupLedgeTop()
|
||||||
|
{
|
||||||
|
Rest();
|
||||||
|
_origin = new Transform3D(Basis.Identity, new Vector3(0f, 1f, 0f));
|
||||||
|
_stage.AddChild(MakeStatic(new Vector3(3f, 1f, 3f), new Vector3(0f, 0.5f, 1f), 29));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool FinishDrop()
|
||||||
|
{
|
||||||
|
GD.Print($"RAGDOLL: drop: feet y {_ragdoll.GroundY:F3}, origin y {_origin.Origin.Y:F3}, pelvis {_ragdoll.Pelvis.GlobalPosition}, eye error {_ragdoll.Eye.DistanceTo(Camera.Origin):F3}, snaps {NewSnaps}");
|
||||||
|
return _finite && NewSnaps == 0 && Mathf.Abs(_ragdoll.GroundY) < 0.05f && Mathf.Abs(_origin.Origin.Y) < 0.1f
|
||||||
|
&& _ragdoll.Eye.DistanceTo(Camera.Origin) < 0.1f && _ragdoll.Pelvis.GlobalPosition.Z > -3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A slope rising away (−Z) from ObstacleFront up to 1 m, then a platform.
|
||||||
|
private void SetupRamp(float degrees)
|
||||||
|
{
|
||||||
|
Rest();
|
||||||
|
float a = Mathf.DegToRad(degrees);
|
||||||
|
float len = 1f / Mathf.Sin(a);
|
||||||
|
float run = Mathf.Cos(a) * len;
|
||||||
|
_stage.AddChild(new StaticBody3D { CollisionMask = 29, Transform = new Transform3D(new Basis(Vector3.Right, a), new Vector3(0f, 0.5f - 0.05f / Mathf.Cos(a), ObstacleFront - run * 0.5f)) });
|
||||||
|
_stage.GetChild<StaticBody3D>(_stage.GetChildCount() - 1).AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(3f, 0.1f, len) } });
|
||||||
|
_stage.AddChild(MakeStatic(new Vector3(3f, 1f, 3f), new Vector3(0f, 0.5f, ObstacleFront - run - 1.5f), 29));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool FinishRamp(bool walkable)
|
||||||
|
{
|
||||||
|
GD.Print($"RAGDOLL: ramp: feet y {_ragdoll.GroundY:F3}, origin y {_origin.Origin.Y:F3}, pelvis {_ragdoll.Pelvis.GlobalPosition}, snaps {NewSnaps}");
|
||||||
|
bool up = _ragdoll.GroundY > 0.6f;
|
||||||
|
// Stopped at the slope's foot: on the floor, view level, not flung away.
|
||||||
|
bool stopped = Mathf.Abs(_origin.Origin.Y) < 0.1f && _ragdoll.Pelvis.GlobalPosition.Length() < 2.5f;
|
||||||
|
return _finite && NewSnaps == 0 && (walkable ? up : !up && stopped);
|
||||||
|
}
|
||||||
|
|
||||||
private readonly RandomNumberGenerator _rng = new() { Seed = 11 };
|
private readonly RandomNumberGenerator _rng = new() { Seed = 11 };
|
||||||
private Vector3 _ra, _rb, _rc;
|
private Vector3 _ra, _rb, _rc;
|
||||||
|
|
||||||
|
|||||||
+24
-11
@@ -14,6 +14,10 @@ public sealed class TrueBodySolver
|
|||||||
public const float ChestLeanMax = 0.75f; // rad (~43°) at a deep crouch
|
public const float ChestLeanMax = 0.75f; // rad (~43°) at a deep crouch
|
||||||
public const float CrouchRange = 0.5f; // crouch factor 1 when eyes are this fraction of standing eye height lower
|
public const float CrouchRange = 0.5f; // crouch factor 1 when eyes are this fraction of standing eye height lower
|
||||||
public const float HeadPitchToChest = 0.25f;
|
public const float HeadPitchToChest = 0.25f;
|
||||||
|
// Crawl: when the head gets so low that the hips would go through the floor, the trunk tips
|
||||||
|
// further (pivoting at the hips, hips kicking back) up to nearly horizontal (Nimso: lower = crawl).
|
||||||
|
public const float CrawlLeanMax = 1.45f; // rad (~83°)
|
||||||
|
public const float HipMinHeight = 0.15f; // hip joint above the floor when crawling
|
||||||
public const float PelvisFrequency = 2.5f; // Hz, critically damped follow → hip lag/sway
|
public const float PelvisFrequency = 2.5f; // Hz, critically damped follow → hip lag/sway
|
||||||
public const float GirdleReachStart = 0.98f; // fraction of arm length where the shoulder starts to help
|
public const float GirdleReachStart = 0.98f; // fraction of arm length where the shoulder starts to help
|
||||||
public const float GirdleSmoothRate = 15f;
|
public const float GirdleSmoothRate = 15f;
|
||||||
@@ -100,18 +104,27 @@ public sealed class TrueBodySolver
|
|||||||
Crouch = Mathf.Clamp((standingPivot - pivotAboveFloor) / (CrouchRange * p.EyeHeight), 0f, 1f);
|
Crouch = Mathf.Clamp((standingPivot - pivotAboveFloor) / (CrouchRange * p.EyeHeight), 0f, 1f);
|
||||||
float headPitchDown = Mathf.Max(0f, -Mathf.Asin(Mathf.Clamp(headFwd.Y, -1f, 1f)));
|
float headPitchDown = Mathf.Max(0f, -Mathf.Asin(Mathf.Clamp(headFwd.Y, -1f, 1f)));
|
||||||
float lean = Crouch * ChestLeanMax + headPitchDown * HeadPitchToChest;
|
float lean = Crouch * ChestLeanMax + headPitchDown * HeadPitchToChest;
|
||||||
Lean = lean;
|
float trunk = TrunkRatio * p.Stature;
|
||||||
ChestBasis = yawBasis * new Basis(Vector3.Right, -lean);
|
|
||||||
|
|
||||||
// Neck: from the head pivot down along the average of chest-up and head-up, sized so a
|
|
||||||
// standing, level head puts the shoulder joints at the measured drop below the eyes.
|
|
||||||
float neckLen = Mathf.Max(0.05f, p.ShoulderDrop + EyeToNeckPivot.Y - NeckBaseAboveShoulders);
|
float neckLen = Mathf.Max(0.05f, p.ShoulderDrop + EyeToNeckPivot.Y - NeckBaseAboveShoulders);
|
||||||
Vector3 neckDir = (ChestBasis.Y + headBasis.Y).Normalized();
|
Vector3 neckDir = Vector3.Up;
|
||||||
NeckBase = NeckPivot - neckDir * neckLen;
|
for (int iter = 0; iter < 3; iter++)
|
||||||
// Keep the shoulder line behind the eyes by the anthropometric offset (neck pivot already
|
{
|
||||||
// supplies part of it).
|
ChestBasis = yawBasis * new Basis(Vector3.Right, -lean);
|
||||||
UpperChest = NeckBase - ChestBasis.Y * NeckBaseAboveShoulders
|
// Neck: from the head pivot down along the average of chest-up and head-up, sized so a
|
||||||
+ ChestBasis * new Vector3(0f, 0f, ShouldersBehindEyes - EyeToNeckPivot.Z);
|
// standing, level head puts the shoulder joints at the measured drop below the eyes.
|
||||||
|
neckDir = (ChestBasis.Y + headBasis.Y).Normalized();
|
||||||
|
NeckBase = NeckPivot - neckDir * neckLen;
|
||||||
|
// Keep the shoulder line behind the eyes by the anthropometric offset (neck pivot already
|
||||||
|
// supplies part of it).
|
||||||
|
UpperChest = NeckBase - ChestBasis.Y * NeckBaseAboveShoulders
|
||||||
|
+ ChestBasis * new Vector3(0f, 0f, ShouldersBehindEyes - EyeToNeckPivot.Z);
|
||||||
|
float room = (UpperChest.Y - floorY - HipMinHeight) / trunk;
|
||||||
|
float crawl = Mathf.Acos(Mathf.Clamp(room, Mathf.Cos(CrawlLeanMax), 1f));
|
||||||
|
if (crawl <= lean + 1e-3f)
|
||||||
|
break;
|
||||||
|
lean = crawl;
|
||||||
|
}
|
||||||
|
Lean = lean;
|
||||||
|
|
||||||
// Pelvis: hangs below the upper chest along the trunk; follows through a critically damped
|
// Pelvis: hangs below the upper chest along the trunk; follows through a critically damped
|
||||||
// spring so it lags quick head motion (hip sway) instead of being rigidly attached.
|
// spring so it lags quick head motion (hip sway) instead of being rigidly attached.
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ public partial class WorldPanel : Node3D
|
|||||||
_menu.ResetPropsRequested += () =>
|
_menu.ResetPropsRequested += () =>
|
||||||
{
|
{
|
||||||
_propShop.Respawn();
|
_propShop.Respawn();
|
||||||
|
GetNodeOrNull<Playground>("../../Playground")?.Reset();
|
||||||
_menu.Log("Props reset");
|
_menu.Log("Props reset");
|
||||||
};
|
};
|
||||||
_menu.PingRequested += () =>
|
_menu.PingRequested += () =>
|
||||||
@@ -75,6 +76,9 @@ public partial class WorldPanel : Node3D
|
|||||||
_click = MakeTone(900f, 25, 0.35f, 1f);
|
_click = MakeTone(900f, 25, 0.35f, 1f);
|
||||||
_ping = MakeTone(1320f, 4, 0.3f, 3f);
|
_ping = MakeTone(1320f, 4, 0.3f, 3f);
|
||||||
_menu.ClickVolumeSlider.ValueChanged += v => _click.VolumeDb = Mathf.LinearToDb((float)v / 70f);
|
_menu.ClickVolumeSlider.ValueChanged += v => _click.VolumeDb = Mathf.LinearToDb((float)v / 70f);
|
||||||
|
_menu.StrengthChanged += v => PlayerRagdoll.Strength = v;
|
||||||
|
_menu.SmoothTurnToggled += on => PlayerRig.SmoothTurn = on;
|
||||||
|
_menu.SpectatorToggled += PlayerRig.SetSpectator;
|
||||||
|
|
||||||
SetPanelActive(false);
|
SetPanelActive(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using Godot;
|
||||||
|
|
||||||
|
// Dev tool (not shipped gameplay): renders a few overview screenshots of the playground to
|
||||||
|
// user-supplied paths. godot-mono --path . tools/playground_shots.tscn -- <outdir>
|
||||||
|
public partial class PlaygroundShots : Node3D
|
||||||
|
{
|
||||||
|
private int _frame;
|
||||||
|
private int _shot;
|
||||||
|
private Camera3D _cam;
|
||||||
|
private string _dir = "user://";
|
||||||
|
private readonly (Vector3 Pos, Vector3 Look)[] _views =
|
||||||
|
{
|
||||||
|
(new Vector3(0f, 9f, 14f), new Vector3(0f, 0f, -1f)),
|
||||||
|
(new Vector3(3f, 4f, 3f), new Vector3(9f, 1f, -4f)),
|
||||||
|
(new Vector3(-2f, 3.5f, 4f), new Vector3(-7f, 1f, -1f)),
|
||||||
|
(new Vector3(0f, 2.5f, 11f), new Vector3(1f, 0.6f, 7f)),
|
||||||
|
};
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
foreach (string a in OS.GetCmdlineUserArgs())
|
||||||
|
_dir = a;
|
||||||
|
AddChild(new DirectionalLight3D { RotationDegrees = new Vector3(-50f, -30f, 0f), ShadowEnabled = true });
|
||||||
|
AddChild(new WorldEnvironment { Environment = new Environment { BackgroundMode = Environment.BGMode.Color, BackgroundColor = new Color(0.55f, 0.65f, 0.8f), AmbientLightSource = Environment.AmbientSource.Color, AmbientLightColor = new Color(0.5f, 0.5f, 0.55f) } });
|
||||||
|
StaticBody3D floor = new();
|
||||||
|
floor.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(40, 0.1f, 40) }, Position = new Vector3(0, -0.05f, 0) });
|
||||||
|
floor.AddChild(new MeshInstance3D { Mesh = new PlaneMesh { Size = new Vector2(40, 40) } });
|
||||||
|
AddChild(floor);
|
||||||
|
AddChild(new Playground());
|
||||||
|
_cam = new Camera3D { Current = true };
|
||||||
|
AddChild(_cam);
|
||||||
|
SetView();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetView() => _cam.LookAtFromPosition(_views[_shot].Pos, _views[_shot].Look);
|
||||||
|
|
||||||
|
public override void _Process(double delta)
|
||||||
|
{
|
||||||
|
if (++_frame < 30)
|
||||||
|
return;
|
||||||
|
GetViewport().GetTexture().GetImage().SavePng($"{_dir}/view{_shot}.png");
|
||||||
|
_frame = 0;
|
||||||
|
if (++_shot >= _views.Length)
|
||||||
|
{
|
||||||
|
GetTree().Quit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SetView();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ceamhylc4wq66
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://tools/PlaygroundShots.cs" id="1"]
|
||||||
|
|
||||||
|
[node name="PlaygroundShots" type="Node3D"]
|
||||||
|
script = ExtResource("1")
|
||||||
Reference in new issue
Block a user