@hology/core 0.0.273 → 0.0.274
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.js +1 -1
- package/dist/effects/vfx/gpu-sprite-runtime.js +1 -1
- package/dist/gameplay/actors/builtin/components/character/character-movement.d.ts +19 -0
- package/dist/gameplay/actors/builtin/components/character/character-movement.js +1 -1
- package/dist/gameplay/actors/builtin/water-spline-volume-actor.d.ts +10 -0
- package/dist/gameplay/actors/builtin/water-spline-volume-actor.js +1 -1
- package/dist/gameplay/actors/builtin/water-volume-actor.d.ts +3 -1
- package/dist/gameplay/actors/builtin/water-volume-actor.js +1 -1
- package/dist/gameplay/actors/camera/first-person-camera-component.js +1 -1
- package/dist/gameplay/actors/camera/third-person-camera-component.d.ts +7 -0
- package/dist/gameplay/actors/camera/third-person-camera-component.js +1 -1
- package/dist/gameplay/ai/behavior-tree/move.d.ts +5 -0
- package/dist/gameplay/ai/behavior-tree/move.js +1 -1
- package/dist/gameplay/ai/behavior-tree/move.test.d.ts +2 -0
- package/dist/gameplay/ai/behavior-tree/move.test.js +4 -0
- package/dist/gameplay/animation/root-motion.js +1 -1
- package/dist/gameplay/input/input-service.d.ts +9 -1
- package/dist/gameplay/input/input-service.js +1 -1
- package/dist/gameplay/services/physics/physics-system.d.ts +2 -0
- package/dist/gameplay/services/physics/physics-system.js +1 -1
- package/dist/gameplay/services/render.d.ts +4 -1
- package/dist/gameplay/services/render.js +1 -1
- package/dist/gameplay/services/world.d.ts +5 -0
- package/dist/rendering/node-material-webgpu-resolver.js +1 -1
- package/dist/rendering/webgpu-experimental-backend.d.ts +4 -2
- package/dist/rendering/webgpu-experimental-backend.js +1 -1
- package/dist/rendering.d.ts +17 -2
- package/dist/rendering.js +1 -1
- package/dist/scene/asset-resource-loader.d.ts +4 -0
- package/dist/scene/asset-resource-loader.js +1 -1
- package/dist/scene/collision/collision-shape-import.js +1 -1
- package/dist/scene/collision/rapier-shape.js +1 -1
- package/dist/scene/landscape/landscape-geometry.d.ts +24 -0
- package/dist/scene/landscape/landscape-geometry.js +4 -0
- package/dist/scene/landscape/landscape-manager.d.ts +1 -0
- package/dist/scene/landscape/landscape-manager.js +1 -1
- package/dist/scene/materializer.d.ts +7 -1
- package/dist/scene/materializer.js +1 -1
- package/dist/scene/scene-data-service.d.ts +2 -1
- package/dist/scene/scene-data-service.js +1 -1
- package/dist/scene/sky.d.ts +2 -0
- package/dist/scene/sky.js +1 -1
- package/dist/shader/graph/compiler.js +1 -1
- package/dist/shader/graph/registry.js +1 -1
- package/dist/shader/parameter.js +1 -1
- package/dist/shader/sprite-shader.js +1 -1
- package/dist/shader-nodes/depth.d.ts +6 -4
- package/dist/shader-nodes/depth.js +1 -1
- package/dist/shader-nodes/index.d.ts +2 -1
- package/dist/shader-nodes/index.js +1 -1
- package/dist/shader-nodes/surface-reflection.d.ts +9 -0
- package/dist/shader-nodes/surface-reflection.js +4 -0
- package/dist/shader-nodes/water-volume.d.ts +6 -2
- package/dist/shader-nodes/water-volume.js +1 -1
- package/dist/test/asset-resource-loader-texture-array.test.js +1 -1
- package/dist/test/character-movement-solve.test.js +1 -1
- package/dist/test/collision-shape-import.test.js +1 -1
- package/dist/test/first-person-camera-component.test.js +1 -1
- package/dist/test/frame-stats.test.js +1 -1
- package/dist/test/gpu-sprite-compiler.test.js +1 -1
- package/dist/test/input.test.js +1 -1
- package/dist/test/landscape-geometry.test.d.ts +2 -0
- package/dist/test/landscape-geometry.test.js +4 -0
- package/dist/test/material-assignment.test.js +1 -1
- package/dist/test/materializer-dynamic-transform.test.js +1 -1
- package/dist/test/materializer-prefetch.test.js +1 -1
- package/dist/test/net-character-movement.test.js +1 -1
- package/dist/test/node-material-webgpu-resolver.test.js +1 -1
- package/dist/test/parameter-definition.test.js +1 -1
- package/dist/test/physics-hitstop.test.d.ts +2 -0
- package/dist/test/physics-hitstop.test.js +4 -0
- package/dist/test/physics-ray-object.test.js +1 -1
- package/dist/test/post-process-effect.test.js +1 -1
- package/dist/test/rapier-trimesh.test.d.ts +2 -0
- package/dist/test/rapier-trimesh.test.js +4 -0
- package/dist/test/sequence-animation-retiming.test.js +1 -1
- package/dist/test/sequence-vfx.test.js +1 -1
- package/dist/test/surface-reflection.test.d.ts +2 -0
- package/dist/test/surface-reflection.test.js +4 -0
- package/dist/test/water-spline-volume-actor.test.js +1 -1
- package/dist/utils/three/frame-stats.d.ts +8 -0
- package/dist/utils/three/frame-stats.js +1 -1
- package/package.json +2 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{expect as t,test as e,vi as o}from"vitest";import{DoubleSide as n,PlaneGeometry as r,Ray as s,Vector3 as a}from"three";import{MeshBVH as c}from"three-mesh-bvh";import{LandscapeGeometryBuilder as i}from"../scene/landscape/landscape-geometry.js";function u(t,e=1){const o=new r(100,100,t,t);o.rotateX(-Math.PI/2);const n=o.getAttribute("position");for(let t=0;t<n.count;t++)n.setY(t,e*(Math.sin(.3*n.getX(t))*Math.cos(.2*n.getZ(t))+.1*n.getX(t)));return o.computeVertexNormals(),o}e("landscape tangents match Three.js across scratch reuse, steep slopes and refitted indices",()=>{const e=new i;for(const[o,n]of[[1,0],[12,30],[12,-20],[40,30]]){const r=u(o,n),s=r.getAttribute("normal");s.setXYZ(0,.8*s.getX(0),.8*s.getY(0),.8*s.getZ(0)),e.updateBoundsTree(r,100,o);const a=r.clone();a.computeTangents(),e.computeTangents(r);const c=r.getAttribute("tangent").array.slice(),i=a.getAttribute("tangent").array;for(let e=0;e<c.length;e++)t(c[e]).toBeCloseTo(i[e],6);e.computeTangents(r),t(r.getAttribute("tangent").array).toEqual(c)}}),e("unsculpted planes use constant tangents and fall back after seam edits",()=>{const e=new i,o=new r(100,100,32,32);o.rotateX(-Math.PI/2),e.computeTangents(o);const n=o.getAttribute("tangent");for(let e=0;e<n.count;e++)t([n.getX(e),n.getY(e),n.getZ(e),n.getW(e)]).toEqual([1,0,0,1]);for(const n of[!1,!0]){n?(o.getAttribute("position").setY(0,10),o.computeVertexNormals()):o.getAttribute("normal").setXYZ(0,.3,.9,.1);const r=o.clone();r.computeTangents(),e.computeTangents(o);const s=r.getAttribute("tangent").array,a=o.getAttribute("tangent").array;for(let e=0;e<a.length;e++)t(a[e]).toBeCloseTo(s[e],6)}}),e("prepared planes own their buffers and skip refitting only while unmodified",()=>{const e=new i,n=e.createGeometry(100,32),r=e.createGeometry(100,32);t(n.getAttribute("position").array).not.toBe(r.getAttribute("position").array),t(n.index.array).not.toBe(r.index.array);const u=o.spyOn(c.prototype,"refit");try{e.updateBoundsTree(n,100,32,e.computeTangents(n)),t(u).not.toHaveBeenCalled();const o=new s(new a(0,100,0),new a(0,-1,0));t(n.boundsTree.raycastFirst(o).point.y).toBeCloseTo(0,8);const c=r.getAttribute("position");for(let t=0;t<c.count;t++)c.setY(t,20);r.computeVertexNormals(),e.updateBoundsTree(r,100,32,e.computeTangents(r)),t(u).toHaveBeenCalledTimes(1),t(r.boundsTree.raycastFirst(o).point.y).toBeCloseTo(20,8),t(n.boundsTree.raycastFirst(o).point.y).toBeCloseTo(0,8),t(r.boundingSphere.center.y).toBeCloseTo(20,8)}finally{u.mockRestore()}const l=e.createGeometry(50,12);t(l.getAttribute("position").count).toBe(169),t(l.boundingBox.max.x).toBeCloseTo(25,4)}),e("template BVH raycasts match rebuilt terrain and section bounds remain independent",()=>{const e=new i,o=u(32,1);e.updateBoundsTree(o,100,32);const r=o.boundingBox.clone(),l=u(32,25);e.updateBoundsTree(l,100,32);const g=new c(l.clone());for(let e=0;e<100;e++){const o=37*e%97-48,r=53*e%97-48;for(const e of[new a(0,-1,0),new a(.1,-1,.05).normalize()]){const c=new s(new a(o,500,r),e),i=l.boundsTree.raycastFirst(c,n),u=g.raycastFirst(c,n);t(null==i).toBe(null==u),null!=u&&t(i.distance).toBeCloseTo(u.distance,5)}}t(o.boundsTree.getBoundingBox(r.clone())).toEqual(r);const p=l.getAttribute("position");for(let t=0;t<p.count;t++)p.setY(t,p.getY(t)+200);e.updateBoundsTree(l,100,32),t(l.boundingBox.min.y).toBeGreaterThan(0),t(o.boundsTree.getBoundingBox(r.clone())).toEqual(r);const d=new s(new a(0,500,0),new a(0,-1,0));t(l.boundsTree.raycastFirst(d).point.y).toBeCloseTo(200,5)});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{describe as e,expect as
|
|
1
|
+
import{describe as e,expect as a,it as t,vi as n}from"vitest";import{BoxGeometry as o,BufferAttribute as s,BufferGeometry as r,Group as i,Mesh as c,MeshBasicMaterial as l,ShaderMaterial as m,Texture as f}from"three";n.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,a)=>(a in e||(e[a]=("string"!=typeof a||!a.startsWith("is"))&&n.fn()),e[a]),set:(e,a,t)=>(e[a]=t,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),n.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),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()}));import{applyMaterial as d,getConvertedFbxToGlbAssignedMaterialTextureFlipY as w}from"../scene/materializer.js";import{AssetResourceLoader as y}from"../scene/asset-resource-loader.js";e("material assignments",()=>{t("preserves Body UVs and slot assignments through optimization and cached mesh copies",async()=>{const e=new r;e.setAttribute("position",new s(new Float32Array([0,0,0,1,0,0,0,1,0,0,0,1,1,0,1,0,1,1]),3)),e.setAttribute("uv",new s(new Float32Array([0,0,1,0,0,1,.25,.125,.75,.125,.25,.875]),2)),e.setIndex([0,1,2,3,4,5]),e.addGroup(0,3,0),e.addGroup(3,3,1);const t=new l;t.name="Head";const o=new l;o.name="Body";const f=new i;f.add(new c(e,[t,o]));const w=new y;w.setDataDir("data"),n.spyOn(w,"loadByAsset").mockResolvedValue({scene:f,animations:[]});const p={name:"Body",color:"#ffffff",materialId:"body"},u={id:"mesh",name:"mesh",type:"mesh",fileKey:"mesh.glb",fileFormat:"glb",materialAssignments:[p]},g=new m,B=n.fn(async()=>g),h=await w.getMesh(u,{mergeGeomtries:!0,applyMaterials:!0,materialResolver:B}),v=await w.getMesh(u,{mergeGeomtries:!0});await d(v.scene,p,B);const b=v.scene.clone(),x=new m;await d(b,p,async()=>x);for(const[e,n]of[[h.scene,g],[v.scene,g],[b,x]]){const o=[];e.traverse(e=>{e instanceof c&&o.push(e)});const s=o.find(e=>e.material===n);a(s).toBeDefined(),a(Array.from(s.geometry.getAttribute("uv").array)).toEqual([.25,.125,.75,.125,.25,.875]),a(o.find(e=>e.material===t)).toBeDefined()}}),t.each([!1,!0])("reassigns an authored slot after a shader material was applied (array: %s)",async e=>{const t=new l;t.name="Body";const n=new f;n.flipY=!1;const s=new m({uniforms:{map:{value:n}}}),r=new f;r.flipY=!0;const i=new m({uniforms:{map:{value:r}}}),w=new c(new o,e?[t]:t),y=new Map,p={name:"Body",color:"#ffffff",materialId:"first"};await d(w,p,async()=>s,y),await d(w,{...p,materialId:"second"},async()=>i,y),a(e?w.material[0]:w.material).toBe(i),a(y.get(e?`${w.id}#0`:w.id)).toBe(t);const u=w.clone();await d(u,p,async()=>s),a(e?u.material[0]:u.material).toBe(s)}),t("keeps color-based slots reassignable after applying a shader material",async()=>{const e=new l({color:"#123456"}),t=new c(new o,e),n=new m,s=new m,r={color:"#123456",materialId:"first"};await d(t,r,async()=>n),await d(t,{...r,materialId:"second"},async()=>s),a(t.material).toBe(s)}),t("matches named material assignments by name when the source color changed",async()=>{const e=new l({color:"#ffffff"});e.name="Body";const t=new l({color:"#123456"}),n=new c(new o,e);await d(n,{color:"#e7e7e7",name:"Body",materialId:"replacement"},async()=>t),a(n.material).toBe(t)}),t("keeps color matching for unnamed material assignments",async()=>{const e=new l({color:"#e7e7e7"}),t=new l({color:"#123456"}),n=new c(new o,e);await d(n,{color:"#e7e7e7",materialId:"replacement"},async()=>t),a(n.material).toBe(t)}),t("does not use color as a fallback when a non-empty assignment name is present",async()=>{const e=new l({color:"#e7e7e7"});e.name="Head";const t=new l({color:"#123456"}),n=new c(new o,e);await d(n,{color:"#e7e7e7",name:"Body",materialId:"replacement"},async()=>t),a(n.material).toBe(e)}),t("uses glTF texture orientation for converted FBX material assignments without cloning geometry",async()=>{const e=new l({color:"#e7e7e7"}),t=new f;t.flipY=!0;const n=new l({color:"#123456",map:t}),s=new o,r=new c(s,e);await d(r,{color:"#e7e7e7",materialId:"replacement"},async()=>n,void 0,{textureFlipY:!1});const i=r.material;a(i).not.toBe(n),a(i.map).not.toBe(t),a(i.map?.flipY).toBe(!1),a(t.flipY).toBe(!0),a(r.geometry).toBe(s)}),t("preserves runtime material userData when cloning for texture orientation",async()=>{class e{}const t=new e,n=new l({color:"#e7e7e7"}),s=new f;s.flipY=!0;const r=new l({color:"#123456",map:s});r.userData.surface=t;const i=new c(new o,n);await d(i,{color:"#e7e7e7",materialId:"replacement"},async()=>r,void 0,{textureFlipY:!1});const m=i.material;a(m).not.toBe(r),a(m.userData.surface).toBe(t),a(m.userData.surface).toBeInstanceOf(e)}),t("infers glTF texture orientation for older converted FBX asset metadata",()=>{a(w({fileFormat:"glb",originalFileKey:"models/tree.FBX"})).toBe(!1),a(w({fileFormat:"glb",originalFileKey:"models/tree.fbx",mesh:{uvConversion:{source:"fbx2gltf",flipV:!1}}})).toBeUndefined()})});/*
|
|
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{Subject as e}from"rxjs";import{
|
|
1
|
+
import{Subject as e}from"rxjs";import{BufferGeometry as t,Float32BufferAttribute as a,Group as i,Mesh as n,Object3D as r,Quaternion as o,Scene as s,Vector3 as c}from"three";import{describe as l,expect as d,it as p,vi as m}from"vitest";m.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&m.fn()),e[t]),set:(e,t,a)=>(e[t]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),m.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),m.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),m.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),m.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:m.fn()}));import{PhysicsBodyType as y}from"../gameplay/services/physics/physics-system.js";import{SceneMaterializer as u}from"../scene/materializer.js";function h(t,a={getObjects:()=>[],onCreate:()=>{},onUpdate:()=>{},onRemove:()=>{}}){const i={onCreate:new e,onDelete:new e,onUpdate:new e,getAsset:m.fn(async()=>null),getAssets:m.fn(async()=>[])};return new u(t,a,i,{},{renderer:{shadowMap:{needsUpdate:!1}},invalidateShadows:m.fn(),refreshStaticSpatialObjects:m.fn(),registerStaticSpatialObject:m.fn(()=>m.fn())},[],[],{create:async()=>null,initActor:async()=>{}})}function g(e){return{name:e.id,...e}}l("SceneMaterializer dynamic transforms",()=>{p("clears both paint attributes when undo restores an empty paint state",()=>{const e=h(new s);e.inEditor=!1;const i=new n((new t).setAttribute("position",new a([0,0,0],3)));i.name="0,0";const r=g({id:"landscape",type:"shape_mesh",vertexMaterials:[{m:"0,0",i:0,w:[.1,.2,.3,.4,.5,.6,.7,.8]}]});e.applyVertexMaterials(r,i);const o=i.geometry.getAttribute("material"),c=i.geometry.getAttribute("material2"),l=o.version,p=c.version;r.vertexMaterials=[],e.applyVertexMaterials(r,i),d(Array.from(o.array)).toEqual([0,0,0,0]),d(Array.from(c.array)).toEqual([0,0,0,0]),d(o.version).toBeGreaterThan(l),d(c.version).toBeGreaterThan(p)}),p("clears sections omitted from paint state without allocating attributes on unpainted sections",()=>{const e=h(new s);e.inEditor=!1;const r=new i,o=new n((new t).setAttribute("position",new a([0,0,0],3)));o.name="0,0",o.geometry.setAttribute("material",new a([1,1,1,1],4)),o.geometry.setAttribute("material2",new a([1,1,1,1],4));const c=new n(o.geometry.clone());c.name="1,0",c.geometry.deleteAttribute("material"),c.geometry.deleteAttribute("material2"),r.add(o,c),e.applyVertexMaterials(g({id:"landscape",type:"shape_mesh",vertexMaterials:[{m:"1,0",i:0,w:[.5]}]}),r),d(Array.from(o.geometry.getAttribute("material").array)).toEqual([0,0,0,0]),d(Array.from(o.geometry.getAttribute("material2").array)).toEqual([0,0,0,0]),d(Array.from(c.geometry.getAttribute("material2").array)).toEqual([0,0,0,0]);const l=new n((new t).setAttribute("position",new a([0,0,0],3)));e.applyVertexMaterials(g({id:"empty",type:"shape_mesh",vertexMaterials:[]}),l),d(l.geometry.hasAttribute("material")).toBe(!1),d(l.geometry.hasAttribute("material2")).toBe(!1)}),p("recreates a changed object type and authored descendants without leaving stale scene objects",async()=>{const e=new s,t=h(e);t.inEditor=!0;const a=g({id:"child",type:"group",position:[1,2,3]}),i=g({id:"parent",type:"group",children:[a],position:[4,5,6]}),n=await t.materialize(i),r=e.getObjectByName("child");i.type="shape_mesh",i.shape="box",await t.update(i);const o=e.getObjectByName("parent"),c=e.getObjectByName("child");d(o).not.toBe(n),d(n.parent).toBeNull(),d(r.parent).toBeNull(),d(c).not.toBe(r),d(c.parent).toBe(o),d(c.userData.src).toBe(a),d(o.position.toArray()).toEqual([4,5,6]),d(c.position.toArray()).toEqual([1,2,3]),d(t.sceneObjectMap.get(a.id)).toBe(c)}),p("serializes explicit recreations while a previous replacement is loading",async()=>{let e;const t=h(new s,{getObjects:()=>[],onCreate:()=>{},onRemove:()=>{},onUpdate:t=>{e=t}});t.inEditor=!0;const a=g({id:"parent",type:"group"});let i;await t.materialize(a);const n=new Promise(e=>{i=e}),r=t.materializeAndInitActor.bind(t);let o=0;m.spyOn(t,"materializeAndInitActor").mockImplementation(async e=>(1===++o&&await n,r(e))),e(a,!1,!0),e(a,!1,!0),await Promise.resolve(),await Promise.resolve(),d(o).toBe(1),i(),await t.whenReplacementsComplete(),d(o).toBe(2),d(t.sceneObjectMap.get(a.id)).toBeDefined()}),p("does not disable matrix updates on an actor root after an editor transform",()=>{const e=h(new s);e.inEditor=!0;const t=new i,a=t.updateMatrixWorld;e.beginEditorStaticSceneEdit([t]),e.endEditorStaticSceneEdit(),d(t.matrixAutoUpdate).toBe(!0),d(t.updateMatrixWorld).toBe(a)}),p("indexes authored static meshes, releases their registration, and leaves actor-owned meshes dynamic",async()=>{const e=new s,t=h(e),a=t.renderingView,n=g({id:"static",type:"shape_mesh",shape:"box"}),r=await t.materialize(n);d(a.registerStaticSpatialObject).toHaveBeenCalledExactlyOnceWith(r);const o=a.registerStaticSpatialObject.mock.results[0].value;t.beginEditorStaticSceneEdit([r]),d(a.refreshStaticSpatialObjects).toHaveBeenLastCalledWith(r,!0),t.endEditorStaticSceneEdit(),d(a.refreshStaticSpatialObjects).toHaveBeenLastCalledWith(r,!1);const c=new i;c.userData.actor={},e.add(c),await t.materialize(g({id:"actor-mesh",type:"shape_mesh",shape:"box"}),c),d(a.registerStaticSpatialObject).toHaveBeenCalledTimes(1),t.remove(n),d(o).toHaveBeenCalledOnce()}),p("preserves a nested dynamic object world transform when moving it to the scene root",async()=>{const e=new s,t=h(e);t.inEditor=!1;const a=[.2,.7,-.1],n=[0,0,0],l=g({id:"parent",type:"group",position:[10,4,-7],rotation:a,scale:[2,3,4],children:[g({id:"dynamic-child",type:"shape_mesh",shape:"box",position:[3,-2,5],rotation:n,scale:[.5,1.5,2],physics:{type:y.dynamic}})]});await t.materialize(l);const p=e.getObjectByName("dynamic-child");d(p?.parent).toBe(e);const m=new i;m.position.set(10,4,-7),m.rotation.set(...a),m.scale.set(2,3,4);const u=new r;u.position.set(3,-2,5),u.rotation.set(...n),u.scale.set(.5,1.5,2),m.add(u);const w=u.getWorldPosition(new c),f=u.getWorldQuaternion(new o),b=u.getWorldScale(new c);d(p.position.distanceTo(w)).toBeLessThan(1e-6),d(1-Math.abs(p.quaternion.dot(f))).toBeLessThan(1e-6),d(p.scale.distanceTo(b)).toBeLessThan(1e-6)})});/*
|
|
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 e,expect as t,it as a,vi as s}from"vitest";import{Scene as
|
|
1
|
+
import{describe as e,expect as t,it as a,vi as s}from"vitest";import{Scene as i,Texture as r}from"three";import{Subject as n}from"rxjs";s.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&s.fn()),e[t]),set:(e,t,a)=>(e[t]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),s.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),s.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),s.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),s.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:s.fn()}));import{SceneMaterializer as o}from"../scene/materializer.js";import{DetailTier as d,SerializedParamType as l}from"../scene/model.js";function c(e,t){const a=s.fn(async(e,t)=>({scene:{},animations:[]})),d=s.fn(async e=>new r),l=s.fn(async e=>({texture:new r,layerIndex:0})),c=s.fn(),p={getObjects:()=>e,onCreate:()=>{},onUpdate:()=>{},onRemove:()=>{}},m={onCreate:new n,onDelete:new n,onUpdate:new n,getAsset:s.fn(async e=>t.get(e)??null),getAssets:s.fn(async()=>Array.from(t.values()))};return{materializer:new o(new i,p,m,{getMesh:a,getTexture:d,getTextureArray:l},{renderer:{initTexture:c},initTexture:c,onLoop:s.fn()},[],[],{create:async()=>null,initActor:async()=>{}}),getMesh:a,getTexture:d,getTextureArray:l,initTexture:c}}function p(e){return{name:e.id,...e}}function m(e,t){return{id:e,name:e,type:"mesh",fileKey:`${e}.glb`,fileFormat:"glb",mesh:t}}function f(e,t){return{id:e,name:e,type:"prefab",prefab:{objects:t}}}function u(e,t){return{id:e,name:e,type:"material",material:{type:"shader",side:0,params:{},shaderParams:{map:{type:l.Texture,value:t}}}}}function h(e){return{id:e,name:e,type:"texture",fileKey:`${e}.png`,fileFormat:"png"}}function y(e){return{color:"#ffffff",materialId:e}}e("SceneMaterializer asset prefetch",()=>{a("materializes independent objects while mesh prefetch is still pending",async()=>{const e=m("slow-mesh"),a=[p({id:"shape",type:"shape_mesh"})],{materializer:r,getMesh:n}=c(a,new Map([[e.id,e]]));let o;a.push(p({id:"mesh",type:"asset_mesh",assetId:e.id})),n.mockImplementation(()=>new Promise(e=>{o=()=>e({scene:{},animations:[]})}));const d=s.spyOn(r,"materialize").mockResolvedValue(new i);for(const e of["initActorsPostInit","generateOcclusionClusters","announcePrefabEventGraphs","refreshPaintedScatterPresence","refreshSurfaceScatterPresence","rebuildEditorStaticDrawSet"])s.spyOn(r,e).mockResolvedValue(void 0);let l=!1;const f=r.init().then(()=>{l=!0});await s.waitFor(()=>t(d).toHaveBeenCalledWith(a[0])),t(n).toHaveBeenCalledTimes(1),t(l).toBe(!1),o(),await f,t(l).toBe(!0)}),a("shares VFX warmup across characters and starts independent effects concurrently",async()=>{const{materializer:e}=c([],new Map);let a;e.inEditor=!1;const r=new Promise(e=>{a=e}),o=[0,1].map(()=>({object:new i,prepareRendering:s.fn(()=>r),onEndPlay:s.fn(),disposed:new n})),d=o.map((e,t)=>({asset:{id:`vfx-${t}`,vfx:{emitters:[]}},create:s.fn(async()=>e)})),l=e.preloadVisualEffects(d),p=e.preloadVisualEffects([d[0]]);await Promise.resolve(),t(d.every(e=>1===e.create.mock.calls.length)).toBe(!0),t(o.every(e=>1===e.prepareRendering.mock.calls.length)).toBe(!0),t(o.every(e=>0===e.onEndPlay.mock.calls.length)).toBe(!0),a(),await Promise.all([l,p]),await e.preloadVisualEffects(d),t(d.every(e=>1===e.create.mock.calls.length)).toBe(!0),t(o.every(e=>1===e.onEndPlay.mock.calls.length)).toBe(!0)}),a("retries a failed VFX preload",async()=>{const{materializer:e}=c([],new Map);e.inEditor=!1;const a={asset:{id:"vfx",vfx:{emitters:[]}},create:s.fn().mockRejectedValueOnce(new Error("load failed")).mockResolvedValue({object:new i,prepareRendering:s.fn(async()=>{}),onEndPlay:s.fn(),disposed:new n})};await t(e.preloadVisualEffects([a])).rejects.toThrow("load failed"),await e.preloadVisualEffects([a]),t(a.create).toHaveBeenCalledTimes(2)}),a("prefetches unique materialized asset meshes without creating discarded instances",async()=>{const e=m("mesh-a"),a=m("mesh-b"),s=m("mesh-c"),i=m("mesh-disabled"),r=m("mesh-high",{detailTier:d.high}),n=f("prefab-a",[p({id:"prefab-mesh-c",type:"asset_mesh",assetId:s.id}),p({id:"prefab-duplicate-mesh-a",type:"asset_mesh",assetId:e.id}),p({id:"prefab-cycle",type:"prefab",assetId:"prefab-a"})]),o=new Map([e,a,s,i,r,n].map(e=>[e.id,e])),l=[p({id:"root",type:"group",children:[p({id:"mesh-a-1",type:"asset_mesh",assetId:e.id}),p({id:"mesh-a-2",type:"asset_mesh",assetId:e.id}),p({id:"disabled-group",type:"group",enabled:!1,children:[p({id:"disabled-mesh",type:"asset_mesh",assetId:i.id})]}),p({id:"prefab-instance",type:"prefab",assetId:n.id}),p({id:"high-detail-mesh",type:"asset_mesh",assetId:r.id})]}),p({id:"mesh-b",type:"asset_mesh",assetId:a.id})],{materializer:u,getMesh:h}=c(l,o);u.detailTier=d.low,await u.preInit(),await u.prefetchAssets(),t(h).toHaveBeenCalledTimes(3),t(new Set(h.mock.calls.map(([e])=>e.id))).toEqual(new Set([e.id,a.id,s.id])),t(h.mock.calls.every(([,e])=>!0===e?.mergeGeomtries)).toBe(!0),t(h.mock.calls.every(([,e])=>!1===e?.clone)).toBe(!0)}),a("initializes textures from nested groups and prefab assets",async()=>{const e=h("texture-root"),a=h("texture-group"),s=h("texture-prefab"),i=h("texture-skipped"),r=u("material-root",e.id),n=u("material-group",a.id),o=u("material-prefab",s.id),d=u("material-skipped",i.id),l=f("prefab-a",[p({id:"prefab-group",type:"group",children:[p({id:"prefab-shape",type:"shape_mesh",materialAssignments:[y(o.id)]})]}),p({id:"prefab-cycle",type:"prefab",assetId:"prefab-a"})]),m=new Map([e,a,s,i,r,n,o,d,l].map(e=>[e.id,e])),g=[p({id:"root-group",type:"group",children:[p({id:"nested-mesh",type:"asset_mesh",materialAssignments:[y(n.id)]}),p({id:"disabled-group",type:"group",enabled:!1,children:[p({id:"skipped-mesh",type:"asset_mesh",materialAssignments:[y(d.id)]})]}),p({id:"prefab-instance",type:"prefab",assetId:l.id})]}),p({id:"root-shape",type:"shape_mesh",materialAssignments:[y(r.id)]})],{materializer:w,getTexture:b,initTexture:v}=c(g,m);await w.preInit(),await w.initTextures(),t(b).toHaveBeenCalledTimes(3),t(new Set(b.mock.calls.map(([e])=>e.id))).toEqual(new Set([e.id,a.id,s.id])),t(v).toHaveBeenCalledTimes(3)})});/*
|
|
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{beforeAll as e,describe as t,expect as o,test as a}from"vitest";import{CharacterMovementMode as n}from"../gameplay/actors/builtin/components/character/modes.js";let c,s;function r(e={}){return{sequence:1,clientTime:.016,dt:.016,inputX:0,inputY:0,yaw:0,flags:0,clientX:0,clientY:0,clientZ:0,rootMotionX:0,rootMotionY:0,rootMotionZ:0,...e}}function i(e={}){return{sequence:0,x:0,y:0,z:0,velocityX:0,velocityY:0,velocityZ:0,yaw:0,mode:n.walking,flags:0,horizontalSpeed:0,...e}}function l(e){return{directionInput:{},jumpInput:{},sprintInput:{},rotationInput:{},setRootMotionAction(){},applyImpulse(){},...e}}e(async()=>{"undefined"!=typeof HTMLCanvasElement&&Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>({fillStyle:"",fillRect(){},clearRect(){},getImageData:()=>({data:new Uint8ClampedArray(4)}),putImageData(){},createImageData:()=>({data:new Uint8ClampedArray(4)}),setTransform(){},drawImage(){},save(){},restore(){},beginPath(){},moveTo(){},lineTo(){},closePath(){},stroke(){},translate(){},scale(){},rotate(){},arc(){},fill(){},measureText:()=>({width:0}),transform(){},rect(){},clip(){}})}),c=await import("../gameplay/actors/builtin/components/character/net-character-movement-protocol.js"),s=(await import("../gameplay/actors/builtin/components/character/character-movement-like.js")).getCharacterMovementLike}),t("net character movement packets",()=>{a("round trips batched saved moves",()=>{const e=[r({sequence:1,dt:.016,inputX:.25,inputY:1,yaw:.5,flags:c.MOVE_FLAG_CROUCH|c.MOVE_FLAG_SLIDE_START,clientX:1,clientY:2,clientZ:3}),r({sequence:2,dt:.033,inputX:-.5,inputY:.5,yaw:-1,flags:1,clientX:4,clientY:5,clientZ:6,rootMotionZ:.2})],t=c.decodeNetCharacterMoveBatch(c.encodeNetCharacterMoveBatch(e));o(t.ok).toBe(!0),t.ok&&(o(t.value.moves).toHaveLength(2),o(t.value.moves[0].sequence).toBe(1),o(t.value.moves[0].clientTime).toBeCloseTo(.016,3),o(t.value.moves[0].dt).toBeCloseTo(.016,3),o(t.value.moves[0].inputX).toBeCloseTo(.25,4),o(t.value.moves[0].flags).toBe(c.MOVE_FLAG_CROUCH|c.MOVE_FLAG_SLIDE_START),o(t.value.moves[1].rootMotionZ).toBeCloseTo(.2))}),a("round trips owner ack, correction, and simulated state packets",
|
|
1
|
+
import{beforeAll as e,describe as t,expect as o,test as a}from"vitest";import{CharacterMovementMode as n}from"../gameplay/actors/builtin/components/character/modes.js";let c,s;function r(e={}){return{sequence:1,clientTime:.016,dt:.016,inputX:0,inputY:0,yaw:0,flags:0,clientX:0,clientY:0,clientZ:0,rootMotionX:0,rootMotionY:0,rootMotionZ:0,...e}}function i(e={}){return{sequence:0,x:0,y:0,z:0,velocityX:0,velocityY:0,velocityZ:0,yaw:0,mode:n.walking,flags:0,horizontalSpeed:0,...e}}function l(e){return{directionInput:{},jumpInput:{},sprintInput:{},rotationInput:{},setRootMotionAction(){},applyImpulse(){},...e}}e(async()=>{"undefined"!=typeof HTMLCanvasElement&&Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>({fillStyle:"",fillRect(){},clearRect(){},getImageData:()=>({data:new Uint8ClampedArray(4)}),putImageData(){},createImageData:()=>({data:new Uint8ClampedArray(4)}),setTransform(){},drawImage(){},save(){},restore(){},beginPath(){},moveTo(){},lineTo(){},closePath(){},stroke(){},translate(){},scale(){},rotate(){},arc(){},fill(){},measureText:()=>({width:0}),transform(){},rect(){},clip(){}})}),c=await import("../gameplay/actors/builtin/components/character/net-character-movement-protocol.js"),s=(await import("../gameplay/actors/builtin/components/character/character-movement-like.js")).getCharacterMovementLike}),t("net character movement packets",()=>{a("round trips batched saved moves",()=>{const e=[r({sequence:1,dt:.016,inputX:.25,inputY:1,yaw:.5,flags:c.MOVE_FLAG_CROUCH|c.MOVE_FLAG_SLIDE_START,clientX:1,clientY:2,clientZ:3}),r({sequence:2,dt:.033,inputX:-.5,inputY:.5,yaw:-1,flags:1,clientX:4,clientY:5,clientZ:6,rootMotionZ:.2})],t=c.decodeNetCharacterMoveBatch(c.encodeNetCharacterMoveBatch(e));o(t.ok).toBe(!0),t.ok&&(o(t.value.moves).toHaveLength(2),o(t.value.moves[0].sequence).toBe(1),o(t.value.moves[0].clientTime).toBeCloseTo(.016,3),o(t.value.moves[0].dt).toBeCloseTo(.016,3),o(t.value.moves[0].inputX).toBeCloseTo(.25,4),o(t.value.moves[0].flags).toBe(c.MOVE_FLAG_CROUCH|c.MOVE_FLAG_SLIDE_START),o(t.value.moves[1].rootMotionZ).toBeCloseTo(.2))}),a.each([n.sliding,n.swimming])("round trips owner ack, correction, and simulated state packets in mode %s",e=>{const t=c.SNAPSHOT_FLAG_CROUCHING|c.SNAPSHOT_FLAG_CROUCH_SUPPRESSED,a=i({sequence:42,x:10,y:2,z:-3,velocityX:1,yaw:1.25,mode:e,flags:t,horizontalSpeed:7}),n=c.decodeNetCharacterOwnerAck(c.encodeNetCharacterOwnerAck(42)),s=c.decodeNetCharacterOwnerCorrection(c.encodeNetCharacterOwnerCorrection(a)),r=c.decodeNetCharacterSimulatedState(c.encodeNetCharacterSimulatedState(a));o(n.ok&&n.value.sequence).toBe(42),o(s.ok&&s.value.snapshot.sequence).toBe(42),o(s.ok&&s.value.snapshot.flags).toBe(t),o(s.ok&&s.value.snapshot.mode).toBe(e),o(r.ok&&r.value.mode).toBe(e),o(r.ok&&r.value.flags).toBe(t),o(r.ok&&r.value.horizontalSpeed).toBeCloseTo(7)}),a("rejects malformed, unsupported version, and wrong-kind packets",()=>{const e=c.encodeNetCharacterMoveBatch([r({sequence:1})]),t=e.slice();t[0]=99;const a=e.slice();a[1]=99,o(c.decodeNetCharacterMoveBatch(new Uint8Array([1,1,0]))).toMatchObject({ok:!1,reason:"malformed"}),o(c.decodeNetCharacterMoveBatch(t)).toMatchObject({ok:!1,reason:"unsupported-version"}),o(c.decodeNetCharacterMoveBatch(a)).toMatchObject({ok:!1,reason:"wrong-kind"}),o(c.NET_CHARACTER_MOVEMENT_VERSION).toBe(4)})}),t("net character prediction helpers",()=>{a("combines adjacent equivalent saved moves by default",()=>{const e=new c.NetCharacterSavedMoveBuffer(4);e.push(r({sequence:1,clientTime:.016,dt:.016,inputY:1,clientZ:.1})),e.push(r({sequence:2,clientTime:.032,dt:.016,inputY:1,clientZ:.3}));const t=e.getMovesForSend(4);o(t).toHaveLength(1),o(t[0].sequence).toBe(2),o(t[0].dt).toBeCloseTo(.032),o(t[0].clientZ).toBeCloseTo(.3)}),a("keeps adjacent saved moves separate when combining is disabled",()=>{const e=new c.NetCharacterSavedMoveBuffer(4,-1);e.push(r({sequence:1,clientTime:.016,dt:.016,inputY:1,clientZ:.1})),e.push(r({sequence:2,clientTime:.032,dt:.016,inputY:1,clientZ:.3}));const t=e.getMovesForSend(4);o(t.map(e=>e.sequence)).toEqual([1,2]),o(t.map(e=>e.dt)).toEqual([.016,.016])}),a("preserves a sprint start move for server simulation and replay",()=>{const e=new c.NetCharacterSavedMoveBuffer(4),t=c.MOVE_FLAG_SPRINT|c.MOVE_FLAG_SPRINT_START;e.push(r({sequence:1,clientTime:.016,dt:.016,inputY:1,flags:t})),e.push(r({sequence:2,clientTime:.032,dt:.016,inputY:1,flags:c.MOVE_FLAG_SPRINT}));const a=c.encodeNetCharacterMoveBatch(e.getMovesForSend(4)),n=c.decodeNetCharacterMoveBatch(a);o(n.ok).toBe(!0),n.ok&&(o(n.value.moves.map(e=>e.sequence)).toEqual([1,2]),o(n.value.moves[0].flags).toBe(t))}),a("preserves a slide start move for server simulation and replay",()=>{const e=new c.NetCharacterSavedMoveBuffer(4),t=c.MOVE_FLAG_CROUCH|c.MOVE_FLAG_SLIDE_START;e.push(r({sequence:1,clientTime:.016,dt:.016,inputY:1,flags:t})),e.push(r({sequence:2,clientTime:.032,dt:.016,inputY:1,flags:c.MOVE_FLAG_CROUCH}));const a=c.encodeNetCharacterMoveBatch(e.getMovesForSend(4)),n=c.decodeNetCharacterMoveBatch(a);o(n.ok).toBe(!0),n.ok&&(o(n.value.moves.map(e=>e.sequence)).toEqual([1,2]),o(n.value.moves[0].flags).toBe(t))}),a("does not combine saved moves across crouch changes",()=>{const e=new c.NetCharacterSavedMoveBuffer(4);e.push(r({sequence:1,clientTime:.016,dt:.016,inputY:1})),e.push(r({sequence:2,clientTime:.032,dt:.016,inputY:1,flags:c.MOVE_FLAG_CROUCH}));const t=e.getMovesForSend(4);o(t.map(e=>e.sequence)).toEqual([1,2]),o(t.map(e=>e.flags)).toEqual([0,c.MOVE_FLAG_CROUCH])}),a("combines adjacent crouched saved moves",()=>{const e=new c.NetCharacterSavedMoveBuffer(4);e.push(r({sequence:1,clientTime:.016,dt:.016,inputY:1,flags:c.MOVE_FLAG_CROUCH})),e.push(r({sequence:2,clientTime:.032,dt:.016,inputY:1,flags:c.MOVE_FLAG_CROUCH}));const t=e.getMovesForSend(4);o(t).toHaveLength(1),o(t[0].sequence).toBe(2),o(t[0].flags).toBe(c.MOVE_FLAG_CROUCH)}),a("respects saved move max combined delta time",()=>{const e=new c.NetCharacterSavedMoveBuffer(4,.02);e.push(r({sequence:1,clientTime:.016,dt:.016,inputY:1})),e.push(r({sequence:2,clientTime:.032,dt:.016,inputY:1})),o(e.getMovesForSend(4).map(e=>e.sequence)).toEqual([1,2])}),a("does not combine a move after it has been selected for sending",()=>{const e=new c.NetCharacterSavedMoveBuffer(4,.1);e.push(r({sequence:1,clientTime:.016,dt:.016,inputY:1})),o(e.getMovesForSend(4).map(e=>e.sequence)).toEqual([1]),e.push(r({sequence:2,clientTime:.032,dt:.016,inputY:1}));const t=e.getMovesForSend(4);o(t.map(e=>e.sequence)).toEqual([1,2]),o(t.map(e=>e.dt)).toEqual([.016,.016])}),a("selects the oldest unacked move and newest moves for send packets",()=>{const e=new c.NetCharacterSavedMoveBuffer(8);for(let t=1;t<=5;t++)e.push(r({sequence:t,clientTime:.02*t,dt:.02,inputX:t%2==0?.25:-.25}));o(e.getMovesForSend(3).map(e=>e.sequence)).toEqual([1,4,5])}),a("replays saved moves with a deterministic simulator",()=>{const e=c.replayNetCharacterSavedMoves({x:0},[r({sequence:1,dt:.1,inputY:1}),r({sequence:2,dt:.2,inputY:1})],(e,t)=>({x:e.x+t.inputY*t.dt*10}));o(e.x).toBeCloseTo(3)}),a("validates move sequence, dt, and normalized input",()=>{o(c.validateNetCharacterMove(r({sequence:2,dt:.016}),1)).toBe(!0),o(c.validateNetCharacterMove(r({sequence:1,dt:.016}),1)).toBe(!1),o(c.validateNetCharacterMove(r({sequence:2,clientTime:0}),1)).toBe(!1),o(c.validateNetCharacterMove(r({sequence:2,dt:.5}),1,.1)).toBe(!1),o(c.validateNetCharacterMove(r({sequence:2,inputX:1,inputY:1}),1)).toBe(!1),o(c.validateNetCharacterMove(r({sequence:2,flags:128}),1)).toBe(!1)}),a("derives server move dt from client timestamps",()=>{const e=c.createNetCharacterServerMoveTimingState();c.accrueNetCharacterServerMoveTime(e,.05);const t=c.consumeNetCharacterServerMoveTime(e,r({sequence:1,clientTime:.016}),{maxMoveDeltaTime:.1}),a=c.consumeNetCharacterServerMoveTime(e,r({sequence:2,clientTime:.05}),{maxMoveDeltaTime:.1});o(t).toBeCloseTo(.016),o(a).toBeCloseTo(.034),o(e.lastClientTime).toBeCloseTo(.05)}),a("clamps server move dt and rejects clients too far ahead of server time",()=>{const e=c.createNetCharacterServerMoveTimingState();c.accrueNetCharacterServerMoveTime(e,.02);const t=c.consumeNetCharacterServerMoveTime(e,r({sequence:1,clientTime:.5}),{maxMoveDeltaTime:.05,maxServerMoveDeltaTimeScalar:2,maxClientTimeAhead:.1}),a=c.consumeNetCharacterServerMoveTime(e,r({sequence:2,clientTime:.7}),{maxMoveDeltaTime:.05,maxServerMoveDeltaTimeScalar:2,maxClientTimeAhead:.01});o(t).toBeCloseTo(.1),o(a).toBe(-1)})}),t("net character simulated proxy smoothing",()=>{a("interpolates delayed snapshots",()=>{const e=new c.NetCharacterProxySmoother(.1,20),t=i();e.push(i({sequence:1,x:0}),0),e.push(i({sequence:2,x:10}),1),o(e.sample(.6,t)).toBe(!0),o(t.x).toBeCloseTo(5)}),a("snaps instead of interpolating large gaps",()=>{const e=new c.NetCharacterProxySmoother(0,1),t=i();e.push(i({sequence:1,x:0}),0),e.push(i({sequence:2,x:10}),1),o(e.sample(.5,t)).toBe(!0),o(t.x).toBe(10)})}),t("character movement compatibility",()=>{a("prefers the default movement component when present",()=>{const e=l({horizontalSpeed:2}),t=l({horizontalSpeed:5,isDefaultCharacterMovementComponent:!0});o(s({oldMovement:e,defaultMovement:t,attachedComponents:[]})).toBe(t)})});/*
|
|
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 e,expect as t,it as o,vi as n}from"vitest";import{DataTexture as a,Material as r,MeshPhysicalMaterial as i,MeshStandardMaterial as s,RGBAFormat as l,SpriteMaterial as d,Vector4 as h}from"three";import{batchingUniformVec4 as u,float as m,NodeShaderMaterial as c,rgb as p,rgba as f,standardMaterial as v,translateX as S,uniformFloat as g,uniformVec4 as w,uniformSampler2d as x,varyingAttributes as C,vec3 as y,vec4 as B}from"three-shader-graph";import{createNodeMaterialSignature as b,markMaterialUniformValuesNeedUpdate as M,NodeMaterialWebGpuResolver as T,setMaterialUniformValuesAutoUpdate as L,withThreeDefaultVertexAttributeValues as W}from"../rendering/node-material-webgpu-resolver.js";import{StandardShader as _}from"../shader/builtin/standard-shader.js";import{LandscapeCompositeShader as G}from"../shader/builtin/landscape-composite-shader.js";import{DecalUnlitShader as A}from"../shader/builtin/decal-unlit-shader.js";import{getSpritePosition as U}from"../shader/sprite-shader.js";import{depthNormal as V,depthWorldPosition as k,fragmentLinearEyeDepth as I,sampleSceneDepth as j,screenUV as D}from"../shader-nodes/depth.js";import{sampleSceneColor as q}from"../shader-nodes/scene-sample.js";import{applyDithering as E}from"../shader-nodes/dither.js";import{parallaxOcclusionMapping as O}from"../shader-nodes/pom.js";import{elapsedTimeUniformName as F}from"../shader-nodes/time.js";import{Trail as P}from"../effects/vfx/trail-renderer.js";import{particleUniforms as N}from"../shader-nodes/particle.js";import{buildShaderGraphMaterial as z,createDefaultShaderGraphDocument as R}from"../shader/graph/index.js";class H extends r{set uniformValuesNeedsUpdate(e){e&&this.uniformValuesAutoVersion++}constructor(e){super(),this.disposed=!1,this.uniformValuesAutoUpdate=!0,this.uniformValuesAutoVersion=0,this.vertexShader=e.vertexShader,this.fragmentShader=e.fragmentShader,this.depthVertexShader=e.depthVertexShader??null,this.depthFragmentShader=e.depthFragmentShader??null,this.pointShadowVertexShader=e.pointShadowVertexShader??null,this.pointShadowFragmentShader=e.pointShadowFragmentShader??null,this.uniforms=e.uniforms,this.sceneTextures=e.sceneTextures??{},this.graphTextures=e.graphTextures??{},this.vertexAttributes=e.vertexAttributes??[],this.transparent=e.transparent??!1,this.alphaTest=e.alphaTest??0,this.depthTest=e.depthTest??!0,this.depthWrite=e.depthWrite??!0,this.side=e.side??this.side}dispose(){this.disposed=!0,super.dispose()}}e("NodeMaterialWebGpuResolver",()=>{o("compiles physical lobes and binds transmission to the HDR late-pass snapshot",()=>{const e=new _;e.clearcoat=1,e.iridescence=1,e.sheen=.4,e.anisotropy=.5,e.transmission=1,e.thickness=.2,e.dispersion=.5;const o=e.build();t(o.uniforms.hology_transmission_map).toBeDefined();const n=new T({WGSLShaderMaterial:H}),a=n.resolve(o,"late");t(a.graphTextures.hology_transmission_map).toMatchObject({resource:"physicalTransmissionSnapshot",requiresLatePass:!0}),t(a.fragmentShader).toContain("tsgTransmission_volume"),t(a.fragmentShader).toContain("tsgIridescence_eval"),t(a.fragmentShader).toContain("textureNumLevels"),t(a.fragmentShader).not.toContain("vec3 "),t(a.fragmentShader).not.toMatch(/\bmat3\(/),n.dispose()}),o("omits disabled physical features from ordinary materials",()=>{const e=new T({WGSLShaderMaterial:H}),o=(new _).build(),n=e.resolve(o,"opaque");t(n.graphTextures).toEqual({}),t(o.uniforms.transmission).toBeUndefined(),t(o.uniforms.clearcoat).toBeUndefined(),t(o.uniforms.iridescence).toBeUndefined(),t(n.fragmentShader).not.toContain("tsgTransmission_volume"),e.dispose()}),o("preserves native physical properties and updates enabled lobe uniforms",()=>{const e=new T({WGSLShaderMaterial:H}),o=new i({clearcoat:.5,iridescence:.2}),n=e.resolve(o);o.clearcoat=.7,t(e.resolve(o)).toBe(n),t(n.uniforms.clearcoat.value).toBe(.7),o.transmission=1;const a=e.resolve(o);t(a).not.toBe(n),t(a.fragmentShader).toContain("tsgTransmission_volume"),t(a.graphTextures.hology_transmission_map).toMatchObject({requiresLatePass:!0}),e.dispose()}),o("keeps explicitly tracked outline uniforms clean until controls change",()=>{const e=new c({color:v({color:p(16777215)})});e.uniformValuesAutoUpdate=!1,e.userData.outlineParameters={thickness:.012,darkening:.65,alpha:.8};const o=new T({WGSLShaderMaterial:H}),n=o.resolve(e,"outline");t(n.uniformValuesAutoUpdate).toBe(!1);const a=n.uniformValuesAutoVersion;t(o.resolve(e,"outline")).toBe(n),t(n.uniformValuesAutoVersion).toBe(a),e.userData.outlineParameters.thickness=.025,o.resolve(e,"outline"),t(n.uniforms.outlineThickness.value).toBe(.025),t(n.uniformValuesAutoVersion).toBeGreaterThan(a),o.dispose()}),o("derives a textured, darkened albedo outline and updates its controls",()=>{const e=new a(new Uint8Array([128,96,64,255]),1,1,l);e.needsUpdate=!0;const o=x("outlineAlbedo",e).sample(C.uv),n=new c({color:v({color:o})});n.userData.outlineParameters={colorMode:"darkenedAlbedo",thickness:.012,darkening:.65,alpha:.8};const r=new T({WGSLShaderMaterial:H}),i=r.resolve(n,"outline");t(i).toBeInstanceOf(H),t(i.side).toBe(1),t(i.vertexShader).toContain("materialUniforms.outlineThickness"),t(i.vertexAttributes).toEqual(t.arrayContaining([t.objectContaining({name:"skinIndex"}),t.objectContaining({name:"skinWeight"})])),t(i.vertexShader).toContain("rendererSkinPosition"),t(i.vertexShader).toContain("outline_local_position_"),t(i.vertexShader).toContain("outline_local_normal_"),t(i.vertexShader).toContain("sqrt("),t(i.fragmentShader).toContain("outlineAlbedoTexture"),t(i.fragmentShader).toContain("materialUniforms.outlineDarkening"),t(i.uniforms.outlineThickness.value).toBe(.012),t(i.uniforms.outlineDarkening.value).toBe(.65),t(i.uniforms.outlineAlpha.value).toBe(.8),n.userData.outlineParameters.darkening=2,n.userData.outlineParameters.thickness=.02;const s=r.resolve(n,"outline");t(s).toBe(i),t(s.uniforms.outlineThickness.value).toBe(.02),t(s.uniforms.outlineDarkening.value).toBe(1)}),o("declines an albedo outline when the source graph has no albedo output",()=>{const e=new c({color:f(16777215)}),o=new T({WGSLShaderMaterial:H});t(o.resolve(e,"outline")).toBeUndefined()}),o("compiles shader graph scene-light nodes against the renderer light ABI",()=>{const e=R("surface");e.shadingModel="unlit",e.nodes.push({id:"ambient",kind:"scene.ambientLight"},{id:"directional",kind:"scene.directionalLight"},{id:"combined",kind:"math.add"}),e.edges.push({from:{nodeId:"ambient",port:"color"},to:{nodeId:"combined",port:"a"}},{from:{nodeId:"directional",port:"color"},to:{nodeId:"combined",port:"b"}}),e.outputs.color={input:{nodeId:"combined",port:"value"}};const o=new T({WGSLShaderMaterial:H}).resolve(z(e));t(o?.fragmentShader).toContain("rendererAmbientLight()"),t(o?.fragmentShader).toContain("rendererDirectionalLightForLight"),t(o?.fragmentShader).toContain("rendererDirectionalLightCount()"),t(o?.fragmentShader).not.toContain("NUM_DIR_LIGHTS")}),o("compiles the shader graph world normal without a WGSL matrix inverse",()=>{const e=R("surface");e.shadingModel="unlit",e.nodes.push({id:"world-normal",kind:"builtin.worldNormal"}),e.outputs.color={input:{nodeId:"world-normal",port:"value"}};const o=new T({WGSLShaderMaterial:H}).resolve(z(e));t(o?.vertexShader).toContain("transpose(rendererViewMatrix())"),t(o?.vertexShader).not.toContain("nodeInverseMat4")}),o("compiles decal surface rejection without undefined WGSL expressions",()=>{const e=(new A).build(),o=new T({WGSLShaderMaterial:H}).resolve(e);t(o?.fragmentShader).not.toContain("undefined"),t(o?.fragmentShader).toContain("hology_scene_mapTexture")}),o("recompiles an opaque variant when receiveDecals changes",()=>{const e=new c({color:f(16777215)});e.userData.receiveDecals=!0;const o=new T({WGSLShaderMaterial:H},e=>[f(16777215,!1===e.userData.receiveDecals?0:2)]),n=o.resolve(e,"opaque");e.userData.receiveDecals=!1,e.needsUpdate=!0;const a=o.resolve(e,"opaque");t(a).not.toBe(n),t(n.disposed).toBe(!0),t(n.fragmentShader).not.toBe(a.fragmentShader)}),o("maps Hology elapsed time to the renderer frame clock",()=>{const e=g(F),o=new c({color:f(y(e)),transform:S(e)}),n=new T({WGSLShaderMaterial:H}).resolve(o);t(n?.vertexShader).toContain("rendererElapsedTime()"),t(n?.fragmentShader).toContain("rendererElapsedTime()"),t(n?.depthVertexShader).toContain("rendererElapsedTime()"),t(n?.pointShadowVertexShader).toContain("rendererElapsedTime()"),t(n?.uniforms).not.toHaveProperty(F)}),o("compiles the native sprite billboard position without a GLSL fallback",()=>{const e=new c({color:f(16777215),position:U(g("rotation",.25),g("screenSpaceSize",0),g("lockYAxis",1))}),o=new T({WGSLShaderMaterial:H}).resolve(e);t(o).toBeInstanceOf(H),t(o?.vertexShader).toContain("cos("),t(o?.vertexShader).toContain("sin("),t(o?.vertexShader).toContain("materialUniforms.lockYAxis"),t(o?.vertexShader).not.toContain("customFunction_")}),o("uses the per-instance rotation attribute for instanced sprites",()=>{const e=new c({color:f(16777215),position:U(g("rotation",0),void 0,void 0,!0)}),o=new T({WGSLShaderMaterial:H}).resolve(e);t(o?.vertexAttributes).toEqual(t.arrayContaining([t.objectContaining({name:"rotation",format:"float32",stepMode:"instance"})])),t(o?.vertexShader).toContain("input.rotation"),t(o?.vertexShader).not.toContain("materialUniforms.rotation")}),o("uses per-instance particle opacity in WebGPU fragment shaders",()=>{const e=new c({color:f(p(16777215),N.opacity)}),o=new T({WGSLShaderMaterial:H}).resolve(e);t(o?.vertexAttributes).toEqual(t.arrayContaining([t.objectContaining({name:"particleData",format:"float32x3",stepMode:"instance"})])),t(o?.fragmentShader).toContain(".x"),t(o?.fragmentShader).toContain("> 0.5")}),o("maps THREE.SpriteMaterial to the native sprite shader",()=>{const e=new a(new Uint8Array([255,255,255,255]),1,1,l),o=new d({map:e,color:8429760,opacity:.6,rotation:.25,sizeAttenuation:!1,alphaTest:.2,transparent:!0}),n=new T({WGSLShaderMaterial:H}),r=n.resolve(o);t(r).toBeInstanceOf(H),t(r?.vertexShader).toContain("materialUniforms.rotation"),t(r?.vertexShader).toContain("materialUniforms.screenSpaceSize"),t(r?.fragmentShader).toContain("textureSample("),t(r?.alphaTest).toBe(.2),t(r?.transparent).toBe(!0),t(r?.uniforms.rotation.value).toBe(.25),t(r?.uniforms.screenSpaceSize.value).toBe(1),o.rotation=.75;const i=n.resolve(o);t(i).toBe(r),t(i?.uniforms.rotation.value).toBe(.75)}),o("memoizes node-material signatures for repeated lookups",()=>{const e=["version","vertexShader","fragmentShader","alphaTest","alphaToCoverage","blending","colorWrite","depthFunc","depthTest","depthWrite","polygonOffset","polygonOffsetFactor","polygonOffsetUnits","premultipliedAlpha","side","stencilWrite","transparent"],o=new Map([["version",1],["vertexShader","#vertex"],["fragmentShader","#fragment"],["alphaTest",0],["alphaToCoverage",!1],["blending",1],["colorWrite",!0],["depthFunc",3],["depthTest",!0],["depthWrite",!0],["polygonOffset",!1],["polygonOffsetFactor",0],["polygonOffsetUnits",0],["premultipliedAlpha",!1],["side",0],["stencilWrite",!1],["transparent",!1]]);let n=0;const a=new Proxy({},{get:(t,a)=>"string"==typeof a&&e.includes(a)?(n+=1,o.get(a)):Reflect.get(t,a)});b(a),b(a),t(n).toBe(e.length)}),o("adapts a NodeShaderMaterial without replacing its scene material",()=>{const e=g("intensity",.75),o=new c({color:v({color:p(5210052).multiplyScalar(e),roughness:.42,metalness:.18}),transparent:!0,alphaTest:.25}),n=new T({WGSLShaderMaterial:H}),a=n.resolve(o);t(a).toBeInstanceOf(H),t(a).not.toBe(o),t(a?.transparent).toBe(!0),t(a?.alphaTest).toBe(.25),t(a?.fragmentShader).toContain("rendererPointLightForLight"),t(o.isShaderMaterial).toBe(!0),e.value=.25,t(n.resolve(o)).toBe(a),t(a?.uniforms.intensity?.value).toBe(.25)}),o("syncs direct NodeShaderMaterial uniform mutations to the resolved material",()=>{const e=w("trailTint",new h(0,0,0,0)),o=g("hology_particle_energy",1),n=new c({color:e.multiplyScalar(o)}),a=new T({WGSLShaderMaterial:H});n.uniforms.trailTint.value=new h(1,.25,.5,.75),n.uniforms.hology_particle_energy.value=.4;const r=a.resolve(n);t(r?.uniforms.trailTint.value).toEqual(new h(1,.25,.5,.75)),t(r?.uniforms.hology_particle_energy.value).toBe(.4),n.uniforms.trailTint.value.set(.2,.4,.6,.8),n.uniforms.hology_particle_energy.value=.1,t(a.resolve(n)).toBe(r),t(r?.uniforms.trailTint.value).toEqual(new h(.2,.4,.6,.8)),t(r?.uniforms.hology_particle_energy.value).toBe(.1)}),o("only syncs explicitly invalidated uniforms when auto update is disabled",()=>{const e=g("intensity",.75),o=new c({color:f(y(e))});o.uniformValuesAutoUpdate=!1;const n=new T({WGSLShaderMaterial:H}),a=n.resolve(o),r=a.uniformValuesAutoVersion;e.value=.25,t(n.resolve(o)).toBe(a),t(a?.uniforms.intensity.value).toBe(.75),t(a?.uniformValuesAutoVersion).toBe(r),o.uniformValuesNeedsUpdate=!0,t(n.resolve(o)).toBe(a),t(a?.uniforms.intensity.value).toBe(.25),t(a?.uniformValuesAutoVersion).toBe(r+1)}),o("supports explicit invalidation for mapped Three.js materials",()=>{const e=new s({roughness:.8});L(e,!1);const o=new T({WGSLShaderMaterial:H}),n=o.resolve(e);e.roughness=.2,t(o.resolve(e)).toBe(n),t(n?.uniforms.roughness.value).toBe(.8),M(e),t(o.resolve(e)).toBe(n),t(n?.uniforms.roughness.value).toBe(.2)}),o("binds uniforms referenced only by an MRT material output",()=>{const e=g("roughness",.7),o=new c({color:f(5601177),alphaTest:.1});o.outputRoughness=e;const n=new T({WGSLShaderMaterial:H},e=>[e.outputColor,B(y(0,0,1),e.outputRoughness)]).resolve(o);t(n?.fragmentShader).toContain("materialUniforms.roughness"),t(n?.fragmentShader).not.toContain("graphColor"),t(n?.fragmentShader).toMatch(/if \(.+ < 0\.1\) \{ discard; \}/),t(n?.uniforms.roughness?.value).toBe(.7)}),o("compiles a landscape composite built from node material semantic outputs",()=>{const e=new T({WGSLShaderMaterial:H}),o=new c({color:v({color:p(5147450),roughness:.82,metalness:0})}),n=new c({color:v({color:p(7432286),roughness:.6,metalness:.05})}),a=new G;a.layers=[o,n];const r=e.resolve(a.build());t(r).toBeInstanceOf(H),t(r?.fragmentShader).toContain("rendererDirectionalLightForLight"),t(r?.vertexShader).toContain("@location(6) material: vec4<f32>"),t(r?.vertexShader).toContain("@location(8) hole: f32")}),o("allocates the trail's authored vertex attributes",()=>{const e=new T({WGSLShaderMaterial:H}).resolve(P.createBaseMaterial());t(e).toBeInstanceOf(H),t(e?.vertexAttributes).toEqual(t.arrayContaining([t.objectContaining({name:"nodeID",shaderLocation:6,format:"float32"}),t.objectContaining({name:"nodeCenter",shaderLocation:7,format:"float32x3"})]))}),o("uses Three's default value when a geometry has no uv1 attribute",()=>{const e=W([{name:"uv1",shaderLocation:6,format:"float32x2"},{name:"custom",shaderLocation:7,format:"float32"}]);t(e).toEqual(t.arrayContaining([t.objectContaining({name:"uv1",defaultValue:[0,0]}),t.objectContaining({name:"custom"})])),t(e[1]).not.toHaveProperty("defaultValue")}),o("compiles the foliage transform and named texture-uniform pattern",()=>{const e=new T({WGSLShaderMaterial:H}),o=new a(new Uint8Array([255,255,255,255]),1,1,l),n=new c({color:x("map").sample(C.uv),transform:S(g("windOffset",.25)),uniforms:{map:{value:o}},alphaTest:.5}),r=e.resolve(n);t(r).toBeInstanceOf(H),t(r?.vertexShader).toContain("rendererClipPosition(_ng_localPosition"),t(r?.depthVertexShader).toContain("materialUniforms.windOffset"),t(r?.depthFragmentShader).toContain("textureSample("),t(r?.depthFragmentShader).toContain("< 0.5) { discard;"),t(r?.pointShadowFragmentShader).toContain("distanceToLight")}),o("rebuilds cached materials when scene-local texture bindings change",()=>{const e=new T({WGSLShaderMaterial:H}),o=new a(new Uint8Array([255,255,255,255]),1,1,l),n=new a(new Uint8Array([32,64,128,255]),1,1,l),r=new c({color:x("map").sample(C.uv),uniforms:{map:{value:o}},alphaTest:.5}),i=e.resolve(r);r.uniforms.map.value=n;const s=e.resolve(r);t(s).not.toBe(i),t(i.disposed).toBe(!0),r.dispose();const d=e.resolve(r);t(d).not.toBe(s),t(s.disposed).toBe(!0)}),o("uses the renderer batch id without Three’s batchingIdTexture",()=>{const e=new T({WGSLShaderMaterial:H}),o=new a(new Uint8Array([255,96,24,255]),1,1,l),n=new c({color:f(u("vp0")),uniforms:{vp0:{value:o}}}),r=e.resolve(n);t(r).toBeInstanceOf(H),t(r?.vertexShader).toContain("rendererBatchingId(input.instanceIndex)")}),o("compiles depth nodes that use fragment-coordinate expressions",()=>{const e=new T({WGSLShaderMaterial:H}),o=new c({color:f(y(D.x,D.y,I))}),n=e.resolve(o);t(n).toBeInstanceOf(H),t(n?.fragmentShader).toContain("input.position.xy"),t(n?.fragmentShader).toContain("input.position.z"),t(n?.fragmentShader).toContain("rendererViewportSize()"),t(n?.fragmentShader).not.toContain("gl_FragCoord")}),o("maps scene color and depth nodes to renderer-owned textures",()=>{const e=new T({WGSLShaderMaterial:H}),o=new c({color:q(D).multiplyScalar(j(D))}),n=e.resolve(o);t(n.sceneTextures).toEqual({hology_scene_map:"sceneColor",hology_depth_map:"sceneDepth"}),t(n.graphTextures).toEqual({}),t(n.fragmentShader).toContain("hology_scene_mapTexture"),t(n.fragmentShader).toContain("hology_depth_mapTexture"),t(n.fragmentShader).toContain("vec4<f32>(textureSample(hology_depth_mapTexture")}),o("caches distinct material variants for opaque and late draw stages",()=>{const e=new T({WGSLShaderMaterial:H},(e,t)=>"opaque"===t?[f(y(1)),f(y(0))]:void 0),o=new c({color:f(y(1))}),n=e.resolve(o,"opaque"),a=e.resolve(o,"late");t(n).not.toBe(a),t(e.resolve(o,"opaque")).toBe(n),t(e.resolve(o,"late")).toBe(a),t(n?.fragmentShader).toContain("@location(1)"),t(a?.fragmentShader).not.toContain("@location(1)")}),o("reconstructs WebGPU depth positions with inverted NDC Y",()=>{const e=new T({WGSLShaderMaterial:H}),o=new c({color:f(k.add(V.multiplyScalar(0)))}),n=e.resolve(o);t(n.fragmentShader).toContain("vec2<f32>(2.0, -2.0)"),t(n.fragmentShader).toMatch(/-\(float_op_\d+\)/)}),o("emits dither and parallax nodes without GLSL custom functions",()=>{const e=new T({WGSLShaderMaterial:H}),o=new a(new Uint8Array([128,128,128,255]),1,1,l),n=O(C.uv,x("heightMap",o),m(.03)),r=new c({color:B(n.x,n.y,E(y(.2,.4,.8)).z,1)}),i=e.resolve(r);t(i).toBeInstanceOf(H),t(i?.fragmentShader).toContain("textureSampleLevel(heightMapTexture, heightMapSampler"),t(i?.fragmentShader).toMatch(/= -dpdy\(-pom_\d+ViewPosition\)/),t(i?.fragmentShader).toMatch(/= -dpdy\(pom_\d+Uv\)/),t(i?.fragmentShader).not.toContain("customFunction_"),t(i?.fragmentShader).not.toContain("gl_FragCoord")}),o("inherits, updates, and clears the WebGPU world environment",()=>{const e=new a(new Uint8Array([255,255,255,255]),1,1,l),o=new c({color:v({color:p(8421504),roughness:.5,metalness:.1})}),n=new T({WGSLShaderMaterial:H});n.setWorldEnvironment({texture:e,intensity:.75});const r=n.resolve(o);t(r.fragmentShader).toContain("envMapTexture"),t(r.uniforms.envMapIntensity?.value).toBe(.75),t(o.envMap).toBeUndefined(),n.setWorldEnvironment({texture:e,intensity:.25});const i=n.resolve(o);t(i).toBe(r),t(i.uniforms.envMapIntensity?.value).toBe(.25),n.setWorldEnvironment(null);const s=n.resolve(o);t(s).not.toBe(r),t(r.disposed).toBe(!0),t(s.fragmentShader).not.toContain("envMapTexture")}),o("keeps an authored material environment ahead of the world environment",()=>{const e=new a(new Uint8Array([255,255,255,255]),1,1,l),o=new a(new Uint8Array([64,96,128,255]),1,1,l),r=new a(new Uint8Array([32,48,64,255]),1,1,l),i=new c({color:v({color:p(8421504),roughness:.5,metalness:.1}),envMap:o}),s=n.fn(()=>r),d=new T({WGSLShaderMaterial:H},void 0,s);d.setWorldEnvironment({texture:e,intensity:.2}),d.resolve(i),t(s).toHaveBeenCalledWith(o),t(i.uniforms.envMap.value).toBe(r),t(i.envMap).toBe(o)})});/*
|
|
1
|
+
import{applySkyMaterialSettings as e,createSky as t}from"../scene/sky.js";import{describe as o,expect as n,it as a,vi as r}from"vitest";import{DataTexture as i,Material as s,MeshPhysicalMaterial as l,MeshStandardMaterial as d,RGBAFormat as h,SpriteMaterial as u,Vector4 as c}from"three";import{batchingUniformVec4 as m,float as p,NodeShaderMaterial as f,rgb as g,rgba as v,standardMaterial as S,translateX as w,uniformFloat as x,uniformVec4 as C,uniformSampler2d as y,varyingAttributes as B,vec3 as M,vec4 as b}from"three-shader-graph";import{createNodeMaterialSignature as L,markMaterialUniformValuesNeedUpdate as _,NodeMaterialWebGpuResolver as T,setMaterialUniformValuesAutoUpdate as W,withThreeDefaultVertexAttributeValues as G}from"../rendering/node-material-webgpu-resolver.js";import{StandardShader as U}from"../shader/builtin/standard-shader.js";import{LandscapeCompositeShader as A}from"../shader/builtin/landscape-composite-shader.js";import{DecalUnlitShader as k}from"../shader/builtin/decal-unlit-shader.js";import{getSpritePosition as V,SpriteShader as D}from"../shader/sprite-shader.js";import{depthNormal as I,depthWorldPosition as j,fragmentLinearEyeDepth as q,sampleSceneDepth as O,screenUV as E}from"../shader-nodes/depth.js";import{sampleSceneColor as F}from"../shader-nodes/scene-sample.js";import{applyDithering as P}from"../shader-nodes/dither.js";import{parallaxOcclusionMapping as H}from"../shader-nodes/pom.js";import{elapsedTimeUniformName as N}from"../shader-nodes/time.js";import{Trail as z}from"../effects/vfx/trail-renderer.js";import{particleUniforms as R}from"../shader-nodes/particle.js";import{buildShaderGraphMaterial as Y,createDefaultShaderGraphDocument as J}from"../shader/graph/index.js";class K extends s{set uniformValuesNeedsUpdate(e){e&&this.uniformValuesAutoVersion++}constructor(e){super(),this.disposed=!1,this.uniformValuesAutoUpdate=!0,this.uniformValuesAutoVersion=0,this.vertexShader=e.vertexShader,this.fragmentShader=e.fragmentShader,this.depthVertexShader=e.depthVertexShader??null,this.depthFragmentShader=e.depthFragmentShader??null,this.pointShadowVertexShader=e.pointShadowVertexShader??null,this.pointShadowFragmentShader=e.pointShadowFragmentShader??null,this.uniforms=e.uniforms,this.sceneTextures=e.sceneTextures??{},this.graphTextures=e.graphTextures??{},this.vertexAttributes=e.vertexAttributes??[],this.transparent=e.transparent??!1,this.alphaTest=e.alphaTest??0,this.depthTest=e.depthTest??!0,this.depthWrite=e.depthWrite??!0,this.side=e.side??this.side}dispose(){this.disposed=!0,super.dispose()}}o("NodeMaterialWebGpuResolver",()=>{a("renders custom sky materials at far depth without modifying their authored graph",()=>{const t=(new U).build(),o=t.nodeMaterialDefinition.position;e(t);const a=new T({WGSLShaderMaterial:K}),r=a.resolve(t,"late");n(r).toBeDefined(),n(r.vertexShader).toMatch(/vec4<f32>\(\w+\.x, \w+\.y, (\w+)\.w, \1\.w\)/),n(r.vertexShader).toContain("vec4<f32>(input.position, 0.0)"),n(r.depthWrite).toBe(!1),n(r.depthTest).toBe(!0),n(r.transparent).toBe(!0),n(t.nodeMaterialDefinition.position).toBe(o),a.dispose()}),a("keeps the default sky at far depth on WebGL and WebGPU",()=>{const e=t(),o=e.material;n(e.frustumCulled).toBe(!1),n(o.vertexShader).toMatch(/vec4\(\w+\.x,\w+\.y,(\w+)\.w,\1\.w\)/);const a=new T({WGSLShaderMaterial:K}),r=a.resolve(o,"late");n(r.vertexShader).toMatch(/vec4<f32>\(\w+\.x, \w+\.y, (\w+)\.w, \1\.w\)/),n(o.fog).toBe(!0),n(o.fragmentShader).toContain("fogDensity"),n(r.fragmentShader).toContain("rendererFogDensity()"),e.geometry.dispose(),a.dispose()}),a("compiles physical lobes and binds transmission to the HDR late-pass snapshot",()=>{const e=new U;e.clearcoat=1,e.iridescence=1,e.sheen=.4,e.anisotropy=.5,e.transmission=1,e.thickness=.2,e.dispersion=.5;const t=e.build();n(t.uniforms.hology_transmission_map).toBeDefined();const o=new T({WGSLShaderMaterial:K}),a=o.resolve(t,"late");n(a.graphTextures.hology_transmission_map).toMatchObject({resource:"physicalTransmissionSnapshot",requiresLatePass:!0}),n(a.fragmentShader).toContain("tsgTransmission_volume"),n(a.fragmentShader).toContain("tsgIridescence_eval"),n(a.fragmentShader).toContain("textureNumLevels"),n(a.fragmentShader).not.toContain("vec3 "),n(a.fragmentShader).not.toMatch(/\bmat3\(/),o.dispose()}),a("omits disabled physical features from ordinary materials",()=>{const e=new T({WGSLShaderMaterial:K}),t=(new U).build(),o=e.resolve(t,"opaque");n(o.graphTextures).toEqual({}),n(t.uniforms.transmission).toBeUndefined(),n(t.uniforms.clearcoat).toBeUndefined(),n(t.uniforms.iridescence).toBeUndefined(),n(o.fragmentShader).not.toContain("tsgTransmission_volume"),e.dispose()}),a("preserves native physical properties and updates enabled lobe uniforms",()=>{const e=new T({WGSLShaderMaterial:K}),t=new l({clearcoat:.5,iridescence:.2}),o=e.resolve(t);t.clearcoat=.7,n(e.resolve(t)).toBe(o),n(o.uniforms.clearcoat.value).toBe(.7),t.transmission=1;const a=e.resolve(t);n(a).not.toBe(o),n(a.fragmentShader).toContain("tsgTransmission_volume"),n(a.graphTextures.hology_transmission_map).toMatchObject({requiresLatePass:!0}),e.dispose()}),a("keeps explicitly tracked outline uniforms clean until controls change",()=>{const e=new f({color:S({color:g(16777215)})});e.uniformValuesAutoUpdate=!1,e.userData.outlineParameters={thickness:.012,darkening:.65,alpha:.8};const t=new T({WGSLShaderMaterial:K}),o=t.resolve(e,"outline");n(o.uniformValuesAutoUpdate).toBe(!1);const a=o.uniformValuesAutoVersion;n(t.resolve(e,"outline")).toBe(o),n(o.uniformValuesAutoVersion).toBe(a),e.userData.outlineParameters.thickness=.025,t.resolve(e,"outline"),n(o.uniforms.outlineThickness.value).toBe(.025),n(o.uniformValuesAutoVersion).toBeGreaterThan(a),t.dispose()}),a("derives a textured, darkened albedo outline and updates its controls",()=>{const e=new i(new Uint8Array([128,96,64,255]),1,1,h);e.needsUpdate=!0;const t=y("outlineAlbedo",e).sample(B.uv),o=new f({color:S({color:t})});o.userData.outlineParameters={colorMode:"darkenedAlbedo",thickness:.012,darkening:.65,alpha:.8};const a=new T({WGSLShaderMaterial:K}),r=a.resolve(o,"outline");n(r).toBeInstanceOf(K),n(r.side).toBe(1),n(r.vertexShader).toContain("materialUniforms.outlineThickness"),n(r.vertexAttributes).toEqual(n.arrayContaining([n.objectContaining({name:"skinIndex"}),n.objectContaining({name:"skinWeight"})])),n(r.vertexShader).toContain("rendererSkinPosition"),n(r.vertexShader).toContain("outline_local_position_"),n(r.vertexShader).toContain("outline_local_normal_"),n(r.vertexShader).toContain("sqrt("),n(r.fragmentShader).toContain("outlineAlbedoTexture"),n(r.fragmentShader).toContain("materialUniforms.outlineDarkening"),n(r.uniforms.outlineThickness.value).toBe(.012),n(r.uniforms.outlineDarkening.value).toBe(.65),n(r.uniforms.outlineAlpha.value).toBe(.8),o.userData.outlineParameters.darkening=2,o.userData.outlineParameters.thickness=.02;const s=a.resolve(o,"outline");n(s).toBe(r),n(s.uniforms.outlineThickness.value).toBe(.02),n(s.uniforms.outlineDarkening.value).toBe(1)}),a("declines an albedo outline when the source graph has no albedo output",()=>{const e=new f({color:v(16777215)}),t=new T({WGSLShaderMaterial:K});n(t.resolve(e,"outline")).toBeUndefined()}),a("compiles shader graph scene-light nodes against the renderer light ABI",()=>{const e=J("surface");e.shadingModel="unlit",e.nodes.push({id:"ambient",kind:"scene.ambientLight"},{id:"directional",kind:"scene.directionalLight"},{id:"combined",kind:"math.add"}),e.edges.push({from:{nodeId:"ambient",port:"color"},to:{nodeId:"combined",port:"a"}},{from:{nodeId:"directional",port:"color"},to:{nodeId:"combined",port:"b"}}),e.outputs.color={input:{nodeId:"combined",port:"value"}};const t=new T({WGSLShaderMaterial:K}).resolve(Y(e));n(t?.fragmentShader).toContain("rendererAmbientLight()"),n(t?.fragmentShader).toContain("rendererDirectionalLightForLight"),n(t?.fragmentShader).toContain("rendererDirectionalLightCount()"),n(t?.fragmentShader).not.toContain("NUM_DIR_LIGHTS")}),a("compiles the shader graph world normal without a WGSL matrix inverse",()=>{const e=J("surface");e.shadingModel="unlit",e.nodes.push({id:"world-normal",kind:"builtin.worldNormal"}),e.outputs.color={input:{nodeId:"world-normal",port:"value"}};const t=new T({WGSLShaderMaterial:K}).resolve(Y(e));n(t?.vertexShader).toContain("transpose(rendererViewMatrix())"),n(t?.vertexShader).not.toContain("nodeInverseMat4")}),a("compiles decal surface rejection without undefined WGSL expressions",()=>{const e=(new k).build(),t=new T({WGSLShaderMaterial:K}).resolve(e);n(t?.fragmentShader).not.toContain("undefined"),n(t?.fragmentShader).toContain("hology_scene_mapTexture")}),a("recompiles an opaque variant when receiveDecals changes",()=>{const e=new f({color:v(16777215)});e.userData.receiveDecals=!0;const t=new T({WGSLShaderMaterial:K},e=>[v(16777215,!1===e.userData.receiveDecals?0:2)]),o=t.resolve(e,"opaque");e.userData.receiveDecals=!1,e.needsUpdate=!0;const a=t.resolve(e,"opaque");n(a).not.toBe(o),n(o.disposed).toBe(!0),n(o.fragmentShader).not.toBe(a.fragmentShader)}),a("maps Hology elapsed time to the renderer frame clock",()=>{const e=x(N),t=new f({color:v(M(e)),transform:w(e)}),o=new T({WGSLShaderMaterial:K}).resolve(t);n(o?.vertexShader).toContain("rendererElapsedTime()"),n(o?.fragmentShader).toContain("rendererElapsedTime()"),n(o?.depthVertexShader).toContain("rendererElapsedTime()"),n(o?.pointShadowVertexShader).toContain("rendererElapsedTime()"),n(o?.uniforms).not.toHaveProperty(N)}),a("compiles the native sprite billboard position without a GLSL fallback",()=>{const e=new f({color:v(16777215),position:V(x("rotation",.25),x("screenSpaceSize",0),x("lockYAxis",1))}),t=new T({WGSLShaderMaterial:K}).resolve(e);n(t).toBeInstanceOf(K),n(t?.vertexShader).toContain("cos("),n(t?.vertexShader).toContain("sin("),n(t?.vertexShader).toContain("materialUniforms.lockYAxis"),n(t?.vertexShader).not.toContain("customFunction_")}),a("shares lens-flare occlusion helpers across all five depth samples",()=>{const e=new D;e.lenseFlare=!0;const t=new f({color:e.output().color}),o=new T({WGSLShaderMaterial:K}).resolve(t);n(o).toBeInstanceOf(K);const a=o?.vertexShader??"",r=a.match(/fn tsg_function_\d+\([^]*?\n}/g)??[];n(r).toHaveLength(2);for(const e of r){const t=e.match(/fn (tsg_function_\d+)/)?.[1];n(t).toBeDefined(),n(a.split(t+"(").length-1).toBe(6),n(e).not.toMatch(/textureSample|materialUniforms|renderer/)}n(a.match(/textureSampleLevel\(/g)).toHaveLength(5),n(t.vertexShader).toMatch(/tsg_function_\d+\(/)}),a("uses the per-instance rotation attribute for instanced sprites",()=>{const e=new f({color:v(16777215),position:V(x("rotation",0),void 0,void 0,!0)}),t=new T({WGSLShaderMaterial:K}).resolve(e);n(t?.vertexAttributes).toEqual(n.arrayContaining([n.objectContaining({name:"rotation",format:"float32",stepMode:"instance"})])),n(t?.vertexShader).toContain("input.rotation"),n(t?.vertexShader).not.toContain("materialUniforms.rotation")}),a("uses per-instance particle opacity in WebGPU fragment shaders",()=>{const e=new f({color:v(g(16777215),R.opacity)}),t=new T({WGSLShaderMaterial:K}).resolve(e);n(t?.vertexAttributes).toEqual(n.arrayContaining([n.objectContaining({name:"particleData",format:"float32x3",stepMode:"instance"})])),n(t?.fragmentShader).toContain(".x"),n(t?.fragmentShader).toContain("> 0.5")}),a("maps THREE.SpriteMaterial to the native sprite shader",()=>{const e=new i(new Uint8Array([255,255,255,255]),1,1,h),t=new u({map:e,color:8429760,opacity:.6,rotation:.25,sizeAttenuation:!1,alphaTest:.2,transparent:!0}),o=new T({WGSLShaderMaterial:K}),a=o.resolve(t);n(a).toBeInstanceOf(K),n(a?.vertexShader).toContain("materialUniforms.rotation"),n(a?.vertexShader).toContain("materialUniforms.screenSpaceSize"),n(a?.fragmentShader).toContain("textureSample("),n(a?.alphaTest).toBe(.2),n(a?.transparent).toBe(!0),n(a?.uniforms.rotation.value).toBe(.25),n(a?.uniforms.screenSpaceSize.value).toBe(1),t.rotation=.75;const r=o.resolve(t);n(r).toBe(a),n(r?.uniforms.rotation.value).toBe(.75)}),a("memoizes node-material signatures for repeated lookups",()=>{const e=["version","vertexShader","fragmentShader","alphaTest","alphaToCoverage","blending","colorWrite","depthFunc","depthTest","depthWrite","polygonOffset","polygonOffsetFactor","polygonOffsetUnits","premultipliedAlpha","side","stencilWrite","transparent"],t=new Map([["version",1],["vertexShader","#vertex"],["fragmentShader","#fragment"],["alphaTest",0],["alphaToCoverage",!1],["blending",1],["colorWrite",!0],["depthFunc",3],["depthTest",!0],["depthWrite",!0],["polygonOffset",!1],["polygonOffsetFactor",0],["polygonOffsetUnits",0],["premultipliedAlpha",!1],["side",0],["stencilWrite",!1],["transparent",!1]]);let o=0;const a=new Proxy({},{get:(n,a)=>"string"==typeof a&&e.includes(a)?(o+=1,t.get(a)):Reflect.get(n,a)});L(a),L(a),n(o).toBe(e.length)}),a("adapts a NodeShaderMaterial without replacing its scene material",()=>{const e=x("intensity",.75),t=new f({color:S({color:g(5210052).multiplyScalar(e),roughness:.42,metalness:.18}),transparent:!0,alphaTest:.25}),o=new T({WGSLShaderMaterial:K}),a=o.resolve(t);n(a).toBeInstanceOf(K),n(a).not.toBe(t),n(a?.transparent).toBe(!0),n(a?.alphaTest).toBe(.25),n(a?.fragmentShader).toContain("rendererPointLightForLight"),n(t.isShaderMaterial).toBe(!0),e.value=.25,n(o.resolve(t)).toBe(a),n(a?.uniforms.intensity?.value).toBe(.25)}),a("syncs direct NodeShaderMaterial uniform mutations to the resolved material",()=>{const e=C("trailTint",new c(0,0,0,0)),t=x("hology_particle_energy",1),o=new f({color:e.multiplyScalar(t)}),a=new T({WGSLShaderMaterial:K});o.uniforms.trailTint.value=new c(1,.25,.5,.75),o.uniforms.hology_particle_energy.value=.4;const r=a.resolve(o);n(r?.uniforms.trailTint.value).toEqual(new c(1,.25,.5,.75)),n(r?.uniforms.hology_particle_energy.value).toBe(.4),o.uniforms.trailTint.value.set(.2,.4,.6,.8),o.uniforms.hology_particle_energy.value=.1,n(a.resolve(o)).toBe(r),n(r?.uniforms.trailTint.value).toEqual(new c(.2,.4,.6,.8)),n(r?.uniforms.hology_particle_energy.value).toBe(.1)}),a("only syncs explicitly invalidated uniforms when auto update is disabled",()=>{const e=x("intensity",.75),t=new f({color:v(M(e))});t.uniformValuesAutoUpdate=!1;const o=new T({WGSLShaderMaterial:K}),a=o.resolve(t),r=a.uniformValuesAutoVersion;e.value=.25,n(o.resolve(t)).toBe(a),n(a?.uniforms.intensity.value).toBe(.75),n(a?.uniformValuesAutoVersion).toBe(r),t.uniformValuesNeedsUpdate=!0,n(o.resolve(t)).toBe(a),n(a?.uniforms.intensity.value).toBe(.25),n(a?.uniformValuesAutoVersion).toBe(r+1)}),a("supports explicit invalidation for mapped Three.js materials",()=>{const e=new d({roughness:.8});W(e,!1);const t=new T({WGSLShaderMaterial:K}),o=t.resolve(e);e.roughness=.2,n(t.resolve(e)).toBe(o),n(o?.uniforms.roughness.value).toBe(.8),_(e),n(t.resolve(e)).toBe(o),n(o?.uniforms.roughness.value).toBe(.2)}),a("binds uniforms referenced only by an MRT material output",()=>{const e=x("roughness",.7),t=new f({color:v(5601177),alphaTest:.1});t.outputRoughness=e;const o=new T({WGSLShaderMaterial:K},e=>[e.outputColor,b(M(0,0,1),e.outputRoughness)]).resolve(t);n(o?.fragmentShader).toContain("materialUniforms.roughness"),n(o?.fragmentShader).not.toContain("graphColor"),n(o?.fragmentShader).toMatch(/if \(.+ < 0\.1\) \{ discard; \}/),n(o?.uniforms.roughness?.value).toBe(.7)}),a("compiles a landscape composite built from node material semantic outputs",()=>{const e=new T({WGSLShaderMaterial:K}),t=new f({color:S({color:g(5147450),roughness:.82,metalness:0})}),o=new f({color:S({color:g(7432286),roughness:.6,metalness:.05})}),a=new A;a.layers=[t,o];const r=e.resolve(a.build());n(r).toBeInstanceOf(K),n(r?.fragmentShader).toContain("rendererDirectionalLightForLight"),n(r?.vertexShader).toContain("@location(6) material: vec4<f32>"),n(r?.vertexShader).toContain("@location(8) hole: f32")}),a("allocates the trail's authored vertex attributes",()=>{const e=new T({WGSLShaderMaterial:K}).resolve(z.createBaseMaterial());n(e).toBeInstanceOf(K),n(e?.vertexAttributes).toEqual(n.arrayContaining([n.objectContaining({name:"nodeID",shaderLocation:6,format:"float32"}),n.objectContaining({name:"nodeCenter",shaderLocation:7,format:"float32x3"})]))}),a("uses Three's default value when a geometry has no uv1 attribute",()=>{const e=G([{name:"uv1",shaderLocation:6,format:"float32x2"},{name:"custom",shaderLocation:7,format:"float32"}]);n(e).toEqual(n.arrayContaining([n.objectContaining({name:"uv1",defaultValue:[0,0]}),n.objectContaining({name:"custom"})])),n(e[1]).not.toHaveProperty("defaultValue")}),a("compiles the foliage transform and named texture-uniform pattern",()=>{const e=new T({WGSLShaderMaterial:K}),t=new i(new Uint8Array([255,255,255,255]),1,1,h),o=new f({color:y("map").sample(B.uv),transform:w(x("windOffset",.25)),uniforms:{map:{value:t}},alphaTest:.5}),a=e.resolve(o);n(a).toBeInstanceOf(K),n(a?.vertexShader).toContain("rendererClipPosition(_ng_localPosition"),n(a?.depthVertexShader).toContain("materialUniforms.windOffset"),n(a?.depthFragmentShader).toContain("textureSample("),n(a?.depthFragmentShader).toContain("< 0.5) { discard;"),n(a?.pointShadowFragmentShader).toContain("distanceToLight")}),a("rebuilds cached materials when scene-local texture bindings change",()=>{const e=new T({WGSLShaderMaterial:K}),t=new i(new Uint8Array([255,255,255,255]),1,1,h),o=new i(new Uint8Array([32,64,128,255]),1,1,h),a=new f({color:y("map").sample(B.uv),uniforms:{map:{value:t}},alphaTest:.5}),r=e.resolve(a);a.uniforms.map.value=o;const s=e.resolve(a);n(s).not.toBe(r),n(r.disposed).toBe(!0),a.dispose();const l=e.resolve(a);n(l).not.toBe(s),n(s.disposed).toBe(!0)}),a("uses the renderer batch id without Three’s batchingIdTexture",()=>{const e=new T({WGSLShaderMaterial:K}),t=new i(new Uint8Array([255,96,24,255]),1,1,h),o=new f({color:v(m("vp0")),uniforms:{vp0:{value:t}}}),a=e.resolve(o);n(a).toBeInstanceOf(K),n(a?.vertexShader).toContain("rendererBatchingId(input.instanceIndex)")}),a("compiles depth nodes that use fragment-coordinate expressions",()=>{const e=new T({WGSLShaderMaterial:K}),t=new f({color:v(M(E.x,E.y,q))}),o=e.resolve(t);n(o).toBeInstanceOf(K),n(o?.fragmentShader).toContain("input.position.xy"),n(o?.fragmentShader).toContain("input.position.z"),n(o?.fragmentShader).toContain("rendererViewportSize()"),n(o?.fragmentShader).not.toContain("gl_FragCoord")}),a("maps scene color and depth nodes to renderer-owned textures",()=>{const e=new T({WGSLShaderMaterial:K}),t=new f({color:F(E).multiplyScalar(O(E))}),o=e.resolve(t);n(o.sceneTextures).toEqual({hology_scene_map:"sceneColor",hology_depth_map:"sceneDepth"}),n(o.graphTextures).toEqual({}),n(o.fragmentShader).toContain("hology_scene_mapTexture"),n(o.fragmentShader).toContain("hology_depth_mapTexture"),n(o.fragmentShader).toContain("vec4<f32>(textureSample(hology_depth_mapTexture")}),a("caches distinct material variants for opaque and late draw stages",()=>{const e=new T({WGSLShaderMaterial:K},(e,t)=>"opaque"===t?[v(M(1)),v(M(0))]:void 0),t=new f({color:v(M(1))}),o=e.resolve(t,"opaque"),a=e.resolve(t,"late");n(o).not.toBe(a),n(e.resolve(t,"opaque")).toBe(o),n(e.resolve(t,"late")).toBe(a),n(o?.fragmentShader).toContain("@location(1)"),n(a?.fragmentShader).not.toContain("@location(1)")}),a("reconstructs WebGPU depth positions with inverted NDC Y",()=>{const e=new T({WGSLShaderMaterial:K}),t=new f({color:v(j.add(I.multiplyScalar(0)))}),o=e.resolve(t),a=o.fragmentShader.match(/fn tsg_function_\d+\([^]*?\n}/g)??[];n(a).toHaveLength(1);const r=a[0]??"",i=r.match(/fn (tsg_function_\d+)/)?.[1];n(i).toBeDefined(),n(o.fragmentShader.split(i+"(").length-1).toBe(4),n(r).not.toMatch(/textureSample|renderer|materialUniforms/),n(r).toContain("0.000001");const s=e.resolve(new f({color:v(j)}));n(s?.fragmentShader).toContain(r),n(t.fragmentShader).toMatch(/tsg_function_\d+\(/),n(o.fragmentShader).toContain("vec2<f32>(2.0, -2.0)"),n(o.fragmentShader).toMatch(/-\(float_op_\d+\)/)}),a("emits dither and parallax nodes without GLSL custom functions",()=>{const e=new T({WGSLShaderMaterial:K}),t=new i(new Uint8Array([128,128,128,255]),1,1,h),o=H(B.uv,y("heightMap",t),p(.03)),a=new f({color:b(o.x,o.y,P(M(.2,.4,.8)).z,1)}),r=e.resolve(a);n(r).toBeInstanceOf(K),n(r?.fragmentShader).toContain("textureSampleLevel(heightMapTexture, heightMapSampler"),n(r?.fragmentShader).toMatch(/= -dpdy\(-pom_\d+ViewPosition\)/),n(r?.fragmentShader).toMatch(/= -dpdy\(pom_\d+Uv\)/),n(r?.fragmentShader).not.toContain("customFunction_"),n(r?.fragmentShader).not.toContain("gl_FragCoord")}),a("inherits, updates, and clears the WebGPU world environment",()=>{const e=new i(new Uint8Array([255,255,255,255]),1,1,h),t=new f({color:S({color:g(8421504),roughness:.5,metalness:.1})}),o=new T({WGSLShaderMaterial:K});o.setWorldEnvironment({texture:e,intensity:.75});const a=o.resolve(t);n(a.fragmentShader).toContain("envMapTexture"),n(a.uniforms.envMapIntensity?.value).toBe(.75),n(t.envMap).toBeUndefined(),o.setWorldEnvironment({texture:e,intensity:.25});const r=o.resolve(t);n(r).toBe(a),n(r.uniforms.envMapIntensity?.value).toBe(.25),o.setWorldEnvironment(null);const s=o.resolve(t);n(s).not.toBe(a),n(a.disposed).toBe(!0),n(s.fragmentShader).not.toContain("envMapTexture")}),a("keeps an authored material environment ahead of the world environment",()=>{const e=new i(new Uint8Array([255,255,255,255]),1,1,h),t=new i(new Uint8Array([64,96,128,255]),1,1,h),o=new i(new Uint8Array([32,48,64,255]),1,1,h),a=new f({color:S({color:g(8421504),roughness:.5,metalness:.1}),envMap:t}),s=r.fn(()=>o),l=new T({WGSLShaderMaterial:K},void 0,s);l.setWorldEnvironment({texture:e,intensity:.2}),l.resolve(a),n(s).toHaveBeenCalledWith(t),n(a.uniforms.envMap.value).toBe(o),n(a.envMap).toBe(t)})});/*
|
|
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 t,__metadata as e}from"tslib";import{describe as a,expect as s,it as o,vi as i}from"vitest";i.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const t=new Proxy({},{get:(t,e)=>(e in t||(t[e]=("string"!=typeof e||!e.startsWith("is"))&&i.fn()),t[e]),set:(t,e,a)=>(t[e]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>t}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),i.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),i.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),i.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),i.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:i.fn()}));import{convertConfiguredParamValueToRuntimeType as r}from"../scene/custom-param-runtime-types.js";import{prepareClassParameters as n,prepareCustomParamsFromType as c}from"../scene/materializer.js";import{SerializedParamType as l}from"../scene/model.js";import{extractShaderParameters as u,getAssignableParameterDefinitions as p,isParameterDefinitionAbstract as m,Parameter as y,ParameterDefinition as d}from"../shader/parameter.js";let b=class{constructor(){this.damage=10,this.label="Slash"}kind(){return"attack"}};t([y({type:Number}),e("design:type",Object)],b.prototype,"damage",void 0),t([y({type:String}),e("design:type",Object)],b.prototype,"label",void 0),b=t([d("test.attack")],b);let g=class extends b{constructor(){super(...arguments),this.manaCost=5}kind(){return"magic"}};t([y({type:Number}),e("design:type",Object)],g.prototype,"manaCost",void 0),g=t([d("test.magicAttack")],g);let v=class{constructor(){this.amount=20}};t([y({type:Number}),e("design:type",Object)],v.prototype,"amount",void 0),v=t([d("test.heal")],v);class f{constructor(){this.attacks=[]}}t([y({type:()=>b,array:!0}),e("design:type",Array)],f.prototype,"attacks",void 0);class k{constructor(){this.attacks=[]}}t([y(),e("design:type",Array)],k.prototype,"attacks",void 0),Reflect.defineMetadata("design:type",Array,k.prototype,"attacks"),Reflect.defineMetadata("parameter:arrayElementType",b,k.prototype,"attacks");class B{constructor(){this.primaryAttack=new b}}t([y({type:()=>b}),e("design:type",Object)],B.prototype,"primaryAttack",void 0);let h=class{constructor(){this.cooldown=1}};t([y({type:Number}),e("design:type",Object)],h.prototype,"cooldown",void 0),h=t([d("test.ability",{abstract:!0})],h);let A=class extends h{constructor(){super(...arguments),this.manaCost=6}kind(){return"magicAbility"}};t([y({type:Number}),e("design:type",Object)],A.prototype,"manaCost",void 0),A=t([d("test.magicAbility")],A);class S{constructor(){this.ability=void 0}}t([y({type:()=>h}),e("design:type",h)],S.prototype,"ability",void 0);class j{constructor(){this.abilities=[]}}t([y({type:()=>h,array:!0}),e("design:type",Array)],j.prototype,"abilities",void 0);class w{constructor(){this.value=void 0}}var O,N;t([y({type:String}),e("design:type",Object)],w.prototype,"value",void 0),function(t){t[t.good=0]="good",t[t.bad=1]="bad"}(O||(O={})),function(t){t.good="good",t.bad="bad"}(N||(N={}));class C{constructor(){this.faction=O.bad,this.stringFaction=N.good}}t([y({enum:O}),e("design:type",Object)],C.prototype,"faction",void 0),t([y({enum:N}),e("design:type",Object)],C.prototype,"stringFaction",void 0);class E{}t([y({enum:N}),e("design:type",String)],E.prototype,"stringFaction",void 0);class z{constructor(){this.faction=O.good}}t([y(),e("design:type",Number)],z.prototype,"faction",void 0),Reflect.defineMetadata("parameter:enum",O,z.prototype,"faction"),a("parameter definitions",()=>{o("generates editor options from enum decorator options",()=>{const t=u(C),e=t.find(t=>"faction"===t.name),a=t.find(t=>"stringFaction"===t.name);s(e?.type).toBe(Number),s(e?.options.options).toEqual([{name:"Good",value:O.good},{name:"Bad",value:O.bad}]),s(a?.type).toBe(String),s(a?.options.options).toEqual([{name:"Good",value:N.good},{name:"Bad",value:N.bad}])}),o("prepares enum params as number or string custom values",()=>{const t=c(C,{});s(t.faction).toMatchObject({type:l.Number,value:O.bad}),s(t.stringFaction).toMatchObject({type:l.String,value:N.good})}),o("uses the first enum option as the default when no initializer is set",()=>{const t=c(E,{});s(t.stringFaction).toMatchObject({type:l.String,value:N.good})}),o("uses reflected enum metadata for direct enum parameter annotations",()=>{const[t]=u(z),e=c(z,{});s(t.type).toBe(Number),s(t.options.options).toEqual([{name:"Good",value:O.good},{name:"Bad",value:O.bad}]),s(e.faction).toMatchObject({type:l.Number,value:O.good})}),o("lists assignable parameter definitions with display labels",()=>{const t=p(b);s(t.map(t=>t.id)).toEqual(["test.attack","test.magicAttack"]),s(t.map(t=>t.label)).toEqual(["Attack","Magic Attack"]),s(t.some(t=>"test.heal"===t.id)).toBe(!1)}),o("lists concrete assignable definitions for abstract bases",()=>{const t=p(h);s(m(h)).toBe(!0),s(m("test.ability")).toBe(!0),s(t.map(t=>t.id)).toEqual(["test.magicAbility"]),s(t.map(t=>t.label)).toEqual(["Magic Ability"])}),o("serializes arrays of nested parameter definitions with stable definition ids",()=>{const t=c(f,{}).attacks;s(t.type).toBe(l.Array),s(t.element).toBe(l.Struct),s(t.elementStruct).toBe("test.attack"),s(t.value).toEqual([])}),o("uses reflected array element type metadata for parameter arrays",()=>{const[t]=u(k),e=c(k,{}).attacks;s(t.type).toBe(b),s(t.options.array).toBe(!0),s(e.type).toBe(l.Array),s(e.element).toBe(l.Struct),s(e.elementStruct).toBe("test.attack"),s(e.value).toEqual([])}),o("serializes nested parameter definition defaults",()=>{const t=c(B,{}).primaryAttack;s(t.type).toBe(l.Struct),s(t.struct).toBe("test.attack"),s(t.value.damage.value).toBe(10),s(t.value.label.value).toBe("Slash")}),o("uses an empty string for string parameters without explicit defaults",()=>{const t=c(w,{}).value;s(t.type).toBe(l.String),s(t.value).toBe("")}),o("serializes nested subclass defaults with concrete definition ids",()=>{class a{constructor(){this.primaryAttack=new g}}t([y({type:()=>b}),e("design:type",Object)],a.prototype,"primaryAttack",void 0);const o=c(a,{}).primaryAttack;s(o.type).toBe(l.Struct),s(o.struct).toBe("test.magicAttack"),s(o.value.damage.value).toBe(10),s(o.value.label.value).toBe("Slash"),s(o.value.manaCost.value).toBe(5)}),o("serializes abstract parameter definitions as unselected structs",()=>{const t=c(S,{}).ability;s(t.type).toBe(l.Struct),s(t.struct).toBe("test.ability"),s(t.value).toEqual({})}),o("serializes abstract base defaults with concrete subclass ids",()=>{class a{constructor(){this.ability=new A}}t([y({type:()=>h}),e("design:type",Object)],a.prototype,"ability",void 0);const o=c(a,{}).ability;s(o.type).toBe(l.Struct),s(o.struct).toBe("test.magicAbility"),s(o.value.cooldown.value).toBe(1),s(o.value.manaCost.value).toBe(6)}),o("preserves previously selected concrete definition ids when preparing params",()=>{const t=c(B,{primaryAttack:{type:l.Struct,struct:"test.magicAttack",value:{damage:{type:l.Number,value:22},manaCost:{type:l.Number,value:7}}}}).primaryAttack;s(t.struct).toBe("test.magicAttack"),s(t.value.damage.value).toBe(22),s(t.value.label.value).toBe("Slash"),s(t.value.manaCost.value).toBe(7)}),o("backfills missing parameters in existing struct array elements",()=>{const t=c(f,{attacks:{type:l.Array,element:l.Struct,elementStruct:"test.attack",value:[{damage:{type:l.Number,value:25}}]}}).attacks.value[0];s(t.damage.value).toBe(25),s(t.label.value).toBe("Slash")}),o("materializes nested parameter definitions as class instances",async()=>{const t=await n({attacks:{type:l.Array,element:l.Struct,elementStruct:"test.attack",value:[{damage:{type:l.Number,value:25},label:{type:l.String,value:"Heavy Slash"}}]}},f,{},{});s(t.attacks).toHaveLength(1),s(t.attacks[0]).toBeInstanceOf(b),s(t.attacks[0].damage).toBe(25),s(t.attacks[0].label).toBe("Heavy Slash")}),o("materializes a selected subclass for a non-array base parameter",async()=>{const t=await n({primaryAttack:{type:l.Struct,struct:"test.magicAttack",value:{damage:{type:l.Number,value:30},label:{type:l.String,value:"Arcane Slash"},manaCost:{type:l.Number,value:12}}}},B,{},{});s(t.primaryAttack).toBeInstanceOf(g),s(t.primaryAttack.kind()).toBe("magic"),s(t.primaryAttack.damage).toBe(30),s(t.primaryAttack.manaCost).toBe(12)}),o("materializes a selected subclass for an abstract base parameter",async()=>{const t=await n({ability:{type:l.Struct,struct:"test.magicAbility",value:{cooldown:{type:l.Number,value:2},manaCost:{type:l.Number,value:9}}}},S,{},{});s(t.ability).toBeInstanceOf(A),s(t.ability.cooldown).toBe(2),s(t.ability.manaCost).toBe(9),s(t.ability.kind()).toBe("magicAbility")}),o("does not instantiate abstract parameter definitions",async()=>{const t=i.spyOn(console,"warn").mockImplementation(()=>{}),e=await n({ability:{type:l.Struct,struct:"test.ability",value:{}}},S,{},{});s(e.ability).toBeUndefined(),s(t).toHaveBeenCalledWith('Can not instantiate abstract parameter definition "test.ability"'),t.mockRestore()}),o("materializes compact and tagged struct array elements",async()=>{const t=await n({attacks:{type:l.Array,element:l.Struct,elementStruct:"test.attack",value:[{damage:{type:l.Number,value:15},label:{type:l.String,value:"Quick Slash"}},{struct:"test.magicAttack",value:{damage:{type:l.Number,value:45},label:{type:l.String,value:"Meteor"},manaCost:{type:l.Number,value:18}}}]}},f,{},{});s(t.attacks).toHaveLength(2),s(t.attacks[0]).toBeInstanceOf(b),s(t.attacks[0]).not.toBeInstanceOf(g),s(t.attacks[1]).toBeInstanceOf(g),s(t.attacks[1].damage).toBe(45),s(t.attacks[1].manaCost).toBe(18)}),o("materializes tagged struct array elements for abstract bases",async()=>{const t=await n({abilities:{type:l.Array,element:l.Struct,elementStruct:"test.ability",value:[{struct:"test.magicAbility",value:{cooldown:{type:l.Number,value:3},manaCost:{type:l.Number,value:11}}}]}},j,{},{});s(t.abilities).toHaveLength(1),s(t.abilities[0]).toBeInstanceOf(A),s(t.abilities[0].cooldown).toBe(3),s(t.abilities[0].manaCost).toBe(11)}),o("preserves compatible selected subclass ids during runtime normalization",()=>{const t=r({type:l.Struct,struct:"test.magicAttack",value:{manaCost:{type:l.Number,value:9}}},{type:l.Struct,struct:"test.attack"});s(t.struct).toBe("test.magicAttack")})});/*
|
|
1
|
+
import{__decorate as t,__metadata as e}from"tslib";import{describe as a,expect as s,it as o,vi as i}from"vitest";i.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const t=new Proxy({},{get:(t,e)=>(e in t||(t[e]=("string"!=typeof e||!e.startsWith("is"))&&i.fn()),t[e]),set:(t,e,a)=>(t[e]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>t}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),i.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),i.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),i.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),i.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:i.fn()}));import{convertConfiguredParamValueToRuntimeType as r}from"../scene/custom-param-runtime-types.js";import{prepareClassParameters as n,prepareCustomParamsFromType as c,toSerializedParamType as l}from"../scene/materializer.js";import{SerializedParamType as u}from"../scene/model.js";import{extractShaderParameters as p,getAssignableParameterDefinitions as m,isParameterDefinitionAbstract as y,Parameter as d,ParameterDefinition as b}from"../shader/parameter.js";let g=class{constructor(){this.damage=10,this.label="Slash"}kind(){return"attack"}};t([d({type:Number}),e("design:type",Object)],g.prototype,"damage",void 0),t([d({type:String}),e("design:type",Object)],g.prototype,"label",void 0),g=t([b("test.attack")],g);let v=class extends g{constructor(){super(...arguments),this.manaCost=5}kind(){return"magic"}};t([d({type:Number}),e("design:type",Object)],v.prototype,"manaCost",void 0),v=t([b("test.magicAttack")],v);let f=class{constructor(){this.amount=20}};t([d({type:Number}),e("design:type",Object)],f.prototype,"amount",void 0),f=t([b("test.heal")],f);class k{constructor(){this.attacks=[]}}t([d({type:()=>g,array:!0}),e("design:type",Array)],k.prototype,"attacks",void 0);class B{constructor(){this.attacks=[]}}t([d(),e("design:type",Array)],B.prototype,"attacks",void 0),Reflect.defineMetadata("design:type",Array,B.prototype,"attacks"),Reflect.defineMetadata("parameter:arrayElementType",g,B.prototype,"attacks");class h{constructor(){this.primaryAttack=new g}}t([d({type:()=>g}),e("design:type",Object)],h.prototype,"primaryAttack",void 0);let A=class{constructor(){this.cooldown=1}};t([d({type:Number}),e("design:type",Object)],A.prototype,"cooldown",void 0),A=t([b("test.ability",{abstract:!0})],A);let S=class extends A{constructor(){super(...arguments),this.manaCost=6}kind(){return"magicAbility"}};t([d({type:Number}),e("design:type",Object)],S.prototype,"manaCost",void 0),S=t([b("test.magicAbility")],S);class w{constructor(){this.ability=void 0}}t([d({type:()=>A}),e("design:type",A)],w.prototype,"ability",void 0);class N{constructor(){this.abilities=[]}}t([d({type:()=>A,array:!0}),e("design:type",Array)],N.prototype,"abilities",void 0);class O{constructor(){this.value=void 0}}var j,C;t([d({type:String}),e("design:type",Object)],O.prototype,"value",void 0),function(t){t[t.good=0]="good",t[t.bad=1]="bad"}(j||(j={})),function(t){t.good="good",t.bad="bad"}(C||(C={}));class z{constructor(){this.faction=j.bad,this.stringFaction=C.good}}t([d({enum:j}),e("design:type",Object)],z.prototype,"faction",void 0),t([d({enum:C}),e("design:type",Object)],z.prototype,"stringFaction",void 0);class E{}t([d({enum:C}),e("design:type",String)],E.prototype,"stringFaction",void 0);class M{constructor(){this.faction=j.good}}t([d(),e("design:type",Number)],M.prototype,"faction",void 0),Reflect.defineMetadata("parameter:enum",j,M.prototype,"faction");class x{constructor(){this.factions=[]}}t([d(),e("design:type",Array)],x.prototype,"factions",void 0),Reflect.defineMetadata("parameter:array",!0,x.prototype,"factions"),Reflect.defineMetadata("parameter:arrayElementType",j,x.prototype,"factions"),a("parameter definitions",()=>{o("normalizes legacy enum array metadata before preparing serialized parameters",()=>{const t=p(x)[0];s(t.type).toBe(Number),s(t.options.array).toBe(!0);const e=c(x,{factions:{type:u.Array,element:u.Number,value:[j.bad]}});s(e.factions).toMatchObject({type:u.Array,element:u.Number,value:[j.bad]})}),o("ignores non-constructor parameter types without reading their prototype",()=>{const t=i.spyOn(console,"warn").mockImplementation(()=>{});try{s(l(j)).toBeUndefined(),s(l(()=>Number)).toBeUndefined()}finally{t.mockRestore()}}),o("generates editor options from enum decorator options",()=>{const t=p(z),e=t.find(t=>"faction"===t.name),a=t.find(t=>"stringFaction"===t.name);s(e?.type).toBe(Number),s(e?.options.options).toEqual([{name:"Good",value:j.good},{name:"Bad",value:j.bad}]),s(a?.type).toBe(String),s(a?.options.options).toEqual([{name:"Good",value:C.good},{name:"Bad",value:C.bad}])}),o("prepares enum params as number or string custom values",()=>{const t=c(z,{});s(t.faction).toMatchObject({type:u.Number,value:j.bad}),s(t.stringFaction).toMatchObject({type:u.String,value:C.good})}),o("uses the first enum option as the default when no initializer is set",()=>{const t=c(E,{});s(t.stringFaction).toMatchObject({type:u.String,value:C.good})}),o("uses reflected enum metadata for direct enum parameter annotations",()=>{const[t]=p(M),e=c(M,{});s(t.type).toBe(Number),s(t.options.options).toEqual([{name:"Good",value:j.good},{name:"Bad",value:j.bad}]),s(e.faction).toMatchObject({type:u.Number,value:j.good})}),o("lists assignable parameter definitions with display labels",()=>{const t=m(g);s(t.map(t=>t.id)).toEqual(["test.attack","test.magicAttack"]),s(t.map(t=>t.label)).toEqual(["Attack","Magic Attack"]),s(t.some(t=>"test.heal"===t.id)).toBe(!1)}),o("lists concrete assignable definitions for abstract bases",()=>{const t=m(A);s(y(A)).toBe(!0),s(y("test.ability")).toBe(!0),s(t.map(t=>t.id)).toEqual(["test.magicAbility"]),s(t.map(t=>t.label)).toEqual(["Magic Ability"])}),o("serializes arrays of nested parameter definitions with stable definition ids",()=>{const t=c(k,{}).attacks;s(t.type).toBe(u.Array),s(t.element).toBe(u.Struct),s(t.elementStruct).toBe("test.attack"),s(t.value).toEqual([])}),o("uses reflected array element type metadata for parameter arrays",()=>{const[t]=p(B),e=c(B,{}).attacks;s(t.type).toBe(g),s(t.options.array).toBe(!0),s(e.type).toBe(u.Array),s(e.element).toBe(u.Struct),s(e.elementStruct).toBe("test.attack"),s(e.value).toEqual([])}),o("serializes nested parameter definition defaults",()=>{const t=c(h,{}).primaryAttack;s(t.type).toBe(u.Struct),s(t.struct).toBe("test.attack"),s(t.value.damage.value).toBe(10),s(t.value.label.value).toBe("Slash")}),o("uses an empty string for string parameters without explicit defaults",()=>{const t=c(O,{}).value;s(t.type).toBe(u.String),s(t.value).toBe("")}),o("serializes nested subclass defaults with concrete definition ids",()=>{class a{constructor(){this.primaryAttack=new v}}t([d({type:()=>g}),e("design:type",Object)],a.prototype,"primaryAttack",void 0);const o=c(a,{}).primaryAttack;s(o.type).toBe(u.Struct),s(o.struct).toBe("test.magicAttack"),s(o.value.damage.value).toBe(10),s(o.value.label.value).toBe("Slash"),s(o.value.manaCost.value).toBe(5)}),o("serializes abstract parameter definitions as unselected structs",()=>{const t=c(w,{}).ability;s(t.type).toBe(u.Struct),s(t.struct).toBe("test.ability"),s(t.value).toEqual({})}),o("serializes abstract base defaults with concrete subclass ids",()=>{class a{constructor(){this.ability=new S}}t([d({type:()=>A}),e("design:type",Object)],a.prototype,"ability",void 0);const o=c(a,{}).ability;s(o.type).toBe(u.Struct),s(o.struct).toBe("test.magicAbility"),s(o.value.cooldown.value).toBe(1),s(o.value.manaCost.value).toBe(6)}),o("preserves previously selected concrete definition ids when preparing params",()=>{const t=c(h,{primaryAttack:{type:u.Struct,struct:"test.magicAttack",value:{damage:{type:u.Number,value:22},manaCost:{type:u.Number,value:7}}}}).primaryAttack;s(t.struct).toBe("test.magicAttack"),s(t.value.damage.value).toBe(22),s(t.value.label.value).toBe("Slash"),s(t.value.manaCost.value).toBe(7)}),o("backfills missing parameters in existing struct array elements",()=>{const t=c(k,{attacks:{type:u.Array,element:u.Struct,elementStruct:"test.attack",value:[{damage:{type:u.Number,value:25}}]}}).attacks.value[0];s(t.damage.value).toBe(25),s(t.label.value).toBe("Slash")}),o("materializes nested parameter definitions as class instances",async()=>{const t=await n({attacks:{type:u.Array,element:u.Struct,elementStruct:"test.attack",value:[{damage:{type:u.Number,value:25},label:{type:u.String,value:"Heavy Slash"}}]}},k,{},{});s(t.attacks).toHaveLength(1),s(t.attacks[0]).toBeInstanceOf(g),s(t.attacks[0].damage).toBe(25),s(t.attacks[0].label).toBe("Heavy Slash")}),o("materializes a selected subclass for a non-array base parameter",async()=>{const t=await n({primaryAttack:{type:u.Struct,struct:"test.magicAttack",value:{damage:{type:u.Number,value:30},label:{type:u.String,value:"Arcane Slash"},manaCost:{type:u.Number,value:12}}}},h,{},{});s(t.primaryAttack).toBeInstanceOf(v),s(t.primaryAttack.kind()).toBe("magic"),s(t.primaryAttack.damage).toBe(30),s(t.primaryAttack.manaCost).toBe(12)}),o("materializes a selected subclass for an abstract base parameter",async()=>{const t=await n({ability:{type:u.Struct,struct:"test.magicAbility",value:{cooldown:{type:u.Number,value:2},manaCost:{type:u.Number,value:9}}}},w,{},{});s(t.ability).toBeInstanceOf(S),s(t.ability.cooldown).toBe(2),s(t.ability.manaCost).toBe(9),s(t.ability.kind()).toBe("magicAbility")}),o("does not instantiate abstract parameter definitions",async()=>{const t=i.spyOn(console,"warn").mockImplementation(()=>{}),e=await n({ability:{type:u.Struct,struct:"test.ability",value:{}}},w,{},{});s(e.ability).toBeUndefined(),s(t).toHaveBeenCalledWith('Can not instantiate abstract parameter definition "test.ability"'),t.mockRestore()}),o("materializes compact and tagged struct array elements",async()=>{const t=await n({attacks:{type:u.Array,element:u.Struct,elementStruct:"test.attack",value:[{damage:{type:u.Number,value:15},label:{type:u.String,value:"Quick Slash"}},{struct:"test.magicAttack",value:{damage:{type:u.Number,value:45},label:{type:u.String,value:"Meteor"},manaCost:{type:u.Number,value:18}}}]}},k,{},{});s(t.attacks).toHaveLength(2),s(t.attacks[0]).toBeInstanceOf(g),s(t.attacks[0]).not.toBeInstanceOf(v),s(t.attacks[1]).toBeInstanceOf(v),s(t.attacks[1].damage).toBe(45),s(t.attacks[1].manaCost).toBe(18)}),o("materializes tagged struct array elements for abstract bases",async()=>{const t=await n({abilities:{type:u.Array,element:u.Struct,elementStruct:"test.ability",value:[{struct:"test.magicAbility",value:{cooldown:{type:u.Number,value:3},manaCost:{type:u.Number,value:11}}}]}},N,{},{});s(t.abilities).toHaveLength(1),s(t.abilities[0]).toBeInstanceOf(S),s(t.abilities[0].cooldown).toBe(3),s(t.abilities[0].manaCost).toBe(11)}),o("preserves compatible selected subclass ids during runtime normalization",()=>{const t=r({type:u.Struct,struct:"test.magicAttack",value:{manaCost:{type:u.Number,value:9}}},{type:u.Struct,struct:"test.attack"});s(t.struct).toBe("test.magicAttack")})});/*
|
|
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"reflect-metadata";import*as e from"@dimforge/rapier3d-simd-compat";import{afterEach as t,beforeAll as o,expect as s,test as n,vi as i}from"vitest";import{Subject as r}from"rxjs";import{Object3D as a,Scene as l}from"three";import{PhysicsSystem as c}from"../gameplay/services/physics/physics-system.js";i.hoisted(()=>{const e=new Proxy({},{get:(e,t)=>e[t]??i.fn(),set:(e,t,o)=>(e[t]=o,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),i.mock("../gameplay/services/render.js",()=>({ViewController:class{}})),i.mock("../gameplay/services/world.js",()=>({World:class{}})),o(async()=>{await e.init()});const d=[];function p(){const t=Object.create(c.prototype),o=new e.World({x:0,y:0,z:0}),s=new e.EventQueue(!0),n=new r,i=new r,a=new r;return Object.assign(t,{world:o,eventQueue:s,beforeStep:i,afterStep:a,viewController:{onUpdate:()=>n},nonFixedBodies:new Set,staticMeshes:new Map,bodyActors:new Map,controlledActors:new Set,releaseExpiredRayDebugArrows:()=>{},handleCollisionEvent:()=>{}}),t.handleTick(),d.push(()=>{t.fixedupdateSub.unsubscribe(),s.free(),o.free()}),{system:t,world:o,updates:n,beforeStep:i,afterStep:a}}t(()=>{for(const e of d.splice(0))e()}),n("hitstop freezes movement callbacks, kinematic transforms and the Rapier timestep until resume",()=>{const{system:t,world:o,updates:n,beforeStep:r,afterStep:c}=p(),d=o.createRigidBody(e.RigidBodyDesc.kinematicPositionBased());o.createCollider(e.ColliderDesc.capsule(.6,.25),d);const m=new a;(new l).add(m),t.nonFixedBodies.add(d),t.bodyActors.set(d,{id:"character",object:m});const x=i.fn(e=>{const t=d.translation();t.x+=6*Math.max(.001,e),d.setNextKinematicTranslation(t)});r.subscribe(x);const f=i.fn();c.subscribe(f),n.next(1/60);const u=d.translation().x,y=d.linvel().x;s(u).toBeCloseTo(.1);for(let e=0;e<6;e++)n.next(0);s(x).toHaveBeenCalledTimes(1),s(f).toHaveBeenCalledTimes(1),s(d.translation().x).toBe(u),s(d.linvel().x).toBe(y),s(m.position.x).toBe(u),s(o.timestep).toBeCloseTo(1/60),n.next(1/60),s(x).toHaveBeenCalledTimes(2),s(d.translation().x).toBeCloseTo(.2),s(m.position.x).toBeCloseTo(.2)}),n("hitstop preserves pending dynamic impulses until simulation resumes",()=>{const{world:t,updates:o}=p(),n=t.createRigidBody(e.RigidBodyDesc.dynamic());t.createCollider(e.ColliderDesc.ball(.25).setMass(1),n),o.next(1/60),n.applyImpulse({x:3,y:0,z:0},!0);for(let e=0;e<6;e++)o.next(0);s(n.translation().x).toBe(0),s(n.linvel().x).toBeCloseTo(3),o.next(1/60),s(n.translation().x).toBeCloseTo(.05),s(n.linvel().x).toBeCloseTo(3)});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import"reflect-metadata";import*as e from"@dimforge/rapier3d-simd-compat";import{afterEach as t,beforeAll as
|
|
1
|
+
import"reflect-metadata";import*as e from"@dimforge/rapier3d-simd-compat";import{afterEach as t,beforeAll as o,expect as s,test as n,vi as a}from"vitest";import{BatchedMesh as i,BoxGeometry as r,InstancedMesh as c,Matrix4 as d,Mesh as l,MeshBasicMaterial as w,Object3D as h,Vector3 as p}from"three";import{PhysicsSystem as y,RayTestResult as m}from"../gameplay/services/physics/physics-system.js";import{BoxCollisionShape as B}from"../scene/collision/collision-shape.js";a.hoisted(()=>{const e=new Proxy({},{get:(e,t)=>e[t]??a.fn(),set:(e,t,o)=>(e[t]=o,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),a.mock("../gameplay/services/render.js",()=>({ViewController:class{}})),a.mock("../gameplay/services/world.js",()=>({World:class{}})),o(async()=>{await e.init()});const b=[];function f(){const t=Object.create(y.prototype),o=new e.World({x:0,y:0,z:0});return b.push(o),Object.assign(t,{world:o,rapier:e,shapeCacheBall:new Map,colliderObjects:new WeakMap,actorBodies:new Map,bodyActors:new Map,nonFixedBodies:new Set,_raytestDiff:new p,_raytestDirection:new p,invalidateStaticDebug:()=>{}}),{system:t,world:o}}t(()=>{for(const e of b.splice(0))e.free()}),n("sphere casts can exit an initial overlap while still detecting obstacles ahead",()=>{const{system:t,world:o}=f();o.createCollider(e.ColliderDesc.cuboid(2,2,.2).setTranslation(0,0,-.3)),o.createCollider(e.ColliderDesc.cuboid(2,2,.1).setTranslation(0,0,2)),o.step();const n=new p,a=new p(0,0,1),i=t.sphereCast(n,.25,a,3);s(i.hasHit).toBe(!0),s(i.distance).toBeCloseTo(0);const r=t.sphereCast(n,.25,a,3,void 0,{stopAtPenetration:!1});s(r.hasHit).toBe(!0),s(r.distance).toBeCloseTo(1.64,2);const c=t.sphereCast(n,.25,a,1,void 0,{stopAtPenetration:!1});s(c.hasHit).toBe(!1)}),n("ground ray collision groups skip overlapping character capsules",()=>{const{system:t,world:o}=f();o.createCollider(e.ColliderDesc.cuboid(5,.1,5).setTranslation(0,-.1,0));const n=o.createRigidBody(e.RigidBodyDesc.kinematicPositionBased());o.createCollider(e.ColliderDesc.capsule(.6,.25).setTranslation(0,1.05,0).setCollisionGroups(131070),n),o.step();const a=new p(0,1,0),i=new p(0,-.1,0),r=new m;t.rayTest(a,i,r,{resolveActor:!1}),s(r.hitPoint.y).toBeGreaterThan(.1),t.rayTest(a,i,r,{resolveActor:!1,collisionFilter:131070}),s(r.hasHit).toBe(!0),s(r.hitPoint.y).toBeCloseTo(0)}),n("ray hits retain the source object without resolving an actor, and clear it on misses",()=>{const{system:e,world:t}=f(),o=new l(new r,new w),n=new B(new p(1,1,1)),a=e.addShape(void 0,n,o);t.step();const i=new m;e.rayTest(new p(0,2,0),new p(0,-2,0),i,{resolveActor:!1}),s(i.hasHit).toBe(!0),s(i.object).toBe(o),s(i.actor).toBeUndefined(),t.removeCollider(a,!1),t.step(),e.rayTest(new p(0,2,0),new p(0,-2,0),i),s(i.hasHit).toBe(!1),s(i.object).toBeUndefined(),i.hasHit=!0,i.object=o,i.instanceId=3,i.batchId=4,e.rayTest(new p,new p,i),s(i.hasHit).toBe(!1),s(i.object).toBeUndefined(),s(i.instanceId).toBeUndefined(),s(i.batchId).toBeUndefined()}),n("instanced colliders return the rendered mesh and the hit instance index",()=>{const{system:e,world:t}=f(),o=new c(new r,new w,2);o.setMatrixAt(0,new d),o.setMatrixAt(1,(new d).makeTranslation(3,0,0)),e.createForInstancedMesh(o,[new B(new p(1,1,1))]),t.step();const n=e.rayTest(new p(3,2,0),new p(3,-2,0),new m);s(n.hasHit).toBe(!0),s(n.object).toBe(o),s(n.instanceId).toBe(1),s(n.batchId).toBeUndefined()}),n("standalone colliders without a scene object return no object",()=>{const{system:e,world:t}=f();e.addShape(void 0,new B(new p(1,1,1))),t.step();const o=new m;o.object=new h,e.rayTest(new p(0,2,0),new p(0,-2,0),o),s(o.hasHit).toBe(!0),s(o.object).toBeUndefined()}),n("batched colliders return the batch instance ID rather than the temporary physics object",()=>{const{system:e,world:t}=f(),o=new i(2,48,72,new w),n=o.addGeometry(new r),a=o.addInstance(n),c=o.addInstance(n);o.setMatrixAt(a,new d),o.setMatrixAt(c,(new d).makeTranslation(3,0,0)),o.userData.collisionShapes=[[new B(new p(1,1,1))],[new B(new p(1,1,1))]],e.createForInstancedMesh(o,[]),t.step();const l=e.rayTest(new p(3,2,0),new p(3,-2,0),new m);s(l.hasHit).toBe(!0),s(l.object).toBe(o),s(l.batchId).toBe(c),s(l.instanceId).toBeUndefined()});/*
|
|
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 e,expect as t,it as o,vi as r}from"vitest";import*as n from"three";import{compileNodeShaderMaterialForWebGpu as a,float as s,NodeShaderMaterial as i,uniformFloat as l}from"../shader-nodes/index.js";import{depthUniformName as
|
|
1
|
+
import{describe as e,expect as t,it as o,vi as r}from"vitest";import*as n from"three";import{compileNodeShaderMaterialForWebGpu as a,float as s,NodeShaderMaterial as i,uniformFloat as l,uniformVec2 as m}from"../shader-nodes/index.js";import{depthUniformName as u,farUniformName as f,linearEyeDepth as d,nearUniformName as p,resolutionUniformName as c,screenUV as h,sceneNormalUniformName as g}from"../shader-nodes/depth.js";import{aoMapUniformName as T}from"../shader-nodes/scene-sample.js";import{elapsedTimeUniformName as v,time as w}from"../shader-nodes/time.js";import{sampleSceneColor as b}from"../shader-nodes/scene-sample.js";import{sampleWaterVolume as A,waterVolumeMask as C,waterVolumeWorldToLocalUniformName as y}from"../shader-nodes/water-volume.js";import{NodeMaterialWebGpuResolver as V}from"../rendering/node-material-webgpu-resolver.js";import{DecalStandardShader as B}from"../shader/builtin/decal-standard-shader.js";import{applyPostProcessEffectUniformState as P,defaultPostProcessEffectStage as S,PostProcessEffectPass as x,PostProcessEffectRegistration as j,postProcessEffectStages as E}from"../rendering/post-process-effect.js";import{createPostProcessMaterial as U,PostProcessShader as _}from"../shader/post-process-shader.js";r.hoisted(()=>{if("undefined"!=typeof window){if("undefined"!=typeof HTMLCanvasElement){const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&r.fn()),e[t]),set:(e,t,o)=>(e[t]=o,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}class e{constructor(){this.currentTime=0,this.destination={},this.listener={positionX:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},positionY:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},positionZ:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},forwardX:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},forwardY:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},forwardZ:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},upX:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},upY:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},upZ:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()}}}createGain(){return{connect:r.fn(),disconnect:r.fn(),gain:{value:1,setValueAtTime:r.fn()}}}}Object.defineProperty(window,"AudioContext",{configurable:!0,value:e}),Object.defineProperty(window,"webkitAudioContext",{configurable:!0,value:e})}});class F extends _{output(){return s(1)}}e("post-process effect",()=>{o("exports stable stage names with a default late-stage slot",()=>{t(E).toEqual(["beforeFog","beforeDepthOfField","beforeColorAdjustment","beforeOutline","beforeAntiAliasing","beforeLut","beforeOutput"]),t(S).toBe("beforeOutput")}),o("builds post-process shaders with fullscreen-safe defaults",()=>{const e=(new F).build();t(e).toBeInstanceOf(i),t(e.depthWrite).toBe(!1),t(e.depthTest).toBe(!1),t(e.toneMapped).toBe(!1)}),o("updates shared renderer uniforms on post-process effect materials",()=>{const e=U(s(1));e.uniforms[c]={value:new n.Vector2},e.uniforms[u]={value:null},e.uniforms[g]={value:null},e.uniforms[p]={value:0},e.uniforms[f]={value:0},e.uniforms[v]={value:0},e.uniforms[T]={value:null};const o=new n.Texture,r=new n.Texture,a=new n.Texture,i=new n.Vector2(640,360);P(e,{aoEnabled:!0,aoTexture:a,cameraFar:500,cameraNear:.25,depthTexture:o,normalTexture:r,resolution:i,simulationTime:12.5}),t(e.uniforms[c].value.toArray()).toEqual([640,360]),t(e.uniforms[u].value).toBe(o),t(e.uniforms[g].value).toBe(r),t(e.uniforms[p].value).toBe(.25),t(e.uniforms[f].value).toBe(500),t(e.uniforms[v].value).toBe(12.5),t(e.uniforms[T].value).toBe(a),t(e.defines.USE_SSAO_MAP).toBe(""),P(e,{aoEnabled:!1,aoTexture:null,cameraFar:500,cameraNear:.25,depthTexture:o,normalTexture:r,resolution:i,simulationTime:13}),t(e.uniforms[T].value).toBeNull(),t(e.defines.USE_SSAO_MAP).toBeUndefined()}),o("compiles screen UV against the WebGPU fullscreen ABI",()=>{const e=U(b(h)),o=new V({}).compileFullscreen(e);t(o.fragmentShader).toContain("input.uv"),t(o.fragmentShader).not.toContain("rendererCamera.viewportAndTime"),t(o.fragmentShader).not.toContain("gl_FragCoord"),t(o.uniforms[c]).toBeUndefined();const r=a(e);t(r.fragmentShader).not.toContain("hologyFullscreenUv"),t(r.fragmentShader).toContain("input.position.xy"),t(r.fragmentShader).toContain("materialUniforms.hology_resolution"),t(r.fragmentShader).toContain("(input.position.xy / max(materialUniforms.hology_resolution, vec2<f32>(1.0, 1.0)))");const n=new V({}).compileFullscreen(U(w));t(n.fragmentShader).not.toContain("rendererElapsedTime"),t(n.uniforms[v]).toBeTruthy();const s=new V({}).compileFullscreen(U(d));t(s.fragmentShader).toContain("input.uv"),t(s.fragmentShader).toContain("textureSample(hology_depth_mapTexture, hology_depth_mapSampler,")}),o("uses runtime material uniform values in WebGPU fullscreen passes",()=>{const e="hology_post_process_weight",o=U(l(e,1)),r=new V({}).compileFullscreen(o);o.uniforms[e].value=.25,r.syncLiveUniforms(r.uniforms),t(r.uniforms[e].value).toBe(.25)}),o("compiles the depth-reconstructed water volume mask for WebGPU fullscreen passes",()=>{const e=U(C),o=new V({}).compileFullscreen(e);t(o.fragmentShader).toContain("hology_depth_map"),t(o.uniforms[y]).toBeTruthy(),t(o.fragmentShader).not.toContain("rendererViewMatrix"),t(o.fragmentShader).not.toContain("rendererProjectionMatrix")}),o("keeps WebGPU decal varyings within the portable location limit",()=>{const e=(new B).build();t(e).toBeInstanceOf(i);const o=a(e),r=[...o.vertexShader.matchAll(/@location\((\d+)\)/g)].map(e=>Number(e[1]));t(Math.max(...r)).toBeLessThan(16),t(o.vertexShader).not.toContain("undefined"),t(o.fragmentShader).not.toContain("undefined")}),o("keeps registration state shared with both renderer adapters",()=>{const e=U(s(1)),o=new x(e),n={id:7,material:e,pass:o,enabled:!0,priority:0,stage:"beforeOutput"},a={refreshPostProcessEffectPassOrder:r.fn(),removePostProcessEffectEntry:r.fn()},i=new j(a,n);t(i.material).toBe(e),i.enabled=!1,t(n.enabled).toBe(!1),t(o.enabled).toBe(!1),i.priority=3,i.stage="beforeFog",t(n.priority).toBe(3),t(n.stage).toBe("beforeFog"),t(a.refreshPostProcessEffectPassOrder).toHaveBeenCalledTimes(3),i.dispose(),i.dispose(),t(a.removePostProcessEffectEntry).toHaveBeenCalledTimes(1),o.dispose()}),o("exposes post-process effect registration through ViewController",async()=>{const e=U(s(1)),o={material:e,enabled:!1,priority:4,stage:"beforeColorAdjustment",dispose:r.fn()},a=r.fn().mockReturnValue(o),{ViewController:i}=await import("../gameplay/services/render.js"),l=new i({camera:new n.PerspectiveCamera,running:!1,paused:!1,onLoop:r.fn(),setCamera(e){this.camera=e},stop:r.fn(),addPostProcessEffect:a,addPostProcessVolume:r.fn(),removePostProcessVolume:r.fn()}).addPostProcessEffect(e,{enabled:!1,priority:4,stage:"beforeColorAdjustment"});t(a).toHaveBeenCalledWith(e,{enabled:!1,priority:4,stage:"beforeColorAdjustment"}),t(l).toBe(o)}),o("uses custom UVs when reconstructing water volume distance",()=>{const e=m("waterSampleUv",new n.Vector2(.25,.75)),o=U(A(e).distance),r=new V({}).compileFullscreen(o);t(r.uniforms.waterSampleUv.value).toEqual(new n.Vector2(.25,.75)),t(r.fragmentShader).toContain("waterSampleUv")})});/*
|
|
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*as e from"@dimforge/rapier3d-simd-compat";import{beforeAll as r,expect as t,test as o}from"vitest";import{BufferAttribute as i,BufferGeometry as n,Float32BufferAttribute as a,InterleavedBuffer as s,InterleavedBufferAttribute as m,Vector3 as c}from"three";import{TrimeshCollisionShape as l}from"../scene/collision/collision-shape.js";import{createRapierCollisionShape as p,extractFloat32Array as y}from"../scene/collision/rapier-shape.js";r(async()=>{await e.init()}),o.each([!1,!0])("trimesh preserves triangles and winding with indexed=%s",r=>{const o=new n,i=[0,0,0,0,0,1,1,0,0,1,0,0,0,0,1,1,0,1];o.setAttribute("position",new a(i,3)),r&&o.setIndex([0,1,2,3,4,5]);const s=new l(o),m=p(e,s,new c(2,3,4)).shape;t(Array.from(m.indices)).toEqual([0,1,2,3,4,5]),t(Array.from(m.vertices)).toEqual([0,0,0,0,0,4,2,0,0,2,0,0,0,0,4,2,0,4]),t(m.castRay(new e.Ray({x:1.5,y:5,z:3},{x:0,y:-1,z:0}),{x:0,y:0,z:0},{x:0,y:0,z:0,w:1},10,!0)).toBeCloseTo(5);const y=p(e,s,new c(1,1,1));t(Array.from(y.shape.vertices)).toEqual(i),t(Array.from(o.getAttribute("position").array)).toEqual(i),t(null!=o.index).toBe(r)}),o("position extraction decodes normalized and interleaved attributes",()=>{const e=new i(new Int16Array([0,32767,-32767]),3,!0);t(Array.from(y(e))).toEqual([0,1,-1]);const r=new m(new s(new Float32Array([99,1,2,3,99,4,5,6]),4),3,1);t(Array.from(y(r))).toEqual([1,2,3,4,5,6])});/*
|
|
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 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
|
|
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 w(a.object),p=new v;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.each([!1,!0])("sequence root motion preserves hip height on a rotated rig (retimed: %s)",t=>{const o=new g,n=l(),s=c("anim",0,1);s.rootMotion=!0,s.rootMotionY=!1,t&&(s.retiming={mode:"impact",points:[{id:"impact",clipProgress:.3,sourceProgress:.5}]}),n.clips.push(s);const a=new i.AnimationClip("slash",1,[new i.VectorKeyframeTrack("hips.position",[0,1],[0,0,100,20,0,100])]),r=new y,m=new i.Group;m.rotation.x=-Math.PI/2,m.scale.setScalar(.01);const p=new i.Bone;p.name="hips",m.add(p),r.object.add(m);const u=o.getPlayableAnimationClip("anim",a,s,!0,r.object).tracks.find(e=>"hips.position"===e.name).values;e(u[0]).toBeCloseTo(0),e(u[1]).toBeCloseTo(0),e(u[2]).toBeCloseTo(100),p.position.fromArray(u),r.object.updateMatrixWorld(!0),e(p.getWorldPosition(new i.Vector3).y).toBeCloseTo(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 w(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 w(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 w(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 w{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 v{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*as e from"three";import{describe as s,expect as
|
|
1
|
+
import*as e from"three";import{describe as s,expect as t,it as a,vi as n}from"vitest";import{createAudioClipInstance as o,createAudioTrack as r,createSequenceData as c,createVfxClipInstance as i,createVfxTrack as l}from"../effects/sequence/sequence-data.js";import{SequencePlayer as p,SequencePlaybackState as d}from"../effects/sequence/sequence-player.js";function f(){return{object:new e.Object3D,restart:n.fn(),play:n.fn(),pause:n.fn(),prepareRendering:n.fn(async()=>{}),previewAt:n.fn(),clearPreview:n.fn(),stop:n.fn(),getParticleCount:n.fn(()=>0)}}async function u(){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("keeps preparation pending until ability audio decoding finishes",async()=>{let e;const s=new Promise(s=>{e=s}),a=n.fn(()=>s),i=new p;i.setAssetLoader({getAudioByAssetId:a});const l=c(),d=r();d.clips.push(o("swing",0,1)),l.tracks.push(d),i.load(l);let f=!1;const m=i.prepare().then(()=>{f=!0});await u(),t(f).toBe(!1),t(a).toHaveBeenCalledTimes(1),e({}),await m,t(f).toBe(!0)}),a("preloads unique VFX assets when preparing a sequence",async()=>{const s=new p,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 o=c(),r=l();r.clips.push(i("vfx-asset",0,1),i("vfx-asset",1,1),i("other-vfx-asset",2,1)),o.tracks.push(r),s.load(o),await s.prepare(),t(a).toHaveBeenCalledTimes(1),t([...a.mock.calls[0][0]]).toHaveLength(2)}),a("creates and retains gameplay VFX actors while preparing a sequence",async()=>{const s=f(),a=n.fn(async()=>s),o=n.fn(async()=>{}),r=new p;r.setWorld({scene:new e.Scene,preloadVisualEffects:o}),r.setActorFactory({}),r.setAssetLoader({getAsset:n.fn(async e=>({id:e,vfx:{emitters:[]}}))}),r.setVfxService({createFromAssetId:a});const d=c(),m=l();m.clips.push(i("vfx-asset",0,1)),d.tracks.push(m),r.load(d),await r.prepare(),t(a).toHaveBeenCalledTimes(1),t(o).not.toHaveBeenCalled(),t(s.prepareRendering).toHaveBeenCalledTimes(1),t(s.play).not.toHaveBeenCalled(),r.play(),await u(),t(a).toHaveBeenCalledTimes(1)}),a("prepares each retained effect as soon as it loads and waits for its rendering resources",async()=>{const s=f(),a=f();let o,r;s.prepareRendering.mockReturnValue(new Promise(e=>{r=e}));const d=new Promise(e=>{o=e}),m=new p;m.setWorld({scene:new e.Scene}),m.setAssetLoader({}),m.setVfxService({createFromAssetId:n.fn(e=>"first"===e?Promise.resolve(s):d)});const v=c(),y=l();y.clips.push(i("first",0,1),i("second",1,1)),v.tracks.push(y),m.load(v);let w=!1;const h=m.prepare().then(()=>{w=!0});await u(),t(s.prepareRendering).toHaveBeenCalledTimes(1),t(a.prepareRendering).not.toHaveBeenCalled(),t(w).toBe(!1),o(a),await u(),t(a.prepareRendering).toHaveBeenCalledTimes(1),t(w).toBe(!1),r(),await h,t(w).toBe(!0)}),a("restarts an already-active VFX clip when seeking backwards within the clip",async()=>{const s=f(),a=new p;a.setWorld({scene:new e.Scene}),a.setVfxService({createFromAssetId:n.fn(async()=>s)});const o=c();o.duration=2;const r=l(),d=i("vfx-asset",0,1.5);r.clips.push(d),o.tracks.push(r),a.load(o),a.play(),await u(),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=f(),a=new p;a.setWorld({scene:new e.Scene}),a.setVfxService({createFromAssetId:n.fn(async()=>s)});const o=c();o.duration=1,o.loop=!0;const r=l(),d=i("vfx-asset",0,1);r.clips.push(d),o.tracks.push(r),a.load(o),a.play(),await u(),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=f(),a=new p;a.setWorld({scene:new e.Scene}),a.setVfxService({createFromAssetId:n.fn(async()=>s)});const o=c();o.duration=1;const r=l();r.clips.push(i("vfx-asset",0,.8)),o.tracks.push(r),a.load(o),a.play(),await u(),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=f(),a=new p;a.setWorld({scene:new e.Scene}),a.setVfxService({createFromAssetId:n.fn(async()=>s)});const o=c();o.duration=2;const r=l(),d=i("vfx-asset",.5,1);r.clips.push(d),o.tracks.push(r),a.load(o),a.play(),a.seek(.75),await u(),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=f(),a=n.fn(async()=>s),o=new p;o.setWorld({scene:new e.Scene}),o.setVfxService({createFromAssetId:a}),o.externalTimeControl=!0,o.inEditor=!0;const r=c();r.duration=2;const d=l();d.clips.push(i("vfx-asset",.5,1)),r.tracks.push(d),o.load(r),o.seek(.75),await u(),t(a).toHaveBeenCalledWith("vfx-asset",t.any(e.Scene),{backend:"cpu"}),t(s.previewAt).toHaveBeenCalledWith(.25,1,"finish",.25,t.any(Number)),t(s.play).not.toHaveBeenCalled()}),a("keeps gameplay sequence VFX on the configured world backend",async()=>{const s=f(),a=n.fn(async()=>s),o=new p;o.setWorld({scene:new e.Scene}),o.setVfxService({createFromAssetId:a});const r=c();r.duration=1;const d=l();d.clips.push(i("vfx-asset",0,1)),r.tracks.push(d),o.load(r),o.play(),await u(),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=f(),a=new p;a.setWorld({scene:new e.Scene}),a.setVfxService({createFromAssetId:n.fn(async()=>s)}),a.externalTimeControl=!0,a.inEditor=!0;const o=c();o.duration=2;const r=l();r.clips.push(i("vfx-asset",.5,1)),o.tracks.push(r),a.load(o),a.seek(.75),await u(),s.clearPreview.mockClear(),a.seek(.25),t(s.clearPreview).toHaveBeenCalledTimes(1)})}),s("sequence audio crops",()=>{a("does not retry a pitched sound that ends before its timeline clip",()=>{const e=new p;e.setAssetLoader({});const s=e;s._state.next(d.Playing);const a=r(),c=o("swoosh",0,.35);a.clips.push(c);const i=`${a.id}-${c.id}`;s.audioByClip.set(i,{isPlaying:!1});const l=n.spyOn(s,"tryStartAudioClip").mockImplementation(()=>{});n.spyOn(s,"applyAudioParameters").mockImplementation(()=>{}),s.evaluateAudioTrack(a,.26),t(l).not.toHaveBeenCalled(),s.audioByClip.delete(i),s.evaluateAudioTrack(a,.27),t(l).toHaveBeenCalledTimes(1)}),a.each([[0,0,1,.37,1,0,.37],[.2,.1,2,.37,1,.4,.54],[.2,0,1,2,1,.2,.8]])("schedules a one-shot crop (offset %s, elapsed %s, rate %s)",async(e,s,a,c,i,l,d)=>{const f={start:n.fn(),connect:n.fn(),detune:{setTargetAtTime:n.fn()},playbackRate:{setTargetAtTime:n.fn()}},u={state:"running",currentTime:0,createGain:()=>({connect:n.fn()}),createBufferSource:()=>f},m=new p;m.setAssetLoader({getAudioByAssetId:n.fn(async()=>({duration:i}))}),m.setAudioListener({context:u,getInput:n.fn()}),n.spyOn(m,"applyAudioParameters").mockImplementation(()=>{});const v=r();v.spatial=!1;const y=o("impact",0,c);y.clipStartOffset=e,y.playbackRate=a;const w=m;w._time.next(s),await w.playAudioClip(v,y,"impact",s,{}),t(f.loop).toBe(!1),t(f.start).toHaveBeenCalledTimes(1);const h=f.start.mock.calls[0];t(h[0]).toBe(0),t(h[1]).toBeCloseTo(l),t(h[2]).toBeCloseTo(d)})});/*
|
|
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 t,it as r}from"vitest";import{NodeShaderMaterial as o,vec3 as a}from"three-shader-graph";import{surfaceScreenSpaceReflection as n}from"../shader-nodes/surface-reflection.js";import{NodeMaterialWebGpuResolver as s}from"../rendering/node-material-webgpu-resolver.js";import{WGSLShaderMaterial as d}from"@hology/webgpu-renderer";e("late surface reflections",()=>{r("requests opaque snapshots and loads scalar depth from the renderer R32F texture",()=>{const e=new o({color:n(a(0,-1,-5),a(0,1,0))}),r=new s({WGSLShaderMaterial:d}).resolve(e);t(r).toBeDefined(),t(r.requiresLatePass()).toBe(!0),t(Object.values(r.sceneTextures).sort()).toEqual(["sceneColor","sceneDepth"]),t(r.fragmentShader).toMatch(/let d = textureLoad\([^;]+\)\.r;/),t(r.fragmentShader).toMatch(/let sampleDepth = textureLoad\([^;]+\)\.r;/),t(r.fragmentShader).toContain("textureSampleLevel("),t(r.fragmentShader).not.toMatch(/\btextureSample\(/),e.dispose()}),r("keeps the WebGL fallback independent of scene captures",()=>{const e=new o({color:n(a(0,-1,-5),a(0,1,0))});t(e.fragmentShader).not.toContain("surfaceReflection_"),t(e.uniforms.hology_scene_map).toBeUndefined(),t(e.uniforms.hology_depth_map).toBeUndefined(),e.dispose()})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|