@forgeax/engine-shader 0.1.23 → 0.1.25
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/README.md +110 -4
- package/dist/ShaderRegistry.d.ts.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +59 -55
- package/dist/index.mjs.map +1 -1
- package/dist/material/artifact-registry.d.ts +1 -0
- package/dist/material/artifact-registry.d.ts.map +1 -1
- package/dist/material-schemas.d.ts +31 -0
- package/dist/material-schemas.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/ShaderRegistry.ts +0 -17
- package/src/__tests__/builtin-texture-sampling-contract.test.ts +10 -1
- package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +9 -3
- package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +48 -166
- package/src/__tests__/deferred-lighting-ssao.test.ts +7 -4
- package/src/__tests__/ibl-irradiance.unit.test.ts +9 -0
- package/src/__tests__/lighting-directional.unit.test.ts +36 -0
- package/src/__tests__/lighting-punctual.unit.test.ts +12 -4
- package/src/__tests__/material-builtins.unit.test.ts +21 -3
- package/src/__tests__/material-contract.unit.test.ts +131 -17
- package/src/__tests__/physical-clearcoat.unit.test.ts +65 -0
- package/src/__tests__/probe-lighting-composition.unit.test.ts +3 -2
- package/src/__tests__/shader-manifest-transmission.unit.test.ts +8 -10
- package/src/__tests__/shader.unit.test.ts +52 -87
- package/src/__tests__/spot-cookie-composition.unit.test.ts +2 -1
- package/src/__tests__/standard-pbr-webgl2-interface.unit.test.ts +33 -0
- package/src/__tests__/standard-physical-layer.unit.test.ts +16 -0
- package/src/__tests__/standard-surface-pass-evaluation.integration.test.ts +38 -0
- package/src/__tests__/standard-webgl2-varying-budget.unit.test.ts +17 -0
- package/src/__tests__/surface-lighting-provenance.integration.test.ts +59 -0
- package/src/__tests__/surface-pass-templates.integration.test.ts +28 -0
- package/src/__tests__/surface-v1-negative.unit.test.ts +37 -0
- package/src/__tests__/surface-v1.unit.test.ts +83 -0
- package/src/__tests__/transmission-thickness-scale.unit.test.ts +2 -6
- package/src/__tests__/vertex-color-variant.unit.test.ts +16 -5
- package/src/default-standard-pbr-skin.wgsl +704 -192
- package/src/default-standard-pbr.wgsl +563 -279
- package/src/default_standard_surface.wgsl +88 -0
- package/src/hdrp-cluster-forward.wgsl +225 -0
- package/src/ibl-irradiance.wgsl +43 -8
- package/src/ibl-prefilter.wgsl +13 -3
- package/src/ibl-shared.wgsl +9 -2
- package/src/index.ts +37 -3
- package/src/lighting-directional.wgsl +3 -1
- package/src/lighting-spot-modifiers.wgsl +8 -1
- package/src/lighting-spot-projector.wgsl +31 -0
- package/src/material/artifact-registry.ts +20 -1
- package/src/material/artifact-types.ts +2 -2
- package/src/material/physical/anisotropy.wgsl +26 -0
- package/src/material/physical/clearcoat.wgsl +24 -0
- package/src/material/physical/iridescence.wgsl +23 -0
- package/src/material/physical/sheen.wgsl +17 -0
- package/src/material/standard-physical-layer.wgsl +12 -0
- package/src/material-schemas.ts +65 -30
- package/src/register-default-standard-pbr-skin.ts +2 -2
- package/src/shadow_caster.wgsl +80 -6
- package/src/standard-cluster.wgsl +1 -36
- package/src/surface_v1.wgsl +26 -0
- package/src/unlit.wgsl +12 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
function source(name: string): string {
|
|
6
|
+
return readFileSync(fileURLToPath(new URL(`../${name}`, import.meta.url)), 'utf8');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('Standard Surface lighting provenance', () => {
|
|
10
|
+
const provenance = {
|
|
11
|
+
pass: 'forward',
|
|
12
|
+
module: 'game_3d::rusted_iron_surface',
|
|
13
|
+
closure: 'forgeax_material::surface_v1',
|
|
14
|
+
artifact: 'inline-wgsl:surface-standard-v1',
|
|
15
|
+
cook: 'createShaderModule',
|
|
16
|
+
generation: 1,
|
|
17
|
+
backend: 'webgpu',
|
|
18
|
+
lane: 'direct',
|
|
19
|
+
head: 'feature-head:m3-gpu-provenance',
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
it('keeps one selected Surface closure across lighting pass adapters', () => {
|
|
23
|
+
const forward = source('default-standard-pbr.wgsl');
|
|
24
|
+
const skinned = source('default-standard-pbr-skin.wgsl');
|
|
25
|
+
const shadow = source('shadow_caster.wgsl');
|
|
26
|
+
for (const shader of [forward, skinned, shadow]) {
|
|
27
|
+
expect(shader).toContain('#import forgeax_material::surface_v1');
|
|
28
|
+
expect(shader).toContain('forgeax_material::slot::surface');
|
|
29
|
+
}
|
|
30
|
+
expect(forward).toContain('fn fs_main');
|
|
31
|
+
expect(skinned).toContain('fn fs_gbuffer');
|
|
32
|
+
expect(shadow).toContain('fn fs_shadow');
|
|
33
|
+
expect(provenance.closure).toBe('forgeax_material::surface_v1');
|
|
34
|
+
expect(provenance.generation).toBeGreaterThan(0);
|
|
35
|
+
expect(provenance.head).toMatch(/^feature-head:/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('keeps direct, clustered, shadow, and controlled feature owners observable', () => {
|
|
39
|
+
const pbr = source('default-standard-pbr.wgsl');
|
|
40
|
+
expect(pbr).toContain('evalDirectional');
|
|
41
|
+
expect(pbr).toContain('evalPoint');
|
|
42
|
+
expect(pbr).toContain('evalSpot');
|
|
43
|
+
expect(pbr).toContain('fs_gbuffer');
|
|
44
|
+
expect(pbr).toContain('sampleIbl');
|
|
45
|
+
expect(pbr).toContain('evaluateStandardSurface');
|
|
46
|
+
expect(pbr).toContain('alphaClipThreshold');
|
|
47
|
+
// Volume integration is a post-lighting producer. Standard must not
|
|
48
|
+
// reconstruct or apply a private analytic fog ray in any pass.
|
|
49
|
+
expect(pbr).not.toContain('apply_fog');
|
|
50
|
+
expect(pbr).not.toContain('applySceneFog');
|
|
51
|
+
expect(pbr).not.toMatch(/forwardFallback|deferredFallback/);
|
|
52
|
+
expect(provenance.pass).toBe('forward');
|
|
53
|
+
expect(provenance.module).toBe('game_3d::rusted_iron_surface');
|
|
54
|
+
expect(provenance.artifact).toContain('inline-wgsl:');
|
|
55
|
+
expect(provenance.cook).toBe('createShaderModule');
|
|
56
|
+
expect(provenance.backend).toBe('webgpu');
|
|
57
|
+
expect(provenance.lane).toBe('direct');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
function source(name: string): string {
|
|
6
|
+
return readFileSync(fileURLToPath(new URL(`../${name}`, import.meta.url)), 'utf8');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('Standard Surface pass templates', () => {
|
|
10
|
+
it('imports the same Surface ABI in rigid and skinned templates', () => {
|
|
11
|
+
for (const name of ['default-standard-pbr.wgsl', 'default-standard-pbr-skin.wgsl']) {
|
|
12
|
+
const shader = source(name);
|
|
13
|
+
expect(shader).toContain('#pragma material_slot surface');
|
|
14
|
+
expect(shader).toContain('#import forgeax_material::surface_v1');
|
|
15
|
+
expect(shader).toContain('evaluate_surface');
|
|
16
|
+
expect(shader).toContain('fs_gbuffer');
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('keeps alpha clip in the shared shadow template without vertex mutation', () => {
|
|
21
|
+
const shader = source('shadow_caster.wgsl');
|
|
22
|
+
expect(shader).toContain('#pragma material_slot surface');
|
|
23
|
+
expect(shader).toContain('#import forgeax_material::surface_v1');
|
|
24
|
+
expect(shader).toContain('alphaClipThreshold');
|
|
25
|
+
expect(shader).toContain('fn fs_shadow');
|
|
26
|
+
expect(shader).not.toMatch(/\bin\.position\s*=|\bin\.positionWS\s*=/);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
|
|
10
|
+
describe('Surface v1 negative contract', () => {
|
|
11
|
+
it.each([
|
|
12
|
+
['missing export', 'struct SurfaceInput {}'],
|
|
13
|
+
[
|
|
14
|
+
'wrong signature',
|
|
15
|
+
'fn evaluate_surface(input: SurfaceInput) -> vec4<f32> { return vec4<f32>(); }',
|
|
16
|
+
],
|
|
17
|
+
])('rejects %s', (_name, source) => {
|
|
18
|
+
expect(source).not.toMatch(
|
|
19
|
+
/fn\s+evaluate_surface\s*\(input\s*:\s*SurfaceInput\s*\)\s*->\s*SurfaceData\s*\{/,
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it.each([
|
|
24
|
+
['fragment entry', '@fragment fn fs_main() -> @location(0) vec4<f32> { return vec4<f32>(); }'],
|
|
25
|
+
['resource binding', '@group(1) @binding(0) var surfaceTexture: texture_2d<f32>;'],
|
|
26
|
+
[
|
|
27
|
+
'vertex mutation',
|
|
28
|
+
'fn evaluate_surface(input: SurfaceInput) -> SurfaceData { input.positionWS = vec3<f32>(); }',
|
|
29
|
+
],
|
|
30
|
+
])('rejects %s', (_name, source) => {
|
|
31
|
+
expect(source).toMatch(/@(fragment|group|binding)|positionWS\s*=/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('keeps physical fields out of the versioned base ABI', () => {
|
|
35
|
+
expect(surfaceSource).not.toMatch(/clearcoat|clearcoatRoughness|anisotropy|sheen|iridescence/);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -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(
|
|
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\(
|
|
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
|
-
|
|
42
|
-
|
|
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('
|
|
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\(
|
|
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
|
});
|