@forgeax/engine-shader 0.1.21 → 0.1.23

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 (59) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +4 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.mjs +33019 -8
  5. package/dist/index.mjs.map +1 -1
  6. package/dist/ltc/provenance.d.ts +17 -0
  7. package/dist/ltc/provenance.d.ts.map +1 -0
  8. package/dist/ltc/tables.d.ts +22 -0
  9. package/dist/ltc/tables.d.ts.map +1 -0
  10. package/dist/material/artifact-types.d.ts +52 -0
  11. package/dist/material/artifact-types.d.ts.map +1 -1
  12. package/dist/material-schemas.d.ts +5 -0
  13. package/dist/material-schemas.d.ts.map +1 -1
  14. package/dist/register-default-sprite-lit.d.ts +3 -6
  15. package/dist/register-default-sprite-lit.d.ts.map +1 -1
  16. package/dist/types.d.ts +2 -1
  17. package/dist/types.d.ts.map +1 -1
  18. package/package.json +4 -4
  19. package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +121 -58
  20. package/src/__tests__/direct-light-layout.unit.test.ts +40 -0
  21. package/src/__tests__/ibl-irradiance.unit.test.ts +7 -4
  22. package/src/__tests__/ibl-sampling.unit.test.ts +3 -4
  23. package/src/__tests__/lighting-point-spot-volume.unit.test.ts +8 -2
  24. package/src/__tests__/lighting-punctual.unit.test.ts +29 -2
  25. package/src/__tests__/lighting-spot-volume-attenuation.unit.test.ts +1 -1
  26. package/src/__tests__/ltc-provenance.unit.test.ts +33 -0
  27. package/src/__tests__/manifest.fixture.json +33 -0
  28. package/src/__tests__/material-contract.unit.test.ts +61 -1
  29. package/src/__tests__/probe-lighting-composition.unit.test.ts +38 -0
  30. package/src/__tests__/rect-area-brdf.unit.test.ts +36 -0
  31. package/src/__tests__/reflection-probe-sampling.unit.test.ts +24 -2
  32. package/src/__tests__/shader.unit.test.ts +6 -2
  33. package/src/__tests__/spot-cookie-composition.unit.test.ts +57 -0
  34. package/src/__tests__/spot-modifier-composition.unit.test.ts +19 -0
  35. package/src/__tests__/sprite-variants.unit.test.ts +4 -1
  36. package/src/brdf.wgsl +115 -1
  37. package/src/common.wgsl +53 -79
  38. package/src/default-standard-pbr-skin.wgsl +80 -23
  39. package/src/default-standard-pbr.wgsl +119 -50
  40. package/src/ibl-irradiance.wgsl +4 -1
  41. package/src/ibl-sampling.wgsl +2 -2
  42. package/src/index.ts +17 -0
  43. package/src/lighting-directional.wgsl +6 -116
  44. package/src/lighting-probe.wgsl +51 -0
  45. package/src/lighting-punctual.wgsl +21 -11
  46. package/src/lighting-rect-area.wgsl +162 -0
  47. package/src/lighting-spot-modifiers.wgsl +93 -0
  48. package/src/ltc/provenance.ts +19 -0
  49. package/src/ltc/tables.ts +2831 -0
  50. package/src/material/artifact-types.ts +137 -0
  51. package/src/material-schemas.ts +12 -0
  52. package/src/register-default-sprite-lit.ts +3 -6
  53. package/src/sprite-lit.wgsl +3 -3
  54. package/src/sprite.wgsl +1 -1
  55. package/src/standard-cluster.wgsl +94 -57
  56. package/src/types.ts +2 -1
  57. package/src/volume/volume-inject.wgsl +34 -36
  58. package/src/volume/volume-integrate.wgsl +48 -38
  59. package/dist/.tsbuildinfo +0 -1
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The table source is the Three.js r184 RectAreaLight implementation. The
3
+ * source arrays are converted to IEEE-754 binary16 offline and checked into
4
+ * `tables.ts`; the runtime never fetches or parses a source file.
5
+ */
6
+ export declare const LTC_SOURCE_PROVENANCE: {
7
+ readonly source: "Three.js";
8
+ readonly revision: "r184";
9
+ readonly sourceUrl: "https://github.com/mrdoob/three.js/blob/r184/examples/jsm/lights/RectAreaLightTexturesLib.js";
10
+ readonly upstreamData: "selfshadow/ltc_code fit/results/ltc.js";
11
+ readonly generator: "deterministic Three.js source-array to rgba16float conversion";
12
+ readonly inputSha256: {
13
+ readonly lambert: "cf5cf21e5c112d2095c7e2418cb0a1ac54636e275d73e42f3453646c67f26814";
14
+ readonly ggx: "3b1b09080b26104498db277c14fc1733786465c6958e7a8403d688b1e24c2ff5";
15
+ };
16
+ };
17
+ //# sourceMappingURL=provenance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provenance.d.ts","sourceRoot":"","sources":["../../src/ltc/provenance.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;CAWxB,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { LTC_SOURCE_PROVENANCE } from './provenance';
2
+ export { LTC_SOURCE_PROVENANCE };
3
+ export declare const LTC_TABLE_WIDTH = 64;
4
+ export declare const LTC_TABLE_HEIGHT = 64;
5
+ export declare const LTC_TABLES: {
6
+ readonly lambert: Uint16Array<ArrayBuffer>;
7
+ readonly ggx: Uint16Array<ArrayBuffer>;
8
+ };
9
+ export declare const LTC_TABLE_HASHES: {
10
+ readonly lambert: "a391de32f868fd4aa8774917b793174b7be804c08e2fb8924c30f31d7aa8dcd7";
11
+ readonly ggx: "fa1ecbc6deb3c85ddf603cdf1e98e30279444f905eb1cebb849d761b570dd696";
12
+ };
13
+ export declare const LTC_TABLE_INPUT_HASHES: {
14
+ readonly lambert: "cf5cf21e5c112d2095c7e2418cb0a1ac54636e275d73e42f3453646c67f26814";
15
+ readonly ggx: "3b1b09080b26104498db277c14fc1733786465c6958e7a8403d688b1e24c2ff5";
16
+ };
17
+ export declare function generateLtcTables(): {
18
+ lambert: Uint16Array;
19
+ ggx: Uint16Array;
20
+ };
21
+ export declare function hashLtcTable(table: Uint16Array): string;
22
+ //# sourceMappingURL=tables.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../src/ltc/tables.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,EAAE,qBAAqB,EAAE,CAAC;AACjC,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAorFnC,eAAO,MAAM,UAAU;;;CAAkD,CAAC;AAC1E,eAAO,MAAM,gBAAgB;;;CAGnB,CAAC;AACX,eAAO,MAAM,sBAAsB;;;CAAoC,CAAC;AAExE,wBAAgB,iBAAiB,IAAI;IAAE,OAAO,EAAE,WAAW,CAAC;IAAC,GAAG,EAAE,WAAW,CAAA;CAAE,CAE9E;AAwED,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAEvD"}
@@ -1,13 +1,65 @@
1
1
  import type { BindGroupLayoutDescriptor } from '@forgeax/engine-types';
2
+ export interface MaterialShaderResourceSlot {
3
+ readonly name: string;
4
+ readonly parameter: string;
5
+ readonly kind: 'sampler' | 'texture';
6
+ readonly group: number;
7
+ readonly binding: number;
8
+ }
9
+ export interface MaterialShaderVertexInput {
10
+ readonly semantic: string;
11
+ readonly location: number;
12
+ readonly format: string;
13
+ }
14
+ export interface MaterialShaderArtifactReceipt {
15
+ readonly directEntry: string;
16
+ readonly sceneIndexEntry: string;
17
+ readonly materialRow: {
18
+ readonly byteLength: number;
19
+ readonly fields: readonly string[];
20
+ };
21
+ readonly resourceSlots: readonly MaterialShaderResourceSlot[];
22
+ readonly uvSets: readonly {
23
+ readonly parameter: string;
24
+ readonly set: number;
25
+ }[];
26
+ readonly vertexInputs: readonly MaterialShaderVertexInput[];
27
+ readonly alphaMask: {
28
+ readonly cutoff: string;
29
+ readonly source: string;
30
+ };
31
+ readonly skinPaletteAddress?: {
32
+ readonly group: number;
33
+ readonly binding: number;
34
+ readonly stride: number;
35
+ };
36
+ readonly reflection: {
37
+ readonly layoutIdentity: string;
38
+ readonly resourceSlots: readonly MaterialShaderResourceSlot[];
39
+ readonly vertexInputs: readonly MaterialShaderVertexInput[];
40
+ };
41
+ readonly receiptIdentity: string;
42
+ readonly generation: number;
43
+ }
2
44
  export interface MaterialShaderArtifact {
3
45
  readonly material: string;
4
46
  readonly pass: string;
5
47
  readonly wgsl: string;
6
48
  readonly layoutIdentity: string;
49
+ /** Naga-reflected UV set count used to derive clamp-to-last vertex aliases. */
50
+ readonly uvSetCount?: number;
7
51
  readonly bindings: readonly BindGroupLayoutDescriptor[];
8
52
  readonly deps: readonly string[];
9
53
  readonly vertexInputs: readonly Readonly<Record<string, unknown>>[];
10
54
  readonly specializationKey?: string;
55
+ /** Producer-owned ABI facts shared by direct and scene-index consumers. */
56
+ readonly receipt?: MaterialShaderArtifactReceipt;
11
57
  }
58
+ /**
59
+ * Builds the one Standard PBR ABI receipt consumed by both material lanes.
60
+ * The skinned module uses the same material producer and only adds its palette
61
+ * address; it must retain the same receipt identity for material compatibility.
62
+ */
63
+ export declare function createStandardPbrArtifactReceipt(skinned?: boolean): MaterialShaderArtifactReceipt;
12
64
  export declare function isMaterialShaderArtifact(value: unknown): value is MaterialShaderArtifact;
13
65
  //# sourceMappingURL=artifact-types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"artifact-types.d.ts","sourceRoot":"","sources":["../../src/material/artifact-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAEvE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,SAAS,yBAAyB,EAAE,CAAC;IACxD,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,YAAY,EAAE,SAAS,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC;IACpE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,sBAAsB,CAYxF"}
1
+ {"version":3,"file":"artifact-types.d.ts","sourceRoot":"","sources":["../../src/material/artifact-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAEvE,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE;QACpB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;KACpC,CAAC;IACF,QAAQ,CAAC,aAAa,EAAE,SAAS,0BAA0B,EAAE,CAAC;IAC9D,QAAQ,CAAC,MAAM,EAAE,SAAS;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjF,QAAQ,CAAC,YAAY,EAAE,SAAS,yBAAyB,EAAE,CAAC;IAC5D,QAAQ,CAAC,SAAS,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzE,QAAQ,CAAC,kBAAkB,CAAC,EAAE;QAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,QAAQ,CAAC,UAAU,EAAE;QACnB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;QAChC,QAAQ,CAAC,aAAa,EAAE,SAAS,0BAA0B,EAAE,CAAC;QAC9D,QAAQ,CAAC,YAAY,EAAE,SAAS,yBAAyB,EAAE,CAAC;KAC7D,CAAC;IACF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,yBAAyB,EAAE,CAAC;IACxD,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,YAAY,EAAE,SAAS,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC;IACpE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,CAAC,EAAE,6BAA6B,CAAC;CAClD;AA8DD;;;;GAIG;AACH,wBAAgB,gCAAgC,CAAC,OAAO,UAAQ,GAAG,6BAA6B,CA2B/F;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,sBAAsB,CAYxF"}
@@ -1,7 +1,12 @@
1
1
  import type { ParamSchemaEntry } from '@forgeax/engine-types';
2
+ import { type MaterialShaderArtifactReceipt } from './material/artifact-types.js';
2
3
  export declare const STANDARD_PBR_ALPHA_CUTOFF_DEFAULT = 0;
3
4
  /** Shared material contract for the standard PBR and skinned PBR shaders. */
4
5
  export declare const DEFAULT_STANDARD_PBR_PARAM_SCHEMA: readonly ParamSchemaEntry[];
6
+ /** The single producer receipt shared by direct and scene-index Standard PBR. */
7
+ export declare const STANDARD_PBR_ARTIFACT_RECEIPT: MaterialShaderArtifactReceipt;
8
+ /** Skinned Standard PBR keeps the material receipt identity and adds palette ABI. */
9
+ export declare const STANDARD_PBR_SKIN_ARTIFACT_RECEIPT: MaterialShaderArtifactReceipt;
5
10
  export declare const DEFAULT_UNLIT_PARAM_SCHEMA: readonly ParamSchemaEntry[];
6
11
  export declare const DEFAULT_SPRITE_PARAM_SCHEMA: readonly ParamSchemaEntry[];
7
12
  export declare const DEFAULT_MSDF_TEXT_PARAM_SCHEMA: readonly ParamSchemaEntry[];
@@ -1 +1 @@
1
- {"version":3,"file":"material-schemas.d.ts","sourceRoot":"","sources":["../src/material-schemas.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAE9D,eAAO,MAAM,iCAAiC,IAAI,CAAC;AAEnD,6EAA6E;AAC7E,eAAO,MAAM,iCAAiC,EAAE,SAAS,gBAAgB,EA6BxE,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,SAAS,gBAAgB,EAIjE,CAAC;AAEF,eAAO,MAAM,2BAA2B,EAAE,SAAS,gBAAgB,EAMlE,CAAC;AAEF,eAAO,MAAM,8BAA8B,EAAE,SAAS,gBAAgB,EAIrE,CAAC"}
1
+ {"version":3,"file":"material-schemas.d.ts","sourceRoot":"","sources":["../src/material-schemas.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAEL,KAAK,6BAA6B,EACnC,MAAM,8BAA8B,CAAC;AAEtC,eAAO,MAAM,iCAAiC,IAAI,CAAC;AAEnD,6EAA6E;AAC7E,eAAO,MAAM,iCAAiC,EAAE,SAAS,gBAAgB,EA6BxE,CAAC;AAEF,iFAAiF;AACjF,eAAO,MAAM,6BAA6B,EAAE,6BACR,CAAC;AAErC,qFAAqF;AACrF,eAAO,MAAM,kCAAkC,EAAE,6BACT,CAAC;AAEzC,eAAO,MAAM,0BAA0B,EAAE,SAAS,gBAAgB,EAIjE,CAAC;AAEF,eAAO,MAAM,2BAA2B,EAAE,SAAS,gBAAgB,EAMlE,CAAC;AAEF,eAAO,MAAM,8BAA8B,EAAE,SAAS,gBAAgB,EAIrE,CAAC"}
@@ -22,12 +22,9 @@ export interface SpriteLitCaps {
22
22
  * compiled by @forgeax/engine-vite-plugin-shader at build time and surfaced
23
23
  * in manifest.json. The caller reads it from the manifest entries.
24
24
  *
25
- * `caps.storageBuffer` switches the runtime BGL buffer types for
26
- * @group(0) bindings 1+2 (pointLightsBuffer / spotLightsBuffer):
27
- * `false` -> uniform fallback (AC-10 WebGL2 path); `true` -> read-only-storage.
28
- * The BGL itself is built downstream in runtime/pbr-pipeline.ts (sprite-lit
29
- * reuses pbr-view / pbr-mesh-array / pbr-instances unchanged; AC-07
30
- * BGL byte-identical).
25
+ * `caps.storageBuffer` is retained for the shared mesh/instance layout
26
+ * selection. Sprite-lit local lighting reads the same Standard Cluster group
27
+ * as PBR; the view group has no direct point/spot arrays.
31
28
  *
32
29
  * @param registry ShaderRegistry instance (engine boot path).
33
30
  * @param composedWgsl Post-naga_oil composed WGSL source string.
@@ -1 +1 @@
1
- {"version":3,"file":"register-default-sprite-lit.d.ts","sourceRoot":"","sources":["../src/register-default-sprite-lit.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAiBjD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,cAAc,EACxB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,aAAa,GAClB,IAAI,CAUN"}
1
+ {"version":3,"file":"register-default-sprite-lit.d.ts","sourceRoot":"","sources":["../src/register-default-sprite-lit.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAiBjD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,cAAc,EACxB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,aAAa,GAClB,IAAI,CAUN"}
package/dist/types.d.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  * no longer carry a bindingLayout sidecar (§Change stance: no v1/v2
12
12
  * dual-path, no incremental drop window).
13
13
  */
14
+ import type { MaterialShaderArtifactReceipt } from './material/artifact-types.js';
14
15
  export interface MaterialShaderManifestVariant {
15
16
  readonly definesKey: string;
16
17
  readonly defines: Record<string, boolean>;
@@ -26,7 +27,7 @@ export interface MaterialShaderManifestVariant {
26
27
  *
27
28
  * For Engine.create() internal use only; AI users never see this type.
28
29
  */
29
- export interface MaterialShaderManifestEntry {
30
+ export interface MaterialShaderManifestEntry extends Partial<MaterialShaderArtifactReceipt> {
30
31
  readonly identifier: string;
31
32
  readonly sourcePath: string;
32
33
  readonly composedWgsl: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,6BAA6B,EAAE,CAAC;IAC5D;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,2BAA2B,EAClC,UAAU,EAAE,MAAM,GACjB,6BAA6B,GAAG,SAAS,CAE3C"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,8BAA8B,CAAC;AAClF,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,2BAA4B,SAAQ,OAAO,CAAC,6BAA6B,CAAC;IACzF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,6BAA6B,EAAE,CAAC;IAC5D;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,2BAA2B,EAClC,UAAU,EAAE,MAAM,GACjB,6BAA6B,GAAG,SAAS,CAE3C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-shader",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
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.21",
26
- "@forgeax/engine-types": "0.1.21",
25
+ "@forgeax/engine-rhi": "0.1.23",
26
+ "@forgeax/engine-types": "0.1.23",
27
27
  "@webgpu/types": "^0.1.71"
28
28
  },
29
29
  "devDependencies": {
30
- "@forgeax/engine-vite-plugin-shader": "0.1.21"
30
+ "@forgeax/engine-vite-plugin-shader": "0.1.23"
31
31
  },
32
32
  "forgeax": {
33
33
  "metrics": {
@@ -19,13 +19,17 @@ function device(): ShaderRegistryDevice {
19
19
  }
20
20
 
21
21
  async function engineManifest(): Promise<EngineShaderManifest> {
22
- const { buildEngineShaderManifest } = await import('@forgeax/engine-vite-plugin-shader');
23
- return buildEngineShaderManifest();
22
+ engineManifestPromise ??= import('@forgeax/engine-vite-plugin-shader').then(
23
+ ({ buildEngineShaderManifest }) => buildEngineShaderManifest(),
24
+ );
25
+ return engineManifestPromise;
24
26
  }
25
27
 
28
+ let engineManifestPromise: Promise<EngineShaderManifest> | undefined;
29
+
26
30
  describe('default Standard PBR transmission manifest contract', () => {
27
31
  it('publishes both TRANSMISSION_AVAILABLE variants from the cooked producer', {
28
- timeout: 20_000,
32
+ timeout: 60_000,
29
33
  }, async () => {
30
34
  const manifest = await engineManifest();
31
35
  const standard = manifest.materialShaders.find(
@@ -37,39 +41,73 @@ describe('default Standard PBR transmission manifest contract', () => {
37
41
  const transmissionVariants = standard.variants.filter(
38
42
  (variant) => 'TRANSMISSION_AVAILABLE' in variant.defines,
39
43
  );
40
- // Cluster-forward + storage-fallback is not a valid ABI pair: the
41
- // clustered Standard group(2) requires storage buffers. The producer
42
- // emits the six-axis Cartesian product minus those sixteen impossible
43
- // combinations, i.e. 48 exact variants.
44
- expect(transmissionVariants).toHaveLength(48);
45
- expect(new Set(transmissionVariants.map((variant) => variant.definesKey)).size).toBe(48);
46
- // Projector sampling is owned by the shared Cluster module. The
47
- // non-cluster capability rows intentionally have no projector consumer
48
- // after the direct-light/fallback cut, so their true/false projector keys
49
- // alias one source instead of carrying duplicate WGSL. The 16 non-cluster
50
- // pairs therefore collapse 48 logical keys to 32 source identities.
51
- expect(new Set(transmissionVariants.map((variant) => variant.composedWgsl)).size).toBe(32);
52
- const nonClusterPairs = transmissionVariants.filter(
53
- (variant) => variant.defines.CLUSTER_FORWARD_AVAILABLE === false,
54
- );
55
- for (const variant of nonClusterPairs) {
56
- const projectorTwin = nonClusterPairs.find(
57
- (candidate) =>
58
- candidate.defines.STORAGE_BUFFER_AVAILABLE === variant.defines.STORAGE_BUFFER_AVAILABLE &&
59
- candidate.defines.TRANSMISSION_AVAILABLE === variant.defines.TRANSMISSION_AVAILABLE &&
60
- candidate.defines.VERTEX_COLOR_AVAILABLE === variant.defines.VERTEX_COLOR_AVAILABLE &&
61
- candidate.defines.DIRECTIONAL_PCSS_AVAILABLE ===
62
- variant.defines.DIRECTIONAL_PCSS_AVAILABLE &&
63
- candidate.defines.PROJECTOR_AVAILABLE !== variant.defines.PROJECTOR_AVAILABLE,
64
- );
65
- expect(projectorTwin?.composedWgsl).toBe(variant.composedWgsl);
44
+ // The producer emits the complete supported Cartesian product. Storage is
45
+ // required for cluster/probe/extended-lighting variants, and extended
46
+ // lighting additionally requires the projector texture lane. Keep the
47
+ // expected keys derived here so adding a new axis cannot silently shrink
48
+ // the published set.
49
+ const expectedKeys = new Set<string>();
50
+ for (const clusterForwardAvailable of [false, true]) {
51
+ for (const storageBufferAvailable of [false, true]) {
52
+ for (const vertexColorAvailable of [false, true]) {
53
+ for (const probeBlendAvailable of [false, true]) {
54
+ for (const extendedLightingAvailable of [false, true]) {
55
+ for (const directionalPcssAvailable of [false, true]) {
56
+ for (const transmissionAvailable of [false, true]) {
57
+ for (const projectorAvailable of [false, true]) {
58
+ for (const reflectionFallbackAvailable of [false, true]) {
59
+ if (
60
+ (clusterForwardAvailable && !storageBufferAvailable) ||
61
+ (probeBlendAvailable && !storageBufferAvailable) ||
62
+ (extendedLightingAvailable &&
63
+ (!storageBufferAvailable || !projectorAvailable))
64
+ ) {
65
+ continue;
66
+ }
67
+ const expectedKeyEntries = [
68
+ `CLUSTER_FORWARD_AVAILABLE=${clusterForwardAvailable}`,
69
+ `DIRECTIONAL_PCSS_AVAILABLE=${directionalPcssAvailable}`,
70
+ `EXTENDED_LIGHTING_AVAILABLE=${extendedLightingAvailable}`,
71
+ `PROBE_BLEND_AVAILABLE=${probeBlendAvailable}`,
72
+ `PROJECTOR_AVAILABLE=${projectorAvailable}`,
73
+ `REFLECTION_FALLBACK_AVAILABLE=${reflectionFallbackAvailable}`,
74
+ `STORAGE_BUFFER_AVAILABLE=${storageBufferAvailable}`,
75
+ `TRANSMISSION_AVAILABLE=${transmissionAvailable}`,
76
+ `VERTEX_COLOR_AVAILABLE=${vertexColorAvailable}`,
77
+ ];
78
+ expectedKeys.add(
79
+ expectedKeyEntries.every((entry) => entry.endsWith('=true'))
80
+ ? ''
81
+ : expectedKeyEntries.join('+'),
82
+ );
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
66
91
  }
92
+ expect(transmissionVariants).toHaveLength(expectedKeys.size);
93
+ expect(new Set(transmissionVariants.map((variant) => variant.definesKey))).toEqual(
94
+ expectedKeys,
95
+ );
67
96
  expect(
68
97
  transmissionVariants.every(
69
98
  (variant) =>
70
99
  !(
71
100
  variant.defines.CLUSTER_FORWARD_AVAILABLE === true &&
72
101
  variant.defines.STORAGE_BUFFER_AVAILABLE === false
102
+ ) &&
103
+ !(
104
+ variant.defines.PROBE_BLEND_AVAILABLE === true &&
105
+ variant.defines.STORAGE_BUFFER_AVAILABLE === false
106
+ ) &&
107
+ !(
108
+ variant.defines.EXTENDED_LIGHTING_AVAILABLE === true &&
109
+ (variant.defines.STORAGE_BUFFER_AVAILABLE === false ||
110
+ variant.defines.PROJECTOR_AVAILABLE === false)
73
111
  ),
74
112
  ),
75
113
  ).toBe(true);
@@ -77,33 +115,54 @@ describe('default Standard PBR transmission manifest contract', () => {
77
115
  for (const storageBufferAvailable of [false, true]) {
78
116
  if (clusterForwardAvailable && !storageBufferAvailable) continue;
79
117
  for (const vertexColorAvailable of [false, true]) {
80
- for (const directionalPcssAvailable of [false, true]) {
81
- for (const transmissionAvailable of [false, true]) {
82
- for (const projectorAvailable of [false, true]) {
83
- const expectedKeyEntries = [
84
- `CLUSTER_FORWARD_AVAILABLE=${clusterForwardAvailable}`,
85
- `DIRECTIONAL_PCSS_AVAILABLE=${directionalPcssAvailable}`,
86
- `PROJECTOR_AVAILABLE=${projectorAvailable}`,
87
- `STORAGE_BUFFER_AVAILABLE=${storageBufferAvailable}`,
88
- `TRANSMISSION_AVAILABLE=${transmissionAvailable}`,
89
- `VERTEX_COLOR_AVAILABLE=${vertexColorAvailable}`,
90
- ];
91
- const expectedKey = expectedKeyEntries.every((entry) => entry.endsWith('=true'))
92
- ? ''
93
- : expectedKeyEntries.join('+');
94
- expect(transmissionVariants).toContainEqual(
95
- expect.objectContaining({
96
- definesKey: expectedKey,
97
- defines: expect.objectContaining({
98
- CLUSTER_FORWARD_AVAILABLE: clusterForwardAvailable,
99
- DIRECTIONAL_PCSS_AVAILABLE: directionalPcssAvailable,
100
- PROJECTOR_AVAILABLE: projectorAvailable,
101
- STORAGE_BUFFER_AVAILABLE: storageBufferAvailable,
102
- TRANSMISSION_AVAILABLE: transmissionAvailable,
103
- VERTEX_COLOR_AVAILABLE: vertexColorAvailable,
104
- }),
105
- }),
106
- );
118
+ for (const probeBlendAvailable of [false, true]) {
119
+ for (const extendedLightingAvailable of [false, true]) {
120
+ for (const directionalPcssAvailable of [false, true]) {
121
+ for (const transmissionAvailable of [false, true]) {
122
+ for (const projectorAvailable of [false, true]) {
123
+ for (const reflectionFallbackAvailable of [false, true]) {
124
+ if (
125
+ (probeBlendAvailable && !storageBufferAvailable) ||
126
+ (extendedLightingAvailable &&
127
+ (!storageBufferAvailable || !projectorAvailable))
128
+ ) {
129
+ continue;
130
+ }
131
+ const expectedKeyEntries = [
132
+ `CLUSTER_FORWARD_AVAILABLE=${clusterForwardAvailable}`,
133
+ `DIRECTIONAL_PCSS_AVAILABLE=${directionalPcssAvailable}`,
134
+ `EXTENDED_LIGHTING_AVAILABLE=${extendedLightingAvailable}`,
135
+ `PROBE_BLEND_AVAILABLE=${probeBlendAvailable}`,
136
+ `PROJECTOR_AVAILABLE=${projectorAvailable}`,
137
+ `REFLECTION_FALLBACK_AVAILABLE=${reflectionFallbackAvailable}`,
138
+ `STORAGE_BUFFER_AVAILABLE=${storageBufferAvailable}`,
139
+ `TRANSMISSION_AVAILABLE=${transmissionAvailable}`,
140
+ `VERTEX_COLOR_AVAILABLE=${vertexColorAvailable}`,
141
+ ];
142
+ const expectedKey = expectedKeyEntries.every((entry) =>
143
+ entry.endsWith('=true'),
144
+ )
145
+ ? ''
146
+ : expectedKeyEntries.join('+');
147
+ expect(transmissionVariants).toContainEqual(
148
+ expect.objectContaining({
149
+ definesKey: expectedKey,
150
+ defines: expect.objectContaining({
151
+ CLUSTER_FORWARD_AVAILABLE: clusterForwardAvailable,
152
+ DIRECTIONAL_PCSS_AVAILABLE: directionalPcssAvailable,
153
+ EXTENDED_LIGHTING_AVAILABLE: extendedLightingAvailable,
154
+ PROBE_BLEND_AVAILABLE: probeBlendAvailable,
155
+ PROJECTOR_AVAILABLE: projectorAvailable,
156
+ REFLECTION_FALLBACK_AVAILABLE: reflectionFallbackAvailable,
157
+ STORAGE_BUFFER_AVAILABLE: storageBufferAvailable,
158
+ TRANSMISSION_AVAILABLE: transmissionAvailable,
159
+ VERTEX_COLOR_AVAILABLE: vertexColorAvailable,
160
+ }),
161
+ }),
162
+ );
163
+ }
164
+ }
165
+ }
107
166
  }
108
167
  }
109
168
  }
@@ -113,7 +172,7 @@ describe('default Standard PBR transmission manifest contract', () => {
113
172
  });
114
173
 
115
174
  it('omits transmission-only texture bindings from the fallback variant', {
116
- timeout: 20_000,
175
+ timeout: 60_000,
117
176
  }, async () => {
118
177
  const manifest = await engineManifest();
119
178
  const standard = manifest.materialShaders.find(
@@ -126,6 +185,8 @@ describe('default Standard PBR transmission manifest contract', () => {
126
185
  (variant) =>
127
186
  variant.defines.CLUSTER_FORWARD_AVAILABLE === false &&
128
187
  variant.defines.STORAGE_BUFFER_AVAILABLE === true &&
188
+ variant.defines.EXTENDED_LIGHTING_AVAILABLE === false &&
189
+ variant.defines.PROBE_BLEND_AVAILABLE === false &&
129
190
  variant.defines.TRANSMISSION_AVAILABLE === false &&
130
191
  variant.defines.VERTEX_COLOR_AVAILABLE === false,
131
192
  );
@@ -133,6 +194,8 @@ describe('default Standard PBR transmission manifest contract', () => {
133
194
  (variant) =>
134
195
  variant.defines.CLUSTER_FORWARD_AVAILABLE === false &&
135
196
  variant.defines.STORAGE_BUFFER_AVAILABLE === true &&
197
+ variant.defines.EXTENDED_LIGHTING_AVAILABLE === false &&
198
+ variant.defines.PROBE_BLEND_AVAILABLE === false &&
136
199
  variant.defines.TRANSMISSION_AVAILABLE === true &&
137
200
  variant.defines.VERTEX_COLOR_AVAILABLE === false,
138
201
  );
@@ -143,7 +206,7 @@ describe('default Standard PBR transmission manifest contract', () => {
143
206
  });
144
207
 
145
208
  it('registers one Standard row and resolves a declared variant by exact key', {
146
- timeout: 20_000,
209
+ timeout: 60_000,
147
210
  }, async () => {
148
211
  const manifest = await engineManifest();
149
212
  const standard = manifest.materialShaders.find(
@@ -0,0 +1,40 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ async function readShader(name: string): Promise<string> {
5
+ return readFile(new URL(`../${name}`, import.meta.url), 'utf8');
6
+ }
7
+
8
+ describe('DirectLightSlot WGSL ABI', () => {
9
+ it('declares the canonical five-row 80-byte slot', async () => {
10
+ const common = await readShader('common.wgsl');
11
+ const match = common.match(/struct DirectLightSlot\s*\{([\s\S]*?)\};/);
12
+
13
+ expect(match).not.toBeNull();
14
+ const struct = match?.[1] ?? '';
15
+ expect(struct.match(/vec4<f32>/g)).toHaveLength(4);
16
+ expect(struct.match(/vec4<u32>/g)).toHaveLength(1);
17
+ expect(struct).toContain('row0');
18
+ expect(struct).toContain('row1');
19
+ expect(struct).toContain('row2');
20
+ expect(struct).toContain('row3');
21
+ expect(struct).toContain('metadata : vec4<u32>');
22
+ expect(common).not.toContain('struct DirectLightArray');
23
+ expect(common).not.toContain('array<DirectLightSlot, 4>');
24
+ expect(common).toContain('@group(0) @binding(0)');
25
+ });
26
+
27
+ it('keeps the slot type owned by the unified Cluster shader consumer', async () => {
28
+ const common = await readShader('common.wgsl');
29
+ const standard = await readShader('default-standard-pbr.wgsl');
30
+ const clustered = await readShader('standard-cluster.wgsl');
31
+
32
+ expect(common).toContain('struct DirectLightSlot');
33
+ expect(standard).not.toContain('DirectLightSlot');
34
+ expect(standard).not.toContain('pointLightsBuffer');
35
+ expect(standard).not.toContain('spotLightsBuffer');
36
+ expect(clustered).toContain('DirectLightSlot');
37
+ expect(clustered).toContain('array<DirectLightSlot, 256>');
38
+ expect(clustered).not.toContain('struct LightSlot');
39
+ });
40
+ });
@@ -27,15 +27,18 @@ beforeAll(async () => {
27
27
  });
28
28
 
29
29
  describe('IBL irradiance payload contract', () => {
30
- it('records that convolution produces E times pi before the Lambert divide', () => {
31
- expect(irradianceSource).toContain('IRRADIANCE_PAYLOAD_E_TIMES_PI');
30
+ it('records that convolution stores Lambert-normalized E over pi', () => {
31
+ expect(irradianceSource).toContain('IRRADIANCE_PAYLOAD_E_OVER_PI');
32
32
  expect(irradianceSource).toMatch(/irradiance\s*=\s*PI\s*\*\s*irradiance/);
33
33
  });
34
34
 
35
35
  it('keeps the constant-environment analytic oracle finite and explicit', () => {
36
36
  const environment = 0.72;
37
- const convolvedPayload = environment * Math.PI;
38
- const diffuseRadiance = convolvedPayload / Math.PI;
37
+ // The hemisphere integral is E=pi*L for a constant environment. The
38
+ // convolution shader's PI*sampleAverage stores E/pi=L directly, which is
39
+ // the Lambert diffuse radiance consumed by the material shader.
40
+ const convolvedPayload = environment;
41
+ const diffuseRadiance = convolvedPayload;
39
42
 
40
43
  expect(diffuseRadiance).toBeCloseTo(environment, 12);
41
44
  expect(Number.isFinite(diffuseRadiance)).toBe(true);
@@ -29,12 +29,11 @@ beforeAll(async () => {
29
29
  describe('IBL diffuse sampling payload contract', () => {
30
30
  it('names the sampled payload as E over pi at the canonical sampling owner', () => {
31
31
  expect(samplingSource).toContain('irradianceEOverPi');
32
- expect(samplingSource).toMatch(/irradianceEOverPi\s*\/\s*PI/);
32
+ expect(samplingSource).toMatch(/return\s+irradianceEOverPi\s*;/);
33
33
  });
34
34
 
35
- it('does not divide the same diffuse payload twice', () => {
35
+ it('does not divide the already Lambert-normalized diffuse payload again', () => {
36
36
  const diffuseBody = samplingSource.match(/fn sampleIblDiffuse\([\s\S]*?\n\}/)?.[0] ?? '';
37
- expect(diffuseBody.match(/\/\s*PI/g) ?? []).toHaveLength(1);
38
- expect(diffuseBody).not.toMatch(/irradianceEOverPi\s*\/\s*PI[\s\S]*\/\s*PI/);
37
+ expect(diffuseBody).not.toMatch(/\/\s*PI/);
39
38
  });
40
39
  });
@@ -24,8 +24,14 @@ describe('shared punctual optics for volumetric fog', () => {
24
24
  expect(inject).toContain('evalVolumeSpot');
25
25
  expect(integrate).toContain('evalVolumePoint');
26
26
  expect(integrate).toContain('evalVolumeSpot');
27
- expect(inject).toContain('pointLightsBuffer');
28
- expect(inject).toContain('spotLightsBuffer');
27
+ expect(inject).toContain('light_data');
28
+ expect(inject).toContain('cluster_uniform');
29
+ expect(inject).not.toContain('pointLightsBuffer');
30
+ expect(inject).not.toContain('spotLightsBuffer');
31
+ expect(integrate).toContain('light_data');
32
+ expect(integrate).toContain('cluster_uniform');
33
+ expect(integrate).not.toContain('pointLightsBuffer');
34
+ expect(integrate).not.toContain('spotLightsBuffer');
29
35
  });
30
36
 
31
37
  it('keeps volume View aligned with the directional filter carrier', () => {
@@ -9,6 +9,7 @@ const standardClusterShaderPath = join(
9
9
  '../standard-cluster.wgsl',
10
10
  );
11
11
  const pbrShaderPath = join(dirname(fileURLToPath(import.meta.url)), '../default-standard-pbr.wgsl');
12
+ const brdfShaderPath = join(dirname(fileURLToPath(import.meta.url)), '../brdf.wgsl');
12
13
  const directionalShaderPath = join(
13
14
  dirname(fileURLToPath(import.meta.url)),
14
15
  '../lighting-directional.wgsl',
@@ -42,10 +43,30 @@ describe('direct punctual lighting shader contract', () => {
42
43
  expect(source).not.toContain('normalize(lightDir)');
43
44
  });
44
45
 
46
+ it('shares Three r184 direct-PBR Fresnel and multiscatter ownership', async () => {
47
+ const [source, brdf, directional] = await Promise.all([
48
+ readFile(shaderPath, 'utf8'),
49
+ readFile(brdfShaderPath, 'utf8'),
50
+ readFile(directionalShaderPath, 'utf8'),
51
+ ]);
52
+
53
+ expect(brdf).toContain('exp2((-5.55473 * vDotH - 6.98316) * vDotH)');
54
+ expect(brdf).toContain('fn threeR184DirectMultiScatter(');
55
+ expect(brdf).toContain('energyLoss * favg * favg + vec3<f32>(1e-6)');
56
+ expect(brdf).not.toContain('energyLoss * favg + vec3<f32>(1e-6)');
57
+ expect(source).toContain('threeR184DirectMultiScatter(roughness, nDotV, nDotL, F0)');
58
+ expect(source).toContain('let diffuse = (1.0 - metallic) * baseColor / 3.14159265;');
59
+ expect(source).toContain('return factor * factor / dSquared;');
60
+ expect(source).not.toContain('let kd = (vec3<f32>(1.0) - f) * (1.0 - metallic);');
61
+ expect(directional).toContain('threeR184DirectMultiScatter');
62
+ expect(directional).not.toContain('THREE_R184_DFG_LUT');
63
+ });
64
+
45
65
  it('keeps cluster punctual evaluation in the shared lighting owner', async () => {
46
66
  const source = await readFile(standardClusterShaderPath, 'utf8');
47
67
 
48
- expect(source).toContain('kind_and_shadow');
68
+ expect(source).toContain('DirectLightSlot');
69
+ expect(source).toContain('light.metadata.x');
49
70
  expect(source).toContain('evalPoint');
50
71
  expect(source).toContain('evalSpot');
51
72
  expect(source).toContain('evalSpotShadowed');
@@ -57,7 +78,7 @@ describe('direct punctual lighting shader contract', () => {
57
78
  it('decodes HDRP spot cone lanes in the shared evaluator order', async () => {
58
79
  const source = await readFile(standardClusterShaderPath, 'utf8');
59
80
 
60
- expect(source).toContain('light.color.w, light.direction.w, light.position.w');
81
+ expect(source).toContain('light.colorTimesIntensity.w, light.direction.w, light.position.w');
61
82
  expect(source).not.toContain('light.direction.w, light.color.w, light.position.w');
62
83
  });
63
84
 
@@ -65,6 +86,12 @@ describe('direct punctual lighting shader contract', () => {
65
86
  const source = await readFile(standardClusterShaderPath, 'utf8');
66
87
 
67
88
  expect(source).toContain('if (encoded_tile >= 0 && (encoded_tile & PROJECTOR_FLAG) != 0)');
89
+ expect(source).toContain(
90
+ 'sampleStandardSpotProjector(view.spotLightViewProj[tile], world_pos, light.metadata)',
91
+ );
92
+ expect(source).toContain(
93
+ 'textureSampleLevel(cookieTexture, spotModifierSampler, uv, metadata.w, 0.0).rgb',
94
+ );
68
95
  });
69
96
 
70
97
  it('passes precomputed base and clearcoat roughness/F0 facts to cluster lights', async () => {
@@ -14,7 +14,7 @@ describe('volume Spot attenuation precision', () => {
14
14
  expect(volumeInject).toContain('spot_shadow_visibility_at');
15
15
  expect(volumeIntegrate).toContain('evalSpotAttenuation');
16
16
  expect(volumeIntegrate).toContain('spot_attenuation_at');
17
- expect(volumeIntegrate).toContain('light.invRangeSquared');
17
+ expect(volumeIntegrate).toContain('light.position.w');
18
18
  // The paired Point+Spot owner applies visibility only to the Spot term;
19
19
  // the Point radiance remains independent of the spot shadow. Keep the
20
20
  // assertion at the semantic call boundary so WGSL formatting changes do