@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
356 lines • 19.6 kB
JavaScript
import { Scene } from "../scene.js";
import { Observable } from "../Misc/observable.js";
import { PointerInfo, PointerEventTypes } from "../Events/pointerEvents.js";
import { PickingInfo } from "../Collisions/pickingInfo.js";
import { EngineStore } from "../Engines/engineStore.js";
import { HemisphericLight } from "../Lights/hemisphericLight.js";
import { Vector3 } from "../Maths/math.vector.js";
import { Color3 } from "../Maths/math.color.js";
/**
* Renders a layer on top of an existing scene
*/
export class UtilityLayerRenderer {
/**
* Gets the camera that is used to render the utility layer (when not set, this will be the last active camera)
* @param getRigParentIfPossible if the current active camera is a rig camera, should its parent camera be returned
* @returns the camera that is used when rendering the utility layer
*/
getRenderCamera(getRigParentIfPossible) {
if (this._renderCamera) {
return this._renderCamera;
}
else {
let activeCam;
if (this.originalScene.activeCameras && this.originalScene.activeCameras.length > 1) {
activeCam = this.originalScene.activeCameras[this.originalScene.activeCameras.length - 1];
}
else {
activeCam = this.originalScene.activeCamera;
}
if (getRigParentIfPossible && activeCam && activeCam.isRigCamera) {
return activeCam.rigParent;
}
return activeCam;
}
}
/**
* Sets the camera that should be used when rendering the utility layer (If set to null the last active camera will be used)
* @param cam the camera that should be used when rendering the utility layer
*/
setRenderCamera(cam) {
this._renderCamera = cam;
}
/**
* @internal
* Light which used by gizmos to get light shading
*/
_getSharedGizmoLight() {
if (!this._sharedGizmoLight) {
this._sharedGizmoLight = new HemisphericLight("shared gizmo light", new Vector3(0, 1, 0), this.utilityLayerScene);
this._sharedGizmoLight.intensity = 2;
this._sharedGizmoLight.groundColor = Color3.Gray();
}
return this._sharedGizmoLight;
}
/**
* A shared utility layer that can be used to overlay objects into a scene (Depth map of the previous scene is cleared before drawing on top of it)
*/
static get DefaultUtilityLayer() {
if (UtilityLayerRenderer._DefaultUtilityLayer == null) {
return UtilityLayerRenderer._CreateDefaultUtilityLayerFromScene(EngineStore.LastCreatedScene);
}
return UtilityLayerRenderer._DefaultUtilityLayer;
}
/**
* Creates an utility layer, and set it as a default utility layer
* @param scene associated scene
* @internal
*/
static _CreateDefaultUtilityLayerFromScene(scene) {
UtilityLayerRenderer._DefaultUtilityLayer = new UtilityLayerRenderer(scene);
UtilityLayerRenderer._DefaultUtilityLayer.originalScene.onDisposeObservable.addOnce(() => {
UtilityLayerRenderer._DefaultUtilityLayer = null;
});
return UtilityLayerRenderer._DefaultUtilityLayer;
}
/**
* A shared utility layer that can be used to embed objects into a scene (Depth map of the previous scene is not cleared before drawing on top of it)
*/
static get DefaultKeepDepthUtilityLayer() {
if (UtilityLayerRenderer._DefaultKeepDepthUtilityLayer == null) {
UtilityLayerRenderer._DefaultKeepDepthUtilityLayer = new UtilityLayerRenderer(EngineStore.LastCreatedScene);
UtilityLayerRenderer._DefaultKeepDepthUtilityLayer.utilityLayerScene.autoClearDepthAndStencil = false;
UtilityLayerRenderer._DefaultKeepDepthUtilityLayer.originalScene.onDisposeObservable.addOnce(() => {
UtilityLayerRenderer._DefaultKeepDepthUtilityLayer = null;
});
}
return UtilityLayerRenderer._DefaultKeepDepthUtilityLayer;
}
/**
* Instantiates a UtilityLayerRenderer
* @param originalScene the original scene that will be rendered on top of
* @param handleEvents boolean indicating if the utility layer should handle events
* @param manualRender boolean indicating if the utility layer should render manually.
*/
constructor(
/** the original scene that will be rendered on top of */
originalScene, handleEvents = true, manualRender = false) {
this.originalScene = originalScene;
this.handleEvents = handleEvents;
this._pointerCaptures = {};
this._lastPointerEvents = {};
this._sharedGizmoLight = null;
this._renderCamera = null;
/**
* If the picking should be done on the utility layer prior to the actual scene (Default: true)
*/
this.pickUtilitySceneFirst = true;
/**
* If the utility layer should automatically be rendered on top of existing scene
*/
this.shouldRender = true;
/**
* If set to true, only pointer down onPointerObservable events will be blocked when picking is occluded by original scene
*/
this.onlyCheckPointerDownEvents = true;
/**
* If set to false, only pointerUp, pointerDown and pointerMove will be sent to the utilityLayerScene (false by default)
*/
this.processAllEvents = false;
/**
* Set to false to disable picking
*/
this.pickingEnabled = true;
/**
* Observable raised when the pointer moves from the utility layer scene to the main scene
*/
this.onPointerOutObservable = new Observable();
// Create scene which will be rendered in the foreground and remove it from being referenced by engine to avoid interfering with existing app
this.utilityLayerScene = new Scene(originalScene.getEngine(), { virtual: true });
this.utilityLayerScene.useRightHandedSystem = originalScene.useRightHandedSystem;
this.utilityLayerScene._allowPostProcessClearColor = false;
// Deactivate post processes
this.utilityLayerScene.postProcessesEnabled = false;
// Detach controls on utility scene, events will be fired by logic below to handle picking priority
this.utilityLayerScene.detachControl();
if (handleEvents) {
this._originalPointerObserver = originalScene.onPrePointerObservable.add((prePointerInfo) => {
if (!this.utilityLayerScene.activeCamera) {
return;
}
if (!this.pickingEnabled) {
return;
}
if (!this.processAllEvents) {
if (prePointerInfo.type !== PointerEventTypes.POINTERMOVE &&
prePointerInfo.type !== PointerEventTypes.POINTERUP &&
prePointerInfo.type !== PointerEventTypes.POINTERDOWN &&
prePointerInfo.type !== PointerEventTypes.POINTERDOUBLETAP) {
return;
}
}
this.utilityLayerScene.pointerX = originalScene.pointerX;
this.utilityLayerScene.pointerY = originalScene.pointerY;
const pointerEvent = prePointerInfo.event;
if (originalScene.isPointerCaptured(pointerEvent.pointerId)) {
this._pointerCaptures[pointerEvent.pointerId] = false;
return;
}
const getNearPickDataForScene = (scene) => {
let scenePick = null;
if (prePointerInfo.nearInteractionPickingInfo) {
if (prePointerInfo.nearInteractionPickingInfo.pickedMesh.getScene() == scene) {
scenePick = prePointerInfo.nearInteractionPickingInfo;
}
else {
scenePick = new PickingInfo();
}
}
else if (scene !== this.utilityLayerScene && prePointerInfo.originalPickingInfo) {
scenePick = prePointerInfo.originalPickingInfo;
}
else {
let previousActiveCamera = null;
// If a camera is set for rendering with this layer
// it will also be used for the ray computation
// To preserve back compat and because scene.pick always use activeCamera
// it's substituted temporarily and a new scenePick is forced.
// otherwise, the ray with previously active camera is always used.
// It's set back to previous activeCamera after operation.
if (this._renderCamera) {
previousActiveCamera = scene._activeCamera;
scene._activeCamera = this._renderCamera;
prePointerInfo.ray = null;
}
scenePick = prePointerInfo.ray ? scene.pickWithRay(prePointerInfo.ray) : scene.pick(originalScene.pointerX, originalScene.pointerY);
if (previousActiveCamera) {
scene._activeCamera = previousActiveCamera;
}
}
return scenePick;
};
const utilityScenePick = getNearPickDataForScene(this.utilityLayerScene);
if (!prePointerInfo.ray && utilityScenePick) {
prePointerInfo.ray = utilityScenePick.ray;
}
if (prePointerInfo.originalPickingInfo?.aimTransform && utilityScenePick) {
utilityScenePick.aimTransform = prePointerInfo.originalPickingInfo.aimTransform;
utilityScenePick.gripTransform = prePointerInfo.originalPickingInfo.gripTransform;
}
// always fire the prepointer observable
this.utilityLayerScene.onPrePointerObservable.notifyObservers(prePointerInfo);
// allow every non pointer down event to flow to the utility layer
if (this.onlyCheckPointerDownEvents && prePointerInfo.type != PointerEventTypes.POINTERDOWN) {
if (!prePointerInfo.skipOnPointerObservable) {
this.utilityLayerScene.onPointerObservable.notifyObservers(new PointerInfo(prePointerInfo.type, prePointerInfo.event, utilityScenePick), prePointerInfo.type);
}
if (prePointerInfo.type === PointerEventTypes.POINTERUP && this._pointerCaptures[pointerEvent.pointerId]) {
this._pointerCaptures[pointerEvent.pointerId] = false;
}
return;
}
if (this.utilityLayerScene.autoClearDepthAndStencil || this.pickUtilitySceneFirst) {
// If this layer is an overlay, check if this layer was hit and if so, skip pointer events for the main scene
if (utilityScenePick && utilityScenePick.hit) {
if (!prePointerInfo.skipOnPointerObservable) {
this.utilityLayerScene.onPointerObservable.notifyObservers(new PointerInfo(prePointerInfo.type, prePointerInfo.event, utilityScenePick), prePointerInfo.type);
}
prePointerInfo.skipOnPointerObservable = true;
}
}
else {
const originalScenePick = getNearPickDataForScene(originalScene);
const pointerEvent = prePointerInfo.event;
// If the layer can be occluded by the original scene, only fire pointer events to the first layer that hit they ray
if (originalScenePick && utilityScenePick) {
// No pick in utility scene
if (utilityScenePick.distance === 0 && originalScenePick.pickedMesh) {
if (this.mainSceneTrackerPredicate && this.mainSceneTrackerPredicate(originalScenePick.pickedMesh)) {
// We touched an utility mesh present in the main scene
this._notifyObservers(prePointerInfo, originalScenePick, pointerEvent);
prePointerInfo.skipOnPointerObservable = true;
}
else if (prePointerInfo.type === PointerEventTypes.POINTERDOWN) {
this._pointerCaptures[pointerEvent.pointerId] = true;
this._notifyObservers(prePointerInfo, originalScenePick, pointerEvent);
}
else if (prePointerInfo.type === PointerEventTypes.POINTERMOVE || prePointerInfo.type === PointerEventTypes.POINTERUP) {
if (this._lastPointerEvents[pointerEvent.pointerId]) {
// We need to send a last pointerup to the utilityLayerScene to make sure animations can complete
this.onPointerOutObservable.notifyObservers(pointerEvent.pointerId);
delete this._lastPointerEvents[pointerEvent.pointerId];
}
this._notifyObservers(prePointerInfo, originalScenePick, pointerEvent);
}
}
else if (!this._pointerCaptures[pointerEvent.pointerId] && (utilityScenePick.distance < originalScenePick.distance || originalScenePick.distance === 0)) {
// We pick something in utility scene or the pick in utility is closer than the one in main scene
this._notifyObservers(prePointerInfo, utilityScenePick, pointerEvent);
// If a previous utility layer set this, do not unset this
if (!prePointerInfo.skipOnPointerObservable) {
prePointerInfo.skipOnPointerObservable = utilityScenePick.distance > 0;
}
}
else if (!this._pointerCaptures[pointerEvent.pointerId] && utilityScenePick.distance >= originalScenePick.distance) {
// We have a pick in both scenes but main is closer than utility
// We touched an utility mesh present in the main scene
if (this.mainSceneTrackerPredicate && this.mainSceneTrackerPredicate(originalScenePick.pickedMesh)) {
this._notifyObservers(prePointerInfo, originalScenePick, pointerEvent);
prePointerInfo.skipOnPointerObservable = true;
}
else {
if (prePointerInfo.type === PointerEventTypes.POINTERMOVE || prePointerInfo.type === PointerEventTypes.POINTERUP) {
if (this._lastPointerEvents[pointerEvent.pointerId]) {
// We need to send a last pointerup to the utilityLayerScene to make sure animations can complete
this.onPointerOutObservable.notifyObservers(pointerEvent.pointerId);
delete this._lastPointerEvents[pointerEvent.pointerId];
}
}
this._notifyObservers(prePointerInfo, utilityScenePick, pointerEvent);
}
}
if (prePointerInfo.type === PointerEventTypes.POINTERUP && this._pointerCaptures[pointerEvent.pointerId]) {
this._pointerCaptures[pointerEvent.pointerId] = false;
}
}
}
});
// As a newly added utility layer will be rendered over the screen last, it's pointer events should be processed first
if (this._originalPointerObserver) {
originalScene.onPrePointerObservable.makeObserverTopPriority(this._originalPointerObserver);
}
}
// Render directly on top of existing scene without clearing
this.utilityLayerScene.autoClear = false;
if (!manualRender) {
this._afterRenderObserver = this.originalScene.onAfterRenderCameraObservable.add((camera) => {
// Only render when the render camera finishes rendering
if (this.shouldRender && camera == this.getRenderCamera()) {
this.render();
}
});
}
this._sceneDisposeObserver = this.originalScene.onDisposeObservable.add(() => {
this.dispose();
});
this._updateCamera();
}
_notifyObservers(prePointerInfo, pickInfo, pointerEvent) {
if (!prePointerInfo.skipOnPointerObservable) {
this.utilityLayerScene.onPointerObservable.notifyObservers(new PointerInfo(prePointerInfo.type, prePointerInfo.event, pickInfo), prePointerInfo.type);
this._lastPointerEvents[pointerEvent.pointerId] = true;
}
}
/**
* Renders the utility layers scene on top of the original scene
*/
render() {
this._updateCamera();
if (this.utilityLayerScene.activeCamera) {
// Set the camera's scene to utility layers scene
const oldScene = this.utilityLayerScene.activeCamera.getScene();
const camera = this.utilityLayerScene.activeCamera;
camera._scene = this.utilityLayerScene;
if (camera.leftCamera) {
camera.leftCamera._scene = this.utilityLayerScene;
}
if (camera.rightCamera) {
camera.rightCamera._scene = this.utilityLayerScene;
}
this.utilityLayerScene.render(false);
// Reset camera's scene back to original
camera._scene = oldScene;
if (camera.leftCamera) {
camera.leftCamera._scene = oldScene;
}
if (camera.rightCamera) {
camera.rightCamera._scene = oldScene;
}
}
}
/**
* Disposes of the renderer
*/
dispose() {
this.onPointerOutObservable.clear();
if (this._afterRenderObserver) {
this.originalScene.onAfterCameraRenderObservable.remove(this._afterRenderObserver);
}
if (this._sceneDisposeObserver) {
this.originalScene.onDisposeObservable.remove(this._sceneDisposeObserver);
}
if (this._originalPointerObserver) {
this.originalScene.onPrePointerObservable.remove(this._originalPointerObserver);
}
this.utilityLayerScene.dispose();
}
_updateCamera() {
this.utilityLayerScene.cameraToUseForPointers = this.getRenderCamera();
this.utilityLayerScene.activeCamera = this.getRenderCamera();
}
}
/** @internal */
UtilityLayerRenderer._DefaultUtilityLayer = null;
/** @internal */
UtilityLayerRenderer._DefaultKeepDepthUtilityLayer = null;
//# sourceMappingURL=utilityLayerRenderer.js.map