text: /**
   * @author alteredq / http://alteredqualia.com/
   */
  
  v3d.MD2Character = function() {
  
      var scope = this;
  
      this.scale = 1;
      this.animationFPS = 6;
  
      this.root = new v3d.Object3D();
  
      this.meshBody = null;
      this.meshWeapon = null;
  
      this.skinsBody = [];
      this.skinsWeapon = [];
  
      this.weapons = [];
  
      this.activeAnimation = null;
  
      this.mixer = null;
  
      this.onLoadComplete = function() {};
  
      this.loadCounter = 0;
  
      this.loadParts = function(config) {
  
          this.loadCounter = config.weapons.length * 2 + config.skins.length + 1;
  
          var weaponsTextures = [];
          for (var i = 0; i < config.weapons.length; i++) weaponsTextures[i] = config.weapons[i][1];
          // SKINS
  
          this.skinsBody = loadTextures(config.baseUrl + "skins/", config.skins);
          this.skinsWeapon = loadTextures(config.baseUrl + "skins/", weaponsTextures);
  
          // BODY
  
          var loader = new v3d.MD2Loader();
  
          loader.load(config.baseUrl + config.body, function(geo) {
  
              var boundingBox = new v3d.Box3();
              boundingBox.setFromBufferAttribute(geo.attributes.position);
  
              scope.root.position.y = - scope.scale * boundingBox.min.y;
  
              var mesh = createPart(geo, scope.skinsBody[0]);
              mesh.scale.set(scope.scale, scope.scale, scope.scale);
  
              scope.root.add(mesh);
  
              scope.meshBody = mesh;
  
              scope.meshBody.clipOffset = 0;
              scope.activeAnimationClipName = mesh.geometry.animations[0].name;
  
              scope.mixer = new v3d.AnimationMixer(mesh);
  
              checkLoadingComplete();
  
          });
  
          // WEAPONS
  
          var generateCallback = function(index, name) {
  
              return function(geo) {
  
                  var mesh = createPart(geo, scope.skinsWeapon[index]);
                  mesh.scale.set(scope.scale, scope.scale, scope.scale);
                  mesh.visible = false;
  
                  mesh.name = name;
  
                  scope.root.add(mesh);
  
                  scope.weapons[index] = mesh;
                  scope.meshWeapon = mesh;
  
                  checkLoadingComplete();
  
              };
  
          };
  
          for (var i = 0; i < config.weapons.length; i++) {
  
              loader.load(config.baseUrl + config.weapons[i][0], generateCallback(i, config.weapons[i][0]));
  
          }
  
      };
  
      this.setPlaybackRate = function(rate) {
  
          if (rate !== 0) {
  
              this.mixer.timeScale = 1 / rate;
  
          } else {
  
              this.mixer.timeScale = 0;
  
          }
  
      };
  
      this.setWireframe = function(wireframeEnabled) {
  
          if (wireframeEnabled) {
  
              if (this.meshBody) this.meshBody.material = this.meshBody.materialWireframe;
              if (this.meshWeapon) this.meshWeapon.material = this.meshWeapon.materialWireframe;
  
          } else {
  
              if (this.meshBody) this.meshBody.material = this.meshBody.materialTexture;
              if (this.meshWeapon) this.meshWeapon.material = this.meshWeapon.materialTexture;
  
          }
  
      };
  
      this.setSkin = function(index) {
  
          if (this.meshBody && this.meshBody.material.wireframe === false) {
  
              this.meshBody.material.map = this.skinsBody[index];
  
          }
  
      };
  
      this.setWeapon = function(index) {
  
          for (var i = 0; i < this.weapons.length; i++) this.weapons[i].visible = false;
  
          var activeWeapon = this.weapons[index];
  
          if (activeWeapon) {
  
              activeWeapon.visible = true;
              this.meshWeapon = activeWeapon;
  
              scope.syncWeaponAnimation();
  
          }
  
      };
  
      this.setAnimation = function(clipName) {
  
          if (this.meshBody) {
  
              if (this.meshBody.activeAction) {
  
                  this.meshBody.activeAction.stop();
                  this.meshBody.activeAction = null;
  
              }
  
              var action = this.mixer.clipAction(clipName, this.meshBody);
  
              if (action) {
  
                  this.meshBody.activeAction = action.play();
  
              }
  
          }
  
          scope.activeClipName = clipName;
  
          scope.syncWeaponAnimation();
  
      };
  
      this.syncWeaponAnimation = function() {
  
          var clipName = scope.activeClipName;
  
          if (scope.meshWeapon) {
  
              if (this.meshWeapon.activeAction) {
  
                  this.meshWeapon.activeAction.stop();
                  this.meshWeapon.activeAction = null;
  
              }
  
              var action = this.mixer.clipAction(clipName, this.meshWeapon);
  
              if (action) {
  
                  this.meshWeapon.activeAction = action.syncWith(this.meshBody.activeAction).play();
  
              }
  
          }
  
      };
  
      this.update = function(delta) {
  
          if (this.mixer) this.mixer.update(delta);
  
      };
  
      function loadTextures(baseUrl, textureUrls) {
  
          var textureLoader = new v3d.TextureLoader();
          var textures = [];
  
          for (var i = 0; i < textureUrls.length; i++) {
  
              textures[i] = textureLoader.load(baseUrl + textureUrls[i], checkLoadingComplete);
              textures[i].mapping = v3d.UVMapping;
              textures[i].name = textureUrls[i];
              textures[i].encoding = v3d.sRGBEncoding;
  
          }
  
          return textures;
  
      }
  
      function createPart(geometry, skinMap) {
  
          var materialWireframe = new v3d.MeshLambertMaterial({ color: 0xffaa00, wireframe: true, morphTargets: true, morphNormals: true });
          var materialTexture = new v3d.MeshLambertMaterial({ color: 0xffffff, wireframe: false, map: skinMap, morphTargets: true, morphNormals: true });
  
          //
  
          var mesh = new v3d.Mesh(geometry, materialTexture);
          mesh.rotation.y = - Math.PI / 2;
  
          mesh.castShadow = true;
          mesh.receiveShadow = true;
  
          //
  
          mesh.materialTexture = materialTexture;
          mesh.materialWireframe = materialWireframe;
  
          return mesh;
  
      }
  
      function checkLoadingComplete() {
  
          scope.loadCounter -= 1;
  
          if (scope.loadCounter === 0) scope.onLoadComplete();
  
      }
  
  };
  