@forgeax/engine-render 0.1.24 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -1
- package/dist/assembly/factory.d.ts +1 -1
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/material/surface-projection.d.ts +24 -0
- package/dist/assembly/material/surface-projection.d.ts.map +1 -0
- package/dist/assembly/material-shader-policy.d.ts +20 -0
- package/dist/assembly/material-shader-policy.d.ts.map +1 -1
- package/dist/assembly/webgpu-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer.d.ts +1 -1
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/assembly/webgpu-vertex-layouts.d.ts +89 -0
- package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -0
- package/dist/assets/asset-decoders.d.ts.map +1 -1
- package/dist/authoring.mjs +1 -1
- package/dist/{chunk-Q752SHI5.mjs → chunk-ADHHVYLW.mjs} +3 -3
- package/dist/chunk-ADHHVYLW.mjs.map +1 -0
- package/dist/{chunk-TFH5I7JJ.mjs → chunk-E34VL5VI.mjs} +3 -3
- package/dist/{chunk-TFH5I7JJ.mjs.map → chunk-E34VL5VI.mjs.map} +1 -1
- package/dist/{chunk-KZFTK6DO.mjs → chunk-HKXTW355.mjs} +7 -4
- package/dist/chunk-HKXTW355.mjs.map +1 -0
- package/dist/{chunk-YKB5DQV7.mjs → chunk-JSEDTZYS.mjs} +87 -19
- package/dist/chunk-JSEDTZYS.mjs.map +1 -0
- package/dist/{chunk-UQYS7FRM.mjs → chunk-KM2NOX2I.mjs} +4 -4
- package/dist/{chunk-UQYS7FRM.mjs.map → chunk-KM2NOX2I.mjs.map} +1 -1
- package/dist/{chunk-Z3YYH2MB.mjs → chunk-N3RT2EUR.mjs} +94 -41
- package/dist/chunk-N3RT2EUR.mjs.map +1 -0
- package/dist/{chunk-YRUYGA4A.mjs → chunk-TCLDX7SZ.mjs} +559 -138
- package/dist/chunk-TCLDX7SZ.mjs.map +1 -0
- package/dist/{chunk-RR5L4SRS.mjs → chunk-ZZ4YQ474.mjs} +2 -2
- package/dist/{chunk-RR5L4SRS.mjs.map → chunk-ZZ4YQ474.mjs.map} +1 -1
- package/dist/components/instances.d.ts +13 -31
- package/dist/components/instances.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +865 -142
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +6 -1
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +4 -0
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +27 -9
- package/dist/index.mjs.map +1 -1
- package/dist/instance-buffer-cache.d.ts +7 -1
- package/dist/instance-buffer-cache.d.ts.map +1 -1
- package/dist/instances.d.ts +101 -0
- package/dist/instances.d.ts.map +1 -0
- package/dist/internal.mjs +5 -5
- package/dist/materials.d.ts +63 -2
- package/dist/materials.d.ts.map +1 -1
- package/dist/pipeline-builder.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +13 -2
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +22 -3
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +2 -1
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts +11 -1
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts +1 -1
- package/dist/recovery/render-system-candidate.d.ts.map +1 -1
- package/dist/render-contract.d.ts +4 -0
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts +4 -0
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +25 -1
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +4 -1
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene-types.d.ts +2 -0
- package/dist/scene/render-scene-types.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +12 -1
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/systems/skin-palette-allocator.d.ts +6 -0
- package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
- package/dist/temporal/index.mjs +5 -5
- package/package.json +20 -20
- package/src/__tests__/gpu-scene.unit.test.ts +38 -2
- package/src/__tests__/instances-store.unit.test.ts +46 -0
- package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
- package/src/__tests__/material-contract-inventory.unit.test.ts +5 -13
- package/src/__tests__/material-pass-policy.unit.test.ts +69 -0
- package/src/__tests__/material-snapshot-mutation.integration.test.ts +69 -0
- package/src/__tests__/material-surface-authoring.test-d.ts +62 -0
- package/src/__tests__/materials-transmission-validation.unit.test.ts +24 -0
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +0 -4
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +42 -0
- package/src/__tests__/skin-motion-regression.unit.test.ts +33 -0
- package/src/__tests__/skinned-shadow-caster.test.ts +30 -2
- package/src/__tests__/surface-adjacent-regressions.integration.test.ts +60 -0
- package/src/__tests__/surface-standard-pipeline.browser.test.ts +20 -0
- package/src/__tests__/surface-standard-pipeline.dawn.test.ts +20 -0
- package/src/__tests__/surface-standard-pipeline.fixture.ts +132 -0
- package/src/__tests__/surface-variant-provenance.integration.test.ts +33 -0
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +1 -1
- package/src/assembly/factory.ts +2 -0
- package/src/assembly/material/__tests__/standard-layer-plan-projection.integration.test.ts +18 -0
- package/src/assembly/material/__tests__/surface-pass-projection.integration.test.ts +62 -0
- package/src/assembly/material/surface-projection.ts +90 -0
- package/src/assembly/material-shader-policy.ts +88 -10
- package/src/assembly/recovery/renderer-recover.ts +1 -1
- package/src/assembly/webgpu-ready.ts +3 -16
- package/src/assembly/webgpu-renderer.ts +90 -158
- package/src/assembly/webgpu-vertex-layouts.ts +132 -0
- package/src/assets/asset-decoders.ts +27 -4
- package/src/components/instances.ts +14 -197
- package/src/gpu-driven/production-raster.ts +34 -1
- package/src/gpu-scene.ts +84 -29
- package/src/index.ts +1 -0
- package/src/instance-buffer-cache.ts +14 -1
- package/src/instances.ts +288 -0
- package/src/materials.ts +107 -12
- package/src/pipeline-builder.ts +14 -10
- package/src/pipeline-spec.ts +6 -14
- package/src/record/frame-snapshot.ts +19 -2
- package/src/record/frame.ts +31 -2
- package/src/record/main-pass-geometry.ts +498 -59
- package/src/record/main-pass-sprite-draws.ts +1 -1
- package/src/record/render-context.ts +4 -0
- package/src/record/shadow-pass.ts +46 -99
- package/src/recovery/render-system-candidate.ts +41 -4
- package/src/render-contract.ts +4 -0
- package/src/render-system-extract-tail.ts +107 -51
- package/src/render-system-extract.ts +53 -26
- package/src/render-system.ts +40 -5
- package/src/scene/render-scene-types.ts +2 -0
- package/src/scene/render-scene.ts +579 -36
- package/src/systems/skin-palette-allocator.ts +129 -7
- package/src/transmission/__tests__/surface-compatibility.integration.test.ts +32 -0
- package/dist/chunk-KZFTK6DO.mjs.map +0 -1
- package/dist/chunk-Q752SHI5.mjs.map +0 -1
- package/dist/chunk-YKB5DQV7.mjs.map +0 -1
- package/dist/chunk-YRUYGA4A.mjs.map +0 -1
- package/dist/chunk-Z3YYH2MB.mjs.map +0 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { AssetGuid, type PackageId } from '@forgeax/engine-pack/guid';
|
|
2
|
+
import type { MaterialAsset } from '@forgeax/engine-types';
|
|
3
|
+
import { deriveStandardLayerPlan } from '@forgeax/engine-types';
|
|
4
|
+
|
|
5
|
+
type SurfacePack = {
|
|
6
|
+
packageId: PackageId;
|
|
7
|
+
build: () =>
|
|
8
|
+
| { readonly ok: true; readonly value: Readonly<Record<string, MaterialAsset>> }
|
|
9
|
+
| Promise<{ readonly ok: true; readonly value: Readonly<Record<string, MaterialAsset>> }>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type ViteImportMeta = ImportMeta & {
|
|
13
|
+
glob<T>(
|
|
14
|
+
patterns: readonly string[],
|
|
15
|
+
options: { readonly eager: true; readonly import: 'default' },
|
|
16
|
+
): Readonly<Record<string, T>>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const packModules = Object.values(
|
|
20
|
+
(import.meta as ViteImportMeta).glob<SurfacePack>(
|
|
21
|
+
[
|
|
22
|
+
'../../../../templates/game-3d/assets/materials.pack.ts',
|
|
23
|
+
'../../../../apps/preview/assets/surface-standard-evidence.pack.ts',
|
|
24
|
+
],
|
|
25
|
+
{
|
|
26
|
+
eager: true,
|
|
27
|
+
import: 'default',
|
|
28
|
+
},
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
if (packModules.length !== 2)
|
|
32
|
+
throw new Error('surface-evidence-fixture: required pack module missing');
|
|
33
|
+
|
|
34
|
+
const builtPacks = packModules.map((pack) => {
|
|
35
|
+
const built = pack.build();
|
|
36
|
+
if (built instanceof Promise || !built.ok) {
|
|
37
|
+
throw new Error('surface-evidence-fixture: materials pack did not produce outputs');
|
|
38
|
+
}
|
|
39
|
+
return { packageId: pack.packageId, values: built.value };
|
|
40
|
+
});
|
|
41
|
+
const materials = Object.assign({}, ...builtPacks.map((pack) => pack.values)) as Record<
|
|
42
|
+
string,
|
|
43
|
+
MaterialAsset | undefined
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
function guidForSourceKey(sourceKey: string): AssetGuid {
|
|
47
|
+
const owner = builtPacks.find((pack) => Object.hasOwn(pack.values, sourceKey));
|
|
48
|
+
if (owner === undefined) {
|
|
49
|
+
throw new Error(`surface-evidence-fixture: source key '${sourceKey}' is not published`);
|
|
50
|
+
}
|
|
51
|
+
return AssetGuid.derive(owner.packageId, sourceKey);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rustedIron = materials['material/rusted-iron'];
|
|
55
|
+
|
|
56
|
+
const defaultBase = materials['material/ground'];
|
|
57
|
+
const defaultPhysical = materials['material/painted'];
|
|
58
|
+
const customPhysical = materials['material/rusted-iron-custom-physical'];
|
|
59
|
+
if (
|
|
60
|
+
rustedIron === undefined ||
|
|
61
|
+
defaultBase === undefined ||
|
|
62
|
+
defaultPhysical === undefined ||
|
|
63
|
+
customPhysical === undefined
|
|
64
|
+
)
|
|
65
|
+
throw new Error('surface-evidence-fixture: required material row missing');
|
|
66
|
+
|
|
67
|
+
export const SURFACE_CLOSURE = 'forgeax_material::surface_v1';
|
|
68
|
+
export const SURFACE_SOURCE_CLOSURE = [
|
|
69
|
+
'templates/game-3d/assets/materials.pack.ts',
|
|
70
|
+
'templates/game-3d/assets/shaders/rusted-iron.wgsl',
|
|
71
|
+
] as const;
|
|
72
|
+
export const SURFACE_SOURCE_PATH = 'templates/game-3d/assets/shaders/rusted-iron.wgsl';
|
|
73
|
+
export const SURFACE_MATERIAL_SOURCE_KEY = 'material/rusted-iron';
|
|
74
|
+
export const EVIDENCE_SOURCE_SHA =
|
|
75
|
+
(import.meta as ImportMeta & { env?: Record<string, string | undefined> }).env
|
|
76
|
+
?.VITE_FORGEAX_EVIDENCE_SOURCE_SHA ?? 'unavailable';
|
|
77
|
+
|
|
78
|
+
export const SURFACE_CASES = [
|
|
79
|
+
{
|
|
80
|
+
id: 'default-base',
|
|
81
|
+
material: defaultBase,
|
|
82
|
+
materialGuid: AssetGuid.format(guidForSourceKey('material/ground')),
|
|
83
|
+
surfaceModule: defaultBase.passes?.[0]?.program.moduleSlots?.surface ?? '',
|
|
84
|
+
physical: false,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'custom-base',
|
|
88
|
+
material: rustedIron,
|
|
89
|
+
materialGuid: AssetGuid.format(guidForSourceKey('material/rusted-iron')),
|
|
90
|
+
surfaceModule: rustedIron.passes?.[0]?.program.moduleSlots?.surface ?? '',
|
|
91
|
+
physical: false,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'default-physical',
|
|
95
|
+
material: defaultPhysical,
|
|
96
|
+
materialGuid: '019fb7ce-3200-7000-8000-000000000002',
|
|
97
|
+
surfaceModule: defaultPhysical.passes?.[0]?.program.moduleSlots?.surface ?? '',
|
|
98
|
+
physical: true,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'custom-physical',
|
|
102
|
+
material: customPhysical,
|
|
103
|
+
materialGuid: '019fb7ce-3f00-7000-8000-000000000001',
|
|
104
|
+
surfaceModule: customPhysical.passes?.[0]?.program.moduleSlots?.surface ?? '',
|
|
105
|
+
physical: true,
|
|
106
|
+
},
|
|
107
|
+
] as const;
|
|
108
|
+
|
|
109
|
+
export function layerPlanFor(surfaceCase: (typeof SURFACE_CASES)[number]) {
|
|
110
|
+
return deriveStandardLayerPlan(
|
|
111
|
+
surfaceCase.physical ? (surfaceCase.material.parameters ?? []) : [],
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function baseColorFor(
|
|
116
|
+
surfaceCase: (typeof SURFACE_CASES)[number],
|
|
117
|
+
): readonly [number, number, number] {
|
|
118
|
+
const values = surfaceCase.material.values ?? {};
|
|
119
|
+
const value = values.baseColor ?? values.ironColor;
|
|
120
|
+
if (!Array.isArray(value) || value.length < 3) {
|
|
121
|
+
throw new Error(`surface-evidence-fixture: missing base color for ${surfaceCase.id}`);
|
|
122
|
+
}
|
|
123
|
+
return [Number(value[0]), Number(value[1]), Number(value[2])];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function passFor(surfaceCase: (typeof SURFACE_CASES)[number]): 'forward' | 'deferred' {
|
|
127
|
+
return layerPlanFor(surfaceCase).mode === 'physical' ? 'forward' : 'deferred';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function publicationGenerationFor(_surfaceCase: (typeof SURFACE_CASES)[number]): number {
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
projectStandardSurfacePasses,
|
|
4
|
+
type StandardSurfaceProjectionOptions,
|
|
5
|
+
} from '../assembly/material/surface-projection';
|
|
6
|
+
|
|
7
|
+
type FullCustomFixture = StandardSurfaceProjectionOptions & {
|
|
8
|
+
readonly surfaceKind: 'full-custom';
|
|
9
|
+
readonly declaredPasses: readonly ('forward' | 'deferred' | 'shadow-caster')[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('full-custom Surface provenance', () => {
|
|
13
|
+
it('does not synthesize an undeclared Deferred pass or fallback identity', () => {
|
|
14
|
+
const options: FullCustomFixture = {
|
|
15
|
+
surfaceModule: 'game_3d::full_custom_surface',
|
|
16
|
+
values: { tint: [1, 0.1, 0.05, 1] },
|
|
17
|
+
surfaceKind: 'full-custom',
|
|
18
|
+
declaredPasses: ['forward', 'shadow-caster'],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const passes = projectStandardSurfacePasses(options);
|
|
22
|
+
|
|
23
|
+
expect(passes.map((pass) => pass.name)).toEqual(['forward', 'shadow-caster']);
|
|
24
|
+
expect(passes.map((pass) => pass.program.module)).toEqual([
|
|
25
|
+
'forgeax_material::standard',
|
|
26
|
+
'forgeax_material::standard',
|
|
27
|
+
]);
|
|
28
|
+
for (const pass of passes) {
|
|
29
|
+
expect(pass.program.moduleSlots?.surface).toBe(options.surfaceModule);
|
|
30
|
+
expect((pass.renderState?.tags as Record<string, string>).SurfaceKind).toBe('full-custom');
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
package/src/assembly/factory.ts
CHANGED
|
@@ -65,7 +65,9 @@ export {
|
|
|
65
65
|
isSharedMaterialUserRegionCompatible,
|
|
66
66
|
normalizeMaterialShaderVariantSet,
|
|
67
67
|
projectMaterialPipeline,
|
|
68
|
+
resolveMaterialShaderBackendArtifactKey,
|
|
68
69
|
resolveMaterialShaderBindingContract,
|
|
70
|
+
resolveMaterialShaderUvSetCount,
|
|
69
71
|
resolveMaterialShaderVariantSet,
|
|
70
72
|
resolveMaterialShaderVertexInputContract,
|
|
71
73
|
routeMaterialPipeline,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { deriveStandardLayerPlan } from '@forgeax/engine-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { projectStandardSurfacePasses } from '../surface-projection.js';
|
|
4
|
+
|
|
5
|
+
describe('StandardLayerPlan pass projection', () => {
|
|
6
|
+
it('keeps a physical Standard material on Forward and shadow passes', () => {
|
|
7
|
+
const plan = deriveStandardLayerPlan([
|
|
8
|
+
{ name: 'clearcoat', type: 'f32' },
|
|
9
|
+
{ name: 'clearcoatRoughness', type: 'f32' },
|
|
10
|
+
]);
|
|
11
|
+
const passes = projectStandardSurfacePasses({
|
|
12
|
+
surfaceModule: 'game::surface',
|
|
13
|
+
values: {},
|
|
14
|
+
layerPlan: plan,
|
|
15
|
+
});
|
|
16
|
+
expect(passes.map((pass) => pass.name)).toEqual(['forward', 'shadow-caster']);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { deriveStandardLayerPlan } from '@forgeax/engine-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import {
|
|
4
|
+
projectStandardSurfacePasses,
|
|
5
|
+
type StandardSurfaceProjectionOptions,
|
|
6
|
+
} from '../surface-projection';
|
|
7
|
+
|
|
8
|
+
type SurfaceFixture = StandardSurfaceProjectionOptions & {
|
|
9
|
+
readonly geometryVariant: 'rigid' | 'skinned' | 'instanced';
|
|
10
|
+
readonly lightingLane: 'direct' | 'clustered';
|
|
11
|
+
readonly alphaClip: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const fixture = (geometryVariant: SurfaceFixture['geometryVariant']): SurfaceFixture => ({
|
|
15
|
+
surfaceModule: 'game_3d::rusted_iron_surface',
|
|
16
|
+
values: { ironColor: [0.42, 0.47, 0.5, 1] },
|
|
17
|
+
geometryVariant,
|
|
18
|
+
lightingLane: geometryVariant === 'instanced' ? 'clustered' : 'direct',
|
|
19
|
+
alphaClip: geometryVariant === 'skinned',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function tagsFor(pass: ReturnType<typeof projectStandardSurfacePasses>[number]) {
|
|
23
|
+
return pass.renderState?.tags as Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('Standard Surface pass projection', () => {
|
|
27
|
+
it.each([
|
|
28
|
+
'rigid',
|
|
29
|
+
'skinned',
|
|
30
|
+
'instanced',
|
|
31
|
+
] as const)('projects one %s Surface closure into every Standard pass', (geometryVariant) => {
|
|
32
|
+
const options = fixture(geometryVariant);
|
|
33
|
+
const passes = projectStandardSurfacePasses(options);
|
|
34
|
+
|
|
35
|
+
expect(passes.map((pass) => pass.name)).toEqual(['forward', 'deferred', 'shadow-caster']);
|
|
36
|
+
for (const pass of passes) {
|
|
37
|
+
expect(pass.program.moduleSlots?.surface).toBe(options.surfaceModule);
|
|
38
|
+
expect(tagsFor(pass)).toMatchObject({
|
|
39
|
+
SurfaceModule: options.surfaceModule,
|
|
40
|
+
GeometryVariant: geometryVariant,
|
|
41
|
+
LightingLane: options.lightingLane,
|
|
42
|
+
AlphaClip: options.alphaClip ? 'enabled' : 'disabled',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('keeps base Standard on Deferred while physical Standard stays Forward-only', () => {
|
|
48
|
+
const base = projectStandardSurfacePasses({
|
|
49
|
+
...fixture('rigid'),
|
|
50
|
+
layerPlan: deriveStandardLayerPlan([]),
|
|
51
|
+
});
|
|
52
|
+
const physical = projectStandardSurfacePasses({
|
|
53
|
+
...fixture('rigid'),
|
|
54
|
+
layerPlan: deriveStandardLayerPlan([
|
|
55
|
+
{ name: 'clearcoat', type: 'f32' },
|
|
56
|
+
{ name: 'clearcoatRoughness', type: 'f32' },
|
|
57
|
+
]),
|
|
58
|
+
});
|
|
59
|
+
expect(base.map((pass) => pass.name)).toEqual(['forward', 'deferred', 'shadow-caster']);
|
|
60
|
+
expect(physical.map((pass) => pass.name)).toEqual(['forward', 'shadow-caster']);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { DEFAULT_STANDARD_SURFACE_MODULE } from '@forgeax/engine-shader';
|
|
2
|
+
import type {
|
|
3
|
+
MaterialPass,
|
|
4
|
+
MaterialRenderState,
|
|
5
|
+
MaterialValue,
|
|
6
|
+
StandardLayerPlan,
|
|
7
|
+
} from '@forgeax/engine-types';
|
|
8
|
+
|
|
9
|
+
export const STANDARD_MATERIAL_MODULE = 'forgeax_material::standard';
|
|
10
|
+
export { DEFAULT_STANDARD_SURFACE_MODULE };
|
|
11
|
+
|
|
12
|
+
export type StandardSurfacePassName = 'forward' | 'deferred' | 'shadow-caster';
|
|
13
|
+
export type StandardSurfaceGeometryVariant = 'rigid' | 'skinned' | 'instanced';
|
|
14
|
+
export type StandardSurfaceLightingLane = 'direct' | 'clustered';
|
|
15
|
+
export type StandardSurfaceKind = 'standard' | 'full-custom';
|
|
16
|
+
|
|
17
|
+
export interface StandardSurfaceProjectionOptions {
|
|
18
|
+
readonly surfaceModule: string;
|
|
19
|
+
readonly values: Readonly<Record<string, MaterialValue | null>>;
|
|
20
|
+
readonly renderState?: MaterialRenderState;
|
|
21
|
+
readonly castShadow?: boolean;
|
|
22
|
+
readonly queue?: number;
|
|
23
|
+
readonly geometryVariant?: StandardSurfaceGeometryVariant;
|
|
24
|
+
readonly lightingLane?: StandardSurfaceLightingLane;
|
|
25
|
+
readonly alphaClip?: boolean;
|
|
26
|
+
readonly surfaceKind?: StandardSurfaceKind;
|
|
27
|
+
readonly declaredPasses?: readonly StandardSurfacePassName[];
|
|
28
|
+
readonly layerPlan?: StandardLayerPlan;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function pass(
|
|
32
|
+
name: StandardSurfacePassName,
|
|
33
|
+
options: StandardSurfaceProjectionOptions,
|
|
34
|
+
fragmentEntry: string,
|
|
35
|
+
): MaterialPass {
|
|
36
|
+
const authoredState = options.renderState ?? {};
|
|
37
|
+
const authoredRecord = authoredState as Readonly<Record<string, unknown>>;
|
|
38
|
+
const tags = (authoredRecord.tags ?? {}) as Readonly<Record<string, string>>;
|
|
39
|
+
const lightMode =
|
|
40
|
+
name === 'shadow-caster' ? 'ShadowCaster' : name === 'forward' ? 'Forward' : 'Deferred';
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
program: {
|
|
44
|
+
module: STANDARD_MATERIAL_MODULE,
|
|
45
|
+
fragmentEntry,
|
|
46
|
+
moduleSlots: { surface: options.surfaceModule },
|
|
47
|
+
},
|
|
48
|
+
renderState: {
|
|
49
|
+
...authoredState,
|
|
50
|
+
tags:
|
|
51
|
+
options.surfaceModule === DEFAULT_STANDARD_SURFACE_MODULE
|
|
52
|
+
? { LightMode: lightMode, ...tags }
|
|
53
|
+
: {
|
|
54
|
+
...tags,
|
|
55
|
+
LightMode: lightMode,
|
|
56
|
+
SurfaceModule: options.surfaceModule,
|
|
57
|
+
SurfaceKind: options.surfaceKind ?? 'standard',
|
|
58
|
+
GeometryVariant: options.geometryVariant ?? 'rigid',
|
|
59
|
+
LightingLane: options.lightingLane ?? 'direct',
|
|
60
|
+
AlphaClip: options.alphaClip === true ? 'enabled' : 'disabled',
|
|
61
|
+
},
|
|
62
|
+
...(options.queue === undefined ? {} : { queue: options.queue }),
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Project one custom or default Surface into the existing Standard pass family. */
|
|
68
|
+
export function projectStandardSurfacePasses(
|
|
69
|
+
options: StandardSurfaceProjectionOptions,
|
|
70
|
+
): [MaterialPass, ...MaterialPass[]] {
|
|
71
|
+
const authoredPasses = options.declaredPasses;
|
|
72
|
+
const passNames: StandardSurfacePassName[] =
|
|
73
|
+
options.surfaceKind === 'full-custom' && authoredPasses !== undefined
|
|
74
|
+
? [...authoredPasses]
|
|
75
|
+
: (['forward'] as StandardSurfacePassName[]);
|
|
76
|
+
if (authoredPasses === undefined || options.surfaceKind !== 'full-custom') {
|
|
77
|
+
const blended = options.renderState?.blend !== undefined;
|
|
78
|
+
if (!blended && options.layerPlan?.mode !== 'physical') passNames.push('deferred');
|
|
79
|
+
if (options.castShadow !== false) passNames.push('shadow-caster');
|
|
80
|
+
}
|
|
81
|
+
const entries = passNames.map((name) =>
|
|
82
|
+
pass(
|
|
83
|
+
name,
|
|
84
|
+
options,
|
|
85
|
+
name === 'forward' ? 'fs_main' : name === 'deferred' ? 'fs_gbuffer' : 'fs_shadow',
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
const passes = entries as [MaterialPass, ...MaterialPass[]];
|
|
89
|
+
return passes;
|
|
90
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type MaterialShaderManifestEntry,
|
|
13
13
|
type ShaderCatalog,
|
|
14
14
|
type ShaderCatalogDevice,
|
|
15
|
+
STANDARD_PIPELINE_PARAM_SCHEMA,
|
|
15
16
|
standardPhysicalTextureFields,
|
|
16
17
|
} from '@forgeax/engine-shader';
|
|
17
18
|
import type { AssetGuid as AssetGuidType, ParamSchemaEntry } from '@forgeax/engine-types';
|
|
@@ -282,11 +283,10 @@ export async function prepareMaterialShaders(
|
|
|
282
283
|
const capValue = (rhiDevice.limits as Readonly<Record<string, number>>)
|
|
283
284
|
.maxStorageBuffersPerShaderStage;
|
|
284
285
|
const webgl2Downlevel = rhiDevice.caps.backendKind === 'wgpu-webgl2';
|
|
285
|
-
|
|
286
|
-
if (typeof capValue === 'number') {
|
|
286
|
+
const storageBufferCapable = rhiDevice.caps.storageBuffer;
|
|
287
|
+
if (storageBufferCapable && typeof capValue === 'number') {
|
|
287
288
|
const capCheck = assertStorageBufferCap(capValue);
|
|
288
289
|
if (!capCheck.ok) throw capCheck.error;
|
|
289
|
-
storageBufferCapable = !webgl2Downlevel && capCheck.value;
|
|
290
290
|
}
|
|
291
291
|
const extendedLightingShaderAvailable = deriveExtendedLightingCapability(rhiDevice).admitted;
|
|
292
292
|
const projectorAvailable =
|
|
@@ -314,9 +314,10 @@ export async function prepareMaterialShaders(
|
|
|
314
314
|
// engine-entry compile path, NOT by installMaterialArtifact.
|
|
315
315
|
if (msEntry.identifier.startsWith('forgeax::engine-')) continue;
|
|
316
316
|
// buildVariantKey logic: sorted key=value pairs joined with +; all-true = ''.
|
|
317
|
-
const variantDefines: Record<string, boolean> = {
|
|
318
|
-
|
|
319
|
-
|
|
317
|
+
const variantDefines: Record<string, boolean> = {};
|
|
318
|
+
if (msEntry.variants.some((variant) => 'STORAGE_BUFFER_AVAILABLE' in variant.defines)) {
|
|
319
|
+
variantDefines.STORAGE_BUFFER_AVAILABLE = storageBufferCapable;
|
|
320
|
+
}
|
|
320
321
|
// User material manifests may declare WEBGL2_COMPAT alongside the shared
|
|
321
322
|
// mesh-storage axis. The fallback backend must select that authored
|
|
322
323
|
// branch at boot; otherwise it asks for the impossible bare
|
|
@@ -450,11 +451,12 @@ export async function prepareMaterialShaders(
|
|
|
450
451
|
(candidate) => candidate.identifier === shadowCasterIdentifier,
|
|
451
452
|
);
|
|
452
453
|
if (manifestEntry !== undefined) {
|
|
453
|
-
// shadow_caster
|
|
454
|
-
//
|
|
454
|
+
// shadow_caster evaluates the same default Surface as Standard. Keep
|
|
455
|
+
// its generated MaterialParameters contract aligned with the Standard
|
|
456
|
+
// pipeline even when a legacy manifest omitted the material entry.
|
|
455
457
|
registry.installMaterialArtifact(shadowCasterIdentifier, {
|
|
456
458
|
source: manifestEntry.composedWgsl,
|
|
457
|
-
paramSchema:
|
|
459
|
+
paramSchema: STANDARD_PIPELINE_PARAM_SCHEMA,
|
|
458
460
|
});
|
|
459
461
|
const shaderGuid = ENGINE_SHADER_GUIDS.get(shadowCasterIdentifier);
|
|
460
462
|
if (shaderGuid !== undefined) {
|
|
@@ -462,7 +464,7 @@ export async function prepareMaterialShaders(
|
|
|
462
464
|
kind: 'shader' as const,
|
|
463
465
|
name: shadowCasterIdentifier,
|
|
464
466
|
source: manifestEntry.composedWgsl,
|
|
465
|
-
paramSchema:
|
|
467
|
+
paramSchema: STANDARD_PIPELINE_PARAM_SCHEMA,
|
|
466
468
|
};
|
|
467
469
|
// feat-20260614 M8 (D-17): catalogue by GUID; no handle minted.
|
|
468
470
|
assets.catalog(shaderGuid, shaderAsset);
|
|
@@ -659,6 +661,82 @@ export function resolveMaterialShaderVertexInputContract(
|
|
|
659
661
|
: 'none';
|
|
660
662
|
}
|
|
661
663
|
|
|
664
|
+
/**
|
|
665
|
+
* Resolve the number of UV sets declared by one material artifact.
|
|
666
|
+
*
|
|
667
|
+
* Runtime-published Surface programs do not necessarily have a manifest
|
|
668
|
+
* reflection row, so the authored vertex input is the fallback source of
|
|
669
|
+
* truth. The renderer then feeds this count into the geometry alias
|
|
670
|
+
* projection, which clamps missing mesh UV sets to the last available set.
|
|
671
|
+
*/
|
|
672
|
+
export function resolveMaterialShaderUvSetCount(
|
|
673
|
+
source: string | undefined,
|
|
674
|
+
reflectedCount?: number,
|
|
675
|
+
): number | undefined {
|
|
676
|
+
if (reflectedCount !== undefined) return reflectedCount;
|
|
677
|
+
if (source === undefined) return undefined;
|
|
678
|
+
|
|
679
|
+
const withoutComments = source.replace(/\/\*[\s\S]*?\*\//gu, '').replace(/\/\/.*$/gmu, '');
|
|
680
|
+
const inputStructs = [
|
|
681
|
+
...withoutComments.matchAll(/struct\s+([A-Za-z_]\w*)\s*\{([^}]*)\}/gu),
|
|
682
|
+
].filter((match) => /@location\s*\(/u.test(match[2] ?? ''));
|
|
683
|
+
for (const inputStruct of inputStructs) {
|
|
684
|
+
const name = inputStruct[1];
|
|
685
|
+
const body = inputStruct[2];
|
|
686
|
+
if (name === undefined || body === undefined) continue;
|
|
687
|
+
const firstParameter = new RegExp(
|
|
688
|
+
`@vertex\\s*fn\\s+\\w+\\s*\\(\\s*\\w+\\s*:\\s*${name}\\b`,
|
|
689
|
+
'u',
|
|
690
|
+
);
|
|
691
|
+
if (!firstParameter.test(withoutComments)) continue;
|
|
692
|
+
let uvSetCount = 0;
|
|
693
|
+
for (const location of body.matchAll(/@location\s*\(\s*(\d+)\s*\)\s+([A-Za-z_]\w*)/gu)) {
|
|
694
|
+
const field = location[2];
|
|
695
|
+
if (field === 'uv') {
|
|
696
|
+
uvSetCount = Math.max(uvSetCount, 1);
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
const uvIndex = field?.match(/^uv(\d+)_?$/u)?.[1];
|
|
700
|
+
if (uvIndex !== undefined) uvSetCount = Math.max(uvSetCount, Number(uvIndex) + 1);
|
|
701
|
+
}
|
|
702
|
+
return uvSetCount > 0 ? uvSetCount : undefined;
|
|
703
|
+
}
|
|
704
|
+
return undefined;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Select a published material artifact that matches a downlevel backend.
|
|
709
|
+
*
|
|
710
|
+
* Cooked Surface publications carry immutable backend variants in their
|
|
711
|
+
* metadata. WebGL2 must select the uniform-fallback specialization before
|
|
712
|
+
* pipeline construction so its shader, bind-group layout, and cache key stay
|
|
713
|
+
* capability-consistent.
|
|
714
|
+
*/
|
|
715
|
+
export function resolveMaterialShaderBackendArtifactKey(
|
|
716
|
+
materialShaderId: string,
|
|
717
|
+
backendKind: 'webgpu' | 'wgpu-native' | 'wgpu-webgl2' | 'null',
|
|
718
|
+
artifact:
|
|
719
|
+
| {
|
|
720
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
721
|
+
}
|
|
722
|
+
| undefined,
|
|
723
|
+
): string {
|
|
724
|
+
if (backendKind !== 'wgpu-webgl2') return materialShaderId;
|
|
725
|
+
const variants = artifact?.metadata?.variants;
|
|
726
|
+
if (!Array.isArray(variants)) return materialShaderId;
|
|
727
|
+
const fallback = variants.find((variant): variant is { specializationKey: string } => {
|
|
728
|
+
if (typeof variant !== 'object' || variant === null) return false;
|
|
729
|
+
const candidate = variant as Record<string, unknown>;
|
|
730
|
+
return (
|
|
731
|
+
candidate.backend === 'webgl2' &&
|
|
732
|
+
candidate.capability === 'uniform-fallback' &&
|
|
733
|
+
typeof candidate.specializationKey === 'string' &&
|
|
734
|
+
candidate.specializationKey.length > 0
|
|
735
|
+
);
|
|
736
|
+
});
|
|
737
|
+
return fallback?.specializationKey ?? materialShaderId;
|
|
738
|
+
}
|
|
739
|
+
|
|
662
740
|
export function allowsUnlitPreparedFallback(
|
|
663
741
|
depthFormatOverride: GPUTextureFormat | null | undefined,
|
|
664
742
|
vertexLayout: string | undefined,
|
|
@@ -632,7 +632,7 @@ export function createRendererRecovery(deps: RendererRecoveryDependencies): Rend
|
|
|
632
632
|
releaseCandidate();
|
|
633
633
|
return err(new RecoverError('recover-device-unavailable'));
|
|
634
634
|
}
|
|
635
|
-
const graphPreparation = renderSystem.prepareRecoveryGraphCandidate({
|
|
635
|
+
const graphPreparation = await renderSystem.prepareRecoveryGraphCandidate({
|
|
636
636
|
internals: candidateInternals,
|
|
637
637
|
pipelineState: candidatePipelineState,
|
|
638
638
|
});
|
|
@@ -54,7 +54,6 @@ import {
|
|
|
54
54
|
} from '../ibl/skylight-bind-group';
|
|
55
55
|
import type { BloomInspection } from '../inspection-types';
|
|
56
56
|
import type { DeviceScope, RhiErrorListenerRegistry } from '../lifecycle';
|
|
57
|
-
import { assertStorageBufferCap } from '../light-buffer-layout';
|
|
58
57
|
import {
|
|
59
58
|
buildPbrPipelineLayouts,
|
|
60
59
|
buildPbrSkinLayouts,
|
|
@@ -218,22 +217,10 @@ export async function buildReadyWebGPU(
|
|
|
218
217
|
setVolumetricFogShaderSources: (sources: VolumetricFogShaderSources) => void,
|
|
219
218
|
): Promise<PipelineState> {
|
|
220
219
|
const extendedLightingShaderAvailable = deriveExtendedLightingCapability(rhiDevice).admitted;
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
// (buildPbrPipelineLayouts, which switches between read-only-storage
|
|
225
|
-
// and uniform BGL entries) walk the same axis. Step 0 already ran in
|
|
226
|
-
// `prepareMaterialShaders`; this is an idempotent re-read driven by
|
|
227
|
-
// `device.limits.maxStorageBuffersPerShaderStage`, which is 0 on the
|
|
228
|
-
// wgpu-wasm WebGL2 backend (downlevel_webgl2_defaults).
|
|
229
|
-
const capValue = (rhiDevice.limits as Readonly<Record<string, number>>)
|
|
230
|
-
.maxStorageBuffersPerShaderStage;
|
|
220
|
+
// Shader layouts and record-stage buffer usage share the RHI capability.
|
|
221
|
+
// Backend names and numeric limits must not independently re-enable storage.
|
|
222
|
+
const storageBufferCapable = rhiDevice.caps.storageBuffer;
|
|
231
223
|
const webgl2Downlevel = rhiDevice.caps.backendKind === 'wgpu-webgl2';
|
|
232
|
-
let storageBufferCapable = !webgl2Downlevel;
|
|
233
|
-
if (typeof capValue === 'number') {
|
|
234
|
-
const capCheck = assertStorageBufferCap(capValue);
|
|
235
|
-
storageBufferCapable = capCheck.ok ? !webgl2Downlevel && capCheck.value : !webgl2Downlevel;
|
|
236
|
-
}
|
|
237
224
|
const directionalPcssAvailable =
|
|
238
225
|
rhiDevice.caps.backendKind === 'webgpu' || rhiDevice.caps.backendKind === 'wgpu-native';
|
|
239
226
|
const projectorAvailable =
|