From 323d08daa8fff29401aa271825fd27cc0b2ea594 Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Thu, 2 Apr 2026 19:37:14 +0200 Subject: [PATCH 01/21] feat: add support of selection outliner --- .vscode/settings.json | 10 ++-- editor/src/editor/layout/preview.tsx | 88 ++++++++-------------------- 2 files changed, 28 insertions(+), 70 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 890c4ed91..babebec02 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,10 +16,8 @@ "source.fixAll.eslint": "always" }, "files.insertFinalNewline": true, - "javascript.format.semicolons": "insert", - "typescript.format.semicolons": "insert", - "typescript.preferences.quoteStyle": "double", - "javascript.preferences.quoteStyle": "double", + "js/ts.format.semicolons": "insert", + "js/ts.preferences.quoteStyle": "double", "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, @@ -35,6 +33,6 @@ "[jsonc]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, - "typescript.preferences.importModuleSpecifier": "project-relative", - "typescript.preferences.autoImportFileExcludePatterns": ["**/export.ts"] + "js/ts.preferences.importModuleSpecifier": "project-relative", + "js/ts.preferences.autoImportFileExcludePatterns": ["**/export.ts"] } diff --git a/editor/src/editor/layout/preview.tsx b/editor/src/editor/layout/preview.tsx index fbe324b93..2da831671 100644 --- a/editor/src/editor/layout/preview.tsx +++ b/editor/src/editor/layout/preview.tsx @@ -16,7 +16,6 @@ import { AbstractMesh, Animation, Camera, - Color3, CubicEase, EasingFunction, Engine, @@ -34,6 +33,7 @@ import { Sprite, Color4, BoundingBox, + SelectionOutlineLayer, } from "babylonjs"; import { Button } from "../../ui/shadcn/ui/button"; @@ -60,7 +60,7 @@ import { createSceneLink, getRootSceneLink } from "../../tools/scene/scene-link" import { UniqueNumber, waitNextAnimationFrame, waitUntil } from "../../tools/tools"; import { isSprite, isSpriteManagerNode, isSpriteMapNode } from "../../tools/guards/sprites"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "../../ui/shadcn/ui/dropdown-menu"; -import { isAbstractMesh, isAnyTransformNode, isCamera, isCollisionInstancedMesh, isCollisionMesh, isInstancedMesh, isLight, isMesh, isNode } from "../../tools/guards/nodes"; +import { isAbstractMesh, isAnyTransformNode, isCamera, isCollisionInstancedMesh, isCollisionMesh, isLight, isNode } from "../../tools/guards/nodes"; import { EditorCamera } from "../nodes/camera"; @@ -131,39 +131,39 @@ export class EditorPreview extends Component { - if (lod.mesh) { - meshes.push(lod.mesh); - } - }); - } - - meshes.forEach((mesh) => { - Tween.create(mesh, 0.1, { - overlayAlpha: 0.5, - overlayColor: Color3.Black(), - onStart: () => (mesh!.renderOverlay = true), - }); - }); - } - if (isSprite(pickedObject)) { pickedObject.overrideColor ??= new Color4(1, 1, 1, 1); - Tween.create(pickedObject, 0.1, { overrideColor: new Color4(0.5, 0.5, 0.5, 1.0), }); + } else { + this.selectionOutlineLayer.addSelection(pickedObject); } } @@ -834,38 +819,13 @@ export class EditorPreview extends Component { - if (lod.mesh) { - meshes.push(lod.mesh); - } - }); - } - - meshes.forEach((mesh) => { - Tween.killTweensOf(mesh); - - mesh.overlayAlpha ??= 0; - mesh.overlayColor ??= Color3.Black(); - - Tween.create(mesh, 0.1, { - overlayAlpha: 0, - overlayColor: Color3.Black(), - onStart: () => (mesh.renderOverlay = true), - }); - }); - } - if (isSprite(objectUnderPointer)) { Tween.killTweensOf(objectUnderPointer); - Tween.create(objectUnderPointer, 0.1, { overrideColor: new Color4(1.0, 1.0, 1.0, 1.0), }); + } else { + this.selectionOutlineLayer.clearSelection(); } } } From 1f7ca335129e2c90ede11d8e7e42b241880c547a Mon Sep 17 00:00:00 2001 From: Cesharpe Date: Fri, 3 Apr 2026 19:25:20 +0200 Subject: [PATCH 02/21] feat: add support of HDR texture for environment texture and outline only selected mesh when available. --- editor/src/editor/layout/assets-browser.tsx | 1 + .../assets-browser/viewers/env-viewer.tsx | 31 ++++++++++----- .../layout/inspector/fields/texture.tsx | 39 ++++++++++++------- .../src/editor/layout/inspector/mesh/mesh.tsx | 4 ++ editor/src/editor/layout/preview.tsx | 13 +++++-- .../editor/layout/preview/import/import.ts | 3 +- editor/src/project/load/scene.ts | 6 +-- editor/src/tools/guards/texture.ts | 10 ++++- 8 files changed, 74 insertions(+), 33 deletions(-) diff --git a/editor/src/editor/layout/assets-browser.tsx b/editor/src/editor/layout/assets-browser.tsx index ec35e3a69..48c189d94 100644 --- a/editor/src/editor/layout/assets-browser.tsx +++ b/editor/src/editor/layout/assets-browser.tsx @@ -1452,6 +1452,7 @@ export class EditorAssetsBrowser extends Component void; + onChange?: (texture: Texture | CubeTexture | ColorGradingTexture | HDRCubeTexture | null) => void; } export interface IEditorInspectorTextureFieldState { @@ -74,8 +74,8 @@ export class EditorInspectorTextureField extends Component )} - {isCubeTexture(texture) && ( + {(isCubeTexture(texture) || isHDRCubeTexture(texture)) && ( <> {isCubeTexture(this.props.object[this.props.property]) ? ( + ) : isHDRCubeTexture(this.props.object[this.props.property]) ? ( + ) : isColorGradingTexture(this.props.object[this.props.property]) ? ( ) : extname(textureUrl).toLowerCase() === ".exr" ? ( @@ -258,7 +260,7 @@ export class EditorInspectorTextureField extends Component <> - {isCubeTexture(this.props.object[this.props.property]) + {isCubeTexture(this.props.object[this.props.property]) || isHDRCubeTexture(this.props.object[this.props.property]) ? this._getCubeTextureInspector() : isColorGradingTexture(this.props.object[this.props.property]) ? this._getColorGradingTextureInspector() @@ -274,8 +276,8 @@ export class EditorInspectorTextureField extends Component { - const texture = this.props.object[this.props.property] as Texture | CubeTexture | null | undefined; + const texture = this.props.object[this.props.property] as Texture | CubeTexture | HDRCubeTexture | null | undefined; if (!texture?.url || extname(texture.url).toLowerCase() === ".exr") { return; } @@ -685,11 +687,20 @@ export class EditorInspectorTextureField extends Component { + if (mesh.geometry) { + mesh.refreshBoundingInfo({ + applyMorph: true, + applySkeleton: true, + }); + } + }); + const pickingInfo = this._getPickingInfo(this.scene.pointerX, this.scene.pointerY); let effectivePickedObject = (pickingInfo.pickedSprite ?? pickingInfo.pickedMesh?._masterMesh ?? pickingInfo.pickedMesh) as Node; @@ -810,8 +819,6 @@ export class EditorPreview extends Component(texture: T, noCheckInvertY?: boolean): T { +export function configureImportedTexture(texture: T, noCheckInvertY?: boolean): T { if (isAbsolute(texture.name)) { if (!noCheckInvertY && isTexture(texture) && !texture.invertY && !texture._buffer) { texture._invertY = true; diff --git a/editor/src/project/load/scene.ts b/editor/src/project/load/scene.ts index 95c6a4ef4..de2770948 100644 --- a/editor/src/project/load/scene.ts +++ b/editor/src/project/load/scene.ts @@ -21,12 +21,12 @@ import { iblShadowsRenderingPipelineCameraConfigurations, parseIblShadowsRenderi import { createDirectoryIfNotExist } from "../../tools/fs"; import { createSceneLink } from "../../tools/scene/scene-link"; -import { isCubeTexture, isTexture } from "../../tools/guards/texture"; import { updateIblShadowsRenderPipeline } from "../../tools/light/ibl"; import { forceCompileAllSceneMaterials } from "../../tools/scene/materials"; import { IAssetCache, loadSavedAssetsCache } from "../../tools/assets/cache"; import { checkProjectCachedCompressedTextures } from "../../tools/assets/ktx"; import { isAbstractMesh, isEditorCamera, isMesh } from "../../tools/guards/nodes"; +import { isCubeTexture, isHDRCubeTexture, isTexture } from "../../tools/guards/texture"; import { updateAllLights, updatePointLightShadowMapRenderListPredicate } from "../../tools/light/shadows"; import { registerTextureParser } from "./texture"; @@ -222,7 +222,7 @@ export async function loadScene(editor: Editor, projectPath: string, scenePath: scene.environmentTexture = Texture.Parse(environmentTexture, scene, join(projectPath, "/")); - if (isCubeTexture(scene.environmentTexture)) { + if (isCubeTexture(scene.environmentTexture) || isHDRCubeTexture(scene.environmentTexture)) { scene.environmentTexture.url = join(projectPath, scene.environmentTexture.name); } } @@ -287,7 +287,7 @@ export async function loadScene(editor: Editor, projectPath: string, scenePath: // Configure textures urls scene.textures.forEach((texture) => { - if (isTexture(texture) || isCubeTexture(texture)) { + if (isTexture(texture) || isCubeTexture(texture) || isHDRCubeTexture(texture)) { texture.url = texture.name; } }); diff --git a/editor/src/tools/guards/texture.ts b/editor/src/tools/guards/texture.ts index afc15bae3..804cd58dc 100644 --- a/editor/src/tools/guards/texture.ts +++ b/editor/src/tools/guards/texture.ts @@ -1,5 +1,5 @@ import { AdvancedDynamicTexture } from "babylonjs-gui"; -import { CubeTexture, Texture, ColorGradingTexture } from "babylonjs"; +import { CubeTexture, Texture, ColorGradingTexture, HDRCubeTexture } from "babylonjs"; /** * Returns wether or not the given object is a Texture. @@ -17,6 +17,14 @@ export function isCubeTexture(object: any): object is CubeTexture { return object?.getClassName?.() === "CubeTexture"; } +/** + * Returns wether or not the given object is a HDRCubeTexture. + * @param object defines the reference to the object to test its class name. + */ +export function isHDRCubeTexture(object: any): object is HDRCubeTexture { + return object?.getClassName?.() === "HDRCubeTexture"; +} + /** * Returns wether or not the given object is a AdvancedDynamicTexture. * @param object defines the reference to the object to test its class name. From 45e4607ad53dbb70024875d43282e1738b148b6e Mon Sep 17 00:00:00 2001 From: Cesharpe Date: Fri, 3 Apr 2026 20:13:12 +0200 Subject: [PATCH 03/21] feat: add support of HDR as environment texture when exporting scene from editor and CLI --- cli/src/pack/assets/process.mts | 2 +- cli/src/pack/scene.mts | 10 +++++++++- editor/src/project/export/assets.ts | 6 +----- editor/src/project/export/export.tsx | 9 +++++++++ editor/src/tools/scene/play/override.tsx | 11 ++++++++++- 5 files changed, 30 insertions(+), 8 deletions(-) diff --git a/cli/src/pack/assets/process.mts b/cli/src/pack/assets/process.mts index 21ce130e5..2876fafaa 100644 --- a/cli/src/pack/assets/process.mts +++ b/cli/src/pack/assets/process.mts @@ -9,7 +9,7 @@ import { processExportedMaterial } from "./material.mjs"; import { processExportedNodeParticleSystemSet } from "./particle-system.mjs"; const supportedImagesExtensions: string[] = [".jpg", ".jpeg", ".webp", ".png", ".bmp"]; -const supportedCubeTexturesExtensions: string[] = [".env", ".dds"]; +const supportedCubeTexturesExtensions: string[] = [".env", ".dds", ".hdr"]; const supportedAudioExtensions: string[] = [".mp3", ".wav", ".wave", ".ogg"]; const supportedJsonExtensions: string[] = [".material", ".gui", ".cinematic", ".npss", ".ragdoll", ".json"]; const supportedMiscExtensions: string[] = [".3dl", ".exr", ".hdr"]; diff --git a/cli/src/pack/scene.mts b/cli/src/pack/scene.mts index 02daada71..414879421 100644 --- a/cli/src/pack/scene.mts +++ b/cli/src/pack/scene.mts @@ -522,7 +522,7 @@ export async function createBabylonScene(options: ICreateBabylonSceneOptions) { postProcesses: [], spriteManagers: [], reflectionProbes: [], - }; + } as any; // Resolve parenting for mesh instances. const allNodes = [...scene.meshes, ...scene.cameras, ...scene.lights, ...scene.transformNodes, ...scene.meshes.map((m) => m.instances ?? []).flat()]; @@ -539,6 +539,14 @@ export async function createBabylonScene(options: ICreateBabylonSceneOptions) { } }); + // Configue ennviornment texture + if (scene.environmentTexture) { + scene.environmentTextureSize = 512; + scene.environmentTextureType = "BABYLON.HDRCubeTexture"; + scene.environmentTextureRotationY = scene.environmentTexture.rotationY; + scene.environmentTexture = scene.environmentTexture.name; + } + // Write final scene file. const destination = join(options.publicDir, `${options.sceneName}.babylon`); await fs.writeJSON(destination, scene, { diff --git a/editor/src/project/export/assets.ts b/editor/src/project/export/assets.ts index fc6427011..6ad15a816 100644 --- a/editor/src/project/export/assets.ts +++ b/editor/src/project/export/assets.ts @@ -11,13 +11,9 @@ import { processExportedMaterial } from "./materials"; import { processExportedNodeParticleSystemSet } from "./particles"; const supportedImagesExtensions: string[] = [".jpg", ".jpeg", ".webp", ".png", ".bmp"]; - -const supportedCubeTexturesExtensions: string[] = [".env", ".dds"]; - +const supportedCubeTexturesExtensions: string[] = [".env", ".dds", ".hdr"]; const supportedAudioExtensions: string[] = [".mp3", ".wav", ".wave", ".ogg"]; - const supportedJsonExtensions: string[] = [".material", ".gui", ".cinematic", ".npss", ".ragdoll", ".json"]; - const supportedMiscExtensions: string[] = [".3dl", ".exr", ".hdr"]; const supportedExtensions: string[] = [ diff --git a/editor/src/project/export/export.tsx b/editor/src/project/export/export.tsx index f20c40258..8a6c36856 100644 --- a/editor/src/project/export/export.tsx +++ b/editor/src/project/export/export.tsx @@ -6,6 +6,7 @@ import { RenderTargetTexture, SceneSerializer } from "babylonjs"; import { toast } from "sonner"; import { isNodeMaterial } from "../../tools/guards/material"; +import { isHDRCubeTexture } from "../../tools/guards/texture"; import { getCollisionMeshFor } from "../../tools/mesh/collision"; import { storeTexturesBaseSize } from "../../tools/material/texture"; import { extractNodeMaterialTextures } from "../../tools/material/extract"; @@ -149,6 +150,7 @@ async function _exportProject(editor: Editor, options: IExportProjectOptions): P taaRenderingPipeline: taaPipelineCameraConfigurations.get(camera), })); + delete data.effectLayers; delete data.postProcesses; delete data.spriteManagers; @@ -159,6 +161,13 @@ async function _exportProject(editor: Editor, options: IExportProjectOptions): P configureMeshesPhysics(data, scene); configureParticleSystems(data, scene); + // Configure environment texture + if (isHDRCubeTexture(scene.environmentTexture)) { + data.environmentTextureSize = 512; + data.environmentTextureType = "BABYLON.HDRCubeTexture"; + data.environmentTextureRotationY = scene.environmentTexture.rotationY; + } + // Write all geometries as incremental. This makes the scene way less heavy as binary saved geometry // is not stored in the JSON scene file. Moreover, this may allow to load geometries on the fly compared // to single JSON file. diff --git a/editor/src/tools/scene/play/override.tsx b/editor/src/tools/scene/play/override.tsx index aa7e1d638..b1087bc20 100644 --- a/editor/src/tools/scene/play/override.tsx +++ b/editor/src/tools/scene/play/override.tsx @@ -31,6 +31,7 @@ const savedWebRequestMethods: Record = { const savedEngineMethods: Record = { createTexture: Engine.prototype.createTexture, createCubeTexture: Engine.prototype.createCubeTexture, + createRawCubeTextureFromUrl: Engine.prototype.createRawCubeTextureFromUrl, }; const savedTextureMethods: Record = { @@ -108,7 +109,7 @@ export function restorePlayOverrides(editor: Editor) { Engine.prototype.createTexture = savedEngineMethods.createTexture; Engine.prototype.createCubeTexture = savedEngineMethods.createCubeTexture; - + Engine.prototype.createRawCubeTextureFromUrl = savedEngineMethods.createRawCubeTextureFromUrl; SerializationHelper._TextureParser = savedTextureMethods.textureParser; Observable.prototype.add = savedObservableMethods.add; @@ -272,6 +273,14 @@ export function applyOverrides(editor: Editor) { }; // Engine + Engine.prototype.createRawCubeTextureFromUrl = (url: string, ...args: any[]) => { + if (url && url.includes(publicScene)) { + url = url.replace(publicScene, projectDir); + } + + return savedEngineMethods.createRawCubeTextureFromUrl.call(editor.layout.preview.engine, url, ...args); + }; + Engine.prototype.createCubeTexture = (rootUrl: string, ...args: any[]) => { if (rootUrl && rootUrl.includes(publicScene)) { rootUrl = rootUrl.replace(publicScene, projectDir); From 19a37b3e68f2a5205e6272ec69275ed3aa611f69 Mon Sep 17 00:00:00 2001 From: Cesharpe Date: Sat, 4 Apr 2026 16:12:05 +0200 Subject: [PATCH 04/21] feat: adding support of clustered lights --- editor/src/editor/layout/graph.tsx | 20 +- editor/src/editor/layout/graph/graph.tsx | 135 +-- editor/src/editor/layout/graph/label.tsx | 7 +- editor/src/editor/layout/graph/remove.ts | 15 +- .../editor/layout/inspector/fields/switch.tsx | 15 +- .../inspector/light/components/cluster.tsx | 60 ++ .../inspector/light/{ => components}/pbr.tsx | 4 +- .../light/{ => components}/shadows.tsx | 782 +++++++++--------- .../layout/inspector/light/directional.tsx | 6 +- .../editor/layout/inspector/light/point.tsx | 8 +- .../editor/layout/inspector/light/spot.tsx | 8 +- editor/src/editor/layout/preview.tsx | 11 + editor/src/editor/layout/preview/icons.tsx | 45 +- editor/src/index.ts | 3 +- editor/src/project/export/export.tsx | 30 +- editor/src/project/export/light.ts | 11 + editor/src/project/load/scene.ts | 8 + editor/src/project/save/scene.ts | 7 +- editor/src/tools/guards/nodes.ts | 9 + editor/src/tools/light/cluster.ts | 7 + editor/src/tools/node/clone.ts | 11 +- editor/test/tools/node/clone.test.mts | 25 +- tools/src/loading/light.ts | 18 + tools/src/loading/loader.ts | 19 +- 24 files changed, 755 insertions(+), 509 deletions(-) create mode 100644 editor/src/editor/layout/inspector/light/components/cluster.tsx rename editor/src/editor/layout/inspector/light/{ => components}/pbr.tsx (87%) rename editor/src/editor/layout/inspector/light/{ => components}/shadows.tsx (91%) create mode 100644 editor/src/project/export/light.ts create mode 100644 editor/src/tools/light/cluster.ts create mode 100644 tools/src/loading/light.ts diff --git a/editor/src/editor/layout/graph.tsx b/editor/src/editor/layout/graph.tsx index 45338e793..892072061 100644 --- a/editor/src/editor/layout/graph.tsx +++ b/editor/src/editor/layout/graph.tsx @@ -52,6 +52,7 @@ import { isAbstractMesh, isAnyTransformNode, isCamera, + isClusteredLightContainer, isCollisionInstancedMesh, isCollisionMesh, isEditorCamera, @@ -350,6 +351,10 @@ export class EditorGraph extends Component source = getSpriteManagerNodeFromSprite(source); } + if (isLight(source) && this.props.editor.layout.preview.clusteredLightContainer.lights.includes(source)) { + source = this.props.editor.layout.preview.clusteredLightContainer; + } + const idsToExpand: string[] = []; while (source) { @@ -918,7 +923,7 @@ export class EditorGraph extends Component return null; } - if (isLight(node) && !node._scene.lights.includes(node)) { + if (isLight(node) && !node._scene.lights.includes(node) && !this.props.editor.layout.preview.clusteredLightContainer.lights.includes(node)) { return null; } @@ -926,6 +931,10 @@ export class EditorGraph extends Component return null; } + if (isClusteredLightContainer(node) && node.lights.length === 0) { + return null; + } + node.id ??= Tools.RandomId(); const info = { @@ -976,6 +985,13 @@ export class EditorGraph extends Component }); } + // Handle clustered lights + if (isClusteredLightContainer(node) && !noChildren) { + node.lights.forEach((light) => { + info.childNodes?.push(this._parseSceneNode(light, false) as TreeNodeInfo); + }); + } + if (info.childNodes?.length) { info.hasCaret = true; } else { @@ -1060,7 +1076,7 @@ export class EditorGraph extends Component return ; } - if (isLight(object)) { + if (isLight(object) || isClusteredLightContainer(object)) { return ; } diff --git a/editor/src/editor/layout/graph/graph.tsx b/editor/src/editor/layout/graph/graph.tsx index da95d8721..d0eeae50d 100644 --- a/editor/src/editor/layout/graph/graph.tsx +++ b/editor/src/editor/layout/graph/graph.tsx @@ -34,12 +34,13 @@ import { isSound } from "../../../tools/guards/sound"; import { reloadSound } from "../../../tools/sound/tools"; import { registerUndoRedo } from "../../../tools/undoredo"; import { waitNextAnimationFrame } from "../../../tools/tools"; +import { isClusteredLight } from "../../../tools/light/cluster"; import { createMeshInstance } from "../../../tools/mesh/instance"; import { isAnyParticleSystem } from "../../../tools/guards/particles"; import { isScene, isSceneLinkNode } from "../../../tools/guards/scene"; import { cloneNode, ICloneNodeOptions } from "../../../tools/node/clone"; import { isSprite, isSpriteMapNode } from "../../../tools/guards/sprites"; -import { isAbstractMesh, isCamera, isMesh, isNode } from "../../../tools/guards/nodes"; +import { isAbstractMesh, isCamera, isClusteredLightContainer, isLight, isMesh, isNode } from "../../../tools/guards/nodes"; import { isNodeLocked, isNodeSerializable, isNodeVisibleInGraph, setNodeLocked, setNodeSerializable } from "../../../tools/node/metadata"; import { addGPUParticleSystem, addParticleSystem } from "../../../project/add/particles"; @@ -76,7 +77,7 @@ export class EditorGraphContextMenu extends Component )} - {!isScene(this.props.object) && !isSound(this.props.object) && ( + {!isScene(this.props.object) && !isSound(this.props.object) && !isClusteredLightContainer(this.props.object) && ( <> this._cloneNode(this.props.object)}>Clone @@ -133,74 +134,82 @@ export class EditorGraphContextMenu extends Component )} - {(isNode(this.props.object) || isScene(this.props.object)) && !isSceneLinkNode(this.props.object) && ( - - - Add - - - {getLightCommands(this.props.editor, parent).map((command) => ( - - {command.text} - - ))} - - {getNodeCommands(this.props.editor, parent).map((command) => { - return ( + {(isNode(this.props.object) || isScene(this.props.object)) && + !isSceneLinkNode(this.props.object) && + !(isLight(this.props.object) && isClusteredLight(this.props.object, this.props.editor)) && ( + + + Add + + + {getLightCommands(this.props.editor, parent).map((command) => ( {command.text} - ); - })} - - - - Meshes - - - {getMeshCommands(this.props.editor, parent).map((command) => ( + ))} + + {getNodeCommands(this.props.editor, parent).map((command) => { + return ( {command.text} - ))} - - - - {getCameraCommands(this.props.editor, parent).map((command) => ( - - {command.text} - - ))} - {isAbstractMesh(this.props.object) && ( - <> - - addParticleSystem(this.props.editor, this.props.object)}>Particle System - addGPUParticleSystem(this.props.editor, this.props.object)}>GPU Particle System - - )} + ); + })} + + + + Meshes + + + {getMeshCommands(this.props.editor, parent).map((command) => ( + + {command.text} + + ))} + + + + {getCameraCommands(this.props.editor, parent).map((command) => ( + + {command.text} + + ))} + {isAbstractMesh(this.props.object) && ( + <> + + addParticleSystem(this.props.editor, this.props.object)}>Particle System + addGPUParticleSystem(this.props.editor, this.props.object)}> + GPU Particle System + + + )} + + {getSpriteCommands(this.props.editor, parent).map((command) => ( + + {command.text} + + ))} + + + )} + + {!isScene(this.props.object) && + !isSound(this.props.object) && + !isSprite(this.props.object) && + !isAnyParticleSystem(this.props.object) && + !isClusteredLightContainer(this.props.object) && ( + <> - {getSpriteCommands(this.props.editor, parent).map((command) => ( - - {command.text} - - ))} - - - )} - - {!isScene(this.props.object) && !isSound(this.props.object) && !isSprite(this.props.object) && !isAnyParticleSystem(this.props.object) && ( - <> - - this._handleSetNodeLocked()}> - Locked - - this._handleSetNodeSerializable()}> - Do not serialize - - - )} - - {!isScene(this.props.object) && ( + this._handleSetNodeLocked()}> + Locked + + this._handleSetNodeSerializable()}> + Do not serialize + + + )} + + {!isScene(this.props.object) && !isClusteredLightContainer(this.props.object) && ( <> {this._getRemoveItems()} diff --git a/editor/src/editor/layout/graph/label.tsx b/editor/src/editor/layout/graph/label.tsx index ce176fbf9..8e533da37 100644 --- a/editor/src/editor/layout/graph/label.tsx +++ b/editor/src/editor/layout/graph/label.tsx @@ -13,9 +13,10 @@ import { isDarwin } from "../../../tools/os"; import { isScene } from "../../../tools/guards/scene"; import { isSound } from "../../../tools/guards/sound"; import { registerUndoRedo } from "../../../tools/undoredo"; +import { isClusteredLight } from "../../../tools/light/cluster"; import { isAnyParticleSystem } from "../../../tools/guards/particles"; import { isNodeSerializable, isNodeLocked } from "../../../tools/node/metadata"; -import { isAbstractMesh, isInstancedMesh, isMesh, isNode, isTransformNode } from "../../../tools/guards/nodes"; +import { isAbstractMesh, isInstancedMesh, isLight, isMesh, isNode, isTransformNode } from "../../../tools/guards/nodes"; import { applyNodeParentingConfiguration, applyTransformNodeParentingConfiguration, IOldNodeHierarchyConfiguration } from "../../../tools/node/parenting"; import { applySoundAsset } from "../preview/import/sound"; @@ -133,6 +134,10 @@ export function EditorGraphLabel(props: IEditorGraphLabelProps) { nodesToMove.forEach((n) => { if (n.nodeData && n.nodeData !== newParent) { + if (isLight(n.nodeData) && isClusteredLight(n.nodeData, props.editor)) { + return; + } + if (isNode(n.nodeData) && n.nodeData.parent !== newParent) { const descendants = n.nodeData.getDescendants(false); if (descendants.includes(newParent)) { diff --git a/editor/src/editor/layout/graph/remove.ts b/editor/src/editor/layout/graph/remove.ts index bc16eaa7b..7ade87b00 100644 --- a/editor/src/editor/layout/graph/remove.ts +++ b/editor/src/editor/layout/graph/remove.ts @@ -5,6 +5,7 @@ import { isSound } from "../../../tools/guards/sound"; import { isSprite } from "../../../tools/guards/sprites"; import { registerUndoRedo } from "../../../tools/undoredo"; import { updateAllLights } from "../../../tools/light/shadows"; +import { isClusteredLight } from "../../../tools/light/cluster"; import { isAnyParticleSystem } from "../../../tools/guards/particles"; import { isAdvancedDynamicTexture } from "../../../tools/guards/texture"; import { getLinkedAnimationGroupsFor } from "../../../tools/animation/group"; @@ -15,6 +16,7 @@ import { Editor } from "../../main"; type _RemoveNodeData = { node: Node; parent: Node | null; + isClusteredLight: boolean; lights: Light[]; sounds: { @@ -61,6 +63,7 @@ export function removeNodes(editor: Editor) { })) ) .flat() ?? [], + isClusteredLight: isLight(descendant) && isClusteredLight(descendant, editor), lights: scene.lights.filter((light) => { return light .getShadowGenerator() @@ -120,7 +123,7 @@ export function removeNodes(editor: Editor) { }, undo: () => { nodes.forEach((d) => { - restoreNodeData(d, scene); + restoreNodeData(editor, d, scene); }); sounds.forEach((d) => { @@ -206,7 +209,7 @@ export function removeNodes(editor: Editor) { }); } -function restoreNodeData(data: _RemoveNodeData, scene: Scene) { +function restoreNodeData(editor: Editor, data: _RemoveNodeData, scene: Scene) { const node = data.node; if (isAbstractMesh(node)) { @@ -227,6 +230,10 @@ function restoreNodeData(data: _RemoveNodeData, scene: Scene) { if (isLight(node)) { scene.addLight(node); + + if (data.isClusteredLight) { + editor.layout.preview.clusteredLightContainer.addLight(node); + } } if (isCamera(node)) { @@ -258,6 +265,10 @@ function removeNodeData(editor: Editor, data: _RemoveNodeData, scene: Scene) { } if (isLight(node)) { + if (data.isClusteredLight) { + editor.layout.preview.clusteredLightContainer.removeLight(node); + } + scene.removeLight(node); } diff --git a/editor/src/editor/layout/inspector/fields/switch.tsx b/editor/src/editor/layout/inspector/fields/switch.tsx index 5801db1f1..4568b38b0 100644 --- a/editor/src/editor/layout/inspector/fields/switch.tsx +++ b/editor/src/editor/layout/inspector/fields/switch.tsx @@ -11,6 +11,7 @@ import { getInspectorPropertyValue, setInspectorEffectivePropertyValue } from ". import { IEditorInspectorFieldProps } from "./field"; export interface IEditorInspectorSwitchFieldProps extends IEditorInspectorFieldProps { + disabled?: boolean; onChange?: (value: boolean) => void; } @@ -26,6 +27,10 @@ export function EditorInspectorSwitchField(props: IEditorInspectorSwitchFieldPro onClick={(ev) => { ev.stopPropagation(); + if (props.disabled) { + return; + } + setValue(!value); setInspectorEffectivePropertyValue(props.object, props.property, !value); props.onChange?.(!value); @@ -40,10 +45,14 @@ export function EditorInspectorSwitchField(props: IEditorInspectorSwitchFieldPro }); } }} - className="flex gap-2 justify-center items-center px-2 cursor-pointer hover:bg-white/10 hover:px-2 rounded-lg transition-all duration-300" + className={` + flex gap-2 justify-center items-center px-2 rounded-lg + ${props.disabled ? "" : "cursor-pointer hover:bg-white/10"} + transition-all ease-in-out duration-300 + `} >
- {props.label} +
{props.label}
{props.tooltip && ( @@ -60,7 +69,7 @@ export function EditorInspectorSwitchField(props: IEditorInspectorSwitchFieldPro
- {}} /> + {}} />
); diff --git a/editor/src/editor/layout/inspector/light/components/cluster.tsx b/editor/src/editor/layout/inspector/light/components/cluster.tsx new file mode 100644 index 000000000..77e7a03a2 --- /dev/null +++ b/editor/src/editor/layout/inspector/light/components/cluster.tsx @@ -0,0 +1,60 @@ +import { Light, ClusteredLightContainer } from "babylonjs"; + +import { Editor } from "../../../../main"; + +import { registerUndoRedo } from "../../../../../tools/undoredo"; + +import { EditorInspectorSwitchField } from "../../fields/switch"; + +export interface IEditorLightClusterInspectorProps { + light: Light; + editor: Editor; +} + +export function EditorLightClusterInspector(props: IEditorLightClusterInspectorProps) { + props.light.metadata ??= {}; + + const isSupported = ClusteredLightContainer.IsLightSupported(props.light); + + return ( + <> + { + const oldValue = !v; + + registerUndoRedo({ + executeRedo: true, + undo: () => { + if (oldValue) { + props.editor.layout.preview.clusteredLightContainer.addLight(props.light); + } else { + props.editor.layout.preview.clusteredLightContainer.removeLight(props.light); + } + }, + redo: () => { + if (v) { + props.editor.layout.preview.clusteredLightContainer.addLight(props.light); + } else { + props.editor.layout.preview.clusteredLightContainer.removeLight(props.light); + } + }, + }); + + props.editor.layout.graph.refresh().then(() => { + if (props.editor.layout.graph.isNodeSelected(props.light)) { + props.editor.layout.graph.setSelectedNode(props.light); + } + }); + + props.editor.layout.inspector.forceUpdate(); + }} + /> + + ); +} diff --git a/editor/src/editor/layout/inspector/light/pbr.tsx b/editor/src/editor/layout/inspector/light/components/pbr.tsx similarity index 87% rename from editor/src/editor/layout/inspector/light/pbr.tsx rename to editor/src/editor/layout/inspector/light/components/pbr.tsx index 94bd51008..ea7a890bb 100644 --- a/editor/src/editor/layout/inspector/light/pbr.tsx +++ b/editor/src/editor/layout/inspector/light/components/pbr.tsx @@ -1,7 +1,7 @@ import { Light } from "babylonjs"; -import { EditorInspectorListField } from "../fields/list"; -import { EditorInspectorNumberField } from "../fields/number"; +import { EditorInspectorListField } from "../../fields/list"; +import { EditorInspectorNumberField } from "../../fields/number"; export interface IEditorLightPBRInspectorProps { object: Light; diff --git a/editor/src/editor/layout/inspector/light/shadows.tsx b/editor/src/editor/layout/inspector/light/components/shadows.tsx similarity index 91% rename from editor/src/editor/layout/inspector/light/shadows.tsx rename to editor/src/editor/layout/inspector/light/components/shadows.tsx index b397928de..757c5070d 100644 --- a/editor/src/editor/layout/inspector/light/shadows.tsx +++ b/editor/src/editor/layout/inspector/light/components/shadows.tsx @@ -1,384 +1,398 @@ -import { Divider } from "@blueprintjs/core"; -import { Component, PropsWithChildren, ReactNode } from "react"; - -import { CascadedShadowGenerator, DirectionalLight, IShadowGenerator, IShadowLight, RenderTargetTexture, ShadowGenerator } from "babylonjs"; - -import { waitNextAnimationFrame } from "../../../../tools/tools"; -import { getPowerOfTwoSizesUntil } from "../../../../tools/maths/scalar"; -import { isDirectionalLight, isPointLight } from "../../../../tools/guards/nodes"; -import { isCascadedShadowGenerator, isShadowGenerator } from "../../../../tools/guards/shadows"; -import { updateLightShadowMapRefreshRate, updatePointLightShadowMapRenderListPredicate } from "../../../../tools/light/shadows"; - -import { EditorInspectorNumberField } from "../fields/number"; -import { EditorInspectorSwitchField } from "../fields/switch"; -import { EditorInspectorSectionField } from "../fields/section"; -import { EditorInspectorListField, IEditorInspectorListFieldItem } from "../fields/list"; - -export interface IEditorLightShadowsInspectorProps extends PropsWithChildren { - light: IShadowLight; -} - -export interface IEditorLightShadowsInspectorState { - generator: IShadowGenerator | null; -} - -export type SoftShadowType = - | "usePoissonSampling" - | "useExponentialShadowMap" - | "useCloseExponentialShadowMap" - | "usePercentageCloserFiltering" - | "useContactHardeningShadow" - | "none"; - -export class EditorLightShadowsInspector extends Component { - protected _generatorSize: number = 1024; - protected _generatorType: string = "none"; - - protected _softShadowType: SoftShadowType = "none"; - - protected _sizes: IEditorInspectorListFieldItem[] = getPowerOfTwoSizesUntil(4096, 256).map( - (s) => - ({ - value: s, - text: `${s}px`, - }) as IEditorInspectorListFieldItem - ); - - public constructor(props: IEditorLightShadowsInspectorProps) { - super(props); - - this.state = { - generator: null, - }; - } - - public render(): ReactNode { - return ( - <> - - {this._getEmptyShadowGeneratorComponent()} - {this._getClassicShadowGeneratorComponent()} - {this._getCascadedShadowGeneratorComponent()} - - - {this._getClassicSoftShadowComponent()} - - ); - } - - public componentDidMount(): void { - this._refreshShadowGenerator(); - } - - private _refreshShadowGenerator(): void { - const generator = this.props.light.getShadowGenerator(); - - this._generatorType = !generator ? "none" : isCascadedShadowGenerator(generator) ? "cascaded" : "classic"; - - this._softShadowType = this._getSoftShadowType(generator); - this._generatorSize = generator?.getShadowMap()?.getSize().width ?? 1024; - - this.setState({ generator }); - } - - private _createShadowGenerator(type: "none" | "classic" | "cascaded"): void { - const mapSize = this.state.generator?.getShadowMap()?.getSize(); - const renderList = this.state.generator?.getShadowMap()?.renderList?.slice(0); - - this.state.generator?.dispose(); - - if (type === "none") { - return this._refreshShadowGenerator(); - } - - if (!isDirectionalLight(this.props.light)) { - type = "classic"; - } - - const generator = - type === "classic" - ? new ShadowGenerator(mapSize?.width ?? 1024, this.props.light, true) - : new CascadedShadowGenerator(mapSize?.width ?? 1024, this.props.light as DirectionalLight, true); - - if (isCascadedShadowGenerator(generator)) { - generator.lambda = 1; - generator.depthClamp = true; - generator.autoCalcDepthBounds = true; - generator.autoCalcDepthBoundsRefreshRate = 60; - } - - if (!isPointLight(this.props.light)) { - generator.usePercentageCloserFiltering = true; - generator.filteringQuality = ShadowGenerator.QUALITY_HIGH; - } - - generator.transparencyShadow = true; - generator.enableSoftTransparentShadow = true; - - if (renderList) { - generator.getShadowMap()?.renderList?.push(...renderList); - } else { - generator.getShadowMap()?.renderList?.push(...generator.getLight().getScene().meshes); - } - - this._refreshShadowGenerator(); - } - - private _reszeShadowGenerator(size: number): void { - const shadowMap = this.state.generator?.getShadowMap(); - if (shadowMap) { - const refreshRate = shadowMap.refreshRate; - shadowMap.resize(size); - - waitNextAnimationFrame().then(() => { - updatePointLightShadowMapRenderListPredicate(this.props.light); - - const newShadowMap = this.state.generator?.getShadowMap(); - if (newShadowMap) { - newShadowMap.refreshRate = refreshRate; - } - }); - } - } - - private _getEmptyShadowGeneratorComponent(): ReactNode { - if (this.state.generator) { - return ( - <> - this._createShadowGenerator(v)} - items={[ - { text: "None", value: "none" }, - { text: "Classic", value: "classic" }, - { text: "Cascaded", value: "cascaded" }, - ]} - /> - this._reszeShadowGenerator(v)} items={this._sizes} /> - - - ); - } - - return ( - this._createShadowGenerator(v)} - items={[ - { text: "None", value: "none" }, - { text: "Classic", value: "classic" }, - { text: "Cascaded", value: "cascaded" }, - ]} - /> - ); - } - - private _getClassicShadowGeneratorComponent(): ReactNode { - const generator = this.state.generator as ShadowGenerator; - - if (!generator) { - return null; - } - - const shadowMap = generator.getShadowMap(); - - return ( - <> - {this.props.children} - updateLightShadowMapRefreshRate(this.props.light)} - /> - updateLightShadowMapRefreshRate(this.props.light)} - /> - - - {shadowMap && ( - - )} - - - - - ); - } - - private _getClassicSoftShadowComponent(): ReactNode { - const generator = this.state.generator as ShadowGenerator | CascadedShadowGenerator; - - if (!generator) { - return null; - } - - return ( - - { - this._updateSoftShadowType(v); - updateLightShadowMapRefreshRate(this.props.light); - }} - items={[ - { text: "None", value: "none" }, - ...(isPointLight(this.props.light) - ? [{ text: "Poisson Sampling", value: "usePoissonSampling" }] - : [ - { text: "Percentage Closer Filtering", value: "usePercentageCloserFiltering" }, - { text: "Contact Hardening Shadow", value: "useContactHardeningShadow" }, - ]), - ]} - /> - - {generator.usePoissonSampling && } - - {generator.usePercentageCloserFiltering && !generator.useContactHardeningShadow && ( - <> - updateLightShadowMapRefreshRate(this.props.light)} - /> - - )} - - {generator.useContactHardeningShadow && ( - updateLightShadowMapRefreshRate(this.props.light)} - /> - )} - - ); - } - - private _getCascadedShadowGeneratorComponent(): ReactNode { - const generator = this.state.generator; - - if (!generator || !isCascadedShadowGenerator(generator)) { - return null; - } - - return ( - <> - {this.props.children} - updateLightShadowMapRefreshRate(this.props.light)} - /> - updateLightShadowMapRefreshRate(this.props.light)} /> - { - this.forceUpdate(); - updateLightShadowMapRefreshRate(this.props.light); - }} - /> - {generator.autoCalcDepthBounds && ( - updateLightShadowMapRefreshRate(this.props.light)} - /> - )} - updateLightShadowMapRefreshRate(this.props.light)} - /> - updateLightShadowMapRefreshRate(this.props.light)} - /> - updateLightShadowMapRefreshRate(this.props.light)} - /> - - ); - } - - private _getSoftShadowType(generator: IShadowGenerator | null): SoftShadowType { - if (generator && (isShadowGenerator(generator) || isCascadedShadowGenerator(generator))) { - if (generator.usePercentageCloserFiltering) { - return "usePercentageCloserFiltering"; - } else if (generator.useContactHardeningShadow) { - return "useContactHardeningShadow"; - } - } - - return "none"; - } - - private _updateSoftShadowType(type: SoftShadowType): void { - if (this.state.generator && (isShadowGenerator(this.state.generator) || isCascadedShadowGenerator(this.state.generator))) { - this.state.generator.usePoissonSampling = false; - this.state.generator.useExponentialShadowMap = false; - this.state.generator.useBlurExponentialShadowMap = false; - this.state.generator.useCloseExponentialShadowMap = false; - this.state.generator.useBlurCloseExponentialShadowMap = false; - this.state.generator.usePercentageCloserFiltering = false; - this.state.generator.useContactHardeningShadow = false; - - this.state.generator[type] = true; - - this.forceUpdate(); - } - } -} +import { Divider } from "@blueprintjs/core"; +import { Component, PropsWithChildren, ReactNode } from "react"; + +import { CascadedShadowGenerator, DirectionalLight, IShadowGenerator, IShadowLight, RenderTargetTexture, ShadowGenerator } from "babylonjs"; + +import { waitNextAnimationFrame } from "../../../../../tools/tools"; +import { getPowerOfTwoSizesUntil } from "../../../../../tools/maths/scalar"; +import { isDirectionalLight, isPointLight } from "../../../../../tools/guards/nodes"; +import { isCascadedShadowGenerator, isShadowGenerator } from "../../../../../tools/guards/shadows"; +import { updateLightShadowMapRefreshRate, updatePointLightShadowMapRenderListPredicate } from "../../../../../tools/light/shadows"; + +import { Editor } from "../../../../main"; + +import { EditorInspectorNumberField } from "../../fields/number"; +import { EditorInspectorSwitchField } from "../../fields/switch"; +import { EditorInspectorSectionField } from "../../fields/section"; +import { EditorInspectorListField, IEditorInspectorListFieldItem } from "../../fields/list"; + +export interface IEditorLightShadowsInspectorProps extends PropsWithChildren { + editor: Editor; + light: IShadowLight; + onShadowGeneratorChanged: () => void; +} + +export interface IEditorLightShadowsInspectorState { + generator: IShadowGenerator | null; +} + +export type SoftShadowType = + | "usePoissonSampling" + | "useExponentialShadowMap" + | "useCloseExponentialShadowMap" + | "usePercentageCloserFiltering" + | "useContactHardeningShadow" + | "none"; + +export class EditorLightShadowsInspector extends Component { + protected _generatorSize: number = 1024; + protected _generatorType: string = "none"; + + protected _softShadowType: SoftShadowType = "none"; + + protected _sizes: IEditorInspectorListFieldItem[] = getPowerOfTwoSizesUntil(4096, 256).map( + (s) => + ({ + value: s, + text: `${s}px`, + }) as IEditorInspectorListFieldItem + ); + + public constructor(props: IEditorLightShadowsInspectorProps) { + super(props); + + this.state = { + generator: null, + }; + } + + public render(): ReactNode { + if (this.props.editor.layout.preview.clusteredLightContainer.lights.includes(this.props.light)) { + return ( + +
Shadows are not supported for lights in a clustered light container.
+
+ ); + } + + return ( + <> + + {this._getEmptyShadowGeneratorComponent()} + {this._getClassicShadowGeneratorComponent()} + {this._getCascadedShadowGeneratorComponent()} + + + {this._getClassicSoftShadowComponent()} + + ); + } + + public componentDidMount(): void { + this._refreshShadowGenerator(); + } + + private _refreshShadowGenerator(): void { + const generator = this.props.light.getShadowGenerator(); + + this._generatorType = !generator ? "none" : isCascadedShadowGenerator(generator) ? "cascaded" : "classic"; + + this._softShadowType = this._getSoftShadowType(generator); + this._generatorSize = generator?.getShadowMap()?.getSize().width ?? 1024; + + this.setState({ generator }); + } + + private _createShadowGenerator(type: "none" | "classic" | "cascaded"): void { + const mapSize = this.state.generator?.getShadowMap()?.getSize(); + const renderList = this.state.generator?.getShadowMap()?.renderList?.slice(0); + + this.state.generator?.dispose(); + + if (type === "none") { + this.props.onShadowGeneratorChanged(); + return this._refreshShadowGenerator(); + } + + if (!isDirectionalLight(this.props.light)) { + type = "classic"; + } + + const generator = + type === "classic" + ? new ShadowGenerator(mapSize?.width ?? 1024, this.props.light, true) + : new CascadedShadowGenerator(mapSize?.width ?? 1024, this.props.light as DirectionalLight, true); + + if (isCascadedShadowGenerator(generator)) { + generator.lambda = 1; + generator.depthClamp = true; + generator.autoCalcDepthBounds = true; + generator.autoCalcDepthBoundsRefreshRate = 60; + } + + if (!isPointLight(this.props.light)) { + generator.usePercentageCloserFiltering = true; + generator.filteringQuality = ShadowGenerator.QUALITY_HIGH; + } + + generator.transparencyShadow = true; + generator.enableSoftTransparentShadow = true; + + if (renderList) { + generator.getShadowMap()?.renderList?.push(...renderList); + } else { + generator.getShadowMap()?.renderList?.push(...generator.getLight().getScene().meshes); + } + + this._refreshShadowGenerator(); + this.props.onShadowGeneratorChanged(); + } + + private _reszeShadowGenerator(size: number): void { + const shadowMap = this.state.generator?.getShadowMap(); + if (shadowMap) { + const refreshRate = shadowMap.refreshRate; + shadowMap.resize(size); + + waitNextAnimationFrame().then(() => { + updatePointLightShadowMapRenderListPredicate(this.props.light); + + const newShadowMap = this.state.generator?.getShadowMap(); + if (newShadowMap) { + newShadowMap.refreshRate = refreshRate; + } + }); + } + } + + private _getEmptyShadowGeneratorComponent(): ReactNode { + if (this.state.generator) { + return ( + <> + this._createShadowGenerator(v)} + items={[ + { text: "None", value: "none" }, + { text: "Classic", value: "classic" }, + { text: "Cascaded", value: "cascaded" }, + ]} + /> + this._reszeShadowGenerator(v)} items={this._sizes} /> + + + ); + } + + return ( + this._createShadowGenerator(v)} + items={[ + { text: "None", value: "none" }, + { text: "Classic", value: "classic" }, + { text: "Cascaded", value: "cascaded" }, + ]} + /> + ); + } + + private _getClassicShadowGeneratorComponent(): ReactNode { + const generator = this.state.generator as ShadowGenerator; + + if (!generator) { + return null; + } + + const shadowMap = generator.getShadowMap(); + + return ( + <> + {this.props.children} + updateLightShadowMapRefreshRate(this.props.light)} + /> + updateLightShadowMapRefreshRate(this.props.light)} + /> + + + {shadowMap && ( + + )} + + + + + ); + } + + private _getClassicSoftShadowComponent(): ReactNode { + const generator = this.state.generator as ShadowGenerator | CascadedShadowGenerator; + + if (!generator) { + return null; + } + + return ( + + { + this._updateSoftShadowType(v); + updateLightShadowMapRefreshRate(this.props.light); + }} + items={[ + { text: "None", value: "none" }, + ...(isPointLight(this.props.light) + ? [{ text: "Poisson Sampling", value: "usePoissonSampling" }] + : [ + { text: "Percentage Closer Filtering", value: "usePercentageCloserFiltering" }, + { text: "Contact Hardening Shadow", value: "useContactHardeningShadow" }, + ]), + ]} + /> + + {generator.usePoissonSampling && } + + {generator.usePercentageCloserFiltering && !generator.useContactHardeningShadow && ( + <> + updateLightShadowMapRefreshRate(this.props.light)} + /> + + )} + + {generator.useContactHardeningShadow && ( + updateLightShadowMapRefreshRate(this.props.light)} + /> + )} + + ); + } + + private _getCascadedShadowGeneratorComponent(): ReactNode { + const generator = this.state.generator; + + if (!generator || !isCascadedShadowGenerator(generator)) { + return null; + } + + return ( + <> + {this.props.children} + updateLightShadowMapRefreshRate(this.props.light)} + /> + updateLightShadowMapRefreshRate(this.props.light)} /> + { + this.forceUpdate(); + updateLightShadowMapRefreshRate(this.props.light); + }} + /> + {generator.autoCalcDepthBounds && ( + updateLightShadowMapRefreshRate(this.props.light)} + /> + )} + updateLightShadowMapRefreshRate(this.props.light)} + /> + updateLightShadowMapRefreshRate(this.props.light)} + /> + updateLightShadowMapRefreshRate(this.props.light)} + /> + + ); + } + + private _getSoftShadowType(generator: IShadowGenerator | null): SoftShadowType { + if (generator && (isShadowGenerator(generator) || isCascadedShadowGenerator(generator))) { + if (generator.usePercentageCloserFiltering) { + return "usePercentageCloserFiltering"; + } else if (generator.useContactHardeningShadow) { + return "useContactHardeningShadow"; + } + } + + return "none"; + } + + private _updateSoftShadowType(type: SoftShadowType): void { + if (this.state.generator && (isShadowGenerator(this.state.generator) || isCascadedShadowGenerator(this.state.generator))) { + this.state.generator.usePoissonSampling = false; + this.state.generator.useExponentialShadowMap = false; + this.state.generator.useBlurExponentialShadowMap = false; + this.state.generator.useCloseExponentialShadowMap = false; + this.state.generator.useBlurCloseExponentialShadowMap = false; + this.state.generator.usePercentageCloserFiltering = false; + this.state.generator.useContactHardeningShadow = false; + + this.state.generator[type] = true; + + this.forceUpdate(); + } + } +} diff --git a/editor/src/editor/layout/inspector/light/directional.tsx b/editor/src/editor/layout/inspector/light/directional.tsx index c2949a3ef..545ee2b09 100644 --- a/editor/src/editor/layout/inspector/light/directional.tsx +++ b/editor/src/editor/layout/inspector/light/directional.tsx @@ -18,8 +18,8 @@ import { EditorInspectorSectionField } from "../fields/section"; import { ScriptInspectorComponent } from "../script/script"; import { CustomMetadataInspector } from "../metadata/custom-metadata"; -import { EditorLightPBRInspector } from "./pbr"; -import { EditorLightShadowsInspector } from "./shadows"; +import { EditorLightPBRInspector } from "./components/pbr"; +import { EditorLightShadowsInspector } from "./components/shadows"; export class EditorDirectionalLightInspector extends Component> { /** @@ -84,7 +84,7 @@ export class EditorDirectionalLightInspector extends Component - + this.forceUpdate()} /> diff --git a/editor/src/editor/layout/inspector/light/point.tsx b/editor/src/editor/layout/inspector/light/point.tsx index 247af8d4a..c73f3933b 100644 --- a/editor/src/editor/layout/inspector/light/point.tsx +++ b/editor/src/editor/layout/inspector/light/point.tsx @@ -18,8 +18,9 @@ import { EditorInspectorSectionField } from "../fields/section"; import { ScriptInspectorComponent } from "../script/script"; import { CustomMetadataInspector } from "../metadata/custom-metadata"; -import { EditorLightPBRInspector } from "./pbr"; -import { EditorLightShadowsInspector } from "./shadows"; +import { EditorLightPBRInspector } from "./components/pbr"; +import { EditorLightClusterInspector } from "./components/cluster"; +import { EditorLightShadowsInspector } from "./components/shadows"; export class EditorPointLightInspector extends Component> { /** @@ -83,11 +84,12 @@ export class EditorPointLightInspector extends Component + - + this.forceUpdate()} /> diff --git a/editor/src/editor/layout/inspector/light/spot.tsx b/editor/src/editor/layout/inspector/light/spot.tsx index fb6236e0b..1c6ea8867 100644 --- a/editor/src/editor/layout/inspector/light/spot.tsx +++ b/editor/src/editor/layout/inspector/light/spot.tsx @@ -19,8 +19,9 @@ import { EditorInspectorSectionField } from "../fields/section"; import { ScriptInspectorComponent } from "../script/script"; import { CustomMetadataInspector } from "../metadata/custom-metadata"; -import { EditorLightPBRInspector } from "./pbr"; -import { EditorLightShadowsInspector } from "./shadows"; +import { EditorLightPBRInspector } from "./components/pbr"; +import { EditorLightClusterInspector } from "./components/cluster"; +import { EditorLightShadowsInspector } from "./components/shadows"; export class EditorSpotLightInspector extends Component> { /** @@ -96,6 +97,7 @@ export class EditorSpotLightInspector extends Component + @@ -109,7 +111,7 @@ export class EditorSpotLightInspector extends Component - + this.forceUpdate()}> diff --git a/editor/src/editor/layout/preview.tsx b/editor/src/editor/layout/preview.tsx index 2225324a3..b5a9a85d5 100644 --- a/editor/src/editor/layout/preview.tsx +++ b/editor/src/editor/layout/preview.tsx @@ -34,6 +34,8 @@ import { Color4, BoundingBox, SelectionOutlineLayer, + ClusteredLightContainer, + Tools, } from "babylonjs"; import { Button } from "../../ui/shadcn/ui/button"; @@ -174,10 +176,15 @@ export class EditorPreview extends Component { - if (!this._isInFrustrum(light.getAbsolutePosition(), scene)) { - return; + if (this._isInFrustrum(light.getAbsolutePosition(), scene) && !isClusteredLightContainer(light)) { + buttons.push({ + node: light, + position: projectVectorOnScreen(light.getAbsolutePosition(), scene), + }); } + }); - buttons.push({ - node: light, - position: projectVectorOnScreen(light.getAbsolutePosition(), scene), - }); + this.props.editor.layout.preview.clusteredLightContainer.lights.forEach((light) => { + if (this._isInFrustrum(light.getAbsolutePosition(), scene)) { + buttons.push({ + node: light, + position: projectVectorOnScreen(light.getAbsolutePosition(), scene), + }); + } }); scene.cameras.forEach((camera) => { @@ -131,14 +138,12 @@ export class EditorPreviewIcons extends Component { @@ -149,14 +154,12 @@ export class EditorPreviewIcons extends Component isEditorCamera(camera)); + const clusteredLightContainer = editor.layout.preview.clusteredLightContainer; if (scene.activeCamera) { saveRenderingConfigurationForCamera(scene.activeCamera); @@ -115,6 +117,7 @@ async function _exportProject(editor: Editor, options: IExportProjectOptions): P scene.lights.forEach((light) => (light.doNotSerialize = light.metadata?.doNotSerialize ?? false)); scene.cameras.forEach((camera) => (camera.doNotSerialize = camera.metadata?.doNotSerialize ?? false)); scene.transformNodes.forEach((transformNode) => (transformNode.doNotSerialize = transformNode.metadata?.doNotSerialize ?? false)); + clusteredLightContainer.lights.forEach((light) => (light.doNotSerialize = light.metadata?.doNotSerialize ?? false)); const data = await SceneSerializer.SerializeAsync(scene); @@ -122,21 +125,19 @@ async function _exportProject(editor: Editor, options: IExportProjectOptions): P scene.lights.forEach((light) => (light.doNotSerialize = false)); scene.cameras.forEach((camera) => (camera.doNotSerialize = false)); scene.transformNodes.forEach((transformNode) => (transformNode.doNotSerialize = false)); + clusteredLightContainer.lights.forEach((light) => (light.doNotSerialize = false)); const editorCameraIndex = data.cameras?.findIndex((camera) => camera.id === editorCamera?.id); if (editorCameraIndex !== -1) { data.cameras?.splice(editorCameraIndex, 1); } + const clusteredLightContainerIndex = data.lights?.findIndex((light) => light.id === clusteredLightContainer.id); + if (clusteredLightContainerIndex !== -1) { + data.lights?.splice(clusteredLightContainerIndex, 1); + } + data.metadata ??= {}; - data.metadata.rendering = { - taaRenderingPipeline: serializeTAARenderingPipeline(), - ssrRenderingPipeline: serializeSSRRenderingPipeline(), - motionBlurPostProcess: serializeMotionBlurPostProcess(), - ssao2RenderingPipeline: serializeSSAO2RenderingPipeline(), - defaultRenderingPipeline: serializeDefaultRenderingPipeline(), - vlsPostProcess: serializeVLSPostProcess(), - }; data.metadata.rendering = scene.cameras .filter((camera) => !isEditorCamera(camera)) @@ -160,6 +161,7 @@ async function _exportProject(editor: Editor, options: IExportProjectOptions): P configureMeshesLODs(data, scene); configureMeshesPhysics(data, scene); configureParticleSystems(data, scene); + configureClusteredLights(data, clusteredLightContainer); // Configure environment texture if (isHDRCubeTexture(scene.environmentTexture)) { diff --git a/editor/src/project/export/light.ts b/editor/src/project/export/light.ts new file mode 100644 index 000000000..27acbc92d --- /dev/null +++ b/editor/src/project/export/light.ts @@ -0,0 +1,11 @@ +import { ClusteredLightContainer } from "babylonjs"; + +export function configureClusteredLights(data: any, clusteredLightContainer: ClusteredLightContainer) { + clusteredLightContainer.lights.forEach((light) => { + if (!light.doNotSerialize) { + data.lights.push(light.serialize()); + } + }); + + data.metadata.clusteredLights = clusteredLightContainer.lights.map((light) => light.id); +} diff --git a/editor/src/project/load/scene.ts b/editor/src/project/load/scene.ts index de2770948..03492663e 100644 --- a/editor/src/project/load/scene.ts +++ b/editor/src/project/load/scene.ts @@ -297,6 +297,14 @@ export async function loadScene(editor: Editor, projectPath: string, scenePath: updatePointLightShadowMapRenderListPredicate(light); }); + // Configure clustered lights + config.clusteredLights?.forEach((lightId) => { + const light = scene.getLightById(lightId); + if (light) { + editor.layout.preview.clusteredLightContainer.addLight(light); + } + }); + // Configure LODs scene.meshes.forEach((mesh) => { if (!mesh._waitingData.lods || !isMesh(mesh)) { diff --git a/editor/src/project/save/scene.ts b/editor/src/project/save/scene.ts index 306228e32..76449e4ce 100644 --- a/editor/src/project/save/scene.ts +++ b/editor/src/project/save/scene.ts @@ -18,7 +18,7 @@ import { isSpriteManagerNode, isSpriteMapNode } from "../../tools/guards/sprites import { serializePhysicsAggregate } from "../../tools/physics/serialization/aggregate"; import { isAnimationGroupFromSceneLink, isFromSceneLink } from "../../tools/scene/scene-link"; import { isGPUParticleSystem, isNodeParticleSystemSetMesh, isParticleSystem } from "../../tools/guards/particles"; -import { isAnyTransformNode, isCollisionMesh, isEditorCamera, isMesh, isTransformNode } from "../../tools/guards/nodes"; +import { isAnyTransformNode, isClusteredLightContainer, isCollisionMesh, isEditorCamera, isMesh, isTransformNode } from "../../tools/guards/nodes"; import { taaPipelineCameraConfigurations } from "../../editor/rendering/taa"; import { vlsPostProcessCameraConfigurations } from "../../editor/rendering/vls"; @@ -390,8 +390,8 @@ export async function saveScene(editor: Editor, projectPath: string, scenePath: // Write lights await Promise.all( - scene.lights.map(async (light) => { - if (isFromSceneLink(light)) { + scene.lights.concat(editor.layout.preview.clusteredLightContainer.lights).map(async (light) => { + if (isFromSceneLink(light) || isClusteredLightContainer(light)) { return; } @@ -773,6 +773,7 @@ export async function saveScene(editor: Editor, projectPath: string, scenePath: uniqueId: undefined, }, animations: scene.animations.map((animation) => animation.serialize()), + clusteredLights: editor.layout.preview.clusteredLightContainer.lights.map((light) => light.id), }, { spaces: 4, diff --git a/editor/src/tools/guards/nodes.ts b/editor/src/tools/guards/nodes.ts index fd0a15006..36e90ba32 100644 --- a/editor/src/tools/guards/nodes.ts +++ b/editor/src/tools/guards/nodes.ts @@ -14,6 +14,7 @@ import { SpotLight, HemisphericLight, Skeleton, + ClusteredLightContainer, } from "babylonjs"; import { EditorCamera } from "../../editor/nodes/camera"; @@ -214,6 +215,14 @@ export function isLight(object: any): object is Light { return false; } +/** + * Returns wether or not the given object is a ClusteredLightContainer. + * @param object defines the reference to the object to test its class name. + */ +export function isClusteredLightContainer(object: any): object is ClusteredLightContainer { + return object.getClassName?.() === "ClusteredLightContainer"; +} + /** * Returns wether or not the given object is a Node. * @param object defines the reference to the object to test its class name. diff --git a/editor/src/tools/light/cluster.ts b/editor/src/tools/light/cluster.ts new file mode 100644 index 000000000..80f49f700 --- /dev/null +++ b/editor/src/tools/light/cluster.ts @@ -0,0 +1,7 @@ +import { Light } from "babylonjs"; + +import { Editor } from "../../editor/main"; + +export function isClusteredLight(light: Light, editor: Editor) { + return editor.layout.preview.clusteredLightContainer.lights.includes(light); +} diff --git a/editor/src/tools/node/clone.ts b/editor/src/tools/node/clone.ts index cbc27d700..347f261f5 100644 --- a/editor/src/tools/node/clone.ts +++ b/editor/src/tools/node/clone.ts @@ -13,9 +13,11 @@ import { UniqueNumber } from "../tools"; import { cloneSprite } from "../sprite/tools"; +import { isClusteredLight } from "../light/cluster"; + import { isTexture } from "../guards/texture"; -import { isAnyParticleSystem, isNodeParticleSystemSetMesh } from "../guards/particles"; import { isSprite, isSpriteManagerNode, isSpriteMapNode } from "../guards/sprites"; +import { isAnyParticleSystem, isNodeParticleSystemSetMesh } from "../guards/particles"; import { isCamera, isInstancedMesh, isLight, isMesh, isNode, isTransformNode } from "../guards/nodes"; import { isNodeVisibleInGraph } from "./metadata"; @@ -41,7 +43,12 @@ export function cloneNode(editor: Editor, node: Node | Sprite | ParticleSystem | clonePhysicsImpostor: true, cloneThinInstances: options?.cloneThinInstances ?? true, }); - } else if (isLight(node) || isCamera(node)) { + } else if (isLight(node)) { + clone = node.clone(name, node.parent); + if (isClusteredLight(node, editor) && isLight(clone)) { + editor.layout.preview.clusteredLightContainer.addLight(clone); + } + } else if (isCamera(node)) { clone = node.clone(name, node.parent); } else if (isTransformNode(node) || isInstancedMesh(node)) { clone = node.clone(name, node.parent, false); diff --git a/editor/test/tools/node/clone.test.mts b/editor/test/tools/node/clone.test.mts index 42279aa83..0ce4821cb 100644 --- a/editor/test/tools/node/clone.test.mts +++ b/editor/test/tools/node/clone.test.mts @@ -1,6 +1,6 @@ import { describe, expect, test, beforeEach, afterEach, vi } from "vitest"; -import { NullEngine, Scene, Mesh, DirectionalLight, Vector3, FreeCamera, TransformNode, InstancedMesh, Skeleton } from "babylonjs"; +import { NullEngine, Scene, Mesh, DirectionalLight, Vector3, FreeCamera, TransformNode, InstancedMesh, Skeleton, ClusteredLightContainer, PointLight } from "babylonjs"; vi.mock("babylonjs-editor-tools", () => ({})); @@ -20,9 +20,14 @@ describe("tools/node/clone", () => { layout: { preview: { scene, + clusteredLightContainer: new ClusteredLightContainer("clusteredLightContainer", [], scene), }, }, } as any; + + editor.layout.preview.clusteredLightContainer.addLight = vi.fn(function (light) { + this._lights.push(light); + }); }); afterEach(() => { @@ -79,5 +84,23 @@ describe("tools/node/clone", () => { expect(treeClone.getDescendants()[0]).not.toBe(childTree); expect(treeClone.getDescendants()[0].name).toBe("testChildTree"); }); + + test("should clone light", () => { + const light = new PointLight("testLight", new Vector3(0, 1, 0), scene); + const clone = cloneNode(editor, light); + + expect(clone).toBeDefined(); + expect(editor.layout.preview.clusteredLightContainer.lights.includes(clone as PointLight)).toBe(false); + }); + + test("should clone clustered light", () => { + const light = new PointLight("testLight", new Vector3(0, 1, 0), scene); + editor.layout.preview.clusteredLightContainer.addLight(light); + + const clone = cloneNode(editor, light); + + expect(clone).toBeDefined(); + expect(editor.layout.preview.clusteredLightContainer.lights.includes(clone as PointLight)).toBe(true); + }); }); }); diff --git a/tools/src/loading/light.ts b/tools/src/loading/light.ts new file mode 100644 index 000000000..bab4830b6 --- /dev/null +++ b/tools/src/loading/light.ts @@ -0,0 +1,18 @@ +import { Scene } from "@babylonjs/core/scene"; +import { ClusteredLightContainer } from "@babylonjs/core/Lights/Clustered/clusteredLightContainer"; + +export function configureLights(scene: Scene, clusteredLightContainer?: ClusteredLightContainer) { + const clusteredLights = scene.metadata?.clusteredLights ?? []; + if (clusteredLights.length > 0) { + clusteredLightContainer ??= new ClusteredLightContainer("Clustered Light Container", [], scene); + } + + scene.metadata?.clusteredLights?.forEach((lightId: any) => { + const light = scene.getLightById(lightId); + if (light) { + clusteredLightContainer?.addLight(light); + } + }); + + return clusteredLightContainer; +} diff --git a/tools/src/loading/loader.ts b/tools/src/loading/loader.ts index add0eb89b..93dd36ec0 100644 --- a/tools/src/loading/loader.ts +++ b/tools/src/loading/loader.ts @@ -3,6 +3,7 @@ import { Vector3 } from "@babylonjs/core/Maths/math.vector"; import { Constants } from "@babylonjs/core/Engines/constants"; import { AppendSceneAsync } from "@babylonjs/core/Loading/sceneLoader"; import { SceneLoaderFlags } from "@babylonjs/core/Loading/sceneLoaderFlags"; +import { ClusteredLightContainer } from "@babylonjs/core/Lights/Clustered/clusteredLightContainer"; import { isMesh } from "../tools/guards"; import { configureShadowMapRefreshRate, configureShadowMapRenderListPredicate } from "../tools/light"; @@ -22,10 +23,11 @@ import { registerTextureParser } from "./texture"; import { registerShadowGeneratorParser } from "./shadows"; import { registerMorphTargetManagerParser } from "./morph-target-manager"; +import { configureLights } from "./light"; import { registerSpriteMapParser } from "./sprite-map"; +import { configureTransformNodes } from "./transform-node"; import { registerSpriteManagerParser } from "./sprite-manager"; import { registerNodeParticleSystemSetParser } from "./node-particle-system-set"; -import { configureTransformNodes } from "./transform-node"; /** * Defines the possible output type of a script. @@ -91,6 +93,13 @@ declare module "@babylonjs/core/scene" { } } +const sceneConfigurationMap: Map< + Scene, + { + clusteredLightContainer?: ClusteredLightContainer; + } +> = new Map(); + export async function loadScene(rootUrl: any, sceneFilename: string, scene: Scene, scriptsMap: ScriptMap, options?: SceneLoaderOptions) { scene.loadingQuality = options?.quality ?? "high"; @@ -108,6 +117,11 @@ export async function loadScene(rootUrl: any, sceneFilename: string, scene: Scen registerNodeParticleSystemSetParser(); + // Check configuration + const configuration = sceneConfigurationMap.get(scene) ?? {}; + sceneConfigurationMap.set(scene, configuration); + + // Append to the given scene await AppendSceneAsync(`${rootUrl}${sceneFilename}`, scene, { pluginExtension: ".babylon", onProgress: (event) => { @@ -193,4 +207,7 @@ export async function loadScene(rootUrl: any, sceneFilename: string, scene: Scen }); configureTransformNodes(scene); + + const clusteredLightContainer = configureLights(scene, configuration.clusteredLightContainer); + configuration.clusteredLightContainer = clusteredLightContainer; } From 6663ad984d144559b1331f6338f361a9face2374 Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Sun, 5 Apr 2026 16:12:16 +0200 Subject: [PATCH 05/21] fix: usage of HDR environment texture in CLI fix: hide clustered lights from their parent in graph component --- cli/src/pack/scene.mts | 8 ++++-- editor/src/editor/layout/graph.tsx | 32 +++++++++++++---------- editor/src/project/load/plugins/sounds.ts | 4 ++- editor/src/project/load/scene.ts | 14 +++++----- 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/cli/src/pack/scene.mts b/cli/src/pack/scene.mts index 414879421..fb7ae6cc8 100644 --- a/cli/src/pack/scene.mts +++ b/cli/src/pack/scene.mts @@ -474,7 +474,11 @@ export async function createBabylonScene(options: ICreateBabylonSceneOptions) { physicsGravity: options.config.physics.gravity, physicsEngine: "HavokPlugin", - metadata: options.config.metadata, + metadata: { + ...options.config.metadata, + rendering: options.config.rendering, + clusteredLights: options.config.clusteredLights, + }, morphTargetManagers, lights, @@ -540,7 +544,7 @@ export async function createBabylonScene(options: ICreateBabylonSceneOptions) { }); // Configue ennviornment texture - if (scene.environmentTexture) { + if (scene.environmentTexture?.name && scene.environmentTexture.customType === "BABYLON.HDRCubeTexture") { scene.environmentTextureSize = 512; scene.environmentTextureType = "BABYLON.HDRCubeTexture"; scene.environmentTextureRotationY = scene.environmentTexture.rotationY; diff --git a/editor/src/editor/layout/graph.tsx b/editor/src/editor/layout/graph.tsx index 892072061..a46626e1c 100644 --- a/editor/src/editor/layout/graph.tsx +++ b/editor/src/editor/layout/graph.tsx @@ -37,6 +37,7 @@ import { registerUndoRedo } from "../../tools/undoredo"; import { isDomTextInputFocused } from "../../tools/dom"; import { isSceneLinkNode } from "../../tools/guards/scene"; import { updateAllLights } from "../../tools/light/shadows"; +import { isClusteredLight } from "../../tools/light/cluster"; import { getCollisionMeshFor } from "../../tools/mesh/collision"; import { isNodeVisibleInGraph } from "../../tools/node/metadata"; import { isAdvancedDynamicTexture } from "../../tools/guards/texture"; @@ -527,17 +528,17 @@ export class EditorGraph extends Component return; } - const sourcePosition = this._nodeToCopyTransform["position"]; - const sourceRotation = this._nodeToCopyTransform["rotation"]; - const sourceScaling = this._nodeToCopyTransform["scaling"]; - const sourceRotationQuaternion = this._nodeToCopyTransform["rotationQuaternion"]; - const sourceDirection = this._nodeToCopyTransform["direction"]; + const sourcePosition = (this._nodeToCopyTransform as any)["position"]; + const sourceRotation = (this._nodeToCopyTransform as any)["rotation"]; + const sourceScaling = (this._nodeToCopyTransform as any)["scaling"]; + const sourceRotationQuaternion = (this._nodeToCopyTransform as any)["rotationQuaternion"]; + const sourceDirection = (this._nodeToCopyTransform as any)["direction"]; - const targetPosition = node["position"]; - const targetRotation = node["rotation"]; - const targetScaling = node["scaling"]; - const targetRotationQuaternion = node["rotationQuaternion"]; - const targetDirection = node["direction"]; + const targetPosition = (node as any)["position"]; + const targetRotation = (node as any)["rotation"]; + const targetScaling = (node as any)["scaling"]; + const targetRotationQuaternion = (node as any)["rotationQuaternion"]; + const targetDirection = (node as any)["direction"]; const savedTargetPosition = targetPosition?.clone(); const savedTargetRotation = targetRotation?.clone(); @@ -562,7 +563,7 @@ export class EditorGraph extends Component if (targetRotationQuaternion) { if (!savedTargetRotationQuaternion) { - node["rotationQuaternion"] = null; + (node as any)["rotationQuaternion"] = null; } else { targetRotationQuaternion.copyFrom(savedTargetRotationQuaternion); } @@ -589,7 +590,7 @@ export class EditorGraph extends Component if (targetRotationQuaternion) { targetRotationQuaternion.copyFrom(sourceRotationQuaternion); } else { - node["rotationQuaternion"] = sourceRotationQuaternion.clone(); + (node as any)["rotationQuaternion"] = sourceRotationQuaternion.clone(); } } @@ -923,7 +924,7 @@ export class EditorGraph extends Component return null; } - if (isLight(node) && !node._scene.lights.includes(node) && !this.props.editor.layout.preview.clusteredLightContainer.lights.includes(node)) { + if (isLight(node) && !node._scene.lights.includes(node) && !isClusteredLight(node, this.props.editor)) { return null; } @@ -948,7 +949,10 @@ export class EditorGraph extends Component } as TreeNodeInfo; if (!isSceneLinkNode(node) && !noChildren) { - const children = node.getDescendants(true); + const children = isClusteredLightContainer(node) + ? node.getDescendants(true) + : node.getDescendants(true, (n) => !(isLight(n) && isClusteredLight(n, this.props.editor))); + if (children.length) { info.childNodes = children.map((c) => this._parseSceneNode(c)).filter((c) => c !== null) as TreeNodeInfo[]; } diff --git a/editor/src/project/load/plugins/sounds.ts b/editor/src/project/load/plugins/sounds.ts index 77cfaf3d4..76ad5b7ea 100644 --- a/editor/src/project/load/plugins/sounds.ts +++ b/editor/src/project/load/plugins/sounds.ts @@ -38,7 +38,9 @@ export async function loadSounds(editor: Editor, soundFiles: string[], scene: Sc return sound; } catch (e) { - editor.layout.console.error(`Failed to load sound file "${file}": ${e.message}`); + if (e instanceof Error) { + editor.layout.console.error(`Failed to load sound file "${file}": ${e.message}`); + } } options.progress.step(options.progressStep); diff --git a/editor/src/project/load/scene.ts b/editor/src/project/load/scene.ts index 03492663e..b6c51849c 100644 --- a/editor/src/project/load/scene.ts +++ b/editor/src/project/load/scene.ts @@ -298,7 +298,7 @@ export async function loadScene(editor: Editor, projectPath: string, scenePath: }); // Configure clustered lights - config.clusteredLights?.forEach((lightId) => { + config.clusteredLights?.forEach((lightId: any) => { const light = scene.getLightById(lightId); if (light) { editor.layout.preview.clusteredLightContainer.addLight(light); @@ -322,13 +322,13 @@ export async function loadScene(editor: Editor, projectPath: string, scenePath: // Scene animations scene.animations ??= []; - config.animations?.forEach((data) => { + config.animations?.forEach((data: any) => { scene.animations.push(Animation.Parse(data)); }); // Scene animation groups // TODO: legacy - config.animationGroups?.forEach((data) => { + config.animationGroups?.forEach((data: any) => { const group = AnimationGroup.Parse(data, scene); if (group.targetedAnimations.length === 0) { group.dispose(); @@ -364,7 +364,9 @@ export async function loadScene(editor: Editor, projectPath: string, scenePath: loadResult.sceneLinks.push(sceneLink); } } catch (e) { - editor.layout.console.error(`Failed to load scene link file "${file}": ${e.message}`); + if (e instanceof Error) { + editor.layout.console.error(`Failed to load scene link file "${file}": ${e.message}`); + } } progress.step(progressStep); @@ -373,7 +375,7 @@ export async function loadScene(editor: Editor, projectPath: string, scenePath: loadedScenes.pop(); // Configure waiting parent ids. - const allNodes = [...scene.transformNodes, ...scene.meshes, ...scene.lights, ...scene.cameras]; + const allNodes = [...scene.transformNodes, ...scene.meshes, ...scene.lights, ...scene.cameras, ...editor.layout.preview.clusteredLightContainer.lights]; allNodes.forEach((n) => { if ((n.metadata?._waitingParentId ?? null) === null) { @@ -415,7 +417,7 @@ export async function loadScene(editor: Editor, projectPath: string, scenePath: // For each camera const postProcessConfigurations = Array.isArray(config.rendering) ? config.rendering : []; - postProcessConfigurations.forEach((configuration) => { + postProcessConfigurations.forEach((configuration: any) => { const camera = scene.getCameraById(configuration.cameraId); if (!camera) { return; From 5f4967cb0ce501639bbce20c82a00a66707c220e Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Sun, 5 Apr 2026 16:35:27 +0200 Subject: [PATCH 06/21] v5.4.1-alpha.0 --- cli/package.json | 2 +- editor/package.json | 2 +- tools/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/package.json b/cli/package.json index 269b87552..12171e6ba 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "babylonjs-editor-cli", - "version": "5.4.0", + "version": "5.4.1-alpha.0", "description": "Babylon.js Editor CLI is a command line interface to help you package your scenes made using the Babylon.js Editor", "productName": "Babylon.js Editor CLI", "scripts": { diff --git a/editor/package.json b/editor/package.json index 4d819384f..5db61ef9d 100644 --- a/editor/package.json +++ b/editor/package.json @@ -1,6 +1,6 @@ { "name": "babylonjs-editor", - "version": "5.4.0", + "version": "5.4.1-alpha.0", "description": "Babylon.js Editor is a Web Application helping artists to work with Babylon.js", "productName": "Babylon.js Editor", "main": "build/src/index.js", diff --git a/tools/package.json b/tools/package.json index a8258ee7c..637ec5ced 100644 --- a/tools/package.json +++ b/tools/package.json @@ -1,6 +1,6 @@ { "name": "babylonjs-editor-tools", - "version": "5.4.0", + "version": "5.4.1-alpha.0", "description": "Babylon.js Editor Tools is a set of tools to help you create, edit and manage your Babylon.js scenes made using the Babylon.js Editor", "productName": "Babylon.js Editor Tools", "scripts": { From 033c23a764c4bc2b7dfdb37a94d4d2ec5ed1d7a0 Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Sun, 5 Apr 2026 19:40:49 +0200 Subject: [PATCH 07/21] feat: make clustered light container editable in inspector --- cli/src/pack/scene.mts | 2 +- editor/src/editor/layout/inspector.tsx | 2 ++ .../inspector/light/clustered-container.tsx | 34 +++++++++++++++++++ editor/src/project/export/light.ts | 10 +++++- editor/src/project/load/scene.ts | 14 ++++++++ editor/src/project/save/scene.ts | 8 ++++- tools/src/loading/light.ts | 26 ++++++++------ 7 files changed, 83 insertions(+), 13 deletions(-) create mode 100644 editor/src/editor/layout/inspector/light/clustered-container.tsx diff --git a/cli/src/pack/scene.mts b/cli/src/pack/scene.mts index fb7ae6cc8..facd57704 100644 --- a/cli/src/pack/scene.mts +++ b/cli/src/pack/scene.mts @@ -477,7 +477,7 @@ export async function createBabylonScene(options: ICreateBabylonSceneOptions) { metadata: { ...options.config.metadata, rendering: options.config.rendering, - clusteredLights: options.config.clusteredLights, + clusteredLight: options.config.clusteredLight, }, morphTargetManagers, diff --git a/editor/src/editor/layout/inspector.tsx b/editor/src/editor/layout/inspector.tsx index aec595d58..1975426d6 100644 --- a/editor/src/editor/layout/inspector.tsx +++ b/editor/src/editor/layout/inspector.tsx @@ -29,6 +29,7 @@ import { EditorSpotLightInspector } from "./inspector/light/spot"; import { EditorPointLightInspector } from "./inspector/light/point"; import { EditorDirectionalLightInspector } from "./inspector/light/directional"; import { EditorHemisphericLightInspector } from "./inspector/light/hemispheric"; +import { EditorClusteredLightContainerInspector } from "./inspector/light/clustered-container"; import { EditorCameraInspector } from "./inspector/camera/editor"; import { EditorFreeCameraInspector } from "./inspector/camera/free"; @@ -77,6 +78,7 @@ export class EditorInspector extends Component> { + /** + * Returns whether or not the given object is supported by this inspector. + * @param object defines the object to check. + * @returns true if the object is supported by this inspector. + */ + public static IsSupported(object: unknown): boolean { + return isClusteredLightContainer(object); + } + + public render(): ReactNode { + return ( + <> + + + + + + + + ); + } +} diff --git a/editor/src/project/export/light.ts b/editor/src/project/export/light.ts index 27acbc92d..3e03f7589 100644 --- a/editor/src/project/export/light.ts +++ b/editor/src/project/export/light.ts @@ -7,5 +7,13 @@ export function configureClusteredLights(data: any, clusteredLightContainer: Clu } }); - data.metadata.clusteredLights = clusteredLightContainer.lights.map((light) => light.id); + if (clusteredLightContainer.lights.length > 0) { + data.metadata.clusteredLight = { + horizontalTiles: clusteredLightContainer.horizontalTiles, + verticalTiles: clusteredLightContainer.verticalTiles, + depthSlices: clusteredLightContainer.depthSlices, + maxRange: clusteredLightContainer.maxRange, + lights: clusteredLightContainer.lights.map((light) => light.id), + }; + } } diff --git a/editor/src/project/load/scene.ts b/editor/src/project/load/scene.ts index b6c51849c..634235ca5 100644 --- a/editor/src/project/load/scene.ts +++ b/editor/src/project/load/scene.ts @@ -298,6 +298,20 @@ export async function loadScene(editor: Editor, projectPath: string, scenePath: }); // Configure clustered lights + if (config.clusteredLight) { + config.clusteredLight.lights.forEach((lightId: any) => { + const light = scene.getLightById(lightId); + if (light) { + editor.layout.preview.clusteredLightContainer.addLight(light); + } + }); + + editor.layout.preview.clusteredLightContainer.horizontalTiles = config.clusteredLight.horizontalTiles; + editor.layout.preview.clusteredLightContainer.verticalTiles = config.clusteredLight.verticalTiles; + editor.layout.preview.clusteredLightContainer.depthSlices = config.clusteredLight.depthSlices; + editor.layout.preview.clusteredLightContainer.maxRange = config.clusteredLight.maxRange; + } + config.clusteredLights?.forEach((lightId: any) => { const light = scene.getLightById(lightId); if (light) { diff --git a/editor/src/project/save/scene.ts b/editor/src/project/save/scene.ts index 76449e4ce..2a1dba576 100644 --- a/editor/src/project/save/scene.ts +++ b/editor/src/project/save/scene.ts @@ -773,7 +773,13 @@ export async function saveScene(editor: Editor, projectPath: string, scenePath: uniqueId: undefined, }, animations: scene.animations.map((animation) => animation.serialize()), - clusteredLights: editor.layout.preview.clusteredLightContainer.lights.map((light) => light.id), + clusteredLight: { + maxRange: editor.layout.preview.clusteredLightContainer.maxRange, + depthSlices: editor.layout.preview.clusteredLightContainer.depthSlices, + verticalTiles: editor.layout.preview.clusteredLightContainer.verticalTiles, + horizontalTiles: editor.layout.preview.clusteredLightContainer.horizontalTiles, + lights: editor.layout.preview.clusteredLightContainer.lights.map((light) => light.id), + }, }, { spaces: 4, diff --git a/tools/src/loading/light.ts b/tools/src/loading/light.ts index bab4830b6..3c9773d9e 100644 --- a/tools/src/loading/light.ts +++ b/tools/src/loading/light.ts @@ -2,17 +2,23 @@ import { Scene } from "@babylonjs/core/scene"; import { ClusteredLightContainer } from "@babylonjs/core/Lights/Clustered/clusteredLightContainer"; export function configureLights(scene: Scene, clusteredLightContainer?: ClusteredLightContainer) { - const clusteredLights = scene.metadata?.clusteredLights ?? []; - if (clusteredLights.length > 0) { - clusteredLightContainer ??= new ClusteredLightContainer("Clustered Light Container", [], scene); - } - - scene.metadata?.clusteredLights?.forEach((lightId: any) => { - const light = scene.getLightById(lightId); - if (light) { - clusteredLightContainer?.addLight(light); + const clusteredLight = scene.metadata?.clusteredLight; + if (clusteredLight) { + if (clusteredLight.lights.length > 0) { + clusteredLightContainer ??= new ClusteredLightContainer("Clustered Light Container", [], scene); + clusteredLightContainer.horizontalTiles = clusteredLight.horizontalTiles; + clusteredLightContainer.verticalTiles = clusteredLight.verticalTiles; + clusteredLightContainer.depthSlices = clusteredLight.depthSlices; + clusteredLightContainer.maxRange = clusteredLight.maxRange; } - }); + + clusteredLight.lights.forEach((lightId: any) => { + const light = scene.getLightById(lightId); + if (light) { + clusteredLightContainer?.addLight(light); + } + }); + } return clusteredLightContainer; } From 5fb8d434396987c641b211626bf4aaf5a58dfbdf Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Mon, 6 Apr 2026 15:15:50 +0200 Subject: [PATCH 08/21] v5.4.1-alpha.1 --- cli/package.json | 2 +- editor/package.json | 2 +- tools/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/package.json b/cli/package.json index 12171e6ba..a424dc577 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "babylonjs-editor-cli", - "version": "5.4.1-alpha.0", + "version": "5.4.1-alpha.1", "description": "Babylon.js Editor CLI is a command line interface to help you package your scenes made using the Babylon.js Editor", "productName": "Babylon.js Editor CLI", "scripts": { diff --git a/editor/package.json b/editor/package.json index 5db61ef9d..613cafe1a 100644 --- a/editor/package.json +++ b/editor/package.json @@ -1,6 +1,6 @@ { "name": "babylonjs-editor", - "version": "5.4.1-alpha.0", + "version": "5.4.1-alpha.1", "description": "Babylon.js Editor is a Web Application helping artists to work with Babylon.js", "productName": "Babylon.js Editor", "main": "build/src/index.js", diff --git a/tools/package.json b/tools/package.json index 637ec5ced..72f13f1de 100644 --- a/tools/package.json +++ b/tools/package.json @@ -1,6 +1,6 @@ { "name": "babylonjs-editor-tools", - "version": "5.4.1-alpha.0", + "version": "5.4.1-alpha.1", "description": "Babylon.js Editor Tools is a set of tools to help you create, edit and manage your Babylon.js scenes made using the Babylon.js Editor", "productName": "Babylon.js Editor Tools", "scripts": { From 83a7c0f07adc294ed220a5a03125f54c0e60a18d Mon Sep 17 00:00:00 2001 From: Julien Moreau-Mathis Date: Wed, 8 Apr 2026 11:40:50 +0200 Subject: [PATCH 09/21] fix: compute clustered light before checking scene is ready in babylonjs-editor-tools --- editor/src/project/load/scene.ts | 7 ----- tools/src/cinematic/parse.ts | 5 ++-- tools/src/decorators/apply.ts | 5 ++-- tools/src/loading/loader.ts | 9 +++--- tools/src/tools/guards.ts | 9 ++++++ tools/src/tools/scene.ts | 51 ++++++++++++++++++++++++++++++++ 6 files changed, 71 insertions(+), 15 deletions(-) create mode 100644 tools/src/tools/scene.ts diff --git a/editor/src/project/load/scene.ts b/editor/src/project/load/scene.ts index 634235ca5..aea0ab185 100644 --- a/editor/src/project/load/scene.ts +++ b/editor/src/project/load/scene.ts @@ -312,13 +312,6 @@ export async function loadScene(editor: Editor, projectPath: string, scenePath: editor.layout.preview.clusteredLightContainer.maxRange = config.clusteredLight.maxRange; } - config.clusteredLights?.forEach((lightId: any) => { - const light = scene.getLightById(lightId); - if (light) { - editor.layout.preview.clusteredLightContainer.addLight(light); - } - }); - // Configure LODs scene.meshes.forEach((mesh) => { if (!mesh._waitingData.lods || !isMesh(mesh)) { diff --git a/tools/src/cinematic/parse.ts b/tools/src/cinematic/parse.ts index 4538efb21..202f8b4b8 100644 --- a/tools/src/cinematic/parse.ts +++ b/tools/src/cinematic/parse.ts @@ -6,6 +6,7 @@ import { Quaternion, Vector2, Vector3, Matrix } from "@babylonjs/core/Maths/math import { getDefaultRenderingPipeline } from "../rendering/default-pipeline"; +import { getNodeById } from "../tools/scene"; import { getSoundById } from "../tools/sound"; import { getAnimationTypeForObject } from "../tools/animation"; @@ -27,7 +28,7 @@ export function parseCinematic(data: ICinematic, scene: Scene): ICinematic { let animationType: number | null = null; if (track.node) { - node = scene.getNodeById(track.node); + node = getNodeById(track.node, scene); if (!node) { node = scene.particleSystems?.find((ps) => ps.id === track.node) ?? null; } @@ -66,7 +67,7 @@ export function parseCinematic(data: ICinematic, scene: Scene): ICinematic { result.data = { type: "set-enabled", value: event.data.value, - node: scene.getNodeById(event.data.node), + node: getNodeById(event.data.node, scene), }; break; case "apply-impulse": diff --git a/tools/src/decorators/apply.ts b/tools/src/decorators/apply.ts index 34e0070cd..ed6449c14 100644 --- a/tools/src/decorators/apply.ts +++ b/tools/src/decorators/apply.ts @@ -17,6 +17,7 @@ import { AdvancedDynamicTexture } from "@babylonjs/gui/2D/advancedDynamicTexture import type { AudioSceneComponent as _AudioSceneComponent } from "@babylonjs/core/Audio/audioSceneComponent"; import { getSoundById } from "../tools/sound"; +import { getNodeById, getNodeByName } from "../tools/scene"; import { copyAndParseRagdollConfiguration } from "../tools/ragdoll"; import { ISpriteAnimation, SpriteManagerNode } from "../tools/sprite"; import { isAbstractMesh, isNode, isSprite, isTransformNode } from "../tools/guards"; @@ -113,7 +114,7 @@ export function applyDecorators(scene: Scene, object: any, script: any, instance // @nodeFromScene ctor._NodesFromScene?.forEach((params) => { - instance[params.propertyKey.toString()] = scene.getNodeByName(params.nodeName); + instance[params.propertyKey.toString()] = getNodeByName(params.nodeName, scene); }); // @nodeFromDescendants @@ -203,7 +204,7 @@ export function applyDecorators(scene: Scene, object: any, script: any, instance const entityType = (params.configuration as VisibleInInspectorDecoratorEntityConfiguration).entityType; switch (entityType) { case "node": - instance[propertyKey] = scene.getNodeById(value) ?? null; + instance[propertyKey] = getNodeById(value, scene) ?? null; break; case "animationGroup": instance[propertyKey] = scene.getAnimationGroupByName(value) ?? null; diff --git a/tools/src/loading/loader.ts b/tools/src/loading/loader.ts index 93dd36ec0..b5d1ac6db 100644 --- a/tools/src/loading/loader.ts +++ b/tools/src/loading/loader.ts @@ -145,9 +145,13 @@ export async function loadScene(rootUrl: any, sceneFilename: string, scene: Scen scene.meshes.forEach((m) => isMesh(m) && m._checkDelayState()); } - const waitingItemsCount = scene.getWaitingItemsCount(); + // Configure clustered lights + const clusteredLightContainer = configureLights(scene, configuration.clusteredLightContainer); + configuration.clusteredLightContainer = clusteredLightContainer; // Wait until scene is ready. + const waitingItemsCount = scene.getWaitingItemsCount(); + while (!scene.isDisposed && (!scene.isReady() || scene.getWaitingItemsCount() > 0)) { await new Promise((resolve) => setTimeout(resolve, 150)); @@ -207,7 +211,4 @@ export async function loadScene(rootUrl: any, sceneFilename: string, scene: Scen }); configureTransformNodes(scene); - - const clusteredLightContainer = configureLights(scene, configuration.clusteredLightContainer); - configuration.clusteredLightContainer = clusteredLightContainer; } diff --git a/tools/src/tools/guards.ts b/tools/src/tools/guards.ts index 58dd7005d..e0e8ca8c1 100644 --- a/tools/src/tools/guards.ts +++ b/tools/src/tools/guards.ts @@ -19,6 +19,7 @@ import { SpotLight } from "@babylonjs/core/Lights/spotLight"; import { PointLight } from "@babylonjs/core/Lights/pointLight"; import { HemisphericLight } from "@babylonjs/core/Lights/hemisphericLight"; import { DirectionalLight } from "@babylonjs/core/Lights/directionalLight"; +import { ClusteredLightContainer } from "@babylonjs/core/Lights/Clustered/clusteredLightContainer"; import { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; import { IParticleSystem } from "@babylonjs/core/Particles/IParticleSystem"; @@ -185,6 +186,14 @@ export function isLight(object: any): object is Light { return false; } +/** + * Returns wether or not the given object is a ClusteredLightContainer. + * @param object defines the reference to the object to test its class name. + */ +export function isClusteredLightContainer(object: any): object is ClusteredLightContainer { + return object.getClassName?.() === "ClusteredLightContainer"; +} + /** * Returns wether or not the given object is a Node. * @param object defines the reference to the object to test its class name. diff --git a/tools/src/tools/scene.ts b/tools/src/tools/scene.ts new file mode 100644 index 000000000..27dd8add9 --- /dev/null +++ b/tools/src/tools/scene.ts @@ -0,0 +1,51 @@ +import { Scene } from "@babylonjs/core/scene"; + +import { isClusteredLightContainer } from "./guards"; + +/** + * Returns the node with the given name in the given scene. + * This method also retrieves light nodes from clustered light containers. + * @param name defines the name of the node to retrieve. + * @param scene defines the reference to the scene to search the node in. + * @returns the node if found, otherwise null. + */ +export function getNodeByName(name: string, scene: Scene) { + const node = scene.getNodeByName(name); + if (node) { + return node; + } + + const clusteredLightContainers = scene.lights.filter((light) => isClusteredLightContainer(light)); + for (const clusteredLightContainer of clusteredLightContainers) { + const lightNode = clusteredLightContainer.lights.find((light) => light.name === name); + if (lightNode) { + return lightNode; + } + } + + return null; +} + +/** + * Returns the node with the given id in the given scene. + * This method also retrieves light nodes from clustered light containers. + * @param id defines the id of the node to retrieve. + * @param scene defines the reference to the scene to search the node in. + * @returns the node if found, otherwise null. + */ +export function getNodeById(id: string, scene: Scene) { + const node = scene.getNodeById(id); + if (node) { + return node; + } + + const clusteredLightContainers = scene.lights.filter((light) => isClusteredLightContainer(light)); + for (const clusteredLightContainer of clusteredLightContainers) { + const lightNode = clusteredLightContainer.lights.find((light) => light.id === id); + if (lightNode) { + return lightNode; + } + } + + return null; +} From 553739645452109d8a2a471213995a2c592b819a Mon Sep 17 00:00:00 2001 From: Julien Moreau-Mathis Date: Wed, 8 Apr 2026 16:56:44 +0200 Subject: [PATCH 10/21] feat: add support of LOD inspector to setup custom LODs --- .../layout/inspector/material/multi.tsx | 1 + .../editor/layout/inspector/material/pbr.tsx | 8 +- .../src/editor/layout/inspector/mesh/lod.tsx | 244 ++++++++++++++++++ .../src/editor/layout/inspector/mesh/mesh.tsx | 48 +--- 4 files changed, 255 insertions(+), 46 deletions(-) create mode 100644 editor/src/editor/layout/inspector/mesh/lod.tsx diff --git a/editor/src/editor/layout/inspector/material/multi.tsx b/editor/src/editor/layout/inspector/material/multi.tsx index 0e5b31b7c..38ea5ca22 100644 --- a/editor/src/editor/layout/inspector/material/multi.tsx +++ b/editor/src/editor/layout/inspector/material/multi.tsx @@ -64,6 +64,7 @@ export class EditorMultiMaterialInspector extends Component {this.props.material.subMaterials.map((material, index) => ( { ev.preventDefault(); ev.currentTarget.classList.remove("bg-muted"); diff --git a/editor/src/editor/layout/inspector/material/pbr.tsx b/editor/src/editor/layout/inspector/material/pbr.tsx index 2bfbf06e8..a4536c609 100644 --- a/editor/src/editor/layout/inspector/material/pbr.tsx +++ b/editor/src/editor/layout/inspector/material/pbr.tsx @@ -246,7 +246,13 @@ export class EditorPBRMaterialInspector extends Component - this._handleSubSurfaceEnabledChange(v)} /> + this._handleSubSurfaceEnabledChange(v)} + /> {this.state.subSurfaceEnabled && ( <> diff --git a/editor/src/editor/layout/inspector/mesh/lod.tsx b/editor/src/editor/layout/inspector/mesh/lod.tsx new file mode 100644 index 000000000..49e526397 --- /dev/null +++ b/editor/src/editor/layout/inspector/mesh/lod.tsx @@ -0,0 +1,244 @@ +import { TreeNodeInfo } from "@blueprintjs/core"; +import { Component, DragEvent, ReactNode } from "react"; + +import { XMarkIcon } from "@heroicons/react/20/solid"; + +import { Mesh } from "babylonjs"; + +import { Button } from "../../../../ui/shadcn/ui/button"; + +import { Editor } from "../../../main"; + +import { isMesh } from "../../../../tools/guards/nodes"; +import { registerUndoRedo } from "../../../../tools/undoredo"; + +import { EditorInspectorNumberField } from "../fields/number"; +import { EditorInspectorSwitchField } from "../fields/switch"; +import { EditorInspectorSectionField } from "../fields/section"; + +export interface IMeshLODInspectorProps { + mesh: Mesh; + editor: Editor; +} + +export interface IIMeshLODInspectorState { + dragOver: boolean; + lodsEnabled: boolean; +} + +export class MeshLODInspector extends Component { + public constructor(props: IMeshLODInspectorProps) { + super(props); + + this.state = { + dragOver: false, + lodsEnabled: props.mesh.getLODLevels().length > 0, + }; + } + + public render(): ReactNode { + return ( + + this._handleLODsEnabledChange()} /> + {this.state.lodsEnabled && this._getLODsComponent()} + + ); + } + + private _handleLODsEnabledChange(): void { + const lods = this.props.mesh.getLODLevels().slice(); + + registerUndoRedo({ + executeRedo: true, + undo: () => lods.forEach((lod) => this.props.mesh.addLODLevel(lod.distanceOrScreenCoverage ?? 0, lod.mesh!)), + redo: () => lods.forEach((lod) => this.props.mesh.removeLODLevel(lod.mesh!)), + }); + + this.forceUpdate(); + this.props.editor.layout.graph.refresh(); + } + + private _getLODsComponent(): ReactNode { + const lods = this.props.mesh.getLODLevels(); + + const o = { + distance: this._getDistance(), + }; + + const sortLods = (value: number) => { + const lods = this.props.mesh.getLODLevels().slice(); + lods.forEach((lod) => this.props.mesh.removeLODLevel(lod.mesh!)); + + lods.reverse().forEach((lod, index) => { + this.props.mesh.addLODLevel(value * (index + 1), lod.mesh); + }); + }; + + return ( + <> + sortLods(v)} + onFinishChange={(value, oldValue) => { + registerUndoRedo({ + executeRedo: true, + undo: () => sortLods(oldValue), + redo: () => sortLods(value), + }); + }} + /> + + {lods.map((lod) => ( +
+
+
{lod.mesh?.name}
+
+ {lod.mesh?.geometry?.getTotalVertices() ?? 0} vertices, {lod.mesh?.geometry?.getTotalIndices() ?? 0} indices +
+
+ +
+ ))} + +
this._handleDrop(ev)} + onDragOver={(ev) => this._handleDragOver(ev)} + onDragLeave={() => this.setState({ dragOver: false })} + className={` + flex flex-col justify-center items-center w-full h-[64px] rounded-lg border-[1px] border-secondary-foreground/35 border-dashed font-semibold text-muted-foreground + ${this.state.dragOver ? "bg-secondary-foreground/35" : ""} + transition-all duration-300 ease-in-out + `} + > + Drop LOD meshes here to add them to the list of LODs. +
+ + ); + } + + private _handleDragOver(ev: DragEvent): void { + ev.preventDefault(); + ev.stopPropagation(); + + this.setState({ + dragOver: true, + }); + } + + private _handleDrop(ev: DragEvent): void { + ev.preventDefault(); + ev.stopPropagation(); + + this.setState({ + dragOver: false, + }); + + const node = ev.dataTransfer.getData("graph/node"); + if (!node) { + return; + } + + const nodesToMove = this.props.editor.layout.graph + .getSelectedNodes() + .filter((n) => n.nodeData && isMesh(n.nodeData) && n.nodeData !== this.props.mesh) as TreeNodeInfo[]; + + const savedNodesData = nodesToMove.map((n) => ({ + oldParent: n.nodeData!.parent, + oldPosition: n.nodeData!.position.clone(), + oldRotation: n.nodeData!.rotation.clone(), + oldScaling: n.nodeData!.scaling.clone(), + oldRotationQuaternion: n.nodeData!.rotationQuaternion?.clone(), + })); + + registerUndoRedo({ + executeRedo: true, + action: () => this._autoSortLODs(), + undo: () => { + nodesToMove.forEach((node, index) => { + const mesh = node.nodeData as Mesh; + this.props.mesh.removeLODLevel(mesh); + + const configuration = savedNodesData[index]; + mesh.parent = configuration.oldParent; + mesh.position.copyFrom(configuration.oldPosition); + mesh.rotation.copyFrom(configuration.oldRotation); + mesh.scaling.copyFrom(configuration.oldScaling); + if (configuration.oldRotationQuaternion) { + mesh.rotationQuaternion?.copyFrom(configuration.oldRotationQuaternion); + } + }); + }, + redo: () => { + nodesToMove.forEach((node) => { + const mesh = node.nodeData as Mesh; + this.props.mesh.addLODLevel(300, mesh); + + mesh.parent = null; + mesh.position.set(0, 0, 0); + mesh.rotation.set(0, 0, 0); + mesh.scaling.set(1, 1, 1); + mesh.rotationQuaternion?.set(0, 0, 0, 1); + }); + }, + }); + + this.forceUpdate(); + this.props.editor.layout.graph.refresh(); + } + + private _handleRemoveLOD(mesh: Mesh | null): void { + const lods = this.props.mesh.getLODLevels(); + const lodToRemove = lods.find((lod) => lod.mesh === mesh); + if (!lodToRemove) { + return; + } + + registerUndoRedo({ + executeRedo: true, + action: () => this._autoSortLODs(), + undo: () => this.props.mesh.addLODLevel(lodToRemove.distanceOrScreenCoverage ?? 0, lodToRemove.mesh!), + redo: () => this.props.mesh.removeLODLevel(lodToRemove.mesh!), + }); + + this.forceUpdate(); + this.props.editor.layout.graph.refresh(); + } + + private _getDistance(): number { + const lods = this.props.mesh.getLODLevels(); + return lods[lods.length - 1]?.distanceOrScreenCoverage ?? 1000; + } + + private _autoSortLODs(): void { + const lods = this.props.mesh.getLODLevels().slice(); + + const sortedLODs = lods.sort((a, b) => { + const aIndices = a.mesh?.geometry?.getIndices()?.length ?? Infinity; + const bIndices = b.mesh?.geometry?.getIndices()?.length ?? Infinity; + + return aIndices - bIndices; + }); + + lods.forEach((lod) => this.props.mesh.removeLODLevel(lod.mesh!)); + + const distance = this._getDistance(); + + sortedLODs.reverse().forEach((lod, index) => { + this.props.mesh.addLODLevel(distance * (index + 1), lod.mesh); + }); + } +} diff --git a/editor/src/editor/layout/inspector/mesh/mesh.tsx b/editor/src/editor/layout/inspector/mesh/mesh.tsx index 7731a58af..76dbd8efd 100644 --- a/editor/src/editor/layout/inspector/mesh/mesh.tsx +++ b/editor/src/editor/layout/inspector/mesh/mesh.tsx @@ -5,7 +5,7 @@ import { Component, ReactNode } from "react"; import { FaLink } from "react-icons/fa6"; import { AiOutlinePlus } from "react-icons/ai"; -import { AbstractMesh, InstancedMesh, Material, Mesh, MorphTarget, MultiMaterial, Node, Observer, PBRMaterial, StandardMaterial, NodeMaterial } from "babylonjs"; +import { AbstractMesh, InstancedMesh, Material, MorphTarget, MultiMaterial, Node, Observer, PBRMaterial, StandardMaterial, NodeMaterial } from "babylonjs"; import { SkyMaterial, GridMaterial, NormalMaterial, WaterMaterial, LavaMaterial, TriPlanarMaterial, CellMaterial, FireMaterial, GradientMaterial } from "babylonjs-materials"; import { CollisionMesh } from "../../../nodes/collision"; @@ -62,6 +62,7 @@ import { EditorGradientMaterialInspector } from "../material/gradient"; import { EditorStandardMaterialInspector } from "../material/standard"; import { EditorTriPlanarMaterialInspector } from "../material/tri-planar"; +import { MeshLODInspector } from "./lod"; import { MeshDecalInspector } from "./decal"; import { MeshGeometryInspector } from "./geometry"; import { EditorSkeletonInspector } from "./skeleton"; @@ -184,7 +185,7 @@ export class EditorMeshInspector extends Component - {this._getLODsComponent()} + )} @@ -234,49 +235,6 @@ export class EditorMeshInspector extends Component mesh.removeLODLevel(lod.mesh!)); - - lods.reverse().forEach((lod, index) => { - mesh.addLODLevel(value * (index + 1), lod.mesh); - }); - } - - return ( - - sortLods(v)} - onFinishChange={(value, oldValue) => { - registerUndoRedo({ - executeRedo: true, - undo: () => sortLods(oldValue), - redo: () => sortLods(value), - }); - }} - /> - - ); - } - private _getMaterialComponent(): ReactNode { if (!this.props.object.geometry) { return; From 498ab9ba2f9f344e8563315fbee3cb73a6218bb7 Mon Sep 17 00:00:00 2001 From: Cesharpe Date: Wed, 8 Apr 2026 17:42:25 +0200 Subject: [PATCH 11/21] fix: compute clustered light container after parenting is resolved --- .../inspector/light/components/cluster.tsx | 9 ++++-- editor/src/project/load/scene.ts | 30 +++++++++---------- editor/src/tools/material/material.ts | 2 +- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/editor/src/editor/layout/inspector/light/components/cluster.tsx b/editor/src/editor/layout/inspector/light/components/cluster.tsx index 77e7a03a2..d29f42dff 100644 --- a/editor/src/editor/layout/inspector/light/components/cluster.tsx +++ b/editor/src/editor/layout/inspector/light/components/cluster.tsx @@ -3,6 +3,7 @@ import { Light, ClusteredLightContainer } from "babylonjs"; import { Editor } from "../../../../main"; import { registerUndoRedo } from "../../../../../tools/undoredo"; +import { isClusteredLight } from "../../../../../tools/light/cluster"; import { EditorInspectorSwitchField } from "../../fields/switch"; @@ -12,7 +13,9 @@ export interface IEditorLightClusterInspectorProps { } export function EditorLightClusterInspector(props: IEditorLightClusterInspectorProps) { - props.light.metadata ??= {}; + const o = { + isClusteredLight: isClusteredLight(props.light, props.editor), + }; const isSupported = ClusteredLightContainer.IsLightSupported(props.light); @@ -20,8 +23,8 @@ export function EditorLightClusterInspector(props: IEditorLightClusterInspectorP <> { - const light = scene.getLightById(lightId); - if (light) { - editor.layout.preview.clusteredLightContainer.addLight(light); - } - }); - - editor.layout.preview.clusteredLightContainer.horizontalTiles = config.clusteredLight.horizontalTiles; - editor.layout.preview.clusteredLightContainer.verticalTiles = config.clusteredLight.verticalTiles; - editor.layout.preview.clusteredLightContainer.depthSlices = config.clusteredLight.depthSlices; - editor.layout.preview.clusteredLightContainer.maxRange = config.clusteredLight.maxRange; - } - // Configure LODs scene.meshes.forEach((mesh) => { if (!mesh._waitingData.lods || !isMesh(mesh)) { @@ -410,6 +395,21 @@ export async function loadScene(editor: Editor, projectPath: string, scenePath: } }); + // Configure clustered lights + if (config.clusteredLight) { + config.clusteredLight.lights.forEach((lightId: any) => { + const light = scene.getLightById(lightId); + if (light) { + editor.layout.preview.clusteredLightContainer.addLight(light); + } + }); + + editor.layout.preview.clusteredLightContainer.horizontalTiles = config.clusteredLight.horizontalTiles; + editor.layout.preview.clusteredLightContainer.verticalTiles = config.clusteredLight.verticalTiles; + editor.layout.preview.clusteredLightContainer.depthSlices = config.clusteredLight.depthSlices; + editor.layout.preview.clusteredLightContainer.maxRange = config.clusteredLight.maxRange; + } + if (!options?.asLink) { allNodes.forEach((n) => { if (n.metadata) { diff --git a/editor/src/tools/material/material.ts b/editor/src/tools/material/material.ts index e9b2882b9..9ef93b256 100644 --- a/editor/src/tools/material/material.ts +++ b/editor/src/tools/material/material.ts @@ -8,7 +8,7 @@ import { isNodeMaterial, isPBRMaterial, isStandardMaterial } from "../guards/mat */ export function configureSimultaneousLightsForMaterial(material: Material) { if (isPBRMaterial(material) || isStandardMaterial(material) || isNodeMaterial(material)) { - material.maxSimultaneousLights = 32; + material.maxSimultaneousLights = 8; } } From e9fc3c4b8f215d75eb1aa1afd0fc117bce9cb039 Mon Sep 17 00:00:00 2001 From: Cesharpe Date: Wed, 8 Apr 2026 17:46:22 +0200 Subject: [PATCH 12/21] chore: use installed typescript version --- .vscode/settings.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index babebec02..0d2999845 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -34,5 +34,7 @@ "editor.defaultFormatter": "esbenp.prettier-vscode" }, "js/ts.preferences.importModuleSpecifier": "project-relative", - "js/ts.preferences.autoImportFileExcludePatterns": ["**/export.ts"] + "js/ts.preferences.autoImportFileExcludePatterns": ["**/export.ts"], + "js/ts.tsdk.path": "node_modules/typescript/lib", + "js/ts.tsdk.promptToUseWorkspaceVersion": true } From 5db51a672ab8f66b5126e82188eb4ca84093ec6b Mon Sep 17 00:00:00 2001 From: Julien Moreau-Mathis Date: Thu, 9 Apr 2026 15:37:57 +0200 Subject: [PATCH 13/21] feat: drag'n'drop lights in clustered light container --- editor/src/editor/layout/graph.tsx | 29 ++-- .../graph/{graph.tsx => context-menu.tsx} | 0 editor/src/editor/layout/graph/label.tsx | 132 +-------------- editor/src/editor/layout/graph/move.ts | 159 ++++++++++++++++++ editor/src/editor/layout/preview.tsx | 2 +- 5 files changed, 178 insertions(+), 144 deletions(-) rename editor/src/editor/layout/graph/{graph.tsx => context-menu.tsx} (100%) create mode 100644 editor/src/editor/layout/graph/move.ts diff --git a/editor/src/editor/layout/graph.tsx b/editor/src/editor/layout/graph.tsx index a46626e1c..d09a9029a 100644 --- a/editor/src/editor/layout/graph.tsx +++ b/editor/src/editor/layout/graph.tsx @@ -11,8 +11,8 @@ import { MdOutlineQuestionMark } from "react-icons/md"; import { GiBrickWall, GiSparkles } from "react-icons/gi"; import { HiOutlineCubeTransparent } from "react-icons/hi"; import { IoCheckmark, IoSparklesSharp } from "react-icons/io5"; -import { FaCamera, FaImage, FaLightbulb, FaBone } from "react-icons/fa"; import { TbGhost2Filled, TbServerSpark, TbBrandAdobeIndesign } from "react-icons/tb"; +import { FaCamera, FaImage, FaLightbulb, FaBone, FaRegLightbulb } from "react-icons/fa"; import { AdvancedDynamicTexture } from "babylonjs-gui"; import { BaseTexture, Node, Scene, Sound, Tools, IParticleSystem, Sprite, Skeleton, TransformNode } from "babylonjs"; @@ -82,7 +82,8 @@ import { getSpriteCommands } from "../dialogs/command-palette/sprite"; import { onProjectConfigurationChangedObservable } from "../../project/configuration"; import { EditorGraphLabel } from "./graph/label"; -import { EditorGraphContextMenu } from "./graph/graph"; +import { EditorGraphContextMenu } from "./graph/context-menu"; +import { setNewParentForGraphSelectedNodes } from "./graph/move"; export interface IEditorGraphProps { /** @@ -290,6 +291,7 @@ export class EditorGraph extends Component */ public refresh(): Promise { const scene = this.props.editor.layout.preview.scene; + const clusteredLightContainer = this.props.editor.layout.preview.clusteredLightContainer; this._soundsList = scene.soundTracks?.map((st) => st.soundCollection).flat() ?? []; @@ -297,7 +299,7 @@ export class EditorGraph extends Component if (this.state.showOnlyLights || this.state.showOnlyDecals) { if (this.state.showOnlyLights) { - nodes.push(...scene.lights.map((light) => this._parseSceneNode(light, true))); + nodes.push(...scene.lights.concat(clusteredLightContainer.lights).map((light) => this._parseSceneNode(light, true))); } if (this.state.showOnlyDecals) { @@ -932,7 +934,7 @@ export class EditorGraph extends Component return null; } - if (isClusteredLightContainer(node) && node.lights.length === 0) { + if (isClusteredLightContainer(node) && (this.state.showOnlyLights || this.state.showOnlyDecals)) { return null; } @@ -1029,7 +1031,7 @@ export class EditorGraph extends Component } selectedNodeData.forEach((node) => { - if (isNode(node)) { + if (isNode(node) || isClusteredLightContainer(node)) { node.setEnabled(enabled); } }); @@ -1080,10 +1082,14 @@ export class EditorGraph extends Component return ; } - if (isLight(object) || isClusteredLightContainer(object)) { + if (isLight(object)) { return ; } + if (isClusteredLightContainer(object)) { + return ; + } + if (isCamera(object)) { return ; } @@ -1153,15 +1159,6 @@ export class EditorGraph extends Component return; } - const nodesToMove: TreeNodeInfo[] = []; - this._forEachNode(this.state.nodes, (n) => n.isSelected && nodesToMove.push(n)); - - nodesToMove.forEach((n) => { - if (n.nodeData && isNode(n.nodeData)) { - n.nodeData.parent = null; - } - }); - - this.refresh(); + setNewParentForGraphSelectedNodes(this.props.editor, this.props.editor.layout.preview.scene, ev.shiftKey); } } diff --git a/editor/src/editor/layout/graph/graph.tsx b/editor/src/editor/layout/graph/context-menu.tsx similarity index 100% rename from editor/src/editor/layout/graph/graph.tsx rename to editor/src/editor/layout/graph/context-menu.tsx diff --git a/editor/src/editor/layout/graph/label.tsx b/editor/src/editor/layout/graph/label.tsx index 8e533da37..37bc2e8b6 100644 --- a/editor/src/editor/layout/graph/label.tsx +++ b/editor/src/editor/layout/graph/label.tsx @@ -5,19 +5,13 @@ import { DragEvent, useEffect, useRef, useState } from "react"; import { FaLock } from "react-icons/fa"; import { useEventListener } from "usehooks-ts"; -import { TransformNode, AbstractMesh, Vector3 } from "babylonjs"; - import { Input } from "../../../ui/shadcn/ui/input"; import { isDarwin } from "../../../tools/os"; import { isScene } from "../../../tools/guards/scene"; -import { isSound } from "../../../tools/guards/sound"; import { registerUndoRedo } from "../../../tools/undoredo"; -import { isClusteredLight } from "../../../tools/light/cluster"; -import { isAnyParticleSystem } from "../../../tools/guards/particles"; import { isNodeSerializable, isNodeLocked } from "../../../tools/node/metadata"; -import { isAbstractMesh, isInstancedMesh, isLight, isMesh, isNode, isTransformNode } from "../../../tools/guards/nodes"; -import { applyNodeParentingConfiguration, applyTransformNodeParentingConfiguration, IOldNodeHierarchyConfiguration } from "../../../tools/node/parenting"; +import { isClusteredLightContainer, isInstancedMesh, isMesh, isNode } from "../../../tools/guards/nodes"; import { applySoundAsset } from "../preview/import/sound"; import { applyTextureAssetToObject } from "../preview/import/texture"; @@ -25,6 +19,8 @@ import { applyMaterialAssetToObject } from "../preview/import/material"; import { Editor } from "../../main"; +import { setNewParentForGraphSelectedNodes } from "./move"; + export interface IEditorGraphLabelProps { name: string; object: any; @@ -111,13 +107,13 @@ export function EditorGraphLabel(props: IEditorGraphLabelProps) { setOver(false); - if (!isNode(props.object) && !isScene(props.object)) { + if (!isNode(props.object) && !isScene(props.object) && !isClusteredLightContainer(props.object)) { return; } const node = ev.dataTransfer.getData("graph/node"); if (node) { - return dropNodeFromGraph(ev.shiftKey); + return setNewParentForGraphSelectedNodes(props.editor, props.object, ev.shiftKey); } const asset = ev.dataTransfer.getData("assets"); @@ -126,124 +122,6 @@ export function EditorGraphLabel(props: IEditorGraphLabelProps) { } } - function dropNodeFromGraph(shift: boolean) { - const nodesToMove = props.editor.layout.graph.getSelectedNodes(); - - const newParent = props.object; - const oldHierarchyMap = new Map(); - - nodesToMove.forEach((n) => { - if (n.nodeData && n.nodeData !== newParent) { - if (isLight(n.nodeData) && isClusteredLight(n.nodeData, props.editor)) { - return; - } - - if (isNode(n.nodeData) && n.nodeData.parent !== newParent) { - const descendants = n.nodeData.getDescendants(false); - if (descendants.includes(newParent)) { - return; - } - - return oldHierarchyMap.set(n.nodeData, { - parent: n.nodeData.parent, - position: n.nodeData["position"]?.clone(), - rotation: n.nodeData["rotation"]?.clone(), - scaling: n.nodeData["scaling"]?.clone(), - rotationQuaternion: n.nodeData["rotationQuaternion"]?.clone(), - } as IOldNodeHierarchyConfiguration); - } - - if (isSound(n.nodeData)) { - return oldHierarchyMap.set(n.nodeData, n.nodeData["_connectedTransformNode"]); - } - - if (isAnyParticleSystem(n.nodeData)) { - return oldHierarchyMap.set(n.nodeData, n.nodeData.emitter); - } - } - }); - - if (!oldHierarchyMap.size) { - return; - } - - registerUndoRedo({ - executeRedo: true, - undo: () => { - nodesToMove.forEach((n) => { - if (n.nodeData && oldHierarchyMap.has(n.nodeData)) { - if (isNode(n.nodeData)) { - return applyNodeParentingConfiguration(n.nodeData, oldHierarchyMap.get(n.nodeData) as IOldNodeHierarchyConfiguration); - } - - if (isSound(n.nodeData)) { - const oldSoundNode = oldHierarchyMap.get(n.nodeData); - - if (oldSoundNode) { - return n.nodeData.attachToMesh(oldSoundNode as TransformNode); - } - - n.nodeData.detachFromMesh(); - n.nodeData.spatialSound = false; - n.nodeData.setPosition(Vector3.Zero()); - return (n.nodeData["_connectedTransformNode"] = null); - } - - if (isAnyParticleSystem(n.nodeData)) { - return (n.nodeData.emitter = oldHierarchyMap.get(n.nodeData) as AbstractMesh); - } - } - }); - }, - redo: () => { - const tempTransfromNode = new TransformNode("tempParent", props.editor.layout.preview.scene); - - try { - nodesToMove.forEach((n) => { - if (n.nodeData === props.object) { - return; - } - - if (n.nodeData && oldHierarchyMap.has(n.nodeData)) { - if (isNode(n.nodeData)) { - if (shift) { - return applyTransformNodeParentingConfiguration(n.nodeData, newParent, tempTransfromNode); - } - - return (n.nodeData.parent = isScene(props.object) ? null : newParent); - } - - if (isSound(n.nodeData)) { - if (isTransformNode(newParent) || isMesh(newParent) || isInstancedMesh(newParent)) { - return n.nodeData.attachToMesh(newParent); - } - - if (isScene(newParent)) { - n.nodeData.detachFromMesh(); - n.nodeData.spatialSound = false; - n.nodeData.setPosition(Vector3.Zero()); - return (n.nodeData["_connectedTransformNode"] = null); - } - } - - if (isAnyParticleSystem(n.nodeData)) { - if (isAbstractMesh(newParent)) { - return (n.nodeData.emitter = newParent); - } - } - } - }); - } catch (e) { - console.error(e); - } - - tempTransfromNode.dispose(false, true); - }, - }); - - props.editor.layout.graph.refresh(); - } - function handleAssetsDropped() { const absolutePaths = props.editor.layout.assets.state.selectedKeys; diff --git a/editor/src/editor/layout/graph/move.ts b/editor/src/editor/layout/graph/move.ts new file mode 100644 index 000000000..ddbbd6b6f --- /dev/null +++ b/editor/src/editor/layout/graph/move.ts @@ -0,0 +1,159 @@ +import { TransformNode, AbstractMesh, Vector3, Node } from "babylonjs"; + +import { isScene } from "../../../tools/guards/scene"; +import { isSound } from "../../../tools/guards/sound"; +import { registerUndoRedo } from "../../../tools/undoredo"; +import { isClusteredLight } from "../../../tools/light/cluster"; +import { isAnyParticleSystem } from "../../../tools/guards/particles"; +import { isAbstractMesh, isClusteredLightContainer, isInstancedMesh, isLight, isMesh, isNode, isTransformNode } from "../../../tools/guards/nodes"; +import { applyNodeParentingConfiguration, applyTransformNodeParentingConfiguration, IOldNodeHierarchyConfiguration } from "../../../tools/node/parenting"; + +import { Editor } from "../../main"; + +export function setNewParentForGraphSelectedNodes(editor: Editor, newParent: any, shift: boolean) { + const nodesToMove = editor.layout.graph.getSelectedNodes(); + const oldHierarchyMap = new Map(); + const clusteredLightContainer = editor.layout.preview.clusteredLightContainer; + + nodesToMove.forEach((n) => { + if (n.nodeData && n.nodeData !== newParent) { + if (isLight(n.nodeData) && isClusteredLight(n.nodeData, editor)) { + return oldHierarchyMap.set(n.nodeData, clusteredLightContainer); + } + + if (isNode(n.nodeData)) { + if (isClusteredLightContainer(newParent)) { + if (!isLight(n.nodeData) || isClusteredLight(n.nodeData, editor)) { + return; + } + + return oldHierarchyMap.set(n.nodeData, n.nodeData.parent); + } else if (n.nodeData.parent !== newParent) { + const descendants = n.nodeData.getDescendants(false); + if (descendants.includes(newParent)) { + return; + } + + return oldHierarchyMap.set(n.nodeData, { + parent: n.nodeData.parent, + position: n.nodeData["position"]?.clone(), + rotation: n.nodeData["rotation"]?.clone(), + scaling: n.nodeData["scaling"]?.clone(), + rotationQuaternion: n.nodeData["rotationQuaternion"]?.clone(), + } as IOldNodeHierarchyConfiguration); + } + } + + if (isSound(n.nodeData)) { + return oldHierarchyMap.set(n.nodeData, n.nodeData["_connectedTransformNode"]); + } + + if (isAnyParticleSystem(n.nodeData)) { + return oldHierarchyMap.set(n.nodeData, n.nodeData.emitter); + } + } + }); + + if (!oldHierarchyMap.size) { + return; + } + + registerUndoRedo({ + executeRedo: true, + undo: () => { + nodesToMove.forEach((n) => { + if (n.nodeData && oldHierarchyMap.has(n.nodeData)) { + if (isLight(n.nodeData)) { + if (isClusteredLight(n.nodeData, editor)) { + clusteredLightContainer.removeLight(n.nodeData); + return (n.nodeData.parent = oldHierarchyMap.get(n.nodeData) as Node | null); + } + + const oldParent = oldHierarchyMap.get(n.nodeData) as Node | null; + if (isClusteredLightContainer(oldParent)) { + return oldParent.addLight(n.nodeData); + } + } + + if (isNode(n.nodeData)) { + return applyNodeParentingConfiguration(n.nodeData, oldHierarchyMap.get(n.nodeData) as IOldNodeHierarchyConfiguration); + } + + if (isSound(n.nodeData)) { + const oldSoundNode = oldHierarchyMap.get(n.nodeData); + + if (oldSoundNode) { + return n.nodeData.attachToMesh(oldSoundNode as TransformNode); + } + + n.nodeData.detachFromMesh(); + n.nodeData.spatialSound = false; + n.nodeData.setPosition(Vector3.Zero()); + return (n.nodeData["_connectedTransformNode"] = null); + } + + if (isAnyParticleSystem(n.nodeData)) { + return (n.nodeData.emitter = oldHierarchyMap.get(n.nodeData) as AbstractMesh); + } + } + }); + }, + redo: () => { + const tempTransfromNode = new TransformNode("tempParent", editor.layout.preview.scene); + + try { + nodesToMove.forEach((n) => { + if (n.nodeData === newParent) { + return; + } + + if (n.nodeData && oldHierarchyMap.has(n.nodeData)) { + if (isNode(n.nodeData)) { + if (isLight(n.nodeData)) { + if (isClusteredLightContainer(newParent)) { + return newParent.addLight(n.nodeData); + } + + if (isClusteredLight(n.nodeData, editor)) { + clusteredLightContainer.removeLight(n.nodeData); + return (n.nodeData.parent = isScene(newParent) ? null : newParent); + } + } + + if (shift) { + return applyTransformNodeParentingConfiguration(n.nodeData, newParent, tempTransfromNode); + } + + return (n.nodeData.parent = isScene(newParent) ? null : newParent); + } + + if (isSound(n.nodeData)) { + if (isTransformNode(newParent) || isMesh(newParent) || isInstancedMesh(newParent)) { + return n.nodeData.attachToMesh(newParent); + } + + if (isScene(newParent)) { + n.nodeData.detachFromMesh(); + n.nodeData.spatialSound = false; + n.nodeData.setPosition(Vector3.Zero()); + return (n.nodeData["_connectedTransformNode"] = null); + } + } + + if (isAnyParticleSystem(n.nodeData)) { + if (isAbstractMesh(newParent)) { + return (n.nodeData.emitter = newParent); + } + } + } + }); + } catch (e) { + console.error(e); + } + + tempTransfromNode.dispose(false, true); + }, + }); + + editor.layout.graph.refresh(); +} diff --git a/editor/src/editor/layout/preview.tsx b/editor/src/editor/layout/preview.tsx index b5a9a85d5..6015be1e6 100644 --- a/editor/src/editor/layout/preview.tsx +++ b/editor/src/editor/layout/preview.tsx @@ -78,7 +78,7 @@ import { disposeSSAO2RenderingPipeline, parseSSAO2RenderingPipeline, ssaoRenderi import { disposeMotionBlurPostProcess, motionBlurPostProcessCameraConfigurations, parseMotionBlurPostProcess } from "../rendering/motion-blur"; import { defaultPipelineCameraConfigurations, disposeDefaultRenderingPipeline, parseDefaultRenderingPipeline } from "../rendering/default-pipeline"; -import { EditorGraphContextMenu } from "./graph/graph"; +import { EditorGraphContextMenu } from "./graph/context-menu"; import { EditorPreviewGizmo } from "./preview/gizmo"; import { EditorPreviewIcons } from "./preview/icons"; From e6117b6e10b44d106cc5b1706ec4257bc47b12e6 Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Thu, 9 Apr 2026 19:19:49 +0200 Subject: [PATCH 14/21] v5.4.1-alpha.2 --- cli/package.json | 2 +- editor/package.json | 2 +- tools/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/package.json b/cli/package.json index a424dc577..3d8c5aa6e 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "babylonjs-editor-cli", - "version": "5.4.1-alpha.1", + "version": "5.4.1-alpha.2", "description": "Babylon.js Editor CLI is a command line interface to help you package your scenes made using the Babylon.js Editor", "productName": "Babylon.js Editor CLI", "scripts": { diff --git a/editor/package.json b/editor/package.json index 613cafe1a..7b12dcdb3 100644 --- a/editor/package.json +++ b/editor/package.json @@ -1,6 +1,6 @@ { "name": "babylonjs-editor", - "version": "5.4.1-alpha.1", + "version": "5.4.1-alpha.2", "description": "Babylon.js Editor is a Web Application helping artists to work with Babylon.js", "productName": "Babylon.js Editor", "main": "build/src/index.js", diff --git a/tools/package.json b/tools/package.json index 72f13f1de..4d1d130a9 100644 --- a/tools/package.json +++ b/tools/package.json @@ -1,6 +1,6 @@ { "name": "babylonjs-editor-tools", - "version": "5.4.1-alpha.1", + "version": "5.4.1-alpha.2", "description": "Babylon.js Editor Tools is a set of tools to help you create, edit and manage your Babylon.js scenes made using the Babylon.js Editor", "productName": "Babylon.js Editor Tools", "scripts": { From 74764a4a8fb25b1dab0fd9a1f56faac9db438806 Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Thu, 9 Apr 2026 21:00:58 +0200 Subject: [PATCH 15/21] feat: add menu to merge animations groups in scene inspector --- .../inspector/scene/animation-groups.tsx | 89 +++++++++++++------ .../editor/layout/inspector/scene/scene.tsx | 2 +- 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/editor/src/editor/layout/inspector/scene/animation-groups.tsx b/editor/src/editor/layout/inspector/scene/animation-groups.tsx index cde29bbfe..2aff6d5ef 100644 --- a/editor/src/editor/layout/inspector/scene/animation-groups.tsx +++ b/editor/src/editor/layout/inspector/scene/animation-groups.tsx @@ -2,21 +2,23 @@ import { Reorder } from "framer-motion"; import { MouseEvent, useEffect, useState } from "react"; -import { AiOutlineMinus } from "react-icons/ai"; import { IoPlay, IoStop } from "react-icons/io5"; +import { AiFillMerge, AiOutlineClose, AiOutlineMinus } from "react-icons/ai"; import { Scene, AnimationGroup } from "babylonjs"; import { Editor } from "../../../main"; +import { showPrompt } from "../../../../ui/dialog"; import { Button } from "../../../../ui/shadcn/ui/button"; +import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger } from "../../../../ui/shadcn/ui/context-menu"; import { registerUndoRedo } from "../../../../tools/undoredo"; import { EditorInspectorSectionField } from "../fields/section"; export interface IEditorSceneAnimationGroupsInspectorProps { - object: Scene; + scene: Scene; editor: Editor; } @@ -28,11 +30,11 @@ export function EditorSceneAnimationGroupsInspector(props: IEditorSceneAnimation const [playingAnimationGroups, setPlayingAnimationGroups] = useState([]); useEffect(() => { - setAnimationGroups(props.object.animationGroups); - setPlayingAnimationGroups(props.object.animationGroups.filter((animationGroup) => animationGroup.isPlaying)); - }, [props.object]); + setAnimationGroups(props.scene.animationGroups); + setPlayingAnimationGroups(props.scene.animationGroups.filter((animationGroup) => animationGroup.isPlaying)); + }, [props.scene]); - function handleAnimationGroupClick(ev: MouseEvent, animationGroup: AnimationGroup): void { + function handleAnimationGroupClick(ev: MouseEvent, animationGroup: AnimationGroup) { if (ev.ctrlKey || ev.metaKey) { const newSelectedAnimationGroups = selectedAnimationGroups.slice(); if (newSelectedAnimationGroups.includes(animationGroup)) { @@ -52,13 +54,13 @@ export function EditorSceneAnimationGroupsInspector(props: IEditorSceneAnimation return setSelectedAnimationGroups([animationGroup]); } - const lastIndex = props.object.animationGroups.indexOf(lastSelectedAnimationGroup); - const currentIndex = props.object.animationGroups.indexOf(animationGroup); + const lastIndex = props.scene.animationGroups.indexOf(lastSelectedAnimationGroup); + const currentIndex = props.scene.animationGroups.indexOf(animationGroup); const [start, end] = lastIndex < currentIndex ? [lastIndex, currentIndex] : [currentIndex, lastIndex]; for (let i = start; i <= end; i++) { - const ag = props.object.animationGroups[i]; + const ag = props.scene.animationGroups[i]; if (!newSelectedAnimationGroups.includes(ag)) { newSelectedAnimationGroups.push(ag); } @@ -70,7 +72,7 @@ export function EditorSceneAnimationGroupsInspector(props: IEditorSceneAnimation } } - function handlePlayOrStopAnimationGroup(animationGroup: AnimationGroup): void { + function handlePlayOrStopAnimationGroup(animationGroup: AnimationGroup) { if (animationGroup.isPlaying) { animationGroup.stop(); setPlayingAnimationGroups(playingAnimationGroups.filter((ag) => ag !== animationGroup)); @@ -80,8 +82,8 @@ export function EditorSceneAnimationGroupsInspector(props: IEditorSceneAnimation } } - function handlePlaySelectedAnimationGroups(): void { - props.object.animationGroups.forEach((animationGroup) => { + function handlePlaySelectedAnimationGroups() { + props.scene.animationGroups.forEach((animationGroup) => { animationGroup.stop(); }); @@ -89,25 +91,43 @@ export function EditorSceneAnimationGroupsInspector(props: IEditorSceneAnimation animationGroup.play(true); }); - setPlayingAnimationGroups(props.object.animationGroups.filter((animationGroup) => animationGroup.isPlaying)); + setPlayingAnimationGroups(props.scene.animationGroups.filter((animationGroup) => animationGroup.isPlaying)); } - function handleRemoveSelectedAnimationGroups(): void { + function handleRemoveSelectedAnimationGroups() { registerUndoRedo({ executeRedo: true, undo: () => { selectedAnimationGroups.forEach((animationGroup) => { - props.object.addAnimationGroup(animationGroup); + props.scene.addAnimationGroup(animationGroup); }); }, redo: () => { selectedAnimationGroups.forEach((animationGroup) => { - props.object.removeAnimationGroup(animationGroup); + props.scene.removeAnimationGroup(animationGroup); }); }, }); - setAnimationGroups(props.object.animationGroups.slice()); + setAnimationGroups(props.scene.animationGroups.slice()); + } + + async function handleMergeSelectedAnimationGroups() { + const name = await showPrompt("Merge Animation Groups", "Enter a name for the merged animation group", "Merged Animation Group"); + if (!name) { + return; + } + + const animationGroup = new AnimationGroup(name, props.scene); + + selectedAnimationGroups.forEach((ag) => { + ag.targetedAnimations.forEach((targetedAnimation) => { + animationGroup.addTargetedAnimation(targetedAnimation.animation, targetedAnimation.target); + }); + }); + + setSelectedAnimationGroups([animationGroup]); + setAnimationGroups(props.scene.animationGroups.slice()); } const hasAnimations = animationGroups.length > 0; @@ -143,28 +163,41 @@ export function EditorSceneAnimationGroupsInspector(props: IEditorSceneAnimation { setAnimationGroups(items); - props.object.animationGroups = items; + props.scene.animationGroups = items; }} className="flex flex-col rounded-lg bg-black/50 text-white/75 h-96 overflow-y-auto" > {animations.map((animationGroup) => ( -
handleAnimationGroupClick(ev, animationGroup)} - className={` + + +
handleAnimationGroupClick(ev, animationGroup)} + className={` flex items-center gap-2 ${selectedAnimationGroups.includes(animationGroup) ? "bg-muted" : "hover:bg-muted/35"} transition-all duration-300 ease-in-out `} - > - - {animationGroup.name} -
+ > + + {animationGroup.name} +
+ + + + Merge... + + + + Remove + + +
))}
diff --git a/editor/src/editor/layout/inspector/scene/scene.tsx b/editor/src/editor/layout/inspector/scene/scene.tsx index c9829afe7..6ef2d7264 100644 --- a/editor/src/editor/layout/inspector/scene/scene.tsx +++ b/editor/src/editor/layout/inspector/scene/scene.tsx @@ -155,7 +155,7 @@ export class EditorSceneInspector extends Component + ); } From 3434ccde41e6f1ac595c21344fe5e5db03cb8acd Mon Sep 17 00:00:00 2001 From: Julien Moreau-Mathis Date: Fri, 10 Apr 2026 14:38:07 +0200 Subject: [PATCH 16/21] feat: add @componentFromScene decorator in babylonjs-editor-tools --- tools/src/decorators/apply.ts | 32 ++++++++++++++++++ tools/src/decorators/scene.ts | 16 +++++++++ tools/test/decorators/scene.test.ts | 27 +++++++++++++++- tools/test/tools/guards.test.ts | 14 ++++++++ tools/test/tools/scene.test.ts | 50 +++++++++++++++++++++++++++++ tools/test/tools/sound.test.ts | 26 +++++++++++++++ tools/test/tools/tools.test.ts | 20 ++++++++++++ tools/test/tools/vector.test.ts | 21 ++++++++++++ 8 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 tools/test/tools/scene.test.ts create mode 100644 tools/test/tools/sound.test.ts create mode 100644 tools/test/tools/tools.test.ts create mode 100644 tools/test/tools/vector.test.ts diff --git a/tools/src/decorators/apply.ts b/tools/src/decorators/apply.ts index ed6449c14..1ebe2ad3b 100644 --- a/tools/src/decorators/apply.ts +++ b/tools/src/decorators/apply.ts @@ -23,6 +23,7 @@ import { ISpriteAnimation, SpriteManagerNode } from "../tools/sprite"; import { isAbstractMesh, isNode, isSprite, isTransformNode } from "../tools/guards"; import { scriptAssetsCache } from "../loading/script/preload"; +import { getScriptByClassForObject } from "../loading/script/apply"; import { IPointerEventDecoratorOptions } from "./events"; import { VisibleInInspectorDecoratorConfiguration, VisibleInInspectorDecoratorEntityConfiguration, VisibleInspectorDecoratorAssetConfiguration } from "./inspector"; @@ -34,6 +35,12 @@ export interface ISceneDecoratorData { propertyKey: string | Symbol; }[]; + // @componentFromScene + _ComponentsFromScene?: { + componentConstructor: new (...args: any) => any; + propertyKey: string | Symbol; + }[]; + // @nodeFromDescendants _NodesFromDescendants?: { nodeName: string; @@ -117,6 +124,31 @@ export function applyDecorators(scene: Scene, object: any, script: any, instance instance[params.propertyKey.toString()] = getNodeByName(params.nodeName, scene); }); + // @componentFromScene + if (ctor._ComponentsFromScene?.length) { + scene.getEngine().onBeginFrameObservable.addOnce(() => { + ctor._ComponentsFromScene?.forEach((params) => { + const components: any[] = []; + + const nodes = [...scene.transformNodes, ...scene.meshes, ...scene.lights, ...scene.cameras]; + nodes.forEach((node) => { + const component = getScriptByClassForObject(node, params.componentConstructor); + if (component) { + components.push(component); + } + }); + + if (components.length > 1) { + throw new Error( + `Multiple components of type ${ctor._ComponentsFromScene![0].componentConstructor.name} found in scene for property "${ctor._ComponentsFromScene![0].propertyKey.toString()}".` + ); + } + + instance[params.propertyKey.toString()] = components[0] ?? null; + }); + }); + } + // @nodeFromDescendants ctor._NodesFromDescendants?.forEach((params) => { const descendant = (object as Partial).getDescendants?.(params.directDescendantsOnly, (node) => node.name === params.nodeName)[0]; diff --git a/tools/src/decorators/scene.ts b/tools/src/decorators/scene.ts index bd001e246..6b2d7707a 100644 --- a/tools/src/decorators/scene.ts +++ b/tools/src/decorators/scene.ts @@ -16,6 +16,22 @@ export function nodeFromScene(nodeName: string) { }; } +/** + * Makes the decorated property linked to the instantiated component of the given constructor type. + * Once the script is instantiated, the reference to the component is retrieved from the scene + * and assigned to the property. Components link cant' be used in constructor. + * This can be used only by scripts using Classes. + * @param componentConstructor defines the class of the type to retrieve. + */ +export function componentFromScene any>(componentConstructor: T) { + return function (target: any, propertyKey: string | Symbol) { + const ctor = target.constructor as ISceneDecoratorData; + + ctor._ComponentsFromScene ??= []; + ctor._ComponentsFromScene.push({ propertyKey, componentConstructor }); + }; +} + /** * Makes the decorated property linked to the node that has the given name. * Once the script is instantiated, the reference to the node is retrieved from the descendants diff --git a/tools/test/decorators/scene.test.ts b/tools/test/decorators/scene.test.ts index 9b18275e8..6bd1207d6 100644 --- a/tools/test/decorators/scene.test.ts +++ b/tools/test/decorators/scene.test.ts @@ -1,7 +1,7 @@ import { describe, beforeEach, test, expect } from "vitest"; import { ISceneDecoratorData } from "../../src/decorators/apply"; -import { nodeFromScene, nodeFromDescendants, animationGroupFromScene } from "../../src/decorators/scene"; +import { nodeFromScene, nodeFromDescendants, animationGroupFromScene, componentFromScene, sceneAsset } from "../../src/decorators/scene"; describe("decorators/scene", () => { let target: { @@ -26,6 +26,19 @@ describe("decorators/scene", () => { }); }); + describe("@componentFromScene", () => { + test("should add configuration to the target", () => { + class TestComponent {} + const fn = componentFromScene(TestComponent); + fn(target, "testProperty"); + + expect(target.constructor._ComponentsFromScene).toBeDefined(); + expect(target.constructor._ComponentsFromScene!.length).toBe(1); + expect(target.constructor._ComponentsFromScene![0].componentConstructor).toBe(TestComponent); + expect(target.constructor._ComponentsFromScene![0].propertyKey).toBe("testProperty"); + }); + }); + describe("@nodeFromDescendants", () => { test("should add configuration to the target", () => { const fn = nodeFromDescendants("test"); @@ -49,4 +62,16 @@ describe("decorators/scene", () => { expect(target.constructor._AnimationGroups![0].propertyKey).toBe("testProperty"); }); }); + + describe("@sceneAsset", () => { + test("should add configuration to the target", () => { + const fn = sceneAsset("test"); + fn(target, "testProperty"); + + expect(target.constructor._SceneAssets).toBeDefined(); + expect(target.constructor._SceneAssets!.length).toBe(1); + expect(target.constructor._SceneAssets![0].sceneName).toBe("test"); + expect(target.constructor._SceneAssets![0].propertyKey).toBe("testProperty"); + }); + }); }); diff --git a/tools/test/tools/guards.test.ts b/tools/test/tools/guards.test.ts index 23734140a..b4cf3759d 100644 --- a/tools/test/tools/guards.test.ts +++ b/tools/test/tools/guards.test.ts @@ -20,6 +20,8 @@ import { isParticleSystem, isGPUParticleSystem, isAnyParticleSystem, + isClusteredLightContainer, + isSprite, } from "../../src/tools/guards"; describe("tools/guards", () => { @@ -183,4 +185,16 @@ describe("tools/guards", () => { expect(isAnyParticleSystem({ getClassName: () => "SolidPS" })).toBeFalsy(); }); }); + + describe("isClusteredLightContainer", () => { + test("should return a boolean indicated if the passed object is a clustered light container or not", () => { + expect(isClusteredLightContainer({ getClassName: () => "ClusteredLightContainer" })).toBeTruthy(); + }); + }); + + describe("isSprite", () => { + test("should return a boolean indicated if the passed object is a sprite or not", () => { + expect(isSprite({ getClassName: () => "Sprite" })).toBeTruthy(); + }); + }); }); diff --git a/tools/test/tools/scene.test.ts b/tools/test/tools/scene.test.ts new file mode 100644 index 000000000..ba1e62173 --- /dev/null +++ b/tools/test/tools/scene.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test, vi } from "vitest"; + +import { getNodeByName, getNodeById } from "../../src/tools/scene"; + +describe("tools/scene", () => { + const sceneGetNodeResult = {}; + const clusteredLightContainerResult = { + lights: [ + { + id: "lightId", + name: "lightName", + }, + ], + getClassName: () => "ClusteredLightContainer", + }; + + const scene = { + lights: [clusteredLightContainerResult], + getNodeById: vi.fn().mockImplementation((id) => id === "node" && sceneGetNodeResult), + getNodeByName: vi.fn().mockImplementation((name) => name === "node" && sceneGetNodeResult), + } as any; + + describe("getNodeByName", () => { + test("should return the node identified by the given name", () => { + expect(getNodeByName("node", scene)).toBe(sceneGetNodeResult); + }); + + test("should return the light contained in a clustered light container if its name is the given one", () => { + expect(getNodeByName("lightName", scene)).toBe(clusteredLightContainerResult.lights[0]); + }); + + test("should return null when node not found", () => { + expect(getNodeByName("unknown", scene)).toBeNull(); + }); + }); + + describe("getNodeById", () => { + test("should return the node identified by the given name", () => { + expect(getNodeById("node", scene)).toBe(sceneGetNodeResult); + }); + + test("should return the light contained in a clustered light container if its name is the given one", () => { + expect(getNodeById("lightId", scene)).toBe(clusteredLightContainerResult.lights[0]); + }); + + test("should return null when node not found", () => { + expect(getNodeById("unknown", scene)).toBeNull(); + }); + }); +}); diff --git a/tools/test/tools/sound.test.ts b/tools/test/tools/sound.test.ts new file mode 100644 index 000000000..ef0eeb155 --- /dev/null +++ b/tools/test/tools/sound.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from "vitest"; + +import { getSoundById } from "../../src/tools/sound"; + +describe("tools/vector", () => { + const soundData = { + id: "soundId", + }; + + const scene = { + soundTracks: [], + mainSoundTrack: { + soundCollection: [soundData], + }, + } as any; + + describe("getSoundById", () => { + test("should return the node identified by the given id", () => { + expect(getSoundById("soundId", scene)).toBe(soundData); + }); + + test("should return null if the sound is not found", () => { + expect(getSoundById("unknown", scene)).toBeNull(); + }); + }); +}); diff --git a/tools/test/tools/tools.test.ts b/tools/test/tools/tools.test.ts new file mode 100644 index 000000000..27a88ad0c --- /dev/null +++ b/tools/test/tools/tools.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from "vitest"; + +import { cloneJSObject } from "../../src/tools/tools"; + +describe("tools/tools", () => { + describe("cloneJSObject", () => { + test("should clone a JavaScript object", () => { + const obj = { a: 1, b: { c: 2 } }; + const clonedObj = cloneJSObject(obj); + expect(clonedObj).toEqual(obj); + expect(clonedObj).not.toBe(obj); + expect(clonedObj.b).not.toBe(obj.b); + }); + + test("should return null or undefined as is", () => { + expect(cloneJSObject(null)).toBeNull(); + expect(cloneJSObject(undefined)).toBeUndefined(); + }); + }); +}); diff --git a/tools/test/tools/vector.test.ts b/tools/test/tools/vector.test.ts new file mode 100644 index 000000000..7791a891d --- /dev/null +++ b/tools/test/tools/vector.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from "vitest"; + +import { Axis } from "@babylonjs/core/Maths/math.axis"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; + +import { parseAxis } from "../../src/tools/vector"; + +describe("tools/vector", () => { + describe("parseAxis", () => { + test("should return the correct axis value", () => { + expect(parseAxis([1, 0, 0])).toBe(Axis.X); + expect(parseAxis([0, 1, 0])).toBe(Axis.Y); + expect(parseAxis([0, 0, 1])).toBe(Axis.Z); + }); + + test("should return current vector if it doesn't match any axis", () => { + const vector = [0.5, 0.5, 0.5]; + expect(parseAxis(vector).equals(Vector3.FromArray(vector))).toBeTruthy(); + }); + }); +}); From a242b56a6f59eb852adfd90f41860ea0c9b72757 Mon Sep 17 00:00:00 2001 From: Julien Moreau-Mathis Date: Fri, 10 Apr 2026 17:02:45 +0200 Subject: [PATCH 17/21] docs: add documentation for common decorators --- .../scripting/common-decorators/decorators.ts | 110 ++++++++++++++++ .../scripting/common-decorators/page.tsx | 120 ++++++++++++++++++ .../scripting/customizing-scripts/page.tsx | 10 -- website/src/app/documentation/sidebar.tsx | 1 + 4 files changed, 231 insertions(+), 10 deletions(-) create mode 100644 website/src/app/documentation/scripting/common-decorators/decorators.ts create mode 100644 website/src/app/documentation/scripting/common-decorators/page.tsx diff --git a/website/src/app/documentation/scripting/common-decorators/decorators.ts b/website/src/app/documentation/scripting/common-decorators/decorators.ts new file mode 100644 index 000000000..bb1fe274a --- /dev/null +++ b/website/src/app/documentation/scripting/common-decorators/decorators.ts @@ -0,0 +1,110 @@ +export const nodeFromScene = ` +import { Mesh } from "@babylonjs/core/Meshes/mesh"; + +import { nodeFromScene } from "babylonjs-editor-tools"; + +export default class MyMeshComponent { + @nodeFromScene("Other Mesh") + private _otherMesh: Mesh | null = null; + + public constructor(public mesh: Mesh) { } + + public onStart(): void { + console.log(this.otherMesh); + } +} +`; + +export const nodeFromDescendants = ` +import { Mesh } from "@babylonjs/core/Meshes/mesh"; + +import { nodeFromDescendants } from "babylonjs-editor-tools"; + +export default class MyMeshComponent { + @nodeFromDescendants("Other Mesh") + private _otherMesh: Mesh | null = null; + + public constructor(public mesh: Mesh) { } + + public onStart(): void { + console.log(this.otherMesh); + } +} +`; + +export const animationGroupFromScene = ` +import { Mesh } from "@babylonjs/core/Meshes/mesh"; +import { AnimationGroup } from "@babylonjs/core/Animations/animationGroup"; + +import { animationGroupFromScene } from "babylonjs-editor-tools"; + +export default class MyMeshComponent { + @animationGroupFromScene("Idle") + private _idle: AnimationGroup | null = null; + + public constructor(public mesh: Mesh) { } + + public onStart(): void { + this._idle.play(); + } +} +`; + +export const sceneAsset = ` +import { Mesh } from "@babylonjs/core/Meshes/mesh"; + +import { sceneAsset, AdvancedAssetContainer } from "babylonjs-editor-tools"; + +export default class MyMeshComponent { + @sceneAsset("enemy.scene") + private _enemy: AdvancedAssetContainer | null = null; + + public constructor(public mesh: Mesh) { } + + public onStart(): void { + // The container is instantiated by default. You can call .removeDefault() to remove the default instances. + this._enemy.removeDefault(); + + // Otherwise, you can keep the default instance and use it in your scene. + // this._enemy.removeDefault(); + + // If the container is used to instantiate multiple entities like enemies, you can call .instantiate(). + for (let i = 0; i < 10; i++) { + const enemy = this._enemy.instantiate({ + doNotInstantiate: (node) => node.name === "DontInstantiateMe", + predicate: (entity) => entity.name.startsWith("Enemy"), + }); + + // You can dispose the instantiated entries using .dispose + enemy.dispose(); + } + } +} +`; + +export const componentFromScene = ` +// my-component.ts +import { Mesh } from "@babylonjs/core/Meshes/mesh"; + +import { nodeFromScene } from "babylonjs-editor-tools"; + +export default class MyMeshComponent { + @componentFromScene(MyOtherComponentClass) + private _myComponennt: MyOtherComponentClass; + + public constructor(public mesh: Mesh) { } + + public onStart(): void { + this._myComponennt.sayHello(); + } +} + +// my-other-component.ts +export default class MyOtherComponentClass { + ... + + public sayHello(): void { + console.log("Hello!"); + } +} +`; diff --git a/website/src/app/documentation/scripting/common-decorators/page.tsx b/website/src/app/documentation/scripting/common-decorators/page.tsx new file mode 100644 index 000000000..a48630eff --- /dev/null +++ b/website/src/app/documentation/scripting/common-decorators/page.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { Fade } from "react-awesome-reveal"; + +import { CodeBlock } from "../../code"; + +import { animationGroupFromScene, componentFromScene, nodeFromDescendants, nodeFromScene, sceneAsset } from "./decorators"; +import { CustomLink } from "../../link"; + +export default function DocumentationCommonDecoratorsPage() { + return ( +
+
+ + +
Common decorators
+
+
+ + +
+
Introduction
+ +
+ Scripts can retrieve instances from the scene by using some common decorators. Those decorators are used to retrieve objects from the scene and link + them to properties in the script. This way, you can easily reference other objects in the scene and use them in your script. +
+ + {/* Node from scene */} +
@nodeFromScene
+ +
+ This decorator is used to retrieve any Mesh, TransformNode, Light or Camera from the scene by its name. The retrieved node + is linked to the decorated property, so you can use it in your script. +
+ + + + {/* Node from descendants */} +
@nodeFromDescendants
+ +
+ This decorator is used to retrieve any Mesh, TransformNode, Light or Camera from the children of the object the + script is attached to. The retrieved node is linked to the decorated property, so you can use it in your script. +
+ + + + {/* Animation group */} +
@animationGroupFromScene
+ +
+ This decorator is used to retrieve any Animation Group from the scene. The retrieved animation group is linked to the decorated property, so you + can use it in your script. +
+ + + + {/* Scene asset */} +
@sceneAsset
+ +
This decorator is used to load and retrieve a scene container.
+ +
+ A scene can be used for multiple reasons. For example, a scene that is used once like a map, or a scene that is set to be instantiated multiple times + like an enemies. In the first case, you can load the scene and retrieve the container with the decorator, while in the second case, you can load the + scene as a container and instantiate it multiple times in the main scene. +
+ +
+ The retrieved scene container instance is of type AdvancedAssetContainer, which is an extended version of the AssetContainer class + provided by Babylon.js. +
+ +
+ A scene container can be used to instantiate the assets multiple time. The goal of the AdvancedAssetContainer is to add support of extra features + to the default AssetContainer, like the possibility instantiate attached scripts to instantiated entries. +
+ +
+ Available methods are: +
    +
  • + removeDefault: When a scene is loaded as a container, it is automatically instantiated once and the instances are added to the main + scene. This method allows to remove those default instances from the main scene as the container is used to be instantiated on-demand, for + example for enemies. +
  • +
  • + instantiate: Instantiates the whole container and returns the root nodes of the instantiated hierarchy. More information about + instantiated entries in{" "} + + Babylon.js Documentation (Duplicating the models) + +
  • +
+
+ + + + {/* Component from scene */} +
@componentFromScene
+ +
+ This decorator is used to retrieve the unique reference to a script attached to an object that has been instantiated in the scene. The retrieved script + reference is linked to the decorated property, so you can use it in your script. +
+ +
+ When using this decorator, make sure that only one instance of the script you want to retrieve is attached to objects in the scene. If multiple + instances of the same script are found in the scene, an error will be thrown and the project won't be able to run, since it won't know which one to link + to. +
+ + +
+
+
+
+ ); +} diff --git a/website/src/app/documentation/scripting/customizing-scripts/page.tsx b/website/src/app/documentation/scripting/customizing-scripts/page.tsx index 791c5ee9b..e6c1e18c8 100644 --- a/website/src/app/documentation/scripting/customizing-scripts/page.tsx +++ b/website/src/app/documentation/scripting/customizing-scripts/page.tsx @@ -1,7 +1,6 @@ "use client"; import { Fade } from "react-awesome-reveal"; -import { IoIosWarning } from "react-icons/io"; import { CodeBlock } from "../../code"; @@ -41,15 +40,6 @@ export default function DocumentationRunningProjectPage() { of the property is used as a label), where the description is used as a tooltip to help the user to understand what's the purpose of the property. -
- - -
- Those decorators are available in the babylonjs-editor-tools package that is provided as a depdendency in the package.json file. In - case a decorator that is documented here is not available in the code, make sure to install the up-to-date package in your project. -
-
-
@visibleAsBoolean
diff --git a/website/src/app/documentation/sidebar.tsx b/website/src/app/documentation/sidebar.tsx index 73658fbfa..58fc8364b 100644 --- a/website/src/app/documentation/sidebar.tsx +++ b/website/src/app/documentation/sidebar.tsx @@ -38,6 +38,7 @@ export function DocumentationSidebar() {
Scripting
+ From eb252a1eedbdb52315af124515ccfafe8e433cc4 Mon Sep 17 00:00:00 2001 From: Cesharpe Date: Fri, 10 Apr 2026 18:40:30 +0200 Subject: [PATCH 18/21] fix: handle cli and tools dependency in case of mono-repo --- editor/src/project/load/load.tsx | 48 +++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/editor/src/project/load/load.tsx b/editor/src/project/load/load.tsx index e4daebda9..afe30f67f 100644 --- a/editor/src/project/load/load.tsx +++ b/editor/src/project/load/load.tsx @@ -93,28 +93,42 @@ export async function checkDependencies( toast.warning(`Package manager "${packageManager}" is not available on your system. Dependencies will not be updated.`); } - const cliPackageJsonPath = join(directory, "node_modules/babylonjs-editor-cli/package.json"); - const toolsPackageJsonPath = join(directory, "node_modules/babylonjs-editor-tools/package.json"); + const cliPackageJsonPath = "node_modules/babylonjs-editor-cli/package.json"; + const toolsPackageJsonPath = "node_modules/babylonjs-editor-tools/package.json"; + let matchesCliVersion = false; let matchesToolsVersion = false; - try { - const toolsPackageJson = await readJSON(toolsPackageJsonPath, "utf-8"); - if (toolsPackageJson.version === packageJson.version) { - matchesToolsVersion = true; + + // Recursively search for the "babylonjs-editor-tools" package in parent directories, to handle monorepos where the package might be hoisted to the root "node_modules" folder. + const toolsPathSplit = directory.split("/"); + do { + try { + const path = join(...toolsPathSplit, toolsPackageJsonPath); + const toolsPackageJson = await readJSON(path, "utf-8"); + + matchesToolsVersion = toolsPackageJson.version === packageJson.version; + break; + } catch (e) { + // Catch silently } - } catch (e) { - // Catch silently - } - let matchesCliVersion = false; - try { - const cliPackageJson = await readJSON(cliPackageJsonPath, "utf-8"); - if (cliPackageJson.version === packageJson.version) { - matchesCliVersion = true; + toolsPathSplit.pop(); + } while (toolsPathSplit.length > 0); + + const cliPathSplit = directory.split("/"); + do { + try { + const path = join(...cliPathSplit, cliPackageJsonPath); + const cliPackageJson = await readJSON(path, "utf-8"); + + matchesCliVersion = cliPackageJson.version === packageJson.version; + break; + } catch (e) { + // Catch silently } - } catch (e) { - // Catch silently - } + + cliPathSplit.pop(); + } while (cliPathSplit.length > 0); let toolsCode = 0; if (!matchesToolsVersion) { From 483eba2a51b891f4054424889d61c1928e648498 Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Fri, 10 Apr 2026 22:08:51 +0200 Subject: [PATCH 19/21] v5.4.1-alpha.3 --- cli/package.json | 2 +- editor/package.json | 2 +- tools/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/package.json b/cli/package.json index 3d8c5aa6e..3acc9db37 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "babylonjs-editor-cli", - "version": "5.4.1-alpha.2", + "version": "5.4.1-alpha.3", "description": "Babylon.js Editor CLI is a command line interface to help you package your scenes made using the Babylon.js Editor", "productName": "Babylon.js Editor CLI", "scripts": { diff --git a/editor/package.json b/editor/package.json index 7b12dcdb3..01b27d60b 100644 --- a/editor/package.json +++ b/editor/package.json @@ -1,6 +1,6 @@ { "name": "babylonjs-editor", - "version": "5.4.1-alpha.2", + "version": "5.4.1-alpha.3", "description": "Babylon.js Editor is a Web Application helping artists to work with Babylon.js", "productName": "Babylon.js Editor", "main": "build/src/index.js", diff --git a/tools/package.json b/tools/package.json index 4d1d130a9..c7d312d62 100644 --- a/tools/package.json +++ b/tools/package.json @@ -1,6 +1,6 @@ { "name": "babylonjs-editor-tools", - "version": "5.4.1-alpha.2", + "version": "5.4.1-alpha.3", "description": "Babylon.js Editor Tools is a set of tools to help you create, edit and manage your Babylon.js scenes made using the Babylon.js Editor", "productName": "Babylon.js Editor Tools", "scripts": { From 133424a42197e15723ff92a98c00fa64a369d9dc Mon Sep 17 00:00:00 2001 From: Cesharpe Date: Sat, 11 Apr 2026 14:29:04 +0200 Subject: [PATCH 20/21] feat: add support of decal map in standard and pbr material inspectors --- editor/src/editor/layout/graph/remove.ts | 2 + .../inspector/material/components/detail.tsx | 46 +++++++++++++++++++ .../editor/layout/inspector/material/pbr.tsx | 3 ++ .../layout/inspector/material/standard.tsx | 3 ++ editor/src/editor/layout/preview.tsx | 40 ++++++++-------- 5 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 editor/src/editor/layout/inspector/material/components/detail.tsx diff --git a/editor/src/editor/layout/graph/remove.ts b/editor/src/editor/layout/graph/remove.ts index 7ade87b00..91fb87c53 100644 --- a/editor/src/editor/layout/graph/remove.ts +++ b/editor/src/editor/layout/graph/remove.ts @@ -207,6 +207,8 @@ export function removeNodes(editor: Editor) { }); }, }); + + editor.layout.preview.selectionOutlineLayer.clearSelection(); } function restoreNodeData(editor: Editor, data: _RemoveNodeData, scene: Scene) { diff --git a/editor/src/editor/layout/inspector/material/components/detail.tsx b/editor/src/editor/layout/inspector/material/components/detail.tsx new file mode 100644 index 000000000..251b5a0ae --- /dev/null +++ b/editor/src/editor/layout/inspector/material/components/detail.tsx @@ -0,0 +1,46 @@ +import { Component, ReactNode } from "react"; + +import { PBRMaterial, StandardMaterial } from "babylonjs"; + +import { EditorInspectorSwitchField } from "../../fields/switch"; +import { EditorInspectorNumberField } from "../../fields/number"; +import { EditorInspectorSectionField } from "../../fields/section"; +import { EditorInspectorTextureField } from "../../fields/texture"; + +export interface IEditorDetailMapInspectorProps { + material: StandardMaterial | PBRMaterial; +} + +export interface IEditorDetailMapInspectorState {} + +export class EditorDetailMapInspector extends Component { + public constructor(props: IEditorDetailMapInspectorProps) { + super(props); + + this.state = {}; + } + + public render(): ReactNode { + return ( + + this.forceUpdate()} /> + + {this.props.material.detailMap.isEnabled && ( + <> + + + + + + )} + + ); + } +} diff --git a/editor/src/editor/layout/inspector/material/pbr.tsx b/editor/src/editor/layout/inspector/material/pbr.tsx index a4536c609..c5f431cfe 100644 --- a/editor/src/editor/layout/inspector/material/pbr.tsx +++ b/editor/src/editor/layout/inspector/material/pbr.tsx @@ -13,6 +13,7 @@ import { EditorInspectorTextureField } from "../fields/texture"; import { EditorInspectorSectionField } from "../fields/section"; import { EditorAlphaModeField } from "./components/alpha"; +import { EditorDetailMapInspector } from "./components/detail"; import { EditorTransparencyModeField } from "./components/transparency"; import { EditorMaterialInspectorUtilsComponent } from "./components/utils"; @@ -245,6 +246,8 @@ export class EditorPBRMaterialInspector extends Component )} + + + + diff --git a/editor/src/editor/layout/preview.tsx b/editor/src/editor/layout/preview.tsx index 6015be1e6..73d17f0a3 100644 --- a/editor/src/editor/layout/preview.tsx +++ b/editor/src/editor/layout/preview.tsx @@ -189,6 +189,7 @@ export class EditorPreview extends Component { - return m.metadata?.decal && m.isVisible && m.isEnabled(); - }, - false - ); + private _decalMeshPredicate(m: AbstractMesh): boolean { + if (!m.isVisible || !m.isEnabled() || !m.metadata?.decal) { + return false; + } - const meshPick = this.scene.pick( - x, - y, - (m) => { - return !m._masterMesh && !isCollisionMesh(m) && !isCollisionInstancedMesh(m) && m.isVisible && m.isEnabled(); - }, - false - ); + if (this._lastPickedDecal) { + return m !== this._lastPickedDecal; + } + return true; + } + + private _meshPredicate(m: AbstractMesh): boolean { + return !m._masterMesh && !isCollisionMesh(m) && !isCollisionInstancedMesh(m) && m.isVisible && m.isEnabled(); + } + + private _getPickingInfo(x: number, y: number): PickingInfo { + const decalPick = this.scene.pick(x, y, (m) => this._decalMeshPredicate(m), false); + const meshPick = this.scene.pick(x, y, (m) => this._meshPredicate(m), false); const spritePick = this.scene.pickSprite(x, y, (s) => isSprite(s), false); + this._lastPickedDecal = null; + let pickingInfo = meshPick; if (decalPick?.pickedPoint && meshPick?.pickedPoint) { const distance = Vector3.Distance(decalPick.pickedPoint, meshPick.pickedPoint); const zOffset = decalPick.pickedMesh?.material?.zOffset ?? 0; - if (distance <= zOffset + 0.01) { + if (distance <= zOffset + 1) { pickingInfo = decalPick; + this._lastPickedDecal = decalPick.pickedMesh; } } From 02a30dfe6813d97abf078a5d61f33486df2d9afa Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Mon, 13 Apr 2026 19:26:00 +0200 Subject: [PATCH 21/21] chore: bump Babylon.js to v9.2.1 --- editor/package.json | 26 ++--- package.json | 24 +++-- plugins/fab/package.json | 2 +- plugins/quixel/package.json | 2 +- templates/electron/package.json | 10 +- templates/nextjs/package.json | 10 +- templates/solidjs/package.json | 10 +- templates/vanillajs/package.json | 10 +- tools/package.json | 6 +- website/package.json | 4 +- yarn.lock | 166 +++++++++++++++---------------- 11 files changed, 136 insertions(+), 134 deletions(-) diff --git a/editor/package.json b/editor/package.json index 01b27d60b..e96d7bbbf 100644 --- a/editor/package.json +++ b/editor/package.json @@ -40,9 +40,9 @@ "vitest": "4.0.17" }, "dependencies": { - "@babylonjs/addons": "9.0.0", - "@babylonjs/core": "9.0.0", - "@babylonjs/havok": "1.3.10", + "@babylonjs/addons": "9.2.1", + "@babylonjs/core": "9.2.1", + "@babylonjs/havok": "1.3.12", "@blueprintjs/core": "^5.10.0", "@blueprintjs/select": "^5.1.2", "@emotion/react": "^11.13.3", @@ -75,18 +75,18 @@ "@xterm/xterm": "6.1.0-beta.22", "assimpjs": "0.0.10", "axios": "1.15.0", - "babylonjs": "9.0.0", - "babylonjs-addons": "9.0.0", + "babylonjs": "9.2.1", + "babylonjs-addons": "9.2.1", "babylonjs-editor-cli": "latest", "babylonjs-editor-tools": "latest", - "babylonjs-gui": "9.0.0", - "babylonjs-gui-editor": "9.0.0", - "babylonjs-loaders": "9.0.0", - "babylonjs-materials": "9.0.0", - "babylonjs-node-editor": "9.0.0", - "babylonjs-node-particle-editor": "9.0.0", - "babylonjs-post-process": "9.0.0", - "babylonjs-procedural-textures": "9.0.0", + "babylonjs-gui": "9.2.1", + "babylonjs-gui-editor": "9.2.1", + "babylonjs-loaders": "9.2.1", + "babylonjs-materials": "9.2.1", + "babylonjs-node-editor": "9.2.1", + "babylonjs-node-particle-editor": "9.2.1", + "babylonjs-post-process": "9.2.1", + "babylonjs-procedural-textures": "9.2.1", "chokidar": "^4.0.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", diff --git a/package.json b/package.json index cb54e92b6..bfcb892a3 100644 --- a/package.json +++ b/package.json @@ -74,18 +74,22 @@ }, "dependencies": {}, "resolutions": { + "@babylonjs/core": "9.2.1", + "@babylonjs/addons": "9.2.1", + "@babylonjs/gui": "9.2.1", + "@babylonjs/materials": "9.2.1", "braces": "3.0.3", "node-abi": "4.14.0", "wrap-ansi": "7.0.0", - "babylonjs": "9.0.0", - "babylonjs-addons": "9.0.0", - "babylonjs-gui": "9.0.0", - "babylonjs-gui-editor": "9.0.0", - "babylonjs-loaders": "9.0.0", - "babylonjs-materials": "9.0.0", - "babylonjs-node-editor": "9.0.0", - "babylonjs-node-particle-editor": "9.0.0", - "babylonjs-post-process": "9.0.0", - "babylonjs-procedural-textures": "9.0.0" + "babylonjs": "9.2.1", + "babylonjs-addons": "9.2.1", + "babylonjs-gui": "9.2.1", + "babylonjs-gui-editor": "9.2.1", + "babylonjs-loaders": "9.2.1", + "babylonjs-materials": "9.2.1", + "babylonjs-node-editor": "9.2.1", + "babylonjs-node-particle-editor": "9.2.1", + "babylonjs-post-process": "9.2.1", + "babylonjs-procedural-textures": "9.2.1" } } diff --git a/plugins/fab/package.json b/plugins/fab/package.json index 8819fd360..d1fbcd23e 100644 --- a/plugins/fab/package.json +++ b/plugins/fab/package.json @@ -20,7 +20,7 @@ "typescript": "5.9.3" }, "dependencies": { - "babylonjs": "9.0.0", + "babylonjs": "9.2.1", "fs-extra": "11.2.0", "react": "18.2.0", "react-icons": "5.6.0", diff --git a/plugins/quixel/package.json b/plugins/quixel/package.json index 165e82dcb..04381899d 100644 --- a/plugins/quixel/package.json +++ b/plugins/quixel/package.json @@ -16,7 +16,7 @@ "typescript": "5.9.3" }, "dependencies": { - "babylonjs": "9.0.0", + "babylonjs": "9.2.1", "fs-extra": "11.2.0", "sharp": "0.34.3" } diff --git a/templates/electron/package.json b/templates/electron/package.json index 6ab69dd74..dcc18ff40 100644 --- a/templates/electron/package.json +++ b/templates/electron/package.json @@ -13,11 +13,11 @@ "package": "node build.mjs" }, "dependencies": { - "@babylonjs/addons": "9.0.0", - "@babylonjs/core": "9.0.0", - "@babylonjs/gui": "9.0.0", - "@babylonjs/havok": "1.3.10", - "@babylonjs/materials": "9.0.0", + "@babylonjs/addons": "9.2.1", + "@babylonjs/core": "9.2.1", + "@babylonjs/gui": "9.2.1", + "@babylonjs/havok": "1.3.12", + "@babylonjs/materials": "9.2.1", "babylonjs-editor-tools": "latest" }, "devDependencies": { diff --git a/templates/nextjs/package.json b/templates/nextjs/package.json index 79b53d926..7d00e360a 100644 --- a/templates/nextjs/package.json +++ b/templates/nextjs/package.json @@ -10,11 +10,11 @@ "lint": "next lint" }, "dependencies": { - "@babylonjs/addons": "9.0.0", - "@babylonjs/core": "9.0.0", - "@babylonjs/gui": "9.0.0", - "@babylonjs/havok": "1.3.10", - "@babylonjs/materials": "9.0.0", + "@babylonjs/addons": "9.2.1", + "@babylonjs/core": "9.2.1", + "@babylonjs/gui": "9.2.1", + "@babylonjs/havok": "1.3.12", + "@babylonjs/materials": "9.2.1", "babylonjs-editor-tools": "latest", "next": "16.2.3", "react": "18.2.0", diff --git a/templates/solidjs/package.json b/templates/solidjs/package.json index 9d599d391..c64ce52bc 100644 --- a/templates/solidjs/package.json +++ b/templates/solidjs/package.json @@ -12,11 +12,11 @@ "serve": "vite preview" }, "dependencies": { - "@babylonjs/addons": "9.0.0", - "@babylonjs/core": "9.0.0", - "@babylonjs/gui": "9.0.0", - "@babylonjs/havok": "1.3.10", - "@babylonjs/materials": "9.0.0", + "@babylonjs/addons": "9.2.1", + "@babylonjs/core": "9.2.1", + "@babylonjs/gui": "9.2.1", + "@babylonjs/havok": "1.3.12", + "@babylonjs/materials": "9.2.1", "@solidjs/router": "^0.15.3", "babylonjs-editor-tools": "latest", "solid-js": "^1.9.5" diff --git a/templates/vanillajs/package.json b/templates/vanillajs/package.json index 6fcf496c6..3b2a3dc89 100644 --- a/templates/vanillajs/package.json +++ b/templates/vanillajs/package.json @@ -12,11 +12,11 @@ "serve": "vite preview" }, "dependencies": { - "@babylonjs/addons": "9.0.0", - "@babylonjs/core": "9.0.0", - "@babylonjs/gui": "9.0.0", - "@babylonjs/havok": "1.3.10", - "@babylonjs/materials": "9.0.0", + "@babylonjs/addons": "9.2.1", + "@babylonjs/core": "9.2.1", + "@babylonjs/gui": "9.2.1", + "@babylonjs/havok": "1.3.12", + "@babylonjs/materials": "9.2.1", "babylonjs-editor-tools": "latest" }, "devDependencies": { diff --git a/tools/package.json b/tools/package.json index c7d312d62..c75b6e302 100644 --- a/tools/package.json +++ b/tools/package.json @@ -25,9 +25,9 @@ "typings": "declaration/src/index.d.ts", "license": "(Apache-2.0)", "devDependencies": { - "@babylonjs/addons": "9.0.0", - "@babylonjs/core": "9.0.0", - "@babylonjs/gui": "9.0.0", + "@babylonjs/addons": "9.2.1", + "@babylonjs/core": "9.2.1", + "@babylonjs/gui": "9.2.1", "@vitest/coverage-v8": "4.0.17", "esbuild": "0.27.2", "typescript": "5.9.3", diff --git a/website/package.json b/website/package.json index 71b300588..5939d2e14 100644 --- a/website/package.json +++ b/website/package.json @@ -10,8 +10,8 @@ "postbuild": "next-sitemap" }, "dependencies": { - "@babylonjs/core": "9.0.0", - "@babylonjs/materials": "9.0.0", + "@babylonjs/core": "9.2.1", + "@babylonjs/materials": "9.2.1", "@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-slot": "1.2.4", "babylonjs-editor-tools": "link:../tools", diff --git a/yarn.lock b/yarn.lock index 4af5e0507..c764f2e26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -930,30 +930,20 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" -"@babylonjs/addons@8.41.0": - version "8.41.0" - resolved "https://registry.yarnpkg.com/@babylonjs/addons/-/addons-8.41.0.tgz#18108e3887d6fdc3640884336cfddbd004bf28e8" - integrity sha512-qnpwfmn6CFqbimf6aL066UYOWlwAyeTCKDr40gjhksxA9DWPBO81pNIXZg24YBL3kbVFtpikPeFeaS6ZtZHgPA== - -"@babylonjs/addons@9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@babylonjs/addons/-/addons-9.0.0.tgz#69e87983896fcff659a004a50ebf1d36dd203fe1" - integrity sha512-yGzXy0RA1KfxauCazmcgHO1Pjht/gjJSrH18yakkDoLb1IDZZXr0JJ97pzEmafROMaEdna716oKHex1dIv9hYg== - -"@babylonjs/core@8.41.0": - version "8.41.0" - resolved "https://registry.yarnpkg.com/@babylonjs/core/-/core-8.41.0.tgz#a7109cba0c269d8cb32c47b0dc21ddd8cc009578" - integrity sha512-zNXebahfCCjOu2VzHtE/EpAFSFnBnvYIK/iTgP1L18XbHJsDjyBPQNmuqYiSgP9pBj/Vkxr6AAZdnuqlHUB7BA== - -"@babylonjs/core@9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@babylonjs/core/-/core-9.0.0.tgz#c12206eb48ff64ae543b02aba851a2acd360c56e" - integrity sha512-Y4xbHFUw28X4EC5C7NOSzPCOaenxZQgztCB1QZ64y0C85FjZaq5DfWd6gy+EUYklbigmcMIFzCpLj78Wc8EwVw== - -"@babylonjs/gui@9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@babylonjs/gui/-/gui-9.0.0.tgz#86674cff88a810f4fe93dd1d933312f9dee225f7" - integrity sha512-PCjJAtMsMGTviA8XxqwGYO/rAz9gR+RwUK3anSx1QzKYnRkJ5jmD6yYprEMzn1L7PdXylq+rn/egMv1CPRa+7A== +"@babylonjs/addons@8.41.0", "@babylonjs/addons@9.2.1": + version "9.2.1" + resolved "https://registry.yarnpkg.com/@babylonjs/addons/-/addons-9.2.1.tgz#dc7c7471e18eff4963088a8339fea06dd9129f02" + integrity sha512-fZCgF+JeuVsa4f/i4GK8ZT7UYLQNGelcOeh4brDLAaKUNxEURkoapAbc7WA8lIsAaD5kVqAgVeVf2U95I+lhlw== + +"@babylonjs/core@8.41.0", "@babylonjs/core@9.2.1": + version "9.2.1" + resolved "https://registry.yarnpkg.com/@babylonjs/core/-/core-9.2.1.tgz#839656f12e370104a82b39efdeb593c65cc0203b" + integrity sha512-G3409wiBQTMJPzbTPV8Lz36cfmCsvp2+7nXYRisnMet1qnelogWXM+XOcZZIzDjMCwV7eII1UzETnnS0GpBfTQ== + +"@babylonjs/gui@9.2.1": + version "9.2.1" + resolved "https://registry.yarnpkg.com/@babylonjs/gui/-/gui-9.2.1.tgz#4347001e94e9bfd20b29ad1bf442f12cb31a46e3" + integrity sha512-PFOHbhfE7R3VMtJskhSkZ6qUJwCFL/piy4nFhDO7V0l7QWTCv3bGu/alx58i8dL4N0qMAydBspiNccYGEXKmMQ== "@babylonjs/havok@1.3.10": version "1.3.10" @@ -962,10 +952,17 @@ dependencies: "@types/emscripten" "^1.39.6" -"@babylonjs/materials@9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@babylonjs/materials/-/materials-9.0.0.tgz#d37e7c148c6aae0986d88c32ecd463bcbf43f441" - integrity sha512-wATGzT/IQZ9/h9VhjgrZt8W59ZWXZ/mEZdCP2zHdopz3fqKP3gpJlbGt8UpAQUQF6XFKJrGuzVe7Vesrg36vyw== +"@babylonjs/havok@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@babylonjs/havok/-/havok-1.3.12.tgz#6bf15c952883aa492ab9e2e064830e6fc815c232" + integrity sha512-KR5Z7DBkVEgdvHLMDh2VWe/nHvUG8+MdLBiAE0iM19KIHAPqPRVITPAZKx4SQusK5nqm4ZXDcKv5OYtViIxLzA== + dependencies: + "@types/emscripten" "^1.39.6" + +"@babylonjs/materials@9.2.1": + version "9.2.1" + resolved "https://registry.yarnpkg.com/@babylonjs/materials/-/materials-9.2.1.tgz#5ee699a1cba7f8849a7a1beccbca0e1e20d47c32" + integrity sha512-Jdk/i/PBlDkJdmQLJI+efBB9+Ut4bPcVCYjEM1PpAQl+mzuxehD6yBzCGndYkI+Pv9aqhc410TN/Q3wkR2GbBA== "@bcoe/v8-coverage@^1.0.2": version "1.0.2" @@ -4856,12 +4853,12 @@ babel-preset-solid@^1.8.4: dependencies: babel-plugin-jsx-dom-expressions "^0.39.8" -babylonjs-addons@8.41.0, babylonjs-addons@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/babylonjs-addons/-/babylonjs-addons-9.0.0.tgz#192409851a30151c51bfcd8fc2f121b047a79694" - integrity sha512-epPMmRdSw9UMOmsUyNs/557+AVdskznlCxdTfdSPUMrG+UEdTFBgjqpz0vCGxd2c/VW1XBm7zVxa1aO2pqWMHA== +babylonjs-addons@8.41.0, babylonjs-addons@9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/babylonjs-addons/-/babylonjs-addons-9.2.1.tgz#6fd960882a507eec15926a0b9f432f2879cf5b1d" + integrity sha512-fu8H8LxT7RMTPvQB6dzwWwmgUDlRy2X1AYRSYhujwz63nreZST4O8nZba8na6mEyLnuJRE1i95Okm5EU4l33wA== dependencies: - babylonjs "9.0.0" + babylonjs "9.2.1" babylonjs-editor-cli@latest: version "5.0.0" @@ -4885,9 +4882,10 @@ babylonjs-editor-tools@latest: "babylonjs-editor-tools@link:../../../Library/Caches/Yarn/v6/npm-babylonjs-editor-5.2.4-3cce3a704dc0c4572a85041a993264060376230a-integrity/node_modules/tools": version "0.0.0" + uid "" "babylonjs-editor-tools@link:tools": - version "5.4.0" + version "5.4.1-alpha.3" babylonjs-editor@latest: version "5.2.4" @@ -4976,73 +4974,73 @@ babylonjs-editor@latest: usehooks-ts "^3.1.0" webm-muxer "^5.0.2" -babylonjs-gltf2interface@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/babylonjs-gltf2interface/-/babylonjs-gltf2interface-9.0.0.tgz#0f4a3f63104a73bd7b04f4b1e281b8dad239be30" - integrity sha512-k2B6B39stVxJcATNqPGJp19732pQouZPGiMsa1uke3812ZPd2M3h2Xm+QHpRo9n8wPMFC2tPUVu+dSka0skR1w== +babylonjs-gltf2interface@9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/babylonjs-gltf2interface/-/babylonjs-gltf2interface-9.2.1.tgz#59b5eec5285e466207e4dc2845289f5d3d5d34de" + integrity sha512-xkB0bpnDIv1ztAKv8Ph6j8s3vsLmAdAByObLq9SOQIppfcCF9iQjpKzz4l9Ul4EA3hd/RXO2Yz97s22R7beG4Q== -babylonjs-gui-editor@8.41.0, babylonjs-gui-editor@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/babylonjs-gui-editor/-/babylonjs-gui-editor-9.0.0.tgz#58d97c4d5752d24de882fd57a19762a894ea9e0f" - integrity sha512-IUwY3gq9fbvSEwgoO82kNM4iUCrkgEPjlfjXP32sQlMQzvQpEfqfLHN/4nZHZsSBSPIZnr4HTN7zl1wHgxp7XA== +babylonjs-gui-editor@8.41.0, babylonjs-gui-editor@9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/babylonjs-gui-editor/-/babylonjs-gui-editor-9.2.1.tgz#88d7164eb75c1a1fa2b246cb8817a7f56264de22" + integrity sha512-R0rmOQglVzWvnHghRUkNov7ZJBj44UWnKl7SlOf6Gfmux1zc/Lwu8PSd0MhNkYxqO5vhf2QE0SVsPutb9vkPUA== dependencies: - babylonjs "9.0.0" - babylonjs-gui "9.0.0" + babylonjs "9.2.1" + babylonjs-gui "9.2.1" -babylonjs-gui@8.41.0, babylonjs-gui@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/babylonjs-gui/-/babylonjs-gui-9.0.0.tgz#dc7f0608cc9fcceaf6d4cdf4b70476f193c0e9c3" - integrity sha512-kBZUHZpJmB/pgs95RXVMGQJjHYC1hEPyE/9emjjGvQAGHARCUTsyJdN4lVY+VoeazM/3oy3vXMZCysAwg0eDew== +babylonjs-gui@8.41.0, babylonjs-gui@9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/babylonjs-gui/-/babylonjs-gui-9.2.1.tgz#f80dcb655db7c4b418f7ee2c5fcfbdd19c3ff96a" + integrity sha512-Px/2+g/swpelVwEQoev/h+foVWHK0LebehxplCLuJIhgmYDesPqnm4+8KY1oL1TJZG78lAwZW7ue3DHEM13dBg== dependencies: - babylonjs "9.0.0" + babylonjs "9.2.1" -babylonjs-loaders@8.41.0, babylonjs-loaders@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/babylonjs-loaders/-/babylonjs-loaders-9.0.0.tgz#5708840fc3bca6583abf76a8b77bc1c13b670dcd" - integrity sha512-hrbXvHi8gvdOcqdMk9Zyx9A7WQ7SJ+Svt4s1IP5bUVLabWIrqo8oBHWpV4Ybw5spK48SE8EjvRPe4xwK0IbNEA== +babylonjs-loaders@8.41.0, babylonjs-loaders@9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/babylonjs-loaders/-/babylonjs-loaders-9.2.1.tgz#bfb61b81f9466286e79b53f25c6ac46320529fa5" + integrity sha512-aL6zegOiA4kXYRn3YRc4Yg34+8YHA+AetP0bv0NVxRFhLC83f+qlFZ7rat+z1LAK1Guqc5dimJ5vxSn7flQ1UQ== dependencies: - babylonjs "9.0.0" - babylonjs-gltf2interface "9.0.0" + babylonjs "9.2.1" + babylonjs-gltf2interface "9.2.1" -babylonjs-materials@8.41.0, babylonjs-materials@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/babylonjs-materials/-/babylonjs-materials-9.0.0.tgz#a133f3e150f3c8ced648b24311aa8d656d08b920" - integrity sha512-4Ua2xfM3Oe7xE5CnZiEyyRWdwBajPOgV5dNlJkLBn5MZUCUU8mptHClYGTnIhDnCIMZg1FWNpqgC3StD3k7XQQ== +babylonjs-materials@8.41.0, babylonjs-materials@9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/babylonjs-materials/-/babylonjs-materials-9.2.1.tgz#1bfc65ce423012bef27e482e5f49eb565817e723" + integrity sha512-8THJOvL6e9NZr5uHu5i+VObrSYXVHHfEVyI8sElYB3oasVK/emeQ0YpccvWpOoooEezUfMEP7PXrxIxeJIH+IQ== dependencies: - babylonjs "9.0.0" + babylonjs "9.2.1" -babylonjs-node-editor@8.41.0, babylonjs-node-editor@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/babylonjs-node-editor/-/babylonjs-node-editor-9.0.0.tgz#1287e69082882f5f8fa75b82d3d08e682fbc632d" - integrity sha512-Npqo3WXiBHqJ0Ef5vOymjySCmzFoyhzZFUguCwGlcx5rb5AFHPZynqd6irxdtHDLIV3oIeK3P3PBt5G7mwO9CQ== +babylonjs-node-editor@8.41.0, babylonjs-node-editor@9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/babylonjs-node-editor/-/babylonjs-node-editor-9.2.1.tgz#13398bcdbafe099ae5f544beb5dc52357d850890" + integrity sha512-qEPFJKvqzuAMr1jFImUzAlkNyhhuZ5I4GtDbCbG7j/g/h1+2gIBOk0JVNmYK4gwAKKr68jMZgXKahZ1VdaB9IQ== dependencies: - babylonjs "9.0.0" + babylonjs "9.2.1" -babylonjs-node-particle-editor@8.41.0, babylonjs-node-particle-editor@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/babylonjs-node-particle-editor/-/babylonjs-node-particle-editor-9.0.0.tgz#d489a505bea31cd72275cd5b96d9849a0d4e8844" - integrity sha512-Hix/H+D4BZrzx6jjc8jlR3J2u7hPkz/USD5drW2gnMBiFnmKO4PSXlak6tv/nxPF+qAVAW8OBtXAixFwQi+SOg== +babylonjs-node-particle-editor@8.41.0, babylonjs-node-particle-editor@9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/babylonjs-node-particle-editor/-/babylonjs-node-particle-editor-9.2.1.tgz#79ce8801c3b0ce83a802483e26ea873d8616ff94" + integrity sha512-hzIxLN+yS8KFTVaIrjNr7BlbkKv/cci4KBFi+SX0s8Mcc68yGdE+SrwRlmGTLgFEP6Zmmj5TKn58ft2YL+kKGQ== dependencies: - babylonjs "9.0.0" + babylonjs "9.2.1" -babylonjs-post-process@8.41.0, babylonjs-post-process@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/babylonjs-post-process/-/babylonjs-post-process-9.0.0.tgz#876e61c064cb322bb8a9d36dedca594850da77c0" - integrity sha512-i5htaO9diAd5GVrMwS3KGCTwrkhOGA+1MxUpK8k4eUfp+jHTbWwxOIPKN10UxllbYMT+McTuhYgpVD/2JAqyMw== +babylonjs-post-process@8.41.0, babylonjs-post-process@9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/babylonjs-post-process/-/babylonjs-post-process-9.2.1.tgz#4cb2680bfb2c06c0f200a403b750d9ec4a5be25e" + integrity sha512-6UeVSWBr5xy6T5vOB3bz80CCqbK8qVcrTxIQkr2PCpfSeHJ3C7P0PxWCWJGQJDVwxx0U4NqCBjbT8iE9IYQKbw== dependencies: - babylonjs "9.0.0" + babylonjs "9.2.1" -babylonjs-procedural-textures@8.41.0, babylonjs-procedural-textures@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/babylonjs-procedural-textures/-/babylonjs-procedural-textures-9.0.0.tgz#b170fc2471d9142c11104e4ca7ffa405472bd0f4" - integrity sha512-LvJew4YlV52mpaGuAPMEjlWNlagxO0/SXNlDbJr1czQuN9q2GZOyx7ATUMkrei5ixzsAZd+R0U/v+EkSlyij1g== +babylonjs-procedural-textures@8.41.0, babylonjs-procedural-textures@9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/babylonjs-procedural-textures/-/babylonjs-procedural-textures-9.2.1.tgz#3a4ad34313035fe040b8cc904d38fda4cf7604c2" + integrity sha512-F5X3jxmIaNu3PqiF79NGhbjXL5X/T1f0ZtxSH38uPmIiFWIKuoW8uC4ntLutt1ZNNc39SpxBx6P4xT+ejyJj0A== dependencies: - babylonjs "9.0.0" + babylonjs "9.2.1" -babylonjs@8.41.0, babylonjs@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/babylonjs/-/babylonjs-9.0.0.tgz#dc26bbf508d90cedf654d0bb9ffee785695b7905" - integrity sha512-NlHOf9R6GwHMoYZ+9uT0VxzhFwjoMqc9HT0o/TpEAymZZKZOa15IS2be8QTBpgHCOq0T3t3gwWq9ecpw3IQlIw== +babylonjs@8.41.0, babylonjs@9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/babylonjs/-/babylonjs-9.2.1.tgz#216f3552884ea3bcabe02588fdb4057258705e9a" + integrity sha512-7QpgiKOjynr/Fq1ES4sT/ez1n61EbMY9zulNGxmzQ0Xz5DY6TtpwSlSafcJ11TnJ+LjOgwpySdslzTA4f+157w== balanced-match@^1.0.0: version "1.0.2"