/**
* A class representing the WebGL renderer.
*
* @property {Object} opts An object containing options.
* @property {Lore.CameraBase} camera The camera associated with this renderer.
* @property {Lore.ControlsBase} controls The controls associated with this renderer.
*/
Lore.Renderer = class Renderer {
/**
* Creates an instance of Renderer.
* @param {String} targetId The id of a canvas element.
* @param {any} options The options.
*/
constructor(targetId, options) {
this.defaults = {
antialiasing: true,
verbose: false,
fpsElement: document.getElementById('fps'),
clearColor: Lore.Color.fromHex('#000000'),
clearDepth: 1.0,
radius: 500,
center: new Lore.Vector3f(),
enableDepthTest: true
}
this.opts = Lore.Utils.extend(true, this.defaults, options);
this.canvas = document.getElementById(targetId);
this.parent = this.canvas.parentElement;
this.fps = 0;
this.fpsCount = 0;
this.maxFps = 1000 / 30;
this.devicePixelRatio = this.getDevicePixelRatio();
this.camera = new Lore.OrthographicCamera(this.getWidth() / -2, this.getWidth() / 2, this.getHeight() / 2, this.getHeight() / -2);
// this.camera = new Lore.PerspectiveCamera(45.0, this.getWidth() / this.getHeight());
this.geometries = {};
this.ready = false;
this.gl = null;
this.render = function (camera, geometries) {};
this.effect = null;
this.lastTiming = performance.now();
this.disableContextMenu();
let that = this;
that.init();
// Attach the controls last
let center = options.center ? options.center : new Lore.Vector3f();
this.controls = new Lore.OrbitalControls(that, this.opts.radius || 500, center);
}
/**
* Initialize and start the renderer.
*/
init() {
let _this = this;
let settings = {
antialias: this.opts.antialiasing
};
this.gl = this.canvas.getContext('webgl', settings) ||
this.canvas.getContext('experimental-webgl', settings);
if (!this.gl) {
console.error('Could not initialize the WebGL context.');
return;
}
let g = this.gl;
if (this.opts.verbose) {
let hasAA = g.getContextAttributes().antialias;
let size = g.getParameter(g.SAMPLES);
console.info('Antialiasing: ' + hasAA + ' (' + size + 'x)');
let highp = g.getShaderPrecisionFormat(g.FRAGMENT_SHADER, g.HIGH_FLOAT);
let hasHighp = highp.precision != 0;
console.info('High precision support: ' + hasHighp);
}
// Blending
/*
g.blendFunc(g.SRC_ALPHA, g.ONE);
g.enable(g.BLEND);
g.disable(g.DEPTH_TEST);
*/
// Extensions
let oes = 'OES_standard_derivatives';
let extOes = g.getExtension(oes);
if (extOes === null) {
console.warn('Could not load extension: ' + oes + '.');
}
let wdb = 'WEBGL_draw_buffers';
let extWdb = g.getExtension(wdb);
if (extWdb === null) {
console.warn('Could not load extension: ' + wdb + '.');
}
let wdt = 'WEBGL_depth_texture';
let extWdt = g.getExtension(wdt);
if (extWdt === null) {
console.warn('Could not load extension: ' + wdt + '.');
}
this.setClearColor(this.opts.clearColor);
g.clearDepth(this.opts.clearDepth);
if (this.opts.enableDepthTest) {
g.enable(g.DEPTH_TEST);
g.depthFunc(g.LEQUAL);
if (this.opts.verbose) {
console.log('enable depth test');
}
}
/*
g.blendFunc(g.SRC_ALPHA, g.ONE_MINUS_SRC_ALPHA);
g.enable(g.BLEND);
*/
setTimeout(function () {
_this.updateViewport(0, 0, _this.getWidth(), _this.getHeight());
}, 1000);
// Also do it immediately, in case the timeout is not needed
this.updateViewport(0, 0, _this.getWidth(), _this.getHeight());
window.addEventListener('resize', function (event) {
let width = _this.getWidth();
let height = _this.getHeight();
_this.updateViewport(0, 0, width, height);
});
// Init effect(s)
this.effect = new Lore.Effect(this, 'fxaaEffect');
this.ready = true;
this.animate();
}
/**
* Disables the context menu on the canvas element.
*/
disableContextMenu() {
// Disable context menu on right click
this.canvas.addEventListener('contextmenu', function (e) {
if (e.button === 2) {
e.preventDefault();
return false;
}
});
}
/**
* Sets the clear color of this renderer.
*
* @param {Lore.Color} color The clear color.
*/
setClearColor(color) {
this.opts.clearColor = color;
let cc = this.opts.clearColor.components;
this.gl.clearColor(cc[0], cc[1], cc[2], cc[3]);
}
/**
* Get the actual width of the canvas.
*
* @returns {Number} The width of the canvas.
*/
getWidth() {
return this.canvas.offsetWidth;
}
/**
* Get the actual height of the canvas.
*
* @returns {Number} The height of the canvas.
*/
getHeight() {
return this.canvas.offsetHeight;
}
/**
* Update the viewport. Should be called when the canvas is resized.
*
* @param {Number} x The horizontal offset of the viewport.
* @param {Number} y The vertical offset of the viewport.
* @param {Number} width The width of the viewport.
* @param {Number} height The height of the viewport.
*/
updateViewport(x, y, width, height) {
// width *= this.devicePixelRatio;
// height *= this.devicePixelRatio;
this.canvas.width = width;
this.canvas.height = height;
this.gl.viewport(x, y, width, height);
this.camera.updateViewport(width, height);
this.camera.updateProjectionMatrix();
// Also reinit the buffers and textures for the effect(s)
this.effect = new Lore.Effect(this, 'fxaaEffect');
this.effect.shader.uniforms.resolution.setValue([width, height]);
}
/**
* The main rendering loop.
*/
animate() {
let that = this;
setTimeout(function () {
requestAnimationFrame(function () {
that.animate();
});
}, this.maxFps);
if (this.opts.fpsElement) {
let now = performance.now();
let delta = now - this.lastTiming;
this.lastTiming = now;
if (this.fpsCount < 10) {
this.fps += Math.round(1000.0 / delta);
this.fpsCount++;
} else {
this.opts.fpsElement.innerHTML = Math.round(this.fps / this.fpsCount);
this.fpsCount = 0;
this.fps = 0;
}
}
// this.effect.bind();
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
this.render(this.camera, this.geometries);
// this.effect.unbind();
this.camera.isProjectionMatrixStale = false;
this.camera.isViewMatrixStale = false;
}
/**
* Creates and adds a geometry to the scene graph.
*
* @param {String} name The name of the geometry.
* @param {String} shaderName The name of the shader used to render the geometry.
* @returns {Lore.Geometry} The created geometry.
*/
createGeometry(name, shaderName) {
let shader = Lore.getShader(shaderName);
shader.init(this.gl);
let geometry = new Lore.Geometry(name, this.gl, shader);
this.geometries[name] = geometry;
return geometry;
}
/**
* Set the maximum frames per second of this renderer.
*
* @param {Number} fps Maximum frames per second.
*/
setMaxFps(fps) {
this.maxFps = 1000 / fps;
}
/**
* Get the device pixel ratio.
*
* @returns {Number} The device pixel ratio.
*/
getDevicePixelRatio() {
return window.devicePixelRatio || 1;
}
}