@forgeax/engine-render 0.1.24 → 0.1.25
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 +100 -1
- package/dist/assembly/factory.d.ts +1 -1
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/material/surface-projection.d.ts +24 -0
- package/dist/assembly/material/surface-projection.d.ts.map +1 -0
- package/dist/assembly/material-shader-policy.d.ts +20 -0
- package/dist/assembly/material-shader-policy.d.ts.map +1 -1
- package/dist/assembly/webgpu-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer.d.ts +1 -1
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/assembly/webgpu-vertex-layouts.d.ts +89 -0
- package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -0
- package/dist/assets/asset-decoders.d.ts.map +1 -1
- package/dist/authoring.mjs +1 -1
- package/dist/{chunk-Q752SHI5.mjs → chunk-ADHHVYLW.mjs} +3 -3
- package/dist/chunk-ADHHVYLW.mjs.map +1 -0
- package/dist/{chunk-TFH5I7JJ.mjs → chunk-E34VL5VI.mjs} +3 -3
- package/dist/{chunk-TFH5I7JJ.mjs.map → chunk-E34VL5VI.mjs.map} +1 -1
- package/dist/{chunk-KZFTK6DO.mjs → chunk-HKXTW355.mjs} +7 -4
- package/dist/chunk-HKXTW355.mjs.map +1 -0
- package/dist/{chunk-YKB5DQV7.mjs → chunk-JSEDTZYS.mjs} +87 -19
- package/dist/chunk-JSEDTZYS.mjs.map +1 -0
- package/dist/{chunk-UQYS7FRM.mjs → chunk-KM2NOX2I.mjs} +4 -4
- package/dist/{chunk-UQYS7FRM.mjs.map → chunk-KM2NOX2I.mjs.map} +1 -1
- package/dist/{chunk-Z3YYH2MB.mjs → chunk-N3RT2EUR.mjs} +94 -41
- package/dist/chunk-N3RT2EUR.mjs.map +1 -0
- package/dist/{chunk-YRUYGA4A.mjs → chunk-TCLDX7SZ.mjs} +559 -138
- package/dist/chunk-TCLDX7SZ.mjs.map +1 -0
- package/dist/{chunk-RR5L4SRS.mjs → chunk-ZZ4YQ474.mjs} +2 -2
- package/dist/{chunk-RR5L4SRS.mjs.map → chunk-ZZ4YQ474.mjs.map} +1 -1
- package/dist/components/instances.d.ts +13 -31
- package/dist/components/instances.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +865 -142
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +6 -1
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +4 -0
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +27 -9
- package/dist/index.mjs.map +1 -1
- package/dist/instance-buffer-cache.d.ts +7 -1
- package/dist/instance-buffer-cache.d.ts.map +1 -1
- package/dist/instances.d.ts +101 -0
- package/dist/instances.d.ts.map +1 -0
- package/dist/internal.mjs +5 -5
- package/dist/materials.d.ts +63 -2
- package/dist/materials.d.ts.map +1 -1
- package/dist/pipeline-builder.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +13 -2
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +22 -3
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +2 -1
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts +11 -1
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts +1 -1
- package/dist/recovery/render-system-candidate.d.ts.map +1 -1
- package/dist/render-contract.d.ts +4 -0
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts +4 -0
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +25 -1
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +4 -1
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene-types.d.ts +2 -0
- package/dist/scene/render-scene-types.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +12 -1
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/systems/skin-palette-allocator.d.ts +6 -0
- package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
- package/dist/temporal/index.mjs +5 -5
- package/package.json +20 -20
- package/src/__tests__/gpu-scene.unit.test.ts +38 -2
- package/src/__tests__/instances-store.unit.test.ts +46 -0
- package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
- package/src/__tests__/material-contract-inventory.unit.test.ts +5 -13
- package/src/__tests__/material-pass-policy.unit.test.ts +69 -0
- package/src/__tests__/material-snapshot-mutation.integration.test.ts +69 -0
- package/src/__tests__/material-surface-authoring.test-d.ts +62 -0
- package/src/__tests__/materials-transmission-validation.unit.test.ts +24 -0
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +0 -4
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +42 -0
- package/src/__tests__/skin-motion-regression.unit.test.ts +33 -0
- package/src/__tests__/skinned-shadow-caster.test.ts +30 -2
- package/src/__tests__/surface-adjacent-regressions.integration.test.ts +60 -0
- package/src/__tests__/surface-standard-pipeline.browser.test.ts +20 -0
- package/src/__tests__/surface-standard-pipeline.dawn.test.ts +20 -0
- package/src/__tests__/surface-standard-pipeline.fixture.ts +132 -0
- package/src/__tests__/surface-variant-provenance.integration.test.ts +33 -0
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +1 -1
- package/src/assembly/factory.ts +2 -0
- package/src/assembly/material/__tests__/standard-layer-plan-projection.integration.test.ts +18 -0
- package/src/assembly/material/__tests__/surface-pass-projection.integration.test.ts +62 -0
- package/src/assembly/material/surface-projection.ts +90 -0
- package/src/assembly/material-shader-policy.ts +88 -10
- package/src/assembly/recovery/renderer-recover.ts +1 -1
- package/src/assembly/webgpu-ready.ts +3 -16
- package/src/assembly/webgpu-renderer.ts +90 -158
- package/src/assembly/webgpu-vertex-layouts.ts +132 -0
- package/src/assets/asset-decoders.ts +27 -4
- package/src/components/instances.ts +14 -197
- package/src/gpu-driven/production-raster.ts +34 -1
- package/src/gpu-scene.ts +84 -29
- package/src/index.ts +1 -0
- package/src/instance-buffer-cache.ts +14 -1
- package/src/instances.ts +288 -0
- package/src/materials.ts +107 -12
- package/src/pipeline-builder.ts +14 -10
- package/src/pipeline-spec.ts +6 -14
- package/src/record/frame-snapshot.ts +19 -2
- package/src/record/frame.ts +31 -2
- package/src/record/main-pass-geometry.ts +498 -59
- package/src/record/main-pass-sprite-draws.ts +1 -1
- package/src/record/render-context.ts +4 -0
- package/src/record/shadow-pass.ts +46 -99
- package/src/recovery/render-system-candidate.ts +41 -4
- package/src/render-contract.ts +4 -0
- package/src/render-system-extract-tail.ts +107 -51
- package/src/render-system-extract.ts +53 -26
- package/src/render-system.ts +40 -5
- package/src/scene/render-scene-types.ts +2 -0
- package/src/scene/render-scene.ts +579 -36
- package/src/systems/skin-palette-allocator.ts +129 -7
- package/src/transmission/__tests__/surface-compatibility.integration.test.ts +32 -0
- package/dist/chunk-KZFTK6DO.mjs.map +0 -1
- package/dist/chunk-Q752SHI5.mjs.map +0 -1
- package/dist/chunk-YKB5DQV7.mjs.map +0 -1
- package/dist/chunk-YRUYGA4A.mjs.map +0 -1
- package/dist/chunk-Z3YYH2MB.mjs.map +0 -1
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type MaterialShaderArtifact,
|
|
27
27
|
} from '@forgeax/engine-shader';
|
|
28
28
|
import type {
|
|
29
|
+
MaterialAsset,
|
|
29
30
|
MaterialRenderState,
|
|
30
31
|
ParamSchemaEntry,
|
|
31
32
|
PassKind,
|
|
@@ -155,6 +156,9 @@ export interface RenderSystemRuntime {
|
|
|
155
156
|
readonly errorRegistry: RhiErrorListenerRegistry;
|
|
156
157
|
readonly debugOverlay?: RenderDebugOverlay | undefined;
|
|
157
158
|
readonly healthRegistry: HealthListenerRegistry;
|
|
159
|
+
readonly getMaterialProjectionForPayload?: (
|
|
160
|
+
material: MaterialAsset,
|
|
161
|
+
) => MaterialRenderProjection | undefined;
|
|
158
162
|
readonly getMaterialProjection?: (materialGuid: string) => MaterialRenderProjection | undefined;
|
|
159
163
|
readonly getMaterialArtifact?: (specializationKey: string) => MaterialRuntimeArtifact | undefined;
|
|
160
164
|
/** Producer-owned cooked Standard PBR artifact used by GPU-driven projection. */
|
|
@@ -8,14 +8,8 @@ import {
|
|
|
8
8
|
type RhiRenderPassEncoder,
|
|
9
9
|
} from '@forgeax/engine-rhi';
|
|
10
10
|
import type { MaterialRenderState, PassSelector } from '@forgeax/engine-types';
|
|
11
|
-
import { GpuBuffer } from '../gpu-resource';
|
|
12
|
-
import {
|
|
13
|
-
GPU_BUFFER_USAGE_COPY_DST,
|
|
14
|
-
GPU_BUFFER_USAGE_STORAGE,
|
|
15
|
-
GPU_BUFFER_USAGE_UNIFORM,
|
|
16
|
-
} from '../gpu-usage';
|
|
11
|
+
import type { GpuBuffer } from '../gpu-resource';
|
|
17
12
|
import { assembleMaterialWithSkylightEntries } from '../ibl/skylight-bind-group';
|
|
18
|
-
import type { InstanceBufferCacheEntry } from '../instance-buffer-cache';
|
|
19
13
|
import {
|
|
20
14
|
buildPbrMaterialUserRegionEntries,
|
|
21
15
|
createPbrSkinMeshBindGroupEntries,
|
|
@@ -26,15 +20,15 @@ import {
|
|
|
26
20
|
import { COOKIE_MATRIX_BYTES } from '../prepare/extended-lighting/resources';
|
|
27
21
|
import type { DispatchEntry, ExtractedLights } from '../render-system-extract';
|
|
28
22
|
import { matchPass } from '../systems/pass-selector';
|
|
29
|
-
import {
|
|
30
|
-
|
|
23
|
+
import {
|
|
24
|
+
resolveGeometryInstanceBuffer,
|
|
25
|
+
resolveGeometryInstancesBindGroup,
|
|
26
|
+
} from './main-pass-geometry';
|
|
31
27
|
import {
|
|
32
28
|
getOrCreateFromChain,
|
|
33
|
-
MAX_UNIFORM_INSTANCES,
|
|
34
29
|
MESH_PER_ENTITY_STRIDE,
|
|
35
30
|
MESH_SSBO_BYTES,
|
|
36
31
|
MESH_UBO_FULL_ARRAY_BYTES,
|
|
37
|
-
packInstanceStorageBuffer,
|
|
38
32
|
} from './mesh-ssbo';
|
|
39
33
|
import type { _InternalRenderPipelineContext } from './render-context';
|
|
40
34
|
import { STANDARD_PBR_UBO_SIZE } from './render-context';
|
|
@@ -253,13 +247,16 @@ function ensureTypedShadowViewBg(
|
|
|
253
247
|
}
|
|
254
248
|
|
|
255
249
|
interface ShadowDispatch {
|
|
250
|
+
readonly materialHandle: number;
|
|
256
251
|
readonly materialShaderId: string;
|
|
257
252
|
readonly renderState: MaterialRenderState | undefined;
|
|
253
|
+
readonly materialSlot: number | undefined;
|
|
254
|
+
readonly paramSnapshot: DispatchEntry['paramSnapshot'];
|
|
258
255
|
}
|
|
259
256
|
|
|
260
257
|
type ShadowDispatchMap = ReadonlyMap<number, ReadonlyMap<number, ShadowDispatch>>;
|
|
261
258
|
|
|
262
|
-
function shadowShaderMap(c: _InternalRenderPipelineContext): ShadowDispatchMap {
|
|
259
|
+
export function shadowShaderMap(c: _InternalRenderPipelineContext): ShadowDispatchMap {
|
|
263
260
|
const shaders = new Map<number, Map<number, ShadowDispatch>>();
|
|
264
261
|
for (const entry of c.dispatch) {
|
|
265
262
|
if (entry.tags.LightMode === 'ShadowCaster' && entry.materialShaderId !== undefined) {
|
|
@@ -268,9 +265,20 @@ function shadowShaderMap(c: _InternalRenderPipelineContext): ShadowDispatchMap {
|
|
|
268
265
|
byMaterial = new Map<number, ShadowDispatch>();
|
|
269
266
|
shaders.set(entry.renderableIndex, byMaterial);
|
|
270
267
|
}
|
|
268
|
+
const candidateSlots =
|
|
269
|
+
c.materialSlotIndices[entry.renderableIndex] ?? c.materialSlotIndices.flat();
|
|
270
|
+
const materialSlot = candidateSlots.find(
|
|
271
|
+
(slot) => c.materialSlots[slot]?.materialHandle === entry.materialHandle,
|
|
272
|
+
);
|
|
271
273
|
byMaterial.set(entry.materialHandle, {
|
|
272
|
-
|
|
274
|
+
materialHandle: entry.materialHandle,
|
|
275
|
+
materialShaderId:
|
|
276
|
+
entry.materialShaderId === 'forgeax::default-standard-pbr'
|
|
277
|
+
? SHADOW_CASTER_SHADER_ID
|
|
278
|
+
: entry.materialShaderId,
|
|
273
279
|
renderState: entry.renderState,
|
|
280
|
+
materialSlot,
|
|
281
|
+
paramSnapshot: entry.paramSnapshot,
|
|
274
282
|
});
|
|
275
283
|
}
|
|
276
284
|
}
|
|
@@ -640,89 +648,25 @@ function recordShadowCasterDraws(
|
|
|
640
648
|
shadowPass.setBindGroup(2, shadowMeshBindGroup, [i * MESH_PER_ENTITY_STRIDE]);
|
|
641
649
|
}
|
|
642
650
|
|
|
643
|
-
//
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
const instancePayload = uniformFallback
|
|
662
|
-
? shadowInst.transforms
|
|
663
|
-
: packInstanceStorageBuffer(shadowInst.transforms);
|
|
664
|
-
const requestedBytes = instancePayload.byteLength;
|
|
665
|
-
const cached = c.frameState.instanceBuffers.get(
|
|
666
|
-
worldEntityKey(entry.source.worldId, shadowInst.cacheKey),
|
|
667
|
-
);
|
|
668
|
-
let active: InstanceBufferCacheEntry | null = null;
|
|
669
|
-
if (
|
|
670
|
-
cached !== undefined &&
|
|
671
|
-
cached.uploadedArchVersion === shadowInst.archVersion &&
|
|
672
|
-
cached.uploadedByteLength === requestedBytes
|
|
673
|
-
) {
|
|
674
|
-
active = cached;
|
|
675
|
-
} else if (requestedBytes > 0) {
|
|
676
|
-
const bufRes = runtime.device.createBuffer({
|
|
677
|
-
size: requestedBytes,
|
|
678
|
-
usage: bufUsage,
|
|
679
|
-
mappedAtCreation: false,
|
|
680
|
-
});
|
|
681
|
-
if (!bufRes.ok) {
|
|
682
|
-
runtime.errorRegistry.fire(bufRes.error);
|
|
683
|
-
} else {
|
|
684
|
-
// feat-20260619 M4 / F12: destroy the old cached buffer
|
|
685
|
-
// before replacing it with the new one (D-6).
|
|
686
|
-
if (cached !== undefined && !cached.buffer.isDestroyed) {
|
|
687
|
-
const r = cached.buffer.destroy();
|
|
688
|
-
if (!r.ok) runtime.errorRegistry.fire(r.error);
|
|
689
|
-
}
|
|
690
|
-
const newBuffer = new GpuBuffer(runtime.device, bufRes.value);
|
|
691
|
-
active = {
|
|
692
|
-
buffer: newBuffer,
|
|
693
|
-
uploadedArchVersion: shadowInst.archVersion,
|
|
694
|
-
uploadedByteLength: requestedBytes,
|
|
695
|
-
};
|
|
696
|
-
c.frameState.instanceBuffers.set(
|
|
697
|
-
worldEntityKey(entry.source.worldId, shadowInst.cacheKey),
|
|
698
|
-
active,
|
|
699
|
-
);
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
if (active !== null) {
|
|
703
|
-
const writeRes = runtime.device.queue.writeBuffer(
|
|
704
|
-
active.buffer.handle,
|
|
705
|
-
0,
|
|
706
|
-
instancePayload,
|
|
707
|
-
);
|
|
708
|
-
if (!writeRes.ok) {
|
|
709
|
-
runtime.errorRegistry.fire(writeRes.error);
|
|
710
|
-
} else {
|
|
711
|
-
shadowInstanceBuffer = active.buffer.handle;
|
|
712
|
-
shadowInstanceCount = Math.max(1, shadowInst.instanceCount);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
}
|
|
651
|
+
// Main and shadow passes share one instance residency owner and upload receipt.
|
|
652
|
+
const shadowInstanceDraws = resolveGeometryInstanceBuffer(
|
|
653
|
+
c,
|
|
654
|
+
entry,
|
|
655
|
+
[
|
|
656
|
+
{
|
|
657
|
+
instanceBuffer: pipelineState.identityInstanceBuffer,
|
|
658
|
+
instanceBindGroup: resolveGeometryInstancesBindGroup(
|
|
659
|
+
c,
|
|
660
|
+
pipelineState.identityInstanceBuffer,
|
|
661
|
+
),
|
|
662
|
+
instanceCount: 1,
|
|
663
|
+
probeOffset: -1,
|
|
664
|
+
},
|
|
665
|
+
],
|
|
666
|
+
false,
|
|
667
|
+
);
|
|
668
|
+
if (shadowInstanceDraws === null) continue;
|
|
717
669
|
|
|
718
|
-
// Bind per-entity instances BG for @group(3) (or fallback identity).
|
|
719
|
-
// D-4: write end of the HDRP shadow-instances producer/consumer pair.
|
|
720
|
-
// outerKey = worldEntityKey(entry.source.worldId, entry.source.entityKey),
|
|
721
|
-
// handle = shadowInstanceBuffer; the HDRP main pass read end (:3820 below)
|
|
722
|
-
// must look up the same (compositeKey, instBuffer) leaf or the shadow
|
|
723
|
-
// instances silently drop.
|
|
724
|
-
const shadowInstancesBg = resolveGeometryInstancesBindGroup(c, shadowInstanceBuffer);
|
|
725
|
-
shadowPass.setBindGroup(3, shadowInstancesBg);
|
|
726
670
|
// feat-20260608 M4 / w16: per-submesh shadow draw loop.
|
|
727
671
|
// Only draw submeshes whose topology is triangle-list or triangle-strip
|
|
728
672
|
// (line-list / point-list submeshes cast no shadow and are skipped).
|
|
@@ -772,10 +716,13 @@ function recordShadowCasterDraws(
|
|
|
772
716
|
shadowPass.setPipeline(entryShadowPipeline);
|
|
773
717
|
shadowLastPipeline = entryShadowPipeline;
|
|
774
718
|
}
|
|
775
|
-
|
|
776
|
-
shadowPass.
|
|
777
|
-
|
|
778
|
-
|
|
719
|
+
for (const instanceDraw of shadowInstanceDraws) {
|
|
720
|
+
shadowPass.setBindGroup(3, instanceDraw.instanceBindGroup);
|
|
721
|
+
if (entry.mesh.indexed) {
|
|
722
|
+
shadowPass.drawIndexed(sm.indexCount, instanceDraw.instanceCount, sm.indexOffset, 0, 0);
|
|
723
|
+
} else {
|
|
724
|
+
shadowPass.draw(sm.vertexCount, instanceDraw.instanceCount, 0, 0);
|
|
725
|
+
}
|
|
779
726
|
}
|
|
780
727
|
}
|
|
781
728
|
}
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
postProcessShaderEntrySignature,
|
|
29
29
|
} from '../fullscreen-post-process-pass';
|
|
30
30
|
import { GpuDrivenProduction } from '../gpu-driven/production-raster';
|
|
31
|
+
import { disposeInstanceBufferChunks, disposeInstanceBuffers } from '../instance-buffer-cache';
|
|
31
32
|
import { inspectStandardLighting } from '../pipeline/standard-lighting/inspection';
|
|
32
33
|
import type { PipelineBuilderShaderModuleFactory } from '../pipeline-builder';
|
|
33
34
|
import { inspectPointShadow } from '../point-shadow-inspection';
|
|
@@ -35,6 +36,7 @@ import type { PreparedGraphicsResolver } from '../prepare/prepared-graphics-reso
|
|
|
35
36
|
import { validateRenderables } from '../record/frame';
|
|
36
37
|
import { prepareFrameLighting } from '../record/frame-lighting';
|
|
37
38
|
import type { RenderFrameState, ValidatedRenderable } from '../record/frame-snapshot';
|
|
39
|
+
import { resolveGeometryInstanceBuffer } from '../record/main-pass-geometry';
|
|
38
40
|
import { computeSplitLdrSprite } from '../record/main-pass-sprite-draws';
|
|
39
41
|
import {
|
|
40
42
|
prepareRecoveryPipelineReadiness,
|
|
@@ -121,7 +123,7 @@ export interface RenderSystemRecoveryOwner {
|
|
|
121
123
|
setLastSuccessfulFrameSeed(seed: RecoveryFrameSeed): void;
|
|
122
124
|
prepareRecoveryGraphCandidate(
|
|
123
125
|
runtime: RecoveryGraphCandidateRuntime,
|
|
124
|
-
): RecoveryGraphCandidatePreparation
|
|
126
|
+
): Promise<RecoveryGraphCandidatePreparation>;
|
|
125
127
|
submitCandidateSetup(
|
|
126
128
|
candidate: RecoveryGraphCandidate,
|
|
127
129
|
isValid: () => boolean,
|
|
@@ -225,6 +227,8 @@ export function createRenderSystemRecovery(
|
|
|
225
227
|
currentDirectionalShadowView: null,
|
|
226
228
|
currentSpotShadowView: null,
|
|
227
229
|
instanceBuffers: new Map(),
|
|
230
|
+
instanceBufferChunks: new Map(),
|
|
231
|
+
instanceResidency: new Map(),
|
|
228
232
|
morphBuffers: new Map(),
|
|
229
233
|
hdrpClusterBinScratch: createClusterBinScratch(),
|
|
230
234
|
hdrpClusterGridScratch: null,
|
|
@@ -313,9 +317,9 @@ export function createRenderSystemRecovery(
|
|
|
313
317
|
candidate.release();
|
|
314
318
|
};
|
|
315
319
|
|
|
316
|
-
const prepareRecoveryGraphCandidate = (
|
|
320
|
+
const prepareRecoveryGraphCandidate = async (
|
|
317
321
|
runtime: RecoveryGraphCandidateRuntime,
|
|
318
|
-
): RecoveryGraphCandidatePreparation => {
|
|
322
|
+
): Promise<RecoveryGraphCandidatePreparation> => {
|
|
319
323
|
const seed = lastSuccessfulFrameSeed;
|
|
320
324
|
if (seed === undefined) return { kind: 'no-seed' };
|
|
321
325
|
const camera = seed.frame.cameras[0];
|
|
@@ -382,6 +386,13 @@ export function createRenderSystemRecovery(
|
|
|
382
386
|
candidateGpuDrivenScene = undefined;
|
|
383
387
|
candidatePointsLines?.release();
|
|
384
388
|
candidatePointsLines = undefined;
|
|
389
|
+
disposeInstanceBuffers(candidateFrameState.instanceBuffers, runtime.internals.errorRegistry);
|
|
390
|
+
if (candidateFrameState.instanceBufferChunks !== undefined) {
|
|
391
|
+
disposeInstanceBufferChunks(
|
|
392
|
+
candidateFrameState.instanceBufferChunks,
|
|
393
|
+
runtime.internals.errorRegistry,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
385
396
|
candidateGpuStore.destroyAll();
|
|
386
397
|
};
|
|
387
398
|
const markPublished = (): void => {
|
|
@@ -403,10 +414,17 @@ export function createRenderSystemRecovery(
|
|
|
403
414
|
if (!gpuScene.ok) return failed('candidate GPU scene preparation failed', gpuScene.error);
|
|
404
415
|
candidateGpuDrivenScene = gpuScene.value;
|
|
405
416
|
if (candidateGpuDrivenScene.state !== undefined) {
|
|
406
|
-
|
|
417
|
+
const production = await GpuDrivenProduction.forRecoveryDevice(
|
|
407
418
|
runtime.internals.device,
|
|
408
419
|
gpuDrivenShaderFactory,
|
|
420
|
+
(descriptor) =>
|
|
421
|
+
internals.createShaderModule === undefined
|
|
422
|
+
? Promise.resolve(gpuDrivenShaderFactory.createShaderModule(descriptor))
|
|
423
|
+
: internals.createShaderModule(runtime.internals.device, descriptor),
|
|
409
424
|
);
|
|
425
|
+
if (!production.ok)
|
|
426
|
+
return failed('candidate GPU-driven shader preparation failed', production.error);
|
|
427
|
+
candidateGpuDrivenProduction = production.value;
|
|
410
428
|
const prepared = candidateGpuDrivenProduction.prepare({
|
|
411
429
|
scene: candidateGpuDrivenScene.state,
|
|
412
430
|
camera,
|
|
@@ -578,6 +596,25 @@ export function createRenderSystemRecovery(
|
|
|
578
596
|
if (candidateValidated.length !== seed.frame.renderables.length) {
|
|
579
597
|
return failed('candidate validation did not retain every LKG-visible renderable');
|
|
580
598
|
}
|
|
599
|
+
for (const entry of candidateValidated) {
|
|
600
|
+
if (entry.source.instances === undefined) continue;
|
|
601
|
+
const prepared = resolveGeometryInstanceBuffer(
|
|
602
|
+
{
|
|
603
|
+
runtime: runtime.internals,
|
|
604
|
+
pipelineState: runtime.pipelineState,
|
|
605
|
+
frameState: candidateFrameState,
|
|
606
|
+
bindGroupCounts: { createBindGroup: 0, keys: [] },
|
|
607
|
+
},
|
|
608
|
+
entry,
|
|
609
|
+
[],
|
|
610
|
+
false,
|
|
611
|
+
);
|
|
612
|
+
if (prepared === null) return failed('candidate instance preparation failed');
|
|
613
|
+
}
|
|
614
|
+
// These uploads belong to recovery preparation, never to a rendered frame.
|
|
615
|
+
for (const [id, residency] of candidateFrameState.instanceResidency ?? []) {
|
|
616
|
+
candidateFrameState.instanceResidency?.set(id, { ...residency, frameNumber: -1 });
|
|
617
|
+
}
|
|
581
618
|
const lighting = prepareFrameLighting(
|
|
582
619
|
runtime.internals,
|
|
583
620
|
candidateFrameState,
|
package/src/render-contract.ts
CHANGED
|
@@ -42,6 +42,7 @@ import type {
|
|
|
42
42
|
TemporalTargetInspection,
|
|
43
43
|
TransmissionInspection,
|
|
44
44
|
} from './inspection-types';
|
|
45
|
+
import type { InstanceCollectionInspection } from './instances';
|
|
45
46
|
import type { SsrAdmissionIdentity } from './ssr/identity';
|
|
46
47
|
import type {
|
|
47
48
|
RenderTarget,
|
|
@@ -294,6 +295,7 @@ export interface SpotLightProjectorFrameContext {
|
|
|
294
295
|
* leases, receipts, inspection PODs, and recovery Result values.
|
|
295
296
|
*/
|
|
296
297
|
export interface Renderer {
|
|
298
|
+
/** Renderer-owned authoring and update path for large instance collections. */
|
|
297
299
|
attach(world: World): RenderResult<RenderWorldLease, RenderError>;
|
|
298
300
|
draw(input: RenderFrameInput): RenderResult<FrameReceipt, RenderError>;
|
|
299
301
|
createRenderTarget(descriptor: RenderTargetDescriptor): RenderResult<RenderTarget, RenderError>;
|
|
@@ -509,6 +511,8 @@ export interface RenderInspection {
|
|
|
509
511
|
readonly featureDiagnostics: readonly RenderFeatureDiagnostics[];
|
|
510
512
|
readonly frustumStats: { readonly culled: number; readonly total: number };
|
|
511
513
|
readonly visibilityStats: { readonly explicitlyHidden: number };
|
|
514
|
+
/** Renderer-owned large-instance residency and upload facts. */
|
|
515
|
+
readonly instanceCollections: readonly InstanceCollectionInspection[];
|
|
512
516
|
readonly renderScene: RenderSceneInspection;
|
|
513
517
|
readonly reflectionProbes: ReflectionProbeInspection;
|
|
514
518
|
/** Renderer-owned SSR dependency seam; consumer facts are projected from live owners. */
|
|
@@ -88,7 +88,8 @@ import type {
|
|
|
88
88
|
} from './features/types';
|
|
89
89
|
import { ensureGlyphMeshMaterialSlots } from './glyph-text-layout-system';
|
|
90
90
|
import type { GpuDrivenGeometryReceipt } from './gpu-driven/prepared-draw';
|
|
91
|
-
import {
|
|
91
|
+
import type { InstanceProjectionStore } from './instances';
|
|
92
|
+
import { deriveInstancesUnionBounds, fingerprintNumericArray } from './instances-derived-bounds';
|
|
92
93
|
import {
|
|
93
94
|
type MeshMaterialBindingDiagnostic,
|
|
94
95
|
type MeshMaterialBindingSource,
|
|
@@ -156,7 +157,7 @@ import {
|
|
|
156
157
|
readStablePersistentMaterialSnapshot,
|
|
157
158
|
resolveMaterialSnapshot,
|
|
158
159
|
resolveVideoFieldHandle,
|
|
159
|
-
|
|
160
|
+
runtimeMaterialShaderIdForMaterial,
|
|
160
161
|
Severity,
|
|
161
162
|
selectCameraRoles,
|
|
162
163
|
selectFogFrame,
|
|
@@ -175,9 +176,12 @@ export function extractFrames(
|
|
|
175
176
|
options: {
|
|
176
177
|
readonly cull?: 'normal' | 'none';
|
|
177
178
|
readonly renderables?: 'full' | 'none';
|
|
179
|
+
readonly renderableEntitiesByWorld?: readonly (ReadonlySet<number> | undefined)[];
|
|
180
|
+
readonly retainHidden?: boolean;
|
|
178
181
|
readonly getMaterialShaderArtifact?: (
|
|
179
182
|
materialShaderId: string,
|
|
180
183
|
) => MaterialShaderArtifact | undefined;
|
|
184
|
+
readonly instanceCollections?: InstanceProjectionStore;
|
|
181
185
|
} = {},
|
|
182
186
|
): ExtractedFrame {
|
|
183
187
|
// w4: normalize the owner argument. A bare number is the legacy single-owner
|
|
@@ -249,9 +253,16 @@ export function extractFrames(
|
|
|
249
253
|
}),
|
|
250
254
|
cull: options.cull === 'none' ? 'none' : isCameraOwner ? 'self' : 'external',
|
|
251
255
|
renderables: options.renderables ?? 'full',
|
|
256
|
+
...(options.renderableEntitiesByWorld?.[wi] === undefined
|
|
257
|
+
? {}
|
|
258
|
+
: { renderableEntities: options.renderableEntitiesByWorld[wi] }),
|
|
259
|
+
retainHidden: options.retainHidden ?? false,
|
|
252
260
|
...(options.getMaterialShaderArtifact === undefined
|
|
253
261
|
? {}
|
|
254
262
|
: { getMaterialShaderArtifact: options.getMaterialShaderArtifact }),
|
|
263
|
+
...(options.instanceCollections === undefined
|
|
264
|
+
? {}
|
|
265
|
+
: { instanceCollections: options.instanceCollections }),
|
|
255
266
|
...(cameraOwnerFrame === undefined ? {} : { cullCameras: cameraOwnerFrame.cameras }),
|
|
256
267
|
});
|
|
257
268
|
const frame = extractFrame(world, prepared);
|
|
@@ -495,6 +506,30 @@ export function extractFrames(
|
|
|
495
506
|
return (f.reflectionProbes ?? []).map((probe) => ({ ...probe, worldId }));
|
|
496
507
|
});
|
|
497
508
|
|
|
509
|
+
if (options.renderables !== 'none' && options.renderableEntitiesByWorld === undefined) {
|
|
510
|
+
options.instanceCollections?.retain(
|
|
511
|
+
new Set(
|
|
512
|
+
renderables.flatMap((row) =>
|
|
513
|
+
row.instances?.collectionId === undefined ? [] : [row.instances.collectionId],
|
|
514
|
+
),
|
|
515
|
+
),
|
|
516
|
+
);
|
|
517
|
+
if (
|
|
518
|
+
skinPaletteAllocator !== null &&
|
|
519
|
+
typeof skinPaletteAllocator.retainSliceKeys === 'function'
|
|
520
|
+
) {
|
|
521
|
+
skinPaletteAllocator.retainSliceKeys(
|
|
522
|
+
new Set(
|
|
523
|
+
renderables.flatMap((row) =>
|
|
524
|
+
row.skin === undefined
|
|
525
|
+
? []
|
|
526
|
+
: [`${worlds[row.worldId]?.identity ?? row.worldId}:${row.entityKey}`],
|
|
527
|
+
),
|
|
528
|
+
),
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
498
533
|
return {
|
|
499
534
|
cameras,
|
|
500
535
|
auxiliaryCameras,
|
|
@@ -604,6 +639,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
604
639
|
cull: cullMode,
|
|
605
640
|
resourceOwnerFog,
|
|
606
641
|
getMaterialShaderArtifact,
|
|
642
|
+
instanceCollections,
|
|
643
|
+
renderableEntities,
|
|
644
|
+
retainHidden,
|
|
607
645
|
} = context;
|
|
608
646
|
// Environment reads are owned by the retained resource projection and are
|
|
609
647
|
// established at the frame boundary; record never revisits World state.
|
|
@@ -1525,18 +1563,13 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
1525
1563
|
// through one World-owned Query. K-2 sniffing scheme B
|
|
1526
1564
|
// (`row.get(X) !== undefined` edge sniff) replaces the prior
|
|
1527
1565
|
// `arch.components.some` row-internal back-door. K-3 invariant: the
|
|
1528
|
-
// variable-length array reads (`MeshRenderer.materials`,
|
|
1529
|
-
//
|
|
1530
|
-
//
|
|
1531
|
-
// `row.entity`.
|
|
1566
|
+
// variable-length array reads (`MeshRenderer.materials`, SpriteInstances
|
|
1567
|
+
// arrays) still flow through `_getArrayView`; explicit Instances bytes also come
|
|
1568
|
+
// from World, before projection into renderer-owned snapshots.
|
|
1532
1569
|
//
|
|
1533
|
-
// archVersion
|
|
1534
|
-
//
|
|
1535
|
-
//
|
|
1536
|
-
// QueryRow does not surface this number, so a single archetype
|
|
1537
|
-
// graph access is retained for instance-bearing rows -- match by entity
|
|
1538
|
-
// packed handle. AC-01 grep ≤ current - 1 still holds: the stale
|
|
1539
|
-
// archetype-graph traversal commentary at the prior call site is gone.
|
|
1570
|
+
// archVersion remains the content fingerprint for SpriteInstances' managed
|
|
1571
|
+
// arrays; explicit Instances use the collection revision below and never
|
|
1572
|
+
// need an archetype graph access for their matrix bytes.
|
|
1540
1573
|
// feat-20260521-sprite-atlas-animation M3 / T-16: SpriteRegionOverride
|
|
1541
1574
|
// column id for the sprite-bucket per-entity region override read
|
|
1542
1575
|
void SpriteRegionOverride;
|
|
@@ -1564,11 +1597,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
1564
1597
|
})
|
|
1565
1598
|
.unwrap();
|
|
1566
1599
|
// QueryRow deliberately exposes component data, not an archetype version.
|
|
1567
|
-
//
|
|
1568
|
-
//
|
|
1600
|
+
// SpriteInstances still uses a content fingerprint for its managed arrays;
|
|
1601
|
+
// explicit Instances use the renderer-owned collection revision below.
|
|
1569
1602
|
const resolveArchVersion = (entity: EntityHandle): number => {
|
|
1570
1603
|
const values = [
|
|
1571
|
-
worldInternal._getArrayView(entity, Instances, 'transforms'),
|
|
1572
1604
|
worldInternal._getArrayView(entity, SpriteInstances, 'transforms'),
|
|
1573
1605
|
worldInternal._getArrayView(entity, SpriteInstances, 'regions'),
|
|
1574
1606
|
];
|
|
@@ -1591,6 +1623,13 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
1591
1623
|
for (const entry of pending) dispatch.push(entry);
|
|
1592
1624
|
};
|
|
1593
1625
|
for (const row of meshRendererQuery) {
|
|
1626
|
+
if (
|
|
1627
|
+
context.renderables === 'full' &&
|
|
1628
|
+
renderableEntities !== undefined &&
|
|
1629
|
+
!renderableEntities.has(row.entity)
|
|
1630
|
+
) {
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1594
1633
|
// K-2 archetype-edge sniff (scheme B): a missing optional component
|
|
1595
1634
|
// surfaces as an absent bundle key, not a row-internal optional chain.
|
|
1596
1635
|
// Presence-only checks use QueryRow.has so large array-bearing components
|
|
@@ -1687,8 +1726,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
1687
1726
|
{
|
|
1688
1727
|
// feat-20260608 M2 / w11: read materials array via _getArrayView
|
|
1689
1728
|
const entity = row.entity;
|
|
1690
|
-
|
|
1691
|
-
|
|
1729
|
+
const authorVisible = !(isRenderable && visibility?.effective(entity) === 'hidden');
|
|
1730
|
+
if (!authorVisible) explicitlyHidden.add(entity);
|
|
1731
|
+
if (!authorVisible && !retainHidden) {
|
|
1692
1732
|
continue;
|
|
1693
1733
|
}
|
|
1694
1734
|
const layerVal = fLayerValue ?? 0;
|
|
@@ -1886,6 +1926,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
1886
1926
|
renderables.length,
|
|
1887
1927
|
layerVal,
|
|
1888
1928
|
materialSnap.paramSnapshot,
|
|
1929
|
+
0,
|
|
1930
|
+
materialSnap.materialShaderId,
|
|
1889
1931
|
);
|
|
1890
1932
|
}
|
|
1891
1933
|
} else if (handleRaw === 0 || assets === undefined || assets === null) {
|
|
@@ -1913,6 +1955,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
1913
1955
|
renderables.length,
|
|
1914
1956
|
layerVal,
|
|
1915
1957
|
materialSnap.paramSnapshot,
|
|
1958
|
+
0,
|
|
1959
|
+
materialSnap.materialShaderId,
|
|
1916
1960
|
);
|
|
1917
1961
|
}
|
|
1918
1962
|
} else {
|
|
@@ -1976,10 +2020,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
1976
2020
|
}
|
|
1977
2021
|
const resolved = resolvedResult.value;
|
|
1978
2022
|
const allPasses = resolved.passes;
|
|
1979
|
-
const firstPassShader =
|
|
1980
|
-
allPasses.length > 0
|
|
1981
|
-
? runtimeMaterialShaderId(allPasses[0]?.program.module, allPasses[0]?.name)
|
|
1982
|
-
: undefined;
|
|
2023
|
+
const firstPassShader = runtimeMaterialShaderIdForMaterial(asset, allPasses, assets);
|
|
1983
2024
|
const pv = materialValuesToLinearRuntime(
|
|
1984
2025
|
resolved.values,
|
|
1985
2026
|
materialColorParameterSchema(resolved.parameters ?? [], firstPassShader, assets),
|
|
@@ -2357,6 +2398,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
2357
2398
|
renderables.length,
|
|
2358
2399
|
layerVal,
|
|
2359
2400
|
paramSnap,
|
|
2401
|
+
0,
|
|
2402
|
+
firstPassShader,
|
|
2360
2403
|
);
|
|
2361
2404
|
}
|
|
2362
2405
|
}
|
|
@@ -2512,7 +2555,18 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
2512
2555
|
// surface is absent (test fixtures that pass undefined) the
|
|
2513
2556
|
// hasSkin segment is skipped silently — bind-pose equivalent.
|
|
2514
2557
|
if (skinPaletteAllocator !== null) {
|
|
2515
|
-
|
|
2558
|
+
// Keep the narrow extraction seam compatible with older test or
|
|
2559
|
+
// host-owned allocator doubles while the real renderer uses the
|
|
2560
|
+
// stable-key path. The fallback is frame-local and therefore
|
|
2561
|
+
// cannot affect production allocators that implement the new
|
|
2562
|
+
// method.
|
|
2563
|
+
const slice =
|
|
2564
|
+
typeof skinPaletteAllocator.allocateSliceFor === 'function'
|
|
2565
|
+
? skinPaletteAllocator.allocateSliceFor(
|
|
2566
|
+
`${world.identity}:${entity}`,
|
|
2567
|
+
skeleton.jointCount,
|
|
2568
|
+
)
|
|
2569
|
+
: skinPaletteAllocator.allocateSlice(skeleton.jointCount);
|
|
2516
2570
|
skinPaletteAllocator.writeJointPalette(slice, ibms, jointWorlds);
|
|
2517
2571
|
skinSlice = {
|
|
2518
2572
|
jointCount: slice.jointCount,
|
|
@@ -2584,6 +2638,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
2584
2638
|
renderables.length,
|
|
2585
2639
|
layerVal,
|
|
2586
2640
|
materialsArr[mi]?.paramSnapshot,
|
|
2641
|
+
0,
|
|
2642
|
+
materialsArr[mi]?.materialShaderId,
|
|
2587
2643
|
);
|
|
2588
2644
|
}
|
|
2589
2645
|
}
|
|
@@ -2727,39 +2783,35 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
2727
2783
|
// Instances are snapshotted before the CPU visibility decision so the
|
|
2728
2784
|
// culling owner can derive the union of every instance's transformed
|
|
2729
2785
|
// mesh bounds. The same detached snapshot is then carried to record;
|
|
2730
|
-
// do not re-read the
|
|
2731
|
-
//
|
|
2786
|
+
// do not re-read the collection after culling (a mutation must not
|
|
2787
|
+
// make the cull and draw disagree within one frame).
|
|
2732
2788
|
let instancesSnap: InstancesSnapshot | undefined;
|
|
2733
2789
|
if (hasInstances) {
|
|
2734
|
-
const transforms =
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
instancesSnap = {
|
|
2741
|
-
transforms: new Float32Array(),
|
|
2742
|
-
instanceCount: 0,
|
|
2743
|
-
cacheKey: entity,
|
|
2744
|
-
archVersion,
|
|
2745
|
-
};
|
|
2746
|
-
} else {
|
|
2747
|
-
const actualLength = transforms.length;
|
|
2748
|
-
if (actualLength % 16 !== 0) {
|
|
2749
|
-
worldInternal._routeError(new InstanceTransformsStrideMismatchError(actualLength), {
|
|
2790
|
+
const transforms =
|
|
2791
|
+
worldInternal._getArrayView(entity, Instances, 'transforms') ?? new Float32Array();
|
|
2792
|
+
if (transforms.length % 16 !== 0) {
|
|
2793
|
+
worldInternal._routeError(
|
|
2794
|
+
new InstanceTransformsStrideMismatchError(transforms.length),
|
|
2795
|
+
{
|
|
2750
2796
|
severity: Severity.Error,
|
|
2751
2797
|
systemName: 'RenderSystem.extract (Instances stride)',
|
|
2752
|
-
}
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
const snapshotCopy = new Float32Array(transforms);
|
|
2756
|
-
instancesSnap = {
|
|
2757
|
-
transforms: snapshotCopy,
|
|
2758
|
-
instanceCount: actualLength / 16,
|
|
2759
|
-
cacheKey: entity,
|
|
2760
|
-
archVersion,
|
|
2761
|
-
};
|
|
2798
|
+
},
|
|
2799
|
+
);
|
|
2800
|
+
continue;
|
|
2762
2801
|
}
|
|
2802
|
+
const projected = instanceCollections?.project(world, entity, transforms);
|
|
2803
|
+
instancesSnap = {
|
|
2804
|
+
transforms: projected?.transforms ?? new Float32Array(transforms),
|
|
2805
|
+
instanceCount: transforms.length / 16,
|
|
2806
|
+
cacheKey: entity,
|
|
2807
|
+
archVersion: projected === undefined ? fingerprintNumericArray(transforms) : 0,
|
|
2808
|
+
...(projected === undefined
|
|
2809
|
+
? {}
|
|
2810
|
+
: {
|
|
2811
|
+
collectionId: projected.collectionId,
|
|
2812
|
+
revision: projected.revision,
|
|
2813
|
+
}),
|
|
2814
|
+
};
|
|
2763
2815
|
}
|
|
2764
2816
|
|
|
2765
2817
|
const baseRenderable: RenderableSnapshot = {
|
|
@@ -2774,7 +2826,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
2774
2826
|
materialBindingDiagnostics,
|
|
2775
2827
|
worldId: 0,
|
|
2776
2828
|
entityKey: entity,
|
|
2829
|
+
...(authorVisible ? {} : { authorVisible: false }),
|
|
2777
2830
|
...(skinSlice !== undefined ? { skin: skinSlice } : {}),
|
|
2831
|
+
...(skin?.joints === undefined
|
|
2832
|
+
? {}
|
|
2833
|
+
: { skinJointEntities: Array.from(skin.joints, (joint) => Number(joint)) }),
|
|
2778
2834
|
...(morph !== undefined ? { morph } : {}),
|
|
2779
2835
|
...(instancesSnap !== undefined ? { instances: instancesSnap } : {}),
|
|
2780
2836
|
...(spriteInstancesSnap !== undefined ? { spriteInstances: spriteInstancesSnap } : {}),
|