@hology/core 0.0.274 → 0.0.276
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/effects/sequence/sequence-player.d.ts +1 -0
- package/dist/effects/sequence/sequence-player.js +1 -1
- package/dist/gameplay/actors/builtin/components/index.d.ts +6 -0
- package/dist/gameplay/actors/builtin/components/index.js +1 -1
- package/dist/gameplay/actors/builtin/components/primitive-mesh-component.d.ts +66 -0
- package/dist/gameplay/actors/builtin/components/primitive-mesh-component.js +4 -0
- package/dist/gameplay/actors/builtin/index.d.ts +6 -0
- package/dist/gameplay/actors/builtin/index.js +1 -1
- package/dist/gameplay/actors/builtin/light-actor.d.ts +54 -0
- package/dist/gameplay/actors/builtin/light-actor.js +4 -0
- package/dist/gameplay/actors/builtin/mesh-actor.d.ts +9 -0
- package/dist/gameplay/actors/builtin/mesh-actor.js +4 -0
- package/dist/gameplay/actors/factory.js +1 -1
- package/dist/gameplay/actors/index.d.ts +5 -0
- package/dist/gameplay/actors/index.js +1 -1
- package/dist/gameplay/index.d.ts +1 -0
- package/dist/gameplay/index.js +1 -1
- package/dist/gameplay/initiate.js +1 -1
- package/dist/gameplay/rendering-host.d.ts +1 -0
- package/dist/gameplay/rendering-host.js +1 -1
- package/dist/gameplay/services/asset-loader.d.ts +3 -0
- package/dist/gameplay/services/asset-loader.js +1 -1
- package/dist/gameplay/services/physics/physics-system.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/rendering/node-material-webgpu-resolver.d.ts +5 -1
- package/dist/rendering/node-material-webgpu-resolver.js +1 -1
- package/dist/rendering/post-process-color.d.ts +9 -0
- package/dist/rendering/post-process-color.js +4 -0
- package/dist/rendering/sdfgi-settings.d.ts +8 -0
- package/dist/rendering/sdfgi-settings.js +4 -0
- package/dist/rendering/sdfgi-surface-material.d.ts +8 -0
- package/dist/rendering/sdfgi-surface-material.js +4 -0
- package/dist/rendering/webgl-backend.d.ts +243 -9
- package/dist/rendering/webgl-backend.js +1 -1
- package/dist/rendering/webgpu-experimental-backend.d.ts +14 -3
- package/dist/rendering/webgpu-experimental-backend.js +1 -1
- package/dist/rendering.d.ts +56 -208
- package/dist/rendering.js +1 -1
- package/dist/scene/actor-material-parameters.d.ts +19 -0
- package/dist/scene/actor-material-parameters.js +4 -0
- package/dist/scene/actor-mesh-provider.d.ts +2 -0
- package/dist/scene/actor-mesh-provider.js +4 -0
- package/dist/scene/actor-mesh-renderer.d.ts +40 -0
- package/dist/scene/actor-mesh-renderer.js +4 -0
- package/dist/scene/asset-resource-loader.d.ts +2 -0
- package/dist/scene/asset-resource-loader.js +1 -1
- package/dist/scene/batched-mesh-2.js +1 -1
- package/dist/scene/custom-param-deserialize.js +1 -1
- package/dist/scene/custom-param-runtime-types.js +1 -1
- package/dist/scene/material-ref.d.ts +7 -0
- package/dist/scene/material-ref.js +4 -0
- package/dist/scene/materializer.d.ts +18 -0
- package/dist/scene/materializer.js +1 -1
- package/dist/scene/model.d.ts +4 -1
- package/dist/scene/model.js +1 -1
- package/dist/scene/runtime-bundled-backend-service.d.ts +1 -0
- package/dist/scene/runtime-bundled-backend-service.js +1 -1
- package/dist/scene/scene-data-service.js +1 -1
- package/dist/scene/scene-object-actor-conversion.d.ts +10 -0
- package/dist/scene/scene-object-actor-conversion.js +4 -0
- package/dist/shader/parameter.d.ts +5 -2
- package/dist/shader-nodes/particle.d.ts +2 -2
- package/dist/shader-nodes/surface-reflection.d.ts +1 -0
- package/dist/shader-nodes/surface-reflection.js +1 -1
- package/dist/test/actor-mesh-renderer.test.d.ts +2 -0
- package/dist/test/actor-mesh-renderer.test.js +4 -0
- package/dist/test/actor-persistence-runtime.test.js +1 -1
- package/dist/test/asset-resource-loader-converted-fbx.test.js +1 -1
- package/dist/test/asset-resource-loader-skinned-merge.test.js +1 -1
- package/dist/test/landscape-static-draw-sets.test.js +1 -1
- package/dist/test/node-material-webgpu-resolver.test.js +1 -1
- package/dist/test/physics-ray-object.test.js +1 -1
- package/dist/test/primitive-actors.test.d.ts +2 -0
- package/dist/test/primitive-actors.test.js +4 -0
- package/dist/test/rendering-host.test.js +1 -1
- package/dist/test/runtime-bundled-backend-service.test.d.ts +2 -0
- package/dist/test/runtime-bundled-backend-service.test.js +4 -0
- package/dist/test/scene-object-actor-conversion.test.d.ts +2 -0
- package/dist/test/scene-object-actor-conversion.test.js +4 -0
- package/dist/test/sdfgi-settings.test.d.ts +2 -0
- package/dist/test/sdfgi-settings.test.js +4 -0
- package/dist/test/shader-graph.test.js +1 -1
- package/dist/test/surface-reflection.test.js +1 -1
- package/dist/test/webgl-backend.test.d.ts +2 -0
- package/dist/test/webgl-backend.test.js +4 -0
- package/dist/utils/async.d.ts +2 -0
- package/dist/utils/async.js +1 -1
- package/dist/utils/mesh.js +1 -1
- package/package.json +3 -3
- package/tsconfig.tsbuildinfo +1 -1
package/dist/scene/model.d.ts
CHANGED
|
@@ -52,7 +52,8 @@ export declare enum SerializedParamType {
|
|
|
52
52
|
DataAsset = 30,
|
|
53
53
|
SequenceData = 31,
|
|
54
54
|
Scene = 32,
|
|
55
|
-
ModelRef = 33
|
|
55
|
+
ModelRef = 33,
|
|
56
|
+
MaterialRef = 34
|
|
56
57
|
}
|
|
57
58
|
export type VfxInputBinding = {
|
|
58
59
|
type: 'vfxInput';
|
|
@@ -365,6 +366,8 @@ export type Asset = {
|
|
|
365
366
|
alphaTest?: number;
|
|
366
367
|
/** Whether surfaces using this material can receive projected decals. Defaults to true. */
|
|
367
368
|
receiveDecals?: boolean;
|
|
369
|
+
/** Whether surfaces receive SDFGI bounced diffuse light. Ambient/environment lighting is unaffected. Defaults to true. */
|
|
370
|
+
receiveGI?: boolean;
|
|
368
371
|
/** One of "normal" | "additive" | "subtractive" | "multiply" */
|
|
369
372
|
blending?: BlendingMode;
|
|
370
373
|
/** @deprecated */
|
package/dist/scene/model.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export var DetailTier;!function(e){e[e.low=0]="low",e[e.medium=1]="medium",e[e.high=2]="high"}(DetailTier||(DetailTier={}));export var SerializedParamType;!function(e){e[e.FloatNode=0]="FloatNode",e[e.Number=1]="Number",e[e.Texture=2]="Texture",e[e.Sampler2DNode=3]="Sampler2DNode",e[e.Boolean=4]="Boolean",e[e.BooleanNode=5]="BooleanNode",e[e.Vector2=6]="Vector2",e[e.Vec2Node=7]="Vec2Node",e[e.Vector3=8]="Vector3",e[e.Vec3Node=9]="Vec3Node",e[e.Color=10]="Color",e[e.RgbNode=11]="RgbNode",e[e.String=12]="String",e[e.BaseActor=13]="BaseActor",e[e.Euler=14]="Euler",e[e.Object3D=15]="Object3D",e[e.Material=16]="Material",e[e.AudioBuffer=17]="AudioBuffer",e[e.Vector4=18]="Vector4",e[e.Vec4Node=19]="Vec4Node",e[e.VisualEffect=20]="VisualEffect",e[e.Array=21]="Array",e[e.Curve=22]="Curve",e[e.ColorLayer=23]="ColorLayer",e[e.MaskLayer=24]="MaskLayer",e[e.Prefab=25]="Prefab",e[e.PrefabActor=26]="PrefabActor",e[e.AnimationClip=27]="AnimationClip",e[e.Sequence=28]="Sequence",e[e.Struct=29]="Struct",e[e.DataAsset=30]="DataAsset",e[e.SequenceData=31]="SequenceData",e[e.Scene=32]="Scene",e[e.ModelRef=33]="ModelRef"}(SerializedParamType||(SerializedParamType={}));/*
|
|
1
|
+
export var DetailTier;!function(e){e[e.low=0]="low",e[e.medium=1]="medium",e[e.high=2]="high"}(DetailTier||(DetailTier={}));export var SerializedParamType;!function(e){e[e.FloatNode=0]="FloatNode",e[e.Number=1]="Number",e[e.Texture=2]="Texture",e[e.Sampler2DNode=3]="Sampler2DNode",e[e.Boolean=4]="Boolean",e[e.BooleanNode=5]="BooleanNode",e[e.Vector2=6]="Vector2",e[e.Vec2Node=7]="Vec2Node",e[e.Vector3=8]="Vector3",e[e.Vec3Node=9]="Vec3Node",e[e.Color=10]="Color",e[e.RgbNode=11]="RgbNode",e[e.String=12]="String",e[e.BaseActor=13]="BaseActor",e[e.Euler=14]="Euler",e[e.Object3D=15]="Object3D",e[e.Material=16]="Material",e[e.AudioBuffer=17]="AudioBuffer",e[e.Vector4=18]="Vector4",e[e.Vec4Node=19]="Vec4Node",e[e.VisualEffect=20]="VisualEffect",e[e.Array=21]="Array",e[e.Curve=22]="Curve",e[e.ColorLayer=23]="ColorLayer",e[e.MaskLayer=24]="MaskLayer",e[e.Prefab=25]="Prefab",e[e.PrefabActor=26]="PrefabActor",e[e.AnimationClip=27]="AnimationClip",e[e.Sequence=28]="Sequence",e[e.Struct=29]="Struct",e[e.DataAsset=30]="DataAsset",e[e.SequenceData=31]="SequenceData",e[e.Scene=32]="Scene",e[e.ModelRef=33]="ModelRef",e[e.MaterialRef=34]="MaterialRef"}(SerializedParamType||(SerializedParamType={}));/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -7,6 +7,7 @@ export declare class RuntimeBundledBackendService implements RuntimeBackendServi
|
|
|
7
7
|
private assets;
|
|
8
8
|
private scenes;
|
|
9
9
|
private sceneBlobs;
|
|
10
|
+
private decodedSceneData;
|
|
10
11
|
/**
|
|
11
12
|
* @param dataBundleUrl Optional target-specific location of hology_data.json.
|
|
12
13
|
* Browser builds use the bundled module location by default; native targets
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{decodeStorageValue as e,parseStorageJson as s}from"./storage/packed-json.js";export class RuntimeBundledBackendService{async getSceneNavigationBakes(e){const s=this.sceneBlobs.get(e)?.navigationBakes??{},t=String(this.dataBundleUrl);return Object.fromEntries(Object.entries(s).map(([e,s])=>{const
|
|
1
|
+
import{decodeStorageValue as e,parseStorageJson as s}from"./storage/packed-json.js";export class RuntimeBundledBackendService{async getSceneNavigationBakes(e){const s=this.sceneBlobs.get(e)?.navigationBakes??{},t=String(this.dataBundleUrl);return Object.fromEntries(Object.entries(s).map(([e,s])=>{const a=this.dataBundleUrl instanceof URL||/^[a-z]+:/i.test(t)||globalThis.location?.href?new URL(s.file,new URL(t,globalThis.location?.href)).href:t.slice(0,t.lastIndexOf("/")+1)+s.file;return[e,{...s,file:a}]}))}constructor(e=new URL("../hology_data.json",import.meta.url)){this.dataBundleUrl=e,this.assets=new Map,this.scenes=new Map,this.sceneBlobs=new Map,this.decodedSceneData=new Map}getAssets(){return Promise.resolve(Array.from(this.assets.values()))}getAsset(e){return Promise.resolve(this.assets.get(e))}getScenes(){return Promise.resolve(Array.from(this.scenes.values()))}getScene(e){return Promise.resolve(this.scenes.get(e))}async getSceneData(t){const a=this.decodedSceneData.get(t);if(null!=a)return a;const n=this.sceneBlobs.get(t),o="string"==typeof n.data?s(n.data):e(n.data);return this.decodedSceneData.set(t,o),o}static tryCreate(){}async preloadData(){const s=await(await fetch(this.dataBundleUrl)).json()??{};for(const t of s.asset??[]){const s=e(t);this.assets.set(s.id,s)}for(const e of s.scene??[])this.scenes.set(e.id,e),this.scenes.set(e.name,e);for(const e of s["scene-blob"]??[])this.sceneBlobs.set(e.id,e),this.decodedSceneData.delete(e.id)}}/*
|
|
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{BehaviorSubject as e}from"rxjs";import{Matrix4 as t,Vector3 as
|
|
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
|
*/
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SceneObject } from './materializer.js';
|
|
2
|
+
export interface SceneObjectActorConversion {
|
|
3
|
+
object?: SceneObject;
|
|
4
|
+
warnings: string[];
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function canConvertSceneObjectToActor(source: SceneObject): boolean;
|
|
8
|
+
/** Pure conversion. The editor owns confirmation, persistence and history. */
|
|
9
|
+
export declare function convertSceneObjectToActor(source: SceneObject): SceneObjectActorConversion;
|
|
10
|
+
//# sourceMappingURL=scene-object-actor-conversion.d.ts.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{SerializedParamType as e}from"./model.js";export function canConvertSceneObjectToActor(e){return"asset_mesh"===e.type||"shape_mesh"===e.type&&"landscape"!==e.shape||"light"===e.type&&("point"===e.light?.type||"spot"===e.light?.type)}export function convertSceneObjectToActor(t){const r=[];if(!canConvertSceneObjectToActor(t))return{warnings:r,error:"This object type cannot be converted to an actor."};if(t.latticeDeformation)return{warnings:r,error:"Lattice-deformed meshes cannot be converted yet."};if(t.vertexMaterials?.length)return{warnings:r,error:"Vertex-painted meshes cannot be converted yet."};const a={},o=(e,t,r)=>{null!=r&&(a[e]={type:t,value:r,override:!0})};let n,s;if("light"===t.type){s="light";const a=t.light;if("point"!==a.type&&"spot"!==a.type)return{warnings:r,error:"Unsupported light type."};n="point"===a.type?"PointLight":"SpotLight";const i="point"===a.type?a.point:a.spot;o("color",e.Color,i.color);for(const t of["intensity","distance","decay","volumetricIntensity"])o(t,e.Number,i[t]);o("castShadow",e.Boolean,i.castShadow),"spot"===a.type&&(o("angle",e.Number,a.spot.angle),o("penumbra",e.Number,a.spot.penumbra),o("map",e.Texture,a.spot.mapTextureId))}else{if(s="mesh",n="asset_mesh"===t.type?"Mesh":"Shape","asset_mesh"===t.type){if(!t.assetId)return{warnings:r,error:"The mesh has no model asset."};o("model",e.ModelRef,t.assetId)}else{if(!t.shape)return{warnings:r,error:"The object has no shape."};a.geometry={type:e.Struct,struct:`hology.shape.${t.shape}`,value:t.shapeParams??{}},o("collider",e.Boolean,t.collider)}if(o("hidden",e.Boolean,t.hidden),o("castShadow",e.Boolean,t.castShadow),o("receiveShadow",e.Boolean,t.receiveShadow),o("renderOrder",e.Number,t.renderOrder),o("collision",e.Boolean,t.collisionDetection),o("mobility",e.String,t.physics&&2!==t.physics.type?"movable":"static"),t.physics){o("bodyType",e.Number,t.physics.type);for(const r of["friction","restitution","mass","density"])o(r,e.Number,t.physics[r])}if(t.materialAssignments?.length)if("shape_mesh"===t.type){const r=t.materialAssignments.find(e=>"Default"===e.name||"#aaaaaa"===e.color);o("material",e.MaterialRef,r?.materialId)}else a.materials={type:e.Array,element:e.Struct,elementStruct:"hology.mesh.materialSlot",value:t.materialAssignments.map(t=>({struct:"hology.mesh.materialSlot",value:{name:{type:e.String,value:t.name??""},color:{type:e.String,value:t.color??""},material:{type:e.MaterialRef,value:t.materialId}}}))};t.surfaceScatter?.meshes?.length&&r.push("Surface scatter will be removed.")}return{warnings:r,object:{id:t.id,name:t.name,type:"actor",position:t.position?.slice(),rotation:t.rotation?.slice(),scale:t.scale?.slice(),enabled:t.enabled,hidden:t.hidden,children:t.children,detailCulling:t.detailCulling,renderOrder:t.renderOrder,actor:{type:n,innerParams:[{path:[s],params:a}]}}}}/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -8,11 +8,12 @@ import type { Prefab, PrefabOf } from '../scene/objects/prefab.js';
|
|
|
8
8
|
import type { DataAssetRef } from '../scene/objects/data-asset.js';
|
|
9
9
|
import type { SceneRef } from '../scene/scene-ref.js';
|
|
10
10
|
import type { ModelRef } from '../scene/model-ref.js';
|
|
11
|
+
import type { MaterialRef } from '../scene/material-ref.js';
|
|
11
12
|
import type { Curve2 } from '../utils/curve.js';
|
|
12
13
|
import type { ColorLayer, MaskLayer } from './color-layer.js';
|
|
13
14
|
import type { Sequence } from '../effects/sequence/sequence-data.js';
|
|
14
15
|
import type { ShaderGraphTarget } from './graph/model.js';
|
|
15
|
-
export type KnownParameterType = Type<FloatNode | Number | Texture | Sampler2DNode | Boolean | BooleanNode | Vector2 | Vec2Node | Vector3 | Vec3Node | Vector4 | Vec4Node | Color | RgbNode | String | BaseActor | Euler | Object3D | Material | AnimationClip | VisualEffect | Sequence | Prefab | PrefabOf<BaseActor> | DataAssetRef | SceneRef | ModelRef | Curve2 | ColorLayer | MaskLayer>;
|
|
16
|
+
export type KnownParameterType = Type<FloatNode | Number | Texture | Sampler2DNode | Boolean | BooleanNode | Vector2 | Vec2Node | Vector3 | Vec3Node | Vector4 | Vec4Node | Color | RgbNode | String | BaseActor | Euler | Object3D | Material | AnimationClip | VisualEffect | Sequence | Prefab | PrefabOf<BaseActor> | DataAssetRef | SceneRef | ModelRef | MaterialRef | Curve2 | ColorLayer | MaskLayer>;
|
|
16
17
|
export type ParameterType = Type<any> | AbstractType<any>;
|
|
17
18
|
export type ParameterTypeResolver = () => ParameterType;
|
|
18
19
|
export type ParameterOption<T> = {
|
|
@@ -34,7 +35,9 @@ export interface ParameterOptions {
|
|
|
34
35
|
/** Optional editor group heading used by grouped parameter forms. */
|
|
35
36
|
group?: string;
|
|
36
37
|
/** Hint for specialized inline editor controls. */
|
|
37
|
-
editorInput?: 'colorGrade';
|
|
38
|
+
editorInput?: 'colorGrade' | 'materialAssignments';
|
|
39
|
+
/** Sibling ModelRef parameter supplying slots for the materialAssignments editor. Omit for a shape's default slot. */
|
|
40
|
+
materialModelParameter?: string;
|
|
38
41
|
/** Restrict a material-valued parameter to shader-graph assets of this target. */
|
|
39
42
|
shaderGraphTarget?: ShaderGraphTarget;
|
|
40
43
|
help?: string;
|
|
@@ -6,14 +6,14 @@ export declare const particleVelcoityUniformName = "hology_particle_velocity";
|
|
|
6
6
|
export declare const particleOpacityUniformName = "hology_particle_opacity";
|
|
7
7
|
export declare const particleColorUniformName = "hology_particle_color";
|
|
8
8
|
declare class InstancedColorNode extends Vec3Node {
|
|
9
|
-
compile(c: Compiler): import("three-shader-graph/build/main/lib/compiler").CompileResult<string>;
|
|
9
|
+
compile(c: Compiler): import("../../../three-shader-graph/build/main/lib/compiler").CompileResult<string>;
|
|
10
10
|
}
|
|
11
11
|
declare class ParticleScalarNode extends FloatNode {
|
|
12
12
|
private readonly uniform;
|
|
13
13
|
private readonly component;
|
|
14
14
|
private readonly particleData;
|
|
15
15
|
constructor(uniform: FloatNode, component: 'x' | 'y');
|
|
16
|
-
compile(c: Compiler): import("three-shader-graph/build/main/lib/compiler").CompileResult<string> | {
|
|
16
|
+
compile(c: Compiler): import("../../../three-shader-graph/build/main/lib/compiler").CompileResult<string> | {
|
|
17
17
|
out: unknown;
|
|
18
18
|
};
|
|
19
19
|
}
|
|
@@ -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
|
|
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
|
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{Box3 as e,BoxGeometry as t,Group as s,Matrix4 as n,Mesh as o,MeshStandardMaterial as a,Raycaster as i,Scene as r,Vector3 as d}from"three";import{describe as l,expect as c,it as p,vi as m}from"vitest";import{ActorMeshRenderer as h}from"../scene/actor-mesh-renderer.js";function u(){const e=new r,n=new h(e),i=new t,d=new a,l=new s,c=new o(i,d);c.position.x=2,l.add(c);const p=new s,m=new s,u=new s;e.add(p),p.add(m,u),u.position.x=10;const v=n.create(m,l,[],!0),g=n.create(u,l,[],!0);return{scene:e,renderer:n,template:l,mesh:c,parent:p,a:m,b:u,first:v,second:g}}function v(e){return e.children.filter(e=>e.isInstancedMesh)}l("actor mesh render handles",()=>{p("seals static pages, skips polling, refreshes explicit edits, and isolates later spawns",()=>{const e=new r,i=[],d=m.fn(e=>{e.visible=!1;const t=Object.assign(m.fn(()=>{e.visible=!0}),{updateTransforms:m.fn()});return i.push(t),t}),l=new h(e,void 0,void 0,d),p=new o(new t,new a),u=new s,g=new s,w=new s;e.add(u,g,w);const x=l.create(u,p,[],!0,{mobility:"static"}),f=l.create(g,p,[],!0,{mobility:"static"}),B=l.create(w,p,[],!0);e.updateMatrixWorld(!0),l.finalizeStaticPages(),l.finalizeStaticPages(),c(d).toHaveBeenCalledTimes(1);const b=v(e)[0],M=new n,H=m.spyOn(u,"updateWorldMatrix");u.position.x=20,e.updateMatrixWorld(!0),c(H).not.toHaveBeenCalled(),b.getMatrixAt(0,M),c(M.elements[12]).toBe(0),x.updateTransform(),e.updateMatrixWorld(!0),b.getMatrixAt(0,M),c(M.elements[12]).toBe(20),c(i[0].updateTransforms).toHaveBeenCalledOnce();const W=new s;e.add(W);const y=l.create(W,p,[],!0,{mobility:"static"});c(v(e)).toHaveLength(3),c(d).toHaveBeenCalledTimes(1),f.setVisible(!1),e.updateMatrixWorld(!0),c(i[0].updateTransforms).toHaveBeenCalledTimes(2),f.dispose(),e.updateMatrixWorld(!0),c(i[0].updateTransforms).toHaveBeenCalledTimes(3),x.setMobility("movable"),c(i[0]).toHaveBeenCalledOnce(),c(b.visible).toBe(!0),u.position.x=30,e.updateMatrixWorld(!0),b.getMatrixAt(0,M),c(M.elements[12]).toBe(30),x.dispose(),B.dispose(),y.dispose(),c(v(e)).toHaveLength(0)}),p("shares draws and synchronizes parent motion and direct axis writes without visual children",()=>{const{scene:t,parent:s,a:o,b:a,first:i}=u();c(i.instanced).toBe(!0),c(v(t)).toHaveLength(1),c(o.children).toHaveLength(0),c(a.children).toHaveLength(0),s.position.x=5,o.position.x=3,t.updateMatrixWorld(!0);const r=v(t)[0],d=new n;r.getMatrixAt(0,d),c(d.elements[12]).toBe(10),r.getMatrixAt(1,d),c(d.elements[12]).toBe(17);const l=r.instanceMatrix.version;t.updateMatrixWorld(!0),c(r.instanceMatrix.version).toBe(l),s.position.x++,t.updateMatrixWorld(!0),r.getMatrixAt(0,d),c(d.elements[12]).toBe(11),c(i.getWorldBounds(new e).min.x).toBeCloseTo(10.5)}),p("composes visibility, resolves picking to the actor root, and safely reuses slots",()=>{const{scene:t,renderer:n,template:o,parent:a,a:r,b:l,first:p,second:m}=u(),h=new i(new d(2,0,5),new d(0,0,-1));t.updateMatrixWorld(!0),c(h.intersectObject(t)[0].object).toBe(r),p.setVisible(!1),t.updateMatrixWorld(!0),c(h.intersectObject(t)).toHaveLength(0),a.visible=!1,p.setVisible(!0),t.updateMatrixWorld(!0),c(h.intersectObject(t)).toHaveLength(0),a.visible=!0,p.dispose(),p.dispose();const g=new s;a.add(g);const w=n.create(g,o,[],!0);t.updateMatrixWorld(!0),c(v(t)).toHaveLength(1),c(h.intersectObject(t)[0].object).toBe(g),c(l.parent).toBe(a),m.dispose(),w.dispose(),c(v(t)).toHaveLength(0),c(p.getWorldBounds(new e).isEmpty()).toBe(!0)}),p("uses independent dedicated hierarchies for incompatible visuals without disposing shared resources",()=>{const{scene:e,renderer:t,template:s,mesh:n,a:o,first:a,second:i}=u(),r=m.spyOn(n.geometry,"dispose"),d=m.spyOn(n.material,"dispose");n.material.transparent=!0;const l=t.create(o,s,[],!0);c(l.instanced).toBe(!1),c(o.children).toHaveLength(1),c(o.children[0]).not.toBe(s),a.dispose(),i.dispose(),l.dispose(),c(v(e)).toHaveLength(0),c(r).not.toHaveBeenCalled(),c(d).not.toHaveBeenCalled()}),p("pages large groups and supports every submesh of a model",()=>{const{scene:e,renderer:n,template:o,mesh:a,parent:i,first:r,second:d}=u(),l=a.clone();l.geometry=new t(2,2,2),o.add(l);const p=[r,d];for(let e=0;e<130;e++){const e=new s;i.add(e),p.push(n.create(e,o,[],!0))}c(v(e)).toHaveLength(4),e.updateMatrixWorld(!0);for(const e of p)e.dispose();c(v(e)).toHaveLength(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"reflect-metadata";import{expect as e,test as t,vi as n}from"vitest";n.hoisted(()=>{const e=new Proxy({},{get:(e,t)=>e[t]??(e[t]=n.fn())});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),n.mock("../scene/bootstrap.js",()=>({loadScene:n.fn()})),n.mock("../rendering.js",()=>({RenderingView:class{},GraphicsRuntime:class{}}));import{loadScene as r}from"../scene/bootstrap.js";import{HologyApplicationRuntime as s}from"../gameplay/initiate.js";import{NetMode as o}from"../gameplay/net/net-session.js";import{SceneRef as
|
|
1
|
+
import"reflect-metadata";import{expect as e,test as t,vi as n}from"vitest";n.hoisted(()=>{const e=new Proxy({},{get:(e,t)=>e[t]??(e[t]=n.fn())});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),n.mock("../scene/bootstrap.js",()=>({loadScene:n.fn()})),n.mock("../rendering.js",()=>({RenderingView:class{},GraphicsRuntime:class{}}));import{loadScene as r}from"../scene/bootstrap.js";import{HologyApplicationRuntime as s}from"../gameplay/initiate.js";import{NetMode as o}from"../gameplay/net/net-session.js";import{SceneRef as a}from"../scene/scene-ref.js";import{BehaviorSubject as i}from"rxjs";function c(){const e=function(){let e,t;return{promise:new Promise((n,r)=>{e=n,t=r}),resolve:e,reject:t}}(),t={},a=[],i={registerPersistentActors:()=>[t]},c={sceneName:"old",netMode:o.none,sceneRoots:[],components:{},materializer:{dispose:n.fn()},container:{set:n.fn()},viewController:{paused:!1},renderingView:{compileAsync:n.fn(),initTexturesAsync:n.fn()},physics:{removeRemoved:n.fn(),addFromScene:n.fn()},sceneTransition:{publish:n.fn()},world:{actors:[],stopSceneEventGraphs:n.fn(),removeSceneLifetimeActors:n.fn(),persistence:{captureAll:n.fn(()=>a.push("capture")),restore:n.fn(()=>e.promise)},addActor:n.fn(function(e){this.actors.push(e),a.push("register")}),beginActorPlay:n.fn(()=>a.push("begin")),startSceneEventGraphs:n.fn(()=>a.push("graphs"))}},p={},l=Object.create(s.prototype);return Object.assign(l,{config:{shaders:{},actors:{},dataDir:""},getApplicationService:n.fn(),loading:n.fn(),checkIntent:n.fn(),setState:n.fn(),renderingHost:{applicationView:p,activate:n.fn(e=>a.push(e===p?"hide":"present"))}}),n.mocked(r).mockResolvedValue({materializer:i,actors:[t],sceneRoots:[],scene:{}}),{runtime:l,session:c,restore:e,order:a}}t.each([!1,!0])("loading preserves GPU overlay presentation: %s",t=>{const r={stopAnimationLoop:n.fn(),running:!0},o=Object.create(s.prototype);Object.assign(o,{renderingHost:{applicationView:r,activeView:r,hasRenderStages:t,activate:n.fn()},stateSubject:new i({type:"menu"})}),o.setState({type:"loading",sceneName:"main",stage:"scene",progress:.42}),e(r.stopAnimationLoop).toHaveBeenCalledTimes(t?0:1),e(o.currentState.type).toBe("loading"),r.running=!1,o.setState({type:"loading",sceneName:"main",stage:"shaders",progress:.7}),e(o.renderingHost.activate).toHaveBeenCalledTimes(t?1:0)}),t("travel suspends presentation and simulation until restore, begin-play and graphs complete",async()=>{const{runtime:t,session:r,restore:s,order:o}=c(),i=t.performSceneTransition(r,{scene:new a("next")},1);await n.waitFor(()=>e(r.world.persistence.restore).toHaveBeenCalled()),e(r.viewController.paused).toBe(!0),e(o).toEqual(["hide","capture","register"]),s.resolve(),await i,e(o).toEqual(["hide","capture","register","begin","graphs","present"]),e(r.viewController.paused).toBe(!1)}),t("failed restore leaves the scene suspended without begin-play or event graphs",async()=>{const{runtime:t,session:r,restore:s,order:o}=c(),i=t.performSceneTransition(r,{scene:new a("next")},1),p=e(i).rejects.toThrow("invalid saved actor");await n.waitFor(()=>e(r.world.persistence.restore).toHaveBeenCalled()),s.reject(new Error("invalid saved actor")),await p,e(r.viewController.paused).toBe(!0),e(o).toEqual(["hide","capture","register"])}),t("cancelled loading cannot begin-play or present a restored scene",async()=>{const{runtime:t,session:r,restore:s,order:o}=c(),i=t.performSceneTransition(r,{scene:new a("next")},1),p=e(i).rejects.toThrow("cancelled");await n.waitFor(()=>e(r.world.persistence.restore).toHaveBeenCalled()),t.checkIntent.mockImplementation(()=>{throw new Error("cancelled")}),s.resolve(),await p,e(o).not.toContain("begin"),e(o).not.toContain("present")}),t("network persistence is rejected before creating a session",async()=>{const{runtime:t}=c();t.createSession=n.fn(),await e(t.performLoad(class{},{persistence:{},multi:{session:{mode:o.client}}},1)).rejects.toThrow("single-player"),e(t.createSession).not.toHaveBeenCalled()});/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{describe as e,expect as
|
|
1
|
+
import{describe as e,expect as o,it as t,vi as n}from"vitest";import{BoxGeometry as s,Group as a,Mesh as i,MeshBasicMaterial as l,Scene as c}from"three";import{AssetResourceLoader as r}from"../scene/asset-resource-loader.js";n.mock("../scene/materializer.js",()=>({applyMaterial:n.fn(),getConvertedFbxToGlbAssignedMaterialTextureFlipY:n.fn()})),n.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,o)=>(o in e||(e[o]=n.fn()),e[o]),set:(e,o,t)=>(e[o]=t,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),e("AssetResourceLoader converted FBX hierarchy",()=>{t("overrides collision defaults per instance without changing the asset or leaking through the shape cache",async()=>{const e=new r;e.setDataDir("");const t=new a;t.add(new i(new s(2,4,6),new l)),n.spyOn(e,"loadMesh").mockResolvedValue({scene:t,animations:[]});const c={id:"collision-override",name:"Model",type:"mesh",fileKey:"model.glb",fileFormat:"glb",collisionDetection:!1};o((await e.getMesh(c)).scene.collisionShapes).toHaveLength(0);const d=await e.getMesh(c,{collisionDetection:!0});o(d.scene.collisionShapes).toHaveLength(1);const h=await e.getMesh(c,{collisionDetection:!0});o(h.scene.collisionShapes).toBe(d.scene.collisionShapes),o((await e.getMesh(c)).scene.collisionShapes).toHaveLength(0),o(c.collisionDetection).toBe(!1);const g={...c,collisionDetection:!0};o((await e.getMesh(g,{collisionDetection:!1})).scene.collisionShapes).toHaveLength(0),o((await e.getMesh(g)).scene.collisionShapes).toHaveLength(1)}),t("unwraps the synthetic glTF scene around a Three-converted FBX root",async()=>{const e=new r;e.setDataDir("");const t=new c,s=new a;s.name="AuthoredFbxRoot",t.add(s),e.glbLoader={loadAsync:n.fn().mockResolvedValue({scene:t,animations:[]})};const i=await e.loadByAsset({id:"converted-fbx",name:"Converted FBX",type:"mesh",fileKey:"model.glb",fileFormat:"glb",originalFileKey:"model.fbx"});o(i.scene).toBe(s),o(s.parent).toBeNull()}),t("keeps the synthetic scene for an ordinary GLB",async()=>{const e=new r;e.setDataDir("");const t=new c;t.add(new a),e.glbLoader={loadAsync:n.fn().mockResolvedValue({scene:t,animations:[]})};const s=await e.loadByAsset({id:"native-glb",name:"Native GLB",type:"mesh",fileKey:"model.glb",fileFormat:"glb"});o(s.scene).toBe(t)})});/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{describe as e,expect as
|
|
1
|
+
import{describe as e,expect as t,test as n}from"vitest";import{Bone as o,BufferAttribute as r,BufferGeometry as i,Group as a,InterleavedBuffer as s,InterleavedBufferAttribute as d,MeshBasicMaterial as w,Skeleton as c,SkinnedMesh as m}from"three";import{mergeCompatibleSkinnedMeshes as l}from"../utils/mesh.js";function f(e){const t=new i;return t.setAttribute("position",new r(new Float32Array([e,0,0,e+1,0,0,e,1,0]),3)),t.setAttribute("skinIndex",new r(new Uint16Array(12),4)),t.setAttribute("skinWeight",new r(new Float32Array([1,0,0,0,1,0,0,0,1,0,0,0]),4)),t}function u(e){e.updateWorldMatrix(!0,!0),l(e)}e("AssetResourceLoader skinned mesh optimization",()=>{n("merges interleaved attributes with offsets and normalized integer data intact",()=>{const e=new a,n=new c([new o]),r=new w,i=new Float32Array([9,1,2,3,9,4,5,6,9,7,8,9]);for(let t=0;t<2;t++){const t=f(0);t.setAttribute("position",new d(new s(i,4),3,1)),t.setAttribute("color",new d(new s(new Uint8Array([9,255,0,128,9,0,128,255,9,128,255,0]),4),3,1,!0)),t.setIndex([0,1,2]);const o=new m(t,r);o.bind(n),e.add(o)}u(e);const l=e.children.filter(e=>e instanceof m);t(l).toHaveLength(1);const A=l[0].geometry;t(Array.from(A.getAttribute("position").array)).toEqual([1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,9]),t(Array.from(A.getAttribute("color").array)).toEqual([255,0,128,0,128,255,128,255,0,255,0,128,0,128,255,128,255,0]),t(A.getAttribute("color").normalized).toBe(!0),t(Array.from(A.index.array)).toEqual([0,1,2,3,4,5]),t(i[0]).toBe(9)}),n("merges meshes sharing a skeleton, bind space, transform and named material slot",()=>{const e=new a,n=new o;e.add(n);const r=new c([n]),i=new w,s=new w;i.name="Body",s.name="Body";const d=new m(f(0),i),l=new m(f(2),s);d.bind(r),l.bind(r),e.add(d,l),u(e);const A=e.children.filter(e=>e instanceof m);t(A).toHaveLength(1),t(A[0].geometry.getAttribute("position").count).toBe(6),t(A[0].skeleton).toBe(r),t(A[0].material).toBe(i)}),n("keeps meshes with different bind-space transforms separate",()=>{const e=new a,n=new o;e.add(n);const r=new c([n]),i=new w;i.name="Body";const s=new m(f(0),i),d=new m(f(2),i);s.bind(r),d.bind(r),d.position.x=1,e.add(s,d),u(e);const l=e.children.filter(e=>e instanceof m);t(l).toHaveLength(2)})});/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{expect as e,test as s,vi as t}from"vitest";import{Group as a,Mesh as i,Vector3 as n}from"three";import{LandscapeStaticDrawSets as o}from"../scene/landscape/landscape-static-draw-sets.js";function r(){const e=new a,s=new a,n=new i;n.x=2,n.y=3,s.add(n);const r=t.fn(()=>{n.visible=!0}),l=t.fn(e=>(e.visible=!1,r)),d=new o({registerExperimentalStaticDrawSet:l},s,"terrain");return{scene:e,landscape:s,section:n,sets:d,dispose:r,registerExperimentalStaticDrawSet:l}}s("waits for attachment and captures the final ancestor transform using strict registration",()=>{const{scene:s,landscape:t,section:a,sets:i,registerExperimentalStaticDrawSet:o}=r();i.invalidate(a),i.flush(),e(o).not.toHaveBeenCalled(),s.position.set(10,0,0),t.position.set(20,0,0),a.position.set(5,0,0),s.add(t),i.flush(),e((new n).setFromMatrixPosition(a.matrixWorld).x).toBe(35),e(o).toHaveBeenCalledWith(a,"terrain-2,3",!1,"throw"),i.flush(),e(o).toHaveBeenCalledTimes(1)}),s("unloading disposes the GPU snapshot and reloading can register again",()=>{const{scene:s,landscape:t,section:a,sets:i,dispose:n,registerExperimentalStaticDrawSet:o}=r();s.add(t),i.invalidate(a),i.flush(),i.remove(a),t.remove(a),e(n).toHaveBeenCalledTimes(1),e(a.visible).toBe(!0),t.add(a),i.invalidate(a),i.flush(),e(o).toHaveBeenCalledTimes(2)}),s("invalidating a snapshot restores its source and coalesces rebuilds",()=>{const{scene:s,landscape:t,section:a,sets:i,dispose:n,registerExperimentalStaticDrawSet:o}=r();s.add(t),i.invalidate(a),i.flush(),i.invalidate(a),i.invalidate(a),e(n).toHaveBeenCalledTimes(1),e(a.visible).toBe(!0),i.flush(),e(o).toHaveBeenCalledTimes(2)}),s("does not snapshot detached sections or hidden ancestors",()=>{const{scene:s,landscape:t,section:a,sets:i,registerExperimentalStaticDrawSet:n}=r();s.add(t),s.visible=!1,i.invalidate(a),i.flush(),e(n).not.toHaveBeenCalled(),s.visible=!0,t.remove(a),i.flush(),e(n).not.toHaveBeenCalled()}),s("disposal releases snapshots and prevents late asynchronous work from registering",()=>{const{scene:s,landscape:t,section:a,sets:i,dispose:n,registerExperimentalStaticDrawSet:o}=r();s.add(t),i.invalidate(a),i.flush(),i.dispose(),i.dispose(),i.invalidate(a),i.flush(),e(n).toHaveBeenCalledTimes(1),e(o).toHaveBeenCalledTimes(1)});/*
|
|
1
|
+
import{expect as e,test as s,vi as t}from"vitest";import{Group as a,Mesh as i,Vector3 as n}from"three";import{LandscapeStaticDrawSets as o}from"../scene/landscape/landscape-static-draw-sets.js";function r(){const e=new a,s=new a,n=new i;n.x=2,n.y=3,s.add(n);const r=t.fn(()=>{n.visible=!0}),l=t.fn(e=>(e.visible=!1,Object.assign(r,{updateTransforms:t.fn()}))),d=new o({registerExperimentalStaticDrawSet:l},s,"terrain");return{scene:e,landscape:s,section:n,sets:d,dispose:r,registerExperimentalStaticDrawSet:l}}s("waits for attachment and captures the final ancestor transform using strict registration",()=>{const{scene:s,landscape:t,section:a,sets:i,registerExperimentalStaticDrawSet:o}=r();i.invalidate(a),i.flush(),e(o).not.toHaveBeenCalled(),s.position.set(10,0,0),t.position.set(20,0,0),a.position.set(5,0,0),s.add(t),i.flush(),e((new n).setFromMatrixPosition(a.matrixWorld).x).toBe(35),e(o).toHaveBeenCalledWith(a,"terrain-2,3",!1,"throw"),i.flush(),e(o).toHaveBeenCalledTimes(1)}),s("unloading disposes the GPU snapshot and reloading can register again",()=>{const{scene:s,landscape:t,section:a,sets:i,dispose:n,registerExperimentalStaticDrawSet:o}=r();s.add(t),i.invalidate(a),i.flush(),i.remove(a),t.remove(a),e(n).toHaveBeenCalledTimes(1),e(a.visible).toBe(!0),t.add(a),i.invalidate(a),i.flush(),e(o).toHaveBeenCalledTimes(2)}),s("invalidating a snapshot restores its source and coalesces rebuilds",()=>{const{scene:s,landscape:t,section:a,sets:i,dispose:n,registerExperimentalStaticDrawSet:o}=r();s.add(t),i.invalidate(a),i.flush(),i.invalidate(a),i.invalidate(a),e(n).toHaveBeenCalledTimes(1),e(a.visible).toBe(!0),i.flush(),e(o).toHaveBeenCalledTimes(2)}),s("does not snapshot detached sections or hidden ancestors",()=>{const{scene:s,landscape:t,section:a,sets:i,registerExperimentalStaticDrawSet:n}=r();s.add(t),s.visible=!1,i.invalidate(a),i.flush(),e(n).not.toHaveBeenCalled(),s.visible=!0,t.remove(a),i.flush(),e(n).not.toHaveBeenCalled()}),s("disposal releases snapshots and prevents late asynchronous work from registering",()=>{const{scene:s,landscape:t,section:a,sets:i,dispose:n,registerExperimentalStaticDrawSet:o}=r();s.add(t),i.invalidate(a),i.flush(),i.dispose(),i.dispose(),i.invalidate(a),i.flush(),e(n).toHaveBeenCalledTimes(1),e(o).toHaveBeenCalledTimes(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,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 r,vi as a}from"vitest";import{DataTexture as s,Material as i,MeshPhysicalMaterial as l,MeshStandardMaterial as d,RGBAFormat as h,SpriteMaterial as c,Vector4 as u}from"three";import{batchingUniformVec4 as m,float as p,NodeShaderMaterial as f,rgb as v,rgba as S,standardMaterial as g,translateX as w,uniformFloat as x,uniformVec4 as B,uniformSampler2d as C,varyingAttributes as y,vec3 as M,vec4 as _}from"three-shader-graph";import{createNodeMaterialSignature as b,markMaterialUniformValuesNeedUpdate as L,NodeMaterialWebGpuResolver as T,setMaterialUniformValuesAutoUpdate as W,withThreeDefaultVertexAttributeValues as G}from"../rendering/node-material-webgpu-resolver.js";import{StandardShader as A}from"../shader/builtin/standard-shader.js";import{LandscapeCompositeShader as D}from"../shader/builtin/landscape-composite-shader.js";import{DecalUnlitShader as U}from"../shader/builtin/decal-unlit-shader.js";import{getSpritePosition as I,SpriteShader as k}from"../shader/sprite-shader.js";import{depthNormal as P,depthWorldPosition as V,fragmentLinearEyeDepth as q,sampleSceneDepth as O,screenUV as j}from"../shader-nodes/depth.js";import{sampleSceneColor as F}from"../shader-nodes/scene-sample.js";import{applyDithering as E}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 X}from"../shader/graph/index.js";class J 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.defines=e.defines??{},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",()=>{r.each([["basic","TSG_SHADOWMAP_TYPE_BASIC"],["pcf-2x2","TSG_SHADOWMAP_TYPE_PCF_2X2"],["pcf-5tap","TSG_SHADOWMAP_TYPE_PCF_5TAP"],["pcf-poisson-5tap","TSG_SHADOWMAP_TYPE_PCF_POISSON_5TAP"]])("preserves per-material %s shadow filtering in WebGPU",(e,t)=>{const o=new f({color:g({color:v(16777215)}),shadowFilter:e}),r=new T({WGSLShaderMaterial:J});for(const e of[void 0,"opaque"]){const a=r.resolve(o,e);n(a.defines[t]).toBe(!0),n(Object.values(a.defines).filter(Boolean)).toHaveLength(1)}const a=r.resolve(o,"opaque");o.defines[t]=!1,o.needsUpdate=!0;const s=r.resolve(o,"opaque");n(s).not.toBe(a),n(a.disposed).toBe(!0),n(Object.values(s.defines).every(e=>!e)).toBe(!0),r.dispose()}),r("opts into vertex indirect lighting and invalidates the selected sampling mode",()=>{const e=new f({color:g({color:v(5210052),specular:!1})});e.userData.indirectLighting="vertex";const t=new T({WGSLShaderMaterial:J});for(const o of[void 0,"opaque"]){const r=t.resolve(e,o);n(r.vertexShader).toContain("rendererDiffuseGIAt("),n(r.fragmentShader).not.toContain("rendererIndirectDiffuseAt("),n(r.fragmentShader).toContain("rendererSampleDirectionalShadowForLightWithNormal")}const o=t.resolve(e,"opaque");e.userData.indirectLighting="fragment",e.needsUpdate=!0;const r=t.resolve(e,"opaque");n(r).not.toBe(o),n(r.vertexShader).not.toContain("rendererDiffuseGIAt("),n(r.fragmentShader).toContain("rendererIndirectDiffuseAt("),t.dispose()}),r("renders custom sky materials at far depth without modifying their authored graph",()=>{const t=(new A).build(),o=t.nodeMaterialDefinition.position;e(t);const r=new T({WGSLShaderMaterial:J}),a=r.resolve(t,"late");n(a).toBeDefined(),n(a.vertexShader).toMatch(/vec4<f32>\(\w+\.x, \w+\.y, (\w+)\.w, \1\.w\)/),n(a.vertexShader).toContain("vec4<f32>(input.position, 0.0)"),n(a.depthWrite).toBe(!1),n(a.depthTest).toBe(!0),n(a.transparent).toBe(!0),n(t.nodeMaterialDefinition.position).toBe(o),r.dispose()}),r("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 r=new T({WGSLShaderMaterial:J}),a=r.resolve(o,"late");n(a.vertexShader).toMatch(/vec4<f32>\(\w+\.x, \w+\.y, (\w+)\.w, \1\.w\)/),n(o.fog).toBe(!0),n(o.fragmentShader).toContain("fogDensity"),n(a.fragmentShader).toContain("rendererFogDensity()"),e.geometry.dispose(),r.dispose()}),r("compiles physical lobes and binds transmission to the HDR late-pass snapshot",()=>{const e=new A;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:J}),r=o.resolve(t,"late");n(r.graphTextures.hology_transmission_map).toMatchObject({resource:"physicalTransmissionSnapshot",requiresLatePass:!0}),n(r.fragmentShader).toContain("tsgTransmission_volume"),n(r.fragmentShader).toContain("tsgIridescence_eval"),n(r.fragmentShader).toContain("textureNumLevels"),n(r.fragmentShader).not.toContain("vec3 "),n(r.fragmentShader).not.toMatch(/\bmat3\(/),o.dispose()}),r("omits disabled physical features from ordinary materials",()=>{const e=new T({WGSLShaderMaterial:J}),t=(new A).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()}),r("preserves native physical properties and updates enabled lobe uniforms",()=>{const e=new T({WGSLShaderMaterial:J}),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 r=e.resolve(t);n(r).not.toBe(o),n(r.fragmentShader).toContain("tsgTransmission_volume"),n(r.graphTextures.hology_transmission_map).toMatchObject({requiresLatePass:!0}),e.dispose()}),r("keeps explicitly tracked outline uniforms clean until controls change",()=>{const e=new f({color:g({color:v(16777215)})});e.uniformValuesAutoUpdate=!1,e.userData.outlineParameters={thickness:.012,darkening:.65,alpha:.8};const t=new T({WGSLShaderMaterial:J}),o=t.resolve(e,"outline");n(o.uniformValuesAutoUpdate).toBe(!1);const r=o.uniformValuesAutoVersion;n(t.resolve(e,"outline")).toBe(o),n(o.uniformValuesAutoVersion).toBe(r),e.userData.outlineParameters.thickness=.025,t.resolve(e,"outline"),n(o.uniforms.outlineThickness.value).toBe(.025),n(o.uniformValuesAutoVersion).toBeGreaterThan(r),t.dispose()}),r("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:g({color:t})});o.userData.outlineParameters={colorMode:"darkenedAlbedo",thickness:.012,darkening:.65,alpha:.8};const r=new T({WGSLShaderMaterial:J}),a=r.resolve(o,"outline");n(a).toBeInstanceOf(J),n(a.side).toBe(1),n(a.vertexShader).toContain("materialUniforms.outlineThickness"),n(a.vertexAttributes).toEqual(n.arrayContaining([n.objectContaining({name:"skinIndex"}),n.objectContaining({name:"skinWeight"})])),n(a.vertexShader).toContain("rendererSkinPosition"),n(a.vertexShader).toContain("outline_local_position_"),n(a.vertexShader).toContain("outline_local_normal_"),n(a.vertexShader).toContain("sqrt("),n(a.fragmentShader).toContain("outlineAlbedoTexture"),n(a.fragmentShader).toContain("materialUniforms.outlineDarkening"),n(a.uniforms.outlineThickness.value).toBe(.012),n(a.uniforms.outlineDarkening.value).toBe(.65),n(a.uniforms.outlineAlpha.value).toBe(.8),o.userData.outlineParameters.darkening=2,o.userData.outlineParameters.thickness=.02;const i=r.resolve(o,"outline");n(i).toBe(a),n(i.uniforms.outlineThickness.value).toBe(.02),n(i.uniforms.outlineDarkening.value).toBe(1)}),r("declines an albedo outline when the source graph has no albedo output",()=>{const e=new f({color:S(16777215)}),t=new T({WGSLShaderMaterial:J});n(t.resolve(e,"outline")).toBeUndefined()}),r("compiles shader graph scene-light nodes against the renderer light ABI",()=>{const e=X("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:J}).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")}),r("compiles the shader graph world normal without a WGSL matrix inverse",()=>{const e=X("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:J}).resolve(Y(e));n(t?.vertexShader).toContain("transpose(rendererViewMatrix())"),n(t?.vertexShader).not.toContain("nodeInverseMat4")}),r("compiles decal surface rejection without undefined WGSL expressions",()=>{const e=(new U).build(),t=new T({WGSLShaderMaterial:J}).resolve(e);n(t?.fragmentShader).not.toContain("undefined"),n(t?.fragmentShader).toContain("hology_scene_mapTexture")}),r("recompiles an opaque variant when receiveDecals changes",()=>{const e=new f({color:S(16777215)});e.userData.receiveDecals=!0;const t=new T({WGSLShaderMaterial:J},e=>[S(16777215,!1===e.userData.receiveDecals?0:2)]),o=t.resolve(e,"opaque");e.userData.receiveDecals=!1,e.needsUpdate=!0;const r=t.resolve(e,"opaque");n(r).not.toBe(o),n(o.disposed).toBe(!0),n(o.fragmentShader).not.toBe(r.fragmentShader)}),r.each(["node","standard"])("preserves GI reception settings for %s materials across refreshes",e=>{const t="node"===e?new f({color:g({color:v(16777215)})}):new d,o=new T({WGSLShaderMaterial:J});n(o.resolve(t,"opaque")?.userData.receiveGI).toBe(!0),t.userData.receiveGI=!1,t.needsUpdate=!0;const r=o.resolve(t,"opaque");n(r.userData.receiveGI).toBe(!1),n(o.resolveSDFGISurface(r)).toBeDefined(),t.userData.receiveGI=!0,t.needsUpdate=!0;const a=o.resolve(t,"opaque");n(a?.userData.receiveGI).toBe(!0),n(a).not.toBe(r),n(r.disposed).toBe(!0),o.dispose(),t.dispose()}),r("maps Hology elapsed time to the renderer frame clock",()=>{const e=x(N),t=new f({color:S(M(e)),transform:w(e)}),o=new T({WGSLShaderMaterial:J}).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)}),r("compiles the native sprite billboard position without a GLSL fallback",()=>{const e=new f({color:S(16777215),position:I(x("rotation",.25),x("screenSpaceSize",0),x("lockYAxis",1))}),t=new T({WGSLShaderMaterial:J}).resolve(e);n(t).toBeInstanceOf(J),n(t?.vertexShader).toContain("cos("),n(t?.vertexShader).toContain("sin("),n(t?.vertexShader).toContain("materialUniforms.lockYAxis"),n(t?.vertexShader).not.toContain("customFunction_")}),r("shares lens-flare occlusion helpers across all five depth samples",()=>{const e=new k;e.lenseFlare=!0;const t=new f({color:e.output().color}),o=new T({WGSLShaderMaterial:J}).resolve(t);n(o).toBeInstanceOf(J);const r=o?.vertexShader??"",a=r.match(/fn tsg_function_\d+\([^]*?\n}/g)??[];n(a).toHaveLength(2);for(const e of a){const t=e.match(/fn (tsg_function_\d+)/)?.[1];n(t).toBeDefined(),n(r.split(t+"(").length-1).toBe(6),n(e).not.toMatch(/textureSample|materialUniforms|renderer/)}n(r.match(/textureSampleLevel\(/g)).toHaveLength(5),n(t.vertexShader).toMatch(/tsg_function_\d+\(/)}),r("uses the per-instance rotation attribute for instanced sprites",()=>{const e=new f({color:S(16777215),position:I(x("rotation",0),void 0,void 0,!0)}),t=new T({WGSLShaderMaterial:J}).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")}),r("uses per-instance particle opacity in WebGPU fragment shaders",()=>{const e=new f({color:S(v(16777215),R.opacity)}),t=new T({WGSLShaderMaterial:J}).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")}),r("maps THREE.SpriteMaterial to the native sprite shader",()=>{const e=new s(new Uint8Array([255,255,255,255]),1,1,h),t=new c({map:e,color:8429760,opacity:.6,rotation:.25,sizeAttenuation:!1,alphaTest:.2,transparent:!0}),o=new T({WGSLShaderMaterial:J}),r=o.resolve(t);n(r).toBeInstanceOf(J),n(r?.vertexShader).toContain("materialUniforms.rotation"),n(r?.vertexShader).toContain("materialUniforms.screenSpaceSize"),n(r?.fragmentShader).toContain("textureSample("),n(r?.alphaTest).toBe(.2),n(r?.transparent).toBe(!0),n(r?.uniforms.rotation.value).toBe(.25),n(r?.uniforms.screenSpaceSize.value).toBe(1),t.rotation=.75;const a=o.resolve(t);n(a).toBe(r),n(a?.uniforms.rotation.value).toBe(.75)}),r("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 r=new Proxy({},{get:(n,r)=>"string"==typeof r&&e.includes(r)?(o+=1,t.get(r)):Reflect.get(n,r)});b(r),b(r),n(o).toBe(e.length)}),r("adapts a NodeShaderMaterial without replacing its scene material",()=>{const e=x("intensity",.75),t=new f({color:g({color:v(5210052).multiplyScalar(e),roughness:.42,metalness:.18}),transparent:!0,alphaTest:.25}),o=new T({WGSLShaderMaterial:J}),r=o.resolve(t);n(r).toBeInstanceOf(J),n(r).not.toBe(t),n(r?.transparent).toBe(!0),n(r?.alphaTest).toBe(.25),n(r?.fragmentShader).toContain("rendererPointLightForLight"),n(t.isShaderMaterial).toBe(!0),e.value=.25,n(o.resolve(t)).toBe(r),n(r?.uniforms.intensity?.value).toBe(.25)}),r("syncs direct NodeShaderMaterial uniform mutations to the resolved material",()=>{const e=B("trailTint",new u(0,0,0,0)),t=x("hology_particle_energy",1),o=new f({color:e.multiplyScalar(t)}),r=new T({WGSLShaderMaterial:J});o.uniforms.trailTint.value=new u(1,.25,.5,.75),o.uniforms.hology_particle_energy.value=.4;const a=r.resolve(o);n(a?.uniforms.trailTint.value).toEqual(new u(1,.25,.5,.75)),n(a?.uniforms.hology_particle_energy.value).toBe(.4),o.uniforms.trailTint.value.set(.2,.4,.6,.8),o.uniforms.hology_particle_energy.value=.1,n(r.resolve(o)).toBe(a),n(a?.uniforms.trailTint.value).toEqual(new u(.2,.4,.6,.8)),n(a?.uniforms.hology_particle_energy.value).toBe(.1)}),r("only syncs explicitly invalidated uniforms when auto update is disabled",()=>{const e=x("intensity",.75),t=new f({color:S(M(e))});t.uniformValuesAutoUpdate=!1;const o=new T({WGSLShaderMaterial:J}),r=o.resolve(t),a=r.uniformValuesAutoVersion;e.value=.25,n(o.resolve(t)).toBe(r),n(r?.uniforms.intensity.value).toBe(.75),n(r?.uniformValuesAutoVersion).toBe(a),t.uniformValuesNeedsUpdate=!0,n(o.resolve(t)).toBe(r),n(r?.uniforms.intensity.value).toBe(.25),n(r?.uniformValuesAutoVersion).toBe(a+1)}),r("supports explicit invalidation for mapped Three.js materials",()=>{const e=new d({roughness:.8});W(e,!1);const t=new T({WGSLShaderMaterial:J}),o=t.resolve(e);e.roughness=.2,n(t.resolve(e)).toBe(o),n(o?.uniforms.roughness.value).toBe(.8),L(e),n(t.resolve(e)).toBe(o),n(o?.uniforms.roughness.value).toBe(.2)}),r("binds uniforms referenced only by an MRT material output",()=>{const e=x("roughness",.7),t=new f({color:S(5601177),alphaTest:.1});t.outputRoughness=e;const o=new T({WGSLShaderMaterial:J},e=>[e.outputColor,_(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)}),r("compiles a landscape composite built from node material semantic outputs",()=>{const e=new T({WGSLShaderMaterial:J}),t=new f({color:g({color:v(5147450),roughness:.82,metalness:0})}),o=new f({color:g({color:v(7432286),roughness:.6,metalness:.05})}),r=new D;r.layers=[t,o];const a=e.resolve(r.build());n(a).toBeInstanceOf(J),n(a?.fragmentShader).toContain("rendererDirectionalLightForLight"),n(a?.vertexShader).toContain("@location(6) material: vec4<f32>"),n(a?.vertexShader).toContain("@location(8) hole: f32")}),r("allocates the trail's authored vertex attributes",()=>{const e=new T({WGSLShaderMaterial:J}).resolve(z.createBaseMaterial());n(e).toBeInstanceOf(J),n(e?.vertexAttributes).toEqual(n.arrayContaining([n.objectContaining({name:"nodeID",shaderLocation:6,format:"float32"}),n.objectContaining({name:"nodeCenter",shaderLocation:7,format:"float32x3"})]))}),r("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")}),r("compiles the foliage transform and named texture-uniform pattern",()=>{const e=new T({WGSLShaderMaterial:J}),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}),r=e.resolve(o);n(r).toBeInstanceOf(J),n(r?.vertexShader).toContain("rendererClipPosition(_ng_localPosition"),n(r?.depthVertexShader).toContain("materialUniforms.windOffset"),n(r?.depthFragmentShader).toContain("textureSample("),n(r?.depthFragmentShader).toContain("< 0.5) { discard;"),n(r?.pointShadowFragmentShader).toContain("distanceToLight")}),r("rebuilds cached materials when scene-local texture bindings change",()=>{const e=new T({WGSLShaderMaterial:J}),t=new s(new Uint8Array([255,255,255,255]),1,1,h),o=new s(new Uint8Array([32,64,128,255]),1,1,h),r=new f({color:C("map").sample(y.uv),uniforms:{map:{value:t}},alphaTest:.5}),a=e.resolve(r);r.uniforms.map.value=o;const i=e.resolve(r);n(i).not.toBe(a),n(a.disposed).toBe(!0),r.dispose();const l=e.resolve(r);n(l).not.toBe(i),n(i.disposed).toBe(!0)}),r("uses the renderer batch id without Three’s batchingIdTexture",()=>{const e=new T({WGSLShaderMaterial:J}),t=new s(new Uint8Array([255,96,24,255]),1,1,h),o=new f({color:S(m("vp0")),uniforms:{vp0:{value:t}}}),r=e.resolve(o);n(r).toBeInstanceOf(J),n(r?.vertexShader).toContain("rendererBatchingId(input.instanceIndex)")}),r("compiles depth nodes that use fragment-coordinate expressions",()=>{const e=new T({WGSLShaderMaterial:J}),t=new f({color:S(M(j.x,j.y,q))}),o=e.resolve(t);n(o).toBeInstanceOf(J),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")}),r("maps scene color and depth nodes to renderer-owned textures",()=>{const e=new T({WGSLShaderMaterial:J}),t=new f({color:F(j).multiplyScalar(O(j))}),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")}),r("caches distinct material variants for opaque and late draw stages",()=>{const e=new T({WGSLShaderMaterial:J},(e,t)=>"opaque"===t?[S(M(1)),S(M(0))]:void 0),t=new f({color:S(M(1))}),o=e.resolve(t,"opaque"),r=e.resolve(t,"late");n(o).not.toBe(r),n(e.resolve(t,"opaque")).toBe(o),n(e.resolve(t,"late")).toBe(r),n(o?.fragmentShader).toContain("@location(1)"),n(r?.fragmentShader).not.toContain("@location(1)")}),r("reconstructs WebGPU depth positions with inverted NDC Y",()=>{const e=new T({WGSLShaderMaterial:J}),t=new f({color:S(V.add(P.multiplyScalar(0)))}),o=e.resolve(t),r=o.fragmentShader.match(/fn tsg_function_\d+\([^]*?\n}/g)??[];n(r).toHaveLength(1);const a=r[0]??"",s=a.match(/fn (tsg_function_\d+)/)?.[1];n(s).toBeDefined(),n(o.fragmentShader.split(s+"(").length-1).toBe(4),n(a).not.toMatch(/textureSample|renderer|materialUniforms/),n(a).toContain("0.000001");const i=e.resolve(new f({color:S(V)}));n(i?.fragmentShader).toContain(a),n(t.fragmentShader).toMatch(/tsg_function_\d+\(/),n(o.fragmentShader).toContain("vec2<f32>(2.0, -2.0)"),n(o.fragmentShader).toMatch(/-\(float_op_\d+\)/)}),r("emits dither and parallax nodes without GLSL custom functions",()=>{const e=new T({WGSLShaderMaterial:J}),t=new s(new Uint8Array([128,128,128,255]),1,1,h),o=H(y.uv,C("heightMap",t),p(.03)),r=new f({color:_(o.x,o.y,E(M(.2,.4,.8)).z,1)}),a=e.resolve(r);n(a).toBeInstanceOf(J),n(a?.fragmentShader).toContain("textureSampleLevel(heightMapTexture, heightMapSampler"),n(a?.fragmentShader).toMatch(/= -dpdy\(-pom_\d+ViewPosition\)/),n(a?.fragmentShader).toMatch(/= -dpdy\(pom_\d+Uv\)/),n(a?.fragmentShader).not.toContain("customFunction_"),n(a?.fragmentShader).not.toContain("gl_FragCoord")}),r("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:g({color:v(8421504),roughness:.5,metalness:.1})}),o=new T({WGSLShaderMaterial:J});o.setWorldEnvironment({texture:e,intensity:.75});const r=o.resolve(t);n(r.fragmentShader).toContain("envMapTexture"),n(r.uniforms.envMapIntensity?.value).toBe(.75),n(t.envMap).toBeUndefined(),o.setWorldEnvironment({texture:e,intensity:.25});const a=o.resolve(t);n(a).toBe(r),n(a.uniforms.envMapIntensity?.value).toBe(.25),o.setWorldEnvironment(null);const i=o.resolve(t);n(i).not.toBe(r),n(r.disposed).toBe(!0),n(i.fragmentShader).not.toContain("envMapTexture")}),r("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),r=new f({color:g({color:v(8421504),roughness:.5,metalness:.1}),envMap:t}),i=a.fn(()=>o),l=new T({WGSLShaderMaterial:J},void 0,i);l.setWorldEnvironment({texture:e,intensity:.2}),l.resolve(r),n(i).toHaveBeenCalledWith(t),n(r.uniforms.envMap.value).toBe(o),n(r.envMap).toBe(t)})});/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import"reflect-metadata";import*as e from"@dimforge/rapier3d-simd-compat";import{afterEach as t,beforeAll as
|
|
1
|
+
import"reflect-metadata";import*as e from"@dimforge/rapier3d-simd-compat";import{afterEach as t,beforeAll as s,expect as n,test as o,vi as a}from"vitest";import{BatchedMesh as r,BoxGeometry as i,Group as c,InstancedMesh as d,Matrix4 as l,Mesh as w,MeshBasicMaterial as h,Object3D as p,Vector3 as y}from"three";import{AssetMeshInstance as m}from"../scene/asset-resource-loader.js";import{PhysicsSystem as B,RayTestResult as b}from"../gameplay/services/physics/physics-system.js";import{BoxCollisionShape as u}from"../scene/collision/collision-shape.js";a.hoisted(()=>{const e=new Proxy({},{get:(e,t)=>e[t]??a.fn(),set:(e,t,s)=>(e[t]=s,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),a.mock("../gameplay/services/render.js",()=>({ViewController:class{}})),a.mock("../gameplay/services/world.js",()=>({World:class{}})),s(async()=>{await e.init()});const f=[];function j(){const t=Object.create(B.prototype),s=new e.World({x:0,y:0,z:0});return f.push(s),Object.assign(t,{world:s,rapier:e,shapeCacheBall:new Map,colliderObjects:new WeakMap,actorBodies:new Map,bodyActors:new Map,nonFixedBodies:new Set,staticBodies:new Map,staticMeshes:new Map,_raytestDiff:new y,_raytestDirection:new y,invalidateStaticDebug:()=>{}}),{system:t,world:s}}t(()=>{for(const e of f.splice(0))e.free()}),o.each([{src:{type:"actor"}},{_src:{type:"actor"}},{actor:{}}])("scene physics skips actor models with root metadata %j",e=>{const{system:t,world:s}=j(),o=new c,a=new c;Object.assign(a.userData,e);const r=()=>{const e=new m;return e.collisionShapes=[new u(new y(1,2,1))],e.add(new w(new i,new h)),e};a.add(r()),o.add(a);const d=r();d.position.x=3,o.add(d),o.updateMatrixWorld(!0),t.addFromScene(o),s.step(),n(s.colliders.len()).toBe(1),n(t.rayTest(new y(0,3,0),new y(0,-3,0),new b).hasHit).toBe(!1);const l=t.rayTest(new y(3,3,0),new y(3,-3,0),new b);n(l.hasHit).toBe(!0),n(l.object).toBe(d)}),o("sphere casts can exit an initial overlap while still detecting obstacles ahead",()=>{const{system:t,world:s}=j();s.createCollider(e.ColliderDesc.cuboid(2,2,.2).setTranslation(0,0,-.3)),s.createCollider(e.ColliderDesc.cuboid(2,2,.1).setTranslation(0,0,2)),s.step();const o=new y,a=new y(0,0,1),r=t.sphereCast(o,.25,a,3);n(r.hasHit).toBe(!0),n(r.distance).toBeCloseTo(0);const i=t.sphereCast(o,.25,a,3,void 0,{stopAtPenetration:!1});n(i.hasHit).toBe(!0),n(i.distance).toBeCloseTo(1.64,2);const c=t.sphereCast(o,.25,a,1,void 0,{stopAtPenetration:!1});n(c.hasHit).toBe(!1)}),o("ground ray collision groups skip overlapping character capsules",()=>{const{system:t,world:s}=j();s.createCollider(e.ColliderDesc.cuboid(5,.1,5).setTranslation(0,-.1,0));const o=s.createRigidBody(e.RigidBodyDesc.kinematicPositionBased());s.createCollider(e.ColliderDesc.capsule(.6,.25).setTranslation(0,1.05,0).setCollisionGroups(131070),o),s.step();const a=new y(0,1,0),r=new y(0,-.1,0),i=new b;t.rayTest(a,r,i,{resolveActor:!1}),n(i.hitPoint.y).toBeGreaterThan(.1),t.rayTest(a,r,i,{resolveActor:!1,collisionFilter:131070}),n(i.hasHit).toBe(!0),n(i.hitPoint.y).toBeCloseTo(0)}),o("ray hits retain the source object without resolving an actor, and clear it on misses",()=>{const{system:e,world:t}=j(),s=new w(new i,new h),o=new u(new y(1,1,1)),a=e.addShape(void 0,o,s);t.step();const r=new b;e.rayTest(new y(0,2,0),new y(0,-2,0),r,{resolveActor:!1}),n(r.hasHit).toBe(!0),n(r.object).toBe(s),n(r.actor).toBeUndefined(),t.removeCollider(a,!1),t.step(),e.rayTest(new y(0,2,0),new y(0,-2,0),r),n(r.hasHit).toBe(!1),n(r.object).toBeUndefined(),r.hasHit=!0,r.object=s,r.instanceId=3,r.batchId=4,e.rayTest(new y,new y,r),n(r.hasHit).toBe(!1),n(r.object).toBeUndefined(),n(r.instanceId).toBeUndefined(),n(r.batchId).toBeUndefined()}),o("instanced colliders return the rendered mesh and the hit instance index",()=>{const{system:e,world:t}=j(),s=new d(new i,new h,2);s.setMatrixAt(0,new l),s.setMatrixAt(1,(new l).makeTranslation(3,0,0)),e.createForInstancedMesh(s,[new u(new y(1,1,1))]),t.step();const o=e.rayTest(new y(3,2,0),new y(3,-2,0),new b);n(o.hasHit).toBe(!0),n(o.object).toBe(s),n(o.instanceId).toBe(1),n(o.batchId).toBeUndefined()}),o("standalone colliders without a scene object return no object",()=>{const{system:e,world:t}=j();e.addShape(void 0,new u(new y(1,1,1))),t.step();const s=new b;s.object=new p,e.rayTest(new y(0,2,0),new y(0,-2,0),s),n(s.hasHit).toBe(!0),n(s.object).toBeUndefined()}),o("batched colliders return the batch instance ID rather than the temporary physics object",()=>{const{system:e,world:t}=j(),s=new r(2,48,72,new h),o=s.addGeometry(new i),a=s.addInstance(o),c=s.addInstance(o);s.setMatrixAt(a,new l),s.setMatrixAt(c,(new l).makeTranslation(3,0,0)),s.userData.collisionShapes=[[new u(new y(1,1,1))],[new u(new y(1,1,1))]],e.createForInstancedMesh(s,[]),t.step();const d=e.rayTest(new y(3,2,0),new y(3,-2,0),new b);n(d.hasHit).toBe(!0),n(d.object).toBe(s),n(d.batchId).toBe(c),n(d.instanceId).toBeUndefined()}),o("deleted batch slots do not create invisible colliders",()=>{const{system:e,world:t}=j(),s=new r(2,48,72,new h),o=s.addGeometry(new i),a=s.addInstance(o),c=s.addInstance(o);s.setMatrixAt(a,new l),s.setMatrixAt(c,(new l).makeTranslation(3,0,0)),s.userData.collisionShapes=[[new u(new y(1,1,1))],[new u(new y(1,1,1))]],s.deleteInstance(a),e.createForInstancedMesh(s,[]),t.step(),n(t.colliders.len()).toBe(1),n(e.rayTest(new y(0,2,0),new y(0,-2,0),new b).hasHit).toBe(!1);const d=e.rayTest(new y(3,2,0),new y(3,-2,0),new b);n(d.hasHit).toBe(!0),n(d.batchId).toBe(c)});/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as t,it as a,vi as s}from"vitest";import{Subject as i}from"rxjs";import o from"typedi";import{BoxGeometry as n,Color as r,Group as d,Matrix4 as l,Mesh as c,MeshStandardMaterial as m,Raycaster as p,Scene as h,ShaderMaterial as g,Vector4 as u}from"three";s.hoisted(()=>{if("undefined"!=typeof HTMLCanvasElement){const e=new Proxy({},{get:(e,t)=>e[t]??(e[t]=("string"!=typeof t||!t.startsWith("is"))&&s.fn())});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),s.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:s.fn()})),s.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),s.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}}));import{ActorFactory as y}from"../gameplay/actors/factory.js";import{MeshActor as f,ShapeActor as v}from"../gameplay/actors/builtin/mesh-actor.js";import{ModelMeshComponent as w,MeshMaterialSlot as b}from"../gameplay/actors/builtin/components/primitive-mesh-component.js";import{PointLightActor as B,SpotLightActor as M}from"../gameplay/actors/builtin/light-actor.js";import x from"../gameplay/actors/builtin/index.js";import{PhysicsSystem as j}from"../gameplay/services/physics/physics-system.js";import{AssetMeshInstance as H,AssetResourceLoader as A}from"../scene/asset-resource-loader.js";import{BoxCollisionShape as C}from"../scene/collision/collision-shape.js";import{Vector3 as P}from"three";import{SceneMaterializer as S,prepareClassParameters as I,serializeCustomParameter as k,prepareShaderGraphParameters as _}from"../scene/materializer.js";import{MaterialRef as z}from"../scene/material-ref.js";import{customParamValueNeedsAsyncResolution as T,deserializeCustomParamValue as W,deserializeCustomParamValueSync as O}from"../scene/custom-param-deserialize.js";import{SceneDataService as E}from"../scene/scene-data-service.js";import{convertSceneObjectToActor as D}from"../scene/scene-object-actor-conversion.js";import{SerializedParamType as L}from"../scene/model.js";import{extractShaderParameters as q}from"../shader/parameter.js";import{createDefaultShaderGraphDocument as N,buildShaderGraphMaterial as R}from"../shader/graph/index.js";import{BatchedMesh2 as V}from"../scene/batched-mesh-2.js";import{SequencePlayer as U}from"../effects/sequence/sequence-player.js";import{createMaterialSubTrack as F,createPropertyKeyframe as K}from"../effects/sequence/sequence-data.js";let G=0;function J(e=new m,t=!1){const a=new h,r=o.of("primitive-actors-"+G++),d=new y(r,{inEditor:t}),l=new n,p=new C(new P(1,1,1)),g={getMesh:s.fn(async(t,a)=>{const s=new H;return s.add(new c(l,e)),s.collisionShapes=[p],{scene:s,animations:[]}})},u={onUpdate:new i,onCreate:new i,onDelete:new i,getAssets:async()=>[],getAsset:async e=>({id:e,name:"Model",type:"mesh",mesh:{}})},f={addActor:s.fn(),removeActor:s.fn()},v={requestedBackend:"webgl",invalidateShadows:s.fn(),renderer:{shadowMap:{}}},w=new S(a,new E,u,g,v,[],[],d);return w.inEditor=t,r.set(S,w),r.set(j,f),{scene:a,container:r,factory:d,materializer:w,resources:g,assets:u,physics:f,collider:p,material:e,geometry:l,view:v}}async function Q(e,t,a){const s=D(t).object;return a&&(s.actor.innerParams[0].params.renderingMode={type:L.String,value:a}),await e.materializer.materialize({...s,position:s.position??[0,0,0],rotation:s.rotation??[0,0,0],scale:s.scale??[1,1,1]}),await e.materializer.initActorsPostInit(),e.materializer.materializedActors.get(t.id)}e("built-in primitive actors",()=>{a("deserializes material asset identities without resolving materials",async()=>{const e=J(),a=s.spyOn(e.assets,"getAsset"),i={type:L.MaterialRef,value:"stone"};t(T(i)).toBe(!1),t(O(i)).toEqual(new z("stone")),t(await W(i,{material:s.fn(()=>{throw new Error("Unexpected material load")})})).toEqual(new z("stone"));const o=await I({material:{type:L.MaterialRef,value:"stone"}},null,e.assets,e.resources,void 0,void 0,void 0,void 0,void 0,new b);t(o.material).toEqual(new z("stone")),t(k(z,o.material)).toBe("stone"),t(a).not.toHaveBeenCalled()}),a("resolves a shape material only while preparing its visual and shares the prepared template",async()=>{const e=J();Object.assign(e.view,{csm:{setupMaterial:s.fn()}});const a=e.assets.getAsset;s.spyOn(e.assets,"getAsset").mockImplementation(async e=>"shape-stone"===e?{id:e,name:"Stone",type:"material",material:{type:"phong",params:{color:"#887766"}}}:a(e));const i={id:"stone-a",name:"Stone",type:"shape_mesh",shape:"box",materialAssignments:[{name:"Default",color:"#aaaaaa",materialId:"shape-stone"}]},o=await Q(e,i),n=await Q(e,{...i,id:"stone-b"});t(o.mesh.material).toEqual(new z("shape-stone")),t(o.mesh.renderHandle.materials[0].material.userData.assetId).toBe("shape-stone"),t(o.mesh.renderHandle.materials[0].material).toBe(n.mesh.renderHandle.materials[0].material),o.disposed.next(!0),n.disposed.next(!0)}),a("registers and cleans up mesh and shape collision in the editor for debugging and picking",async()=>{const e=J(void 0,!0),a=await Q(e,{id:"editor-mesh",name:"Window",type:"asset_mesh",assetId:"model",scale:[.01,.01,.01],collisionDetection:!0}),s=await Q(e,{id:"editor-shape",name:"Box",type:"shape_mesh",shape:"box",collisionDetection:!0});t(a.mesh.renderHandle.representation).toBe("dedicated"),t(s.mesh.renderHandle.representation).toBe("dedicated"),t(e.physics.addActor).toHaveBeenCalledWith(a,a.mesh.renderHandle.collisionShapes,t.objectContaining({type:2})),t(e.physics.addActor).toHaveBeenCalledWith(s,s.mesh.renderHandle.collisionShapes,t.objectContaining({type:2}));const i=await Q(e,{id:"editor-disabled",name:"Window",type:"asset_mesh",assetId:"model",collisionDetection:!1});t(e.physics.addActor).toHaveBeenCalledTimes(2),a.disposed.next(!0),s.disposed.next(!0),i.disposed.next(!0),t(e.physics.removeActor).toHaveBeenCalledWith(a),t(e.physics.removeActor).toHaveBeenCalledWith(s)}),a("creates actor-owned collision when enabled on an actor whose model asset disables collision",async()=>{const e=J(),a={id:"model",name:"Window",type:"mesh",fileKey:"window.fbx",fileFormat:"fbx",collisionDetection:!1};s.spyOn(e.assets,"getAsset").mockResolvedValue({...a,mesh:{}});const i=new A;i.setDataDir("");const o=new d;o.add(new c(e.geometry,e.material)),s.spyOn(i,"loadMesh").mockResolvedValue({scene:o,animations:[]}),e.resources.getMesh.mockImplementation((e,t)=>i.getMesh(e,t));const n=await Q(e,{id:"window",name:"Window",type:"asset_mesh",assetId:"model",scale:[.01,.01,.01],collisionDetection:!0});t(n.mesh.renderHandle.collisionShapes).toHaveLength(1),t(e.physics.addActor).toHaveBeenCalledWith(n,n.mesh.renderHandle.collisionShapes,t.objectContaining({type:2})),t(a.collisionDetection).toBe(!1);const r=await Q(e,{id:"disabled-window",name:"Window",type:"asset_mesh",assetId:"model",collisionDetection:!1});t(r.mesh.renderHandle.collisionShapes).toHaveLength(0),t(e.physics.addActor).toHaveBeenCalledTimes(1),n.disposed.next(!0),r.disposed.next(!0)}),a("registers authored static batches, keeps material overrides live, and forces physics-driven actors movable",async()=>{const e=J();e.view.requestedBackend="webgpu",e.view.initializeBackend=s.fn(async()=>{});const a=[];e.view.registerExperimentalStaticDrawSet=s.fn(e=>{e.visible=!1;const t=Object.assign(s.fn(()=>{e.visible=!0}),{updateTransforms:s.fn()});return a.push(t),t});const i={id:"static-a",name:"",type:"asset_mesh",assetId:"model"},o=await Q(e,i),n=await Q(e,{...i,id:"static-b"}),r=D({...i,id:"dynamic",physics:{type:1}}).object;r.actor.innerParams[0].params.mobility={type:L.String,value:"static"},await e.materializer.materialize({...r,position:[0,0,0],rotation:[0,0,0],scale:[1,1,1]}),await e.materializer.initActorsPostInit();const d=e.materializer.materializedActors.get("dynamic");e.materializer.actorMeshRenderer.finalizeStaticPages(),t(a).toHaveLength(1),t(e.view.registerExperimentalStaticDrawSet).toHaveBeenCalledWith(t.any(V),t.any(String),!1,"throw",!0);const c=e.scene.children.find(e=>e.isBatchedMesh),m=new l;o.position.x=9,e.scene.updateMatrixWorld(!0),c.getMatrixAt(0,m),t(m.elements[12]).toBe(0),o.mesh.updateTransform(),e.scene.updateMatrixWorld(!0),c.getMatrixAt(0,m),t(m.elements[12]).toBe(9),t(a[0].updateTransforms).toHaveBeenCalledOnce(),t(o.mesh.setMaterialParameter("roughness",.25)).toBe(!0),t(o.mesh.getMaterialParameter("roughness")).toBe(.25),t(n.mesh.getMaterialParameter("roughness")).toBe(1),t(a[0]).not.toHaveBeenCalled(),d.position.x=10,e.scene.updateMatrixWorld(!0);e.scene.children.filter(e=>e.isBatchedMesh)[1].getMatrixAt(0,m),t(m.elements[12]).toBe(10),n.disposed.next(!0),e.scene.updateMatrixWorld(!0),t(a[0].updateTransforms).toHaveBeenCalledTimes(2),o.mesh.setMobility("movable"),t(a[0]).toHaveBeenCalledOnce(),o.disposed.next(!0),d.disposed.next(!0)}),a("registers each actor and materializes converted meshes into shared draws with actor-owned collision",async()=>{t(x).toMatchObject({Mesh:f,Shape:v,PointLight:B,SpotLight:M});const e=J(),a={id:"a",name:"Mesh",type:"asset_mesh",assetId:"model",physics:{type:1,mass:3,density:2}},s=await Q(e,a),i=await Q(e,{...a,id:"b"});t(s).toBeInstanceOf(f),t(s.mesh.renderHandle.representation).toBe("batched"),t(i.mesh.renderHandle.representation).toBe("batched"),t(s.object.children).toHaveLength(0),t(e.resources.getMesh).toHaveBeenCalledTimes(1),t(e.physics.addActor).toHaveBeenCalledWith(s,[e.collider],t.objectContaining({mass:3,density:2,type:1})),s.disposed.next(!0),t(e.physics.removeActor).toHaveBeenCalledWith(s),t(e.scene.children.filter(e=>e.isBatchedMesh)).toHaveLength(1),i.disposed.next(!0),t(e.scene.children.filter(e=>e.isBatchedMesh)).toHaveLength(0)}),a("round-trips concrete shape definitions and creates the authored collider",async()=>{const e=J(),a=await Q(e,{id:"shape",name:"Stairs",type:"shape_mesh",shape:"stairs",shapeParams:{steps:{type:L.Number,value:7},height:{type:L.Number,value:.8}}});t(a.mesh.geometry.shape).toBe("stairs"),t(a.mesh.geometry.steps).toBe(7),t(a.mesh.geometry.height).toBe(.8),t(a.mesh.renderHandle.collisionShapes).toHaveLength(1),t(e.physics.addActor).toHaveBeenCalled(),a.disposed.next(!0)}),a("applies converted light parameters and runtime changes without ticking",async()=>{const e=J(),a=await Q(e,{id:"spot",name:"Spot",type:"light",light:{type:"spot",spot:{color:"#123456",intensity:12,distance:9,decay:2,angle:.4,penumbra:.6,castShadow:!1,volumetricIntensity:.3}}});t(a.light.object.intensity).toBe(12),t(a.light.object.angle).toBe(.4),t(a.light.object.color.getHexString()).toBe("123456"),t(a.light.object.target.position.toArray()).toEqual([0,-1,0]),a.light.intensity=0,a.light.color=new r("red"),a.light.color.set("blue"),t(a.light.object.intensity).toBe(0),t(a.light.object.color.getHexString()).toBe("0000ff");const i=s.spyOn(a.light.object,"dispose");a.disposed.next(!0),t(i).toHaveBeenCalledOnce()}),a("keeps inherited authored mesh parameters available for the editor",async()=>{const e=J(),a=await e.factory.create(f,void 0,void 0,!0),s=q(w);t(s.map(e=>e.name)).toEqual(t.arrayContaining(["model","materials","collision","castShadow","bodyType","density"]));const i=await I({castShadow:{type:L.Boolean,value:!1,override:!0}},null,e.assets,{},void 0,void 0,void 0,void 0,void 0,a.mesh);t(i.castShadow).toBe(!1)}),a("automatically discovers uniforms, isolates values, and resets reused slots",async()=>{const e=N();e.shadingModel="unlit",e.parameters=[{name:"color",type:"color",defaultValue:"#ffffff"},{name:"gain",type:"float",defaultValue:1}],e.nodes=[{id:"color",kind:"input.parameter",params:{parameter:"color"}},{id:"gain",kind:"input.parameter",params:{parameter:"gain"}}],e.outputs={color:{input:{nodeId:"color",port:"value"}},opacity:{input:{nodeId:"gain",port:"value"}}};const a=R(e,{parameters:await _({},e,{},{})}),s=J(a),i={id:"a",name:"A",type:"asset_mesh",assetId:"model"},o=await Q(s,i),n=await Q(s,{...i,id:"b"});t(o.mesh.renderHandle.representation).toBe("batched");const d=s.scene.children.find(e=>e.isBatchedMesh);t(s.scene.children.filter(e=>e.isBatchedMesh)).toHaveLength(1),t(o.mesh.setMaterialParameter("color",new r("red"))).toBe(!0),t(o.mesh.setMaterialParameter("pu_gain",.25)).toBe(!0),t(o.mesh.getMaterialParameter("gain")).toBe(.25),t(n.mesh.getMaterialParameter("gain")).toBe(1),t(o.mesh.setMaterialParameter("misspelled",3)).toBe(!1),t(a.uniforms.pu_gain.value).toBe(1);const l=d.definition.bindings.get("pu_color"),c=d.getUniformAt(l.storage,0,new u);t(c.toArray().slice(0,3)).toEqual([1,0,0]),a.userData.assetId="test-material";const m=F({materialAssetId:"test-material",parameterName:"gain",uniformName:"pu_gain",uniformType:L.FloatNode,valueType:L.Number});m.keyframes=[K(0,{type:L.Number,value:0}),K(1,{type:L.Number,value:1})];const p=new U;p.evaluateParameterSubTrack(m,o,.5),t(o.mesh.getMaterialParameter("gain")).toBeCloseTo(.5),t(n.mesh.getMaterialParameter("gain")).toBe(1),p.stop(),t(o.mesh.getMaterialParameter("gain")).toBe(.25);const h=F({materialAssetId:"test-material",parameterName:"color",uniformName:"pu_color",uniformType:L.RgbNode,valueType:L.Color});h.keyframes=[K(0,{type:L.Color,value:"#0000ff"})],p.evaluateParameterSubTrack(h,o.object,0),p.stop();const g=o.mesh.getMaterialParameter("color");t(g.toArray()).toEqual([1,0,0]);const y=o.mesh.renderHandle.materials[0];o.disposed.next(!0);const f=await Q(s,{...i,id:"c"});t(f.mesh.getMaterialParameter("gain")).toBe(1),t(y.setParameter("gain",.9)).toBe(!1),n.disposed.next(!0),f.disposed.next(!0)}),a("preserves packed scalar neighbours and supports explicit instancing",async()=>{const e=J(),a={id:"a",name:"A",type:"asset_mesh",assetId:"model"},s=await Q(e,a);t(s.mesh.setMaterialParameter("roughness",.2)).toBe(!0),t(s.mesh.setMaterialParameter("metalness",.7)).toBe(!0);const i=e.scene.children.find(e=>e.isBatchedMesh),o=i.definition.bindings,n=o.get("roughness"),r=o.get("metalness");t(i.getUniformAt(n.storage,0,new u).getComponent(n.component)).toBeCloseTo(.2),t(i.getUniformAt(r.storage,0,new u).getComponent(r.component)).toBeCloseTo(.7);const d=await Q(e,{...a,id:"b"},"instanced");t(d.mesh.renderHandle.representation).toBe("instanced"),t(d.mesh.setMaterialParameter("roughness",.1)).toBe(!1),s.disposed.next(!0),d.disposed.next(!0)}),a("batches different geometries sharing a material and preserves movement, visibility and picking",async()=>{const e=J(),a=e.materializer.createActorMesh(new d,{id:"detached",name:"",type:"asset_mesh",assetId:"model"},[]),s=new d,i=new d;e.scene.add(s,i),i.position.x=5;const o=await e.materializer.createActorMesh(s,{id:"a",name:"",type:"asset_mesh",assetId:"model"},[]),r=new n(2,2,2);e.resources.getMesh.mockImplementation(async()=>{const t=new H;return t.add(new c(r,e.material)),t.collisionShapes=[e.collider],{scene:t,animations:[]}});const l=await e.materializer.createActorMesh(i,{id:"b",name:"",type:"asset_mesh",assetId:"other-model"},[]);t(e.scene.children.filter(e=>e.isBatchedMesh)).toHaveLength(1),e.scene.updateMatrixWorld(!0);const m=new p(new P(0,0,4),new P(0,0,-1));t(m.intersectObject(e.scene)[0].object).toBe(s),s.position.x=2,e.scene.updateMatrixWorld(!0),t(m.intersectObject(e.scene)).toHaveLength(0),m.ray.origin.x=2,t(m.intersectObject(e.scene)[0].object).toBe(s),o.setVisible(!1),e.scene.updateMatrixWorld(!0),t(m.intersectObject(e.scene)).toHaveLength(0),o.dispose(),l.dispose(),(await a).dispose()}),a("falls back for unsupported shaders and keeps dedicated numeric and boolean overrides isolated",async()=>{const e=new g({uniforms:{pu_enabled:{value:!0},pu_gain:{value:1}}}),a=J(e),i={id:"a",name:"",type:"asset_mesh",assetId:"model"},o=await Q(a,i),n=await Q(a,{...i,id:"b"});t(o.mesh.renderHandle.representation).toBe("dedicated"),t(o.mesh.setMaterialParameter("enabled",!1)).toBe(!0),t(o.mesh.setMaterialParameter("gain",.4)).toBe(!0),t(o.mesh.getMaterialParameter("enabled")).toBe(!1),t(n.mesh.getMaterialParameter("enabled")).toBe(!0),t(e.uniforms.pu_enabled.value).toBe(!0);const r=o.object.children[0].children[0].material,d=s.spyOn(r,"dispose");o.disposed.next(!0),t(d).toHaveBeenCalledOnce(),n.disposed.next(!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{describe as e,expect as t,it as n,vi as a}from"vitest";import{RenderingHost as o}from"../gameplay/rendering-host.js";function s(){let e;return{simulationTime:12,webGpuDevice:{label:"device"},loop:a.fn(e=>e(.25)),stop:a.fn(),stopAnimationLoop:a.fn(),suspendPresentation:a.fn(),activatePresentation:a.fn(),registerWebGpuPresentationExtension:a.fn(t=>(e=t,a.fn())),get extension(){return e}}}e("RenderingHost",()=>{n("installs the input sampler on each active view separately from gameplay frames",()=>{const e=s(),n=s(),i=new o(e,!1,()=>n),r=a.fn(),l=a.fn();i.start(r,l),t(e.loop).toHaveBeenCalledWith(t.any(Function),!1,l),i.activate(i.createView()),t(n.loop).toHaveBeenCalledWith(t.any(Function),!1,l)}),n("owns frame scheduling and preserves a caller-owned view on shutdown",()=>{const e=s(),n=new o(e,!1,()=>s()),i=a.fn();n.start(i),n.shutdown(),n.shutdown(),t(i).toHaveBeenCalledWith(.25),t(e.suspendPresentation).toHaveBeenCalledOnce(),t(e.stop).not.toHaveBeenCalled()}),n("disposes a host-owned view",()=>{const e=s();new o(e,!0,()=>s()).shutdown(),t(e.stop).toHaveBeenCalledOnce()}),n("creates and switches distinct scene views",()=>{const e=s(),n=s(),a=new o(e,!1,()=>n);a.start(()=>{});const i=a.createView();t(i).toBe(n),t(n.suspendPresentation).toHaveBeenCalledOnce(),a.activate(i),t(e.suspendPresentation).toHaveBeenCalledOnce(),t(n.activatePresentation).toHaveBeenCalledOnce(),a.activate(e),a.releaseView(i),t(n.stop).toHaveBeenCalledOnce()}),n("adds a persistent native-resolution presentation pass with frame timing",()=>{const e=s(),n=new o(e,!1,()=>s());n.start(()=>{});const i=a.fn();n.registerStage(i);const
|
|
1
|
+
import{describe as e,expect as t,it as n,vi as a}from"vitest";import{RenderingHost as o}from"../gameplay/rendering-host.js";function s(){let e;return{simulationTime:12,webGpuDevice:{label:"device"},loop:a.fn(e=>e(.25)),stop:a.fn(),stopAnimationLoop:a.fn(),suspendPresentation:a.fn(),activatePresentation:a.fn(),registerWebGpuPresentationExtension:a.fn(t=>(e=t,a.fn())),get extension(){return e}}}e("RenderingHost",()=>{n("installs the input sampler on each active view separately from gameplay frames",()=>{const e=s(),n=s(),i=new o(e,!1,()=>n),r=a.fn(),l=a.fn();i.start(r,l),t(e.loop).toHaveBeenCalledWith(t.any(Function),!1,l),i.activate(i.createView()),t(n.loop).toHaveBeenCalledWith(t.any(Function),!1,l)}),n("owns frame scheduling and preserves a caller-owned view on shutdown",()=>{const e=s(),n=new o(e,!1,()=>s()),i=a.fn();n.start(i),n.shutdown(),n.shutdown(),t(i).toHaveBeenCalledWith(.25),t(e.suspendPresentation).toHaveBeenCalledOnce(),t(e.stop).not.toHaveBeenCalled()}),n("disposes a host-owned view",()=>{const e=s();new o(e,!0,()=>s()).shutdown(),t(e.stop).toHaveBeenCalledOnce()}),n("creates and switches distinct scene views",()=>{const e=s(),n=s(),a=new o(e,!1,()=>n);a.start(()=>{});const i=a.createView();t(i).toBe(n),t(n.suspendPresentation).toHaveBeenCalledOnce(),a.activate(i),t(e.suspendPresentation).toHaveBeenCalledOnce(),t(n.activatePresentation).toHaveBeenCalledOnce(),a.activate(e),a.releaseView(i),t(n.stop).toHaveBeenCalledOnce()}),n("adds a persistent native-resolution presentation pass with frame timing",()=>{const e=s(),n=new o(e,!1,()=>s());n.start(()=>{});const i=a.fn();t(n.hasRenderStages).toBe(!1);const r=n.registerStage(i);t(n.hasRenderStages).toBe(!0);const l={},d={device:{},encoder:{},targetView:l,targetFormat:"bgra8unorm",presentationWidth:1920,presentationHeight:1080};e.extension(d),t(i).toHaveBeenCalledWith(t.objectContaining({deltaTime:.25,elapsedTime:12,targetView:l,targetFormat:"bgra8unorm",presentationWidth:1920})),r(),t(n.hasRenderStages).toBe(!1)})});/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{afterEach as e,expect as a,test as t,vi as s}from"vitest";import{RuntimeBundledBackendService as n}from"../scene/runtime-bundled-backend-service.js";import{serializeStorageJson as o}from"../scene/storage/packed-json.js";e(()=>s.unstubAllGlobals()),t("only decodes requested scenes and reuses their decoded data",async()=>{const e=[{id:"mesh",vertexMaterials:Array.from({length:100},(e,a)=>({i:a,w:[1,0,0]}))}],t=JSON.parse(o({id:"selected",data:e}));s.stubGlobal("fetch",s.fn().mockResolvedValue({json:async()=>({"scene-blob":[t,{id:"unused",data:[{vertexMaterials:"@hology/packed-json:1:gzip:base64:invalid"}]}]})}));const d=new n("hology_data.json");await d.preloadData();const c=await d.getSceneData("selected");a(c).toEqual(e),a(await d.getSceneData("selected")).toBe(c),await a(d.getSceneData("unused")).rejects.toThrow("Failed to decode packed")}),t("loads legacy string scenes and invalidates decoded data on bundle reload",async()=>{let e=[{id:"first"}];s.stubGlobal("fetch",s.fn().mockImplementation(async()=>({json:async()=>({"scene-blob":[{id:"scene",data:JSON.stringify(e)}]})})));const t=new n("hology_data.json");await t.preloadData(),a(await t.getSceneData("scene")).toEqual(e),e=[{id:"second"}],await t.preloadData(),a(await t.getSceneData("scene")).toEqual(e)});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as t,it as a}from"vitest";import{convertSceneObjectToActor as o}from"../scene/scene-object-actor-conversion.js";import{SerializedParamType as s}from"../scene/model.js";e("scene object actor conversion",()=>{a("preserves identity, transforms and children, and maps model/material/physics settings",()=>{const e={id:"door",name:"Door",type:"asset_mesh",assetId:"model",position:[1,2,3],rotation:[.2,.3,.4,"ZYX"],scale:[2,3,4],enabled:!1,castShadow:!1,receiveShadow:!0,hidden:!0,collisionDetection:!1,physics:{type:1,mass:4,friction:.7,density:2,restitution:.3},children:[{id:"child",name:"Child",type:"group"}],materialAssignments:[{name:"Metal",color:"#fff",materialId:"material"}]},a=JSON.stringify(e),{object:r,error:i}=o(e);t(i).toBeUndefined(),t(r).toMatchObject({id:"door",name:"Door",type:"actor",position:[1,2,3],rotation:[.2,.3,.4,"ZYX"],scale:[2,3,4],enabled:!1}),t(r.children).toBe(e.children),t(r.assetId).toBeUndefined(),t(r.physics).toBeUndefined(),t(r.actor.type).toBe("Mesh"),t(r.actor.innerParams[0]).toMatchObject({path:["mesh"],params:{model:{type:s.ModelRef,value:"model"},castShadow:{value:!1,override:!0},collision:{value:!1},mass:{value:4},density:{value:2},materials:{type:s.Array,elementStruct:"hology.mesh.materialSlot",value:[{struct:"hology.mesh.materialSlot",value:{material:{value:"material"}}}]}}}),t(JSON.stringify(e)).toBe(a)}),a("preserves shape parameters and collider-only mode",()=>{const e=o({id:"shape",name:"Stairs",type:"shape_mesh",shape:"stairs",collider:!0,shapeParams:{steps:{type:s.Number,value:7},floating:{type:s.Boolean,value:!0}}});t(e.object.actor.type).toBe("Shape"),t(e.object.actor.innerParams[0].params).toMatchObject({geometry:{type:s.Struct,struct:"hology.shape.stairs",value:{steps:{value:7},floating:{value:!0}}},collider:{value:!0}})}),a("converts the shape default slot into a single material reference",()=>{const e=o({id:"box",name:"Box",type:"shape_mesh",shape:"box",materialAssignments:[{name:"Default",color:"#aaaaaa",materialId:"stone"}]}).object.actor.innerParams[0].params;t(e.material).toMatchObject({type:s.MaterialRef,value:"stone"}),t(e.materials).toBeUndefined()}),a("maps spotlight projection and all light settings",()=>{const e=o({id:"light",name:"Spot",type:"light",light:{type:"spot",spot:{color:"#123456",intensity:20,distance:6,decay:2,angle:.5,penumbra:.3,castShadow:!1,volumetricIntensity:.2,mapTextureId:"cookie"}}});t(e.object.actor).toMatchObject({type:"SpotLight",innerParams:[{path:["light"],params:{color:{type:s.Color,value:"#123456"},map:{type:s.Texture,value:"cookie"},angle:{value:.5},penumbra:{value:.3},volumetricIntensity:{value:.2},castShadow:{value:!1}}}]})}),a("reports lossy scatter conversion and blocks unsupported authored geometry",()=>{const e={id:"mesh",name:"Mesh",type:"asset_mesh",assetId:"model",surfaceScatter:{meshes:[{}]}};t(o(e).warnings).toEqual(["Surface scatter will be removed."]),t(o({...e,latticeDeformation:{}}).object).toBeUndefined(),t(o({...e,vertexMaterials:[{}]}).object).toBeUndefined(),t(o({id:"landscape",name:"",type:"shape_mesh",shape:"landscape"}).error).toBeDefined()})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as 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
|
+
*/
|