@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.
Files changed (59) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/shape/CapsuleShape3D.d.ts +1 -1
  3. package/src/core/geom/3d/shape/CapsuleShape3D.js +1 -1
  4. package/src/core/geom/3d/shape/SphereShape3D.d.ts +47 -0
  5. package/src/core/geom/3d/shape/SphereShape3D.d.ts.map +1 -0
  6. package/src/core/geom/3d/shape/SphereShape3D.js +127 -0
  7. package/src/core/geom/3d/shape/UnitSphereShape3D.d.ts +30 -18
  8. package/src/core/geom/3d/shape/UnitSphereShape3D.d.ts.map +1 -1
  9. package/src/core/geom/3d/shape/UnitSphereShape3D.js +44 -92
  10. package/src/core/geom/3d/shape/json/shape_to_type.d.ts.map +1 -1
  11. package/src/core/geom/3d/shape/json/shape_to_type.js +4 -2
  12. package/src/core/geom/3d/shape/json/type_adapters.d.ts +12 -3
  13. package/src/core/geom/3d/shape/json/type_adapters.d.ts.map +1 -1
  14. package/src/core/geom/3d/shape/json/type_adapters.js +16 -4
  15. package/src/core/geom/3d/shape/util/shape_to_visual_entity.js +2 -2
  16. package/src/engine/control/first-person/DESIGN_COLLISION.md +255 -0
  17. package/src/engine/control/first-person/prototype_first_person_controller.js +5 -0
  18. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
  19. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +70 -43
  20. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.d.ts +12 -22
  21. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.d.ts.map +1 -1
  22. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.js +345 -186
  23. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.d.ts +44 -0
  24. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.d.ts.map +1 -0
  25. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.js +151 -0
  26. package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.d.ts +14 -0
  27. package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.d.ts.map +1 -0
  28. package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.js +78 -0
  29. package/src/engine/physics/PLAN.md +705 -578
  30. package/src/engine/physics/REVIEW_003.md +166 -0
  31. package/src/engine/physics/constraint/solve_constraints.d.ts +24 -2
  32. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
  33. package/src/engine/physics/constraint/solve_constraints.js +402 -165
  34. package/src/engine/physics/ecs/Joint.d.ts +115 -0
  35. package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
  36. package/src/engine/physics/ecs/Joint.js +168 -0
  37. package/src/engine/physics/ecs/JointSerializationAdapter.d.ts +29 -0
  38. package/src/engine/physics/ecs/JointSerializationAdapter.d.ts.map +1 -0
  39. package/src/engine/physics/ecs/JointSerializationAdapter.js +72 -0
  40. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  41. package/src/engine/physics/narrowphase/narrowphase_step.js +20 -13
  42. package/src/engine/physics/narrowphase/ray_shapes.d.ts +66 -0
  43. package/src/engine/physics/narrowphase/ray_shapes.d.ts.map +1 -0
  44. package/src/engine/physics/narrowphase/ray_shapes.js +187 -0
  45. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +16 -0
  46. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -0
  47. package/src/engine/physics/narrowphase/refine_ray_concave.js +145 -0
  48. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +39 -0
  49. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -0
  50. package/src/engine/physics/narrowphase/refine_ray_hit.js +78 -0
  51. package/src/engine/physics/narrowphase/sphere_sphere_contact.d.ts +8 -7
  52. package/src/engine/physics/narrowphase/sphere_sphere_contact.d.ts.map +1 -1
  53. package/src/engine/physics/narrowphase/sphere_sphere_contact.js +8 -7
  54. package/src/engine/physics/queries/raycast.d.ts +11 -9
  55. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  56. package/src/engine/physics/queries/raycast.js +108 -159
  57. package/src/engine/physics/vehicle/RaycastVehicle.d.ts +114 -0
  58. package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -0
  59. 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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AmbientOcclusionPostProcessEffect.d.ts","sourceRoot":"","sources":["../../../../../../../../src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js"],"names":[],"mappings":"AAmBA;IAIQ,WAA0C;IAI1C,2BAQE;IA2BF;;;;OAIG;IACH,sBAA0B;IAE1B;;;;OAIG;IACH,2BAA6B;IAG7B;;;;OAIG;IACH,yBAA4B;IAE5B;;;;OAIG;IACH,wBAA2B;IAE3B,8CAUE;IAEF;;;;OAIG;IACH,uBAAwB;IAExB;;;;OAIG;IACH,eAAgF;IAGpF;;;OAGG;IACH,2BAEC;IAED;;;OAGG;IACH,wBAEC;IAED;;;;OAIG;IACH,oBAmBC;IAED,8BAKC;IAED,iBAEC;IAED,2BAuBC;IAED,uBASC;IAED;;;OAGG;IACH,iBAqBC;IAED;;;OAGG;IACH,4BAFW,OAAO,QAiCjB;IAED;;;;OAIG;IACH,gCASC;IAED;;;;OAIG;IACH,mCAUC;IAGD,oCAcC;IAED,wBA2CC;IAED,yBAgBC;CAEJ;6BAtX4B,uCAAuC;+BAD7D,OAAO;6CAAP,OAAO"}
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 { DepthLimitedBlur } from "../DepthLimitedBlur.js";
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
- uniforms.kernelRadius.value = 30;
44
- uniforms.minResolution.value = 0;
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 = 13;
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
- * @type {DepthLimitedBlur}
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.__blur = new DepthLimitedBlur({ format: RedFormat, clear_color: 0xFFFFFF });
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 material = this.__material;
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
- // material has changed and will need to be re-compiled
150
- material.needsUpdate = true;
151
- } else if (camera.isPerspectiveCamera !== true && defines.PERSPECTIVE_CAMERA !== 0) {
152
- defines.PERSPECTIVE_CAMERA = 0;
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
- // material has changed and will need to be re-compiled
155
- material.needsUpdate = true;
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
- uniforms.size.value.set(this.__render_target.width, this.__render_target.height);
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
- // do draw
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
- //restore rt
229
- renderer.setRenderTarget(__old_state_rt);
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
- // do blur
232
- this.__blur.execute(renderer);
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
- // configure blur
360
- this.__blur.setDepthBuffer(depthTexture);
361
- this.__blur.setInput(this.__render_target);
362
- this.__blur.setOutput(this.__composit_layer.renderTarget);
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.__blur.dispose();
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 size {
19
- let value_2: Vector2;
16
+ namespace tHilbert {
17
+ let value_2: any;
20
18
  export { value_2 as value };
21
19
  }
22
- namespace cameraNear {
23
- let value_3: number;
20
+ namespace size {
21
+ let value_3: Vector2;
24
22
  export { value_3 as value };
25
23
  }
26
- namespace cameraFar {
24
+ namespace cameraNear {
27
25
  let value_4: number;
28
26
  export { value_4 as value };
29
27
  }
30
- namespace cameraProjectionMatrix {
31
- let value_5: Matrix4;
28
+ namespace cameraFar {
29
+ let value_5: number;
32
30
  export { value_5 as value };
33
31
  }
34
- namespace cameraInverseProjectionMatrix {
32
+ namespace cameraProjectionMatrix {
35
33
  let value_6: Matrix4;
36
34
  export { value_6 as value };
37
35
  }
38
- namespace intensity {
39
- let value_7: number;
36
+ namespace cameraInverseProjectionMatrix {
37
+ let value_7: Matrix4;
40
38
  export { value_7 as value };
41
39
  }
42
- namespace bias {
40
+ namespace intensity {
43
41
  let value_8: number;
44
42
  export { value_8 as value };
45
43
  }
46
- namespace minResolution {
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAAiC,OAAO;wBAAP,OAAO"}
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"}