Home Reference Source

src/components/VerticesRenderer.js

import Component from '../systems/EntitySystem/Component';
import { mat4 } from '../utils/gl-matrix';

const RenderMode = {
  TRIANGLES: 'triangles',
  LINES: 'lines',
  POINTS: 'points'
};

const BufferUsage = {
  STATIC: 'static',
  DYNAMIC: 'dynamic',
  STREAM: 'stream'
}

const DirtyMode = {
  NONE: 0,
  SUB: 1,
  ALL: 2
};

export default class VerticesRenderer extends Component {

  static factory() {
    return new VerticesRenderer();
  }

  static get propsTypes() {
    return {
      visible: 'boolean',
      shader: 'asset(shader)',
      vertices: 'array(number)',
      indices: 'array(integer)',
      verticesUsage: 'enum(static, dynamic, stream)',
      indicesUsage: 'enum(static, dynamic, stream)',
      overrideUniforms: 'uniforms',
      overrideSamplers: 'samplers',
      renderMode: 'enum(triangles, lines, points)',
      layers: 'array(string)'
    };
  }

  static get RenderMode() {
    return RenderMode;
  }

  static get BufferUsage() {
    return BufferUsage;
  }

  get visible() {
    return this._visible;
  }

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

    this._visible = value;
  }

  get shader() {
    return this._shader;
  }

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

    this._shader = value;
  }

  get vertices() {
    return this._vertices;
  }

  set vertices(value) {
    if (!value) {
      throw new Error('`value` cannot be null!');
    }

    if (value instanceof Array) {
      value = new Float32Array(value);
    }

    if (!(value instanceof Float32Array)) {
      throw new Error('`value` is not type of either Array or Float32Array!');
    }

    const { _vertices } = this;
    this._dirty = true;
    if (this._dirtyMode !== DirtyMode.ALL) {
      this._dirtyMode = !_vertices || _vertices.length != value.length
        ? DirtyMode.ALL
        : DirtyMode.SUB;
    }
    this._vertices = value;
  }

  get indices() {
    return this._indices;
  }

  set indices(value) {
    if (!value) {
      throw new Error('`value` cannot be null!');
    }

    if (value instanceof Array) {
      value = new Uint16Array(value);
    }

    if (!(value instanceof Uint16Array)) {
      throw new Error('`value` is not type of either Array or Uint16Array!');
    }

    const { _indices } = this;
    this._dirty = true;
    if (this._dirtyMode !== DirtyMode.ALL) {
      this._dirtyMode = !_indices || _indices.length != value.length
        ? DirtyMode.ALL
        : DirtyMode.SUB;
    }
    this._indices = value;
  }

  get verticesUsage() {
    return this._verticesUsage;
  }

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

    this._dirty = true;
    this._dirtyMode = DirtyMode.ALL;
    this._verticesUsage = value;
  }

  get indicesUsage() {
    return this._indicesUsage;
  }

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

    this._dirty = true;
    this._dirtyMode = DirtyMode.ALL;
    this._indicesUsage = value;
  }

  get overrideUniforms() {
    const result = {};
    for (const [key, value] of this._overrideUniforms) {
      result[key] = value;
    }

    return result;
  }

  set overrideUniforms(value) {
    const { _overrideUniforms } = this;

    _overrideUniforms.clear();

    if (!value) {
      return;
    }

    for (const name in value) {
      _overrideUniforms.set(name, value[name]);
    }
  }

  get overrideSamplers() {
    const result = {};
    for (const [key, value] of this._overrideSamplers) {
      result[key] = value;
    }

    return result;
  }

  set overrideSamplers(value) {
    const { _overrideSamplers } = this;

    _overrideSamplers.clear();

    if (!value) {
      return;
    }

    for (const name in value) {
      _overrideSamplers.set(name, value[name]);
    }
  }

  get renderMode() {
    return this._renderMode;
  }

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

    this._renderMode = value;
  }

  get layers() {
    return [ ...this._layers ];
  }

  set layers(value) {
    if (!value) {
      this._layers = null;
      return;
    }

    if (!(value instanceof Array)) {
      throw new Error('`value` is not type of Array!');
    }
    for (let i = 0, c = value.length; i < c; ++i) {
      if (typeof value[i] !== 'string') {
        throw new Error(`\`value\` item #${i} is not type of String!`);
      }
    }

    this._layers = [ ...value ];
  }

  constructor() {
    super();

    this._visible = true;
    this._context = null;
    this._vertexBuffer = null;
    this._indexBuffer = null;
    this._shader = null;
    this._vertices = null;
    this._indices = null;
    this._verticesUsage = BufferUsage.STATIC;
    this._indicesUsage = BufferUsage.STATIC;
    this._overrideUniforms = new Map();
    this._overrideSamplers = new Map();
    this._renderMode = RenderMode.TRIANGLES;
    this._layers = null;
    this._verticesChunksToUpdate = [];
    this._indicesChunksToUpdate = [];
    this._dirty = true;
    this._dirtyMode = DirtyMode.ALL;
  }

  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._vertices = null;
    this._indices = null;
    this._overrideUniforms = null;
    this._overrideSamplers = null;
    this._renderMode = null;
    this._layers = null;
    this._verticesChunksToUpdate = null;
    this._indicesChunksToUpdate = null;
  }

  updateVerticesChunk(data, offset, size = 0) {
    if (!data) {
      throw new Error('`data` cannot be null!');
    }

    const { _vertices } = this;
    if (!_vertices) {
      throw new Error('Cannot update non-existing vertices!');
    }

    if (data instanceof Array || data instanceof Float32Array) {
      if (offset < 0 || offset + data.length > _vertices.length) {
        throw new Error('Trying to update chunk exceeding vertices bounds!');
      }

      _vertices.set(data, offset);
      this._verticesChunksToUpdate.push(offset, data.length);
    } else {
      throw new Error('`data` is not type of either Array or Float32Array!');
    }
  }

  updateIndicesChunk(data, offset, size = 0) {
    if (!data) {
      throw new Error('`data` cannot be null!');
    }

    const { _indices } = this;
    if (!_indices) {
      throw new Error('Cannot update non-existing indices!');
    }

    if (data instanceof Array || data instanceof Uint16Array) {
      if (offset < 0 || offset + data.length > _indices.length) {
        throw new Error('Trying to update chunk exceeding indices bounds!');
      }

      _indices.set(data, offset);
      this._indicesChunksToUpdate.push(offset, data.length);
    } else {
      throw new Error('`data` is not type of either Array or Float32Array!');
    }
  }

  reuploadData() {
    this._dirty = true;
    if (this._dirtyMode !== DirtyMode.ALL) {
      this._dirtyMode = DirtyMode.SUB;
    }
  }

  onAction(name, ...args) {
    if (name === 'render') {
      return this.onRender(...args);
    } else if (name === 'render-layer') {
      return this.onRenderLayer(...args);
    } else if (name === 'preview') {
      return this.onPreview(...args);
    }
  }

  onPropertySerialize(name, value) {
    if (name === 'vertices' || name === 'indices') {
      if (!value) {
        return;
      }

      return [ ...value ];
    } else if (name === 'overrideUniforms' || name === 'overrideSamplers') {
      if (value.size === 0) {
        return;
      }

      const result = {};

      for (const [key, value] of value.entries()) {
        result[key] = value;
      }
      return result;
    } else if (name === 'layers') {
      return !value ? [] : [ ...value ];
    } else {
      return super.onPropertySerialize(name, value);
    }
  }

  onRender(gl, renderer, deltaTime, layer) {
    const {
      _visible,
      _shader,
      _vertices,
      _indices,
      _verticesUsage,
      _indicesUsage,
      _overrideUniforms,
      _overrideSamplers,
      _renderMode,
      _verticesChunksToUpdate,
      _indicesChunksToUpdate
    } = this;

    if (!_visible || !_renderMode) {
      return;
    }

    if (!_shader) {
      console.warn('Trying to render VerticesRenderer without shader!');
      return;
    }
    if (!_vertices || _vertices.length <= 0) {
      console.warn('Trying to render VerticesRenderer without vertices!');
      return;
    }
    if (!_indices || _indices.length <= 0) {
      console.warn('Trying to render VerticesRenderer without indices!');
      return;
    }

    this._ensureState(gl);
    mat4.copy(renderer.modelMatrix, this.entity.transform);

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

    if (this._dirty) {
      this._dirty = false;
      this._verticesChunksToUpdate = [];
      this._indicesChunksToUpdate = [];

      if (this._dirtyMode === DirtyMode.ALL) {
        let verticesUsage = gl.STATIC_DRAW;
        if (_verticesUsage === BufferUsage.DYNAMIC) {
          verticesUsage = gl.DYNAMIC_DRAW;
        } else if (_verticesUsage === BufferUsage.STREAM) {
          verticesUsage = gl.STREAM_DRAW;
        }

        let indicesUsage = gl.STATIC_DRAW;
        if (_indicesUsage === BufferUsage.DYNAMIC) {
          indicesUsage = gl.DYNAMIC_DRAW;
        } else if (_indicesUsage === BufferUsage.STREAM) {
          indicesUsage = gl.STREAM_DRAW;
        }

        gl.bufferData(gl.ARRAY_BUFFER, _vertices, verticesUsage);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, _indices, indicesUsage);
      } else if (this._dirtyMode === DirtyMode.SUB) {
        gl.bufferSubData(gl.ARRAY_BUFFER, 0, _vertices);
        gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, _indices);
      }

      this._dirtyMode = DirtyMode.NONE;
    }

    if (_verticesChunksToUpdate.length > 0) {
      for (let i = 0, c = _verticesChunksToUpdate.length; i < c; i += 2) {
        gl.bufferSubData(
          gl.ARRAY_BUFFER,
          _verticesChunksToUpdate[i],
          _vertices.subarray(
            _verticesChunksToUpdate[i],
            _verticesChunksToUpdate[i + 1]
          )
        );
      }

      this._verticesChunksToUpdate = [];
    }

    if (_indicesChunksToUpdate.length > 0) {
      for (let i = 0, c = _indicesChunksToUpdate.length; i < c; i += 2) {
        gl.bufferSubData(
          gl.ELEMENT_ARRAY_BUFFER,
          _indicesChunksToUpdate[i],
          _indices.subarray(
            _indicesChunksToUpdate[i],
            _indicesChunksToUpdate[i + 1]
          )
        );
      }

      this._indicesChunksToUpdate = [];
    }

    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) {
        renderer.overrideShaderSampler(name, texture, filtering);
      }
    }

    gl.drawElements(
      gl[_renderMode.toUpperCase()],
      _indices.length,
      gl.UNSIGNED_SHORT,
      0
    );

    renderer.disableShader();
  }

  onRenderLayer(gl, renderer, deltaTime, layer) {
    const { _layers } = this;
    if (!!layer &&
        !!_layers &&
        _layers.length > 0 &&
        _layers.indexOf(layer) < 0
    ) {
      return;
    }

    this.onRender(gl, renderer, deltaTime, layer);
  }

  onPreview(gl, renderer, deltaTime) {
    this.onRender(gl, renderer, deltaTime, null);
  }

  _ensureState(gl) {
    this._context = gl;

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

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

}