@forgeax/engine-shader 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +99 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/ShaderRegistry.d.ts +208 -0
- package/dist/ShaderRegistry.d.ts.map +1 -0
- package/dist/__tests__/atmosphere-background.test.d.ts +2 -0
- package/dist/__tests__/atmosphere-background.test.d.ts.map +1 -0
- package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts +2 -0
- package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts.map +1 -0
- package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts +2 -0
- package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts.map +1 -0
- package/dist/__tests__/atmosphere-preetham-red.test.d.ts +2 -0
- package/dist/__tests__/atmosphere-preetham-red.test.d.ts.map +1 -0
- package/dist/__tests__/atmosphere-preetham.test.d.ts +2 -0
- package/dist/__tests__/atmosphere-preetham.test.d.ts.map +1 -0
- package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts +2 -0
- package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts.map +1 -0
- package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts +2 -0
- package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts.map +1 -0
- package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts +2 -0
- package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts.map +1 -0
- package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts +2 -0
- package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts.map +1 -0
- package/dist/__tests__/deferred-lighting-ssao.test.d.ts +2 -0
- package/dist/__tests__/deferred-lighting-ssao.test.d.ts.map +1 -0
- package/dist/__tests__/fog-oracle.unit.test.d.ts +2 -0
- package/dist/__tests__/fog-oracle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts +2 -0
- package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-matrix.integration.test.d.ts +2 -0
- package/dist/__tests__/fog-producer-matrix.integration.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-ssao-compile.test.d.ts +2 -0
- package/dist/__tests__/hdrp-ssao-compile.test.d.ts.map +1 -0
- package/dist/__tests__/ibl-irradiance.unit.test.d.ts +2 -0
- package/dist/__tests__/ibl-irradiance.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ibl-sampling.unit.test.d.ts +2 -0
- package/dist/__tests__/ibl-sampling.unit.test.d.ts.map +1 -0
- package/dist/__tests__/lighting-punctual.unit.test.d.ts +2 -0
- package/dist/__tests__/lighting-punctual.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-artifact-dedup.unit.test.d.ts +2 -0
- package/dist/__tests__/material-artifact-dedup.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-builtins.unit.test.d.ts +2 -0
- package/dist/__tests__/material-builtins.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/material-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-derived-builtins.integration.test.d.ts +2 -0
- package/dist/__tests__/material-derived-builtins.integration.test.d.ts.map +1 -0
- package/dist/__tests__/public-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/public-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scene-temporal.unit.test.d.ts +2 -0
- package/dist/__tests__/scene-temporal.unit.test.d.ts.map +1 -0
- package/dist/__tests__/shader-registry-transaction.unit.test.d.ts +2 -0
- package/dist/__tests__/shader-registry-transaction.unit.test.d.ts.map +1 -0
- package/dist/__tests__/shader.unit.test.d.ts +2 -0
- package/dist/__tests__/shader.unit.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-lit-shader.test.d.ts +2 -0
- package/dist/__tests__/sprite-lit-shader.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-variants.unit.test.d.ts +2 -0
- package/dist/__tests__/sprite-variants.unit.test.d.ts.map +1 -0
- package/dist/__tests__/taa-resolve.unit.test.d.ts +2 -0
- package/dist/__tests__/taa-resolve.unit.test.d.ts.map +1 -0
- package/dist/__tests__/transparent-pbr.unit.test.d.ts +2 -0
- package/dist/__tests__/transparent-pbr.unit.test.d.ts.map +1 -0
- package/dist/__tests__/vertex-color-variant.unit.test.d.ts +2 -0
- package/dist/__tests__/vertex-color-variant.unit.test.d.ts.map +1 -0
- package/dist/errors.d.ts +96 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +560 -0
- package/dist/index.mjs.map +1 -0
- package/dist/material/artifact-registry.d.ts +31 -0
- package/dist/material/artifact-registry.d.ts.map +1 -0
- package/dist/material/artifact-types.d.ts +13 -0
- package/dist/material/artifact-types.d.ts.map +1 -0
- package/dist/material-schemas.d.ts +8 -0
- package/dist/material-schemas.d.ts.map +1 -0
- package/dist/register-default-sprite-lit.d.ts +40 -0
- package/dist/register-default-sprite-lit.d.ts.map +1 -0
- package/dist/register-default-standard-pbr-skin.d.ts +34 -0
- package/dist/register-default-standard-pbr-skin.d.ts.map +1 -0
- package/dist/tonemap.d.ts +13 -0
- package/dist/tonemap.d.ts.map +1 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +58 -0
- package/src/ShaderRegistry.ts +519 -0
- package/src/__tests__/atmosphere-background.test.ts +33 -0
- package/src/__tests__/atmosphere-cubemap-orientation.test.ts +68 -0
- package/src/__tests__/atmosphere-preetham-numeric.test.ts +122 -0
- package/src/__tests__/atmosphere-preetham-red.test.ts +52 -0
- package/src/__tests__/atmosphere-preetham.test.ts +132 -0
- package/src/__tests__/atmosphere-three-r184-reference.test.ts +122 -0
- package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +31 -0
- package/src/__tests__/builtin-texture-sampling-contract.test.ts +56 -0
- package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +19 -0
- package/src/__tests__/deferred-lighting-ssao.test.ts +114 -0
- package/src/__tests__/fog-oracle.unit.test.ts +132 -0
- package/src/__tests__/fog-orthographic-ray.unit.test.ts +62 -0
- package/src/__tests__/fog-producer-matrix.integration.test.ts +77 -0
- package/src/__tests__/hdrp-ssao-compile.test.ts +185 -0
- package/src/__tests__/ibl-irradiance.unit.test.ts +43 -0
- package/src/__tests__/ibl-sampling.unit.test.ts +40 -0
- package/src/__tests__/lighting-punctual.unit.test.ts +66 -0
- package/src/__tests__/manifest.fixture.json +22 -0
- package/src/__tests__/material-artifact-dedup.unit.test.ts +21 -0
- package/src/__tests__/material-builtins.unit.test.ts +111 -0
- package/src/__tests__/material-contract.unit.test.ts +79 -0
- package/src/__tests__/material-derived-builtins.integration.test.ts +30 -0
- package/src/__tests__/public-surface.unit.test.ts +9 -0
- package/src/__tests__/scene-temporal.unit.test.ts +32 -0
- package/src/__tests__/shader-registry-transaction.unit.test.ts +116 -0
- package/src/__tests__/shader.unit.test.ts +1909 -0
- package/src/__tests__/sprite-lit-shader.test.ts +240 -0
- package/src/__tests__/sprite-variants.unit.test.ts +209 -0
- package/src/__tests__/taa-resolve.unit.test.ts +32 -0
- package/src/__tests__/transparent-pbr.unit.test.ts +21 -0
- package/src/__tests__/vertex-color-variant.unit.test.ts +52 -0
- package/src/atmosphere-background.wgsl +80 -0
- package/src/atmosphere-cubemap.wgsl +72 -0
- package/src/atmosphere-ibl.wgsl +127 -0
- package/src/atmosphere-preetham.wgsl +214 -0
- package/src/bloom-blur.wgsl +89 -0
- package/src/bloom-bright.wgsl +50 -0
- package/src/bloom-composite.wgsl +54 -0
- package/src/brdf.wgsl +25 -0
- package/src/common.wgsl +523 -0
- package/src/default-standard-pbr-skin.wgsl +538 -0
- package/src/default-standard-pbr.wgsl +794 -0
- package/src/errors.ts +169 -0
- package/src/fog.wgsl +27 -0
- package/src/fxaa.wgsl +259 -0
- package/src/hdrp-cluster-forward.wgsl +186 -0
- package/src/hdrp-deferred-lighting.wgsl +69 -0
- package/src/hdrp-ssao.wgsl +174 -0
- package/src/ibl-brdf-lut.wgsl +92 -0
- package/src/ibl-equirect-to-cube.wgsl +56 -0
- package/src/ibl-irradiance.wgsl +89 -0
- package/src/ibl-prefilter.wgsl +101 -0
- package/src/ibl-sampling.wgsl +58 -0
- package/src/ibl-shared.wgsl +118 -0
- package/src/index.ts +166 -0
- package/src/lighting-directional.wgsl +471 -0
- package/src/lighting-punctual.wgsl +274 -0
- package/src/material/artifact-registry.ts +78 -0
- package/src/material/artifact-types.ts +26 -0
- package/src/material-schemas.ts +48 -0
- package/src/msdf-text.wgsl +208 -0
- package/src/pbr-temporal.wgsl +73 -0
- package/src/register-default-sprite-lit.ts +87 -0
- package/src/register-default-standard-pbr-skin.ts +74 -0
- package/src/shadow-pcf.wgsl +120 -0
- package/src/shadow_caster.wgsl +54 -0
- package/src/skybox.wgsl +75 -0
- package/src/sprite-lit.wgsl +307 -0
- package/src/sprite.wgsl +365 -0
- package/src/taa-resolve.wgsl +129 -0
- package/src/tbn.wgsl +54 -0
- package/src/tonemap.ts +13 -0
- package/src/tonemap.wgsl +177 -0
- package/src/types.ts +59 -0
- package/src/unlit.wgsl +186 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { derive } from '@forgeax/engine-types';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
BUILTIN_MATERIAL_MODULES,
|
|
7
|
+
type BuiltinMaterialKind,
|
|
8
|
+
createBuiltinMaterialAsset,
|
|
9
|
+
isEngineMaterial,
|
|
10
|
+
isEngineMaterialModule,
|
|
11
|
+
} from '../index.js';
|
|
12
|
+
import type { MaterialShaderArtifact } from '../material/artifact-types.js';
|
|
13
|
+
import { DEFAULT_STANDARD_PBR_PARAM_SCHEMA } from '../material-schemas.js';
|
|
14
|
+
|
|
15
|
+
const builtinSources = [
|
|
16
|
+
'default-standard-pbr.wgsl',
|
|
17
|
+
'default-standard-pbr-skin.wgsl',
|
|
18
|
+
'unlit.wgsl',
|
|
19
|
+
'sprite.wgsl',
|
|
20
|
+
'sprite-lit.wgsl',
|
|
21
|
+
'msdf-text.wgsl',
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
const kinds: readonly BuiltinMaterialKind[] = ['standard', 'unlit', 'sprite'];
|
|
25
|
+
|
|
26
|
+
function source(file: string): string {
|
|
27
|
+
return readFileSync(fileURLToPath(new URL(`../${file}`, import.meta.url)), 'utf8');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('built-in MaterialAsset sources', () => {
|
|
31
|
+
it('keeps Engine-owned module identity in one shared predicate', () => {
|
|
32
|
+
expect(isEngineMaterial(createBuiltinMaterialAsset('standard'))).toBe(true);
|
|
33
|
+
expect(isEngineMaterialModule('forgeax::default-standard-pbr')).toBe(true);
|
|
34
|
+
expect(isEngineMaterialModule('game::custom')).toBe(false);
|
|
35
|
+
expect(
|
|
36
|
+
isEngineMaterial({
|
|
37
|
+
passes: [{ name: 'forward', program: { module: 'game::custom' } }],
|
|
38
|
+
}),
|
|
39
|
+
).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it.each(kinds)('uses one authored MaterialAsset contract for %s', (kind) => {
|
|
43
|
+
const material = createBuiltinMaterialAsset(kind);
|
|
44
|
+
const pass = material.passes?.[0];
|
|
45
|
+
|
|
46
|
+
expect(material.kind).toBe('material');
|
|
47
|
+
expect(pass?.program.module).toBe(BUILTIN_MATERIAL_MODULES[kind]);
|
|
48
|
+
expect(pass).not.toHaveProperty('shader');
|
|
49
|
+
expect(material.parameters?.length).toBeGreaterThan(0);
|
|
50
|
+
expect(material.values).toBeDefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('keeps cooked artifact identity separate from authored values', () => {
|
|
54
|
+
const material = createBuiltinMaterialAsset('standard');
|
|
55
|
+
const artifact: MaterialShaderArtifact = {
|
|
56
|
+
material: 'builtin-standard',
|
|
57
|
+
pass: material.passes?.[0]?.name ?? 'forward',
|
|
58
|
+
wgsl: 'builtin-standard-wgsl',
|
|
59
|
+
layoutIdentity: 'sha256-builtin-standard-layout',
|
|
60
|
+
bindings: [],
|
|
61
|
+
deps: [BUILTIN_MATERIAL_MODULES.standard],
|
|
62
|
+
vertexInputs: [],
|
|
63
|
+
specializationKey: 'builtin-standard-key',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
expect(artifact.specializationKey).toBe('builtin-standard-key');
|
|
67
|
+
expect(artifact.deps).toEqual([BUILTIN_MATERIAL_MODULES.standard]);
|
|
68
|
+
expect(material.values?.baseColor).toEqual([1, 1, 1, 1]);
|
|
69
|
+
expect(
|
|
70
|
+
{ ...material, values: { ...material.values, baseColor: [0.2, 0.3, 0.4, 1] } }.values,
|
|
71
|
+
).toMatchObject({ baseColor: [0.2, 0.3, 0.4, 1] });
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('built-in and custom material derived layout matrix', () => {
|
|
76
|
+
it('models emissive and occlusion as real standard material texture parameters', () => {
|
|
77
|
+
const names = DEFAULT_STANDARD_PBR_PARAM_SCHEMA.map((entry) => entry.name);
|
|
78
|
+
expect(names).toContain('emissiveTexture');
|
|
79
|
+
expect(names).toContain('occlusionTexture');
|
|
80
|
+
expect(
|
|
81
|
+
DEFAULT_STANDARD_PBR_PARAM_SCHEMA.filter((entry) => entry.type === 'texture2d'),
|
|
82
|
+
).toHaveLength(6);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('derives a coordinate record for every texture parameter in an interleaved custom schema', () => {
|
|
86
|
+
const derived = derive([
|
|
87
|
+
{ name: 'before', type: 'vec4' },
|
|
88
|
+
{ name: 'albedo', type: 'texture2d' },
|
|
89
|
+
{ name: 'roughness', type: 'f32' },
|
|
90
|
+
{ name: 'normal', type: 'texture2d' },
|
|
91
|
+
]);
|
|
92
|
+
expect(derived.coordinateRecords.map((record) => record.parameter)).toEqual([
|
|
93
|
+
'albedo',
|
|
94
|
+
'normal',
|
|
95
|
+
]);
|
|
96
|
+
expect(derived.coordinateRecords.every((record) => record.size === 32)).toBe(true);
|
|
97
|
+
expect(derived.coordinateRecords[0]?.offset).not.toBe(derived.coordinateRecords[1]?.offset);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('keeps the built-in source matrix on the derived coordinate contract', () => {
|
|
101
|
+
for (const file of builtinSources) {
|
|
102
|
+
const text = source(file);
|
|
103
|
+
expect(text, `${file} must expose derived coordinate members`).toContain(
|
|
104
|
+
'CoordinatesTransform',
|
|
105
|
+
);
|
|
106
|
+
expect(text, `${file} must not carry a fixed coordinate tail`).not.toContain(
|
|
107
|
+
'textureScalePadding',
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { MaterialArtifactRegistry } from '../material/artifact-registry.js';
|
|
6
|
+
import { DEFAULT_STANDARD_PBR_PARAM_SCHEMA } from '../material-schemas.js';
|
|
7
|
+
|
|
8
|
+
const alphaSchema = JSON.parse(
|
|
9
|
+
readFileSync(
|
|
10
|
+
fileURLToPath(
|
|
11
|
+
new URL(
|
|
12
|
+
'../../../../apps/parity/color-lighting/schemas/material-alpha.schema.json',
|
|
13
|
+
import.meta.url,
|
|
14
|
+
),
|
|
15
|
+
),
|
|
16
|
+
'utf8',
|
|
17
|
+
),
|
|
18
|
+
) as object;
|
|
19
|
+
const validateAlpha = new Ajv2020({ allErrors: true, strict: false }).compile(alphaSchema);
|
|
20
|
+
|
|
21
|
+
describe('standard PBR material alpha contract', () => {
|
|
22
|
+
it('keeps one immutable cooked artifact per specialization key', () => {
|
|
23
|
+
const registry = new MaterialArtifactRegistry();
|
|
24
|
+
const first = registry.register({
|
|
25
|
+
key: 'forgeax::standard:forward',
|
|
26
|
+
bytes: Uint8Array.of(1, 2, 3),
|
|
27
|
+
digest: 'sha256:standard-forward',
|
|
28
|
+
});
|
|
29
|
+
const conflict = registry.register({
|
|
30
|
+
key: 'forgeax::standard:forward',
|
|
31
|
+
bytes: Uint8Array.of(9),
|
|
32
|
+
digest: 'sha256:changed',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(first.ok).toBe(true);
|
|
36
|
+
expect(conflict).toMatchObject({
|
|
37
|
+
ok: false,
|
|
38
|
+
error: { code: 'material-artifact-conflict', detail: { key: 'forgeax::standard:forward' } },
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('owns RGBA and factor-times-texture alpha in the baseColor schema', () => {
|
|
43
|
+
const baseColor = DEFAULT_STANDARD_PBR_PARAM_SCHEMA.find((entry) => entry.name === 'baseColor');
|
|
44
|
+
const alphaCutoff = DEFAULT_STANDARD_PBR_PARAM_SCHEMA.find(
|
|
45
|
+
(entry) => entry.name === 'alphaCutoff',
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(baseColor).toMatchObject({
|
|
49
|
+
name: 'baseColor',
|
|
50
|
+
type: 'color',
|
|
51
|
+
default: [1, 1, 1, 1],
|
|
52
|
+
});
|
|
53
|
+
expect(alphaCutoff).toMatchObject({ name: 'alphaCutoff', type: 'f32' });
|
|
54
|
+
expect(DEFAULT_STANDARD_PBR_PARAM_SCHEMA.map((entry) => entry.name)).not.toContain(
|
|
55
|
+
'baseColorAlpha',
|
|
56
|
+
);
|
|
57
|
+
expect(DEFAULT_STANDARD_PBR_PARAM_SCHEMA.map((entry) => entry.name)).not.toContain(
|
|
58
|
+
'textureAlphaFactor',
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it.each([
|
|
63
|
+
['short RGBA', { baseColor: [1, 1, 1], alphaMode: 'MASK' }],
|
|
64
|
+
['unknown alpha mode', { baseColor: [1, 1, 1, 1], alphaMode: 'DITHER' }],
|
|
65
|
+
['negative cutoff', { baseColor: [1, 1, 1, 1], alphaMode: 'MASK', alphaCutoff: -0.1 }],
|
|
66
|
+
])('rejects %s before GPU execution', (_name, value) => {
|
|
67
|
+
expect(validateAlpha(value)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('keeps the glTF MASK default cutoff explicit in the schema', () => {
|
|
71
|
+
const properties = alphaSchema as {
|
|
72
|
+
readonly properties?: {
|
|
73
|
+
readonly alphaCutoff?: { readonly default?: number };
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
expect(properties.properties?.alphaCutoff?.default).toBe(0.5);
|
|
77
|
+
expect(validateAlpha({ baseColor: [1, 1, 1, 0.5], alphaMode: 'MASK' })).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { derive } from '@forgeax/engine-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { DEFAULT_STANDARD_PBR_PARAM_SCHEMA } from '../material-schemas.js';
|
|
4
|
+
|
|
5
|
+
describe('material derived built-in integration contract', () => {
|
|
6
|
+
it('uses the same derived identity for standard and skinned standard schemas', () => {
|
|
7
|
+
const standard = derive(DEFAULT_STANDARD_PBR_PARAM_SCHEMA);
|
|
8
|
+
const skinned = derive(DEFAULT_STANDARD_PBR_PARAM_SCHEMA);
|
|
9
|
+
expect(skinned.layoutIdentity).toBe(standard.layoutIdentity);
|
|
10
|
+
expect(skinned.totalBytes).toBe(standard.totalBytes);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('includes a coordinate member pair for every built-in texture binding', () => {
|
|
14
|
+
const derived = derive(DEFAULT_STANDARD_PBR_PARAM_SCHEMA);
|
|
15
|
+
const textureNames = DEFAULT_STANDARD_PBR_PARAM_SCHEMA.filter(
|
|
16
|
+
(entry) => entry.type === 'texture2d',
|
|
17
|
+
).map((entry) => entry.name);
|
|
18
|
+
expect(derived.coordinateRecords.map((record) => record.parameter)).toEqual(textureNames);
|
|
19
|
+
expect(
|
|
20
|
+
derived.coordinateRecords.every((record) =>
|
|
21
|
+
record.transformMember.endsWith('CoordinatesTransform'),
|
|
22
|
+
),
|
|
23
|
+
).toBe(true);
|
|
24
|
+
expect(
|
|
25
|
+
derived.coordinateRecords.every((record) =>
|
|
26
|
+
record.metadataMember.endsWith('CoordinatesMetadata'),
|
|
27
|
+
),
|
|
28
|
+
).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import * as publicSurface from '../index.js';
|
|
3
|
+
|
|
4
|
+
describe('shader public surface', () => {
|
|
5
|
+
test('keeps the runtime registry without a version mirror', () => {
|
|
6
|
+
expect(typeof publicSurface.ShaderRegistry).toBe('function');
|
|
7
|
+
expect('SHADER_PACKAGE_VERSION' in publicSurface).toBe(false);
|
|
8
|
+
});
|
|
9
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const localTemporalShaders = ['unlit.wgsl', 'sprite.wgsl', 'sprite-lit.wgsl'] as const;
|
|
6
|
+
|
|
7
|
+
const pbrTemporalConsumers = [
|
|
8
|
+
'default-standard-pbr.wgsl',
|
|
9
|
+
'default-standard-pbr-skin.wgsl',
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
describe('scene temporal varyings', () => {
|
|
13
|
+
it.each(localTemporalShaders)('%s linearly interpolates both clip authorities', (file) => {
|
|
14
|
+
const source = readFileSync(resolve(import.meta.dirname, '..', file), 'utf8');
|
|
15
|
+
expect(source).toMatch(/@interpolate\(linear\) currentClip : vec4<f32>/);
|
|
16
|
+
expect(source).toMatch(/@interpolate\(linear\) previousClip : vec4<f32>/);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('keeps PBR UV selection and fragment projection in one module', () => {
|
|
20
|
+
const source = readFileSync(resolve(import.meta.dirname, '..', 'pbr-temporal.wgsl'), 'utf8');
|
|
21
|
+
expect(source).toContain('fn transformedPbrTemporalUv');
|
|
22
|
+
expect(source).toContain('fn projectPbrSceneTemporal');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it.each(pbrTemporalConsumers)('%s imports the shared PBR temporal authority', (file) => {
|
|
26
|
+
const source = readFileSync(resolve(import.meta.dirname, '..', file), 'utf8');
|
|
27
|
+
expect(source).toContain('#import forgeax_pbr::temporal::{projectPbrSceneTemporal}');
|
|
28
|
+
expect(source).toMatch(/@interpolate\(linear\) currentClip : vec4<f32>/);
|
|
29
|
+
expect(source).toMatch(/@interpolate\(linear\) previousClip : vec4<f32>/);
|
|
30
|
+
expect(source).not.toContain('fn transformedTemporalUv');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { ShaderModule } from '@forgeax/engine-rhi';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { type ManifestEntry, ShaderRegistry, type ShaderRegistryDevice } from '../index.js';
|
|
4
|
+
|
|
5
|
+
const FIRST_ENTRY: ManifestEntry = {
|
|
6
|
+
hash: 'first123',
|
|
7
|
+
wgsl: 'first-wgsl',
|
|
8
|
+
glsl: undefined,
|
|
9
|
+
bindings: '[]',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const SECOND_ENTRY: ManifestEntry = {
|
|
13
|
+
hash: 'second12',
|
|
14
|
+
wgsl: 'second-wgsl',
|
|
15
|
+
glsl: '',
|
|
16
|
+
bindings: '[]',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const CORRECTED_MATERIAL_SHADER = {
|
|
20
|
+
identifier: 'my-game::corrected',
|
|
21
|
+
sourcePath: 'src/corrected.wgsl',
|
|
22
|
+
composedWgsl: 'corrected-wgsl',
|
|
23
|
+
paramSchema: '[]',
|
|
24
|
+
variants: [],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const CORRECTED_MANIFEST = {
|
|
28
|
+
entries: [SECOND_ENTRY, FIRST_ENTRY],
|
|
29
|
+
materialShaders: [CORRECTED_MATERIAL_SHADER],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function createTrackingDevice(): { device: ShaderRegistryDevice; created: string[] } {
|
|
33
|
+
const created: string[] = [];
|
|
34
|
+
const device: ShaderRegistryDevice = {
|
|
35
|
+
createShaderModule({ code }) {
|
|
36
|
+
created.push(code);
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
value: {} as ShaderModule,
|
|
40
|
+
} as ReturnType<ShaderRegistryDevice['createShaderModule']>;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
return { device, created };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function dataUrl(): string {
|
|
47
|
+
return 'data:application/json,shader-registry-transaction';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('ShaderRegistry public manifest transaction', () => {
|
|
51
|
+
it.each([
|
|
52
|
+
{
|
|
53
|
+
name: 'a malformed later entry',
|
|
54
|
+
manifest: {
|
|
55
|
+
entries: [FIRST_ENTRY, { ...SECOND_ENTRY, bindings: 42 }],
|
|
56
|
+
materialShaders: [CORRECTED_MATERIAL_SHADER],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'a malformed material row',
|
|
61
|
+
manifest: {
|
|
62
|
+
entries: [FIRST_ENTRY, SECOND_ENTRY],
|
|
63
|
+
materialShaders: [{ identifier: 'my-game::malformed' }],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
])('refuses $name atomically and retries the same source on the same registry', async ({
|
|
67
|
+
manifest,
|
|
68
|
+
}) => {
|
|
69
|
+
let currentManifest: unknown = manifest;
|
|
70
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async () => {
|
|
71
|
+
return new Response(JSON.stringify(currentManifest), {
|
|
72
|
+
headers: { 'content-type': 'application/json' },
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
const { device, created } = createTrackingDevice();
|
|
76
|
+
const manifestUrl = dataUrl();
|
|
77
|
+
const registry = new ShaderRegistry({ device, manifestUrl });
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const refused = await registry.loadManifest();
|
|
81
|
+
expect(refused.ok).toBe(false);
|
|
82
|
+
if (refused.ok) return;
|
|
83
|
+
expect(refused.error.code).toBe('manifest-malformed');
|
|
84
|
+
expect(Array.from(registry.entries())).toEqual([]);
|
|
85
|
+
expect(Array.from(registry.materialShaderManifestEntries())).toEqual([]);
|
|
86
|
+
expect(registry.get(FIRST_ENTRY.hash).ok).toBe(false);
|
|
87
|
+
expect(created).toEqual([]);
|
|
88
|
+
|
|
89
|
+
currentManifest = CORRECTED_MANIFEST;
|
|
90
|
+
const repaired = await registry.loadManifest();
|
|
91
|
+
expect(repaired.ok).toBe(true);
|
|
92
|
+
expect(Array.from(registry.entries()).map((entry) => entry.hash)).toEqual([
|
|
93
|
+
SECOND_ENTRY.hash,
|
|
94
|
+
FIRST_ENTRY.hash,
|
|
95
|
+
]);
|
|
96
|
+
expect(
|
|
97
|
+
Array.from(registry.materialShaderManifestEntries()).map((entry) => entry.identifier),
|
|
98
|
+
).toEqual([CORRECTED_MATERIAL_SHADER.identifier]);
|
|
99
|
+
|
|
100
|
+
const module = registry.get(SECOND_ENTRY.hash);
|
|
101
|
+
expect(module.ok).toBe(true);
|
|
102
|
+
expect(created).toEqual([SECOND_ENTRY.wgsl]);
|
|
103
|
+
|
|
104
|
+
const reloaded = await registry.loadManifest();
|
|
105
|
+
expect(reloaded.ok).toBe(true);
|
|
106
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
107
|
+
expect(Array.from(registry.entries()).map((entry) => entry.hash)).toEqual([
|
|
108
|
+
SECOND_ENTRY.hash,
|
|
109
|
+
FIRST_ENTRY.hash,
|
|
110
|
+
]);
|
|
111
|
+
expect(created).toEqual([SECOND_ENTRY.wgsl]);
|
|
112
|
+
} finally {
|
|
113
|
+
fetchSpy.mockRestore();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|