import { ChangeDetectorRef, NgZone } from "@angular/core";
import {
ArcRotateCamera,
ArcRotateCameraPointersInput,
Camera,
Color3,
DefaultLoadingScreen,
Engine,
HardwareScalingOptimization,
HemisphericLight,
LensFlaresOptimization,
Light,
Matrix,
Mesh,
ParticlesOptimization,
PointLight,
PostProcessesOptimization,
Scene,
SceneOptimizer,
SceneOptimizerOptions,
ShadowsOptimization,
TextureOptimization,
Vector3
} from "@babylonjs/core";
import { ignoreElements, Subject } from "rxjs";
import { AppUtils } from "../app-utils";
import { CabriDataService } from "../services/cabri-data.service";
import { GlobalService } from "../services/global.service";
import { CustomLoadingScreen } from "./babylon-custom-loading";
import { ParticleAnimName } from "./babylonjs-confetti";
import { BabylonJs5Confetti } from "./babylonjs5-confetti";
import { EngineIntegration } from "./engine-integration";
export class BabylonIntegration extends EngineIntegration {
engine: Engine;
scene: Scene;
light: PointLight;
centerlight: Light;
oldInputElement: HTMLCanvasElement;
changeHSLStatus: boolean;
canvas: HTMLCanvasElement;
cleanup: { remove: string; handles: any[] }[];
sceneDisposeSubject: any;
optimizerOption: SceneOptimizerOptions;
optimizer: SceneOptimizer;
fpsStatus: boolean;
fps: number;
fpsInterval: NodeJS.Timeout;
loaderCamera: any;
constructor(
public globalService: GlobalService,
public dataService: CabriDataService,
public ngZone: NgZone,
public cd?: ChangeDetectorRef
) {
super(globalService);
}
startRender() {
if (this.engine && this.scene && (!this.engine["_activeRenderLoops"] || this.engine["_activeRenderLoops"].length === 0)) {
this.ngZone.runOutsideAngular(() => {
this.engine.runRenderLoop(() => this.scene.render());
});
}
}
stopRender() {
if (this.engine) {
this.ngZone.runOutsideAngular(() => {
this.engine.stopRenderLoop();
});
}
}
/**
* Binds the required events for a full experience.
*/
private bindEvents(): void {
//window.addEventListener("resize", this.engine.resize);
}
public createBabylonEngine(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.ngZone.runOutsideAngular(() => {
if (!this.dataService.engineBabylon) {
this.dataService.engineBabylon = this.engine = new Engine(canvas, false, {
disableWebGL2Support: false,
doNotHandleContextLost: true,
audioEngine: true
});
} else {
this.engine = this.dataService.engineBabylon;
if (this.engine.activeView && this.engine.inputElement) {
this.oldInputElement = this.engine.activeView.target;
this.engine.unRegisterView(this.oldInputElement);
}
this.engine.inputElement = canvas as unknown as HTMLElement;
this.engine.registerView(canvas);
}
});
this.changeHSL(true);
// this.resizeCanvas();
const gl = this.engine._gl;
this.cleanup = ["Buffer", "Framebuffer", "Renderbuffer", "Program", "Shader", "Texture"].map(suffix => {
const remove = "delete" + suffix;
const create = "create" + suffix;
const original = gl[create];
const handles = [];
gl[create] = function () {
const handle = original.apply(this, arguments);
handles.push(handle);
return handle;
};
return {
remove,
handles
};
});
this.globalService.webGl1 = this.engine.webGLVersion === 1;
this.globalService._lowPerformanceMode = this.engine.webGLVersion === 1 || this.globalService.lowPerformanceMode;
this.globalService.lowPerformanceModeSetStorageValue();
if (Engine && Engine.audioEngine) {
// set babylon volume to default (music & sounds handled separately):
Engine.audioEngine.setGlobalVolume(1);
}
// fix black shaders bug:
if (!this.globalService.isIos) {
this.engine.getCaps().highPrecisionShaderSupported = false;
}
this.scene = new Scene(this.engine);
this.sceneDisposeSubject = new Subject<void>();
this.scene.onDispose = () => {
this.sceneDisposeSubject.next();
this.sceneDisposeSubject.complete();
};
this.bindEvents();
this.changeBabylonLoader();
this.scene.onAfterRenderObservable.addOnce(() => {
// disable physics if any engine (Cannon in narrations)
this.disablePhysics();
// this.removeFxaaSSao();
if (!this.globalService.modeProd) {
this.showFPS(true);
}
if (this.globalService.lowPerformanceMode) {
if (!this.optimizer) {
this.createSceneOptimizer();
}
this.optimizer.reset();
this.optimizer.start();
}
});
}
changeHSL(value? : boolean) {
if (value === true) {
this.changeHSLStatus = true;
} else if (value === false) {
this.changeHSLStatus = false;
} else {
this.changeHSLStatus = !this.changeHSLStatus;
}
if (this.changeHSLStatus === true) {
if (!this.globalService.isDesktop && !this.globalService.lowPerformanceMode) {
this.scaleBabylonCanvasAndEngine();
} else {
if (this.globalService.isDesktop) {
if (window.innerHeight > 2000 || window.innerWidth > 3000) {
this.engine.setHardwareScalingLevel(2);
this.globalService.hSL = 2;
} else {
this.engine.setHardwareScalingLevel(1);
this.globalService.hSL = 1;
}
} else {
this.changeHSLStatus = false;
}
}
} else {
this.engine.setHardwareScalingLevel(1);
this.globalService.hSL = 1;
}
this.cd?.detectChanges();
}
scaleBabylonCanvasAndEngine() {
// SCALING CANVAS
this.canvas.style.aspectRatio = `${this.canvas.offsetWidth} / ${this.canvas.offsetHeight}`;
const scaleCanvas = window.devicePixelRatio === 2 ? 2 : 1;
const scaledCanvasWidth = this.canvas.offsetWidth * scaleCanvas;
const scaledCanvasHeight = this.canvas.offsetHeight * scaleCanvas;
const roundedscaledCanvasWidth = Math.round(scaledCanvasWidth);
const roundedscaledCanvasHeight = Math.round(scaledCanvasHeight);
this.canvas.width = roundedscaledCanvasWidth;
this.canvas.height = roundedscaledCanvasHeight;
// SCALING ENGINE
let scale; // Change to 1 on retina screens to see blurry canvas.
if (window.devicePixelRatio >= 1) {
scale = 1.6; // HDPI displays
} else {
scale = 1.2; // no-HDPI displays
}
this.globalService.hSL = Math.round((scale / window.devicePixelRatio) * 10000) / 10000;
// console.error("hSL = ", this.globalService.hSL);
this.engine?.setHardwareScalingLevel(this.globalService.hSL);
this.engine?.resize();
}
// dev menu:
/**
* show babylon engine FPS
*/
showFPS(value?: boolean) {
if (this.engine) {
if (value === true) {
this.fpsStatus = true;
} else if (value === false) {
this.fpsStatus = false;
} else {
this.fpsStatus = !this.fpsStatus;
}
if (this.fpsInterval) {
clearInterval(this.fpsInterval);
this.fpsInterval = null;
this.fps = null;
}
if (this.fpsStatus === true) {
this.fpsInterval = setInterval(() => {
this.fps = Math.floor(this.engine.getFps());
this.cd?.detectChanges();
}, 1000);
} else {
if (this.fpsInterval) {
clearInterval(this.fpsInterval);
this.fpsInterval = null;
this.fps = null;
this.cd?.detectChanges();
}
}
}
}
cleanWebGl(gl = null) {
if (!gl && this.engine) {
gl = this.engine._gl;
}
if (gl) {
this.cleanup.forEach(kind => {
// console.log("call", kind);
for (let i = 0; i < kind.handles.length; i++) {
// console.log("call", kind.remove, kind.handles[i]);
gl[kind.remove].call(gl, kind.handles[i]);
}
});
}
}
public changeBabylonLoader() {
const loadingScreen = new CustomDefaultLoadingScreen(this.canvas,null,"#88B627");
// replace the default loading screen
this.engine.loadingScreen = loadingScreen;
}
disablePhysics() {
if (this.scene) {
this.scene.disablePhysicsEngine();
this.scene.physicsEnabled = false;
}
}
createSceneOptimizer() {
// this.optimizerOption = SceneOptimizerOptions.LowDegradationAllowed(60);
this.optimizerOption = this.customOptimizerSet();
this.optimizer = new SceneOptimizer(this.scene, this.optimizerOption, false, false);
this.optimizer.onNewOptimizationAppliedObservable.add(optim => {
// console.log("Optimizer :" + optim.getDescription());
});
}
customOptimizerSet(): SceneOptimizerOptions {
const result = new SceneOptimizerOptions(25, 2000);
let priority = 0;
result.optimizations.push(new PostProcessesOptimization(priority));
result.optimizations.push(new ParticlesOptimization(priority));
// Next priority
priority++;
result.optimizations.push(new ShadowsOptimization(priority));
result.optimizations.push(new LensFlaresOptimization(priority));
// Next priority
priority++;
result.optimizations.push(new TextureOptimization(priority, 256));
// Next priority
priority++;
// result.optimizations.push(new RenderTargetsOptimization(priority));
// Next priority
priority++;
result.optimizations.push(new HardwareScalingOptimization(priority, 4));
return result;
}
/**
* Confetti animation
* @param duration animation duration
*/
public async launchConfetti(duration = 1000,position?: Vector3) {
return new Promise<void>(resolve => {
const particlesRightAnswer = new BabylonJs5Confetti(this, 1000, duration, ParticleAnimName.explosion,position);
particlesRightAnswer.runParticles().then(() => {
resolve();
});
});
}
initScene(){
this.createCamera();
this.createLight();
}
createCamera() {
//outside angular zone camera declaration attach control by defaut an so declare event listener
this.ngZone.runOutsideAngular(() => {
this.camera = new ArcRotateCamera("Camera", -Math.PI / 2, Math.PI / 2, 10, Vector3.Zero(), this.scene);
(this.camera as ArcRotateCamera).setTarget(Vector3.Zero());
this.camera.attachControl(this.canvas, true);
});
this.cameraControl();
}
cameraControl() {
(this.camera as ArcRotateCamera).inputs.attached.mousewheel.detachControl();
//remove pinch zone interaction
(this.camera.inputs.attached.pointers as ArcRotateCameraPointersInput).pinchZoom = false;
(this.camera.inputs.attached.pointers as ArcRotateCameraPointersInput).multiTouchPanAndZoom = false;
(this.camera.inputs.attached.pointers as ArcRotateCameraPointersInput).multiTouchPanning = false;
//specify use button to only left button to remove right mouse action
(this.camera.inputs.attached.pointers as ArcRotateCameraPointersInput).buttons = [0];
}
createLight() {
this.light = new PointLight("bulb", new Vector3(0, 40, 0), this.scene);
this.light.intensity = 9000;
this.centerlight = new HemisphericLight("centerbulb", new Vector3(0, 0, 0), this.scene);
this.centerlight.intensity = 1;
(this.centerlight as HemisphericLight).groundColor = new Color3(1,1,1);
}
}
class CustomDefaultLoadingScreen extends DefaultLoadingScreen{
constructor(renderingCanvas: HTMLCanvasElement, loadingText?: string, loadingDivBackgroundColor?: string){
DefaultLoadingScreen.DefaultLogoUrl = "./assets/kidaia/logo/logoKidaiaWhiteV2.png";
super(renderingCanvas,loadingText,loadingDivBackgroundColor);
}
}