@hology/core 0.0.207 → 0.0.209
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/effects/sequence/sequence-definitions.js +1 -1
- package/dist/effects/sequence/sequence-player.d.ts +2 -0
- package/dist/effects/sequence/sequence-player.js +1 -1
- package/dist/gameplay/actors/builtin/components/index.d.ts +2 -0
- package/dist/gameplay/actors/builtin/components/index.js +1 -1
- package/dist/gameplay/actors/camera/first-person-camera-component.d.ts +53 -0
- package/dist/gameplay/actors/camera/first-person-camera-component.js +4 -0
- package/dist/gameplay/actors/camera/third-person-camera-component.d.ts +4 -0
- package/dist/gameplay/actors/camera/third-person-camera-component.js +1 -1
- package/dist/gameplay/actors/index.d.ts +1 -0
- package/dist/gameplay/actors/index.js +1 -1
- package/dist/gameplay/initiate.d.ts +3 -0
- package/dist/gameplay/initiate.js +1 -1
- package/dist/rendering/upscaling-pass.d.ts +95 -0
- package/dist/rendering/upscaling-pass.js +4 -0
- package/dist/rendering.d.ts +75 -1
- package/dist/rendering.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/materializer.js +1 -1
- package/dist/scene/materials/water.js +1 -1
- package/dist/scene/runtime-asset-service.d.ts +2 -0
- package/dist/scene/runtime-asset-service.js +1 -1
- package/dist/scene/storage/storage.d.ts +2 -0
- package/dist/scene/storage/storage.js +1 -1
- package/dist/shader/builtin/landscape-composite-shader.d.ts +0 -1
- package/dist/shader/builtin/landscape-composite-shader.js +1 -1
- package/dist/shader/builtin/standard-shader.d.ts +13 -12
- package/dist/shader/builtin/standard-shader.js +1 -1
- package/dist/shader/builtin/toon-shader.d.ts +9 -7
- package/dist/shader/builtin/toon-shader.js +1 -1
- package/dist/shader/builtin/unlit-shader.d.ts +5 -4
- package/dist/shader/builtin/unlit-shader.js +1 -1
- package/dist/shader-nodes/pom.js +1 -1
- package/dist/test/first-person-camera-component.test.d.ts +2 -0
- package/dist/test/first-person-camera-component.test.js +4 -0
- package/dist/test/runtime-asset-service.test.d.ts +2 -0
- package/dist/test/runtime-asset-service.test.js +4 -0
- package/dist/test/runtime-param-type-inference.test.js +1 -1
- package/dist/test/sequence-post-process.test.js +1 -1
- package/dist/test/shader-graph.test.js +1 -1
- package/dist/test/storage-case-collision.test.js +1 -1
- package/package.json +2 -2
- 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{combineLatest as a,EMPTY as i,filter as n,firstValueFrom as s,from as r,map as h,mergeAll as o,mergeMap as l,Observable as c,of as d,startWith as p,Subject as u,switchMap as f,tap as w}from"rxjs";import{sleepDelay as m}from"../../utils/async.js";const v={},y={},b={},j={};null==v.read&&window.require&&(Object.assign(v,window.require("fs")),Object.assign(y,v.promises),Object.assign(b,window.require("path")),Object.assign(j,window.require("chokidar")));const x=null!=v.existsSync;function g(){if(x){const e="--path=",t=window.process.argv.find(t=>t.startsWith(e));return t?t.substring(e.length):""}return""}const F=/^[A-Z]:/;function P(...e){return 0===e.length?"":F.test(e[0])||x?b.join(...e):t(...e)}export class ObjectStorage{get pathResources(){return P(this.path+"-resources")}constructor(e,t,a=defaultSerializer){this.name=e,this.filePathFn=t,this.serializer=a,this.basePathUpdates=new u,this.basePath=this.basePathUpdates.pipe(h(e=>P(g(),e)),w(e=>{this.path=P(e,this.name),I(this.path),this.determineIfMetaFileShouldBeCreated()})),this.loaded=s(this.basePath),this.shouldCreateIndex=!1}setBasePath(e){this.basePathUpdates.next(e)}async determineIfMetaFileShouldBeCreated(){if(x)try{const e=["vite.renderer.config.ts","vite.config.ts"];for(const t of e){const e=P(g(),t);if(!await S(e))continue;if((await y.readFile(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=""){C(),await y.mkdir(b.join(this.path,t,e),{recursive:!0})}async deleteFolderForceDangerous(e){await y.rm(b.join(this.path,e),{recursive:!0,force:!0})}async moveFolder(e,t){if(C(),""==e)return void console.warn("Can not move a folder in root");const a=b.resolve(b.join(this.path,t),b.basename(e));if(await E(a)){if(a!==b.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=b.resolve(e),i=b.resolve(t),n=b.normalize(a)+b.sep;return(b.normalize(i)+b.sep).startsWith(n)}(b.join(this.path,e),b.join(this.path,t)))throw new Error("Can not move a folder into a folder it contains");await y.rename(b.join(this.path,e),a)}}async renameFolder(e,t){await y.rename(b.join(this.path,e),b.resolve(b.dirname(b.join(this.path,e)),t))}async moveToFolder(e,t){if(C(),e.path===t)return;await this.assertObjectFilePathAvailable({...e,path:t},e.id);const a=this.privateObjectPath({...e,path:t});await y.rename(this.privateObjectPath(e),a)}getAbsolutePath(e=""){return b.join(this.path,e)}getResourceAbsolutePath(e=""){return b.join(this.pathResources,e)}watchFolders(){return r(this.loaded).pipe(f(()=>r(I(this.path))),f(()=>this.watchDir(this.path).pipe(n(e=>!e.filename.endsWith(".json")),p(null),f(()=>r(y.readdir(this.path,{recursive:!0,withFileTypes:!0}))),h(e=>Array.from(new Set(e.filter(e=>e.isDirectory()).map(e=>b.relative(this.path,b.join(e.parentPath,e.name)).replace(/^\.$/,"")).values()))))))}watch(){C();r(this.loaded).pipe(f(()=>a([this.watchDir(this.pathResources),r(this.getAll())]).pipe(n(([e])=>null!=e),l(([e,t])=>{if("change"===e.event){const a=b.basename(e.filename),i=t.filter(e=>e.fileKey===a);if(i.length>0)return r(i).pipe(l(e=>{const t=this.privateObjectRelativePath(e);return r(this.readFileIfExists(t)).pipe(h(a=>({event:"change",object:a,path:e.path,filename:t})))}))}return i}))));return r(this.loaded).pipe(f(()=>this.watchDir(this.path)),l(e=>{const t={event:e.event,path:b.dirname(e.filename).replace(/^\.$/,""),filename:b.basename(e.filename)};return e.filename.endsWith(".json")?"unlink"!==e.event?r(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())}),h(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?r(this.reloadSubdirectory(e.filename)).pipe(o(),h(e=>({object:e,...t}))):i}))}async reloadSubdirectory(e){return(await this.getAll(e)).filter(t=>t.path.startsWith(e))}async readFileIfExists(e){const t=P(this.path,e);try{const a=await y.readFile(t);return{...JSON.parse(a.toString()),path:b.dirname(e).replace(/^\.$/,""),filename:b.basename(e)}}catch{return console.error("Could not find file at "+t),null}}async getAll(e){if(x){await this.loaded,await I(this.path);const t=await this.getObjectFilePaths(e??""),a=100,i=[];for(let e=0;e<t.length;e+=a){const n=t.slice(e,e+a),s=await Promise.all(n.map(e=>y.readFile(P(this.path,e)).then(t=>({...JSON.parse(t.toString()),path:b.dirname(e).replace(/^\.$/,""),filename:b.basename(e)}))));i.push(...s)}return 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=b.join(t,e),i=[];return await async function e(a){const n=await y.readdir(a,{withFileTypes:!0});for(const s of n){const n=b.join(a,s.name);s.isDirectory()?await e(n):s.isFile()&&s.name.endsWith(".json")&&!/^[\._]/.test(s.name)&&i.push(b.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(!x)return(await fetch(i)).json();return await S(i)?{...JSON.parse((await y.readFile(i)).toString()),path:a.path??"",filename:b.basename(this.privateObjectRelativePath(a))}:null}async save(e){if(C(),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 y.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 y.rename(i,n)}catch(e){console.error(e),console.warn("Rename failed, retrying",{currentPath:i,newPath:n}),await m(400),await y.rename(i,n)}return await this.save(a),a}async delete(e){await y.unlink(this.privateObjectPath(e)),this.removeIndexEntry(e.id)}async create(t){C(),await this.loaded,t.id=e();const a=this.privateObjectPath(t);if(await this.assertObjectFilePathAvailable(t),await E(a))throw Error(`Can not create because a file already exists at ${a}`);return await y.writeFile(a,this.serialize(t)),await this.addOrUpdateIndexEntry(t),t}prepareCreate(t){return C(),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(){x&&this.shouldCreateIndex&&null!=this.cachedIndex&&await y.writeFile(this.indexFilePath,JSON.stringify(this.cachedIndex,null,2))}get indexFilePath(){return P(this.path,"_meta.json")}async loadIndex(){if(null==this.cachedIndex)if(x){const e=await this.getAll(),t={};for(const a of e)t[a.id]={id:a.id,name:a.name??a.id,path:a.path};this.cachedIndex=t,await this.persistIndex()}else this.cachedIndex=await(await fetch(this.indexFilePath)).json();return this.cachedIndex}async ensureResourceDir(){await I(P(this.path+"-resources"))}async saveFile(e,t){return C(),await I(P(this.path+"-resources")),y.copyFile(t.path,P(this.path+"-resources",e.fileKey))}async saveExtraFile(e,t){return C(),await I(P(this.path+"-resources")),y.copyFile(e,P(this.path+"-resources",t))}getAssetPath(e){return window&&"function"==typeof window.require?window.require("path").join(this.path+"-resources",e.fileKey):P(this.path+"-resources",e.fileKey)}async replaceFile(e,t){if(await S(t))return y.copyFile(t,P(this.path+"-resources",e.fileKey));console.error("Failed to replace file using path "+t)}async deleteFile(e){if(null==e)return;C();const t=P(this.path+"-resources",e);return await S(t)?y.unlink(t):void 0}privateObjectPath(e){return P(this.path,this.privateObjectRelativePath(e))}privateObjectRelativePath(e){return this.filePathFn?P(e.path??"",this.filePathFn(e)):P(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=O(this.privateObjectRelativePath(e)),i=await this.loadIndex();return Object.values(i).find(e=>e.id!==t&&O(this.privateObjectRelativePath(e))===a)??null}watchDir(e){return new c(t=>{if("win32"===process.platform&&x){const a=v.watch(e,{recursive:!0},(a,i)=>{if(i){const n=b.join(e,i);v.access(n,v.constants.F_OK,e=>{const n=e?"unlink":"rename"===a?"add":"change";t.next({event:n,filename:i})})}});return()=>a.close()}const a=j.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 O(e){return e.replace(/\\/g,"/").toLowerCase()}async function I(e){x&&(await S(e)||await y.mkdir(e,{recursive:!0}))}function S(e){return!!x&&new Promise(function(t,a){v.exists(e,function(e){t(e)})})}function C(){if(!x)throw new Error("Must have direct access to filesystem")}async function E(e){try{await y.access(e,y.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{combineLatest as a,EMPTY as i,filter as n,firstValueFrom as s,from as r,map 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 A(...e){return 0===e.length?"":C.test(e[0])||j?x.join(...e):t(...e)}export class ObjectStorage{get pathResources(){return A(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(h(e=>A(S(),e)),w(e=>{this.path=A(e,this.name),this.cachedIndex=null,this.loadingIndex=null,z(this.path),this.determineIfMetaFileShouldBeCreated()})),this.loaded=s(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=A(S(),t);if(!await k(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 K(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;await this.assertObjectFilePathAvailable({...e,path:t},e.id);const a=this.privateObjectPath({...e,path:t});await v.rename(this.privateObjectPath(e),a)}getAbsolutePath(e=""){return x.join(this.path,e)}getResourceAbsolutePath(e=""){return x.join(this.pathResources,e)}watchFolders(){return r(this.loaded).pipe(f(()=>r(z(this.path))),f(()=>this.watchDir(this.path).pipe(n(e=>!e.filename.endsWith(".json")),u(null),f(()=>r(v.readdir(this.path,{recursive:!0,withFileTypes:!0}))),h(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();r(this.loaded).pipe(f(()=>a([this.watchDir(this.pathResources),r(this.getAll())]).pipe(n(([e])=>null!=e),l(([e,t])=>{if("change"===e.event){const a=x.basename(e.filename),i=t.filter(e=>e.fileKey===a);if(i.length>0)return r(i).pipe(l(e=>{const t=this.privateObjectRelativePath(e);return r(this.readFileIfExists(t)).pipe(h(a=>({event:"change",object:a,path:e.path,filename:t})))}))}return i}))));return r(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?r(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())}),h(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?r(this.reloadSubdirectory(e.filename)).pipe(o(),h(e=>({object:e,...t}))):i}))}async reloadSubdirectory(e){return(await this.getAll(e)).filter(t=>t.path.startsWith(e))}async readFileIfExists(e){const t=A(this.path,e);try{const a=await O(t);return{...JSON.parse(a.toString()),path:x.dirname(e).replace(/^\.$/,""),filename:x.basename(e)}}catch{return console.error("Could not find file at "+t),null}}async getAll(e){if(j){await this.loaded,await z(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(A(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 k(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 K(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 A(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 z(A(this.path+"-resources"))}async saveFile(e,t){return D(),await z(A(this.path+"-resources")),v.copyFile(t.path,A(this.path+"-resources",e.fileKey))}async saveExtraFile(e,t){return D(),await z(A(this.path+"-resources")),v.copyFile(e,A(this.path+"-resources",t))}getAssetPath(e){return window&&"function"==typeof window.require?window.require("path").join(this.path+"-resources",e.fileKey):A(this.path+"-resources",e.fileKey)}async replaceFile(e,t){if(await k(t))return v.copyFile(t,A(this.path+"-resources",e.fileKey));console.error("Failed to replace file using path "+t)}async deleteFile(e){if(null==e)return;D();const t=A(this.path+"-resources",e);return await k(t)?v.unlink(t):void 0}privateObjectPath(e){return A(this.path,this.privateObjectRelativePath(e))}privateObjectRelativePath(e){return this.filePathFn?A(e.path??"",this.filePathFn(e)):A(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=E(this.privateObjectRelativePath(e)),i=await this.loadIndex();return Object.values(i).find(e=>e.id!==t&&E(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 E(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 z(e){j&&(await k(e)||await v.mkdir(e,{recursive:!0}))}function k(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 K(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
|
*/
|
|
@@ -3,7 +3,6 @@ import { LayerMixMode } from '../../shader-nodes/layers.js';
|
|
|
3
3
|
import { NodeShader, NodeShaderOutput } from '../shader.js';
|
|
4
4
|
export declare class LandscapeCompositeShader extends NodeShader {
|
|
5
5
|
layers: Material[];
|
|
6
|
-
uvScale: number;
|
|
7
6
|
transition: LayerMixMode;
|
|
8
7
|
noiseScale: number;
|
|
9
8
|
noiseAmount: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{__decorate as e,__metadata as s}from"tslib";import{Material as t,MathUtils as a,MeshBasicMaterial as o,MeshLambertMaterial as l,MeshStandardMaterial as i}from"three";import{cos as
|
|
1
|
+
import{__decorate as e,__metadata as s}from"tslib";import{Material as t,MathUtils as a,MeshBasicMaterial as o,MeshLambertMaterial as l,MeshStandardMaterial as i}from"three";import{cos as n,float as r,mix as p,rgb as h,standardMaterial as u,step as m,varyingAttributes as c,SimplexNoiseNode as d,varying as y,uniforms as M,vec4 as f,attributes as g,smoothstep as S,textureSampler2d as b,rgba as v,NodeShaderMaterial as A,varyingTransformed as w,colorToNormal as C}from"three-shader-graph";import{LayerMixMode as L,mixColorsByLayer as N}from"../../shader-nodes/layers.js";import{Parameter as j}from"../parameter.js";import{NodeShader as x}from"../shader.js";import{ParallaxStandardMaterial as R}from"./standard-shader.js";import{parallaxOcclusionMapping as T}from"../../shader-nodes/pom.js";import{applyUvTiling as O}from"./../../shader/uv-nodes.js";const P=y(M.modelMatrix.multiplyVec(f(g.position,1)).xz),k=new i({color:"gray"});export class LandscapeCompositeShader extends x{constructor(){super(...arguments),this.layers=[],this.transition=L.soft,this.noiseScale=1,this.noiseAmount=.2,this.softness=.3,this.enableSlope=!1,this.slopeAngle=35}output(){if(0===this.layers.length)return{color:u({color:h("gray")}),landscape:!0};for(let e=0;e<this.layers.length;e++){null==this.layers[e]&&(console.warn("One of the layers in the landscape composite shader is null or undefined. This layer will be rendered as gray. Please make sure to assign a material to all layers.",this.layers),this.layers[e]=k);this.layers[e]instanceof i||this.layers[e]instanceof l||this.layers[e]instanceof o||this.layers[e]instanceof A||(console.warn("One of the layers in the landscape composite shader is not a supported material. This layer will be rendered as gray. Please use a supported material.",this.layers[e]),this.layers[e]=k)}const e=new Map,s=new z,t=c.uv;function y(s,t){let a=t;if(s instanceof R&&null!=s.heightMap){let o=e.get(s);null==o&&(o=T(t,b(s.heightMap),r(s.heightScale??1)),e.set(s,o)),a=o}return a}const M=this.layers.map(e=>{if(e instanceof i||e instanceof l||e instanceof o){let s=v(e.color,1);return null!=e.map&&(s=s.multiply(b(e.map).sample(y(e,O(t,e.map))))),s}return e instanceof A&&null!=e.outputAlbedo?e.outputAlbedo.rgba(1):v("white")}),f=this.layers.map(e=>{if(e instanceof i||e instanceof l){if(null!=e.normalMap)return C(b(e.normalMap).sample(y(e,O(t,e.normalMap))),e.normalScale?.x??1)}else if(e instanceof A&&null!=e.outputNormal)return e.outputNormal;return w.normal}),g=this.layers.map(e=>{let a=r(1);return e instanceof i?(a=r(e.roughness??1),null!=e.roughnessMap&&(a=a.multiply(s.get(e.roughnessMap,y(e,O(t,e.roughnessMap))).g))):e instanceof A&&null!=e.outputRoughness&&(a=e.outputRoughness),a}),j=this.layers.map(e=>{let a=r(0);return e instanceof i?(a=r(e.metalness??0),null!=e.metalnessMap&&(a=a.multiply(s.get(e.metalnessMap,y(e,O(t,e.metalnessMap))).b))):e instanceof A&&null!=e.outputMetalness&&(a=e.outputMetalness),a});let x,B,H,V;if(this.enableSlope&&null!=this.slopeMaterial)if(this.slopeMaterial instanceof i){const e=y(this.slopeMaterial,t);x=v(this.slopeMaterial.color,1),null!=this.slopeMaterial.map&&(x=x.multiply(s.get(this.slopeMaterial.map,O(e,this.slopeMaterial.map)))),B=w.normal,null!=this.slopeMaterial.normalMap&&(B=C(s.get(this.slopeMaterial.normalMap,O(e,this.slopeMaterial.normalMap)),this.slopeMaterial.normalScale?.x??1)),H=r(this.slopeMaterial.roughness??1),null!=this.slopeMaterial.roughnessMap&&(H=H.multiply(s.get(this.slopeMaterial.roughnessMap,O(e,this.slopeMaterial.roughnessMap)).g)),V=r(this.slopeMaterial.metalness??0),null!=this.slopeMaterial.metalnessMap&&(V=V.multiply(s.get(this.slopeMaterial.metalnessMap,O(e,this.slopeMaterial.metalnessMap)).b))}else this.slopeMaterial instanceof A&&(x=this.slopeMaterial.outputAlbedo.rgba(1)??v("white"),B=this.slopeMaterial.outputNormal??w.normal,H=this.slopeMaterial.outputRoughness??r(1),V=this.slopeMaterial.outputMetalness??r(0));const q=(e,s)=>{const t=N({layerColors:e.slice(0,8),noiseScale:this.noiseScale,noiseAmount:this.noiseAmount,mode:this.transition,decay:this.softness}),o=r(this.noiseAmount).multiply(r(.1)),l=r(this.noiseScale),i=new d(P.multiplyScalar(l)).multiply(r(2)).subtract(r(1)).multiply(o),h=r(a.degToRad(this.slopeAngle)).add(i.multiply(r(.1)));let u=p(t,s,m(h,n(c.normal.y)));if(this.transition==L.soft){const e=h,a=r(this.softness).divide(r(2)),o=e.subtract(a),l=e.add(a);u=p(t,s,S(o,l,n(c.normal.y)))}return this.enableSlope&&null!=s?u:t},D=q(M,x),E=q(f,B),F=q(g,H),G=q(j,V);return{color:u({color:D,normal:E,roughness:F,metalness:G}),landscape:!0}}}e([j({type:t}),s("design:type",Array)],LandscapeCompositeShader.prototype,"layers",void 0),e([j({options:[{value:L.hard,name:"Hard"},{value:L.soft,name:"Soft"}]}),s("design:type",Number)],LandscapeCompositeShader.prototype,"transition",void 0),e([j(),s("design:type",Number)],LandscapeCompositeShader.prototype,"noiseScale",void 0),e([j(),s("design:type",Number)],LandscapeCompositeShader.prototype,"noiseAmount",void 0),e([j(),s("design:type",Number)],LandscapeCompositeShader.prototype,"softness",void 0),e([j(),s("design:type",Boolean)],LandscapeCompositeShader.prototype,"enableSlope",void 0),e([j(),s("design:type",t)],LandscapeCompositeShader.prototype,"slopeMaterial",void 0),e([j(),s("design:type",Number)],LandscapeCompositeShader.prototype,"slopeAngle",void 0);class z{constructor(){this.cache=new Map}get(e,s){return this.cache.has(e)||this.cache.set(e,b(e).sample(s)),this.cache.get(e)}}/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -1,34 +1,35 @@
|
|
|
1
1
|
import { Color, Material, MeshStandardMaterial, MeshStandardMaterialParameters, Texture, Vector2 } from 'three';
|
|
2
2
|
import { Shader } from '../shader.js';
|
|
3
|
+
import { Sampler2DNode } from 'three-shader-graph';
|
|
3
4
|
export declare class StandardShader extends Shader {
|
|
4
5
|
color: Color;
|
|
5
|
-
map?:
|
|
6
|
+
map?: Sampler2DNode;
|
|
6
7
|
opacity: number;
|
|
7
|
-
alphaMap?:
|
|
8
|
+
alphaMap?: Sampler2DNode;
|
|
8
9
|
roughness?: number;
|
|
9
|
-
roughnessMap?:
|
|
10
|
+
roughnessMap?: Sampler2DNode;
|
|
10
11
|
metalness?: number;
|
|
11
|
-
metalnessMap?:
|
|
12
|
-
lightMap?:
|
|
12
|
+
metalnessMap?: Sampler2DNode;
|
|
13
|
+
lightMap?: Sampler2DNode;
|
|
13
14
|
lightMapIntensity?: number;
|
|
14
|
-
aoMap?:
|
|
15
|
+
aoMap?: Sampler2DNode;
|
|
15
16
|
aoMapIntensity?: number;
|
|
16
17
|
emissive?: Color;
|
|
17
18
|
emissiveIntensity?: number;
|
|
18
|
-
emissiveMap?:
|
|
19
|
-
normalMap?:
|
|
19
|
+
emissiveMap?: Sampler2DNode;
|
|
20
|
+
normalMap?: Sampler2DNode;
|
|
20
21
|
normalScale?: Vector2;
|
|
21
22
|
sheen: number;
|
|
22
23
|
sheenColor?: Color;
|
|
23
|
-
sheenColorMap
|
|
24
|
+
sheenColorMap?: Sampler2DNode;
|
|
24
25
|
sheenRoughness: number;
|
|
25
|
-
sheenRoughnessMap
|
|
26
|
+
sheenRoughnessMap?: Sampler2DNode;
|
|
26
27
|
anisotropyRotation?: number;
|
|
27
|
-
anisotropyMap?:
|
|
28
|
+
anisotropyMap?: Sampler2DNode;
|
|
28
29
|
anisotropy?: number;
|
|
29
30
|
envMap?: Texture;
|
|
30
31
|
envMapIntensity?: number;
|
|
31
|
-
heightMap?:
|
|
32
|
+
heightMap?: Sampler2DNode;
|
|
32
33
|
heightScale?: number;
|
|
33
34
|
vertexColor?: boolean;
|
|
34
35
|
uvScale: Vector2;
|
|
@@ -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 o}from"../shader.js";import{Parameter as s}from"../parameter.js";import{removeObjectUndefined as p}from"../../utils/collections.js";import*as l from"three";const d=new r;export class StandardShader extends o{constructor(){super(...arguments),this.color=new t("#FFFFFF"),this.opacity=1,this.roughness=1,this.metalness=0,this.normalScale=new i(1,1),this.sheen=0,this.sheenColor=new t("#FFFFFF"),this.sheenRoughness=.5,this.anisotropy=0,this.heightMap=d,this.vertexColor=!1,this.uvScale=new i(1,1)}build(){const e=this.sheen>0,a=this.anisotropy>0,t=a||e,r=(null!=this.map||null!=this.normalMap)&&null!=this.heightMap&&this.heightMap!=d&&0!==this.heightScale,i=r?ParallaxStandardMaterial:t?l.MeshPhysicalMaterial:n;null!=this.map&&(this.map.colorSpace=l.SRGBColorSpace),null!=this.normalMap&&(this.normalMap.colorSpace=l.LinearSRGBColorSpace),null!=this.metalnessMap&&(this.metalnessMap.colorSpace=l.LinearSRGBColorSpace),null!=this.roughnessMap&&(this.roughnessMap.colorSpace=l.LinearSRGBColorSpace),null!=this.aoMap&&(this.aoMap.colorSpace=l.LinearSRGBColorSpace),null!=this.heightMap&&(this.heightMap.colorSpace=l.NoColorSpace),null!=this.emissiveMap&&(this.emissiveMap.colorSpace=l.SRGBColorSpace),null!=this.alphaMap&&(this.alphaMap.colorSpace=l.LinearSRGBColorSpace),null!=this.lightMap&&(this.lightMap.colorSpace=l.LinearSRGBColorSpace);return new i(p({color:this.color,vertexColors:this.vertexColor,opacity:this.opacity,roughness:this.roughness,metalness:this.metalness,map:applyTiling(this.map,this.uvScale),lightMap:this.lightMap,lightMapIntensity:this.lightMapIntensity,aoMap:applyTiling(this.aoMap,this.uvScale),aoMapIntensity:this.aoMapIntensity,emissive:this.emissive,emissiveIntensity:this.emissiveIntensity,emissiveMap:applyTiling(this.emissiveMap,this.uvScale),normalMap:applyTiling(this.normalMap,this.uvScale),normalScale:this.normalScale,sheen:e?this.sheen:void 0,sheenColor:e?this.sheenColor:void 0,sheenColorMap:e?this.sheenColorMap:void 0,sheenRoughness:e?this.sheenRoughness:void 0,sheenRoughnessMap:e?this.sheenRoughnessMap:void 0,anisotropy:a?this.anisotropy:void 0,anisotropyMap:a?this.anisotropyMap:void 0,anisotropyRotation:a?this.anisotropyRotation:void 0,roughnessMap:applyTiling(this.roughnessMap,this.uvScale),metalnessMap:applyTiling(this.metalnessMap,this.uvScale),alphaMap:applyTiling(this.alphaMap,this.uvScale),envMap:this.envMap,envMapIntensity:this.envMapIntensity,...r?{heightMap:this.heightMap,heightScale:this.heightScale}:{}}))}}e([s(),a("design:type",t)],StandardShader.prototype,"color",void 0),e([s({label:"Color Map"}),a("design:type",r)],StandardShader.prototype,"map",void 0),e([s({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"opacity",void 0),e([s(),a("design:type",r)],StandardShader.prototype,"alphaMap",void 0),e([s({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"roughness",void 0),e([s(),a("design:type",r)],StandardShader.prototype,"roughnessMap",void 0),e([s({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"metalness",void 0),e([s(),a("design:type",r)],StandardShader.prototype,"metalnessMap",void 0),e([s(),a("design:type",r)],StandardShader.prototype,"lightMap",void 0),e([s(),a("design:type",Number)],StandardShader.prototype,"lightMapIntensity",void 0),e([s(),a("design:type",r)],StandardShader.prototype,"aoMap",void 0),e([s(),a("design:type",Number)],StandardShader.prototype,"aoMapIntensity",void 0),e([s(),a("design:type",t)],StandardShader.prototype,"emissive",void 0),e([s({range:[0,10]}),a("design:type",Number)],StandardShader.prototype,"emissiveIntensity",void 0),e([s(),a("design:type",r)],StandardShader.prototype,"emissiveMap",void 0),e([s(),a("design:type",r)],StandardShader.prototype,"normalMap",void 0),e([s(),a("design:type",i)],StandardShader.prototype,"normalScale",void 0),e([s({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"sheen",void 0),e([s(),a("design:type",t)],StandardShader.prototype,"sheenColor",void 0),e([s(),a("design:type",r)],StandardShader.prototype,"sheenColorMap",void 0),e([s({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"sheenRoughness",void 0),e([s(),a("design:type",r)],StandardShader.prototype,"sheenRoughnessMap",void 0),e([s(),a("design:type",Number)],StandardShader.prototype,"anisotropyRotation",void 0),e([s(),a("design:type",r)],StandardShader.prototype,"anisotropyMap",void 0),e([s({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"anisotropy",void 0),e([s(),a("design:type",r)],StandardShader.prototype,"envMap",void 0),e([s({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"envMapIntensity",void 0),e([s(),a("design:type",r)],StandardShader.prototype,"heightMap",void 0),e([s(),a("design:type",Number)],StandardShader.prototype,"heightScale",void 0),e([s(),a("design:type",Boolean)],StandardShader.prototype,"vertexColor",void 0),e([s(),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}/*
|
|
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,colorToNormal as d,NodeShaderMaterial as h,normalize as m,Sampler2DNode as v,standardMaterial as u,UniformSampler2d as c,uniformFloat as y,uniformVec2 as g,uniformVec3 as f,varying as S,varyingAttributes as M,varyingTransformed as x,vec2 as b}from"three-shader-graph";import{parallaxOcclusionMapping as C}from"../../shader-nodes/pom.js";const w=new r,O=new i(1,1);export class StandardShader extends s{constructor(){super(...arguments),this.color=new t("#FFFFFF"),this.map=new c("map",w),this.opacity=1,this.alphaMap=new c("alphaMap",w),this.roughness=1,this.roughnessMap=new c("roughnessMap",w),this.metalness=0,this.metalnessMap=new c("metalnessMap",w),this.lightMap=new c("lightMap",w),this.aoMap=new c("aoMap",w),this.emissiveMap=new c("emissiveMap",w),this.normalMap=new c("normalMap",w),this.normalScale=new i(1,1),this.sheen=0,this.sheenColor=new t("#FFFFFF"),this.sheenColorMap=new c("sheenColorMap",w),this.sheenRoughness=.5,this.sheenRoughnessMap=new c("sheenRoughnessMap",w),this.anisotropyMap=new c("anisotropyMap",w),this.anisotropy=0,this.heightMap=new c("heightMap",w),this.vertexColor=!1,this.uvScale=O}build(){let e=!1;const a=g("uvScale",this.uvScale??O);let n=M.uv.multiply(a);T(this.heightMap)&&0!==(this.heightScale??.05)&&(n=C(n,this.heightMap,y("heightScale",this.heightScale??.05)));let r=y("opacity",this.opacity),s=f("color",(new p.Vector3).setFromColor(this.color)).rgb;if(T(this.map)){const e=this.map.sample(n);s=s.multiply(e.rgb),r=r.multiply(e.a)}T(this.alphaMap)&&(r=r.multiply(this.alphaMap.sample(n).r)),!0===this.vertexColor&&(s=s.multiply(S(l.color.rgb)));let o=null,v=null;T(this.aoMap)&&(v=y("aoMapIntensity",this.aoMapIntensity??1),o=this.aoMap.sample(n).r);let c=y("roughness",this.roughness??1);T(this.roughnessMap)&&(c=c.multiply(this.roughnessMap.sample(n).g),e=!0);let w=y("metalness",this.metalness??0);T(this.metalnessMap)&&(w=w.multiply(this.metalnessMap.sample(n).b),e=!0);let N=null;T(this.lightMap)&&(N=this.lightMap.sample(n).rgb.multiplyScalar(y("lightMapIntensity",this.lightMapIntensity??1)));let F=f("emissive",(new p.Vector3).setFromColor(this.emissive??new t(0))).rgb;T(this.emissiveMap)&&(F=F.multiply(this.emissiveMap.sample(n).rgb));const L=y("emissiveIntensity",this.emissiveIntensity??1);let R=x.normal;if(T(this.normalMap)){const e=y("normalScale",this.normalScale?.x??1);R=d(this.normalMap.sample(n),e)}let P=null,D=null;if((this.sheen??0)>0){const e=y("sheen",this.sheen??0);P=f("sheenColor",(new p.Vector3).setFromColor(this.sheenColor??new t(16777215))).rgb.multiplyScalar(e),T(this.sheenColorMap)&&(P=P.multiply(this.sheenColorMap.sample(n).rgb)),D=y("sheenRoughness",this.sheenRoughness??.5),T(this.sheenRoughnessMap)&&(D=D.multiply(this.sheenRoughnessMap.sample(n).a))}(this.roughness<1||this.metalness>0)&&(e=!0);let B=null,_=null;if((this.anisotropy??0)>0&&(B=y("anisotropy",this.anisotropy??0),_=g("anisotropyDirection",(I=this.anisotropyRotation??0,new i(Math.cos(I),Math.sin(I)))),T(this.anisotropyMap))){const e=this.anisotropyMap.sample(n),a=m(b(e.r,e.g).multiplyScalar(2).subtract(b(1,1)));_=b(_.x.multiply(a.x).subtract(_.y.multiply(a.y)),_.y.multiply(a.x).add(_.x.multiply(a.y))),B=B.multiply(e.b)}var I;const V=new h({color:u({color:s.rgba(r),roughness:c,metalness:w,ambientOcclusion:o,ambientOcclusionIntensity:v,emissive:F,emissiveIntensity:L,normal:R,bakedLight:N,sheenColor:P,sheenRoughness:D,anisotropy:B,anisotropyDirection:_,specular:e}),opacity:r,roughness:c,normal:R,emissive:F.multiplyScalar(L),envMap:this.envMap});return null!=this.envMap&&(V.uniforms.envMapIntensity={value:this.envMapIntensity??1}),V.vertexColors=!0===this.vertexColor,V}}function T(e){return null!=e&&(!(e instanceof c)||null!=e.value&&e.value!==w)}e([o(),a("design:type",t)],StandardShader.prototype,"color",void 0),e([o({label:"Color Map"}),a("design:type",v)],StandardShader.prototype,"map",void 0),e([o({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"opacity",void 0),e([o(),a("design:type",v)],StandardShader.prototype,"alphaMap",void 0),e([o({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"roughness",void 0),e([o(),a("design:type",v)],StandardShader.prototype,"roughnessMap",void 0),e([o({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"metalness",void 0),e([o(),a("design:type",v)],StandardShader.prototype,"metalnessMap",void 0),e([o(),a("design:type",v)],StandardShader.prototype,"lightMap",void 0),e([o(),a("design:type",Number)],StandardShader.prototype,"lightMapIntensity",void 0),e([o(),a("design:type",v)],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",v)],StandardShader.prototype,"emissiveMap",void 0),e([o(),a("design:type",v)],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",v)],StandardShader.prototype,"sheenColorMap",void 0),e([o({range:[0,1]}),a("design:type",Number)],StandardShader.prototype,"sheenRoughness",void 0),e([o(),a("design:type",v)],StandardShader.prototype,"sheenRoughnessMap",void 0),e([o(),a("design:type",Number)],StandardShader.prototype,"anisotropyRotation",void 0),e([o(),a("design:type",v)],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",v)],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}/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import { Color, Material,
|
|
1
|
+
import { Color, Material, Vector2 } from 'three';
|
|
2
2
|
import { Shader } from '../shader.js';
|
|
3
|
+
import { Sampler2DNode } from 'three-shader-graph';
|
|
3
4
|
export declare class ToonShader extends Shader {
|
|
4
5
|
color: Color;
|
|
5
6
|
opacity: number;
|
|
6
7
|
intensity: number;
|
|
7
|
-
map?:
|
|
8
|
+
map?: Sampler2DNode;
|
|
8
9
|
lightSteps: number[];
|
|
9
|
-
lightMap?:
|
|
10
|
+
lightMap?: Sampler2DNode;
|
|
10
11
|
lightMapIntensity?: number;
|
|
11
|
-
normalMap?:
|
|
12
|
+
normalMap?: Sampler2DNode;
|
|
12
13
|
normalScale?: Vector2;
|
|
13
|
-
aoMap?:
|
|
14
|
+
aoMap?: Sampler2DNode;
|
|
14
15
|
aoMapIntensity?: number;
|
|
15
|
-
alphaMap?:
|
|
16
|
+
alphaMap?: Sampler2DNode;
|
|
16
17
|
emissive?: Color;
|
|
17
18
|
emissiveIntensity?: number;
|
|
18
|
-
emissiveMap?:
|
|
19
|
+
emissiveMap?: Sampler2DNode;
|
|
19
20
|
vertexColor?: boolean;
|
|
21
|
+
uvScale: Vector2;
|
|
20
22
|
build(): Material;
|
|
21
23
|
}
|
|
22
24
|
//# sourceMappingURL=toon-shader.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{__decorate as e,__metadata as t}from"tslib";import{Color as o,DataTexture as i,
|
|
1
|
+
import{__decorate as e,__metadata as t}from"tslib";import{Color as o,DataTexture as i,RedFormat as s,Texture as a,Vector2 as n}from"three";import{Parameter as r}from"../parameter.js";import{Shader as p}from"../shader.js";import*as l from"three";import{attributes as h,colorToNormal as m,NodeShaderMaterial as y,Sampler2DNode as d,toonMaterial as g,UniformSampler2d as v,uniformFloat as c,uniformVec3 as u,varying as S,varyingAttributes as M,varyingTransformed as b,vec2 as T}from"three-shader-graph";const w=new a;export class ToonShader extends p{constructor(){super(...arguments),this.color=new o("#FFFFFF"),this.opacity=1,this.intensity=1,this.map=new v("map",w),this.lightSteps=[],this.lightMap=new v("lightMap",w),this.normalMap=new v("normalMap",w),this.normalScale=new n(1,1),this.aoMap=new v("aoMap",w),this.alphaMap=new v("alphaMap",w),this.emissiveMap=new v("emissiveMap",w),this.vertexColor=!1,this.uvScale=new n(1,1)}build(){let e;if(null!=this.lightSteps&&this.lightSteps.length>0){const t=new Uint8Array(this.lightSteps.length);for(let e=0;e<t.length;e++)t[e]=255*this.lightSteps[e];e=new i(t,t.length,1,s),e.needsUpdate=!0}const t=function(e,t){if(null==t||1===t.x&&1===t.y)return e;return e.multiply(T(t))}(M.uv,this.uvScale);let a=c("opacity",this.opacity),n=u("color",(new l.Vector3).setFromColor(this.color)).rgb.multiplyScalar(c("intensity",this.intensity));if(f(this.map)){const e=this.map.sample(t);n=n.multiply(e.rgb),a=a.multiply(e.a)}f(this.alphaMap)&&(a=a.multiply(this.alphaMap.sample(t).r)),!0===this.vertexColor&&(n=n.multiply(S(h.color.rgb)));let r=null;f(this.lightMap)&&(r=this.lightMap.sample(t).rgb.multiplyScalar(c("lightMapIntensity",this.lightMapIntensity??1)));let p=b.normal;f(this.normalMap)&&(p=m(this.normalMap.sample(t),c("normalScale",this.normalScale?.x??1)));let d=null;f(this.aoMap)&&(d=this.aoMap.sample(t).r);const v=c("aoMapIntensity",this.aoMapIntensity??1);let w=u("emissive",(new l.Vector3).setFromColor(this.emissive??new o(0))).rgb;f(this.emissiveMap)&&(w=w.multiply(this.emissiveMap.sample(t).rgb));const I=c("emissiveIntensity",this.emissiveIntensity??1),x=new y({color:g({color:n.rgba(a),gradientMap:e,bakedLight:r,normal:p,ambientOcclusion:d,ambientOcclusionIntensity:v,emissive:w,emissiveIntensity:I}),opacity:a,normal:p,emissive:w.multiplyScalar(I)});return x.vertexColors=!0===this.vertexColor,x}}function f(e){return null!=e&&(!(e instanceof v)||null!=e.value&&e.value!==w)}e([r(),t("design:type",o)],ToonShader.prototype,"color",void 0),e([r({range:[0,1]}),t("design:type",Number)],ToonShader.prototype,"opacity",void 0),e([r({range:[0,10]}),t("design:type",Number)],ToonShader.prototype,"intensity",void 0),e([r(),t("design:type",d)],ToonShader.prototype,"map",void 0),e([r({type:Number,array:!0,range:[0,1]}),t("design:type",Array)],ToonShader.prototype,"lightSteps",void 0),e([r(),t("design:type",d)],ToonShader.prototype,"lightMap",void 0),e([r(),t("design:type",Number)],ToonShader.prototype,"lightMapIntensity",void 0),e([r(),t("design:type",d)],ToonShader.prototype,"normalMap",void 0),e([r(),t("design:type",n)],ToonShader.prototype,"normalScale",void 0),e([r(),t("design:type",d)],ToonShader.prototype,"aoMap",void 0),e([r(),t("design:type",Number)],ToonShader.prototype,"aoMapIntensity",void 0),e([r(),t("design:type",d)],ToonShader.prototype,"alphaMap",void 0),e([r(),t("design:type",o)],ToonShader.prototype,"emissive",void 0),e([r({range:[0,10]}),t("design:type",Number)],ToonShader.prototype,"emissiveIntensity",void 0),e([r(),t("design:type",d)],ToonShader.prototype,"emissiveMap",void 0),e([r(),t("design:type",Boolean)],ToonShader.prototype,"vertexColor",void 0),e([r(),t("design:type",n)],ToonShader.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,15 +1,16 @@
|
|
|
1
1
|
import { Color, Material, Texture, Vector2 } from 'three';
|
|
2
2
|
import { Shader } from '../shader.js';
|
|
3
|
+
import { Sampler2DNode } from 'three-shader-graph';
|
|
3
4
|
export declare class UnlitShader extends Shader {
|
|
4
5
|
color: Color;
|
|
5
6
|
opacity: number;
|
|
6
7
|
intensity: number;
|
|
7
|
-
map?:
|
|
8
|
-
lightMap?:
|
|
8
|
+
map?: Sampler2DNode;
|
|
9
|
+
lightMap?: Sampler2DNode;
|
|
9
10
|
lightMapIntensity?: number;
|
|
10
|
-
aoMap?:
|
|
11
|
+
aoMap?: Sampler2DNode;
|
|
11
12
|
aoMapIntensity?: number;
|
|
12
|
-
alphaMap?:
|
|
13
|
+
alphaMap?: Sampler2DNode;
|
|
13
14
|
envMap?: Texture;
|
|
14
15
|
vertexColor?: boolean;
|
|
15
16
|
uvScale: Vector2;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{__decorate as t,__metadata as e}from"tslib";import{Color as i,
|
|
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 n,NodeShaderMaterial as l,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";const M=new o;export class UnlitShader extends a{constructor(){super(...arguments),this.color=new i("#FFFFFF"),this.opacity=1,this.intensity=1,this.map=new y("map",M),this.lightMap=new y("lightMap",M),this.aoMap=new y("aoMap",M),this.alphaMap=new y("alphaMap",M),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(S(this.map)){const o=this.map.sample(t);i=i.multiply(o.rgb),e=e.multiply(o.a)}if(S(this.alphaMap)&&(e=e.multiply(this.alphaMap.sample(t).r)),!0===this.vertexColor&&(i=i.multiply(u(n.color.rgb))),S(this.lightMap)&&(i=i.add(this.lightMap.sample(t).rgb.multiplyScalar(d("lightMapIntensity",this.lightMapIntensity??1)))),S(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 l({color:s(i,e),opacity:e,emissive:i,envMap:this.envMap});return o.vertexColors=!0===this.vertexColor,o}}function S(t){return null!=t&&(!(t instanceof y)||null!=t.value&&t.value!==M)}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
|
*/
|
package/dist/shader-nodes/pom.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{attributes as e,dFdx as t,dFdy as n,cross as r,float as a,normalize as i,transformed as f,uniforms as
|
|
1
|
+
import{attributes as e,dFdx as t,dFdy as n,cross as r,float as a,normalize as i,transformed as f,uniforms as v,varying as l,Vec2Node as p,vec4 as o,vec3 as s,abs as u,select as d,UniformSampler2dArraySlice as m}from"three-shader-graph";import{glslFunction as c}from"./glsl-node";export function computeTangent(e,a,f){const v=t(a),l=n(a),p=t(f),s=n(f),u=r(l,e),d=r(e,v),m=u.multiplyScalar(p.x).add(d.multiplyScalar(s.x));return o(i(m),1)}export function fallbackTangent(e){const t=u(e.y),n=d(t.lt(a(.999)),s(0,1,0),s(1,0,0));return i(r(n,e))}const S=e.normal,h=l(i(v.normalMatrix.multiplyVec(S))),x=l(f.mvPosition.xyz.multiplyScalar(-1));export function parallaxOcclusionMapping(e,t,n,r=8,i=24){if(function(e){return e instanceof m||"sampler2DArray"===e.constructor.typeName}(t)){const f=t.index??a(0);return c(p,{uv:e,heightScale:n,heightMap:t,heightMapLayer:f,minLayers:a(r),maxLayers:a(i),vN:h,vViewPosition:x},"\n vec3 N = normalize(vN);\n vec3 viewDir = normalize(vViewPosition);\n\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, N);\n vec3 dp1perp = cross(N, 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, N);\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 nNumSamples = mix(float(maxLayers), float(minLayers), dot(-viewDir, N));\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 < 24; nCurrSample++) {\n if (float(nCurrSample) > nNumSamples) break;\n\n fCurrSampledHeight = texture(heightMap, vec3(uv + vCurrOffset, heightMapLayer)).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 ")}return c(p,{uv:e,heightScale:n,heightMap:t,minLayers:a(r),maxLayers:a(i),vN:h,vViewPosition:x},"\n vec3 N = normalize(vN);\n vec3 viewDir = normalize(vViewPosition);\n\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, N);\n vec3 dp1perp = cross(N, dp1);\n vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;\n vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;\n \n // Construct TBN and transform view direction to tangent space\n float invmax = inversesqrt(max(dot(T,T), dot(B,B)));\n mat3 tbn = mat3(T * invmax, B * invmax, N);\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 nNumSamples = mix(float(maxLayers), float(minLayers), dot(-viewDir, N));\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 < 24; 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 ")}/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import"reflect-metadata";import{afterEach as e,expect as t,test as o,vi as n}from"vitest";n.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&n.fn()),e[t]),set:(e,t,o)=>(e[t]=o,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),n.mock("../gameplay/services/render.js",()=>({ViewController:class{}})),n.mock("../gameplay/services/physics/physics-system.js",()=>({PhysicsSystem:class{},RayTestResult:class{}})),n.mock("../gameplay/services/world.js",()=>({World:class{}})),n.mock("@hology/nebula",()=>({ease:{easeInOutCubic:e=>e}}));import{Subject as a}from"rxjs";import{Container as i}from"typedi";import{Object3D as r,Ray as s,Scene as c,Vector3 as m}from"three";import{BaseActor as p}from"../gameplay/actors/actor.js";import{FirstPersonCameraComponent as l}from"../gameplay/actors/camera/first-person-camera-component.js";import{ThirdPersonCameraComponent as d}from"../gameplay/actors/camera/third-person-camera-component.js";import{PhysicsSystem as y}from"../gameplay/services/physics/physics-system.js";import{ViewController as u}from"../gameplay/services/render.js";import{World as v}from"../gameplay/services/world.js";function f(e=new B){const t=h(),o=new l;return o.actor=e,{camera:o,actor:e,element:t}}function h(){const e=document.createElement("div"),t=document.createElement("canvas");return e.appendChild(t),Object.defineProperty(e,"clientWidth",{configurable:!0,value:1280}),Object.defineProperty(e,"clientHeight",{configurable:!0,value:720}),i.set(u,{htmlElement:e,setCamera:n.fn(),onUpdate:()=>new a,onLateUpdate:()=>new a}),i.set(v,{scene:new c}),i.set(y,{sphereCast:n.fn()}),e}function w(){Object.defineProperty(document.body,"requestPointerLock",{configurable:!0,value:n.fn()}),Object.defineProperty(document,"pointerLockElement",{configurable:!0,value:null})}function g(e,t){return e.mock.calls.filter(e=>e[0]===t).length}e(()=>{n.restoreAllMocks(),i.reset()}),o("first-person camera clamps pitch input",()=>{const{camera:e}=f();e.rotationInput.rotateX(Math.PI),t(e.rotationInput.rotation.x).toBeCloseTo(Math.PI/2-.01),e.rotationInput.rotateX(2*-Math.PI),t(e.rotationInput.rotation.x).toBeCloseTo(-Math.PI/2+.01)}),o("first-person camera follows actor eye offset",async()=>{const e=new B;e.position.set(1,2,3),e.rotation.y=Math.PI/2,e.object.updateMatrixWorld(!0);const{camera:o}=f(e);o.eyeHeight=1.5,o.offsetZ=.25,o.autoActivate=!1,await o.onInit(),o.updateCameraTransform(),t(o.camera.position.x).toBeCloseTo(1.25),t(o.camera.position.y).toBeCloseTo(3.5),t(o.camera.position.z).toBeCloseTo(3)}),o("first-person aim helpers write to caller-provided objects",async()=>{const e=new B;e.position.set(0,1,0);const{camera:o}=f(e);o.autoActivate=!1,await o.onInit();const n=new m,a=new m,i=new s;t(o.getAimOrigin(n)).toBe(n),t(o.getAimDirection(a)).toBe(a),t(o.getAimRay(i)).toBe(i),t(i.origin).toEqual(n),t(i.direction).toEqual(a),t(a.x).toBeCloseTo(0),t(a.y).toBeCloseTo(0),t(a.z).toBeCloseTo(1),t(a.length()).toBeCloseTo(1)}),o("first-person camera pitch input tilts aim downward when positive",async()=>{const{camera:e}=f();e.autoActivate=!1,await e.onInit(),e.rotationInput.rotateX(.25),e.updateCameraTransform();const o=e.getAimDirection(new m);t(o.y).toBeLessThan(0),t(o.z).toBeGreaterThan(0),t(o.length()).toBeCloseTo(1)}),o("first-person hidden objects restore their previous visibility",()=>{const{camera:e}=f(),o=new r,n=new r;n.visible=!1,e.hideObjects(o,n),t(o.visible).toBe(!1),t(n.visible).toBe(!1),e.restoreHiddenObjects(),t(o.visible).toBe(!0),t(n.visible).toBe(!1)}),o("first-person activation is idempotent",async()=>{const{camera:e,element:o}=f();e.autoActivate=!1,await e.onInit(),w();const a=n.spyOn(o,"addEventListener"),i=n.spyOn(o,"removeEventListener"),r=n.spyOn(document,"addEventListener"),s=n.spyOn(document,"removeEventListener");e.activate(),e.activate(),t(g(a,"pointerdown")).toBe(1),t(g(a,"keydown")).toBe(1),t(g(r,"pointerlockchange")).toBe(1),e.deactivate(),e.deactivate(),t(g(i,"pointerdown")).toBe(1),t(g(i,"keydown")).toBe(1),t(g(s,"pointerlockchange")).toBe(1)}),o("third-person activation remains idempotent",async()=>{const e=new B,o=h(),a=new d;a.actor=e,a.autoActivate=!1,await a.onInit(),w();const i=n.spyOn(o,"addEventListener"),r=n.spyOn(o,"removeEventListener"),s=n.spyOn(document,"addEventListener"),c=n.spyOn(document,"removeEventListener");a.activate(),a.activate(),t(g(i,"pointerdown")).toBe(1),t(g(i,"keydown")).toBe(1),t(g(s,"pointerlockchange")).toBe(1),a.deactivate(),a.deactivate(),t(g(r,"pointerdown")).toBe(1),t(g(r,"keydown")).toBe(1),t(g(c,"pointerlockchange")).toBe(1)});class B extends p{constructor(){super(),this.__isInitialised=!0}}/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as s,it as t,vi as a}from"vitest";import{RuntimeAssetsService as n}from"../scene/runtime-asset-service.js";e("RuntimeAssetsService",()=>{t("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"}],t={getAssets:a.fn(async()=>(await new Promise(e=>setTimeout(e,5)),e)),getAsset:a.fn(),getScenes:a.fn(),getScene:a.fn(),getSceneData:a.fn()},i=new n(t);await Promise.all([i.getAssets(),i.getAssets()]),await i.getAssets(),s(t.getAssets).toHaveBeenCalledTimes(1)})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{expect as e,test as t,vi as
|
|
1
|
+
import{expect as e,test as t,vi as a}from"vitest";import{Vector3 as r}from"three";import{rgb as o,UniformSampler2d as p}from"three-shader-graph";a.mock("three/examples/jsm/Addons.js",()=>({BufferGeometryUtils:{},ConvexHull:class{},SkeletonUtils:{clone:e=>e}}));import{convertConfiguredParamsToRuntimeTypes as n,convertConfiguredParamValueToRuntimeType as s,inferRuntimeSerializedParamTypeHint as i}from"../scene/custom-param-runtime-types.js";import{SerializedParamType as c}from"../scene/model.js";class m{constructor(){this.tint=o("white"),this.direction=new r(1,1,1),this.map=new p("map")}}t("runtime parameter type inference uses class instance default values",()=>{e(i((new m).tint)?.type).toBe(c.RgbNode),e(i((new m).direction)?.type).toBe(c.Vector3),e(i((new m).map)?.type).toBe(c.Sampler2DNode)}),t("configured params can be retargeted to inferred runtime types",()=>{const t=new m,{params:a,skipped:r}=n({tint:{type:c.Color,value:"#336699"},direction:{type:c.Number,value:.5}},{parameterTarget:t});e(r).toEqual([]),e(a.tint).toMatchObject({type:c.RgbNode,value:"#336699"}),e(a.direction).toMatchObject({type:c.Vector3,value:[.5,.5,.5]})}),t("stored texture params can be retargeted to sampler node params",()=>{const{params:t,skipped:a}=n({map:{type:c.Texture,value:"texture-asset-id"}},{parameterTarget:new m});e(a).toEqual([]),e(t.map).toMatchObject({type:c.Sampler2DNode,value:"texture-asset-id"})}),t("configured params are normalized when stored type changed but value shape did not",()=>{const t=s({type:c.Vector3,value:.5},{type:c.Vector3});e(t).toMatchObject({type:c.Vector3,value:[.5,.5,.5]})}),t("configured params that cannot be safely coerced are skipped",()=>{const t=new m,a=s({type:c.Texture,value:"texture-asset-id"},i(t.direction));e(a).toBeNull()}),t("runtime type conversion caches extracted parameter metadata",()=>{const t=a.fn(()=>[{name:"direction",type:r,options:{}}]),o=a.fn(()=>c.Vector3),p={direction:{type:c.Number,value:.5}};n(p,{parameterType:m,extractPropertyParameters:t,toSerializedParamType:o}),n(p,{parameterType:m,extractPropertyParameters:t,toSerializedParamType:o}),e(t).toHaveBeenCalledTimes(1),e(o).toHaveBeenCalledTimes(1)}),t("runtime type conversion can be disabled",()=>{const t={direction:{type:c.Number,value:.5}},{params:a,skipped:r}=n(t,{parameterTarget:new m,applyRuntimeParamTypeInference:!1});e(a).toBe(t),e(a.direction).toMatchObject({type:c.Number,value:.5}),e(r).toEqual([])});/*
|
|
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 s,it as
|
|
1
|
+
import{describe as e,expect as s,it as a,vi as t}from"vitest";import*as r from"three";import{createMaterialSubTrack as o,createPostProcessClipInstance as n,createPostProcessTrack as i,createPropertyKeyframe as c,createSequenceData as l}from"../effects/sequence/sequence-data.js";import{createDefaultPostProcessShaderGraphDocument as p,shaderGraphPostProcessWeightUniformName as d}from"../shader/graph/index.js";import{shaderParameterUniformName as u}from"../shader/parameter.js";import{UniformFloatNode as f}from"../shader-nodes/index.js";import{SerializedParamType as m}from"../scene/model.js";async function h(e,s){const{SequencePlayer:a}=await import("../effects/sequence/sequence-player.js"),t=new a;return t.setWorld({scene:new r.Scene}),t.setViewController(e),t.setAssetLoader(s),t}function y(e,s,a){const t=l();t.duration=Math.max(2,s+a+.25);const r=i(),o=n(s,a);return o.shaderGraphAssetId=e,r.clips.push(o),t.tracks.push(r),{sequence:t,track:r,clip:o}}function v(e,s={}){return{clearCacheById:t.fn(),getShaderGraphByAssetId:t.fn().mockResolvedValue(e),prepareShaderGraphParameters:t.fn().mockResolvedValue(s)}}function w(){const e=[],s=t.fn((s,a={})=>{const r={material:s,enabled:a.enabled??!0,priority:a.priority??0,stage:a.stage??"beforeOutput",dispose:t.fn()};return e.push(r),r});return{handles:e,addPostProcessEffect:s,clearCameraShakeContributionsForOwner:t.fn(),releaseCameraOverridesForOwner:t.fn(),pushCameraOverride:t.fn(),setCameraShakeContribution:t.fn()}}function C(e){return e.material.uniforms[d].value}async function g(e=6){for(let s=0;s<e;s++)await Promise.resolve()}t.mock("../gameplay/actors/builtin/index.js",()=>({builtInActors:{},default:{}})),t.mock("../gameplay/actors/index.js",()=>({CharacterAnimationComponent:class{},CharacterMovementComponent:class{}})),t.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),t.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),t.mock("../effects/vfx/vfx-service.js",()=>({VfxService:class{}})),t.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,s)=>(s in e||(e[s]=("string"!=typeof s||!s.startsWith("is"))&&t.fn()),e[s]),set:(e,s,a)=>(e[s]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),e("sequence post-process tracks",()=>{a("registers active post-process clips with the view controller",async()=>{const e=p(),a=w(),t=await h(a,v(e)),{clip:r,sequence:o}=y("graph-asset",0,1);t.load(o),t.play(),await g(),s(a.addPostProcessEffect).toHaveBeenCalledTimes(1),s(a.addPostProcessEffect).toHaveBeenCalledWith(s.anything(),s.objectContaining({stage:"beforeOutput",priority:0,enabled:!0})),s(a.handles[0].material.uniforms[d]).toBeTruthy(),s(r.shaderGraphAssetId).toBe("graph-asset")}),a("updates post-process blend weight from clip fades",async()=>{const e=p(),a=w(),t=await h(a,v(e)),{clip:r,sequence:o}=y("graph-asset",0,2);r.fadeInDuration=.5,r.fadeOutDuration=.5,t.load(o),t.play(),await g(),t.seek(.25),s(C(a.handles[0])).toBeCloseTo(.5),t.seek(1),s(C(a.handles[0])).toBeCloseTo(1),t.seek(1.75),s(C(a.handles[0])).toBeCloseTo(.5)}),a("animates shader graph parameters on active post-process clips",async()=>{const e=(a="intensity",{...p(),parameters:[{id:a,name:a,type:"float",defaultValue:0}],nodes:[{id:a,kind:"input.parameter",label:"Parameter",params:{parameter:a}}],edges:[],outputs:{color:{input:{nodeId:a,port:"value"}}}});var a;const t=u("intensity"),r=w(),n=await h(r,v(e,{intensity:new f(t,0,void 0,!1)})),{sequence:i,track:l}=y("graph-asset",0,1),d=o({materialMatchMode:"parameterName",materialAssetId:null,parameterName:"intensity",uniformName:t,uniformType:m.FloatNode,valueType:m.Number});d.keyframes.push(c(0,{type:m.Number,value:0}),c(1,{type:m.Number,value:1})),l.subTracks.push(d),n.load(i),n.play(),await g(),n.seek(.5),s(r.handles[0].material.uniforms[t].value).toBeCloseTo(.5)}),a("disposes post-process handles when clips exit and when stopped",async()=>{const e=p(),a=w(),t=await h(a,v(e)),{sequence:r}=y("graph-asset",0,1);t.load(r),t.play(),await g();const o=a.handles[0];t.seek(1.1),s(o.dispose).toHaveBeenCalledTimes(1),t.seek(.1),await g();const n=a.handles[1];t.stop(),s(n.dispose).toHaveBeenCalledTimes(1)}),a("does not register stale async post-process loads after stop",async()=>{const e=function(){let e;return{promise:new Promise(s=>{e=s}),resolve:e}}(),a=w(),t=await h(a,v(e.promise)),{sequence:r}=y("graph-asset",0,1);t.load(r),t.play(),t.stop(),e.resolve(p()),await g(),s(a.addPostProcessEffect).not.toHaveBeenCalled()})});/*
|
|
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 e,test as t,vi as o}from"vitest";import{DoubleSide as a,FrontSide as r,Texture as n}from"three";o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&o.fn()),e[t]),set:(e,t,o)=>(e[t]=o,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),o.mock("three/examples/jsm/Addons.js",async e=>({...await e(),ConvexHull:class{},SkeletonUtils:{clone:e=>e},RectAreaLightHelper:class{}})),o.mock("three-shader-graph",async e=>{const t=await e();return{...t,rgb:(e,o,a)=>null==o&&null==a&&!0===e?.isColor&&"function"==typeof e.getHexString?t.rgb("#"+e.getHexString()):t.rgb(e,o,a)}}),o.mock("../rendering.js",()=>({RenderingView:class{constructor(){this._id="test",this.csm={setupMaterial:o.fn()}}getEnvTexture(e){return e}},setRenderingPaused:o.fn()}));import{materialFromAsset as s,prepareCustomParamsFromShaderGraph as d,prepareShaderGraphParameters as p}from"../scene/materializer.js";import{SerializedParamType as u}from"../scene/model.js";import{AssetsProvider as l}from"../scene/assets-provider.js";import{AssetResourceLoader as i}from"../scene/asset-resource-loader.js";import{buildShaderGraphMaterial as c,buildShaderGraphPostProcessMaterial as m,compileShaderGraph as v,compileShaderGraphPreview as f,createDefaultPostProcessShaderGraphDocument as h,createDefaultShaderGraphDocument as y,createDefaultVfxShaderGraphDocument as g,shaderGraphPostProcessWeightUniformName as I,shaderGraphParametersToPropertyParameters as b}from"../shader/graph/index.js";import{NodeShaderMaterial as B,textureSampler2d as k}from"../shader-nodes/index.js";import{sceneMapUniformName as x}from"../shader-nodes/scene-sample.js";function T(e,t,o){return{version:1,target:"surface",parameters:[],nodes:e,edges:t,outputs:o}}function w(e,t){return{id:e,name:e,type:"material",material:{type:"shaderGraph",side:r,params:{},shaderParams:{},shaderGraph:t}}}t("graph validation rejects missing nodes, invalid ports, and incompatible edges",()=>{const t=y();t.outputs.color={input:{nodeId:"missing",port:"value"}},e(()=>v(t)).toThrow(/Missing shader graph node/);const o=y();o.outputs.color={input:{nodeId:"base-color",port:"missing"}},e(()=>v(o)).toThrow(/does not have output/);const a=T([{id:"a",kind:"constant.vec2",params:{value:[1,1]}},{id:"b",kind:"constant.vec3",params:{value:[1,1,1]}},{id:"add",kind:"math.add"}],[{from:{nodeId:"a",port:"value"},to:{nodeId:"add",port:"a"}},{from:{nodeId:"b",port:"value"},to:{nodeId:"add",port:"b"}}],{color:{input:{nodeId:"add",port:"value"}}});e(()=>v(a)).toThrow(/Cannot add vec2 and vec3/)}),t("surface graph compiles to NodeShaderOutput",()=>{const t=v(y("surface"));e(t.output.color).toBeTruthy(),e(t.output.transparent).toBe(!1)}),t("inline literal inputs compile without separate constant nodes",()=>{const t=T([{id:"uv",kind:"builtin.uv"},{id:"scale-uv",kind:"math.multiply",inputs:{b:{value:2,valueType:"float"}}},{id:"rotate",kind:"uv.rotate",inputs:{angle:{value:.5,valueType:"float"},center:{value:[.25,.75],valueType:"vec2"}}},{id:"translate",kind:"transform.translate",inputs:{offset:{value:[0,1,0],valueType:"vec3"}}}],[{from:{nodeId:"uv",port:"value"},to:{nodeId:"scale-uv",port:"a"}},{from:{nodeId:"scale-uv",port:"value"},to:{nodeId:"rotate",port:"uv"}}],{color:{input:{nodeId:"rotate",port:"value"}},transform:{input:{nodeId:"translate",port:"value"}}}),o=v(t);e(o.nodes.get("scale-uv")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("rotate")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("translate")?.outputs.value.type).toBe("mat4"),e(o.output.color).toBeTruthy(),e(o.output.transform).toBeTruthy()}),t("sprite graph compiles with sprite-compatible material output",()=>{const t=y("sprite"),o=v(t);e(o.output.color).toBeTruthy(),e(o.output.transparent).toBe(!0)}),t("default VFX shader graphs compile mapped targets and build non-sprite materials",()=>{for(const t of["sprite","surface","decal","trail"]){const o=g(t);if(e(v(o).output.color).toBeTruthy(),"sprite"===t)continue;const a=c(o,{trailBillboard:"trail"===t});e(a.isMaterial).toBe(!0)}}),t("graph material options apply to supported material targets",()=>{const t=y("surface");t.materialOptions={side:"double",transparent:!0};const o=c(t);e(o.side).toBe(a),e(o.transparent).toBe(!0);const r=y("decal");r.materialOptions={transparent:!0};const n=c(r);e(n.transparent).toBe(!0)}),t("standard graph compiles extended material outputs and vertex transform",()=>{const t=T([{id:"metalness",kind:"constant.float",params:{value:.8}},{id:"ao",kind:"constant.float",params:{value:.5}},{id:"sheen",kind:"constant.vec3",params:{value:[.1,.2,.3]}},{id:"offset",kind:"constant.vec3",params:{value:[0,1,0]}},{id:"translate",kind:"transform.translate"}],[{from:{nodeId:"offset",port:"value"},to:{nodeId:"translate",port:"offset"}}],{metalness:{input:{nodeId:"metalness",port:"value"}},ambientOcclusion:{input:{nodeId:"ao",port:"value"}},sheenColor:{input:{nodeId:"sheen",port:"value"}},transform:{input:{nodeId:"translate",port:"value"}}}),o=v(t);e(o.output.metalness).toBeTruthy(),e(o.output.ambientOcclusion).toBeTruthy(),e(o.output.sheenColor).toBeTruthy(),e(o.output.transform).toBeTruthy();const a=c(t);e(a.outputMetalness).toBeTruthy(),e(a.outputAmbientOcclusion).toBeTruthy(),e(a.outputTransform).toBeTruthy()}),t("math multiply composes transform matrices",()=>{const t=T([{id:"translate",kind:"transform.translate",inputs:{offset:{value:[0,1,0],valueType:"vec3"}}},{id:"scale",kind:"transform.scale",inputs:{scale:{value:[2,2,2],valueType:"vec3"}}},{id:"compose",kind:"math.multiply"}],[{from:{nodeId:"translate",port:"value"},to:{nodeId:"compose",port:"a"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"compose",port:"b"}}],{transform:{input:{nodeId:"compose",port:"value"}}}),o=v(t);e(o.nodes.get("compose")?.outputs.value.type).toBe("mat4"),e(o.output.transform).toBeTruthy(),e(c(t).outputTransform).toBeTruthy()}),t("matrix input nodes compile as mat4 uniforms",()=>{for(const t of["builtin.viewMatrix","builtin.projectionMatrix","builtin.modelViewMatrix"]){const o=T([{id:"matrix",kind:t}],[],{transform:{input:{nodeId:"matrix",port:"value"}}}),a=v(o);e(a.nodes.get("matrix")?.outputs.value.type).toBe("mat4"),e(a.output.transform).toBeTruthy()}}),t("compose vec4 node compiles from float components",()=>{const t=T([{id:"compose",kind:"compose.vec4",inputs:{x:{value:.1},y:{value:.2},z:{value:.3},w:{value:1}}}],[],{color:{input:{nodeId:"compose",port:"value"}}}),o=v(t);e(o.nodes.get("compose")?.outputs.value.type).toBe("vec4"),e(o.output.color).toBeTruthy()}),t("atan2 node compiles from y and x inputs",()=>{const t=T([{id:"atan2",kind:"math.atan2",inputs:{y:{value:1},x:{value:0}}}],[],{color:{input:{nodeId:"atan2",port:"value"}}}),o=v(t);e(o.nodes.get("atan2")?.outputs.value.type).toBe("float"),e(o.output.color).toBeTruthy()}),t("built-in attribute nodes compile for fragment and transform outputs",()=>{const t=T([{id:"color",kind:"builtin.color"},{id:"normal",kind:"builtin.normal"},{id:"position",kind:"builtin.position"},{id:"worldPosition",kind:"builtin.worldPosition"},{id:"objectPosition",kind:"builtin.objectPosition"},{id:"translate",kind:"transform.translate"}],[{from:{nodeId:"objectPosition",port:"value"},to:{nodeId:"translate",port:"offset"}}],{color:{input:{nodeId:"color",port:"value"}},normal:{input:{nodeId:"normal",port:"value"}},roughness:{input:{nodeId:"position",port:"x"}},transform:{input:{nodeId:"translate",port:"value"}}}),o=v(t);e(o.nodes.get("color")?.outputs.value.type).toBe("rgba"),e(o.nodes.get("normal")?.outputs.value.type).toBe("vec3"),e(o.nodes.get("position")?.outputs.value.type).toBe("vec3"),e(o.nodes.get("worldPosition")?.outputs.value.type).toBe("vec3"),e(o.nodes.get("objectPosition")?.outputs.value.type).toBe("vec3"),e(o.output.color).toBeTruthy(),e(o.output.normal).toBeTruthy(),e(o.output.transform).toBeTruthy(),e(c(t).outputTransform).toBeTruthy()}),t("painted layer mix node compiles color layers and builds a material",()=>{const t=T([{id:"base",kind:"constant.rgba",params:{value:"#224422",alpha:1}},{id:"grass",kind:"constant.rgba",params:{value:"#55aa33",alpha:1}},{id:"stone",kind:"constant.rgba",params:{value:"#777777",alpha:1}},{id:"mix",kind:"layer.mixPainted",params:{mode:"soft",enableNoise:!0,softness:.25,noiseScale:.2,noiseAmount:.4}}],[{from:{nodeId:"base",port:"value"},to:{nodeId:"mix",port:"base"}},{from:{nodeId:"grass",port:"value"},to:{nodeId:"mix",port:"layer1"}},{from:{nodeId:"stone",port:"value"},to:{nodeId:"mix",port:"layer3"}}],{color:{input:{nodeId:"mix",port:"value"}}}),o=v(t);e(o.nodes.get("mix")?.outputs.value.type).toBe("rgba"),e(o.output.color).toBeTruthy(),e(c(t).isMaterial).toBe(!0)}),t("painted layer mix node compiles sparse float layers in hard and soft modes",()=>{for(const t of["hard","soft"]){const o=T([{id:"base",kind:"constant.float",params:{value:.8}},{id:"mud",kind:"constant.float",params:{value:1}},{id:"wet",kind:"constant.float",params:{value:.25}},{id:"mix",kind:"layer.mixPainted",params:{mode:t,enableNoise:!1,softness:.3,noiseScale:.1,noiseAmount:.5}}],[{from:{nodeId:"base",port:"value"},to:{nodeId:"mix",port:"base"}},{from:{nodeId:"mud",port:"value"},to:{nodeId:"mix",port:"layer2"}},{from:{nodeId:"wet",port:"value"},to:{nodeId:"mix",port:"layer8"}}],{roughness:{input:{nodeId:"mix",port:"value"}}}),a=v(o);e(a.nodes.get("mix")?.outputs.value.type).toBe("float"),e(a.output.roughness).toBeTruthy()}}),t("painted layer mix node rejects incompatible layer types",()=>{const t=T([{id:"base",kind:"constant.vec2",params:{value:[0,1]}},{id:"layer",kind:"constant.vec3",params:{value:[1,0,0]}},{id:"mix",kind:"layer.mixPainted"}],[{from:{nodeId:"base",port:"value"},to:{nodeId:"mix",port:"base"}},{from:{nodeId:"layer",port:"value"},to:{nodeId:"mix",port:"layer1"}}],{color:{input:{nodeId:"mix",port:"value"}}});e(()=>v(t)).toThrow(/Cannot mix vec2 and vec3/)}),t("additional math, uv, shape, noise, and effect nodes compile",()=>{const t=T([{id:"uv",kind:"builtin.uv"},{id:"angle",kind:"constant.float",params:{value:.5}},{id:"scale",kind:"constant.float",params:{value:4}},{id:"half",kind:"constant.float",params:{value:.5}},{id:"axis",kind:"constant.vec3",params:{value:[0,1,0]}},{id:"rotatedUv",kind:"uv.rotate"},{id:"twirl",kind:"uv.twirl"},{id:"bulge",kind:"uv.bulge"},{id:"radial",kind:"uv.radial"},{id:"flipbook",kind:"uv.flipbook"},{id:"shape",kind:"shape.roundedRectangle"},{id:"noise",kind:"noise.simplex"},{id:"voronoi",kind:"noise.voronoi2d"},{id:"fresnel",kind:"effect.fresnel"},{id:"sin",kind:"math.sin"},{id:"pow",kind:"math.pow"},{id:"mix",kind:"math.mix"},{id:"rotateAxis",kind:"transform.rotateAxis"}],[{from:{nodeId:"uv",port:"value"},to:{nodeId:"rotatedUv",port:"uv"}},{from:{nodeId:"angle",port:"value"},to:{nodeId:"rotatedUv",port:"angle"}},{from:{nodeId:"rotatedUv",port:"value"},to:{nodeId:"twirl",port:"uv"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"twirl",port:"strength"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"bulge",port:"uv"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"bulge",port:"power"}},{from:{nodeId:"bulge",port:"value"},to:{nodeId:"radial",port:"uv"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"flipbook",port:"uv"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"shape",port:"uv"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"shape",port:"width"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"shape",port:"height"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"shape",port:"radius"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"noise",port:"uv"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"noise",port:"scale"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"voronoi",port:"uv"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"voronoi",port:"scale"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"fresnel",port:"power"}},{from:{nodeId:"radial",port:"angle"},to:{nodeId:"sin",port:"value"}},{from:{nodeId:"noise",port:"value"},to:{nodeId:"pow",port:"a"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"pow",port:"b"}},{from:{nodeId:"shape",port:"value"},to:{nodeId:"mix",port:"a"}},{from:{nodeId:"voronoi",port:"value"},to:{nodeId:"mix",port:"b"}},{from:{nodeId:"fresnel",port:"value"},to:{nodeId:"mix",port:"t"}},{from:{nodeId:"axis",port:"value"},to:{nodeId:"rotateAxis",port:"axis"}},{from:{nodeId:"angle",port:"value"},to:{nodeId:"rotateAxis",port:"angle"}}],{color:{input:{nodeId:"mix",port:"value"}},opacity:{input:{nodeId:"sin",port:"value"}},anisotropyDirection:{input:{nodeId:"flipbook",port:"value"}},transform:{input:{nodeId:"rotateAxis",port:"value"}}}),o=v(t);e(o.nodes.get("mix")?.outputs.value.type).toBe("float"),e(o.nodes.get("twirl")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("bulge")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("radial")?.outputs.coords.type).toBe("vec2"),e(o.nodes.get("flipbook")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("rotateAxis")?.outputs.value.type).toBe("mat4"),e(o.output.color).toBeTruthy(),e(o.output.opacity).toBeTruthy(),e(o.output.transform).toBeTruthy()}),t("slider input node outputs a clamped float value",()=>{const t=T([{id:"slider",kind:"input.slider",params:{value:1.25}}],[],{roughness:{input:{nodeId:"slider",port:"value"}}}),o=v(t);e(o.nodes.get("slider")?.outputs.value.type).toBe("float"),e(o.output.roughness).toBeTruthy()}),t("comparison and boolean graph nodes compile to boolean outputs",()=>{const t=T([{id:"a",kind:"constant.float",params:{value:.75}},{id:"b",kind:"constant.float",params:{value:.5}},{id:"greater",kind:"compare.greaterThan"},{id:"lessEqual",kind:"compare.lessThanOrEqual"},{id:"not",kind:"boolean.not"},{id:"and",kind:"boolean.and"},{id:"or",kind:"boolean.or"}],[{from:{nodeId:"a",port:"value"},to:{nodeId:"greater",port:"a"}},{from:{nodeId:"b",port:"value"},to:{nodeId:"greater",port:"b"}},{from:{nodeId:"a",port:"value"},to:{nodeId:"lessEqual",port:"a"}},{from:{nodeId:"b",port:"value"},to:{nodeId:"lessEqual",port:"b"}},{from:{nodeId:"lessEqual",port:"value"},to:{nodeId:"not",port:"value"}},{from:{nodeId:"greater",port:"value"},to:{nodeId:"and",port:"a"}},{from:{nodeId:"not",port:"value"},to:{nodeId:"and",port:"b"}},{from:{nodeId:"and",port:"value"},to:{nodeId:"or",port:"a"}},{from:{nodeId:"lessEqual",port:"value"},to:{nodeId:"or",port:"b"}}],{discard:{input:{nodeId:"or",port:"value"}}}),o=v(t);e(o.nodes.get("greater")?.outputs.value.type).toBe("boolean"),e(o.nodes.get("lessEqual")?.outputs.value.type).toBe("boolean"),e(o.nodes.get("not")?.outputs.value.type).toBe("boolean"),e(o.nodes.get("and")?.outputs.value.type).toBe("boolean"),e(o.nodes.get("or")?.outputs.value.type).toBe("boolean"),e(o.output.discard).toBeTruthy()}),t("split component node exposes vector and color channels as separate outputs",()=>{const t=T([{id:"vec",kind:"constant.vec3",params:{value:[.2,.4,.6]}},{id:"color",kind:"constant.rgba",params:{value:"#336699",alpha:.75}},{id:"splitVec",kind:"decompose.split"},{id:"splitColor",kind:"decompose.split"},{id:"sum",kind:"math.add"}],[{from:{nodeId:"vec",port:"value"},to:{nodeId:"splitVec",port:"value"}},{from:{nodeId:"color",port:"value"},to:{nodeId:"splitColor",port:"value"}},{from:{nodeId:"splitVec",port:"z"},to:{nodeId:"sum",port:"a"}},{from:{nodeId:"splitColor",port:"a"},to:{nodeId:"sum",port:"b"}}],{roughness:{input:{nodeId:"sum",port:"value"}}}),o=v(t);e(Object.keys(o.nodes.get("splitVec")?.outputs??{})).toEqual(["x","y","z"]),e(Object.keys(o.nodes.get("splitColor")?.outputs??{})).toEqual(["r","g","b","a","x","y","z","w"]),e(o.nodes.get("splitVec")?.outputs.z.type).toBe("float"),e(o.nodes.get("splitColor")?.outputs.a.type).toBe("float"),e(o.output.roughness).toBeTruthy()}),t("color gradient node compiles editable stops to a sampled color",()=>{const t=T([{id:"threshold",kind:"constant.float",params:{value:.75}},{id:"gradient",kind:"color.gradient",params:{stops:[{position:0,color:"#000000",alpha:1},{position:.5,color:"#ff0000",alpha:1},{position:1,color:"#ffffff",alpha:1}]}}],[{from:{nodeId:"threshold",port:"value"},to:{nodeId:"gradient",port:"t"}}],{color:{input:{nodeId:"gradient",port:"value"}}}),o=v(t);e(o.nodes.get("gradient")?.outputs.value.type).toBe("rgba"),e(o.output.color).toBeTruthy(),e(f(t,"gradient").color).toBeTruthy()}),t("curve node compiles editable curve to a sampled float",()=>{const t=T([{id:"t",kind:"constant.float",params:{value:.75}},{id:"curve",kind:"utility.curve",params:{curve:"InOutQuad"}}],[{from:{nodeId:"t",port:"value"},to:{nodeId:"curve",port:"t"}}],{roughness:{input:{nodeId:"curve",port:"value"}}}),o=v(t);e(o.nodes.get("curve")?.outputs.value.type).toBe("float"),e(o.output.roughness).toBeTruthy(),e(f(t,"curve").color).toBeTruthy()}),t("graph parameters convert to property params and custom defaults",()=>{const t=y();t.parameters.push({name:"opacity",type:"float",defaultValue:.5});const o=b(t);e(o.map(e=>e.name)).toEqual(["baseColor","opacity"]),e(o.every(e=>!0===e.options.optional)).toBe(!0);const a=d(t);e(a.baseColor).toMatchObject({type:u.RgbNode,value:"#ffffff",override:!1}),e(a.opacity).toMatchObject({type:u.FloatNode,value:.5,override:!1})}),t("existing shader graph param values keep overriding unless explicitly disabled",()=>{const t=y(),o=d(t,{baseColor:{type:u.RgbNode,value:"#ff0000"}});e(o.baseColor).toMatchObject({type:u.RgbNode,value:"#ff0000",override:!0});const a=d(t,{baseColor:{type:u.RgbNode,value:"#00ff00",override:!1}});e(a.baseColor).toMatchObject({type:u.RgbNode,value:"#ffffff",override:!1})}),t("texture graph parameter defaults serialize as sampler parameters",()=>{const t=T([],[],{});t.parameters.push({name:"map",type:"texture",defaultValue:"texture-asset-id"});const o=d(t);e(o.map).toMatchObject({type:u.Sampler2DNode,value:"texture-asset-id"})}),t("shader graph params keep graph-declared serialized types over previous string params",()=>{const t=y();t.parameters.push({name:"map",type:"texture",defaultValue:null});const o=d(t,{baseColor:{type:u.String,value:"#ff0000"},map:{type:u.String,value:"texture-asset-id"}});e(o.baseColor).toMatchObject({type:u.RgbNode,value:"#ff0000"}),e(o.map).toMatchObject({type:u.Sampler2DNode,value:"texture-asset-id"})}),t("shader graph texture params resolve asset ids before compile",async()=>{const t=T([{id:"texture",kind:"input.parameter",params:{parameter:"map"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"map",type:"texture",defaultValue:"texture-asset-id"});const o=await p({},t,new j([{id:"texture-asset-id",name:"Map",type:"texture",fileKey:"map.png",fileFormat:"png"}]),new C);e(o.map).toBeTruthy(),e(typeof o.map).not.toBe("string"),e(v(t,{parameters:o}).output.color).toBeTruthy()}),t("material graph asset reference and local embedded graph both build materials",async()=>{const t={id:"graph-asset",name:"Reusable Graph",type:"shaderGraph",shaderGraph:y()},o=w("referenced",{source:"asset",assetId:t.id}),a=w("local",{source:"local",graph:y()}),r=new j([t,o,a]),n=new i;e((await s(o,null,r,n,[],!1)).isMaterial).toBe(!0),e((await s(a,null,r,n,[],!1)).isMaterial).toBe(!0)}),t("selected-node preview compiles scalar, vector, color, rgba, and boolean outputs",()=>{const t=[{id:"float",kind:"constant.float",params:{value:.25}},{id:"slider",kind:"input.slider",params:{value:.5}},{id:"vec2",kind:"constant.vec2",params:{value:[1,0]}},{id:"rgb",kind:"constant.vec3",params:{value:[1,.5,0]}},{id:"rgba",kind:"constant.rgba",params:{value:"#ff8800",alpha:.75}},{id:"boolean",kind:"constant.boolean",params:{value:!0}}];for(const o of t){const t=T([o],[],{color:{input:{nodeId:o.id,port:"value"}}});e(f(t,o.id).color).toBeTruthy()}}),t("post-process shader graph builds a fullscreen-safe material",()=>{const t=h(),o=m(t);e(o).toBeInstanceOf(B),e(o.depthWrite).toBe(!1),e(o.depthTest).toBe(!1),e(o.toneMapped).toBe(!1),e(o.transparent).toBe(!1),e(o.userData.isPostProcessShaderGraph).toBe(!0),e(o.uniforms[x]).toBeTruthy(),e(o.uniforms[I]).toBeTruthy()}),t("empty post-process shader graph defaults to identity scene color",()=>{const t=h();t.nodes=[],t.edges=[],t.outputs={};const o=v(t),a=m(t);e(o.output.color).toBeTruthy(),e(o.output.transparent).toBe(!1),e(a.uniforms[x]).toBeTruthy(),e(a.uniforms[I]).toBeTruthy()}),t("post-process color and opacity outputs expose blend weight uniform",()=>{const t=T([{id:"color",kind:"constant.rgba",params:{value:"#ff0044",alpha:1}},{id:"opacity",kind:"constant.float",params:{value:.35}}],[],{color:{input:{nodeId:"color",port:"value"}},opacity:{input:{nodeId:"opacity",port:"value"}},roughness:{value:.8}});t.target="postProcess";const o=v(t),a=m(t);e(o.output.color).toBeTruthy(),e(o.output.opacity).toBeTruthy(),e(o.output.roughness).toBeUndefined(),e(a.uniforms[I]).toBeTruthy()}),t("selected-node preview builds through post-process fullscreen path",()=>{const t=T([{id:"float",kind:"constant.float",params:{value:.25}}],[],{color:{input:{nodeId:"float",port:"value"}}});t.target="postProcess";const o=m(t,{previewNodeId:"float"});e(o).toBeInstanceOf(B),e(o.userData.isPostProcessShaderGraph).toBe(!0),e(o.uniforms[x]).toBeTruthy(),e(o.uniforms[I]).toBeTruthy()}),t("texture parameter nodes expose sampled value and channel outputs",()=>{const t=T([{id:"texture",kind:"input.parameter",params:{parameter:"albedo"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"albedo",type:"texture",defaultValue:null});const o=v(t,{parameters:{albedo:new n}}),a=o.nodes.get("texture")?.outputs??{};e(Object.keys(a)).toEqual(["value","rgb","r","g","b","a","sampler"]),e(a.value.type).toBe("rgba"),e(a.rgb.type).toBe("rgb"),e(a.r.type).toBe("float"),e(a.g.type).toBe("float"),e(a.b.type).toBe("float"),e(a.a.type).toBe("float"),e(a.sampler.type).toBe("sampler2d"),e(o.output.color).toBeTruthy()}),t("texture parameter nodes accept resolved sampler values",()=>{const t=T([{id:"texture",kind:"input.parameter",params:{parameter:"map"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"map",type:"texture",defaultValue:"texture-asset-id"});const o=v(t,{parameters:{map:k(new n)}});e(o.output.color).toBeTruthy()}),t("scene depth graph nodes expose sampled and fragment linear eye depth",()=>{const t=T([{id:"uv",kind:"builtin.screenUv"},{id:"sceneDepth",kind:"scene.sampleDepth"},{id:"fragmentDepth",kind:"scene.fragmentDepth"},{id:"depthFade",kind:"math.subtract"}],[{from:{nodeId:"uv",port:"value"},to:{nodeId:"sceneDepth",port:"uv"}},{from:{nodeId:"sceneDepth",port:"linearEyeDepth"},to:{nodeId:"depthFade",port:"a"}},{from:{nodeId:"fragmentDepth",port:"linearEyeDepth"},to:{nodeId:"depthFade",port:"b"}}],{opacity:{input:{nodeId:"depthFade",port:"value"}}}),o=v(t);e(o.nodes.get("sceneDepth")?.outputs.depth.type).toBe("float"),e(o.nodes.get("sceneDepth")?.outputs.linearEyeDepth.type).toBe("float"),e(o.nodes.get("fragmentDepth")?.outputs.linearEyeDepth.type).toBe("float"),e(o.output.opacity).toBeTruthy()}),t("unpack normal node compiles with various input combinations",()=>{const t=T([{id:"sample",kind:"constant.vec3",params:{value:[.5,.5,1]}},{id:"scale",kind:"constant.float",params:{value:1}},{id:"normal",kind:"builtin.normal"},{id:"toNormal",kind:"color.unpackNormal"}],[{from:{nodeId:"sample",port:"value"},to:{nodeId:"toNormal",port:"color"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"toNormal",port:"scale"}},{from:{nodeId:"normal",port:"value"},to:{nodeId:"toNormal",port:"normal"}}],{normal:{input:{nodeId:"toNormal",port:"value"}}}),o=v(t);e(o.nodes.get("toNormal")?.outputs.value.type).toBe("vec3"),e(o.output.normal).toBeTruthy()});class j extends l{constructor(e){super(),this.onCreate=null,this.onDelete=null,this.onUpdate=null,this.assets=new Map(e.map(e=>[e.id,e]))}async getAsset(e){return this.assets.get(e)}async getAssets(){return Array.from(this.assets.values())}}class C extends i{async getTexture(){return new n}async getTextureArray(){return{texture:null,layerIndex:null}}}/*
|
|
1
|
+
import{expect as e,test as t,vi as o}from"vitest";import{DoubleSide as a,FrontSide as r,Texture as n}from"three";o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&o.fn()),e[t]),set:(e,t,o)=>(e[t]=o,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),o.mock("three/examples/jsm/Addons.js",async e=>({...await e(),ConvexHull:class{},SkeletonUtils:{clone:e=>e},RectAreaLightHelper:class{}})),o.mock("three-shader-graph",async e=>{const t=await e();return{...t,rgb:(e,o,a)=>null==o&&null==a&&!0===e?.isColor&&"function"==typeof e.getHexString?t.rgb("#"+e.getHexString()):t.rgb(e,o,a)}}),o.mock("../rendering.js",()=>({RenderingView:class{constructor(){this._id="test",this.csm={setupMaterial:o.fn()}}getEnvTexture(e){return e}},setRenderingPaused:o.fn()}));import{materialFromAsset as s,prepareCustomParamsFromShaderGraph as d,prepareShaderGraphParameters as p}from"../scene/materializer.js";import{SerializedParamType as u}from"../scene/model.js";import{AssetsProvider as l}from"../scene/assets-provider.js";import{AssetResourceLoader as i}from"../scene/asset-resource-loader.js";import{buildShaderGraphMaterial as m,buildShaderGraphPostProcessMaterial as c,compileShaderGraph as v,compileShaderGraphPreview as f,createDefaultPostProcessShaderGraphDocument as h,createDefaultShaderGraphDocument as y,createDefaultVfxShaderGraphDocument as g,shaderGraphPostProcessWeightUniformName as I,shaderGraphParametersToPropertyParameters as b}from"../shader/graph/index.js";import{NodeShaderMaterial as x,textureSampler2d as B,UniformFloatNode as k,UniformSampler2dArraySlice as T}from"../shader-nodes/index.js";import{StandardShader as w}from"../shader/builtin/standard-shader.js";import{sceneMapUniformName as j}from"../shader-nodes/scene-sample.js";function M(e,t,o){return{version:1,target:"surface",parameters:[],nodes:e,edges:t,outputs:o}}function C(e,t){return{id:e,name:e,type:"material",material:{type:"shaderGraph",side:r,params:{},shaderParams:{},shaderGraph:t}}}t("graph validation rejects missing nodes, invalid ports, and incompatible edges",()=>{const t=y();t.outputs.color={input:{nodeId:"missing",port:"value"}},e(()=>v(t)).toThrow(/Missing shader graph node/);const o=y();o.outputs.color={input:{nodeId:"base-color",port:"missing"}},e(()=>v(o)).toThrow(/does not have output/);const a=M([{id:"a",kind:"constant.vec2",params:{value:[1,1]}},{id:"b",kind:"constant.vec3",params:{value:[1,1,1]}},{id:"add",kind:"math.add"}],[{from:{nodeId:"a",port:"value"},to:{nodeId:"add",port:"a"}},{from:{nodeId:"b",port:"value"},to:{nodeId:"add",port:"b"}}],{color:{input:{nodeId:"add",port:"value"}}});e(()=>v(a)).toThrow(/Cannot add vec2 and vec3/)}),t("surface graph compiles to NodeShaderOutput",()=>{const t=v(y("surface"));e(t.output.color).toBeTruthy(),e(t.output.transparent).toBe(!1)}),t("inline literal inputs compile without separate constant nodes",()=>{const t=M([{id:"uv",kind:"builtin.uv"},{id:"scale-uv",kind:"math.multiply",inputs:{b:{value:2,valueType:"float"}}},{id:"rotate",kind:"uv.rotate",inputs:{angle:{value:.5,valueType:"float"},center:{value:[.25,.75],valueType:"vec2"}}},{id:"translate",kind:"transform.translate",inputs:{offset:{value:[0,1,0],valueType:"vec3"}}}],[{from:{nodeId:"uv",port:"value"},to:{nodeId:"scale-uv",port:"a"}},{from:{nodeId:"scale-uv",port:"value"},to:{nodeId:"rotate",port:"uv"}}],{color:{input:{nodeId:"rotate",port:"value"}},transform:{input:{nodeId:"translate",port:"value"}}}),o=v(t);e(o.nodes.get("scale-uv")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("rotate")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("translate")?.outputs.value.type).toBe("mat4"),e(o.output.color).toBeTruthy(),e(o.output.transform).toBeTruthy()}),t("sprite graph compiles with sprite-compatible material output",()=>{const t=y("sprite"),o=v(t);e(o.output.color).toBeTruthy(),e(o.output.transparent).toBe(!0)}),t("default VFX shader graphs compile mapped targets and build non-sprite materials",()=>{for(const t of["sprite","surface","decal","trail"]){const o=g(t);if(e(v(o).output.color).toBeTruthy(),"sprite"===t)continue;const a=m(o,{trailBillboard:"trail"===t});e(a.isMaterial).toBe(!0)}}),t("graph material options apply to supported material targets",()=>{const t=y("surface");t.materialOptions={side:"double",transparent:!0};const o=m(t);e(o.side).toBe(a),e(o.transparent).toBe(!0);const r=y("decal");r.materialOptions={transparent:!0};const n=m(r);e(n.transparent).toBe(!0)}),t("standard graph compiles extended material outputs and vertex transform",()=>{const t=M([{id:"metalness",kind:"constant.float",params:{value:.8}},{id:"ao",kind:"constant.float",params:{value:.5}},{id:"sheen",kind:"constant.vec3",params:{value:[.1,.2,.3]}},{id:"offset",kind:"constant.vec3",params:{value:[0,1,0]}},{id:"translate",kind:"transform.translate"}],[{from:{nodeId:"offset",port:"value"},to:{nodeId:"translate",port:"offset"}}],{metalness:{input:{nodeId:"metalness",port:"value"}},ambientOcclusion:{input:{nodeId:"ao",port:"value"}},sheenColor:{input:{nodeId:"sheen",port:"value"}},transform:{input:{nodeId:"translate",port:"value"}}}),o=v(t);e(o.output.metalness).toBeTruthy(),e(o.output.ambientOcclusion).toBeTruthy(),e(o.output.sheenColor).toBeTruthy(),e(o.output.transform).toBeTruthy();const a=m(t);e(a.outputMetalness).toBeTruthy(),e(a.outputAmbientOcclusion).toBeTruthy(),e(a.outputTransform).toBeTruthy()}),t("math multiply composes transform matrices",()=>{const t=M([{id:"translate",kind:"transform.translate",inputs:{offset:{value:[0,1,0],valueType:"vec3"}}},{id:"scale",kind:"transform.scale",inputs:{scale:{value:[2,2,2],valueType:"vec3"}}},{id:"compose",kind:"math.multiply"}],[{from:{nodeId:"translate",port:"value"},to:{nodeId:"compose",port:"a"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"compose",port:"b"}}],{transform:{input:{nodeId:"compose",port:"value"}}}),o=v(t);e(o.nodes.get("compose")?.outputs.value.type).toBe("mat4"),e(o.output.transform).toBeTruthy(),e(m(t).outputTransform).toBeTruthy()}),t("matrix input nodes compile as mat4 uniforms",()=>{for(const t of["builtin.viewMatrix","builtin.projectionMatrix","builtin.modelViewMatrix"]){const o=M([{id:"matrix",kind:t}],[],{transform:{input:{nodeId:"matrix",port:"value"}}}),a=v(o);e(a.nodes.get("matrix")?.outputs.value.type).toBe("mat4"),e(a.output.transform).toBeTruthy()}}),t("compose vec4 node compiles from float components",()=>{const t=M([{id:"compose",kind:"compose.vec4",inputs:{x:{value:.1},y:{value:.2},z:{value:.3},w:{value:1}}}],[],{color:{input:{nodeId:"compose",port:"value"}}}),o=v(t);e(o.nodes.get("compose")?.outputs.value.type).toBe("vec4"),e(o.output.color).toBeTruthy()}),t("atan2 node compiles from y and x inputs",()=>{const t=M([{id:"atan2",kind:"math.atan2",inputs:{y:{value:1},x:{value:0}}}],[],{color:{input:{nodeId:"atan2",port:"value"}}}),o=v(t);e(o.nodes.get("atan2")?.outputs.value.type).toBe("float"),e(o.output.color).toBeTruthy()}),t("built-in attribute nodes compile for fragment and transform outputs",()=>{const t=M([{id:"color",kind:"builtin.color"},{id:"normal",kind:"builtin.normal"},{id:"position",kind:"builtin.position"},{id:"worldPosition",kind:"builtin.worldPosition"},{id:"objectPosition",kind:"builtin.objectPosition"},{id:"translate",kind:"transform.translate"}],[{from:{nodeId:"objectPosition",port:"value"},to:{nodeId:"translate",port:"offset"}}],{color:{input:{nodeId:"color",port:"value"}},normal:{input:{nodeId:"normal",port:"value"}},roughness:{input:{nodeId:"position",port:"x"}},metalness:{input:{nodeId:"worldPosition",port:"x"}},transform:{input:{nodeId:"translate",port:"value"}}}),o=v(t);e(o.nodes.get("color")?.outputs.value.type).toBe("rgba"),e(o.nodes.get("normal")?.outputs.value.type).toBe("vec3"),e(o.nodes.get("position")?.outputs.value.type).toBe("vec3"),e(o.nodes.get("worldPosition")?.outputs.value.type).toBe("vec3"),e(o.nodes.get("objectPosition")?.outputs.value.type).toBe("vec3"),e(o.output.color).toBeTruthy(),e(o.output.normal).toBeTruthy(),e(o.output.transform).toBeTruthy(),e(m(t).outputTransform).toBeTruthy()}),t("painted layer mix node compiles color layers and builds a material",()=>{const t=M([{id:"base",kind:"constant.rgba",params:{value:"#224422",alpha:1}},{id:"grass",kind:"constant.rgba",params:{value:"#55aa33",alpha:1}},{id:"stone",kind:"constant.rgba",params:{value:"#777777",alpha:1}},{id:"mix",kind:"layer.mixPainted",params:{mode:"soft",enableNoise:!0,softness:.25,noiseScale:.2,noiseAmount:.4}}],[{from:{nodeId:"base",port:"value"},to:{nodeId:"mix",port:"base"}},{from:{nodeId:"grass",port:"value"},to:{nodeId:"mix",port:"layer1"}},{from:{nodeId:"stone",port:"value"},to:{nodeId:"mix",port:"layer3"}}],{color:{input:{nodeId:"mix",port:"value"}}}),o=v(t);e(o.nodes.get("mix")?.outputs.value.type).toBe("rgba"),e(o.output.color).toBeTruthy(),e(m(t).isMaterial).toBe(!0)}),t("painted layer mix node compiles sparse float layers in hard and soft modes",()=>{for(const t of["hard","soft"]){const o=M([{id:"base",kind:"constant.float",params:{value:.8}},{id:"mud",kind:"constant.float",params:{value:1}},{id:"wet",kind:"constant.float",params:{value:.25}},{id:"mix",kind:"layer.mixPainted",params:{mode:t,enableNoise:!1,softness:.3,noiseScale:.1,noiseAmount:.5}}],[{from:{nodeId:"base",port:"value"},to:{nodeId:"mix",port:"base"}},{from:{nodeId:"mud",port:"value"},to:{nodeId:"mix",port:"layer2"}},{from:{nodeId:"wet",port:"value"},to:{nodeId:"mix",port:"layer8"}}],{roughness:{input:{nodeId:"mix",port:"value"}}}),a=v(o);e(a.nodes.get("mix")?.outputs.value.type).toBe("float"),e(a.output.roughness).toBeTruthy()}}),t("painted layer mix node rejects incompatible layer types",()=>{const t=M([{id:"base",kind:"constant.vec2",params:{value:[0,1]}},{id:"layer",kind:"constant.vec3",params:{value:[1,0,0]}},{id:"mix",kind:"layer.mixPainted"}],[{from:{nodeId:"base",port:"value"},to:{nodeId:"mix",port:"base"}},{from:{nodeId:"layer",port:"value"},to:{nodeId:"mix",port:"layer1"}}],{color:{input:{nodeId:"mix",port:"value"}}});e(()=>v(t)).toThrow(/Cannot mix vec2 and vec3/)}),t("additional math, uv, shape, noise, and effect nodes compile",()=>{const t=M([{id:"uv",kind:"builtin.uv"},{id:"angle",kind:"constant.float",params:{value:.5}},{id:"scale",kind:"constant.float",params:{value:4}},{id:"half",kind:"constant.float",params:{value:.5}},{id:"axis",kind:"constant.vec3",params:{value:[0,1,0]}},{id:"rotatedUv",kind:"uv.rotate"},{id:"twirl",kind:"uv.twirl"},{id:"bulge",kind:"uv.bulge"},{id:"radial",kind:"uv.radial"},{id:"flipbook",kind:"uv.flipbook"},{id:"shape",kind:"shape.roundedRectangle"},{id:"noise",kind:"noise.simplex"},{id:"voronoi",kind:"noise.voronoi2d"},{id:"fresnel",kind:"effect.fresnel"},{id:"sin",kind:"math.sin"},{id:"pow",kind:"math.pow"},{id:"mix",kind:"math.mix"},{id:"rotateAxis",kind:"transform.rotateAxis"}],[{from:{nodeId:"uv",port:"value"},to:{nodeId:"rotatedUv",port:"uv"}},{from:{nodeId:"angle",port:"value"},to:{nodeId:"rotatedUv",port:"angle"}},{from:{nodeId:"rotatedUv",port:"value"},to:{nodeId:"twirl",port:"uv"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"twirl",port:"strength"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"bulge",port:"uv"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"bulge",port:"power"}},{from:{nodeId:"bulge",port:"value"},to:{nodeId:"radial",port:"uv"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"flipbook",port:"uv"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"shape",port:"uv"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"shape",port:"width"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"shape",port:"height"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"shape",port:"radius"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"noise",port:"uv"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"noise",port:"scale"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"voronoi",port:"uv"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"voronoi",port:"scale"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"fresnel",port:"power"}},{from:{nodeId:"radial",port:"angle"},to:{nodeId:"sin",port:"value"}},{from:{nodeId:"noise",port:"value"},to:{nodeId:"pow",port:"a"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"pow",port:"b"}},{from:{nodeId:"shape",port:"value"},to:{nodeId:"mix",port:"a"}},{from:{nodeId:"voronoi",port:"value"},to:{nodeId:"mix",port:"b"}},{from:{nodeId:"fresnel",port:"value"},to:{nodeId:"mix",port:"t"}},{from:{nodeId:"axis",port:"value"},to:{nodeId:"rotateAxis",port:"axis"}},{from:{nodeId:"angle",port:"value"},to:{nodeId:"rotateAxis",port:"angle"}}],{color:{input:{nodeId:"mix",port:"value"}},opacity:{input:{nodeId:"sin",port:"value"}},anisotropyDirection:{input:{nodeId:"flipbook",port:"value"}},transform:{input:{nodeId:"rotateAxis",port:"value"}}}),o=v(t);e(o.nodes.get("mix")?.outputs.value.type).toBe("float"),e(o.nodes.get("twirl")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("bulge")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("radial")?.outputs.coords.type).toBe("vec2"),e(o.nodes.get("flipbook")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("rotateAxis")?.outputs.value.type).toBe("mat4"),e(o.output.color).toBeTruthy(),e(o.output.opacity).toBeTruthy(),e(o.output.transform).toBeTruthy()}),t("slider input node outputs a clamped float value",()=>{const t=M([{id:"slider",kind:"input.slider",params:{value:1.25}}],[],{roughness:{input:{nodeId:"slider",port:"value"}}}),o=v(t);e(o.nodes.get("slider")?.outputs.value.type).toBe("float"),e(o.output.roughness).toBeTruthy()}),t("comparison and boolean graph nodes compile to boolean outputs",()=>{const t=M([{id:"a",kind:"constant.float",params:{value:.75}},{id:"b",kind:"constant.float",params:{value:.5}},{id:"greater",kind:"compare.greaterThan"},{id:"lessEqual",kind:"compare.lessThanOrEqual"},{id:"not",kind:"boolean.not"},{id:"and",kind:"boolean.and"},{id:"or",kind:"boolean.or"}],[{from:{nodeId:"a",port:"value"},to:{nodeId:"greater",port:"a"}},{from:{nodeId:"b",port:"value"},to:{nodeId:"greater",port:"b"}},{from:{nodeId:"a",port:"value"},to:{nodeId:"lessEqual",port:"a"}},{from:{nodeId:"b",port:"value"},to:{nodeId:"lessEqual",port:"b"}},{from:{nodeId:"lessEqual",port:"value"},to:{nodeId:"not",port:"value"}},{from:{nodeId:"greater",port:"value"},to:{nodeId:"and",port:"a"}},{from:{nodeId:"not",port:"value"},to:{nodeId:"and",port:"b"}},{from:{nodeId:"and",port:"value"},to:{nodeId:"or",port:"a"}},{from:{nodeId:"lessEqual",port:"value"},to:{nodeId:"or",port:"b"}}],{discard:{input:{nodeId:"or",port:"value"}}}),o=v(t);e(o.nodes.get("greater")?.outputs.value.type).toBe("boolean"),e(o.nodes.get("lessEqual")?.outputs.value.type).toBe("boolean"),e(o.nodes.get("not")?.outputs.value.type).toBe("boolean"),e(o.nodes.get("and")?.outputs.value.type).toBe("boolean"),e(o.nodes.get("or")?.outputs.value.type).toBe("boolean"),e(o.output.discard).toBeTruthy()}),t("split component node exposes vector and color channels as separate outputs",()=>{const t=M([{id:"vec",kind:"constant.vec3",params:{value:[.2,.4,.6]}},{id:"color",kind:"constant.rgba",params:{value:"#336699",alpha:.75}},{id:"splitVec",kind:"decompose.split"},{id:"splitColor",kind:"decompose.split"},{id:"sum",kind:"math.add"}],[{from:{nodeId:"vec",port:"value"},to:{nodeId:"splitVec",port:"value"}},{from:{nodeId:"color",port:"value"},to:{nodeId:"splitColor",port:"value"}},{from:{nodeId:"splitVec",port:"z"},to:{nodeId:"sum",port:"a"}},{from:{nodeId:"splitColor",port:"a"},to:{nodeId:"sum",port:"b"}}],{roughness:{input:{nodeId:"sum",port:"value"}}}),o=v(t);e(Object.keys(o.nodes.get("splitVec")?.outputs??{})).toEqual(["x","y","z"]),e(Object.keys(o.nodes.get("splitColor")?.outputs??{})).toEqual(["r","g","b","a","x","y","z","w"]),e(o.nodes.get("splitVec")?.outputs.z.type).toBe("float"),e(o.nodes.get("splitColor")?.outputs.a.type).toBe("float"),e(o.output.roughness).toBeTruthy()}),t("color gradient node compiles editable stops to a sampled color",()=>{const t=M([{id:"threshold",kind:"constant.float",params:{value:.75}},{id:"gradient",kind:"color.gradient",params:{stops:[{position:0,color:"#000000",alpha:1},{position:.5,color:"#ff0000",alpha:1},{position:1,color:"#ffffff",alpha:1}]}}],[{from:{nodeId:"threshold",port:"value"},to:{nodeId:"gradient",port:"t"}}],{color:{input:{nodeId:"gradient",port:"value"}}}),o=v(t);e(o.nodes.get("gradient")?.outputs.value.type).toBe("rgba"),e(o.output.color).toBeTruthy(),e(f(t,"gradient").color).toBeTruthy()}),t("curve node compiles editable curve to a sampled float",()=>{const t=M([{id:"t",kind:"constant.float",params:{value:.75}},{id:"curve",kind:"utility.curve",params:{curve:"InOutQuad"}}],[{from:{nodeId:"t",port:"value"},to:{nodeId:"curve",port:"t"}}],{roughness:{input:{nodeId:"curve",port:"value"}}}),o=v(t);e(o.nodes.get("curve")?.outputs.value.type).toBe("float"),e(o.output.roughness).toBeTruthy(),e(f(t,"curve").color).toBeTruthy()}),t("graph parameters convert to property params and custom defaults",()=>{const t=y();t.parameters.push({name:"opacity",type:"float",defaultValue:.5});const o=b(t);e(o.map(e=>e.name)).toEqual(["baseColor","opacity"]),e(o.every(e=>!0===e.options.optional)).toBe(!0);const a=d(t);e(a.baseColor).toMatchObject({type:u.RgbNode,value:"#ffffff",override:!1}),e(a.opacity).toMatchObject({type:u.FloatNode,value:.5,override:!1})}),t("existing shader graph param values keep overriding unless explicitly disabled",()=>{const t=y(),o=d(t,{baseColor:{type:u.RgbNode,value:"#ff0000"}});e(o.baseColor).toMatchObject({type:u.RgbNode,value:"#ff0000",override:!0});const a=d(t,{baseColor:{type:u.RgbNode,value:"#00ff00",override:!1}});e(a.baseColor).toMatchObject({type:u.RgbNode,value:"#ffffff",override:!1})}),t("texture graph parameter defaults serialize as sampler parameters",()=>{const t=M([],[],{});t.parameters.push({name:"map",type:"texture",defaultValue:"texture-asset-id"});const o=d(t);e(o.map).toMatchObject({type:u.Sampler2DNode,value:"texture-asset-id"})}),t("shader graph params keep graph-declared serialized types over previous string params",()=>{const t=y();t.parameters.push({name:"map",type:"texture",defaultValue:null});const o=d(t,{baseColor:{type:u.String,value:"#ff0000"},map:{type:u.String,value:"texture-asset-id"}});e(o.baseColor).toMatchObject({type:u.RgbNode,value:"#ff0000"}),e(o.map).toMatchObject({type:u.Sampler2DNode,value:"texture-asset-id"})}),t("shader graph texture params resolve asset ids before compile",async()=>{const t=M([{id:"texture",kind:"input.parameter",params:{parameter:"map"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"map",type:"texture",defaultValue:"texture-asset-id"});const o=await p({},t,new A([{id:"texture-asset-id",name:"Map",type:"texture",fileKey:"map.png",fileFormat:"png"}]),new O);e(o.map).toBeTruthy(),e(typeof o.map).not.toBe("string"),e(v(t,{parameters:o}).output.color).toBeTruthy()}),t("shader graph texture array params expose named slice uniforms",async()=>{const t=M([{id:"texture",kind:"input.parameter",params:{parameter:"map"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"map",type:"texture",defaultValue:"texture-array-layer"});const o=await p({},t,new A([{id:"texture-array-layer",name:"Map Layer",type:"texture",fileKey:"map.png",fileFormat:"png",texture:{textureArrayFileKey:"textures/packed.ktx2",textureArrayLayer:5}}]),new P(new n,5)),a=m(t,{parameters:o});e(a.uniforms.pu_map_i?.value).toBe(5),e(a.fragmentShader).toContain("uniform float pu_map_i")}),t("standard shader height maps support texture array slices",()=>{const t=new w;t.heightMap=new T("heightMap",new n,new k("heightMap_i",3,void 0,!1)),t.map=B(new n),t.heightScale=.05;const o=t.build();e(o.fragmentShader).toContain("uniform sampler2DArray heightMap"),e(o.fragmentShader).toContain("uniform float heightMap_i"),e(o.fragmentShader).toContain("texture(heightMap, vec3(uv + vCurrOffset, heightMapLayer)).r")}),t("material graph asset reference and local embedded graph both build materials",async()=>{const t={id:"graph-asset",name:"Reusable Graph",type:"shaderGraph",shaderGraph:y()},o=C("referenced",{source:"asset",assetId:t.id}),a=C("local",{source:"local",graph:y()}),r=new A([t,o,a]),n=new i;e((await s(o,null,r,n,[],!1)).isMaterial).toBe(!0),e((await s(a,null,r,n,[],!1)).isMaterial).toBe(!0)}),t("selected-node preview compiles scalar, vector, color, rgba, and boolean outputs",()=>{const t=[{id:"float",kind:"constant.float",params:{value:.25}},{id:"slider",kind:"input.slider",params:{value:.5}},{id:"vec2",kind:"constant.vec2",params:{value:[1,0]}},{id:"rgb",kind:"constant.vec3",params:{value:[1,.5,0]}},{id:"rgba",kind:"constant.rgba",params:{value:"#ff8800",alpha:.75}},{id:"boolean",kind:"constant.boolean",params:{value:!0}}];for(const o of t){const t=M([o],[],{color:{input:{nodeId:o.id,port:"value"}}});e(f(t,o.id).color).toBeTruthy()}}),t("post-process shader graph builds a fullscreen-safe material",()=>{const t=h(),o=c(t);e(o).toBeInstanceOf(x),e(o.depthWrite).toBe(!1),e(o.depthTest).toBe(!1),e(o.toneMapped).toBe(!1),e(o.transparent).toBe(!1),e(o.userData.isPostProcessShaderGraph).toBe(!0),e(o.uniforms[j]).toBeTruthy(),e(o.uniforms[I]).toBeTruthy()}),t("empty post-process shader graph defaults to identity scene color",()=>{const t=h();t.nodes=[],t.edges=[],t.outputs={};const o=v(t),a=c(t);e(o.output.color).toBeTruthy(),e(o.output.transparent).toBe(!1),e(a.uniforms[j]).toBeTruthy(),e(a.uniforms[I]).toBeTruthy()}),t("post-process color and opacity outputs expose blend weight uniform",()=>{const t=M([{id:"color",kind:"constant.rgba",params:{value:"#ff0044",alpha:1}},{id:"opacity",kind:"constant.float",params:{value:.35}}],[],{color:{input:{nodeId:"color",port:"value"}},opacity:{input:{nodeId:"opacity",port:"value"}},roughness:{value:.8}});t.target="postProcess";const o=v(t),a=c(t);e(o.output.color).toBeTruthy(),e(o.output.opacity).toBeTruthy(),e(o.output.roughness).toBeUndefined(),e(a.uniforms[I]).toBeTruthy()}),t("selected-node preview builds through post-process fullscreen path",()=>{const t=M([{id:"float",kind:"constant.float",params:{value:.25}}],[],{color:{input:{nodeId:"float",port:"value"}}});t.target="postProcess";const o=c(t,{previewNodeId:"float"});e(o).toBeInstanceOf(x),e(o.userData.isPostProcessShaderGraph).toBe(!0),e(o.uniforms[j]).toBeTruthy(),e(o.uniforms[I]).toBeTruthy()}),t("texture parameter nodes expose sampled value and channel outputs",()=>{const t=M([{id:"texture",kind:"input.parameter",params:{parameter:"albedo"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"albedo",type:"texture",defaultValue:null});const o=v(t,{parameters:{albedo:new n}}),a=o.nodes.get("texture")?.outputs??{};e(Object.keys(a)).toEqual(["value","rgb","r","g","b","a","sampler"]),e(a.value.type).toBe("rgba"),e(a.rgb.type).toBe("rgb"),e(a.r.type).toBe("float"),e(a.g.type).toBe("float"),e(a.b.type).toBe("float"),e(a.a.type).toBe("float"),e(a.sampler.type).toBe("sampler2d"),e(o.output.color).toBeTruthy()}),t("texture parameter nodes accept resolved sampler values",()=>{const t=M([{id:"texture",kind:"input.parameter",params:{parameter:"map"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"map",type:"texture",defaultValue:"texture-asset-id"});const o=v(t,{parameters:{map:B(new n)}});e(o.output.color).toBeTruthy()}),t("scene depth graph nodes expose sampled and fragment linear eye depth",()=>{const t=M([{id:"uv",kind:"builtin.screenUv"},{id:"sceneDepth",kind:"scene.sampleDepth"},{id:"fragmentDepth",kind:"scene.fragmentDepth"},{id:"depthFade",kind:"math.subtract"}],[{from:{nodeId:"uv",port:"value"},to:{nodeId:"sceneDepth",port:"uv"}},{from:{nodeId:"sceneDepth",port:"linearEyeDepth"},to:{nodeId:"depthFade",port:"a"}},{from:{nodeId:"fragmentDepth",port:"linearEyeDepth"},to:{nodeId:"depthFade",port:"b"}}],{opacity:{input:{nodeId:"depthFade",port:"value"}}}),o=v(t);e(o.nodes.get("sceneDepth")?.outputs.depth.type).toBe("float"),e(o.nodes.get("sceneDepth")?.outputs.linearEyeDepth.type).toBe("float"),e(o.nodes.get("fragmentDepth")?.outputs.linearEyeDepth.type).toBe("float"),e(o.output.opacity).toBeTruthy()}),t("unpack normal node compiles with various input combinations",()=>{const t=M([{id:"sample",kind:"constant.vec3",params:{value:[.5,.5,1]}},{id:"scale",kind:"constant.float",params:{value:1}},{id:"normal",kind:"builtin.normal"},{id:"toNormal",kind:"color.unpackNormal"}],[{from:{nodeId:"sample",port:"value"},to:{nodeId:"toNormal",port:"color"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"toNormal",port:"scale"}},{from:{nodeId:"normal",port:"value"},to:{nodeId:"toNormal",port:"normal"}}],{normal:{input:{nodeId:"toNormal",port:"value"}}}),o=v(t);e(o.nodes.get("toNormal")?.outputs.value.type).toBe("vec3"),e(o.output.normal).toBeTruthy()});class A extends l{constructor(e){super(),this.onCreate=null,this.onDelete=null,this.onUpdate=null,this.assets=new Map(e.map(e=>[e.id,e]))}async getAsset(e){return this.assets.get(e)}async getAssets(){return Array.from(this.assets.values())}}class O extends i{async getTexture(){return new n}async getTextureArray(){return{texture:null,layerIndex:null}}}class P extends O{constructor(e,t){super(),this.textureArray=e,this.layerIndex=t}async getTextureArray(){return{texture:this.textureArray,layerIndex:this.layerIndex}}}/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|