@haiyue/extensions 0.1.0 → 0.2.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.
Files changed (269) hide show
  1. package/README.md +25 -0
  2. package/dist/animation/Animation2DComponent.d.ts +18 -0
  3. package/dist/animation/Animation2DExtensionRegistry.d.ts +7 -0
  4. package/dist/animation/Animation2DRenderSystem.d.ts +6 -0
  5. package/dist/animation/Animation2DRuntime.d.ts +6 -2
  6. package/dist/animation/AnimationTextRasterizer.d.ts +4 -0
  7. package/dist/animation/AnimationVisual2D.d.ts +58 -2
  8. package/dist/animation/interaction/InteractionRuntime.d.ts +83 -0
  9. package/dist/animation/interaction/runtime-types.d.ts +257 -0
  10. package/dist/animation.d.ts +3 -1
  11. package/dist/animation.js +551 -59
  12. package/dist/animation3d.js +1 -1
  13. package/dist/canvas-text.js +1 -1
  14. package/dist/chunks/2d-ui-animation-2d.generated-FpziAeGJ.js +4 -0
  15. package/dist/chunks/2d-ui-indexed-sprite.generated-DmKR0CC3.js +4 -0
  16. package/dist/chunks/Animation2DStateMachine-BHcA6Cu3.js +18212 -0
  17. package/dist/chunks/{AnimationStateMachineController-BIcAh-sT.js → AnimationStateMachineController-4sKaqh9E.js} +2 -2
  18. package/dist/chunks/AnimationVisual2D-8y3qp0_U.js +230 -0
  19. package/dist/chunks/{RenderSystem2DBase-DDT61dAu.js → RenderSystem2DBase-CF2s00vg.js} +2 -2
  20. package/dist/controls/VirtualJoystickControls.d.ts +46 -0
  21. package/dist/controls/contracts.d.ts +91 -0
  22. package/dist/controls.d.ts +2 -0
  23. package/dist/controls.js +358 -0
  24. package/dist/deformable-animation/runtime/DeformableMesh2DRuntimeExtension.d.ts +11 -0
  25. package/dist/deformable-animation/runtime/DeformableMesh2DSampler.d.ts +11 -0
  26. package/dist/deformable-animation.d.ts +5 -0
  27. package/dist/deformable-animation.js +316 -0
  28. package/dist/experimental-indexed-sprite.d.ts +5 -0
  29. package/dist/experimental-indexed-sprite.js +482 -0
  30. package/dist/gltf-worker-runtime.js +9 -9
  31. package/dist/hya-state-machine.js +3 -2
  32. package/dist/index.js +1 -1
  33. package/dist/indexed-sprite/AtlasLayout.d.ts +2 -0
  34. package/dist/indexed-sprite/IndexedSpriteRenderer.d.ts +23 -0
  35. package/dist/indexed-sprite/contracts.d.ts +89 -0
  36. package/dist/internal/2d-ui-shader-artifact.js +232 -23
  37. package/dist/ray-tracing/acceleration/bvh.d.ts +37 -0
  38. package/dist/ray-tracing/acceleration/index.d.ts +6 -0
  39. package/dist/ray-tracing/acceleration/pack.d.ts +152 -0
  40. package/dist/ray-tracing/acceleration/query.d.ts +4 -0
  41. package/dist/ray-tracing/acceleration/runtime.d.ts +30 -0
  42. package/dist/ray-tracing/acceleration/tlas.d.ts +4 -0
  43. package/dist/ray-tracing/acceleration/types.d.ts +165 -0
  44. package/dist/ray-tracing/denoise/index.d.ts +3 -0
  45. package/dist/ray-tracing/denoise/layout.d.ts +9 -0
  46. package/dist/ray-tracing/denoise/runtime.d.ts +23 -0
  47. package/dist/ray-tracing/denoise/types.d.ts +28 -0
  48. package/dist/ray-tracing/hybrid/contract.d.ts +2 -0
  49. package/dist/ray-tracing/hybrid/index.d.ts +5 -0
  50. package/dist/ray-tracing/hybrid/layout.d.ts +17 -0
  51. package/dist/ray-tracing/hybrid/plan.d.ts +2 -0
  52. package/dist/ray-tracing/hybrid/runtime.d.ts +31 -0
  53. package/dist/ray-tracing/hybrid/shaders/hybrid-artifact.generated.d.ts +625 -0
  54. package/dist/ray-tracing/hybrid/types.d.ts +144 -0
  55. package/dist/ray-tracing/lifecycle/device-recovery.d.ts +31 -0
  56. package/dist/ray-tracing/lifecycle/index.d.ts +2 -0
  57. package/dist/ray-tracing/lifecycle/types.d.ts +37 -0
  58. package/dist/ray-tracing/material/index.d.ts +2 -0
  59. package/dist/ray-tracing/material/pack.d.ts +5 -0
  60. package/dist/ray-tracing/material/types.d.ts +63 -0
  61. package/dist/ray-tracing/reference/index.d.ts +122 -0
  62. package/dist/ray-tracing/renderer/index.d.ts +7 -0
  63. package/dist/ray-tracing/renderer/layout.d.ts +20 -0
  64. package/dist/ray-tracing/renderer/plan.d.ts +2 -0
  65. package/dist/ray-tracing/renderer/reference.d.ts +17 -0
  66. package/dist/ray-tracing/renderer/runtime.d.ts +39 -0
  67. package/dist/ray-tracing/renderer/scene.d.ts +4 -0
  68. package/dist/ray-tracing/renderer/types.d.ts +115 -0
  69. package/dist/ray-tracing/sampling/index.d.ts +8 -0
  70. package/dist/ray-tracing/sampling/layout.d.ts +12 -0
  71. package/dist/ray-tracing/sampling/plan.d.ts +5 -0
  72. package/dist/ray-tracing/sampling/revision.d.ts +15 -0
  73. package/dist/ray-tracing/sampling/runtime.d.ts +44 -0
  74. package/dist/ray-tracing/sampling/sequence.d.ts +2 -0
  75. package/dist/ray-tracing/sampling/types.d.ts +96 -0
  76. package/dist/ray-tracing/scene/index.d.ts +41 -0
  77. package/dist/ray-tracing/traversal/index.d.ts +4 -0
  78. package/dist/ray-tracing/traversal/layout.d.ts +16 -0
  79. package/dist/ray-tracing/traversal/plan.d.ts +2 -0
  80. package/dist/ray-tracing/traversal/runtime.d.ts +25 -0
  81. package/dist/ray-tracing/traversal/types.d.ts +79 -0
  82. package/dist/ray-tracing/worker/client.d.ts +25 -0
  83. package/dist/ray-tracing/worker/index.d.ts +5 -0
  84. package/dist/ray-tracing/worker/runtime.d.ts +11 -0
  85. package/dist/ray-tracing/worker/serialization.d.ts +16 -0
  86. package/dist/ray-tracing/worker/source.d.ts +1 -0
  87. package/dist/ray-tracing/worker/types.d.ts +91 -0
  88. package/dist/ray-tracing.d.ts +17 -0
  89. package/dist/ray-tracing.js +7666 -0
  90. package/dist/spine.js +1 -1
  91. package/dist/tilemap.js +1 -1
  92. package/package.json +26 -23
  93. package/dist/animation/Animation2DStateMachineVisualRuntime.d.ts +0 -27
  94. package/dist/animation/Particle2DDescriptorAdapter.d.ts +0 -5
  95. package/dist/animation3d/Animation3DResource.d.ts +0 -28
  96. package/dist/animation3d/hya/HyaAnimation3DStateMachineRuntime.d.ts +0 -65
  97. package/dist/animation3d/hya/index.d.ts +0 -4
  98. package/dist/animation3d/runtime/integration/Animation3DStateMachineMixerAdapter.d.ts +0 -81
  99. package/dist/animation3d/runtime/integration/Animation3DStateMachineMixerIntegration.d.ts +0 -39
  100. package/dist/animation3d/runtime/integration/index.d.ts +0 -2
  101. package/dist/animation3d/runtime/mixer/Action.d.ts +0 -150
  102. package/dist/animation3d/runtime/mixer/Animation3DMixerRuntimeStore.d.ts +0 -6
  103. package/dist/animation3d/runtime/mixer/Mixer.d.ts +0 -59
  104. package/dist/animation3d/runtime/mixer/PoseBuffer.d.ts +0 -1
  105. package/dist/animation3d/runtime/mixer/TrackSampler.d.ts +0 -24
  106. package/dist/animation3d/runtime/mixer/index.d.ts +0 -4
  107. package/dist/animation3d/runtime/state-machine/Animation3DBlendTreeWeights.d.ts +0 -1
  108. package/dist/animation3d/runtime/state-machine/Animation3DStateMachineCompiler.d.ts +0 -1
  109. package/dist/animation3d/runtime/state-machine/Animation3DStateMachineController.d.ts +0 -1
  110. package/dist/animation3d/runtime/state-machine/Animation3DStateMachineMixerPort.d.ts +0 -1
  111. package/dist/animation3d/runtime/state-machine/index.d.ts +0 -6
  112. package/dist/benchmark.d.ts +0 -30
  113. package/dist/chunks/2d-ui-animation-2d.generated-D4aHzFU2.js +0 -4
  114. package/dist/chunks/Animation2DStateMachine-9IonguNa.js +0 -3245
  115. package/dist/gltf/GltfAccessorReader.d.ts +0 -7
  116. package/dist/gltf/GltfDracoDecoder.d.ts +0 -4
  117. package/dist/gltf/GltfLoaderErrors.d.ts +0 -4
  118. package/dist/gltf/GltfMaterialDescriptor.d.ts +0 -23
  119. package/dist/gltf/GltfMaterialLoader.d.ts +0 -6
  120. package/dist/gltf-worker-runtime.d.ts +0 -7
  121. package/dist/plugins/pluginUtils.d.ts +0 -20
  122. package/dist/plugins.d.ts +0 -6
  123. package/dist/shaders/generated/2d-ui-artifact.generated.d.ts +0 -658
  124. package/dist/spine/Spine2DGpuRenderer.d.ts +0 -65
  125. package/dist/spine/Spine2DRuntime.d.ts +0 -103
  126. package/dist/spine/SpineAnimationBoneTimelines.d.ts +0 -8
  127. package/dist/spine/SpineFloatBuilder.d.ts +0 -15
  128. package/dist/spine/SpinePathConstraintSolver.d.ts +0 -32
  129. package/dist/spine/SpineSkeletonRuntime.d.ts +0 -185
  130. package/dist/spine/SpineSlotAnimation.d.ts +0 -17
  131. package/dist/spine/SpineTimelineRuntime.d.ts +0 -25
  132. package/dist/spine/SpineVertexBuilder.d.ts +0 -61
  133. package/dist/tilemap/Tilemap2DRenderer.d.ts +0 -24
  134. package/dist/utils/arrayAccess.d.ts +0 -4
  135. package/dist/utils/camera2d.d.ts +0 -12
  136. package/src/animation/Animation2DComponent.ts +0 -135
  137. package/src/animation/Animation2DExtensionRegistry.ts +0 -52
  138. package/src/animation/Animation2DRenderSystem.ts +0 -1026
  139. package/src/animation/Animation2DRuntime.ts +0 -1326
  140. package/src/animation/Animation2DStateMachine.ts +0 -39
  141. package/src/animation/Animation2DStateMachineComponent.ts +0 -228
  142. package/src/animation/Animation2DStateMachineRuntime.ts +0 -141
  143. package/src/animation/Animation2DStateMachineSystem.ts +0 -60
  144. package/src/animation/Animation2DStateMachineVisualRuntime.ts +0 -282
  145. package/src/animation/Animation2DSystem.ts +0 -72
  146. package/src/animation/AnimationAssetLoader.ts +0 -84
  147. package/src/animation/AnimationAudioClip.ts +0 -140
  148. package/src/animation/AnimationPathTessellator.ts +0 -691
  149. package/src/animation/AnimationTextRasterizer.ts +0 -340
  150. package/src/animation/AnimationVisual2D.ts +0 -153
  151. package/src/animation/HyaAnimation2DClipAdapter.ts +0 -845
  152. package/src/animation/Particle2DDescriptorAdapter.ts +0 -35
  153. package/src/animation/runtime/mixer/Action.ts +0 -876
  154. package/src/animation/runtime/mixer/Mixer.ts +0 -505
  155. package/src/animation/runtime/mixer/PoseBuffer.ts +0 -105
  156. package/src/animation/runtime/mixer/Sampler.ts +0 -252
  157. package/src/animation/runtime/mixer/Types.ts +0 -138
  158. package/src/animation/runtime/mixer/index.ts +0 -31
  159. package/src/animation-state-machine/AnimationStateMachine.ts +0 -146
  160. package/src/animation-state-machine/AnimationStateMachineChannels.ts +0 -116
  161. package/src/animation-state-machine/runtime/Animation2DStateMachineMixerAdapter.ts +0 -197
  162. package/src/animation-state-machine/runtime/AnimationStateMachineBlendTreeWeights.ts +0 -503
  163. package/src/animation-state-machine/runtime/AnimationStateMachineCompiler.ts +0 -763
  164. package/src/animation-state-machine/runtime/AnimationStateMachineController.ts +0 -987
  165. package/src/animation-state-machine/runtime/AnimationStateMachineMixerPort.ts +0 -54
  166. package/src/animation-state-machine/runtime/index.ts +0 -47
  167. package/src/animation.ts +0 -33
  168. package/src/animation3d/Animation3DAction.ts +0 -73
  169. package/src/animation3d/Animation3DBinding.ts +0 -116
  170. package/src/animation3d/Animation3DClip.ts +0 -22
  171. package/src/animation3d/Animation3DError.ts +0 -34
  172. package/src/animation3d/Animation3DMixer.ts +0 -82
  173. package/src/animation3d/Animation3DPose.ts +0 -100
  174. package/src/animation3d/Animation3DPoseBuffer.ts +0 -85
  175. package/src/animation3d/Animation3DResource.ts +0 -49
  176. package/src/animation3d/Animation3DStateMachine.ts +0 -126
  177. package/src/animation3d/Animation3DStateMachineRuntime.ts +0 -191
  178. package/src/animation3d/Animation3DTrack.ts +0 -29
  179. package/src/animation3d/hya/HyaAnimation3DRuntime.ts +0 -756
  180. package/src/animation3d/hya/HyaAnimation3DStateMachineRuntime.ts +0 -618
  181. package/src/animation3d/hya/HyaAnimation3DTypes.ts +0 -168
  182. package/src/animation3d/hya/index.ts +0 -10
  183. package/src/animation3d/index.ts +0 -76
  184. package/src/animation3d/runtime/integration/Animation3DStateMachineMixerAdapter.ts +0 -325
  185. package/src/animation3d/runtime/integration/Animation3DStateMachineMixerIntegration.ts +0 -158
  186. package/src/animation3d/runtime/integration/index.ts +0 -9
  187. package/src/animation3d/runtime/mixer/Action.ts +0 -833
  188. package/src/animation3d/runtime/mixer/Animation3DMixerRuntimeStore.ts +0 -26
  189. package/src/animation3d/runtime/mixer/Mixer.ts +0 -562
  190. package/src/animation3d/runtime/mixer/PoseBuffer.ts +0 -1
  191. package/src/animation3d/runtime/mixer/TrackSampler.ts +0 -399
  192. package/src/animation3d/runtime/mixer/index.ts +0 -8
  193. package/src/animation3d/runtime/state-machine/Animation3DBlendTreeWeights.ts +0 -4
  194. package/src/animation3d/runtime/state-machine/Animation3DStateMachineCompiler.ts +0 -1
  195. package/src/animation3d/runtime/state-machine/Animation3DStateMachineController.ts +0 -1
  196. package/src/animation3d/runtime/state-machine/Animation3DStateMachineMixerPort.ts +0 -1
  197. package/src/animation3d/runtime/state-machine/index.ts +0 -45
  198. package/src/animation3d.ts +0 -5
  199. package/src/benchmark.ts +0 -168
  200. package/src/canvas-text/CanvasText2DRenderSystem.ts +0 -292
  201. package/src/canvas-text/CanvasTextComponent.ts +0 -47
  202. package/src/canvas-text.ts +0 -4
  203. package/src/experimental-gltf-worker.ts +0 -18
  204. package/src/experimental-spine-worker.ts +0 -9
  205. package/src/gltf/GltfAccessorReader.ts +0 -271
  206. package/src/gltf/GltfAnimation3DAdapter.ts +0 -680
  207. package/src/gltf/GltfAnimationRuntime.ts +0 -392
  208. package/src/gltf/GltfAssetWorkerClient.ts +0 -136
  209. package/src/gltf/GltfCompatibilityReport.ts +0 -214
  210. package/src/gltf/GltfConservativeBounds.ts +0 -219
  211. package/src/gltf/GltfDracoDecoder.ts +0 -251
  212. package/src/gltf/GltfExtensionAdapter.ts +0 -340
  213. package/src/gltf/GltfLoaderContract.ts +0 -333
  214. package/src/gltf/GltfLoaderErrors.ts +0 -59
  215. package/src/gltf/GltfMaterialDescriptor.ts +0 -134
  216. package/src/gltf/GltfMaterialEncoder.ts +0 -173
  217. package/src/gltf/GltfMaterialLoader.ts +0 -293
  218. package/src/gltf/GltfModelComponent.ts +0 -115
  219. package/src/gltf/GltfModelSystem.ts +0 -208
  220. package/src/gltf/GltfPlugin.ts +0 -84
  221. package/src/gltf/GltfSchema.ts +0 -229
  222. package/src/gltf/GltfUvSemanticPlanner.ts +0 -166
  223. package/src/gltf/gltfLoader.ts +0 -910
  224. package/src/gltf-animation3d.ts +0 -10
  225. package/src/gltf-worker-runtime.ts +0 -10
  226. package/src/gltf.ts +0 -56
  227. package/src/grid/Grid2DComponent.ts +0 -69
  228. package/src/grid.ts +0 -2
  229. package/src/hya-state-machine.ts +0 -19
  230. package/src/index.ts +0 -2
  231. package/src/plugins/pluginUtils.ts +0 -50
  232. package/src/plugins.ts +0 -6
  233. package/src/shaders/generated/2d-ui-animation-2d.generated.wgsl +0 -242
  234. package/src/shaders/generated/2d-ui-artifact.generated.ts +0 -867
  235. package/src/shaders/generated/2d-ui-canvas-text-2d.generated.wgsl +0 -38
  236. package/src/shaders/generated/2d-ui-spine2d.generated.wgsl +0 -41
  237. package/src/shaders/generated/2d-ui-tilemap2d.generated.wgsl +0 -36
  238. package/src/spine/Spine2DComponent.ts +0 -168
  239. package/src/spine/Spine2DGpuRenderer.ts +0 -449
  240. package/src/spine/Spine2DRenderSystem.ts +0 -199
  241. package/src/spine/Spine2DRuntime.ts +0 -1529
  242. package/src/spine/SpineAnimationBoneTimelines.ts +0 -30
  243. package/src/spine/SpineAssetParser.ts +0 -20
  244. package/src/spine/SpineAssetWorkerClient.ts +0 -112
  245. package/src/spine/SpineAssetWorkerContract.ts +0 -4
  246. package/src/spine/SpineAtlasParser.ts +0 -127
  247. package/src/spine/SpineFloatBuilder.ts +0 -66
  248. package/src/spine/SpinePathConstraintSolver.ts +0 -582
  249. package/src/spine/SpinePlugin.ts +0 -76
  250. package/src/spine/SpineSkeletonRuntime.ts +0 -521
  251. package/src/spine/SpineSlotAnimation.ts +0 -195
  252. package/src/spine/SpineTimelineRuntime.ts +0 -453
  253. package/src/spine/SpineVertexBuilder.ts +0 -686
  254. package/src/spine.ts +0 -7
  255. package/src/tilemap/Tilemap2DComponent.ts +0 -232
  256. package/src/tilemap/Tilemap2DRenderSystem.ts +0 -58
  257. package/src/tilemap/Tilemap2DRenderer.ts +0 -289
  258. package/src/tilemap/TilemapPlugin.ts +0 -75
  259. package/src/tilemap.ts +0 -6
  260. package/src/tween/Tween2DComponent.ts +0 -83
  261. package/src/tween/Tween2DSystem.ts +0 -78
  262. package/src/tween.ts +0 -4
  263. package/src/types/earcut.d.ts +0 -7
  264. package/src/types/wgsl.d.ts +0 -4
  265. package/src/utils/RenderSystem2DBase.ts +0 -164
  266. package/src/utils/arrayAccess.ts +0 -17
  267. package/src/utils/camera2d.ts +0 -68
  268. package/src/utils/render2dGpu.ts +0 -132
  269. package/test/fixtures/gltf/animation-characterization.gltf +0 -344
@@ -1,1026 +0,0 @@
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
- }