@bigmistqke/view.gl 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/test/tag.test.ts CHANGED
@@ -230,8 +230,14 @@ describe('interleave', () => {
230
230
  })
231
231
 
232
232
  describe('glsl', () => {
233
+ let gl: GL
234
+
235
+ beforeEach(() => {
236
+ gl = createMockGL()
237
+ })
238
+
233
239
  it('should create GLSL template with uniforms and attributes', () => {
234
- const result = glsl`
240
+ const vertex = glsl`
235
241
  ${uniform.vec2('u_resolution')}
236
242
  ${attribute.vec2('a_position')}
237
243
 
@@ -240,23 +246,21 @@ describe('glsl', () => {
240
246
  }
241
247
  `
242
248
 
243
- expect(result.template).toContain('uniform vec2 u_resolution;')
244
- expect(result.template).toContain('attribute vec2 a_position;')
245
- expect(result.template).toContain('void main()')
249
+ const { schema } = compile(gl, vertex, glsl``)
246
250
 
247
- expect(result.uniforms).toEqual({
251
+ expect(schema.uniforms).toEqual({
248
252
  u_resolution: { kind: 'vec2' },
249
253
  })
250
254
 
251
- expect(result.attributes).toEqual({
255
+ expect(schema.attributes).toEqual({
252
256
  a_position: { kind: 'vec2' },
253
257
  })
254
258
 
255
- expect(result.interleavedAttributes).toEqual({})
259
+ expect(schema.interleavedAttributes).toEqual({})
256
260
  })
257
261
 
258
262
  it('should handle WebGL2 syntax with #version 300 es', () => {
259
- const result = glsl`#version 300 es
263
+ const vertex = glsl`#version 300 es
260
264
  ${uniform.vec2('u_resolution')}
261
265
  ${attribute.vec2('a_position')}
262
266
 
@@ -265,12 +269,18 @@ describe('glsl', () => {
265
269
  }
266
270
  `
267
271
 
268
- expect(result.template).toContain('uniform vec2 u_resolution;')
269
- expect(result.template).toContain('in vec2 a_position;') // 'in' instead of 'attribute'
272
+ // For this test, we need to check the shader compilation calls
273
+ compile(gl, vertex, glsl``)
274
+
275
+ // Check that shaderSource was called with WebGL2 syntax
276
+ const vertexShaderSource = (gl.shaderSource as any).mock.calls.find(
277
+ (call: any[]) => call[1].includes('in vec2 a_position')
278
+ )
279
+ expect(vertexShaderSource).toBeTruthy()
270
280
  })
271
281
 
272
282
  it('should handle interleaved attributes', () => {
273
- const result = glsl`
283
+ const vertex = glsl`
274
284
  ${interleave('vertexData', [attribute.vec2('a_position'), attribute.vec4('a_color')])}
275
285
 
276
286
  void main() {
@@ -278,10 +288,9 @@ describe('glsl', () => {
278
288
  }
279
289
  `
280
290
 
281
- expect(result.template).toContain('attribute vec2 a_position;')
282
- expect(result.template).toContain('attribute vec4 a_color;')
291
+ const { schema } = compile(gl, vertex, glsl``)
283
292
 
284
- expect(result.interleavedAttributes).toEqual({
293
+ expect(schema.interleavedAttributes).toEqual({
285
294
  vertexData: {
286
295
  layout: [
287
296
  { key: 'a_position', kind: 'vec2' },
@@ -293,7 +302,7 @@ describe('glsl', () => {
293
302
  })
294
303
 
295
304
  it('should handle interleaved attributes in WebGL2', () => {
296
- const result = glsl`#version 300 es
305
+ const vertex = glsl`#version 300 es
297
306
  ${interleave('vertexData', [attribute.vec2('a_position'), attribute.vec4('a_color')])}
298
307
 
299
308
  void main() {
@@ -301,12 +310,17 @@ describe('glsl', () => {
301
310
  }
302
311
  `
303
312
 
304
- expect(result.template).toContain('in vec2 a_position;')
305
- expect(result.template).toContain('in vec4 a_color;')
313
+ compile(gl, vertex, glsl``)
314
+
315
+ // Check that shaderSource was called with WebGL2 syntax
316
+ const vertexShaderSource = (gl.shaderSource as any).mock.calls.find(
317
+ (call: any[]) => call[1].includes('in vec2 a_position') && call[1].includes('in vec4 a_color')
318
+ )
319
+ expect(vertexShaderSource).toBeTruthy()
306
320
  })
307
321
 
308
322
  it('should handle uniform arrays', () => {
309
- const result = glsl`
323
+ const fragment = glsl`
310
324
  ${uniform.vec3('u_palette', { size: 7 })}
311
325
 
312
326
  void main() {
@@ -314,9 +328,9 @@ describe('glsl', () => {
314
328
  }
315
329
  `
316
330
 
317
- expect(result.template).toContain('uniform vec3 u_palette[7];')
331
+ const { schema } = compile(gl, glsl``, fragment)
318
332
 
319
- expect(result.uniforms).toEqual({
333
+ expect(schema.uniforms).toEqual({
320
334
  u_palette: { kind: 'vec3', size: 7 },
321
335
  })
322
336
  })
@@ -325,7 +339,7 @@ describe('glsl', () => {
325
339
  const maxLights = 8
326
340
  const shaderName = 'MyShader'
327
341
 
328
- const result = glsl`
342
+ const fragment = glsl`
329
343
  // ${shaderName}
330
344
  #define MAX_LIGHTS ${maxLights}
331
345
  ${uniform.float('u_time')}
@@ -335,13 +349,19 @@ describe('glsl', () => {
335
349
  }
336
350
  `
337
351
 
338
- expect(result.template).toContain('// MyShader')
339
- expect(result.template).toContain('#define MAX_LIGHTS 8')
340
- expect(result.template).toContain('uniform float u_time;')
352
+ compile(gl, glsl``, fragment)
353
+
354
+ // Check that shaderSource was called with the interpolated values
355
+ const fragmentShaderSource = (gl.shaderSource as any).mock.calls.find(
356
+ (call: any[]) => call[1].includes('// MyShader') &&
357
+ call[1].includes('#define MAX_LIGHTS 8') &&
358
+ call[1].includes('uniform float u_time;')
359
+ )
360
+ expect(fragmentShaderSource).toBeTruthy()
341
361
  })
342
362
 
343
363
  it('should handle multiple attributes and uniforms', () => {
344
- const result = glsl`
364
+ const vertex = glsl`
345
365
  ${uniform.mat4('u_mvpMatrix')}
346
366
  ${uniform.sampler2D('u_texture')}
347
367
  ${attribute.vec3('a_position')}
@@ -353,12 +373,14 @@ describe('glsl', () => {
353
373
  }
354
374
  `
355
375
 
356
- expect(result.uniforms).toEqual({
376
+ const { schema } = compile(gl, vertex, glsl``)
377
+
378
+ expect(schema.uniforms).toEqual({
357
379
  u_mvpMatrix: { kind: 'mat4' },
358
380
  u_texture: { kind: 'sampler2D' },
359
381
  })
360
382
 
361
- expect(result.attributes).toEqual({
383
+ expect(schema.attributes).toEqual({
362
384
  a_position: { kind: 'vec3' },
363
385
  a_uv: { kind: 'vec2' },
364
386
  a_color: { kind: 'vec4', instanced: true },
@@ -366,16 +388,15 @@ describe('glsl', () => {
366
388
  })
367
389
 
368
390
  it('should handle empty template', () => {
369
- const result = glsl``
391
+ const { schema } = compile(gl, glsl``, glsl``)
370
392
 
371
- expect(result.template).toBe('')
372
- expect(result.uniforms).toEqual({})
373
- expect(result.attributes).toEqual({})
374
- expect(result.interleavedAttributes).toEqual({})
393
+ expect(schema.uniforms).toEqual({})
394
+ expect(schema.attributes).toEqual({})
395
+ expect(schema.interleavedAttributes).toEqual({})
375
396
  })
376
397
 
377
398
  it('should skip string resources when collecting schema', () => {
378
- const result = glsl`
399
+ const fragment = glsl`
379
400
  ${'// This is a comment'}
380
401
  ${42}
381
402
  ${uniform.float('u_time')}
@@ -384,19 +405,27 @@ describe('glsl', () => {
384
405
  void main() {}
385
406
  `
386
407
 
387
- expect(result.uniforms).toEqual({
408
+ const { schema } = compile(gl, glsl``, fragment)
409
+
410
+ expect(schema.uniforms).toEqual({
388
411
  u_time: { kind: 'float' },
389
412
  })
390
- expect(Object.keys(result.uniforms).length).toBe(1)
413
+ expect(Object.keys(schema.uniforms).length).toBe(1)
391
414
  })
392
415
  })
393
416
 
394
417
  describe('glsl with symbols', () => {
418
+ let gl: GL
419
+
420
+ beforeEach(() => {
421
+ gl = createMockGL()
422
+ })
423
+
395
424
  it('should support symbols in uniform tags', () => {
396
425
  const u_time_symbol = Symbol('u_time')
397
426
  const u_color_symbol = Symbol('u_color')
398
427
 
399
- const shader = glsl`
428
+ const fragment = glsl`
400
429
  precision mediump float;
401
430
 
402
431
  ${uniform.float(u_time_symbol)}
@@ -406,17 +435,24 @@ describe('glsl with symbols', () => {
406
435
  gl_FragColor = vec4(${u_color_symbol} * ${u_time_symbol}, 1.0);
407
436
  }`
408
437
 
409
- expect(shader.uniforms[u_time_symbol]).toEqual({ kind: 'float' })
410
- expect(shader.uniforms[u_color_symbol]).toEqual({ kind: 'vec3' })
411
- expect(shader.template).toContain('uniform float VIEW_GL_ALIAS_')
412
- expect(shader.template).toContain('uniform vec3 VIEW_GL_ALIAS_')
438
+ const { schema } = compile(gl, glsl``, fragment)
439
+
440
+ expect(schema.uniforms[u_time_symbol]).toEqual({ kind: 'float' })
441
+ expect(schema.uniforms[u_color_symbol]).toEqual({ kind: 'vec3' })
442
+
443
+ // Check that shaderSource was called with symbol aliases
444
+ const fragmentShaderSource = (gl.shaderSource as any).mock.calls.find(
445
+ (call: any[]) => call[1].includes('uniform float VIEW_GL_ALIAS_') &&
446
+ call[1].includes('uniform vec3 VIEW_GL_ALIAS_')
447
+ )
448
+ expect(fragmentShaderSource).toBeTruthy()
413
449
  })
414
450
 
415
451
  it('should support symbols in attribute tags', () => {
416
452
  const a_position_symbol = Symbol('a_position')
417
453
  const a_color_symbol = Symbol('a_color')
418
454
 
419
- const shader = glsl`
455
+ const vertex = glsl`
420
456
  ${attribute.vec2(a_position_symbol)}
421
457
  ${attribute.vec4(a_color_symbol, { instanced: true })}
422
458
 
@@ -424,10 +460,17 @@ describe('glsl with symbols', () => {
424
460
  gl_Position = vec4(${a_position_symbol}, 0.0, 1.0);
425
461
  }`
426
462
 
427
- expect(shader.attributes[a_position_symbol]).toEqual({ kind: 'vec2' })
428
- expect(shader.attributes[a_color_symbol]).toEqual({ kind: 'vec4', instanced: true })
429
- expect(shader.template).toContain('attribute vec2 VIEW_GL_ALIAS_')
430
- expect(shader.template).toContain('attribute vec4 VIEW_GL_ALIAS_')
463
+ const { schema } = compile(gl, vertex, glsl``)
464
+
465
+ expect(schema.attributes[a_position_symbol]).toEqual({ kind: 'vec2' })
466
+ expect(schema.attributes[a_color_symbol]).toEqual({ kind: 'vec4', instanced: true })
467
+
468
+ // Check that shaderSource was called with symbol aliases
469
+ const vertexShaderSource = (gl.shaderSource as any).mock.calls.find(
470
+ (call: any[]) => call[1].includes('attribute vec2 VIEW_GL_ALIAS_') &&
471
+ call[1].includes('attribute vec4 VIEW_GL_ALIAS_')
472
+ )
473
+ expect(vertexShaderSource).toBeTruthy()
431
474
  })
432
475
 
433
476
  it('should support symbols in interleaved attribute tags', () => {
@@ -435,74 +478,117 @@ describe('glsl with symbols', () => {
435
478
  const a_pos_symbol = Symbol('a_pos')
436
479
  const a_col_symbol = Symbol('a_col')
437
480
 
438
- const shader = glsl`
481
+ const vertex = glsl`
439
482
  ${interleave(data_symbol, [attribute.vec3(a_pos_symbol), attribute.vec4(a_col_symbol)])}
440
483
 
441
484
  void main() {
442
485
  gl_Position = vec4(${a_pos_symbol}, 1.0);
443
486
  }`
444
487
 
445
- expect(shader.interleavedAttributes[data_symbol]).toEqual({
488
+ const { schema } = compile(gl, vertex, glsl``)
489
+
490
+ expect(schema.interleavedAttributes[data_symbol]).toEqual({
446
491
  layout: [
447
492
  { key: a_pos_symbol, kind: 'vec3' },
448
493
  { key: a_col_symbol, kind: 'vec4' },
449
494
  ],
450
495
  instanced: false,
451
496
  })
452
- expect(shader.template).toContain('attribute vec3 VIEW_GL_ALIAS_')
453
- expect(shader.template).toContain('attribute vec4 VIEW_GL_ALIAS_')
497
+
498
+ // Check that shaderSource was called with symbol aliases
499
+ const vertexShaderSource = (gl.shaderSource as any).mock.calls.find(
500
+ (call: any[]) => call[1].includes('attribute vec3 VIEW_GL_ALIAS_') &&
501
+ call[1].includes('attribute vec4 VIEW_GL_ALIAS_')
502
+ )
503
+ expect(vertexShaderSource).toBeTruthy()
454
504
  })
455
505
 
456
506
  it('should support mixed string and symbol keys', () => {
457
507
  const u_color_symbol = Symbol('u_color')
458
508
  const a_normal_symbol = Symbol('a_normal')
459
509
 
460
- const shader = glsl`
461
- ${uniform.float('u_time')}
462
- ${uniform.vec3(u_color_symbol)}
510
+ const vertex = glsl`
463
511
  ${attribute.vec3('a_position')}
464
512
  ${attribute.vec3(a_normal_symbol)}
513
+ ${uniform.float('u_time')}
465
514
 
466
515
  void main() {
467
516
  gl_Position = vec4(a_position + ${a_normal_symbol}, u_time);
517
+ }`
518
+
519
+ const fragment = glsl`
520
+ ${uniform.float('u_time')}
521
+ ${uniform.vec3(u_color_symbol)}
522
+
523
+ void main() {
468
524
  gl_FragColor = vec4(${u_color_symbol}, 1.0);
469
525
  }`
470
526
 
471
- expect(shader.uniforms.u_time).toEqual({ kind: 'float' })
472
- expect(shader.uniforms[u_color_symbol]).toEqual({ kind: 'vec3' })
473
- expect(shader.attributes.a_position).toEqual({ kind: 'vec3' })
474
- expect(shader.attributes[a_normal_symbol]).toEqual({ kind: 'vec3' })
527
+ const { schema } = compile(gl, vertex, fragment)
528
+
529
+ expect(schema.uniforms.u_time).toEqual({ kind: 'float' })
530
+ expect(schema.uniforms[u_color_symbol]).toEqual({ kind: 'vec3' })
531
+ expect(schema.attributes.a_position).toEqual({ kind: 'vec3' })
532
+ expect(schema.attributes[a_normal_symbol]).toEqual({ kind: 'vec3' })
475
533
 
476
- expect(shader.template).toContain('uniform float u_time')
477
- expect(shader.template).toContain('uniform vec3 VIEW_GL_ALIAS_')
478
- expect(shader.template).toContain('attribute vec3 a_position')
479
- expect(shader.template).toContain('attribute vec3 VIEW_GL_ALIAS_')
534
+ // Check vertex shader source
535
+ const vertexShaderSource = (gl.shaderSource as any).mock.calls.find(
536
+ (call: any[]) => call[1].includes('attribute vec3 a_position') &&
537
+ call[1].includes('attribute vec3 VIEW_GL_ALIAS_')
538
+ )
539
+ expect(vertexShaderSource).toBeTruthy()
540
+
541
+ // Check fragment shader source
542
+ const fragmentShaderSource = (gl.shaderSource as any).mock.calls.find(
543
+ (call: any[]) => call[1].includes('uniform float u_time') &&
544
+ call[1].includes('uniform vec3 VIEW_GL_ALIAS_')
545
+ )
546
+ expect(fragmentShaderSource).toBeTruthy()
480
547
  })
481
548
 
482
549
  it('should support symbols in array uniforms', () => {
483
550
  const u_positions_symbol = Symbol('u_positions')
484
551
 
485
- const shader = glsl`
552
+ const fragment = glsl`
486
553
  ${uniform.vec3(u_positions_symbol, { size: 10 })}
487
554
 
488
555
  void main() {
489
556
  gl_FragColor = vec4(${u_positions_symbol}[0], 1.0);
490
557
  }`
491
558
 
492
- expect(shader.uniforms[u_positions_symbol]).toEqual({ kind: 'vec3', size: 10 })
493
- expect(shader.template).toContain('uniform vec3 VIEW_GL_ALIAS_')
494
- expect(shader.template).toContain('[10];')
559
+ const { schema } = compile(gl, glsl``, fragment)
560
+
561
+ expect(schema.uniforms[u_positions_symbol]).toEqual({ kind: 'vec3', size: 10 })
562
+
563
+ // Check that shaderSource was called with array syntax
564
+ const fragmentShaderSource = (gl.shaderSource as any).mock.calls.find(
565
+ (call: any[]) => call[1].includes('uniform vec3 VIEW_GL_ALIAS_') &&
566
+ call[1].includes('[10];')
567
+ )
568
+ expect(fragmentShaderSource).toBeTruthy()
495
569
  })
496
570
 
497
571
  it('should preserve symbol identity across multiple uses', () => {
498
572
  const u_shared_symbol = Symbol('u_shared')
499
573
 
500
- const shader1 = glsl`${uniform.float(u_shared_symbol)}`
501
- const shader2 = glsl`${uniform.float(u_shared_symbol)}`
574
+ const fragment1 = glsl`${uniform.float(u_shared_symbol)}
575
+ void main() { gl_FragColor = vec4(1.0); }`
576
+ const fragment2 = glsl`${uniform.float(u_shared_symbol)}
577
+ void main() { gl_FragColor = vec4(0.5); }`
578
+
579
+ compile(gl, glsl``, fragment1)
580
+ compile(gl, glsl``, fragment2)
502
581
 
503
582
  // Both should generate the same alias
504
- const alias1 = shader1.template.match(/VIEW_GL_ALIAS_\d+/)?.[0]
505
- const alias2 = shader2.template.match(/VIEW_GL_ALIAS_\d+/)?.[0]
583
+ const shader1Source = (gl.shaderSource as any).mock.calls.find(
584
+ (call: any[]) => call[1].includes('gl_FragColor = vec4(1.0)')
585
+ )?.[1]
586
+ const shader2Source = (gl.shaderSource as any).mock.calls.find(
587
+ (call: any[]) => call[1].includes('gl_FragColor = vec4(0.5)')
588
+ )?.[1]
589
+
590
+ const alias1 = shader1Source?.match(/VIEW_GL_ALIAS_\d+/)?.[0]
591
+ const alias2 = shader2Source?.match(/VIEW_GL_ALIAS_\d+/)?.[0]
506
592
 
507
593
  expect(alias1).toBe(alias2)
508
594
  })
@@ -511,7 +597,7 @@ describe('glsl with symbols', () => {
511
597
  const u_matrix_symbol = Symbol('u_matrix')
512
598
  const a_vertex_symbol = Symbol('a_vertex')
513
599
 
514
- const shader = glsl`#version 300 es
600
+ const vertex = glsl`#version 300 es
515
601
  precision mediump float;
516
602
 
517
603
  ${uniform.mat4(u_matrix_symbol)}
@@ -521,22 +607,310 @@ describe('glsl with symbols', () => {
521
607
  gl_Position = ${u_matrix_symbol} * vec4(${a_vertex_symbol}, 1.0);
522
608
  }`
523
609
 
524
- expect(shader.template).toContain('uniform mat4 VIEW_GL_ALIAS_')
525
- expect(shader.template).toContain('in vec3 VIEW_GL_ALIAS_') // WebGL2 uses 'in' instead of 'attribute'
610
+ compile(gl, vertex, glsl``)
611
+
612
+ // Check that shaderSource was called with WebGL2 syntax and symbol aliases
613
+ const vertexShaderSource = (gl.shaderSource as any).mock.calls.find(
614
+ (call: any[]) => call[1].includes('uniform mat4 VIEW_GL_ALIAS_') &&
615
+ call[1].includes('in vec3 VIEW_GL_ALIAS_')
616
+ )
617
+ expect(vertexShaderSource).toBeTruthy()
526
618
  })
527
619
 
528
620
  it('should handle symbol interpolation in template', () => {
529
621
  const color_symbol = Symbol('color')
530
622
 
531
- const shader = glsl`
623
+ const fragment = glsl`
532
624
  void main() {
533
625
  vec3 ${color_symbol} = vec3(1.0, 0.0, 0.0);
534
626
  gl_FragColor = vec4(${color_symbol}, 1.0);
535
627
  }`
536
628
 
537
- // Symbol used directly in template should be converted to alias
538
- expect(shader.template).toMatch(/vec3 VIEW_GL_ALIAS_\d+ = vec3/)
539
- expect(shader.template).toMatch(/vec4\(VIEW_GL_ALIAS_\d+, 1\.0\)/)
629
+ compile(gl, glsl``, fragment)
630
+
631
+ // Check that shaderSource was called with symbol aliases in template
632
+ const fragmentShaderSource = (gl.shaderSource as any).mock.calls.find(
633
+ (call: any[]) => call[1].includes('vec3') && call[1].includes('VIEW_GL_ALIAS_')
634
+ )?.[1]
635
+
636
+ expect(fragmentShaderSource).toMatch(/vec3 VIEW_GL_ALIAS_\d+ = vec3/)
637
+ expect(fragmentShaderSource).toMatch(/vec4\(VIEW_GL_ALIAS_\d+, 1\.0\)/)
638
+ })
639
+ })
640
+
641
+ describe('glsl composition', () => {
642
+ let gl: GL
643
+
644
+ beforeEach(() => {
645
+ gl = createMockGL()
646
+ })
647
+
648
+ it('should compose glsl templates inside other glsl templates', () => {
649
+ const sharedUniforms = glsl`
650
+ ${uniform.vec2('u_resolution')}
651
+ ${uniform.float('u_time')}
652
+ `
653
+
654
+ const vertex = glsl`
655
+ ${sharedUniforms}
656
+ ${attribute.vec2('a_position')}
657
+
658
+ void main() {
659
+ gl_Position = vec4(a_position * u_resolution, 0.0, 1.0);
660
+ }
661
+ `
662
+
663
+ const { schema } = compile(gl, vertex, glsl``)
664
+
665
+ expect(schema.uniforms).toEqual({
666
+ u_resolution: { kind: 'vec2' },
667
+ u_time: { kind: 'float' },
668
+ })
669
+
670
+ expect(schema.attributes).toEqual({
671
+ a_position: { kind: 'vec2' },
672
+ })
673
+ })
674
+
675
+ it('should compose nested glsl with symbols', () => {
676
+ const u_time_symbol = Symbol('u_time')
677
+ const a_pos_symbol = Symbol('a_position')
678
+
679
+ const sharedCode = glsl`
680
+ ${uniform.float(u_time_symbol)}
681
+ ${uniform.vec3('u_color')}
682
+ `
683
+
684
+ const vertex = glsl`
685
+ ${sharedCode}
686
+ ${attribute.vec2(a_pos_symbol)}
687
+
688
+ void main() {
689
+ gl_Position = vec4(${a_pos_symbol}, 0.0, 1.0);
690
+ }
691
+ `
692
+
693
+ const { schema } = compile(gl, vertex, glsl``)
694
+
695
+ expect(schema.uniforms).toEqual({
696
+ [u_time_symbol]: { kind: 'float' },
697
+ u_color: { kind: 'vec3' },
698
+ })
699
+
700
+ expect(schema.attributes).toEqual({
701
+ [a_pos_symbol]: { kind: 'vec2' },
702
+ })
703
+ })
704
+
705
+ it('should compose multiple levels of nesting', () => {
706
+ const basicUniforms = glsl`
707
+ ${uniform.float('u_time')}
708
+ ${uniform.vec2('u_resolution')}
709
+ `
710
+
711
+ const lightingUniforms = glsl`
712
+ ${basicUniforms}
713
+ ${uniform.vec3('u_lightPos')}
714
+ ${uniform.vec3('u_lightColor')}
715
+ `
716
+
717
+ const materialUniforms = glsl`
718
+ ${lightingUniforms}
719
+ ${uniform.float('u_roughness')}
720
+ ${uniform.float('u_metallic')}
721
+ `
722
+
723
+ const vertex = glsl`
724
+ ${materialUniforms}
725
+ ${attribute.vec3('a_position')}
726
+ ${attribute.vec3('a_normal')}
727
+
728
+ void main() {
729
+ gl_Position = vec4(a_position, 1.0);
730
+ }
731
+ `
732
+
733
+ const { schema } = compile(gl, vertex, glsl``)
734
+
735
+ expect(schema.uniforms).toEqual({
736
+ u_time: { kind: 'float' },
737
+ u_resolution: { kind: 'vec2' },
738
+ u_lightPos: { kind: 'vec3' },
739
+ u_lightColor: { kind: 'vec3' },
740
+ u_roughness: { kind: 'float' },
741
+ u_metallic: { kind: 'float' },
742
+ })
743
+
744
+ expect(schema.attributes).toEqual({
745
+ a_position: { kind: 'vec3' },
746
+ a_normal: { kind: 'vec3' },
747
+ })
748
+ })
749
+
750
+ it('should compose glsl with interleaved attributes', () => {
751
+ const vertexLayout = glsl`
752
+ ${interleave('vertexData', [
753
+ attribute.vec3('a_position'),
754
+ attribute.vec2('a_uv'),
755
+ attribute.vec3('a_normal')
756
+ ])}
757
+ `
758
+
759
+ const instanceLayout = glsl`
760
+ ${interleave('instanceData', [
761
+ attribute.vec3('a_instancePos'),
762
+ attribute.float('a_instanceScale')
763
+ ], { instanced: true })}
764
+ `
765
+
766
+ const vertex = glsl`
767
+ ${vertexLayout}
768
+ ${instanceLayout}
769
+ ${uniform.mat4('u_mvpMatrix')}
770
+
771
+ void main() {
772
+ vec3 worldPos = a_position * a_instanceScale + a_instancePos;
773
+ gl_Position = u_mvpMatrix * vec4(worldPos, 1.0);
774
+ }
775
+ `
776
+
777
+ const { schema } = compile(gl, vertex, glsl``)
778
+
779
+ expect(schema.uniforms).toEqual({
780
+ u_mvpMatrix: { kind: 'mat4' },
781
+ })
782
+
783
+ expect(schema.interleavedAttributes).toEqual({
784
+ vertexData: {
785
+ layout: [
786
+ { key: 'a_position', kind: 'vec3' },
787
+ { key: 'a_uv', kind: 'vec2' },
788
+ { key: 'a_normal', kind: 'vec3' },
789
+ ],
790
+ instanced: false,
791
+ },
792
+ instanceData: {
793
+ layout: [
794
+ { key: 'a_instancePos', kind: 'vec3' },
795
+ { key: 'a_instanceScale', kind: 'float' },
796
+ ],
797
+ instanced: true,
798
+ },
799
+ })
800
+ })
801
+
802
+ it('should compose glsl in both vertex and fragment shaders', () => {
803
+ const sharedUniforms = glsl`
804
+ ${uniform.float('u_time')}
805
+ ${uniform.vec2('u_resolution')}
806
+ `
807
+
808
+ const lightingCode = glsl`
809
+ ${uniform.vec3('u_lightPos')}
810
+
811
+ vec3 calculateLighting(vec3 pos, vec3 normal) {
812
+ vec3 lightDir = normalize(u_lightPos - pos);
813
+ return max(dot(normal, lightDir), 0.0) * vec3(1.0);
814
+ }
815
+ `
816
+
817
+ const vertex = glsl`
818
+ ${sharedUniforms}
819
+ ${attribute.vec3('a_position')}
820
+ ${attribute.vec3('a_normal')}
821
+
822
+ varying vec3 v_normal;
823
+ varying vec3 v_position;
824
+
825
+ void main() {
826
+ v_normal = a_normal;
827
+ v_position = a_position;
828
+ gl_Position = vec4(a_position, 1.0);
829
+ }
830
+ `
831
+
832
+ const fragment = glsl`
833
+ ${sharedUniforms}
834
+ ${lightingCode}
835
+
836
+ varying vec3 v_normal;
837
+ varying vec3 v_position;
838
+
839
+ void main() {
840
+ vec3 lighting = calculateLighting(v_position, v_normal);
841
+ gl_FragColor = vec4(lighting, 1.0);
842
+ }
843
+ `
844
+
845
+ const { schema } = compile(gl, vertex, fragment)
846
+
847
+ expect(schema.uniforms).toEqual({
848
+ u_time: { kind: 'float' },
849
+ u_resolution: { kind: 'vec2' },
850
+ u_lightPos: { kind: 'vec3' },
851
+ })
852
+
853
+ expect(schema.attributes).toEqual({
854
+ a_position: { kind: 'vec3' },
855
+ a_normal: { kind: 'vec3' },
856
+ })
857
+ })
858
+
859
+ it('should handle empty composed glsl templates', () => {
860
+ const emptyCode = glsl``
861
+
862
+ const vertex = glsl`
863
+ ${emptyCode}
864
+ ${attribute.vec2('a_position')}
865
+
866
+ void main() {
867
+ gl_Position = vec4(a_position, 0.0, 1.0);
868
+ }
869
+ `
870
+
871
+ const { schema } = compile(gl, vertex, glsl``)
872
+
873
+ expect(schema.attributes).toEqual({
874
+ a_position: { kind: 'vec2' },
875
+ })
876
+
877
+ expect(schema.uniforms).toEqual({})
878
+ })
879
+
880
+ it('should deduplicate uniforms across composed templates', () => {
881
+ const timeUniform = glsl`${uniform.float('u_time')}`
882
+ const resolutionUniform = glsl`${uniform.vec2('u_resolution')}`
883
+
884
+ const sharedUniforms1 = glsl`
885
+ ${timeUniform}
886
+ ${resolutionUniform}
887
+ `
888
+
889
+ const sharedUniforms2 = glsl`
890
+ ${timeUniform} // Same uniform appears again
891
+ ${uniform.vec3('u_color')}
892
+ `
893
+
894
+ const vertex = glsl`
895
+ ${sharedUniforms1}
896
+ ${sharedUniforms2}
897
+ ${attribute.vec2('a_position')}
898
+
899
+ void main() {
900
+ gl_Position = vec4(a_position * u_resolution, 0.0, 1.0);
901
+ }
902
+ `
903
+
904
+ const { schema } = compile(gl, vertex, glsl``)
905
+
906
+ // Should only have one instance of u_time, despite appearing multiple times
907
+ expect(schema.uniforms).toEqual({
908
+ u_time: { kind: 'float' },
909
+ u_resolution: { kind: 'vec2' },
910
+ u_color: { kind: 'vec3' },
911
+ })
912
+
913
+ expect(Object.keys(schema.uniforms)).toHaveLength(3)
540
914
  })
541
915
  })
542
916
 
@@ -618,6 +992,275 @@ describe('compile with symbols', () => {
618
992
  })
619
993
  })
620
994
 
995
+ describe('glsl array interpolation', () => {
996
+ let gl: GL
997
+
998
+ beforeEach(() => {
999
+ gl = createMockGL()
1000
+ })
1001
+
1002
+ it('should interpolate arrays of resource tags', () => {
1003
+ const shader = glsl`
1004
+ ${[uniform.float('u_time'), uniform.vec2('u_resolution')]}
1005
+ ${[attribute.vec3('a_position'), attribute.vec2('a_uv')]}
1006
+
1007
+ void main() {
1008
+ gl_Position = vec4(a_position, 1.0);
1009
+ }
1010
+ `
1011
+
1012
+ const { schema } = compile(gl, shader, glsl``)
1013
+
1014
+ expect(schema.uniforms).toEqual({
1015
+ u_time: { kind: 'float' },
1016
+ u_resolution: { kind: 'vec2' },
1017
+ })
1018
+
1019
+ expect(schema.attributes).toEqual({
1020
+ a_position: { kind: 'vec3' },
1021
+ a_uv: { kind: 'vec2' },
1022
+ })
1023
+ })
1024
+
1025
+ it('should interpolate arrays with mixed types', () => {
1026
+ const sharedUniforms = glsl`
1027
+ ${uniform.float('u_time')}
1028
+ ${uniform.vec2('u_resolution')}
1029
+ `
1030
+
1031
+ const shader = glsl`
1032
+ ${[sharedUniforms, uniform.vec3('u_color')]}
1033
+ ${[attribute.vec3('a_position'), 'precision mediump float;']}
1034
+
1035
+ void main() {
1036
+ gl_Position = vec4(a_position, 1.0);
1037
+ }
1038
+ `
1039
+
1040
+ const { schema } = compile(gl, shader, glsl``)
1041
+
1042
+ expect(schema.uniforms).toEqual({
1043
+ u_time: { kind: 'float' },
1044
+ u_resolution: { kind: 'vec2' },
1045
+ u_color: { kind: 'vec3' },
1046
+ })
1047
+
1048
+ expect(schema.attributes).toEqual({
1049
+ a_position: { kind: 'vec3' },
1050
+ })
1051
+
1052
+ // Check that the string was interpolated in the shader source
1053
+ const shaderSource = (gl.shaderSource as any).mock.calls.find(
1054
+ (call: any[]) => call[1].includes('precision mediump float;')
1055
+ )
1056
+ expect(shaderSource).toBeTruthy()
1057
+ })
1058
+
1059
+ it('should interpolate arrays with symbols', () => {
1060
+ const u_time_symbol = Symbol('u_time')
1061
+ const a_pos_symbol = Symbol('a_position')
1062
+
1063
+ const shader = glsl`
1064
+ ${[uniform.float(u_time_symbol), uniform.vec3('u_color')]}
1065
+ ${[attribute.vec3(a_pos_symbol), attribute.vec2('a_uv')]}
1066
+
1067
+ void main() {
1068
+ gl_Position = vec4(${a_pos_symbol}, 1.0);
1069
+ }
1070
+ `
1071
+
1072
+ const { schema } = compile(gl, shader, glsl``)
1073
+
1074
+ expect(schema.uniforms).toEqual({
1075
+ [u_time_symbol]: { kind: 'float' },
1076
+ u_color: { kind: 'vec3' },
1077
+ })
1078
+
1079
+ expect(schema.attributes).toEqual({
1080
+ [a_pos_symbol]: { kind: 'vec3' },
1081
+ a_uv: { kind: 'vec2' },
1082
+ })
1083
+
1084
+ // Check that symbol was interpolated in the shader source
1085
+ const shaderSource = (gl.shaderSource as any).mock.calls.find(
1086
+ (call: any[]) => call[1].includes('attribute vec3 VIEW_GL_ALIAS_') &&
1087
+ call[1].includes('uniform float VIEW_GL_ALIAS_')
1088
+ )
1089
+ expect(shaderSource).toBeTruthy()
1090
+ })
1091
+
1092
+ it('should interpolate nested arrays', () => {
1093
+ const shader = glsl`
1094
+ ${[[uniform.float('u_time'), uniform.vec2('u_resolution')], uniform.vec3('u_color')]}
1095
+ ${[[attribute.vec3('a_position')], [attribute.vec2('a_uv'), attribute.vec3('a_normal')]]}
1096
+
1097
+ void main() {
1098
+ gl_Position = vec4(a_position, 1.0);
1099
+ }
1100
+ `
1101
+
1102
+ const { schema } = compile(gl, shader, glsl``)
1103
+
1104
+ expect(schema.uniforms).toEqual({
1105
+ u_time: { kind: 'float' },
1106
+ u_resolution: { kind: 'vec2' },
1107
+ u_color: { kind: 'vec3' },
1108
+ })
1109
+
1110
+ expect(schema.attributes).toEqual({
1111
+ a_position: { kind: 'vec3' },
1112
+ a_uv: { kind: 'vec2' },
1113
+ a_normal: { kind: 'vec3' },
1114
+ })
1115
+ })
1116
+
1117
+ it('should interpolate arrays with interleaved attributes', () => {
1118
+ const vertexData = interleave('vertexData', [
1119
+ attribute.vec2('a_position'),
1120
+ attribute.vec2('a_uv'),
1121
+ ])
1122
+
1123
+ const shader = glsl`
1124
+ ${[vertexData, uniform.mat4('u_mvpMatrix')]}
1125
+
1126
+ void main() {
1127
+ gl_Position = u_mvpMatrix * vec4(a_position, 0.0, 1.0);
1128
+ }
1129
+ `
1130
+
1131
+ const { schema } = compile(gl, shader, glsl``)
1132
+
1133
+ expect(schema.uniforms).toEqual({
1134
+ u_mvpMatrix: { kind: 'mat4' },
1135
+ })
1136
+
1137
+ expect(schema.interleavedAttributes).toEqual({
1138
+ vertexData: {
1139
+ layout: [
1140
+ { key: 'a_position', kind: 'vec2' },
1141
+ { key: 'a_uv', kind: 'vec2' },
1142
+ ],
1143
+ instanced: false,
1144
+ },
1145
+ })
1146
+ })
1147
+
1148
+ it('should interpolate arrays with composed glsl templates', () => {
1149
+ const sharedUniforms = glsl`
1150
+ ${uniform.float('u_time')}
1151
+ ${uniform.vec2('u_resolution')}
1152
+ `
1153
+
1154
+ const lightingCode = glsl`
1155
+ ${uniform.vec3('u_lightPos')}
1156
+
1157
+ vec3 calculateLighting(vec3 pos) {
1158
+ return normalize(u_lightPos - pos);
1159
+ }
1160
+ `
1161
+
1162
+ const shader = glsl`
1163
+ ${[sharedUniforms, lightingCode]}
1164
+ ${[attribute.vec3('a_position'), attribute.vec3('a_normal')]}
1165
+
1166
+ void main() {
1167
+ vec3 lighting = calculateLighting(a_position);
1168
+ gl_Position = vec4(a_position, 1.0);
1169
+ }
1170
+ `
1171
+
1172
+ const { schema } = compile(gl, shader, glsl``)
1173
+
1174
+ expect(schema.uniforms).toEqual({
1175
+ u_time: { kind: 'float' },
1176
+ u_resolution: { kind: 'vec2' },
1177
+ u_lightPos: { kind: 'vec3' },
1178
+ })
1179
+
1180
+ expect(schema.attributes).toEqual({
1181
+ a_position: { kind: 'vec3' },
1182
+ a_normal: { kind: 'vec3' },
1183
+ })
1184
+
1185
+ // Check that both templates and the function were interpolated
1186
+ const shaderSource = (gl.shaderSource as any).mock.calls.find(
1187
+ (call: any[]) => call[1].includes('calculateLighting') &&
1188
+ call[1].includes('uniform float u_time') &&
1189
+ call[1].includes('uniform vec3 u_lightPos')
1190
+ )
1191
+ expect(shaderSource).toBeTruthy()
1192
+ })
1193
+
1194
+ it('should handle empty arrays', () => {
1195
+ const shader = glsl`
1196
+ ${[]}
1197
+ ${uniform.float('u_time')}
1198
+ ${[]}
1199
+
1200
+ void main() {
1201
+ gl_Position = vec4(1.0);
1202
+ }
1203
+ `
1204
+
1205
+ const { schema } = compile(gl, shader, glsl``)
1206
+
1207
+ expect(schema.uniforms).toEqual({
1208
+ u_time: { kind: 'float' },
1209
+ })
1210
+
1211
+ expect(schema.attributes).toEqual({})
1212
+ expect(schema.interleavedAttributes).toEqual({})
1213
+ })
1214
+
1215
+ it('should interpolate arrays with WebGL2 syntax', () => {
1216
+ const shader = glsl`#version 300 es
1217
+ ${[uniform.float('u_time'), attribute.vec3('a_position')]}
1218
+
1219
+ void main() {
1220
+ gl_Position = vec4(a_position, 1.0);
1221
+ }
1222
+ `
1223
+
1224
+ compile(gl, shader, glsl``)
1225
+
1226
+ // Check that shaderSource was called with WebGL2 syntax
1227
+ const shaderSource = (gl.shaderSource as any).mock.calls.find(
1228
+ (call: any[]) => call[1].includes('uniform float u_time') &&
1229
+ call[1].includes('in vec3 a_position')
1230
+ )
1231
+ expect(shaderSource).toBeTruthy()
1232
+ })
1233
+
1234
+ it('should preserve order when interpolating arrays', () => {
1235
+ const shader = glsl`
1236
+ ${['// First comment', uniform.float('u_time'), '// Second comment']}
1237
+
1238
+ void main() {
1239
+ gl_Position = vec4(1.0);
1240
+ }
1241
+ `
1242
+
1243
+ compile(gl, shader, glsl``)
1244
+
1245
+ // Check that order is preserved in the shader source
1246
+ const shaderSource = (gl.shaderSource as any).mock.calls.find(
1247
+ (call: any[]) => call[1].includes('// First comment') &&
1248
+ call[1].includes('uniform float u_time') &&
1249
+ call[1].includes('// Second comment')
1250
+ )?.[1]
1251
+
1252
+ expect(shaderSource).toBeTruthy()
1253
+
1254
+ // Verify order
1255
+ const firstIndex = shaderSource.indexOf('// First comment')
1256
+ const uniformIndex = shaderSource.indexOf('uniform float u_time')
1257
+ const secondIndex = shaderSource.indexOf('// Second comment')
1258
+
1259
+ expect(firstIndex).toBeLessThan(uniformIndex)
1260
+ expect(uniformIndex).toBeLessThan(secondIndex)
1261
+ })
1262
+ })
1263
+
621
1264
  describe('compile', () => {
622
1265
  let gl: GL
623
1266
  let mockProgram: WebGLProgram
@@ -697,6 +1340,87 @@ describe('compile', () => {
697
1340
  })
698
1341
  })
699
1342
 
1343
+ it('should merge attributes and interleaved attributes from vertex and fragment', () => {
1344
+ const vertex = glsl`
1345
+ ${attribute.vec3('a_position')}
1346
+ ${interleave('vertexData', [attribute.vec2('a_uv'), attribute.vec3('a_normal')])}
1347
+
1348
+ void main() {}
1349
+ `
1350
+
1351
+ const fragment = glsl`
1352
+ ${attribute.vec4('a_color')} // Fragment attributes (rare but valid)
1353
+ ${interleave('materialData', [attribute.float('a_roughness'), attribute.float('a_metallic')])}
1354
+
1355
+ void main() {}
1356
+ `
1357
+
1358
+ const result = compile(gl, vertex, fragment)
1359
+
1360
+ expect(result.schema.attributes).toEqual({
1361
+ a_position: { kind: 'vec3' },
1362
+ a_color: { kind: 'vec4' },
1363
+ })
1364
+
1365
+ expect(result.schema.interleavedAttributes).toEqual({
1366
+ vertexData: {
1367
+ layout: [
1368
+ { key: 'a_uv', kind: 'vec2' },
1369
+ { key: 'a_normal', kind: 'vec3' },
1370
+ ],
1371
+ instanced: false,
1372
+ },
1373
+ materialData: {
1374
+ layout: [
1375
+ { key: 'a_roughness', kind: 'float' },
1376
+ { key: 'a_metallic', kind: 'float' },
1377
+ ],
1378
+ instanced: false,
1379
+ },
1380
+ })
1381
+ })
1382
+
1383
+ it('should merge schemas with symbol keys', () => {
1384
+ const u_time_symbol = Symbol('u_time')
1385
+ const a_pos_symbol = Symbol('a_pos')
1386
+ const data_symbol = Symbol('data')
1387
+
1388
+ const vertex = glsl`
1389
+ ${uniform.float(u_time_symbol)}
1390
+ ${attribute.vec2(a_pos_symbol)}
1391
+ ${interleave(data_symbol, [attribute.vec3('a_normal')])}
1392
+
1393
+ void main() {}
1394
+ `
1395
+
1396
+ const fragment = glsl`
1397
+ ${uniform.float(u_time_symbol)} // Same symbol in both shaders
1398
+ ${uniform.vec3('u_color')}
1399
+
1400
+ void main() {}
1401
+ `
1402
+
1403
+ const result = compile(gl, vertex, fragment)
1404
+
1405
+ expect(result.schema.uniforms).toEqual({
1406
+ [u_time_symbol]: { kind: 'float' },
1407
+ u_color: { kind: 'vec3' },
1408
+ })
1409
+
1410
+ expect(result.schema.attributes).toEqual({
1411
+ [a_pos_symbol]: { kind: 'vec2' },
1412
+ })
1413
+
1414
+ expect(result.schema.interleavedAttributes).toEqual({
1415
+ [data_symbol]: {
1416
+ layout: [
1417
+ { key: 'a_normal', kind: 'vec3' },
1418
+ ],
1419
+ instanced: false,
1420
+ },
1421
+ })
1422
+ })
1423
+
700
1424
  it('should handle interleaved attributes in schema', () => {
701
1425
  const vertexData = interleave('vertexData', [
702
1426
  attribute.vec2('a_position'),
@@ -718,13 +1442,7 @@ describe('compile', () => {
718
1442
  }
719
1443
  `
720
1444
 
721
- const fragment = glsl`
722
- void main() {
723
- gl_FragColor = vec4(1.0);
724
- }
725
- `
726
-
727
- const result = compile(gl, vertex, fragment)
1445
+ const result = compile(gl, vertex, glsl``)
728
1446
 
729
1447
  expect(result.schema.interleavedAttributes).toEqual({
730
1448
  vertexData: {