@woosh/meep-engine 3.4.0 → 3.6.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/gjk/gjk_epa_penetration.d.ts +30 -3
- package/src/core/geom/3d/gjk/gjk_epa_penetration.d.ts.map +1 -1
- package/src/core/geom/3d/gjk/gjk_epa_penetration.js +83 -10
- package/src/core/geom/3d/gjk/mpr.d.ts +7 -1
- package/src/core/geom/3d/gjk/mpr.d.ts.map +1 -1
- package/src/core/geom/3d/gjk/mpr.js +76 -13
- package/src/engine/Engine.js +2 -2
- package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +1 -1
- package/src/engine/graphics3/GraphicsEngine3.d.ts +7 -0
- package/src/engine/graphics3/GraphicsEngine3.d.ts.map +1 -1
- package/src/engine/graphics3/GraphicsEngine3.js +9 -0
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +20 -2
- package/src/shade/playground/skinned_blas_refit/README.md +56 -33
- package/src/shade/playground/skinned_blas_refit/place_character.d.ts +2 -1
- package/src/shade/playground/skinned_blas_refit/place_character.d.ts.map +1 -1
- package/src/shade/playground/skinned_blas_refit/place_character.js +2 -1
- package/src/shade/renderer/scene/bvh/StaticSceneBVH.js +4 -4
- package/src/view/graphics/{WebGLContextFailureView.d.ts → GraphicsContextFailureView.d.ts} +2 -2
- package/src/view/graphics/GraphicsContextFailureView.d.ts.map +1 -0
- package/src/view/graphics/{WebGLContextFailureView.js → GraphicsContextFailureView.js} +1 -1
- package/src/core/geom/3d/aabb/aabb3_from_three_geometry.d.ts +0 -7
- package/src/core/geom/3d/aabb/aabb3_from_three_geometry.d.ts.map +0 -1
- package/src/core/geom/3d/aabb/aabb3_from_three_geometry.js +0 -18
- package/src/core/geom/3d/aabb/aabb3_from_threejs_geometry.d.ts +0 -7
- package/src/core/geom/3d/aabb/aabb3_from_threejs_geometry.d.ts.map +0 -1
- package/src/core/geom/3d/aabb/aabb3_from_threejs_geometry.js +0 -7
- package/src/engine/ecs/transform/copy_three_transform.d.ts +0 -7
- package/src/engine/ecs/transform/copy_three_transform.d.ts.map +0 -1
- package/src/engine/ecs/transform/copy_three_transform.js +0 -15
- package/src/engine/graphics/ecs/mesh-v2/DeferredBoundsQueue.d.ts +0 -48
- package/src/engine/graphics/ecs/mesh-v2/DeferredBoundsQueue.d.ts.map +0 -1
- package/src/engine/graphics/ecs/mesh-v2/DeferredBoundsQueue.js +0 -88
- package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshSystem.d.ts +0 -63
- package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshSystem.d.ts.map +0 -1
- package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshSystem.js +0 -314
- package/src/engine/graphics/ecs/mesh-v2/render/adapters/AbstractRenderAdapter.d.ts +0 -67
- package/src/engine/graphics/ecs/mesh-v2/render/adapters/AbstractRenderAdapter.d.ts.map +0 -1
- package/src/engine/graphics/ecs/mesh-v2/render/adapters/AbstractRenderAdapter.js +0 -130
- package/src/engine/graphics/ecs/mesh-v2/render/adapters/SGCacheKey.d.ts +0 -42
- package/src/engine/graphics/ecs/mesh-v2/render/adapters/SGCacheKey.d.ts.map +0 -1
- package/src/engine/graphics/ecs/mesh-v2/render/adapters/SGCacheKey.js +0 -71
- package/src/engine/graphics/ecs/mesh-v2/render/optimization/RuntimeDrawMethodOptimizer.d.ts +0 -20
- package/src/engine/graphics/ecs/mesh-v2/render/optimization/RuntimeDrawMethodOptimizer.d.ts.map +0 -1
- package/src/engine/graphics/ecs/mesh-v2/render/optimization/RuntimeDrawMethodOptimizer.js +0 -123
- package/src/engine/graphics/ecs/mesh-v2/three_object_to_entity_composition.d.ts +0 -8
- package/src/engine/graphics/ecs/mesh-v2/three_object_to_entity_composition.d.ts.map +0 -1
- package/src/engine/graphics/ecs/mesh-v2/three_object_to_entity_composition.js +0 -55
- package/src/engine/graphics/impostors/octahedral/util/load_mesh_for_bake.d.ts +0 -12
- package/src/engine/graphics/impostors/octahedral/util/load_mesh_for_bake.d.ts.map +0 -1
- package/src/engine/graphics/impostors/octahedral/util/load_mesh_for_bake.js +0 -31
- package/src/engine/graphics/sh3/build_injected_shader.d.ts +0 -51
- package/src/engine/graphics/sh3/build_injected_shader.d.ts.map +0 -1
- package/src/engine/graphics/sh3/build_injected_shader.js +0 -56
- package/src/engine/graphics/sh3/fromCubeRenderTarget.d.ts +0 -9
- package/src/engine/graphics/sh3/fromCubeRenderTarget.d.ts.map +0 -1
- package/src/engine/graphics/sh3/fromCubeRenderTarget.js +0 -142
- package/src/engine/graphics/sh3/shader/visualize.frag.d.ts +0 -3
- package/src/engine/graphics/sh3/shader/visualize.frag.d.ts.map +0 -1
- package/src/engine/graphics/sh3/shader/visualize.frag.js +0 -51
- package/src/engine/graphics/sh3/shader/visualize.vert.d.ts +0 -3
- package/src/engine/graphics/sh3/shader/visualize.vert.d.ts.map +0 -1
- package/src/engine/graphics/sh3/shader/visualize.vert.js +0 -12
- package/src/view/graphics/WebGLContextFailureView.d.ts.map +0 -1
package/package.json
CHANGED
|
@@ -3,15 +3,42 @@
|
|
|
3
3
|
* Writes the unit separation normal (A→B: the direction A must move along
|
|
4
4
|
* NEGATED to separate — equivalently, negate `out` for the B→A push axis;
|
|
5
5
|
* the narrowphase sign-check sites canonicalise it against the body centres)
|
|
6
|
-
* into `out` and returns the depth
|
|
7
|
-
*
|
|
6
|
+
* into `out` and returns the depth. `out` is untouched unless the return is
|
|
7
|
+
* positive.
|
|
8
8
|
*
|
|
9
9
|
* @param {Float64Array|number[]} out destination for the unit normal, length ≥ 3
|
|
10
10
|
* @param {SupportProvider} A world-space support provider (e.g. PosedShape3D)
|
|
11
11
|
* @param {SupportProvider} B world-space support provider (e.g. PosedShape3D)
|
|
12
|
-
* @returns {number} penetration depth
|
|
12
|
+
* @returns {number} the penetration depth (> 0) on overlap;
|
|
13
|
+
* {@link GJK_SEPARATED} when GJK PROVED the shapes apart; `0` for no usable
|
|
14
|
+
* axis — EPA degenerated on an established overlap, or GJK reached no
|
|
15
|
+
* verdict at all. `0` is not a separation and callers with a fallback
|
|
16
|
+
* should still run it
|
|
13
17
|
*/
|
|
14
18
|
export function gjk_epa_penetration(out: Float64Array | number[], A: SupportProvider, B: SupportProvider): number;
|
|
19
|
+
/**
|
|
20
|
+
* Sentinel returned by {@link gjk_epa_penetration} when GJK PROVED the shapes
|
|
21
|
+
* apart — {@link GJK_APART}, a support plane with the whole Minkowski
|
|
22
|
+
* difference on the far side of the origin — as distinct from `0`, "no usable
|
|
23
|
+
* axis": a degenerate EPA, or a GJK that reached no verdict at all.
|
|
24
|
+
*
|
|
25
|
+
* The two need separate values because callers act on them differently. A
|
|
26
|
+
* proof of separation is the final answer and the pair is done; the rest is a
|
|
27
|
+
* gap in this query that a fallback (MPR) may still be able to fill. Note the
|
|
28
|
+
* asymmetry — this is claimed ONLY from the proof, never from GJK's other two
|
|
29
|
+
* non-overlap exits, which are inconclusive rather than negative.
|
|
30
|
+
* Collapsing both to `0`, as this function used to, sent every non-touching
|
|
31
|
+
* pair the broadphase produced into that fallback — where MPR, whose portal
|
|
32
|
+
* argument assumes an overlap it was never told to doubt, answered a question
|
|
33
|
+
* GJK had already settled and reported the gap as a penetration of its own
|
|
34
|
+
* size (see the separating-plane certificate in {@link mpr}).
|
|
35
|
+
*
|
|
36
|
+
* Negative so that the existing `depth > 0` guard at every call site keeps
|
|
37
|
+
* rejecting it unchanged.
|
|
38
|
+
*
|
|
39
|
+
* @type {number}
|
|
40
|
+
*/
|
|
41
|
+
export const GJK_SEPARATED: number;
|
|
15
42
|
/**
|
|
16
43
|
* A convex shape exposing a world-space support mapping: `support(out, off, dx,
|
|
17
44
|
* dy, dz)` writes the farthest point of the shape in direction `(dx, dy, dz)`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gjk_epa_penetration.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/gjk/gjk_epa_penetration.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"gjk_epa_penetration.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/gjk/gjk_epa_penetration.js"],"names":[],"mappings":"AA2iBA;;;;;;;;;;;;;;;;GAgBG;AACH,yCATW,YAAY,GAAC,MAAM,EAAE,KACrB,eAAe,KACf,eAAe,GACb,MAAM,CA0BlB;AA7DD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,4BAFU,MAAM,CAEgB;;;;;;8BAzdnB;IAAE,OAAO,SAAW,YAAY,QAAE,MAAM,QAAE,MAAM,QAAE,MAAM,QAAE,MAAM,KAAG,IAAI,CAAA;CAAE"}
|
|
@@ -81,14 +81,45 @@ const CEN = new Float64Array(3); // polytope centroid (for outward orientation)
|
|
|
81
81
|
* @typedef {{ support: function(Float64Array, number, number, number, number): void }} SupportProvider
|
|
82
82
|
*/
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* {@link gjk} enclosed the origin: the shapes overlap and {@link SIMPLEX} holds
|
|
86
|
+
* a rank-4 simplex EPA can expand from.
|
|
87
|
+
* @type {number}
|
|
88
|
+
*/
|
|
89
|
+
const GJK_ENCLOSED = 4;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* {@link gjk} found a support plane with the whole Minkowski difference on the
|
|
93
|
+
* far side of the origin. That is a proof of separation, and the only one of
|
|
94
|
+
* this function's non-overlap exits that is.
|
|
95
|
+
* @type {number}
|
|
96
|
+
*/
|
|
97
|
+
const GJK_APART = -1;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* {@link gjk} ran out of road: the search direction collapsed, or the iteration
|
|
101
|
+
* cap hit. Neither says the shapes are apart — the direction collapses when the
|
|
102
|
+
* origin lands exactly ON a simplex feature, which is a TOUCH (two spheres at
|
|
103
|
+
* exactly 2r do this), and the cap is just non-convergence on a smooth support.
|
|
104
|
+
* Callers must treat it as "no answer" and keep whatever fallback they have.
|
|
105
|
+
* @type {number}
|
|
106
|
+
*/
|
|
107
|
+
const GJK_INCONCLUSIVE = 0;
|
|
108
|
+
|
|
84
109
|
/**
|
|
85
110
|
* GJK intersection. Fills {@link SIMPLEX} with a rank-4 simplex enclosing the
|
|
86
|
-
* origin and returns
|
|
111
|
+
* origin and returns {@link GJK_ENCLOSED} on overlap.
|
|
87
112
|
* `SIMPLEX` slot 0 is the most-recent point throughout (Muratori convention).
|
|
88
113
|
*
|
|
114
|
+
* The two non-overlap verdicts are deliberately distinct: only
|
|
115
|
+
* {@link GJK_APART} is a proof, and collapsing it together with
|
|
116
|
+
* {@link GJK_INCONCLUSIVE} silently converts "I could not tell" into "they are
|
|
117
|
+
* not touching" — which drops touching contacts.
|
|
118
|
+
*
|
|
89
119
|
* @param {SupportProvider} A first support provider
|
|
90
120
|
* @param {SupportProvider} B second support provider
|
|
91
|
-
* @returns {number}
|
|
121
|
+
* @returns {number} {@link GJK_ENCLOSED}, {@link GJK_APART}, or
|
|
122
|
+
* {@link GJK_INCONCLUSIVE}
|
|
92
123
|
*/
|
|
93
124
|
function gjk(A, B) {
|
|
94
125
|
minkowski_support(SIMPLEX, 0, A, B, 1, 0, 0);
|
|
@@ -102,11 +133,11 @@ function gjk(A, B) {
|
|
|
102
133
|
|
|
103
134
|
for (let iter = 0; iter < GJK_MAX_ITER; iter++) {
|
|
104
135
|
if (v3_length_sqr(DIR[0], DIR[1], DIR[2]) < DIR_EPS) {
|
|
105
|
-
return
|
|
136
|
+
return GJK_INCONCLUSIVE; // direction collapsed → touching, or apart; unknown
|
|
106
137
|
}
|
|
107
138
|
minkowski_support(_w, 0, A, B, DIR[0], DIR[1], DIR[2]);
|
|
108
139
|
if (v3_dot(_w[0], _w[1], _w[2], DIR[0], DIR[1], DIR[2]) < 0) {
|
|
109
|
-
return
|
|
140
|
+
return GJK_APART; // farthest point short of the origin → proven separated
|
|
110
141
|
}
|
|
111
142
|
// push the new point to slot 0, shifting the existing points down one slot
|
|
112
143
|
// (copyWithin is memmove-style: the overlapping forward copy is well-defined)
|
|
@@ -118,10 +149,10 @@ function gjk(A, B) {
|
|
|
118
149
|
|
|
119
150
|
count = do_simplex(count);
|
|
120
151
|
if (count === 0) {
|
|
121
|
-
return
|
|
152
|
+
return GJK_ENCLOSED; // origin enclosed by the tetrahedron
|
|
122
153
|
}
|
|
123
154
|
}
|
|
124
|
-
return
|
|
155
|
+
return GJK_INCONCLUSIVE; // iteration cap → non-convergent, not a separation
|
|
125
156
|
}
|
|
126
157
|
|
|
127
158
|
/**
|
|
@@ -498,23 +529,65 @@ function epa(A, B, out) {
|
|
|
498
529
|
return FACE_DIST[best];
|
|
499
530
|
}
|
|
500
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Sentinel returned by {@link gjk_epa_penetration} when GJK PROVED the shapes
|
|
534
|
+
* apart — {@link GJK_APART}, a support plane with the whole Minkowski
|
|
535
|
+
* difference on the far side of the origin — as distinct from `0`, "no usable
|
|
536
|
+
* axis": a degenerate EPA, or a GJK that reached no verdict at all.
|
|
537
|
+
*
|
|
538
|
+
* The two need separate values because callers act on them differently. A
|
|
539
|
+
* proof of separation is the final answer and the pair is done; the rest is a
|
|
540
|
+
* gap in this query that a fallback (MPR) may still be able to fill. Note the
|
|
541
|
+
* asymmetry — this is claimed ONLY from the proof, never from GJK's other two
|
|
542
|
+
* non-overlap exits, which are inconclusive rather than negative.
|
|
543
|
+
* Collapsing both to `0`, as this function used to, sent every non-touching
|
|
544
|
+
* pair the broadphase produced into that fallback — where MPR, whose portal
|
|
545
|
+
* argument assumes an overlap it was never told to doubt, answered a question
|
|
546
|
+
* GJK had already settled and reported the gap as a penetration of its own
|
|
547
|
+
* size (see the separating-plane certificate in {@link mpr}).
|
|
548
|
+
*
|
|
549
|
+
* Negative so that the existing `depth > 0` guard at every call site keeps
|
|
550
|
+
* rejecting it unchanged.
|
|
551
|
+
*
|
|
552
|
+
* @type {number}
|
|
553
|
+
*/
|
|
554
|
+
export const GJK_SEPARATED = -1;
|
|
555
|
+
|
|
501
556
|
/**
|
|
502
557
|
* Penetration depth + normal between two convex support providers at world pose.
|
|
503
558
|
* Writes the unit separation normal (A→B: the direction A must move along
|
|
504
559
|
* NEGATED to separate — equivalently, negate `out` for the B→A push axis;
|
|
505
560
|
* the narrowphase sign-check sites canonicalise it against the body centres)
|
|
506
|
-
* into `out` and returns the depth
|
|
507
|
-
*
|
|
561
|
+
* into `out` and returns the depth. `out` is untouched unless the return is
|
|
562
|
+
* positive.
|
|
508
563
|
*
|
|
509
564
|
* @param {Float64Array|number[]} out destination for the unit normal, length ≥ 3
|
|
510
565
|
* @param {SupportProvider} A world-space support provider (e.g. PosedShape3D)
|
|
511
566
|
* @param {SupportProvider} B world-space support provider (e.g. PosedShape3D)
|
|
512
|
-
* @returns {number} penetration depth
|
|
567
|
+
* @returns {number} the penetration depth (> 0) on overlap;
|
|
568
|
+
* {@link GJK_SEPARATED} when GJK PROVED the shapes apart; `0` for no usable
|
|
569
|
+
* axis — EPA degenerated on an established overlap, or GJK reached no
|
|
570
|
+
* verdict at all. `0` is not a separation and callers with a fallback
|
|
571
|
+
* should still run it
|
|
513
572
|
*/
|
|
514
573
|
export function gjk_epa_penetration(out, A, B) {
|
|
515
|
-
|
|
574
|
+
const verdict = gjk(A, B);
|
|
575
|
+
|
|
576
|
+
if (verdict === GJK_APART) {
|
|
577
|
+
return GJK_SEPARATED;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (verdict !== GJK_ENCLOSED) {
|
|
581
|
+
// GJK_INCONCLUSIVE — no simplex to expand and no proof of separation
|
|
582
|
+
// either, so this reports the same "no usable axis" as a degenerate
|
|
583
|
+
// EPA and leaves the caller's fallback in play. Reporting it as
|
|
584
|
+
// GJK_SEPARATED would be an overclaim, and an expensive one: the
|
|
585
|
+
// collapse exit fires on EXACTLY touching pairs (two spheres at 2r),
|
|
586
|
+
// whose resting contact would then be dropped without anything ever
|
|
587
|
+
// looking at the geometry.
|
|
516
588
|
return 0;
|
|
517
589
|
}
|
|
590
|
+
|
|
518
591
|
const depth = epa(A, B, out);
|
|
519
592
|
return depth > 0 && Number.isFinite(depth) ? depth : 0;
|
|
520
593
|
}
|
|
@@ -39,7 +39,13 @@
|
|
|
39
39
|
* any narrowphase call site.
|
|
40
40
|
*
|
|
41
41
|
* On failure modes:
|
|
42
|
-
* - Returns `false` if the shapes don't overlap
|
|
42
|
+
* - Returns `false` if the shapes don't overlap, decided by a
|
|
43
|
+
* separating-plane certificate — the support of the Mink-diff along
|
|
44
|
+
* the portal normal fails to reach the origin — which is one exact
|
|
45
|
+
* evaluation carrying none of the refinement's error. A miss leaves
|
|
46
|
+
* `result` untouched: the separation distance is deliberately NOT
|
|
47
|
+
* written, because every caller reads the depth as |MTV| and cannot
|
|
48
|
+
* tell a gap from a penetration once the sign is gone.
|
|
43
49
|
* - On a degenerate Mink-diff (shapes touching at a point, perfectly
|
|
44
50
|
* coincident centres with collinear support points), returns
|
|
45
51
|
* `true` with a small fallback MTV — same approach as EPA's
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mpr.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/gjk/mpr.js"],"names":[],"mappings":"AAsDA
|
|
1
|
+
{"version":3,"file":"mpr.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/gjk/mpr.js"],"names":[],"mappings":"AAsDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,4BANW,YAAY,iBACZ,MAAM,iDAGJ,OAAO,CA8TnB"}
|
|
@@ -93,7 +93,13 @@ const V4 = new Float64Array(3);
|
|
|
93
93
|
* any narrowphase call site.
|
|
94
94
|
*
|
|
95
95
|
* On failure modes:
|
|
96
|
-
* - Returns `false` if the shapes don't overlap
|
|
96
|
+
* - Returns `false` if the shapes don't overlap, decided by a
|
|
97
|
+
* separating-plane certificate — the support of the Mink-diff along
|
|
98
|
+
* the portal normal fails to reach the origin — which is one exact
|
|
99
|
+
* evaluation carrying none of the refinement's error. A miss leaves
|
|
100
|
+
* `result` untouched: the separation distance is deliberately NOT
|
|
101
|
+
* written, because every caller reads the depth as |MTV| and cannot
|
|
102
|
+
* tell a gap from a penetration once the sign is gone.
|
|
97
103
|
* - On a degenerate Mink-diff (shapes touching at a point, perfectly
|
|
98
104
|
* coincident centres with collinear support points), returns
|
|
99
105
|
* `true` with a small fallback MTV — same approach as EPA's
|
|
@@ -116,6 +122,17 @@ export function mpr(result, result_offset, shape_a, shape_b) {
|
|
|
116
122
|
// origins are interior to their geometry — true for spheres,
|
|
117
123
|
// boxes, capsules, and any closed mesh whose bounding box contains
|
|
118
124
|
// its centroid.
|
|
125
|
+
//
|
|
126
|
+
// This is a PRECONDITION, not a preference, and nothing here can
|
|
127
|
+
// check it: every argument below assumes the ray V0→origin starts
|
|
128
|
+
// inside. An authored ConvexHullShape3D whose vertices sit far from
|
|
129
|
+
// its own local origin (a level brush keeping map coordinates under
|
|
130
|
+
// an identity collider transform, say) violates it, and MPR then
|
|
131
|
+
// returns a confidently wrong MTV — measured at 17.8 m, the slab's
|
|
132
|
+
// half-width, for a 0.05 m overlap. Callers that can answer overlap
|
|
133
|
+
// some other way should do so and reach for MPR only when that
|
|
134
|
+
// answer is genuinely unavailable; {@link narrowphase_step} keeps it
|
|
135
|
+
// behind a GJK verdict for exactly this reason.
|
|
119
136
|
V0[0] = shape_a.px - shape_b.px;
|
|
120
137
|
V0[1] = shape_a.py - shape_b.py;
|
|
121
138
|
V0[2] = shape_a.pz - shape_b.pz;
|
|
@@ -281,10 +298,22 @@ export function mpr(result, result_offset, shape_a, shape_b) {
|
|
|
281
298
|
const inv_pn = 1 / Math.sqrt(pn_len_sqr);
|
|
282
299
|
pn_x *= inv_pn; pn_y *= inv_pn; pn_z *= inv_pn;
|
|
283
300
|
|
|
284
|
-
//
|
|
285
|
-
// = dot(V1, portal_normal)
|
|
286
|
-
//
|
|
287
|
-
//
|
|
301
|
+
// Signed offset of the portal plane from the origin along the
|
|
302
|
+
// outward normal, = dot(V1, portal_normal), and the depth this
|
|
303
|
+
// returns once the portal settles. In exact arithmetic its sign
|
|
304
|
+
// would also BE the overlap test — V0 is interior and the ray
|
|
305
|
+
// V0→origin pierces the portal, so the origin is inside the
|
|
306
|
+
// Mink-diff exactly when it lies on V0's side of that plane —
|
|
307
|
+
// and a negative value would mean the ray left the Mink-diff
|
|
308
|
+
// before reaching the origin, making |portal_dist| a SEPARATION
|
|
309
|
+
// rather than a depth.
|
|
310
|
+
//
|
|
311
|
+
// It is not the test used, because this is an iterate: near zero
|
|
312
|
+
// its sign is refinement noise, not geometry. The certificate
|
|
313
|
+
// below decides instead, and getting a magnitude out of a
|
|
314
|
+
// separated pair — which every caller then reads as a depth,
|
|
315
|
+
// |MTV| having thrown the sign away — is the failure that made
|
|
316
|
+
// this loop worth re-deriving.
|
|
288
317
|
const portal_dist = V1[0] * pn_x + V1[1] * pn_y + V1[2] * pn_z;
|
|
289
318
|
|
|
290
319
|
// New support V4 in the portal normal direction.
|
|
@@ -292,8 +321,41 @@ export function mpr(result, result_offset, shape_a, shape_b) {
|
|
|
292
321
|
|
|
293
322
|
const v4_dist = V4[0] * pn_x + V4[1] * pn_y + V4[2] * pn_z;
|
|
294
323
|
|
|
324
|
+
// Separating-plane certificate — and it has to be tested BEFORE
|
|
325
|
+
// the convergence branch below. If the farthest point of the
|
|
326
|
+
// Mink-diff along pn does not reach the origin, the plane
|
|
327
|
+
// through the origin with normal pn separates the two shapes,
|
|
328
|
+
// which is a proof of a miss. It can never reject a true
|
|
329
|
+
// overlap: the origin is then inside the Mink-diff, whose
|
|
330
|
+
// support is ≥ 0 in EVERY direction.
|
|
331
|
+
//
|
|
332
|
+
// The order is the whole point. A small shape near a large flat
|
|
333
|
+
// face seeds a portal already within MPR_TOLERANCE of the
|
|
334
|
+
// support plane, so the convergence test succeeds on the first
|
|
335
|
+
// iteration and this proof was never reached — the old order
|
|
336
|
+
// returned `true` with a negative portal_dist, and callers
|
|
337
|
+
// (which read the depth as |MTV|) saw the gap as a penetration
|
|
338
|
+
// of exactly its own size.
|
|
339
|
+
if (v4_dist < 0) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
295
343
|
// Convergence: V4 doesn't extend the portal beyond the current
|
|
296
|
-
// face (within tolerance)
|
|
344
|
+
// face (within tolerance), so the portal face is the closest
|
|
345
|
+
// face of the Mink-diff and its offset is the depth.
|
|
346
|
+
//
|
|
347
|
+
// Note what does NOT appear here: a `portal_dist <= 0` gate. It
|
|
348
|
+
// is tempting — a negative offset is exactly what a miss looks
|
|
349
|
+
// like — but portal_dist is the one quantity in this loop that
|
|
350
|
+
// carries the refinement's error, and near zero that error is
|
|
351
|
+
// the whole signal. A capsule resting exactly on a floor
|
|
352
|
+
// converges at portal_dist ≈ −3e-6 with v4_dist ≈ +2.5e-6: a
|
|
353
|
+
// real touching contact whose sign is pure noise, which such a
|
|
354
|
+
// gate drops, taking the resting contact with it. The certificate
|
|
355
|
+
// above is error-free (one exact support evaluation) and is the
|
|
356
|
+
// right arbiter; by the time it passes, convergence bounds
|
|
357
|
+
// portal_dist below at −MPR_TOLERANCE, which is the same
|
|
358
|
+
// accuracy the depth itself is quoted to.
|
|
297
359
|
if (v4_dist - portal_dist < MPR_TOLERANCE) {
|
|
298
360
|
result[result_offset] = pn_x * portal_dist;
|
|
299
361
|
result[result_offset + 1] = pn_y * portal_dist;
|
|
@@ -301,13 +363,6 @@ export function mpr(result, result_offset, shape_a, shape_b) {
|
|
|
301
363
|
return true;
|
|
302
364
|
}
|
|
303
365
|
|
|
304
|
-
// V4 didn't pass the origin along the portal normal — the
|
|
305
|
-
// shapes aren't actually overlapping (shouldn't happen after
|
|
306
|
-
// successful portal discovery, but defensive).
|
|
307
|
-
if (v4_dist < 0) {
|
|
308
|
-
return false;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
366
|
// Replace one of V1, V2, V3 with V4 such that the new portal
|
|
312
367
|
// still contains the V0→origin ray. The split is determined by
|
|
313
368
|
// three cross-product / dot tests against V4 × V0: signs of
|
|
@@ -360,6 +415,14 @@ export function mpr(result, result_offset, shape_a, shape_b) {
|
|
|
360
415
|
}
|
|
361
416
|
const inv_pn = 1 / Math.sqrt(pn_len_sqr);
|
|
362
417
|
pn_x *= inv_pn; pn_y *= inv_pn; pn_z *= inv_pn;
|
|
418
|
+
// The portal never settled, so this face is a guess — but the
|
|
419
|
+
// separating-plane certificate costs one support call and holds
|
|
420
|
+
// regardless of how the refinement terminated. Apply the same rule
|
|
421
|
+
// both exits use rather than emitting an unexamined MTV.
|
|
422
|
+
minkowski_support(V4, 0, shape_a, shape_b, pn_x, pn_y, pn_z);
|
|
423
|
+
if (V4[0] * pn_x + V4[1] * pn_y + V4[2] * pn_z < 0) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
363
426
|
const portal_dist = V1[0] * pn_x + V1[1] * pn_y + V1[2] * pn_z;
|
|
364
427
|
result[result_offset] = pn_x * portal_dist;
|
|
365
428
|
result[result_offset + 1] = pn_y * portal_dist;
|
package/src/engine/Engine.js
CHANGED
|
@@ -10,7 +10,7 @@ import ObservedBoolean from "../core/model/ObservedBoolean.js";
|
|
|
10
10
|
import ConcurrentExecutor from '../core/process/executor/ConcurrentExecutor.js';
|
|
11
11
|
import EmptyView from "../view/elements/EmptyView.js";
|
|
12
12
|
import { ViewStack } from "../view/elements/navigation/ViewStack.js";
|
|
13
|
-
import {
|
|
13
|
+
import { GraphicsContextFailureView } from "../view/graphics/GraphicsContextFailureView.js";
|
|
14
14
|
|
|
15
15
|
import { AssetManager } from './asset/AssetManager.js';
|
|
16
16
|
import { AssetPreloader } from "./asset/preloader/AssetPreloader.js";
|
|
@@ -416,7 +416,7 @@ class Engine {
|
|
|
416
416
|
// Application state is left intact, which keeps a save-on-failure handler viable.
|
|
417
417
|
this.#pause_simulation_for_context_loss();
|
|
418
418
|
|
|
419
|
-
this.viewStack.addChild(new
|
|
419
|
+
this.viewStack.addChild(new GraphicsContextFailureView());
|
|
420
420
|
}
|
|
421
421
|
|
|
422
422
|
#initialize_audio() {
|
|
@@ -203,7 +203,7 @@ already paused is left alone, and never resumed by us — hence the new `Ticker.
|
|
|
203
203
|
accessors, since `pause()`/`resume()` throw when the loop is not cycling.
|
|
204
204
|
|
|
205
205
|
On `Failed`: the simulation is paused, application state is left intact (so a save-on-failure handler
|
|
206
|
-
stays viable), and `
|
|
206
|
+
stays viable), and `GraphicsContextFailureView` explains the situation with a reload button, instead of
|
|
207
207
|
leaving the player looking at a black canvas.
|
|
208
208
|
|
|
209
209
|
---
|
|
@@ -31,6 +31,12 @@ export class GraphicsEngine3 {
|
|
|
31
31
|
* @returns {boolean}
|
|
32
32
|
*/
|
|
33
33
|
get isGraphicsEngine(): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Danger zone.
|
|
36
|
+
* Be careful with what you do, with great Renderer comes great responsibility.
|
|
37
|
+
* @returns {Renderer|null}
|
|
38
|
+
*/
|
|
39
|
+
get renderer(): Renderer;
|
|
34
40
|
/**
|
|
35
41
|
* The camera, in the shape the engine's existing consumers read it — `projectionMatrix` and
|
|
36
42
|
* `matrixWorldInverse`, which is what `HeadsUpDisplaySystem` multiplies together. The real
|
|
@@ -317,6 +323,7 @@ export class GraphicsEngine3 {
|
|
|
317
323
|
render(time_delta_seconds?: number): boolean;
|
|
318
324
|
#private;
|
|
319
325
|
}
|
|
326
|
+
import { Renderer } from "../../shade/renderer/Renderer.js";
|
|
320
327
|
import { ShadeCameraAdapter } from "./ShadeCameraAdapter.js";
|
|
321
328
|
import Signal from "../../core/events/signal/Signal.js";
|
|
322
329
|
import Vector1 from "../../core/geom/Vector1.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GraphicsEngine3.d.ts","sourceRoot":"","sources":["../../../../src/engine/graphics3/GraphicsEngine3.js"],"names":[],"mappings":"AAkDA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH;IACI;;;;;OAKG;IACH,gCAEC;
|
|
1
|
+
{"version":3,"file":"GraphicsEngine3.d.ts","sourceRoot":"","sources":["../../../../src/engine/graphics3/GraphicsEngine3.js"],"names":[],"mappings":"AAkDA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH;IACI;;;;;OAKG;IACH,gCAEC;IAUD;;;;OAIG;IACH,yBAEC;IA0DG;;;;;;OAMG;IACH,QAFU,kBAAkB,CAEsB;IAElD;QACI;;;;;;;;WAQG;mBADO,MAAM;QAIhB;;;;;WAKG;oBADO,MAAM;QAIhB;;;;WAIG;qBADO,MAAM;QAIhB;;;;;;;WAOG;;QAGH;;;;;WAKG;;MAEN;IAED;;OAEG;IACH,YAFU,OAAO,CAEe;IAEhC;;;;;OAKG;IACH,4BAFU,OAAO,CAEyB;IAE1C;;;;;;OAMG;IACH,UAFU,SAAS,CAEY;IAG/B;;OAEG;IACH,YAFU,iBAAiB,GAAC,IAAI,CAEV;IAEtB;;;;;OAKG;IACH,UAFU,OAAO,CAEG;IAEpB;;;;;OAKG;IACH,UAFU,OAAO,CAEG;IAEpB;;OAEG;IACH,YAFU,MAAM,CAEG;IAoBvB;;;;;;;;;;;;OAYG;IACH,mDAEC;IAED;;;;;;;;OAQG;IACH,iBAFW,KAAK,QAoBf;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,iDAOC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,2DAWC;IAED;;;;;;;;;;OAUG;IACH,oCAFa,MAAM,CAclB;IAED;;;;;;OAMG;IACH,8CAFa,OAAO,CAgBnB;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACH,qBALW,KAAK,GACH,kBAAgB,IAAI,CAehC;IAED;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,yBARW,KAAK,yBAEL,MAAM,UACN,MAAM,GACJ,QAAQ,SAAS,CAAC,CAwD9B;IAED;;;;OAIG;IACH,0BAEC;IAED;;;;;;;;;;OAUG;IACH,SAFa,QAAQ,IAAI,CAAC,CA4DzB;IAED;;;;;OAKG;IACH,aAiBC;IASD;;;;;;;;OAQG;IACH,8BAHW,OAAO,UAAQ,UACf,OAAO,UAAQ,QAQzB;IAED;;;;;;;;;;;;;;;OAeG;IACH,yBALW,MAAM,KACN,MAAM,6CAMhB;IAED;;;;;;;OAOG;IACH,mBA0BC;IAED;;;;;OAKG;IACH,4BAHW,MAAM,GACJ,OAAO,CAoDnB;;CACJ;yBAxwBwB,kCAAkC;mCAMxB,yBAAyB;mBAbzC,oCAAoC;oBACnC,4BAA4B;wBACxB,4BAA4B;sBAQ9B,kCAAkC;yCAJf,kDAAkD;sBAErE,qCAAqC;0BACjC,0CAA0C"}
|
|
@@ -92,6 +92,15 @@ export class GraphicsEngine3 {
|
|
|
92
92
|
*/
|
|
93
93
|
#renderer = null;
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Danger zone.
|
|
97
|
+
* Be careful with what you do, with great Renderer comes great responsibility.
|
|
98
|
+
* @returns {Renderer|null}
|
|
99
|
+
*/
|
|
100
|
+
get renderer(){
|
|
101
|
+
return this.#renderer;
|
|
102
|
+
}
|
|
103
|
+
|
|
95
104
|
/**
|
|
96
105
|
* Trades internal resolution for frame time — see {@link dynamic_resolution}.
|
|
97
106
|
*
|
|
@@ -64,11 +64,11 @@ export class FluidObstacleSystem extends System<any> {
|
|
|
64
64
|
/**
|
|
65
65
|
* @param {FluidComponent} fluid
|
|
66
66
|
*/
|
|
67
|
-
static "__#
|
|
67
|
+
static "__#349@#refresh_masks"(fluid: FluidComponent): void;
|
|
68
68
|
/**
|
|
69
69
|
* @param {FluidComponent} fluid
|
|
70
70
|
*/
|
|
71
|
-
static "__#
|
|
71
|
+
static "__#349@#clear_field"(fluid: FluidComponent): void;
|
|
72
72
|
/**
|
|
73
73
|
* Mark every cell of `fluid` whose centre lies within `inflation` of the
|
|
74
74
|
* posed shape as solid. Iteration is clipped to the shape's world AABB
|
|
@@ -80,7 +80,7 @@ export class FluidObstacleSystem extends System<any> {
|
|
|
80
80
|
* @param {number} inflation world-units SDF threshold
|
|
81
81
|
* @param {Float64Array} point length-3 scratch
|
|
82
82
|
*/
|
|
83
|
-
static "__#
|
|
83
|
+
static "__#349@#voxelize"(fluid: FluidComponent, posed: PosedShape3D, aabb: Float64Array, inflation: number, point: Float64Array): void;
|
|
84
84
|
/**
|
|
85
85
|
* Write the obstacle's translation velocity onto every face of every cell
|
|
86
86
|
* it voxelized — the moving-wall boundary condition. Runs AFTER the mask
|
|
@@ -100,7 +100,7 @@ export class FluidObstacleSystem extends System<any> {
|
|
|
100
100
|
* @param {number} wvy
|
|
101
101
|
* @param {number} wvz
|
|
102
102
|
*/
|
|
103
|
-
static "__#
|
|
103
|
+
static "__#349@#stamp_wall_velocity"(fluid: FluidComponent, posed: PosedShape3D, aabb: Float64Array, inflation: number, point: Float64Array, wvx: number, wvy: number, wvz: number): void;
|
|
104
104
|
constructor();
|
|
105
105
|
dependencies: (typeof FluidObstacle)[];
|
|
106
106
|
components_used: (ResourceAccessSpecification<typeof Transform> | ResourceAccessSpecification<typeof RigidBody> | ResourceAccessSpecification<typeof Collider> | ResourceAccessSpecification<typeof FluidComponent> | ResourceAccessSpecification<typeof FluidObstacle>)[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"narrowphase_step.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/narrowphase_step.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"narrowphase_step.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/narrowphase_step.js"],"names":[],"mappings":"AA2zCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qDAVW,YAAY,GAAC,MAAM,EAAE,iCAGrB;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,QAC5B;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,iCAErC;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,QAC5B;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,GACnC,MAAM,CAuClB;AAED;;;;;;;;;;;GAWG;AACH,uFALW,MAAM,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,CAAC,QA4LlE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,uEAJW,MAAM,UACN,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,UACjD,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,QA2H3D"}
|
|
@@ -3,7 +3,7 @@ import { Triangle3D } from "../../../core/geom/3d/shape/Triangle3D.js";
|
|
|
3
3
|
import { body_id_index } from "../body/BodyStorage.js";
|
|
4
4
|
import { combine_friction, combine_restitution } from "../contact/combine_material.js";
|
|
5
5
|
import { CONTACT_STRIDE, MAX_CONTACTS_PER_MANIFOLD } from "../contact/ManifoldStore.js";
|
|
6
|
-
import { gjk_epa_penetration } from "../../../core/geom/3d/gjk/gjk_epa_penetration.js";
|
|
6
|
+
import { GJK_SEPARATED, gjk_epa_penetration } from "../../../core/geom/3d/gjk/gjk_epa_penetration.js";
|
|
7
7
|
import { reduce_manifold_contacts } from "./reduce_manifold_contacts.js";
|
|
8
8
|
import { mpr } from "../../../core/geom/3d/gjk/mpr.js";
|
|
9
9
|
import { box_box_manifold, BOX_BOX_OUT_LENGTH } from "./box_box_manifold.js";
|
|
@@ -1124,12 +1124,15 @@ function dispatch_pair(count, colA, trA, colB, trB) {
|
|
|
1124
1124
|
// expanding_polytope_algorithm pair was replaced because it returned
|
|
1125
1125
|
// a non-minimal axis on degenerate simplices (see memory:
|
|
1126
1126
|
// feedback_epa_unreliable_polytopes); MPR is kept as a secondary
|
|
1127
|
-
// safety net for the
|
|
1127
|
+
// safety net for the case EPA degenerates on a real overlap, or
|
|
1128
|
+
// GJK reaches no verdict — never for a triangle GJK proved clear.
|
|
1128
1129
|
let ex, ey, ez, depth;
|
|
1129
1130
|
const pen_depth = gjk_epa_penetration(epa_result, posed_a, posed_b);
|
|
1130
1131
|
if (pen_depth > 0 && Number.isFinite(pen_depth)) {
|
|
1131
1132
|
ex = epa_result[0] * pen_depth; ey = epa_result[1] * pen_depth; ez = epa_result[2] * pen_depth;
|
|
1132
1133
|
depth = pen_depth;
|
|
1134
|
+
} else if (pen_depth === GJK_SEPARATED) {
|
|
1135
|
+
continue;
|
|
1133
1136
|
} else {
|
|
1134
1137
|
if (!mpr(epa_result, 0, posed_a, posed_b)) continue;
|
|
1135
1138
|
ex = epa_result[0]; ey = epa_result[1]; ez = epa_result[2];
|
|
@@ -1247,7 +1250,22 @@ function dispatch_pair(count, colA, trA, colB, trB) {
|
|
|
1247
1250
|
if (pen_depth > 0 && Number.isFinite(pen_depth)) {
|
|
1248
1251
|
ex = epa_result[0] * pen_depth; ey = epa_result[1] * pen_depth; ez = epa_result[2] * pen_depth;
|
|
1249
1252
|
depth = pen_depth;
|
|
1253
|
+
} else if (pen_depth === GJK_SEPARATED) {
|
|
1254
|
+
// GJK proved the pair apart — the answer, not a gap in the query, so
|
|
1255
|
+
// the safety net below has nothing to add and must not be consulted.
|
|
1256
|
+
// This branch is the common case on this path, not an edge: the
|
|
1257
|
+
// broadphase hands over every pair whose fat AABBs touch, and most of
|
|
1258
|
+
// them are near misses. Asking MPR about them is what turned a clear
|
|
1259
|
+
// gap into a ContactBegin carrying a positive depth equal to the gap
|
|
1260
|
+
// — for a sphere near a large ConvexHullShape3D face, which is every
|
|
1261
|
+
// projectile in a brush-built level (mpr.js's separating-plane
|
|
1262
|
+
// certificate now refuses that answer at the source too).
|
|
1263
|
+
return count;
|
|
1250
1264
|
} else {
|
|
1265
|
+
// No usable axis: EPA degenerated on a real overlap, or GJK itself
|
|
1266
|
+
// reached no verdict (its search direction collapses on an EXACTLY
|
|
1267
|
+
// touching pair). Neither is a separation, so the net still gets to
|
|
1268
|
+
// look. THIS is what it is for.
|
|
1251
1269
|
if (!mpr(epa_result, 0, posed_a, posed_b)) return count;
|
|
1252
1270
|
ex = epa_result[0]; ey = epa_result[1]; ez = epa_result[2];
|
|
1253
1271
|
depth = Math.sqrt(ex * ex + ey * ey + ez * ez);
|
|
@@ -68,36 +68,59 @@ union, and each clone at its own arena address on a uniform stride.
|
|
|
68
68
|
## Assets
|
|
69
69
|
|
|
70
70
|
`/test_assets/Michelle.glb`, which is gitignored and unpublished — see `test_assets/README.md`.
|
|
71
|
-
|
|
72
|
-
## What the floor disagreement turned out to be
|
|
73
|
-
|
|
74
|
-
For a while the two images disagreed about where the floor ends: a smooth band along its far edge
|
|
75
|
-
that the rasterizer drew and the tracer did not, several thousand pixels at an ordinary camera and
|
|
76
|
-
growing as the camera approached the edge. It read like a hole in a tree, and it was not one.
|
|
77
|
-
|
|
78
|
-
The slab was placed by writing `Node3D.transform_global`, which is **derived** — the doc comment on
|
|
79
|
-
it says so. Its two consumers take placement from opposite ends: `Mesh.updateBoundsBasic` reads
|
|
80
|
-
`transform_global` and publishes the result as the mesh's `meshes` row, and the GPU's `transforms`
|
|
81
|
-
row carries the node's *local* TRS for the hierarchy pass to compose `node.global` from. Writing
|
|
82
|
-
only the first left the second at identity, 0.208 m away:
|
|
83
|
-
|
|
84
|
-
- the rasterizer multiplied positions by `node.global` and drew the slab at the geometry's own
|
|
85
|
-
origin, its far edge ten rows higher up the screen than the floor actually ends;
|
|
86
|
-
- the tracer tested each ray against the *translated* published box before traversing the
|
|
87
|
-
*untranslated* geometry, so rays that would have struck the slab near its far edge were turned
|
|
88
|
-
away by a box that was not around it.
|
|
89
|
-
|
|
90
|
-
Neither renderer was wrong about the data it was given. Padding the published box shrank the band
|
|
91
|
-
because it widened the interval the TLAS test allows — not because anything was tangent, which is
|
|
92
|
-
why a few per cent of pad did what a rounding-scale slack could not.
|
|
93
|
-
|
|
94
|
-
The invariant is in `add_floor.spec.js` and needs no device: published bounds must survive
|
|
95
|
-
`updateMatrices`, because that is the walk which recomputes `transform_global` from the local
|
|
96
|
-
transform. `chunk_ray_query_nearest.spec.js` holds the traversal itself against closed-form slab
|
|
97
|
-
arithmetic in the emulator, including the case where the instance box and the node transform
|
|
98
|
-
disagree.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
71
|
+
|
|
72
|
+
## What the floor disagreement turned out to be
|
|
73
|
+
|
|
74
|
+
For a while the two images disagreed about where the floor ends: a smooth band along its far edge
|
|
75
|
+
that the rasterizer drew and the tracer did not, several thousand pixels at an ordinary camera and
|
|
76
|
+
growing as the camera approached the edge. It read like a hole in a tree, and it was not one.
|
|
77
|
+
|
|
78
|
+
The slab was placed by writing `Node3D.transform_global`, which is **derived** — the doc comment on
|
|
79
|
+
it says so. Its two consumers take placement from opposite ends: `Mesh.updateBoundsBasic` reads
|
|
80
|
+
`transform_global` and publishes the result as the mesh's `meshes` row, and the GPU's `transforms`
|
|
81
|
+
row carries the node's *local* TRS for the hierarchy pass to compose `node.global` from. Writing
|
|
82
|
+
only the first left the second at identity, 0.208 m away:
|
|
83
|
+
|
|
84
|
+
- the rasterizer multiplied positions by `node.global` and drew the slab at the geometry's own
|
|
85
|
+
origin, its far edge ten rows higher up the screen than the floor actually ends;
|
|
86
|
+
- the tracer tested each ray against the *translated* published box before traversing the
|
|
87
|
+
*untranslated* geometry, so rays that would have struck the slab near its far edge were turned
|
|
88
|
+
away by a box that was not around it.
|
|
89
|
+
|
|
90
|
+
Neither renderer was wrong about the data it was given. Padding the published box shrank the band
|
|
91
|
+
because it widened the interval the TLAS test allows — not because anything was tangent, which is
|
|
92
|
+
why a few per cent of pad did what a rounding-scale slack could not.
|
|
93
|
+
|
|
94
|
+
The invariant is in `add_floor.spec.js` and needs no device: published bounds must survive
|
|
95
|
+
`updateMatrices`, because that is the walk which recomputes `transform_global` from the local
|
|
96
|
+
transform. `chunk_ray_query_nearest.spec.js` holds the traversal itself against closed-form slab
|
|
97
|
+
arithmetic in the emulator, including the case where the instance box and the node transform
|
|
98
|
+
disagree.
|
|
99
|
+
|
|
100
|
+
## The quieter version of the same bug: `?count=N`
|
|
101
|
+
|
|
102
|
+
The dancers were placed the same way — by writing `transform_global` — and stacked at the origin
|
|
103
|
+
rather than spreading over a grid. Both renderers agreed about it, so there was no picture to look
|
|
104
|
+
at; the only symptom was that the characters did not go where they were put.
|
|
105
|
+
|
|
106
|
+
They also could not have been fixed by copying the floor's fix, because a skinned mesh's own node
|
|
107
|
+
is not its placement point at all. A skinned vertex lands at
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
mesh.global × inverse(mesh.global) × joint.global × inverse_bind × v
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
— `chunk_skin_blend_mesh_local` folds the leftmost inverse into each joint matrix and the rasterizer
|
|
114
|
+
re-applies `mesh.global`, so the mesh node's transform **cancels exactly**, and
|
|
115
|
+
`compute_skin_world_bounds` composes the published box out of joints without reading it either.
|
|
116
|
+
Michelle's file makes the point structurally: her mesh node `Ch03` carries no local transform, the
|
|
117
|
+
0.01 centimetre scale and the up-axis rotation sit on the root above it, and the joint root
|
|
118
|
+
`mixamorig:Hips` is the mesh node's *sibling*. Nothing under `Ch03` drives a joint.
|
|
119
|
+
|
|
120
|
+
`place_character` therefore hangs each character's roots under a fresh parent whose whole transform
|
|
121
|
+
is the grid offset. That reaches the joints, leaves the file's own transform untouched below it, and
|
|
122
|
+
— because the offset is a *local* transform — survives the walk that recomputes `transform_global`.
|
|
123
|
+
|
|
124
|
+
`place_character.spec.js` holds the invariant the floor's cannot see: **published bounds must move
|
|
125
|
+
by the offset the character was placed at.** Its negative control is the bug itself — writing either
|
|
126
|
+
transform on the mesh node and watching the box not move.
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* actually sit under.
|
|
20
20
|
*
|
|
21
21
|
* Michelle is the shape that makes this concrete. Her file is one root carrying the centimetre
|
|
22
|
-
* scale and the
|
|
22
|
+
* scale and the up-axis rotation (+90° about x, the file's Z-up into the renderer's Y-up), with the
|
|
23
|
+
* mesh node and the joint root as *siblings* beneath it:
|
|
23
24
|
*
|
|
24
25
|
* ```
|
|
25
26
|
* "Character" R=(0.70711, 0, 0, 0.70711) S=(0.01, 0.01, 0.01)
|