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