From 937606e27efa0eabfe94c39e42bcb6aab59e7a22 Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Fri, 24 Jul 2026 03:55:27 +0200 Subject: [PATCH 01/22] feat: adding support of gaussian splatting meshes --- editor/package.json | 2 + editor/src/editor/layout/assets-browser.tsx | 8 ++++ .../layout/assets-browser/items/mesh-item.tsx | 2 +- editor/src/editor/layout/preview.tsx | 7 +++- .../editor/layout/preview/import/import.ts | 38 +++++++++++++++--- editor/src/mcp/assets/assets.ts | 2 +- editor/src/project/load/plugins/meshes.ts | 17 +++++++- editor/src/project/save/scene.ts | 39 ++++++++++++++++++- editor/src/tools/guards/nodes.ts | 13 +++++++ editor/src/tools/mesh/gaussian-splatting.ts | 30 ++++++++++++++ editor/src/tools/workers/thumbnail/mesh.ts | 2 +- plugins/fab/src/import/mesh.ts | 2 +- tools/src/tools/guards.ts | 9 +++++ yarn.lock | 8 +++- 14 files changed, 163 insertions(+), 16 deletions(-) create mode 100644 editor/src/tools/mesh/gaussian-splatting.ts diff --git a/editor/package.json b/editor/package.json index 0e302a04d..20c1bae6c 100644 --- a/editor/package.json +++ b/editor/package.json @@ -40,6 +40,7 @@ "vitest": "4.1.0" }, "dependencies": { + "@adobe/spz": "0.2.2", "@babylonjs/addons": "9.12.1", "@babylonjs/core": "9.12.1", "@babylonjs/havok": "1.3.12", @@ -100,6 +101,7 @@ "dunder-proto": "1.0.1", "electron-updater": "6.6.2", "esbuild": "0.28.1", + "fflate": "0.8.3", "filenamify": "4.3.0", "flexlayout-react": "0.7.15", "fluent-ffmpeg": "^2.1.3", diff --git a/editor/src/editor/layout/assets-browser.tsx b/editor/src/editor/layout/assets-browser.tsx index c46e29888..8944561cc 100644 --- a/editor/src/editor/layout/assets-browser.tsx +++ b/editor/src/editor/layout/assets-browser.tsx @@ -895,6 +895,10 @@ export class EditorAssetsBrowser extends Component; @@ -1474,6 +1478,10 @@ export class EditorAssetsBrowser extends Component { if (pick.pickedPoint) { diff --git a/editor/src/editor/layout/preview/import/import.ts b/editor/src/editor/layout/preview/import/import.ts index b5bc3273d..f844ce017 100644 --- a/editor/src/editor/layout/preview/import/import.ts +++ b/editor/src/editor/layout/preview/import/import.ts @@ -1,5 +1,5 @@ import { isAbsolute } from "path"; -import { join, dirname, basename } from "path/posix"; +import { join, dirname, basename, extname } from "path/posix"; import { pathExists, readFile, readJSON, writeFile } from "fs-extra"; import axios from "axios"; @@ -20,15 +20,19 @@ import { Sprite, IParticleSystem, HDRCubeTexture, + GaussianSplattingMesh, } from "babylonjs"; +import * as fflate from "fflate"; + import { UniqueNumber } from "../../../../tools/tools"; -import { isMesh } from "../../../../tools/guards/nodes"; import { isSprite } from "../../../../tools/guards/sprites"; import { isTexture } from "../../../../tools/guards/texture"; import { executeSimpleWorker } from "../../../../tools/worker"; import { isMultiMaterial } from "../../../../tools/guards/material"; +import { isGaussianSplattingMesh, isMesh } from "../../../../tools/guards/nodes"; import { configureSimultaneousLightsForMaterial } from "../../../../tools/material/material"; +import { removeGaussianSplattingCameraMeshes } from "../../../../tools/mesh/gaussian-splatting"; import { onNodesAddedObservable, onTextureAddedObservable } from "../../../../tools/observables"; import { projectConfiguration } from "../../../../project/configuration"; @@ -62,7 +66,7 @@ export async function tryConvertSceneFile(absolutePath: string, progress?: (perc } } -export async function loadImportedSceneFile(scene: Scene, absolutePath: string) { +export async function loadImportedSceneFile(scene: Scene, absolutePath: string, appPath: string | null) { if (!projectConfiguration.path) { return null; } @@ -70,8 +74,17 @@ export async function loadImportedSceneFile(scene: Scene, absolutePath: string) let result: ISceneLoaderAsyncResult; try { + const nodeModules = process.env.DEBUG ? "../node_modules" : "node_modules"; + result = await ImportMeshAsync(basename(absolutePath), scene, { rootUrl: join(dirname(absolutePath), "/"), + pluginOptions: { + splat: { + fflate, + spzLibraryUrl: join(appPath ?? "", nodeModules, "@adobe/spz/dist/spz.js"), + gaussianSplattingMesh: new GaussianSplattingMesh(basename(absolutePath), null, scene, true), + }, + }, }); // result = await SceneLoader.ImportMeshAsync("", join(dirname(absolutePath), "/"), basename(absolutePath), scene); } catch (e) { @@ -92,7 +105,20 @@ export async function loadImportedSceneFile(scene: Scene, absolutePath: string) result.meshes.forEach((mesh) => { configureImportedNodeIds(mesh); - mesh.receiveShadows = true; + if (isGaussianSplattingMesh(mesh)) { + mesh.scaling.scaleInPlace(100); + + removeGaussianSplattingCameraMeshes(mesh); + + switch (extname(absolutePath).toLowerCase()) { + case ".sog": + case ".spz": + mesh.rotation.x = Math.PI; + break; + } + } else { + mesh.receiveShadows = true; + } if (mesh.skeleton) { mesh.skeleton.id = Tools.RandomId(); @@ -127,7 +153,9 @@ export async function loadImportedSceneFile(scene: Scene, absolutePath: string) } result.meshes.forEach((mesh) => { - shadowMap.renderList!.push(mesh); + if (!isGaussianSplattingMesh(mesh)) { + shadowMap.renderList!.push(mesh); + } }); }); diff --git a/editor/src/mcp/assets/assets.ts b/editor/src/mcp/assets/assets.ts index 00d901261..ef1a55151 100644 --- a/editor/src/mcp/assets/assets.ts +++ b/editor/src/mcp/assets/assets.ts @@ -152,7 +152,7 @@ export async function getAssetPreview(_scene: Scene, data: any): Promise { export async function instantiateMeshAsset(scene: Scene, data: any, options: IMCPActionOptions): Promise { const absolutePath = resolveProjectPath(data.path); - const result = await loadImportedSceneFile(scene, absolutePath); + const result = await loadImportedSceneFile(scene, absolutePath, options.editor.path); if (!result) { throw new Error(`Failed to load mesh asset: ${data.path}`); } diff --git a/editor/src/project/load/plugins/meshes.ts b/editor/src/project/load/plugins/meshes.ts index 449f34a3c..e9d5d342d 100644 --- a/editor/src/project/load/plugins/meshes.ts +++ b/editor/src/project/load/plugins/meshes.ts @@ -1,7 +1,7 @@ import { join } from "path/posix"; -import { readJSON } from "fs-extra"; +import { readFile, readJSON } from "fs-extra"; -import { Scene, Constants, Matrix, Mesh, SceneLoader, MultiMaterial, Geometry } from "babylonjs"; +import { Scene, Constants, Matrix, Mesh, SceneLoader, MultiMaterial, Geometry, GaussianSplattingMesh } from "babylonjs"; import { ISceneLoaderPluginOptions } from "../scene"; @@ -10,6 +10,7 @@ import { isCollisionMesh, isMesh } from "../../../tools/guards/nodes"; import { isMultiMaterial, isNodeMaterial } from "../../../tools/guards/material"; import { parsePhysicsAggregate } from "../../../tools/physics/serialization/aggregate"; import { configureSimultaneousLightsForMaterial, normalizeNodeMaterialUniqueIds } from "../../../tools/material/material"; +import { configureGaussianSplattingMeshFromData, removeGaussianSplattingCameraMeshes } from "../../../tools/mesh/gaussian-splatting"; import { CollisionMesh } from "../../../editor/nodes/collision"; @@ -26,6 +27,18 @@ export async function loadMeshes(meshesFiles: string[], scene: Scene, options: I return; } + if (initialData.type === "GaussianSplattingMesh") { + const splatBuffer = await readFile(join(options.projectPath, initialData.splatDataPath)); + initialData.splatsData = splatBuffer.buffer; + + const parsedMesh = GaussianSplattingMesh.Parse(initialData, scene); + + configureGaussianSplattingMeshFromData(parsedMesh, initialData); + removeGaussianSplattingCameraMeshes(parsedMesh); + + return [parsedMesh]; + } + const filesToLoad = [join(options.relativeScenePath, "meshes", file), ...(initialData.lods?.map((file) => join(options.relativeScenePath, "lods", file)) ?? [])]; return await Promise.all( diff --git a/editor/src/project/save/scene.ts b/editor/src/project/save/scene.ts index 2faf4a9b3..7cabc58cf 100644 --- a/editor/src/project/save/scene.ts +++ b/editor/src/project/save/scene.ts @@ -19,7 +19,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, isClusteredLightContainer, isCollisionMesh, isEditorCamera, isMesh, isTransformNode } from "../../tools/guards/nodes"; +import { isAnyTransformNode, isClusteredLightContainer, isCollisionMesh, isEditorCamera, isGaussianSplattingMesh, isMesh, isTransformNode } from "../../tools/guards/nodes"; import { taaPipelineCameraConfigurations } from "../../editor/rendering/taa"; import { vlsPostProcessCameraConfigurations } from "../../editor/rendering/vls"; @@ -56,6 +56,7 @@ export function ensureSceneFolders(scenePath: string) { createDirectoryIfNotExist(join(scenePath, "sprite-maps")), createDirectoryIfNotExist(join(scenePath, "sprite-managers")), createDirectoryIfNotExist(join(scenePath, "nodeParticleSystemSets")), + createDirectoryIfNotExist(join(scenePath, "splats")), ]); } @@ -99,7 +100,7 @@ export async function saveScene(editor: Editor, projectPath: string, scenePath: // Write geometries and meshes await Promise.all( meshesToSave.map(async (mesh) => { - if ((!isMesh(mesh) && !isCollisionMesh(mesh)) || mesh._masterMesh || isFromSceneLink(mesh) || !isNodeVisibleInGraph(mesh)) { + if ((!isMesh(mesh) && !isCollisionMesh(mesh)) || mesh._masterMesh || isFromSceneLink(mesh) || !isNodeVisibleInGraph(mesh) || isGaussianSplattingMesh(mesh)) { return; } @@ -291,6 +292,40 @@ export async function saveScene(editor: Editor, projectPath: string, scenePath: }) ); + // Write gaussian splatting meshes + await Promise.all( + scene.meshes.map(async (mesh) => { + if (!isGaussianSplattingMesh(mesh) || isFromSceneLink(mesh)) { + return; + } + + const meshPath = join(scenePath, "meshes", `${mesh.id}.json`); + const splatPath = join(scenePath, "splats", `${mesh.id}.babylonbinarysplatdata`); + + try { + const data = mesh.serialize( + { + splatDataPath: join(relativeScenePath, `splats/${mesh.id}.babylonbinarysplatdata`), + }, + "binary" + ); + + await writeFile(splatPath, Buffer.from(data.splatsData)); + delete data.splatsData; + + await writeJSON(meshPath, data, { + spaces: 4, + }); + } catch (e) { + editor.layout.console.error(`Failed to write gaussian splatting mesh ${mesh.name}`); + } finally { + savedFiles.push(meshPath, splatPath); + } + + dialog.step(progressStep); + }) + ); + // Write skeletons await Promise.all( scene.skeletons.map(async (skeleton) => { diff --git a/editor/src/tools/guards/nodes.ts b/editor/src/tools/guards/nodes.ts index d441dfbd9..82e76a987 100644 --- a/editor/src/tools/guards/nodes.ts +++ b/editor/src/tools/guards/nodes.ts @@ -15,6 +15,7 @@ import { HemisphericLight, Skeleton, ClusteredLightContainer, + GaussianSplattingMesh, } from "babylonjs"; import { EditorCamera } from "../../editor/nodes/camera"; @@ -35,6 +36,8 @@ export function isAbstractMesh(object: any): object is Mesh { case "GroundMesh": case "InstancedMesh": case "NodeParticleSystemSetMesh": + case "GaussianSplattingMesh": + case "GaussianSplattingMeshBase": return true; } @@ -49,6 +52,8 @@ export function isMesh(object: any): object is Mesh { switch (object.getClassName?.()) { case "Mesh": case "GroundMesh": + case "GaussianSplattingMesh": + case "GaussianSplattingMeshBase": return true; } @@ -231,3 +236,11 @@ export function isClusteredLightContainer(object: any): object is ClusteredLight export function isNode(object: any): object is Node { return isAbstractMesh(object) || isAnyTransformNode(object) || isLight(object) || isCamera(object); } + +/** + * Returns wether or not the given object is a GaussianSplattingMesh. + * @param object defines the reference to the object to test its class name. + */ +export function isGaussianSplattingMesh(object: any): object is GaussianSplattingMesh { + return object.getClassName?.() === "GaussianSplattingMesh"; +} diff --git a/editor/src/tools/mesh/gaussian-splatting.ts b/editor/src/tools/mesh/gaussian-splatting.ts new file mode 100644 index 000000000..b40c2ed4d --- /dev/null +++ b/editor/src/tools/mesh/gaussian-splatting.ts @@ -0,0 +1,30 @@ +import { GaussianSplattingMesh } from "babylonjs"; + +import { isGaussianSplattingMesh } from "../guards/nodes"; + +export function configureGaussianSplattingMeshFromData(gaussianSplattingMesh: GaussianSplattingMesh, data: any) { + gaussianSplattingMesh.name = data.name; + gaussianSplattingMesh.id = data.id; + gaussianSplattingMesh.uniqueId = data.uniqueId; + + if (data.position) { + gaussianSplattingMesh.position.copyFromFloats(data.position[0], data.position[1], data.position[2]); + } + if (data.rotation) { + gaussianSplattingMesh.rotation.copyFromFloats(data.rotation[0], data.rotation[1], data.rotation[2]); + } + if (data.rotationQuaternion) { + gaussianSplattingMesh.rotationQuaternion?.copyFromFloats(data.rotationQuaternion[0], data.rotationQuaternion[1], data.rotationQuaternion[2], data.rotationQuaternion[3]); + } + if (data.scaling) { + gaussianSplattingMesh.scaling.copyFromFloats(data.scaling[0], data.scaling[1], data.scaling[2]); + } +} + +export function removeGaussianSplattingCameraMeshes(gaussianSplattingMesh: GaussianSplattingMesh) { + gaussianSplattingMesh.material?.getBindedMeshes().forEach((gaussianMesh) => { + if (!isGaussianSplattingMesh(gaussianMesh)) { + gaussianSplattingMesh.getScene().removeMesh(gaussianMesh); + } + }); +} diff --git a/editor/src/tools/workers/thumbnail/mesh.ts b/editor/src/tools/workers/thumbnail/mesh.ts index 0c8725921..6c4c39a6c 100644 --- a/editor/src/tools/workers/thumbnail/mesh.ts +++ b/editor/src/tools/workers/thumbnail/mesh.ts @@ -52,7 +52,7 @@ export async function getPreview( }); } - return new Promise((resolve) => { + return new Promise(async (resolve) => { scene.executeWhenReady(async () => { scene.createDefaultCameraOrLight(true, true, true); scene.createDefaultEnvironment({ diff --git a/plugins/fab/src/import/mesh.ts b/plugins/fab/src/import/mesh.ts index 2332565be..3aa2b03b6 100644 --- a/plugins/fab/src/import/mesh.ts +++ b/plugins/fab/src/import/mesh.ts @@ -16,7 +16,7 @@ export async function importMesh(editor: Editor, parameters: IImportMeshParamete const dest = join(parameters.finalAssetsFolder, basename(parameters.json.file)); const rootNodes: Node[] = []; - const result = await loadImportedSceneFile(editor.layout.preview.scene, dest); + const result = await loadImportedSceneFile(editor.layout.preview.scene, dest, editor.path); result?.meshes.forEach((mesh) => { mesh.material = parameters.materialsMap.get(parameters.json.material_index) ?? mesh.material; diff --git a/tools/src/tools/guards.ts b/tools/src/tools/guards.ts index 2beb71c0d..81bd2d95e 100644 --- a/tools/src/tools/guards.ts +++ b/tools/src/tools/guards.ts @@ -7,6 +7,7 @@ import { GroundMesh } from "@babylonjs/core/Meshes/groundMesh"; import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh"; import { InstancedMesh } from "@babylonjs/core/Meshes/instancedMesh"; import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; +import { GaussianSplattingMesh } from "@babylonjs/core/Meshes/GaussianSplatting/gaussianSplattingMesh"; import { Texture } from "@babylonjs/core/Materials/Textures/texture"; @@ -257,3 +258,11 @@ export function isSprite(object: any): object is Sprite { export function isSoundNode(object: any): object is SoundNode { return object.getClassName?.() === "SoundNode"; } + +/** + * Returns wether or not the given object is a GaussianSplattingMesh. + * @param object defines the reference to the object to test its class name. + */ +export function isGaussianSplattingMesh(object: any): object is GaussianSplattingMesh { + return object.getClassName?.() === "GaussianSplattingMesh"; +} diff --git a/yarn.lock b/yarn.lock index adeca242a..f705bff91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,11 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== +"@adobe/spz@0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@adobe/spz/-/spz-0.2.2.tgz#f2c3547a62e63daa1e5a52a36204300fceeed012" + integrity sha512-KUCw5R52rCBnfyvOOIa9nUQGfR6TEcHjYzuWDYRGb1fpkpnkci6VXFqCx/xFJ1yEN7V5gc9YxU2zIY6sEjIbhw== + "@alloc/quick-lru@^5.2.0": version "5.2.0" resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" @@ -7878,6 +7883,7 @@ 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.3-alpha.4" @@ -10771,7 +10777,7 @@ fetch-blob@^3.1.2, fetch-blob@^3.1.4: node-domexception "^1.0.0" web-streams-polyfill "^3.0.3" -fflate@^0.8.2: +fflate@0.8.3, fflate@^0.8.2: version "0.8.3" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.3.tgz#bc27d8eb30343d4d512abb03480202ce65d825fc" integrity sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA== From 55895a6c5a55e177d4f4e984469fd9372f493e50 Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Fri, 24 Jul 2026 11:15:38 +0200 Subject: [PATCH 02/22] fix: clone of gaussian splatting --- editor/src/editor/layout/graph.tsx | 12 +++++++++--- .../layout/inspector/light/components/shadows.tsx | 8 ++++++-- editor/src/editor/layout/inspector/mesh/mesh.tsx | 10 +++++----- editor/src/editor/nodes/sound.ts | 4 ++++ editor/src/mcp/lights/lights.ts | 4 ++-- editor/src/tools/light/ibl.ts | 4 ++-- editor/src/tools/light/shadows.ts | 6 +++++- editor/src/tools/node/clone.ts | 3 +++ 8 files changed, 36 insertions(+), 15 deletions(-) diff --git a/editor/src/editor/layout/graph.tsx b/editor/src/editor/layout/graph.tsx index 20eed09af..638223599 100644 --- a/editor/src/editor/layout/graph.tsx +++ b/editor/src/editor/layout/graph.tsx @@ -60,6 +60,7 @@ import { isCollisionInstancedMesh, isCollisionMesh, isEditorCamera, + isGaussianSplattingMesh, isInstancedMesh, isLight, isMesh, @@ -524,7 +525,10 @@ export class EditorGraph extends Component } const newNodes: (Node | IParticleSystem | Sprite)[] = []; - const nodesToCopy = this._objectsToCopy.map((n) => n.nodeData); + const nodesToCopy = this._objectsToCopy.map((n) => n.nodeData).filter((n) => !isGaussianSplattingMesh(n)); + if (!nodesToCopy.length) { + return; + } registerUndoRedo({ executeRedo: true, @@ -558,7 +562,7 @@ export class EditorGraph extends Component nodesToCopy.forEach((object) => { let node: Node | IParticleSystem | Sprite | null = null; - if (isAbstractMesh(object) && !isNodeParticleSystemSetMesh(object)) { + if (isAbstractMesh(object) && !isNodeParticleSystemSetMesh(object) && !isGaussianSplattingMesh(object)) { const suffix = "(Instanced Mesh)"; const name = isInstancedMesh(object) ? object.name : `${object.name.replace(` ${suffix}`, "")} ${suffix}`; @@ -582,6 +586,8 @@ export class EditorGraph extends Component const name = `${object.name.replace(` ${suffix}`, "")} ${suffix}`; node = object.clone(name, parent, false); + } else if (isGaussianSplattingMesh(object)) { + // TODO } else if (isNode(object) || isSprite(object)) { node = cloneNode(this.props.editor, object); } @@ -602,7 +608,7 @@ export class EditorGraph extends Component } } - if (isAbstractMesh(node)) { + if (isAbstractMesh(node) && !isGaussianSplattingMesh(node)) { this.props.editor.layout.preview.scene.lights .map((light) => light.getShadowGenerator()) .forEach((generator) => generator?.getShadowMap()?.renderList?.push(node)); diff --git a/editor/src/editor/layout/inspector/light/components/shadows.tsx b/editor/src/editor/layout/inspector/light/components/shadows.tsx index 757c5070d..e949a8ffb 100644 --- a/editor/src/editor/layout/inspector/light/components/shadows.tsx +++ b/editor/src/editor/layout/inspector/light/components/shadows.tsx @@ -5,8 +5,8 @@ import { CascadedShadowGenerator, DirectionalLight, IShadowGenerator, IShadowLig 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 { isDirectionalLight, isGaussianSplattingMesh, isPointLight } from "../../../../../tools/guards/nodes"; import { updateLightShadowMapRefreshRate, updatePointLightShadowMapRenderListPredicate } from "../../../../../tools/light/shadows"; import { Editor } from "../../../../main"; @@ -131,7 +131,11 @@ export class EditorLightShadowsInspector extends Component !isGaussianSplattingMesh(m)); + generator.getShadowMap()?.renderList?.push(...meshes); } this._refreshShadowGenerator(); diff --git a/editor/src/editor/layout/inspector/mesh/mesh.tsx b/editor/src/editor/layout/inspector/mesh/mesh.tsx index 3910787c2..0b884dd50 100644 --- a/editor/src/editor/layout/inspector/mesh/mesh.tsx +++ b/editor/src/editor/layout/inspector/mesh/mesh.tsx @@ -29,7 +29,7 @@ import { registerUndoRedo } from "../../../../tools/undoredo"; import { waitNextAnimationFrame } from "../../../../tools/tools"; import { onNodeModifiedObservable } from "../../../../tools/observables"; import { updateIblShadowsRenderPipeline } from "../../../../tools/light/ibl"; -import { isAbstractMesh, isInstancedMesh, isMesh } from "../../../../tools/guards/nodes"; +import { isAbstractMesh, isGaussianSplattingMesh, isInstancedMesh, isMesh } from "../../../../tools/guards/nodes"; import { updateAllLights, updateLightShadowMapRefreshRate, updatePointLightShadowMapRenderListPredicate } from "../../../../tools/light/shadows"; import { applyMaterialAssetToObject } from "../../preview/import/material"; @@ -162,11 +162,11 @@ export class EditorMeshInspector extends Component - + {!isGaussianSplattingMesh(this.props.object) && } )} - {this.props.object.getScene().lights.length > 0 && this.props.object.geometry && ( + {this.props.object.getScene().lights.length > 0 && this.props.object.geometry && !isGaussianSplattingMesh(this.props.object) && ( { lightsWithShadows.forEach((light) => { - if (enabled) { + if (enabled && !isGaussianSplattingMesh(this.props.object)) { light.getShadowGenerator()?.getShadowMap()?.renderList?.push(this.props.object); } else { const index = light.getShadowGenerator()?.getShadowMap()?.renderList?.indexOf(this.props.object); diff --git a/editor/src/editor/nodes/sound.ts b/editor/src/editor/nodes/sound.ts index 2f9f56412..7daac5b7b 100644 --- a/editor/src/editor/nodes/sound.ts +++ b/editor/src/editor/nodes/sound.ts @@ -187,6 +187,10 @@ export class SoundNode extends TransformNode { super.dispose(false, true); } + public clone(name: string): SoundNode { + return SerializationHelper.Clone(() => new SoundNode(name, this.getScene()), this); + } + /** * Gets the current object class name. * @return the class name diff --git a/editor/src/mcp/lights/lights.ts b/editor/src/mcp/lights/lights.ts index d5cf64f5e..9a2015aa1 100644 --- a/editor/src/mcp/lights/lights.ts +++ b/editor/src/mcp/lights/lights.ts @@ -1,6 +1,6 @@ import { Scene, Node, Light, ShadowGenerator, CascadedShadowGenerator, IShadowLight, DirectionalLight, SpotLight, PointLight } from "babylonjs"; -import { isLight } from "../../tools/guards/nodes"; +import { isGaussianSplattingMesh, isLight } from "../../tools/guards/nodes"; import { addPointLight, addDirectionalLight, addSpotLight, addHemisphericLight } from "../../project/add/light"; @@ -133,7 +133,7 @@ export function setLightShadows(scene: Scene, data: any, options: IMCPActionOpti generator.setDarkness(data.darkness); } - generator.getShadowMap()?.renderList?.push(...scene.meshes); + generator.getShadowMap()?.renderList?.push(...scene.meshes.filter((m) => !isGaussianSplattingMesh(m))); } options.editor.layout.inspector.setEditedObject(node); diff --git a/editor/src/tools/light/ibl.ts b/editor/src/tools/light/ibl.ts index ab6759150..c1604079c 100644 --- a/editor/src/tools/light/ibl.ts +++ b/editor/src/tools/light/ibl.ts @@ -4,7 +4,7 @@ import { getIblShadowsRenderingPipeline } from "../../editor/rendering/ibl-shado import { unique } from "../tools"; -import { isMesh } from "../guards/nodes"; +import { isGaussianSplattingMesh, isMesh } from "../guards/nodes"; export function updateIblShadowsRenderPipeline(scene: Scene, updateVoxelization?: boolean) { const iblShadowRenderPipeline = getIblShadowsRenderingPipeline(); @@ -23,7 +23,7 @@ export function updateIblShadowsRenderPipeline(scene: Scene, updateVoxelization? const shadowGenerators = scene.lights.map((l) => l.getShadowGenerator()); shadowGenerators.forEach((shadowGenerator) => { shadowGenerator?.getShadowMap()?.renderList?.forEach((mesh) => { - if (isMesh(mesh)) { + if (isMesh(mesh) && !isGaussianSplattingMesh(mesh)) { meshes.push(mesh); } }); diff --git a/editor/src/tools/light/shadows.ts b/editor/src/tools/light/shadows.ts index d70c0960a..ece4dec6a 100644 --- a/editor/src/tools/light/shadows.ts +++ b/editor/src/tools/light/shadows.ts @@ -1,6 +1,6 @@ import { Light, RenderTargetTexture, Scene, Vector3 } from "babylonjs"; -import { isPointLight, isSpotLight } from "../guards/nodes"; +import { isGaussianSplattingMesh, isPointLight, isSpotLight } from "../guards/nodes"; /** * Updates the shadow map render list predicate of the given point light. @@ -19,6 +19,10 @@ export function updatePointLightShadowMapRenderListPredicate(light: Light): void } shadowMap.renderListPredicate = (mesh) => { + if (isGaussianSplattingMesh(mesh)) { + return false; + } + const distance = Vector3.Distance(mesh.getAbsolutePosition(), light.getAbsolutePosition()); return distance <= light.range; }; diff --git a/editor/src/tools/node/clone.ts b/editor/src/tools/node/clone.ts index 3e5ddd951..fbd787ba9 100644 --- a/editor/src/tools/node/clone.ts +++ b/editor/src/tools/node/clone.ts @@ -18,6 +18,7 @@ import { isClusteredLight } from "../light/cluster"; import { parsePhysicsAggregate, serializePhysicsAggregate } from "../physics/serialization/aggregate"; import { isTexture } from "../guards/texture"; +import { isSoundNode } from "../guards/sound"; import { isSprite, isSpriteManagerNode, isSpriteMapNode } from "../guards/sprites"; import { isAnyParticleSystem, isNodeParticleSystemSetMesh } from "../guards/particles"; import { isCamera, isInstancedMesh, isLight, isMesh, isNode, isTransformNode } from "../guards/nodes"; @@ -61,6 +62,8 @@ export function cloneNode(editor: Editor, node: Node | Sprite | ParticleSystem | clone = node.clone(name, node.parent, false); } else if (isSprite(node)) { clone = cloneSprite(node); + } else if (isSoundNode(node)) { + clone = node.clone(name); } else if (isSpriteManagerNode(node)) { const serializationData = node.serialize(); From 846bf3623642f37325dde985b4d058865f88fbe4 Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Fri, 24 Jul 2026 12:44:33 +0200 Subject: [PATCH 03/22] feat: add support of gaussian splatting in babylonjs-editor-tools --- editor/src/project/export/export.tsx | 25 ++++++++++++++-- editor/src/project/save/scene.ts | 2 ++ tools/src/loading/gaussian-splatting.ts | 40 +++++++++++++++++++++++++ tools/src/loading/loader.ts | 7 +++-- tools/src/tools/light.ts | 5 ++++ 5 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 tools/src/loading/gaussian-splatting.ts diff --git a/editor/src/project/export/export.tsx b/editor/src/project/export/export.tsx index 786aae63d..f338ec393 100644 --- a/editor/src/project/export/export.tsx +++ b/editor/src/project/export/export.tsx @@ -1,5 +1,5 @@ import { join, dirname, basename, extname } from "path/posix"; -import { readJSON, readdir, remove, writeJSON } from "fs-extra"; +import { readJSON, readdir, remove, writeFile, writeJSON } from "fs-extra"; import { RenderTargetTexture, SceneSerializer } from "babylonjs"; @@ -11,7 +11,7 @@ import { getCollisionMeshFor } from "../../tools/mesh/collision"; import { storeTexturesBaseSize } from "../../tools/material/texture"; import { extractNodeMaterialTextures } from "../../tools/material/extract"; import { createDirectoryIfNotExist, normalizedGlob } from "../../tools/fs"; -import { isCollisionMesh, isEditorCamera, isMesh } from "../../tools/guards/nodes"; +import { isCollisionMesh, isEditorCamera, isGaussianSplattingMesh, isMesh } from "../../tools/guards/nodes"; import { extractNodeParticleSystemSetTextures, extractParticleSystemTextures } from "../../tools/particles/extract"; import { taaPipelineCameraConfigurations } from "../../editor/rendering/taa"; @@ -221,7 +221,26 @@ async function _exportProject(editor: Editor, options: IExportProjectOptions): P } } - const geometry = data.geometries?.vertexData?.find((v) => v.id === mesh.geometryId); + let geometry: any = null; + + if (isGaussianSplattingMesh(instantiatedMesh)) { + if (instantiatedMesh.splatsData) { + const splatPath = join(scenePath, sceneName, `${instantiatedMesh.id}.babylonbinarysplatdata`); + + try { + await writeFile(splatPath, Buffer.from(instantiatedMesh.splatsData)); + + mesh.splatDataPath = `${sceneName}/${instantiatedMesh.id}.babylonbinarysplatdata`; + delete mesh.splatsData; + + savedGeometries.push(`${instantiatedMesh.id}.babylonbinarysplatdata`); + } catch (e) { + editor.layout.console.error(`Export: Failed to write gaussian splatting data for mesh ${mesh.name}`); + } + } + } else { + geometry = data.geometries?.vertexData?.find((v) => v.id === mesh.geometryId); + } if (geometry) { const geometryFileName = `${geometry.id}.babylonbinarymeshdata`; diff --git a/editor/src/project/save/scene.ts b/editor/src/project/save/scene.ts index 7cabc58cf..c2e96dd46 100644 --- a/editor/src/project/save/scene.ts +++ b/editor/src/project/save/scene.ts @@ -311,6 +311,8 @@ export async function saveScene(editor: Editor, projectPath: string, scenePath: ); await writeFile(splatPath, Buffer.from(data.splatsData)); + + delete data.shData; delete data.splatsData; await writeJSON(meshPath, data, { diff --git a/tools/src/loading/gaussian-splatting.ts b/tools/src/loading/gaussian-splatting.ts new file mode 100644 index 000000000..b0254f521 --- /dev/null +++ b/tools/src/loading/gaussian-splatting.ts @@ -0,0 +1,40 @@ +import { Scene } from "@babylonjs/core/scene"; +import { AssetContainer } from "@babylonjs/core/assetContainer"; +import { AddParser } from "@babylonjs/core/Loading/Plugins/babylonFileParser.function"; +import { GaussianSplattingMesh } from "@babylonjs/core/Meshes/GaussianSplatting/gaussianSplattingMesh"; + +import { loadFile } from "../tools/request"; + +let registered = false; + +export function registerGaussianSplattingParser() { + if (registered) { + return; + } + + registered = true; + + AddParser("GaussianSplattingMeshEditorPlugin", (parsedData: any, scene: Scene, container: AssetContainer, rootUrl: string) => { + parsedData.meshes?.forEach((mesh) => { + if (mesh.type !== "GaussianSplattingMesh" || !mesh.splatDataPath) { + return; + } + + const instantiatedMesh = scene.getMeshById(mesh.id) as GaussianSplattingMesh; + if (!instantiatedMesh) { + return; + } + + const splatDataUrl = rootUrl + mesh.splatDataPath; + scene.addPendingData(splatDataUrl); + + loadFile(splatDataUrl, "arraybuffer").then(async (data) => { + instantiatedMesh.updateData(data, undefined, { + flipY: mesh._flipY, + }); + + scene.removePendingData(splatDataUrl); + }); + }); + }); +} diff --git a/tools/src/loading/loader.ts b/tools/src/loading/loader.ts index 6befaa27b..10252da69 100644 --- a/tools/src/loading/loader.ts +++ b/tools/src/loading/loader.ts @@ -22,13 +22,14 @@ import { _preloadScriptsAssets } from "./script/preload"; import { registerAudioParser } from "./sound"; import { registerTextureParser } from "./texture"; import { registerShadowGeneratorParser } from "./shadows"; +import { registerSpriteManagerParser } from "./sprite-manager"; +import { registerGaussianSplattingParser } from "./gaussian-splatting"; import { registerMorphTargetManagerParser } from "./morph-target-manager"; +import { registerNodeParticleSystemSetParser } from "./node-particle-system-set"; 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"; /** * Defines the possible output type of a script. @@ -152,6 +153,8 @@ export async function loadScene(rootUrl: any, sceneFilename: string, scene: Scen registerNodeParticleSystemSetParser(); + registerGaussianSplattingParser(); + // Check configuration const configuration = sceneConfigurationMap.get(scene) ?? {}; sceneConfigurationMap.set(scene, configuration); diff --git a/tools/src/tools/light.ts b/tools/src/tools/light.ts index ea744392c..8e912f9c5 100644 --- a/tools/src/tools/light.ts +++ b/tools/src/tools/light.ts @@ -5,6 +5,7 @@ import { RenderTargetTexture } from "@babylonjs/core/Materials/Textures/renderTa import { SceneLoaderQualitySelector } from "../loading/loader"; import { getPowerOfTwoUntil } from "./scalar"; +import { isGaussianSplattingMesh } from "./guards"; declare module "@babylonjs/core/Lights/Shadows/shadowGenerator" { export interface IShadowGenerator { @@ -20,6 +21,10 @@ export function configureShadowMapRenderListPredicate(scene: Scene) { } shadowMap.renderListPredicate = (mesh) => { + if (isGaussianSplattingMesh(mesh)) { + return false; + } + const distance = Vector3.Distance(mesh.getAbsolutePosition(), light.getAbsolutePosition()); return distance <= light.range; }; From 44e88c625183fa86bd7d1ed2349cec42a370ee3c Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Fri, 24 Jul 2026 13:04:25 +0200 Subject: [PATCH 04/22] feat: add support of gaussian splatting in babylonjs-editor-cli --- cli/src/pack/scene.mts | 16 ++++++++++++++++ cli/src/tools/scene.mts | 4 ++++ tools/src/loading/gaussian-splatting.ts | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/cli/src/pack/scene.mts b/cli/src/pack/scene.mts index 347c80f5b..d446199c0 100644 --- a/cli/src/pack/scene.mts +++ b/cli/src/pack/scene.mts @@ -88,6 +88,22 @@ export async function createBabylonScene(options: ICreateBabylonSceneOptions) { const meshesResult = await Promise.all( options.directories.meshesFiles.map(async (file) => { const data = await fs.readJSON(join(options.sceneFile, "meshes", file)); + + if (data.type === "GaussianSplattingMesh") { + data.splatDataPath = join(options.sceneName, basename(data.splatDataPath)); + + const splatDataDestination = join(options.publicDir, data.splatDataPath); + await fs.copyFile(join(options.sceneFile, "splats", basename(data.splatDataPath)), splatDataDestination); + + options.exportedAssets.push(splatDataDestination); + + return { + mesh: data, + lodMeshes: [], + effectiveMaterials: [], + }; + } + const mesh = data.meshes[0]; if (mesh.metadata?.doNotSerialize) { diff --git a/cli/src/tools/scene.mts b/cli/src/tools/scene.mts index a61f7066e..cccdfe027 100644 --- a/cli/src/tools/scene.mts +++ b/cli/src/tools/scene.mts @@ -24,6 +24,7 @@ export async function ensureSceneDirectories(scenePath: string) { fs.ensureDir(join(scenePath, "sprite-maps")), fs.ensureDir(join(scenePath, "sprite-managers")), fs.ensureDir(join(scenePath, "nodeParticleSystemSets")), + fs.ensureDir(join(scenePath, "splats")), ]); } @@ -48,6 +49,7 @@ export async function readSceneDirectories(scenePath: string) { spriteManagerFiles, geometryFiles, nodeParticleSystemSetFiles, + gaussianSplattingFiles, ] = await Promise.all([ readdir(join(scenePath, "nodes")), readdir(join(scenePath, "meshes")), @@ -68,6 +70,7 @@ export async function readSceneDirectories(scenePath: string) { readdir(join(scenePath, "sprite-managers")), readdir(join(scenePath, "geometries")), readdir(join(scenePath, "nodeParticleSystemSets")), + readdir(join(scenePath, "splats")), ]); return { @@ -90,5 +93,6 @@ export async function readSceneDirectories(scenePath: string) { spriteManagerFiles, geometryFiles, nodeParticleSystemSetFiles, + gaussianSplattingFiles, }; } diff --git a/tools/src/loading/gaussian-splatting.ts b/tools/src/loading/gaussian-splatting.ts index b0254f521..c129d0f00 100644 --- a/tools/src/loading/gaussian-splatting.ts +++ b/tools/src/loading/gaussian-splatting.ts @@ -20,7 +20,7 @@ export function registerGaussianSplattingParser() { return; } - const instantiatedMesh = scene.getMeshById(mesh.id) as GaussianSplattingMesh; + const instantiatedMesh = container.meshes.find((m) => m.id === mesh.id) as GaussianSplattingMesh; if (!instantiatedMesh) { return; } From 5a8fc233be3f9e7b243b130cf4ba0dc5af180005 Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Tue, 28 Jul 2026 16:18:05 +0200 Subject: [PATCH 05/22] fix: re-apply old active render loops when stoping game in preview to fix custom frame requester behavior --- editor/src/editor/layout/inspector/mesh/mesh.tsx | 2 +- editor/src/project/save/scene.ts | 1 + editor/src/tools/mesh/gaussian-splatting.ts | 2 ++ editor/src/tools/scene/play/override.tsx | 9 +++++++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/editor/src/editor/layout/inspector/mesh/mesh.tsx b/editor/src/editor/layout/inspector/mesh/mesh.tsx index 0b884dd50..4d5a2b1a3 100644 --- a/editor/src/editor/layout/inspector/mesh/mesh.tsx +++ b/editor/src/editor/layout/inspector/mesh/mesh.tsx @@ -181,7 +181,7 @@ export class EditorMeshInspector extends Component - {isMesh(this.props.object) && ( + {isMesh(this.props.object) && !isGaussianSplattingMesh(this.props.object) && ( <> diff --git a/editor/src/project/save/scene.ts b/editor/src/project/save/scene.ts index c2e96dd46..e7e9b0253 100644 --- a/editor/src/project/save/scene.ts +++ b/editor/src/project/save/scene.ts @@ -305,6 +305,7 @@ export async function saveScene(editor: Editor, projectPath: string, scenePath: try { const data = mesh.serialize( { + metadata: mesh.metadata, splatDataPath: join(relativeScenePath, `splats/${mesh.id}.babylonbinarysplatdata`), }, "binary" diff --git a/editor/src/tools/mesh/gaussian-splatting.ts b/editor/src/tools/mesh/gaussian-splatting.ts index b40c2ed4d..a2f062218 100644 --- a/editor/src/tools/mesh/gaussian-splatting.ts +++ b/editor/src/tools/mesh/gaussian-splatting.ts @@ -7,6 +7,8 @@ export function configureGaussianSplattingMeshFromData(gaussianSplattingMesh: Ga gaussianSplattingMesh.id = data.id; gaussianSplattingMesh.uniqueId = data.uniqueId; + gaussianSplattingMesh.metadata = data.metadata ?? {}; + if (data.position) { gaussianSplattingMesh.position.copyFromFloats(data.position[0], data.position[1], data.position[2]); } diff --git a/editor/src/tools/scene/play/override.tsx b/editor/src/tools/scene/play/override.tsx index b1087bc20..849b8bcd0 100644 --- a/editor/src/tools/scene/play/override.tsx +++ b/editor/src/tools/scene/play/override.tsx @@ -29,6 +29,7 @@ const savedWebRequestMethods: Record = { }; const savedEngineMethods: Record = { + activeRenderLoops: [], createTexture: Engine.prototype.createTexture, createCubeTexture: Engine.prototype.createCubeTexture, createRawCubeTextureFromUrl: Engine.prototype.createRawCubeTextureFromUrl, @@ -107,9 +108,15 @@ export function restorePlayOverrides(editor: Editor) { WebRequest.prototype.open = savedWebRequestMethods.open; + editor.layout.preview.engine.stopRenderLoop(); + savedEngineMethods.activeRenderLoops.forEach((loop) => { + editor.layout.preview.engine.runRenderLoop(loop); + }); + Engine.prototype.createTexture = savedEngineMethods.createTexture; Engine.prototype.createCubeTexture = savedEngineMethods.createCubeTexture; Engine.prototype.createRawCubeTextureFromUrl = savedEngineMethods.createRawCubeTextureFromUrl; + SerializationHelper._TextureParser = savedTextureMethods.textureParser; Observable.prototype.add = savedObservableMethods.add; @@ -273,6 +280,8 @@ export function applyOverrides(editor: Editor) { }; // Engine + savedEngineMethods.activeRenderLoops = editor.layout.preview.engine.activeRenderLoops.slice(); + Engine.prototype.createRawCubeTextureFromUrl = (url: string, ...args: any[]) => { if (url && url.includes(publicScene)) { url = url.replace(publicScene, projectDir); From b344b94f98d61d6b02541394b2e399115e0df85a Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Tue, 28 Jul 2026 16:36:01 +0200 Subject: [PATCH 06/22] fix: keep parenting and isEnabled properties when saving Gaussian Splatting Meshes #841 --- editor/src/editor/layout/graph.tsx | 4 ++++ editor/src/project/save/scene.ts | 4 ++++ editor/src/tools/mesh/gaussian-splatting.ts | 3 +++ tools/src/loading/gaussian-splatting.ts | 3 +++ 4 files changed, 14 insertions(+) diff --git a/editor/src/editor/layout/graph.tsx b/editor/src/editor/layout/graph.tsx index 638223599..1cc544c0e 100644 --- a/editor/src/editor/layout/graph.tsx +++ b/editor/src/editor/layout/graph.tsx @@ -984,6 +984,10 @@ export class EditorGraph extends Component return null; } + if (this.state.playScene && node.reservedDataStore?.hidden) { + return null; + } + node.id ??= Tools.RandomId(); const info = { diff --git a/editor/src/project/save/scene.ts b/editor/src/project/save/scene.ts index e7e9b0253..2128b1625 100644 --- a/editor/src/project/save/scene.ts +++ b/editor/src/project/save/scene.ts @@ -306,11 +306,15 @@ export async function saveScene(editor: Editor, projectPath: string, scenePath: const data = mesh.serialize( { metadata: mesh.metadata, + isEnabled: mesh.isEnabled(false), splatDataPath: join(relativeScenePath, `splats/${mesh.id}.babylonbinarysplatdata`), }, "binary" ); + data.metadata ??= {}; + data.metadata.parentId = mesh.parent?.uniqueId; + await writeFile(splatPath, Buffer.from(data.splatsData)); delete data.shData; diff --git a/editor/src/tools/mesh/gaussian-splatting.ts b/editor/src/tools/mesh/gaussian-splatting.ts index a2f062218..02c5df432 100644 --- a/editor/src/tools/mesh/gaussian-splatting.ts +++ b/editor/src/tools/mesh/gaussian-splatting.ts @@ -8,6 +8,9 @@ export function configureGaussianSplattingMeshFromData(gaussianSplattingMesh: Ga gaussianSplattingMesh.uniqueId = data.uniqueId; gaussianSplattingMesh.metadata = data.metadata ?? {}; + gaussianSplattingMesh.metadata._waitingParentId = data.metadata?.parentId; + + gaussianSplattingMesh.setEnabled(data.isEnabled ?? true); if (data.position) { gaussianSplattingMesh.position.copyFromFloats(data.position[0], data.position[1], data.position[2]); diff --git a/tools/src/loading/gaussian-splatting.ts b/tools/src/loading/gaussian-splatting.ts index c129d0f00..a01dc700e 100644 --- a/tools/src/loading/gaussian-splatting.ts +++ b/tools/src/loading/gaussian-splatting.ts @@ -33,6 +33,9 @@ export function registerGaussianSplattingParser() { flipY: mesh._flipY, }); + instantiatedMesh.metadata = mesh.metadata ?? {}; + instantiatedMesh.setEnabled(mesh.isEnabled ?? true); + scene.removePendingData(splatDataUrl); }); }); From d291fcc937677d8c777412f7214e3bed74efc68d Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Wed, 29 Jul 2026 17:28:07 +0200 Subject: [PATCH 07/22] feat: adding support of Gaussian Splatting parts --- .../layout/assets-browser/items/mesh-item.tsx | 2 +- editor/src/editor/layout/graph.tsx | 30 ++++++-- .../src/editor/layout/inspector/mesh/mesh.tsx | 12 +-- editor/src/editor/layout/preview.tsx | 8 +- .../editor/layout/preview/import/import.ts | 41 +++++++---- editor/src/mcp/assets/assets.ts | 2 +- editor/src/mcp/lights/lights.ts | 4 +- editor/src/project/export/export.tsx | 73 +++++++++++++------ editor/src/project/load/plugins/meshes.ts | 25 +++++-- editor/src/project/load/scene.ts | 2 +- editor/src/project/save/scene.ts | 49 +++++++++++-- editor/src/tools/guards/nodes.ts | 18 +++++ editor/src/tools/mesh/augmentations.ts | 16 +++- editor/src/tools/mesh/gaussian-splatting.ts | 36 ++++++--- plugins/fab/src/import/mesh.ts | 2 +- tools/src/loading/gaussian-splatting.ts | 49 ++++++++++++- 16 files changed, 285 insertions(+), 84 deletions(-) diff --git a/editor/src/editor/layout/assets-browser/items/mesh-item.tsx b/editor/src/editor/layout/assets-browser/items/mesh-item.tsx index 9306b3081..2d6681e90 100644 --- a/editor/src/editor/layout/assets-browser/items/mesh-item.tsx +++ b/editor/src/editor/layout/assets-browser/items/mesh-item.tsx @@ -93,7 +93,7 @@ export class AssetBrowserMeshItem extends AssetsBrowserItem { this.props.onRefresh(); const scene = new Scene(this.props.editor.layout.preview.engine); - await loadImportedSceneFile(scene, file, this.props.editor.path); + await loadImportedSceneFile(scene, file, this.props.editor); const data = await SceneSerializer.SerializeAsync(scene); await writeJSON(`${file}.babylon`, data, "utf-8"); diff --git a/editor/src/editor/layout/graph.tsx b/editor/src/editor/layout/graph.tsx index 1cc544c0e..a2685d561 100644 --- a/editor/src/editor/layout/graph.tsx +++ b/editor/src/editor/layout/graph.tsx @@ -60,7 +60,7 @@ import { isCollisionInstancedMesh, isCollisionMesh, isEditorCamera, - isGaussianSplattingMesh, + isGaussianSplattingPartProxyMesh, isInstancedMesh, isLight, isMesh, @@ -91,6 +91,7 @@ import { applySoundAsset } from "./preview/import/sound"; import { EditorGraphLabel } from "./graph/label"; import { EditorGraphContextMenu } from "./graph/context-menu"; import { setNewParentForGraphSelectedNodes } from "./graph/move"; +import { addGaussianSplattingMeshPartProxyMesh } from "../../tools/mesh/gaussian-splatting"; export interface IEditorGraphProps { /** @@ -525,7 +526,7 @@ export class EditorGraph extends Component } const newNodes: (Node | IParticleSystem | Sprite)[] = []; - const nodesToCopy = this._objectsToCopy.map((n) => n.nodeData).filter((n) => !isGaussianSplattingMesh(n)); + const nodesToCopy = this._objectsToCopy.map((n) => n.nodeData); if (!nodesToCopy.length) { return; } @@ -559,10 +560,10 @@ export class EditorGraph extends Component const tempTransfromNode = new TransformNode("tempParent", this.props.editor.layout.preview.scene); try { - nodesToCopy.forEach((object) => { + nodesToCopy.forEach(async (object) => { let node: Node | IParticleSystem | Sprite | null = null; - if (isAbstractMesh(object) && !isNodeParticleSystemSetMesh(object) && !isGaussianSplattingMesh(object)) { + if (isAbstractMesh(object) && !isNodeParticleSystemSetMesh(object) && !isGaussianSplattingPartProxyMesh(object)) { const suffix = "(Instanced Mesh)"; const name = isInstancedMesh(object) ? object.name : `${object.name.replace(` ${suffix}`, "")} ${suffix}`; @@ -586,8 +587,21 @@ export class EditorGraph extends Component const name = `${object.name.replace(` ${suffix}`, "")} ${suffix}`; node = object.clone(name, parent, false); - } else if (isGaussianSplattingMesh(object)) { - // TODO + } else if (isGaussianSplattingPartProxyMesh(object) && object.baseGaussianSplattingMesh) { + const proxyMesh = addGaussianSplattingMeshPartProxyMesh(object.baseGaussianSplattingMesh, this.props.editor); + if (proxyMesh) { + node = proxyMesh; + + const suffix = "(Proxy Mesh)"; + const name = `${object.name.replace(` ${suffix}`, "")} ${suffix}`; + + proxyMesh.name = name; + proxyMesh.position.copyFrom(object.position); + proxyMesh.rotation.copyFrom(object.rotation); + proxyMesh.scaling.copyFrom(object.scaling); + proxyMesh.rotationQuaternion = object.rotationQuaternion?.clone() ?? null; + proxyMesh.parent = object.parent; + } } else if (isNode(object) || isSprite(object)) { node = cloneNode(this.props.editor, object); } @@ -608,7 +622,7 @@ export class EditorGraph extends Component } } - if (isAbstractMesh(node) && !isGaussianSplattingMesh(node)) { + if (isAbstractMesh(node) && !isGaussianSplattingPartProxyMesh(node)) { this.props.editor.layout.preview.scene.lights .map((light) => light.getShadowGenerator()) .forEach((generator) => generator?.getShadowMap()?.renderList?.push(node)); @@ -984,7 +998,7 @@ export class EditorGraph extends Component return null; } - if (this.state.playScene && node.reservedDataStore?.hidden) { + if (node.reservedDataStore?.hidden) { return null; } diff --git a/editor/src/editor/layout/inspector/mesh/mesh.tsx b/editor/src/editor/layout/inspector/mesh/mesh.tsx index 4d5a2b1a3..98875bac0 100644 --- a/editor/src/editor/layout/inspector/mesh/mesh.tsx +++ b/editor/src/editor/layout/inspector/mesh/mesh.tsx @@ -29,7 +29,7 @@ import { registerUndoRedo } from "../../../../tools/undoredo"; import { waitNextAnimationFrame } from "../../../../tools/tools"; import { onNodeModifiedObservable } from "../../../../tools/observables"; import { updateIblShadowsRenderPipeline } from "../../../../tools/light/ibl"; -import { isAbstractMesh, isGaussianSplattingMesh, isInstancedMesh, isMesh } from "../../../../tools/guards/nodes"; +import { isAbstractMesh, isGaussianSplattingPartProxyMesh, isInstancedMesh, isMesh } from "../../../../tools/guards/nodes"; import { updateAllLights, updateLightShadowMapRefreshRate, updatePointLightShadowMapRenderListPredicate } from "../../../../tools/light/shadows"; import { applyMaterialAssetToObject } from "../../preview/import/material"; @@ -162,11 +162,11 @@ export class EditorMeshInspector extends Component - {!isGaussianSplattingMesh(this.props.object) && } + {!isGaussianSplattingPartProxyMesh(this.props.object) && } )} - {this.props.object.getScene().lights.length > 0 && this.props.object.geometry && !isGaussianSplattingMesh(this.props.object) && ( + {this.props.object.getScene().lights.length > 0 && this.props.object.geometry && !isGaussianSplattingPartProxyMesh(this.props.object) && ( - {isMesh(this.props.object) && !isGaussianSplattingMesh(this.props.object) && ( + {isMesh(this.props.object) && !isGaussianSplattingPartProxyMesh(this.props.object) && ( <> @@ -447,7 +447,7 @@ export class EditorMeshInspector extends Component { lightsWithShadows.forEach((light) => { - if (enabled && !isGaussianSplattingMesh(this.props.object)) { + if (enabled && !isGaussianSplattingPartProxyMesh(this.props.object)) { light.getShadowGenerator()?.getShadowMap()?.renderList?.push(this.props.object); } else { const index = light.getShadowGenerator()?.getShadowMap()?.renderList?.indexOf(this.props.object); diff --git a/editor/src/editor/layout/preview.tsx b/editor/src/editor/layout/preview.tsx index c08a40a33..bb5f76840 100644 --- a/editor/src/editor/layout/preview.tsx +++ b/editor/src/editor/layout/preview.tsx @@ -37,6 +37,7 @@ import { ClusteredLightContainer, Tools, _GetAudioEngine, + GaussianSplattingCompoundMesh, } from "babylonjs"; import { SpinnerUIComponent } from "../../ui/spinner"; @@ -191,6 +192,11 @@ export class EditorPreview extends Component void) { const toolsUrl = process.env.EDITOR_TOOLS_URL ?? "https://editor.babylonjs.com"; const buffer = (await readFile(absolutePath)) as Buffer; @@ -66,7 +68,7 @@ export async function tryConvertSceneFile(absolutePath: string, progress?: (perc } } -export async function loadImportedSceneFile(scene: Scene, absolutePath: string, appPath: string | null) { +export async function loadImportedSceneFile(scene: Scene, absolutePath: string, editor: Editor) { if (!projectConfiguration.path) { return null; } @@ -81,7 +83,7 @@ export async function loadImportedSceneFile(scene: Scene, absolutePath: string, pluginOptions: { splat: { fflate, - spzLibraryUrl: join(appPath ?? "", nodeModules, "@adobe/spz/dist/spz.js"), + spzLibraryUrl: join(editor.path ?? "", nodeModules, "@adobe/spz/dist/spz.js"), gaussianSplattingMesh: new GaussianSplattingMesh(basename(absolutePath), null, scene, true), }, }, @@ -102,19 +104,30 @@ export async function loadImportedSceneFile(scene: Scene, absolutePath: string, // cleanImportedGltf(result); } - result.meshes.forEach((mesh) => { + for (const mesh of result.meshes) { configureImportedNodeIds(mesh); if (isGaussianSplattingMesh(mesh)) { - mesh.scaling.scaleInPlace(100); + const meshIndex = result.meshes.indexOf(mesh); + if (meshIndex !== -1) { + result.meshes.splice(meshIndex, 1); + } + + scene.removeMesh(mesh); - removeGaussianSplattingCameraMeshes(mesh); + const proxyMesh = addGaussianSplattingMeshPartProxyMesh(mesh, editor); + if (proxyMesh) { + proxyMesh.scaling.scaleInPlace(100); - switch (extname(absolutePath).toLowerCase()) { - case ".sog": - case ".spz": - mesh.rotation.x = Math.PI; - break; + switch (extname(absolutePath).toLowerCase()) { + case ".sog": + case ".spz": + proxyMesh.rotation.x = Math.PI; + break; + } + + configureImportedNodeIds(proxyMesh); + result.meshes.push(proxyMesh); } } else { mesh.receiveShadows = true; @@ -140,7 +153,7 @@ export async function loadImportedSceneFile(scene: Scene, absolutePath: string, target.name = `${mesh.name}_${target.name}`; } } - }); + } result.lights.forEach((light) => configureImportedNodeIds(light)); result.transformNodes.forEach((transformNode) => configureImportedNodeIds(transformNode)); @@ -153,7 +166,7 @@ export async function loadImportedSceneFile(scene: Scene, absolutePath: string, } result.meshes.forEach((mesh) => { - if (!isGaussianSplattingMesh(mesh)) { + if (!isGaussianSplattingPartProxyMesh(mesh) && !isGaussianSplattingMesh(mesh)) { shadowMap.renderList!.push(mesh); } }); diff --git a/editor/src/mcp/assets/assets.ts b/editor/src/mcp/assets/assets.ts index ef1a55151..dc8edbdf1 100644 --- a/editor/src/mcp/assets/assets.ts +++ b/editor/src/mcp/assets/assets.ts @@ -152,7 +152,7 @@ export async function getAssetPreview(_scene: Scene, data: any): Promise { export async function instantiateMeshAsset(scene: Scene, data: any, options: IMCPActionOptions): Promise { const absolutePath = resolveProjectPath(data.path); - const result = await loadImportedSceneFile(scene, absolutePath, options.editor.path); + const result = await loadImportedSceneFile(scene, absolutePath, options.editor); if (!result) { throw new Error(`Failed to load mesh asset: ${data.path}`); } diff --git a/editor/src/mcp/lights/lights.ts b/editor/src/mcp/lights/lights.ts index 9a2015aa1..0b7201471 100644 --- a/editor/src/mcp/lights/lights.ts +++ b/editor/src/mcp/lights/lights.ts @@ -1,6 +1,6 @@ import { Scene, Node, Light, ShadowGenerator, CascadedShadowGenerator, IShadowLight, DirectionalLight, SpotLight, PointLight } from "babylonjs"; -import { isGaussianSplattingMesh, isLight } from "../../tools/guards/nodes"; +import { isGaussianSplattingMesh, isGaussianSplattingPartProxyMesh, isLight } from "../../tools/guards/nodes"; import { addPointLight, addDirectionalLight, addSpotLight, addHemisphericLight } from "../../project/add/light"; @@ -133,7 +133,7 @@ export function setLightShadows(scene: Scene, data: any, options: IMCPActionOpti generator.setDarkness(data.darkness); } - generator.getShadowMap()?.renderList?.push(...scene.meshes.filter((m) => !isGaussianSplattingMesh(m))); + generator.getShadowMap()?.renderList?.push(...scene.meshes.filter((m) => !isGaussianSplattingPartProxyMesh(m) && !isGaussianSplattingMesh(m))); } options.editor.layout.inspector.setEditedObject(node); diff --git a/editor/src/project/export/export.tsx b/editor/src/project/export/export.tsx index f338ec393..23e917370 100644 --- a/editor/src/project/export/export.tsx +++ b/editor/src/project/export/export.tsx @@ -1,7 +1,7 @@ import { join, dirname, basename, extname } from "path/posix"; import { readJSON, readdir, remove, writeFile, writeJSON } from "fs-extra"; -import { RenderTargetTexture, SceneSerializer } from "babylonjs"; +import { RenderTargetTexture, SceneSerializer, GaussianSplattingMesh } from "babylonjs"; import { toast } from "sonner"; @@ -11,8 +11,8 @@ import { getCollisionMeshFor } from "../../tools/mesh/collision"; import { storeTexturesBaseSize } from "../../tools/material/texture"; import { extractNodeMaterialTextures } from "../../tools/material/extract"; import { createDirectoryIfNotExist, normalizedGlob } from "../../tools/fs"; -import { isCollisionMesh, isEditorCamera, isGaussianSplattingMesh, isMesh } from "../../tools/guards/nodes"; import { extractNodeParticleSystemSetTextures, extractParticleSystemTextures } from "../../tools/particles/extract"; +import { isCollisionMesh, isEditorCamera, isGaussianSplattingPartProxyMesh, isMesh } from "../../tools/guards/nodes"; import { taaPipelineCameraConfigurations } from "../../editor/rendering/taa"; import { vlsPostProcessCameraConfigurations } from "../../editor/rendering/vls"; @@ -113,7 +113,7 @@ async function _exportProject(editor: Editor, options: IExportProjectOptions): P storeTexturesBaseSize(scene); - scene.meshes.forEach((mesh) => (mesh.doNotSerialize = mesh.metadata?.doNotSerialize ?? false)); + scene.meshes.forEach((mesh) => (mesh.doNotSerialize = ((mesh.metadata?.doNotSerialize ?? false) || mesh.reservedDataStore?.hidden) ?? false)); 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)); @@ -221,26 +221,7 @@ async function _exportProject(editor: Editor, options: IExportProjectOptions): P } } - let geometry: any = null; - - if (isGaussianSplattingMesh(instantiatedMesh)) { - if (instantiatedMesh.splatsData) { - const splatPath = join(scenePath, sceneName, `${instantiatedMesh.id}.babylonbinarysplatdata`); - - try { - await writeFile(splatPath, Buffer.from(instantiatedMesh.splatsData)); - - mesh.splatDataPath = `${sceneName}/${instantiatedMesh.id}.babylonbinarysplatdata`; - delete mesh.splatsData; - - savedGeometries.push(`${instantiatedMesh.id}.babylonbinarysplatdata`); - } catch (e) { - editor.layout.console.error(`Export: Failed to write gaussian splatting data for mesh ${mesh.name}`); - } - } - } else { - geometry = data.geometries?.vertexData?.find((v) => v.id === mesh.geometryId); - } + const geometry = data.geometries?.vertexData?.find((v) => v.id === mesh.geometryId); if (geometry) { const geometryFileName = `${geometry.id}.babylonbinarymeshdata`; @@ -282,6 +263,52 @@ async function _exportProject(editor: Editor, options: IExportProjectOptions): P }) ); + // Add gaussian splatting meshes to the list + const computedGaussianSplattingMeshes: GaussianSplattingMesh[] = []; + + await Promise.all( + scene.meshes.map(async (mesh) => { + if (!isGaussianSplattingPartProxyMesh(mesh) || !mesh.baseGaussianSplattingMesh?.splatsData) { + return; + } + + if (computedGaussianSplattingMeshes.includes(mesh.baseGaussianSplattingMesh)) { + return; + } + + computedGaussianSplattingMeshes.push(mesh.baseGaussianSplattingMesh); + + const splatDataPath = join(scenePath, sceneName, `${mesh.baseGaussianSplattingMesh.id}.babylonbinarysplatdata`); + + try { + await writeFile(splatDataPath, Buffer.from(mesh.baseGaussianSplattingMesh.splatsData)); + + const gaussianSplatData = mesh.baseGaussianSplattingMesh.serialize( + { + proxies: [], + metadata: mesh.metadata, + isEnabled: mesh.isEnabled(false), + splatDataPath: `${sceneName}/${mesh.baseGaussianSplattingMesh.id}.babylonbinarysplatdata`, + }, + "binary" + ); + + const allMeshProxies = scene.meshes.filter((m) => isGaussianSplattingPartProxyMesh(m) && m.baseGaussianSplattingMesh === mesh.baseGaussianSplattingMesh); + allMeshProxies.forEach((proxy) => { + const proxyData = proxy.serialize(); + proxyData.parentId = proxy.parent?.id; + delete proxyData.compoundSplatMeshId; + gaussianSplatData.proxies.push(proxyData); + }); + + data.meshes?.push(gaussianSplatData); + savedGeometries.push(`${mesh.baseGaussianSplattingMesh.id}.babylonbinarysplatdata`); + } catch (e) { + editor.layout.console.error(`Export: Failed to write gaussian splatting data for mesh ${mesh.name}`); + } + }) + ); + // Configure lights data.shadowGenerators?.forEach((shadowGenerator) => { const instantiatedLight = scene.getLightById(shadowGenerator.lightId); diff --git a/editor/src/project/load/plugins/meshes.ts b/editor/src/project/load/plugins/meshes.ts index e9d5d342d..a41a3d95c 100644 --- a/editor/src/project/load/plugins/meshes.ts +++ b/editor/src/project/load/plugins/meshes.ts @@ -1,7 +1,7 @@ import { join } from "path/posix"; import { readFile, readJSON } from "fs-extra"; -import { Scene, Constants, Matrix, Mesh, SceneLoader, MultiMaterial, Geometry, GaussianSplattingMesh } from "babylonjs"; +import { Scene, Constants, Matrix, Mesh, SceneLoader, MultiMaterial, Geometry, GaussianSplattingMesh, GaussianSplattingPartProxyMesh } from "babylonjs"; import { ISceneLoaderPluginOptions } from "../scene"; @@ -10,11 +10,13 @@ import { isCollisionMesh, isMesh } from "../../../tools/guards/nodes"; import { isMultiMaterial, isNodeMaterial } from "../../../tools/guards/material"; import { parsePhysicsAggregate } from "../../../tools/physics/serialization/aggregate"; import { configureSimultaneousLightsForMaterial, normalizeNodeMaterialUniqueIds } from "../../../tools/material/material"; -import { configureGaussianSplattingMeshFromData, removeGaussianSplattingCameraMeshes } from "../../../tools/mesh/gaussian-splatting"; +import { addGaussianSplattingMeshPartProxyMesh, configureGaussianSplattingMeshFromData } from "../../../tools/mesh/gaussian-splatting"; import { CollisionMesh } from "../../../editor/nodes/collision"; -export async function loadMeshes(meshesFiles: string[], scene: Scene, options: ISceneLoaderPluginOptions) { +import { Editor } from "../../../editor/main"; + +export async function loadMeshes(editor: Editor, meshesFiles: string[], scene: Scene, options: ISceneLoaderPluginOptions) { const loadedMeshes = await Promise.all( meshesFiles.map(async (file) => { if (file.startsWith(".")) { @@ -32,11 +34,22 @@ export async function loadMeshes(meshesFiles: string[], scene: Scene, options: I initialData.splatsData = splatBuffer.buffer; const parsedMesh = GaussianSplattingMesh.Parse(initialData, scene); + parsedMesh.id = initialData.id; + parsedMesh.uniqueId = initialData.uniqueId; + + scene.removeMesh(parsedMesh); + + const proxyMeshes: GaussianSplattingPartProxyMesh[] = []; - configureGaussianSplattingMeshFromData(parsedMesh, initialData); - removeGaussianSplattingCameraMeshes(parsedMesh); + initialData.proxies?.forEach((proxy) => { + const proxyMesh = addGaussianSplattingMeshPartProxyMesh(parsedMesh, editor); + if (proxyMesh) { + configureGaussianSplattingMeshFromData(proxyMesh, proxy); + proxyMeshes.push(proxyMesh); + } + }); - return [parsedMesh]; + return proxyMeshes; } const filesToLoad = [join(options.relativeScenePath, "meshes", file), ...(initialData.lods?.map((file) => join(options.relativeScenePath, "lods", file)) ?? [])]; diff --git a/editor/src/project/load/scene.ts b/editor/src/project/load/scene.ts index f165bad50..0b4a6a8fb 100644 --- a/editor/src/project/load/scene.ts +++ b/editor/src/project/load/scene.ts @@ -287,7 +287,7 @@ export async function loadScene(editor: Editor, projectPath: string, scenePath: await loadTransformNodes(editor, nodesFiles, scene, pluginLoadOptions); await loadSkeletons(editor, skeletonFiles, scene, pluginLoadOptions); - await loadMeshes(meshesFiles, scene, pluginLoadOptions); + await loadMeshes(editor, meshesFiles, scene, pluginLoadOptions); await loadMorphTargetManagers(editor, morphTargetManagerFiles, scene, pluginLoadOptions); await loadLights(editor, lightsFiles, scene, pluginLoadOptions); await loadCameras(editor, cameraFiles, scene, pluginLoadOptions); diff --git a/editor/src/project/save/scene.ts b/editor/src/project/save/scene.ts index 2128b1625..085398985 100644 --- a/editor/src/project/save/scene.ts +++ b/editor/src/project/save/scene.ts @@ -3,7 +3,7 @@ import { pathExists, readJSON, remove, stat, writeFile, writeJSON } from "fs-ext import filenamify from "filenamify"; -import { RenderTargetTexture, SceneSerializer } from "babylonjs"; +import { RenderTargetTexture, SceneSerializer, GaussianSplattingMesh } from "babylonjs"; import { Editor } from "../../editor/main"; @@ -19,7 +19,16 @@ 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, isClusteredLightContainer, isCollisionMesh, isEditorCamera, isGaussianSplattingMesh, isMesh, isTransformNode } from "../../tools/guards/nodes"; +import { + isAnyTransformNode, + isClusteredLightContainer, + isCollisionMesh, + isEditorCamera, + isGaussianSplattingMesh, + isGaussianSplattingPartProxyMesh, + isMesh, + isTransformNode, +} from "../../tools/guards/nodes"; import { taaPipelineCameraConfigurations } from "../../editor/rendering/taa"; import { vlsPostProcessCameraConfigurations } from "../../editor/rendering/vls"; @@ -100,7 +109,16 @@ export async function saveScene(editor: Editor, projectPath: string, scenePath: // Write geometries and meshes await Promise.all( meshesToSave.map(async (mesh) => { - if ((!isMesh(mesh) && !isCollisionMesh(mesh)) || mesh._masterMesh || isFromSceneLink(mesh) || !isNodeVisibleInGraph(mesh) || isGaussianSplattingMesh(mesh)) { + if ( + (!isMesh(mesh) && !isCollisionMesh(mesh)) || + mesh._masterMesh || + isFromSceneLink(mesh) || + !isNodeVisibleInGraph(mesh) || + isGaussianSplattingPartProxyMesh(mesh) || + isGaussianSplattingMesh(mesh) || + mesh === editor.layout.preview.gaussianSplattingCompoundMesh || + mesh.reservedDataStore?.hidden + ) { return; } @@ -293,18 +311,27 @@ export async function saveScene(editor: Editor, projectPath: string, scenePath: ); // Write gaussian splatting meshes + const computedGaussianSplattingMeshes: GaussianSplattingMesh[] = []; + await Promise.all( scene.meshes.map(async (mesh) => { - if (!isGaussianSplattingMesh(mesh) || isFromSceneLink(mesh)) { + if (!isGaussianSplattingPartProxyMesh(mesh) || isFromSceneLink(mesh) || !mesh.baseGaussianSplattingMesh) { return; } + if (computedGaussianSplattingMeshes.includes(mesh.baseGaussianSplattingMesh)) { + return; + } + + computedGaussianSplattingMeshes.push(mesh.baseGaussianSplattingMesh); + const meshPath = join(scenePath, "meshes", `${mesh.id}.json`); const splatPath = join(scenePath, "splats", `${mesh.id}.babylonbinarysplatdata`); try { - const data = mesh.serialize( + const data = mesh.baseGaussianSplattingMesh.serialize( { + proxies: [], metadata: mesh.metadata, isEnabled: mesh.isEnabled(false), splatDataPath: join(relativeScenePath, `splats/${mesh.id}.babylonbinarysplatdata`), @@ -312,8 +339,16 @@ export async function saveScene(editor: Editor, projectPath: string, scenePath: "binary" ); - data.metadata ??= {}; - data.metadata.parentId = mesh.parent?.uniqueId; + const allMeshProxies = scene.meshes.filter((m) => isGaussianSplattingPartProxyMesh(m) && m.baseGaussianSplattingMesh === mesh.baseGaussianSplattingMesh); + allMeshProxies.forEach((proxy) => { + const proxyData = proxy.serialize(); + proxyData.metadata ??= {}; + proxyData.metadata.parentId = proxy.parent?.uniqueId; + + delete proxyData.compoundSplatMeshId; + + data.proxies.push(proxyData); + }); await writeFile(splatPath, Buffer.from(data.splatsData)); diff --git a/editor/src/tools/guards/nodes.ts b/editor/src/tools/guards/nodes.ts index 82e76a987..1aa054b9b 100644 --- a/editor/src/tools/guards/nodes.ts +++ b/editor/src/tools/guards/nodes.ts @@ -16,6 +16,7 @@ import { Skeleton, ClusteredLightContainer, GaussianSplattingMesh, + GaussianSplattingPartProxyMesh, } from "babylonjs"; import { EditorCamera } from "../../editor/nodes/camera"; @@ -38,6 +39,7 @@ export function isAbstractMesh(object: any): object is Mesh { case "NodeParticleSystemSetMesh": case "GaussianSplattingMesh": case "GaussianSplattingMeshBase": + case "GaussianSplattingPartProxyMesh": return true; } @@ -244,3 +246,19 @@ export function isNode(object: any): object is Node { export function isGaussianSplattingMesh(object: any): object is GaussianSplattingMesh { return object.getClassName?.() === "GaussianSplattingMesh"; } + +/** + * Returns wether or not the given object is a GaussianSplattingPartProxyMesh. + * @param object defines the reference to the object to test its class name. + */ +export function isGaussianSplattingPartProxyMesh(object: any): object is GaussianSplattingPartProxyMesh { + return object.getClassName?.() === "GaussianSplattingPartProxyMesh"; +} + +/** + * Returns wether or not the given object is a GaussianSplattingMesh or a GaussianSplattingPartProxyMesh. + * @param object defines the reference to the object to test its class name. + */ +export function isAnyGaussianSplattingMesh(object: any): object is GaussianSplattingMesh | GaussianSplattingPartProxyMesh { + return isGaussianSplattingMesh(object) || isGaussianSplattingPartProxyMesh(object); +} diff --git a/editor/src/tools/mesh/augmentations.ts b/editor/src/tools/mesh/augmentations.ts index 82b103448..e3d6d22fa 100644 --- a/editor/src/tools/mesh/augmentations.ts +++ b/editor/src/tools/mesh/augmentations.ts @@ -1,6 +1,6 @@ -import { PhysicsAggregate } from "babylonjs"; +import { GaussianSplattingMesh, PhysicsAggregate } from "babylonjs"; -export { AbstractMesh } from "babylonjs"; +import { GaussianSplattingPartProxyMesh as _GaussianSplattingPartProxyMesh } from "babylonjs/Meshes/GaussianSplatting/gaussianSplattingPartProxyMesh"; declare module "babylonjs" { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -8,4 +8,16 @@ declare module "babylonjs" { _waitingLod?: any; physicsAggregate?: PhysicsAggregate | null; } + + // eslint-disable-next-line @typescript-eslint/naming-convention + export interface GaussianSplattingPartProxyMesh extends _GaussianSplattingPartProxyMesh { + baseGaussianSplattingMesh?: GaussianSplattingMesh; + } +} + +declare module "babylonjs/Meshes/GaussianSplatting/gaussianSplattingPartProxyMesh" { + // eslint-disable-next-line @typescript-eslint/naming-convention + export interface GaussianSplattingPartProxyMesh { + baseGaussianSplattingMesh?: GaussianSplattingMesh; + } } diff --git a/editor/src/tools/mesh/gaussian-splatting.ts b/editor/src/tools/mesh/gaussian-splatting.ts index 02c5df432..5327c0aa2 100644 --- a/editor/src/tools/mesh/gaussian-splatting.ts +++ b/editor/src/tools/mesh/gaussian-splatting.ts @@ -1,8 +1,11 @@ -import { GaussianSplattingMesh } from "babylonjs"; +import { GaussianSplattingMesh, GetGaussianSplattingMaxPartCount, GaussianSplattingCompoundMesh, GaussianSplattingPartProxyMesh } from "babylonjs"; -import { isGaussianSplattingMesh } from "../guards/nodes"; +import { Editor } from "../../editor/main"; +import { configureImportedNodeIds } from "../../editor/layout/preview/import/import"; -export function configureGaussianSplattingMeshFromData(gaussianSplattingMesh: GaussianSplattingMesh, data: any) { +import { setNodeSerializable, setNodeVisibleInGraph } from "../node/metadata"; + +export function configureGaussianSplattingMeshFromData(gaussianSplattingMesh: GaussianSplattingPartProxyMesh, data: any) { gaussianSplattingMesh.name = data.name; gaussianSplattingMesh.id = data.id; gaussianSplattingMesh.uniqueId = data.uniqueId; @@ -26,10 +29,25 @@ export function configureGaussianSplattingMeshFromData(gaussianSplattingMesh: Ga } } -export function removeGaussianSplattingCameraMeshes(gaussianSplattingMesh: GaussianSplattingMesh) { - gaussianSplattingMesh.material?.getBindedMeshes().forEach((gaussianMesh) => { - if (!isGaussianSplattingMesh(gaussianMesh)) { - gaussianSplattingMesh.getScene().removeMesh(gaussianMesh); - } - }); +export function addGaussianSplattingMeshPartProxyMesh(mesh: GaussianSplattingMesh, editor: Editor) { + const scene = editor.layout.preview.scene; + const maxGaussianSplattingPartCount = GetGaussianSplattingMaxPartCount(scene.getEngine()); + + let gaussianSplattingCompoundMesh = editor.layout.preview.gaussianSplattingCompoundMesh; + if (!gaussianSplattingCompoundMesh) { + gaussianSplattingCompoundMesh = new GaussianSplattingCompoundMesh("GaussianSplattingCompoundMesh", undefined, scene, true); + configureImportedNodeIds(gaussianSplattingCompoundMesh); + + setNodeSerializable(gaussianSplattingCompoundMesh, false); + setNodeVisibleInGraph(gaussianSplattingCompoundMesh, false); + + editor.layout.preview.gaussianSplattingCompoundMesh = gaussianSplattingCompoundMesh; + } + + if (gaussianSplattingCompoundMesh.partCount < maxGaussianSplattingPartCount) { + const proxyMesh = gaussianSplattingCompoundMesh.addPart(mesh); + proxyMesh.baseGaussianSplattingMesh = mesh; + + return proxyMesh; + } } diff --git a/plugins/fab/src/import/mesh.ts b/plugins/fab/src/import/mesh.ts index 3aa2b03b6..b5a145000 100644 --- a/plugins/fab/src/import/mesh.ts +++ b/plugins/fab/src/import/mesh.ts @@ -16,7 +16,7 @@ export async function importMesh(editor: Editor, parameters: IImportMeshParamete const dest = join(parameters.finalAssetsFolder, basename(parameters.json.file)); const rootNodes: Node[] = []; - const result = await loadImportedSceneFile(editor.layout.preview.scene, dest, editor.path); + const result = await loadImportedSceneFile(editor.layout.preview.scene, dest, editor); result?.meshes.forEach((mesh) => { mesh.material = parameters.materialsMap.get(parameters.json.material_index) ?? mesh.material; diff --git a/tools/src/loading/gaussian-splatting.ts b/tools/src/loading/gaussian-splatting.ts index a01dc700e..3278df9e1 100644 --- a/tools/src/loading/gaussian-splatting.ts +++ b/tools/src/loading/gaussian-splatting.ts @@ -2,6 +2,7 @@ import { Scene } from "@babylonjs/core/scene"; import { AssetContainer } from "@babylonjs/core/assetContainer"; import { AddParser } from "@babylonjs/core/Loading/Plugins/babylonFileParser.function"; import { GaussianSplattingMesh } from "@babylonjs/core/Meshes/GaussianSplatting/gaussianSplattingMesh"; +import { GaussianSplattingCompoundMesh } from "@babylonjs/core/Meshes/GaussianSplatting/gaussianSplattingCompoundMesh"; import { loadFile } from "../tools/request"; @@ -14,6 +15,8 @@ export function registerGaussianSplattingParser() { registered = true; + let compountMesh: GaussianSplattingCompoundMesh | null = null; + AddParser("GaussianSplattingMeshEditorPlugin", (parsedData: any, scene: Scene, container: AssetContainer, rootUrl: string) => { parsedData.meshes?.forEach((mesh) => { if (mesh.type !== "GaussianSplattingMesh" || !mesh.splatDataPath) { @@ -33,8 +36,50 @@ export function registerGaussianSplattingParser() { flipY: mesh._flipY, }); - instantiatedMesh.metadata = mesh.metadata ?? {}; - instantiatedMesh.setEnabled(mesh.isEnabled ?? true); + scene.removeMesh(instantiatedMesh); + + mesh.proxies.forEach((proxy) => { + compountMesh ??= new GaussianSplattingCompoundMesh("GaussianSplattingCompoundMesh", undefined, scene); + + const proxyMesh = compountMesh.addPart(instantiatedMesh, false); + + proxyMesh.name = proxy.name; + proxyMesh.id = proxy.id; + proxyMesh.uniqueId = proxy.uniqueId; + + proxyMesh.metadata = proxy.metadata ?? {}; + proxyMesh.metadata._waitingParentId = proxy.metadata?.parentId; + + proxyMesh.setEnabled(proxy.isEnabled ?? true); + + if (proxy.position) { + proxyMesh.position.copyFromFloats(proxy.position[0], proxy.position[1], proxy.position[2]); + } + if (proxy.rotation) { + proxyMesh.rotation.copyFromFloats(proxy.rotation[0], proxy.rotation[1], proxy.rotation[2]); + } + if (proxy.rotationQuaternion) { + proxyMesh.rotationQuaternion?.copyFromFloats( + proxy.rotationQuaternion[0], + proxy.rotationQuaternion[1], + proxy.rotationQuaternion[2], + proxy.rotationQuaternion[3] + ); + } + if (proxy.scaling) { + proxyMesh.scaling.copyFromFloats(proxy.scaling[0], proxy.scaling[1], proxy.scaling[2]); + } + + const parent = container.getNodes().find((n) => n.id === proxy.parentId); + if (parent) { + proxyMesh.parent = parent; + } else { + proxyMesh._waitingParentId = proxy.parentId; + } + }); + + // instantiatedMesh.metadata = mesh.metadata ?? {}; + // instantiatedMesh.setEnabled(mesh.isEnabled ?? true); scene.removePendingData(splatDataUrl); }); From 07c350bd37e2a4d7cdd52d0cc41a48dcc61a9f8b Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Tue, 4 Aug 2026 19:40:12 +0200 Subject: [PATCH 08/22] fix: create gaussian splatting mesh receiver only when gaussian splatting mesh was dropped in preview --- editor/src/editor/layout/preview/import/import.ts | 13 +++++++++++-- editor/src/loader/assimpjs.ts | 6 +++--- editor/src/loader/maps.ts | 1 + editor/src/loader/material.ts | 1 + 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/editor/src/editor/layout/preview/import/import.ts b/editor/src/editor/layout/preview/import/import.ts index af1cb9aa9..66d9a1866 100644 --- a/editor/src/editor/layout/preview/import/import.ts +++ b/editor/src/editor/layout/preview/import/import.ts @@ -78,17 +78,26 @@ export async function loadImportedSceneFile(scene: Scene, absolutePath: string, try { const nodeModules = process.env.DEBUG ? "../node_modules" : "node_modules"; + let gaussianSplattingMesh: GaussianSplattingMesh | undefined = undefined; + + switch (extname(absolutePath).toLowerCase()) { + case ".sog": + case ".spz": + case ".splat": + gaussianSplattingMesh = new GaussianSplattingMesh(basename(absolutePath), null, scene, true); + break; + } + result = await ImportMeshAsync(basename(absolutePath), scene, { rootUrl: join(dirname(absolutePath), "/"), pluginOptions: { splat: { fflate, + gaussianSplattingMesh, spzLibraryUrl: join(editor.path ?? "", nodeModules, "@adobe/spz/dist/spz.js"), - gaussianSplattingMesh: new GaussianSplattingMesh(basename(absolutePath), null, scene, true), }, }, }); - // result = await SceneLoader.ImportMeshAsync("", join(dirname(absolutePath), "/"), basename(absolutePath), scene); } catch (e) { console.error(e); toast.error("Failed to load the scene file."); diff --git a/editor/src/loader/assimpjs.ts b/editor/src/loader/assimpjs.ts index 25593ef79..81ee29251 100644 --- a/editor/src/loader/assimpjs.ts +++ b/editor/src/loader/assimpjs.ts @@ -20,9 +20,9 @@ export class AssimpJSLoader implements ISceneLoaderPluginAsync { ".x": { isBinary: true, }, - // ".fbx": { - // isBinary: true, - // }, + ".fbx": { + isBinary: true, + }, ".3ds": { isBinary: true, }, diff --git a/editor/src/loader/maps.ts b/editor/src/loader/maps.ts index 1afb35d17..078193529 100644 --- a/editor/src/loader/maps.ts +++ b/editor/src/loader/maps.ts @@ -6,4 +6,5 @@ export const materialPropertyMap: Record = { "$raw.SpecularColor|file": "reflectivityTexture", "$raw.AmbientColor|file": "ambientTexture", "$raw.Bump|file": "bumpTexture", + "$raw.NormalMap|file": "bumpTexture", }; diff --git a/editor/src/loader/material.ts b/editor/src/loader/material.ts index e5db4e918..609945314 100644 --- a/editor/src/loader/material.ts +++ b/editor/src/loader/material.ts @@ -19,6 +19,7 @@ export function parseMaterial(runtime: AssimpJSRuntime, data: IAssimpJSMaterialD // Textures case "$raw.Bump|file": + case "$raw.NormalMap|file": case "$raw.DiffuseColor|file": case "$raw.AmbientColor|file": case "$raw.SpecularColor|file": From 26a9f98235270fd5b728a3dc27e3dbf6d3c84c9a Mon Sep 17 00:00:00 2001 From: Julien Moreau-Mathis Date: Thu, 6 Aug 2026 11:33:28 +0200 Subject: [PATCH 09/22] fix: remove and restore gaussian splatting proxy meshes #841 --- editor/src/editor/layout/graph/remove.ts | 42 +++++++++++++++++-- .../src/editor/layout/inspector/mesh/mesh.tsx | 4 +- editor/src/editor/layout/preview.tsx | 1 + 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/editor/src/editor/layout/graph/remove.ts b/editor/src/editor/layout/graph/remove.ts index e479becf3..5468e02cc 100644 --- a/editor/src/editor/layout/graph/remove.ts +++ b/editor/src/editor/layout/graph/remove.ts @@ -8,7 +8,19 @@ import { isClusteredLight } from "../../../tools/light/cluster"; import { isAnyParticleSystem } from "../../../tools/guards/particles"; import { isAdvancedDynamicTexture } from "../../../tools/guards/texture"; import { getLinkedAnimationGroupsFor } from "../../../tools/animation/group"; -import { isNode, isMesh, isAbstractMesh, isInstancedMesh, isCollisionInstancedMesh, isLight, isCamera, isAnyTransformNode, isSkeleton } from "../../../tools/guards/nodes"; +import { + isNode, + isMesh, + isAbstractMesh, + isInstancedMesh, + isCollisionInstancedMesh, + isLight, + isCamera, + isAnyTransformNode, + isSkeleton, + isGaussianSplattingPartProxyMesh, +} from "../../../tools/guards/nodes"; +import { addGaussianSplattingMeshPartProxyMesh, configureGaussianSplattingMeshFromData } from "../../../tools/mesh/gaussian-splatting"; import { Editor } from "../../main"; @@ -20,6 +32,8 @@ type _RemoveNodeData = { lights: Light[]; skeletons: Skeleton[]; particleSystems: IParticleSystem[]; + + metadata: any; }; /** @@ -44,6 +58,7 @@ export function removeNodes(editor: Editor) { .map((descendant) => { return { node: descendant, + metadata: descendant.metadata, parent: descendant.parent, skeletons: scene.skeletons.filter((skeleton) => isAbstractMesh(descendant) && descendant.skeleton === skeleton), particleSystems: scene.particleSystems.filter((ps) => ps.emitter === descendant), @@ -93,7 +108,10 @@ export function removeNodes(editor: Editor) { }, undo: () => { nodes.forEach((d) => { - restoreNodeData(editor, d, scene); + const newNode = restoreNodeData(editor, d, scene); + if (newNode) { + d.node = newNode; + } }); particleSystems.forEach((particleSystem) => { @@ -181,7 +199,19 @@ function restoreNodeData(editor: Editor, data: _RemoveNodeData, scene: Scene) { node.sourceMesh.addInstance(node); } - scene.addMesh(node); + if (isGaussianSplattingPartProxyMesh(node)) { + if (node.baseGaussianSplattingMesh) { + const newPart = addGaussianSplattingMeshPartProxyMesh(node.baseGaussianSplattingMesh, editor); + if (newPart) { + configureGaussianSplattingMeshFromData(newPart, node.serialize()); + newPart.parent = data.parent; + newPart.metadata = data.metadata; + return newPart; + } + } + } else { + scene.addMesh(node); + } data.lights.forEach((light) => { light.getShadowGenerator()?.getShadowMap()?.renderList?.push(node); @@ -213,7 +243,11 @@ function removeNodeData(editor: Editor, data: _RemoveNodeData, scene: Scene) { node.sourceMesh.removeInstance(node); } - scene.removeMesh(node); + if (isGaussianSplattingPartProxyMesh(node)) { + editor.layout.preview.gaussianSplattingCompoundMesh?.removePart(node.partIndex); + } else { + scene.removeMesh(node); + } data.lights.forEach((light) => { const renderList = light.getShadowGenerator()?.getShadowMap()?.renderList; diff --git a/editor/src/editor/layout/inspector/mesh/mesh.tsx b/editor/src/editor/layout/inspector/mesh/mesh.tsx index 98875bac0..7431048f0 100644 --- a/editor/src/editor/layout/inspector/mesh/mesh.tsx +++ b/editor/src/editor/layout/inspector/mesh/mesh.tsx @@ -124,13 +124,15 @@ export class EditorMeshInspector extends Component + onNodeModifiedObservable.notifyObservers(this.props.object)} /> - {this.props.object.geometry && ( + + {(this.props.object.geometry || isGaussianSplattingPartProxyMesh(this.props.object)) && ( <> Date: Thu, 6 Aug 2026 11:48:30 +0200 Subject: [PATCH 10/22] fix: compute parentId only when parent exist for gaussian splatting meshes in babylonjs-editor-tools #841 --- tools/src/loading/gaussian-splatting.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tools/src/loading/gaussian-splatting.ts b/tools/src/loading/gaussian-splatting.ts index 3278df9e1..2aa6a5f7b 100644 --- a/tools/src/loading/gaussian-splatting.ts +++ b/tools/src/loading/gaussian-splatting.ts @@ -70,17 +70,16 @@ export function registerGaussianSplattingParser() { proxyMesh.scaling.copyFromFloats(proxy.scaling[0], proxy.scaling[1], proxy.scaling[2]); } - const parent = container.getNodes().find((n) => n.id === proxy.parentId); - if (parent) { - proxyMesh.parent = parent; - } else { - proxyMesh._waitingParentId = proxy.parentId; + if (proxy.parentId) { + const parent = container.getNodes().find((n) => n.id === proxy.parentId); + if (parent) { + proxyMesh.parent = parent; + } else { + proxyMesh._waitingParentId = proxy.parentId; + } } }); - // instantiatedMesh.metadata = mesh.metadata ?? {}; - // instantiatedMesh.setEnabled(mesh.isEnabled ?? true); - scene.removePendingData(splatDataUrl); }); }); From 87b0641ccc3f2a8e63811c5f27bddc07dd33b81f Mon Sep 17 00:00:00 2001 From: Julien Moreau-Mathis Date: Thu, 6 Aug 2026 12:45:07 +0200 Subject: [PATCH 11/22] feat: add support of sh data for gaussian splatting meshes --- cli/src/pack/scene.mts | 16 ++++++++++++- editor/src/project/export/export.tsx | 22 +++++++++++++++++- editor/src/project/load/plugins/meshes.ts | 10 +++++++- editor/src/project/save/scene.ts | 12 ++++++++-- tools/src/index.ts | 1 + tools/src/loading/gaussian-splatting.ts | 28 +++++++++++++++++++---- tools/src/loading/loader.ts | 3 --- 7 files changed, 79 insertions(+), 13 deletions(-) diff --git a/cli/src/pack/scene.mts b/cli/src/pack/scene.mts index d446199c0..ae57d56c3 100644 --- a/cli/src/pack/scene.mts +++ b/cli/src/pack/scene.mts @@ -92,10 +92,24 @@ export async function createBabylonScene(options: ICreateBabylonSceneOptions) { if (data.type === "GaussianSplattingMesh") { data.splatDataPath = join(options.sceneName, basename(data.splatDataPath)); + data.shDataPaths?.forEach((shDataPath: string, index: number) => { + data.shDataPaths[index] = join(options.sceneName, basename(shDataPath)); + }); + const splatDataDestination = join(options.publicDir, data.splatDataPath); - await fs.copyFile(join(options.sceneFile, "splats", basename(data.splatDataPath)), splatDataDestination); + const shDataDestinations = data.shDataPaths?.map((shDataPath: string) => join(options.publicDir, shDataPath)); + + const promises = [fs.copyFile(join(options.sceneFile, "splats", basename(data.splatDataPath)), splatDataDestination)]; + shDataDestinations?.forEach((shDataDestination: string, index: number) => { + promises.push(fs.copyFile(join(options.sceneFile, "splats", basename(data.shDataPaths[index])), shDataDestination)); + }); + + await Promise.all(promises); options.exportedAssets.push(splatDataDestination); + shDataDestinations?.forEach((shDataDestination: string) => { + options.exportedAssets.push(shDataDestination); + }); return { mesh: data, diff --git a/editor/src/project/export/export.tsx b/editor/src/project/export/export.tsx index 23e917370..0b9e9aec6 100644 --- a/editor/src/project/export/export.tsx +++ b/editor/src/project/export/export.tsx @@ -279,9 +279,18 @@ async function _exportProject(editor: Editor, options: IExportProjectOptions): P computedGaussianSplattingMeshes.push(mesh.baseGaussianSplattingMesh); const splatDataPath = join(scenePath, sceneName, `${mesh.baseGaussianSplattingMesh.id}.babylonbinarysplatdata`); + const shPaths = mesh.baseGaussianSplattingMesh.shData?.map((_, index) => + join(scenePath, sceneName, `${mesh.baseGaussianSplattingMesh!.id}-sh${index}.babylonbinarysplatshdata`) + ); try { - await writeFile(splatDataPath, Buffer.from(mesh.baseGaussianSplattingMesh.splatsData)); + const promises = [writeFile(splatDataPath, Buffer.from(mesh.baseGaussianSplattingMesh.splatsData))]; + + mesh.baseGaussianSplattingMesh.shData?.forEach((shData, index) => { + promises.push(writeFile(shPaths![index], Buffer.from(shData))); + }); + + await Promise.all(promises); const gaussianSplatData = mesh.baseGaussianSplattingMesh.serialize( { @@ -289,10 +298,16 @@ async function _exportProject(editor: Editor, options: IExportProjectOptions): P metadata: mesh.metadata, isEnabled: mesh.isEnabled(false), splatDataPath: `${sceneName}/${mesh.baseGaussianSplattingMesh.id}.babylonbinarysplatdata`, + shDataPaths: mesh.baseGaussianSplattingMesh.shData?.map( + (_, index) => `${sceneName}/${mesh.baseGaussianSplattingMesh!.id}-sh${index}.babylonbinarysplatshdata` + ), }, "binary" ); + delete gaussianSplatData.shData; + delete gaussianSplatData.splatsData; + const allMeshProxies = scene.meshes.filter((m) => isGaussianSplattingPartProxyMesh(m) && m.baseGaussianSplattingMesh === mesh.baseGaussianSplattingMesh); allMeshProxies.forEach((proxy) => { const proxyData = proxy.serialize(); @@ -302,7 +317,12 @@ async function _exportProject(editor: Editor, options: IExportProjectOptions): P }); data.meshes?.push(gaussianSplatData); + savedGeometries.push(`${mesh.baseGaussianSplattingMesh.id}.babylonbinarysplatdata`); + + mesh.baseGaussianSplattingMesh.shData?.forEach((_, index) => { + savedGeometries.push(`${mesh.baseGaussianSplattingMesh!.id}-sh${index}.babylonbinarysplatshdata`); + }); } catch (e) { editor.layout.console.error(`Export: Failed to write gaussian splatting data for mesh ${mesh.name}`); } diff --git a/editor/src/project/load/plugins/meshes.ts b/editor/src/project/load/plugins/meshes.ts index a41a3d95c..e9152d3ce 100644 --- a/editor/src/project/load/plugins/meshes.ts +++ b/editor/src/project/load/plugins/meshes.ts @@ -30,8 +30,16 @@ export async function loadMeshes(editor: Editor, meshesFiles: string[], scene: S } if (initialData.type === "GaussianSplattingMesh") { - const splatBuffer = await readFile(join(options.projectPath, initialData.splatDataPath)); + const promises = [readFile(join(options.projectPath, initialData.splatDataPath))]; + + initialData.shDataPaths?.forEach((shDataPath) => { + promises.push(readFile(join(options.projectPath, shDataPath))); + }); + + const [splatBuffer, ...shData] = await Promise.all(promises); + initialData.splatsData = splatBuffer.buffer; + initialData.shData = shData?.map((buffer) => buffer.buffer); const parsedMesh = GaussianSplattingMesh.Parse(initialData, scene); parsedMesh.id = initialData.id; diff --git a/editor/src/project/save/scene.ts b/editor/src/project/save/scene.ts index 085398985..226cd3a15 100644 --- a/editor/src/project/save/scene.ts +++ b/editor/src/project/save/scene.ts @@ -327,6 +327,7 @@ export async function saveScene(editor: Editor, projectPath: string, scenePath: const meshPath = join(scenePath, "meshes", `${mesh.id}.json`); const splatPath = join(scenePath, "splats", `${mesh.id}.babylonbinarysplatdata`); + const shPaths = mesh.baseGaussianSplattingMesh.shData?.map((_, index) => join(scenePath, "splats", `${mesh.id}-sh${index}.babylonbinarysplatshdata`)) ?? []; try { const data = mesh.baseGaussianSplattingMesh.serialize( @@ -335,6 +336,7 @@ export async function saveScene(editor: Editor, projectPath: string, scenePath: metadata: mesh.metadata, isEnabled: mesh.isEnabled(false), splatDataPath: join(relativeScenePath, `splats/${mesh.id}.babylonbinarysplatdata`), + shDataPaths: mesh.baseGaussianSplattingMesh.shData?.map((_, index) => join(relativeScenePath, `splats/${mesh.id}-sh${index}.babylonbinarysplatshdata`)), }, "binary" ); @@ -350,7 +352,13 @@ export async function saveScene(editor: Editor, projectPath: string, scenePath: data.proxies.push(proxyData); }); - await writeFile(splatPath, Buffer.from(data.splatsData)); + const promises = [writeFile(splatPath, Buffer.from(data.splatsData))]; + + data.shData?.forEach((shData, index) => { + promises.push(writeFile(shPaths[index], Buffer.from(shData))); + }); + + await Promise.all(promises); delete data.shData; delete data.splatsData; @@ -361,7 +369,7 @@ export async function saveScene(editor: Editor, projectPath: string, scenePath: } catch (e) { editor.layout.console.error(`Failed to write gaussian splatting mesh ${mesh.name}`); } finally { - savedFiles.push(meshPath, splatPath); + savedFiles.push(meshPath, splatPath, ...shPaths); } dialog.step(progressStep); diff --git a/tools/src/index.ts b/tools/src/index.ts index 8bbf8f566..b132dfa51 100644 --- a/tools/src/index.ts +++ b/tools/src/index.ts @@ -1,5 +1,6 @@ export * from "./loading/loader"; export * from "./loading/material"; + export * from "./loading/container/container"; export * from "./loading/container/entries"; diff --git a/tools/src/loading/gaussian-splatting.ts b/tools/src/loading/gaussian-splatting.ts index 2aa6a5f7b..b0a5e9860 100644 --- a/tools/src/loading/gaussian-splatting.ts +++ b/tools/src/loading/gaussian-splatting.ts @@ -3,6 +3,7 @@ import { AssetContainer } from "@babylonjs/core/assetContainer"; import { AddParser } from "@babylonjs/core/Loading/Plugins/babylonFileParser.function"; import { GaussianSplattingMesh } from "@babylonjs/core/Meshes/GaussianSplatting/gaussianSplattingMesh"; import { GaussianSplattingCompoundMesh } from "@babylonjs/core/Meshes/GaussianSplatting/gaussianSplattingCompoundMesh"; +import { GetGaussianSplattingMaxPartCount } from "@babylonjs/core/Materials/GaussianSplatting/gaussianSplattingMaterial"; import { loadFile } from "../tools/request"; @@ -18,6 +19,8 @@ export function registerGaussianSplattingParser() { let compountMesh: GaussianSplattingCompoundMesh | null = null; AddParser("GaussianSplattingMeshEditorPlugin", (parsedData: any, scene: Scene, container: AssetContainer, rootUrl: string) => { + const maxGaussianSplattingPartCount = GetGaussianSplattingMaxPartCount(scene.getEngine()); + parsedData.meshes?.forEach((mesh) => { if (mesh.type !== "GaussianSplattingMesh" || !mesh.splatDataPath) { return; @@ -31,16 +34,28 @@ export function registerGaussianSplattingParser() { const splatDataUrl = rootUrl + mesh.splatDataPath; scene.addPendingData(splatDataUrl); - loadFile(splatDataUrl, "arraybuffer").then(async (data) => { - instantiatedMesh.updateData(data, undefined, { - flipY: mesh._flipY, - }); + const promises = [loadFile(splatDataUrl, "arraybuffer")]; - scene.removeMesh(instantiatedMesh); + mesh.shDataPaths?.forEach((shData) => { + promises.push(loadFile(rootUrl + shData, "arraybuffer")); + }); + + Promise.all(promises).then(([splatData, ...shDataArray]) => { + instantiatedMesh.updateData( + splatData, + shDataArray.map((shData) => new Uint8Array(shData)), + { + flipY: mesh._flipY, + } + ); mesh.proxies.forEach((proxy) => { compountMesh ??= new GaussianSplattingCompoundMesh("GaussianSplattingCompoundMesh", undefined, scene); + if (compountMesh.partCount >= maxGaussianSplattingPartCount) { + return; + } + const proxyMesh = compountMesh.addPart(instantiatedMesh, false); proxyMesh.name = proxy.name; @@ -80,8 +95,11 @@ export function registerGaussianSplattingParser() { } }); + scene.removeMesh(instantiatedMesh); scene.removePendingData(splatDataUrl); }); }); }); } + +registerGaussianSplattingParser(); diff --git a/tools/src/loading/loader.ts b/tools/src/loading/loader.ts index 10252da69..fd1b42073 100644 --- a/tools/src/loading/loader.ts +++ b/tools/src/loading/loader.ts @@ -23,7 +23,6 @@ import { registerAudioParser } from "./sound"; import { registerTextureParser } from "./texture"; import { registerShadowGeneratorParser } from "./shadows"; import { registerSpriteManagerParser } from "./sprite-manager"; -import { registerGaussianSplattingParser } from "./gaussian-splatting"; import { registerMorphTargetManagerParser } from "./morph-target-manager"; import { registerNodeParticleSystemSetParser } from "./node-particle-system-set"; @@ -153,8 +152,6 @@ export async function loadScene(rootUrl: any, sceneFilename: string, scene: Scen registerNodeParticleSystemSetParser(); - registerGaussianSplattingParser(); - // Check configuration const configuration = sceneConfigurationMap.get(scene) ?? {}; sceneConfigurationMap.set(scene, configuration); From 733d6a875cf12fe1a80082cf548001cd933e325c Mon Sep 17 00:00:00 2001 From: Julien Moreau-Mathis Date: Thu, 6 Aug 2026 13:37:06 +0200 Subject: [PATCH 12/22] fix: minor fixes for gaussian splatting support --- editor/src/editor/layout/preview/play.tsx | 4 +++ editor/src/project/export/scripts.ts | 4 +++ editor/src/project/load/plugins/meshes.ts | 2 +- tools/src/loading/gaussian-splatting.ts | 34 +++++++++++++++-------- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/editor/src/editor/layout/preview/play.tsx b/editor/src/editor/layout/preview/play.tsx index 141a68d23..7ac8efcd7 100644 --- a/editor/src/editor/layout/preview/play.tsx +++ b/editor/src/editor/layout/preview/play.tsx @@ -6,6 +6,7 @@ import { basename, dirname, join } from "path/posix"; import { Button } from "@blueprintjs/core"; import { Component, ReactNode } from "react"; +import { toast } from "sonner"; import { Grid } from "react-loader-spinner"; import { IoPlay, IoStop, IoRefresh } from "react-icons/io5"; @@ -374,6 +375,9 @@ export class EditorPreviewPlayComponent extends Component buffer.buffer); + initialData.shData = shData?.map((buffer) => new Uint8Array(buffer.buffer)); const parsedMesh = GaussianSplattingMesh.Parse(initialData, scene); parsedMesh.id = initialData.id; diff --git a/tools/src/loading/gaussian-splatting.ts b/tools/src/loading/gaussian-splatting.ts index b0a5e9860..ad6b3e0c8 100644 --- a/tools/src/loading/gaussian-splatting.ts +++ b/tools/src/loading/gaussian-splatting.ts @@ -31,23 +31,31 @@ export function registerGaussianSplattingParser() { return; } + instantiatedMesh.dispose(true); + const splatDataUrl = rootUrl + mesh.splatDataPath; + const shDataUrls = mesh.shDataPaths?.map((shData) => rootUrl + shData); + scene.addPendingData(splatDataUrl); + shDataUrls?.forEach((shDataUrl) => { + scene.addPendingData(shDataUrl); + }); const promises = [loadFile(splatDataUrl, "arraybuffer")]; - mesh.shDataPaths?.forEach((shData) => { - promises.push(loadFile(rootUrl + shData, "arraybuffer")); + shDataUrls?.forEach((shDataUrl) => { + promises.push(loadFile(shDataUrl, "arraybuffer")); }); Promise.all(promises).then(([splatData, ...shDataArray]) => { - instantiatedMesh.updateData( - splatData, - shDataArray.map((shData) => new Uint8Array(shData)), - { - flipY: mesh._flipY, - } - ); + mesh.splatsData = splatData; + mesh.shData = shDataArray?.map((buffer) => new Uint8Array(buffer)); + + const parsedMesh = GaussianSplattingMesh.Parse(mesh, scene); + parsedMesh.id = mesh.id; + parsedMesh.uniqueId = mesh.uniqueId; + + scene.removeMesh(parsedMesh); mesh.proxies.forEach((proxy) => { compountMesh ??= new GaussianSplattingCompoundMesh("GaussianSplattingCompoundMesh", undefined, scene); @@ -56,7 +64,7 @@ export function registerGaussianSplattingParser() { return; } - const proxyMesh = compountMesh.addPart(instantiatedMesh, false); + const proxyMesh = compountMesh.addPart(parsedMesh, false); proxyMesh.name = proxy.name; proxyMesh.id = proxy.id; @@ -70,9 +78,11 @@ export function registerGaussianSplattingParser() { if (proxy.position) { proxyMesh.position.copyFromFloats(proxy.position[0], proxy.position[1], proxy.position[2]); } + if (proxy.rotation) { proxyMesh.rotation.copyFromFloats(proxy.rotation[0], proxy.rotation[1], proxy.rotation[2]); } + if (proxy.rotationQuaternion) { proxyMesh.rotationQuaternion?.copyFromFloats( proxy.rotationQuaternion[0], @@ -95,8 +105,10 @@ export function registerGaussianSplattingParser() { } }); - scene.removeMesh(instantiatedMesh); scene.removePendingData(splatDataUrl); + shDataUrls?.forEach((shDataUrl) => { + scene.removePendingData(shDataUrl); + }); }); }); }); From a33d18bfc78463e1cc099c924d3dab60478cec67 Mon Sep 17 00:00:00 2001 From: Julien Moreau-Mathis Date: Thu, 6 Aug 2026 14:50:24 +0200 Subject: [PATCH 13/22] docs: starting documentation for Gaussian Splatting support --- .../assets/using-gaussian-splatting/page.tsx | 84 +++++++++++++++++++ .../using-gaussian-splatting/scripts.ts | 11 +++ .../using-sprite-manager/page.tsx | 0 .../using-sprite-manager/scripts.ts | 0 website/src/app/documentation/sidebar.tsx | 5 +- 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 website/src/app/documentation/assets/using-gaussian-splatting/page.tsx create mode 100644 website/src/app/documentation/assets/using-gaussian-splatting/scripts.ts rename website/src/app/documentation/{sprites => assets}/using-sprite-manager/page.tsx (100%) rename website/src/app/documentation/{sprites => assets}/using-sprite-manager/scripts.ts (100%) diff --git a/website/src/app/documentation/assets/using-gaussian-splatting/page.tsx b/website/src/app/documentation/assets/using-gaussian-splatting/page.tsx new file mode 100644 index 000000000..8341e7dd3 --- /dev/null +++ b/website/src/app/documentation/assets/using-gaussian-splatting/page.tsx @@ -0,0 +1,84 @@ +"use client"; + +import Link from "next/link"; + +import { Fade } from "react-awesome-reveal"; + +import { CodeBlock } from "../../code"; +import { loadSceneWithGaussianSplatting } from "./scripts"; + +export default function DocumentationRunningProjectPage() { + return ( +
+
+ + +
Using Sprite Manager
+
+
+ + +
+
Introduction
+ +
+ Gaussian Splatting is a volume-rendering method. It's useful for capturing real-life data. You can find more information about Gaussian Splatting + support in Babylon.js{" "} + + here + + . +
+ +
+ The Babylon.js Editor supports Gaussian Splatting assets. You can import them in your project and use them in your scene. The Editor will automatically + create a Gaussian Splatting instance. You can then manipulate them in the scene and change their properties in the Inspector. You can also add scripts + to them. +
+ +
+ Supported formats are: +
    +
  • + .splat: JavaScript typed-array serialized version of .PLY data +
  • +
  • + .spz:{" "} + + Niantic Labs + {" "} + SPZ format{" "} +
  • +
  • + .sog:{" "} + + Self-Organizing Gaussian + {" "} + format +
  • +
+
+ +
Importing Gaussian Splatting assets
+ +
Supporting Gaussian Splatting in your app
+ +
+ By default, Gaussian Splatting support is NOT included when you import the Babylon.js Editor tools. For tree-shaking purpose, you need to + explicitely import the Gaussian Splatting support in your app. You can do this by adding the following line in your code: +
+ + + +
+ And then you can load your scene(s) that contain Gaussian Splatting assets. The loader will automatically create the Gaussian Splatting instances in + your scene. Here is an example of the code to load a scene with Gaussian Splatting support: +
+ + +
+
+
+
+ ); +} diff --git a/website/src/app/documentation/assets/using-gaussian-splatting/scripts.ts b/website/src/app/documentation/assets/using-gaussian-splatting/scripts.ts new file mode 100644 index 000000000..e174d64b6 --- /dev/null +++ b/website/src/app/documentation/assets/using-gaussian-splatting/scripts.ts @@ -0,0 +1,11 @@ +export const loadSceneWithGaussianSplatting = ` +import { loadScene } from "babylonjs-editor-tools"; + +import "babylonjs-editor-tools/loading/gaussian-splatting"; + +// ... + +await loadScene("/scene/", "my-scene.babylon", scene, scriptsMap, { + quality: "high", +}); +`; diff --git a/website/src/app/documentation/sprites/using-sprite-manager/page.tsx b/website/src/app/documentation/assets/using-sprite-manager/page.tsx similarity index 100% rename from website/src/app/documentation/sprites/using-sprite-manager/page.tsx rename to website/src/app/documentation/assets/using-sprite-manager/page.tsx diff --git a/website/src/app/documentation/sprites/using-sprite-manager/scripts.ts b/website/src/app/documentation/assets/using-sprite-manager/scripts.ts similarity index 100% rename from website/src/app/documentation/sprites/using-sprite-manager/scripts.ts rename to website/src/app/documentation/assets/using-sprite-manager/scripts.ts diff --git a/website/src/app/documentation/sidebar.tsx b/website/src/app/documentation/sidebar.tsx index 58fc8364b..dcae6e236 100644 --- a/website/src/app/documentation/sidebar.tsx +++ b/website/src/app/documentation/sidebar.tsx @@ -43,9 +43,10 @@ export function DocumentationSidebar() { -
Sprites
+
Assets
- + +
Deploying
From 1beaa66e31e6bbccd996de0c2a6d4bf928794038 Mon Sep 17 00:00:00 2001 From: Julien Moreau-Mathis Date: Thu, 6 Aug 2026 15:33:26 +0200 Subject: [PATCH 14/22] chore: bump electron to v42.8.0 --- editor/package.json | 4 ++-- package.json | 2 +- templates/electron/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/editor/package.json b/editor/package.json index 20c1bae6c..473cbf05e 100644 --- a/editor/package.json +++ b/editor/package.json @@ -31,7 +31,7 @@ "@types/react-dom": "18.2.18", "@vitest/coverage-v8": "4.0.17", "concurrently": "9.2.0", - "electron": "39.8.5", + "electron": "42.8.0", "electron-builder": "26.0.12", "electron-reloader": "1.2.3", "postcss-import": "16.1.0", @@ -113,7 +113,7 @@ "md5": "2.3.0", "motion": "12.23.24", "next-themes": "^0.3.0", - "node-pty": "1.2.0-beta.12", + "node-pty": "1.2.0-beta.15", "node-stream-zip": "1.15.0", "pngjs": "7.0.0", "react": "18.2.0", diff --git a/package.json b/package.json index a9ef64565..430135f84 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@babylonjs/gui": "9.12.1", "@babylonjs/materials": "9.12.1", "braces": "3.0.3", - "node-abi": "4.14.0", + "node-abi": "^3.3.0", "wrap-ansi": "7.0.0", "babylonjs": "9.12.1", "babylonjs-addons": "9.12.1", diff --git a/templates/electron/package.json b/templates/electron/package.json index efe4bc1db..828f9c6e1 100644 --- a/templates/electron/package.json +++ b/templates/electron/package.json @@ -25,7 +25,7 @@ "@types/node": "^22", "babylonjs-editor-cli": "latest", "concurrently": "9.2.0", - "electron": "39.8.5", + "electron": "42.8.0", "electron-builder": "26.0.12", "tailwindcss": "4.1.18", "tsc-watch": "7.2.0", From 74ceb7061896321de4507b6933cdd52933060ba1 Mon Sep 17 00:00:00 2001 From: Julien Moreau-Mathis Date: Thu, 6 Aug 2026 15:55:38 +0200 Subject: [PATCH 15/22] fix: add .ply to gaussian splatting supported formats --- editor/src/editor/layout/preview/import/import.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/editor/src/editor/layout/preview/import/import.ts b/editor/src/editor/layout/preview/import/import.ts index 66d9a1866..399de0642 100644 --- a/editor/src/editor/layout/preview/import/import.ts +++ b/editor/src/editor/layout/preview/import/import.ts @@ -81,6 +81,7 @@ export async function loadImportedSceneFile(scene: Scene, absolutePath: string, let gaussianSplattingMesh: GaussianSplattingMesh | undefined = undefined; switch (extname(absolutePath).toLowerCase()) { + case ".ply": case ".sog": case ".spz": case ".splat": From ce33fb1f4a114bbdb855f709af815c4dd8c88dc4 Mon Sep 17 00:00:00 2001 From: Julien Moreau-Mathis Date: Thu, 6 Aug 2026 16:57:43 +0200 Subject: [PATCH 16/22] fix: set navmesh editor a vertical tool --- editor/src/editor/layout.tsx | 7 ++- editor/src/editor/layout/assets-browser.tsx | 5 +- .../assets-browser/items/navmesh-item.tsx | 38 +++++++++++- editor/src/editor/layout/navmesh/editor.tsx | 61 ++++++++++++++----- .../src/editor/layout/navmesh/inspector.tsx | 4 +- editor/src/editor/layout/navmesh/meshes.tsx | 5 +- .../src/editor/layout/navmesh/obstacles.tsx | 4 +- editor/src/editor/layout/navmesh/preview.tsx | 7 ++- 8 files changed, 101 insertions(+), 30 deletions(-) diff --git a/editor/src/editor/layout.tsx b/editor/src/editor/layout.tsx index e5e736d0a..3ff34e716 100644 --- a/editor/src/editor/layout.tsx +++ b/editor/src/editor/layout.tsx @@ -1,7 +1,7 @@ import { platform } from "os"; import { Component, ReactNode } from "react"; -import { Actions, IJsonModel, Layout, Model, TabNode, TabSetNode } from "flexlayout-react"; +import { Actions, ICloseType, IJsonModel, Layout, Model, TabNode, TabSetNode } from "flexlayout-react"; import { Observable, Tools } from "babylonjs"; import { ipcRenderer } from "electron"; @@ -204,7 +204,7 @@ export class EditorLayout extends Component { * @param component defines the reference to the React component to draw in. * @param options defines the options of the tab such as the title etc. */ - public addLayoutTab(component: ReactNode, options: IEditorLayoutTabOptions): void { + public addLayoutTab(component: ReactNode, options: IEditorLayoutTabOptions): string { options.id ??= Tools.RandomId(); const activeTabId = this._layoutRef?.props.model.getActiveTabset()?.getSelectedNode()?.getId(); @@ -238,11 +238,14 @@ export class EditorLayout extends Component { type: "tab", component: options.id, enableClose: options.enableClose, + closeType: ICloseType.Visible, }); if (activeTabId && !options.setAsActiveTab) { this._layoutRef?.props.model.doAction(Actions.selectTab(activeTabId)); } + + return options.id; } /** diff --git a/editor/src/editor/layout/assets-browser.tsx b/editor/src/editor/layout/assets-browser.tsx index 8944561cc..a482e4be2 100644 --- a/editor/src/editor/layout/assets-browser.tsx +++ b/editor/src/editor/layout/assets-browser.tsx @@ -841,13 +841,14 @@ export class EditorAssetsBrowser extends Component this._handleAddNodeParticleSystem()}>Node Particle System + + this._handleAddNavmesh()}>Navmesh + {this.props.editor.state.enableExperimentalFeatures && ( <> this._handleAddCinematic()}>Cinematic - this._handleAddNavmesh()}>Navmesh - this._handleAddRagdoll()}>Ragdoll )} diff --git a/editor/src/editor/layout/assets-browser/items/navmesh-item.tsx b/editor/src/editor/layout/assets-browser/items/navmesh-item.tsx index 78d840835..428ef7a61 100644 --- a/editor/src/editor/layout/assets-browser/items/navmesh-item.tsx +++ b/editor/src/editor/layout/assets-browser/items/navmesh-item.tsx @@ -8,6 +8,13 @@ import { NavMeshEditor } from "../../navmesh/editor"; import { AssetsBrowserItem } from "./item"; +interface _IEditedNavMeshConfiguration { + tabId: string; + absolutePath: string; +} + +const editedNavMeshConfigurations: _IEditedNavMeshConfiguration[] = []; + export class AssetBrowserNavmeshItem extends AssetsBrowserItem { /** * @override @@ -24,11 +31,36 @@ export class AssetBrowserNavmeshItem extends AssetsBrowserItem { * @override */ protected async onDoubleClick(): Promise { + const existingConfiguration = editedNavMeshConfigurations.find((c) => c.absolutePath === this.props.absolutePath); + if (existingConfiguration) { + this.props.editor.layout.selectTab(existingConfiguration.tabId); + return; + } + const data = await readJSON(join(this.props.absolutePath, "config.json")); - this.props.editor.layout.addLayoutTab(, { - setAsActiveTab: true, - title: "NavMesh Editor", + const tabId = this.props.editor.layout.addLayoutTab( + { + const index = editedNavMeshConfigurations.findIndex((c) => c.tabId === tabId); + if (index !== -1) { + editedNavMeshConfigurations.splice(index, 1); + } + }} + />, + { + setAsActiveTab: true, + title: "NavMesh Editor", + neighborId: "inspector", + } + ); + + editedNavMeshConfigurations.push({ + tabId, + absolutePath: this.props.absolutePath, }); } } diff --git a/editor/src/editor/layout/navmesh/editor.tsx b/editor/src/editor/layout/navmesh/editor.tsx index 1add38f7a..83968174e 100644 --- a/editor/src/editor/layout/navmesh/editor.tsx +++ b/editor/src/editor/layout/navmesh/editor.tsx @@ -19,7 +19,7 @@ import { Editor } from "../../main"; import { INavMeshConfiguration } from "./types"; import { NavMeshEditorToolbar } from "./toolbar"; -import { NavMeshEditorPreview } from "./preview"; +// import { NavMeshEditorPreview } from "./preview"; import { NavMeshEditorMeshesList } from "./meshes"; import { NavMeshEditorObstacles } from "./obstacles"; import { NavMeshEditorInspector } from "./inspector"; @@ -30,6 +30,8 @@ export interface INavmeshEditorProps { configuration: INavMeshConfiguration; absolutePath: string; + + onClose: () => void; } export interface INavmeshEditorState { @@ -38,7 +40,7 @@ export interface INavmeshEditorState { export class NavMeshEditor extends Component { public configuration: INavMeshConfiguration; - public plugin: RecastNavigationJSPluginV2; + public plugin: RecastNavigationJSPluginV2 | null = null; public result: CreateNavMeshResult | null = null; @@ -64,13 +66,21 @@ export class NavMeshEditor extends Component -
- +
Navmesh Editor
+
+ + {/*
+ + + + + +
*/}
); } @@ -78,11 +88,23 @@ export class NavMeshEditor extends Component { await this._createPlugin(); + // Clean data + this.props.configuration.staticMeshes = this.props.configuration.staticMeshes.filter((mesh) => { + const sceneMesh = this.props.editor.layout.preview.scene.getMeshById(mesh.id); + return sceneMesh !== null; + }); + + this.props.configuration.obstacleMeshes = this.props.configuration.obstacleMeshes.filter((mesh) => { + const sceneMesh = this.props.editor.layout.preview.scene.getMeshById(mesh.id); + return sceneMesh !== null; + }); + + // Build navmesh from data if exists const navMeshBinPath = join(this.props.absolutePath, "navmesh.bin"); const tilecacheBinPath = join(this.props.absolutePath, "tilecache.bin"); if ((await pathExists(navMeshBinPath)) && (await pathExists(tilecacheBinPath))) { - this.plugin.buildFromNavmeshData(await readFile(navMeshBinPath)); - this.plugin.buildFromTileCacheData(await readFile(tilecacheBinPath)); + this.plugin!.buildFromNavmeshData(await readFile(navMeshBinPath)); + this.plugin!.buildFromTileCacheData(await readFile(tilecacheBinPath)); this._createDebugNavMesh(); this.createDebugObstacles(); } @@ -96,6 +118,8 @@ export class NavMeshEditor extends Component {} @@ -110,7 +134,7 @@ export class NavMeshEditor extends Component { try { - await Promise?.all([ + const promises = [ writeJSON(join(this.props.absolutePath, "config.json"), this.configuration, { spaces: "\t", encoding: "utf-8", }), - writeFile(join(this.props.absolutePath, "navmesh.bin"), Buffer.from(this.plugin.getNavmeshData())), - writeFile(join(this.props.absolutePath, "tilecache.bin"), Buffer.from(this.plugin.getTileCacheData())), - ]); + ]; + + if (this.plugin) { + promises.push( + writeFile(join(this.props.absolutePath, "navmesh.bin"), Buffer.from(this.plugin.getNavmeshData())), + writeFile(join(this.props.absolutePath, "tilecache.bin"), Buffer.from(this.plugin.getTileCacheData())) + ); + } + + await Promise.all(promises); toast.success("NavMesh saved successfully."); } catch (e) { @@ -237,6 +268,7 @@ export class NavMeshEditor extends Component -
Inspector
+
+ {/*
Inspector
*/} diff --git a/editor/src/editor/layout/navmesh/meshes.tsx b/editor/src/editor/layout/navmesh/meshes.tsx index 0305b894e..2cd2295b8 100644 --- a/editor/src/editor/layout/navmesh/meshes.tsx +++ b/editor/src/editor/layout/navmesh/meshes.tsx @@ -19,10 +19,9 @@ export function NavMeshEditorMeshesList(props: INavMeshEditorMeshesListProps) { const [search, setSearch] = useState(""); return ( -
+
-
Static meshes
- +
Static meshes
diff --git a/editor/src/editor/layout/navmesh/obstacles.tsx b/editor/src/editor/layout/navmesh/obstacles.tsx index 1bf698763..214880dcd 100644 --- a/editor/src/editor/layout/navmesh/obstacles.tsx +++ b/editor/src/editor/layout/navmesh/obstacles.tsx @@ -17,9 +17,9 @@ export function NavMeshEditorObstacles(props: INavMeshEditorObstaclesProps) { const [search, setSearch] = useState(""); return ( -
+
-
Obstacle meshes
+
Obstacle meshes
diff --git a/editor/src/editor/layout/navmesh/preview.tsx b/editor/src/editor/layout/navmesh/preview.tsx index 8edd5f9f7..191200470 100644 --- a/editor/src/editor/layout/navmesh/preview.tsx +++ b/editor/src/editor/layout/navmesh/preview.tsx @@ -5,7 +5,7 @@ import { Engine, Scene, Mesh, StandardMaterial, Color3, Vector3, ArcRotateCamera export interface INavMeshEditorPreviewProps { mesh: Mesh | null; - plugin: RecastNavigationJSPluginV2; + plugin: RecastNavigationJSPluginV2 | null; } export function NavMeshEditorPreview(props: INavMeshEditorPreviewProps) { @@ -64,7 +64,10 @@ export function NavMeshEditorPreview(props: INavMeshEditorPreviewProps) { useEffect(() => { if (scene && props.mesh) { - const mesh = props.plugin.createDebugNavMesh(scene); + const mesh = props.plugin?.createDebugNavMesh(scene); + if (!mesh) { + return; + } const debugMaterial = new StandardMaterial("navmesh-debug-material", scene); debugMaterial.emissiveColor = Color3.Magenta(); From 83f235fac04f10e011fe11aaaef8ded136fe179d Mon Sep 17 00:00:00 2001 From: Julien Moreau-Mathis Date: Fri, 7 Aug 2026 11:32:27 +0200 Subject: [PATCH 17/22] fix: fixed preview of gaussian splatting meshes in assets browser --- .../assets-browser/viewers/model-viewer.tsx | 2 ++ .../editor/layout/preview/import/import.ts | 11 ++++---- editor/src/tools/assets/loader.ts | 17 ++++++++++++ editor/src/tools/workers/thumbnail/mesh.ts | 26 +++++++++++-------- 4 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 editor/src/tools/assets/loader.ts diff --git a/editor/src/editor/layout/assets-browser/viewers/model-viewer.tsx b/editor/src/editor/layout/assets-browser/viewers/model-viewer.tsx index 6a41adad3..17a5edaf4 100644 --- a/editor/src/editor/layout/assets-browser/viewers/model-viewer.tsx +++ b/editor/src/editor/layout/assets-browser/viewers/model-viewer.tsx @@ -9,6 +9,7 @@ import { showAlert } from "../../../../ui/dialog"; import { Progress } from "../../../../ui/shadcn/ui/progress"; import { isMesh } from "../../../../tools/guards/nodes"; +import { getLoaderPluginOptions } from "../../../../tools/assets/loader"; import { projectConfiguration } from "../../../../project/configuration"; @@ -73,6 +74,7 @@ function AssetBrowserModelViewer(props: IAssetBrowserModelViewerProps) { await AppendSceneAsync(source, scene, { rootUrl, onProgress: (ev) => setProgress((ev.loaded / ev.total) * 100), + pluginOptions: getLoaderPluginOptions(props.editor.path ?? ""), }); if (props.options?.overrideMaterialAbsolutePath) { diff --git a/editor/src/editor/layout/preview/import/import.ts b/editor/src/editor/layout/preview/import/import.ts index 399de0642..c88e97d9d 100644 --- a/editor/src/editor/layout/preview/import/import.ts +++ b/editor/src/editor/layout/preview/import/import.ts @@ -23,13 +23,12 @@ import { GaussianSplattingMesh, } from "babylonjs"; -import * as fflate from "fflate"; - import { UniqueNumber } from "../../../../tools/tools"; import { isSprite } from "../../../../tools/guards/sprites"; import { isTexture } from "../../../../tools/guards/texture"; import { executeSimpleWorker } from "../../../../tools/worker"; import { isMultiMaterial } from "../../../../tools/guards/material"; +import { getLoaderPluginOptions } from "../../../../tools/assets/loader"; import { configureSimultaneousLightsForMaterial } from "../../../../tools/material/material"; import { onNodesAddedObservable, onTextureAddedObservable } from "../../../../tools/observables"; import { addGaussianSplattingMeshPartProxyMesh } from "../../../../tools/mesh/gaussian-splatting"; @@ -76,8 +75,6 @@ export async function loadImportedSceneFile(scene: Scene, absolutePath: string, let result: ISceneLoaderAsyncResult; try { - const nodeModules = process.env.DEBUG ? "../node_modules" : "node_modules"; - let gaussianSplattingMesh: GaussianSplattingMesh | undefined = undefined; switch (extname(absolutePath).toLowerCase()) { @@ -89,13 +86,15 @@ export async function loadImportedSceneFile(scene: Scene, absolutePath: string, break; } + const pluginOptions = getLoaderPluginOptions(editor.path ?? ""); + result = await ImportMeshAsync(basename(absolutePath), scene, { rootUrl: join(dirname(absolutePath), "/"), pluginOptions: { + ...pluginOptions, splat: { - fflate, gaussianSplattingMesh, - spzLibraryUrl: join(editor.path ?? "", nodeModules, "@adobe/spz/dist/spz.js"), + ...pluginOptions.splat, }, }, }); diff --git a/editor/src/tools/assets/loader.ts b/editor/src/tools/assets/loader.ts new file mode 100644 index 000000000..65c404286 --- /dev/null +++ b/editor/src/tools/assets/loader.ts @@ -0,0 +1,17 @@ +import { join } from "path/posix"; + +import * as fflate from "fflate"; + +import { SPLATLoadingOptions } from "babylonjs-loaders"; + +export function getLoaderPluginOptions(appPath: string) { + const nodeModules = process.env.DEBUG ? "../node_modules" : "node_modules"; + + return { + splat: { + fflate, + keepInRam: true, + spzLibraryUrl: join(appPath ?? "", nodeModules, "@adobe/spz/dist/spz.js"), + } satisfies Partial, + }; +} diff --git a/editor/src/tools/workers/thumbnail/mesh.ts b/editor/src/tools/workers/thumbnail/mesh.ts index 6c4c39a6c..7c740e2ce 100644 --- a/editor/src/tools/workers/thumbnail/mesh.ts +++ b/editor/src/tools/workers/thumbnail/mesh.ts @@ -5,6 +5,8 @@ import { AssimpJSLoader } from "../../../loader/assimpjs"; import { readBlobAsDataUrl } from "../../tools"; +import { getLoaderPluginOptions } from "../../assets/loader"; + import { forceCompileAllSceneMaterials } from "../../scene/materials"; const assimpLoader = new AssimpJSLoader(false, false); @@ -42,7 +44,9 @@ export async function getPreview( scene.environmentTexture = environmentTexture; } - const container = await LoadAssetContainerAsync(absolutePath, scene); + const container = await LoadAssetContainerAsync(absolutePath, scene, { + pluginOptions: getLoaderPluginOptions(appPath ?? ""), + }); container.addAllToScene(); if (serializedOverrideMaterial) { @@ -53,18 +57,18 @@ export async function getPreview( } return new Promise(async (resolve) => { - scene.executeWhenReady(async () => { - scene.createDefaultCameraOrLight(true, true, true); - scene.createDefaultEnvironment({ - createSkybox: true, - enableGroundShadow: true, - enableGroundMirror: true, - }); + scene.createDefaultCameraOrLight(true, true, true); + scene.createDefaultEnvironment({ + createSkybox: true, + enableGroundShadow: true, + enableGroundMirror: true, + }); - const camera = scene.activeCamera as ArcRotateCamera; - camera.alpha = -Math.PI * 0.666; - camera.beta = Math.PI * 0.35; + const camera = scene.activeCamera as ArcRotateCamera; + camera.alpha = -Math.PI * 0.666; + camera.beta = Math.PI * 0.35; + scene.executeWhenReady(async () => { await forceCompileAllSceneMaterials(scene); scene.render(); From b249946b4dd4a2d59137060dfc6e6e1215ba08ce Mon Sep 17 00:00:00 2001 From: Julien Moreau-Mathis Date: Fri, 7 Aug 2026 13:22:06 +0200 Subject: [PATCH 18/22] feat: make graph optionally updated automatically when playing inline in editor fix: reworked UI for texture fields in inspector --- cli/src/pack/scripts.mts | 4 ++ editor/src/editor/layout/graph.tsx | 29 +++++++-- .../layout/inspector/fields/texture.tsx | 63 ++++++++++--------- editor/src/project/export/scripts.ts | 4 ++ editor/src/tools/scene/play/override.tsx | 6 +- editor/src/tools/tools.ts | 13 ++++ 6 files changed, 85 insertions(+), 34 deletions(-) diff --git a/cli/src/pack/scripts.mts b/cli/src/pack/scripts.mts index 97024138b..a70e3fa85 100644 --- a/cli/src/pack/scripts.mts +++ b/cli/src/pack/scripts.mts @@ -106,6 +106,10 @@ export async function createScriptsFile(projectDir: string): Promise { const promises: Promise[] = []; availableMetadata.forEach((configuration) => { configuration.metadata.scripts?.forEach((script) => { + if (!script.enabled) { + return; + } + promises.push( new Promise(async (resolve) => { const path = join(projectDir, "src", script.key); diff --git a/editor/src/editor/layout/graph.tsx b/editor/src/editor/layout/graph.tsx index a2685d561..f77bcebfb 100644 --- a/editor/src/editor/layout/graph.tsx +++ b/editor/src/editor/layout/graph.tsx @@ -1,17 +1,17 @@ import { extname } from "path/posix"; import { Component, DragEvent, ReactNode } from "react"; -import { Button, Tree, TreeNodeInfo } from "@blueprintjs/core"; +import { Button as BPButton, Tree, TreeNodeInfo } from "@blueprintjs/core"; import { FaLink } from "react-icons/fa6"; import { IoMdCube } from "react-icons/io"; import { AiOutlinePlus } from "react-icons/ai"; import { HiSpeakerWave } from "react-icons/hi2"; import { SiBabylondotjs } from "react-icons/si"; -import { MdOutlineQuestionMark } from "react-icons/md"; import { GiBrickWall, GiSparkles } from "react-icons/gi"; import { HiOutlineCubeTransparent } from "react-icons/hi"; import { IoCheckmark, IoPlay, IoSparklesSharp } from "react-icons/io5"; +import { MdOutlineQuestionMark, MdOutlineRefresh } from "react-icons/md"; import { TbGhost2Filled, TbServerSpark, TbBrandAdobeIndesign } from "react-icons/tb"; import { FaCamera, FaImage, FaLightbulb, FaBone, FaRegLightbulb } from "react-icons/fa"; @@ -21,6 +21,7 @@ import { BaseTexture, Node, Scene, Tools, IParticleSystem, Sprite, Skeleton, Tra import { Editor } from "../main"; import { Badge } from "../../ui/shadcn/ui/badge"; +import { Button } from "../../ui/shadcn/ui/button"; import { SpinnerUIComponent } from "../../ui/spinner"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "../../ui/shadcn/ui/dropdown-menu"; import { @@ -137,6 +138,11 @@ export interface IEditorGraphState { * Defines the reference to the play scene if the player is running, null otherwise. */ playScene: Scene | null; + /** + * Defines wether or not the graph should automatically refresh when the play scene is running. This is useful to see changes in the graph when the play scene is running. + * But in case of performance issues, this can be disabled to improve performance in-game. + */ + autoRefreshPlayScene: boolean; } export class EditorGraph extends Component { @@ -160,6 +166,7 @@ export class EditorGraph extends Component playScene: null, isLoading: false, + autoRefreshPlayScene: true, }; onNodesAddedObservable.add(() => this.refresh()); @@ -189,7 +196,17 @@ export class EditorGraph extends Component Runtime scene. Changes are not saved.
-
+ +
+ {!this.state.autoRefreshPlayScene && ( + + )} + +
)} @@ -205,7 +222,7 @@ export class EditorGraph extends Component - + +
setEnabled(v)} />
-
props.onRemove()}> +
props.onRemove()}>
diff --git a/editor/src/editor/layout/preview/play.tsx b/editor/src/editor/layout/preview/play.tsx index 7ac8efcd7..46a7dfa4e 100644 --- a/editor/src/editor/layout/preview/play.tsx +++ b/editor/src/editor/layout/preview/play.tsx @@ -274,6 +274,7 @@ export class EditorPreviewPlayComponent extends Component { - exportProject(this.props.editor, { optimize: false })}> + exportProject(this.props.editor, { optimize: false, debugMode: false })}> Generate Current Scene CTRL+G this.props.editor.setState({ generateProject: true })}>Generate All Scenes and Assets... diff --git a/editor/src/editor/main.tsx b/editor/src/editor/main.tsx index b5234b6c0..7e12c0f05 100644 --- a/editor/src/editor/main.tsx +++ b/editor/src/editor/main.tsx @@ -235,7 +235,7 @@ export class Editor extends Component { public async componentDidMount(): Promise { ipcRenderer.on("save", () => saveProject(this)); - ipcRenderer.on("generate", () => exportProject(this, { optimize: false })); + ipcRenderer.on("generate", () => exportProject(this, { optimize: false, debugMode: false })); ipcRenderer.on("editor:edit-project", () => this.setState({ editProject: true })); ipcRenderer.on("editor:edit-preferences", () => this.setState({ editPreferences: true })); diff --git a/editor/src/project/export/export.tsx b/editor/src/project/export/export.tsx index 0b9e9aec6..e8bebd418 100644 --- a/editor/src/project/export/export.tsx +++ b/editor/src/project/export/export.tsx @@ -38,6 +38,7 @@ import { ExportSceneProgressComponent, showExportSceneProgressDialog } from "./d export type IExportProjectOptions = { optimize: boolean; + debugMode: boolean; noDialog?: boolean; noProgress?: boolean; }; @@ -415,7 +416,7 @@ async function _exportProject(editor: Editor, options: IExportProjectOptions): P }); // Export scripts - await handleExportScripts(editor); + await handleExportScripts(editor, options.debugMode); // Export assets const promises: Promise[] = []; diff --git a/editor/src/project/export/scripts.ts b/editor/src/project/export/scripts.ts index 646385cd8..4109f4d87 100644 --- a/editor/src/project/export/scripts.ts +++ b/editor/src/project/export/scripts.ts @@ -66,7 +66,7 @@ interface ICollectedMetadata { metadata: any; } -export async function handleExportScripts(editor: Editor): Promise { +export async function handleExportScripts(editor: Editor, debugMode: boolean): Promise { if (!editor.state.projectPath) { return; } @@ -78,12 +78,15 @@ export async function handleExportScripts(editor: Editor): Promise { }); const scriptsMap: Record = {}; - const availableMetadata: ICollectedMetadata[] = []; // Check on all scenes in assets await Promise.all( sceneFolders.map(async (file) => { + if (file === editor.state.lastOpenedScenePath) { + return; + } + try { const config = await readJSON(join(file, "config.json")); if (config.metadata) { @@ -145,6 +148,13 @@ export async function handleExportScripts(editor: Editor): Promise { ); // Check on all nodes in current scene + if (editor.layout.preview.scene.metadata) { + availableMetadata.push({ + entityName: "currentScene", + metadata: editor.layout.preview.scene.metadata, + }); + } + const entities = [ ...editor.layout.preview.scene.meshes, ...editor.layout.preview.scene.lights, @@ -169,7 +179,7 @@ export async function handleExportScripts(editor: Editor): Promise { const promises: Promise[] = []; availableMetadata.forEach((configuration) => { configuration.metadata.scripts?.forEach((script) => { - if (!script.enabled) { + if (!script.enabled || (!debugMode && script.debugOnly)) { return; } From aee1766a1fa9078686a4de94ce478841be8deb11 Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Fri, 7 Aug 2026 17:24:25 +0200 Subject: [PATCH 20/22] chore: update yarn.lock --- yarn.lock | 205 +++++++++++++++++------------------------------------- 1 file changed, 62 insertions(+), 143 deletions(-) diff --git a/yarn.lock b/yarn.lock index f705bff91..61e96b86a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1345,6 +1345,11 @@ resolved "https://registry.yarnpkg.com/@dxup/unimport/-/unimport-0.1.2.tgz#a08e4039efe41c50d9c36fbb39d9976b88eefa68" integrity sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ== +"@electron-internal/extract-zip@^1.0.1": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@electron-internal/extract-zip/-/extract-zip-1.0.5.tgz#6782c0f6066e60b7fd286fe7a5c7600f7650d420" + integrity sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA== + "@electron/asar@3.2.18": version "3.2.18" resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.18.tgz#fa607f829209bab8b9e0ce6658d3fe81b2cba517" @@ -1372,20 +1377,19 @@ fs-extra "^9.0.1" minimist "^1.2.5" -"@electron/get@^2.0.0": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@electron/get/-/get-2.0.3.tgz#fba552683d387aebd9f3fcadbcafc8e12ee4f960" - integrity sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ== +"@electron/get@^5.0.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@electron/get/-/get-5.1.0.tgz#f96ca2a0e89b27490ff8f7b5a392bd4df6942998" + integrity sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA== dependencies: debug "^4.1.1" - env-paths "^2.2.0" - fs-extra "^8.1.0" - got "^11.8.5" + env-paths "^3.0.0" + graceful-fs "^4.2.11" progress "^2.0.3" - semver "^6.2.0" + semver "^7.6.3" sumchecker "^3.0.1" optionalDependencies: - global-agent "^3.0.0" + undici "^7.24.4" "@electron/node-gyp@https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2": version "10.2.0-electron.1" @@ -6458,7 +6462,7 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@^22", "@types/node@^22.7.7": +"@types/node@*", "@types/node@^22": version "22.15.32" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.32.tgz#c301cc2275b535a5e54bb81d516b1d2e9afe06e5" integrity sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA== @@ -6472,6 +6476,13 @@ dependencies: undici-types "~6.21.0" +"@types/node@^24.9.0": + version "24.13.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.13.3.tgz#49f18bd3c647866dcda51a0756c145e14590ce16" + integrity sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q== + dependencies: + undici-types "~7.18.0" + "@types/parse-json@^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" @@ -6555,13 +6566,6 @@ resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd" integrity sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g== -"@types/yauzl@^2.9.1": - version "2.10.3" - resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" - integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== - dependencies: - "@types/node" "*" - "@typescript-eslint/eslint-plugin@8.35.1": version "8.35.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz#06b1129fe26d6532abd58fb2b3fe9810bd016935" @@ -8182,11 +8186,6 @@ boolbase@^1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== -boolean@^3.0.1: - version "3.2.0" - resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" - integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== - bowser@^2.11.0: version "2.13.1" resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.13.1.tgz#5a4c652de1d002f847dd011819f5fc729f308a7e" @@ -9500,11 +9499,6 @@ detect-node-es@^1.1.0: resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== -detect-node@^2.0.4: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" - integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== - devalue@^5.8.2: version "5.9.0" resolved "https://registry.yarnpkg.com/devalue/-/devalue-5.9.0.tgz#5d30db41a0db9171cf4ee9dbf6617e0b77cd7c2a" @@ -9766,14 +9760,14 @@ electron-updater@6.6.2, electron-updater@^6.6.2: semver "^7.6.3" tiny-typed-emitter "^2.1.0" -electron@39.8.5: - version "39.8.5" - resolved "https://registry.yarnpkg.com/electron/-/electron-39.8.5.tgz#422d42318d993a77a960ea1a9b4bfb4822221388" - integrity sha512-q6+LiQIcTadSyvtPgLDQkCtVA9jQJXQVMrQcctfOJILh6OFMN+UJJLRkuUTy8CZDYeCIBn1ZycqsL1dAXugxZA== +electron@42.8.0: + version "42.8.0" + resolved "https://registry.yarnpkg.com/electron/-/electron-42.8.0.tgz#e3bbebc98dcc99266eaaed365c6555a022248712" + integrity sha512-lgeUDjUuUzUSBchBudmjCZ8ApeYdVneMi17nLRMdfxGw7FyLFligsLtIF+dL3UoNnOsupAwdqVNJCK5MFE82kQ== dependencies: - "@electron/get" "^2.0.0" - "@types/node" "^22.7.7" - extract-zip "^2.0.1" + "@electron-internal/extract-zip" "^1.0.1" + "@electron/get" "^5.0.0" + "@types/node" "^24.9.0" embla-carousel-react@8.6.0: version "8.6.0" @@ -9876,6 +9870,11 @@ env-paths@^2.2.0: resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== +env-paths@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-3.0.0.tgz#2f1e89c2f6dbd3408e1b1711dd82d62e317f58da" + integrity sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A== + err-code@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" @@ -10103,11 +10102,6 @@ es-to-primitive@^1.3.0: is-date-object "^1.0.5" is-symbol "^1.0.4" -es6-error@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" - integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== - esbuild@0.25.5: version "0.25.5" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.5.tgz#71075054993fdfae76c66586f9b9c1f8d7edd430" @@ -10643,17 +10637,6 @@ externality@^1.0.2: pathe "^1.1.1" ufo "^1.1.2" -extract-zip@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" - integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== - dependencies: - debug "^4.1.1" - get-stream "^5.1.0" - yauzl "^2.10.0" - optionalDependencies: - "@types/yauzl" "^2.9.1" - extsprintf@^1.2.0: version "1.4.1" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" @@ -10998,15 +10981,6 @@ fs-extra@^10.0.0, fs-extra@^10.1.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - fs-extra@^9.0.0, fs-extra@^9.0.1: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" @@ -11299,18 +11273,6 @@ glob@^8.0.1, glob@^8.1.0: minimatch "^5.0.1" once "^1.3.0" -global-agent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/global-agent/-/global-agent-3.0.0.tgz#ae7cd31bd3583b93c5a16437a1afe27cc33a1ab6" - integrity sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q== - dependencies: - boolean "^3.0.1" - es6-error "^4.1.1" - matcher "^3.0.0" - roarr "^2.15.3" - semver "^7.3.2" - serialize-error "^7.0.1" - global-directory@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/global-directory/-/global-directory-4.0.1.tgz#4d7ac7cfd2cb73f304c53b8810891748df5e361e" @@ -11328,7 +11290,7 @@ globals@^14.0.0: resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== -globalthis@^1.0.1, globalthis@^1.0.3: +globalthis@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== @@ -11367,7 +11329,7 @@ gopd@^1.2.0: resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -got@^11.7.0, got@^11.8.5: +got@^11.7.0: version "11.8.6" resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a" integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g== @@ -12425,11 +12387,6 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json-stringify-safe@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== - json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" @@ -12442,13 +12399,6 @@ json5@^2.1.2, json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== - optionalDependencies: - graceful-fs "^4.1.6" - jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" @@ -13019,13 +12969,6 @@ markdown-to-jsx@7.6.2: resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.6.2.tgz#254cbf7d412a37073486c0a2dd52266d2191a793" integrity sha512-gEcyiJXzBxmId2Y/kydLbD6KRNccDiUy/Src1cFGn3s2X0LZZ/hUiEc2VisFyA5kUE3SXclTCczjQiAuqKZiFQ== -matcher@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca" - integrity sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng== - dependencies: - escape-string-regexp "^4.0.0" - math-expression-evaluator@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-2.0.6.tgz#a33028e4d55e7dcfcca17a696738e1a25b8e2c22" @@ -13594,12 +13537,12 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -node-abi@4.14.0, node-abi@^3.45.0, node-abi@^4.2.0: - version "4.14.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-4.14.0.tgz#7846b36a732f1afa221be3ca0e6248448bc111ef" - integrity sha512-E4n91K4Nk1Rch2KzD+edU2bfZTP4W42GypAUDXU4vu1A+4u9PvUNDkGI0dXbsy8ZeF3WGj0SD/uHxnXD/sW+3w== +node-abi@^3.3.0, node-abi@^3.45.0, node-abi@^4.2.0: + version "3.94.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.94.0.tgz#007181ed0d1b56ae9670ea6c084d2bf83538405f" + integrity sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g== dependencies: - semver "^7.6.3" + semver "^7.3.5" node-addon-api@^1.6.3: version "1.7.2" @@ -13704,10 +13647,10 @@ node-pty@1.1.0-beta35: dependencies: node-addon-api "^7.1.0" -node-pty@1.2.0-beta.12: - version "1.2.0-beta.12" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.2.0-beta.12.tgz#4ea572e1b655e19c005f6e34e948e18ffd040d21" - integrity sha512-uExTCG/4VmSJa4+TjxFwPXv8BfacmfFEBL6JpxCMDghcwqzvD0yTcGmZ1fKOK6HY33tp0CelLblqTECJizc+Yw== +node-pty@1.2.0-beta.15: + version "1.2.0-beta.15" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.2.0-beta.15.tgz#cd62e0e32b69ce226c650935a35613fa41bf77d4" + integrity sha512-vORSzHXi4Ofl7HemVWpuudLqCPdaQb4LfpRCUpE5HPxhp4JYscl8zZwxh11p26v2wvW24WMwnMfLjhRLixrfxA== dependencies: node-addon-api "^7.1.0" @@ -15438,18 +15381,6 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -roarr@^2.15.3: - version "2.15.4" - resolved "https://registry.yarnpkg.com/roarr/-/roarr-2.15.4.tgz#f5fe795b7b838ccfe35dc608e0282b9eba2e7afd" - integrity sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A== - dependencies: - boolean "^3.0.1" - detect-node "^2.0.4" - globalthis "^1.0.1" - json-stringify-safe "^5.0.1" - semver-compare "^1.0.0" - sprintf-js "^1.1.2" - rolldown@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.3.tgz#db88a3008fb0e28230a00423727ce75ba32121ac" @@ -15708,22 +15639,22 @@ seek-bzip@^1.0.5: dependencies: commander "^2.8.1" -semver-compare@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" - integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== - semver@^5.5.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@^6.2.0, semver@^6.3.1: +semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.2, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.6.0, semver@^7.6.3, semver@^7.7.2, semver@^7.7.3, semver@^7.7.4, semver@^7.8.5: +semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.6.0, semver@^7.6.3, semver@^7.7.2, semver@^7.7.3, semver@^7.7.4: + version "7.8.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.0.tgz#ed0661039fcbcda2ce71f01fa6adbefaa77040df" + integrity sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA== + +semver@^7.8.5: version "7.8.5" resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69" integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA== @@ -15754,13 +15685,6 @@ sentence-case@^3.0.4: tslib "^2.0.3" upper-case-first "^2.0.2" -serialize-error@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-7.0.1.tgz#f1360b0447f61ffb483ec4157c737fab7d778e18" - integrity sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw== - dependencies: - type-fest "^0.13.1" - serialize-javascript@^7.0.3: version "7.0.5" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-7.0.5.tgz#c798cc0552ffbb08981914a42a8756e339d0d5b1" @@ -16246,11 +16170,6 @@ split@0.3: dependencies: through "2" -sprintf-js@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" - integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== - srvx@^0.11.22: version "0.11.22" resolved "https://registry.yarnpkg.com/srvx/-/srvx-0.11.22.tgz#f83413628014eb37416ba4609e10b4bb38472295" @@ -17069,11 +16988,6 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-fest@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" - integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== - type-fest@^0.21.3: version "0.21.3" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" @@ -17267,6 +17181,16 @@ undici-types@~6.21.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== +undici-types@~7.18.0: + version "7.18.2" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.18.2.tgz#29357a89e7b7ca4aef3bf0fd3fd0cd73884229e9" + integrity sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w== + +undici@^7.24.4: + version "7.29.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-7.29.0.tgz#ae0f6f62e06e057a9cbb7b2b5fde2bb74f791b8f" + integrity sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw== + unenv@2.0.0-rc.24, unenv@^2.0.0-rc.24: version "2.0.0-rc.24" resolved "https://registry.yarnpkg.com/unenv/-/unenv-2.0.0-rc.24.tgz#dd0035c3e93fedfa12c8454e34b7f17fe83efa2e" @@ -17377,11 +17301,6 @@ unist-util-visit@^5.0.0: unist-util-is "^6.0.0" unist-util-visit-parents "^6.0.0" -universalify@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== - universalify@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" @@ -18144,7 +18063,7 @@ yargs@^18.0.0: y18n "^5.0.5" yargs-parser "^22.0.0" -yauzl@^2.10.0, yauzl@^2.4.2: +yauzl@^2.4.2: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== From bf885b40a6da50f8fee0d33d49fb05e572c8f55d Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Fri, 7 Aug 2026 17:25:43 +0200 Subject: [PATCH 21/22] v5.4.3-alpha.5 --- 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 365735db7..4d18a85bb 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "babylonjs-editor-cli", - "version": "5.4.3-alpha.4", + "version": "5.4.3-alpha.5", "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 473cbf05e..622216036 100644 --- a/editor/package.json +++ b/editor/package.json @@ -1,6 +1,6 @@ { "name": "babylonjs-editor", - "version": "5.4.3-alpha.4", + "version": "5.4.3-alpha.5", "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 73de11a66..412743b64 100644 --- a/tools/package.json +++ b/tools/package.json @@ -1,6 +1,6 @@ { "name": "babylonjs-editor-tools", - "version": "5.4.3-alpha.4", + "version": "5.4.3-alpha.5", "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 6371e8ad9bcf566a773f244d83250e86d60e521e Mon Sep 17 00:00:00 2001 From: julien-moreau Date: Sat, 8 Aug 2026 00:41:47 +0200 Subject: [PATCH 22/22] chore: bump electron-updater to v6.8.9 and postcss to v8.5.26 --- editor/package.json | 2 +- templates/nextjs/package.json | 2 +- website/package.json | 2 +- yarn.lock | 63 +++++++++++++++++++++++++++-------- 4 files changed, 53 insertions(+), 16 deletions(-) diff --git a/editor/package.json b/editor/package.json index 622216036..02c08a89c 100644 --- a/editor/package.json +++ b/editor/package.json @@ -99,7 +99,7 @@ "decompress-targz": "^4.1.1", "dotenv": "17.2.3", "dunder-proto": "1.0.1", - "electron-updater": "6.6.2", + "electron-updater": "6.8.9", "esbuild": "0.28.1", "fflate": "0.8.3", "filenamify": "4.3.0", diff --git a/templates/nextjs/package.json b/templates/nextjs/package.json index 4677ce585..c3df49893 100644 --- a/templates/nextjs/package.json +++ b/templates/nextjs/package.json @@ -28,7 +28,7 @@ "babylonjs-editor-cli": "latest", "eslint": "9.29.0", "eslint-config-next": "16.2.6", - "postcss": "^8", + "postcss": "8.5.26", "raw-loader": "^4.0.2", "tailwindcss": "3.4.4", "typescript": "5.9.3" diff --git a/website/package.json b/website/package.json index 5ce4f7d7c..90d31fe75 100644 --- a/website/package.json +++ b/website/package.json @@ -43,7 +43,7 @@ "autoprefixer": "^10.0.1", "eslint": "9.29.0", "eslint-config-next": "16.2.6", - "postcss": "^8", + "postcss": "8.5.26", "tailwindcss": "3.4.4", "typescript": "5.9.3" } diff --git a/yarn.lock b/yarn.lock index 61e96b86a..ba8d9069d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7890,7 +7890,7 @@ babylonjs-editor-tools@latest: uid "" "babylonjs-editor-tools@link:tools": - version "5.4.3-alpha.4" + version "5.4.3-alpha.5" babylonjs-editor@latest: version "5.2.4" @@ -8319,6 +8319,14 @@ builder-util-runtime@9.3.1: debug "^4.3.4" sax "^1.2.4" +builder-util-runtime@9.7.0: + version "9.7.0" + resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz#c86fba303684e877daee15c29eede81987166fef" + integrity sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw== + dependencies: + debug "^4.3.4" + sax "^1.2.4" + builder-util@26.0.11: version "26.0.11" resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-26.0.11.tgz#ad85b92c93f2b976b973e1d87337e0c6813fcb8f" @@ -9746,7 +9754,21 @@ electron-to-chromium@^1.5.73: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz#8ea537b369c32527b3cc47df7973bffe5d3c2980" integrity sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ== -electron-updater@6.6.2, electron-updater@^6.6.2: +electron-updater@6.8.9: + version "6.8.9" + resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.8.9.tgz#21e3e2400ee3a58b7496f0a023d95d7ab1dae019" + integrity sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig== + dependencies: + builder-util-runtime "9.7.0" + fs-extra "^10.1.0" + js-yaml "^4.1.0" + lazy-val "^1.0.5" + lodash.escaperegexp "^4.1.2" + lodash.isequal "^4.5.0" + semver "~7.7.3" + tiny-typed-emitter "^2.1.0" + +electron-updater@^6.6.2: version "6.6.2" resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.6.2.tgz#3e65e044f1a99b00d61e200e24de8e709c69ce99" integrity sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw== @@ -13381,11 +13403,21 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nanoid@^3.3.16, nanoid@^3.3.17, nanoid@^3.3.6: +nanoid@^3.3.16: + version "3.3.16" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.16.tgz#a04d8ec4b1f10009d2d533947aefe4293737816c" + integrity sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q== + +nanoid@^3.3.17: version "3.3.18" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.18.tgz#f66a2de1199ffde0fcf21c8a5f13106b1c081913" integrity sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w== +nanoid@^3.3.6: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + nanotar@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/nanotar/-/nanotar-0.3.0.tgz#0a1839731ecbdd910129244a563f73183eb80c6a" @@ -14709,16 +14741,7 @@ postcss@8.4.31: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8, postcss@^8.4.23, postcss@^8.4.48, postcss@^8.5.10, postcss@^8.5.15, postcss@^8.5.6: - version "8.5.25" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.25.tgz#5012a598eaaa897f21bbe8553be3cb7bd2bd78cb" - integrity sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw== - dependencies: - nanoid "^3.3.16" - picocolors "^1.1.1" - source-map-js "^1.2.1" - -postcss@^8.5.19, postcss@^8.5.22: +postcss@8.5.26, postcss@^8.5.19, postcss@^8.5.22: version "8.5.26" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.26.tgz#6e75135780c7e10df3433bf2266c552d35c8c620" integrity sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ== @@ -14727,6 +14750,15 @@ postcss@^8.5.19, postcss@^8.5.22: picocolors "^1.1.1" source-map-js "^1.2.1" +postcss@^8.4.23, postcss@^8.4.48, postcss@^8.5.10, postcss@^8.5.15, postcss@^8.5.6: + version "8.5.25" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.25.tgz#5012a598eaaa897f21bbe8553be3cb7bd2bd78cb" + integrity sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw== + dependencies: + nanoid "^3.3.16" + picocolors "^1.1.1" + source-map-js "^1.2.1" + powershell-utils@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/powershell-utils/-/powershell-utils-0.1.0.tgz#5a42c9a824fb4f2f251ccb41aaae73314f5d6ac2" @@ -15659,6 +15691,11 @@ semver@^7.8.5: resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69" integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA== +semver@~7.7.3: + version "7.7.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" + integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== + send@^1.1.0, send@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/send/-/send-1.2.1.tgz#9eab743b874f3550f40a26867bf286ad60d3f3ed"