@hology/core 0.0.274 → 0.0.275

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.
@@ -365,6 +365,8 @@ export type Asset = {
365
365
  alphaTest?: number;
366
366
  /** Whether surfaces using this material can receive projected decals. Defaults to true. */
367
367
  receiveDecals?: boolean;
368
+ /** Whether surfaces receive SDFGI bounced diffuse light. Ambient/environment lighting is unaffected. Defaults to true. */
369
+ receiveGI?: boolean;
368
370
  /** One of "normal" | "additive" | "subtractive" | "multiply" */
369
371
  blending?: BlendingMode;
370
372
  /** @deprecated */
@@ -1,4 +1,4 @@
1
- import{BehaviorSubject as e}from"rxjs";import{Matrix4 as t,Vector3 as s,Quaternion as i,Euler as n,NoToneMapping as o}from"three";export class SceneDataService{constructor(){this.objects=[],this.callbacks=[],this.createCallbacks=[],this.removeCallbacks=[],this.objectChange=new e([])}initiate(e){this.objects=e??[],this.objectChange.next(this.objects)}createDefaultObjects(){this.objects.some(e=>"global_fog"===e.type)||this.objects.unshift(createInitialFog());this.objects.some(e=>"sky"===e.type)||this.objects.unshift(createInitialSky())}removeOnUpdateCallback(e){this.callbacks.splice(this.callbacks.indexOf(e),1)}onCreate(e){this.createCallbacks.push(e)}onUpdate(e){this.callbacks.push(e)}onRemove(e){this.removeCallbacks.push(e)}getObjects(){return this.objects}getObjectsRecursive(){const e=[],t=this.objects.slice();for(;t.length>0;){const s=t.pop();e.push(s),null!=s.children&&t.push(...s.children)}return e}addObject(e,t=!1){this.objects.push(e),this.createCallbacks.forEach(s=>s(e,t)),this.objectChange.next(this.objects)}updateObject(e,t=!1,s=!1){const i=this.findObjectById(e.id);i?(Object.assign(i,e),this.callbacks.forEach(i=>i(e,t,s))):this.addObject(e),this.objectChange.next(this.objects)}findObjectById(e){const t=[...this.objects];for(;t.length>0;){const s=t.pop();if(s.id===e)return s;s.children&&t.push(...s.children)}}findAncestorsById(e){const t=this.findParentById(e);return null==t?[]:[t,...this.findAncestorsById(t.id)]}findParentById(e){return this._findParentById(e,this.objects)}_findParentById(e,t,s=null){return t.some(t=>t.id===e)?s:t.filter(e=>null!=e.children).map(t=>this._findParentById(e,t.children,t)).find(e=>null!=e)}updateParent(e,o){const r=this.findParentById(e.id);if("sky"!==e.type&&"directional"!==e.light?.type&&"ambient"!==e.light?.type&&null!=e.position){const c=null!=o?this.getMatrixWorld(o).invert():(new t).identity();null!=r&&c.multiply(this.getMatrixWorld(r));const l=this.getLocalMatrix(e),a=c.multiply(l),h=new s,d=new i,b=new s;a.decompose(h,d,b),e.position=h.toArray(),e.rotation=(new n).setFromQuaternion(d).toArray(),e.scale=b.toArray()}this.removeRecursive(e.id,r?.children??this.objects),null!=r?r.children=r.children.slice():this.objects=this.objects.slice(),null!=o?(o.children??(o.children=[]),o.children.push(e),o.children=o.children.slice()):(this.objects.push(e),this.objects=this.objects.slice()),this.objectChange.next(this.objects),this.callbacks.forEach(t=>t(e,!1))}getMatrixWorld(e){const t=this.findAncestorsById(e.id).reverse();t.push(e);const s=this.getLocalMatrix(t.shift());for(const e of t){const t=this.getLocalMatrix(e);s.multiply(t)}return s}getLocalMatrix(e){const o=new t,r=(new s).fromArray(e.position),c=(new i).setFromEuler((new n).fromArray(e.rotation)),l=(new s).fromArray(e.scale);return o.compose(r,c,l),o}removeObjectById(e,t=!1){const s=this.findObjectById(e);this.removeObject(s,t)}removeObject(e,t=!1){this.removeRecursive(e.id,this.objects),this.removeCallbacks.forEach(s=>s(e,t)),this.objectChange.next([...this.objects])}removeRecursive(e,t){const s=t.findIndex(t=>t.id===e);if(s>-1)t.splice(s,1);else for(const s of t)null!=s.children&&this.removeRecursive(e,s.children??[])}save(){}serialize(){return JSON.stringify(this.objects)}}export function createInitialFog(){return{id:"auto-global-fog",name:"Global fog",type:"global_fog",fog:{type:"density",color:"#b8f8ff",density:.005,near:100,far:1e3}}}export function createInitialSky(){return{id:"auto-sky",name:"Sky",type:"sky",sky:{materialId:null}}}export function createWorldEnvironment(){return{id:"auto-world-env",name:"World Environment",type:"world_env",worldEnv:{ao:{enabled:!1,blendIntensity:1,radius:.25,distanceExponent:1,thickness:1,distanceFallOff:1,scale:1,samples:16,onlyAO:!1},toneMapping:{mapping:o,exposure:1},environment:{textureId:null,intensity:1}}}}/*
1
+ import{BehaviorSubject as e}from"rxjs";import{Matrix4 as t,Vector3 as i,Quaternion as s,Euler as n,NoToneMapping as o}from"three";export class SceneDataService{constructor(){this.objects=[],this.callbacks=[],this.createCallbacks=[],this.removeCallbacks=[],this.objectChange=new e([])}initiate(e){this.objects=e??[],this.objectChange.next(this.objects)}createDefaultObjects(){this.objects.some(e=>"global_fog"===e.type)||this.objects.unshift(createInitialFog());this.objects.some(e=>"sky"===e.type)||this.objects.unshift(createInitialSky())}removeOnUpdateCallback(e){this.callbacks.splice(this.callbacks.indexOf(e),1)}onCreate(e){this.createCallbacks.push(e)}onUpdate(e){this.callbacks.push(e)}onRemove(e){this.removeCallbacks.push(e)}getObjects(){return this.objects}getObjectsRecursive(){const e=[],t=this.objects.slice();for(;t.length>0;){const i=t.pop();e.push(i),null!=i.children&&t.push(...i.children)}return e}addObject(e,t=!1){this.objects.push(e),this.createCallbacks.forEach(i=>i(e,t)),this.objectChange.next(this.objects)}updateObject(e,t=!1,i=!1){const s=this.findObjectById(e.id);s?(Object.assign(s,e),this.callbacks.forEach(s=>s(e,t,i))):this.addObject(e),this.objectChange.next(this.objects)}findObjectById(e){const t=[...this.objects];for(;t.length>0;){const i=t.pop();if(i.id===e)return i;i.children&&t.push(...i.children)}}findAncestorsById(e){const t=this.findParentById(e);return null==t?[]:[t,...this.findAncestorsById(t.id)]}findParentById(e){return this._findParentById(e,this.objects)}_findParentById(e,t,i=null){return t.some(t=>t.id===e)?i:t.filter(e=>null!=e.children).map(t=>this._findParentById(e,t.children,t)).find(e=>null!=e)}updateParent(e,o){const r=this.findParentById(e.id);if("sky"!==e.type&&"directional"!==e.light?.type&&"ambient"!==e.light?.type&&null!=e.position){const c=null!=o?this.getMatrixWorld(o).invert():(new t).identity();null!=r&&c.multiply(this.getMatrixWorld(r));const l=this.getLocalMatrix(e),a=c.multiply(l),h=new i,d=new s,b=new i;a.decompose(h,d,b),e.position=h.toArray(),e.rotation=(new n).setFromQuaternion(d).toArray(),e.scale=b.toArray()}this.removeRecursive(e.id,r?.children??this.objects),null!=r?r.children=r.children.slice():this.objects=this.objects.slice(),null!=o?(o.children??(o.children=[]),o.children.push(e),o.children=o.children.slice()):(this.objects.push(e),this.objects=this.objects.slice()),this.objectChange.next(this.objects),this.callbacks.forEach(t=>t(e,!1))}getMatrixWorld(e){const t=this.findAncestorsById(e.id).reverse();t.push(e);const i=this.getLocalMatrix(t.shift());for(const e of t){const t=this.getLocalMatrix(e);i.multiply(t)}return i}getLocalMatrix(e){const o=new t,r=(new i).fromArray(e.position),c=(new s).setFromEuler((new n).fromArray(e.rotation)),l=(new i).fromArray(e.scale);return o.compose(r,c,l),o}removeObjectById(e,t=!1){const i=this.findObjectById(e);this.removeObject(i,t)}removeObject(e,t=!1){this.removeRecursive(e.id,this.objects),this.removeCallbacks.forEach(i=>i(e,t)),this.objectChange.next([...this.objects])}removeRecursive(e,t){const i=t.findIndex(t=>t.id===e);if(i>-1)t.splice(i,1);else for(const i of t)null!=i.children&&this.removeRecursive(e,i.children??[])}save(){}serialize(){return JSON.stringify(this.objects)}}export function createInitialFog(){return{id:"auto-global-fog",name:"Global fog",type:"global_fog",fog:{type:"density",color:"#b8f8ff",density:.005,near:100,far:1e3}}}export function createInitialSky(){return{id:"auto-sky",name:"Sky",type:"sky",sky:{materialId:null}}}export function createWorldEnvironment(){return{id:"auto-world-env",name:"World Environment",type:"world_env",worldEnv:{globalIllumination:{enabled:!1,intensity:1},ao:{enabled:!1,blendIntensity:1,radius:.25,distanceExponent:1,thickness:1,distanceFallOff:1,scale:1,samples:16,onlyAO:!1},toneMapping:{mapping:o,exposure:1},environment:{textureId:null,intensity:1}}}}/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -1,4 +1,5 @@
1
1
  import { Vec3Node, Vec4Node } from 'three-shader-graph';
2
+ export declare const surfaceReflectionStepsUniformName = "hology_surface_reflection_steps";
2
3
  /**
3
4
  * WebGPU reflection for late/refractive surfaces, using the opaque scene snapshot.
4
5
  * Arguments are in view space. RGB is reflected radiance; A is hit confidence
@@ -1,4 +1,4 @@
1
- import{isWgslCompiler as e,Vec4Node as t,uniforms as n}from"three-shader-graph";import{depthSampler as i}from"./depth.js";import{sceneColorSampler as r}from"./scene-sample.js";export function surfaceScreenSpaceReflection(e,t){return new o(e,t)}class o extends t{constructor(e,t){super(),this.position=e,this.normal=t}compile(t){if(!e(t))return{out:"vec4(0.0)"};const o=t.get(i),c=t.get(r),l=t.get(n.projectionMatrix),a=t.get(this.position),p=t.get(this.normal),s=`surfaceReflection_${t.variable()}`;return t.addWgslDefinition(`\nfn ${s}(position: vec3<f32>, normal: vec3<f32>, projection: mat4x4<f32>) -> vec4<f32> {\n let direction = reflect(normalize(position), normalize(normal));\n let origin = position + normal * 0.04;\n let size = vec2<i32>(textureDimensions(${o}Texture));\n var previousDistance = 0.0;\n var previousDelta = -1.0;\n // Quadratic spacing concentrates work near the receiver. Fixed budget, no\n // screen-sized loop: 32 depth taps plus five refinement taps on a crossing.\n for (var i = 1; i <= 32; i += 1) {\n let t = f32(i) / 32.0;\n let distance = 60.0 * t * t;\n let point = origin + direction * distance;\n let clip = projection * vec4<f32>(point, 1.0);\n if (clip.w <= 0.0 || clip.z <= 0.0) { break; }\n let uv = clip.xy / clip.w * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5);\n if (any(uv <= vec2<f32>(0.0)) || any(uv >= vec2<f32>(1.0))) { break; }\n let pixel = clamp(vec2<i32>(uv * vec2<f32>(size)), vec2<i32>(0), size - vec2<i32>(1));\n let d = textureLoad(${o}Texture, pixel, 0).r;\n let sceneZ = (projection[3].z - d * projection[3].w) / (d * projection[2].w - projection[2].z);\n let delta = sceneZ - point.z;\n if (d < 1.0 && delta >= 0.0 && previousDelta < 0.0) {\n var low = previousDistance;\n var high = distance;\n var hitUv = uv;\n var hitDelta = delta;\n for (var j = 0; j < 5; j += 1) {\n let mid = (low + high) * 0.5;\n let p = origin + direction * mid;\n let projected = projection * vec4<f32>(p, 1.0);\n let sampleUv = projected.xy / projected.w * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5);\n let coord = clamp(vec2<i32>(sampleUv * vec2<f32>(size)), vec2<i32>(0), size - vec2<i32>(1));\n let sampleDepth = textureLoad(${o}Texture, coord, 0).r;\n let z = (projection[3].z - sampleDepth * projection[3].w) / (sampleDepth * projection[2].w - projection[2].z);\n if (z - p.z >= 0.0 && sampleDepth < 1.0) {\n high = mid; hitUv = sampleUv; hitDelta = z - p.z;\n } else { low = mid; }\n }\n // Reject silhouette crossings that never converge onto a real surface.\n if (hitDelta < 0.25) {\n let edge = min(min(hitUv.x, hitUv.y), min(1.0 - hitUv.x, 1.0 - hitUv.y));\n let confidence = smoothstep(0.0, 0.08, edge) * (1.0 - high / 60.0);\n let reflected = textureSampleLevel(${c}Texture, ${c}Sampler, hitUv, 0.0).rgb;\n return vec4<f32>(reflected, confidence);\n }\n return vec4<f32>(0.0);\n }\n previousDistance = distance;\n previousDelta = delta;\n }\n return vec4<f32>(0.0);\n}`),{out:`${s}(${a}, ${p}, ${l})`}}}/*
1
+ import{isWgslCompiler as e,Vec4Node as t,uniformFloat as n,uniforms as i}from"three-shader-graph";import{depthSampler as r}from"./depth.js";import{sceneColorSampler as o}from"./scene-sample.js";export const surfaceReflectionStepsUniformName="hology_surface_reflection_steps";const c=n("hology_surface_reflection_steps",32);export function surfaceScreenSpaceReflection(e,t){return new s(e,t)}class s extends t{constructor(e,t){super(),this.position=e,this.normal=t}compile(t){if(!e(t))return{out:"vec4(0.0)"};const n=t.get(r),s=t.get(o),l=t.get(c),p=t.get(i.projectionMatrix),a=t.get(this.position),v=t.get(this.normal),f=`surfaceReflection_${t.variable()}`;return t.addWgslDefinition(`\nfn ${f}(position: vec3<f32>, normal: vec3<f32>, projection: mat4x4<f32>, steps: f32) -> vec4<f32> {\n if (steps <= 0.0) { return vec4<f32>(0.0); }\n let direction = reflect(normalize(position), normalize(normal));\n let origin = position + normal * 0.04;\n let size = vec2<i32>(textureDimensions(${n}Texture));\n var previousDistance = 0.0;\n var previousDelta = -1.0;\n // Quadratic spacing concentrates work near the receiver. Fixed budget, no\n // screen-sized loop: up to 32 depth taps plus five refinement taps on a crossing.\n for (var i = 1; i <= 32; i += 1) {\n if (f32(i) > steps) { break; }\n let t = f32(i) / steps;\n let distance = 60.0 * t * t;\n let point = origin + direction * distance;\n let clip = projection * vec4<f32>(point, 1.0);\n if (clip.w <= 0.0 || clip.z <= 0.0) { break; }\n let uv = clip.xy / clip.w * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5);\n if (any(uv <= vec2<f32>(0.0)) || any(uv >= vec2<f32>(1.0))) { break; }\n let pixel = clamp(vec2<i32>(uv * vec2<f32>(size)), vec2<i32>(0), size - vec2<i32>(1));\n let d = textureLoad(${n}Texture, pixel, 0).r;\n let sceneZ = (projection[3].z - d * projection[3].w) / (d * projection[2].w - projection[2].z);\n let delta = sceneZ - point.z;\n if (d < 1.0 && delta >= 0.0 && previousDelta < 0.0) {\n var low = previousDistance;\n var high = distance;\n var hitUv = uv;\n var hitDelta = delta;\n for (var j = 0; j < 5; j += 1) {\n let mid = (low + high) * 0.5;\n let p = origin + direction * mid;\n let projected = projection * vec4<f32>(p, 1.0);\n let sampleUv = projected.xy / projected.w * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5);\n let coord = clamp(vec2<i32>(sampleUv * vec2<f32>(size)), vec2<i32>(0), size - vec2<i32>(1));\n let sampleDepth = textureLoad(${n}Texture, coord, 0).r;\n let z = (projection[3].z - sampleDepth * projection[3].w) / (sampleDepth * projection[2].w - projection[2].z);\n if (z - p.z >= 0.0 && sampleDepth < 1.0) {\n high = mid; hitUv = sampleUv; hitDelta = z - p.z;\n } else { low = mid; }\n }\n // Reject silhouette crossings that never converge onto a real surface.\n if (hitDelta < 0.25) {\n let edge = min(min(hitUv.x, hitUv.y), min(1.0 - hitUv.x, 1.0 - hitUv.y));\n let confidence = smoothstep(0.0, 0.08, edge) * (1.0 - high / 60.0);\n let reflected = textureSampleLevel(${s}Texture, ${s}Sampler, hitUv, 0.0).rgb;\n return vec4<f32>(reflected, confidence);\n }\n return vec4<f32>(0.0);\n }\n previousDistance = distance;\n previousDelta = delta;\n }\n return vec4<f32>(0.0);\n}`),{out:`${f}(${a}, ${v}, ${p}, ${l})`}}}/*
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{applySkyMaterialSettings as e,createSky as t}from"../scene/sky.js";import{describe as o,expect as n,it as a,vi as r}from"vitest";import{DataTexture as i,Material as s,MeshPhysicalMaterial as l,MeshStandardMaterial as d,RGBAFormat as h,SpriteMaterial as u,Vector4 as c}from"three";import{batchingUniformVec4 as m,float as p,NodeShaderMaterial as f,rgb as g,rgba as v,standardMaterial as S,translateX as w,uniformFloat as x,uniformVec4 as C,uniformSampler2d as y,varyingAttributes as B,vec3 as M,vec4 as b}from"three-shader-graph";import{createNodeMaterialSignature as L,markMaterialUniformValuesNeedUpdate as _,NodeMaterialWebGpuResolver as T,setMaterialUniformValuesAutoUpdate as W,withThreeDefaultVertexAttributeValues as G}from"../rendering/node-material-webgpu-resolver.js";import{StandardShader as U}from"../shader/builtin/standard-shader.js";import{LandscapeCompositeShader as A}from"../shader/builtin/landscape-composite-shader.js";import{DecalUnlitShader as k}from"../shader/builtin/decal-unlit-shader.js";import{getSpritePosition as V,SpriteShader as D}from"../shader/sprite-shader.js";import{depthNormal as I,depthWorldPosition as j,fragmentLinearEyeDepth as q,sampleSceneDepth as O,screenUV as E}from"../shader-nodes/depth.js";import{sampleSceneColor as F}from"../shader-nodes/scene-sample.js";import{applyDithering as P}from"../shader-nodes/dither.js";import{parallaxOcclusionMapping as H}from"../shader-nodes/pom.js";import{elapsedTimeUniformName as N}from"../shader-nodes/time.js";import{Trail as z}from"../effects/vfx/trail-renderer.js";import{particleUniforms as R}from"../shader-nodes/particle.js";import{buildShaderGraphMaterial as Y,createDefaultShaderGraphDocument as J}from"../shader/graph/index.js";class K extends s{set uniformValuesNeedsUpdate(e){e&&this.uniformValuesAutoVersion++}constructor(e){super(),this.disposed=!1,this.uniformValuesAutoUpdate=!0,this.uniformValuesAutoVersion=0,this.vertexShader=e.vertexShader,this.fragmentShader=e.fragmentShader,this.depthVertexShader=e.depthVertexShader??null,this.depthFragmentShader=e.depthFragmentShader??null,this.pointShadowVertexShader=e.pointShadowVertexShader??null,this.pointShadowFragmentShader=e.pointShadowFragmentShader??null,this.uniforms=e.uniforms,this.sceneTextures=e.sceneTextures??{},this.graphTextures=e.graphTextures??{},this.vertexAttributes=e.vertexAttributes??[],this.transparent=e.transparent??!1,this.alphaTest=e.alphaTest??0,this.depthTest=e.depthTest??!0,this.depthWrite=e.depthWrite??!0,this.side=e.side??this.side}dispose(){this.disposed=!0,super.dispose()}}o("NodeMaterialWebGpuResolver",()=>{a("renders custom sky materials at far depth without modifying their authored graph",()=>{const t=(new U).build(),o=t.nodeMaterialDefinition.position;e(t);const a=new T({WGSLShaderMaterial:K}),r=a.resolve(t,"late");n(r).toBeDefined(),n(r.vertexShader).toMatch(/vec4<f32>\(\w+\.x, \w+\.y, (\w+)\.w, \1\.w\)/),n(r.vertexShader).toContain("vec4<f32>(input.position, 0.0)"),n(r.depthWrite).toBe(!1),n(r.depthTest).toBe(!0),n(r.transparent).toBe(!0),n(t.nodeMaterialDefinition.position).toBe(o),a.dispose()}),a("keeps the default sky at far depth on WebGL and WebGPU",()=>{const e=t(),o=e.material;n(e.frustumCulled).toBe(!1),n(o.vertexShader).toMatch(/vec4\(\w+\.x,\w+\.y,(\w+)\.w,\1\.w\)/);const a=new T({WGSLShaderMaterial:K}),r=a.resolve(o,"late");n(r.vertexShader).toMatch(/vec4<f32>\(\w+\.x, \w+\.y, (\w+)\.w, \1\.w\)/),n(o.fog).toBe(!0),n(o.fragmentShader).toContain("fogDensity"),n(r.fragmentShader).toContain("rendererFogDensity()"),e.geometry.dispose(),a.dispose()}),a("compiles physical lobes and binds transmission to the HDR late-pass snapshot",()=>{const e=new U;e.clearcoat=1,e.iridescence=1,e.sheen=.4,e.anisotropy=.5,e.transmission=1,e.thickness=.2,e.dispersion=.5;const t=e.build();n(t.uniforms.hology_transmission_map).toBeDefined();const o=new T({WGSLShaderMaterial:K}),a=o.resolve(t,"late");n(a.graphTextures.hology_transmission_map).toMatchObject({resource:"physicalTransmissionSnapshot",requiresLatePass:!0}),n(a.fragmentShader).toContain("tsgTransmission_volume"),n(a.fragmentShader).toContain("tsgIridescence_eval"),n(a.fragmentShader).toContain("textureNumLevels"),n(a.fragmentShader).not.toContain("vec3 "),n(a.fragmentShader).not.toMatch(/\bmat3\(/),o.dispose()}),a("omits disabled physical features from ordinary materials",()=>{const e=new T({WGSLShaderMaterial:K}),t=(new U).build(),o=e.resolve(t,"opaque");n(o.graphTextures).toEqual({}),n(t.uniforms.transmission).toBeUndefined(),n(t.uniforms.clearcoat).toBeUndefined(),n(t.uniforms.iridescence).toBeUndefined(),n(o.fragmentShader).not.toContain("tsgTransmission_volume"),e.dispose()}),a("preserves native physical properties and updates enabled lobe uniforms",()=>{const e=new T({WGSLShaderMaterial:K}),t=new l({clearcoat:.5,iridescence:.2}),o=e.resolve(t);t.clearcoat=.7,n(e.resolve(t)).toBe(o),n(o.uniforms.clearcoat.value).toBe(.7),t.transmission=1;const a=e.resolve(t);n(a).not.toBe(o),n(a.fragmentShader).toContain("tsgTransmission_volume"),n(a.graphTextures.hology_transmission_map).toMatchObject({requiresLatePass:!0}),e.dispose()}),a("keeps explicitly tracked outline uniforms clean until controls change",()=>{const e=new f({color:S({color:g(16777215)})});e.uniformValuesAutoUpdate=!1,e.userData.outlineParameters={thickness:.012,darkening:.65,alpha:.8};const t=new T({WGSLShaderMaterial:K}),o=t.resolve(e,"outline");n(o.uniformValuesAutoUpdate).toBe(!1);const a=o.uniformValuesAutoVersion;n(t.resolve(e,"outline")).toBe(o),n(o.uniformValuesAutoVersion).toBe(a),e.userData.outlineParameters.thickness=.025,t.resolve(e,"outline"),n(o.uniforms.outlineThickness.value).toBe(.025),n(o.uniformValuesAutoVersion).toBeGreaterThan(a),t.dispose()}),a("derives a textured, darkened albedo outline and updates its controls",()=>{const e=new i(new Uint8Array([128,96,64,255]),1,1,h);e.needsUpdate=!0;const t=y("outlineAlbedo",e).sample(B.uv),o=new f({color:S({color:t})});o.userData.outlineParameters={colorMode:"darkenedAlbedo",thickness:.012,darkening:.65,alpha:.8};const a=new T({WGSLShaderMaterial:K}),r=a.resolve(o,"outline");n(r).toBeInstanceOf(K),n(r.side).toBe(1),n(r.vertexShader).toContain("materialUniforms.outlineThickness"),n(r.vertexAttributes).toEqual(n.arrayContaining([n.objectContaining({name:"skinIndex"}),n.objectContaining({name:"skinWeight"})])),n(r.vertexShader).toContain("rendererSkinPosition"),n(r.vertexShader).toContain("outline_local_position_"),n(r.vertexShader).toContain("outline_local_normal_"),n(r.vertexShader).toContain("sqrt("),n(r.fragmentShader).toContain("outlineAlbedoTexture"),n(r.fragmentShader).toContain("materialUniforms.outlineDarkening"),n(r.uniforms.outlineThickness.value).toBe(.012),n(r.uniforms.outlineDarkening.value).toBe(.65),n(r.uniforms.outlineAlpha.value).toBe(.8),o.userData.outlineParameters.darkening=2,o.userData.outlineParameters.thickness=.02;const s=a.resolve(o,"outline");n(s).toBe(r),n(s.uniforms.outlineThickness.value).toBe(.02),n(s.uniforms.outlineDarkening.value).toBe(1)}),a("declines an albedo outline when the source graph has no albedo output",()=>{const e=new f({color:v(16777215)}),t=new T({WGSLShaderMaterial:K});n(t.resolve(e,"outline")).toBeUndefined()}),a("compiles shader graph scene-light nodes against the renderer light ABI",()=>{const e=J("surface");e.shadingModel="unlit",e.nodes.push({id:"ambient",kind:"scene.ambientLight"},{id:"directional",kind:"scene.directionalLight"},{id:"combined",kind:"math.add"}),e.edges.push({from:{nodeId:"ambient",port:"color"},to:{nodeId:"combined",port:"a"}},{from:{nodeId:"directional",port:"color"},to:{nodeId:"combined",port:"b"}}),e.outputs.color={input:{nodeId:"combined",port:"value"}};const t=new T({WGSLShaderMaterial:K}).resolve(Y(e));n(t?.fragmentShader).toContain("rendererAmbientLight()"),n(t?.fragmentShader).toContain("rendererDirectionalLightForLight"),n(t?.fragmentShader).toContain("rendererDirectionalLightCount()"),n(t?.fragmentShader).not.toContain("NUM_DIR_LIGHTS")}),a("compiles the shader graph world normal without a WGSL matrix inverse",()=>{const e=J("surface");e.shadingModel="unlit",e.nodes.push({id:"world-normal",kind:"builtin.worldNormal"}),e.outputs.color={input:{nodeId:"world-normal",port:"value"}};const t=new T({WGSLShaderMaterial:K}).resolve(Y(e));n(t?.vertexShader).toContain("transpose(rendererViewMatrix())"),n(t?.vertexShader).not.toContain("nodeInverseMat4")}),a("compiles decal surface rejection without undefined WGSL expressions",()=>{const e=(new k).build(),t=new T({WGSLShaderMaterial:K}).resolve(e);n(t?.fragmentShader).not.toContain("undefined"),n(t?.fragmentShader).toContain("hology_scene_mapTexture")}),a("recompiles an opaque variant when receiveDecals changes",()=>{const e=new f({color:v(16777215)});e.userData.receiveDecals=!0;const t=new T({WGSLShaderMaterial:K},e=>[v(16777215,!1===e.userData.receiveDecals?0:2)]),o=t.resolve(e,"opaque");e.userData.receiveDecals=!1,e.needsUpdate=!0;const a=t.resolve(e,"opaque");n(a).not.toBe(o),n(o.disposed).toBe(!0),n(o.fragmentShader).not.toBe(a.fragmentShader)}),a("maps Hology elapsed time to the renderer frame clock",()=>{const e=x(N),t=new f({color:v(M(e)),transform:w(e)}),o=new T({WGSLShaderMaterial:K}).resolve(t);n(o?.vertexShader).toContain("rendererElapsedTime()"),n(o?.fragmentShader).toContain("rendererElapsedTime()"),n(o?.depthVertexShader).toContain("rendererElapsedTime()"),n(o?.pointShadowVertexShader).toContain("rendererElapsedTime()"),n(o?.uniforms).not.toHaveProperty(N)}),a("compiles the native sprite billboard position without a GLSL fallback",()=>{const e=new f({color:v(16777215),position:V(x("rotation",.25),x("screenSpaceSize",0),x("lockYAxis",1))}),t=new T({WGSLShaderMaterial:K}).resolve(e);n(t).toBeInstanceOf(K),n(t?.vertexShader).toContain("cos("),n(t?.vertexShader).toContain("sin("),n(t?.vertexShader).toContain("materialUniforms.lockYAxis"),n(t?.vertexShader).not.toContain("customFunction_")}),a("shares lens-flare occlusion helpers across all five depth samples",()=>{const e=new D;e.lenseFlare=!0;const t=new f({color:e.output().color}),o=new T({WGSLShaderMaterial:K}).resolve(t);n(o).toBeInstanceOf(K);const a=o?.vertexShader??"",r=a.match(/fn tsg_function_\d+\([^]*?\n}/g)??[];n(r).toHaveLength(2);for(const e of r){const t=e.match(/fn (tsg_function_\d+)/)?.[1];n(t).toBeDefined(),n(a.split(t+"(").length-1).toBe(6),n(e).not.toMatch(/textureSample|materialUniforms|renderer/)}n(a.match(/textureSampleLevel\(/g)).toHaveLength(5),n(t.vertexShader).toMatch(/tsg_function_\d+\(/)}),a("uses the per-instance rotation attribute for instanced sprites",()=>{const e=new f({color:v(16777215),position:V(x("rotation",0),void 0,void 0,!0)}),t=new T({WGSLShaderMaterial:K}).resolve(e);n(t?.vertexAttributes).toEqual(n.arrayContaining([n.objectContaining({name:"rotation",format:"float32",stepMode:"instance"})])),n(t?.vertexShader).toContain("input.rotation"),n(t?.vertexShader).not.toContain("materialUniforms.rotation")}),a("uses per-instance particle opacity in WebGPU fragment shaders",()=>{const e=new f({color:v(g(16777215),R.opacity)}),t=new T({WGSLShaderMaterial:K}).resolve(e);n(t?.vertexAttributes).toEqual(n.arrayContaining([n.objectContaining({name:"particleData",format:"float32x3",stepMode:"instance"})])),n(t?.fragmentShader).toContain(".x"),n(t?.fragmentShader).toContain("> 0.5")}),a("maps THREE.SpriteMaterial to the native sprite shader",()=>{const e=new i(new Uint8Array([255,255,255,255]),1,1,h),t=new u({map:e,color:8429760,opacity:.6,rotation:.25,sizeAttenuation:!1,alphaTest:.2,transparent:!0}),o=new T({WGSLShaderMaterial:K}),a=o.resolve(t);n(a).toBeInstanceOf(K),n(a?.vertexShader).toContain("materialUniforms.rotation"),n(a?.vertexShader).toContain("materialUniforms.screenSpaceSize"),n(a?.fragmentShader).toContain("textureSample("),n(a?.alphaTest).toBe(.2),n(a?.transparent).toBe(!0),n(a?.uniforms.rotation.value).toBe(.25),n(a?.uniforms.screenSpaceSize.value).toBe(1),t.rotation=.75;const r=o.resolve(t);n(r).toBe(a),n(r?.uniforms.rotation.value).toBe(.75)}),a("memoizes node-material signatures for repeated lookups",()=>{const e=["version","vertexShader","fragmentShader","alphaTest","alphaToCoverage","blending","colorWrite","depthFunc","depthTest","depthWrite","polygonOffset","polygonOffsetFactor","polygonOffsetUnits","premultipliedAlpha","side","stencilWrite","transparent"],t=new Map([["version",1],["vertexShader","#vertex"],["fragmentShader","#fragment"],["alphaTest",0],["alphaToCoverage",!1],["blending",1],["colorWrite",!0],["depthFunc",3],["depthTest",!0],["depthWrite",!0],["polygonOffset",!1],["polygonOffsetFactor",0],["polygonOffsetUnits",0],["premultipliedAlpha",!1],["side",0],["stencilWrite",!1],["transparent",!1]]);let o=0;const a=new Proxy({},{get:(n,a)=>"string"==typeof a&&e.includes(a)?(o+=1,t.get(a)):Reflect.get(n,a)});L(a),L(a),n(o).toBe(e.length)}),a("adapts a NodeShaderMaterial without replacing its scene material",()=>{const e=x("intensity",.75),t=new f({color:S({color:g(5210052).multiplyScalar(e),roughness:.42,metalness:.18}),transparent:!0,alphaTest:.25}),o=new T({WGSLShaderMaterial:K}),a=o.resolve(t);n(a).toBeInstanceOf(K),n(a).not.toBe(t),n(a?.transparent).toBe(!0),n(a?.alphaTest).toBe(.25),n(a?.fragmentShader).toContain("rendererPointLightForLight"),n(t.isShaderMaterial).toBe(!0),e.value=.25,n(o.resolve(t)).toBe(a),n(a?.uniforms.intensity?.value).toBe(.25)}),a("syncs direct NodeShaderMaterial uniform mutations to the resolved material",()=>{const e=C("trailTint",new c(0,0,0,0)),t=x("hology_particle_energy",1),o=new f({color:e.multiplyScalar(t)}),a=new T({WGSLShaderMaterial:K});o.uniforms.trailTint.value=new c(1,.25,.5,.75),o.uniforms.hology_particle_energy.value=.4;const r=a.resolve(o);n(r?.uniforms.trailTint.value).toEqual(new c(1,.25,.5,.75)),n(r?.uniforms.hology_particle_energy.value).toBe(.4),o.uniforms.trailTint.value.set(.2,.4,.6,.8),o.uniforms.hology_particle_energy.value=.1,n(a.resolve(o)).toBe(r),n(r?.uniforms.trailTint.value).toEqual(new c(.2,.4,.6,.8)),n(r?.uniforms.hology_particle_energy.value).toBe(.1)}),a("only syncs explicitly invalidated uniforms when auto update is disabled",()=>{const e=x("intensity",.75),t=new f({color:v(M(e))});t.uniformValuesAutoUpdate=!1;const o=new T({WGSLShaderMaterial:K}),a=o.resolve(t),r=a.uniformValuesAutoVersion;e.value=.25,n(o.resolve(t)).toBe(a),n(a?.uniforms.intensity.value).toBe(.75),n(a?.uniformValuesAutoVersion).toBe(r),t.uniformValuesNeedsUpdate=!0,n(o.resolve(t)).toBe(a),n(a?.uniforms.intensity.value).toBe(.25),n(a?.uniformValuesAutoVersion).toBe(r+1)}),a("supports explicit invalidation for mapped Three.js materials",()=>{const e=new d({roughness:.8});W(e,!1);const t=new T({WGSLShaderMaterial:K}),o=t.resolve(e);e.roughness=.2,n(t.resolve(e)).toBe(o),n(o?.uniforms.roughness.value).toBe(.8),_(e),n(t.resolve(e)).toBe(o),n(o?.uniforms.roughness.value).toBe(.2)}),a("binds uniforms referenced only by an MRT material output",()=>{const e=x("roughness",.7),t=new f({color:v(5601177),alphaTest:.1});t.outputRoughness=e;const o=new T({WGSLShaderMaterial:K},e=>[e.outputColor,b(M(0,0,1),e.outputRoughness)]).resolve(t);n(o?.fragmentShader).toContain("materialUniforms.roughness"),n(o?.fragmentShader).not.toContain("graphColor"),n(o?.fragmentShader).toMatch(/if \(.+ < 0\.1\) \{ discard; \}/),n(o?.uniforms.roughness?.value).toBe(.7)}),a("compiles a landscape composite built from node material semantic outputs",()=>{const e=new T({WGSLShaderMaterial:K}),t=new f({color:S({color:g(5147450),roughness:.82,metalness:0})}),o=new f({color:S({color:g(7432286),roughness:.6,metalness:.05})}),a=new A;a.layers=[t,o];const r=e.resolve(a.build());n(r).toBeInstanceOf(K),n(r?.fragmentShader).toContain("rendererDirectionalLightForLight"),n(r?.vertexShader).toContain("@location(6) material: vec4<f32>"),n(r?.vertexShader).toContain("@location(8) hole: f32")}),a("allocates the trail's authored vertex attributes",()=>{const e=new T({WGSLShaderMaterial:K}).resolve(z.createBaseMaterial());n(e).toBeInstanceOf(K),n(e?.vertexAttributes).toEqual(n.arrayContaining([n.objectContaining({name:"nodeID",shaderLocation:6,format:"float32"}),n.objectContaining({name:"nodeCenter",shaderLocation:7,format:"float32x3"})]))}),a("uses Three's default value when a geometry has no uv1 attribute",()=>{const e=G([{name:"uv1",shaderLocation:6,format:"float32x2"},{name:"custom",shaderLocation:7,format:"float32"}]);n(e).toEqual(n.arrayContaining([n.objectContaining({name:"uv1",defaultValue:[0,0]}),n.objectContaining({name:"custom"})])),n(e[1]).not.toHaveProperty("defaultValue")}),a("compiles the foliage transform and named texture-uniform pattern",()=>{const e=new T({WGSLShaderMaterial:K}),t=new i(new Uint8Array([255,255,255,255]),1,1,h),o=new f({color:y("map").sample(B.uv),transform:w(x("windOffset",.25)),uniforms:{map:{value:t}},alphaTest:.5}),a=e.resolve(o);n(a).toBeInstanceOf(K),n(a?.vertexShader).toContain("rendererClipPosition(_ng_localPosition"),n(a?.depthVertexShader).toContain("materialUniforms.windOffset"),n(a?.depthFragmentShader).toContain("textureSample("),n(a?.depthFragmentShader).toContain("< 0.5) { discard;"),n(a?.pointShadowFragmentShader).toContain("distanceToLight")}),a("rebuilds cached materials when scene-local texture bindings change",()=>{const e=new T({WGSLShaderMaterial:K}),t=new i(new Uint8Array([255,255,255,255]),1,1,h),o=new i(new Uint8Array([32,64,128,255]),1,1,h),a=new f({color:y("map").sample(B.uv),uniforms:{map:{value:t}},alphaTest:.5}),r=e.resolve(a);a.uniforms.map.value=o;const s=e.resolve(a);n(s).not.toBe(r),n(r.disposed).toBe(!0),a.dispose();const l=e.resolve(a);n(l).not.toBe(s),n(s.disposed).toBe(!0)}),a("uses the renderer batch id without Three’s batchingIdTexture",()=>{const e=new T({WGSLShaderMaterial:K}),t=new i(new Uint8Array([255,96,24,255]),1,1,h),o=new f({color:v(m("vp0")),uniforms:{vp0:{value:t}}}),a=e.resolve(o);n(a).toBeInstanceOf(K),n(a?.vertexShader).toContain("rendererBatchingId(input.instanceIndex)")}),a("compiles depth nodes that use fragment-coordinate expressions",()=>{const e=new T({WGSLShaderMaterial:K}),t=new f({color:v(M(E.x,E.y,q))}),o=e.resolve(t);n(o).toBeInstanceOf(K),n(o?.fragmentShader).toContain("input.position.xy"),n(o?.fragmentShader).toContain("input.position.z"),n(o?.fragmentShader).toContain("rendererViewportSize()"),n(o?.fragmentShader).not.toContain("gl_FragCoord")}),a("maps scene color and depth nodes to renderer-owned textures",()=>{const e=new T({WGSLShaderMaterial:K}),t=new f({color:F(E).multiplyScalar(O(E))}),o=e.resolve(t);n(o.sceneTextures).toEqual({hology_scene_map:"sceneColor",hology_depth_map:"sceneDepth"}),n(o.graphTextures).toEqual({}),n(o.fragmentShader).toContain("hology_scene_mapTexture"),n(o.fragmentShader).toContain("hology_depth_mapTexture"),n(o.fragmentShader).toContain("vec4<f32>(textureSample(hology_depth_mapTexture")}),a("caches distinct material variants for opaque and late draw stages",()=>{const e=new T({WGSLShaderMaterial:K},(e,t)=>"opaque"===t?[v(M(1)),v(M(0))]:void 0),t=new f({color:v(M(1))}),o=e.resolve(t,"opaque"),a=e.resolve(t,"late");n(o).not.toBe(a),n(e.resolve(t,"opaque")).toBe(o),n(e.resolve(t,"late")).toBe(a),n(o?.fragmentShader).toContain("@location(1)"),n(a?.fragmentShader).not.toContain("@location(1)")}),a("reconstructs WebGPU depth positions with inverted NDC Y",()=>{const e=new T({WGSLShaderMaterial:K}),t=new f({color:v(j.add(I.multiplyScalar(0)))}),o=e.resolve(t),a=o.fragmentShader.match(/fn tsg_function_\d+\([^]*?\n}/g)??[];n(a).toHaveLength(1);const r=a[0]??"",i=r.match(/fn (tsg_function_\d+)/)?.[1];n(i).toBeDefined(),n(o.fragmentShader.split(i+"(").length-1).toBe(4),n(r).not.toMatch(/textureSample|renderer|materialUniforms/),n(r).toContain("0.000001");const s=e.resolve(new f({color:v(j)}));n(s?.fragmentShader).toContain(r),n(t.fragmentShader).toMatch(/tsg_function_\d+\(/),n(o.fragmentShader).toContain("vec2<f32>(2.0, -2.0)"),n(o.fragmentShader).toMatch(/-\(float_op_\d+\)/)}),a("emits dither and parallax nodes without GLSL custom functions",()=>{const e=new T({WGSLShaderMaterial:K}),t=new i(new Uint8Array([128,128,128,255]),1,1,h),o=H(B.uv,y("heightMap",t),p(.03)),a=new f({color:b(o.x,o.y,P(M(.2,.4,.8)).z,1)}),r=e.resolve(a);n(r).toBeInstanceOf(K),n(r?.fragmentShader).toContain("textureSampleLevel(heightMapTexture, heightMapSampler"),n(r?.fragmentShader).toMatch(/= -dpdy\(-pom_\d+ViewPosition\)/),n(r?.fragmentShader).toMatch(/= -dpdy\(pom_\d+Uv\)/),n(r?.fragmentShader).not.toContain("customFunction_"),n(r?.fragmentShader).not.toContain("gl_FragCoord")}),a("inherits, updates, and clears the WebGPU world environment",()=>{const e=new i(new Uint8Array([255,255,255,255]),1,1,h),t=new f({color:S({color:g(8421504),roughness:.5,metalness:.1})}),o=new T({WGSLShaderMaterial:K});o.setWorldEnvironment({texture:e,intensity:.75});const a=o.resolve(t);n(a.fragmentShader).toContain("envMapTexture"),n(a.uniforms.envMapIntensity?.value).toBe(.75),n(t.envMap).toBeUndefined(),o.setWorldEnvironment({texture:e,intensity:.25});const r=o.resolve(t);n(r).toBe(a),n(r.uniforms.envMapIntensity?.value).toBe(.25),o.setWorldEnvironment(null);const s=o.resolve(t);n(s).not.toBe(a),n(a.disposed).toBe(!0),n(s.fragmentShader).not.toContain("envMapTexture")}),a("keeps an authored material environment ahead of the world environment",()=>{const e=new i(new Uint8Array([255,255,255,255]),1,1,h),t=new i(new Uint8Array([64,96,128,255]),1,1,h),o=new i(new Uint8Array([32,48,64,255]),1,1,h),a=new f({color:S({color:g(8421504),roughness:.5,metalness:.1}),envMap:t}),s=r.fn(()=>o),l=new T({WGSLShaderMaterial:K},void 0,s);l.setWorldEnvironment({texture:e,intensity:.2}),l.resolve(a),n(s).toHaveBeenCalledWith(t),n(a.uniforms.envMap.value).toBe(o),n(a.envMap).toBe(t)})});/*
1
+ import{applySkyMaterialSettings as e,createSky as t}from"../scene/sky.js";import{describe as o,expect as n,it as a,vi as r}from"vitest";import{DataTexture as s,Material as i,MeshPhysicalMaterial as l,MeshStandardMaterial as d,RGBAFormat as h,SpriteMaterial as u,Vector4 as c}from"three";import{batchingUniformVec4 as m,float as p,NodeShaderMaterial as f,rgb as v,rgba as g,standardMaterial as S,translateX as w,uniformFloat as x,uniformVec4 as B,uniformSampler2d as C,varyingAttributes as y,vec3 as M,vec4 as b}from"three-shader-graph";import{createNodeMaterialSignature as L,markMaterialUniformValuesNeedUpdate as _,NodeMaterialWebGpuResolver as T,setMaterialUniformValuesAutoUpdate as W,withThreeDefaultVertexAttributeValues as G}from"../rendering/node-material-webgpu-resolver.js";import{StandardShader as U}from"../shader/builtin/standard-shader.js";import{LandscapeCompositeShader as A}from"../shader/builtin/landscape-composite-shader.js";import{DecalUnlitShader as D}from"../shader/builtin/decal-unlit-shader.js";import{getSpritePosition as k,SpriteShader as I}from"../shader/sprite-shader.js";import{depthNormal as V,depthWorldPosition as j,fragmentLinearEyeDepth as q,sampleSceneDepth as F,screenUV as O}from"../shader-nodes/depth.js";import{sampleSceneColor as E}from"../shader-nodes/scene-sample.js";import{applyDithering as P}from"../shader-nodes/dither.js";import{parallaxOcclusionMapping as H}from"../shader-nodes/pom.js";import{elapsedTimeUniformName as N}from"../shader-nodes/time.js";import{Trail as z}from"../effects/vfx/trail-renderer.js";import{particleUniforms as R}from"../shader-nodes/particle.js";import{buildShaderGraphMaterial as Y,createDefaultShaderGraphDocument as J}from"../shader/graph/index.js";class K extends i{set uniformValuesNeedsUpdate(e){e&&this.uniformValuesAutoVersion++}constructor(e){super(),this.disposed=!1,this.uniformValuesAutoUpdate=!0,this.uniformValuesAutoVersion=0,this.vertexShader=e.vertexShader,this.fragmentShader=e.fragmentShader,this.depthVertexShader=e.depthVertexShader??null,this.depthFragmentShader=e.depthFragmentShader??null,this.pointShadowVertexShader=e.pointShadowVertexShader??null,this.pointShadowFragmentShader=e.pointShadowFragmentShader??null,this.uniforms=e.uniforms,this.sceneTextures=e.sceneTextures??{},this.graphTextures=e.graphTextures??{},this.vertexAttributes=e.vertexAttributes??[],this.transparent=e.transparent??!1,this.alphaTest=e.alphaTest??0,this.depthTest=e.depthTest??!0,this.depthWrite=e.depthWrite??!0,this.side=e.side??this.side}dispose(){this.disposed=!0,super.dispose()}}o("NodeMaterialWebGpuResolver",()=>{a("renders custom sky materials at far depth without modifying their authored graph",()=>{const t=(new U).build(),o=t.nodeMaterialDefinition.position;e(t);const a=new T({WGSLShaderMaterial:K}),r=a.resolve(t,"late");n(r).toBeDefined(),n(r.vertexShader).toMatch(/vec4<f32>\(\w+\.x, \w+\.y, (\w+)\.w, \1\.w\)/),n(r.vertexShader).toContain("vec4<f32>(input.position, 0.0)"),n(r.depthWrite).toBe(!1),n(r.depthTest).toBe(!0),n(r.transparent).toBe(!0),n(t.nodeMaterialDefinition.position).toBe(o),a.dispose()}),a("keeps the default sky at far depth on WebGL and WebGPU",()=>{const e=t(),o=e.material;n(e.frustumCulled).toBe(!1),n(o.vertexShader).toMatch(/vec4\(\w+\.x,\w+\.y,(\w+)\.w,\1\.w\)/);const a=new T({WGSLShaderMaterial:K}),r=a.resolve(o,"late");n(r.vertexShader).toMatch(/vec4<f32>\(\w+\.x, \w+\.y, (\w+)\.w, \1\.w\)/),n(o.fog).toBe(!0),n(o.fragmentShader).toContain("fogDensity"),n(r.fragmentShader).toContain("rendererFogDensity()"),e.geometry.dispose(),a.dispose()}),a("compiles physical lobes and binds transmission to the HDR late-pass snapshot",()=>{const e=new U;e.clearcoat=1,e.iridescence=1,e.sheen=.4,e.anisotropy=.5,e.transmission=1,e.thickness=.2,e.dispersion=.5;const t=e.build();n(t.uniforms.hology_transmission_map).toBeDefined();const o=new T({WGSLShaderMaterial:K}),a=o.resolve(t,"late");n(a.graphTextures.hology_transmission_map).toMatchObject({resource:"physicalTransmissionSnapshot",requiresLatePass:!0}),n(a.fragmentShader).toContain("tsgTransmission_volume"),n(a.fragmentShader).toContain("tsgIridescence_eval"),n(a.fragmentShader).toContain("textureNumLevels"),n(a.fragmentShader).not.toContain("vec3 "),n(a.fragmentShader).not.toMatch(/\bmat3\(/),o.dispose()}),a("omits disabled physical features from ordinary materials",()=>{const e=new T({WGSLShaderMaterial:K}),t=(new U).build(),o=e.resolve(t,"opaque");n(o.graphTextures).toEqual({}),n(t.uniforms.transmission).toBeUndefined(),n(t.uniforms.clearcoat).toBeUndefined(),n(t.uniforms.iridescence).toBeUndefined(),n(o.fragmentShader).not.toContain("tsgTransmission_volume"),e.dispose()}),a("preserves native physical properties and updates enabled lobe uniforms",()=>{const e=new T({WGSLShaderMaterial:K}),t=new l({clearcoat:.5,iridescence:.2}),o=e.resolve(t);t.clearcoat=.7,n(e.resolve(t)).toBe(o),n(o.uniforms.clearcoat.value).toBe(.7),t.transmission=1;const a=e.resolve(t);n(a).not.toBe(o),n(a.fragmentShader).toContain("tsgTransmission_volume"),n(a.graphTextures.hology_transmission_map).toMatchObject({requiresLatePass:!0}),e.dispose()}),a("keeps explicitly tracked outline uniforms clean until controls change",()=>{const e=new f({color:S({color:v(16777215)})});e.uniformValuesAutoUpdate=!1,e.userData.outlineParameters={thickness:.012,darkening:.65,alpha:.8};const t=new T({WGSLShaderMaterial:K}),o=t.resolve(e,"outline");n(o.uniformValuesAutoUpdate).toBe(!1);const a=o.uniformValuesAutoVersion;n(t.resolve(e,"outline")).toBe(o),n(o.uniformValuesAutoVersion).toBe(a),e.userData.outlineParameters.thickness=.025,t.resolve(e,"outline"),n(o.uniforms.outlineThickness.value).toBe(.025),n(o.uniformValuesAutoVersion).toBeGreaterThan(a),t.dispose()}),a("derives a textured, darkened albedo outline and updates its controls",()=>{const e=new s(new Uint8Array([128,96,64,255]),1,1,h);e.needsUpdate=!0;const t=C("outlineAlbedo",e).sample(y.uv),o=new f({color:S({color:t})});o.userData.outlineParameters={colorMode:"darkenedAlbedo",thickness:.012,darkening:.65,alpha:.8};const a=new T({WGSLShaderMaterial:K}),r=a.resolve(o,"outline");n(r).toBeInstanceOf(K),n(r.side).toBe(1),n(r.vertexShader).toContain("materialUniforms.outlineThickness"),n(r.vertexAttributes).toEqual(n.arrayContaining([n.objectContaining({name:"skinIndex"}),n.objectContaining({name:"skinWeight"})])),n(r.vertexShader).toContain("rendererSkinPosition"),n(r.vertexShader).toContain("outline_local_position_"),n(r.vertexShader).toContain("outline_local_normal_"),n(r.vertexShader).toContain("sqrt("),n(r.fragmentShader).toContain("outlineAlbedoTexture"),n(r.fragmentShader).toContain("materialUniforms.outlineDarkening"),n(r.uniforms.outlineThickness.value).toBe(.012),n(r.uniforms.outlineDarkening.value).toBe(.65),n(r.uniforms.outlineAlpha.value).toBe(.8),o.userData.outlineParameters.darkening=2,o.userData.outlineParameters.thickness=.02;const i=a.resolve(o,"outline");n(i).toBe(r),n(i.uniforms.outlineThickness.value).toBe(.02),n(i.uniforms.outlineDarkening.value).toBe(1)}),a("declines an albedo outline when the source graph has no albedo output",()=>{const e=new f({color:g(16777215)}),t=new T({WGSLShaderMaterial:K});n(t.resolve(e,"outline")).toBeUndefined()}),a("compiles shader graph scene-light nodes against the renderer light ABI",()=>{const e=J("surface");e.shadingModel="unlit",e.nodes.push({id:"ambient",kind:"scene.ambientLight"},{id:"directional",kind:"scene.directionalLight"},{id:"combined",kind:"math.add"}),e.edges.push({from:{nodeId:"ambient",port:"color"},to:{nodeId:"combined",port:"a"}},{from:{nodeId:"directional",port:"color"},to:{nodeId:"combined",port:"b"}}),e.outputs.color={input:{nodeId:"combined",port:"value"}};const t=new T({WGSLShaderMaterial:K}).resolve(Y(e));n(t?.fragmentShader).toContain("rendererAmbientLight()"),n(t?.fragmentShader).toContain("rendererDirectionalLightForLight"),n(t?.fragmentShader).toContain("rendererDirectionalLightCount()"),n(t?.fragmentShader).not.toContain("NUM_DIR_LIGHTS")}),a("compiles the shader graph world normal without a WGSL matrix inverse",()=>{const e=J("surface");e.shadingModel="unlit",e.nodes.push({id:"world-normal",kind:"builtin.worldNormal"}),e.outputs.color={input:{nodeId:"world-normal",port:"value"}};const t=new T({WGSLShaderMaterial:K}).resolve(Y(e));n(t?.vertexShader).toContain("transpose(rendererViewMatrix())"),n(t?.vertexShader).not.toContain("nodeInverseMat4")}),a("compiles decal surface rejection without undefined WGSL expressions",()=>{const e=(new D).build(),t=new T({WGSLShaderMaterial:K}).resolve(e);n(t?.fragmentShader).not.toContain("undefined"),n(t?.fragmentShader).toContain("hology_scene_mapTexture")}),a("recompiles an opaque variant when receiveDecals changes",()=>{const e=new f({color:g(16777215)});e.userData.receiveDecals=!0;const t=new T({WGSLShaderMaterial:K},e=>[g(16777215,!1===e.userData.receiveDecals?0:2)]),o=t.resolve(e,"opaque");e.userData.receiveDecals=!1,e.needsUpdate=!0;const a=t.resolve(e,"opaque");n(a).not.toBe(o),n(o.disposed).toBe(!0),n(o.fragmentShader).not.toBe(a.fragmentShader)}),a.each(["node","standard"])("preserves GI reception settings for %s materials across refreshes",e=>{const t="node"===e?new f({color:S({color:v(16777215)})}):new d,o=new T({WGSLShaderMaterial:K});n(o.resolve(t,"opaque")?.userData.receiveGI).toBe(!0),t.userData.receiveGI=!1,t.needsUpdate=!0;const a=o.resolve(t,"opaque");n(a.userData.receiveGI).toBe(!1),n(o.resolveSDFGISurface(a)).toBeDefined(),t.userData.receiveGI=!0,t.needsUpdate=!0;const r=o.resolve(t,"opaque");n(r?.userData.receiveGI).toBe(!0),n(r).not.toBe(a),n(a.disposed).toBe(!0),o.dispose(),t.dispose()}),a("maps Hology elapsed time to the renderer frame clock",()=>{const e=x(N),t=new f({color:g(M(e)),transform:w(e)}),o=new T({WGSLShaderMaterial:K}).resolve(t);n(o?.vertexShader).toContain("rendererElapsedTime()"),n(o?.fragmentShader).toContain("rendererElapsedTime()"),n(o?.depthVertexShader).toContain("rendererElapsedTime()"),n(o?.pointShadowVertexShader).toContain("rendererElapsedTime()"),n(o?.uniforms).not.toHaveProperty(N)}),a("compiles the native sprite billboard position without a GLSL fallback",()=>{const e=new f({color:g(16777215),position:k(x("rotation",.25),x("screenSpaceSize",0),x("lockYAxis",1))}),t=new T({WGSLShaderMaterial:K}).resolve(e);n(t).toBeInstanceOf(K),n(t?.vertexShader).toContain("cos("),n(t?.vertexShader).toContain("sin("),n(t?.vertexShader).toContain("materialUniforms.lockYAxis"),n(t?.vertexShader).not.toContain("customFunction_")}),a("shares lens-flare occlusion helpers across all five depth samples",()=>{const e=new I;e.lenseFlare=!0;const t=new f({color:e.output().color}),o=new T({WGSLShaderMaterial:K}).resolve(t);n(o).toBeInstanceOf(K);const a=o?.vertexShader??"",r=a.match(/fn tsg_function_\d+\([^]*?\n}/g)??[];n(r).toHaveLength(2);for(const e of r){const t=e.match(/fn (tsg_function_\d+)/)?.[1];n(t).toBeDefined(),n(a.split(t+"(").length-1).toBe(6),n(e).not.toMatch(/textureSample|materialUniforms|renderer/)}n(a.match(/textureSampleLevel\(/g)).toHaveLength(5),n(t.vertexShader).toMatch(/tsg_function_\d+\(/)}),a("uses the per-instance rotation attribute for instanced sprites",()=>{const e=new f({color:g(16777215),position:k(x("rotation",0),void 0,void 0,!0)}),t=new T({WGSLShaderMaterial:K}).resolve(e);n(t?.vertexAttributes).toEqual(n.arrayContaining([n.objectContaining({name:"rotation",format:"float32",stepMode:"instance"})])),n(t?.vertexShader).toContain("input.rotation"),n(t?.vertexShader).not.toContain("materialUniforms.rotation")}),a("uses per-instance particle opacity in WebGPU fragment shaders",()=>{const e=new f({color:g(v(16777215),R.opacity)}),t=new T({WGSLShaderMaterial:K}).resolve(e);n(t?.vertexAttributes).toEqual(n.arrayContaining([n.objectContaining({name:"particleData",format:"float32x3",stepMode:"instance"})])),n(t?.fragmentShader).toContain(".x"),n(t?.fragmentShader).toContain("> 0.5")}),a("maps THREE.SpriteMaterial to the native sprite shader",()=>{const e=new s(new Uint8Array([255,255,255,255]),1,1,h),t=new u({map:e,color:8429760,opacity:.6,rotation:.25,sizeAttenuation:!1,alphaTest:.2,transparent:!0}),o=new T({WGSLShaderMaterial:K}),a=o.resolve(t);n(a).toBeInstanceOf(K),n(a?.vertexShader).toContain("materialUniforms.rotation"),n(a?.vertexShader).toContain("materialUniforms.screenSpaceSize"),n(a?.fragmentShader).toContain("textureSample("),n(a?.alphaTest).toBe(.2),n(a?.transparent).toBe(!0),n(a?.uniforms.rotation.value).toBe(.25),n(a?.uniforms.screenSpaceSize.value).toBe(1),t.rotation=.75;const r=o.resolve(t);n(r).toBe(a),n(r?.uniforms.rotation.value).toBe(.75)}),a("memoizes node-material signatures for repeated lookups",()=>{const e=["version","vertexShader","fragmentShader","alphaTest","alphaToCoverage","blending","colorWrite","depthFunc","depthTest","depthWrite","polygonOffset","polygonOffsetFactor","polygonOffsetUnits","premultipliedAlpha","side","stencilWrite","transparent"],t=new Map([["version",1],["vertexShader","#vertex"],["fragmentShader","#fragment"],["alphaTest",0],["alphaToCoverage",!1],["blending",1],["colorWrite",!0],["depthFunc",3],["depthTest",!0],["depthWrite",!0],["polygonOffset",!1],["polygonOffsetFactor",0],["polygonOffsetUnits",0],["premultipliedAlpha",!1],["side",0],["stencilWrite",!1],["transparent",!1]]);let o=0;const a=new Proxy({},{get:(n,a)=>"string"==typeof a&&e.includes(a)?(o+=1,t.get(a)):Reflect.get(n,a)});L(a),L(a),n(o).toBe(e.length)}),a("adapts a NodeShaderMaterial without replacing its scene material",()=>{const e=x("intensity",.75),t=new f({color:S({color:v(5210052).multiplyScalar(e),roughness:.42,metalness:.18}),transparent:!0,alphaTest:.25}),o=new T({WGSLShaderMaterial:K}),a=o.resolve(t);n(a).toBeInstanceOf(K),n(a).not.toBe(t),n(a?.transparent).toBe(!0),n(a?.alphaTest).toBe(.25),n(a?.fragmentShader).toContain("rendererPointLightForLight"),n(t.isShaderMaterial).toBe(!0),e.value=.25,n(o.resolve(t)).toBe(a),n(a?.uniforms.intensity?.value).toBe(.25)}),a("syncs direct NodeShaderMaterial uniform mutations to the resolved material",()=>{const e=B("trailTint",new c(0,0,0,0)),t=x("hology_particle_energy",1),o=new f({color:e.multiplyScalar(t)}),a=new T({WGSLShaderMaterial:K});o.uniforms.trailTint.value=new c(1,.25,.5,.75),o.uniforms.hology_particle_energy.value=.4;const r=a.resolve(o);n(r?.uniforms.trailTint.value).toEqual(new c(1,.25,.5,.75)),n(r?.uniforms.hology_particle_energy.value).toBe(.4),o.uniforms.trailTint.value.set(.2,.4,.6,.8),o.uniforms.hology_particle_energy.value=.1,n(a.resolve(o)).toBe(r),n(r?.uniforms.trailTint.value).toEqual(new c(.2,.4,.6,.8)),n(r?.uniforms.hology_particle_energy.value).toBe(.1)}),a("only syncs explicitly invalidated uniforms when auto update is disabled",()=>{const e=x("intensity",.75),t=new f({color:g(M(e))});t.uniformValuesAutoUpdate=!1;const o=new T({WGSLShaderMaterial:K}),a=o.resolve(t),r=a.uniformValuesAutoVersion;e.value=.25,n(o.resolve(t)).toBe(a),n(a?.uniforms.intensity.value).toBe(.75),n(a?.uniformValuesAutoVersion).toBe(r),t.uniformValuesNeedsUpdate=!0,n(o.resolve(t)).toBe(a),n(a?.uniforms.intensity.value).toBe(.25),n(a?.uniformValuesAutoVersion).toBe(r+1)}),a("supports explicit invalidation for mapped Three.js materials",()=>{const e=new d({roughness:.8});W(e,!1);const t=new T({WGSLShaderMaterial:K}),o=t.resolve(e);e.roughness=.2,n(t.resolve(e)).toBe(o),n(o?.uniforms.roughness.value).toBe(.8),_(e),n(t.resolve(e)).toBe(o),n(o?.uniforms.roughness.value).toBe(.2)}),a("binds uniforms referenced only by an MRT material output",()=>{const e=x("roughness",.7),t=new f({color:g(5601177),alphaTest:.1});t.outputRoughness=e;const o=new T({WGSLShaderMaterial:K},e=>[e.outputColor,b(M(0,0,1),e.outputRoughness)]).resolve(t);n(o?.fragmentShader).toContain("materialUniforms.roughness"),n(o?.fragmentShader).not.toContain("graphColor"),n(o?.fragmentShader).toMatch(/if \(.+ < 0\.1\) \{ discard; \}/),n(o?.uniforms.roughness?.value).toBe(.7)}),a("compiles a landscape composite built from node material semantic outputs",()=>{const e=new T({WGSLShaderMaterial:K}),t=new f({color:S({color:v(5147450),roughness:.82,metalness:0})}),o=new f({color:S({color:v(7432286),roughness:.6,metalness:.05})}),a=new A;a.layers=[t,o];const r=e.resolve(a.build());n(r).toBeInstanceOf(K),n(r?.fragmentShader).toContain("rendererDirectionalLightForLight"),n(r?.vertexShader).toContain("@location(6) material: vec4<f32>"),n(r?.vertexShader).toContain("@location(8) hole: f32")}),a("allocates the trail's authored vertex attributes",()=>{const e=new T({WGSLShaderMaterial:K}).resolve(z.createBaseMaterial());n(e).toBeInstanceOf(K),n(e?.vertexAttributes).toEqual(n.arrayContaining([n.objectContaining({name:"nodeID",shaderLocation:6,format:"float32"}),n.objectContaining({name:"nodeCenter",shaderLocation:7,format:"float32x3"})]))}),a("uses Three's default value when a geometry has no uv1 attribute",()=>{const e=G([{name:"uv1",shaderLocation:6,format:"float32x2"},{name:"custom",shaderLocation:7,format:"float32"}]);n(e).toEqual(n.arrayContaining([n.objectContaining({name:"uv1",defaultValue:[0,0]}),n.objectContaining({name:"custom"})])),n(e[1]).not.toHaveProperty("defaultValue")}),a("compiles the foliage transform and named texture-uniform pattern",()=>{const e=new T({WGSLShaderMaterial:K}),t=new s(new Uint8Array([255,255,255,255]),1,1,h),o=new f({color:C("map").sample(y.uv),transform:w(x("windOffset",.25)),uniforms:{map:{value:t}},alphaTest:.5}),a=e.resolve(o);n(a).toBeInstanceOf(K),n(a?.vertexShader).toContain("rendererClipPosition(_ng_localPosition"),n(a?.depthVertexShader).toContain("materialUniforms.windOffset"),n(a?.depthFragmentShader).toContain("textureSample("),n(a?.depthFragmentShader).toContain("< 0.5) { discard;"),n(a?.pointShadowFragmentShader).toContain("distanceToLight")}),a("rebuilds cached materials when scene-local texture bindings change",()=>{const e=new T({WGSLShaderMaterial:K}),t=new s(new Uint8Array([255,255,255,255]),1,1,h),o=new s(new Uint8Array([32,64,128,255]),1,1,h),a=new f({color:C("map").sample(y.uv),uniforms:{map:{value:t}},alphaTest:.5}),r=e.resolve(a);a.uniforms.map.value=o;const i=e.resolve(a);n(i).not.toBe(r),n(r.disposed).toBe(!0),a.dispose();const l=e.resolve(a);n(l).not.toBe(i),n(i.disposed).toBe(!0)}),a("uses the renderer batch id without Three’s batchingIdTexture",()=>{const e=new T({WGSLShaderMaterial:K}),t=new s(new Uint8Array([255,96,24,255]),1,1,h),o=new f({color:g(m("vp0")),uniforms:{vp0:{value:t}}}),a=e.resolve(o);n(a).toBeInstanceOf(K),n(a?.vertexShader).toContain("rendererBatchingId(input.instanceIndex)")}),a("compiles depth nodes that use fragment-coordinate expressions",()=>{const e=new T({WGSLShaderMaterial:K}),t=new f({color:g(M(O.x,O.y,q))}),o=e.resolve(t);n(o).toBeInstanceOf(K),n(o?.fragmentShader).toContain("input.position.xy"),n(o?.fragmentShader).toContain("input.position.z"),n(o?.fragmentShader).toContain("rendererViewportSize()"),n(o?.fragmentShader).not.toContain("gl_FragCoord")}),a("maps scene color and depth nodes to renderer-owned textures",()=>{const e=new T({WGSLShaderMaterial:K}),t=new f({color:E(O).multiplyScalar(F(O))}),o=e.resolve(t);n(o.sceneTextures).toEqual({hology_scene_map:"sceneColor",hology_depth_map:"sceneDepth"}),n(o.graphTextures).toEqual({}),n(o.fragmentShader).toContain("hology_scene_mapTexture"),n(o.fragmentShader).toContain("hology_depth_mapTexture"),n(o.fragmentShader).toContain("vec4<f32>(textureSample(hology_depth_mapTexture")}),a("caches distinct material variants for opaque and late draw stages",()=>{const e=new T({WGSLShaderMaterial:K},(e,t)=>"opaque"===t?[g(M(1)),g(M(0))]:void 0),t=new f({color:g(M(1))}),o=e.resolve(t,"opaque"),a=e.resolve(t,"late");n(o).not.toBe(a),n(e.resolve(t,"opaque")).toBe(o),n(e.resolve(t,"late")).toBe(a),n(o?.fragmentShader).toContain("@location(1)"),n(a?.fragmentShader).not.toContain("@location(1)")}),a("reconstructs WebGPU depth positions with inverted NDC Y",()=>{const e=new T({WGSLShaderMaterial:K}),t=new f({color:g(j.add(V.multiplyScalar(0)))}),o=e.resolve(t),a=o.fragmentShader.match(/fn tsg_function_\d+\([^]*?\n}/g)??[];n(a).toHaveLength(1);const r=a[0]??"",s=r.match(/fn (tsg_function_\d+)/)?.[1];n(s).toBeDefined(),n(o.fragmentShader.split(s+"(").length-1).toBe(4),n(r).not.toMatch(/textureSample|renderer|materialUniforms/),n(r).toContain("0.000001");const i=e.resolve(new f({color:g(j)}));n(i?.fragmentShader).toContain(r),n(t.fragmentShader).toMatch(/tsg_function_\d+\(/),n(o.fragmentShader).toContain("vec2<f32>(2.0, -2.0)"),n(o.fragmentShader).toMatch(/-\(float_op_\d+\)/)}),a("emits dither and parallax nodes without GLSL custom functions",()=>{const e=new T({WGSLShaderMaterial:K}),t=new s(new Uint8Array([128,128,128,255]),1,1,h),o=H(y.uv,C("heightMap",t),p(.03)),a=new f({color:b(o.x,o.y,P(M(.2,.4,.8)).z,1)}),r=e.resolve(a);n(r).toBeInstanceOf(K),n(r?.fragmentShader).toContain("textureSampleLevel(heightMapTexture, heightMapSampler"),n(r?.fragmentShader).toMatch(/= -dpdy\(-pom_\d+ViewPosition\)/),n(r?.fragmentShader).toMatch(/= -dpdy\(pom_\d+Uv\)/),n(r?.fragmentShader).not.toContain("customFunction_"),n(r?.fragmentShader).not.toContain("gl_FragCoord")}),a("inherits, updates, and clears the WebGPU world environment",()=>{const e=new s(new Uint8Array([255,255,255,255]),1,1,h),t=new f({color:S({color:v(8421504),roughness:.5,metalness:.1})}),o=new T({WGSLShaderMaterial:K});o.setWorldEnvironment({texture:e,intensity:.75});const a=o.resolve(t);n(a.fragmentShader).toContain("envMapTexture"),n(a.uniforms.envMapIntensity?.value).toBe(.75),n(t.envMap).toBeUndefined(),o.setWorldEnvironment({texture:e,intensity:.25});const r=o.resolve(t);n(r).toBe(a),n(r.uniforms.envMapIntensity?.value).toBe(.25),o.setWorldEnvironment(null);const i=o.resolve(t);n(i).not.toBe(a),n(a.disposed).toBe(!0),n(i.fragmentShader).not.toContain("envMapTexture")}),a("keeps an authored material environment ahead of the world environment",()=>{const e=new s(new Uint8Array([255,255,255,255]),1,1,h),t=new s(new Uint8Array([64,96,128,255]),1,1,h),o=new s(new Uint8Array([32,48,64,255]),1,1,h),a=new f({color:S({color:v(8421504),roughness:.5,metalness:.1}),envMap:t}),i=r.fn(()=>o),l=new T({WGSLShaderMaterial:K},void 0,i);l.setWorldEnvironment({texture:e,intensity:.2}),l.resolve(a),n(i).toHaveBeenCalledWith(t),n(a.uniforms.envMap.value).toBe(o),n(a.envMap).toBe(t)})});/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sdfgi-settings.test.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{describe as e,expect as n,it as t}from"vitest";import{resolveSDFGISettings as i}from"../rendering/sdfgi-settings.js";e("scene and player GI settings",()=>{t("keeps legacy scenes on project defaults without enabling GI for unconfigured projects",()=>{n(i(void 0,{},null).enabled).toBe(!1),n(i({raysPerProbe:32},{},null).enabled).toBe(!0),n(i(!1,{},{enabled:!0,intensity:1}).enabled).toBe(!1)}),t("allows a scene to opt in when the project has no explicit GI configuration",()=>{n(i(void 0,{},{enabled:!0,intensity:1}).enabled).toBe(!0)}),t("preserves scene disabling when the player increases graphics quality",()=>{const e=i({enabled:!0},{enabled:!0,raysPerProbe:64},{enabled:!1,intensity:2});n(e.enabled).toBe(!1),n(e.raysPerProbe).toBe(64)}),t("keeps player Off authoritative over scene enable and intensity edits",()=>{n(i({enabled:!0},{enabled:!1},{enabled:!0,intensity:3}).enabled).toBe(!1)}),t("combines scene lighting with runtime budgets without modifying the inputs",()=>{const e={resolution:32,probeResolution:8,intensity:.5,raysPerProbe:32},t={enabled:!0,raysPerProbe:16,updateFraction:.125},s={enabled:!0,intensity:2};n(i(e,t,s)).toMatchObject({enabled:!0,resolution:32,probeResolution:8,intensity:1,raysPerProbe:16,updateFraction:.125}),n(e.intensity).toBe(.5),n(s.intensity).toBe(2),n(i(e,t,null).intensity).toBe(.5)}),t("sanitizes scene intensity from serialized data",()=>{n(i({},{},{enabled:!0,intensity:NaN}).intensity).toBe(1),n(i({},{},{enabled:!0,intensity:-5}).intensity).toBe(0)})});/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -1,4 +1,4 @@
1
- import{expect as e,test as t,vi as o}from"vitest";import{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 u}from"../scene/materializer.js";import{createShaderGraphMaterial as i}from"../effects/vfx/vfx-materializer.js";import{getCompiledGpuSpriteMaterial as p,getGpuSpriteMaterialWgsl as l}from"../effects/vfx/gpu-sprite-runtime.js";import{SerializedParamType as m}from"../scene/model.js";import{AssetsProvider as c}from"../scene/assets-provider.js";import{AssetResourceLoader as v}from"../scene/asset-resource-loader.js";import{buildShaderGraphMaterial as f,buildShaderGraphPostProcessMaterial as h,compileShaderGraph as g,compileShaderGraphPreview as y,createDefaultPostProcessShaderGraphDocument as I,createDefaultShaderGraphDocument as b,createDefaultVfxShaderGraphDocument as B,shaderGraphPostProcessWeightUniformName as x,shaderGraphParametersToPropertyParameters as k}from"../shader/graph/index.js";import{NodeShaderMaterial as w,textureSampler2d as T,UniformFloatNode as M,UniformSampler2dArraySlice as S,vec2 as C}from"../shader-nodes/index.js";import{StandardShader as j}from"../shader/builtin/standard-shader.js";import{sceneMapUniformName as _}from"../shader-nodes/scene-sample.js";function O(e,t,o){return{version:1,target:"surface",parameters:[],nodes:e,edges:t,outputs:o}}function D(e,t){return{id:e,name:e,type:"material",material:{type:"shaderGraph",side:r,params:{},shaderParams:{},shaderGraph:t}}}t("VFX graph materials retain graph and animation profile diagnostic metadata",async()=>{const t=B("sprite");t.parameters.push({name:"amount",type:"float",defaultValue:1});const o=await i({type:"sprite",shaderGraph:{source:"asset",assetId:"diagnostic-graph"},shaderGraphParams:{amount:{type:m.FloatNode,value:{a:0,b:1,easing:"InQuad"},override:!0}}},"sprite",new A([{id:"diagnostic-graph",name:"Diagnostic graph",type:"shaderGraph",shaderGraph:t}]),new E,{shaders:[]});e(o?.userData.vfxShader).toMatchObject({key:"shaderGraph:diagnostic-graph",name:"Diagnostic graph",target:"sprite",parameters:{amount:{mode:"animated",easing:"InQuad"}}})}),t.each(["sprite","decal"])("VFX %s graphs share static and animated parameter shader layouts",async t=>{const o=B(t);o.shadingModel="decal"===t?"standard":"unlit",o.parameters.push({name:"amount",type:"float",defaultValue:1}),o.nodes.push({id:"amount",kind:"input.parameter",params:{parameter:"amount"}}),o.outputs.opacity={input:{nodeId:"amount",port:"value"}};const a=new A([{id:"graph",name:"Graph",type:"shaderGraph",shaderGraph:o}]),r=async e=>await i({type:t,shaderGraph:{source:"asset",assetId:"graph"},shaderGraphParams:{amount:{type:m.FloatNode,value:e,override:!0}}},t,a,new E,{shaders:[]}),n=await r(.5),s=await r({a:0,b:1,easing:"InQuad"}),d=await r({a:1,b:0,easing:"OutCubic"});e(n.userData.vfxShader.animationLayout).toBe("shared"),e(n.vertexShader).toBe(s.vertexShader),e(n.fragmentShader).toBe(s.fragmentShader),e(s.fragmentShader).toBe(d.fragmentShader);const u=[n,s,d].map(p);e(u[0].vertexMain).toBe(u[1].vertexMain),e(u[0].fragmentMain).toBe(u[1].fragmentMain),e(u[1].fragmentMain).toBe(u[2].fragmentMain),e(Object.keys(u[0].textures)).toEqual(Object.keys(u[1].textures)),e(n.uniforms.pu_amount_animation.value.z).toBe(0),e(s.uniforms.pu_amount_animation.value.z).toBe(1),e(n.transparent).toBe(s.transparent),e(n.side).toBe(s.side),e(n.depthTest).toBe(s.depthTest)}),t("graph validation rejects missing nodes, invalid ports, and incompatible edges",()=>{const t=b();t.outputs.color={input:{nodeId:"missing",port:"value"}},e(()=>g(t)).toThrow(/Missing shader graph node/);const o=b();o.outputs.color={input:{nodeId:"base-color",port:"missing"}},e(()=>g(o)).toThrow(/does not have output/);const a=O([{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(()=>g(a)).toThrow(/Cannot add vec2 and vec3/)}),t("scene lighting nodes expose renderer ambient and directional light values",()=>{const t=O([{id:"ambient",kind:"scene.ambientLight"},{id:"directional",kind:"scene.directionalLight"}],[],{color:{input:{nodeId:"ambient",port:"color"}}}),o=g(t);e(o.nodes.get("ambient")?.outputs.color.type).toBe("rgb"),e(o.nodes.get("directional")?.outputs.color.type).toBe("rgb"),e(o.nodes.get("directional")?.outputs.direction.type).toBe("vec3");const a=f(t);e(a.fragmentShader).toContain("ambientLightColor"),t.outputs.color={input:{nodeId:"directional",port:"color"}};const r=f(t);e(r.fragmentShader).toContain("directionalLights"),e(r.fragmentShader).toContain("NUM_DIR_LIGHTS"),e(r.fragmentShader).toMatch(/max\s*\(\s*float\s*\(\s*NUM_DIR_LIGHTS\s*\)/)}),t("named reroutes pass values through declarations to multiple usages",()=>{const t=O([{id:"source",kind:"constant.float",params:{value:.75}},{id:"declaration",kind:"utility.namedRerouteDeclaration",params:{name:"Mask"}},{id:"usage-a",kind:"utility.namedRerouteUsage",params:{declarationId:"declaration"}},{id:"usage-b",kind:"utility.namedRerouteUsage",params:{declarationId:"declaration"}},{id:"add",kind:"math.add"}],[{from:{nodeId:"source",port:"value"},to:{nodeId:"declaration",port:"value"}},{from:{nodeId:"usage-a",port:"value"},to:{nodeId:"add",port:"a"}},{from:{nodeId:"usage-b",port:"value"},to:{nodeId:"add",port:"b"}}],{color:{input:{nodeId:"add",port:"value"}}}),o=g(t),a=o.nodes.get("declaration")?.outputs.value;e(a?.type).toBe("float"),e(o.nodes.get("usage-a")?.outputs.value).toBe(a),e(o.nodes.get("usage-b")?.outputs.value).toBe(a),e(o.output.color).toBeTruthy()}),t("named reroutes preserve dynamic vector types",()=>{const t=O([{id:"source",kind:"constant.vec3",params:{value:[1,2,3]}},{id:"declaration",kind:"utility.namedRerouteDeclaration",params:{name:"Direction"}},{id:"usage",kind:"utility.namedRerouteUsage",params:{declarationId:"declaration"}}],[{from:{nodeId:"source",port:"value"},to:{nodeId:"declaration",port:"value"}}],{color:{input:{nodeId:"usage",port:"value"}}}),o=g(t);e(o.nodes.get("usage")?.outputs.value.type).toBe("vec3")}),t("named reroute validation rejects missing and invalid declarations",()=>{const t=O([{id:"usage",kind:"utility.namedRerouteUsage",params:{declarationId:"missing"}}],[],{});e(()=>g(t)).toThrow(/declaration "missing" does not exist/);const o=O([{id:"not-declaration",kind:"constant.float",params:{value:1}},{id:"usage",kind:"utility.namedRerouteUsage",params:{declarationId:"not-declaration"}}],[],{});e(()=>g(o)).toThrow(/not a named reroute declaration/)}),t("named reroute cycles report a compile error",()=>{const t=O([{id:"declaration",kind:"utility.namedRerouteDeclaration",params:{name:"Cycle"}},{id:"usage",kind:"utility.namedRerouteUsage",params:{declarationId:"declaration"}}],[{from:{nodeId:"usage",port:"value"},to:{nodeId:"declaration",port:"value"}}],{color:{input:{nodeId:"usage",port:"value"}}});e(()=>g(t)).toThrow(/contains a cycle/)}),t("surface graph compiles to NodeShaderOutput",()=>{const t=g(b("surface"));e(t.output.color).toBeTruthy(),e(t.output.transparent).toBe(!1)}),t("inline literal inputs compile without separate constant nodes",()=>{const t=O([{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=g(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("uv attribute node supports selecting non-default uv channels",()=>{const t=O([{id:"uv",kind:"builtin.uv",params:{channel:2}}],[],{color:{input:{nodeId:"uv",port:"value"}}}),o=g(t);e(o.nodes.get("uv")?.outputs.value.type).toBe("vec2");const a=f(t);e(a.fragmentShader).toContain("uv2")}),t.each(["surface","decal"])("world normal node compiles for %s graphs",t=>{const o=O([{id:"world-normal",kind:"builtin.worldNormal"}],[],{color:{input:{nodeId:"world-normal",port:"value"}}});o.target=t;const a=g(o);e(a.nodes.get("world-normal")?.outputs.value.type).toBe("vec3"),e(f(o).fragmentShader).toBeTruthy()}),t("sprite graph compiles with sprite-compatible material output",()=>{const t=b("sprite"),o=g(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=B(t);if(e(g(o).output.color).toBeTruthy(),"sprite"===t)continue;const a=f(o,{trailBillboard:"trail"===t});e(a.isMaterial).toBe(!0)}}),t("graph material options apply to supported material targets",()=>{const t=b("surface");t.materialOptions={side:"double",transparent:!0};const o=f(t);e(o.side).toBe(a),e(o.transparent).toBe(!0);const r=b("decal");r.materialOptions={transparent:!0};const n=f(r);e(n.transparent).toBe(!0)}),t("standard graph compiles extended material outputs and vertex transform",()=>{const t=O([{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=g(t);e(o.output.metalness).toBeTruthy(),e(o.output.ambientOcclusion).toBeTruthy(),e(o.output.sheenColor).toBeTruthy(),e(o.output.transform).toBeTruthy();const a=f(t);e(a.outputMetalness).toBeTruthy(),e(a.outputAmbientOcclusion).toBeTruthy(),e(a.outputTransform).toBeTruthy()}),t("math multiply composes transform matrices",()=>{const t=O([{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=g(t);e(o.nodes.get("compose")?.outputs.value.type).toBe("mat4"),e(o.output.transform).toBeTruthy(),e(f(t).outputTransform).toBeTruthy()}),t("matrix input nodes compile as mat4 uniforms",()=>{for(const t of["builtin.viewMatrix","builtin.projectionMatrix","builtin.modelViewMatrix"]){const o=O([{id:"matrix",kind:t}],[],{transform:{input:{nodeId:"matrix",port:"value"}}}),a=g(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=O([{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=g(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=O([{id:"atan2",kind:"math.atan2",inputs:{y:{value:1},x:{value:0}}}],[],{color:{input:{nodeId:"atan2",port:"value"}}}),o=g(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=O([{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=g(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(f(t).outputTransform).toBeTruthy()}),t("painted layer mix node compiles color layers and builds a material",()=>{const t=O([{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=g(t);e(o.nodes.get("mix")?.outputs.value.type).toBe("rgba"),e(o.output.color).toBeTruthy(),e(f(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=O([{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=g(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=O([{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(()=>g(t)).toThrow(/Cannot mix vec2 and vec3/)}),t("additional math, uv, shape, noise, and effect nodes compile",()=>{const t=O([{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=g(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=O([{id:"slider",kind:"input.slider",params:{value:1.25}}],[],{roughness:{input:{nodeId:"slider",port:"value"}}}),o=g(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=O([{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=g(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=O([{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=g(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=O([{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=g(t);e(o.nodes.get("gradient")?.outputs.value.type).toBe("rgba"),e(o.output.color).toBeTruthy(),e(y(t,"gradient").color).toBeTruthy()}),t("curve node compiles editable curve to a sampled float",()=>{const t=O([{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=g(t);e(o.nodes.get("curve")?.outputs.value.type).toBe("float"),e(o.output.roughness).toBeTruthy(),e(y(t,"curve").color).toBeTruthy()}),t("graph parameters convert to property params and custom defaults",()=>{const t=b();t.parameters.push({name:"opacity",type:"float",group:"Surface",defaultValue:.5});const o=k(t);e(o.map(e=>e.name)).toEqual(["baseColor","opacity"]),e(o.every(e=>!0===e.options.optional)).toBe(!0),e(o.map(e=>e.options.group??null)).toEqual([null,"Surface"]);const a=d(t);e(a.baseColor).toMatchObject({type:m.RgbNode,value:"#ffffff",override:!1}),e(a.opacity).toMatchObject({type:m.FloatNode,value:.5,override:!1})}),t("existing shader graph param values keep overriding unless explicitly disabled",()=>{const t=b(),o=d(t,{baseColor:{type:m.RgbNode,value:"#ff0000"}});e(o.baseColor).toMatchObject({type:m.RgbNode,value:"#ff0000",override:!0});const a=d(t,{baseColor:{type:m.RgbNode,value:"#00ff00",override:!1}});e(a.baseColor).toMatchObject({type:m.RgbNode,value:"#ffffff",override:!1})}),t("texture graph parameter defaults serialize as sampler parameters",()=>{const t=O([],[],{});t.parameters.push({name:"map",type:"texture",defaultValue:"texture-asset-id"});const o=d(t);e(o.map).toMatchObject({type:m.Sampler2DNode,value:"texture-asset-id"})}),t("shader graph params keep graph-declared serialized types over previous string params",()=>{const t=b();t.parameters.push({name:"map",type:"texture",defaultValue:null});const o=d(t,{baseColor:{type:m.String,value:"#ff0000"},map:{type:m.String,value:"texture-asset-id"}});e(o.baseColor).toMatchObject({type:m.RgbNode,value:"#ff0000"}),e(o.map).toMatchObject({type:m.Sampler2DNode,value:"texture-asset-id"})}),t("shader graph texture params resolve asset ids before compile",async()=>{const t=O([{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 u({},t,new A([{id:"texture-asset-id",name:"Map",type:"texture",fileKey:"map.png",fileFormat:"png"}]),new E);e(o.map).toBeTruthy(),e(typeof o.map).not.toBe("string"),e(g(t,{parameters:o}).output.color).toBeTruthy()}),t("shader graph requests all texture parameters before waiting for decoding",async()=>{const t=b();let a;t.parameters=["colorMap","normalMap","controlMap"].map(e=>({name:e,type:"texture",defaultValue:e}));const r=new Promise(e=>{a=e}),s=o.fn(async()=>(await r,new n)),d=u({},t,{getAsset:async e=>({id:e,type:"texture"})},{getTexture:s});await Promise.resolve(),await Promise.resolve(),e(s).toHaveBeenCalledTimes(3),a();const i=await d;e(Object.keys(i)).toEqual(["colorMap","normalMap","controlMap"]),e(Object.values(i).every(e=>null!=e)).toBe(!0)}),t("shader graph texture array params expose named slice uniforms",async()=>{const t=O([{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 u({},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=f(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 j;t.heightMap=new S("heightMap",new n,new M("heightMap_i",3,void 0,!1)),t.map=T(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("standard shader supports particle opacity for instanced VFX materials",()=>{const t=(new j).build();e(t.fragmentShader).toContain("IS_PARTICLE"),e(t.fragmentShader).toContain("particleData")}),t("material graph asset reference and local embedded graph both build materials",async()=>{const t={id:"graph-asset",name:"Reusable Graph",type:"shaderGraph",shaderGraph:b()},o=D("referenced",{source:"asset",assetId:t.id}),a=D("local",{source:"local",graph:b()}),r=new A([t,o,a]),n=new v;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=O([o],[],{color:{input:{nodeId:o.id,port:"value"}}});e(y(t,o.id).color).toBeTruthy()}}),t("post-process shader graph builds a fullscreen-safe material",()=>{const t=I(),o=h(t);e(o).toBeInstanceOf(w),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[_]).toBeTruthy(),e(o.uniforms[x]).toBeTruthy()}),t("empty post-process shader graph defaults to identity scene color",()=>{const t=I();t.nodes=[],t.edges=[],t.outputs={};const o=g(t),a=h(t);e(o.output.color).toBeTruthy(),e(o.output.transparent).toBe(!1),e(a.uniforms[_]).toBeTruthy(),e(a.uniforms[x]).toBeTruthy()}),t("post-process color and opacity outputs expose blend weight uniform",()=>{const t=O([{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=g(t),a=h(t);e(o.output.color).toBeTruthy(),e(o.output.opacity).toBeTruthy(),e(o.output.roughness).toBeUndefined(),e(a.uniforms[x]).toBeTruthy()}),t("selected-node preview builds through post-process fullscreen path",()=>{const t=O([{id:"float",kind:"constant.float",params:{value:.25}}],[],{color:{input:{nodeId:"float",port:"value"}}});t.target="postProcess";const o=h(t,{previewNodeId:"float"});e(o).toBeInstanceOf(w),e(o.userData.isPostProcessShaderGraph).toBe(!0),e(o.uniforms[_]).toBeTruthy(),e(o.uniforms[x]).toBeTruthy()}),t("texture parameter nodes expose sampled value and channel outputs",()=>{const t=O([{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=g(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=O([{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=g(t,{parameters:{map:T(new n)}});e(o.output.color).toBeTruthy()}),t("missing texture parameter values warn and use fallback sampler",()=>{const t=O([{id:"texture",kind:"input.parameter",params:{parameter:"missingTexture"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"missingTexture",type:"texture",defaultValue:null});const a=o.spyOn(console,"warn").mockImplementation(()=>{});try{const o=g(t),r=o.nodes.get("texture")?.outputs??{};e(r.sampler.type).toBe("sampler2d"),e(o.output.color).toBeTruthy(),e(a).toHaveBeenCalledWith(e.stringContaining("missingTexture"))}finally{a.mockRestore()}}),t("scene depth graph nodes expose sampled and fragment linear eye depth",()=>{const t=O([{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=g(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("screen UV builtin accepts a fullscreen compiler override",()=>{const t=O([{id:"uv",kind:"builtin.screenUv"}],[],{}),o=C(.25,.75),a=g(t,{screenUV:o});e(a.nodes.get("uv")?.outputs.value.value).toBe(o)}),t("unpack normal node compiles with various input combinations",()=>{const t=O([{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=g(t);e(o.nodes.get("toNormal")?.outputs.value.type).toBe("vec3"),e(o.output.normal).toBeTruthy()});class A extends c{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 E extends v{async getTexture(){return new n}async getTextureArray(){return{texture:null,layerIndex:null}}}class P extends E{constructor(e,t){super(),this.textureArray=e,this.layerIndex=t}async getTextureArray(){return{texture:this.textureArray,layerIndex:this.layerIndex}}}t("animated graph ranges share shader source while retaining independent endpoint uniforms",async()=>{const t=O([{id:"amount",kind:"input.parameter",params:{parameter:"amount"}}],[],{opacity:{input:{nodeId:"amount",port:"value"}}});t.parameters.push({name:"amount",type:"float",defaultValue:1});const o=async(e,o,a)=>{const r=await u({amount:{type:m.FloatNode,value:{a:e,b:o,easing:a},override:!0}},t,new A([]),new E);return f(t,{parameters:r})},a=await o(0,1,"Linear"),r=await o(2,8,"Linear"),s=await o(0,1,"InQuad");e(a.vertexShader).toBe(r.vertexShader),e(a.fragmentShader).toBe(r.fragmentShader),e(a.uniforms.pu_amount_curveEndpoints.value.toArray()).toEqual([0,1]),e(r.uniforms.pu_amount_curveEndpoints.value.toArray()).toEqual([2,8]),e(a.uniforms.pu_amount_curveEndpoints.value).not.toBe(r.uniforms.pu_amount_curveEndpoints.value);const d=e=>Object.values(e.uniforms).map(e=>e.value).find(e=>e instanceof n);e(d(a)).toBeTruthy(),e(d(a)).toBe(d(r)),e(d(a)).not.toBe(d(s))}),t("stable particle parameters share GLSL and WGSL across static values and easing profiles",async()=>{const t=O([{id:"amount",kind:"input.parameter",params:{parameter:"amount"}}],[],{opacity:{input:{nodeId:"amount",port:"value"}}});t.shadingModel="unlit",t.parameters.push({name:"amount",type:"float",defaultValue:1});const o=async(e,o=!0)=>{const a=await u({amount:{type:m.FloatNode,value:e,override:!0}},t,new A([]),new E,void 0,void 0,o);return f(t,{parameters:a})},a=await o(2),r=await o({a:0,b:3,easing:"InQuad"}),s=await o({a:-2,b:8,easing:"OutCubic"});e(a.fragmentShader).toBe(r.fragmentShader),e(r.fragmentShader).toBe(s.fragmentShader),e(a.uniforms.pu_amount.value).toBe(2),e(a.uniforms.pu_amount_animation.value.z).toBe(0),e(r.uniforms.pu_amount.value).toBe(0),e(r.uniforms.pu_amount_animation.value.x).toBe(3),e(r.uniforms.pu_amount_animation.value.z).toBe(1);const d=e=>l(p(e),!0),i=d(a);e(i).toBe(d(r)),e(i).toBe(d(s)),e(i).toContain("if ("),e(i).toContain("textureSampleLevel(");const c=e=>Object.values(e.uniforms).map(e=>e.value).filter(e=>e instanceof n);e(c(r)).toHaveLength(1),e(c(a)[0]).toBe(c(r)[0]),e(c(s)[0]).toBe(c(r)[0]);const v=c(r)[0].image,h=Math.floor(r.uniforms.pu_amount_animation.value.y*v.height),g=c(await o({a:0,b:3,easing:"InQuad"},!1))[0].image;e(Array.from(v.data.slice(h*v.width,(h+1)*v.width))).toEqual(Array.from(g.data))});/*
1
+ import{expect as e,test as t,vi as o}from"vitest";import{DoubleSide as a,FrontSide as r,Scene as n,Texture as s}from"three";import{Subject as d}from"rxjs";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 i,prepareCustomParamsFromShaderGraph as u,prepareShaderGraphParameters as p,SceneMaterializer as l}from"../scene/materializer.js";import{createWorldEnvironment as m,SceneDataService as c}from"../scene/scene-data-service.js";import{createShaderGraphMaterial as v}from"../effects/vfx/vfx-materializer.js";import{getCompiledGpuSpriteMaterial as f,getGpuSpriteMaterialWgsl as h}from"../effects/vfx/gpu-sprite-runtime.js";import{SerializedParamType as g}from"../scene/model.js";import{AssetsProvider as y}from"../scene/assets-provider.js";import{AssetResourceLoader as I}from"../scene/asset-resource-loader.js";import{buildShaderGraphMaterial as b,buildShaderGraphPostProcessMaterial as B,compileShaderGraph as x,compileShaderGraphPreview as k,createDefaultPostProcessShaderGraphDocument as w,createDefaultShaderGraphDocument as T,createDefaultVfxShaderGraphDocument as S,shaderGraphPostProcessWeightUniformName as M,shaderGraphParametersToPropertyParameters as C}from"../shader/graph/index.js";import{NodeShaderMaterial as j,textureSampler2d as O,UniformFloatNode as D,UniformSampler2dArraySlice as E,vec2 as _}from"../shader-nodes/index.js";import{StandardShader as N}from"../shader/builtin/standard-shader.js";import{sceneMapUniformName as A}from"../shader-nodes/scene-sample.js";function G(e,t,o){return{version:1,target:"surface",parameters:[],nodes:e,edges:t,outputs:o}}function P(e,t){return{id:e,name:e,type:"material",material:{type:"shaderGraph",side:r,params:{},shaderParams:{},shaderGraph:t}}}t("VFX graph materials retain graph and animation profile diagnostic metadata",async()=>{const t=S("sprite");t.parameters.push({name:"amount",type:"float",defaultValue:1});const o=await v({type:"sprite",shaderGraph:{source:"asset",assetId:"diagnostic-graph"},shaderGraphParams:{amount:{type:g.FloatNode,value:{a:0,b:1,easing:"InQuad"},override:!0}}},"sprite",new V([{id:"diagnostic-graph",name:"Diagnostic graph",type:"shaderGraph",shaderGraph:t}]),new L,{shaders:[]});e(o?.userData.vfxShader).toMatchObject({key:"shaderGraph:diagnostic-graph",name:"Diagnostic graph",target:"sprite",parameters:{amount:{mode:"animated",easing:"InQuad"}}})}),t.each(["sprite","decal"])("VFX %s graphs share static and animated parameter shader layouts",async t=>{const o=S(t);o.shadingModel="decal"===t?"standard":"unlit",o.parameters.push({name:"amount",type:"float",defaultValue:1}),o.nodes.push({id:"amount",kind:"input.parameter",params:{parameter:"amount"}}),o.outputs.opacity={input:{nodeId:"amount",port:"value"}};const a=new V([{id:"graph",name:"Graph",type:"shaderGraph",shaderGraph:o}]),r=async e=>await v({type:t,shaderGraph:{source:"asset",assetId:"graph"},shaderGraphParams:{amount:{type:g.FloatNode,value:e,override:!0}}},t,a,new L,{shaders:[]}),n=await r(.5),s=await r({a:0,b:1,easing:"InQuad"}),d=await r({a:1,b:0,easing:"OutCubic"});e(n.userData.vfxShader.animationLayout).toBe("shared"),e(n.vertexShader).toBe(s.vertexShader),e(n.fragmentShader).toBe(s.fragmentShader),e(s.fragmentShader).toBe(d.fragmentShader);const i=[n,s,d].map(f);e(i[0].vertexMain).toBe(i[1].vertexMain),e(i[0].fragmentMain).toBe(i[1].fragmentMain),e(i[1].fragmentMain).toBe(i[2].fragmentMain),e(Object.keys(i[0].textures)).toEqual(Object.keys(i[1].textures)),e(n.uniforms.pu_amount_animation.value.z).toBe(0),e(s.uniforms.pu_amount_animation.value.z).toBe(1),e(n.transparent).toBe(s.transparent),e(n.side).toBe(s.side),e(n.depthTest).toBe(s.depthTest)}),t("graph validation rejects missing nodes, invalid ports, and incompatible edges",()=>{const t=T();t.outputs.color={input:{nodeId:"missing",port:"value"}},e(()=>x(t)).toThrow(/Missing shader graph node/);const o=T();o.outputs.color={input:{nodeId:"base-color",port:"missing"}},e(()=>x(o)).toThrow(/does not have output/);const a=G([{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(()=>x(a)).toThrow(/Cannot add vec2 and vec3/)}),t("scene lighting nodes expose renderer ambient and directional light values",()=>{const t=G([{id:"ambient",kind:"scene.ambientLight"},{id:"directional",kind:"scene.directionalLight"}],[],{color:{input:{nodeId:"ambient",port:"color"}}}),o=x(t);e(o.nodes.get("ambient")?.outputs.color.type).toBe("rgb"),e(o.nodes.get("directional")?.outputs.color.type).toBe("rgb"),e(o.nodes.get("directional")?.outputs.direction.type).toBe("vec3");const a=b(t);e(a.fragmentShader).toContain("ambientLightColor"),t.outputs.color={input:{nodeId:"directional",port:"color"}};const r=b(t);e(r.fragmentShader).toContain("directionalLights"),e(r.fragmentShader).toContain("NUM_DIR_LIGHTS"),e(r.fragmentShader).toMatch(/max\s*\(\s*float\s*\(\s*NUM_DIR_LIGHTS\s*\)/)}),t("named reroutes pass values through declarations to multiple usages",()=>{const t=G([{id:"source",kind:"constant.float",params:{value:.75}},{id:"declaration",kind:"utility.namedRerouteDeclaration",params:{name:"Mask"}},{id:"usage-a",kind:"utility.namedRerouteUsage",params:{declarationId:"declaration"}},{id:"usage-b",kind:"utility.namedRerouteUsage",params:{declarationId:"declaration"}},{id:"add",kind:"math.add"}],[{from:{nodeId:"source",port:"value"},to:{nodeId:"declaration",port:"value"}},{from:{nodeId:"usage-a",port:"value"},to:{nodeId:"add",port:"a"}},{from:{nodeId:"usage-b",port:"value"},to:{nodeId:"add",port:"b"}}],{color:{input:{nodeId:"add",port:"value"}}}),o=x(t),a=o.nodes.get("declaration")?.outputs.value;e(a?.type).toBe("float"),e(o.nodes.get("usage-a")?.outputs.value).toBe(a),e(o.nodes.get("usage-b")?.outputs.value).toBe(a),e(o.output.color).toBeTruthy()}),t("named reroutes preserve dynamic vector types",()=>{const t=G([{id:"source",kind:"constant.vec3",params:{value:[1,2,3]}},{id:"declaration",kind:"utility.namedRerouteDeclaration",params:{name:"Direction"}},{id:"usage",kind:"utility.namedRerouteUsage",params:{declarationId:"declaration"}}],[{from:{nodeId:"source",port:"value"},to:{nodeId:"declaration",port:"value"}}],{color:{input:{nodeId:"usage",port:"value"}}}),o=x(t);e(o.nodes.get("usage")?.outputs.value.type).toBe("vec3")}),t("named reroute validation rejects missing and invalid declarations",()=>{const t=G([{id:"usage",kind:"utility.namedRerouteUsage",params:{declarationId:"missing"}}],[],{});e(()=>x(t)).toThrow(/declaration "missing" does not exist/);const o=G([{id:"not-declaration",kind:"constant.float",params:{value:1}},{id:"usage",kind:"utility.namedRerouteUsage",params:{declarationId:"not-declaration"}}],[],{});e(()=>x(o)).toThrow(/not a named reroute declaration/)}),t("named reroute cycles report a compile error",()=>{const t=G([{id:"declaration",kind:"utility.namedRerouteDeclaration",params:{name:"Cycle"}},{id:"usage",kind:"utility.namedRerouteUsage",params:{declarationId:"declaration"}}],[{from:{nodeId:"usage",port:"value"},to:{nodeId:"declaration",port:"value"}}],{color:{input:{nodeId:"usage",port:"value"}}});e(()=>x(t)).toThrow(/contains a cycle/)}),t("surface graph compiles to NodeShaderOutput",()=>{const t=x(T("surface"));e(t.output.color).toBeTruthy(),e(t.output.transparent).toBe(!1)}),t("inline literal inputs compile without separate constant nodes",()=>{const t=G([{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=x(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("uv attribute node supports selecting non-default uv channels",()=>{const t=G([{id:"uv",kind:"builtin.uv",params:{channel:2}}],[],{color:{input:{nodeId:"uv",port:"value"}}}),o=x(t);e(o.nodes.get("uv")?.outputs.value.type).toBe("vec2");const a=b(t);e(a.fragmentShader).toContain("uv2")}),t.each(["surface","decal"])("world normal node compiles for %s graphs",t=>{const o=G([{id:"world-normal",kind:"builtin.worldNormal"}],[],{color:{input:{nodeId:"world-normal",port:"value"}}});o.target=t;const a=x(o);e(a.nodes.get("world-normal")?.outputs.value.type).toBe("vec3"),e(b(o).fragmentShader).toBeTruthy()}),t("sprite graph compiles with sprite-compatible material output",()=>{const t=T("sprite"),o=x(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=S(t);if(e(x(o).output.color).toBeTruthy(),"sprite"===t)continue;const a=b(o,{trailBillboard:"trail"===t});e(a.isMaterial).toBe(!0)}}),t("graph material options apply to supported material targets",()=>{const t=T("surface");t.materialOptions={side:"double",transparent:!0};const o=b(t);e(o.side).toBe(a),e(o.transparent).toBe(!0);const r=T("decal");r.materialOptions={transparent:!0};const n=b(r);e(n.transparent).toBe(!0)}),t("standard graph compiles extended material outputs and vertex transform",()=>{const t=G([{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=x(t);e(o.output.metalness).toBeTruthy(),e(o.output.ambientOcclusion).toBeTruthy(),e(o.output.sheenColor).toBeTruthy(),e(o.output.transform).toBeTruthy();const a=b(t);e(a.outputMetalness).toBeTruthy(),e(a.outputAmbientOcclusion).toBeTruthy(),e(a.outputTransform).toBeTruthy()}),t("math multiply composes transform matrices",()=>{const t=G([{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=x(t);e(o.nodes.get("compose")?.outputs.value.type).toBe("mat4"),e(o.output.transform).toBeTruthy(),e(b(t).outputTransform).toBeTruthy()}),t("matrix input nodes compile as mat4 uniforms",()=>{for(const t of["builtin.viewMatrix","builtin.projectionMatrix","builtin.modelViewMatrix"]){const o=G([{id:"matrix",kind:t}],[],{transform:{input:{nodeId:"matrix",port:"value"}}}),a=x(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=G([{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=x(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=G([{id:"atan2",kind:"math.atan2",inputs:{y:{value:1},x:{value:0}}}],[],{color:{input:{nodeId:"atan2",port:"value"}}}),o=x(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=G([{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=x(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(b(t).outputTransform).toBeTruthy()}),t("painted layer mix node compiles color layers and builds a material",()=>{const t=G([{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=x(t);e(o.nodes.get("mix")?.outputs.value.type).toBe("rgba"),e(o.output.color).toBeTruthy(),e(b(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=G([{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=x(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=G([{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(()=>x(t)).toThrow(/Cannot mix vec2 and vec3/)}),t("additional math, uv, shape, noise, and effect nodes compile",()=>{const t=G([{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=x(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=G([{id:"slider",kind:"input.slider",params:{value:1.25}}],[],{roughness:{input:{nodeId:"slider",port:"value"}}}),o=x(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=G([{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=x(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=G([{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=x(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=G([{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=x(t);e(o.nodes.get("gradient")?.outputs.value.type).toBe("rgba"),e(o.output.color).toBeTruthy(),e(k(t,"gradient").color).toBeTruthy()}),t("curve node compiles editable curve to a sampled float",()=>{const t=G([{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=x(t);e(o.nodes.get("curve")?.outputs.value.type).toBe("float"),e(o.output.roughness).toBeTruthy(),e(k(t,"curve").color).toBeTruthy()}),t("graph parameters convert to property params and custom defaults",()=>{const t=T();t.parameters.push({name:"opacity",type:"float",group:"Surface",defaultValue:.5});const o=C(t);e(o.map(e=>e.name)).toEqual(["baseColor","opacity"]),e(o.every(e=>!0===e.options.optional)).toBe(!0),e(o.map(e=>e.options.group??null)).toEqual([null,"Surface"]);const a=u(t);e(a.baseColor).toMatchObject({type:g.RgbNode,value:"#ffffff",override:!1}),e(a.opacity).toMatchObject({type:g.FloatNode,value:.5,override:!1})}),t("existing shader graph param values keep overriding unless explicitly disabled",()=>{const t=T(),o=u(t,{baseColor:{type:g.RgbNode,value:"#ff0000"}});e(o.baseColor).toMatchObject({type:g.RgbNode,value:"#ff0000",override:!0});const a=u(t,{baseColor:{type:g.RgbNode,value:"#00ff00",override:!1}});e(a.baseColor).toMatchObject({type:g.RgbNode,value:"#ffffff",override:!1})}),t("texture graph parameter defaults serialize as sampler parameters",()=>{const t=G([],[],{});t.parameters.push({name:"map",type:"texture",defaultValue:"texture-asset-id"});const o=u(t);e(o.map).toMatchObject({type:g.Sampler2DNode,value:"texture-asset-id"})}),t("shader graph params keep graph-declared serialized types over previous string params",()=>{const t=T();t.parameters.push({name:"map",type:"texture",defaultValue:null});const o=u(t,{baseColor:{type:g.String,value:"#ff0000"},map:{type:g.String,value:"texture-asset-id"}});e(o.baseColor).toMatchObject({type:g.RgbNode,value:"#ff0000"}),e(o.map).toMatchObject({type:g.Sampler2DNode,value:"texture-asset-id"})}),t("shader graph texture params resolve asset ids before compile",async()=>{const t=G([{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 V([{id:"texture-asset-id",name:"Map",type:"texture",fileKey:"map.png",fileFormat:"png"}]),new L);e(o.map).toBeTruthy(),e(typeof o.map).not.toBe("string"),e(x(t,{parameters:o}).output.color).toBeTruthy()}),t("shader graph requests all texture parameters before waiting for decoding",async()=>{const t=T();let a;t.parameters=["colorMap","normalMap","controlMap"].map(e=>({name:e,type:"texture",defaultValue:e}));const r=new Promise(e=>{a=e}),n=o.fn(async()=>(await r,new s)),d=p({},t,{getAsset:async e=>({id:e,type:"texture"})},{getTexture:n});await Promise.resolve(),await Promise.resolve(),e(n).toHaveBeenCalledTimes(3),a();const i=await d;e(Object.keys(i)).toEqual(["colorMap","normalMap","controlMap"]),e(Object.values(i).every(e=>null!=e)).toBe(!0)}),t("shader graph texture array params expose named slice uniforms",async()=>{const t=G([{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 V([{id:"texture-array-layer",name:"Map Layer",type:"texture",fileKey:"map.png",fileFormat:"png",texture:{textureArrayFileKey:"textures/packed.ktx2",textureArrayLayer:5}}]),new R(new s,5)),a=b(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 N;t.heightMap=new E("heightMap",new s,new D("heightMap_i",3,void 0,!1)),t.map=O(new s),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("standard shader supports particle opacity for instanced VFX materials",()=>{const t=(new N).build();e(t.fragmentShader).toContain("IS_PARTICLE"),e(t.fragmentShader).toContain("particleData")}),t("material graph asset reference and local embedded graph both build materials",async()=>{const t={id:"graph-asset",name:"Reusable Graph",type:"shaderGraph",shaderGraph:T()},o=P("referenced",{source:"asset",assetId:t.id}),a=P("local",{source:"local",graph:T()}),r=new V([t,o,a]),n=new I;e((await i(o,null,r,n,[],!1)).isMaterial).toBe(!0),e((await i(a,null,r,n,[],!1)).isMaterial).toBe(!0)}),t.each([void 0,!1,!0])("material assets persist receive GI = %s with a backwards-compatible default",async t=>{const o=P("gi-reception",{source:"local",graph:T()});o.material.receiveGI=t;const a=JSON.parse(JSON.stringify(o)),r=await i(a,null,new V([a]),new I,[],!1);e(r.userData.receiveGI).toBe(t??!0),r.dispose()}),t("editor scene initialization and live World Environment updates apply GI settings",async()=>{const t=JSON.parse(JSON.stringify(m()));e(t.worldEnv.globalIllumination).toEqual({enabled:!1,intensity:1}),t.worldEnv.globalIllumination={enabled:!0,intensity:2};const a={setSceneSDFGISettings:o.fn(),configureAmbientOcclusion:o.fn(),setBaseToneMapping:o.fn(),setWorldEnvironment:o.fn(),onLoop:o.fn(),invalidateShadows:o.fn()},r=new c;r.initiate([t]);const s=Object.assign(new V([]),{onCreate:new d,onUpdate:new d}),i=new l(new n,r,s,new L,a,[],[],{});i.editorStaticDrawSetsEnabled=!1,await i.init(),e(a.setSceneSDFGISettings).toHaveBeenLastCalledWith({enabled:!0,intensity:2}),r.updateObject({...t,worldEnv:{...t.worldEnv,globalIllumination:{enabled:!1,intensity:3}}}),e(a.setSceneSDFGISettings).toHaveBeenLastCalledWith({enabled:!1,intensity:3}),r.removeObject(t),e(a.setSceneSDFGISettings).toHaveBeenLastCalledWith(null),i.dispose()}),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=G([o],[],{color:{input:{nodeId:o.id,port:"value"}}});e(k(t,o.id).color).toBeTruthy()}}),t("post-process shader graph builds a fullscreen-safe material",()=>{const t=w(),o=B(t);e(o).toBeInstanceOf(j),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[A]).toBeTruthy(),e(o.uniforms[M]).toBeTruthy()}),t("empty post-process shader graph defaults to identity scene color",()=>{const t=w();t.nodes=[],t.edges=[],t.outputs={};const o=x(t),a=B(t);e(o.output.color).toBeTruthy(),e(o.output.transparent).toBe(!1),e(a.uniforms[A]).toBeTruthy(),e(a.uniforms[M]).toBeTruthy()}),t("post-process color and opacity outputs expose blend weight uniform",()=>{const t=G([{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=x(t),a=B(t);e(o.output.color).toBeTruthy(),e(o.output.opacity).toBeTruthy(),e(o.output.roughness).toBeUndefined(),e(a.uniforms[M]).toBeTruthy()}),t("selected-node preview builds through post-process fullscreen path",()=>{const t=G([{id:"float",kind:"constant.float",params:{value:.25}}],[],{color:{input:{nodeId:"float",port:"value"}}});t.target="postProcess";const o=B(t,{previewNodeId:"float"});e(o).toBeInstanceOf(j),e(o.userData.isPostProcessShaderGraph).toBe(!0),e(o.uniforms[A]).toBeTruthy(),e(o.uniforms[M]).toBeTruthy()}),t("texture parameter nodes expose sampled value and channel outputs",()=>{const t=G([{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=x(t,{parameters:{albedo:new s}}),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=G([{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=x(t,{parameters:{map:O(new s)}});e(o.output.color).toBeTruthy()}),t("missing texture parameter values warn and use fallback sampler",()=>{const t=G([{id:"texture",kind:"input.parameter",params:{parameter:"missingTexture"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"missingTexture",type:"texture",defaultValue:null});const a=o.spyOn(console,"warn").mockImplementation(()=>{});try{const o=x(t),r=o.nodes.get("texture")?.outputs??{};e(r.sampler.type).toBe("sampler2d"),e(o.output.color).toBeTruthy(),e(a).toHaveBeenCalledWith(e.stringContaining("missingTexture"))}finally{a.mockRestore()}}),t("scene depth graph nodes expose sampled and fragment linear eye depth",()=>{const t=G([{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=x(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("screen UV builtin accepts a fullscreen compiler override",()=>{const t=G([{id:"uv",kind:"builtin.screenUv"}],[],{}),o=_(.25,.75),a=x(t,{screenUV:o});e(a.nodes.get("uv")?.outputs.value.value).toBe(o)}),t("unpack normal node compiles with various input combinations",()=>{const t=G([{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=x(t);e(o.nodes.get("toNormal")?.outputs.value.type).toBe("vec3"),e(o.output.normal).toBeTruthy()});class V extends y{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 L extends I{async getTexture(){return new s}async getTextureArray(){return{texture:null,layerIndex:null}}}class R extends L{constructor(e,t){super(),this.textureArray=e,this.layerIndex=t}async getTextureArray(){return{texture:this.textureArray,layerIndex:this.layerIndex}}}t("animated graph ranges share shader source while retaining independent endpoint uniforms",async()=>{const t=G([{id:"amount",kind:"input.parameter",params:{parameter:"amount"}}],[],{opacity:{input:{nodeId:"amount",port:"value"}}});t.parameters.push({name:"amount",type:"float",defaultValue:1});const o=async(e,o,a)=>{const r=await p({amount:{type:g.FloatNode,value:{a:e,b:o,easing:a},override:!0}},t,new V([]),new L);return b(t,{parameters:r})},a=await o(0,1,"Linear"),r=await o(2,8,"Linear"),n=await o(0,1,"InQuad");e(a.vertexShader).toBe(r.vertexShader),e(a.fragmentShader).toBe(r.fragmentShader),e(a.uniforms.pu_amount_curveEndpoints.value.toArray()).toEqual([0,1]),e(r.uniforms.pu_amount_curveEndpoints.value.toArray()).toEqual([2,8]),e(a.uniforms.pu_amount_curveEndpoints.value).not.toBe(r.uniforms.pu_amount_curveEndpoints.value);const d=e=>Object.values(e.uniforms).map(e=>e.value).find(e=>e instanceof s);e(d(a)).toBeTruthy(),e(d(a)).toBe(d(r)),e(d(a)).not.toBe(d(n))}),t("stable particle parameters share GLSL and WGSL across static values and easing profiles",async()=>{const t=G([{id:"amount",kind:"input.parameter",params:{parameter:"amount"}}],[],{opacity:{input:{nodeId:"amount",port:"value"}}});t.shadingModel="unlit",t.parameters.push({name:"amount",type:"float",defaultValue:1});const o=async(e,o=!0)=>{const a=await p({amount:{type:g.FloatNode,value:e,override:!0}},t,new V([]),new L,void 0,void 0,o);return b(t,{parameters:a})},a=await o(2),r=await o({a:0,b:3,easing:"InQuad"}),n=await o({a:-2,b:8,easing:"OutCubic"});e(a.fragmentShader).toBe(r.fragmentShader),e(r.fragmentShader).toBe(n.fragmentShader),e(a.uniforms.pu_amount.value).toBe(2),e(a.uniforms.pu_amount_animation.value.z).toBe(0),e(r.uniforms.pu_amount.value).toBe(0),e(r.uniforms.pu_amount_animation.value.x).toBe(3),e(r.uniforms.pu_amount_animation.value.z).toBe(1);const d=e=>h(f(e),!0),i=d(a);e(i).toBe(d(r)),e(i).toBe(d(n)),e(i).toContain("if ("),e(i).toContain("textureSampleLevel(");const u=e=>Object.values(e.uniforms).map(e=>e.value).filter(e=>e instanceof s);e(u(r)).toHaveLength(1),e(u(a)[0]).toBe(u(r)[0]),e(u(n)[0]).toBe(u(r)[0]);const l=u(r)[0].image,m=Math.floor(r.uniforms.pu_amount_animation.value.y*l.height),c=u(await o({a:0,b:3,easing:"InQuad"},!1))[0].image;e(Array.from(l.data.slice(m*l.width,(m+1)*l.width))).toEqual(Array.from(c.data))});/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -1,4 +1,4 @@
1
- import{describe as e,expect as t,it as r}from"vitest";import{NodeShaderMaterial as o,vec3 as a}from"three-shader-graph";import{surfaceScreenSpaceReflection as n}from"../shader-nodes/surface-reflection.js";import{NodeMaterialWebGpuResolver as s}from"../rendering/node-material-webgpu-resolver.js";import{WGSLShaderMaterial as d}from"@hology/webgpu-renderer";e("late surface reflections",()=>{r("requests opaque snapshots and loads scalar depth from the renderer R32F texture",()=>{const e=new o({color:n(a(0,-1,-5),a(0,1,0))}),r=new s({WGSLShaderMaterial:d}).resolve(e);t(r).toBeDefined(),t(r.requiresLatePass()).toBe(!0),t(Object.values(r.sceneTextures).sort()).toEqual(["sceneColor","sceneDepth"]),t(r.fragmentShader).toMatch(/let d = textureLoad\([^;]+\)\.r;/),t(r.fragmentShader).toMatch(/let sampleDepth = textureLoad\([^;]+\)\.r;/),t(r.fragmentShader).toContain("textureSampleLevel("),t(r.fragmentShader).not.toMatch(/\btextureSample\(/),e.dispose()}),r("keeps the WebGL fallback independent of scene captures",()=>{const e=new o({color:n(a(0,-1,-5),a(0,1,0))});t(e.fragmentShader).not.toContain("surfaceReflection_"),t(e.uniforms.hology_scene_map).toBeUndefined(),t(e.uniforms.hology_depth_map).toBeUndefined(),e.dispose()})});/*
1
+ import{describe as e,expect as t,it as r}from"vitest";import{NodeShaderMaterial as o,vec3 as a}from"three-shader-graph";import{surfaceReflectionStepsUniformName as n,surfaceScreenSpaceReflection as s}from"../shader-nodes/surface-reflection.js";import{NodeMaterialWebGpuResolver as f}from"../rendering/node-material-webgpu-resolver.js";import{WGSLShaderMaterial as d}from"@hology/webgpu-renderer";e("late surface reflections",()=>{r("requests opaque snapshots and loads scalar depth from the renderer R32F texture",()=>{const e=new o({color:s(a(0,-1,-5),a(0,1,0))}),r=new f({WGSLShaderMaterial:d}).resolve(e);t(r).toBeDefined(),t(r.requiresLatePass()).toBe(!0),t(Object.values(r.sceneTextures).sort()).toEqual(["sceneColor","sceneDepth"]),t(r.fragmentShader).toMatch(/let d = textureLoad\([^;]+\)\.r;/),t(r.fragmentShader).toMatch(/let sampleDepth = textureLoad\([^;]+\)\.r;/),t(r.fragmentShader).toContain("textureSampleLevel("),t(r.uniforms[n].value).toBe(32),t(r.fragmentShader).toContain("if (steps <= 0.0) { return vec4<f32>(0.0); }"),t(r.fragmentShader).toContain("if (f32(i) > steps) { break; }"),t(r.fragmentShader).toContain("let t = f32(i) / steps;"),t(r.fragmentShader).not.toMatch(/\btextureSample\(/),e.dispose()}),r("keeps the WebGL fallback independent of scene captures",()=>{const e=new o({color:s(a(0,-1,-5),a(0,1,0))});t(e.fragmentShader).not.toContain("surfaceReflection_"),t(e.uniforms.hology_scene_map).toBeUndefined(),t(e.uniforms.hology_depth_map).toBeUndefined(),e.dispose()})});/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hology/core",
3
- "version": "0.0.274",
3
+ "version": "0.0.275",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -230,7 +230,7 @@
230
230
  "fflate": "^0.8.3",
231
231
  "recast-navigation": "0.39.0",
232
232
  "rxjs": "7.8.1",
233
- "three-mesh-bvh": "^0.7.5",
233
+ "three-mesh-bvh": "^0.9.16",
234
234
  "three-shader-graph": "0.2.74",
235
235
  "three-stdlib": "2.34.0",
236
236
  "ts-key-enum": "^2.0.12",