diff --git a/editor/src/editor/layout/assets-browser.tsx b/editor/src/editor/layout/assets-browser.tsx index 2cad2e5e6..f9cdd94a9 100644 --- a/editor/src/editor/layout/assets-browser.tsx +++ b/editor/src/editor/layout/assets-browser.tsx @@ -65,6 +65,7 @@ import { FileInspectorObject } from "./inspector/file"; import { AssetBrowserGUIItem } from "./assets-browser/items/gui-item"; import { AssetBrowserHDRItem } from "./assets-browser/items/hdr-item"; import { AssetBrowserMeshItem } from "./assets-browser/items/mesh-item"; +import { AssetBrowserSkeletonItem } from "./assets-browser/items/skeleton-item"; import { AssetBrowserSceneItem } from "./assets-browser/items/scene-item"; import { AssetBrowserImageItem } from "./assets-browser/items/image-item"; import { AssetBrowserMaterialItem } from "./assets-browser/items/material-item"; @@ -90,6 +91,7 @@ const ImageSelectable = createSelectable(AssetBrowserImageItem); const SceneSelectable = createSelectable(AssetBrowserSceneItem); const MaterialSelectable = createSelectable(AssetBrowserMaterialItem); const CinematicSelectable = createSelectable(AssetBrowserCinematicItem); +const SkeletonSelectable = createSelectable(AssetBrowserSkeletonItem); export interface IEditorAssetsBrowserProps { /** @@ -791,6 +793,10 @@ export class EditorAssetsBrowser extends Component; + case ".bvh": + case ".BVH": + return ; + case ".material": return ; diff --git a/editor/src/editor/layout/assets-browser/items/item.tsx b/editor/src/editor/layout/assets-browser/items/item.tsx index 7a511506c..90633a6c7 100644 --- a/editor/src/editor/layout/assets-browser/items/item.tsx +++ b/editor/src/editor/layout/assets-browser/items/item.tsx @@ -15,7 +15,7 @@ import { toast } from "sonner"; import { VscJson } from "react-icons/vsc"; import { ImFinder } from "react-icons/im"; import { BiSolidFileCss } from "react-icons/bi"; -import { GiCeilingLight } from "react-icons/gi"; +import { GiCeilingLight, GiSkeletonInside } from "react-icons/gi"; import { GrStatusUnknown } from "react-icons/gr"; import { BsFiletypeMp3, BsFiletypeWav } from "react-icons/bs"; import { AiFillFileMarkdown, AiOutlineClose } from "react-icons/ai"; @@ -455,6 +455,10 @@ export class AssetsBrowserItem extends Component; + case ".bvh": + case ".BVH": + return ; + case ".ies": return ; diff --git a/editor/src/editor/layout/assets-browser/items/skeleton-item.tsx b/editor/src/editor/layout/assets-browser/items/skeleton-item.tsx new file mode 100644 index 000000000..70a8e5df2 --- /dev/null +++ b/editor/src/editor/layout/assets-browser/items/skeleton-item.tsx @@ -0,0 +1,87 @@ +import { ReactNode } from "react"; +import { extname, basename } from "path"; + +import { GiSkeletonInside } from "react-icons/gi"; + +import { TransformNode, Tools, Debug, SceneLoader, Skeleton, Scene } from "babylonjs"; + +import { AssetsBrowserItem } from "./item"; +import { ContextMenuItem } from "../../../../ui/shadcn/ui/context-menu"; +import { UniqueNumber } from "../../../../tools/tools"; + +const DEFAULT_ANIMATION_FRAMES = 60; +const SKELETON_VIEWER_SCALE = 1; +const SKELETON_CONTAINER_SUFFIX = "_Container"; +export const SKELETON_CONTAINER_TYPE = "SkeletonContainer"; + +export class AssetBrowserSkeletonItem extends AssetsBrowserItem { + /** + * @override + */ + protected getContextMenuContent(): ReactNode { + return ( + <> + this._handleLoadSkeletonToScene()}> + Load to Scene + + + ); + } + + /** + * @override + */ + protected getIcon(): ReactNode { + return ; + } + + private async _handleLoadSkeletonToScene(): Promise { + const scene = this.props.editor.layout.preview.scene; + try { + const result = await SceneLoader.ImportMeshAsync("", "", this.props.absolutePath, scene); + + const skeleton = result.skeletons[0]; + + if (skeleton) { + // Define the skeleton name from the file name + const extension = extname(this.props.absolutePath).toLowerCase(); + if (extension === ".bvh") { + const skeletonName = basename(this.props.absolutePath, extension); + skeleton.name = skeletonName; + } + + this._createSkeletonContainer(skeleton, scene); + this.props.editor.layout.graph.refresh(); + } + } catch (error) { + this.props.editor.layout.console.error(`Failed to load BVH file: ${error}`); + } + } + + /** + * Creates a skeleton container with viewer and animation + */ + private _createSkeletonContainer(skeleton: Skeleton, scene: Scene): void { + const skeletonContainer = new TransformNode(`${skeleton.name}${SKELETON_CONTAINER_SUFFIX}`, scene); + skeletonContainer.id = Tools.RandomId(); + skeletonContainer.uniqueId = UniqueNumber.Get(); + + const viewer = new Debug.SkeletonViewer(skeleton, null, scene, false, SKELETON_VIEWER_SCALE, { + displayMode: Debug.SkeletonViewer.DISPLAY_SPHERE_AND_SPURS, + }); + viewer.isEnabled = true; + + // Store the skeleton reference and viewer in the container's metadata for easy access + skeletonContainer.metadata = { + ...skeletonContainer.metadata, + skeleton: skeleton, + viewer: viewer, + type: SKELETON_CONTAINER_TYPE, + }; + + const highestFrame = skeleton.bones[0]?.animations[0]?.getHighestFrame() ?? DEFAULT_ANIMATION_FRAMES; + scene.beginAnimation(skeleton, 0, highestFrame, true); + + this.props.editor.layout.console.log(`Loaded skeleton: ${skeleton.name} with ${skeleton.bones.length} bones`); + } +} diff --git a/editor/src/editor/layout/graph.tsx b/editor/src/editor/layout/graph.tsx index 248ddf862..2985c9eae 100644 --- a/editor/src/editor/layout/graph.tsx +++ b/editor/src/editor/layout/graph.tsx @@ -3,7 +3,7 @@ import { Button, Tree, TreeNodeInfo } from "@blueprintjs/core"; import { FaLink } from "react-icons/fa6"; import { IoMdCube } from "react-icons/io"; -import { GiSparkles } from "react-icons/gi"; +import { GiSparkles, GiSkeletonInside } from "react-icons/gi"; import { BsSoundwave } from "react-icons/bs"; import { AiOutlinePlus } from "react-icons/ai"; import { HiSpeakerWave } from "react-icons/hi2"; @@ -12,9 +12,10 @@ import { MdOutlineQuestionMark } from "react-icons/md"; import { HiOutlineCubeTransparent } from "react-icons/hi"; import { IoCheckmark, IoSparklesSharp } from "react-icons/io5"; import { SiAdobeindesign, SiBabylondotjs } from "react-icons/si"; +import { PiBoneLight } from "react-icons/pi"; import { AdvancedDynamicTexture } from "babylonjs-gui"; -import { BaseTexture, Node, Scene, Sound, Tools, IParticleSystem, ParticleSystem } from "babylonjs"; +import { BaseTexture, Node, Scene, Sound, Tools, IParticleSystem, ParticleSystem, Skeleton } from "babylonjs"; import { Editor } from "../main"; @@ -47,12 +48,13 @@ import { isCamera, isCollisionInstancedMesh, isCollisionMesh, - isEditorCamera, isInstancedMesh, isLight, isMesh, isNode, isTransformNode, + isSkeleton, + isBone, } from "../../tools/guards/nodes"; import { onNodeModifiedObservable, @@ -69,6 +71,7 @@ import { EditorGraphContextMenu } from "./graph/graph"; import { getMeshCommands } from "../dialogs/command-palette/mesh"; import { getLightCommands } from "../dialogs/command-palette/light"; import { getCameraCommands } from "../dialogs/command-palette/camera"; +import { SKELETON_CONTAINER_TYPE } from "./assets-browser/items/skeleton-item"; export interface IEditorGraphProps { /** @@ -262,8 +265,19 @@ export class EditorGraph extends Component if (this.state.showOnlyDecals) { nodes.push(...scene.meshes.filter((mesh) => mesh.metadata?.decal).map((mesh) => this._parseSceneNode(mesh, true))); } - } else { - nodes = scene.rootNodes.filter((n) => !isEditorCamera(n)).map((n) => this._parseSceneNode(n)); + } + + // Add skeleton containers (TransformNodes that contain skeletons) to avoid duplication + const skeletonContainers = scene.transformNodes.filter((transformNode) => { + return transformNode.metadata?.type === SKELETON_CONTAINER_TYPE; + }); + + // Add skeleton containers to the graph + if (skeletonContainers.length > 0) { + const containerNodes = skeletonContainers.map((container) => { + return this._parseSkeletonContainerNode(container); + }); + nodes.push(...containerNodes); } const guiNode = this._parseGuiNode(scene); @@ -295,8 +309,25 @@ export class EditorGraph extends Component * become unselected to have only the given node selected. All parents are expanded. * @param node defines the reference tot the node to select in the graph. */ - public setSelectedNode(node: Node | Sound | IParticleSystem): void { - let source = isSound(node) ? node["_connectedTransformNode"] : isAnyParticleSystem(node) ? node.emitter : node; + public setSelectedNode(node: Node | Sound | IParticleSystem | Skeleton): void { + let source: Node | null = null; + + if (isSound(node)) { + source = node["_connectedTransformNode"]; + } else if (isAnyParticleSystem(node)) { + if (isNode(node.emitter)) { + source = node.emitter; + } + } else if (isSkeleton(node)) { + // For skeletons, we don't have a parent to expand, just select the skeleton + this._forEachNode(this.state.nodes, (n) => { + n.isSelected = n.nodeData === node; + }); + this.setState({ nodes: this.state.nodes }); + return; + } else { + source = node; + } if (!source) { return; @@ -586,6 +617,66 @@ export class EditorGraph extends Component return rootSoundNode; } + private _parseSkeletonNode(skeleton: Skeleton): TreeNodeInfo | null { + if (!skeleton.name.toLowerCase().includes(this.state.search.toLowerCase())) { + return null; + } + + const info = { + id: skeleton.id, + nodeData: skeleton, + isSelected: false, + childNodes: [], + hasCaret: false, + icon: this._getSkeletonIconComponent(skeleton), + label: this._getNodeLabelComponent(skeleton, skeleton.name), + } as TreeNodeInfo; + + if (skeleton.bones.length > 0) { + info.childNodes = skeleton.bones.map((bone) => this._parseBoneNode(bone)).filter((b) => b !== null) as TreeNodeInfo[]; + info.hasCaret = true; + } + + this._forEachNode(this.state.nodes, (n) => { + if (n.id === info.id) { + info.isSelected = n.isSelected; + info.isExpanded = n.isExpanded; + } + }); + + return info; + } + + private _parseBoneNode(bone: any): TreeNodeInfo | null { + if (!bone.name.toLowerCase().includes(this.state.search.toLowerCase())) { + return null; + } + + const info = { + id: bone.id, + nodeData: bone, + isSelected: false, + childNodes: [], + hasCaret: false, + icon: this._getBoneIconComponent(bone), + label: this._getNodeLabelComponent(bone, bone.name), + } as TreeNodeInfo; + + if (bone.children && bone.children.length > 0) { + info.childNodes = bone.children.map((childBone: any) => this._parseBoneNode(childBone)).filter((b) => b !== null) as TreeNodeInfo[]; + info.hasCaret = true; + } + + this._forEachNode(this.state.nodes, (n) => { + if (n.id === info.id) { + info.isSelected = n.isSelected; + info.isExpanded = n.isExpanded; + } + }); + + return info; + } + private _getSoundNode(sound: Sound): TreeNodeInfo { const info = { nodeData: sound, @@ -674,6 +765,38 @@ export class EditorGraph extends Component return rootGuiNode; } + private _parseSkeletonContainerNode(container: Node): TreeNodeInfo { + const info = { + id: container.id, + nodeData: container, + isSelected: false, + childNodes: [], + hasCaret: false, + icon: this._getIcon(container), + label: this._getNodeLabelComponent(container, container.name), + } as TreeNodeInfo; + + if (container.metadata?.type === SKELETON_CONTAINER_TYPE && container.metadata?.skeleton) { + const skeletonNode = this._parseSkeletonNode(container.metadata.skeleton); + if (skeletonNode) { + info.childNodes!.push(skeletonNode); + } + } + + if (info.childNodes && info.childNodes.length > 0) { + info.hasCaret = true; + } + + this._forEachNode(this.state.nodes, (n) => { + if (n.id === info.id) { + info.isSelected = n.isSelected; + info.isExpanded = n.isExpanded; + } + }); + + return info; + } + private _parseSceneNode(node: Node, noChildren?: boolean): TreeNodeInfo | null { if ((isMesh(node) && (node._masterMesh || !isNodeVisibleInGraph(node))) || isCollisionMesh(node) || isCollisionInstancedMesh(node)) { return null; @@ -745,6 +868,13 @@ export class EditorGraph extends Component info.childNodes?.push(this._getParticleSystemNode(particleSystem)); } }); + + if (node.skeleton) { + const skeletonNode = this._parseSkeletonNode(node.skeleton); + if (skeletonNode) { + info.childNodes?.push(skeletonNode); + } + } } if (info.childNodes?.length) { @@ -818,6 +948,14 @@ export class EditorGraph extends Component ); } + private _getSkeletonIconComponent(skeleton: Skeleton): ReactNode { + return
{this._getIcon(skeleton)}
; + } + + private _getBoneIconComponent(bone: any): ReactNode { + return
{this._getIcon(bone)}
; + } + private _getIcon(object: any): ReactNode { if (isTransformNode(object)) { return ; @@ -855,6 +993,14 @@ export class EditorGraph extends Component return ; } + if (isSkeleton(object)) { + return ; + } + + if (isBone(object)) { + return ; + } + return ; } diff --git a/editor/src/editor/layout/graph/remove.ts b/editor/src/editor/layout/graph/remove.ts index 1b84841a4..2152a5f13 100644 --- a/editor/src/editor/layout/graph/remove.ts +++ b/editor/src/editor/layout/graph/remove.ts @@ -1,205 +1,236 @@ -import { Node, Light, AbstractMesh, Scene } from "babylonjs"; - -import { isSound } from "../../../tools/guards/sound"; -import { registerUndoRedo } from "../../../tools/undoredo"; -import { isSceneLinkNode } from "../../../tools/guards/scene"; -import { updateAllLights } from "../../../tools/light/shadows"; -import { isParticleSystem } from "../../../tools/guards/particles"; -import { isAdvancedDynamicTexture } from "../../../tools/guards/texture"; -import { getLinkedAnimationGroupsFor } from "../../../tools/animation/group"; -import { isNode, isMesh, isAbstractMesh, isInstancedMesh, isCollisionInstancedMesh, isTransformNode, isLight, isCamera } from "../../../tools/guards/nodes"; - -import { Editor } from "../../main"; - -type _RemoveNodeData = { - node: Node; - parent: Node | null; - - lights: Light[]; -}; - -/** - * Removes the currently selected nodes in the graph with undo/redo support. - * @param editor defines the reference to the editor used to get the selected nodes and refresh the graph. - */ -export function removeNodes(editor: Editor) { - const scene = editor.layout.preview.scene; - - const allData = editor.layout.graph - .getSelectedNodes() - .filter((n) => n.nodeData) - .map((n) => n.nodeData); - - const nodes = allData - .filter((n) => isNode(n)) - .map((node) => { - const attached = [node] - .concat(node.getDescendants(false, (n) => isNode(n))) - .map((descendant) => (isMesh(descendant) ? [descendant, ...descendant.instances] : [descendant])) - .flat() - .map((descendant) => { - return { - node: descendant, - parent: descendant.parent, - lights: scene.lights.filter((light) => { - return light - .getShadowGenerator() - ?.getShadowMap() - ?.renderList?.includes(descendant as AbstractMesh); - }), - } as _RemoveNodeData; - }); - - return attached; - }) - .flat(); - - const sounds = allData - .filter((d) => isSound(d)) - .map((sound) => ({ - sound, - soundtrack: scene.soundTracks?.[sound.soundTrackId + 1], - })); - - const particleSystems = allData.filter((d) => isParticleSystem(d)); - const advancedGuiTextures = allData.filter((d) => isAdvancedDynamicTexture(d)); - - const animationGroups = getLinkedAnimationGroupsFor([...particleSystems, ...advancedGuiTextures, ...sounds.map((d) => d.sound), ...nodes.map((d) => d.node)], scene); - - registerUndoRedo({ - executeRedo: true, - action: () => { - editor.layout.graph.refresh(); - editor.layout.preview.gizmo.setAttachedNode(null); - editor.layout.inspector.setEditedObject(editor.layout.preview.scene); - - updateAllLights(scene); - }, - undo: () => { - nodes.forEach((d) => { - restoreNodeData(d, scene); - }); - - sounds.forEach((d) => { - d.soundtrack?.addSound(d.sound); - }); - - particleSystems.forEach((particleSystem) => { - scene.addParticleSystem(particleSystem); - }); - - advancedGuiTextures.forEach((node) => { - scene.addTexture(node); - - const layer = scene.layers.find((layer) => layer.texture === node); - if (layer) { - layer.isEnabled = true; - } - }); - - animationGroups.forEach((targetedAnimations, animationGroup) => { - targetedAnimations.forEach((targetedAnimation) => { - animationGroup.addTargetedAnimation(targetedAnimation.animation, targetedAnimation.target); - }); - - if (!scene.animationGroups.includes(animationGroup)) { - scene.addAnimationGroup(animationGroup); - } - }); - }, - redo: () => { - nodes.forEach((d) => { - removeNodeData(d, scene); - }); - - sounds.forEach((d) => { - d.soundtrack?.removeSound(d.sound); - }); - - particleSystems.forEach((particleSystem) => { - scene.removeParticleSystem(particleSystem); - }); - - advancedGuiTextures.forEach((node) => { - scene.removeTexture(node); - - const layer = scene.layers.find((layer) => layer.texture === node); - if (layer) { - layer.isEnabled = false; - } - }); - - animationGroups.forEach((targetedAnimations, animationGroup) => { - targetedAnimations.forEach((targetedAnimation) => { - animationGroup.removeTargetedAnimation(targetedAnimation.animation); - }); - - if (!animationGroup.targetedAnimations.length) { - scene.removeAnimationGroup(animationGroup); - } else { - console.log(nodes.find((d) => d.node === animationGroup.targetedAnimations[0].target)); - } - }); - }, - }); -} - -function restoreNodeData(data: _RemoveNodeData, scene: Scene) { - const node = data.node; - - if (isAbstractMesh(node)) { - if (isInstancedMesh(node) || isCollisionInstancedMesh(node)) { - node.sourceMesh.addInstance(node); - } - - scene.addMesh(node); - - data.lights.forEach((light) => { - light.getShadowGenerator()?.getShadowMap()?.renderList?.push(node); - }); - } - - if (isTransformNode(node) || isSceneLinkNode(node)) { - scene.addTransformNode(node); - } - - if (isLight(node)) { - scene.addLight(node); - } - - if (isCamera(node)) { - scene.addCamera(node); - } -} - -function removeNodeData(data: _RemoveNodeData, scene: Scene) { - const node = data.node; - - if (isAbstractMesh(node)) { - if (isInstancedMesh(node) || isCollisionInstancedMesh(node)) { - node.sourceMesh.removeInstance(node); - } - - scene.removeMesh(node); - - data.lights.forEach((light) => { - const renderList = light.getShadowGenerator()?.getShadowMap()?.renderList; - const index = renderList?.indexOf(node) ?? -1; - if (index !== -1) { - renderList?.splice(index, 1); - } - }); - } - - if (isTransformNode(node) || isSceneLinkNode(node)) { - scene.removeTransformNode(node); - } - - if (isLight(node)) { - scene.removeLight(node); - } - - if (isCamera(node)) { - scene.removeCamera(node); - } -} +import { Scene, Node, AbstractMesh, Light, Debug } from "babylonjs"; +import { isAbstractMesh, isInstancedMesh, isCollisionInstancedMesh, isTransformNode, isLight, isCamera, isNode, isMesh } from "../../../tools/guards/nodes"; +import { isSceneLinkNode } from "../../../tools/guards/scene"; +import { isSound } from "../../../tools/guards/sound"; +import { registerUndoRedo } from "../../../tools/undoredo"; +import { updateAllLights } from "../../../tools/light/shadows"; +import { isParticleSystem } from "../../../tools/guards/particles"; +import { isAdvancedDynamicTexture } from "../../../tools/guards/texture"; +import { getLinkedAnimationGroupsFor } from "../../../tools/animation/group"; + +import { Editor } from "../../main"; +import { SKELETON_CONTAINER_TYPE } from "../assets-browser/items/skeleton-item"; + +type _RemoveNodeData = { + node: Node; + parent: Node | null; + + lights: Light[]; +}; + +/** + * Removes the currently selected nodes in the graph with undo/redo support. + * @param editor defines the reference to the editor used to get the selected nodes and refresh the graph. + */ +export function removeNodes(editor: Editor) { + const scene = editor.layout.preview.scene; + + const allData = editor.layout.graph + .getSelectedNodes() + .filter((n) => n.nodeData) + .map((n) => n.nodeData); + + const nodes = allData + .filter((n) => isNode(n)) + .map((node) => { + const attached = [node] + .concat(node.getDescendants(false, (n) => isNode(n))) + .map((descendant) => (isMesh(descendant) ? [descendant, ...descendant.instances] : [descendant])) + .flat() + .map((descendant) => { + return { + node: descendant, + parent: descendant.parent, + lights: scene.lights.filter((light) => { + return light + .getShadowGenerator() + ?.getShadowMap() + ?.renderList?.includes(descendant as AbstractMesh); + }), + } as _RemoveNodeData; + }); + + return attached; + }) + .flat(); + + const sounds = allData + .filter((d) => isSound(d)) + .map((sound) => ({ + sound, + soundtrack: scene.soundTracks?.[sound.soundTrackId + 1], + })); + + const particleSystems = allData.filter((d) => isParticleSystem(d)); + const advancedGuiTextures = allData.filter((d) => isAdvancedDynamicTexture(d)); + + const animationGroups = getLinkedAnimationGroupsFor([...particleSystems, ...advancedGuiTextures, ...sounds.map((d) => d.sound), ...nodes.map((d) => d.node)], scene); + + registerUndoRedo({ + executeRedo: true, + action: () => { + editor.layout.graph.refresh(); + editor.layout.preview.gizmo.setAttachedNode(null); + editor.layout.inspector.setEditedObject(editor.layout.preview.scene); + + updateAllLights(scene); + }, + undo: () => { + nodes.forEach((d) => { + restoreNodeData(d, scene); + }); + + sounds.forEach((d) => { + d.soundtrack?.addSound(d.sound); + }); + + particleSystems.forEach((particleSystem) => { + scene.addParticleSystem(particleSystem); + }); + + advancedGuiTextures.forEach((node) => { + scene.addTexture(node); + + const layer = scene.layers.find((layer) => layer.texture === node); + if (layer) { + layer.isEnabled = true; + } + }); + + animationGroups.forEach((targetedAnimations, animationGroup) => { + targetedAnimations.forEach((targetedAnimation) => { + animationGroup.addTargetedAnimation(targetedAnimation.animation, targetedAnimation.target); + }); + + if (!scene.animationGroups.includes(animationGroup)) { + scene.addAnimationGroup(animationGroup); + } + }); + }, + redo: () => { + nodes.forEach((d) => { + removeNodeData(d, scene); + }); + + sounds.forEach((d) => { + d.soundtrack?.removeSound(d.sound); + }); + + particleSystems.forEach((particleSystem) => { + scene.removeParticleSystem(particleSystem); + }); + + advancedGuiTextures.forEach((node) => { + scene.removeTexture(node); + + const layer = scene.layers.find((layer) => layer.texture === node); + if (layer) { + layer.isEnabled = false; + } + }); + + animationGroups.forEach((targetedAnimations, animationGroup) => { + targetedAnimations.forEach((targetedAnimation) => { + animationGroup.removeTargetedAnimation(targetedAnimation.animation); + }); + + if (!animationGroup.targetedAnimations.length) { + scene.removeAnimationGroup(animationGroup); + } else { + console.log(nodes.find((d) => d.node === animationGroup.targetedAnimations[0].target)); + } + }); + }, + }); +} + +function restoreNodeData(data: _RemoveNodeData, scene: Scene) { + const node = data.node; + + if (isTransformNode(node) && node.metadata?.type === SKELETON_CONTAINER_TYPE) { + const skeleton = node.metadata.skeleton; + const viewer = node.metadata.viewer; + + if (skeleton) { + scene.addSkeleton(skeleton); + } + + if (viewer && skeleton) { + const newViewer = new Debug.SkeletonViewer(skeleton, null, scene, false, 1, { + displayMode: Debug.SkeletonViewer.DISPLAY_SPHERE_AND_SPURS, + }); + newViewer.isEnabled = true; + + node.metadata.viewer = newViewer; + } + } + + if (isAbstractMesh(node)) { + if (isInstancedMesh(node) || isCollisionInstancedMesh(node)) { + node.sourceMesh.addInstance(node); + } + + scene.addMesh(node); + + data.lights.forEach((light) => { + light.getShadowGenerator()?.getShadowMap()?.renderList?.push(node); + }); + } + + if (isTransformNode(node) || isSceneLinkNode(node)) { + scene.addTransformNode(node); + } + + if (isLight(node)) { + scene.addLight(node); + } + + if (isCamera(node)) { + scene.addCamera(node); + } +} + +function removeNodeData(data: _RemoveNodeData, scene: Scene) { + const node = data.node; + + if (isAbstractMesh(node)) { + if (isInstancedMesh(node) || isCollisionInstancedMesh(node)) { + node.sourceMesh.removeInstance(node); + } + + scene.removeMesh(node); + + data.lights.forEach((light) => { + const renderList = light.getShadowGenerator()?.getShadowMap()?.renderList; + const index = renderList?.indexOf(node) ?? -1; + if (index !== -1) { + renderList?.splice(index, 1); + } + }); + } + + if (isTransformNode(node) || isSceneLinkNode(node)) { + scene.removeTransformNode(node); + } + + if (isLight(node)) { + scene.removeLight(node); + } + + if (isCamera(node)) { + scene.removeCamera(node); + } + + if (isTransformNode(node) && node.metadata?.type === SKELETON_CONTAINER_TYPE) { + const skeleton = node.metadata.skeleton; + const viewer = node.metadata.viewer; + + if (viewer) { + viewer.dispose(); + } + + if (skeleton) { + skeleton.dispose(); + } + } +} diff --git a/editor/src/editor/layout/preview.tsx b/editor/src/editor/layout/preview.tsx index c355a29ce..c96a54805 100644 --- a/editor/src/editor/layout/preview.tsx +++ b/editor/src/editor/layout/preview.tsx @@ -1099,6 +1099,8 @@ export class EditorPreview extends Component configureImportedNodeIds(bone)); + configureImportedSkeleton(mesh.skeleton); } if (mesh.morphTargetManager) { @@ -90,6 +88,10 @@ export async function loadImportedSceneFile(scene: Scene, absolutePath: string, } }); + result.skeletons?.forEach((skeleton) => { + configureImportedSkeleton(skeleton, absolutePath); + }); + result.lights.forEach((light) => configureImportedNodeIds(light)); result.transformNodes.forEach((transformNode) => configureImportedNodeIds(transformNode)); result.animationGroups.forEach((animationGroup) => (animationGroup.uniqueId = UniqueNumber.Get())); @@ -170,6 +172,21 @@ export function configureImportedTexture configureImportedNodeIds(bone)); +} + export async function configureEmbeddedTexture(texture: Texture, absolutePath: string): Promise { if (!projectConfiguration.path) { return; diff --git a/editor/src/tools/guards/nodes.ts b/editor/src/tools/guards/nodes.ts index 7d1571a3e..eaf2dbccd 100644 --- a/editor/src/tools/guards/nodes.ts +++ b/editor/src/tools/guards/nodes.ts @@ -13,6 +13,7 @@ import { ArcRotateCamera, SpotLight, HemisphericLight, + Skeleton, } from "babylonjs"; import { EditorCamera } from "../../editor/nodes/camera"; @@ -178,6 +179,14 @@ export function isHemisphericLight(object: any): object is HemisphericLight { return object.getClassName?.() === "HemisphericLight"; } +/** + * Returns wether or not the given object is a Skeleton. + * @param object defines the reference to the object to test its class name. + */ +export function isSkeleton(object: any): object is Skeleton { + return object.getClassName?.() === "Skeleton"; +} + /** * Returns wether or not the given object is a Light. * @param object defines the reference to the object to test its class name. @@ -200,5 +209,5 @@ export function isLight(object: any): object is Light { * @param object defines the reference to the object to test its class name. */ export function isNode(object: any): object is Node { - return isAbstractMesh(object) || isTransformNode(object) || isLight(object) || isCamera(object) || isSceneLinkNode(object); + return isAbstractMesh(object) || isTransformNode(object) || isLight(object) || isCamera(object) || isSceneLinkNode(object) || isSkeleton(object); }