Home Reference Source

src/renderer/index.js

import { mat4, vec4 } from 'gl-matrix';
import { library, version, getContext, setContext } from '../session';
import UniformBuffer from './helpers/ubo';
import { MAX_DIRECTIONAL } from '../constants';

let supported = false;
let child;

let lastProgram;

const viewMatrix = mat4.create();
const normalMatrix = mat4.create();
const modelViewMatrix = mat4.create();
const inversedModelViewMatrix = mat4.create();

const startTime = Date.now();

class Renderer {
    constructor(props = {}) {
        this.ratio = global.devicePixelRatio;

        this.fog = {
            start: 500,
            end: 1000,
            color: vec4.fromValues(0, 0, 0, 1),
            density: 0.002,
        };

        const canvas = props.canvas || this.createCanvas();

        const gl = canvas.getContext('webgl2', {
            antialias: false,
        });

        if (gl) {
            if (props.greeting !== false) {
                const lib = 'color:#666;font-size:x-small;font-weight:bold;';
                const parameters = 'color:#777;font-size:x-small';
                const values = 'color:#f33;font-size:x-small';
                const args = [
                    `%c${library}\n%cversion: %c${version} %crunning: %c${gl.getParameter(gl.VERSION)}`,
                    lib, parameters, values, parameters, values,
                ];

                console.log(...args);
            }

            setContext(gl);

            this.perScene = new UniformBuffer([
                ...mat4.create(), // projectionMatrix
                ...mat4.create(), // viewMatrix
                ...vec4.create(), // fog start, fog end, fog density, 0
                ...vec4.create(), // fog color
                ...vec4.create(), // iGlobalTime, 0, 0, 0
            ], 0);

            this.perModel = new UniformBuffer([
                ...mat4.create(),
                ...mat4.create(),
            ], 1);

            this.directional = new UniformBuffer(new Float32Array(MAX_DIRECTIONAL * 12), 2);

            this.frameBuffer = gl.createFramebuffer();
            gl.bindFramebuffer(gl.FRAMEBUFFER, this.frameBuffer);

            this.texture = gl.createTexture();
            gl.bindTexture(gl.TEXTURE_2D, this.texture);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 256, 256, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);

            this.renderBuffer = gl.createRenderbuffer();
            gl.bindRenderbuffer(gl.RENDERBUFFER, this.renderBuffer);
            gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, 256, 256);
            gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0); // eslint-disable-line
            gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, this.renderBuffer);  // eslint-disable-line

            gl.bindTexture(gl.TEXTURE_2D, null);
            gl.bindRenderbuffer(gl.RENDERBUFFER, null);
            gl.bindFramebuffer(gl.FRAMEBUFFER, null);

            supported = true;
        } else {
            alert('webgl2 not supported'); // eslint-disable-line
        }
    }

    createCanvas() {
        const canvas = document.createElement('canvas');
        canvas.style.display = 'block';
        canvas.style.boxSizing = 'border-box';
        canvas.style.width = global.innerWidth * global.devicePixelRatio;
        canvas.style.height = global.innerHeight * global.devicePixelRatio;
        document.body.appendChild(canvas);

        return canvas;
    }

    setSize(width, height) {
        const w = width * this.ratio;
        const h = height * this.ratio;

        const gl = getContext();
        gl.canvas.width = w;
        gl.canvas.height = h;
        gl.canvas.style.width = `${w / this.ratio}px`;
        gl.canvas.style.height = `${h / this.ratio}px`;
    }

    setRatio(ratio) {
        this.ratio = ratio;
    }

    renderObject(object) {
        if (object === undefined) {
            object = this;
        }

        for (let i = 0; i < object.children.length; i++) {
            this.renderObject(object.children[i]);
        }

        if (object.parent === null) {
            return;
        }

        // --------------------------
        const gl = getContext();
        child = object;

        if (!child.material) {
            // its just an empty container, we don't need to render it
            return;
        }

        // first time
        if (!child.material.program) {
            child.init();
            return;
        }

        // change program
        if (lastProgram !== child.material.program) {
            lastProgram = child.material.program;
            gl.useProgram(lastProgram);

            // change progam, so bind UBO
            const sceneLocation = gl.getUniformBlockIndex(lastProgram, 'perScene');
            const modelLocation = gl.getUniformBlockIndex(lastProgram, 'perModel');
            const directionalLocation = gl.getUniformBlockIndex(lastProgram, 'directional');

            gl.uniformBlockBinding(lastProgram, sceneLocation, this.perScene.boundLocation);
            gl.uniformBlockBinding(lastProgram, modelLocation, this.perModel.boundLocation);
            gl.uniformBlockBinding(lastProgram, directionalLocation, this.directional.boundLocation); // eslint-disable-line

            // https://jsfiddle.net/andrevenancio/m9qchtdb/14/
        }

        // model view matrix
        mat4.identity(modelViewMatrix);
        // OPTION I multiply view takes OrbitControls in consideration to normalMatrix
        // mat4.multiply(modelViewMatrix, viewMatrix, child.modelMatrix);

        // OPTION II dont use view
        mat4.copy(modelViewMatrix, child.modelMatrix);

        // inversed model view matrix
        mat4.invert(inversedModelViewMatrix, modelViewMatrix);
        mat4.transpose(inversedModelViewMatrix, inversedModelViewMatrix);

        // update matrices per model
        mat4.identity(normalMatrix);
        mat4.copy(normalMatrix, inversedModelViewMatrix);

        // render child
        child.bind();

        this.perModel.update([
            ...child.modelMatrix,
            ...normalMatrix,
        ]);

        child.update();
        child.unbind();
        child = null;
    }

    draw(scene, camera, clear = true) {
        if (supported) {
            const gl = getContext();
            gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

            if (clear) {
                gl.clearColor(0, 0, 0, 1);
                gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
            }

            gl.enable(gl.CULL_FACE);
            gl.enable(gl.DEPTH_TEST);

            camera.updateCameraMatrix(gl.canvas.width, gl.canvas.height);

            // common matrices
            mat4.identity(viewMatrix);
            mat4.lookAt(viewMatrix, camera.position.data, camera.target, camera.up);

            scene.traverse();

            // bind buffers
            this.perScene.bind();
            this.perModel.bind();
            this.directional.bind();

            this.perScene.update([
                ...camera.projectionMatrix,
                ...viewMatrix,
                ...[this.fog.start, this.fog.end, this.fog.density, 0],
                ...this.fog.color,
                ...[(Date.now() - startTime) / 1000, 0, 0, 0],
            ]);

            for (let i = 0; i < scene.directional.length; i++) {
                this.directional.update([
                    ...[...scene.directional[i].position.data, 0],
                    ...[...scene.directional[i].color, 0],
                    ...[scene.directional[i].intensity, 0, 0, 0],
                ], i * 12);
            }

            // TODO: sort opaque and transparent objects
            // temporary render until I sort the "sort" :p
            this.renderObject(scene);
        }
    }

    render(scene, camera) {
        this.draw(scene, camera);
    }

    rtt(scene, camera, width, height) {
        const gl = getContext();

        if (width !== this.rttwidth || height !== this.rttheight) {
            const w = width * this.ratio;
            const h = height * this.ratio;
            gl.bindTexture(gl.TEXTURE_2D, this.texture);
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
            gl.bindTexture(gl.TEXTURE_2D, null);

            // resize depth attachment
            gl.bindRenderbuffer(gl.RENDERBUFFER, this.renderBuffer);
            gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width * this.ratio, height * this.ratio); // eslint-disable-line
            gl.bindRenderbuffer(gl.RENDERBUFFER, null);

            this.rttwidth = width;
            this.rttheight = height;
        }

        gl.bindFramebuffer(gl.FRAMEBUFFER, this.frameBuffer);

        this.draw(scene, camera);

        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, this.texture);
        gl.generateMipmap(gl.TEXTURE_2D);

        gl.bindTexture(gl.TEXTURE_2D, null);
        gl.bindRenderbuffer(gl.RENDERBUFFER, null);
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);

        return this.texture;
    }
}

export default Renderer;