text: /**
   * @author yomotsu / http://yomotsu.net
   * ported from http://webgl-fire.appspot.com/html/fire.html
   *
   * https://www.youtube.com/watch?v=jKRHmQmduDI
   * https://graphics.ethz.ch/teaching/former/imagesynthesis_06/miniprojects/p3/
   * https://www.iusb.edu/math-compsci/_prior-thesis/YVanzine_thesis.pdf
   */
  
  //THREE.JS
  import * as THREE from 'three';
  
  const vs = [
  
    'attribute vec3 position;',
    'attribute vec3 tex;',
    'uniform mat4 projectionMatrix;',
    'uniform mat4 modelViewMatrix;',
  
    'varying vec3 texOut;',
  
    'void main ( void ) {',
  
    'gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0 );',
    'texOut = tex;',
  
    '}',
  
  ].join('\n');
  
  const fs = [
    'precision highp float;',
  
    // Modified Blum Blum Shub pseudo-random number generator.
    'vec2 mBBS( vec2 val, float modulus ) {',
  
    'val = mod( val, modulus ); // For numerical consistancy.',
    'return mod(val * val, modulus);',
  
    '}',
  
    // Pregenerated noise texture.
    'uniform sampler2D nzw;',
    'const float modulus = 61.0;  // Value used in pregenerated noise texture.',
  
    /**
     * Modified noise function.
     * @see http://www.csee.umbc.edu/~olano/papers/index.html#mNoise
     **/
    'float mnoise ( vec3 pos ) {',
  
    'float intArg = floor( pos.z );',
    'float fracArg = fract( pos.z );',
    'vec2 hash = mBBS( intArg * 3.0 + vec2( 0, 3 ), modulus );',
    'vec4 g = vec4 (',
    'texture2D( nzw, vec2( pos.x, pos.y + hash.x ) / modulus ).xy,',
    'texture2D( nzw, vec2( pos.x, pos.y + hash.y ) / modulus ).xy',
    ') * 2.0 - 1.0;',
  
    'return mix(',
    'g.x + g.y * fracArg,',
    'g.z + g.w * ( fracArg - 1.0 ),',
    'smoothstep( 0.0, 1.0, fracArg )',
    ');',
  
    '}',
  
    'const int octives = 4;',
    'const float lacunarity = 2.0;',
    'const float gain = 0.5;',
  
    /**
     * Adds multiple octives of noise together.
     **/
    'float turbulence( vec3 pos ) {',
  
    'float sum  = 0.0;',
    'float freq = 1.0;',
    'float amp  = 1.0;',
  
    'for ( int i = 0; i < 4; i++ ) {',
  
    'sum += abs( mnoise( pos * freq ) ) * amp;',
    'freq *= lacunarity;',
    'amp *= gain;',
  
    '}',
  
    'return sum;',
  
    '}',
  
    'const float magnatude = 1.3;',
    'uniform float time;',
    'uniform sampler2D fireProfile;',
  
    /**
     * Samples the fire.
     *
     * @param loc the normalized location (0.0-1.0) to sample the fire
     * @param scale the 'size' of the fire in world space and time
     **/
    'vec4 sampleFire( vec3 loc, vec4 scale ) {',
  
    // Convert xz to [-1.0, 1.0] range.
    'loc.xz = loc.xz * 2.0 - 1.0;',
  
    // Convert to (radius, height) to sample fire profile texture.
    'vec2 st = vec2( sqrt( dot( loc.xz, loc.xz ) ), loc.y );',
  
    // Convert loc to 'noise' space
    'loc.y -= time * scale.w; // Scrolling noise upwards over time.',
    'loc *= scale.xyz; // Scaling noise space.',
  
    // Offsetting vertial texture lookup.
    // We scale this by the sqrt of the height so that things are
    // relatively stable at the base of the fire and volital at the
    // top.
    'float offset = sqrt( st.y ) * magnatude * turbulence( loc );',
    'st.y += offset;',
  
    'if ( st.y > 1.0 ) {',
    'return vec4( 0, 0, 0, 1 );',
    '}',
    'vec4 result = texture2D( fireProfile, st );',
  
    // Fading out bottom so slice clipping isnt obvious
    'if ( st.y < 0.1 ) {',
    'result *= st.y / 0.1;',
    '}',
    'return result;',
    '}',
  
    'varying vec3 texOut;',
    'void main( void ) {',
  
    // Mapping texture coordinate to -1 => 1 for xy, 0=> 1 for y
    'vec3 color = sampleFire( texOut, vec4( 1.0, 2.0, 1.0, 0.5 ) ).xyz;',
    'gl_FragColor = vec4( color * 1.5, 1 );',
    '}',
  
  ].join('\n');
  
  const initMaterial = (function() {
    let material;
    const textureLoader = new THREE.TextureLoader();
  
    return function() {
      if (!!material) return material;
  
      const nzw = textureLoader.load(VolumetricFire.texturePath + 'nzw.png');
      nzw.wrapS = THREE.RepeatWrapping;
      nzw.wrapT = THREE.RepeatWrapping;
      nzw.magFilter = THREE.LinearFilter;
      nzw.minFilter = THREE.LinearFilter;
  
      const fireProfile = textureLoader.load(VolumetricFire.texturePath + 'firetex.png');
      fireProfile.wrapS = THREE.ClampToEdgeWrapping;
      fireProfile.wrapT = THREE.ClampToEdgeWrapping;
      fireProfile.magFilter = THREE.LinearFilter;
      fireProfile.minFilter = THREE.LinearFilter;
  
      const uniforms = {
        nzw: {
          type: 't',
          value: nzw
        },
        fireProfile: {
          type: 't',
          value: fireProfile
        },
        time: {
          type: 'f',
          value: 1.0
        }
      };
  
      material = new THREE.RawShaderMaterial({
        vertexShader: vs,
        fragmentShader: fs,
        uniforms: uniforms,
        side: THREE.DoubleSide,
        blending: THREE.AdditiveBlending,
        transparent: true
      });
      return material;
    };
  })();
  
  
  const cornerNeighbors = [
    [1, 2, 4],
    [0, 5, 3],
    [0, 3, 6],
    [1, 7, 2],
    [0, 6, 5],
    [1, 4, 7],
    [2, 7, 4],
    [3, 5, 6],
  ];
  
  const incomingEdges = [
    [-1, 2, 4, -1, 1, -1, -1, -1],
    [5, -1, -1, 0, -1, 3, -1, -1],
    [3, -1, -1, 6, -1, -1, 0, -1],
    [-1, 7, 1, -1, -1, -1, -1, 2],
    [6, -1, -1, -1, -1, 0, 5, -1],
    [-1, 4, -1, -1, 7, -1, -1, 1],
    [-1, -1, 7, -1, 2, -1, -1, 4],
    [-1, -1, -1, 5, -1, 6, 3, -1],
  ];
  
  export const VolumetricFire = function(width, height, depth, sliceSpacing, camera) {
    this.camera = camera;
    this._sliceSpacing = sliceSpacing;
  
    const widthHalf = width * 0.5;
    const heightHalf = height * 0.5;
    const depthHalf = depth * 0.5;
  
    this._posCorners = [
      new THREE.Vector3(-widthHalf, -heightHalf, -depthHalf),
      new THREE.Vector3(widthHalf, -heightHalf, -depthHalf),
      new THREE.Vector3(-widthHalf, heightHalf, -depthHalf),
      new THREE.Vector3(widthHalf, heightHalf, -depthHalf),
      new THREE.Vector3(-widthHalf, -heightHalf, depthHalf),
      new THREE.Vector3(widthHalf, -heightHalf, depthHalf),
      new THREE.Vector3(-widthHalf, heightHalf, depthHalf),
      new THREE.Vector3(widthHalf, heightHalf, depthHalf)
    ];
    this._texCorners = [
      new THREE.Vector3(0, 0, 0),
      new THREE.Vector3(1, 0, 0),
      new THREE.Vector3(0, 1, 0),
      new THREE.Vector3(1, 1, 0),
      new THREE.Vector3(0, 0, 1),
      new THREE.Vector3(1, 0, 1),
      new THREE.Vector3(0, 1, 1),
      new THREE.Vector3(1, 1, 1)
    ];
  
    this._viewVector = new THREE.Vector3();
  
    const index = new Uint16Array((width + height + depth) * 30);
    const position = new Float32Array((width + height + depth) * 30 * 3);
    const tex = new Float32Array((width + height + depth) * 30 * 3);
  
    const geometry = new THREE.BufferGeometry();
    geometry.dynamic = true;
    geometry.setIndex(new THREE.BufferAttribute(index, 1));
    geometry.addAttribute('position', new THREE.BufferAttribute(position, 3));
    geometry.addAttribute('tex', new THREE.BufferAttribute(tex, 3));
  
    const material = initMaterial();
  
    this.mesh = new THREE.Mesh(
      geometry,
      material
    );
    this.mesh.updateMatrixWorld();
  
  }
  
  VolumetricFire.prototype.update = function(elapsed) {
    this.updateViewVector();
    this.slice();
    this.updateGeometry();
    this.mesh.material.uniforms.time.value = elapsed;
  }
  
  VolumetricFire.prototype.updateGeometry = function() {
    this.mesh.geometry.index.array.set(this._indexes);
    this.mesh.geometry.attributes.position.array.set(this._points);
    this.mesh.geometry.attributes.tex.array.set(this._texCoords);
  
    this.mesh.geometry.index.needsUpdate = true;
    this.mesh.geometry.attributes.position.needsUpdate = true;
    this.mesh.geometry.attributes.tex.needsUpdate = true;
  }
  
  VolumetricFire.prototype.updateViewVector = function() {
    const modelViewMatrix = new THREE.Matrix4();
  
    modelViewMatrix.multiplyMatrices(
      this.camera.matrixWorldInverse,
      this.mesh.matrixWorld
    );
  
    this._viewVector.set(-modelViewMatrix.elements[2], -modelViewMatrix.elements[6], -modelViewMatrix.elements[10]).normalize();
  };
  
  VolumetricFire.prototype.slice = function() {
    this._points = [];
    this._texCoords = [];
    this._indexes = [];
  
    let i;
    let cornerDistance0 = this._posCorners[0].dot(this._viewVector);
  
    let cornerDistance = [cornerDistance0];
    let maxCorner = 0;
    let minDistance = cornerDistance0;
    let maxDistance = cornerDistance0;
  
    for (i = 1; i < 8; i = (i + 1) | 0) {
      cornerDistance[i] = this._posCorners[i].dot(this._viewVector);
  
      if (cornerDistance[i] > maxDistance) {
        maxCorner = i;
        maxDistance = cornerDistance[i];
      }
      if (cornerDistance[i] < minDistance) {
        minDistance = cornerDistance[i];
      }
    }
  
    // Aligning slices
    let sliceDistance = Math.floor(maxDistance / this._sliceSpacing) * this._sliceSpacing;
  
    let activeEdges = [];
    let firstEdge = 0;
    let nextEdge = 0;
    let expirations = new PriorityQueue();
  
    const createEdge = function(startIndex, endIndex) {
      if (nextEdge >= 12) return undefined;
  
      const activeEdge = {
        expired: false,
        startIndex: startIndex,
        endIndex: endIndex,
        deltaPos: new THREE.Vector3(),
        deltaTex: new THREE.Vector3(),
        pos: new THREE.Vector3(),
        tex: new THREE.Vector3(),
        cur: nextEdge
      }
  
      const range = cornerDistance[startIndex] - cornerDistance[endIndex];
  
      if (range !== 0.0) {
        let irange = 1.0 / range;
  
        activeEdge.deltaPos.subVectors(
          this._posCorners[endIndex],
          this._posCorners[startIndex]
        ).multiplyScalar(irange);
  
        activeEdge.deltaTex.subVectors(
          this._texCorners[endIndex],
          this._texCorners[startIndex]
        ).multiplyScalar(irange);
  
        let step = cornerDistance[startIndex] - sliceDistance;
  
        activeEdge.pos.addVectors(
          activeEdge.deltaPos.clone().multiplyScalar(step),
          this._posCorners[startIndex]
        );
  
        activeEdge.tex.addVectors(
          activeEdge.deltaTex.clone().multiplyScalar(step),
          this._texCorners[startIndex]
        );
  
        activeEdge.deltaPos.multiplyScalar(this._sliceSpacing);
        activeEdge.deltaTex.multiplyScalar(this._sliceSpacing);
      }
      expirations.push(activeEdge, cornerDistance[endIndex]);
      activeEdges[nextEdge++] = activeEdge;
      return activeEdge;
  
    };
  
    for (i = 0; i < 3; i = (i + 1) | 0) {
      let activeEdge = createEdge.call(this, maxCorner, cornerNeighbors[maxCorner][i]);
      activeEdge.prev = (i + 2) % 3;
      activeEdge.next = (i + 1) % 3;
    }
  
    let nextIndex = 0;
  
    while (sliceDistance > minDistance) {
      while (expirations.top().priority >= sliceDistance) {
        let edge = expirations.pop().object;
        if (edge.expired) {
          continue; }
        if (
          edge.endIndex !== activeEdges[edge.prev].endIndex &&
          edge.endIndex !== activeEdges[edge.next].endIndex
        ) {
          // split this edge.
          edge.expired = true;
          // create two new edges.
          let activeEdge1 = createEdge.call(
            this,
            edge.endIndex,
            incomingEdges[edge.endIndex][edge.startIndex]
          );
          activeEdge1.prev = edge.prev;
          activeEdges[edge.prev].next = nextEdge - 1;
          activeEdge1.next = nextEdge;
  
          let activeEdge2 = createEdge.call(
            this,
            edge.endIndex,
            incomingEdges[edge.endIndex][activeEdge1.endIndex]
          );
          activeEdge2.prev = nextEdge - 2;
          activeEdge2.next = edge.next;
          activeEdges[activeEdge2.next].prev = nextEdge - 1;
          firstEdge = nextEdge - 1;
        } else {
          // merge edge.
          let prev;
          let next;
          if (edge.endIndex === activeEdges[edge.prev].endIndex) {
            prev = activeEdges[edge.prev];
            next = edge;
          } else {
            prev = edge;
            next = activeEdges[edge.next];
          }
  
          prev.expired = true;
          next.expired = true;
  
          // make new edge
          let activeEdge = createEdge.call(
            this,
            edge.endIndex,
            incomingEdges[edge.endIndex][prev.startIndex]
          );
          activeEdge.prev = prev.prev;
          activeEdges[activeEdge.prev].next = nextEdge - 1;
          activeEdge.next = next.next;
          activeEdges[activeEdge.next].prev = nextEdge - 1;
          firstEdge = nextEdge - 1;
        }
      }
  
      let cur = firstEdge;
      let count = 0;
  
      do {
        ++count;
        let activeEdge = activeEdges[cur];
        this._points.push(
          activeEdge.pos.x,
          activeEdge.pos.y,
          activeEdge.pos.z
        );
        this._texCoords.push(
          activeEdge.tex.x,
          activeEdge.tex.y,
          activeEdge.tex.z
        );
        activeEdge.pos.add(activeEdge.deltaPos);
        activeEdge.tex.add(activeEdge.deltaTex);
        cur = activeEdge.next;
      } while (cur !== firstEdge);
  
      for (i = 2; i < count; i = (i + 1) | 0) {
        this._indexes.push(
          nextIndex,
          nextIndex + i - 1,
          nextIndex + i
        );
      }
      nextIndex += count;
      sliceDistance -= this._sliceSpacing;
    }
  };
  
  VolumetricFire.texturePath = 'images/';
  
  const PriorityQueue = function() {
    this.contents = [];
    this.sorted = false;
  
  };
  
  PriorityQueue.prototype = {
    sort: function() {
      this.contents.sort();
      this.sorted = true;
    },
  
    pop: function() {
      if (!this.sorted) {
        this.sort();
      }
      return this.contents.pop();
    },
  
    top: function() {
      if (!this.sorted) {
        this.sort();
      }
      return this.contents[this.contents.length - 1];
    },
  
    push: function(object, priority) {
      this.contents.push({ object: object, priority: priority });
      this.sorted = false;
    }
  };
  