p5 2.2.1-rc.0 → 2.2.1

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 (117) hide show
  1. package/dist/accessibility/color_namer.js +5 -6
  2. package/dist/accessibility/describe.js +4 -26
  3. package/dist/accessibility/index.js +5 -6
  4. package/dist/accessibility/outputs.js +6 -38
  5. package/dist/app.js +5 -6
  6. package/dist/color/color_conversion.js +5 -6
  7. package/dist/color/creating_reading.js +1 -1
  8. package/dist/color/index.js +2 -2
  9. package/dist/color/p5.Color.js +1 -1
  10. package/dist/color/setting.js +59 -357
  11. package/dist/{constants-DEJVKr9Z.js → constants-DQyACdzq.js} +11 -61
  12. package/dist/core/constants.js +1 -1
  13. package/dist/core/environment.js +26 -158
  14. package/dist/core/filterShaders.js +1 -1
  15. package/dist/core/friendly_errors/fes_core.js +1 -1
  16. package/dist/core/friendly_errors/file_errors.js +1 -1
  17. package/dist/core/friendly_errors/index.js +1 -1
  18. package/dist/core/friendly_errors/param_validator.js +1 -1
  19. package/dist/core/friendly_errors/sketch_verifier.js +1 -1
  20. package/dist/core/helpers.js +1 -1
  21. package/dist/core/init.js +5 -6
  22. package/dist/core/internationalization.js +1 -1
  23. package/dist/core/legacy.js +5 -6
  24. package/dist/core/main.js +5 -6
  25. package/dist/core/p5.Graphics.js +4 -5
  26. package/dist/core/p5.Renderer.js +3 -4
  27. package/dist/core/p5.Renderer2D.js +5 -6
  28. package/dist/core/p5.Renderer3D.js +4 -5
  29. package/dist/core/rendering.js +4 -5
  30. package/dist/core/structure.js +13 -52
  31. package/dist/core/transform.js +32 -176
  32. package/dist/{creating_reading-CgHCHxqN.js → creating_reading-ZXzcZEsb.js} +3 -196
  33. package/dist/data/local_storage.js +4 -30
  34. package/dist/dom/dom.js +24 -159
  35. package/dist/dom/index.js +2 -2
  36. package/dist/dom/p5.Element.js +31 -208
  37. package/dist/dom/p5.File.js +1 -32
  38. package/dist/dom/p5.MediaElement.js +10 -113
  39. package/dist/events/acceleration.js +11 -64
  40. package/dist/events/keyboard.js +13 -81
  41. package/dist/events/pointer.js +18 -160
  42. package/dist/image/const.js +1 -1
  43. package/dist/image/filterRenderer2D.js +4 -5
  44. package/dist/image/image.js +4 -5
  45. package/dist/image/index.js +4 -5
  46. package/dist/image/loading_displaying.js +4 -5
  47. package/dist/image/p5.Image.js +3 -4
  48. package/dist/image/pixels.js +17 -100
  49. package/dist/io/files.js +4 -5
  50. package/dist/io/index.js +4 -5
  51. package/dist/io/p5.Table.js +66 -158
  52. package/dist/io/p5.TableRow.js +48 -71
  53. package/dist/io/p5.XML.js +6 -99
  54. package/dist/io/utilities.js +8 -3
  55. package/dist/{main-_RXV5Lx8.js → main-DvN69W3f.js} +13 -42
  56. package/dist/math/Matrices/Matrix.js +87 -126
  57. package/dist/math/Matrices/MatrixNumjs.js +1 -5
  58. package/dist/math/calculation.js +10 -112
  59. package/dist/math/index.js +1 -1
  60. package/dist/math/math.js +2 -12
  61. package/dist/math/noise.js +5 -32
  62. package/dist/math/p5.Matrix.js +3 -3
  63. package/dist/math/p5.Vector.js +104 -345
  64. package/dist/math/random.js +5 -32
  65. package/dist/math/trigonometry.js +15 -105
  66. package/dist/{p5.Renderer-QoFcvj3f.js → p5.Renderer-D-5LdCRz.js} +25 -178
  67. package/dist/{rendering-CsICjEXA.js → rendering-h9unX5K0.js} +254 -1156
  68. package/dist/shape/2d_primitives.js +33 -194
  69. package/dist/shape/attributes.js +12 -73
  70. package/dist/shape/curves.js +30 -95
  71. package/dist/shape/custom_shapes.js +63 -144
  72. package/dist/shape/index.js +2 -2
  73. package/dist/shape/vertex.js +21 -106
  74. package/dist/strands/p5.strands.js +248 -46
  75. package/dist/type/index.js +3 -4
  76. package/dist/type/p5.Font.js +4 -49
  77. package/dist/type/textCore.js +5 -158
  78. package/dist/utilities/conversion.js +17 -104
  79. package/dist/utilities/time_date.js +3 -40
  80. package/dist/utilities/utility_functions.js +6 -48
  81. package/dist/webgl/3d_primitives.js +4 -5
  82. package/dist/webgl/GeometryBuilder.js +1 -2
  83. package/dist/webgl/ShapeBuilder.js +22 -2
  84. package/dist/webgl/enums.js +1 -1
  85. package/dist/webgl/index.js +4 -5
  86. package/dist/webgl/interaction.js +6 -33
  87. package/dist/webgl/light.js +4 -5
  88. package/dist/webgl/loading.js +12 -46
  89. package/dist/webgl/material.js +4 -5
  90. package/dist/webgl/p5.Camera.js +4 -5
  91. package/dist/webgl/p5.DataArray.js +0 -4
  92. package/dist/webgl/p5.Framebuffer.js +4 -5
  93. package/dist/webgl/p5.Geometry.js +12 -106
  94. package/dist/webgl/p5.Quat.js +1 -1
  95. package/dist/webgl/p5.RendererGL.js +7 -18
  96. package/dist/webgl/p5.Shader.js +12 -36
  97. package/dist/webgl/p5.Texture.js +4 -5
  98. package/dist/webgl/text.js +4 -5
  99. package/dist/webgl/utils.js +4 -5
  100. package/dist/webgpu/index.js +1 -1
  101. package/dist/webgpu/p5.RendererWebGPU.js +529 -208
  102. package/dist/webgpu/shaders/color.js +32 -17
  103. package/dist/webgpu/shaders/filters/base.js +18 -7
  104. package/dist/webgpu/shaders/font.js +52 -40
  105. package/dist/webgpu/shaders/line.js +50 -36
  106. package/dist/webgpu/shaders/material.js +90 -83
  107. package/dist/webgpu/strands_wgslBackend.js +5 -2
  108. package/lib/p5.esm.js +5576 -7811
  109. package/lib/p5.esm.min.js +1 -1
  110. package/lib/p5.js +5576 -7811
  111. package/lib/p5.min.js +1 -1
  112. package/lib/p5.webgpu.esm.js +786 -453
  113. package/lib/p5.webgpu.js +786 -453
  114. package/lib/p5.webgpu.min.js +1 -1
  115. package/package.json +13 -13
  116. package/types/global.d.ts +16905 -16783
  117. package/types/p5.d.ts +11142 -11081
package/lib/p5.webgpu.js CHANGED
@@ -14,7 +14,7 @@
14
14
  * @property {String} VERSION
15
15
  * @final
16
16
  */
17
- const VERSION = '2.2.1-rc.0';
17
+ const VERSION = '2.2.1';
18
18
 
19
19
  // GRAPHICS RENDERER
20
20
  /**
@@ -149,8 +149,6 @@
149
149
  * @final
150
150
  *
151
151
  * @example
152
- * <div>
153
- * <code>
154
152
  * function setup() {
155
153
  * createCanvas(100, 100);
156
154
  *
@@ -161,11 +159,8 @@
161
159
  *
162
160
  * describe('The bottom-right quarter of a circle drawn in white on a gray background.');
163
161
  * }
164
- * </code>
165
- * </div>
166
162
  *
167
- * <div>
168
- * <code>
163
+ * @example
169
164
  * function setup() {
170
165
  * createCanvas(100, 100);
171
166
  *
@@ -185,11 +180,8 @@
185
180
  *
186
181
  * describe('Two black lines on a gray background. One line extends from the center to the right. The other line extends from the center to the bottom.');
187
182
  * }
188
- * </code>
189
- * </div>
190
183
  *
191
- * <div>
192
- * <code>
184
+ * @example
193
185
  * function setup() {
194
186
  * createCanvas(100, 100);
195
187
  *
@@ -219,8 +211,6 @@
219
211
  * fill(0, 0, 255);
220
212
  * circle(x2, 0, 20);
221
213
  * }
222
- * </code>
223
- * </div>
224
214
  */
225
215
  const HALF_PI = _PI / 2;
226
216
 
@@ -238,8 +228,6 @@
238
228
  * @final
239
229
  *
240
230
  * @example
241
- * <div>
242
- * <code>
243
231
  * function setup() {
244
232
  * createCanvas(100, 100);
245
233
  *
@@ -250,11 +238,8 @@
250
238
  *
251
239
  * describe('The bottom half of a circle drawn in white on a gray background.');
252
240
  * }
253
- * </code>
254
- * </div>
255
241
  *
256
- * <div>
257
- * <code>
242
+ * @example
258
243
  * function setup() {
259
244
  * createCanvas(100, 100);
260
245
  *
@@ -274,11 +259,8 @@
274
259
  *
275
260
  * describe('A horizontal black line on a gray background.');
276
261
  * }
277
- * </code>
278
- * </div>
279
262
  *
280
- * <div>
281
- * <code>
263
+ * @example
282
264
  * function setup() {
283
265
  * createCanvas(100, 100);
284
266
  *
@@ -308,8 +290,6 @@
308
290
  * fill(0, 0, 255);
309
291
  * circle(x2, 0, 20);
310
292
  * }
311
- * </code>
312
- * </div>
313
293
  */
314
294
  const PI = _PI;
315
295
 
@@ -328,8 +308,6 @@
328
308
  * @final
329
309
  *
330
310
  * @example
331
- * <div>
332
- * <code>
333
311
  * function setup() {
334
312
  * createCanvas(100, 100);
335
313
  *
@@ -340,11 +318,8 @@
340
318
  *
341
319
  * describe('A one-eighth slice of a circle drawn in white on a gray background.');
342
320
  * }
343
- * </code>
344
- * </div>
345
321
  *
346
- * <div>
347
- * <code>
322
+ * @example
348
323
  * function setup() {
349
324
  * createCanvas(100, 100);
350
325
  *
@@ -364,11 +339,8 @@
364
339
  *
365
340
  * describe('Two black lines that form a "V" opening towards the bottom-right corner of a gray square.');
366
341
  * }
367
- * </code>
368
- * </div>
369
342
  *
370
- * <div>
371
- * <code>
343
+ * @example
372
344
  * function setup() {
373
345
  * createCanvas(100, 100);
374
346
  *
@@ -398,8 +370,6 @@
398
370
  * fill(0, 0, 255);
399
371
  * circle(x2, 0, 20);
400
372
  * }
401
- * </code>
402
- * </div>
403
373
  */
404
374
  const QUARTER_PI = _PI / 4;
405
375
 
@@ -418,8 +388,6 @@
418
388
  * @final
419
389
  *
420
390
  * @example
421
- * <div>
422
- * <code>
423
391
  * function setup() {
424
392
  * createCanvas(100, 100);
425
393
  *
@@ -430,11 +398,8 @@
430
398
  *
431
399
  * describe('A white circle drawn on a gray background.');
432
400
  * }
433
- * </code>
434
- * </div>
435
401
  *
436
- * <div>
437
- * <code>
402
+ * @example
438
403
  * function setup() {
439
404
  * createCanvas(100, 100);
440
405
  *
@@ -459,11 +424,8 @@
459
424
  * 'Two horizontal black lines on a gray background. A thick line extends from the center toward the right. A thin line extends from the end of the thick line.'
460
425
  * );
461
426
  * }
462
- * </code>
463
- * </div>
464
427
  *
465
- * <div>
466
- * <code>
428
+ * @example
467
429
  * function setup() {
468
430
  * createCanvas(100, 100);
469
431
  *
@@ -493,8 +455,6 @@
493
455
  * fill(0, 0, 255);
494
456
  * circle(x2, 0, 10);
495
457
  * }
496
- * </code>
497
- * </div>
498
458
  */
499
459
  const TAU = _PI * 2;
500
460
 
@@ -513,8 +473,6 @@
513
473
  * @final
514
474
  *
515
475
  * @example
516
- * <div>
517
- * <code>
518
476
  * function setup() {
519
477
  * createCanvas(100, 100);
520
478
  *
@@ -525,11 +483,8 @@
525
483
  *
526
484
  * describe('A white circle drawn on a gray background.');
527
485
  * }
528
- * </code>
529
- * </div>
530
486
  *
531
- * <div>
532
- * <code>
487
+ * @example
533
488
  * function setup() {
534
489
  * createCanvas(100, 100);
535
490
  *
@@ -554,11 +509,8 @@
554
509
  * 'Two horizontal black lines on a gray background. A thick line extends from the center toward the right. A thin line extends from the end of the thick line.'
555
510
  * );
556
511
  * }
557
- * </code>
558
- * </div>
559
512
  *
560
- * <div>
561
- * <code>
513
+ * @example
562
514
  * function setup() {
563
515
  * createCanvas(100, 100);
564
516
  *
@@ -588,8 +540,6 @@
588
540
  * fill(0, 0, 255);
589
541
  * circle(x2, 0, 10);
590
542
  * }
591
- * </code>
592
- * </div>
593
543
  */
594
544
  const TWO_PI = _PI * 2;
595
545
 
@@ -1629,21 +1579,32 @@
1629
1579
  );
1630
1580
 
1631
1581
  const uniforms$5 = `
1632
- struct Uniforms {
1582
+ // Group 0: Material Properties
1583
+ struct MaterialUniforms {
1584
+ uUseVertexColor: u32,
1585
+ }
1586
+
1587
+ // Group 1: Model Transform
1588
+ struct ModelUniforms {
1633
1589
  // @p5 ifdef Vertex getWorldInputs
1634
1590
  uModelMatrix: mat4x4<f32>,
1635
- uViewMatrix: mat4x4<f32>,
1636
1591
  uModelNormalMatrix: mat3x3<f32>,
1637
- uCameraNormalMatrix: mat3x3<f32>,
1638
1592
  // @p5 endif
1639
1593
  // @p5 ifndef Vertex getWorldInputs
1640
1594
  uModelViewMatrix: mat4x4<f32>,
1641
1595
  uNormalMatrix: mat3x3<f32>,
1642
1596
  // @p5 endif
1643
- uProjectionMatrix: mat4x4<f32>,
1644
1597
  uMaterialColor: vec4<f32>,
1645
- uUseVertexColor: u32,
1646
- };
1598
+ }
1599
+
1600
+ // Group 2: Camera and Projection
1601
+ struct CameraUniforms {
1602
+ uProjectionMatrix: mat4x4<f32>,
1603
+ // @p5 ifdef Vertex getWorldInputs
1604
+ uViewMatrix: mat4x4<f32>,
1605
+ // @p5 endif
1606
+ uCameraNormalMatrix: mat3x3<f32>,
1607
+ }
1647
1608
  `;
1648
1609
 
1649
1610
  const colorVertexShader = `
@@ -1662,7 +1623,9 @@ struct VertexOutput {
1662
1623
  };
1663
1624
 
1664
1625
  ${uniforms$5}
1665
- @group(0) @binding(0) var<uniform> uniforms: Uniforms;
1626
+ @group(0) @binding(0) var<uniform> material: MaterialUniforms;
1627
+ @group(1) @binding(0) var<uniform> model: ModelUniforms;
1628
+ @group(2) @binding(0) var<uniform> camera: CameraUniforms;
1666
1629
 
1667
1630
  struct Vertex {
1668
1631
  position: vec3<f32>,
@@ -1676,12 +1639,12 @@ fn main(input: VertexInput) -> VertexOutput {
1676
1639
  HOOK_beforeVertex();
1677
1640
  var output: VertexOutput;
1678
1641
 
1679
- let useVertexColor = (uniforms.uUseVertexColor != 0 && input.aVertexColor.x >= 0.0);
1642
+ let useVertexColor = (material.uUseVertexColor != 0 && input.aVertexColor.x >= 0.0);
1680
1643
  var inputs = Vertex(
1681
1644
  input.aPosition,
1682
1645
  input.aNormal,
1683
1646
  input.aTexCoord,
1684
- select(uniforms.uMaterialColor, input.aVertexColor, useVertexColor)
1647
+ select(model.uMaterialColor, input.aVertexColor, useVertexColor)
1685
1648
  );
1686
1649
 
1687
1650
  // @p5 ifdef Vertex getObjectInputs
@@ -1689,20 +1652,20 @@ fn main(input: VertexInput) -> VertexOutput {
1689
1652
  // @p5 endif
1690
1653
 
1691
1654
  // @p5 ifdef Vertex getWorldInputs
1692
- inputs.position = (uniforms.uModelMatrix * vec4<f32>(inputs.position, 1.0)).xyz;
1693
- inputs.normal = uniforms.uModelNormalMatrix * inputs.normal;
1655
+ inputs.position = (model.uModelMatrix * vec4<f32>(inputs.position, 1.0)).xyz;
1656
+ inputs.normal = model.uModelNormalMatrix * inputs.normal;
1694
1657
  inputs = HOOK_getWorldInputs(inputs);
1695
1658
  // @p5 endif
1696
1659
 
1697
1660
  // @p5 ifdef Vertex getWorldInputs
1698
1661
  // Already multiplied by the model matrix, just apply view
1699
- inputs.position = (uniforms.uViewMatrix * vec4<f32>(inputs.position, 1.0)).xyz;
1700
- inputs.normal = uniforms.uCameraNormalMatrix * inputs.normal;
1662
+ inputs.position = (camera.uViewMatrix * vec4<f32>(inputs.position, 1.0)).xyz;
1663
+ inputs.normal = camera.uCameraNormalMatrix * inputs.normal;
1701
1664
  // @p5 endif
1702
1665
  // @p5 ifndef Vertex getWorldInputs
1703
1666
  // Apply both at once
1704
- inputs.position = (uniforms.uModelViewMatrix * vec4<f32>(inputs.position, 1.0)).xyz;
1705
- inputs.normal = uniforms.uNormalMatrix * inputs.normal;
1667
+ inputs.position = (model.uModelViewMatrix * vec4<f32>(inputs.position, 1.0)).xyz;
1668
+ inputs.normal = model.uNormalMatrix * inputs.normal;
1706
1669
  // @p5 endif
1707
1670
 
1708
1671
  // @p5 ifdef Vertex getCameraInputs
@@ -1713,7 +1676,7 @@ fn main(input: VertexInput) -> VertexOutput {
1713
1676
  output.vVertexNormal = normalize(inputs.normal);
1714
1677
  output.vColor = inputs.color;
1715
1678
 
1716
- output.Position = uniforms.uProjectionMatrix * vec4<f32>(inputs.position, 1.0);
1679
+ output.Position = camera.uProjectionMatrix * vec4<f32>(inputs.position, 1.0);
1717
1680
 
1718
1681
  HOOK_afterVertex();
1719
1682
  return output;
@@ -1728,7 +1691,9 @@ struct FragmentInput {
1728
1691
  };
1729
1692
 
1730
1693
  ${uniforms$5}
1731
- @group(0) @binding(0) var<uniform> uniforms: Uniforms;
1694
+ @group(0) @binding(0) var<uniform> material: MaterialUniforms;
1695
+ @group(1) @binding(0) var<uniform> model: ModelUniforms;
1696
+ @group(2) @binding(0) var<uniform> camera: CameraUniforms;
1732
1697
 
1733
1698
 
1734
1699
  @fragment
@@ -1742,7 +1707,17 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
1742
1707
  `;
1743
1708
 
1744
1709
  const uniforms$4 = `
1745
- struct Uniforms {
1710
+ // Group 0: Stroke Properties
1711
+ struct StrokeUniforms {
1712
+ uStrokeWeight: f32,
1713
+ uUseLineColor: f32,
1714
+ uSimpleLines: f32,
1715
+ uStrokeCap: u32,
1716
+ uStrokeJoin: u32,
1717
+ }
1718
+
1719
+ // Group 1: Model Transform
1720
+ struct ModelUniforms {
1746
1721
  // @p5 ifdef StrokeVertex getWorldInputs
1747
1722
  uModelMatrix: mat4x4<f32>,
1748
1723
  uViewMatrix: mat4x4<f32>,
@@ -1751,15 +1726,15 @@ struct Uniforms {
1751
1726
  uModelViewMatrix: mat4x4<f32>,
1752
1727
  // @p5 endif
1753
1728
  uMaterialColor: vec4<f32>,
1729
+ }
1730
+
1731
+ // Group 2: Camera and Projection
1732
+ struct CameraUniforms {
1754
1733
  uProjectionMatrix: mat4x4<f32>,
1755
- uStrokeWeight: f32,
1756
- uUseLineColor: f32,
1757
- uSimpleLines: f32,
1758
1734
  uViewport: vec4<f32>,
1759
1735
  uPerspective: u32,
1760
- uStrokeCap: u32,
1761
- uStrokeJoin: u32,
1762
- }`;
1736
+ }
1737
+ `;
1763
1738
 
1764
1739
  const lineVertexShader = `
1765
1740
  struct StrokeVertexInput {
@@ -1783,7 +1758,9 @@ struct StrokeVertexOutput {
1783
1758
  };
1784
1759
 
1785
1760
  ${uniforms$4}
1786
- @group(0) @binding(0) var<uniform> uniforms: Uniforms;
1761
+ @group(0) @binding(0) var<uniform> stroke: StrokeUniforms;
1762
+ @group(1) @binding(0) var<uniform> model: ModelUniforms;
1763
+ @group(2) @binding(0) var<uniform> camera: CameraUniforms;
1787
1764
 
1788
1765
  struct StrokeVertex {
1789
1766
  position: vec3<f32>,
@@ -1816,7 +1793,7 @@ fn lineIntersection(aPoint: vec2f, aDir: vec2f, bPoint: vec2f, bDir: vec2f) -> v
1816
1793
  fn main(input: StrokeVertexInput) -> StrokeVertexOutput {
1817
1794
  HOOK_beforeVertex();
1818
1795
  var output: StrokeVertexOutput;
1819
- let simpleLines = (uniforms.uSimpleLines != 0.);
1796
+ let simpleLines = (stroke.uSimpleLines != 0.);
1820
1797
  if (!simpleLines) {
1821
1798
  if (all(input.aTangentIn == vec3<f32>()) != all(input.aTangentOut == vec3<f32>())) {
1822
1799
  output.vCap = 1.;
@@ -1833,17 +1810,17 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput {
1833
1810
  }
1834
1811
  }
1835
1812
  var lineColor: vec4<f32>;
1836
- if (uniforms.uUseLineColor != 0.) {
1813
+ if (stroke.uUseLineColor != 0.) {
1837
1814
  lineColor = input.aVertexColor;
1838
1815
  } else {
1839
- lineColor = uniforms.uMaterialColor;
1816
+ lineColor = model.uMaterialColor;
1840
1817
  }
1841
1818
  var inputs = StrokeVertex(
1842
1819
  input.aPosition.xyz,
1843
1820
  input.aTangentIn,
1844
1821
  input.aTangentOut,
1845
1822
  lineColor,
1846
- uniforms.uStrokeWeight
1823
+ stroke.uStrokeWeight
1847
1824
  );
1848
1825
 
1849
1826
  // @p5 ifdef StrokeVertex getObjectInputs
@@ -1851,23 +1828,23 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput {
1851
1828
  // @p5 endif
1852
1829
 
1853
1830
  // @p5 ifdef StrokeVertex getWorldInputs
1854
- inputs.position = (uniforms.uModelMatrix * vec4<f32>(inputs.position, 1.)).xyz;
1855
- inputs.tangentIn = (uniforms.uModelMatrix * vec4<f32>(input.aTangentIn, 1.)).xyz;
1856
- inputs.tangentOut = (uniforms.uModelMatrix * vec4<f32>(input.aTangentOut, 1.)).xyz;
1831
+ inputs.position = (model.uModelMatrix * vec4<f32>(inputs.position, 1.)).xyz;
1832
+ inputs.tangentIn = (model.uModelMatrix * vec4<f32>(input.aTangentIn, 1.)).xyz;
1833
+ inputs.tangentOut = (model.uModelMatrix * vec4<f32>(input.aTangentOut, 1.)).xyz;
1857
1834
  inputs = HOOK_getWorldInputs(inputs);
1858
1835
  // @p5 endif
1859
1836
 
1860
1837
  // @p5 ifdef StrokeVertex getWorldInputs
1861
1838
  // Already multiplied by the model matrix, just apply view
1862
- inputs.position = (uniforms.uViewMatrix * vec4<f32>(inputs.position, 1.)).xyz;
1863
- inputs.tangentIn = (uniforms.uViewMatrix * vec4<f32>(input.aTangentIn, 0.)).xyz;
1864
- inputs.tangentOut = (uniforms.uViewMatrix * vec4<f32>(input.aTangentOut, 0.)).xyz;
1839
+ inputs.position = (model.uViewMatrix * vec4<f32>(inputs.position, 1.)).xyz;
1840
+ inputs.tangentIn = (model.uViewMatrix * vec4<f32>(input.aTangentIn, 0.)).xyz;
1841
+ inputs.tangentOut = (model.uViewMatrix * vec4<f32>(input.aTangentOut, 0.)).xyz;
1865
1842
  // @p5 endif
1866
1843
  // @p5 ifndef StrokeVertex getWorldInputs
1867
1844
  // Apply both at once
1868
- inputs.position = (uniforms.uModelViewMatrix * vec4<f32>(inputs.position, 1.)).xyz;
1869
- inputs.tangentIn = (uniforms.uModelViewMatrix * vec4<f32>(input.aTangentIn, 0.)).xyz;
1870
- inputs.tangentOut = (uniforms.uModelViewMatrix * vec4<f32>(input.aTangentOut, 0.)).xyz;
1845
+ inputs.position = (model.uModelViewMatrix * vec4<f32>(inputs.position, 1.)).xyz;
1846
+ inputs.tangentIn = (model.uModelViewMatrix * vec4<f32>(input.aTangentIn, 0.)).xyz;
1847
+ inputs.tangentOut = (model.uModelViewMatrix * vec4<f32>(input.aTangentOut, 0.)).xyz;
1871
1848
  // @p5 endif
1872
1849
  // @p5 ifdef StrokeVertex getCameraInputs
1873
1850
  inputs = HOOK_getCameraInputs(inputs);
@@ -1917,27 +1894,27 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput {
1917
1894
  posqIn.z -= dynamicZAdjustment;
1918
1895
  posqOut.z -= dynamicZAdjustment;
1919
1896
 
1920
- var p = uniforms.uProjectionMatrix * posp;
1921
- var qIn = uniforms.uProjectionMatrix * posqIn;
1922
- var qOut = uniforms.uProjectionMatrix * posqOut;
1897
+ var p = camera.uProjectionMatrix * posp;
1898
+ var qIn = camera.uProjectionMatrix * posqIn;
1899
+ var qOut = camera.uProjectionMatrix * posqOut;
1923
1900
 
1924
- var tangentIn = normalize((qIn.xy * p.w - p.xy * qIn.w) * uniforms.uViewport.zw);
1925
- var tangentOut = normalize((qOut.xy * p.w - p.xy * qOut.w) * uniforms.uViewport.zw);
1901
+ var tangentIn = normalize((qIn.xy * p.w - p.xy * qIn.w) * camera.uViewport.zw);
1902
+ var tangentOut = normalize((qOut.xy * p.w - p.xy * qOut.w) * camera.uViewport.zw);
1926
1903
 
1927
1904
  var curPerspScale = vec2<f32>();
1928
- if (uniforms.uPerspective == 1) {
1905
+ if (camera.uPerspective == 1) {
1929
1906
  // Perspective ---
1930
1907
  // convert from world to clip by multiplying with projection scaling factor
1931
1908
  // to get the right thickness (see https://github.com/processing/processing/issues/5182)
1932
1909
 
1933
1910
  // The y value of the projection matrix may be flipped if rendering to a Framebuffer.
1934
1911
  // Multiplying again by its sign here negates the flip to get just the scale.
1935
- curPerspScale = (uniforms.uProjectionMatrix * vec4(1., sign(uniforms.uProjectionMatrix[1][1]), 0., 0.)).xy;
1912
+ curPerspScale = (camera.uProjectionMatrix * vec4(1., sign(camera.uProjectionMatrix[1][1]), 0., 0.)).xy;
1936
1913
  } else {
1937
1914
  // No Perspective ---
1938
1915
  // multiply by W (to cancel out division by W later in the pipeline) and
1939
1916
  // convert from screen to clip (derived from clip to screen above)
1940
- curPerspScale = p.w / (0.5 * uniforms.uViewport.zw);
1917
+ curPerspScale = p.w / (0.5 * camera.uViewport.zw);
1941
1918
  }
1942
1919
 
1943
1920
  var offset = vec2<f32>();
@@ -1960,7 +1937,7 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput {
1960
1937
  if (sideEnum == 2.) {
1961
1938
  // Calculate the position + tangent on either side of the join, and
1962
1939
  // find where the lines intersect to find the elbow of the join
1963
- var c = (posp.xy / posp.w + vec2<f32>(1.)) * 0.5 * uniforms.uViewport.zw;
1940
+ var c = (posp.xy / posp.w + vec2<f32>(1.)) * 0.5 * camera.uViewport.zw;
1964
1941
 
1965
1942
  var intersection = lineIntersection(
1966
1943
  c + (side * normalIn * inputs.weight / 2.),
@@ -1986,7 +1963,7 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput {
1986
1963
  offset = side * normalOut * inputs.weight / 2.;
1987
1964
  }
1988
1965
  }
1989
- if (uniforms.uStrokeJoin == 2) {
1966
+ if (stroke.uStrokeJoin == 2) {
1990
1967
  var avgNormal = vec2<f32>(-output.vTangent.y, output.vTangent.x);
1991
1968
  output.vMaxDist = abs(dot(avgNormal, normalIn * inputs.weight / 2.));
1992
1969
  } else {
@@ -2035,7 +2012,9 @@ struct StrokeFragmentInput {
2035
2012
  }
2036
2013
 
2037
2014
  ${uniforms$4}
2038
- @group(0) @binding(0) var<uniform> uniforms: Uniforms;
2015
+ @group(0) @binding(0) var<uniform> stroke: StrokeUniforms;
2016
+ @group(1) @binding(0) var<uniform> model: ModelUniforms;
2017
+ @group(2) @binding(0) var<uniform> camera: CameraUniforms;
2039
2018
 
2040
2019
 
2041
2020
  fn distSquared(a: vec2<f32>, b: vec2<f32>) -> f32 {
@@ -2064,12 +2043,12 @@ fn main(input: StrokeFragmentInput) -> @location(0) vec4<f32> {
2064
2043
 
2065
2044
  if (input.vCap > 0.) {
2066
2045
  if (
2067
- uniforms.uStrokeCap == STROKE_CAP_ROUND &&
2046
+ stroke.uStrokeCap == STROKE_CAP_ROUND &&
2068
2047
  HOOK_shouldDiscard(distSquared(inputs.position, inputs.center) > inputs.strokeWeight * inputs.strokeWeight * 0.25)
2069
2048
  ) {
2070
2049
  discard;
2071
2050
  } else if (
2072
- uniforms.uStrokeCap == STROKE_CAP_SQUARE &&
2051
+ stroke.uStrokeCap == STROKE_CAP_SQUARE &&
2073
2052
  HOOK_shouldDiscard(dot(inputs.position - inputs.center, inputs.tangent) > 0.)
2074
2053
  ) {
2075
2054
  discard;
@@ -2078,11 +2057,11 @@ fn main(input: StrokeFragmentInput) -> @location(0) vec4<f32> {
2078
2057
  }
2079
2058
  } else if (input.vJoin > 0.) {
2080
2059
  if (
2081
- uniforms.uStrokeJoin == STROKE_JOIN_ROUND &&
2060
+ stroke.uStrokeJoin == STROKE_JOIN_ROUND &&
2082
2061
  HOOK_shouldDiscard(distSquared(inputs.position, inputs.center) > inputs.strokeWeight * inputs.strokeWeight * 0.25)
2083
2062
  ) {
2084
2063
  discard;
2085
- } else if (uniforms.uStrokeJoin == STROKE_JOIN_BEVEL) {
2064
+ } else if (stroke.uStrokeJoin == STROKE_JOIN_BEVEL) {
2086
2065
  let normal = vec2<f32>(-inputs.tangent.y, -inputs.tangent.x);
2087
2066
  if (HOOK_shouldDiscard(abs(dot(inputs.position - inputs.center, normal)) > input.vMaxDist)) {
2088
2067
  discard;
@@ -2099,42 +2078,31 @@ fn main(input: StrokeFragmentInput) -> @location(0) vec4<f32> {
2099
2078
  `;
2100
2079
 
2101
2080
  const uniforms$3 = `
2102
- struct Uniforms {
2103
- // @p5 ifdef Vertex getWorldInputs
2104
- uModelMatrix: mat4x4<f32>,
2105
- uModelNormalMatrix: mat3x3<f32>,
2106
- uCameraNormalMatrix: mat3x3<f32>,
2107
- // @p5 endif
2108
- // @p5 ifndef Vertex getWorldInputs
2109
- uModelViewMatrix: mat4x4<f32>,
2110
- uNormalMatrix: mat3x3<f32>,
2111
- // @p5 endif
2112
- uViewMatrix: mat4x4<f32>,
2113
- uProjectionMatrix: mat4x4<f32>,
2114
- uMaterialColor: vec4<f32>,
2081
+ // Group 0: Material Properties
2082
+ struct MaterialUniforms {
2115
2083
  uUseVertexColor: u32,
2116
-
2117
2084
  uHasSetAmbient: u32,
2118
2085
  uAmbientColor: vec3<f32>,
2119
2086
  uSpecularMatColor: vec4<f32>,
2120
2087
  uAmbientMatColor: vec4<f32>,
2121
2088
  uEmissiveMatColor: vec4<f32>,
2122
-
2123
2089
  uTint: vec4<f32>,
2124
2090
  isTexture: u32,
2091
+ uSpecular: u32,
2092
+ uShininess: f32,
2093
+ uMetallic: f32,
2094
+ }
2125
2095
 
2126
- uCameraRotation: mat3x3<f32>,
2127
-
2096
+ // Group 0: Lighting
2097
+ struct LightingUniforms {
2128
2098
  uDirectionalLightCount: i32,
2129
2099
  uLightingDirection: array<vec3<f32>, 5>,
2130
2100
  uDirectionalDiffuseColors: array<vec3<f32>, 5>,
2131
2101
  uDirectionalSpecularColors: array<vec3<f32>, 5>,
2132
-
2133
2102
  uPointLightCount: i32,
2134
2103
  uPointLightLocation: array<vec3<f32>, 5>,
2135
2104
  uPointLightDiffuseColors: array<vec3<f32>, 5>,
2136
2105
  uPointLightSpecularColors: array<vec3<f32>, 5>,
2137
-
2138
2106
  uSpotLightCount: i32,
2139
2107
  uSpotLightAngle: vec4<f32>,
2140
2108
  uSpotLightConc: vec4<f32>,
@@ -2142,18 +2110,32 @@ struct Uniforms {
2142
2110
  uSpotLightSpecularColors: array<vec3<f32>, 4>,
2143
2111
  uSpotLightLocation: array<vec3<f32>, 4>,
2144
2112
  uSpotLightDirection: array<vec3<f32>, 4>,
2145
-
2146
- uSpecular: u32,
2147
- uShininess: f32,
2148
- uMetallic: f32,
2149
-
2150
2113
  uConstantAttenuation: f32,
2151
2114
  uLinearAttenuation: f32,
2152
2115
  uQuadraticAttenuation: f32,
2153
-
2154
2116
  uUseImageLight: u32,
2155
2117
  uUseLighting: u32,
2156
- };
2118
+ }
2119
+
2120
+ // Group 1: Model Transform
2121
+ struct ModelUniforms {
2122
+ // @p5 ifdef Vertex getWorldInputs
2123
+ uModelMatrix: mat4x4<f32>,
2124
+ uModelNormalMatrix: mat3x3<f32>,
2125
+ // @p5 endif
2126
+ // @p5 ifndef Vertex getWorldInputs
2127
+ uModelViewMatrix: mat4x4<f32>,
2128
+ uNormalMatrix: mat3x3<f32>,
2129
+ // @p5 endif
2130
+ uMaterialColor: vec4<f32>,
2131
+ }
2132
+
2133
+ // Group 2: Camera and Projection
2134
+ struct CameraUniforms {
2135
+ uViewMatrix: mat4x4<f32>,
2136
+ uProjectionMatrix: mat4x4<f32>,
2137
+ uCameraNormalMatrix: mat3x3<f32>,
2138
+ }
2157
2139
  `;
2158
2140
 
2159
2141
  const materialVertexShader = `
@@ -2173,7 +2155,10 @@ struct VertexOutput {
2173
2155
  };
2174
2156
 
2175
2157
  ${uniforms$3}
2176
- @group(0) @binding(0) var<uniform> uniforms: Uniforms;
2158
+ @group(0) @binding(0) var<uniform> material: MaterialUniforms;
2159
+ @group(0) @binding(1) var<uniform> lighting: LightingUniforms;
2160
+ @group(1) @binding(0) var<uniform> model: ModelUniforms;
2161
+ @group(2) @binding(0) var<uniform> camera: CameraUniforms;
2177
2162
 
2178
2163
  struct Vertex {
2179
2164
  position: vec3<f32>,
@@ -2187,12 +2172,12 @@ fn main(input: VertexInput) -> VertexOutput {
2187
2172
  HOOK_beforeVertex();
2188
2173
  var output: VertexOutput;
2189
2174
 
2190
- let useVertexColor = (uniforms.uUseVertexColor != 0 && input.aVertexColor.x >= 0.0);
2175
+ let useVertexColor = (material.uUseVertexColor != 0 && input.aVertexColor.x >= 0.0);
2191
2176
  var inputs = Vertex(
2192
2177
  input.aPosition,
2193
2178
  input.aNormal,
2194
2179
  input.aTexCoord,
2195
- select(uniforms.uMaterialColor, input.aVertexColor, useVertexColor)
2180
+ select(model.uMaterialColor, input.aVertexColor, useVertexColor)
2196
2181
  );
2197
2182
 
2198
2183
  // @p5 ifdef Vertex getObjectInputs
@@ -2200,20 +2185,20 @@ fn main(input: VertexInput) -> VertexOutput {
2200
2185
  // @p5 endif
2201
2186
 
2202
2187
  // @p5 ifdef Vertex getWorldInputs
2203
- inputs.position = (uniforms.uModelMatrix * vec4<f32>(inputs.position, 1.0)).xyz;
2204
- inputs.normal = uniforms.uModelNormalMatrix * inputs.normal;
2188
+ inputs.position = (model.uModelMatrix * vec4<f32>(inputs.position, 1.0)).xyz;
2189
+ inputs.normal = model.uModelNormalMatrix * inputs.normal;
2205
2190
  inputs = HOOK_getWorldInputs(inputs);
2206
2191
  // @p5 endif
2207
2192
 
2208
2193
  // @p5 ifdef Vertex getWorldInputs
2209
2194
  // Already multiplied by the model matrix, just apply view
2210
- inputs.position = (uniforms.uViewMatrix * vec4<f32>(inputs.position, 1.0)).xyz;
2211
- inputs.normal = uniforms.uCameraNormalMatrix * inputs.normal;
2195
+ inputs.position = (camera.uViewMatrix * vec4<f32>(inputs.position, 1.0)).xyz;
2196
+ inputs.normal = camera.uCameraNormalMatrix * inputs.normal;
2212
2197
  // @p5 endif
2213
2198
  // @p5 ifndef Vertex getWorldInputs
2214
2199
  // Apply both at once
2215
- inputs.position = (uniforms.uModelViewMatrix * vec4<f32>(inputs.position, 1.0)).xyz;
2216
- inputs.normal = uniforms.uNormalMatrix * inputs.normal;
2200
+ inputs.position = (model.uModelViewMatrix * vec4<f32>(inputs.position, 1.0)).xyz;
2201
+ inputs.normal = model.uNormalMatrix * inputs.normal;
2217
2202
  // @p5 endif
2218
2203
 
2219
2204
  // @p5 ifdef Vertex getCameraInputs
@@ -2225,7 +2210,7 @@ fn main(input: VertexInput) -> VertexOutput {
2225
2210
  output.vNormal = normalize(inputs.normal);
2226
2211
  output.vColor = inputs.color;
2227
2212
 
2228
- output.Position = uniforms.uProjectionMatrix * vec4<f32>(inputs.position, 1.0);
2213
+ output.Position = camera.uProjectionMatrix * vec4<f32>(inputs.position, 1.0);
2229
2214
 
2230
2215
  HOOK_afterVertex();
2231
2216
  return output;
@@ -2241,15 +2226,16 @@ struct FragmentInput {
2241
2226
  };
2242
2227
 
2243
2228
  ${uniforms$3}
2244
- @group(0) @binding(0) var<uniform> uniforms: Uniforms;
2245
-
2246
- @group(0) @binding(1) var uSampler: texture_2d<f32>;
2247
- @group(0) @binding(2) var uSampler_sampler: sampler;
2248
-
2249
- @group(0) @binding(3) var environmentMapDiffused: texture_2d<f32>;
2250
- @group(0) @binding(4) var environmentMapDiffused_sampler: sampler;
2251
- @group(0) @binding(5) var environmentMapSpecular: texture_2d<f32>;
2252
- @group(0) @binding(6) var environmentMapSpecular_sampler: sampler;
2229
+ @group(0) @binding(0) var<uniform> material: MaterialUniforms;
2230
+ @group(0) @binding(1) var<uniform> lighting: LightingUniforms;
2231
+ @group(0) @binding(2) var uSampler: texture_2d<f32>;
2232
+ @group(0) @binding(3) var uSampler_sampler: sampler;
2233
+ @group(0) @binding(4) var environmentMapDiffused: texture_2d<f32>;
2234
+ @group(0) @binding(5) var environmentMapDiffused_sampler: sampler;
2235
+ @group(0) @binding(6) var environmentMapSpecular: texture_2d<f32>;
2236
+ @group(0) @binding(7) var environmentMapSpecular_sampler: sampler;
2237
+ @group(1) @binding(0) var<uniform> model: ModelUniforms;
2238
+ @group(2) @binding(0) var<uniform> camera: CameraUniforms;
2253
2239
 
2254
2240
  struct ColorComponents {
2255
2241
  baseColor: vec3<f32>,
@@ -2312,7 +2298,7 @@ fn mapTextureToNormal(v: vec3<f32>) -> vec2<f32> {
2312
2298
  fn calculateImageDiffuse(vNormal: vec3<f32>, vViewPosition: vec3<f32>, metallic: f32) -> vec3<f32> {
2313
2299
  // make 2 seperate builds
2314
2300
  let worldCameraPosition = vec3<f32>(0.0, 0.0, 0.0); // hardcoded world camera position
2315
- let worldNormal = normalize(vNormal * uniforms.uCameraRotation);
2301
+ let worldNormal = normalize(vNormal * camera.uCameraNormalMatrix);
2316
2302
  let newTexCoord = mapTextureToNormal(worldNormal);
2317
2303
  let texture = textureSample(environmentMapDiffused, environmentMapDiffused_sampler, newTexCoord);
2318
2304
  // this is to make the darker sections more dark
@@ -2324,7 +2310,7 @@ fn calculateImageSpecular(vNormal: vec3<f32>, vViewPosition: vec3<f32>, shinines
2324
2310
  let worldCameraPosition = vec3<f32>(0.0, 0.0, 0.0);
2325
2311
  let worldNormal = normalize(vNormal);
2326
2312
  let lightDirection = normalize(vViewPosition - worldCameraPosition);
2327
- let R = reflect(lightDirection, worldNormal) * uniforms.uCameraRotation;
2313
+ let R = reflect(lightDirection, worldNormal) * camera.uCameraNormalMatrix;
2328
2314
  let newTexCoord = mapTextureToNormal(R);
2329
2315
 
2330
2316
  // In p5js the range of shininess is >= 1,
@@ -2373,7 +2359,7 @@ fn singleLight(
2373
2359
  let specular = select(
2374
2360
  0.,
2375
2361
  phongSpecular(lightDir, viewDirection, normal, shininess) * specularIntensity,
2376
- uniforms.uSpecular == 1
2362
+ material.uSpecular == 1
2377
2363
  );
2378
2364
  return LightIntensityResult(diffuse, specular);
2379
2365
  }
@@ -2387,69 +2373,69 @@ fn totalLight(
2387
2373
  var totalSpecular = vec3<f32>(0.0, 0.0, 0.0);
2388
2374
  var totalDiffuse = vec3<f32>(0.0, 0.0, 0.0);
2389
2375
 
2390
- if (uniforms.uUseLighting == 0) {
2376
+ if (lighting.uUseLighting == 0) {
2391
2377
  return LightResult(vec3<f32>(1.0, 1.0, 1.0), totalSpecular);
2392
2378
  }
2393
2379
 
2394
2380
  let viewDirection = normalize(-modelPosition);
2395
2381
 
2396
2382
  for (var j = 0; j < 5; j++) {
2397
- if (j < uniforms.uDirectionalLightCount) {
2398
- let lightVector = (uniforms.uViewMatrix * vec4<f32>(
2399
- uniforms.uLightingDirection[j],
2383
+ if (j < lighting.uDirectionalLightCount) {
2384
+ let lightVector = (camera.uViewMatrix * vec4<f32>(
2385
+ lighting.uLightingDirection[j],
2400
2386
  0.0
2401
2387
  )).xyz;
2402
- let lightColor = uniforms.uDirectionalDiffuseColors[j];
2403
- let specularColor = uniforms.uDirectionalSpecularColors[j];
2388
+ let lightColor = lighting.uDirectionalDiffuseColors[j];
2389
+ let specularColor = lighting.uDirectionalSpecularColors[j];
2404
2390
  let result = singleLight(viewDirection, normal, lightVector, shininess, metallic);
2405
2391
  totalDiffuse += result.diffuse * lightColor;
2406
2392
  totalSpecular += result.specular * specularColor;
2407
2393
  }
2408
2394
 
2409
- if (j < uniforms.uPointLightCount) {
2410
- let lightPosition = (uniforms.uViewMatrix * vec4<f32>(
2411
- uniforms.uPointLightLocation[j],
2395
+ if (j < lighting.uPointLightCount) {
2396
+ let lightPosition = (camera.uViewMatrix * vec4<f32>(
2397
+ lighting.uPointLightLocation[j],
2412
2398
  1.0
2413
2399
  )).xyz;
2414
2400
  let lightVector = modelPosition - lightPosition;
2415
2401
  let lightDistance = length(lightVector);
2416
2402
  let lightFalloff = 1.0 / (
2417
- uniforms.uConstantAttenuation +
2418
- lightDistance * uniforms.uLinearAttenuation +
2419
- lightDistance * lightDistance * uniforms.uQuadraticAttenuation
2403
+ lighting.uConstantAttenuation +
2404
+ lightDistance * lighting.uLinearAttenuation +
2405
+ lightDistance * lightDistance * lighting.uQuadraticAttenuation
2420
2406
  );
2421
- let lightColor = uniforms.uPointLightDiffuseColors[j] * lightFalloff;
2422
- let specularColor = uniforms.uPointLightSpecularColors[j] * lightFalloff;
2407
+ let lightColor = lighting.uPointLightDiffuseColors[j] * lightFalloff;
2408
+ let specularColor = lighting.uPointLightSpecularColors[j] * lightFalloff;
2423
2409
  let result = singleLight(viewDirection, normal, lightVector, shininess, metallic);
2424
2410
  totalDiffuse += result.diffuse * lightColor;
2425
2411
  totalSpecular += result.specular * specularColor;
2426
2412
  }
2427
2413
 
2428
- if (j < uniforms.uSpotLightCount) {
2429
- let lightPosition = (uniforms.uViewMatrix * vec4<f32>(
2430
- uniforms.uSpotLightLocation[j],
2414
+ if (j < lighting.uSpotLightCount) {
2415
+ let lightPosition = (camera.uViewMatrix * vec4<f32>(
2416
+ lighting.uSpotLightLocation[j],
2431
2417
  1.0
2432
2418
  )).xyz;
2433
2419
  let lightVector = modelPosition - lightPosition;
2434
2420
  let lightDistance = length(lightVector);
2435
2421
  var lightFalloff = 1.0 / (
2436
- uniforms.uConstantAttenuation +
2437
- lightDistance * uniforms.uLinearAttenuation +
2438
- lightDistance * lightDistance * uniforms.uQuadraticAttenuation
2422
+ lighting.uConstantAttenuation +
2423
+ lightDistance * lighting.uLinearAttenuation +
2424
+ lightDistance * lightDistance * lighting.uQuadraticAttenuation
2439
2425
  );
2440
- let lightDirection = (uniforms.uViewMatrix * vec4<f32>(
2441
- uniforms.uSpotLightDirection[j],
2426
+ let lightDirection = (camera.uViewMatrix * vec4<f32>(
2427
+ lighting.uSpotLightDirection[j],
2442
2428
  0.0
2443
2429
  )).xyz;
2444
2430
  let spotDot = dot(normalize(lightVector), normalize(lightDirection));
2445
2431
  let spotFalloff = select(
2446
2432
  0.0,
2447
- pow(spotDot, uniforms.uSpotLightConc[j]),
2448
- spotDot < uniforms.uSpotLightAngle[j]
2433
+ pow(spotDot, lighting.uSpotLightConc[j]),
2434
+ spotDot < lighting.uSpotLightAngle[j]
2449
2435
  );
2450
2436
  lightFalloff *= spotFalloff;
2451
- let lightColor = uniforms.uSpotLightDiffuseColors[j];
2452
- let specularColor = uniforms.uSpotLightSpecularColors[j];
2437
+ let lightColor = lighting.uSpotLightDiffuseColors[j];
2438
+ let specularColor = lighting.uSpotLightSpecularColors[j];
2453
2439
  let result = singleLight(viewDirection, normal, lightVector, shininess, metallic);
2454
2440
  totalDiffuse += result.diffuse * lightColor;
2455
2441
  totalSpecular += result.specular * specularColor;
@@ -2457,7 +2443,7 @@ fn totalLight(
2457
2443
  }
2458
2444
 
2459
2445
  // Image light contribution
2460
- if (uniforms.uUseImageLight != 0) {
2446
+ if (lighting.uUseImageLight != 0) {
2461
2447
  totalDiffuse += calculateImageDiffuse(normal, modelPosition, metallic);
2462
2448
  totalSpecular += calculateImageSpecular(normal, modelPosition, shininess, metallic);
2463
2449
  }
@@ -2474,19 +2460,19 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
2474
2460
 
2475
2461
  let color = select(
2476
2462
  input.vColor,
2477
- textureSample(uSampler, uSampler_sampler, input.vTexCoord) * (uniforms.uTint/255.0),
2478
- uniforms.isTexture == 1
2463
+ textureSample(uSampler, uSampler_sampler, input.vTexCoord) * (material.uTint/255.0),
2464
+ material.isTexture == 1
2479
2465
  ); // TODO: check isTexture and apply tint
2480
2466
  var inputs = Inputs(
2481
2467
  normalize(input.vNormal),
2482
2468
  input.vTexCoord,
2483
- uniforms.uAmbientColor,
2484
- select(color.rgb, uniforms.uAmbientMatColor.rgb, uniforms.uHasSetAmbient == 1),
2485
- uniforms.uSpecularMatColor.rgb,
2486
- uniforms.uEmissiveMatColor.rgb,
2469
+ material.uAmbientColor,
2470
+ select(color.rgb, material.uAmbientMatColor.rgb, material.uHasSetAmbient == 1),
2471
+ material.uSpecularMatColor.rgb,
2472
+ material.uEmissiveMatColor.rgb,
2487
2473
  color,
2488
- uniforms.uShininess,
2489
- uniforms.uMetallic
2474
+ material.uShininess,
2475
+ material.uMetallic
2490
2476
  );
2491
2477
  inputs = HOOK_getPixelInputs(inputs);
2492
2478
 
@@ -2519,9 +2505,8 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
2519
2505
  `;
2520
2506
 
2521
2507
  const uniforms$2 = `
2522
- struct Uniforms {
2523
- uModelViewMatrix: mat4x4<f32>,
2524
- uProjectionMatrix: mat4x4<f32>,
2508
+ // Group 0: Font Properties
2509
+ struct FontUniforms {
2525
2510
  uStrokeImageSize: vec2<i32>,
2526
2511
  uCellsImageSize: vec2<i32>,
2527
2512
  uGridImageSize: vec2<i32>,
@@ -2530,7 +2515,17 @@ struct Uniforms {
2530
2515
  uGlyphRect: vec4<f32>,
2531
2516
  uGlyphOffset: f32,
2532
2517
  uMaterialColor: vec4<f32>,
2533
- };
2518
+ }
2519
+
2520
+ // Group 1: Model Transform
2521
+ struct ModelUniforms {
2522
+ uModelViewMatrix: mat4x4<f32>,
2523
+ }
2524
+
2525
+ // Group 2: Camera and Projection
2526
+ struct CameraUniforms {
2527
+ uProjectionMatrix: mat4x4<f32>,
2528
+ }
2534
2529
  `;
2535
2530
 
2536
2531
  const fontVertexShader = `
@@ -2545,7 +2540,9 @@ struct VertexOutput {
2545
2540
  };
2546
2541
 
2547
2542
  ${uniforms$2}
2548
- @group(0) @binding(0) var<uniform> uniforms: Uniforms;
2543
+ @group(0) @binding(0) var<uniform> font: FontUniforms;
2544
+ @group(1) @binding(0) var<uniform> model: ModelUniforms;
2545
+ @group(2) @binding(0) var<uniform> camera: CameraUniforms;
2549
2546
 
2550
2547
  @vertex
2551
2548
  fn main(input: VertexInput) -> VertexOutput {
@@ -2553,35 +2550,35 @@ fn main(input: VertexInput) -> VertexOutput {
2553
2550
  var positionVec4 = vec4<f32>(input.aPosition, 1.0);
2554
2551
 
2555
2552
  // scale by the size of the glyph's rectangle
2556
- positionVec4.x = positionVec4.x * (uniforms.uGlyphRect.z - uniforms.uGlyphRect.x);
2557
- positionVec4.y = positionVec4.y * (uniforms.uGlyphRect.w - uniforms.uGlyphRect.y);
2553
+ positionVec4.x = positionVec4.x * (font.uGlyphRect.z - font.uGlyphRect.x);
2554
+ positionVec4.y = positionVec4.y * (font.uGlyphRect.w - font.uGlyphRect.y);
2558
2555
 
2559
2556
  // Expand glyph bounding boxes by 1px on each side to give a bit of room
2560
2557
  // for antialiasing
2561
- let newOrigin = (uniforms.uModelViewMatrix * vec4<f32>(0.0, 0.0, 0.0, 1.0)).xyz;
2562
- let newDX = (uniforms.uModelViewMatrix * vec4<f32>(1.0, 0.0, 0.0, 1.0)).xyz;
2563
- let newDY = (uniforms.uModelViewMatrix * vec4<f32>(0.0, 1.0, 0.0, 1.0)).xyz;
2558
+ let newOrigin = (model.uModelViewMatrix * vec4<f32>(0.0, 0.0, 0.0, 1.0)).xyz;
2559
+ let newDX = (model.uModelViewMatrix * vec4<f32>(1.0, 0.0, 0.0, 1.0)).xyz;
2560
+ let newDY = (model.uModelViewMatrix * vec4<f32>(0.0, 1.0, 0.0, 1.0)).xyz;
2564
2561
  let pixelScale = vec2<f32>(
2565
2562
  1.0 / length(newOrigin - newDX),
2566
2563
  1.0 / length(newOrigin - newDY)
2567
2564
  );
2568
2565
  let offset = pixelScale * normalize(input.aTexCoord - vec2<f32>(0.5, 0.5));
2569
2566
  let textureOffset = offset * (1.0 / vec2<f32>(
2570
- uniforms.uGlyphRect.z - uniforms.uGlyphRect.x,
2571
- uniforms.uGlyphRect.w - uniforms.uGlyphRect.y
2567
+ font.uGlyphRect.z - font.uGlyphRect.x,
2568
+ font.uGlyphRect.w - font.uGlyphRect.y
2572
2569
  ));
2573
2570
 
2574
2571
  // move to the corner of the glyph
2575
- positionVec4.x = positionVec4.x + uniforms.uGlyphRect.x;
2576
- positionVec4.y = positionVec4.y + uniforms.uGlyphRect.y;
2572
+ positionVec4.x = positionVec4.x + font.uGlyphRect.x;
2573
+ positionVec4.y = positionVec4.y + font.uGlyphRect.y;
2577
2574
 
2578
2575
  // move to the letter's line offset
2579
- positionVec4.x = positionVec4.x + uniforms.uGlyphOffset;
2576
+ positionVec4.x = positionVec4.x + font.uGlyphOffset;
2580
2577
 
2581
2578
  positionVec4.x = positionVec4.x + offset.x;
2582
2579
  positionVec4.y = positionVec4.y + offset.y;
2583
2580
 
2584
- output.Position = uniforms.uProjectionMatrix * uniforms.uModelViewMatrix * positionVec4;
2581
+ output.Position = camera.uProjectionMatrix * model.uModelViewMatrix * positionVec4;
2585
2582
  output.vTexCoord = input.aTexCoord + textureOffset;
2586
2583
 
2587
2584
  return output;
@@ -2594,18 +2591,19 @@ struct FragmentInput {
2594
2591
  };
2595
2592
 
2596
2593
  ${uniforms$2}
2597
- @group(0) @binding(0) var<uniform> uniforms: Uniforms;
2598
-
2599
- @group(1) @binding(0) var uSamplerStrokes: texture_2d<f32>;
2600
- @group(1) @binding(1) var uSamplerStrokes_sampler: sampler;
2601
- @group(1) @binding(2) var uSamplerRowStrokes: texture_2d<f32>;
2602
- @group(1) @binding(3) var uSamplerRowStrokes_sampler: sampler;
2603
- @group(1) @binding(4) var uSamplerRows: texture_2d<f32>;
2604
- @group(1) @binding(5) var uSamplerRows_sampler: sampler;
2605
- @group(1) @binding(6) var uSamplerColStrokes: texture_2d<f32>;
2606
- @group(1) @binding(7) var uSamplerColStrokes_sampler: sampler;
2607
- @group(1) @binding(8) var uSamplerCols: texture_2d<f32>;
2608
- @group(1) @binding(9) var uSamplerCols_sampler: sampler;
2594
+ @group(0) @binding(0) var<uniform> font: FontUniforms;
2595
+ @group(0) @binding(1) var uSamplerStrokes: texture_2d<f32>;
2596
+ @group(0) @binding(2) var uSamplerStrokes_sampler: sampler;
2597
+ @group(0) @binding(3) var uSamplerRowStrokes: texture_2d<f32>;
2598
+ @group(0) @binding(4) var uSamplerRowStrokes_sampler: sampler;
2599
+ @group(0) @binding(5) var uSamplerRows: texture_2d<f32>;
2600
+ @group(0) @binding(6) var uSamplerRows_sampler: sampler;
2601
+ @group(0) @binding(7) var uSamplerColStrokes: texture_2d<f32>;
2602
+ @group(0) @binding(8) var uSamplerColStrokes_sampler: sampler;
2603
+ @group(0) @binding(9) var uSamplerCols: texture_2d<f32>;
2604
+ @group(0) @binding(10) var uSamplerCols_sampler: sampler;
2605
+ @group(1) @binding(0) var<uniform> model: ModelUniforms;
2606
+ @group(2) @binding(0) var<uniform> camera: CameraUniforms;
2609
2607
 
2610
2608
  // some helper functions
2611
2609
  fn ROUND_f32(v: f32) -> i32 { return i32(floor(v + 0.5)); }
@@ -2737,14 +2735,14 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
2737
2735
  let pixelScale = hardness / fwidth(input.vTexCoord);
2738
2736
 
2739
2737
  // which grid cell is this pixel in?
2740
- let gridCoord = vec2<i32>(floor(input.vTexCoord * vec2<f32>(uniforms.uGridSize)));
2738
+ let gridCoord = vec2<i32>(floor(input.vTexCoord * vec2<f32>(font.uGridSize)));
2741
2739
 
2742
2740
  // intersect curves in this row
2743
2741
  {
2744
2742
  // the index into the row info bitmap
2745
- let rowIndex = gridCoord.y + uniforms.uGridOffset.y;
2743
+ let rowIndex = gridCoord.y + font.uGridOffset.y;
2746
2744
  // fetch the info texel
2747
- let rowInfo = getTexel(uSamplerRows, uSamplerRows_sampler, rowIndex, uniforms.uGridImageSize);
2745
+ let rowInfo = getTexel(uSamplerRows, uSamplerRows_sampler, rowIndex, font.uGridImageSize);
2748
2746
  // unpack the rowInfo
2749
2747
  let rowStrokeIndex = getInt16(rowInfo.xy);
2750
2748
  let rowStrokeCount = getInt16(rowInfo.zw);
@@ -2757,14 +2755,14 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
2757
2755
  // each stroke is made up of 3 points: the start and control point
2758
2756
  // and the start of the next curve.
2759
2757
  // fetch the indices of this pair of strokes:
2760
- let strokeIndices = getTexel(uSamplerRowStrokes, uSamplerRowStrokes_sampler, rowStrokeIndex + iRowStroke, uniforms.uCellsImageSize);
2758
+ let strokeIndices = getTexel(uSamplerRowStrokes, uSamplerRowStrokes_sampler, rowStrokeIndex + iRowStroke, font.uCellsImageSize);
2761
2759
 
2762
2760
  // unpack the stroke index
2763
2761
  let strokePos = getInt16(strokeIndices.xy);
2764
2762
 
2765
2763
  // fetch the two strokes
2766
- let stroke0 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 0, uniforms.uStrokeImageSize);
2767
- let stroke1 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 1, uniforms.uStrokeImageSize);
2764
+ let stroke0 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 0, font.uStrokeImageSize);
2765
+ let stroke1 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 1, font.uStrokeImageSize);
2768
2766
 
2769
2767
  // calculate the coverage
2770
2768
  coverageX(stroke0.xy, stroke0.zw, stroke1.xy, input.vTexCoord, pixelScale, &coverage, &weight);
@@ -2773,8 +2771,8 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
2773
2771
 
2774
2772
  // intersect curves in this column
2775
2773
  {
2776
- let colIndex = gridCoord.x + uniforms.uGridOffset.x;
2777
- let colInfo = getTexel(uSamplerCols, uSamplerCols_sampler, colIndex, uniforms.uGridImageSize);
2774
+ let colIndex = gridCoord.x + font.uGridOffset.x;
2775
+ let colInfo = getTexel(uSamplerCols, uSamplerCols_sampler, colIndex, font.uGridImageSize);
2778
2776
  let colStrokeIndex = getInt16(colInfo.xy);
2779
2777
  let colStrokeCount = getInt16(colInfo.zw);
2780
2778
 
@@ -2783,11 +2781,11 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
2783
2781
  break;
2784
2782
  }
2785
2783
 
2786
- let strokeIndices = getTexel(uSamplerColStrokes, uSamplerColStrokes_sampler, colStrokeIndex + iColStroke, uniforms.uCellsImageSize);
2784
+ let strokeIndices = getTexel(uSamplerColStrokes, uSamplerColStrokes_sampler, colStrokeIndex + iColStroke, font.uCellsImageSize);
2787
2785
 
2788
2786
  let strokePos = getInt16(strokeIndices.xy);
2789
- let stroke0 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 0, uniforms.uStrokeImageSize);
2790
- let stroke1 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 1, uniforms.uStrokeImageSize);
2787
+ let stroke0 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 0, font.uStrokeImageSize);
2788
+ let stroke1 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 1, font.uStrokeImageSize);
2791
2789
  coverageY(stroke0.xy, stroke0.zw, stroke1.xy, input.vTexCoord, pixelScale, &coverage, &weight);
2792
2790
  }
2793
2791
  }
@@ -2796,7 +2794,7 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
2796
2794
  let distance = max(weight.x + weight.y, minDistance); // manhattan approx.
2797
2795
  let antialias = abs(dot(coverage, weight) / distance);
2798
2796
  let cover = min(abs(coverage.x), abs(coverage.y));
2799
- var outColor = vec4<f32>(uniforms.uMaterialColor.rgb, 1.0) * uniforms.uMaterialColor.a;
2797
+ var outColor = vec4<f32>(font.uMaterialColor.rgb, 1.0) * font.uMaterialColor.a;
2800
2798
  outColor = outColor * saturate_f32(max(antialias, cover));
2801
2799
  return outColor;
2802
2800
  }
@@ -3786,7 +3784,10 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
3786
3784
  if (!strandsContext.renderer || !strandsContext.baseShader) return;
3787
3785
 
3788
3786
  // Get the next available binding index from the renderer
3789
- let bindingIndex = strandsContext.renderer.getNextBindingIndex(strandsContext.baseShader);
3787
+ let bindingIndex = strandsContext.renderer.getNextBindingIndex({
3788
+ vert: strandsContext.baseShader.vertSrc(),
3789
+ frag: strandsContext.baseShader.fragSrc(),
3790
+ });
3790
3791
 
3791
3792
  for (const {name, typeInfo} of strandsContext.uniforms) {
3792
3793
  if (typeInfo.baseType === 'sampler2D') {
@@ -3955,7 +3956,7 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
3955
3956
  // Check if this is a uniform variable (but not a texture)
3956
3957
  const uniform = generationContext.strandsContext?.uniforms?.find(uniform => uniform.name === node.identifier);
3957
3958
  if (uniform && uniform.typeInfo.baseType !== 'sampler2D') {
3958
- return `uniforms.${node.identifier}`;
3959
+ return `hooks.${node.identifier}`;
3959
3960
  }
3960
3961
 
3961
3962
  return node.identifier;
@@ -4205,16 +4206,27 @@ fn noise(st: vec3<f32>, octaves: i32, ampFalloff: f32) -> f32 {
4205
4206
  }`;
4206
4207
 
4207
4208
  const filterUniforms = `
4208
- struct Uniforms {
4209
- uModelViewMatrix: mat4x4<f32>,
4210
- uProjectionMatrix: mat4x4<f32>,
4209
+ // Group 0: Filter Properties
4210
+ struct FilterUniforms {
4211
4211
  canvasSize: vec2<f32>,
4212
4212
  texelSize: vec2<f32>,
4213
4213
  }
4214
4214
 
4215
- @group(0) @binding(0) var<uniform> uniforms: Uniforms;
4215
+ // Group 1: Model Transform
4216
+ struct ModelUniforms {
4217
+ uModelViewMatrix: mat4x4<f32>,
4218
+ }
4219
+
4220
+ // Group 2: Camera and Projection
4221
+ struct CameraUniforms {
4222
+ uProjectionMatrix: mat4x4<f32>,
4223
+ }
4224
+
4225
+ @group(0) @binding(0) var<uniform> filterParams: FilterUniforms;
4216
4226
  @group(0) @binding(1) var tex0: texture_2d<f32>;
4217
4227
  @group(0) @binding(2) var tex0_sampler: sampler;
4228
+ @group(1) @binding(0) var<uniform> model: ModelUniforms;
4229
+ @group(2) @binding(0) var<uniform> camera: CameraUniforms;
4218
4230
  `;
4219
4231
 
4220
4232
  const baseFilterVertexShader = filterUniforms + `
@@ -4239,7 +4251,7 @@ fn main(input: VertexInput) -> VertexOutput {
4239
4251
  let positionVec4 = vec4<f32>(input.aPosition, 1.0);
4240
4252
 
4241
4253
  // project to 3D space
4242
- output.position = uniforms.uProjectionMatrix * uniforms.uModelViewMatrix * positionVec4;
4254
+ output.position = camera.uProjectionMatrix * model.uModelViewMatrix * positionVec4;
4243
4255
 
4244
4256
  return output;
4245
4257
  }
@@ -4265,8 +4277,8 @@ fn main(input: FragmentInput) -> FragmentOutput {
4265
4277
  var output: FragmentOutput;
4266
4278
  var inputs: FilterInputs;
4267
4279
  inputs.texCoord = input.vTexCoord;
4268
- inputs.canvasSize = uniforms.canvasSize;
4269
- inputs.texelSize = uniforms.texelSize;
4280
+ inputs.canvasSize = filterParams.canvasSize;
4281
+ inputs.texelSize = filterParams.texelSize;
4270
4282
 
4271
4283
  var outColor = HOOK_getColor(inputs, tex0, tex0_sampler);
4272
4284
  outColor = vec4<f32>(outColor.rgb * outColor.a, outColor.a);
@@ -4528,10 +4540,27 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
4528
4540
  constructor(pInst, w, h, isMainCanvas, elt) {
4529
4541
  super(pInst, w, h, isMainCanvas, elt);
4530
4542
 
4531
- this.renderPass = {};
4543
+ // Used to group draws into one big render pass
4544
+ this.activeRenderPass = null;
4545
+ this.activeRenderPassEncoder = null;
4546
+ this.activeShaderOptions = null;
4547
+ this.activeShader = null;
4532
4548
 
4533
4549
  this.samplers = new Map();
4534
4550
 
4551
+ // Some uniforms update every frame, like model matrices and sometimes colors.
4552
+ // The fastest way to handle these is to use mapped memory. We'll batch those
4553
+ // into bigger buffers with dynamic offsets, separate from the usual system
4554
+ // where bind groups have their own little buffers that get cached when they
4555
+ // are unchanged
4556
+ this.uniformBufferAlignment = 256;
4557
+ this.activeUniformBuffers = [];
4558
+ this.currentUniformBuffer = undefined;
4559
+ this.uniformBufferPool = [];
4560
+ this.resettingUniformBuffers = [];
4561
+
4562
+ this.dynamicEntryOffsets = new Uint32Array(64);
4563
+
4535
4564
  // Cache for current frame's canvas texture view
4536
4565
  this.currentCanvasColorTexture = null;
4537
4566
  this.currentCanvasColorTextureView = null;
@@ -4695,6 +4724,68 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
4695
4724
  return this.currentCanvasColorTextureView;
4696
4725
  }
4697
4726
 
4727
+ _beginActiveRenderPass() {
4728
+ if (this.activeRenderPass) return;
4729
+
4730
+ // Use framebuffer texture if active, otherwise use canvas texture
4731
+ const activeFramebuffer = this.activeFramebuffer();
4732
+
4733
+ const colorAttachment = {
4734
+ view: activeFramebuffer
4735
+ ? (activeFramebuffer.aaColorTexture
4736
+ ? activeFramebuffer.aaColorTextureView
4737
+ : activeFramebuffer.colorTextureView)
4738
+ : this._getCanvasColorTextureView(),
4739
+ loadOp: "load",
4740
+ storeOp: "store",
4741
+ // If using multisampled texture, resolve to non-multisampled texture
4742
+ resolveTarget: activeFramebuffer && activeFramebuffer.aaColorTexture
4743
+ ? activeFramebuffer.colorTextureView
4744
+ : undefined,
4745
+ };
4746
+
4747
+ // Use framebuffer depth texture if active, otherwise use canvas depth texture
4748
+ const depthTextureView = activeFramebuffer
4749
+ ? (activeFramebuffer.aaDepthTexture
4750
+ ? activeFramebuffer.aaDepthTextureView
4751
+ : activeFramebuffer.depthTextureView)
4752
+ : this.depthTextureView;
4753
+ const renderPassDescriptor = {
4754
+ colorAttachments: [colorAttachment],
4755
+ depthStencilAttachment: depthTextureView
4756
+ ? {
4757
+ view: depthTextureView,
4758
+ depthLoadOp: "load",
4759
+ depthStoreOp: "store",
4760
+ depthClearValue: 1.0,
4761
+ stencilLoadOp: "load",
4762
+ stencilStoreOp: "store",
4763
+ depthReadOnly: false,
4764
+ stencilReadOnly: false,
4765
+ }
4766
+ : undefined,
4767
+ };
4768
+ const commandEncoder = this.device.createCommandEncoder();
4769
+ const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
4770
+ this.activeRenderPassEncoder = commandEncoder;
4771
+ this.activeRenderPass = passEncoder;
4772
+ }
4773
+
4774
+ _finishActiveRenderPass() {
4775
+ if (!this.activeRenderPass) return;
4776
+
4777
+ const commandEncoder = this.activeRenderPassEncoder;
4778
+ const passEncoder = this.activeRenderPass;
4779
+ passEncoder.end();
4780
+
4781
+ // Store the command encoder for later submission
4782
+ this._pendingCommandEncoders.push(commandEncoder.finish());
4783
+ this.activeRenderPassEncoder = null;
4784
+ this.activeRenderPass = null;
4785
+ this.activeShader = null;
4786
+ this.activeShaderOptions = null;
4787
+ }
4788
+
4698
4789
  clear(...args) {
4699
4790
  const _r = args[0] || 0;
4700
4791
  const _g = args[1] || 0;
@@ -4706,6 +4797,8 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
4706
4797
  this._frameState = FRAME_STATE.UNPROMOTED;
4707
4798
  }
4708
4799
 
4800
+ this._finishActiveRenderPass();
4801
+
4709
4802
  const commandEncoder = this.device.createCommandEncoder();
4710
4803
 
4711
4804
  // Use framebuffer texture if active, otherwise use canvas texture
@@ -4760,6 +4853,7 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
4760
4853
  * occlude anything subsequently drawn.
4761
4854
  */
4762
4855
  clearDepth(depth = 1) {
4856
+ this._finishActiveRenderPass();
4763
4857
  const commandEncoder = this.device.createCommandEncoder();
4764
4858
 
4765
4859
  // Use framebuffer texture if active, otherwise use canvas texture
@@ -4850,7 +4944,6 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
4850
4944
  const loc = attr.location;
4851
4945
  if (!this.registerEnabled.has(loc)) {
4852
4946
  // TODO
4853
- // this.renderPass.setVertexBuffer(loc, buffer);
4854
4947
  this.registerEnabled.add(loc);
4855
4948
  }
4856
4949
  }
@@ -4933,6 +5026,14 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
4933
5026
  }
4934
5027
  }
4935
5028
 
5029
+ _shaderOptionsDifferent(newOptions) {
5030
+ if (!this.activeShaderOptions) return true;
5031
+ for (const key in this.activeShaderOptions) {
5032
+ if (this.activeShaderOptions[key] !== newOptions[key]) return true;
5033
+ }
5034
+ return false;
5035
+ }
5036
+
4936
5037
  _initShader(shader) {
4937
5038
  const device = this.device;
4938
5039
 
@@ -4987,39 +5088,39 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
4987
5088
  }
4988
5089
 
4989
5090
  _finalizeShader(shader) {
4990
- const rawSize = Math.max(
4991
- 0,
4992
- ...Object.values(shader.uniforms).filter(u => !u.isSampler).map(u => u.offsetEnd)
4993
- );
4994
- const alignedSize = Math.ceil(rawSize / 16) * 16;
4995
- shader._uniformData = new Float32Array(alignedSize / 4);
4996
- shader._uniformDataView = new DataView(shader._uniformData.buffer);
4997
-
4998
- // Create pools for uniform buffers (both GPU buffers and data arrays.) This
4999
- // is so that we can queue up multiple things to be able to be drawn and have
5000
- // the GPU go through them as fast as possible. If we're overwriting the same
5001
- // data again and again, we would have to wait for the GPU after each primitive
5002
- // that we draw.
5003
- shader._uniformBufferPool = [];
5004
- shader._uniformBuffersInUse = [];
5005
- shader._uniformBufferSize = alignedSize;
5006
-
5007
- // Create the first buffer for the pool
5008
- const firstGPUBuffer = this.device.createBuffer({
5009
- size: alignedSize,
5010
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
5011
- });
5012
- const firstData = new Float32Array(alignedSize / 4);
5013
- const firstDataView = new DataView(firstData.buffer);
5014
-
5015
- shader._uniformBufferPool.push({
5016
- buffer: firstGPUBuffer,
5017
- data: firstData,
5018
- dataView: firstDataView
5019
- });
5020
-
5021
- // Keep backward compatibility reference
5022
- shader._uniformBuffer = firstGPUBuffer;
5091
+ // Per-group buffer pools. We will pull from these when we draw multiple
5092
+ // times using the shader in a render pass. These are per group instead of
5093
+ // global so that we can reuse the last used buffer when uniform values
5094
+ // don't change.
5095
+ shader._uniformBufferGroups = [];
5096
+ shader.buffersDirty = new Set();
5097
+
5098
+ for (const group of shader._uniformGroups) {
5099
+ // Calculate the size needed for this group's uniforms
5100
+ const groupUniforms = Object.values(group.uniforms);
5101
+ const rawSize = Math.max(
5102
+ 0,
5103
+ ...groupUniforms.map(u => u.offsetEnd)
5104
+ );
5105
+ const alignedSize = Math.ceil(rawSize / 16) * 16;
5106
+
5107
+ shader._uniformBufferGroups.push({
5108
+ group: group.group,
5109
+ binding: group.binding,
5110
+ cacheKey: group.group * 1000 + group.binding,
5111
+ varName: group.varName,
5112
+ structType: group.structType,
5113
+ uniforms: groupUniforms,
5114
+ size: alignedSize,
5115
+
5116
+ bufferPool: [],
5117
+ nextBufferPool: [],
5118
+
5119
+ dynamic: groupUniforms.some(u => u.name.startsWith('uModel')),
5120
+ buffersInUse: new Set(),
5121
+ currentBuffer: null, // For caching
5122
+ });
5123
+ }
5023
5124
 
5024
5125
  // Register this shader in our registry for pool cleanup
5025
5126
  this._shadersWithPools.push(shader);
@@ -5027,12 +5128,22 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
5027
5128
  const bindGroupLayouts = new Map(); // group index -> bindGroupLayout
5028
5129
  const groupEntries = new Map(); // group index -> array of entries
5029
5130
 
5030
- // We're enforcing that every shader have a single uniform struct in binding 0
5031
- groupEntries.set(0, [{
5032
- binding: 0,
5033
- visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
5034
- buffer: { type: 'uniform' },
5035
- }]);
5131
+ // Add all uniform group bindings to group 0
5132
+ const structEntries = new Map();
5133
+ for (const bufferGroup of shader._uniformBufferGroups) {
5134
+ const entries = structEntries.get(bufferGroup.group) || [];
5135
+ entries.push({
5136
+ bufferGroup,
5137
+ binding: bufferGroup.binding,
5138
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
5139
+ buffer: { type: 'uniform', hasDynamicOffset: bufferGroup.dynamic },
5140
+ });
5141
+ structEntries.set(bufferGroup.group, entries);
5142
+ }
5143
+ for (const [group, entries] of structEntries.entries()) {
5144
+ entries.sort((a, b) => a.binding - b.binding);
5145
+ groupEntries.set(group, entries);
5146
+ }
5036
5147
 
5037
5148
  // Add the variable amount of samplers and texture bindings that can come after
5038
5149
  for (const sampler of shader.samplers) {
@@ -5054,17 +5165,25 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
5054
5165
  uniform: sampler,
5055
5166
  });
5056
5167
 
5168
+ entries.sort((a, b) => a.binding - b.binding);
5057
5169
  groupEntries.set(group, entries);
5058
5170
  }
5059
5171
 
5060
5172
  // Create layouts and bind groups
5173
+ const groupEntriesArr = [];
5061
5174
  for (const [group, entries] of groupEntries) {
5062
5175
  const layout = this.device.createBindGroupLayout({ entries });
5063
5176
  bindGroupLayouts.set(group, layout);
5177
+ groupEntriesArr.push([group, entries]);
5064
5178
  }
5065
5179
 
5066
- shader._groupEntries = groupEntries;
5180
+ shader._groupEntries = groupEntriesArr;
5067
5181
  shader._bindGroupLayouts = [...bindGroupLayouts.values()];
5182
+ // Reuse bind groups if they don't change
5183
+ shader._cachedBindGroup = {};
5184
+ // Remember which dynamic buffer we last used, so that we can
5185
+ // possibly cache bind groups if unchanged
5186
+ shader._lastDynamicBuffer = {};
5068
5187
  shader._pipelineLayout = this.device.createPipelineLayout({
5069
5188
  bindGroupLayouts: shader._bindGroupLayouts,
5070
5189
  });
@@ -5253,22 +5372,25 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
5253
5372
  }
5254
5373
 
5255
5374
  _getVertexBuffers(shader) {
5256
- const buffers = [];
5375
+ if (!shader._vertexBuffers) {
5376
+ const buffers = [];
5257
5377
 
5258
- for (const attrName in shader.attributes) {
5259
- const attr = shader.attributes[attrName];
5260
- if (!attr || attr.location === -1) continue;
5378
+ for (const attrName in shader.attributes) {
5379
+ const attr = shader.attributes[attrName];
5380
+ if (!attr || attr.location === -1) continue;
5261
5381
 
5262
- // Get the vertex buffer info associated with this attribute
5263
- const renderBuffer =
5264
- this.buffers[shader.shaderType].find(buf => buf.attr === attrName) ||
5265
- this.buffers.user.find(buf => buf.attr === attrName);
5266
- if (!renderBuffer) continue;
5382
+ // Get the vertex buffer info associated with this attribute
5383
+ const renderBuffer =
5384
+ this.buffers[shader.shaderType].find(buf => buf.attr === attrName) ||
5385
+ this.buffers.user.find(buf => buf.attr === attrName);
5386
+ if (!renderBuffer) continue;
5267
5387
 
5268
- buffers.push(renderBuffer);
5388
+ buffers.push(renderBuffer);
5389
+ }
5390
+ shader._vertexBuffers = buffers;
5269
5391
  }
5270
5392
 
5271
- return buffers;
5393
+ return shader._vertexBuffers;
5272
5394
  }
5273
5395
 
5274
5396
  _getFormatFromSize(size) {
@@ -5310,6 +5432,7 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
5310
5432
  }
5311
5433
 
5312
5434
  _resetBuffersBeforeDraw() {
5435
+ this._finishActiveRenderPass();
5313
5436
  // Set state to PENDING - we'll decide on first draw
5314
5437
  this._frameState = FRAME_STATE.PENDING;
5315
5438
 
@@ -5563,50 +5686,106 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
5563
5686
  // Uniform buffer pool management
5564
5687
  //////////////////////////////////////////////
5565
5688
 
5566
- _getUniformBufferFromPool(shader) {
5689
+ _getUniformBufferFromPool(bufferGroup) {
5567
5690
  // Try to get a buffer from the pool
5568
- if (shader._uniformBufferPool.length > 0) {
5569
- const bufferInfo = shader._uniformBufferPool.pop();
5570
- shader._uniformBuffersInUse.push(bufferInfo);
5691
+ if (bufferGroup.bufferPool.length > 0) {
5692
+ const bufferInfo = bufferGroup.bufferPool.pop();
5693
+ bufferGroup.buffersInUse.add(bufferInfo);
5571
5694
  return bufferInfo;
5572
5695
  }
5573
5696
 
5574
5697
  // No buffers available, create a new one
5575
5698
  const newBuffer = this.device.createBuffer({
5576
- size: shader._uniformBufferSize,
5699
+ size: bufferGroup.size,
5577
5700
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
5578
5701
  });
5579
- const newData = new Float32Array(shader._uniformBufferSize / 4);
5702
+ const newData = new Float32Array(bufferGroup.size / 4);
5580
5703
  const newDataView = new DataView(newData.buffer);
5581
-
5582
5704
  const bufferInfo = {
5583
5705
  buffer: newBuffer,
5584
5706
  data: newData,
5585
5707
  dataView: newDataView
5586
5708
  };
5587
5709
 
5588
- shader._uniformBuffersInUse.push(bufferInfo);
5710
+ bufferGroup.buffersInUse.add(bufferInfo);
5589
5711
  return bufferInfo;
5590
5712
  }
5591
5713
 
5714
+ _getDynamicUniformBufferFromPool(bufferGroup) {
5715
+ //
5716
+ let buffer;
5717
+ if (
5718
+ this.currentUniformBuffer &&
5719
+ this.currentUniformBuffer.offset + bufferGroup.size < this.currentUniformBuffer.size
5720
+ ) {
5721
+ // We can fit this next block of uniforms into the current active memory chunk
5722
+ buffer = this.currentUniformBuffer;
5723
+ } else if (this.uniformBufferPool.length > 0) {
5724
+ buffer = this.uniformBufferPool.pop();
5725
+ this.activeUniformBuffers.push(buffer);
5726
+ } else {
5727
+ // Kinda arbitrary. Each dynamic offset has to be in groups of 256, but then
5728
+ // we can choose how many things we want to be able to fit into a block.
5729
+ // There's some overhead to each block so if we're drawing a lot of stuff,
5730
+ // bigger is better. But it's also a lot of wasted memory if we AREN'T drawing
5731
+ // a lot of stuff. So.... right now it's 40. Feel free to update this if
5732
+ // a better balance can be achieved.
5733
+ const size = 256 * 40;
5734
+ buffer = {
5735
+ dynamic: true,
5736
+ lastOffset: 0,
5737
+ offset: 0,
5738
+ size,
5739
+ buffer: this.device.createBuffer({
5740
+ size,
5741
+ usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
5742
+ mappedAtCreation: true,
5743
+ }),
5744
+ uniformBuffer: this.device.createBuffer({
5745
+ size,
5746
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
5747
+ }),
5748
+ };
5749
+
5750
+ buffer.data = new Float32Array(buffer.buffer.getMappedRange());
5751
+ buffer.dataView = new DataView(buffer.data.buffer);
5752
+
5753
+ this.activeUniformBuffers.push(buffer);
5754
+ }
5755
+
5756
+ this.currentUniformBuffer = buffer;
5757
+
5758
+ return buffer;
5759
+ }
5760
+
5592
5761
  _returnUniformBuffersToPool() {
5593
5762
  // Return all used buffers back to their pools for all registered shaders
5594
5763
  for (const shader of this._shadersWithPools) {
5595
- if (shader._uniformBuffersInUse && shader._uniformBuffersInUse.length > 0) {
5596
- this._returnShaderBuffersToPool(shader);
5597
- }
5764
+ this._returnShaderBuffersToPool(shader);
5598
5765
  }
5599
5766
  }
5600
5767
 
5601
5768
  _returnShaderBuffersToPool(shader) {
5602
- // Move all buffers from inUse back to pool
5603
- while (shader._uniformBuffersInUse.length > 0) {
5604
- const bufferInfo = shader._uniformBuffersInUse.pop();
5605
- shader._uniformBufferPool.push(bufferInfo);
5769
+ if (shader._uniformBufferGroups) {
5770
+ for (const bufferGroup of shader._uniformBufferGroups) {
5771
+ while (bufferGroup.nextBufferPool.length > 0) {
5772
+ bufferGroup.bufferPool.push(bufferGroup.nextBufferPool.pop());
5773
+ }
5774
+ for (const bufferInfo of bufferGroup.buffersInUse.keys()) {
5775
+ if (bufferInfo !== bufferGroup.currentBuffer) {
5776
+ bufferGroup.nextBufferPool.push(bufferInfo);
5777
+ }
5778
+ }
5779
+ bufferGroup.buffersInUse.clear();
5780
+ if (bufferGroup.currentBuffer) {
5781
+ bufferGroup.buffersInUse.add(bufferGroup.currentBuffer);
5782
+ }
5783
+ }
5606
5784
  }
5607
5785
  }
5608
5786
 
5609
5787
  flushDraw() {
5788
+ this._finishActiveRenderPass();
5610
5789
  // Only submit if we actually had any draws
5611
5790
  if (this._hasPendingDraws) {
5612
5791
  // Create a copy of pending command encoders
@@ -5614,9 +5793,41 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
5614
5793
  this._pendingCommandEncoders = [];
5615
5794
  this._hasPendingDraws = false;
5616
5795
 
5796
+ if (this.activeUniformBuffers.length > 0) {
5797
+ const encoder = this.device.createCommandEncoder();
5798
+ for (const bufferInfo of this.activeUniformBuffers) {
5799
+ bufferInfo.buffer.unmap();
5800
+ encoder.copyBufferToBuffer(
5801
+ bufferInfo.buffer,
5802
+ bufferInfo.uniformBuffer,
5803
+ );
5804
+ }
5805
+ commandsToSubmit.unshift(encoder.finish());
5806
+ }
5807
+
5617
5808
  // Submit the commands
5618
5809
  this.queue.submit(commandsToSubmit);
5619
5810
 
5811
+ for (const buf of this.activeUniformBuffers) {
5812
+ // buf.buffer = this.device.createBuffer({
5813
+ // size: buf.size,
5814
+ // usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
5815
+ // mappedAtCreation: true,
5816
+ // });
5817
+ buf.offset = 0;
5818
+ buf.lastOffset = 0;
5819
+ // this.resettingUniformBuffers.push(
5820
+ buf.buffer.mapAsync(GPUMapMode.WRITE).then(() => {
5821
+ buf.data = new Float32Array(buf.buffer.getMappedRange());
5822
+ buf.dataView = new DataView(buf.data.buffer);
5823
+ this.uniformBufferPool.push(buf);
5824
+ return buf;
5825
+ });
5826
+ // )
5827
+ }
5828
+ this.activeUniformBuffers = [];
5829
+ this.currentUniformBuffer = undefined;
5830
+
5620
5831
  // Execute post-submit callbacks after GPU work completes
5621
5832
  if (this._postSubmitCallbacks.length > 0) {
5622
5833
  const callbacks = this._postSubmitCallbacks;
@@ -5690,15 +5901,21 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
5690
5901
  this._markGeometryBuffersForReturn(geometry);
5691
5902
  }
5692
5903
 
5904
+ // this.uniformBufferPool.push(...(await Promise.all(this.resettingUniformBuffers)));
5905
+ this.resettingUniformBuffers = [];
5906
+
5693
5907
  // Return all vertex buffers to their pools
5694
5908
  this._returnVertexBuffersToPool();
5695
5909
 
5696
5910
  // Destroy all retired buffers
5697
- for (const buffer of this._retiredBuffers) {
5698
- if (buffer && buffer.destroy) {
5699
- buffer.destroy();
5911
+ const retired = this._retiredBuffers;
5912
+ this._postSubmitCallbacks.push(() => {
5913
+ for (const buffer of retired) {
5914
+ if (buffer && buffer.destroy) {
5915
+ buffer.destroy();
5916
+ }
5700
5917
  }
5701
- }
5918
+ });
5702
5919
  this._retiredBuffers = [];
5703
5920
 
5704
5921
  if (this._frameState === FRAME_STATE.PROMOTED) {
@@ -5726,50 +5943,15 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
5726
5943
  this._promoteToFramebufferWithoutCopy();
5727
5944
  }
5728
5945
 
5729
- const commandEncoder = this.device.createCommandEncoder();
5730
-
5731
- // Use framebuffer texture if active, otherwise use canvas texture
5732
- const activeFramebuffer = this.activeFramebuffer();
5733
-
5734
- const colorAttachment = {
5735
- view: activeFramebuffer
5736
- ? (activeFramebuffer.aaColorTexture
5737
- ? activeFramebuffer.aaColorTextureView
5738
- : activeFramebuffer.colorTextureView)
5739
- : this._getCanvasColorTextureView(),
5740
- loadOp: "load",
5741
- storeOp: "store",
5742
- // If using multisampled texture, resolve to non-multisampled texture
5743
- resolveTarget: activeFramebuffer && activeFramebuffer.aaColorTexture
5744
- ? activeFramebuffer.colorTextureView
5745
- : undefined,
5746
- };
5747
-
5748
- // Use framebuffer depth texture if active, otherwise use canvas depth texture
5749
- const depthTextureView = activeFramebuffer
5750
- ? (activeFramebuffer.aaDepthTexture
5751
- ? activeFramebuffer.aaDepthTextureView
5752
- : activeFramebuffer.depthTextureView)
5753
- : this.depthTextureView;
5754
- const renderPassDescriptor = {
5755
- colorAttachments: [colorAttachment],
5756
- depthStencilAttachment: depthTextureView
5757
- ? {
5758
- view: depthTextureView,
5759
- depthLoadOp: "load",
5760
- depthStoreOp: "store",
5761
- depthClearValue: 1.0,
5762
- stencilLoadOp: "load",
5763
- stencilStoreOp: "store",
5764
- depthReadOnly: false,
5765
- stencilReadOnly: false,
5766
- }
5767
- : undefined,
5768
- };
5769
-
5770
- const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
5946
+ this._beginActiveRenderPass();
5947
+ const passEncoder = this.activeRenderPass;
5771
5948
  const currentShader = this._curShader;
5772
- passEncoder.setPipeline(currentShader.getPipeline(this._shaderOptions({ mode })));
5949
+ const shaderOptions = this._shaderOptions({ mode });
5950
+ if (this.activeShader !== currentShader || this._shaderOptionsDifferent(shaderOptions)) {
5951
+ passEncoder.setPipeline(currentShader.getPipeline(shaderOptions));
5952
+ }
5953
+ this.activeShader = currentShader;
5954
+ this.activeShaderOptions = shaderOptions;
5773
5955
 
5774
5956
  // Set stencil reference value for clipping
5775
5957
  const drawTarget = this.drawTarget();
@@ -5783,52 +5965,125 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
5783
5965
  passEncoder.setStencilReference(1);
5784
5966
  }
5785
5967
  // Bind vertex buffers
5786
- for (const buffer of this._getVertexBuffers(currentShader)) {
5968
+ for (const buffer of currentShader._vertexBuffers || this._getVertexBuffers(currentShader)) {
5787
5969
  const location = currentShader.attributes[buffer.attr].location;
5788
5970
  const gpuBuffer = buffers[buffer.dst];
5789
5971
  passEncoder.setVertexBuffer(location, gpuBuffer, 0);
5790
5972
  }
5791
- // Bind uniforms - get a buffer from the pool
5792
- const uniformBufferInfo = this._getUniformBufferFromPool(currentShader);
5793
- this._packUniforms(currentShader, uniformBufferInfo);
5794
- this.device.queue.writeBuffer(
5795
- uniformBufferInfo.buffer,
5796
- 0,
5797
- uniformBufferInfo.data.buffer,
5798
- uniformBufferInfo.data.byteOffset,
5799
- uniformBufferInfo.data.byteLength
5800
- );
5801
5973
 
5802
- // Bind sampler/texture uniforms
5803
- for (const [group, entries] of currentShader._groupEntries) {
5804
- const bgEntries = entries.map(entry => {
5805
- if (group === 0 && entry.binding === 0) {
5806
- return {
5807
- binding: 0,
5808
- resource: { buffer: uniformBufferInfo.buffer },
5809
- };
5974
+ for (const bufferGroup of currentShader._uniformBufferGroups) {
5975
+ if (bufferGroup.dynamic) {
5976
+ // Bind uniforms into a part of a big dynamic memory block because
5977
+ // the group changes often
5978
+ const uniformBufferInfo = this._getDynamicUniformBufferFromPool(bufferGroup);
5979
+ if (currentShader._lastDynamicBuffer[bufferGroup.cacheKey] !== uniformBufferInfo) {
5980
+ currentShader._cachedBindGroup[bufferGroup.group] = undefined;
5981
+ currentShader._lastDynamicBuffer[bufferGroup.cacheKey] = uniformBufferInfo;
5810
5982
  }
5983
+ this._packUniformGroup(currentShader, bufferGroup.uniforms, uniformBufferInfo);
5984
+ uniformBufferInfo.lastOffset = uniformBufferInfo.offset;
5985
+ uniformBufferInfo.offset += Math.ceil(bufferGroup.size / this.uniformBufferAlignment) * this.uniformBufferAlignment;
5811
5986
 
5812
- if (!entry.uniform.isSampler) {
5813
- throw new Error(
5814
- 'All non-texture/sampler uniforms should be in the uniform struct!'
5987
+ // Make a shallow copy so that we keep track of the last offset for this uniform
5988
+ bufferGroup.currentDynamicBuffer = uniformBufferInfo;
5989
+ bufferGroup.lastOffset = uniformBufferInfo.lastOffset;
5990
+ } else {
5991
+ // Bind uniforms to a binding-specific buffer, which may be cached for performance
5992
+ let bufferInfo;
5993
+ const dataChanged = this._hasGroupDataChanged(currentShader, bufferGroup);
5994
+
5995
+ if (!dataChanged && bufferGroup.currentBuffer) {
5996
+ // Reuse the cached buffer - no need to pack or write
5997
+ bufferInfo = bufferGroup.currentBuffer;
5998
+ bufferGroup.buffersInUse.add(bufferInfo);
5999
+ } else {
6000
+ // Data changed - get a new buffer and write to it
6001
+ bufferInfo = this._getUniformBufferFromPool(bufferGroup);
6002
+ this._packUniformGroup(currentShader, bufferGroup.uniforms, bufferInfo);
6003
+ this.device.queue.writeBuffer(
6004
+ bufferInfo.buffer,
6005
+ 0,
6006
+ bufferInfo.data.buffer,
6007
+ bufferInfo.data.byteOffset,
6008
+ bufferInfo.data.byteLength
5815
6009
  );
6010
+
6011
+ currentShader.buffersDirty.delete(bufferGroup.group * 1000 + bufferGroup.binding);
6012
+ currentShader._cachedBindGroup[bufferGroup.group] = undefined;
6013
+
6014
+ // Cache this buffer and data for next frame
6015
+ bufferGroup.currentBuffer = bufferInfo;
5816
6016
  }
6017
+ }
6018
+ }
6019
+ for (const sampler of currentShader.samplers) {
6020
+ const key = sampler.group * 1000 + sampler.binding;
6021
+ if (currentShader.buffersDirty.has(key)) {
6022
+ currentShader._cachedBindGroup[sampler.group] = undefined;
6023
+ currentShader.buffersDirty.delete(key);
6024
+ }
6025
+ }
5817
6026
 
5818
- return {
5819
- binding: entry.binding,
5820
- resource: entry.uniform.type === 'sampler'
5821
- ? (entry.uniform.textureSource.texture || this._getEmptyTexture()).getSampler()
5822
- : (entry.uniform.texture || this._getEmptyTexture()).textureHandle.view,
5823
- };
5824
- });
6027
+ // Bind sampler/texture uniforms and uniform buffers
6028
+ for (const iter of currentShader._groupEntries) {
6029
+ const group = iter[0];
6030
+ const entries = iter[1];
6031
+ let dynamicOffsetIdx = 0;
6032
+ const bgEntries = [];
6033
+ let bindGroup = currentShader._cachedBindGroup[group];
6034
+ for (const entry of entries) {
6035
+ const bufferGroup = entry.bufferGroup;
6036
+ // Check if this is a uniform buffer binding
6037
+ const uniformBufferInfo =
6038
+ bufferGroup?.currentBuffer || bufferGroup?.currentDynamicBuffer;
6039
+ if (uniformBufferInfo) {
6040
+ if (bufferGroup.dynamic) {
6041
+ this.dynamicEntryOffsets[dynamicOffsetIdx++] = bufferGroup.lastOffset;
6042
+ }
6043
+ if (!bindGroup) {
6044
+ bgEntries.push({
6045
+ binding: entry.binding,
6046
+ resource: bufferGroup.dynamic
6047
+ ? {
6048
+ buffer: uniformBufferInfo.uniformBuffer,
6049
+ offset: 0,
6050
+ size: Math.ceil(bufferGroup.size / this.uniformBufferAlignment) * this.uniformBufferAlignment,
6051
+ }
6052
+ : { buffer: uniformBufferInfo.buffer },
6053
+ });
6054
+ }
6055
+ } else if (!bindGroup) {
6056
+ bgEntries.push({
6057
+ binding: entry.binding,
6058
+ resource: entry.uniform.type === 'sampler'
6059
+ ? (entry.uniform.textureSource.texture || this._getEmptyTexture()).getSampler()
6060
+ : (entry.uniform.texture || this._getEmptyTexture()).textureHandle.view,
6061
+ });
6062
+ }
6063
+ }
5825
6064
 
5826
6065
  const layout = currentShader._bindGroupLayouts[group];
5827
- const bindGroup = this.device.createBindGroup({
5828
- layout,
5829
- entries: bgEntries,
5830
- });
5831
- passEncoder.setBindGroup(group, bindGroup);
6066
+ if (!bindGroup) {
6067
+ bindGroup = this.device.createBindGroup({
6068
+ layout,
6069
+ entries: bgEntries,
6070
+ });
6071
+ }
6072
+ currentShader._cachedBindGroup[group] = bindGroup;
6073
+ if (dynamicOffsetIdx === 0) {
6074
+ passEncoder.setBindGroup(
6075
+ group,
6076
+ bindGroup,
6077
+ );
6078
+ } else {
6079
+ passEncoder.setBindGroup(
6080
+ group,
6081
+ bindGroup,
6082
+ this.dynamicEntryOffsets,
6083
+ 0,
6084
+ dynamicOffsetIdx
6085
+ );
6086
+ }
5832
6087
  }
5833
6088
 
5834
6089
  if (currentShader.shaderType === "fill") {
@@ -5853,11 +6108,6 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
5853
6108
  passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0);
5854
6109
  }
5855
6110
 
5856
- passEncoder.end();
5857
-
5858
- // Store the command encoder for later submission
5859
- this._pendingCommandEncoders.push(commandEncoder.finish());
5860
-
5861
6111
  // Mark that we have pending draws that need submission
5862
6112
  this._hasPendingDraws = true;
5863
6113
  }
@@ -5866,46 +6116,62 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
5866
6116
  // SHADER
5867
6117
  //////////////////////////////////////////////
5868
6118
 
5869
- _packUniforms(shader, bufferInfo) {
6119
+ _packUniformGroup(shader, groupUniforms, bufferInfo) {
6120
+ // Pack a single group's uniforms into a buffer
5870
6121
  const data = bufferInfo.data;
5871
6122
  const dataView = bufferInfo.dataView;
5872
6123
 
5873
- for (const name in shader.uniforms) {
5874
- const uniform = shader.uniforms[name];
5875
- if (uniform.isSampler) continue;
6124
+ const offset = bufferInfo.offset || 0;
6125
+ for (const uniform of groupUniforms) {
6126
+ const fullUniform = shader.uniforms[uniform.name];
6127
+ if (!fullUniform || fullUniform.isSampler) continue;
6128
+ const uniformData = fullUniform._mappedData;
5876
6129
 
5877
- if (uniform.baseType === 'u32') {
5878
- if (uniform.size === 4) {
5879
- // Single u32
5880
- dataView.setUint32(uniform.offset, uniform._cachedData, true);
6130
+ if (fullUniform.baseType === 'u32') {
6131
+ if (fullUniform.size === 4) {
6132
+ dataView.setUint32(offset + fullUniform.offset, uniformData, true);
5881
6133
  } else {
5882
- // Vector of u32s
5883
- const uniformData = uniform._cachedData;
5884
6134
  for (let i = 0; i < uniformData.length; i++) {
5885
- dataView.setUint32(uniform.offset + i * 4, uniformData[i], true);
6135
+ dataView.setUint32(offset + fullUniform.offset + i * 4, uniformData[i], true);
5886
6136
  }
5887
6137
  }
5888
- } else if (uniform.baseType === 'i32') {
5889
- if (uniform.size === 4) {
5890
- // Single i32
5891
- dataView.setInt32(uniform.offset, uniform._cachedData, true);
6138
+ } else if (fullUniform.baseType === 'i32') {
6139
+ if (fullUniform.size === 4) {
6140
+ dataView.setInt32(offset + fullUniform.offset, uniformData, true);
5892
6141
  } else {
5893
- // Vector of i32s
5894
- const uniformData = uniform._cachedData;
5895
6142
  for (let i = 0; i < uniformData.length; i++) {
5896
- dataView.setInt32(uniform.offset + i * 4, uniformData[i], true);
6143
+ dataView.setInt32(offset + fullUniform.offset + i * 4, uniformData[i], true);
5897
6144
  }
5898
6145
  }
5899
- } else if (uniform.size === 4) {
5900
- // Single float value
5901
- data.set([uniform._cachedData], uniform.offset / 4);
5902
- } else if (uniform._cachedData !== undefined) {
5903
- // Float array (including vec2<f32>, vec3<f32>, vec4<f32>, mat4x4<f32>)
5904
- data.set(uniform._cachedData, uniform.offset / 4);
6146
+ } else if (fullUniform.packInPlace) {
6147
+ // In-place packing for mat3: write directly to buffer with padding
6148
+ const baseOffset = (offset + fullUniform.offset) / 4;
6149
+ // Column 0
6150
+ data[baseOffset + 0] = uniformData[0];
6151
+ data[baseOffset + 1] = uniformData[1];
6152
+ data[baseOffset + 2] = uniformData[2];
6153
+ // Column 1
6154
+ data[baseOffset + 4] = uniformData[3];
6155
+ data[baseOffset + 5] = uniformData[4];
6156
+ data[baseOffset + 6] = uniformData[5];
6157
+ // Column 2
6158
+ data[baseOffset + 8] = uniformData[6];
6159
+ data[baseOffset + 9] = uniformData[7];
6160
+ data[baseOffset + 10] = uniformData[8];
6161
+ } else if (fullUniform.size === 4) {
6162
+ data.set([uniformData], (offset + fullUniform.offset) / 4);
6163
+ } else if (uniformData !== undefined) {
6164
+ data.set(uniformData, (offset + fullUniform.offset) / 4);
5905
6165
  }
5906
6166
  }
5907
6167
  }
5908
6168
 
6169
+ _hasGroupDataChanged(shader, bufferGroup) {
6170
+ // First time
6171
+ if (!bufferGroup.currentBuffer) return true;
6172
+ return shader.buffersDirty.has(bufferGroup.group * 1000 + bufferGroup.binding);
6173
+ }
6174
+
5909
6175
  _parseStruct(shaderSource, structName) {
5910
6176
  const structMatch = shaderSource.match(
5911
6177
  new RegExp(`struct\\s+${structName}\\s*\\{([^\\}]+)\\}`)
@@ -5949,17 +6215,16 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
5949
6215
  const align = dim === 2 ? 8 : 16;
5950
6216
  // Each column must be aligned
5951
6217
  const size = Math.ceil(dim * 4 / align) * align * dim;
6218
+ // For mat3, use in-place packing to avoid array allocation
5952
6219
  const pack = dim === 3
5953
6220
  ? (data) => [
5954
6221
  ...data.slice(0, 3),
5955
- 0,
5956
6222
  ...data.slice(3, 6),
5957
- 0,
5958
6223
  ...data.slice(6, 9),
5959
- 0
5960
6224
  ]
5961
6225
  : undefined;
5962
- return { align, size, pack, items: dim * dim, baseType: 'f32' };
6226
+ const packInPlace = dim === 3;
6227
+ return { align, size, pack, packInPlace, items: dim * dim, baseType: 'f32' };
5963
6228
  }
5964
6229
  if (/^array<.+>$/.test(type)) {
5965
6230
  const [, subtype, rawLength] = type.match(/^array<(.+),\s*(\d+)>/);
@@ -5996,7 +6261,7 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
5996
6261
 
5997
6262
  while ((match = elementRegex.exec(structBody)) !== null) {
5998
6263
  const [_, location, name, type] = match;
5999
- const { size, align, pack, baseType } = baseAlignAndSize(type);
6264
+ const { size, align, pack, packInPlace, baseType } = baseAlignAndSize(type);
6000
6265
  offset = Math.ceil(offset / align) * align;
6001
6266
  const offsetEnd = offset + size;
6002
6267
  elements[name] = {
@@ -6008,6 +6273,7 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
6008
6273
  offset,
6009
6274
  offsetEnd,
6010
6275
  pack,
6276
+ packInPlace,
6011
6277
  baseType
6012
6278
  };
6013
6279
  index++;
@@ -6033,22 +6299,64 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
6033
6299
  }
6034
6300
 
6035
6301
  getUniformMetadata(shader) {
6036
- // Currently, for ease of parsing, we enforce that the first bind group is a
6037
- // struct, which contains all non-sampler uniforms. Then, any subsequent
6038
- // groups contain samplers.
6039
-
6040
- // Extract the struct name from the uniform variable declaration
6041
- const uniformVarRegex = /@group\(0\)\s+@binding\(0\)\s+var<uniform>\s+(\w+)\s*:\s*(\w+);/;
6042
- const uniformVarMatch = uniformVarRegex.exec(shader.vertSrc());
6043
- if (!uniformVarMatch) {
6044
- throw new Error('Expected a uniform struct bound to @group(0) @binding(0)');
6045
- }
6046
- const structType = uniformVarMatch[2];
6047
- const uniforms = this._parseStruct(shader.vertSrc(), structType);
6302
+ // Parse all uniform struct bindings in group 0.
6303
+ // TODO: support non-sampler uniforms being in other groups
6304
+
6305
+ // Each binding represents a logical group of uniforms, since they get
6306
+ // updated or cached all at once.
6307
+
6308
+ const uniformGroups = [];
6309
+ const uniformVarRegex = /@group\((\d+)\)\s+@binding\((\d+)\)\s+var<uniform>\s+(\w+)\s*:\s*(\w+);/g;
6310
+
6311
+ let match;
6312
+ while ((match = uniformVarRegex.exec(shader.vertSrc())) !== null) {
6313
+ const [_, groupNum, binding, varName, structType] = match;
6314
+ const bindingIndex = parseInt(binding);
6315
+ const uniforms = this._parseStruct(shader.vertSrc(), structType);
6316
+
6317
+ uniformGroups.push({
6318
+ group: parseInt(groupNum),
6319
+ binding: bindingIndex,
6320
+ varName,
6321
+ structType,
6322
+ uniforms
6323
+ });
6324
+ }
6325
+
6326
+ if (uniformGroups.length === 0) {
6327
+ throw new Error('Expected at least one uniform struct bound to @group(0)');
6328
+ }
6329
+
6330
+ // While we're also keeping track of the groups, the API we expose
6331
+ // to users of p5 is just a flat list of uniforms (which can be the
6332
+ // individual struct items in the group.)
6333
+ const allUniforms = {};
6334
+ for (const group of uniformGroups) {
6335
+ for (const [uniformName, uniformData] of Object.entries(group.uniforms)) {
6336
+ allUniforms[uniformName] = {
6337
+ ...uniformData,
6338
+ group: group.group,
6339
+ binding: group.binding,
6340
+ varName: group.varName
6341
+ };
6342
+ }
6343
+ }
6344
+
6345
+ // Store uniform groups for buffer pooling
6346
+ shader._uniformGroups = uniformGroups;
6347
+
6048
6348
  // Extract samplers from group bindings
6049
6349
  const samplers = {};
6050
6350
  // TODO: support other texture types
6051
6351
  const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d<f32>|sampler);/g;
6352
+
6353
+ // Track which bindings are taken by the struct properties we've parsed
6354
+ // (the rest should be textures/samplers)
6355
+ const structUniformBindings = {};
6356
+ for (const g of uniformGroups) {
6357
+ structUniformBindings[g.group + ',' + g.binding] = true;
6358
+ }
6359
+
6052
6360
  for (const [src, visibility] of [
6053
6361
  [shader.vertSrc(), GPUShaderStage.VERTEX],
6054
6362
  [shader.fragSrc(), GPUShaderStage.FRAGMENT]
@@ -6058,10 +6366,8 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
6058
6366
  const [_, group, binding, name, type] = match;
6059
6367
  const groupIndex = parseInt(group);
6060
6368
  const bindingIndex = parseInt(binding);
6061
- // We're currently reserving group 0 for non-sampler stuff, which we parse
6062
- // above, so we can skip it here while we grab the remaining sampler
6063
- // uniforms
6064
- if (groupIndex === 0 && bindingIndex === 0) continue;
6369
+ // Skip struct uniform bindings which we've already parsed
6370
+ if (structUniformBindings[groupIndex + ',' + bindingIndex]) continue;
6065
6371
 
6066
6372
  const key = `${groupIndex},${bindingIndex}`;
6067
6373
  samplers[key] = {
@@ -6090,17 +6396,17 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
6090
6396
  }
6091
6397
  }
6092
6398
  }
6093
- return [...Object.values(uniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers)];
6399
+ return [...Object.values(allUniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers)];
6094
6400
  }
6095
6401
 
6096
- getNextBindingIndex(shader, group = 0) {
6402
+ getNextBindingIndex({ vert, frag }, group = 0) {
6097
6403
  // Get the highest binding index in the specified group and return the next available
6098
6404
  const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d<f32>|sampler|uniform)/g;
6099
6405
  let maxBindingIndex = -1;
6100
6406
 
6101
6407
  for (const [src, visibility] of [
6102
- [shader.vertSrc(), GPUShaderStage.VERTEX],
6103
- [shader.fragSrc(), GPUShaderStage.FRAGMENT]
6408
+ [vert, GPUShaderStage.VERTEX],
6409
+ [frag, GPUShaderStage.FRAGMENT]
6104
6410
  ]) {
6105
6411
  let match;
6106
6412
  while ((match = samplerRegex.exec(src)) !== null) {
@@ -6114,11 +6420,14 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
6114
6420
  return maxBindingIndex + 1;
6115
6421
  }
6116
6422
 
6117
- updateUniformValue(_shader, uniform, data) {
6423
+ updateUniformValue(shader, uniform, data) {
6118
6424
  if (uniform.isSampler) {
6119
6425
  uniform.texture =
6120
6426
  data instanceof Texture ? data : this.getTexture(data);
6427
+ } else {
6428
+ uniform._mappedData = this._mapUniformData(uniform, uniform._cachedData);
6121
6429
  }
6430
+ shader.buffersDirty.add(uniform.group * 1000 + uniform.binding);
6122
6431
  }
6123
6432
 
6124
6433
  _updateTexture(uniform, tex) {
@@ -6355,6 +6664,7 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
6355
6664
  }
6356
6665
 
6357
6666
  _clearClipBuffer() {
6667
+ this._finishActiveRenderPass();
6358
6668
  const commandEncoder = this.device.createCommandEncoder();
6359
6669
 
6360
6670
  const activeFramebuffer = this.activeFramebuffer();
@@ -6438,12 +6748,33 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
6438
6748
  }
6439
6749
  }
6440
6750
 
6441
- let uniforms = '';
6751
+ // Inject hook uniforms as a separate struct at a new binding
6752
+ let hookUniformFields = '';
6442
6753
  for (const key in shader.hooks.uniforms) {
6443
6754
  // WGSL format: "name: type"
6444
- uniforms += `${key},\n`;
6755
+ hookUniformFields += ` ${key},\n`;
6756
+ }
6757
+
6758
+ if (hookUniformFields) {
6759
+ // Find the next available binding in group 0
6760
+ // Use the source we're currently building (preMain) which has texture bindings. We can't call `fragSrc()`
6761
+ // or `vertSrc()` because we may be in one of those calls already, and might infinite loop
6762
+ const nextBinding = this.getNextBindingIndex({
6763
+ vert: shaderType === 'vertex' ? preMain + (shader.hooks.vertex?.declarations ?? '') + shader.hooks.declarations : shader._vertSrc,
6764
+ frag: shaderType === 'fragment' ? preMain + (shader.hooks.fragment?.declarations ?? '') + shader.hooks.declarations : shader._fragSrc,
6765
+ }, 0);
6766
+
6767
+ // Create HookUniforms struct and binding
6768
+ const hookUniformsDecl = `
6769
+ // Hook Uniforms (from .modify())
6770
+ struct HookUniforms {
6771
+ ${hookUniformFields}}
6772
+
6773
+ @group(0) @binding(${nextBinding}) var<uniform> hooks: HookUniforms;
6774
+ `;
6775
+ // Insert before the first @group binding
6776
+ preMain = preMain.replace(/(@group\(0\)\s+@binding)/, `${hookUniformsDecl}\n$1`);
6445
6777
  }
6446
- preMain = preMain.replace(/struct\s+Uniforms\s+\{/, `$&\n${uniforms}`);
6447
6778
 
6448
6779
  // Handle varying variables by injecting them into VertexOutput and FragmentInput structs
6449
6780
  if (shader.hooks.varyingVariables && shader.hooks.varyingVariables.length > 0) {
@@ -6914,6 +7245,7 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
6914
7245
  }
6915
7246
 
6916
7247
  _clearFramebufferTextures(framebuffer) {
7248
+ this._finishActiveRenderPass();
6917
7249
  const commandEncoder = this.device.createCommandEncoder();
6918
7250
 
6919
7251
  // Clear the color texture (and multisampled texture if it exists)
@@ -7354,6 +7686,7 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
7354
7686
  * Copy framebuffer content directly to WebGPU texture mip level
7355
7687
  */
7356
7688
  _accumulateMipLevel(framebuffer, mipmapData, mipLevel, width, height) {
7689
+ this.flushDraw();
7357
7690
  // Copy from framebuffer texture to the mip level
7358
7691
  const commandEncoder = this.device.createCommandEncoder();
7359
7692