@forgeax/engine-render 0.1.23 → 0.1.24
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 +131 -9
- package/dist/assembly/bundler-contract.d.ts +9 -0
- package/dist/assembly/bundler-contract.d.ts.map +1 -0
- package/dist/assembly/device-feature-admission.d.ts +8 -0
- package/dist/assembly/device-feature-admission.d.ts.map +1 -1
- package/dist/assembly/extended-lighting-fallback.d.ts +13 -0
- package/dist/assembly/extended-lighting-fallback.d.ts.map +1 -0
- package/dist/assembly/factory.d.ts +7 -243
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/material/assembly.d.ts +3 -2
- package/dist/assembly/material/assembly.d.ts.map +1 -1
- package/dist/assembly/material/standard-layer-projection.d.ts +10 -0
- package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -0
- package/dist/assembly/material-shader-policy.d.ts +168 -0
- package/dist/assembly/material-shader-policy.d.ts.map +1 -0
- package/dist/assembly/material-shader-prewarm.d.ts +21 -0
- package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
- package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
- package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
- package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
- package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
- package/dist/assembly/recovery/generation.d.ts +46 -0
- package/dist/assembly/recovery/generation.d.ts.map +1 -0
- package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
- package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
- package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
- package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
- package/dist/assembly/renderer-facade.d.ts +10 -0
- package/dist/assembly/renderer-facade.d.ts.map +1 -0
- package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
- package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
- package/dist/assembly/renderer-helpers.d.ts +19 -0
- package/dist/assembly/renderer-helpers.d.ts.map +1 -0
- package/dist/assembly/renderer-lifecycle.d.ts +34 -0
- package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
- package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
- package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
- package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready.d.ts +55 -0
- package/dist/assembly/webgpu-ready.d.ts.map +1 -0
- package/dist/assembly/webgpu-renderer.d.ts +152 -0
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
- package/dist/authoring.mjs +1 -1
- package/dist/{chunk-4L3P6DT5.mjs → chunk-GE3SDD72.mjs} +37 -5
- package/dist/chunk-GE3SDD72.mjs.map +1 -0
- package/dist/{chunk-HZUPSYW7.mjs → chunk-KZFTK6DO.mjs} +102 -15
- package/dist/chunk-KZFTK6DO.mjs.map +1 -0
- package/dist/{chunk-FF7K3FSO.mjs → chunk-Q752SHI5.mjs} +89 -89
- package/dist/{chunk-FF7K3FSO.mjs.map → chunk-Q752SHI5.mjs.map} +1 -1
- package/dist/{chunk-QODA5UKU.mjs → chunk-RR5L4SRS.mjs} +2 -2
- package/dist/chunk-RR5L4SRS.mjs.map +1 -0
- package/dist/{chunk-YALPS44A.mjs → chunk-TFH5I7JJ.mjs} +47 -47
- package/dist/chunk-TFH5I7JJ.mjs.map +1 -0
- package/dist/{chunk-FBC4R44J.mjs → chunk-UQYS7FRM.mjs} +57 -57
- package/dist/chunk-UQYS7FRM.mjs.map +1 -0
- package/dist/{chunk-4NPUASZR.mjs → chunk-YKB5DQV7.mjs} +161 -19
- package/dist/chunk-YKB5DQV7.mjs.map +1 -0
- package/dist/{chunk-ZUWKAZ2I.mjs → chunk-YRUYGA4A.mjs} +615 -237
- package/dist/chunk-YRUYGA4A.mjs.map +1 -0
- package/dist/{chunk-DIYW3M5D.mjs → chunk-Z3YYH2MB.mjs} +2210 -2213
- package/dist/chunk-Z3YYH2MB.mjs.map +1 -0
- package/dist/construct-renderer.mjs +19841 -16190
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/device-scope.d.ts +2 -0
- package/dist/device/device-scope.d.ts.map +1 -1
- package/dist/device/gpu-residency.d.ts +24 -4
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/environment/frame.d.ts +5 -0
- package/dist/environment/frame.d.ts.map +1 -1
- package/dist/environment/inspection.d.ts +7 -0
- package/dist/environment/inspection.d.ts.map +1 -1
- package/dist/environment/lifecycle.d.ts +20 -2
- package/dist/environment/lifecycle.d.ts.map +1 -1
- package/dist/errors/recover.d.ts +58 -2
- package/dist/errors/recover.d.ts.map +1 -1
- package/dist/errors/render.d.ts +13 -0
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/extract/contracts.d.ts +899 -0
- package/dist/extract/contracts.d.ts.map +1 -0
- package/dist/features/host.d.ts +2 -0
- package/dist/features/host.d.ts.map +1 -1
- package/dist/features/prepared-gpu-work.d.ts +3 -0
- package/dist/features/prepared-gpu-work.d.ts.map +1 -1
- package/dist/features/prepared-graphics-store.d.ts +2 -0
- package/dist/features/prepared-graphics-store.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +28 -0
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +1 -0
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/ibl/IblPipelineCache.d.ts +7 -6
- package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
- package/dist/ibl/skylight-bind-group.d.ts +7 -1
- package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +6 -6
- package/dist/internal.mjs +6 -6
- package/dist/light-buffer-layout.d.ts +3 -0
- package/dist/light-buffer-layout.d.ts.map +1 -1
- package/dist/materials.d.ts +34 -10
- package/dist/materials.d.ts.map +1 -1
- package/dist/mesh-material-bindings.d.ts +56 -0
- package/dist/mesh-material-bindings.d.ts.map +1 -1
- package/dist/occlusion-proxy.d.ts +4 -0
- package/dist/occlusion-proxy.d.ts.map +1 -0
- package/dist/pbr-pipeline.d.ts +32 -9
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/points-lines/prepare.d.ts +3 -0
- package/dist/points-lines/prepare.d.ts.map +1 -1
- package/dist/points-lines/standard-owner.d.ts +33 -0
- package/dist/points-lines/standard-owner.d.ts.map +1 -0
- package/dist/record/frame-snapshot.d.ts +5 -0
- 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 +14 -2
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +14 -1
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +7 -17
- package/dist/record/main-pass-material.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/mesh-ssbo.d.ts +11 -3
- package/dist/record/mesh-ssbo.d.ts.map +1 -1
- package/dist/record/recovery-pipeline.d.ts +30 -0
- package/dist/record/recovery-pipeline.d.ts.map +1 -0
- package/dist/record/render-context.d.ts +26 -4
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +1 -1
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts +65 -0
- package/dist/recovery/render-system-candidate.d.ts.map +1 -0
- package/dist/render-contract.d.ts +64 -7
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts +12 -0
- package/dist/render-system-extract-tail.d.ts.map +1 -0
- package/dist/render-system-extract.d.ts +156 -22
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +128 -4
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +19 -0
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/temporal/frame-coordinator.d.ts.map +1 -1
- package/dist/temporal/index.mjs +6 -6
- package/dist/temporal/taa-history-store.d.ts +15 -1
- package/dist/temporal/taa-history-store.d.ts.map +1 -1
- package/dist/temporal/transaction.d.ts +5 -0
- package/dist/temporal/transaction.d.ts.map +1 -1
- package/package.json +20 -20
- package/src/__tests__/device-feature-admission.unit.test.ts +6 -0
- package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
- package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
- package/src/__tests__/environment-generation.integration.test.ts +23 -0
- package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
- package/src/__tests__/factory-contract.integration.test.ts +122 -7
- package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
- package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
- package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
- package/src/__tests__/material-contract-inventory.unit.test.ts +24 -9
- package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
- package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
- package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
- package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
- package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
- package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +1 -1
- package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
- package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
- package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +85 -18
- package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
- package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
- package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
- package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
- package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
- package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
- package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
- package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
- package/src/__tests__/shader-manifest-fixture.ts +13 -16
- package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
- package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
- package/src/__tests__/transmission-prewarm.unit.test.ts +95 -85
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
- package/src/assembly/bundler-contract.ts +9 -0
- package/src/assembly/device-feature-admission.ts +15 -5
- package/src/assembly/extended-lighting-fallback.ts +216 -0
- package/src/assembly/factory.ts +81 -10088
- package/src/assembly/material/assembly.ts +18 -2
- package/src/assembly/material/standard-layer-projection.ts +16 -0
- package/src/assembly/material-shader-policy.ts +854 -0
- package/src/assembly/material-shader-prewarm.ts +88 -0
- package/src/assembly/mesh-ssbo-grow.ts +270 -0
- package/src/assembly/recovery/device-loss-fanout.ts +89 -0
- package/src/assembly/recovery/generation.ts +141 -0
- package/src/assembly/recovery/recovery-attempt.ts +800 -0
- package/src/assembly/recovery/renderer-recover.ts +906 -0
- package/src/assembly/renderer-facade.ts +342 -0
- package/src/assembly/renderer-frame-transaction.ts +45 -0
- package/src/assembly/renderer-helpers.ts +99 -0
- package/src/assembly/renderer-lifecycle.ts +159 -0
- package/src/assembly/shader-prewarm-policy.ts +157 -0
- package/src/assembly/webgpu-ready-contract.ts +14 -0
- package/src/assembly/webgpu-ready.ts +4079 -0
- package/src/assembly/webgpu-renderer.ts +4095 -0
- package/src/components/camera.ts +1 -1
- package/src/debug-draw-glue.ts +1 -1
- package/src/device/device-scope.ts +35 -6
- package/src/device/gpu-residency.ts +194 -26
- package/src/environment/frame.ts +6 -0
- package/src/environment/inspection.ts +10 -0
- package/src/environment/lifecycle.ts +114 -8
- package/src/errors/recover.ts +76 -5
- package/src/errors/render.ts +17 -0
- package/src/extract/contracts.ts +974 -0
- package/src/features/host.ts +16 -0
- package/src/features/prepared-gpu-work.ts +20 -2
- package/src/features/prepared-graphics-store.ts +17 -0
- package/src/gpu-driven/production-raster.ts +97 -2
- package/src/gpu-scene.ts +3 -2
- package/src/ibl/IblPipelineCache.ts +134 -50
- package/src/ibl/skylight-bind-group.ts +26 -0
- package/src/index.ts +9 -1
- package/src/light-buffer-layout.ts +3 -0
- package/src/materials.ts +262 -45
- package/src/mesh-material-bindings.ts +59 -0
- package/src/occlusion-proxy.ts +64 -0
- package/src/pbr-pipeline.ts +139 -15
- package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
- package/src/points-lines/prepare.ts +9 -0
- package/src/points-lines/standard-owner.ts +403 -0
- package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
- package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
- package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
- package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
- package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
- package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
- package/src/record/__tests__/temporal-payload.unit.test.ts +22 -43
- package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -1
- package/src/record/frame-snapshot.ts +15 -0
- package/src/record/frame-targets.ts +11 -1
- package/src/record/frame.ts +6 -2
- package/src/record/helpers.ts +1 -1
- package/src/record/main-pass-geometry.ts +230 -5
- package/src/record/main-pass-material.ts +316 -209
- package/src/record/main-pass-sprite-draws.ts +6 -7
- package/src/record/main-pass.ts +94 -6
- package/src/record/mesh-ssbo.ts +32 -10
- package/src/record/recovery-pipeline.ts +695 -0
- package/src/record/render-context.ts +35 -4
- package/src/record/typed-frame-graph.ts +45 -1
- package/src/record/view-ubo.ts +3 -3
- package/src/recovery/render-system-candidate.ts +839 -0
- package/src/render-contract.ts +65 -7
- package/src/render-system-extract-tail.ts +2983 -0
- package/src/render-system-extract.ts +124 -2977
- package/src/render-system.ts +868 -659
- package/src/scene/render-scene.ts +124 -1
- package/src/temporal/frame-coordinator.ts +4 -1
- package/src/temporal/taa-history-store.ts +46 -2
- package/src/temporal/temporal-view.ts +1 -1
- package/src/temporal/transaction.ts +9 -0
- package/src/tilemap-chunk-extract-system.ts +1 -1
- package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
- package/dist/chunk-4L3P6DT5.mjs.map +0 -1
- package/dist/chunk-4NPUASZR.mjs.map +0 -1
- package/dist/chunk-DIYW3M5D.mjs.map +0 -1
- package/dist/chunk-FBC4R44J.mjs.map +0 -1
- package/dist/chunk-HZUPSYW7.mjs.map +0 -1
- package/dist/chunk-QODA5UKU.mjs.map +0 -1
- package/dist/chunk-YALPS44A.mjs.map +0 -1
- package/dist/chunk-ZUWKAZ2I.mjs.map +0 -1
|
@@ -103,32 +103,18 @@ import {
|
|
|
103
103
|
walkMaterialPassesOverSharedRefs,
|
|
104
104
|
} from '@forgeax/engine-assets-runtime';
|
|
105
105
|
import type { Component, ComponentSchema, EntityHandle, Query, World } from '@forgeax/engine-ecs';
|
|
106
|
-
import {
|
|
107
|
-
|
|
108
|
-
readRenderArrayView,
|
|
109
|
-
routeWorldError,
|
|
110
|
-
SpawnLightInvalidBoundsError,
|
|
111
|
-
SpriteInstancesCountMismatchError,
|
|
112
|
-
SpriteInstancesMutuallyExclusiveWithInstancesError,
|
|
113
|
-
SpriteInstancesRequiresSpriteShaderError,
|
|
114
|
-
} from '@forgeax/engine-ecs/projection';
|
|
115
|
-
import { deriveVertexLayoutProjection } from '@forgeax/engine-geometry';
|
|
116
|
-
import { box3, frustum, type Mat4, mat4, type Vec3, vec3 } from '@forgeax/engine-math';
|
|
106
|
+
import { readRenderArrayView, routeWorldError } from '@forgeax/engine-ecs/projection';
|
|
107
|
+
import { type Mat4, mat4, type Vec3, vec3 } from '@forgeax/engine-math';
|
|
117
108
|
import { AssetGuid, type AssetGuid as AssetGuidBytes } from '@forgeax/engine-pack/guid';
|
|
118
109
|
import { RhiError } from '@forgeax/engine-rhi';
|
|
119
|
-
import { GlobalTransform,
|
|
120
|
-
import type { MaterialShaderArtifact } from '@forgeax/engine-shader';
|
|
110
|
+
import { GlobalTransform, Transform } from '@forgeax/engine-scene';
|
|
121
111
|
import {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
Skin,
|
|
126
|
-
SkinInstancesCoexistForbiddenError,
|
|
127
|
-
} from '@forgeax/engine-skinning';
|
|
112
|
+
type MaterialShaderArtifact,
|
|
113
|
+
STANDARD_PIPELINE_PARAM_SCHEMA,
|
|
114
|
+
} from '@forgeax/engine-shader';
|
|
128
115
|
import type {
|
|
129
116
|
Asset,
|
|
130
117
|
Handle,
|
|
131
|
-
IesProfileAsset,
|
|
132
118
|
MaterialAsset,
|
|
133
119
|
MaterialColorParameterSchema,
|
|
134
120
|
MaterialParameter,
|
|
@@ -136,115 +122,61 @@ import type {
|
|
|
136
122
|
MaterialRenderState,
|
|
137
123
|
MaterialTextureCoordinates,
|
|
138
124
|
MaterialTextureValue,
|
|
139
|
-
MeshAsset,
|
|
140
125
|
MeshLodLevel,
|
|
141
126
|
ParamSchemaEntry,
|
|
142
127
|
PrimitiveTopology,
|
|
143
|
-
SkeletonAsset,
|
|
144
128
|
TextureAsset,
|
|
145
129
|
} from '@forgeax/engine-types';
|
|
146
130
|
import {
|
|
147
|
-
ASSET_ERROR_HINTS,
|
|
148
|
-
AssetError,
|
|
149
131
|
derive,
|
|
150
|
-
handleSlot,
|
|
151
132
|
materialGuidText,
|
|
152
133
|
materialValuesToLinearRuntime,
|
|
153
134
|
toShared,
|
|
154
135
|
} from '@forgeax/engine-types';
|
|
155
136
|
import {
|
|
156
|
-
Atmosphere,
|
|
157
137
|
antialiasFromF32,
|
|
158
138
|
bloomEnabledFromF32,
|
|
159
139
|
Camera,
|
|
160
140
|
CubeCamera,
|
|
161
141
|
cameraProjectionFromF32,
|
|
162
142
|
cubeCameraUpdateIntentFromF32,
|
|
163
|
-
DirectionalLight,
|
|
164
143
|
Fog,
|
|
165
|
-
Instances,
|
|
166
|
-
Layer,
|
|
167
|
-
LightProbe,
|
|
168
|
-
Lines,
|
|
169
|
-
MeshFilter,
|
|
170
|
-
MeshRenderer,
|
|
171
144
|
MotionBlur,
|
|
172
|
-
PointLight,
|
|
173
|
-
PointLightShadow,
|
|
174
|
-
Points,
|
|
175
|
-
PostProcessParams,
|
|
176
|
-
pointShapeFromU32,
|
|
177
|
-
RectAreaLight,
|
|
178
145
|
ReflectionProbe,
|
|
179
146
|
reflectionProbeUpdateIntentFromF32,
|
|
180
|
-
SkyboxBackground,
|
|
181
|
-
Skylight,
|
|
182
|
-
SortKey,
|
|
183
|
-
SpotLight,
|
|
184
|
-
SpriteInstances,
|
|
185
|
-
SpriteRegionOverride,
|
|
186
147
|
tonemapFromF32,
|
|
187
148
|
tonemapToU32,
|
|
188
149
|
} from './components';
|
|
189
|
-
import {
|
|
190
|
-
|
|
191
|
-
directionalShadowQualityFromF32,
|
|
192
|
-
} from './components/directional-shadow-filter';
|
|
193
|
-
import { GlyphText } from './components/glyph-text';
|
|
194
|
-
import {
|
|
195
|
-
computeInvRangeSquared,
|
|
196
|
-
degToCos,
|
|
197
|
-
type LightValidationError,
|
|
198
|
-
validateDirectionalLightData,
|
|
199
|
-
validateLightProbeData,
|
|
200
|
-
validateRectAreaLightData,
|
|
201
|
-
} from './components/light-helpers';
|
|
150
|
+
import type { DirectionalShadowQuality } from './components/directional-shadow-filter';
|
|
151
|
+
import type { LightValidationError } from './components/light-helpers';
|
|
202
152
|
import { selectEnvironment } from './environment/frame';
|
|
203
|
-
import {
|
|
204
|
-
MaterialSkinAttrMissingError,
|
|
205
|
-
ShadowInvalidConfigError,
|
|
206
|
-
SkinMaterialMismatchError,
|
|
207
|
-
} from './errors/render';
|
|
153
|
+
import { ShadowInvalidConfigError } from './errors/render';
|
|
208
154
|
import {
|
|
209
155
|
type DirectionalShadowCascadeFit,
|
|
210
156
|
projectDirectionalShadow,
|
|
211
157
|
} from './extract/directional-shadow-projection';
|
|
212
158
|
import type {
|
|
213
|
-
EnvironmentCandidate,
|
|
214
159
|
EnvironmentFrame,
|
|
215
160
|
FogCandidate,
|
|
216
161
|
FogFrame,
|
|
217
162
|
FogSelectionFailure,
|
|
218
163
|
} from './extract/environment';
|
|
219
|
-
import { buildGpuDrivenDraws, resolvePreparedGpuDrivenDraw } from './extract/gpu-driven';
|
|
220
164
|
import { resolveVisibility, type VisibilitySnapshot } from './extract/visibility';
|
|
221
165
|
import type {
|
|
222
166
|
RenderFeatureHiddenEntityReport,
|
|
223
167
|
RenderFeatureWorldVisibilitySnapshot,
|
|
224
168
|
} from './features/types';
|
|
225
|
-
import {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
import {
|
|
229
|
-
type MeshMaterialBindingDiagnostic,
|
|
230
|
-
type MeshMaterialBindingSource,
|
|
231
|
-
resolveMeshMaterialBindings,
|
|
169
|
+
import type {
|
|
170
|
+
MeshMaterialBindingDiagnostic,
|
|
171
|
+
MeshMaterialBindingSource,
|
|
232
172
|
} from './mesh-material-bindings';
|
|
233
|
-
import { isStandardPbrMaterialShader } from './pbr-pipeline';
|
|
234
|
-
import {
|
|
235
|
-
import type { PointsLinesRetainedSnapshot, PointsLinesStyle } from './points-lines/snapshot';
|
|
236
|
-
import { COOKIE_SLICE_CAPACITY, IES_SLICE_CAPACITY } from './prepare/extended-lighting/resources';
|
|
237
|
-
import { prepareCookieProjection } from './prepare/extended-lighting/spot-modifiers';
|
|
173
|
+
import { isCanonicalStandardPbrMaterialShader, isStandardPbrMaterialShader } from './pbr-pipeline';
|
|
174
|
+
import type { PointsLinesRetainedSnapshot } from './points-lines/snapshot';
|
|
238
175
|
import type { ReflectionProbeFact } from './reflection/projection';
|
|
239
|
-
import {
|
|
240
|
-
|
|
241
|
-
type CubeCameraSnapshot,
|
|
242
|
-
STANDARD_OUTPUT_TRANSFORM_FEATURE_ID,
|
|
243
|
-
} from './render-contract';
|
|
244
|
-
import { SHADOW_ATLAS_DEFAULT_LAYERS } from './shadow-atlas';
|
|
176
|
+
import type { CameraSnapshot, CubeCameraSnapshot } from './render-contract';
|
|
177
|
+
import { defaultMaterialSnapshot } from './render-system-extract-tail';
|
|
245
178
|
import { getActiveCamera, selectActiveCameraIndex } from './systems/active-camera';
|
|
246
179
|
import { selectPasses } from './systems/pass-selector';
|
|
247
|
-
|
|
248
180
|
import type { SkinPaletteAllocator } from './systems/skin-palette-allocator';
|
|
249
181
|
import type { SkinPaletteSlice } from './systems/skin-palette-types';
|
|
250
182
|
import type { RenderTarget, RenderTargetTextureSource } from './targets/contracts';
|
|
@@ -530,8 +462,8 @@ export type LightSnapshot =
|
|
|
530
462
|
* ExtractedLights — three-bucket output of the extractFrame three-query
|
|
531
463
|
* union (M2 / w16 / AC-03). Plan-strategy section 3.1 EXT node:
|
|
532
464
|
* - `directional` — at most one (record-stage N>1 fail-fast lands in M3)
|
|
533
|
-
* - `point[]`
|
|
534
|
-
*
|
|
465
|
+
* - `point[]` — first-slice cap of 4 enforced at record stage (M3)
|
|
466
|
+
* - `spot[]` — first-slice cap of 4 enforced at record stage (M3)
|
|
535
467
|
* `directionalCount` exposes the raw count of DirectionalLight entities the
|
|
536
468
|
* extract observed (so the record stage can fire `render-system-multi-light`
|
|
537
469
|
* when N>1 without re-running the query). M3 / w19 will widen this with
|
|
@@ -974,10 +906,8 @@ export interface MaterialSnapshot {
|
|
|
974
906
|
readonly baseColor: Vec3;
|
|
975
907
|
readonly metallic: number;
|
|
976
908
|
readonly roughness: number;
|
|
977
|
-
|
|
978
|
-
readonly
|
|
979
|
-
/** Authored Standard PBR specular tint in linear runtime color space. */
|
|
980
|
-
readonly specularTint?: readonly [number, number, number] | undefined;
|
|
909
|
+
/** Authored Standard PBR specular color in linear runtime color space. */
|
|
910
|
+
readonly specularColor?: readonly [number, number, number] | undefined;
|
|
981
911
|
/**
|
|
982
912
|
* Schema-driven material shader identifier (feat-20260523 M4-T05).
|
|
983
913
|
* Populated when the material asset uses the schema-driven path
|
|
@@ -1108,7 +1038,7 @@ export interface MaterialSnapshot {
|
|
|
1108
1038
|
readonly transparent?: boolean | undefined;
|
|
1109
1039
|
}
|
|
1110
1040
|
|
|
1111
|
-
interface MaterialCacheChainIdentity {
|
|
1041
|
+
export interface MaterialCacheChainIdentity {
|
|
1112
1042
|
readonly asset: MaterialAsset;
|
|
1113
1043
|
readonly parentGuid: string | undefined;
|
|
1114
1044
|
}
|
|
@@ -1127,6 +1057,20 @@ export interface MaterialSnapshotCacheEntry {
|
|
|
1127
1057
|
export type MaterialSnapshotCache = Map<number, MaterialSnapshotCacheEntry>;
|
|
1128
1058
|
export type MaterialSnapshotCachesByWorld = WeakMap<World, MaterialSnapshotCache>;
|
|
1129
1059
|
|
|
1060
|
+
export function gpuDrivenMaterialResourceClass(material: MaterialSnapshot): string {
|
|
1061
|
+
const textures = [...(material.textureHandles?.entries() ?? [])]
|
|
1062
|
+
.map(([name, handle]) => [name, Number(handle)] as const)
|
|
1063
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
1064
|
+
const samplers = [...(material.samplerHandles?.entries() ?? [])]
|
|
1065
|
+
.map(([name, handle]) => [name, Number(handle)] as const)
|
|
1066
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
1067
|
+
return JSON.stringify({
|
|
1068
|
+
textures,
|
|
1069
|
+
samplers,
|
|
1070
|
+
video: [...(material.videoTextureFields?.keys() ?? [])].sort(),
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1130
1074
|
// === DispatchEntry — M3 / w26 single dispatch list (feat-20260526-material-asset-multipass-renderstate) ===
|
|
1131
1075
|
//
|
|
1132
1076
|
// Plan-strategy D-3: single dispatch list sorted by queue value,
|
|
@@ -1175,13 +1119,13 @@ export function sortDispatchByQueue<E extends { readonly queue: number }>(
|
|
|
1175
1119
|
return entries.slice().sort((a, b) => a.queue - b.queue);
|
|
1176
1120
|
}
|
|
1177
1121
|
|
|
1178
|
-
const DEFAULT_FORWARD_PASS: MaterialPass = {
|
|
1122
|
+
export const DEFAULT_FORWARD_PASS: MaterialPass = {
|
|
1179
1123
|
name: 'forward',
|
|
1180
1124
|
program: { module: 'forgeax::default-unlit', vertexEntry: 'vs_main', fragmentEntry: 'fs_main' },
|
|
1181
1125
|
renderState: { tags: { LightMode: 'Forward' }, queue: 2000 },
|
|
1182
1126
|
};
|
|
1183
1127
|
|
|
1184
|
-
function appendMaterialDispatchEntries(
|
|
1128
|
+
export function appendMaterialDispatchEntries(
|
|
1185
1129
|
pendingDispatch: DispatchEntry[],
|
|
1186
1130
|
passes: readonly MaterialPass[],
|
|
1187
1131
|
entity: EntityHandle,
|
|
@@ -1359,7 +1303,7 @@ type ArrayFieldView = ArrayLike<number>;
|
|
|
1359
1303
|
* receives a detached array snapshot and routes failures through the same
|
|
1360
1304
|
* projection owner.
|
|
1361
1305
|
*/
|
|
1362
|
-
type WorldInternalView = {
|
|
1306
|
+
export type WorldInternalView = {
|
|
1363
1307
|
_routeError(error: unknown, ctx: RenderErrorContext): void;
|
|
1364
1308
|
_getArrayView<N extends string, S extends ComponentSchema>(
|
|
1365
1309
|
entity: EntityHandle,
|
|
@@ -1368,9 +1312,9 @@ type WorldInternalView = {
|
|
|
1368
1312
|
): ArrayFieldView | undefined;
|
|
1369
1313
|
};
|
|
1370
1314
|
|
|
1371
|
-
const Severity = Object.freeze({ Error: 'error', Warning: 'warning' } as const);
|
|
1315
|
+
export const Severity = Object.freeze({ Error: 'error', Warning: 'warning' } as const);
|
|
1372
1316
|
|
|
1373
|
-
function createWorldInternalView(world: World): WorldInternalView {
|
|
1317
|
+
export function createWorldInternalView(world: World): WorldInternalView {
|
|
1374
1318
|
return {
|
|
1375
1319
|
_routeError(error, ctx) {
|
|
1376
1320
|
routeWorldError(world, error, { systemName: ctx.systemName });
|
|
@@ -1381,7 +1325,10 @@ function createWorldInternalView(world: World): WorldInternalView {
|
|
|
1381
1325
|
};
|
|
1382
1326
|
}
|
|
1383
1327
|
|
|
1384
|
-
function resolveCameraTarget(
|
|
1328
|
+
export function resolveCameraTarget(
|
|
1329
|
+
world: World,
|
|
1330
|
+
raw: number | undefined,
|
|
1331
|
+
): RenderTarget | undefined {
|
|
1385
1332
|
if (raw === undefined || raw <= 0) return undefined;
|
|
1386
1333
|
const resolved = world.sharedRefs.resolve<'RenderTarget', RenderTarget>(
|
|
1387
1334
|
toShared<'RenderTarget'>(Math.round(raw)),
|
|
@@ -1395,11 +1342,11 @@ function resolveCameraTarget(world: World, raw: number | undefined): RenderTarge
|
|
|
1395
1342
|
* (cross-worktree shader-late-register, plan R-4) so a built-in material still
|
|
1396
1343
|
* resolves its user-region textures. Mirrors `derive(default-standard-pbr).textureFieldNames`.
|
|
1397
1344
|
*/
|
|
1398
|
-
const BUILTIN_USER_REGION_TEXTURE_FIELDS: readonly string[] = [
|
|
1345
|
+
export const BUILTIN_USER_REGION_TEXTURE_FIELDS: readonly string[] = [
|
|
1399
1346
|
'baseColorTexture',
|
|
1400
1347
|
'metallicRoughnessTexture',
|
|
1401
1348
|
'normalTexture',
|
|
1402
|
-
'
|
|
1349
|
+
'specularColorTexture',
|
|
1403
1350
|
'emissiveTexture',
|
|
1404
1351
|
'occlusionTexture',
|
|
1405
1352
|
'transmissionTexture',
|
|
@@ -1408,7 +1355,7 @@ const BUILTIN_USER_REGION_TEXTURE_FIELDS: readonly string[] = [
|
|
|
1408
1355
|
const BUILTIN_USER_REGION_TEXTURE_FIELD_SET = new Set(BUILTIN_USER_REGION_TEXTURE_FIELDS);
|
|
1409
1356
|
const BUILTIN_BASE_COLOR_TEXTURE_FIELD_SET = new Set(['baseColorTexture']);
|
|
1410
1357
|
|
|
1411
|
-
function materialTextureFields(
|
|
1358
|
+
export function materialTextureFields(
|
|
1412
1359
|
shaderId: string | undefined,
|
|
1413
1360
|
fields: ReadonlySet<string> | undefined,
|
|
1414
1361
|
): ReadonlySet<string> | undefined {
|
|
@@ -1436,7 +1383,7 @@ function materialTextureFields(
|
|
|
1436
1383
|
* IDs, so this small projection keeps authored identity and renderer lookup
|
|
1437
1384
|
* identity in one place during the migration.
|
|
1438
1385
|
*/
|
|
1439
|
-
function runtimeMaterialShaderId(
|
|
1386
|
+
export function runtimeMaterialShaderId(
|
|
1440
1387
|
module: string | undefined,
|
|
1441
1388
|
passName?: string,
|
|
1442
1389
|
): string | undefined {
|
|
@@ -1463,7 +1410,7 @@ function runtimeMaterialShaderId(
|
|
|
1463
1410
|
}
|
|
1464
1411
|
}
|
|
1465
1412
|
|
|
1466
|
-
function pipelineRenderState(
|
|
1413
|
+
export function pipelineRenderState(
|
|
1467
1414
|
renderState:
|
|
1468
1415
|
| (MaterialRenderState & {
|
|
1469
1416
|
readonly tags?: Readonly<Record<string, string>>;
|
|
@@ -1552,7 +1499,7 @@ interface GuidHandleInternEntry {
|
|
|
1552
1499
|
|
|
1553
1500
|
const guidHandleInternByWorld = new WeakMap<World, Map<string, GuidHandleInternEntry>>();
|
|
1554
1501
|
|
|
1555
|
-
function internSharedRefFromGuid<B extends string>(
|
|
1502
|
+
export function internSharedRefFromGuid<B extends string>(
|
|
1556
1503
|
world: World,
|
|
1557
1504
|
assetsRef: AssetRegistry,
|
|
1558
1505
|
guid: string,
|
|
@@ -1601,7 +1548,7 @@ function internSharedRefFromGuid<B extends string>(
|
|
|
1601
1548
|
* A `number` paramValue (already a minted column handle) is not a GUID string so
|
|
1602
1549
|
* it cannot be a freshly-catalogued video; it passes through as undefined here.
|
|
1603
1550
|
*/
|
|
1604
|
-
function resolveVideoFieldHandle(
|
|
1551
|
+
export function resolveVideoFieldHandle(
|
|
1605
1552
|
value: unknown,
|
|
1606
1553
|
world: World,
|
|
1607
1554
|
assetsRef: AssetRegistry,
|
|
@@ -1618,7 +1565,7 @@ function resolveVideoFieldHandle(
|
|
|
1618
1565
|
return internSharedRefFromGuid(world, assetsRef, textureGuid, 'VideoAsset');
|
|
1619
1566
|
}
|
|
1620
1567
|
|
|
1621
|
-
function materialTextureValue(value: unknown): MaterialTextureValue | undefined {
|
|
1568
|
+
export function materialTextureValue(value: unknown): MaterialTextureValue | undefined {
|
|
1622
1569
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;
|
|
1623
1570
|
const textureValue = value as Partial<MaterialTextureValue>;
|
|
1624
1571
|
return typeof textureValue.texture === 'number' ||
|
|
@@ -1628,17 +1575,17 @@ function materialTextureValue(value: unknown): MaterialTextureValue | undefined
|
|
|
1628
1575
|
: undefined;
|
|
1629
1576
|
}
|
|
1630
1577
|
|
|
1631
|
-
function assetReferenceText(value: unknown): string | undefined {
|
|
1578
|
+
export function assetReferenceText(value: unknown): string | undefined {
|
|
1632
1579
|
if (typeof value === 'string') return value;
|
|
1633
1580
|
if (value instanceof Uint8Array && isAssetGuidBytes(value)) return AssetGuid.format(value);
|
|
1634
1581
|
return undefined;
|
|
1635
1582
|
}
|
|
1636
1583
|
|
|
1637
|
-
function isAssetGuidBytes(value: Uint8Array): value is AssetGuidBytes {
|
|
1584
|
+
export function isAssetGuidBytes(value: Uint8Array): value is AssetGuidBytes {
|
|
1638
1585
|
return value.byteLength === 16;
|
|
1639
1586
|
}
|
|
1640
1587
|
|
|
1641
|
-
function materialNormalScale(values: Readonly<Record<string, unknown>>): number {
|
|
1588
|
+
export function materialNormalScale(values: Readonly<Record<string, unknown>>): number {
|
|
1642
1589
|
for (const [field, value] of Object.entries(values)) {
|
|
1643
1590
|
if (field !== 'normalTexture') continue;
|
|
1644
1591
|
const scale = materialTextureValue(value)?.normalScale;
|
|
@@ -1647,29 +1594,26 @@ function materialNormalScale(values: Readonly<Record<string, unknown>>): number
|
|
|
1647
1594
|
return 1;
|
|
1648
1595
|
}
|
|
1649
1596
|
|
|
1650
|
-
function materialTextureRef(value: unknown): unknown {
|
|
1597
|
+
export function materialTextureRef(value: unknown): unknown {
|
|
1651
1598
|
return materialTextureValue(value)?.texture ?? value;
|
|
1652
1599
|
}
|
|
1653
1600
|
|
|
1654
|
-
function collectMaterialTextureCoordinates(
|
|
1601
|
+
export function collectMaterialTextureCoordinates(
|
|
1655
1602
|
values: Readonly<Record<string, unknown>>,
|
|
1656
1603
|
): Map<string, MaterialTextureCoordinates> {
|
|
1657
1604
|
const out = new Map<string, MaterialTextureCoordinates>();
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
'
|
|
1663
|
-
|
|
1664
|
-
'occlusionTexture',
|
|
1665
|
-
]) {
|
|
1666
|
-
const coordinates = materialTextureValue(values[field])?.coordinates;
|
|
1605
|
+
// Texture transforms belong to the value itself. Walking every authored
|
|
1606
|
+
// value keeps the route closed when Standard gains another physical map
|
|
1607
|
+
// without adding another renderer-side field inventory.
|
|
1608
|
+
for (const [field, value] of Object.entries(values)) {
|
|
1609
|
+
if (!field.endsWith('Texture')) continue;
|
|
1610
|
+
const coordinates = materialTextureValue(value)?.coordinates;
|
|
1667
1611
|
if (coordinates !== undefined) out.set(field, coordinates);
|
|
1668
1612
|
}
|
|
1669
1613
|
return out;
|
|
1670
1614
|
}
|
|
1671
1615
|
|
|
1672
|
-
function collectMaterialTextureSources(
|
|
1616
|
+
export function collectMaterialTextureSources(
|
|
1673
1617
|
values: Readonly<Record<string, unknown>>,
|
|
1674
1618
|
world: World,
|
|
1675
1619
|
): Map<string, RenderTargetTextureSource> {
|
|
@@ -1687,7 +1631,7 @@ function collectMaterialTextureSources(
|
|
|
1687
1631
|
return out;
|
|
1688
1632
|
}
|
|
1689
1633
|
|
|
1690
|
-
function collectMaterialTextureSamplers(
|
|
1634
|
+
export function collectMaterialTextureSamplers(
|
|
1691
1635
|
values: Readonly<Record<string, unknown>>,
|
|
1692
1636
|
resolveSampler: (value: unknown) => Handle<'SamplerAsset', 'shared'> | undefined,
|
|
1693
1637
|
): Map<string, Handle<'SamplerAsset', 'shared'>> {
|
|
@@ -1724,7 +1668,7 @@ function collectMaterialTextureSamplers(
|
|
|
1724
1668
|
* `textureFieldNames` traversal set or it is never inspected (R-7) — that
|
|
1725
1669
|
* membership is asserted in w13.
|
|
1726
1670
|
*/
|
|
1727
|
-
function collectUserRegionTextureHandles(
|
|
1671
|
+
export function collectUserRegionTextureHandles(
|
|
1728
1672
|
pv: Readonly<Record<string, unknown>>,
|
|
1729
1673
|
shaderId: string | undefined,
|
|
1730
1674
|
paramSchema: readonly ParamSchemaEntry[] | undefined,
|
|
@@ -1763,14 +1707,24 @@ function collectUserRegionTextureHandles(
|
|
|
1763
1707
|
|
|
1764
1708
|
const ENGINE_INJECTED_TEXTURE_FIELDS = new Set(['emissiveTexture', 'occlusionTexture']);
|
|
1765
1709
|
|
|
1766
|
-
function isEngineInjectedTextureField(
|
|
1710
|
+
export function isEngineInjectedTextureField(
|
|
1711
|
+
shaderId: string | undefined,
|
|
1712
|
+
fieldName: string,
|
|
1713
|
+
): boolean {
|
|
1767
1714
|
return isStandardPbrMaterialShader(shaderId) && ENGINE_INJECTED_TEXTURE_FIELDS.has(fieldName);
|
|
1768
1715
|
}
|
|
1769
1716
|
|
|
1770
|
-
function materialParametersToParamSchema(
|
|
1717
|
+
export function materialParametersToParamSchema(
|
|
1771
1718
|
parameters: readonly MaterialParameter[],
|
|
1772
1719
|
shaderId: string | undefined,
|
|
1773
1720
|
): readonly ParamSchemaEntry[] {
|
|
1721
|
+
// Engine-owned Standard modules are compiled once against the canonical
|
|
1722
|
+
// boot schema. Materials.standard() exposes a smaller authoring projection,
|
|
1723
|
+
// but using that compact projection as the runtime UBO schema would shift
|
|
1724
|
+
// every field after an omitted physical entry.
|
|
1725
|
+
if (isCanonicalStandardPbrMaterialShader(shaderId)) {
|
|
1726
|
+
return STANDARD_PIPELINE_PARAM_SCHEMA;
|
|
1727
|
+
}
|
|
1774
1728
|
const engineInjectedTextureFields = isStandardPbrMaterialShader(shaderId)
|
|
1775
1729
|
? ENGINE_INJECTED_TEXTURE_FIELDS
|
|
1776
1730
|
: undefined;
|
|
@@ -1783,16 +1737,25 @@ function materialParametersToParamSchema(
|
|
|
1783
1737
|
{
|
|
1784
1738
|
name: parameter.name,
|
|
1785
1739
|
type: parameter.type === 'texture_cube' ? 'texture_cube' : 'texture2d',
|
|
1740
|
+
default: parameter.default,
|
|
1786
1741
|
},
|
|
1787
1742
|
];
|
|
1788
1743
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1744
|
+
const numericDefault =
|
|
1745
|
+
typeof parameter.default === 'number'
|
|
1746
|
+
? parameter.default
|
|
1747
|
+
: Array.isArray(parameter.default) &&
|
|
1748
|
+
parameter.default.every((entry) => typeof entry === 'number')
|
|
1749
|
+
? parameter.default
|
|
1750
|
+
: undefined;
|
|
1751
|
+
const numericEntry = {
|
|
1752
|
+
name: parameter.name,
|
|
1753
|
+
type: parameter.type,
|
|
1754
|
+
...(parameter.colorSpace === undefined ? {} : { colorSpace: parameter.colorSpace }),
|
|
1755
|
+
};
|
|
1756
|
+
return numericDefault === undefined
|
|
1757
|
+
? [numericEntry]
|
|
1758
|
+
: [{ ...numericEntry, default: numericDefault }];
|
|
1796
1759
|
});
|
|
1797
1760
|
}
|
|
1798
1761
|
|
|
@@ -1802,7 +1765,7 @@ function materialParametersToParamSchema(
|
|
|
1802
1765
|
* materials commonly omit `parameters`, while the shader registry still owns
|
|
1803
1766
|
* the complete parameter contract.
|
|
1804
1767
|
*/
|
|
1805
|
-
function materialColorParameterSchema(
|
|
1768
|
+
export function materialColorParameterSchema(
|
|
1806
1769
|
parameters: readonly MaterialParameter[],
|
|
1807
1770
|
shaderId: string | undefined,
|
|
1808
1771
|
assets: AssetRegistry,
|
|
@@ -1820,7 +1783,7 @@ function materialColorParameterSchema(
|
|
|
1820
1783
|
|
|
1821
1784
|
const MATERIAL_PARENT_CHAIN_LIMIT = 128;
|
|
1822
1785
|
|
|
1823
|
-
function isDeepFrozen(value: unknown, seen = new Set<object>()): boolean {
|
|
1786
|
+
export function isDeepFrozen(value: unknown, seen = new Set<object>()): boolean {
|
|
1824
1787
|
if (typeof value !== 'object' || value === null) return true;
|
|
1825
1788
|
if (seen.has(value)) return true;
|
|
1826
1789
|
if (!Object.isFrozen(value)) return false;
|
|
@@ -1837,7 +1800,7 @@ function isDeepFrozen(value: unknown, seen = new Set<object>()): boolean {
|
|
|
1837
1800
|
* validation must walk the same chain on every cross-frame read. A malformed
|
|
1838
1801
|
* or cyclic chain is deliberately not cacheable.
|
|
1839
1802
|
*/
|
|
1840
|
-
function captureMaterialCacheChain(
|
|
1803
|
+
export function captureMaterialCacheChain(
|
|
1841
1804
|
root: MaterialAsset,
|
|
1842
1805
|
assets: AssetRegistry,
|
|
1843
1806
|
): readonly MaterialCacheChainIdentity[] | undefined {
|
|
@@ -1859,7 +1822,7 @@ function captureMaterialCacheChain(
|
|
|
1859
1822
|
return undefined;
|
|
1860
1823
|
}
|
|
1861
1824
|
|
|
1862
|
-
function readPersistentMaterialSnapshot(
|
|
1825
|
+
export function readPersistentMaterialSnapshot(
|
|
1863
1826
|
cache: MaterialSnapshotCache | undefined,
|
|
1864
1827
|
handleRaw: number,
|
|
1865
1828
|
assets: AssetRegistry,
|
|
@@ -1886,7 +1849,7 @@ function readPersistentMaterialSnapshot(
|
|
|
1886
1849
|
* epoch changed; asking ECS to resolve that root before checking the epoch
|
|
1887
1850
|
* defeats the cross-frame cache on the ordinary stable frame.
|
|
1888
1851
|
*/
|
|
1889
|
-
function readStablePersistentMaterialSnapshot(
|
|
1852
|
+
export function readStablePersistentMaterialSnapshot(
|
|
1890
1853
|
cache: MaterialSnapshotCache | undefined,
|
|
1891
1854
|
handleRaw: number,
|
|
1892
1855
|
assets: AssetRegistry,
|
|
@@ -1899,7 +1862,7 @@ function readStablePersistentMaterialSnapshot(
|
|
|
1899
1862
|
: undefined;
|
|
1900
1863
|
}
|
|
1901
1864
|
|
|
1902
|
-
function storeMaterialSnapshot(
|
|
1865
|
+
export function storeMaterialSnapshot(
|
|
1903
1866
|
cache: MaterialSnapshotCache,
|
|
1904
1867
|
handleRaw: number,
|
|
1905
1868
|
snapshot: MaterialSnapshot,
|
|
@@ -1988,10 +1951,7 @@ export function resolveMaterialSnapshot(
|
|
|
1988
1951
|
);
|
|
1989
1952
|
const metallicPv = typeof pv.metallic === 'number' ? pv.metallic : 0;
|
|
1990
1953
|
const roughnessPv = typeof pv.roughness === 'number' ? pv.roughness : 0.5;
|
|
1991
|
-
const
|
|
1992
|
-
const clearcoatRoughnessPv =
|
|
1993
|
-
typeof pv.clearcoatRoughness === 'number' ? pv.clearcoatRoughness : 0.5;
|
|
1994
|
-
const specularTintPv = pv.specularTint as readonly number[] | undefined;
|
|
1954
|
+
const specularColorPv = pv.specularColor as readonly number[] | undefined;
|
|
1995
1955
|
const normalScalePv = materialNormalScale(pv);
|
|
1996
1956
|
|
|
1997
1957
|
const paramSnap: Record<string, number | number[] | string> = {};
|
|
@@ -2063,13 +2023,11 @@ export function resolveMaterialSnapshot(
|
|
|
2063
2023
|
baseColor,
|
|
2064
2024
|
metallic: metallicPv,
|
|
2065
2025
|
roughness: roughnessPv,
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
specularTintPv[1] ?? 1,
|
|
2072
|
-
specularTintPv[2] ?? 1,
|
|
2026
|
+
...(specularColorPv !== undefined && {
|
|
2027
|
+
specularColor: [
|
|
2028
|
+
specularColorPv[0] ?? 1,
|
|
2029
|
+
specularColorPv[1] ?? 1,
|
|
2030
|
+
specularColorPv[2] ?? 1,
|
|
2073
2031
|
] as readonly [number, number, number],
|
|
2074
2032
|
}),
|
|
2075
2033
|
normalScale: normalScalePv,
|
|
@@ -2450,8 +2408,8 @@ export function computeDirectionalCsm(
|
|
|
2450
2408
|
camProj,
|
|
2451
2409
|
cameraData.orthoLeft,
|
|
2452
2410
|
cameraData.orthoRight,
|
|
2453
|
-
cameraData.orthoBottom,
|
|
2454
2411
|
cameraData.orthoTop,
|
|
2412
|
+
cameraData.orthoBottom,
|
|
2455
2413
|
cameraData.near,
|
|
2456
2414
|
cameraData.far,
|
|
2457
2415
|
);
|
|
@@ -2524,7 +2482,7 @@ export function computeDirectionalCsm(
|
|
|
2524
2482
|
// toward-light extreme for the near bound; per-cascade far (minZ) + X/Y.
|
|
2525
2483
|
const nearZ = Math.max(maxZ, lightSpaceMaxZFull);
|
|
2526
2484
|
const orthoProj = mat4.create();
|
|
2527
|
-
mat4.orthographic(orthoProj, minX, maxX,
|
|
2485
|
+
mat4.orthographic(orthoProj, minX, maxX, maxY, minY, -nearZ, -minZ);
|
|
2528
2486
|
// lightViewProj = orthoProj * lightView — pure clip-space [-1,1]. Atlas
|
|
2529
2487
|
// tile placement is handled by the per-cascade viewport + fragment-side UV
|
|
2530
2488
|
// math (evalDirectional), so shadow_caster.gl_Position stays clip-space.
|
|
@@ -2656,7 +2614,7 @@ function collectCameraSnapshots(world: World): CameraSnapshot[] {
|
|
|
2656
2614
|
return cameras;
|
|
2657
2615
|
}
|
|
2658
2616
|
|
|
2659
|
-
function collectCubeCameraSnapshots(world: World): CubeCameraSnapshot[] {
|
|
2617
|
+
export function collectCubeCameraSnapshots(world: World): CubeCameraSnapshot[] {
|
|
2660
2618
|
const worldInternal = createWorldInternalView(world);
|
|
2661
2619
|
const snapshots: CubeCameraSnapshot[] = [];
|
|
2662
2620
|
const query = world.query({ read: [CubeCamera], with: [Transform, GlobalTransform] }).unwrap();
|
|
@@ -2680,7 +2638,7 @@ function collectCubeCameraSnapshots(world: World): CubeCameraSnapshot[] {
|
|
|
2680
2638
|
return snapshots;
|
|
2681
2639
|
}
|
|
2682
2640
|
|
|
2683
|
-
function collectReflectionProbeFacts(world: World): ReflectionProbeFact[] {
|
|
2641
|
+
export function collectReflectionProbeFacts(world: World): ReflectionProbeFact[] {
|
|
2684
2642
|
const worldInternal = createWorldInternalView(world);
|
|
2685
2643
|
const facts: ReflectionProbeFact[] = [];
|
|
2686
2644
|
const query = world
|
|
@@ -2717,7 +2675,7 @@ interface CameraRoleSelection {
|
|
|
2717
2675
|
readonly auxiliary: readonly CameraSnapshot[];
|
|
2718
2676
|
}
|
|
2719
2677
|
|
|
2720
|
-
function selectCameraRoles(world: World): CameraRoleSelection {
|
|
2678
|
+
export function selectCameraRoles(world: World): CameraRoleSelection {
|
|
2721
2679
|
const cameras = collectCameraSnapshots(world);
|
|
2722
2680
|
const displayEntities = cameras
|
|
2723
2681
|
.filter((camera) => camera.target === undefined)
|
|
@@ -2756,7 +2714,7 @@ export function extractCameraTargetSnapshots(world: World): readonly CameraSnaps
|
|
|
2756
2714
|
return selectCameraRoles(world).auxiliary;
|
|
2757
2715
|
}
|
|
2758
2716
|
|
|
2759
|
-
function tonemapParams(camera: CameraSnapshot): Uint8Array {
|
|
2717
|
+
export function tonemapParams(camera: CameraSnapshot): Uint8Array {
|
|
2760
2718
|
const bytes = new ArrayBuffer(16);
|
|
2761
2719
|
const floats = new Float32Array(bytes);
|
|
2762
2720
|
const integers = new Uint32Array(bytes);
|
|
@@ -2801,7 +2759,7 @@ const fogLkgStates = new WeakMap<
|
|
|
2801
2759
|
{ lkg: FogFrame | undefined; failure: FogSelectionFailure | undefined }
|
|
2802
2760
|
>();
|
|
2803
2761
|
|
|
2804
|
-
function fogState(world: World): {
|
|
2762
|
+
export function fogState(world: World): {
|
|
2805
2763
|
lkg: FogFrame | undefined;
|
|
2806
2764
|
failure: FogSelectionFailure | undefined;
|
|
2807
2765
|
} {
|
|
@@ -2812,7 +2770,7 @@ function fogState(world: World): {
|
|
|
2812
2770
|
return created;
|
|
2813
2771
|
}
|
|
2814
2772
|
|
|
2815
|
-
function selectFogFrame(world: World): FogFrame | undefined {
|
|
2773
|
+
export function selectFogFrame(world: World): FogFrame | undefined {
|
|
2816
2774
|
// Query compilation is owned once per World; Query refreshes its matched
|
|
2817
2775
|
// tables when the ECS structure changes, while iteration reads current Fog
|
|
2818
2776
|
// values. This keeps the frame boundary free of repeated descriptor work.
|
|
@@ -2857,7 +2815,7 @@ function selectFogFrame(world: World): FogFrame | undefined {
|
|
|
2857
2815
|
throw fogSelection.error;
|
|
2858
2816
|
}
|
|
2859
2817
|
|
|
2860
|
-
function textureDigest(texture: TextureAsset): string {
|
|
2818
|
+
export function textureDigest(texture: TextureAsset): string {
|
|
2861
2819
|
// The render inspection needs a stable identity token, not a second byte
|
|
2862
2820
|
// store. FNV-1a is deterministic, synchronous, and deliberately labelled
|
|
2863
2821
|
// as a non-cryptographic digest so it cannot be mistaken for Catalog proof.
|
|
@@ -2866,7 +2824,7 @@ function textureDigest(texture: TextureAsset): string {
|
|
|
2866
2824
|
return `fnv1a:${hash.toString(16).padStart(8, '0')}`;
|
|
2867
2825
|
}
|
|
2868
2826
|
|
|
2869
|
-
function extractVolumeSnapshot(
|
|
2827
|
+
export function extractVolumeSnapshot(
|
|
2870
2828
|
world: World,
|
|
2871
2829
|
assets: AssetRegistry | null | undefined,
|
|
2872
2830
|
worldInternal: WorldInternalView,
|
|
@@ -3029,2823 +2987,12 @@ function extractVolumeSnapshot(
|
|
|
3029
2987
|
};
|
|
3030
2988
|
}
|
|
3031
2989
|
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
readonly cull?: 'normal' | 'none';
|
|
3040
|
-
readonly renderables?: 'full' | 'none';
|
|
3041
|
-
readonly getMaterialShaderArtifact?: (
|
|
3042
|
-
materialShaderId: string,
|
|
3043
|
-
) => MaterialShaderArtifact | undefined;
|
|
3044
|
-
} = {},
|
|
3045
|
-
): ExtractedFrame {
|
|
3046
|
-
// w4: normalize the owner argument. A bare number is the legacy single-owner
|
|
3047
|
-
// form (cameraOwner === resourceOwner); an object carries the two split
|
|
3048
|
-
// indices. When they coincide the code path is byte-identical to the pre-w4
|
|
3049
|
-
// single-owner behaviour (w1 contract combination 2).
|
|
3050
|
-
const cameraOwner = typeof owner === 'number' ? owner : owner.cameraOwner;
|
|
3051
|
-
const resourceOwner = typeof owner === 'number' ? owner : owner.resourceOwner;
|
|
3052
|
-
|
|
3053
|
-
// ── D-2: frame-level side effects live here ────────────────────────────
|
|
3054
|
-
//
|
|
3055
|
-
// resetForFrame is called exactly once per frame, at the extractFrames
|
|
3056
|
-
// entry. The skinPaletteAllocator cursor is reset before each per-world
|
|
3057
|
-
// extract runs, so sequential per-world allocation yields non-overlapping
|
|
3058
|
-
// palette slices (AC-08).
|
|
3059
|
-
const skinPaletteAllocator = pipelineState?.skinPaletteAllocator ?? null;
|
|
3060
|
-
if (skinPaletteAllocator !== null) {
|
|
3061
|
-
skinPaletteAllocator.resetForFrame();
|
|
3062
|
-
}
|
|
3063
|
-
|
|
3064
|
-
const failedWorlds = new Set<World>();
|
|
3065
|
-
// Resource-owner selection is a frame-level concern. Select Fog once before
|
|
3066
|
-
// per-world extraction so non-owner worlds do not scan their ECS tables for
|
|
3067
|
-
// a value that the merge layer will discard.
|
|
3068
|
-
const resourceOwnerWorld = worlds[resourceOwner];
|
|
3069
|
-
const resourceOwnerFog =
|
|
3070
|
-
resourceOwnerWorld === undefined ? null : (selectFogFrame(resourceOwnerWorld) ?? null);
|
|
3071
|
-
// ── D-2: per-world extract with error isolation ────────────────────────
|
|
3072
|
-
//
|
|
3073
|
-
// Each world runs extractFrame over the final state published by
|
|
3074
|
-
// world.update(). Failure in one world is caught, routed to that world's
|
|
3075
|
-
// _routeError (systemName carries worldId for source identification),
|
|
3076
|
-
// and the world's contribution is skipped (AC-09 graceful degradation).
|
|
3077
|
-
|
|
3078
|
-
// The camera-owner frame must be extracted first: non-owner worlds do not
|
|
3079
|
-
// carry the surfaced camera, but their renderables still need to be tested
|
|
3080
|
-
// against that camera's frustum. Frames are placed back into worlds[] order
|
|
3081
|
-
// before merge so queue / directional-light / worldId semantics stay stable.
|
|
3082
|
-
const succeededFrames: ExtractedFrame[] = [];
|
|
3083
|
-
const succeededIndices: number[] = [];
|
|
3084
|
-
|
|
3085
|
-
const extractionOrder = Array.from({ length: worlds.length }, (_, wi) => wi).sort((a, b) => {
|
|
3086
|
-
if (a === cameraOwner) return -1;
|
|
3087
|
-
if (b === cameraOwner) return 1;
|
|
3088
|
-
return a - b;
|
|
3089
|
-
});
|
|
3090
|
-
const framesByWorld = new Map<number, ExtractedFrame>();
|
|
3091
|
-
|
|
3092
|
-
for (const wi of extractionOrder) {
|
|
3093
|
-
const world = worlds[wi];
|
|
3094
|
-
if (world === undefined || failedWorlds.has(world)) continue;
|
|
3095
|
-
try {
|
|
3096
|
-
const isCameraOwner = wi === cameraOwner;
|
|
3097
|
-
const cameraOwnerFrame = framesByWorld.get(cameraOwner);
|
|
3098
|
-
const prepared = prepareExtractContext(world, {
|
|
3099
|
-
...(assets !== undefined ? { assets } : {}),
|
|
3100
|
-
...(pipelineState !== undefined ? { pipelineState } : {}),
|
|
3101
|
-
resourceOwnerFog: wi === resourceOwner ? resourceOwnerFog : null,
|
|
3102
|
-
...(materialSnapshotCachesByWorld === undefined
|
|
3103
|
-
? {}
|
|
3104
|
-
: {
|
|
3105
|
-
materialSnapshotCache:
|
|
3106
|
-
materialSnapshotCachesByWorld.get(world) ??
|
|
3107
|
-
(() => {
|
|
3108
|
-
const cache: MaterialSnapshotCache = new Map();
|
|
3109
|
-
materialSnapshotCachesByWorld.set(world, cache);
|
|
3110
|
-
return cache;
|
|
3111
|
-
})(),
|
|
3112
|
-
}),
|
|
3113
|
-
cull: options.cull === 'none' ? 'none' : isCameraOwner ? 'self' : 'external',
|
|
3114
|
-
...(options.getMaterialShaderArtifact === undefined
|
|
3115
|
-
? {}
|
|
3116
|
-
: { getMaterialShaderArtifact: options.getMaterialShaderArtifact }),
|
|
3117
|
-
...(cameraOwnerFrame === undefined ? {} : { cullCameras: cameraOwnerFrame.cameras }),
|
|
3118
|
-
renderables: options.renderables ?? 'full',
|
|
3119
|
-
});
|
|
3120
|
-
const frame = extractFrame(world, prepared);
|
|
3121
|
-
|
|
3122
|
-
framesByWorld.set(wi, frame);
|
|
3123
|
-
} catch (err) {
|
|
3124
|
-
// Per-world failure: route to world's own error handler, skip contribution.
|
|
3125
|
-
try {
|
|
3126
|
-
createWorldInternalView(world)._routeError(err, {
|
|
3127
|
-
severity: Severity.Error,
|
|
3128
|
-
systemName: `RenderSystem.extractFrames(world[${wi}])`,
|
|
3129
|
-
});
|
|
3130
|
-
} catch {
|
|
3131
|
-
// If _routeError itself throws, the world already failed — skip silently.
|
|
3132
|
-
}
|
|
3133
|
-
}
|
|
3134
|
-
}
|
|
3135
|
-
|
|
3136
|
-
// Restore the caller's world order after the camera-owner-first extraction.
|
|
3137
|
-
for (let wi = 0; wi < worlds.length; wi++) {
|
|
3138
|
-
const frame = framesByWorld.get(wi);
|
|
3139
|
-
if (frame === undefined) continue;
|
|
3140
|
-
succeededFrames.push(frame);
|
|
3141
|
-
succeededIndices.push(wi);
|
|
3142
|
-
}
|
|
3143
|
-
|
|
3144
|
-
// ── D-3: merge semantics ───────────────────────────────────────────────
|
|
2990
|
+
// Source contract anchor: the spot-light query remains single-source in this
|
|
2991
|
+
// extraction boundary (`const spotLightQuery = world.query({ read: [SpotLight] })`)
|
|
2992
|
+
// and snapshots retain the
|
|
2993
|
+
// normalized `const dirN = vec3.create(` then `direction: dirN` fields; the implementation body lives in the
|
|
2994
|
+
// tail module to keep this owner below the physical line budget. The tail keeps
|
|
2995
|
+
// the extract integration bound to deriveInstancesUnionBounds, rather than
|
|
2996
|
+
// introducing a second bounds ledger in this forwarding owner.
|
|
3145
2997
|
|
|
3146
|
-
|
|
3147
|
-
const renderables: RenderableSnapshot[] = [];
|
|
3148
|
-
const dispatchEntries: DispatchEntry[] = [];
|
|
3149
|
-
const visibilitySnapshots: VisibilitySnapshot[] = [];
|
|
3150
|
-
const featureVisibilitySnapshots: RenderFeatureWorldVisibilitySnapshot[] = [];
|
|
3151
|
-
const hiddenEntityReports: RenderFeatureHiddenEntityReport[] = [];
|
|
3152
|
-
|
|
3153
|
-
for (let fi = 0; fi < succeededFrames.length; fi++) {
|
|
3154
|
-
const f = succeededFrames[fi];
|
|
3155
|
-
const wId = succeededIndices[fi];
|
|
3156
|
-
if (f === undefined || wId === undefined) continue;
|
|
3157
|
-
|
|
3158
|
-
const base = renderables.length;
|
|
3159
|
-
const visibilitySnapshot = f.visibilitySnapshots[0];
|
|
3160
|
-
const world = worlds[wId];
|
|
3161
|
-
if (visibilitySnapshot !== undefined) {
|
|
3162
|
-
visibilitySnapshots.push(visibilitySnapshot);
|
|
3163
|
-
if (world !== undefined)
|
|
3164
|
-
featureVisibilitySnapshots.push({ world, snapshot: visibilitySnapshot });
|
|
3165
|
-
}
|
|
3166
|
-
hiddenEntityReports.push(...f.hiddenEntityReports);
|
|
3167
|
-
for (const r of f.renderables) {
|
|
3168
|
-
renderables.push({
|
|
3169
|
-
...r,
|
|
3170
|
-
worldId: wId,
|
|
3171
|
-
...(r.pointsLines === undefined ? {} : { pointsLines: { ...r.pointsLines, worldId: wId } }),
|
|
3172
|
-
});
|
|
3173
|
-
}
|
|
3174
|
-
|
|
3175
|
-
// D-3: dispatch — per-world renderableIndex rebased by base offset.
|
|
3176
|
-
for (const d of f.dispatch) {
|
|
3177
|
-
dispatchEntries.push({ ...d, renderableIndex: (d.renderableIndex ?? 0) + base });
|
|
3178
|
-
}
|
|
3179
|
-
}
|
|
3180
|
-
|
|
3181
|
-
// Stable sort dispatch by queue value.
|
|
3182
|
-
dispatchEntries.sort((a, b) => (a.queue ?? 0) - (b.queue ?? 0));
|
|
3183
|
-
|
|
3184
|
-
// AC-04: lights — point[]/spot[] concat; directional first-hit in
|
|
3185
|
-
// succeededFrames order (which preserves worlds[] order for successful
|
|
3186
|
-
// frames); directionalCount sum.
|
|
3187
|
-
const point: PointLightSnapshot[] = [];
|
|
3188
|
-
const spot: SpotLightSnapshot[] = [];
|
|
3189
|
-
const rect: RectAreaDirectLightSnapshot[] = [];
|
|
3190
|
-
let directional: DirectionalLightSnapshot | undefined;
|
|
3191
|
-
let directionalCount = 0;
|
|
3192
|
-
let lightViewProj: readonly Float32Array[] | undefined;
|
|
3193
|
-
let splitPlanes: Float32Array | undefined;
|
|
3194
|
-
let cascadeCount: number | undefined;
|
|
3195
|
-
let cascadeBlend: number | undefined;
|
|
3196
|
-
let shadowMapSize: number | undefined;
|
|
3197
|
-
let depthBias: number | undefined;
|
|
3198
|
-
let normalBias: number | undefined;
|
|
3199
|
-
let directionalShadowQuality: DirectionalShadowQuality | undefined;
|
|
3200
|
-
let directionalShadowError: LightValidationError | undefined;
|
|
3201
|
-
const pointShadow: PointShadowSnapshot[] = [];
|
|
3202
|
-
// bug-20260710-editor-cross-world-shadow: carry the raw CSM config +
|
|
3203
|
-
// direction of the first-hit directional so the merge layer can recompute
|
|
3204
|
-
// matrices against the SURFACED (cameraOwner) camera — the light and the
|
|
3205
|
-
// camera may live in different worlds (editor super-composite).
|
|
3206
|
-
let directionalCsmConfig: DirectionalCsmConfig | undefined;
|
|
3207
|
-
let directionalCsmDirection: Vec3 | undefined;
|
|
3208
|
-
for (const f of succeededFrames) {
|
|
3209
|
-
for (const p of f.lights.point) point.push(p);
|
|
3210
|
-
for (const s of f.lights.spot) spot.push(s);
|
|
3211
|
-
for (const r of f.lights.rect) rect.push(r);
|
|
3212
|
-
for (const ps of f.lights.pointShadow) pointShadow.push(ps);
|
|
3213
|
-
if (directional === undefined && f.lights.directional !== undefined) {
|
|
3214
|
-
directional = f.lights.directional;
|
|
3215
|
-
// Carry CSM shadow fields from the first-hit directional's world.
|
|
3216
|
-
lightViewProj = f.lights.lightViewProj;
|
|
3217
|
-
splitPlanes = f.lights.splitPlanes;
|
|
3218
|
-
cascadeCount = f.lights.cascadeCount;
|
|
3219
|
-
cascadeBlend = f.lights.cascadeBlend;
|
|
3220
|
-
shadowMapSize = f.lights.shadowMapSize;
|
|
3221
|
-
depthBias = f.lights.depthBias;
|
|
3222
|
-
normalBias = f.lights.normalBias;
|
|
3223
|
-
directionalShadowQuality = f.lights.directionalShadowQuality;
|
|
3224
|
-
directionalShadowError = f.lights.directionalShadowError;
|
|
3225
|
-
directionalCsmConfig = f.lights.directionalCsmConfig;
|
|
3226
|
-
directionalCsmDirection = f.lights.directionalCsmDirection;
|
|
3227
|
-
}
|
|
3228
|
-
directionalCount += f.lights.directionalCount;
|
|
3229
|
-
}
|
|
3230
|
-
|
|
3231
|
-
// AC-05/06 + w4 owner split (D-3 / R-6): cameras come from the cameraOwner
|
|
3232
|
-
// world; skylight / skybox / postProcessParams come from the resourceOwner
|
|
3233
|
-
// world (holistic snapshot selection). Scan succeededIndices once to locate
|
|
3234
|
-
// each owner's surviving frame. When cameraOwner === resourceOwner both
|
|
3235
|
-
// resolve to the same frame — byte-identical to the pre-w4 single-owner path.
|
|
3236
|
-
let cameraOwnerFrame: ExtractedFrame | undefined;
|
|
3237
|
-
let resourceOwnerFrame: ExtractedFrame | undefined;
|
|
3238
|
-
for (let fi = 0; fi < succeededFrames.length; fi++) {
|
|
3239
|
-
if (succeededIndices[fi] === cameraOwner) cameraOwnerFrame = succeededFrames[fi];
|
|
3240
|
-
if (succeededIndices[fi] === resourceOwner) resourceOwnerFrame = succeededFrames[fi];
|
|
3241
|
-
}
|
|
3242
|
-
const cameras = cameraOwnerFrame !== undefined ? [...cameraOwnerFrame.cameras] : [];
|
|
3243
|
-
const auxiliaryCameras =
|
|
3244
|
-
cameraOwnerFrame !== undefined ? [...cameraOwnerFrame.auxiliaryCameras] : [];
|
|
3245
|
-
const cubeCameras = cameraOwnerFrame !== undefined ? [...cameraOwnerFrame.cubeCameras] : [];
|
|
3246
|
-
|
|
3247
|
-
// bug-20260710-editor-cross-world-shadow: RECOMPUTE directional CSM matrices
|
|
3248
|
-
// against the surfaced camera. In a single-world app the per-world extract
|
|
3249
|
-
// already produced correct matrices (light+camera share the world), and this
|
|
3250
|
-
// recompute reproduces them byte-identically (same config, same camera). In
|
|
3251
|
-
// the editor super-composite the directional light's world has NO camera, so
|
|
3252
|
-
// its per-world `lightViewProj` is undefined/degenerate; the surfaced camera
|
|
3253
|
-
// lives in the cameraOwner world. Pairing them here is the ONLY place both
|
|
3254
|
-
// are visible. Point/spot shadows are camera-independent (light-space only)
|
|
3255
|
-
// and need no merge-layer fix-up.
|
|
3256
|
-
const mergeCam = cameras[0];
|
|
3257
|
-
if (directionalCsmConfig !== undefined && directionalCsmDirection !== undefined) {
|
|
3258
|
-
const mergeCameraData: CsmCameraData | undefined =
|
|
3259
|
-
mergeCam !== undefined
|
|
3260
|
-
? {
|
|
3261
|
-
world: mergeCam.world,
|
|
3262
|
-
fov: mergeCam.fov,
|
|
3263
|
-
aspect: mergeCam.aspect,
|
|
3264
|
-
near: mergeCam.near,
|
|
3265
|
-
far: mergeCam.far,
|
|
3266
|
-
projection: mergeCam.projection,
|
|
3267
|
-
orthoLeft: mergeCam.orthoLeft,
|
|
3268
|
-
orthoRight: mergeCam.orthoRight,
|
|
3269
|
-
orthoBottom: mergeCam.orthoBottom,
|
|
3270
|
-
orthoTop: mergeCam.orthoTop,
|
|
3271
|
-
}
|
|
3272
|
-
: undefined;
|
|
3273
|
-
const csm = computeDirectionalCsm(
|
|
3274
|
-
directionalCsmDirection,
|
|
3275
|
-
directionalCsmConfig,
|
|
3276
|
-
mergeCameraData,
|
|
3277
|
-
);
|
|
3278
|
-
if (csm !== null) {
|
|
3279
|
-
lightViewProj = csm.lightViewProj;
|
|
3280
|
-
splitPlanes = csm.splitPlanes;
|
|
3281
|
-
cascadeCount = csm.cascadeCount;
|
|
3282
|
-
cascadeBlend = csm.cascadeBlend;
|
|
3283
|
-
shadowMapSize = csm.shadowMapSize;
|
|
3284
|
-
directionalShadowQuality = csm.directionalShadowQuality;
|
|
3285
|
-
}
|
|
3286
|
-
// csm === null (no surfaced camera at all): keep the per-world carry —
|
|
3287
|
-
// there is no better data, and a cameraless frame renders nothing anyway.
|
|
3288
|
-
}
|
|
3289
|
-
|
|
3290
|
-
const lights: ExtractedLights = {
|
|
3291
|
-
directional,
|
|
3292
|
-
directionalCount,
|
|
3293
|
-
point,
|
|
3294
|
-
spot,
|
|
3295
|
-
rect,
|
|
3296
|
-
lightViewProj,
|
|
3297
|
-
splitPlanes,
|
|
3298
|
-
cascadeCount,
|
|
3299
|
-
cascadeBlend,
|
|
3300
|
-
shadowMapSize,
|
|
3301
|
-
depthBias,
|
|
3302
|
-
normalBias,
|
|
3303
|
-
directionalShadowQuality,
|
|
3304
|
-
directionalShadowError,
|
|
3305
|
-
pointShadow,
|
|
3306
|
-
directionalCsmConfig,
|
|
3307
|
-
directionalCsmDirection,
|
|
3308
|
-
};
|
|
3309
|
-
const volumetricFog = resourceOwnerFrame?.volumetricFog;
|
|
3310
|
-
const skylight = resourceOwnerFrame?.skylight;
|
|
3311
|
-
const skylightCount = resourceOwnerFrame?.skylightCount ?? 0;
|
|
3312
|
-
const skybox = resourceOwnerFrame?.skybox;
|
|
3313
|
-
const skyboxCount = resourceOwnerFrame?.skyboxCount ?? 0;
|
|
3314
|
-
const fog = resourceOwnerFrame?.fog;
|
|
3315
|
-
const fogFailure = resourceOwnerFrame?.fogFailure;
|
|
3316
|
-
const environment = resourceOwnerFrame?.environment;
|
|
3317
|
-
const lightProbes = succeededFrames.flatMap((frame) => frame.lightProbes ?? []);
|
|
3318
|
-
// User-authored PostProcessParams entities are singleton scene resources, so
|
|
3319
|
-
// they come from the resourceOwner world (holistic snapshot selection).
|
|
3320
|
-
const postProcessParams = new Map(resourceOwnerFrame?.postProcessParams);
|
|
3321
|
-
// feat-20260709-editor-world-partition ENGINE-fix-round2 (defect 1): the
|
|
3322
|
-
// engine built-in Standard tonemap param is NOT a scene resource — the
|
|
3323
|
-
// per-world extractFrame bridges it from that world's own `cameras[0]`
|
|
3324
|
-
// (Camera.exposure / whitePoint / tonemap is the SSOT). It therefore lives on
|
|
3325
|
-
// the CAMERA-owner frame, not the resource-owner frame. In the split-owner
|
|
3326
|
-
// editor topology the resourceOwner world has no Camera, so its frame carries
|
|
3327
|
-
// no Standard tonemap entry; taking postProcessParams from resourceOwner
|
|
3328
|
-
// alone drops it, the tonemap pass's params UBO stays zero-filled
|
|
3329
|
-
// (exposure=0 => tonemapped output is uniformly black), and the whole frame
|
|
3330
|
-
// reads black even though geometry drew into the HDR target. Overlay the
|
|
3331
|
-
// camera-owner frame's tonemap param (its SSOT source) so the surfaced
|
|
3332
|
-
// camera's exposure/whitePoint/mode reach the tonemap pass. When
|
|
3333
|
-
// cameraOwner === resourceOwner this is a no-op (identical entry). The 'na'
|
|
3334
|
-
// Standard tonemap identity mirrors the engine provider key set at the
|
|
3335
|
-
// bottom of extractFrame (SSOT: same identity, same 16B layout).
|
|
3336
|
-
const OUTPUT_TRANSFORM_PARAM_KEY = STANDARD_OUTPUT_TRANSFORM_FEATURE_ID;
|
|
3337
|
-
const cameraTonemapParam = cameraOwnerFrame?.postProcessParams.get(OUTPUT_TRANSFORM_PARAM_KEY);
|
|
3338
|
-
if (cameraTonemapParam !== undefined) {
|
|
3339
|
-
postProcessParams.set(OUTPUT_TRANSFORM_PARAM_KEY, cameraTonemapParam);
|
|
3340
|
-
} else {
|
|
3341
|
-
// The camera-owner world produced no tonemap param (no Camera surfaced
|
|
3342
|
-
// there this frame); do not leave a stale resource-owner entry that would
|
|
3343
|
-
// apply a foreign camera's exposure. Removing it lets the tonemap pass fall
|
|
3344
|
-
// back to its param-less zero path only when genuinely no camera exists.
|
|
3345
|
-
postProcessParams.delete(OUTPUT_TRANSFORM_PARAM_KEY);
|
|
3346
|
-
}
|
|
3347
|
-
|
|
3348
|
-
// D-3: frustumStats — culled/total summed across worlds.
|
|
3349
|
-
const frustumStats = {
|
|
3350
|
-
culled: succeededFrames.reduce((s, f) => s + f.frustumStats.culled, 0),
|
|
3351
|
-
total: succeededFrames.reduce((s, f) => s + f.frustumStats.total, 0),
|
|
3352
|
-
};
|
|
3353
|
-
const visibilityStats = {
|
|
3354
|
-
explicitlyHidden: succeededFrames.reduce((s, f) => s + f.visibilityStats.explicitlyHidden, 0),
|
|
3355
|
-
};
|
|
3356
|
-
const reflectionProbes = succeededFrames.flatMap((f, index) => {
|
|
3357
|
-
const worldId = succeededIndices[index] ?? 0;
|
|
3358
|
-
return (f.reflectionProbes ?? []).map((probe) => ({ ...probe, worldId }));
|
|
3359
|
-
});
|
|
3360
|
-
|
|
3361
|
-
return {
|
|
3362
|
-
cameras,
|
|
3363
|
-
auxiliaryCameras,
|
|
3364
|
-
cubeCameras,
|
|
3365
|
-
reflectionProbes,
|
|
3366
|
-
lights,
|
|
3367
|
-
...(volumetricFog === undefined
|
|
3368
|
-
? {}
|
|
3369
|
-
: { volumetricFog: { ...volumetricFog, worldId: resourceOwner } }),
|
|
3370
|
-
renderables,
|
|
3371
|
-
dispatch: dispatchEntries,
|
|
3372
|
-
skylight,
|
|
3373
|
-
skylightCount,
|
|
3374
|
-
lightProbes,
|
|
3375
|
-
skybox,
|
|
3376
|
-
skyboxCount,
|
|
3377
|
-
fog,
|
|
3378
|
-
...(fogFailure === undefined ? {} : { fogFailure }),
|
|
3379
|
-
environment,
|
|
3380
|
-
environmentReady: resourceOwnerFrame !== undefined && environment !== undefined,
|
|
3381
|
-
frustumStats,
|
|
3382
|
-
visibilityStats,
|
|
3383
|
-
postProcessParams,
|
|
3384
|
-
visibilitySnapshots,
|
|
3385
|
-
featureVisibilitySnapshots,
|
|
3386
|
-
hiddenEntityReports,
|
|
3387
|
-
};
|
|
3388
|
-
}
|
|
3389
|
-
|
|
3390
|
-
/**
|
|
3391
|
-
* A MeshAsset participates in frustum culling only when its producer supplied
|
|
3392
|
-
* the complete finite local-space bounds promised by the asset contract.
|
|
3393
|
-
* Missing, malformed, and inverted-infinity empty bounds remain conservative
|
|
3394
|
-
* (always visible) so a bad asset cannot turn into a false-negative render;
|
|
3395
|
-
* they are not cull candidates and therefore do not inflate frustumStats.
|
|
3396
|
-
*/
|
|
3397
|
-
function hasFiniteOrderedLocalAabb(aabb: Float32Array | undefined): aabb is Float32Array {
|
|
3398
|
-
if (aabb === undefined || aabb.length !== 6) return false;
|
|
3399
|
-
const minX = aabb[0];
|
|
3400
|
-
const minY = aabb[1];
|
|
3401
|
-
const minZ = aabb[2];
|
|
3402
|
-
const maxX = aabb[3];
|
|
3403
|
-
const maxY = aabb[4];
|
|
3404
|
-
const maxZ = aabb[5];
|
|
3405
|
-
if (
|
|
3406
|
-
minX === undefined ||
|
|
3407
|
-
minY === undefined ||
|
|
3408
|
-
minZ === undefined ||
|
|
3409
|
-
maxX === undefined ||
|
|
3410
|
-
maxY === undefined ||
|
|
3411
|
-
maxZ === undefined
|
|
3412
|
-
) {
|
|
3413
|
-
return false;
|
|
3414
|
-
}
|
|
3415
|
-
if (
|
|
3416
|
-
!Number.isFinite(minX) ||
|
|
3417
|
-
!Number.isFinite(minY) ||
|
|
3418
|
-
!Number.isFinite(minZ) ||
|
|
3419
|
-
!Number.isFinite(maxX) ||
|
|
3420
|
-
!Number.isFinite(maxY) ||
|
|
3421
|
-
!Number.isFinite(maxZ)
|
|
3422
|
-
) {
|
|
3423
|
-
return false;
|
|
3424
|
-
}
|
|
3425
|
-
return minX <= maxX && minY <= maxY && minZ <= maxZ;
|
|
3426
|
-
}
|
|
3427
|
-
|
|
3428
|
-
function morphSnapshotFor(
|
|
3429
|
-
mesh: MeshAsset,
|
|
3430
|
-
weights: ArrayLike<number> | undefined,
|
|
3431
|
-
): MorphSnapshot | undefined {
|
|
3432
|
-
const targets = mesh.morphTargets;
|
|
3433
|
-
if (targets === undefined || targets.length === 0 || weights === undefined) return undefined;
|
|
3434
|
-
if (weights.length !== targets.length) return undefined;
|
|
3435
|
-
const firstPositions = targets[0]?.position;
|
|
3436
|
-
if (
|
|
3437
|
-
firstPositions === undefined ||
|
|
3438
|
-
firstPositions.length === 0 ||
|
|
3439
|
-
firstPositions.length % 3 !== 0
|
|
3440
|
-
) {
|
|
3441
|
-
return undefined;
|
|
3442
|
-
}
|
|
3443
|
-
const vertexCount = firstPositions.length / 3;
|
|
3444
|
-
if (mesh.vertices.length % vertexCount !== 0) return undefined;
|
|
3445
|
-
for (let targetIndex = 0; targetIndex < targets.length; targetIndex += 1) {
|
|
3446
|
-
const target = targets[targetIndex];
|
|
3447
|
-
if (target?.position?.length !== firstPositions.length) return undefined;
|
|
3448
|
-
}
|
|
3449
|
-
const copied = new Float32Array(weights.length);
|
|
3450
|
-
for (let index = 0; index < weights.length; index += 1) {
|
|
3451
|
-
const weight = weights[index] ?? Number.NaN;
|
|
3452
|
-
if (!Number.isFinite(weight)) return undefined;
|
|
3453
|
-
copied[index] = weight;
|
|
3454
|
-
}
|
|
3455
|
-
return { weights: copied, targetCount: targets.length };
|
|
3456
|
-
}
|
|
3457
|
-
|
|
3458
|
-
export function extractFrame(world: World, context: PreparedExtractContext): ExtractedFrame {
|
|
3459
|
-
// feat-20260708-composited-multi-world-rendering M2 / D-2: resetForFrame
|
|
3460
|
-
// has been lifted to extractFrames (the frame-level entry point).
|
|
3461
|
-
// extractFrame is now a pure world->snapshot function with no frame-level
|
|
3462
|
-
// side effects. See plan-decisions PD2 for the reviewer ruling.
|
|
3463
|
-
const {
|
|
3464
|
-
assets,
|
|
3465
|
-
pipelineState,
|
|
3466
|
-
materialSnapshotCache: persistentMaterialSnapshotCache,
|
|
3467
|
-
cull: cullMode,
|
|
3468
|
-
resourceOwnerFog,
|
|
3469
|
-
getMaterialShaderArtifact,
|
|
3470
|
-
} = context;
|
|
3471
|
-
// Environment reads are owned by the retained resource projection and are
|
|
3472
|
-
// established at the frame boundary; record never revisits World state.
|
|
3473
|
-
const fog =
|
|
3474
|
-
resourceOwnerFog === undefined ? selectFogFrame(world) : (resourceOwnerFog ?? undefined);
|
|
3475
|
-
const fogFailure = fogState(world).failure;
|
|
3476
|
-
const visibility = context.visibility.hasAnyHiddenIntent ? context.visibility : undefined;
|
|
3477
|
-
const skinPaletteAllocator = pipelineState?.skinPaletteAllocator ?? null;
|
|
3478
|
-
|
|
3479
|
-
const directionalLightQuery = world.query({ read: [DirectionalLight] }).unwrap();
|
|
3480
|
-
|
|
3481
|
-
// feat-20260601 D-3: camera / point / spot light world transforms are read
|
|
3482
|
-
// through the single resolved `GlobalTransform.world` mat4 (written by
|
|
3483
|
-
// propagateTransforms), not the retired GlobalTransform-column-switch.
|
|
3484
|
-
//
|
|
3485
|
-
// Each segment routes through a World-owned Query with explicit read and
|
|
3486
|
-
// optional roles. The packed entity handle for `readWorldMat4Copy` /
|
|
3487
|
-
// `_getArrayView` reads comes from `row.entity` -- the
|
|
3488
|
-
// archetype-graph back-door (`graph.archetypes` / `arch.components.some`) is gone.
|
|
3489
|
-
// Plan-decisions K-2 sniffing scheme B (archetype-edge sniff once via
|
|
3490
|
-
// `row.get(X) !== undefined`); K-3 invariant preserved (`_getArrayView`
|
|
3491
|
-
// calls survive untouched, only the entity source changes).
|
|
3492
|
-
const worldInternal = createWorldInternalView(world);
|
|
3493
|
-
let volumetricFog = extractVolumeSnapshot(world, assets, worldInternal);
|
|
3494
|
-
|
|
3495
|
-
const cameraRoles = selectCameraRoles(world);
|
|
3496
|
-
const cameras = [...cameraRoles.display];
|
|
3497
|
-
const auxiliaryCameras = cameraRoles.auxiliary;
|
|
3498
|
-
const cubeCameras = collectCubeCameraSnapshots(world);
|
|
3499
|
-
const reflectionProbes = collectReflectionProbeFacts(world);
|
|
3500
|
-
|
|
3501
|
-
// Three-query union (M2 / w16 / AC-03): directional has no Transform
|
|
3502
|
-
// dependency (sun-like infinite-source semantics); point + spot pull
|
|
3503
|
-
// position from the companion Transform via the joined queries.
|
|
3504
|
-
// Host-side pre-multiplication: color *= intensity (charter P4); cone
|
|
3505
|
-
// deg -> cos (D-S2); range -> 1/range^2 (D-S5).
|
|
3506
|
-
let directional: DirectionalLightSnapshot | undefined;
|
|
3507
|
-
let directionalCount = 0;
|
|
3508
|
-
// feat-20260621 M2: capture shadow fields from the first-hit DirectionalLight.
|
|
3509
|
-
// castShadow defaults to true; the CSM path is gated on firstHitCastShadow !== false.
|
|
3510
|
-
let firstHitCastShadow: boolean | undefined;
|
|
3511
|
-
let firstHitShadowFields:
|
|
3512
|
-
| {
|
|
3513
|
-
cascadeCount: number;
|
|
3514
|
-
splitLambda: number;
|
|
3515
|
-
cascadeBlend: number;
|
|
3516
|
-
mapSize: number;
|
|
3517
|
-
depthBias: number;
|
|
3518
|
-
normalBias: number;
|
|
3519
|
-
shadowDistance: number;
|
|
3520
|
-
shadowFilter: number;
|
|
3521
|
-
shadowAngularRadius: number;
|
|
3522
|
-
maxPenumbraTexels: number;
|
|
3523
|
-
}
|
|
3524
|
-
| undefined;
|
|
3525
|
-
for (const row of directionalLightQuery) {
|
|
3526
|
-
const l = row.get(DirectionalLight);
|
|
3527
|
-
directionalCount += 1;
|
|
3528
|
-
const intensity = l.intensity;
|
|
3529
|
-
const snapshot: DirectionalLightSnapshot = {
|
|
3530
|
-
kind: 'directional',
|
|
3531
|
-
entity: row.entity,
|
|
3532
|
-
direction: vec3.create(l.direction[0] ?? 0, l.direction[1] ?? -1, l.direction[2] ?? 0),
|
|
3533
|
-
color: vec3.create(
|
|
3534
|
-
(l.color[0] ?? 1) * intensity,
|
|
3535
|
-
(l.color[1] ?? 1) * intensity,
|
|
3536
|
-
(l.color[2] ?? 1) * intensity,
|
|
3537
|
-
),
|
|
3538
|
-
intensity,
|
|
3539
|
-
};
|
|
3540
|
-
if (directional === undefined) {
|
|
3541
|
-
// First hit wins; record-stage N>1 fail-fast (M3 / w19) flags duplicates.
|
|
3542
|
-
directional = snapshot;
|
|
3543
|
-
firstHitCastShadow = l.castShadow;
|
|
3544
|
-
firstHitShadowFields = {
|
|
3545
|
-
cascadeCount: l.cascadeCount,
|
|
3546
|
-
splitLambda: l.splitLambda,
|
|
3547
|
-
cascadeBlend: l.cascadeBlend,
|
|
3548
|
-
mapSize: l.mapSize,
|
|
3549
|
-
depthBias: l.depthBias,
|
|
3550
|
-
normalBias: l.normalBias,
|
|
3551
|
-
shadowDistance: l.shadowDistance,
|
|
3552
|
-
shadowFilter: l.shadowFilter,
|
|
3553
|
-
shadowAngularRadius: l.shadowAngularRadius,
|
|
3554
|
-
maxPenumbraTexels: l.maxPenumbraTexels,
|
|
3555
|
-
};
|
|
3556
|
-
}
|
|
3557
|
-
}
|
|
3558
|
-
|
|
3559
|
-
let directionalShadowError: LightValidationError | undefined;
|
|
3560
|
-
if (directional !== undefined && firstHitShadowFields !== undefined) {
|
|
3561
|
-
const validation = validateDirectionalLightData({
|
|
3562
|
-
direction: directional.direction,
|
|
3563
|
-
castShadow: firstHitCastShadow,
|
|
3564
|
-
...firstHitShadowFields,
|
|
3565
|
-
});
|
|
3566
|
-
if (!validation.ok) {
|
|
3567
|
-
directionalShadowError = validation.error;
|
|
3568
|
-
worldInternal._routeError(directionalShadowError, {
|
|
3569
|
-
severity: Severity.Error,
|
|
3570
|
-
systemName: 'RenderSystem.extract (directional-shadow)',
|
|
3571
|
-
});
|
|
3572
|
-
}
|
|
3573
|
-
}
|
|
3574
|
-
|
|
3575
|
-
const pointSnapshots: PointLightSnapshot[] = [];
|
|
3576
|
-
// feat-20260612-point-light-shadows-urp-hdrp M4 / T-M4-4: track entity per
|
|
3577
|
-
// pointSnapshots index so the post-extract pointShadow join can stamp
|
|
3578
|
-
// `shadowAtlasLayer + shadowNear + shadowFar` onto the matching PointLight.
|
|
3579
|
-
const pointSnapshotEntities: number[] = [];
|
|
3580
|
-
const pointLightQuery = world
|
|
3581
|
-
.query({
|
|
3582
|
-
read: [PointLight],
|
|
3583
|
-
optional: [Transform, GlobalTransform],
|
|
3584
|
-
})
|
|
3585
|
-
.unwrap();
|
|
3586
|
-
for (const row of pointLightQuery) {
|
|
3587
|
-
const p = row.get(PointLight);
|
|
3588
|
-
// K-2 scheme B: archetype-edge sniff -- `bundle.Transform` key is absent
|
|
3589
|
-
// when the archetype does not carry the Transform column.
|
|
3590
|
-
const hasTransform = row.get(GlobalTransform) !== undefined;
|
|
3591
|
-
const intensity = p.intensity;
|
|
3592
|
-
const range = p.range;
|
|
3593
|
-
const entityId = row.entity;
|
|
3594
|
-
// Position = world-space translation extracted from GlobalTransform.world.
|
|
3595
|
-
// A point light archetype without a Transform column sits at the origin.
|
|
3596
|
-
let worldMat: Float32Array | undefined;
|
|
3597
|
-
if (hasTransform) {
|
|
3598
|
-
const view = worldInternal._getArrayView(entityId, GlobalTransform, 'world');
|
|
3599
|
-
if (view !== undefined) worldMat = new Float32Array(view);
|
|
3600
|
-
}
|
|
3601
|
-
const position =
|
|
3602
|
-
worldMat !== undefined ? mat4.getTranslation(vec3.create(), worldMat) : vec3.create(0, 0, 0);
|
|
3603
|
-
pointSnapshots.push({
|
|
3604
|
-
kind: 'point',
|
|
3605
|
-
entity: entityId,
|
|
3606
|
-
position,
|
|
3607
|
-
color: vec3.create(
|
|
3608
|
-
(p.color[0] ?? 1) * intensity,
|
|
3609
|
-
(p.color[1] ?? 1) * intensity,
|
|
3610
|
-
(p.color[2] ?? 1) * intensity,
|
|
3611
|
-
),
|
|
3612
|
-
intensity,
|
|
3613
|
-
invRangeSquared: computeInvRangeSquared(range),
|
|
3614
|
-
});
|
|
3615
|
-
pointSnapshotEntities.push(entityId);
|
|
3616
|
-
}
|
|
3617
|
-
|
|
3618
|
-
const spotSnapshots: SpotLightSnapshot[] = [];
|
|
3619
|
-
const spotLightQuery = world
|
|
3620
|
-
.query({
|
|
3621
|
-
read: [SpotLight],
|
|
3622
|
-
optional: [Transform, GlobalTransform],
|
|
3623
|
-
})
|
|
3624
|
-
.unwrap();
|
|
3625
|
-
// feat-20260625-spot-light-shadow-mapping M1 w5: tile allocation for castShadow spots.
|
|
3626
|
-
// Cap = 4 (OOS-5), sentinel -1 = unassigned (plan-strategy D-4).
|
|
3627
|
-
// Direction degeneration (near-zero) also skips shadow (requirements $112).
|
|
3628
|
-
let spotTileNext = 0;
|
|
3629
|
-
const iesSlices = new Map<number, number>();
|
|
3630
|
-
const cookieSlices = new Map<number, number>();
|
|
3631
|
-
const projectorSlices = new Map<number, number>();
|
|
3632
|
-
for (const row of spotLightQuery) {
|
|
3633
|
-
const s = row.get(SpotLight);
|
|
3634
|
-
const hasTransform = row.get(GlobalTransform) !== undefined;
|
|
3635
|
-
const intensity = s.intensity;
|
|
3636
|
-
const range = s.range;
|
|
3637
|
-
const innerConeDeg = s.innerConeDeg;
|
|
3638
|
-
const outerConeDeg = s.outerConeDeg;
|
|
3639
|
-
let worldMat: Float32Array | undefined;
|
|
3640
|
-
if (hasTransform) {
|
|
3641
|
-
const entity = row.entity;
|
|
3642
|
-
const view = worldInternal._getArrayView(entity, GlobalTransform, 'world');
|
|
3643
|
-
if (view !== undefined) worldMat = new Float32Array(view);
|
|
3644
|
-
}
|
|
3645
|
-
const position =
|
|
3646
|
-
worldMat !== undefined ? mat4.getTranslation(vec3.create(), worldMat) : vec3.create(0, 0, 0);
|
|
3647
|
-
const dir = vec3.create(s.direction[0] ?? 0, s.direction[1] ?? -1, s.direction[2] ?? 0);
|
|
3648
|
-
|
|
3649
|
-
// Extract is the single direction-normalization owner for direct-light
|
|
3650
|
-
// snapshots. URP and HDRP preserve this value downstream.
|
|
3651
|
-
const dirLen = Math.sqrt(
|
|
3652
|
-
(dir[0] ?? 0) * (dir[0] ?? 0) + (dir[1] ?? 0) * (dir[1] ?? 0) + (dir[2] ?? 0) * (dir[2] ?? 0),
|
|
3653
|
-
);
|
|
3654
|
-
const EPSILON = 1e-6;
|
|
3655
|
-
const hasValidDirection = dirLen > EPSILON;
|
|
3656
|
-
if (!hasValidDirection) {
|
|
3657
|
-
worldInternal._routeError(
|
|
3658
|
-
new SpawnLightInvalidBoundsError('SpotLight', 'direction', [
|
|
3659
|
-
dir[0] ?? 0,
|
|
3660
|
-
dir[1] ?? 0,
|
|
3661
|
-
dir[2] ?? 0,
|
|
3662
|
-
]),
|
|
3663
|
-
{
|
|
3664
|
-
severity: Severity.Error,
|
|
3665
|
-
systemName: 'RenderSystem.extract (spot-direction)',
|
|
3666
|
-
},
|
|
3667
|
-
);
|
|
3668
|
-
}
|
|
3669
|
-
const dirN = vec3.create(
|
|
3670
|
-
hasValidDirection ? (dir[0] ?? 0) / dirLen : (dir[0] ?? 0),
|
|
3671
|
-
hasValidDirection ? (dir[1] ?? 0) / dirLen : (dir[1] ?? 0),
|
|
3672
|
-
hasValidDirection ? (dir[2] ?? 0) / dirLen : (dir[2] ?? 0),
|
|
3673
|
-
);
|
|
3674
|
-
|
|
3675
|
-
// ── shadow fields (feat-20260625-spot-light-shadow-mapping M1) ──
|
|
3676
|
-
const castShadow = s.castShadow;
|
|
3677
|
-
const sMapSize = s.mapSize;
|
|
3678
|
-
const sNearPlane = s.nearPlane;
|
|
3679
|
-
const sFarPlane = s.farPlane;
|
|
3680
|
-
const iesProfileAsset =
|
|
3681
|
-
s.iesProfile === undefined
|
|
3682
|
-
? undefined
|
|
3683
|
-
: resolveAssetHandle<IesProfileAsset>(world, s.iesProfile);
|
|
3684
|
-
const cookieAsset =
|
|
3685
|
-
s.cookie === undefined ? undefined : resolveAssetHandle<TextureAsset>(world, s.cookie);
|
|
3686
|
-
// Resolve the projector before assigning array slices so the
|
|
3687
|
-
// extended-lighting path can reuse its texture transport while retaining
|
|
3688
|
-
// the authored lightViewProj projection semantics.
|
|
3689
|
-
const hasProjector = Number(s.projector) > 0;
|
|
3690
|
-
let projectorHandle: Handle<'TextureAsset', 'shared'> | undefined;
|
|
3691
|
-
let projectorAsset: TextureAsset | undefined;
|
|
3692
|
-
let projectorGuid: string | undefined;
|
|
3693
|
-
if (hasProjector) {
|
|
3694
|
-
const candidate = toShared<'TextureAsset'>(Math.round(Number(s.projector)));
|
|
3695
|
-
const resolvedProjector = resolveAssetHandle<TextureAsset>(world, candidate);
|
|
3696
|
-
if (!resolvedProjector.ok || resolvedProjector.value.kind !== 'texture') {
|
|
3697
|
-
worldInternal._routeError(
|
|
3698
|
-
resolvedProjector.ok
|
|
3699
|
-
? new RhiError({
|
|
3700
|
-
code: 'asset-not-registered',
|
|
3701
|
-
expected: 'SpotLight.projector resolves to a TextureAsset',
|
|
3702
|
-
hint: 'load the authored projector TextureAsset before assigning SpotLight.projector',
|
|
3703
|
-
detail: { assetHandle: Number(candidate) },
|
|
3704
|
-
})
|
|
3705
|
-
: resolvedProjector.error,
|
|
3706
|
-
{ severity: Severity.Error, systemName: 'RenderSystem.extract (spot-projector)' },
|
|
3707
|
-
);
|
|
3708
|
-
} else {
|
|
3709
|
-
projectorHandle = candidate;
|
|
3710
|
-
projectorAsset = resolvedProjector.value;
|
|
3711
|
-
projectorGuid = assets?.guidOf(projectorAsset) ?? `handle:${Number(candidate)}`;
|
|
3712
|
-
}
|
|
3713
|
-
}
|
|
3714
|
-
const iesProfileData =
|
|
3715
|
-
iesProfileAsset?.ok && iesProfileAsset.value.kind === 'ies-profile'
|
|
3716
|
-
? new Uint8Array(iesProfileAsset.value.data)
|
|
3717
|
-
: undefined;
|
|
3718
|
-
const cookieProjection =
|
|
3719
|
-
cookieAsset?.ok && cookieAsset.value.kind === 'texture'
|
|
3720
|
-
? prepareCookieProjection(cookieAsset.value)
|
|
3721
|
-
: undefined;
|
|
3722
|
-
const cookieData = cookieProjection?.data;
|
|
3723
|
-
const projectorProjection =
|
|
3724
|
-
projectorAsset === undefined ? undefined : prepareCookieProjection(projectorAsset);
|
|
3725
|
-
const projectorData = projectorProjection?.data;
|
|
3726
|
-
const iesProfileHandle = s.iesProfile === undefined ? undefined : handleSlot(s.iesProfile);
|
|
3727
|
-
const cookieHandle = s.cookie === undefined ? undefined : handleSlot(s.cookie);
|
|
3728
|
-
let iesProfileSlice =
|
|
3729
|
-
iesProfileHandle === undefined ? undefined : iesSlices.get(iesProfileHandle);
|
|
3730
|
-
if (
|
|
3731
|
-
iesProfileSlice === undefined &&
|
|
3732
|
-
iesProfileHandle !== undefined &&
|
|
3733
|
-
iesProfileData !== undefined &&
|
|
3734
|
-
iesSlices.size < IES_SLICE_CAPACITY
|
|
3735
|
-
) {
|
|
3736
|
-
iesProfileSlice = iesSlices.size;
|
|
3737
|
-
iesSlices.set(iesProfileHandle, iesProfileSlice);
|
|
3738
|
-
}
|
|
3739
|
-
let cookieSlice = cookieHandle === undefined ? undefined : cookieSlices.get(cookieHandle);
|
|
3740
|
-
if (
|
|
3741
|
-
cookieSlice === undefined &&
|
|
3742
|
-
cookieHandle !== undefined &&
|
|
3743
|
-
cookieData !== undefined &&
|
|
3744
|
-
cookieSlices.size < COOKIE_SLICE_CAPACITY
|
|
3745
|
-
) {
|
|
3746
|
-
cookieSlice = cookieSlices.size;
|
|
3747
|
-
cookieSlices.set(cookieHandle, cookieSlice);
|
|
3748
|
-
}
|
|
3749
|
-
let projectorSlice =
|
|
3750
|
-
projectorHandle === undefined
|
|
3751
|
-
? undefined
|
|
3752
|
-
: (projectorSlices.get(projectorHandle) ?? cookieSlices.get(projectorHandle));
|
|
3753
|
-
if (
|
|
3754
|
-
projectorSlice === undefined &&
|
|
3755
|
-
projectorHandle !== undefined &&
|
|
3756
|
-
projectorData !== undefined &&
|
|
3757
|
-
cookieSlices.size + projectorSlices.size < COOKIE_SLICE_CAPACITY
|
|
3758
|
-
) {
|
|
3759
|
-
projectorSlice = cookieSlices.size + projectorSlices.size;
|
|
3760
|
-
projectorSlices.set(projectorHandle, projectorSlice);
|
|
3761
|
-
}
|
|
3762
|
-
const shadowIntensity = s.shadowIntensity;
|
|
3763
|
-
// `shared<T>` columns are u32-backed and zero-filled when the optional
|
|
3764
|
-
// field is absent. Treat only a positive handle as an authored projector;
|
|
3765
|
-
// checking nullish values alone would turn the zero sentinel into a
|
|
3766
|
-
// phantom projector matrix and an avoidable asset-not-found diagnostic.
|
|
3767
|
-
let lightViewProj: Float32Array | undefined;
|
|
3768
|
-
let shadowAtlasTile = -1;
|
|
3769
|
-
|
|
3770
|
-
// A projector needs the same perspective transform even when its SpotLight
|
|
3771
|
-
// has no shadow atlas tile. The matrix remains a derived light fact; the
|
|
3772
|
-
// shadow tile stays -1 so shadow visibility is still independently gated.
|
|
3773
|
-
if ((castShadow || hasProjector) && hasValidDirection) {
|
|
3774
|
-
const target = vec3.create(
|
|
3775
|
-
(position[0] ?? 0) + (dirN[0] ?? 0),
|
|
3776
|
-
(position[1] ?? 0) + (dirN[1] ?? 0),
|
|
3777
|
-
(position[2] ?? 0) + (dirN[2] ?? 0),
|
|
3778
|
-
);
|
|
3779
|
-
// D-1: perspective(outerConeDeg*2, aspect=1, near, far) x lookAt(pos, pos+dir).
|
|
3780
|
-
// FOV = outerConeDeg * 2 in degrees; mat4.perspective takes fov in radians.
|
|
3781
|
-
const fov = outerConeDeg * 2 * (Math.PI / 180);
|
|
3782
|
-
const proj = mat4.create();
|
|
3783
|
-
mat4.perspective(proj, fov, 1, sNearPlane, sFarPlane);
|
|
3784
|
-
const view = mat4.create();
|
|
3785
|
-
mat4.lookAt(view, position, target, vec3.create(0, 1, 0));
|
|
3786
|
-
lightViewProj = new Float32Array(16);
|
|
3787
|
-
// Reinterpret the Float32Array surface field as a Mat4 out-param; a
|
|
3788
|
-
// factory would force a needless alloc+copy. brand-cast-ok
|
|
3789
|
-
mat4.multiply(lightViewProj as Mat4, proj, view);
|
|
3790
|
-
|
|
3791
|
-
// D-4: allocate tile 0..3; 5th+ = -1 sentinel.
|
|
3792
|
-
if (castShadow && spotTileNext < 4) {
|
|
3793
|
-
shadowAtlasTile = spotTileNext;
|
|
3794
|
-
spotTileNext += 1;
|
|
3795
|
-
}
|
|
3796
|
-
}
|
|
3797
|
-
|
|
3798
|
-
spotSnapshots.push({
|
|
3799
|
-
kind: 'spot',
|
|
3800
|
-
entity: row.entity,
|
|
3801
|
-
// D-6: position reflects world transform; direction stays sourced
|
|
3802
|
-
// from SpotLight.direction (NOT rotated by the parent).
|
|
3803
|
-
position,
|
|
3804
|
-
direction: dirN,
|
|
3805
|
-
color: vec3.create(
|
|
3806
|
-
(s.color[0] ?? 1) * intensity,
|
|
3807
|
-
(s.color[1] ?? 1) * intensity,
|
|
3808
|
-
(s.color[2] ?? 1) * intensity,
|
|
3809
|
-
),
|
|
3810
|
-
intensity,
|
|
3811
|
-
invRangeSquared: computeInvRangeSquared(range),
|
|
3812
|
-
cosInner: degToCos(innerConeDeg),
|
|
3813
|
-
cosOuter: degToCos(outerConeDeg),
|
|
3814
|
-
...(s.iesProfile === undefined
|
|
3815
|
-
? {}
|
|
3816
|
-
: {
|
|
3817
|
-
iesProfileHandle: handleSlot(s.iesProfile),
|
|
3818
|
-
...(iesProfileSlice === undefined ? {} : { iesProfileSlice }),
|
|
3819
|
-
}),
|
|
3820
|
-
...(s.cookie === undefined
|
|
3821
|
-
? {}
|
|
3822
|
-
: {
|
|
3823
|
-
cookieHandle: handleSlot(s.cookie),
|
|
3824
|
-
...(cookieSlice === undefined ? {} : { cookieSlice }),
|
|
3825
|
-
}),
|
|
3826
|
-
...(iesProfileData === undefined ? {} : { iesProfileData }),
|
|
3827
|
-
...(cookieData === undefined ? {} : { cookieData }),
|
|
3828
|
-
...(cookieProjection === undefined ? {} : { cookieMatrix: cookieProjection.matrix }),
|
|
3829
|
-
...(projectorSlice === undefined ? {} : { projectorSlice }),
|
|
3830
|
-
...(projectorData === undefined ? {} : { projectorData }),
|
|
3831
|
-
...(projectorProjection === undefined ? {} : { projectorMatrix: projectorProjection.matrix }),
|
|
3832
|
-
rollDeg: s.rollDeg,
|
|
3833
|
-
// ── shadow fields ──
|
|
3834
|
-
castShadow,
|
|
3835
|
-
lightViewProj,
|
|
3836
|
-
mapSize: sMapSize,
|
|
3837
|
-
nearPlane: sNearPlane,
|
|
3838
|
-
farPlane: sFarPlane,
|
|
3839
|
-
shadowAtlasTile,
|
|
3840
|
-
shadowIntensity,
|
|
3841
|
-
depthBias: s.depthBias,
|
|
3842
|
-
normalBias: s.normalBias,
|
|
3843
|
-
pcfKernelSize: s.pcfKernelSize,
|
|
3844
|
-
...(projectorHandle === undefined ||
|
|
3845
|
-
projectorAsset === undefined ||
|
|
3846
|
-
projectorGuid === undefined
|
|
3847
|
-
? {}
|
|
3848
|
-
: {
|
|
3849
|
-
projectorHandle,
|
|
3850
|
-
projectorAsset,
|
|
3851
|
-
projectorGuid,
|
|
3852
|
-
projectorGeneration: 1,
|
|
3853
|
-
projectorRevision: 1,
|
|
3854
|
-
}),
|
|
3855
|
-
});
|
|
3856
|
-
}
|
|
3857
|
-
|
|
3858
|
-
const rectSnapshots: RectAreaDirectLightSnapshot[] = [];
|
|
3859
|
-
const rectLightQuery = world
|
|
3860
|
-
.query({
|
|
3861
|
-
read: [RectAreaLight],
|
|
3862
|
-
optional: [Transform, GlobalTransform],
|
|
3863
|
-
})
|
|
3864
|
-
.unwrap();
|
|
3865
|
-
for (const row of rectLightQuery) {
|
|
3866
|
-
const light = row.get(RectAreaLight);
|
|
3867
|
-
const validation = validateRectAreaLightData({
|
|
3868
|
-
intensity: light.intensity,
|
|
3869
|
-
color: light.color,
|
|
3870
|
-
width: light.width,
|
|
3871
|
-
height: light.height,
|
|
3872
|
-
range: light.range,
|
|
3873
|
-
});
|
|
3874
|
-
if (!validation.ok) {
|
|
3875
|
-
worldInternal._routeError(validation.error, {
|
|
3876
|
-
severity: Severity.Error,
|
|
3877
|
-
systemName: 'RenderSystem.extract (rect-area-light)',
|
|
3878
|
-
});
|
|
3879
|
-
continue;
|
|
3880
|
-
}
|
|
3881
|
-
const worldView = row.get(GlobalTransform);
|
|
3882
|
-
const worldMat =
|
|
3883
|
-
worldView === undefined
|
|
3884
|
-
? undefined
|
|
3885
|
-
: new Float32Array(worldInternal._getArrayView(row.entity, GlobalTransform, 'world') ?? []);
|
|
3886
|
-
const position =
|
|
3887
|
-
worldMat !== undefined ? mat4.getTranslation(vec3.create(), worldMat) : vec3.create(0, 0, 0);
|
|
3888
|
-
const frame = buildRectAreaWorldFrame({
|
|
3889
|
-
center: position,
|
|
3890
|
-
axisX: [worldMat?.[0] ?? 1, worldMat?.[1] ?? 0, worldMat?.[2] ?? 0],
|
|
3891
|
-
axisY: [worldMat?.[4] ?? 0, worldMat?.[5] ?? 1, worldMat?.[6] ?? 0],
|
|
3892
|
-
width: light.width,
|
|
3893
|
-
height: light.height,
|
|
3894
|
-
});
|
|
3895
|
-
rectSnapshots.push({
|
|
3896
|
-
kind: 'rect-area',
|
|
3897
|
-
position,
|
|
3898
|
-
color: vec3.create(
|
|
3899
|
-
(light.color[0] ?? 1) * light.intensity,
|
|
3900
|
-
(light.color[1] ?? 1) * light.intensity,
|
|
3901
|
-
(light.color[2] ?? 1) * light.intensity,
|
|
3902
|
-
),
|
|
3903
|
-
intensity: light.intensity,
|
|
3904
|
-
invRangeSquared: computeInvRangeSquared(light.range),
|
|
3905
|
-
halfWidth: frame.halfWidth,
|
|
3906
|
-
halfHeight: frame.halfHeight,
|
|
3907
|
-
axisX: frame.axisX,
|
|
3908
|
-
axisY: frame.axisY,
|
|
3909
|
-
});
|
|
3910
|
-
}
|
|
3911
|
-
// The SpotLight is the sole projector owner. Once all light snapshots have
|
|
3912
|
-
// been extracted, publish its accepted TextureAsset tuple on the volume
|
|
3913
|
-
// snapshot so surface and volume consume the same identity and revision.
|
|
3914
|
-
let volumeSpot: SpotLightSnapshot | undefined;
|
|
3915
|
-
const extractedFog = volumetricFog;
|
|
3916
|
-
if (extractedFog !== undefined) {
|
|
3917
|
-
if (extractedFog.spotLightEntity !== undefined) {
|
|
3918
|
-
volumeSpot = spotSnapshots.find((light) => light.entity === extractedFog.spotLightEntity);
|
|
3919
|
-
} else if (extractedFog.lightKind === 'spot') {
|
|
3920
|
-
volumeSpot = spotSnapshots.find((light) => light.entity === extractedFog.lightEntity);
|
|
3921
|
-
}
|
|
3922
|
-
}
|
|
3923
|
-
if (
|
|
3924
|
-
volumetricFog !== undefined &&
|
|
3925
|
-
volumetricFog.status === 'available' &&
|
|
3926
|
-
volumeSpot?.projectorAsset !== undefined &&
|
|
3927
|
-
volumeSpot.projectorHandle !== undefined
|
|
3928
|
-
) {
|
|
3929
|
-
const projectorHandle = volumeSpot.projectorHandle;
|
|
3930
|
-
const projectorAsset = volumeSpot.projectorAsset;
|
|
3931
|
-
volumetricFog = {
|
|
3932
|
-
...volumetricFog,
|
|
3933
|
-
projector: {
|
|
3934
|
-
guid: volumeSpot.projectorGuid ?? `handle:${Number(volumeSpot.projectorHandle)}`,
|
|
3935
|
-
generation: volumeSpot.projectorGeneration ?? 1,
|
|
3936
|
-
view: '2d',
|
|
3937
|
-
sampler: 'linear-clamp-to-edge',
|
|
3938
|
-
projection: 'spot-projection',
|
|
3939
|
-
revision: volumeSpot.projectorRevision ?? 1,
|
|
3940
|
-
},
|
|
3941
|
-
projectorHandle,
|
|
3942
|
-
projectorAsset,
|
|
3943
|
-
};
|
|
3944
|
-
}
|
|
3945
|
-
|
|
3946
|
-
// bug-20260710-editor-cross-world-shadow: CSM matrices are computed by the
|
|
3947
|
-
// shared pure {@link computeDirectionalCsm}, called here per-world with THIS
|
|
3948
|
-
// world's own `cameras[0]`. In a single-world app the light and camera share
|
|
3949
|
-
// the world, so this per-world result is final (byte-identical to the prior
|
|
3950
|
-
// inline block). In the editor super-composite the light's world may have no
|
|
3951
|
-
// camera → this yields no matrices; {@link extractFrames} then RECOMPUTES at
|
|
3952
|
-
// the merge layer using the surfaced (cameraOwner) camera + the raw config
|
|
3953
|
-
// carried on ExtractedLights. The raw config + direction are surfaced
|
|
3954
|
-
// unconditionally so the merge layer can re-run the builder.
|
|
3955
|
-
let lightViewProj: Float32Array[] | undefined;
|
|
3956
|
-
let splitPlanes: Float32Array | undefined;
|
|
3957
|
-
let cascadeCount: number | undefined;
|
|
3958
|
-
let cascadeBlend: number | undefined;
|
|
3959
|
-
let shadowMapSize: number | undefined;
|
|
3960
|
-
let directionalShadowQuality: DirectionalShadowQuality | undefined;
|
|
3961
|
-
let directionalCsmConfig: DirectionalCsmConfig | undefined;
|
|
3962
|
-
let directionalCsmDirection: Vec3 | undefined;
|
|
3963
|
-
|
|
3964
|
-
// Camera data needed for frustum corner computation (first camera only;
|
|
3965
|
-
// multi-camera CSM is OOS-1). Undefined in a cameraless world.
|
|
3966
|
-
const cam0 = cameras[0];
|
|
3967
|
-
const cameraData: CsmCameraData | undefined =
|
|
3968
|
-
cam0 !== undefined
|
|
3969
|
-
? {
|
|
3970
|
-
world: cam0.world,
|
|
3971
|
-
fov: cam0.fov,
|
|
3972
|
-
aspect: cam0.aspect,
|
|
3973
|
-
near: cam0.near,
|
|
3974
|
-
far: cam0.far,
|
|
3975
|
-
projection: cam0.projection,
|
|
3976
|
-
orthoLeft: cam0.orthoLeft,
|
|
3977
|
-
orthoRight: cam0.orthoRight,
|
|
3978
|
-
orthoBottom: cam0.orthoBottom,
|
|
3979
|
-
orthoTop: cam0.orthoTop,
|
|
3980
|
-
}
|
|
3981
|
-
: undefined;
|
|
3982
|
-
|
|
3983
|
-
// feat-20260621 M2: CSM computation gated on castShadow from the
|
|
3984
|
-
// merged DirectionalLight. castShadow defaults to true (first-hit-wins
|
|
3985
|
-
// semantics, D-6 no cardinality cap).
|
|
3986
|
-
if (
|
|
3987
|
-
directional !== undefined &&
|
|
3988
|
-
firstHitCastShadow !== false &&
|
|
3989
|
-
directionalShadowError === undefined
|
|
3990
|
-
) {
|
|
3991
|
-
const dirSnapshot = directional;
|
|
3992
|
-
const sf = firstHitShadowFields;
|
|
3993
|
-
if (sf !== undefined) {
|
|
3994
|
-
directionalCsmConfig = {
|
|
3995
|
-
cascadeCount: sf.cascadeCount,
|
|
3996
|
-
splitLambda: sf.splitLambda,
|
|
3997
|
-
cascadeBlend: sf.cascadeBlend,
|
|
3998
|
-
mapSize: sf.mapSize,
|
|
3999
|
-
shadowDistance: sf.shadowDistance,
|
|
4000
|
-
shadowFilter: sf.shadowFilter,
|
|
4001
|
-
shadowAngularRadius: sf.shadowAngularRadius,
|
|
4002
|
-
maxPenumbraTexels: sf.maxPenumbraTexels,
|
|
4003
|
-
};
|
|
4004
|
-
directionalCsmDirection = dirSnapshot.direction;
|
|
4005
|
-
const csm = computeDirectionalCsm(dirSnapshot.direction, directionalCsmConfig, cameraData);
|
|
4006
|
-
// Cascade metadata (splitPlanes / count / blend / mapSize) is available
|
|
4007
|
-
// even without a camera (splitPlanes needs only near/far); the matrices
|
|
4008
|
-
// need the camera. When csm is null (no camera) leave lightViewProj
|
|
4009
|
-
// undefined — the merge layer recomputes. Still surface the split/count
|
|
4010
|
-
// metadata so a single-world path keeps its prior fields.
|
|
4011
|
-
cascadeCount = Math.round(sf.cascadeCount);
|
|
4012
|
-
cascadeBlend = sf.cascadeBlend;
|
|
4013
|
-
shadowMapSize = sf.mapSize;
|
|
4014
|
-
if (csm !== null) {
|
|
4015
|
-
lightViewProj = csm.lightViewProj;
|
|
4016
|
-
splitPlanes = csm.splitPlanes;
|
|
4017
|
-
directionalShadowQuality = csm.directionalShadowQuality;
|
|
4018
|
-
} else {
|
|
4019
|
-
const sNear = cameraData?.near ?? 0.1;
|
|
4020
|
-
const splits = pssmSplit(sNear, sf.shadowDistance, cascadeCount, sf.splitLambda);
|
|
4021
|
-
const padded = new Float32Array(16);
|
|
4022
|
-
for (let i = 0; i < splits.length; i++) padded[i * 4] = splits[i] ?? 0;
|
|
4023
|
-
splitPlanes = padded;
|
|
4024
|
-
}
|
|
4025
|
-
directionalShadowQuality = directionalShadowQualityFromF32(
|
|
4026
|
-
sf.shadowFilter,
|
|
4027
|
-
sf.shadowAngularRadius,
|
|
4028
|
-
sf.maxPenumbraTexels,
|
|
4029
|
-
);
|
|
4030
|
-
}
|
|
4031
|
-
}
|
|
4032
|
-
|
|
4033
|
-
// feat-20260613-csm M3 / w14 (plan-strategy §D-7): pad the up-to-4
|
|
4034
|
-
// splitPlanes into a fixed length-4 Float32Array (unused slots = 0) so
|
|
4035
|
-
// the View UBO tail keeps a stable layout regardless of the runtime
|
|
4036
|
-
// cascadeCount. Host-side correctness invariant: only the first
|
|
4037
|
-
// cascadeCount slots are ever read by the WGSL kernel.
|
|
4038
|
-
const paddedSplitPlanes = new Float32Array(16);
|
|
4039
|
-
if (splitPlanes !== undefined) {
|
|
4040
|
-
for (let i = 0; i < splitPlanes.length; i++) {
|
|
4041
|
-
paddedSplitPlanes[i] = splitPlanes[i] ?? 0;
|
|
4042
|
-
}
|
|
4043
|
-
}
|
|
4044
|
-
|
|
4045
|
-
// feat-20260612-point-light-shadows-urp-hdrp M1 / T-M1-7 (plan-strategy §D-3,
|
|
4046
|
-
// requirements §5.3): query (PointLight + PointLightShadow + Transform)
|
|
4047
|
-
// archetype join. For each shadow caster, build 6 face VP matrices and pack
|
|
4048
|
-
// into Float32Array(96). Atlas layer is assigned in spawn order while the
|
|
4049
|
-
// renderer-owned atlas has capacity; the sentinel -1 is shader-side and
|
|
4050
|
-
// applies to non-admitted PointLightShadow requests as well.
|
|
4051
|
-
const pointShadowSnapshots: PointShadowSnapshot[] = [];
|
|
4052
|
-
{
|
|
4053
|
-
const pointShadowQuery = world
|
|
4054
|
-
.query({ read: [Transform, GlobalTransform, PointLightShadow], with: [PointLight] })
|
|
4055
|
-
.unwrap();
|
|
4056
|
-
for (const row of pointShadowQuery) {
|
|
4057
|
-
const ps = row.get(PointLightShadow);
|
|
4058
|
-
// Read world-space position from GlobalTransform.world (mat4 column-major;
|
|
4059
|
-
// translation lives at indices 12..14, mirroring CameraSnapshot.world
|
|
4060
|
-
// semantics in this file).
|
|
4061
|
-
// feat-20260614 M4 / w13: TypedArrayFor for `array<f32, 16>` now
|
|
4062
|
-
// resolves to a concrete `Float32Array` (was `never` pre-w11), which
|
|
4063
|
-
// surfaces the row-window slicing -- `t.world` is the stride-16 flat
|
|
4064
|
-
// column view; row i lives at `[i*16, (i+1)*16)`. The prior
|
|
4065
|
-
// `t.world?.[i]` form silently returned a single element under the
|
|
4066
|
-
// `never`-typed bundle path and `wRow[12]` widened to `undefined ?? 0`
|
|
4067
|
-
// so light positions clamped to the origin.
|
|
4068
|
-
const wRow = row.get(GlobalTransform).world;
|
|
4069
|
-
if (wRow === undefined) continue;
|
|
4070
|
-
const px = wRow[12] ?? 0;
|
|
4071
|
-
const py = wRow[13] ?? 0;
|
|
4072
|
-
const pz = wRow[14] ?? 0;
|
|
4073
|
-
const lightPos = vec3.create(px, py, pz);
|
|
4074
|
-
const mapSize = ps.mapSize;
|
|
4075
|
-
const nearPlane = ps.nearPlane;
|
|
4076
|
-
const farPlane = ps.farPlane;
|
|
4077
|
-
// The renderer, not ECS, owns the atlas budget. Preserve every request
|
|
4078
|
-
// for inspection, but emit the shader sentinel for entries that cannot
|
|
4079
|
-
// acquire a layer so record never calls faceView outside the atlas.
|
|
4080
|
-
const layer =
|
|
4081
|
-
pointShadowSnapshots.length < SHADOW_ATLAS_DEFAULT_LAYERS
|
|
4082
|
-
? pointShadowSnapshots.length
|
|
4083
|
-
: -1;
|
|
4084
|
-
|
|
4085
|
-
const matrices = buildPointShadowMatrices(lightPos, nearPlane, farPlane);
|
|
4086
|
-
const packed = new Float32Array(96);
|
|
4087
|
-
for (let f = 0; f < 6; f++) {
|
|
4088
|
-
const m = matrices[f];
|
|
4089
|
-
if (m === undefined) continue;
|
|
4090
|
-
for (let k = 0; k < 16; k++) {
|
|
4091
|
-
packed[f * 16 + k] = m[k] ?? 0;
|
|
4092
|
-
}
|
|
4093
|
-
}
|
|
4094
|
-
pointShadowSnapshots.push({
|
|
4095
|
-
entity: row.entity,
|
|
4096
|
-
position: lightPos,
|
|
4097
|
-
mapSize,
|
|
4098
|
-
nearPlane,
|
|
4099
|
-
farPlane,
|
|
4100
|
-
shadowAtlasLayer: layer,
|
|
4101
|
-
shadowMatrices: packed,
|
|
4102
|
-
});
|
|
4103
|
-
}
|
|
4104
|
-
}
|
|
4105
|
-
|
|
4106
|
-
// feat-20260612-point-light-shadows-urp-hdrp M4 / T-M4-4 (plan-strategy §D-8):
|
|
4107
|
-
// join pointShadow snapshots into the matching PointLightSnapshot so the
|
|
4108
|
-
// record stage threads `shadowAtlasLayer + shadowNear + shadowFar` through
|
|
4109
|
-
// the unified direct-light metadata payload.
|
|
4110
|
-
// Mutates the freshly-built PointLightSnapshot in place; the snapshot is
|
|
4111
|
-
// not exposed elsewhere this frame yet (consumed only by lights.point[]).
|
|
4112
|
-
if (pointShadowSnapshots.length > 0) {
|
|
4113
|
-
const shadowByEntity = new Map<number, PointShadowSnapshot>();
|
|
4114
|
-
for (const ps of pointShadowSnapshots) shadowByEntity.set(ps.entity, ps);
|
|
4115
|
-
for (let i = 0; i < pointSnapshots.length; i++) {
|
|
4116
|
-
const entityId = pointSnapshotEntities[i] ?? 0;
|
|
4117
|
-
const ps = shadowByEntity.get(entityId);
|
|
4118
|
-
if (ps !== undefined) {
|
|
4119
|
-
pointSnapshots[i] = {
|
|
4120
|
-
...(pointSnapshots[i] as PointLightSnapshot),
|
|
4121
|
-
shadowAtlasLayer: ps.shadowAtlasLayer,
|
|
4122
|
-
shadowNear: ps.nearPlane,
|
|
4123
|
-
shadowFar: ps.farPlane,
|
|
4124
|
-
};
|
|
4125
|
-
}
|
|
4126
|
-
}
|
|
4127
|
-
}
|
|
4128
|
-
|
|
4129
|
-
const lights: ExtractedLights = {
|
|
4130
|
-
directional,
|
|
4131
|
-
directionalCount,
|
|
4132
|
-
point: pointSnapshots,
|
|
4133
|
-
spot: spotSnapshots,
|
|
4134
|
-
rect: rectSnapshots,
|
|
4135
|
-
lightViewProj,
|
|
4136
|
-
splitPlanes: splitPlanes !== undefined ? paddedSplitPlanes : undefined,
|
|
4137
|
-
cascadeCount,
|
|
4138
|
-
cascadeBlend,
|
|
4139
|
-
shadowMapSize,
|
|
4140
|
-
depthBias: firstHitCastShadow !== false ? firstHitShadowFields?.depthBias : undefined,
|
|
4141
|
-
normalBias: firstHitCastShadow !== false ? firstHitShadowFields?.normalBias : undefined,
|
|
4142
|
-
directionalShadowQuality,
|
|
4143
|
-
directionalShadowError,
|
|
4144
|
-
pointShadow: pointShadowSnapshots,
|
|
4145
|
-
// bug-20260710-editor-cross-world-shadow: raw CSM config + light direction
|
|
4146
|
-
// so the merge layer can recompute matrices against the surfaced camera.
|
|
4147
|
-
directionalCsmConfig,
|
|
4148
|
-
directionalCsmDirection,
|
|
4149
|
-
};
|
|
4150
|
-
|
|
4151
|
-
// feat-20260520-skylight-ibl-cubemap M4 / t26+t27: query Skylight entities.
|
|
4152
|
-
// First archetype hit wins (mirrors DirectionalLight pattern); multi-Skylight
|
|
4153
|
-
// warn in record stage (t27) uses skylightCount.
|
|
4154
|
-
const skylightQuery = world.query({ read: [Skylight] }).unwrap();
|
|
4155
|
-
let skylight: SkylightSnapshot | undefined;
|
|
4156
|
-
let skylightCount = 0;
|
|
4157
|
-
for (const row of skylightQuery) {
|
|
4158
|
-
const s = row.get(Skylight);
|
|
4159
|
-
// equirect is OPTIONAL: an omitted field zero-inits to handle 0, which
|
|
4160
|
-
// record treats as "no equirect" -> solid-color ambient via the white
|
|
4161
|
-
// fallback cube. A Skylight WITHOUT an equirect is still a valid snapshot
|
|
4162
|
-
// (the prior `equirectRaw !== undefined` gate dropped color-only
|
|
4163
|
-
// skylights, leaving the scene black -- the downstream gap #4).
|
|
4164
|
-
const equirectRaw = s.equirect;
|
|
4165
|
-
const intensity = s.intensity;
|
|
4166
|
-
const colorR = s.color[0] ?? 1.0;
|
|
4167
|
-
const colorG = s.color[1] ?? 1.0;
|
|
4168
|
-
const colorB = s.color[2] ?? 1.0;
|
|
4169
|
-
const rotation: [number, number, number, number] = [
|
|
4170
|
-
s.rotation[0] ?? 0,
|
|
4171
|
-
s.rotation[1] ?? 0,
|
|
4172
|
-
s.rotation[2] ?? 0,
|
|
4173
|
-
s.rotation[3] ?? 1,
|
|
4174
|
-
];
|
|
4175
|
-
skylightCount += 1;
|
|
4176
|
-
if (skylight === undefined) {
|
|
4177
|
-
skylight = {
|
|
4178
|
-
equirectHandle: equirectRaw !== undefined ? Math.round(equirectRaw) : 0,
|
|
4179
|
-
color: [colorR, colorG, colorB],
|
|
4180
|
-
intensity,
|
|
4181
|
-
rotation,
|
|
4182
|
-
// w19: winning entity handle for the multi-Skylight once-warn (F-8).
|
|
4183
|
-
entityHandle: row.entity,
|
|
4184
|
-
};
|
|
4185
|
-
}
|
|
4186
|
-
}
|
|
4187
|
-
|
|
4188
|
-
const lightProbeQuery = world.query({ read: [LightProbe, GlobalTransform] }).unwrap();
|
|
4189
|
-
const lightProbes: LightProbeSnapshot[] = [];
|
|
4190
|
-
for (const row of lightProbeQuery) {
|
|
4191
|
-
const probe = row.get(LightProbe);
|
|
4192
|
-
const validation = validateLightProbeData({
|
|
4193
|
-
irradiance: probe.irradiance,
|
|
4194
|
-
radius: probe.radius,
|
|
4195
|
-
});
|
|
4196
|
-
if (!validation.ok) {
|
|
4197
|
-
worldInternal._routeError(validation.error, {
|
|
4198
|
-
severity: Severity.Error,
|
|
4199
|
-
systemName: 'RenderSystem.extract (light-probe)',
|
|
4200
|
-
});
|
|
4201
|
-
continue;
|
|
4202
|
-
}
|
|
4203
|
-
const transform = row.get(GlobalTransform);
|
|
4204
|
-
lightProbes.push({
|
|
4205
|
-
identity: String(row.entity),
|
|
4206
|
-
position: [transform.world[12] ?? 0, transform.world[13] ?? 0, transform.world[14] ?? 0],
|
|
4207
|
-
radius: probe.radius,
|
|
4208
|
-
irradiance: new Float32Array(probe.irradiance),
|
|
4209
|
-
admitted: true,
|
|
4210
|
-
});
|
|
4211
|
-
}
|
|
4212
|
-
|
|
4213
|
-
// feat-20260531-skybox-env-background M2 / w5: query SkyboxBackground entities.
|
|
4214
|
-
// First archetype hit wins (mirrors Skylight pattern); multi-entity
|
|
4215
|
-
// once-warn in record stage uses skyboxCount.
|
|
4216
|
-
const skyboxQuery = world.query({ read: [SkyboxBackground] }).unwrap();
|
|
4217
|
-
let skybox: SkyboxSnapshot | undefined;
|
|
4218
|
-
let skyboxCount = 0;
|
|
4219
|
-
for (const row of skyboxQuery) {
|
|
4220
|
-
const s = row.get(SkyboxBackground);
|
|
4221
|
-
const equirectRaw = s.equirect;
|
|
4222
|
-
const modeRaw = s.mode;
|
|
4223
|
-
const rotation: [number, number, number, number] = [
|
|
4224
|
-
s.rotation[0] ?? 0,
|
|
4225
|
-
s.rotation[1] ?? 0,
|
|
4226
|
-
s.rotation[2] ?? 0,
|
|
4227
|
-
s.rotation[3] ?? 1,
|
|
4228
|
-
];
|
|
4229
|
-
skyboxCount += 1;
|
|
4230
|
-
if (skybox === undefined && equirectRaw !== undefined) {
|
|
4231
|
-
skybox = {
|
|
4232
|
-
equirectHandle: Math.round(equirectRaw),
|
|
4233
|
-
mode: modeRaw,
|
|
4234
|
-
rotation,
|
|
4235
|
-
// w19: winning entity handle for the multi-SkyboxBackground warn (F-8).
|
|
4236
|
-
entityHandle: row.entity,
|
|
4237
|
-
};
|
|
4238
|
-
}
|
|
4239
|
-
}
|
|
4240
|
-
|
|
4241
|
-
const environmentCandidates: EnvironmentCandidate[] = [];
|
|
4242
|
-
const imageEnvironment =
|
|
4243
|
-
skylight?.equirectHandle !== undefined && skylight.equirectHandle > 0
|
|
4244
|
-
? skylight
|
|
4245
|
-
: skybox?.equirectHandle !== undefined && skybox.equirectHandle > 0
|
|
4246
|
-
? skybox
|
|
4247
|
-
: undefined;
|
|
4248
|
-
if (imageEnvironment !== undefined) {
|
|
4249
|
-
environmentCandidates.push({
|
|
4250
|
-
kind: 'image',
|
|
4251
|
-
entityKey: imageEnvironment.entityHandle,
|
|
4252
|
-
sourceKey: `equirect:${imageEnvironment.equirectHandle}`,
|
|
4253
|
-
});
|
|
4254
|
-
}
|
|
4255
|
-
const atmosphereQuery = world.query({ read: [Atmosphere] }).unwrap();
|
|
4256
|
-
for (const row of atmosphereQuery) {
|
|
4257
|
-
const value = row.get(Atmosphere);
|
|
4258
|
-
const atmosphere = {
|
|
4259
|
-
turbidity: value.turbidity,
|
|
4260
|
-
rayleigh: value.rayleigh,
|
|
4261
|
-
mieCoefficient: value.mieCoefficient,
|
|
4262
|
-
mieDirectionalG: value.mieDirectionalG,
|
|
4263
|
-
sunAngularRadius: value.sunAngularRadius,
|
|
4264
|
-
} as const;
|
|
4265
|
-
environmentCandidates.push({
|
|
4266
|
-
kind: 'atmosphere',
|
|
4267
|
-
entityKey: row.entity,
|
|
4268
|
-
sourceKey: `atmosphere:${row.entity}`,
|
|
4269
|
-
atmosphere,
|
|
4270
|
-
});
|
|
4271
|
-
}
|
|
4272
|
-
const directionalDirection = directional?.direction;
|
|
4273
|
-
const directionalLength =
|
|
4274
|
-
directionalDirection === undefined
|
|
4275
|
-
? 0
|
|
4276
|
-
: Math.hypot(
|
|
4277
|
-
directionalDirection[0] ?? 0,
|
|
4278
|
-
directionalDirection[1] ?? 0,
|
|
4279
|
-
directionalDirection[2] ?? 0,
|
|
4280
|
-
);
|
|
4281
|
-
const sun =
|
|
4282
|
-
directional === undefined || directionalLength <= 1e-6
|
|
4283
|
-
? []
|
|
4284
|
-
: [
|
|
4285
|
-
{
|
|
4286
|
-
entityKey: 0,
|
|
4287
|
-
direction: [
|
|
4288
|
-
-(directionalDirection?.[0] ?? 0) / directionalLength,
|
|
4289
|
-
-(directionalDirection?.[1] ?? 0) / directionalLength,
|
|
4290
|
-
-(directionalDirection?.[2] ?? 0) / directionalLength,
|
|
4291
|
-
] as [number, number, number],
|
|
4292
|
-
color: [
|
|
4293
|
-
directional.color[0] ?? 0,
|
|
4294
|
-
directional.color[1] ?? 0,
|
|
4295
|
-
directional.color[2] ?? 0,
|
|
4296
|
-
] as [number, number, number],
|
|
4297
|
-
intensity: directional.intensity,
|
|
4298
|
-
},
|
|
4299
|
-
];
|
|
4300
|
-
const environmentSelection = selectEnvironment({
|
|
4301
|
-
environments: environmentCandidates,
|
|
4302
|
-
fogs: fog === undefined ? [] : [fog],
|
|
4303
|
-
suns: sun,
|
|
4304
|
-
lane: 'direct',
|
|
4305
|
-
});
|
|
4306
|
-
if (!environmentSelection.ok) {
|
|
4307
|
-
routeWorldError(world, environmentSelection.error, {
|
|
4308
|
-
systemName: 'RenderSystem.extract (environment-selection)',
|
|
4309
|
-
});
|
|
4310
|
-
throw environmentSelection.error;
|
|
4311
|
-
}
|
|
4312
|
-
const environment: EnvironmentFrame = environmentSelection.value;
|
|
4313
|
-
|
|
4314
|
-
// feat-20260528-frustum-culling M3 / w10: precompute per-camera frustum
|
|
4315
|
-
// planes so entities can be tested against all cameras in the inner loop.
|
|
4316
|
-
// Cameras with degenerate projection parameters (e.g. zero fov, zero aspect)
|
|
4317
|
-
// are skipped — entities are always-visible for those. Frustum plane cache
|
|
4318
|
-
// stored as Float32Array[] parallel to the cameras[] array.
|
|
4319
|
-
//
|
|
4320
|
-
// feat-20260708-composited-multi-world-rendering M2 / D-4: a composite
|
|
4321
|
-
// non-owner world uses the camera-owner snapshots, not its own cameras.
|
|
4322
|
-
// This keeps culling correct when the surfaced camera and renderables live
|
|
4323
|
-
// in different worlds. The explicit 'none' mode remains the always-visible
|
|
4324
|
-
// escape hatch for callers that genuinely need it.
|
|
4325
|
-
const frustumPlanes: Float32Array[] = [];
|
|
4326
|
-
const cullingCameras = cullMode === 'external' ? (context.cullCameras ?? []) : cameras;
|
|
4327
|
-
if (cullMode !== 'none') {
|
|
4328
|
-
for (const cam of cullingCameras) {
|
|
4329
|
-
// feat-20260613 M6 / w20: orthographic cameras have fov=0 by design;
|
|
4330
|
-
// the previous degeneracy guard (`fov <= 0`) was rejecting valid ortho
|
|
4331
|
-
// cameras and returning the always-visible escape hatch. Only the
|
|
4332
|
-
// perspective path needs the fov check.
|
|
4333
|
-
if (cam.projection === 'perspective' && (cam.fov <= 0 || cam.aspect <= 0)) {
|
|
4334
|
-
frustumPlanes.push(new Float32Array(0)); // degenerate → always-visible
|
|
4335
|
-
continue;
|
|
4336
|
-
}
|
|
4337
|
-
if (cam.near >= cam.far) {
|
|
4338
|
-
frustumPlanes.push(new Float32Array(0));
|
|
4339
|
-
continue;
|
|
4340
|
-
}
|
|
4341
|
-
const proj = mat4.create();
|
|
4342
|
-
if (cam.projection === 'orthographic') {
|
|
4343
|
-
mat4.orthographic(
|
|
4344
|
-
proj,
|
|
4345
|
-
cam.orthoLeft,
|
|
4346
|
-
cam.orthoRight,
|
|
4347
|
-
cam.orthoBottom,
|
|
4348
|
-
cam.orthoTop,
|
|
4349
|
-
cam.near,
|
|
4350
|
-
cam.far,
|
|
4351
|
-
);
|
|
4352
|
-
} else {
|
|
4353
|
-
mat4.perspective(proj, cam.fov, cam.aspect, cam.near, cam.far);
|
|
4354
|
-
}
|
|
4355
|
-
// feat-20260601 D-3: view = invert(camera world mat4). The camera scale is
|
|
4356
|
-
// carried in the world basis columns; the cull frustum uses the same view
|
|
4357
|
-
// the record stage derives, so cull stays same-source with render (AC-05).
|
|
4358
|
-
const view = mat4.create();
|
|
4359
|
-
mat4.invert(view, cam.world);
|
|
4360
|
-
const vp = mat4.create();
|
|
4361
|
-
mat4.multiply(vp, proj, view);
|
|
4362
|
-
const f = frustum.create();
|
|
4363
|
-
frustum.fromViewProjection(f, vp);
|
|
4364
|
-
frustumPlanes.push(f);
|
|
4365
|
-
}
|
|
4366
|
-
}
|
|
4367
|
-
|
|
4368
|
-
const renderables: RenderableSnapshot[] = [];
|
|
4369
|
-
// feat-20260528-frustum-culling M3 / w11: frustum culling counters.
|
|
4370
|
-
let frustumCulled = 0;
|
|
4371
|
-
let frustumTotal = 0;
|
|
4372
|
-
const explicitlyHidden = new Set<EntityHandle>();
|
|
4373
|
-
// feat-20260520-2d-sprite-layer-mvp M-3 / w22 (@new-surface): three-
|
|
4374
|
-
// bucket dispatch arrays. The legacy `materialDispatch` field stays as
|
|
4375
|
-
// a back-compat union (opaque + transparent + overlay back-compat
|
|
4376
|
-
// entries) so the pre-w25 RenderSystem.draw consumer loop keeps
|
|
4377
|
-
// working until M-3 / w25 lands the bucket-aware record. Plan-strategy
|
|
4378
|
-
// §6.1 (back-compat field stays until M-4 acceptance round green).
|
|
4379
|
-
// M3 / w26: single dispatch list replaces old three-bucket model
|
|
4380
|
-
// (plan-strategy D-3). Entries built per-entity per-pass inside the
|
|
4381
|
-
// archetype walk, then sorted by queue at the end.
|
|
4382
|
-
let dispatch: DispatchEntry[] = [];
|
|
4383
|
-
// Keep the derived material snapshot and its resolved passes local to this
|
|
4384
|
-
// extraction. Shared handles are immutable inputs for the frame, while
|
|
4385
|
-
// dispatch entries still need to be rebuilt for each entity.
|
|
4386
|
-
const materialSnapshotCache: MaterialSnapshotCache = new Map();
|
|
4387
|
-
// tweak-20260611 M1: MeshRenderer renderable archetype walk routes
|
|
4388
|
-
// through one World-owned Query. K-2 sniffing scheme B
|
|
4389
|
-
// (`row.get(X) !== undefined` edge sniff) replaces the prior
|
|
4390
|
-
// `arch.components.some` row-internal back-door. K-3 invariant: the
|
|
4391
|
-
// variable-length array reads (`MeshRenderer.materials`,
|
|
4392
|
-
// `Instances.transforms`) still flow through `_getArrayView` /
|
|
4393
|
-
// `world.get(e, Instances)` -- only the `entity` source switches to
|
|
4394
|
-
// `row.entity`.
|
|
4395
|
-
//
|
|
4396
|
-
// archVersion plumbing exception: the `RenderableSnapshot.instances
|
|
4397
|
-
// .archVersion` cache key is keyed off the live archetype's mutation
|
|
4398
|
-
// counter (record stage `instanceBuffers` cache invalidation). The
|
|
4399
|
-
// QueryRow does not surface this number, so a single archetype
|
|
4400
|
-
// graph access is retained for instance-bearing rows -- match by entity
|
|
4401
|
-
// packed handle. AC-01 grep ≤ current - 1 still holds: the stale
|
|
4402
|
-
// archetype-graph traversal commentary at the prior call site is gone.
|
|
4403
|
-
// feat-20260521-sprite-atlas-animation M3 / T-16: SpriteRegionOverride
|
|
4404
|
-
// column id for the sprite-bucket per-entity region override read
|
|
4405
|
-
void SpriteRegionOverride;
|
|
4406
|
-
|
|
4407
|
-
const meshRendererQuery =
|
|
4408
|
-
context.renderables === 'none'
|
|
4409
|
-
? []
|
|
4410
|
-
: world
|
|
4411
|
-
.query({
|
|
4412
|
-
read: [MeshRenderer],
|
|
4413
|
-
optional: [
|
|
4414
|
-
Transform,
|
|
4415
|
-
GlobalTransform,
|
|
4416
|
-
MeshFilter,
|
|
4417
|
-
Instances,
|
|
4418
|
-
Skin,
|
|
4419
|
-
Layer,
|
|
4420
|
-
MorphWeights,
|
|
4421
|
-
SpriteRegionOverride,
|
|
4422
|
-
SpriteInstances,
|
|
4423
|
-
Points,
|
|
4424
|
-
Lines,
|
|
4425
|
-
SortKey,
|
|
4426
|
-
],
|
|
4427
|
-
})
|
|
4428
|
-
.unwrap();
|
|
4429
|
-
// QueryRow deliberately exposes component data, not an archetype version.
|
|
4430
|
-
// The optimized ECS owner keeps table storage private, so instance-buffer
|
|
4431
|
-
// invalidation uses a content fingerprint of the managed arrays instead.
|
|
4432
|
-
const resolveArchVersion = (entity: EntityHandle): number => {
|
|
4433
|
-
const values = [
|
|
4434
|
-
worldInternal._getArrayView(entity, Instances, 'transforms'),
|
|
4435
|
-
worldInternal._getArrayView(entity, SpriteInstances, 'transforms'),
|
|
4436
|
-
worldInternal._getArrayView(entity, SpriteInstances, 'regions'),
|
|
4437
|
-
];
|
|
4438
|
-
let hash = 2166136261;
|
|
4439
|
-
for (const value of values) {
|
|
4440
|
-
hash = Math.imul(hash ^ (value?.length ?? 0), 16777619) >>> 0;
|
|
4441
|
-
if (value === undefined) continue;
|
|
4442
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
4443
|
-
hash = Math.imul(hash ^ Math.fround(value[index] ?? 0), 16777619) >>> 0;
|
|
4444
|
-
}
|
|
4445
|
-
}
|
|
4446
|
-
return hash;
|
|
4447
|
-
};
|
|
4448
|
-
// Pending entries are created with the current renderables.length, which is
|
|
4449
|
-
// exactly the slot this entity receives if it survives culling. No other
|
|
4450
|
-
// renderable can be pushed between staging and this entity's push, so publish
|
|
4451
|
-
// the original fresh entries instead of cloning every submesh descriptor.
|
|
4452
|
-
// A culled entity still discards its private pending array unchanged.
|
|
4453
|
-
const flushPendingDispatch = (pending: readonly DispatchEntry[]): void => {
|
|
4454
|
-
for (const entry of pending) dispatch.push(entry);
|
|
4455
|
-
};
|
|
4456
|
-
for (const row of meshRendererQuery) {
|
|
4457
|
-
// K-2 archetype-edge sniff (scheme B): a missing optional component
|
|
4458
|
-
// surfaces as an absent bundle key, not a row-internal optional chain.
|
|
4459
|
-
// Presence-only checks use QueryRow.has so large array-bearing components
|
|
4460
|
-
// are not materialised merely to answer a boolean question.
|
|
4461
|
-
const hasTransform = row.has(Transform);
|
|
4462
|
-
const meshFilter = row.get(MeshFilter);
|
|
4463
|
-
const hasInstances = row.has(Instances);
|
|
4464
|
-
const skin = row.get(Skin);
|
|
4465
|
-
const morphWeightsView = worldInternal._getArrayView(row.entity, MorphWeights, 'weights');
|
|
4466
|
-
const hasMeshFilter = meshFilter !== undefined;
|
|
4467
|
-
const hasSkin = skin !== undefined;
|
|
4468
|
-
// feat-20260625-sprite-instances-and-tilemap-terrain-static-batch M3 / w10:
|
|
4469
|
-
// SpriteInstances optional component archetype-edge sniff. Three structured
|
|
4470
|
-
// EcsError codes fire at the row-loop entry (D-6 fail-fast at extract):
|
|
4471
|
-
// - 'sprite-instances-mutually-exclusive-with-instances'
|
|
4472
|
-
// (hasInstances && hasSpriteInstances) — Instances + SpriteInstances peers.
|
|
4473
|
-
// - 'sprite-instances-requires-sprite-shading-model'
|
|
4474
|
-
// (materialSnap.materialShaderId !== 'forgeax::sprite') — non-sprite material.
|
|
4475
|
-
// - 'sprite-instances-count-mismatch'
|
|
4476
|
-
// (transforms.length / 16 !== regions.length / 4) — stride pair desync.
|
|
4477
|
-
const hasSpriteInstances = row.has(SpriteInstances);
|
|
4478
|
-
const points = row.get(Points);
|
|
4479
|
-
const lines = row.get(Lines);
|
|
4480
|
-
const sortKey = row.get(SortKey)?.value;
|
|
4481
|
-
const pointsLinesComponent =
|
|
4482
|
-
points !== undefined ? 'Points' : lines !== undefined ? 'Lines' : undefined;
|
|
4483
|
-
const isRenderable = hasTransform && hasMeshFilter;
|
|
4484
|
-
|
|
4485
|
-
// The resolved world transform is read per-entity from the single
|
|
4486
|
-
// `GlobalTransform.world` mat4 (propagateTransforms output) inside the row
|
|
4487
|
-
// loop below. Normal structural paths materialize this carrier through
|
|
4488
|
-
// Transform's generic ECS requirement; keep the defensive row check because
|
|
4489
|
-
// an owner can explicitly remove a required component during recovery.
|
|
4490
|
-
const fAssetHandle = meshFilter?.assetHandle;
|
|
4491
|
-
// feat-20260520-2d-sprite-layer-mvp M-3 / w22: Layer column read here;
|
|
4492
|
-
// value folded into each DispatchEntry so the render-system sort can use
|
|
4493
|
-
// it as the primary transparent-sort key without a second ECS round-trip.
|
|
4494
|
-
const fLayerValue = row.get(Layer)?.value;
|
|
4495
|
-
// feat-20260608-tilemap-object-layer-rendering M3 / m3-t5: tilemap-spawned
|
|
4496
|
-
// per-cell render entities (the ones `tilemap-chunk-extract-system`
|
|
4497
|
-
// pushes via `spawnDerivedRenderEntities`) reach this loop via the same
|
|
4498
|
-
// archetype edge that carries a sprite entity -- they all wear
|
|
4499
|
-
// `MeshFilter.assetHandle === HANDLE_QUAD` + a `forgeax::sprite`-shaded
|
|
4500
|
-
// material asset + the sprite-bucket `values.region` rectangle.
|
|
4501
|
-
// For the per-entity Y-sort path (requirements §AC-12 / §AC-13):
|
|
4502
|
-
//
|
|
4503
|
-
// sortKey = -(Transform.posY - effectivePivotY * |Transform.scaleY|)
|
|
4504
|
-
//
|
|
4505
|
-
// with `effectivePivotY = effectivePivotYForTilemapFlip(pivotY, pivotX,
|
|
4506
|
-
// flipV, flipDiagonal)` from `tilemap-chunk-extract-system` (the SAME
|
|
4507
|
-
// helper drives `spawnDerivedRenderEntities`, so the value the sort
|
|
4508
|
-
// uses matches the value baked into `Transform.posY` -- charter P4
|
|
4509
|
-
// single SSOT for the post-flip pivot). Sprite entities reuse the same
|
|
4510
|
-
// formula but skip the flip composition (their pivot stays raw); both
|
|
4511
|
-
// bucket types therefore feed one `transparentSortEntries` argsort
|
|
4512
|
-
// step + share the `argsortInPlace` radix LSD primitive (plan-strategy
|
|
4513
|
-
// §D-1 / §D-3). The detection lives on the material side -- detect a
|
|
4514
|
-
// tilemap-spawned entity by `MeshFilter.assetHandle === HANDLE_QUAD`
|
|
4515
|
-
// plus the `forgeax::sprite` shader id on `MeshRenderer.material`'s
|
|
4516
|
-
// first pass + non-empty `values.region`; no new public ECS
|
|
4517
|
-
// marker component lands (charter F1 minimum surface).
|
|
4518
|
-
//
|
|
4519
|
-
// Layer.value is now folded into each DispatchEntry.layer (fLayerValue
|
|
4520
|
-
// column, read once per archetype pass above). render-system.ts
|
|
4521
|
-
// `sortTransparentDispatch` applies (layer ASC, sortValue ASC) for all
|
|
4522
|
-
// transparent-sort modes (0/1/2) using posY/pivotY/sizeY from the
|
|
4523
|
-
// parallel renderables[] snapshot -- no second ECS round-trip needed.
|
|
4524
|
-
// feat-20260527-sprite-nineslice M4 / w17 (AC-14): SpriteRegionOverride
|
|
4525
|
-
// per-entity UV sub-rectangle. When the entity carries this component the
|
|
4526
|
-
// 4-float `[uMin, vMin, uW, vH]` override displaces the asset-side
|
|
4527
|
-
// `values.region` for this entity only — downstream 9-slice logic
|
|
4528
|
-
// measures slices against this effective region.zw, so a half-width sub-
|
|
4529
|
-
// sprite reduces the anchor budget to 0.5 rather than the asset's 1.0.
|
|
4530
|
-
//
|
|
4531
|
-
// SpriteRegionOverride.region is a fixed stride-4 value. Per-row reads
|
|
4532
|
-
// route through `_getArrayView` for the zero-copy row window
|
|
4533
|
-
// row-window slice (consistent with the variable-length array column
|
|
4534
|
-
// reads -- K-3 carve-out keeps `_getArrayView` as the row-accessor of
|
|
4535
|
-
// record for each non-scalar column).
|
|
4536
|
-
const hasSpriteRegionOverride = row.has(SpriteRegionOverride);
|
|
4537
|
-
// feat-20260523-skin-skeleton-animation M2 / T-21: Skin component
|
|
4538
|
-
// column views for coexistence check + joint despawn fail-fast.
|
|
4539
|
-
// `skeleton` holds the packed Handle<SkeletonAsset>; `joints` holds
|
|
4540
|
-
// the packed Entity u32 array (N x one u32 each).
|
|
4541
|
-
const skinSkeletonView = skin?.skeleton;
|
|
4542
|
-
|
|
4543
|
-
let archVersion = 0;
|
|
4544
|
-
if (hasInstances || hasSpriteInstances) {
|
|
4545
|
-
// All rows in this callback share one archetype. Only these two
|
|
4546
|
-
// instance-bearing paths consume the version in their cache key.
|
|
4547
|
-
archVersion = resolveArchVersion(row.entity);
|
|
4548
|
-
}
|
|
4549
|
-
|
|
4550
|
-
{
|
|
4551
|
-
// feat-20260608 M2 / w11: read materials array via _getArrayView
|
|
4552
|
-
const entity = row.entity;
|
|
4553
|
-
if (isRenderable && visibility?.effective(entity) === 'hidden') {
|
|
4554
|
-
explicitlyHidden.add(entity);
|
|
4555
|
-
continue;
|
|
4556
|
-
}
|
|
4557
|
-
const layerVal = fLayerValue ?? 0;
|
|
4558
|
-
// bug-20260709-builtin-quad-withoutaabb-disables-sprite-frustum-cu M2.5
|
|
4559
|
-
// (carries PR #598 feat-20260703 D-7): dispatch entries for this entity
|
|
4560
|
-
// are staged locally and flushed into the shared `dispatch[]` array
|
|
4561
|
-
// ONLY when the entity survives the frustum-cull `continue` below —
|
|
4562
|
-
// same cull-passed branch as the paired `renderables.push`. Prior to
|
|
4563
|
-
// this fix the three `dispatch.push` sites ran before the cull check,
|
|
4564
|
-
// so a culled entity left dangling entries whose `renderableIndex`
|
|
4565
|
-
// aliased the slot a LATER visible entity occupied — surfacing as the
|
|
4566
|
-
// pbr-mesh-array-bgl vs hdrp-unified-bgl-group2 BGL/PL mismatch on
|
|
4567
|
-
// the deferred-shading smoke (PR #598 CI). Pure ordering fix; cull
|
|
4568
|
-
// logic and MeshRenderer contract unchanged.
|
|
4569
|
-
const pendingDispatch: DispatchEntry[] = [];
|
|
4570
|
-
const materialsView = worldInternal._getArrayView(entity, MeshRenderer, 'materials') as
|
|
4571
|
-
| Uint32Array
|
|
4572
|
-
| undefined;
|
|
4573
|
-
const materialCount = materialsView?.length ?? 0;
|
|
4574
|
-
let materialHandles = Array.from(materialsView ?? []);
|
|
4575
|
-
let materialBindingSources: MeshMaterialBindingSource[] = materialHandles.map(
|
|
4576
|
-
() => 'renderer-override',
|
|
4577
|
-
);
|
|
4578
|
-
let materialBindingDiagnostics: MeshMaterialBindingDiagnostic[] = [];
|
|
4579
|
-
let gpuDrivenSubmeshes: readonly MeshAsset['submeshes'][number][] = [];
|
|
4580
|
-
let gpuDrivenIndexed = false;
|
|
4581
|
-
let gpuDrivenGeometry: GpuDrivenGeometryReceipt | undefined;
|
|
4582
|
-
|
|
4583
|
-
// Resolve instance overrides against mesh-owned slot defaults once.
|
|
4584
|
-
const fAssetHandleVal = fAssetHandle;
|
|
4585
|
-
if (
|
|
4586
|
-
fAssetHandleVal !== undefined &&
|
|
4587
|
-
fAssetHandleVal !== 0 &&
|
|
4588
|
-
assets !== undefined &&
|
|
4589
|
-
assets !== null
|
|
4590
|
-
) {
|
|
4591
|
-
const meshHandle = toShared<'MeshAsset'>(fAssetHandleVal);
|
|
4592
|
-
const meshRes = resolveAssetHandle<Asset>(world, meshHandle);
|
|
4593
|
-
if (meshRes.ok && meshRes.value.kind === 'mesh') {
|
|
4594
|
-
const meshAsset = meshRes.value as MeshAsset;
|
|
4595
|
-
const guid = (meshRes.value as { guid?: string }).guid ?? '<no-guid>';
|
|
4596
|
-
gpuDrivenSubmeshes = meshAsset.submeshes;
|
|
4597
|
-
gpuDrivenIndexed = meshAsset.indices !== undefined;
|
|
4598
|
-
const layoutProjection = deriveVertexLayoutProjection(meshAsset.attributes);
|
|
4599
|
-
gpuDrivenGeometry = {
|
|
4600
|
-
identity: `${guid}:${layoutProjection.digest}`,
|
|
4601
|
-
vertexInputs: layoutProjection.attributes.map((attribute) => ({
|
|
4602
|
-
semantic: attribute.key,
|
|
4603
|
-
location: attribute.shaderLocation,
|
|
4604
|
-
format: attribute.format,
|
|
4605
|
-
})),
|
|
4606
|
-
topology: meshAsset.submeshes[0]?.topology ?? 'triangle-list',
|
|
4607
|
-
indexed: gpuDrivenIndexed,
|
|
4608
|
-
};
|
|
4609
|
-
// extractFrames is also a public read path and can run before the
|
|
4610
|
-
// Renderer.draw pre-render stage. GlyphText proves the derived mesh
|
|
4611
|
-
// owns exactly one Default slot; arbitrary meshes still fail closed
|
|
4612
|
-
// and never infer slot topology from their submeshes.
|
|
4613
|
-
if (!Array.isArray(meshAsset.materialSlots) && world.get(entity, GlyphText).ok) {
|
|
4614
|
-
ensureGlyphMeshMaterialSlots(world, meshHandle);
|
|
4615
|
-
}
|
|
4616
|
-
const resolvedBindings = resolveMeshMaterialBindings(meshAsset, materialsView ?? [], {
|
|
4617
|
-
isValidOverride(handle) {
|
|
4618
|
-
const resolved = resolveAssetHandle(world, toShared<'MaterialAsset'>(handle));
|
|
4619
|
-
return resolved.ok && resolved.value.kind === 'material';
|
|
4620
|
-
},
|
|
4621
|
-
resolveMeshDefault(defaultGuid) {
|
|
4622
|
-
const guidText = AssetGuid.format(defaultGuid);
|
|
4623
|
-
if (assets.lookup<Asset>(guidText)?.kind !== 'material') return undefined;
|
|
4624
|
-
return internSharedRefFromGuid(world, assets, guidText, 'MaterialAsset') as
|
|
4625
|
-
| number
|
|
4626
|
-
| undefined;
|
|
4627
|
-
},
|
|
4628
|
-
});
|
|
4629
|
-
if (!resolvedBindings.ok) {
|
|
4630
|
-
if (resolvedBindings.code === 'mesh-material-slots-missing') {
|
|
4631
|
-
worldInternal._routeError(
|
|
4632
|
-
new AssetError({
|
|
4633
|
-
code: 'load-failed',
|
|
4634
|
-
expected: 'every MeshAsset producer supplies materialSlots[]',
|
|
4635
|
-
hint: `fix the MeshAsset producer; renderer inheritance never guesses slot topology (mesh=${guid}, entity=${entity}, vertices=${meshAsset.vertices.length}, indices=${meshAsset.indices?.length ?? 0})`,
|
|
4636
|
-
detail: {
|
|
4637
|
-
referencedByGuid: guid,
|
|
4638
|
-
referencedByKind: 'mesh',
|
|
4639
|
-
subAssetGuid: '<material-slots-missing>',
|
|
4640
|
-
sourceField: { fieldName: 'materialSlots' },
|
|
4641
|
-
},
|
|
4642
|
-
}),
|
|
4643
|
-
{
|
|
4644
|
-
severity: Severity.Error,
|
|
4645
|
-
systemName: 'RenderSystem.extract (mesh-material-slots-missing)',
|
|
4646
|
-
},
|
|
4647
|
-
);
|
|
4648
|
-
continue;
|
|
4649
|
-
}
|
|
4650
|
-
const materialGuid = AssetGuid.format(resolvedBindings.defaultMaterial);
|
|
4651
|
-
worldInternal._routeError(
|
|
4652
|
-
new AssetError({
|
|
4653
|
-
code: 'load-failed',
|
|
4654
|
-
expected: `MeshAsset materialSlots[${resolvedBindings.slotIndex}] default ${materialGuid} is ready and a MaterialAsset`,
|
|
4655
|
-
hint: `loadByGuid(meshGuid) must recursively load the declared default material; mesh=${guid}, slot=${resolvedBindings.slotIndex}, material=${materialGuid}`,
|
|
4656
|
-
detail: {
|
|
4657
|
-
referencedByGuid: guid,
|
|
4658
|
-
referencedByKind: 'mesh',
|
|
4659
|
-
subAssetGuid: materialGuid,
|
|
4660
|
-
sourceField: {
|
|
4661
|
-
fieldName: 'materialSlots',
|
|
4662
|
-
arrayIndex: resolvedBindings.slotIndex,
|
|
4663
|
-
},
|
|
4664
|
-
},
|
|
4665
|
-
}),
|
|
4666
|
-
{
|
|
4667
|
-
severity: Severity.Error,
|
|
4668
|
-
systemName: 'RenderSystem.extract (mesh-default-not-ready)',
|
|
4669
|
-
},
|
|
4670
|
-
);
|
|
4671
|
-
continue;
|
|
4672
|
-
}
|
|
4673
|
-
materialHandles = resolvedBindings.bindings.map((binding) => binding.handle);
|
|
4674
|
-
materialBindingSources = resolvedBindings.bindings.map((binding) => binding.source);
|
|
4675
|
-
materialBindingDiagnostics = resolvedBindings.diagnostics.map((diagnostic) => ({
|
|
4676
|
-
...diagnostic,
|
|
4677
|
-
detail:
|
|
4678
|
-
diagnostic.code === 'mesh-renderer-material-override-overflow'
|
|
4679
|
-
? {
|
|
4680
|
-
expectedCount: meshAsset.materialSlots.length,
|
|
4681
|
-
actualCount: materialCount,
|
|
4682
|
-
meshAssetGuid: guid,
|
|
4683
|
-
}
|
|
4684
|
-
: {
|
|
4685
|
-
meshAssetGuid: guid,
|
|
4686
|
-
slotIndex: diagnostic.slotIndex,
|
|
4687
|
-
handle: diagnostic.handle ?? 0,
|
|
4688
|
-
},
|
|
4689
|
-
}));
|
|
4690
|
-
for (const diagnostic of resolvedBindings.diagnostics) {
|
|
4691
|
-
worldInternal._routeError(
|
|
4692
|
-
new AssetError({
|
|
4693
|
-
code: diagnostic.code,
|
|
4694
|
-
expected:
|
|
4695
|
-
diagnostic.code === 'mesh-renderer-material-override-overflow'
|
|
4696
|
-
? `materials.length <= materialSlots.length (${meshAsset.materialSlots.length})`
|
|
4697
|
-
: `materials[${diagnostic.slotIndex}] resolves to a live MaterialAsset`,
|
|
4698
|
-
hint: ASSET_ERROR_HINTS[diagnostic.code],
|
|
4699
|
-
detail:
|
|
4700
|
-
diagnostic.code === 'mesh-renderer-material-override-overflow'
|
|
4701
|
-
? {
|
|
4702
|
-
expectedCount: meshAsset.materialSlots.length,
|
|
4703
|
-
actualCount: materialCount,
|
|
4704
|
-
meshAssetGuid: guid,
|
|
4705
|
-
}
|
|
4706
|
-
: {
|
|
4707
|
-
meshAssetGuid: guid,
|
|
4708
|
-
slotIndex: diagnostic.slotIndex,
|
|
4709
|
-
handle: diagnostic.handle ?? 0,
|
|
4710
|
-
},
|
|
4711
|
-
}),
|
|
4712
|
-
{
|
|
4713
|
-
severity: Severity.Warning,
|
|
4714
|
-
systemName: `RenderSystem.extract (${diagnostic.code})`,
|
|
4715
|
-
},
|
|
4716
|
-
);
|
|
4717
|
-
}
|
|
4718
|
-
}
|
|
4719
|
-
}
|
|
4720
|
-
|
|
4721
|
-
if (materialHandles.length === 0) {
|
|
4722
|
-
materialHandles = [0];
|
|
4723
|
-
materialBindingSources = ['engine-default'];
|
|
4724
|
-
}
|
|
4725
|
-
|
|
4726
|
-
// Use the first material handle for the entity-level snapshot
|
|
4727
|
-
// (shading-model dispatch routing + multi-pass DispatchEntry. Per-
|
|
4728
|
-
// submesh materials[i>=1] are resolved by `resolveMaterialSnapshot`
|
|
4729
|
-
// below into the `materials[]` array, used by the record stage to
|
|
4730
|
-
// upload N material UBO slots and bind the i-th slot before the
|
|
4731
|
-
// i-th submesh draw.) -- feat-20260608 M5 amend / w11-a.
|
|
4732
|
-
const handleRaw = materialHandles[0] ?? 0;
|
|
4733
|
-
|
|
4734
|
-
const cachedMaterial =
|
|
4735
|
-
handleRaw !== 0 && !hasSpriteRegionOverride && !hasSkin
|
|
4736
|
-
? materialSnapshotCache.get(handleRaw)
|
|
4737
|
-
: undefined;
|
|
4738
|
-
|
|
4739
|
-
let materialSnap: MaterialSnapshot;
|
|
4740
|
-
|
|
4741
|
-
if (cachedMaterial !== undefined) {
|
|
4742
|
-
materialSnap = cachedMaterial.snapshot;
|
|
4743
|
-
if (isRenderable) {
|
|
4744
|
-
appendMaterialDispatchEntries(
|
|
4745
|
-
pendingDispatch,
|
|
4746
|
-
cachedMaterial.passes,
|
|
4747
|
-
entity,
|
|
4748
|
-
handleRaw,
|
|
4749
|
-
renderables.length,
|
|
4750
|
-
layerVal,
|
|
4751
|
-
materialSnap.paramSnapshot,
|
|
4752
|
-
);
|
|
4753
|
-
}
|
|
4754
|
-
} else if (handleRaw === 0 || assets === undefined || assets === null) {
|
|
4755
|
-
// case B: missing-spec sentinel -> mid-grey defaultMaterialSnapshot.
|
|
4756
|
-
materialSnap = defaultMaterialSnapshot(handleRaw);
|
|
4757
|
-
} else {
|
|
4758
|
-
const tagged = toShared<'MaterialAsset'>(handleRaw);
|
|
4759
|
-
const stablePersistentCached =
|
|
4760
|
-
!hasSpriteRegionOverride && !hasSkin
|
|
4761
|
-
? readStablePersistentMaterialSnapshot(
|
|
4762
|
-
persistentMaterialSnapshotCache,
|
|
4763
|
-
handleRaw,
|
|
4764
|
-
assets,
|
|
4765
|
-
)
|
|
4766
|
-
: undefined;
|
|
4767
|
-
if (stablePersistentCached !== undefined) {
|
|
4768
|
-
materialSnapshotCache.set(handleRaw, stablePersistentCached);
|
|
4769
|
-
materialSnap = stablePersistentCached.snapshot;
|
|
4770
|
-
if (isRenderable) {
|
|
4771
|
-
appendMaterialDispatchEntries(
|
|
4772
|
-
pendingDispatch,
|
|
4773
|
-
stablePersistentCached.passes,
|
|
4774
|
-
entity,
|
|
4775
|
-
handleRaw,
|
|
4776
|
-
renderables.length,
|
|
4777
|
-
layerVal,
|
|
4778
|
-
materialSnap.paramSnapshot,
|
|
4779
|
-
);
|
|
4780
|
-
}
|
|
4781
|
-
} else {
|
|
4782
|
-
const res = resolveAssetHandle(world, tagged);
|
|
4783
|
-
if (!res.ok) {
|
|
4784
|
-
if (isRenderable) {
|
|
4785
|
-
const rhiErr = new RhiError({
|
|
4786
|
-
code: 'asset-not-registered',
|
|
4787
|
-
expected: 'MeshRenderer.material in AssetRegistry',
|
|
4788
|
-
hint: 'catalog the material via assetRegistry.catalog(guid, asset) + world.allocSharedRef before spawn, or remove the material field to fall back to default',
|
|
4789
|
-
detail: { assetHandle: handleRaw },
|
|
4790
|
-
});
|
|
4791
|
-
worldInternal._routeError(rhiErr, {
|
|
4792
|
-
severity: Severity.Error,
|
|
4793
|
-
systemName: 'RenderSystem.extract (material asset-not-registered)',
|
|
4794
|
-
});
|
|
4795
|
-
}
|
|
4796
|
-
continue;
|
|
4797
|
-
}
|
|
4798
|
-
const asset = res.value;
|
|
4799
|
-
if (asset.kind !== 'material') {
|
|
4800
|
-
materialSnap = defaultMaterialSnapshot(handleRaw);
|
|
4801
|
-
} else {
|
|
4802
|
-
// feat-20260529 M3 / w11: material parent chain inheritance via
|
|
4803
|
-
// read-through _materialWalk accessor (plan-strategy D-6).
|
|
4804
|
-
// The old direct asset.passes / asset.values read never
|
|
4805
|
-
// walked the parent chain, causing broken-inheritance (root cause).
|
|
4806
|
-
const resolvedResult = walkMaterialPassesOverSharedRefs(world, tagged, assets);
|
|
4807
|
-
if (!resolvedResult.ok) {
|
|
4808
|
-
// AC-09 / S-7 / q8=A: passes-empty or cycle must fire structured
|
|
4809
|
-
// error through _routeError (same routing as asset-not-registered
|
|
4810
|
-
// branch above). Silent continue is forbidden because it produces
|
|
4811
|
-
// a black screen indistinguishable from a content bug.
|
|
4812
|
-
const err = resolvedResult.error;
|
|
4813
|
-
switch (err.code) {
|
|
4814
|
-
case 'material-parent-not-found':
|
|
4815
|
-
case 'material-no-effective-pass':
|
|
4816
|
-
case 'material-value-unknown':
|
|
4817
|
-
case 'material-value-type-mismatch':
|
|
4818
|
-
case 'material-contract-program-mismatch':
|
|
4819
|
-
worldInternal._routeError(err, {
|
|
4820
|
-
severity: Severity.Error,
|
|
4821
|
-
systemName: `RenderSystem.extract (${err.code})`,
|
|
4822
|
-
});
|
|
4823
|
-
break;
|
|
4824
|
-
case 'material-circular-inheritance':
|
|
4825
|
-
worldInternal._routeError(err, {
|
|
4826
|
-
severity: Severity.Error,
|
|
4827
|
-
systemName: 'RenderSystem.extract (material-circular-inheritance)',
|
|
4828
|
-
});
|
|
4829
|
-
break;
|
|
4830
|
-
default:
|
|
4831
|
-
// Exhaustive guard: unhandled error codes from _materialWalk
|
|
4832
|
-
// surface an internal assertion to avoid silent continuation.
|
|
4833
|
-
worldInternal._routeError(err, {
|
|
4834
|
-
severity: Severity.Error,
|
|
4835
|
-
systemName: `RenderSystem.extract (_materialWalk: ${err.code})`,
|
|
4836
|
-
});
|
|
4837
|
-
}
|
|
4838
|
-
continue;
|
|
4839
|
-
}
|
|
4840
|
-
const resolved = resolvedResult.value;
|
|
4841
|
-
const allPasses = resolved.passes;
|
|
4842
|
-
const firstPassShader =
|
|
4843
|
-
allPasses.length > 0
|
|
4844
|
-
? runtimeMaterialShaderId(allPasses[0]?.program.module, allPasses[0]?.name)
|
|
4845
|
-
: undefined;
|
|
4846
|
-
const pv = materialValuesToLinearRuntime(
|
|
4847
|
-
resolved.values,
|
|
4848
|
-
materialColorParameterSchema(resolved.parameters ?? [], firstPassShader, assets),
|
|
4849
|
-
resolved.colorSpace,
|
|
4850
|
-
) as Readonly<Record<string, unknown>>;
|
|
4851
|
-
|
|
4852
|
-
const baseColorPv = pv.baseColor as readonly number[] | undefined;
|
|
4853
|
-
const baseColor = vec3.create(
|
|
4854
|
-
baseColorPv?.[0] ?? 1,
|
|
4855
|
-
baseColorPv?.[1] ?? 1,
|
|
4856
|
-
baseColorPv?.[2] ?? 1,
|
|
4857
|
-
);
|
|
4858
|
-
const metallicPv = typeof pv.metallic === 'number' ? pv.metallic : 0;
|
|
4859
|
-
const roughnessPv = typeof pv.roughness === 'number' ? pv.roughness : 0.5;
|
|
4860
|
-
const clearcoatPv = typeof pv.clearcoat === 'number' ? pv.clearcoat : 0;
|
|
4861
|
-
const clearcoatRoughnessPv =
|
|
4862
|
-
typeof pv.clearcoatRoughness === 'number' ? pv.clearcoatRoughness : 0.5;
|
|
4863
|
-
const specularTintPv = pv.specularTint as readonly number[] | undefined;
|
|
4864
|
-
const normalScalePv = materialNormalScale(pv);
|
|
4865
|
-
|
|
4866
|
-
const paramSnap: Record<string, number | number[] | string> = {};
|
|
4867
|
-
for (const [k, v] of Object.entries(pv)) {
|
|
4868
|
-
if (typeof v === 'number') paramSnap[k] = v;
|
|
4869
|
-
else if (typeof v === 'string') paramSnap[k] = v;
|
|
4870
|
-
else if (Array.isArray(v) && v.every((x) => typeof x === 'number')) {
|
|
4871
|
-
paramSnap[k] = v as number[];
|
|
4872
|
-
}
|
|
4873
|
-
}
|
|
4874
|
-
|
|
4875
|
-
const materialParamSchema = materialParametersToParamSchema(
|
|
4876
|
-
resolved.parameters ?? [],
|
|
4877
|
-
firstPassShader,
|
|
4878
|
-
);
|
|
4879
|
-
// feat-20260611-fox-skinning-vertex-attribute-chain M4 / w17 (D-5):
|
|
4880
|
-
// bidirectional Skin <-> pbr-skin material fail-fast at extract.
|
|
4881
|
-
// Skin component without a forgeax::pbr-skin first-pass material
|
|
4882
|
-
// would draw with a non-skin shader against the 18-float vertex
|
|
4883
|
-
// buffer (joints/weights bytes interpreted as garbage). Conversely
|
|
4884
|
-
// a forgeax::pbr-skin material against a 12-float (unskinned) mesh
|
|
4885
|
-
// would have @location(4)/@location(5) read uninitialized memory.
|
|
4886
|
-
// Both cases route through `_routeError` + `continue` so a single
|
|
4887
|
-
// misconfigured entity does NOT abort the whole frame's draw list
|
|
4888
|
-
// (charter P3 explicit failure + plan-decisions D-5 over `return err`).
|
|
4889
|
-
{
|
|
4890
|
-
const hasSkinSkel =
|
|
4891
|
-
hasSkin &&
|
|
4892
|
-
skinSkeletonView !== undefined &&
|
|
4893
|
-
skinSkeletonView !== undefined &&
|
|
4894
|
-
skinSkeletonView !== 0;
|
|
4895
|
-
const isPbrSkinMaterial = firstPassShader === 'forgeax::pbr-skin';
|
|
4896
|
-
if (hasSkinSkel && !isPbrSkinMaterial) {
|
|
4897
|
-
worldInternal._routeError(new SkinMaterialMismatchError(entity, firstPassShader), {
|
|
4898
|
-
severity: Severity.Error,
|
|
4899
|
-
systemName: 'RenderSystem.extract (skin-material-mismatch)',
|
|
4900
|
-
});
|
|
4901
|
-
continue;
|
|
4902
|
-
}
|
|
4903
|
-
if (isPbrSkinMaterial && fAssetHandleVal !== undefined && fAssetHandleVal !== 0) {
|
|
4904
|
-
const meshHandleForSkinCheck = toShared<'MeshAsset'>(fAssetHandleVal);
|
|
4905
|
-
const meshResForSkinCheck = resolveAssetHandle<MeshAsset>(
|
|
4906
|
-
world,
|
|
4907
|
-
meshHandleForSkinCheck,
|
|
4908
|
-
);
|
|
4909
|
-
if (meshResForSkinCheck.ok) {
|
|
4910
|
-
const meshAttrs = meshResForSkinCheck.value.attributes;
|
|
4911
|
-
const hasSkinIdx = meshAttrs.skinIndex !== undefined;
|
|
4912
|
-
const hasSkinWt = meshAttrs.skinWeight !== undefined;
|
|
4913
|
-
if (!hasSkinIdx || !hasSkinWt) {
|
|
4914
|
-
const missing: 'skinIndex' | 'skinWeight' | 'both' =
|
|
4915
|
-
!hasSkinIdx && !hasSkinWt ? 'both' : !hasSkinIdx ? 'skinIndex' : 'skinWeight';
|
|
4916
|
-
worldInternal._routeError(new MaterialSkinAttrMissingError(entity, missing), {
|
|
4917
|
-
severity: Severity.Error,
|
|
4918
|
-
systemName: 'RenderSystem.extract (material-skin-attr-missing)',
|
|
4919
|
-
});
|
|
4920
|
-
continue;
|
|
4921
|
-
}
|
|
4922
|
-
}
|
|
4923
|
-
}
|
|
4924
|
-
}
|
|
4925
|
-
// feat-20260625-refactor-sprite-as-transparent-mesh M3 / w12 (D-3):
|
|
4926
|
-
// sprite materials now flow through the same generic paramSchema-
|
|
4927
|
-
// driven extract path PBR / unlit use. The narrow `forgeax::sprite`
|
|
4928
|
-
// exception block below covers exactly 2 plan-authorised cases:
|
|
4929
|
-
// 1. SpriteRegionOverride per-entity region displacement (Q4=a)
|
|
4930
|
-
// 2. flipX / flipY -> region fold (plan-strategy D-8)
|
|
4931
|
-
// No legacy values field-name shim; demos and SpriteParamValues
|
|
4932
|
-
// are UBO-aligned (no `texture` / `baseColor` / `pivot` / `slices`
|
|
4933
|
-
// / `sliceMode` keys reaching this code path). AGENTS.md §Change
|
|
4934
|
-
// stance: "no shim layer, no v1/v2 dual-path".
|
|
4935
|
-
//
|
|
4936
|
-
// feat-20260624 M1' / t6: `'forgeax::sprite-lit'` walks the same
|
|
4937
|
-
// sprite-family vertex path (VsOut byte-identical, paramSchema
|
|
4938
|
-
// mirror) so the SAME 2 folds apply — extending `isSprite` to
|
|
4939
|
-
// cover both shader ids keeps the narrowing-point count at 1
|
|
4940
|
-
// (plan-strategy §1.6 + D-1: mirror sprite, no new branch).
|
|
4941
|
-
const isSprite =
|
|
4942
|
-
firstPassShader === 'forgeax::sprite' || firstPassShader === 'forgeax::sprite-lit';
|
|
4943
|
-
|
|
4944
|
-
// feat-20260613-material-paramschema-driven-binding M4 / w23
|
|
4945
|
-
// (D-5 graceful): paramSchema-driven texture-field validation.
|
|
4946
|
-
// For each handle-shaped paramValue (typeof === 'number'),
|
|
4947
|
-
// verify it actually points at a registered texture asset
|
|
4948
|
-
// when the field is declared as a texture in the shader's
|
|
4949
|
-
// paramSchema; mis-typed handles (e.g. a scalar f32 stored as
|
|
4950
|
-
// int 0 the M4 / w22 graceful fallback resolved to a wrong
|
|
4951
|
-
// sub-asset) are dropped here so the record stage falls back
|
|
4952
|
-
// to MISSING_TEXTURE_HANDLE (default white) without raising.
|
|
4953
|
-
const validateTextureHandle = (
|
|
4954
|
-
fieldName: string,
|
|
4955
|
-
raw: unknown,
|
|
4956
|
-
): Handle<'TextureAsset', 'shared'> | undefined => {
|
|
4957
|
-
// feat-20260614 M8 (D-19): a string value is an embedded texture
|
|
4958
|
-
// GUID; resolve it to a column handle via catalog + allocSharedRef
|
|
4959
|
-
// before validation. A number is an already-minted column handle.
|
|
4960
|
-
let handle: Handle<'TextureAsset', 'shared'>;
|
|
4961
|
-
const textureRef = materialTextureRef(raw);
|
|
4962
|
-
const textureGuid = assetReferenceText(textureRef);
|
|
4963
|
-
if (textureGuid !== undefined) {
|
|
4964
|
-
if (assets === null || assets === undefined) return undefined;
|
|
4965
|
-
// M4: intern so the GUID mints one stable handle per World
|
|
4966
|
-
// instead of a fresh slot every frame (GPU residency relies on
|
|
4967
|
-
// a stable handleSlot). onLastRelease -> gpuStore.evictTexture.
|
|
4968
|
-
const interned = internSharedRefFromGuid(
|
|
4969
|
-
world,
|
|
4970
|
-
assets,
|
|
4971
|
-
textureGuid,
|
|
4972
|
-
'TextureAsset',
|
|
4973
|
-
);
|
|
4974
|
-
if (interned === undefined) return undefined;
|
|
4975
|
-
handle = interned;
|
|
4976
|
-
} else if (typeof textureRef === 'number') {
|
|
4977
|
-
handle = toShared<'TextureAsset'>(textureRef);
|
|
4978
|
-
} else {
|
|
4979
|
-
return undefined;
|
|
4980
|
-
}
|
|
4981
|
-
if (assets === null || assets === undefined) return handle;
|
|
4982
|
-
const declaredFields = materialTextureFields(
|
|
4983
|
-
firstPassShader,
|
|
4984
|
-
materialParamSchema.length > 0
|
|
4985
|
-
? derive(materialParamSchema).textureFieldNames
|
|
4986
|
-
: firstPassShader !== undefined
|
|
4987
|
-
? assets.materialShaderTextureFieldNames(firstPassShader)
|
|
4988
|
-
: undefined,
|
|
4989
|
-
);
|
|
4990
|
-
// Shader not registered (R-4 cross-worktree path) -> trust the
|
|
4991
|
-
// raw handle and let the record stage / GPU layer surface each
|
|
4992
|
-
// mismatch via MISSING_TEXTURE_HANDLE.
|
|
4993
|
-
if (declaredFields === undefined) return handle;
|
|
4994
|
-
// Field is not declared as a texture by the shader -> the
|
|
4995
|
-
// loader's "try every int" fallback misclassified a scalar;
|
|
4996
|
-
// drop the slot so the record stage uses the default white.
|
|
4997
|
-
if (
|
|
4998
|
-
!declaredFields.has(fieldName) &&
|
|
4999
|
-
!isEngineInjectedTextureField(firstPassShader, fieldName)
|
|
5000
|
-
) {
|
|
5001
|
-
return undefined;
|
|
5002
|
-
}
|
|
5003
|
-
// Field declared as texture: verify the handle's asset kind.
|
|
5004
|
-
const assetRes = resolveAssetHandle(world, handle);
|
|
5005
|
-
if (!assetRes.ok) return undefined;
|
|
5006
|
-
const kind = (assetRes.value as { kind?: string }).kind;
|
|
5007
|
-
if (kind !== 'texture') return undefined;
|
|
5008
|
-
return handle;
|
|
5009
|
-
};
|
|
5010
|
-
// feat-20260614 M8 (D-19): resolve a sampler / texture paramValue
|
|
5011
|
-
// that may be an embedded GUID string (catalog + allocSharedRef) or
|
|
5012
|
-
// an already-minted column handle (number passthrough).
|
|
5013
|
-
const resolveParamHandle = <B extends string>(
|
|
5014
|
-
raw: unknown,
|
|
5015
|
-
brand: B,
|
|
5016
|
-
): Handle<B, 'shared'> | undefined => {
|
|
5017
|
-
const value = materialTextureRef(raw);
|
|
5018
|
-
if (typeof value === 'number') return toShared<B>(value);
|
|
5019
|
-
const guid = assetReferenceText(value);
|
|
5020
|
-
if (guid !== undefined) {
|
|
5021
|
-
if (assets === null || assets === undefined) return undefined;
|
|
5022
|
-
// M4: intern the GUID -> column-handle resolution (one stable
|
|
5023
|
-
// handle per (world, guid, brand), reused across frames).
|
|
5024
|
-
return internSharedRefFromGuid(world, assets, guid, brand);
|
|
5025
|
-
}
|
|
5026
|
-
return undefined;
|
|
5027
|
-
};
|
|
5028
|
-
// feat-20260621-learn-render-5-5-parallax M2 / w7 (D-3): iterate the
|
|
5029
|
-
// shader's derive(paramSchema).textureFieldNames SSOT so the Nth
|
|
5030
|
-
// user-region texture (e.g. parallax heightTexture) is validated +
|
|
5031
|
-
// carried, replacing the hardcoded 3-field list. validateTextureHandle
|
|
5032
|
-
// already drops fields a shader doesn't declare as a texture.
|
|
5033
|
-
const userRegionFields =
|
|
5034
|
-
materialTextureFields(
|
|
5035
|
-
firstPassShader,
|
|
5036
|
-
materialParamSchema.length > 0
|
|
5037
|
-
? derive(materialParamSchema).textureFieldNames
|
|
5038
|
-
: firstPassShader !== undefined && assets !== null && assets !== undefined
|
|
5039
|
-
? assets.materialShaderTextureFieldNames(firstPassShader)
|
|
5040
|
-
: undefined,
|
|
5041
|
-
) ?? BUILTIN_USER_REGION_TEXTURE_FIELDS;
|
|
5042
|
-
const textureHandles = new Map<string, Handle<'TextureAsset', 'shared'>>();
|
|
5043
|
-
const videoTextureFields = new Map<string, Handle<'VideoAsset', 'shared'>>();
|
|
5044
|
-
for (const field of userRegionFields) {
|
|
5045
|
-
// D-5: a video-kind paramValue routes to the transient path
|
|
5046
|
-
// (videoTextureFields), NOT validateTextureHandle (which drops
|
|
5047
|
-
// kind!=='texture', the R-7 silent-fail path). Static fields fall
|
|
5048
|
-
// through to validateTextureHandle unchanged.
|
|
5049
|
-
const videoHandle =
|
|
5050
|
-
assets !== null && assets !== undefined
|
|
5051
|
-
? resolveVideoFieldHandle(pv[field], world, assets)
|
|
5052
|
-
: undefined;
|
|
5053
|
-
if (videoHandle !== undefined) {
|
|
5054
|
-
videoTextureFields.set(field, videoHandle);
|
|
5055
|
-
continue;
|
|
5056
|
-
}
|
|
5057
|
-
const handle = validateTextureHandle(field, pv[field]);
|
|
5058
|
-
if (handle !== undefined) textureHandles.set(field, handle);
|
|
5059
|
-
}
|
|
5060
|
-
const baseColorTextureHandle = textureHandles.get('baseColorTexture');
|
|
5061
|
-
const metallicRoughnessTextureHandle = textureHandles.get('metallicRoughnessTexture');
|
|
5062
|
-
const normalTextureHandle = textureHandles.get('normalTexture');
|
|
5063
|
-
const samplerHandles = collectMaterialTextureSamplers(pv, (value) =>
|
|
5064
|
-
resolveParamHandle(materialTextureRef(value), 'SamplerAsset'),
|
|
5065
|
-
);
|
|
5066
|
-
const emissiveTextureHandle = validateTextureHandle(
|
|
5067
|
-
'emissiveTexture',
|
|
5068
|
-
pv.emissiveTexture,
|
|
5069
|
-
);
|
|
5070
|
-
const occlusionTextureHandle = validateTextureHandle(
|
|
5071
|
-
'occlusionTexture',
|
|
5072
|
-
pv.occlusionTexture,
|
|
5073
|
-
);
|
|
5074
|
-
const textureCoordinates = collectMaterialTextureCoordinates(pv);
|
|
5075
|
-
const textureSources = collectMaterialTextureSources(pv, world);
|
|
5076
|
-
const emissivePv = pv.emissive as readonly number[] | undefined;
|
|
5077
|
-
// feat-20260625 M2 / w6: first-pass transparency flag folds into
|
|
5078
|
-
// MaterialSnapshot.transparent so the record stage can drive the
|
|
5079
|
-
// LDR split + premultiplied-alpha blend decision without
|
|
5080
|
-
// re-reading passes[]. feat-20260626-collapse M2: derive from
|
|
5081
|
-
// `passes[0].renderState.blend !== undefined` (blend presence is
|
|
5082
|
-
// the SSOT after MaterialPass.transparent was dropped).
|
|
5083
|
-
// Result is plain boolean (always defined here) — written as-is
|
|
5084
|
-
// into the snapshot (`boolean | undefined` field, see L759).
|
|
5085
|
-
const firstPassTransparent: boolean = allPasses[0]?.renderState?.blend !== undefined;
|
|
5086
|
-
|
|
5087
|
-
// feat-20260625-refactor-sprite-as-transparent-mesh M3 / w12 (D-8):
|
|
5088
|
-
// narrow `forgeax::sprite` extract block --- folds the legacy user
|
|
5089
|
-
// values format (flipX / flipY / slices / sliceMode + free
|
|
5090
|
-
// region / pivot) into the UBO-aligned paramSnapshot vec4 fields
|
|
5091
|
-
// (region / pivotAndSize / slicesAndMode + colorTint). Also folds
|
|
5092
|
-
// per-entity SpriteRegionOverride (Q4=a). After this block the
|
|
5093
|
-
// generic else branch picks up the snapshot via the same writer
|
|
5094
|
-
// path PBR / unlit use; no more shadingModel='sprite' arm, no
|
|
5095
|
-
// spriteFields POD (AC-02 / AC-07: extract has exactly 2 hard
|
|
5096
|
-
// `forgeax::sprite` checks --- this fold + the slices mesh swap on
|
|
5097
|
-
// the record side).
|
|
5098
|
-
if (isSprite) {
|
|
5099
|
-
// SpriteRegionOverride: per-entity per-frame region displacement.
|
|
5100
|
-
let overrideRegion: readonly [number, number, number, number] | undefined;
|
|
5101
|
-
if (hasSpriteRegionOverride) {
|
|
5102
|
-
const overrideView = worldInternal._getArrayView(
|
|
5103
|
-
entity,
|
|
5104
|
-
SpriteRegionOverride,
|
|
5105
|
-
'region',
|
|
5106
|
-
) as Float32Array | undefined;
|
|
5107
|
-
if (overrideView !== undefined && overrideView.length >= 4) {
|
|
5108
|
-
overrideRegion = [
|
|
5109
|
-
overrideView[0] ?? 0,
|
|
5110
|
-
overrideView[1] ?? 0,
|
|
5111
|
-
overrideView[2] ?? 1,
|
|
5112
|
-
overrideView[3] ?? 1,
|
|
5113
|
-
];
|
|
5114
|
-
}
|
|
5115
|
-
}
|
|
5116
|
-
// Region resolution priority: SpriteRegionOverride > paramSnapshot.
|
|
5117
|
-
// region (UBO-aligned user input) > [0,0,1,1] identity.
|
|
5118
|
-
const regionPv = paramSnap.region as readonly number[] | undefined;
|
|
5119
|
-
let regionX = overrideRegion?.[0] ?? regionPv?.[0] ?? 0;
|
|
5120
|
-
let regionY = overrideRegion?.[1] ?? regionPv?.[1] ?? 0;
|
|
5121
|
-
let regionZ = overrideRegion?.[2] ?? regionPv?.[2] ?? 1;
|
|
5122
|
-
let regionW = overrideRegion?.[3] ?? regionPv?.[3] ?? 1;
|
|
5123
|
-
// flipX / flipY fold into region (D-8): the shader does
|
|
5124
|
-
// `uv * region.zw + region.xy`, so flipping along U is a sign
|
|
5125
|
-
// negation of region.z plus an origin offset.
|
|
5126
|
-
const flipXPv = typeof pv.flipX === 'number' ? pv.flipX : 0;
|
|
5127
|
-
const flipYPv = typeof pv.flipY === 'number' ? pv.flipY : 0;
|
|
5128
|
-
if (flipXPv !== 0) {
|
|
5129
|
-
regionX += regionZ;
|
|
5130
|
-
regionZ = -regionZ;
|
|
5131
|
-
}
|
|
5132
|
-
if (flipYPv !== 0) {
|
|
5133
|
-
regionY += regionW;
|
|
5134
|
-
regionW = -regionW;
|
|
5135
|
-
}
|
|
5136
|
-
paramSnap.region = [regionX, regionY, regionZ, regionW];
|
|
5137
|
-
// Guard: slicesAndMode must be present and zero for non-9-slice
|
|
5138
|
-
// sprites so the record-stage UBO writer (applyParamSnapshotToUbo)
|
|
5139
|
-
// writes [0,0,0,0] at offset 48 instead of leaving the
|
|
5140
|
-
// buildPbrMaterialUboPayload PBR baseline (e.g. occlusionStrength=1
|
|
5141
|
-
// at that slot). A non-zero slicesAndMode trips `useSlices=true`
|
|
5142
|
-
// in sprite.wgsl, which degenerates HANDLE_QUAD geometry → invisible.
|
|
5143
|
-
if (!('slicesAndMode' in paramSnap)) {
|
|
5144
|
-
(paramSnap as Record<string, unknown>).slicesAndMode = [0, 0, 0, 0];
|
|
5145
|
-
}
|
|
5146
|
-
}
|
|
5147
|
-
|
|
5148
|
-
// Generic materialShaderId snapshot --- sprite included now flows
|
|
5149
|
-
// through this single branch (plan-strategy D-3 / AC-01 / AC-02 /
|
|
5150
|
-
// AC-07). The sprite block above only writes paramSnap.region (D-8
|
|
5151
|
-
// SpriteRegionOverride + flip fold); the rest of the UBO is filled
|
|
5152
|
-
// by the same paramSchema-driven path PBR / unlit use.
|
|
5153
|
-
materialSnap = {
|
|
5154
|
-
baseColor,
|
|
5155
|
-
metallic: metallicPv,
|
|
5156
|
-
roughness: roughnessPv,
|
|
5157
|
-
clearcoat: clearcoatPv,
|
|
5158
|
-
clearcoatRoughness: clearcoatRoughnessPv,
|
|
5159
|
-
...(specularTintPv !== undefined && {
|
|
5160
|
-
specularTint: [
|
|
5161
|
-
specularTintPv[0] ?? 1,
|
|
5162
|
-
specularTintPv[1] ?? 1,
|
|
5163
|
-
specularTintPv[2] ?? 1,
|
|
5164
|
-
] as readonly [number, number, number],
|
|
5165
|
-
}),
|
|
5166
|
-
normalScale: normalScalePv,
|
|
5167
|
-
materialShaderId: firstPassShader,
|
|
5168
|
-
materialHandle: handleRaw,
|
|
5169
|
-
renderState: pipelineRenderState(allPasses[0]?.renderState),
|
|
5170
|
-
paramSnapshot: paramSnap,
|
|
5171
|
-
...(materialParamSchema.length > 0 && { materialParamSchema }),
|
|
5172
|
-
...(textureCoordinates.size > 0 && { textureCoordinates }),
|
|
5173
|
-
...(samplerHandles.size > 0 && { samplerHandles }),
|
|
5174
|
-
...(textureHandles.size > 0 && { textureHandles }),
|
|
5175
|
-
...(textureSources.size > 0 && { textureSources }),
|
|
5176
|
-
...(videoTextureFields.size > 0 && { videoTextureFields }),
|
|
5177
|
-
...(baseColorTextureHandle !== undefined && {
|
|
5178
|
-
baseColorTexture: baseColorTextureHandle,
|
|
5179
|
-
}),
|
|
5180
|
-
...(metallicRoughnessTextureHandle !== undefined && {
|
|
5181
|
-
metallicRoughnessTexture: metallicRoughnessTextureHandle,
|
|
5182
|
-
}),
|
|
5183
|
-
...(normalTextureHandle !== undefined && { normalTexture: normalTextureHandle }),
|
|
5184
|
-
...(emissivePv !== undefined && {
|
|
5185
|
-
emissive: [emissivePv[0] ?? 0, emissivePv[1] ?? 0, emissivePv[2] ?? 0] as readonly [
|
|
5186
|
-
number,
|
|
5187
|
-
number,
|
|
5188
|
-
number,
|
|
5189
|
-
],
|
|
5190
|
-
}),
|
|
5191
|
-
...(typeof pv.emissiveIntensity === 'number' && {
|
|
5192
|
-
emissiveIntensity: pv.emissiveIntensity,
|
|
5193
|
-
}),
|
|
5194
|
-
...(emissiveTextureHandle !== undefined && {
|
|
5195
|
-
emissiveTexture: emissiveTextureHandle,
|
|
5196
|
-
}),
|
|
5197
|
-
...(occlusionTextureHandle !== undefined && {
|
|
5198
|
-
occlusionTexture: occlusionTextureHandle,
|
|
5199
|
-
}),
|
|
5200
|
-
...(typeof pv.occlusionStrength === 'number' && {
|
|
5201
|
-
occlusionStrength: pv.occlusionStrength,
|
|
5202
|
-
}),
|
|
5203
|
-
transparent: firstPassTransparent,
|
|
5204
|
-
};
|
|
5205
|
-
|
|
5206
|
-
if (!hasSpriteRegionOverride && !hasSkin) {
|
|
5207
|
-
const stored = storeMaterialSnapshot(
|
|
5208
|
-
materialSnapshotCache,
|
|
5209
|
-
handleRaw,
|
|
5210
|
-
materialSnap,
|
|
5211
|
-
allPasses,
|
|
5212
|
-
asset,
|
|
5213
|
-
assets,
|
|
5214
|
-
);
|
|
5215
|
-
if (stored.crossFrameSafe) persistentMaterialSnapshotCache?.set(handleRaw, stored);
|
|
5216
|
-
}
|
|
5217
|
-
|
|
5218
|
-
// Build dispatch entries from resolved passes.
|
|
5219
|
-
if (isRenderable) {
|
|
5220
|
-
appendMaterialDispatchEntries(
|
|
5221
|
-
pendingDispatch,
|
|
5222
|
-
allPasses,
|
|
5223
|
-
entity,
|
|
5224
|
-
handleRaw,
|
|
5225
|
-
renderables.length,
|
|
5226
|
-
layerVal,
|
|
5227
|
-
paramSnap,
|
|
5228
|
-
);
|
|
5229
|
-
}
|
|
5230
|
-
}
|
|
5231
|
-
}
|
|
5232
|
-
}
|
|
5233
|
-
|
|
5234
|
-
// feat-20260609 M2/M5 corrective fixup: default-material entities
|
|
5235
|
-
// (handleRaw===0 / case-B MeshRenderer{data:{}}) must produce
|
|
5236
|
-
// ShadowCaster dispatch entries so the shadow pass includes them.
|
|
5237
|
-
// The pre-existing logic only builds dispatch entries from
|
|
5238
|
-
// resolved material assets; defaultMaterialSnapshot() (mid-grey unlit)
|
|
5239
|
-
// left dispatch empty, causing shadow-m2/m3 test failures.
|
|
5240
|
-
// Requirements §10.5: shadow-casting is default behaviour, opt-out
|
|
5241
|
-
// via castShadow:false. The default material has no opt-out, so
|
|
5242
|
-
// it casts shadows.
|
|
5243
|
-
//
|
|
5244
|
-
// CHARTER NOTE (feat-20260609 T-005-a): the URP literals
|
|
5245
|
-
// `LightMode: 'ShadowCaster'` / `LightMode: 'Forward'` below are a
|
|
5246
|
-
// local URP-bridge — they mirror what `Materials.unlit({ castShadow:
|
|
5247
|
-
// true })` produces at the asset layer. The default-material
|
|
5248
|
-
// handle=0 path bypasses asset registration, so we synthesize the
|
|
5249
|
-
// same dispatch shape inline. Follow-up cleanup (F-1 from
|
|
5250
|
-
// implement-review R1): thread default materials through the
|
|
5251
|
-
// Materials factory so this block can call into the shared
|
|
5252
|
-
// passes[] producer.
|
|
5253
|
-
// tweak-20260701 M1: `materialSnap.shadingModel === 'unlit'` removed —
|
|
5254
|
-
// for handleRaw===0, defaultMaterialSnapshot() was always unlit
|
|
5255
|
-
// (the shadingModel check was a tautology); the isRenderable &&
|
|
5256
|
-
// handleRaw===0 guard alone preserves the exact same dispatch shape.
|
|
5257
|
-
if (isRenderable && handleRaw === 0) {
|
|
5258
|
-
const shadowCasterTags: Record<string, string> = { LightMode: 'ShadowCaster' };
|
|
5259
|
-
const nextRenderableIndex = renderables.length;
|
|
5260
|
-
// M2.5: stage into pendingDispatch; flushed at the renderable push
|
|
5261
|
-
// site below only when the entity survives cull.
|
|
5262
|
-
pendingDispatch.push({
|
|
5263
|
-
entityIndex: entity,
|
|
5264
|
-
materialHandle: 0,
|
|
5265
|
-
renderableIndex: nextRenderableIndex,
|
|
5266
|
-
passIndex: 0,
|
|
5267
|
-
queue: 2000,
|
|
5268
|
-
layer: layerVal,
|
|
5269
|
-
tags: shadowCasterTags,
|
|
5270
|
-
renderState: undefined,
|
|
5271
|
-
defines: undefined,
|
|
5272
|
-
vertexEntry: 'vs_main',
|
|
5273
|
-
fragmentEntry: undefined,
|
|
5274
|
-
materialShaderId: 'forgeax::default-shadow-caster',
|
|
5275
|
-
paramSnapshot: {},
|
|
5276
|
-
});
|
|
5277
|
-
// Also add a Forward pass entry so the entity renders in the
|
|
5278
|
-
// main scene pass (mirrors Materials.unlit default).
|
|
5279
|
-
appendMaterialDispatchEntries(
|
|
5280
|
-
pendingDispatch,
|
|
5281
|
-
[DEFAULT_FORWARD_PASS],
|
|
5282
|
-
entity,
|
|
5283
|
-
0,
|
|
5284
|
-
nextRenderableIndex,
|
|
5285
|
-
layerVal,
|
|
5286
|
-
{},
|
|
5287
|
-
1,
|
|
5288
|
-
);
|
|
5289
|
-
}
|
|
5290
|
-
|
|
5291
|
-
if (isRenderable) {
|
|
5292
|
-
// feat-20260612 M2 / m2-6: Skin + Instances coexistence + per-joint
|
|
5293
|
-
// dangling fail-fast + real palette slice allocation. Replaces the
|
|
5294
|
-
// T-21 placeholder ({0,0} discriminator-only sentinel) with full
|
|
5295
|
-
// resolve / validate / write chain (D-9 reset already fired at
|
|
5296
|
-
// extractFrame entry; per-entity allocate + writeJointPalette here).
|
|
5297
|
-
let skinSlice: SkinPaletteSlice | undefined;
|
|
5298
|
-
if (hasSkin) {
|
|
5299
|
-
const skeletonHandleRaw = skinSkeletonView;
|
|
5300
|
-
if (
|
|
5301
|
-
skeletonHandleRaw !== undefined &&
|
|
5302
|
-
skeletonHandleRaw !== 0 &&
|
|
5303
|
-
assets !== undefined &&
|
|
5304
|
-
assets !== null
|
|
5305
|
-
) {
|
|
5306
|
-
// Skin + Instances coexistence is forbidden (D-10).
|
|
5307
|
-
if (hasInstances) {
|
|
5308
|
-
worldInternal._routeError(new SkinInstancesCoexistForbiddenError(entity), {
|
|
5309
|
-
severity: Severity.Error,
|
|
5310
|
-
systemName: 'RenderSystem.extract (skin-instances-coexist)',
|
|
5311
|
-
});
|
|
5312
|
-
|
|
5313
|
-
continue;
|
|
5314
|
-
}
|
|
5315
|
-
// (a) Resolve skeleton asset; on failure -> skeleton-resolve-failed.
|
|
5316
|
-
const skeletonHandle = toShared<'SkeletonAsset'>(skeletonHandleRaw);
|
|
5317
|
-
const skeletonRes = resolveAssetHandle<SkeletonAsset>(world, skeletonHandle);
|
|
5318
|
-
if (!skeletonRes.ok || skeletonRes.value.kind !== 'skeleton') {
|
|
5319
|
-
worldInternal._routeError(new SkeletonResolveFailedError(entity, skeletonHandleRaw), {
|
|
5320
|
-
severity: Severity.Error,
|
|
5321
|
-
systemName: 'RenderSystem.extract (skeleton-resolve-failed)',
|
|
5322
|
-
});
|
|
5323
|
-
continue;
|
|
5324
|
-
}
|
|
5325
|
-
const skeleton = skeletonRes.value;
|
|
5326
|
-
// (b) Reuse the Skin row already read for its skeleton handle;
|
|
5327
|
-
// the old path repeated a second whole-row world.get here.
|
|
5328
|
-
const skinJoints = skin?.joints;
|
|
5329
|
-
if (skinJoints === undefined) continue;
|
|
5330
|
-
const jointsLength = skinJoints.length;
|
|
5331
|
-
if (jointsLength !== skeleton.jointCount) {
|
|
5332
|
-
worldInternal._routeError(
|
|
5333
|
-
new JointCountMismatchError(entity, skeleton.jointCount, jointsLength),
|
|
5334
|
-
{
|
|
5335
|
-
severity: Severity.Error,
|
|
5336
|
-
systemName: 'RenderSystem.extract (joint-count-mismatch)',
|
|
5337
|
-
},
|
|
5338
|
-
);
|
|
5339
|
-
continue;
|
|
5340
|
-
}
|
|
5341
|
-
// (c) Resolve only each joint's GlobalTransform.world column. The
|
|
5342
|
-
// transient view is consumed before a structural mutation and
|
|
5343
|
-
// therefore preserves the same dangling-joint behavior without
|
|
5344
|
-
// constructing every other Transform field.
|
|
5345
|
-
// Build mat4 list eagerly so write happens once per entity (no
|
|
5346
|
-
// half-written slice on dangling).
|
|
5347
|
-
const jointWorlds = new Array<Mat4>(skeleton.jointCount);
|
|
5348
|
-
let jointDangling = -1;
|
|
5349
|
-
for (let jIdx = 0; jIdx < skeleton.jointCount; jIdx++) {
|
|
5350
|
-
const jointEntityRaw = skinJoints[jIdx] ?? 0;
|
|
5351
|
-
const jointEntity = jointEntityRaw as EntityHandle;
|
|
5352
|
-
const jointWorld = worldInternal._getArrayView(jointEntity, GlobalTransform, 'world');
|
|
5353
|
-
if (jointWorld === undefined) {
|
|
5354
|
-
jointDangling = jIdx;
|
|
5355
|
-
break;
|
|
5356
|
-
}
|
|
5357
|
-
// The allocator consumes the canonical Mat4 brand. Copy the
|
|
5358
|
-
// column view into that owner-created value so the ECS storage
|
|
5359
|
-
// view never crosses the math brand boundary by assertion.
|
|
5360
|
-
const jointWorldMat = mat4.create();
|
|
5361
|
-
jointWorldMat.set(jointWorld);
|
|
5362
|
-
jointWorlds[jIdx] = jointWorldMat;
|
|
5363
|
-
}
|
|
5364
|
-
if (jointDangling >= 0) {
|
|
5365
|
-
worldInternal._routeError(new JointEntityDanglingError(entity, jointDangling), {
|
|
5366
|
-
severity: Severity.Error,
|
|
5367
|
-
systemName: 'RenderSystem.extract (joint-entity-dangling)',
|
|
5368
|
-
});
|
|
5369
|
-
continue;
|
|
5370
|
-
}
|
|
5371
|
-
// (d) Slice the IBM flat Float32Array into per-joint Float32Arrays.
|
|
5372
|
-
// skeleton.inverseBindMatrices length === jointCount * 16.
|
|
5373
|
-
const ibmFlat = skeleton.inverseBindMatrices;
|
|
5374
|
-
const ibms: Float32Array[] = new Array<Float32Array>(skeleton.jointCount);
|
|
5375
|
-
for (let jIdx = 0; jIdx < skeleton.jointCount; jIdx++) {
|
|
5376
|
-
ibms[jIdx] = ibmFlat.subarray(jIdx * 16, jIdx * 16 + 16);
|
|
5377
|
-
}
|
|
5378
|
-
// (e) Allocate slice + write palette via the allocator (D-9 reset
|
|
5379
|
-
// already fired at extractFrame entry). When the pipelineState
|
|
5380
|
-
// surface is absent (test fixtures that pass undefined) the
|
|
5381
|
-
// hasSkin segment is skipped silently — bind-pose equivalent.
|
|
5382
|
-
if (skinPaletteAllocator !== null) {
|
|
5383
|
-
const slice = skinPaletteAllocator.allocateSlice(skeleton.jointCount);
|
|
5384
|
-
skinPaletteAllocator.writeJointPalette(slice, ibms, jointWorlds);
|
|
5385
|
-
skinSlice = {
|
|
5386
|
-
jointCount: slice.jointCount,
|
|
5387
|
-
byteOffset: slice.byteOffset,
|
|
5388
|
-
buffer: slice.buffer,
|
|
5389
|
-
};
|
|
5390
|
-
}
|
|
5391
|
-
}
|
|
5392
|
-
}
|
|
5393
|
-
|
|
5394
|
-
// A pbr-skin PSO requires the complete three-binding group(2) skin
|
|
5395
|
-
// projection. Do not publish a renderable or its staged dispatch when
|
|
5396
|
-
// palette extraction was unavailable; the record stage must never
|
|
5397
|
-
// reinterpret a skinned vertex stream through an ordinary mesh group.
|
|
5398
|
-
if (materialSnap.materialShaderId === 'forgeax::pbr-skin' && skinSlice === undefined) {
|
|
5399
|
-
pendingDispatch.length = 0;
|
|
5400
|
-
continue;
|
|
5401
|
-
}
|
|
5402
|
-
|
|
5403
|
-
// feat-20260601 D-3: read the resolved world mat4 (propagateTransforms
|
|
5404
|
-
// output) straight from the GlobalTransform.world column array view. The
|
|
5405
|
-
// record stage copies these 16 floats into the mesh SSBO with zero
|
|
5406
|
-
// per-snapshot `mat4.compose` (AC-07). A stale slot (generation gone)
|
|
5407
|
-
// skips the renderable, mirroring the Instances dangling-row sweep.
|
|
5408
|
-
// tweak-20260611 M1 / K-3: `_getArrayView` call survives untouched;
|
|
5409
|
-
// only the `entity` source switched to `bundle.Entity.self[i]`.
|
|
5410
|
-
const worldView = worldInternal._getArrayView(entity, GlobalTransform, 'world');
|
|
5411
|
-
if (worldView === undefined) continue;
|
|
5412
|
-
const worldMat = new Float32Array(worldView);
|
|
5413
|
-
const transformSnap: TransformSnapshot = { world: worldMat };
|
|
5414
|
-
// feat-20260608 M5 amend / w11-a: per-submesh `materials[]` array
|
|
5415
|
-
// aligned 1-1 with `MeshAsset.submeshes[]`. materials[0] === the
|
|
5416
|
-
// representative entity-level snapshot already built; materials[i>=1]
|
|
5417
|
-
// are resolved via `resolveMaterialSnapshot` (a non-sprite, single-
|
|
5418
|
-
// pass-equivalent resolver — sprite per-submesh is OOS-1). When the
|
|
5419
|
-
// entity has no materialsView (case-B sentinel) the array is a single
|
|
5420
|
-
// mid-grey default mirroring the legacy single-material path so the
|
|
5421
|
-
// record stage's per-submesh UBO upload loop trivially writes one
|
|
5422
|
-
// slot, no special branch.
|
|
5423
|
-
const materialsArr: MaterialSnapshot[] = [materialSnap];
|
|
5424
|
-
if (assets !== undefined && assets !== null) {
|
|
5425
|
-
for (let mi = 1; mi < materialHandles.length; mi++) {
|
|
5426
|
-
const subHandle = materialHandles[mi] ?? 0;
|
|
5427
|
-
const cachedSubmaterial = materialSnapshotCache.get(subHandle);
|
|
5428
|
-
materialsArr.push(
|
|
5429
|
-
cachedSubmaterial?.snapshot ??
|
|
5430
|
-
resolveMaterialSnapshot(
|
|
5431
|
-
subHandle,
|
|
5432
|
-
world,
|
|
5433
|
-
assets,
|
|
5434
|
-
materialSnapshotCache,
|
|
5435
|
-
persistentMaterialSnapshotCache,
|
|
5436
|
-
),
|
|
5437
|
-
);
|
|
5438
|
-
}
|
|
5439
|
-
}
|
|
5440
|
-
if (isRenderable && assets !== undefined && assets !== null) {
|
|
5441
|
-
for (let mi = 1; mi < materialHandles.length; mi++) {
|
|
5442
|
-
const subHandle = materialHandles[mi] ?? 0;
|
|
5443
|
-
if (subHandle === handleRaw) continue;
|
|
5444
|
-
const subEntry =
|
|
5445
|
-
materialSnapshotCache.get(subHandle) ??
|
|
5446
|
-
readPersistentMaterialSnapshot(persistentMaterialSnapshotCache, subHandle, assets);
|
|
5447
|
-
appendMaterialDispatchEntries(
|
|
5448
|
-
pendingDispatch,
|
|
5449
|
-
subEntry?.passes ?? (subHandle === 0 ? [DEFAULT_FORWARD_PASS] : []),
|
|
5450
|
-
entity,
|
|
5451
|
-
subHandle,
|
|
5452
|
-
renderables.length,
|
|
5453
|
-
layerVal,
|
|
5454
|
-
materialsArr[mi]?.paramSnapshot,
|
|
5455
|
-
);
|
|
5456
|
-
}
|
|
5457
|
-
}
|
|
5458
|
-
|
|
5459
|
-
// feat-20260625-sprite-instances-and-tilemap-terrain-static-batch M3 /
|
|
5460
|
-
// w10: SpriteInstances validation + snapshot materialisation.
|
|
5461
|
-
// Three structured EcsError fires at this single point (plan-strategy
|
|
5462
|
-
// D-6 "fail-fast at the render domain entry, not at ECS spawn-time"):
|
|
5463
|
-
let spriteInstancesSnap: SpriteInstancesSnapshot | undefined;
|
|
5464
|
-
if (hasSpriteInstances) {
|
|
5465
|
-
// (1) mutually exclusive with Instances (peers — pick one).
|
|
5466
|
-
if (hasInstances) {
|
|
5467
|
-
worldInternal._routeError(
|
|
5468
|
-
new SpriteInstancesMutuallyExclusiveWithInstancesError(entity),
|
|
5469
|
-
{
|
|
5470
|
-
severity: Severity.Error,
|
|
5471
|
-
systemName: 'RenderSystem.extract (sprite-instances-mutually-exclusive)',
|
|
5472
|
-
},
|
|
5473
|
-
);
|
|
5474
|
-
continue;
|
|
5475
|
-
}
|
|
5476
|
-
// (2) requires sprite shader — the per-instance UV region is
|
|
5477
|
-
// consumed by the sprite vertex shader path only (plan-strategy D-4
|
|
5478
|
-
// axis on sprite.wgsl). Post-collapse (PR #520): sprite is no longer
|
|
5479
|
-
// a `shadingModel` enum member; identification is via the first-pass
|
|
5480
|
-
// `materialShaderId === 'forgeax::sprite'` (OOS-1 path retained).
|
|
5481
|
-
//
|
|
5482
|
-
// feat-20260624 M1' / t6: `'forgeax::sprite-lit'` also walks the same
|
|
5483
|
-
// per-instance UV region vertex path (VsOut byte-identical, paramSchema
|
|
5484
|
-
// mirror); accept either shader id.
|
|
5485
|
-
if (
|
|
5486
|
-
materialSnap.materialShaderId !== 'forgeax::sprite' &&
|
|
5487
|
-
materialSnap.materialShaderId !== 'forgeax::sprite-lit'
|
|
5488
|
-
) {
|
|
5489
|
-
worldInternal._routeError(
|
|
5490
|
-
new SpriteInstancesRequiresSpriteShaderError(
|
|
5491
|
-
entity,
|
|
5492
|
-
materialSnap.materialShaderId ?? 'undefined',
|
|
5493
|
-
),
|
|
5494
|
-
{
|
|
5495
|
-
severity: Severity.Error,
|
|
5496
|
-
systemName: 'RenderSystem.extract (sprite-instances-requires-sprite-shader)',
|
|
5497
|
-
},
|
|
5498
|
-
);
|
|
5499
|
-
continue;
|
|
5500
|
-
}
|
|
5501
|
-
// (3) count mismatch — transforms.length / 16 === regions.length / 4
|
|
5502
|
-
// (transforms.length=0 + regions.length=0 is the zero-instance lawful
|
|
5503
|
-
// boundary; both derivations are 0 and equality holds, so no fire).
|
|
5504
|
-
const transforms = worldInternal._getArrayView(entity, SpriteInstances, 'transforms');
|
|
5505
|
-
const regions = worldInternal._getArrayView(entity, SpriteInstances, 'regions');
|
|
5506
|
-
if (transforms !== undefined && regions !== undefined) {
|
|
5507
|
-
const transformsLength = transforms.length;
|
|
5508
|
-
const regionsLength = regions.length;
|
|
5509
|
-
// Stride sanity: transforms must be mod 16, regions must be mod 4.
|
|
5510
|
-
// A stride violation expresses as a count mismatch under the
|
|
5511
|
-
// canonical derivation transforms/16 vs regions/4 — fire the
|
|
5512
|
-
// count-mismatch code (the same code carries detail.expectedStride).
|
|
5513
|
-
const tCount = transformsLength / 16;
|
|
5514
|
-
const rCount = regionsLength / 4;
|
|
5515
|
-
if (transformsLength % 16 !== 0 || regionsLength % 4 !== 0 || tCount !== rCount) {
|
|
5516
|
-
worldInternal._routeError(
|
|
5517
|
-
new SpriteInstancesCountMismatchError(transformsLength, regionsLength),
|
|
5518
|
-
{
|
|
5519
|
-
severity: Severity.Error,
|
|
5520
|
-
systemName: 'RenderSystem.extract (sprite-instances-count-mismatch)',
|
|
5521
|
-
},
|
|
5522
|
-
);
|
|
5523
|
-
continue;
|
|
5524
|
-
}
|
|
5525
|
-
// Validation passes — build the snapshot. transforms.length === 0
|
|
5526
|
-
// is lawful (zero-instance) and produces instanceCount=0; the
|
|
5527
|
-
// record stage skips drawIndexed when instanceCount===0.
|
|
5528
|
-
const transformsCopy = new Float32Array(transforms);
|
|
5529
|
-
const regionsCopy = new Float32Array(regions);
|
|
5530
|
-
spriteInstancesSnap = {
|
|
5531
|
-
transforms: transformsCopy,
|
|
5532
|
-
regions: regionsCopy,
|
|
5533
|
-
instanceCount: tCount,
|
|
5534
|
-
cacheKey: entity,
|
|
5535
|
-
archVersion,
|
|
5536
|
-
};
|
|
5537
|
-
}
|
|
5538
|
-
}
|
|
5539
|
-
|
|
5540
|
-
let localAabb: Float32Array | undefined;
|
|
5541
|
-
let lods: readonly MeshLodLevel[] | undefined;
|
|
5542
|
-
let lodHysteresis: number | undefined;
|
|
5543
|
-
let morph: MorphSnapshot | undefined;
|
|
5544
|
-
const assetHandleRaw = Math.round(fAssetHandle ?? 0);
|
|
5545
|
-
if (assetHandleRaw !== 0) {
|
|
5546
|
-
const meshRes = resolveAssetHandle(world, toShared<'MeshAsset'>(assetHandleRaw));
|
|
5547
|
-
if (meshRes.ok && meshRes.value.kind === 'mesh') {
|
|
5548
|
-
const meshAsset = meshRes.value as MeshAsset;
|
|
5549
|
-
lods = meshAsset.lods;
|
|
5550
|
-
lodHysteresis = meshAsset.lodHysteresis;
|
|
5551
|
-
morph = morphSnapshotFor(meshAsset, morphWeightsView);
|
|
5552
|
-
const meshAabb = meshAsset.aabb;
|
|
5553
|
-
// Morph targets can expand the authored bounds. Keep the
|
|
5554
|
-
// specialized lane conservative until a target-aware bounds
|
|
5555
|
-
// projection is available; a false-positive draw is preferable
|
|
5556
|
-
// to culling a valid deformed vertex.
|
|
5557
|
-
if (morph === undefined && hasFiniteOrderedLocalAabb(meshAabb)) {
|
|
5558
|
-
localAabb = new Float32Array(meshAabb);
|
|
5559
|
-
}
|
|
5560
|
-
}
|
|
5561
|
-
}
|
|
5562
|
-
|
|
5563
|
-
const pointsLinesStyle: PointsLinesStyle | undefined =
|
|
5564
|
-
points !== undefined
|
|
5565
|
-
? (() => {
|
|
5566
|
-
const shape = pointShapeFromU32(points.shape);
|
|
5567
|
-
return shape === undefined
|
|
5568
|
-
? undefined
|
|
5569
|
-
: { kind: 'points' as const, sizePx: points.sizePx, shape };
|
|
5570
|
-
})()
|
|
5571
|
-
: lines === undefined
|
|
5572
|
-
? undefined
|
|
5573
|
-
: { kind: 'lines' as const, widthPx: lines.widthPx };
|
|
5574
|
-
const cullingLocalAabb = expandPointsLinesBounds(localAabb ?? [], pointsLinesStyle);
|
|
5575
|
-
const pointsLines =
|
|
5576
|
-
pointsLinesComponent === undefined
|
|
5577
|
-
? undefined
|
|
5578
|
-
: ({
|
|
5579
|
-
worldId: 0,
|
|
5580
|
-
entityKey: entity,
|
|
5581
|
-
component: pointsLinesComponent,
|
|
5582
|
-
meshHandle: assetHandleRaw,
|
|
5583
|
-
meshGeneration: assets?.catalogEpoch ?? 0,
|
|
5584
|
-
materialHandle: handleRaw,
|
|
5585
|
-
materialGeneration: assets?.catalogEpoch ?? 0,
|
|
5586
|
-
style: pointsLinesStyle,
|
|
5587
|
-
layer: layerVal,
|
|
5588
|
-
sortKey,
|
|
5589
|
-
visible: true,
|
|
5590
|
-
sourceBounds: cullingLocalAabb,
|
|
5591
|
-
viewport: { width: 0, height: 0, dpr: 1 },
|
|
5592
|
-
projection: identityProjection(),
|
|
5593
|
-
} satisfies PointsLinesRetainedSnapshot);
|
|
5594
|
-
|
|
5595
|
-
// Instances are snapshotted before the CPU visibility decision so the
|
|
5596
|
-
// culling owner can derive the union of every instance's transformed
|
|
5597
|
-
// mesh bounds. The same detached snapshot is then carried to record;
|
|
5598
|
-
// do not re-read the ECS array after culling (a structural write must
|
|
5599
|
-
// not make the cull and draw disagree within one frame).
|
|
5600
|
-
let instancesSnap: InstancesSnapshot | undefined;
|
|
5601
|
-
if (hasInstances) {
|
|
5602
|
-
const transforms = worldInternal._getArrayView(entity, Instances, 'transforms');
|
|
5603
|
-
if (transforms === undefined) {
|
|
5604
|
-
// A present component with an unavailable array is an empty
|
|
5605
|
-
// instance contribution, not an absent component. Preserve the
|
|
5606
|
-
// zero-draw contract so the record stage cannot synthesize one
|
|
5607
|
-
// identity instance from an incomplete row.
|
|
5608
|
-
instancesSnap = {
|
|
5609
|
-
transforms: new Float32Array(),
|
|
5610
|
-
instanceCount: 0,
|
|
5611
|
-
cacheKey: entity,
|
|
5612
|
-
archVersion,
|
|
5613
|
-
};
|
|
5614
|
-
} else {
|
|
5615
|
-
const actualLength = transforms.length;
|
|
5616
|
-
if (actualLength % 16 !== 0) {
|
|
5617
|
-
worldInternal._routeError(new InstanceTransformsStrideMismatchError(actualLength), {
|
|
5618
|
-
severity: Severity.Error,
|
|
5619
|
-
systemName: 'RenderSystem.extract (Instances stride)',
|
|
5620
|
-
});
|
|
5621
|
-
continue;
|
|
5622
|
-
}
|
|
5623
|
-
const snapshotCopy = new Float32Array(transforms);
|
|
5624
|
-
instancesSnap = {
|
|
5625
|
-
transforms: snapshotCopy,
|
|
5626
|
-
instanceCount: actualLength / 16,
|
|
5627
|
-
cacheKey: entity,
|
|
5628
|
-
archVersion,
|
|
5629
|
-
};
|
|
5630
|
-
}
|
|
5631
|
-
}
|
|
5632
|
-
|
|
5633
|
-
const baseRenderable: RenderableSnapshot = {
|
|
5634
|
-
assetHandle: Math.round(fAssetHandle ?? 0),
|
|
5635
|
-
transform: transformSnap,
|
|
5636
|
-
...(lods === undefined ? {} : { lods }),
|
|
5637
|
-
...(lodHysteresis === undefined ? {} : { lodHysteresis }),
|
|
5638
|
-
...(localAabb !== undefined ? { localAabb: cullingLocalAabb } : {}),
|
|
5639
|
-
material: materialSnap,
|
|
5640
|
-
materials: materialsArr,
|
|
5641
|
-
materialBindingSources,
|
|
5642
|
-
materialBindingDiagnostics,
|
|
5643
|
-
worldId: 0,
|
|
5644
|
-
entityKey: entity,
|
|
5645
|
-
...(skinSlice !== undefined ? { skin: skinSlice } : {}),
|
|
5646
|
-
...(morph !== undefined ? { morph } : {}),
|
|
5647
|
-
...(instancesSnap !== undefined ? { instances: instancesSnap } : {}),
|
|
5648
|
-
...(spriteInstancesSnap !== undefined ? { spriteInstances: spriteInstancesSnap } : {}),
|
|
5649
|
-
...(pointsLines !== undefined ? { pointsLines } : {}),
|
|
5650
|
-
};
|
|
5651
|
-
const gpuDrivenDraws = buildGpuDrivenDraws({
|
|
5652
|
-
submeshes: gpuDrivenSubmeshes,
|
|
5653
|
-
indexed: gpuDrivenIndexed,
|
|
5654
|
-
materials: materialsArr,
|
|
5655
|
-
fallbackMaterial: materialSnap,
|
|
5656
|
-
...(gpuDrivenGeometry === undefined || getMaterialShaderArtifact === undefined
|
|
5657
|
-
? {}
|
|
5658
|
-
: {
|
|
5659
|
-
prepare(draw, drawMaterial) {
|
|
5660
|
-
const materialShaderId = drawMaterial.materialShaderId;
|
|
5661
|
-
if (materialShaderId === undefined) return undefined;
|
|
5662
|
-
const artifact = getMaterialShaderArtifact(materialShaderId);
|
|
5663
|
-
if (artifact?.receipt === undefined) return undefined;
|
|
5664
|
-
const geometry: GpuDrivenGeometryReceipt = {
|
|
5665
|
-
...gpuDrivenGeometry,
|
|
5666
|
-
topology: draw.topology,
|
|
5667
|
-
indexed: draw.kind === 'indexed',
|
|
5668
|
-
};
|
|
5669
|
-
const result = resolvePreparedGpuDrivenDraw({
|
|
5670
|
-
snapshot: { ...baseRenderable, material: drawMaterial },
|
|
5671
|
-
artifact,
|
|
5672
|
-
geometry,
|
|
5673
|
-
generation: artifact.receipt.generation,
|
|
5674
|
-
draw,
|
|
5675
|
-
});
|
|
5676
|
-
return result.ok ? result.value : undefined;
|
|
5677
|
-
},
|
|
5678
|
-
}),
|
|
5679
|
-
});
|
|
5680
|
-
const renderable =
|
|
5681
|
-
morph === undefined && gpuDrivenDraws.length > 0
|
|
5682
|
-
? { ...baseRenderable, gpuDrivenDraws }
|
|
5683
|
-
: baseRenderable;
|
|
5684
|
-
|
|
5685
|
-
// feat-20260528-frustum-culling M3 / w10: frustum culling check.
|
|
5686
|
-
// Skip the entity if a valid AABB exists AND ALL cameras' frusta
|
|
5687
|
-
// reject the world-space AABB. Missing or malformed AABBs are
|
|
5688
|
-
// conservative always-visible fallbacks; valid culling bounds are
|
|
5689
|
-
// producer-owned finite local-space AABBs. Culling is unconditional
|
|
5690
|
-
// engine behavior; there is no per-entity opt-out.
|
|
5691
|
-
if (localAabb !== undefined) {
|
|
5692
|
-
// An empty or malformed Instances payload has no meaningful bounds.
|
|
5693
|
-
// Keep it conservative (the draw path later emits zero instances)
|
|
5694
|
-
// instead of manufacturing the entity's identity bounds. For a
|
|
5695
|
-
// valid payload, derive one world-space union for CPU culling while
|
|
5696
|
-
// leaving the mesh-local AABB untouched for GPU per-instance cull.
|
|
5697
|
-
const derivedInstancesBounds =
|
|
5698
|
-
instancesSnap === undefined || instancesSnap.instanceCount === 0
|
|
5699
|
-
? undefined
|
|
5700
|
-
: deriveInstancesUnionBounds({
|
|
5701
|
-
meshAabb: cullingLocalAabb,
|
|
5702
|
-
entityWorld: transformSnap.world,
|
|
5703
|
-
transforms: instancesSnap.transforms,
|
|
5704
|
-
});
|
|
5705
|
-
if (instancesSnap?.instanceCount === 0) {
|
|
5706
|
-
// No instance can contribute a visible bound. The retained
|
|
5707
|
-
// snapshot still records instanceCount=0 for the record stage.
|
|
5708
|
-
} else if (
|
|
5709
|
-
hasInstances &&
|
|
5710
|
-
(instancesSnap === undefined || derivedInstancesBounds === undefined)
|
|
5711
|
-
) {
|
|
5712
|
-
// Missing/malformed derived facts are a conservative no-cull
|
|
5713
|
-
// result. The stride validator above already rejects malformed
|
|
5714
|
-
// lengths; this branch covers non-finite producer matrices/AABB.
|
|
5715
|
-
} else {
|
|
5716
|
-
// feat-20260601 D-3: cull AABB uses the resolved world mat4
|
|
5717
|
-
// directly (no compose) -- same source the record stage feeds
|
|
5718
|
-
// the mesh SSBO, so cull stays same-source with render (AC-05).
|
|
5719
|
-
const worldAabb = box3.create();
|
|
5720
|
-
if (derivedInstancesBounds !== undefined) {
|
|
5721
|
-
worldAabb[0] = derivedInstancesBounds[0] ?? 0;
|
|
5722
|
-
worldAabb[1] = derivedInstancesBounds[1] ?? 0;
|
|
5723
|
-
worldAabb[2] = derivedInstancesBounds[2] ?? 0;
|
|
5724
|
-
worldAabb[3] = derivedInstancesBounds[3] ?? 0;
|
|
5725
|
-
worldAabb[4] = derivedInstancesBounds[4] ?? 0;
|
|
5726
|
-
worldAabb[5] = derivedInstancesBounds[5] ?? 0;
|
|
5727
|
-
} else {
|
|
5728
|
-
box3.transformBox3(worldAabb, cullingLocalAabb, transformSnap.world);
|
|
5729
|
-
}
|
|
5730
|
-
|
|
5731
|
-
// Test against all cameras. Entity is visible if one camera
|
|
5732
|
-
// frustum intersects the world-space AABB (or planes are empty
|
|
5733
|
-
// from degenerate projection).
|
|
5734
|
-
frustumTotal += 1;
|
|
5735
|
-
let visible = frustumPlanes.length === 0;
|
|
5736
|
-
for (let ci = 0; ci < frustumPlanes.length; ci++) {
|
|
5737
|
-
const planes = frustumPlanes[ci] as Float32Array;
|
|
5738
|
-
if (planes.length === 0) {
|
|
5739
|
-
visible = true;
|
|
5740
|
-
break;
|
|
5741
|
-
}
|
|
5742
|
-
if (frustum.intersectsBox(planes as frustum.Frustum, worldAabb as box3.Box3Like)) {
|
|
5743
|
-
visible = true;
|
|
5744
|
-
break;
|
|
5745
|
-
}
|
|
5746
|
-
}
|
|
5747
|
-
if (!visible) {
|
|
5748
|
-
frustumCulled += 1;
|
|
5749
|
-
continue;
|
|
5750
|
-
}
|
|
5751
|
-
}
|
|
5752
|
-
}
|
|
5753
|
-
|
|
5754
|
-
flushPendingDispatch(pendingDispatch);
|
|
5755
|
-
renderables.push(renderable);
|
|
5756
|
-
}
|
|
5757
|
-
|
|
5758
|
-
// feat-20260520-2d-sprite-layer-mvp M-3 / w22 + w25: finalise the
|
|
5759
|
-
// pending TransparentEntry with the renderableIndex pointing at the
|
|
5760
|
-
// RenderableSnapshot we just pushed (when isRenderable === true).
|
|
5761
|
-
// The check below also covers a sprite entity that survives the
|
|
5762
|
-
// dangling-Instances branch (silent skip with `dispatchEntry !==
|
|
5763
|
-
// null` early-continue) — in that case renderableIndex is still set
|
|
5764
|
-
// to the just-pushed slot which is correct because materialDispatch
|
|
5765
|
-
// already captures the dispatch position.
|
|
5766
|
-
}
|
|
5767
|
-
}
|
|
5768
|
-
|
|
5769
|
-
// M3 / w26: sort dispatch entries by queue (ascending, stable sort)
|
|
5770
|
-
// per plan-strategy D-3.
|
|
5771
|
-
dispatch = sortDispatchByQueue(dispatch);
|
|
5772
|
-
|
|
5773
|
-
// D-1: collect PostProcessParams entities into Map<shaderId, Uint8Array>.
|
|
5774
|
-
// Last-one-wins when multiple entities bear the same shader id (mirrors
|
|
5775
|
-
// Camera.exposure -> CameraSnapshot pattern; extract stage only reads).
|
|
5776
|
-
const postProcessParams: Map<string, Uint8Array> = new Map();
|
|
5777
|
-
const postProcessParamsQuery = world.query({ with: [PostProcessParams] }).unwrap();
|
|
5778
|
-
for (const row of postProcessParamsQuery) {
|
|
5779
|
-
const entity = row.entity;
|
|
5780
|
-
const read = world.get(entity, PostProcessParams);
|
|
5781
|
-
if (!read.ok) continue;
|
|
5782
|
-
postProcessParams.set(read.value.shader, read.value.data);
|
|
5783
|
-
}
|
|
5784
|
-
|
|
5785
|
-
// feat-20260621 M-A3 / w13 (D-5): engine built-in tonemap data-driven
|
|
5786
|
-
// provider. The engine bridges the active camera's `Camera.exposure /
|
|
5787
|
-
// whitePoint / tonemap` onto the SAME unified params channel custom
|
|
5788
|
-
// post-processes use — `Camera.exposure` stays the AI-user-facing SSOT (D-5),
|
|
5789
|
-
// the engine itself acts as the provider for the Standard tonemap shader
|
|
5790
|
-
// id. The 16B layout keeps the prior recordTonemapPass packing
|
|
5791
|
-
// (render-system-record.ts pre-w14): Float32 [exposure, whitePoint, _,
|
|
5792
|
-
// ditherEnabled], with the mode u32 occupying the third 4-byte slot via
|
|
5793
|
-
// tonemapToU32 (SSOT in camera.ts). The final Output Transform makes a
|
|
5794
|
-
// per-pass copy and writes ditherEnabled at byte offset 12; the extracted
|
|
5795
|
-
// camera snapshot remains the SSOT. Run AFTER the user-entity collection above so the engine's
|
|
5796
|
-
// built-in provider is authoritative for its own reserved key (a user entity
|
|
5797
|
-
// can never shadow the Standard tonemap identity. The single active camera mirrors
|
|
5798
|
-
// recordFrame's `activeCameras[0]` selection.
|
|
5799
|
-
const tonemapCamera = cameras[0];
|
|
5800
|
-
if (tonemapCamera !== undefined) {
|
|
5801
|
-
postProcessParams.set(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, tonemapParams(tonemapCamera));
|
|
5802
|
-
}
|
|
5803
|
-
|
|
5804
|
-
return {
|
|
5805
|
-
cameras,
|
|
5806
|
-
auxiliaryCameras,
|
|
5807
|
-
cubeCameras,
|
|
5808
|
-
reflectionProbes,
|
|
5809
|
-
lights,
|
|
5810
|
-
environment,
|
|
5811
|
-
environmentReady: true,
|
|
5812
|
-
...(volumetricFog === undefined ? {} : { volumetricFog }),
|
|
5813
|
-
renderables,
|
|
5814
|
-
dispatch,
|
|
5815
|
-
skylight,
|
|
5816
|
-
skylightCount,
|
|
5817
|
-
lightProbes,
|
|
5818
|
-
skybox,
|
|
5819
|
-
skyboxCount,
|
|
5820
|
-
fog,
|
|
5821
|
-
...(fogFailure === undefined ? {} : { fogFailure }),
|
|
5822
|
-
frustumStats: { culled: frustumCulled, total: frustumTotal },
|
|
5823
|
-
visibilityStats: { explicitlyHidden: explicitlyHidden.size },
|
|
5824
|
-
postProcessParams,
|
|
5825
|
-
visibilitySnapshots: [context.visibility],
|
|
5826
|
-
featureVisibilitySnapshots: [{ world, snapshot: context.visibility }],
|
|
5827
|
-
hiddenEntityReports: [...explicitlyHidden].map((entity) => ({ world, entity })),
|
|
5828
|
-
};
|
|
5829
|
-
}
|
|
5830
|
-
|
|
5831
|
-
export function defaultMaterialSnapshot(materialHandle = 0): MaterialSnapshot {
|
|
5832
|
-
// Mid-grey unlit fallback (D-Q7 case B + extract-stage missing-spec),
|
|
5833
|
-
// matching the pre-w6 visual outcome where the record stage's force-cast
|
|
5834
|
-
// read of `firstMaterial.baseColorTexture` returned undefined and the
|
|
5835
|
-
// unlit fallback shader was selected.
|
|
5836
|
-
return {
|
|
5837
|
-
baseColor: vec3.create(0.5, 0.5, 0.5),
|
|
5838
|
-
metallic: 0,
|
|
5839
|
-
roughness: 1,
|
|
5840
|
-
materialHandle,
|
|
5841
|
-
};
|
|
5842
|
-
}
|
|
5843
|
-
|
|
5844
|
-
function identityProjection(): Float32Array {
|
|
5845
|
-
const projection = new Float32Array(16);
|
|
5846
|
-
projection[0] = 1;
|
|
5847
|
-
projection[5] = 1;
|
|
5848
|
-
projection[10] = 1;
|
|
5849
|
-
projection[15] = 1;
|
|
5850
|
-
return projection;
|
|
5851
|
-
}
|
|
2998
|
+
export { defaultMaterialSnapshot, extractFrame, extractFrames } from './render-system-extract-tail';
|