Przeglądaj źródła

Camera Gizmo with Frustum display (#8688)

* display gizmo mesh and camera frustum

* more dynamic frustum update

* what's new

* gizmo manager + inspector update for lights & camera gizmo

* updates work better in inspector for cameraGizmo

* record movement for cameragizmo

* doc comments

* triggering new build
Cedric Guillemet 5 lat temu
rodzic
commit
e8e08c529f

+ 1 - 0
dist/preview release/what's new.md

@@ -20,6 +20,7 @@
 - Added support for querystrings on KTX file URLs ([abogartz](https://github.com/abogartz/Babylon.js)
 - Refactored React refs from old string API to React.createRef() API ([belfortk](https://github.com/belfortk))
 - Scale on one axis for `BoundingBoxGizmo` ([cedricguillemet](https://github.com/cedricguillemet))
+- Camera gizmo ([cedricguillemet](https://github.com/cedricguillemet))
 - Node support (Transform, Bone) for gizmos ([cedricguillemet](https://github.com/cedricguillemet))
 - Simplified code contributions by fully automating the dev setup with gitpod ([nisarhassan12](https://github.com/nisarhassan12))
 - Add a `CascadedShadowMap.IsSupported` method and log an error instead of throwing an exception when CSM is not supported ([Popov72](https://github.com/Popov72))

+ 21 - 0
inspector/src/components/globalState.ts

@@ -6,7 +6,9 @@ import { Observable, Observer } from "babylonjs/Misc/observable";
 import { ISceneLoaderPlugin, ISceneLoaderPluginAsync } from "babylonjs/Loading/sceneLoader";
 import { Scene } from "babylonjs/scene";
 import { Light } from "babylonjs/Lights/light";
+import { Camera } from "babylonjs/Cameras/camera";
 import { LightGizmo } from "babylonjs/Gizmos/lightGizmo";
+import { CameraGizmo } from "babylonjs/Gizmos/cameraGizmo";
 import { PropertyChangedEvent } from "./propertyChangedEvent";
 import { ReplayRecorder } from './replayRecorder';
 import { DataStorage } from 'babylonjs/Misc/dataStorage';
@@ -130,4 +132,23 @@ export class GlobalState {
             light.reservedDataStore.lightGizmo = null;
         }
     }
+    // Camera gizmos
+    public cameraGizmos: Array<CameraGizmo> = [];
+    public enableCameraGizmo(camera: Camera, enable = true) {
+        if (enable) {
+            if (!camera.reservedDataStore) {
+                camera.reservedDataStore = {}
+            }
+            if (!camera.reservedDataStore.cameraGizmo) {
+                camera.reservedDataStore.cameraGizmo = new CameraGizmo();
+                this.cameraGizmos.push(camera.reservedDataStore.cameraGizmo)
+                camera.reservedDataStore.cameraGizmo.camera = camera;
+                camera.reservedDataStore.cameraGizmo.material.reservedDataStore = {hidden: true};
+            }
+        } else if (camera.reservedDataStore && camera.reservedDataStore.cameraGizmo) {
+            this.cameraGizmos.splice(this.cameraGizmos.indexOf(camera.reservedDataStore.cameraGizmo), 1);
+            camera.reservedDataStore.cameraGizmo.dispose();
+            camera.reservedDataStore.cameraGizmo = null;
+        }
+    }
 }

+ 24 - 5
inspector/src/components/sceneExplorer/entities/cameraTreeItemComponent.tsx

@@ -5,18 +5,20 @@ import { Camera } from "babylonjs/Cameras/camera";
 import { Scene } from "babylonjs/scene";
 
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faVideo, faCamera } from '@fortawesome/free-solid-svg-icons';
+import { faVideo, faCamera, faEye } from '@fortawesome/free-solid-svg-icons';
 import { TreeItemLabelComponent } from "../treeItemLabelComponent";
 import { ExtensionsComponent } from "../extensionsComponent";
 import * as React from "react";
+import { GlobalState } from "../../globalState";
 
 interface ICameraTreeItemComponentProps {
     camera: Camera,
     extensibilityGroups?: IExplorerExtensibilityGroup[],
-    onClick: () => void
+    onClick: () => void,
+    globalState: GlobalState
 }
 
-export class CameraTreeItemComponent extends React.Component<ICameraTreeItemComponentProps, { isActive: boolean }> {
+export class CameraTreeItemComponent extends React.Component<ICameraTreeItemComponentProps, { isActive: boolean, isGizmoEnabled:boolean }> {
     private _onBeforeRenderObserver: Nullable<Observer<Scene>>;
 
     constructor(props: ICameraTreeItemComponentProps) {
@@ -25,7 +27,7 @@ export class CameraTreeItemComponent extends React.Component<ICameraTreeItemComp
         const camera = this.props.camera;
         const scene = camera.getScene();
 
-        this.state = { isActive: scene.activeCamera === camera };
+        this.state = { isActive: scene.activeCamera === camera, isGizmoEnabled: (camera.reservedDataStore && camera.reservedDataStore.cameraGizmo) };
     }
 
     setActive(): void {
@@ -65,10 +67,24 @@ export class CameraTreeItemComponent extends React.Component<ICameraTreeItemComp
         }
     }
 
+    toggleGizmo(): void {
+        const camera = this.props.camera;
+        if(camera.reservedDataStore && camera.reservedDataStore.cameraGizmo){
+            if (camera.getScene().reservedDataStore && camera.getScene().reservedDataStore.gizmoManager) {
+                camera.getScene().reservedDataStore.gizmoManager.attachToMesh(null);
+            }
+            this.props.globalState.enableCameraGizmo(camera, false);
+            this.setState({ isGizmoEnabled: false });
+        }else{
+            this.props.globalState.enableCameraGizmo(camera, true);
+            this.setState({ isGizmoEnabled: true });
+        }
+    }
+
     render() {
         const isActiveElement = this.state.isActive ? <FontAwesomeIcon icon={faVideo} /> : <FontAwesomeIcon icon={faVideo} className="isNotActive" />;
         const scene = this.props.camera.getScene()!;
-
+        const isGizmoEnabled = (this.state.isGizmoEnabled || (this.props.camera && this.props.camera.reservedDataStore && this.props.camera.reservedDataStore.cameraGizmo)) ? <FontAwesomeIcon icon={faEye} /> : <FontAwesomeIcon icon={faEye} className="isNotActive" />;
         return (
             <div className="cameraTools">
                 <TreeItemLabelComponent label={this.props.camera.name} onClick={() => this.props.onClick()} icon={faCamera} color="green" />
@@ -78,6 +94,9 @@ export class CameraTreeItemComponent extends React.Component<ICameraTreeItemComp
                         {isActiveElement}
                     </div>
                 }
+                <div className="enableGizmo icon" onClick={() => this.toggleGizmo()} title="Turn on/off the camera's gizmo">
+                    {isGizmoEnabled}
+                </div>
                 <ExtensionsComponent target={this.props.camera} extensibilityGroups={this.props.extensibilityGroups} />
             </div>
         )

+ 78 - 23
inspector/src/components/sceneExplorer/entities/sceneTreeItemComponent.tsx

@@ -14,6 +14,7 @@ import { GlobalState } from "../../globalState";
 import { UtilityLayerRenderer } from "babylonjs/Rendering/utilityLayerRenderer";
 import { PropertyChangedEvent } from '../../../components/propertyChangedEvent';
 import { LightGizmo } from 'babylonjs/Gizmos/lightGizmo';
+import { CameraGizmo } from 'babylonjs/Gizmos/cameraGizmo';
 import { TmpVectors, Vector3 } from 'babylonjs/Maths/math';
 
 interface ISceneTreeItemComponentProps {
@@ -89,11 +90,17 @@ export class SceneTreeItemComponent extends React.Component<ISceneTreeItemCompon
                         this.props.globalState.enableLightGizmo(this._selectedEntity, true);
                         this.forceUpdate();
                     }
-                    manager.attachToMesh(this._selectedEntity.reservedDataStore.lightGizmo.attachedMesh);
+                    manager.attachToNode(this._selectedEntity.reservedDataStore.lightGizmo.attachedNode);
+                } else if (className.indexOf("Camera") !== -1) {
+                    if (!this._selectedEntity.reservedDataStore || !this._selectedEntity.reservedDataStore.cameraGizmo) {
+                        this.props.globalState.enableCameraGizmo(this._selectedEntity, true);
+                        this.forceUpdate();
+                    }
+                    manager.attachToNode(this._selectedEntity.reservedDataStore.cameraGizmo.attachedNode);
                 }else if(className.indexOf("Bone") !== -1){
                     manager.attachToMesh((this._selectedEntity._linkedTransformNode)?this._selectedEntity._linkedTransformNode:this._selectedEntity);
                 } else {
-                    manager.attachToMesh(null);
+                    manager.attachToNode(null);
                 }
             }
         });
@@ -160,7 +167,23 @@ export class SceneTreeItemComponent extends React.Component<ISceneTreeItemCompon
                     var gizmoScene = this.props.globalState.lightGizmos[0].gizmoLayer.utilityLayerScene;
                     let pickInfo = gizmoScene.pick(pickPosition.x, pickPosition.y, (m: any) => {
                         for (var g of (this.props.globalState.lightGizmos as any)) {
-                            if (g.attachedMesh == m) {
+                            if (g.attachedNode == m) {
+                                return true;
+                            }
+                        }
+                        return false;
+                    });
+                    if (pickInfo && pickInfo.hit && this.props.onSelectionChangedObservable) {
+                        this.props.onSelectionChangedObservable.notifyObservers(pickInfo.pickedMesh);
+                        return;
+                    }
+                }
+                // Pick camera gizmos
+                if (this.props.globalState.cameraGizmos.length > 0) {
+                    var gizmoScene = this.props.globalState.cameraGizmos[0].gizmoLayer.utilityLayerScene;
+                    let pickInfo = gizmoScene.pick(pickPosition.x, pickPosition.y, (m: any) => {
+                        for (var g of (this.props.globalState.cameraGizmos as any)) {
+                            if (g.attachedNode == m) {
                                 return true;
                             }
                         }
@@ -209,7 +232,7 @@ export class SceneTreeItemComponent extends React.Component<ISceneTreeItemCompon
                     }
                     for (var gizmo of this.props.globalState.lightGizmos) {
                         if (gizmo._rootMesh == node) {
-                            manager.attachToMesh(gizmo.attachedMesh);
+                            manager.attachToNode(gizmo.attachedNode);
                         }
                     }
                 }
@@ -232,16 +255,28 @@ export class SceneTreeItemComponent extends React.Component<ISceneTreeItemCompon
                     if (!this._posDragEnd) {
                         // Record movement for generating replay code
                         this._posDragEnd = manager.gizmos.positionGizmo!.onDragEndObservable.add(() => {
-                            if (manager.gizmos.positionGizmo && manager.gizmos.positionGizmo.attachedMesh) {
-                                var lightGizmo: Nullable<LightGizmo> = manager.gizmos.positionGizmo.attachedMesh.reservedDataStore ? manager.gizmos.positionGizmo.attachedMesh.reservedDataStore.lightGizmo : null;
-                                var obj: any = (lightGizmo && lightGizmo.light) ? lightGizmo.light : manager.gizmos.positionGizmo.attachedMesh;
+                            if (manager.gizmos.positionGizmo && manager.gizmos.positionGizmo.attachedNode) {
+                                var lightGizmo: Nullable<LightGizmo> = manager.gizmos.positionGizmo.attachedNode.reservedDataStore ? manager.gizmos.positionGizmo.attachedNode.reservedDataStore.lightGizmo : null;
+                                var objLight: any = (lightGizmo && lightGizmo.light) ? lightGizmo.light : manager.gizmos.positionGizmo.attachedNode;
 
-                                if (obj.position) {
+                                if (objLight.position) {
                                     var e = new PropertyChangedEvent();
-                                    e.object = obj
+                                    e.object = objLight
                                     e.property = "position"
-                                    e.value = obj.position;
+                                    e.value = objLight.position;
                                     this.props.globalState.onPropertyChangedObservable.notifyObservers(e)
+                                } else {
+                                    var cameraGizmo: Nullable<CameraGizmo> = manager.gizmos.positionGizmo.attachedNode.reservedDataStore ? manager.gizmos.positionGizmo.attachedNode.reservedDataStore.cameraGizmo : null;
+                                    var objCamera: any = (cameraGizmo && cameraGizmo.camera) ? cameraGizmo.camera : manager.gizmos.positionGizmo.attachedNode;
+    
+                                    if (objCamera.position) {
+                                        var e = new PropertyChangedEvent();
+                                        e.object = objCamera
+                                        e.property = "position"
+                                        e.value = objCamera.position;
+                                        this.props.globalState.onPropertyChangedObservable.notifyObservers(e)
+                                    }
+
                                 }
                             }
                         })
@@ -253,27 +288,41 @@ export class SceneTreeItemComponent extends React.Component<ISceneTreeItemCompon
                     if (!this._rotateDragEnd) {
                         // Record movement for generating replay code
                         this._rotateDragEnd = manager.gizmos.rotationGizmo!.onDragEndObservable.add(() => {
-                            if (manager.gizmos.rotationGizmo && manager.gizmos.rotationGizmo.attachedMesh) {
-                                var lightGizmo: Nullable<LightGizmo> = manager.gizmos.rotationGizmo.attachedMesh.reservedDataStore ? manager.gizmos.rotationGizmo.attachedMesh.reservedDataStore.lightGizmo : null;
-                                var obj: any = (lightGizmo && lightGizmo.light) ? lightGizmo.light : manager.gizmos.rotationGizmo.attachedMesh;
+                            if (manager.gizmos.rotationGizmo && manager.gizmos.rotationGizmo.attachedNode) {
+                                var lightGizmo: Nullable<LightGizmo> = manager.gizmos.rotationGizmo.attachedNode.reservedDataStore ? manager.gizmos.rotationGizmo.attachedNode.reservedDataStore.lightGizmo : null;
+                                var objLight: any = (lightGizmo && lightGizmo.light) ? lightGizmo.light : manager.gizmos.rotationGizmo.attachedNode;
+                                var cameraGizmo: Nullable<CameraGizmo> = manager.gizmos.rotationGizmo.attachedNode.reservedDataStore ? manager.gizmos.rotationGizmo.attachedNode.reservedDataStore.cameraGizmo : null;
+                                var objCamera: any = (cameraGizmo && cameraGizmo.camera) ? cameraGizmo.camera : manager.gizmos.rotationGizmo.attachedNode;
 
-                                if (obj.rotationQuaternion) {
+                                if (objLight.rotationQuaternion) {
                                     var e = new PropertyChangedEvent();
-                                    e.object = obj;
+                                    e.object = objLight;
                                     e.property = "rotationQuaternion";
-                                    e.value = obj.rotationQuaternion;
+                                    e.value = objLight.rotationQuaternion;
                                     this.props.globalState.onPropertyChangedObservable.notifyObservers(e);
-                                } else if (obj.rotation) {
+                                } else if (objLight.rotation) {
                                     var e = new PropertyChangedEvent();
-                                    e.object = obj;
+                                    e.object = objLight;
                                     e.property = "rotation";
-                                    e.value = obj.rotation;
+                                    e.value = objLight.rotation;
                                     this.props.globalState.onPropertyChangedObservable.notifyObservers(e);
-                                } else if (obj.direction) {
+                                } else if (objLight.direction) {
                                     var e = new PropertyChangedEvent();
-                                    e.object = obj;
+                                    e.object = objLight;
                                     e.property = "direction";
-                                    e.value = obj.direction;
+                                    e.value = objLight.direction;
+                                    this.props.globalState.onPropertyChangedObservable.notifyObservers(e);
+                                } else if (objCamera.rotationQuaternion) {
+                                    var e = new PropertyChangedEvent();
+                                    e.object = objCamera;
+                                    e.property = "rotationQuaternion";
+                                    e.value = objCamera.rotationQuaternion;
+                                    this.props.globalState.onPropertyChangedObservable.notifyObservers(e);
+                                } else if (objCamera.rotation) {
+                                    var e = new PropertyChangedEvent();
+                                    e.object = objCamera;
+                                    e.property = "rotation";
+                                    e.value = objCamera.rotation;
                                     this.props.globalState.onPropertyChangedObservable.notifyObservers(e);
                                 }
                             }
@@ -320,7 +369,13 @@ export class SceneTreeItemComponent extends React.Component<ISceneTreeItemCompon
                         this.props.globalState.enableLightGizmo(this._selectedEntity, true);
                         this.forceUpdate();
                     }
-                    manager.attachToMesh(this._selectedEntity.reservedDataStore.lightGizmo.attachedMesh);
+                    manager.attachToNode(this._selectedEntity.reservedDataStore.lightGizmo.attachedNode);
+                } else if (className.indexOf("Camera") !== -1) {
+                    if (!this._selectedEntity.reservedDataStore || !this._selectedEntity.reservedDataStore.cameraGizmo) {
+                        this.props.globalState.enableCameraGizmo(this._selectedEntity, true);
+                        this.forceUpdate();
+                    }
+                    manager.attachToNode(this._selectedEntity.reservedDataStore.cameraGizmo.attachedNode);
                 } else if(className.indexOf("Bone") !== -1){
                     manager.attachToMesh((this._selectedEntity._linkedTransformNode)?this._selectedEntity._linkedTransformNode:this._selectedEntity);
                 }

+ 1 - 1
inspector/src/components/sceneExplorer/treeItemSpecializedComponent.tsx

@@ -96,7 +96,7 @@ export class TreeItemSpecializedComponent extends React.Component<ITreeItemSpeci
             }
 
             if (className.indexOf("Camera") !== -1) {
-                return (<CameraTreeItemComponent extensibilityGroups={this.props.extensibilityGroups} camera={entity as Camera} onClick={() => this.onClick()} />);
+                return (<CameraTreeItemComponent globalState={this.props.globalState} extensibilityGroups={this.props.extensibilityGroups} camera={entity as Camera} onClick={() => this.onClick()} />);
             }
 
             if (className.indexOf("Light", className.length - 5) !== -1) {

+ 5 - 0
inspector/src/inspector.ts

@@ -492,6 +492,11 @@ export class Inspector {
                 this._GlobalState.enableLightGizmo(g.light, false);
             }
         });
+        this._GlobalState.cameraGizmos.forEach((g) => {
+            if (g.camera) {
+                this._GlobalState.enableCameraGizmo(g.camera, false);
+            }
+        });
         if (this._Scene && this._Scene.reservedDataStore && this._Scene.reservedDataStore.gizmoManager) {
             this._Scene.reservedDataStore.gizmoManager.dispose();
             this._Scene.reservedDataStore.gizmoManager = null;

+ 176 - 0
src/Gizmos/cameraGizmo.ts

@@ -0,0 +1,176 @@
+import { Nullable } from "../types";
+import { Vector3 } from "../Maths/math.vector";
+import { Color3 } from '../Maths/math.color';
+import { Mesh } from "../Meshes/mesh";
+import { Gizmo } from "./gizmo";
+import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer";
+import { StandardMaterial } from '../Materials/standardMaterial';
+import { Scene } from '../scene';
+import { Camera } from '../Cameras/camera';
+import { BoxBuilder } from "../Meshes/Builders/boxBuilder";
+import { CylinderBuilder } from '../Meshes/Builders/cylinderBuilder';
+import { Matrix } from '../Maths/math';
+import { LinesBuilder } from "../Meshes/Builders/linesBuilder";
+
+/**
+ * Gizmo that enables viewing a camera
+ */
+export class CameraGizmo extends Gizmo {
+    private _cameraMesh: Mesh;
+    private _cameraLinesMesh: Mesh;
+    private _material: StandardMaterial;
+
+    /**
+     * Creates a CameraGizmo
+     * @param gizmoLayer The utility layer the gizmo will be added to
+     */
+    constructor(gizmoLayer?: UtilityLayerRenderer) {
+        super(gizmoLayer);
+
+        this._material = new StandardMaterial("cameraGizmoMaterial", this.gizmoLayer.utilityLayerScene);
+        this._material.diffuseColor = new Color3(0.5, 0.5, 0.5);
+        this._material.specularColor = new Color3(0.1, 0.1, 0.1);
+    }
+    private _camera: Nullable<Camera> = null;
+
+    /** Gets or sets a boolean indicating if frustum lines must be rendered (true by default)) */
+    public get displayFrustum() {
+        return this._cameraLinesMesh.isEnabled();
+    }
+    public set displayFrustum(value) {
+        this._cameraLinesMesh.setEnabled(value);
+    }
+
+    /**
+     * The camera that the gizmo is attached to
+     */
+    public set camera(camera: Nullable<Camera>) {
+        this._camera = camera;
+        this.attachedNode = camera;
+        if (camera) {
+            // Create the mesh for the given camera
+            if (this._cameraMesh) {
+                this._cameraMesh.dispose();
+            }
+            if (this._cameraLinesMesh) {
+                this._cameraLinesMesh.dispose();
+            }
+            this._cameraMesh = CameraGizmo._CreateCameraMesh(this.gizmoLayer.utilityLayerScene);
+            this._cameraLinesMesh = CameraGizmo._CreateCameraFrustum(this.gizmoLayer.utilityLayerScene);
+
+            this._cameraMesh.getChildMeshes(false).forEach((m) => {
+                m.material = this._material;
+            });
+            this._cameraMesh.parent = this._rootMesh;
+
+            this._cameraLinesMesh.parent = this._rootMesh;
+
+            if (!this.attachedNode!.reservedDataStore) {
+                this.attachedNode!.reservedDataStore = {};
+            }
+            this.attachedNode!.reservedDataStore.cameraGizmo = this;
+
+            // Add lighting to the camera gizmo
+            var gizmoLight = this.gizmoLayer._getSharedGizmoLight();
+            gizmoLight.includedOnlyMeshes = gizmoLight.includedOnlyMeshes.concat(this._cameraMesh.getChildMeshes(false));
+
+            this._update();
+        }
+    }
+
+    public get camera() {
+        return this._camera;
+    }
+
+    /**
+     * Gets the material used to render the camera gizmo
+     */
+    public get material() {
+        return this._material;
+    }
+    /**
+     * @hidden
+     * Updates the gizmo to match the attached mesh's position/rotation
+     */
+
+    protected _update() {
+        super._update();
+        if (!this._camera) {
+            return;
+        }
+
+        // frustum matrix
+        this._camera.getProjectionMatrix().invertToRef(this._invProjection);
+        this._cameraLinesMesh.setPivotMatrix(this._invProjection, false);
+
+        this._cameraLinesMesh.scaling.x = 1 / this._rootMesh.scaling.x;
+        this._cameraLinesMesh.scaling.y = 1 / this._rootMesh.scaling.y;
+        this._cameraLinesMesh.scaling.z = 1 / this._rootMesh.scaling.z;
+    }
+
+    // Static helper methods
+    private static _Scale = 0.05;
+    private _invProjection = new Matrix();
+    /**
+     * Disposes of the camera gizmo
+     */
+    public dispose() {
+        this._material.dispose();
+        super.dispose();
+    }
+
+    private static _CreateCameraMesh(scene: Scene) {
+        var root = new Mesh("rootCameraGizmo", scene);
+
+        var mesh = new Mesh(root.name, scene);
+        mesh.parent = root;
+
+        var box = BoxBuilder.CreateBox(root.name, {width: 1.0, height: 0.8, depth: 0.5 }, scene);
+        box.parent = mesh;
+
+        var cyl1 = CylinderBuilder.CreateCylinder(root.name, {height: 0.5, diameterTop: 0.8, diameterBottom: 0.8}, scene);
+        cyl1.parent = mesh;
+        cyl1.position.y = 0.3;
+        cyl1.position.x = -0.6;
+        cyl1.rotation.x = Math.PI * 0.5;
+
+        var cyl2 = CylinderBuilder.CreateCylinder(root.name, {height: 0.5, diameterTop: 0.6, diameterBottom: 0.6}, scene);
+        cyl2.parent = mesh;
+        cyl2.position.y = 0.5;
+        cyl2.position.x = 0.4;
+        cyl2.rotation.x = Math.PI * 0.5;
+
+        var cyl3 = CylinderBuilder.CreateCylinder(root.name, {height: 0.5, diameterTop: 0.5, diameterBottom: 0.5}, scene);
+        cyl3.parent = mesh;
+        cyl3.position.y = 0.0;
+        cyl3.position.x = 0.6;
+        cyl3.rotation.z = Math.PI * 0.5;
+
+        root.scaling.scaleInPlace(CameraGizmo._Scale);
+        root.rotation.y = -Math.PI * 0.5;
+        mesh.position.x = -0.9;
+
+        return root;
+    }
+
+    private static _CreateCameraFrustum(scene: Scene) {
+        var root = new Mesh("rootCameraGizmo", scene);
+        var mesh = new Mesh(root.name, scene);
+        mesh.parent = root;
+
+        for (var y = 0; y < 4; y += 2)
+        {
+            for (var x = 0; x < 4; x += 2)
+            {
+                var line = LinesBuilder.CreateLines("lines", { points: [new Vector3(-1 + x, -1 + y, -1), new Vector3(-1 + x, -1 + y, 1)] }, scene);
+                line.parent = mesh;
+                var line = LinesBuilder.CreateLines("lines", { points: [new Vector3(-1, -1 + x, -1 + y), new Vector3(1, -1 + x, -1 + y)] }, scene);
+                line.parent = mesh;
+                var line = LinesBuilder.CreateLines("lines", { points: [new Vector3(-1 + x, -1, -1 + y), new Vector3(-1 + x,  1, -1 + y)] }, scene);
+                line.parent = mesh;
+            }
+        }
+
+        return root;
+    }
+}

+ 72 - 10
src/Gizmos/gizmoManager.ts

@@ -15,20 +15,27 @@ import { ScaleGizmo } from "./scaleGizmo";
 import { BoundingBoxGizmo } from "./boundingBoxGizmo";
 
 /**
- * Helps setup gizmo's in the scene to rotate/scale/position meshes
+ * Helps setup gizmo's in the scene to rotate/scale/position nodes
  */
 export class GizmoManager implements IDisposable {
     /**
      * Gizmo's created by the gizmo manager, gizmo will be null until gizmo has been enabled for the first time
      */
     public gizmos: { positionGizmo: Nullable<PositionGizmo>, rotationGizmo: Nullable<RotationGizmo>, scaleGizmo: Nullable<ScaleGizmo>, boundingBoxGizmo: Nullable<BoundingBoxGizmo> };
+
     /** When true, the gizmo will be detached from the current object when a pointer down occurs with an empty picked mesh */
     public clearGizmoOnEmptyPointerEvent = false;
+
     /** Fires an event when the manager is attached to a mesh */
     public onAttachedToMeshObservable = new Observable<Nullable<AbstractMesh>>();
+
+    /** Fires an event when the manager is attached to a node */
+    public onAttachedToNodeObservable = new Observable<Nullable<Node>>();
+
     private _gizmosEnabled = { positionGizmo: false, rotationGizmo: false, scaleGizmo: false, boundingBoxGizmo: false };
     private _pointerObserver: Nullable<Observer<PointerInfo>> = null;
     private _attachedMesh: Nullable<AbstractMesh> = null;
+    private _attachedNode: Nullable<Node> = null;
     private _boundingBoxColor = Color3.FromHexString("#0984e3");
     private _defaultUtilityLayer: UtilityLayerRenderer;
     private _defaultKeepDepthUtilityLayer: UtilityLayerRenderer;
@@ -42,7 +49,11 @@ export class GizmoManager implements IDisposable {
      */
     public attachableMeshes: Nullable<Array<AbstractMesh>> = null;
     /**
-     * If pointer events should perform attaching/detaching a gizmo, if false this can be done manually via attachToMesh. (Default: true)
+     * Array of nodes which will have the gizmo attached when a pointer selected them. If null, all nodes are attachable. (Default: null)
+     */
+    public attachableNodes: Nullable<Array<Node>> = null;
+    /**
+     * If pointer events should perform attaching/detaching a gizmo, if false this can be done manually via attachToMesh/attachToNode. (Default: true)
      */
     public usePointerToAttachGizmos = true;
 
@@ -124,7 +135,11 @@ export class GizmoManager implements IDisposable {
         if (this._attachedMesh) {
             this._attachedMesh.removeBehavior(this.boundingBoxDragBehavior);
         }
+        if (this._attachedNode) {
+            this._attachedNode.removeBehavior(this.boundingBoxDragBehavior);
+        }
         this._attachedMesh = mesh;
+        this._attachedNode = null;
         for (var key in this.gizmos) {
             var gizmo = <Nullable<Gizmo>>((<any>this.gizmos)[key]);
             if (gizmo && (<any>this._gizmosEnabled)[key]) {
@@ -138,6 +153,31 @@ export class GizmoManager implements IDisposable {
     }
 
     /**
+     * Attaches a set of gizmos to the specified node
+     * @param node The node the gizmo's should be attached to
+     */
+    public attachToNode(node: Nullable<Node>) {
+        if (this._attachedMesh) {
+            this._attachedMesh.removeBehavior(this.boundingBoxDragBehavior);
+        }
+        if (this._attachedNode) {
+            this._attachedNode.removeBehavior(this.boundingBoxDragBehavior);
+        }
+        this._attachedMesh = null;
+        this._attachedNode = node;
+        for (var key in this.gizmos) {
+            var gizmo = <Nullable<Gizmo>>((<any>this.gizmos)[key]);
+            if (gizmo && (<any>this._gizmosEnabled)[key]) {
+                gizmo.attachedNode = node;
+            }
+        }
+        if (this.boundingBoxGizmoEnabled && this._attachedNode) {
+            this._attachedNode.addBehavior(this.boundingBoxDragBehavior);
+        }
+        this.onAttachedToNodeObservable.notifyObservers(node);
+    }
+
+    /**
      * If the position gizmo is enabled
      */
     public set positionGizmoEnabled(value: boolean) {
@@ -145,9 +185,13 @@ export class GizmoManager implements IDisposable {
             if (!this.gizmos.positionGizmo) {
                 this.gizmos.positionGizmo = new PositionGizmo(this._defaultUtilityLayer, this._thickness);
             }
-            this.gizmos.positionGizmo.attachedMesh = this._attachedMesh;
+            if (this._attachedNode) {
+                this.gizmos.positionGizmo.attachedNode = this._attachedNode;
+            } else {
+                this.gizmos.positionGizmo.attachedMesh = this._attachedMesh;
+            }
         } else if (this.gizmos.positionGizmo) {
-            this.gizmos.positionGizmo.attachedMesh = null;
+            this.gizmos.positionGizmo.attachedNode = null;
         }
         this._gizmosEnabled.positionGizmo = value;
     }
@@ -162,9 +206,13 @@ export class GizmoManager implements IDisposable {
             if (!this.gizmos.rotationGizmo) {
                 this.gizmos.rotationGizmo = new RotationGizmo(this._defaultUtilityLayer, 32, false, this._thickness);
             }
-            this.gizmos.rotationGizmo.attachedMesh = this._attachedMesh;
+            if (this._attachedNode) {
+                this.gizmos.rotationGizmo.attachedNode = this._attachedNode;
+            } else {
+                this.gizmos.rotationGizmo.attachedMesh = this._attachedMesh;
+            }
         } else if (this.gizmos.rotationGizmo) {
-            this.gizmos.rotationGizmo.attachedMesh = null;
+            this.gizmos.rotationGizmo.attachedNode = null;
         }
         this._gizmosEnabled.rotationGizmo = value;
     }
@@ -177,9 +225,13 @@ export class GizmoManager implements IDisposable {
     public set scaleGizmoEnabled(value: boolean) {
         if (value) {
             this.gizmos.scaleGizmo = this.gizmos.scaleGizmo || new ScaleGizmo(this._defaultUtilityLayer, this._thickness);
-            this.gizmos.scaleGizmo.attachedMesh = this._attachedMesh;
+            if (this._attachedNode) {
+                this.gizmos.scaleGizmo.attachedNode = this._attachedNode;
+            } else {
+                this.gizmos.scaleGizmo.attachedMesh = this._attachedMesh;
+            }
         } else if (this.gizmos.scaleGizmo) {
-            this.gizmos.scaleGizmo.attachedMesh = null;
+            this.gizmos.scaleGizmo.attachedNode = null;
         }
         this._gizmosEnabled.scaleGizmo = value;
     }
@@ -192,16 +244,26 @@ export class GizmoManager implements IDisposable {
     public set boundingBoxGizmoEnabled(value: boolean) {
         if (value) {
             this.gizmos.boundingBoxGizmo = this.gizmos.boundingBoxGizmo || new BoundingBoxGizmo(this._boundingBoxColor, this._defaultKeepDepthUtilityLayer);
-            this.gizmos.boundingBoxGizmo.attachedMesh = this._attachedMesh;
+            if (this._attachedMesh) {
+                this.gizmos.boundingBoxGizmo.attachedMesh = this._attachedMesh;
+            } else {
+                this.gizmos.boundingBoxGizmo.attachedNode = this._attachedNode;
+            }
+
             if (this._attachedMesh) {
                 this._attachedMesh.removeBehavior(this.boundingBoxDragBehavior);
                 this._attachedMesh.addBehavior(this.boundingBoxDragBehavior);
+            } else if (this._attachedNode) {
+                this._attachedNode.removeBehavior(this.boundingBoxDragBehavior);
+                this._attachedNode.addBehavior(this.boundingBoxDragBehavior);
             }
         } else if (this.gizmos.boundingBoxGizmo) {
             if (this._attachedMesh) {
                 this._attachedMesh.removeBehavior(this.boundingBoxDragBehavior);
+            } else if (this._attachedNode) {
+                this._attachedNode.removeBehavior(this.boundingBoxDragBehavior);
             }
-            this.gizmos.boundingBoxGizmo.attachedMesh = null;
+            this.gizmos.boundingBoxGizmo.attachedNode = null;
         }
         this._gizmosEnabled.boundingBoxGizmo = value;
     }

+ 1 - 0
src/Gizmos/index.ts

@@ -8,4 +8,5 @@ export * from "./positionGizmo";
 export * from "./rotationGizmo";
 export * from "./scaleGizmo";
 export * from "./lightGizmo";
+export * from "./cameraGizmo";
 export * from "./planeDragGizmo";