@forgeax/engine-render 0.1.25 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/assembly/factory.d.ts +1 -1
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/material/assembly.d.ts +4 -17
- package/dist/assembly/material/assembly.d.ts.map +1 -1
- package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -1
- package/dist/assembly/material/surface-projection.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer.d.ts +0 -1
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/authoring.mjs +1 -1
- package/dist/{chunk-JSEDTZYS.mjs → chunk-GF523LHF.mjs} +6 -6
- package/dist/chunk-GF523LHF.mjs.map +1 -0
- package/dist/{chunk-TCLDX7SZ.mjs → chunk-TMIASV2N.mjs} +403 -457
- package/dist/chunk-TMIASV2N.mjs.map +1 -0
- package/dist/{chunk-N3RT2EUR.mjs → chunk-YUQXSEHG.mjs} +87 -53
- package/dist/chunk-YUQXSEHG.mjs.map +1 -0
- package/dist/construct-renderer.mjs +153 -13
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/extract/contracts.d.ts +6 -6
- package/dist/extract/contracts.d.ts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/internal.mjs +1 -1
- package/dist/pipeline-spec-types.d.ts +2 -0
- package/dist/pipeline-spec-types.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts.map +1 -1
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +2 -2
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +20 -2
- package/dist/record/main-pass-material.d.ts.map +1 -1
- package/dist/record/main-pass-sprite-draws.d.ts +3 -3
- package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/material-uniforms.d.ts +4 -0
- package/dist/record/material-uniforms.d.ts.map +1 -0
- package/dist/record/render-context.d.ts +2 -2
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts +2 -3
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts +2 -0
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +21 -10
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts.map +1 -1
- package/dist/temporal/index.mjs +2 -2
- package/package.json +20 -20
- package/src/__tests__/fixtures/ai-weapon-spirit/README.md +20 -0
- package/src/__tests__/fixtures/ai-weapon-spirit/low-poly-toon.wgsl +178 -0
- package/src/__tests__/fixtures/ai-weapon-spirit/material.json +97 -0
- package/src/__tests__/fixtures/ai-weapon-spirit/painterly-surface.wgsl +55 -0
- package/src/__tests__/gpu-driven-view-gpu-evidence.ts +3 -3
- package/src/__tests__/gpu-driven-view.browser.test.ts +11 -3
- package/src/__tests__/material-contract-inventory.unit.test.ts +43 -0
- package/src/__tests__/{material-static-dynamic-route.unit.test.ts → material-layer-projection.unit.test.ts} +30 -47
- package/src/__tests__/material-program-dispatch.unit.test.ts +48 -0
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +2 -2
- package/src/__tests__/skinned-shadow-caster.test.ts +14 -14
- package/src/__tests__/surface-variant-provenance.integration.test.ts +1 -1
- package/src/assembly/factory.ts +0 -3
- package/src/assembly/material/assembly.ts +8 -40
- package/src/assembly/material/standard-layer-projection.ts +3 -1
- package/src/assembly/material/surface-projection.ts +2 -1
- package/src/assembly/webgpu-renderer.ts +17 -9
- package/src/extract/contracts.ts +6 -6
- package/src/materials.ts +3 -3
- package/src/pipeline-spec-types.ts +2 -0
- package/src/pipeline-spec.ts +7 -3
- package/src/record/frame.ts +7 -1
- package/src/record/main-pass-geometry.ts +340 -279
- package/src/record/main-pass-material.ts +87 -2
- package/src/record/main-pass-sprite-draws.ts +65 -168
- package/src/record/main-pass.ts +24 -246
- package/src/record/material-uniforms.ts +186 -0
- package/src/record/render-context.ts +4 -0
- package/src/record/shadow-pass.ts +86 -28
- package/src/recovery/render-system-candidate.ts +5 -1
- package/src/render-system-extract-tail.ts +51 -11
- package/src/render-system-extract.ts +73 -63
- package/src/render-system.ts +19 -0
- package/dist/assembly/material/pipeline-projection.d.ts +0 -30
- package/dist/assembly/material/pipeline-projection.d.ts.map +0 -1
- package/dist/chunk-JSEDTZYS.mjs.map +0 -1
- package/dist/chunk-N3RT2EUR.mjs.map +0 -1
- package/dist/chunk-TCLDX7SZ.mjs.map +0 -1
- package/src/assembly/material/pipeline-projection.ts +0 -63
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#define_import_path ai_weapon_spirit::painterly_surface
|
|
2
|
+
|
|
3
|
+
fn pigmentHash(point : vec2<f32>) -> f32 {
|
|
4
|
+
var p = fract(vec3<f32>(point.x, point.y, point.x) * .1031);
|
|
5
|
+
p += vec3<f32>(dot(p, p.yzx + vec3<f32>(33.33)));
|
|
6
|
+
return fract((p.x + p.y) * p.z);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
fn pigmentNoise(point : vec2<f32>) -> f32 {
|
|
10
|
+
let cell = floor(point);
|
|
11
|
+
let f = fract(point);
|
|
12
|
+
let u = f * f * (vec2<f32>(3.0) - 2.0 * f);
|
|
13
|
+
return mix(mix(pigmentHash(cell), pigmentHash(cell + vec2<f32>(1.0, 0.0)), u.x),
|
|
14
|
+
mix(pigmentHash(cell + vec2<f32>(0.0, 1.0)), pigmentHash(cell + vec2<f32>(1.0, 1.0)), u.x), u.y);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn pigmentPlane(position : vec3<f32>, normal : vec3<f32>) -> vec2<f32> {
|
|
18
|
+
let axis = abs(normal);
|
|
19
|
+
if (axis.y >= max(axis.x, axis.z)) { return position.xz; }
|
|
20
|
+
if (axis.x > axis.z) { return position.zy; }
|
|
21
|
+
return position.xy;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Soft pigment pools, with no repeated stroke or bristle shapes. Rotate and
|
|
25
|
+
// gently warp the field so its broad transitions do not reveal the noise grid.
|
|
26
|
+
// Units are metres: moving actors use local coordinates, fixed terrain world coordinates.
|
|
27
|
+
fn applyPigment(base : vec3<f32>, position : vec3<f32>, normal : vec3<f32>, scale : f32, strength : f32) -> vec3<f32> {
|
|
28
|
+
let plane = pigmentPlane(position, normal) * scale;
|
|
29
|
+
let point = vec2<f32>(plane.x * .8 + plane.y * .6, plane.y * .8 - plane.x * .6);
|
|
30
|
+
let drift = vec2<f32>(
|
|
31
|
+
pigmentNoise(point * .11 + vec2<f32>(5.2, 11.7)),
|
|
32
|
+
pigmentNoise(point * .11 + vec2<f32>(19.3, -4.6)),
|
|
33
|
+
) - vec2<f32>(.5);
|
|
34
|
+
let field = point * .18 + drift * .65;
|
|
35
|
+
let wash = pigmentNoise(field + vec2<f32>(3.2, 8.1)) - .5;
|
|
36
|
+
let bloom = pigmentNoise(field * .47 + vec2<f32>(-7.4, 2.9)) - .5;
|
|
37
|
+
let pigment = (wash * .72 + bloom * .28) * strength;
|
|
38
|
+
// Pigment changes local reflectance; it never emits light or changes normals.
|
|
39
|
+
return max(vec3<f32>(0.0), base * (1.0 + pigment) + vec3<f32>(.08, .02, -.055) * pigment);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn paintedSpecular(base : vec3<f32>, normal : vec3<f32>, light : vec3<f32>, camera : vec3<f32>,
|
|
43
|
+
roughness : f32, metallic : f32, strength : f32, visibility : f32) -> vec3<f32> {
|
|
44
|
+
let halfVector = normalize(light + camera);
|
|
45
|
+
let smoothness = 1.0 - clamp(roughness, .04, 1.0);
|
|
46
|
+
let lobe = pow(max(dot(normal, halfVector), 0.0), mix(12.0, 112.0, smoothness));
|
|
47
|
+
let reflection = strength * smoothness * smoothness * lobe
|
|
48
|
+
* max(dot(normal, light), 0.0) * visibility;
|
|
49
|
+
return mix(vec3<f32>(.88, .86, .81), base, metallic) * reflection;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn paintedRim(normal : vec3<f32>, camera : vec3<f32>, roughness : f32, strength : f32, visibility : f32) -> f32 {
|
|
53
|
+
let fresnel = pow(1.0 - clamp(dot(normal, camera), 0.0, 1.0), 4.0);
|
|
54
|
+
return fresnel * strength * mix(1.0, .18, roughness) * mix(.35, 1.0, visibility);
|
|
55
|
+
}
|
|
@@ -61,7 +61,9 @@ export interface GpuDrivenViewLifecycleEvidence {
|
|
|
61
61
|
readonly candidateCapacity: number;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export async function runGpuDrivenViewLifecycleEvidence(
|
|
64
|
+
export async function runGpuDrivenViewLifecycleEvidence(
|
|
65
|
+
frames = 300,
|
|
66
|
+
): Promise<GpuDrivenViewLifecycleEvidence> {
|
|
65
67
|
const adapter = (await rhi.requestAdapter()).unwrap();
|
|
66
68
|
const device = (await adapter.requestDevice()).unwrap();
|
|
67
69
|
const rawDevice = _internal_getRawDevice(device);
|
|
@@ -85,8 +87,6 @@ export async function runGpuDrivenViewLifecycleEvidence(): Promise<GpuDrivenView
|
|
|
85
87
|
const retirements: Array<Promise<unknown>> = [];
|
|
86
88
|
let activeCount = 0;
|
|
87
89
|
let previous: CompiledRenderGraph<{ readonly encoder: RhiCommandEncoder }> | undefined;
|
|
88
|
-
const frames = 300;
|
|
89
|
-
|
|
90
90
|
for (let frame = 0; frame < frames; frame += 1) {
|
|
91
91
|
const targetCount = frame < 4 ? 2 ** frame : frame % 2 === 0 ? 1 : 8;
|
|
92
92
|
const operations: Array<
|
|
@@ -5,6 +5,14 @@ import {
|
|
|
5
5
|
} from './gpu-driven-view-gpu-evidence';
|
|
6
6
|
|
|
7
7
|
const browserReady = typeof navigator !== 'undefined' && navigator.gpu !== undefined;
|
|
8
|
+
const lifecycleFrames =
|
|
9
|
+
(
|
|
10
|
+
import.meta as ImportMeta & {
|
|
11
|
+
readonly env?: { readonly FORGEAX_BROWSER_CI_LIGHTWEIGHT?: string };
|
|
12
|
+
}
|
|
13
|
+
).env?.FORGEAX_BROWSER_CI_LIGHTWEIGHT === '1'
|
|
14
|
+
? 60
|
|
15
|
+
: 300;
|
|
8
16
|
|
|
9
17
|
describe.skipIf(!browserReady)('GPU-driven View Browser WebGPU', () => {
|
|
10
18
|
it('produces compact visible IDs and portable indirect args on the real browser device', async () => {
|
|
@@ -20,9 +28,9 @@ describe.skipIf(!browserReady)('GPU-driven View Browser WebGPU', () => {
|
|
|
20
28
|
});
|
|
21
29
|
});
|
|
22
30
|
|
|
23
|
-
it(
|
|
24
|
-
await expect(runGpuDrivenViewLifecycleEvidence()).resolves.toEqual({
|
|
25
|
-
frames:
|
|
31
|
+
it(`survives ${lifecycleFrames} frames of spawn/despawn capacity crossings without stale buffers`, async () => {
|
|
32
|
+
await expect(runGpuDrivenViewLifecycleEvidence(lifecycleFrames)).resolves.toEqual({
|
|
33
|
+
frames: lifecycleFrames,
|
|
26
34
|
bufferRebuilds: 4,
|
|
27
35
|
candidateCapacity: 8,
|
|
28
36
|
});
|
|
@@ -2,10 +2,12 @@ import {
|
|
|
2
2
|
createBuiltinMaterialAsset,
|
|
3
3
|
DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
|
|
4
4
|
DEFAULT_UNLIT_PARAM_SCHEMA,
|
|
5
|
+
STANDARD_PIPELINE_PARAM_SCHEMA,
|
|
5
6
|
} from '@forgeax/engine-shader';
|
|
6
7
|
import { resolveMaterialAsset } from '@forgeax/engine-types';
|
|
7
8
|
import { describe, expect, it } from 'vitest';
|
|
8
9
|
import { Materials } from '../materials.js';
|
|
10
|
+
import { materialParametersToParamSchema } from '../render-system-extract.js';
|
|
9
11
|
|
|
10
12
|
describe('vertex color does not change the material contract', () => {
|
|
11
13
|
it('keeps colored and plain materials on the same schema and resource identity', () => {
|
|
@@ -62,6 +64,47 @@ describe('vertex color does not change the material contract', () => {
|
|
|
62
64
|
expect(material.values).toHaveProperty('ior', 1.5);
|
|
63
65
|
});
|
|
64
66
|
|
|
67
|
+
it('routes canonical Standard roots through the complete runtime UBO schema', () => {
|
|
68
|
+
const material = Materials.standard({ baseColor: [1, 1, 1, 1] });
|
|
69
|
+
const authoredSchema = materialParametersToParamSchema(material.parameters ?? []);
|
|
70
|
+
const canonicalSchema = materialParametersToParamSchema(
|
|
71
|
+
material.parameters ?? [],
|
|
72
|
+
'forgeax::default-standard-pbr',
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(authoredSchema.map((entry) => entry.name)).toEqual(
|
|
76
|
+
material.parameters?.map((parameter) => parameter.name),
|
|
77
|
+
);
|
|
78
|
+
expect(canonicalSchema).toBe(STANDARD_PIPELINE_PARAM_SCHEMA);
|
|
79
|
+
expect(authoredSchema).not.toBe(STANDARD_PIPELINE_PARAM_SCHEMA);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('publishes the complete Standard contract when authoring a custom Surface', () => {
|
|
83
|
+
const custom = Materials.standard({
|
|
84
|
+
surfaceModule: 'project::paint',
|
|
85
|
+
parameters: [{ name: 'paintStrength', type: 'f32', default: 0.75 }],
|
|
86
|
+
values: { paintStrength: 0.5 },
|
|
87
|
+
});
|
|
88
|
+
const names = custom.parameters?.map((parameter) => parameter.name) ?? [];
|
|
89
|
+
expect(names).toEqual(
|
|
90
|
+
expect.arrayContaining([
|
|
91
|
+
'baseColor',
|
|
92
|
+
'metallic',
|
|
93
|
+
'roughness',
|
|
94
|
+
'baseColorTexture',
|
|
95
|
+
'paintStrength',
|
|
96
|
+
]),
|
|
97
|
+
);
|
|
98
|
+
expect(new Set(names).size).toBe(names.length);
|
|
99
|
+
expect(custom.parameters?.find((parameter) => parameter.name === 'paintStrength')).toEqual({
|
|
100
|
+
name: 'paintStrength',
|
|
101
|
+
type: 'f32',
|
|
102
|
+
default: 0.75,
|
|
103
|
+
});
|
|
104
|
+
expect(custom.values).toEqual({ paintStrength: 0.5 });
|
|
105
|
+
expect(names).not.toContain('clearcoat');
|
|
106
|
+
});
|
|
107
|
+
|
|
65
108
|
it('routes active transmission to Forward and ShadowCaster only', () => {
|
|
66
109
|
const material = Materials.standard({ baseColor: [1, 1, 1, 1], transmission: 0.5 });
|
|
67
110
|
expect(material.passes?.map((pass) => pass.name)).toEqual(['forward', 'shadow-caster']);
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import type { CookedMaterialRecord } from '@forgeax/engine-pack';
|
|
2
2
|
import { deriveStandardLayerPlan } from '@forgeax/engine-types';
|
|
3
|
-
import { describe, expect, it
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
4
|
import { assembleMaterialProjection } from '../assembly/material/assembly.js';
|
|
5
|
-
import {
|
|
6
|
-
routeMaterialPipeline,
|
|
7
|
-
updateMaterialRuntimeValues,
|
|
8
|
-
} from '../assembly/material/pipeline-projection.js';
|
|
9
5
|
|
|
10
6
|
const record: CookedMaterialRecord = {
|
|
11
|
-
schemaVersion: 'material-cook/
|
|
7
|
+
schemaVersion: 'material-cook/4',
|
|
12
8
|
guid: 'material-route',
|
|
13
9
|
specializationKey: 'route-key',
|
|
14
10
|
authored: {
|
|
@@ -24,14 +20,34 @@ const record: CookedMaterialRecord = {
|
|
|
24
20
|
values: { baseColor: [1, 1, 1, 1], useNormalMap: false },
|
|
25
21
|
},
|
|
26
22
|
refs: { parent: [], textures: [], samplers: [], modules: ['project::standard'] },
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
programs: [
|
|
24
|
+
{
|
|
25
|
+
specializationKey: 'route-program',
|
|
26
|
+
artifact: {
|
|
27
|
+
mediaType: 'text/wgsl',
|
|
28
|
+
path: 'material-route.wgsl',
|
|
29
|
+
digest: 'sha256:route',
|
|
30
|
+
bytes: new Uint8Array([4]),
|
|
31
|
+
},
|
|
32
|
+
selections: [
|
|
33
|
+
{
|
|
34
|
+
pass: 'forward',
|
|
35
|
+
context: {
|
|
36
|
+
backend: 'webgpu',
|
|
37
|
+
capability: 'storage-buffer',
|
|
38
|
+
pipeline: 'forward',
|
|
39
|
+
geometry: 'mesh',
|
|
40
|
+
pass: 'forward',
|
|
41
|
+
profile: 'forgeax-material-wgsl-v1',
|
|
42
|
+
toolchain: 'naga-oil',
|
|
43
|
+
instrumentation: 'none',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
33
49
|
receipt: {
|
|
34
|
-
schemaVersion: 'material-cook/
|
|
50
|
+
schemaVersion: 'material-cook/4',
|
|
35
51
|
sourceClosure: ['project::standard'],
|
|
36
52
|
profile: 'forgeax-material-wgsl-v1',
|
|
37
53
|
compilerVersion: 'compiler/1',
|
|
@@ -59,7 +75,7 @@ const record: CookedMaterialRecord = {
|
|
|
59
75
|
},
|
|
60
76
|
};
|
|
61
77
|
|
|
62
|
-
describe('cooked material
|
|
78
|
+
describe('cooked material layer projection', () => {
|
|
63
79
|
it('rejects a stale Standard layer plan before render projection', () => {
|
|
64
80
|
const standardPass = { name: 'forward', program: { module: 'forgeax::default-standard-pbr' } };
|
|
65
81
|
const standardResolved = { ...record.resolved, passes: [standardPass] };
|
|
@@ -87,37 +103,4 @@ describe('cooked material static and dynamic routing', () => {
|
|
|
87
103
|
}),
|
|
88
104
|
).toThrow('Standard material layer-plan identity mismatch');
|
|
89
105
|
});
|
|
90
|
-
|
|
91
|
-
it('updates runtime values without requesting a compile or changing the artifact key', () => {
|
|
92
|
-
const projection = assembleMaterialProjection(record);
|
|
93
|
-
const updated = updateMaterialRuntimeValues(projection, {
|
|
94
|
-
baseColor: [0.25, 0.5, 0.75, 1],
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
expect(updated.runtimeValues.baseColor).toEqual([0.25, 0.5, 0.75, 1]);
|
|
98
|
-
expect(updated.specializationKey).toBe(projection.specializationKey);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('selects a cooked static specialization and returns a structured miss', () => {
|
|
102
|
-
const projection = assembleMaterialProjection(record);
|
|
103
|
-
const lookup = vi.fn((key: string) =>
|
|
104
|
-
key === 'route-key' ? { key, bytes: new Uint8Array([4]), digest: 'sha256:route' } : undefined,
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
const ready = routeMaterialPipeline({ projection, lookupArtifact: lookup });
|
|
108
|
-
expect(ready).toMatchObject({ kind: 'ready', specializationKey: 'route-key' });
|
|
109
|
-
expect(lookup).toHaveBeenCalledWith('route-key');
|
|
110
|
-
|
|
111
|
-
const missing = routeMaterialPipeline({
|
|
112
|
-
projection: { ...projection, specializationKey: 'uncooked-static-selection' },
|
|
113
|
-
lookupArtifact: lookup,
|
|
114
|
-
});
|
|
115
|
-
expect(missing).toMatchObject({
|
|
116
|
-
kind: 'error',
|
|
117
|
-
error: {
|
|
118
|
-
code: 'material-specialization-not-cooked',
|
|
119
|
-
detail: { specializationKey: 'uncooked-static-selection' },
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
106
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { EntityHandle } from '@forgeax/engine-ecs';
|
|
2
|
+
import type { MaterialPass } from '@forgeax/engine-types';
|
|
3
|
+
import { expect, it } from 'vitest';
|
|
4
|
+
import { appendMaterialDispatchEntries, type DispatchEntry } from '../render-system-extract.js';
|
|
5
|
+
|
|
6
|
+
it('dispatches each Pass with its published program, including an Engine-authored shadow', () => {
|
|
7
|
+
const passes: MaterialPass[] = [
|
|
8
|
+
{
|
|
9
|
+
name: 'Forward',
|
|
10
|
+
program: { module: 'game::toon', vertexEntry: 'vs_main', fragmentEntry: 'fs_toon' },
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'ShadowCaster',
|
|
14
|
+
program: { module: 'forgeax::default-shadow-caster', vertexEntry: 'vs_shadow' },
|
|
15
|
+
renderState: { tags: { LightMode: 'ShadowCaster' } },
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
const dispatch: DispatchEntry[] = [];
|
|
19
|
+
appendMaterialDispatchEntries(dispatch, passes, 1 as EntityHandle, 7, 0, 0, { amount: 0 }, 0, {
|
|
20
|
+
Forward: 'cooked-toon',
|
|
21
|
+
ShadowCaster: 'cooked-shadow',
|
|
22
|
+
});
|
|
23
|
+
expect(
|
|
24
|
+
dispatch.map((entry) => [entry.materialShaderId, entry.vertexEntry, entry.fragmentEntry]),
|
|
25
|
+
).toEqual([
|
|
26
|
+
['cooked-toon', 'vs_main', 'fs_toon'],
|
|
27
|
+
['cooked-shadow', 'vs_shadow', undefined],
|
|
28
|
+
]);
|
|
29
|
+
expect(
|
|
30
|
+
dispatch.every((entry) => entry.materialHandle === 7 && entry.paramSnapshot?.amount === 0),
|
|
31
|
+
).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('refuses an incomplete published Pass map instead of using an authored shader', () => {
|
|
35
|
+
expect(() =>
|
|
36
|
+
appendMaterialDispatchEntries(
|
|
37
|
+
[],
|
|
38
|
+
[{ name: 'ShadowCaster', program: { module: 'forgeax::default-shadow-caster' } }],
|
|
39
|
+
1 as EntityHandle,
|
|
40
|
+
7,
|
|
41
|
+
0,
|
|
42
|
+
0,
|
|
43
|
+
{},
|
|
44
|
+
0,
|
|
45
|
+
{ Forward: 'cooked-toon' },
|
|
46
|
+
),
|
|
47
|
+
).toThrow('Missing published material program');
|
|
48
|
+
});
|
|
@@ -628,10 +628,10 @@ describe('material shader binding contract', () => {
|
|
|
628
628
|
'utf8',
|
|
629
629
|
);
|
|
630
630
|
const projectionSource = readFileSync(
|
|
631
|
-
resolve(import.meta.dirname, '../assembly/material/
|
|
631
|
+
resolve(import.meta.dirname, '../assembly/material/assembly.ts'),
|
|
632
632
|
'utf8',
|
|
633
633
|
);
|
|
634
|
-
expect(projectionSource).toContain('
|
|
634
|
+
expect(projectionSource).toContain('projectMaterialRecord');
|
|
635
635
|
expect(recordSource).not.toMatch(/internals\.assets\.get<MaterialAsset>/);
|
|
636
636
|
expect(recordSource).not.toMatch(/firstMaterial as \{/);
|
|
637
637
|
expect(recordSource).not.toContain('baseColorHandle');
|
|
@@ -130,31 +130,31 @@ describe('skinned shadow caster', () => {
|
|
|
130
130
|
expect(recordSource).toContain('binding: 12');
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
it('keeps
|
|
134
|
-
const paramSnapshot = { alphaClipThreshold: 0.4 };
|
|
133
|
+
it('keeps each submesh material associated with its selected shadow program and entries', () => {
|
|
135
134
|
const context = {
|
|
136
135
|
dispatch: [
|
|
137
136
|
{
|
|
138
137
|
renderableIndex: 4,
|
|
139
138
|
materialHandle: 42,
|
|
140
139
|
materialShaderId: 'forgeax::default-standard-pbr',
|
|
141
|
-
|
|
140
|
+
tags: { LightMode: 'ShadowCaster' },
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
renderableIndex: 4,
|
|
144
|
+
materialHandle: 43,
|
|
145
|
+
materialShaderId: 'custom::cutout',
|
|
146
|
+
vertexEntry: 'vs_displaced',
|
|
147
|
+
fragmentEntry: 'fs_cutout',
|
|
142
148
|
tags: { LightMode: 'ShadowCaster' },
|
|
143
149
|
},
|
|
144
150
|
],
|
|
145
|
-
validatedOrdered: [{ renderableIndex: 4 }],
|
|
146
|
-
materialSlotIndices: [[3, 7]],
|
|
147
|
-
materialSlots: Array.from({ length: 8 }, (_, slot) => ({
|
|
148
|
-
materialHandle: slot === 7 ? 42 : 11,
|
|
149
|
-
})),
|
|
150
151
|
} as never;
|
|
151
|
-
|
|
152
152
|
const bindings = shadowShaderMap(context);
|
|
153
|
-
expect(bindings.get(4)?.get(42)).
|
|
154
|
-
|
|
155
|
-
materialShaderId:
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
expect(bindings.get(4)?.get(42)?.materialShaderId).toBe(SHADOW_CASTER_SHADER_ID);
|
|
154
|
+
expect(bindings.get(4)?.get(43)).toMatchObject({
|
|
155
|
+
materialShaderId: 'custom::cutout',
|
|
156
|
+
vertexEntry: 'vs_displaced',
|
|
157
|
+
fragmentEntry: 'fs_cutout',
|
|
158
158
|
});
|
|
159
159
|
});
|
|
160
160
|
});
|
|
@@ -23,7 +23,7 @@ describe('full-custom Surface provenance', () => {
|
|
|
23
23
|
expect(passes.map((pass) => pass.name)).toEqual(['forward', 'shadow-caster']);
|
|
24
24
|
expect(passes.map((pass) => pass.program.module)).toEqual([
|
|
25
25
|
'forgeax_material::standard',
|
|
26
|
-
'
|
|
26
|
+
'forgeax::default-shadow-caster',
|
|
27
27
|
]);
|
|
28
28
|
for (const pass of passes) {
|
|
29
29
|
expect(pass.program.moduleSlots?.surface).toBe(options.surfaceModule);
|
package/src/assembly/factory.ts
CHANGED
|
@@ -64,13 +64,11 @@ export {
|
|
|
64
64
|
exposeRenderer,
|
|
65
65
|
isSharedMaterialUserRegionCompatible,
|
|
66
66
|
normalizeMaterialShaderVariantSet,
|
|
67
|
-
projectMaterialPipeline,
|
|
68
67
|
resolveMaterialShaderBackendArtifactKey,
|
|
69
68
|
resolveMaterialShaderBindingContract,
|
|
70
69
|
resolveMaterialShaderUvSetCount,
|
|
71
70
|
resolveMaterialShaderVariantSet,
|
|
72
71
|
resolveMaterialShaderVertexInputContract,
|
|
73
|
-
routeMaterialPipeline,
|
|
74
72
|
selectHdrpPbrPrewarmVariants,
|
|
75
73
|
selectNoColorPbrVariant,
|
|
76
74
|
selectPipelineLayoutForVariant,
|
|
@@ -78,7 +76,6 @@ export {
|
|
|
78
76
|
selectSkinPrewarmVariants,
|
|
79
77
|
selectStandardPbrTransmissionPrewarmVariants,
|
|
80
78
|
shouldDeferMissingPreparedMaterialShader,
|
|
81
|
-
updateMaterialRuntimeValues,
|
|
82
79
|
} from './webgpu-renderer';
|
|
83
80
|
|
|
84
81
|
const GPU_ENVIRONMENT_ERROR_CODES = new Set([
|
|
@@ -1,46 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type MaterialRenderProjection as AssetMaterialRenderProjection,
|
|
3
|
+
projectMaterialRecord,
|
|
4
|
+
} from '@forgeax/engine-assets-runtime';
|
|
1
5
|
import {
|
|
2
6
|
type CookedMaterialRecord,
|
|
3
7
|
materialLayerPlanIdentity,
|
|
4
8
|
} from '@forgeax/engine-pack/material-cook';
|
|
5
|
-
import type {
|
|
9
|
+
import type { StandardLayerPlan } from '@forgeax/engine-types';
|
|
6
10
|
import { projectStandardLayerPlan } from './standard-layer-projection.js';
|
|
7
11
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
readonly module: string;
|
|
11
|
-
readonly vertexEntry?: string;
|
|
12
|
-
readonly fragmentEntry?: string;
|
|
13
|
-
readonly moduleSlots?: Readonly<Record<string, string>>;
|
|
14
|
-
readonly renderState?: Readonly<Record<string, unknown>>;
|
|
15
|
-
readonly artifactHash: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface MaterialRenderProjection {
|
|
19
|
-
readonly materialGuid: string;
|
|
20
|
-
readonly specializationKey: string;
|
|
21
|
-
readonly artifactHash: string;
|
|
22
|
-
readonly passes: readonly MaterialRenderPassProjection[];
|
|
23
|
-
readonly runtimeValues: Readonly<Record<string, MaterialValue | null>>;
|
|
24
|
-
readonly staticSelection: readonly string[];
|
|
12
|
+
export type { MaterialRenderPassProjection } from '@forgeax/engine-assets-runtime';
|
|
13
|
+
export interface MaterialRenderProjection extends AssetMaterialRenderProjection {
|
|
25
14
|
readonly layerPlan: StandardLayerPlan;
|
|
26
15
|
}
|
|
27
16
|
|
|
28
|
-
function projectPass(
|
|
29
|
-
record: CookedMaterialRecord,
|
|
30
|
-
pass: CookedMaterialRecord['resolved']['passes'][number],
|
|
31
|
-
): MaterialRenderPassProjection {
|
|
32
|
-
const program = pass.program;
|
|
33
|
-
return {
|
|
34
|
-
name: pass.name,
|
|
35
|
-
module: program.module,
|
|
36
|
-
...(program.vertexEntry === undefined ? {} : { vertexEntry: program.vertexEntry }),
|
|
37
|
-
...(program.fragmentEntry === undefined ? {} : { fragmentEntry: program.fragmentEntry }),
|
|
38
|
-
...(program.moduleSlots === undefined ? {} : { moduleSlots: program.moduleSlots }),
|
|
39
|
-
...(pass.renderState === undefined ? {} : { renderState: pass.renderState }),
|
|
40
|
-
artifactHash: record.artifact.digest,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
17
|
export function assembleMaterialProjection(record: CookedMaterialRecord): MaterialRenderProjection {
|
|
45
18
|
const layerPlan = projectStandardLayerPlan(record.resolved.parameters, record.resolved.passes);
|
|
46
19
|
const expectedLayerPlanIdentity = materialLayerPlanIdentity(record);
|
|
@@ -53,12 +26,7 @@ export function assembleMaterialProjection(record: CookedMaterialRecord): Materi
|
|
|
53
26
|
);
|
|
54
27
|
}
|
|
55
28
|
return {
|
|
56
|
-
|
|
57
|
-
specializationKey: record.specializationKey ?? record.receipt.identity.programIdentity,
|
|
58
|
-
artifactHash: record.artifact.digest,
|
|
59
|
-
passes: record.resolved.passes.map((pass) => projectPass(record, pass)),
|
|
60
|
-
runtimeValues: record.resolved.values,
|
|
61
|
-
staticSelection: [],
|
|
29
|
+
...projectMaterialRecord(record),
|
|
62
30
|
layerPlan,
|
|
63
31
|
};
|
|
64
32
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isStandardRootModule } from '@forgeax/engine-pack';
|
|
1
2
|
import type { MaterialParameter, MaterialPass, StandardLayerPlan } from '@forgeax/engine-types';
|
|
2
3
|
import { deriveStandardLayerPlan } from '@forgeax/engine-types';
|
|
3
4
|
|
|
@@ -12,5 +13,6 @@ export function projectStandardLayerPlan(
|
|
|
12
13
|
parameters: readonly MaterialParameter[],
|
|
13
14
|
passes: readonly MaterialPass[],
|
|
14
15
|
): StandardLayerPlan {
|
|
15
|
-
|
|
16
|
+
const standard = passes.some((pass) => isStandardRootModule(pass.program.module));
|
|
17
|
+
return deriveStandardLayerPlan(standard ? parameters : [], standard ? passes : undefined);
|
|
16
18
|
}
|
|
@@ -41,7 +41,8 @@ function pass(
|
|
|
41
41
|
return {
|
|
42
42
|
name,
|
|
43
43
|
program: {
|
|
44
|
-
module:
|
|
44
|
+
module:
|
|
45
|
+
name === 'shadow-caster' ? 'forgeax::default-shadow-caster' : STANDARD_MATERIAL_MODULE,
|
|
45
46
|
fragmentEntry,
|
|
46
47
|
moduleSlots: { surface: options.surfaceModule },
|
|
47
48
|
},
|
|
@@ -229,11 +229,6 @@ import {
|
|
|
229
229
|
|
|
230
230
|
export type { BundlerOptions } from './bundler-contract';
|
|
231
231
|
export { assembleMaterialProjection } from './material/assembly';
|
|
232
|
-
export {
|
|
233
|
-
projectMaterialPipeline,
|
|
234
|
-
routeMaterialPipeline,
|
|
235
|
-
updateMaterialRuntimeValues,
|
|
236
|
-
} from './material/pipeline-projection';
|
|
237
232
|
export type {
|
|
238
233
|
LayoutKind,
|
|
239
234
|
MaterialShaderBindingContract,
|
|
@@ -2002,6 +1997,8 @@ async function makeWebGPURenderer(
|
|
|
2002
1997
|
shaderModuleMode?: RenderFeatureShaderModuleMode,
|
|
2003
1998
|
layoutKindOverride?: LayoutKind,
|
|
2004
1999
|
additionalColorFormats?: readonly GPUTextureFormat[],
|
|
2000
|
+
vertexEntry?: string,
|
|
2001
|
+
authoredFragmentEntry?: string,
|
|
2005
2002
|
): RenderPipeline | null => {
|
|
2006
2003
|
const group2Contract = resolvePipelineGroup2Contract(entry.source);
|
|
2007
2004
|
const ctx = buildPipelineContext(
|
|
@@ -2024,10 +2021,11 @@ async function makeWebGPURenderer(
|
|
|
2024
2021
|
if (currentState === null) return null;
|
|
2025
2022
|
const ldrColorFormat = colorFormatOverride ?? currentState.colorAttachmentFormat;
|
|
2026
2023
|
const fragmentEntry =
|
|
2027
|
-
|
|
2024
|
+
authoredFragmentEntry ??
|
|
2025
|
+
(isHdr &&
|
|
2028
2026
|
(materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit')
|
|
2029
2027
|
? 'fs_main_hdr'
|
|
2030
|
-
: undefined;
|
|
2028
|
+
: undefined);
|
|
2031
2029
|
const built = buildPipelineForMaterialShader(
|
|
2032
2030
|
cacheKey,
|
|
2033
2031
|
// The catalog entry is projected into the material-shader entry shape.
|
|
@@ -2064,7 +2062,7 @@ async function makeWebGPURenderer(
|
|
|
2064
2062
|
: {}),
|
|
2065
2063
|
}
|
|
2066
2064
|
: undefined,
|
|
2067
|
-
|
|
2065
|
+
vertexEntry,
|
|
2068
2066
|
fragmentEntry,
|
|
2069
2067
|
undefined, // defines — none
|
|
2070
2068
|
passKind,
|
|
@@ -2131,6 +2129,8 @@ async function makeWebGPURenderer(
|
|
|
2131
2129
|
shaderModuleMode: RenderFeatureShaderModuleMode = 'validated',
|
|
2132
2130
|
layoutKindOverride?: LayoutKind,
|
|
2133
2131
|
additionalColorFormats?: readonly GPUTextureFormat[],
|
|
2132
|
+
vertexEntry?: string,
|
|
2133
|
+
fragmentEntry?: string,
|
|
2134
2134
|
): RenderPipeline | null => {
|
|
2135
2135
|
// WebGL2 is a downlevel backend even when the record stage asks for the
|
|
2136
2136
|
// native-capability variant string. Resolve the variant axes at this
|
|
@@ -2237,7 +2237,13 @@ async function makeWebGPURenderer(
|
|
|
2237
2237
|
const colorFormat: GPUTextureFormat = isHdr ? 'rgba16float' : ldrColorFormat;
|
|
2238
2238
|
const isDepthOnlyPass = passKindPolicyTable[passKind]?.shape === 'depth-only';
|
|
2239
2239
|
const spec: PipelineSpec = {
|
|
2240
|
-
shader: {
|
|
2240
|
+
shader: {
|
|
2241
|
+
id: pipelineShaderId,
|
|
2242
|
+
passKind,
|
|
2243
|
+
variantSet: effectiveVariantSet,
|
|
2244
|
+
...(vertexEntry === undefined ? {} : { vertexEntry }),
|
|
2245
|
+
...(fragmentEntry === undefined ? {} : { fragmentEntry }),
|
|
2246
|
+
},
|
|
2241
2247
|
attachments: {
|
|
2242
2248
|
colorFormats:
|
|
2243
2249
|
additionalColorFormats === undefined
|
|
@@ -2476,6 +2482,8 @@ async function makeWebGPURenderer(
|
|
|
2476
2482
|
shaderModuleMode,
|
|
2477
2483
|
layoutKindOverride,
|
|
2478
2484
|
additionalColorFormats,
|
|
2485
|
+
vertexEntry,
|
|
2486
|
+
fragmentEntry,
|
|
2479
2487
|
);
|
|
2480
2488
|
};
|
|
2481
2489
|
const getMaterialShaderPipelineEntry = (
|
package/src/extract/contracts.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MaterialCookProgramContext } from '@forgeax/engine-pack/material-cook';
|
|
1
2
|
// Shared render-extract snapshot contracts.
|
|
2
3
|
//
|
|
3
4
|
// The extract orchestrator owns traversal and policy; this leaf owns the
|
|
@@ -658,6 +659,7 @@ export interface MaterialSnapshot {
|
|
|
658
659
|
* discriminator (M4-T06).
|
|
659
660
|
*/
|
|
660
661
|
readonly materialShaderId?: string | undefined;
|
|
662
|
+
readonly materialProgramKeys?: Readonly<Record<string, string>> | undefined;
|
|
661
663
|
/**
|
|
662
664
|
* Stable source handle for the material asset. Record uses this only for
|
|
663
665
|
* same-frame assembly reuse; GPU bind-group validity still comes from the
|
|
@@ -763,12 +765,9 @@ export interface MaterialSnapshot {
|
|
|
763
765
|
* from the legacy `shadingModel` discriminant (feat-20260625 M2 D-3,
|
|
764
766
|
* finalised in w15; `shadingModel` field removed in tweak-20260701 M1).
|
|
765
767
|
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
* pipeline cache key"). Multi-pass materials whose mix of opaque +
|
|
770
|
-
* transparent passes need finer routing should split into separate
|
|
771
|
-
* MaterialAsset entries (the normal forgeax pattern).
|
|
768
|
+
* This describes the primary-pass snapshot for consumers that need a
|
|
769
|
+
* material summary. Actual draw routing and the LDR split read the selected
|
|
770
|
+
* DispatchEntry.renderState, so mixed opaque/transparent Passes remain valid.
|
|
772
771
|
*
|
|
773
772
|
* Type is `boolean | undefined` (derived): `undefined` means "no
|
|
774
773
|
* passes / unknown"; consumers must read `=== true` / `!== true` to
|
|
@@ -961,6 +960,7 @@ export interface ExtractFramesOwner {
|
|
|
961
960
|
}
|
|
962
961
|
|
|
963
962
|
export interface PreparedExtractContext {
|
|
963
|
+
readonly materialContext?: MaterialCookProgramContext | undefined;
|
|
964
964
|
readonly assets: AssetRegistry | null | undefined;
|
|
965
965
|
readonly pipelineState: ExtractPipelineSurface | null | undefined;
|
|
966
966
|
readonly materialSnapshotCache: MaterialSnapshotCache | undefined;
|
package/src/materials.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
MaterialRenderState,
|
|
9
9
|
MaterialValue,
|
|
10
10
|
} from '@forgeax/engine-types';
|
|
11
|
-
import { deriveStandardLayerPlan } from '@forgeax/engine-types';
|
|
11
|
+
import { deriveStandardLayerPlan, standardSurfaceParameters } from '@forgeax/engine-types';
|
|
12
12
|
import {
|
|
13
13
|
DEFAULT_STANDARD_SURFACE_MODULE,
|
|
14
14
|
projectStandardSurfacePasses,
|
|
@@ -578,7 +578,7 @@ function standardDefault(opts: DefaultStandardOptions): MaterialAsset {
|
|
|
578
578
|
if (opts.attenuationDistance !== undefined) values.attenuationDistance = opts.attenuationDistance;
|
|
579
579
|
const forwardRenderState =
|
|
580
580
|
transmission > 0 ? { ...(opts.renderState ?? {}), depthWriteEnabled: false } : opts.renderState;
|
|
581
|
-
const parameters = standardParameters('linear', opts);
|
|
581
|
+
const parameters = standardSurfaceParameters(standardParameters('linear', opts));
|
|
582
582
|
const declaredNames = new Set(parameters.map((parameter) => parameter.name));
|
|
583
583
|
for (const name of Object.keys(values)) {
|
|
584
584
|
if (!declaredNames.has(name)) delete values[name];
|
|
@@ -621,7 +621,7 @@ function standardCustom(opts: CustomStandardSurfaceOptions): MaterialAsset {
|
|
|
621
621
|
...(opts.queue === undefined ? {} : { queue: opts.queue }),
|
|
622
622
|
layerPlan,
|
|
623
623
|
}),
|
|
624
|
-
parameters: opts.parameters,
|
|
624
|
+
parameters: standardSurfaceParameters(opts.parameters),
|
|
625
625
|
values: opts.values,
|
|
626
626
|
};
|
|
627
627
|
}
|
|
@@ -10,6 +10,8 @@ export interface PipelineSpec {
|
|
|
10
10
|
readonly id: string;
|
|
11
11
|
readonly passKind: string;
|
|
12
12
|
readonly variantSet: string | undefined;
|
|
13
|
+
readonly vertexEntry?: string;
|
|
14
|
+
readonly fragmentEntry?: string;
|
|
13
15
|
};
|
|
14
16
|
readonly attachments: {
|
|
15
17
|
readonly colorFormats: readonly GPUTextureFormat[];
|