@forgeax/engine-render 0.1.19 → 0.1.20
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/dist/.tsbuildinfo +1 -1
- package/dist/assembly/factory.d.ts +16 -5
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/{chunk-RJRUS55L.mjs → chunk-4XINDLAY.mjs} +267 -186
- package/dist/chunk-4XINDLAY.mjs.map +1 -0
- package/dist/{chunk-YTQ5C7YQ.mjs → chunk-CDTWTHZR.mjs} +176 -8
- package/dist/chunk-CDTWTHZR.mjs.map +1 -0
- package/dist/{chunk-IPXCABEP.mjs → chunk-M2MP4ZOU.mjs} +2 -2
- package/dist/chunk-M2MP4ZOU.mjs.map +1 -0
- package/dist/{chunk-LOSO4CTV.mjs → chunk-TBQJOK2L.mjs} +67 -3
- package/dist/chunk-TBQJOK2L.mjs.map +1 -0
- package/dist/components/light-helpers.d.ts.map +1 -1
- package/dist/components/spot-light.d.ts +12 -0
- package/dist/components/spot-light.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +2281 -164
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/gpu-residency.d.ts +19 -2
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/errors/render.d.ts +69 -1
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/gpu-usage.d.ts +2 -0
- package/dist/gpu-usage.d.ts.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +3 -3
- package/dist/internal.mjs +1 -1
- package/dist/light-buffer-layout.d.ts +20 -12
- package/dist/light-buffer-layout.d.ts.map +1 -1
- package/dist/pbr-pipeline.d.ts +6 -3
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
- package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
- package/dist/pipeline/standard-profile.d.ts +13 -0
- package/dist/pipeline/standard-profile.d.ts.map +1 -1
- package/dist/record/frame-lighting.d.ts +2 -0
- package/dist/record/frame-lighting.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +37 -1
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame-targets.d.ts.map +1 -1
- package/dist/record/frame.d.ts +3 -2
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/gpu-timing.d.ts +32 -0
- package/dist/record/gpu-timing.d.ts.map +1 -0
- package/dist/record/main-pass-material.d.ts +1 -1
- package/dist/record/main-pass-material.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +5 -1
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +11 -3
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/render-contract.d.ts +66 -1
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-data.d.ts +10 -6
- package/dist/render-data.d.ts.map +1 -1
- package/dist/render-pipeline.d.ts +36 -0
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +55 -3
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +7 -1
- package/dist/render-system.d.ts.map +1 -1
- package/dist/renderer-inspect.d.ts +29 -0
- package/dist/renderer-inspect.d.ts.map +1 -0
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/temporal/frame.d.ts +44 -0
- package/dist/temporal/frame.d.ts.map +1 -0
- package/dist/temporal/index.mjs +3 -3
- package/dist/typed-render-graph-primitives.d.ts +6 -1
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/dist/typed-shadow-atlas.d.ts +10 -0
- package/dist/typed-shadow-atlas.d.ts.map +1 -0
- package/dist/typed-shadow-passes.d.ts.map +1 -1
- package/dist/volume/capability.d.ts +57 -0
- package/dist/volume/capability.d.ts.map +1 -0
- package/dist/volume/component.d.ts +61 -0
- package/dist/volume/component.d.ts.map +1 -0
- package/dist/volume/extract.d.ts +11 -0
- package/dist/volume/extract.d.ts.map +1 -0
- package/dist/volume/inspection.d.ts +38 -0
- package/dist/volume/inspection.d.ts.map +1 -0
- package/dist/volume/passes.d.ts +38 -0
- package/dist/volume/passes.d.ts.map +1 -0
- package/dist/volume/recovery.d.ts +38 -0
- package/dist/volume/recovery.d.ts.map +1 -0
- package/dist/volume/resources.d.ts +57 -0
- package/dist/volume/resources.d.ts.map +1 -0
- package/dist/volume/temporal.d.ts +35 -0
- package/dist/volume/temporal.d.ts.map +1 -0
- package/package.json +20 -20
- package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +14 -14
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
- package/src/__tests__/render-error-code-owner.test-d.ts +5 -0
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +28 -0
- package/src/__tests__/renderer-frame-transaction.integration.test.ts +1 -1
- package/src/__tests__/skinned-shadow-caster.test.ts +21 -0
- package/src/__tests__/standard-profile-oracle.unit.test.ts +7 -5
- package/src/__tests__/volume-optics-oracle.unit.test.ts +85 -0
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +655 -0
- package/src/__tests__/volumetric-fog-component.unit.test.ts +71 -0
- package/src/__tests__/volumetric-fog-composite-stage-readback.dawn.test.ts +376 -0
- package/src/__tests__/volumetric-fog-errors.unit.test.ts +51 -0
- package/src/__tests__/volumetric-fog-inspection.unit.test.ts +77 -0
- package/src/__tests__/volumetric-fog-light-capability.unit.test.ts +19 -0
- package/src/__tests__/volumetric-fog-light-consumers.integration.test.ts +18 -0
- package/src/__tests__/volumetric-fog-light-errors.unit.test.ts +17 -0
- package/src/__tests__/volumetric-fog-light-topology.unit.test.ts +21 -0
- package/src/__tests__/volumetric-fog-light-transaction.integration.test.ts +53 -0
- package/src/__tests__/volumetric-fog-m9-watcher.integration.test.ts +27 -0
- package/src/__tests__/volumetric-fog-point-light-owner.integration.test.ts +44 -0
- package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +49 -0
- package/src/__tests__/volumetric-fog-point-spot-contract.unit.test.ts +28 -0
- package/src/__tests__/volumetric-fog-point-spot-lkg.integration.test.ts +65 -0
- package/src/__tests__/volumetric-fog-point-spot-selection.integration.test.ts +49 -0
- package/src/__tests__/volumetric-fog-point-spot-temporal.unit.test.ts +55 -0
- package/src/__tests__/volumetric-fog-projector-contract.unit.test.ts +22 -0
- package/src/__tests__/volumetric-fog-projector-inspection.unit.test.ts +12 -0
- package/src/__tests__/volumetric-fog-projector-surface-volume.integration.test.ts +11 -0
- package/src/__tests__/volumetric-fog-projector-transaction.integration.test.ts +26 -0
- package/src/__tests__/volumetric-fog-recovery.integration.test.ts +47 -0
- package/src/__tests__/volumetric-fog-selected-light-resolution.integration.test.ts +63 -0
- package/src/__tests__/volumetric-fog-spot-closure.integration.test.ts +26 -0
- package/src/__tests__/volumetric-fog-spot-contract.test-d.ts +21 -0
- package/src/__tests__/volumetric-fog-spot-contract.unit.test.ts +27 -0
- package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +14 -0
- package/src/__tests__/volumetric-fog-spot-optics.integration.test.ts +29 -0
- package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +550 -0
- package/src/__tests__/volumetric-fog-temporal-selected-light.unit.test.ts +33 -0
- package/src/__tests__/volumetric-fog-temporal.unit.test.ts +45 -0
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +237 -0
- package/src/__tests__/volumetric-fog-transparent.integration.test.ts +27 -0
- package/src/assembly/factory.ts +271 -15
- package/src/components/light-helpers.ts +4 -0
- package/src/components/spot-light.ts +22 -3
- package/src/device/gpu-residency.ts +168 -58
- package/src/errors/render.ts +119 -1
- package/src/gpu-usage.ts +2 -0
- package/src/index.ts +38 -1
- package/src/light-buffer-layout.ts +59 -26
- package/src/pbr-pipeline.ts +33 -10
- package/src/pipeline/standard-forward-lane.ts +20 -0
- package/src/pipeline/standard-pipeline.ts +20 -0
- package/src/pipeline/standard-profile.ts +23 -0
- package/src/record/frame-lighting.ts +103 -89
- package/src/record/frame-snapshot.ts +41 -1
- package/src/record/frame-targets.ts +6 -2
- package/src/record/frame.ts +908 -5
- package/src/record/gpu-timing.ts +224 -0
- package/src/record/main-pass-material.ts +6 -2
- package/src/record/render-context.ts +5 -0
- package/src/record/shadow-pass.ts +87 -38
- package/src/record/typed-frame-graph.ts +143 -8
- package/src/render-contract.ts +72 -0
- package/src/render-data.ts +50 -22
- package/src/render-pipeline.ts +44 -0
- package/src/render-system-extract.ts +329 -3
- package/src/render-system.ts +79 -0
- package/src/renderer-inspect.ts +66 -0
- package/src/scene/render-scene.ts +2 -0
- package/src/temporal/frame.ts +128 -0
- package/src/transmission/__tests__/standard-transmission.dawn.test.ts +2 -3
- package/src/typed-render-graph-primitives.ts +32 -2
- package/src/typed-shadow-atlas.ts +17 -0
- package/src/typed-shadow-passes.ts +6 -5
- package/src/volume/capability.ts +173 -0
- package/src/volume/component.ts +110 -0
- package/src/volume/extract.ts +21 -0
- package/src/volume/inspection.ts +78 -0
- package/src/volume/passes.ts +680 -0
- package/src/volume/recovery.ts +86 -0
- package/src/volume/resources.ts +236 -0
- package/src/volume/temporal.ts +89 -0
- package/dist/chunk-IPXCABEP.mjs.map +0 -1
- package/dist/chunk-LOSO4CTV.mjs.map +0 -1
- package/dist/chunk-RJRUS55L.mjs.map +0 -1
- package/dist/chunk-YTQ5C7YQ.mjs.map +0 -1
package/src/assembly/factory.ts
CHANGED
|
@@ -59,6 +59,7 @@ import type {
|
|
|
59
59
|
import { err, ok, RhiError, validateDrawArgs } from '@forgeax/engine-rhi';
|
|
60
60
|
import { registerPropagateTransforms } from '@forgeax/engine-scene';
|
|
61
61
|
import {
|
|
62
|
+
DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
|
|
62
63
|
findVariantByKey,
|
|
63
64
|
type MaterialShaderEntry,
|
|
64
65
|
type MaterialShaderManifestEntry,
|
|
@@ -134,6 +135,7 @@ import {
|
|
|
134
135
|
SPOT_LIGHT_STD430_BYTES,
|
|
135
136
|
} from '../light-buffer-layout';
|
|
136
137
|
import {
|
|
138
|
+
buildPbrMaterialUserRegionEntries,
|
|
137
139
|
buildPbrPipelineLayouts,
|
|
138
140
|
buildPbrSkinLayouts,
|
|
139
141
|
isSkinnedShadowCasterVariant,
|
|
@@ -163,6 +165,7 @@ import {
|
|
|
163
165
|
} from '../pipeline-spec';
|
|
164
166
|
import { POINTS_LINES_MATERIAL_SHADER_ID } from '../points-lines/record';
|
|
165
167
|
import type { FrameObservationOptions } from '../record/frame';
|
|
168
|
+
import { GpuTimingCapture, type VolumeTimingObservation } from '../record/gpu-timing';
|
|
166
169
|
import type { BloomPersistentBundle } from '../record/render-context';
|
|
167
170
|
import { SHADOW_CASTER_BUFFER_SIZE } from '../record/shadow-pass';
|
|
168
171
|
import { POINTS_LINES_VIEW_BUFFER_SIZE, VIEW_UNIFORM_BUFFER_SIZE } from '../record/view-ubo';
|
|
@@ -185,6 +188,7 @@ import type {
|
|
|
185
188
|
RenderProfile,
|
|
186
189
|
RenderResult,
|
|
187
190
|
RenderWorldLease,
|
|
191
|
+
VolumetricFogShaderSources,
|
|
188
192
|
} from '../render-contract';
|
|
189
193
|
import { STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from '../render-contract';
|
|
190
194
|
import {
|
|
@@ -499,12 +503,15 @@ export function selectHdrpPbrPrewarmVariants(
|
|
|
499
503
|
manifestEntry: MaterialShaderManifestEntry | undefined,
|
|
500
504
|
storageBufferCapable: boolean,
|
|
501
505
|
transmissionCapable: boolean,
|
|
506
|
+
projectorAvailable: boolean = true,
|
|
502
507
|
): readonly MaterialShaderManifestEntry['variants'][number][] {
|
|
503
508
|
return (
|
|
504
509
|
manifestEntry?.variants.filter(
|
|
505
510
|
(variant) =>
|
|
506
511
|
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
|
|
507
512
|
variant.defines.CLUSTER_FORWARD_AVAILABLE === true &&
|
|
513
|
+
(!('PROJECTOR_AVAILABLE' in variant.defines) ||
|
|
514
|
+
variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
|
|
508
515
|
(transmissionCapable || variant.defines.TRANSMISSION_AVAILABLE !== true),
|
|
509
516
|
) ?? []
|
|
510
517
|
);
|
|
@@ -518,6 +525,7 @@ export function selectHdrpPbrPrewarmVariants(
|
|
|
518
525
|
export function selectStandardPbrTransmissionPrewarmVariants(
|
|
519
526
|
manifestEntry: MaterialShaderManifestEntry | undefined,
|
|
520
527
|
storageBufferCapable: boolean,
|
|
528
|
+
projectorAvailable: boolean = true,
|
|
521
529
|
): readonly MaterialShaderManifestEntry['variants'][number][] {
|
|
522
530
|
if (manifestEntry === undefined) {
|
|
523
531
|
throw new Error('Standard material shader manifest row is missing');
|
|
@@ -529,6 +537,8 @@ export function selectStandardPbrTransmissionPrewarmVariants(
|
|
|
529
537
|
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
|
|
530
538
|
variant.defines.CLUSTER_FORWARD_AVAILABLE === false &&
|
|
531
539
|
variant.defines.VERTEX_COLOR_AVAILABLE === false &&
|
|
540
|
+
(!('PROJECTOR_AVAILABLE' in variant.defines) ||
|
|
541
|
+
variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
|
|
532
542
|
variant.defines.TRANSMISSION_AVAILABLE === transmissionAvailable,
|
|
533
543
|
);
|
|
534
544
|
if (declared === undefined) {
|
|
@@ -556,6 +566,7 @@ export function selectNoColorPbrVariant(
|
|
|
556
566
|
manifestEntry: MaterialShaderManifestEntry | undefined,
|
|
557
567
|
storageBufferCapable: boolean,
|
|
558
568
|
variantSet: string | undefined,
|
|
569
|
+
projectorAvailable: boolean = true,
|
|
559
570
|
): MaterialShaderManifestEntry['variants'][number] | undefined {
|
|
560
571
|
const hdrpRequested =
|
|
561
572
|
variantSet === '' ||
|
|
@@ -565,6 +576,8 @@ export function selectNoColorPbrVariant(
|
|
|
565
576
|
(variant) =>
|
|
566
577
|
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
|
|
567
578
|
variant.defines.VERTEX_COLOR_AVAILABLE === false &&
|
|
579
|
+
(!('PROJECTOR_AVAILABLE' in variant.defines) ||
|
|
580
|
+
variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
|
|
568
581
|
(!('CLUSTER_FORWARD_AVAILABLE' in variant.defines) ||
|
|
569
582
|
variant.defines.CLUSTER_FORWARD_AVAILABLE === hdrpRequested),
|
|
570
583
|
);
|
|
@@ -623,6 +636,46 @@ export function resolveMaterialShaderVertexInputContract(
|
|
|
623
636
|
: 'none';
|
|
624
637
|
}
|
|
625
638
|
|
|
639
|
+
/**
|
|
640
|
+
* Decide whether a custom material's user-region can use the shared PBR
|
|
641
|
+
* bind-group layout. Resource names are not part of a WebGPU layout contract:
|
|
642
|
+
* the only safe reuse test is that every derived binding in the custom schema
|
|
643
|
+
* is an identical prefix of the canonical user-region descriptor, including
|
|
644
|
+
* binding kind and its full shape (sample type, view dimension, sampler mode,
|
|
645
|
+
* dynamic-offset flags, and visibility). A texture3d/array or comparison /
|
|
646
|
+
* storage resource must therefore take the per-shader layout path even when
|
|
647
|
+
* its field count is small.
|
|
648
|
+
*/
|
|
649
|
+
export function isSharedMaterialUserRegionCompatible(
|
|
650
|
+
paramSchema: readonly ParamSchemaEntry[],
|
|
651
|
+
): boolean {
|
|
652
|
+
const shared = buildPbrMaterialUserRegionEntries(DEFAULT_STANDARD_PBR_PARAM_SCHEMA);
|
|
653
|
+
const candidate = buildPbrMaterialUserRegionEntries(paramSchema);
|
|
654
|
+
if (candidate.length > shared.length) return false;
|
|
655
|
+
return candidate.every((entry, index) => {
|
|
656
|
+
const canonical = shared[index];
|
|
657
|
+
if (canonical === undefined) return false;
|
|
658
|
+
return (
|
|
659
|
+
JSON.stringify({
|
|
660
|
+
binding: entry.binding,
|
|
661
|
+
visibility: entry.visibility,
|
|
662
|
+
buffer: entry.buffer ?? null,
|
|
663
|
+
sampler: entry.sampler ?? null,
|
|
664
|
+
texture: entry.texture ?? null,
|
|
665
|
+
storageTexture: entry.storageTexture ?? null,
|
|
666
|
+
}) ===
|
|
667
|
+
JSON.stringify({
|
|
668
|
+
binding: canonical.binding,
|
|
669
|
+
visibility: canonical.visibility,
|
|
670
|
+
buffer: canonical.buffer ?? null,
|
|
671
|
+
sampler: canonical.sampler ?? null,
|
|
672
|
+
texture: canonical.texture ?? null,
|
|
673
|
+
storageTexture: canonical.storageTexture ?? null,
|
|
674
|
+
})
|
|
675
|
+
);
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
626
679
|
export function allowsUnlitPreparedFallback(
|
|
627
680
|
depthFormatOverride: GPUTextureFormat | null | undefined,
|
|
628
681
|
vertexLayout: string | undefined,
|
|
@@ -727,10 +780,13 @@ export function resolveMaterialShaderVariantSet(
|
|
|
727
780
|
}>,
|
|
728
781
|
backendKind: RhiDevice['caps']['backendKind'],
|
|
729
782
|
storageBuffer: boolean,
|
|
783
|
+
projectorAvailable: boolean = true,
|
|
730
784
|
): string | undefined {
|
|
731
785
|
const declaredAxes = new Set(variants.flatMap((variant) => Object.keys(variant.defines)));
|
|
732
786
|
const declaresCapabilityVariants =
|
|
733
|
-
declaredAxes.has('WEBGL2_COMPAT') ||
|
|
787
|
+
declaredAxes.has('WEBGL2_COMPAT') ||
|
|
788
|
+
declaredAxes.has('STORAGE_BUFFER_AVAILABLE') ||
|
|
789
|
+
declaredAxes.has('PROJECTOR_AVAILABLE');
|
|
734
790
|
if (!declaresCapabilityVariants) return requestedVariantSet;
|
|
735
791
|
|
|
736
792
|
const requested: Record<string, boolean> = {};
|
|
@@ -755,7 +811,9 @@ export function resolveMaterialShaderVariantSet(
|
|
|
755
811
|
? requestedVariantSet === '' || (requested[axis] ?? false)
|
|
756
812
|
: axis === 'TRANSMISSION_AVAILABLE'
|
|
757
813
|
? (requested[axis] ?? false)
|
|
758
|
-
:
|
|
814
|
+
: axis === 'PROJECTOR_AVAILABLE'
|
|
815
|
+
? projectorAvailable
|
|
816
|
+
: (requested[axis] ?? true);
|
|
759
817
|
}
|
|
760
818
|
const sorted = Object.entries(filtered).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
761
819
|
return sorted.every(([, value]) => value)
|
|
@@ -951,7 +1009,10 @@ const COMPRESSION_FEATURES: GPUFeatureName[] = [
|
|
|
951
1009
|
// 16; only request the elevated limit when the adapter can provide it.
|
|
952
1010
|
const STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES = 17;
|
|
953
1011
|
|
|
954
|
-
function deviceOptionsForAdapter(
|
|
1012
|
+
function deviceOptionsForAdapter(
|
|
1013
|
+
adapter: Pick<RhiAdapter, 'features' | 'limits'>,
|
|
1014
|
+
options?: Pick<RendererOptions, 'captureGpuTimings'>,
|
|
1015
|
+
):
|
|
955
1016
|
| {
|
|
956
1017
|
requiredFeatures?: GPUFeatureName[];
|
|
957
1018
|
requiredLimits?: GPUDeviceDescriptor['requiredLimits'];
|
|
@@ -961,6 +1022,9 @@ function deviceOptionsForAdapter(adapter: Pick<RhiAdapter, 'features' | 'limits'
|
|
|
961
1022
|
const supportsTransmission =
|
|
962
1023
|
(adapter.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
963
1024
|
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
|
|
1025
|
+
if (options?.captureGpuTimings === true && adapter.features.has('timestamp-query')) {
|
|
1026
|
+
requiredFeatures.push('timestamp-query');
|
|
1027
|
+
}
|
|
964
1028
|
return {
|
|
965
1029
|
...(requiredFeatures.length > 0 ? { requiredFeatures } : {}),
|
|
966
1030
|
...(supportsTransmission
|
|
@@ -1020,7 +1084,7 @@ async function tryCreateWebGPURenderer(
|
|
|
1020
1084
|
const adapter = adapterResult.value;
|
|
1021
1085
|
// M4 w28: filter compression features from adapter.features (AC-07). The
|
|
1022
1086
|
// same helper is used by recover() below so both paths share one profile.
|
|
1023
|
-
const deviceOpts = deviceOptionsForAdapter(adapter);
|
|
1087
|
+
const deviceOpts = deviceOptionsForAdapter(adapter, options);
|
|
1024
1088
|
const result = await adapter.requestDevice(deviceOpts);
|
|
1025
1089
|
if (!result.ok) {
|
|
1026
1090
|
return { kind: 'rhi-err', error: result.error };
|
|
@@ -1150,6 +1214,8 @@ interface WebGPURendererInternals {
|
|
|
1150
1214
|
* has wired the controller; identity stable across grow events.
|
|
1151
1215
|
*/
|
|
1152
1216
|
meshSsboState?: MeshSsboState;
|
|
1217
|
+
/** Device-bound, manifest-owned volumetric utility shader sources. */
|
|
1218
|
+
volumetricFogShaders?: VolumetricFogShaderSources | undefined;
|
|
1153
1219
|
/** Host-owned producer features; created before device-bound assembly starts. */
|
|
1154
1220
|
featureHost?: RenderFeatureHost;
|
|
1155
1221
|
/** App-owned overlay capability read by the typed graph at frame time. */
|
|
@@ -1628,6 +1694,18 @@ async function makeWebGPURenderer(
|
|
|
1628
1694
|
}
|
|
1629
1695
|
if (pipelineState === null) return null;
|
|
1630
1696
|
const paramSchema = lookup.value.paramSchema;
|
|
1697
|
+
// Reuse the shared BGL only when the complete derived resource shape is a
|
|
1698
|
+
// canonical prefix. Field names alone are not a layout contract: a
|
|
1699
|
+
// texture3d/array, comparison sampler, or storage buffer must never be
|
|
1700
|
+
// paired with the PBR texture2d/filtering descriptor by accident.
|
|
1701
|
+
if (isSharedMaterialUserRegionCompatible(paramSchema)) {
|
|
1702
|
+
perShaderMaterialLayoutCache.set(materialShaderId, {
|
|
1703
|
+
source: lookup.value.source,
|
|
1704
|
+
paramSchema,
|
|
1705
|
+
layout: null,
|
|
1706
|
+
});
|
|
1707
|
+
return null;
|
|
1708
|
+
}
|
|
1631
1709
|
const spec: PipelineSpec = {
|
|
1632
1710
|
shader: { id: materialShaderId, passKind: 'forward', variantSet: undefined },
|
|
1633
1711
|
attachments: { colorFormats: [], depthFormat: undefined, sampleCount: 1 },
|
|
@@ -1674,6 +1752,8 @@ async function makeWebGPURenderer(
|
|
|
1674
1752
|
[],
|
|
1675
1753
|
internals.device.caps.backendKind,
|
|
1676
1754
|
internals.device.caps.storageBuffer,
|
|
1755
|
+
(internals.device.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
1756
|
+
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
|
|
1677
1757
|
);
|
|
1678
1758
|
}
|
|
1679
1759
|
const variants = manifestEntry.variants;
|
|
@@ -1682,13 +1762,17 @@ async function makeWebGPURenderer(
|
|
|
1682
1762
|
byRequest = new Map();
|
|
1683
1763
|
materialShaderVariantResolutionCache.set(variants, byRequest);
|
|
1684
1764
|
}
|
|
1685
|
-
const
|
|
1765
|
+
const projectorAvailable =
|
|
1766
|
+
(internals.device.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
1767
|
+
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
|
|
1768
|
+
const cacheKey = `${requestedVariantSet ?? '\u0000'}|${internals.device.caps.backendKind}|${internals.device.caps.storageBuffer ? '1' : '0'}|${projectorAvailable ? '1' : '0'}`;
|
|
1686
1769
|
if (byRequest.has(cacheKey)) return byRequest.get(cacheKey);
|
|
1687
1770
|
const resolved = resolveMaterialShaderVariantSet(
|
|
1688
1771
|
requestedVariantSet,
|
|
1689
1772
|
variants,
|
|
1690
1773
|
internals.device.caps.backendKind,
|
|
1691
1774
|
internals.device.caps.storageBuffer,
|
|
1775
|
+
projectorAvailable,
|
|
1692
1776
|
);
|
|
1693
1777
|
byRequest.set(cacheKey, resolved);
|
|
1694
1778
|
return resolved;
|
|
@@ -1782,6 +1866,9 @@ async function makeWebGPURenderer(
|
|
|
1782
1866
|
}
|
|
1783
1867
|
temporalPostProcessesRegistered = true;
|
|
1784
1868
|
},
|
|
1869
|
+
(sources: VolumetricFogShaderSources) => {
|
|
1870
|
+
internals.volumetricFogShaders = sources;
|
|
1871
|
+
},
|
|
1785
1872
|
);
|
|
1786
1873
|
const ready: Promise<Result<void, RhiError>> = buildPipeline().then(
|
|
1787
1874
|
(state): Result<void, RhiError> => {
|
|
@@ -1827,6 +1914,8 @@ async function makeWebGPURenderer(
|
|
|
1827
1914
|
pbrManifestEntry,
|
|
1828
1915
|
internals.device.caps.storageBuffer,
|
|
1829
1916
|
variantSet,
|
|
1917
|
+
(internals.device.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
1918
|
+
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
|
|
1830
1919
|
);
|
|
1831
1920
|
if (noColorVariant !== undefined && noColorVariant.composedWgsl !== standardEntry.wgsl) {
|
|
1832
1921
|
return { ...standardEntry, wgsl: noColorVariant.composedWgsl };
|
|
@@ -2705,6 +2794,9 @@ async function makeWebGPURenderer(
|
|
|
2705
2794
|
get meshSsboState() {
|
|
2706
2795
|
return internals.meshSsboState;
|
|
2707
2796
|
},
|
|
2797
|
+
get volumetricFogShaders() {
|
|
2798
|
+
return internals.volumetricFogShaders;
|
|
2799
|
+
},
|
|
2708
2800
|
});
|
|
2709
2801
|
if (
|
|
2710
2802
|
internals.device.caps.backendKind === 'null' &&
|
|
@@ -2730,7 +2822,8 @@ async function makeWebGPURenderer(
|
|
|
2730
2822
|
const transformReleases = new Map<World, () => void>();
|
|
2731
2823
|
const attachmentOwner = {};
|
|
2732
2824
|
let frameId = 0;
|
|
2733
|
-
|
|
2825
|
+
const issuedReceipts = new WeakSet<FrameReceipt>();
|
|
2826
|
+
const receiptTimings = new WeakMap<FrameReceipt, Promise<VolumeTimingObservation>>();
|
|
2734
2827
|
let surfaceReleased = false;
|
|
2735
2828
|
|
|
2736
2829
|
const renderer: RendererAssemblyImplementation = {
|
|
@@ -2794,6 +2887,7 @@ async function makeWebGPURenderer(
|
|
|
2794
2887
|
? {}
|
|
2795
2888
|
: { presentationProof: internals.context.presentationProof }),
|
|
2796
2889
|
});
|
|
2890
|
+
const volumetricFog = renderSystem.volumetricFog;
|
|
2797
2891
|
return Object.freeze({
|
|
2798
2892
|
state,
|
|
2799
2893
|
surface: surfaceReleased ? 'released' : 'available',
|
|
@@ -2807,6 +2901,7 @@ async function makeWebGPURenderer(
|
|
|
2807
2901
|
(internals.featureHost?.features ?? []).map((feature) => feature.identity),
|
|
2808
2902
|
),
|
|
2809
2903
|
featureDiagnostics: internals.featureHost?.diagnostics() ?? Object.freeze([]),
|
|
2904
|
+
volumetricFog,
|
|
2810
2905
|
frustumStats: Object.freeze({ ...renderSystem.frustumStats }),
|
|
2811
2906
|
visibilityStats: Object.freeze({ ...renderSystem.visibilityStats }),
|
|
2812
2907
|
renderScene: renderSystem.renderScene,
|
|
@@ -3234,8 +3329,23 @@ async function makeWebGPURenderer(
|
|
|
3234
3329
|
// through onError separately; the facade Result is the synchronous
|
|
3235
3330
|
// summary AI users can ignore or branch on).
|
|
3236
3331
|
try {
|
|
3237
|
-
const
|
|
3332
|
+
const timingRequested =
|
|
3333
|
+
isFrameRequest &&
|
|
3334
|
+
(frameRequest?.profileFrame !== undefined ||
|
|
3335
|
+
internals.options?.captureGpuTimings === true);
|
|
3336
|
+
let timingCapture: GpuTimingCapture | undefined;
|
|
3337
|
+
let timingUnavailableReason = timingRequested
|
|
3338
|
+
? 'timestamp capture is unavailable on this device'
|
|
3339
|
+
: 'timestamp capture was not requested for this frame';
|
|
3340
|
+
if (timingRequested) {
|
|
3341
|
+
const createdTiming = GpuTimingCapture.create(internals.device);
|
|
3342
|
+
if (createdTiming.ok) timingCapture = createdTiming.value;
|
|
3343
|
+
else
|
|
3344
|
+
timingUnavailableReason = `timestamp capture setup failed: ${createdTiming.error.code}`;
|
|
3345
|
+
}
|
|
3346
|
+
const submitted = renderSystem.draw(worlds, drawOptions, renderReadLeases, timingCapture);
|
|
3238
3347
|
if (!submitted) {
|
|
3348
|
+
timingCapture?.discard();
|
|
3239
3349
|
return err(
|
|
3240
3350
|
new RendererContractFailureError(
|
|
3241
3351
|
'draw',
|
|
@@ -3265,7 +3375,15 @@ async function makeWebGPURenderer(
|
|
|
3265
3375
|
deviceGeneration: receiptGeneration,
|
|
3266
3376
|
completed,
|
|
3267
3377
|
});
|
|
3268
|
-
|
|
3378
|
+
receiptTimings.set(
|
|
3379
|
+
receipt,
|
|
3380
|
+
timingCapture?.observation() ??
|
|
3381
|
+
Promise.resolve({
|
|
3382
|
+
status: 'unavailable',
|
|
3383
|
+
reason: timingUnavailableReason,
|
|
3384
|
+
}),
|
|
3385
|
+
);
|
|
3386
|
+
issuedReceipts.add(receipt);
|
|
3269
3387
|
return ok(receipt);
|
|
3270
3388
|
}
|
|
3271
3389
|
return ok(undefined);
|
|
@@ -3290,10 +3408,8 @@ async function makeWebGPURenderer(
|
|
|
3290
3408
|
request: FrameObservationRequest,
|
|
3291
3409
|
): Promise<RenderResult<FrameReceiptObservation, RenderError>> {
|
|
3292
3410
|
if (
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
latestReceipt.frameId !== receipt.frameId ||
|
|
3296
|
-
latestReceipt.deviceGeneration !== receipt.deviceGeneration
|
|
3411
|
+
!issuedReceipts.has(receipt) ||
|
|
3412
|
+
receipt.deviceGeneration !== activeDeviceScope.generation
|
|
3297
3413
|
) {
|
|
3298
3414
|
const currentGeneration = activeDeviceScope.generation;
|
|
3299
3415
|
return err(
|
|
@@ -3316,11 +3432,19 @@ async function makeWebGPURenderer(
|
|
|
3316
3432
|
}),
|
|
3317
3433
|
);
|
|
3318
3434
|
}
|
|
3435
|
+
const timings = request.include.includes('timings')
|
|
3436
|
+
? await (receiptTimings.get(receipt) ??
|
|
3437
|
+
Promise.resolve({
|
|
3438
|
+
status: 'unavailable',
|
|
3439
|
+
reason: 'timing receipt was not retained',
|
|
3440
|
+
}))
|
|
3441
|
+
: undefined;
|
|
3319
3442
|
return ok(
|
|
3320
3443
|
Object.freeze({
|
|
3321
3444
|
frameId: receipt.frameId,
|
|
3322
3445
|
deviceGeneration: receipt.deviceGeneration,
|
|
3323
3446
|
include: Object.freeze([...request.include]),
|
|
3447
|
+
...(timings === undefined ? {} : { timings }),
|
|
3324
3448
|
}),
|
|
3325
3449
|
);
|
|
3326
3450
|
},
|
|
@@ -3595,7 +3719,7 @@ async function makeWebGPURenderer(
|
|
|
3595
3719
|
let device: RhiDevice;
|
|
3596
3720
|
try {
|
|
3597
3721
|
const deviceResult = await adapterResult.value.requestDevice(
|
|
3598
|
-
deviceOptionsForAdapter(adapterResult.value),
|
|
3722
|
+
deviceOptionsForAdapter(adapterResult.value, internals.options),
|
|
3599
3723
|
);
|
|
3600
3724
|
if (!deviceResult.ok) {
|
|
3601
3725
|
return err(new RecoverError('recover-device-unavailable'));
|
|
@@ -3964,6 +4088,11 @@ const RENDER_PROFILE_KEYS = new Set([
|
|
|
3964
4088
|
'pbr',
|
|
3965
4089
|
'ibl',
|
|
3966
4090
|
'ssao',
|
|
4091
|
+
'bloom',
|
|
4092
|
+
'tone',
|
|
4093
|
+
'antialias',
|
|
4094
|
+
'sky',
|
|
4095
|
+
'volumetricFog',
|
|
3967
4096
|
'fallback',
|
|
3968
4097
|
'postStages',
|
|
3969
4098
|
]);
|
|
@@ -3990,6 +4119,24 @@ function validateRenderProfile(profile: RenderProfile): string | undefined {
|
|
|
3990
4119
|
for (const field of ['pbr', 'ibl', 'ssao'] as const) {
|
|
3991
4120
|
if (typeof profile[field] !== 'boolean') return `RenderProfile.${field} must be boolean`;
|
|
3992
4121
|
}
|
|
4122
|
+
if (profile.volumetricFog !== undefined) {
|
|
4123
|
+
if (profile.volumetricFog.quality !== 'low' && profile.volumetricFog.quality !== 'high')
|
|
4124
|
+
return "RenderProfile.volumetricFog.quality must be 'low' or 'high'";
|
|
4125
|
+
if (profile.volumetricFog.depth !== 48 && profile.volumetricFog.depth !== 64) {
|
|
4126
|
+
return 'RenderProfile.volumetricFog.depth must be 48 or 64';
|
|
4127
|
+
}
|
|
4128
|
+
if (profile.volumetricFog.tileSize !== 4 && profile.volumetricFog.tileSize !== 16) {
|
|
4129
|
+
return 'RenderProfile.volumetricFog.tileSize must be 4 or 16';
|
|
4130
|
+
}
|
|
4131
|
+
if (profile.volumetricFog.quality === 'low' && profile.volumetricFog.depth !== 48)
|
|
4132
|
+
return 'RenderProfile.volumetricFog.low must use depth 48';
|
|
4133
|
+
if (profile.volumetricFog.quality === 'high' && profile.volumetricFog.depth !== 64)
|
|
4134
|
+
return 'RenderProfile.volumetricFog.high must use depth 64';
|
|
4135
|
+
if (profile.volumetricFog.quality === 'low' && profile.volumetricFog.tileSize !== 16)
|
|
4136
|
+
return 'RenderProfile.volumetricFog.low must use tileSize 16';
|
|
4137
|
+
if (profile.volumetricFog.quality === 'high' && profile.volumetricFog.tileSize !== 4)
|
|
4138
|
+
return 'RenderProfile.volumetricFog.high must use tileSize 4';
|
|
4139
|
+
}
|
|
3993
4140
|
const stages = STANDARD_POST_STAGE_NAMES;
|
|
3994
4141
|
if (
|
|
3995
4142
|
!Array.isArray(profile.postStages) ||
|
|
@@ -4767,6 +4914,14 @@ async function prepareMaterialShaders(
|
|
|
4767
4914
|
if (!capCheck.ok) throw capCheck.error;
|
|
4768
4915
|
storageBufferCapable = !webgl2Downlevel && capCheck.value;
|
|
4769
4916
|
}
|
|
4917
|
+
// The shared PBR view/material ABI consumes the WebGPU minimum of 16
|
|
4918
|
+
// fragment sampled textures. The authored SpotLight projector is the
|
|
4919
|
+
// optional 17th texture; select its shader/BGL pair from the same limit so
|
|
4920
|
+
// low-limit adapters keep analytic lighting instead of failing layout
|
|
4921
|
+
// creation at boot.
|
|
4922
|
+
const projectorAvailable =
|
|
4923
|
+
(rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
4924
|
+
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
|
|
4770
4925
|
|
|
4771
4926
|
// ── Step 1: manifest load ─────────────────────────────────────────────────
|
|
4772
4927
|
const registry = getShader();
|
|
@@ -4808,6 +4963,12 @@ async function prepareMaterialShaders(
|
|
|
4808
4963
|
if (msEntry.variants.some((v) => 'VERTEX_COLOR_AVAILABLE' in v.defines)) {
|
|
4809
4964
|
variantDefines.VERTEX_COLOR_AVAILABLE = false;
|
|
4810
4965
|
}
|
|
4966
|
+
if (
|
|
4967
|
+
msEntry.identifier === 'forgeax::default-standard-pbr' &&
|
|
4968
|
+
msEntry.variants.some((v) => 'PROJECTOR_AVAILABLE' in v.defines)
|
|
4969
|
+
) {
|
|
4970
|
+
variantDefines.PROJECTOR_AVAILABLE = projectorAvailable;
|
|
4971
|
+
}
|
|
4811
4972
|
if (
|
|
4812
4973
|
msEntry.identifier === 'forgeax::default-standard-pbr' &&
|
|
4813
4974
|
msEntry.variants.some((v) => 'TRANSMISSION_AVAILABLE' in v.defines)
|
|
@@ -4984,6 +5145,8 @@ async function buildReadyWebGPU(
|
|
|
4984
5145
|
readonly motionBlur?: string;
|
|
4985
5146
|
readonly taaResolve?: string;
|
|
4986
5147
|
}) => void,
|
|
5148
|
+
/** Install manifest-owned volume utility sources only after all three modules compile. */
|
|
5149
|
+
setVolumetricFogShaderSources: (sources: VolumetricFogShaderSources) => void,
|
|
4987
5150
|
): Promise<PipelineState> {
|
|
4988
5151
|
// bug-20260610: WebGL2 fallback path — read storage-buffer capability up
|
|
4989
5152
|
// front so both Step 2 (shader-module compile, where we patch engine
|
|
@@ -5001,6 +5164,9 @@ async function buildReadyWebGPU(
|
|
|
5001
5164
|
const capCheck = assertStorageBufferCap(capValue);
|
|
5002
5165
|
storageBufferCapable = capCheck.ok ? !webgl2Downlevel && capCheck.value : !webgl2Downlevel;
|
|
5003
5166
|
}
|
|
5167
|
+
const projectorAvailable =
|
|
5168
|
+
(rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
5169
|
+
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
|
|
5004
5170
|
|
|
5005
5171
|
// bug-20260612: choose swap-chain storage / view formats by backend.
|
|
5006
5172
|
// Channel 2 (native WebGPU) follows navigator.gpu.getPreferredCanvasFormat();
|
|
@@ -5303,6 +5469,8 @@ async function buildReadyWebGPU(
|
|
|
5303
5469
|
v.defines.CLUSTER_FORWARD_AVAILABLE === false) &&
|
|
5304
5470
|
(!('VERTEX_COLOR_AVAILABLE' in v.defines) ||
|
|
5305
5471
|
v.defines.VERTEX_COLOR_AVAILABLE === false) &&
|
|
5472
|
+
(!('PROJECTOR_AVAILABLE' in v.defines) ||
|
|
5473
|
+
v.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
|
|
5306
5474
|
(!('SKINNING_DISABLED' in v.defines) || v.defines.SKINNING_DISABLED === true),
|
|
5307
5475
|
);
|
|
5308
5476
|
const v = exact === undefined ? undefined : findVariantByKey(ms, exact.definesKey);
|
|
@@ -5346,6 +5514,8 @@ async function buildReadyWebGPU(
|
|
|
5346
5514
|
variant.defines.CLUSTER_FORWARD_AVAILABLE === false) &&
|
|
5347
5515
|
(!('VERTEX_COLOR_AVAILABLE' in variant.defines) ||
|
|
5348
5516
|
variant.defines.VERTEX_COLOR_AVAILABLE === false) &&
|
|
5517
|
+
(!('PROJECTOR_AVAILABLE' in variant.defines) ||
|
|
5518
|
+
variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
|
|
5349
5519
|
(!('SKINNING_DISABLED' in variant.defines) ||
|
|
5350
5520
|
variant.defines.SKINNING_DISABLED === true) &&
|
|
5351
5521
|
(!('TRANSMISSION_AVAILABLE' in variant.defines) ||
|
|
@@ -5480,6 +5650,80 @@ async function buildReadyWebGPU(
|
|
|
5480
5650
|
seedShaderModule(label, shaderResult.value);
|
|
5481
5651
|
}
|
|
5482
5652
|
|
|
5653
|
+
// The volume graph is authored from ECS state and therefore cannot be
|
|
5654
|
+
// compiled while the static material manifest is assembled. Its pass
|
|
5655
|
+
// encoder nevertheless uses the same shared lazy shader adapter, so the
|
|
5656
|
+
// four utility modules must be prewarmed before the first synchronous
|
|
5657
|
+
// draw. The sources come from the shader manifest (the package-owned WGSL
|
|
5658
|
+
// files are the sole source of truth); compile all four first and only
|
|
5659
|
+
// then publish the labels and source bundle. A partial bundle would let a
|
|
5660
|
+
// later frame mix generations after a device recovery.
|
|
5661
|
+
if (rhiDevice.caps.compute && rhiDevice.caps.storageTexture) {
|
|
5662
|
+
const volumeSources = [
|
|
5663
|
+
['inject', 'volume_inject', (source: string) => source.includes('fn volume_inject(')],
|
|
5664
|
+
['temporal', 'volume_temporal', (source: string) => source.includes('fn volume_temporal(')],
|
|
5665
|
+
[
|
|
5666
|
+
'integrate',
|
|
5667
|
+
'volume_integrate',
|
|
5668
|
+
(source: string) => source.includes('fn volume_integrate('),
|
|
5669
|
+
],
|
|
5670
|
+
['composite', 'volume_composite', (source: string) => source.includes('fn volume_fs(')],
|
|
5671
|
+
] as const;
|
|
5672
|
+
const resolved = volumeSources.map(([stage, label, matches]) => {
|
|
5673
|
+
const entry = manifestEntries.find((candidate) => matches(candidate.wgsl));
|
|
5674
|
+
return { stage, label, entry };
|
|
5675
|
+
});
|
|
5676
|
+
// Volumetric fog is an optional graph feature. Minimal hosts/tests may
|
|
5677
|
+
// intentionally ship only material/post-process entries; keep those
|
|
5678
|
+
// renderers bootable while still rejecting a partially declared volume
|
|
5679
|
+
// bundle (which would otherwise fail later as a lazy pending pipeline).
|
|
5680
|
+
const present = resolved.filter((candidate) => candidate.entry !== undefined).length;
|
|
5681
|
+
if (present === 0) {
|
|
5682
|
+
// No volume manifest entries: leave the source/module bundle unset and
|
|
5683
|
+
// let the topology capability gate keep volume work disabled.
|
|
5684
|
+
} else {
|
|
5685
|
+
const missing = resolved.find((candidate) => candidate.entry === undefined);
|
|
5686
|
+
if (missing !== undefined) {
|
|
5687
|
+
throw new RhiError({
|
|
5688
|
+
code: 'shader-compile-failed',
|
|
5689
|
+
expected: 'shader manifest contains all four volumetric fog utility entries',
|
|
5690
|
+
hint: `add the package-owned WGSL entry for '${missing.label}' to the engine shader manifest`,
|
|
5691
|
+
});
|
|
5692
|
+
}
|
|
5693
|
+
const compiled = [] as Array<{
|
|
5694
|
+
readonly stage: (typeof volumeSources)[number][0];
|
|
5695
|
+
readonly label: string;
|
|
5696
|
+
readonly source: string;
|
|
5697
|
+
readonly module: ShaderModule;
|
|
5698
|
+
}>;
|
|
5699
|
+
for (const { stage, label, entry } of resolved) {
|
|
5700
|
+
// `missing` above proves the narrowing for this loop; keep the guard
|
|
5701
|
+
// explicit so a future refactor cannot publish an undefined source.
|
|
5702
|
+
if (entry === undefined) throw new Error(`missing volumetric shader ${label}`);
|
|
5703
|
+
const shaderResult = await runShimStep(
|
|
5704
|
+
() =>
|
|
5705
|
+
asyncCreateShaderModule
|
|
5706
|
+
? asyncCreateShaderModule(rhiDevice, { code: entry.wgsl, label })
|
|
5707
|
+
: invokeDeviceCreateShaderModule(rhiDevice, { code: entry.wgsl, label }),
|
|
5708
|
+
'shader-compile-failed',
|
|
5709
|
+
`volumetric fog shader module '${label}' compiled`,
|
|
5710
|
+
`inspect manifest WGSL for '${label}' and check device.features`,
|
|
5711
|
+
);
|
|
5712
|
+
if (!shaderResult.ok) throw shaderResult.error;
|
|
5713
|
+
compiled.push({ stage, label, source: entry.wgsl, module: shaderResult.value });
|
|
5714
|
+
}
|
|
5715
|
+
for (const { label, module } of compiled) seedShaderModule(label, module);
|
|
5716
|
+
setVolumetricFogShaderSources({
|
|
5717
|
+
inject: compiled.find((candidate) => candidate.stage === 'inject')?.source as string,
|
|
5718
|
+
temporal: compiled.find((candidate) => candidate.stage === 'temporal')?.source as string,
|
|
5719
|
+
integrate: compiled.find((candidate) => candidate.stage === 'integrate')
|
|
5720
|
+
?.source as string,
|
|
5721
|
+
composite: compiled.find((candidate) => candidate.stage === 'composite')
|
|
5722
|
+
?.source as string,
|
|
5723
|
+
});
|
|
5724
|
+
}
|
|
5725
|
+
}
|
|
5726
|
+
|
|
5483
5727
|
const pbrShaderResult = await runShimStep(
|
|
5484
5728
|
() =>
|
|
5485
5729
|
asyncCreateShaderModule
|
|
@@ -5511,7 +5755,11 @@ async function buildReadyWebGPU(
|
|
|
5511
5755
|
(rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
5512
5756
|
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
|
|
5513
5757
|
const transmissionVariants = transmissionCapable
|
|
5514
|
-
? selectStandardPbrTransmissionPrewarmVariants(
|
|
5758
|
+
? selectStandardPbrTransmissionPrewarmVariants(
|
|
5759
|
+
pbrManifestEntry,
|
|
5760
|
+
storageBufferCapable,
|
|
5761
|
+
projectorAvailable,
|
|
5762
|
+
)
|
|
5515
5763
|
: [];
|
|
5516
5764
|
|
|
5517
5765
|
// HDRP uses the canonical all-true variant key (`''`) and the lazy
|
|
@@ -5549,6 +5797,7 @@ async function buildReadyWebGPU(
|
|
|
5549
5797
|
pbrManifestEntry,
|
|
5550
5798
|
storageBufferCapable,
|
|
5551
5799
|
transmissionCapable,
|
|
5800
|
+
projectorAvailable,
|
|
5552
5801
|
)) {
|
|
5553
5802
|
const moduleLabel = `module-forgeax::default-standard-pbr#${hdrpVariant.definesKey}`;
|
|
5554
5803
|
let hdrpModule = prewarmedHdrpModules.get(hdrpVariant.composedWgsl);
|
|
@@ -6042,7 +6291,13 @@ async function buildReadyWebGPU(
|
|
|
6042
6291
|
// buildReadyWebGPU (above) so Step 2 (variant patch) and this layout
|
|
6043
6292
|
// build read the same value.
|
|
6044
6293
|
const pbrLayouts = runShimSyncStep(
|
|
6045
|
-
() =>
|
|
6294
|
+
() =>
|
|
6295
|
+
ok(
|
|
6296
|
+
buildPbrPipelineLayouts(rhiDevice, {
|
|
6297
|
+
storageBuffer: storageBufferCapable,
|
|
6298
|
+
projectorAvailable,
|
|
6299
|
+
}),
|
|
6300
|
+
),
|
|
6046
6301
|
'webgpu-runtime-error',
|
|
6047
6302
|
'buildPbrPipelineLayouts succeeded',
|
|
6048
6303
|
'check device.limits.maxBindingsPerBindGroup (need >=14) and maxBindGroupsPerPipelineLayout',
|
|
@@ -8589,6 +8844,7 @@ async function buildReadyWebGPU(
|
|
|
8589
8844
|
colorAttachmentFormat: swapChainFormats.view,
|
|
8590
8845
|
surfaceProfile: surfaceViewFormats ? 'dual-view' : 'raw-only',
|
|
8591
8846
|
viewBindGroupLayout: viewBglResult.value,
|
|
8847
|
+
projectorAvailable,
|
|
8592
8848
|
materialBindGroupLayout: materialBglResult.value,
|
|
8593
8849
|
meshBindGroupLayout: meshArrayBglResult.value,
|
|
8594
8850
|
viewUniformBuffer: viewUboResult.value,
|
|
@@ -155,6 +155,10 @@ export function validateSpotLightData(
|
|
|
155
155
|
if (pcfKernelSize < 1 || pcfKernelSize % 2 === 0) {
|
|
156
156
|
return err(new ShadowInvalidConfigError('pcfKernelSize', pcfKernelSize, 1));
|
|
157
157
|
}
|
|
158
|
+
const shadowIntensity = (data.shadowIntensity as number | undefined) ?? 1;
|
|
159
|
+
if (!Number.isFinite(shadowIntensity) || shadowIntensity < 0 || shadowIntensity > 1) {
|
|
160
|
+
return err(new ShadowInvalidConfigError('shadowIntensity', shadowIntensity, 0, 1));
|
|
161
|
+
}
|
|
158
162
|
return ok(undefined);
|
|
159
163
|
}
|
|
160
164
|
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
// spotCount === 0" (M5 / w25).
|
|
22
22
|
//
|
|
23
23
|
// feat-20260625-spot-light-shadow-mapping M1 w4: embedded castShadow (default true)
|
|
24
|
-
// +
|
|
25
|
-
// pcfKernelSize) aligned with DirectionalLight vocabulary (plan-strategy D-6;
|
|
24
|
+
// + 7 shadow fields (mapSize / depthBias / normalBias / nearPlane / farPlane /
|
|
25
|
+
// pcfKernelSize / shadowIntensity) aligned with DirectionalLight vocabulary (plan-strategy D-6;
|
|
26
26
|
// charter P4 consistent abstraction). Zero-config spawns cast spot shadows;
|
|
27
27
|
// set castShadow:false to opt out (validate short-circuits on false, AC-03).
|
|
28
28
|
// Shadow atlas cap of 4 is enforced at extract stage, not component layer (OOS-5).
|
|
@@ -37,6 +37,17 @@
|
|
|
37
37
|
|
|
38
38
|
import { defineComponent } from '@forgeax/engine-ecs';
|
|
39
39
|
|
|
40
|
+
export interface SpotLightProjector {
|
|
41
|
+
readonly guid: string;
|
|
42
|
+
readonly generation: number;
|
|
43
|
+
readonly revision: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SpotLightAuthoring {
|
|
47
|
+
/** Optional GUID-backed TextureAsset projector; absent keeps the normal spot path. */
|
|
48
|
+
readonly projector?: SpotLightProjector;
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
/**
|
|
41
52
|
* Cone-restricted spot light (KHR_lights_punctual `spot` type). Casts shadows
|
|
42
53
|
* by default (castShadow defaults to true) — zero-config spawns project hard
|
|
@@ -71,6 +82,7 @@ import { defineComponent } from '@forgeax/engine-ecs';
|
|
|
71
82
|
* nearPlane — shadow-camera near (default 0.1)
|
|
72
83
|
* farPlane — shadow-camera far (default 50)
|
|
73
84
|
* pcfKernelSize odd >= 1 — PCF kernel width (default 3)
|
|
85
|
+
* shadowIntensity in [0,1] — residual light in shadow (default 1)
|
|
74
86
|
*
|
|
75
87
|
* Atlas capacity is capped at 4 castShadow spot lights by the extract stage;
|
|
76
88
|
* the 5th light onward keeps direct illumination but shadowAtlasTile = -1
|
|
@@ -114,11 +126,18 @@ export const SpotLight = defineComponent('SpotLight', {
|
|
|
114
126
|
outerConeDeg: { type: 'f32', default: 45 },
|
|
115
127
|
// Shadow opt-out gate: defaults to true so zero-config spawns cast shadows.
|
|
116
128
|
castShadow: { type: 'bool', default: true },
|
|
117
|
-
//
|
|
129
|
+
// 7 shadow fields aligned with the shared light shadow vocabulary.
|
|
118
130
|
mapSize: { type: 'f32', default: 2048 },
|
|
119
131
|
depthBias: { type: 'f32', default: 0.005 },
|
|
120
132
|
normalBias: { type: 'f32', default: 0.05 },
|
|
121
133
|
nearPlane: { type: 'f32', default: 0.1 },
|
|
122
134
|
farPlane: { type: 'f32', default: 50 },
|
|
123
135
|
pcfKernelSize: { type: 'f32', default: 3 },
|
|
136
|
+
// Three's SpotLight.shadow.intensity is the fraction of visibility applied
|
|
137
|
+
// to a shadowed contribution. Keep it in the author-facing [0,1] domain;
|
|
138
|
+
// extract/record transport this one fact to surface and volume consumers.
|
|
139
|
+
shadowIntensity: { type: 'f32', default: 1 },
|
|
140
|
+
// Shared TextureAsset identity; publication and residency remain owned by
|
|
141
|
+
// the existing asset and GPU resource owners.
|
|
142
|
+
projector: { type: 'shared<TextureAsset>', simulationTransient: true },
|
|
124
143
|
});
|