text: // Copyright 2018 The Immersive Web Community Group
  //
  // Permission is hereby granted, free of charge, to any person obtaining a copy
  // of this software and associated documentation files (the "Software"), to deal
  // in the Software without restriction, including without limitation the rights
  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  // copies of the Software, and to permit persons to whom the Software is
  // furnished to do so, subject to the following conditions:
  
  // The above copyright notice and this permission notice shall be included in
  // all copies or substantial portions of the Software.
  
  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  // SOFTWARE.
  
  import { Material, RENDER_ORDER } from '../core/material.js';
  import { Node } from '../core/node.js';
  import { Primitive, PrimitiveAttribute } from '../core/primitive.js';
  import { DataTexture } from '../core/texture.js';
  import { Gltf2Node } from '../nodes/gltf2.js';
  
  // This library matches XRInputSource profiles to available controller models for us.
  // import { fetchProfile } from 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/motion-controllers@1.0/dist/motion-controllers.module.js';
  import { fetchProfile } from '../../third-party/motion-controllers.module.js'
  // The path of the CDN the sample will fetch controller models from.
  const DEFAULT_PROFILES_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles';
  
  const GL = WebGLRenderingContext; // For enums
  
  // Laser texture data, 48x1 RGBA (not premultiplied alpha). This represents a
  // "cross section" of the laser beam with a bright core and a feathered edge.
  // Borrowed from Chromium source code.
  const LASER_TEXTURE_DATA = new Uint8Array([
    0xff, 0xff, 0xff, 0x01, 0xff, 0xff, 0xff, 0x02, 0xbf, 0xbf, 0xbf, 0x04, 0xcc, 0xcc, 0xcc, 0x05,
    0xdb, 0xdb, 0xdb, 0x07, 0xcc, 0xcc, 0xcc, 0x0a, 0xd8, 0xd8, 0xd8, 0x0d, 0xd2, 0xd2, 0xd2, 0x11,
    0xce, 0xce, 0xce, 0x15, 0xce, 0xce, 0xce, 0x1a, 0xce, 0xce, 0xce, 0x1f, 0xcd, 0xcd, 0xcd, 0x24,
    0xc8, 0xc8, 0xc8, 0x2a, 0xc9, 0xc9, 0xc9, 0x2f, 0xc9, 0xc9, 0xc9, 0x34, 0xc9, 0xc9, 0xc9, 0x39,
    0xc9, 0xc9, 0xc9, 0x3d, 0xc8, 0xc8, 0xc8, 0x41, 0xcb, 0xcb, 0xcb, 0x44, 0xee, 0xee, 0xee, 0x87,
    0xfa, 0xfa, 0xfa, 0xc8, 0xf9, 0xf9, 0xf9, 0xc9, 0xf9, 0xf9, 0xf9, 0xc9, 0xfa, 0xfa, 0xfa, 0xc9,
    0xfa, 0xfa, 0xfa, 0xc9, 0xf9, 0xf9, 0xf9, 0xc9, 0xf9, 0xf9, 0xf9, 0xc9, 0xfa, 0xfa, 0xfa, 0xc8,
    0xee, 0xee, 0xee, 0x87, 0xcb, 0xcb, 0xcb, 0x44, 0xc8, 0xc8, 0xc8, 0x41, 0xc9, 0xc9, 0xc9, 0x3d,
    0xc9, 0xc9, 0xc9, 0x39, 0xc9, 0xc9, 0xc9, 0x34, 0xc9, 0xc9, 0xc9, 0x2f, 0xc8, 0xc8, 0xc8, 0x2a,
    0xcd, 0xcd, 0xcd, 0x24, 0xce, 0xce, 0xce, 0x1f, 0xce, 0xce, 0xce, 0x1a, 0xce, 0xce, 0xce, 0x15,
    0xd2, 0xd2, 0xd2, 0x11, 0xd8, 0xd8, 0xd8, 0x0d, 0xcc, 0xcc, 0xcc, 0x0a, 0xdb, 0xdb, 0xdb, 0x07,
    0xcc, 0xcc, 0xcc, 0x05, 0xbf, 0xbf, 0xbf, 0x04, 0xff, 0xff, 0xff, 0x02, 0xff, 0xff, 0xff, 0x01,
  ]);
  
  const LASER_LENGTH = 1.0;
  const LASER_DIAMETER = 0.01;
  const LASER_FADE_END = 0.535;
  const LASER_FADE_POINT = 0.5335;
  const LASER_DEFAULT_COLOR = [1.0, 1.0, 1.0, 0.25];
  
  const CURSOR_RADIUS = 0.004;
  const CURSOR_SHADOW_RADIUS = 0.007;
  const CURSOR_SHADOW_INNER_LUMINANCE = 0.5;
  const CURSOR_SHADOW_OUTER_LUMINANCE = 0.0;
  const CURSOR_SHADOW_INNER_OPACITY = 0.75;
  const CURSOR_SHADOW_OUTER_OPACITY = 0.0;
  const CURSOR_OPACITY = 0.9;
  const CURSOR_SEGMENTS = 16;
  const CURSOR_DEFAULT_COLOR = [1.0, 1.0, 1.0, 1.0];
  const CURSOR_DEFAULT_HIDDEN_COLOR = [0.5, 0.5, 0.5, 0.25];
  
  const DEFAULT_RESET_OPTIONS = {
    controllers: true,
    lasers: true,
    cursors: true,
  };
  
  class LaserMaterial extends Material {
    constructor() {
      super();
      this.renderOrder = RENDER_ORDER.ADDITIVE;
      this.state.cullFace = false;
      this.state.blend = true;
      this.state.blendFuncSrc = GL.ONE;
      this.state.blendFuncDst = GL.ONE;
      this.state.depthMask = false;
  
      this.laser = this.defineSampler('diffuse');
      this.laser.texture = new DataTexture(LASER_TEXTURE_DATA, 48, 1);
      this.laserColor = this.defineUniform('laserColor', LASER_DEFAULT_COLOR);
    }
  
    get materialName() {
      return 'INPUT_LASER';
    }
  
    get vertexSource() {
      return `
      attribute vec3 POSITION;
      attribute vec2 TEXCOORD_0;
  
      varying vec2 vTexCoord;
  
      vec4 vertex_main(mat4 proj, mat4 view, mat4 model) {
        vTexCoord = TEXCOORD_0;
        return proj * view * model * vec4(POSITION, 1.0);
      }`;
    }
  
    get fragmentSource() {
      return `
      precision mediump float;
  
      uniform vec4 laserColor;
      uniform sampler2D diffuse;
      varying vec2 vTexCoord;
  
      const float fadePoint = ${LASER_FADE_POINT};
      const float fadeEnd = ${LASER_FADE_END};
  
      vec4 fragment_main() {
        vec2 uv = vTexCoord;
        float front_fade_factor = 1.0 - clamp(1.0 - (uv.y - fadePoint) / (1.0 - fadePoint), 0.0, 1.0);
        float back_fade_factor = clamp((uv.y - fadePoint) / (fadeEnd - fadePoint), 0.0, 1.0);
        vec4 color = laserColor * texture2D(diffuse, vTexCoord);
        float opacity = color.a * front_fade_factor * back_fade_factor;
        return vec4(color.rgb * opacity, opacity);
      }`;
    }
  }
  
  const CURSOR_VERTEX_SHADER = `
  attribute vec4 POSITION;
  
  varying float vLuminance;
  varying float vOpacity;
  
  vec4 vertex_main(mat4 proj, mat4 view, mat4 model) {
    vLuminance = POSITION.z;
    vOpacity = POSITION.w;
  
    // Billboarded, constant size vertex transform.
    vec4 screenPos = proj * view * model * vec4(0.0, 0.0, 0.0, 1.0);
    screenPos /= screenPos.w;
    screenPos.xy += POSITION.xy;
    return screenPos;
  }`;
  
  const CURSOR_FRAGMENT_SHADER = `
  precision mediump float;
  
  uniform vec4 cursorColor;
  varying float vLuminance;
  varying float vOpacity;
  
  vec4 fragment_main() {
    vec3 color = cursorColor.rgb * vLuminance;
    float opacity = cursorColor.a * vOpacity;
    return vec4(color * opacity, opacity);
  }`;
  
  // Cursors are drawn as billboards that always face the camera and are rendered
  // as a fixed size no matter how far away they are.
  class CursorMaterial extends Material {
    constructor() {
      super();
      this.renderOrder = RENDER_ORDER.ADDITIVE;
      this.state.cullFace = false;
      this.state.blend = true;
      this.state.blendFuncSrc = GL.ONE;
      this.state.depthMask = false;
  
      this.cursorColor = this.defineUniform('cursorColor', CURSOR_DEFAULT_COLOR);
    }
  
    get materialName() {
      return 'INPUT_CURSOR';
    }
  
    get vertexSource() {
      return CURSOR_VERTEX_SHADER;
    }
  
    get fragmentSource() {
      return CURSOR_FRAGMENT_SHADER;
    }
  }
  
  class CursorHiddenMaterial extends Material {
    constructor() {
      super();
      this.renderOrder = RENDER_ORDER.ADDITIVE;
      this.state.cullFace = false;
      this.state.blend = true;
      this.state.blendFuncSrc = GL.ONE;
      this.state.depthFunc = GL.GEQUAL;
      this.state.depthMask = false;
  
      this.cursorColor = this.defineUniform('cursorColor', CURSOR_DEFAULT_HIDDEN_COLOR);
    }
  
    // TODO: Rename to "program_name"
    get materialName() {
      return 'INPUT_CURSOR_2';
    }
  
    get vertexSource() {
      return CURSOR_VERTEX_SHADER;
    }
  
    get fragmentSource() {
      return CURSOR_FRAGMENT_SHADER;
    }
  }
  
  export class InputRenderer extends Node {
    constructor() {
      super();
  
      this._maxInputElements = 32;
  
      this._controllers = null;
      this._lasers = null;
      this._cursors = null;
  
      this._activeControllers = 0;
      this._activeLasers = 0;
      this._activeCursors = 0;
    }
  
    onRendererChanged(renderer) {
      this._controllers = null;
      this._lasers = null;
      this._cursors = null;
  
      this._activeControllers = 0;
      this._activeLasers = 0;
      this._activeCursors = 0;
    }
  
    useProfileControllerMeshes(session) {
      // As input sources are connected if they are tracked-pointer devices
      // look up which meshes should be associated with their profile and
      // load as the controller model for that hand.
      session.addEventListener('inputsourceschange', (event) => {
        for (let inputSource of event.added) {
          if (inputSource.targetRayMode == 'tracked-pointer') {
            fetchProfile(inputSource, DEFAULT_PROFILES_PATH).then(({ profile, assetPath }) => {
              this.setControllerMesh(new Gltf2Node({ url: assetPath }), inputSource.handedness);
            });
          }
        }
      });
    }
  
    setControllerMesh(controllerNode, handedness = 'right') {
      if (!this._controllers) {
        this._controllers = {};
      }
      this._controllers[handedness] = { nodes: [controllerNode], activeCount: 0 };
      controllerNode.visible = false;
      // FIXME: Temporary fix to initialize for cloning.
      this.addNode(controllerNode);
    }
  
    addController(gripMatrix, handedness = 'right') {
      if (!this._controllers) { return; }
      let controller = this._controllers[handedness];
  
      if (!controller) { return; }
  
      let controllerNode = null;
      if (controller.activeCount < controller.nodes.length) {
        controllerNode = controller.nodes[controller.activeCount];
      } else {
        controllerNode = controller.nodes[0].clone();
        this.addNode(controllerNode);
        controller.nodes.push(controllerNode);
      }
      controller.activeCount = (controller.activeCount + 1) % this._maxInputElements;
  
      controllerNode.matrix = gripMatrix;
      controllerNode.visible = true;
    }
  
    addLaserPointer(rigidTransform) {
      // Create the laser pointer mesh if needed.
      if (!this._lasers && this._renderer) {
        this._lasers = [this._createLaserMesh()];
        this.addNode(this._lasers[0]);
      }
  
      let laser = null;
      if (this._activeLasers < this._lasers.length) {
        laser = this._lasers[this._activeLasers];
      } else {
        laser = this._lasers[0].clone();
        this.addNode(laser);
        this._lasers.push(laser);
      }
      this._activeLasers = (this._activeLasers + 1) % this._maxInputElements;
  
      laser.matrix = rigidTransform.matrix;
      laser.visible = true;
    }
  
    addCursor(cursorPos) {
      // Create the cursor mesh if needed.
      if (!this._cursors && this._renderer) {
        this._cursors = [this._createCursorMesh()];
        this.addNode(this._cursors[0]);
      }
  
      let cursor = null;
      if (this._activeCursors < this._cursors.length) {
        cursor = this._cursors[this._activeCursors];
      } else {
        cursor = this._cursors[0].clone();
        this.addNode(cursor);
        this._cursors.push(cursor);
      }
      this._activeCursors = (this._activeCursors + 1) % this._maxInputElements;
  
      cursor.translation = cursorPos;
      cursor.visible = true;
    }
  
    reset(options) {
      if (!options) {
        options = DEFAULT_RESET_OPTIONS;
      }
      if (this._controllers && options.controllers) {
        for (let handedness in this._controllers) {
          let controller = this._controllers[handedness];
          controller.activeCount = 0;
          for (let controllerNode of controller.nodes) {
            controllerNode.visible = false;
          }
        }
      }
      if (this._lasers && options.lasers) {
        for (let laser of this._lasers) {
          laser.visible = false;
        }
        this._activeLasers = 0;
      }
      if (this._cursors && options.cursors) {
        for (let cursor of this._cursors) {
          cursor.visible = false;
        }
        this._activeCursors = 0;
      }
    }
  
    _createLaserMesh() {
      let gl = this._renderer._gl;
  
      let lr = LASER_DIAMETER * 0.5;
      let ll = LASER_LENGTH;
  
      // Laser is rendered as cross-shaped beam
      let laserVerts = [
        // X    Y   Z    U    V
        0.0, lr, 0.0, 0.0, 1.0,
        0.0, lr, -ll, 0.0, 0.0,
        0.0, -lr, 0.0, 1.0, 1.0,
        0.0, -lr, -ll, 1.0, 0.0,
  
        lr, 0.0, 0.0, 0.0, 1.0,
        lr, 0.0, -ll, 0.0, 0.0,
        -lr, 0.0, 0.0, 1.0, 1.0,
        -lr, 0.0, -ll, 1.0, 0.0,
  
        0.0, -lr, 0.0, 0.0, 1.0,
        0.0, -lr, -ll, 0.0, 0.0,
        0.0, lr, 0.0, 1.0, 1.0,
        0.0, lr, -ll, 1.0, 0.0,
  
        -lr, 0.0, 0.0, 0.0, 1.0,
        -lr, 0.0, -ll, 0.0, 0.0,
        lr, 0.0, 0.0, 1.0, 1.0,
        lr, 0.0, -ll, 1.0, 0.0,
      ];
      let laserIndices = [
        0, 1, 2, 1, 3, 2,
        4, 5, 6, 5, 7, 6,
        8, 9, 10, 9, 11, 10,
        12, 13, 14, 13, 15, 14,
      ];
  
      let laserVertexBuffer = this._renderer.createRenderBuffer(gl.ARRAY_BUFFER, new Float32Array(laserVerts));
      let laserIndexBuffer = this._renderer.createRenderBuffer(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(laserIndices));
  
      let laserIndexCount = laserIndices.length;
  
      let laserAttribs = [
        new PrimitiveAttribute('POSITION', laserVertexBuffer, 3, gl.FLOAT, 20, 0),
        new PrimitiveAttribute('TEXCOORD_0', laserVertexBuffer, 2, gl.FLOAT, 20, 12),
      ];
  
      let laserPrimitive = new Primitive(laserAttribs, laserIndexCount);
      laserPrimitive.setIndexBuffer(laserIndexBuffer);
  
      let laserMaterial = new LaserMaterial();
  
      let laserRenderPrimitive = this._renderer.createRenderPrimitive(laserPrimitive, laserMaterial);
      let meshNode = new Node();
      meshNode.addRenderPrimitive(laserRenderPrimitive);
      return meshNode;
    }
  
    _createCursorMesh() {
      let gl = this._renderer._gl;
  
      // Cursor is a circular white dot with a dark "shadow" skirt around the edge
      // that fades from black to transparent as it moves out from the center.
      // Cursor verts are packed as [X, Y, Luminance, Opacity]
      let cursorVerts = [];
      let cursorIndices = [];
  
      let segRad = (2.0 * Math.PI) / CURSOR_SEGMENTS;
  
      // Cursor center
      for (let i = 0; i < CURSOR_SEGMENTS; ++i) {
        let rad = i * segRad;
        let x = Math.cos(rad);
        let y = Math.sin(rad);
        cursorVerts.push(x * CURSOR_RADIUS, y * CURSOR_RADIUS, 1.0, CURSOR_OPACITY);
  
        if (i > 1) {
          cursorIndices.push(0, i - 1, i);
        }
      }
  
      let indexOffset = CURSOR_SEGMENTS;
  
      // Cursor Skirt
      for (let i = 0; i < CURSOR_SEGMENTS; ++i) {
        let rad = i * segRad;
        let x = Math.cos(rad);
        let y = Math.sin(rad);
        cursorVerts.push(x * CURSOR_RADIUS, y * CURSOR_RADIUS,
          CURSOR_SHADOW_INNER_LUMINANCE, CURSOR_SHADOW_INNER_OPACITY);
        cursorVerts.push(x * CURSOR_SHADOW_RADIUS, y * CURSOR_SHADOW_RADIUS,
          CURSOR_SHADOW_OUTER_LUMINANCE, CURSOR_SHADOW_OUTER_OPACITY);
  
        if (i > 0) {
          let idx = indexOffset + (i * 2);
          cursorIndices.push(idx - 2, idx - 1, idx);
          cursorIndices.push(idx - 1, idx + 1, idx);
        }
      }
  
      let idx = indexOffset + (CURSOR_SEGMENTS * 2);
      cursorIndices.push(idx - 2, idx - 1, indexOffset);
      cursorIndices.push(idx - 1, indexOffset + 1, indexOffset);
  
      let cursorVertexBuffer = this._renderer.createRenderBuffer(gl.ARRAY_BUFFER, new Float32Array(cursorVerts));
      let cursorIndexBuffer = this._renderer.createRenderBuffer(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cursorIndices));
  
      let cursorIndexCount = cursorIndices.length;
  
      let cursorAttribs = [
        new PrimitiveAttribute('POSITION', cursorVertexBuffer, 4, gl.FLOAT, 16, 0),
      ];
  
      let cursorPrimitive = new Primitive(cursorAttribs, cursorIndexCount);
      cursorPrimitive.setIndexBuffer(cursorIndexBuffer);
  
      let cursorMaterial = new CursorMaterial();
      let cursorHiddenMaterial = new CursorHiddenMaterial();
  
      // Cursor renders two parts: The bright opaque cursor for areas where it's
      // not obscured and a more transparent, darker version for areas where it's
      // behind another object.
      let cursorRenderPrimitive = this._renderer.createRenderPrimitive(cursorPrimitive, cursorMaterial);
      let cursorHiddenRenderPrimitive = this._renderer.createRenderPrimitive(cursorPrimitive, cursorHiddenMaterial);
      let meshNode = new Node();
      meshNode.addRenderPrimitive(cursorRenderPrimitive);
      meshNode.addRenderPrimitive(cursorHiddenRenderPrimitive);
      return meshNode;
    }
  }
  