@hology/core 0.0.262 → 0.0.264

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 (45) hide show
  1. package/dist/effects/sequence/sequence-player.d.ts +1 -0
  2. package/dist/effects/sequence/sequence-player.js +1 -1
  3. package/dist/effects/vfx/gpu-sprite-runtime.js +1 -1
  4. package/dist/effects/vfx/vfx-actor.js +1 -1
  5. package/dist/gameplay/input/input.d.ts +1 -0
  6. package/dist/gameplay/input/input.js +1 -1
  7. package/dist/gameplay/services/render.d.ts +3 -0
  8. package/dist/gameplay/services/render.js +1 -1
  9. package/dist/rendering/node-material-webgpu-resolver.d.ts +1 -0
  10. package/dist/rendering/node-material-webgpu-resolver.js +1 -1
  11. package/dist/rendering/webgpu-experimental-backend.d.ts +1 -0
  12. package/dist/rendering/webgpu-experimental-backend.js +1 -1
  13. package/dist/scene/assets-provider.d.ts +1 -0
  14. package/dist/scene/assets-provider.js +1 -1
  15. package/dist/scene/materializer.js +1 -1
  16. package/dist/scene/objects/data-asset.d.ts +6 -1
  17. package/dist/scene/objects/data-asset.js +1 -1
  18. package/dist/scene/runtime-asset-service.d.ts +5 -1
  19. package/dist/scene/runtime-asset-service.js +1 -1
  20. package/dist/scene/storage/packed-json.js +1 -1
  21. package/dist/secondary-motion/budget.d.ts +26 -0
  22. package/dist/secondary-motion/budget.js +4 -0
  23. package/dist/secondary-motion/cache.d.ts +16 -0
  24. package/dist/secondary-motion/cache.js +4 -0
  25. package/dist/secondary-motion/runtime.d.ts +15 -0
  26. package/dist/secondary-motion/runtime.js +1 -1
  27. package/dist/secondary-motion/skinned-targets.d.ts +35 -0
  28. package/dist/secondary-motion/skinned-targets.js +4 -0
  29. package/dist/test/data-asset-definition.test.js +1 -1
  30. package/dist/test/gpu-sprite-compiler.test.js +1 -1
  31. package/dist/test/input.test.js +1 -1
  32. package/dist/test/node-material-webgpu-resolver.test.js +1 -1
  33. package/dist/test/packed-json.test.js +1 -1
  34. package/dist/test/runtime-asset-service.test.js +1 -1
  35. package/dist/test/secondary-motion-budget.test.d.ts +2 -0
  36. package/dist/test/secondary-motion-budget.test.js +4 -0
  37. package/dist/test/secondary-motion-cache.test.d.ts +2 -0
  38. package/dist/test/secondary-motion-cache.test.js +4 -0
  39. package/dist/test/secondary-motion-gpu-check.d.ts +2 -0
  40. package/dist/test/secondary-motion-gpu-check.js +4 -0
  41. package/dist/test/sequence-scene-binding.test.js +1 -1
  42. package/dist/test/vfx-playback-transform.test.d.ts +2 -0
  43. package/dist/test/vfx-playback-transform.test.js +4 -0
  44. package/package.json +4 -1
  45. package/tsconfig.tsbuildinfo +1 -1
@@ -1,10 +1,15 @@
1
1
  import type { Asset } from '../model.js';
2
+ import { Observable } from 'rxjs';
2
3
  export declare class DataAssetRef<T = unknown> {
3
4
  readonly asset: Asset;
4
5
  readonly value: T;
6
+ /** Fresh snapshots on saved changes. Unsubscribe when the consumer is destroyed. */
7
+ readonly onChange: Observable<DataAssetRef<T>>;
5
8
  readonly id: string;
6
9
  readonly name: string;
7
10
  readonly definitionId: string;
8
- constructor(asset: Asset, value: T);
11
+ constructor(asset: Asset, value: T,
12
+ /** Fresh snapshots on saved changes. Unsubscribe when the consumer is destroyed. */
13
+ onChange?: Observable<DataAssetRef<T>>);
9
14
  }
10
15
  //# sourceMappingURL=data-asset.d.ts.map
@@ -1,4 +1,4 @@
1
- export class DataAssetRef{constructor(t,s){this.asset=t,this.value=s,this.id=t.id,this.name=t.name,this.definitionId=t.dataAsset?.definition??""}}/*
1
+ import{EMPTY as t}from"rxjs";export class DataAssetRef{constructor(s,i,e=t){this.asset=s,this.value=i,this.onChange=e,this.id=s.id,this.name=s.name,this.definitionId=s.dataAsset?.definition??""}}/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -10,14 +10,18 @@ export declare class RuntimeAssetsService extends AssetsProvider {
10
10
  private readonly liveUpdateQueues;
11
11
  private readonly hot;
12
12
  private readonly livePrefabParametersHandler;
13
+ private readonly liveDataAssetHandler;
14
+ readonly supportsLiveDataAssets: boolean;
13
15
  readonly supportsLivePrefabParameters: boolean;
14
16
  onCreate: Subject<Asset>;
15
17
  onDelete: Subject<Asset>;
16
18
  onUpdate: Subject<Asset>;
17
- constructor(backend: RuntimeBackendService, livePrefabParametersEnabled?: boolean);
19
+ constructor(backend: RuntimeBackendService, livePrefabParametersEnabled?: boolean, liveDataAssetsEnabled?: boolean);
18
20
  getAssets(): Promise<Asset[]>;
19
21
  getAsset(id: string): Promise<Asset>;
20
22
  private loadAssets;
23
+ /** Updates the catalog; subscribed consumers decide how to apply the change. */
24
+ applyLiveDataAsset(asset: Asset): Promise<boolean>;
21
25
  dispose(): void;
22
26
  private queueLivePrefabParameters;
23
27
  }
@@ -1,4 +1,4 @@
1
- import{AssetsProvider as e}from"../scene/assets-provider.js";import{Subject as t}from"rxjs";export class RuntimeAssetsService extends e{constructor(e,s=!0){super(),this.backend=e,this.assets=new Map,this.loaded=!1,this.loadingAssets=null,this.liveUpdateQueues=new Map,this.hot=import.meta.hot,this.livePrefabParametersHandler=e=>{(function(e){const t=e;return 1===t?.version&&"prefab"===t.asset?.type&&null!=t.asset.prefab})(e)&&this.queueLivePrefabParameters(e.asset)},this.onCreate=new t,this.onDelete=new t,this.onUpdate=new t,this.supportsLivePrefabParameters=s&&null!=this.hot,this.supportsLivePrefabParameters&&this.hot.on("hology:prefab-parameters",this.livePrefabParametersHandler)}async getAssets(){return this.loaded||(this.loadingAssets??(this.loadingAssets=this.loadAssets()),await this.loadingAssets),Array.from(this.assets.values())}async getAsset(e){const t=this.assets.get(e);if(null!=t)return t;const s=await async function(e,t){r.has(e)||r.set(e,t(e));return r.get(e)}(e,()=>this.backend.getAsset(e));return null!=s&&this.assets.set(e,s),s}async loadAssets(){try{const e=await this.backend.getAssets();for(const t of e)this.assets.set(t.id,t);return this.loaded=!0,e}finally{this.loadingAssets=null}}async applyLivePrefabParameters(e){if("prefab"!==e?.type||null==e.prefab)return!1;await this.getAssets();const t=this.assets.get(e.id);if("prefab"!==t?.type||null==t.prefab)return console.warn(`Unable to apply live prefab parameters for ${e.name??e.id}: the prefab is not loaded.`),!1;const a=function(e,t){if(e.id!==t.id||"prefab"!==e.type||"prefab"!==t.type||null==e.prefab||null==t.prefab||e.prefab.mainActorId!==t.prefab.mainActorId)return null;const a=s(e.prefab.objects,t.prefab.objects);if(null==a)return null;return{...e,prefab:{...e.prefab,objects:a}}}(t,e);return null==a?(console.warn(`Unable to apply live prefab parameters for ${e.name??e.id}: its structure changed. Respawn or restart to use the saved structure.`),!1):(JSON.stringify(t.prefab.eventGraph)!==JSON.stringify(e.prefab.eventGraph)&&console.warn(`Live event graph updates are not supported for prefab ${e.name??e.id}. The running graph was preserved.`),this.assets.set(a.id,a),this.onUpdate.next(a),!0)}dispose(){this.supportsLivePrefabParameters&&this.hot.off("hology:prefab-parameters",this.livePrefabParametersHandler),this.liveUpdateQueues.clear()}queueLivePrefabParameters(e){const t=(this.liveUpdateQueues.get(e.id)??Promise.resolve()).catch(()=>{}).then(()=>this.applyLivePrefabParameters(e)).then(()=>{},t=>{console.warn(`Unable to apply live prefab parameters for ${e.name??e.id}.`,t)});this.liveUpdateQueues.set(e.id,t),t.finally(()=>{this.liveUpdateQueues.get(e.id)===t&&this.liveUpdateQueues.delete(e.id)})}}function s(e,t){if(e.length!==t.length)return null;const r=[];for(let n=0;n<e.length;n++){const i=e[n],o=t[n];if(null==i||null==o||i.id!==o.id||i.type!==o.type||"prefab"===i.type&&i.assetId!==o.assetId)return null;const l=s(i.children??[],o.children??[]);if(null==l)return null;let p={...i,children:null!=i.children||null!=o.children?l:void 0};if("actor"===i.type){if(i.actor?.type!==o.actor?.type||!a(i.actor?.components,o.actor?.components))return null;const e=i.actor?.components??[],t=o.actor?.components??[];p={...p,actor:{...i.actor,params:o.actor?.params,innerParams:o.actor?.innerParams,components:e.map((e,s)=>({...e,params:t[s]?.params,innerParams:t[s]?.innerParams}))}}}else"prefab"===i.type&&(p={...p,prefab:{...i.prefab,params:o.prefab?.params,innerParams:o.prefab?.innerParams}});r.push(p)}return r}function a(e,t){const s=e??[],a=t??[];return s.length===a.length&&s.every((e,t)=>e.type===a[t]?.type)}const r=new Map;/*
1
+ import{AssetsProvider as e}from"../scene/assets-provider.js";import{Subject as t}from"rxjs";export class RuntimeAssetsService extends e{constructor(e,s=!0,a=!0){super(),this.backend=e,this.assets=new Map,this.loaded=!1,this.loadingAssets=null,this.liveUpdateQueues=new Map,this.hot=import.meta.hot,this.livePrefabParametersHandler=e=>{(function(e){const t=e;return 1===t?.version&&"prefab"===t.asset?.type&&null!=t.asset.prefab})(e)&&this.queueLivePrefabParameters(e.asset)},this.liveDataAssetHandler=e=>{const t=e;1===t?.version&&"data"===t.asset?.type&&"string"==typeof t.asset.id&&null!=t.asset.dataAsset&&this.applyLiveDataAsset(t.asset).catch(e=>console.warn("Unable to reload data asset",e))},this.supportsLiveDataAssets=!1,this.supportsLivePrefabParameters=!1,this.onCreate=new t,this.onDelete=new t,this.onUpdate=new t,this.supportsLiveDataAssets=a&&null!=this.hot,this.supportsLiveDataAssets&&this.hot.on("hology:data-asset",this.liveDataAssetHandler),this.supportsLivePrefabParameters=s&&null!=this.hot,this.supportsLivePrefabParameters&&this.hot.on("hology:prefab-parameters",this.livePrefabParametersHandler)}async getAssets(){return this.loaded||(this.loadingAssets??(this.loadingAssets=this.loadAssets()),await this.loadingAssets),Array.from(this.assets.values())}async getAsset(e){const t=this.assets.get(e);if(null!=t)return t;const s=await async function(e,t){r.has(e)||r.set(e,t(e));return r.get(e)}(e,()=>this.backend.getAsset(e));return null!=s&&this.assets.set(e,s),s}async loadAssets(){try{const e=await this.backend.getAssets();for(const t of e)this.assets.set(t.id,t);return this.loaded=!0,e}finally{this.loadingAssets=null}}async applyLivePrefabParameters(e){if("prefab"!==e?.type||null==e.prefab)return!1;await this.getAssets();const t=this.assets.get(e.id);if("prefab"!==t?.type||null==t.prefab)return console.warn(`Unable to apply live prefab parameters for ${e.name??e.id}: the prefab is not loaded.`),!1;const a=function(e,t){if(e.id!==t.id||"prefab"!==e.type||"prefab"!==t.type||null==e.prefab||null==t.prefab||e.prefab.mainActorId!==t.prefab.mainActorId)return null;const a=s(e.prefab.objects,t.prefab.objects);if(null==a)return null;return{...e,prefab:{...e.prefab,objects:a}}}(t,e);return null==a?(console.warn(`Unable to apply live prefab parameters for ${e.name??e.id}: its structure changed. Respawn or restart to use the saved structure.`),!1):(JSON.stringify(t.prefab.eventGraph)!==JSON.stringify(e.prefab.eventGraph)&&console.warn(`Live event graph updates are not supported for prefab ${e.name??e.id}. The running graph was preserved.`),this.assets.set(a.id,a),this.onUpdate.next(a),!0)}async applyLiveDataAsset(e){if("data"!==e?.type||null==e.dataAsset)return!1;await this.getAssets();const t=this.assets.get(e.id);return null!=t&&t.dataAsset?.definition!==e.dataAsset.definition?(console.warn(`Data asset ${e.name}: changing definition type requires a restart.`),!1):(this.assets.set(e.id,e),this.onUpdate.next(e),!0)}dispose(){this.supportsLiveDataAssets&&this.hot.off("hology:data-asset",this.liveDataAssetHandler),this.supportsLivePrefabParameters&&this.hot.off("hology:prefab-parameters",this.livePrefabParametersHandler),this.liveUpdateQueues.clear()}queueLivePrefabParameters(e){const t=(this.liveUpdateQueues.get(e.id)??Promise.resolve()).catch(()=>{}).then(()=>this.applyLivePrefabParameters(e)).then(()=>{},t=>{console.warn(`Unable to apply live prefab parameters for ${e.name??e.id}.`,t)});this.liveUpdateQueues.set(e.id,t),t.finally(()=>{this.liveUpdateQueues.get(e.id)===t&&this.liveUpdateQueues.delete(e.id)})}}function s(e,t){if(e.length!==t.length)return null;const r=[];for(let n=0;n<e.length;n++){const i=e[n],o=t[n];if(null==i||null==o||i.id!==o.id||i.type!==o.type||"prefab"===i.type&&i.assetId!==o.assetId)return null;const l=s(i.children??[],o.children??[]);if(null==l)return null;let p={...i,children:null!=i.children||null!=o.children?l:void 0};if("actor"===i.type){if(i.actor?.type!==o.actor?.type||!a(i.actor?.components,o.actor?.components))return null;const e=i.actor?.components??[],t=o.actor?.components??[];p={...p,actor:{...i.actor,params:o.actor?.params,innerParams:o.actor?.innerParams,components:e.map((e,s)=>({...e,params:t[s]?.params,innerParams:t[s]?.innerParams}))}}}else"prefab"===i.type&&(p={...p,prefab:{...i.prefab,params:o.prefab?.params,innerParams:o.prefab?.innerParams}});r.push(p)}return r}function a(e,t){const s=e??[],a=t??[];return s.length===a.length&&s.every((e,t)=>e.type===a[t]?.type)}const r=new Map;/*
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{gzipSync as r,gunzipSync as e,strFromU8 as t,strToU8 as n}from"fflate";const o="@hology/packed-json:1:gzip:base64:";export function serializeStorageJson(r){return JSON.stringify(a(r),null,2)}export function parseStorageJson(r){return JSON.parse(r,(r,e)=>f(e))}export function decodeStorageValue(r){const e=f(r);return e!==r?decodeStorageValue(e):Array.isArray(r)?r.map(r=>decodeStorageValue(r)):y(r)?Object.fromEntries(Object.entries(r).map(([r,e])=>[r,decodeStorageValue(e)])):r}function a(r){if(Array.isArray(r))return r.map(a);if(!y(r))return r;const e={};for(const[t,n]of Object.entries(r))"vertexMaterials"===t&&Array.isArray(n)?e[t]=l(n):"landscape"===t&&y(n)?e[t]=s(n):"paintedScatter"===t&&y(n)?e[t]=i(n):"secondaryMotion"===t&&y(n)?e[t]=c(n):e[t]=a(n);return e}function s(r){const e=u(r);return Array.isArray(r.heightMaps)&&(e.heightMaps=l(r.heightMaps)),Array.isArray(r.holes)&&(e.holes=l(r.holes)),e}function i(r){const e=u(r);return Array.isArray(r.cells)&&(e.cells=r.cells.map(r=>{if(!y(r))return a(r);const e=u(r);return Array.isArray(r.instances)&&(e.instances=l(r.instances)),e})),e}function c(r){const e=u(r);return Array.isArray(r.regions)&&(e.regions=r.regions.map(r=>{if(!y(r))return a(r);const e=u(r);return Array.isArray(r.paintedVertices)&&(e.paintedVertices=l(r.paintedVertices)),e})),e}function u(r){return Object.fromEntries(Object.entries(r).map(([r,e])=>[r,a(e)]))}function l(e){const t=JSON.stringify(e);return t.length<128?a(e):o+function(r){let e="";const t=32768;for(let n=0;n<r.length;n+=t)e+=String.fromCharCode(...r.subarray(n,n+t));return btoa(e)}(r(n(t),{level:9,mtime:0}))}function f(r){if("string"!=typeof r||!r.startsWith(o))return r;const n=r.slice(34);try{return JSON.parse(t(e(function(r){const e=atob(r),t=new Uint8Array(e.length);for(let r=0;r<e.length;r++)t[r]=e.charCodeAt(r);return t}(n))))}catch(r){const e=r instanceof Error?`${r.name}: ${r.message}`:String(r);throw Object.assign(new Error(`Failed to decode packed Hology storage data: ${e}`),{cause:r})}}function y(r){return null!=r&&"object"==typeof r&&!Array.isArray(r)}/*
1
+ import{gzipSync as r,gunzipSync as e,strFromU8 as t,strToU8 as n}from"fflate";const o="@hology/packed-json:1:gzip:base64:";export function serializeStorageJson(r){return JSON.stringify(a(r),null,2)}export function parseStorageJson(r){return JSON.parse(r,(r,e)=>f(e))}export function decodeStorageValue(r){const e=f(r);return e!==r?decodeStorageValue(e):Array.isArray(r)?r.map(r=>decodeStorageValue(r)):y(r)?Object.fromEntries(Object.entries(r).map(([r,e])=>[r,decodeStorageValue(e)])):r}function a(r){if(Array.isArray(r))return r.map(a);if(!y(r))return r;const e={};for(const[t,n]of Object.entries(r))"vertexMaterials"===t&&Array.isArray(n)?e[t]=l(n):"latticeDeformation"===t&&y(n)?e[t]=l(n,0):"landscape"===t&&y(n)?e[t]=s(n):"paintedScatter"===t&&y(n)?e[t]=i(n):"secondaryMotion"===t&&y(n)?e[t]=c(n):e[t]=a(n);return e}function s(r){const e=u(r);return Array.isArray(r.heightMaps)&&(e.heightMaps=l(r.heightMaps)),Array.isArray(r.holes)&&(e.holes=l(r.holes)),e}function i(r){const e=u(r);return Array.isArray(r.cells)&&(e.cells=r.cells.map(r=>{if(!y(r))return a(r);const e=u(r);return Array.isArray(r.instances)&&(e.instances=l(r.instances)),e})),e}function c(r){const e=u(r);return Array.isArray(r.regions)&&(e.regions=r.regions.map(r=>{if(!y(r))return a(r);const e=u(r);return Array.isArray(r.paintedVertices)&&(e.paintedVertices=l(r.paintedVertices)),e})),e}function u(r){return Object.fromEntries(Object.entries(r).map(([r,e])=>[r,a(e)]))}function l(e,t=128){const s=JSON.stringify(e);return s.length<t?a(e):o+function(r){let e="";const t=32768;for(let n=0;n<r.length;n+=t)e+=String.fromCharCode(...r.subarray(n,n+t));return btoa(e)}(r(n(s),{level:9,mtime:0}))}function f(r){if("string"!=typeof r||!r.startsWith(o))return r;const n=r.slice(34);try{return JSON.parse(t(e(function(r){const e=atob(r),t=new Uint8Array(e.length);for(let r=0;r<e.length;r++)t[r]=e.charCodeAt(r);return t}(n))))}catch(r){const e=r instanceof Error?`${r.name}: ${r.message}`:String(r);throw Object.assign(new Error(`Failed to decode packed Hology storage data: ${e}`),{cause:r})}}function y(r){return null!=r&&"object"==typeof r&&!Array.isArray(r)}/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -0,0 +1,26 @@
1
+ import { Camera, Object3D } from 'three';
2
+ export interface SecondaryMotionBudgetHandle {
3
+ target: Object3D;
4
+ enabled: boolean;
5
+ /** Positive priority reserves a slot before distance-based candidates. */
6
+ priority: number;
7
+ readonly active: boolean;
8
+ dispose(): void;
9
+ }
10
+ /** Selects once per simulation frame. Entries and selection scratch are persistent. */
11
+ export declare class SecondaryMotionBudget {
12
+ readonly maxCharacters: number;
13
+ readonly maxDistance: number;
14
+ private readonly entries;
15
+ private readonly frustum;
16
+ private readonly viewProjection;
17
+ private readonly sphere;
18
+ private readonly cameraPosition;
19
+ private selectionFrame;
20
+ private nextId;
21
+ private readonly selected;
22
+ constructor(maxCharacters?: number, maxDistance?: number);
23
+ register(target: Object3D): SecondaryMotionBudgetHandle;
24
+ select(frame: number, camera: Camera): void;
25
+ }
26
+ //# sourceMappingURL=budget.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{Frustum as e,Matrix4 as t,Sphere as i,Vector3 as s}from"three";export class SecondaryMotionBudget{constructor(r=8,o=24){if(this.maxCharacters=r,this.maxDistance=o,this.entries=[],this.frustum=new e,this.viewProjection=new t,this.sphere=new i(new s,2),this.cameraPosition=new s,this.selectionFrame=-1,this.nextId=0,!Number.isInteger(r)||r<0||!Number.isFinite(o)||o<0)throw new Error("Invalid secondary-motion budget");this.selected=new Array(r)}register(e){const t={target:e,enabled:!0,priority:0,active:!1,score:1/0,id:this.nextId++,dispose:()=>{const e=this.entries.indexOf(t);e>=0&&this.entries.splice(e,1);for(let e=0;e<this.selected.length;e++)this.selected[e]===t&&(this.selected[e]=void 0);t.active=!1,t.enabled=!1,this.selectionFrame=-1}};return this.entries.push(t),this.selectionFrame=-1,t}select(e,t){if(this.selectionFrame===e)return;this.selectionFrame=e,this.cameraPosition.setFromMatrixPosition(t.matrixWorld),this.viewProjection.multiplyMatrices(t.projectionMatrix,t.matrixWorldInverse),this.frustum.setFromProjectionMatrix(this.viewProjection);for(const e of this.entries){this.sphere.center.setFromMatrixPosition(e.target.matrixWorld);const t=this.sphere.center.distanceToSquared(this.cameraPosition),i=this.maxDistance*(e.active?1.15:1),s=e.enabled&&e.target.visible&&(e.priority>0||t<=i*i&&this.frustum.intersectsSphere(this.sphere));e.score=s?t*(e.active?.8:1):1/0}let i=0;for(const e of this.entries){if(!Number.isFinite(e.score))continue;let t=0;for(;t<i&&r(this.selected[t],e)<=0;)t++;if(t>=this.maxCharacters)continue;for(let e=Math.min(i,this.maxCharacters-1);e>t;e--)this.selected[e]=this.selected[e-1];this.selected[t]=e,i=Math.min(i+1,this.maxCharacters)}for(const e of this.entries)e.active=!1;for(let e=0;e<this.selected.length;e++)e<i?this.selected[e].active=!0:this.selected[e]=void 0}}function r(e,t){const i=Number.isFinite(e.score);return i!==Number.isFinite(t.score)?i?-1:1:t.priority-e.priority||e.score-t.score||e.id-t.id}/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -0,0 +1,16 @@
1
+ import type { BufferGeometry } from 'three';
2
+ import type { SecondaryMotionGeneratedRegion } from './data.js';
3
+ /** Borrowed immutable CPU data. GPU buffers and simulation state remain instance-owned. */
4
+ export interface SharedClothRegion {
5
+ readonly data: Readonly<SecondaryMotionGeneratedRegion>;
6
+ readonly activeParticleMask: Uint8Array;
7
+ readonly activeParticles: Uint32Array;
8
+ readonly hasSkinning: boolean;
9
+ }
10
+ /** Identity/version checks only on configuration/activation, never per simulation step. */
11
+ export declare class SharedClothRegionCache {
12
+ private readonly geometries;
13
+ get(geometry: BufferGeometry, id: string, paint?: readonly (readonly [number, number])[]): SharedClothRegion;
14
+ }
15
+ export declare const sharedClothRegionCache: SharedClothRegionCache;
16
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{generateClothSecondaryMotionRegion as t}from"./generation.js";const e=[],n=new WeakSet;export function isSharedClothRegionData(t){return n.has(t)}export class SharedClothRegionCache{constructor(){this.geometries=new WeakMap}get(i,s,a=e){const c=i.getAttribute("position");if(!c)throw new Error("Secondary-motion geometry needs positions");const l=i.getIndex(),u=i.getAttribute("skinIndex"),g=i.getAttribute("skinWeight");let h=this.geometries.get(i);h&&o(h.sources[0],c)&&o(h.sources[1],l)&&o(h.sources[2],u)&&o(h.sources[3],g)||(h={sources:[r(c),r(l),r(u),r(g)],variants:new Map},this.geometries.set(i,h));const f=h.variants.get(s);if(f&&function(t,e){if(t.length!==2*e.length)return!1;for(let n=0;n<e.length;n++)if(t[2*n]!==e[n][0]||t[2*n+1]!==e[n][1])return!1;return!0}(f.paint,a))return f.region;const d=new Float32Array(3*c.count);for(let t=0;t<c.count;t++)d[3*t]=c.getX(t),d[3*t+1]=c.getY(t),d[3*t+2]=c.getZ(t);const m=new Float32Array(c.count),y=new Float64Array(2*a.length);for(let t=0;t<a.length;t++){const[e,n]=a[t];y[2*t]=e,y[2*t+1]=n,e>=0&&e<m.length&&(m[e]=n)}const p=l?Uint32Array.from(l.array):Uint32Array.from({length:c.count},(t,e)=>e),v=null!=u&&null!=g,w=v?new Uint16Array(4*c.count):void 0,A=v?new Float32Array(4*c.count):void 0;if(w&&A)for(let t=0;t<c.count;t++)for(let e=0;e<4;e++)w[4*t+e]=u.getComponent(t,e),A[4*t+e]=g.getComponent(t,e);const z=t(s,{positions:d,indices:p},m,w,A);Object.freeze(z),n.add(z);const C=new Uint8Array(z.particleInverseMass.length);for(let t=0;t<C.length;t++)z.particleInverseMass[t]>0&&(C[t]=1);for(const t of z.constraints)C[t]=1;let S=0;for(const t of C)S+=t;const b=new Uint32Array(S);for(let t=0,e=0;t<C.length;t++)C[t]&&(b[e++]=t);const k={data:z,activeParticleMask:C,activeParticles:b,hasSkinning:v};return h.variants.set(s,{paint:y,region:k}),k}}function r(t){const e=i(t);return{attribute:t,array:e?.array??null,version:e?.version??0,count:t?.count??0,size:t?.itemSize??0,normalized:t?.normalized??!1}}function o(t,e){const n=i(e);return t.attribute===e&&t.array===(n?.array??null)&&t.version===(n?.version??0)&&t.count===(e?.count??0)&&t.size===(e?.itemSize??0)&&t.normalized===(e?.normalized??!1)}function i(t){return t?.isInterleavedBufferAttribute?t.data:t}export const sharedClothRegionCache=new SharedClothRegionCache;/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -2,7 +2,10 @@ import type { RendererCustomPassContext } from '@hology/webgpu-renderer';
2
2
  import type { BufferGeometry } from 'three';
3
3
  import type { ExperimentalWebGpuRenderGraphExtension } from '../rendering/webgpu-experimental-backend.js';
4
4
  import type { SecondaryMotionGeneratedRegion } from './data.js';
5
+ import { type SecondaryMotionSkinningTargets } from './skinned-targets.js';
6
+ export type { SecondaryMotionSkinningTargets } from './skinned-targets.js';
5
7
  export interface SecondaryMotionComputeRuntime {
8
+ readonly enabled?: boolean;
6
9
  execute(context: RendererCustomPassContext): void;
7
10
  dispose(): void;
8
11
  }
@@ -35,9 +38,15 @@ export interface WebGpuSecondaryMotionRegionHandle {
35
38
  readonly positionBuffer: GPUBuffer;
36
39
  setRenderGeometry(geometry: BufferGeometry | null): void;
37
40
  updateTargets(positions: Float32Array): void;
41
+ /** Installs persistent particle inputs and consumes the renderer's shared GPU bone palette. */
42
+ setSkinningTargets(source: SecondaryMotionSkinningTargets): void;
43
+ /** Resets particles to their current GPU-skinned pose, including animation seeks. */
44
+ resetToSkinningTargets(): void;
38
45
  reset(positions?: Float32Array): void;
39
46
  dispose(): void;
40
47
  }
48
+ /** Prepares immutable pipelines at loading time; simulation buffers stay instance-owned. */
49
+ export declare function prepareSecondaryMotionRuntime(device: GPUDevice): void;
41
50
  /**
42
51
  * First-stage GPU secondary-motion solver. Positions use vec4 storage so the
43
52
  * same buffers can later be consumed directly by render/skinning pipelines.
@@ -57,6 +66,8 @@ export declare class WebGpuSecondaryMotionRuntime implements SecondaryMotionComp
57
66
  private readonly previousCollisionSpheres;
58
67
  private readonly collisionParams;
59
68
  private readonly collisionColliderData;
69
+ private readonly collisionNextData;
70
+ private readonly collisionCountData;
60
71
  private collisionCollidersInitialized;
61
72
  private readonly collisionBindGroups;
62
73
  private readonly integrationParamsData;
@@ -64,12 +75,16 @@ export declare class WebGpuSecondaryMotionRuntime implements SecondaryMotionComp
64
75
  private readonly regions;
65
76
  private pendingSteps;
66
77
  private disposed;
78
+ private _enabled;
67
79
  private readonly constraintIterations;
68
80
  private readonly constraintStiffness;
69
81
  private readonly autoAdvance;
70
82
  private lastExecuteSeconds;
83
+ private skinningTargetKernels;
71
84
  constructor(device: GPUDevice, options?: WebGpuSecondaryMotionOptions);
72
85
  /** Updates non-inertial reference-frame motion before the next render. */
86
+ get enabled(): boolean;
87
+ setEnabled(enabled: boolean): void;
73
88
  setFrameMotion(motion: SecondaryMotionFrameMotion): void;
74
89
  /** Sets up to eight mesh-local spheres used to keep simulated particles outside the body. */
75
90
  setSphereColliders(colliders: readonly SecondaryMotionSphereCollider[]): void;
@@ -1,4 +1,4 @@
1
- import{createWebGpuComputeKernel as e}from"../rendering/webgpu-runtime-bridge.js";const t=64;export class WebGpuSecondaryMotionRuntime{constructor(t,n={}){this.device=t,this.collisionColliderData=new Float32Array(96),this.collisionCollidersInitialized=!1,this.collisionBindGroups=new Map,this.integrationParamsData=new ArrayBuffer(80),this.integrationParamsView=new DataView(this.integrationParamsData),this.regions=new Map,this.pendingSteps=0,this.disposed=!1,this.lastExecuteSeconds=null,this.fixedStep=new SecondaryMotionFixedStep(n),this.constraintIterations=Math.max(0,Math.trunc(n.constraintIterations??4)),this.constraintStiffness=Math.max(0,Math.min(1,n.constraintStiffness??.9)),this.autoAdvance=n.autoAdvance??!0,this.integrationKernel=e(t,{label:"secondary-motion-integrate",code:o,bindGroupLayoutEntries:[i(0,!0),i(1,!1),i(2,!1),i(3,!0),i(4,!0),{binding:5,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),this.constraintKernel=e(t,{label:"secondary-motion-distance-constraints",code:s,bindGroupLayoutEntries:[i(0,!1),i(1,!0),i(2,!0),i(3,!0),{binding:4,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),this.packKernel=e(t,{label:"secondary-motion-pack-render-positions",code:a,bindGroupLayoutEntries:[i(0,!0),i(1,!0),i(2,!1)]}),this.finalizeKernel=e(t,{label:"secondary-motion-finalize-velocity",code:l,bindGroupLayoutEntries:[i(0,!0),i(1,!0),i(2,!1),{binding:3,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),this.collisionKernel=e(t,{label:"secondary-motion-sphere-collision",code:c,bindGroupLayoutEntries:[i(0,!1),i(1,!0),i(2,!0),i(3,!0),i(4,!0),i(5,!1),{binding:6,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),this.integrationParams=t.createBuffer({label:"secondary-motion-integration-params",size:80,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),this.collisionSpheres=t.createBuffer({label:"secondary-motion-collision-spheres",size:384,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),this.previousCollisionSpheres=t.createBuffer({label:"secondary-motion-previous-collision-spheres",size:384,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),this.collisionParams=t.createBuffer({label:"secondary-motion-collision-params",size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),t.queue.writeBuffer(this.collisionParams,0,new Uint32Array(4));const r=this.integrationParamsView,d=n.gravity??[0,-9.81,0];r.setFloat32(0,this.fixedStep.step,!0),r.setFloat32(4,Math.max(0,Math.min(1,n.damping??.985)),!0),r.setFloat32(8,Math.max(0,n.attachment??12),!0),r.setFloat32(16,d[0],!0),r.setFloat32(20,d[1],!0),r.setFloat32(24,d[2],!0),r.setFloat32(76,Math.max(1,4*Math.hypot(d[0],d[1],d[2])),!0),t.queue.writeBuffer(this.integrationParams,0,this.integrationParamsData)}setFrameMotion(e){this.writeFrameVector(32,e.linearAcceleration),this.writeFrameVector(48,e.angularVelocity),this.writeFrameVector(64,e.angularAcceleration),this.device.queue.writeBuffer(this.integrationParams,32,this.integrationParamsData,32,48)}setSphereColliders(e){this.setEllipsoidColliders(e.map(e=>({center:e.center,radii:[e.radius,e.radius,e.radius]})))}setEllipsoidColliders(e){const t=Math.min(e.length,8),i=new Float32Array(96);for(let n=0;n<t;n++){const t=e[n],r=12*n;i[r]=t.center[0],i[r+1]=t.center[1],i[r+2]=t.center[2],i[r+4]=Math.max(0,t.radii[0]),i[r+5]=Math.max(0,t.radii[1]),i[r+6]=Math.max(0,t.radii[2]),i[r+8]=t.rotation?.[0]??0,i[r+9]=t.rotation?.[1]??0,i[r+10]=t.rotation?.[2]??0,i[r+11]=t.rotation?.[3]??1}this.collisionCollidersInitialized?this.device.queue.writeBuffer(this.previousCollisionSpheres,0,this.collisionColliderData):(this.device.queue.writeBuffer(this.previousCollisionSpheres,0,i),this.collisionCollidersInitialized=!0),this.collisionColliderData.set(i),this.device.queue.writeBuffer(this.collisionSpheres,0,this.collisionColliderData),this.device.queue.writeBuffer(this.collisionParams,0,new Uint32Array([t,0,0,0]))}advance(e){const t=this.fixedStep.consume(e);return this.pendingSteps=Math.min(this.pendingSteps+t,this.fixedStep.maxSubsteps),t}addRegion(e){if(this.assertActive(),this.regions.has(e.id))throw new Error(`Secondary-motion region "${e.id}" is already registered`);const t=e.particleInverseMass.length,i=e.renderVertexIndices.length;if(e.particlePositions.length!==3*t)throw new Error("Secondary-motion particle positions must contain three values per particle");const o=r(e.particlePositions),s=[this.createStorageBuffer(`${e.id}-positions-a`,o),this.createStorageBuffer(`${e.id}-positions-b`,o)],a=this.createStorageBuffer(`${e.id}-velocities`,new Float32Array(4*t)),c=this.createStorageBuffer(`${e.id}-targets`,e.particlePositions),l=this.createStorageBuffer(`${e.id}-inverse-mass`,e.particleInverseMass),d=this.createStorageBuffer(`${e.id}-render-positions`,new Float32Array(3*i)),u=this.createStorageBuffer(`${e.id}-render-vertex-indices`,e.renderVertexIndices),p=[0,1].map(t=>this.integrationKernel.createBindGroup(this.device,[n(0,s[t]),n(1,a),n(2,s[1-t]),n(3,c),n(4,l),n(5,this.integrationParams)],`${e.id}-integration-${t}`)),f=s.map(e=>this.packKernel.createBindGroup(this.device,[n(0,e),n(1,u),n(2,d)])),h=[0,1].map(e=>this.finalizeKernel.createBindGroup(this.device,[n(0,s[1-e]),n(1,s[e]),n(2,a),n(3,this.integrationParams)])),g=function(e,t){if(0===e.constraints.length)return null;if(e.constraints.length%2!=0)throw new Error("Secondary-motion distance constraints must contain particle pairs");const i=e.constraints.length/2;if(e.constraintRest.length!==i||e.constraintColors.length!==i)throw new Error("Secondary-motion constraint arrays have inconsistent lengths");const n=Array.from({length:i},(e,t)=>t).sort((t,i)=>e.constraintColors[t]-e.constraintColors[i]),r=new Uint32Array(e.constraints.length),o=new Float32Array(i),s=[];let a=-1;for(let c=0;c<i;c++){const i=n[c],l=e.constraintColors[i],d=e.constraints[2*i],u=e.constraints[2*i+1];if(d>=t||u>=t)throw new Error("Secondary-motion constraint references an invalid particle");r[2*c]=d,r[2*c+1]=u,o[c]=e.constraintRest[i],l!==a?(s.push({offset:c,count:1}),a=l):s[s.length-1].count++}return{pairs:r,rest:o,batches:s}}(e,t),m=g?this.createStorageBuffer(`${e.id}-constraints`,g.pairs):null,v=g?this.createStorageBuffer(`${e.id}-constraint-rest`,g.rest):null,y=[];if(g&&m&&v)for(const t of g.batches){const i=this.device.createBuffer({label:`${e.id}-constraint-params`,size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),r=new ArrayBuffer(16),o=new DataView(r);o.setUint32(0,t.offset,!0),o.setUint32(4,t.count,!0),o.setFloat32(8,this.constraintStiffness,!0),this.device.queue.writeBuffer(i,0,r),y.push({count:t.count,params:i,bindGroups:s.map(e=>this.constraintKernel.createBindGroup(this.device,[n(0,e),n(1,l),n(2,m),n(3,v),n(4,i)]))})}const x={id:e.id,particleCount:t,renderVertexCount:i,positions:s,velocities:a,targets:c,inverseMass:l,renderPositions:d,renderVertexIndices:u,integrationBindGroups:p,packBindGroups:f,finalizeBindGroups:h,constraints:m,constraintRest:v,constraintBatches:y,state:0,disposed:!1,renderGeometry:null};this.regions.set(e.id,x);const P=this;return{id:e.id,particleCount:t,renderVertexCount:i,get positionBuffer(){return x.positions[x.state]},setRenderGeometry(e){x.renderGeometry=e},updateTargets(e){P.writeTargets(x.targets,e,t)},reset(t=e.particlePositions){P.resetRegion(x,t)},dispose(){P.removeRegion(x)}}}execute(e){if(this.assertActive(),e.device!==this.device)throw new Error("Secondary-motion runtime was created for a different GPU device");if(this.autoAdvance){const e=.001*performance.now();null!=this.lastExecuteSeconds&&this.advance(e-this.lastExecuteSeconds),this.lastExecuteSeconds=e}const i=this.pendingSteps;if(this.pendingSteps=0,0===this.regions.size)return;const n=e.encoder.beginComputePass({label:"secondary-motion-simulation"});for(let e=0;e<i;e++)for(const e of this.regions.values())if(!e.disposed&&0!==e.particleCount){if(n.setPipeline(this.integrationKernel.pipeline),n.setBindGroup(0,e.integrationBindGroups[e.state]),n.dispatchWorkgroups(Math.ceil(e.particleCount/t)),e.state=0===e.state?1:0,e.constraintBatches.length>0){n.setPipeline(this.constraintKernel.pipeline);for(let i=0;i<this.constraintIterations;i++)for(const i of e.constraintBatches)n.setBindGroup(0,i.bindGroups[e.state]),n.dispatchWorkgroups(Math.ceil(i.count/t))}n.setPipeline(this.finalizeKernel.pipeline),n.setBindGroup(0,e.finalizeBindGroups[e.state]),n.dispatchWorkgroups(Math.ceil(e.particleCount/t)),n.setPipeline(this.collisionKernel.pipeline),n.setBindGroup(0,this.getCollisionBindGroup(e)),n.dispatchWorkgroups(Math.ceil(e.particleCount/t))}for(const e of this.regions.values())e.disposed||0===e.particleCount||null==e.renderGeometry||(n.setPipeline(this.packKernel.pipeline),n.setBindGroup(0,e.packBindGroups[e.state]),n.dispatchWorkgroups(Math.ceil(e.renderVertexCount/t)));n.end();for(const t of this.regions.values())if(!t.disposed&&null!=t.renderGeometry){const i=e.getGeometryVertexBuffer(t.renderGeometry,"position");e.encoder.copyBufferToBuffer(t.renderPositions,0,i,0,12*t.renderVertexCount)}}dispose(){if(!this.disposed){for(const e of Array.from(this.regions.values()))this.removeRegion(e);this.integrationParams.destroy(),this.collisionSpheres.destroy(),this.previousCollisionSpheres.destroy(),this.collisionParams.destroy(),this.collisionBindGroups.clear(),this.disposed=!0}}resetRegion(e,t){this.writeTargets(e.targets,t,e.particleCount);const i=function(e,t){if(e.length!==3*t)throw new Error(`Expected ${3*t} secondary-motion position values, received ${e.length}`);return r(e)}(t,e.particleCount);for(const t of e.positions)this.device.queue.writeBuffer(t,0,i);this.device.queue.writeBuffer(e.velocities,0,new Float32Array(4*e.particleCount)),e.state=0,this.fixedStep.reset(),this.pendingSteps=0}writeTargets(e,t,i){if(t.length!==3*i)throw new Error(`Expected ${3*i} secondary-motion target values, received ${t.length}`);this.device.queue.writeBuffer(e,0,t)}removeRegion(e){if(!e.disposed){e.disposed=!0,this.regions.delete(e.id);for(const t of e.positions)this.collisionBindGroups.delete(t);for(const t of e.positions)t.destroy();e.velocities.destroy(),e.targets.destroy(),e.inverseMass.destroy(),e.renderPositions.destroy(),e.renderVertexIndices.destroy(),e.constraints?.destroy(),e.constraintRest?.destroy();for(const t of e.constraintBatches)t.params.destroy()}}createStorageBuffer(e,t){const i=this.device.createBuffer({label:e,size:Math.max(4,t.byteLength+3&-4),usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST|GPUBufferUsage.COPY_SRC});return t.byteLength>0&&this.device.queue.writeBuffer(i,0,t.buffer,t.byteOffset,t.byteLength),i}getCollisionBindGroup(e){const t=e.positions[e.state];let i=this.collisionBindGroups.get(t);return null==i&&(i=this.collisionKernel.createBindGroup(this.device,[n(0,t),n(1,e.positions[1-e.state]),n(2,e.inverseMass),n(3,this.collisionSpheres),n(4,this.previousCollisionSpheres),n(5,e.velocities),n(6,this.collisionParams)],`${e.id}-sphere-collision-${e.state}`),this.collisionBindGroups.set(t,i)),i}assertActive(){if(this.disposed)throw new Error("Secondary-motion runtime is disposed")}writeFrameVector(e,t){this.integrationParamsView.setFloat32(e,t?.[0]??0,!0),this.integrationParamsView.setFloat32(e+4,t?.[1]??0,!0),this.integrationParamsView.setFloat32(e+8,t?.[2]??0,!0)}}export function createSecondaryMotionRenderGraphExtension(e,t="SecondaryMotionSimulation"){const i=i=>{i.addPass({name:t,execute:t=>e.execute(t)})};return i.stage="beforeScene",i}export class SecondaryMotionFixedStep{constructor(e={}){this.accumulator=0,this.step=1/(e.frequency??60),this.maxSubsteps=e.maxSubsteps??4,this.maxFrameDelta=e.maxFrameDelta??.1}consume(e){if(!Number.isFinite(e)||e<0||e>this.maxFrameDelta)return this.accumulator=0,0;this.accumulator=Math.min(this.accumulator+e,this.step*this.maxSubsteps);const t=Math.min(Math.floor(this.accumulator/this.step),this.maxSubsteps);return this.accumulator-=t*this.step,t}reset(){this.accumulator=0}}export function selectSecondaryMotionLod(e,t,i){return t?i?"hero":e<8?"near":e<20?"mid":e<40?"far":"sleep":"sleep"}export function secondaryMotionTransformDiscontinuous(e,t,i=2,n=.5){if(e.length<7||t.length<7)return!0;const r=t[0]-e[0],o=t[1]-e[1],s=t[2]-e[2];if(r*r+o*o+s*s>i*i)return!0;return Math.abs(e[3]*t[3]+e[4]*t[4]+e[5]*t[5]+e[6]*t[6])<n}function i(e,t){return{binding:e,visibility:GPUShaderStage.COMPUTE,buffer:{type:t?"read-only-storage":"storage"}}}function n(e,t){return{binding:e,resource:{buffer:t}}}function r(e){const t=new Float32Array(e.length/3*4);for(let i=0,n=0;i<e.length;i+=3,n+=4)t[n]=e[i],t[n+1]=e[i+1],t[n+2]=e[i+2],t[n+3]=1;return t}const o="\nstruct IntegrationParams {\n deltaTime: f32,\n damping: f32,\n attachment: f32,\n _padding: f32,\n gravity: vec3<f32>,\n _padding2: f32,\n frameLinearAcceleration: vec3<f32>,\n _padding3: f32,\n frameAngularVelocity: vec3<f32>,\n _padding4: f32,\n frameAngularAcceleration: vec3<f32>,\n maxFrameAcceleration: f32,\n}\n@group(0) @binding(0) var<storage, read> currentPositions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read_write> velocities: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read_write> outputPositions: array<vec4<f32>>;\nstruct PackedPosition { x: f32, y: f32, z: f32 }\n@group(0) @binding(3) var<storage, read> targetPositions: array<PackedPosition>;\n@group(0) @binding(4) var<storage, read> inverseMass: array<f32>;\n@group(0) @binding(5) var<uniform> params: IntegrationParams;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&inverseMass)) { return; }\n let packedTarget = targetPositions[index];\n let targetPosition = vec3<f32>(packedTarget.x, packedTarget.y, packedTarget.z);\n if (inverseMass[index] <= 0.0) {\n velocities[index] = vec4(0.0);\n outputPositions[index] = vec4(targetPosition, 1.0);\n return;\n }\n let current = currentPositions[index].xyz;\n // Fully simulated cloth should be driven through its structural connection\n // to the animated roots. Pulling every free particle toward its skinned rest\n // pose makes collision sandwich it against the character like shrink-wrap.\n let animationInfluence = 1.0 - clamp(inverseMass[index], 0.0, 1.0);\n let targetAcceleration = (targetPosition - current) * params.attachment * animationInfluence;\n let previousVelocity = velocities[index].xyz;\n let rotationalAcceleration =\n cross(params.frameAngularAcceleration, current) +\n cross(params.frameAngularVelocity, cross(params.frameAngularVelocity, current)) +\n 2.0 * cross(params.frameAngularVelocity, previousVelocity);\n var frameAcceleration = params.frameLinearAcceleration + rotationalAcceleration;\n let frameAccelerationLength = length(frameAcceleration);\n if (frameAccelerationLength > params.maxFrameAcceleration) {\n frameAcceleration *= params.maxFrameAcceleration / frameAccelerationLength;\n }\n let acceleration = params.gravity + targetAcceleration - frameAcceleration;\n let velocity = previousVelocity * params.damping + acceleration * params.deltaTime;\n let next = current + velocity * params.deltaTime;\n velocities[index] = vec4(velocity, 0.0);\n outputPositions[index] = vec4(next, 1.0);\n}\n",s="\nstruct ConstraintParams {\n offset: u32,\n count: u32,\n stiffness: f32,\n _padding: f32,\n}\n@group(0) @binding(0) var<storage, read_write> positions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read> inverseMass: array<f32>;\n@group(0) @binding(2) var<storage, read> pairs: array<vec2<u32>>;\n@group(0) @binding(3) var<storage, read> restLengths: array<f32>;\n@group(0) @binding(4) var<uniform> params: ConstraintParams;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n if (invocation.x >= params.count) { return; }\n let constraintIndex = params.offset + invocation.x;\n let pair = pairs[constraintIndex];\n let weightA = inverseMass[pair.x];\n let weightB = inverseMass[pair.y];\n let weight = weightA + weightB;\n if (weight <= 0.0) { return; }\n let delta = positions[pair.y].xyz - positions[pair.x].xyz;\n let length = length(delta);\n if (length <= 0.000001) { return; }\n let correction = delta * ((length - restLengths[constraintIndex]) / length) * params.stiffness;\n positions[pair.x] = vec4(positions[pair.x].xyz + correction * (weightA / weight), 1.0);\n positions[pair.y] = vec4(positions[pair.y].xyz - correction * (weightB / weight), 1.0);\n}\n",a="\nstruct PackedPosition { x: f32, y: f32, z: f32 }\n@group(0) @binding(0) var<storage, read> positions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read> particleByRenderVertex: array<u32>;\n@group(0) @binding(2) var<storage, read_write> output: array<PackedPosition>;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&particleByRenderVertex)) { return; }\n let position = positions[particleByRenderVertex[index]];\n output[index].x = position.x; output[index].y = position.y; output[index].z = position.z;\n}\n",c="\nstruct CollisionParams { count: u32, _padding1: u32, _padding2: u32, _padding3: u32 }\n@group(0) @binding(0) var<storage, read_write> positions: array<vec4<f32>>;\nstruct Ellipsoid { center: vec4<f32>, radii: vec4<f32>, rotation: vec4<f32> }\n@group(0) @binding(1) var<storage, read> previousPositions: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read> inverseMass: array<f32>;\n@group(0) @binding(3) var<storage, read> ellipsoids: array<Ellipsoid>;\n@group(0) @binding(4) var<storage, read> previousEllipsoids: array<Ellipsoid>;\n@group(0) @binding(5) var<storage, read_write> velocities: array<vec4<f32>>;\n@group(0) @binding(6) var<uniform> params: CollisionParams;\n\nfn rotateByQuaternion(value: vec3<f32>, rotation: vec4<f32>) -> vec3<f32> {\n return value + 2.0 * cross(rotation.xyz, cross(rotation.xyz, value) + rotation.w * value);\n}\n\nfn toEllipsoidSpace(value: vec3<f32>, shape: Ellipsoid) -> vec3<f32> {\n return rotateByQuaternion(value - shape.center.xyz, vec4<f32>(-shape.rotation.xyz, shape.rotation.w)) / shape.radii.xyz;\n}\n\nfn fromEllipsoidSpace(value: vec3<f32>, shape: Ellipsoid) -> vec3<f32> {\n return shape.center.xyz + rotateByQuaternion(value * shape.radii.xyz, shape.rotation);\n}\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&inverseMass) || inverseMass[index] <= 0.0) { return; }\n var position = positions[index].xyz;\n var velocity = velocities[index].xyz;\n for (var collider = 0u; collider < params.count; collider++) {\n let shape = ellipsoids[collider];\n if (all(shape.radii.xyz > vec3<f32>(0.0))) {\n let scaled = toEllipsoidSpace(position, shape);\n let scaledLengthSquared = dot(scaled, scaled);\n if (scaledLengthSquared < 1.0) {\n let direction = select(vec3<f32>(0.0, 0.0, 1.0), scaled * inverseSqrt(scaledLengthSquared), scaledLengthSquared > 0.00000001);\n position = fromEllipsoidSpace(direction * 1.02, shape);\n let normal = normalize(rotateByQuaternion(direction / shape.radii.xyz, shape.rotation));\n velocity -= normal * min(dot(velocity, normal), 0.0);\n } else {\n let previousShape = previousEllipsoids[collider];\n let start = toEllipsoidSpace(previousPositions[index].xyz, previousShape);\n let travel = scaled - start;\n let a = dot(travel, travel);\n let b = 2.0 * dot(start, travel);\n let c = dot(start, start) - 1.0;\n let discriminant = b * b - 4.0 * a * c;\n if (a > 0.00000001 && discriminant >= 0.0) {\n let hit = (-b - sqrt(discriminant)) / (2.0 * a);\n if (hit > 0.0001 && hit <= 1.0) {\n let direction = normalize(start + travel * hit);\n position = fromEllipsoidSpace(direction * 1.02, shape);\n let normal = normalize(rotateByQuaternion(direction / shape.radii.xyz, shape.rotation));\n velocity -= normal * min(dot(velocity, normal), 0.0);\n }\n }\n }\n }\n }\n positions[index] = vec4(position, 1.0);\n velocities[index] = vec4(velocity, 0.0);\n}\n",l="\nstruct IntegrationParams {\n deltaTime: f32,\n damping: f32,\n attachment: f32,\n _padding: f32,\n gravity: vec3<f32>,\n _padding2: f32,\n}\n@group(0) @binding(0) var<storage, read> previousPositions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read> constrainedPositions: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read_write> velocities: array<vec4<f32>>;\n@group(0) @binding(3) var<uniform> params: IntegrationParams;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&velocities)) { return; }\n velocities[index] = vec4(\n (constrainedPositions[index].xyz - previousPositions[index].xyz) / params.deltaTime,\n 0.0,\n );\n}\n";/*
1
+ import{createWebGpuComputeKernel as e}from"../rendering/webgpu-runtime-bridge.js";import{isSharedClothRegionData as t}from"./cache.js";import{createSkinningTargetKernels as i,SkinnedSecondaryMotionTargets as n}from"./skinned-targets.js";const r=64,s=new WeakMap;export function prepareSecondaryMotionRuntime(e){o(e),i(e)}function o(t){const i=s.get(t);if(i)return i;const n={integration:e(t,{label:"secondary-motion-integrate",code:u,bindGroupLayoutEntries:[a(0,!0),a(1,!1),a(2,!1),a(3,!0),a(4,!0),{binding:5,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),constraint:e(t,{label:"secondary-motion-distance-constraints",code:p,bindGroupLayoutEntries:[a(0,!1),a(1,!0),a(2,!0),a(3,!0),{binding:4,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),pack:e(t,{label:"secondary-motion-pack-render-positions",code:g,bindGroupLayoutEntries:[a(0,!0),a(1,!0),a(2,!1)]}),finalize:e(t,{label:"secondary-motion-finalize-velocity",code:f,bindGroupLayoutEntries:[a(0,!0),a(1,!0),a(2,!1),{binding:3,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),collision:e(t,{label:"secondary-motion-sphere-collision",code:h,bindGroupLayoutEntries:[a(0,!1),a(1,!0),a(2,!0),a(3,!0),a(4,!0),a(5,!1),{binding:6,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]})};return s.set(t,n),n}export class WebGpuSecondaryMotionRuntime{constructor(e,t={}){this.device=e,this.collisionColliderData=new Float32Array(96),this.collisionNextData=new Float32Array(96),this.collisionCountData=new Uint32Array(4),this.collisionCollidersInitialized=!1,this.collisionBindGroups=new Map,this.integrationParamsData=new ArrayBuffer(80),this.integrationParamsView=new DataView(this.integrationParamsData),this.regions=new Map,this.pendingSteps=0,this.disposed=!1,this._enabled=!0,this.lastExecuteSeconds=null,this.skinningTargetKernels=null,this.fixedStep=new SecondaryMotionFixedStep(t),this.constraintIterations=Math.max(0,Math.trunc(t.constraintIterations??4)),this.constraintStiffness=Math.max(0,Math.min(1,t.constraintStiffness??.9)),this.autoAdvance=t.autoAdvance??!0;const i=o(e);this.integrationKernel=i.integration,this.constraintKernel=i.constraint,this.packKernel=i.pack,this.finalizeKernel=i.finalize,this.collisionKernel=i.collision,this.integrationParams=e.createBuffer({label:"secondary-motion-integration-params",size:80,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),this.collisionSpheres=e.createBuffer({label:"secondary-motion-collision-spheres",size:384,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),this.previousCollisionSpheres=e.createBuffer({label:"secondary-motion-previous-collision-spheres",size:384,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),this.collisionParams=e.createBuffer({label:"secondary-motion-collision-params",size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),e.queue.writeBuffer(this.collisionParams,0,new Uint32Array(4));const n=this.integrationParamsView,r=t.gravity??[0,-9.81,0];n.setFloat32(0,this.fixedStep.step,!0),n.setFloat32(4,Math.max(0,Math.min(1,t.damping??.985)),!0),n.setFloat32(8,Math.max(0,t.attachment??12),!0),n.setFloat32(16,r[0],!0),n.setFloat32(20,r[1],!0),n.setFloat32(24,r[2],!0),n.setFloat32(76,Math.max(1,4*Math.hypot(r[0],r[1],r[2])),!0),e.queue.writeBuffer(this.integrationParams,0,this.integrationParamsData)}get enabled(){return this._enabled}setEnabled(e){if(this._enabled!==e&&(this._enabled=e,this.pendingSteps=0,this.fixedStep.reset(),this.lastExecuteSeconds=null,this.collisionCollidersInitialized=!1,e))for(const e of this.regions.values())e.skinningTargets?.requestReset(),e.state=0}setFrameMotion(e){this.writeFrameVector(32,e.linearAcceleration),this.writeFrameVector(48,e.angularVelocity),this.writeFrameVector(64,e.angularAcceleration),this.device.queue.writeBuffer(this.integrationParams,32,this.integrationParamsData,32,48)}setSphereColliders(e){this.setEllipsoidColliders(e.map(e=>({center:e.center,radii:[e.radius,e.radius,e.radius]})))}setEllipsoidColliders(e){const t=Math.min(e.length,8),i=this.collisionNextData;i.fill(0);for(let n=0;n<t;n++){const t=e[n],r=12*n;i[r]=t.center[0],i[r+1]=t.center[1],i[r+2]=t.center[2],i[r+4]=Math.max(0,t.radii[0]),i[r+5]=Math.max(0,t.radii[1]),i[r+6]=Math.max(0,t.radii[2]),i[r+8]=t.rotation?.[0]??0,i[r+9]=t.rotation?.[1]??0,i[r+10]=t.rotation?.[2]??0,i[r+11]=t.rotation?.[3]??1}this.collisionCollidersInitialized?this.device.queue.writeBuffer(this.previousCollisionSpheres,0,this.collisionColliderData):(this.device.queue.writeBuffer(this.previousCollisionSpheres,0,i),this.collisionCollidersInitialized=!0),this.collisionColliderData.set(i),this.device.queue.writeBuffer(this.collisionSpheres,0,this.collisionColliderData),this.collisionCountData[0]=t,this.device.queue.writeBuffer(this.collisionParams,0,this.collisionCountData)}advance(e){const t=this.fixedStep.consume(e);return this.pendingSteps=Math.min(this.pendingSteps+t,this.fixedStep.maxSubsteps),t}addRegion(e){if(this.assertActive(),this.regions.has(e.id))throw new Error(`Secondary-motion region "${e.id}" is already registered`);const r=e.particleInverseMass.length,s=e.renderVertexIndices.length;if(e.particlePositions.length!==3*r)throw new Error("Secondary-motion particle positions must contain three values per particle");const o=l(e.particlePositions),a=[this.createStorageBuffer(`${e.id}-positions-a`,o),this.createStorageBuffer(`${e.id}-positions-b`,o)],u=this.createStorageBuffer(`${e.id}-velocities`,new Float32Array(4*r)),p=this.createStorageBuffer(`${e.id}-targets`,e.particlePositions),g=this.createStorageBuffer(`${e.id}-inverse-mass`,e.particleInverseMass),h=this.createStorageBuffer(`${e.id}-render-positions`,new Float32Array(3*s)),f=this.createStorageBuffer(`${e.id}-render-vertex-indices`,e.renderVertexIndices),m=[0,1].map(t=>this.integrationKernel.createBindGroup(this.device,[c(0,a[t]),c(1,u),c(2,a[1-t]),c(3,p),c(4,g),c(5,this.integrationParams)],`${e.id}-integration-${t}`)),v=a.map(e=>this.packKernel.createBindGroup(this.device,[c(0,e),c(1,f),c(2,h)])),y=[0,1].map(e=>this.finalizeKernel.createBindGroup(this.device,[c(0,a[1-e]),c(1,a[e]),c(2,u),c(3,this.integrationParams)])),x=function(e,i){if(0===e.constraints.length)return null;const n=t(e),r=n?d.get(e):null;if(r)return r;if(e.constraints.length%2!=0)throw new Error("Secondary-motion distance constraints must contain particle pairs");const s=e.constraints.length/2;if(e.constraintRest.length!==s||e.constraintColors.length!==s)throw new Error("Secondary-motion constraint arrays have inconsistent lengths");const o=Array.from({length:s},(e,t)=>t).sort((t,i)=>e.constraintColors[t]-e.constraintColors[i]),a=new Uint32Array(e.constraints.length),c=new Float32Array(s),l=[];let u=-1;for(let t=0;t<s;t++){const n=o[t],r=e.constraintColors[n],s=e.constraints[2*n],d=e.constraints[2*n+1];if(s>=i||d>=i)throw new Error("Secondary-motion constraint references an invalid particle");a[2*t]=s,a[2*t+1]=d,c[t]=e.constraintRest[n],r!==u?(l.push({offset:t,count:1}),u=r):l[l.length-1].count++}const p={pairs:a,rest:c,batches:l};n&&d.set(e,p);return p}(e,r),b=x?this.createStorageBuffer(`${e.id}-constraints`,x.pairs):null,S=x?this.createStorageBuffer(`${e.id}-constraint-rest`,x.rest):null,w=[];if(x&&b&&S)for(const t of x.batches){const i=this.device.createBuffer({label:`${e.id}-constraint-params`,size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),n=new ArrayBuffer(16),r=new DataView(n);r.setUint32(0,t.offset,!0),r.setUint32(4,t.count,!0),r.setFloat32(8,this.constraintStiffness,!0),this.device.queue.writeBuffer(i,0,n),w.push({count:t.count,params:i,bindGroups:a.map(e=>this.constraintKernel.createBindGroup(this.device,[c(0,e),c(1,g),c(2,b),c(3,S),c(4,i)]))})}const P={id:e.id,particleCount:r,renderVertexCount:s,positions:a,velocities:u,targets:p,inverseMass:g,renderPositions:h,renderVertexIndices:f,integrationBindGroups:m,packBindGroups:v,finalizeBindGroups:y,constraints:b,constraintRest:S,constraintBatches:w,state:0,disposed:!1,renderGeometry:null,skinningTargets:null,resetThisFrame:!1};this.regions.set(e.id,P);const B=this;return{id:e.id,particleCount:r,renderVertexCount:s,get positionBuffer(){return P.positions[P.state]},setRenderGeometry(e){P.renderGeometry=e},updateTargets(e){B.writeTargets(P.targets,e,r)},setSkinningTargets(e){if(B.assertActive(),P.disposed)throw new Error("Secondary-motion region is disposed");const t=B.skinningTargetKernels??(B.skinningTargetKernels=i(B.device)),s=new n(B.device,e,t,P.targets,P.positions,P.velocities,r);P.skinningTargets?.dispose(),P.skinningTargets=s},resetToSkinningTargets(){if(B.assertActive(),P.disposed||!P.skinningTargets)throw new Error("Secondary-motion region has no skinning targets");P.skinningTargets.requestReset(),P.state=0,B.fixedStep.reset(),B.pendingSteps=0},reset(t=e.particlePositions){B.resetRegion(P,t)},dispose(){B.removeRegion(P)}}}execute(e){if(this.assertActive(),!this._enabled)return;if(e.device!==this.device)throw new Error("Secondary-motion runtime was created for a different GPU device");if(this.autoAdvance){const e=.001*performance.now();null!=this.lastExecuteSeconds&&this.advance(e-this.lastExecuteSeconds),this.lastExecuteSeconds=e}const t=this.pendingSteps;if(this.pendingSteps=0,0===this.regions.size)return;const i=e.encoder.beginComputePass({label:"secondary-motion-simulation"});for(const t of this.regions.values())t.resetThisFrame=!1,!t.disposed&&t.particleCount>0&&(t.resetThisFrame=t.skinningTargets?.execute(e,i)??!1);for(let e=0;e<t;e++)for(const e of this.regions.values())if(!e.disposed&&0!==e.particleCount&&!e.resetThisFrame){if(i.setPipeline(this.integrationKernel.pipeline),i.setBindGroup(0,e.integrationBindGroups[e.state]),i.dispatchWorkgroups(Math.ceil(e.particleCount/r)),e.state=0===e.state?1:0,e.constraintBatches.length>0){i.setPipeline(this.constraintKernel.pipeline);for(let t=0;t<this.constraintIterations;t++)for(const t of e.constraintBatches)i.setBindGroup(0,t.bindGroups[e.state]),i.dispatchWorkgroups(Math.ceil(t.count/r))}i.setPipeline(this.finalizeKernel.pipeline),i.setBindGroup(0,e.finalizeBindGroups[e.state]),i.dispatchWorkgroups(Math.ceil(e.particleCount/r)),i.setPipeline(this.collisionKernel.pipeline),i.setBindGroup(0,this.getCollisionBindGroup(e)),i.dispatchWorkgroups(Math.ceil(e.particleCount/r))}for(const e of this.regions.values())e.disposed||0===e.particleCount||null==e.renderGeometry||(i.setPipeline(this.packKernel.pipeline),i.setBindGroup(0,e.packBindGroups[e.state]),i.dispatchWorkgroups(Math.ceil(e.renderVertexCount/r)));i.end();for(const t of this.regions.values())if(!t.disposed&&null!=t.renderGeometry){const i=e.getGeometryVertexBuffer(t.renderGeometry,"position");e.encoder.copyBufferToBuffer(t.renderPositions,0,i,0,12*t.renderVertexCount)}}dispose(){if(!this.disposed){for(const e of Array.from(this.regions.values()))this.removeRegion(e);this.integrationParams.destroy(),this.collisionSpheres.destroy(),this.previousCollisionSpheres.destroy(),this.collisionParams.destroy(),this.collisionBindGroups.clear(),this.disposed=!0}}resetRegion(e,t){this.writeTargets(e.targets,t,e.particleCount);const i=function(e,t){if(e.length!==3*t)throw new Error(`Expected ${3*t} secondary-motion position values, received ${e.length}`);return l(e)}(t,e.particleCount);for(const t of e.positions)this.device.queue.writeBuffer(t,0,i);this.device.queue.writeBuffer(e.velocities,0,new Float32Array(4*e.particleCount)),e.state=0,this.fixedStep.reset(),this.pendingSteps=0}writeTargets(e,t,i){if(t.length!==3*i)throw new Error(`Expected ${3*i} secondary-motion target values, received ${t.length}`);this.device.queue.writeBuffer(e,0,t)}removeRegion(e){if(!e.disposed){e.disposed=!0,this.regions.delete(e.id),e.skinningTargets?.dispose();for(const t of e.positions)this.collisionBindGroups.delete(t);for(const t of e.positions)t.destroy();e.velocities.destroy(),e.targets.destroy(),e.inverseMass.destroy(),e.renderPositions.destroy(),e.renderVertexIndices.destroy(),e.constraints?.destroy(),e.constraintRest?.destroy();for(const t of e.constraintBatches)t.params.destroy()}}createStorageBuffer(e,t){const i=this.device.createBuffer({label:e,size:Math.max(4,t.byteLength+3&-4),usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST|GPUBufferUsage.COPY_SRC});return t.byteLength>0&&this.device.queue.writeBuffer(i,0,t.buffer,t.byteOffset,t.byteLength),i}getCollisionBindGroup(e){const t=e.positions[e.state];let i=this.collisionBindGroups.get(t);return null==i&&(i=this.collisionKernel.createBindGroup(this.device,[c(0,t),c(1,e.positions[1-e.state]),c(2,e.inverseMass),c(3,this.collisionSpheres),c(4,this.previousCollisionSpheres),c(5,e.velocities),c(6,this.collisionParams)],`${e.id}-sphere-collision-${e.state}`),this.collisionBindGroups.set(t,i)),i}assertActive(){if(this.disposed)throw new Error("Secondary-motion runtime is disposed")}writeFrameVector(e,t){this.integrationParamsView.setFloat32(e,t?.[0]??0,!0),this.integrationParamsView.setFloat32(e+4,t?.[1]??0,!0),this.integrationParamsView.setFloat32(e+8,t?.[2]??0,!0)}}export function createSecondaryMotionRenderGraphExtension(e,t="SecondaryMotionSimulation"){const i=i=>{!1!==e.enabled&&i.addPass({name:t,execute:t=>e.execute(t)})};return i.stage="beforeScene",i}export class SecondaryMotionFixedStep{constructor(e={}){this.accumulator=0,this.step=1/(e.frequency??60),this.maxSubsteps=e.maxSubsteps??4,this.maxFrameDelta=e.maxFrameDelta??.1}consume(e){if(!Number.isFinite(e)||e<0||e>this.maxFrameDelta)return this.accumulator=0,0;this.accumulator=Math.min(this.accumulator+e,this.step*this.maxSubsteps);const t=Math.min(Math.floor(this.accumulator/this.step),this.maxSubsteps);return this.accumulator-=t*this.step,t}reset(){this.accumulator=0}}export function selectSecondaryMotionLod(e,t,i){return t?i?"hero":e<8?"near":e<20?"mid":e<40?"far":"sleep":"sleep"}export function secondaryMotionTransformDiscontinuous(e,t,i=2,n=.5){if(e.length<7||t.length<7)return!0;const r=t[0]-e[0],s=t[1]-e[1],o=t[2]-e[2];if(r*r+s*s+o*o>i*i)return!0;return Math.abs(e[3]*t[3]+e[4]*t[4]+e[5]*t[5]+e[6]*t[6])<n}function a(e,t){return{binding:e,visibility:GPUShaderStage.COMPUTE,buffer:{type:t?"read-only-storage":"storage"}}}function c(e,t){return{binding:e,resource:{buffer:t}}}function l(e){const t=new Float32Array(e.length/3*4);for(let i=0,n=0;i<e.length;i+=3,n+=4)t[n]=e[i],t[n+1]=e[i+1],t[n+2]=e[i+2],t[n+3]=1;return t}const d=new WeakMap;const u="\nstruct IntegrationParams {\n deltaTime: f32,\n damping: f32,\n attachment: f32,\n _padding: f32,\n gravity: vec3<f32>,\n _padding2: f32,\n frameLinearAcceleration: vec3<f32>,\n _padding3: f32,\n frameAngularVelocity: vec3<f32>,\n _padding4: f32,\n frameAngularAcceleration: vec3<f32>,\n maxFrameAcceleration: f32,\n}\n@group(0) @binding(0) var<storage, read> currentPositions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read_write> velocities: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read_write> outputPositions: array<vec4<f32>>;\nstruct PackedPosition { x: f32, y: f32, z: f32 }\n@group(0) @binding(3) var<storage, read> targetPositions: array<PackedPosition>;\n@group(0) @binding(4) var<storage, read> inverseMass: array<f32>;\n@group(0) @binding(5) var<uniform> params: IntegrationParams;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&inverseMass)) { return; }\n let packedTarget = targetPositions[index];\n let targetPosition = vec3<f32>(packedTarget.x, packedTarget.y, packedTarget.z);\n if (inverseMass[index] <= 0.0) {\n velocities[index] = vec4(0.0);\n outputPositions[index] = vec4(targetPosition, 1.0);\n return;\n }\n let current = currentPositions[index].xyz;\n // Fully simulated cloth should be driven through its structural connection\n // to the animated roots. Pulling every free particle toward its skinned rest\n // pose makes collision sandwich it against the character like shrink-wrap.\n let animationInfluence = 1.0 - clamp(inverseMass[index], 0.0, 1.0);\n let targetAcceleration = (targetPosition - current) * params.attachment * animationInfluence;\n let previousVelocity = velocities[index].xyz;\n let rotationalAcceleration =\n cross(params.frameAngularAcceleration, current) +\n cross(params.frameAngularVelocity, cross(params.frameAngularVelocity, current)) +\n 2.0 * cross(params.frameAngularVelocity, previousVelocity);\n var frameAcceleration = params.frameLinearAcceleration + rotationalAcceleration;\n let frameAccelerationLength = length(frameAcceleration);\n if (frameAccelerationLength > params.maxFrameAcceleration) {\n frameAcceleration *= params.maxFrameAcceleration / frameAccelerationLength;\n }\n let acceleration = params.gravity + targetAcceleration - frameAcceleration;\n let velocity = previousVelocity * params.damping + acceleration * params.deltaTime;\n let next = current + velocity * params.deltaTime;\n velocities[index] = vec4(velocity, 0.0);\n outputPositions[index] = vec4(next, 1.0);\n}\n",p="\nstruct ConstraintParams {\n offset: u32,\n count: u32,\n stiffness: f32,\n _padding: f32,\n}\n@group(0) @binding(0) var<storage, read_write> positions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read> inverseMass: array<f32>;\n@group(0) @binding(2) var<storage, read> pairs: array<vec2<u32>>;\n@group(0) @binding(3) var<storage, read> restLengths: array<f32>;\n@group(0) @binding(4) var<uniform> params: ConstraintParams;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n if (invocation.x >= params.count) { return; }\n let constraintIndex = params.offset + invocation.x;\n let pair = pairs[constraintIndex];\n let weightA = inverseMass[pair.x];\n let weightB = inverseMass[pair.y];\n let weight = weightA + weightB;\n if (weight <= 0.0) { return; }\n let delta = positions[pair.y].xyz - positions[pair.x].xyz;\n let length = length(delta);\n if (length <= 0.000001) { return; }\n let correction = delta * ((length - restLengths[constraintIndex]) / length) * params.stiffness;\n positions[pair.x] = vec4(positions[pair.x].xyz + correction * (weightA / weight), 1.0);\n positions[pair.y] = vec4(positions[pair.y].xyz - correction * (weightB / weight), 1.0);\n}\n",g="\nstruct PackedPosition { x: f32, y: f32, z: f32 }\n@group(0) @binding(0) var<storage, read> positions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read> particleByRenderVertex: array<u32>;\n@group(0) @binding(2) var<storage, read_write> output: array<PackedPosition>;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&particleByRenderVertex)) { return; }\n let position = positions[particleByRenderVertex[index]];\n output[index].x = position.x; output[index].y = position.y; output[index].z = position.z;\n}\n",h="\nstruct CollisionParams { count: u32, _padding1: u32, _padding2: u32, _padding3: u32 }\n@group(0) @binding(0) var<storage, read_write> positions: array<vec4<f32>>;\nstruct Ellipsoid { center: vec4<f32>, radii: vec4<f32>, rotation: vec4<f32> }\n@group(0) @binding(1) var<storage, read> previousPositions: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read> inverseMass: array<f32>;\n@group(0) @binding(3) var<storage, read> ellipsoids: array<Ellipsoid>;\n@group(0) @binding(4) var<storage, read> previousEllipsoids: array<Ellipsoid>;\n@group(0) @binding(5) var<storage, read_write> velocities: array<vec4<f32>>;\n@group(0) @binding(6) var<uniform> params: CollisionParams;\n\nfn rotateByQuaternion(value: vec3<f32>, rotation: vec4<f32>) -> vec3<f32> {\n return value + 2.0 * cross(rotation.xyz, cross(rotation.xyz, value) + rotation.w * value);\n}\n\nfn toEllipsoidSpace(value: vec3<f32>, shape: Ellipsoid) -> vec3<f32> {\n return rotateByQuaternion(value - shape.center.xyz, vec4<f32>(-shape.rotation.xyz, shape.rotation.w)) / shape.radii.xyz;\n}\n\nfn fromEllipsoidSpace(value: vec3<f32>, shape: Ellipsoid) -> vec3<f32> {\n return shape.center.xyz + rotateByQuaternion(value * shape.radii.xyz, shape.rotation);\n}\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&inverseMass) || inverseMass[index] <= 0.0) { return; }\n var position = positions[index].xyz;\n var velocity = velocities[index].xyz;\n for (var collider = 0u; collider < params.count; collider++) {\n let shape = ellipsoids[collider];\n if (all(shape.radii.xyz > vec3<f32>(0.0))) {\n let scaled = toEllipsoidSpace(position, shape);\n let scaledLengthSquared = dot(scaled, scaled);\n if (scaledLengthSquared < 1.0) {\n let direction = select(vec3<f32>(0.0, 0.0, 1.0), scaled * inverseSqrt(scaledLengthSquared), scaledLengthSquared > 0.00000001);\n position = fromEllipsoidSpace(direction * 1.02, shape);\n let normal = normalize(rotateByQuaternion(direction / shape.radii.xyz, shape.rotation));\n velocity -= normal * min(dot(velocity, normal), 0.0);\n } else {\n let previousShape = previousEllipsoids[collider];\n let start = toEllipsoidSpace(previousPositions[index].xyz, previousShape);\n let travel = scaled - start;\n let a = dot(travel, travel);\n let b = 2.0 * dot(start, travel);\n let c = dot(start, start) - 1.0;\n let discriminant = b * b - 4.0 * a * c;\n if (a > 0.00000001 && discriminant >= 0.0) {\n let hit = (-b - sqrt(discriminant)) / (2.0 * a);\n if (hit > 0.0001 && hit <= 1.0) {\n let direction = normalize(start + travel * hit);\n position = fromEllipsoidSpace(direction * 1.02, shape);\n let normal = normalize(rotateByQuaternion(direction / shape.radii.xyz, shape.rotation));\n velocity -= normal * min(dot(velocity, normal), 0.0);\n }\n }\n }\n }\n }\n positions[index] = vec4(position, 1.0);\n velocities[index] = vec4(velocity, 0.0);\n}\n",f="\nstruct IntegrationParams {\n deltaTime: f32,\n damping: f32,\n attachment: f32,\n _padding: f32,\n gravity: vec3<f32>,\n _padding2: f32,\n}\n@group(0) @binding(0) var<storage, read> previousPositions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read> constrainedPositions: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read_write> velocities: array<vec4<f32>>;\n@group(0) @binding(3) var<uniform> params: IntegrationParams;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&velocities)) { return; }\n velocities[index] = vec4(\n (constrainedPositions[index].xyz - previousPositions[index].xyz) / params.deltaTime,\n 0.0,\n );\n}\n";/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -0,0 +1,35 @@
1
+ import { type SkinnedMesh } from 'three';
2
+ import type { RendererCustomPassContext } from '@hology/webgpu-renderer';
3
+ import { type WebGpuComputeKernel } from '../rendering/webgpu-runtime-bridge.js';
4
+ export interface SecondaryMotionSkinningTargets {
5
+ readonly mesh: SkinnedMesh;
6
+ readonly bindPositions: Float32Array;
7
+ readonly skinIndices: Uint16Array | Uint32Array;
8
+ readonly skinWeights: Float32Array;
9
+ /** Includes movable particles and pinned particles touched by constraints. */
10
+ readonly activeParticles: Uint32Array;
11
+ }
12
+ export declare function createSkinningTargetKernels(device: GPUDevice): readonly [WebGpuComputeKernel, WebGpuComputeKernel];
13
+ /** Persistent particle inputs; pose matrices come from the renderer's shared palette. */
14
+ export declare class SkinnedSecondaryMotionTargets {
15
+ private readonly device;
16
+ private readonly source;
17
+ private readonly kernels;
18
+ private readonly targets;
19
+ private readonly buffers;
20
+ private readonly uniformData;
21
+ private readonly inverseWorld;
22
+ private readonly uniforms;
23
+ private readonly resetBindGroup;
24
+ private bindGroup;
25
+ private palette;
26
+ private pendingReset;
27
+ private readonly inputs;
28
+ constructor(device: GPUDevice, source: SecondaryMotionSkinningTargets, kernels: readonly [WebGpuComputeKernel, WebGpuComputeKernel], targets: GPUBuffer, positions: readonly [GPUBuffer, GPUBuffer], velocities: GPUBuffer, particleCount: number);
29
+ requestReset(): void;
30
+ execute(context: RendererCustomPassContext, pass: GPUComputePassEncoder): boolean;
31
+ dispose(): void;
32
+ }
33
+ export declare const SKIN_TARGETS_WGSL = "\nstruct Transforms { inverseWorld: mat4x4<f32>, bind: mat4x4<f32> }\n@group(0) @binding(0) var<storage, read> bindPositions: array<f32>;\n@group(0) @binding(1) var<storage, read> indices: array<vec4<u32>>;\n@group(0) @binding(2) var<storage, read> weights: array<vec4<f32>>;\n@group(0) @binding(3) var<storage, read> activeParticles: array<u32>;\n@group(0) @binding(4) var<storage, read> bones: array<mat4x4<f32>>;\n@group(0) @binding(5) var<storage, read_write> targets: array<f32>;\n@group(0) @binding(6) var<uniform> transforms: Transforms;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n if (invocation.x >= arrayLength(&activeParticles)) { return; }\n let particle = activeParticles[invocation.x];\n let offset = particle * 3u;\n let original = vec3<f32>(bindPositions[offset], bindPositions[offset + 1u], bindPositions[offset + 2u]);\n let bound = transforms.bind * vec4<f32>(original, 1.0);\n var result = vec3<f32>(0.0);\n var total = 0.0;\n for (var influence = 0u; influence < 4u; influence++) {\n let weight = weights[particle][influence];\n let bone = indices[particle][influence];\n if (weight > 0.0 && bone < arrayLength(&bones)) {\n result += (transforms.inverseWorld * bones[bone] * bound).xyz * weight;\n total += weight;\n }\n }\n if (total <= 0.000001) { result = original; }\n targets[offset] = result.x; targets[offset + 1u] = result.y; targets[offset + 2u] = result.z;\n}\n";
34
+ export declare const RESET_TARGETS_WGSL = "\n@group(0) @binding(0) var<storage, read> targets: array<f32>;\n@group(0) @binding(1) var<storage, read_write> positionsA: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read_write> positionsB: array<vec4<f32>>;\n@group(0) @binding(3) var<storage, read_write> velocities: array<vec4<f32>>;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let particle = invocation.x;\n if (particle >= arrayLength(&positionsA)) { return; }\n let offset = particle * 3u;\n let value = vec4<f32>(targets[offset], targets[offset + 1u], targets[offset + 2u], 1.0);\n positionsA[particle] = value; positionsB[particle] = value; velocities[particle] = vec4<f32>(0.0);\n}\n";
35
+ //# sourceMappingURL=skinned-targets.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{Matrix4 as e}from"three";import{createWebGpuComputeKernel as t}from"../rendering/webgpu-runtime-bridge.js";const i=new WeakMap;export function createSkinningTargetKernels(e){const n=i.get(e);if(n)return n;const r=(e,t)=>({binding:e,visibility:GPUShaderStage.COMPUTE,buffer:{type:t?"read-only-storage":"storage"}}),s=[t(e,{label:"secondary-motion-skin-targets",code:SKIN_TARGETS_WGSL,bindGroupLayoutEntries:[r(0,!0),r(1,!0),r(2,!0),r(3,!0),r(4,!0),r(5,!1),{binding:6,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),t(e,{label:"secondary-motion-reset-skinned-targets",code:RESET_TARGETS_WGSL,bindGroupLayoutEntries:[r(0,!0),r(1,!1),r(2,!1),r(3,!1)]})];return i.set(e,s),s}export class SkinnedSecondaryMotionTargets{constructor(t,i,r,s,o,a,u){if(this.device=t,this.source=i,this.kernels=r,this.targets=s,this.buffers=[],this.uniformData=new Float32Array(32),this.inverseWorld=new e,this.bindGroup=null,this.palette=null,this.pendingReset=!0,i.bindPositions.length!==3*u||i.skinIndices.length!==4*u||i.skinWeights.length!==4*u)throw new Error("Secondary-motion skinning input lengths do not match particles");const c=new Set;for(const e of i.activeParticles){if(e>=u||c.has(e))throw new Error("Secondary-motion active particles must be unique and in range");c.add(e);for(let t=0;t<4;t++){const n=4*e+t;if(i.skinWeights[n]>0&&i.skinIndices[n]>=i.mesh.skeleton.bones.length)throw new Error("Secondary-motion skinning references an unavailable bone")}}const f=(e,i)=>{const n=t.createBuffer({label:e,size:Math.max(4,i.byteLength),usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST});return i.byteLength&&t.queue.writeBuffer(n,0,i.buffer,i.byteOffset,i.byteLength),this.buffers.push(n),n},l=f("secondary-motion-bind-positions",i.bindPositions),g=f("secondary-motion-skin-indices",new Uint32Array(i.skinIndices)),d=f("secondary-motion-skin-weights",i.skinWeights),h=f("secondary-motion-active-particles",i.activeParticles);this.uniforms=t.createBuffer({label:"secondary-motion-skin-transform",size:128,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),this.buffers.push(this.uniforms),this.inputs=[n(0,l),n(1,g),n(2,d),n(3,h),n(4,s),n(5,s),n(6,this.uniforms)],this.resetBindGroup=r[1].createBindGroup(t,[n(0,s),n(1,o[0]),n(2,o[1]),n(3,a)])}requestReset(){this.pendingReset=!0}execute(e,t){const i=this.source.mesh,r=e.getSkeletonBoneBuffer(i.skeleton);this.palette!==r&&(this.inputs[4]=n(4,r),this.bindGroup=this.kernels[0].createBindGroup(this.device,this.inputs),this.palette=r),this.inverseWorld.copy(i.matrixWorld).invert(),this.uniformData.set(this.inverseWorld.elements,0),this.uniformData.set(i.bindMatrix.elements,16),this.device.queue.writeBuffer(this.uniforms,0,this.uniformData),this.source.activeParticles.length>0&&(t.setPipeline(this.kernels[0].pipeline),t.setBindGroup(0,this.bindGroup),t.dispatchWorkgroups(Math.ceil(this.source.activeParticles.length/64)));const s=this.pendingReset;return s&&(t.setPipeline(this.kernels[1].pipeline),t.setBindGroup(0,this.resetBindGroup),t.dispatchWorkgroups(Math.ceil(this.source.bindPositions.length/3/64)),this.pendingReset=!1),s}dispose(){for(const e of this.buffers)e.destroy()}}function n(e,t){return{binding:e,resource:{buffer:t}}}export const SKIN_TARGETS_WGSL="\nstruct Transforms { inverseWorld: mat4x4<f32>, bind: mat4x4<f32> }\n@group(0) @binding(0) var<storage, read> bindPositions: array<f32>;\n@group(0) @binding(1) var<storage, read> indices: array<vec4<u32>>;\n@group(0) @binding(2) var<storage, read> weights: array<vec4<f32>>;\n@group(0) @binding(3) var<storage, read> activeParticles: array<u32>;\n@group(0) @binding(4) var<storage, read> bones: array<mat4x4<f32>>;\n@group(0) @binding(5) var<storage, read_write> targets: array<f32>;\n@group(0) @binding(6) var<uniform> transforms: Transforms;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n if (invocation.x >= arrayLength(&activeParticles)) { return; }\n let particle = activeParticles[invocation.x];\n let offset = particle * 3u;\n let original = vec3<f32>(bindPositions[offset], bindPositions[offset + 1u], bindPositions[offset + 2u]);\n let bound = transforms.bind * vec4<f32>(original, 1.0);\n var result = vec3<f32>(0.0);\n var total = 0.0;\n for (var influence = 0u; influence < 4u; influence++) {\n let weight = weights[particle][influence];\n let bone = indices[particle][influence];\n if (weight > 0.0 && bone < arrayLength(&bones)) {\n result += (transforms.inverseWorld * bones[bone] * bound).xyz * weight;\n total += weight;\n }\n }\n if (total <= 0.000001) { result = original; }\n targets[offset] = result.x; targets[offset + 1u] = result.y; targets[offset + 2u] = result.z;\n}\n";export const RESET_TARGETS_WGSL="\n@group(0) @binding(0) var<storage, read> targets: array<f32>;\n@group(0) @binding(1) var<storage, read_write> positionsA: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read_write> positionsB: array<vec4<f32>>;\n@group(0) @binding(3) var<storage, read_write> velocities: array<vec4<f32>>;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let particle = invocation.x;\n if (particle >= arrayLength(&positionsA)) { return; }\n let offset = particle * 3u;\n let value = vec4<f32>(targets[offset], targets[offset + 1u], targets[offset + 2u], 1.0);\n positionsA[particle] = value; positionsB[particle] = value; velocities[particle] = vec4<f32>(0.0);\n}\n";/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -1,4 +1,4 @@
1
- import{__decorate as e,__metadata as t}from"tslib";import{describe as a,expect as s,it as i,vi as o}from"vitest";o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&o.fn()),e[t]),set:(e,t,a)=>(e[t]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),o.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),o.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),o.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),o.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:o.fn()}));import{prepareClassParameters as n,prepareCustomParamsFromType as r,materializeDataAssetRef as d}from"../scene/materializer.js";import{SerializedParamType as m}from"../scene/model.js";import{DataAssetRef as l}from"../scene/objects/data-asset.js";import{DataAssetDefinition as p,getAssignableDataAssetDefinitions as c,getDataAssetDefinitionId as f,getDataAssetDefinitions as u,getDataAssetDefinitionType as y,isDataAssetDefinitionAbstract as v,Parameter as g,ParameterDefinition as B}from"../shader/parameter.js";let w=class{constructor(){this.amount=1}};e([g({type:Number}),t("design:type",Object)],w.prototype,"amount",void 0),w=e([B("test.data.statModifier")],w);let b=class{constructor(){this.displayName="Item"}};e([g({type:String}),t("design:type",Object)],b.prototype,"displayName",void 0),b=e([p("test.data.item",{abstract:!0})],b);let A=class extends b{constructor(){super(...arguments),this.damage=5,this.modifier=new w}};e([g({type:Number}),t("design:type",Object)],A.prototype,"damage",void 0),e([g({type:()=>w}),t("design:type",Object)],A.prototype,"modifier",void 0),A=e([p("test.data.weapon")],A);let h=class extends b{constructor(){super(...arguments),this.charges=1}};e([g({type:Number}),t("design:type",Object)],h.prototype,"charges",void 0),h=e([p("test.data.consumable")],h);class j{constructor(){this.item=null,this.items=[]}}e([g({type:()=>l,dataAssetOf:()=>b}),t("design:type",l)],j.prototype,"item",void 0),e([g({type:()=>l,dataAssetOf:()=>b,array:!0}),t("design:type",Array)],j.prototype,"items",void 0);class N{constructor(){this.items=[]}}function D(e){const t=new Map(e.map(e=>[e.id,e]));return{getAsset:o.fn(async e=>t.get(e)??null),getAssets:o.fn(async()=>e)}}e([g(),t("design:type",Array)],N.prototype,"items",void 0),Reflect.defineMetadata("design:type",Array,N.prototype,"items"),Reflect.defineMetadata("parameter:arrayElementType",l,N.prototype,"items"),Reflect.defineMetadata("parameter:dataAssetOf",b,N.prototype,"items"),a("data asset definitions",()=>{i("registers data asset definitions separately from parameter definitions",()=>{s(y("test.data.item")).toBe(b),s(f(A)).toBe("test.data.weapon"),s(v(b)).toBe(!0),s(v("test.data.item")).toBe(!0);const e=c(b);s(e.map(e=>e.id)).toEqual(["test.data.consumable","test.data.weapon"]);const t=u({includeAbstract:!0});s(t.some(e=>"test.data.item"===e.id)).toBe(!0),s(t.some(e=>"test.data.statModifier"===e.id)).toBe(!1),s(y("test.data.statModifier")).toBeUndefined()}),i("serializes data asset references as asset ids, including arrays",()=>{const e=r(j,{}),t=e.item,a=e.items;s(t.type).toBe(m.DataAsset),s(t.value).toBeNull(),s(a.type).toBe(m.Array),s(a.element).toBe(m.DataAsset),s(a.value).toEqual([])}),i("uses reflected metadata for data asset reference arrays",()=>{const e=r(N,{}).items;s(e.type).toBe(m.Array),s(e.element).toBe(m.DataAsset),s(e.value).toEqual([])}),i("materializes a data asset reference into a wrapper with nested inline structs",async()=>{const e={id:"asset.weapon",name:"Bronze Sword",type:"data",dataAsset:{definition:"test.data.weapon",params:{displayName:{type:m.String,value:"Bronze Sword"},damage:{type:m.Number,value:12},modifier:{type:m.Struct,struct:"test.data.statModifier",value:{amount:{type:m.Number,value:3}}}}}},t=D([e]),a=await n({item:{type:m.DataAsset,value:e.id},items:{type:m.Array,element:m.DataAsset,value:[e.id]}},j,t,{}),i=a.item;s(i).toBeInstanceOf(l),s(i.id).toBe(e.id),s(i.name).toBe("Bronze Sword"),s(i.asset).toBe(e),s(i.definitionId).toBe("test.data.weapon"),s(i.value).toBeInstanceOf(A),s(i.value.displayName).toBe("Bronze Sword"),s(i.value.damage).toBe(12),s(i.value.modifier).toBeInstanceOf(w),s(i.value.modifier.amount).toBe(3),s(a.items).toHaveLength(1),s(a.items[0].value.damage).toBe(12)}),i("resolves missing and invalid data asset references safely",async()=>{const e=o.spyOn(console,"warn").mockImplementation(()=>{}),t=D([{id:"asset.notData",name:"Texture pretending",type:"texture"},{id:"asset.unknownDefinition",name:"Unknown Definition",type:"data",dataAsset:{definition:"test.data.unknown",params:{}}},{id:"asset.abstractDefinition",name:"Abstract Definition",type:"data",dataAsset:{definition:"test.data.item",params:{}}},{id:"asset.invalidParams",name:"Invalid Params",type:"data",dataAsset:{definition:"test.data.weapon",params:"bad"}}]);await s(d("asset.missing",t,{})).resolves.toBeNull(),await s(d("asset.notData",t,{})).resolves.toBeNull(),await s(d("asset.unknownDefinition",t,{})).resolves.toBeNull(),await s(d("asset.abstractDefinition",t,{})).resolves.toBeNull(),await s(d("asset.invalidParams",t,{})).resolves.toBeNull();const a=await n({item:{type:m.DataAsset,value:"asset.missing"}},j,t,{});s(a.item).toBeUndefined(),s(e).toHaveBeenCalled(),e.mockRestore()})});/*
1
+ import{__decorate as e,__metadata as t}from"tslib";import{describe as a,expect as s,it as i,vi as o}from"vitest";o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&o.fn()),e[t]),set:(e,t,a)=>(e[t]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),o.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),o.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),o.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),o.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:o.fn()}));import{prepareClassParameters as n,prepareCustomParamsFromType as r,materializeDataAssetRef as d}from"../scene/materializer.js";import{SerializedParamType as m}from"../scene/model.js";import{DataAssetRef as p}from"../scene/objects/data-asset.js";import{Subject as l}from"rxjs";import{DataAssetDefinition as c,getAssignableDataAssetDefinitions as u,getDataAssetDefinitionId as f,getDataAssetDefinitions as y,getDataAssetDefinitionType as v,isDataAssetDefinitionAbstract as g,Parameter as B,ParameterDefinition as b}from"../shader/parameter.js";let w=class{constructor(){this.amount=1}};e([B({type:Number}),t("design:type",Object)],w.prototype,"amount",void 0),w=e([b("test.data.statModifier")],w);let A=class{constructor(){this.displayName="Item"}};e([B({type:String}),t("design:type",Object)],A.prototype,"displayName",void 0),A=e([c("test.data.item",{abstract:!0})],A);let h=class extends A{constructor(){super(...arguments),this.damage=5,this.modifier=new w}};e([B({type:Number}),t("design:type",Object)],h.prototype,"damage",void 0),e([B({type:()=>w}),t("design:type",Object)],h.prototype,"modifier",void 0),h=e([c("test.data.weapon")],h);let N=class extends A{constructor(){super(...arguments),this.charges=1}};e([B({type:Number}),t("design:type",Object)],N.prototype,"charges",void 0),N=e([c("test.data.consumable")],N);class j{constructor(){this.item=null,this.items=[]}}e([B({type:()=>p,dataAssetOf:()=>A}),t("design:type",p)],j.prototype,"item",void 0),e([B({type:()=>p,dataAssetOf:()=>A,array:!0}),t("design:type",Array)],j.prototype,"items",void 0);class x{constructor(){this.items=[]}}function D(){return{id:"asset.weapon",name:"Bronze Sword",type:"data",dataAsset:{definition:"test.data.weapon",params:{displayName:{type:m.String,value:"Bronze Sword"},damage:{type:m.Number,value:12},modifier:{type:m.Struct,struct:"test.data.statModifier",value:{amount:{type:m.Number,value:3}}}}}}}function O(e){const t=new Map(e.map(e=>[e.id,e]));return{getAsset:o.fn(async e=>t.get(e)??null),getAssets:o.fn(async()=>e)}}e([B(),t("design:type",Array)],x.prototype,"items",void 0),Reflect.defineMetadata("design:type",Array,x.prototype,"items"),Reflect.defineMetadata("parameter:arrayElementType",p,x.prototype,"items"),Reflect.defineMetadata("parameter:dataAssetOf",A,x.prototype,"items"),a("data asset definitions",()=>{i("registers data asset definitions separately from parameter definitions",()=>{s(v("test.data.item")).toBe(A),s(f(h)).toBe("test.data.weapon"),s(g(A)).toBe(!0),s(g("test.data.item")).toBe(!0);const e=u(A);s(e.map(e=>e.id)).toEqual(["test.data.consumable","test.data.weapon"]);const t=y({includeAbstract:!0});s(t.some(e=>"test.data.item"===e.id)).toBe(!0),s(t.some(e=>"test.data.statModifier"===e.id)).toBe(!1),s(v("test.data.statModifier")).toBeUndefined()}),i("serializes data asset references as asset ids, including arrays",()=>{const e=r(j,{}),t=e.item,a=e.items;s(t.type).toBe(m.DataAsset),s(t.value).toBeNull(),s(a.type).toBe(m.Array),s(a.element).toBe(m.DataAsset),s(a.value).toEqual([])}),i("uses reflected metadata for data asset reference arrays",()=>{const e=r(x,{}).items;s(e.type).toBe(m.Array),s(e.element).toBe(m.DataAsset),s(e.value).toEqual([])}),i("materializes a data asset reference into a wrapper with nested inline structs",async()=>{const e=D(),t=O([e]),a=await n({item:{type:m.DataAsset,value:e.id},items:{type:m.Array,element:m.DataAsset,value:[e.id]}},j,t,{}),i=a.item;s(i).toBeInstanceOf(p),s(i.id).toBe(e.id),s(i.name).toBe("Bronze Sword"),s(i.asset).toBe(e),s(i.definitionId).toBe("test.data.weapon"),s(i.value).toBeInstanceOf(h),s(i.value.displayName).toBe("Bronze Sword"),s(i.value.damage).toBe(12),s(i.value.modifier).toBeInstanceOf(w),s(i.value.modifier.amount).toBe(3),s(a.items).toHaveLength(1),s(a.items[0].value.damage).toBe(12)}),i("reloads fresh snapshots, restores removed parameters to defaults, and unsubscribes",async()=>{let e=D();const t=new l,a={supportsLiveDataAssets:!0,onUpdate:t,getAsset:async()=>e},i=await d(e.id,a,{}),n=[],r=i.onChange.subscribe(e=>n.push(e));e={...e,dataAsset:{...e.dataAsset,params:{damage:{type:m.Number,value:25}}}},t.next(e),await o.waitFor(()=>s(n).toHaveLength(1)),s(n[0].value.damage).toBe(25),s(n[0].value.displayName).toBe((new h).displayName),s(i.value.damage).toBe(12),r.unsubscribe(),s(t.observers).toHaveLength(0),t.next(e),s(n).toHaveLength(1)}),i("resolves missing and invalid data asset references safely",async()=>{const e=o.spyOn(console,"warn").mockImplementation(()=>{}),t=O([{id:"asset.notData",name:"Texture pretending",type:"texture"},{id:"asset.unknownDefinition",name:"Unknown Definition",type:"data",dataAsset:{definition:"test.data.unknown",params:{}}},{id:"asset.abstractDefinition",name:"Abstract Definition",type:"data",dataAsset:{definition:"test.data.item",params:{}}},{id:"asset.invalidParams",name:"Invalid Params",type:"data",dataAsset:{definition:"test.data.weapon",params:"bad"}}]);await s(d("asset.missing",t,{})).resolves.toBeNull(),await s(d("asset.notData",t,{})).resolves.toBeNull(),await s(d("asset.unknownDefinition",t,{})).resolves.toBeNull(),await s(d("asset.abstractDefinition",t,{})).resolves.toBeNull(),await s(d("asset.invalidParams",t,{})).resolves.toBeNull();const a=await n({item:{type:m.DataAsset,value:"asset.missing"}},j,t,{});s(a.item).toBeUndefined(),s(e).toHaveBeenCalled(),e.mockRestore()})});/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */