Newer
Older
reroad-test / 2020-ryusei / aframe-master / src / components / scene / screenshot.js
@ryusei ryusei on 22 Oct 2020 8 KB パノラマ表示
/* global ImageData, URL */
var registerComponent = require('../../core/component').registerComponent;
var THREE = require('../../lib/three');

var VERTEX_SHADER = [
  'attribute vec3 position;',
  'attribute vec2 uv;',
  'uniform mat4 projectionMatrix;',
  'uniform mat4 modelViewMatrix;',
  'varying vec2 vUv;',
  'void main()  {',
  '  vUv = vec2( 1.- uv.x, uv.y );',
  '  gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
  '}'
].join('\n');

var FRAGMENT_SHADER = [
  'precision mediump float;',
  'uniform samplerCube map;',
  'varying vec2 vUv;',
  '#define M_PI 3.141592653589793238462643383279',
  'void main() {',
  '  vec2 uv = vUv;',
  '  float longitude = uv.x * 2. * M_PI - M_PI + M_PI / 2.;',
  '  float latitude = uv.y * M_PI;',
  '  vec3 dir = vec3(',
  '    - sin( longitude ) * sin( latitude ),',
  '    cos( latitude ),',
  '    - cos( longitude ) * sin( latitude )',
  '  );',
  '  normalize( dir );',
  '  gl_FragColor = vec4( textureCube( map, dir ).rgb, 1.0 );',
  '}'
].join('\n');

/**
 * Component to take screenshots of the scene using a keboard shortcut (alt+s).
 * It can be configured to either take 360° captures (`equirectangular`)
 * or regular screenshots (`projection`)
 *
 * This is based on https://github.com/spite/THREE.CubemapToEquirectangular
 * To capture an equirectangular projection of the scene a THREE.CubeCamera is used
 * The cube map produced by the CubeCamera is projected on a quad and then rendered to
 * WebGLRenderTarget with an ortographic camera.
 */
module.exports.Component = registerComponent('screenshot', {
  schema: {
    width: {default: 4096},
    height: {default: 2048},
    camera: {type: 'selector'}
  },

  init: function () {
    var el = this.el;
    var self = this;

    if (el.renderer) {
      setup();
    } else {
      el.addEventListener('render-target-loaded', setup);
    }

    function setup () {
      var gl = el.renderer.getContext();
      if (!gl) { return; }
      self.cubeMapSize = gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE);
      self.material = new THREE.RawShaderMaterial({
        uniforms: {map: {type: 't', value: null}},
        vertexShader: VERTEX_SHADER,
        fragmentShader: FRAGMENT_SHADER,
        side: THREE.DoubleSide
      });
      self.quad = new THREE.Mesh(
        new THREE.PlaneBufferGeometry(1, 1),
        self.material
      );
      self.quad.visible = false;
      self.camera = new THREE.OrthographicCamera(-1 / 2, 1 / 2, 1 / 2, -1 / 2, -10000, 10000);
      self.canvas = document.createElement('canvas');
      self.ctx = self.canvas.getContext('2d');
      el.object3D.add(self.quad);
      self.onKeyDown = self.onKeyDown.bind(self);
    }
  },

  getRenderTarget: function (width, height) {
    return new THREE.WebGLRenderTarget(width, height, {
      minFilter: THREE.LinearFilter,
      magFilter: THREE.LinearFilter,
      wrapS: THREE.ClampToEdgeWrapping,
      wrapT: THREE.ClampToEdgeWrapping,
      format: THREE.RGBAFormat,
      type: THREE.UnsignedByteType
    });
  },

  resize: function (width, height) {
    // Resize quad.
    this.quad.scale.set(width, height, 1);

    // Resize camera.
    this.camera.left = -1 * width / 2;
    this.camera.right = width / 2;
    this.camera.top = height / 2;
    this.camera.bottom = -1 * height / 2;
    this.camera.updateProjectionMatrix();

    // Resize canvas.
    this.canvas.width = width;
    this.canvas.height = height;
  },

  play: function () {
    window.addEventListener('keydown', this.onKeyDown);
  },

  /**
   * <ctrl> + <alt> + s = Regular screenshot.
   * <ctrl> + <alt> + <shift> + s = Equirectangular screenshot.
  */
  onKeyDown: function (evt) {
    var shortcutPressed = evt.keyCode === 83 && evt.ctrlKey && evt.altKey;
    if (!this.data || !shortcutPressed) { return; }
    var projection = evt.shiftKey ? 'equirectangular' : 'perspective';
    this.capture(projection);
  },

  /**
   * Capture a screenshot of the scene.
   *
   * @param {string} projection - Screenshot projection (equirectangular or perspective).
   */
  setCapture: function (projection) {
    var el = this.el;
    var size;
    var camera;
    var cubeCamera;
    var cubeRenderTarget;
    // Configure camera.
    if (projection === 'perspective') {
      // Quad is only used in equirectangular mode. Hide it in this case.
      this.quad.visible = false;
      // Use scene camera.
      camera = (this.data.camera && this.data.camera.components.camera.camera) || el.camera;
      size = {width: this.data.width, height: this.data.height};
    } else {
      // Use ortho camera.
      camera = this.camera;
      cubeRenderTarget = new THREE.WebGLCubeRenderTarget(
        Math.min(this.cubeMapSize, 2048),
        {
          format: THREE.RGBFormat,
          generateMipmaps: true,
          minFilter: THREE.LinearMipmapLinearFilter,
          encoding: THREE.sRGBEncoding
        });
      // Create cube camera and copy position from scene camera.
      cubeCamera = new THREE.CubeCamera(el.camera.near, el.camera.far, cubeRenderTarget);
      // Copy camera position into cube camera;
      el.camera.getWorldPosition(cubeCamera.position);
      el.camera.getWorldQuaternion(cubeCamera.quaternion);
      // Render scene with cube camera.
      cubeCamera.update(el.renderer, el.object3D);
      this.quad.material.uniforms.map.value = cubeCamera.renderTarget.texture;
      size = {width: this.data.width, height: this.data.height};
      // Use quad to project image taken by the cube camera.
      this.quad.visible = true;
    }
    return {
      camera: camera,
      size: size,
      projection: projection
    };
  },

  /**
   * Maintained for backwards compatibility.
   */
  capture: function (projection) {
    var isVREnabled = this.el.renderer.xr.enabled;
    var renderer = this.el.renderer;
    var params;
    // Disable VR.
    renderer.xr.enabled = false;
    params = this.setCapture(projection);
    this.renderCapture(params.camera, params.size, params.projection);
    // Trigger file download.
    this.saveCapture();
    // Restore VR.
    renderer.xr.enabled = isVREnabled;
  },

  /**
   * Return canvas instead of triggering download (e.g., for uploading blob to server).
   */
  getCanvas: function (projection) {
    var isVREnabled = this.el.renderer.xr.enabled;
    var renderer = this.el.renderer;
    // Disable VR.
    var params = this.setCapture(projection);
    renderer.xr.enabled = false;
    this.renderCapture(params.camera, params.size, params.projection);
    // Restore VR.
    renderer.xr.enabled = isVREnabled;
    return this.canvas;
  },

  renderCapture: function (camera, size, projection) {
    var autoClear = this.el.renderer.autoClear;
    var el = this.el;
    var imageData;
    var output;
    var pixels;
    var renderer = el.renderer;
    // Create rendering target and buffer to store the read pixels.
    output = this.getRenderTarget(size.width, size.height);
    pixels = new Uint8Array(4 * size.width * size.height);
    // Resize quad, camera, and canvas.
    this.resize(size.width, size.height);
    // Render scene to render target.
    renderer.autoClear = true;
    renderer.clear();
    renderer.setRenderTarget(output);
    renderer.render(el.object3D, camera);
    renderer.autoClear = autoClear;
    // Read image pizels back.
    renderer.readRenderTargetPixels(output, 0, 0, size.width, size.height, pixels);
    renderer.setRenderTarget(null);
    if (projection === 'perspective') {
      pixels = this.flipPixelsVertically(pixels, size.width, size.height);
    }
    imageData = new ImageData(new Uint8ClampedArray(pixels), size.width, size.height);
    // Hide quad after projecting the image.
    this.quad.visible = false;
    // Copy pixels into canvas.
    this.ctx.putImageData(imageData, 0, 0);
  },

  flipPixelsVertically: function (pixels, width, height) {
    var flippedPixels = pixels.slice(0);
    for (var x = 0; x < width; ++x) {
      for (var y = 0; y < height; ++y) {
        flippedPixels[x * 4 + y * width * 4] = pixels[x * 4 + (height - y) * width * 4];
        flippedPixels[x * 4 + 1 + y * width * 4] = pixels[x * 4 + 1 + (height - y) * width * 4];
        flippedPixels[x * 4 + 2 + y * width * 4] = pixels[x * 4 + 2 + (height - y) * width * 4];
        flippedPixels[x * 4 + 3 + y * width * 4] = pixels[x * 4 + 3 + (height - y) * width * 4];
      }
    }
    return flippedPixels;
  },

  /**
   * Download capture to file.
   */
  saveCapture: function () {
    this.canvas.toBlob(function (blob) {
      var fileName = 'screenshot-' + document.title.toLowerCase() + '-' + Date.now() + '.png';
      var linkEl = document.createElement('a');
      var url = URL.createObjectURL(blob);
      linkEl.href = url;
      linkEl.setAttribute('download', fileName);
      linkEl.innerHTML = 'downloading...';
      linkEl.style.display = 'none';
      document.body.appendChild(linkEl);
      setTimeout(function () {
        linkEl.click();
        document.body.removeChild(linkEl);
      }, 1);
    }, 'image/png');
  }
});