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..9f7e20e3f 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.5", "description": "Babylon.js Editor CLI is a command line interface to help you package your scenes made using the Babylon.js Editor", "productName": "Babylon.js Editor CLI", "scripts": { diff --git a/cli/src/pack/assets/ktx.mts b/cli/src/pack/assets/ktx.mts index 3469204da..7848b2161 100644 --- a/cli/src/pack/assets/ktx.mts +++ b/cli/src/pack/assets/ktx.mts @@ -95,7 +95,7 @@ export async function compressFileToKtxFormat(absolutePath: string, options: Com break; case "-dxt.ktx": - command = `"${pvrTexToolAbsolutePath}" -i "${absolutePath}" -flip y -pot + -m -ics lRGB ${hasAlpha ? "-l" : ""} -f ${hasAlpha ? "BC2" : "BC1"},UBN,lRGB -o "${options.destinationFolder}"`; + command = `"${pvrTexToolAbsolutePath}" -i "${absolutePath}" -flip y -pot + -m -dither -ics lRGB ${hasAlpha ? "-l" : ""} -f ${hasAlpha ? "BC2" : "BC1"},UBN,lRGB -o "${options.destinationFolder}"`; break; case "-pvrtc.ktx": 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/assets/texture.mts b/cli/src/pack/assets/texture.mts index 1d7b9bdc9..91a62d0a2 100644 --- a/cli/src/pack/assets/texture.mts +++ b/cli/src/pack/assets/texture.mts @@ -19,7 +19,7 @@ export interface IComputeExportedTextureOptions extends IProcessAssetFileOptions } export async function processExportedTexture(absolutePath: string, options: IComputeExportedTextureOptions): Promise { - const extension = extname(absolutePath).toLocaleLowerCase(); + const extension = extname(absolutePath); const metadata = await sharp(absolutePath).metadata(); if (!metadata.width || !metadata.height) { 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..b8d9ac0eb 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.5", "description": "Babylon.js Editor is a Web Application helping artists to work with Babylon.js", "productName": "Babylon.js Editor", "main": "build/src/index.js", @@ -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..661151de9 100644 --- a/editor/src/editor/layout/assets-browser.tsx +++ b/editor/src/editor/layout/assets-browser.tsx @@ -112,7 +112,7 @@ const ParticleSystemSelectable = createSelectable(AssetBrowserParticleSystemItem const directoryPackagesExtensions = [".scene", ".navmesh"]; -RegisterSceneLoaderPlugin(new AssimpJSLoader(true)); +RegisterSceneLoaderPlugin(new AssimpJSLoader(true, true)); export interface IEditorAssetsBrowserProps { /** @@ -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 66% rename from editor/src/editor/layout/graph/graph.tsx rename to editor/src/editor/layout/graph/context-menu.tsx index da95d8721..31dea6098 100644 --- a/editor/src/editor/layout/graph/graph.tsx +++ b/editor/src/editor/layout/graph/context-menu.tsx @@ -5,7 +5,7 @@ import { Component, PropsWithChildren, ReactNode } from "react"; import { IoMdCube } from "react-icons/io"; import { AiOutlinePlus, AiOutlineClose } from "react-icons/ai"; -import { Mesh, SubMesh, Node, InstancedMesh, Sprite, IParticleSystem } from "babylonjs"; +import { Mesh, Node, InstancedMesh, Sprite, IParticleSystem } from "babylonjs"; import { ContextMenu, @@ -22,7 +22,6 @@ import { import { showConfirm } from "../../../ui/dialog"; import { Separator } from "../../../ui/shadcn/ui/separator"; -import { SceneAssetBrowserDialogMode, showAssetBrowserDialog } from "../../../ui/scene-asset-browser"; import { getNodeCommands } from "../../dialogs/command-palette/node"; import { getMeshCommands } from "../../dialogs/command-palette/mesh"; @@ -34,12 +33,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"; @@ -50,6 +50,7 @@ import { Editor } from "../../main"; import { removeNodes } from "./remove"; import { exportScene, exportNode } from "./export"; +import { showUpdateResourcesFromAsset } from "./update-resources"; export interface IEditorGraphContextMenuProps extends PropsWithChildren { editor: Editor; @@ -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 @@ -119,6 +120,10 @@ export class EditorGraphContextMenu extends Component exportNode(this.props.editor, this.props.object)}>Export Node (.babylon) + showUpdateResourcesFromAsset(this.props.editor, this.props.object)}> + Update Resources... + + )} @@ -133,74 +138,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()} @@ -234,9 +247,6 @@ export class EditorGraphContextMenu extends Component this._createMeshInstance(this.props.object)}>Create Instance - - - this._updateMeshGeometry(this.props.object)}>Update Geometry... )} @@ -382,66 +392,6 @@ export class EditorGraphContextMenu extends Component { - const result = await showAssetBrowserDialog(this.props.editor, { - multiSelect: false, - filter: SceneAssetBrowserDialogMode.Meshes, - }); - - const selectedMesh = result.selectedMeshes[0]; - if (!selectedMesh?.geometry) { - return; - } - - const scene = this.props.editor.layout.preview.scene; - - scene.addGeometry(selectedMesh.geometry); - if (selectedMesh.skeleton) { - scene.addSkeleton(selectedMesh.skeleton); - } - - const oldkeleton = mesh.skeleton; - const oldGeometry = mesh.geometry; - - const oldSubMeshes = mesh.subMeshes.slice(0); - const newSubMeshes = selectedMesh.subMeshes.slice(0); - - const newSkeleton = selectedMesh.skeleton; - const newGeometry = selectedMesh.geometry; - - registerUndoRedo({ - executeRedo: true, - undo: () => { - newGeometry.releaseForMesh(mesh, false); - oldGeometry?.applyToMesh(mesh); - - mesh.skeleton = oldkeleton; - mesh.subMeshes = oldSubMeshes.map( - (subMesh, index) => new SubMesh(index, subMesh.verticesStart, subMesh.verticesCount, subMesh.indexStart, subMesh.indexCount, mesh, mesh, true, false) - ); - - result.selectedAnimationGroups.forEach((animationGroup) => { - scene.removeAnimationGroup(animationGroup); - }); - }, - redo: () => { - oldGeometry?.releaseForMesh(mesh, false); - newGeometry.applyToMesh(mesh); - - mesh.skeleton = newSkeleton; - mesh.subMeshes = newSubMeshes.map( - (subMesh, index) => new SubMesh(index, subMesh.verticesStart, subMesh.verticesCount, subMesh.indexStart, subMesh.indexCount, mesh, mesh, true, false) - ); - - result.selectedAnimationGroups.forEach((animationGroup) => { - scene.addAnimationGroup(animationGroup); - }); - }, - }); - - result.container.dispose(); - } - private _reloadSound(): void { reloadSound(this.props.editor, this.props.object); 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/graph/update-resources.tsx b/editor/src/editor/layout/graph/update-resources.tsx new file mode 100644 index 000000000..b3522c0c6 --- /dev/null +++ b/editor/src/editor/layout/graph/update-resources.tsx @@ -0,0 +1,411 @@ +import { basename, dirname, join } from "path/posix"; + +import { Component, ReactNode } from "react"; +import { createRoot } from "react-dom/client"; + +import { Node, LoadAssetContainerAsync, AssetContainer, Mesh, AnimationGroup, SubMesh, Tools, AnimatorAvatar, TransformNode } from "babylonjs"; + +import { SpinnerUIComponent } from "../../../ui/spinner"; +import { Checkbox } from "../../../ui/shadcn/ui/checkbox"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../../ui/shadcn/ui/tabs"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../ui/shadcn/ui/select"; +import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "../../../ui/shadcn/ui/table"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "../../../ui/shadcn/ui/alert-dialog"; + +import { UniqueNumber } from "../../../tools/tools"; +import { openSingleFileDialog } from "../../../tools/dialog"; +import { isMesh, isTransformNode } from "../../../tools/guards/nodes"; + +import { Editor } from "../../main"; + +export async function showUpdateResourcesFromAsset(editor: Editor, node: Node) { + const filename = openSingleFileDialog({ + title: "Select the asset to update the hierarchy from", + filters: [ + { name: "GLTF", extensions: ["gltf", "glb"] }, + { name: "Babylon.js Scene File", extensions: ["babylon"] }, + ], + defaultPath: editor.layout.assets.state.browsedPath, + }); + + if (!filename) { + return; + } + + const div = document.createElement("div"); + div.style.width = "100%"; + div.style.height = "100%"; + document.body.appendChild(div); + + const root = createRoot(div); + + return new Promise((resolve) => { + root.render( + { + resolve(); + + root.unmount(); + document.body.removeChild(div); + }} + /> + ); + }); +} + +export interface IUpdateResourcesMeshItem { + object: Mesh; + update: boolean; + matchedResource: Mesh | null; +} + +export interface IUpdateResourcesAnimationGroupItem { + update: boolean; + object: AnimationGroup; + rootNode: TransformNode | null; + matchedRootNode: TransformNode | null; +} + +export interface IUpdateResourcesFromAssetProps { + editor: Editor; + object: Node; + filename: string; + onClose: () => void; +} + +export interface IUpdateResourcesFromAssetState { + loading: boolean; + + objectMeshes: Mesh[]; + objectTransformNodes: TransformNode[]; + + assetMeshes: IUpdateResourcesMeshItem[]; + assetAnimationGroups: IUpdateResourcesAnimationGroupItem[]; +} + +export class UpdateResourcesFromAsset extends Component { + private _container!: AssetContainer; + + public constructor(props: IUpdateResourcesFromAssetProps) { + super(props); + + this.state = { + loading: true, + + objectMeshes: [], + objectTransformNodes: [], + + assetMeshes: [], + assetAnimationGroups: [], + }; + } + + public render(): ReactNode { + return ( + + + + Update Resources From Asset + {basename(this.props.filename)} + + + {this.state.loading && ( +
+ +
+ )} + + {!this.state.loading && ( + + + + Meshes + + + Animations + + + + {!this.state.loading && {this._getMeshesGridComponent(this.state.assetMeshes)}} + {!this.state.loading && {this._getAnimationGroupsGridComponent(this.state.assetAnimationGroups)}} + + )} + + + this.props.onClose()}>Cancel + this._update()}> + Update + + +
+
+ ); + } + + public async componentDidMount(): Promise { + this._container = await LoadAssetContainerAsync(basename(this.props.filename), this.props.editor.layout.preview.scene, { + rootUrl: join(dirname(this.props.filename), "/"), + }); + + this._container.meshes.forEach((mesh) => { + mesh.id = Tools.RandomId(); + mesh.uniqueId = UniqueNumber.Get(); + }); + + this._container.transformNodes.forEach((node) => { + node.id = Tools.RandomId(); + node.uniqueId = UniqueNumber.Get(); + }); + + this._container.geometries.forEach((geometry) => { + geometry.id = Tools.RandomId(); + geometry.uniqueId = UniqueNumber.Get(); + }); + + // Get all meshes from the edtited object. + const objectMeshes: Mesh[] = []; + if (isMesh(this.props.object)) { + objectMeshes.push(this.props.object); + } + + const objectTransformNodes: TransformNode[] = []; + if (isTransformNode(this.props.object)) { + objectTransformNodes.push(this.props.object); + } + + objectMeshes.push(...(this.props.object.getDescendants(false, (n) => isMesh(n)) as Mesh[])); + objectTransformNodes.push(...(this.props.object.getDescendants(false, (n) => isTransformNode(n)) as TransformNode[])); + + // Get all meshes from the asset that match the name of the meshes from the edited object. + const assetMeshes = this._container.meshes + .filter((m) => isMesh(m) && m.geometry) + .map((m) => { + const matchedResource = objectMeshes.find((om) => om.name === m.name) ?? null; + + return { + matchedResource, + object: m, + update: false, + } as IUpdateResourcesMeshItem; + }); + + // Get all animation groups + const assetAnimationGroups = this._container.animationGroups.map( + (ag) => + ({ + object: ag, + update: false, + rootNode: null, + }) as IUpdateResourcesAnimationGroupItem + ); + + this.setState({ + loading: false, + + objectMeshes, + objectTransformNodes, + + assetMeshes, + assetAnimationGroups, + }); + } + + public componentWillUnmount(): void { + this._container?.dispose(); + } + + private _update(): void { + // Update geometries if needed. + this.state.assetMeshes.forEach((c) => { + if (c.update && c.matchedResource) { + c.matchedResource.geometry?.releaseForMesh(c.matchedResource, true); + + if (c.object.geometry) { + const geometryContainerIndex = this._container.geometries.indexOf(c.object.geometry); + if (geometryContainerIndex !== -1) { + this._container.geometries.splice(geometryContainerIndex, 1); + } + + this.props.editor.layout.preview.scene.addGeometry(c.object.geometry); + c.object.geometry?.applyToMesh(c.matchedResource); + } + + c.matchedResource.subMeshes = c.object.subMeshes.map( + (sm, index) => new SubMesh(index, sm.verticesStart, sm.verticesCount, sm.indexStart, sm.indexCount, c.matchedResource!, c.matchedResource!, true, false) + ); + } + }); + + // Update animation groups + this.state.assetAnimationGroups.forEach((c) => { + if (c.update && c.rootNode && c.matchedRootNode) { + const oldName = c.rootNode?.name; + + const avatar = new AnimatorAvatar(c.object.name, c.rootNode ?? undefined, false); + + const animationGroup = avatar.retargetAnimationGroup(c.object, { + fixRootPosition: false, + fixGroundReference: false, + retargetAnimationKeys: false, + animationGroupName: c.object.name, + rootNodeName: c.matchedRootNode?.name, + }); + + animationGroup.play(); + + if (c.rootNode && oldName) { + c.rootNode.name = oldName; + } + } + }); + + this.props.onClose(); + } + + private _getMeshesGridComponent(components: IUpdateResourcesMeshItem[]): ReactNode { + const onRowClick = (c: IUpdateResourcesMeshItem) => { + c.update = !c.update; + this.forceUpdate(); + }; + + const onMatchedResourceChange = (c: IUpdateResourcesMeshItem, value: string) => { + c.matchedResource = this.state.objectMeshes.find((om) => om.name === value) ?? null; + this.forceUpdate(); + }; + + return ( + + List of all available meshes. + + + + Name + Target Mesh + + + + {components.map((c) => ( + onRowClick(c)}> + + + + {c.object.name} + + + + + ))} + +
+ ); + } + + private _getAnimationGroupsGridComponent(components: IUpdateResourcesAnimationGroupItem[]): ReactNode { + const allSceneNodes = [...this.state.objectMeshes, ...this.state.objectTransformNodes]; + const allAssetNodes = [...this._container.meshes, ...this._container.transformNodes]; + + const onRowClick = (c: IUpdateResourcesAnimationGroupItem) => { + c.update = !c.update; + this.forceUpdate(); + }; + + const onRootNodeChange = (c: IUpdateResourcesAnimationGroupItem, value: string) => { + c.rootNode = allSceneNodes.find((om) => om.name === value) ?? null; + + components.forEach((comp) => { + if (comp.update && !comp.rootNode) { + comp.rootNode = c.rootNode; + } + }); + + this.forceUpdate(); + }; + + const onAssetRootNodeChange = (c: IUpdateResourcesAnimationGroupItem, value: string) => { + c.matchedRootNode = allAssetNodes.find((om) => om.name === value) ?? null; + + components.forEach((comp) => { + if (comp.update && !comp.matchedRootNode) { + comp.matchedRootNode = c.matchedRootNode; + } + }); + + this.forceUpdate(); + }; + + return ( + + List of all available animation groups. + + + + Name + Root Node + Asset Root Node + + + + {components.map((c) => ( + onRowClick(c)}> + + + + {c.object.name} + + + + + + + + ))} + +
+ ); + } +} 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/electron/events/dialog.ts b/editor/src/electron/events/dialog.ts index 434001732..c18cfbc64 100644 --- a/editor/src/electron/events/dialog.ts +++ b/editor/src/electron/events/dialog.ts @@ -1,9 +1,10 @@ import { ipcMain, dialog } from "electron"; -ipcMain.on("editor:open-single-file-dialog", async (ev, title, filters) => { +ipcMain.on("editor:open-single-file-dialog", async (ev, title, filters, defaultPath) => { const result = await dialog.showOpenDialog({ title, filters, + defaultPath, properties: ["openFile"], }); 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/loader/assimpjs.ts b/editor/src/loader/assimpjs.ts index f9c5a6614..b0c26cd73 100644 --- a/editor/src/loader/assimpjs.ts +++ b/editor/src/loader/assimpjs.ts @@ -44,7 +44,10 @@ export class AssimpJSLoader implements ISceneLoaderPluginAsync { }, }; - public constructor(private _useWorker: boolean) {} + public constructor( + private _useWorker: boolean, + private _writeTextures: boolean + ) {} /** * Import meshes into a scene. @@ -152,7 +155,9 @@ export class AssimpJSLoader implements ISceneLoaderPluginAsync { // Textures runtime.data.textures?.forEach((t) => { - writeTexture(runtime, t); + if (this._writeTextures) { + writeTexture(runtime, t); + } }); parseNodes(runtime, [runtime.data.rootnode], null); diff --git a/editor/src/loader/material.ts b/editor/src/loader/material.ts index fabacf53a..e5db4e918 100644 --- a/editor/src/loader/material.ts +++ b/editor/src/loader/material.ts @@ -22,7 +22,7 @@ export function parseMaterial(runtime: AssimpJSRuntime, data: IAssimpJSMaterialD case "$raw.DiffuseColor|file": case "$raw.AmbientColor|file": case "$raw.SpecularColor|file": - if (typeof p.value === "string" && p.value) { + if (p.value && typeof p.value === "string") { const map = materialPropertyMap[p.key]; const texturePath = join(runtime.rootUrl, p.value.replace(/\\/g, "/")); 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/ktx.tsx b/editor/src/project/export/ktx.tsx index b8c4f01ce..ea55a868a 100644 --- a/editor/src/project/export/ktx.tsx +++ b/editor/src/project/export/ktx.tsx @@ -123,7 +123,7 @@ export async function compressFileToKtxFormat(editor: Editor, absolutePath: stri break; case "-dxt.ktx": - command = `"${cliPath}" -i "${absolutePath}" -flip y -pot + -m -ics lRGB ${hasAlpha ? "-l" : ""} -f ${hasAlpha ? "BC2" : "BC1"},UBN,lRGB -o "${options.destinationFolder}"`; + command = `"${cliPath}" -i "${absolutePath}" -flip y -pot + -m -dither -ics lRGB ${hasAlpha ? "-l" : ""} -f ${hasAlpha ? "BC2" : "BC1"},UBN,lRGB -o "${options.destinationFolder}"`; break; case "-pvrtc.ktx": 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..6d32594aa 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.join("/"), 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.join("/"), 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/save.tsx b/editor/src/project/save/save.tsx index afacf7615..aae8c3595 100644 --- a/editor/src/project/save/save.tsx +++ b/editor/src/project/save/save.tsx @@ -38,6 +38,7 @@ export async function saveProject(editor: Editor): Promise { } } finally { saving = false; + editor.layout.preview.setRenderScene(true); } } @@ -82,8 +83,6 @@ async function _saveProject(editor: Editor) { editor.layout.console.log(`Project "${project.lastOpenedScene}" saved.`); } - editor.layout.preview.setRenderScene(true); - toast.dismiss(toastId); toast.success("Project saved"); diff --git a/editor/src/project/save/scene.ts b/editor/src/project/save/scene.ts index 306228e32..d693c9e56 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, @@ -784,8 +791,6 @@ export async function saveScene(editor: Editor, projectPath: string, scenePath: savedFiles.push(configPath); } - dialog.dispose(); - // Remove old files const files = await normalizedGlob(join(scenePath, "/**"), { nodir: true, @@ -834,4 +839,6 @@ export async function saveScene(editor: Editor, projectPath: string, scenePath: // Update assets cache in all scenes and assets files. await applyAssetsCache(); + + dialog.dispose(); } diff --git a/editor/src/tools/assets/thumbnail.ts b/editor/src/tools/assets/thumbnail.ts index 0b76ea5bb..56bb92d2a 100644 --- a/editor/src/tools/assets/thumbnail.ts +++ b/editor/src/tools/assets/thumbnail.ts @@ -65,8 +65,8 @@ export async function computeOrGetThumbnail(editor: Editor, options: IComputeThu ++requestedPreviewCount; - if (previewCount > 0) { - await waitUntil(() => previewCount === 0); + if (previewCount === 4) { + await waitUntil(() => previewCount < 4); } ++previewCount; @@ -114,26 +114,19 @@ export async function computeOrGetThumbnail(editor: Editor, options: IComputeThu return thumbnail; } -let worker: Worker | null = null; - /** * Creates or gets the current worker used to compute thumbnails. * Worker is null by default and can be terminated in case the asset takes too much time to compute its thumbnail. */ export function createOrGetThumbnailWorker() { - if (!worker) { - worker = loadWorker("workers/thumbnail/main.js"); - } - - return worker; + return loadWorker("workers/thumbnail/main.js"); } /** * Terminates the current thumbnail worker if any. */ -export function terminateWorker() { - worker?.terminate(); - worker = null; +export function terminateWorker(worker: Worker) { + worker.terminate(); } export interface IThumbnailOptions { @@ -167,18 +160,21 @@ export interface IThumbnailOptions { */ export async function getAssetThumbnailBase64(absolutePath: string, options: IThumbnailOptions) { const result = await new Promise<{ preview: string }>(async (resolve) => { + const worker = createOrGetThumbnailWorker(); + const timeoutId = setTimeout(() => { - terminateWorker(); + terminateWorker(worker); resolve({ preview: "" }); }, 10_000); - const r = await executeSimpleWorker<{ preview: string }>(createOrGetThumbnailWorker(), { + const r = await executeSimpleWorker<{ preview: string }>(worker, { absolutePath, ...options, id: Tools.RandomId(), }); clearTimeout(timeoutId); + terminateWorker(worker); if (r) { resolve(r); diff --git a/editor/src/tools/dialog.ts b/editor/src/tools/dialog.ts index d7d90e512..a996a16be 100644 --- a/editor/src/tools/dialog.ts +++ b/editor/src/tools/dialog.ts @@ -3,10 +3,11 @@ import { FileFilter, ipcRenderer } from "electron"; export type OpenFileDialogOptions = { title?: string; filters?: FileFilter[]; + defaultPath?: string; }; export function openSingleFileDialog(options?: OpenFileDialogOptions): string { - return ipcRenderer.sendSync("editor:open-single-file-dialog", options?.title, options?.filters); + return ipcRenderer.sendSync("editor:open-single-file-dialog", options?.title, options?.filters, options?.defaultPath); } export function openMultipleFilesDialog(options?: OpenFileDialogOptions): string[] { 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/src/tools/workers/script.ts b/editor/src/tools/workers/script.ts index 909fe998c..544d7e371 100644 --- a/editor/src/tools/workers/script.ts +++ b/editor/src/tools/workers/script.ts @@ -99,6 +99,7 @@ function extract(outputAbsolutePath: string) { // Catch silently. } } catch (e) { + console.error(e); // Catch silently. } diff --git a/editor/src/tools/workers/thumbnail/mesh.ts b/editor/src/tools/workers/thumbnail/mesh.ts index d4afe5586..0c8725921 100644 --- a/editor/src/tools/workers/thumbnail/mesh.ts +++ b/editor/src/tools/workers/thumbnail/mesh.ts @@ -7,7 +7,7 @@ import { readBlobAsDataUrl } from "../../tools"; import { forceCompileAllSceneMaterials } from "../../scene/materials"; -const assimpLoader = new AssimpJSLoader(false); +const assimpLoader = new AssimpJSLoader(false, false); RegisterSceneLoaderPlugin(assimpLoader); let engine: Engine; diff --git a/editor/src/ui/scene-asset-browser.tsx b/editor/src/ui/scene-asset-browser.tsx deleted file mode 100644 index 3ff719fbe..000000000 --- a/editor/src/ui/scene-asset-browser.tsx +++ /dev/null @@ -1,388 +0,0 @@ -import { Component, ReactNode } from "react"; -import { createRoot } from "react-dom/client"; - -import { Node, AssetContainer, SceneLoader, Mesh, Skeleton, AnimationGroup } from "babylonjs"; - -import { Editor } from "../editor/main"; - -import { isMesh } from "../tools/guards/nodes"; -import { openSingleFileDialog } from "../tools/dialog"; -import { unique, waitNextAnimationFrame } from "../tools/tools"; - -import { Checkbox } from "./shadcn/ui/checkbox"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "./shadcn/ui/tabs"; -import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "./shadcn/ui/table"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "./shadcn/ui/alert-dialog"; - -import { showAlert } from "./dialog"; -import { SpinnerUIComponent } from "./spinner"; - -export enum SceneAssetBrowserDialogMode { - Meshes = 1, - Skeletons = 2, - Materials = 4, - Lights = 8, - AnimationGroups = 16, -} - -export type AssetsBrowserDialogOptions = { - /** - * Defines wether or not multi selection is enabled. - */ - multiSelect: boolean; - /** - * Defines the filter to apply to the scene asset browser dialog to only show specific elements. - */ - filter: SceneAssetBrowserDialogMode; -}; - -export type AssetsBrowserDialogResult = { - container: AssetContainer; - - selectedMeshes: Mesh[]; - selectedSkeletons: Skeleton[]; - selectedAnimationGroups: AnimationGroup[]; -}; - -/** - * Shows the file open dialog to select a scene file (.babylon, .gltf, etc.) and then opens the dialog - * used to browse the scene file and select elements to import. - * @param editor defines the reference to the editor. - * @param options defines the options of the assets browser dialog. - */ -export function showAssetBrowserDialog(editor: Editor, options: AssetsBrowserDialogOptions): Promise { - const filename = openSingleFileDialog({ - title: "Select Asset", - filters: [ - { - name: "Supported Scene Files", - extensions: [".babylon", ".gltf", ".glb", ".fbx"], - }, - ], - }); - - if (!filename) { - return Promise.reject("User decided to not pick any asset in scene file."); - } - - const div = document.createElement("div"); - div.style.width = "100%"; - div.style.height = "100%"; - document.body.appendChild(div); - - const root = createRoot(div); - - return new Promise((resolve, reject) => { - root.render( - { - reject("User decided to close the dialog without selecting any asset."); - - root.unmount(); - document.body.removeChild(div); - }} - onSelectedAssets={(result) => { - resolve(result); - - root.unmount(); - document.body.removeChild(div); - }} - /> - ); - }); -} - -export interface ISceneAssetBrowserDialogProps { - /** - * Defines the reference to the editor. - */ - editor: Editor; - /** - * Defines the absolute path to the scene file to load and pick items in. - */ - filename: string; - - /** - * Defines wether or not multi-select is enabled. - */ - multiSelect: boolean; - /** - * Defines the filter to apply to the scene asset browser dialog to only show specific elements. - */ - filter: SceneAssetBrowserDialogMode; - - /** - * Defines the callback called on the user wants to close the dialog. - */ - onClose: () => void; - /** - * Defines the callback called on the user wants to import some assets. - */ - onSelectedAssets: (result: AssetsBrowserDialogResult) => void; -} - -export interface ISceneAssetBrowserDialogState { - /** - * Defines wether or not the scene file is being loaded. - */ - loading: boolean; - /** - * Defines the list of all selected meshes. - */ - selectedMeshes: Mesh[]; - /** - * Defines the list of all selected skeletons. - */ - selectedSkeletons: Skeleton[]; - /** - * Defines the list of all selected animation groups. - */ - selectedAnimationGroups: AnimationGroup[]; -} - -export class SceneAssetBrowserDialog extends Component { - private _container: AssetContainer | null = null; - - public constructor(props: ISceneAssetBrowserDialogProps) { - super(props); - - this.state = { - loading: true, - selectedMeshes: [], - selectedSkeletons: [], - selectedAnimationGroups: [], - }; - } - - public render(): ReactNode { - return ( - - - - Scene Browser - - - - {(this.props.filter & SceneAssetBrowserDialogMode.Meshes) !== 0 && ( - - Meshes - - )} - {(this.props.filter & SceneAssetBrowserDialogMode.Skeletons) !== 0 && ( - - Skeletons - - )} - - - {!this.state.loading && (this.props.filter & SceneAssetBrowserDialogMode.Meshes) !== 0 && ( - {this._getMeshesGridComponent()} - )} - - {!this.state.loading && (this.props.filter & SceneAssetBrowserDialogMode.Skeletons) !== 0 && ( - {this._getSkeletonsGridComponent()} - )} - - - {this.state.loading && ( -
- -
- )} -
-
- - this.props.onClose()}>Cancel - this._handleImport()}> - Import - - -
-
- ); - } - - public async componentDidMount(): Promise { - try { - this._container = await SceneLoader.LoadAssetContainerAsync("", this.props.filename, this.props.editor.layout.preview.scene); - } catch (e) { - return showAlert("Error", e.message); - } - - this.setState({ - loading: false, - }); - } - - private _handleImport(): void { - if (!this._container) { - return; - } - - this.state.selectedSkeletons.forEach((s) => { - this._container?.skeletons.splice(this._container?.skeletons.indexOf(s), 1); - }); - - this.state.selectedMeshes.forEach((m) => { - this._container?.meshes.splice(this._container?.meshes.indexOf(m), 1); - - if (m.geometry) { - this._container?.geometries.splice(this._container?.geometries.indexOf(m.geometry), 1); - } - }); - - const animationGroups = this._getAnimationGroupsToImport(); - animationGroups.forEach((ag) => { - this._container?.animationGroups.splice(this._container?.animationGroups.indexOf(ag), 1); - }); - - this.props.onSelectedAssets({ - container: this._container, - selectedAnimationGroups: animationGroups, - - selectedMeshes: this.state.selectedMeshes, - selectedSkeletons: this.state.selectedSkeletons, - }); - } - - private _getAnimationGroupsToImport(): AnimationGroup[] { - if (!this._container) { - return []; - } - - let nodes: Node[] = []; - this.state.selectedMeshes.forEach((m) => { - nodes.push(m); - - m.skeleton?.bones.forEach((b) => { - nodes.push(b); - if (b._linkedTransformNode) { - nodes.push(b._linkedTransformNode); - } - }); - }); - - this.state.selectedSkeletons.forEach((s) => { - s.bones.forEach((b) => { - nodes.push(b); - if (b._linkedTransformNode) { - nodes.push(b._linkedTransformNode); - } - }); - }); - - nodes = unique(nodes); - - const animationGroups = this._container.animationGroups.filter((ag) => { - return ag.targetedAnimations.find((ta) => nodes.includes(ta.target)); - }); - - animationGroups.push(...this.state.selectedAnimationGroups); - - return unique(animationGroups); - } - - private async _handleSelectedAsset(asset: T, array: T[]): Promise { - if (!this.props.multiSelect) { - this.setState({ - selectedMeshes: [], - selectedSkeletons: [], - selectedAnimationGroups: [], - }); - - await waitNextAnimationFrame(); - } - - const slice = this.props.multiSelect ? array.slice(0) : []; - - const index = slice.indexOf(asset); - if (index !== -1) { - slice.splice(index, 1); - } else { - slice.push(asset); - } - - return slice; - } - - private _getMeshesGridComponent(): ReactNode { - const meshes = this._container?.meshes.filter((m) => isMesh(m) && m.geometry) as Mesh[]; - - return ( - - List of all available meshes with geometry. - - - - Name - Vertices - - - - {meshes?.map((m) => ( - { - this.setState({ - selectedMeshes: await this._handleSelectedAsset(m, this.state.selectedMeshes), - }); - }} - > - - - - {m.name} - {m.geometry!.getTotalVertices()} - - ))} - -
- ); - } - - private _getSkeletonsGridComponent(): ReactNode { - return ( - - List of all available skeletons. - - - - Name - Bones - - - - {this._container?.skeletons.map((s) => ( - { - this.setState({ - selectedSkeletons: await this._handleSelectedAsset(s, this.state.selectedSkeletons), - }); - }} - > - - - - {s.name} - {s.bones.length} - - ))} - -
- ); - } -} 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..3c799cb74 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.5", "description": "Babylon.js Editor Tools is a set of tools to help you create, edit and manage your Babylon.js scenes made using the Babylon.js Editor", "productName": "Babylon.js Editor Tools", "scripts": { @@ -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/index.ts b/tools/src/index.ts index 28994a895..c30029927 100644 --- a/tools/src/index.ts +++ b/tools/src/index.ts @@ -13,12 +13,15 @@ export * from "./tools/animation"; export * from "./tools/sprite"; export * from "./tools/particle"; export * from "./tools/ragdoll"; +export * from "./tools/mesh"; -export * from "./rendering/ssao"; +export * from "./rendering/vls"; export * from "./rendering/ssr"; +export * from "./rendering/taa"; +export * from "./rendering/ssao"; export * from "./rendering/motion-blur"; export * from "./rendering/default-pipeline"; -export * from "./rendering/vls"; +export * from "./rendering/tools"; export * from "./decorators/scene"; export * from "./decorators/gui"; 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..a11ef731c 100644 --- a/tools/src/loading/loader.ts +++ b/tools/src/loading/loader.ts @@ -3,13 +3,15 @@ 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 { applyMeshesLODQuality, configureMeshDistanceOrScreenCoverage } from "../tools/mesh"; import { configureShadowMapRefreshRate, configureShadowMapRenderListPredicate } from "../tools/light"; import { IScript } from "../script"; -import { applyRenderingConfigurationForCamera } from "../rendering/tools"; +import { applyRenderingConfigurationForCamera, IApplyRenderingConfigurationOptions } from "../rendering/tools"; import { configurePhysicsAggregate } from "./physics"; import { applyRenderingConfigurations } from "./rendering"; @@ -22,10 +24,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. @@ -67,6 +70,18 @@ export type SceneLoaderOptions = { * Same as "quality" but only applied to shadows. If set, this has priority over "quality". */ shadowsQuality?: SceneLoaderQualitySelector; + /** + * Sames as "quality" but only applied to LODs. If set, this has priority over "quality". + * This will affect the screen coverage or distance used to switch between LODs. The "very-low" quality level is even more aggressive with LODs. + */ + lodsQuality?: SceneLoaderQualitySelector; + + /** + * Defines the optional configuration to apply when applying the rendering configuration for a camera. + * This allows to selectively disable some post-processes when applying the rendering configuration for a camera. + * This is particularly useful for when your game provides options to enable/disable post-processes. + */ + postProcessConfiguration?: IApplyRenderingConfigurationOptions; /** * Defines the function called to notify the loading progress in interval [0, 1] @@ -88,14 +103,23 @@ declare module "@babylonjs/core/scene" { loadingQuality: SceneLoaderQualitySelector; loadingTexturesQuality: SceneLoaderQualitySelector; loadingShadowsQuality: SceneLoaderQualitySelector; + loadingLodsQuality: SceneLoaderQualitySelector; } } +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"; scene.loadingTexturesQuality = options?.texturesQuality ?? scene.loadingQuality; scene.loadingShadowsQuality = options?.shadowsQuality ?? scene.loadingQuality; + scene.loadingLodsQuality = options?.lodsQuality ?? scene.loadingQuality; registerAudioParser(); registerTextureParser(); @@ -108,6 +132,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 +160,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)); @@ -152,6 +185,9 @@ export async function loadScene(rootUrl: any, sceneFilename: string, scene: Scen options?.onProgress?.(1); + configureMeshDistanceOrScreenCoverage(scene); + applyMeshesLODQuality(scene.loadingLodsQuality, scene); + configureShadowMapRenderListPredicate(scene); configureShadowMapRefreshRate(scene); @@ -159,7 +195,7 @@ export async function loadScene(rootUrl: any, sceneFilename: string, scene: Scen applyRenderingConfigurations(scene, scene.metadata.rendering); if (scene.activeCamera) { - applyRenderingConfigurationForCamera(scene.activeCamera, rootUrl); + applyRenderingConfigurationForCamera(scene.activeCamera, rootUrl, options?.postProcessConfiguration); } } diff --git a/tools/src/loading/shadows.ts b/tools/src/loading/shadows.ts index 7370b75ed..f5c746d84 100644 --- a/tools/src/loading/shadows.ts +++ b/tools/src/loading/shadows.ts @@ -17,8 +17,12 @@ export function registerShadowGeneratorParser() { const shadowsGeneratorParser = GetParser(SceneComponentConstants.NAME_SHADOWGENERATOR); AddParser("ShadowGeneratorEditorPlugin", (parsedData: any, scene: Scene, container: AssetContainer, rootUrl: string) => { - if (scene.loadingShadowsQuality !== "high") { - parsedData.shadowGenerators?.forEach((shadowGenerator: any) => { + const savedShadowGenerators = new Map(); + + parsedData.shadowGenerators?.forEach((shadowGenerator: any) => { + savedShadowGenerators.set(shadowGenerator.id, shadowGenerator.mapSize); + + if (scene.loadingShadowsQuality !== "high") { switch (scene.loadingShadowsQuality) { case "medium": shadowGenerator.mapSize = shadowGenerator.mapSize * 0.5; @@ -34,9 +38,22 @@ export function registerShadowGeneratorParser() { } shadowGenerator.mapSize = Math.max(128, getPowerOfTwoUntil(shadowGenerator.mapSize)); - }); - } + } + }); shadowsGeneratorParser?.(parsedData, scene, container, rootUrl); + + scene.lights.forEach((light) => { + const shadowGenerator = light.getShadowGenerator(); + const shadowMap = shadowGenerator?.getShadowMap(); + + if (shadowMap) { + const id = shadowGenerator!.id; + const savedMapSize = savedShadowGenerators.get(id); + if (savedMapSize) { + shadowGenerator!.originalMapSize = savedMapSize; + } + } + }); }); } diff --git a/tools/src/loading/texture.ts b/tools/src/loading/texture.ts index c69ab0177..f22de882d 100644 --- a/tools/src/loading/texture.ts +++ b/tools/src/loading/texture.ts @@ -3,7 +3,7 @@ import { Nullable } from "@babylonjs/core/types"; import { BaseTexture } from "@babylonjs/core/Materials/Textures/baseTexture"; import { SerializationHelper } from "@babylonjs/core/Misc/decorators.serialization"; -import { getPowerOfTwoUntil } from "../tools/scalar"; +import { getTextureUrl } from "../tools/texture"; let registered = false; @@ -17,66 +17,19 @@ export function registerTextureParser() { const textureParser = SerializationHelper._TextureParser; SerializationHelper._TextureParser = (sourceProperty: any, scene: Scene, rootUrl: string): Nullable => { - if (scene.loadingTexturesQuality === "high" || !sourceProperty.metadata?.baseSize) { + const suffix = getTextureUrl(sourceProperty, scene); + if (!suffix) { return textureParser(sourceProperty, scene, rootUrl); } - const width = sourceProperty.metadata.baseSize.width; - const height = sourceProperty.metadata.baseSize.height; + const originalName = sourceProperty.name; + sourceProperty.name = suffix; - const isPowerOfTwo = width === getPowerOfTwoUntil(width) || height === getPowerOfTwoUntil(height); - - let suffix = ""; - - switch (scene.loadingTexturesQuality) { - case "medium": - let midWidth = (width * 0.66) >> 0; - let midHeight = (height * 0.66) >> 0; - - if (isPowerOfTwo) { - midWidth = getPowerOfTwoUntil(midWidth); - midHeight = getPowerOfTwoUntil(midHeight); - } - - suffix = `_${midWidth}_${midHeight}`; - break; - - case "low": - case "very-low": - let lowWidth = (width * 0.33) >> 0; - let lowHeight = (height * 0.33) >> 0; - - if (isPowerOfTwo) { - lowWidth = getPowerOfTwoUntil(lowWidth); - lowHeight = getPowerOfTwoUntil(lowHeight); - } - - suffix = `_${lowWidth}_${lowHeight}`; - break; + const texture = textureParser(sourceProperty, scene, rootUrl); + if (texture) { + texture.name = originalName; } - const name = sourceProperty.name as string; - - if (!name || !suffix) { - return textureParser(sourceProperty, scene, rootUrl); - } - - const finalUrl = name.split("/"); - - const filename = finalUrl.pop(); - if (!filename) { - return textureParser(sourceProperty, scene, rootUrl); - } - - const extension = filename.split(".").pop(); - const baseFilename = filename.replace(`.${extension}`, ""); - - const newFilename = `${baseFilename}${suffix}.${extension}`; - - finalUrl.push(newFilename); - - sourceProperty.name = finalUrl.join("/"); - - return textureParser(sourceProperty, scene, rootUrl); + return texture; }; } diff --git a/tools/src/rendering/tools.ts b/tools/src/rendering/tools.ts index 6d1582b9a..65e9bf223 100644 --- a/tools/src/rendering/tools.ts +++ b/tools/src/rendering/tools.ts @@ -21,6 +21,15 @@ export function saveRenderingConfigurationForCamera(camera: Camera) { taaRenderingPipelineCameraConfigurations.set(camera, serializeTAARenderingPipeline()); } +export interface IApplyRenderingConfigurationOptions { + ssao2Disabled?: boolean; + vlsDisabled?: boolean; + ssrDisabled?: boolean; + motionBlurDisabled?: boolean; + defaultPipelineDisabled?: boolean; + taaDisabled?: boolean; +} + /** * Applies the post-processes configurations for the given camera. Rendering configurations (motion blur, ssao, etc.) are * saved per-camera and can be applied on demand using this function. @@ -28,7 +37,7 @@ export function saveRenderingConfigurationForCamera(camera: Camera) { * @param camera defines the reference to the camera to apply its rendering configurations. * @param rootUrl defines the rootUrl that contains all resource files needed by the post-processes (color grading texture, etc.). */ -export function applyRenderingConfigurationForCamera(camera: Camera, rootUrl: string) { +export function applyRenderingConfigurationForCamera(camera: Camera, rootUrl: string, options?: IApplyRenderingConfigurationOptions) { disposeSSAO2RenderingPipeline(); disposeVLSPostProcess(camera.getScene()); disposeSSRRenderingPipeline(); @@ -37,32 +46,32 @@ export function applyRenderingConfigurationForCamera(camera: Camera, rootUrl: st disposeTAARenderingPipeline(); const ssao2RenderingPipeline = ssaoRenderingPipelineCameraConfigurations.get(camera); - if (ssao2RenderingPipeline) { + if (ssao2RenderingPipeline && !options?.ssao2Disabled) { parseSSAO2RenderingPipeline(camera.getScene(), camera, ssao2RenderingPipeline); } const vlsPostProcess = vlsPostProcessCameraConfigurations.get(camera); - if (vlsPostProcess) { + if (vlsPostProcess && !options?.vlsDisabled) { parseVLSPostProcess(camera.getScene(), vlsPostProcess); } const ssrRenderingPipeline = ssrRenderingPipelineCameraConfigurations.get(camera); - if (ssrRenderingPipeline) { + if (ssrRenderingPipeline && !options?.ssrDisabled) { parseSSRRenderingPipeline(camera.getScene(), camera, ssrRenderingPipeline); } const motionBlurPostProcess = motionBlurPostProcessCameraConfigurations.get(camera); - if (motionBlurPostProcess) { + if (motionBlurPostProcess && !options?.motionBlurDisabled) { parseMotionBlurPostProcess(camera.getScene(), camera, motionBlurPostProcess); } const defaultRenderingPipeline = defaultPipelineCameraConfigurations.get(camera); - if (defaultRenderingPipeline) { + if (defaultRenderingPipeline && !options?.defaultPipelineDisabled) { parseDefaultRenderingPipeline(camera.getScene(), camera, defaultRenderingPipeline, rootUrl); } const taaRenderingPipeline = taaRenderingPipelineCameraConfigurations.get(camera); - if (taaRenderingPipeline) { + if (taaRenderingPipeline && !options?.taaDisabled) { parseTAARenderingPipeline(camera.getScene(), camera, taaRenderingPipeline); } } 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/light.ts b/tools/src/tools/light.ts index d090d5a9e..ea744392c 100644 --- a/tools/src/tools/light.ts +++ b/tools/src/tools/light.ts @@ -2,6 +2,16 @@ import { Scene } from "@babylonjs/core/scene"; import { Vector3 } from "@babylonjs/core/Maths/math.vector"; import { RenderTargetTexture } from "@babylonjs/core/Materials/Textures/renderTargetTexture"; +import { SceneLoaderQualitySelector } from "../loading/loader"; + +import { getPowerOfTwoUntil } from "./scalar"; + +declare module "@babylonjs/core/Lights/Shadows/shadowGenerator" { + export interface IShadowGenerator { + originalMapSize?: number; + } +} + export function configureShadowMapRenderListPredicate(scene: Scene) { scene.lights.forEach((light) => { const shadowMap = light.getShadowGenerator()?.getShadowMap(); @@ -26,3 +36,42 @@ export async function configureShadowMapRefreshRate(scene: Scene) { }); }); } + +/** + * Updates the map size of the shadow generators in the scene to match the given quality when possible. + * @param quality defines the quality to apply to the shadow generators. + * @param scene defines the scene to update the shadows in. + * @see `SceneLoaderQualitySelector` for more information on the available quality levels. + */ +export function applyShadowsQuality(quality: SceneLoaderQualitySelector, scene: Scene) { + scene.lights.forEach((light) => { + const shadowGenerator = light.getShadowGenerator(); + const shadowMap = shadowGenerator?.getShadowMap(); + + if (shadowGenerator?.originalMapSize) { + let newMapSize = shadowGenerator.originalMapSize; + + switch (quality) { + case "medium": + newMapSize = newMapSize * 0.5; + break; + + case "low": + newMapSize = newMapSize * 0.25; + break; + + case "very-low": + newMapSize = newMapSize * 0.125; + break; + } + + newMapSize = Math.max(128, getPowerOfTwoUntil(newMapSize)); + + if (shadowMap?.getSize().width !== newMapSize) { + shadowMap!.resize(newMapSize); + } + } + }); + + configureShadowMapRefreshRate(scene); +} diff --git a/tools/src/tools/mesh.ts b/tools/src/tools/mesh.ts index de755361c..69e653384 100644 --- a/tools/src/tools/mesh.ts +++ b/tools/src/tools/mesh.ts @@ -1,8 +1,66 @@ +import { Scene } from "@babylonjs/core/scene"; import { PhysicsAggregate } from "@babylonjs/core/Physics/v2/physicsAggregate"; +import { SceneLoaderQualitySelector } from "../loading/loader"; + +import { isMesh } from "./guards"; + declare module "@babylonjs/core/Meshes/abstractMesh" { // eslint-disable-next-line @typescript-eslint/naming-convention export interface AbstractMesh { physicsAggregate?: PhysicsAggregate | null; } } + +declare module "@babylonjs/core/Meshes/mesh" { + // eslint-disable-next-line @typescript-eslint/naming-convention + export interface Mesh { + originalDistanceOrScreenCoverage?: number; + } +} + +export function configureMeshDistanceOrScreenCoverage(scene: Scene) { + scene.meshes.forEach((mesh) => { + if (isMesh(mesh)) { + mesh.getLODLevels().forEach((lod) => { + if (lod.mesh) { + lod.mesh.originalDistanceOrScreenCoverage = lod.distanceOrScreenCoverage; + } + }); + } + }); +} + +/** + * Updates the distance or screen coverage of the LOD levels of the meshes in the scene to match the given quality when possible. + * @param quality defines the quality to apply to the meshes LOD levels. + * @param scene defines the scene to update the meshes LOD levels in. + * @see `SceneLoaderQualitySelector` for more information on the available quality levels. + */ +export function applyMeshesLODQuality(quality: SceneLoaderQualitySelector, scene: Scene) { + scene.meshes.forEach((mesh) => { + if (isMesh(mesh)) { + mesh.getLODLevels().forEach((lod) => { + if (lod.mesh?.originalDistanceOrScreenCoverage) { + switch (quality) { + case "very-low": + lod.distanceOrScreenCoverage = lod.mesh.originalDistanceOrScreenCoverage * 0.125; + break; + + case "low": + lod.distanceOrScreenCoverage = lod.mesh.originalDistanceOrScreenCoverage * 0.25; + break; + + case "medium": + lod.distanceOrScreenCoverage = lod.mesh.originalDistanceOrScreenCoverage * 0.5; + break; + + case "high": + lod.distanceOrScreenCoverage = lod.mesh.originalDistanceOrScreenCoverage; + break; + } + } + }); + } + }); +} 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/src/tools/texture.ts b/tools/src/tools/texture.ts index bf6f65662..c7f8975d9 100644 --- a/tools/src/tools/texture.ts +++ b/tools/src/tools/texture.ts @@ -1,5 +1,11 @@ +import { Scene } from "@babylonjs/core/scene"; import { Engine } from "@babylonjs/core/Engines/engine"; +import { SceneLoaderQualitySelector } from "../loading/loader"; + +import { isTexture } from "./guards"; +import { getPowerOfTwoUntil } from "./scalar"; + /** * Set the compressed texture format to use, based on the formats you have, and the formats * supported by the hardware / browser. @@ -7,5 +13,102 @@ import { Engine } from "@babylonjs/core/Engines/engine"; * @see `@babylonjs/core/Engines/Extensions/engine.textureSelector.d.ts` for more information. */ export function configureEngineToUseCompressedTextures(engine: Engine) { + engine.setCompressedTextureExclusions([".env", ".hdr", ".dds"]); engine.setTextureFormatToUse(["-dxt.ktx", "-astc.ktx", "-pvrtc.ktx", "-etc1.ktx", "-etc2.ktx"]); } + +/** + * Updates URL of all textures in the scene to match the given quality when possible. + * @param quality defines the quality to apply to the textures. + * @param scene defines the scene to update the textures in. + * @param rootUrl defines the root URL to use when updating the texture URLs. + * @see `SceneLoaderQualitySelector` for more information on the available quality levels. + */ +export function applyTexturesQuality(quality: SceneLoaderQualitySelector, scene: Scene, rootUrl: string) { + if (scene.loadingTexturesQuality === quality) { + return; + } + + scene.loadingTexturesQuality = quality; + + scene.textures.forEach((texture) => { + if (!isTexture(texture) || !texture.url) { + return; + } + + const suffix = getTextureUrl(texture, scene); + if (!suffix || texture.url.includes(suffix)) { + return; + } + + texture.updateURL(rootUrl + suffix); + }); +} + +/** + * Gets the URL suffix to use for a texture based on the scene's loading texture quality and the texture's metadata. + * @param sourceProperty defines the texture to get the suffix for. Can be a texture or a texture serialization object. + * @param scene defines the scene to get the loading texture quality from. + * @returns the URL to use for the texture, or the original texture name if no suffix should be applied. + */ +export function getTextureUrl(sourceProperty: any, scene: Scene) { + if (scene.loadingTexturesQuality === "high" || !sourceProperty.metadata?.baseSize) { + return sourceProperty.name; + } + + const width = sourceProperty.metadata.baseSize.width; + const height = sourceProperty.metadata.baseSize.height; + + const isPowerOfTwo = width === getPowerOfTwoUntil(width) || height === getPowerOfTwoUntil(height); + + let suffix = ""; + + switch (scene.loadingTexturesQuality) { + case "medium": + let midWidth = (width * 0.66) >> 0; + let midHeight = (height * 0.66) >> 0; + + if (isPowerOfTwo) { + midWidth = getPowerOfTwoUntil(midWidth); + midHeight = getPowerOfTwoUntil(midHeight); + } + + suffix = `_${midWidth}_${midHeight}`; + break; + + case "low": + case "very-low": + let lowWidth = (width * 0.33) >> 0; + let lowHeight = (height * 0.33) >> 0; + + if (isPowerOfTwo) { + lowWidth = getPowerOfTwoUntil(lowWidth); + lowHeight = getPowerOfTwoUntil(lowHeight); + } + + suffix = `_${lowWidth}_${lowHeight}`; + break; + } + + const name = sourceProperty.name as string; + + if (!name || !suffix) { + return sourceProperty.name; + } + + const finalUrl = name.split("/"); + + const filename = finalUrl.pop(); + if (!filename) { + return sourceProperty.name; + } + + const extension = filename.split(".").pop(); + const baseFilename = filename.replace(`.${extension}`, ""); + + const newFilename = `${baseFilename}${suffix}.${extension}`; + + finalUrl.push(newFilename); + + return finalUrl.join("/"); +} 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"