@hology/core 0.0.271 → 0.0.273
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/vfx/gpu-sprite-compiler.js +1 -1
- package/dist/effects/vfx/gpu-sprite-runtime.js +1 -1
- package/dist/effects/vfx/gpu-vfx-world-service.js +1 -1
- package/dist/effects/vfx/rates.d.ts +2 -0
- package/dist/effects/vfx/rates.js +1 -1
- package/dist/effects/vfx/vfx-actor.d.ts +2 -1
- package/dist/effects/vfx/vfx-actor.js +1 -1
- package/dist/effects/vfx/vfx-asset.d.ts +10 -1
- package/dist/effects/vfx/vfx-backend-partition.d.ts +8 -0
- package/dist/effects/vfx/vfx-backend-partition.js +4 -0
- package/dist/effects/vfx/vfx-materializer.js +1 -1
- package/dist/effects/vfx/vfx-render-preparation.js +1 -1
- package/dist/effects/vfx/vfx-renderers.d.ts +27 -2
- package/dist/effects/vfx/vfx-renderers.js +1 -1
- package/dist/gameplay/actors/builtin/components/character/character-animation.d.ts +2 -0
- package/dist/gameplay/actors/builtin/components/character/character-animation.js +1 -1
- package/dist/gameplay/initiate.js +1 -1
- package/dist/scene/asset-resource-loader.d.ts +5 -1
- package/dist/scene/asset-resource-loader.js +1 -1
- package/dist/scene/materializer.d.ts +1 -1
- package/dist/scene/materializer.js +1 -1
- package/dist/scene/model.d.ts +4 -0
- package/dist/scene/native-texture.d.ts +13 -0
- package/dist/scene/native-texture.js +4 -0
- package/dist/shader-nodes/particle-parameter.d.ts +4 -0
- package/dist/shader-nodes/particle-parameter.js +4 -0
- package/dist/test/asset-resource-loader-texture-array.test.js +1 -1
- package/dist/test/gpu-sprite-compiler.test.js +1 -1
- package/dist/test/gpu-sprite-pipeline-cache.test.js +1 -1
- package/dist/test/native-texture.test.d.ts +2 -0
- package/dist/test/native-texture.test.js +4 -0
- package/dist/test/shader-graph.test.js +1 -1
- package/dist/test/vfx-backend-partition.test.d.ts +2 -0
- package/dist/test/vfx-backend-partition.test.js +4 -0
- package/dist/test/vfx-playback-transform.test.js +1 -1
- package/dist/test/vfx-render-preparation.test.js +1 -1
- package/dist/utils/mesh.d.ts +1 -1
- package/dist/utils/mesh.js +1 -1
- package/package.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{Object3D as e,Vector3 as t}from"three";import{describe as
|
|
1
|
+
import{Object3D as e,Vector3 as t}from"three";import{describe as a,expect as n,it as o,vi as s}from"vitest";s.mock("../gameplay/actors/builtin/index.js",()=>({builtInActors:{},default:{}})),s.mock("../gameplay/actors/index.js",()=>({CharacterAnimationComponent:class{},CharacterMovementComponent:class{}})),s.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:s.fn()})),s.hoisted(()=>{const e=new Proxy({},{get:(e,t)=>e[t]??(e[t]=s.fn()),set:(e,t,a)=>(e[t]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),a("VFX playback transform",()=>{o("refreshes a reused GPU effect before resuming emission",async()=>{const{VfxActor:a}=await import("../effects/vfx/vfx-actor.js"),o=Object.create(a.prototype),r=new e;r.position.set(12,0,8),o.object=new e,o.object.position.set(0,0,1.75),r.add(o.object),o._worldPos=new t;const i=new t;o.gpuPrepared={},o.gpuHandle={active:!0,setPosition:s.fn(e=>i.copy(e)),setTransform:s.fn(),play:s.fn(()=>n(i.toArray()).toEqual([12,0,9.75])),restart:s.fn(()=>n(i.toArray()).toEqual([20,0,9.75]))},o.play(),n(o.gpuHandle.setTransform).toHaveBeenCalledWith(o.object.matrixWorld),n(o.gpuHandle.play).toHaveBeenCalledOnce(),r.position.x=20,o.restart(),n(o.gpuHandle.restart).toHaveBeenCalledOnce()})}),a("mixed VFX playback",()=>{async function a(){const{VfxActor:a}=await import("../effects/vfx/vfx-actor.js"),{Rate:n}=await import("@hology/nebula"),{Subject:o}=await import("rxjs"),{Euler:r,Quaternion:i}=await import("three"),l=Object.create(a.prototype),p={dead:!1,rate:new n(1,1),particles:[{age:1,life:10}],clearParticlesRecursive:s.fn(),removeAllParticles:s.fn()};return Object.assign(l,{object:new e,_worldPos:new t,_worldRot:new i,_worldScale:new t,_worldEul:new r,_timescale:.5,system:{emitters:[p],update:s.fn(),getCount:s.fn(()=>1)},gpuPrepared:{},gpuHandle:{active:!0,setPosition:s.fn(),setTransform:s.fn(),setTimescale:s.fn(),play:s.fn(),pause:s.fn(),stop:s.fn(),restart:s.fn(),kill:s.fn(),expire:s.fn()},onComplete:new o,paused:!1}),{actor:l,emitter:p}}o("updates CPU simulation with timescale and pauses/resumes both groups",async()=>{const{actor:e}=await a();e.onUpdate(.2),n(e.system.update).toHaveBeenCalledWith(.1),e.pause(),e.onUpdate(.2),n(e.system.update).toHaveBeenCalledTimes(1),n(e.gpuHandle.pause).toHaveBeenCalledOnce(),e.play(),n(e.gpuHandle.play).toHaveBeenCalledOnce(),n(e.paused).toBe(!1)}),o("restarts, stops, expires and kills CPU lights alongside GPU particles",async()=>{const{actor:e,emitter:t}=await a();e.restart(),n(t.clearParticlesRecursive).toHaveBeenCalledOnce(),n(e.gpuHandle.restart).toHaveBeenCalledOnce(),e.stop(),n(t.rate.nextTime).toBe(1/0),n(e.gpuHandle.stop).toHaveBeenCalledOnce(),e.applyClipEndBehavior("expire",.2),n(t.particles[0].life).toBe(1.2),n(e.gpuHandle.expire).toHaveBeenCalledWith(.2),e.applyClipEndBehavior("kill"),n(t.removeAllParticles).toHaveBeenCalledOnce(),n(e.gpuHandle.kill).toHaveBeenCalledOnce()}),o("waits for both groups before completing and does not replay a finished GPU tail on resume",async()=>{const{actor:e,emitter:t}=await a(),o=s.fn();e.onComplete.subscribe(o),e.gpuHandle.active=!1,e.updateCompletionState(),n(o).not.toHaveBeenCalled(),e.pause(),e.play(),n(e.gpuHandle.play).not.toHaveBeenCalled(),t.dead=!0,e.updateCompletionState(),e.updateCompletionState(),n(o).toHaveBeenCalledOnce()}),o("waits for delayed light emission, then completes after its last particle drains",async()=>{const{actor:e,emitter:t}=await a(),{OnceRate:o}=await import("../effects/vfx/rates.js");t.rate=new o(.5,1),t.particles=[],e.gpuHandle.active=!1;const r=s.fn();e.onComplete.subscribe(r),e.updateCompletionState(),n(r).not.toHaveBeenCalled(),n(t.rate.getValue(.6)).toBe(1),t.particles.push({age:0,life:1}),e.updateCompletionState(),n(r).not.toHaveBeenCalled(),t.particles.length=0,e.updateCompletionState(),n(r).toHaveBeenCalledOnce()}),o("can restart a completed GPU group while paused and spawn it on the next play",async()=>{const{actor:e}=await a(),t=e.gpuHandle;t.active=!1,t.release=s.fn(),e.paused=!0,e.inputRuntime={getDefinitions:()=>[]},e.gpuPrepared.spawn=s.fn(()=>(t.active=!0,t)),e.restart(),n(t.release).toHaveBeenCalledOnce(),n(e.gpuHandle).toBeNull(),e.play(),n(e.gpuPrepared.spawn).toHaveBeenCalledOnce(),n(e.gpuHandle.active).toBe(!0)})}),o("materializes only light roots on CPU when the visual roots prepare on GPU",async()=>{const{VfxActor:t}=await import("../effects/vfx/vfx-actor.js"),{VfxInputRuntime:a}=await import("../effects/vfx/vfx-input-runtime.js"),o=Object.create(t.prototype),r=(e,t)=>({id:e,name:e,output:t,rate:{type:"once",count:1,delay:0},position:{x:0,y:0,z:0},rotation:{x:0,y:0,z:0},initializers:[],behaviours:[]}),i=r("visual",{type:"sprite",space:"world"}),l={id:"mixed",type:"vfx",vfx:{emitters:[i,r("light",{type:"pointLight",space:"local",maxLights:1})]}};Object.assign(o,{object:new e,world:{scene:new e},inputOverrides:new Map,inputRuntime:new a,view:{vfxBackendPreference:"auto"},gpuWorld:{prepare:s.fn(async()=>({}))}}),await o.fromAsset(l),n(o.runtimeBackend).toBe("mixed"),n(o.gpuWorld.prepare.mock.calls[0][0].vfx.emitters).toEqual([i]),n(o.system.emitters).toHaveLength(1),n(o.renderBodies).toHaveLength(1),n(o.renderBodies[0].type).toBe("pointLight"),o.system.destroy(),o.disposeSystem()});/*
|
|
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 n,vi as o}from"vitest";import*as a from"three";import{NodeShaderMaterial as s}from"three-shader-graph";import{RenderingView as
|
|
1
|
+
import{describe as e,expect as t,it as n,vi as o}from"vitest";import*as a from"three";import{NodeShaderMaterial as s}from"three-shader-graph";import{RenderingView as i}from"../rendering.js";import{createVfxRenderPreparation as r,prepareVfxRendering as c}from"../effects/vfx/vfx-render-preparation.js";import{StretchedSprite as l}from"../effects/vfx/stretched-sprite.js";import{materializeVfx as d}from"../effects/vfx/vfx-materializer.js";o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&o.fn()),e[t]),set:(e,t,n)=>(e[t]=n,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),e("VFX rendering preload",()=>{n("batches concurrent effects and keeps their probe resources alive until compilation finishes",async()=>{let e;const n=new Promise(t=>{e=t}),i=o.fn(e=>n),r={compileAsync:i,activeBackend:"webgpu"},l={getCamera:()=>new a.PerspectiveCamera},d=new s({}),p=new a.PlaneGeometry,m=[0,1].map(()=>c([new a.Mesh(p,d)],l,r));await new Promise(e=>setTimeout(e,5)),t(i).toHaveBeenCalledTimes(1);const f=i.mock.calls[0][0];t(f.children).toHaveLength(2);let u=0;f.traverse(e=>{e instanceof a.Mesh&&u++}),t(u).toBe(2);const v=f.children[0].children[0],h=o.spyOn(v.geometry,"dispose"),w=o.spyOn(p,"dispose"),y=o.spyOn(d,"dispose");t(h).not.toHaveBeenCalled(),e(),await Promise.all(m),t(h).toHaveBeenCalledOnce(),t(w).not.toHaveBeenCalled(),t(y).not.toHaveBeenCalled(),t(f.children).toHaveLength(0)}),n("rejects every effect in a failed batch and allows subsequent preparation",async()=>{const e=o.fn().mockRejectedValueOnce(new Error("compile failed")).mockResolvedValue(void 0),n=o.fn(),s={compileAsync:e,initTextures:n,activeBackend:"webgl"},i={getCamera:()=>new a.PerspectiveCamera},r=await Promise.allSettled([0,1].map(()=>c([],i,s)));t(r.map(e=>e.status)).toEqual(["rejected","rejected"]),t(e).toHaveBeenCalledTimes(1),t(n).not.toHaveBeenCalled(),await c([],i,s),t(e).toHaveBeenCalledTimes(2),t(n).toHaveBeenCalledOnce()}),n("includes delayed and nested collision outputs without emitting any particles",async()=>{const e=(e,t=[])=>({id:e,name:e,position:[0,0,0],rotation:[0,0,0],rate:{type:"once",count:0,delay:100},initializers:[],behaviours:[],children:t,spawnEvent:"collision",output:{type:"sprite",texture:e,space:"world",color:"#ffffff",materialSource:"default"}}),n={type:"vfx",id:"test",vfx:{emitters:[e("root",[e("child",[e("grandchild")])])]}},s={getCamera:()=>new a.PerspectiveCamera},i=await d(n,new a.Object3D,{getTexture:o.fn(async()=>new a.Texture)},s,{},{},{},{});t(i.renderBodies).toHaveLength(3),t(i.system.getCount()).toBe(0);const c=r(i.renderBodies,s);t(c.scene.children).toHaveLength(3),t(i.system.getCount()).toBe(0),c.dispose(),i.dispose(),i.system.destroy()}),n("prepares mesh, sprite and stretched output variants without disposing source resources",()=>{const e=new s({});e.transparent=!0;const n=new a.PlaneGeometry,i=new a.Mesh(n,e),c=new l(new a.PlaneGeometry,new s({})),d=new a.Sprite(new a.SpriteMaterial),p=o.spyOn(n,"dispose"),m=o.spyOn(e,"dispose"),f=r([i,c,d],{getCamera:()=>new a.PerspectiveCamera}),u=[];f.scene.traverse(e=>{(e instanceof a.Mesh||e instanceof a.Sprite)&&u.push(e)}),t(u).toHaveLength(3);const v=u.find(e=>e instanceof a.InstancedMesh);t(v.count).toBe(1),t(v.material).toBe(e),t(v.geometry.getAttribute("particleData")).toBeDefined(),t(v.geometry).not.toBe(n),f.dispose(),t(p).not.toHaveBeenCalled(),t(m).not.toHaveBeenCalled(),t(f.scene.children).toHaveLength(0)}),n("waits for every concurrent compilation and recovers after a failed request",async()=>{let e;const n=new Promise(t=>{e=t}),s=o.fn().mockImplementationOnce(()=>n).mockRejectedValueOnce(new Error("compile failed")).mockResolvedValue(void 0),r=Object.create(i.prototype);r.compileTail=Promise.resolve(),r.compileSceneAsync=s;const c=[new a.Scene,new a.Scene,new a.Scene],l=c.map(e=>r.compileAsync(e)),d=Promise.allSettled(l);await Promise.resolve(),await Promise.resolve(),t(s).toHaveBeenCalledTimes(1),e();const p=await d;t(s.mock.calls.map(e=>e[0])).toEqual(c),t(p.map(e=>e.status)).toEqual(["fulfilled","rejected","fulfilled"]),t(r.compileInProgress).toBe(!1)})}),e("VFX point lights",()=>{n("caps live lights, reuses slots, and maps alpha/scale without unlimited radii",async()=>{const{Particle:e}=await import("@hology/nebula"),{PointLightRenderer:n}=await import("../effects/vfx/vfx-renderers.js"),o=new a.Object3D,s=new n(o),i={type:"pointLight",output:{type:"pointLight",color:"#ffffff",intensity:8,radius:4,maxLights:1}},r=()=>{const t=new e;return t.body=i,t.scale=.5,t.useAlpha=!0,t.alpha=.25,t.position.set(1,2,3),t},c=r();s.onParticleCreated(c);const l=c.target;t(l.position.toArray()).toEqual([1,2,3]),t(l.intensity).toBe(2),t(l.distance).toBe(2),t(l.castShadow).toBe(!1);const d=r();s.onParticleCreated(d),t(d.target).toBeUndefined(),t(o.children).toHaveLength(1),s.onParticleDead(c),t(o.children).toHaveLength(0);const p=r();s.onParticleCreated(p),t(p.target).toBe(l),p.scale=0,s.onParticleUpdate(p),t(l.visible).toBe(!1),t(l.distance).toBeGreaterThan(0),s.dispose(),t(o.children).toHaveLength(0)})});/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
package/dist/utils/mesh.d.ts
CHANGED
package/dist/utils/mesh.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import*as e from"three";import{clone as t}from"three/examples/jsm/utils/SkeletonUtils.js";import{mergeGeometries as
|
|
1
|
+
import*as e from"three";import{clone as t}from"three/examples/jsm/utils/SkeletonUtils.js";import{mergeGeometries as r,mergeGroups as n}from"three/examples/jsm/utils/BufferGeometryUtils.js";import{Matrix4 as s}from"three";export class NoRaycastMesh extends e.Mesh{raycast(...e){}}export function cloneMesh(e){const r=t(e),n=[],s=[];e.traverse(e=>n.push(e)),r.traverse(e=>s.push(e));const i=new Map,l=[];for(let e=0;e<n.length;e++){const t=n[e],r=s[e];if(!0!==t?.isSkinnedMesh||!0!==r?.isSkinnedMesh)continue;let a=i.get(t.skeleton);null==a&&(a=l.find(e=>o(t.skeleton,e.source))?.clone),null!=a?(i.set(t.skeleton,a),r.bind(a,r.bindMatrix)):(i.set(t.skeleton,r.skeleton),l.push({source:t.skeleton,clone:r.skeleton}))}return r}function o(e,t){if(e.bones.length!==t.bones.length||e.boneInverses.length!==t.boneInverses.length)return!1;for(let r=0;r<e.bones.length;r++){if(e.bones[r].name!==t.bones[r].name)return!1;const n=e.boneInverses[r].elements,s=t.boneInverses[r].elements;for(let e=0;e<16;e++)if(n[e]!==s[e])return!1}return!0}export function mergeCompatibleSkinnedMeshes(t,o=()=>!1){t.updateWorldMatrix(!0,!0);const a=t.matrixWorld.clone().invert(),m=[],u=new s;t.traverse(e=>{const t=e;if(!0!==t.isSkinnedMesh||Array.isArray(t.material)||null==t.material||o(t))return;if(!function(e){for(const t in e)if(Object.hasOwn(e,t))return!1;return!0}(t.geometry.morphAttributes))return;u.copy(a).multiply(t.matrixWorld);const r=m.find(e=>{return e.skeleton===t.skeleton&&i(e.bindMatrix,t.bindMatrix)&&i(e.relativeMatrix,u)&&(r=e.material,n=t.material,r===n||r.name.length>0&&r.name===n.name)&&function(e,t){return e.visible===t.visible&&e.renderOrder===t.renderOrder&&e.frustumCulled===t.frustumCulled&&e.layers.mask===t.layers.mask}(e.meshes[0],t);var r,n});null==r?m.push({material:t.material,skeleton:t.skeleton,bindMatrix:t.bindMatrix.clone(),relativeMatrix:u,meshes:[t],geometries:[t.geometry]}):(r.meshes.push(t),r.geometries.push(t.geometry))});for(const s of m){if(s.meshes.length<2||!l(s.geometries))continue;const o=r(s.geometries,!1);if(null==o)continue;o.groups.length>1&&n(o);const i=new e.SkinnedMesh(o,s.material);i.copy(s.meshes[0],!1),i.geometry=o,i.material=s.material,s.relativeMatrix.decompose(i.position,i.quaternion,i.scale),i.matrix.copy(s.relativeMatrix),i.bind(s.skeleton,s.bindMatrix);for(const e of s.meshes)e.removeFromParent(),e.geometry.dispose();t.add(i)}}function i(e,t,r=1e-6){const n=e.elements,s=t.elements;for(let e=0;e<16;e++)if(Math.abs(n[e]-s[e])>r)return!1;return!0}function l(e){const t=e[0],r=Object.keys(t.attributes).sort();return e.slice(1).every(e=>null==e.index==(null==t.index)&&Object.keys(e.attributes).sort().join("|")===r.join("|")&&r.every(r=>{const n=t.getAttribute(r),s=e.getAttribute(r);return n.itemSize===s.itemSize&&n.normalized===s.normalized&&n.array.constructor===s.array.constructor}))}/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|