Home Reference Source

src/systems/RenderSystem.js

import System from './System';
import Events from '../utils/Events';
import { vec2, vec3, vec4, mat2, mat3, mat4 } from '../utils/gl-matrix';
import { isPOT, getPOT, getMipmapScale } from '../utils';
import funcParser from '../utils/funcParser';

let rtwUidGenerator = 0;

const versions = [
  // TODO: change order to provide fallback to previous versions if requested is not supported.
  [1, 'webgl'],
  [2, 'webgl2'],
];
const vertices = new Float32Array([
  -1, -1, 0, 0,
  1, -1, 1, 0,
  1, 1, 1, 1,
  -1, 1, 0, 1
]);
const indices = new Uint16Array([
  0, 1, 2,
  2, 3, 0
]);
const extensions = {
  instanced_arrays: [
    1, 'ANGLE_instanced_arrays', null,
    2, null, null
  ],
  blend_minmax: [
    1, 'EXT_blend_minmax', null,
    2, null, null
  ],
  color_buffer_float: [
    1, 'WEBGL_color_buffer_float', null,
    2, 'EXT_color_buffer_float', null
  ],
  color_buffer_half_float: [
    1, 'WEBGL_color_buffer_half_float', null,
    2, 'EXT_color_buffer_half_float', null
  ],
  disjoint_timer_query: [
    1, 'EXT_disjoint_timer_query', null,
    2, 'EXT_disjoint_timer_query_webgl2', null
  ],
  frag_depth: [
    1, 'EXT_frag_depth', null,
    2, null, null
  ],
  sRGB: [
    1, 'EXT_sRGB', null,
    2, null, null
  ],
  shader_texture_lod: [
    1, 'EXT_shader_texture_lod', null,
    2, null, null
  ],
  texture_filter_anisotropic: [
    1, 'EXT_texture_filter_anisotropic', null,
    2, 'EXT_texture_filter_anisotropic', null
  ],
  element_index_uint: [
    1, 'OES_element_index_uint', null,
    2, null, null
  ],
  standard_derivatives: [
    1, 'OES_standard_derivatives', null,
    2, null, null
  ],
  texture_float: [
    1, 'OES_texture_float', null,
    2, null, null
  ],
  texture_float_linear: [
    1, 'OES_texture_float_linear', null,
    2, 'OES_texture_float_linear', null
  ],
  texture_half_float: [
    1, 'OES_texture_half_float', null,
    2, null, null
  ],
  texture_half_float_linear: [
    1, 'OES_texture_half_float_linear', null,
    2, 'OES_texture_half_float_linear', null
  ],
  vertex_array_object: [
    1, 'OES_vertex_array_object', null,
    2, null, null
  ],
  compressed_texture_astc: [
    1, 'WEBGL_compressed_texture_astc', null,
    2, 'WEBGL_compressed_texture_astc', null
  ],
  compressed_texture_atc: [
    1, 'WEBGL_compressed_texture_atc', null,
    2, 'WEBGL_compressed_texture_atc', null
  ],
  compressed_texture_etc: [
    1, 'WEBGL_compressed_texture_etc', null,
    2, 'WEBGL_compressed_texture_etc', null
  ],
  compressed_texture_etc1: [
    1, 'WEBGL_compressed_texture_etc1', null,
    2, 'WEBGL_compressed_texture_etc1', null
  ],
  compressed_texture_pvrtc: [
    1, 'WEBGL_compressed_texture_pvrtc', null,
    2, 'WEBGL_compressed_texture_pvrtc', null
  ],
  compressed_texture_s3tc: [
    1, 'WEBGL_compressed_texture_s3tc', null,
    2, 'WEBGL_compressed_texture_s3tc', null
  ],
  compressed_texture_s3tc_srgb: [
    1, 'WEBGL_compressed_texture_s3tc_srgb', null,
    2, 'WEBGL_compressed_texture_s3tc_srgb', null
  ],
  debug_renderer_info: [
    1, 'WEBGL_debug_renderer_info', null,
    2, 'WEBGL_debug_renderer_info', null
  ],
  debug_shaders: [
    1, 'WEBGL_debug_shaders', null,
    2, 'WEBGL_debug_shaders', null
  ],
  depth_texture: [
    1, 'WEBGL_depth_texture', null,
    2, null, null
  ],
  draw_buffers: [
    1, 'WEBGL_draw_buffers', [
      'drawBuffersWEBGL', 'drawBuffers'
    ],
    2, null, null
  ],
  lose_context: [
    1, 'WEBGL_lose_context', null,
    2, 'WEBGL_lose_context', null
  ]
};
const functions = new Map();

function getExtensionByVersion(meta, context, version) {
  for (var i = 0, c = meta.length; i < c; i += 3) {
    if (meta[i] === version) {
      const name = meta[i + 1];
      if (!name) {
        return context;
      } else {
        const ext = context.getExtension(name);
        if (!ext) {
          return null;
        }

        const mappings = meta[i + 2];
        if (!!mappings) {
          for (var j = 0, n = mappings.length; j < n; j += 2) {
            context[mappings[j + 1]] = ext[mappings[j]].bind(ext);
          }
        }
        return ext;
      }
    }
  }
  return null;
}

function makeApplierFunction(code) {
  code = funcParser.parse(code);
  return new Function('location', 'gl', 'out', 'getValue', 'mat4', code);
}

export class RenderTargetWrapper {

  get id() {
    return this._id;
  }

  set id(value) {
    if (!value) {
      this._id = `#RenderTargetWrapper-rt-${++rtwUidGenerator}`;
      this._dirty = true;
      return;
    }
    if (typeof value !== 'string') {
      throw new Error('`value` is not type of String!');
    }

    this._id = value;
    this._dirty = true;
  }

  get width() {
    return this._width;
  }

  set width(value) {
    if (typeof value !== 'number') {
      throw new Error('`value` is not type of Number!');
    }

    this._width = value;
    this._dirty = true;
  }

  get height() {
    return this._height;
  }

  set height(value) {
    if (typeof value !== 'number') {
      throw new Error('`value` is not type of Number!');
    }

    this._height = value;
    this._dirty = true;
  }

  get level() {
    return this._level;
  }

  set level(value) {
    if (typeof value !== 'number') {
      throw new Error('`value` is not type of Number!');
    }

    this._level = Math.max(0, value | 0);
    this._dirty = true;
  }

  get potMode() {
    return this._potMode;
  }

  set potMode(value) {
    if (!value) {
      this._potMode = null;
      return;
    }

    if (typeof value !== 'string') {
      throw new Error('`value` is not type of String!');
    }

    this._potMode = value;
    this._dirty = true;
  }

  get floatPointData() {
    return this._floatPointData;
  }

  set floatPointData(value) {
    if (typeof value !== 'boolean') {
      throw new Error('`value` is not type of Boolean!');
    }

    this._floatPointData = value;
    this._dirty = true;
  }

  get pushPopMode() {
    return this._pushPopMode;
  }

  set pushPopMode(value) {
    if (typeof value !== 'boolean') {
      throw new Error('`value` is not type of Boolean!');
    }

    this._pushPopMode = value;
  }

  get targets() {
    return this._targets;
  }

  set targets(value) {
    if (!value) {
      this._targets = null;
      return;
    }

    if (!Array.isArray(value)) {
      throw new Error('`value` is not type of Array!');
    }

    this._targets = value;
    this._dirty = true;
  }

  constructor() {
    this._renderer = null;
    this._id = `#RenderTargetWrapper-rt-${++rtwUidGenerator}`;
    this._idUsed = null;
    this._width = -1;
    this._height = -1;
    this._level = 0;
    this._potMode = null;
    this._floatPointData = false;
    this._pushPopMode = false;
    this._targets = null;
    this._dirty = true;
  }

  dispose() {
    const { _renderer, _idUsed } = this;
    if (!!_renderer) {
      if (!!_idUsed) {
        _renderer.unregisterRenderTarget(_idUsed);
      }
    }

    this._renderer = null;
    this._id = null;
    this._idUsed = null;
    this._potMode = null;
    this._targets = null;
  }

  enable(renderer) {
    this._ensureState(renderer);

    const { _idUsed } = this;
    if (!!_idUsed) {
      if (this._pushPopMode) {
        renderer.pushRenderTarget(_idUsed);
      } else {
        renderer.enableRenderTarget(_idUsed);
      }
    }
  }

  disable() {
    const { _renderer } = this;
    if (!!_renderer) {
      if (this._pushPopMode) {
        _renderer.popRenderTarget();
      } else {
        _renderer.disableRenderTarget();
      }
    }
  }

  rebuild() {
    this._dirty = true;
  }

  _ensureState(renderer) {
    if (!this._dirty) {
      return;
    }
    const { _id } = this;
    if (!_id) {
      throw new Error('`id` cannot be null!');
    }

    if (!!this._idUsed) {
      renderer.unregisterRenderTarget(this._idUsed);
    }
    const { _potMode, _level, _width, _height } = this;
    const width = _width < 0 ? renderer.canvas.width : _width;
    const height = _height < 0 ? renderer.canvas.height : _height;
    const w = !_potMode
      ? width
      : getPOT(width, _potMode === 'upper');
    const h = !_potMode
      ? height
      : getPOT(height, _potMode === 'upper');
    const s = getMipmapScale(_level);

    this._idUsed = this._id;
    if (targets === undefined || targets === null) {
      renderer.registerRenderTarget(
        this._idUsed,
        (w * s) | 0,
        (h * s) | 0,
        this._floatPointData
      );
    } else {
      renderer.registerRenderTargetMulti(
        this._idUsed,
        (w * s) | 0,
        (h * s) | 0,
        this._targets
      );
    }

    this._renderer = renderer;
    this._dirty = false;
  }

}

/**
 * Rendering command base class.
 */
export class Command {

  /**
   * Dispose (release all internal resources).
   *
   * @example
   * command.dispose();
   * command = null;
   */
  dispose() {}

  /**
   * Called when command is executed.
   *
   * @abstract
   * @param {WebGLRenderingContext}	gl - WebGL context.
   * @param {RenderSystem}	renderer - Render system that is used to render.
   * @param {number}	deltaTime - Delta time.
   * @param {string}	layer - Layer id.
   */
  onRender(gl, renderer, deltaTime, layer) {
    throw new Error('Not implemented!');
  }

  /**
   * Called on view resize.
   *
   * @param {number}	width - Width.
   * @param {number}	height - Height.
   */
  onResize(width, height) {}

}

/**
 * Rendering pipeline base class.
 * Pipeline is a set of commands to render at once.
 */
export class Pipeline extends Command {

  get commands() {
    return this._commands;
  }

  set commands(value) {
    if (!value) {
      this._commands = null;
      return;
    }

    if (!Array.isArray(value)) {
      throw new Error('`value` is not type of Array!');
    }
    for (const item of value) {
      if (!(item instanceof Command)) {
        throw new Error('One of `value` items is not type of Command!');
      }
    }

    this._commands = value;
  }

  /**
   * Constructor.
   */
  constructor(commands = null) {
    super();

    this.commands = commands;
  }

  /**
   * @override
   */
  dispose() {
    const { _commands } = this;
    if (!!_commands) {
      for (const command of _commands) {
        command.dispose();
      }

      this._commands = null;
    }
  }

  /**
   * @override
   */
  onRender(gl, renderer, deltaTime, layer) {
    const { _commands } = this;
    if (!!_commands) {
      for (const command of _commands) {
        command.onRender(gl, renderer, deltaTime, layer);
      }
    }
  }

  /**
   * @override
   */
  onResize(width, height) {
    const { _commands } = this;
    if (!!_commands) {
      for (const command of _commands) {
        command.onResize(width, height);
      }
    }
  }

}

/**
 * Command to render fullscreen image with given shader.
 */
export class RenderFullscreenCommand extends Command {

  /** @type {string|null} */
  get shader() {
    return this._shader;
  }

  /** @type {string|null} */
  set shader(value) {
    if (!value) {
      this._shader = null;
      return;
    }
    if (typeof value !== 'string') {
      throw new Error('`value` is not type of String!');
    }

    this._shader = value;
  }

  /** @type {*} */
  get overrideUniforms() {
    return this._overrideUniforms;
  }

  /** @type {*} */
  get overrideSamplers() {
    return this._overrideSamplers;
  }

  /**
   * Constructor.
   */
  constructor() {
    super();

    this._context = null;
    this._vertexBuffer = null;
    this._indexBuffer = null;
    this._shader = null;
    this._overrideUniforms = new Map();
    this._overrideSamplers = new Map();
    this._dirty = true;
  }

  /**
   * Destructor (dispose internal resources).
   *
   * @example
   * command.dispose();
   * pass = null;
   */
  dispose() {
    const { _context, _vertexBuffer, _indexBuffer } = this;

    if (!!_context) {
      if (!!_vertexBuffer) {
        _context.deleteBuffer(_vertexBuffer);
      }
      if (!!_indexBuffer) {
        _context.deleteBuffer(_indexBuffer);
      }
    }

    this._overrideUniforms.clear();
    this._overrideSamplers.clear();

    this._context = null;
    this._vertexBuffer = null;
    this._indexBuffer = null;
    this._shader = null;
    this._overrideUniforms = null;
    this._overrideSamplers = null;
  }

  /**
   * Called when camera need to postprocess it's rendered image.
   *
   * @param {WebGLRenderingContext}	gl - WebGL context.
   * @param {RenderSystem}	renderer - Render system that is used to render.
   * @param {number}	deltaTime - Delta time.
   * @param {string|null}  layer - Layer ID.
   */
  onRender(gl, renderer, deltaTime, layer) {
    const {
      _shader,
      _overrideUniforms,
      _overrideSamplers
    } = this;

    if (!_shader) {
      console.warn('Trying to render PostprocessPass without shader!');
      return;
    }

    this._ensureState(gl, renderer);
    gl.bindBuffer(gl.ARRAY_BUFFER, this._vertexBuffer);
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._indexBuffer);

    if (this._dirty) {
      this._dirty = false;
      gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
      gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
    }

    renderer.enableShader(_shader);

    if (_overrideUniforms.size > 0) {
      for (const [ name, value ] of _overrideUniforms) {
        renderer.overrideShaderUniform(name, value);
      }
    }

    if (_overrideSamplers.size > 0) {
      for (const [ name, { texture, filtering } ] of _overrideSamplers) {
        if (texture !== '') {
          renderer.overrideShaderSampler(name, texture, filtering);
        }
      }
    }

    gl.drawElements(
      gl.TRIANGLES,
      indices.length,
      gl.UNSIGNED_SHORT,
      0
    );

    renderer.disableShader();
  }

  _ensureState(gl, renderer) {
    this._context = gl;

    if (!this._vertexBuffer) {
      this._vertexBuffer = gl.createBuffer();
      this._dirty = true;
    }

    if (!this._indexBuffer) {
      this._indexBuffer = gl.createBuffer();
      this._dirty = true;
    }
  }

}

/**
 * Rendering graphics onto screen canvas.
 *
 * @example
 * const system = new RenderSystem('screen-0');
 */
export default class RenderSystem extends System {

  /** @type {*} */
  static get propsTypes() {
    return {
      useDevicePixelRatio: 'boolean',
      timeScale: 'number',
      collectStats: 'boolean'
    };
  }

  /** @type {number} */
  get contextVersion() {
    return this._contextVersion;
  }

  /** @type {boolean} */
  get useDevicePixelRatio() {
    return this._useDevicePixelRatio;
  }

  /** @type {boolean} */
  set useDevicePixelRatio(value) {
    this._useDevicePixelRatio = !!value;
  }

  /** @type {number} */
  get timeScale() {
    return this._timeScale;
  }

  /** @type {number} */
  set timeScale(value) {
    if (typeof value !== 'number') {
      throw new Error('`value` is not type of Number!');
    }

    this._timeScale = value;
  }

  /** @type {boolean} */
  get collectStats() {
    return this._collectStats;
  }

  /** @type {boolean} */
  set collectStats(value) {
    if (typeof value !== 'boolean') {
      throw new Error('`value` is not type of Boolean!');
    }

    this._collectStats = value;
  }

  /** @type {number} */
  get passedTime() {
    return this._passedTime;
  }

  /** @type {HTMLCanvasElement} */
  get canvas() {
    return this._canvas;
  }

  /** @type {Events} */
  get events() {
    return this._events;
  }

  /** @type {string} */
  get activeShader() {
    return this._activeShader;
  }

  /** @type {string} */
  get activeRenderTarget() {
    return this._activeRenderTarget;
  }

  /** @type {string} */
  get clearColor() {
    return this._clearColor;
  }

  /** @type {mat4} */
  get projectionMatrix() {
    return this._projectionMatrix;
  }

  /** @type {mat4} */
  get viewMatrix() {
    return this._viewMatrix;
  }

  /** @type {mat4} */
  get modelMatrix() {
    return this._modelMatrix;
  }

  /** @type {Map} */
  get stats() {
    return this._stats;
  }

  /** @type {string} */
  get statsText() {
    if (!this._collectStats) {
      return '';
    }

    const { _stats } = this;
    const deltaTime = _stats.get('delta-time');
    const passedTime = _stats.get('passed-time');
    const fps = `FPS: ${(1000 / deltaTime) | 0} (${1000 / deltaTime})`;
    const dt = `Delta time: ${deltaTime | 0} ms (${deltaTime})`;
    const pt = `Passed time: ${passedTime | 0} ms (${passedTime})`;
    const sc = `Shader changes: ${_stats.get('shader-changes')}`;
    const f = `Frames: ${_stats.get('frames')}`;
    const s = `Shaders: ${_stats.get('shaders')}`;
    const t = `Textures: ${_stats.get('textures')}`;
    const rt = `Render targets: ${_stats.get('renderTargets')}`;
    const e = `Extensions:${_stats.get('extensions').map(e => `\n - ${e}`).join()}`;
    return `${fps}\n${dt}\n${pt}\n${sc}\n${f}\n${s}\n${t}\n${rt}\n${e}`;
  }

  /**
   * Constructor.
   * Automaticaly binds into specified canvas.
   *
   * @param {string}	canvas - HTML canvas element id.
   * @param {boolean}	optimize - Optimize rendering pipeline.
   * @param {Array.<string>}	extensions - array with WebGL extensions list.
   * @param {number}	contextVersion - WebGL context version number.
   * @param {boolean}	manualMode - Manually trigger rendering next frames.
   */
  constructor(canvas, optimize = true, extensions = null, contextVersion = 1, manualMode = false) {
    super();

    this._manualMode = !!manualMode;
    this._extensions = new Map();
    this._contextVersion = contextVersion | 0;
    this._useDevicePixelRatio = false;
    this._timeScale = 1;
    this._collectStats = false;
    this._animationFrame = 0;
    this._lastTimestamp = null;
    this._canvas = null;
    this._context = null;
    this._shaders = new Map();
    this._textures = new Map();
    this._renderTargets = new Map();
    this._renderTargetsStack = [];
    this._events = new Events();
    this._activeShader = null;
    this._activeRenderTarget = null;
    this._activeViewportSize = vec2.create();
    this._clearColor = vec4.create();
    this._projectionMatrix = mat4.create();
    this._viewMatrix = mat4.create();
    this._modelMatrix = mat4.create();
    this._blendingConstants = {};
    this._stats = new Map();
    this._counterShaderChanges = 0;
    this._counterFrames = 0;
    this._optimize = !!optimize;
    this._passedTime = 0;
    this._shaderApplierOut = mat4.create();
    this._shaderApplierGetValue = name => {
      if (name === 'model-matrix') {
        return this._modelMatrix;
      } else if (name === 'view-matrix') {
        return this._viewMatrix;
      } else if (name === 'projection-matrix') {
        return this._projectionMatrix;
      } else {
        throw new Error(`Unknown matrix: ${name}`);
      }
    };
    this.__onFrame = this._onFrame.bind(this);

    if (!!extensions) {
      for (const name of extensions) {
        this._extensions.set(name, null);
      }
    }
    this._setup(canvas);
  }

  /**
   * Destructor (disposes internal resources).
   *
   * @example
   * system.dispose();
   * sustem = null;
   */
  dispose() {
    const { _context, _shaders, _textures, _renderTargets, _events } = this;

    this._stopAnimation();
    _context.clear(_context.COLOR_BUFFER_BIT);

    for (const shader of _shaders.keys()) {
      this.unregisterShader(shader);
    }
    for (const texture of _textures.keys()) {
      this.unregisterTexture(texture);
    }
    for (const renderTarget of _renderTargets.keys()) {
      this.unregisterRenderTarget(renderTarget);
    }

    _events.dispose();

    this._extensions = null;
    this._lastTimestamp = null;
    this._canvas = null;
    this._context = null;
    this._shaders = null;
    this._textures = null;
    this._renderTargets = null;
    this._renderTargetsStack = null;
    this._events = null;
    this._activeShader = null;
    this._activeRenderTarget = null;
    this._activeViewportSize = null;
    this._clearColor = null;
    this._projectionMatrix = null;
    this._viewMatrix = null;
    this._modelMatrix = null;
    this._blendingConstants = null;
    this._stats = null;
    this._shaderApplierOut = null;
    this._shaderApplierGetValue = null;
    this.__onFrame = null;
  }

  /**
   * Get loaded WebGL extension by it's name.
   *
   * @param {string}	name - Extension name.
   *
   * @return {*|null} WebGL extension or null if not found.
   *
   * @example
   * const extension = system.extension('vertex_array_object');
   * if (!!extension) {
   *   const vao = extension.createVertexArrayOES();
   *   extension.bindVertexArrayOES(vao);
   * }
   */
  extension(name) {
    return this._extensions.get(name) || null;
  }

  /**
   * Load WebGL extension by it's name.
   *
   * @param {string}	name - Extension name.
   *
   * @return {*|null} WebGL extension or null if not supported.
   *
   * @example
   * const extension = system.requestExtension('vertex_array_object');
   * if (!!extension) {
   *   const vao = extension.createVertexArrayOES();
   *   extension.bindVertexArrayOES(vao);
   * }
   */
  requestExtension(name) {
    const { _context, _contextVersion, _extensions } = this;
    if (!_context) {
      throw new Error('WebGL context is not yet ready!');
    }

    let ext = _extensions.get(name);
    if (!!ext) {
      return ext;
    }

    const meta = extensions[name];
    if (!meta) {
      throw new Error(`Unsupported extension: ${name}`);
    }
    ext = getExtensionByVersion(meta, _context, _contextVersion);

    if (!!ext) {
      _extensions.set(name, ext);
    } else {
      console.warn(`Could not get WebGL extension: ${name}`);
    }

    return ext || null;
  }

  /**
   * Load WebGL extensions by their names.
   *
   * @param {string[]}	args - Extension names.
   *
   * @return {boolean} True if all are supported and loaded, false otherwise.
   *
   * @example
   * const supported = system.requestExtensions('texture_float', 'texture_float_linear');
   * if (!supported) {
   *   throw new Error('One of requested WebGL extensions is not supported!');
   * }
   */
  requestExtensions(...args) {
    for (const arg of args) {
      if (!this.requestExtension(arg)) {
        return false;
      }
    }

    return true;
  }

  /**
   * Execute rendering command.
   *
   * @param {Command}	command - command to execute.
   * @param {number}	deltaTime - Delta time.
   * @param {string|null}	layer - Layer ID.
   */
  executeCommand(command, deltaTime, layer) {
    if (!(command instanceof Command)) {
      throw new Error('`command` is not type of Command!');
    }
    if (typeof deltaTime !== 'number') {
      throw new Error('`deltaTime` is not type of Number!');
    }

    command.onRender(this._context, this, deltaTime, layer);
  }

  /**
   * Register new shader.
   *
   * @param {string}	id - Shader id.
   * @param {string}	vertex - Vertex shader code.
   * @param {string}	fragment - Fragment shader code.
   * @param {*}	layoutInfo - Vertex layout description.
   * @param {*}	uniformsInfo - Uniforms description.
   * @param {*}	samplersInfo - Samplers description.
   * @param {*}	blendingInfo - Blending mode description.
   * @param {string[]|null}	extensionsInfo - Required extensions list.
   *
   * @example
   * system.registerShader(
   *   'red',
   *   'attribute vec2 aPosition;\nvoid main() { gl_Position = vec4(aPosition, 0.0, 1.0); }',
   *   'void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }',
   *   { aPosition: { size: 2, stride: 2, offset: 0 } },
   *   {},
   *   { source: 'src-alpha', destination: 'one-minus-src-alpha' }
   * );
   */
  registerShader(
    id,
    vertex,
    fragment,
    layoutInfo,
    uniformsInfo,
    samplersInfo,
    blendingInfo,
    extensionsInfo
  ) {
    if (typeof id !== 'string') {
      throw new Error('`id` is not type of String!');
    }
    if (typeof vertex !== 'string') {
      throw new Error('`vertex` is not type of String!');
    }
    if (typeof fragment !== 'string') {
      throw new Error('`fragment` is not type of String!');
    }
    if (!layoutInfo) {
      throw new Error('`layoutInfo` cannot be null!');
    }

    this.unregisterShader(id);

    if (Array.isArray(extensionsInfo) && extensionsInfo.length > 0) {
      if (!this.requestExtensions(...extensionsInfo)) {
        throw new Error(`One of shader extensions is not supported (${id})!`);
      }
    }

    const gl = this._context;
    const shader = gl.createProgram();
    const vshader = gl.createShader(gl.VERTEX_SHADER);
    const fshader = gl.createShader(gl.FRAGMENT_SHADER);
    const deleteAll = () => {
      gl.deleteShader(vshader);
      gl.deleteShader(fshader);
      gl.deleteProgram(shader);
    };

    // TODO: fix problem with forced GLSL 3 in WebGL 2.
    // const { _contextVersion } = this;
    // if (_contextVersion > 1) {
    //   vertex = `#version 300 es\n#define OXY_ctx_ver ${_contextVersion}\n${vertex}`;
    //   fragment = `#version 300 es\n#define OXY_ctx_ver ${_contextVersion}\n${fragment}`;
    // } else {
    //   vertex = `#define OXY_ctx_ver ${_contextVersion}\n${vertex}`;
    //   fragment = `#define OXY_ctx_ver ${_contextVersion}\n${fragment}`;
    // }
    gl.shaderSource(vshader, vertex);
    gl.shaderSource(fshader, fragment);
    gl.compileShader(vshader);
    gl.compileShader(fshader);

    if (!gl.getShaderParameter(vshader, gl.COMPILE_STATUS)) {
      const log = gl.getShaderInfoLog(vshader);
      deleteAll();
      throw new Error(`Cannot compile vertex shader: ${id}\nLog: ${log}`);
    }
    if (!gl.getShaderParameter(fshader, gl.COMPILE_STATUS)) {
      const log = gl.getShaderInfoLog(fshader);
      deleteAll();
      throw new Error(`Cannot compile fragment shader: ${id}\nLog: ${log}`);
    }

    gl.attachShader(shader, vshader);
    gl.attachShader(shader, fshader);
    gl.linkProgram(shader);

    if (!gl.getProgramParameter(shader, gl.LINK_STATUS)) {
      const log = gl.getProgramInfoLog(shader);
      deleteAll();
      throw new Error(`Cannot link shader program: ${id}\nLog: ${log}`);
    }

    const layout = new Map();
    const uniforms = new Map();
    const samplers = new Map();
    let blending = null;

    for (const name in layoutInfo) {
      const { size, stride, offset } = layoutInfo[name];

      if (typeof size !== 'number' ||
          typeof stride !== 'number' ||
          typeof offset !== 'number'
      ) {
        deleteAll();
        throw new Error(
          `Shader layout does not have proper settings: ${id} (${name})`
        );
      }

      const location = gl.getAttribLocation(shader, name);
      if (location < 0) {
        deleteAll();
        throw new Error(
          `Shader does not have attribute: ${id} (${name})`
        );
      }

      layout.set(name, {
        location,
        size,
        stride,
        offset
      });
    }

    if (layout.size === 0) {
      deleteAll();
      throw new Error(`Shader layout cannot be empty: ${id}`);
    }

    if (!!uniformsInfo) {
      for (const name in uniformsInfo) {
        const mapping = uniformsInfo[name];

        if (typeof mapping !== 'string' &&
            typeof mapping !== 'number' &&
            !(mapping instanceof Array)
        ) {
          deleteAll();
          throw new Error(
            `Shader uniform does not have proper settings: ${id} (${name})`
          );
        }

        let func = null;
        if (typeof mapping === 'string' && mapping.startsWith('@')) {
          func = functions[mapping];
          if (!func) {
            func = functions[mapping] = makeApplierFunction(mapping);
          }
        }

        const location = gl.getUniformLocation(shader, name);
        if (!location) {
          deleteAll();
          throw new Error(
            `Shader does not have uniform: ${id} (${name})`
          );
        }

        const forcedUpdate =
          !!func ||
          mapping === 'projection-matrix' ||
          mapping === 'view-matrix' ||
          mapping === 'model-matrix' ||
          mapping === 'time' ||
          mapping === 'viewport-size' ||
          mapping === 'inverse-viewport-size';

        uniforms.set(name, {
          location,
          mapping: !func ? mapping : func,
          forcedUpdate
        });
      }
    }

    if (!!samplersInfo) {
      for (const name in samplersInfo) {
        const { channel, texture, filtering } = samplersInfo[name];

        if (typeof channel !== 'number' ||
          (!!texture && typeof texture !== 'string') ||
          (!!filtering  && typeof filtering !== 'string')
        ) {
          deleteAll();
          throw new Error(
            `Shader sampler does not have proper settings: ${id} (${name})`
          );
        }

        const location = gl.getUniformLocation(shader, name);
        if (!location) {
          deleteAll();
          throw new Error(
            `Shader does not have sampler: ${id} (${name})`
          );
        }

        samplers.set(name, {
          location,
          channel,
          texture,
          filtering
        });
      }
    }

    if (!!blendingInfo) {
      const { source, destination } = blendingInfo;
      if (typeof source !== 'string' || typeof destination !== 'string') {
        throw new Error(`Shader blending does not have proper settings: ${id}`);
      }

      blending = {
        source: this._getBlendingFromName(source),
        destination: this._getBlendingFromName(destination)
      };
    }

    this._shaders.set(id, { shader, layout, uniforms, samplers, blending });
  }

  /**
   * Unregister existing shader.
   *
   * @param {string}	id - Shader id.
   *
   * @example
   * system.unregisterShader('red');
   */
  unregisterShader(id) {
    const { _shaders } = this;
    const gl = this._context;
    const meta = _shaders.get(id);

    if (!meta) {
      return;
    }

    const { shader } = meta;
    const shaders = gl.getAttachedShaders(shader);

    for (let i = 0, c = shaders.length; i < c; ++i) {
      gl.deleteShader(shaders[i]);
    }
    gl.deleteProgram(shader);
    _shaders.delete(id);
  }

  /**
   * Enable given shader (make it currently active for further rendering).
   *
   * @param {string}	id - Shader id.
   * @param {boolean}	forced - ignore optimizations (by default it will not enable if is currently active).
   *
   * @example
   * system.enableShader('red');
   */
  enableShader(id, forced = false) {
    const {
      _shaders,
      _textures,
      _activeShader,
      _projectionMatrix,
      _viewMatrix,
      _modelMatrix,
      _optimize,
      _passedTime
    } = this;
    const changeShader = forced || _activeShader !== id || !_optimize;
    const gl = this._context;
    const meta = _shaders.get(id);

    if (!meta) {
      console.warn(`Trying to enable non-existing shader: ${id}`);
      return;
    }

    const { shader, layout, uniforms, samplers, blending } = meta;

    if (changeShader) {
      gl.useProgram(shader);
      this._activeShader = id;
      ++this._counterShaderChanges;
    }

    for (const { location, size, stride, offset } of layout.values()) {
      gl.vertexAttribPointer(
        location,
        size,
        gl.FLOAT,
        false,
        stride * 4,
        offset * 4
      );
      gl.enableVertexAttribArray(location);
    }

    for (const [name, { location, mapping, forcedUpdate }] of uniforms.entries()) {
      const { length } = mapping;

      if (mapping === '' || (!changeShader && !forcedUpdate)) {
        continue;

      } else if (mapping === 'projection-matrix') {
        gl.uniformMatrix4fv(location, false, _projectionMatrix);

      } else if (mapping === 'view-matrix') {
        gl.uniformMatrix4fv(location, false, _viewMatrix);

      } else if (mapping === 'model-matrix') {
        gl.uniformMatrix4fv(location, false, _modelMatrix);

      } else if (mapping === 'time') {
        gl.uniform1f(location, _passedTime * 0.001);

      } else if (mapping === 'viewport-size') {
        gl.uniform2f(
          location,
          this._activeViewportSize[0],
          this._activeViewportSize[1]
        );

      } else if (mapping === 'inverse-viewport-size') {
        const [ width, height ] = this._activeViewportSize;

        gl.uniform2f(
          location,
          width === 0 ? 1 : 1 / width,
          height === 0 ? 1 : 1 / height
        );

      } else if (typeof mapping === 'number') {
        gl.uniform1f(location, mapping);

      } else if (length === 2) {
        gl.uniform2fv(location, mapping);

      } else if (length === 3) {
        gl.uniform3fv(location, mapping);

      } else if (length === 4) {
        gl.uniform4fv(location, mapping);

      } else if (length === 9) {
        gl.uniformMatrix3fv(location, false, mapping);

      } else if (length === 16) {
        gl.uniformMatrix4fv(location, false, mapping);

      } else if (mapping instanceof Function) {
        mapping(
          location,
          gl,
          this._shaderApplierOut,
          this._shaderApplierGetValue,
          mat4
        );

      } else {
        console.warn(`Trying to set non-proper uniform: ${name} (${id})`);
      }
    }

    if (!changeShader) {
      return;
    }

    for (const { location, channel, texture, filtering } of samplers.values()) {
      const tex = _textures.get(texture);
      if (!tex) {
        console.warn(`Trying to enable non-existing texture: ${texture} (${id})`);
        continue;
      }

      gl.activeTexture(gl.TEXTURE0 + channel | 0);
      gl.bindTexture(gl.TEXTURE_2D, tex.texture);
      if (filtering === 'trilinear') {
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
      } else if (filtering === 'bilinear') {
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_LINEAR);
      } else if (filtering === 'linear') {
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
      } else {
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
      }
      gl.uniform1i(location, channel | 0);
    }

    if (!!blending) {
      gl.enable(gl.BLEND);
      gl.blendFunc(blending.source, blending.destination);
    } else {
      gl.disable(gl.BLEND);
    }
  }

  /**
   * Disable active shader.
   * Make sure that this is currently active shader (otherwise it will unbind wrong shader locations)!
   *
   * @example
   * system.enableShader('red');
   * system.disableShader();
   */
  disableShader() {
    const gl = this._context;
    const meta = this._shaders.get(this._activeShader);
    if (!meta) {
      console.warn(`Trying to disable non-existing shader: ${this._activeShader}`);
      return;
    }

    const { layout } = meta;
    for (const { location } of layout.values()) {
      gl.disableVertexAttribArray(location);
    }
  }

  /**
   * Give active shader uniform a different than it's default value.
   *
   * @param {string}	name - Uniform name.
   * @param {*}	value - Uniform value. Can be number of array of numbers.
   *
   * @example
   * system.enableShader('color');
   * system.overrideShaderUniform('uColor', [1, 0, 0, 1]);
   */
  overrideShaderUniform(name, value) {
    const { _shaders, _activeShader } = this;
    const gl = this._context;
    const meta = _shaders.get(_activeShader);

    if (!meta) {
      console.warn(`Trying to set uniform of non-existing shader: ${_activeShader}`);
      return;
    }

    const { uniforms } = meta;
    const uniform = uniforms.get(name);

    if (!uniform) {
      console.warn(`Trying to set value of non-existing uniform: ${_activeShader} (${name})`);
      return;
    }

    const { location } = uniform;
    const { length } = value;

    if (typeof value === 'number') {
      gl.uniform1f(location, value);

    } else if (length === 2) {
      gl.uniform2fv(location, value);

    } else if (length === 3) {
      gl.uniform3fv(location, value);

    } else if (length === 4) {
      gl.uniform4fv(location, value);

    } else if (length === 9) {
      gl.uniformMatrix3fv(location, false, value);

    } else if (length === 16) {
      gl.uniformMatrix4fv(location, false, value);

    }
  }

  /**
   * Give active shader sampler different than it's default texture.
   *
   * @param {string}	name - Sampler id.
   * @param {string|null}	texture - Texture id.
   * @param {string|null}	filtering - Sampler filtering. Can be trilinear, bilinear, linear or nearest.
   *
   * @example
   * system.enableShader('sprite');
   * system.overrideShaderSampler('sTexture', 'martian', 'linear');
   */
  overrideShaderSampler(name, texture, filtering) {
    const { _shaders, _textures, _activeShader } = this;
    const gl = this._context;
    const meta = _shaders.get(_activeShader);
    if (!meta) {
      console.warn(`Trying to set sampler of non-existing shader: ${_activeShader}`);
      return;
    }

    const { samplers } = meta;
    const sampler = samplers.get(name);
    if (!sampler) {
      console.warn(`Trying to set non-existing sampler: ${_activeShader} (${name})`);
      return;
    }

    texture = texture || sampler.texture;
    filtering = filtering || sampler.filtering;

    const tex = _textures.get(texture);
    if (!tex) {
      console.warn(`Trying to enable non-existing texture: ${texture} (${name})`);
      return;
    }

    const { location, channel } = sampler;
    gl.activeTexture(gl.TEXTURE0 + channel | 0);
    gl.bindTexture(gl.TEXTURE_2D, tex.texture);
    if (filtering === 'trilinear') {
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
    } else if (filtering === 'bilinear') {
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_LINEAR);
    } else if (filtering === 'linear') {
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    } else {
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    }
    gl.uniform1i(location, channel | 0);
  }

  /**
   * Register new texture.
   *
   * @param {string}	id - Texture id.
   * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement}	image - Image, canvas or video instance.
   * @param {boolean} generateMipmap - Should generate mipmaps.
   *
   * @example
   * const image = new Image();
   * image.src = 'martian.png';
   * system.registerTexture('martian', image);
   */
  registerTexture(id, image, generateMipmap = false) {
    this.unregisterTexture(id);

    const gl = this._context;

    const texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.bindTexture(gl.TEXTURE_2D, null);

    this._textures.set(id, {
      texture,
      width: image.width,
      height: image.height
    });

    if (!!generateMipmap) {
      this.generateTextureMipmap(id);
    }
  }

  /**
   * Register empty texture (mostly used in offscreen rendering cases).
   *
   * @param {string}	id - Texture id.
   * @param {number}	width - Width.
   * @param {number}	height - Height.
   * @param {boolean}	floatPointData - Tells if this texture will store floating point data.
   * @param {ArrayBufferView|null} pixelData - ArrayBuffer view with pixel data or null if empty.
   *
   * @example
   * system.registerTextureEmpty('offscreen', 512, 512);
   */
  registerTextureEmpty(id, width, height, floatPointData = false, pixelData = null) {
    if (!!floatPointData && !this.requestExtensions(
      'texture_float',
      'texture_float_linear'
    )) {
      throw new Error('Float textures are not supported!');
    }

    this.unregisterTexture(id);

    const gl = this._context;
    width = Math.max(1, width | 0);
    height = Math.max(1, height | 0);

    const texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(
      gl.TEXTURE_2D,
      0,
      gl.RGBA,
      width,
      height,
      0,
      gl.RGBA,
      !!floatPointData ? gl.FLOAT : gl.UNSIGNED_BYTE,
      pixelData
    );
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.bindTexture(gl.TEXTURE_2D, null);

    this._textures.set(id, {
      texture,
      width,
      height
    });
  }

  /**
   * Register colored texture (mostly used to create solid color textures).
   *
   * @param {string}	id - Texture id.
   * @param {number}	width - Width.
   * @param {number}	height - Height.
   * @param {number}  r - Red value.
   * @param {number}  g - Green value.
   * @param {number}  b - Blue value.
   * @param {number}  a - Alpha value.
   *
   * @example
   * system.registerTextureEmpty('offscreen', 512, 512);
   */
  registerTextureColor(id, width, height, r, g, b, a) {
    const c = width * height * 4;
    const data = new Uint8Array(c);
    for (let i = 0; i < c; i += 4) {
      data[i] = r;
      data[i + 1] = g;
      data[i + 2] = b;
      data[i + 3] = a;
    }
    return this.registerTextureEmpty(id, width, height, false, data);
  }

  /**
   * Unregister existing texture.
   *
   * @param {string}	id - Texture id.
   *
   * @example
   * system.unregisterTexture('red');
   */
  unregisterTexture(id) {
    const { _textures } = this;
    const gl = this._context;
    const texture = _textures.get(id);

    if (!!texture) {
      gl.deleteTexture(texture.texture);
      _textures.delete(id);
    }
  }

  /**
   * Get texture meta information (width and height).
   *
   * @param {string}	id - Texture id.
   *
   * @return {*|null} Object with width and height properties or null if not found.
   */
  getTextureMeta(id) {
    const { _textures } = this;
    const texture = _textures.get(id);

    return !!texture
      ? { width: texture.width, height: texture.height }
      : null;
  }

  /**
   * Try to generate mipmaps for given texture.
   *
   * @param {string}	id - Texture id.
   */
  generateTextureMipmap(id) {
    const { _textures } = this;
    const gl = this._context;
    const texture = _textures.get(id);

    if (!!texture) {
      if ((!isPOT(texture.width, texture.height)) && this._contextVersion < 2) {
        console.warn(
          'Cannot generate mipmaps for non-POT texture within version < 2'
        );
        return;
      }

      gl.bindTexture(gl.TEXTURE_2D, texture.texture);
      gl.generateMipmap(gl.TEXTURE_2D);
      gl.bindTexture(gl.TEXTURE_2D, null);
    }
  }

  /**
   * Register new render target.
   *
   * @param {string}	id - Render target id.
   * @param {number}	width - Width.
   * @param {number}	height - Height.
   * @param {boolean}	floatPointData - Tells if render target will store floating point data.
   *
   * @example
   * system.registerRenderTarget('offscreen', 512, 512);
   */
  registerRenderTarget(
    id,
    width,
    height,
    floatPointData = false
  ) {
    if (!!floatPointData && !this.requestExtensions(
      'texture_float',
      'texture_float_linear'
    )) {
      throw new Error('Float textures are not supported!');
    }

    this.unregisterRenderTarget(id);

    const gl = this._context;
    width = Math.max(1, width);
    height = Math.max(1, height);

    this.registerTextureEmpty(id, width, height, floatPointData);
    const texture = this._textures.get(id);
    if (!texture) {
      return;
    }

    const target = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, target);
    gl.framebufferTexture2D(
      gl.FRAMEBUFFER,
      gl.COLOR_ATTACHMENT0,
      gl.TEXTURE_2D,
      texture.texture,
      0
    );
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);

    this._renderTargets.set(id, {
      target,
      width,
      height,
      multiTargets: null,
      textures: [ texture ]
    });
  }

  /**
   * Register new multiple render target.
   *
   * @param {string}	id - Render target id.
   * @param {number}	width - Width.
   * @param {number}	height - Height.
   * @param {number|*[]}	targets - Number of render targets or array with target descriptors.
   *
   * @example
   * system.registerRenderTargetMulti('offscreen', 512, 512, 2);
   */
  registerRenderTargetMulti(id, width, height, targets) {
    if (!this.requestExtensions('draw_buffers')) {
      throw new Error('Draw buffers are not supported!');
    }

    this.unregisterRenderTarget(id);

    const gl = this._context;
    width = Math.max(1, width);
    height = Math.max(1, height);

    const isArray = Array.isArray(targets);
    const c = isArray ? targets.length : (targets | 0);
    const ext = this.extension('draw_buffers');
    const textures = [];
    const buffers = [];
    if (isArray) {
      for (let i = 0; i < c; ++i) {
        const target = targets[i];
        const tid = `${id}-${i}`;
        const w = 'width' in target ? Math.max(1, target.width | 0) : width;
        const h = 'height' in target ? Math.max(1, target.height | 0) : height;
        this.registerTextureEmpty(tid, w, h, !!target.floatPointData);
        const texture = this._textures.get(tid);
        if (!texture) {
          return;
        }
        textures.push(texture);
        buffers.push(ext.COLOR_ATTACHMENT0_WEBGL + i);
      }
    } else {
      for (let i = 0; i < c; ++i) {
        const tid = `${id}-${i}`;
        this.registerTextureEmpty(tid, width, height, false);
        const texture = this._textures.get(tid);
        if (!texture) {
          return;
        }
        textures.push(texture);
        buffers.push(ext.COLOR_ATTACHMENT0_WEBGL + i);
      }
    }

    const target = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, target);
    for (let i = 0; i < c; ++i) {
      gl.framebufferTexture2D(
        gl.FRAMEBUFFER,
        buffers[i],
        gl.TEXTURE_2D,
        textures[i].texture,
        0
      );
    }
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);

    this._renderTargets.set(id, {
      target,
      width,
      height,
      multiTargets: buffers,
      textures
    });
  }

  /**
   * Unregister existing render target.
   *
   * @param {string}	id - Render target id.
   *
   * @example
   * system.unregisterRenderTarget('offscreen');
   */
  unregisterRenderTarget(id) {
    const { _renderTargets } = this;
    const gl = this._context;
    const target = _renderTargets.get(id);

    if (!!target) {
      if (target.multiTargets && target.multiTargets.length > 0) {
        for (let i = 0; i < target.multiTargets.length; ++i) {
          this.unregisterTexture(`${id}-${i}`);
        }
      } else {
        this.unregisterTexture(id);
      }
      gl.deleteFramebuffer(target.target);
      _renderTargets.delete(id);
      target.textures = null;
      target.multiTargets = null;
    }
  }

  /**
   * Get render target meta information (width and height).
   *
   * @param {string}	id - Texture id.
   *
   * @return {*|null} Object with width and height properties or null if not found.
   */
  getRenderTargetMeta(id) {
    const { _renderTargets } = this;
    const target = _renderTargets.get(id);

    return !!target
      ? {
        width: target.width,
        height: target.height,
        targets: !!target.multiTargets ? target.multiTargets.length : 0
      }
      : null;
  }

  /**
   * Make given render target active for further rendering.
   *
   * @param {string}	id - Render target id
   * @param {bool} clearBuffer - clear buffer.
   *
   * @example
   * system.enableRenderTarget('offscreen');
   */
  enableRenderTarget(id, clearBuffer = true) {
    const { _renderTargets } = this;
    const gl = this._context;
    const target = _renderTargets.get(id);
    if (!target) {
      this.disableRenderTarget();
      return;
    }

    gl.bindFramebuffer(gl.FRAMEBUFFER, target.target);
    if (!!target.multiTargets) {
      gl.drawBuffers(target.multiTargets);
    }
    gl.viewport(0, 0, target.width, target.height);
    gl.scissor(0, 0, target.width, target.height);
    vec2.set(this._activeViewportSize, target.width, target.height);
    if (!!clearBuffer) {
      gl.clear(gl.COLOR_BUFFER_BIT);
    }
    this._activeRenderTarget = id;
  }

  /**
   * Disable active render target.
   *
   * @example
   * system.disableRenderTarget();
   */
  disableRenderTarget() {
    const { _context, _canvas, _activeRenderTarget } = this;
    if (!_activeRenderTarget) {
      return;
    }

    const target = this._renderTargets.get(_activeRenderTarget);
    const { width, height } = _canvas;
    if (!!target.multiTargets) {
      _context.drawBuffers([]);
    }
    _context.bindFramebuffer(_context.FRAMEBUFFER, null);
    _context.viewport(0, 0, width, height);
    _context.scissor(0, 0, width, height);
    vec2.set(this._activeViewportSize, width, height);
    this._activeRenderTarget = null;
  }

  /**
   * Push current render target on stack and make given render target active for further rendering.
   *
   * @param {string}	id - Render target id
   * @param {bool} clearBuffer - clear buffer.
   *
   * @example
   * system.pushRenderTarget('offscreen');
   */
  pushRenderTarget(id, clearBuffer) {
    this._renderTargetsStack.push(this._activeRenderTarget);
    this.enableRenderTarget(id, clearBuffer);
  }

  /**
   * Enable last render target stored on stack (or disable if stack is empty).
   *
   * @example
   * system.popRenderTarget();
   */
  popRenderTarget() {
    const last = this._renderTargetsStack.pop();
    if (!last) {
      this.disableRenderTarget();
    } else {
      this.enableRenderTarget(last, false);
    }
  }

  /**
   * Tells if there is registered given shader.
   *
   * @param {string}	id - Shader id.
   *
   * @return {boolean}
   */
  hasShader(id) {
    return this._shaders.has(id);
  }

  /**
   * Tells if there is registered given texture.
   *
   * @param {string}	id - Texture id.
   *
   * @return {boolean}
   */
  hasTexture(id) {
    return this._textures.has(id);
  }

  /**
   * Tells if there is registered given render target.
   *
   * @param {string}	id - Render target id.
   *
   * @return {boolean}
   */
  hasRenderTarget(id) {
    return this._renderTargets.has(id);
  }

  /**
   * Resize frame buffer to match canvas size.
   *
   * @param {boolean}	forced - True if ignore optimizations.
   */
  resize(forced = false) {
    const { _canvas, _context } = this;
    let { width, height, clientWidth, clientHeight } = _canvas;

    if (this._useDevicePixelRatio) {
      const { devicePixelRatio } = window;

      clientWidth = (clientWidth * devicePixelRatio) | 0;
      clientHeight = (clientHeight * devicePixelRatio) | 0;
    }

    if (forced || width !== clientWidth || height !== clientHeight) {
      _canvas.width = clientWidth;
      _canvas.height = clientHeight;
      _context.viewport(0, 0, clientWidth, clientHeight);
      _context.scissor(0, 0, clientWidth, clientHeight);
      vec2.set(this._activeViewportSize, clientWidth, clientHeight);
      this._events.trigger('resize', clientWidth, clientHeight);
    }
  }

  renderFrame() {
    if (!this._manualMode) {
      console.warn('Trying to manually render frame without manual render mode!');
      return;
    }

    this._onFrame(this._lastTimestamp);
  }

  /**
   * @override
   */
  onRegister() {
    this._startAnimation();
  }

  /**
   * @override
   */
  onUnregister() {
    this._stopAnimation();
  }

  _setup(canvas) {
    if (typeof canvas === 'string') {
      canvas = document.getElementById(canvas);
    }
    if (!(canvas instanceof HTMLCanvasElement)) {
      throw new Error('`canvas` is not type of either HTMLCanvasElement or String!');
    }

    this._canvas = canvas;

    let { _contextVersion } = this;
    const options = {
      alpha: false,
      depth: false,
      stencil: false,
      antialias: false,
      premultipliedAlpha: false,
      preserveDrawingBuffer: false,
      failIfMajorPerformanceCaveat: false
    };
    let version = versions.reduce((r, v) => {
      if (!!r || v[0] > _contextVersion) {
        return r;
      }
      const gl =
        canvas.getContext(v[1], options) ||
        canvas.getContext(`experimental-${v[1]}`, options);
      return !!gl ? { context: gl, contextVersion: v[0] } : r;
    }, null);
    if (!version) {
      throw new Error(
        `Cannot create WebGL context for version: ${_contextVersion}`
      );
    }
    const gl = this._context = version.context;
    this._contextVersion = version.contextVersion;

    for (const name of this._extensions.keys()) {
      this.requestExtension(name);
    }

    gl.enable(gl.SCISSOR_TEST);
    gl.viewport(0, 0, canvas.width, canvas.height);
    gl.scissor(0, 0, canvas.width, canvas.height);
    vec2.set(this._activeViewportSize, canvas.width, canvas.height);
    gl.clearColor(0.0, 0.0, 0.0, 0.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    this.registerTextureColor(
      '',
      1, 1,
      255, 255, 255, 255
    );
    this.registerTextureColor(
      '#default-albedo',
      1, 1,
      255, 255, 255, 255
    );
    this.registerTextureColor(
      '#default-normal',
      1, 1,
      (255 * 0.5) | 0,
      (255 * 0.5) | 0,
      255,
      255
    );
    this.registerTextureColor(
      '#default-metalness-smoothness-emission',
      1, 1,
      0, 0, 0, 255
    );
    this.registerTextureColor(
      '#default-environment',
      1, 1,
      0, 0, 0, 255
    );

    this._blendingConstants = {
      'zero': gl.ZERO,
      'one': gl.ONE,
      'src-color': gl.SRC_COLOR,
      'one-minus-src-color': gl.ONE_MINUS_SRC_COLOR,
      'dst-color': gl.DST_COLOR,
      'one-minus-dst-color': gl.ONE_MINUS_DST_COLOR,
      'src-alpha': gl.SRC_ALPHA,
      'one-minus-src-alpha': gl.ONE_MINUS_SRC_ALPHA,
      'dst-alpha': gl.DST_ALPHA,
      'one-minus-dst-alpha': gl.ONE_MINUS_DST_ALPHA,
      'constant-color': gl.CONSTANT_COLOR,
      'one-minus-constant-color': gl.ONE_MINUS_CONSTANT_COLOR,
      'constant-alpha': gl.CONSTANT_ALPHA,
      'one-minus-constant-alpha': gl.ONE_MINUS_CONSTANT_ALPHA,
      'src-alpha-saturate': gl.SRC_ALPHA_SATURATE
    };
  }

  _startAnimation() {
    this._stopAnimation();

    this._passedTime = 0;
    this._lastTimestamp = performance.now();
    this._requestFrame();
  }

  _stopAnimation() {
    cancelAnimationFrame(this._animationFrame);
    this._passedTime = 0;
    this._lastTimestamp = null;
  }

  _requestFrame() {
    if (!!this._manualMode) {
      return;
    }

    this._animationFrame = requestAnimationFrame(this.__onFrame);
  }

  _onFrame(timestamp) {
    this.resize();

    const { _clearColor, _stats, _counterShaderChanges } = this;
    const [ cr, cg, cb, ca ] = _clearColor;
    const gl = this._context;
    const deltaTime = (timestamp - this._lastTimestamp) * this._timeScale;
    this._passedTime += deltaTime;
    this._lastTimestamp = timestamp;
    this._counterShaderChanges = 0
    this._activeShader = null;
    this._activeRenderTarget = null;

    gl.clearColor(cr, cg, cb, ca);
    gl.clear(gl.COLOR_BUFFER_BIT);
    this.events.trigger('render', gl, this, deltaTime);

    if (!!this._collectStats) {
      _stats.set('delta-time', deltaTime);
      _stats.set('passed-time', this._passedTime);
      _stats.set('shader-changes', _counterShaderChanges);
      _stats.set('frames', ++this._counterFrames);
      _stats.set('shaders', this._shaders.size);
      _stats.set('textures', this._textures.size);
      _stats.set('renderTargets', this._renderTargets.size);
      _stats.set('extensions', [...this._extensions.keys()]);
    }

    if (this._renderTargetsStack.length > 0) {
      console.warn(
        `There are ${this._renderTargetsStack.length} render targets on stack after frame!`
      );
      this._renderTargetsStack = [];
      this.disableRenderTarget();
    }

    this._requestFrame();
  }

  _getBlendingFromName(name) {
    const { _blendingConstants } = this;

    if (!(name in _blendingConstants)) {
      throw new Error(`There is no blending function: ${name}`);
    }

    return _blendingConstants[name];
  }

}