@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.
@@ -17,6 +17,7 @@ export interface MaterialArtifactConflictError {
17
17
  readonly hint: string;
18
18
  readonly detail: {
19
19
  readonly key: string;
20
+ readonly dimension: 'bytes' | 'param-schema';
20
21
  readonly existingDigest?: string;
21
22
  readonly incomingDigest?: string;
22
23
  };
@@ -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;AAMD,qBAAa,wBAAwB;;IAGnC,QAAQ,CACN,QAAQ,EAAE,uBAAuB,GAChC,MAAM,CAAC,uBAAuB,EAAE,6BAA6B,CAAC;IAoBjE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,uBAAuB,GAAG,SAAS;IAIrD,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,0BAA0B,GAAG,SAAS;CAU7D"}
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.24",
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.24",
26
- "@forgeax/engine-types": "0.1.24",
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.24"
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
- expect(text, `${file} must name coordinate metadata`).toMatch(/CoordinatesMetadata/);
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 = fs.readFileSync(path.resolve(srcDir, 'default-standard-pbr.wgsl'), 'utf8');
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
- 'baseColorSampler',
80
+ 'baseColorTexture_sampler',
81
81
  'baseColorTexture',
82
- 'metallicRoughnessSampler',
82
+ 'metallicRoughnessTexture_sampler',
83
83
  'metallicRoughnessTexture',
84
- 'normalSampler',
84
+ 'normalTexture_sampler',
85
85
  'normalTexture',
86
- 'emissiveSampler',
86
+ 'emissiveTexture_sampler',
87
87
  'emissiveTexture',
88
- 'occlusionSampler',
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 = text.indexOf(`var ${name}`);
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) baseColorSampler sampler
70
+ // @group(1) @binding(1) baseColorTexture_sampler sampler
71
71
  // @group(1) @binding(2) baseColorTexture texture_2d<f32>
72
- // @group(1) @binding(3) metallicRoughnessSampler sampler
72
+ // @group(1) @binding(3) metallicRoughnessTexture_sampler sampler
73
73
  // @group(1) @binding(4) metallicRoughnessTexture texture_2d<f32>
74
- // @group(1) @binding(5) normalSampler sampler
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 baseColorSampler : sampler;
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 metallicRoughnessSampler : sampler;
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 normalSampler : sampler;
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 emissiveSampler : sampler;
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 occlusionSampler : sampler;
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, baseColorSampler, vec2<f32>(0.0));
189
- let metallicRoughnessWitness = textureSample(metallicRoughness, metallicRoughnessSampler, vec2<f32>(0.0));
190
- let normalWitness = textureSample(normal, normalSampler, vec2<f32>(0.0));
191
- let emissiveWitness = textureSample(emissive, emissiveSampler, vec2<f32>(0.0));
192
- let occlusionWitness = textureSample(occlusion, occlusionSampler, vec2<f32>(0.0));
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
- baseColorSampler,
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) baseColorSampler sampler
86
+ // @group(1) @binding(1) baseColorTexture_sampler sampler
87
87
  // @group(1) @binding(2) baseColorTexture texture_2d<f32>
88
- // @group(1) @binding(3) metallicRoughnessSampler sampler
88
+ // @group(1) @binding(3) metallicRoughnessTexture_sampler sampler
89
89
  // @group(1) @binding(4) metallicRoughnessTexture texture_2d<f32>
90
- // @group(1) @binding(5) normalSampler sampler
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 baseColorSampler : sampler;
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 metallicRoughnessSampler : sampler;
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 normalSampler : sampler;
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 emissiveSampler : sampler;
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 occlusionSampler : sampler;
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, baseColorSampler, vec2<f32>(0.0));
213
- let metallicRoughnessWitness = textureSample(metallicRoughness, metallicRoughnessSampler, vec2<f32>(0.0));
214
- let normalWitness = textureSample(normal, normalSampler, vec2<f32>(0.0));
215
- let emissiveWitness = textureSample(emissive, emissiveSampler, vec2<f32>(0.0));
216
- let occlusionWitness = textureSample(occlusion, occlusionSampler, vec2<f32>(0.0));
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
- baseColorSampler,
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
- baseColorSampler,
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
- metallicRoughnessSampler,
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
- normalSampler,
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
- emissiveSampler,
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
- occlusionSampler,
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 passes.length > 0 && passes.every((pass) => isEngineMaterialModule(pass.program.module));
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: [{ name: 'forward', program: { module: BUILTIN_MATERIAL_MODULES[kind] } }],
179
- parameters: BUILTIN_PARAMETERS[kind],
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)) return ok(previous);
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
+ }