@haiyue/extensions 0.1.0
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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/animation/Animation2DComponent.d.ts +46 -0
- package/dist/animation/Animation2DExtensionRegistry.d.ts +27 -0
- package/dist/animation/Animation2DRenderSystem.d.ts +89 -0
- package/dist/animation/Animation2DRuntime.d.ts +99 -0
- package/dist/animation/Animation2DStateMachine.d.ts +13 -0
- package/dist/animation/Animation2DStateMachineComponent.d.ts +54 -0
- package/dist/animation/Animation2DStateMachineRuntime.d.ts +30 -0
- package/dist/animation/Animation2DStateMachineSystem.d.ts +14 -0
- package/dist/animation/Animation2DStateMachineVisualRuntime.d.ts +27 -0
- package/dist/animation/Animation2DSystem.d.ts +14 -0
- package/dist/animation/AnimationAssetLoader.d.ts +9 -0
- package/dist/animation/AnimationAudioClip.d.ts +26 -0
- package/dist/animation/AnimationPathTessellator.d.ts +52 -0
- package/dist/animation/AnimationTextRasterizer.d.ts +21 -0
- package/dist/animation/AnimationVisual2D.d.ts +68 -0
- package/dist/animation/HyaAnimation2DClipAdapter.d.ts +16 -0
- package/dist/animation/Particle2DDescriptorAdapter.d.ts +5 -0
- package/dist/animation/runtime/mixer/Action.d.ts +150 -0
- package/dist/animation/runtime/mixer/Mixer.d.ts +54 -0
- package/dist/animation/runtime/mixer/PoseBuffer.d.ts +23 -0
- package/dist/animation/runtime/mixer/Sampler.d.ts +17 -0
- package/dist/animation/runtime/mixer/Types.d.ts +101 -0
- package/dist/animation/runtime/mixer/index.d.ts +6 -0
- package/dist/animation-state-machine/AnimationStateMachine.d.ts +111 -0
- package/dist/animation-state-machine/AnimationStateMachineChannels.d.ts +23 -0
- package/dist/animation-state-machine/runtime/Animation2DStateMachineMixerAdapter.d.ts +51 -0
- package/dist/animation-state-machine/runtime/AnimationStateMachineBlendTreeWeights.d.ts +14 -0
- package/dist/animation-state-machine/runtime/AnimationStateMachineCompiler.d.ts +109 -0
- package/dist/animation-state-machine/runtime/AnimationStateMachineController.d.ts +103 -0
- package/dist/animation-state-machine/runtime/AnimationStateMachineMixerPort.d.ts +35 -0
- package/dist/animation-state-machine/runtime/index.d.ts +7 -0
- package/dist/animation.d.ts +15 -0
- package/dist/animation.js +1171 -0
- package/dist/animation3d/Animation3DAction.d.ts +46 -0
- package/dist/animation3d/Animation3DBinding.d.ts +85 -0
- package/dist/animation3d/Animation3DClip.d.ts +20 -0
- package/dist/animation3d/Animation3DError.d.ts +13 -0
- package/dist/animation3d/Animation3DMixer.d.ts +27 -0
- package/dist/animation3d/Animation3DPose.d.ts +40 -0
- package/dist/animation3d/Animation3DPoseBuffer.d.ts +24 -0
- package/dist/animation3d/Animation3DResource.d.ts +28 -0
- package/dist/animation3d/Animation3DStateMachine.d.ts +95 -0
- package/dist/animation3d/Animation3DStateMachineRuntime.d.ts +70 -0
- package/dist/animation3d/Animation3DTrack.d.ts +19 -0
- package/dist/animation3d/hya/HyaAnimation3DRuntime.d.ts +73 -0
- package/dist/animation3d/hya/HyaAnimation3DStateMachineRuntime.d.ts +65 -0
- package/dist/animation3d/hya/HyaAnimation3DTypes.d.ts +170 -0
- package/dist/animation3d/hya/index.d.ts +4 -0
- package/dist/animation3d/index.d.ts +14 -0
- package/dist/animation3d/runtime/integration/Animation3DStateMachineMixerAdapter.d.ts +81 -0
- package/dist/animation3d/runtime/integration/Animation3DStateMachineMixerIntegration.d.ts +39 -0
- package/dist/animation3d/runtime/integration/index.d.ts +2 -0
- package/dist/animation3d/runtime/mixer/Action.d.ts +150 -0
- package/dist/animation3d/runtime/mixer/Animation3DMixerRuntimeStore.d.ts +6 -0
- package/dist/animation3d/runtime/mixer/Mixer.d.ts +59 -0
- package/dist/animation3d/runtime/mixer/PoseBuffer.d.ts +1 -0
- package/dist/animation3d/runtime/mixer/TrackSampler.d.ts +24 -0
- package/dist/animation3d/runtime/mixer/index.d.ts +4 -0
- package/dist/animation3d/runtime/state-machine/Animation3DBlendTreeWeights.d.ts +1 -0
- package/dist/animation3d/runtime/state-machine/Animation3DStateMachineCompiler.d.ts +1 -0
- package/dist/animation3d/runtime/state-machine/Animation3DStateMachineController.d.ts +1 -0
- package/dist/animation3d/runtime/state-machine/Animation3DStateMachineMixerPort.d.ts +1 -0
- package/dist/animation3d/runtime/state-machine/index.d.ts +6 -0
- package/dist/animation3d.d.ts +5 -0
- package/dist/animation3d.js +1708 -0
- package/dist/benchmark.d.ts +30 -0
- package/dist/benchmark.js +119 -0
- package/dist/canvas-text/CanvasText2DRenderSystem.d.ts +24 -0
- package/dist/canvas-text/CanvasTextComponent.d.ts +18 -0
- package/dist/canvas-text.d.ts +4 -0
- package/dist/canvas-text.js +276 -0
- package/dist/chunks/2d-ui-animation-2d.generated-D4aHzFU2.js +4 -0
- package/dist/chunks/2d-ui-canvas-text-2d.generated-C0Eaej2m.js +4 -0
- package/dist/chunks/2d-ui-spine2d.generated-CUUi8xjl.js +4 -0
- package/dist/chunks/2d-ui-tilemap2d.generated-gebw4mX7.js +4 -0
- package/dist/chunks/Animation2DStateMachine-9IonguNa.js +3245 -0
- package/dist/chunks/AnimationStateMachineController-BIcAh-sT.js +1438 -0
- package/dist/chunks/GltfAnimation3DAdapter-Cu3rQQsW.js +2240 -0
- package/dist/chunks/GltfAnimationRuntime-CRHsLycN.js +631 -0
- package/dist/chunks/RenderSystem2DBase-DDT61dAu.js +227 -0
- package/dist/chunks/Spine2DRuntime-D9M9LAQR.js +3056 -0
- package/dist/chunks/SpineAssetParser-BSDmPge1.js +378 -0
- package/dist/chunks/gltfLoader-Dm22jLYB.js +2085 -0
- package/dist/chunks/pluginUtils-CvwroRMd.js +33 -0
- package/dist/experimental-gltf-worker.d.ts +4 -0
- package/dist/experimental-gltf-worker.js +138 -0
- package/dist/experimental-spine-worker.d.ts +4 -0
- package/dist/experimental-spine-worker.js +110 -0
- package/dist/gltf/GltfAccessorReader.d.ts +7 -0
- package/dist/gltf/GltfAnimation3DAdapter.d.ts +47 -0
- package/dist/gltf/GltfAnimationRuntime.d.ts +10 -0
- package/dist/gltf/GltfAssetWorkerClient.d.ts +20 -0
- package/dist/gltf/GltfCompatibilityReport.d.ts +70 -0
- package/dist/gltf/GltfConservativeBounds.d.ts +18 -0
- package/dist/gltf/GltfDracoDecoder.d.ts +4 -0
- package/dist/gltf/GltfExtensionAdapter.d.ts +54 -0
- package/dist/gltf/GltfLoaderContract.d.ts +249 -0
- package/dist/gltf/GltfLoaderErrors.d.ts +4 -0
- package/dist/gltf/GltfMaterialDescriptor.d.ts +23 -0
- package/dist/gltf/GltfMaterialEncoder.d.ts +20 -0
- package/dist/gltf/GltfMaterialLoader.d.ts +6 -0
- package/dist/gltf/GltfModelComponent.d.ts +72 -0
- package/dist/gltf/GltfModelSystem.d.ts +37 -0
- package/dist/gltf/GltfPlugin.d.ts +6 -0
- package/dist/gltf/GltfSchema.d.ts +225 -0
- package/dist/gltf/GltfUvSemanticPlanner.d.ts +33 -0
- package/dist/gltf/gltfLoader.d.ts +9 -0
- package/dist/gltf-animation3d.d.ts +3 -0
- package/dist/gltf-animation3d.js +8 -0
- package/dist/gltf-worker-runtime.d.ts +7 -0
- package/dist/gltf-worker-runtime.js +6981 -0
- package/dist/gltf.d.ts +12 -0
- package/dist/gltf.js +517 -0
- package/dist/grid/Grid2DComponent.d.ts +25 -0
- package/dist/grid.d.ts +2 -0
- package/dist/grid.js +56 -0
- package/dist/hya-state-machine.d.ts +7 -0
- package/dist/hya-state-machine.js +2934 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- package/dist/internal/2d-ui-shader-artifact.js +870 -0
- package/dist/plugins/pluginUtils.d.ts +20 -0
- package/dist/plugins.d.ts +6 -0
- package/dist/shaders/generated/2d-ui-artifact.generated.d.ts +658 -0
- package/dist/spine/Spine2DComponent.d.ts +116 -0
- package/dist/spine/Spine2DGpuRenderer.d.ts +65 -0
- package/dist/spine/Spine2DRenderSystem.d.ts +27 -0
- package/dist/spine/Spine2DRuntime.d.ts +103 -0
- package/dist/spine/SpineAnimationBoneTimelines.d.ts +8 -0
- package/dist/spine/SpineAssetParser.d.ts +18 -0
- package/dist/spine/SpineAssetWorkerClient.d.ts +19 -0
- package/dist/spine/SpineAssetWorkerContract.d.ts +6 -0
- package/dist/spine/SpineAtlasParser.d.ts +21 -0
- package/dist/spine/SpineFloatBuilder.d.ts +15 -0
- package/dist/spine/SpinePathConstraintSolver.d.ts +32 -0
- package/dist/spine/SpinePlugin.d.ts +8 -0
- package/dist/spine/SpineSkeletonRuntime.d.ts +185 -0
- package/dist/spine/SpineSlotAnimation.d.ts +17 -0
- package/dist/spine/SpineTimelineRuntime.d.ts +25 -0
- package/dist/spine/SpineVertexBuilder.d.ts +61 -0
- package/dist/spine.d.ts +7 -0
- package/dist/spine.js +578 -0
- package/dist/tilemap/Tilemap2DComponent.d.ts +110 -0
- package/dist/tilemap/Tilemap2DRenderSystem.d.ts +11 -0
- package/dist/tilemap/Tilemap2DRenderer.d.ts +24 -0
- package/dist/tilemap/TilemapPlugin.d.ts +8 -0
- package/dist/tilemap.d.ts +6 -0
- package/dist/tilemap.js +535 -0
- package/dist/tween/Tween2DComponent.d.ts +70 -0
- package/dist/tween/Tween2DSystem.d.ts +12 -0
- package/dist/tween.d.ts +4 -0
- package/dist/tween.js +138 -0
- package/dist/utils/RenderSystem2DBase.d.ts +42 -0
- package/dist/utils/arrayAccess.d.ts +4 -0
- package/dist/utils/camera2d.d.ts +12 -0
- package/dist/utils/render2dGpu.d.ts +32 -0
- package/package.json +124 -0
- package/src/animation/Animation2DComponent.ts +135 -0
- package/src/animation/Animation2DExtensionRegistry.ts +52 -0
- package/src/animation/Animation2DRenderSystem.ts +1026 -0
- package/src/animation/Animation2DRuntime.ts +1326 -0
- package/src/animation/Animation2DStateMachine.ts +39 -0
- package/src/animation/Animation2DStateMachineComponent.ts +228 -0
- package/src/animation/Animation2DStateMachineRuntime.ts +141 -0
- package/src/animation/Animation2DStateMachineSystem.ts +60 -0
- package/src/animation/Animation2DStateMachineVisualRuntime.ts +282 -0
- package/src/animation/Animation2DSystem.ts +72 -0
- package/src/animation/AnimationAssetLoader.ts +84 -0
- package/src/animation/AnimationAudioClip.ts +140 -0
- package/src/animation/AnimationPathTessellator.ts +691 -0
- package/src/animation/AnimationTextRasterizer.ts +340 -0
- package/src/animation/AnimationVisual2D.ts +153 -0
- package/src/animation/HyaAnimation2DClipAdapter.ts +845 -0
- package/src/animation/Particle2DDescriptorAdapter.ts +35 -0
- package/src/animation/runtime/mixer/Action.ts +876 -0
- package/src/animation/runtime/mixer/Mixer.ts +505 -0
- package/src/animation/runtime/mixer/PoseBuffer.ts +105 -0
- package/src/animation/runtime/mixer/Sampler.ts +252 -0
- package/src/animation/runtime/mixer/Types.ts +138 -0
- package/src/animation/runtime/mixer/index.ts +31 -0
- package/src/animation-state-machine/AnimationStateMachine.ts +146 -0
- package/src/animation-state-machine/AnimationStateMachineChannels.ts +116 -0
- package/src/animation-state-machine/runtime/Animation2DStateMachineMixerAdapter.ts +197 -0
- package/src/animation-state-machine/runtime/AnimationStateMachineBlendTreeWeights.ts +503 -0
- package/src/animation-state-machine/runtime/AnimationStateMachineCompiler.ts +763 -0
- package/src/animation-state-machine/runtime/AnimationStateMachineController.ts +987 -0
- package/src/animation-state-machine/runtime/AnimationStateMachineMixerPort.ts +54 -0
- package/src/animation-state-machine/runtime/index.ts +47 -0
- package/src/animation.ts +33 -0
- package/src/animation3d/Animation3DAction.ts +73 -0
- package/src/animation3d/Animation3DBinding.ts +116 -0
- package/src/animation3d/Animation3DClip.ts +22 -0
- package/src/animation3d/Animation3DError.ts +34 -0
- package/src/animation3d/Animation3DMixer.ts +82 -0
- package/src/animation3d/Animation3DPose.ts +100 -0
- package/src/animation3d/Animation3DPoseBuffer.ts +85 -0
- package/src/animation3d/Animation3DResource.ts +49 -0
- package/src/animation3d/Animation3DStateMachine.ts +126 -0
- package/src/animation3d/Animation3DStateMachineRuntime.ts +191 -0
- package/src/animation3d/Animation3DTrack.ts +29 -0
- package/src/animation3d/hya/HyaAnimation3DRuntime.ts +756 -0
- package/src/animation3d/hya/HyaAnimation3DStateMachineRuntime.ts +618 -0
- package/src/animation3d/hya/HyaAnimation3DTypes.ts +168 -0
- package/src/animation3d/hya/index.ts +10 -0
- package/src/animation3d/index.ts +76 -0
- package/src/animation3d/runtime/integration/Animation3DStateMachineMixerAdapter.ts +325 -0
- package/src/animation3d/runtime/integration/Animation3DStateMachineMixerIntegration.ts +158 -0
- package/src/animation3d/runtime/integration/index.ts +9 -0
- package/src/animation3d/runtime/mixer/Action.ts +833 -0
- package/src/animation3d/runtime/mixer/Animation3DMixerRuntimeStore.ts +26 -0
- package/src/animation3d/runtime/mixer/Mixer.ts +562 -0
- package/src/animation3d/runtime/mixer/PoseBuffer.ts +1 -0
- package/src/animation3d/runtime/mixer/TrackSampler.ts +399 -0
- package/src/animation3d/runtime/mixer/index.ts +8 -0
- package/src/animation3d/runtime/state-machine/Animation3DBlendTreeWeights.ts +4 -0
- package/src/animation3d/runtime/state-machine/Animation3DStateMachineCompiler.ts +1 -0
- package/src/animation3d/runtime/state-machine/Animation3DStateMachineController.ts +1 -0
- package/src/animation3d/runtime/state-machine/Animation3DStateMachineMixerPort.ts +1 -0
- package/src/animation3d/runtime/state-machine/index.ts +45 -0
- package/src/animation3d.ts +5 -0
- package/src/benchmark.ts +168 -0
- package/src/canvas-text/CanvasText2DRenderSystem.ts +292 -0
- package/src/canvas-text/CanvasTextComponent.ts +47 -0
- package/src/canvas-text.ts +4 -0
- package/src/experimental-gltf-worker.ts +18 -0
- package/src/experimental-spine-worker.ts +9 -0
- package/src/gltf/GltfAccessorReader.ts +271 -0
- package/src/gltf/GltfAnimation3DAdapter.ts +680 -0
- package/src/gltf/GltfAnimationRuntime.ts +392 -0
- package/src/gltf/GltfAssetWorkerClient.ts +136 -0
- package/src/gltf/GltfCompatibilityReport.ts +214 -0
- package/src/gltf/GltfConservativeBounds.ts +219 -0
- package/src/gltf/GltfDracoDecoder.ts +251 -0
- package/src/gltf/GltfExtensionAdapter.ts +340 -0
- package/src/gltf/GltfLoaderContract.ts +333 -0
- package/src/gltf/GltfLoaderErrors.ts +59 -0
- package/src/gltf/GltfMaterialDescriptor.ts +134 -0
- package/src/gltf/GltfMaterialEncoder.ts +173 -0
- package/src/gltf/GltfMaterialLoader.ts +293 -0
- package/src/gltf/GltfModelComponent.ts +115 -0
- package/src/gltf/GltfModelSystem.ts +208 -0
- package/src/gltf/GltfPlugin.ts +84 -0
- package/src/gltf/GltfSchema.ts +229 -0
- package/src/gltf/GltfUvSemanticPlanner.ts +166 -0
- package/src/gltf/gltfLoader.ts +910 -0
- package/src/gltf-animation3d.ts +10 -0
- package/src/gltf-worker-runtime.ts +10 -0
- package/src/gltf.ts +56 -0
- package/src/grid/Grid2DComponent.ts +69 -0
- package/src/grid.ts +2 -0
- package/src/hya-state-machine.ts +19 -0
- package/src/index.ts +2 -0
- package/src/plugins/pluginUtils.ts +50 -0
- package/src/plugins.ts +6 -0
- package/src/shaders/generated/2d-ui-animation-2d.generated.wgsl +242 -0
- package/src/shaders/generated/2d-ui-artifact.generated.ts +867 -0
- package/src/shaders/generated/2d-ui-canvas-text-2d.generated.wgsl +38 -0
- package/src/shaders/generated/2d-ui-spine2d.generated.wgsl +41 -0
- package/src/shaders/generated/2d-ui-tilemap2d.generated.wgsl +36 -0
- package/src/spine/Spine2DComponent.ts +168 -0
- package/src/spine/Spine2DGpuRenderer.ts +449 -0
- package/src/spine/Spine2DRenderSystem.ts +199 -0
- package/src/spine/Spine2DRuntime.ts +1529 -0
- package/src/spine/SpineAnimationBoneTimelines.ts +30 -0
- package/src/spine/SpineAssetParser.ts +20 -0
- package/src/spine/SpineAssetWorkerClient.ts +112 -0
- package/src/spine/SpineAssetWorkerContract.ts +4 -0
- package/src/spine/SpineAtlasParser.ts +127 -0
- package/src/spine/SpineFloatBuilder.ts +66 -0
- package/src/spine/SpinePathConstraintSolver.ts +582 -0
- package/src/spine/SpinePlugin.ts +76 -0
- package/src/spine/SpineSkeletonRuntime.ts +521 -0
- package/src/spine/SpineSlotAnimation.ts +195 -0
- package/src/spine/SpineTimelineRuntime.ts +453 -0
- package/src/spine/SpineVertexBuilder.ts +686 -0
- package/src/spine.ts +7 -0
- package/src/tilemap/Tilemap2DComponent.ts +232 -0
- package/src/tilemap/Tilemap2DRenderSystem.ts +58 -0
- package/src/tilemap/Tilemap2DRenderer.ts +289 -0
- package/src/tilemap/TilemapPlugin.ts +75 -0
- package/src/tilemap.ts +6 -0
- package/src/tween/Tween2DComponent.ts +83 -0
- package/src/tween/Tween2DSystem.ts +78 -0
- package/src/tween.ts +4 -0
- package/src/types/earcut.d.ts +7 -0
- package/src/types/wgsl.d.ts +4 -0
- package/src/utils/RenderSystem2DBase.ts +164 -0
- package/src/utils/arrayAccess.ts +17 -0
- package/src/utils/camera2d.ts +68 -0
- package/src/utils/render2dGpu.ts +132 -0
- package/test/fixtures/gltf/animation-characterization.gltf +344 -0
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
import {
|
|
2
|
+
alignUp4,
|
|
3
|
+
beginRenderCommandPass,
|
|
4
|
+
cloneRenderPassDescriptor,
|
|
5
|
+
estimateTextureBytes,
|
|
6
|
+
getExtensionGPUResourceTracker,
|
|
7
|
+
requireEngineDevice,
|
|
8
|
+
type IEngine,
|
|
9
|
+
type ExtensionGPUResourceTracker,
|
|
10
|
+
type RenderCommandContext,
|
|
11
|
+
getExtensionSharedRendererResource,
|
|
12
|
+
} from '@haiyue/engine/extension-authoring';
|
|
13
|
+
import { Entity, type World } from '@haiyue/engine/ecs';
|
|
14
|
+
import { RenderSystem2DBase, type RenderSystem2DBaseOptions } from '../utils/RenderSystem2DBase';
|
|
15
|
+
import {
|
|
16
|
+
createCamera2DGpu,
|
|
17
|
+
createCamera2DLayout,
|
|
18
|
+
destroyCamera2DGpu,
|
|
19
|
+
type Camera2DGpu,
|
|
20
|
+
} from '../utils/render2dGpu';
|
|
21
|
+
import animation2dWgsl from '../shaders/generated/2d-ui-animation-2d.generated.wgsl';
|
|
22
|
+
import { AnimationVisual2D } from './AnimationVisual2D';
|
|
23
|
+
import type { AnimationCompositeLayer } from '@haiyue/animation-spec';
|
|
24
|
+
|
|
25
|
+
export interface Animation2DRenderSystemOptions extends RenderSystem2DBaseOptions {
|
|
26
|
+
/** Bounds view-sized alpha targets used by masks and mattes. */
|
|
27
|
+
maxMaskTargets?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Animation2DRenderStats {
|
|
31
|
+
readonly visualCount: number;
|
|
32
|
+
readonly maskTargetCount: number;
|
|
33
|
+
readonly compositeLayerCount: number;
|
|
34
|
+
readonly droppedCompositeCount: number;
|
|
35
|
+
readonly maskPixels: number;
|
|
36
|
+
readonly effectTargetCount: number;
|
|
37
|
+
readonly droppedEffectCount: number;
|
|
38
|
+
/** Counts the reusable ping-pong RGBA targets owned by each active view. */
|
|
39
|
+
readonly effectPixels: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface EntityGpu {
|
|
43
|
+
buffer: GPUBuffer;
|
|
44
|
+
bindGroup: GPUBindGroup;
|
|
45
|
+
data: Float32Array;
|
|
46
|
+
externalTexture: GPUTexture | null;
|
|
47
|
+
externalSource: ImageBitmap | HTMLCanvasElement | HTMLImageElement | null;
|
|
48
|
+
externalVersion: number;
|
|
49
|
+
externalWidth: number;
|
|
50
|
+
externalHeight: number;
|
|
51
|
+
externalBindGroup: GPUBindGroup | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface GeometryGpu {
|
|
55
|
+
vertexBuffer: GPUBuffer;
|
|
56
|
+
indexBuffer: GPUBuffer | null;
|
|
57
|
+
indexCount: number;
|
|
58
|
+
vertexCount: number;
|
|
59
|
+
indexFormat: GPUIndexFormat;
|
|
60
|
+
version: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface MaskTarget {
|
|
64
|
+
texture: GPUTexture;
|
|
65
|
+
view: GPUTextureView;
|
|
66
|
+
width: number;
|
|
67
|
+
height: number;
|
|
68
|
+
lastFrame: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface EffectTarget {
|
|
72
|
+
textures: readonly [GPUTexture, GPUTexture];
|
|
73
|
+
views: readonly [GPUTextureView, GPUTextureView];
|
|
74
|
+
width: number;
|
|
75
|
+
height: number;
|
|
76
|
+
lastFrame: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface AnimationRenderItem {
|
|
80
|
+
entity: Entity;
|
|
81
|
+
visual: AnimationVisual2D;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const SHADER = animation2dWgsl;
|
|
85
|
+
const MAX_COMPOSITE_LAYERS = 8;
|
|
86
|
+
const COMPOSITE_PARAMS_OFFSET = 28;
|
|
87
|
+
const COMPOSITE_EXPANSION_OFFSET = COMPOSITE_PARAMS_OFFSET + MAX_COMPOSITE_LAYERS * 4;
|
|
88
|
+
const GRADIENT_PARAMS_OFFSET = COMPOSITE_EXPANSION_OFFSET + MAX_COMPOSITE_LAYERS;
|
|
89
|
+
const GRADIENT_GEOMETRY_OFFSET = GRADIENT_PARAMS_OFFSET + 4;
|
|
90
|
+
const GRADIENT_COLORS_OFFSET = GRADIENT_GEOMETRY_OFFSET + 4;
|
|
91
|
+
const GRADIENT_OFFSETS_OFFSET = GRADIENT_COLORS_OFFSET + 8 * 4;
|
|
92
|
+
const EFFECT_KINDS_OFFSET = GRADIENT_OFFSETS_OFFSET + 8;
|
|
93
|
+
const EFFECT_DATA_OFFSET = EFFECT_KINDS_OFFSET + 8;
|
|
94
|
+
const EFFECT_FLOATS = 24;
|
|
95
|
+
const MAX_EFFECTS = 8;
|
|
96
|
+
const OBJECT_FLOATS = EFFECT_DATA_OFFSET + MAX_EFFECTS * EFFECT_FLOATS;
|
|
97
|
+
const OBJECT_BYTES = OBJECT_FLOATS * 4;
|
|
98
|
+
|
|
99
|
+
interface Animation2DSharedGpu {
|
|
100
|
+
readonly cameraLayout: GPUBindGroupLayout;
|
|
101
|
+
readonly objectLayout: GPUBindGroupLayout;
|
|
102
|
+
readonly textureLayout: GPUBindGroupLayout;
|
|
103
|
+
readonly compositeLayout: GPUBindGroupLayout;
|
|
104
|
+
readonly sampler: GPUSampler;
|
|
105
|
+
readonly shader: GPUShaderModule;
|
|
106
|
+
readonly pipelineLayout: GPUPipelineLayout;
|
|
107
|
+
readonly pipelines: Map<string, GPURenderPipeline>;
|
|
108
|
+
readonly whiteTexture: GPUTexture;
|
|
109
|
+
readonly whiteBindGroup: GPUBindGroup;
|
|
110
|
+
readonly emptyCompositeBindGroup: GPUBindGroup;
|
|
111
|
+
readonly transparentTexture: GPUTexture;
|
|
112
|
+
readonly transparentBindGroup: GPUBindGroup;
|
|
113
|
+
destroy(): void;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getAnimation2DSharedGpu(
|
|
117
|
+
device: GPUDevice,
|
|
118
|
+
tracker: ExtensionGPUResourceTracker | undefined,
|
|
119
|
+
): Animation2DSharedGpu {
|
|
120
|
+
return getExtensionSharedRendererResource(
|
|
121
|
+
device,
|
|
122
|
+
'Animation2D.sharedGpu:v1',
|
|
123
|
+
() => createAnimation2DSharedGpu(device, tracker),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function createAnimation2DSharedGpu(
|
|
128
|
+
device: GPUDevice,
|
|
129
|
+
tracker: ExtensionGPUResourceTracker | undefined,
|
|
130
|
+
): Animation2DSharedGpu {
|
|
131
|
+
const cameraLayout = createCamera2DLayout(device);
|
|
132
|
+
const objectLayout = device.createBindGroupLayout({
|
|
133
|
+
entries: [{
|
|
134
|
+
binding: 0,
|
|
135
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
136
|
+
buffer: { type: 'uniform' },
|
|
137
|
+
}],
|
|
138
|
+
});
|
|
139
|
+
const textureLayout = device.createBindGroupLayout({ entries: [
|
|
140
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
|
|
141
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
|
|
142
|
+
] });
|
|
143
|
+
const compositeLayout = device.createBindGroupLayout({ entries: [
|
|
144
|
+
...Array.from({ length: MAX_COMPOSITE_LAYERS }, (_, binding) => ({
|
|
145
|
+
binding,
|
|
146
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
147
|
+
texture: { sampleType: 'float' as const },
|
|
148
|
+
})),
|
|
149
|
+
{ binding: MAX_COMPOSITE_LAYERS, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
|
|
150
|
+
] });
|
|
151
|
+
const sampler = device.createSampler({ magFilter: 'linear', minFilter: 'linear' });
|
|
152
|
+
const shader = device.createShaderModule({ label: 'Animation2D.shader', code: SHADER });
|
|
153
|
+
const pipelineLayout = device.createPipelineLayout({
|
|
154
|
+
bindGroupLayouts: [cameraLayout, objectLayout, textureLayout, compositeLayout],
|
|
155
|
+
});
|
|
156
|
+
const createTextureBindGroup = (texture: GPUTexture): GPUBindGroup => device.createBindGroup({
|
|
157
|
+
layout: textureLayout,
|
|
158
|
+
entries: [
|
|
159
|
+
{ binding: 0, resource: texture.createView() },
|
|
160
|
+
{ binding: 1, resource: sampler },
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
const whiteTexture = device.createTexture({
|
|
164
|
+
size: [1, 1],
|
|
165
|
+
format: 'rgba8unorm',
|
|
166
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
167
|
+
});
|
|
168
|
+
tracker?.trackTexture(whiteTexture, 'Animation2D.sharedWhiteTexture', 4);
|
|
169
|
+
device.queue.writeTexture(
|
|
170
|
+
{ texture: whiteTexture },
|
|
171
|
+
new Uint8Array([255, 255, 255, 255]),
|
|
172
|
+
{ bytesPerRow: 4 },
|
|
173
|
+
[1, 1],
|
|
174
|
+
);
|
|
175
|
+
const whiteBindGroup = createTextureBindGroup(whiteTexture);
|
|
176
|
+
const emptyCompositeBindGroup = device.createBindGroup({
|
|
177
|
+
layout: compositeLayout,
|
|
178
|
+
entries: [
|
|
179
|
+
...Array.from({ length: MAX_COMPOSITE_LAYERS }, (_, binding) => ({
|
|
180
|
+
binding,
|
|
181
|
+
resource: whiteTexture.createView(),
|
|
182
|
+
})),
|
|
183
|
+
{ binding: MAX_COMPOSITE_LAYERS, resource: sampler },
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
const transparentTexture = device.createTexture({
|
|
187
|
+
size: [1, 1],
|
|
188
|
+
format: 'rgba8unorm',
|
|
189
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
190
|
+
});
|
|
191
|
+
tracker?.trackTexture(transparentTexture, 'Animation2D.sharedTransparentTexture', 4);
|
|
192
|
+
device.queue.writeTexture(
|
|
193
|
+
{ texture: transparentTexture },
|
|
194
|
+
new Uint8Array([255, 255, 255, 0]),
|
|
195
|
+
{ bytesPerRow: 4 },
|
|
196
|
+
[1, 1],
|
|
197
|
+
);
|
|
198
|
+
const transparentBindGroup = createTextureBindGroup(transparentTexture);
|
|
199
|
+
return {
|
|
200
|
+
cameraLayout,
|
|
201
|
+
objectLayout,
|
|
202
|
+
textureLayout,
|
|
203
|
+
compositeLayout,
|
|
204
|
+
sampler,
|
|
205
|
+
shader,
|
|
206
|
+
pipelineLayout,
|
|
207
|
+
pipelines: new Map(),
|
|
208
|
+
whiteTexture,
|
|
209
|
+
whiteBindGroup,
|
|
210
|
+
emptyCompositeBindGroup,
|
|
211
|
+
transparentTexture,
|
|
212
|
+
transparentBindGroup,
|
|
213
|
+
destroy(): void {
|
|
214
|
+
tracker?.untrackTexture(whiteTexture);
|
|
215
|
+
tracker?.untrackTexture(transparentTexture);
|
|
216
|
+
whiteTexture.destroy();
|
|
217
|
+
transparentTexture.destroy();
|
|
218
|
+
this.pipelines.clear();
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export class Animation2DRenderSystem extends RenderSystem2DBase {
|
|
224
|
+
private readonly maxMaskTargets: number;
|
|
225
|
+
private rendererReady = false;
|
|
226
|
+
private cameraGpu!: Camera2DGpu;
|
|
227
|
+
private objectLayout!: GPUBindGroupLayout;
|
|
228
|
+
private textureLayout!: GPUBindGroupLayout;
|
|
229
|
+
private compositeLayout!: GPUBindGroupLayout;
|
|
230
|
+
private sampler!: GPUSampler;
|
|
231
|
+
private whiteTexture!: GPUTexture;
|
|
232
|
+
private whiteBindGroup!: GPUBindGroup;
|
|
233
|
+
private emptyCompositeBindGroup!: GPUBindGroup;
|
|
234
|
+
private transparentTexture!: GPUTexture;
|
|
235
|
+
private transparentBindGroup!: GPUBindGroup;
|
|
236
|
+
private shader!: GPUShaderModule;
|
|
237
|
+
private pipelineLayout!: GPUPipelineLayout;
|
|
238
|
+
private pipelines!: Map<string, GPURenderPipeline>;
|
|
239
|
+
private readonly entityGpu = new Map<number, EntityGpu>();
|
|
240
|
+
private readonly geometryGpu = new Map<number, GeometryGpu>();
|
|
241
|
+
private textureBindGroups = new WeakMap<GPUTexture, GPUBindGroup>();
|
|
242
|
+
private readonly maskTargets = new Map<string, MaskTarget>();
|
|
243
|
+
private readonly effectTargets = new Map<string, EffectTarget>();
|
|
244
|
+
private readonly items: AnimationRenderItem[] = [];
|
|
245
|
+
private readonly sourceItems = new Map<string, AnimationRenderItem[]>();
|
|
246
|
+
private readonly sourceGroupPool: AnimationRenderItem[][] = [];
|
|
247
|
+
private readonly activeMaskTargets = new Map<string, MaskTarget>();
|
|
248
|
+
private readonly compositeBindGroups = new Map<string, GPUBindGroup>();
|
|
249
|
+
private readonly liveGeometryIds = new Set<number>();
|
|
250
|
+
private _visualCount = 0;
|
|
251
|
+
private _maskTargetCount = 0;
|
|
252
|
+
private _compositeLayerCount = 0;
|
|
253
|
+
private _droppedCompositeCount = 0;
|
|
254
|
+
private _maskPixels = 0;
|
|
255
|
+
private _effectTargetCount = 0;
|
|
256
|
+
private _droppedEffectCount = 0;
|
|
257
|
+
private _effectPixels = 0;
|
|
258
|
+
|
|
259
|
+
constructor(engine: IEngine, cameraEntity: Entity, options: Animation2DRenderSystemOptions = {}) {
|
|
260
|
+
// Optional extension subpaths can be bundled as independent module graphs.
|
|
261
|
+
// Query by the stable global component symbol so a visual created by the
|
|
262
|
+
// HYA state-machine subpath is visible to the animation renderer subpath.
|
|
263
|
+
super({ all: [AnimationVisual2D.UniqueSymbol] }, engine, cameraEntity, { ...options, loadOp: options.loadOp ?? 'load' }, 'Animation2DRenderSystem');
|
|
264
|
+
this.maxMaskTargets = positiveInteger(options.maxMaskTargets ?? 32, 'maxMaskTargets');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
get stats(): Animation2DRenderStats {
|
|
268
|
+
return Object.freeze({
|
|
269
|
+
visualCount: this._visualCount,
|
|
270
|
+
maskTargetCount: this._maskTargetCount,
|
|
271
|
+
compositeLayerCount: this._compositeLayerCount,
|
|
272
|
+
droppedCompositeCount: this._droppedCompositeCount,
|
|
273
|
+
maskPixels: this._maskPixels,
|
|
274
|
+
effectTargetCount: this._effectTargetCount,
|
|
275
|
+
droppedEffectCount: this._droppedEffectCount,
|
|
276
|
+
effectPixels: this._effectPixels,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
get renderPipelineOptions() {
|
|
281
|
+
return { pass: 'isolated' as const, loadOp: this.loadOp, sort: this.priority };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
record(world: World, context: RenderCommandContext): this {
|
|
285
|
+
if (this.disabled) return this;
|
|
286
|
+
if (!this.rendererReady) this.prepare();
|
|
287
|
+
if (!this.writeCameraBuffer(context.device.queue, this.cameraGpu.buffer, context)) return this;
|
|
288
|
+
const liveEntities = this.beginLiveEntityTracking();
|
|
289
|
+
this.items.length = 0;
|
|
290
|
+
const entities = this.entitySet.get(world);
|
|
291
|
+
if (entities) for (const entity of entities) {
|
|
292
|
+
if (!this.isEntityRenderable(entity)) continue;
|
|
293
|
+
const visual = entity.getComponent(AnimationVisual2D.UniqueSymbol) as AnimationVisual2D | null;
|
|
294
|
+
if (!visual) continue;
|
|
295
|
+
this.items.push({ entity, visual });
|
|
296
|
+
this.markEntityLive(entity);
|
|
297
|
+
}
|
|
298
|
+
this.items.sort(compareItems);
|
|
299
|
+
|
|
300
|
+
const frame = context.frameData?.frameId ?? 0;
|
|
301
|
+
this.sourceItems.clear();
|
|
302
|
+
let sourceGroupCount = 0;
|
|
303
|
+
for (const item of this.items) {
|
|
304
|
+
if (!item.visual.sourceOnly) continue;
|
|
305
|
+
const key = visualNodeKey(item.visual);
|
|
306
|
+
let group = this.sourceItems.get(key);
|
|
307
|
+
if (!group) {
|
|
308
|
+
group = this.sourceGroupPool[sourceGroupCount] ?? [];
|
|
309
|
+
group.length = 0;
|
|
310
|
+
this.sourceGroupPool[sourceGroupCount++] = group;
|
|
311
|
+
this.sourceItems.set(key, group);
|
|
312
|
+
}
|
|
313
|
+
group.push(item);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const maskTargets = this.activeMaskTargets;
|
|
317
|
+
maskTargets.clear();
|
|
318
|
+
let maskPixels = 0;
|
|
319
|
+
let maskCount = 0;
|
|
320
|
+
let droppedCompositeCount = 0;
|
|
321
|
+
let compositeLayerCount = 0;
|
|
322
|
+
let effectCount = 0;
|
|
323
|
+
let droppedEffectCount = 0;
|
|
324
|
+
let effectPixels = 0;
|
|
325
|
+
for (const sourceKey of orderSourceGroups(this.sourceItems)) {
|
|
326
|
+
const sources = this.sourceItems.get(sourceKey)!;
|
|
327
|
+
if (maskCount >= this.maxMaskTargets) break;
|
|
328
|
+
const targetKey = `${context.view?.key ?? 'default'}:${sourceKey}`;
|
|
329
|
+
const target = this.getMaskTarget(targetKey, context, frame);
|
|
330
|
+
maskPixels += target.width * target.height;
|
|
331
|
+
maskCount++;
|
|
332
|
+
let initialized = false;
|
|
333
|
+
for (const source of sources) {
|
|
334
|
+
const resolved = this.resolveCompositeBindGroup(source.visual, maskTargets, context.view?.key ?? 'default');
|
|
335
|
+
if (!resolved) { droppedCompositeCount++; continue; }
|
|
336
|
+
compositeLayerCount += source.visual.compositeLayers.length;
|
|
337
|
+
const effectTarget = source.visual.effects.length > 0 && !context.passEncoder
|
|
338
|
+
? this.renderEffectStack(source, context, resolved, frame)
|
|
339
|
+
: null;
|
|
340
|
+
if (effectTarget) {
|
|
341
|
+
effectCount++;
|
|
342
|
+
if (effectPixels === 0) effectPixels = effectTarget.width * effectTarget.height * 2;
|
|
343
|
+
} else if (source.visual.effects.length > 0) droppedEffectCount++;
|
|
344
|
+
const pass = this.beginMaskSegment(context, target, sourceKey, initialized);
|
|
345
|
+
if (effectTarget) {
|
|
346
|
+
pass.setPipeline(this.getPresentPipeline('rgba8unorm', 1, false, false));
|
|
347
|
+
this.drawEffectResult(pass, source.entity, effectTarget.texture);
|
|
348
|
+
} else {
|
|
349
|
+
pass.setPipeline(this.getPipeline('rgba8unorm', 1, false, false));
|
|
350
|
+
this.draw(pass, source.entity, source.visual, context, resolved, true);
|
|
351
|
+
}
|
|
352
|
+
pass.end();
|
|
353
|
+
initialized = true;
|
|
354
|
+
}
|
|
355
|
+
if (!initialized) this.beginMaskSegment(context, target, sourceKey, false).end();
|
|
356
|
+
maskTargets.set(sourceKey, target);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const pending: Array<{ item: AnimationRenderItem; composite: GPUBindGroup }> = [];
|
|
360
|
+
const outputFormat = context.view?.target.format ?? this.engine.format;
|
|
361
|
+
let firstOutputSegment = true;
|
|
362
|
+
let visualCount = 0;
|
|
363
|
+
const flushOutput = (effectItem?: AnimationRenderItem, effectTexture?: GPUTexture): void => {
|
|
364
|
+
if (pending.length === 0 && !effectItem && !firstOutputSegment) return;
|
|
365
|
+
const { passEncoder, ownsPass } = this.beginOutputSegment(context, firstOutputSegment);
|
|
366
|
+
firstOutputSegment = false;
|
|
367
|
+
applyViewport(passEncoder, context);
|
|
368
|
+
passEncoder.setBindGroup(0, this.cameraGpu.bindGroup);
|
|
369
|
+
for (const entry of pending) {
|
|
370
|
+
passEncoder.setPipeline(this.getPipeline(
|
|
371
|
+
outputFormat, context.view?.sampleCount ?? this.engine.msaaSamples, true, context.view?.reverseZ ?? this.engine.reverseZ,
|
|
372
|
+
));
|
|
373
|
+
this.draw(passEncoder, entry.item.entity, entry.item.visual, context, entry.composite, false);
|
|
374
|
+
visualCount++;
|
|
375
|
+
}
|
|
376
|
+
pending.length = 0;
|
|
377
|
+
if (effectItem && effectTexture) {
|
|
378
|
+
passEncoder.setPipeline(this.getPresentPipeline(
|
|
379
|
+
outputFormat, context.view?.sampleCount ?? this.engine.msaaSamples, true, context.view?.reverseZ ?? this.engine.reverseZ,
|
|
380
|
+
));
|
|
381
|
+
this.drawEffectResult(passEncoder, effectItem.entity, effectTexture);
|
|
382
|
+
visualCount++;
|
|
383
|
+
}
|
|
384
|
+
if (ownsPass) passEncoder.end();
|
|
385
|
+
};
|
|
386
|
+
for (const item of this.items) {
|
|
387
|
+
const { visual } = item;
|
|
388
|
+
if (visual.sourceOnly) continue;
|
|
389
|
+
const resolved = this.resolveCompositeBindGroup(visual, maskTargets, context.view?.key ?? 'default');
|
|
390
|
+
if (!resolved) { droppedCompositeCount++; continue; }
|
|
391
|
+
compositeLayerCount += visual.compositeLayers.length;
|
|
392
|
+
if (visual.effects.length > 0 && !context.passEncoder) {
|
|
393
|
+
const effectTarget = this.renderEffectStack(item, context, resolved, frame);
|
|
394
|
+
effectCount++;
|
|
395
|
+
if (effectPixels === 0) effectPixels = effectTarget.width * effectTarget.height * 2;
|
|
396
|
+
flushOutput(item, effectTarget.texture);
|
|
397
|
+
} else {
|
|
398
|
+
if (visual.effects.length > 0) droppedEffectCount++;
|
|
399
|
+
pending.push({ item, composite: resolved });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
flushOutput();
|
|
403
|
+
|
|
404
|
+
this.releaseEntityGpuEntriesNotIn(this.entityGpu, gpu => this.destroyEntityGpu(gpu), liveEntities);
|
|
405
|
+
this.sweepGeometryGpu();
|
|
406
|
+
this.sweepMaskTargets(frame, context);
|
|
407
|
+
this.sweepEffectTargets(frame, context);
|
|
408
|
+
this._visualCount = visualCount;
|
|
409
|
+
this._maskTargetCount = maskCount;
|
|
410
|
+
this._compositeLayerCount = compositeLayerCount;
|
|
411
|
+
this._droppedCompositeCount = droppedCompositeCount;
|
|
412
|
+
this._maskPixels = maskPixels;
|
|
413
|
+
this._effectTargetCount = effectCount > 0 ? 1 : 0;
|
|
414
|
+
this._droppedEffectCount = droppedEffectCount;
|
|
415
|
+
this._effectPixels = effectPixels;
|
|
416
|
+
return this;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
override destroy(): this {
|
|
420
|
+
this.releaseGpuResourcesForRecovery();
|
|
421
|
+
return super.destroy();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
protected releaseGpuResourcesForRecovery(): void {
|
|
425
|
+
if (!this.rendererReady) return;
|
|
426
|
+
destroyCamera2DGpu(this.cameraGpu);
|
|
427
|
+
this.destroyEntityGpuEntries(this.entityGpu, gpu => this.destroyEntityGpu(gpu));
|
|
428
|
+
for (const gpu of this.geometryGpu.values()) this.destroyGeometryGpu(gpu);
|
|
429
|
+
this.geometryGpu.clear();
|
|
430
|
+
for (const target of this.maskTargets.values()) this.destroyMaskTarget(target);
|
|
431
|
+
this.maskTargets.clear();
|
|
432
|
+
for (const target of this.effectTargets.values()) this.destroyEffectTarget(target);
|
|
433
|
+
this.effectTargets.clear();
|
|
434
|
+
this.activeMaskTargets.clear();
|
|
435
|
+
this.compositeBindGroups.clear();
|
|
436
|
+
this.textureBindGroups = new WeakMap();
|
|
437
|
+
this.rendererReady = false;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private prepare(): void {
|
|
441
|
+
const device = requireEngineDevice(this.engine);
|
|
442
|
+
const tracker = getExtensionGPUResourceTracker(this.engine);
|
|
443
|
+
const shared = getAnimation2DSharedGpu(device, tracker);
|
|
444
|
+
this.cameraGpu = createCamera2DGpu(device, tracker, shared.cameraLayout);
|
|
445
|
+
this.objectLayout = shared.objectLayout;
|
|
446
|
+
this.textureLayout = shared.textureLayout;
|
|
447
|
+
this.compositeLayout = shared.compositeLayout;
|
|
448
|
+
this.sampler = shared.sampler;
|
|
449
|
+
this.whiteTexture = shared.whiteTexture;
|
|
450
|
+
this.whiteBindGroup = shared.whiteBindGroup;
|
|
451
|
+
this.emptyCompositeBindGroup = shared.emptyCompositeBindGroup;
|
|
452
|
+
this.transparentTexture = shared.transparentTexture;
|
|
453
|
+
this.transparentBindGroup = shared.transparentBindGroup;
|
|
454
|
+
this.shader = shared.shader;
|
|
455
|
+
this.pipelineLayout = shared.pipelineLayout;
|
|
456
|
+
this.pipelines = shared.pipelines;
|
|
457
|
+
this.rendererReady = true;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private beginMaskSegment(
|
|
461
|
+
context: RenderCommandContext,
|
|
462
|
+
target: MaskTarget,
|
|
463
|
+
sourceKey: string,
|
|
464
|
+
initialized: boolean,
|
|
465
|
+
): GPURenderPassEncoder {
|
|
466
|
+
const pass = context.encoder.beginRenderPass({
|
|
467
|
+
label: `Animation2D.mask:${sourceKey}`,
|
|
468
|
+
colorAttachments: [{
|
|
469
|
+
view: target.view,
|
|
470
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
471
|
+
loadOp: initialized ? 'load' : 'clear',
|
|
472
|
+
storeOp: 'store',
|
|
473
|
+
}],
|
|
474
|
+
});
|
|
475
|
+
applyViewport(pass, context);
|
|
476
|
+
pass.setBindGroup(0, this.cameraGpu.bindGroup);
|
|
477
|
+
return pass;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private beginOutputSegment(
|
|
481
|
+
context: RenderCommandContext,
|
|
482
|
+
first: boolean,
|
|
483
|
+
): { passEncoder: GPURenderPassEncoder; ownsPass: boolean } {
|
|
484
|
+
if (first) return beginRenderCommandPass(context);
|
|
485
|
+
if (!context.descriptor) throw new Error('Animation2D ordered effects require an isolated render-pass descriptor.');
|
|
486
|
+
return { passEncoder: context.encoder.beginRenderPass(cloneRenderPassDescriptor(context.descriptor, 'load')), ownsPass: true };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private getPipeline(format: GPUTextureFormat, sampleCount: 1 | 4, withDepth: boolean, reverseZ: boolean): GPURenderPipeline {
|
|
490
|
+
const depthFormat = withDepth ? this.engine.getDepthFormat(reverseZ) : 'none';
|
|
491
|
+
const key = `visual:${format}:${sampleCount}:${depthFormat}:${reverseZ ? 1 : 0}`;
|
|
492
|
+
const cached = this.pipelines.get(key);
|
|
493
|
+
if (cached) return cached;
|
|
494
|
+
const pipeline = requireEngineDevice(this.engine).createRenderPipeline({
|
|
495
|
+
label: `Animation2D.pipeline:${key}`,
|
|
496
|
+
layout: this.pipelineLayout,
|
|
497
|
+
vertex: { module: this.shader, entryPoint: 'vs_main', buffers: [{
|
|
498
|
+
arrayStride: 16,
|
|
499
|
+
attributes: [
|
|
500
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x2' },
|
|
501
|
+
{ shaderLocation: 1, offset: 8, format: 'float32x2' },
|
|
502
|
+
],
|
|
503
|
+
}] },
|
|
504
|
+
fragment: { module: this.shader, entryPoint: 'fs_main', targets: [{ format, blend: {
|
|
505
|
+
color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' },
|
|
506
|
+
alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
|
|
507
|
+
} }] },
|
|
508
|
+
primitive: { topology: 'triangle-list', cullMode: 'none' },
|
|
509
|
+
...(withDepth ? { depthStencil: {
|
|
510
|
+
format: depthFormat as GPUTextureFormat,
|
|
511
|
+
depthWriteEnabled: false,
|
|
512
|
+
depthCompare: 'always' as const,
|
|
513
|
+
} } : {}),
|
|
514
|
+
multisample: { count: sampleCount },
|
|
515
|
+
});
|
|
516
|
+
this.pipelines.set(key, pipeline);
|
|
517
|
+
return pipeline;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private getEffectPipeline(): GPURenderPipeline {
|
|
521
|
+
const key = 'effect:rgba8unorm';
|
|
522
|
+
const cached = this.pipelines.get(key);
|
|
523
|
+
if (cached) return cached;
|
|
524
|
+
const pipeline = requireEngineDevice(this.engine).createRenderPipeline({
|
|
525
|
+
label: 'Animation2D.effectPipeline',
|
|
526
|
+
layout: this.pipelineLayout,
|
|
527
|
+
vertex: { module: this.shader, entryPoint: 'vs_effect' },
|
|
528
|
+
fragment: { module: this.shader, entryPoint: 'fs_effect', targets: [{ format: 'rgba8unorm' }] },
|
|
529
|
+
primitive: { topology: 'triangle-list', cullMode: 'none' },
|
|
530
|
+
});
|
|
531
|
+
this.pipelines.set(key, pipeline);
|
|
532
|
+
return pipeline;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private getPresentPipeline(format: GPUTextureFormat, sampleCount: 1 | 4, withDepth: boolean, reverseZ: boolean): GPURenderPipeline {
|
|
536
|
+
const depthFormat = withDepth ? this.engine.getDepthFormat(reverseZ) : 'none';
|
|
537
|
+
const key = `present:${format}:${sampleCount}:${depthFormat}:${reverseZ ? 1 : 0}`;
|
|
538
|
+
const cached = this.pipelines.get(key);
|
|
539
|
+
if (cached) return cached;
|
|
540
|
+
const pipeline = requireEngineDevice(this.engine).createRenderPipeline({
|
|
541
|
+
label: `Animation2D.presentPipeline:${key}`,
|
|
542
|
+
layout: this.pipelineLayout,
|
|
543
|
+
vertex: { module: this.shader, entryPoint: 'vs_effect' },
|
|
544
|
+
fragment: { module: this.shader, entryPoint: 'fs_present', targets: [{ format, blend: {
|
|
545
|
+
color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
|
|
546
|
+
alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
|
|
547
|
+
} }] },
|
|
548
|
+
primitive: { topology: 'triangle-list', cullMode: 'none' },
|
|
549
|
+
...(withDepth ? { depthStencil: {
|
|
550
|
+
format: depthFormat as GPUTextureFormat, depthWriteEnabled: false, depthCompare: 'always' as const,
|
|
551
|
+
} } : {}),
|
|
552
|
+
multisample: { count: sampleCount },
|
|
553
|
+
});
|
|
554
|
+
this.pipelines.set(key, pipeline);
|
|
555
|
+
return pipeline;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private draw(
|
|
559
|
+
pass: GPURenderPassEncoder,
|
|
560
|
+
entity: Entity,
|
|
561
|
+
visual: AnimationVisual2D,
|
|
562
|
+
context: RenderCommandContext,
|
|
563
|
+
compositeBindGroup: GPUBindGroup,
|
|
564
|
+
outputMask: boolean,
|
|
565
|
+
): void {
|
|
566
|
+
const device = requireEngineDevice(this.engine);
|
|
567
|
+
const object = this.getEntityGpu(entity);
|
|
568
|
+
object.data.set(this.getWorldMatrix2D(entity, context), 0);
|
|
569
|
+
object.data.set(visual.color, 16);
|
|
570
|
+
object.data[20] = visual.compositeLayers.length;
|
|
571
|
+
object.data[21] = outputMask ? 1 : 0;
|
|
572
|
+
object.data[22] = Math.min(MAX_EFFECTS, visual.effects.length);
|
|
573
|
+
object.data[23] = 0;
|
|
574
|
+
object.data.set(visual.uvRect, 24);
|
|
575
|
+
object.data.fill(0, COMPOSITE_PARAMS_OFFSET);
|
|
576
|
+
for (let index = 0; index < visual.compositeLayers.length; index++) {
|
|
577
|
+
const layer = visual.compositeLayers[index]!;
|
|
578
|
+
const offset = COMPOSITE_PARAMS_OFFSET + index * 4;
|
|
579
|
+
object.data[offset] = compositeModeCode(layer.mode);
|
|
580
|
+
object.data[offset + 1] = compositeOperationCode(layer.operation ?? 'add');
|
|
581
|
+
object.data[offset + 2] = layer.feather?.[0] ?? 0;
|
|
582
|
+
object.data[offset + 3] = layer.feather?.[1] ?? 0;
|
|
583
|
+
object.data[COMPOSITE_EXPANSION_OFFSET + index] = layer.expansion ?? 0;
|
|
584
|
+
}
|
|
585
|
+
const gradient = visual.gradient;
|
|
586
|
+
if (gradient) {
|
|
587
|
+
const stopCount = Math.min(8, gradient.stops.length / 5);
|
|
588
|
+
object.data[GRADIENT_PARAMS_OFFSET] = gradient.kind === 'linear' ? 1 : 2;
|
|
589
|
+
object.data[GRADIENT_PARAMS_OFFSET + 1] = stopCount;
|
|
590
|
+
object.data[GRADIENT_PARAMS_OFFSET + 2] = gradient.opacity;
|
|
591
|
+
object.data.set(gradient.start, GRADIENT_GEOMETRY_OFFSET);
|
|
592
|
+
object.data.set(gradient.end, GRADIENT_GEOMETRY_OFFSET + 2);
|
|
593
|
+
for (let index = 0; index < stopCount; index++) {
|
|
594
|
+
const source = index * 5;
|
|
595
|
+
const colorOffset = GRADIENT_COLORS_OFFSET + index * 4;
|
|
596
|
+
object.data[colorOffset] = gradient.stops[source + 1] ?? 1;
|
|
597
|
+
object.data[colorOffset + 1] = gradient.stops[source + 2] ?? 1;
|
|
598
|
+
object.data[colorOffset + 2] = gradient.stops[source + 3] ?? 1;
|
|
599
|
+
object.data[colorOffset + 3] = gradient.stops[source + 4] ?? 1;
|
|
600
|
+
object.data[GRADIENT_OFFSETS_OFFSET + index] = gradient.stops[source] ?? 0;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
for (let index = 0; index < Math.min(MAX_EFFECTS, visual.effects.length); index++) {
|
|
604
|
+
const effect = visual.effects[index]!;
|
|
605
|
+
object.data[EFFECT_KINDS_OFFSET + index] = effectKindCode(effect.kind);
|
|
606
|
+
object.data.set(effect.values.subarray(0, EFFECT_FLOATS), EFFECT_DATA_OFFSET + index * EFFECT_FLOATS);
|
|
607
|
+
}
|
|
608
|
+
device.queue.writeBuffer(object.buffer, 0, object.data as ArrayBufferView<ArrayBuffer>);
|
|
609
|
+
const geometry = this.getGeometryGpu(visual);
|
|
610
|
+
pass.setBindGroup(1, object.bindGroup);
|
|
611
|
+
pass.setBindGroup(2, this.getVisualTextureBindGroup(entity, visual, object, context));
|
|
612
|
+
pass.setBindGroup(3, compositeBindGroup);
|
|
613
|
+
pass.setVertexBuffer(0, geometry.vertexBuffer);
|
|
614
|
+
if (geometry.indexBuffer && geometry.indexCount > 0) {
|
|
615
|
+
pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
|
|
616
|
+
pass.drawIndexed(geometry.indexCount);
|
|
617
|
+
} else {
|
|
618
|
+
pass.draw(geometry.vertexCount);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private renderEffectStack(
|
|
623
|
+
item: AnimationRenderItem,
|
|
624
|
+
context: RenderCommandContext,
|
|
625
|
+
compositeBindGroup: GPUBindGroup,
|
|
626
|
+
frame: number,
|
|
627
|
+
): { texture: GPUTexture; width: number; height: number } {
|
|
628
|
+
// One reusable ping-pong pair per view. Command ordering presents each result
|
|
629
|
+
// before the next visual overwrites it, so memory is O(view), not O(entities).
|
|
630
|
+
const key = context.view?.key ?? 'default';
|
|
631
|
+
const target = this.getEffectTarget(key, context, frame);
|
|
632
|
+
const sourcePass = context.encoder.beginRenderPass({
|
|
633
|
+
label: `Animation2D.effectSource:${item.entity.id}`,
|
|
634
|
+
colorAttachments: [{
|
|
635
|
+
view: target.views[0], clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: 'clear', storeOp: 'store',
|
|
636
|
+
}],
|
|
637
|
+
});
|
|
638
|
+
applyViewport(sourcePass, context);
|
|
639
|
+
sourcePass.setPipeline(this.getPipeline('rgba8unorm', 1, false, false));
|
|
640
|
+
sourcePass.setBindGroup(0, this.cameraGpu.bindGroup);
|
|
641
|
+
this.draw(sourcePass, item.entity, item.visual, context, compositeBindGroup, false);
|
|
642
|
+
sourcePass.end();
|
|
643
|
+
|
|
644
|
+
const object = this.getEntityGpu(item.entity);
|
|
645
|
+
let sourceIndex: 0 | 1 = 0;
|
|
646
|
+
for (let index = 0; index < Math.min(MAX_EFFECTS, item.visual.effects.length); index++) {
|
|
647
|
+
const outputIndex: 0 | 1 = sourceIndex === 0 ? 1 : 0;
|
|
648
|
+
const pass = context.encoder.beginRenderPass({
|
|
649
|
+
label: `Animation2D.effect:${item.entity.id}:${index}`,
|
|
650
|
+
colorAttachments: [{
|
|
651
|
+
view: target.views[outputIndex], clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: 'clear', storeOp: 'store',
|
|
652
|
+
}],
|
|
653
|
+
});
|
|
654
|
+
applyViewport(pass, context);
|
|
655
|
+
pass.setPipeline(this.getEffectPipeline());
|
|
656
|
+
pass.setBindGroup(0, this.cameraGpu.bindGroup);
|
|
657
|
+
pass.setBindGroup(1, object.bindGroup);
|
|
658
|
+
pass.setBindGroup(2, this.getTextureBindGroup(target.textures[sourceIndex]));
|
|
659
|
+
pass.setBindGroup(3, this.emptyCompositeBindGroup);
|
|
660
|
+
pass.draw(3, 1, 0, index);
|
|
661
|
+
pass.end();
|
|
662
|
+
sourceIndex = outputIndex;
|
|
663
|
+
}
|
|
664
|
+
return { texture: target.textures[sourceIndex], width: target.width, height: target.height };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
private drawEffectResult(pass: GPURenderPassEncoder, entity: Entity, texture: GPUTexture): void {
|
|
668
|
+
pass.setBindGroup(1, this.getEntityGpu(entity).bindGroup);
|
|
669
|
+
pass.setBindGroup(2, this.getTextureBindGroup(texture));
|
|
670
|
+
pass.setBindGroup(3, this.emptyCompositeBindGroup);
|
|
671
|
+
pass.draw(3);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
private getEntityGpu(entity: Entity): EntityGpu {
|
|
675
|
+
return this.getOrCreateEntityGpu(this.entityGpu, entity, () => {
|
|
676
|
+
const device = requireEngineDevice(this.engine);
|
|
677
|
+
const buffer = device.createBuffer({ size: OBJECT_BYTES, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
678
|
+
getExtensionGPUResourceTracker(this.engine)?.trackBuffer(buffer, 'Animation2D.objectBuffer', OBJECT_BYTES);
|
|
679
|
+
return {
|
|
680
|
+
buffer,
|
|
681
|
+
bindGroup: device.createBindGroup({ layout: this.objectLayout, entries: [{ binding: 0, resource: { buffer } }] }),
|
|
682
|
+
data: new Float32Array(OBJECT_FLOATS),
|
|
683
|
+
externalTexture: null,
|
|
684
|
+
externalSource: null,
|
|
685
|
+
externalVersion: -1,
|
|
686
|
+
externalWidth: 0,
|
|
687
|
+
externalHeight: 0,
|
|
688
|
+
externalBindGroup: null,
|
|
689
|
+
};
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private getVisualTextureBindGroup(
|
|
694
|
+
entity: Entity,
|
|
695
|
+
visual: AnimationVisual2D,
|
|
696
|
+
object: EntityGpu,
|
|
697
|
+
context: RenderCommandContext,
|
|
698
|
+
): GPUBindGroup {
|
|
699
|
+
const texture = visual.resolveTexture();
|
|
700
|
+
if (texture) return this.getTextureBindGroup(texture);
|
|
701
|
+
const material = visual.textMaterial;
|
|
702
|
+
const source = material?.texture;
|
|
703
|
+
if (material && isExternalImageSource(source)) {
|
|
704
|
+
const size = externalImageSize(source);
|
|
705
|
+
if (!object.externalTexture || object.externalWidth !== size.width || object.externalHeight !== size.height) {
|
|
706
|
+
const retiredTexture = object.externalTexture;
|
|
707
|
+
if (retiredTexture) {
|
|
708
|
+
const retire = () => {
|
|
709
|
+
getExtensionGPUResourceTracker(this.engine)?.untrackTexture(retiredTexture);
|
|
710
|
+
retiredTexture.destroy();
|
|
711
|
+
};
|
|
712
|
+
if (context.afterSubmit) {
|
|
713
|
+
context.afterSubmit(queue => {
|
|
714
|
+
void queue.onSubmittedWorkDone()
|
|
715
|
+
.then(retire)
|
|
716
|
+
.catch(retire);
|
|
717
|
+
});
|
|
718
|
+
} else {
|
|
719
|
+
retire();
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
object.externalTexture = requireEngineDevice(this.engine).createTexture({
|
|
723
|
+
label: `Animation2D.text:${entity.id}`,
|
|
724
|
+
size: [size.width, size.height],
|
|
725
|
+
format: 'rgba8unorm',
|
|
726
|
+
// WebGPU copyExternalImageToTexture requires RENDER_ATTACHMENT in addition
|
|
727
|
+
// to COPY_DST for browser-backed image sources.
|
|
728
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
729
|
+
});
|
|
730
|
+
getExtensionGPUResourceTracker(this.engine)?.trackTexture(object.externalTexture, 'Animation2D.textTexture', size.width * size.height * 4);
|
|
731
|
+
object.externalBindGroup = this.createTextureBindGroup(object.externalTexture);
|
|
732
|
+
object.externalWidth = size.width;
|
|
733
|
+
object.externalHeight = size.height;
|
|
734
|
+
object.externalVersion = -1;
|
|
735
|
+
}
|
|
736
|
+
if (object.externalSource !== source || object.externalVersion !== material.textureVersion) {
|
|
737
|
+
requireEngineDevice(this.engine).queue.copyExternalImageToTexture({ source }, { texture: object.externalTexture! }, [size.width, size.height]);
|
|
738
|
+
object.externalSource = source;
|
|
739
|
+
object.externalVersion = material.textureVersion;
|
|
740
|
+
}
|
|
741
|
+
return object.externalBindGroup!;
|
|
742
|
+
}
|
|
743
|
+
return visual.requiresTexture ? this.transparentBindGroup : this.whiteBindGroup;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
private getGeometryGpu(visual: AnimationVisual2D): GeometryGpu {
|
|
747
|
+
const geometry = visual.geometry;
|
|
748
|
+
let gpu = this.geometryGpu.get(geometry.id);
|
|
749
|
+
if (gpu && gpu.version === geometry.version) return gpu;
|
|
750
|
+
if (gpu) this.destroyGeometryGpu(gpu);
|
|
751
|
+
const vertices = buildVertices(geometry.positions);
|
|
752
|
+
const device = requireEngineDevice(this.engine);
|
|
753
|
+
const vertexBuffer = device.createBuffer({ size: vertices.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
|
|
754
|
+
device.queue.writeBuffer(vertexBuffer, 0, vertices as ArrayBufferView<ArrayBuffer>);
|
|
755
|
+
let indexBuffer: GPUBuffer | null = null;
|
|
756
|
+
if (geometry.indices?.length) {
|
|
757
|
+
const allocationBytes = alignUp4(geometry.indices.byteLength);
|
|
758
|
+
indexBuffer = device.createBuffer({ size: allocationBytes, usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST });
|
|
759
|
+
writeBufferAligned(device.queue, indexBuffer, geometry.indices);
|
|
760
|
+
}
|
|
761
|
+
const tracker = getExtensionGPUResourceTracker(this.engine);
|
|
762
|
+
tracker?.trackBuffer(vertexBuffer, 'Animation2D.vertexBuffer', vertices.byteLength);
|
|
763
|
+
if (indexBuffer && geometry.indices) tracker?.trackBuffer(indexBuffer, 'Animation2D.indexBuffer', alignUp4(geometry.indices.byteLength));
|
|
764
|
+
gpu = {
|
|
765
|
+
vertexBuffer,
|
|
766
|
+
indexBuffer,
|
|
767
|
+
indexCount: geometry.indices?.length ?? 0,
|
|
768
|
+
vertexCount: geometry.vertexCount,
|
|
769
|
+
indexFormat: geometry.indices instanceof Uint32Array ? 'uint32' : 'uint16',
|
|
770
|
+
version: geometry.version,
|
|
771
|
+
};
|
|
772
|
+
this.geometryGpu.set(geometry.id, gpu);
|
|
773
|
+
return gpu;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
private getTextureBindGroup(texture: GPUTexture): GPUBindGroup {
|
|
777
|
+
let bindGroup = this.textureBindGroups.get(texture);
|
|
778
|
+
if (!bindGroup) {
|
|
779
|
+
bindGroup = this.createTextureBindGroup(texture);
|
|
780
|
+
this.textureBindGroups.set(texture, bindGroup);
|
|
781
|
+
}
|
|
782
|
+
return bindGroup;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
private createTextureBindGroup(texture: GPUTexture): GPUBindGroup {
|
|
786
|
+
return requireEngineDevice(this.engine).createBindGroup({ layout: this.textureLayout, entries: [
|
|
787
|
+
{ binding: 0, resource: texture.createView() },
|
|
788
|
+
{ binding: 1, resource: this.sampler },
|
|
789
|
+
] });
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
private resolveCompositeBindGroup(
|
|
793
|
+
visual: AnimationVisual2D,
|
|
794
|
+
targets: ReadonlyMap<string, MaskTarget>,
|
|
795
|
+
viewKey: string,
|
|
796
|
+
): GPUBindGroup | null {
|
|
797
|
+
if (visual.compositeLayers.length === 0) return this.emptyCompositeBindGroup;
|
|
798
|
+
const resolved: MaskTarget[] = [];
|
|
799
|
+
for (const key of visual.compositeKeys) {
|
|
800
|
+
const target = targets.get(key);
|
|
801
|
+
if (!target) return null;
|
|
802
|
+
resolved.push(target);
|
|
803
|
+
}
|
|
804
|
+
const key = `${viewKey}:${visual.compositeKeys.join('|')}`;
|
|
805
|
+
let bindGroup = this.compositeBindGroups.get(key);
|
|
806
|
+
if (!bindGroup) {
|
|
807
|
+
bindGroup = this.createCompositeBindGroup(resolved.map(target => target.texture));
|
|
808
|
+
this.compositeBindGroups.set(key, bindGroup);
|
|
809
|
+
}
|
|
810
|
+
return bindGroup;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
private createCompositeBindGroup(textures: readonly GPUTexture[]): GPUBindGroup {
|
|
814
|
+
return requireEngineDevice(this.engine).createBindGroup({
|
|
815
|
+
layout: this.compositeLayout,
|
|
816
|
+
entries: [
|
|
817
|
+
...Array.from({ length: MAX_COMPOSITE_LAYERS }, (_, binding) => ({
|
|
818
|
+
binding,
|
|
819
|
+
resource: (textures[binding] ?? this.whiteTexture).createView(),
|
|
820
|
+
})),
|
|
821
|
+
{ binding: MAX_COMPOSITE_LAYERS, resource: this.sampler },
|
|
822
|
+
],
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
private getMaskTarget(key: string, context: RenderCommandContext, frame: number): MaskTarget {
|
|
827
|
+
const width = context.view?.target.width ?? this.engine.width;
|
|
828
|
+
const height = context.view?.target.height ?? this.engine.height;
|
|
829
|
+
let target = this.maskTargets.get(key);
|
|
830
|
+
if (target && target.width === width && target.height === height) { target.lastFrame = frame; return target; }
|
|
831
|
+
if (target) {
|
|
832
|
+
this.maskTargets.delete(key);
|
|
833
|
+
this.compositeBindGroups.clear();
|
|
834
|
+
const retired = target;
|
|
835
|
+
context.afterSubmit?.(queue => void queue.onSubmittedWorkDone().then(() => this.destroyMaskTarget(retired)).catch(() => this.destroyMaskTarget(retired)));
|
|
836
|
+
if (!context.afterSubmit) this.destroyMaskTarget(retired);
|
|
837
|
+
}
|
|
838
|
+
const texture = requireEngineDevice(this.engine).createTexture({
|
|
839
|
+
label: `Animation2D.mask:${key}`,
|
|
840
|
+
size: [width, height],
|
|
841
|
+
format: 'rgba8unorm',
|
|
842
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
843
|
+
});
|
|
844
|
+
getExtensionGPUResourceTracker(this.engine)?.trackTexture(texture, 'Animation2D.maskTexture', estimateTextureBytes([width, height, 1], 'rgba8unorm'));
|
|
845
|
+
target = { texture, view: texture.createView(), width, height, lastFrame: frame };
|
|
846
|
+
this.maskTargets.set(key, target);
|
|
847
|
+
return target;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
private getEffectTarget(key: string, context: RenderCommandContext, frame: number): EffectTarget {
|
|
851
|
+
const width = context.view?.target.width ?? this.engine.width;
|
|
852
|
+
const height = context.view?.target.height ?? this.engine.height;
|
|
853
|
+
let target = this.effectTargets.get(key);
|
|
854
|
+
if (target && target.width === width && target.height === height) { target.lastFrame = frame; return target; }
|
|
855
|
+
if (target) {
|
|
856
|
+
this.effectTargets.delete(key);
|
|
857
|
+
const retired = target;
|
|
858
|
+
context.afterSubmit?.(queue => void queue.onSubmittedWorkDone().then(() => this.destroyEffectTarget(retired)).catch(() => this.destroyEffectTarget(retired)));
|
|
859
|
+
if (!context.afterSubmit) this.destroyEffectTarget(retired);
|
|
860
|
+
}
|
|
861
|
+
const device = requireEngineDevice(this.engine);
|
|
862
|
+
const makeTexture = (index: number): GPUTexture => device.createTexture({
|
|
863
|
+
label: `Animation2D.effect:${key}:${index}`,
|
|
864
|
+
size: [width, height],
|
|
865
|
+
format: 'rgba8unorm',
|
|
866
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
867
|
+
});
|
|
868
|
+
const textures = [makeTexture(0), makeTexture(1)] as const;
|
|
869
|
+
const bytes = estimateTextureBytes([width, height, 1], 'rgba8unorm');
|
|
870
|
+
const tracker = getExtensionGPUResourceTracker(this.engine);
|
|
871
|
+
tracker?.trackTexture(textures[0], 'Animation2D.effectTexture', bytes);
|
|
872
|
+
tracker?.trackTexture(textures[1], 'Animation2D.effectTexture', bytes);
|
|
873
|
+
target = { textures, views: [textures[0].createView(), textures[1].createView()], width, height, lastFrame: frame };
|
|
874
|
+
this.effectTargets.set(key, target);
|
|
875
|
+
return target;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
private sweepMaskTargets(frame: number, context: RenderCommandContext): void {
|
|
879
|
+
let swept = false;
|
|
880
|
+
for (const [key, target] of this.maskTargets) {
|
|
881
|
+
if (target.lastFrame >= frame - 1) continue;
|
|
882
|
+
this.maskTargets.delete(key);
|
|
883
|
+
swept = true;
|
|
884
|
+
context.afterSubmit?.(queue => void queue.onSubmittedWorkDone().then(() => this.destroyMaskTarget(target)).catch(() => this.destroyMaskTarget(target)));
|
|
885
|
+
if (!context.afterSubmit) this.destroyMaskTarget(target);
|
|
886
|
+
}
|
|
887
|
+
if (swept) this.compositeBindGroups.clear();
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
private sweepEffectTargets(frame: number, context: RenderCommandContext): void {
|
|
891
|
+
for (const [key, target] of this.effectTargets) {
|
|
892
|
+
if (target.lastFrame >= frame - 1) continue;
|
|
893
|
+
this.effectTargets.delete(key);
|
|
894
|
+
context.afterSubmit?.(queue => void queue.onSubmittedWorkDone().then(() => this.destroyEffectTarget(target)).catch(() => this.destroyEffectTarget(target)));
|
|
895
|
+
if (!context.afterSubmit) this.destroyEffectTarget(target);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
private sweepGeometryGpu(): void {
|
|
900
|
+
this.liveGeometryIds.clear();
|
|
901
|
+
for (const item of this.items) this.liveGeometryIds.add(item.visual.geometry.id);
|
|
902
|
+
for (const [id, gpu] of this.geometryGpu) if (!this.liveGeometryIds.has(id)) { this.destroyGeometryGpu(gpu); this.geometryGpu.delete(id); }
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
private destroyEntityGpu(gpu: EntityGpu): void {
|
|
906
|
+
getExtensionGPUResourceTracker(this.engine)?.untrackBuffer(gpu.buffer);
|
|
907
|
+
gpu.buffer.destroy();
|
|
908
|
+
if (gpu.externalTexture) getExtensionGPUResourceTracker(this.engine)?.untrackTexture(gpu.externalTexture);
|
|
909
|
+
gpu.externalTexture?.destroy();
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
private destroyGeometryGpu(gpu: GeometryGpu): void {
|
|
913
|
+
const tracker = getExtensionGPUResourceTracker(this.engine);
|
|
914
|
+
tracker?.untrackBuffer(gpu.vertexBuffer);
|
|
915
|
+
gpu.vertexBuffer.destroy();
|
|
916
|
+
if (gpu.indexBuffer) tracker?.untrackBuffer(gpu.indexBuffer);
|
|
917
|
+
gpu.indexBuffer?.destroy();
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
private destroyMaskTarget(target: MaskTarget): void {
|
|
921
|
+
getExtensionGPUResourceTracker(this.engine)?.untrackTexture(target.texture);
|
|
922
|
+
target.texture.destroy();
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
private destroyEffectTarget(target: EffectTarget): void {
|
|
926
|
+
const tracker = getExtensionGPUResourceTracker(this.engine);
|
|
927
|
+
for (const texture of target.textures) {
|
|
928
|
+
tracker?.untrackTexture(texture);
|
|
929
|
+
texture.destroy();
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function writeBufferAligned(
|
|
935
|
+
queue: GPUQueue,
|
|
936
|
+
buffer: GPUBuffer,
|
|
937
|
+
source: Uint16Array | Uint32Array,
|
|
938
|
+
): void {
|
|
939
|
+
if (source.byteLength % 4 === 0) {
|
|
940
|
+
queue.writeBuffer(buffer, 0, source as ArrayBufferView<ArrayBuffer>);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const padded = new Uint8Array(alignUp4(source.byteLength));
|
|
944
|
+
padded.set(new Uint8Array(source.buffer, source.byteOffset, source.byteLength));
|
|
945
|
+
queue.writeBuffer(buffer, 0, padded);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function isExternalImageSource(value: unknown): value is ImageBitmap | HTMLCanvasElement | HTMLImageElement {
|
|
949
|
+
if (!value || typeof value !== 'object') return false;
|
|
950
|
+
return (typeof ImageBitmap !== 'undefined' && value instanceof ImageBitmap)
|
|
951
|
+
|| (typeof HTMLCanvasElement !== 'undefined' && value instanceof HTMLCanvasElement)
|
|
952
|
+
|| (typeof HTMLImageElement !== 'undefined' && value instanceof HTMLImageElement);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function externalImageSize(source: ImageBitmap | HTMLCanvasElement | HTMLImageElement): { width: number; height: number } {
|
|
956
|
+
const isImage = typeof HTMLImageElement !== 'undefined' && source instanceof HTMLImageElement;
|
|
957
|
+
return {
|
|
958
|
+
width: Math.max(1, isImage ? source.naturalWidth || source.width : source.width),
|
|
959
|
+
height: Math.max(1, isImage ? source.naturalHeight || source.height : source.height),
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function compareItems(a: AnimationRenderItem, b: AnimationRenderItem): number {
|
|
964
|
+
return a.visual.instanceId - b.visual.instanceId || a.visual.order - b.visual.order || a.entity.id - b.entity.id;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function orderSourceGroups(groups: ReadonlyMap<string, readonly AnimationRenderItem[]>): string[] {
|
|
968
|
+
const ordered: string[] = [];
|
|
969
|
+
const states = new Map<string, 1 | 2>();
|
|
970
|
+
const visit = (key: string): void => {
|
|
971
|
+
if (states.get(key) === 2) return;
|
|
972
|
+
if (states.get(key) === 1) return;
|
|
973
|
+
states.set(key, 1);
|
|
974
|
+
for (const item of groups.get(key) ?? []) {
|
|
975
|
+
for (const dependency of item.visual.compositeKeys) if (groups.has(dependency)) visit(dependency);
|
|
976
|
+
}
|
|
977
|
+
states.set(key, 2);
|
|
978
|
+
ordered.push(key);
|
|
979
|
+
};
|
|
980
|
+
for (const key of groups.keys()) visit(key);
|
|
981
|
+
return ordered;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function compositeModeCode(mode: AnimationCompositeLayer['mode']): number {
|
|
985
|
+
return mode === 'alpha' ? 0 : mode === 'alpha-inverted' ? 1 : mode === 'luma' ? 2 : 3;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function compositeOperationCode(operation: NonNullable<AnimationCompositeLayer['operation']>): number {
|
|
989
|
+
return operation === 'add' ? 0 : operation === 'subtract' ? 1 : operation === 'intersect' ? 2 : 3;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function effectKindCode(kind: AnimationVisual2D['effects'][number]['kind']): number {
|
|
993
|
+
return kind === 'tint' ? 1 : kind === 'fill' ? 2 : kind === 'opacity' ? 3
|
|
994
|
+
: kind === 'color-matrix' ? 4 : kind === 'blur' ? 5 : 6;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function visualNodeKey(visual: AnimationVisual2D): string { return visual.nodeKey; }
|
|
998
|
+
|
|
999
|
+
function buildVertices(positions: Float32Array): Float32Array {
|
|
1000
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1001
|
+
for (let i = 0; i < positions.length; i += 2) {
|
|
1002
|
+
const x = positions[i]!, y = positions[i + 1]!;
|
|
1003
|
+
minX = Math.min(minX, x); maxX = Math.max(maxX, x);
|
|
1004
|
+
minY = Math.min(minY, y); maxY = Math.max(maxY, y);
|
|
1005
|
+
}
|
|
1006
|
+
const width = Math.max(maxX - minX, 1e-6), height = Math.max(maxY - minY, 1e-6);
|
|
1007
|
+
const out = new Float32Array(positions.length * 2);
|
|
1008
|
+
for (let source = 0, target = 0; source < positions.length; source += 2) {
|
|
1009
|
+
const x = positions[source]!, y = positions[source + 1]!;
|
|
1010
|
+
out[target++] = x; out[target++] = y;
|
|
1011
|
+
out[target++] = (x - minX) / width; out[target++] = 1 - (y - minY) / height;
|
|
1012
|
+
}
|
|
1013
|
+
return out;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function applyViewport(pass: GPURenderPassEncoder, context: RenderCommandContext): void {
|
|
1017
|
+
const viewport = context.view?.viewport;
|
|
1018
|
+
if (viewport) pass.setViewport(viewport.x, viewport.y, viewport.width, viewport.height, viewport.minDepth ?? 0, viewport.maxDepth ?? 1);
|
|
1019
|
+
const scissor = context.view?.scissor;
|
|
1020
|
+
if (scissor) pass.setScissorRect(scissor.x, scissor.y, scissor.width, scissor.height);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function positiveInteger(value: number, label: string): number {
|
|
1024
|
+
if (!Number.isSafeInteger(value) || value < 1) throw new RangeError(`${label} must be a positive safe integer.`);
|
|
1025
|
+
return value;
|
|
1026
|
+
}
|