@forgeax/engine-render 0.1.30 → 0.1.32
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 +68 -1
- package/dist/assembly/webgpu-ready.d.ts +2 -1
- package/dist/assembly/webgpu-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/{chunk-C3DDSXYK.mjs → chunk-4NNZQCL3.mjs} +110 -35
- package/dist/chunk-4NNZQCL3.mjs.map +1 -0
- package/dist/{chunk-JRBDO2KB.mjs → chunk-6TFBXLRJ.mjs} +19 -7
- package/dist/chunk-6TFBXLRJ.mjs.map +1 -0
- package/dist/{chunk-FUUO4UND.mjs → chunk-DRR7D3KX.mjs} +3 -3
- package/dist/chunk-DRR7D3KX.mjs.map +1 -0
- package/dist/{chunk-S5UE2T5P.mjs → chunk-GWCEMDVR.mjs} +380 -4
- package/dist/chunk-GWCEMDVR.mjs.map +1 -0
- package/dist/{chunk-MDQJPY3T.mjs → chunk-JCIRYQ4V.mjs} +3 -3
- package/dist/{chunk-MDQJPY3T.mjs.map → chunk-JCIRYQ4V.mjs.map} +1 -1
- package/dist/{chunk-PSEQ6QPH.mjs → chunk-JRIPCMYC.mjs} +17 -10
- package/dist/chunk-JRIPCMYC.mjs.map +1 -0
- package/dist/{chunk-7MQ3XBSP.mjs → chunk-LULCA3OF.mjs} +4 -4
- package/dist/{chunk-7MQ3XBSP.mjs.map → chunk-LULCA3OF.mjs.map} +1 -1
- package/dist/{chunk-NO4WZCAH.mjs → chunk-S665NJNF.mjs} +3 -3
- package/dist/{chunk-NO4WZCAH.mjs.map → chunk-S665NJNF.mjs.map} +1 -1
- package/dist/components/depth-of-field.d.ts +28 -0
- package/dist/components/depth-of-field.d.ts.map +1 -0
- package/dist/components/directional-light.d.ts +2 -1
- package/dist/components/directional-light.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +898 -328
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/extract/camera.d.ts.map +1 -1
- package/dist/features/depth-of-field/depth-of-field-assembly.d.ts +19 -0
- package/dist/features/depth-of-field/depth-of-field-assembly.d.ts.map +1 -0
- package/dist/features/depth-of-field/depth-of-field-feature.d.ts +43 -0
- package/dist/features/depth-of-field/depth-of-field-feature.d.ts.map +1 -0
- package/dist/features/depth-of-field/depth-of-field-inspection.d.ts +34 -0
- package/dist/features/depth-of-field/depth-of-field-inspection.d.ts.map +1 -0
- package/dist/features/depth-of-field/depth-of-field-params.d.ts +83 -0
- package/dist/features/depth-of-field/depth-of-field-params.d.ts.map +1 -0
- package/dist/features/types.d.ts +19 -1
- package/dist/features/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +6 -5
- package/dist/index.mjs.map +1 -1
- package/dist/inspection-types.d.ts +35 -0
- package/dist/inspection-types.d.ts.map +1 -1
- package/dist/instances.d.ts +7 -0
- package/dist/instances.d.ts.map +1 -1
- package/dist/internal.mjs +6 -6
- package/dist/mesh-material-bindings.d.ts +3 -0
- package/dist/mesh-material-bindings.d.ts.map +1 -1
- package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
- package/dist/pipeline/standard-output/types.d.ts +4 -2
- package/dist/pipeline/standard-output/types.d.ts.map +1 -1
- package/dist/pipeline/standard-post.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +21 -1
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +7 -0
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts.map +1 -1
- package/dist/render-contract.d.ts +8 -1
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-graph-primitives.d.ts +2 -0
- package/dist/render-graph-primitives.d.ts.map +1 -1
- package/dist/render-pipeline.d.ts +13 -1
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +18 -4
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +2 -1
- package/dist/render-system.d.ts.map +1 -1
- package/dist/temporal/index.mjs +5 -5
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/package.json +21 -21
- package/src/__tests__/gpu-driven-baseline.characterization.test.ts +12 -0
- package/src/__tests__/gpu-driven-production.integration.test.ts +27 -1
- package/src/__tests__/instances-store.unit.test.ts +15 -0
- package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +28 -0
- package/src/assembly/webgpu-ready.ts +5 -5
- package/src/assembly/webgpu-renderer.ts +14 -14
- package/src/components/depth-of-field.ts +45 -0
- package/src/components/directional-light.ts +2 -1
- package/src/components/index.ts +1 -0
- package/src/extract/camera.ts +22 -2
- package/src/features/depth-of-field/__tests__/depth-of-field-params.unit.test.ts +943 -0
- package/src/features/depth-of-field/depth-of-field-assembly.ts +100 -0
- package/src/features/depth-of-field/depth-of-field-feature.ts +295 -0
- package/src/features/depth-of-field/depth-of-field-inspection.ts +237 -0
- package/src/features/depth-of-field/depth-of-field-params.ts +556 -0
- package/src/features/host.ts +1 -1
- package/src/features/types.ts +18 -1
- package/src/index.ts +32 -0
- package/src/inspection-types.ts +38 -0
- package/src/instances.ts +19 -5
- package/src/mesh-material-bindings.ts +4 -0
- package/src/pipeline/standard-output/graph.ts +3 -0
- package/src/pipeline/standard-output/types.ts +4 -1
- package/src/pipeline/standard-post.ts +21 -2
- package/src/plugin.ts +2 -0
- package/src/record/frame-snapshot.ts +26 -1
- package/src/record/frame.ts +39 -6
- package/src/record/main-pass.ts +1 -5
- package/src/record/typed-frame-graph.ts +106 -6
- package/src/recovery/render-system-candidate.ts +6 -0
- package/src/render-contract.ts +11 -0
- package/src/render-graph-primitives.ts +8 -1
- package/src/render-pipeline.ts +17 -1
- package/src/render-system-extract-tail.ts +87 -13
- package/src/render-system-extract.ts +103 -25
- package/src/render-system.ts +78 -0
- package/src/typed-render-graph-primitives.ts +3 -0
- package/dist/chunk-C3DDSXYK.mjs.map +0 -1
- package/dist/chunk-FUUO4UND.mjs.map +0 -1
- package/dist/chunk-JRBDO2KB.mjs.map +0 -1
- package/dist/chunk-PSEQ6QPH.mjs.map +0 -1
- package/dist/chunk-S5UE2T5P.mjs.map +0 -1
|
@@ -6,6 +6,7 @@ const read = (path: string): string => readFileSync(new URL(path, import.meta.ur
|
|
|
6
6
|
const count = (source: string, pattern: RegExp): number => source.match(pattern)?.length ?? 0;
|
|
7
7
|
|
|
8
8
|
const productionRaster = read('../gpu-driven/production-raster.ts');
|
|
9
|
+
const mainPass = read('../record/main-pass.ts');
|
|
9
10
|
const shadowPass = read('../record/shadow-pass.ts');
|
|
10
11
|
const paletteAllocator = read('../systems/skin-palette-allocator.ts');
|
|
11
12
|
const extractRoot = read('../render-system-extract.ts');
|
|
@@ -46,6 +47,17 @@ describe('GPU-driven baseline characterization', () => {
|
|
|
46
47
|
expect(productionRaster).toContain('vertexLayout.arrayStride');
|
|
47
48
|
});
|
|
48
49
|
|
|
50
|
+
it('keeps CPU-only video slots out of the global GPU receipt gate', () => {
|
|
51
|
+
expect(mainPass).toContain('videoTextureFields?.size');
|
|
52
|
+
expect(mainPass).toContain('if (gpuDrivenStandardPbrFrameResources === undefined)');
|
|
53
|
+
expect(mainPass).not.toContain(
|
|
54
|
+
'gpuDrivenStandardPbrFrameResources.materialBindGroups.some((group) => group === undefined)',
|
|
55
|
+
);
|
|
56
|
+
expect(productionRaster).toMatch(
|
|
57
|
+
/material bind group for global slot \$\{globalMaterialSlot\}/,
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
49
61
|
it('freezes CPU shadow caster enumeration as the pre-GPU baseline', () => {
|
|
50
62
|
expect(shadowPass).toContain('validatedOrdered');
|
|
51
63
|
expect(shadowPass).toMatch(/for \(let i = 0; i < validatedOrdered\.length; i\+\+\)/);
|
|
@@ -824,6 +824,7 @@ describe('GPU-driven production projection', () => {
|
|
|
824
824
|
device.createBindGroup({ layout: materialBindGroup, entries: [] }).unwrap(),
|
|
825
825
|
],
|
|
826
826
|
};
|
|
827
|
+
let activeFrameResources = frameResources;
|
|
827
828
|
const color = graph
|
|
828
829
|
.createTexture('gpu-driven-production-color', {
|
|
829
830
|
format: 'rgba8unorm',
|
|
@@ -842,7 +843,8 @@ describe('GPU-driven production projection', () => {
|
|
|
842
843
|
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
843
844
|
},
|
|
844
845
|
],
|
|
845
|
-
encode: ({ pass, resources }) =>
|
|
846
|
+
encode: ({ pass, resources }) =>
|
|
847
|
+
gpu.encode(viewBindGroup, pass, resources, activeFrameResources),
|
|
846
848
|
})
|
|
847
849
|
.unwrap();
|
|
848
850
|
const compiled = graph.compile({ device, surfaceSize: { width: 1, height: 1 } }).unwrap();
|
|
@@ -872,6 +874,30 @@ describe('GPU-driven production projection', () => {
|
|
|
872
874
|
production.inspect().batchCount,
|
|
873
875
|
);
|
|
874
876
|
|
|
877
|
+
const missingMaterialFrameResources = {
|
|
878
|
+
...frameResources,
|
|
879
|
+
materialBindGroups: [undefined] as unknown as typeof frameResources.materialBindGroups,
|
|
880
|
+
};
|
|
881
|
+
activeFrameResources = missingMaterialFrameResources;
|
|
882
|
+
const missingMaterialEncoder = device
|
|
883
|
+
.createCommandEncoder({ label: 'gpu-driven-production-missing-material' })
|
|
884
|
+
.unwrap();
|
|
885
|
+
const missingMaterialResult = compiled.execute({
|
|
886
|
+
viewBindGroup,
|
|
887
|
+
encoder: missingMaterialEncoder,
|
|
888
|
+
gpuDrivenStandardPbrFrameResources: missingMaterialFrameResources,
|
|
889
|
+
} as unknown as RenderPipelineFrame);
|
|
890
|
+
expect(missingMaterialResult.ok).toBe(false);
|
|
891
|
+
if (!missingMaterialResult.ok) {
|
|
892
|
+
expect(missingMaterialResult.error.code).toBe('pass-encode-failed');
|
|
893
|
+
const detail = missingMaterialResult.error.detail;
|
|
894
|
+
expect(detail).toBeDefined();
|
|
895
|
+
expect(detail !== undefined && 'cause' in detail).toBe(true);
|
|
896
|
+
if (detail === undefined || !('cause' in detail)) return;
|
|
897
|
+
expect(detail.cause).toBeInstanceOf(RhiError);
|
|
898
|
+
expect((detail.cause as RhiError).code).toBe('rhi-not-available');
|
|
899
|
+
}
|
|
900
|
+
|
|
875
901
|
// The facet projection is the same admission set consumed by the GPU
|
|
876
902
|
// candidate upload. A suppressed entity must disappear from the indirect
|
|
877
903
|
// plan without making the remaining entity fall back to CPU validation.
|
|
@@ -51,6 +51,21 @@ describe('InstanceCollectionStore', () => {
|
|
|
51
51
|
]);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
it('keeps renderer dirty ranges intact when inspection peeks between updates', () => {
|
|
55
|
+
const store = new InstanceCollectionStore();
|
|
56
|
+
const created = store.create({ transforms: identityTransforms(2) }).unwrap();
|
|
57
|
+
store.snapshot(created.collectionId).unwrap();
|
|
58
|
+
|
|
59
|
+
store.update(created.collectionId, { start: 0, transforms: identityTransforms(1) }).unwrap();
|
|
60
|
+
const inspected = store.peek(created.collectionId).unwrap();
|
|
61
|
+
expect(inspected.dirtyRanges).toEqual([{ start: 0, end: 1 }]);
|
|
62
|
+
store.update(created.collectionId, { start: 1, transforms: identityTransforms(1) }).unwrap();
|
|
63
|
+
|
|
64
|
+
expect(store.snapshot(created.collectionId).unwrap().dirtyRanges).toEqual([
|
|
65
|
+
{ start: 0, end: 2 },
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
54
69
|
it('keeps collection identity in the renderer cache key', () => {
|
|
55
70
|
const first = instanceCollectionCacheKey(0, { cacheKey: 9, collectionId: 1 });
|
|
56
71
|
const second = instanceCollectionCacheKey(0, { cacheKey: 9, collectionId: 2 });
|
|
@@ -404,6 +404,34 @@ describe('prepared Standard PBR producer-to-record integration', () => {
|
|
|
404
404
|
expect(result.ok).toBe(true);
|
|
405
405
|
});
|
|
406
406
|
|
|
407
|
+
it('keeps a video material on the CPU residual beside an eligible static PBR draw', () => {
|
|
408
|
+
const source = artifact();
|
|
409
|
+
const staticMaterial = material();
|
|
410
|
+
const videoMaterial: MaterialSnapshot = {
|
|
411
|
+
...staticMaterial,
|
|
412
|
+
videoTextureFields: new Map([['baseColorTexture', 7]]) as never,
|
|
413
|
+
};
|
|
414
|
+
const mesh = createBoxGeometry(1, 1, 1).unwrap();
|
|
415
|
+
const staticDraws = buildGpuDrivenDraws({
|
|
416
|
+
mesh,
|
|
417
|
+
materials: [staticMaterial],
|
|
418
|
+
fallbackMaterial: staticMaterial,
|
|
419
|
+
baseSnapshot: snapshot(staticMaterial),
|
|
420
|
+
getMaterialShaderArtifact: () => source,
|
|
421
|
+
});
|
|
422
|
+
const videoDraws = buildGpuDrivenDraws({
|
|
423
|
+
mesh,
|
|
424
|
+
materials: [videoMaterial],
|
|
425
|
+
fallbackMaterial: videoMaterial,
|
|
426
|
+
baseSnapshot: snapshot(videoMaterial),
|
|
427
|
+
getMaterialShaderArtifact: () => source,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
expect(staticDraws[0]?.prepared).toBeDefined();
|
|
431
|
+
expect(videoDraws[0]?.prepared).toBeUndefined();
|
|
432
|
+
expect(videoDraws[0]?.preparationError).toBeUndefined();
|
|
433
|
+
});
|
|
434
|
+
|
|
407
435
|
it('admits a producer-backed custom material without a Standard shader name', () => {
|
|
408
436
|
const source = customArtifact();
|
|
409
437
|
const customMaterial: MaterialSnapshot = {
|
|
@@ -28,6 +28,7 @@ import { ok, RhiError } from '@forgeax/engine-rhi';
|
|
|
28
28
|
import { findVariantByKey, type ShaderCatalog } from '@forgeax/engine-shader';
|
|
29
29
|
import type { ManifestEntry } from '@forgeax/engine-types';
|
|
30
30
|
import { handleSlot } from '@forgeax/engine-types';
|
|
31
|
+
import { prewarmDepthOfField } from '../features/depth-of-field/depth-of-field-assembly';
|
|
31
32
|
import { prewarmAtmosphereShaders } from './atmosphere-shader-prewarm';
|
|
32
33
|
import { prewarmSsrShaders } from './ssr-shader-prewarm';
|
|
33
34
|
|
|
@@ -222,6 +223,7 @@ export async function buildReadyWebGPU(
|
|
|
222
223
|
readonly motionBlur?: string;
|
|
223
224
|
readonly taaResolve?: string;
|
|
224
225
|
}) => void,
|
|
226
|
+
registerBuiltinDepthOfField: Parameters<typeof prewarmDepthOfField>[2],
|
|
225
227
|
/** Install manifest-owned volume utility sources only after all four modules compile. */
|
|
226
228
|
setVolumetricFogShaderSources: (sources: VolumetricFogShaderSources) => void,
|
|
227
229
|
/** Install manifest-owned SSR utility sources only after all five modules compile. */
|
|
@@ -1145,11 +1147,8 @@ export async function buildReadyWebGPU(
|
|
|
1145
1147
|
);
|
|
1146
1148
|
registerBuiltinTonemap(tonemapEntry.wgsl);
|
|
1147
1149
|
|
|
1148
|
-
//
|
|
1149
|
-
//
|
|
1150
|
-
// resolves. Otherwise the first tight draw loop sees an asynchronous
|
|
1151
|
-
// `rhi-not-available` result and leaves the temporal targets cleared even
|
|
1152
|
-
// though the graph topology is present.
|
|
1150
|
+
// Prewarm temporal fullscreen modules before readiness so first-frame
|
|
1151
|
+
// recording cannot encounter an asynchronous compile miss.
|
|
1153
1152
|
const prewarmTemporal = async (entry: ManifestEntry | undefined, id: string) => {
|
|
1154
1153
|
if (entry === undefined) return;
|
|
1155
1154
|
const prewarm = await runShimStep(
|
|
@@ -1172,6 +1171,7 @@ export async function buildReadyWebGPU(
|
|
|
1172
1171
|
};
|
|
1173
1172
|
await prewarmTemporal(motionBlurEntry, 'forgeax.motion-blur');
|
|
1174
1173
|
await prewarmTemporal(taaResolveEntry, 'forgeax.taa-resolve');
|
|
1174
|
+
await prewarmDepthOfField(manifestEntries, prewarmTemporal, registerBuiltinDepthOfField);
|
|
1175
1175
|
registerBuiltinTemporalPostProcesses({
|
|
1176
1176
|
...(motionBlurEntry === undefined ? {} : { motionBlur: motionBlurEntry.wgsl }),
|
|
1177
1177
|
...(taaResolveEntry === undefined ? {} : { taaResolve: taaResolveEntry.wgsl }),
|
|
@@ -53,6 +53,10 @@ import {
|
|
|
53
53
|
RenderFeatureCapabilityMissingError,
|
|
54
54
|
RenderFeatureStageFailedError,
|
|
55
55
|
} from '../errors/render';
|
|
56
|
+
import {
|
|
57
|
+
dofInspection,
|
|
58
|
+
registerDepthOfFieldBuiltins,
|
|
59
|
+
} from '../features/depth-of-field/depth-of-field-assembly';
|
|
56
60
|
import { createRenderFeatureHost } from '../features/host';
|
|
57
61
|
import { RENDER_FEATURE_VERTEX_LAYOUTS } from '../features/prepared-graphics';
|
|
58
62
|
import type { RenderFeature, RenderFeatureShaderModuleMode } from '../features/types';
|
|
@@ -1248,13 +1252,6 @@ async function makeWebGPURenderer(
|
|
|
1248
1252
|
}
|
|
1249
1253
|
return undefined;
|
|
1250
1254
|
};
|
|
1251
|
-
// feat-20260622-s5 M3 / w17: the pipeline build is factored into a closure so
|
|
1252
|
-
// the recover() rebuild can re-run the SAME three-step assembly against the
|
|
1253
|
-
// freshly-acquired device (SSOT — one build path, one set of seed callbacks).
|
|
1254
|
-
// All captured references (getShader / gpuStore / getShaderModuleAdapter /
|
|
1255
|
-
// materialShaderPipelineCache / renderSystem) are stable across recover; only
|
|
1256
|
-
// `internals.device` / `internals.pack` are read live, so a rebuild after a
|
|
1257
|
-
// device swap compiles against the new device.
|
|
1258
1255
|
const promotePipelineExtendedLighting = (state: PipelineState, scope: DeviceScope): void => {
|
|
1259
1256
|
if (
|
|
1260
1257
|
state.extendedLightingAvailable !== true ||
|
|
@@ -1287,9 +1284,10 @@ async function makeWebGPURenderer(
|
|
|
1287
1284
|
};
|
|
1288
1285
|
extendedLightingState = promoteExtendedLightingCandidate(extendedLightingState, candidate);
|
|
1289
1286
|
};
|
|
1290
|
-
let tonemapRegistered = false
|
|
1291
|
-
|
|
1292
|
-
let temporalPostProcessesRegistered = false
|
|
1287
|
+
let tonemapRegistered = false,
|
|
1288
|
+
fxaaRegistered = false;
|
|
1289
|
+
let temporalPostProcessesRegistered = false,
|
|
1290
|
+
depthOfFieldRegistered = false;
|
|
1293
1291
|
const buildPipeline = (
|
|
1294
1292
|
scope: DeviceScope = activeDeviceScope,
|
|
1295
1293
|
device: RhiDevice = currentBuildDevice(),
|
|
@@ -1311,7 +1309,6 @@ async function makeWebGPURenderer(
|
|
|
1311
1309
|
),
|
|
1312
1310
|
materialShaderUvSetCounts.get('forgeax::default-standard-pbr') ?? 0,
|
|
1313
1311
|
requiredFullscreenPostProcesses,
|
|
1314
|
-
// Publish growth and capacity together for the active or candidate device.
|
|
1315
1312
|
(hook, state) => {
|
|
1316
1313
|
if (candidateBuildState !== undefined) {
|
|
1317
1314
|
candidateBuildState.growMeshSsbo = hook;
|
|
@@ -1321,12 +1318,10 @@ async function makeWebGPURenderer(
|
|
|
1321
1318
|
internals.meshSsboState = state;
|
|
1322
1319
|
}
|
|
1323
1320
|
},
|
|
1324
|
-
// Both module adapters consume the generation's eager shader cache.
|
|
1325
1321
|
(label, module) => {
|
|
1326
1322
|
getShaderModuleAdapter().seedModule(label, module);
|
|
1327
1323
|
getImmediateShaderModuleAdapter().seedModule(label, module);
|
|
1328
1324
|
},
|
|
1329
|
-
// Prewarm only the active build generation's material pipeline cache.
|
|
1330
1325
|
(key, pso, group2Contract) => {
|
|
1331
1326
|
const pipelineCache = currentPipelineCacheState().materialShaderPipelineCache;
|
|
1332
1327
|
if (!pipelineCache.has(key)) {
|
|
@@ -1334,7 +1329,6 @@ async function makeWebGPURenderer(
|
|
|
1334
1329
|
}
|
|
1335
1330
|
materialShaderPipelineGroup2Contracts.set(pso as object, group2Contract);
|
|
1336
1331
|
},
|
|
1337
|
-
// Register the output transform after manifest loading, once RenderSystem exists.
|
|
1338
1332
|
(source: string) => {
|
|
1339
1333
|
if (candidateBuildState !== undefined || tonemapRegistered) return;
|
|
1340
1334
|
renderSystem.registerBuiltinPostProcess(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, {
|
|
@@ -1371,6 +1365,11 @@ async function makeWebGPURenderer(
|
|
|
1371
1365
|
}
|
|
1372
1366
|
temporalPostProcessesRegistered = true;
|
|
1373
1367
|
},
|
|
1368
|
+
(entries: { readonly singleSample?: string; readonly multisampled?: string }) => {
|
|
1369
|
+
if (candidateBuildState !== undefined || depthOfFieldRegistered) return;
|
|
1370
|
+
registerDepthOfFieldBuiltins(renderSystem, entries);
|
|
1371
|
+
depthOfFieldRegistered = true;
|
|
1372
|
+
},
|
|
1374
1373
|
(sources: VolumetricFogShaderSources) => {
|
|
1375
1374
|
internals.volumetricFogShaders = sources;
|
|
1376
1375
|
},
|
|
@@ -2810,6 +2809,7 @@ async function makeWebGPURenderer(
|
|
|
2810
2809
|
...(renderSystem.motionBlurInspection === undefined
|
|
2811
2810
|
? {}
|
|
2812
2811
|
: { motionBlur: Object.freeze(renderSystem.motionBlurInspection) }),
|
|
2812
|
+
...dofInspection(renderSystem.depthOfFieldInspection),
|
|
2813
2813
|
...(renderSystem.lodOcclusionInspection === undefined
|
|
2814
2814
|
? {}
|
|
2815
2815
|
: { lodOcclusion: Object.freeze(renderSystem.lodOcclusionInspection) }),
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { defineComponent, type SchemaOf, type ShapeOf } from '@forgeax/engine-ecs';
|
|
2
|
+
|
|
3
|
+
/** Closed quality budget used by the built-in camera depth-of-field pass. */
|
|
4
|
+
export type DepthOfFieldQuality = 'low' | 'medium' | 'high';
|
|
5
|
+
|
|
6
|
+
export const DepthOfFieldQualityValue = Object.freeze({
|
|
7
|
+
low: 0,
|
|
8
|
+
medium: 1,
|
|
9
|
+
high: 2,
|
|
10
|
+
} as const);
|
|
11
|
+
|
|
12
|
+
/** Selects which signed circle-of-confusion side contributes to the image. */
|
|
13
|
+
export type DepthOfFieldSide = 'both' | 'near' | 'far';
|
|
14
|
+
|
|
15
|
+
export const DepthOfFieldSideValue = Object.freeze({
|
|
16
|
+
both: 0,
|
|
17
|
+
near: 1,
|
|
18
|
+
far: 2,
|
|
19
|
+
} as const);
|
|
20
|
+
|
|
21
|
+
/** Active-camera physical depth-of-field authoring data; presence enables it. */
|
|
22
|
+
export const DepthOfField = defineComponent('DepthOfField', {
|
|
23
|
+
focusDistance: { type: 'f32', default: 8 },
|
|
24
|
+
fStop: { type: 'f32', default: 2.8 },
|
|
25
|
+
sensorHeight: { type: 'f32', default: 0.024 },
|
|
26
|
+
maxRadiusPixels: { type: 'f32', default: 16 },
|
|
27
|
+
quality: { type: 'f32', default: DepthOfFieldQualityValue.medium },
|
|
28
|
+
blurSide: { type: 'f32', default: DepthOfFieldSideValue.both },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type DepthOfFieldData = ShapeOf<SchemaOf<typeof DepthOfField>>;
|
|
32
|
+
|
|
33
|
+
export function depthOfFieldQualityFromF32(value: number): DepthOfFieldQuality | undefined {
|
|
34
|
+
if (value === DepthOfFieldQualityValue.low) return 'low';
|
|
35
|
+
if (value === DepthOfFieldQualityValue.medium) return 'medium';
|
|
36
|
+
if (value === DepthOfFieldQualityValue.high) return 'high';
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function depthOfFieldSideFromF32(value: number): DepthOfFieldSide | undefined {
|
|
41
|
+
if (value === DepthOfFieldSideValue.both) return 'both';
|
|
42
|
+
if (value === DepthOfFieldSideValue.near) return 'near';
|
|
43
|
+
if (value === DepthOfFieldSideValue.far) return 'far';
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
@@ -60,7 +60,8 @@ export {
|
|
|
60
60
|
* splitLambda ∈ [0,1] — PSSM split weight (default 0.75)
|
|
61
61
|
* cascadeBlend ∈ [0,0.5] — blend width between cascades (default 0.2)
|
|
62
62
|
* mapSize >= 1 — shadow map resolution (default 2048)
|
|
63
|
-
* depthBias — normalized receiver depth
|
|
63
|
+
* depthBias — normalized receiver depth floor (default 0.00001),
|
|
64
|
+
* plus automatic texel/filter/slope coverage
|
|
64
65
|
* normalBias — world-space receiver normal offset (default 0.05)
|
|
65
66
|
* shadowDistance > 0 — how far shadows reach in front of the camera,
|
|
66
67
|
* in world units (default 200). This is the sole
|
package/src/components/index.ts
CHANGED
package/src/extract/camera.ts
CHANGED
|
@@ -11,12 +11,17 @@ import {
|
|
|
11
11
|
tonemapFromF32,
|
|
12
12
|
validateCameraColorGrading,
|
|
13
13
|
} from '../components/camera';
|
|
14
|
+
import { DepthOfField } from '../components/depth-of-field';
|
|
14
15
|
import {
|
|
15
16
|
DynamicResolution,
|
|
16
17
|
validateDynamicResolutionCamera,
|
|
17
18
|
} from '../components/dynamic-resolution';
|
|
18
19
|
import { MotionBlur } from '../components/motion-blur';
|
|
19
20
|
import { ScreenSpaceReflection } from '../components/screen-space-reflection';
|
|
21
|
+
import {
|
|
22
|
+
depthOfFieldRequestFailure,
|
|
23
|
+
resolveDepthOfFieldParams,
|
|
24
|
+
} from '../features/depth-of-field/depth-of-field-params';
|
|
20
25
|
import type { CameraSnapshot } from '../render-contract';
|
|
21
26
|
import { getActiveCamera, selectActiveCameraIndex } from '../systems/active-camera';
|
|
22
27
|
|
|
@@ -32,15 +37,26 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
|
|
|
32
37
|
const cameraQuery = world
|
|
33
38
|
.query({
|
|
34
39
|
read: [Camera],
|
|
35
|
-
optional: [MotionBlur, DynamicResolution, ScreenSpaceReflection],
|
|
40
|
+
optional: [MotionBlur, DynamicResolution, ScreenSpaceReflection, DepthOfField],
|
|
36
41
|
with: [GlobalTransform],
|
|
37
42
|
})
|
|
38
43
|
.unwrap();
|
|
39
44
|
for (const row of cameraQuery) {
|
|
40
45
|
const cam = row.get(Camera);
|
|
41
46
|
const motionBlur = row.get(MotionBlur);
|
|
47
|
+
const depthOfField = row.get(DepthOfField);
|
|
42
48
|
validateCameraColorGrading(cam.temperature, cam.tint, cam.colorLutStrength);
|
|
43
49
|
const dynamicResolution = row.get(DynamicResolution);
|
|
50
|
+
const projection = cameraProjectionFromF32(cam.projection);
|
|
51
|
+
const depthOfFieldResult = resolveDepthOfFieldParams(depthOfField, {
|
|
52
|
+
projection,
|
|
53
|
+
fov: cam.fov,
|
|
54
|
+
near: cam.near,
|
|
55
|
+
far: cam.far,
|
|
56
|
+
});
|
|
57
|
+
const depthOfFieldError = depthOfFieldResult.ok
|
|
58
|
+
? undefined
|
|
59
|
+
: depthOfFieldRequestFailure(depthOfFieldResult.error);
|
|
44
60
|
const screenSpaceReflection = row.get(ScreenSpaceReflection);
|
|
45
61
|
const entity = row.entity as EntityHandle;
|
|
46
62
|
const worldMat = readWorldMatrix(world, entity);
|
|
@@ -57,7 +73,7 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
|
|
|
57
73
|
aspect: cam.aspect,
|
|
58
74
|
near: cam.near,
|
|
59
75
|
far: cam.far,
|
|
60
|
-
projection
|
|
76
|
+
projection,
|
|
61
77
|
orthoLeft: cam.left,
|
|
62
78
|
orthoRight: cam.right,
|
|
63
79
|
orthoBottom: cam.bottom,
|
|
@@ -86,6 +102,10 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
|
|
|
86
102
|
cam.clearColor[2] ?? 0,
|
|
87
103
|
cam.clearColor[3] ?? 1,
|
|
88
104
|
],
|
|
105
|
+
...(depthOfFieldResult.ok && depthOfFieldResult.value !== undefined
|
|
106
|
+
? { depthOfField: depthOfFieldResult.value }
|
|
107
|
+
: {}),
|
|
108
|
+
...(depthOfFieldError === undefined ? {} : { depthOfFieldError }),
|
|
89
109
|
...(motionBlur === undefined
|
|
90
110
|
? {}
|
|
91
111
|
: {
|