@hology/core 0.0.236 → 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.
- package/dist/effects/sequence/sequence-player.d.ts +3 -0
- package/dist/effects/sequence/sequence-player.js +1 -1
- package/dist/effects/vfx/gpu-particle-arena.d.ts +57 -0
- package/dist/effects/vfx/gpu-particle-arena.js +4 -0
- package/dist/effects/vfx/gpu-sprite-backend.d.ts +33 -0
- package/dist/effects/vfx/gpu-sprite-backend.js +4 -0
- package/dist/effects/vfx/gpu-sprite-compiler.d.ts +58 -0
- package/dist/effects/vfx/gpu-sprite-compiler.js +4 -0
- package/dist/effects/vfx/gpu-sprite-render-graph.d.ts +10 -0
- package/dist/effects/vfx/gpu-sprite-render-graph.js +4 -0
- package/dist/effects/vfx/gpu-sprite-runtime.d.ts +304 -0
- package/dist/effects/vfx/gpu-sprite-runtime.js +4 -0
- package/dist/effects/vfx/gpu-sprite-scheduler-runtime.d.ts +71 -0
- package/dist/effects/vfx/gpu-sprite-scheduler-runtime.js +4 -0
- package/dist/effects/vfx/gpu-sprite-template.d.ts +47 -0
- package/dist/effects/vfx/gpu-sprite-template.js +4 -0
- package/dist/effects/vfx/gpu-sprite-world-runtime.d.ts +61 -0
- package/dist/effects/vfx/gpu-sprite-world-runtime.js +4 -0
- package/dist/effects/vfx/gpu-vfx-capacity.d.ts +3 -0
- package/dist/effects/vfx/gpu-vfx-capacity.js +4 -0
- package/dist/effects/vfx/gpu-vfx-scheduler.d.ts +52 -0
- package/dist/effects/vfx/gpu-vfx-scheduler.js +4 -0
- package/dist/effects/vfx/gpu-vfx-world-service.d.ts +103 -0
- package/dist/effects/vfx/gpu-vfx-world-service.js +4 -0
- package/dist/effects/vfx/index.d.ts +11 -0
- package/dist/effects/vfx/index.js +1 -1
- package/dist/effects/vfx/trail-renderer.js +1 -1
- package/dist/effects/vfx/vfx-actor.d.ts +19 -2
- package/dist/effects/vfx/vfx-actor.js +1 -1
- package/dist/effects/vfx/vfx-asset.d.ts +2 -0
- package/dist/effects/vfx/vfx-default-sprite-texture.d.ts +3 -0
- package/dist/effects/vfx/vfx-default-sprite-texture.js +4 -0
- package/dist/effects/vfx/vfx-input-runtime.d.ts +3 -1
- package/dist/effects/vfx/vfx-materializer.d.ts +10 -0
- package/dist/effects/vfx/vfx-materializer.js +1 -1
- package/dist/effects/vfx/vfx-mesh-transform.d.ts +4 -0
- package/dist/effects/vfx/vfx-mesh-transform.js +4 -0
- package/dist/effects/vfx/vfx-param.d.ts +2 -0
- package/dist/effects/vfx/vfx-param.js +1 -1
- package/dist/effects/vfx/vfx-renderers.js +1 -1
- package/dist/effects/vfx/vfx-runtime.d.ts +35 -0
- package/dist/effects/vfx/vfx-runtime.js +4 -0
- package/dist/effects/vfx/vfx-service.d.ts +3 -0
- package/dist/effects/vfx/vfx-service.js +1 -1
- package/dist/gameplay/actors/builtin/components/character/character-animation.d.ts +19 -0
- package/dist/gameplay/actors/builtin/components/character/character-animation.js +1 -1
- package/dist/gameplay/actors/builtin/components/character/character-movement-like.d.ts +1 -1
- package/dist/gameplay/actors/builtin/components/character/character-movement.d.ts +8 -3
- package/dist/gameplay/actors/builtin/components/character/character-movement.js +1 -1
- package/dist/gameplay/actors/builtin/components/character/root-motion-constraint.d.ts +10 -0
- package/dist/gameplay/actors/builtin/components/character/root-motion-constraint.js +1 -1
- package/dist/gameplay/ai/behavior-tree/move.d.ts +14 -0
- package/dist/gameplay/ai/behavior-tree/move.js +1 -1
- package/dist/gameplay/initiate.d.ts +3 -0
- package/dist/gameplay/initiate.js +1 -1
- package/dist/gameplay/services/physics/physics-system.js +1 -1
- package/dist/gameplay/services/render.d.ts +14 -1
- package/dist/gameplay/services/render.js +1 -1
- package/dist/rendering/webgpu-experimental-backend.d.ts +8 -2
- package/dist/rendering/webgpu-experimental-backend.js +1 -1
- package/dist/rendering.d.ts +32 -3
- package/dist/rendering.js +1 -1
- package/dist/scene/asset-resource-loader.js +1 -1
- package/dist/scene/materializer.js +1 -1
- package/dist/scene/objects/shapes.js +1 -1
- package/dist/shader/builtin/decal-standard-shader.d.ts +1 -0
- package/dist/shader/builtin/decal-standard-shader.js +1 -1
- package/dist/shader/decal-shader.js +1 -1
- package/dist/shader-nodes/effects.js +1 -1
- package/dist/shader-nodes/material-flags.js +1 -1
- package/dist/shader-nodes/particle.d.ts +5 -2
- package/dist/shader-nodes/particle.js +1 -1
- package/dist/test/asset-resource-loader-skinned-merge.test.d.ts +2 -0
- package/dist/test/asset-resource-loader-skinned-merge.test.js +4 -0
- package/dist/test/character-animation-transition.test.js +1 -1
- package/dist/test/character-root-motion-constraint.test.js +1 -1
- package/dist/test/gpu-particle-arena.test.d.ts +2 -0
- package/dist/test/gpu-particle-arena.test.js +4 -0
- package/dist/test/gpu-sprite-compiler.test.d.ts +2 -0
- package/dist/test/gpu-sprite-compiler.test.js +4 -0
- package/dist/test/gpu-vfx-scheduler.test.d.ts +2 -0
- package/dist/test/gpu-vfx-scheduler.test.js +4 -0
- package/dist/test/mesh-clone.test.d.ts +2 -0
- package/dist/test/mesh-clone.test.js +4 -0
- package/dist/test/sequence-animation-retiming.test.js +1 -1
- package/dist/test/sequence-scene-binding.test.js +1 -1
- package/dist/test/sequence-vfx.test.js +1 -1
- package/dist/test/vfx-mesh-transform.test.d.ts +2 -0
- package/dist/test/vfx-mesh-transform.test.js +4 -0
- package/dist/utils/mesh.d.ts +7 -1
- package/dist/utils/mesh.js +1 -1
- package/package.json +15 -3
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{BoxCollisionShape as e,CapsuleCollisionShape as t,ConeCollisionShape as r,ConvexPolyhedronCollisionShape as s,CylinderCollisionShape as n,PlaneCollisionShape as i,SphereCollisionShape as o,TrimeshCollisionShape as a}from"../collision/collision-shape.js";import{BoxGeometry as m,PlaneGeometry as h,Vector3 as l,CylinderGeometry as c,SphereGeometry as g,Euler as p,TorusGeometry as u,ConeGeometry as d,CircleGeometry as w,CapsuleGeometry as y}from"three";import{rampGeometry as f}from"./ramp-geometry.js";import{RoundedBoxGeometry as S}from"three/examples/jsm/geometries/RoundedBoxGeometry.js";import{TextGeometry as x}from"three/examples/jsm/geometries/TextGeometry.js";import{Font as j}from"three/examples/jsm/loaders/FontLoader.js";import*as b from"three/examples/fonts/helvetiker_regular.typeface.json";import{createSpiralStairsGeometry as L,createStairsGeometry as z}from"./stairs-geometry.js";import
|
|
1
|
+
import{BoxCollisionShape as e,CapsuleCollisionShape as t,ConeCollisionShape as r,ConvexPolyhedronCollisionShape as s,CylinderCollisionShape as n,PlaneCollisionShape as i,SphereCollisionShape as o,TrimeshCollisionShape as a}from"../collision/collision-shape.js";import{BoxGeometry as m,PlaneGeometry as h,Vector3 as l,CylinderGeometry as c,SphereGeometry as g,Euler as p,TorusGeometry as u,ConeGeometry as d,CircleGeometry as w,CapsuleGeometry as y}from"three";import{rampGeometry as f}from"./ramp-geometry.js";import{RoundedBoxGeometry as S}from"three/examples/jsm/geometries/RoundedBoxGeometry.js";import{TextGeometry as x}from"three/examples/jsm/geometries/TextGeometry.js";import{Font as j}from"three/examples/jsm/loaders/FontLoader.js";import*as b from"three/examples/fonts/helvetiker_regular.typeface.json";import{createSpiralStairsGeometry as L,createStairsGeometry as z}from"./stairs-geometry.js";import*as B from"three/examples/jsm/utils/BufferGeometryUtils.js";function E(e){return{type:"number",default:e}}function G(e){return{type:"number",default:e,float:!0}}function k(e){return{type:"boolean",default:e}}function v(e,t){const r={};for(let s in t)null==e[s]?r[s]=t[s].default:r[s]=e[s];return r}const T={box:{geometry:()=>new m(1,1,1),collision:()=>new e(new l(1,1,1))},ramp:{geometry:()=>f(),collision:()=>new s(f())},plane:new class{constructor(){this.parameters={widthSegments:E(1),heightSegments:E(1)}}geometry(e={}){const t=v(e,this.parameters);return new h(1,1,t.widthSegments,t.heightSegments)}collision(e={}){return new i(1,1)}},cylinder:new class{constructor(){this.parameters={radialSegments:E(12),heightSegments:E(1),openEnded:k(!1)}}geometry(e={}){const t=v(e,this.parameters);return new c(.5,.5,1,t.radialSegments,t.heightSegments,t.openEnded)}collision(e={}){const t=v(e,this.parameters);return new n(.5,.5,1,t.radialSegments,new p(0,1,0))}},sphere:new class{constructor(){this.parameters={withSegments:E(12),heightSegments:E(12)}}geometry(e={}){const t=v(e,this.parameters);return new g(.5,t.withSegments,t.heightSegments)}collision(){return new o(.5)}},torus:new class{constructor(){this.parameters={radius:G(1),tube:G(.3),radialSegments:E(16),tubularSegments:E(50)}}geometry(e={}){const t=v(e,this.parameters);return new u(t.radius,t.tube,t.radialSegments,t.tubularSegments)}collision(e={}){return new a(this.geometry(e))}},cone:new class{constructor(){this.parameters={radialSegments:E(12),heightSegments:E(1),openEnded:k(!1)}}geometry(e={}){const t=v(e,this.parameters);return new d(.5,1,t.radialSegments,t.heightSegments)}collision(e={}){return new r(1,.5)}},circle:new class{constructor(){this.parameters={segments:E(32)}}geometry(e={}){const t=v(e,this.parameters);return new w(.5,t.segments)}collision(e={}){return new a(this.geometry(e))}},capsule:new class{constructor(){this.parameters={radius:G(.5),length:G(1),capSegments:E(8),radialSegments:E(32)}}geometry(e={}){const t=v(e,this.parameters);return new y(t.radius,t.length,t.capSegments,t.radialSegments)}collision(e={}){const r=v(e,this.parameters);return new t(r.length,r.radius)}},roundedBox:new class{constructor(){this.parameters={width:G(1),height:G(1),depth:G(1),segments:E(4),radius:G(.1)}}geometry(e={}){const t=v(e,this.parameters),r=new S(t.width,t.height,t.depth,t.segments,t.radius);return B.mergeVertices(r)}collision(t={}){const r=v(t,this.parameters);return new e(new l(r.width,r.height,r.depth))}},text:new class{constructor(){var e;this.parameters={text:(e="Text",{type:"string",default:e}),size:G(1),depth:G(.2)}}geometry(e={}){const t=v(e,this.parameters);return new x(t.text,{font:new j(b),size:t.size,height:t.depth})}collision(e={}){return v(e,this.parameters).text.trim().length,null}},stairs:new class{constructor(){this.parameters={floating:k(!1),width:G(1),height:G(.25),depth:G(.25),steps:E(4)}}geometry(e={}){const t=v(e,this.parameters);return z(t.floating,t.steps,t.depth,t.height,t.width)}collision(e={}){return new a(this.geometry(e))}},stairsSpiral:new class{constructor(){this.parameters={floating:k(!1),width:G(1),height:G(.25),steps:E(4),degrees:E(90),radius:G(.2),flipped:k(!1)}}geometry(e={}){const t=v(e,this.parameters);return L(t.floating,t.steps,t.height,t.width,t.degrees,t.radius,t.flipped)}collision(e={}){return new a(this.geometry(e))}}};export const ShapeLibrary=T;export const ShapeLibraryKeys=Object.keys(ShapeLibrary);/*
|
|
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{__decorate as e,__metadata as t}from"tslib";import{Color as a,Texture as s,Vector2 as r}from"three";import{colorToNormal as i,float as o,rgb as l,rgba as p,standardMaterial as d,textureSampler2d as n,uniforms as h,normalize as m,varying as y,vec2 as c}from"three-shader-graph";import{decalAlpha as u,decalNormal as S,decalUV as g}from"../../shader-nodes/decal.js";import{depthWorldPosition as v}from"../../shader-nodes/depth.js";import{particleUniforms as M}from"../../shader-nodes/particle.js";import{DecalNodeShader as D}from"../decal-shader.js";import{Parameter as b}from"../parameter.js";import{sampleScreenAO as f}from"../../shader-nodes/scene-sample.js";export class DecalStandardShader extends D{constructor(){super(...arguments),this.color=new a("#FFFFFF"),this.opacity=1,this.roughness=1,this.metalness=0,this.normalScale=new r(1,1),this.atlasSize=new r(1,1),this.atlasTile=1}output(){let e=tileUv(g,this.atlasSize,this.atlasTile),t=o(this.opacity).multiply(M.opacity);null!=this.alphaMap&&(t=t.multiply(n(this.alphaMap).sample(e).r)),t=t.multiply(u);let a=p(l(this.color).multiply(M.color),t);null!=this.colorMap&&(a=a.multiply(n(this.colorMap).sample(e)));let s=p(this.emissive,t);null!=this.emissiveMap&&(s=s.multiply(n(this.emissiveMap).sample(e)));let r=o(this.roughness);null!=this.roughnessMap&&(r=r.multiply(n(this.roughnessMap).sample(e).g));let c=o(this.metalness);null!=this.metalnessMap&&(c=c.multiply(n(this.metalnessMap).sample(e).b));let D=m(y(h.normalMatrix).multiplyVec(S));null!=this.normalMap&&(D=i(n(this.normalMap).sample(e),this.normalScale?.x??1,D,v,e));let b=null;return null!=this.aoMap&&(b=n(this.aoMap).sample(e).r),{color:d({color:p(a.rgb.multiplyScalar(f()),a.a),emissive:s,emissiveIntensity:o(this.emissiveIntensity??1),roughness:r,metalness:c,normal:D,ambientOcclusion:b,ambientOcclusionIntensity:o(this.aoMapIntensity??1),worldPosition:v}),normal:D,roughness:r}}build(){const e=super.build();return e.depthTest=!0,e.depthWrite=!1,e.userData.isDecal=!0,e}}e([b(),t("design:type",a)],DecalStandardShader.prototype,"color",void 0),e([b({range:[0,1]}),t("design:type",Number)],DecalStandardShader.prototype,"opacity",void 0),e([b(),t("design:type",s)],DecalStandardShader.prototype,"colorMap",void 0),e([b(),t("design:type",s)],DecalStandardShader.prototype,"alphaMap",void 0),e([b({range:[0,1]}),t("design:type",Number)],DecalStandardShader.prototype,"roughness",void 0),e([b(),t("design:type",s)],DecalStandardShader.prototype,"roughnessMap",void 0),e([b({range:[0,1]}),t("design:type",Number)],DecalStandardShader.prototype,"metalness",void 0),e([b(),t("design:type",s)],DecalStandardShader.prototype,"metalnessMap",void 0),e([b(),t("design:type",s)],DecalStandardShader.prototype,"aoMap",void 0),e([b(),t("design:type",Number)],DecalStandardShader.prototype,"aoMapIntensity",void 0),e([b(),t("design:type",a)],DecalStandardShader.prototype,"emissive",void 0),e([b({range:[0,10]}),t("design:type",Number)],DecalStandardShader.prototype,"emissiveIntensity",void 0),e([b(),t("design:type",s)],DecalStandardShader.prototype,"emissiveMap",void 0),e([b(),t("design:type",s)],DecalStandardShader.prototype,"normalMap",void 0),e([b(),t("design:type",r)],DecalStandardShader.prototype,"normalScale",void 0),e([b(),t("design:type",r)],DecalStandardShader.prototype,"atlasSize",void 0),e([b({range:[1,64],precision:0}),t("design:type",Number)],DecalStandardShader.prototype,"atlasTile",void 0);export function tileUv(e,t,a){if(t&&(t.x>1||t.y>1)&&t.x>0&&t.y>0&&null!=a&&a>0){const s=t.x,r=t.y;if(s>0&&r>0){const t=a-1,i=t%s,o=Math.floor(t/s);e=e.divide(c(s,r)).add(c(i/s,o/r))}}return e}/*
|
|
1
|
+
import{__decorate as e,__metadata as t}from"tslib";import{Color as a,Texture as s,Vector2 as r}from"three";import{colorToNormal as i,float as o,rgb as l,rgba as p,standardMaterial as d,textureSampler2d as n,uniforms as h,normalize as m,varying as y,vec2 as c}from"three-shader-graph";import{decalAlpha as u,decalNormal as S,decalUV as g}from"../../shader-nodes/decal.js";import{depthWorldPosition as v}from"../../shader-nodes/depth.js";import{particleUniforms as M}from"../../shader-nodes/particle.js";import{DecalNodeShader as D}from"../decal-shader.js";import{Parameter as b}from"../parameter.js";import{sampleScreenAO as f}from"../../shader-nodes/scene-sample.js";export class DecalStandardShader extends D{constructor(){super(...arguments),this.color=new a("#FFFFFF"),this.opacity=1,this.roughness=1,this.metalness=0,this.normalScale=new r(1,1),this.atlasSize=new r(1,1),this.atlasTile=1}output(){let e=tileUv(g,this.atlasSize,this.atlasTile),t=o(this.opacity).multiply(M.opacity);null!=this.alphaMap&&(t=t.multiply(n(this.alphaMap).sample(e).r)),t=t.multiply(u);let a=p(l(this.color).multiply(M.color),t);null!=this.colorMap&&(a=a.multiply(n(this.colorMap).sample(e)));let s=p(this.emissive,t);null!=this.emissiveMap&&(s=s.multiply(n(this.emissiveMap).sample(e)));let r=o(this.roughness);null!=this.roughnessMap&&(r=r.multiply(n(this.roughnessMap).sample(e).g));let c=o(this.metalness);null!=this.metalnessMap&&(c=c.multiply(n(this.metalnessMap).sample(e).b));let D=m(y(h.normalMatrix).multiplyVec(S));null!=this.normalMap&&(D=i(n(this.normalMap).sample(e),this.normalScale?.x??1,D,v,e));let b=null;return null!=this.aoMap&&(b=n(this.aoMap).sample(e).r),{color:d({color:p(a.rgb.multiplyScalar(f()),a.a),emissive:s,emissiveIntensity:o(this.emissiveIntensity??1),roughness:r,metalness:c,normal:D,ambientOcclusion:b,ambientOcclusionIntensity:o(this.aoMapIntensity??1),worldPosition:v}),alphaTest:this.alphaTest,normal:D,roughness:r}}build(){const e=super.build();return e.depthTest=!0,e.depthWrite=!1,e.userData.isDecal=!0,e}}e([b(),t("design:type",a)],DecalStandardShader.prototype,"color",void 0),e([b({range:[0,1]}),t("design:type",Number)],DecalStandardShader.prototype,"opacity",void 0),e([b(),t("design:type",s)],DecalStandardShader.prototype,"colorMap",void 0),e([b(),t("design:type",s)],DecalStandardShader.prototype,"alphaMap",void 0),e([b({range:[0,1]}),t("design:type",Number)],DecalStandardShader.prototype,"roughness",void 0),e([b(),t("design:type",s)],DecalStandardShader.prototype,"roughnessMap",void 0),e([b({range:[0,1]}),t("design:type",Number)],DecalStandardShader.prototype,"metalness",void 0),e([b(),t("design:type",s)],DecalStandardShader.prototype,"metalnessMap",void 0),e([b(),t("design:type",s)],DecalStandardShader.prototype,"aoMap",void 0),e([b(),t("design:type",Number)],DecalStandardShader.prototype,"aoMapIntensity",void 0),e([b(),t("design:type",a)],DecalStandardShader.prototype,"emissive",void 0),e([b({range:[0,10]}),t("design:type",Number)],DecalStandardShader.prototype,"emissiveIntensity",void 0),e([b(),t("design:type",s)],DecalStandardShader.prototype,"emissiveMap",void 0),e([b(),t("design:type",s)],DecalStandardShader.prototype,"normalMap",void 0),e([b(),t("design:type",r)],DecalStandardShader.prototype,"normalScale",void 0),e([b(),t("design:type",r)],DecalStandardShader.prototype,"atlasSize",void 0),e([b({range:[1,64],precision:0}),t("design:type",Number)],DecalStandardShader.prototype,"atlasTile",void 0);export function tileUv(e,t,a){if(t&&(t.x>1||t.y>1)&&t.x>0&&t.y>0&&null!=a&&a>0){const s=t.x,r=t.y;if(s>0&&r>0){const t=a-1,i=t%s,o=Math.floor(t/s);e=e.divide(c(s,r)).add(c(i/s,o/r))}}return e}/*
|
|
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{NodeShaderMaterial as
|
|
1
|
+
import{NodeShaderMaterial as s}from"../shader-nodes/index.js";import{decalDiscard as r}from"../shader-nodes/decal.js";import{sampledSurfaceRejectsDecals as e}from"../shader-nodes/material-flags.js";import{Shader as a}from"./shader.js";export class DecalNodeShader extends a{build(){const a=this.output();let t=r.or(e);null!=a.discard&&(t=t.or(a.discard));const d=new s({transparent:!1,alphaTest:this.alphaTest,...a,discard:t});return d.userData.isDecal=!0,d}}/*
|
|
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{attributes as t,clamp as r,dot as e,float as o,ifDefApply as i,
|
|
1
|
+
import{attributes as t,clamp as r,dot as e,float as o,ifDefApply as i,max as n,normalize as c,pow as p,saturate as a,step as f,transformed as m,uniforms as d,varying as u,varyingAttributes as l,vec4 as s}from"three-shader-graph";import{fragmentLinearEyeDepth as h,linearEyeDepth as x}from"./depth.js";import{oneMinus as D}from"./math.js";d.modelMatrix.multiplyVec(s(t.position,1));export function fresnelEffect(t=1){const r=i("FLIP_SIDED",m.normal,()=>m.normal.multiplyScalar(-1));return p(D(a(e(m.viewDir,r))),o(t))}const E=o(1).subtract(r(x.subtract(h).divide(x),0,1));export function edgeDepthEffect(t=1){return p(E,o(t))}export function depthFadeEffect(t){const r=x.subtract(h),e=o(1e-4),i=f(e,t);return a(r.divide(n(t,e))).multiply(i).add(D(i))}/*
|
|
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{BooleanNode as e}from"three-shader-graph";import{isWgslCompiler as t}from"three-shader-graph";import{screenUV as r}from"./depth.js";import{sceneColorSampler as o}from"./scene-sample.js";export const MATERIAL_FLAG_ALPHA_TEST=1;export const MATERIAL_FLAG_RECEIVE_DECALS=2;const s=o.sample(r).a.lt(2);export const sampledSurfaceRejectsDecals=new class extends e{compile(e){return t(e)?{out:e.get(s)}:{out:"false"}}};/*
|
|
1
|
+
import{BooleanNode as e}from"three-shader-graph";import{isWgslCompiler as t}from"three-shader-graph";import{screenUV as r}from"./depth.js";import{sceneColorSampler as o}from"./scene-sample.js";export const MATERIAL_FLAG_ALPHA_TEST=1;export const MATERIAL_FLAG_RECEIVE_DECALS=2;const s=o.sample(r).a.multiply(3).lt(2);export const sampledSurfaceRejectsDecals=new class extends e{compile(e){return t(e)?{out:e.get(s)}:{out:"false"}}};/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { FloatNode, Vec3Node } from 'three-shader-graph';
|
|
1
|
+
import { FloatNode, Vec3Node, UniformVec3Node } from 'three-shader-graph';
|
|
2
2
|
import type { Compiler } from 'three-shader-graph';
|
|
3
3
|
export declare const particleEnergyUniformName = "hology_particle_energy";
|
|
4
4
|
export declare const particleTimeUniformName = "hology_particle_time";
|
|
5
5
|
export declare const particleVelcoityUniformName = "hology_particle_velocity";
|
|
6
|
+
export declare const particleOpacityUniformName = "hology_particle_opacity";
|
|
7
|
+
export declare const particleColorUniformName = "hology_particle_color";
|
|
6
8
|
declare class InstancedColorNode extends Vec3Node {
|
|
7
9
|
compile(c: Compiler): import("three-shader-graph/build/main/lib/compiler").CompileResult<string>;
|
|
8
10
|
}
|
|
@@ -11,7 +13,7 @@ declare class ParticleScalarNode extends FloatNode {
|
|
|
11
13
|
private readonly component;
|
|
12
14
|
private readonly particleData;
|
|
13
15
|
constructor(uniform: FloatNode, component: 'x' | 'y');
|
|
14
|
-
compile(c: Compiler): {
|
|
16
|
+
compile(c: Compiler): import("three-shader-graph/build/main/lib/compiler").CompileResult<string> | {
|
|
15
17
|
out: unknown;
|
|
16
18
|
};
|
|
17
19
|
}
|
|
@@ -20,6 +22,7 @@ export declare const particleUniforms: {
|
|
|
20
22
|
color: InstancedColorNode;
|
|
21
23
|
opacity: ParticleScalarNode;
|
|
22
24
|
time: import("three-shader-graph").UniformFloatNode;
|
|
25
|
+
velocity: UniformVec3Node;
|
|
23
26
|
};
|
|
24
27
|
export declare const particleAttributes: {};
|
|
25
28
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{Vector3 as e}from"three";import{attributeVec3 as
|
|
1
|
+
import{Vector3 as e}from"three";import{attributeVec3 as o,FloatNode as t,ifDefApply as r,uniformFloat as i,uniformVec3 as n,Vec3Node as c,Vec3ExpressionNode as l,UniformVec3Node as a,autoVarying as p}from"three-shader-graph";import{isWgslCompiler as s,isWgslFragmentCompiler as m}from"three-shader-graph";export const particleEnergyUniformName="hology_particle_energy";export const particleTimeUniformName="hology_particle_time";export const particleVelcoityUniformName="hology_particle_velocity";export const particleOpacityUniformName="hology_particle_opacity";export const particleColorUniformName="hology_particle_color";const h=new a("color",new e(1,1,1),void 0,!1);const y=p(new class extends c{compile(o){if(s(o))return null!=o.getExternalBindings().uniformExpressions?.hology_particle_color?o.defineUniform("vec3","hology_particle_color",new e(1,1,1)):m(o)?o.defineVarying("vec3",new l("instanceColor")):{out:"rendererObjectColor(input.instanceIndex).xyz"};return{chunk:`\n #ifdef USE_INSTANCING_COLOR\n vec3 instanceColor = instanceColor;\n #else\n vec3 instanceColor = ${o.get(h)};\n #endif\n `,out:"instanceColor"}}});class g extends t{constructor(e,t){super(),this.uniform=e,this.component=t,this.particleData=o("particleData")}compile(e){if(s(e)){const o="x"===this.component?"hology_particle_opacity":"hology_particle_energy";if(null!=e.getExternalBindings().uniformExpressions?.[o])return e.defineUniform("float",o,1);const t=e.get(this.uniform),r=e.get(this.particleData);return{out:`select(${t}, ${r}.${this.component}, ${r}.z > 0.5)`}}return{out:e.get(r("USE_INSTANCING",this.uniform,()=>this.particleData[this.component]))}}}export const particleUniforms={energy:new g(i("hology_particle_energy",1),"y"),color:y,opacity:new g(i("hology_particle_opacity",1),"x"),time:i("hology_particle_time"),velocity:n("hology_particle_velocity")};export const particleAttributes={};/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as n,test as t}from"vitest";import{Bone as o,BufferAttribute as s,BufferGeometry as i,Group as r,MeshBasicMaterial as a,Skeleton as d,SkinnedMesh as m}from"three";import{mergeCompatibleSkinnedMeshes as w}from"../utils/mesh.js";function c(e){const n=new i;return n.setAttribute("position",new s(new Float32Array([e,0,0,e+1,0,0,e,1,0]),3)),n.setAttribute("skinIndex",new s(new Uint16Array(12),4)),n.setAttribute("skinWeight",new s(new Float32Array([1,0,0,0,1,0,0,0,1,0,0,0]),4)),n}function f(e){e.updateWorldMatrix(!0,!0),w(e)}e("AssetResourceLoader skinned mesh optimization",()=>{t("merges meshes sharing a skeleton, bind space, transform and named material slot",()=>{const e=new r,t=new o;e.add(t);const s=new d([t]),i=new a,w=new a;i.name="Body",w.name="Body";const l=new m(c(0),i),h=new m(c(2),w);l.bind(s),h.bind(s),e.add(l,h),f(e);const p=e.children.filter(e=>e instanceof m);n(p).toHaveLength(1),n(p[0].geometry.getAttribute("position").count).toBe(6),n(p[0].skeleton).toBe(s),n(p[0].material).toBe(i)}),t("keeps meshes with different bind-space transforms separate",()=>{const e=new r,t=new o;e.add(t);const s=new d([t]),i=new a;i.name="Body";const w=new m(c(0),i),l=new m(c(2),i);w.bind(s),l.bind(s),l.position.x=1,e.add(w,l),f(e);const h=e.children.filter(e=>e instanceof m);n(h).toHaveLength(2)})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{expect as e,test as t}from"vitest";import{AnimationClip as i,AnimationMixer as o,LoopOnce as n,Object3D as a,VectorKeyframeTrack as s}from"three";import{canCrossFadeFromAnimationAction as
|
|
1
|
+
import{expect as e,test as t}from"vitest";import{AnimationClip as i,AnimationMixer as o,LoopOnce as n,Object3D as a,VectorKeyframeTrack as s}from"three";import{canCrossFadeFromAnimationAction as c}from"../gameplay/animation/animation-action.js";t("crossfades away from a finished clamped animation",()=>{const t=new a,p=new o(t),d=new i("slide",.5,[new s(".position",[0,.5],[0,0,0,0,0,1])]),l=new i("idle",1,[new s(".position",[0,1],[0,0,0,0,0,0])]),m=p.clipAction(d);m.setLoop(n,1),m.clampWhenFinished=!0,m.play(),p.update(d.duration+.1),e(m.isRunning()).toBe(!1),e(m.isScheduled()).toBe(!0),e(m.enabled).toBe(!0),e(c(m)).toBe(!0);const r=p.clipAction(l);r.reset().play(),m.crossFadeTo(r,.2,!1),p.update(.1),e(m.getEffectiveWeight()).toBeLessThan(1),e(r.getEffectiveWeight()).toBeGreaterThan(0),e(r.getEffectiveTimeScale()).toBeCloseTo(1),p.update(.11),e(m.enabled).toBe(!1)});/*
|
|
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{describe as
|
|
1
|
+
import{describe as e,expect as o,it as t}from"vitest";import{Vector3 as n}from"three";import{calculateRootMotionTargetApproachCorrection as a,constrainRootMotionTargetApproach as r,warpRootMotionTargetApproach as s}from"../gameplay/actors/builtin/components/character/root-motion-constraint.js";e("constrainRootMotionTargetApproach",()=>{t("stops approaching at the minimum target distance",()=>{const e=new n(0,0,2);r(e,new n(0,0,0),new n(0,0,1.5),.9),o(e.x).toBeCloseTo(0),o(e.z).toBeCloseTo(.6)}),t("preserves lateral and vertical motion while clamping approach",()=>{const e=new n(.75,.4,1);r(e,new n(0,0,0),new n(0,0,1.2),.9),o(e.x).toBeCloseTo(.75),o(e.y).toBeCloseTo(.4),o(e.z).toBeCloseTo(.3)}),t("preserves retreating motion when already inside the minimum distance",()=>{const e=new n(0,0,-.5);r(e,new n(0,0,0),new n(0,0,.5),.9),o(e.z).toBeCloseTo(-.5)})}),e("warpRootMotionTargetApproach",()=>{t("adds target-directed motion without exceeding the requested distance",()=>{const e=new n(.25,.4,.2),t=s(e,new n(0,0,0),new n(0,0,3),.9,.6);o(t).toBeCloseTo(.6),o(e.x).toBeCloseTo(.25),o(e.y).toBeCloseTo(.4),o(e.z).toBeCloseTo(.8)}),t("clamps combined authored and added motion at the minimum distance",()=>{const e=new n(0,0,1),t=s(e,new n(0,0,0),new n(0,0,1.4),.9,.75);o(t).toBeCloseTo(.5),o(e.z).toBeCloseTo(.5)}),t("does not pull the actor when already inside the minimum distance",()=>{const e=new n(0,0,-.2),t=s(e,new n(0,0,0),new n(0,0,.5),.9,.75);o(t).toBe(0),o(e.z).toBeCloseTo(-.2)})}),e("calculateRootMotionTargetApproachCorrection",()=>{t("subtracts authored approach from the correction needed this frame",()=>{const e=a(new n(0,0,.25),new n(0,0,0),new n(0,0,3),2.4);o(e).toBeCloseTo(.35)}),t("adds no correction when authored motion already reaches the trajectory",()=>{const e=a(new n(0,0,.75),new n(0,0,0),new n(0,0,3),2.4);o(e).toBe(0)}),t("compensates for authored motion moving away from the target",()=>{const e=a(new n(0,0,-.2),new n(0,0,0),new n(0,0,3),2.4);o(e).toBeCloseTo(.8)})});/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as a,it as t}from"vitest";import{GpuParticleArena as s}from"../effects/vfx/gpu-particle-arena.js";e("GPU particle arena",()=>{t("reuses released pages without increasing committed capacity",()=>{const e=new s({pageSize:64,maximumParticles:256}),t=e.allocate(65);a(t.pages).toEqual([0,1]),a(e.release(t.handle)).toBe(!0);const l=e.allocate(128);a(new Set(l.pages)).toEqual(new Set([0,1])),a(e.getStats()).toMatchObject({committedPages:2,usedPages:2,allocationCount:1})}),t("grows and shrinks an allocation without relocating existing pages",()=>{const e=new s({pageSize:32,maximumParticles:256}),t=e.allocate(33),l=e.resize(t.handle,100);a(l.pages.slice(0,2)).toEqual(t.pages),a(l.pages).toHaveLength(4);const o=e.resize(t.handle,16);a(o.pages).toEqual([0]),a(e.getStats()).toMatchObject({committedPages:4,usedPages:1,freePages:3})}),t("rejects stale handles after their slot is recycled",()=>{const e=new s({pageSize:64,maximumParticles:128}),t=e.allocate(1);e.release(t.handle);const l=e.allocate(1);a(l.handle.slot).toBe(t.handle.slot),a(l.handle.generation).not.toBe(t.handle.generation),a(e.get(t.handle)).toBeNull(),a(e.resize(t.handle,64)).toBeNull(),a(e.release(t.handle)).toBe(!1)}),t("fails atomically when the global budget cannot satisfy growth",()=>{const e=new s({pageSize:64,maximumParticles:128}),t=e.allocate(64);a(e.resize(t.handle,192)).toBeNull(),a(e.get(t.handle)).toMatchObject({capacity:64,pages:[0]}),a(e.getStats()).toMatchObject({committedPages:1,usedPages:1})}),t("reports fragmentation and preserves the allocation high watermark",()=>{const e=new s({pageSize:64,maximumParticles:512}),t=e.allocate(65),l=e.allocate(10);a(e.getStats()).toMatchObject({requestedCapacity:75,usedCapacity:192,internalFragmentation:117,highWatermarkPages:3,highWatermarkCapacity:192}),e.release(t.handle),e.resize(l.handle,1),a(e.getStats()).toMatchObject({requestedCapacity:1,usedCapacity:64,internalFragmentation:63,highWatermarkPages:3})})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as t,it as a}from"vitest";import*as o from"three";import{NodeShaderMaterial as i,rgb as n,rgba as s,standardMaterial as r,textureSampler2d as c,varyingAttributes as l,vec3 as p}from"three-shader-graph";import{SerializedParamType as m}from"../scene/model.js";import{compileGpuSpriteEmitters as d,materializeGpuSpriteRenderClasses as u,selectVfxBackend as h}from"../effects/vfx/gpu-sprite-compiler.js";import{prepareGpuSpriteBackend as y}from"../effects/vfx/gpu-sprite-backend.js";import{calculateGpuSpriteOptionalStateLayout as g,GPU_TRAIL_RENDER_CLASS_BYTES as f,GpuSpriteDirtyRange as b,getCompiledGpuSpriteMaterial as w,getGpuSpriteComputeWgsl as C,getGpuSpriteMaterialWgsl as v,getGpuTrailMaterialWgsl as x}from"../effects/vfx/gpu-sprite-runtime.js";import{Trail as B,trailUV as S}from"../effects/vfx/trail-renderer.js";import{timeUniforms as V}from"../shader-nodes/time.js";import{GpuSpriteSchedulerRuntime as T}from"../effects/vfx/gpu-sprite-scheduler-runtime.js";import{GpuSpriteTemplateCache as M,instantiateGpuSpriteTemplate as I}from"../effects/vfx/gpu-sprite-template.js";import{GpuSpriteWorldRuntime as P}from"../effects/vfx/gpu-sprite-world-runtime.js";import{GpuSpriteRenderClassCache as E}from"../effects/vfx/gpu-sprite-compiler.js";import{addGpuSpriteRenderGraphPasses as z}from"../effects/vfx/gpu-sprite-render-graph.js";import{gpuVfxCapacityClass as j}from"../effects/vfx/gpu-vfx-capacity.js";import{buildParticleLightingShaderHeader as q}from"@hology/webgpu-renderer";import{sampleSceneColor as O}from"../shader-nodes/scene-sample.js";import{screenUV as A}from"../shader-nodes/depth.js";import{particleUniforms as R}from"../shader-nodes/particle.js";const G=e=>({type:m.Number,value:e}),k=e=>({type:m.Vector3,value:e});function L(e={}){return{type:"sprite",texture:"dot",color:"#ffffff",intensity:1,blendingMode:"additive",softness:0,bloom:!1,space:"world",opacityChannel:"alpha",...e}}function U(e={}){return{type:"mesh",assetId:"simple-mesh",space:"world",...e}}function _(e={}){return{type:"trail",length:10,width:.5,taper:!0,color:"#40a0ff",colorEnd:"#ff4080",intensity:2,intensityEnd:.25,opacityStart:1,opacityEnd:0,bloom:!1,billboard:!0,opacityChannel:"alpha",space:"world",...e}}function D(e,t=L()){return{id:e,name:e,position:[0,0,0],rotation:[0,0,0],rate:{type:"once",count:2,delay:0},output:t,initializers:[{id:`${e}-life`,type:"lifetime",enabled:!0,params:{duration:G(2)}},{id:`${e}-position`,type:"positionSphere",enabled:!0,params:{position:k([0,0,0]),radius:G(1),surface:{type:m.Boolean,value:!1}}}],behaviours:[]}}function W(...e){return{id:"test-vfx",name:"test-vfx",type:"vfx",vfx:{emitters:e}}}e("GPU sprite compiler compatibility",()=>{a("compiles pay-for-play child emitters and screen-space collision metadata",()=>{const e=D("parent");e.behaviours.push({id:"collision",type:"worldCollision",enabled:!0,params:{bounce:G(.6),friction:G(.2),lifeLoss:G(.5),debug:{type:m.Boolean,value:!0}}});const a=D("child");a.rate={type:"once",count:3,delay:0},a.spawnEvent="collision",e.children=[a];const i=d(W(e),[new o.Vector3]);t(i.compatibility.compatible).toBe(!0),t(i.emitters).toHaveLength(2),t(i.emitters[0]).toMatchObject({hasWorldCollision:!0,collisionBounce:.6,collisionFriction:.2,collisionLifeLoss:.5,collisionDebug:!0}),t(i.emitters[1]).toMatchObject({parentEmitter:0,childParticlesPerParent:3,childSpawnEvent:2,childLevel:1,capacity:6});const n=C(16);t(n).not.toContain("collisionDepth"),t(n).not.toMatch(/__[A-Z_]+__/);const s=C(16,!1,!1,!1,!1,!1,!1,0,0,!0,!0,32,1);t(s).toContain("collisionDepth"),t(s).toContain("collisionStep <= 6u"),t(s).toContain("approachingVisibleSurface"),t(s).toContain("optionalStates[32u + owner.padding]"),t(s).not.toMatch(/__[A-Z_]+__/)}),a("orders child render classes by authored render order without changing simulation hierarchy",()=>{const e=D("parent",U({renderOrder:10})),a=D("child",L({renderOrder:-10}));e.children=[a];const i=d(W(e),[new o.Vector3]);t(i.renderClasses.map(e=>e.output.renderOrder)).toEqual([-10,10]),t(i.emitters[0].parentEmitter).toBeUndefined(),t(i.emitters[0].renderClass).toBe(1),t(i.emitters[1]).toMatchObject({parentEmitter:0,renderClass:0})}),a("uses geometric world reservation classes without fixed template maxima",()=>{t([1,2,3,4,5,17].map(j)).toEqual([1,2,4,4,8,32])}),a("compiles stretched sprites into a velocity-aligned render class",async()=>{const e=d(W(D("stretched",function(e={}){return{type:"stretchedSprite",texture:"dot",color:"#ffffff",intensity:1,scale:.5,blendingMode:"additive",softness:0,bloom:!1,space:"world",opacityChannel:"alpha",...e}}({scale:.75}))),[new o.Vector3]);t(e.compatibility.compatible).toBe(!0);const a=await u(e.renderClasses,{getTexture:async()=>new o.Texture});t(a.renderClasses[0].stretchScale).toBe(.75);const i=v(w(a.renderClasses[0].material),!1);t(i).toContain("stretchedSpriteClipPosition"),t(i).toContain("bitcast<f32>(renderClass.data.w)")}),a("materializes billboard trails with CPU-compatible history sizing",async()=>{const e=d(W(D("trail",_({length:9.6}))),[new o.Vector3]);t(e.compatibility.compatible).toBe(!0),t(e.emitters).toHaveLength(1),t(e.emitters[0].capacity).toBe(2);const a=await u(e.renderClasses,{getTexture:async()=>new o.Texture});t(a.renderClasses[0].trail).toMatchObject({historyLength:11,width:.5,taper:!0}),t(f).toBe(80);const i=C(16,!1,!1,!1,!0);t(i).toContain("@group(0) @binding(8) var<storage, read_write> optionalStates"),t(i).not.toContain("@binding(9)")}),a("streams local trail transforms through emitter commands without another storage binding",()=>{const e=d(W(D("attached-trail",_({space:"local"}))),[new o.Vector3]);t(e.compatibility.compatible).toBe(!0);const a=C(16,!1,!1,!1,!0,!0);t(a).toContain("trailTransform0: vec4<f32>"),t(a).toContain("emitter.trailTransform3 = command.trailTransform3"),t(a).not.toContain("@binding(12)")}),a("compiles trail graph UV varyings into the procedural ribbon ABI",()=>{const e=new i({color:s(p(S.x,S.y,R.opacity),R.opacity)});B.applyTrailShaderParameters(e);const a=x(e,!0);t(a).toContain("struct GraphVertexInput"),t(a).toContain("nodeVertexID: f32"),t(a).toContain("emitterTransforms"),t(a).not.toContain("__TRAIL_"),t(a).not.toContain("output.uv ="),t(a).not.toContain("output.color ="),t(a).not.toContain("normalize(after - before)"),t(a).not.toContain("2.0, 2.0, 2.0, 0.0")}),a("binds shader graph elapsed time to the GPU particle frame clock",()=>{const e=new i({color:s(p(V.elapsed),1)}),a=x(e),o=v(w(e),!1);t(a).toContain("frame.time"),t(o).toContain("frame.time"),t(a).not.toContain("hology_elapsed_time:"),t(o).not.toContain("hology_elapsed_time:")}),a("preserves CPU raw-channel colors for color behaviours",()=>{const e=D("legacy-color",_());e.behaviours.push({id:"color",type:"changeColor",enabled:!0,params:{a:{type:m.Color,value:"#918383"},b:{type:m.Color,value:"#4f4f4f"}}});const a=d(W(e),[new o.Vector3]);t(a.emitters[0].colorStart.toArray()).toEqual([145/255,131/255,131/255]),t(a.emitters[0].colorEnd.toArray()).toEqual([79/255,79/255,79/255])}),a("materializes one mesh geometry and rejects mesh hierarchies with multiple geometries",async()=>{const e=D("mesh",U({space:"local"}));e.initializers.push({id:"mesh-scale-components",type:"scaleComponents",enabled:!0,params:{scale:k([1,2,3])}},{id:"mesh-rotation",type:"rotation",enabled:!0,params:{rotation:k([30,45,60])}}),e.behaviours.push({id:"mesh-scale-components-behaviour",type:"scaleComponents",enabled:!0,params:{a:k([2,1,1]),b:k([1,.5,2])}});const a=W(e),r=d(a,[new o.Vector3]);t(r.compatibility.compatible).toBe(!0),t(r.renderClasses[0].output.type).toBe("mesh"),t(r.emitters[0].localSpace).toBe(!0),t(r.emitters[0].meshScaleStart.toArray()).toEqual([2,2,3]),t(r.emitters[0].meshScaleEnd.toArray()).toEqual([1,1,6]),t(r.emitters[0].meshRotation.w).not.toBe(1),t(r.emitters[0].meshRotationEuler.toArray()).toEqual([o.MathUtils.degToRad(30),o.MathUtils.degToRad(45),o.MathUtils.degToRad(60)]);const c=new o.Mesh(new o.BoxGeometry(1,1,1),new o.MeshBasicMaterial({color:3368601,opacity:.75})),l=await u(r.renderClasses,{getTexture:async()=>new o.Texture,getMesh:async()=>c});t(l.renderClasses[0].meshVertices?.length).toBe(288),t(l.renderClasses[0].meshColor?.getHex()).toBe(3368601),t(l.renderClasses[0].meshOpacity).toBe(.75),t(l.renderClasses[0].localSpace).toBe(!0);const p=new i({color:s(n(.4),1)}),m=await u(r.renderClasses,{getTexture:async()=>new o.Texture,getMesh:async()=>c},async()=>p);t(m.renderClasses[0].material).toBe(p);const h=new o.Group;h.add(new o.Mesh(new o.BoxGeometry),new o.Mesh(new o.BoxGeometry)),await t(u(r.renderClasses,{getTexture:async()=>new o.Texture,getMesh:async()=>h})).rejects.toThrow("exactly one mesh geometry")}),a("keeps an unconfigured mesh output GPU-compatible without rendering it",()=>{const e=d(W(D("unconfigured-mesh",U({assetId:null}))),[new o.Vector3]);t(e.compatibility.compatible).toBe(!0),t(e.compatibility.issues).toEqual([]),t(e.emitters).toEqual([]),t(e.renderClasses).toEqual([])}),a("treats an unselected mesh material override as the default material source",async()=>{const e=d(W(D("unselected-mesh-material",U({materialSource:"material",material:void 0}))),[new o.Vector3]),a=new i({color:s(n(.4),1)}),r=await u(e.renderClasses,{getTexture:async()=>new o.Texture,getMesh:async()=>new o.Mesh(new o.BoxGeometry)},async()=>a);t(e.compatibility.compatible).toBe(!0),t(r.renderClasses[0].material).toBe(a)}),a("keeps an unselected mesh shader graph on the GPU default material",async()=>{const e=d(W(D("unselected-mesh-graph",U({materialSource:"shaderGraph",shaderGraph:{source:"asset",assetId:null}}))),[new o.Vector3]),a=await u(e.renderClasses,{getTexture:async()=>new o.Texture,getMesh:async()=>new o.Mesh(new o.BoxGeometry)});t(e.compatibility.compatible).toBe(!0),t(()=>w(a.renderClasses[0].material)).not.toThrow()}),a("gates custom mesh materials on a compatible GPU material resolver",async()=>{const e=d(W(D("mesh-material",U({material:"material-id"}))),[new o.Vector3]);t(e.compatibility.compatible).toBe(!0);const a={getTexture:async()=>new o.Texture,getMesh:async()=>new o.Mesh(new o.BoxGeometry)};await t(u(e.renderClasses,a)).rejects.toThrow("compatible GPU material resolver");const n=new o.Texture,s=await u(e.renderClasses,a,async()=>new i({color:c(n).sample(l.uv)}));t(s.renderClasses[0].material).toBeInstanceOf(i);const r=w(s.renderClasses[0].material);t(Object.values(r.textures)).toEqual([n])}),a("materializes non-billboard trails with orientation history",async()=>{const e=d(W(D("oriented-trail",_({billboard:!1}))),[new o.Vector3]);t(e.compatibility.compatible).toBe(!0);const a=await u(e.renderClasses,{getTexture:async()=>new o.Texture});t(a.renderClasses[0].trail).toMatchObject({billboard:!1});const i=C(16,!0,!1,!1,!0);t(i).toContain("trailSampleOffset + 1u"),t(i).toContain("quaternionMultiply(particleRotation, movementRotation)");const n=x(B.createBaseMaterial(),!1);t(n).toContain("renderClass.trail.w == 0u"),t(n).toContain("historySide(state, control)")}),a("changes the mesh render class when shader graph parameters change",()=>{const e={source:"asset",assetId:"mesh-graph"},a=d(W(D("mesh",U({materialSource:"shaderGraph",shaderGraph:e,shaderGraphParams:{intensity:G(1)}}))),[new o.Vector3]),i=d(W(D("mesh",U({materialSource:"shaderGraph",shaderGraph:e,shaderGraphParams:{intensity:G(2)}}))),[new o.Vector3]);t(a.renderClasses[0].key).not.toBe(i.renderClasses[0].key)}),a("preserves legacy mesh geometry transforms when requested",async()=>{const e=new o.Group,a=new o.Mesh((new o.BufferGeometry).setAttribute("position",new o.Float32BufferAttribute([1,0,0],3)));a.position.set(2,0,0),e.add(a);const i=d(W(D("modern",U())),[new o.Vector3]),n=d(W(D("legacy",U({useLegacyTransform:!0}))),[new o.Vector3]);t(i.renderClasses[0].key).not.toBe(n.renderClasses[0].key);const s={getTexture:async()=>new o.Texture,getMesh:async()=>e},r=await u(i.renderClasses,s),c=await u(n.renderClasses,s);t(r.renderClasses[0].meshVertices?.[0]).toBe(3),t(c.renderClasses[0].meshVertices?.[0]).toBe(1)}),a("does not reuse render classes across explicit template revisions",async()=>{const e=W(D("revisioned")),a={assets:{getTexture:async()=>new o.Texture},renderClassCache:new E},i=await(new M).resolve(e,{...a,revision:1}),n=await(new M).resolve(e,{...a,revision:2});t(i.template?.renderClasses[0]).not.toBe(n.template?.renderClasses[0])}),a("retains renderer-owned scene texture semantics for particle materials",()=>{const e=new i({color:O(A)}),a=w(e);t(Object.values(a.sceneTextures)).toEqual(["sceneColor"])}),a("retains the shared renderer geometry semantics for lit particle materials",()=>{const e=w(new i({color:r({color:n(6724044),roughness:.5,metalness:.1})}));t(e.needsWorldPosition).toBe(!0),t(e.needsWorldNormal).toBe(!0),t(e.requiresRendererShaderHeader).toBe(!0),t(e.fragmentMain).toContain("rendererDirectionalLightForLight"),t(q()).toContain("fn rendererCameraPosition()");const a=v(e,!0);t(a).toContain("fn rendererCameraPosition()"),t(a).toContain("rendererCameraPosition() - input.worldPosition"),t(a).toContain("@group(1) @binding(0)"),t(a).toContain("emitterTransform[0].xyz"),t(a).not.toContain("mat3x3<f32>(emitterTransforms["),t(a).toContain("fn normalMatrixFromLinearTransform"),t(a).not.toContain("inverse("),t(e.renderState.transparent).toBe(!1),t(e.renderState.depthWrite).toBe(!0),t(e.renderState.side).toBe(o.FrontSide)}),a("registers one ordered compute/render pair for a shared runtime",()=>{const e=[];let a;z({mainDepth:"depth",addPass:t=>e.push(t)},{executeCompute:()=>{},executeRender:(e,t)=>{a=t},getSceneTextureReads:()=>["sceneColorSnapshot"]},"color","WorldVfx"),t(e.map(e=>e.name)).toEqual(["WorldVfxSimulation","WorldVfxRender"]),t(e[1].dependsOn).toEqual(["WorldVfxSimulation"]),t(e[1].inputs).toMatchObject({sceneColorSnapshot:"sceneColorSnapshot"}),e[1].execute({}),t(a).toBe("color")}),a("coalesces touched emitter slots without allocating per mark",()=>{const e=new b;e.mark(7),e.mark(3),e.mark(5),t(e).toMatchObject({start:3,end:8,length:5}),e.clear(),t(e.length).toBe(0)}),a("generates particle-owner WGSL without reserved active identifiers",()=>{const e=C(16);t(e).toContain("enabled: u32"),t(e).not.toMatch(/\bactive\s*:/),t(e).not.toContain("owner.active"),t(e).toContain("fn applyEmitterCommandsMain"),t(e).toContain("@group(0) @binding(7) var<storage, read> emitterCommands"),t(e).toContain("var<storage, read_write> emitters"),t(e).toContain("timing: vec4<f32>"),t(e).toContain("let emitterDelta = select(frame.delta * emitter.timing.w, 0.0"),t(e).toContain("let sampledLife = mix(emitter.emission.z, emitter.emission.w"),t(e).toContain("particle.velocityLife = vec4(initialVelocity, particleLife)"),t(e).toContain("mix(emitter.colorStart.rgb, emitter.colorEnd.rgb, hash(particleSeed + 71u))"),t(e).toContain("select(animatedColor, particle.color.rgb, emitter.meshScaleStart.w > 0.5)")}),a("compiles random lifetime as a per-particle birth range",()=>{const e=D("random-life");e.initializers[0]={id:"random-life-range",type:"randomLifetime",enabled:!0,params:{minDuration:G(.5),maxDuration:G(2.5)}};const a=d(W(e),[new o.Vector3]);t(a.compatibility.compatible).toBe(!0),t(a.emitters[0]).toMatchObject({lifeMin:.5,life:2.5})}),a("compiles random initial color without lifetime interpolation",()=>{const e=D("random-color");e.initializers.push({id:"random-color-range",type:"randomColor",enabled:!0,params:{a:{type:m.Color,value:"#ff0000"},b:{type:m.Color,value:"#0000ff"}}});const a=d(W(e),[new o.Vector3]);t(a.compatibility.compatible).toBe(!0),t(a.emitters[0].randomColor).toBe(!0)}),a("compiles point, box, line, ring, and sphere-surface spawn zones",()=>{const e=[["positionPoint",{position:k([1,2,3])},0],["positionSphere",{position:k([1,2,3]),radius:G(2),surface:{type:m.Boolean,value:!0}},1],["positionBox",{position:k([1,2,3]),dimensions:k([4,5,6])},2],["positionLine",{a:k([1,2,3]),b:k([4,5,6])},3],["positionRing",{position:k([1,2,3]),rotation:k([10,20,30]),radius:G(4),thickness:G(.5),angle:G(180)},4]];for(const[a,i,n]of e){const e=D(`zone-${a}`);e.initializers[1]={id:`zone-${a}`,type:a,enabled:!0,params:i};const s=d(W(e),[new o.Vector3]);t(s.compatibility.compatible).toBe(!0),t(s.emitters[0].spawnZoneType).toBe(n)}const a=C(16);t(a).toContain("fn rotateSpawnPoint"),t(a).toContain("if (zoneType == 4u)")}),a("preserves legacy per-update sprite rotation behavior",()=>{const e=D("sprite-rotate");e.behaviours.push({id:"rotate-add",type:"rotateAdd",enabled:!0,params:{rotation:k([0,0,200])}});const a=d(W(e),[new o.Vector3]);t(a.compatibility.compatible).toBe(!0),t(a.emitters[0].spriteRotationStep).toBeCloseTo(o.MathUtils.degToRad(o.MathUtils.degToRad(200)));const i=C(16);t(i).toContain("var rotation = particle.color.w;"),t(i).toContain("rotation += emitter.rotationInitialSpriteStep.w * frame.padding1"),e.output=U();const n=d(W(e),[new o.Vector3]);t(n.compatibility.compatible).toBe(!0),t(n.emitters[0].meshRotationStep.z).toBeCloseTo(o.MathUtils.degToRad(o.MathUtils.degToRad(200)));const s=C(16,!0);t(s).toContain("rotationEuler: vec4<f32>"),t(s).toContain("fn quaternionFromEulerXYZ"),t(s).toContain("emitter.rotationStep.xyz * frame.padding1"),e.initializers.push({id:"random-mesh-rotation",type:"randomRotation",enabled:!0,params:{axis:{type:m.String,value:"Y"}}});const r=d(W(e),[new o.Vector3]);t(r.compatibility.compatible).toBe(!0),t(r.emitters[0].randomMeshRotationAxis).toBe(3),t(s).toContain("randomRotationAxis == 1u"),t(s).toContain("randomRotationAxis == 4u")}),a("compiles radial velocity without per-particle auxiliary storage",()=>{const e=D("radial-velocity");e.initializers.push({id:"radial",type:"radialVelocity",enabled:!0,params:{speed:G(4),direction:k([0,1,0]),spread:G(30)}});const a=d(W(e),[new o.Vector3]);t(a.compatibility.compatible).toBe(!0),t(a.emitters[0].hasRadialVelocity).toBe(!0),t(a.emitters[0].radialVelocitySpeed).toBe(4),t(a.emitters[0].radialVelocitySpread).toBeCloseTo(Math.PI/6);const i=C(16);t(i).toContain("fn radialVelocity"),t(i).toContain("emitter.radialMetadata.y > 0.5")}),a("compiles one attraction or repulsion field per emitter",()=>{const e=D("attraction");e.behaviours.push({id:"attract",type:"attraction",enabled:!0,params:{target:k([1,2,3]),force:G(4),radius:G(5)}});const a=d(W(e),[new o.Vector3]);t(a.compatibility.compatible).toBe(!0),t(a.emitters[0].attractionTarget.toArray()).toEqual([1,2,3]),t(a.emitters[0].attractionForce).toBe(4),e.behaviours[0].type="repulsion";const i=d(W(e),[new o.Vector3]);t(i.compatibility.compatible).toBe(!0),t(i.emitters[0].attractionForce).toBe(-4);const n=C(16);t(n).toContain("attractionDistanceSq < attractionRadiusSq")}),a("compiles mesh alignment modes into spawn-only parameters",()=>{const e=D("aligned-mesh",U());e.initializers.push({id:"align",type:"alignRotation",enabled:!0,params:{axis:{type:m.String,value:"+X"},mode:{type:m.String,value:"AlongVelocity"},offset:k([10,20,30]),center:k([1,2,3])}});const a=d(W(e),[new o.Vector3]);t(a.compatibility.compatible).toBe(!0),t(a.emitters[0].alignRotationAxis.toArray()).toEqual([1,0,0]),t(a.emitters[0].alignRotationMode).toBe(3),t(a.emitters[0].alignRotationOffset.x).toBeCloseTo(o.MathUtils.degToRad(10));const i=C(16,!0);t(i).toContain("fn quaternionFromUnitVectors"),t(i).toContain("alignRotationMode == 3u")}),a("groups authored render classes deterministically",()=>{const e=d(W(D("a"),D("b"),D("c",L({blendingMode:"normal"}))),[new o.Vector3]);t(e.compatibility.compatible).toBe(!0),t(e.renderClasses).toHaveLength(2),t(e.emitters.map(e=>e.renderClass)).toEqual([0,0,1])}),a("can lay out emitter allocations contiguously per dynamic effect instance",()=>{const e=W(D("first"),D("second",L({blendingMode:"normal"}))),a=[new o.Vector3,new o.Vector3(1,0,0)],i=d(e,a),n=d(e,a,"instance-major");t(i.emitters.map(e=>e.renderClass)).toEqual([0,0,1,1]),t(n.emitters.map(e=>e.renderClass)).toEqual([0,1,0,1])}),a("reports sparse active-page simulation capacity",async()=>{const e=await y({asset:W(D("paged")),instancePositions:[new o.Vector3,new o.Vector3,new o.Vector3],instanceLayout:"instance-major",particlePageSize:2,assets:{getTexture:async()=>new o.Texture}});e.runtime.setActivePages([0,2]),t(e.runtime.getStats()).toMatchObject({capacity:6,activePageCount:2,simulatedCapacity:4})}),a("compacts active page padding out of the simulation schedule",async()=>{const e=await T.create({asset:W(D("tiny-paged-effect")),maximumInstances:2,particlePageSize:256,assets:{getTexture:async()=>new o.Texture}});e.spawn(new o.Vector3),e.flushActivePages(),t(e.runtime.getStats()).toMatchObject({capacity:512,activePageCount:1,simulatedCapacity:2});const a=C(256);t(a).toContain("let particleIndex = activeParticleIndices[id.x]"),t(a).not.toContain("id.x % pageSize"),e.dispose()}),a("sizes alive indices by logical particles instead of render-class count",async()=>{const e=await y({asset:W(D("additive"),D("normal",L({blendingMode:"normal"}))),instancePositions:[new o.Vector3,new o.Vector3],instanceLayout:"instance-major",particlePageSize:4,assets:{getTexture:async()=>new o.Texture}});t(e.runtime.getStats()).toMatchObject({capacity:8,renderClassCount:2,aliveIndexCapacity:8,aliveIndexBytes:32});const a=C(4);t(a).toContain("aliveIndices[indirect[renderClass].firstInstance + outputIndex]"),t(a).not.toContain("renderClass * u32(frame.capacity)")}),a("allocates and recycles multi-page dynamic effect instances",async()=>{const e=D("paged-instance");e.position=[2,0,0];const a=await T.create({asset:W(e),maximumInstances:2,particlePageSize:1,assets:{getTexture:async()=>new o.Texture}}),i=a.spawn(new o.Vector3);t(a.runtime.emitters[0].position.x).toBe(2),t(a.pause(i)).toBe(!0),t(a.runtime.emitters[0].paused).toBe(!0),a.update(.1),t(a.scheduler.getInstance(i.handle)).not.toBeNull(),t(a.play(i)).toBe(!0),t(a.runtime.emitters[0].paused).toBe(!1),t(a.setTimescale(i,2)).toBe(!0),t(a.runtime.emitters[0].timescale).toBe(2),t(a.stop(i)).toBe(!0),t(a.runtime.emitters[0].stopTime).toBeGreaterThanOrEqual(0),t(a.runtime.emitters[0].expireWithin).toBe(-1),t(i.pages).toHaveLength(2),a.flushActivePages(),t(a.runtime.getStats()).toMatchObject({activePageCount:2,simulatedCapacity:2}),t(a.destroy(i)).toBe(!0);const n=a.spawn(new o.Vector3(1,0,0));t(new Set(n.pages)).toEqual(new Set(i.pages)),t(n.handle.generation).not.toBe(i.handle.generation),a.dispose()}),a("caps GPU particle drain time for expire and kill lifecycle behavior",async()=>{const e=await T.create({asset:W(D("expiring")),maximumInstances:2,assets:{getTexture:async()=>new o.Texture}}),a=e.spawn(new o.Vector3);t(e.stop(a,.5)).toBe(!0),t(e.runtime.emitters[0].expireWithin).toBe(.5);for(let t=0;t<4;t++)e.update(.1);t(e.scheduler.getInstance(a.handle)).not.toBeNull(),e.update(.1),t(e.scheduler.getInstance(a.handle)).toBeNull();const i=e.spawn(new o.Vector3);t(e.stop(i,0)).toBe(!0),e.update(.001),t(e.scheduler.getInstance(i.handle)).toBeNull(),e.dispose()}),a("rejects a template pool before allocation when its storage budget is exceeded",async()=>{const e=await T.create({asset:W(D("over-budget")),maximumInstances:128,particlePageSize:1,maximumStorageBytes:1,assets:{getTexture:async()=>new o.Texture}});t(e).toBeNull()}),a("compiles delayed and finite continuous emission timing",()=>{const e=D("delayed");e.rate={type:"once",count:2,delay:.25};const a=D("finite");a.rate={type:"continuous",count:2,time:.1,delay:0,duration:1};const i=d(W(e,a),[new o.Vector3]);t(i.compatibility.compatible).toBe(!0),t(i.emitters[0].rate).toMatchObject({type:"once",delay:.25}),t(i.emitters[1].rate).toMatchObject({type:"continuous",delay:0,duration:1})}),a("treats zero continuous duration as unlimited like the CPU rate",()=>{const e=D("unlimited");e.rate={type:"continuous",count:2,time:.1,delay:0,duration:0};const a=d(W(e),[new o.Vector3]);t(a.compatibility.issues.map(e=>e.code)).not.toContain("rate-duration")}),a("compiles bound lifetime metadata but falls back for unsupported input targets",()=>{const e=W(D("input-test"));e.vfx.inputs=[{id:"speed",name:"Speed",type:"number",defaultValue:1}],t(d(e,[new o.Vector3]).compatibility.compatible).toBe(!0),e.vfx.emitters[0].initializers[0].params.duration.binding={type:"vfxInput",inputId:"speed"};const a=d(e,[new o.Vector3]);t(a.compatibility.compatible).toBe(!0),t(a.inputBindings).toEqual([t.objectContaining({inputId:"speed",emitterIndex:0,target:"initializer:lifetime:duration"})]),e.vfx.emitters[0].initializers.push({id:"bound-velocity",type:"velocity",enabled:!0,params:{velocity:{type:m.Vector3,value:[0,1,0],binding:{type:"vfxInput",inputId:"speed"}}}}),t(d(e,[new o.Vector3]).compatibility.issues.map(e=>e.code)).toContain("vfx-input-binding")}),a("falls back for a bound continuous lifetime until particle capacity can grow",()=>{const e=D("continuous-bound-life");e.rate={type:"continuous",count:1,time:.1,delay:0,duration:0},e.initializers[0].params.duration.binding={type:"vfxInput",inputId:"life"};const a=W(e);a.vfx.inputs=[{id:"life",name:"Life",type:"number",defaultValue:2}];const i=d(a,[new o.Vector3]);t(i.compatibility.compatible).toBe(!1),t(i.compatibility.issues.map(e=>e.code)).toContain("vfx-input-binding"),t(i.inputBindings).toEqual([])}),a("caches compiled templates while keeping instance emitter state independent",async()=>{const e=W(D("cached-template")),a=new M;let i=0;const n={assets:{getTexture:async()=>(i+=1,new o.Texture)}},s=await a.resolve(e,n),r=await a.resolve(e,n);t(r.template).toBe(s.template),t(i).toBe(1),t(a.getStats()).toMatchObject({builds:1,hits:1});const c=I(s.template,[new o.Vector3(1,0,0),new o.Vector3(2,0,0)]);t(c[0].position).not.toBe(c[1].position),c[0].position.x=100,t(c[1].position.x).toBe(2),t(s.template.emitters[0].position.x).toBe(0),a.invalidate(e),await a.resolve(e,n),t(a.getStats()).toMatchObject({builds:2,hits:1,invalidations:1}),await a.resolve(e,{...n,revision:2}),t(a.getStats()).toMatchObject({builds:3,hits:1,invalidations:2})}),a("applies a bound lifetime only to the addressed scheduled instance",async()=>{const e=D("bound-life");e.initializers[0].params.duration.binding={type:"vfxInput",inputId:"life"},e.initializers[1].params.radius.binding={type:"vfxInput",inputId:"radius"},e.initializers.push({id:"velocity",type:"velocity",enabled:!0,params:{velocity:{type:m.Vector3,value:[0,0,0],binding:{type:"vfxInput",inputId:"velocity"}}}});const a=W(e);a.vfx.inputs=[{id:"life",name:"Life",type:"number",defaultValue:1},{id:"radius",name:"Radius",type:"number",defaultValue:1},{id:"velocity",name:"Velocity",type:"vec3",defaultValue:[0,0,0]}];const i=await T.create({asset:a,maximumInstances:2,particlePageSize:1,assets:{getTexture:async()=>new o.Texture}}),n=i.spawn(new o.Vector3),s=i.spawn(new o.Vector3);t(i.setInput(n,"life",4)).toBe(!0),t(i.runtime.emitters[0].life).toBe(4),t(i.runtime.emitters[1].life).toBe(2),t(i.setInput(n,"radius",3)).toBe(!0),t(i.runtime.emitters[0].positionRadius).toBe(3),t(i.setInput(n,"velocity",new o.Vector3(1,2,3))).toBe(!0),t(i.runtime.emitters[0].velocityMin.toArray()).toEqual([1,2,3]),t(i.setInput(s,"missing",5)).toBe(!1),i.dispose()}),a("schedules different templates in one arena and merges shared render classes",async()=>{const e=L(),a=await P.create({templates:[{asset:W(D("world-a",e)),maximumInstances:2},{asset:W(D("world-b",{...e})),maximumInstances:2}],assets:{getTexture:async()=>new o.Texture},templateCache:new M,renderClassCache:new E,particlePageSize:1}),i=a.spawn(0,new o.Vector3),n=a.spawn(1,new o.Vector3(1,0,0));a.flushActivePages(),t(a.templateCount).toBe(2),t(a.runtime.getStats()).toMatchObject({renderClassCount:1,activePageCount:4}),t(a.scheduler.getStats()).toMatchObject({activeInstances:2,arena:{usedPages:4}}),t(a.destroy(i)).toBe(!0),t(a.destroy(n)).toBe(!0),a.flushActivePages(),t(a.runtime.hasActiveParticles).toBe(!1),t(a.runtime.getStats()).toMatchObject({aliveParticles:0,activePageCount:0,simulatedCapacity:0}),a.dispose()}),a("routes per-instance lifecycle changes through a shared multi-template world",async()=>{const e=D("world-lifecycle");e.position=[2,0,0];const a=await P.create({templates:[{asset:W(e),maximumInstances:1},{asset:W(D("world-neighbour")),maximumInstances:1}],assets:{getTexture:async()=>new o.Texture},particlePageSize:1}),i=a.spawn(0,new o.Vector3(3,0,0));t(a.runtime.emitters[0].position.x).toBe(5),t(a.setPosition(i,new o.Vector3(7,0,0))).toBe(!0),t(a.runtime.emitters[0].position.x).toBe(9),t(a.pause(i)).toBe(!0),t(a.runtime.emitters[0].paused).toBe(!0),t(a.play(i)).toBe(!0),t(a.setTimescale(i,.5)).toBe(!0),t(a.runtime.emitters[0].timescale).toBe(.5),t(a.stop(i,0)).toBe(!0),a.update(.001),t(a.has(i)).toBe(!1),t(a.setPosition(i,new o.Vector3)).toBe(!1),a.dispose()}),a("rejects a shared world before runtime allocation when its storage budget is exceeded",async()=>{const e=await P.create({templates:[{asset:W(D("world-over-budget")),maximumInstances:2}],assets:{getTexture:async()=>new o.Texture},particlePageSize:1,maximumStorageBytes:1});t(e).toBeNull()}),a("falls back to CPU for semantic mismatches",()=>{const e=D("soft-local",L({softness:1,space:"local",lockY:!0}));e.behaviours.push({id:"animation",type:"animation",enabled:!0,params:{}});const a=d(W(e),[new o.Vector3]),i=h(a.compatibility);t(a.compatibility.compatible).toBe(!1),t(a.compatibility.issues.map(e=>e.code)).toEqual(t.arrayContaining(["behaviour:animation"])),t(i.backend).toBe("cpu"),t(h(a.compatibility,"gpu").backend).toBe("gpu")}),a("compiles vortex with pay-for-play previous-velocity state",()=>{const e=D("vortex");e.behaviours.push({id:"vortex",type:"vortex",enabled:!0,params:{axis:k([0,5,0]),amount:G(1.25)}});const a=d(W(e),[new o.Vector3]);t(a.compatibility.compatible).toBe(!0),t(a.emitters[0].vortexAxis.toArray()).toEqual([0,5,0]),t(a.emitters[0].vortexAmount).toBe(1.25),t(C(16,!1,!1)).not.toContain("vortexVelocities");const i=C(16,!1,!0);t(i).toContain("@group(0) @binding(8)"),t(i).toContain("behaviourVelocity -= optionalStates[0u + particleIndex].xyz")}),a("compiles the remaining initializer and motion behaviours",()=>{const e=D("remaining-motion");e.initializers.push({id:"mass",type:"mass",enabled:!0,params:{min:G(2)}}),e.behaviours.push({id:"drift",type:"randomDrift",enabled:!0,params:{drift:k([1,2,3]),delay:G(.25),ease:{type:m.String,value:"OutQuad"}}},{id:"follow",type:"followParent",enabled:!0,params:{speed:G(4)}},{id:"move",type:"moveTo",enabled:!0,params:{target:k([5,6,7]),ease:{type:m.String,value:"InQuad"}}});const a=d(W(e),[new o.Vector3]);t(a.compatibility.compatible).toBe(!0),t(a.emitters[0]).toMatchObject({mass:2,randomDriftDelay:.25,randomDriftEasing:2,followParentSpeed:4,hasFollowParent:!0,moveToEasing:1,hasMoveTo:!0}),t(a.emitters[0].randomDrift.toArray()).toEqual([1,2,3]),t(a.emitters[0].moveToTarget.toArray()).toEqual([5,6,7])}),a("compiles authored easing independently for visual behavior channels",()=>{const e=D("easing");e.behaviours.push({id:"scale",type:"scale",enabled:!0,params:{a:G(0),b:G(1),ease:{type:m.String,value:"OutBack"}}},{id:"color",type:"changeColor",enabled:!0,params:{a:{type:m.Color,value:"#ff0000"},b:{type:m.Color,value:"#0000ff"},ease:{type:m.String,value:"InCubic"}}},{id:"opacity",type:"changeOpacity",enabled:!0,params:{a:G(0),b:G(1),ease:{type:m.String,value:"BumpSharp"}}});const a=d(W(e),[new o.Vector3]);t(a.compatibility.compatible).toBe(!0),t(a.emitters[0].curveEasing.toArray()).toEqual([18,4,20,0]);const i=C(16);t(i).toContain("fn applyEasing"),t(i).toContain("mix(emitter.colorStart.rgb, emitter.colorEnd.rgb, colorProgress)"),t(i).toContain("mix(emitter.scales.xy, emitter.scales.zw, scaleProgress)")}),a("compiles disperse into optional shared particle state",()=>{const e=D("disperse");e.behaviours.push({id:"disperse",type:"disperse",enabled:!0,params:{distance:G(3),axis:{type:m.String,value:"XZ"},ease:{type:m.String,value:"InOutSine"}}});const a=d(W(e),[new o.Vector3]);t(a.compatibility.compatible).toBe(!0),t(a.emitters[0].disperseDistance).toBe(3),t(a.emitters[0].dispersePlane).toBe(2),t(a.emitters[0].disperseEasing).toBe(12),t(C(16)).not.toContain("disperseStates");const i=C(16,!1,!1,!0);t(i).toContain("@group(0) @binding(8)"),t(i).toContain("optionalStates[0u + particleIndex].w = disperseProgress")}),a("consolidates every optional simulation feature into the eighth storage binding",()=>{const e=C(16,!0,!0,!0,!0,!1,!1,48,64),a=[...e.matchAll(/@group\(0\) @binding\((\d+)\) var<storage/g)].map(e=>Number(e[1]));t(a).toEqual([1,2,3,4,5,6,7,8]),t(e).toContain("optionalStates[48u + particleIndex]"),t(e).toContain("optionalStates[64u + particleIndex]"),t(e).not.toMatch(/@binding\((9|10|11)\)/)}),a("lays out optional state regions without overlap",()=>{t(g({capacity:16,particleTransforms:!0,vortex:!0,disperse:!0,trailHistoryEntries:20})).toEqual({vortexOffset:48,disperseOffset:64,collisionEventOffset:80,trailOffset:80,entries:100,bytes:1600})}),a("materializes decal outputs as shared unit-box mesh render classes",async()=>{const e=D("decal");e.output={type:"decal",space:"world",colorMap:"",alphaMap:"",color:"#ffffff",unlit:!0,intensity:1,blendingMode:"normal",bloom:!1};const a=d(W(e),[new o.Vector3]);t(a.compatibility.compatible).toBe(!0);const r=new i({color:s(n(1),1)}),c=await u(a.renderClasses,{getTexture:async()=>new o.Texture},async()=>r);t(c.renderClasses[0].meshVertices?.length).toBe(288),t(c.renderClasses[0].material).toBe(r)}),a("materializes generated shape outputs through the shared mesh path",async()=>{const e=D("shape");e.output={type:"shape",shape:"box",params:{},material:"shape-material",materialSource:"material",space:"world"};const a=d(W(e),[new o.Vector3]);t(a.compatibility.compatible).toBe(!0);const r=new i({color:s(n(1),1)}),c=await u(a.renderClasses,{getTexture:async()=>new o.Texture},async()=>r);t(c.renderClasses[0].meshVertices?.length).toBe(288)}),a("forwards the physical particle index to fragment-stage model-matrix graphs",()=>{const e=w(new i({color:s(n(1),1)})),a=v(e,!0);t(a).toContain("color: vec4<f32>, instanceIndex: u32"),t(a).toContain("gpuParticle.color, particleIndex)"),t(a).toContain("@interpolate(flat) instanceIndex: u32"),t(a).toContain("output.instanceIndex = particleIndex")}),a("compiles soft default sprites with a pay-for-play scene-depth requirement",async()=>{const e=D("soft-sprite",L({softness:2})),a=d(W(e),[new o.Vector3]);t(a.compatibility.compatible).toBe(!0);const i=(await u(a.renderClasses,{getTexture:async()=>new o.Texture})).renderClasses[0].material,n=w(i);t(Object.values(n.sceneTextures)).toContain("sceneDepth")}),a("supports local-space sprite render classes through the shared emitter transform table",async()=>{const e=D("local-sprite",L({space:"local"})),a=d(W(e),[new o.Vector3(4,5,6)]);t(a.compatibility.compatible).toBe(!0),t(a.emitters[0].localSpace).toBe(!0),t(a.emitters[0].position.toArray()).toEqual([0,0,0]);const i=await u(a.renderClasses,{getTexture:async()=>new o.Texture});t(i.renderClasses[0].localSpace).toBe(!0)}),a("falls back before rendering when an authored graph cannot be resolved",async()=>{const e=D("graph",L({materialSource:"shaderGraph",shaderGraph:{source:"asset",assetId:"missing-graph"}})),a=await y({asset:W(e),instancePositions:[new o.Vector3],assets:{getTexture:async()=>new o.Texture}});t(a.backend).toBe("cpu"),t(a.runtime).toBeNull(),t(a.reasons[0]).toContain("GPU material compilation failed")}),a("compiles default flipbook, texture, UV, and particle semantics to WGSL",async()=>{const e=D("flipbook",L({flipbook:{enabled:!0,rows:4,columns:4,fps:12,mode:"loop"}})),a=d(W(e),[new o.Vector3]),i=await u(a.renderClasses,{getTexture:async()=>new o.Texture}),n=w(i.renderClasses[0].material);t(n.fragmentMain).toContain("textureSample"),t(`${n.vertexMain}\n${n.fragmentMain}`).toContain("gpuParticle.positionAge.w"),t(n.varyings.length).toBeGreaterThan(0)}),a("supports component scale-over-life for sprites",()=>{const e=D("stretched-smoke");e.behaviours.push({id:"stretch",type:"scaleComponents",enabled:!0,params:{a:k([.25,1,1]),b:k([2,.5,1]),ease:{type:22,value:"Linear"}}});const a=d(W(e),[new o.Vector3]);t(a.compatibility.compatible).toBe(!0),t(a.emitters[0].meshScaleStart.toArray()).toEqual([.25,1,1]),t(a.emitters[0].meshScaleEnd.toArray()).toEqual([2,.5,1])}),a("compiles authored scale and rotation input bindings",()=>{const e=D("input-driven",U());e.initializers.push({id:"scale",type:"scale",enabled:!0,params:{scale:{...G(1),binding:{type:"vfxInput",inputId:"scale-input"}}}},{id:"rotation",type:"rotation",enabled:!0,params:{rotation:{...k([0,-100,0]),binding:{type:"vfxInput",inputId:"rotation-input"}}}}),e.behaviours.push({id:"rotate",type:"rotate",enabled:!0,params:{rotation:{...k([0,-30,0]),binding:{type:"vfxInput",inputId:"step-input"}}}});const a=W(e);a.vfx.inputs=[{id:"scale-input",name:"Scale",type:"number",defaultValue:1},{id:"rotation-input",name:"Rotation",type:"vec3",defaultValue:[0,-100,0]},{id:"step-input",name:"Rotation step",type:"vec3",defaultValue:[0,-30,0]}];const i=d(a,[new o.Vector3]);t(i.compatibility.compatible).toBe(!0),t(i.inputBindings.map(e=>e.target)).toEqual(["initializer:scale:scale","initializer:rotation:rotation","behaviour:rotate:rotation"])})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as t,it as a}from"vitest";import{GpuVfxScheduler as n}from"../effects/vfx/gpu-vfx-scheduler.js";e("GPU VFX scheduler",()=>{a("shares one arena across independently controlled effect handles",()=>{const e=new n({pageSize:64,maximumParticles:256}),a=e.createInstance({particleCapacity:32}),s=e.createInstance({particleCapacity:80});e.play(a),e.play(s),e.update(.05),t(e.getInstance(a)?.elapsedTime).toBeCloseTo(.05),t(e.getInstance(s)?.elapsedTime).toBeCloseTo(.05),t(e.getStats()).toMatchObject({activeInstances:2,playingInstances:2,arena:{usedPages:3,allocationCount:2}})}),a("invalidates destroyed handles before reusing their slot and pages",()=>{const e=new n({pageSize:64,maximumParticles:64}),a=e.createInstance({particleCapacity:64});t(e.destroyInstance(a)).toBe(!0);const s=e.createInstance({particleCapacity:64});t(s.slot).toBe(a.slot),t(s.generation).not.toBe(a.generation),t(e.play(a)).toBe(!1),t(e.getStats().arena.committedPages).toBe(1)}),a("tracks finite effect lifecycle centrally",()=>{const e=new n,a=e.createInstance({particleCapacity:1,duration:.15});e.play(a),e.update(.1),e.update(.1),t(e.getInstance(a)).toMatchObject({elapsedTime:.15,playing:!1}),t(e.getStats().playingInstances).toBe(0)})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as n,test as s}from"vitest";import{Bone as t,BoxGeometry as o,Group as w,MeshBasicMaterial as d,Skeleton as i,SkinnedMesh as a}from"three";import{cloneMesh as r}from"../utils/mesh.js";e("cloneMesh",()=>{s("preserves skeleton sharing between skinned submeshes",()=>{const e=new w,s=new t;e.add(s);const h=new i([s]),c=new a(new o,new d),k=new a(new o,new d);c.bind(h),k.bind(h),e.add(c,k);const l=r(e),p=[];l.traverse(e=>{!0===e.isSkinnedMesh&&p.push(e)}),n(p).toHaveLength(2),n(p[0].skeleton).toBe(p[1].skeleton),n(p[0].skeleton).not.toBe(h),n(p[0].skeleton.bones[0]).not.toBe(s)}),s("consolidates separate skeleton objects with the same bind pose",()=>{const e=new w,s=new t;s.name="hips";const h=new t;h.name="hips",e.add(s,h);const c=new a(new o,new d),k=new a(new o,new d);c.bind(new i([s])),k.bind(new i([h])),e.add(c,k);const l=r(e),p=[];l.traverse(e=>{!0===e.isSkinnedMesh&&p.push(e)}),n(p).toHaveLength(2),n(p[0].skeleton).toBe(p[1].skeleton)}),s("keeps independent source skeletons independent",()=>{const e=new w,s=new t;s.name="first";const h=new t;h.name="second",e.add(s,h);const c=new a(new o,new d),k=new a(new o,new d);c.bind(new i([s])),k.bind(new i([h])),e.add(c,k);const l=r(e),p=[];l.traverse(e=>{!0===e.isSkinnedMesh&&p.push(e)}),n(p).toHaveLength(2),n(p[0].skeleton).not.toBe(p[1].skeleton)})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{expect as e,test as t,vi as o}from"vitest";import*as i from"three";import{BaseActor as n}from"../gameplay/actors/actor.js";import{CharacterAnimationComponent as s,OldCharacterMovementComponent as
|
|
1
|
+
import{expect as e,test as t,vi as o}from"vitest";import*as i from"three";import{BaseActor as n}from"../gameplay/actors/actor.js";import{CharacterAnimationComponent as s,OldCharacterMovementComponent as a}from"../gameplay/actors/index.js";import{RootMotionClip as r}from"../gameplay/animation/root-motion.js";import{createAnimationClipInstance as c,createAnimationSubTrack as l,createSpawnClipInstance as m,createSpawnTrack as p}from"../effects/sequence/sequence-data.js";import{evaluateAnimationClipOutputProgress as u,evaluateAnimationClipSourceProgress as d,normalizeAnimationClipRetiming as h,buildRetimedAnimationClip as f}from"../effects/sequence/sequence-animation-retiming.js";import{SequencePlayer as g}from"../effects/sequence/sequence-player.js";function A(e){return Array.from(e).map(e=>Math.round(1e3*e)/1e3)}o.mock("../gameplay/actors/builtin/index.js",()=>({builtInActors:{},default:{}})),o.mock("../gameplay/actors/index.js",()=>({CharacterAnimationComponent:class{},OldCharacterMovementComponent:class{},getCharacterMovementLike:e=>e.movement??void 0})),o.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),o.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),o.mock("../effects/vfx/vfx-service.js",()=>({VfxService:class{}})),o.hoisted(()=>{if("undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}}),"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,o)=>(e[t]=o,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),t("uniform retiming mapping stays linear",()=>{e(d(void 0,.25)).toBeCloseTo(.25),e(u(void 0,.75)).toBeCloseTo(.75),e(d({mode:"uniform",points:[]},.6)).toBeCloseTo(.6)}),t("impact retiming redistributes time around the impact marker",()=>{const t={mode:"impact",points:[{id:"impact",clipProgress:.35,sourceProgress:.55}]};e(d(t,.35)).toBeCloseTo(.55),e(u(t,.55)).toBeCloseTo(.35),e(d(t,.7)).toBeGreaterThan(.7)}),t("advanced retiming sanitization stays monotonic and bounded",()=>{const t=h({mode:"advanced",points:[{id:"a",clipProgress:.9,sourceProgress:.2},{id:"b",clipProgress:-4,sourceProgress:.8},{id:"c",clipProgress:.9,sourceProgress:2},{id:"d",clipProgress:.4,sourceProgress:.1}]});e(t.points).toHaveLength(3);for(let o=1;o<t.points.length;o++)e(t.points[o].clipProgress).toBeGreaterThan(t.points[o-1].clipProgress),e(t.points[o].sourceProgress).toBeGreaterThan(t.points[o-1].sourceProgress);e(t.points[0].clipProgress).toBeGreaterThan(0),e(t.points[t.points.length-1].sourceProgress).toBeLessThan(1)}),t("retimed clip baking preserves trim boundary poses and remaps track times to clip duration",()=>{const t=new i.AnimationClip("trim-test",2,[new i.NumberKeyframeTrack("weight",[0,1,2],[0,10,20])]),o=c("anim",0,1);o.clipStartOffset=.5,o.clipEndOffset=.25,o.retiming={mode:"advanced",points:[]};const n=f(t,o),s=n.tracks[0];e(n.duration).toBeCloseTo(1),e(A(s.times)).toEqual([0,.4,1]),e(A(s.values)).toEqual([5,10,17.5])}),t("root motion track baking stays aligned with the retimed visual clip",()=>{const t=new i.AnimationClip("root-motion-test",2,[new i.VectorKeyframeTrack("hips.position",[0,1,2],[0,0,0,1,0,0,2,0,0])]),o=c("anim",0,1);o.retiming={mode:"impact",points:[{id:"impact",clipProgress:.25,sourceProgress:.5}]};const n=f(t,o),s=n.tracks[0],a=r.fromClip(n,!1);e(A(s.times)).toEqual([0,.25,1]),e(A(a.motionTrack.times)).toEqual([0,.25,1]),e(A(a.motionTrack.values)).toEqual([0,0,0,1,0,0,2,0,0])}),t("sequence player keeps uniform clips on the existing playback path",()=>{const t=new g,o=l(),n=c("anim",0,1);n.playbackRate=1.75,n.clipStartOffset=.25,o.clips.push(n);const s=new i.AnimationClip("uniform-test",2,[new i.NumberKeyframeTrack("weight",[0,1,2],[0,1,2])]);t.animationClips.set("anim",s);const a=new i.Object3D,r={target:a,mixer:new i.AnimationMixer(a),activeActions:new Map};t.evaluateAnimationSubTrack(o,r,.1);const m=r.activeActions.get(`${o.id}-${n.id}`);e(m).toBeDefined(),e(m.getClip()).toBe(s),e(m.timeScale).toBeCloseTo(1.75),e(m.time).toBeCloseTo(.25)}),t("sequence player uses baked retimed clips and still wires root motion through the action",()=>{const t=new g,o=l(),n=c("anim",0,1.5);n.rootMotion=!0,n.playbackRate=2,n.retiming={mode:"impact",points:[{id:"impact",clipProgress:.3,sourceProgress:.55}]},o.clips.push(n);const s=new i.AnimationClip("retimed-root",2,[new i.VectorKeyframeTrack("hips.position",[0,1,2],[0,0,0,1,0,0,2,0,0]),new i.NumberKeyframeTrack("weight",[0,1,2],[0,1,2])]);t.animationClips.set("anim",s);const a=new y,m=new v(a.object),p=new w;a.charAnim=m,a.movement=p;const u={target:a,activeActions:new Map};t.evaluateAnimationSubTrack(o,u,.1);const d=u.activeActions.get(`${o.id}-${n.id}`);e(d).toBeDefined(),e(d.timeScale).toBeCloseTo(1),e(d.getClip()).toBeInstanceOf(r),e(d.getClip().duration).toBeCloseTo(1.5),e(p.lastRootMotionAction).toBe(d),e(t.retimedAnimationClips.size).toBe(1)}),t("external sequence control previews root motion deterministically while scrubbing",()=>{const t=new g;t.externalTimeControl=!0;const o=l(),n=c("anim",0,2);n.rootMotion=!0,o.clips.push(n);const s=new i.AnimationClip("root-motion-preview",2,[new i.VectorKeyframeTrack("hips.position",[0,1,2],[0,0,0,1,1,0,2,2,0])]);t.animationClips.set("anim",s);const a=new y;a.object.position.set(10,3,0),a.object.scale.setScalar(2);const r=new i.Bone;r.name="hips",a.object.add(r),a.charAnim=new v(a.object);const m={target:a,activeActions:new Map};t.evaluateSubTracks([o],m,.25),e(a.object.position.x).toBeCloseTo(10.5),e(a.object.position.y).toBeCloseTo(3),t.evaluateSubTracks([o],m,1.5),e(a.object.position.x).toBeCloseTo(13),e(a.object.position.y).toBeCloseTo(3),t.evaluateSubTracks([o],m,.5),e(a.object.position.x).toBeCloseTo(11),e(a.object.position.y).toBeCloseTo(3)}),t("ending a role-bound spawn proxy releases its clamped character animation",()=>{const t=new g,o=p(),n=m(0,1),s=l(),a=c("anim",0,1);o.clips.push(n),o.subTracks.push(s),s.clips.push(a);const r=new y,u=new v(r.object);r.charAnim=u,u.play(new i.AnimationClip("ability",1,[]));const d=`${s.id}-${a.id}`,h=`${o.id}-${n.id}`,f={track:o,target:r,activeActions:new Map([[d,u.getFullBodyAction()]]),charAnimComponent:u,charAnimActionKeys:new Set([d])};t.activeClips.add(h),t.spawnedInstances.set(h,{type:"proxy",target:r}),t.evaluateSpawnTrack(o,f,1),e(f.activeActions.size).toBe(0),e(f.charAnimActionKeys.size).toBe(0),e(u.sequenceReleaseRequests).toBe(1)}),t("sequence completion releases character animation when its proxy overhangs the timeline",()=>{const t=new g,o=p(),n=l(),s=c("anim",0,2.06);o.clips.push(m(0,2.07)),o.subTracks.push(n),n.clips.push(s);const a=new y,r=new v(a.object);a.charAnim=r,r.play(new i.AnimationClip("overhanging-ability",2.06,[]));const u=`${n.id}-${s.id}`,d={track:o,target:a,activeActions:new Map([[u,r.getFullBodyAction()]]),charAnimComponent:r,charAnimActionKeys:new Set([u])};t.bindings.set(o.id,d),t.deactivateCharacterAnimationActions(),e(d.activeActions.size).toBe(0),e(d.charAnimActionKeys.size).toBe(0),e(r.sequenceReleaseRequests).toBe(1)});class y extends n{constructor(){super(...arguments),this.charAnim=null,this.movement=null}getComponent(e){return e===s?this.charAnim:e===a?this.movement:void 0}}class v{constructor(e){this.fullBodyAction=null,this.sequenceReleaseRequests=0,this.mixer=new i.AnimationMixer(e)}play(e,t={}){this.fullBodyAction=this.mixer.clipAction(e),this.fullBodyAction.setLoop(i.LoopOnce,1),this.fullBodyAction.clampWhenFinished=!0,this.fullBodyAction.timeScale=t.timeScale??1,null!=t.offset&&(this.fullBodyAction.time=t.offset),this.fullBodyAction.play()}beginExternalAnimationControl(e,t={}){return this.play(e,t),this.getFullBodyAction()}getFullBodyAction(){if(null==this.fullBodyAction)throw new Error("No full body action is active");return this.fullBodyAction}getMixer(){return this.mixer}beginExternalControl(){}endExternalControl(){}stopSequenceAnimation(){}requestSequenceAnimationRelease(){this.sequenceReleaseRequests++}}class w{constructor(){this.lastRootMotionAction=null}setRootMotionAction(e){this.lastRootMotionAction=e}}/*
|
|
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{__decorate as e,__metadata as t}from"tslib";import{describe as a,expect as s,it as c,vi as n}from"vitest";import{Object3D as r}from"three";import{createGroupTrack as o,createObjectTrack as i,createSequenceData as u,
|
|
1
|
+
import{__decorate as e,__metadata as t}from"tslib";import{describe as a,expect as s,it as c,vi as n}from"vitest";import{Object3D as r}from"three";import{createGroupTrack as o,createObjectTrack as i,createSequenceData as u,createSpawnClipInstance as l,createSpawnTrack as d,createTransformKeyframe as p,createTransformSubTrack as f,remapSequenceObjectTrackTargets as g,SequenceRole as m}from"../effects/sequence/sequence-data.js";import{Parameter as q}from"../shader/parameter.js";import{prepareClassParameters as b,prepareCustomParamsFromType as y}from"../scene/materializer.js";import{SerializedParamType as h}from"../scene/model.js";n.mock("../gameplay/actors/builtin/index.js",()=>({builtInActors:{},default:{}})),n.mock("../gameplay/actors/index.js",()=>({CharacterAnimationComponent:class{},CharacterMovementComponent:class{}})),n.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),n.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),n.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:n.fn()})),n.hoisted(()=>{if("undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}}),"undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&n.fn()),e[t]),set:(e,t,a)=>(e[t]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})});class j{constructor(){this.sequenceData=u()}}e([q({label:"Sequence Data"}),t("design:type",Object)],j.prototype,"sequenceData",void 0),a("sequence scene bindings",()=>{c("binds scene object tracks nested inside groups",async()=>{const{SequencePlayer:e}=await import("../effects/sequence/sequence-player.js"),t=new e,a=u(),c=o(),n=i(),l=new r;n.targetId="scene-actor",c.childTracks.push(n),a.tracks.push(c),t.load(a),t.bindObject("scene-actor",l),s(t.getActiveTrackObjects(n.id)).toEqual([l])}),c("gives role bindings precedence over a malformed scene target id",async()=>{const{SequencePlayer:e}=await import("../effects/sequence/sequence-player.js"),t=new e,a=u(),c=i(),n=new r,o=new r;c.targetId="scene-actor",c.role=m.Self,a.tracks.push(c),t.load(a),t.bindObject("scene-actor",n),t.bindRole(m.Self,o),s(t.getActiveTrackObjects(c.id)).toEqual([o])}),c("preserves the existing parent of a role-bound spawn proxy",async()=>{const{SequencePlayer:e}=await import("../effects/sequence/sequence-player.js"),t=new e,a=new r,c=new r,n=new r;a.add(c),c.add(n);const o=u();o.duration=1;const i=d();i.role=m.Self,i.clips.push(l(0,1)),o.tracks.push(i),t.setWorld({scene:a}),t.load(o),t.bindRole(m.Self,n),t.seek(.1),t.seek(.2),s(n.parent).toBe(c)}),c("removes a scene binding when its target can no longer be resolved",async()=>{const{SequencePlayer:e}=await import("../effects/sequence/sequence-player.js"),t=new e,a=u(),c=i();c.targetId="scene-actor",a.tracks.push(c),t.load(a),t.bindObject("scene-actor",new r),t.unbindObject("scene-actor"),s(t.getActiveTrackObjects(c.id)).toEqual([])}),c("preserves the original transform when refreshing the same binding while scrubbing",async()=>{const{SequencePlayer:e}=await import("../effects/sequence/sequence-player.js"),t=new e,a=u(),c=i(),n=f(),o=p(1);o.position=[5,0,0],n.keyframes.push(o),c.targetId="scene-actor",c.subTracks.push(n),a.tracks.push(c);const l=new r;l.position.set(2,0,0),t.load(a),t.bindObject("scene-actor",l),t.seek(1),t.bindObject("scene-actor",l),t.stop(),s(l.position.toArray()).toEqual([2,0,0])}),c("remaps grouped internal targets while preserving external references",()=>{const e=u(),t=o(),a=i(),c=i();a.targetId="internal-actor",c.targetId="external-actor",t.childTracks.push(a,c),e.tracks.push(t),g(e,new Map([["internal-actor","internal-actor-copy"]])),s(a.targetId).toBe("internal-actor-copy"),s(c.targetId).toBe("external-actor")}),c("preserves legacy raw inline sequence data when preparing editor params",()=>{const e=u(),t=i();t.targetId="scene-actor",e.tracks.push(t);const a=y(j,{sequenceData:e});s(a.sequenceData.type).toBe(h.SequenceData),s(a.sequenceData.value.tracks[0]).toMatchObject({type:"object",targetId:"scene-actor"})}),c("materializes legacy Sequence-tagged inline scene sequence data without loading a sequence asset",async()=>{const e=u(),t=i();t.targetId="scene-actor",e.tracks.push(t);const a=await b({sequenceData:{type:h.Sequence,value:e}},j,{},{});s(a.sequenceData).toBe(e),s(a.sequenceData.tracks[0]).toMatchObject({type:"object",targetId:"scene-actor"})}),c("materializes current inline scene sequence data without loading a sequence asset",async()=>{const e=u(),t=i();t.targetId="scene-actor",e.tracks.push(t);const a=await b({sequenceData:{type:h.SequenceData,value:e}},j,{},{});s(a.sequenceData).toBe(e),s(a.sequenceData.tracks[0]).toMatchObject({type:"object",targetId:"scene-actor"})}),c("materializes legacy raw inline scene sequence data after reload",async()=>{const e=u(),t=i();t.targetId="scene-actor",e.tracks.push(t);const a=await b({sequenceData:e},j,{},{});s(a.sequenceData).toBe(e),s(a.sequenceData.tracks[0]).toMatchObject({type:"object",targetId:"scene-actor"})})});/*
|
|
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*as e from"three";import{describe as s,expect as t,it as a,vi as n}from"vitest";import{createSequenceData as
|
|
1
|
+
import*as e from"three";import{describe as s,expect as t,it as a,vi as n}from"vitest";import{createSequenceData as c,createVfxClipInstance as r,createVfxTrack as o}from"../effects/sequence/sequence-data.js";import{SequencePlayer as l}from"../effects/sequence/sequence-player.js";function i(){return{object:new e.Object3D,restart:n.fn(),play:n.fn(),pause:n.fn(),previewAt:n.fn(),clearPreview:n.fn(),stop:n.fn(),getParticleCount:n.fn(()=>0)}}async function p(){await Promise.resolve(),await Promise.resolve()}n.mock("../gameplay/actors/builtin/index.js",()=>({builtInActors:{},default:{}})),n.mock("../gameplay/actors/index.js",()=>({CharacterAnimationComponent:class{},CharacterMovementComponent:class{}})),n.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),n.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),n.mock("../effects/vfx/vfx-service.js",()=>({VfxService:class{}})),n.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,s)=>(s in e||(e[s]=("string"!=typeof s||!s.startsWith("is"))&&n.fn()),e[s]),set:(e,s,t)=>(e[s]=t,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),s("sequence VFX tracks",()=>{a("preloads unique VFX assets when preparing a sequence",async()=>{const s=new l,a=n.fn(async e=>{});s.setWorld({scene:new e.Scene,preloadVisualEffects:a}),s.setActorFactory({}),s.setAssetLoader({getAsset:n.fn(async e=>({id:e,vfx:{emitters:[]}}))});const i=c(),p=o();p.clips.push(r("vfx-asset",0,1),r("vfx-asset",1,1),r("other-vfx-asset",2,1)),i.tracks.push(p),s.load(i),await s.prepare(),t(a).toHaveBeenCalledTimes(1),t([...a.mock.calls[0][0]]).toHaveLength(2)}),a("restarts an already-active VFX clip when seeking backwards within the clip",async()=>{const s=i(),a=new l;a.setWorld({scene:new e.Scene}),a.setVfxService({createFromAssetId:n.fn(async()=>s)});const d=c();d.duration=2;const f=o(),v=r("vfx-asset",0,1.5);f.clips.push(v),d.tracks.push(f),a.load(d),a.play(),await p(),a.seek(1),s.restart.mockClear(),s.play.mockClear(),a.seek(.25),t(s.restart).toHaveBeenCalledTimes(1),t(s.play).toHaveBeenCalledTimes(1)}),a("restarts active VFX clips when a looping sequence wraps to the start",async()=>{const s=i(),a=new l;a.setWorld({scene:new e.Scene}),a.setVfxService({createFromAssetId:n.fn(async()=>s)});const d=c();d.duration=1,d.loop=!0;const f=o(),v=r("vfx-asset",0,1);f.clips.push(v),d.tracks.push(f),a.load(d),a.play(),await p(),s.restart.mockClear(),s.play.mockClear(),a.update(1.1),t(s.restart).toHaveBeenCalledTimes(1),t(s.play).toHaveBeenCalledTimes(1)}),a("does not replay a VFX while its sequence clip remains active",async()=>{const s=i(),a=new l;a.setWorld({scene:new e.Scene}),a.setVfxService({createFromAssetId:n.fn(async()=>s)});const d=c();d.duration=1;const f=o();f.clips.push(r("vfx-asset",0,.8)),d.tracks.push(f),a.load(d),a.play(),await p(),t(s.play).toHaveBeenCalledTimes(1),a.update(.3),a.update(.3),t(s.play).toHaveBeenCalledTimes(1)}),a("clears VFX particles when seeking backwards before the clip starts",async()=>{const s=i(),a=new l;a.setWorld({scene:new e.Scene}),a.setVfxService({createFromAssetId:n.fn(async()=>s)});const d=c();d.duration=2;const f=o(),v=r("vfx-asset",.5,1);f.clips.push(v),d.tracks.push(f),a.load(d),a.play(),a.seek(.75),await p(),s.restart.mockClear(),s.stop.mockClear(),s.pause.mockClear(),a.seek(.25),t(s.restart).toHaveBeenCalledTimes(1),t(s.stop).toHaveBeenCalled(),t(s.pause).toHaveBeenCalledTimes(1)}),a("rebuilds VFX at the playhead when an editor preview seeks while paused",async()=>{const s=i(),a=n.fn(async()=>s),d=new l;d.setWorld({scene:new e.Scene}),d.setVfxService({createFromAssetId:a}),d.externalTimeControl=!0,d.inEditor=!0;const f=c();f.duration=2;const v=o();v.clips.push(r("vfx-asset",.5,1)),f.tracks.push(v),d.load(f),d.seek(.75),await p(),t(a).toHaveBeenCalledWith("vfx-asset",t.any(e.Scene),{backend:"cpu"}),t(s.previewAt).toHaveBeenCalledWith(.25,1,"finish",.25),t(s.play).not.toHaveBeenCalled()}),a("keeps gameplay sequence VFX on the configured world backend",async()=>{const s=i(),a=n.fn(async()=>s),d=new l;d.setWorld({scene:new e.Scene}),d.setVfxService({createFromAssetId:a});const f=c();f.duration=1;const v=o();v.clips.push(r("vfx-asset",0,1)),f.tracks.push(v),d.load(f),d.play(),await p(),t(a).toHaveBeenCalledWith("vfx-asset",t.any(e.Scene),void 0)}),a("clears an editor VFX preview when seeking before its clip",async()=>{const s=i(),a=new l;a.setWorld({scene:new e.Scene}),a.setVfxService({createFromAssetId:n.fn(async()=>s)}),a.externalTimeControl=!0,a.inEditor=!0;const d=c();d.duration=2;const f=o();f.clips.push(r("vfx-asset",.5,1)),d.tracks.push(f),a.load(d),a.seek(.75),await p(),s.clearPreview.mockClear(),a.seek(.25),t(s.clearPreview).toHaveBeenCalledTimes(1)})});/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as t,expect as e,it as r}from"vitest";import*as o from"three";import{applyMeshWorldTransformToGeometry as s}from"../effects/vfx/vfx-mesh-transform.js";t("CPU VFX mesh transforms",()=>{r("bakes imported hierarchy transforms into private geometry",()=>{const t=(new o.BufferGeometry).setAttribute("position",new o.Float32BufferAttribute([0,1,0],3)),r=new o.Group;r.rotation.z=Math.PI/2,r.scale.setScalar(2);const i=new o.Mesh(t);i.position.set(1,0,0),r.add(i);const f=t.clone();s(i,f),e(f).not.toBe(t),e((new o.Vector3).fromBufferAttribute(f.getAttribute("position"),0).toArray()).toEqual([-2,2,0]),e((new o.Vector3).fromBufferAttribute(t.getAttribute("position"),0).toArray()).toEqual([0,1,0])})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
package/dist/utils/mesh.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
-
import type { Object3D } from 'three';
|
|
2
|
+
import type { Object3D, SkinnedMesh } from 'three';
|
|
3
3
|
export declare class NoRaycastMesh extends THREE.Mesh {
|
|
4
4
|
raycast(...args: any[]): void;
|
|
5
5
|
}
|
|
6
6
|
export declare function cloneMesh<T extends Object3D>(o: T): T;
|
|
7
|
+
/**
|
|
8
|
+
* Merges skinned submeshes that differ only by geometry but represent the same
|
|
9
|
+
* named material slot. Meshes in different bind spaces are deliberately left
|
|
10
|
+
* alone because rebasing their vertices would change the skinning equation.
|
|
11
|
+
*/
|
|
12
|
+
export declare function mergeCompatibleSkinnedMeshes(root: Object3D, shouldSkip?: (mesh: SkinnedMesh) => boolean): void;
|
|
7
13
|
//# sourceMappingURL=mesh.d.ts.map
|
package/dist/utils/mesh.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import*as e from"three";import{
|
|
1
|
+
import*as e from"three";import{clone as t}from"three/examples/jsm/utils/SkeletonUtils.js";import{mergeGeometries as n,mergeGroups as r}from"three/examples/jsm/utils/BufferGeometryUtils.js";export class NoRaycastMesh extends e.Mesh{raycast(...e){}}export function cloneMesh(e){const n=t(e),r=[],o=[];e.traverse(e=>r.push(e)),n.traverse(e=>o.push(e));const i=new Map,l=[];for(let e=0;e<r.length;e++){const t=r[e],n=o[e];if(!0!==t?.isSkinnedMesh||!0!==n?.isSkinnedMesh)continue;let a=i.get(t.skeleton);null==a&&(a=l.find(e=>s(t.skeleton,e.source))?.clone),null!=a?(i.set(t.skeleton,a),n.bind(a,n.bindMatrix)):(i.set(t.skeleton,n.skeleton),l.push({source:t.skeleton,clone:n.skeleton}))}return n}function s(e,t){if(e.bones.length!==t.bones.length||e.boneInverses.length!==t.boneInverses.length)return!1;for(let n=0;n<e.bones.length;n++){if(e.bones[n].name!==t.bones[n].name)return!1;const r=e.boneInverses[n].elements,s=t.boneInverses[n].elements;for(let e=0;e<16;e++)if(r[e]!==s[e])return!1}return!0}export function mergeCompatibleSkinnedMeshes(t,s=()=>!1){t.updateWorldMatrix(!0,!0);const l=t.matrixWorld.clone().invert(),a=[];t.traverse(e=>{const t=e;if(!0!==t.isSkinnedMesh||Array.isArray(t.material)||null==t.material||s(t))return;if(Object.keys(t.geometry.morphAttributes).length>0)return;const n=l.clone().multiply(t.matrixWorld),r=a.find(e=>{return e.skeleton===t.skeleton&&o(e.bindMatrix,t.bindMatrix)&&o(e.relativeMatrix,n)&&(r=e.material,s=t.material,r===s||r.name.length>0&&r.name===s.name)&&function(e,t){return e.visible===t.visible&&e.renderOrder===t.renderOrder&&e.frustumCulled===t.frustumCulled&&e.layers.mask===t.layers.mask}(e.meshes[0],t);var r,s});null==r?a.push({material:t.material,skeleton:t.skeleton,bindMatrix:t.bindMatrix.clone(),relativeMatrix:n,meshes:[t],geometries:[t.geometry]}):(r.meshes.push(t),r.geometries.push(t.geometry))});for(const s of a){if(s.meshes.length<2||!i(s.geometries))continue;const o=n(s.geometries,!1);if(null==o)continue;o.groups.length>1&&r(o);const l=new e.SkinnedMesh(o,s.material);l.copy(s.meshes[0],!1),l.geometry=o,l.material=s.material,s.relativeMatrix.decompose(l.position,l.quaternion,l.scale),l.matrix.copy(s.relativeMatrix),l.bind(s.skeleton,s.bindMatrix);for(const e of s.meshes)e.removeFromParent(),e.geometry.dispose();t.add(l)}}function o(e,t,n=1e-6){return e.elements.every((e,r)=>Math.abs(e-t.elements[r])<=n)}function i(e){const t=e[0],n=Object.keys(t.attributes).sort();return e.slice(1).every(e=>null==e.index==(null==t.index)&&Object.keys(e.attributes).sort().join("|")===n.join("|")&&n.every(n=>{const r=t.getAttribute(n),s=e.getAttribute(n);return r.itemSize===s.itemSize&&r.normalized===s.normalized&&r.array.constructor===s.array.constructor}))}/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hology/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.238",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"exports": {
|
|
17
17
|
".": "./dist/index.js",
|
|
18
18
|
"./rendering": "./dist/rendering.js",
|
|
19
|
+
"./rendering/node-material-webgpu-resolver": "./dist/rendering/node-material-webgpu-resolver.js",
|
|
19
20
|
"./utils/math": "./dist/utils/math.js",
|
|
20
21
|
"./utils/files": "./dist/utils/files.js",
|
|
21
22
|
"./utils/polyfill": "./dist/utils/polyfill.js",
|
|
@@ -53,6 +54,8 @@
|
|
|
53
54
|
"./effects/vfx/vfx-actor": "./dist/effects/vfx/vfx-actor.js",
|
|
54
55
|
"./effects/vfx/vfx-asset": "./dist/effects/vfx/vfx-asset.js",
|
|
55
56
|
"./effects/vfx/vfx-defs": "./dist/effects/vfx/vfx-defs.js",
|
|
57
|
+
"./effects/vfx/vfx-runtime": "./dist/effects/vfx/vfx-runtime.js",
|
|
58
|
+
"./effects/vfx": "./dist/effects/vfx/index.js",
|
|
56
59
|
"./config/project-config": "./dist/config/project-config.js",
|
|
57
60
|
"./worker": "./dist/worker/index.js"
|
|
58
61
|
},
|
|
@@ -62,6 +65,9 @@
|
|
|
62
65
|
"rendering": [
|
|
63
66
|
"./dist/rendering.d.ts"
|
|
64
67
|
],
|
|
68
|
+
"rendering/node-material-webgpu-resolver": [
|
|
69
|
+
"./dist/rendering/node-material-webgpu-resolver.d.ts"
|
|
70
|
+
],
|
|
65
71
|
"utils/math": [
|
|
66
72
|
"./dist/utils/math.d.ts"
|
|
67
73
|
],
|
|
@@ -173,6 +179,12 @@
|
|
|
173
179
|
"effects/vfx/vfx-defs": [
|
|
174
180
|
"./dist/effects/vfx/vfx-defs.d.ts"
|
|
175
181
|
],
|
|
182
|
+
"effects/vfx/vfx-runtime": [
|
|
183
|
+
"./dist/effects/vfx/vfx-runtime.d.ts"
|
|
184
|
+
],
|
|
185
|
+
"effects/vfx": [
|
|
186
|
+
"./dist/effects/vfx/index.d.ts"
|
|
187
|
+
],
|
|
176
188
|
"effects/sequence": [
|
|
177
189
|
"./dist/effects/sequence/index.d.ts"
|
|
178
190
|
],
|
|
@@ -197,14 +209,14 @@
|
|
|
197
209
|
},
|
|
198
210
|
"dependencies": {
|
|
199
211
|
"@babel/runtime": "^7.24.8",
|
|
200
|
-
"@dimforge/rapier3d-simd-compat": "^0.
|
|
212
|
+
"@dimforge/rapier3d-simd-compat": "^0.20.0",
|
|
201
213
|
"@hology/nebula": "^0.0.160",
|
|
202
214
|
"@plumier/reflect": "^1.1.0",
|
|
203
215
|
"@recast-navigation/three": "0.39.0",
|
|
204
216
|
"recast-navigation": "0.39.0",
|
|
205
217
|
"rxjs": "7.8.1",
|
|
206
218
|
"three-mesh-bvh": "^0.7.5",
|
|
207
|
-
"three-shader-graph": "0.2.
|
|
219
|
+
"three-shader-graph": "0.2.62",
|
|
208
220
|
"three-stdlib": "2.34.0",
|
|
209
221
|
"ts-key-enum": "^2.0.12",
|
|
210
222
|
"typedi": "^0.10.0"
|