@forgeax/engine-shader 0.1.28 → 0.1.29

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 (73) hide show
  1. package/NOTICE +35 -0
  2. package/README.md +39 -0
  3. package/dist/ShaderRegistry.d.ts +8 -0
  4. package/dist/ShaderRegistry.d.ts.map +1 -1
  5. package/dist/index.d.ts +6 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.mjs +312 -71
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/material/artifact-types.d.ts +12 -2
  10. package/dist/material/artifact-types.d.ts.map +1 -1
  11. package/dist/material-schemas.d.ts +5 -0
  12. package/dist/material-schemas.d.ts.map +1 -1
  13. package/package.json +5 -4
  14. package/src/ShaderRegistry.ts +10 -0
  15. package/src/__tests__/auto-exposure-graph.unit.test.ts +59 -0
  16. package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +30 -15
  17. package/src/__tests__/builtin-texture-sampling-contract.test.ts +1 -1
  18. package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +2 -1
  19. package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +167 -6
  20. package/src/__tests__/direct-light-layout.unit.test.ts +1 -1
  21. package/src/__tests__/lighting-punctual.unit.test.ts +29 -1
  22. package/src/__tests__/ltc-provenance.unit.test.ts +13 -2
  23. package/src/__tests__/material-builtins.unit.test.ts +1 -0
  24. package/src/__tests__/material-contract.unit.test.ts +42 -9
  25. package/src/__tests__/material-derived-builtins.integration.test.ts +18 -2
  26. package/src/__tests__/probe-lighting-composition.unit.test.ts +1 -1
  27. package/src/__tests__/reflection-probe-sampling.unit.test.ts +23 -2
  28. package/src/__tests__/scene-temporal.unit.test.ts +10 -0
  29. package/src/__tests__/shader.unit.test.ts +1 -0
  30. package/src/__tests__/sprite-variants.unit.test.ts +1 -0
  31. package/src/__tests__/ssr-artifact.unit.test.ts +141 -0
  32. package/src/__tests__/ssr-bgl.integration.test.ts +185 -0
  33. package/src/__tests__/standard-output-domain.unit.test.ts +36 -0
  34. package/src/__tests__/standard-pbr-artifact-receipt.unit.test.ts +93 -0
  35. package/src/__tests__/standard-surface-pass-evaluation.integration.test.ts +16 -4
  36. package/src/__tests__/surface-v1.unit.test.ts +6 -0
  37. package/src/__tests__/taa-resolve.unit.test.ts +20 -9
  38. package/src/__tests__/transmission-thickness-scale.unit.test.ts +6 -1
  39. package/src/__tests__/transparent-pbr.unit.test.ts +1 -1
  40. package/src/__tests__/vertex-color-variant.unit.test.ts +4 -1
  41. package/src/atmosphere-background.wgsl +3 -3
  42. package/src/auto-exposure-meter.wgsl +179 -0
  43. package/src/color-lut.wgsl +17 -0
  44. package/src/common.wgsl +7 -5
  45. package/src/default-standard-pbr-skin.wgsl +142 -40
  46. package/src/default-standard-pbr.wgsl +198 -82
  47. package/src/default_standard_surface.wgsl +56 -19
  48. package/src/fxaa.wgsl +9 -5
  49. package/src/ibl-sampling.wgsl +30 -6
  50. package/src/index.ts +189 -0
  51. package/src/lighting-punctual.wgsl +5 -6
  52. package/src/material/artifact-types.ts +109 -63
  53. package/src/material-schemas.ts +84 -8
  54. package/src/output-encoding.wgsl +10 -0
  55. package/src/pbr-temporal.wgsl +9 -1
  56. package/src/scene-temporal.wgsl +2 -0
  57. package/src/shadow-pcf.wgsl +6 -8
  58. package/src/shadow-surface.wgsl +16 -0
  59. package/src/shadow_caster.wgsl +99 -61
  60. package/src/sprite-lit.wgsl +4 -4
  61. package/src/sprite.wgsl +3 -3
  62. package/src/ssr-compose.wgsl +129 -0
  63. package/src/ssr-hiz-reduce.wgsl +53 -0
  64. package/src/ssr-hiz.wgsl +66 -0
  65. package/src/ssr-temporal.wgsl +305 -0
  66. package/src/ssr-trace.wgsl +784 -0
  67. package/src/standard-cluster.wgsl +6 -4
  68. package/src/standard-surface.wgsl +696 -0
  69. package/src/surface_v1.wgsl +6 -0
  70. package/src/taa-resolve.wgsl +222 -28
  71. package/src/tbn.wgsl +1 -1
  72. package/src/tonemap.wgsl +37 -18
  73. package/src/unlit.wgsl +3 -3
@@ -1,4 +1,4 @@
1
- import type { BindGroupLayoutDescriptor } from '@forgeax/engine-types';
1
+ import { type BindGroupLayoutDescriptor, type MaterialParticleInput, type ParamSchemaEntry } from '@forgeax/engine-types';
2
2
  export interface MaterialShaderResourceSlot {
3
3
  readonly name: string;
4
4
  readonly parameter: string;
@@ -51,15 +51,25 @@ export interface MaterialShaderArtifact {
51
51
  readonly bindings: readonly BindGroupLayoutDescriptor[];
52
52
  readonly deps: readonly string[];
53
53
  readonly vertexInputs: readonly Readonly<Record<string, unknown>>[];
54
+ /** Reflection-owned dynamic values supplied by a VFX renderer. */
55
+ readonly particleInputs?: readonly MaterialParticleInput[];
54
56
  readonly specializationKey?: string;
55
57
  /** Producer-owned ABI facts shared by direct and scene-index consumers. */
56
58
  readonly receipt?: MaterialShaderArtifactReceipt;
57
59
  }
60
+ /**
61
+ * Canonical Standard material schema consumed by the scene-index row.
62
+ * Physical texture handles stay in the fixed injected range (26..45); their
63
+ * numeric factors remain in this row, after the seven stable user texture
64
+ * coordinate pairs. Keeping this projection beside the receipt lets the
65
+ * shader producer and the render-side table consume one schema identity.
66
+ */
67
+ export declare const STANDARD_PIPELINE_PARAM_SCHEMA: readonly ParamSchemaEntry[];
58
68
  /**
59
69
  * Builds the one Standard PBR ABI receipt consumed by both material lanes.
60
70
  * The skinned module uses the same material producer and only adds its palette
61
71
  * address; it must retain the same receipt identity for material compatibility.
62
72
  */
63
- export declare function createStandardPbrArtifactReceipt(skinned?: boolean): MaterialShaderArtifactReceipt;
73
+ export declare function createStandardPbrArtifactReceipt(skinned?: boolean, vertexColorAvailable?: boolean): MaterialShaderArtifactReceipt;
64
74
  export declare function isMaterialShaderArtifact(value: unknown): value is MaterialShaderArtifact;
65
75
  //# 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,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
+ {"version":3,"file":"artifact-types.d.ts","sourceRoot":"","sources":["../../src/material/artifact-types.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,yBAAyB,EAE9B,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EAGtB,MAAM,uBAAuB,CAAC;AAE/B,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,kEAAkE;IAClE,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC3D,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,CAAC,EAAE,6BAA6B,CAAC;CAClD;AAED;;;;;;GAMG;AACH,eAAO,MAAM,8BAA8B,EAAE,SAAS,gBAAgB,EASnE,CAAC;AAqDJ;;;;GAIG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,UAAQ,EACf,oBAAoB,UAAQ,GAC3B,6BAA6B,CAqC/B;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,sBAAsB,CA2BxF"}
@@ -3,6 +3,10 @@ import { type MaterialShaderArtifactReceipt } from './material/artifact-types.js
3
3
  export declare const STANDARD_PBR_ALPHA_CUTOFF_DEFAULT = 0;
4
4
  /** Shared material contract for the standard PBR and skinned PBR shaders. */
5
5
  export declare const DEFAULT_STANDARD_PBR_PARAM_SCHEMA: readonly ParamSchemaEntry[];
6
+ /** Numeric WGSL override ID, stable across Naga symbol mangling. */
7
+ export declare const STANDARD_TEXTURE_MASK_OVERRIDE = "64000";
8
+ /** Presence follows the authored contract, never asynchronous GPU residency. */
9
+ export declare function standardTextureMask(schema: readonly ParamSchemaEntry[]): number;
6
10
  export type { StandardPhysicalTextureField } from '@forgeax/engine-types';
7
11
  /**
8
12
  * Backward-compatible export name for callers that need the physical
@@ -34,6 +38,7 @@ export declare const STANDARD_BASE_PARAM_SCHEMA: readonly ParamSchemaEntry[];
34
38
  * root contract that declares them.
35
39
  */
36
40
  export declare const STANDARD_PIPELINE_PARAM_SCHEMA: readonly ParamSchemaEntry[];
41
+ export declare const PARTICLE_MESH_SURFACE_PARAM_SCHEMA: readonly ParamSchemaEntry[];
37
42
  /** The single producer receipt shared by direct and scene-index Standard PBR. */
38
43
  export declare const STANDARD_PBR_ARTIFACT_RECEIPT: MaterialShaderArtifactReceipt;
39
44
  /** Skinned Standard PBR keeps the material receipt identity and adds palette ABI. */
@@ -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;AAK9D,OAAO,EAEL,KAAK,6BAA6B,EACnC,MAAM,8BAA8B,CAAC;AAEtC,eAAO,MAAM,iCAAiC,IAAI,CAAC;AAEnD,6EAA6E;AAC7E,eAAO,MAAM,iCAAiC,6BAAiC,CAAC;AAEhF,YAAY,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAC1E;;;;GAIG;AACH;;;;;GAKG;AACH,OAAO,EACL,gCAAgC,EAChC,6BAA6B,GAC9B,MAAM,uBAAuB,CAAC;AAE/B,eAAO,MAAM,oCAAoC,EAAE,SAAS,gBAAgB,EAGzE,CAAC;AAEJ;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,EAAE,SAAS,gBAAgB,EAY/D,CAAC;AAEJ;;;;;;;;;GASG;AACH,eAAO,MAAM,8BAA8B,EAAE,SAAS,gBAAgB,EAOnE,CAAC;AAEJ,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"}
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;AAK9D,OAAO,EAGL,KAAK,6BAA6B,EACnC,MAAM,8BAA8B,CAAC;AAEtC,eAAO,MAAM,iCAAiC,IAAI,CAAC;AAEnD,6EAA6E;AAC7E,eAAO,MAAM,iCAAiC,6BAAiC,CAAC;AAEhF,oEAAoE;AACpE,eAAO,MAAM,8BAA8B,UAAU,CAAC;AAQtD,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAM/E;AAED,YAAY,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAC1E;;;;GAIG;AACH;;;;;GAKG;AACH,OAAO,EACL,gCAAgC,EAChC,6BAA6B,GAC9B,MAAM,uBAAuB,CAAC;AAE/B,eAAO,MAAM,oCAAoC,EAAE,SAAS,gBAAgB,EAGzE,CAAC;AAEJ;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,EAAE,SAAS,gBAAgB,EAY/D,CAAC;AAEJ;;;;;;;;;GASG;AAIH,eAAO,MAAM,8BAA8B,6BAA2C,CAAC;AA0DvF,eAAO,MAAM,kCAAkC,EAAE,SAAS,gBAAgB,EAGxE,CAAC;AAEH,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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-shader",
3
- "version": "0.1.28",
3
+ "version": "0.1.29",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -19,15 +19,16 @@
19
19
  "dist",
20
20
  "src",
21
21
  "README.md",
22
+ "NOTICE",
22
23
  "LICENSE"
23
24
  ],
24
25
  "dependencies": {
25
- "@forgeax/engine-rhi": "0.1.28",
26
- "@forgeax/engine-types": "0.1.28",
26
+ "@forgeax/engine-rhi": "0.1.29",
27
+ "@forgeax/engine-types": "0.1.29",
27
28
  "@webgpu/types": "^0.1.71"
28
29
  },
29
30
  "devDependencies": {
30
- "@forgeax/engine-vite-plugin-shader": "0.1.28"
31
+ "@forgeax/engine-vite-plugin-shader": "0.1.29"
31
32
  },
32
33
  "forgeax": {
33
34
  "metrics": {
@@ -73,6 +73,16 @@ export interface ShaderRegistryDevice {
73
73
  */
74
74
  export const FORGEAX_RESERVED_PATH_PREFIX = 'forgeax::' as const;
75
75
 
76
+ /** Stable build-time module ids for the M1 spatial SSR artifact set. */
77
+ export const SSR_SHADER_MODULES = Object.freeze({
78
+ hiz: 'forgeax_ssr::hiz',
79
+ hizReduce: 'forgeax_ssr::hiz_reduce',
80
+ trace: 'forgeax_ssr::trace',
81
+ temporal: 'forgeax_ssr::temporal',
82
+ } as const);
83
+
84
+ export type SsrShaderModule = (typeof SSR_SHADER_MODULES)[keyof typeof SSR_SHADER_MODULES];
85
+
76
86
  /**
77
87
  * Material shader admission input accepted by
78
88
  * `installMaterialArtifact(identifier, entry)`. `findMaterialArtifact`
@@ -0,0 +1,59 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { AUTO_EXPOSURE_METER_WGSL } from '../index';
5
+
6
+ const standaloneMeterSource = readFileSync(
7
+ fileURLToPath(new URL('../auto-exposure-meter.wgsl', import.meta.url)),
8
+ 'utf8',
9
+ );
10
+
11
+ describe('auto exposure shader graph', () => {
12
+ it('exposes one fixed-cohort WGSL module without compiler or Naga imports', () => {
13
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('@compute');
14
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('@workgroup_size(256, 1, 1)');
15
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain(
16
+ 'var<workgroup> localHistogram: array<atomic<u32>, 256>',
17
+ );
18
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('atomicStore(&localHistogram[localIndex], 0u)');
19
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('fn auto_exposure_clear');
20
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('fn auto_exposure_histogram');
21
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('fn auto_exposure_adapt');
22
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('@builtin(num_workgroups) numWorkgroups');
23
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('let blockStride = numWorkgroups.xy * tileSize');
24
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('for (var blockY = blockStart.y;');
25
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('for (var blockX = blockStart.x;');
26
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('fn centerWeight');
27
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('value == value');
28
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain(
29
+ 'atomicAdd(&localHistogram[bin], centerWeight(block, center, extent))',
30
+ );
31
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('atomicAdd(&histogram[localIndex], localCount)');
32
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('parameters.compensationEv');
33
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('parameters.rangeMinEv');
34
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('parameters.upRate');
35
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('parameters.deltaTime');
36
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('validParameters');
37
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('previous.x');
38
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('state[0] = candidate[0]');
39
+ expect(AUTO_EXPOSURE_METER_WGSL).not.toContain('fn auto_exposure_meter');
40
+ expect(AUTO_EXPOSURE_METER_WGSL).not.toContain('sampleBins');
41
+ expect(AUTO_EXPOSURE_METER_WGSL).not.toContain('shader-compiler');
42
+ expect(AUTO_EXPOSURE_METER_WGSL).not.toContain('naga');
43
+ });
44
+
45
+ it('keeps the standalone source and runtime entrypoints single and aligned', () => {
46
+ expect(standaloneMeterSource.match(/struct AutoExposureParameters/g)).toHaveLength(1);
47
+ expect(standaloneMeterSource.match(/fn finite\(/g)).toHaveLength(1);
48
+ expect(standaloneMeterSource.match(/@group\(0\) @binding\(0\)/g)).toHaveLength(1);
49
+ expect(standaloneMeterSource).toContain('@compute @workgroup_size(256, 1, 1)');
50
+ expect(standaloneMeterSource).toContain('fn auto_exposure_clear');
51
+ expect(standaloneMeterSource).toContain('fn auto_exposure_histogram');
52
+ expect(standaloneMeterSource).toContain('fn auto_exposure_adapt');
53
+ expect(standaloneMeterSource).toContain('@builtin(num_workgroups) numWorkgroups');
54
+ expect(standaloneMeterSource).toContain('let blockStride = numWorkgroups.xy * tileSize');
55
+ expect(standaloneMeterSource).not.toContain('fn auto_exposure_meter');
56
+ expect(standaloneMeterSource).not.toContain('@workgroup_size(16');
57
+ expect(AUTO_EXPOSURE_METER_WGSL).toContain('fn auto_exposure_histogram');
58
+ });
59
+ });
@@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest';
4
4
 
5
5
  const shaderRoot = resolve(import.meta.dirname, '..');
6
6
  const tonemap = readFileSync(resolve(shaderRoot, 'tonemap.wgsl'), 'utf8');
7
+ const outputEncoding = readFileSync(resolve(shaderRoot, 'output-encoding.wgsl'), 'utf8');
7
8
  const common = readFileSync(resolve(shaderRoot, 'common.wgsl'), 'utf8');
8
9
  const bloom = readFileSync(resolve(shaderRoot, 'bloom-blur.wgsl'), 'utf8');
9
10
  const fxaa = readFileSync(resolve(shaderRoot, 'fxaa.wgsl'), 'utf8');
@@ -34,32 +35,46 @@ describe('post shader color-domain contract', () => {
34
35
  it('declares Output Transform as the only display conversion owner', () => {
35
36
  const code = codeOnly(tonemap);
36
37
  expect(tonemap).toContain('Output Transform');
37
- expect(tonemap).toContain('display-encoded');
38
- expect(code.match(/linearToSrgbOetf\s*\(/g)).toHaveLength(1);
38
+ expect(tonemap).toContain('fs_encode_only');
39
+ expect(outputEncoding.match(/linearToSrgbOetf\s*\(/g)).toHaveLength(1);
40
+ expect(tonemap).toContain('#import forgeax_view::output_encoding');
39
41
  expect(code).toContain('let exposed : vec3<f32> = sample * params.exposure;');
40
- expect(code.indexOf('let source = textureSample')).toBeLessThan(code.indexOf('let exposed'));
41
- expect(code.indexOf('switch (params.mode)')).toBeLessThan(
42
- code.indexOf('linearToSrgbOetf(mapped)'),
43
- );
42
+ const mapping = functionBody(code, 'mapTonemap');
43
+ expect(mapping.indexOf('let exposed')).toBeGreaterThanOrEqual(0);
44
+ expect(mapping.indexOf('let exposed')).toBeLessThan(mapping.indexOf('switch (params.mode)'));
45
+ expect(code).toContain('encodeFinal(mapTonemap');
44
46
  });
45
47
 
46
48
  it('maps every public mode before one shared OETF', () => {
47
49
  const code = codeOnly(tonemap);
48
- const body = functionBody(code, 'fs_main');
50
+ const body = functionBody(code, 'mapTonemap');
49
51
  for (const mode of [1, 2, 3, 4, 5, 6, 7]) {
50
- expect(body).toMatch(new RegExp(`case ${mode}u:\\s*\\{\\s*mapped\\s*=\\s*tonemap`));
52
+ expect(body).toMatch(
53
+ new RegExp(`case ${mode}u:\\s*\\{\\s*mapped\\s*=\\s*(?:toneMap|tonemap)`),
54
+ );
51
55
  }
52
- expect(body).toMatch(/default:\s*\{\s*mapped\s*=\s*sample;\s*\}/);
53
- expect(body).toContain('let encoded = linearToSrgbOetf(mapped);');
54
- expect(body).toContain('let output = select(');
55
- expect(body).toContain('ditherUnorm8(encoded, in.position.xy)');
56
- expect(body).toContain('params.ditherEnabled > 0.5');
57
- expect(body).toContain('return vec4<f32>(output, source.a);');
58
- expect(body).not.toContain('linearToSrgbOetf(exposed)');
56
+ expect(body).toMatch(/default:\s*\{\s*mapped\s*=\s*toneMapLinearLdr\(sample\);\s*\}/);
57
+ expect(functionBody(code, 'fs_main')).toContain(
58
+ 'return encodeFinal(mapTonemap(source.rgb), source.a, in.position);',
59
+ );
60
+ expect(functionBody(code, 'fs_main')).not.toContain('linearToSrgbOetf(');
61
+ });
62
+
63
+ it('exposes tone-only and encoding-only entry points for the positive LUT path', () => {
64
+ const code = codeOnly(tonemap);
65
+ const toneOnly = functionBody(code, 'fs_tone_only');
66
+ const encodeOnly = functionBody(code, 'fs_encode_only');
67
+ expect(toneOnly).toContain('mapTonemap(source.rgb)');
68
+ expect(toneOnly).not.toContain('encodeOutput(');
69
+ expect(encodeOnly).toContain('return encodeFinal(source.rgb, source.a, in.position);');
70
+ expect(functionBody(code, 'encodeFinal')).toContain('encodeOutput(linearColor, alpha)');
71
+ expect(functionBody(code, 'encodeFinal')).toContain('ditherUnorm8(encoded.rgb, position.xy)');
72
+ expect(code.indexOf('fn fs_tone_only')).toBeLessThan(code.indexOf('fn fs_encode_only'));
59
73
  });
60
74
 
61
75
  it('keeps the sole OETF free of software precision emulation', () => {
62
76
  expect(common).not.toContain('quantizeToF16');
77
+ expect(outputEncoding).not.toContain('quantizeToF16');
63
78
  expect(tonemap).not.toContain('quantizeToF16');
64
79
  expect(tonemap).not.toContain('Float16');
65
80
  });
@@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url';
3
3
  import { describe, expect, it } from 'vitest';
4
4
 
5
5
  const shaderFiles = [
6
- 'default-standard-pbr.wgsl',
6
+ 'standard-surface.wgsl',
7
7
  'default-standard-pbr-skin.wgsl',
8
8
  'unlit.wgsl',
9
9
  'sprite.wgsl',
@@ -14,8 +14,9 @@ const surface = readFileSync(
14
14
  describe('standard PBR MASK alpha contract', () => {
15
15
  it('multiplies factor alpha by sampled texture alpha before discard', () => {
16
16
  expect(surface).toContain(
17
- 'clamp(material.baseColor.a * baseSample.a * vertexColor.a, 0.0, 1.0)',
17
+ 'clamp(materialValue.baseColor.a * baseSample.a * vertexColor.a, 0.0, 1.0)',
18
18
  );
19
+ expect(surface).toContain('return evaluate_standard_surface(input, material)');
19
20
  });
20
21
 
21
22
  it('matches Three r184 alphaTest by discarding values equal to or below cutoff', () => {
@@ -38,12 +38,72 @@ describe('default Standard PBR transmission manifest contract', () => {
38
38
 
39
39
  expect(standard).toBeDefined();
40
40
  if (standard === undefined) return;
41
- // Nine declared axes produce 512 combinations. The compiler publishes
42
- // the supported subset: storage is required by cluster/probe/extended
43
- // lighting, and extended lighting also requires the projector lane.
44
- expect(standard.variants).toHaveLength(224);
45
- expect(new Set(standard.variants.map((variant) => variant.definesKey)).size).toBe(224);
46
- expect(new Set(standard.variants.map((variant) => variant.composedWgsl)).size).toBe(224);
41
+ const transmissionVariants = standard.variants.filter(
42
+ (variant) => 'TRANSMISSION_AVAILABLE' in variant.defines,
43
+ );
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. GPU-driven
47
+ // scene-index and reflection fallback are storage-backed lanes as well.
48
+ // Keep the expected keys derived here so adding a new axis cannot silently
49
+ // shrink the published set.
50
+ const expectedKeys = new Set<string>();
51
+ for (const clusterForwardAvailable of [false, true]) {
52
+ for (const storageBufferAvailable of [false, true]) {
53
+ for (const vertexColorAvailable of [false, true]) {
54
+ for (const probeBlendAvailable of [false, true]) {
55
+ for (const extendedLightingAvailable of [false, true]) {
56
+ for (const directionalPcssAvailable of [false, true]) {
57
+ for (const transmissionAvailable of [false, true]) {
58
+ for (const projectorAvailable of [false, true]) {
59
+ for (const gpuDrivenSceneIndexAvailable of [false, true]) {
60
+ for (const reflectionFallbackAvailable of [false, true]) {
61
+ if (
62
+ (clusterForwardAvailable && !storageBufferAvailable) ||
63
+ (probeBlendAvailable && !storageBufferAvailable) ||
64
+ (extendedLightingAvailable &&
65
+ (!storageBufferAvailable || !projectorAvailable)) ||
66
+ (gpuDrivenSceneIndexAvailable && !storageBufferAvailable)
67
+ ) {
68
+ continue;
69
+ }
70
+ const expectedKeyEntries = [
71
+ `CLUSTER_FORWARD_AVAILABLE=${clusterForwardAvailable}`,
72
+ `DIRECTIONAL_PCSS_AVAILABLE=${directionalPcssAvailable}`,
73
+ `EXTENDED_LIGHTING_AVAILABLE=${extendedLightingAvailable}`,
74
+ `GPU_DRIVEN_SCENE_INDEX_AVAILABLE=${gpuDrivenSceneIndexAvailable}`,
75
+ `PROBE_BLEND_AVAILABLE=${probeBlendAvailable}`,
76
+ `PROJECTOR_AVAILABLE=${projectorAvailable}`,
77
+ `REFLECTION_FALLBACK_AVAILABLE=${reflectionFallbackAvailable}`,
78
+ `STORAGE_BUFFER_AVAILABLE=${storageBufferAvailable}`,
79
+ `TRANSMISSION_AVAILABLE=${transmissionAvailable}`,
80
+ `VERTEX_COLOR_AVAILABLE=${vertexColorAvailable}`,
81
+ ];
82
+ expectedKeys.add(
83
+ expectedKeyEntries.every((entry) => entry.endsWith('=true'))
84
+ ? ''
85
+ : expectedKeyEntries.join('+'),
86
+ );
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ expect(transmissionVariants).toHaveLength(expectedKeys.size);
98
+ expect(new Set(transmissionVariants.map((variant) => variant.definesKey)).size).toBe(
99
+ expectedKeys.size,
100
+ );
101
+ expect(new Set(transmissionVariants.map((variant) => variant.definesKey))).toEqual(
102
+ expectedKeys,
103
+ );
104
+ expect(new Set(standard.variants.map((variant) => variant.composedWgsl)).size).toBe(
105
+ expectedKeys.size,
106
+ );
47
107
  expect(
48
108
  standard.variants.every(
49
109
  (variant) =>
@@ -62,6 +122,69 @@ describe('default Standard PBR transmission manifest contract', () => {
62
122
  ),
63
123
  ),
64
124
  ).toBe(true);
125
+ for (const clusterForwardAvailable of [false, true]) {
126
+ for (const storageBufferAvailable of [false, true]) {
127
+ if (clusterForwardAvailable && !storageBufferAvailable) continue;
128
+ for (const vertexColorAvailable of [false, true]) {
129
+ for (const probeBlendAvailable of [false, true]) {
130
+ for (const extendedLightingAvailable of [false, true]) {
131
+ for (const directionalPcssAvailable of [false, true]) {
132
+ for (const transmissionAvailable of [false, true]) {
133
+ for (const projectorAvailable of [false, true]) {
134
+ for (const gpuDrivenSceneIndexAvailable of [false, true]) {
135
+ for (const reflectionFallbackAvailable of [false, true]) {
136
+ if (
137
+ (probeBlendAvailable && !storageBufferAvailable) ||
138
+ (extendedLightingAvailable &&
139
+ (!storageBufferAvailable || !projectorAvailable)) ||
140
+ (gpuDrivenSceneIndexAvailable && !storageBufferAvailable)
141
+ ) {
142
+ continue;
143
+ }
144
+ const expectedKeyEntries = [
145
+ `CLUSTER_FORWARD_AVAILABLE=${clusterForwardAvailable}`,
146
+ `DIRECTIONAL_PCSS_AVAILABLE=${directionalPcssAvailable}`,
147
+ `EXTENDED_LIGHTING_AVAILABLE=${extendedLightingAvailable}`,
148
+ `GPU_DRIVEN_SCENE_INDEX_AVAILABLE=${gpuDrivenSceneIndexAvailable}`,
149
+ `PROBE_BLEND_AVAILABLE=${probeBlendAvailable}`,
150
+ `PROJECTOR_AVAILABLE=${projectorAvailable}`,
151
+ `REFLECTION_FALLBACK_AVAILABLE=${reflectionFallbackAvailable}`,
152
+ `STORAGE_BUFFER_AVAILABLE=${storageBufferAvailable}`,
153
+ `TRANSMISSION_AVAILABLE=${transmissionAvailable}`,
154
+ `VERTEX_COLOR_AVAILABLE=${vertexColorAvailable}`,
155
+ ];
156
+ const expectedKey = expectedKeyEntries.every((entry) =>
157
+ entry.endsWith('=true'),
158
+ )
159
+ ? ''
160
+ : expectedKeyEntries.join('+');
161
+ expect(transmissionVariants).toContainEqual(
162
+ expect.objectContaining({
163
+ definesKey: expectedKey,
164
+ defines: expect.objectContaining({
165
+ CLUSTER_FORWARD_AVAILABLE: clusterForwardAvailable,
166
+ DIRECTIONAL_PCSS_AVAILABLE: directionalPcssAvailable,
167
+ EXTENDED_LIGHTING_AVAILABLE: extendedLightingAvailable,
168
+ GPU_DRIVEN_SCENE_INDEX_AVAILABLE: gpuDrivenSceneIndexAvailable,
169
+ PROBE_BLEND_AVAILABLE: probeBlendAvailable,
170
+ PROJECTOR_AVAILABLE: projectorAvailable,
171
+ REFLECTION_FALLBACK_AVAILABLE: reflectionFallbackAvailable,
172
+ STORAGE_BUFFER_AVAILABLE: storageBufferAvailable,
173
+ TRANSMISSION_AVAILABLE: transmissionAvailable,
174
+ VERTEX_COLOR_AVAILABLE: vertexColorAvailable,
175
+ }),
176
+ }),
177
+ );
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
186
+ }
187
+ }
65
188
  expect(standard.variants.every((variant) => 'TRANSMISSION_AVAILABLE' in variant.defines)).toBe(
66
189
  true,
67
190
  );
@@ -93,6 +216,44 @@ describe('default Standard PBR transmission manifest contract', () => {
93
216
  );
94
217
  });
95
218
 
219
+ it('keeps material-index varying GPU-only while preserving the scene-index variant', {
220
+ timeout: 60_000,
221
+ }, async () => {
222
+ const manifest = await engineManifest();
223
+ const standard = manifest.materialShaders.find(
224
+ (entry) => entry.identifier === 'forgeax::default-standard-pbr',
225
+ );
226
+ expect(standard).toBeDefined();
227
+ if (standard === undefined) return;
228
+
229
+ const sceneIndex = standard.variants.find(
230
+ (variant) =>
231
+ variant.defines.GPU_DRIVEN_SCENE_INDEX_AVAILABLE === true &&
232
+ variant.defines.STORAGE_BUFFER_AVAILABLE === true &&
233
+ variant.defines.TRANSMISSION_AVAILABLE === false &&
234
+ variant.defines.CLUSTER_FORWARD_AVAILABLE === false &&
235
+ variant.defines.VERTEX_COLOR_AVAILABLE === false,
236
+ );
237
+ expect(sceneIndex).toBeDefined();
238
+ expect(sceneIndex?.composedWgsl).toMatch(/out(?:_\d+)?\.materialIndex\s*=\s*materialIndex/);
239
+
240
+ const directFallback = standard.variants.find(
241
+ (variant) =>
242
+ variant.defines.GPU_DRIVEN_SCENE_INDEX_AVAILABLE === false &&
243
+ variant.defines.STORAGE_BUFFER_AVAILABLE === false &&
244
+ variant.defines.TRANSMISSION_AVAILABLE === false &&
245
+ variant.defines.CLUSTER_FORWARD_AVAILABLE === false &&
246
+ variant.defines.VERTEX_COLOR_AVAILABLE === false,
247
+ );
248
+ expect(directFallback).toBeDefined();
249
+ expect(directFallback?.composedWgsl).not.toMatch(
250
+ /@location\(15\)\s+@interpolate\(flat\)\s+materialIndex/,
251
+ );
252
+ expect(directFallback?.composedWgsl).not.toMatch(
253
+ /out(?:_\d+)?\.materialIndex\s*=\s*materialIndex/,
254
+ );
255
+ });
256
+
96
257
  it('publishes the same explicit transmission axis for the skin Standard template', {
97
258
  timeout: 60_000,
98
259
  }, async () => {
@@ -26,7 +26,7 @@ describe('DirectLightSlot WGSL ABI', () => {
26
26
 
27
27
  it('keeps the slot type owned by the unified Cluster shader consumer', async () => {
28
28
  const common = await readShader('common.wgsl');
29
- const standard = await readShader('default-standard-pbr.wgsl');
29
+ const standard = await readShader('standard-surface.wgsl');
30
30
  const clustered = await readShader('standard-cluster.wgsl');
31
31
 
32
32
  expect(common).toContain('struct DirectLightSlot');
@@ -40,6 +40,14 @@ describe('direct punctual lighting shader contract', () => {
40
40
  expect(source).not.toMatch(/intensityMultiplier|magicMultiplier|profile/);
41
41
  });
42
42
 
43
+ it('keeps point-shadow cube direction and bias arguments in the shared convention', async () => {
44
+ const source = await readFile(shaderPath, 'utf8');
45
+
46
+ expect(source).toContain('let lightLocal = fromLight;');
47
+ expect(source).not.toContain('vec3<f32>(fromLight.x, fromLight.y, -fromLight.z)');
48
+ expect(source).toContain('depthRef, normalBias, depthBias, nDotL,');
49
+ });
50
+
43
51
  it('leaves spot direction normalization to extract instead of HDRP', async () => {
44
52
  const source = await readFile(shaderPath, 'utf8');
45
53
 
@@ -47,6 +55,26 @@ describe('direct punctual lighting shader contract', () => {
47
55
  expect(source).not.toContain('normalize(lightDir)');
48
56
  });
49
57
 
58
+ it('keeps the point receiver on the canonical normalized depth bias', async () => {
59
+ const source = await readFile(join(dirname(shaderPath), 'shadow-pcf.wgsl'), 'utf8');
60
+ const point = source.slice(source.indexOf('fn sample_shadow_cube_hw2x2('));
61
+ expect(point).toContain('shadow_biased_receiver_depth(depthRef, normalBias, depthBias, nDotL)');
62
+ expect(point).not.toContain('depthBias / 1000');
63
+ });
64
+
65
+ it('shares default Surface evaluation between CPU, GPU-driven and skinned Standard', async () => {
66
+ for (const name of ['default-standard-pbr.wgsl', 'default-standard-pbr-skin.wgsl']) {
67
+ const source = await readFile(join(dirname(shaderPath), name), 'utf8');
68
+ expect(source).toContain('slot::surface::{evaluate_surface, evaluate_standard_surface}');
69
+ expect(source).not.toContain('fn evaluateSelectedStandardSurface');
70
+ }
71
+ const source = await readFile(
72
+ join(dirname(shaderPath), 'default_standard_surface.wgsl'),
73
+ 'utf8',
74
+ );
75
+ expect(source).toContain('return evaluate_standard_surface(input, material);');
76
+ });
77
+
50
78
  it('shares Three r184 direct-PBR Fresnel and multiscatter ownership', async () => {
51
79
  const [source, brdf, directional] = await Promise.all([
52
80
  readFile(shaderPath, 'utf8'),
@@ -105,7 +133,7 @@ describe('direct punctual lighting shader contract', () => {
105
133
  const source = await readFile(pbrShaderPath, 'utf8');
106
134
 
107
135
  expect(source).toMatch(
108
- /evaluateStandardClusterLights\(\s*in\.ndc\.xyz,\s*standardViewZ\(in\),\s*in\.worldPos,\s*physicalNormal,\s*v,\s*diffuseAlbedo,\s*metallic,\s*a,\s*f0,\s*false,?\s*\)/u,
136
+ /evaluateStandardClusterLights\(\s*in\.ndc\.xyz,\s*(?:standardViewZ\(in\)|in\.viewZ),\s*in\.worldPos,\s*physicalNormal,\s*v,\s*diffuseAlbedo,\s*metallic,\s*a,\s*f0,\s*false,\s*(?:true|factors\.receiveShadows)\s*,?\s*\)/u,
109
137
  );
110
138
  expect(source).toContain('clearcoatAlpha,');
111
139
  expect(source).toContain('vec3<f32>(0.04),');
@@ -1,3 +1,4 @@
1
+ import { Buffer } from 'node:buffer';
1
2
  import { describe, expect, it } from 'vitest';
2
3
  import {
3
4
  generateLtcTables,
@@ -9,6 +10,14 @@ import {
9
10
  LTC_TABLES,
10
11
  } from '../ltc/tables';
11
12
 
13
+ function tableBytes(table: Uint16Array): Buffer {
14
+ return Buffer.from(table.buffer, table.byteOffset, table.byteLength);
15
+ }
16
+
17
+ function expectSameTableBytes(left: Uint16Array, right: Uint16Array): void {
18
+ expect(Buffer.compare(tableBytes(left), tableBytes(right))).toBe(0);
19
+ }
20
+
12
21
  describe('LTC provenance and tracked data contract', () => {
13
22
  it('pins the fixed Three.js reference and two 64 by 64 rgba16float tables', () => {
14
23
  expect(LTC_SOURCE_PROVENANCE.source).toBe('Three.js');
@@ -23,8 +32,10 @@ describe('LTC provenance and tracked data contract', () => {
23
32
  const first = generateLtcTables();
24
33
  const second = generateLtcTables();
25
34
 
26
- expect(first).toEqual(second);
27
- expect(first).toEqual(LTC_TABLES);
35
+ expectSameTableBytes(first.lambert, second.lambert);
36
+ expectSameTableBytes(first.ggx, second.ggx);
37
+ expectSameTableBytes(first.lambert, LTC_TABLES.lambert);
38
+ expectSameTableBytes(first.ggx, LTC_TABLES.ggx);
28
39
  expect(hashLtcTable(first.lambert)).toBe(LTC_TABLE_HASHES.lambert);
29
40
  expect(hashLtcTable(first.ggx)).toBe(LTC_TABLE_HASHES.ggx);
30
41
  expect(LTC_TABLE_HASHES.lambert).toMatch(/^[a-f0-9]{64}$/);
@@ -45,6 +45,7 @@ describe('built-in MaterialAsset sources', () => {
45
45
 
46
46
  expect(material.kind).toBe('material');
47
47
  expect(pass?.program.module).toBe(BUILTIN_MATERIAL_MODULES[kind]);
48
+ expect(pass?.renderState?.tags).toEqual({ LightMode: 'Forward' });
48
49
  expect(pass).not.toHaveProperty('shader');
49
50
  expect(material.parameters?.length).toBeGreaterThan(0);
50
51
  expect(material.values).toBeDefined();