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