@hology/core 0.0.213 → 0.0.215
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/config/project-config.d.ts +1 -0
- package/dist/effects/sequence/sequence-action.d.ts +4 -0
- package/dist/effects/sequence/sequence-actor.js +1 -1
- package/dist/effects/sequence/sequence-animation-retiming.d.ts +2 -1
- package/dist/effects/sequence/sequence-animation-retiming.js +1 -1
- package/dist/effects/sequence/sequence-data.d.ts +6 -0
- package/dist/effects/sequence/sequence-data.js +1 -1
- package/dist/effects/sequence/sequence-player.d.ts +11 -0
- package/dist/effects/sequence/sequence-player.js +1 -1
- package/dist/effects/vfx/initializsers.d.ts +4 -0
- package/dist/effects/vfx/initializsers.js +1 -1
- package/dist/effects/vfx/vfx-collision-behaviour.js +1 -1
- package/dist/effects/vfx/vfx-materializer.js +1 -1
- package/dist/gameplay/actors/actor.d.ts +6 -0
- package/dist/gameplay/actors/builtin/components/character/character-animation.js +1 -1
- package/dist/gameplay/actors/builtin/components/character/character-movement-like.d.ts +3 -0
- package/dist/gameplay/actors/builtin/components/character/character-movement.d.ts +22 -0
- package/dist/gameplay/actors/builtin/components/character/character-movement.js +1 -1
- package/dist/gameplay/actors/builtin/components/character/character-slide.d.ts +7 -0
- package/dist/gameplay/actors/builtin/components/character/character-slide.js +4 -0
- package/dist/gameplay/actors/builtin/components/character/modes.d.ts +2 -1
- package/dist/gameplay/actors/builtin/components/character/modes.js +1 -1
- package/dist/gameplay/actors/builtin/components/character/net-character-movement-protocol.d.ts +3 -1
- package/dist/gameplay/actors/builtin/components/character/net-character-movement-protocol.js +1 -1
- package/dist/gameplay/actors/builtin/navmesh-actor.js +1 -1
- package/dist/gameplay/actors/camera/third-person-camera-component.d.ts +1 -0
- package/dist/gameplay/actors/camera/third-person-camera-component.js +1 -1
- package/dist/gameplay/actors/component.d.ts +6 -0
- package/dist/gameplay/ai/behavior-tree/move.js +1 -1
- package/dist/gameplay/ai/dynamic-tiled-navmesh.d.ts +20 -2
- package/dist/gameplay/ai/dynamic-tiled-navmesh.js +1 -1
- package/dist/gameplay/animation/animation-action.d.ts +3 -0
- package/dist/gameplay/animation/animation-action.js +4 -0
- package/dist/gameplay/animation/retarget.js +1 -1
- package/dist/gameplay/initiate.js +1 -1
- package/dist/gameplay/services/physics/physics-system.d.ts +56 -3
- package/dist/gameplay/services/physics/physics-system.js +1 -1
- package/dist/gameplay/services/world.js +1 -1
- package/dist/scene/asset-resource-loader.d.ts +2 -0
- package/dist/scene/asset-resource-loader.js +1 -1
- package/dist/scene/assets-provider.d.ts +1 -0
- package/dist/scene/assets-provider.js +1 -1
- package/dist/scene/collision/collision-shape.d.ts +1 -1
- package/dist/scene/materializer.d.ts +8 -1
- package/dist/scene/materializer.js +1 -1
- package/dist/scene/runtime-asset-service.d.ts +7 -1
- package/dist/scene/runtime-asset-service.js +1 -1
- package/dist/scene/storage/storage.js +1 -1
- package/dist/shader/builtin/standard-shader.js +1 -1
- package/dist/shader/builtin/unlit-shader.js +1 -1
- package/dist/shader/decal-shader.d.ts +2 -2
- package/dist/shader/decal-shader.js +1 -1
- package/dist/shader/shader.d.ts +2 -1
- package/dist/shader/shader.js +1 -1
- package/dist/shader/sprite-shader.d.ts +2 -2
- package/dist/shader/sprite-shader.js +1 -1
- package/dist/shader/trail-shader.d.ts +2 -2
- package/dist/shader/trail-shader.js +1 -1
- package/dist/test/animation-retarget.test.js +1 -1
- package/dist/test/asset-resource-loader-texture-array.test.d.ts +2 -0
- package/dist/test/asset-resource-loader-texture-array.test.js +4 -0
- package/dist/test/character-animation-transition.test.d.ts +2 -0
- package/dist/test/character-animation-transition.test.js +4 -0
- package/dist/test/character-slide.test.d.ts +2 -0
- package/dist/test/character-slide.test.js +4 -0
- package/dist/test/navmesh-worker-pool.test.d.ts +2 -0
- package/dist/test/navmesh-worker-pool.test.js +4 -0
- package/dist/test/net-character-movement.test.js +1 -1
- package/dist/test/prefab-instance-params.test.js +1 -1
- package/dist/test/runtime-asset-service.test.js +1 -1
- package/dist/test/sequence-animation-retiming.test.js +1 -1
- package/dist/test/sequence-root-motion-axis-mask.test.d.ts +2 -0
- package/dist/test/sequence-root-motion-axis-mask.test.js +4 -0
- package/dist/test/vfx-world-space-emitter-rotation.test.d.ts +2 -0
- package/dist/test/vfx-world-space-emitter-rotation.test.js +4 -0
- package/dist/test/world-prefab-spawn.test.js +1 -1
- package/package.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{randomUUID as e}from"../../utils/uuid.js";import{pathJoin as t}from"../../utils/files.js";import{EMPTY as a,filter as i,firstValueFrom as n,from as s,map as r,merge as h,mergeAll as o,mergeMap as l,Observable as c,of as d,startWith as u,Subject as p,switchMap as f,tap as w}from"rxjs";import{sleepDelay as m}from"../../utils/async.js";const y={},v={},x={},b={};null==y.read&&window.require&&(Object.assign(y,window.require("fs")),Object.assign(v,y.promises),Object.assign(x,window.require("path")),Object.assign(b,window.require("chokidar")));const j=null!=y.existsSync;let g=0,I=0;const P=[];async function F(e){await function(){if(g<32)return g++,Promise.resolve();return new Promise(e=>P.push(e))}();try{return await e()}finally{!function(){const e=P[I];if(null==e)return P.length=0,I=0,void g--;I++,I>64&&2*I>P.length&&(P.splice(0,I),I=0);e()}()}}function O(e){return F(()=>v.readFile(e))}function S(){if(j){const e="--path=",t=window.process.argv.find(t=>t.startsWith(e));return t?t.substring(e.length):""}return""}const C=/^[A-Z]:/;function E(...e){return 0===e.length?"":C.test(e[0])||j?x.join(...e):t(...e)}export class ObjectStorage{get pathResources(){return E(this.path+"-resources")}constructor(e,t,a=defaultSerializer){this.name=e,this.filePathFn=t,this.serializer=a,this.basePathUpdates=new p,this.basePath=this.basePathUpdates.pipe(r(e=>E(S(),e)),w(e=>{this.path=E(e,this.name),this.cachedIndex=null,this.loadingIndex=null,k(this.path),this.determineIfMetaFileShouldBeCreated()})),this.loaded=n(this.basePath),this.cachedIndex=null,this.loadingIndex=null,this.shouldCreateIndex=!1}setBasePath(e){this.basePathUpdates.next(e)}async determineIfMetaFileShouldBeCreated(){if(j)try{const e=["vite.renderer.config.ts","vite.config.ts"];for(const t of e){const e=E(S(),t);if(!await z(e))continue;if((await O(e)).toString().includes("hologyBuild"))return void(this.shouldCreateIndex=!1)}this.shouldCreateIndex=!0}catch(e){console.warn("Failed to read vite config to determine if meta files should be created")}}async createFolder(e,t=""){D(),await v.mkdir(x.join(this.path,t,e),{recursive:!0})}async deleteFolderForceDangerous(e){await v.rm(x.join(this.path,e),{recursive:!0,force:!0})}async moveFolder(e,t){if(D(),""==e)return void console.warn("Can not move a folder in root");const a=x.resolve(x.join(this.path,t),x.basename(e));if(await U(a)){if(a!==x.resolve(this.path,e))throw new Error("Can not move to directory as a file already exists with the same name")}else{if(function(e,t){const a=x.resolve(e),i=x.resolve(t),n=x.normalize(a)+x.sep;return(x.normalize(i)+x.sep).startsWith(n)}(x.join(this.path,e),x.join(this.path,t)))throw new Error("Can not move a folder into a folder it contains");await v.rename(x.join(this.path,e),a)}}async renameFolder(e,t){await v.rename(x.join(this.path,e),x.resolve(x.dirname(x.join(this.path,e)),t))}async moveToFolder(e,t){if(D(),(e.path??"")===t)return e;const a={...e,path:t};await this.assertObjectFilePathAvailable(a,e.id);const i=this.privateObjectPath(a);return await v.rename(this.privateObjectPath(e),i),await this.addOrUpdateIndexEntry(a),a}getAbsolutePath(e=""){return x.join(this.path,e)}getResourceAbsolutePath(e=""){return x.join(this.pathResources,e)}watchFolders(){return s(this.loaded).pipe(f(()=>s(k(this.path))),f(()=>this.watchDir(this.path).pipe(i(e=>!e.filename.endsWith(".json")),u(null),f(()=>s(v.readdir(this.path,{recursive:!0,withFileTypes:!0}))),r(e=>Array.from(new Set(e.filter(e=>e.isDirectory()).map(e=>x.relative(this.path,x.join(e.parentPath,e.name)).replace(/^\.$/,"")).values()))))))}watch(){D();const e=s(this.loaded).pipe(f(()=>s(k(this.pathResources))),f(()=>this.watchDir(this.pathResources)),l(e=>{if(null==e||!["add","change","unlink"].includes(e.event))return a;const t=x.basename(e.filename);return s(this.getAll()).pipe(l(e=>{const i=e.filter(e=>e.fileKey===t);return 0===i.length?a:s(i).pipe(l(e=>{const t=this.privateObjectRelativePath(e);return s(this.readFileIfExists(t)).pipe(r(a=>({event:"change",object:a,path:e.path,filename:t})))}))}))})),t=s(this.loaded).pipe(f(()=>this.watchDir(this.path)),l(e=>{const t={event:e.event,path:x.dirname(e.filename).replace(/^\.$/,""),filename:x.basename(e.filename)};return e.filename.endsWith(".json")?"unlink"!==e.event?s(this.readFileIfExists(e.filename)).pipe(w(e=>{null!=e&&(null==this.cachedIndex&&(this.cachedIndex={}),this.cachedIndex[e.id]={id:e.id,name:e.name??e.id,path:e.path},this.persistIndex())}),r(e=>({object:e,...t}))):d({object:null,...t}).pipe(w(()=>{const t=Object.keys(this.cachedIndex??{}).find(t=>this.privateObjectRelativePath(this.cachedIndex[t])===e.filename);t&&this.removeIndexEntry(t)})):"change"===e.event?s(this.reloadSubdirectory(e.filename)).pipe(o(),r(e=>({object:e,...t}))):a}));return h(t,e)}async reloadSubdirectory(e){return(await this.getAll(e)).filter(t=>t.path.startsWith(e))}async readFileIfExists(e){const t=E(this.path,e);for(let a=0;a<3;a++)try{const a=await O(t);return{...JSON.parse(a.toString()),path:x.dirname(e).replace(/^\.$/,""),filename:x.basename(e)}}catch(e){if(2===a)return console.error("Could not read file at "+t,e),null;await m(50*(a+1))}return null}async getAll(e){if(j){await this.loaded,await k(this.path);const t=await this.getObjectFilePaths(e??""),a=null==e||""===e,i=[];for(let e=0;e<t.length;e+=32){const a=Math.min(32,t.length-e),n=await Promise.all(Array.from({length:a},(a,i)=>{const n=t[e+i];return O(E(this.path,n)).then(e=>({...JSON.parse(e.toString()),path:x.dirname(n).replace(/^\.$/,""),filename:x.basename(n)}))}));i.push(...n)}return a&&(this.cachedIndex=R(i),await this.persistIndex()),i}const t=await this.loadIndex();return Promise.all(Object.keys(t).map(e=>this.get(e)))}async getObjectFilePaths(e){const t=this.path,a=x.join(t,e),i=[];return await async function e(a){const n=await v.readdir(a,{withFileTypes:!0});for(const s of n){const n=x.join(a,s.name);s.isDirectory()?await e(n):s.isFile()&&s.name.endsWith(".json")&&!/^[\._]/.test(s.name)&&i.push(x.relative(t,n))}}(a),i}async get(e){const t=await this.loadIndex(),a=t[e]??Object.values(t).find(t=>t.name===e);if(null==a)return;const i=this.privateObjectPath(a);if(!j)return(await fetch(i)).json();return await z(i)?{...JSON.parse((await O(i)).toString()),path:a.path??"",filename:x.basename(this.privateObjectRelativePath(a))}:null}async save(e){if(D(),await this.loaded,null==e.path){const t=(await this.loadIndex())[e.id];null!=t?.path&&(e={...e,path:t.path})}return await this.assertObjectFilePathAvailable(e,e.id),await v.writeFile(this.privateObjectPath(e),this.serialize(e)),await this.addOrUpdateIndexEntry(e),e}async rename(e,t){const a={...e,name:t};await this.assertObjectFilePathAvailable(a,e.id);const i=this.privateObjectPath(e),n=this.privateObjectPath(a);try{await v.rename(i,n)}catch(e){console.error(e),console.warn("Rename failed, retrying",{currentPath:i,newPath:n}),await m(400),await v.rename(i,n)}return await this.save(a),a}async delete(e){await v.unlink(this.privateObjectPath(e)),this.removeIndexEntry(e.id)}async create(t){D(),await this.loaded,t.id=e();const a=this.privateObjectPath(t);if(await this.assertObjectFilePathAvailable(t),await U(a))throw Error(`Can not create because a file already exists at ${a}`);return await v.writeFile(a,this.serialize(t)),await this.addOrUpdateIndexEntry(t),t}prepareCreate(t){return D(),t.id=e(),t}serialize(e){return this.serializer(e)}async addOrUpdateIndexEntry(e){null==this.cachedIndex&&await this.loadIndex(),this.cachedIndex[e.id]={id:e.id,name:e.name??e.id,path:e.path},await this.persistIndex()}removeIndexEntry(e){null!=this.cachedIndex&&delete this.cachedIndex[e],this.persistIndex()}async persistIndex(){j&&this.shouldCreateIndex&&null!=this.cachedIndex&&await v.writeFile(this.indexFilePath,JSON.stringify(this.cachedIndex,null,2))}get indexFilePath(){return E(this.path,"_meta.json")}async loadIndex(){if(null!=this.cachedIndex)return this.cachedIndex;this.loadingIndex??(this.loadingIndex=this.createIndex());try{return await this.loadingIndex}finally{this.loadingIndex=null}}async createIndex(){if(j){const e=await this.getAll();this.cachedIndex=R(e),await this.persistIndex()}else this.cachedIndex=await(await fetch(this.indexFilePath)).json();return this.cachedIndex}async ensureResourceDir(){await k(E(this.path+"-resources"))}async saveFile(e,t){return D(),await k(E(this.path+"-resources")),v.copyFile(t.path,E(this.path+"-resources",e.fileKey))}async saveExtraFile(e,t){return D(),await k(E(this.path+"-resources")),v.copyFile(e,E(this.path+"-resources",t))}getAssetPath(e){return window&&"function"==typeof window.require?window.require("path").join(this.path+"-resources",e.fileKey):E(this.path+"-resources",e.fileKey)}async replaceFile(e,t){if(await z(t))return v.copyFile(t,E(this.path+"-resources",e.fileKey));console.error("Failed to replace file using path "+t)}async deleteFile(e){if(null==e)return;D();const t=E(this.path+"-resources",e);return await z(t)?v.unlink(t):void 0}privateObjectPath(e){return E(this.path,this.privateObjectRelativePath(e))}privateObjectRelativePath(e){return this.filePathFn?E(e.path??"",this.filePathFn(e)):E(e.path??"",tokenizeName(e.name??e.id)+".json")}async assertObjectFilePathAvailable(e,t){const a=await this.findObjectFilePathCollision(e,t);if(null!=a)throw Error(`Can not save "${e.name??e.id}" because "${a.name}" already uses the same storage file name. Use a name that differs beyond letter casing.`)}async findObjectFilePathCollision(e,t){const a=A(this.privateObjectRelativePath(e)),i=await this.loadIndex();return Object.values(i).find(e=>e.id!==t&&A(this.privateObjectRelativePath(e))===a)??null}watchDir(e){return new c(t=>{if("win32"===process.platform&&j){const a=y.watch(e,{recursive:!0},(a,i)=>{if(i){const n=x.join(e,i);y.access(n,y.constants.F_OK,e=>{const n=e?"unlink":"rename"===a?"add":"change";t.next({event:n,filename:i})})}});return()=>a.close()}const a=b.watch(e,{cwd:e,ignoreInitial:!0,disableGlobbing:!0});return a.on("all",(e,a,i)=>{t.next({event:e,filename:a})}),a.on("error",()=>{}),()=>{a.close()}})}}export function tokenizeName(e){return e.trim().replace(/\s/g,"_").replace(/[^a-z0-9_\-\.]/gi,"")}function A(e){return e.replace(/\\/g,"/").toLowerCase()}function R(e){const t={};for(const a of e)t[a.id]={id:a.id,name:a.name??a.id,path:a.path};return t}async function k(e){j&&(await z(e)||await v.mkdir(e,{recursive:!0}))}function z(e){return!!j&&new Promise(function(t,a){y.exists(e,function(e){t(e)})})}function D(){if(!j)throw new Error("Must have direct access to filesystem")}async function U(e){try{await v.access(e,v.constants.F_OK)}catch(e){return!1}return!0}export function defaultSerializer(e){const t={...e};return delete t.path,delete t.filename,JSON.stringify(t,null,2)}/*
|
|
1
|
+
import{randomUUID as e}from"../../utils/uuid.js";import{pathJoin as t}from"../../utils/files.js";import{EMPTY as a,filter as i,firstValueFrom as n,from as s,map as r,merge as h,mergeAll as o,mergeMap as l,Observable as c,of as d,startWith as u,Subject as p,switchMap as f,tap as w}from"rxjs";import{sleepDelay as m}from"../../utils/async.js";const y={},v={},x={},b={};null==y.read&&window.require&&(Object.assign(y,window.require("fs")),Object.assign(v,y.promises),Object.assign(x,window.require("path")),Object.assign(b,window.require("chokidar")));const j=null!=y.existsSync;let g=0,I=0;const P=[];async function F(e){await function(){if(g<32)return g++,Promise.resolve();return new Promise(e=>P.push(e))}();try{return await e()}finally{!function(){const e=P[I];if(null==e)return P.length=0,I=0,void g--;I++,I>64&&2*I>P.length&&(P.splice(0,I),I=0);e()}()}}function O(e){return F(()=>v.readFile(e))}function S(){if(j){const e="--path=",t=window.process.argv.find(t=>t.startsWith(e));return t?t.substring(e.length):""}return""}const C=/^[A-Z]:/;function E(...e){return 0===e.length?"":C.test(e[0])||j?x.join(...e):t(...e)}export class ObjectStorage{get pathResources(){return E(this.path+"-resources")}constructor(e,t,a=defaultSerializer){this.name=e,this.filePathFn=t,this.serializer=a,this.basePathUpdates=new p,this.basePath=this.basePathUpdates.pipe(r(e=>E(S(),e)),w(e=>{this.path=E(e,this.name),this.cachedIndex=null,this.loadingIndex=null,k(this.path),this.determineIfMetaFileShouldBeCreated()})),this.loaded=n(this.basePath),this.cachedIndex=null,this.loadingIndex=null,this.shouldCreateIndex=!1}setBasePath(e){this.basePathUpdates.next(e)}async determineIfMetaFileShouldBeCreated(){if(j)try{const e=["vite.renderer.config.ts","vite.config.ts"];for(const t of e){const e=E(S(),t);if(!await z(e))continue;if((await O(e)).toString().includes("hologyBuild"))return void(this.shouldCreateIndex=!1)}this.shouldCreateIndex=!0}catch(e){console.warn("Failed to read vite config to determine if meta files should be created")}}async createFolder(e,t=""){D(),await v.mkdir(x.join(this.path,t,e),{recursive:!0})}async deleteFolderForceDangerous(e){await v.rm(x.join(this.path,e),{recursive:!0,force:!0})}async moveFolder(e,t){if(D(),""==e)return void console.warn("Can not move a folder in root");const a=x.resolve(x.join(this.path,t),x.basename(e));if(await U(a)){if(a!==x.resolve(this.path,e))throw new Error("Can not move to directory as a file already exists with the same name")}else{if(function(e,t){const a=x.resolve(e),i=x.resolve(t),n=x.normalize(a)+x.sep;return(x.normalize(i)+x.sep).startsWith(n)}(x.join(this.path,e),x.join(this.path,t)))throw new Error("Can not move a folder into a folder it contains");await v.rename(x.join(this.path,e),a)}}async renameFolder(e,t){await v.rename(x.join(this.path,e),x.resolve(x.dirname(x.join(this.path,e)),t))}async moveToFolder(e,t){if(D(),(e.path??"")===t)return e;const a={...e,path:t};await this.assertObjectFilePathAvailable(a,e.id);const i=this.privateObjectPath(a);return await v.rename(this.privateObjectPath(e),i),await this.addOrUpdateIndexEntry(a),a}getAbsolutePath(e=""){return x.join(this.path,e)}getResourceAbsolutePath(e=""){return x.join(this.pathResources,e)}watchFolders(){return s(this.loaded).pipe(f(()=>s(k(this.path))),f(()=>this.watchDir(this.path).pipe(i(e=>!e.filename.endsWith(".json")),u(null),f(()=>s(v.readdir(this.path,{recursive:!0,withFileTypes:!0}))),r(e=>Array.from(new Set(e.filter(e=>e.isDirectory()).map(e=>x.relative(this.path,x.join(e.parentPath,e.name)).replace(/^\.$/,"")).values()))))))}watch(){D();const e=s(this.loaded).pipe(f(()=>s(k(this.pathResources))),f(()=>this.watchDir(this.pathResources)),l(e=>{if(null==e||!["add","change","unlink"].includes(e.event))return a;const t=x.basename(e.filename);return s(this.getAll()).pipe(l(e=>{const i=e.filter(e=>e.fileKey===t);return 0===i.length?a:s(i).pipe(l(e=>{const t=this.privateObjectRelativePath(e);return s(this.readFileIfExists(t)).pipe(r(a=>({event:"change",object:a,path:e.path,filename:t})))}))}))})),t=s(this.loaded).pipe(f(()=>this.watchDir(this.path)),i(e=>"_meta.json"!==e.filename),l(e=>{const t={event:e.event,path:x.dirname(e.filename).replace(/^\.$/,""),filename:x.basename(e.filename)};return e.filename.endsWith(".json")?"unlink"!==e.event?s(this.readFileIfExists(e.filename)).pipe(w(e=>{null!=e&&(null==this.cachedIndex&&(this.cachedIndex={}),this.cachedIndex[e.id]={id:e.id,name:e.name??e.id,path:e.path},this.persistIndex())}),r(e=>({object:e,...t}))):d({object:null,...t}).pipe(w(()=>{const t=Object.keys(this.cachedIndex??{}).find(t=>this.privateObjectRelativePath(this.cachedIndex[t])===e.filename);t&&this.removeIndexEntry(t)})):"change"===e.event?s(this.reloadSubdirectory(e.filename)).pipe(o(),r(e=>({object:e,...t}))):a}));return h(t,e)}async reloadSubdirectory(e){return(await this.getAll(e)).filter(t=>t.path.startsWith(e))}async readFileIfExists(e){const t=E(this.path,e);for(let a=0;a<3;a++)try{const a=await O(t);return{...JSON.parse(a.toString()),path:x.dirname(e).replace(/^\.$/,""),filename:x.basename(e)}}catch(e){if(2===a)return console.error("Could not read file at "+t,e),null;await m(50*(a+1))}return null}async getAll(e){if(j){await this.loaded,await k(this.path);const t=await this.getObjectFilePaths(e??""),a=null==e||""===e,i=[];for(let e=0;e<t.length;e+=32){const a=Math.min(32,t.length-e),n=await Promise.all(Array.from({length:a},(a,i)=>{const n=t[e+i];return O(E(this.path,n)).then(e=>({...JSON.parse(e.toString()),path:x.dirname(n).replace(/^\.$/,""),filename:x.basename(n)}))}));i.push(...n)}return a&&(this.cachedIndex=R(i),await this.persistIndex()),i}const t=await this.loadIndex();return Promise.all(Object.keys(t).map(e=>this.get(e)))}async getObjectFilePaths(e){const t=this.path,a=x.join(t,e),i=[];return await async function e(a){const n=await v.readdir(a,{withFileTypes:!0});for(const s of n){const n=x.join(a,s.name);s.isDirectory()?await e(n):s.isFile()&&s.name.endsWith(".json")&&!/^[\._]/.test(s.name)&&i.push(x.relative(t,n))}}(a),i}async get(e){const t=await this.loadIndex(),a=t[e]??Object.values(t).find(t=>t.name===e);if(null==a)return;const i=this.privateObjectPath(a);if(!j)return(await fetch(i)).json();return await z(i)?{...JSON.parse((await O(i)).toString()),path:a.path??"",filename:x.basename(this.privateObjectRelativePath(a))}:null}async save(e){if(D(),await this.loaded,null==e.path){const t=(await this.loadIndex())[e.id];null!=t?.path&&(e={...e,path:t.path})}return await this.assertObjectFilePathAvailable(e,e.id),await v.writeFile(this.privateObjectPath(e),this.serialize(e)),await this.addOrUpdateIndexEntry(e),e}async rename(e,t){const a={...e,name:t};await this.assertObjectFilePathAvailable(a,e.id);const i=this.privateObjectPath(e),n=this.privateObjectPath(a);try{await v.rename(i,n)}catch(e){console.error(e),console.warn("Rename failed, retrying",{currentPath:i,newPath:n}),await m(400),await v.rename(i,n)}return await this.save(a),a}async delete(e){await v.unlink(this.privateObjectPath(e)),this.removeIndexEntry(e.id)}async create(t){D(),await this.loaded,t.id=e();const a=this.privateObjectPath(t);if(await this.assertObjectFilePathAvailable(t),await U(a))throw Error(`Can not create because a file already exists at ${a}`);return await v.writeFile(a,this.serialize(t)),await this.addOrUpdateIndexEntry(t),t}prepareCreate(t){return D(),t.id=e(),t}serialize(e){return this.serializer(e)}async addOrUpdateIndexEntry(e){null==this.cachedIndex&&await this.loadIndex(),this.cachedIndex[e.id]={id:e.id,name:e.name??e.id,path:e.path},await this.persistIndex()}removeIndexEntry(e){null!=this.cachedIndex&&delete this.cachedIndex[e],this.persistIndex()}async persistIndex(){j&&this.shouldCreateIndex&&null!=this.cachedIndex&&await v.writeFile(this.indexFilePath,JSON.stringify(this.cachedIndex,null,2))}get indexFilePath(){return E(this.path,"_meta.json")}async loadIndex(){if(null!=this.cachedIndex)return this.cachedIndex;this.loadingIndex??(this.loadingIndex=this.createIndex());try{return await this.loadingIndex}finally{this.loadingIndex=null}}async createIndex(){if(j){const e=await this.getAll();this.cachedIndex=R(e),await this.persistIndex()}else this.cachedIndex=await(await fetch(this.indexFilePath)).json();return this.cachedIndex}async ensureResourceDir(){await k(E(this.path+"-resources"))}async saveFile(e,t){return D(),await k(E(this.path+"-resources")),v.copyFile(t.path,E(this.path+"-resources",e.fileKey))}async saveExtraFile(e,t){return D(),await k(E(this.path+"-resources")),v.copyFile(e,E(this.path+"-resources",t))}getAssetPath(e){return window&&"function"==typeof window.require?window.require("path").join(this.path+"-resources",e.fileKey):E(this.path+"-resources",e.fileKey)}async replaceFile(e,t){if(await z(t))return v.copyFile(t,E(this.path+"-resources",e.fileKey));console.error("Failed to replace file using path "+t)}async deleteFile(e){if(null==e)return;D();const t=E(this.path+"-resources",e);return await z(t)?v.unlink(t):void 0}privateObjectPath(e){return E(this.path,this.privateObjectRelativePath(e))}privateObjectRelativePath(e){return this.filePathFn?E(e.path??"",this.filePathFn(e)):E(e.path??"",tokenizeName(e.name??e.id)+".json")}async assertObjectFilePathAvailable(e,t){const a=await this.findObjectFilePathCollision(e,t);if(null!=a)throw Error(`Can not save "${e.name??e.id}" because "${a.name}" already uses the same storage file name. Use a name that differs beyond letter casing.`)}async findObjectFilePathCollision(e,t){const a=A(this.privateObjectRelativePath(e)),i=await this.loadIndex();return Object.values(i).find(e=>e.id!==t&&A(this.privateObjectRelativePath(e))===a)??null}watchDir(e){return new c(t=>{if("win32"===process.platform&&j){const a=y.watch(e,{recursive:!0},(a,i)=>{if(i){const n=x.join(e,i);y.access(n,y.constants.F_OK,e=>{const n=e?"unlink":"rename"===a?"add":"change";t.next({event:n,filename:i})})}});return()=>a.close()}const a=b.watch(e,{cwd:e,ignoreInitial:!0,disableGlobbing:!0});return a.on("all",(e,a,i)=>{t.next({event:e,filename:a})}),a.on("error",()=>{}),()=>{a.close()}})}}export function tokenizeName(e){return e.trim().replace(/\s/g,"_").replace(/[^a-z0-9_\-\.]/gi,"")}function A(e){return e.replace(/\\/g,"/").toLowerCase()}function R(e){const t={};for(const a of e)t[a.id]={id:a.id,name:a.name??a.id,path:a.path};return t}async function k(e){j&&(await z(e)||await v.mkdir(e,{recursive:!0}))}function z(e){return!!j&&new Promise(function(t,a){y.exists(e,function(e){t(e)})})}function D(){if(!j)throw new Error("Must have direct access to filesystem")}async function U(e){try{await v.access(e,v.constants.F_OK)}catch(e){return!1}return!0}export function defaultSerializer(e){const t={...e};return delete t.path,delete t.filename,JSON.stringify(t,null,2)}/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{__decorate as e,__metadata as a}from"tslib";import{Color as t,MeshStandardMaterial as n,Texture as r,Vector2 as i}from"three";import{Shader as s}from"../shader.js";import{Parameter as o}from"../parameter.js";import*as p from"three";import{attributes as l,AttributeVec3Node as d,colorToNormal as h,ifDefApply as m,NodeShaderMaterial as u,normalize as v,Sampler2DNode as c,standardMaterial as y,UniformSampler2d as S,uniformFloat as g,uniformVec2 as f,uniformVec3 as M,varying as x,varyingAttributes as C,varyingTransformed as b,vec2 as w,UniformVec3Node as O,UniformFloatNode as R}from"three-shader-graph";import{parallaxOcclusionMapping as L}from"../../shader-nodes/pom.js";import{createPlaceholderTexture as T}from"../../utils/three/placeholder-texture.js";const N=T(),B=new i(1,1);export class StandardShader extends s{constructor(){super(...arguments),this.color=new t("#FFFFFF"),this.map=new S("map",N),this.opacity=1,this.alphaMap=new S("alphaMap",N),this.roughness=1,this.roughnessMap=new S("roughnessMap",N),this.metalness=0,this.metalnessMap=new S("metalnessMap",N),this.lightMap=new S("lightMap",N),this.aoMap=new S("aoMap",N),this.emissiveMap=new S("emissiveMap",N),this.normalMap=new S("normalMap",N),this.normalScale=new i(1,1),this.sheen=0,this.sheenColor=new t("#FFFFFF"),this.sheenColorMap=new S("sheenColorMap",N),this.sheenRoughness=.5,this.sheenRoughnessMap=new S("sheenRoughnessMap",N),this.anisotropyMap=new S("anisotropyMap",N),this.anisotropy=0,this.heightMap=new S("heightMap",N),this.vertexColor=!1,this.uvScale=B}build(){P(this.map,p.SRGBColorSpace),P(this.alphaMap,p.LinearSRGBColorSpace),P(this.roughnessMap,p.LinearSRGBColorSpace),P(this.metalnessMap,p.LinearSRGBColorSpace),P(this.aoMap,p.LinearSRGBColorSpace),P(this.emissiveMap,p.SRGBColorSpace),P(this.normalMap,p.LinearSRGBColorSpace),P(this.sheenColorMap,p.LinearSRGBColorSpace),P(this.sheenRoughnessMap,p.LinearSRGBColorSpace),P(this.anisotropyMap,p.LinearSRGBColorSpace);let e=!1;const a=f("uvScale",this.uvScale??B);let n=C.uv.multiply(a);F(this.heightMap)&&0!==(this.heightScale??.05)&&(n=L(n,this.heightMap,g("heightScale",this.heightScale??.05)));const r=x(new d("particleData"));let s=m("IS_PARTICLE",new R("opacity",this.opacity,void 0,!1),e=>e.multiply(r.x)),o=new O("color",(new p.Vector3).setFromColor(this.color),void 0,!1).rgb;if(F(this.map)){const e=this.map.sample(n);o=o.multiply(e.rgb),s=s.multiply(e.a)}F(this.alphaMap)&&(s=s.multiply(this.alphaMap.sample(n).r)),!0===this.vertexColor&&(o=o.multiply(x(l.color.rgb)));let c=null,S=null;F(this.aoMap)&&(S=g("aoMapIntensity",this.aoMapIntensity??1),c=this.aoMap.sample(n).r);let T=g("roughness",this.roughness??1);F(this.roughnessMap)&&(T=T.multiply(this.roughnessMap.sample(n).g),e=!0);let N=g("metalness",this.metalness??0);F(this.metalnessMap)&&(N=N.multiply(this.metalnessMap.sample(n).b),e=!0);let D=null;F(this.lightMap)&&(D=this.lightMap.sample(n).rgb.multiplyScalar(g("lightMapIntensity",this.lightMapIntensity??1)));let _=M("emissive",(new p.Vector3).setFromColor(this.emissive??new t(0))).rgb;F(this.emissiveMap)&&(_=_.multiply(this.emissiveMap.sample(n).rgb));const I=g("emissiveIntensity",this.emissiveIntensity??1);let E=b.normal;if(F(this.normalMap)){const e=g("normalScale",this.normalScale?.x??1);E=h(this.normalMap.sample(n),e)}let V=null,G=null;if((this.sheen??0)>0){const e=g("sheen",this.sheen??0);V=M("sheenColor",(new p.Vector3).setFromColor(this.sheenColor??new t(16777215))).rgb.multiplyScalar(e),F(this.sheenColorMap)&&(V=V.multiply(this.sheenColorMap.sample(n).rgb)),G=g("sheenRoughness",this.sheenRoughness??.5),F(this.sheenRoughnessMap)&&(G=G.multiply(this.sheenRoughnessMap.sample(n).a))}(this.roughness<1||this.metalness>0)&&(e=!0);let H=null,A=null;if((this.anisotropy??0)>0&&(H=g("anisotropy",this.anisotropy??0),A=f("anisotropyDirection",(U=this.anisotropyRotation??0,new i(Math.cos(U),Math.sin(U)))),F(this.anisotropyMap))){const e=this.anisotropyMap.sample(n),a=v(w(e.r,e.g).multiplyScalar(2).subtract(w(1,1)));A=w(A.x.multiply(a.x).subtract(A.y.multiply(a.y)),A.y.multiply(a.x).add(A.x.multiply(a.y))),H=H.multiply(e.b)}var U;const z=new u({color:y({color:o.rgba(s),roughness:T,metalness:N,ambientOcclusion:c,ambientOcclusionIntensity:S,emissive:_,emissiveIntensity:I,normal:E,bakedLight:D,sheenColor:V,sheenRoughness:G,anisotropy:H,anisotropyDirection:A,specular:e}),opacity:s,roughness:T,normal:E,emissive:_.multiplyScalar(I),envMap:this.envMap});return null!=this.envMap&&(z.uniforms.envMapIntensity={value:this.envMapIntensity??1}),z.vertexColors=!0===this.vertexColor,z}}function F(e){return null!=e&&(!(e instanceof S)||null!=e.value&&e.value!==N)}e([o(),a("design:type",t)],StandardShader.prototype,"color",void 0),e([o({label:"Color Map"}),a("design:type",c)],StandardShader.prototype,"map",void 0),e([o({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"opacity",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"alphaMap",void 0),e([o({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"roughness",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"roughnessMap",void 0),e([o({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"metalness",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"metalnessMap",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"lightMap",void 0),e([o(),a("design:type",Number)],StandardShader.prototype,"lightMapIntensity",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"aoMap",void 0),e([o(),a("design:type",Number)],StandardShader.prototype,"aoMapIntensity",void 0),e([o(),a("design:type",t)],StandardShader.prototype,"emissive",void 0),e([o({range:[0,10]}),a("design:type",Number)],StandardShader.prototype,"emissiveIntensity",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"emissiveMap",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"normalMap",void 0),e([o(),a("design:type",i)],StandardShader.prototype,"normalScale",void 0),e([o({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"sheen",void 0),e([o(),a("design:type",t)],StandardShader.prototype,"sheenColor",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"sheenColorMap",void 0),e([o({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"sheenRoughness",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"sheenRoughnessMap",void 0),e([o(),a("design:type",Number)],StandardShader.prototype,"anisotropyRotation",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"anisotropyMap",void 0),e([o({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"anisotropy",void 0),e([o(),a("design:type",r)],StandardShader.prototype,"envMap",void 0),e([o({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"envMapIntensity",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"heightMap",void 0),e([o(),a("design:type",Number)],StandardShader.prototype,"heightScale",void 0),e([o(),a("design:type",Boolean)],StandardShader.prototype,"vertexColor",void 0),e([o(),a("design:type",i)],StandardShader.prototype,"uvScale",void 0);export var ParallaxType;!function(e){e[e.none=0]="none",e[e.offset=1]="offset",e[e.pom=2]="pom"}(ParallaxType||(ParallaxType={}));export class ParallaxStandardMaterial extends n{constructor(e={}){super(e),e.heightMap&&(this.heightMap=e.heightMap),this.heightScale=e.heightScale??.05,this.parallaxType=e.parallaxType??ParallaxType.pom,this.minLayers=e.minLayers??10,this.maxLayers=e.maxLayers??32}onBeforeCompile(e){e.uniforms.heightMap={value:this.heightMap},e.uniforms.heightScale={value:this.heightScale},e.uniforms.minLayers={value:this.minLayers},e.uniforms.maxLayers={value:this.maxLayers},e.uniforms.parallaxType={value:this.parallaxType},e.vertexTangents=!0,e.vertexShader=e.vertexShader.replace("#include <uv_pars_vertex>","\n #include <uv_pars_vertex>\n "),e.vertexShader=e.vertexShader.replace("#include <begin_vertex>","\n #include <begin_vertex>\n "),e.fragmentShader=e.fragmentShader.replace("#include <uv_pars_fragment>","\n #include <uv_pars_fragment>\n uniform sampler2D heightMap;\n uniform float heightScale;\n uniform int minLayers;\n uniform int maxLayers;\n uniform int parallaxType;\n\n vec2 parallaxOffset(vec2 uv, vec3 viewDir, vec3 normal) {\n // Compute TBN in fragment shader using screen-space derivatives\n vec3 dp1 = dFdx(-vViewPosition);\n vec3 dp2 = dFdy(-vViewPosition);\n vec2 duv1 = dFdx(uv);\n vec2 duv2 = dFdy(uv);\n \n vec3 dp2perp = cross(dp2, normal);\n vec3 dp1perp = cross(normal, dp1);\n vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;\n vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;\n \n float invmax = inversesqrt(max(dot(T,T), dot(B,B)));\n mat3 tbn = mat3(T * invmax, B * invmax, normal);\n vec3 viewDirTS = -viewDir * tbn;\n\n float h = texture(heightMap, uv).r;\n return uv - (viewDirTS.xy / viewDirTS.z) * (h * heightScale);\n }\n\n vec2 parallaxOcclusion2(vec2 uv, vec3 viewDir, vec3 normal) {\n // Compute TBN in fragment shader using screen-space derivatives\n vec3 dp1 = dFdx(-vViewPosition);\n vec3 dp2 = dFdy(-vViewPosition);\n vec2 duv1 = dFdx(uv);\n vec2 duv2 = dFdy(uv);\n \n vec3 dp2perp = cross(dp2, normal);\n vec3 dp1perp = cross(normal, dp1);\n vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;\n vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;\n \n float invmax = inversesqrt(max(dot(T,T), dot(B,B)));\n mat3 tbn = mat3(T * invmax, B * invmax, normal);\n vec3 vv = -viewDir * tbn;\n\n float parallaxLimit = -length(vv.xy) / vv.z;\n parallaxLimit *= heightScale;\n\n vec2 vOffsetDir = normalize(vv.xy);\n vec2 vMaxOffset = vOffsetDir * parallaxLimit;\n\n float factor = pow(1.0 - abs(vv.z), 2.0);\n float nNumSamples = mix(float(minLayers), float(maxLayers), factor);\n float fStepSize = 1.0 / nNumSamples;\n\n float fCurrRayHeight = 1.0;\n vec2 vCurrOffset = vec2(0.0);\n vec2 vLastOffset = vec2(0.0);\n float fLastSampledHeight = 1.0;\n float fCurrSampledHeight = 1.0;\n\n for (int nCurrSample = 0; nCurrSample < 128; nCurrSample++) {\n if (float(nCurrSample) > nNumSamples) break;\n\n fCurrSampledHeight = texture2D(heightMap, uv + vCurrOffset).r;\n if (fCurrSampledHeight > fCurrRayHeight) {\n float delta1 = fCurrSampledHeight - fCurrRayHeight;\n float delta2 = (fCurrRayHeight + fStepSize) - fLastSampledHeight;\n float ratio = delta1 / (delta1 + delta2);\n vCurrOffset = ratio * vLastOffset + (1.0 - ratio) * vCurrOffset;\n break;\n } else {\n fCurrRayHeight -= fStepSize;\n vLastOffset = vCurrOffset;\n vCurrOffset += fStepSize * vMaxOffset;\n fLastSampledHeight = fCurrSampledHeight;\n }\n }\n\n return uv + vCurrOffset;\n }\n "),e.fragmentShader=e.fragmentShader.replace("#include <map_fragment>","\n vec3 pomViewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition );\n vec2 uvPOM = vMapUv ;\n if (parallaxType == 1) {\n uvPOM = parallaxOffset(vMapUv, pomViewDir, vNormal);\n } else if (parallaxType == 2) {\n uvPOM = parallaxOcclusion2(vMapUv, pomViewDir, vNormal);\n }\n\n vec4 texelColor = texture2D(map, uvPOM);\n diffuseColor *= texelColor;\n "),e.fragmentShader=e.fragmentShader.replace("#include <normal_fragment_maps>","\n #ifdef USE_NORMALMAP\n // RE-COMPUTE TBN for normal mapping consistency\n vec3 dp1 = dFdx(-vViewPosition);\n vec3 dp2 = dFdy(-vViewPosition);\n vec2 duv1 = dFdx(uvPOM);\n vec2 duv2 = dFdy(uvPOM);\n vec3 dp2perp = cross(dp2, vNormal);\n vec3 dp1perp = cross(vNormal, dp1);\n vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;\n vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;\n float invmax = inversesqrt(max(dot(T,T), dot(B,B)));\n mat3 localTBN = mat3(T * invmax, B * invmax, vNormal);\n\n vec3 mapN = texture2D(normalMap, uvPOM).xyz * 2.0 - 1.0;\n mapN.xy *= normalScale;\n normal = normalize( localTBN * mapN );\n #endif\n "),e.fragmentShader=e.fragmentShader.replace("#include <roughnessmap_fragment>","\n float roughnessFactor = roughness;\n #ifdef USE_ROUGHNESSMAP\n vec4 texelRoughness = texture2D( roughnessMap, uvPOM );\n // reads channel G, compatible with a combined OcclusionRoughnessMetallic (RGB) texture\n roughnessFactor *= texelRoughness.g;\n #endif\n "),e.fragmentShader=e.fragmentShader.replace("#include <metalnesssmap_fragment>","\n float metalnessFactor = metalness;\n #ifdef USE_METALNESSMAP\n vec4 texelMetalness = texture2D( metalnessMap, uvPOM );\n // reads channel B, compatible with a combined OcclusionRoughnessMetallic (RGB) texture\n metalnessFactor *= texelMetalness.b;\n #endif\n "),e.fragmentShader=e.fragmentShader.replace("#include <aomap_fragment>","\n #ifdef USE_AOMAP\n\n // reads channel R, compatible with a combined OcclusionRoughnessMetallic (RGB) texture\n float ambientOcclusion = ( texture2D( aoMap, uvPOM ).r - 1.0 ) * aoMapIntensity + 1.0;\n\n reflectedLight.indirectDiffuse *= ambientOcclusion;\n\n #if defined( USE_CLEARCOAT ) \n clearcoatSpecularIndirect *= ambientOcclusion;\n #endif\n\n #if defined( USE_SHEEN ) \n sheenSpecularIndirect *= ambientOcclusion;\n #endif\n\n #if defined( USE_ENVMAP ) && defined( STANDARD )\n\n float dotNV = saturate( dot( geometryNormal, geometryViewDir ) );\n\n reflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ambientOcclusion, material.roughness );\n\n #endif\n\n #endif\n "),this.userData.shader=e}clone(){const e=super.clone();return e.heightMap=this.heightMap,e.heightScale=this.heightScale,e.parallaxType=this.parallaxType,e.minLayers=this.minLayers,e.maxLayers=this.maxLayers,e}}export function applyTiling(e,a){if(null==e||null==a)return e;if(1===a.x&&1===a.y)return e;const t=e.clone();return t.repeat.copy(a),t.needsUpdate=!0,t}function P(e,a){null!=e&&(e instanceof r?e.colorSpace=a:e instanceof S&&null!=e.value&&(e.value.colorSpace=a))}/*
|
|
1
|
+
import{__decorate as e,__metadata as a}from"tslib";import{Color as t,MeshStandardMaterial as n,Texture as r,Vector2 as i}from"three";import{Shader as s}from"../shader.js";import{Parameter as o}from"../parameter.js";import*as p from"three";import{attributes as l,AttributeVec3Node as d,colorToNormal as h,ifDefApply as m,NodeShaderMaterial as u,normalize as v,Sampler2DNode as c,standardMaterial as y,UniformSampler2d as S,uniformFloat as g,uniformVec2 as f,uniformVec3 as M,varying as x,varyingAttributes as C,varyingTransformed as b,vec2 as w,UniformVec3Node as O,UniformFloatNode as R}from"three-shader-graph";import{parallaxOcclusionMapping as T}from"../../shader-nodes/pom.js";import{createPlaceholderTexture as L}from"../../utils/three/placeholder-texture.js";const N=L(),B=new i(1,1);export class StandardShader extends s{constructor(){super(...arguments),this.color=new t("#FFFFFF"),this.map=new S("map",N),this.opacity=1,this.alphaMap=new S("alphaMap",N),this.roughness=1,this.roughnessMap=new S("roughnessMap",N),this.metalness=0,this.metalnessMap=new S("metalnessMap",N),this.lightMap=new S("lightMap",N),this.aoMap=new S("aoMap",N),this.emissiveMap=new S("emissiveMap",N),this.normalMap=new S("normalMap",N),this.normalScale=new i(1,1),this.sheen=0,this.sheenColor=new t("#FFFFFF"),this.sheenColorMap=new S("sheenColorMap",N),this.sheenRoughness=.5,this.sheenRoughnessMap=new S("sheenRoughnessMap",N),this.anisotropyMap=new S("anisotropyMap",N),this.anisotropy=0,this.heightMap=new S("heightMap",N),this.vertexColor=!1,this.uvScale=B}build(){P(this.map,p.SRGBColorSpace),P(this.alphaMap,p.LinearSRGBColorSpace),P(this.roughnessMap,p.LinearSRGBColorSpace),P(this.metalnessMap,p.LinearSRGBColorSpace),P(this.aoMap,p.LinearSRGBColorSpace),P(this.emissiveMap,p.SRGBColorSpace),P(this.normalMap,p.LinearSRGBColorSpace),P(this.sheenColorMap,p.LinearSRGBColorSpace),P(this.sheenRoughnessMap,p.LinearSRGBColorSpace),P(this.anisotropyMap,p.LinearSRGBColorSpace);let e=!1;const a=f("uvScale",this.uvScale??B);let n=C.uv.multiply(a);F(this.heightMap)&&0!==(this.heightScale??.05)&&(n=T(n,this.heightMap,g("heightScale",this.heightScale??.05)));const r=x(new d("particleData"));let s=m("IS_PARTICLE",new R("opacity",this.opacity,void 0,!1),e=>e.multiply(r.x)),o=new O("color",(new p.Vector3).setFromColor(this.color),void 0,!1).rgb;if(F(this.map)){const e=this.map.sample(n);o=o.multiply(e.rgb),s=s.multiply(e.a)}F(this.alphaMap)&&(s=s.multiply(this.alphaMap.sample(n).r)),!0===this.vertexColor&&(o=o.multiply(x(l.color.rgb)));let c=null,S=null;F(this.aoMap)&&(S=g("aoMapIntensity",this.aoMapIntensity??1),c=this.aoMap.sample(n).r);let L=g("roughness",this.roughness??1);F(this.roughnessMap)&&(L=L.multiply(this.roughnessMap.sample(n).g),e=!0);let N=g("metalness",this.metalness??0);F(this.metalnessMap)&&(N=N.multiply(this.metalnessMap.sample(n).b),e=!0);let D=null;F(this.lightMap)&&(D=this.lightMap.sample(n).rgb.multiplyScalar(g("lightMapIntensity",this.lightMapIntensity??1)));let _=M("emissive",(new p.Vector3).setFromColor(this.emissive??new t(0))).rgb;F(this.emissiveMap)&&(_=_.multiply(this.emissiveMap.sample(n).rgb));const I=g("emissiveIntensity",this.emissiveIntensity??1);let E=b.normal;if(F(this.normalMap)){const e=g("normalScale",this.normalScale?.x??1);E=h(this.normalMap.sample(n),e)}let V=null,G=null;if((this.sheen??0)>0){const e=g("sheen",this.sheen??0);V=M("sheenColor",(new p.Vector3).setFromColor(this.sheenColor??new t(16777215))).rgb.multiplyScalar(e),F(this.sheenColorMap)&&(V=V.multiply(this.sheenColorMap.sample(n).rgb)),G=g("sheenRoughness",this.sheenRoughness??.5),F(this.sheenRoughnessMap)&&(G=G.multiply(this.sheenRoughnessMap.sample(n).a))}(this.roughness<1||this.metalness>0)&&(e=!0);let H=null,A=null;if((this.anisotropy??0)>0&&(H=g("anisotropy",this.anisotropy??0),A=f("anisotropyDirection",(U=this.anisotropyRotation??0,new i(Math.cos(U),Math.sin(U)))),F(this.anisotropyMap))){const e=this.anisotropyMap.sample(n),a=v(w(e.r,e.g).multiplyScalar(2).subtract(w(1,1)));A=w(A.x.multiply(a.x).subtract(A.y.multiply(a.y)),A.y.multiply(a.x).add(A.x.multiply(a.y))),H=H.multiply(e.b)}var U;const z=new u({color:y({color:o.rgba(s),roughness:L,metalness:N,ambientOcclusion:c,ambientOcclusionIntensity:S,emissive:_,emissiveIntensity:I,normal:E,bakedLight:D,sheenColor:V,sheenRoughness:G,anisotropy:H,anisotropyDirection:A,specular:e}),opacity:s,roughness:L,normal:E,emissive:_.multiplyScalar(I),envMap:this.envMap,alphaTest:this.alphaTest});return null!=this.envMap&&(z.uniforms.envMapIntensity={value:this.envMapIntensity??1}),z.vertexColors=!0===this.vertexColor,z}}function F(e){return null!=e&&(!(e instanceof S)||null!=e.value&&e.value!==N)}e([o(),a("design:type",t)],StandardShader.prototype,"color",void 0),e([o({label:"Color Map"}),a("design:type",c)],StandardShader.prototype,"map",void 0),e([o({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"opacity",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"alphaMap",void 0),e([o({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"roughness",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"roughnessMap",void 0),e([o({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"metalness",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"metalnessMap",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"lightMap",void 0),e([o(),a("design:type",Number)],StandardShader.prototype,"lightMapIntensity",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"aoMap",void 0),e([o(),a("design:type",Number)],StandardShader.prototype,"aoMapIntensity",void 0),e([o(),a("design:type",t)],StandardShader.prototype,"emissive",void 0),e([o({range:[0,10]}),a("design:type",Number)],StandardShader.prototype,"emissiveIntensity",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"emissiveMap",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"normalMap",void 0),e([o(),a("design:type",i)],StandardShader.prototype,"normalScale",void 0),e([o({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"sheen",void 0),e([o(),a("design:type",t)],StandardShader.prototype,"sheenColor",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"sheenColorMap",void 0),e([o({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"sheenRoughness",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"sheenRoughnessMap",void 0),e([o(),a("design:type",Number)],StandardShader.prototype,"anisotropyRotation",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"anisotropyMap",void 0),e([o({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"anisotropy",void 0),e([o(),a("design:type",r)],StandardShader.prototype,"envMap",void 0),e([o({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"envMapIntensity",void 0),e([o(),a("design:type",c)],StandardShader.prototype,"heightMap",void 0),e([o(),a("design:type",Number)],StandardShader.prototype,"heightScale",void 0),e([o(),a("design:type",Boolean)],StandardShader.prototype,"vertexColor",void 0),e([o(),a("design:type",i)],StandardShader.prototype,"uvScale",void 0);export var ParallaxType;!function(e){e[e.none=0]="none",e[e.offset=1]="offset",e[e.pom=2]="pom"}(ParallaxType||(ParallaxType={}));export class ParallaxStandardMaterial extends n{constructor(e={}){super(e),e.heightMap&&(this.heightMap=e.heightMap),this.heightScale=e.heightScale??.05,this.parallaxType=e.parallaxType??ParallaxType.pom,this.minLayers=e.minLayers??10,this.maxLayers=e.maxLayers??32}onBeforeCompile(e){e.uniforms.heightMap={value:this.heightMap},e.uniforms.heightScale={value:this.heightScale},e.uniforms.minLayers={value:this.minLayers},e.uniforms.maxLayers={value:this.maxLayers},e.uniforms.parallaxType={value:this.parallaxType},e.vertexTangents=!0,e.vertexShader=e.vertexShader.replace("#include <uv_pars_vertex>","\n #include <uv_pars_vertex>\n "),e.vertexShader=e.vertexShader.replace("#include <begin_vertex>","\n #include <begin_vertex>\n "),e.fragmentShader=e.fragmentShader.replace("#include <uv_pars_fragment>","\n #include <uv_pars_fragment>\n uniform sampler2D heightMap;\n uniform float heightScale;\n uniform int minLayers;\n uniform int maxLayers;\n uniform int parallaxType;\n\n vec2 parallaxOffset(vec2 uv, vec3 viewDir, vec3 normal) {\n // Compute TBN in fragment shader using screen-space derivatives\n vec3 dp1 = dFdx(-vViewPosition);\n vec3 dp2 = dFdy(-vViewPosition);\n vec2 duv1 = dFdx(uv);\n vec2 duv2 = dFdy(uv);\n \n vec3 dp2perp = cross(dp2, normal);\n vec3 dp1perp = cross(normal, dp1);\n vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;\n vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;\n \n float invmax = inversesqrt(max(dot(T,T), dot(B,B)));\n mat3 tbn = mat3(T * invmax, B * invmax, normal);\n vec3 viewDirTS = -viewDir * tbn;\n\n float h = texture(heightMap, uv).r;\n return uv - (viewDirTS.xy / viewDirTS.z) * (h * heightScale);\n }\n\n vec2 parallaxOcclusion2(vec2 uv, vec3 viewDir, vec3 normal) {\n // Compute TBN in fragment shader using screen-space derivatives\n vec3 dp1 = dFdx(-vViewPosition);\n vec3 dp2 = dFdy(-vViewPosition);\n vec2 duv1 = dFdx(uv);\n vec2 duv2 = dFdy(uv);\n \n vec3 dp2perp = cross(dp2, normal);\n vec3 dp1perp = cross(normal, dp1);\n vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;\n vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;\n \n float invmax = inversesqrt(max(dot(T,T), dot(B,B)));\n mat3 tbn = mat3(T * invmax, B * invmax, normal);\n vec3 vv = -viewDir * tbn;\n\n float parallaxLimit = -length(vv.xy) / vv.z;\n parallaxLimit *= heightScale;\n\n vec2 vOffsetDir = normalize(vv.xy);\n vec2 vMaxOffset = vOffsetDir * parallaxLimit;\n\n float factor = pow(1.0 - abs(vv.z), 2.0);\n float nNumSamples = mix(float(minLayers), float(maxLayers), factor);\n float fStepSize = 1.0 / nNumSamples;\n\n float fCurrRayHeight = 1.0;\n vec2 vCurrOffset = vec2(0.0);\n vec2 vLastOffset = vec2(0.0);\n float fLastSampledHeight = 1.0;\n float fCurrSampledHeight = 1.0;\n\n for (int nCurrSample = 0; nCurrSample < 128; nCurrSample++) {\n if (float(nCurrSample) > nNumSamples) break;\n\n fCurrSampledHeight = texture2D(heightMap, uv + vCurrOffset).r;\n if (fCurrSampledHeight > fCurrRayHeight) {\n float delta1 = fCurrSampledHeight - fCurrRayHeight;\n float delta2 = (fCurrRayHeight + fStepSize) - fLastSampledHeight;\n float ratio = delta1 / (delta1 + delta2);\n vCurrOffset = ratio * vLastOffset + (1.0 - ratio) * vCurrOffset;\n break;\n } else {\n fCurrRayHeight -= fStepSize;\n vLastOffset = vCurrOffset;\n vCurrOffset += fStepSize * vMaxOffset;\n fLastSampledHeight = fCurrSampledHeight;\n }\n }\n\n return uv + vCurrOffset;\n }\n "),e.fragmentShader=e.fragmentShader.replace("#include <map_fragment>","\n vec3 pomViewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition );\n vec2 uvPOM = vMapUv ;\n if (parallaxType == 1) {\n uvPOM = parallaxOffset(vMapUv, pomViewDir, vNormal);\n } else if (parallaxType == 2) {\n uvPOM = parallaxOcclusion2(vMapUv, pomViewDir, vNormal);\n }\n\n vec4 texelColor = texture2D(map, uvPOM);\n diffuseColor *= texelColor;\n "),e.fragmentShader=e.fragmentShader.replace("#include <normal_fragment_maps>","\n #ifdef USE_NORMALMAP\n // RE-COMPUTE TBN for normal mapping consistency\n vec3 dp1 = dFdx(-vViewPosition);\n vec3 dp2 = dFdy(-vViewPosition);\n vec2 duv1 = dFdx(uvPOM);\n vec2 duv2 = dFdy(uvPOM);\n vec3 dp2perp = cross(dp2, vNormal);\n vec3 dp1perp = cross(vNormal, dp1);\n vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;\n vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;\n float invmax = inversesqrt(max(dot(T,T), dot(B,B)));\n mat3 localTBN = mat3(T * invmax, B * invmax, vNormal);\n\n vec3 mapN = texture2D(normalMap, uvPOM).xyz * 2.0 - 1.0;\n mapN.xy *= normalScale;\n normal = normalize( localTBN * mapN );\n #endif\n "),e.fragmentShader=e.fragmentShader.replace("#include <roughnessmap_fragment>","\n float roughnessFactor = roughness;\n #ifdef USE_ROUGHNESSMAP\n vec4 texelRoughness = texture2D( roughnessMap, uvPOM );\n // reads channel G, compatible with a combined OcclusionRoughnessMetallic (RGB) texture\n roughnessFactor *= texelRoughness.g;\n #endif\n "),e.fragmentShader=e.fragmentShader.replace("#include <metalnesssmap_fragment>","\n float metalnessFactor = metalness;\n #ifdef USE_METALNESSMAP\n vec4 texelMetalness = texture2D( metalnessMap, uvPOM );\n // reads channel B, compatible with a combined OcclusionRoughnessMetallic (RGB) texture\n metalnessFactor *= texelMetalness.b;\n #endif\n "),e.fragmentShader=e.fragmentShader.replace("#include <aomap_fragment>","\n #ifdef USE_AOMAP\n\n // reads channel R, compatible with a combined OcclusionRoughnessMetallic (RGB) texture\n float ambientOcclusion = ( texture2D( aoMap, uvPOM ).r - 1.0 ) * aoMapIntensity + 1.0;\n\n reflectedLight.indirectDiffuse *= ambientOcclusion;\n\n #if defined( USE_CLEARCOAT ) \n clearcoatSpecularIndirect *= ambientOcclusion;\n #endif\n\n #if defined( USE_SHEEN ) \n sheenSpecularIndirect *= ambientOcclusion;\n #endif\n\n #if defined( USE_ENVMAP ) && defined( STANDARD )\n\n float dotNV = saturate( dot( geometryNormal, geometryViewDir ) );\n\n reflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ambientOcclusion, material.roughness );\n\n #endif\n\n #endif\n "),this.userData.shader=e}clone(){const e=super.clone();return e.heightMap=this.heightMap,e.heightScale=this.heightScale,e.parallaxType=this.parallaxType,e.minLayers=this.minLayers,e.maxLayers=this.maxLayers,e}}export function applyTiling(e,a){if(null==e||null==a)return e;if(1===a.x&&1===a.y)return e;const t=e.clone();return t.repeat.copy(a),t.needsUpdate=!0,t}function P(e,a){null!=e&&(e instanceof r?e.colorSpace=a:e instanceof S&&null!=e.value&&(e.value.colorSpace=a))}/*
|
|
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{Color as i,Texture as o,Vector2 as p}from"three";import{Shader as
|
|
1
|
+
import{__decorate as t,__metadata as e}from"tslib";import{Color as i,Texture as o,Vector2 as p}from"three";import{Shader as a}from"../shader.js";import{Parameter as r}from"../parameter.js";import{attributes as l,NodeShaderMaterial as n,rgba as s,Sampler2DNode as h,UniformSampler2d as y,uniformFloat as d,uniformVec3 as m,varying as u,varyingAttributes as c,vec2 as g}from"three-shader-graph";import*as v from"three";import{createPlaceholderTexture as M}from"../../utils/three/placeholder-texture.js";const S=M();export class UnlitShader extends a{constructor(){super(...arguments),this.color=new i("#FFFFFF"),this.opacity=1,this.intensity=1,this.map=new y("map",S),this.lightMap=new y("lightMap",S),this.aoMap=new y("aoMap",S),this.alphaMap=new y("alphaMap",S),this.vertexColor=!1,this.uvScale=new p(1,1)}build(){const t=function(t,e){if(null==e||1===e.x&&1===e.y)return t;return t.multiply(g(e))}(c.uv,this.uvScale);let e=d("opacity",this.opacity),i=m("color",(new v.Vector3).setFromColor(this.color)).rgb.multiplyScalar(d("intensity",this.intensity));if(f(this.map)){const o=this.map.sample(t);i=i.multiply(o.rgb),e=e.multiply(o.a)}if(f(this.alphaMap)&&(e=e.multiply(this.alphaMap.sample(t).r)),!0===this.vertexColor&&(i=i.multiply(u(l.color.rgb))),f(this.lightMap)&&(i=i.add(this.lightMap.sample(t).rgb.multiplyScalar(d("lightMapIntensity",this.lightMapIntensity??1)))),f(this.aoMap)){const e=this.aoMap.sample(t).r,o=d("aoMapIntensity",this.aoMapIntensity??1);i=i.multiplyScalar(e.subtract(1).multiply(o).add(1))}const o=new n({color:s(i,e),opacity:e,emissive:i,envMap:this.envMap,alphaTest:this.alphaTest});return o.vertexColors=!0===this.vertexColor,o}}function f(t){return null!=t&&(!(t instanceof y)||null!=t.value&&t.value!==S)}t([r(),e("design:type",i)],UnlitShader.prototype,"color",void 0),t([r({range:[0,1]}),e("design:type",Number)],UnlitShader.prototype,"opacity",void 0),t([r({range:[0,10]}),e("design:type",Number)],UnlitShader.prototype,"intensity",void 0),t([r(),e("design:type",h)],UnlitShader.prototype,"map",void 0),t([r(),e("design:type",h)],UnlitShader.prototype,"lightMap",void 0),t([r(),e("design:type",Number)],UnlitShader.prototype,"lightMapIntensity",void 0),t([r(),e("design:type",h)],UnlitShader.prototype,"aoMap",void 0),t([r(),e("design:type",Number)],UnlitShader.prototype,"aoMapIntensity",void 0),t([r(),e("design:type",h)],UnlitShader.prototype,"alphaMap",void 0),t([r(),e("design:type",o)],UnlitShader.prototype,"envMap",void 0),t([r(),e("design:type",Boolean)],UnlitShader.prototype,"vertexColor",void 0),t([r(),e("design:type",p)],UnlitShader.prototype,"uvScale",void 0);/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Material } from "three";
|
|
2
|
-
import { NodeShaderOutput } from "./shader.js";
|
|
3
|
-
export declare abstract class DecalNodeShader {
|
|
2
|
+
import { NodeShaderOutput, Shader } from "./shader.js";
|
|
3
|
+
export declare abstract class DecalNodeShader extends Shader {
|
|
4
4
|
build(): Material;
|
|
5
5
|
abstract output(): NodeShaderOutput;
|
|
6
6
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{NodeShaderMaterial as r}from"../shader-nodes/index.js";import{decalDiscard as s}from"../shader-nodes/decal.js";export class DecalNodeShader{build(){const e=this.output();let d=s;null!=e.discard&&(d=d.or(e.discard));const a=new r({transparent:!1,...e,discard:d});return a.userData.isDecal=!0,a}}/*
|
|
1
|
+
import{NodeShaderMaterial as r}from"../shader-nodes/index.js";import{decalDiscard as s}from"../shader-nodes/decal.js";import{Shader as e}from"./shader.js";export class DecalNodeShader extends e{build(){const e=this.output();let d=s;null!=e.discard&&(d=d.or(e.discard));const a=new r({transparent:!1,...e,discard:d});return a.userData.isDecal=!0,a}}/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
package/dist/shader/shader.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Mat4Node, BooleanNode, Vec4Node, Vec3Node, FloatNode, Vec2Node } from '
|
|
|
5
5
|
import { Type } from '../utils/type.js';
|
|
6
6
|
export * from './parameter.js';
|
|
7
7
|
export declare abstract class Shader {
|
|
8
|
+
alphaTest: number;
|
|
8
9
|
abstract build(): Material;
|
|
9
10
|
}
|
|
10
11
|
export type ShaderType = Type<Shader>;
|
|
@@ -29,7 +30,7 @@ export type NodeShaderOutput = {
|
|
|
29
30
|
opacity?: FloatNode;
|
|
30
31
|
emissive?: Vec3Node;
|
|
31
32
|
};
|
|
32
|
-
export declare abstract class NodeShader {
|
|
33
|
+
export declare abstract class NodeShader extends Shader {
|
|
33
34
|
build(): Material;
|
|
34
35
|
abstract output(): NodeShaderOutput;
|
|
35
36
|
}
|
package/dist/shader/shader.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{NodeShaderMaterial as r,BooleanNode as s,attributeFloat as e}from"../shader-nodes/index.js";export*from"./parameter.js";export class Shader{}const
|
|
1
|
+
import{NodeShaderMaterial as r,BooleanNode as s,attributeFloat as e}from"../shader-nodes/index.js";export*from"./parameter.js";export class Shader{constructor(){this.alphaTest=0}}const t=e("hole").gt(.5);export class NodeShader extends Shader{build(){const e=this.output();return new r({transparent:!1,...e,...!0===e.landscape?{discard:null!=e.discard&&e.discard instanceof s?e.discard.or(t):t}:{}})}}/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -2,9 +2,9 @@ import * as THREE from 'three';
|
|
|
2
2
|
import { Material } from "three";
|
|
3
3
|
import { ConstantMat4Node, FloatNode, Vec4Node } from 'three-shader-graph';
|
|
4
4
|
import { NodeShaderMaterial } from "../shader-nodes/index.js";
|
|
5
|
-
import { NodeShaderOutput } from "./shader.js";
|
|
5
|
+
import { NodeShaderOutput, Shader } from "./shader.js";
|
|
6
6
|
export declare const identityTransform: ConstantMat4Node;
|
|
7
|
-
export declare abstract class SpriteNodeShader {
|
|
7
|
+
export declare abstract class SpriteNodeShader extends Shader {
|
|
8
8
|
build(): Material;
|
|
9
9
|
abstract output(): NodeShaderOutput;
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{__decorate as e,__metadata as t}from"tslib";import*as i from"three";import{batchingMatrix as r,ConstantMat4Node as o,float as a,glslFunction as n,ifDefApply as s,rgba as l,step as c,textureSampler2d as p,UniformFloatNode as d,uniforms as u,varying as h,varyingAttributes as S,vec3 as m,vec4 as x,Vec4Node as y}from"three-shader-graph";import{depthSampler as v,NodeShaderMaterial as f}from"../shader-nodes/index.js";import{Parameter as M}from"./shader.js";export const identityTransform=new o((new i.Matrix4).identity());const
|
|
1
|
+
import{__decorate as e,__metadata as t}from"tslib";import*as i from"three";import{batchingMatrix as r,ConstantMat4Node as o,float as a,glslFunction as n,ifDefApply as s,rgba as l,step as c,textureSampler2d as p,UniformFloatNode as d,uniforms as u,varying as h,varyingAttributes as S,vec3 as m,vec4 as x,Vec4Node as y}from"three-shader-graph";import{depthSampler as v,NodeShaderMaterial as f}from"../shader-nodes/index.js";import{Parameter as M,Shader as g}from"./shader.js";export const identityTransform=new o((new i.Matrix4).identity());const P=s("USE_BATCHING",identityTransform,e=>r);export class SpriteNodeShader extends g{build(){const e=this.output(),t=getSpritePosition(new d("rotation",0),new d("screenSpaceSize",0));return new SpriteNodeShaderMaterial({color:e.color,discard:e.discard,alphaTest:e.alphaTest,transparent:e.transparent??!0,position:t,uniforms:{color:{value:new i.Color(0)}}})}}export class SpriteShader extends SpriteNodeShader{constructor(){super(...arguments),this.color=new i.Color("#FFFFFF"),this.opacity=1,this.intensity=1,this.lockY=!1,this.screenSpaceSize=0,this.lenseFlare=!1,this.occlusionRadius=.2}output(){let e=a(this.opacity);this.alphaMap&&(e=e.multiply(p(this.alphaMap).sample(S.uv).r));let t=l(this.color,e);if(this.map&&(t=t.multiply(p(this.map).sample(S.uv))),null!=this.intensity&&1!==this.intensity&&(t=l(t.rgb.multiplyScalar(this.intensity),t.a)),this.lenseFlare){const e=a(this.occlusionRadius),i=b(w(m(0,0,0))),r=b(w(m(e.multiply(-1),0,0))),o=b(w(m(e,0,0))),n=b(w(m(0,e,0))),s=b(w(m(0,e.multiply(-1),0))),c=i.add(r).add(o).add(n).add(s).divide(5),p=h(c);return{color:l(t.rgb,t.a.multiply(p))}}return{color:t}}build(){const e=super.build();return this.lenseFlare&&(e.depthTest=!1,e.depthWrite=!1),this.lockY&&(e.defines.LOCK_Y_AXIS=""),null!=this.screenSpaceSize&&this.screenSpaceSize>0&&(e.defines.SCREEN_SPACE_SIZE=""),e.uniforms.screenSpaceSize.value=this.screenSpaceSize,e}}function w(e){let t=u.instanceMatrix.multiplyVec(x(e,1));return t=s("USE_BATCHING",t,e=>r.multiplyVec(e)),u.modelMatrix.multiplyVec(t)}function b(e){const t=u.projectionMatrix.multiplyVec(u.viewMatrix.multiplyVec(e)),i=t.xyz.divideScalar(t.w),r=i.xy.multiplyScalar(.5).addScalar(.5),o=v.sample(r).x,n=i.z.multiply(.5).add(.5),s=c(n,o.add(a(1e-4))),l=c(a(0),t.w);return s.multiply(l)}e([M(),t("design:type",i.Color)],SpriteShader.prototype,"color",void 0),e([M({range:[0,1]}),t("design:type",Number)],SpriteShader.prototype,"opacity",void 0),e([M({range:[0,10]}),t("design:type",Number)],SpriteShader.prototype,"intensity",void 0),e([M(),t("design:type",i.Texture)],SpriteShader.prototype,"map",void 0),e([M(),t("design:type",i.Texture)],SpriteShader.prototype,"alphaMap",void 0),e([M({help:"Sprite will rotate around Y axis to face camera"}),t("design:type",Boolean)],SpriteShader.prototype,"lockY",void 0),e([M({range:[0,1],help:"0 = Perspective (gets smaller with distance), 1 = Screen Space (constant size)"}),t("design:type",Number)],SpriteShader.prototype,"screenSpaceSize",void 0),e([M({help:"Make the sprite behave as a lense flare that appear in front of geometry if center is visible.",label:"Lens Flare"}),t("design:type",Boolean)],SpriteShader.prototype,"lenseFlare",void 0),e([M({range:[0,2],help:"The radius of the occlusion check. Larger radius will make the sprite fade out more gradually.",requires:{lenseFlare:!0}}),t("design:type",Number)],SpriteShader.prototype,"occlusionRadius",void 0);export function getSpritePosition(e,t=a(0)){return n(y,{cameraPosition:u.cameraPosition,viewMatrix:u.viewMatrix,modelViewMatrix:u.modelViewMatrix,rotation:e,screenSpaceSize:t,batchingMatrix:P,instanceMatrix:u.instanceMatrix},"\n mat4 finalModelMatrix = modelMatrix * batchingMatrix * instanceMatrix;\n\n // Scale from your model matrix\n vec2 scale;\n scale.x = length(vec3(finalModelMatrix[0].x, finalModelMatrix[0].y, finalModelMatrix[0].z));\n scale.y = length(vec3(finalModelMatrix[1].x, finalModelMatrix[1].y, finalModelMatrix[1].z));\n\n vec3 right, up;\n vec3 objectPos = (finalModelMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;\n\n #ifdef SCREEN_SPACE_SIZE\n float distanceToCamera = length(objectPos - cameraPosition);\n float screenScaleFactor = mix(1.0, distanceToCamera, screenSpaceSize);\n scale *= screenScaleFactor;\n #endif\n\n\n #ifdef LOCK_Y_AXIS\n vec3 cameraPos = cameraPosition;\n vec3 lookDir = normalize(vec3(cameraPos.x - objectPos.x, 0.0, cameraPos.z - objectPos.z));\n right = normalize(vec3(lookDir.z, 0.0, -lookDir.x));\n up = vec3(0.0, 1.0, 0.0);\n #else\n right = vec3(viewMatrix[0][0], viewMatrix[1][0], viewMatrix[2][0]);\n up = vec3(viewMatrix[0][1], viewMatrix[1][1], viewMatrix[2][1]);\n #endif\n\n // then your shared code:\n vec2 center = vec2(0.5, 0.5);\n vec2 alignedPosition = (position.xy - (center - vec2(0.5))) * scale;\n\n vec2 rotatedPosition;\n rotatedPosition.x = cos(rotation) * alignedPosition.x - sin(rotation) * alignedPosition.y;\n rotatedPosition.y = sin(rotation) * alignedPosition.x + cos(rotation) * alignedPosition.y;\n\n vec3 billboardPos = objectPos + right * rotatedPosition.x + up * rotatedPosition.y;\n\n // This can be used just to test outputting without sprite functionality\n // return projectionMatrix * viewMatrix * finalModelMatrix * vec4(position, 1.0);\n return projectionMatrix * viewMatrix * vec4(billboardPos, 1.0);\n ")}export class SpriteNodeShaderMaterial extends f{get color(){return this.uniforms.color.value}set color(e){this.uniforms.color.value=new i.Color(e)}get rotation(){return this.uniforms.rotation.value}set rotation(e){this.uniforms.rotation.value=e}get screenSpaceSize(){return this.uniforms.screenSpaceSize.value}set screenSpaceSize(e){this.uniforms.screenSpaceSize.value=e}get occlusionRadius(){return this.uniforms.occlusionRadius.value}set occlusionRadius(e){this.uniforms.occlusionRadius.value=e}}/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Material } from "three";
|
|
2
|
-
import { NodeShaderOutput } from "./shader.js";
|
|
3
|
-
export declare abstract class TrailNodeShader {
|
|
2
|
+
import { NodeShaderOutput, Shader } from "./shader.js";
|
|
3
|
+
export declare abstract class TrailNodeShader extends Shader {
|
|
4
4
|
build(): Material;
|
|
5
5
|
abstract output(): NodeShaderOutput;
|
|
6
6
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{Shader as r}from"./shader.js";import{NodeShaderMaterial as e}from"three-shader-graph";import{Trail as o}from"../effects/vfx/trail-renderer";export class TrailNodeShader extends r{build(){const r=this.output(),{position:t}=o.getTrailShaderNodes(!1),a=new e({position:t,color:r.color});return o.applyTrailShaderParameters(a),a}}/*
|
|
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}from"vitest";import{Bone as o,AnimationClip as r,BufferGeometry as a,Group as s,MeshBasicMaterial as i,Quaternion as g,QuaternionKeyframeTrack as c,Skeleton as d,SkinnedMesh as p,Vector3 as l,VectorKeyframeTrack as u}from"three";import{areAnimationRigsCompatible as m,bakeRetargetedAnimationClip as f,createAutoAlignedRetargetPose as b,createDefaultAnimationRetargeter as h,extractAnimationRigs as w,findGeneratedAnimationOutput as B,getAnimationRigChainBoneNames as y,isAnimationClipCompatibleWithRig as L,resolveAnimationRigBasisRotation as k,resolveAnimationClipAssetForTarget as v,validateAnimationRetargeter as R,validateAnimationRig as A}from"../gameplay/animation/retarget.js";function T(e,t=!1){const n=["Root","Hips","Spine","Chest","Neck","Head","LeftUpperArm",...t?["LeftArmTwist"]:[],"LeftHand","RightUpperArm","RightHand","LeftUpLeg","LeftLeg","LeftFoot","RightUpLeg","RightLeg","RightFoot"],o=new Map(n.map(e=>[e,x(e)]));o.get("Root").add(o.get("Hips")),o.get("Hips").add(o.get("Spine"),o.get("LeftUpLeg"),o.get("RightUpLeg")),o.get("Spine").add(o.get("Chest")),o.get("Chest").add(o.get("Neck"),o.get("LeftUpperArm"),o.get("RightUpperArm")),o.get("Neck").add(o.get("Head")),t?(o.get("LeftUpperArm").add(o.get("LeftArmTwist")),o.get("LeftArmTwist").add(o.get("LeftHand"))):o.get("LeftUpperArm").add(o.get("LeftHand")),o.get("RightUpperArm").add(o.get("RightHand")),o.get("LeftUpLeg").add(o.get("LeftLeg")),o.get("LeftLeg").add(o.get("LeftFoot")),o.get("RightUpLeg").add(o.get("RightLeg")),o.get("RightLeg").add(o.get("RightFoot"));const r=new p(new a,new i);return r.name=e,r.add(o.get("Root")),r.bind(new d([...o.values()])),r}function x(e){const t=new o;return t.name=e,t.position.y="Root"===e?0:1,t}function U(e){const t=[...T("Canonical").skeleton.bones],n=new Map(t.map((e,t)=>[e,t])),r=t.map(e=>e.parent instanceof o?n.get(e.parent)??-1:-1);for(const e of t)e.removeFromParent();const s=t.map(e=>{const t=new o;return t.name=e.name,t}),g=new p(new a,new i);g.name=e;for(let e=0;e<t.length;e++){const n=r[e];n<0?g.add(t[e]):s[n].add(t[e]),t[e].add(s[e])}return g.updateMatrixWorld(!0),g.bind(new d(s)),g}function P(e,t){let n=H(e,"LeftHand");for(const o of t){const t=x(o);n.add(t),e.skeleton.bones.push(t),n=t}e.updateMatrixWorld(!0),e.skeleton.calculateInverses()}function H(e,t){const n=e.skeleton.getBoneByName(t);if(null==n)throw new Error(`Missing test bone ${t}`);return n}function M(e,t){return{...e,previewMeshId:t}}e("animation retargeting assets",()=>{n("extracts stable hierarchy and bind-pose fingerprints",()=>{const e=w(T("Body")).rigs[0],n=w(T("Body")).rigs[0];t(e.topologyFingerprint).toBe(n.topologyFingerprint),t(e.bindPoseFingerprint).toBe(n.bindPoseFingerprint),t(e.skinnedMeshName).toBe("Body"),t(e.chains.map(e=>e.role)).toEqual(t.arrayContaining(["spine","leftArm","rightArm","leftLeg","rightLeg"]));const o=T("Body");H(o,"LeftHand").position.x+=.25;const r=w(o).rigs[0];t(r.topologyFingerprint).toBe(e.topologyFingerprint),t(r.bindPoseFingerprint).not.toBe(e.bindPoseFingerprint)}),n("extracts each distinct skeleton and reuses compatible rigs",()=>{const e=new s;e.add(T("BodyA"),T("BodyB")),t(w(e).rigs).toHaveLength(1);const n=T("BodyC");H(n,"LeftHand").position.x+=.1,e.add(n);const o=w(e).rigs;t(o).toHaveLength(2),t(m(M(o[0],"mesh-a"),M(o[0],"mesh-b"))).toBe(!0),t(m(M(o[0],"mesh-a"),M(o[1],"mesh-c"))).toBe(!1)}),n("extracts a rig from a standalone bone hierarchy",()=>{const e=new s,n=new o;n.name="Hips";const r=new o;r.name="Spine",r.position.y=1,n.add(r),e.add(n);const a=w(e);t(a.rigs).toHaveLength(1),t(a.skinnedMeshNames).toEqual(["Hips"]),t(a.rigs[0].skinnedMeshName).toBeUndefined(),t(a.rigs[0].bones.map(e=>e.name)).toEqual(["Hips","Spine"]),t(a.rigs[0].bones[1].parentIndex).toBe(0),t(a.rigs[0].bones[1].inverseBindMatrix).toHaveLength(16)}),n("canonicalizes same-named FBX bone wrappers and prefers a direct skinned hierarchy",()=>{const e=new s,n=U("WrappedLOD"),o=T("DirectLOD");e.add(n,o);const a=w(e),i=w(T("AnimationSource")).rigs[0];t(a.rigs).toHaveLength(1),t(a.skinnedMeshNames).toEqual(["DirectLOD"]),t(a.rigs[0].bones).toHaveLength(i.bones.length),t(a.rigs[0].topologyFingerprint).toBe(i.topologyFingerprint),t(a.rigs[0].bindPoseFingerprint).toBe(i.bindPoseFingerprint),t(A(M(a.rigs[0],"direct"))).toEqual([]);const g=M(a.rigs[0],"source"),d=M(i,"target");g.skinnedMeshName=void 0,g.basisPreset="yUp",d.basisPreset="yUp";const p=f({sourceRig:g,targetRig:d,retargeter:h("source-rig","target-rig",g,d),sourceObject:e,targetObject:T("AnimationTarget"),clip:new r("Wrapped source",1,[new c("Spine.quaternion",[0,1],[0,0,0,1,0,.3826834,0,.9238795])])}).tracks.find(e=>e.name.includes("[Spine]"));t(p).toBeDefined(),t(Array.from(p?.createInterpolant().evaluate(1)??[])).toEqual(t.arrayContaining([t.closeTo(0,5),t.closeTo(.3826834,5),t.closeTo(0,5),t.closeTo(.9238795,5)]));const l=U("StandaloneWrapped"),u=new s;for(;l.children.length>0;)u.add(l.children[0]);const m=w(u).rigs[0];t(m.bones).toHaveLength(i.bones.length),t(m.topologyFingerprint).toBe(i.topologyFingerprint),t(m.bindPoseFingerprint).toBe(i.bindPoseFingerprint)}),n("reports duplicate bone names and stale rig fingerprints",()=>{const e=w(T("Body")).rigs[0],n=M(e,"source-mesh"),o=M(e,"target-mesh"),r=h("source-rig","target-rig",n,o);o.bones[1].name=o.bones[0].name,o.bindPoseFingerprint="changed",t(A(o).map(e=>e.code)).toContain("duplicate-bone-names"),t(R(r,n,o).map(e=>e.code)).toContain("target-rig-changed")}),n("resolves semantic chain bones and generated output indexes",()=>{const e=M(w(T("Body")).rigs[0],"mesh");t(y(e,"leftLeg")).toEqual(["LeftUpLeg","LeftLeg","LeftFoot"]);const n=[{id:"retargeter",name:"A to B",type:"animationRetargeter",animationRetargeter:{...h("a","b",e,e),generatedOutputs:{walk:"walk-b"}}},{id:"walk-b",name:"Walk B",type:"animationclip",anim:{clip:"Walk",rigId:"b"}}];t(B(n,"walk","b")?.id).toBe("walk-b"),t(B(n,"walk","other")).toBeUndefined();const o={id:"walk",name:"Walk",type:"animationclip",anim:{clip:"Walk",rigId:"a"}};t(v(o,n,"b").id).toBe("walk-b"),t(()=>v(o,n,"missing")).toThrow(/Generate this source\/target output/);const a={...o,id:"legacy-walk",anim:{clip:"Walk"}};t(v(a,n,"b")).toBe(a),t(L(new r("Known",1,[new c(".bones[LeftHand].quaternion",[0],[0,0,0,1])]),e)).toBe(!0),t(L(new r("Other",1,[new c(".bones[Tail].quaternion",[0],[0,0,0,1])]),e)).toBe(!1)}),n("bakes target-named tracks at a minimum 60 Hz and maps root axes",()=>{const e=T("SourceBody"),n=T("TargetBody"),o=M(w(e).rigs[0],"source"),a=M(w(n).rigs[0],"target");o.basisPreset="yUp",a.basisPreset="yUp";const s=h("source-rig","target-rig",o,a);s.bakeSettings.framesPerSecond=10,s.rootSettings.translationAxisMap=[3,2,-1];const i=new r("Move",1,[new u(".bones[Root].position",[0,1],[0,0,0,2,0,4]),new c(".bones[Spine].quaternion",[0,1],[0,0,0,1,0,0,0,1])]),g=f({sourceRig:o,targetRig:a,retargeter:s,sourceObject:e,targetObject:n,clip:i});t(g.name).toBe("Move"),t(g.tracks.some(e=>e.name.includes("Spine"))).toBe(!0),t(Math.max(...g.tracks.map(e=>e.times.length))).toBeGreaterThanOrEqual(2);const d=g.tracks.find(e=>e instanceof u);t(d).toBeInstanceOf(u),t(d?.name).toContain("Root"),t(d?.values.at(-3)).toBeCloseTo(4),t(d?.values.at(-1)).toBeCloseTo(-2)}),n("detects a Z-up rig basis and converts root motion into target Y-up space",()=>{const e=T("ZUpSource");for(const t of e.skeleton.bones)t.position.set(t.position.x,0,t.position.y);e.updateMatrixWorld(!0),e.skeleton.calculateInverses();const n=T("YUpTarget"),o=M(w(e).rigs[0],"source"),a=M(w(n).rigs[0],"target"),s=new l(0,0,1).applyQuaternion(k(o));t(s.angleTo(new l(0,1,0))).toBeLessThan(1e-6);const i=h("source-rig","target-rig",o,a),c=b(o,a,i).rotationOffsets[a.retargetRootBoneId];t((new g).fromArray(c).angleTo(new g)).toBeLessThan(1e-6);const d=f({sourceRig:o,targetRig:a,retargeter:i,sourceObject:e,targetObject:n,clip:new r("Vertical",1,[new u(".bones[Root].position",[0,1],[0,0,0,0,0,1])])}).tracks.find(e=>e instanceof u);t(d?.values.at(-2)).toBeCloseTo(1),t(d?.values.at(-1)).toBeCloseTo(0)}),n("combines source pelvis height with root motion when the target pelvis is its root",()=>{const e=T("SourceBody"),n=T("TargetBody"),o=M(w(e).rigs[0],"source"),a=M(w(n).rigs[0],"target");o.basisPreset="yUp",a.basisPreset="yUp",a.retargetRootBoneId=a.pelvisBoneId;const s=h("source-rig","target-rig",o,a),i=f({sourceRig:o,targetRig:a,retargeter:s,sourceObject:e,targetObject:n,clip:new r("Crouch forward",1,[new u(".bones[Root].position",[0,1],[0,0,0,2,0,0]),new u(".bones[Hips].position",[0,1],[0,1,0,0,.6,0])])}).tracks.find(e=>e instanceof u&&e.name.includes("[Hips]"));t(i?.values.at(-3)).toBeCloseTo(2),t(i?.values.at(-2)).toBeCloseTo(.6),t(i?.values.at(-1)).toBeCloseTo(0)}),n("maps semantic chain position when target limb bone counts differ",()=>{const e=T("SourceBody"),n=T("TargetBody",!0),o=M(w(e).rigs[0],"source"),a=M(w(n).rigs[0],"target");o.basisPreset="yUp",a.basisPreset="yUp";const s=h("source-rig","target-rig",o,a),i=new r("Wave",1,[new c(".bones[LeftUpperArm].quaternion",[0,1],[0,0,0,1,0,.3826834,0,.9238795])]),g=f({sourceRig:o,targetRig:a,retargeter:s,sourceObject:e,targetObject:n,clip:i});t(g.tracks.some(e=>e.name.includes("LeftArmTwist"))).toBe(!0),t(g.tracks.every(e=>!1===e.name.includes("Target"))).toBe(!0)}),n("transfers bind-relative rotation across different root coordinate bases",()=>{const e=T("SourceBody"),n=T("TargetBody");H(n,"Root").quaternion.setFromAxisAngle(new l(1,0,0),Math.PI/2),e.updateMatrixWorld(!0),n.updateMatrixWorld(!0),n.skeleton.calculateInverses();const o=M(w(e).rigs[0],"source"),a=M(w(n).rigs[0],"target");o.basisPreset="yUp",a.basisPreset="yUp";const s=h("source-rig","target-rig",o,a),i=(new g).setFromAxisAngle(new l(0,1,0),Math.PI/3),d=new r("Turn",1,[new c(".bones[Spine].quaternion",[0,1],[0,0,0,1,...i.toArray()])]),p=H(e,"Spine"),u=H(n,"Spine"),m=p.getWorldQuaternion(new g),b=u.getWorldQuaternion(new g);p.quaternion.copy(i),e.updateMatrixWorld(!0);const B=p.getWorldQuaternion(new g).multiply(m.clone().invert()).multiply(b),y=f({sourceRig:o,targetRig:a,retargeter:s,sourceObject:e,targetObject:n,clip:d}).tracks.find(e=>e.name.includes("[Spine]"));t(y).toBeInstanceOf(c),u.quaternion.fromArray(y.values,y.values.length-4),n.updateMatrixWorld(!0),t(u.getWorldQuaternion(new g).angleTo(B)).toBeLessThan(1e-5)}),n("transfers root displacement through the target root parent basis",()=>{const e=T("SourceBody"),n=T("TargetBody");n.quaternion.setFromAxisAngle(new l(1,0,0),-Math.PI/2),H(n,"Root").quaternion.setFromAxisAngle(new l(1,0,0),Math.PI/2),e.updateMatrixWorld(!0),n.updateMatrixWorld(!0),n.skeleton.calculateInverses();const o=M(w(e).rigs[0],"source"),a=M(w(n).rigs[0],"target");o.basisPreset="yUp",a.basisPreset="yUp";const s=h("source-rig","target-rig",o,a),i=new r("Move forward",1,[new u(".bones[Root].position",[0,1],[0,0,0,0,0,2])]),g=f({sourceRig:o,targetRig:a,retargeter:s,sourceObject:e,targetObject:n,clip:i}).tracks.find(e=>e instanceof u);t(g?.values.at(-3)).toBeCloseTo(0),t(g?.values.at(-2)).toBeCloseTo(-2),t(g?.values.at(-1)).toBeCloseTo(0)}),n("maps finger segments with different humanoid naming conventions",()=>{const e=T("SourceBody"),n=T("TargetBody");P(e,["mixamorigLeftHandIndex1","mixamorigLeftHandIndex2"]),P(n,["index1l","index2l"]);const o=M(w(e).rigs[0],"source"),a=M(w(n).rigs[0],"target"),s=h("source-rig","target-rig",o,a),i=(new g).setFromAxisAngle(new l(1,0,0),Math.PI/3),d=new r("Grip",1,[new c(".bones[mixamorigLeftHandIndex1].quaternion",[0,1],[0,0,0,1,...i.toArray()])]),p=f({sourceRig:o,targetRig:a,retargeter:s,sourceObject:e,targetObject:n,clip:d});t(p.tracks.some(e=>e.name.includes("[index1l]"))).toBe(!0),t(p.tracks.some(e=>e.name.includes("[index2l]"))).toBe(!0)}),n("creates a target pose that aligns mapped limb directions",()=>{const e=T("SourceBody"),n=T("TargetBody");H(n,"LeftUpperArm").quaternion.setFromAxisAngle(new l(0,0,1),Math.PI/4),n.updateMatrixWorld(!0),n.skeleton.calculateInverses();const o=M(w(e).rigs[0],"source"),r=M(w(n).rigs[0],"target"),a=h("source-rig","target-rig",o,r),s=b(o,r,a),i=H(n,"LeftUpperArm");i.quaternion.fromArray(r.bones.find(e=>"LeftUpperArm"===e.name).quaternion).multiply((new g).fromArray(s.rotationOffsets[r.bones.find(e=>"LeftUpperArm"===e.name).id])),n.updateMatrixWorld(!0);const c=H(n,"LeftHand").getWorldPosition(new l).sub(i.getWorldPosition(new l)).normalize(),d=H(e,"LeftHand").getWorldPosition(new l).sub(H(e,"LeftUpperArm").getWorldPosition(new l)).normalize();t(c.angleTo(d)).toBeLessThan(1e-5)})});/*
|
|
1
|
+
import{describe as e,expect as t,it as o}from"vitest";import{Bone as n,AnimationClip as r,BufferGeometry as s,Group as a,MeshBasicMaterial as i,Quaternion as g,QuaternionKeyframeTrack as c,Skeleton as d,SkinnedMesh as p,Vector3 as l,VectorKeyframeTrack as u}from"three";import{areAnimationRigsCompatible as m,bakeRetargetedAnimationClip as b,createAutoAlignedRetargetPose as f,createDefaultAnimationRetargeter as w,extractAnimationRigs as h,findGeneratedAnimationOutput as B,getAnimationRigChainBoneNames as y,isAnimationClipCompatibleWithRig as L,resolveAnimationRigBasisRotation as k,resolveAnimationClipAssetForTarget as A,validateAnimationRetargeter as R,validateAnimationRig as v}from"../gameplay/animation/retarget.js";function T(e,t=!1){const o=["Root","Hips","Spine","Chest","Neck","Head","LeftUpperArm",...t?["LeftArmTwist"]:[],"LeftHand","RightUpperArm","RightHand","LeftUpLeg","LeftLeg","LeftFoot","RightUpLeg","RightLeg","RightFoot"],n=new Map(o.map(e=>[e,x(e)]));n.get("Root").add(n.get("Hips")),n.get("Hips").add(n.get("Spine"),n.get("LeftUpLeg"),n.get("RightUpLeg")),n.get("Spine").add(n.get("Chest")),n.get("Chest").add(n.get("Neck"),n.get("LeftUpperArm"),n.get("RightUpperArm")),n.get("Neck").add(n.get("Head")),t?(n.get("LeftUpperArm").add(n.get("LeftArmTwist")),n.get("LeftArmTwist").add(n.get("LeftHand"))):n.get("LeftUpperArm").add(n.get("LeftHand")),n.get("RightUpperArm").add(n.get("RightHand")),n.get("LeftUpLeg").add(n.get("LeftLeg")),n.get("LeftLeg").add(n.get("LeftFoot")),n.get("RightUpLeg").add(n.get("RightLeg")),n.get("RightLeg").add(n.get("RightFoot"));const r=new p(new s,new i);return r.name=e,r.add(n.get("Root")),r.bind(new d([...n.values()])),r}function x(e){const t=new n;return t.name=e,t.position.y="Root"===e?0:1,t}function P(e,t=new g){const o=F(T(`${e}Skeleton`),"Root");o.removeFromParent();const n=new a;n.name=e,n.quaternion.copy(t),n.add(o);const r=new a;return r.add(n),r.updateMatrixWorld(!0),r}function U(e){const t=[...T("Canonical").skeleton.bones],o=new Map(t.map((e,t)=>[e,t])),r=t.map(e=>e.parent instanceof n?o.get(e.parent)??-1:-1);for(const e of t)e.removeFromParent();const a=t.map(e=>{const t=new n;return t.name=e.name,t}),g=new p(new s,new i);g.name=e;for(let e=0;e<t.length;e++){const o=r[e];o<0?g.add(t[e]):a[o].add(t[e]),t[e].add(a[e])}return g.updateMatrixWorld(!0),g.bind(new d(a)),g}function M(e,t){let o=F(e,"LeftHand");for(const n of t){const t=x(n);o.add(t),e.skeleton.bones.push(t),o=t}e.updateMatrixWorld(!0),e.skeleton.calculateInverses()}function F(e,t){const o=e.skeleton.getBoneByName(t);if(null==o)throw new Error(`Missing test bone ${t}`);return o}function S(e,t){return{...e,previewMeshId:t}}e("animation retargeting assets",()=>{o("extracts stable hierarchy and bind-pose fingerprints",()=>{const e=h(T("Body")).rigs[0],o=h(T("Body")).rigs[0];t(e.topologyFingerprint).toBe(o.topologyFingerprint),t(e.bindPoseFingerprint).toBe(o.bindPoseFingerprint),t(e.skinnedMeshName).toBe("Body"),t(e.chains.map(e=>e.role)).toEqual(t.arrayContaining(["spine","leftArm","rightArm","leftLeg","rightLeg"]));const n=T("Body");F(n,"LeftHand").position.x+=.25;const r=h(n).rigs[0];t(r.topologyFingerprint).toBe(e.topologyFingerprint),t(r.bindPoseFingerprint).not.toBe(e.bindPoseFingerprint)}),o("extracts each distinct skeleton and reuses compatible rigs",()=>{const e=new a;e.add(T("BodyA"),T("BodyB")),t(h(e).rigs).toHaveLength(1);const o=T("BodyC");F(o,"LeftHand").position.x+=.1,e.add(o);const n=h(e).rigs;t(n).toHaveLength(2),t(m(S(n[0],"mesh-a"),S(n[0],"mesh-b"))).toBe(!0),t(m(S(n[0],"mesh-a"),S(n[1],"mesh-c"))).toBe(!1)}),o("extracts a rig from a standalone bone hierarchy",()=>{const e=new a,o=new n;o.name="Hips";const r=new n;r.name="Spine",r.position.y=1,o.add(r),e.add(o);const s=h(e);t(s.rigs).toHaveLength(1),t(s.skinnedMeshNames).toEqual(["Hips"]),t(s.rigs[0].skinnedMeshName).toBeUndefined(),t(s.rigs[0].bones.map(e=>e.name)).toEqual(["Hips","Spine"]),t(s.rigs[0].bones[1].parentIndex).toBe(0),t(s.rigs[0].bones[1].inverseBindMatrix).toHaveLength(16)}),o("canonicalizes same-named FBX bone wrappers and prefers a direct skinned hierarchy",()=>{const e=new a,o=U("WrappedLOD"),n=T("DirectLOD");e.add(o,n);const s=h(e),i=h(T("AnimationSource")).rigs[0];t(s.rigs).toHaveLength(1),t(s.skinnedMeshNames).toEqual(["DirectLOD"]),t(s.rigs[0].bones).toHaveLength(i.bones.length),t(s.rigs[0].topologyFingerprint).toBe(i.topologyFingerprint),t(s.rigs[0].bindPoseFingerprint).toBe(i.bindPoseFingerprint),t(v(S(s.rigs[0],"direct"))).toEqual([]);const g=S(s.rigs[0],"source"),d=S(i,"target");g.skinnedMeshName=void 0,g.basisPreset="yUp",d.basisPreset="yUp";const p=b({sourceRig:g,targetRig:d,retargeter:w("source-rig","target-rig",g,d),sourceObject:e,targetObject:T("AnimationTarget"),clip:new r("Wrapped source",1,[new c("Spine.quaternion",[0,1],[0,0,0,1,0,.3826834,0,.9238795])])}).tracks.find(e=>e.name.includes("[Spine]"));t(p).toBeDefined(),t(Array.from(p?.createInterpolant().evaluate(1)??[])).toEqual(t.arrayContaining([t.closeTo(0,5),t.closeTo(.3826834,5),t.closeTo(0,5),t.closeTo(.9238795,5)]));const l=U("StandaloneWrapped"),u=new a;for(;l.children.length>0;)u.add(l.children[0]);const m=h(u).rigs[0];t(m.bones).toHaveLength(i.bones.length),t(m.topologyFingerprint).toBe(i.topologyFingerprint),t(m.bindPoseFingerprint).toBe(i.bindPoseFingerprint)}),o("reports duplicate bone names and stale rig fingerprints",()=>{const e=h(T("Body")).rigs[0],o=S(e,"source-mesh"),n=S(e,"target-mesh"),r=w("source-rig","target-rig",o,n);n.bones[1].name=n.bones[0].name,n.bindPoseFingerprint="changed",t(v(n).map(e=>e.code)).toContain("duplicate-bone-names"),t(R(r,o,n).map(e=>e.code)).toContain("target-rig-changed")}),o("resolves semantic chain bones and generated output indexes",()=>{const e=S(h(T("Body")).rigs[0],"mesh");t(y(e,"leftLeg")).toEqual(["LeftUpLeg","LeftLeg","LeftFoot"]);const o=[{id:"retargeter",name:"A to B",type:"animationRetargeter",animationRetargeter:{...w("a","b",e,e),generatedOutputs:{walk:"walk-b"}}},{id:"walk-b",name:"Walk B",type:"animationclip",anim:{clip:"Walk",rigId:"b"}}];t(B(o,"walk","b")?.id).toBe("walk-b"),t(B(o,"walk","other")).toBeUndefined();const n={id:"walk",name:"Walk",type:"animationclip",anim:{clip:"Walk",rigId:"a"}};t(A(n,o,"b").id).toBe("walk-b"),t(()=>A(n,o,"missing")).toThrow(/Generate this source\/target output/);const s={...n,id:"legacy-walk",anim:{clip:"Walk"}};t(A(s,o,"b")).toBe(s),t(L(new r("Known",1,[new c(".bones[LeftHand].quaternion",[0],[0,0,0,1])]),e)).toBe(!0),t(L(new r("Other",1,[new c(".bones[Tail].quaternion",[0],[0,0,0,1])]),e)).toBe(!1)}),o("bakes target-named tracks at a minimum 60 Hz and maps root axes",()=>{const e=T("SourceBody"),o=T("TargetBody"),n=S(h(e).rigs[0],"source"),s=S(h(o).rigs[0],"target");n.basisPreset="yUp",s.basisPreset="yUp";const a=w("source-rig","target-rig",n,s);a.bakeSettings.framesPerSecond=10,a.rootSettings.translationAxisMap=[3,2,-1];const i=new r("Move",1,[new u(".bones[Root].position",[0,1],[0,0,0,2,0,4]),new c(".bones[Spine].quaternion",[0,1],[0,0,0,1,0,0,0,1])]),g=b({sourceRig:n,targetRig:s,retargeter:a,sourceObject:e,targetObject:o,clip:i});t(g.name).toBe("Move"),t(g.tracks.some(e=>e.name.includes("Spine"))).toBe(!0),t(Math.max(...g.tracks.map(e=>e.times.length))).toBeGreaterThanOrEqual(2);const d=g.tracks.find(e=>e instanceof u);t(d).toBeInstanceOf(u),t(d?.name).toContain("Root"),t(d?.values.at(-3)).toBeCloseTo(4),t(d?.values.at(-1)).toBeCloseTo(-2)}),o("bakes from a standalone source skeleton without a skinned mesh",()=>{const e=P("AnimationSource"),o=T("TargetBody"),n=S(h(e).rigs[0],"source"),s=S(h(o).rigs[0],"target");n.basisPreset="yUp",s.basisPreset="yUp";const a=w("source-rig","target-rig",n,s),i=(new g).setFromAxisAngle(new l(0,1,0),Math.PI/3),d=b({sourceRig:n,targetRig:s,retargeter:a,sourceObject:e,targetObject:o,clip:new r("Animation only",1,[new c(".bones[Spine].quaternion",[0,1],[0,0,0,1,...i.toArray()])])});t(d.tracks.some(e=>e.name.includes("[Spine]"))).toBe(!0)}),o("preserves the imported parent basis of a standalone source skeleton",()=>{const e=P("RotatedAnimationSource",(new g).setFromAxisAngle(new l(0,1,0),Math.PI/2)),o=T("TargetBody"),n=S(h(e).rigs[0],"source"),s=S(h(o).rigs[0],"target");n.basisPreset="yUp",s.basisPreset="yUp";const a=w("source-rig","target-rig",n,s),i=b({sourceRig:n,targetRig:s,retargeter:a,sourceObject:e,targetObject:o,clip:new r("Move forward",1,[new u(".bones[Root].position",[0,1],[0,0,0,0,0,2])])}).tracks.find(e=>e instanceof u);t(i?.values.at(-3)).toBeCloseTo(2),t(i?.values.at(-2)).toBeCloseTo(0),t(i?.values.at(-1)).toBeCloseTo(0)}),o("detects a Z-up rig basis and converts root motion into target Y-up space",()=>{const e=T("ZUpSource");for(const t of e.skeleton.bones)t.position.set(t.position.x,0,t.position.y);e.updateMatrixWorld(!0),e.skeleton.calculateInverses();const o=T("YUpTarget"),n=S(h(e).rigs[0],"source"),s=S(h(o).rigs[0],"target"),a=new l(0,0,1).applyQuaternion(k(n));t(a.angleTo(new l(0,1,0))).toBeLessThan(1e-6);const i=w("source-rig","target-rig",n,s),c=f(n,s,i).rotationOffsets[s.retargetRootBoneId];t((new g).fromArray(c).angleTo(new g)).toBeLessThan(1e-6);const d=b({sourceRig:n,targetRig:s,retargeter:i,sourceObject:e,targetObject:o,clip:new r("Vertical",1,[new u(".bones[Root].position",[0,1],[0,0,0,0,0,1])])}).tracks.find(e=>e instanceof u);t(d?.values.at(-2)).toBeCloseTo(1),t(d?.values.at(-1)).toBeCloseTo(0)}),o("combines source pelvis height with root motion when the target pelvis is its root",()=>{const e=T("SourceBody"),o=T("TargetBody"),n=S(h(e).rigs[0],"source"),s=S(h(o).rigs[0],"target");n.basisPreset="yUp",s.basisPreset="yUp",s.retargetRootBoneId=s.pelvisBoneId;const a=w("source-rig","target-rig",n,s),i=b({sourceRig:n,targetRig:s,retargeter:a,sourceObject:e,targetObject:o,clip:new r("Crouch forward",1,[new u(".bones[Root].position",[0,1],[0,0,0,2,0,0]),new u(".bones[Hips].position",[0,1],[0,1,0,0,.6,0])])}).tracks.find(e=>e instanceof u&&e.name.includes("[Hips]"));t(i?.values.at(-3)).toBeCloseTo(2),t(i?.values.at(-2)).toBeCloseTo(.6),t(i?.values.at(-1)).toBeCloseTo(0)}),o("maps semantic chain position when target limb bone counts differ",()=>{const e=T("SourceBody"),o=T("TargetBody",!0),n=S(h(e).rigs[0],"source"),s=S(h(o).rigs[0],"target");n.basisPreset="yUp",s.basisPreset="yUp";const a=w("source-rig","target-rig",n,s),i=new r("Wave",1,[new c(".bones[LeftUpperArm].quaternion",[0,1],[0,0,0,1,0,.3826834,0,.9238795])]),g=b({sourceRig:n,targetRig:s,retargeter:a,sourceObject:e,targetObject:o,clip:i});t(g.tracks.some(e=>e.name.includes("LeftArmTwist"))).toBe(!0),t(g.tracks.every(e=>!1===e.name.includes("Target"))).toBe(!0)}),o("transfers bind-relative rotation across different root coordinate bases",()=>{const e=T("SourceBody"),o=T("TargetBody");F(o,"Root").quaternion.setFromAxisAngle(new l(1,0,0),Math.PI/2),e.updateMatrixWorld(!0),o.updateMatrixWorld(!0),o.skeleton.calculateInverses();const n=S(h(e).rigs[0],"source"),s=S(h(o).rigs[0],"target");n.basisPreset="yUp",s.basisPreset="yUp";const a=w("source-rig","target-rig",n,s),i=(new g).setFromAxisAngle(new l(0,1,0),Math.PI/3),d=new r("Turn",1,[new c(".bones[Spine].quaternion",[0,1],[0,0,0,1,...i.toArray()])]),p=F(e,"Spine"),u=F(o,"Spine"),m=p.getWorldQuaternion(new g),f=u.getWorldQuaternion(new g);p.quaternion.copy(i),e.updateMatrixWorld(!0);const B=p.getWorldQuaternion(new g).multiply(m.clone().invert()).multiply(f),y=b({sourceRig:n,targetRig:s,retargeter:a,sourceObject:e,targetObject:o,clip:d}).tracks.find(e=>e.name.includes("[Spine]"));t(y).toBeInstanceOf(c),u.quaternion.fromArray(y.values,y.values.length-4),o.updateMatrixWorld(!0),t(u.getWorldQuaternion(new g).angleTo(B)).toBeLessThan(1e-5)}),o("transfers root displacement through the target root parent basis",()=>{const e=T("SourceBody"),o=T("TargetBody");o.quaternion.setFromAxisAngle(new l(1,0,0),-Math.PI/2),F(o,"Root").quaternion.setFromAxisAngle(new l(1,0,0),Math.PI/2),e.updateMatrixWorld(!0),o.updateMatrixWorld(!0),o.skeleton.calculateInverses();const n=S(h(e).rigs[0],"source"),s=S(h(o).rigs[0],"target");n.basisPreset="yUp",s.basisPreset="yUp";const a=w("source-rig","target-rig",n,s),i=new r("Move forward",1,[new u(".bones[Root].position",[0,1],[0,0,0,0,0,2])]),g=b({sourceRig:n,targetRig:s,retargeter:a,sourceObject:e,targetObject:o,clip:i}).tracks.find(e=>e instanceof u);t(g?.values.at(-3)).toBeCloseTo(0),t(g?.values.at(-2)).toBeCloseTo(-2),t(g?.values.at(-1)).toBeCloseTo(0)}),o("maps finger segments with different humanoid naming conventions",()=>{const e=T("SourceBody"),o=T("TargetBody");M(e,["mixamorigLeftHandIndex1","mixamorigLeftHandIndex2"]),M(o,["index1l","index2l"]);const n=S(h(e).rigs[0],"source"),s=S(h(o).rigs[0],"target"),a=w("source-rig","target-rig",n,s),i=(new g).setFromAxisAngle(new l(1,0,0),Math.PI/3),d=new r("Grip",1,[new c(".bones[mixamorigLeftHandIndex1].quaternion",[0,1],[0,0,0,1,...i.toArray()])]),p=b({sourceRig:n,targetRig:s,retargeter:a,sourceObject:e,targetObject:o,clip:d});t(p.tracks.some(e=>e.name.includes("[index1l]"))).toBe(!0),t(p.tracks.some(e=>e.name.includes("[index2l]"))).toBe(!0)}),o("creates a target pose that aligns mapped limb directions",()=>{const e=T("SourceBody"),o=T("TargetBody");F(o,"LeftUpperArm").quaternion.setFromAxisAngle(new l(0,0,1),Math.PI/4),o.updateMatrixWorld(!0),o.skeleton.calculateInverses();const n=S(h(e).rigs[0],"source"),r=S(h(o).rigs[0],"target"),s=w("source-rig","target-rig",n,r),a=f(n,r,s),i=F(o,"LeftUpperArm");i.quaternion.fromArray(r.bones.find(e=>"LeftUpperArm"===e.name).quaternion).multiply((new g).fromArray(a.rotationOffsets[r.bones.find(e=>"LeftUpperArm"===e.name).id])),o.updateMatrixWorld(!0);const c=F(o,"LeftHand").getWorldPosition(new l).sub(i.getWorldPosition(new l)).normalize(),d=F(e,"LeftHand").getWorldPosition(new l).sub(F(e,"LeftUpperArm").getWorldPosition(new l)).normalize();t(c.angleTo(d)).toBeLessThan(1e-5)}),o("auto-aligns using a root bone external parent from the bind pose",()=>{const e=T("SourceBody"),o=T("TargetBody");o.quaternion.setFromAxisAngle(new l(1,0,0),-Math.PI/2),F(o,"Root").quaternion.setFromAxisAngle(new l(1,0,0),Math.PI/2),o.updateMatrixWorld(!0),o.skeleton.calculateInverses();const n=S(h(e).rigs[0],"source"),r=S(h(o).rigs[0],"target");n.basisPreset="yUp",r.basisPreset="yUp";const s=w("source-rig","target-rig",n,r),a=f(n,r,s),i=r.bones.find(e=>"Spine"===e.name).id,c=(new g).fromArray(a.rotationOffsets[i]??[0,0,0,1]);t(c.angleTo(new g)).toBeLessThan(1e-5)})});/*
|
|
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,vi as a}from"vitest";import{Texture as o}from"three";import{AssetResourceLoader as n}from"../scene/asset-resource-loader.js";function s(e,t){return{id:e,name:e,type:"texture",fileKey:`${e}.ktx2`,fileFormat:"ktx2",texture:{textureArrayFileKey:"shared-array.ktx2",textureArrayLayer:t}}}a.mock("../scene/materializer.js",()=>({applyMaterial:a.fn(),getConvertedFbxToGlbAssignedMaterialTextureFlipY:a.fn()})),a.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&a.fn()),e[t]),set:(e,t,r)=>(e[t]=r,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),e("AssetResourceLoader texture arrays",()=>{r("deduplicates concurrent loads by texture array file key",async()=>{const e=new n;let r;const l=new Promise(e=>{r=e}),i=a.fn(()=>l);e._getTextureLoader=()=>({loadAsync:i}),e.getUri=e=>e;const u=e.getTextureArray(s("texture-a",0)),d=e.getTextureArray(s("texture-b",1));t(i).toHaveBeenCalledTimes(1);const c=new o;r(c);const[y,f]=await Promise.all([u,d]);t(y.texture).toBe(c),t(f.texture).toBe(c),t(y.layerIndex).toBe(0),t(f.layerIndex).toBe(1)}),r("allows a failed texture array load to be retried",async()=>{const e=new n,r=new o,l=a.fn().mockRejectedValueOnce(new Error("load failed")).mockResolvedValueOnce(r);e._getTextureLoader=()=>({loadAsync:l}),e.getUri=e=>e;const i=s("texture-a",0);await t(e.getTextureArray(i)).rejects.toThrow("load failed"),await t(e.getTextureArray(i)).resolves.toEqual({texture:r,layerIndex:0}),t(l).toHaveBeenCalledTimes(2)})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{expect as e,test as t}from"vitest";import{AnimationClip as i,AnimationMixer as o,LoopOnce as n,Object3D as a,VectorKeyframeTrack as s}from"three";import{canCrossFadeFromAnimationAction as p}from"../gameplay/animation/animation-action.js";t("crossfades away from a finished clamped animation",()=>{const t=new a,d=new o(t),c=new i("slide",.5,[new s(".position",[0,.5],[0,0,0,0,0,1])]),r=new i("idle",1,[new s(".position",[0,1],[0,0,0,0,0,0])]),m=d.clipAction(c);m.setLoop(n,1),m.clampWhenFinished=!0,m.play(),d.update(c.duration+.1),e(m.isRunning()).toBe(!1),e(m.isScheduled()).toBe(!0),e(m.enabled).toBe(!0),e(p(m)).toBe(!0);const l=d.clipAction(r);l.reset().play(),m.crossFadeTo(l,.2,!0),d.update(.1),e(m.getEffectiveWeight()).toBeLessThan(1),e(l.getEffectiveWeight()).toBeGreaterThan(0),d.update(.11),e(m.enabled).toBe(!1)});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as t,test as o}from"vitest";import{Vector3 as n}from"three";import{setSlideJumpVelocity as s,shouldContinueSlide as a,shouldStartSlide as r,updateSlideVelocity as c,updateSlideVelocityFromDisplacement as l}from"../gameplay/actors/builtin/components/character/character-slide.js";const h=new n(0,1,0),i=new n;e("character slide movement",()=>{o("decelerates at a constant rate on flat ground",()=>{const e=new n(0,0,10),o=c(e,e,h,i,-9.81,1,3,1,2);t(o).toBeCloseTo(7),t(e.z).toBeCloseTo(7)}),o("uses the surface angle to accelerate downhill and slow uphill",()=>{const e=Math.PI/6,o=new n(0,Math.cos(e),Math.sin(e)),s=new n(0,Math.cos(e),-Math.sin(e)),a=new n(0,0,10),r=new n(0,0,10),l=new n(0,0,10),w=c(a,a,o,i,-9.81,.5,3,1,0),d=c(l,l,h,i,-9.81,.5,3,1,0),B=c(r,r,s,i,-9.81,.5,3,1,0);t(w).toBeGreaterThan(d),t(B).toBeLessThan(d)}),o("steers gradually without changing the computed speed",()=>{const e=new n(0,0,10),o=new n(1,0,0),s=c(e,e,h,o,0,.1,0,1,2);t(s).toBeCloseTo(10),t(e.length()).toBeCloseTo(10),t(e.x).toBeGreaterThan(0),t(e.z).toBeGreaterThan(0)}),o("turns predictably when steering opposite the slide direction",()=>{const e=new n(0,0,10);c(e,e,h,new n(0,0,-1),0,.1,0,1,2),t(e.length()).toBeCloseTo(10),t(e.z).toBeGreaterThan(0),t(Math.abs(e.x)).toBeGreaterThan(0)}),o("derives speed from collision-corrected displacement",()=>{const e=new n,o=l(e,new n(0,0,.08),new n,h,.016),s=l(e,new n,new n,h,.016);t(o).toBeCloseTo(5),t(s).toBe(0)}),o("requires a start edge and uses exit-speed hysteresis",()=>{t(r(!0,!0,!0,!0,!1,8,8)).toBe(!0),t(r(!0,!1,!0,!0,!1,12,8)).toBe(!1),t(r(!0,!0,!0,!0,!0,12,8)).toBe(!1),t(a(!0,!0,!0,!0,!1,4,4)).toBe(!0),t(a(!0,!0,!0,!0,!1,3.99,4)).toBe(!1),t(a(!0,!1,!0,!0,!1,12,4)).toBe(!1)}),o("preserves horizontal momentum when jumping",()=>{const e=s(new n,new n(3,-2,5),7);t(e.toArray()).toEqual([3,7,5])})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as t,it as s,vi as a}from"vitest";import{NavMeshWorkerPool as n}from"../gameplay/ai/dynamic-tiled-navmesh.js";e("NavMeshWorkerPool",()=>{s("creates workers lazily and caps concurrent work",()=>{const e=[],s=new n(()=>{const t=new o;return e.push(t),t}),l={},r=a.fn();t(e).toHaveLength(0);for(let e=0;e<8;e++)s.schedule(l,{tileX:e},void 0,r,24);t(e).toHaveLength(4),t(e.flatMap(e=>e.messages)).toHaveLength(4),e[0].complete(0),t(e).toHaveLength(4),t(e.flatMap(e=>e.messages)).toHaveLength(5),t(r).toHaveBeenCalledTimes(1)}),s("does not deliver queued or active results after an owner is cancelled",()=>{const e=[],s=new n(()=>{const t=new o;return e.push(t),t}),l={},r=a.fn();s.schedule(l,{tileX:0},void 0,r,1),s.schedule(l,{tileX:1},void 0,r,1),s.cancel(l),e[0].complete(0),t(r).not.toHaveBeenCalled(),t(e[0].messages).toHaveLength(1)})});class o{constructor(){this.onmessage=null,this.onerror=null,this.messages=[],this.terminate=a.fn()}postMessage(e){this.messages.push(e)}complete(e){this.onmessage?.({data:{tileX:e,tileY:0,navMeshData:new Uint8Array}})}}/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
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,
|
|
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",()=>{const e=c.SNAPSHOT_FLAG_CROUCHING|c.SNAPSHOT_FLAG_CROUCH_SUPPRESSED,t=i({sequence:42,x:10,y:2,z:-3,velocityX:1,yaw:1.25,mode:n.sliding,flags:e,horizontalSpeed:7}),a=c.decodeNetCharacterOwnerAck(c.encodeNetCharacterOwnerAck(42)),s=c.decodeNetCharacterOwnerCorrection(c.encodeNetCharacterOwnerCorrection(t)),r=c.decodeNetCharacterSimulatedState(c.encodeNetCharacterSimulatedState(t));o(a.ok&&a.value.sequence).toBe(42),o(s.ok&&s.value.snapshot.sequence).toBe(42),o(s.ok&&s.value.snapshot.flags).toBe(e),o(r.ok&&r.value.mode).toBe(n.sliding),o(r.ok&&r.value.flags).toBe(e),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{__decorate as e,__metadata as a}from"tslib";import{describe as t,expect as r,it as n,vi as o}from"vitest";import{Scene as i}from"three";import{Subject as s}from"rxjs";o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,a)=>(a in e||(e[a]=("string"!=typeof a||!a.startsWith("is"))&&o.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{}})}),o.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),o.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),o.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),o.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:o.fn()}));import{BaseActor as c}from"../gameplay/actors/actor.js";import{ActorComponent as p}from"../gameplay/actors/component.js";import{Parameter as m}from"../shader/parameter.js";import{SceneMaterializer as l}from"../scene/materializer.js";import{SerializedParamType as
|
|
1
|
+
import{__decorate as e,__metadata as a}from"tslib";import{describe as t,expect as r,it as n,vi as o}from"vitest";import{Scene as i}from"three";import{Subject as s}from"rxjs";o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,a)=>(a in e||(e[a]=("string"!=typeof a||!a.startsWith("is"))&&o.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{}})}),o.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),o.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),o.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),o.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:o.fn()}));import{BaseActor as c}from"../gameplay/actors/actor.js";import{ActorComponent as p}from"../gameplay/actors/component.js";import{Parameter as m}from"../shader/parameter.js";import{SceneMaterializer as l}from"../scene/materializer.js";import{SerializedParamType as u}from"../scene/model.js";class f extends p{constructor(){super(...arguments),this.animationPreset="mixamo"}}e([m(),a("design:type",Object)],f.prototype,"animationPreset",void 0);class d extends c{constructor(){super(...arguments),this.animation=new f,this.animationPreset="mixamo"}}e([m(),a("design:type",Object)],d.prototype,"animationPreset",void 0);class v extends c{constructor(){super(...arguments),this.value="before"}applySceneParameters(e,a){return!(1!==a.size||!a.has("value"))&&(this.value=e.value,!0)}}e([m(),a("design:type",Object)],v.prototype,"value",void 0);class y extends p{constructor(){super(...arguments),this.value="before"}applySceneParameters(e,a){return!(1!==a.size||!a.has("value"))&&(this.value=e.value,!0)}}e([m(),a("design:type",Object)],y.prototype,"value",void 0);class b extends c{constructor(){super(...arguments),this.component=new y}}class P extends c{}async function g(e){return(await w(e)).actor}async function w(e){const a=x("prefab-asset",[A({id:"prefab-actor",type:"actor",actor:{type:"PrefabParamActor",params:{animationPreset:{type:u.String,value:e.prefabActorValue}},innerParams:null==e.prefabComponentValue?[]:[{path:["animation"],params:{animationPreset:{type:u.String,value:e.prefabComponentValue}}}],components:[]}})],"prefab-actor"),t=A({id:"prefab-instance",type:"prefab",assetId:a.id,prefab:{mainActorId:"prefab-actor",params:null==e.instanceParam?{}:{animationPreset:e.instanceParam},innerParams:null==e.instanceInnerParam?[]:[{path:["animation"],params:{animationPreset:e.instanceInnerParam}}]}}),{materializer:r}=h([t],new Map([[a.id,a]]));return await r.materialize(t),await r.initActorsPostInit(),{actor:r.materializedActors.get("prefab-instance/prefab-actor"),materializer:r}}function h(e,a,t=!1){const r={getObjects:()=>e,onCreate:()=>{},onUpdate:()=>{},onRemove:()=>{}},n={supportsLivePrefabParameters:t,onCreate:new s,onDelete:new s,onUpdate:new s,getAsset:o.fn(async e=>a.get(e)??null),getAssets:o.fn(async()=>Array.from(a.values()))},c={create:o.fn(async(e,a)=>{const t=new e;return null!=a&&t.position.copy(a),t}),initActor:o.fn(async e=>{e.__isInitialised=!0})};return{materializer:new l(new i,r,n,{},{renderer:{shadowMap:{needsUpdate:!1}}},[],[{name:"PrefabParamActor",type:d},{name:"LiveParamActor",type:v},{name:"SceneAttachedComponentActor",type:P}],c,[{name:"LiveParamComponent",type:y}]),assetsProvider:n}}function A(e){return{name:e.id,position:[0,0,0],rotation:[0,0,0,"XYZ"],scale:[1,1,1],...e}}function x(e,a,t){return{id:e,name:e,type:"prefab",prefab:{objects:a,mainActorId:t}}}t("prefab instance actor params",()=>{n("does not let generated prefab instance defaults override the prefab actor value",async()=>{const e=await g({prefabActorValue:"basic",instanceParam:{type:u.String,value:"mixamo"}});r(e.animationPreset).toBe("basic")}),n("applies prefab instance params when the override is explicit",async()=>{const e=await g({prefabActorValue:"basic",instanceParam:{type:u.String,value:"mixamo",override:!0}});r(e.animationPreset).toBe("mixamo")}),n("does not let generated prefab instance component defaults override prefab component values",async()=>{const e=await g({prefabActorValue:"basic",prefabComponentValue:"basic",instanceInnerParam:{type:u.String,value:"mixamo"}});r(e.animation.animationPreset).toBe("basic")}),n("applies prefab instance component params when the override is explicit",async()=>{const e=await g({prefabActorValue:"basic",prefabComponentValue:"basic",instanceInnerParam:{type:u.String,value:"mixamo",override:!0}});r(e.animation.animationPreset).toBe("mixamo")}),n("resolves direct scene actors and exposed prefab actors by scene object id",async()=>{const{actor:e,materializer:a}=await w({prefabActorValue:"basic"}),t=new d;a.materializedActors.set("direct-actor",t),r(a.resolveSceneActor("direct-actor")).toBe(t),r(a.resolveSceneActor("prefab-instance")).toBe(e)}),n("applies supported scene parameter changes without recreating the actor",async()=>{const e=A({id:"live-actor",type:"actor",actor:{type:"LiveParamActor",params:{value:{type:u.String,value:"after"}},innerParams:[],components:[]}}),a={...e.actor,params:{value:{type:u.String,value:"before"}}},{materializer:t}=h([e],new Map),n=new v,o=await t.tryApplyLiveActorParameterUpdate(n,e,JSON.stringify(a));r(o).toBe(!0),r(n.value).toBe("after")}),n("applies supported component parameter changes without recreating the actor",async()=>{const e=A({id:"live-component",type:"actor",actor:{type:"LiveComponentParamActor",params:{},innerParams:[{path:["component"],params:{value:{type:u.String,value:"after"}}}],components:[]}}),a={...e.actor,innerParams:[{path:["component"],params:{value:{type:u.String,value:"before"}}}]},{materializer:t}=h([e],new Map),n=new b,o=await t.tryApplyLiveActorParameterUpdate(n,e,JSON.stringify(a));r(o).toBe(!0),r(n.component.value).toBe("after")}),n("applies supported scene-attached component parameter changes without recreating the actor",async()=>{const e=A({id:"scene-attached-component",type:"actor",actor:{type:"SceneAttachedComponentActor",params:{},innerParams:[],components:[{type:"LiveParamComponent",params:{value:{type:u.String,value:"before"}},innerParams:[]}]}}),{materializer:a}=h([e],new Map);await a.materialize(e),await a.initActorsPostInit();const t=a.materializedActors.get(e.id),n=t.attachedComponents[0],o=JSON.stringify(e.actor);e.actor.components[0].params.value={type:u.String,value:"after"};const i=await a.tryApplyLiveActorParameterUpdate(t,e,o);r(i).toBe(!0),r(n.value).toBe("after"),r(t.attachedComponents[0]).toBe(n)}),n("applies saved prefab parameters to a runtime-spawned prefab instance",async()=>{const e=x("live-prefab",[A({id:"live-actor",type:"actor",actor:{type:"LiveParamActor",params:{value:{type:u.String,value:"before"}},components:[]}})],"live-actor"),a=structuredClone(e);a.prefab.objects[0].actor.params.value={type:u.String,value:"after"};const{materializer:t,assetsProvider:n}=h([],new Map([[e.id,e]]),!0),i=await t.createFromPrefabAsset(e,{sceneObjectChain:["runtime-instance"],runtimeSpawn:!0},void 0,void 0,!1),s=i.mainActor;n.onUpdate.next(a),await o.waitFor(()=>r(s.value).toBe("after")),r(i.mainActor).toBe(s)}),n("applies supported component changes when the actor rejects an independent change",async()=>{const e=x("partially-live-prefab",[A({id:"component-actor",type:"actor",actor:{type:"SceneAttachedComponentActor",params:{unsupported:{type:u.String,value:"before"}},components:[{type:"LiveParamComponent",params:{value:{type:u.String,value:"before"}}}]}})],"component-actor"),a=structuredClone(e);a.prefab.objects[0].actor.params.unsupported.value="after",a.prefab.objects[0].actor.components[0].params.value.value="after";const{materializer:t,assetsProvider:n}=h([],new Map([[e.id,e]]),!0),i=await t.createFromPrefabAsset(e,{sceneObjectChain:["runtime-component-instance"],runtimeSpawn:!0},void 0,void 0,!1),s=i.mainActor,c=s.attachedComponents[0],p=o.spyOn(console,"warn").mockImplementation(()=>{});n.onUpdate.next(a),await o.waitFor(()=>r(c.value).toBe("after")),r(i.mainActor).toBe(s),r(p).toHaveBeenCalledWith(r.stringContaining("were not applied")),p.mockRestore()})});/*
|
|
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
|
|
1
|
+
import{describe as e,expect as t,it as a,vi as n}from"vitest";import{RuntimeAssetsService as s}from"../scene/runtime-asset-service.js";function o(e){return{getAssets:n.fn(async()=>e),getAsset:n.fn(async t=>e.find(e=>e.id===t)),getScenes:n.fn(),getScene:n.fn(),getSceneData:n.fn()}}function r(e){return{id:"prefab-a",name:"Prefab A",type:"prefab",prefab:{mainActorId:"actor-a",objects:[{id:"actor-a",name:"Actor A",type:"actor",position:[1,2,3],rotation:[0,0,0],scale:[1,1,1],actor:{type:"TestActor",params:{label:{type:0,value:e}},components:[{type:"TestComponent",params:{label:{type:0,value:`${e}-component`}}}]}}]}}}e("RuntimeAssetsService",()=>{a("shares concurrent asset list loads and reuses the loaded cache",async()=>{const e=[{id:"mesh-a",name:"Mesh A",type:"mesh"},{id:"material-a",name:"Material A",type:"material"}],a={getAssets:n.fn(async()=>(await new Promise(e=>setTimeout(e,5)),e)),getAsset:n.fn(),getScenes:n.fn(),getScene:n.fn(),getSceneData:n.fn()},o=new s(a);await Promise.all([o.getAssets(),o.getAssets()]),await o.getAssets(),t(a.getAssets).toHaveBeenCalledTimes(1)}),a("merges only compatible prefab parameter data into the runtime asset",async()=>{const e=r("old-value"),a=o([e]),c=new s(a,!1),i=[];c.onUpdate.subscribe(e=>i.push(e));const p=r("new-value");p.prefab.objects[0].position=[99,99,99],p.prefab.eventGraph={nodes:[{id:"ignored"}]};const l=n.spyOn(console,"warn").mockImplementation(()=>{});t(await c.applyLivePrefabParameters(p)).toBe(!0);const m=await c.getAsset(e.id);t(m.prefab.objects[0].actor.params.label.value).toBe("new-value"),t(m.prefab.objects[0].actor.components[0].params.label.value).toBe("new-value-component"),t(m.prefab.objects[0].position).toEqual([1,2,3]),t(m.prefab.eventGraph).toBeUndefined(),t(i).toEqual([m]),t(l).toHaveBeenCalledWith(t.stringContaining("event graph updates are not supported")),l.mockRestore()}),a("rejects structural prefab changes without replacing the cached asset",async()=>{const e=r("old-value"),a=o([e]),c=new s(a,!1),i=r("new-value");i.prefab.objects[0].actor.components[0].type="OtherComponent";const p=n.spyOn(console,"warn").mockImplementation(()=>{});t(await c.applyLivePrefabParameters(i)).toBe(!1),t(await c.getAsset(e.id)).toBe(e),t(p).toHaveBeenCalledWith(t.stringContaining("structure changed")),p.mockRestore()})});/*
|
|
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{expect as
|
|
1
|
+
import{expect as e,test as t,vi as o}from"vitest";import*as i from"three";import{BaseActor as n}from"../gameplay/actors/actor.js";import{CharacterAnimationComponent as s,OldCharacterMovementComponent as r}from"../gameplay/actors/index.js";import{RootMotionClip as a}from"../gameplay/animation/root-motion.js";import{createAnimationClipInstance as c,createAnimationSubTrack as l}from"../effects/sequence/sequence-data.js";import{evaluateAnimationClipOutputProgress as m,evaluateAnimationClipSourceProgress as p,normalizeAnimationClipRetiming as u,buildRetimedAnimationClip as f}from"../effects/sequence/sequence-animation-retiming.js";import{SequencePlayer as d}from"../effects/sequence/sequence-player.js";function g(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(p(void 0,.25)).toBeCloseTo(.25),e(m(void 0,.75)).toBeCloseTo(.75),e(p({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(p(t,.35)).toBeCloseTo(.55),e(m(t,.55)).toBeCloseTo(.35),e(p(t,.7)).toBeGreaterThan(.7)}),t("advanced retiming sanitization stays monotonic and bounded",()=>{const t=u({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(g(s.times)).toEqual([0,.4,1]),e(g(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],r=a.fromClip(n,!1);e(g(s.times)).toEqual([0,.25,1]),e(g(r.motionTrack.times)).toEqual([0,.25,1]),e(g(r.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 d,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 r=new i.Object3D,a={target:r,mixer:new i.AnimationMixer(r),activeActions:new Map};t.evaluateAnimationSubTrack(o,a,.1);const m=a.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 d,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 r=new h,m=new y(r.object),p=new v;r.charAnim=m,r.movement=p;const u={target:r,activeActions:new Map};t.evaluateAnimationSubTrack(o,u,.1);const f=u.activeActions.get(`${o.id}-${n.id}`);e(f).toBeDefined(),e(f.timeScale).toBeCloseTo(1),e(f.getClip()).toBeInstanceOf(a),e(f.getClip().duration).toBeCloseTo(1.5),e(p.lastRootMotionAction).toBe(f),e(t.retimedAnimationClips.size).toBe(1)}),t("external sequence control previews root motion deterministically while scrubbing",()=>{const t=new d;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 r=new h;r.object.position.set(10,3,0),r.object.scale.setScalar(2);const a=new i.Bone;a.name="hips",r.object.add(a),r.charAnim=new y(r.object);const m={target:r,activeActions:new Map};t.evaluateSubTracks([o],m,.25),e(r.object.position.x).toBeCloseTo(10.5),e(r.object.position.y).toBeCloseTo(3),t.evaluateSubTracks([o],m,1.5),e(r.object.position.x).toBeCloseTo(13),e(r.object.position.y).toBeCloseTo(3),t.evaluateSubTracks([o],m,.5),e(r.object.position.x).toBeCloseTo(11),e(r.object.position.y).toBeCloseTo(3)});class h extends n{constructor(){super(...arguments),this.charAnim=null,this.movement=null}getComponent(e){return e===s?this.charAnim:e===r?this.movement:void 0}}class y{constructor(e){this.fullBodyAction=null,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(){}}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
|
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{expect as o,test as e}from"vitest";import*as t from"three";import{createAnimationClipInstance as n}from"../effects/sequence/sequence-data.js";import{applyAnimationClipRootMotionAxisMask as i}from"../effects/sequence/sequence-animation-retiming.js";function a(o){return Array.from(o).map(o=>Math.round(1e3*o)/1e3)}e("root motion axis masking removes disabled translation without changing the source clip",()=>{const e=new t.AnimationClip("root-motion-mask",1,[new t.VectorKeyframeTrack("hips.position",[0,.5,1],[2,3,4,3,5,7,1,8,10])]),r=n("anim",0,1);r.rootMotion=!0,r.rootMotionX=!1;const s=i(e,r),m=s.tracks[0],c=e.tracks[0];o(s).not.toBe(e),o(a(m.values)).toEqual([2,3,4,2,5,7,2,8,10]),o(a(c.values)).toEqual([2,3,4,3,5,7,1,8,10])}),e("root motion axis masking keeps the original clip when all axes are enabled",()=>{const e=new t.AnimationClip("root-motion-unmasked",1,[new t.VectorKeyframeTrack("hips.position",[0,1],[0,0,0,1,2,3])]),a=n("anim",0,1);o(i(e,a)).toBe(e)});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|