js/Mobilizing/renderer/3D/RendererThree.js
import * as THREE from "three";
import Component from "../../core/Component";
import * as _DOM from "../../core/util/Dom";
import * as debug from "../../core/util/Debug";
import Color from "./three/types/Color";
import Scene from "./three/scene/Scene";
//Used for singletonize this class instance (avoid mutilple rendrer)
let instance = null;
export default class RendererThree extends Component {
/**
* Renderer3D is a Three.js based simple renderer.
*
* @example
* //to do
* @param {Object} params Parameters object, given by the constructor.
* @param {Canvas} params.canvas the canvas to draw to
* @param {Boolean} params.alpha alpha channel in the webgl canvas
* @param {Boolean} params.antialias global antialiasing
* @param {Boolean} params.preserveDrawingBuffer
* @param {String} params.powerPreference "high-performance", "low-power" or "default"
*/
constructor({
canvas = undefined,
alpha = false,
antialias = false,
preserveDrawingBuffer = false,
powerPreference = "default",
} = {}) {
//if an instance already exists return it
if (instance) {
console.warn("an instance of RendererThree already exists!");
return instance;
}
super(...arguments);
instance = this;
console.log("three revision", THREE.REVISION);
this.canvas = canvas;
this.alpha = alpha;
this.antialias = antialias;
this.preserveDrawingBuffer = preserveDrawingBuffer;
this.powerPreference = powerPreference;
this.isFullscreen = false;
//build a canvas if none given as a param
console.log("build a canvas if none given as a param");
if (!this.canvas) {
this.canvas = document.createElement("canvas");
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
this.canvas.id = "Mobilizing_three_canvas";
this.canvas.style.position = "absolute";
this.canvas.style.left = 0;
this.canvas.style.top = 0;
//this.canvas.style.zIndex = 1;
this.isFullscreen = true;
document.body.appendChild(this.canvas);
//removes the css loader if any
const cssLoader = document.getElementById("loaderContainer");
if(cssLoader){
cssLoader.remove();
}
}
else {
if (this.canvas.width === window.innerWidth && this.canvas.height && window.innerHeight) {
this.isFullscreen = true;
}
}
//make three.js webgl renderer
console.log("make three.js webgl renderer");
this.renderer = new THREE.WebGLRenderer({
"canvas": this.canvas,
"alpha": this.alpha,
"antialias": this.antialias,
"preserveDrawingBuffer": this.preserveDrawingBuffer,
"powerPreference": this.powerPreference
});
//take care of display pixels density
console.log("take care of display pixels density");
if (this.renderer.devicePixelRatio === undefined) {
this.renderer.devicePixelRatio = window.devicePixelRatio;
}
this.renderer.setPixelRatio(window.devicePixelRatio ? window.devicePixelRatio : 1); //force retina
this.renderer.setSize(this.canvas.width / window.devicePixelRatio, this.canvas.height / window.devicePixelRatio);
this.canvas.style.width = `${this.canvas.width / window.devicePixelRatio}px`;
this.canvas.style.height = `${this.canvas.height / window.devicePixelRatio}px`;
//scene creation
console.log("scene creation");
this.scenes = {};
this.cameras = [];
this.fog = undefined;
this.setCurrentScene("default");
this.objects = [];
//window management
window.addEventListener("orientationchange", (event) => this.onWindowResize(event), false);
window.addEventListener("resize", (event) => this.onWindowResize(event), false);
// deal with the page getting resized or scrolled
window.addEventListener("scroll", (event) => this.updateCanvasPosition(event), false);
this.canvasPosition = this.getCanvasPosition();
}
/**
* Avoid Infinite Logs coming from browser shader bad compilation
*/
killShaderInfoLog() {
//kills the log error on shaders
this.renderer.context.getShaderInfoLog = function () {
return "";
};
}
getInfo(){
return this.renderer.info;
}
update() {
this.render();
}
/**
* Activates the rendering of projected shadows
* @param {Boolean} state
*/
setEnableShadowMap(state) {
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.renderer.shadowMap.enabled = state;
}
/**
* Tells the context to use this scene (defined by a string) or create it and switch to it.
* @param {String} name
*/
setCurrentScene(name) {
this.scene = this.scenes[name];
if (this.scene === undefined) {
this.scene = new Scene();
this.scene.name = name;
this.scenes[name] = this.scene;
console.log("current scene is : ", this.scene);
}
}
/**
* @return {String} the current scene's name
*/
getCurrentScene(){
return this.scene.name;
}
/**
* Adds on object to the current scene
* @param {Object} object A Mesh or Light to add the scene
*/
addToCurrentScene(object) {
this.objects.push(object);
object.context = this.context;
//object.setEventEmitterTrigger(this.context, "objectCreated");
//console.log(object);
this.scene.transform.addChild(object.transform);
}
/**
* Remove from the current scene
* @param {Object} object the object to remove
*/
removeFromCurrentScene(object) {
this.scene.transform.removeChild(object.transform);
this.objects.splice(this.objects.indexOf(object), 1);
}
/**
* erases this renderer contents : remove all cameras and scene objects
*/
erase() {
debug.log("scene :", this.scene);
this.scene.transform.children.forEach((child) => {
console.log("removing ", child);
this.scene.transform.removeChild(child.transform);
});
this.scene.getNativeObject().children = [];
this.cameras = [];
}
/**
* clears this renderer buffers
*/
clear() {
this.renderer.clear();
}
/**
* change the clearcolor of this renderer color buffers
*/
setClearColor(color) {
this.renderer.setClearColor(color);
}
/**
* Adds a camera to the current context
* @param {Camera} cam the camera to add
*/
addCamera(cam) {
this.cameras.push(cam);
}
/**
* Removes the camera from the current context
* @param {Camera} cam
*/
removeCamera(cam) {
this.cameras.splice(this.cameras.indexOf(cam), 1);
}
/**
* Defines the type and color of the fog to be used for rendering the scene
* @param {String} type one of "linear", "exp"
* @param {Color} color the fog color
*/
setFog(type, color) {
if (!color) {
color = Color.black.clone();
}
switch (type) {
case "linear":
this.fog = new THREE.Fog(color.getHex());
this.fog.type = "linear";
break;
case "exp":
this.fog = new THREE.FogExp2(color.getHex());
this.fog.type = "exp";
break;
default:
this.fog = new THREE.Fog(color.getHex());
this.fog.type = "linear";
break;
}
this.scene.getNativeObject().fog = this.fog;
}
/**
* Defines the near distance of the linear fog. Default is 1.
* @param {Number} near
*/
setFogNear(near) {
if (this.fog.type === "linear") {
this.fog.near = near;
}
}
/**
* Defines the far distance of the linear fog. Default is 1000.
* @param {Number} far
*/
setFogFar(far) {
if (this.fog.type === "linear") {
this.fog.far = far;
}
}
/**
* Defines the density of the exponential fog. Default is 0.00025.
* @param {Number} density
*/
setFogDensity(val) {
if (this.fog.type === "exp") {
this.fog.density = val;
}
}
/**
* Defines the fog color
* @param {Color} color
*/
setFogColor(color) {
this.fog.color = color;
}
/**
* Returns the current canvas
* @return {Object} the canvas
*/
getCanvas() {
return this.canvas;
}
/**
* Returns the current canvas'size as {width, height}
* @return {Object} the size of the canvas as {width, height}
*/
getCanvasSize() {
const size = {
width: this.canvas.width / window.devicePixelRatio,
height: this.canvas.height / window.devicePixelRatio
};
return size;
}
/**
* Returns the current canvas'width
* @return {Number} the width of the canvas
*/
getCanvasWidth() {
const size = this.getCanvasSize();
return size.width;
}
/**
* Returns the current canvas'height
* @return {Number} the height of the canvas
*/
getCanvasHeight() {
const size = this.getCanvasSize();
return size.height;
}
/**
* Returns the current canvas'width divided by 2
* @return {Number} the half width of the canvas
*/
getCanvasHalfWidth() {
return this.getCanvasWidth() / 2;
}
/**
* Returns the current canvas'height divided by 2
* @return {Number} the half height of the canvas
*/
getCanvasHalfHeight() {
return this.getCanvasHeight() / 2;
}
/**
* Get the canvas position as {x, y}
* @return {Object} the position of the canvas as {x, y}
*/
getCanvasPosition() {
return _DOM.getElementPosition(this.canvas);
}
/**
* Update the canvas position in screen
*/
updateCanvasPosition() {
this.canvasPosition = this.getCanvasPosition();
}
onWindowResize() {
if (this.isFullscreen) {
this.cameras.forEach((cam) => {
if (cam.type === "perspective") {
const w = this.canvas.width / this.renderer.devicePixelRatio;
const h = this.canvas.height / this.renderer.devicePixelRatio;
cam.setAspect((w * cam.viewport.width) / (h * cam.viewport.height));
this.renderer.setSize(this.canvas.width / window.devicePixelRatio, this.canvas.height / window.devicePixelRatio);
//avoid shift of coordinates when setToPixel() is used on the camera
if (cam.isToPixel) {
cam.setToPixel();
}
}
else if (cam.type === "ortho") {
cam.setOrthoPlanes(window.innerWidth / -2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / -2);
}
});
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
}
/**
* Saves the current frame to an image file. Note that the preserveFrameBuffer must be "true" on renderer construction
* @param {String} name the image file name
* @param {String} format the format of the image (jpg, png, gif)
*/
saveImageAs(name, format) {
let formatArg = format;
if (!formatArg) {
formatArg = "jpeg";
}
try {
const mimeType = `image/${formatArg}`;
const downloadMime = "image/octet-stream";
let imgData = this.renderer.domElement.toDataURL(mimeType);
imgData = imgData.replace(mimeType, downloadMime);
const link = document.createElement("a");
if (typeof link.download === "string") {
document.body.appendChild(link); //Firefox requires the link to be in the body
link.download = `${name}.${formatArg}`;
link.href = imgData;
link.click();
document.body.removeChild(link); //remove the link when done
}
else {
console.error("link not working");
}
}
catch (e) {
console.error(e);
}
}
/**
* Renders all the camera in the context
*/
render() {
for (let i = 0; i < this.cameras.length; ++i) {
const cam = this.cameras[i];
if (cam.transform.getVisible() && (cam.dirty || cam.autoRender)) {
const w = this.canvas.width / this.renderer.devicePixelRatio;
const h = this.canvas.height / this.renderer.devicePixelRatio;
if (cam.type !== "cube" && !cam.aspect) {
cam.setAspect((w * cam.viewport.width) / (h * cam.viewport.height));
}
const viewport_y = h * cam.viewport.y;
const viewport_x = w * cam.viewport.x;
const viewport_w = w * cam.viewport.width;
const viewport_h = h * cam.viewport.height;
this.renderer.setViewport(viewport_x, viewport_y, viewport_w, viewport_h);
this.renderer.setScissor(viewport_x, viewport_y, viewport_w, viewport_h);
this.renderer.setScissorTest(true);
if (cam.getClearColor()) {
this.renderer.setClearColor(cam.clearColor, cam.clearColorAplha);
}
this.renderer.autoClear = cam.autoClear;
this.renderer.autoClearColor = cam.autoClearColor;
this.renderer.autoClearDepth = cam.autoClearDepth;
this.renderer.autoClearStencil = cam.autoClearStencil;
Object.entries(this.scenes).forEach(([ks, scene]) => {
if (this.logLevel === 1) {
console.log("render cam/scene ", cam, ks);
}
//si la scène a le même nom que le layer de la caméra
for (let j = 0; j < cam.layers.length; j++) {
if (cam.layers[j] === ks) {
if (cam.renderTexture !== undefined) {
this.renderer.setRenderTarget(cam.renderTexture.renderTarget);
this.renderer.clear();
this.renderer.render(scene.getNativeObject(), cam.getNativeObject());
this.renderer.setRenderTarget(null);
}
else if (cam.type === "cube") {
// ??
}
else {
this.renderer.setRenderTarget(null);
this.renderer.render(scene.getNativeObject(), cam.getNativeObject());
this.renderer.autoClear = false;
}
cam.dirty = false;
}
}
});
}
}
}
}