text: /**
   * glTF Loader
   * Specification https://github.com/KhronosGroup/glTF/blob/master/specification/README.md
   *
   * TODO Morph targets
   */
  import Base from '../core/Base';
  import util from '../core/util';
  import vendor from '../core/vendor';
  
  import Scene from '../Scene';
  import Material from '../Material';
  import StandardMaterial from '../StandardMaterial';
  import Mesh from '../Mesh';
  import Node from '../Node';
  import Texture from '../Texture';
  import Texture2D from '../Texture2D';
  import shaderLibrary from '../shader/library';
  import Skeleton from '../Skeleton';
  import Joint from '../Joint';
  import PerspectiveCamera from '../camera/Perspective';
  import OrthographicCamera from '../camera/Orthographic';
  import glenum from '../core/glenum';
  
  import BoundingBox from '../math/BoundingBox';
  
  import TrackClip from '../animation/TrackClip';
  import SamplerTrack from '../animation/SamplerTrack';
  
  import Geometry from '../Geometry';
  
  // Import builtin shader
  import '../shader/builtin';
  import Shader from '../Shader';
  
  var semanticAttributeMap = {
      'NORMAL': 'normal',
      'POSITION': 'position',
      'TEXCOORD_0': 'texcoord0',
      'TEXCOORD_1': 'texcoord1',
      'WEIGHTS_0': 'weight',
      'JOINTS_0': 'joint',
      'COLOR_0': 'color'
  };
  
  var ARRAY_CTOR_MAP = {
      5120: vendor.Int8Array,
      5121: vendor.Uint8Array,
      5122: vendor.Int16Array,
      5123: vendor.Uint16Array,
      5125: vendor.Uint32Array,
      5126: vendor.Float32Array
  };
  var SIZE_MAP = {
      SCALAR: 1,
      VEC2: 2,
      VEC3: 3,
      VEC4: 4,
      MAT2: 4,
      MAT3: 9,
      MAT4: 16
  };
  
  function getAccessorData(json, lib, accessorIdx, isIndices) {
      var accessorInfo = json.accessors[accessorIdx];
  
      var buffer = lib.bufferViews[accessorInfo.bufferView];
      var byteOffset = accessorInfo.byteOffset || 0;
      var ArrayCtor = ARRAY_CTOR_MAP[accessorInfo.componentType] || vendor.Float32Array;
  
      var size = SIZE_MAP[accessorInfo.type];
      if (size == null && isIndices) {
          size = 1;
      }
      var arr = new ArrayCtor(buffer, byteOffset, size * accessorInfo.count);
  
      var quantizeExtension = accessorInfo.extensions && accessorInfo.extensions['WEB3D_quantized_attributes'];
      if (quantizeExtension) {
          var decodedArr = new vendor.Float32Array(size * accessorInfo.count);
          var decodeMatrix = quantizeExtension.decodeMatrix;
          var decodeOffset;
          var decodeScale;
          var decodeOffset = new Array(size);
          var decodeScale = new Array(size);
          for (var k = 0; k < size; k++) {
              decodeOffset[k] = decodeMatrix[size * (size + 1) + k];
              decodeScale[k] = decodeMatrix[k * (size + 1) + k];
          }
          for (var i = 0; i < accessorInfo.count; i++) {
              for (var k = 0; k < size; k++) {
                  decodedArr[i * size + k] = arr[i * size + k] * decodeScale[k] + decodeOffset[k];
              }
          }
  
          arr = decodedArr;
      }
      return arr;
  }
  
  function base64ToBinary(input, charStart) {
      var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
      var lookup = new Uint8Array(130);
      for (var i = 0; i < chars.length; i++) {
          lookup[chars.charCodeAt(i)] = i;
      }
      // Ignore
      var len = input.length - charStart;
      if (input.charAt(len - 1) === '=') { len--; }
      if (input.charAt(len - 1) === '=') { len--; }
  
      var uarray = new Uint8Array((len / 4) * 3);
  
      for (var i = 0, j = charStart; i < uarray.length;) {
          var c1 = lookup[input.charCodeAt(j++)];
          var c2 = lookup[input.charCodeAt(j++)];
          var c3 = lookup[input.charCodeAt(j++)];
          var c4 = lookup[input.charCodeAt(j++)];
  
          uarray[i++] = (c1 << 2) | (c2 >> 4);
          uarray[i++] = ((c2 & 15) << 4) | (c3 >> 2);
          uarray[i++] = ((c3 & 3) << 6) | c4;
      }
  
      return uarray.buffer;
  }
  
  
  /**
   * @typedef {Object} clay.loader.GLTF.Result
   * @property {Object} json
   * @property {clay.Scene} scene
   * @property {clay.Node} rootNode
   * @property {clay.Camera[]} cameras
   * @property {clay.Texture[]} textures
   * @property {clay.Material[]} materials
   * @property {clay.Skeleton[]} skeletons
   * @property {clay.Mesh[]} meshes
   * @property {clay.animation.TrackClip[]} clips
   * @property {clay.Node[]} nodes
   */
  
  /**
   * @constructor clay.loader.GLTF
   * @extends clay.core.Base
   */
  var GLTFLoader = Base.extend(/** @lends clay.loader.GLTF# */ {
      /**
       *
       * @type {clay.Node}
       */
      rootNode: null,
      /**
       * Root path for uri parsing.
       * @type {string}
       */
      rootPath: null,
  
      /**
       * Root path for texture uri parsing. Defaultly use the rootPath
       * @type {string}
       */
      textureRootPath: null,
  
      /**
       * Root path for buffer uri parsing. Defaultly use the rootPath
       * @type {string}
       */
      bufferRootPath: null,
  
      /**
       * Shader used when creating the materials.
       * @type {string|clay.Shader}
       * @default 'clay.standard'
       */
      shader: 'clay.standard',
  
      /**
       * If use {@link clay.StandardMaterial}
       * @type {string}
       */
      useStandardMaterial: false,
  
      /**
       * If loading the cameras.
       * @type {boolean}
       */
      includeCamera: true,
  
      /**
       * If loading the animations.
       * @type {boolean}
       */
      includeAnimation: true,
      /**
       * If loading the meshes
       * @type {boolean}
       */
      includeMesh: true,
      /**
       * If loading the textures.
       * @type {boolean}
       */
      includeTexture: true,
  
      /**
       * @type {string}
       */
      crossOrigin: '',
      /**
       * @type {boolean}
       * @see https://github.com/KhronosGroup/glTF/issues/674
       */
      textureFlipY: false,
  
      /**
       * If convert texture to power-of-two
       * @type {boolean}
       */
      textureConvertToPOT: false,
  
      shaderLibrary: null
  },
  function () {
      if (!this.shaderLibrary) {
          this.shaderLibrary = shaderLibrary.createLibrary();
      }
  },
  /** @lends clay.loader.GLTF.prototype */
  {
      /**
       * @param {string} url
       */
      load: function (url) {
          var self = this;
          var isBinary = url.endsWith('.glb');
  
          if (this.rootPath == null) {
              this.rootPath = url.slice(0, url.lastIndexOf('/'));
          }
  
          vendor.request.get({
              url: url,
              onprogress: function (percent, loaded, total) {
                  self.trigger('progress', percent, loaded, total);
              },
              onerror: function (e) {
                  self.trigger('error', e);
              },
              responseType: isBinary ? 'arraybuffer' : 'text',
              onload: function (data) {
                  if (isBinary) {
                      self.parseBinary(data);
                  }
                  else {
                      if (typeof data === 'string') {
                          data = JSON.parse(data);
                      }
                      self.parse(data);
                  }
              }
          });
      },
  
      /**
       * Parse glTF binary
       * @param {ArrayBuffer} buffer
       * @return {clay.loader.GLTF.Result}
       */
      parseBinary: function (buffer) {
          var header = new Uint32Array(buffer, 0, 4);
          if (header[0] !== 0x46546C67) {
              this.trigger('error', 'Invalid glTF binary format: Invalid header');
              return;
          }
          if (header[0] < 2) {
              this.trigger('error', 'Only glTF2.0 is supported.');
              return;
          }
  
          var dataView = new DataView(buffer, 12);
  
          var json;
          var buffers = [];
          // Read chunks
          for (var i = 0; i < dataView.byteLength;) {
              var chunkLength = dataView.getUint32(i, true);
              i += 4;
              var chunkType = dataView.getUint32(i, true);
              i += 4;
  
              // json
              if (chunkType === 0x4E4F534A) {
                  var arr = new Uint8Array(buffer, i + 12, chunkLength);
                  // TODO, for the browser not support TextDecoder.
                  var decoder = new TextDecoder();
                  var str = decoder.decode(arr);
                  try {
                      json = JSON.parse(str);
                  }
                  catch (e) {
                      this.trigger('error', 'JSON Parse error:' + e.toString());
                      return;
                  }
              }
              else if (chunkType === 0x004E4942) {
                  buffers.push(buffer.slice(i + 12, i + 12 + chunkLength));
              }
  
              i += chunkLength;
          }
          if (!json) {
              this.trigger('error', 'Invalid glTF binary format: Can\'t find JSON.');
              return;
          }
  
          return this.parse(json, buffers);
      },
  
      /**
       * @param {Object} json
       * @param {ArrayBuffer[]} [buffer]
       * @return {clay.loader.GLTF.Result}
       */
      parse: function (json, buffers) {
          var self = this;
  
          var lib = {
              json: json,
              buffers: [],
              bufferViews: [],
              materials: [],
              textures: [],
              meshes: [],
              joints: [],
              skeletons: [],
              cameras: [],
              nodes: [],
              clips: []
          };
          // Mount on the root node if given
          var rootNode = this.rootNode || new Scene();
  
          var loading = 0;
          function checkLoad() {
              loading--;
              if (loading === 0) {
                  afterLoadBuffer();
              }
          }
          // If already load buffers
          if (buffers) {
              lib.buffers = buffers.slice();
              afterLoadBuffer(true);
          }
          else {
              // Load buffers
              util.each(json.buffers, function (bufferInfo, idx) {
                  loading++;
                  var path = bufferInfo.uri;
  
                  self._loadBuffers(path, function (buffer) {
                      lib.buffers[idx] = buffer;
                      checkLoad();
                  }, checkLoad);
              });
          }
  
          function getResult() {
              return {
                  json: json,
                  scene: self.rootNode ? null : rootNode,
                  rootNode: self.rootNode ? rootNode : null,
                  cameras: lib.cameras,
                  textures: lib.textures,
                  materials: lib.materials,
                  skeletons: lib.skeletons,
                  meshes: lib.instancedMeshes,
                  clips: lib.clips,
                  nodes: lib.nodes
              };
          }
  
          function afterLoadBuffer(immediately) {
              // Buffer not load complete.
              if (lib.buffers.length !== json.buffers.length) {
                  setTimeout(function () {
                      self.trigger('error', 'Buffer not load complete.');
                  });
                  return;
              }
  
              json.bufferViews.forEach(function (bufferViewInfo, idx) {
                  // PENDING Performance
                  lib.bufferViews[idx] = lib.buffers[bufferViewInfo.buffer]
                      .slice(bufferViewInfo.byteOffset || 0, (bufferViewInfo.byteOffset || 0) + (bufferViewInfo.byteLength || 0));
              });
              lib.buffers = null;
              if (self.includeMesh) {
                  if (self.includeTexture) {
                      self._parseTextures(json, lib);
                  }
                  self._parseMaterials(json, lib);
                  self._parseMeshes(json, lib);
              }
              self._parseNodes(json, lib);
  
              // Only support one scene.
              if (json.scenes) {
                  var sceneInfo = json.scenes[json.scene || 0]; // Default use the first scene.
                  if (sceneInfo) {
                      for (var i = 0; i < sceneInfo.nodes.length; i++) {
                          var node = lib.nodes[sceneInfo.nodes[i]];
                          node.update();
                          rootNode.add(node);
                      }
                  }
              }
  
              if (self.includeMesh) {
                  self._parseSkins(json, lib);
              }
  
              if (self.includeAnimation) {
                  self._parseAnimations(json, lib);
              }
              if (immediately) {
                  setTimeout(function () {
                      self.trigger('success', getResult());
                  });
              }
              else {
                  self.trigger('success', getResult());
              }
          }
  
          return getResult();
      },
  
      /**
       * Binary file path resolver. User can override it
       * @param {string} path
       */
      resolveBufferPath: function (path) {
          if (path && path.match(/^data:(.*?)base64,/)) {
              return path;
          }
  
          var rootPath = this.bufferRootPath;
          if (rootPath == null) {
              rootPath = this.rootPath;
          }
          return util.relative2absolute(path, rootPath);
      },
  
      /**
       * Texture file path resolver. User can override it
       * @param {string} path
       */
      resolveTexturePath: function (path) {
          if (path && path.match(/^data:(.*?)base64,/)) {
              return path;
          }
  
          var rootPath = this.textureRootPath;
          if (rootPath == null) {
              rootPath = this.rootPath;
          }
          return util.relative2absolute(path, rootPath);
      },
  
      /**
       * Buffer loader
       * @param {string}
       * @param {Function} onsuccess
       * @param {Function} onerror
       */
      loadBuffer: function (path, onsuccess, onerror) {
          vendor.request.get({
              url: path,
              responseType: 'arraybuffer',
              onload: function (buffer) {
                  onsuccess && onsuccess(buffer);
              },
              onerror: function (buffer) {
                  onerror && onerror(buffer);
              }
          });
      },
  
      _getShader: function () {
          if (typeof this.shader === 'string') {
              return this.shaderLibrary.get(this.shader);
          }
          else if (this.shader instanceof Shader) {
              return this.shader;
          }
      },
  
      _loadBuffers: function (path, onsuccess, onerror) {
          var base64Prefix = 'data:application/octet-stream;base64,';
          var strStart = path.substr(0, base64Prefix.length);
          if (strStart === base64Prefix) {
              onsuccess(
                  base64ToBinary(path, base64Prefix.length)
              );
          }
          else {
              this.loadBuffer(
                  this.resolveBufferPath(path),
                  onsuccess,
                  onerror
              );
          }
      },
  
      // https://github.com/KhronosGroup/glTF/issues/100
      // https://github.com/KhronosGroup/glTF/issues/193
      _parseSkins: function (json, lib) {
  
          // Create skeletons and joints
          var haveInvBindMatrices = false;
          util.each(json.skins, function (skinInfo, idx) {
              var skeleton = new Skeleton({
                  name: skinInfo.name
              });
              for (var i = 0; i < skinInfo.joints.length; i++) {
                  var nodeIdx = skinInfo.joints[i];
                  var node = lib.nodes[nodeIdx];
                  var joint = new Joint({
                      name: node.name,
                      node: node,
                      index: skeleton.joints.length
                  });
                  skeleton.joints.push(joint);
              }
              skeleton.relativeRootNode = lib.nodes[skinInfo.skeleton] || this.rootNode;
              if (skinInfo.inverseBindMatrices) {
                  haveInvBindMatrices = true;
                  var IBMInfo = json.accessors[skinInfo.inverseBindMatrices];
                  var buffer = lib.bufferViews[IBMInfo.bufferView];
  
                  var offset = IBMInfo.byteOffset || 0;
                  var size = IBMInfo.count * 16;
  
                  var array = new vendor.Float32Array(buffer, offset, size);
  
                  skeleton.setJointMatricesArray(array);
              }
              else {
                  skeleton.updateJointMatrices();
              }
              lib.skeletons[idx] = skeleton;
          }, this);
  
          function enableSkinningForMesh(mesh, skeleton, jointIndices) {
              mesh.skeleton = skeleton;
              mesh.joints = jointIndices;
  
              if (!skeleton.boundingBox) {
                  skeleton.updateJointsBoundingBoxes(mesh.geometry);
              }
          }
  
          function getJointIndex(joint) {
              return joint.index;
          }
  
          util.each(json.nodes, function (nodeInfo, nodeIdx) {
              if (nodeInfo.skin != null) {
                  var skinIdx = nodeInfo.skin;
                  var skeleton = lib.skeletons[skinIdx];
  
                  var node = lib.nodes[nodeIdx];
                  var jointIndices = skeleton.joints.map(getJointIndex);
                  if (node instanceof Mesh) {
                      enableSkinningForMesh(node, skeleton, jointIndices);
                  }
                  else {
                      // Mesh have multiple primitives
                      var children = node.children();
                      for (var i = 0; i < children.length; i++) {
                          enableSkinningForMesh(children[i], skeleton, jointIndices);
                      }
                  }
              }
          }, this);
      },
  
      _parseTextures: function (json, lib) {
          util.each(json.textures, function (textureInfo, idx){
              // samplers is optional
              var samplerInfo = (json.samplers && json.samplers[textureInfo.sampler]) || {};
              var parameters = {};
              ['wrapS', 'wrapT', 'magFilter', 'minFilter'].forEach(function (name) {
                  var value = samplerInfo[name];
                  if (value != null) {
                      parameters[name] = value;
                  }
              });
              util.defaults(parameters, {
                  wrapS: Texture.REPEAT,
                  wrapT: Texture.REPEAT,
                  flipY: this.textureFlipY,
                  convertToPOT: this.textureConvertToPOT
              });
  
              var target = textureInfo.target || glenum.TEXTURE_2D;
              var format = textureInfo.format;
              if (format != null) {
                  parameters.format = format;
              }
  
              if (target === glenum.TEXTURE_2D) {
                  var texture = new Texture2D(parameters);
                  var imageInfo = json.images[textureInfo.source];
                  var uri;
                  if (imageInfo.uri) {
                      uri = this.resolveTexturePath(imageInfo.uri);
                  }
                  else if (imageInfo.bufferView != null) {
                      uri = URL.createObjectURL(new Blob([lib.bufferViews[imageInfo.bufferView]], {
                          type: imageInfo.mimeType
                      }));
                  }
                  if (uri) {
                      texture.load(uri, this.crossOrigin);
                      lib.textures[idx] = texture;
                  }
              }
          }, this);
      },
  
      _KHRCommonMaterialToStandard: function (materialInfo, lib) {
          var uniforms = {};
          var commonMaterialInfo = materialInfo.extensions['KHR_materials_common'];
          uniforms = commonMaterialInfo.values || {};
  
          if (typeof uniforms.diffuse === 'number') {
              uniforms.diffuse = lib.textures[uniforms.diffuse] || null;
          }
          if (typeof uniforms.emission === 'number') {
              uniforms.emission = lib.textures[uniforms.emission] || null;
          }
  
          var enabledTextures = [];
          if (uniforms['diffuse'] instanceof Texture2D) {
              enabledTextures.push('diffuseMap');
          }
          if (materialInfo.normalTexture) {
              enabledTextures.push('normalMap');
          }
          if (uniforms['emission'] instanceof Texture2D) {
              enabledTextures.push('emissiveMap');
          }
          var material;
          var isStandardMaterial = this.useStandardMaterial;
          if (isStandardMaterial) {
              material = new StandardMaterial({
                  name: materialInfo.name,
                  doubleSided: materialInfo.doubleSided
              });
          }
          else {
              material = new Material({
                  name: materialInfo.name,
                  shader: this._getShader()
              });
  
              material.define('fragment', 'USE_ROUGHNESS');
              material.define('fragment', 'USE_METALNESS');
  
              if (materialInfo.doubleSided) {
                  material.define('fragment', 'DOUBLE_SIDED');
              }
          }
  
          if (uniforms.transparent) {
              material.depthMask = false;
              material.depthTest = true;
              material.transparent = true;
          }
  
          var diffuseProp = uniforms['diffuse'];
          if (diffuseProp) {
              // Color
              if (Array.isArray(diffuseProp)) {
                  diffuseProp = diffuseProp.slice(0, 3);
                  isStandardMaterial ? (material.color = diffuseProp)
                      : material.set('color', diffuseProp);
              }
              else { // Texture
                  isStandardMaterial ? (material.diffuseMap = diffuseProp)
                      : material.set('diffuseMap', diffuseProp);
              }
          }
          var emissionProp = uniforms['emission'];
          if (emissionProp != null) {
              // Color
              if (Array.isArray(emissionProp)) {
                  emissionProp = emissionProp.slice(0, 3);
                  isStandardMaterial ? (material.emission = emissionProp)
                      : material.set('emission', emissionProp);
              }
              else { // Texture
                  isStandardMaterial ? (material.emissiveMap = emissionProp)
                      : material.set('emissiveMap', emissionProp);
              }
          }
          if (materialInfo.normalTexture != null) {
              // TODO texCoord
              var normalTextureIndex = materialInfo.normalTexture.index;
              if (isStandardMaterial) {
                  material.normalMap = lib.textures[normalTextureIndex] || null;
              }
              else {
                  material.set('normalMap', lib.textures[normalTextureIndex] || null);
              }
          }
          if (uniforms['shininess'] != null) {
              var glossiness = Math.log(uniforms['shininess']) / Math.log(8192);
              // Uniform glossiness
              material.set('glossiness', glossiness);
              material.set('roughness', 1 - glossiness);
          }
          else {
              material.set('glossiness', 0.3);
              material.set('roughness', 0.3);
          }
          if (uniforms['specular'] != null) {
              material.set('specularColor', uniforms['specular'].slice(0, 3));
          }
          if (uniforms['transparency'] != null) {
              material.set('alpha', uniforms['transparency']);
          }
  
          return material;
      },
  
      _pbrMetallicRoughnessToStandard: function (materialInfo, metallicRoughnessMatInfo, lib) {
          var alphaTest = materialInfo.alphaMode === 'MASK';
  
          var isStandardMaterial = this.useStandardMaterial;
          var material;
          var diffuseMap, roughnessMap, metalnessMap, normalMap, emissiveMap, occlusionMap;
          var enabledTextures = [];
  
          /**
           * The scalar multiplier applied to each normal vector of the normal texture.
           *
           * @type {number}
           *
           * XXX This value is ignored if `materialInfo.normalTexture` is not specified.
           */
          var normalScale = 1.0;
  
          // TODO texCoord
          if (metallicRoughnessMatInfo.baseColorTexture) {
              diffuseMap = lib.textures[metallicRoughnessMatInfo.baseColorTexture.index] || null;
              diffuseMap && enabledTextures.push('diffuseMap');
          }
          if (metallicRoughnessMatInfo.metallicRoughnessTexture) {
              roughnessMap = metalnessMap = lib.textures[metallicRoughnessMatInfo.metallicRoughnessTexture.index] || null;
              roughnessMap && enabledTextures.push('metalnessMap', 'roughnessMap');
          }
          if (materialInfo.normalTexture) {
  
              normalMap = lib.textures[materialInfo.normalTexture.index] || null;
              normalMap && enabledTextures.push('normalMap');
  
              if (typeof materialInfo.normalTexture.scale === 'number') {
                  normalScale = materialInfo.normalTexture.scale;
              }
  
          }
          if (materialInfo.emissiveTexture) {
              emissiveMap = lib.textures[materialInfo.emissiveTexture.index] || null;
              emissiveMap && enabledTextures.push('emissiveMap');
          }
          if (materialInfo.occlusionTexture) {
              occlusionMap = lib.textures[materialInfo.occlusionTexture.index] || null;
              occlusionMap && enabledTextures.push('occlusionMap');
          }
          var baseColor = metallicRoughnessMatInfo.baseColorFactor || [1, 1, 1, 1];
  
          var commonProperties = {
              diffuseMap: diffuseMap || null,
              roughnessMap: roughnessMap || null,
              metalnessMap: metalnessMap || null,
              normalMap: normalMap || null,
              occlusionMap: occlusionMap || null,
              emissiveMap: emissiveMap || null,
              color: baseColor.slice(0, 3),
              alpha: baseColor[3],
              metalness: metallicRoughnessMatInfo.metallicFactor || 0,
              roughness: metallicRoughnessMatInfo.roughnessFactor || 0,
              emission: materialInfo.emissiveFactor || [0, 0, 0],
              emissionIntensity: 1,
              alphaCutoff: materialInfo.alphaCutoff || 0.5,
              normalScale: normalScale
          };
          if (commonProperties.roughnessMap) {
              // In glTF metallicFactor will do multiply, which is different from StandardMaterial.
              // So simply ignore it
              commonProperties.metalness = 0.5;
              commonProperties.roughness = 0.5;
          }
          if (isStandardMaterial) {
              material = new StandardMaterial(util.extend({
                  name: materialInfo.name,
                  alphaTest: alphaTest,
                  doubleSided: materialInfo.doubleSided,
                  // G channel
                  roughnessChannel: 1,
                  // B Channel
                  metalnessChannel: 2
              }, commonProperties));
          }
          else {
  
              material = new Material({
                  name: materialInfo.name,
                  shader: this._getShader()
              });
  
              material.define('fragment', 'USE_ROUGHNESS');
              material.define('fragment', 'USE_METALNESS');
              material.define('fragment', 'ROUGHNESS_CHANNEL', 1);
              material.define('fragment', 'METALNESS_CHANNEL', 2);
  
              material.define('fragment', 'DIFFUSEMAP_ALPHA_ALPHA');
  
              if (alphaTest) {
                  material.define('fragment', 'ALPHA_TEST');
              }
              if (materialInfo.doubleSided) {
                  material.define('fragment', 'DOUBLE_SIDED');
              }
  
              material.set(commonProperties);
          }
  
          if (materialInfo.alphaMode === 'BLEND') {
              material.depthMask = false;
              material.depthTest = true;
              material.transparent = true;
          }
  
          return material;
      },
  
      _pbrSpecularGlossinessToStandard: function (materialInfo, specularGlossinessMatInfo, lib) {
          var alphaTest = materialInfo.alphaMode === 'MASK';
  
          if (this.useStandardMaterial) {
              console.error('StandardMaterial doesn\'t support specular glossiness workflow yet');
          }
  
          var material;
          var diffuseMap, glossinessMap, specularMap, normalMap, emissiveMap, occlusionMap;
          var enabledTextures = [];
          // TODO texCoord
          if (specularGlossinessMatInfo.diffuseTexture) {
              diffuseMap = lib.textures[specularGlossinessMatInfo.diffuseTexture.index] || null;
              diffuseMap && enabledTextures.push('diffuseMap');
          }
          if (specularGlossinessMatInfo.specularGlossinessTexture) {
              glossinessMap = specularMap = lib.textures[specularGlossinessMatInfo.specularGlossinessTexture.index] || null;
              glossinessMap && enabledTextures.push('specularMap', 'glossinessMap');
          }
          if (materialInfo.normalTexture) {
              normalMap = lib.textures[materialInfo.normalTexture.index] || null;
              normalMap && enabledTextures.push('normalMap');
          }
          if (materialInfo.emissiveTexture) {
              emissiveMap = lib.textures[materialInfo.emissiveTexture.index] || null;
              emissiveMap && enabledTextures.push('emissiveMap');
          }
          if (materialInfo.occlusionTexture) {
              occlusionMap = lib.textures[materialInfo.occlusionTexture.index] || null;
              occlusionMap && enabledTextures.push('occlusionMap');
          }
          var diffuseColor = specularGlossinessMatInfo.diffuseFactor || [1, 1, 1, 1];
  
          var commonProperties = {
              diffuseMap: diffuseMap || null,
              glossinessMap: glossinessMap || null,
              specularMap: specularMap || null,
              normalMap: normalMap || null,
              emissiveMap: emissiveMap || null,
              occlusionMap: occlusionMap || null,
              color: diffuseColor.slice(0, 3),
              alpha: diffuseColor[3],
              specularColor: specularGlossinessMatInfo.specularFactor || [1, 1, 1],
              glossiness: specularGlossinessMatInfo.glossinessFactor || 0,
              emission: materialInfo.emissiveFactor || [0, 0, 0],
              emissionIntensity: 1,
              alphaCutoff: materialInfo.alphaCutoff == null ? 0.5 : materialInfo.alphaCutoff
          };
          if (commonProperties.glossinessMap) {
              // Ignore specularFactor
              commonProperties.glossiness = 0.5;
          }
          if (commonProperties.specularMap) {
              // Ignore specularFactor
              commonProperties.specularColor = [1, 1, 1];
          }
  
          material = new Material({
              name: materialInfo.name,
              shader: this._getShader()
          });
  
          material.define('fragment', 'GLOSSINESS_CHANNEL', 3);
          material.define('fragment', 'DIFFUSEMAP_ALPHA_ALPHA');
  
          if (alphaTest) {
              material.define('fragment', 'ALPHA_TEST');
          }
          if (materialInfo.doubleSided) {
              material.define('fragment', 'DOUBLE_SIDED');
          }
  
          material.set(commonProperties);
  
          if (materialInfo.alphaMode === 'BLEND') {
              material.depthMask = false;
              material.depthTest = true;
              material.transparent = true;
          }
  
          return material;
      },
  
      _parseMaterials: function (json, lib) {
          util.each(json.materials, function (materialInfo, idx) {
              if (materialInfo.extensions && materialInfo.extensions['KHR_materials_common']) {
                  lib.materials[idx] = this._KHRCommonMaterialToStandard(materialInfo, lib);
              }
              else if (materialInfo.extensions && materialInfo.extensions['KHR_materials_pbrSpecularGlossiness']) {
                  lib.materials[idx] = this._pbrSpecularGlossinessToStandard(materialInfo, materialInfo.extensions['KHR_materials_pbrSpecularGlossiness'], lib);
              }
              else {
                  lib.materials[idx] = this._pbrMetallicRoughnessToStandard(materialInfo, materialInfo.pbrMetallicRoughness || {}, lib);
              }
          }, this);
      },
  
      _parseMeshes: function (json, lib) {
          var self = this;
  
          util.each(json.meshes, function (meshInfo, idx) {
              lib.meshes[idx] = [];
              // Geometry
              for (var pp = 0; pp < meshInfo.primitives.length; pp++) {
                  var primitiveInfo = meshInfo.primitives[pp];
                  var geometry = new Geometry({
                      dynamic: false,
                      // PENDIGN
                      name: meshInfo.name,
                      boundingBox: new BoundingBox()
                  });
                  // Parse attributes
                  var semantics = Object.keys(primitiveInfo.attributes);
                  for (var ss = 0; ss < semantics.length; ss++) {
                      var semantic = semantics[ss];
                      var accessorIdx = primitiveInfo.attributes[semantic];
                      var attributeInfo = json.accessors[accessorIdx];
                      var attributeName = semanticAttributeMap[semantic];
                      if (!attributeName) {
                          continue;
                      }
                      var size = SIZE_MAP[attributeInfo.type];
                      var attributeArray = getAccessorData(json, lib, accessorIdx);
                      // WebGL attribute buffer not support uint32.
                      // Direct use Float32Array may also have issue.
                      if (attributeArray instanceof vendor.Uint32Array) {
                          attributeArray = new Float32Array(attributeArray);
                      }
                      if (semantic === 'WEIGHTS_0' && size === 4) {
                          // Weight data in QTEK has only 3 component, the last component can be evaluated since it is normalized
                          var weightArray = new attributeArray.constructor(attributeInfo.count * 3);
                          for (var i = 0; i < attributeInfo.count; i++) {
                              var i4 = i * 4, i3 = i * 3;
                              var w1 = attributeArray[i4], w2 = attributeArray[i4 + 1], w3 = attributeArray[i4 + 2], w4 = attributeArray[i4 + 3];
                              var wSum = w1 + w2 + w3 + w4;
                              weightArray[i3] = w1 / wSum;
                              weightArray[i3 + 1] = w2 / wSum;
                              weightArray[i3 + 2] = w3 / wSum;
                          }
                          geometry.attributes[attributeName].value = weightArray;
                      }
                      else if (semantic === 'COLOR_0' && size === 3) {
                          var colorArray = new attributeArray.constructor(attributeInfo.count * 4);
                          for (var i = 0; i < attributeInfo.count; i++) {
                              var i4 = i * 4, i3 = i * 3;
                              colorArray[i4] = attributeArray[i3];
                              colorArray[i4 + 1] = attributeArray[i3 + 1];
                              colorArray[i4 + 2] = attributeArray[i3 + 2];
                              colorArray[i4 + 3] = 1;
                          }
                          geometry.attributes[attributeName].value = colorArray;
                      }
                      else {
                          geometry.attributes[attributeName].value = attributeArray;
                      }
  
                      var attributeType = 'float';
                      if (attributeArray instanceof vendor.Uint16Array) {
                          attributeType = 'ushort';
                      }
                      else if (attributeArray instanceof vendor.Int16Array) {
                          attributeType = 'short';
                      }
                      else if (attributeArray instanceof vendor.Uint8Array) {
                          attributeType = 'ubyte';
                      }
                      else if (attributeArray instanceof vendor.Int8Array) {
                          attributeType = 'byte';
                      }
                      geometry.attributes[attributeName].type = attributeType;
  
                      if (semantic === 'POSITION') {
                          // Bounding Box
                          var min = attributeInfo.min;
                          var max = attributeInfo.max;
                          if (min) {
                              geometry.boundingBox.min.set(min[0], min[1], min[2]);
                          }
                          if (max) {
                              geometry.boundingBox.max.set(max[0], max[1], max[2]);
                          }
                      }
                  }
  
                  // Parse indices
                  if (primitiveInfo.indices != null) {
                      geometry.indices = getAccessorData(json, lib, primitiveInfo.indices, true);
                      if (geometry.vertexCount <= 0xffff && geometry.indices instanceof vendor.Uint32Array) {
                          geometry.indices = new vendor.Uint16Array(geometry.indices);
                      }
                      if(geometry.indices instanceof vendor.Uint8Array) {
                          geometry.indices = new vendor.Uint16Array(geometry.indices);
                      }
                  }
  
                  var material = lib.materials[primitiveInfo.material];
                  var materialInfo = (json.materials || [])[primitiveInfo.material];
                  // Use default material
                  if (!material) {
                      material = new Material({
                          shader: self._getShader()
                      });
                  }
                  var mesh = new Mesh({
                      geometry: geometry,
                      material: material,
                      mode: [Mesh.POINTS, Mesh.LINES, Mesh.LINE_LOOP, Mesh.LINE_STRIP, Mesh.TRIANGLES, Mesh.TRIANGLE_STRIP, Mesh.TRIANGLE_FAN][primitiveInfo.mode] || Mesh.TRIANGLES,
                      ignoreGBuffer: material.transparent
                  });
                  if (materialInfo != null) {
                      mesh.culling = !materialInfo.doubleSided;
                  }
                  if (!mesh.geometry.attributes.normal.value) {
                      mesh.geometry.generateVertexNormals();
                  }
                  if (((material instanceof StandardMaterial) && material.normalMap)
                      || (material.isTextureEnabled('normalMap'))
                  ) {
                      if (!mesh.geometry.attributes.tangent.value) {
                          mesh.geometry.generateTangents();
                      }
                  }
                  if (mesh.geometry.attributes.color.value) {
                      mesh.material.define('VERTEX_COLOR');
                  }
  
                  mesh.name = GLTFLoader.generateMeshName(json.meshes, idx, pp);
  
                  lib.meshes[idx].push(mesh);
              }
          }, this);
      },
  
      _instanceCamera: function (json, nodeInfo) {
          var cameraInfo = json.cameras[nodeInfo.camera];
  
          if (cameraInfo.type === 'perspective') {
              var perspectiveInfo = cameraInfo.perspective || {};
              return new PerspectiveCamera({
                  name: nodeInfo.name,
                  aspect: perspectiveInfo.aspectRatio,
                  fov: perspectiveInfo.yfov / Math.PI * 180,
                  far: perspectiveInfo.zfar,
                  near: perspectiveInfo.znear
              });
          }
          else {
              var orthographicInfo = cameraInfo.orthographic || {};
              return new OrthographicCamera({
                  name: nodeInfo.name,
                  top: orthographicInfo.ymag,
                  right: orthographicInfo.xmag,
                  left: -orthographicInfo.xmag,
                  bottom: -orthographicInfo.ymag,
                  near: orthographicInfo.znear,
                  far: orthographicInfo.zfar
              });
          }
      },
  
      _parseNodes: function (json, lib) {
  
          function instanceMesh(mesh) {
              return new Mesh({
                  name: mesh.name,
                  geometry: mesh.geometry,
                  material: mesh.material,
                  culling: mesh.culling,
                  mode: mesh.mode
              });
          }
  
          lib.instancedMeshes = [];
  
          util.each(json.nodes, function (nodeInfo, idx) {
              var node;
              if (nodeInfo.camera != null && this.includeCamera) {
                  node = this._instanceCamera(json, nodeInfo);
                  lib.cameras.push(node);
              }
              else if (nodeInfo.mesh != null && this.includeMesh) {
                  var primitives = lib.meshes[nodeInfo.mesh];
                  if (primitives) {
                      if (primitives.length === 1) {
                          // Replace the node with mesh directly
                          node = instanceMesh(primitives[0]);
                          node.setName(nodeInfo.name);
                          lib.instancedMeshes.push(node);
                      }
                      else {
                          node = new Node();
                          node.setName(nodeInfo.name);
                          for (var j = 0; j < primitives.length; j++) {
                              var newMesh = instanceMesh(primitives[j]);
                              node.add(newMesh);
                              lib.instancedMeshes.push(newMesh);
                          }
                      }
                  }
              }
              else {
                  node = new Node();
                  // PENDING Dulplicate name.
                  node.setName(nodeInfo.name);
              }
              if (nodeInfo.matrix) {
                  node.localTransform.setArray(nodeInfo.matrix);
                  node.decomposeLocalTransform();
              }
              else {
                  if (nodeInfo.translation) {
                      node.position.setArray(nodeInfo.translation);
                  }
                  if (nodeInfo.rotation) {
                      node.rotation.setArray(nodeInfo.rotation);
                  }
                  if (nodeInfo.scale) {
                      node.scale.setArray(nodeInfo.scale);
                  }
              }
  
              lib.nodes[idx] = node;
          }, this);
  
          // Build hierarchy
          util.each(json.nodes, function (nodeInfo, idx) {
              var node = lib.nodes[idx];
              if (nodeInfo.children) {
                  for (var i = 0; i < nodeInfo.children.length; i++) {
                      var childIdx = nodeInfo.children[i];
                      var child = lib.nodes[childIdx];
                      node.add(child);
                  }
              }
          });
          },
  
      _parseAnimations: function (json, lib) {
          function checkChannelPath(channelInfo) {
              if (channelInfo.path === 'weights') {
                  console.warn('GLTFLoader not support morph targets yet.');
                  return false;
              }
              return true;
          }
  
          function getChannelHash(channelInfo, animationInfo) {
              return channelInfo.target.node + '_' + animationInfo.samplers[channelInfo.sampler].input;
          }
  
          var timeAccessorMultiplied = {};
          util.each(json.animations, function (animationInfo, idx) {
              var channels = animationInfo.channels.filter(checkChannelPath);
  
              if (!channels.length) {
                  return;
              }
              var tracks = {};
              for (var i = 0; i < channels.length; i++) {
                  var channelInfo = channels[i];
                  var channelHash = getChannelHash(channelInfo, animationInfo);
  
                  var targetNode = lib.nodes[channelInfo.target.node];
                  var track = tracks[channelHash];
                  var samplerInfo = animationInfo.samplers[channelInfo.sampler];
  
                  if (!track) {
                      track = tracks[channelHash] = new SamplerTrack({
                          name: targetNode ? targetNode.name : '',
                          target: targetNode
                      });
                      track.targetNodeIndex = channelInfo.target.node;
                      track.channels.time = getAccessorData(json, lib, samplerInfo.input);
                      var frameLen = track.channels.time.length;
                      if (!timeAccessorMultiplied[samplerInfo.input]) {
                          for (var k = 0; k < frameLen; k++) {
                              track.channels.time[k] *= 1000;
                          }
                          timeAccessorMultiplied[samplerInfo.input] = true;
                      }
                  }
  
                  var interpolation = samplerInfo.interpolation || 'LINEAR';
                  if (interpolation !== 'LINEAR') {
                      console.warn('GLTFLoader only support LINEAR interpolation.');
                  }
  
                  var path = channelInfo.target.path;
                  if (path === 'translation') {
                      path = 'position';
                  }
  
                  track.channels[path] = getAccessorData(json, lib, samplerInfo.output);
              }
              var tracksList = [];
              for (var hash in tracks) {
                  tracksList.push(tracks[hash]);
              }
              var clip = new TrackClip({
                  name: animationInfo.name,
                  loop: true,
                  tracks: tracksList
              });
              lib.clips.push(clip);
          }, this);
  
  
          // PENDING
          var maxLife = lib.clips.reduce(function (maxTime, clip) {
              return Math.max(maxTime, clip.life);
          }, 0);
          lib.clips.forEach(function (clip) {
              clip.life = maxLife;
          });
  
          return lib.clips;
      }
  });
  
  GLTFLoader.generateMeshName = function (meshes, idx, primitiveIdx) {
      var meshInfo = meshes[idx];
      var meshName = meshInfo.name || ('mesh_' + idx);
      return primitiveIdx === 0 ? meshName : (meshName + '$' + primitiveIdx);
  };
  
  export default GLTFLoader;
  