@forgeax/engine-shader 0.1.23 → 0.1.24

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 (48) hide show
  1. package/README.md +13 -4
  2. package/dist/ShaderRegistry.d.ts.map +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.mjs +28 -52
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/material-schemas.d.ts +31 -0
  8. package/dist/material-schemas.d.ts.map +1 -1
  9. package/package.json +4 -4
  10. package/src/ShaderRegistry.ts +0 -17
  11. package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +9 -3
  12. package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +48 -166
  13. package/src/__tests__/deferred-lighting-ssao.test.ts +3 -3
  14. package/src/__tests__/ibl-irradiance.unit.test.ts +9 -0
  15. package/src/__tests__/lighting-directional.unit.test.ts +36 -0
  16. package/src/__tests__/lighting-punctual.unit.test.ts +12 -4
  17. package/src/__tests__/material-builtins.unit.test.ts +21 -3
  18. package/src/__tests__/material-contract.unit.test.ts +131 -17
  19. package/src/__tests__/physical-clearcoat.unit.test.ts +65 -0
  20. package/src/__tests__/probe-lighting-composition.unit.test.ts +3 -2
  21. package/src/__tests__/shader-manifest-transmission.unit.test.ts +8 -10
  22. package/src/__tests__/shader.unit.test.ts +52 -87
  23. package/src/__tests__/spot-cookie-composition.unit.test.ts +2 -1
  24. package/src/__tests__/standard-webgl2-varying-budget.unit.test.ts +17 -0
  25. package/src/__tests__/surface-v1-negative.unit.test.ts +37 -0
  26. package/src/__tests__/surface-v1.unit.test.ts +83 -0
  27. package/src/__tests__/transmission-thickness-scale.unit.test.ts +2 -6
  28. package/src/__tests__/vertex-color-variant.unit.test.ts +16 -5
  29. package/src/default-standard-pbr-skin.wgsl +678 -192
  30. package/src/default-standard-pbr.wgsl +563 -279
  31. package/src/default_standard_surface.wgsl +88 -0
  32. package/src/hdrp-cluster-forward.wgsl +225 -0
  33. package/src/ibl-irradiance.wgsl +43 -8
  34. package/src/ibl-prefilter.wgsl +13 -3
  35. package/src/ibl-shared.wgsl +9 -2
  36. package/src/index.ts +6 -0
  37. package/src/lighting-directional.wgsl +3 -1
  38. package/src/lighting-spot-modifiers.wgsl +8 -1
  39. package/src/lighting-spot-projector.wgsl +31 -0
  40. package/src/material/artifact-types.ts +2 -2
  41. package/src/material/physical/anisotropy.wgsl +26 -0
  42. package/src/material/physical/clearcoat.wgsl +24 -0
  43. package/src/material/physical/iridescence.wgsl +23 -0
  44. package/src/material/physical/sheen.wgsl +17 -0
  45. package/src/material-schemas.ts +65 -30
  46. package/src/register-default-standard-pbr-skin.ts +2 -2
  47. package/src/standard-cluster.wgsl +1 -36
  48. package/src/surface_v1.wgsl +26 -0
@@ -0,0 +1,83 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ const surfaceSource = readFileSync(
6
+ fileURLToPath(new URL('../surface_v1.wgsl', import.meta.url)),
7
+ 'utf8',
8
+ );
9
+ const defaultSource = readFileSync(
10
+ fileURLToPath(new URL('../default_standard_surface.wgsl', import.meta.url)),
11
+ 'utf8',
12
+ );
13
+
14
+ const inputFields = [
15
+ ['positionOS', 'vec3<f32>'],
16
+ ['positionWS', 'vec3<f32>'],
17
+ ['geometricNormalWS', 'vec3<f32>'],
18
+ ['tangentWS', 'vec4<f32>'],
19
+ ['viewDirectionWS', 'vec3<f32>'],
20
+ ['uv0', 'vec2<f32>'],
21
+ ['uv1', 'vec2<f32>'],
22
+ ['vertexColor', 'vec4<f32>'],
23
+ ['frontFacing', 'bool'],
24
+ ] as const;
25
+
26
+ const dataFields = [
27
+ ['baseColor', 'vec3<f32>'],
28
+ ['normalWS', 'vec3<f32>'],
29
+ ['metallic', 'f32'],
30
+ ['roughness', 'f32'],
31
+ ['emissive', 'vec3<f32>'],
32
+ ['occlusion', 'f32'],
33
+ ['opacity', 'f32'],
34
+ ['alphaClipThreshold', 'f32'],
35
+ ] as const;
36
+
37
+ function assertOrderedFields(source: string, fields: readonly (readonly [string, string])[]) {
38
+ let previous = -1;
39
+ for (const [name, type] of fields) {
40
+ const index = new RegExp(`${name}\\s*:\\s*${type.replace(/[<>]/g, '\\$&')}`).exec(
41
+ source,
42
+ )?.index;
43
+ expect(index, `Surface ABI must contain ${name}: ${type}`).toBeGreaterThan(previous);
44
+ previous = index ?? previous;
45
+ }
46
+ }
47
+
48
+ describe('surface_v1 ABI', () => {
49
+ it('publishes one versioned base-only ABI in stable field order', () => {
50
+ expect(surfaceSource).toContain('#define_import_path forgeax_material::surface_v1');
51
+ expect(surfaceSource).toContain('struct SurfaceInput');
52
+ expect(surfaceSource).toContain('struct SurfaceData');
53
+ assertOrderedFields(surfaceSource, inputFields);
54
+ assertOrderedFields(surfaceSource, dataFields);
55
+ expect(surfaceSource).not.toMatch(/clearcoat|anisotropy|sheen|iridescence|specular/);
56
+ });
57
+
58
+ it('keeps authored Surface entry points free of stage and resource ownership', () => {
59
+ const authoredSource = `
60
+ #import forgeax_material::surface_v1::{SurfaceInput, SurfaceData}
61
+ fn evaluate_surface(input : SurfaceInput) -> SurfaceData {
62
+ return SurfaceData(vec3<f32>(1.0), input.geometricNormalWS, 0.0, 0.5, vec3<f32>(0.0), 1.0, 1.0, 0.0);
63
+ }
64
+ `;
65
+ expect(authoredSource).toMatch(
66
+ /fn\s+evaluate_surface\s*\(input\s*:\s*SurfaceInput\s*\)\s*->\s*SurfaceData/,
67
+ );
68
+ expect(authoredSource.match(/fn\s+evaluate_surface\s*\(/g)).toHaveLength(1);
69
+ expect(authoredSource).not.toMatch(/@(fragment|vertex|compute)|@(group|binding)\s*\(/);
70
+ expect(surfaceSource).not.toMatch(/@(fragment|vertex|compute)|@(group|binding)\s*\(/);
71
+ });
72
+
73
+ it('keeps the default evaluator in the same Surface ABI family', () => {
74
+ expect(defaultSource).toContain(
75
+ '#define_import_path forgeax_material::default_standard_surface',
76
+ );
77
+ expect(defaultSource).toMatch(
78
+ /fn\s+evaluate_surface\s*\(input\s*:\s*SurfaceInput\s*\)\s*->\s*SurfaceData/,
79
+ );
80
+ expect(defaultSource).toContain('input.geometricNormalWS');
81
+ expect(defaultSource).not.toMatch(/@(fragment|vertex|compute)|@(group|binding)\s*\(/);
82
+ });
83
+ });
@@ -6,16 +6,12 @@ const shader = readFileSync(resolve(import.meta.dirname, '../default-standard-pb
6
6
 
7
7
  describe('standard transmission thickness scale contract', () => {
8
8
  it('converts local authored thickness through the combined local-to-world basis', () => {
9
- expect(shader).toContain('let localToWorld = entityWorld * instanceLocal;');
10
9
  expect(shader).toContain('@location(4) @interpolate(flat) transmissionBasis0 : vec4<f32>');
11
- expect(shader).toContain('@location(15) @interpolate(flat) transmissionBasis1 : vec4<f32>');
10
+ expect(shader).toContain('@location(13) @interpolate(flat) transmissionBasis1 : vec4<f32>');
11
+ expect(shader).not.toContain('@location(15)');
12
12
  expect(shader).toContain('let localToWorld0 = in.transmissionBasis0.xyz;');
13
13
  expect(shader).toContain('let localToWorld1 = vec3<f32>(');
14
14
  expect(shader).toContain('let localToWorld2 = vec3<f32>(');
15
- expect(shader).toContain('in.ndc.w,');
16
- expect(shader).not.toContain(
17
- 'let localToWorld = meshes[0].worldFromLocal * instances[in.instanceIdx].localFromInstance;',
18
- );
19
15
  expect(shader).toContain('let worldToLocal0 = vec3<f32>(');
20
16
  expect(shader).toContain('let worldRefractedDirection =');
21
17
  expect(shader).toMatch(
@@ -23,7 +23,7 @@ describe('M4 vertex-color shader contract', () => {
23
23
  /#ifdef\s+VERTEX_COLOR_AVAILABLE[\s\S]*?@location\(14\)\s+color\s*:\s*vec4<f32>/,
24
24
  );
25
25
  if (file !== 'unlit.wgsl') {
26
- expect(text).toMatch(/@location\(13\)\s+uv7\s*:\s*vec2<f32>/);
26
+ expect(text).toMatch(/@location\(12\)\s+uv6And7\s*:\s*vec4<f32>/);
27
27
  }
28
28
  });
29
29
 
@@ -38,15 +38,26 @@ describe('M4 vertex-color shader contract', () => {
38
38
 
39
39
  it.each(SOURCES)('%s applies vertex color to linear RGB and all alpha consumers', (file) => {
40
40
  const text = source(file);
41
- expect(text).toMatch(/baseColor\.rgb\s*\*\s*(?:baseSample|texSample)\.rgb\s*\*\s*\w+\.rgb/);
42
- expect(text).toMatch(/baseColor\.a\s*\*\s*(?:baseSample|texSample)\.a\s*\*\s*\w+\.a/);
41
+ if (file === 'unlit.wgsl') {
42
+ expect(text).toMatch(/baseColor\.rgb\s*\*\s*texSample\.rgb\s*\*\s*vertexColor\.rgb/);
43
+ expect(text).toMatch(/baseColor\.a\s*\*\s*texSample\.a\s*\*\s*vertexColor\.a/);
44
+ } else {
45
+ expect(text).toContain('#import forgeax_material::slot::surface::{evaluate_surface}');
46
+ }
43
47
  expect(text).toMatch(/alphaCutoff/);
44
48
  expect(text).toMatch(/fs_temporal/);
45
49
  });
46
50
 
47
- it('keeps the existing inter-stage uv7 location while reserving color at 14', () => {
51
+ it('canonical Standard surface applies vertex color to linear RGB and alpha', () => {
52
+ const text = readFileSync(new URL('../default_standard_surface.wgsl', import.meta.url), 'utf8');
53
+ expect(text).toMatch(/baseColor\.rgb\s*\*\s*baseSample\.rgb\s*\*\s*vertexColor\.rgb/);
54
+ expect(text).toMatch(/baseColor\.a\s*\*\s*baseSample\.a\s*\*\s*vertexColor\.a/);
55
+ });
56
+
57
+ it('packs the high UV pair while reserving color at 14', () => {
48
58
  const text = source('default-standard-pbr.wgsl');
49
- expect(text).toMatch(/@location\(13\)\s+uv7\s*:\s*vec2<f32>/);
59
+ expect(text).toMatch(/@location\(12\)\s+uv6And7\s*:\s*vec4<f32>/);
60
+ expect(text).toContain('in.uv6And7.zw');
50
61
  expect(text).toMatch(/@location\(14\)\s+color\s*:\s*vec4<f32>/);
51
62
  });
52
63
  });