Repository

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 "";
        };
    }

    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.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") {
                                cam._camera.updateCubeMap(this.renderer, scene.getNativeObject());
                            }
                            else {
                                this.renderer.setRenderTarget(null);
                                this.renderer.render(scene.getNativeObject(), cam.getNativeObject());
                                this.renderer.autoClear = false;
                            }
                            cam.dirty = false;
                        }
                    }
                });
            }
        }
    }
}