Source: tiny/core/renderers/webgl/WebGLRenderer.js

import SystemRenderer from '../SystemRenderer';
import MaskManager from './managers/MaskManager';
import StencilManager from './managers/StencilManager';
import FilterManager from './managers/FilterManager';
import RenderTarget from './utils/RenderTarget';
import ObjectRenderer from './utils/ObjectRenderer';
import TextureManager from './TextureManager';
import BaseTexture from '../../textures/BaseTexture';
import TextureGarbageCollector from './TextureGarbageCollector';
import WebGLState from './WebGLState';
import mapWebGLDrawModesToTiny from './utils/mapWebGLDrawModesToTiny';
import validateContext from './utils/validateContext';
import { pluginTarget } from '../../utils';
import glCore from '../../../../libs/gl-core';
import { RENDERER_TYPE } from '../../const';
import { config } from '../../settings';

let CONTEXT_UID = 0;

/**
 * The WebGLRenderer draws the scene and all its content onto a webGL enabled canvas. This renderer should be used for browsers that support webGL. This Render works by automatically managing webGLBatchs.
 * So no need for Sprite Batches or Sprite Clouds.
 * Don't forget to add the view to your DOM or you will not see anything :)
 *
 * @class
 * @memberof Tiny
 * @extends Tiny.SystemRenderer
 */
export default class WebGLRenderer extends SystemRenderer {
  /**
   *
   * @param {number} width - the width of the screen
   * @param {number} height - the height of the screen
   * @param {object} [options] - The optional renderer parameters
   * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional
   * @param {boolean} [options.transparent=false] - If the render view is transparent, default false
   * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false
   * @param {boolean} [options.antialias=false] - sets antialias. If not available natively then FXAA antialiasing is used
   * @param {boolean} [options.forceFXAA=false] - forces FXAA antialiasing to be used over native. FXAA is faster, but may not always look as great
   * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer. The resolution of the renderer retina would be 2.
   * @param {boolean} [options.clearBeforeRender=true] - This sets if the CanvasRenderer will clear the canvas or not before the new render pass. If you wish to set this to false, you *must* set preserveDrawingBuffer to `true`.
   * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you need to call toDataUrl on the webgl context.
   * @param {boolean} [options.roundPixels=false] - If true Tiny will Math.floor() x/y values when rendering, stopping pixel interpolation.
   * @param {number} [options.backgroundColor=0x000000] - The background color of the rendered area (shown if not transparent).
   * @param {boolean} [options.legacy=false] - If true Tiny will aim to ensure compatibility with older / less advanced devices. If you experiance unexplained flickering try setting this to true.
   * @param {string} [options.powerPreference] - Parameter passed to webgl context, set to "high-performance" for devices with dual graphics card
   */
  constructor(width, height, options) {
    super('WebGL', width, height, options);

    this.legacy = this.options.legacy;

    if (this.legacy) {
      glCore.VertexArrayObject.FORCE_NATIVE = true;
    }

    /**
     * The type of this renderer as a standardised const
     *
     * @member {number}
     * @see Tiny.RENDERER_TYPE
     */
    this.type = RENDERER_TYPE.WEBGL;

    this.handleContextLost = this.handleContextLost.bind(this);
    this.handleContextRestored = this.handleContextRestored.bind(this);

    this.view.addEventListener('webglcontextlost', this.handleContextLost, false);
    this.view.addEventListener('webglcontextrestored', this.handleContextRestored, false);

    /**
     * The options passed in to create a new webgl context.
     *
     * @member {object}
     * @private
     */
    this._contextOptions = Object.assign({
      alpha: this.transparent,
      antialias: this.options.antialias,
      premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied',
      stencil: true,
      preserveDrawingBuffer: this.options.preserveDrawingBuffer,
      powerPreference: this.options.powerPreference,
    }, config.extraContextAttributes || {});

    this._backgroundColorRgba[3] = this.transparent ? 0 : 1;

    /**
     * Manages the masks using the stencil buffer.
     *
     * @member {Tiny.MaskManager}
     */
    this.maskManager = new MaskManager(this);

    /**
     * Manages the stencil buffer.
     *
     * @member {Tiny.StencilManager}
     */
    this.stencilManager = new StencilManager(this);

    /**
     * An empty renderer.
     *
     * @member {Tiny.ObjectRenderer}
     */
    this.emptyRenderer = new ObjectRenderer(this);

    /**
     * The currently active ObjectRenderer.
     *
     * @member {Tiny.ObjectRenderer}
     */
    this.currentRenderer = this.emptyRenderer;

    /**
     * Manages textures
     *
     * @version 1.2.0
     * @member {Tiny.TextureManager}
     */
    this.textureManager = null;

    /**
     * Manages the filters.
     *
     * @version 1.2.0
     * @member {Tiny.FilterManager}
     */
    this.filterManager = null;

    this.initPlugins();

    /**
     * The current WebGL rendering context, it is created here
     *
     * @member {WebGLRenderingContext}
     */
    // initialize the context so it is ready for the managers.
    if (this.options.context) {
      // checks to see if a context is valid..
      validateContext(this.options.context);
    }

    this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions);

    this.CONTEXT_UID = CONTEXT_UID++;

    /**
     * The currently active ObjectRenderer.
     *
     * @member {Tiny.WebGLState}
     */
    this.state = new WebGLState(this.gl);

    this.renderingToScreen = true;

    /**
     * Holds the current state of textures bound to the GPU.
     * @type {array}
     */
    this.boundTextures = null;

    /**
     * Holds the current shader
     *
     * @member {Tiny.Shader}
     */
    this._activeShader = null;

    this._activeVao = null;

    /**
     * Holds the current render target
     *
     * @member {Tiny.RenderTarget}
     */
    this._activeRenderTarget = null;

    this._initContext();

    // map some webGL blend and drawmodes..
    this.drawModes = mapWebGLDrawModesToTiny(this.gl);

    this._nextTextureLocation = 0;

    this.setBlendMode(0);

    /**
     * Fired after rendering finishes.
     *
     * @event Tiny.WebGLRenderer#postrender
     */

    /**
     * Fired before rendering starts.
     *
     * @event Tiny.WebGLRenderer#prerender
     */

    /**
     * Fired when the WebGL context is set.
     *
     * @event Tiny.WebGLRenderer#context
     * @param {WebGLRenderingContext} gl - WebGL context.
     */
  }

  /**
   * Creates the WebGL context
   *
   * @private
   */
  _initContext() {
    const gl = this.gl;

    // restore a context if it was previously lost
    if (gl.isContextLost() && gl.getExtension('WEBGL_lose_context')) {
      gl.getExtension('WEBGL_lose_context').restoreContext();
    }

    const maxTextures = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);

    this._activeShader = null;
    this._activeVao = null;

    this.boundTextures = new Array(maxTextures);
    this.emptyTextures = new Array(maxTextures);

    /**
     * Did someone temper with textures state? We'll overwrite them when we need to unbind something.
     *
     * @member {boolean}
     * @private
     */
    this._unknownBoundTextures = false;

    // create a texture manager...
    this.textureManager = new TextureManager(this);
    this.filterManager = new FilterManager(this);
    this.textureGC = new TextureGarbageCollector(this);

    this.state.resetToDefault();

    this.rootRenderTarget = new RenderTarget(gl, this.width, this.height, null, this.resolution, true);
    this.rootRenderTarget.clearColor = this._backgroundColorRgba;

    this.bindRenderTarget(this.rootRenderTarget);

    // now lets fill up the textures with empty ones!
    const emptyGLTexture = new glCore.GLTexture.fromData(gl, null, 1, 1); // eslint-disable-line

    const tempObj = { _glTextures: {} };

    tempObj._glTextures[this.CONTEXT_UID] = {};

    for (let i = 0; i < maxTextures; i++) {
      const empty = new BaseTexture();

      empty._glTextures[this.CONTEXT_UID] = emptyGLTexture;

      this.boundTextures[i] = tempObj;
      this.emptyTextures[i] = empty;
      this.bindTexture(null, i);
    }

    this.emit('context', gl);

    // setup the width/height properties and gl viewport
    this.resize(this.screen.width, this.screen.height);
  }

  /**
   * Renders the object to its webGL view
   *
   * @param {Tiny.DisplayObject} displayObject - the object to be rendered
   * @param {Tiny.RenderTexture} renderTexture - The render texture to render to.
   * @param {boolean} [clear] - Should the canvas be cleared before the new render
   * @param {Tiny.Transform} [transform] - A transform to apply to the render texture before rendering.
   * @param {boolean} [skipUpdateTransform] - Should we skip the update transform pass?
   */
  render(displayObject, renderTexture, clear, transform, skipUpdateTransform) {
    // can be handy to know!
    this.renderingToScreen = !renderTexture;

    this.emit('prerender');

    // no point rendering if our context has been blown up!
    if (!this.gl || this.gl.isContextLost()) {
      return;
    }

    this._nextTextureLocation = 0;

    if (!renderTexture) {
      this._lastObjectRendered = displayObject;
    }

    if (!skipUpdateTransform) {
      // update the scene graph
      const cacheParent = displayObject.parent;

      displayObject.parent = this._tempDisplayObjectParent;
      displayObject.updateTransform();
      displayObject.parent = cacheParent;
      // displayObject.hitArea = //TODO add a temp hit area
    }

    this.bindRenderTexture(renderTexture, transform);

    this.currentRenderer.start();

    if (clear !== undefined ? clear : this.clearBeforeRender) {
      this._activeRenderTarget.clear();
    }

    displayObject.renderWebGL(this);

    // apply transform..
    this.currentRenderer.flush();

    // this.setObjectRenderer(this.emptyRenderer);

    this.textureGC.update();

    this.emit('postrender');
  }

  /**
   * Changes the current renderer to the one given in parameter
   *
   * @param {Tiny.ObjectRenderer} objectRenderer - The object renderer to use.
   */
  setObjectRenderer(objectRenderer) {
    if (this.currentRenderer === objectRenderer) {
      return;
    }

    this.currentRenderer.stop();
    this.currentRenderer = objectRenderer;
    this.currentRenderer.start();
  }

  /**
   * This should be called if you wish to do some custom rendering
   * It will basically render anything that may be batched up such as sprites
   *
   */
  flush() {
    this.setObjectRenderer(this.emptyRenderer);
  }

  /**
   * Resizes the webGL view to the specified width and height.
   *
   * @param {number} screenWidth - the new width of the screen
   * @param {number} screenHeight - the new height of the screen
   */
  resize(screenWidth, screenHeight) {
    //  if(width * this.resolution === this.width && height * this.resolution === this.height)return;

    SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight);

    this.rootRenderTarget.resize(screenWidth, screenHeight);

    if (this._activeRenderTarget === this.rootRenderTarget) {
      this.rootRenderTarget.activate();

      if (this._activeShader) {
        this._activeShader.uniforms.projectionMatrix = this.rootRenderTarget.projectionMatrix.toArray(true);
      }
    }
  }

  /**
   * Resizes the webGL view to the specified width and height.
   *
   * @param {number} blendMode - the desired blend mode
   */
  setBlendMode(blendMode) {
    this.state.setBlendMode(blendMode);
  }

  /**
   * Erases the active render target and fills the drawing area with a colour
   *
   * @param {number} [clearColor] - The colour
   */
  clear(clearColor) {
    this._activeRenderTarget.clear(clearColor);
  }

  /**
   * Sets the transform of the active render target to the given matrix
   *
   * @param {Tiny.Matrix} matrix - The transformation matrix
   */
  setTransform(matrix) {
    this._activeRenderTarget.transform = matrix;
  }

  /**
   * Erases the render texture and fills the drawing area with a colour
   *
   * @param {Tiny.RenderTexture} renderTexture - The render texture to clear
   * @param {number} [clearColor] - The colour
   * @return {Tiny.WebGLRenderer} Returns itself.
   */
  clearRenderTexture(renderTexture, clearColor) {
    const baseTexture = renderTexture.baseTexture;
    const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID];

    if (renderTarget) {
      renderTarget.clear(clearColor);
    }

    return this;
  }

  /**
   * Binds a render texture for rendering
   *
   * @param {Tiny.RenderTexture} renderTexture - The render texture to render
   * @param {Tiny.Transform} transform - The transform to be applied to the render texture
   * @return {Tiny.WebGLRenderer} Returns itself.
   */
  bindRenderTexture(renderTexture, transform) {
    let renderTarget;

    if (renderTexture) {
      const baseTexture = renderTexture.baseTexture;

      if (!baseTexture._glRenderTargets[this.CONTEXT_UID]) {
        // bind the current texture
        this.textureManager.updateTexture(baseTexture, 0);
      }

      this.unbindTexture(baseTexture);

      renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID];
      renderTarget.setFrame(renderTexture.frame);
    } else {
      renderTarget = this.rootRenderTarget;
    }

    renderTarget.transform = transform;
    this.bindRenderTarget(renderTarget);

    return this;
  }

  /**
   * Changes the current render target to the one given in parameter
   *
   * @param {Tiny.RenderTarget} renderTarget - the new render target
   * @return {Tiny.WebGLRenderer} Returns itself.
   */
  bindRenderTarget(renderTarget) {
    if (renderTarget !== this._activeRenderTarget) {
      this._activeRenderTarget = renderTarget;
      renderTarget.activate();

      if (this._activeShader) {
        this._activeShader.uniforms.projectionMatrix = renderTarget.projectionMatrix.toArray(true);
      }

      this.stencilManager.setMaskStack(renderTarget.stencilMaskStack);
    }

    return this;
  }

  /**
   * Changes the current shader to the one given in parameter
   *
   * @param {Tiny.Shader} shader - the new shader
   * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix
   * @return {Tiny.WebGLRenderer} Returns itself.
   */
  bindShader(shader, autoProject) {
    // TODO cache
    if (this._activeShader !== shader) {
      this._activeShader = shader;
      shader.bind();

      // `autoProject` normally would be a default parameter set to true
      // but because of how Babel transpiles default parameters
      // it hinders the performance of this method.
      if (autoProject !== false) {
        // automatically set the projection matrix
        shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true);
      }
    }

    return this;
  }

  /**
   * Binds the texture. This will return the location of the bound texture.
   * It may not be the same as the one you pass in. This is due to optimisation that prevents needless binding of textures. For example if the texture is already bound it will return the current location of the texture instead of the one provided. To bypass this use force location
   *
   * @param {Tiny.Texture} texture - the new texture
   * @param {number} location - the suggested texture location
   * @param {boolean} forceLocation - force the location
   * @return {number} bound texture location
   */
  bindTexture(texture, location, forceLocation) {
    texture = texture || this.emptyTextures[location];
    texture = texture.baseTexture || texture;
    texture.touched = this.textureGC.count;

    if (!forceLocation) {
      // TODO - maybe look into adding boundIds.. save us the loop?
      for (let i = 0; i < this.boundTextures.length; i++) {
        if (this.boundTextures[i] === texture) {
          return i;
        }
      }

      if (location === undefined) {
        this._nextTextureLocation++;
        this._nextTextureLocation %= this.boundTextures.length;
        location = this.boundTextures.length - this._nextTextureLocation - 1;
      }
    } else {
      location = location || 0;
    }

    const gl = this.gl;
    const glTexture = texture._glTextures[this.CONTEXT_UID];

    if (!glTexture) {
      // this will also bind the texture..
      this.textureManager.updateTexture(texture, location);
    } else {
      // bind the current texture
      this.boundTextures[location] = texture;
      gl.activeTexture(gl.TEXTURE0 + location);
      gl.bindTexture(gl.TEXTURE_2D, glTexture.texture);
    }

    return location;
  }

  /**
   * unbinds the texture ...
   *
   * @param {Tiny.Texture} texture - the texture to unbind
   * @return {Tiny.WebGLRenderer} Returns itself.
   */
  unbindTexture(texture) {
    const gl = this.gl;

    texture = texture.baseTexture || texture;

    if (this._unknownBoundTextures) {
      this._unknownBoundTextures = false;
      // someone changed webGL state,
      // we have to be sure that our texture does not appear in multitexture renderer samplers

      for (let i = 0; i < this.boundTextures.length; i++) {
        if (this.boundTextures[i] === this.emptyTextures[i]) {
          gl.activeTexture(gl.TEXTURE0 + i);
          gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[i]._glTextures[this.CONTEXT_UID].texture);
        }
      }
    }

    for (let i = 0; i < this.boundTextures.length; i++) {
      if (this.boundTextures[i] === texture) {
        this.boundTextures[i] = this.emptyTextures[i];

        gl.activeTexture(gl.TEXTURE0 + i);
        gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[i]._glTextures[this.CONTEXT_UID].texture);
      }
    }

    return this;
  }

  /**
   * Creates a new VAO from this renderer's context and state.
   *
   * @return {VertexArrayObject} The new VAO.
   */
  createVao() {
    return new glCore.VertexArrayObject(this.gl, this.state.attribState);
  }

  /**
   * Changes the current Vao to the one given in parameter
   *
   * @param {Tiny.VertexArrayObject} vao - the new Vao
   * @return {Tiny.WebGLRenderer} Returns itself.
   */
  bindVao(vao) {
    if (this._activeVao === vao) {
      return this;
    }

    if (vao) {
      vao.bind();
    } else if (this._activeVao) {
      // TODO this should always be true i think?
      this._activeVao.unbind();
    }

    this._activeVao = vao;

    return this;
  }

  /**
   * Resets the WebGL state so you can render things however you fancy!
   *
   * @return {Tiny.WebGLRenderer} Returns itself.
   */
  reset() {
    this.setObjectRenderer(this.emptyRenderer);

    this.bindVao(null);
    this._activeShader = null;
    this._activeRenderTarget = this.rootRenderTarget;

    this._unknownBoundTextures = true;

    for (let i = 0; i < this.boundTextures.length; i++) {
      this.boundTextures[i] = this.emptyTextures[i];
    }

    // bind the main frame buffer (the screen);
    this.rootRenderTarget.activate();

    this.state.resetToDefault();

    return this;
  }

  /**
   * Handles a lost webgl context
   *
   * @private
   * @param {WebGLContextEvent} event - The context lost event.
   */
  handleContextLost(event) {
    event.preventDefault();
  }

  /**
   * Handles a restored webgl context
   *
   * @private
   */
  handleContextRestored() {
    this.textureManager.removeAll();
    this.filterManager.destroy(true);
    this._initContext();
  }

  /**
   * Removes everything from the renderer (event listeners, spritebatch, etc...)
   *
   * @param {boolean} [removeView=false] - Removes the Canvas element from the DOM.
   */
  destroy(removeView) {
    this.destroyPlugins();

    // remove listeners
    this.view.removeEventListener('webglcontextlost', this.handleContextLost);
    this.view.removeEventListener('webglcontextrestored', this.handleContextRestored);

    this.textureManager.destroy();

    // call base destroy
    super.destroy(removeView);

    this.uid = 0;

    // destroy the managers
    this.maskManager.destroy();
    this.stencilManager.destroy();
    this.filterManager.destroy();

    this.maskManager = null;
    this.filterManager = null;
    this.textureManager = null;
    this.currentRenderer = null;

    this.handleContextLost = null;
    this.handleContextRestored = null;

    this._contextOptions = null;
    this.gl.useProgram(null);

    if (this.gl.getExtension('WEBGL_lose_context')) {
      this.gl.getExtension('WEBGL_lose_context').loseContext();
    }

    this.gl = null;

    // this = null;
  }
}

/**
 * Collection of installed plugins. These are included by default in Tiny, but can be excluded by creating a custom build. Consult the README for more information about creating custom builds and excluding plugins.
 *
 * @name Tiny.WebGLRenderer#plugins
 * @type {object}
 * @readonly
 * @property {Tiny.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements.
 * @property {Tiny.extract.WebGLExtract} extract Extract image data from renderer.
 * @property {Tiny.interaction.InteractionManager} interaction Handles mouse, touch and pointer events.
 * @property {Tiny.prepare.WebGLPrepare} prepare Pre-render display objects.
 */

/**
 * Adds a plugin to the renderer.
 *
 * @method Tiny.WebGLRenderer#registerPlugin
 * @param {string} pluginName - The name of the plugin.
 * @param {function} ctor - The constructor function or class for the plugin.
 */

pluginTarget.mixin(WebGLRenderer);
Documentation generated by JSDoc 3.4.3 on Fri Jul 09 2021 19:32:26 GMT+0800 (CST)