@hology/core 0.0.237 → 0.0.238

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 (90) hide show
  1. package/dist/effects/sequence/sequence-player.js +1 -1
  2. package/dist/effects/vfx/gpu-particle-arena.d.ts +57 -0
  3. package/dist/effects/vfx/gpu-particle-arena.js +4 -0
  4. package/dist/effects/vfx/gpu-sprite-backend.d.ts +33 -0
  5. package/dist/effects/vfx/gpu-sprite-backend.js +4 -0
  6. package/dist/effects/vfx/gpu-sprite-compiler.d.ts +58 -0
  7. package/dist/effects/vfx/gpu-sprite-compiler.js +4 -0
  8. package/dist/effects/vfx/gpu-sprite-render-graph.d.ts +10 -0
  9. package/dist/effects/vfx/gpu-sprite-render-graph.js +4 -0
  10. package/dist/effects/vfx/gpu-sprite-runtime.d.ts +304 -0
  11. package/dist/effects/vfx/gpu-sprite-runtime.js +4 -0
  12. package/dist/effects/vfx/gpu-sprite-scheduler-runtime.d.ts +71 -0
  13. package/dist/effects/vfx/gpu-sprite-scheduler-runtime.js +4 -0
  14. package/dist/effects/vfx/gpu-sprite-template.d.ts +47 -0
  15. package/dist/effects/vfx/gpu-sprite-template.js +4 -0
  16. package/dist/effects/vfx/gpu-sprite-world-runtime.d.ts +61 -0
  17. package/dist/effects/vfx/gpu-sprite-world-runtime.js +4 -0
  18. package/dist/effects/vfx/gpu-vfx-capacity.d.ts +3 -0
  19. package/dist/effects/vfx/gpu-vfx-capacity.js +4 -0
  20. package/dist/effects/vfx/gpu-vfx-scheduler.d.ts +52 -0
  21. package/dist/effects/vfx/gpu-vfx-scheduler.js +4 -0
  22. package/dist/effects/vfx/gpu-vfx-world-service.d.ts +103 -0
  23. package/dist/effects/vfx/gpu-vfx-world-service.js +4 -0
  24. package/dist/effects/vfx/index.d.ts +11 -0
  25. package/dist/effects/vfx/index.js +1 -1
  26. package/dist/effects/vfx/trail-renderer.js +1 -1
  27. package/dist/effects/vfx/vfx-actor.d.ts +19 -2
  28. package/dist/effects/vfx/vfx-actor.js +1 -1
  29. package/dist/effects/vfx/vfx-asset.d.ts +2 -0
  30. package/dist/effects/vfx/vfx-default-sprite-texture.d.ts +3 -0
  31. package/dist/effects/vfx/vfx-default-sprite-texture.js +4 -0
  32. package/dist/effects/vfx/vfx-input-runtime.d.ts +3 -1
  33. package/dist/effects/vfx/vfx-materializer.d.ts +10 -0
  34. package/dist/effects/vfx/vfx-materializer.js +1 -1
  35. package/dist/effects/vfx/vfx-mesh-transform.d.ts +4 -0
  36. package/dist/effects/vfx/vfx-mesh-transform.js +4 -0
  37. package/dist/effects/vfx/vfx-param.d.ts +2 -0
  38. package/dist/effects/vfx/vfx-param.js +1 -1
  39. package/dist/effects/vfx/vfx-renderers.js +1 -1
  40. package/dist/effects/vfx/vfx-runtime.d.ts +35 -0
  41. package/dist/effects/vfx/vfx-runtime.js +4 -0
  42. package/dist/effects/vfx/vfx-service.d.ts +3 -0
  43. package/dist/effects/vfx/vfx-service.js +1 -1
  44. package/dist/gameplay/actors/builtin/components/character/character-animation.d.ts +14 -0
  45. package/dist/gameplay/actors/builtin/components/character/character-animation.js +1 -1
  46. package/dist/gameplay/actors/builtin/components/character/character-movement-like.d.ts +1 -1
  47. package/dist/gameplay/actors/builtin/components/character/character-movement.d.ts +8 -3
  48. package/dist/gameplay/actors/builtin/components/character/character-movement.js +1 -1
  49. package/dist/gameplay/actors/builtin/components/character/root-motion-constraint.d.ts +10 -0
  50. package/dist/gameplay/actors/builtin/components/character/root-motion-constraint.js +1 -1
  51. package/dist/gameplay/ai/behavior-tree/move.d.ts +14 -0
  52. package/dist/gameplay/ai/behavior-tree/move.js +1 -1
  53. package/dist/gameplay/initiate.d.ts +3 -0
  54. package/dist/gameplay/initiate.js +1 -1
  55. package/dist/gameplay/services/physics/physics-system.js +1 -1
  56. package/dist/gameplay/services/render.d.ts +14 -1
  57. package/dist/gameplay/services/render.js +1 -1
  58. package/dist/rendering/webgpu-experimental-backend.d.ts +8 -2
  59. package/dist/rendering/webgpu-experimental-backend.js +1 -1
  60. package/dist/rendering.d.ts +32 -3
  61. package/dist/rendering.js +1 -1
  62. package/dist/scene/asset-resource-loader.js +1 -1
  63. package/dist/scene/materializer.js +1 -1
  64. package/dist/scene/objects/shapes.js +1 -1
  65. package/dist/shader/builtin/decal-standard-shader.d.ts +1 -0
  66. package/dist/shader/builtin/decal-standard-shader.js +1 -1
  67. package/dist/shader/decal-shader.js +1 -1
  68. package/dist/shader-nodes/effects.js +1 -1
  69. package/dist/shader-nodes/material-flags.js +1 -1
  70. package/dist/shader-nodes/particle.d.ts +5 -2
  71. package/dist/shader-nodes/particle.js +1 -1
  72. package/dist/test/asset-resource-loader-skinned-merge.test.d.ts +2 -0
  73. package/dist/test/asset-resource-loader-skinned-merge.test.js +4 -0
  74. package/dist/test/character-root-motion-constraint.test.js +1 -1
  75. package/dist/test/gpu-particle-arena.test.d.ts +2 -0
  76. package/dist/test/gpu-particle-arena.test.js +4 -0
  77. package/dist/test/gpu-sprite-compiler.test.d.ts +2 -0
  78. package/dist/test/gpu-sprite-compiler.test.js +4 -0
  79. package/dist/test/gpu-vfx-scheduler.test.d.ts +2 -0
  80. package/dist/test/gpu-vfx-scheduler.test.js +4 -0
  81. package/dist/test/mesh-clone.test.d.ts +2 -0
  82. package/dist/test/mesh-clone.test.js +4 -0
  83. package/dist/test/sequence-scene-binding.test.js +1 -1
  84. package/dist/test/sequence-vfx.test.js +1 -1
  85. package/dist/test/vfx-mesh-transform.test.d.ts +2 -0
  86. package/dist/test/vfx-mesh-transform.test.js +4 -0
  87. package/dist/utils/mesh.d.ts +7 -1
  88. package/dist/utils/mesh.js +1 -1
  89. package/package.json +15 -3
  90. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,71 @@
1
+ import * as THREE from 'three';
2
+ import type { VfxAsset } from '../../scene/model.js';
3
+ import type { VfxAssetProvider } from './vfx-materializer.js';
4
+ import type { PrepareGpuSpriteBackendOptions } from './gpu-sprite-backend.js';
5
+ import type { GpuSpriteCompatibilityReport } from './gpu-sprite-compiler.js';
6
+ import { GpuVfxScheduler, type GpuVfxInstanceHandle } from './gpu-vfx-scheduler.js';
7
+ import { GpuSpriteRuntime, type GpuSpriteRuntimeStats } from './gpu-sprite-runtime.js';
8
+ import { GpuSpriteTemplateCache } from './gpu-sprite-template.js';
9
+ export type GpuSpriteSchedulerRuntimeOptions = {
10
+ asset: VfxAsset;
11
+ maximumInstances: number;
12
+ assets: Pick<VfxAssetProvider, 'getTexture'> & Partial<Pick<VfxAssetProvider, 'getMesh'>>;
13
+ cache?: PrepareGpuSpriteBackendOptions['cache'];
14
+ resolveCustomMaterial?: PrepareGpuSpriteBackendOptions['resolveCustomMaterial'];
15
+ device?: GPUDevice;
16
+ particlePageSize?: number;
17
+ templateCache?: GpuSpriteTemplateCache;
18
+ templateRevision?: string | number;
19
+ aliveCountReadback?: boolean;
20
+ maximumStorageBytes?: number;
21
+ };
22
+ export type GpuSpriteSchedulerInstance = Readonly<{
23
+ handle: GpuVfxInstanceHandle;
24
+ pages: readonly number[];
25
+ }>;
26
+ /**
27
+ * Observable bridge from dynamic effect handles to one shared GPU sprite
28
+ * runtime. Physical particle pages may be recycled independently of logical
29
+ * effect and emitter slots.
30
+ */
31
+ export declare class GpuSpriteSchedulerRuntime {
32
+ private readonly maximumInstances;
33
+ private readonly emittersPerInstance;
34
+ private readonly instanceDuration;
35
+ private readonly maximumParticleLife;
36
+ private readonly emitterCapacities;
37
+ private readonly emitterPositionOffsets;
38
+ private readonly inputBindings;
39
+ readonly scheduler: GpuVfxScheduler;
40
+ readonly runtime: GpuSpriteRuntime;
41
+ readonly compatibility: GpuSpriteCompatibilityReport;
42
+ readonly reasons: readonly string[];
43
+ private readonly activeBySlot;
44
+ private readonly activePages;
45
+ private readonly emitterIndexScratch;
46
+ private readonly instanceParticleCapacity;
47
+ private readonly stoppingDeadlines;
48
+ private readonly positionScratch;
49
+ private activePagesDirty;
50
+ private constructor();
51
+ static create(options: GpuSpriteSchedulerRuntimeOptions): Promise<GpuSpriteSchedulerRuntime | null>;
52
+ spawn(position: THREE.Vector3): GpuSpriteSchedulerInstance | null;
53
+ destroy(instance: GpuSpriteSchedulerInstance): boolean;
54
+ setPosition(instance: GpuSpriteSchedulerInstance, position: THREE.Vector3): boolean;
55
+ setTransform(instance: GpuSpriteSchedulerInstance, transform: THREE.Matrix4): boolean;
56
+ pause(instance: GpuSpriteSchedulerInstance): boolean;
57
+ play(instance: GpuSpriteSchedulerInstance): boolean;
58
+ setTimescale(instance: GpuSpriteSchedulerInstance, timescale: number): boolean;
59
+ setInput(instance: GpuSpriteSchedulerInstance, inputId: string, value: unknown): boolean;
60
+ private calculateInstanceDuration;
61
+ restart(instance: GpuSpriteSchedulerInstance): boolean;
62
+ stop(instance: GpuSpriteSchedulerInstance, expireWithin?: number): boolean;
63
+ update(deltaTime: number): void;
64
+ flushActivePages(): void;
65
+ getStats(): {
66
+ scheduler: ReturnType<GpuVfxScheduler['getStats']>;
67
+ particles: GpuSpriteRuntimeStats;
68
+ };
69
+ dispose(): void;
70
+ }
71
+ //# sourceMappingURL=gpu-sprite-scheduler-runtime.d.ts.map
@@ -0,0 +1,4 @@
1
+ import*as e from"three";import{GpuVfxScheduler as t}from"./gpu-vfx-scheduler.js";import{calculateGpuSpriteOptionalStateLayout as s,GPU_SPRITE_BASE_COMMAND_BYTES as i,GPU_SPRITE_EMITTER_BYTES as r,GPU_SPRITE_TRANSFORM_COMMAND_BYTES as n,GpuSpriteRuntime as a}from"./gpu-sprite-runtime.js";import{GpuSpriteTemplateCache as l,instantiateGpuSpriteTemplate as h}from"./gpu-sprite-template.js";export class GpuSpriteSchedulerRuntime{constructor(t,s,i,r,n,a,l,h,c,u,o){this.maximumInstances=n,this.emittersPerInstance=a,this.instanceDuration=l,this.maximumParticleLife=h,this.emitterCapacities=c,this.emitterPositionOffsets=u,this.inputBindings=o,this.activePages=new Set,this.positionScratch=new e.Vector3,this.activePagesDirty=!1,this.runtime=t,this.scheduler=s,this.compatibility=i,this.reasons=r,this.activeBySlot=new Array(n).fill(null),this.emitterIndexScratch=new Array(a).fill(0),this.instanceParticleCapacity=c.reduce((e,t)=>e+t,0),this.stoppingDeadlines=new Float64Array(n),this.stoppingDeadlines.fill(Number.POSITIVE_INFINITY);for(let e=0;e<t.emitters.length;e++)t.setEmitterActive(e,!1);t.setActivePages([])}static async create(c){const u=Math.max(1,Math.floor(c.maximumInstances)),o=(await(c.templateCache??new l).resolve(c.asset,{assets:c.assets,renderClassCache:c.cache,resolveCustomMaterial:c.resolveCustomMaterial,revision:c.templateRevision})).template;if(null==o)return null;const m=o.particleCapacity,d=Math.max(1,Math.floor(c.particlePageSize??m)),p=Math.ceil(m/d)*d*u,f=m*u,I=u*o.emitters.length,P=o.renderClasses.some(e=>null!=e.meshVertices),g=o.renderClasses.some(e=>!0===e.localSpace&&null==e.trail),S=I*(o.emitters.some(e=>!0===e.localSpace&&null!=o.renderClasses[e.renderClass??0]?.trail)?n:i),y=P||o.emitters.some(e=>1!==e.meshScaleStart.x||1!==e.meshScaleStart.y||1!==e.meshScaleStart.z||1!==e.meshScaleEnd.x||1!==e.meshScaleEnd.y||1!==e.meshScaleEnd.z),v=o.emitters.reduce((e,t)=>{const s=o.renderClasses[t.renderClass??0]?.trail;return e+(null==s?0:t.capacity*(s.historyLength+1))},0)*u,x=s({capacity:p,particleTransforms:y,vortex:o.emitters.some(e=>e.hasVortex),disperse:o.emitters.some(e=>e.hasDisperse),trailHistoryEntries:v}),C=P?16*I*4+o.renderClasses.reduce((e,t)=>e+(t.meshVertices?.byteLength??0),0):0;if(16*p*4+4*f+4*p+4*p*4+I*r+Math.max(i,S)+160+16*o.renderClasses.length*2+C+x.bytes+(g&&!P?16*I*4:0)>(c.maximumStorageBytes??Number.POSITIVE_INFINITY))return null;if(null!=c.device){const e=c.device.limits.maxStorageBufferBindingSize;if([16*p*4,4*f,4*p,4*p*4,I*r,Math.max(i,S),...P||g?[16*I*4]:[],...x.bytes>0?[x.bytes]:[]].some(t=>t>e))return null;if(7+(x.bytes>0?1:0)>c.device.limits.maxStorageBuffersPerShaderStage)return null}const E=new e.Vector3,D=h(o,new Array(u).fill(E)),T=new a(D,o.renderClasses,void 0,{pageSize:d,capacity:p,aliveCountReadback:c.aliveCountReadback});return new GpuSpriteSchedulerRuntime(T,new t({pageSize:d,maximumParticles:p}),o.compatibility,o.reasons,u,o.emitters.length,o.duration,o.emitters.reduce((e,t)=>Math.max(e,t.life),0),o.emitters.map(e=>e.capacity),o.emitters.map(e=>e.position.clone()),o.inputBindings)}spawn(e){const t=this.scheduler.createInstance({particleCapacity:this.instanceParticleCapacity,duration:this.instanceDuration});if(null==t)return null;const s=this.scheduler.arena.get(t).pages;if(0===s.length)return this.scheduler.destroyInstance(t),null;const i={handle:t,pages:s};this.activeBySlot[t.slot]=i;for(const e of s)this.activePages.add(e);this.activePagesDirty=!0;for(let e=0;e<this.emittersPerInstance;e++)this.emitterIndexScratch[e]=t.slot*this.emittersPerInstance+e;this.runtime.mapParticlePages(s,this.emitterIndexScratch,this.emitterCapacities);for(let s=0;s<this.emittersPerInstance;s++){const i=t.slot*this.emittersPerInstance+s;this.positionScratch.copy(this.emitterPositionOffsets[s]),!0!==this.runtime.emitters[i].localSpace&&null==this.runtime.emitters[i].parentEmitter&&this.positionScratch.add(e),this.runtime.queueEmitterState(i,this.positionScratch,!0,!0)}return this.scheduler.play(t),i}destroy(e){if(null==this.scheduler.getInstance(e.handle))return!1;for(let t=0;t<this.emittersPerInstance;t++){const s=e.handle.slot*this.emittersPerInstance+t;this.runtime.queueEmitterState(s,this.runtime.emitters[s].position,!1)}for(const t of e.pages)this.activePages.delete(t);return this.activePagesDirty=!0,this.activeBySlot[e.handle.slot]=null,this.scheduler.destroyInstance(e.handle)}setPosition(e,t){if(null==this.scheduler.getInstance(e.handle))return!1;for(let s=0;s<this.emittersPerInstance;s++){const i=e.handle.slot*this.emittersPerInstance+s;!0!==this.runtime.emitters[i].localSpace&&null==this.runtime.emitters[i].parentEmitter&&(this.positionScratch.copy(t).add(this.emitterPositionOffsets[s]),this.runtime.queueEmitterState(i,this.positionScratch,!0))}return!0}setTransform(e,t){if(null==this.scheduler.getInstance(e.handle))return!1;for(let s=0;s<this.emittersPerInstance;s++)this.runtime.setEmitterTransform(e.handle.slot*this.emittersPerInstance+s,t);return!0}pause(e){if(!this.scheduler.pause(e.handle))return!1;for(let t=0;t<this.emittersPerInstance;t++)this.runtime.queueEmitterPaused(e.handle.slot*this.emittersPerInstance+t,!0);return!0}play(e){if(!this.scheduler.play(e.handle))return!1;for(let t=0;t<this.emittersPerInstance;t++)this.runtime.queueEmitterPaused(e.handle.slot*this.emittersPerInstance+t,!1);return!0}setTimescale(e,t){if(!this.scheduler.setTimescale(e.handle,t))return!1;for(let s=0;s<this.emittersPerInstance;s++)this.runtime.queueEmitterTimescale(e.handle.slot*this.emittersPerInstance+s,t);return!0}setInput(e,t,s){if(null==this.scheduler.getInstance(e.handle))return!1;let i=!1;for(const r of this.inputBindings){if(r.inputId!==t)continue;const n=e.handle.slot*this.emittersPerInstance+r.emitterIndex;i=this.runtime.setEmitterBoundValue(n,r,s)||i}return i&&this.scheduler.setDuration(e.handle,this.calculateInstanceDuration(e.handle.slot)),i}calculateInstanceDuration(e){let t=0;for(let s=0;s<this.emittersPerInstance;s++){const i=this.runtime.emitters[e*this.emittersPerInstance+s],r="continuous"===i.rate.type?i.rate.duration<0?Number.POSITIVE_INFINITY:i.rate.delay+i.rate.duration+i.life:i.rate.delay+i.life;t=Math.max(t,r)}return t}restart(e){if(!this.scheduler.restart(e.handle))return!1;this.stoppingDeadlines[e.handle.slot]=Number.POSITIVE_INFINITY;for(let t=0;t<this.emittersPerInstance;t++){const s=e.handle.slot*this.emittersPerInstance+t;this.runtime.queueEmitterState(s,this.runtime.emitters[s].position,!0,!0)}return!0}stop(e,t){const s=this.scheduler.getInstance(e.handle);if(null==s||!this.scheduler.stop(e.handle))return!1;const i=null==t?this.maximumParticleLife:Math.max(0,t);this.stoppingDeadlines[e.handle.slot]=s.elapsedTime+i;for(let s=0;s<this.emittersPerInstance;s++)this.runtime.queueEmitterStop(e.handle.slot*this.emittersPerInstance+s,t);return!0}update(e){this.scheduler.update(e),this.runtime.update(e);for(const e of this.activeBySlot){if(null==e)continue;const t=this.scheduler.getInstance(e.handle);null!=t&&((!1===t.playing&&t.elapsedTime>=this.instanceDuration||t.stopping&&t.elapsedTime>=this.stoppingDeadlines[e.handle.slot])&&this.destroy(e))}this.flushActivePages()}flushActivePages(){this.activePagesDirty&&(this.runtime.setActivePages(this.activePages),this.activePagesDirty=!1)}getStats(){return{scheduler:this.scheduler.getStats(),particles:this.runtime.getStats()}}dispose(){this.runtime.dispose();for(const e of this.activeBySlot)null!=e&&this.scheduler.destroyInstance(e.handle);this.activeBySlot.fill(null)}}/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -0,0 +1,47 @@
1
+ import * as THREE from 'three';
2
+ import type { NodeShaderMaterial } from 'three-shader-graph';
3
+ import type { VfxAsset } from '../../scene/model.js';
4
+ import type { VfxAssetProvider } from './vfx-materializer.js';
5
+ import { type GpuSpriteCompatibilityReport, type GpuSpriteInputBinding, type GpuSpriteRenderClassCache } from './gpu-sprite-compiler.js';
6
+ import { type GpuSpriteEmitter, type GpuSpriteRenderClass } from './gpu-sprite-runtime.js';
7
+ export type GpuSpriteTemplate = Readonly<{
8
+ assetId: string;
9
+ emitters: readonly GpuSpriteEmitter[];
10
+ renderClasses: readonly GpuSpriteRenderClass[];
11
+ compatibility: GpuSpriteCompatibilityReport;
12
+ reasons: readonly string[];
13
+ particleCapacity: number;
14
+ duration: number;
15
+ inputBindings: readonly GpuSpriteInputBinding[];
16
+ }>;
17
+ export type GpuSpriteTemplateResult = Readonly<{
18
+ template: GpuSpriteTemplate | null;
19
+ compatibility: GpuSpriteCompatibilityReport;
20
+ reasons: readonly string[];
21
+ }>;
22
+ export type GpuSpriteTemplateCacheStats = Readonly<{
23
+ builds: number;
24
+ hits: number;
25
+ invalidations: number;
26
+ }>;
27
+ export type GpuSpriteTemplateBuildOptions = {
28
+ assets: Pick<VfxAssetProvider, 'getTexture'> & Partial<Pick<VfxAssetProvider, 'getMesh'>>;
29
+ renderClassCache?: GpuSpriteRenderClassCache;
30
+ resolveCustomMaterial?: (output: import('./vfx-asset.js').SpriteOutput | import('./vfx-asset.js').MeshOutput | import('./vfx-asset.js').DecalOutput | import('./vfx-asset.js').ShapeOutput) => Promise<NodeShaderMaterial | null>;
31
+ /** Asset-system revision/hash when an asset object may be mutated in place. */
32
+ revision?: string | number;
33
+ };
34
+ /** Caches immutable compiled asset templates by asset object revision. */
35
+ export declare class GpuSpriteTemplateCache {
36
+ private entries;
37
+ private builds;
38
+ private hits;
39
+ private invalidations;
40
+ resolve(asset: VfxAsset, options: GpuSpriteTemplateBuildOptions): Promise<GpuSpriteTemplateResult>;
41
+ invalidate(asset: VfxAsset): void;
42
+ clear(): void;
43
+ getStats(): GpuSpriteTemplateCacheStats;
44
+ }
45
+ export declare function buildGpuSpriteTemplate(asset: VfxAsset, options: GpuSpriteTemplateBuildOptions): Promise<GpuSpriteTemplateResult>;
46
+ export declare function instantiateGpuSpriteTemplate(template: GpuSpriteTemplate, positions: readonly THREE.Vector3[]): GpuSpriteEmitter[];
47
+ //# sourceMappingURL=gpu-sprite-template.d.ts.map
@@ -0,0 +1,4 @@
1
+ import*as e from"three";import{compileGpuSpriteEmitters as t,materializeGpuSpriteRenderClasses as i}from"./gpu-sprite-compiler.js";import{getCompiledGpuRenderClassMaterial as a}from"./gpu-sprite-runtime.js";export class GpuSpriteTemplateCache{constructor(){this.entries=new WeakMap,this.builds=0,this.hits=0,this.invalidations=0}resolve(e,t){const i=this.entries.get(e);if(null!=i&&i.revision===t.revision)return this.hits+=1,i.result;null!=i&&(this.invalidations+=1),this.builds+=1;const a=buildGpuSpriteTemplate(e,t).catch(t=>{throw this.entries.get(e)?.result===a&&this.entries.delete(e),t});return this.entries.set(e,{revision:t.revision,result:a}),a}invalidate(e){this.entries.delete(e)&&(this.invalidations+=1)}clear(){this.entries=new WeakMap,this.invalidations+=1}getStats(){return{builds:this.builds,hits:this.hits,invalidations:this.invalidations}}}export async function buildGpuSpriteTemplate(s,n){const o=t(s,[new e.Vector3]);if(!o.compatibility.compatible||0===o.emitters.length)return{template:null,compatibility:o.compatibility,reasons:o.compatibility.issues.map(e=>e.message)};try{const e=await i(null==n.revision?o.renderClasses:o.renderClasses.map(e=>({...e,key:`${n.revision}:${e.key}`})),n.assets,n.resolveCustomMaterial,n.renderClassCache);for(const t of e.renderClasses)null!=t.material&&a(t);const t=o.emitters.reduce((e,t)=>e+t.capacity,0),r=o.emitters.map(e=>"continuous"===e.rate.type?e.rate.duration<0?Number.POSITIVE_INFINITY:e.rate.delay+e.rate.duration+e.life:e.rate.delay+e.life),l=e=>{const t=o.emitters[e].parentEmitter;return r[e]+(null==t?0:l(t))},c=o.emitters.reduce((e,t,i)=>Math.max(e,l(i)),0);return{template:{assetId:s.id,emitters:o.emitters,renderClasses:e.renderClasses,compatibility:o.compatibility,reasons:e.unsupported,particleCapacity:t,duration:c,inputBindings:o.inputBindings},compatibility:o.compatibility,reasons:e.unsupported}}catch(e){const t=e instanceof Error?e.message:String(e);return{template:null,compatibility:{compatible:!1,issues:[...o.compatibility.issues,{severity:"error",code:"material-graph",emitter:"material",message:t}]},reasons:[`GPU material compilation failed: ${t}`]}}}export function instantiateGpuSpriteTemplate(e,t){return t.flatMap((t,i)=>e.emitters.map(a=>({...a,parentEmitter:null==a.parentEmitter?void 0:i*e.emitters.length+a.parentEmitter,position:!0===a.localSpace||null!=a.parentEmitter?a.position.clone():a.position.clone().add(t),velocityMin:a.velocityMin.clone(),velocityMax:a.velocityMax.clone(),force:a.force.clone(),randomDrift:a.randomDrift.clone(),moveToTarget:a.moveToTarget.clone(),colorStart:a.colorStart.clone(),colorEnd:a.colorEnd.clone(),meshRotation:a.meshRotation.clone(),meshRotationEuler:a.meshRotationEuler.clone(),meshRotationStep:a.meshRotationStep.clone(),randomMeshRotationAxis:a.randomMeshRotationAxis,radialVelocityDirection:a.radialVelocityDirection.clone(),radialVelocitySpeed:a.radialVelocitySpeed,radialVelocitySpread:a.radialVelocitySpread,hasRadialVelocity:a.hasRadialVelocity,attractionTarget:a.attractionTarget.clone(),attractionForce:a.attractionForce,attractionRadius:a.attractionRadius,hasAttraction:a.hasAttraction,alignRotationAxis:a.alignRotationAxis.clone(),alignRotationMode:a.alignRotationMode,alignRotationCenter:a.alignRotationCenter.clone(),alignRotationOffset:a.alignRotationOffset.clone(),vortexAxis:a.vortexAxis.clone(),vortexAmount:a.vortexAmount,hasVortex:a.hasVortex,disperseDistance:a.disperseDistance,dispersePlane:a.dispersePlane,disperseEasing:a.disperseEasing,hasDisperse:a.hasDisperse,curveEasing:a.curveEasing.clone(),meshScaleStart:a.meshScaleStart.clone(),meshScaleEnd:a.meshScaleEnd.clone(),spawnZoneA:a.spawnZoneA?.clone(),spawnZoneB:a.spawnZoneB?.clone(),scaleStart:[...a.scaleStart],scaleEnd:[...a.scaleEnd]})))}/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -0,0 +1,61 @@
1
+ import * as THREE from 'three';
2
+ import type { NodeShaderMaterial } from 'three-shader-graph';
3
+ import type { VfxAsset } from '../../scene/model.js';
4
+ import type { VfxAssetProvider } from './vfx-materializer.js';
5
+ import type { GpuSpriteRenderClassCache } from './gpu-sprite-compiler.js';
6
+ import { GpuSpriteRuntime } from './gpu-sprite-runtime.js';
7
+ import { GpuSpriteTemplateCache } from './gpu-sprite-template.js';
8
+ import { GpuVfxScheduler, type GpuVfxInstanceHandle } from './gpu-vfx-scheduler.js';
9
+ export type GpuSpriteWorldTemplateSource = Readonly<{
10
+ asset: VfxAsset;
11
+ maximumInstances: number;
12
+ revision?: string | number;
13
+ }>;
14
+ export type GpuSpriteWorldRuntimeOptions = {
15
+ templates: readonly GpuSpriteWorldTemplateSource[];
16
+ assets: Pick<VfxAssetProvider, 'getTexture'> & Partial<Pick<VfxAssetProvider, 'getMesh'>>;
17
+ templateCache?: GpuSpriteTemplateCache;
18
+ renderClassCache?: GpuSpriteRenderClassCache;
19
+ resolveCustomMaterial?: (output: import('./vfx-asset.js').SpriteOutput | import('./vfx-asset.js').MeshOutput | import('./vfx-asset.js').DecalOutput | import('./vfx-asset.js').ShapeOutput) => Promise<NodeShaderMaterial | null>;
20
+ particlePageSize?: number;
21
+ device?: GPUDevice;
22
+ aliveCountReadback?: boolean;
23
+ maximumStorageBytes?: number;
24
+ };
25
+ export type GpuSpriteWorldInstance = Readonly<{
26
+ handle: GpuVfxInstanceHandle;
27
+ templateIndex: number;
28
+ pages: readonly number[];
29
+ }>;
30
+ /** One shared sprite simulation arena for all registered templates in a world. */
31
+ export declare class GpuSpriteWorldRuntime {
32
+ private readonly pools;
33
+ readonly scheduler: GpuVfxScheduler;
34
+ readonly runtime: GpuSpriteRuntime;
35
+ readonly templateCount: number;
36
+ private readonly activeByHandleSlot;
37
+ private readonly activePages;
38
+ private readonly positionScratch;
39
+ private activePagesDirty;
40
+ private constructor();
41
+ static create(options: GpuSpriteWorldRuntimeOptions): Promise<GpuSpriteWorldRuntime | null>;
42
+ spawn(templateIndex: number, position: THREE.Vector3): GpuSpriteWorldInstance | null;
43
+ destroy(instance: GpuSpriteWorldInstance): boolean;
44
+ setPosition(instance: GpuSpriteWorldInstance, position: THREE.Vector3): boolean;
45
+ setTransform(instance: GpuSpriteWorldInstance, transform: THREE.Matrix4): boolean;
46
+ pause(instance: GpuSpriteWorldInstance): boolean;
47
+ play(instance: GpuSpriteWorldInstance): boolean;
48
+ restart(instance: GpuSpriteWorldInstance): boolean;
49
+ stop(instance: GpuSpriteWorldInstance, expireWithin?: number): boolean;
50
+ setTimescale(instance: GpuSpriteWorldInstance, timescale: number): boolean;
51
+ setInput(instance: GpuSpriteWorldInstance, inputId: string, value: unknown): boolean;
52
+ has(instance: GpuSpriteWorldInstance): boolean;
53
+ update(deltaTime: number): void;
54
+ flushActivePages(): void;
55
+ dispose(): void;
56
+ private resolve;
57
+ private emitterIndex;
58
+ private forEachEmitter;
59
+ private calculateInstanceDuration;
60
+ }
61
+ //# sourceMappingURL=gpu-sprite-world-runtime.d.ts.map
@@ -0,0 +1,4 @@
1
+ import*as e from"three";import{calculateGpuSpriteOptionalStateLayout as t,GPU_SPRITE_BASE_COMMAND_BYTES as s,GPU_SPRITE_EMITTER_BYTES as i,GPU_SPRITE_TRANSFORM_COMMAND_BYTES as r,GpuSpriteRuntime as n}from"./gpu-sprite-runtime.js";import{GpuSpriteTemplateCache as l,instantiateGpuSpriteTemplate as a}from"./gpu-sprite-template.js";import{GpuVfxScheduler as o}from"./gpu-vfx-scheduler.js";export class GpuSpriteWorldRuntime{constructor(t,s,i){this.pools=i,this.activeByHandleSlot=[],this.activePages=new Set,this.positionScratch=new e.Vector3,this.activePagesDirty=!1,this.runtime=t,this.scheduler=s,this.templateCount=i.length;for(let e=0;e<t.emitters.length;e++)t.setEmitterActive(e,!1);t.setActivePages([])}static async create(h){if(0===h.templates.length)return null;const c=h.templateCache??new l,u=await Promise.all(h.templates.map(e=>c.resolve(e.asset,{assets:h.assets,renderClassCache:h.renderClassCache,resolveCustomMaterial:h.resolveCustomMaterial,revision:e.revision})));if(u.some(e=>null==e.template))return null;const m=u.map(e=>e.template),p=Math.max(1,Math.floor(h.particlePageSize??256)),d=[],f=new Map,g=[],S=[];let I=0;for(let t=0;t<m.length;t++){const s=m[t],i=h.templates[t],r=Math.max(1,Math.floor(i.maximumInstances)),n=s.renderClasses.map(e=>{let t=f.get(e);return null==t&&(t=d.length,f.set(e,t),d.push(e)),t}),l=g.length,o=new e.Vector3,c=a(s,new Array(r).fill(o));for(const e of c)e.renderClass=n[e.renderClass??0]??0,null!=e.parentEmitter&&(e.parentEmitter+=l),g.push(e);S.push({template:s,baseEmitterIndex:l,maximumInstances:r,freeSlots:Array.from({length:r},(e,t)=>r-t-1),emitterCapacities:s.emitters.map(e=>e.capacity),emitterIndexScratch:new Array(s.emitters.length).fill(0),emitterPositionOffsets:s.emitters.map(e=>e.position.clone()),maximumParticleLife:s.emitters.reduce((e,t)=>Math.max(e,t.life),0)}),I+=Math.ceil(s.particleCapacity/p)*p*r}const y=g.reduce((e,t)=>e+t.capacity,0),v=d.some(e=>null!=e.meshVertices),x=d.some(e=>!0===e.localSpace&&null==e.trail),E=g.some(e=>!0===e.localSpace&&null!=d[e.renderClass??0]?.trail),P=g.length*(E?r:s),C=v||g.some(e=>1!==e.meshScaleStart.x||1!==e.meshScaleStart.y||1!==e.meshScaleStart.z||1!==e.meshScaleEnd.x||1!==e.meshScaleEnd.y||1!==e.meshScaleEnd.z),T=g.reduce((e,t)=>{const s=d[t.renderClass??0]?.trail;return e+(null==s?0:t.capacity*(s.historyLength+1))},0),b=t({capacity:I,particleTransforms:C,vortex:g.some(e=>e.hasVortex),disperse:g.some(e=>e.hasDisperse),collisionEvents:g.some(e=>2===e.childSpawnEvent),trailHistoryEntries:T});if(16*I*4+4*y+4*I+4*I*4+g.length*i+Math.max(s,P)+160+16*d.length*2+d.reduce((e,t)=>e+(t.meshVertices?.byteLength??0),0)+b.bytes+(v||x?16*g.length*4:0)>(h.maximumStorageBytes??Number.POSITIVE_INFINITY))return null;if(null!=h.device){const e=h.device.limits.maxStorageBufferBindingSize;if([16*I*4,4*g.reduce((e,t)=>e+t.capacity,0),4*I,4*I*4,g.length*i,Math.max(s,P),...v||x?[16*g.length*4]:[],...b.bytes>0?[b.bytes]:[]].some(t=>t>e))return null;if(7+(b.bytes>0?1:0)>h.device.limits.maxStorageBuffersPerShaderStage)return null}const B=new n(g,d,void 0,{pageSize:p,capacity:I,aliveCountReadback:h.aliveCountReadback});return new GpuSpriteWorldRuntime(B,new o({pageSize:p,maximumParticles:I}),S)}spawn(e,t){const s=this.pools[e],i=s?.freeSlots.pop();if(null==s||null==i)return null;const r=this.scheduler.createInstance({particleCapacity:s.template.particleCapacity,duration:s.template.duration});if(null==r)return s.freeSlots.push(i),null;const n=this.scheduler.arena.get(r).pages,l={handle:r,templateIndex:e,pages:n,logicalSlot:i,stoppingDeadline:Number.POSITIVE_INFINITY};this.activeByHandleSlot[r.slot]=l;for(const e of n)this.activePages.add(e);this.activePagesDirty=!0;const a=s.template.emitters.length;for(let e=0;e<a;e++){const r=s.baseEmitterIndex+i*a+e;s.emitterIndexScratch[e]=r;const n=this.runtime.emitters[r];this.positionScratch.copy(s.emitterPositionOffsets[e]),!0!==n.localSpace&&null==n.parentEmitter&&this.positionScratch.add(t),this.runtime.queueEmitterState(r,this.positionScratch,!0,!0)}return this.runtime.mapParticlePages(n,s.emitterIndexScratch,s.emitterCapacities),this.scheduler.play(r),l}destroy(e){const t=this.activeByHandleSlot[e.handle.slot];if(null==t||t.handle.generation!==e.handle.generation)return!1;const s=this.pools[t.templateIndex],i=s.template.emitters.length;for(let e=0;e<i;e++){const r=s.baseEmitterIndex+t.logicalSlot*i+e;this.runtime.queueEmitterState(r,this.runtime.emitters[r].position,!1)}for(const e of t.pages)this.activePages.delete(e);return this.activePagesDirty=!0,s.freeSlots.push(t.logicalSlot),this.activeByHandleSlot[e.handle.slot]=null,this.scheduler.destroyInstance(e.handle)}setPosition(e,t){const s=this.resolve(e);if(null==s)return!1;const i=this.pools[s.templateIndex];for(let e=0;e<i.template.emitters.length;e++){const r=this.emitterIndex(s,i,e);!0!==this.runtime.emitters[r].localSpace&&null==this.runtime.emitters[r].parentEmitter&&(this.positionScratch.copy(t).add(i.emitterPositionOffsets[e]),this.runtime.queueEmitterState(r,this.positionScratch,!0))}return!0}setTransform(e,t){const s=this.resolve(e);if(null==s)return!1;const i=this.pools[s.templateIndex];for(let e=0;e<i.template.emitters.length;e++)this.runtime.setEmitterTransform(this.emitterIndex(s,i,e),t);return!0}pause(e){const t=this.resolve(e);return!(null==t||!this.scheduler.pause(t.handle))&&(this.forEachEmitter(t,e=>this.runtime.queueEmitterPaused(e,!0)),!0)}play(e){const t=this.resolve(e);return!(null==t||!this.scheduler.play(t.handle))&&(this.forEachEmitter(t,e=>this.runtime.queueEmitterPaused(e,!1)),!0)}restart(e){const t=this.resolve(e);return!(null==t||!this.scheduler.restart(t.handle))&&(t.stoppingDeadline=Number.POSITIVE_INFINITY,this.forEachEmitter(t,e=>{this.runtime.queueEmitterState(e,this.runtime.emitters[e].position,!0,!0)}),!0)}stop(e,t){const s=this.resolve(e),i=null==s?null:this.scheduler.getInstance(s.handle);if(null==s||null==i||!this.scheduler.stop(s.handle))return!1;const r=this.pools[s.templateIndex];return s.stoppingDeadline=i.elapsedTime+(null==t?r.maximumParticleLife:Math.max(0,t)),this.forEachEmitter(s,e=>this.runtime.queueEmitterStop(e,t)),!0}setTimescale(e,t){const s=this.resolve(e);return!(null==s||!this.scheduler.setTimescale(s.handle,t))&&(this.forEachEmitter(s,e=>this.runtime.queueEmitterTimescale(e,t)),!0)}setInput(e,t,s){const i=this.resolve(e);if(null==i)return!1;const r=this.pools[i.templateIndex];let n=!1;for(const e of r.template.inputBindings)e.inputId===t&&(n=this.runtime.setEmitterBoundValue(this.emitterIndex(i,r,e.emitterIndex),e,s)||n);return n&&this.scheduler.setDuration(i.handle,this.calculateInstanceDuration(i,r)),n}has(e){return null!=this.resolve(e)}update(e){this.scheduler.update(e),this.runtime.update(e);for(const e of this.activeByHandleSlot){if(null==e)continue;const t=this.scheduler.getInstance(e.handle);if(null==t)continue;const s=this.pools[e.templateIndex];(!1===t.playing&&t.elapsedTime>=s.template.duration||t.stopping&&t.elapsedTime>=e.stoppingDeadline)&&this.destroy(e)}this.flushActivePages()}flushActivePages(){this.activePagesDirty&&(this.runtime.setActivePages(this.activePages),this.activePagesDirty=!1)}dispose(){this.runtime.dispose();for(const e of this.activeByHandleSlot)null!=e&&this.scheduler.destroyInstance(e.handle);this.activeByHandleSlot.length=0}resolve(e){const t=this.activeByHandleSlot[e.handle.slot];return null!=t&&t.handle.generation===e.handle.generation&&null!=this.scheduler.getInstance(t.handle)?t:null}emitterIndex(e,t,s){return t.baseEmitterIndex+e.logicalSlot*t.template.emitters.length+s}forEachEmitter(e,t){const s=this.pools[e.templateIndex];for(let i=0;i<s.template.emitters.length;i++)t(this.emitterIndex(e,s,i))}calculateInstanceDuration(e,t){let s=0;for(let i=0;i<t.template.emitters.length;i++){const r=this.runtime.emitters[this.emitterIndex(e,t,i)],n="continuous"===r.rate.type?r.rate.duration<0?Number.POSITIVE_INFINITY:r.rate.delay+r.rate.duration+r.life:r.rate.delay+r.life;s=Math.max(s,n)}return s}}/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -0,0 +1,3 @@
1
+ /** Smallest power-of-two reservation class that can contain the requested instances. */
2
+ export declare function gpuVfxCapacityClass(required: number): number;
3
+ //# sourceMappingURL=gpu-vfx-capacity.d.ts.map
@@ -0,0 +1,4 @@
1
+ export function gpuVfxCapacityClass(t){let r=1;for(;r<t&&r<1073741824;)r*=2;return r}/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -0,0 +1,52 @@
1
+ import { GpuParticleArena, type GpuParticleArenaHandle } from './gpu-particle-arena.js';
2
+ export type GpuVfxInstanceHandle = GpuParticleArenaHandle;
3
+ export type GpuVfxInstanceDescriptor = Readonly<{
4
+ particleCapacity: number;
5
+ duration?: number;
6
+ looping?: boolean;
7
+ }>;
8
+ export type GpuVfxScheduledInstance = Readonly<{
9
+ handle: GpuVfxInstanceHandle;
10
+ particleCapacity: number;
11
+ elapsedTime: number;
12
+ playing: boolean;
13
+ stopping: boolean;
14
+ timescale: number;
15
+ }>;
16
+ export type GpuVfxSchedulerStats = Readonly<{
17
+ activeInstances: number;
18
+ playingInstances: number;
19
+ arena: ReturnType<GpuParticleArena['getStats']>;
20
+ }>;
21
+ /**
22
+ * Shared lifecycle and allocation owner for GPU VFX instances.
23
+ *
24
+ * There is intentionally one scheduler per rendered world. VFX actors retain
25
+ * only generational handles; emitter data, page ownership, simulation dispatch,
26
+ * and render-class batching belong here. This initial layer owns lifecycle and
27
+ * arena pages. GPU buffer/page-table materialization is added behind this API.
28
+ */
29
+ export declare class GpuVfxScheduler {
30
+ readonly arena: GpuParticleArena;
31
+ private readonly instances;
32
+ private activeInstances;
33
+ private playingInstances;
34
+ constructor(options?: {
35
+ pageSize?: number;
36
+ maximumParticles?: number;
37
+ });
38
+ createInstance(descriptor: GpuVfxInstanceDescriptor): GpuVfxInstanceHandle | null;
39
+ play(handle: GpuVfxInstanceHandle): boolean;
40
+ pause(handle: GpuVfxInstanceHandle): boolean;
41
+ restart(handle: GpuVfxInstanceHandle): boolean;
42
+ stop(handle: GpuVfxInstanceHandle): boolean;
43
+ setTimescale(handle: GpuVfxInstanceHandle, timescale: number): boolean;
44
+ setDuration(handle: GpuVfxInstanceHandle, duration: number): boolean;
45
+ resize(handle: GpuVfxInstanceHandle, particleCapacity: number): boolean;
46
+ destroyInstance(handle: GpuVfxInstanceHandle): boolean;
47
+ update(deltaTime: number): void;
48
+ getInstance(handle: GpuVfxInstanceHandle): GpuVfxScheduledInstance | null;
49
+ getStats(): GpuVfxSchedulerStats;
50
+ private resolve;
51
+ }
52
+ //# sourceMappingURL=gpu-vfx-scheduler.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{GpuParticleArena as t}from"./gpu-particle-arena.js";export class GpuVfxScheduler{constructor(e={}){this.instances=[],this.activeInstances=0,this.playingInstances=0,this.arena=new t(e)}createInstance(t){const e=this.arena.allocate(t.particleCapacity);if(null==e)return null;const n=e.handle.slot;return this.instances[n]={generation:e.handle.generation,particleCapacity:t.particleCapacity,duration:Math.max(0,t.duration??Number.POSITIVE_INFINITY),elapsedTime:0,looping:!0===t.looping,playing:!1,stopping:!1,timescale:1},this.activeInstances+=1,e.handle}play(t){const e=this.resolve(t);return null!=e&&(e.playing||(this.playingInstances+=1),e.playing=!0,e.stopping=!1,!0)}pause(t){const e=this.resolve(t);return null!=e&&(e.playing&&(this.playingInstances-=1),e.playing=!1,!0)}restart(t){const e=this.resolve(t);return null!=e&&(e.elapsedTime=0,e.stopping=!1,!0)}stop(t){const e=this.resolve(t);return null!=e&&(e.stopping=!0,!0)}setTimescale(t,e){const n=this.resolve(t);return null!=n&&(n.timescale=Math.max(0,e),!0)}setDuration(t,e){const n=this.resolve(t);return null!=n&&(n.duration=Math.max(0,e),!0)}resize(t,e){const n=this.resolve(t);if(null==n)return!1;return null!=this.arena.resize(t,e)&&(n.particleCapacity=e,!0)}destroyInstance(t){const e=this.resolve(t);return null!=e&&(e.playing&&(this.playingInstances-=1),this.instances[t.slot]=null,this.activeInstances-=1,this.arena.release(t))}update(t){const e=Math.max(0,Math.min(t,.1));if(0!==e&&0!==this.playingInstances)for(const t of this.instances)null!=t&&t.playing&&(t.elapsedTime+=e*t.timescale,!t.looping&&t.elapsedTime>=t.duration&&(t.elapsedTime=t.duration,t.playing=!1,this.playingInstances-=1))}getInstance(t){const e=this.resolve(t);return null==e?null:{handle:t,particleCapacity:e.particleCapacity,elapsedTime:e.elapsedTime,playing:e.playing,stopping:e.stopping,timescale:e.timescale}}getStats(){return{activeInstances:this.activeInstances,playingInstances:this.playingInstances,arena:this.arena.getStats()}}resolve(t){const e=this.instances[t.slot];return e?.generation===t.generation&&this.arena.has(t)?e:null}}/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -0,0 +1,103 @@
1
+ import type { RendererCustomPassContext } from '@hology/webgpu-renderer';
2
+ import * as THREE from 'three';
3
+ import type { VfxAsset } from '../../scene/model.js';
4
+ import { GpuSpriteWorldRuntime, type GpuSpriteWorldInstance } from './gpu-sprite-world-runtime.js';
5
+ type TemplateEntry = {
6
+ asset: VfxAsset;
7
+ revision: string | number | undefined;
8
+ references: number;
9
+ reservedInstances: number;
10
+ retired: boolean;
11
+ };
12
+ type WorldEpoch = {
13
+ generation: number;
14
+ runtime: GpuSpriteWorldRuntime;
15
+ templateIndices: ReadonlyMap<TemplateEntry, number>;
16
+ reservations: ReadonlyMap<TemplateEntry, number>;
17
+ estimatedBytes: number;
18
+ retired: boolean;
19
+ disposalQueued: boolean;
20
+ };
21
+ export declare class GpuVfxEffectHandle {
22
+ private readonly epoch;
23
+ private readonly instance;
24
+ private released;
25
+ constructor(epoch: WorldEpoch, instance: GpuSpriteWorldInstance);
26
+ setPosition(position: THREE.Vector3): boolean;
27
+ setTransform(transform: THREE.Matrix4): boolean;
28
+ pause(): boolean;
29
+ play(): boolean;
30
+ restart(): boolean;
31
+ stop(): boolean;
32
+ expire(remainingLifeSeconds: number): boolean;
33
+ kill(): boolean;
34
+ setTimescale(timescale: number): boolean;
35
+ setInput(inputId: string, value: unknown): boolean;
36
+ release(): boolean;
37
+ get active(): boolean;
38
+ }
39
+ export declare class PreparedGpuVfxEffect {
40
+ private readonly entry;
41
+ private readonly spawnEntry;
42
+ private readonly releaseEntry;
43
+ private released;
44
+ constructor(entry: TemplateEntry, spawnEntry: (entry: TemplateEntry, position: THREE.Vector3) => GpuVfxEffectHandle | null, releaseEntry: (entry: TemplateEntry) => void);
45
+ spawn(position: THREE.Vector3): GpuVfxEffectHandle | null;
46
+ release(): void;
47
+ }
48
+ /**
49
+ * World-owned production bridge using demand-sized shared runtime generations.
50
+ *
51
+ * Preparing more concurrent actors grows reservations geometrically. A new
52
+ * shared generation becomes current without invalidating handles in the old
53
+ * generation; the old GPU resources drain and retire after submitted work.
54
+ */
55
+ export declare class GpuVfxWorldService {
56
+ private readonly view;
57
+ private readonly assetLoader;
58
+ private readonly assetsProvider;
59
+ private readonly assetResources;
60
+ private readonly shaderProvider;
61
+ private readonly templateCache;
62
+ private readonly renderClassCache;
63
+ private readonly entriesByAssetId;
64
+ private readonly entries;
65
+ private readonly epochs;
66
+ private readonly reportedCpuFallbacks;
67
+ private currentEpoch;
68
+ private nextGeneration;
69
+ private rebuildTail;
70
+ private unregisterRenderGraph;
71
+ private estimatedWorldBytes;
72
+ private profilingEnabled;
73
+ constructor();
74
+ /** Must be enabled before preparing effects. Intended for editor/diagnostic views only. */
75
+ setProfilingEnabled(enabled: boolean): void;
76
+ getStats(): {
77
+ particles: import("./gpu-sprite-runtime.js").GpuSpriteRuntimeStats;
78
+ scheduler: Readonly<{
79
+ activeInstances: number;
80
+ playingInstances: number;
81
+ arena: ReturnType<import("./gpu-particle-arena.js").GpuParticleArena["getStats"]>;
82
+ }>;
83
+ };
84
+ prepare(asset: VfxAsset, revision?: string | number): Promise<PreparedGpuVfxEffect | null>;
85
+ private createEntry;
86
+ private spawn;
87
+ private releaseEntry;
88
+ private retireEntry;
89
+ private queueRebuild;
90
+ private rebuildWorld;
91
+ private ensureRenderGraph;
92
+ private update;
93
+ private disposeRetiredEpochs;
94
+ private reportCpuFallback;
95
+ executeCompute(context: RendererCustomPassContext): void;
96
+ executeRender(context: RendererCustomPassContext, colorTarget?: string): void;
97
+ getSceneTextureReads(): readonly string[];
98
+ getComputeTextureReads(): readonly string[];
99
+ private get assets();
100
+ private get resolveCustomMaterial();
101
+ }
102
+ export {};
103
+ //# sourceMappingURL=gpu-vfx-world-service.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{__decorate as e,__metadata as t}from"tslib";import{Service as s}from"typedi";import{inject as r}from"../../gameplay/inject.js";import{AssetLoader as i}from"../../gameplay/services/asset-loader.js";import{ViewController as n}from"../../gameplay/services/render.js";import{AssetResourceLoader as a}from"../../scene/asset-resource-loader.js";import{AssetsProvider as o}from"../../scene/assets-provider.js";import{ShaderProvider as h}from"../../gameplay/services/shader-provider.js";import{createGpuSpriteShaderGraphResolver as l}from"./vfx-materializer.js";import{GpuSpriteRenderClassCache as u,hasGpuUnsupportedVfxInputBindings as c}from"./gpu-sprite-compiler.js";import{addGpuSpriteRenderGraphPasses as d}from"./gpu-sprite-render-graph.js";import{GpuSpriteWorldRuntime as p}from"./gpu-sprite-world-runtime.js";import{GpuSpriteTemplateCache as m}from"./gpu-sprite-template.js";import{gpuVfxCapacityClass as g}from"./gpu-vfx-capacity.js";export class GpuVfxEffectHandle{constructor(e,t){this.epoch=e,this.instance=t,this.released=!1}setPosition(e){return!this.released&&this.epoch.runtime.setPosition(this.instance,e)}setTransform(e){return!this.released&&this.epoch.runtime.setTransform(this.instance,e)}pause(){return!this.released&&this.epoch.runtime.pause(this.instance)}play(){return!this.released&&this.epoch.runtime.play(this.instance)}restart(){return!this.released&&this.epoch.runtime.restart(this.instance)}stop(){return!this.released&&this.epoch.runtime.stop(this.instance)}expire(e){return!this.released&&this.epoch.runtime.stop(this.instance,Math.max(0,e))}kill(){return!this.released&&this.epoch.runtime.stop(this.instance,0)}setTimescale(e){return!this.released&&this.epoch.runtime.setTimescale(this.instance,e)}setInput(e,t){return!this.released&&this.epoch.runtime.setInput(this.instance,e,t)}release(){return!this.released&&(this.released=!0,this.epoch.runtime.destroy(this.instance))}get active(){return!this.released&&this.epoch.runtime.has(this.instance)}}export class PreparedGpuVfxEffect{constructor(e,t,s){this.entry=e,this.spawnEntry=t,this.releaseEntry=s,this.released=!1}spawn(e){return this.released?null:this.spawnEntry(this.entry,e)}release(){this.released||(this.released=!0,this.releaseEntry(this.entry))}}let f=class{constructor(){this.view=r(n),this.assetLoader=r(i),this.assetsProvider=r(o),this.assetResources=r(a),this.shaderProvider=r(h),this.templateCache=new m,this.renderClassCache=new u,this.entriesByAssetId=new Map,this.entries=new Set,this.epochs=new Set,this.reportedCpuFallbacks=new Set,this.currentEpoch=null,this.nextGeneration=1,this.rebuildTail=Promise.resolve(!0),this.unregisterRenderGraph=null,this.estimatedWorldBytes=0,this.profilingEnabled=!1,this.view.onLateUpdate().subscribe(e=>this.update(e))}setProfilingEnabled(e){this.profilingEnabled=e}getStats(){const e=this.currentEpoch;return null==e?null:{particles:e.runtime.runtime.getStats(),scheduler:e.runtime.scheduler.getStats()}}async prepare(e,t){if("webgpu-experimental"!==this.view.requestedBackend)return null;if(c(e))return this.reportCpuFallback(e,t,["one or more authored VFX input bindings are not supported on GPU"]),null;let s=this.entriesByAssetId.get(e.id);if(null==s||s.asset!==e||s.revision!==t){const r=s;s={asset:e,revision:t,pending:(async()=>(null!=r&&await this.retireEntry(r.pending),this.createEntry(e,t)))()},this.entriesByAssetId.set(e.id,s)}const r=await s.pending;if(null==r)return this.reportCpuFallback(e,t,["GPU template preparation rejected this asset without a diagnostic"]),null;r.references+=1;const i=g(r.references),n=this.currentEpoch?.reservations.get(r)??0,a=r.reservedInstances;return i>r.reservedInstances&&(r.reservedInstances=i),n<i&&!await this.queueRebuild()?(r.references-=1,r.reservedInstances=a,this.reportCpuFallback(e,t,["the shared GPU VFX world could not grow within its memory budget or WebGPU device limits"]),null):new PreparedGpuVfxEffect(r,(e,t)=>this.spawn(e,t),e=>this.releaseEntry(e))}async createEntry(e,t){const s=await this.templateCache.resolve(e,{assets:this.assets,renderClassCache:this.renderClassCache,resolveCustomMaterial:this.resolveCustomMaterial,revision:t});if(null==s.template){const r=s.compatibility.issues.filter(e=>"error"===e.severity).map(e=>`${e.emitter}: ${e.message}`);return this.reportCpuFallback(e,t,r.length>0?r:s.reasons),null}const r={asset:e,revision:t,references:0,reservedInstances:1,retired:!1};return this.entries.add(r),r}spawn(e,t){let s=this.currentEpoch,r=s?.templateIndices.get(e);if(null==r&&e.retired)for(const t of this.epochs){const i=t.templateIndices.get(e);t.disposalQueued||null==i||(s=t,r=i)}if(null==s||null==r)return null;const i=s.runtime.spawn(r,t);return null==i?null:new GpuVfxEffectHandle(s,i)}releaseEntry(e){e.references=Math.max(0,e.references-1)}async retireEntry(e){const t=await e;null!=t&&(t.retired=!0,this.entries.delete(t))}queueRebuild(){const e=this.rebuildTail.then(()=>this.rebuildWorld()).catch(e=>(console.error("[Hology GPU VFX] Failed to rebuild the shared world generation.",e),!1));return this.rebuildTail=e,e}async rebuildWorld(){const e=[...this.entries].filter(e=>!e.retired);if(0===e.length)return null!=this.currentEpoch&&(this.currentEpoch.retired=!0,this.currentEpoch=null,this.disposeRetiredEpochs()),!0;if(null!=this.currentEpoch&&this.currentEpoch.templateIndices.size===e.length&&e.every(e=>(this.currentEpoch.reservations.get(e)??0)>=e.reservedInstances))return!0;const t=[...this.epochs].reduce((e,t)=>t===this.currentEpoch&&0===t.runtime.scheduler.getStats().activeInstances?e:e+t.estimatedBytes,0),s=Math.max(0,this.view.webgpuVfxWorldMemoryBudgetBytes-t),r=await p.create({templates:e.map(e=>({asset:e.asset,revision:e.revision,maximumInstances:e.reservedInstances})),assets:this.assets,templateCache:this.templateCache,renderClassCache:this.renderClassCache,resolveCustomMaterial:this.resolveCustomMaterial,particlePageSize:256,aliveCountReadback:this.profilingEnabled,device:this.view.experimentalWebGpuDevice??void 0,maximumStorageBytes:s});if(null==r)return!1;const i=r.runtime.getStats().storageBytes;if(i>this.view.webgpuVfxWorldMemoryBudgetBytes||t+i>this.view.webgpuVfxWorldMemoryBudgetBytes)return r.dispose(),console.warn(`[Hology GPU VFX] Refusing shared world growth: retained storage would become ${y(t+i)} MiB, exceeding the ${y(this.view.webgpuVfxWorldMemoryBudgetBytes)} MiB budget.`),!1;const n={generation:this.nextGeneration++,runtime:r,templateIndices:new Map(e.map((e,t)=>[e,t])),reservations:new Map(e.map(e=>[e,e.reservedInstances])),estimatedBytes:i,retired:!1,disposalQueued:!1};return null!=this.currentEpoch&&(this.currentEpoch.retired=!0),this.currentEpoch=n,this.epochs.add(n),this.estimatedWorldBytes+=i,this.ensureRenderGraph(),console.info(`[Hology GPU VFX] Shared world generation ${n.generation}: ${e.length} template(s), ${e.reduce((e,t)=>e+t.reservedInstances,0)} reserved instance(s), ${r.runtime.emitters.length} emitter slots, ${r.runtime.getStats().capacity.toLocaleString()} particle slots, ${y(i)} MiB estimated storage.`),this.disposeRetiredEpochs(),!0}ensureRenderGraph(){if(null!=this.unregisterRenderGraph)return;const e=(e,t)=>d(e,this,t,"WorldGpuVfx");e.getSceneTextureReads=()=>this.getSceneTextureReads(),this.unregisterRenderGraph=this.view.registerExperimentalWebGpuRenderGraphExtension(e)}update(e){for(const t of this.epochs)t.runtime.update(e);this.disposeRetiredEpochs()}disposeRetiredEpochs(){for(const e of this.epochs){if(!e.retired||e.disposalQueued||0!==e.runtime.scheduler.getStats().activeInstances||[...e.templateIndices.keys()].some(e=>e.retired&&e.references>0))continue;e.disposalQueued=!0,this.epochs.delete(e),this.estimatedWorldBytes=Math.max(0,this.estimatedWorldBytes-e.estimatedBytes);const t=()=>{e.runtime.dispose(),console.info(`[Hology GPU VFX] Retired shared world generation ${e.generation}, releasing ${y(e.estimatedBytes)} MiB estimated storage.`)},s=this.view.experimentalWebGpuDevice;null==s?t():s.queue.onSubmittedWorkDone().then(t,t)}}reportCpuFallback(e,t,s){const r=`${e.id}:${t??""}`;this.reportedCpuFallbacks.has(r)||(this.reportedCpuFallbacks.add(r),console.warn(`[Hology GPU VFX] Falling back to CPU for "${e.name}".`,s))}executeCompute(e){for(const t of this.epochs)t.runtime.runtime.executeCompute(e)}executeRender(e,t){for(const s of this.epochs)s.runtime.runtime.executeRender(e,t)}getSceneTextureReads(){const e=new Set;for(const t of this.epochs)if(t.runtime.runtime.hasActiveParticles)for(const s of t.runtime.runtime.getSceneTextureReads())e.add(s);return[...e]}getComputeTextureReads(){const e=new Set;for(const t of this.epochs)if(t.runtime.runtime.hasActiveParticles)for(const s of t.runtime.runtime.getComputeTextureReads())e.add(s);return[...e]}get assets(){return{getTexture:e=>this.assetLoader.getTextureByAssetId(e),getMesh:async e=>(await this.assetResources.getMesh(await this.assetsProvider.getAsset(e),{applyMaterials:!0,rescale:!0})).scene}}get resolveCustomMaterial(){return l(this.assetsProvider,this.assetResources,this.shaderProvider,{getAsset:e=>this.assetLoader.getAsset(e),getTexture:e=>this.assetLoader.getTextureByAssetId(e),getMaterial:e=>this.assetLoader.getMaterialByAssetId(e),getMesh:e=>this.assetLoader.getModelByAssetId(e).then(e=>e.scene)})}};f=e([s(),t("design:paramtypes",[])],f);export{f as GpuVfxWorldService};function y(e){return(e/1048576).toFixed(1)}/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -4,4 +4,15 @@ export * from './vfx-binding-runtime.js';
4
4
  export * from './vfx-input-runtime.js';
5
5
  export * from './vfx-actor.js';
6
6
  export * from './vfx-service.js';
7
+ export * from './vfx-runtime.js';
8
+ export * from './gpu-sprite-runtime.js';
9
+ export * from './gpu-sprite-compiler.js';
10
+ export * from './gpu-sprite-backend.js';
11
+ export * from './gpu-particle-arena.js';
12
+ export * from './gpu-vfx-scheduler.js';
13
+ export * from './gpu-sprite-scheduler-runtime.js';
14
+ export * from './gpu-sprite-template.js';
15
+ export * from './gpu-sprite-world-runtime.js';
16
+ export * from './gpu-sprite-render-graph.js';
17
+ export * from './gpu-vfx-world-service.js';
7
18
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,4 @@
1
- export*from"./vfx-asset.js";export*from"./vfx-materializer.js";export*from"./vfx-binding-runtime.js";export*from"./vfx-input-runtime.js";export*from"./vfx-actor.js";export*from"./vfx-service.js";/*
1
+ export*from"./vfx-asset.js";export*from"./vfx-materializer.js";export*from"./vfx-binding-runtime.js";export*from"./vfx-input-runtime.js";export*from"./vfx-actor.js";export*from"./vfx-service.js";export*from"./vfx-runtime.js";export*from"./gpu-sprite-runtime.js";export*from"./gpu-sprite-compiler.js";export*from"./gpu-sprite-backend.js";export*from"./gpu-particle-arena.js";export*from"./gpu-vfx-scheduler.js";export*from"./gpu-sprite-scheduler-runtime.js";export*from"./gpu-sprite-template.js";export*from"./gpu-sprite-world-runtime.js";export*from"./gpu-sprite-render-graph.js";export*from"./gpu-vfx-world-service.js";/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -1,4 +1,4 @@
1
- import{timeUniforms as t}from"../../shader-nodes/index.js";import*as e from"three";import{NodeShaderMaterial as i,uniformBool as r,uniformFloat as o,uniformVec2 as s,mix as a,uniformVec4 as n,attributeFloat as h,attributeVec3 as l,select as d,float as u,textureSampler2d as c,attributes as m,uniforms as y,rgba as g,vec4 as p,varying as x,vec2 as f,vec3 as v,cross as b,max as T,length as C}from"three-shader-graph";import{markMaterialUniformValuesNeedUpdate as V}from"../../rendering/node-material-webgpu-resolver.js";o("trailLength");const P=o("verticesPerNode"),w=o("minID"),F=o("maxID"),I=(o("dragTexture"),o("maxTrailLength")),D=s("textureTileFactor",new e.Vector2(1,1)),N=n("headColor",new e.Vector4),U=n("tailColor",new e.Vector4),A=r("taper",!1),M=(o("scale",1),h("nodeID")),z=h("nodeVertexID"),B=l("nodeCenter"),S=u(1e-4),H=F.subtract(M).divide(T(F.subtract(w),S)),j=d(A,H,u(0)),q=x(u(1).subtract(H).multiplyVec4(N).add(H.multiplyVec4(U))),L=y.cameraPosition.subtract(B),G=d(C(L).gt(S),L.divideScalar(T(C(L),S)),v(0,0,1)),O=l("nodeTangent"),Q=d(C(O).gt(S),O.divideScalar(T(C(O),S)),v(1,0,0)),R=o("width",1),W=b(Q,G),_=b(Q,v(0,1,0)),k=d(C(_).gt(S),_,b(Q,v(1,0,0))),E=d(C(W).gt(S),W.divideScalar(T(C(W),S)),k.divideScalar(T(C(k),S))).multiplyScalar(z.subtract(u(.5))).multiplyScalar(R),J=p(u(1).subtract(j).multiplyVec3(E).add(T(j,u(1)).multiplyVec3(B)),1),K=p(u(1).subtract(j).multiplyVec3(m.position).add(j.multiplyVec3(B)),1),X=y.projectionMatrix.multiply(y.viewMatrix).multiplyVec(J),Y=y.projectionMatrix.multiply(y.viewMatrix).multiplyVec(K);export const trailUV=x(f(H.multiply(D.x),z.divide(P).multiply(D.y).multiply(2)));export const trailDragUV=x(f(M.divide(I).multiply(D.x),z.divide(P).multiply(D.y).multiply(2)));const Z={position:new e.Vector3,direction:new e.Vector3,offset:new e.Vector3,worldOrientation:new e.Vector3,matrix4:new e.Matrix4,quat1:new e.Quaternion,quat2:new e.Quaternion,quaternion:new e.Quaternion,quaternion2:new e.Quaternion,boundingBox:new e.Box3,localHeadGeometry:Array.from({length:128},()=>new e.Vector3)};export class Trail extends e.Object3D{constructor(t,i){super(),this.historyCount=0,this.historyHeadIndex=-1,this.history=[],this.renderNodeCount=0,this.subdivisions=4,this.currentNodeID=0,this.advance=function(){const t=new e.Matrix4;return function(){this.targetObject.updateMatrixWorld(),t.copy(this.targetObject.matrixWorld),this.advanceWithTransform(t),this.updateUniforms()}}(),this.connectNodes=(()=>{const t={attribute:null,offset:0,count:-1};return(e,i)=>{const r=this.geometry.getIndex();for(let t=0;t<this.localHeadGeometry.length-1;t++){const o=this.VerticesPerNode*e+t,s=this.VerticesPerNode*i+t,a=(e*this.FacesPerNode+t*Trail.FacesPerQuad)*Trail.IndicesPerFace;r.array[a]=o,r.array[a+1]=s,r.array[a+2]=o+1,r.array[a+3]=s,r.array[a+4]=s+1,r.array[a+5]=o+1}return r.needsUpdate=!0,r.clearUpdateRanges(),t.attribute=r,t.offset=e*this.FacesPerNode*Trail.IndicesPerFace,t.count=this.FacesPerNode*Trail.IndicesPerFace,t}})(),this.disconnectNodes=(()=>{const t={attribute:null,offset:0,count:-1};return e=>{const i=this.geometry.getIndex();for(let t=0;t<this.localHeadGeometry.length-1;t++){const r=(e*this.FacesPerNode+t*Trail.FacesPerQuad)*Trail.IndicesPerFace;i.array[r]=0,i.array[r+1]=0,i.array[r+2]=0,i.array[r+3]=0,i.array[r+4]=0,i.array[r+5]=0}return i.needsUpdate=!0,i.clearUpdateRanges(),t.attribute=i,t.offset=e*this.FacesPerNode*Trail.IndicesPerFace,t.count=this.FacesPerNode*Trail.IndicesPerFace,t}})(),this.active=!1,this.orientToMovement=!1,i&&(this.orientToMovement=!0),this.scene=t,this.geometry=null,this.mesh=null,this.historyCount=0,this.historyHeadIndex=-1,this.history=[],this.renderNodeCount=0,this.currentNodeID=0,this.advanceFrequency=60,this.advancePeriod=1/this.advanceFrequency,this.lastAdvanceTime=0,this.paused=!1,this.pauseAdvanceUpdateTimeDiff=0}setAdvanceFrequency(t){this.advanceFrequency=t,this.advancePeriod=1/this.advanceFrequency}initialize(t,i,r,o,s,a,n=!1){this.deactivate(),this.destroyMesh(),this.length=i>0?i+1:0,this.dragTexture=r?1:0,this.targetObject=a,this.billboard=n,this.initializeLocalHeadGeometry(o,s),null!=t.uniforms.width&&(t.uniforms.width.value=o),this.renderNodeCount=this.length*this.subdivisions,this.historyCount=0,this.historyHeadIndex=-1,this.history=[];for(let t=0;t<this.length;t++)this.history.push({position:new e.Vector3,tangent:new e.Vector3(1,0,0),transformMatrix:new e.Matrix4,id:-1});this.setAllNodesToCurrentTargetPosition(),this.material=t,this.initializeGeometry(),this.initializeMesh(),this.material.uniforms.dragTexture&&(this.material.uniforms.trailLength.value=0),this.material.uniforms.minID.value=0,this.material.uniforms.maxID.value=0,this.material.uniforms.dragTexture&&(this.material.uniforms.dragTexture.value=this.dragTexture),this.material.uniforms.maxTrailLength&&(this.material.uniforms.maxTrailLength.value=this.length),this.material.uniforms.verticesPerNode&&(this.material.uniforms.verticesPerNode.value=this.VerticesPerNode),this.material.uniforms.textureTileFactor&&(this.material.uniforms.textureTileFactor.value=new e.Vector2(1,1)),this.material.uniforms.scale&&(this.material.uniforms.scale.value=1),this.reset()}initializeLocalHeadGeometry(t,i){if(this.localHeadGeometry=[],i){this.VerticesPerNode=0;for(let t=0;t<i.length&&t<Trail.MaxHeadVertices;t++){const r=i[t];if(r&&r instanceof e.Vector3){const t=new e.Vector3;t.copy(r),this.localHeadGeometry.push(t),this.VerticesPerNode++}}}else{const i=(t||1)/2;this.localHeadGeometry.push(new e.Vector3(-i,0,0)),this.localHeadGeometry.push(new e.Vector3(i,0,0)),this.VerticesPerNode=2}this.FacesPerNode=2*(this.VerticesPerNode-1),this.FaceIndicesPerNode=3*this.FacesPerNode}initializeGeometry(){this.vertexCount=this.renderNodeCount*this.VerticesPerNode,this.faceCount=this.renderNodeCount*this.FacesPerNode;const t=new e.BufferGeometry,i=new Float32Array(this.vertexCount),r=new Float32Array(this.vertexCount*this.VerticesPerNode),o=new Float32Array(this.vertexCount*Trail.PositionComponentCount),s=new Float32Array(this.vertexCount*Trail.PositionComponentCount),a=new Float32Array(this.vertexCount*Trail.UVComponentCount),n=new Uint32Array(this.faceCount*Trail.IndicesPerFace),h=new Float32Array(3*this.vertexCount),l=new e.BufferAttribute(h,3);l.setUsage(e.DynamicDrawUsage),t.setAttribute("nodeTangent",l);const d=new e.BufferAttribute(i,1);d.setUsage(e.DynamicDrawUsage),t.setAttribute("nodeID",d);const u=new e.BufferAttribute(r,1);u.setUsage(e.DynamicDrawUsage),t.setAttribute("nodeVertexID",u);const c=new e.BufferAttribute(s,Trail.PositionComponentCount);c.setUsage(e.DynamicDrawUsage),t.setAttribute("nodeCenter",c);const m=new e.BufferAttribute(o,Trail.PositionComponentCount);m.setUsage(e.DynamicDrawUsage),t.setAttribute("position",m);const y=new e.BufferAttribute(a,Trail.UVComponentCount);y.setUsage(e.DynamicDrawUsage),t.setAttribute("uv",y);const g=new e.BufferAttribute(n,1);g.setUsage(e.DynamicDrawUsage),t.setIndex(g),this.geometry=t}zeroVertices(){const t=this.geometry.getAttribute("position");for(let e=0;e<this.vertexCount;e++){const i=3*e;t.array[i]=0,t.array[i+1]=0,t.array[i+2]=0}t.needsUpdate=!0}zeroIndices(){if(null==this.geometry)throw"Geometry not initialized";const t=this.geometry.getIndex();for(let e=0;e<this.faceCount;e++){const i=3*e;t.array[i]=0,t.array[i+1]=0,t.array[i+2]=0}t.needsUpdate=!0,t.clearUpdateRanges()}formInitialFaces(){this.zeroIndices();const t=this.geometry.getIndex();for(let t=0;t<this.renderNodeCount-1;t++)this.connectNodes(t,t+1);t.needsUpdate=!0,t.clearUpdateRanges()}initializeMesh(){if(null==this.geometry)throw"Geometry not initialized";this.mesh=new e.Mesh(this.geometry,this.material),this.mesh.matrixAutoUpdate=!1,this.mesh.visible=!1}destroyMesh(){this.mesh&&(this.scene.remove(this.mesh),this.mesh=null)}reset(){this.historyCount=0,this.historyHeadIndex=-1,this.currentNodeID=0,this.formInitialFaces(),this.zeroVertices(),this.geometry.setDrawRange(0,0),this.setAllNodesToCurrentTargetPosition(),this.mesh&&(this.mesh.visible=!0)}setAllNodesToCurrentTargetPosition(){if(!this.targetObject)return;this.targetObject.updateMatrixWorld();const t=this.targetObject.matrixWorld,i=(new e.Vector3).setFromMatrixPosition(t),r=new e.Vector3(1,0,0).transformDirection(t);for(let e=0;e<this.length;e++)this.history[e].position.copy(i),this.history[e].tangent.copy(r),this.history[e].transformMatrix.copy(t),this.history[e].id=-1;this.historyCount=0,this.historyHeadIndex=-1}updateUniforms(){if(0===this.historyCount)return;const t=this.getControlPoint(0).id,e=this.getControlPoint(this.historyCount-1).id;this.material.uniforms.minID.value=t,this.material.uniforms.maxID.value=e,this.material.uniforms.trailLength&&(this.material.uniforms.trailLength.value=this.historyCount),this.material.uniforms.maxTrailLength&&(this.material.uniforms.maxTrailLength.value=this.length),this.material.uniforms.verticesPerNode&&(this.material.uniforms.verticesPerNode.value=this.VerticesPerNode),V(this.material)}advanceWithTransform(t){this.historyCount<this.length&&this.historyCount++,this.historyHeadIndex=(this.historyHeadIndex+1)%this.length;const e=this.history[this.historyHeadIndex];if(e.id=this.currentNodeID++,Z.position.set(0,0,0).applyMatrix4(t),e.position.copy(Z.position),e.transformMatrix.copy(t),this.billboard){const i=this.getControlPoint(this.historyCount-2);i&&!i.position.equals(Z.position)?e.tangent.copy(Z.position).sub(i.position).normalize():e.tangent.set(1,0,0).transformDirection(t)}this.rebuildGeometry()}updateHead(){if(0===this.historyCount)return;this.targetObject.updateMatrixWorld();const t=this.targetObject.matrixWorld,e=this.history[this.historyHeadIndex];if(Z.position.set(0,0,0).applyMatrix4(t),e.position.copy(Z.position),e.transformMatrix.copy(t),this.billboard){const i=this.getControlPoint(this.historyCount-2);i&&!i.position.equals(Z.position)?e.tangent.copy(Z.position).sub(i.position).normalize():e.tangent.set(1,0,0).transformDirection(t)}this.rebuildGeometry()}getControlPoint(t){if(t<0)return this.history[(this.historyHeadIndex-this.historyCount+1+this.length)%this.length];t>=this.historyCount&&(t=this.historyCount-1);const e=(this.historyHeadIndex-this.historyCount+1+this.length)%this.length;return this.history[(e+t)%this.length]}rebuildGeometry(){if(this.historyCount<2)return this.geometry.setDrawRange(0,0),void(this.mesh&&(this.mesh.visible=!1));const t=this.geometry.getAttribute("position"),i=this.geometry.getAttribute("nodeID"),r=this.geometry.getAttribute("nodeVertexID"),o=this.geometry.getAttribute("nodeCenter"),s=this.geometry.getAttribute("nodeTangent"),a=this.historyCount-1,n=a*this.subdivisions+1;let h=0;Z.boundingBox.makeEmpty();for(let e=0;e<a;e++){const n=this.getControlPoint(e-1),l=this.getControlPoint(e),d=this.getControlPoint(e+1),u=this.getControlPoint(e+2),c=e===a-1?this.subdivisions+1:this.subdivisions;for(let e=0;e<c;e++){const a=e/this.subdivisions,c=n.position,m=l.position,y=d.position,g=u.position,p=a*a,x=p*a,f=.5*(y.x-c.x),v=.5*(g.x-m.x),b=(2*(m.x-y.x)+f+v)*x+(-3*(m.x-y.x)-2*f-v)*p+f*a+m.x,T=.5*(y.y-c.y),C=.5*(g.y-m.y),V=(2*(m.y-y.y)+T+C)*x+(-3*(m.y-y.y)-2*T-C)*p+T*a+m.y,P=.5*(y.z-c.z),w=.5*(g.z-m.z),F=(2*(m.z-y.z)+P+w)*x+(-3*(m.z-y.z)-2*P-w)*p+P*a+m.z;Z.position.set(b,V,F);const I=l.id+(d.id-l.id)*a,D=Z.direction.copy(l.tangent).lerp(d.tangent,a).normalize();Z.matrix4.copy(l.transformMatrix),Z.quat1.setFromRotationMatrix(l.transformMatrix),Z.quat2.setFromRotationMatrix(d.transformMatrix),Z.quaternion.copy(Z.quat1).slerp(Z.quat2,a),Z.matrix4.makeRotationFromQuaternion(Z.quaternion),Z.matrix4.setPosition(Z.position);for(let e=0;e<this.VerticesPerNode;e++){const n=h*this.VerticesPerNode+e;i.array[n]=I,r.array[n]=e,o.array[3*n]=Z.position.x,o.array[3*n+1]=Z.position.y,o.array[3*n+2]=Z.position.z;const l=Z.localHeadGeometry[e].copy(this.localHeadGeometry[e]);if(this.billboard)s.array[3*n]=D.x,s.array[3*n+1]=D.y,s.array[3*n+2]=D.z,Z.boundingBox.expandByPoint(Z.position);else{if(l.applyMatrix4(Z.matrix4),this.orientToMovement){Z.worldOrientation.set(0,0,-1).applyQuaternion(Z.quaternion);const t=(3*p-2*a)*(m.x-y.x)+(x-p)*v+(3*p-4*a+1)*f,e=(3*p-2*a)*(m.y-y.y)+(x-p)*C+(3*p-4*a+1)*T,i=(3*p-2*a)*(m.z-y.z)+(x-p)*w+(3*p-4*a+1)*P;Z.direction.set(t,e,i).normalize(),Z.direction.lengthSq()>1e-4&&(Z.quaternion2.setFromUnitVectors(Z.worldOrientation,Z.direction),l.sub(Z.position).applyQuaternion(Z.quaternion2).add(Z.position))}t.array[3*n]=l.x,t.array[3*n+1]=l.y,t.array[3*n+2]=l.z,Z.boundingBox.expandByPoint(l)}}h++}}if(i.needsUpdate=!0,r.needsUpdate=!0,o.needsUpdate=!0,this.billboard){s.needsUpdate=!0,this.mesh.frustumCulled=!1;const t=this.material.uniforms.width?.5*this.material.uniforms.width.value:.5;Z.boundingBox.min.subScalar(t),Z.boundingBox.max.addScalar(t)}else t.needsUpdate=!0;this.geometry.boundingBox=this.geometry.boundingBox||new e.Box3,this.geometry.boundingBox.copy(Z.boundingBox),this.geometry.boundingSphere=this.geometry.boundingSphere||new e.Sphere,this.geometry.boundingBox.getBoundingSphere(this.geometry.boundingSphere),this.mesh.visible=!Z.boundingBox.isEmpty(),this.geometry.setDrawRange(0,(n-1)*this.FaceIndicesPerNode)}currentTime(){return performance.now()/1e3}pause(){this.paused||(this.paused=!0,this.pauseAdvanceUpdateTimeDiff=this.currentTime()-this.lastAdvanceTime)}resume(){this.paused&&(this.paused=!1,this.lastAdvanceTime=this.currentTime()-this.pauseAdvanceUpdateTimeDiff)}update(){if(!this.paused){const t=this.currentTime();this.lastAdvanceTime||(this.lastAdvanceTime=t),t-this.lastAdvanceTime>this.advancePeriod?(this.advance(),this.lastAdvanceTime=t):this.updateHead()}}deactivate(){this.active&&(this.scene.remove(this.mesh),this.active=!1)}activate(){this.active||(this.scene.add(this.mesh),this.active=!0)}static getTrailShaderNodes(t){return{position:t?X:Y,color:q,fraction:H}}static applyTrailShaderParameters(t){t.transparent=!0,t.alphaTest=.1,t.blending=e.CustomBlending,t.blendSrc=e.SrcAlphaFactor,t.blendDst=e.OneMinusSrcAlphaFactor,t.blendEquation=e.AddEquation,t.depthTest=!0,t.depthWrite=!1,t.side=e.DoubleSide}static createBaseMaterial(e,r,o,s=!1){let{position:n}=Trail.getTrailShaderNodes(s),h=a(N,U,H).rgba,l=h.a;if(null!=e){let i=trailUV;null!=r&&(i=i.add(f(t.elapsed.multiply(-r),0)));const s=c(e).sample(i);switch(o??"red"){case"none":break;case"red":l=l.multiply(s.r);break;case"alpha":l=l.multiply(s.a)}h=g(h.rgb.multiply(s.rgb),l)}const d=new i({position:n,opacity:l,color:h});return Trail.applyTrailShaderParameters(d),d}static get MaxHeadVertices(){return 128}static get LocalOrientationTangent(){return Trail._LocalOrientationTangent}static get LocalHeadOrigin(){return Trail._LocalHeadOrigin}static get PositionComponentCount(){return 3}static get UVComponentCount(){return 2}static get IndicesPerFace(){return 3}static get FacesPerQuad(){return 2}}Trail._LocalOrientationTangent=new e.Vector3(1,0,0),Trail._LocalHeadOrigin=new e.Vector3(0,0,0),Trail.Shader={get BaseVertexVars(){return["attribute float nodeID;","attribute float nodeVertexID;","attribute vec3 nodeCenter;","uniform float minID;","uniform float maxID;","uniform float trailLength;","uniform float maxTrailLength;","uniform float verticesPerNode;","uniform vec2 textureTileFactor;","uniform bool taper;","uniform vec4 headColor;","uniform vec4 tailColor;","varying vec4 vColor;"].join("\n")},get TexturedVertexVars(){return[this.BaseVertexVars,"varying vec2 vUV;","uniform float dragTexture;"].join("\n")},BaseFragmentVars:["varying vec4 vColor;","uniform sampler2D trailTexture;"].join("\n"),get TexturedFragmentVars(){return[this.BaseFragmentVars,"varying vec2 vUV;"].join("\n")},get VertexShaderCore(){return["float fraction = (maxID - nodeID) / (maxID - minID);","float fractionSize = taper ? fraction : 0.0;","vColor = (1.0 - fraction) * headColor + fraction * tailColor;","vec4 realPosition = vec4((1.0 - fractionSize) * position.xyz + fractionSize * nodeCenter.xyz, 1.0); "].join("\n")},get BaseVertexShader(){return[this.BaseVertexVars,"void main() { ",this.VertexShaderCore,"gl_Position = projectionMatrix * viewMatrix * realPosition;","}"].join("\n")},get BaseFragmentShader(){return[this.BaseFragmentVars,"void main() { ","gl_FragColor = vColor;","}"].join("\n")},get TexturedVertexShader(){return[this.TexturedVertexVars,"void main() { ",this.VertexShaderCore,"float s = 0.0;","float t = 0.0;","if (dragTexture == 1.0) { "," s = fraction * textureTileFactor.s; "," t = (nodeVertexID / verticesPerNode) * textureTileFactor.t;","} else { "," s = nodeID / maxTrailLength * textureTileFactor.s;"," t = (nodeVertexID / verticesPerNode) * textureTileFactor.t;","}","vUV = vec2(s, t); ","gl_Position = projectionMatrix * viewMatrix * realPosition;","}"].join("\n")},get TexturedFragmentShader(){return[this.TexturedFragmentVars,"void main() { ","vec4 textureColor = texture2D(trailTexture, vUV);","gl_FragColor = vColor * textureColor;","}"].join("\n")}};/*
1
+ import{timeUniforms as t}from"../../shader-nodes/index.js";import*as e from"three";import{NodeShaderMaterial as i,uniformBool as r,uniformFloat as o,uniformVec2 as s,mix as a,uniformVec4 as n,attributeFloat as h,attributeVec3 as l,select as d,float as u,textureSampler2d as c,attributes as m,uniforms as y,rgba as g,vec4 as p,varying as x,vec2 as f,vec3 as v,cross as b,max as T,length as C}from"three-shader-graph";import{markMaterialUniformValuesNeedUpdate as V}from"../../rendering/node-material-webgpu-resolver.js";o("trailLength");const P=o("verticesPerNode"),w=o("minID"),F=o("maxID"),I=(o("dragTexture"),o("maxTrailLength")),D=s("textureTileFactor",new e.Vector2(1,1)),N=n("headColor",new e.Vector4),U=n("tailColor",new e.Vector4),A=r("taper",!1),M=(o("scale",1),h("nodeID")),z=h("nodeVertexID"),B=l("nodeCenter"),S=u(1e-4),H=F.subtract(M).divide(T(F.subtract(w),S)),j=d(A,H,u(0)),q=x(u(1).subtract(H).multiplyVec4(N).add(H.multiplyVec4(U))),L=y.cameraPosition.subtract(B),G=d(C(L).gt(S),L.divideScalar(T(C(L),S)),v(0,0,1)),O=l("nodeTangent"),Q=d(C(O).gt(S),O.divideScalar(T(C(O),S)),v(1,0,0)),R=o("width",1),W=b(Q,G),_=b(Q,v(0,1,0)),k=d(C(_).gt(S),_,b(Q,v(1,0,0))),E=d(C(W).gt(S),W.divideScalar(T(C(W),S)),k.divideScalar(T(C(k),S))).multiplyScalar(z.subtract(u(.5))).multiplyScalar(R),J=p(u(1).subtract(j).multiplyVec3(E).add(T(j,u(1)).multiplyVec3(B)),1),K=p(u(1).subtract(j).multiplyVec3(m.position).add(j.multiplyVec3(B)),1),X=y.projectionMatrix.multiply(y.viewMatrix).multiplyVec(J),Y=y.projectionMatrix.multiply(y.viewMatrix).multiplyVec(K);export const trailUV=x(f(H.multiply(D.x),z.divide(P).multiply(D.y).multiply(2)));export const trailDragUV=x(f(M.divide(I).multiply(D.x),z.divide(P).multiply(D.y).multiply(2)));const Z={position:new e.Vector3,direction:new e.Vector3,offset:new e.Vector3,worldOrientation:new e.Vector3,matrix4:new e.Matrix4,quat1:new e.Quaternion,quat2:new e.Quaternion,quaternion:new e.Quaternion,quaternion2:new e.Quaternion,boundingBox:new e.Box3,localHeadGeometry:Array.from({length:128},()=>new e.Vector3)};export class Trail extends e.Object3D{constructor(t,i){super(),this.historyCount=0,this.historyHeadIndex=-1,this.history=[],this.renderNodeCount=0,this.subdivisions=4,this.currentNodeID=0,this.advance=function(){const t=new e.Matrix4;return function(){this.targetObject.updateMatrixWorld(),t.copy(this.targetObject.matrixWorld),this.advanceWithTransform(t),this.updateUniforms()}}(),this.connectNodes=(()=>{const t={attribute:null,offset:0,count:-1};return(e,i)=>{const r=this.geometry.getIndex();for(let t=0;t<this.localHeadGeometry.length-1;t++){const o=this.VerticesPerNode*e+t,s=this.VerticesPerNode*i+t,a=(e*this.FacesPerNode+t*Trail.FacesPerQuad)*Trail.IndicesPerFace;r.array[a]=o,r.array[a+1]=s,r.array[a+2]=o+1,r.array[a+3]=s,r.array[a+4]=s+1,r.array[a+5]=o+1}return r.needsUpdate=!0,r.clearUpdateRanges(),t.attribute=r,t.offset=e*this.FacesPerNode*Trail.IndicesPerFace,t.count=this.FacesPerNode*Trail.IndicesPerFace,t}})(),this.disconnectNodes=(()=>{const t={attribute:null,offset:0,count:-1};return e=>{const i=this.geometry.getIndex();for(let t=0;t<this.localHeadGeometry.length-1;t++){const r=(e*this.FacesPerNode+t*Trail.FacesPerQuad)*Trail.IndicesPerFace;i.array[r]=0,i.array[r+1]=0,i.array[r+2]=0,i.array[r+3]=0,i.array[r+4]=0,i.array[r+5]=0}return i.needsUpdate=!0,i.clearUpdateRanges(),t.attribute=i,t.offset=e*this.FacesPerNode*Trail.IndicesPerFace,t.count=this.FacesPerNode*Trail.IndicesPerFace,t}})(),this.active=!1,this.orientToMovement=!1,i&&(this.orientToMovement=!0),this.scene=t,this.geometry=null,this.mesh=null,this.historyCount=0,this.historyHeadIndex=-1,this.history=[],this.renderNodeCount=0,this.currentNodeID=0,this.advanceFrequency=60,this.advancePeriod=1/this.advanceFrequency,this.lastAdvanceTime=0,this.paused=!1,this.pauseAdvanceUpdateTimeDiff=0}setAdvanceFrequency(t){this.advanceFrequency=t,this.advancePeriod=1/this.advanceFrequency}initialize(t,i,r,o,s,a,n=!1){this.deactivate(),this.destroyMesh(),this.length=i>0?i+1:0,this.dragTexture=r?1:0,this.targetObject=a,this.billboard=n,this.initializeLocalHeadGeometry(o,s),null!=t.uniforms.width&&(t.uniforms.width.value=o),this.renderNodeCount=this.length*this.subdivisions,this.historyCount=0,this.historyHeadIndex=-1,this.history=[];for(let t=0;t<this.length;t++)this.history.push({position:new e.Vector3,tangent:new e.Vector3(1,0,0),transformMatrix:new e.Matrix4,id:-1});this.setAllNodesToCurrentTargetPosition(),this.material=t,this.initializeGeometry(),this.initializeMesh(),this.material.uniforms.dragTexture&&(this.material.uniforms.trailLength.value=0),this.material.uniforms.minID.value=0,this.material.uniforms.maxID.value=0,this.material.uniforms.dragTexture&&(this.material.uniforms.dragTexture.value=this.dragTexture),this.material.uniforms.maxTrailLength&&(this.material.uniforms.maxTrailLength.value=this.length),this.material.uniforms.verticesPerNode&&(this.material.uniforms.verticesPerNode.value=this.VerticesPerNode),this.material.uniforms.textureTileFactor&&(this.material.uniforms.textureTileFactor.value=new e.Vector2(1,1)),this.material.uniforms.scale&&(this.material.uniforms.scale.value=1),this.reset()}initializeLocalHeadGeometry(t,i){if(this.localHeadGeometry=[],i){this.VerticesPerNode=0;for(let t=0;t<i.length&&t<Trail.MaxHeadVertices;t++){const r=i[t];if(r&&r instanceof e.Vector3){const t=new e.Vector3;t.copy(r),this.localHeadGeometry.push(t),this.VerticesPerNode++}}}else{const i=(t||1)/2;this.localHeadGeometry.push(new e.Vector3(-i,0,0)),this.localHeadGeometry.push(new e.Vector3(i,0,0)),this.VerticesPerNode=2}this.FacesPerNode=2*(this.VerticesPerNode-1),this.FaceIndicesPerNode=3*this.FacesPerNode}initializeGeometry(){this.vertexCount=this.renderNodeCount*this.VerticesPerNode,this.faceCount=this.renderNodeCount*this.FacesPerNode;const t=new e.BufferGeometry,i=new Float32Array(this.vertexCount),r=new Float32Array(this.vertexCount*this.VerticesPerNode),o=new Float32Array(this.vertexCount*Trail.PositionComponentCount),s=new Float32Array(this.vertexCount*Trail.PositionComponentCount),a=new Float32Array(this.vertexCount*Trail.UVComponentCount),n=new Uint32Array(this.faceCount*Trail.IndicesPerFace),h=new Float32Array(3*this.vertexCount),l=new e.BufferAttribute(h,3);l.setUsage(e.DynamicDrawUsage),t.setAttribute("nodeTangent",l);const d=new e.BufferAttribute(i,1);d.setUsage(e.DynamicDrawUsage),t.setAttribute("nodeID",d);const u=new e.BufferAttribute(r,1);u.setUsage(e.DynamicDrawUsage),t.setAttribute("nodeVertexID",u);const c=new e.BufferAttribute(s,Trail.PositionComponentCount);c.setUsage(e.DynamicDrawUsage),t.setAttribute("nodeCenter",c);const m=new e.BufferAttribute(o,Trail.PositionComponentCount);m.setUsage(e.DynamicDrawUsage),t.setAttribute("position",m);const y=new e.BufferAttribute(a,Trail.UVComponentCount);y.setUsage(e.DynamicDrawUsage),t.setAttribute("uv",y);const g=new e.BufferAttribute(n,1);g.setUsage(e.DynamicDrawUsage),t.setIndex(g),this.geometry=t}zeroVertices(){const t=this.geometry.getAttribute("position");for(let e=0;e<this.vertexCount;e++){const i=3*e;t.array[i]=0,t.array[i+1]=0,t.array[i+2]=0}t.needsUpdate=!0}zeroIndices(){if(null==this.geometry)throw"Geometry not initialized";const t=this.geometry.getIndex();for(let e=0;e<this.faceCount;e++){const i=3*e;t.array[i]=0,t.array[i+1]=0,t.array[i+2]=0}t.needsUpdate=!0,t.clearUpdateRanges()}formInitialFaces(){this.zeroIndices();const t=this.geometry.getIndex();for(let t=0;t<this.renderNodeCount-1;t++)this.connectNodes(t,t+1);t.needsUpdate=!0,t.clearUpdateRanges()}initializeMesh(){if(null==this.geometry)throw"Geometry not initialized";this.mesh=new e.Mesh(this.geometry,this.material),this.mesh.matrixAutoUpdate=!1,this.mesh.visible=!1}destroyMesh(){this.mesh&&(this.scene.remove(this.mesh),this.mesh=null)}reset(){this.historyCount=0,this.historyHeadIndex=-1,this.currentNodeID=0,this.formInitialFaces(),this.zeroVertices(),this.geometry.setDrawRange(0,0),this.setAllNodesToCurrentTargetPosition(),this.mesh&&(this.mesh.visible=!0)}setAllNodesToCurrentTargetPosition(){if(!this.targetObject)return;this.targetObject.updateMatrixWorld();const t=this.targetObject.matrixWorld,i=(new e.Vector3).setFromMatrixPosition(t),r=new e.Vector3(1,0,0).transformDirection(t);for(let e=0;e<this.length;e++)this.history[e].position.copy(i),this.history[e].tangent.copy(r),this.history[e].transformMatrix.copy(t),this.history[e].id=-1;this.historyCount=0,this.historyHeadIndex=-1}updateUniforms(){if(0===this.historyCount)return;const t=this.getControlPoint(0).id,e=this.getControlPoint(this.historyCount-1).id;this.material.uniforms.minID.value=t,this.material.uniforms.maxID.value=e,this.material.uniforms.trailLength&&(this.material.uniforms.trailLength.value=this.historyCount),this.material.uniforms.maxTrailLength&&(this.material.uniforms.maxTrailLength.value=this.length),this.material.uniforms.verticesPerNode&&(this.material.uniforms.verticesPerNode.value=this.VerticesPerNode),V(this.material)}advanceWithTransform(t){this.historyCount<this.length&&this.historyCount++,this.historyHeadIndex=(this.historyHeadIndex+1)%this.length;const e=this.history[this.historyHeadIndex];if(e.id=this.currentNodeID++,Z.position.set(0,0,0).applyMatrix4(t),e.position.copy(Z.position),e.transformMatrix.copy(t),this.billboard){const i=this.getControlPoint(this.historyCount-2);i&&!i.position.equals(Z.position)?e.tangent.copy(Z.position).sub(i.position).normalize():e.tangent.set(1,0,0).transformDirection(t)}this.rebuildGeometry()}updateHead(){if(0===this.historyCount)return;this.targetObject.updateMatrixWorld();const t=this.targetObject.matrixWorld,e=this.history[this.historyHeadIndex];if(Z.position.set(0,0,0).applyMatrix4(t),e.position.copy(Z.position),e.transformMatrix.copy(t),this.billboard){const i=this.getControlPoint(this.historyCount-2);i&&!i.position.equals(Z.position)?e.tangent.copy(Z.position).sub(i.position).normalize():e.tangent.set(1,0,0).transformDirection(t)}this.rebuildGeometry()}getControlPoint(t){if(t<0)return this.history[(this.historyHeadIndex-this.historyCount+1+this.length)%this.length];t>=this.historyCount&&(t=this.historyCount-1);const e=(this.historyHeadIndex-this.historyCount+1+this.length)%this.length;return this.history[(e+t)%this.length]}rebuildGeometry(){if(this.historyCount<2)return this.geometry.setDrawRange(0,0),void(this.mesh&&(this.mesh.visible=!1));const t=this.geometry.getAttribute("position"),i=this.geometry.getAttribute("nodeID"),r=this.geometry.getAttribute("nodeVertexID"),o=this.geometry.getAttribute("nodeCenter"),s=this.geometry.getAttribute("nodeTangent"),a=this.historyCount-1,n=a*this.subdivisions+1;let h=0;Z.boundingBox.makeEmpty();for(let e=0;e<a;e++){const n=this.getControlPoint(e-1),l=this.getControlPoint(e),d=this.getControlPoint(e+1),u=this.getControlPoint(e+2),c=e===a-1?this.subdivisions+1:this.subdivisions;for(let e=0;e<c;e++){const a=e/this.subdivisions,c=n.position,m=l.position,y=d.position,g=u.position,p=a*a,x=p*a,f=.5*(y.x-c.x),v=.5*(g.x-m.x),b=(2*(m.x-y.x)+f+v)*x+(-3*(m.x-y.x)-2*f-v)*p+f*a+m.x,T=.5*(y.y-c.y),C=.5*(g.y-m.y),V=(2*(m.y-y.y)+T+C)*x+(-3*(m.y-y.y)-2*T-C)*p+T*a+m.y,P=.5*(y.z-c.z),w=.5*(g.z-m.z),F=(2*(m.z-y.z)+P+w)*x+(-3*(m.z-y.z)-2*P-w)*p+P*a+m.z;Z.position.set(b,V,F);const I=l.id+(d.id-l.id)*a,D=Z.direction.copy(l.tangent).lerp(d.tangent,a).normalize();Z.matrix4.copy(l.transformMatrix),Z.quat1.setFromRotationMatrix(l.transformMatrix),Z.quat2.setFromRotationMatrix(d.transformMatrix),Z.quaternion.copy(Z.quat1).slerp(Z.quat2,a),Z.matrix4.makeRotationFromQuaternion(Z.quaternion),Z.matrix4.setPosition(Z.position);for(let e=0;e<this.VerticesPerNode;e++){const n=h*this.VerticesPerNode+e;i.array[n]=I,r.array[n]=e,o.array[3*n]=Z.position.x,o.array[3*n+1]=Z.position.y,o.array[3*n+2]=Z.position.z;const l=Z.localHeadGeometry[e].copy(this.localHeadGeometry[e]);if(this.billboard)s.array[3*n]=D.x,s.array[3*n+1]=D.y,s.array[3*n+2]=D.z,Z.boundingBox.expandByPoint(Z.position);else{if(l.applyMatrix4(Z.matrix4),this.orientToMovement){Z.worldOrientation.set(0,0,-1).applyQuaternion(Z.quaternion);const t=(3*p-2*a)*(m.x-y.x)+(x-p)*v+(3*p-4*a+1)*f,e=(3*p-2*a)*(m.y-y.y)+(x-p)*C+(3*p-4*a+1)*T,i=(3*p-2*a)*(m.z-y.z)+(x-p)*w+(3*p-4*a+1)*P;Z.direction.set(t,e,i).normalize(),Z.direction.lengthSq()>1e-4&&(Z.quaternion2.setFromUnitVectors(Z.worldOrientation,Z.direction),l.sub(Z.position).applyQuaternion(Z.quaternion2).add(Z.position))}t.array[3*n]=l.x,t.array[3*n+1]=l.y,t.array[3*n+2]=l.z,Z.boundingBox.expandByPoint(l)}}h++}}if(i.needsUpdate=!0,r.needsUpdate=!0,o.needsUpdate=!0,this.billboard){s.needsUpdate=!0,this.mesh.frustumCulled=!1;const t=this.material.uniforms.width?.5*this.material.uniforms.width.value:.5;Z.boundingBox.min.subScalar(t),Z.boundingBox.max.addScalar(t)}else t.needsUpdate=!0;this.geometry.boundingBox=this.geometry.boundingBox||new e.Box3,this.geometry.boundingBox.copy(Z.boundingBox),this.geometry.boundingSphere=this.geometry.boundingSphere||new e.Sphere,this.geometry.boundingBox.getBoundingSphere(this.geometry.boundingSphere),this.mesh.visible=!Z.boundingBox.isEmpty(),this.geometry.setDrawRange(0,(n-1)*this.FaceIndicesPerNode)}currentTime(){return performance.now()/1e3}pause(){this.paused||(this.paused=!0,this.pauseAdvanceUpdateTimeDiff=this.currentTime()-this.lastAdvanceTime)}resume(){this.paused&&(this.paused=!1,this.lastAdvanceTime=this.currentTime()-this.pauseAdvanceUpdateTimeDiff)}update(){if(!this.paused){const t=this.currentTime();this.lastAdvanceTime||(this.lastAdvanceTime=t),t-this.lastAdvanceTime>this.advancePeriod?(this.advance(),this.lastAdvanceTime=t):this.updateHead()}}deactivate(){this.active&&(this.scene.remove(this.mesh),this.active=!1)}activate(){this.active||(this.scene.add(this.mesh),this.active=!0)}static getTrailShaderNodes(t){return{position:t?X:Y,color:q,fraction:H}}static applyTrailShaderParameters(t){t.transparent=!0,t.alphaTest=1e-4,t.blending=e.CustomBlending,t.blendSrc=e.SrcAlphaFactor,t.blendDst=e.OneMinusSrcAlphaFactor,t.blendEquation=e.AddEquation,t.depthTest=!0,t.depthWrite=!1,t.side=e.DoubleSide}static createBaseMaterial(e,r,o,s=!1){let{position:n}=Trail.getTrailShaderNodes(s),h=a(N,U,H).rgba,l=h.a;if(null!=e){let i=trailUV;null!=r&&(i=i.add(f(t.elapsed.multiply(-r),0)));const s=c(e).sample(i);switch(o??"red"){case"none":break;case"red":l=l.multiply(s.r);break;case"alpha":l=l.multiply(s.a)}h=g(h.rgb.multiply(s.rgb),l)}const d=new i({position:n,opacity:l,color:h});return Trail.applyTrailShaderParameters(d),d}static get MaxHeadVertices(){return 128}static get LocalOrientationTangent(){return Trail._LocalOrientationTangent}static get LocalHeadOrigin(){return Trail._LocalHeadOrigin}static get PositionComponentCount(){return 3}static get UVComponentCount(){return 2}static get IndicesPerFace(){return 3}static get FacesPerQuad(){return 2}}Trail._LocalOrientationTangent=new e.Vector3(1,0,0),Trail._LocalHeadOrigin=new e.Vector3(0,0,0),Trail.Shader={get BaseVertexVars(){return["attribute float nodeID;","attribute float nodeVertexID;","attribute vec3 nodeCenter;","uniform float minID;","uniform float maxID;","uniform float trailLength;","uniform float maxTrailLength;","uniform float verticesPerNode;","uniform vec2 textureTileFactor;","uniform bool taper;","uniform vec4 headColor;","uniform vec4 tailColor;","varying vec4 vColor;"].join("\n")},get TexturedVertexVars(){return[this.BaseVertexVars,"varying vec2 vUV;","uniform float dragTexture;"].join("\n")},BaseFragmentVars:["varying vec4 vColor;","uniform sampler2D trailTexture;"].join("\n"),get TexturedFragmentVars(){return[this.BaseFragmentVars,"varying vec2 vUV;"].join("\n")},get VertexShaderCore(){return["float fraction = (maxID - nodeID) / (maxID - minID);","float fractionSize = taper ? fraction : 0.0;","vColor = (1.0 - fraction) * headColor + fraction * tailColor;","vec4 realPosition = vec4((1.0 - fractionSize) * position.xyz + fractionSize * nodeCenter.xyz, 1.0); "].join("\n")},get BaseVertexShader(){return[this.BaseVertexVars,"void main() { ",this.VertexShaderCore,"gl_Position = projectionMatrix * viewMatrix * realPosition;","}"].join("\n")},get BaseFragmentShader(){return[this.BaseFragmentVars,"void main() { ","gl_FragColor = vColor;","}"].join("\n")},get TexturedVertexShader(){return[this.TexturedVertexVars,"void main() { ",this.VertexShaderCore,"float s = 0.0;","float t = 0.0;","if (dragTexture == 1.0) { "," s = fraction * textureTileFactor.s; "," t = (nodeVertexID / verticesPerNode) * textureTileFactor.t;","} else { "," s = nodeID / maxTrailLength * textureTileFactor.s;"," t = (nodeVertexID / verticesPerNode) * textureTileFactor.t;","}","vUV = vec2(s, t); ","gl_Position = projectionMatrix * viewMatrix * realPosition;","}"].join("\n")},get TexturedFragmentShader(){return[this.TexturedFragmentVars,"void main() { ","vec4 textureColor = texture2D(trailTexture, vUV);","gl_FragColor = vColor * textureColor;","}"].join("\n")}};/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -2,13 +2,21 @@ import { BaseActor } from '../../gameplay/actors/actor.js';
2
2
  import { Asset } from '../../scene/model.js';
3
3
  import type { VfxClipEndBehavior } from '../sequence/sequence-data.js';
4
4
  import { Subject } from 'rxjs';
5
+ import { type VfxBackendPreference } from './gpu-sprite-compiler.js';
5
6
  export declare class VfxActor extends BaseActor {
6
7
  readonly onComplete: Subject<void>;
7
8
  /**
8
9
  * A multiplier for the speed of the visual effect.
9
10
  */
10
- timescale: number;
11
+ private _timescale;
12
+ get timescale(): number;
13
+ set timescale(value: number);
11
14
  paused: boolean;
15
+ /** Backend selected for the currently materialized effect. */
16
+ runtimeBackend: 'cpu' | 'gpu';
17
+ backendFallbackReasons: readonly string[];
18
+ profilingEnabled: boolean;
19
+ private simulationCpuMs;
12
20
  private system;
13
21
  private disposeSystem;
14
22
  private sourceAsset;
@@ -17,7 +25,10 @@ export declare class VfxActor extends BaseActor {
17
25
  private assetService;
18
26
  private world;
19
27
  private view;
28
+ private gpuWorld;
20
29
  private particleSystemContainer;
30
+ private gpuPrepared;
31
+ private gpuHandle;
21
32
  private physics;
22
33
  private shaderProvider;
23
34
  private inputRuntime;
@@ -27,7 +38,7 @@ export declare class VfxActor extends BaseActor {
27
38
  /**
28
39
  * Replace visual effect with its definition in an asset
29
40
  */
30
- fromAsset(asset: Asset, initialInputs?: Record<string, unknown>): Promise<void>;
41
+ fromAsset(asset: Asset, initialInputs?: Record<string, unknown>, backend?: VfxBackendPreference, templateRevision?: string | number): Promise<void>;
31
42
  setInput(inputNameOrId: string, value: unknown): void;
32
43
  setInputs(inputs: Record<string, unknown>): void;
33
44
  play(): void;
@@ -67,6 +78,12 @@ export declare class VfxActor extends BaseActor {
67
78
  *
68
79
  */
69
80
  getParticleCount(): number;
81
+ getProfilingStats(): {
82
+ particles: number;
83
+ drawCalls: number;
84
+ triangles: number;
85
+ cpuTimeMs: number;
86
+ };
70
87
  onEndPlay(): void;
71
88
  private updateCompletionState;
72
89
  private updateSystem;