@woosh/meep-engine 2.141.0 → 2.143.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/geom/3d/shape/CapsuleShape3D.d.ts +1 -1
- package/src/core/geom/3d/shape/CapsuleShape3D.js +1 -1
- package/src/core/geom/3d/shape/SphereShape3D.d.ts +47 -0
- package/src/core/geom/3d/shape/SphereShape3D.d.ts.map +1 -0
- package/src/core/geom/3d/shape/SphereShape3D.js +127 -0
- package/src/core/geom/3d/shape/UnitSphereShape3D.d.ts +30 -18
- package/src/core/geom/3d/shape/UnitSphereShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/UnitSphereShape3D.js +44 -92
- package/src/core/geom/3d/shape/json/shape_to_type.d.ts.map +1 -1
- package/src/core/geom/3d/shape/json/shape_to_type.js +4 -2
- package/src/core/geom/3d/shape/json/type_adapters.d.ts +12 -3
- package/src/core/geom/3d/shape/json/type_adapters.d.ts.map +1 -1
- package/src/core/geom/3d/shape/json/type_adapters.js +16 -4
- package/src/core/geom/3d/shape/util/shape_to_visual_entity.js +2 -2
- package/src/engine/control/first-person/DESIGN_COLLISION.md +255 -0
- package/src/engine/control/first-person/prototype_first_person_controller.js +5 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +70 -43
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.d.ts +12 -22
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.d.ts.map +1 -1
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.js +345 -186
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.d.ts +44 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.d.ts.map +1 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.js +151 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.d.ts +14 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.d.ts.map +1 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.js +78 -0
- package/src/engine/physics/PLAN.md +705 -578
- package/src/engine/physics/REVIEW_003.md +166 -0
- package/src/engine/physics/constraint/solve_constraints.d.ts +24 -2
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
- package/src/engine/physics/constraint/solve_constraints.js +402 -165
- package/src/engine/physics/ecs/Joint.d.ts +115 -0
- package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
- package/src/engine/physics/ecs/Joint.js +168 -0
- package/src/engine/physics/ecs/JointSerializationAdapter.d.ts +29 -0
- package/src/engine/physics/ecs/JointSerializationAdapter.d.ts.map +1 -0
- package/src/engine/physics/ecs/JointSerializationAdapter.js +72 -0
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +20 -13
- package/src/engine/physics/narrowphase/ray_shapes.d.ts +66 -0
- package/src/engine/physics/narrowphase/ray_shapes.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/ray_shapes.js +187 -0
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +16 -0
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/refine_ray_concave.js +145 -0
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +39 -0
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/refine_ray_hit.js +78 -0
- package/src/engine/physics/narrowphase/sphere_sphere_contact.d.ts +8 -7
- package/src/engine/physics/narrowphase/sphere_sphere_contact.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/sphere_sphere_contact.js +8 -7
- package/src/engine/physics/queries/raycast.d.ts +11 -9
- package/src/engine/physics/queries/raycast.d.ts.map +1 -1
- package/src/engine/physics/queries/raycast.js +108 -159
- package/src/engine/physics/vehicle/RaycastVehicle.d.ts +114 -0
- package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -0
- package/src/engine/physics/vehicle/RaycastVehicle.js +333 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# Collision handling — construction plan
|
|
2
|
+
|
|
3
|
+
Companion to DESIGN.md (base controller) and TODO.md. This is the
|
|
4
|
+
roadmap for taking the controller's collision response from the current
|
|
5
|
+
2.5D split to a unified, slope-aware, anti-tunnelling mover modelled on
|
|
6
|
+
the Quake → Source `TryPlayerMove` / `CategorizePosition` lineage —
|
|
7
|
+
using the now-accurate `shapeCast` / `raycast` the physics engine
|
|
8
|
+
gained over the P1–P6 narrowphase work.
|
|
9
|
+
|
|
10
|
+
Every phase is **guard-test-first**: write the failing spec that pins
|
|
11
|
+
the behaviour, then implement until green, then run the full
|
|
12
|
+
first-person suite. Phases are ordered so the controller stays shippable
|
|
13
|
+
and tested after each one — no flag day.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Goal & non-goals
|
|
18
|
+
|
|
19
|
+
**Goal.** A single swept move-and-slide that handles walls, floors,
|
|
20
|
+
ceilings, ramps, and getting unstuck — with true surface normals — so
|
|
21
|
+
abilities (Slide, WallRun, Mantle) inherit correct collision for free
|
|
22
|
+
by routing their motion through it.
|
|
23
|
+
|
|
24
|
+
**Non-goals.**
|
|
25
|
+
- Bunny-hop / strafe-jump emergent acceleration. We deliberately
|
|
26
|
+
*preserve* momentum (Mirror's Edge / modern-CoD model), not *amplify*
|
|
27
|
+
it (Quake / Titanfall model). See the momentum note in DESIGN.md.
|
|
28
|
+
- A full dynamics solver for the player. The player stays a
|
|
29
|
+
`KinematicPosition` body; the controller owns the Transform.
|
|
30
|
+
- Replacing the abilities' bespoke motion models — only giving them a
|
|
31
|
+
correct shared mover to call.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 2. Current state (one paragraph)
|
|
36
|
+
|
|
37
|
+
`_integrateVerticalAndResolveGround` does a **2.5D split**: gravity →
|
|
38
|
+
horizontal-only swept slide (`_moveAndSlide`, 4-iteration collide-and-
|
|
39
|
+
slide, `SKIN = 0.005`, `CAST_STEP_HEIGHT = 0.05`) → a *direct* vertical
|
|
40
|
+
position add → a downward-ray ground resolver that snaps a scalar Y.
|
|
41
|
+
The slide projects against true narrowphase normals (good), but: the
|
|
42
|
+
ground resolver discards the surface normal (no slopes), vertical motion
|
|
43
|
+
isn't swept (tunnels through thin floors, no ceiling detection), corners
|
|
44
|
+
re-project planes independently (no crease/dead-stop), and there's no
|
|
45
|
+
depenetration. Full analysis in the review that preceded this doc.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 3. Physics capabilities we can now lean on
|
|
50
|
+
|
|
51
|
+
The P1–P6 narrowphase + query work made the two primitives we depend on
|
|
52
|
+
narrowphase-exact. This plan is only feasible because of it.
|
|
53
|
+
|
|
54
|
+
| Capability | Status | Source |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `raycast` exact surface **normal** (sphere/box/capsule/mesh/heightmap) | ✅ landed | refine_ray_hit P1–P3 (`6f931b4`, `5cc2e3e`, `1a2a602`, `4c2292c`) |
|
|
57
|
+
| `shapeCast` true contact **normal at TOI** via MPR (not broadphase −dir) | ✅ landed | MPR normal-recovery in `shape_cast.js` |
|
|
58
|
+
| `shapeCast` **start-in-contact** → returns `t=0` + valid normal | ✅ landed | `shape_cast.js:195`, normal probe `:350` |
|
|
59
|
+
| `shapeCast` analytic slab-narrowing (long-sweep accuracy/perf) | ✅ landed | `shape_cast.js:203` |
|
|
60
|
+
| EPA adaptive tolerance (no silent sub-mm failure) | ✅ landed | P2.2 (`8ff15a6`) |
|
|
61
|
+
| MPR fallback when EPA doesn't converge | ✅ landed | P3.1 (`419b94a`) |
|
|
62
|
+
| box-box edge-edge closest-pair (not centre-midpoint) | ✅ landed | P3.2 (`419b94a`) |
|
|
63
|
+
| capsule-vs-triangle closed-form multi-point manifold | ✅ landed | P1.1c (`eaa75fa`) |
|
|
64
|
+
| GJK separating-axis cache (cheap warm casts) | ✅ landed | P2.1 (`60ca759`) |
|
|
65
|
+
| `overlap(shape,pos,rot,out,off,filter)` → body-id list | ✅ landed | `overlap_shape.js` |
|
|
66
|
+
| `compute_penetration(...)` → depth + B→A direction | ⚠️ **internal-only** | `compute_penetration.js` — not on PhysicsSystem |
|
|
67
|
+
|
|
68
|
+
Query contract notes the plan relies on:
|
|
69
|
+
- `shapeCast` `result.t` is a **world distance** when the ray direction
|
|
70
|
+
is unit-length and `tMax = sweepLength` (how `_moveAndSlide` already
|
|
71
|
+
uses it). New casts must keep that convention.
|
|
72
|
+
- `result.position` is the **swept shape centre**, not the surface
|
|
73
|
+
contact point. We never need the contact point in this plan; flagged
|
|
74
|
+
in case edge/foot placement ever does.
|
|
75
|
+
- `result.normal` is the **target's outward** surface normal.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 4. Target architecture
|
|
80
|
+
|
|
81
|
+
Replace the 2.5D split with a **unified swept solve + ground
|
|
82
|
+
categorize**, the Source pattern:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
move(dt):
|
|
86
|
+
applyGravityToVelocity() # vy -= g·dt
|
|
87
|
+
delta = velocity · dt # full 3D, gravity folded in
|
|
88
|
+
collideAndSlide(delta) # swept, all axes, crease-aware
|
|
89
|
+
categorizeGround() # short down-probe → grounded + normal
|
|
90
|
+
if grounded and walkable: stayOnGround() # snap to surface, zero vy
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Three pieces:
|
|
94
|
+
|
|
95
|
+
1. **`collideAndSlide(delta)`** — the existing 4-iteration loop,
|
|
96
|
+
generalised to 3D and made crease-aware (two-plane seam projection,
|
|
97
|
+
three-plane dead-stop). Gravity is part of `delta`, so floors and
|
|
98
|
+
ceilings are just contact planes — anti-tunnelling falls out.
|
|
99
|
+
|
|
100
|
+
2. **`categorizeGround()`** — a short **downward `shapeCast`** (the
|
|
101
|
+
capsule swept down by `SKIN + groundProbeBand`, e.g. ~6 cm). Returns
|
|
102
|
+
`{grounded, surfaceY, normal}`. Replaces the scalar-Y resolver as the
|
|
103
|
+
authoritative ground source when physics is present; the host
|
|
104
|
+
resolver / flat-ground stays as the no-physics fallback, behind one
|
|
105
|
+
`_probeGround()` function so callers never branch (uniform flow).
|
|
106
|
+
|
|
107
|
+
3. **`stayOnGround()`** — when categorize says grounded on a walkable
|
|
108
|
+
plane, snap `position.y` to `surfaceY` and zero `vy`. This is what
|
|
109
|
+
kills the SKIN-vs-snap **bounce** (the bug we hit earlier): grounded
|
|
110
|
+
is a *band test + active snap*, not a strict `y <= testY`
|
|
111
|
+
inequality, so landing at `floor + SKIN` doesn't re-flag airborne.
|
|
112
|
+
|
|
113
|
+
Walkability gate: `groundNormal.y >= MIN_WALK_NORMAL` (≈0.7, ~45°). On a
|
|
114
|
+
steeper face the player isn't grounded — they slide, because the slope
|
|
115
|
+
is just another clip plane in `collideAndSlide` and gravity carries them
|
|
116
|
+
down it.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 5. Phased plan
|
|
121
|
+
|
|
122
|
+
### Phase 1 — Ground normal + slope policy
|
|
123
|
+
*Review gap #1. Highest feel/realism leverage; lowest risk.*
|
|
124
|
+
|
|
125
|
+
- Add `_probeGround(runtime, bodyTransform) → {grounded, surfaceY, normal}`.
|
|
126
|
+
Physics path: downward `shapeCast` of the capsule, read `hit.normal`.
|
|
127
|
+
No-physics path: existing resolver / flat-ground, normal = `(0,1,0)`.
|
|
128
|
+
- Write `state.groundNormal`. Add `MIN_WALK_NORMAL` to config.
|
|
129
|
+
- Reproject grounded horizontal velocity onto the ground plane
|
|
130
|
+
(`v -= (v·n)·n`) so ramps don't launch (downhill) or clip (uphill).
|
|
131
|
+
- Steep face → not grounded → slides under gravity.
|
|
132
|
+
|
|
133
|
+
**Guard tests** (new `GroundSlope.spec.js`): walk up a 30° ramp keeps
|
|
134
|
+
speed and stays grounded; stand on shallow slope = grounded with correct
|
|
135
|
+
normal; 60° face = not grounded, player slides downhill, not glued.
|
|
136
|
+
|
|
137
|
+
**Risk:** low. Self-contained; resolver fallback unchanged for existing
|
|
138
|
+
tests.
|
|
139
|
+
|
|
140
|
+
### Phase 2 — Crease / corner handling
|
|
141
|
+
*Review gap #3. Cheap correctness win in the existing loop.*
|
|
142
|
+
|
|
143
|
+
- Track planes hit within one `_moveAndSlide` call. On a second plane
|
|
144
|
+
that re-violates the first, project the residual onto the seam
|
|
145
|
+
`normalize(n₁ × n₂)`. On a third plane in one frame, zero the residual
|
|
146
|
+
(and the into-wall velocity) — Quake's dead-stop.
|
|
147
|
+
|
|
148
|
+
**Guard tests** (extend `MoveAndSlide.spec.js`): oblique (non-axis)
|
|
149
|
+
inside corner stops cleanly with no residual leak and no tick-to-tick
|
|
150
|
+
chatter; narrow wedge dead-stops instead of squirting through.
|
|
151
|
+
|
|
152
|
+
**Risk:** low–medium. Pure addition to the slide loop; existing
|
|
153
|
+
single-plane tests must stay green.
|
|
154
|
+
|
|
155
|
+
### Phase 3 — Unify vertical into the swept solve
|
|
156
|
+
*Review gap #2 (anti-tunnel). The structural change; do it after 1 & 2
|
|
157
|
+
so slope + crease are already trustworthy.*
|
|
158
|
+
|
|
159
|
+
- Fold gravity-driven vertical into the swept `delta`; remove the direct
|
|
160
|
+
`position.y += vy·dt` add.
|
|
161
|
+
- Replace the strict-inequality resolver snap with
|
|
162
|
+
`categorizeGround()` + `stayOnGround()` (§4).
|
|
163
|
+
- Route Slide / WallRun vertical through the same mover (TODO's "ability
|
|
164
|
+
motion routing" — they stop hand-integrating gravity).
|
|
165
|
+
|
|
166
|
+
**Guard tests** (new `VerticalSweep.spec.js`): drop from height onto a
|
|
167
|
+
thin physics slab — no tunnelling, lands on top; jump straight into a
|
|
168
|
+
ceiling — stops, doesn't pass through; **the stationary-on-floor case
|
|
169
|
+
stays at y≈0 across 120 ticks** (generalise the earlier no-bounce repro,
|
|
170
|
+
now without an escape pass to mask it).
|
|
171
|
+
|
|
172
|
+
**Risk:** medium–high. Changes the contract `_integrate…` exposes to
|
|
173
|
+
abilities; they're updated in the same phase. This is the phase most
|
|
174
|
+
likely to surface a physics gap (see §6).
|
|
175
|
+
|
|
176
|
+
### Phase 4 — Depenetration safety net
|
|
177
|
+
*Review gap #4. Minimal, not the rolled-back ring-buffer escape.*
|
|
178
|
+
|
|
179
|
+
- One bounded nudge: if the capsule starts a tick overlapping (detect
|
|
180
|
+
via `shapeCast` `t=0`, or `overlap()`), push out along the contact
|
|
181
|
+
normal by `depth + SKIN`, capped at ~4 iterations. No ring buffer, no
|
|
182
|
+
teleport, no rescue signal — just don't stay solid.
|
|
183
|
+
- **Blocked on §6 requirement #1** for a clean depth. Without it, the
|
|
184
|
+
fallback is a fixed-step nudge along the normal (no exact depth),
|
|
185
|
+
which is cruder but unblocks the phase.
|
|
186
|
+
|
|
187
|
+
**Guard tests** (new `Depenetration.spec.js`): spawn the player
|
|
188
|
+
straddling a box surface → within a few ticks they're outside; a body
|
|
189
|
+
that spawns on top of a standing player → player ends up clear; deep
|
|
190
|
+
burial terminates (no infinite loop), doesn't teleport.
|
|
191
|
+
|
|
192
|
+
**Risk:** low if requirement #1 lands; medium otherwise (cruder nudge
|
|
193
|
+
needs more tuning).
|
|
194
|
+
|
|
195
|
+
### Phase 5 — Real step-up *(optional / deferrable)*
|
|
196
|
+
*Review gap #5.*
|
|
197
|
+
|
|
198
|
+
- Source-style `raise → move → trace-down` for stairs above the 5 cm
|
|
199
|
+
implicit step, OR keep stairs as the Mantle ability's job and document
|
|
200
|
+
the curb ceiling. Likely **defer** — Mantle already covers ledges; a
|
|
201
|
+
decision, not necessarily code.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 6. Unsatisfied physics requirements
|
|
206
|
+
|
|
207
|
+
Things I'd want from the physics engine to build §5 cleanly. None block
|
|
208
|
+
Phases 1–3; #1 shapes Phase 4.
|
|
209
|
+
|
|
210
|
+
1. **Public penetration/contact query — `PhysicsSystem.computePenetration`
|
|
211
|
+
(or `contact`).** `compute_penetration(out_dir, shapeA, posA, rotA,
|
|
212
|
+
shapeB, posB, rotB) → depth` already exists and is exactly right, but
|
|
213
|
+
it's an **internal narrowphase utility, not exposed** on
|
|
214
|
+
`PhysicsSystem`. Phase 4 depenetration wants `{normal, depth}` for the
|
|
215
|
+
player capsule vs each overlapping body. Today I can get the *fact* of
|
|
216
|
+
overlap (`overlap()` → ids) and a *normal* (`shapeCast` `t=0`), but
|
|
217
|
+
**not the depth** — `shapeCast` discards it at `t=0`. Wiring this
|
|
218
|
+
through is small (the function is written and tested) and is the
|
|
219
|
+
single highest-value API add for the controller. **Priority: HIGH.**
|
|
220
|
+
|
|
221
|
+
2. **`overlap()` that can optionally emit per-body `{normal, depth}`,
|
|
222
|
+
not just ids.** Subsumed by #1 if I call `computePenetration` per
|
|
223
|
+
returned id — acceptable. Only worth doing natively if profiling
|
|
224
|
+
shows the per-body re-query matters. **Priority: LOW** (covered by #1).
|
|
225
|
+
|
|
226
|
+
3. **Contact *point* (not just swept-centre) from `shapeCast`.** Not
|
|
227
|
+
needed for this plan, but any future foot-placement / ledge-edge or
|
|
228
|
+
multi-point ground balancing wants the world contact point.
|
|
229
|
+
`result.position` is the swept centre today. **Priority: LOW.**
|
|
230
|
+
|
|
231
|
+
4. **Multi-contact ground manifold from one query.** `shapeCast` returns
|
|
232
|
+
the single closest-`t` contact. A capsule straddling the seam between
|
|
233
|
+
two boxes gets one normal, not both — fine for v1 grounded snap, a
|
|
234
|
+
limitation for precise edge-balancing. **Priority: LOW.**
|
|
235
|
+
|
|
236
|
+
If only one thing gets built: **#1**. It turns Phase 4 from "tune a
|
|
237
|
+
blind nudge" into "push out by the exact reported depth," and it's
|
|
238
|
+
mostly plumbing over code that already exists and is tested.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 7. Constants (ours ← reference lineage)
|
|
243
|
+
|
|
244
|
+
| Constant | Plan value | Reference | Note |
|
|
245
|
+
|---|---|---|---|
|
|
246
|
+
| slide iterations | 4 | Quake `numbumps` 4 | unchanged |
|
|
247
|
+
| `SKIN` | 0.005 m | Fauerby `veryCloseDistance` ~0.005 | unchanged |
|
|
248
|
+
| `MIN_WALK_NORMAL` | ~0.7 (≈45°) | Quake3 `MIN_WALK_NORMAL` 0.7 / Source `normal.z ≥ 0.7` | **new, Phase 1** |
|
|
249
|
+
| ground-probe band | ~0.06 m | Source `StayOnGround` ~2 u down-snap | **new, Phase 3** |
|
|
250
|
+
| crease dead-stop | zero on 3rd plane | Quake `SV_FlyMove` | **new, Phase 2** |
|
|
251
|
+
| step height | 0.05 m (implicit) | Source `sv_stepsize` 18 u (~0.34 m) | Phase 5 decision |
|
|
252
|
+
|
|
253
|
+
Overbounce: we keep the clip at effective `1.0` gated on into-wall
|
|
254
|
+
(`v·n < 0`), relying on `SKIN` for clearance, rather than Quake3's
|
|
255
|
+
`OVERCLIP 1.001` nudge. No change planned — equivalent in practice.
|
|
@@ -16,6 +16,9 @@ import { CameraSystem } from "../../graphics/ecs/camera/CameraSystem.js";
|
|
|
16
16
|
import { ShadedGeometry } from "../../graphics/ecs/mesh-v2/ShadedGeometry.js";
|
|
17
17
|
import { ShadedGeometryFlags } from "../../graphics/ecs/mesh-v2/ShadedGeometryFlags.js";
|
|
18
18
|
import { ShadedGeometrySystem } from "../../graphics/ecs/mesh-v2/ShadedGeometrySystem.js";
|
|
19
|
+
import {
|
|
20
|
+
AmbientOcclusionPostProcessEffect
|
|
21
|
+
} from "../../graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js";
|
|
19
22
|
import InputController from "../../input/ecs/components/InputController.js";
|
|
20
23
|
import InputControllerSystem from "../../input/ecs/systems/InputControllerSystem.js";
|
|
21
24
|
import { BodyKind } from "../../physics/ecs/BodyKind.js";
|
|
@@ -97,6 +100,8 @@ async function main(engine) {
|
|
|
97
100
|
sunShadowDistance: 30,
|
|
98
101
|
});
|
|
99
102
|
|
|
103
|
+
engine.plugins.acquire(AmbientOcclusionPostProcessEffect);
|
|
104
|
+
|
|
100
105
|
if (em.getSystem(CameraSystem) === null) {
|
|
101
106
|
await em.addSystem(new CameraSystem(engine.graphics));
|
|
102
107
|
}
|
package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AmbientOcclusionPostProcessEffect.d.ts","sourceRoot":"","sources":["../../../../../../../../src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AmbientOcclusionPostProcessEffect.d.ts","sourceRoot":"","sources":["../../../../../../../../src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js"],"names":[],"mappings":"AAoBA;IAIQ,WAA0C;IAI1C,2BAQE;IAyBF;;;;OAIG;IACH,sBAA0B;IAE1B;;;;OAIG;IACH,2BAA6B;IAG7B;;;;OAIG;IACH,yBAA4B;IAE5B;;;;OAIG;IACH,wBAA2B;IAE3B,8CAUE;IAEF;;;;OAIG;IACH,uBAAwB;IAExB;;;;;OAKG;IACH,2BASE;IAKF;;;;OAIG;IACH,0BAAwD;IAK5D;;;OAGG;IACH,2BAEC;IAED;;;OAGG;IACH,wBAEC;IAED;;;;OAIG;IACH,oBAeC;IAED,iBAEC;IAED,2BAoCC;IAED,uBASC;IAED;;;OAGG;IACH,iBAwBC;IAED;;;OAGG;IACH,4BAFW,OAAO,QAiCjB;IAED;;;;OAIG;IACH,gCASC;IAED;;;;OAIG;IACH,mCAUC;IAGD,oCAcC;IAED,wBA2CC;IAED,yBAiBC;CAEJ;6BAjZ4B,uCAAuC;+BAD7D,OAAO;6CAAP,OAAO"}
|
|
@@ -14,8 +14,9 @@ import { CompositLayer } from "../../../../composit/CompositLayer.js";
|
|
|
14
14
|
import { StandardFrameBuffers } from "../../../../StandardFrameBuffers.js";
|
|
15
15
|
import { BlendingType } from "../../../../texture/sampler/BlendingType.js";
|
|
16
16
|
import { renderScreenSpace } from "../../../utils/renderScreenSpace.js";
|
|
17
|
-
import {
|
|
17
|
+
import { generateHilbertNoiseTexture } from "./generateHilbertNoiseTexture.js";
|
|
18
18
|
import { SAOShader } from "./SAOShader.js";
|
|
19
|
+
import { SAOUpscaleShader } from "./SAOUpscaleShader.js";
|
|
19
20
|
|
|
20
21
|
export class AmbientOcclusionPostProcessEffect extends EnginePlugin {
|
|
21
22
|
constructor() {
|
|
@@ -37,11 +38,12 @@ export class AmbientOcclusionPostProcessEffect extends EnginePlugin {
|
|
|
37
38
|
|
|
38
39
|
// AO parameters
|
|
39
40
|
const uniforms = this.__material.uniforms;
|
|
40
|
-
uniforms.bias.value = 0.5;
|
|
41
|
-
uniforms.intensity.value = 2;
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
uniforms.
|
|
42
|
+
// artistic darkening control; 1.0 == physically plausible
|
|
43
|
+
uniforms.intensity.value = 1.0;
|
|
44
|
+
|
|
45
|
+
// sampling hemisphere radius (also the max occluder distance), in world units (scene dependent)
|
|
46
|
+
uniforms.kernelRadius.value = 0.5;
|
|
45
47
|
|
|
46
48
|
this.__material.blending = NoBlending;
|
|
47
49
|
|
|
@@ -55,10 +57,7 @@ export class AmbientOcclusionPostProcessEffect extends EnginePlugin {
|
|
|
55
57
|
defines.PERSPECTIVE_CAMERA = 1;
|
|
56
58
|
|
|
57
59
|
// More samples = better quality
|
|
58
|
-
defines.NUM_SAMPLES =
|
|
59
|
-
|
|
60
|
-
// More rings = better quality
|
|
61
|
-
defines.NUM_RINGS = 5;
|
|
60
|
+
defines.NUM_SAMPLES = 24;
|
|
62
61
|
|
|
63
62
|
/**
|
|
64
63
|
* Should normal buffer be used or not? Using normal buffer might be significantly more expensive, as this would require normal buffer to be built every frame. If there are other users of the buffer - this is not an issue
|
|
@@ -109,11 +108,33 @@ export class AmbientOcclusionPostProcessEffect extends EnginePlugin {
|
|
|
109
108
|
this.__used_buffers = [];
|
|
110
109
|
|
|
111
110
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
111
|
+
* Edge-aware upscale + denoise: a single non-separable pass that resolves the half-res AO
|
|
112
|
+
* into the full-res composit layer using plane-distance weights.
|
|
113
|
+
* @type {ShaderMaterial}
|
|
114
|
+
* @private
|
|
115
|
+
*/
|
|
116
|
+
this.__upscale_material = new ShaderMaterial({
|
|
117
|
+
defines: Object.assign({}, SAOUpscaleShader.defines),
|
|
118
|
+
fragmentShader: SAOUpscaleShader.fragmentShader,
|
|
119
|
+
vertexShader: SAOUpscaleShader.vertexShader,
|
|
120
|
+
uniforms: UniformsUtils.clone(SAOUpscaleShader.uniforms),
|
|
121
|
+
glslVersion: GLSL3,
|
|
122
|
+
depthWrite: false,
|
|
123
|
+
depthTest: false,
|
|
124
|
+
blending: NoBlending
|
|
125
|
+
});
|
|
126
|
+
this.__upscale_material.extensions.derivatives = true;
|
|
127
|
+
// depth is stored unpacked, matching the AO material
|
|
128
|
+
this.__upscale_material.defines.DEPTH_PACKING = 0;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Hilbert-index permutation LUT, shared by the AO sampling and upscale dithers.
|
|
132
|
+
* @type {DataTexture}
|
|
114
133
|
* @private
|
|
115
134
|
*/
|
|
116
|
-
this.
|
|
135
|
+
this.__hilbert_texture = generateHilbertNoiseTexture(64);
|
|
136
|
+
this.__material.uniforms.tHilbert.value = this.__hilbert_texture;
|
|
137
|
+
this.__upscale_material.uniforms.tHilbert.value = this.__hilbert_texture;
|
|
117
138
|
}
|
|
118
139
|
|
|
119
140
|
/**
|
|
@@ -140,29 +161,18 @@ export class AmbientOcclusionPostProcessEffect extends EnginePlugin {
|
|
|
140
161
|
__setCamera(camera) {
|
|
141
162
|
this.__render_camera = camera;
|
|
142
163
|
|
|
143
|
-
const
|
|
144
|
-
const defines = material.defines;
|
|
145
|
-
|
|
146
|
-
if (camera.isPerspectiveCamera === true && defines.PERSPECTIVE_CAMERA !== 1) {
|
|
147
|
-
defines.PERSPECTIVE_CAMERA = 1;
|
|
164
|
+
const desired = camera.isPerspectiveCamera === true ? 1 : 0;
|
|
148
165
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
defines.PERSPECTIVE_CAMERA
|
|
166
|
+
// the AO and upscale materials both reconstruct view-Z, so both track the projection type
|
|
167
|
+
const materials = [this.__material, this.__upscale_material];
|
|
168
|
+
for (const material of materials) {
|
|
169
|
+
if (material.defines.PERSPECTIVE_CAMERA !== desired) {
|
|
170
|
+
material.defines.PERSPECTIVE_CAMERA = desired;
|
|
153
171
|
|
|
154
|
-
|
|
155
|
-
|
|
172
|
+
// material has changed and will need to be re-compiled
|
|
173
|
+
material.needsUpdate = true;
|
|
174
|
+
}
|
|
156
175
|
}
|
|
157
|
-
|
|
158
|
-
this.__blur.setRenderCamera(camera);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
initialize(engine) {
|
|
162
|
-
|
|
163
|
-
this.__blur.configureMaterials();
|
|
164
|
-
|
|
165
|
-
super.initialize(engine);
|
|
166
176
|
}
|
|
167
177
|
|
|
168
178
|
finalize() {
|
|
@@ -177,7 +187,9 @@ export class AmbientOcclusionPostProcessEffect extends EnginePlugin {
|
|
|
177
187
|
|
|
178
188
|
const camera = this.__render_camera;
|
|
179
189
|
|
|
180
|
-
|
|
190
|
+
// AO renders at half-res but samples the full-res depth buffer, so `size` is the full (depth)
|
|
191
|
+
// resolution -- all texel math then lands on depth texel centres rather than texel boundaries
|
|
192
|
+
uniforms.size.value.set(this.__composit_layer.renderTarget.width, this.__composit_layer.renderTarget.height);
|
|
181
193
|
|
|
182
194
|
// setup camera
|
|
183
195
|
const near = camera.near;
|
|
@@ -192,6 +204,17 @@ export class AmbientOcclusionPostProcessEffect extends EnginePlugin {
|
|
|
192
204
|
|
|
193
205
|
// trigger uniform update
|
|
194
206
|
material.uniformsNeedUpdate = true;
|
|
207
|
+
|
|
208
|
+
// upscale runs at full resolution (the composit layer), reading the half-res AO target
|
|
209
|
+
const upscale = this.__upscale_material;
|
|
210
|
+
const upscaleUniforms = upscale.uniforms;
|
|
211
|
+
const output = this.__composit_layer.renderTarget;
|
|
212
|
+
|
|
213
|
+
upscaleUniforms.size.value.set(output.width, output.height);
|
|
214
|
+
upscaleUniforms.cameraNear.value = near;
|
|
215
|
+
upscaleUniforms.cameraFar.value = far;
|
|
216
|
+
|
|
217
|
+
upscale.uniformsNeedUpdate = true;
|
|
195
218
|
}
|
|
196
219
|
|
|
197
220
|
__prepare_draw() {
|
|
@@ -219,17 +242,20 @@ export class AmbientOcclusionPostProcessEffect extends EnginePlugin {
|
|
|
219
242
|
|
|
220
243
|
const __old_state_rt = renderer.getRenderTarget();
|
|
221
244
|
|
|
222
|
-
//
|
|
245
|
+
// render AO at half resolution
|
|
223
246
|
renderer.setRenderTarget(this.__render_target);
|
|
224
247
|
renderer.clearColor();
|
|
225
248
|
|
|
226
249
|
renderScreenSpace(renderer, this.__material);
|
|
227
250
|
|
|
228
|
-
//
|
|
229
|
-
|
|
251
|
+
// edge-aware upscale + denoise into the full-res composit layer (single non-separable pass;
|
|
252
|
+
// the fullscreen draw writes every pixel, so no clear is required)
|
|
253
|
+
renderer.setRenderTarget(this.__composit_layer.renderTarget);
|
|
254
|
+
|
|
255
|
+
renderScreenSpace(renderer, this.__upscale_material);
|
|
230
256
|
|
|
231
|
-
//
|
|
232
|
-
|
|
257
|
+
// restore rt
|
|
258
|
+
renderer.setRenderTarget(__old_state_rt);
|
|
233
259
|
}
|
|
234
260
|
|
|
235
261
|
/**
|
|
@@ -356,10 +382,10 @@ export class AmbientOcclusionPostProcessEffect extends EnginePlugin {
|
|
|
356
382
|
|
|
357
383
|
this.__update_render_target_size();
|
|
358
384
|
|
|
359
|
-
//
|
|
360
|
-
this.
|
|
361
|
-
|
|
362
|
-
|
|
385
|
+
// bind upscale inputs: half-res AO target + full-res depth
|
|
386
|
+
const upscaleUniforms = this.__upscale_material.uniforms;
|
|
387
|
+
upscaleUniforms.tAO.value = this.__render_target.texture;
|
|
388
|
+
upscaleUniforms.tDepth.value = depthTexture;
|
|
363
389
|
|
|
364
390
|
return super.startup();
|
|
365
391
|
}
|
|
@@ -376,7 +402,8 @@ export class AmbientOcclusionPostProcessEffect extends EnginePlugin {
|
|
|
376
402
|
graphics.viewport.size.onChanged.add(this.__update_render_target_size, this);
|
|
377
403
|
|
|
378
404
|
// release memory
|
|
379
|
-
this.
|
|
405
|
+
this.__upscale_material.dispose();
|
|
406
|
+
this.__hilbert_texture.dispose();
|
|
380
407
|
this.__render_target.dispose();
|
|
381
408
|
|
|
382
409
|
return super.shutdown();
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
export namespace SAOShader {
|
|
2
2
|
namespace defines {
|
|
3
3
|
let NUM_SAMPLES: number;
|
|
4
|
-
let NUM_RINGS: number;
|
|
5
4
|
let NORMAL_TEXTURE: number;
|
|
6
|
-
let DIFFUSE_TEXTURE: number;
|
|
7
5
|
let DEPTH_PACKING: number;
|
|
8
6
|
let PERSPECTIVE_CAMERA: number;
|
|
9
7
|
}
|
|
@@ -15,46 +13,38 @@ export namespace SAOShader {
|
|
|
15
13
|
let value_1: any;
|
|
16
14
|
export { value_1 as value };
|
|
17
15
|
}
|
|
18
|
-
namespace
|
|
19
|
-
let value_2:
|
|
16
|
+
namespace tHilbert {
|
|
17
|
+
let value_2: any;
|
|
20
18
|
export { value_2 as value };
|
|
21
19
|
}
|
|
22
|
-
namespace
|
|
23
|
-
let value_3:
|
|
20
|
+
namespace size {
|
|
21
|
+
let value_3: Vector2;
|
|
24
22
|
export { value_3 as value };
|
|
25
23
|
}
|
|
26
|
-
namespace
|
|
24
|
+
namespace cameraNear {
|
|
27
25
|
let value_4: number;
|
|
28
26
|
export { value_4 as value };
|
|
29
27
|
}
|
|
30
|
-
namespace
|
|
31
|
-
let value_5:
|
|
28
|
+
namespace cameraFar {
|
|
29
|
+
let value_5: number;
|
|
32
30
|
export { value_5 as value };
|
|
33
31
|
}
|
|
34
|
-
namespace
|
|
32
|
+
namespace cameraProjectionMatrix {
|
|
35
33
|
let value_6: Matrix4;
|
|
36
34
|
export { value_6 as value };
|
|
37
35
|
}
|
|
38
|
-
namespace
|
|
39
|
-
let value_7:
|
|
36
|
+
namespace cameraInverseProjectionMatrix {
|
|
37
|
+
let value_7: Matrix4;
|
|
40
38
|
export { value_7 as value };
|
|
41
39
|
}
|
|
42
|
-
namespace
|
|
40
|
+
namespace intensity {
|
|
43
41
|
let value_8: number;
|
|
44
42
|
export { value_8 as value };
|
|
45
43
|
}
|
|
46
|
-
namespace
|
|
44
|
+
namespace kernelRadius {
|
|
47
45
|
let value_9: number;
|
|
48
46
|
export { value_9 as value };
|
|
49
47
|
}
|
|
50
|
-
namespace kernelRadius {
|
|
51
|
-
let value_10: number;
|
|
52
|
-
export { value_10 as value };
|
|
53
|
-
}
|
|
54
|
-
namespace randomSeed {
|
|
55
|
-
let value_11: number;
|
|
56
|
-
export { value_11 as value };
|
|
57
|
-
}
|
|
58
48
|
}
|
|
59
49
|
let vertexShader: string;
|
|
60
50
|
let fragmentShader: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SAOShader.d.ts","sourceRoot":"","sources":["../../../../../../../../src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"SAOShader.d.ts","sourceRoot":"","sources":["../../../../../../../../src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAAiC,OAAO;wBAAP,OAAO"}
|