MMD Loader Camera Animation fail

I have tried over the last days to implement a working animated camera to my already running MMD / PMX environment.

However I cannot for the life of me figure out how to do it.
The regular animations for model and stage props already work,

But the camera eludes me.

I currently call the camera in a simple manner:

async function LoadCamera(id) {
  const path = "./camera/" + id + ".vmd";
  return new Promise((resolve, reject) => {
    const loader = new THREE.MMDLoader();
    
    loader.loadAnimation(path, camera, (animation) => {
      if (!animation || !animation.animations || animation.animations.length === 0) {
        reject(new Error(`Invalid animation file: ${path}`));
        return;
      }
      
      const mixer = new THREE.AnimationMixer(camera);
      const action = mixer.clipAction(animation.animations[0]);
      action.play();

      function animateCamera() {
        requestAnimationFrame(animateCamera);
        mixer.update(clock.getDelta());
        renderer.render(scene, camera);
      }
      
      animateCamera();
      resolve(true);
    }, (error) => {
      console.error('Error loading animation:', error);
      reject(new Error(`Error loading animation file: ${path}`));
    });
  });
}


However this is seemingly not enough as the console log claims:
Error loading camera animation: ProgressEvent {isTrusted: true, lengthComputable: false, loaded: 0, total: 0, type: ‘error’, …}

Any help would be appreciated

I also have the current - disfunctional - code here, with “camerafail.js” being the current - faily attempt

let scene, renderer, camera, mesh, helper, cameraAnimation;
let ready = false;
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const clock = new THREE.Clock();

function getQueryStringValue(key) {
  const urlParams = new URLSearchParams(window.location.search);
  return urlParams.get(key);
}

const pmx = getQueryStringValue('pmx');
const stage = getQueryStringValue('stage');
let Pmx;
let Stage;

if (pmx) {
  Pmx = `./pmx/pronama/${pmx.trim()}`;
} else {
  console.log("No PMX selected.");
}

if (stage) {
  Stage = `./stages${stage.trim()}`;
} else {
  console.log("No stage selected.");
}

if (!Pmx) {
  Pmx = `./pmx/pronama/AoiZaizen/AoiZaizen.pmx`;
}

if (!Stage) {
  Stage = "./stages/sorclair/sorclair.pmx";
}

const MotionObjects = [
  { id: "001", pose: "001", VmdClip: null, AudioClip: false },
  { id: "002", pose: "002", VmdClip: null, AudioClip: false },
  { id: "003", pose: "003", VmdClip: null, AudioClip: false },
  { id: "004walknthink", pose: "004walknthink", VmdClip: null, AudioClip: false },
  { id: "005hard-carry", pose: "005hard-carry", VmdClip: null, AudioClip: true },
  { id: "006_girlwaving", pose: "008_girlwaving", VmdClip: null, AudioClip: false },
  { id: "007_dance", pose: "007_dance", VmdClip: null, AudioClip: false },
  { id: "008_rusian", pose: "008_rusian", VmdClip: null, AudioClip: false },
  { id: "bts-bestofme", pose: "bts-bestofme", VmdClip: null, AudioClip: true },
  { id: "seniorita", pose: "seniorita", VmdClip: null, AudioClip: true },
  { id: "test", pose: "test", VmdClip: null, AudioClip: false },
  { id: "pekaboo", pose: "pekaboo", VmdClip: null, AudioClip: true },
  { id: "lupin", pose: "lupin", VmdClip: null, AudioClip: true },
  { id: "lp-alightthatnevercomes", pose: "lp-alightthatnevercomes", VmdClip: null, AudioClip: true },
  { id: "halseywithoutme", pose: "halseywithoutme", VmdClip: null, AudioClip: true },
  { id: "pillarmen", pose: "pillarmen", VmdClip: null, AudioClip: true },

];

window.onload = () => {
  Init();
  LoadModeler();
  Render();
};

function Init() {
  document.getElementById("moveLeftButton").addEventListener("click", moveCameraLeft);
  document.getElementById("moveRightButton").addEventListener("click", moveCameraRight);
  document.getElementById("moveUpButton").addEventListener("click", moveCameraUp);
  document.getElementById("moveDownButton").addEventListener("click", moveCameraDown);
  document.getElementById("rotaterightButton").addEventListener("click", rotateCameraRight);
  document.getElementById("rotateleftButton").addEventListener("click", rotateCameraLeft);

  function moveCameraLeft() { camera.position.x -= 1; }
  function moveCameraRight() { camera.position.x += 1; }
  function moveCameraUp() { camera.position.y += 1; }
  function moveCameraDown() { camera.position.y -= 1; }
  function rotateCameraRight() { mesh.rotateY(Math.PI / 4); }
  function rotateCameraLeft() { mesh.rotateY(-Math.PI / 4); }

  scene = new THREE.Scene();
  const ambient = new THREE.AmbientLight(0xeeeeee);
  scene.add(ambient);

  renderer = new THREE.WebGLRenderer({ alpha: true });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setClearColor(0xcccccc, 0);
  document.body.appendChild(renderer.domElement);

  camera = new THREE.PerspectiveCamera(100, windowWidth / windowHeight, 1, 1000);
  camera.position.set(0, 19, 20);
}
async function LoadModeler() {
  const loader = new THREE.MMDLoader();
function LoadPMX() {
  return new Promise(resolve => {
    loader.load(Pmx, object => {
      mesh = object;
      scene.add(mesh);
      resolve(true);
      },);});}

function LoadVMD(id) {
    return new Promise(resolve => {
      const path = "./vmd/" + id + ".vmd";
      const val = MotionObjects.findIndex(MotionObject => MotionObject.id == id);
      loader.loadAnimation(path, mesh, vmd => {
        vmd.name = id;
        MotionObjects[val].VmdClip = vmd;
         //newid = id;
          //console.log(newid);
         document.getElementById('readystate').innerHTML = "Checking motion vmd - " + id + ' (if stuck, click <a href="js/main.js" target="_blank">here</a>)';
          resolve(true);
      },);});}



function LoadCamera(id) {
  let camid;
  if (new URLSearchParams(window.location.search).has('camera')) {
    camid = new URLSearchParams(window.location.search).get('camera');
  } else {
    camid = localStorage.getItem('camid');
    if (!camid) {
      camid = 'bts-bestofme';
    }
  }
  const path = "./camera/" + camid + ".vmd";
  return new Promise((resolve, reject) => {
    loader.loadAnimation(path, camera, cameraAnimation => {
      cameraAnimation.name = path;
      resolve(cameraAnimation);
      console.log(path);
    }, onProgress, (xhr) => reject(new Error(`Failed to load Camera VMD file: ${path}`)));
  });
}

  function LoadAudio(id) {
    return new Promise(resolve => {
      const path = "./audio/" + id + ".mp3";
      const val = MotionObjects.findIndex(MotionObject => MotionObject.id == id);
      if (MotionObjects[val].AudioClip) {
        new THREE.AudioLoader().load(path, buffer => {
          const listener = new THREE.AudioListener();
          const audio = new THREE.Audio(listener).setBuffer(buffer);
          MotionObjects[val].AudioClip = audio;
         document.getElementById('readystate').innerHTML = "Checking audio - " + id + ' (if stuck, click <a href="audio" target="_blank">here</a>)';
          resolve(true);
        },);
      } else {
        resolve(false);}});}
  const loadAdditionalPMX = () => {
    return new Promise(resolve => {
      loader.load(Stage, object => {
        const additionalMesh = object;
        scene.add(additionalMesh);
        resolve(true);
const loadAdditionalPMX = () => {
  return new Promise(resolve => {
    loader.load(Stage, object => {
      const additionalMesh = object;
      scene.add(additionalMesh);
      resolve(true);
    });
  });
};

let path; //preperations for 001.vmd and 002.vmd
try {path = `./stages${stage.trim()}`;} catch (e) {
  path = '/default/default';}
const lastIndex = path.lastIndexOf("/");
const basePath = path.substring(0, lastIndex);
const vmd1 = `${basePath}/001.vmd`;
const vmd2 = `${basePath}/002.vmd`;
console.log(vmd1);
console.log(vmd2);
let mixer;
const url = window.location.href;
const isAnimated = url.indexOf('animated') !== -1;
if (isAnimated) {
function createMixer() {
  mixer = new THREE.AnimationMixer(object);
}
function playAnimation(animationClip) {
  const action = mixer.clipAction(animationClip);
  action.play();
}
loader.loadAnimation(vmd1, object, (animationClip) => {
  createMixer();
  playAnimation(animationClip);
});
loader.loadAnimation(vmd2, object, (animationClip) => {
  playAnimation(animationClip);
});
function animate() {
  requestAnimationFrame(animate);
  const deltaTime = clock.getDelta();
  if (mixer) {
    mixer.update(deltaTime);
  }
  renderer.render(scene, camera);
}
animate();
}


      },);});};
  await Promise.all([loadAdditionalPMX()]);
  await LoadPMX();
  await Promise.all(MotionObjects.map(async (MotionObject) => {return await LoadVMD(MotionObject.id);}));
  await Promise.all(MotionObjects.map(async (MotionObject) => {return await LoadAudio(MotionObject.id);}));
    cameraAnimation = await LoadCamera("camera-animation-id");  // Ensure to load camera animation
  VmdControl("loop", true);}
function VmdControl(id, loop) {
  const index = MotionObjects.findIndex(MotionObject => MotionObject.id == id);
  if (index === -1) {
    console.log("not Found ID");
    //document.getElementById('readystate').textContent = "Default Camera: ready";
    document.getElementById('readystate').textContent = "Camera: ready -  " + localStorage.getItem('camid');
    return;
  }
  ready = false;
  helper = new THREE.MMDAnimationHelper({ afterglow: 2.0, resetPhysicsOnLoop: true });
  const enablePhysics = localStorage.getItem("physicsareon") === "true";

  helper.add(mesh, {
    animation: MotionObjects[index].VmdClip,
    physics: enablePhysics
  });

  if (MotionObjects[index].AudioClip) {
    MotionObjects[index].AudioClip.play();
  }






  if (cameraAnimation) {
    helper.add(camera, {animation: cameraAnimation});
}




  const mixer = helper.objects.get(mesh).mixer;
  if (!loop) {
    mixer.existingAction(MotionObjects[index].VmdClip).setLoop(THREE.LoopOnce);
  }
  mixer.addEventListener("loop", (event) => {
    console.log("loop");
  });
  mixer.addEventListener("finished", (event) => {
    console.log("finished");
    VmdControl("loop", true);
  });
  ready = true;
}
function onProgress(xhr) {
  if (xhr.lengthComputable) {
    const percentComplete = xhr.loaded / xhr.total * 100;
    console.log(Math.round(percentComplete, 2) + '% downloaded');
  }
}
function onError(xhr) {
  console.log("ERROR", xhr);
}
function Render() {
  requestAnimationFrame(Render);
  renderer.clear();
  renderer.render(scene, camera);
  if (ready) {
    helper.update(clock.getDelta());
  }
}

const generatePoseClickEvent = (motionObjects) => {
  const functionBody = motionObjects.map(function (motion) {
    return "case '" + motion.pose + "':\n" +
      "  VmdControl('" + motion.id + "', " + (motion.pose === "pose1") + ");\n" +
      "  break;\n";
  }).join("");
  const poseClickEventFunction = new Function("id", "switch (id) { " + functionBody +
    " default: VmdControl('001', true); break; }");
  return poseClickEventFunction;
};

const PoseClickEvent = generatePoseClickEvent(MotionObjects);
console.log(PoseClickEvent);

This loads a default camera now

@Alcea you should check the official MMD example to see the order of loading, which can be summarized as this:

  • the loadWithAnimation function would be the first to use to load the model and its own animation
    • within this same function, once the model is loaded, then use the second function loadAnimation to load camera
      • the example also loads audio within this second function if you would need that part

All of these are added to the MMDAnimationHelper, which in the example is helper, so there is no need to create any mixer separately.

If you can stick with the example then your files should work fine.

Thanks.

I already found the solution tho

  • See initial post addendum