webgl/quadFeature.js

var inherit = require('../inherit');
var registerFeature = require('../registry').registerFeature;
var quadFeature = require('../quadFeature');
var timestamp = require('../timestamp');
var util = require('../util');

let _memoryCheckLargestTested = 4096 * 4096;

/**
 * Create a new instance of class quadFeature.
 *
 * @class
 * @alias geo.webgl.quadFeature
 * @param {geo.quadFeature.spec} arg Options object.
 * @extends geo.quadFeature
 * @returns {geo.webgl.quadFeature}
 */
var webgl_quadFeature = function (arg) {
  'use strict';
  if (!(this instanceof webgl_quadFeature)) {
    return new webgl_quadFeature(arg);
  }
  quadFeature.call(this, arg);

  var $ = require('jquery');
  var vgl = require('../vgl');
  var object = require('./object');
  var fragmentShaderImage = require('./quadFeatureImage.frag');
  var vertexShaderImage = require('./quadFeatureImage.vert');
  var fragmentShaderColor = require('./quadFeatureColor.frag');
  var vertexShaderColor = require('./quadFeatureColor.vert');

  object.call(this);

  var m_this = this,
      s_exit = this._exit,
      s_update = this._update,
      m_modelViewUniform,
      m_actor_image, m_actor_color, m_glBuffers = {}, m_imgposbuf,
      m_clrposbuf, m_clrModelViewUniform,
      m_glCompileTimestamp = timestamp(),
      m_glColorCompileTimestamp = timestamp(),
      m_quads;

  /**
   * Allocate buffers that we need to control for image quads.  This mimics
   * the actions from vgl.mapper to some degree.
   *
   * @private
   * @param {vgl.renderState} renderState An object that contains the context
   *   used for drawing.
   */
  function setupDrawObjects(renderState) {
    var context = renderState.m_context,
        newbuf = false;

    if (m_quads.imgQuads.length) {
      if (!m_imgposbuf || m_imgposbuf.length < m_quads.imgQuads.length * 12 ||
          !m_glBuffers.imgQuadsPosition) {
        if (m_glBuffers.imgQuadsPosition) {
          context.deleteBuffer(m_glBuffers.imgQuadsPosition);
        }
        m_glBuffers.imgQuadsPosition = context.createBuffer();
        m_imgposbuf = new Float32Array(Math.max(
          128, m_quads.imgQuads.length * 2) * 12);
        newbuf = true;
      }
      $.each(m_quads.imgQuads, function (idx, quad) {
        for (var i = 0; i < 12; i += 1) {
          m_imgposbuf[idx * 12 + i] = quad.pos[i] - m_quads.origin[i % 3];
        }
      });
      context.bindBuffer(context.ARRAY_BUFFER, m_glBuffers.imgQuadsPosition);
      if (newbuf) {
        context.bufferData(context.ARRAY_BUFFER, m_imgposbuf, context.DYNAMIC_DRAW);
      } else {
        context.bufferSubData(context.ARRAY_BUFFER, 0, m_imgposbuf);
      }
    }
    m_glCompileTimestamp.modified();
  }

  /**
   * Allocate buffers that we need to control for color quads.  This mimics
   * the actions from vgl.mapper to some degree.
   *
   * @private
   * @param {vgl.renderState} renderState An object that contains the context
   *   used for drawing.
   */
  function setupColorDrawObjects(renderState) {
    var context = renderState.m_context,
        newbuf = false;

    if (m_quads.clrQuads.length) {
      if (!m_clrposbuf || m_clrposbuf.length < m_quads.clrQuads.length * 12 ||
          !m_glBuffers.clrQuadsPosition) {
        if (m_glBuffers.clrQuadsPosition) {
          context.deleteBuffer(m_glBuffers.clrQuadsPosition);
        }
        m_glBuffers.clrQuadsPosition = context.createBuffer();
        m_clrposbuf = new Float32Array(Math.max(
          128, m_quads.clrQuads.length * 2) * 12);
        newbuf = true;
      }
      $.each(m_quads.clrQuads, function (idx, quad) {
        for (var i = 0; i < 12; i += 1) {
          m_clrposbuf[idx * 12 + i] = quad.pos[i] - m_quads.origin[i % 3];
        }
      });
      context.bindBuffer(context.ARRAY_BUFFER, m_glBuffers.clrQuadsPosition);
      if (newbuf) {
        context.bufferData(context.ARRAY_BUFFER, m_clrposbuf, context.DYNAMIC_DRAW);
      } else {
        context.bufferSubData(context.ARRAY_BUFFER, 0, m_clrposbuf);
      }
    }
    m_glColorCompileTimestamp.modified();
  }

  /**
   * Get a vgl mapper, mark dynamicDraw, augment the timestamp and the render
   * function.
   *
   * @private
   * @param {function} renderFunc Our own render function.
   * @returns {vgl.mapper} a vgl mapper object.
   */
  function getVGLMapper(renderFunc) {
    var mapper = new vgl.mapper({dynamicDraw: true});
    mapper.s_modified = mapper.modified;
    mapper.g_timestamp = timestamp();
    mapper.timestamp = mapper.g_timestamp.timestamp;
    mapper.modified = function () {
      mapper.s_modified();
      mapper.g_timestamp.modified();
      return mapper;
    };
    mapper.s_render = mapper.render;
    mapper.render = renderFunc;
    return mapper;
  }

  /**
   * List vgl actors.
   *
   * @returns {vgl.actor[]} The list of actors.
   */
  this.actors = function () {
    var actors = [];
    if (m_actor_image) {
      actors.push(m_actor_image);
    }
    if (m_actor_color) {
      actors.push(m_actor_color);
    }
    return actors;
  };

  /**
   * Build this feature.
   */
  this._build = function () {
    var mapper, mat, prog, srctex, unicrop, unicropsource, geom, context, sampler2d;

    if (!m_this.position()) {
      return;
    }
    m_quads = m_this._generateQuads();
    /* Create an actor to render image quads */
    if (m_quads.imgQuads.length && !m_actor_image) {
      m_this.visible(false);
      mapper = getVGLMapper(m_this._renderImageQuads);
      m_actor_image = new vgl.actor();
      /* This is similar to vgl.utils.createTextureMaterial */
      m_actor_image.setMapper(mapper);
      mat = new vgl.material();
      prog = new vgl.shaderProgram();
      prog.addVertexAttribute(new vgl.vertexAttribute('vertexPosition'),
                              vgl.vertexAttributeKeys.Position);
      prog.addVertexAttribute(new vgl.vertexAttribute('textureCoord'),
                              vgl.vertexAttributeKeys.TextureCoordinate);
      m_modelViewUniform = new vgl.modelViewOriginUniform(
        'modelViewMatrix', m_quads.origin);
      prog.addUniform(m_modelViewUniform);
      prog.addUniform(new vgl.projectionUniform('projectionMatrix'));
      prog.addUniform(new vgl.floatUniform('opacity', 1.0));
      prog.addUniform(new vgl.floatUniform('zOffset', 0.0));
      /* Use texture unit 0 */
      sampler2d = new vgl.uniform(vgl.GL.INT, 'sampler2d');
      sampler2d.set(0);
      prog.addUniform(sampler2d);
      context = m_this.renderer()._glContext();
      unicrop = new vgl.uniform(context.FLOAT_VEC2, 'crop');
      unicrop.set([1.0, 1.0]);
      prog.addUniform(unicrop);
      unicropsource = new vgl.uniform(context.FLOAT_VEC4, 'cropsource');
      unicropsource.set([0.0, 0.0, 0.0, 0.0]);
      prog.addUniform(unicropsource);
      prog.addShader(vgl.getCachedShader(
        context.VERTEX_SHADER, context, vertexShaderImage));
      prog.addShader(vgl.getCachedShader(
        context.FRAGMENT_SHADER, context, fragmentShaderImage));
      if (m_this._hookBuild) {
        m_this._hookBuild(prog);
      }
      mat.addAttribute(prog);
      mat.addAttribute(new vgl.blend());
      /* This is similar to vgl.planeSource */
      geom = new vgl.geometryData();
      m_imgposbuf = undefined;
      srctex = new vgl.sourceDataT2fv();
      srctex.pushBack([0, 0, 1, 0, 0, 1, 1, 1]);
      geom.addSource(srctex);
      /* We deliberately do not add a primitive to our geometry -- we take care
       * of that ourselves. */

      mapper.setGeometryData(geom);
      m_actor_image.setMaterial(mat);
      m_this.renderer().contextRenderer().addActor(m_actor_image);
      m_this.visible(true);
    }
    /* Create an actor to render color quads */
    if (m_quads.clrQuads.length && !m_actor_color) {
      m_this.visible(false);
      mapper = getVGLMapper(m_this._renderColorQuads);
      m_actor_color = new vgl.actor();
      /* This is similar to vgl.utils.createTextureMaterial */
      m_actor_color.setMapper(mapper);
      mat = new vgl.material();
      prog = new vgl.shaderProgram();
      prog.addVertexAttribute(new vgl.vertexAttribute('vertexPosition'),
                              vgl.vertexAttributeKeys.Position);
      m_clrModelViewUniform = new vgl.modelViewOriginUniform(
        'modelViewMatrix', m_quads.origin);
      prog.addUniform(m_clrModelViewUniform);
      prog.addUniform(new vgl.projectionUniform('projectionMatrix'));
      prog.addUniform(new vgl.floatUniform('opacity', 1.0));
      prog.addUniform(new vgl.floatUniform('zOffset', 0.0));
      context = m_this.renderer()._glContext();
      prog.addUniform(new vgl.uniform(context.FLOAT_VEC3, 'vertexColor'));
      prog.addShader(vgl.getCachedShader(
        context.VERTEX_SHADER, context, vertexShaderColor));
      prog.addShader(vgl.getCachedShader(
        context.FRAGMENT_SHADER, context, fragmentShaderColor));
      mat.addAttribute(prog);
      mat.addAttribute(new vgl.blend());
      /* This is similar to vgl.planeSource */
      geom = new vgl.geometryData();
      m_clrposbuf = undefined;
      /* We deliberately do not add a primitive to our geometry -- we take care
       * of that ourselves. */

      mapper.setGeometryData(geom);
      m_actor_color.setMaterial(mat);

      m_this.renderer().contextRenderer().addActor(m_actor_color);
      m_this.visible(true);
    }
    if (m_modelViewUniform) {
      m_modelViewUniform.setOrigin(m_quads.origin);
    }
    if (m_clrModelViewUniform) {
      m_clrModelViewUniform.setOrigin(m_quads.origin);
    }
    m_this._updateTextures();
    m_this.buildTime().modified();
  };

  /**
   * Check all of the image quads.  If any do not have the correct texture,
   * update them.
   */
  this._updateTextures = function () {
    var texture;

    $.each(m_quads.imgQuads, function (idx, quad) {
      if (!quad.image) {
        return;
      }
      if (quad.image._texture) {
        quad.texture = quad.image._texture;
      } else {
        texture = new vgl.texture();
        texture.setImage(quad.image);
        let nearestPixel = m_this.nearestPixel();
        if (nearestPixel !== undefined) {
          if (nearestPixel !== true && util.isNonNullFinite(nearestPixel)) {
            const curZoom = m_this.layer().map().zoom();
            nearestPixel = curZoom >= nearestPixel;
          }
        }
        if (nearestPixel) {
          texture.setNearestPixel(true);
        }
        quad.texture = quad.image._texture = texture;
      }
    });
  };

  /**
   * Render all of the color quads using a single mapper.
   *
   * @param {vgl.renderState} renderState An object that contains the context
   *   used for drawing.
   */
  this._renderColorQuads = function (renderState) {
    if (!m_quads.clrQuads.length) {
      return;
    }
    var mapper = this;
    if (mapper.timestamp() > m_glColorCompileTimestamp.timestamp() ||
        m_this.dataTime().timestamp() > m_glColorCompileTimestamp.timestamp() ||
        renderState.m_contextChanged || !m_clrposbuf ||
        m_quads.clrQuads.length * 12 > m_clrposbuf.length) {
      setupColorDrawObjects(renderState);
    }
    mapper.s_render(renderState, true);

    var context = renderState.m_context, opacity, zOffset, color;

    context.bindBuffer(context.ARRAY_BUFFER, m_glBuffers.clrQuadsPosition);
    $.each(m_quads.clrQuads, function (idx, quad) {
      if (quad.opacity !== opacity) {
        opacity = quad.opacity;
        context.uniform1fv(renderState.m_material.shaderProgram()
          .uniformLocation('opacity'), new Float32Array([opacity]));
      }
      if ((quad.zOffset || 0.0) !== zOffset) {
        zOffset = quad.zOffset || 0.0;
        context.uniform1fv(renderState.m_material.shaderProgram()
          .uniformLocation('zOffset'), new Float32Array([zOffset]));
      }
      if (!color || color.r !== quad.color.r || color.g !== quad.color.g ||
          color.b !== quad.color.b) {
        color = quad.color;
        context.uniform3fv(renderState.m_material.shaderProgram()
          .uniformLocation('vertexColor'), new Float32Array([
          color.r, color.g, color.b]));
      }

      context.bindBuffer(context.ARRAY_BUFFER, m_glBuffers.clrQuadsPosition);
      context.vertexAttribPointer(vgl.vertexAttributeKeys.Position, 3,
                                  context.FLOAT, false, 12, idx * 12 * 4);
      context.enableVertexAttribArray(vgl.vertexAttributeKeys.Position);

      context.drawArrays(context.TRIANGLE_STRIP, 0, 4);
    });
    context.bindBuffer(context.ARRAY_BUFFER, null);
    mapper.undoBindVertexData(renderState);
  };

  /**
   * Render all of the image quads using a single mapper.
   *
   * @param {vgl.renderState} renderState An object that contains the context
   *   used for drawing.
   */
  this._renderImageQuads = function (renderState) {
    if (!m_quads.imgQuads.length) {
      return;
    }
    var mapper = this;
    if (mapper.timestamp() > m_glCompileTimestamp.timestamp() ||
        m_this.dataTime().timestamp() > m_glCompileTimestamp.timestamp() ||
        renderState.m_contextChanged || !m_imgposbuf ||
        m_quads.imgQuads.length * 12 > m_imgposbuf.length) {
      setupDrawObjects(renderState);
    }
    mapper.s_render(renderState, true);

    var context = renderState.m_context,
        opacity, zOffset,
        crop = {x: 1, y: 1}, quadcrop,
        cropsrc = {x0: 0, y0: 0, x1: 1, y1: 1}, quadcropsrc,
        w, h, quadw, quadh;

    let nearestPixel = m_this.nearestPixel();
    if (nearestPixel !== undefined) {
      if (nearestPixel !== true && util.isNonNullFinite(nearestPixel)) {
        const curZoom = m_this.layer().map().zoom();
        nearestPixel = curZoom >= nearestPixel;
      }
      m_quads.imgQuads.forEach((quad) => {
        if (quad.image && quad.texture && quad.texture.nearestPixel() !== nearestPixel && quad.texture.textureHandle()) {
          /* This could just be
           *   quad.texture.setNearestPixel(nearestPixel);
           * but that needlessly redecodes the image.  Instead, just change the
           * the interpolation flags, then change the nearestPixel value
           * without triggering a complete re-setup. */
          renderState.m_context.bindTexture(vgl.GL.TEXTURE_2D, quad.texture.textureHandle());
          renderState.m_context.texParameteri(vgl.GL.TEXTURE_2D, vgl.GL.TEXTURE_MIN_FILTER, nearestPixel ? vgl.GL.NEAREST : vgl.GL.LINEAR);
          renderState.m_context.texParameteri(vgl.GL.TEXTURE_2D, vgl.GL.TEXTURE_MAG_FILTER, nearestPixel ? vgl.GL.NEAREST : vgl.GL.LINEAR);
          renderState.m_context.bindTexture(vgl.GL.TEXTURE_2D, null);
          const oldmod = quad.texture.modified;
          quad.texture.modified = () => {};
          quad.texture.setNearestPixel(nearestPixel);
          quad.texture.modified = oldmod;
        }
      });
    }
    if (m_this._hookRenderImageQuads) {
      m_this._hookRenderImageQuads(renderState, m_quads.imgQuads);
    }
    context.bindBuffer(context.ARRAY_BUFFER, m_glBuffers.imgQuadsPosition);
    $.each(m_quads.imgQuads, function (idx, quad) {
      if (!quad.image) {
        return;
      }
      quad.texture.bind(renderState);
      // only check if the context is out of memory when using modestly large
      // textures.  The check is slow.
      if (quad.image.width * quad.image.height > _memoryCheckLargestTested) {
        _memoryCheckLargestTested = quad.image.width * quad.image.height;
        if (context.getError() === context.OUT_OF_MEMORY) {
          console.log('Insufficient GPU memory for texture');
        }
      }
      if (quad.opacity !== opacity) {
        opacity = quad.opacity;
        context.uniform1fv(renderState.m_material.shaderProgram()
          .uniformLocation('opacity'), new Float32Array([opacity]));
      }
      if ((quad.zOffset || 0.0) !== zOffset) {
        zOffset = quad.zOffset || 0.0;
        context.uniform1fv(renderState.m_material.shaderProgram()
          .uniformLocation('zOffset'), new Float32Array([zOffset]));
      }
      quadcrop = quad.crop || {x: 1, y: 1};
      if (!crop || quadcrop.x !== crop.x || quadcrop.y !== crop.y) {
        crop = quadcrop;
        context.uniform2fv(renderState.m_material.shaderProgram()
          .uniformLocation('crop'), new Float32Array([crop.x === undefined ? 1 : crop.x, crop.y === undefined ? 1 : crop.y]));
      }
      w = quad.image.width;
      h = quad.image.height;
      quadcropsrc = quad.crop || {left: 0, top: 0, right: w, bottom: h};
      if (!cropsrc || quadcropsrc.left !== cropsrc.left || quadcropsrc.top !== cropsrc.top || quadcropsrc.right !== cropsrc.right || quadcropsrc.bottom !== cropsrc.bottom || quadw !== w || quadh !== h) {
        cropsrc = quadcropsrc;
        quadw = w;
        quadh = h;
        context.uniform4fv(renderState.m_material.shaderProgram()
          .uniformLocation('cropsource'), new Float32Array([
          cropsrc.left / w, cropsrc.top / h, cropsrc.right / w, cropsrc.bottom / h]));
      }
      context.bindBuffer(context.ARRAY_BUFFER, m_glBuffers.imgQuadsPosition);
      context.vertexAttribPointer(vgl.vertexAttributeKeys.Position, 3,
                                  context.FLOAT, false, 12, idx * 12 * 4);
      context.enableVertexAttribArray(vgl.vertexAttributeKeys.Position);

      context.drawArrays(context.TRIANGLE_STRIP, 0, 4);
      quad.texture.undoBind(renderState);
    });
    context.bindBuffer(context.ARRAY_BUFFER, null);
    mapper.undoBindVertexData(renderState);
  };

  /**
   * Update.
   */
  this._update = function () {
    s_update.call(m_this);
    if (m_this.buildTime().timestamp() <= m_this.dataTime().timestamp() ||
        m_this.updateTime().timestamp() < m_this.timestamp()) {
      m_this._build();
    }
    if (m_actor_color) {
      m_actor_color.setVisible(m_this.visible());
      m_actor_color.material().setBinNumber(m_this.bin());
    }
    if (m_actor_image) {
      m_actor_image.setVisible(m_this.visible());
      m_actor_image.material().setBinNumber(m_this.bin());
    }
    m_this.updateTime().modified();
  };

  /**
   * Cleanup.
   */
  this._cleanup = function () {
    if (m_actor_image) {
      m_this.renderer().contextRenderer().removeActor(m_actor_image);
      m_actor_image = null;
    }
    if (m_actor_color) {
      m_this.renderer().contextRenderer().removeActor(m_actor_color);
      m_actor_color = null;
    }
    m_imgposbuf = undefined;
    m_clrposbuf = undefined;
    Object.keys(m_glBuffers).forEach(function (key) { delete m_glBuffers[key]; });
    if (m_quads && m_quads.imgQuads) {
      m_quads.imgQuads.forEach(function (quad) {
        if (quad.texture) {
          delete quad.texture;
          delete quad.image._texture;
        }
      });
      m_this._updateTextures();
    }
    m_this.modified();
  };

  /**
   * Set the image or color vertex or fragment shader.
   *
   * @param {string} shaderType One of `image_vertex`, `image_fragment`,
   *   `color_vertex`, or `color_fragment`.
   * @param {string} shaderCode The shader program.
   * @returns {this} The class instance on success, undefined in an unknown
   *    shaderType was specified.
   */
  this.setShader = function (shaderType, shaderCode) {
    switch (shaderType) {
      case 'image_vertex': vertexShaderImage = shaderCode; break;
      case 'image_fragment': fragmentShaderImage = shaderCode; break;
      case 'color_vertex': vertexShaderColor = shaderCode; break;
      case 'color_fragment': fragmentShaderColor = shaderCode; break;
      default:
        return;
    }
    return m_this;
  };

  /**
   * Destroy.
   */
  this._exit = function () {
    m_this._cleanup();
    s_exit.call(m_this);
  };

  m_this._init(arg);
  return this;
};

inherit(webgl_quadFeature, quadFeature);

// Now register it
var capabilities = {};
capabilities[quadFeature.capabilities.color] = true;
capabilities[quadFeature.capabilities.image] = true;
capabilities[quadFeature.capabilities.imageCrop] = true;
capabilities[quadFeature.capabilities.imageFixedScale] = false;
capabilities[quadFeature.capabilities.imageFull] = true;
capabilities[quadFeature.capabilities.canvas] = false;
capabilities[quadFeature.capabilities.video] = false;

registerFeature('webgl', 'quad', webgl_quadFeature, capabilities);
module.exports = webgl_quadFeature;