@forgeax/engine-render 0.1.31 → 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
|
@@ -120,6 +120,7 @@ import type {
|
|
|
120
120
|
ExtractPipelineSurface,
|
|
121
121
|
InstancesSnapshot,
|
|
122
122
|
LightProbeSnapshot,
|
|
123
|
+
MaterialProgramSelection,
|
|
123
124
|
MaterialSnapshot,
|
|
124
125
|
MaterialSnapshotCache,
|
|
125
126
|
MaterialSnapshotCachesByWorld,
|
|
@@ -161,7 +162,7 @@ import {
|
|
|
161
162
|
materialColorParameterSchema,
|
|
162
163
|
materialNormalScale,
|
|
163
164
|
materialParamSchemaForMaterial,
|
|
164
|
-
|
|
165
|
+
materialProgramSelectionsForMaterial,
|
|
165
166
|
materialSceneIndexProgramKeysForMaterial,
|
|
166
167
|
materialStandardTextureMask,
|
|
167
168
|
materialTextureFields,
|
|
@@ -191,6 +192,25 @@ function skinPaletteIdentity(world: World, entity: number): string {
|
|
|
191
192
|
return `skin:${world.identity}:${entity}`;
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
function materialProgramSelectionRequiresSkin(selection: {
|
|
196
|
+
readonly abi?: {
|
|
197
|
+
readonly skinPaletteAddress?: unknown;
|
|
198
|
+
readonly vertexInputs?: readonly { readonly semantic?: unknown }[];
|
|
199
|
+
};
|
|
200
|
+
}): boolean {
|
|
201
|
+
if (selection.abi?.skinPaletteAddress !== undefined) return true;
|
|
202
|
+
const semantics = new Set((selection.abi?.vertexInputs ?? []).map((input) => input.semantic));
|
|
203
|
+
return semantics.has('skinIndex') && semantics.has('skinWeight');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isMissingMaterialProgram(error: unknown): boolean {
|
|
207
|
+
return (
|
|
208
|
+
error !== null &&
|
|
209
|
+
typeof error === 'object' &&
|
|
210
|
+
(error as { readonly code?: unknown }).code === 'material-specialization-not-cooked'
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
194
214
|
export function extractFrames(
|
|
195
215
|
worlds: readonly World[],
|
|
196
216
|
owner: number | ExtractFramesOwner,
|
|
@@ -482,8 +502,8 @@ export function extractFrames(
|
|
|
482
502
|
// bug-20260710-editor-cross-world-shadow: RECOMPUTE directional CSM matrices
|
|
483
503
|
// against the surfaced camera. In a single-world app the per-world extract
|
|
484
504
|
// already produced correct matrices (light+camera share the world), and this
|
|
485
|
-
// recompute
|
|
486
|
-
// the
|
|
505
|
+
// recompute uses the same config and camera. In the editor super-composite
|
|
506
|
+
// the directional light's world has NO camera, so
|
|
487
507
|
// its per-world `lightViewProj` is undefined/degenerate; the surfaced camera
|
|
488
508
|
// lives in the cameraOwner world. Pairing them here is the ONLY place both
|
|
489
509
|
// are visible. Point/spot shadows are camera-independent (light-space only)
|
|
@@ -1226,9 +1246,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
1226
1246
|
// bug-20260710-editor-cross-world-shadow: CSM matrices are computed by the
|
|
1227
1247
|
// shared pure {@link computeDirectionalCsm}, called here per-world with THIS
|
|
1228
1248
|
// world's own `cameras[0]`. In a single-world app the light and camera share
|
|
1229
|
-
// the world, so this per-world result is final
|
|
1230
|
-
//
|
|
1231
|
-
//
|
|
1249
|
+
// the world, so this per-world result is final. In the editor
|
|
1250
|
+
// super-composite the light's world may have no camera → this yields no
|
|
1251
|
+
// matrices; {@link extractFrames} then RECOMPUTES at
|
|
1232
1252
|
// the merge layer using the surfaced (cameraOwner) camera + the raw config
|
|
1233
1253
|
// carried on ExtractedLights. The raw config + direction are surfaced
|
|
1234
1254
|
// unconditionally so the merge layer can re-run the builder.
|
|
@@ -2030,6 +2050,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
2030
2050
|
: undefined;
|
|
2031
2051
|
|
|
2032
2052
|
let materialSnap: MaterialSnapshot;
|
|
2053
|
+
let materialProgramSelections: readonly MaterialProgramSelection[] | undefined;
|
|
2033
2054
|
|
|
2034
2055
|
if (cachedMaterial !== undefined) {
|
|
2035
2056
|
materialSnap = cachedMaterial.snapshot;
|
|
@@ -2143,11 +2164,59 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
2143
2164
|
...context.materialContext,
|
|
2144
2165
|
geometry: hasSkin ? ('skinned' as const) : context.materialContext.geometry,
|
|
2145
2166
|
};
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2167
|
+
try {
|
|
2168
|
+
materialProgramSelections = materialProgramSelectionsForMaterial(
|
|
2169
|
+
asset,
|
|
2170
|
+
assets,
|
|
2171
|
+
materialContext,
|
|
2172
|
+
);
|
|
2173
|
+
} catch (error) {
|
|
2174
|
+
// A standalone mesh can only satisfy a cooked skinned program
|
|
2175
|
+
// through a Skin-bearing mesh. Probe that same exact owner
|
|
2176
|
+
// selection only when the resolved mesh really lacks the
|
|
2177
|
+
// advertised attributes; otherwise retain the producer's exact
|
|
2178
|
+
// publication error instead of guessing from module names/order.
|
|
2179
|
+
if (isMissingMaterialProgram(error) && materialContext?.geometry === 'mesh') {
|
|
2180
|
+
try {
|
|
2181
|
+
const skinnedSelections = materialProgramSelectionsForMaterial(asset, assets, {
|
|
2182
|
+
...materialContext,
|
|
2183
|
+
geometry: 'skinned',
|
|
2184
|
+
});
|
|
2185
|
+
if (
|
|
2186
|
+
skinnedSelections?.some(materialProgramSelectionRequiresSkin) === true &&
|
|
2187
|
+
gpuDrivenMesh !== undefined
|
|
2188
|
+
) {
|
|
2189
|
+
const hasSkinIndex = gpuDrivenMesh.attributes.skinIndex !== undefined;
|
|
2190
|
+
const hasSkinWeight = gpuDrivenMesh.attributes.skinWeight !== undefined;
|
|
2191
|
+
if (!hasSkinIndex || !hasSkinWeight) {
|
|
2192
|
+
const missing =
|
|
2193
|
+
!hasSkinIndex && !hasSkinWeight
|
|
2194
|
+
? 'both'
|
|
2195
|
+
: !hasSkinIndex
|
|
2196
|
+
? 'skinIndex'
|
|
2197
|
+
: 'skinWeight';
|
|
2198
|
+
worldInternal._routeError(new MaterialSkinAttrMissingError(entity, missing), {
|
|
2199
|
+
severity: Severity.Error,
|
|
2200
|
+
systemName: 'RenderSystem.extract (material-skin-attr-missing)',
|
|
2201
|
+
});
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
} catch {
|
|
2206
|
+
// Preserve the original exact-context publication error.
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
throw error;
|
|
2210
|
+
}
|
|
2211
|
+
const materialProgramKeys =
|
|
2212
|
+
materialProgramSelections === undefined
|
|
2213
|
+
? undefined
|
|
2214
|
+
: Object.fromEntries(
|
|
2215
|
+
materialProgramSelections.map(({ pass, specializationKey }) => [
|
|
2216
|
+
pass,
|
|
2217
|
+
specializationKey,
|
|
2218
|
+
]),
|
|
2219
|
+
);
|
|
2151
2220
|
const materialSceneIndexProgramKeys = materialSceneIndexProgramKeysForMaterial(
|
|
2152
2221
|
asset,
|
|
2153
2222
|
assets,
|
|
@@ -2208,7 +2277,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
2208
2277
|
skinSkeletonView !== undefined &&
|
|
2209
2278
|
skinSkeletonView !== undefined &&
|
|
2210
2279
|
skinSkeletonView !== 0;
|
|
2211
|
-
const isPbrSkinMaterial =
|
|
2280
|
+
const isPbrSkinMaterial =
|
|
2281
|
+
materialProgramSelections?.some(materialProgramSelectionRequiresSkin) === true ||
|
|
2282
|
+
isStandardPbrSkinMaterialShader(firstPassShader);
|
|
2212
2283
|
if (hasSkinSkel && !isPbrSkinMaterial) {
|
|
2213
2284
|
worldInternal._routeError(new SkinMaterialMismatchError(entity, firstPassShader), {
|
|
2214
2285
|
severity: Severity.Error,
|
|
@@ -2759,7 +2830,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
2759
2830
|
// reinterpret a skinned vertex stream through an ordinary mesh group.
|
|
2760
2831
|
if (skinSlice === undefined && hasSkin) {
|
|
2761
2832
|
releaseSkinIdentity();
|
|
2762
|
-
if (
|
|
2833
|
+
if (
|
|
2834
|
+
materialSnap.materialShaderId === 'forgeax::pbr-skin' ||
|
|
2835
|
+
materialProgramSelections?.some(materialProgramSelectionRequiresSkin) === true
|
|
2836
|
+
) {
|
|
2763
2837
|
pendingDispatch.length = 0;
|
|
2764
2838
|
continue;
|
|
2765
2839
|
}
|
|
@@ -116,6 +116,7 @@ import type {
|
|
|
116
116
|
MaterialColorParameterSchema,
|
|
117
117
|
MaterialParameter,
|
|
118
118
|
MaterialPass,
|
|
119
|
+
MaterialProgramAbi,
|
|
119
120
|
MaterialRenderState,
|
|
120
121
|
MaterialTextureCoordinates,
|
|
121
122
|
MaterialTextureValue,
|
|
@@ -149,6 +150,7 @@ import {
|
|
|
149
150
|
tonemapToU32,
|
|
150
151
|
validateDynamicResolutionCamera,
|
|
151
152
|
} from './components';
|
|
153
|
+
import { DepthOfField } from './components/depth-of-field';
|
|
152
154
|
import type { DirectionalShadowQuality } from './components/directional-shadow-filter';
|
|
153
155
|
import type { LightValidationError } from './components/light-helpers';
|
|
154
156
|
import { selectEnvironment } from './environment/frame';
|
|
@@ -165,6 +167,10 @@ import type {
|
|
|
165
167
|
} from './extract/environment';
|
|
166
168
|
import type { GpuDrivenDrawSnapshot } from './extract/gpu-driven';
|
|
167
169
|
import { resolveVisibility, type VisibilitySnapshot } from './extract/visibility';
|
|
170
|
+
import {
|
|
171
|
+
depthOfFieldRequestFailure,
|
|
172
|
+
resolveDepthOfFieldParams,
|
|
173
|
+
} from './features/depth-of-field/depth-of-field-params';
|
|
168
174
|
import type {
|
|
169
175
|
RenderFeatureHiddenEntityReport,
|
|
170
176
|
RenderFeatureWorldVisibilitySnapshot,
|
|
@@ -1543,6 +1549,53 @@ export function materialProgramKeysForMaterial(
|
|
|
1543
1549
|
return materialProgramKeysForAddress(material, assets, context, 'direct');
|
|
1544
1550
|
}
|
|
1545
1551
|
|
|
1552
|
+
/**
|
|
1553
|
+
* Return the exact direct/scene-index programs selected for one renderer
|
|
1554
|
+
* context. The selection result is kept next to the key projection so
|
|
1555
|
+
* consumers that validate a mesh contract can inspect the producer ABI
|
|
1556
|
+
* without rebuilding pass/context matching from payload order.
|
|
1557
|
+
*/
|
|
1558
|
+
export interface MaterialProgramSelection {
|
|
1559
|
+
readonly pass: string;
|
|
1560
|
+
readonly context: MaterialCookProgramContext;
|
|
1561
|
+
readonly specializationKey: string;
|
|
1562
|
+
readonly abi?: MaterialProgramAbi;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
export function materialProgramSelectionsForMaterial(
|
|
1566
|
+
material: MaterialAsset,
|
|
1567
|
+
assets: AssetRegistry,
|
|
1568
|
+
context: MaterialCookProgramContext | undefined,
|
|
1569
|
+
address: 'direct' | 'scene-index' = 'direct',
|
|
1570
|
+
): readonly MaterialProgramSelection[] | undefined {
|
|
1571
|
+
const projection = assets.getMaterialProjectionForPayload(material);
|
|
1572
|
+
if (projection === undefined) return undefined;
|
|
1573
|
+
if (context === undefined)
|
|
1574
|
+
throw new Error('Published material selection requires renderer-owned compiler context');
|
|
1575
|
+
return projection.passes.map((pass) => {
|
|
1576
|
+
const mode = String(
|
|
1577
|
+
(pass.renderState?.tags as Record<string, unknown> | undefined)?.LightMode ?? pass.name,
|
|
1578
|
+
);
|
|
1579
|
+
const passKind = /shadow/i.test(mode)
|
|
1580
|
+
? ('shadow' as const)
|
|
1581
|
+
: /depth/i.test(mode)
|
|
1582
|
+
? ('depth' as const)
|
|
1583
|
+
: ('forward' as const);
|
|
1584
|
+
const selectedContext: MaterialCookProgramContext = {
|
|
1585
|
+
...context,
|
|
1586
|
+
pass: passKind,
|
|
1587
|
+
pipeline: /deferred|gbuffer/i.test(mode) ? 'deferred' : context.pipeline,
|
|
1588
|
+
};
|
|
1589
|
+
const selected = selectMaterialPassProgram(projection, pass.name, selectedContext, address);
|
|
1590
|
+
return {
|
|
1591
|
+
pass: pass.name,
|
|
1592
|
+
context: selectedContext,
|
|
1593
|
+
specializationKey: selected.specializationKey,
|
|
1594
|
+
...(selected.abi === undefined ? {} : { abi: selected.abi }),
|
|
1595
|
+
};
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1546
1599
|
/** Resolve the producer's scene-index program key for each cooked Pass. */
|
|
1547
1600
|
export function materialSceneIndexProgramKeysForMaterial(
|
|
1548
1601
|
material: MaterialAsset,
|
|
@@ -1597,26 +1650,12 @@ function materialProgramKeysForAddress(
|
|
|
1597
1650
|
context: MaterialCookProgramContext | undefined,
|
|
1598
1651
|
address: 'direct' | 'scene-index',
|
|
1599
1652
|
): Readonly<Record<string, string>> | undefined {
|
|
1600
|
-
const
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
const mode = String(
|
|
1607
|
-
(pass.renderState?.tags as Record<string, unknown> | undefined)?.LightMode ?? pass.name,
|
|
1608
|
-
);
|
|
1609
|
-
const selectedContext: MaterialCookProgramContext = {
|
|
1610
|
-
...context,
|
|
1611
|
-
pass: /shadow/i.test(mode) ? 'shadow' : /depth/i.test(mode) ? 'depth' : 'forward',
|
|
1612
|
-
pipeline: /deferred|gbuffer/i.test(mode) ? 'deferred' : context.pipeline,
|
|
1613
|
-
};
|
|
1614
|
-
entries.push([
|
|
1615
|
-
pass.name,
|
|
1616
|
-
selectMaterialPassProgram(projection, pass.name, selectedContext, address).specializationKey,
|
|
1617
|
-
]);
|
|
1618
|
-
}
|
|
1619
|
-
return entries.length === 0 ? undefined : Object.fromEntries(entries);
|
|
1653
|
+
const selections = materialProgramSelectionsForMaterial(material, assets, context, address);
|
|
1654
|
+
return selections === undefined
|
|
1655
|
+
? undefined
|
|
1656
|
+
: Object.fromEntries(
|
|
1657
|
+
selections.map(({ pass, specializationKey }) => [pass, specializationKey]),
|
|
1658
|
+
);
|
|
1620
1659
|
}
|
|
1621
1660
|
|
|
1622
1661
|
export function runtimeMaterialShaderIdForMaterial(
|
|
@@ -2712,9 +2751,10 @@ export interface DirectionalCsmResult {
|
|
|
2712
2751
|
* the caller leaves lightViewProj undefined rather than emitting zero matrices
|
|
2713
2752
|
* that the WGSL reader would sample as NaN → "fully lit").
|
|
2714
2753
|
*
|
|
2715
|
-
*
|
|
2716
|
-
*
|
|
2717
|
-
*
|
|
2754
|
+
* Keeps the prior PSSM lambda, toward-light Z reach RC-2 fix, and clip-space
|
|
2755
|
+
* matrix / tile-placement-in-shader split. The XY fit is additionally snapped
|
|
2756
|
+
* to the cascade's shadow-texel grid so camera motion cannot continuously
|
|
2757
|
+
* shimmer a receiver across raster texel boundaries.
|
|
2718
2758
|
*/
|
|
2719
2759
|
export function computeDirectionalCsm(
|
|
2720
2760
|
direction: Vec3,
|
|
@@ -2820,6 +2860,29 @@ export function computeDirectionalCsm(
|
|
|
2820
2860
|
if ((ls[2] ?? 0) < minZ) minZ = ls[2] ?? 0;
|
|
2821
2861
|
if ((ls[2] ?? 0) > maxZ) maxZ = ls[2] ?? 0;
|
|
2822
2862
|
}
|
|
2863
|
+
// Stabilize the orthographic receiver grid against sub-texel camera
|
|
2864
|
+
// motion. The raster pass writes each cascade into a square `mapSize`
|
|
2865
|
+
// viewport, so the light-space texel pitch is the fitted extent divided
|
|
2866
|
+
// by that viewport size. Snap the fitted center to that grid and retain a
|
|
2867
|
+
// half-texel guard on each side; caster and receiver projections then move
|
|
2868
|
+
// together in whole shadow texels without clipping a frustum corner when
|
|
2869
|
+
// the rounded center shifts by half a texel.
|
|
2870
|
+
const texelX = (maxX - minX) / config.mapSize;
|
|
2871
|
+
const texelY = (maxY - minY) / config.mapSize;
|
|
2872
|
+
if (Number.isFinite(texelX) && texelX > 0) {
|
|
2873
|
+
const centerX = (minX + maxX) * 0.5;
|
|
2874
|
+
const snappedCenterX = Math.round(centerX / texelX) * texelX;
|
|
2875
|
+
const halfWidth = (maxX - minX) * 0.5 + texelX * 0.5;
|
|
2876
|
+
minX = snappedCenterX - halfWidth;
|
|
2877
|
+
maxX = snappedCenterX + halfWidth;
|
|
2878
|
+
}
|
|
2879
|
+
if (Number.isFinite(texelY) && texelY > 0) {
|
|
2880
|
+
const centerY = (minY + maxY) * 0.5;
|
|
2881
|
+
const snappedCenterY = Math.round(centerY / texelY) * texelY;
|
|
2882
|
+
const halfHeight = (maxY - minY) * 0.5 + texelY * 0.5;
|
|
2883
|
+
minY = snappedCenterY - halfHeight;
|
|
2884
|
+
maxY = snappedCenterY + halfHeight;
|
|
2885
|
+
}
|
|
2823
2886
|
// Orthographic projection from light-space AABB. RC-2: whole-frustum
|
|
2824
2887
|
// toward-light extreme for the near bound; per-cascade far (minZ) + X/Y.
|
|
2825
2888
|
const nearZ = Math.max(maxZ, lightSpaceMaxZFull);
|
|
@@ -2924,15 +2987,26 @@ function collectCameraSnapshots(world: World): CameraSnapshot[] {
|
|
|
2924
2987
|
const cameraQuery = world
|
|
2925
2988
|
.query({
|
|
2926
2989
|
read: [Camera],
|
|
2927
|
-
optional: [MotionBlur, DynamicResolution, ScreenSpaceReflection],
|
|
2990
|
+
optional: [MotionBlur, DynamicResolution, ScreenSpaceReflection, DepthOfField],
|
|
2928
2991
|
with: [Transform, GlobalTransform],
|
|
2929
2992
|
})
|
|
2930
2993
|
.unwrap();
|
|
2931
2994
|
for (const row of cameraQuery) {
|
|
2932
2995
|
const cam = row.get(Camera);
|
|
2933
2996
|
const motionBlur = row.get(MotionBlur);
|
|
2997
|
+
const depthOfField = row.get(DepthOfField);
|
|
2934
2998
|
const dynamicResolution = row.get(DynamicResolution);
|
|
2935
2999
|
const screenSpaceReflection = row.get(ScreenSpaceReflection);
|
|
3000
|
+
const projection = cameraProjectionFromF32(cam.projection);
|
|
3001
|
+
const depthOfFieldResult = resolveDepthOfFieldParams(depthOfField, {
|
|
3002
|
+
projection,
|
|
3003
|
+
fov: cam.fov,
|
|
3004
|
+
near: cam.near,
|
|
3005
|
+
far: cam.far,
|
|
3006
|
+
});
|
|
3007
|
+
const depthOfFieldError = depthOfFieldResult.ok
|
|
3008
|
+
? undefined
|
|
3009
|
+
: depthOfFieldRequestFailure(depthOfFieldResult.error);
|
|
2936
3010
|
const entity = row.entity;
|
|
2937
3011
|
const view = worldInternal._getArrayView(entity, GlobalTransform, 'world');
|
|
2938
3012
|
if (view === undefined) continue;
|
|
@@ -2967,7 +3041,7 @@ function collectCameraSnapshots(world: World): CameraSnapshot[] {
|
|
|
2967
3041
|
aspect: cam.aspect,
|
|
2968
3042
|
near: cam.near,
|
|
2969
3043
|
far: cam.far,
|
|
2970
|
-
projection
|
|
3044
|
+
projection,
|
|
2971
3045
|
orthoLeft: cam.left,
|
|
2972
3046
|
orthoRight: cam.right,
|
|
2973
3047
|
orthoBottom: cam.bottom,
|
|
@@ -2990,6 +3064,10 @@ function collectCameraSnapshots(world: World): CameraSnapshot[] {
|
|
|
2990
3064
|
cam.clearColor[2] ?? 0,
|
|
2991
3065
|
cam.clearColor[3] ?? 1,
|
|
2992
3066
|
],
|
|
3067
|
+
...(depthOfFieldResult.ok && depthOfFieldResult.value !== undefined
|
|
3068
|
+
? { depthOfField: depthOfFieldResult.value }
|
|
3069
|
+
: {}),
|
|
3070
|
+
...(depthOfFieldError === undefined ? {} : { depthOfFieldError }),
|
|
2993
3071
|
...(motionBlur === undefined
|
|
2994
3072
|
? {}
|
|
2995
3073
|
: {
|
package/src/render-system.ts
CHANGED
|
@@ -46,6 +46,8 @@ import {
|
|
|
46
46
|
type RenderError,
|
|
47
47
|
TransmissionCapabilityMissingError,
|
|
48
48
|
} from './errors/render';
|
|
49
|
+
import { projectDepthOfFieldInspection } from './features/depth-of-field/depth-of-field-inspection';
|
|
50
|
+
import { resolveDepthOfFieldFrameParams } from './features/depth-of-field/depth-of-field-params';
|
|
49
51
|
import {
|
|
50
52
|
type RenderFeaturePreparedGraphicsResolverInput,
|
|
51
53
|
runRenderFeatureFrame,
|
|
@@ -68,6 +70,7 @@ import {
|
|
|
68
70
|
} from './fullscreen-post-process-pass';
|
|
69
71
|
import type {
|
|
70
72
|
BloomInspection,
|
|
73
|
+
DepthOfFieldInspection,
|
|
71
74
|
DirectionalShadowInspection,
|
|
72
75
|
GpuDrivenProductionInspection,
|
|
73
76
|
LodOcclusionInspection,
|
|
@@ -215,6 +218,7 @@ import {
|
|
|
215
218
|
type RenderFeatureGraphCandidate,
|
|
216
219
|
recordRenderFeatureCandidate,
|
|
217
220
|
resetRenderFeatureGraphState,
|
|
221
|
+
settleDepthOfFieldGraphCandidate,
|
|
218
222
|
settleVolumetricFogGraphCandidate,
|
|
219
223
|
} from './record/typed-frame-graph';
|
|
220
224
|
import {
|
|
@@ -576,6 +580,7 @@ export interface RenderSystem {
|
|
|
576
580
|
readonly temporal: TemporalInspection;
|
|
577
581
|
readonly bloom: BloomInspection;
|
|
578
582
|
readonly motionBlurInspection: MotionBlurInspection | undefined;
|
|
583
|
+
readonly depthOfFieldInspection: DepthOfFieldInspection | undefined;
|
|
579
584
|
readonly temporalTargetInspection: TemporalTargetInspection | undefined;
|
|
580
585
|
readonly lodOcclusionInspection: LodOcclusionInspection | undefined;
|
|
581
586
|
/** Bounded GPU-driven counters without materializing the persistent scene table. */
|
|
@@ -1159,6 +1164,14 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
|
|
|
1159
1164
|
compiledFrameGraph: null,
|
|
1160
1165
|
compiledFrameGraphTopologyKey: null,
|
|
1161
1166
|
compiledFrameGraphGeneration: 0,
|
|
1167
|
+
depthOfFieldPreviousGraph: null,
|
|
1168
|
+
depthOfFieldPreviousGraphKey: null,
|
|
1169
|
+
depthOfFieldPreviousPerFrameGraph: undefined,
|
|
1170
|
+
depthOfFieldPreviousGraphGeneration: 0,
|
|
1171
|
+
depthOfFieldPreviousStandardLightingGraphSignature: '',
|
|
1172
|
+
depthOfFieldCandidateGraph: null,
|
|
1173
|
+
depthOfFieldAccepted: undefined,
|
|
1174
|
+
depthOfFieldLastSubmitFailed: false,
|
|
1162
1175
|
volumetricFogPreviousGraph: null,
|
|
1163
1176
|
volumetricFogPreviousGraphKey: null,
|
|
1164
1177
|
volumetricFogCandidateGraph: null,
|
|
@@ -2280,6 +2293,26 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
|
|
|
2280
2293
|
...(unavailable ? { lastFailure: 'scene-data-unavailable' as const } : {}),
|
|
2281
2294
|
};
|
|
2282
2295
|
},
|
|
2296
|
+
get depthOfFieldInspection(): DepthOfFieldInspection | undefined {
|
|
2297
|
+
const camera = latestCamera;
|
|
2298
|
+
if (camera === undefined) return undefined;
|
|
2299
|
+
const graph = frameState.compiledFrameGraph?.inspect();
|
|
2300
|
+
return projectDepthOfFieldInspection({
|
|
2301
|
+
...(camera.depthOfField === undefined ? {} : { params: camera.depthOfField }),
|
|
2302
|
+
...(camera.depthOfFieldError === undefined ? {} : { error: camera.depthOfFieldError }),
|
|
2303
|
+
...(graph === undefined ? {} : { graph }),
|
|
2304
|
+
// The explicit property keeps the inspector on the submit-bound
|
|
2305
|
+
// transaction path even before the first accepted frame. A missing
|
|
2306
|
+
// property remains supported for direct structural callers/tests.
|
|
2307
|
+
accepted: frameState.depthOfFieldAccepted,
|
|
2308
|
+
...(frameState.depthOfFieldLastSubmitFailed === true ? { lastSubmitFailed: true } : {}),
|
|
2309
|
+
// Inspection can be queried while the renderer is being assembled,
|
|
2310
|
+
// before a device scope has been published. Keep that observation
|
|
2311
|
+
// bounded instead of turning a not-yet-ready renderer into a getter
|
|
2312
|
+
// exception.
|
|
2313
|
+
deviceGeneration: internals.deviceScope?.generation ?? 0,
|
|
2314
|
+
});
|
|
2315
|
+
},
|
|
2283
2316
|
get temporalTargetInspection(): TemporalTargetInspection | undefined {
|
|
2284
2317
|
const graph = frameState.compiledFrameGraph?.inspect();
|
|
2285
2318
|
if (graph === undefined) return undefined;
|
|
@@ -2888,6 +2921,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
|
|
|
2888
2921
|
const materialWorld = preparedWorlds[renderable.worldId];
|
|
2889
2922
|
return projectMeshMaterialBindingObservation({
|
|
2890
2923
|
worldId: renderable.worldId,
|
|
2924
|
+
...(materialWorld === undefined ? {} : { worldIdentity: materialWorld.identity }),
|
|
2891
2925
|
entityKey: renderable.entityKey,
|
|
2892
2926
|
bindings: renderable.materials.map((material, slotIndex) => ({
|
|
2893
2927
|
handle: material.materialHandle ?? 0,
|
|
@@ -3283,6 +3317,29 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
|
|
|
3283
3317
|
internals.errorRegistry.fire(temporalCommit.error);
|
|
3284
3318
|
}
|
|
3285
3319
|
frameState.lastSuccessfulCameraAntialias = cameras[0]?.antialias;
|
|
3320
|
+
const submittedCamera = cameras[0];
|
|
3321
|
+
const submittedDepthOfField =
|
|
3322
|
+
submittedCamera === undefined
|
|
3323
|
+
? undefined
|
|
3324
|
+
: resolveDepthOfFieldFrameParams(
|
|
3325
|
+
submittedCamera.depthOfField,
|
|
3326
|
+
submittedCamera.depthOfFieldError,
|
|
3327
|
+
frameState.depthOfFieldAccepted?.params,
|
|
3328
|
+
);
|
|
3329
|
+
const submittedDepthOfFieldGraph = frameState.compiledFrameGraph?.inspect();
|
|
3330
|
+
if (submittedDepthOfField !== undefined && submittedDepthOfFieldGraph !== undefined) {
|
|
3331
|
+
frameState.depthOfFieldAccepted = {
|
|
3332
|
+
params: submittedDepthOfField,
|
|
3333
|
+
graph: submittedDepthOfFieldGraph,
|
|
3334
|
+
deviceGeneration: internals.deviceScope.generation,
|
|
3335
|
+
};
|
|
3336
|
+
frameState.depthOfFieldLastSubmitFailed = false;
|
|
3337
|
+
} else if (submittedDepthOfField === undefined) {
|
|
3338
|
+
frameState.depthOfFieldAccepted = undefined;
|
|
3339
|
+
frameState.depthOfFieldLastSubmitFailed = false;
|
|
3340
|
+
} else {
|
|
3341
|
+
frameState.depthOfFieldLastSubmitFailed = true;
|
|
3342
|
+
}
|
|
3286
3343
|
internals.getPipelineState()?.perPassResources.commitBloomResources?.();
|
|
3287
3344
|
const nextBloom = cameras[0]?.bloom ?? 'off';
|
|
3288
3345
|
if (frameState.lastSuccessfulBloom === 'on' && nextBloom !== 'on') {
|
|
@@ -3294,6 +3351,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
|
|
|
3294
3351
|
}
|
|
3295
3352
|
frameState.lastSuccessfulBloom = nextBloom;
|
|
3296
3353
|
} else {
|
|
3354
|
+
if (cameras[0]?.depthOfField !== undefined) {
|
|
3355
|
+
frameState.depthOfFieldLastSubmitFailed = true;
|
|
3356
|
+
}
|
|
3297
3357
|
internals.getPipelineState()?.perPassResources.discardBloomResources?.();
|
|
3298
3358
|
}
|
|
3299
3359
|
if (!submitted && frameState.environmentGeneration !== undefined) {
|
|
@@ -3638,6 +3698,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
|
|
|
3638
3698
|
frameState.volumetricFogHistorySlot = null;
|
|
3639
3699
|
frameState.volumetricFogHistorySignature = null;
|
|
3640
3700
|
frameState.hdrpClusterMembership = null;
|
|
3701
|
+
settleDepthOfFieldGraphCandidate(frameState, false);
|
|
3641
3702
|
const compiled = frameState.compiledFrameGraph;
|
|
3642
3703
|
frameState.compiledFrameGraph = null;
|
|
3643
3704
|
frameState.compiledFrameGraphTopologyKey = null;
|
|
@@ -3645,6 +3706,14 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
|
|
|
3645
3706
|
frameState.standardLightingGraphSignature = '';
|
|
3646
3707
|
frameState.standardLightingInspection = undefined;
|
|
3647
3708
|
frameState.pointShadowInspection = undefined;
|
|
3709
|
+
frameState.depthOfFieldPreviousGraph = null;
|
|
3710
|
+
frameState.depthOfFieldPreviousGraphKey = null;
|
|
3711
|
+
frameState.depthOfFieldPreviousPerFrameGraph = undefined;
|
|
3712
|
+
frameState.depthOfFieldPreviousGraphGeneration = 0;
|
|
3713
|
+
frameState.depthOfFieldPreviousStandardLightingGraphSignature = '';
|
|
3714
|
+
frameState.depthOfFieldCandidateGraph = null;
|
|
3715
|
+
frameState.depthOfFieldAccepted = undefined;
|
|
3716
|
+
frameState.depthOfFieldLastSubmitFailed = false;
|
|
3648
3717
|
if (compiled !== null) compiled.retire().catch(() => undefined);
|
|
3649
3718
|
for (const retired of frameState.retiredCompiledFrameGraphs) {
|
|
3650
3719
|
retired.retire().catch(() => undefined);
|
|
@@ -3746,12 +3815,19 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
|
|
|
3746
3815
|
// handles cannot be used on the fresh device and the next draw must
|
|
3747
3816
|
// lazily build a new graph from the preserved ECS / asset POD caches.
|
|
3748
3817
|
settleVolumetricFogGraphCandidate(frameState, false);
|
|
3818
|
+
settleDepthOfFieldGraphCandidate(frameState, false);
|
|
3749
3819
|
frameState.compiledFrameGraph?.retire().catch(() => undefined);
|
|
3750
3820
|
frameState.compiledFrameGraph = null;
|
|
3751
3821
|
frameState.compiledFrameGraphTopologyKey = null;
|
|
3752
3822
|
frameState.volumetricFogPreviousGraph = null;
|
|
3753
3823
|
frameState.volumetricFogPreviousGraphKey = null;
|
|
3754
3824
|
frameState.volumetricFogCandidateGraph = null;
|
|
3825
|
+
frameState.depthOfFieldPreviousGraph = null;
|
|
3826
|
+
frameState.depthOfFieldPreviousGraphKey = null;
|
|
3827
|
+
frameState.depthOfFieldPreviousPerFrameGraph = undefined;
|
|
3828
|
+
frameState.depthOfFieldPreviousGraphGeneration = 0;
|
|
3829
|
+
frameState.depthOfFieldPreviousStandardLightingGraphSignature = '';
|
|
3830
|
+
frameState.depthOfFieldCandidateGraph = null;
|
|
3755
3831
|
frameState.volumetricFogAccepted = undefined;
|
|
3756
3832
|
frameState.volumetricFogAcceptedContext = undefined;
|
|
3757
3833
|
frameState.volumetricFogHistoryGraph = null;
|
|
@@ -3772,6 +3848,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
|
|
|
3772
3848
|
frameState.standardLightingGraphSignature = '';
|
|
3773
3849
|
frameState.standardLightingInspection = undefined;
|
|
3774
3850
|
frameState.pointShadowInspection = undefined;
|
|
3851
|
+
frameState.depthOfFieldAccepted = undefined;
|
|
3852
|
+
frameState.depthOfFieldLastSubmitFailed = false;
|
|
3775
3853
|
frameState.directionalShadowCache = null;
|
|
3776
3854
|
frameState.directionalShadowCacheRecorded = false;
|
|
3777
3855
|
frameState.currentFrameObservationSource = undefined;
|
|
@@ -84,6 +84,8 @@ function resolvedDepthView(
|
|
|
84
84
|
if (!texture.ok) throw texture.error;
|
|
85
85
|
const view = frame.runtime.device.createTextureView(texture.value, {
|
|
86
86
|
label: 'typed-ssao-depth-only-view',
|
|
87
|
+
// Multisampled WebGPU textures still use a `2d` view dimension; the
|
|
88
|
+
// sample count is carried by the source texture and WGSL type.
|
|
87
89
|
dimension: '2d',
|
|
88
90
|
aspect: 'depth-only',
|
|
89
91
|
baseMipLevel: 0,
|
|
@@ -892,6 +894,7 @@ export function addTypedFullscreenPass(
|
|
|
892
894
|
...(options.fragmentEntryPoint === undefined
|
|
893
895
|
? {}
|
|
894
896
|
: { fragmentEntryPoint: options.fragmentEntryPoint }),
|
|
897
|
+
...(options.depth === undefined ? {} : { msaaActive: options.depth.sampleCount === 4 }),
|
|
895
898
|
});
|
|
896
899
|
},
|
|
897
900
|
});
|