@forgeax/engine-shader 0.1.24 → 0.1.26
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 +97 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +35 -7
- 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/package.json +4 -4
- package/src/__tests__/builtin-texture-sampling-contract.test.ts +10 -1
- package/src/__tests__/deferred-lighting-ssao.test.ts +4 -1
- package/src/__tests__/material-builtins.unit.test.ts +6 -6
- 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__/surface-lighting-provenance.integration.test.ts +59 -0
- package/src/__tests__/surface-pass-templates.integration.test.ts +28 -0
- package/src/default-standard-pbr-skin.wgsl +40 -14
- package/src/default-standard-pbr.wgsl +14 -14
- package/src/default_standard_surface.wgsl +5 -5
- package/src/index.ts +36 -4
- package/src/material/artifact-registry.ts +20 -1
- package/src/material/standard-physical-layer.wgsl +12 -0
- package/src/opaque_surface.wgsl +9 -0
- package/src/shadow_caster.wgsl +80 -6
- package/src/unlit.wgsl +12 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"artifact-registry.d.ts","sourceRoot":"","sources":["../../src/material/artifact-registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,IAAI,EAAE,4BAA4B,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE;QACf,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QACjC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;KAClC,CAAC;CACH;
|
|
1
|
+
{"version":3,"file":"artifact-registry.d.ts","sourceRoot":"","sources":["../../src/material/artifact-registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,IAAI,EAAE,4BAA4B,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE;QACf,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,SAAS,EAAE,OAAO,GAAG,cAAc,CAAC;QAC7C,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QACjC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;KAClC,CAAC;CACH;AAUD,qBAAa,wBAAwB;;IAGnC,QAAQ,CACN,QAAQ,EAAE,uBAAuB,GAChC,MAAM,CAAC,uBAAuB,EAAE,6BAA6B,CAAC;IAkCjE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,uBAAuB,GAAG,SAAS;IAIrD,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,0BAA0B,GAAG,SAAS;CAU7D"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-shader",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
"LICENSE"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@forgeax/engine-rhi": "0.1.
|
|
26
|
-
"@forgeax/engine-types": "0.1.
|
|
25
|
+
"@forgeax/engine-rhi": "0.1.26",
|
|
26
|
+
"@forgeax/engine-types": "0.1.26",
|
|
27
27
|
"@webgpu/types": "^0.1.71"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@forgeax/engine-vite-plugin-shader": "0.1.
|
|
30
|
+
"@forgeax/engine-vite-plugin-shader": "0.1.26"
|
|
31
31
|
},
|
|
32
32
|
"forgeax": {
|
|
33
33
|
"metrics": {
|
|
@@ -49,7 +49,16 @@ describe('built-in texture coordinate records', () => {
|
|
|
49
49
|
it('keeps custom coordinate metadata distinct from texture resources', () => {
|
|
50
50
|
for (const file of shaderFiles) {
|
|
51
51
|
const text = source(file);
|
|
52
|
-
|
|
52
|
+
if (file === 'default-standard-pbr-skin.wgsl') {
|
|
53
|
+
// Standard templates are parameter-agnostic inputs. The compiler
|
|
54
|
+
// lowers the generated coordinate metadata together with the selected
|
|
55
|
+
// Surface, so the skinned template only needs to expose that seam.
|
|
56
|
+
expect(text, `${file} must delegate material facts to Surface`).toContain(
|
|
57
|
+
'evaluate_surface',
|
|
58
|
+
);
|
|
59
|
+
} else {
|
|
60
|
+
expect(text, `${file} must name coordinate metadata`).toMatch(/CoordinatesMetadata/);
|
|
61
|
+
}
|
|
53
62
|
expect(text, `${file} must preserve texture resources`).toMatch(/texture_2d|texture_cube/);
|
|
54
63
|
}
|
|
55
64
|
});
|
|
@@ -39,7 +39,10 @@ beforeAll(async () => {
|
|
|
39
39
|
const url = (await import(/* @vite-ignore */ urlId)) as NodeUrl;
|
|
40
40
|
const here = url.fileURLToPath(import.meta.url);
|
|
41
41
|
const srcDir = path.resolve(path.dirname(here), '..');
|
|
42
|
-
pbrSource =
|
|
42
|
+
pbrSource = [
|
|
43
|
+
fs.readFileSync(path.resolve(srcDir, 'default-standard-pbr.wgsl'), 'utf8'),
|
|
44
|
+
fs.readFileSync(path.resolve(srcDir, 'default_standard_surface.wgsl'), 'utf8'),
|
|
45
|
+
].join('\n');
|
|
43
46
|
});
|
|
44
47
|
|
|
45
48
|
function stripComments(src: string): string {
|
|
@@ -77,15 +77,15 @@ describe('built-in MaterialAsset sources', () => {
|
|
|
77
77
|
] as const)('keeps the Standard user-region ABI before Skylight in %s', (file) => {
|
|
78
78
|
const text = source(file);
|
|
79
79
|
const bindings = [
|
|
80
|
-
'
|
|
80
|
+
'baseColorTexture_sampler',
|
|
81
81
|
'baseColorTexture',
|
|
82
|
-
'
|
|
82
|
+
'metallicRoughnessTexture_sampler',
|
|
83
83
|
'metallicRoughnessTexture',
|
|
84
|
-
'
|
|
84
|
+
'normalTexture_sampler',
|
|
85
85
|
'normalTexture',
|
|
86
|
-
'
|
|
86
|
+
'emissiveTexture_sampler',
|
|
87
87
|
'emissiveTexture',
|
|
88
|
-
'
|
|
88
|
+
'occlusionTexture_sampler',
|
|
89
89
|
'occlusionTexture',
|
|
90
90
|
'transmissionSampler',
|
|
91
91
|
'transmissionTexture',
|
|
@@ -95,7 +95,7 @@ describe('built-in MaterialAsset sources', () => {
|
|
|
95
95
|
];
|
|
96
96
|
let previous = -1;
|
|
97
97
|
for (const name of bindings) {
|
|
98
|
-
const index =
|
|
98
|
+
const index = new RegExp(`\\bvar\\s+${name}\\s*:`).exec(text)?.index ?? -1;
|
|
99
99
|
expect(index, `${file} must declare ${name}`).toBeGreaterThan(previous);
|
|
100
100
|
previous = index;
|
|
101
101
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const standardPbrSource = readFileSync(
|
|
6
|
+
fileURLToPath(new URL('../default-standard-pbr.wgsl', import.meta.url)),
|
|
7
|
+
'utf8',
|
|
8
|
+
);
|
|
9
|
+
const standardPbrSkinSource = readFileSync(
|
|
10
|
+
fileURLToPath(new URL('../default-standard-pbr-skin.wgsl', import.meta.url)),
|
|
11
|
+
'utf8',
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
describe('standard PBR WebGL2 interface', () => {
|
|
15
|
+
it('packs the non-transmission position and view depth into one varying', () => {
|
|
16
|
+
for (const source of [standardPbrSource, standardPbrSkinSource]) {
|
|
17
|
+
expect(source).toContain('@location(7) positionOSAndViewZ : vec4<f32>');
|
|
18
|
+
expect(source).not.toContain('@location(4) positionOS : vec3<f32>');
|
|
19
|
+
expect(source).not.toContain('@location(15) positionOS : vec3<f32>');
|
|
20
|
+
expect(source).not.toContain('@location(4) @interpolate(flat) instanceIdx : u32');
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('retains the compact transmission basis locations for adapters that advertise it', () => {
|
|
25
|
+
for (const source of [standardPbrSource, standardPbrSkinSource]) {
|
|
26
|
+
expect(source).toContain('@location(4) @interpolate(flat) transmissionBasis0 : vec4<f32>');
|
|
27
|
+
expect(source).toContain('@location(13) @interpolate(flat) transmissionBasis1 : vec4<f32>');
|
|
28
|
+
expect(source).not.toContain(
|
|
29
|
+
'@location(15) @interpolate(flat) transmissionBasis1 : vec4<f32>',
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const source = readFileSync(
|
|
6
|
+
fileURLToPath(new URL('../material/standard-physical-layer.wgsl', import.meta.url)),
|
|
7
|
+
'utf8',
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
describe('standard physical layer module', () => {
|
|
11
|
+
it('is an Engine-owned layer module after Surface evaluation', () => {
|
|
12
|
+
expect(source).toContain('standard_physical_layer');
|
|
13
|
+
expect(source).toContain('evaluate_standard_physical_layer');
|
|
14
|
+
expect(source).not.toContain('moduleSlots');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
function entryBody(sourceText: string, entry: string): string {
|
|
10
|
+
const start = sourceText.indexOf(`fn ${entry}`);
|
|
11
|
+
const end = sourceText.indexOf('\n@fragment', start + 1);
|
|
12
|
+
return sourceText.slice(start, end < 0 ? sourceText.length : end);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('Standard Surface pass evaluation', () => {
|
|
16
|
+
it('evaluates the selected Surface once in every rigid and skinned material pass', () => {
|
|
17
|
+
for (const name of ['default-standard-pbr.wgsl', 'default-standard-pbr-skin.wgsl']) {
|
|
18
|
+
const shader = source(name);
|
|
19
|
+
// ShadowCaster is a shared `shadow_caster.wgsl` material entry, while
|
|
20
|
+
// each Standard material template owns the forward and deferred entries.
|
|
21
|
+
for (const entry of ['fs_main', 'fs_gbuffer']) {
|
|
22
|
+
const body = entryBody(shader, entry);
|
|
23
|
+
expect(body, `${name}:${entry} must exist`).toContain(`fn ${entry}`);
|
|
24
|
+
expect(body, `${name}:${entry}`).toMatch(
|
|
25
|
+
/(?:evaluateStandardSurface|evaluate_surface|forgeax_evaluate_surface_from_fragments)\s*\(/,
|
|
26
|
+
);
|
|
27
|
+
expect(body).toContain('alphaTestSurface(surface)');
|
|
28
|
+
if (entry === 'fs_main') expect(body).toContain('surface.opacity');
|
|
29
|
+
expect(body).toContain('surface.baseColor');
|
|
30
|
+
expect(body).toContain('surface.normalWS');
|
|
31
|
+
expect(body).toContain('surface.metallic');
|
|
32
|
+
expect(body).toContain('surface.roughness');
|
|
33
|
+
expect(body).toContain('surface.emissive');
|
|
34
|
+
expect(body).toContain('surface.occlusion');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -67,11 +67,11 @@ fn composeProbeDiffuse(
|
|
|
67
67
|
//
|
|
68
68
|
// @group(0) @binding(0) view uniform (see common.wgsl)
|
|
69
69
|
// @group(1) @binding(0) material uniform
|
|
70
|
-
// @group(1) @binding(1)
|
|
70
|
+
// @group(1) @binding(1) baseColorTexture_sampler sampler
|
|
71
71
|
// @group(1) @binding(2) baseColorTexture texture_2d<f32>
|
|
72
|
-
// @group(1) @binding(3)
|
|
72
|
+
// @group(1) @binding(3) metallicRoughnessTexture_sampler sampler
|
|
73
73
|
// @group(1) @binding(4) metallicRoughnessTexture texture_2d<f32>
|
|
74
|
-
// @group(1) @binding(5)
|
|
74
|
+
// @group(1) @binding(5) normalTexture_sampler sampler
|
|
75
75
|
// @group(1) @binding(6) normalTexture texture_2d<f32>
|
|
76
76
|
// @group(1) @binding(9..15) Skylight (irradiance / prefilter / brdfLut +
|
|
77
77
|
// samplers) + skylight uniform (intensity)
|
|
@@ -106,15 +106,15 @@ fn composeProbeDiffuse(
|
|
|
106
106
|
// from the root ParamSchema during material composition. Keeping the ABI out
|
|
107
107
|
// of this template prevents a second handwritten interface from drifting from
|
|
108
108
|
// the runtime UBO writer.
|
|
109
|
-
@group(1) @binding(1) var
|
|
109
|
+
@group(1) @binding(1) var baseColorTexture_sampler : sampler;
|
|
110
110
|
@group(1) @binding(2) var baseColorTexture : texture_2d<f32>;
|
|
111
|
-
@group(1) @binding(3) var
|
|
111
|
+
@group(1) @binding(3) var metallicRoughnessTexture_sampler : sampler;
|
|
112
112
|
@group(1) @binding(4) var metallicRoughnessTexture : texture_2d<f32>;
|
|
113
|
-
@group(1) @binding(5) var
|
|
113
|
+
@group(1) @binding(5) var normalTexture_sampler : sampler;
|
|
114
114
|
@group(1) @binding(6) var normalTexture : texture_2d<f32>;
|
|
115
|
-
@group(1) @binding(9) var
|
|
115
|
+
@group(1) @binding(9) var emissiveTexture_sampler : sampler;
|
|
116
116
|
@group(1) @binding(10) var emissiveTexture : texture_2d<f32>;
|
|
117
|
-
@group(1) @binding(11) var
|
|
117
|
+
@group(1) @binding(11) var occlusionTexture_sampler : sampler;
|
|
118
118
|
@group(1) @binding(12) var occlusionTexture : texture_2d<f32>;
|
|
119
119
|
#ifdef TRANSMISSION_AVAILABLE
|
|
120
120
|
@group(1) @binding(13) var transmissionSampler : sampler;
|
|
@@ -185,11 +185,11 @@ fn materialTextureFilteringWitness() {
|
|
|
185
185
|
let transmission = transmissionTexture;
|
|
186
186
|
let thickness = thicknessTexture;
|
|
187
187
|
#endif
|
|
188
|
-
let baseWitness = textureSample(base,
|
|
189
|
-
let metallicRoughnessWitness = textureSample(metallicRoughness,
|
|
190
|
-
let normalWitness = textureSample(normal,
|
|
191
|
-
let emissiveWitness = textureSample(emissive,
|
|
192
|
-
let occlusionWitness = textureSample(occlusion,
|
|
188
|
+
let baseWitness = textureSample(base, baseColorTexture_sampler, vec2<f32>(0.0));
|
|
189
|
+
let metallicRoughnessWitness = textureSample(metallicRoughness, metallicRoughnessTexture_sampler, vec2<f32>(0.0));
|
|
190
|
+
let normalWitness = textureSample(normal, normalTexture_sampler, vec2<f32>(0.0));
|
|
191
|
+
let emissiveWitness = textureSample(emissive, emissiveTexture_sampler, vec2<f32>(0.0));
|
|
192
|
+
let occlusionWitness = textureSample(occlusion, occlusionTexture_sampler, vec2<f32>(0.0));
|
|
193
193
|
#ifdef CLEARCOAT_TEXTURE_AVAILABLE
|
|
194
194
|
let clearcoatWitness = textureSample(clearcoat, clearcoatSampler, vec2<f32>(0.0));
|
|
195
195
|
#endif
|
|
@@ -961,6 +961,32 @@ fn fs_main(in : VsOut, @builtin(front_facing) frontFacing : bool) -> @location(0
|
|
|
961
961
|
return vec4<f32>(color, alpha);
|
|
962
962
|
}
|
|
963
963
|
|
|
964
|
+
// Keep the skinned Standard shader on the same multi-entry pass contract as
|
|
965
|
+
// the rigid Standard shader. Deferred draws select this entry point from the
|
|
966
|
+
// existing `forgeax::pbr-skin` artifact; lighting is performed by the deferred
|
|
967
|
+
// pass after these base surface facts are written to the shared G-buffer.
|
|
968
|
+
struct GBufferOutput {
|
|
969
|
+
@location(0) normal_roughness : vec4<f32>,
|
|
970
|
+
@location(1) albedo_metallic : vec4<f32>,
|
|
971
|
+
@location(2) emissive_ao : vec4<f32>,
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
@fragment
|
|
975
|
+
fn fs_gbuffer(in : VsOut, @builtin(front_facing) frontFacing : bool) -> GBufferOutput {
|
|
976
|
+
let surface = evaluateStandardSurface(in, frontFacing);
|
|
977
|
+
alphaTestSurface(surface);
|
|
978
|
+
let albedo = surface.baseColor;
|
|
979
|
+
let metallic = clamp(surface.metallic, 0.0, 1.0);
|
|
980
|
+
let roughness = clamp(surface.roughness, 0.04, 1.0);
|
|
981
|
+
let n = normalize(surface.normalWS);
|
|
982
|
+
|
|
983
|
+
var out : GBufferOutput;
|
|
984
|
+
out.normal_roughness = vec4<f32>(n * 0.5 + 0.5, roughness);
|
|
985
|
+
out.albedo_metallic = vec4<f32>(albedo, metallic);
|
|
986
|
+
out.emissive_ao = vec4<f32>(surface.emissive, surface.occlusion);
|
|
987
|
+
return out;
|
|
988
|
+
}
|
|
989
|
+
|
|
964
990
|
struct TemporalVsOut {
|
|
965
991
|
@builtin(position) clip : vec4<f32>,
|
|
966
992
|
@location(0) uv : vec2<f32>,
|
|
@@ -1028,7 +1054,7 @@ fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
|
|
|
1028
1054
|
material.baseColor.a * temporalVertexAlpha(in),
|
|
1029
1055
|
material.alphaCutoff,
|
|
1030
1056
|
baseColorTexture,
|
|
1031
|
-
|
|
1057
|
+
baseColorTexture_sampler,
|
|
1032
1058
|
material.baseColorTextureCoordinatesTransform,
|
|
1033
1059
|
material.baseColorTextureCoordinatesMetadata,
|
|
1034
1060
|
in.currentClip,
|
|
@@ -83,11 +83,11 @@ fn reflection_probe_sampling_contract(
|
|
|
83
83
|
// + 4 channel selectors f32
|
|
84
84
|
// + emissive vec3 + emissiveIntensity
|
|
85
85
|
// + uv/alpha/specularColor = 104 B)
|
|
86
|
-
// @group(1) @binding(1)
|
|
86
|
+
// @group(1) @binding(1) baseColorTexture_sampler sampler
|
|
87
87
|
// @group(1) @binding(2) baseColorTexture texture_2d<f32>
|
|
88
|
-
// @group(1) @binding(3)
|
|
88
|
+
// @group(1) @binding(3) metallicRoughnessTexture_sampler sampler
|
|
89
89
|
// @group(1) @binding(4) metallicRoughnessTexture texture_2d<f32>
|
|
90
|
-
// @group(1) @binding(5)
|
|
90
|
+
// @group(1) @binding(5) normalTexture_sampler sampler
|
|
91
91
|
// @group(1) @binding(6) normalTexture texture_2d<f32>
|
|
92
92
|
// @group(1) @binding(9..15) Skylight (irradiance / prefilter / brdfLut +
|
|
93
93
|
// samplers) + skylight uniform (intensity)
|
|
@@ -132,15 +132,15 @@ fn reflection_probe_sampling_contract(
|
|
|
132
132
|
// from the root ParamSchema during composition. Keeping the ABI out of this
|
|
133
133
|
// template prevents a second handwritten interface from drifting from the
|
|
134
134
|
// runtime UBO writer.
|
|
135
|
-
@group(1) @binding(1) var
|
|
135
|
+
@group(1) @binding(1) var baseColorTexture_sampler : sampler;
|
|
136
136
|
@group(1) @binding(2) var baseColorTexture : texture_2d<f32>;
|
|
137
|
-
@group(1) @binding(3) var
|
|
137
|
+
@group(1) @binding(3) var metallicRoughnessTexture_sampler : sampler;
|
|
138
138
|
@group(1) @binding(4) var metallicRoughnessTexture : texture_2d<f32>;
|
|
139
|
-
@group(1) @binding(5) var
|
|
139
|
+
@group(1) @binding(5) var normalTexture_sampler : sampler;
|
|
140
140
|
@group(1) @binding(6) var normalTexture : texture_2d<f32>;
|
|
141
|
-
@group(1) @binding(9) var
|
|
141
|
+
@group(1) @binding(9) var emissiveTexture_sampler : sampler;
|
|
142
142
|
@group(1) @binding(10) var emissiveTexture : texture_2d<f32>;
|
|
143
|
-
@group(1) @binding(11) var
|
|
143
|
+
@group(1) @binding(11) var occlusionTexture_sampler : sampler;
|
|
144
144
|
@group(1) @binding(12) var occlusionTexture : texture_2d<f32>;
|
|
145
145
|
#ifdef TRANSMISSION_AVAILABLE
|
|
146
146
|
@group(1) @binding(13) var transmissionSampler : sampler;
|
|
@@ -209,11 +209,11 @@ fn materialTextureFilteringWitness() {
|
|
|
209
209
|
#ifdef CLEARCOAT_NORMAL_TEXTURE_AVAILABLE
|
|
210
210
|
let clearcoatNormal = clearcoatNormalTexture;
|
|
211
211
|
#endif
|
|
212
|
-
let baseWitness = textureSample(base,
|
|
213
|
-
let metallicRoughnessWitness = textureSample(metallicRoughness,
|
|
214
|
-
let normalWitness = textureSample(normal,
|
|
215
|
-
let emissiveWitness = textureSample(emissive,
|
|
216
|
-
let occlusionWitness = textureSample(occlusion,
|
|
212
|
+
let baseWitness = textureSample(base, baseColorTexture_sampler, vec2<f32>(0.0));
|
|
213
|
+
let metallicRoughnessWitness = textureSample(metallicRoughness, metallicRoughnessTexture_sampler, vec2<f32>(0.0));
|
|
214
|
+
let normalWitness = textureSample(normal, normalTexture_sampler, vec2<f32>(0.0));
|
|
215
|
+
let emissiveWitness = textureSample(emissive, emissiveTexture_sampler, vec2<f32>(0.0));
|
|
216
|
+
let occlusionWitness = textureSample(occlusion, occlusionTexture_sampler, vec2<f32>(0.0));
|
|
217
217
|
#ifdef CLEARCOAT_TEXTURE_AVAILABLE
|
|
218
218
|
let clearcoatWitness = textureSample(clearcoat, clearcoatSampler, vec2<f32>(0.0));
|
|
219
219
|
#endif
|
|
@@ -1263,7 +1263,7 @@ fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
|
|
|
1263
1263
|
material.baseColor.a * temporalVertexAlpha(in),
|
|
1264
1264
|
material.alphaCutoff,
|
|
1265
1265
|
baseColorTexture,
|
|
1266
|
-
|
|
1266
|
+
baseColorTexture_sampler,
|
|
1267
1267
|
material.baseColorTextureCoordinatesTransform,
|
|
1268
1268
|
material.baseColorTextureCoordinatesMetadata,
|
|
1269
1269
|
in.currentClip,
|
|
@@ -34,27 +34,27 @@ fn surfaceNormal(input : SurfaceInput, encoded : vec4<f32>, normalScale : f32) -
|
|
|
34
34
|
fn evaluate_surface(input : SurfaceInput) -> SurfaceData {
|
|
35
35
|
let baseSample = textureSample(
|
|
36
36
|
baseColorTexture,
|
|
37
|
-
|
|
37
|
+
baseColorTexture_sampler,
|
|
38
38
|
surfaceUv(input, material.baseColorTextureCoordinatesTransform, material.baseColorTextureCoordinatesMetadata) * material.baseColorTextureCoordinatesMetadata.zw,
|
|
39
39
|
);
|
|
40
40
|
let metallicRoughnessSample = textureSample(
|
|
41
41
|
metallicRoughnessTexture,
|
|
42
|
-
|
|
42
|
+
metallicRoughnessTexture_sampler,
|
|
43
43
|
surfaceUv(input, material.metallicRoughnessTextureCoordinatesTransform, material.metallicRoughnessTextureCoordinatesMetadata) * material.metallicRoughnessTextureCoordinatesMetadata.zw,
|
|
44
44
|
);
|
|
45
45
|
let normalSample = textureSample(
|
|
46
46
|
normalTexture,
|
|
47
|
-
|
|
47
|
+
normalTexture_sampler,
|
|
48
48
|
surfaceUv(input, material.normalTextureCoordinatesTransform, material.normalTextureCoordinatesMetadata) * material.normalTextureCoordinatesMetadata.zw,
|
|
49
49
|
);
|
|
50
50
|
let emissiveSample = textureSample(
|
|
51
51
|
emissiveTexture,
|
|
52
|
-
|
|
52
|
+
emissiveTexture_sampler,
|
|
53
53
|
surfaceUv(input, material.emissiveTextureCoordinatesTransform, material.emissiveTextureCoordinatesMetadata) * material.emissiveTextureCoordinatesMetadata.zw,
|
|
54
54
|
);
|
|
55
55
|
let occlusionSample = textureSample(
|
|
56
56
|
occlusionTexture,
|
|
57
|
-
|
|
57
|
+
occlusionTexture_sampler,
|
|
58
58
|
surfaceUv(input, material.occlusionTextureCoordinatesTransform, material.occlusionTextureCoordinatesMetadata) * material.occlusionTextureCoordinatesMetadata.zw,
|
|
59
59
|
);
|
|
60
60
|
let vertexColor = input.vertexColor;
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
export type { ManifestEntry, ParamSchemaEntry } from '@forgeax/engine-types';
|
|
24
24
|
|
|
25
25
|
import type { MaterialAsset, MaterialParameter, MaterialValue } from '@forgeax/engine-types';
|
|
26
|
+
import { standardSurfaceParameters } from '@forgeax/engine-types';
|
|
26
27
|
import { STANDARD_PBR_ALPHA_CUTOFF_DEFAULT } from './material-schemas.js';
|
|
27
28
|
|
|
28
29
|
export { MATERIAL_PARAM_TYPES } from '@forgeax/engine-types';
|
|
@@ -107,6 +108,14 @@ export const BUILTIN_MATERIAL_MODULES = {
|
|
|
107
108
|
sprite: 'forgeax_material::sprite',
|
|
108
109
|
} as const;
|
|
109
110
|
|
|
111
|
+
/**
|
|
112
|
+
* The engine-owned Standard Surface selected by a built-in Standard material.
|
|
113
|
+
* Keep this explicit in the authored pass so the build-time cooker sees the
|
|
114
|
+
* same slot contract as Materials.standard().
|
|
115
|
+
*/
|
|
116
|
+
export const DEFAULT_STANDARD_SURFACE_MODULE =
|
|
117
|
+
'forgeax_material::default_standard_surface' as const;
|
|
118
|
+
|
|
110
119
|
/** Stable build-time module id for the temporal-v1 accessor owner. */
|
|
111
120
|
export const SCENE_DATA_TEMPORAL_V1_SHADER_MODULE = 'forgeax_scene_temporal' as const;
|
|
112
121
|
|
|
@@ -114,7 +123,9 @@ export const SCENE_DATA_TEMPORAL_V1_SHADER_MODULE = 'forgeax_scene_temporal' as
|
|
|
114
123
|
* Material modules whose shader source and parameter contract are owned by
|
|
115
124
|
* the Engine. They live in the runtime ShaderRegistry, so a Pack containing
|
|
116
125
|
* one of these materials carries authored values only and does not require a
|
|
117
|
-
* project material-cook artifact.
|
|
126
|
+
* project material-cook artifact. A Standard pass with a project Surface slot
|
|
127
|
+
* is deliberately excluded: its root composition is project-owned and must
|
|
128
|
+
* arrive with a cooked material publication.
|
|
118
129
|
*/
|
|
119
130
|
export const ENGINE_MATERIAL_MODULES = [
|
|
120
131
|
'forgeax::default-standard-pbr',
|
|
@@ -143,7 +154,15 @@ export function isEngineMaterialModule(module: string): boolean {
|
|
|
143
154
|
|
|
144
155
|
export function isEngineMaterial(material: Pick<MaterialAsset, 'passes'>): boolean {
|
|
145
156
|
const passes = material.passes ?? [];
|
|
146
|
-
return
|
|
157
|
+
return (
|
|
158
|
+
passes.length > 0 &&
|
|
159
|
+
passes.every(
|
|
160
|
+
(pass) =>
|
|
161
|
+
isEngineMaterialModule(pass.program.module) &&
|
|
162
|
+
(pass.program.moduleSlots?.surface === undefined ||
|
|
163
|
+
pass.program.moduleSlots.surface === DEFAULT_STANDARD_SURFACE_MODULE),
|
|
164
|
+
)
|
|
165
|
+
);
|
|
147
166
|
}
|
|
148
167
|
|
|
149
168
|
export type BuiltinMaterialKind = keyof typeof BUILTIN_MATERIAL_MODULES;
|
|
@@ -175,8 +194,21 @@ const BUILTIN_VALUES: Readonly<
|
|
|
175
194
|
export function createBuiltinMaterialAsset(kind: BuiltinMaterialKind): MaterialAsset {
|
|
176
195
|
return {
|
|
177
196
|
kind: 'material',
|
|
178
|
-
passes: [
|
|
179
|
-
|
|
197
|
+
passes: [
|
|
198
|
+
{
|
|
199
|
+
name: 'forward',
|
|
200
|
+
program: {
|
|
201
|
+
module: BUILTIN_MATERIAL_MODULES[kind],
|
|
202
|
+
...(kind === 'standard'
|
|
203
|
+
? { moduleSlots: { surface: DEFAULT_STANDARD_SURFACE_MODULE } }
|
|
204
|
+
: {}),
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
parameters:
|
|
209
|
+
kind === 'standard'
|
|
210
|
+
? standardSurfaceParameters(BUILTIN_PARAMETERS[kind])
|
|
211
|
+
: BUILTIN_PARAMETERS[kind],
|
|
180
212
|
values: BUILTIN_VALUES[kind],
|
|
181
213
|
};
|
|
182
214
|
}
|
|
@@ -20,6 +20,7 @@ export interface MaterialArtifactConflictError {
|
|
|
20
20
|
readonly hint: string;
|
|
21
21
|
readonly detail: {
|
|
22
22
|
readonly key: string;
|
|
23
|
+
readonly dimension: 'bytes' | 'param-schema';
|
|
23
24
|
readonly existingDigest?: string;
|
|
24
25
|
readonly incomingDigest?: string;
|
|
25
26
|
};
|
|
@@ -29,6 +30,10 @@ function equalBytes(left: Uint8Array, right: Uint8Array): boolean {
|
|
|
29
30
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function equalParamSchema(left: MaterialRuntimeArtifact, right: MaterialRuntimeArtifact): boolean {
|
|
34
|
+
return JSON.stringify(left.metadata?.paramSchema) === JSON.stringify(right.metadata?.paramSchema);
|
|
35
|
+
}
|
|
36
|
+
|
|
32
37
|
export class MaterialArtifactRegistry {
|
|
33
38
|
readonly #artifacts = new Map<string, MaterialRuntimeArtifact>();
|
|
34
39
|
|
|
@@ -37,13 +42,27 @@ export class MaterialArtifactRegistry {
|
|
|
37
42
|
): Result<MaterialRuntimeArtifact, MaterialArtifactConflictError> {
|
|
38
43
|
const previous = this.#artifacts.get(artifact.key);
|
|
39
44
|
if (previous !== undefined) {
|
|
40
|
-
if (equalBytes(previous.bytes, artifact.bytes))
|
|
45
|
+
if (equalBytes(previous.bytes, artifact.bytes)) {
|
|
46
|
+
if (equalParamSchema(previous, artifact)) return ok(previous);
|
|
47
|
+
return err({
|
|
48
|
+
code: 'material-artifact-conflict',
|
|
49
|
+
expected: 'one immutable parameter schema per specialization key',
|
|
50
|
+
hint: 're-cook the conflicting specialization with one canonical parameter schema',
|
|
51
|
+
detail: {
|
|
52
|
+
key: artifact.key,
|
|
53
|
+
dimension: 'param-schema',
|
|
54
|
+
...(previous.digest ? { existingDigest: previous.digest } : {}),
|
|
55
|
+
...(artifact.digest ? { incomingDigest: artifact.digest } : {}),
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
41
59
|
return err({
|
|
42
60
|
code: 'material-artifact-conflict',
|
|
43
61
|
expected: 'one immutable artifact byte sequence per specialization key',
|
|
44
62
|
hint: 're-cook the conflicting specialization and publish one artifact digest',
|
|
45
63
|
detail: {
|
|
46
64
|
key: artifact.key,
|
|
65
|
+
dimension: 'bytes',
|
|
47
66
|
...(previous.digest ? { existingDigest: previous.digest } : {}),
|
|
48
67
|
...(artifact.digest ? { incomingDigest: artifact.digest } : {}),
|
|
49
68
|
},
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#define_import_path forgeax_material::standard_physical_layer
|
|
2
|
+
|
|
3
|
+
// Engine-owned physical layer facts are selected from the root StandardLayerPlan.
|
|
4
|
+
// Surface modules only produce base SurfaceData and never own this interface.
|
|
5
|
+
// The Standard template owns the facts struct; this module supplies the one
|
|
6
|
+
// physical evaluator body when the clearcoat plan is present.
|
|
7
|
+
fn evaluate_standard_physical_layer() -> StandardPhysicalLayerFacts {
|
|
8
|
+
return StandardPhysicalLayerFacts(
|
|
9
|
+
clamp(material.clearcoat, 0.0, 1.0),
|
|
10
|
+
clamp(material.clearcoatRoughness, 0.04, 1.0),
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#define_import_path forgeax_material::opaque_surface
|
|
2
|
+
#import forgeax_material::surface_v1::{SurfaceInput, SurfaceData}
|
|
3
|
+
|
|
4
|
+
// The default depth coverage requires no material values or lighting model.
|
|
5
|
+
// Cutout materials select their shared Surface explicitly on every Pass.
|
|
6
|
+
fn evaluate_surface(input: SurfaceInput) -> SurfaceData {
|
|
7
|
+
return SurfaceData(vec3<f32>(1.0), input.geometricNormalWS, 0.0, 1.0,
|
|
8
|
+
vec3<f32>(0.0), 1.0, 1.0, 0.0);
|
|
9
|
+
}
|