@neutrinoparticles/js-v1.1-phaser 1.0.0 → 1.1.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.
@@ -1,6 +1,6 @@
1
1
  var K = Object.defineProperty;
2
- var k = (s, e, t) => e in s ? K(s, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : s[e] = t;
3
- var n = (s, e, t) => k(s, typeof e != "symbol" ? e + "" : e, t);
2
+ var z = (s, e, t) => e in s ? K(s, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : s[e] = t;
3
+ var n = (s, e, t) => z(s, typeof e != "symbol" ? e + "" : e, t);
4
4
  import Phaser from "phaser";
5
5
  import * as Neutrino from "@neutrinoparticles/js-v1.1";
6
6
  const vertexShaderSource = `#version 300 es
@@ -54,7 +54,8 @@ vec4 fetchTexel(int globalTexelIndex) {
54
54
  }
55
55
 
56
56
  // Strip geometry helpers (port of GPU33 makeScreenSpaceStrip).
57
- // 2D screen-space variant PIXI7 renderer is 2D, world Z is dropped at gl_Position.
57
+ // 2D screen-space variant used by the Faced strip frame; world Z reaches
58
+ // gl_Position only through uViewProjMatrix (ortho or v1.0-parity perspective).
58
59
  vec2 stripOrtho(vec2 v) {
59
60
  return vec2(-v.y, v.x);
60
61
  }
@@ -91,9 +92,9 @@ void main() {
91
92
  // 0 (Faced): screen-space ortho to dir12 — equivalent to camera-faced
92
93
  // billboard normal in a 2D camera setup.
93
94
  // 1 (Free): the quaternion stored in texel 2 nominally encodes a 3D
94
- // orientation. The current PIXI7 renderer is fundamentally
95
- // 2D (worldPos.z is dropped at gl_Position for quads), so
96
- // the quat is read but its 3D y-axis is projected to screen
95
+ // orientation. The PIXI7 renderer has no true 3D camera
96
+ // (uViewProjMatrix is ortho or the v1.0-parity perspective),
97
+ // so the quat is read but its 3D y-axis is projected to screen
97
98
  // and used as the section normal. When the projection
98
99
  // degenerates (looking along the y-axis) we fall back to
99
100
  // the screen-space ortho. Real 3D free-rotation requires a
@@ -128,6 +129,10 @@ void main() {
128
129
  float((packedColorNex >> 8u) & 0xFFu) / 255.0,
129
130
  float((packedColorNex >> 16u) & 0xFFu) / 255.0,
130
131
  float((packedColorNex >> 24u) & 0xFFu) / 255.0);
132
+ // HDR color scale S (RGBS, bloom TDD #175 §13.12): offset+60 (t3.a) per-particle
133
+ // float scale; multiply BEFORE the cross-fade. S=1 for LDR (no-op).
134
+ color1.rgb *= (isCur ? t3.a : ts3.a);
135
+ color2.rgb *= (isCur ? ts3.a : t3.a);
131
136
 
132
137
  // neighborIdxP0 / neighborIdxP3 are stored on every meta of a pair
133
138
  // (both meta_cur and meta_nex hold the same pair of indices), so we
@@ -174,6 +179,10 @@ void main() {
174
179
  // by the sign of w). Forces a discard outside the [-w,w] cube at the end.
175
180
  bool nearPlaneClipped = (sp1.w < 1e-3) || (sp2.w < 1e-3);
176
181
 
182
+ // #236: set when the segment itself is degenerate (much shorter than
183
+ // the ribbon half-width) — the pair is discarded like nearPlaneClipped.
184
+ bool stripDegenerate = false;
185
+
177
186
  // Aspect correction so the 2D Faced ortho gives visually-perpendicular
178
187
  // dirs on non-square viewports (inverted before gl_Position).
179
188
  vec2 aspectMul = vec2(1.0, 1.0 / max(uViewportAspect, 1e-6));
@@ -211,9 +220,15 @@ void main() {
211
220
  float wlen01 = length(wp01);
212
221
  float wlen12 = length(wp12);
213
222
  float wlen23 = length(wp23);
223
+ // #236: degeneracy guards at visual scale (mirror of GPU33).
224
+ // A segment much shorter than the ribbon half-width cannot define
225
+ // a visually meaningful direction; a degenerate neighbour falls
226
+ // back to the straight cap instead of a noise knee.
227
+ float wDegenEps = max(max(abs(halfSize1), abs(halfSize2)) * 0.02, 1e-6);
228
+ if (wlen12 < wDegenEps) stripDegenerate = true;
214
229
  vec3 wdir12 = (wlen12 > 1e-6) ? (wp12 / wlen12) : vec3(1.0, 0.0, 0.0);
215
- vec3 wdir01 = (wlen01 > 1e-6) ? (wp01 / wlen01) : wdir12;
216
- vec3 wdir23 = (wlen23 > 1e-6) ? (wp23 / wlen23) : wdir12;
230
+ vec3 wdir01 = (wlen01 > wDegenEps) ? (wp01 / wlen01) : wdir12;
231
+ vec3 wdir23 = (wlen23 > wDegenEps) ? (wp23 / wlen23) : wdir12;
217
232
  noKnee1 = dot(wdir01, wdir12) > 0.999;
218
233
  noKnee2 = dot(wdir12, wdir23) > 0.999;
219
234
  pathLen12 = wlen12;
@@ -225,9 +240,12 @@ void main() {
225
240
  vec3 K1 = Kmul1 * normalize(cross(wdir01 + wdir12, n1));
226
241
  Bmul1 = sign(dot(B1raw, K1));
227
242
  vec3 B1 = B1raw * Bmul1;
228
- float ik1 = halfSize1 / max(dot(B1, K1), 1e-6);
229
- if (dot(-wdir01, ik1 * -K1) > wlen01) ik1 = wlen01;
230
- if (dot( wdir12, ik1 * -K1) > wlen12) ik1 = wlen12;
243
+ // #236: inner vertex at the ribbon half-width along the
244
+ // bisector, symmetric to the outer vertex (mirror of GPU33).
245
+ // The old true-miter-with-neighbour-length-clamp cut an
246
+ // uncovered wedge out of sharp bends whenever halfSize
247
+ // exceeded the particle spacing.
248
+ float ik1 = halfSize1;
231
249
  worldK1 = p1 + K1 * halfSize1;
232
250
  worldB1 = p1 + B1 * halfSize1;
233
251
  worldIK1 = p1 - K1 * ik1;
@@ -248,9 +266,8 @@ void main() {
248
266
  vec3 K2 = Kmul2 * normalize(cross(wdir12 + wdir23, n2));
249
267
  Bmul2 = sign(dot(B2raw, K2));
250
268
  vec3 B2 = B2raw * Bmul2;
251
- float ik2 = halfSize2 / max(dot(B2, K2), 1e-6);
252
- if (dot(-wdir12, ik2 * -K2) > wlen12) ik2 = wlen12;
253
- if (dot( wdir23, ik2 * -K2) > wlen23) ik2 = wlen23;
269
+ // #236: see ik1 inner vertex at the ribbon half-width.
270
+ float ik2 = halfSize2;
254
271
  worldK2 = p2 + K2 * halfSize2;
255
272
  worldB2 = p2 + B2 * halfSize2;
256
273
  worldIK2 = p2 - K2 * ik2;
@@ -267,10 +284,16 @@ void main() {
267
284
  // Faced: screen-space NDC frame around dir12.
268
285
  vec4 sp0 = mvpMatrix * vec4(p0, 1.0);
269
286
  vec4 sp3 = mvpMatrix * vec4(p3, 1.0);
270
- vec2 ndc0 = (sp0.xy / sp0.w) * aspectMul;
271
287
  vec2 ndc1 = (sp1.xy / sp1.w) * aspectMul;
272
288
  vec2 ndc2 = (sp2.xy / sp2.w) * aspectMul;
273
- vec2 ndc3 = (sp3.xy / sp3.w) * aspectMul;
289
+ // Neighbor anchors behind the near plane produce garbage NDC after
290
+ // the perspective divide (possible only with Projection3D, where w
291
+ // varies); collapse them onto their segment anchor so the existing
292
+ // zero-length degeneracy path (dir01 = dir12 / dir23 = dir12)
293
+ // handles the knee instead of a poisoned miter. spU1/spU2 need no
294
+ // guard: uCameraUp has zero Z, so their w equals sp1.w / sp2.w.
295
+ vec2 ndc0 = (sp0.w < 1e-3) ? ndc1 : (sp0.xy / sp0.w) * aspectMul;
296
+ vec2 ndc3 = (sp3.w < 1e-3) ? ndc2 : (sp3.xy / sp3.w) * aspectMul;
274
297
 
275
298
  vec2 d01 = ndc1 - ndc0;
276
299
  vec2 d12 = ndc2 - ndc1;
@@ -278,12 +301,6 @@ void main() {
278
301
  float len01 = length(d01);
279
302
  float len12 = length(d12);
280
303
  float len23 = length(d23);
281
- vec2 dir12 = (len12 > 1e-6) ? (d12 / len12) : vec2(1.0, 0.0);
282
- vec2 dir01 = (len01 > 1e-6) ? (d01 / len01) : dir12;
283
- vec2 dir23 = (len23 > 1e-6) ? (d23 / len23) : dir12;
284
- noKnee1 = dot(dir01, dir12) > 0.999;
285
- noKnee2 = dot(dir12, dir23) > 0.999;
286
- pathLen12 = len12;
287
304
 
288
305
  // Section magnitude: project camera_up*halfSize per anchor into NDC.
289
306
  vec4 spU1 = mvpMatrix * vec4(p1 + uCameraUp * halfSize1, 1.0);
@@ -291,15 +308,25 @@ void main() {
291
308
  float ndcHalfSize1 = length((spU1.xy / spU1.w) * aspectMul - ndc1);
292
309
  float ndcHalfSize2 = length((spU2.xy / spU2.w) * aspectMul - ndc2);
293
310
 
311
+ // #236: degeneracy guards at visual scale (mirror of GPU33).
312
+ float degenEps = max(max(ndcHalfSize1, ndcHalfSize2) * 0.02, 1e-6);
313
+ if (len12 < degenEps) stripDegenerate = true;
314
+ vec2 dir12 = (len12 > 1e-6) ? (d12 / len12) : vec2(1.0, 0.0);
315
+ vec2 dir01 = (len01 > degenEps) ? (d01 / len01) : dir12;
316
+ vec2 dir23 = (len23 > degenEps) ? (d23 / len23) : dir12;
317
+ noKnee1 = dot(dir01, dir12) > 0.999;
318
+ noKnee2 = dot(dir12, dir23) > 0.999;
319
+ pathLen12 = len12;
320
+
294
321
  vec2 B = stripOrtho(dir12);
295
322
  if (!noKnee1) {
296
323
  float Kmul1 = -sign(dot(stripOrtho(dir01), dir12));
297
324
  vec2 K1 = Kmul1 * normalize(stripOrtho(dir01 + dir12));
298
325
  Bmul1 = sign(dot(B, K1));
299
326
  vec2 B1 = B * Bmul1;
300
- float ik1 = ndcHalfSize1 / max(dot(B1, K1), 1e-6);
301
- if (dot(-dir01, ik1 * -K1) > len01) ik1 = len01;
302
- if (dot(dir12, ik1 * -K1) > len12) ik1 = len12;
327
+ // #236: inner vertex at the ribbon half-width (see the Free
328
+ // branch comment above).
329
+ float ik1 = ndcHalfSize1;
303
330
  k1 = ndc1 + K1 * ndcHalfSize1;
304
331
  b1 = ndc1 + B1 * ndcHalfSize1;
305
332
  IK1 = ndc1 - K1 * ik1;
@@ -317,9 +344,8 @@ void main() {
317
344
  vec2 K2 = Kmul2 * normalize(stripOrtho(dir12 + dir23));
318
345
  Bmul2 = sign(dot(B, K2));
319
346
  vec2 B2 = B * Bmul2;
320
- float ik2 = ndcHalfSize2 / max(dot(B2, K2), 1e-6);
321
- if (dot(-dir12, ik2 * -K2) > len12) ik2 = len12;
322
- if (dot(dir23, ik2 * -K2) > len23) ik2 = len23;
347
+ // #236: see ik1 inner vertex at the ribbon half-width.
348
+ float ik2 = ndcHalfSize2;
323
349
  k2 = ndc2 + K2 * ndcHalfSize2;
324
350
  b2 = ndc2 + B2 * ndcHalfSize2;
325
351
  IK2 = ndc2 - K2 * ik2;
@@ -334,6 +360,34 @@ void main() {
334
360
  }
335
361
  }
336
362
 
363
+ // #236: a noKnee (straight-cap) end carries no bend side of its own —
364
+ // its raw side label is always the +B side. If the segment's other
365
+ // end is a real knee on the -B side, the mismatched labels would send
366
+ // the ladder into the opposite-sides (twist) branch and flip the
367
+ // texture V across a segment that does not actually change bend side.
368
+ // Relabel the straight end to the knee end's side (swap its k/IK and
369
+ // re-collapse b onto the new k) so the ladder stays untwisted. Both
370
+ // frames are swapped; only the active one is meaningful.
371
+ if (noKnee1 && !noKnee2 && Bmul2 < 0.0) {
372
+ // Swap only the ACTIVE frame's variables - the inactive frame's
373
+ // are uninitialized and must not be read.
374
+ if (isFreeRotation) {
375
+ vec3 tRw1 = worldK1; worldK1 = worldIK1; worldIK1 = tRw1; worldB1 = worldK1;
376
+ } else {
377
+ vec2 tRl1 = k1; k1 = IK1; IK1 = tRl1; b1 = k1;
378
+ }
379
+ Bmul1 = Bmul2;
380
+ }
381
+ if (noKnee2 && !noKnee1 && Bmul1 < 0.0) {
382
+ if (isFreeRotation) {
383
+ vec3 tRw2 = worldK2; worldK2 = worldIK2; worldIK2 = tRw2; worldB2 = worldK2;
384
+ } else {
385
+ vec2 tRl2 = k2; k2 = IK2; IK2 = tRl2; b2 = k2;
386
+ }
387
+ Bmul2 = Bmul1;
388
+ }
389
+
390
+
337
391
  // V coords (mirror of preview3 / GPU33 make*Strip):
338
392
  // V at outer knee (k) = (Bmul + 1) * 0.5
339
393
  // V at inner knee (IK) = 1 - V_outer ; bend (b) = outer
@@ -435,7 +489,10 @@ void main() {
435
489
 
436
490
  // Free projects the world-space vertex through MVP directly; Faced
437
491
  // reconstructs clip space from aspect-corrected NDC (mirror of preview3).
438
- if (isFreeRotation) {
492
+ if (stripDegenerate) {
493
+ // #236: sub-visual segment — discard the pair entirely.
494
+ gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
495
+ } else if (isFreeRotation) {
439
496
  gl_Position = mvpMatrix * vec4(vertPos3D, 1.0);
440
497
  } else if (nearPlaneClipped) {
441
498
  // At least one anchor is at / behind the near plane → the NDC math
@@ -472,6 +529,10 @@ void main() {
472
529
  float((packedColor >> 16u) & 0xFFu) / 255.0,
473
530
  float((packedColor >> 24u) & 0xFFu) / 255.0
474
531
  );
532
+ // HDR color scale S (RGBS, bloom TDD #175 §13.12): offset+60 (t3.a) is a
533
+ // per-particle float scale. RGB was normalized by S at pack time; restore HDR
534
+ // here. S=1 for LDR colors (no-op). Alpha is untouched.
535
+ vColor.rgb *= t3.a;
475
536
 
476
537
  uint gridConfig = floatBitsToUint(t3.g);
477
538
  float gridWidth = max(float(gridConfig & 0xFFFFu), 1.0);
@@ -519,7 +580,23 @@ void main() {
519
580
 
520
581
  mat4 mvpMatrix = uViewProjMatrix * uModelMatrix;
521
582
  vec3 worldPos = position + posDisp;
522
- gl_Position = mvpMatrix * vec4(worldPos.xy, 0.0, 1.0);
583
+ gl_Position = mvpMatrix * vec4(worldPos, 1.0);
584
+
585
+ // v1.0-parity near-plane culling for the perspective projection: the
586
+ // whole quad is discarded when ANY of its 4 corners is behind the near
587
+ // plane (w < 0.01 == pos.z > 0.99 * z_cam in PerspectiveProjection
588
+ // terms). w is linear in position, so the corner minimum comes from the
589
+ // w-row and the corner offsets (x in {-ox, 1-ox}, y in {-oy, 1-oy}).
590
+ // Without a projection every w is 1.0 and this is a no-op.
591
+ vec4 wRow = vec4(mvpMatrix[0].w, mvpMatrix[1].w, mvpMatrix[2].w, mvpMatrix[3].w);
592
+ float wx = dot(wRow.xyz, xaxis);
593
+ float wy = dot(wRow.xyz, yaxis);
594
+ float minW = dot(wRow.xyz, position) + wRow.w
595
+ - origin.x * wx + min(wx, 0.0)
596
+ - origin.y * wy + min(wy, 0.0);
597
+ if (minW < 0.01) {
598
+ gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
599
+ }
523
600
 
524
601
  // Texture coordinates with grid + atlas remap
525
602
  vec2 baseTexCoord = TEX_COORD[vertexIndex];
@@ -739,6 +816,11 @@ void main() {
739
816
  // Colors: the meta M1 sample is the normalized RGBA directly.
740
817
  vec4 color1 = fetchMeta(curBase + 1.0);
741
818
  vec4 color2 = fetchMeta(nexBase + 1.0);
819
+ // HDR color scale S (RGBS, bloom TDD #175 §13.12): a plain float at offset+60,
820
+ // read straight from the float DATA texture (texel 3, .a) — like gridIndex — not
821
+ // the meta texture. Multiply BEFORE the cross-fade. S=1 → no-op.
822
+ color1.rgb *= fetchData(curBase + 3.0).a;
823
+ color2.rgb *= fetchData(nexBase + 3.0).a;
742
824
 
743
825
  // Neighbor RECORD indices (system.js convention: neighborIdxP0 on the
744
826
  // pair-cur meta, neighborIdxP3 on the pair-nex meta; boundary
@@ -770,6 +852,10 @@ void main() {
770
852
  // unaffected by the sign of w).
771
853
  bool nearPlaneClipped = (sp1.w < 1e-3) || (sp2.w < 1e-3);
772
854
 
855
+ // #236: set when the segment itself is degenerate (much shorter than
856
+ // the ribbon half-width) - the pair is discarded like nearPlaneClipped.
857
+ bool stripDegenerate = false;
858
+
773
859
  // Aspect correction for the 2D Faced ortho on non-square viewports.
774
860
  vec2 aspectMul = vec2(1.0, 1.0 / max(uViewportAspect, 1e-6));
775
861
 
@@ -812,9 +898,12 @@ void main() {
812
898
  float wlen01 = length(wp01);
813
899
  float wlen12 = length(wp12);
814
900
  float wlen23 = length(wp23);
901
+ // #236: degeneracy guards at visual scale (mirror of GPU33).
902
+ float wDegenEps = max(max(abs(halfSize1), abs(halfSize2)) * 0.02, 1e-6);
903
+ if (wlen12 < wDegenEps) stripDegenerate = true;
815
904
  vec3 wdir12 = (wlen12 > 1e-6) ? (wp12 / wlen12) : vec3(1.0, 0.0, 0.0);
816
- vec3 wdir01 = (wlen01 > 1e-6) ? (wp01 / wlen01) : wdir12;
817
- vec3 wdir23 = (wlen23 > 1e-6) ? (wp23 / wlen23) : wdir12;
905
+ vec3 wdir01 = (wlen01 > wDegenEps) ? (wp01 / wlen01) : wdir12;
906
+ vec3 wdir23 = (wlen23 > wDegenEps) ? (wp23 / wlen23) : wdir12;
818
907
  noKnee1 = dot(wdir01, wdir12) > 0.999;
819
908
  noKnee2 = dot(wdir12, wdir23) > 0.999;
820
909
  pathLen12 = wlen12;
@@ -826,9 +915,9 @@ void main() {
826
915
  vec3 K1 = Kmul1 * normalize(cross(wdir01 + wdir12, n1));
827
916
  Bmul1 = sign(dot(B1raw, K1));
828
917
  vec3 B1 = B1raw * Bmul1;
829
- float ik1 = halfSize1 / max(dot(B1, K1), 1e-6);
830
- if (dot(-wdir01, ik1 * -K1) > wlen01) ik1 = wlen01;
831
- if (dot( wdir12, ik1 * -K1) > wlen12) ik1 = wlen12;
918
+ // #236: inner vertex at the ribbon half-width (mirror of
919
+ // GPU33; see the ES 3.00 shader for the rationale).
920
+ float ik1 = halfSize1;
832
921
  worldK1 = p1 + K1 * halfSize1;
833
922
  worldB1 = p1 + B1 * halfSize1;
834
923
  worldIK1 = p1 - K1 * ik1;
@@ -849,9 +938,8 @@ void main() {
849
938
  vec3 K2 = Kmul2 * normalize(cross(wdir12 + wdir23, n2));
850
939
  Bmul2 = sign(dot(B2raw, K2));
851
940
  vec3 B2 = B2raw * Bmul2;
852
- float ik2 = halfSize2 / max(dot(B2, K2), 1e-6);
853
- if (dot(-wdir12, ik2 * -K2) > wlen12) ik2 = wlen12;
854
- if (dot( wdir23, ik2 * -K2) > wlen23) ik2 = wlen23;
941
+ // #236: see ik1 - inner vertex at the ribbon half-width.
942
+ float ik2 = halfSize2;
855
943
  worldK2 = p2 + K2 * halfSize2;
856
944
  worldB2 = p2 + B2 * halfSize2;
857
945
  worldIK2 = p2 - K2 * ik2;
@@ -868,10 +956,16 @@ void main() {
868
956
  // Faced: screen-space NDC frame around dir12.
869
957
  vec4 sp0 = mvpMatrix * vec4(p0, 1.0);
870
958
  vec4 sp3 = mvpMatrix * vec4(p3, 1.0);
871
- vec2 ndc0 = (sp0.xy / sp0.w) * aspectMul;
872
959
  vec2 ndc1 = (sp1.xy / sp1.w) * aspectMul;
873
960
  vec2 ndc2 = (sp2.xy / sp2.w) * aspectMul;
874
- vec2 ndc3 = (sp3.xy / sp3.w) * aspectMul;
961
+ // Neighbor anchors behind the near plane produce garbage NDC after
962
+ // the perspective divide (possible only with Projection3D, where w
963
+ // varies); collapse them onto their segment anchor so the existing
964
+ // zero-length degeneracy path (dir01 = dir12 / dir23 = dir12)
965
+ // handles the knee instead of a poisoned miter. spU1/spU2 need no
966
+ // guard: uCameraUp has zero Z, so their w equals sp1.w / sp2.w.
967
+ vec2 ndc0 = (sp0.w < 1e-3) ? ndc1 : (sp0.xy / sp0.w) * aspectMul;
968
+ vec2 ndc3 = (sp3.w < 1e-3) ? ndc2 : (sp3.xy / sp3.w) * aspectMul;
875
969
 
876
970
  vec2 d01 = ndc1 - ndc0;
877
971
  vec2 d12 = ndc2 - ndc1;
@@ -879,12 +973,6 @@ void main() {
879
973
  float len01 = length(d01);
880
974
  float len12 = length(d12);
881
975
  float len23 = length(d23);
882
- vec2 dir12 = (len12 > 1e-6) ? (d12 / len12) : vec2(1.0, 0.0);
883
- vec2 dir01 = (len01 > 1e-6) ? (d01 / len01) : dir12;
884
- vec2 dir23 = (len23 > 1e-6) ? (d23 / len23) : dir12;
885
- noKnee1 = dot(dir01, dir12) > 0.999;
886
- noKnee2 = dot(dir12, dir23) > 0.999;
887
- pathLen12 = len12;
888
976
 
889
977
  // Section magnitude: project camera_up*halfSize per anchor into NDC.
890
978
  vec4 spU1 = mvpMatrix * vec4(p1 + uCameraUp * halfSize1, 1.0);
@@ -892,15 +980,24 @@ void main() {
892
980
  float ndcHalfSize1 = length((spU1.xy / spU1.w) * aspectMul - ndc1);
893
981
  float ndcHalfSize2 = length((spU2.xy / spU2.w) * aspectMul - ndc2);
894
982
 
983
+ // #236: degeneracy guards at visual scale (mirror of GPU33).
984
+ float degenEps = max(max(ndcHalfSize1, ndcHalfSize2) * 0.02, 1e-6);
985
+ if (len12 < degenEps) stripDegenerate = true;
986
+ vec2 dir12 = (len12 > 1e-6) ? (d12 / len12) : vec2(1.0, 0.0);
987
+ vec2 dir01 = (len01 > degenEps) ? (d01 / len01) : dir12;
988
+ vec2 dir23 = (len23 > degenEps) ? (d23 / len23) : dir12;
989
+ noKnee1 = dot(dir01, dir12) > 0.999;
990
+ noKnee2 = dot(dir12, dir23) > 0.999;
991
+ pathLen12 = len12;
992
+
895
993
  vec2 B = stripOrtho(dir12);
896
994
  if (!noKnee1) {
897
995
  float Kmul1 = -sign(dot(stripOrtho(dir01), dir12));
898
996
  vec2 K1 = Kmul1 * normalize(stripOrtho(dir01 + dir12));
899
997
  Bmul1 = sign(dot(B, K1));
900
998
  vec2 B1 = B * Bmul1;
901
- float ik1 = ndcHalfSize1 / max(dot(B1, K1), 1e-6);
902
- if (dot(-dir01, ik1 * -K1) > len01) ik1 = len01;
903
- if (dot(dir12, ik1 * -K1) > len12) ik1 = len12;
999
+ // #236: inner vertex at the ribbon half-width.
1000
+ float ik1 = ndcHalfSize1;
904
1001
  k1 = ndc1 + K1 * ndcHalfSize1;
905
1002
  b1 = ndc1 + B1 * ndcHalfSize1;
906
1003
  IK1 = ndc1 - K1 * ik1;
@@ -918,9 +1015,8 @@ void main() {
918
1015
  vec2 K2 = Kmul2 * normalize(stripOrtho(dir12 + dir23));
919
1016
  Bmul2 = sign(dot(B, K2));
920
1017
  vec2 B2 = B * Bmul2;
921
- float ik2 = ndcHalfSize2 / max(dot(B2, K2), 1e-6);
922
- if (dot(-dir12, ik2 * -K2) > len12) ik2 = len12;
923
- if (dot(dir23, ik2 * -K2) > len23) ik2 = len23;
1018
+ // #236: see ik1 - inner vertex at the ribbon half-width.
1019
+ float ik2 = ndcHalfSize2;
924
1020
  k2 = ndc2 + K2 * ndcHalfSize2;
925
1021
  b2 = ndc2 + B2 * ndcHalfSize2;
926
1022
  IK2 = ndc2 - K2 * ik2;
@@ -935,6 +1031,33 @@ void main() {
935
1031
  }
936
1032
  }
937
1033
 
1034
+ // #236: a noKnee (straight-cap) end carries no bend side of its own -
1035
+ // its raw side label is always the +B side. If the segment's other
1036
+ // end is a real knee on the -B side, the mismatched labels would send
1037
+ // the ladder into the opposite-sides (twist) branch and flip the
1038
+ // texture V across a segment that does not actually change bend side.
1039
+ // Relabel the straight end to the knee end's side (swap its k/IK and
1040
+ // re-collapse b onto the new k) so the ladder stays untwisted. Both
1041
+ // frames are swapped; only the active one is meaningful.
1042
+ if (noKnee1 && !noKnee2 && Bmul2 < 0.0) {
1043
+ // Swap only the ACTIVE frame's variables - the inactive frame's
1044
+ // are uninitialized and must not be read.
1045
+ if (isFreeRotation) {
1046
+ vec3 tRw1 = worldK1; worldK1 = worldIK1; worldIK1 = tRw1; worldB1 = worldK1;
1047
+ } else {
1048
+ vec2 tRl1 = k1; k1 = IK1; IK1 = tRl1; b1 = k1;
1049
+ }
1050
+ Bmul1 = Bmul2;
1051
+ }
1052
+ if (noKnee2 && !noKnee1 && Bmul1 < 0.0) {
1053
+ if (isFreeRotation) {
1054
+ vec3 tRw2 = worldK2; worldK2 = worldIK2; worldIK2 = tRw2; worldB2 = worldK2;
1055
+ } else {
1056
+ vec2 tRl2 = k2; k2 = IK2; IK2 = tRl2; b2 = k2;
1057
+ }
1058
+ Bmul2 = Bmul1;
1059
+ }
1060
+
938
1061
  // V coords: V at outer knee (k) = (Bmul + 1) * 0.5; inner = 1 - outer.
939
1062
  float Vtop1 = (Bmul1 + 1.0) * 0.5;
940
1063
  float Vbot1 = 1.0 - Vtop1;
@@ -1006,7 +1129,10 @@ void main() {
1006
1129
 
1007
1130
  // Free projects the world-space vertex through MVP directly; Faced
1008
1131
  // reconstructs clip space from aspect-corrected NDC.
1009
- if (isFreeRotation) {
1132
+ if (stripDegenerate) {
1133
+ // #236: sub-visual segment - discard the pair entirely.
1134
+ gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
1135
+ } else if (isFreeRotation) {
1010
1136
  gl_Position = mvpMatrix * vec4(vertPos3D, 1.0);
1011
1137
  } else if (nearPlaneClipped) {
1012
1138
  gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
@@ -1031,6 +1157,9 @@ void main() {
1031
1157
 
1032
1158
  // Meta M1: the packedColor u32 LE bytes ARE the normalized RGBA sample.
1033
1159
  vColor = fetchMeta(baseTexel + 1.0);
1160
+ // HDR color scale S (RGBS, bloom TDD #175 §13.12): a plain float at offset+60,
1161
+ // read directly from the float DATA texture (t3.a) like gridIndex. S=1 → no-op.
1162
+ vColor.rgb *= t3.a;
1034
1163
 
1035
1164
  // Meta M2: gridConfig — [gridW lo, gridW hi, gridH lo, gridH hi].
1036
1165
  vec4 m2 = fetchMeta(baseTexel + 2.0);
@@ -1077,7 +1206,23 @@ void main() {
1077
1206
 
1078
1207
  mat4 mvpMatrix = uViewProjMatrix * uModelMatrix;
1079
1208
  vec3 worldPos = position + posDisp;
1080
- gl_Position = mvpMatrix * vec4(worldPos.xy, 0.0, 1.0);
1209
+ gl_Position = mvpMatrix * vec4(worldPos, 1.0);
1210
+
1211
+ // v1.0-parity near-plane culling for the perspective projection: the
1212
+ // whole quad is discarded when ANY of its 4 corners is behind the near
1213
+ // plane (w < 0.01 == pos.z > 0.99 * z_cam in PerspectiveProjection
1214
+ // terms). w is linear in position, so the corner minimum comes from the
1215
+ // w-row and the corner offsets (x in {-ox, 1-ox}, y in {-oy, 1-oy}).
1216
+ // Without a projection every w is 1.0 and this is a no-op.
1217
+ vec4 wRow = vec4(mvpMatrix[0].w, mvpMatrix[1].w, mvpMatrix[2].w, mvpMatrix[3].w);
1218
+ float wx = dot(wRow.xyz, xaxis);
1219
+ float wy = dot(wRow.xyz, yaxis);
1220
+ float minW = dot(wRow.xyz, position) + wRow.w
1221
+ - origin.x * wx + min(wx, 0.0)
1222
+ - origin.y * wy + min(wy, 0.0);
1223
+ if (minW < 0.01) {
1224
+ gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
1225
+ }
1081
1226
 
1082
1227
  // Texture coordinates with grid + atlas remap
1083
1228
  vec2 baseTexCoord = texCoordOf(corner);
@@ -1152,7 +1297,7 @@ void main() {
1152
1297
  // world alpha scales the whole RGBA (mirror of the ES 3.00 shader).
1153
1298
  gl_FragColor = texColor * vColor * uWorldAlpha;
1154
1299
  }
1155
- `, S = class S {
1300
+ `, A = class A {
1156
1301
  constructor(e, t) {
1157
1302
  n(this, "neutrino");
1158
1303
  // On a WebGL1 context this actually holds a WebGLRenderingContext; the
@@ -1165,7 +1310,7 @@ void main() {
1165
1310
  n(this, "_vao", null);
1166
1311
  // WebGL1 render path state (TDD_js-v1.1-webgl1.md)
1167
1312
  n(this, "_isWebGL1", !1);
1168
- n(this, "_maxBatchTextures", S.MAX_BATCH_TEXTURES);
1313
+ n(this, "_maxBatchTextures", A.MAX_BATCH_TEXTURES);
1169
1314
  n(this, "_hasElementIndexUint", !1);
1170
1315
  n(this, "_vaoExt", null);
1171
1316
  n(this, "_aIdBuffer", null);
@@ -1325,7 +1470,7 @@ void main() {
1325
1470
  const r = e.getParameter(e.MAX_COMBINED_TEXTURE_IMAGE_UNITS), o = e.getParameter(e.MAX_TEXTURE_IMAGE_UNITS);
1326
1471
  this._maxBatchTextures = Math.max(
1327
1472
  1,
1328
- Math.min(S.MAX_BATCH_TEXTURES, r - 2, o)
1473
+ Math.min(A.MAX_BATCH_TEXTURES, r - 2, o)
1329
1474
  );
1330
1475
  }
1331
1476
  /**
@@ -1354,29 +1499,29 @@ void main() { gl_FragColor = vC; }
1354
1499
  e.activeTexture(e.TEXTURE0);
1355
1500
  const u = e.getParameter(e.TEXTURE_BINDING_2D);
1356
1501
  e.activeTexture(e.TEXTURE1);
1357
- const _ = e.getParameter(e.TEXTURE_BINDING_2D), U = e.isEnabled(e.BLEND), E = e.isEnabled(e.DEPTH_TEST), I = e.isEnabled(e.SCISSOR_TEST);
1358
- let x = null, b = null, B = null, p = null, v = null, T = null, d = -1, w = !1, f = null, h = 4, y = 0, g = 0, P = 0, D = !1, A = !1;
1359
- const m = new Uint8Array(4);
1502
+ const _ = e.getParameter(e.TEXTURE_BINDING_2D), y = e.isEnabled(e.BLEND), E = e.isEnabled(e.DEPTH_TEST), I = e.isEnabled(e.SCISSOR_TEST);
1503
+ let m = null, w = null, B = null, f = null, v = null, g = null, d = -1, b = !1, x = null, h = 4, U = 0, p = 0, R = 0, D = !1, S = !1;
1504
+ const T = new Uint8Array(4);
1360
1505
  try {
1361
- x = e.createProgram(), e.attachShader(x, this._compileShader(e.VERTEX_SHADER, t)), e.attachShader(x, this._compileShader(e.FRAGMENT_SHADER, a)), e.linkProgram(x);
1362
- const C = (R, F) => {
1506
+ m = e.createProgram(), e.attachShader(m, this._compileShader(e.VERTEX_SHADER, t)), e.attachShader(m, this._compileShader(e.FRAGMENT_SHADER, a)), e.linkProgram(m);
1507
+ const C = (P, F) => {
1363
1508
  const M = e.createTexture();
1364
- return e.bindTexture(e.TEXTURE_2D, M), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_MIN_FILTER, e.NEAREST), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_MAG_FILTER, e.NEAREST), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_WRAP_S, e.CLAMP_TO_EDGE), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_WRAP_T, e.CLAMP_TO_EDGE), e.texImage2D(e.TEXTURE_2D, 0, e.RGBA, 1, 1, 0, e.RGBA, R, F), M;
1509
+ return e.bindTexture(e.TEXTURE_2D, M), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_MIN_FILTER, e.NEAREST), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_MAG_FILTER, e.NEAREST), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_WRAP_S, e.CLAMP_TO_EDGE), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_WRAP_T, e.CLAMP_TO_EDGE), e.texImage2D(e.TEXTURE_2D, 0, e.RGBA, 1, 1, 0, e.RGBA, P, F), M;
1365
1510
  };
1366
- e.activeTexture(e.TEXTURE0), b = C(e.FLOAT, new Float32Array([1, 0.5, 0.25, 1])), B = C(e.UNSIGNED_BYTE, new Uint8Array([51, 102, 153, 255])), p = e.createTexture(), e.bindTexture(e.TEXTURE_2D, p), e.texImage2D(e.TEXTURE_2D, 0, e.RGBA, 1, 1, 0, e.RGBA, e.UNSIGNED_BYTE, null), v = e.createFramebuffer(), e.bindFramebuffer(e.FRAMEBUFFER, v), e.framebufferTexture2D(e.FRAMEBUFFER, e.COLOR_ATTACHMENT0, e.TEXTURE_2D, p, 0), T = e.createBuffer(), e.bindBuffer(e.ARRAY_BUFFER, T), e.bufferData(e.ARRAY_BUFFER, new Float32Array([0]), e.STATIC_DRAW), A = e.getProgramParameter(x, e.LINK_STATUS), A && (e.useProgram(x), e.activeTexture(e.TEXTURE0), e.bindTexture(e.TEXTURE_2D, b), e.activeTexture(e.TEXTURE1), e.bindTexture(e.TEXTURE_2D, B), e.uniform1i(e.getUniformLocation(x, "uF"), 0), e.uniform1i(e.getUniformLocation(x, "uB"), 1), d = e.getAttribLocation(x, "aId"), w = e.getVertexAttrib(d, e.VERTEX_ATTRIB_ARRAY_ENABLED), f = e.getVertexAttrib(d, e.VERTEX_ATTRIB_ARRAY_BUFFER_BINDING), h = e.getVertexAttrib(d, e.VERTEX_ATTRIB_ARRAY_SIZE), y = e.getVertexAttrib(d, e.VERTEX_ATTRIB_ARRAY_TYPE), D = e.getVertexAttrib(d, e.VERTEX_ATTRIB_ARRAY_NORMALIZED), g = e.getVertexAttrib(d, e.VERTEX_ATTRIB_ARRAY_STRIDE), P = e.getVertexAttribOffset(d, e.VERTEX_ATTRIB_ARRAY_POINTER), e.enableVertexAttribArray(d), e.vertexAttribPointer(d, 1, e.FLOAT, !1, 0, 0), e.viewport(0, 0, 1, 1), e.disable(e.BLEND), e.disable(e.DEPTH_TEST), e.disable(e.SCISSOR_TEST), e.drawArrays(e.POINTS, 0, 1), e.readPixels(0, 0, 1, 1, e.RGBA, e.UNSIGNED_BYTE, m), A = Math.abs(m[0] - 51) <= 2 && Math.abs(m[1] - 51) <= 2 && Math.abs(m[2] - 38) <= 2);
1511
+ e.activeTexture(e.TEXTURE0), w = C(e.FLOAT, new Float32Array([1, 0.5, 0.25, 1])), B = C(e.UNSIGNED_BYTE, new Uint8Array([51, 102, 153, 255])), f = e.createTexture(), e.bindTexture(e.TEXTURE_2D, f), e.texImage2D(e.TEXTURE_2D, 0, e.RGBA, 1, 1, 0, e.RGBA, e.UNSIGNED_BYTE, null), v = e.createFramebuffer(), e.bindFramebuffer(e.FRAMEBUFFER, v), e.framebufferTexture2D(e.FRAMEBUFFER, e.COLOR_ATTACHMENT0, e.TEXTURE_2D, f, 0), g = e.createBuffer(), e.bindBuffer(e.ARRAY_BUFFER, g), e.bufferData(e.ARRAY_BUFFER, new Float32Array([0]), e.STATIC_DRAW), S = e.getProgramParameter(m, e.LINK_STATUS), S && (e.useProgram(m), e.activeTexture(e.TEXTURE0), e.bindTexture(e.TEXTURE_2D, w), e.activeTexture(e.TEXTURE1), e.bindTexture(e.TEXTURE_2D, B), e.uniform1i(e.getUniformLocation(m, "uF"), 0), e.uniform1i(e.getUniformLocation(m, "uB"), 1), d = e.getAttribLocation(m, "aId"), b = e.getVertexAttrib(d, e.VERTEX_ATTRIB_ARRAY_ENABLED), x = e.getVertexAttrib(d, e.VERTEX_ATTRIB_ARRAY_BUFFER_BINDING), h = e.getVertexAttrib(d, e.VERTEX_ATTRIB_ARRAY_SIZE), U = e.getVertexAttrib(d, e.VERTEX_ATTRIB_ARRAY_TYPE), D = e.getVertexAttrib(d, e.VERTEX_ATTRIB_ARRAY_NORMALIZED), p = e.getVertexAttrib(d, e.VERTEX_ATTRIB_ARRAY_STRIDE), R = e.getVertexAttribOffset(d, e.VERTEX_ATTRIB_ARRAY_POINTER), e.enableVertexAttribArray(d), e.vertexAttribPointer(d, 1, e.FLOAT, !1, 0, 0), e.viewport(0, 0, 1, 1), e.disable(e.BLEND), e.disable(e.DEPTH_TEST), e.disable(e.SCISSOR_TEST), e.drawArrays(e.POINTS, 0, 1), e.readPixels(0, 0, 1, 1, e.RGBA, e.UNSIGNED_BYTE, T), S = Math.abs(T[0] - 51) <= 2 && Math.abs(T[1] - 51) <= 2 && Math.abs(T[2] - 38) <= 2);
1367
1512
  } finally {
1368
- d >= 0 && (f && (e.bindBuffer(e.ARRAY_BUFFER, f), e.vertexAttribPointer(
1513
+ d >= 0 && (x && (e.bindBuffer(e.ARRAY_BUFFER, x), e.vertexAttribPointer(
1369
1514
  d,
1370
1515
  h,
1371
- y,
1516
+ U,
1372
1517
  D,
1373
- g,
1374
- P
1375
- )), w ? e.enableVertexAttribArray(d) : e.disableVertexAttribArray(d)), v && e.deleteFramebuffer(v), p && e.deleteTexture(p), b && e.deleteTexture(b), B && e.deleteTexture(B), T && e.deleteBuffer(T), x && e.deleteProgram(x), e.bindFramebuffer(e.FRAMEBUFFER, c), e.bindBuffer(e.ARRAY_BUFFER, l), e.useProgram(o), e.activeTexture(e.TEXTURE0), e.bindTexture(e.TEXTURE_2D, u), e.activeTexture(e.TEXTURE1), e.bindTexture(e.TEXTURE_2D, _), e.activeTexture(i), U && e.enable(e.BLEND), E && e.enable(e.DEPTH_TEST), I && e.enable(e.SCISSOR_TEST), e.viewport(r[0], r[1], r[2], r[3]);
1518
+ p,
1519
+ R
1520
+ )), b ? e.enableVertexAttribArray(d) : e.disableVertexAttribArray(d)), v && e.deleteFramebuffer(v), f && e.deleteTexture(f), w && e.deleteTexture(w), B && e.deleteTexture(B), g && e.deleteBuffer(g), m && e.deleteProgram(m), e.bindFramebuffer(e.FRAMEBUFFER, c), e.bindBuffer(e.ARRAY_BUFFER, l), e.useProgram(o), e.activeTexture(e.TEXTURE0), e.bindTexture(e.TEXTURE_2D, u), e.activeTexture(e.TEXTURE1), e.bindTexture(e.TEXTURE_2D, _), e.activeTexture(i), y && e.enable(e.BLEND), E && e.enable(e.DEPTH_TEST), I && e.enable(e.SCISSOR_TEST), e.viewport(r[0], r[1], r[2], r[3]);
1376
1521
  }
1377
- if (!A)
1522
+ if (!S)
1378
1523
  throw new Error(
1379
- `NeutrinoParticles (js-v1.1): WebGL1 vertex texture fetch smoke test failed (got ${m[0]},${m[1]},${m[2]}); WebGL2 or WebGL1 with working vertex float textures is required.`
1524
+ `NeutrinoParticles (js-v1.1): WebGL1 vertex texture fetch smoke test failed (got ${T[0]},${T[1]},${T[2]}); WebGL2 or WebGL1 with working vertex float textures is required.`
1380
1525
  );
1381
1526
  }
1382
1527
  _initShader() {
@@ -1384,7 +1529,7 @@ void main() { gl_FragColor = vC; }
1384
1529
  if (this._shaderProgram = e.createProgram(), e.attachShader(this._shaderProgram, r), e.attachShader(this._shaderProgram, o), e.linkProgram(this._shaderProgram), !e.getProgramParameter(this._shaderProgram, e.LINK_STATUS))
1385
1530
  throw new Error("Shader link error: " + e.getProgramInfoLog(this._shaderProgram));
1386
1531
  e.deleteShader(r), e.deleteShader(o), this._uDataTexture = e.getUniformLocation(this._shaderProgram, "uDataTexture"), this._uCameraRight = e.getUniformLocation(this._shaderProgram, "uCameraRight"), this._uCameraUp = e.getUniformLocation(this._shaderProgram, "uCameraUp"), this._uCameraDir = e.getUniformLocation(this._shaderProgram, "uCameraDir"), this._uViewProjMatrix = e.getUniformLocation(this._shaderProgram, "uViewProjMatrix"), this._uModelMatrix = e.getUniformLocation(this._shaderProgram, "uModelMatrix"), this._uViewportAspect = e.getUniformLocation(this._shaderProgram, "uViewportAspect"), this._uWorldAlpha = e.getUniformLocation(this._shaderProgram, "uWorldAlpha"), this._isWebGL1 ? (this._uMetaTexture = e.getUniformLocation(this._shaderProgram, "uMetaTexture"), this._uDataTexSize = e.getUniformLocation(this._shaderProgram, "uDataTexSize"), this._aIdLocation = e.getAttribLocation(this._shaderProgram, "aId")) : this._uDataTextureWidth = e.getUniformLocation(this._shaderProgram, "uDataTextureWidth"), this._uTextures = [], this._uTexRemaps = [];
1387
- for (let l = 0; l < S.MAX_BATCH_TEXTURES; l++)
1532
+ for (let l = 0; l < A.MAX_BATCH_TEXTURES; l++)
1388
1533
  this._uTextures.push(e.getUniformLocation(this._shaderProgram, `uTextures[${l}]`)), this._uTexRemaps.push(e.getUniformLocation(this._shaderProgram, `uTexRemaps[${l}]`));
1389
1534
  }
1390
1535
  _compileShader(e, t) {
@@ -1396,8 +1541,8 @@ void main() { gl_FragColor = vC; }
1396
1541
  return r;
1397
1542
  }
1398
1543
  };
1399
- n(S, "MAX_BATCH_TEXTURES", 8);
1400
- let NeutrinoContext = S;
1544
+ n(A, "MAX_BATCH_TEXTURES", 8);
1545
+ let NeutrinoContext = A;
1401
1546
  class GamePlugin extends Phaser.Plugins.BasePlugin {
1402
1547
  constructor() {
1403
1548
  super(...arguments);
@@ -1729,6 +1874,9 @@ class DrawBatch {
1729
1874
  class NeutrinoRenderer {
1730
1875
  constructor() {
1731
1876
  n(this, "_orthoMatrix", new Float32Array(16));
1877
+ n(this, "_perspMatrix", new Float32Array(16));
1878
+ n(this, "_vpMatrix", new Float32Array(16));
1879
+ n(this, "_projFrame", { x: 0, y: 0, width: 0, height: 0 });
1732
1880
  n(this, "_modelMatrix", new Float32Array(16));
1733
1881
  n(this, "_texSlotMap", /* @__PURE__ */ new Map());
1734
1882
  n(this, "_batchPool");
@@ -1739,66 +1887,70 @@ class NeutrinoRenderer {
1739
1887
  }
1740
1888
  render(e, t, a, r, o, l, c) {
1741
1889
  if (l === 0) return;
1742
- const i = e.gl, u = e.isWebGL1, _ = e.maxBatchTextures, U = c.renderStyles, E = c.materials, I = u ? t.meta : null;
1890
+ const i = e.gl, u = e.isWebGL1, _ = e.maxBatchTextures, y = c.renderStyles, E = c.materials, I = u ? t.meta : null;
1743
1891
  I && I.build(r, l), this._batchCount = 0;
1744
- const x = this._texSlotMap;
1892
+ const m = this._texSlotMap;
1745
1893
  for (; this._batchPool.length < o.length; )
1746
1894
  this._batchPool.push(new DrawBatch());
1747
- let b = 0;
1748
- for (; b < o.length; ) {
1749
- const p = this._batchPool[this._batchCount];
1750
- p.reset(), p.blendMode = this._resolveBlend(o[b].blendMode, E), p.startParticle = o[b].startParticleIndex, x.clear();
1751
- let v = b;
1752
- for (; v < o.length && this._resolveBlend(o[v].blendMode, E) === p.blendMode; ) {
1753
- const T = U[o[v].renderStyleIndex].textureIndices[0];
1754
- if (!x.has(T)) {
1755
- if (x.size >= _) break;
1756
- const f = x.size;
1757
- x.set(T, f), p.glTextures[f] = c.glTextures[T] || null, p.remaps[f] = c.remaps[T] || null, p.numTextures = f + 1;
1895
+ let w = 0;
1896
+ for (; w < o.length; ) {
1897
+ const f = this._batchPool[this._batchCount];
1898
+ f.reset(), f.blendMode = this._resolveBlend(o[w].blendMode, E), f.startParticle = o[w].startParticleIndex, m.clear();
1899
+ let v = w;
1900
+ for (; v < o.length && this._resolveBlend(o[v].blendMode, E) === f.blendMode; ) {
1901
+ const g = y[o[v].renderStyleIndex].textureIndices[0];
1902
+ if (!m.has(g)) {
1903
+ if (m.size >= _) break;
1904
+ const x = m.size;
1905
+ m.set(g, x), f.glTextures[x] = c.glTextures[g] || null, f.remaps[x] = c.remaps[g] || null, f.numTextures = x + 1;
1758
1906
  }
1759
- const d = x.get(T), w = o[v];
1907
+ const d = m.get(g), b = o[v];
1760
1908
  if (I)
1761
- I.patchSlot(w.startParticleIndex, w.numParticles, d);
1762
- else
1763
- for (let f = 0; f < w.numParticles; f++) {
1764
- const h = (w.startParticleIndex + f) * BYTES_PER_PARTICLE, y = a.getUint32(h, !0);
1765
- a.setUint32(h, y & 4294967040 | d, !0);
1909
+ I.patchSlot(b.startParticleIndex, b.numParticles, d);
1910
+ else if (d !== 0)
1911
+ for (let x = 0; x < b.numParticles; x++) {
1912
+ const h = (b.startParticleIndex + x) * BYTES_PER_PARTICLE, U = a.getUint32(h, !0);
1913
+ a.setUint32(h, U & 4294967040 | d, !0);
1766
1914
  }
1767
- p.numParticles += w.numParticles, v++;
1915
+ f.numParticles += b.numParticles, v++;
1768
1916
  }
1769
- this._batchCount++, b = v;
1917
+ this._batchCount++, w = v;
1770
1918
  }
1771
1919
  const B = i.isEnabled(i.SCISSOR_TEST);
1772
1920
  try {
1773
1921
  if (i.disable(i.DEPTH_TEST), i.disable(i.STENCIL_TEST), i.disable(i.SCISSOR_TEST), i.disable(i.CULL_FACE), i.depthMask(!1), i.colorMask(!0, !0, !0, !0), i.useProgram(e.shaderProgram), u) {
1774
- const g = t;
1775
- g.upload(l, DATA_TEXTURE_UNIT, META_TEXTURE_UNIT), i.uniform1i(e.uDataTexture, DATA_TEXTURE_UNIT), i.uniform1i(e.uMetaTexture, META_TEXTURE_UNIT), i.uniform2f(e.uDataTexSize, g.textureWidth, g.textureHeight);
1922
+ const p = t;
1923
+ p.upload(l, DATA_TEXTURE_UNIT, META_TEXTURE_UNIT), i.uniform1i(e.uDataTexture, DATA_TEXTURE_UNIT), i.uniform1i(e.uMetaTexture, META_TEXTURE_UNIT), i.uniform2f(e.uDataTexSize, p.textureWidth, p.textureHeight);
1776
1924
  } else {
1777
- const g = t;
1778
- g.uploadAndBind(DATA_TEXTURE_UNIT), i.uniform1i(e.uDataTexture, DATA_TEXTURE_UNIT), i.uniform1i(e.uDataTextureWidth, g.textureWidth);
1925
+ const p = t;
1926
+ p.uploadAndBind(DATA_TEXTURE_UNIT), i.uniform1i(e.uDataTexture, DATA_TEXTURE_UNIT), i.uniform1i(e.uDataTextureWidth, p.textureWidth);
1779
1927
  }
1780
1928
  i.uniform3f(e.uCameraRight, 1, 0, 0), i.uniform3f(e.uCameraUp, 0, -1, 0), i.uniform3f(e.uCameraDir, 0, 0, -1);
1781
- const p = c.viewportWidth, v = c.viewportHeight, T = c.zoom || 1, d = this._orthoMatrix;
1782
- d.fill(0), d[0] = 2 / p * T, d[5] = -2 / v * T, d[10] = 1, d[15] = 1, d[12] = -1 - 2 / p * T * c.scrollX, d[13] = 1 + 2 / v * T * c.scrollY, i.uniformMatrix4fv(e.uViewProjMatrix, !1, d);
1783
- const w = v > 0 ? p / v : 1;
1784
- e.uViewportAspect && i.uniform1f(e.uViewportAspect, w), e.uWorldAlpha && i.uniform1f(e.uWorldAlpha, c.worldAlpha);
1785
- const f = c.model, h = this._modelMatrix;
1786
- h[0] = f.a, h[1] = f.b, h[2] = 0, h[3] = 0, h[4] = f.c, h[5] = f.d, h[6] = 0, h[7] = 0, h[8] = 0, h[9] = 0, h[10] = 1, h[11] = 0, h[12] = f.tx, h[13] = f.ty, h[14] = 0, h[15] = 1, i.uniformMatrix4fv(e.uModelMatrix, !1, h), u ? (e.unbindVao(), i.bindBuffer(i.ARRAY_BUFFER, e.aIdBuffer), i.enableVertexAttribArray(e.aIdLocation), i.vertexAttribPointer(e.aIdLocation, 1, i.FLOAT, !1, 0, 0), i.bindBuffer(i.ELEMENT_ARRAY_BUFFER, e.indexBuffer)) : i.bindVertexArray(e.vao);
1787
- const y = u ? PARTICLE_TEXTURE_START_UNIT_GL1 : PARTICLE_TEXTURE_START_UNIT;
1929
+ const f = c.viewportWidth, v = c.viewportHeight, g = c.zoom || 1, d = this._orthoMatrix;
1930
+ if (d.fill(0), d[0] = 2 / f * g, d[5] = -2 / v * g, d[10] = 0, d[15] = 1, d[12] = -1 - 2 / f * g * c.scrollX, d[13] = 1 + 2 / v * g * c.scrollY, c.projection) {
1931
+ const p = this._projFrame;
1932
+ p.x = 0, p.y = 0, p.width = f, p.height = v, c.projection.setScreenFrame(p), c.projection.writeMatrix(this._perspMatrix), this._mulMatrix(this._vpMatrix, d, this._perspMatrix), i.uniformMatrix4fv(e.uViewProjMatrix, !1, this._vpMatrix);
1933
+ } else
1934
+ i.uniformMatrix4fv(e.uViewProjMatrix, !1, d);
1935
+ const b = v > 0 ? f / v : 1;
1936
+ e.uViewportAspect && i.uniform1f(e.uViewportAspect, b), e.uWorldAlpha && i.uniform1f(e.uWorldAlpha, c.worldAlpha);
1937
+ const x = c.model, h = this._modelMatrix;
1938
+ h[0] = x.a, h[1] = x.b, h[2] = 0, h[3] = 0, h[4] = x.c, h[5] = x.d, h[6] = 0, h[7] = 0, h[8] = 0, h[9] = 0, h[10] = 1, h[11] = 0, h[12] = x.tx, h[13] = x.ty, h[14] = 0, h[15] = 1, i.uniformMatrix4fv(e.uModelMatrix, !1, h), u ? (e.unbindVao(), i.bindBuffer(i.ARRAY_BUFFER, e.aIdBuffer), i.enableVertexAttribArray(e.aIdLocation), i.vertexAttribPointer(e.aIdLocation, 1, i.FLOAT, !1, 0, 0), i.bindBuffer(i.ELEMENT_ARRAY_BUFFER, e.indexBuffer)) : i.bindVertexArray(e.vao);
1939
+ const U = u ? PARTICLE_TEXTURE_START_UNIT_GL1 : PARTICLE_TEXTURE_START_UNIT;
1788
1940
  i.enable(i.BLEND);
1789
- for (let g = 0; g < this._batchCount; g++) {
1790
- const P = this._batchPool[g];
1791
- this._applyBlendMode(i, P.blendMode);
1792
- for (let m = 0; m < P.numTextures; m++) {
1793
- const C = y + m;
1794
- i.activeTexture(i.TEXTURE0 + C), i.bindTexture(i.TEXTURE_2D, P.glTextures[m]), i.uniform1i(e.uTextures[m], C);
1795
- const R = P.remaps[m];
1796
- R ? i.uniform4f(e.uTexRemaps[m], R.x, R.y, R.width, R.height) : i.uniform4f(e.uTexRemaps[m], 0, 0, 1, 1);
1941
+ for (let p = 0; p < this._batchCount; p++) {
1942
+ const R = this._batchPool[p];
1943
+ this._applyBlendMode(i, R.blendMode);
1944
+ for (let T = 0; T < R.numTextures; T++) {
1945
+ const C = U + T;
1946
+ i.activeTexture(i.TEXTURE0 + C), i.bindTexture(i.TEXTURE_2D, R.glTextures[T]), i.uniform1i(e.uTextures[T], C);
1947
+ const P = R.remaps[T];
1948
+ P ? i.uniform4f(e.uTexRemaps[T], P.x, P.y, P.width, P.height) : i.uniform4f(e.uTexRemaps[T], 0, 0, 1, 1);
1797
1949
  }
1798
- const D = P.startParticle * 6, A = P.numParticles * 6;
1950
+ const D = R.startParticle * 6, S = R.numParticles * 6;
1799
1951
  i.drawElements(
1800
1952
  i.TRIANGLES,
1801
- A,
1953
+ S,
1802
1954
  e.indexType,
1803
1955
  D * (e.indexType === i.UNSIGNED_INT ? 4 : 2)
1804
1956
  );
@@ -1813,6 +1965,13 @@ class NeutrinoRenderer {
1813
1965
  // materials[], NOT the blend itself — without this deref a single-material effect
1814
1966
  // (materialIndex 0) would always render as materials[0]'s slot 0 (Normal). Falls back to
1815
1967
  // the raw value if materials is missing/out of range (mirror of cocos _resolveBlendMode).
1968
+ // Column-major 4x4 multiply: out = a * b. Preallocated outputs only.
1969
+ _mulMatrix(e, t, a) {
1970
+ for (let r = 0; r < 16; r += 4) {
1971
+ const o = a[r], l = a[r + 1], c = a[r + 2], i = a[r + 3];
1972
+ e[r] = t[0] * o + t[4] * l + t[8] * c + t[12] * i, e[r + 1] = t[1] * o + t[5] * l + t[9] * c + t[13] * i, e[r + 2] = t[2] * o + t[6] * l + t[10] * c + t[14] * i, e[r + 3] = t[3] * o + t[7] * l + t[11] * c + t[15] * i;
1973
+ }
1974
+ }
1816
1975
  _resolveBlend(e, t) {
1817
1976
  return t && e >= 0 && e < t.length ? t[e] : e;
1818
1977
  }
@@ -1858,6 +2017,8 @@ class Effect extends Phaser.GameObjects.GameObject {
1858
2017
  n(this, "alpha", 1);
1859
2018
  n(this, "depth", 0);
1860
2019
  n(this, "blendMode", Phaser.BlendModes.NORMAL);
2020
+ /** Optional v1.0-parity perspective projection (Projection3D). */
2021
+ n(this, "projection");
1861
2022
  n(this, "_dataTextureManager", null);
1862
2023
  n(this, "_worldPosition");
1863
2024
  n(this, "_worldScaledPosition");
@@ -1867,7 +2028,7 @@ class Effect extends Phaser.GameObjects.GameObject {
1867
2028
  n(this, "_tempMatrix2");
1868
2029
  n(this, "_unpauseOnUpdateRender");
1869
2030
  n(this, "_glTexturesScratch", []);
1870
- this.gamePlugin = a.plugins.get("neutrino"), this.effectModel = t, this.x = r.position[0], this.y = r.position[1], this.z = r.position[2], this.angle = r.angle, this.scaleX = r.scale, this.scaleY = r.scale, this._worldPosition = new Phaser.Math.Vector2(), this._worldScaledPosition = [0, 0, 0], this._worldScale = 1, this._tempMatrix1 = new Phaser.GameObjects.Components.TransformMatrix(), this._tempMatrix2 = new Phaser.GameObjects.Components.TransformMatrix(), this._unpauseOnUpdateRender = r.pause === Pause.BEFORE_UPDATE_OR_RENDER, this._updateWorldTransform();
2031
+ this.gamePlugin = a.plugins.get("neutrino"), this.effectModel = t, this.x = r.position[0], this.y = r.position[1], this.z = r.position[2], this.angle = r.angle, this.scaleX = r.scale, this.scaleY = r.scale, this.projection = r.projection, this._worldPosition = new Phaser.Math.Vector2(), this._worldScaledPosition = [0, 0, 0], this._worldScale = 1, this._tempMatrix1 = new Phaser.GameObjects.Components.TransformMatrix(), this._tempMatrix2 = new Phaser.GameObjects.Components.TransformMatrix(), this._unpauseOnUpdateRender = r.pause === Pause.BEFORE_UPDATE_OR_RENDER, this._updateWorldTransform();
1871
2032
  const o = {
1872
2033
  paused: r.pause !== Pause.NO,
1873
2034
  generatorsPaused: r.generatorsPaused
@@ -1906,7 +2067,7 @@ class Effect extends Phaser.GameObjects.GameObject {
1906
2067
  _.length = u;
1907
2068
  for (let E = 0; E < u; ++E)
1908
2069
  _[E] = this.effectModel.glTexture(E);
1909
- const U = this._worldScale;
2070
+ const y = this._worldScale;
1910
2071
  try {
1911
2072
  sharedRenderer.render(
1912
2073
  o,
@@ -1916,13 +2077,14 @@ class Effect extends Phaser.GameObjects.GameObject {
1916
2077
  c,
1917
2078
  i,
1918
2079
  {
1919
- model: { a: U, b: 0, c: 0, d: U, tx: 0, ty: 0 },
2080
+ model: { a: y, b: 0, c: 0, d: y, tx: 0, ty: 0 },
1920
2081
  viewportWidth: t.width,
1921
2082
  viewportHeight: t.height,
1922
2083
  scrollX: r.scrollX * this.scrollFactorX,
1923
2084
  scrollY: r.scrollY * this.scrollFactorY,
1924
2085
  zoom: r.zoom,
1925
2086
  worldAlpha: this.alpha,
2087
+ projection: this.projection,
1926
2088
  glTextures: _,
1927
2089
  remaps: this.effectModel.texturesRemap,
1928
2090
  renderStyles: this.effect.model.renderStyles,
@@ -1960,6 +2122,19 @@ class Effect extends Phaser.GameObjects.GameObject {
1960
2122
  setPropertyInAllEmitters(t, a) {
1961
2123
  this.ready && this.effect.setPropertyInAllEmitters(t, a);
1962
2124
  }
2125
+ setPropertyInEmitter(t, a, r) {
2126
+ this.ready && this.effect.setPropertyInEmitter(t, a, r);
2127
+ }
2128
+ getEmitterPropertyValue(t, a) {
2129
+ if (this.ready)
2130
+ return a === void 0 ? this.effect.getEmitterPropertyValue(t) : this.effect.getEmitterPropertyValue(t, a);
2131
+ }
2132
+ hasEmitterProperty(t, a) {
2133
+ return this.ready ? a === void 0 ? this.effect.hasEmitterProperty(t) : this.effect.hasEmitterProperty(t, a) : !1;
2134
+ }
2135
+ getEmitterProperties() {
2136
+ return this.ready ? this.effect.getEmitterProperties() : [];
2137
+ }
1963
2138
  getNumParticles() {
1964
2139
  return this.ready ? this.effect.getNumParticles() : 0;
1965
2140
  }
@@ -2063,6 +2238,19 @@ class PerspectiveProjection {
2063
2238
  _getScale(e) {
2064
2239
  return this._z / (this._z - e[2]);
2065
2240
  }
2241
+ /**
2242
+ * Writes the projection as a 4x4 column-major matrix operating in screen
2243
+ * space, for the GPU render path. Homogeneous equivalent of
2244
+ * transformPosition(): clip.xy = p.xy - center * (p.z / z),
2245
+ * clip.w = 1 - p.z / z; the z row is zeroed (no depth on this path).
2246
+ * setScreenFrame() must have been called before.
2247
+ *
2248
+ * @param {Float32Array} out 16-element column-major output matrix.
2249
+ */
2250
+ writeMatrix(e) {
2251
+ const t = this._z, a = Number.isFinite(t) && t > 0 ? 1 / t : 0;
2252
+ e.fill(0), e[0] = 1, e[5] = 1, e[8] = -this._screenPosX * a, e[9] = -this._screenPosY * a, e[11] = -a, e[15] = 1;
2253
+ }
2066
2254
  }
2067
2255
  let _installed = !1;
2068
2256
  function installPlugin() {